Files
dev-intel-poc/mcp_server.py

165 lines
5.6 KiB
Python
Raw Normal View History

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