|
@@ -4,8 +4,6 @@
|
|
|
* Defines the tools exposed by the CodeGraph MCP server.
|
|
* Defines the tools exposed by the CodeGraph MCP server.
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
-import * as fs from 'fs';
|
|
|
|
|
-import * as path from 'path';
|
|
|
|
|
import CodeGraph from '../index';
|
|
import CodeGraph from '../index';
|
|
|
import type { Node, SearchResult, Subgraph, TaskContext, NodeKind } from '../types';
|
|
import type { Node, SearchResult, Subgraph, TaskContext, NodeKind } from '../types';
|
|
|
|
|
|
|
@@ -179,29 +177,6 @@ export const tools: ToolDefinition[] = [
|
|
|
properties: {},
|
|
properties: {},
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
- {
|
|
|
|
|
- name: 'codegraph_explore',
|
|
|
|
|
- description: 'RECOMMENDED FOR COMPLEX TASKS: Deep exploration that READS CODE INTERNALLY and returns synthesis with key snippets. You do NOT need to make separate Read calls - the code is included in the response. Checks for existing implementations, traces data flow, and includes relevant code. For feature requests, use AskUserQuestion to clarify requirements BEFORE planning.',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- task: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Detailed description of the feature, bug, or task to explore',
|
|
|
|
|
- },
|
|
|
|
|
- focus: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Optional focus area: "architecture" (structure & patterns), "implementation" (specific code), or "impact" (what would change). Default: auto-detect.',
|
|
|
|
|
- enum: ['architecture', 'implementation', 'impact'],
|
|
|
|
|
- },
|
|
|
|
|
- keywords: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Optional comma-separated keywords to search for (e.g., "bundle,swap,subscription")',
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- required: ['task'],
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -230,8 +205,6 @@ export class ToolHandler {
|
|
|
return await this.handleNode(args);
|
|
return await this.handleNode(args);
|
|
|
case 'codegraph_status':
|
|
case 'codegraph_status':
|
|
|
return await this.handleStatus();
|
|
return await this.handleStatus();
|
|
|
- case 'codegraph_explore':
|
|
|
|
|
- return await this.handleExplore(args);
|
|
|
|
|
default:
|
|
default:
|
|
|
return this.errorResult(`Unknown tool: ${toolName}`);
|
|
return this.errorResult(`Unknown tool: ${toolName}`);
|
|
|
}
|
|
}
|
|
@@ -448,305 +421,6 @@ export class ToolHandler {
|
|
|
return this.textResult(lines.join('\n'));
|
|
return this.textResult(lines.join('\n'));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * Handle codegraph_explore - deep exploration with internal file reading
|
|
|
|
|
- * Returns synthesized context so Claude doesn't need separate Read calls
|
|
|
|
|
- */
|
|
|
|
|
- private async handleExplore(args: Record<string, unknown>): Promise<ToolResult> {
|
|
|
|
|
- const task = args.task as string;
|
|
|
|
|
- const keywordsArg = args.keywords as string | undefined;
|
|
|
|
|
- const projectRoot = this.cg.getProjectRoot();
|
|
|
|
|
-
|
|
|
|
|
- // Phase 1: Extract search terms
|
|
|
|
|
- const keywords = this.extractKeywords(task, keywordsArg);
|
|
|
|
|
-
|
|
|
|
|
- // Phase 2: Find relevant symbols
|
|
|
|
|
- const symbolMap = new Map<string, Node>();
|
|
|
|
|
- const fileSet = new Set<string>();
|
|
|
|
|
-
|
|
|
|
|
- for (const keyword of keywords.slice(0, 5)) {
|
|
|
|
|
- const results = this.cg.searchNodes(keyword, { limit: 10 });
|
|
|
|
|
- for (const r of results) {
|
|
|
|
|
- if (!symbolMap.has(r.node.id)) {
|
|
|
|
|
- symbolMap.set(r.node.id, r.node);
|
|
|
|
|
- fileSet.add(r.node.filePath);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Phase 3: Look for EXISTING implementations
|
|
|
|
|
- const existingImplNodes: Node[] = [];
|
|
|
|
|
- const searchPatterns = this.generateExistingPatternSearches(keywords);
|
|
|
|
|
-
|
|
|
|
|
- for (const pattern of searchPatterns) {
|
|
|
|
|
- const results = this.cg.searchNodes(pattern, { limit: 5 });
|
|
|
|
|
- for (const r of results) {
|
|
|
|
|
- const node = r.node;
|
|
|
|
|
- if (['function', 'method', 'component'].includes(node.kind)) {
|
|
|
|
|
- if (!symbolMap.has(node.id)) {
|
|
|
|
|
- symbolMap.set(node.id, node);
|
|
|
|
|
- fileSet.add(node.filePath);
|
|
|
|
|
- }
|
|
|
|
|
- existingImplNodes.push(node);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Phase 4: Categorize symbols by type
|
|
|
|
|
- const allSymbols = Array.from(symbolMap.values());
|
|
|
|
|
- const functions = allSymbols.filter(n => n.kind === 'function' || n.kind === 'method');
|
|
|
|
|
- const components = allSymbols.filter(n => n.kind === 'component');
|
|
|
|
|
- const types = allSymbols.filter(n => n.kind === 'interface' || n.kind === 'type_alias');
|
|
|
|
|
- const apiRoutes = allSymbols.filter(n => n.filePath.includes('/api/') || n.kind === 'route');
|
|
|
|
|
-
|
|
|
|
|
- // Phase 5: READ CODE INTERNALLY for key symbols (the sub-agent behavior)
|
|
|
|
|
- // Prioritize: existing implementations > API routes > types > functions
|
|
|
|
|
- const codeSnippets: Array<{ node: Node; code: string }> = [];
|
|
|
|
|
- const maxSnippets = 8;
|
|
|
|
|
- const maxCodeLength = 1500; // chars per snippet
|
|
|
|
|
-
|
|
|
|
|
- const priorityNodes = [
|
|
|
|
|
- ...existingImplNodes.slice(0, 3),
|
|
|
|
|
- ...apiRoutes.slice(0, 2),
|
|
|
|
|
- ...types.slice(0, 2),
|
|
|
|
|
- ...components.slice(0, 2),
|
|
|
|
|
- ...functions.filter(n => !existingImplNodes.includes(n)).slice(0, 1),
|
|
|
|
|
- ];
|
|
|
|
|
-
|
|
|
|
|
- for (const node of priorityNodes) {
|
|
|
|
|
- if (codeSnippets.length >= maxSnippets) break;
|
|
|
|
|
-
|
|
|
|
|
- const code = this.extractNodeCode(projectRoot, node, maxCodeLength);
|
|
|
|
|
- if (code) {
|
|
|
|
|
- codeSnippets.push({ node, code });
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Phase 6: Trace call graphs for data flow understanding
|
|
|
|
|
- const dataFlowInsights: string[] = [];
|
|
|
|
|
- for (const node of existingImplNodes.slice(0, 3)) {
|
|
|
|
|
- const callers = this.cg.getCallers(node.id);
|
|
|
|
|
- const callees = this.cg.getCallees(node.id);
|
|
|
|
|
-
|
|
|
|
|
- if (callers.length > 0 || callees.length > 0) {
|
|
|
|
|
- let flow = `${node.name}`;
|
|
|
|
|
- if (callers.length > 0) flow = `${callers.slice(0, 2).map(c => c.node.name).join(', ')} → ${flow}`;
|
|
|
|
|
- if (callees.length > 0) flow = `${flow} → ${callees.slice(0, 2).map(c => c.node.name).join(', ')}`;
|
|
|
|
|
- dataFlowInsights.push(flow);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Phase 7: Build comprehensive synthesis
|
|
|
|
|
- const isFeatureQuery = this.looksLikeFeatureRequest(task);
|
|
|
|
|
- const hasExistingImpl = existingImplNodes.length > 0;
|
|
|
|
|
-
|
|
|
|
|
- const synthesis = this.buildExploreSynthesis({
|
|
|
|
|
- task,
|
|
|
|
|
- existingImplNodes,
|
|
|
|
|
- codeSnippets,
|
|
|
|
|
- types,
|
|
|
|
|
- apiRoutes,
|
|
|
|
|
- dataFlowInsights,
|
|
|
|
|
- totalSymbols: symbolMap.size,
|
|
|
|
|
- totalFiles: fileSet.size,
|
|
|
|
|
- isFeatureQuery,
|
|
|
|
|
- hasExistingImpl,
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- return this.textResult(synthesis);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Extract code from a node's source file
|
|
|
|
|
- */
|
|
|
|
|
- private extractNodeCode(projectRoot: string, node: Node, maxLength: number): string | null {
|
|
|
|
|
- const filePath = path.join(projectRoot, node.filePath);
|
|
|
|
|
-
|
|
|
|
|
- if (!fs.existsSync(filePath)) {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- const content = fs.readFileSync(filePath, 'utf-8');
|
|
|
|
|
- const lines = content.split('\n');
|
|
|
|
|
-
|
|
|
|
|
- const startIdx = Math.max(0, node.startLine - 1);
|
|
|
|
|
- const endIdx = Math.min(lines.length, node.endLine);
|
|
|
|
|
-
|
|
|
|
|
- let code = lines.slice(startIdx, endIdx).join('\n');
|
|
|
|
|
-
|
|
|
|
|
- // Truncate if too long
|
|
|
|
|
- if (code.length > maxLength) {
|
|
|
|
|
- code = code.slice(0, maxLength) + '\n// ... truncated ...';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return code;
|
|
|
|
|
- } catch {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Build comprehensive synthesis with code included
|
|
|
|
|
- */
|
|
|
|
|
- private buildExploreSynthesis(data: {
|
|
|
|
|
- task: string;
|
|
|
|
|
- existingImplNodes: Node[];
|
|
|
|
|
- codeSnippets: Array<{ node: Node; code: string }>;
|
|
|
|
|
- types: Node[];
|
|
|
|
|
- apiRoutes: Node[];
|
|
|
|
|
- dataFlowInsights: string[];
|
|
|
|
|
- totalSymbols: number;
|
|
|
|
|
- totalFiles: number;
|
|
|
|
|
- isFeatureQuery: boolean;
|
|
|
|
|
- hasExistingImpl: boolean;
|
|
|
|
|
- }): string {
|
|
|
|
|
- const lines: string[] = [];
|
|
|
|
|
-
|
|
|
|
|
- // Critical warnings at TOP
|
|
|
|
|
- if (data.hasExistingImpl) {
|
|
|
|
|
- lines.push('⚠️ **EXISTING IMPLEMENTATIONS FOUND** - Review code below before planning. Feature may already exist.');
|
|
|
|
|
- lines.push('');
|
|
|
|
|
- }
|
|
|
|
|
- if (data.isFeatureQuery) {
|
|
|
|
|
- lines.push('📋 **BEFORE PLANNING:** Use `AskUserQuestion` to clarify UX preferences and requirements.');
|
|
|
|
|
- lines.push('');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Summary stats
|
|
|
|
|
- lines.push(`**Explored ${data.totalSymbols} symbols across ${data.totalFiles} files**`);
|
|
|
|
|
- lines.push('');
|
|
|
|
|
-
|
|
|
|
|
- // Data flow (if available)
|
|
|
|
|
- if (data.dataFlowInsights.length > 0) {
|
|
|
|
|
- lines.push('**Data Flow:**');
|
|
|
|
|
- for (const flow of data.dataFlowInsights.slice(0, 3)) {
|
|
|
|
|
- lines.push(` ${flow}`);
|
|
|
|
|
- }
|
|
|
|
|
- lines.push('');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Key types (signatures only, no code)
|
|
|
|
|
- if (data.types.length > 0) {
|
|
|
|
|
- lines.push('**Key Types:**');
|
|
|
|
|
- for (const t of data.types.slice(0, 4)) {
|
|
|
|
|
- lines.push(` - \`${t.name}\` (${t.filePath}:${t.startLine})`);
|
|
|
|
|
- }
|
|
|
|
|
- lines.push('');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // API routes (signatures only)
|
|
|
|
|
- if (data.apiRoutes.length > 0) {
|
|
|
|
|
- lines.push('**API Routes:**');
|
|
|
|
|
- for (const r of data.apiRoutes.slice(0, 3)) {
|
|
|
|
|
- const routePath = r.filePath.split('/api/')[1] || r.filePath;
|
|
|
|
|
- lines.push(` - \`/api/${routePath}\` (${r.name})`);
|
|
|
|
|
- }
|
|
|
|
|
- lines.push('');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // CODE SNIPPETS - the key sub-agent behavior
|
|
|
|
|
- if (data.codeSnippets.length > 0) {
|
|
|
|
|
- lines.push('---');
|
|
|
|
|
- lines.push('**Key Code (read internally):**');
|
|
|
|
|
- lines.push('');
|
|
|
|
|
-
|
|
|
|
|
- for (const { node, code } of data.codeSnippets) {
|
|
|
|
|
- lines.push(`### ${node.name} (${node.kind}) - ${node.filePath}:${node.startLine}`);
|
|
|
|
|
- lines.push('```' + (node.language || 'typescript'));
|
|
|
|
|
- lines.push(code);
|
|
|
|
|
- lines.push('```');
|
|
|
|
|
- lines.push('');
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return lines.join('\n');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Generate search patterns to find existing implementations
|
|
|
|
|
- * Language-agnostic: generates patterns for multiple naming conventions
|
|
|
|
|
- */
|
|
|
|
|
- private generateExistingPatternSearches(keywords: string[]): string[] {
|
|
|
|
|
- const patterns: string[] = [];
|
|
|
|
|
-
|
|
|
|
|
- for (const keyword of keywords.slice(0, 3)) {
|
|
|
|
|
- // Skip very short or common words
|
|
|
|
|
- if (keyword.length < 3) continue;
|
|
|
|
|
-
|
|
|
|
|
- const lower = keyword.toLowerCase();
|
|
|
|
|
- const capitalized = keyword.charAt(0).toUpperCase() + keyword.slice(1).toLowerCase();
|
|
|
|
|
-
|
|
|
|
|
- // The keyword itself in various cases
|
|
|
|
|
- patterns.push(lower);
|
|
|
|
|
- patterns.push(capitalized);
|
|
|
|
|
-
|
|
|
|
|
- // Common suffixes (work across most languages)
|
|
|
|
|
- // These patterns find: SwapService, swap_service, SwapHandler, etc.
|
|
|
|
|
- const suffixes = ['Service', 'Handler', 'Controller', 'Manager', 'Helper', 'Util', 'Utils'];
|
|
|
|
|
- for (const suffix of suffixes) {
|
|
|
|
|
- patterns.push(`${capitalized}${suffix}`); // PascalCase: SwapService
|
|
|
|
|
- patterns.push(`${lower}_${suffix.toLowerCase()}`); // snake_case: swap_service
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Common prefixes (work across most languages)
|
|
|
|
|
- const prefixes = ['handle', 'process', 'do', 'execute', 'perform', 'run'];
|
|
|
|
|
- for (const prefix of prefixes) {
|
|
|
|
|
- patterns.push(`${prefix}_${lower}`); // snake_case: handle_swap
|
|
|
|
|
- patterns.push(`${prefix}${capitalized}`); // camelCase: handleSwap
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Common action patterns
|
|
|
|
|
- patterns.push(`create_${lower}`);
|
|
|
|
|
- patterns.push(`update_${lower}`);
|
|
|
|
|
- patterns.push(`delete_${lower}`);
|
|
|
|
|
- patterns.push(`get_${lower}`);
|
|
|
|
|
- patterns.push(`create${capitalized}`);
|
|
|
|
|
- patterns.push(`update${capitalized}`);
|
|
|
|
|
- patterns.push(`delete${capitalized}`);
|
|
|
|
|
- patterns.push(`get${capitalized}`);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return [...new Set(patterns)];
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Extract keywords from task description
|
|
|
|
|
- */
|
|
|
|
|
- private extractKeywords(task: string, explicitKeywords?: string): string[] {
|
|
|
|
|
- const keywords: string[] = [];
|
|
|
|
|
-
|
|
|
|
|
- // Add explicit keywords first
|
|
|
|
|
- if (explicitKeywords) {
|
|
|
|
|
- keywords.push(...explicitKeywords.split(',').map(k => k.trim()).filter(Boolean));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Extract likely code identifiers from task (camelCase, PascalCase, snake_case)
|
|
|
|
|
- const identifierPattern = /\b([A-Z][a-zA-Z0-9]*|[a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*|[a-z]+_[a-z_]+)\b/g;
|
|
|
|
|
- const matches = task.match(identifierPattern) || [];
|
|
|
|
|
- keywords.push(...matches);
|
|
|
|
|
-
|
|
|
|
|
- // Extract quoted terms
|
|
|
|
|
- const quotedPattern = /"([^"]+)"|'([^']+)'/g;
|
|
|
|
|
- let match;
|
|
|
|
|
- while ((match = quotedPattern.exec(task)) !== null) {
|
|
|
|
|
- const quoted = match[1] || match[2];
|
|
|
|
|
- if (quoted) keywords.push(quoted);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Extract domain-specific terms (nouns that might be code concepts)
|
|
|
|
|
- const commonTerms = task.toLowerCase()
|
|
|
|
|
- .split(/\s+/)
|
|
|
|
|
- .filter(word =>
|
|
|
|
|
- word.length > 3 &&
|
|
|
|
|
- !['this', 'that', 'with', 'from', 'have', 'been', 'will', 'would', 'could', 'should', 'when', 'where', 'what', 'which', 'their', 'there', 'these', 'those', 'about', 'into', 'then', 'than', 'some', 'other', 'after', 'before'].includes(word)
|
|
|
|
|
- );
|
|
|
|
|
- keywords.push(...commonTerms);
|
|
|
|
|
-
|
|
|
|
|
- // Deduplicate and return
|
|
|
|
|
- return [...new Set(keywords)];
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
// =========================================================================
|
|
// =========================================================================
|
|
|
// Formatting helpers (compact by default to reduce context usage)
|
|
// Formatting helpers (compact by default to reduce context usage)
|
|
|
// =========================================================================
|
|
// =========================================================================
|