Coverage for gco_mcp / resources / source.py: 100.00%
55 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +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_CONFIG_FILES = {
32 "pyproject.toml": PROJECT_ROOT / "pyproject.toml",
33 "cdk.json": PROJECT_ROOT / "cdk.json",
34 "app.py": PROJECT_ROOT / "app.py",
35 "Dockerfile.dev": PROJECT_ROOT / "Dockerfile.dev",
36 ".pre-commit-config.yaml": PROJECT_ROOT / ".pre-commit-config.yaml",
37 ".dockerignore": PROJECT_ROOT / ".dockerignore",
38 ".gitignore": PROJECT_ROOT / ".gitignore",
39 ".semgrepignore": PROJECT_ROOT / ".semgrepignore",
40 ".yamllint.yml": _GITHUB_CONFIG_DIR / ".yamllint.yml",
41 ".checkov.yaml": _GITHUB_CONFIG_DIR / ".checkov.yaml",
42 ".kics.yaml": _GITHUB_CONFIG_DIR / ".kics.yaml",
43 ".gitleaks.toml": _GITHUB_CONFIG_DIR / ".gitleaks.toml",
44}
47def _list_source_files(base: Path) -> list[Path]:
48 """Walk a directory and return all source files, skipping noise."""
49 files = []
50 for p in sorted(base.rglob("*")):
51 if any(skip in p.parts for skip in _SKIP_DIRS):
52 continue
53 if p.is_file() and p.suffix in _SOURCE_EXTENSIONS:
54 files.append(p)
55 return files
58@mcp.resource("source://gco/index")
59def source_index() -> str:
60 """List all source code files available for reading, grouped by package."""
61 sections = ["# GCO Source Code Index\n"]
62 sections.append("## Project Config")
63 for name, path in sorted(_CONFIG_FILES.items()):
64 if path.is_file():
65 sections.append(f"- `source://gco/config/{name}`")
66 for pkg, base in _SOURCE_DIRS.items():
67 if not base.is_dir():
68 continue
69 files = _list_source_files(base)
70 if not files:
71 continue
72 sections.append(f"\n## {pkg}/ ({len(files)} files)")
73 for f in files:
74 rel = f.relative_to(PROJECT_ROOT)
75 sections.append(f"- `source://gco/file/{rel}`")
76 return "\n".join(sections)
79@mcp.resource("source://gco/config/{filename}")
80def config_file_resource(filename: str) -> str:
81 """Read a top-level project config file (pyproject.toml, cdk.json, etc.)."""
82 if filename not in _CONFIG_FILES:
83 return f"Not available. Allowed: {', '.join(sorted(_CONFIG_FILES))}"
84 path = _CONFIG_FILES[filename]
85 if not path.is_file():
86 return f"File '{filename}' not found."
87 return path.read_text()
90@mcp.resource("source://gco/file/{filepath*}")
91def source_file_resource(filepath: str) -> str:
92 """Read a source file confined beneath the project root."""
93 root = PROJECT_ROOT.resolve()
94 path = (root / filepath).resolve()
95 if not path.is_relative_to(root):
96 return "Access denied: path is outside the project."
97 if any(skip in path.parts for skip in _SKIP_DIRS):
98 return "Access denied: path is in a skipped directory."
99 if not path.is_file():
100 return f"File '{filepath}' not found."
101 if path.suffix not in _SOURCE_EXTENSIONS:
102 return f"File type '{path.suffix}' not served. Allowed: {', '.join(_SOURCE_EXTENSIONS)}"
103 return path.read_text()