Initial POC: Developer Intelligence knowledge graph
- SQLite backend with file/repo/relationship entities - tree-sitter Go AST parser for deterministic import detection - Ollama doc generation with concurrent batch processing - MCP server (FastMCP) for Claude Code integration - Merge simulation with staleness cascade - Lazy refresh for stale relationship and repo docs - CLAUDE.md for agent context
This commit is contained in:
154
mcp_server.py
Normal file
154
mcp_server.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""MCP server for Developer Intelligence POC. Queries SQLite, serves to Claude Code."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
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")
|
||||
Reference in New Issue
Block a user