"""MCP server for Developer Intelligence POC. Queries SQLite, serves to Claude Code.""" import os import sys import json from pathlib import Path # Load .env if present _env_file = Path(__file__).parent / ".env" if _env_file.exists(): for line in _env_file.read_text().splitlines(): line = line.strip() if line and not line.startswith("#") and "=" in line: key, _, val = line.partition("=") os.environ.setdefault(key.strip(), val.strip()) sys.path.insert(0, os.path.dirname(__file__)) from db import GraphDB from mcp.server.fastmcp import FastMCP mcp = FastMCP("Developer Intelligence") def _get_db(): return GraphDB() @mcp.tool() def get_file_doc(path: str) -> str: """Get the generated documentation for a source file. Pass the relative file path (e.g. 'echo.go' or 'middleware/compress.go').""" db = _get_db() f = db.get_file(path) db.close() if not f: return f"File not found: {path}" staleness = " [STALE]" if f["staleness"] == "stale" else "" prev = "" if f.get("prev_documentation"): prev = f"\n\n--- Previous version ---\n{f['prev_documentation']}" return f"{f['documentation']}{staleness}\n\n(commit: {f['last_commit']}, updated: {f['updated_at']}){prev}" @mcp.tool() def get_relationship(file_a: str, file_b: str) -> str: """Get the documentation for the import relationship between two files.""" db = _get_db() rel = db.get_relationship(file_a, file_b) db.close() if not rel: return f"No relationship found between {file_a} and {file_b}" doc = rel["documentation"] or "(no relationship documentation generated yet)" staleness = " [STALE]" if rel["staleness"] == "stale" else "" return f"{doc}{staleness}" @mcp.tool() def get_repo_overview() -> str: """Get the repo-level documentation summary — a high-level overview of the entire project.""" db = _get_db() repo = db.get_repo() db.close() if not repo: return "No repo found" staleness = " [STALE]" if repo["staleness"] == "stale" else "" return f"# {repo['name']}{staleness}\n\n{repo['documentation']}" @mcp.tool() def get_dependents(path: str) -> str: """Get all files that import/depend on the given file. Shows what breaks if you change this file.""" db = _get_db() deps = db.get_dependents(path) db.close() if not deps: return f"No files depend on {path}" lines = [f"Files that depend on {path} ({len(deps)} total):\n"] for d in deps: staleness = " [STALE]" if d["rel_staleness"] == "stale" else "" doc = d["rel_doc"] or "(no relationship doc)" lines.append(f" {d['from_file']}{staleness}") lines.append(f" Relationship: {doc}") lines.append(f" File: {d['file_doc'][:150]}...") return "\n".join(lines) @mcp.tool() def get_dependencies(path: str) -> str: """Get all files that the given file imports/depends on.""" db = _get_db() deps = db.get_dependencies(path) db.close() if not deps: return f"{path} has no tracked dependencies" lines = [f"Dependencies of {path} ({len(deps)} total):\n"] for d in deps: staleness = " [STALE]" if d["rel_staleness"] == "stale" else "" doc = d["rel_doc"] or "(no relationship doc)" lines.append(f" {d['to_file']}{staleness}") lines.append(f" Relationship: {doc}") return "\n".join(lines) @mcp.tool() def search_docs(query: str) -> str: """Search across all file documentation by keyword. Use to find files related to a concept (e.g. 'routing', 'middleware', 'authentication').""" db = _get_db() results = db.search_docs(query) db.close() if not results: return f"No files found matching '{query}'" lines = [f"Files matching '{query}' ({len(results)} results):\n"] for r in results: staleness = " [STALE]" if r["staleness"] == "stale" else "" doc = r["documentation"][:200] + "..." if len(r["documentation"]) > 200 else r["documentation"] lines.append(f" {r['path']}{staleness}: {doc}") return "\n".join(lines) @mcp.tool() def get_stale_docs() -> str: """List all entities and relationships with stale (outdated) documentation.""" db = _get_db() stale_rels = db.get_stale_relationships() stale_repos = db.get_stale_repos() stats = db.get_stats() db.close() lines = ["Stale documentation:\n"] if stale_repos: lines.append(f" Repos ({len(stale_repos)}):") for r in stale_repos: lines.append(f" {r['name']}") lines.append(f" Files: {stats['stale_files']} stale") if stale_rels: lines.append(f" Relationships ({len(stale_rels)}):") for r in stale_rels[:20]: # Cap output lines.append(f" {r['from_file']} -> {r['to_file']}") if len(stale_rels) > 20: lines.append(f" ... and {len(stale_rels) - 20} more") if stats["stale_files"] == 0 and stats["stale_relationships"] == 0: lines.append(" Everything is fresh!") return "\n".join(lines) @mcp.tool() def get_graph_stats() -> str: """Get overall knowledge graph statistics — file count, relationship count, staleness.""" db = _get_db() stats = db.get_stats() repo = db.get_repo() db.close() return json.dumps({ "repo": repo["name"] if repo else None, "files": stats["files"], "relationships": stats["relationships"], "stale_files": stats["stale_files"], "stale_relationships": stats["stale_relationships"], }, indent=2) if __name__ == "__main__": print("Starting Developer Intelligence MCP Server (stdio)...") mcp.run(transport="stdio")