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:
Jarvis Prime
2026-03-04 04:25:14 +00:00
commit af2e54b5f3
9 changed files with 889 additions and 0 deletions

154
mcp_server.py Normal file
View 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")