diff --git a/__tests__/mcp-tool-registry.test.ts b/__tests__/mcp-tool-registry.test.ts
new file mode 100644
index 000000000..6ca9cef88
--- /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<Name>`
+ * 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<string, unknown>)[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 bc3552ae1..243ec0d15 100644
--- a/src/mcp/index.ts
+++ b/src/mcp/index.ts
@@ -18,7 +18,9 @@
 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';
+import { SERVER_INSTRUCTIONS } from './server-instructions';
 
 /**
  * Convert a file:// URI to a filesystem path.
@@ -34,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:\/\/\/?/, ''));
   }
 }
 
@@ -268,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,
     });
   }
 
@@ -309,8 +318,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/server-instructions.ts b/src/mcp/server-instructions.ts
new file mode 100644
index 000000000..685ebeac4
--- /dev/null
+++ b/src/mcp/server-instructions.ts
@@ -0,0 +1,75 @@
+/**
+ * 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\`
+- **"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 <lcov>\` 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\`
+- **"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_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. 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 <lcov>\` 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)
+
+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.
+`;
diff --git a/src/mcp/tool-types.ts b/src/mcp/tool-types.ts
new file mode 100644
index 000000000..90e94fe8d
--- /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<string, PropertySchema>;
+    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 53713145c..7a5b995a5 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/<name>.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<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;
-}
-
-/**
- * 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<string, CodeGraph> = 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<Name>` 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<Name>(args)` here.
    */
   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_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<ToolResult>`
+      // 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<string, unknown>): Promise<ToolResult> {
+  async handleSearch(args: Record<string, unknown>): Promise<ToolResult> {
     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<string, unknown>): Promise<ToolResult> {
+  async handleContext(args: Record<string, unknown>): Promise<ToolResult> {
     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<string, unknown>): Promise<ToolResult> {
+  async handleCallers(args: Record<string, unknown>): Promise<ToolResult> {
     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<string, unknown>): Promise<ToolResult> {
+  async handleCallees(args: Record<string, unknown>): Promise<ToolResult> {
     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<string, unknown>): Promise<ToolResult> {
+  async handleImpact(args: Record<string, unknown>): Promise<ToolResult> {
     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<string, unknown>): Promise<ToolResult> {
+  async handleExplore(args: Record<string, unknown>): Promise<ToolResult> {
     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<string, unknown>): Promise<ToolResult> {
+  async handleNode(args: Record<string, unknown>): Promise<ToolResult> {
     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<string, unknown>): Promise<ToolResult> {
+  async handleStatus(args: Record<string, unknown>): Promise<ToolResult> {
     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<string, unknown>): Promise<ToolResult> {
+  async handleFiles(args: Record<string, unknown>): Promise<ToolResult> {
     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 000000000..3c0d9740e
--- /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 000000000..a5d339126
--- /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 000000000..e8618671f
--- /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 000000000..d61b24e92
--- /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 000000000..117b06764
--- /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 000000000..45386e6be
--- /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 000000000..fe61b254b
--- /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 000000000..3219f88d9
--- /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/<name>.ts` exporting an
+ *      `<NAME>_TOOL: ToolModule` constant (definition + handlerKey).
+ *   2. Add **one** import line and **one** array entry to this file.
+ *   3. Add a `handle<Name>` 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<string, ToolModule> | null = null;
+function ensureIndex(): Map<string, ToolModule> {
+  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 000000000..c6678333c
--- /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 000000000..84bebcc3e
--- /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 000000000..6741d965e
--- /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<string, unknown>) => Promise<ToolResult>;
+} & {
+  errorResult(message: string): ToolResult;
+};
+
+export interface ToolModule {
+  readonly definition: ToolDefinition;
+  /** Method name on `ToolHandler` that runs this tool. */
+  readonly handlerKey: HandlerKey;
+}
