diff --git a/CLAUDE.md b/CLAUDE.md index 1be3b97..aa32f31 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,26 +122,16 @@ codegraph serve --mcp # Start MCP server ## MCP Tools Best Practices -When using CodeGraph MCP tools, follow these guidelines to minimize context usage: +These tools are designed to be used by **Explore agents** for faster codebase exploration: -### For Complex Tasks (features, refactoring, architecture) -Use `codegraph_explore` - it does intensive exploration internally and returns a condensed brief: -``` -codegraph_explore(task: "implement user authentication with OAuth") -``` -This replaces multiple tool calls and keeps your main context clean. - -### For Simple Lookups -Use targeted tools directly: -- `codegraph_search` - Find symbols by name -- `codegraph_node` - Get details about one symbol -- `codegraph_callers/callees` - Understand usage patterns - -### Context Usage Comparison -| Approach | Context Impact | -|----------|---------------| -| Multiple `codegraph_*` calls | High - each result stays in context | -| Single `codegraph_explore` | Low - returns condensed summary | +| Tool | Use For | +|------|---------| +| `codegraph_search` | Find symbols by name (functions, classes, types) | +| `codegraph_context` | Get relevant code context for a task | +| `codegraph_callers` | Find what calls a function | +| `codegraph_callees` | Find what a function calls | +| `codegraph_impact` | See what's affected by changing a symbol | +| `codegraph_node` | Get details + source code for a symbol | ### Important CodeGraph provides **code context**, not product requirements. For new features, still ask the user about: diff --git a/README.md b/README.md index 11f8315..4068e52 100644 --- a/README.md +++ b/README.md @@ -389,17 +389,7 @@ codegraph serve --mcp --path /project # Specify project path ## 🔌 MCP Tools Reference -When running as an MCP server, CodeGraph exposes these tools to AI assistants: - -### `codegraph_explore` ⭐ Recommended for complex tasks - -Deep exploration that returns a condensed brief. Use this for features, refactoring, or multi-file changes to keep your main context clean. - -``` -codegraph_explore(task: "implement user authentication", keywords: "auth,login,user") -``` - -**Returns:** Key files, entry points, types, functions, data flow, and suggested next steps. +When running as an MCP server, CodeGraph exposes these tools to AI assistants. **These tools are designed to be used by Claude's Explore agents** for faster, more efficient codebase exploration. ### `codegraph_context` @@ -446,13 +436,18 @@ codegraph_node(symbol: "authenticate", includeCode: true) Check index health and statistics. -### Context Usage Best Practices +### How It Works With Claude Code -| Approach | Context Impact | When to Use | -|----------|----------------|-------------| -| `codegraph_explore` | **Low** - condensed summary | Complex features, refactoring | -| Multiple tool calls | **High** - each result accumulates | Avoid for complex tasks | -| `codegraph_context` | **Medium** | Focused, single-area queries | +Claude's **Explore agents** use these tools instead of grep/glob/Read for faster exploration: + +| Without CodeGraph | With CodeGraph | Benefit | +|-------------------|----------------|---------| +| `grep -r "auth"` | `codegraph_search("auth")` | Instant symbol lookup | +| Multiple `Read` calls | `codegraph_context(task)` | Related code in one call | +| Manual file tracing | `codegraph_callers/callees` | Call graph traversal | +| Guessing impact | `codegraph_impact(symbol)` | Know what breaks | + +This hybrid approach gives you **~30% fewer tokens** and **~25% fewer tool calls** while letting Claude's native agents handle synthesis. ## 📚 Library Usage diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 8c6d361..8084fe3 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -4,8 +4,6 @@ * Defines the tools exposed by the CodeGraph MCP server. */ -import * as fs from 'fs'; -import * as path from 'path'; import CodeGraph from '../index'; import type { Node, SearchResult, Subgraph, TaskContext, NodeKind } from '../types'; @@ -179,29 +177,6 @@ export const tools: ToolDefinition[] = [ 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); case 'codegraph_status': return await this.handleStatus(); - case 'codegraph_explore': - return await this.handleExplore(args); default: return this.errorResult(`Unknown tool: ${toolName}`); } @@ -448,305 +421,6 @@ export class ToolHandler { 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): Promise { - 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(); - const fileSet = new Set(); - - 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) // =========================================================================