Files
codegraph/src/mcp/index.ts
T
Colby McHenryandClaude Opus 4.5 6b672f9152 Add evaluation framework and fix call graph extraction
- Add evaluation test suite with TypeScript and Python fixtures
- Fix MCP server to defer CodeGraph init until rootUri received
- Fix call edge extraction by calling resolveReferences() after indexAll/sync
- Fix glob matching for root-level files (e.g., **/*.py now matches auth.py)
- Fix duplicate node extraction for methods inside classes
- Update context tests to use buildContext for semantic search + graph traversal
- Export unused formatter functions to fix build

Evaluation results:
- TypeScript: 96% precision, 79% recall, 85% F1
- Python: 99% precision, 80% recall, 85% F1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 18:48:22 -06:00

250 lines
6.4 KiB
TypeScript

/**
* CodeGraph MCP Server
*
* Model Context Protocol server that exposes CodeGraph functionality
* as tools for AI assistants like Claude.
*
* @module mcp
*
* @example
* ```typescript
* import { MCPServer } from 'codegraph';
*
* const server = new MCPServer('/path/to/project');
* await server.start();
* ```
*/
import CodeGraph from '../index';
import { StdioTransport, JsonRpcRequest, JsonRpcNotification, ErrorCodes } from './transport';
import { tools, ToolHandler } from './tools';
/**
* MCP Server Info
*/
const SERVER_INFO = {
name: 'codegraph',
version: '0.1.0',
};
/**
* MCP Protocol Version
*/
const PROTOCOL_VERSION = '2024-11-05';
/**
* MCP Server for CodeGraph
*
* Implements the Model Context Protocol to expose CodeGraph
* functionality as tools that can be called by AI assistants.
*/
export class MCPServer {
private transport: StdioTransport;
private cg: CodeGraph | null = null;
private toolHandler: ToolHandler | null = null;
private projectPath: string | null;
private initError: string | null = null;
constructor(projectPath?: string) {
this.projectPath = projectPath || null;
this.transport = new StdioTransport();
}
/**
* Start the MCP server
*
* Note: CodeGraph initialization is deferred until the initialize request
* is received, which includes the rootUri from the client.
*/
async start(): Promise<void> {
// Start listening for messages immediately - don't check initialization yet
// We'll get the project path from the initialize request's rootUri
this.transport.start(this.handleMessage.bind(this));
// Keep the process running
process.on('SIGINT', () => this.stop());
process.on('SIGTERM', () => this.stop());
}
/**
* Initialize CodeGraph for the project
*/
private async initializeCodeGraph(projectPath: string): Promise<void> {
this.projectPath = projectPath;
if (!CodeGraph.isInitialized(projectPath)) {
this.initError = `CodeGraph not initialized in ${projectPath}. Run 'codegraph init' first.`;
return;
}
try {
this.cg = await CodeGraph.open(projectPath);
this.toolHandler = new ToolHandler(this.cg);
this.initError = null;
} catch (err) {
this.initError = `Failed to open CodeGraph: ${err instanceof Error ? err.message : String(err)}`;
}
}
/**
* Stop the server
*/
stop(): void {
if (this.cg) {
this.cg.close();
this.cg = null;
}
this.transport.stop();
process.exit(0);
}
/**
* Handle incoming JSON-RPC messages
*/
private async handleMessage(message: JsonRpcRequest | JsonRpcNotification): Promise<void> {
// Check if it's a request (has id) or notification (no id)
const isRequest = 'id' in message;
switch (message.method) {
case 'initialize':
if (isRequest) {
await this.handleInitialize(message as JsonRpcRequest);
}
break;
case 'initialized':
// Notification that client has finished initialization
// No action needed - the client is ready
break;
case 'tools/list':
if (isRequest) {
await this.handleToolsList(message as JsonRpcRequest);
}
break;
case 'tools/call':
if (isRequest) {
await this.handleToolsCall(message as JsonRpcRequest);
}
break;
case 'ping':
if (isRequest) {
this.transport.sendResult((message as JsonRpcRequest).id, {});
}
break;
default:
if (isRequest) {
this.transport.sendError(
(message as JsonRpcRequest).id,
ErrorCodes.MethodNotFound,
`Method not found: ${message.method}`
);
}
}
}
/**
* Handle initialize request
*/
private async handleInitialize(request: JsonRpcRequest): Promise<void> {
const params = request.params as {
rootUri?: string;
workspaceFolders?: Array<{ uri: string; name: string }>;
} | undefined;
// Extract project path from rootUri or workspaceFolders
let projectPath = this.projectPath;
if (params?.rootUri) {
// Convert file:// URI to path
projectPath = params.rootUri.replace(/^file:\/\//, '');
} else if (params?.workspaceFolders?.[0]?.uri) {
projectPath = params.workspaceFolders[0].uri.replace(/^file:\/\//, '');
}
// Fall back to current working directory if no path provided
if (!projectPath) {
projectPath = process.cwd();
}
// Initialize CodeGraph if we have a project path
await this.initializeCodeGraph(projectPath);
// We accept the client's protocol version but respond with our supported version
this.transport.sendResult(request.id, {
protocolVersion: PROTOCOL_VERSION,
capabilities: {
tools: {},
},
serverInfo: SERVER_INFO,
});
}
/**
* Handle tools/list request
*/
private async handleToolsList(request: JsonRpcRequest): Promise<void> {
this.transport.sendResult(request.id, {
tools: tools,
});
}
/**
* Handle tools/call request
*/
private async handleToolsCall(request: JsonRpcRequest): Promise<void> {
const params = request.params as {
name: string;
arguments?: Record<string, unknown>;
};
if (!params || !params.name) {
this.transport.sendError(
request.id,
ErrorCodes.InvalidParams,
'Missing tool name'
);
return;
}
const toolName = params.name;
const toolArgs = params.arguments || {};
// Validate tool exists
const tool = tools.find(t => t.name === toolName);
if (!tool) {
this.transport.sendError(
request.id,
ErrorCodes.InvalidParams,
`Unknown tool: ${toolName}`
);
return;
}
// Execute the tool
if (!this.toolHandler) {
const errorMsg = this.initError ||
(this.projectPath
? `CodeGraph not initialized in ${this.projectPath}. Run 'codegraph init' first.`
: 'No project path provided. Ensure Claude Code is running in a project directory.');
this.transport.sendError(
request.id,
ErrorCodes.InternalError,
errorMsg
);
return;
}
const result = await this.toolHandler.execute(toolName, toolArgs);
this.transport.sendResult(request.id, result);
}
}
// Export for use in CLI
export { StdioTransport } from './transport';
export { tools, ToolHandler } from './tools';