ctured lookup tools to any compliant AI agent.
Architecture Decisions & Rationale
- Tree-sitter for Syntax Parsing: Tree-sitter provides incremental, error-tolerant AST generation across 150+ languages. Unlike regex or naive AST builders, it handles malformed files gracefully and supports fast re-parsing on file changes.
- Hybrid LSP Type Resolution: Pure syntax parsing misses polymorphic calls, interface implementations, and module re-exports. Integrating an LSP client layer refines call edges with actual type information, reducing false positives in dependency tracing.
- SQLite for Edge Storage: Relational storage optimizes for fast JOIN operations when tracing transitive dependencies. It also supports incremental updates without rebuilding the entire graph.
- MCP Tool Exposure: Standardizing on MCP ensures agent-agnostic compatibility. Tools are named with explicit namespaces (
graph:query_callers, graph:analyze_blast_radius) to prevent collision when multiple intelligence servers run concurrently.
Implementation
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { CodeGraphIndex } from "./graph-index.js";
import { LspTypeResolver } from "./lsp-resolver.js";
// Core graph client interface
interface GraphQueryResult {
nodes: DependencyNode[];
edges: ConnectionEdge[];
metadata: QueryMetadata;
}
interface DependencyNode {
id: string;
kind: "function" | "class" | "interface" | "module";
path: string;
span: { start: number; end: number };
}
interface ConnectionEdge {
source: string;
target: string;
type: "calls" | "imports" | "implements" | "extends";
confidence: number;
}
interface QueryMetadata {
token_estimate: number;
resolution_depth: number;
fallback_used: boolean;
}
// MCP Server initialization
const server = new McpServer({
name: "local-code-graph",
version: "1.0.0",
});
const graphIndex = new CodeGraphIndex({
storagePath: ".graph-index/sqlite",
maxDepth: 4,
enableLspRefinement: true,
});
// Tool: Resolve direct and transitive callers
server.tool(
"graph:resolve_callers",
"Returns all functions/methods that directly or transitively call the target symbol.",
{
symbol: z.string().describe("Fully qualified symbol name (e.g., UserService.create)"),
depth: z.number().min(1).max(6).default(3).describe("Maximum traversal depth"),
includeTests: z.boolean().default(false).describe("Include test file references"),
},
async ({ symbol, depth, includeTests }) => {
const result = await graphIndex.queryCallers(symbol, { depth, includeTests });
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
metadata: {
token_estimate: Math.ceil(result.nodes.length * 45),
resolution_depth: depth,
fallback_used: false,
},
};
}
);
// Tool: Analyze blast radius for changed files
server.tool(
"graph:analyze_blast_radius",
"Traces downstream dependencies and affected test suites for a set of modified files.",
{
changedFiles: z.array(z.string()).describe("Relative paths of modified files"),
threshold: z.number().min(0.1).max(1.0).default(0.3).describe("Minimum confidence to include edge"),
},
async ({ changedFiles, threshold }) => {
const radius = await graphIndex.computeBlastRadius(changedFiles, threshold);
return {
content: [{ type: "text", text: JSON.stringify(radius, null, 2) }],
metadata: {
token_estimate: Math.ceil(radius.edges.length * 30),
resolution_depth: radius.maxDepth,
fallback_used: radius.nodes.length > 50,
},
};
}
);
// Tool: Cross-service dependency mapping
server.tool(
"graph:map_service_links",
"Identifies HTTP, gRPC, GraphQL, or pub-sub call sites across microservices.",
{
protocol: z.enum(["http", "grpc", "graphql", "pubsub"]).default("http"),
targetService: z.string().optional().describe("Filter by specific service name"),
},
async ({ protocol, targetService }) => {
const links = await graphIndex.queryCrossService(protocol, targetService);
return {
content: [{ type: "text", text: JSON.stringify(links, null, 2) }],
metadata: {
token_estimate: Math.ceil(links.connections.length * 60),
resolution_depth: 1,
fallback_used: false,
},
};
}
);
// Transport setup
const transport = new StdioTransport();
await server.connect(transport);
The implementation prioritizes deterministic output over generative flexibility. Each tool returns structured JSON with explicit token estimates, allowing the agent to budget context before committing to a query. The fallback_used flag signals when the graph exceeds a safe threshold, prompting the agent to switch to raw file reads or chunked retrieval. LSP integration runs asynchronously during indexing, ensuring syntax parsing remains fast while type resolution refines edges in the background.
Pitfall Guide
Explanation: Querying a graph for a single-line change in an isolated utility function often returns more metadata tokens than simply reading the file. The graph traversal logic adds structural overhead that negates efficiency gains on trivial diffs.
Fix: Implement a size-aware fallback. If changedFiles.length <= 2 and totalLinesChanged < 15, bypass the graph and stream raw files. Reserve graph queries for multi-file or cross-module changes.
2. Ignoring Type Resolution for Polymorphic Code
Explanation: Pure Tree-sitter parsing captures syntax but misses interface implementations, abstract class overrides, and dynamic imports. Agents receive incomplete call graphs, leading to missed dependencies during refactoring.
Fix: Integrate an LSP client or type-inference layer during indexing. For TypeScript/JSX, resolve implements and extends chains. For Python, track Protocol and ABC inheritance. Store type confidence scores alongside edges.
Explanation: Running multiple code intelligence servers (e.g., PR-focused + multi-modal) overwrites shared storage paths or collides on MCP tool names. Agents receive mixed signals or stale indices.
Fix: Namespace all MCP tools with explicit prefixes (pr-graph:, multi-graph:). Isolate storage directories per tool. Use environment variables to route agents to specific graph backends based on task type.
4. Non-Code API Leakage
Explanation: Multi-modal graph tools often route PDFs, meeting transcripts, and architecture docs through cloud LLM APIs for semantic extraction. This violates data residency policies and introduces unpredictable latency.
Fix: Disable non-code ingestion by default. If semantic extraction is required, configure a local Ollama or vLLM endpoint. Audit network calls during indexing to ensure zero outbound traffic for source code.
5. Stale Index Drift
Explanation: Graphs become outdated after merges, branch switches, or dependency updates. Agents query stale edges, missing newly introduced call sites or deprecated modules.
Fix: Hook into pre-commit or CI pipelines. Trigger incremental re-indexing on package.json, go.mod, or requirements.txt changes. Use file hash comparison to skip unchanged modules during rebuilds.
6. Windows Compatibility Gaps
Explanation: High-performance graph backends compiled in C or Rust often lack native Windows binaries. Developers relying on WSL2 face path translation issues and slower I/O during indexing.
Fix: Use containerized MCP runners (Docker/Podman) with volume mounts for consistent path resolution. Alternatively, pre-compile Windows targets using zig cc or mingw-w64 to avoid WSL2 overhead.
7. Unbounded Traversal Depth
Explanation: Agents request deep transitive queries without depth limits, causing exponential edge expansion. The graph returns thousands of nodes, exhausting context windows and degrading query latency.
Fix: Enforce server-side depth caps (max 4-5). Return pagination tokens for large result sets. Train agents to request breadth-first summaries before drilling into specific branches.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small TypeScript/React monorepo with frequent PRs | PR-Focused Graph | Blast-radius analysis and auto-config hooks optimize review loops | Low (Python runtime, SQLite storage) |
| Polyglot enterprise with 50k+ files | High-Performance Graph | Sub-millisecond queries, LSP type resolution, zero dependencies | Medium (C binary, higher initial indexing CPU) |
| Cross-artifact knowledge (code + RFCs + meeting notes) | Multi-Modal Graph | Unified graph for code, docs, and transcripts with community clustering | Variable (LLM API costs for non-code extraction) |
| Strict data residency / air-gapped environment | High-Performance Graph | Fully local parsing, SLSA L3 attestation, no cloud routing | Low (static binary, no external dependencies) |
Configuration Template
{
"mcpServers": {
"local-code-graph": {
"command": "node",
"args": ["./dist/mcp-server.js"],
"env": {
"GRAPH_STORAGE_PATH": ".graph-index/sqlite",
"MAX_TRAVERSAL_DEPTH": "4",
"ENABLE_LSP_REFINEMENT": "true",
"FALLBACK_TOKEN_THRESHOLD": "12000",
"LOG_LEVEL": "warn"
},
"tools": [
"graph:resolve_callers",
"graph:analyze_blast_radius",
"graph:map_service_links"
],
"fallback": {
"enabled": true,
"strategy": "raw_file_read",
"conditions": {
"max_changed_files": 2,
"max_lines_changed": 15
}
}
}
}
}
Quick Start Guide
- Initialize the graph backend: Run the indexing command in your repository root. The parser will traverse all supported languages, build the AST, and populate the local storage layer.
- Configure your AI agent: Add the MCP server configuration to your agent's settings file. Ensure the tool namespace matches your agent's routing expectations.
- Verify connectivity: Open a terminal and send a test query through your agent. Confirm that the graph returns structured JSON with token estimates and no fallback flags.
- Hook into your workflow: Replace grep/glob patterns in your review or refactoring prompts with explicit graph tool calls. Monitor context consumption and adjust depth thresholds as needed.