This commit is contained in:
Colby McHenry
2026-01-18 16:25:00 -06:00
parent 08ccabb5a9
commit cc6e7a5c89
57 changed files with 23315 additions and 1 deletions
+491
View File
@@ -0,0 +1,491 @@
/**
* MCP Tool Definitions
*
* Defines the tools exposed by the CodeGraph MCP server.
*/
import CodeGraph from '../index';
import type { Node, SearchResult, Subgraph, TaskContext, NodeKind } from '../types';
/**
* MCP Tool definition
*/
export interface ToolDefinition {
name: string;
description: string;
inputSchema: {
type: 'object';
properties: Record<string, PropertySchema>;
required?: string[];
};
}
interface PropertySchema {
type: string;
description: string;
enum?: string[];
default?: unknown;
}
/**
* Tool execution result
*/
export interface ToolResult {
content: Array<{
type: 'text';
text: string;
}>;
isError?: boolean;
}
/**
* All CodeGraph MCP tools
*/
export const tools: ToolDefinition[] = [
{
name: 'codegraph_search',
description: 'Search for code symbols (functions, classes, methods) by name or semantic similarity. Returns matching nodes with their locations and signatures.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query - can be a symbol name or natural language description',
},
kind: {
type: 'string',
description: 'Filter by node kind',
enum: ['function', 'method', 'class', 'interface', 'type', 'variable', 'route', 'component'],
},
limit: {
type: 'number',
description: 'Maximum number of results to return (default: 10)',
default: 10,
},
},
required: ['query'],
},
},
{
name: 'codegraph_context',
description: 'Build relevant code context for a task or issue. Finds related symbols and their code, formatted for understanding the codebase.',
inputSchema: {
type: 'object',
properties: {
task: {
type: 'string',
description: 'Description of the task, bug, or feature to build context for',
},
maxNodes: {
type: 'number',
description: 'Maximum number of code symbols to include (default: 20)',
default: 20,
},
includeCode: {
type: 'boolean',
description: 'Include full code snippets (default: true)',
default: true,
},
},
required: ['task'],
},
},
{
name: 'codegraph_callers',
description: 'Find all functions/methods that call a specific symbol. Useful for understanding usage patterns and impact of changes.',
inputSchema: {
type: 'object',
properties: {
symbol: {
type: 'string',
description: 'Name of the function, method, or class to find callers for',
},
limit: {
type: 'number',
description: 'Maximum number of callers to return (default: 20)',
default: 20,
},
},
required: ['symbol'],
},
},
{
name: 'codegraph_callees',
description: 'Find all functions/methods that a specific symbol calls. Useful for understanding dependencies and code flow.',
inputSchema: {
type: 'object',
properties: {
symbol: {
type: 'string',
description: 'Name of the function, method, or class to find callees for',
},
limit: {
type: 'number',
description: 'Maximum number of callees to return (default: 20)',
default: 20,
},
},
required: ['symbol'],
},
},
{
name: 'codegraph_impact',
description: 'Analyze the impact radius of changing a symbol. Shows what code could be affected by modifications.',
inputSchema: {
type: 'object',
properties: {
symbol: {
type: 'string',
description: 'Name of the symbol to analyze impact for',
},
depth: {
type: 'number',
description: 'How many levels of dependencies to traverse (default: 2)',
default: 2,
},
},
required: ['symbol'],
},
},
{
name: 'codegraph_node',
description: 'Get detailed information about a specific code symbol, including its full code.',
inputSchema: {
type: 'object',
properties: {
symbol: {
type: 'string',
description: 'Name of the symbol to get details for',
},
includeCode: {
type: 'boolean',
description: 'Include full source code (default: true)',
default: true,
},
},
required: ['symbol'],
},
},
{
name: 'codegraph_status',
description: 'Get the status of the CodeGraph index, including statistics about indexed files, nodes, and edges.',
inputSchema: {
type: 'object',
properties: {},
},
},
];
/**
* Tool handler that executes tools against a CodeGraph instance
*/
export class ToolHandler {
constructor(private cg: CodeGraph) {}
/**
* Execute a tool by name
*/
async execute(toolName: string, args: Record<string, unknown>): Promise<ToolResult> {
try {
switch (toolName) {
case 'codegraph_search':
return await this.handleSearch(args);
case 'codegraph_context':
return await this.handleContext(args);
case 'codegraph_callers':
return await this.handleCallers(args);
case 'codegraph_callees':
return await this.handleCallees(args);
case 'codegraph_impact':
return await this.handleImpact(args);
case 'codegraph_node':
return await this.handleNode(args);
case 'codegraph_status':
return await this.handleStatus();
default:
return this.errorResult(`Unknown tool: ${toolName}`);
}
} catch (err) {
return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
/**
* Handle codegraph_search
*/
private async handleSearch(args: Record<string, unknown>): Promise<ToolResult> {
const query = args.query as string;
const kind = args.kind as string | undefined;
const limit = (args.limit as number) || 10;
const results = this.cg.searchNodes(query, {
limit,
kinds: kind ? [kind as NodeKind] : undefined,
});
if (results.length === 0) {
return this.textResult(`No results found for "${query}"`);
}
const formatted = this.formatSearchResults(results);
return this.textResult(formatted);
}
/**
* Handle codegraph_context
*/
private async handleContext(args: Record<string, unknown>): Promise<ToolResult> {
const task = args.task as string;
const maxNodes = (args.maxNodes as number) || 20;
const includeCode = args.includeCode !== false;
const context = await this.cg.buildContext(task, {
maxNodes,
includeCode,
format: 'markdown',
});
// buildContext returns string when format is 'markdown'
if (typeof context === 'string') {
return this.textResult(context);
}
// If it returns TaskContext, format it
return this.textResult(this.formatTaskContext(context));
}
/**
* Handle codegraph_callers
*/
private async handleCallers(args: Record<string, unknown>): Promise<ToolResult> {
const symbol = args.symbol as string;
const limit = (args.limit as number) || 20;
// First find the node by name
const results = this.cg.searchNodes(symbol, { limit: 1 });
if (results.length === 0 || !results[0]) {
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
}
const node = results[0].node;
const callers = this.cg.getCallers(node.id);
if (callers.length === 0) {
return this.textResult(`No callers found for "${symbol}"`);
}
// Extract just the nodes from the { node, edge } tuples
const callerNodes = callers.slice(0, limit).map(c => c.node);
const formatted = this.formatNodeList(callerNodes, `Callers of ${symbol}`);
return this.textResult(formatted);
}
/**
* Handle codegraph_callees
*/
private async handleCallees(args: Record<string, unknown>): Promise<ToolResult> {
const symbol = args.symbol as string;
const limit = (args.limit as number) || 20;
// First find the node by name
const results = this.cg.searchNodes(symbol, { limit: 1 });
if (results.length === 0 || !results[0]) {
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
}
const node = results[0].node;
const callees = this.cg.getCallees(node.id);
if (callees.length === 0) {
return this.textResult(`No callees found for "${symbol}"`);
}
// Extract just the nodes from the { node, edge } tuples
const calleeNodes = callees.slice(0, limit).map(c => c.node);
const formatted = this.formatNodeList(calleeNodes, `Callees of ${symbol}`);
return this.textResult(formatted);
}
/**
* Handle codegraph_impact
*/
private async handleImpact(args: Record<string, unknown>): Promise<ToolResult> {
const symbol = args.symbol as string;
const depth = (args.depth as number) || 2;
// First find the node by name
const results = this.cg.searchNodes(symbol, { limit: 1 });
if (results.length === 0 || !results[0]) {
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
}
const node = results[0].node;
const impact = this.cg.getImpactRadius(node.id, depth);
const formatted = this.formatImpact(symbol, impact);
return this.textResult(formatted);
}
/**
* Handle codegraph_node
*/
private async handleNode(args: Record<string, unknown>): Promise<ToolResult> {
const symbol = args.symbol as string;
const includeCode = args.includeCode !== false;
// Find the node by name
const results = this.cg.searchNodes(symbol, { limit: 1 });
if (results.length === 0 || !results[0]) {
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
}
const node = results[0].node;
let code: string | null = null;
if (includeCode) {
code = await this.cg.getCode(node.id);
}
const formatted = this.formatNodeDetails(node, code);
return this.textResult(formatted);
}
/**
* Handle codegraph_status
*/
private async handleStatus(): Promise<ToolResult> {
const stats = this.cg.getStats();
const lines: string[] = [
'## CodeGraph Status',
'',
`**Files indexed:** ${stats.fileCount}`,
`**Total nodes:** ${stats.nodeCount}`,
`**Total edges:** ${stats.edgeCount}`,
`**Database size:** ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`,
'',
'### Nodes by Kind:',
];
for (const [kind, count] of Object.entries(stats.nodesByKind)) {
if ((count as number) > 0) {
lines.push(`- ${kind}: ${count}`);
}
}
lines.push('', '### Languages:');
for (const [lang, count] of Object.entries(stats.filesByLanguage)) {
if ((count as number) > 0) {
lines.push(`- ${lang}: ${count}`);
}
}
return this.textResult(lines.join('\n'));
}
// =========================================================================
// Formatting helpers
// =========================================================================
private formatSearchResults(results: SearchResult[]): string {
const lines: string[] = [`## Search Results (${results.length} found)`, ''];
for (const result of results) {
const { node, score } = result;
const location = node.startLine ? `:${node.startLine}` : '';
lines.push(`### ${node.name} (${node.kind})`);
lines.push(`**Location:** ${node.filePath}${location}`);
lines.push(`**Score:** ${Math.round(score * 100)}%`);
if (node.signature) lines.push(`**Signature:** ${node.signature}`);
lines.push('');
}
return lines.join('\n');
}
private formatNodeList(nodes: Node[], title: string): string {
const lines: string[] = [`## ${title} (${nodes.length} found)`, ''];
for (const node of nodes) {
const location = node.startLine ? `:${node.startLine}` : '';
lines.push(`- **${node.name}** (${node.kind}) - ${node.filePath}${location}`);
}
return lines.join('\n');
}
private formatImpact(symbol: string, impact: Subgraph): string {
const nodeCount = impact.nodes.size;
const edgeCount = impact.edges.length;
const lines: string[] = [
`## Impact Analysis for "${symbol}"`,
'',
`**Nodes affected:** ${nodeCount}`,
`**Relationships:** ${edgeCount}`,
'',
'### Affected Symbols:',
'',
];
// Group by file
const byFile = new Map<string, Node[]>();
for (const node of impact.nodes.values()) {
const existing = byFile.get(node.filePath) || [];
existing.push(node);
byFile.set(node.filePath, existing);
}
for (const [file, nodes] of byFile) {
lines.push(`**${file}:**`);
for (const node of nodes) {
const location = node.startLine ? `:${node.startLine}` : '';
lines.push(` - ${node.name} (${node.kind})${location}`);
}
lines.push('');
}
return lines.join('\n');
}
private formatNodeDetails(node: Node, code: string | null): string {
const location = node.startLine ? `:${node.startLine}` : '';
const lines: string[] = [
`## ${node.name} (${node.kind})`,
'',
`**Location:** ${node.filePath}${location}`,
`**Language:** ${node.language}`,
];
if (node.signature) {
lines.push(`**Signature:** ${node.signature}`);
}
if (node.docstring) {
lines.push('', '### Documentation:', '', node.docstring);
}
if (code) {
lines.push('', '### Code:', '', '```' + node.language, code, '```');
}
return lines.join('\n');
}
private formatTaskContext(context: TaskContext): string {
return context.summary || 'No context found';
}
private textResult(text: string): ToolResult {
return {
content: [{ type: 'text', text }],
};
}
private errorResult(message: string): ToolResult {
return {
content: [{ type: 'text', text: `Error: ${message}` }],
isError: true,
};
}
}