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
« 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."""
3from pathlib import Path
5from cli_runner import PROJECT_ROOT # runtime-resolved checkout root (uvx-safe)
6from server import mcp
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"}
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}
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
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)
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()
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()