Coverage for gco_mcp/resources/source.py: 87.06%

55 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-30 21:22 +0000

1"""Source code resources (source:// scheme) for the GCO MCP server.""" 

2 

3from pathlib import Path 

4 

5from cli_runner import PROJECT_ROOT # runtime-resolved checkout root (uvx-safe) 

6from server import mcp 

7 

8_SOURCE_DIRS = { 

9 "gco": PROJECT_ROOT / "gco", 

10 "cli": PROJECT_ROOT / "cli", 

11 "lambda": PROJECT_ROOT / "lambda", 

12 "gco_mcp": PROJECT_ROOT / "gco_mcp", 

13 "scripts": PROJECT_ROOT / "scripts", 

14 "demo": PROJECT_ROOT / "demo", 

15 "dockerfiles": PROJECT_ROOT / "dockerfiles", 

16} 

17_SKIP_DIRS = { 

18 "__pycache__", 

19 ".git", 

20 "cdk.out", 

21 "node_modules", 

22 "kubectl-applier-simple-build", 

23 "helm-installer-build", 

24} 

25_SOURCE_EXTENSIONS = {".py", ".yaml", ".yml", ".json", ".txt", ".toml", ".cfg", ".sh", ".md"} 

26 

27# Config files exposed via the source://gco/config/<name> URI. The logical name 

28# (the key) is kept stable even though several files now live under .github/, so 

29# existing references to these URIs keep resolving. 

30_GITHUB_CONFIG_DIR = PROJECT_ROOT / ".github" / "config" 

31_GITHUB_LEGACY_DIR = PROJECT_ROOT / ".github" / "legacy" 

32_CONFIG_FILES = { 

33 "pyproject.toml": PROJECT_ROOT / "pyproject.toml", 

34 "cdk.json": PROJECT_ROOT / "cdk.json", 

35 "app.py": PROJECT_ROOT / "app.py", 

36 "Dockerfile.dev": PROJECT_ROOT / "Dockerfile.dev", 

37 ".pre-commit-config.yaml": PROJECT_ROOT / ".pre-commit-config.yaml", 

38 ".dockerignore": PROJECT_ROOT / ".dockerignore", 

39 ".gitignore": PROJECT_ROOT / ".gitignore", 

40 ".semgrepignore": PROJECT_ROOT / ".semgrepignore", 

41 ".gitlab-ci.yml": _GITHUB_LEGACY_DIR / ".gitlab-ci.yml", 

42 ".yamllint.yml": _GITHUB_CONFIG_DIR / ".yamllint.yml", 

43 ".checkov.yaml": _GITHUB_CONFIG_DIR / ".checkov.yaml", 

44 ".kics.yaml": _GITHUB_CONFIG_DIR / ".kics.yaml", 

45 ".gitleaks.toml": _GITHUB_CONFIG_DIR / ".gitleaks.toml", 

46} 

47 

48 

49def _list_source_files(base: Path) -> list[Path]: 

50 """Walk a directory and return all source files, skipping noise.""" 

51 files = [] 

52 for p in sorted(base.rglob("*")): 

53 if any(skip in p.parts for skip in _SKIP_DIRS): 

54 continue 

55 if p.is_file() and p.suffix in _SOURCE_EXTENSIONS: 

56 files.append(p) 

57 return files 

58 

59 

60@mcp.resource("source://gco/index") 

61def source_index() -> str: 

62 """List all source code files available for reading, grouped by package.""" 

63 sections = ["# GCO Source Code Index\n"] 

64 sections.append("## Project Config") 

65 for name, path in sorted(_CONFIG_FILES.items()): 

66 if path.is_file(): 66 ↛ 65line 66 didn't jump to line 65 because the condition on line 66 was always true

67 sections.append(f"- `source://gco/config/{name}`") 

68 for pkg, base in _SOURCE_DIRS.items(): 

69 if not base.is_dir(): 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true

70 continue 

71 files = _list_source_files(base) 

72 if not files: 72 ↛ 73line 72 didn't jump to line 73 because the condition on line 72 was never true

73 continue 

74 sections.append(f"\n## {pkg}/ ({len(files)} files)") 

75 for f in files: 

76 rel = f.relative_to(PROJECT_ROOT) 

77 sections.append(f"- `source://gco/file/{rel}`") 

78 return "\n".join(sections) 

79 

80 

81@mcp.resource("source://gco/config/{filename}") 

82def config_file_resource(filename: str) -> str: 

83 """Read a top-level project config file (pyproject.toml, cdk.json, etc.).""" 

84 if filename not in _CONFIG_FILES: 

85 return f"Not available. Allowed: {', '.join(sorted(_CONFIG_FILES))}" 

86 path = _CONFIG_FILES[filename] 

87 if not path.is_file(): 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true

88 return f"File '{filename}' not found." 

89 return path.read_text() 

90 

91 

92@mcp.resource("source://gco/file/{filepath*}") 

93def source_file_resource(filepath: str) -> str: 

94 """Read any source file by its path relative to the project root.""" 

95 path = (PROJECT_ROOT / filepath).resolve() 

96 if not str(path).startswith(str(PROJECT_ROOT.resolve())): 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true

97 return "Access denied: path is outside the project." 

98 if any(skip in path.parts for skip in _SKIP_DIRS): 

99 return "Access denied: path is in a skipped directory." 

100 if not path.is_file(): 

101 return f"File '{filepath}' not found." 

102 if path.suffix not in _SOURCE_EXTENSIONS: 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true

103 return f"File type '{path.suffix}' not served. Allowed: {', '.join(_SOURCE_EXTENSIONS)}" 

104 return path.read_text()