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

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_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} 

45 

46 

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 

56 

57 

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) 

77 

78 

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() 

88 

89 

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()