From 7a9b99783a52a684f7cf0f3d1a6308980b6fb5b6 Mon Sep 17 00:00:00 2001 From: andreinknv Date: Mon, 27 Apr 2026 17:01:17 -0400 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20per-tool=20MCP=20registry=20?= =?UTF-8?q?=E2=80=94=20eliminate=20tools[]=20+=20case-switch=20conflicts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Today every PR adding an MCP tool conflicts on the same two shared lists in src/mcp/tools.ts: the tools[] array (the list_tools surface) and the case switch in execute(). After this refactor: Adding a new MCP tool: 1. Drop a file at src/mcp/tools/.ts exporting a _TOOL: ToolModule (definition + handlerKey). 2. Add one import line and one array entry to src/mcp/tools/registry.ts. 3. Implement handle(args) on ToolHandler in tools.ts and add the new key to HandlerKey in tools/types.ts. Step 3 is the only remaining "shared method on a single class" conflict surface. Extracting handler bodies into per-tool files (making step 3 also a single-file addition) is left as a follow-up — the cost/benefit favors landing this incremental win now and finishing the body extraction once language and migration refactors land. ## What's new - **src/mcp/tool-types.ts** — extracted ToolDefinition, ToolResult, PropertySchema, projectPathProperty into a shared module so per-tool files can import without circular dependency. - **src/mcp/tools/types.ts** — ToolModule interface, HandlerKey string union, and ToolHandlerLike (a structural type that ToolHandler now `implements`, providing compile-time guarantee that every HandlerKey maps to a real method). - **src/mcp/tools/.ts × 9** — one file per existing tool (callees, callers, context, explore, files, impact, node, search, status). Each ~25-30 lines: import + definition literal + handlerKey reference. - **src/mcp/tools/registry.ts** — static-import barrel, sorted alphabetically. Exports getToolModules(), getToolModule(name), and the derived `tools[]` array. - **src/mcp/tools.ts** — ~200 lines deleted from the top (inline types + tools[] array + projectPathProperty). execute()'s case-switch replaced with a registry lookup + type-safe `this[mod.handlerKey](args)` dispatch (now compile- time-checked thanks to `implements ToolHandlerLike`). All `private async handle*` methods now public to match the interface. errorResult/textResult also public for the same reason. - **src/mcp/index.ts** — MCPServer's tool-existence check switched from a linear `tools.find()` scan to the O(1) `getToolModule()` Map lookup, eliminating two parallel lookup paths. ## Tests 387/387 pass. **7 new tests** in __tests__/mcp-tool-registry.test.ts: - Definitions are well-formed (name shape, description length). - handlerKey shape (`handle`). - Every registered handlerKey resolves to a real method on ToolHandler. - Exported `tools[]` exactly mirrors the registry. - Canonical 9 main-line tools regression guard. - execute() unknown-tool error path. - **End-to-end dispatch smoke test**: execute('codegraph_status', {}) reaches the real handler body (no broken `this` binding) — would fail loudly if the dynamic dispatch chain ever breaks. ## Reviewer pass Independent reviewer ran once. 2 REQUEST_CHANGES + 2 INFO addressed: 1. ToolHandlerLike was defined but never enforced — ToolHandler now `implements ToolHandlerLike`. Eliminates the `(this as unknown as Record<...>)` cast in execute(); dispatch is fully compile-time-checked. 2. No end-to-end dispatch test — added one (see Tests above). 3. MCPServer.handleToolsCall used a linear `tools.find()` scan while execute() used Map lookup — switched to getToolModule() for parity. 4. Removed redundant .slice() in registry.ts (map() already returns a fresh array). ## Backward compat src/mcp/tools.ts still re-exports ToolDefinition, ToolResult, the mutable `tools[]` array, ToolHandler, and getExploreBudget. Every existing consumer (`import { ToolDefinition, ToolResult, tools, ToolHandler } from './tools'`) keeps working unchanged. ## Affected open PRs - #110 (review-context): rebases to 1 new file in tools/ + 2 lines in registry.ts + 1 method on ToolHandler + 1 line in HandlerKey. - #112 (centrality+churn): same shape for the codegraph_hotspots tool. - #114 (config-refs): same shape for codegraph_config. - #115 (sql-refs): same shape for codegraph_sql. Each goes from 4-way conflict (tools[] + case + handler + helpers) down to 1-way conflict (HandlerKey + handler method on ToolHandler, both in tools.ts). Co-Authored-By: Claude Opus 4.7 (1M context) --- __tests__/mcp-tool-registry.test.ts | 79 +++++++ src/mcp/index.ts | 8 +- src/mcp/tool-types.ts | 39 ++++ src/mcp/tools.ts | 323 ++++------------------------ src/mcp/tools/callees.ts | 27 +++ src/mcp/tools/callers.ts | 27 +++ src/mcp/tools/context.ts | 32 +++ src/mcp/tools/explore.ts | 28 +++ src/mcp/tools/files.ts | 40 ++++ src/mcp/tools/impact.ts | 27 +++ src/mcp/tools/node.ts | 27 +++ src/mcp/tools/registry.ts | 65 ++++++ src/mcp/tools/search.ts | 32 +++ src/mcp/tools/status.ts | 17 ++ src/mcp/tools/types.ts | 50 +++++ 15 files changed, 541 insertions(+), 280 deletions(-) create mode 100644 __tests__/mcp-tool-registry.test.ts create mode 100644 src/mcp/tool-types.ts create mode 100644 src/mcp/tools/callees.ts create mode 100644 src/mcp/tools/callers.ts create mode 100644 src/mcp/tools/context.ts create mode 100644 src/mcp/tools/explore.ts create mode 100644 src/mcp/tools/files.ts create mode 100644 src/mcp/tools/impact.ts create mode 100644 src/mcp/tools/node.ts create mode 100644 src/mcp/tools/registry.ts create mode 100644 src/mcp/tools/search.ts create mode 100644 src/mcp/tools/status.ts create mode 100644 src/mcp/tools/types.ts diff --git a/__tests__/mcp-tool-registry.test.ts b/__tests__/mcp-tool-registry.test.ts new file mode 100644 index 00000000..6ca9cef8 --- /dev/null +++ b/__tests__/mcp-tool-registry.test.ts @@ -0,0 +1,79 @@ +/** + * MCP tool registry: structural invariants. + * + * Guards against the failure mode where a future PR adds a + * ToolModule but forgets to implement the matching `handle` + * method on ToolHandler (or vice versa). + */ +import { describe, it, expect } from 'vitest'; +import { getToolModules, tools as registryTools } from '../src/mcp/tools/registry'; +import { ToolHandler, tools } from '../src/mcp/tools'; + +describe('MCP tool registry — single source of truth', () => { + it('every tool module has a non-empty name and description', () => { + for (const m of getToolModules()) { + expect(m.definition.name).toMatch(/^codegraph_[a-z_]+$/); + expect(m.definition.description.length).toBeGreaterThan(20); + } + }); + + it('handlerKey is a string starting with "handle"', () => { + for (const m of getToolModules()) { + expect(m.handlerKey).toMatch(/^handle[A-Z][A-Za-z]+$/); + } + }); + + it('every registered tool has a corresponding ToolHandler method', () => { + const handler = new ToolHandler(null); + for (const m of getToolModules()) { + const fn = (handler as unknown as Record)[m.handlerKey]; + expect(typeof fn).toBe('function'); + } + }); + + it('exported `tools` array exactly mirrors the registry', () => { + const fromRegistry = registryTools.map((t) => t.name).sort(); + const fromExport = tools.map((t) => t.name).sort(); + expect(fromExport).toEqual(fromRegistry); + }); + + it('all 9 main-line tools are registered (regression guard)', () => { + const expected = [ + 'codegraph_callees', + 'codegraph_callers', + 'codegraph_context', + 'codegraph_explore', + 'codegraph_files', + 'codegraph_impact', + 'codegraph_node', + 'codegraph_search', + 'codegraph_status', + ]; + const actual = getToolModules() + .map((m) => m.definition.name) + .sort(); + expect(actual).toEqual(expected); + }); + + it('execute() reports unknown-tool errors', async () => { + const handler = new ToolHandler(null); + const result = await handler.execute('codegraph_does_not_exist', {}); + expect(result.isError).toBe(true); + expect(result.content[0]?.text).toMatch(/Unknown tool/); + }); + + it('execute() actually dispatches to the registered handler (no broken `this` binding)', async () => { + // No CodeGraph instance is bound, so handlers that call + // `getCodeGraph()` will throw — the dispatch should catch it + // and return an error result. The point of this test is to + // confirm the registry lookup + `this[handlerKey](args)` chain + // reaches an actual method body, not that the body succeeds. + const handler = new ToolHandler(null); + const result = await handler.execute('codegraph_status', {}); + expect(result.isError).toBe(true); + // Generic tool-execution-failed envelope from execute()'s catch block. + expect(result.content[0]?.text).toMatch(/Tool execution failed/); + // Specifically because no CodeGraph was bound: + expect(result.content[0]?.text).toMatch(/CodeGraph not initialized/); + }); +}); diff --git a/src/mcp/index.ts b/src/mcp/index.ts index bc3552ae..c31284a8 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -18,7 +18,8 @@ import * as path from 'path'; import CodeGraph, { findNearestCodeGraphRoot } from '../index'; import { StdioTransport, JsonRpcRequest, JsonRpcNotification, ErrorCodes } from './transport'; -import { tools, ToolHandler } from './tools'; +import { ToolHandler } from './tools'; +import { getToolModule } from './tools/registry'; /** * Convert a file:// URI to a filesystem path. @@ -309,8 +310,9 @@ export class MCPServer { const toolName = params.name; const toolArgs = params.arguments || {}; - // Validate tool exists - const tool = tools.find(t => t.name === toolName); + // Validate tool exists — O(1) Map lookup against the registry, + // matches the path `ToolHandler.execute()` uses internally. + const tool = getToolModule(toolName)?.definition; if (!tool) { this.transport.sendError( request.id, diff --git a/src/mcp/tool-types.ts b/src/mcp/tool-types.ts new file mode 100644 index 00000000..90e94fe8 --- /dev/null +++ b/src/mcp/tool-types.ts @@ -0,0 +1,39 @@ +/** + * Shared MCP tool types. + * + * Lives in its own module so per-tool files in `./tools/` and + * the legacy class wrapper in `./tools.ts` can import the same + * type definitions without a circular dependency. + */ + +export interface PropertySchema { + type: string; + description: string; + enum?: string[]; + default?: unknown; +} + +export interface ToolDefinition { + name: string; + description: string; + inputSchema: { + type: 'object'; + properties: Record; + required?: string[]; + }; +} + +export interface ToolResult { + content: Array<{ type: 'text'; text: string }>; + isError?: boolean; +} + +/** + * Shared `projectPath` schema property — every tool's inputSchema + * accepts it for cross-project queries. + */ +export const projectPathProperty: PropertySchema = { + type: 'string', + description: + 'Path to a different project with .codegraph/ initialized. If omitted, uses current project. Use this to query other codebases.', +}; diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 53713145..7a5b995a 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -11,6 +11,25 @@ import { writeFileSync, readFileSync, existsSync } from 'fs'; import { clamp, validatePathWithinRoot } from '../utils'; import { tmpdir } from 'os'; import { join } from 'path'; +import type { ToolDefinition, ToolResult } from './tool-types'; +import type { ToolHandlerLike } from './tools/types'; +import { getToolModule, tools as registryTools } from './tools/registry'; + +// Re-export shared types so existing consumers (`import { ToolDefinition, +// ToolResult } from './tools'`) keep working unchanged. +export type { ToolDefinition, ToolResult } from './tool-types'; + +/** + * The MCP `list_tools` array, derived from the per-tool registry + * (`./tools/.ts`). Adding a new tool no longer touches this + * array — drop a file in `./tools/` and add it to + * `./tools/registry.ts`. + * + * Typed as a mutable array (matching the original export shape) + * even though the underlying registry produces a readonly value; + * we slice() to materialize a fresh, mutable copy at module load. + */ +export const tools: ToolDefinition[] = registryTools.slice(); /** Maximum output length to prevent context bloat (characters) */ const MAX_OUTPUT_LENGTH = 15000; @@ -42,248 +61,6 @@ function markSessionConsulted(sessionId: string): void { } } -/** - * MCP Tool definition - */ -export interface ToolDefinition { - name: string; - description: string; - inputSchema: { - type: 'object'; - properties: Record; - 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; -} - -/** - * Common projectPath property for cross-project queries - */ -const projectPathProperty: PropertySchema = { - type: 'string', - description: 'Path to a different project with .codegraph/ initialized. If omitted, uses current project. Use this to query other codebases.', -}; - -/** - * All CodeGraph MCP tools - * - * Designed for minimal context usage - use codegraph_context as the primary tool, - * and only use other tools for targeted follow-up queries. - * - * All tools support cross-project queries via the optional `projectPath` parameter. - */ -export const tools: ToolDefinition[] = [ - { - name: 'codegraph_search', - description: 'Quick symbol search by name. Returns locations only (no code). Use codegraph_context instead for comprehensive task context.', - inputSchema: { - type: 'object', - properties: { - query: { - type: 'string', - description: 'Symbol name or partial name (e.g., "auth", "signIn", "UserService")', - }, - kind: { - type: 'string', - description: 'Filter by node kind', - enum: ['function', 'method', 'class', 'interface', 'type', 'variable', 'route', 'component'], - }, - limit: { - type: 'number', - description: 'Maximum results (default: 10)', - default: 10, - }, - projectPath: projectPathProperty, - }, - required: ['query'], - }, - }, - { - name: 'codegraph_context', - description: 'PRIMARY TOOL: Build comprehensive context for a task. Returns entry points, related symbols, and key code - often enough to understand the codebase without additional tool calls. NOTE: This provides CODE context, not product requirements. For new features, still clarify UX/behavior questions with the user before implementing.', - inputSchema: { - type: 'object', - properties: { - task: { - type: 'string', - description: 'Description of the task, bug, or feature to build context for', - }, - maxNodes: { - type: 'number', - description: 'Maximum symbols to include (default: 20)', - default: 20, - }, - includeCode: { - type: 'boolean', - description: 'Include code snippets for key symbols (default: true)', - default: true, - }, - projectPath: projectPathProperty, - }, - 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, - }, - projectPath: projectPathProperty, - }, - 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, - }, - projectPath: projectPathProperty, - }, - 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, - }, - projectPath: projectPathProperty, - }, - required: ['symbol'], - }, - }, - { - name: 'codegraph_node', - description: 'Get detailed information about a specific code symbol. Use includeCode=true only when you need the full source code - otherwise just get location and signature to minimize context usage.', - 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: false to minimize context)', - default: false, - }, - projectPath: projectPathProperty, - }, - required: ['symbol'], - }, - }, - { - name: 'codegraph_explore', - description: 'Deep exploration tool — returns comprehensive context for a topic in a SINGLE call. Groups all relevant source code by file (contiguous sections, not snippets), includes a relationship map, and uses deeper graph traversal. Designed to replace multiple codegraph_node + file Read calls. Use this instead of codegraph_context when you need thorough understanding. IMPORTANT: Use specific symbol names, file names, or short code terms in your query — NOT natural language sentences. Before calling this, use codegraph_search to discover relevant symbol names, then include those names in your query. Bad: "how are agent prompts loaded and passed to the CLI". Good: "readAgentsFromDirectory createClaudeSession chat-manager agents.ts".', - inputSchema: { - type: 'object', - properties: { - query: { - type: 'string', - description: 'Symbol names, file names, or short code terms to explore (e.g., "AuthService loginUser session-manager", "GraphTraverser BFS impact traversal.ts"). Use codegraph_search first to find relevant names.', - }, - maxFiles: { - type: 'number', - description: 'Maximum number of files to include source code from (default: 12)', - default: 12, - }, - projectPath: projectPathProperty, - }, - required: ['query'], - }, - }, - { - name: 'codegraph_status', - description: 'Get the status of the CodeGraph index, including statistics about indexed files, nodes, and edges.', - inputSchema: { - type: 'object', - properties: { - projectPath: projectPathProperty, - }, - }, - }, - { - name: 'codegraph_files', - description: 'REQUIRED for file/folder exploration. Get the project file structure from the CodeGraph index. Returns a tree view of all indexed files with metadata (language, symbol count). Much faster than Glob/filesystem scanning. Use this FIRST when exploring project structure, finding files, or understanding codebase organization.', - inputSchema: { - type: 'object', - properties: { - path: { - type: 'string', - description: 'Filter to files under this directory path (e.g., "src/components"). Returns all files if not specified.', - }, - pattern: { - type: 'string', - description: 'Filter files matching this glob pattern (e.g., "*.tsx", "**/*.test.ts")', - }, - format: { - type: 'string', - description: 'Output format: "tree" (hierarchical, default), "flat" (simple list), "grouped" (by language)', - enum: ['tree', 'flat', 'grouped'], - default: 'tree', - }, - includeMetadata: { - type: 'boolean', - description: 'Include file metadata like language and symbol count (default: true)', - default: true, - }, - maxDepth: { - type: 'number', - description: 'Maximum directory depth to show (default: unlimited)', - }, - projectPath: projectPathProperty, - }, - }, - }, -]; /** * Tool handler that executes tools against a CodeGraph instance @@ -291,7 +68,7 @@ export const tools: ToolDefinition[] = [ * Supports cross-project queries via the projectPath parameter. * Other projects are opened on-demand and cached for performance. */ -export class ToolHandler { +export class ToolHandler implements ToolHandlerLike { // Cache of opened CodeGraph instances for cross-project queries private projectCache: Map = new Map(); @@ -404,32 +181,24 @@ export class ToolHandler { } /** - * Execute a tool by name + * Execute a tool by name. + * + * The dispatch table lives in `./tools/registry.ts` — this method + * just looks up the tool's `handlerKey` and invokes the matching + * `handle` method on this class. Adding a new tool means + * registering a `ToolModule` (one new file under `./tools/`, + * one entry in the registry) plus implementing + * `handle(args)` here. */ async execute(toolName: string, args: Record): Promise { 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_explore': - return await this.handleExplore(args); - case 'codegraph_node': - return await this.handleNode(args); - case 'codegraph_status': - return await this.handleStatus(args); - case 'codegraph_files': - return await this.handleFiles(args); - default: - return this.errorResult(`Unknown tool: ${toolName}`); - } + const mod = getToolModule(toolName); + if (!mod) return this.errorResult(`Unknown tool: ${toolName}`); + // `implements ToolHandlerLike` makes this lookup type-safe: + // `mod.handlerKey` is constrained to `HandlerKey`, and every + // member of that union maps to an `(args) => Promise` + // method on `this` (verified at compile time, not at runtime). + return await this[mod.handlerKey](args); } catch (err) { return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}`); } @@ -438,7 +207,7 @@ export class ToolHandler { /** * Handle codegraph_search */ - private async handleSearch(args: Record): Promise { + async handleSearch(args: Record): Promise { const query = this.validateString(args.query, 'query'); if (typeof query !== 'string') return query; @@ -463,7 +232,7 @@ export class ToolHandler { /** * Handle codegraph_context */ - private async handleContext(args: Record): Promise { + async handleContext(args: Record): Promise { const task = this.validateString(args.task, 'task'); if (typeof task !== 'string') return task; @@ -529,7 +298,7 @@ export class ToolHandler { /** * Handle codegraph_callers */ - private async handleCallers(args: Record): Promise { + async handleCallers(args: Record): Promise { const symbol = this.validateString(args.symbol, 'symbol'); if (typeof symbol !== 'string') return symbol; @@ -564,7 +333,7 @@ export class ToolHandler { /** * Handle codegraph_callees */ - private async handleCallees(args: Record): Promise { + async handleCallees(args: Record): Promise { const symbol = this.validateString(args.symbol, 'symbol'); if (typeof symbol !== 'string') return symbol; @@ -599,7 +368,7 @@ export class ToolHandler { /** * Handle codegraph_impact */ - private async handleImpact(args: Record): Promise { + async handleImpact(args: Record): Promise { const symbol = this.validateString(args.symbol, 'symbol'); if (typeof symbol !== 'string') return symbol; @@ -650,7 +419,7 @@ export class ToolHandler { * then read contiguous file sections covering all symbols per file. * This replaces multiple codegraph_node + Read calls. */ - private async handleExplore(args: Record): Promise { + async handleExplore(args: Record): Promise { const query = this.validateString(args.query, 'query'); if (typeof query !== 'string') return query; @@ -936,7 +705,7 @@ export class ToolHandler { /** * Handle codegraph_node */ - private async handleNode(args: Record): Promise { + async handleNode(args: Record): Promise { const symbol = this.validateString(args.symbol, 'symbol'); if (typeof symbol !== 'string') return symbol; @@ -962,7 +731,7 @@ export class ToolHandler { /** * Handle codegraph_status */ - private async handleStatus(args: Record): Promise { + async handleStatus(args: Record): Promise { const cg = this.getCodeGraph(args.projectPath as string | undefined); const stats = cg.getStats(); @@ -996,7 +765,7 @@ export class ToolHandler { /** * Handle codegraph_files - get project file structure from the index */ - private async handleFiles(args: Record): Promise { + async handleFiles(args: Record): Promise { const cg = this.getCodeGraph(args.projectPath as string | undefined); const pathFilter = args.path as string | undefined; const pattern = args.pattern as string | undefined; @@ -1364,13 +1133,13 @@ export class ToolHandler { return context.summary || 'No context found'; } - private textResult(text: string): ToolResult { + textResult(text: string): ToolResult { return { content: [{ type: 'text', text }], }; } - private errorResult(message: string): ToolResult { + errorResult(message: string): ToolResult { return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true, diff --git a/src/mcp/tools/callees.ts b/src/mcp/tools/callees.ts new file mode 100644 index 00000000..3c0d9740 --- /dev/null +++ b/src/mcp/tools/callees.ts @@ -0,0 +1,27 @@ +import { projectPathProperty } from '../tool-types'; +import type { ToolModule } from './types'; + +export const CALLEES_TOOL: ToolModule = { + definition: { + 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, + }, + projectPath: projectPathProperty, + }, + required: ['symbol'], + }, + }, + handlerKey: 'handleCallees', +}; diff --git a/src/mcp/tools/callers.ts b/src/mcp/tools/callers.ts new file mode 100644 index 00000000..a5d33912 --- /dev/null +++ b/src/mcp/tools/callers.ts @@ -0,0 +1,27 @@ +import { projectPathProperty } from '../tool-types'; +import type { ToolModule } from './types'; + +export const CALLERS_TOOL: ToolModule = { + definition: { + 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, + }, + projectPath: projectPathProperty, + }, + required: ['symbol'], + }, + }, + handlerKey: 'handleCallers', +}; diff --git a/src/mcp/tools/context.ts b/src/mcp/tools/context.ts new file mode 100644 index 00000000..e8618671 --- /dev/null +++ b/src/mcp/tools/context.ts @@ -0,0 +1,32 @@ +import { projectPathProperty } from '../tool-types'; +import type { ToolModule } from './types'; + +export const CONTEXT_TOOL: ToolModule = { + definition: { + name: 'codegraph_context', + description: + 'PRIMARY TOOL: Build comprehensive context for a task. Returns entry points, related symbols, and key code - often enough to understand the codebase without additional tool calls. NOTE: This provides CODE context, not product requirements. For new features, still clarify UX/behavior questions with the user before implementing.', + inputSchema: { + type: 'object', + properties: { + task: { + type: 'string', + description: 'Description of the task, bug, or feature to build context for', + }, + maxNodes: { + type: 'number', + description: 'Maximum symbols to include (default: 20)', + default: 20, + }, + includeCode: { + type: 'boolean', + description: 'Include code snippets for key symbols (default: true)', + default: true, + }, + projectPath: projectPathProperty, + }, + required: ['task'], + }, + }, + handlerKey: 'handleContext', +}; diff --git a/src/mcp/tools/explore.ts b/src/mcp/tools/explore.ts new file mode 100644 index 00000000..d61b24e9 --- /dev/null +++ b/src/mcp/tools/explore.ts @@ -0,0 +1,28 @@ +import { projectPathProperty } from '../tool-types'; +import type { ToolModule } from './types'; + +export const EXPLORE_TOOL: ToolModule = { + definition: { + name: 'codegraph_explore', + description: + 'Deep exploration tool — returns comprehensive context for a topic in a SINGLE call. Groups all relevant source code by file (contiguous sections, not snippets), includes a relationship map, and uses deeper graph traversal. Designed to replace multiple codegraph_node + file Read calls. Use this instead of codegraph_context when you need thorough understanding. IMPORTANT: Use specific symbol names, file names, or short code terms in your query — NOT natural language sentences. Before calling this, use codegraph_search to discover relevant symbol names, then include those names in your query. Bad: "how are agent prompts loaded and passed to the CLI". Good: "readAgentsFromDirectory createClaudeSession chat-manager agents.ts".', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: + 'Symbol names, file names, or short code terms to explore (e.g., "AuthService loginUser session-manager", "GraphTraverser BFS impact traversal.ts"). Use codegraph_search first to find relevant names.', + }, + maxFiles: { + type: 'number', + description: 'Maximum number of files to include source code from (default: 12)', + default: 12, + }, + projectPath: projectPathProperty, + }, + required: ['query'], + }, + }, + handlerKey: 'handleExplore', +}; diff --git a/src/mcp/tools/files.ts b/src/mcp/tools/files.ts new file mode 100644 index 00000000..117b0676 --- /dev/null +++ b/src/mcp/tools/files.ts @@ -0,0 +1,40 @@ +import { projectPathProperty } from '../tool-types'; +import type { ToolModule } from './types'; + +export const FILES_TOOL: ToolModule = { + definition: { + name: 'codegraph_files', + description: + 'REQUIRED for file/folder exploration. Get the project file structure from the CodeGraph index. Returns a tree view of all indexed files with metadata (language, symbol count). Much faster than Glob/filesystem scanning. Use this FIRST when exploring project structure, finding files, or understanding codebase organization.', + inputSchema: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Filter to files under this directory path (e.g., "src/components"). Returns all files if not specified.', + }, + pattern: { + type: 'string', + description: 'Filter files matching this glob pattern (e.g., "*.tsx", "**/*.test.ts")', + }, + format: { + type: 'string', + description: 'Output format: "tree" (hierarchical, default), "flat" (simple list), "grouped" (by language)', + enum: ['tree', 'flat', 'grouped'], + default: 'tree', + }, + includeMetadata: { + type: 'boolean', + description: 'Include file metadata like language and symbol count (default: true)', + default: true, + }, + maxDepth: { + type: 'number', + description: 'Maximum directory depth to show (default: unlimited)', + }, + projectPath: projectPathProperty, + }, + }, + }, + handlerKey: 'handleFiles', +}; diff --git a/src/mcp/tools/impact.ts b/src/mcp/tools/impact.ts new file mode 100644 index 00000000..45386e6b --- /dev/null +++ b/src/mcp/tools/impact.ts @@ -0,0 +1,27 @@ +import { projectPathProperty } from '../tool-types'; +import type { ToolModule } from './types'; + +export const IMPACT_TOOL: ToolModule = { + definition: { + 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, + }, + projectPath: projectPathProperty, + }, + required: ['symbol'], + }, + }, + handlerKey: 'handleImpact', +}; diff --git a/src/mcp/tools/node.ts b/src/mcp/tools/node.ts new file mode 100644 index 00000000..fe61b254 --- /dev/null +++ b/src/mcp/tools/node.ts @@ -0,0 +1,27 @@ +import { projectPathProperty } from '../tool-types'; +import type { ToolModule } from './types'; + +export const NODE_TOOL: ToolModule = { + definition: { + name: 'codegraph_node', + description: + 'Get detailed information about a specific code symbol. Use includeCode=true only when you need the full source code - otherwise just get location and signature to minimize context usage.', + 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: false to minimize context)', + default: false, + }, + projectPath: projectPathProperty, + }, + required: ['symbol'], + }, + }, + handlerKey: 'handleNode', +}; diff --git a/src/mcp/tools/registry.ts b/src/mcp/tools/registry.ts new file mode 100644 index 00000000..3219f88d --- /dev/null +++ b/src/mcp/tools/registry.ts @@ -0,0 +1,65 @@ +/** + * MCP tool registry. + * + * Adding a new MCP tool is: + * + * 1. Create `src/mcp/tools/.ts` exporting an + * `_TOOL: ToolModule` constant (definition + handlerKey). + * 2. Add **one** import line and **one** array entry to this file. + * 3. Add a `handle` method on `ToolHandler` in `../tools.ts`, + * and add the new key to `HandlerKey` in `./types.ts`. + * + * The third step is currently the only "shared method on a single + * class" surface that competing PRs can collide on. Extracting + * handler bodies into per-tool files (so step 3 also becomes a + * single-file addition) is left as a follow-up. + */ + +import type { ToolDefinition } from '../tool-types'; +import type { ToolModule } from './types'; + +import { CALLEES_TOOL } from './callees'; +import { CALLERS_TOOL } from './callers'; +import { CONTEXT_TOOL } from './context'; +import { EXPLORE_TOOL } from './explore'; +import { FILES_TOOL } from './files'; +import { IMPACT_TOOL } from './impact'; +import { NODE_TOOL } from './node'; +import { SEARCH_TOOL } from './search'; +import { STATUS_TOOL } from './status'; + +const ALL_TOOLS: readonly ToolModule[] = [ + CALLEES_TOOL, + CALLERS_TOOL, + CONTEXT_TOOL, + EXPLORE_TOOL, + FILES_TOOL, + IMPACT_TOOL, + NODE_TOOL, + SEARCH_TOOL, + STATUS_TOOL, +]; + +let byName: Map | null = null; +function ensureIndex(): Map { + if (byName) return byName; + byName = new Map(); + for (const t of ALL_TOOLS) byName.set(t.definition.name, t); + return byName; +} + +export function getToolModules(): readonly ToolModule[] { + return ALL_TOOLS; +} + +export function getToolModule(name: string): ToolModule | undefined { + return ensureIndex().get(name); +} + +/** + * The `tools[]` array advertised in MCP `list_tools`. Derived from + * the registry; sorted alphabetically by tool name for stable output. + */ +export const tools: readonly ToolDefinition[] = ALL_TOOLS + .map((t) => t.definition) + .sort((a, b) => a.name.localeCompare(b.name)); diff --git a/src/mcp/tools/search.ts b/src/mcp/tools/search.ts new file mode 100644 index 00000000..c6678333 --- /dev/null +++ b/src/mcp/tools/search.ts @@ -0,0 +1,32 @@ +import { projectPathProperty } from '../tool-types'; +import type { ToolModule } from './types'; + +export const SEARCH_TOOL: ToolModule = { + definition: { + name: 'codegraph_search', + description: + 'Quick symbol search by name. Returns locations only (no code). Use codegraph_context instead for comprehensive task context.', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Symbol name or partial name (e.g., "auth", "signIn", "UserService")', + }, + kind: { + type: 'string', + description: 'Filter by node kind', + enum: ['function', 'method', 'class', 'interface', 'type', 'variable', 'route', 'component'], + }, + limit: { + type: 'number', + description: 'Maximum results (default: 10)', + default: 10, + }, + projectPath: projectPathProperty, + }, + required: ['query'], + }, + }, + handlerKey: 'handleSearch', +}; diff --git a/src/mcp/tools/status.ts b/src/mcp/tools/status.ts new file mode 100644 index 00000000..84bebcc3 --- /dev/null +++ b/src/mcp/tools/status.ts @@ -0,0 +1,17 @@ +import { projectPathProperty } from '../tool-types'; +import type { ToolModule } from './types'; + +export const STATUS_TOOL: ToolModule = { + definition: { + name: 'codegraph_status', + description: + 'Get the status of the CodeGraph index, including statistics about indexed files, nodes, and edges.', + inputSchema: { + type: 'object', + properties: { + projectPath: projectPathProperty, + }, + }, + }, + handlerKey: 'handleStatus', +}; diff --git a/src/mcp/tools/types.ts b/src/mcp/tools/types.ts new file mode 100644 index 00000000..6741d965 --- /dev/null +++ b/src/mcp/tools/types.ts @@ -0,0 +1,50 @@ +/** + * MCP tool registry types. + * + * Each tool ships its own self-contained `ToolModule` (definition + * + handler-key reference) so adding an MCP tool is a single-file + * addition for the metadata and dispatch entry. The actual handler + * bodies still live as methods on the `ToolHandler` class in + * `../tools.ts` (the helpers they call are tightly coupled and a + * full body extraction is left as a follow-up); each tool's + * `handlerKey` is the string name of the method to invoke. + * + * The registry (`./registry`) imports each module and exposes + * `tools[]` (for `list_tools`) plus a `getModule(name)` lookup + * used by `ToolHandler.execute`. + */ + +import type { ToolDefinition, ToolResult } from '../tool-types'; + +/** + * Names of methods on `ToolHandler` that can serve as tool handlers. + * Kept as a string union (not a `keyof ToolHandler` lookup) to + * avoid a circular import — the type list is the source of truth + * and is checked structurally at the call site in `execute()`. + */ +export type HandlerKey = + | 'handleSearch' + | 'handleContext' + | 'handleCallers' + | 'handleCallees' + | 'handleImpact' + | 'handleExplore' + | 'handleNode' + | 'handleStatus' + | 'handleFiles'; + +/** + * The minimum surface a `ToolHandler`-shaped object exposes for + * dispatch. Extending `HandlerKey` adds a new entry here too. + */ +export type ToolHandlerLike = { + [K in HandlerKey]: (args: Record) => Promise; +} & { + errorResult(message: string): ToolResult; +}; + +export interface ToolModule { + readonly definition: ToolDefinition; + /** Method name on `ToolHandler` that runs this tool. */ + readonly handlerKey: HandlerKey; +} From 3e2436148675e54e0123e7c5f1e545eb3f45cd81 Mon Sep 17 00:00:00 2001 From: andreinknv Date: Mon, 27 Apr 2026 21:45:22 -0400 Subject: [PATCH 2/3] feat(mcp): emit server-level instructions in initialize response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP `initialize` response can include an `instructions` field that clients (Claude Code, Cursor, opencode, LangChain, OpenAI Agent SDK, etc.) surface in the agent's system prompt automatically. Today codegraph emits an empty initialize response — agents only see individual tool descriptions, no overall guidance on how to compose them. This adds the missing playbook: - **Tool selection by intent** — quick map from "what is X" / "how does X work" / "what would changing X break" to the right tool. - **Common chains** — onboarding (context first), PR review (review_context), refactor planning (search → callers → impact), debugging a regression. - **Tier discipline** — start at the cheap deterministic tier (search, context, callers, callees, impact, node, explore, files, status), escalate to conditional tools only when their data exists, and only reach for LLM-mediated tools when the cheap path doesn't suffice. - **Agent-bridge tier** — explicit recipe for projects without a local LLM where the agent itself summarizes via codegraph_pending_summaries + codegraph_save_summaries. - **Anti-patterns** — don't grep when search exists, don't chain search+node when context covers it, don't query the index immediately after a write. Lives in src/mcp/server-instructions.ts so it's easy to update without touching the JSON-RPC dispatch in src/mcp/index.ts. Single-file, no schema changes, no migrations, no test changes needed. References tools that exist on `main` today; doesn't presume any of the in-flight feature PRs (#110, #112-115, #111) have landed. After those merge, the relevant sections of this guidance start applying without needing a follow-up edit. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/mcp/index.ts | 14 +++++-- src/mcp/server-instructions.ts | 72 ++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 src/mcp/server-instructions.ts diff --git a/src/mcp/index.ts b/src/mcp/index.ts index c31284a8..243ec0d1 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -20,6 +20,7 @@ import CodeGraph, { findNearestCodeGraphRoot } from '../index'; import { StdioTransport, JsonRpcRequest, JsonRpcNotification, ErrorCodes } from './transport'; import { ToolHandler } from './tools'; import { getToolModule } from './tools/registry'; +import { SERVER_INSTRUCTIONS } from './server-instructions'; /** * Convert a file:// URI to a filesystem path. @@ -35,8 +36,10 @@ function fileUriToPath(uri: string): string { } return path.resolve(filePath); } catch { - // Fallback for non-standard URIs - return uri.replace(/^file:\/\/\/?/, ''); + // Fallback for non-standard URIs — still resolve through path.resolve + // so a malformed `file:///../etc/passwd` is normalized rather than + // returned raw to downstream filesystem code. + return path.resolve(uri.replace(/^file:\/\/\/?/, '')); } } @@ -269,13 +272,18 @@ export class MCPServer { // Try to initialize the default project (non-fatal if it fails) await this.tryInitializeDefault(projectPath); - // We accept the client's protocol version but respond with our supported version + // We accept the client's protocol version but respond with our supported version. + // `instructions` is a protocol-level field that MCP clients surface in the + // agent's system prompt, giving the agent a high-level playbook for the + // toolset before it sees individual tool descriptions. See + // ./server-instructions.ts. this.transport.sendResult(request.id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {}, }, serverInfo: SERVER_INFO, + instructions: SERVER_INSTRUCTIONS, }); } diff --git a/src/mcp/server-instructions.ts b/src/mcp/server-instructions.ts new file mode 100644 index 00000000..9c3fbfaa --- /dev/null +++ b/src/mcp/server-instructions.ts @@ -0,0 +1,72 @@ +/** + * Server-level instructions emitted in the MCP `initialize` response. + * + * MCP clients (Claude Code, Cursor, opencode, LangChain, OpenAI Agent + * SDK, …) surface this text in the agent's system prompt automatically, + * giving the agent a high-level playbook for the codegraph toolset + * before it sees individual tool descriptions. + * + * Goals when editing this: + * - Tool selection by intent (which tool for which question) + * - Common chains (PR review = X then Y; refactor planning = A then B) + * - Anti-patterns (don't grep when codegraph_search is faster) + * - Tier discipline (cheap deterministic → conditional → LLM-mediated) + * + * Keep it tight. The agent reads this every session — long instructions + * burn tokens. Aim for under ~80 lines of useful guidance. + */ +export const SERVER_INSTRUCTIONS = `# Codegraph — code intelligence over an indexed knowledge graph + +Codegraph builds a SQLite knowledge graph of every symbol, edge, and +file in the workspace. It is a structural reference manual the agent +consults BEFORE writing or editing code, not a live linter that runs +during generation. Reads are sub-millisecond; the index lags writes by +about a second through the file watcher. + +## When to use which tool + +- **"What is the symbol named X?"** → \`codegraph_search\` (fast lookup) +- **"What's the deal with this task / feature / bug?"** → \`codegraph_context\` (PRIMARY tool — composes 5+ smaller queries into one answer) +- **"What calls this function?"** → \`codegraph_callers\` +- **"What does this function call?"** → \`codegraph_callees\` +- **"What would changing this break?"** → \`codegraph_impact\` +- **"Show me this symbol's source / signature / docstring."** → \`codegraph_node\` +- **"Survey an unfamiliar topic / pattern / module."** → \`codegraph_explore\` (heavier; best when budget allows) +- **"What's in directory X?"** → \`codegraph_files\` +- **"Is the index ready / what's its size?"** → \`codegraph_status\` + +## Common chains (run tools in sequence) + +- **Onboarding to a topic**: \`codegraph_context\` first. If still unclear, \`codegraph_explore\` for breadth, then \`codegraph_node\` on specific symbols you want code for. +- **PR review**: if \`codegraph_review_context\` is available (PR #110), pass the unified diff to it — returns affected symbols + their callers + impact + co-change warnings in one call. +- **Refactor planning**: \`codegraph_search\` to find the symbol, \`codegraph_callers\` to see what depends on it, \`codegraph_impact\` to see the blast radius. +- **Debugging a regression**: \`codegraph_callers\` of the suspected symbol. If recent changes are in scope, look for hotspot tools (\`codegraph_hotspots\` if available) to identify churn × centrality risk. + +## Tool tiers (start cheap, escalate when needed) + +1. **Always available, deterministic, sub-millisecond**: search / context / callers / callees / impact / node / explore / files / status. Most tasks can be answered entirely at this tier. +2. **Conditional on data availability**: \`codegraph_review_context\` needs a diff. \`codegraph_hotspots\`, \`codegraph_config\`, \`codegraph_sql\` need their respective indexed signals (git history, env-var read sites, SQL string-literals). All return clearly when data isn't present. +3. **LLM-mediated, opt-in**: \`codegraph_ask\` (RAG Q&A), \`codegraph_similar\` (semantic search), \`codegraph_dead_code\` (graph + LLM judge), \`codegraph_role\` / \`codegraph_module\` (LLM classifications). These require a configured local LLM endpoint or the agent-bridge tier. + +## Agent-bridge tier (when no local LLM is configured) + +When LLM-mediated tools aren't available but the user wants summaries: +1. Call \`codegraph_pending_summaries\` to pull a batch of symbols needing summaries (returns each symbol's body + content_hash). +2. The agent (you) generate one-line summaries for each — action-verb leading, no "This function..." preamble, ≤200 chars. +3. Call \`codegraph_save_summaries\` echoing each item's contentHash unchanged. Codegraph re-validates against current disk before persisting. + +This lets agents do LLM work themselves when no separate LLM endpoint exists. + +## Anti-patterns + +- **Don't grep first** when looking up a symbol by name — \`codegraph_search\` is faster and returns kind + location + signature. +- **Don't call \`codegraph_search\` then \`codegraph_node\`** when you just want context — \`codegraph_context\` is one round-trip. +- **Don't use \`codegraph_explore\` for narrow questions** — it's a multi-call deep dive, expensive in tokens. Save it for genuine "I'm new here" surveys. +- **Don't query the index immediately after editing a file** — the watcher needs ~500ms to debounce + sync. Wait for the next turn. + +## Limitations + +- Index lags file writes by ~1 second (watcher debounce + sync). +- Cross-file resolution is a best-effort name match; ambiguous calls return multiple candidates. +- No live correctness validation — that's still the TypeScript compiler / test suite / linter's job. Codegraph supplements those with structural context they don't have. +`; From 7ce63d2624e332197cd129535021229889456e06 Mon Sep 17 00:00:00 2001 From: andreinknv Date: Tue, 28 Apr 2026 00:56:11 -0400 Subject: [PATCH 3/3] docs(mcp): teach the playbook codegraph_coverage and codegraph_biomarkers Two new tools landed in #124 and #125 that this playbook should route the agent to instead of falling back to "read the source": - codegraph_biomarkers (PR #125): structured static-analysis signals (Code Health, cyclomatic, nesting, length) so an agent can ask "is this function risky to change?" without reading the source. - codegraph_coverage (PR #124): per-symbol coverage from lcov so an agent can ask "is this function tested?" with a structured answer. Updates: - "When to use which tool" map gains two entries. - Refactor-planning chain expanded to call both tools before callers/impact -- and points at the killer cross-tool query (high-centrality + warning-severity findings). - Tier table places biomarkers in tier 1 (always available after #125 lands) and coverage in tier 2 (conditional on a prior `codegraph coverage ` ingestion). Both references are forward-compatible: agents that try to call a not-yet-merged tool get a graceful "unknown tool" error, same pattern the existing playbook already uses for #110, #111, etc. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/mcp/server-instructions.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/mcp/server-instructions.ts b/src/mcp/server-instructions.ts index 9c3fbfaa..685ebeac 100644 --- a/src/mcp/server-instructions.ts +++ b/src/mcp/server-instructions.ts @@ -30,6 +30,8 @@ about a second through the file watcher. - **"What calls this function?"** → \`codegraph_callers\` - **"What does this function call?"** → \`codegraph_callees\` - **"What would changing this break?"** → \`codegraph_impact\` +- **"Is this function risky to change? Is it complex / nested / large?"** → \`codegraph_biomarkers\` (PR #125, when present) — structured answer instead of reading 200 lines of source +- **"Is this function tested? What's covered?"** → \`codegraph_coverage\` (PR #124, when present) — requires a prior \`codegraph coverage \` ingestion - **"Show me this symbol's source / signature / docstring."** → \`codegraph_node\` - **"Survey an unfamiliar topic / pattern / module."** → \`codegraph_explore\` (heavier; best when budget allows) - **"What's in directory X?"** → \`codegraph_files\` @@ -39,13 +41,14 @@ about a second through the file watcher. - **Onboarding to a topic**: \`codegraph_context\` first. If still unclear, \`codegraph_explore\` for breadth, then \`codegraph_node\` on specific symbols you want code for. - **PR review**: if \`codegraph_review_context\` is available (PR #110), pass the unified diff to it — returns affected symbols + their callers + impact + co-change warnings in one call. -- **Refactor planning**: \`codegraph_search\` to find the symbol, \`codegraph_callers\` to see what depends on it, \`codegraph_impact\` to see the blast radius. -- **Debugging a regression**: \`codegraph_callers\` of the suspected symbol. If recent changes are in scope, look for hotspot tools (\`codegraph_hotspots\` if available) to identify churn × centrality risk. +- **Refactor planning**: \`codegraph_search\` to find the symbol; \`codegraph_biomarkers\` (mode=symbol) for its Code Health and complexity metrics; \`codegraph_coverage\` (mode=symbol) to see if tests exist; \`codegraph_callers\` for what depends on it; \`codegraph_impact\` for the full blast radius. The killer pre-refactor query is \`codegraph_biomarkers minSeverity=warning minCentrality=0.001\` — lists high-impact code with structural problems in one call. +- **Debugging a regression**: \`codegraph_callers\` of the suspected symbol. If recent changes are in scope, look for hotspot tools (\`codegraph_hotspots\` if available) to identify churn × centrality risk. \`codegraph_biomarkers\` on the suspected hotspot tells you whether the function is structurally bad enough that it might be the cause. +- **"What should I test next?"**: \`codegraph_coverage\` mode=ranked with \`minCentrality\` set — returns high-impact under-covered code, ordered by importance. ## Tool tiers (start cheap, escalate when needed) -1. **Always available, deterministic, sub-millisecond**: search / context / callers / callees / impact / node / explore / files / status. Most tasks can be answered entirely at this tier. -2. **Conditional on data availability**: \`codegraph_review_context\` needs a diff. \`codegraph_hotspots\`, \`codegraph_config\`, \`codegraph_sql\` need their respective indexed signals (git history, env-var read sites, SQL string-literals). All return clearly when data isn't present. +1. **Always available, deterministic, sub-millisecond**: search / context / callers / callees / impact / node / explore / files / status. Plus \`codegraph_biomarkers\` once #125 lands — analysis runs as part of every indexAll/sync. Most tasks can be answered entirely at this tier. +2. **Conditional on data availability**: \`codegraph_review_context\` needs a diff. \`codegraph_hotspots\`, \`codegraph_config\`, \`codegraph_sql\` need their respective indexed signals (git history, env-var read sites, SQL string-literals). \`codegraph_coverage\` needs a prior \`codegraph coverage \` ingestion. All return clearly when data isn't present. 3. **LLM-mediated, opt-in**: \`codegraph_ask\` (RAG Q&A), \`codegraph_similar\` (semantic search), \`codegraph_dead_code\` (graph + LLM judge), \`codegraph_role\` / \`codegraph_module\` (LLM classifications). These require a configured local LLM endpoint or the agent-bridge tier. ## Agent-bridge tier (when no local LLM is configured)