Files
KiCAD-MCP-Server/docs/mcp-router-guide.md
T
KiCAD MCP Bot c65600049e feat: Implement intelligent tool router pattern (Phase 1)
Adds tool discovery system to reduce AI context usage by up to 70% while
maintaining full access to all 59 tools. Organizes tools into 7 logical
categories with automatic discovery and execution.

## What's New

### Tool Router System
- 12 direct tools (always visible for high-frequency operations)
- 47 routed tools (organized into 7 discoverable categories)
- 4 router tools for discovery and execution:
  - list_tool_categories - Browse all categories
  - get_category_tools - View tools in a category
  - search_tools - Find tools by keyword
  - execute_tool - Execute any routed tool

### Tool Categories
1. board (9 tools) - Board configuration, layers, zones
2. component (8 tools) - Advanced component operations
3. export (8 tools) - Manufacturing file generation
4. drc (8 tools) - Design rule checking & validation
5. schematic (8 tools) - Schematic editor operations
6. library (4 tools) - Footprint library access
7. routing (2 tools) - Advanced routing (vias, copper pours)

## Implementation Details

### New Files
- src/tools/registry.ts - Tool categorization and lookup system
- src/tools/router.ts - Router tool implementations
- docs/ROUTER_ARCHITECTURE.md - Design specification
- docs/ROUTER_IMPLEMENTATION_STATUS.md - Implementation status
- docs/TOOL_INVENTORY.md - Complete tool catalog
- docs/ROUTER_QUICK_START.md - User guide
- docs/mcp-router-guide.md - Implementation guide
- test-router.js - Registry test suite

### Modified Files
- src/server.ts - Integrated router tool registration
- README.md - Updated with router documentation and user feedback section

## Benefits
- Reduces AI context by organizing tools into discoverable categories
- Maintains backwards compatibility (all tools still functional)
- Seamless user experience (discovery is automatic)
- Extensible architecture for adding new tools
- Comprehensive documentation

## Testing
 Build passes (npm run build)
 Registry tests pass (node test-router.js)
 Server starts successfully with router tools
 All 59 tools remain accessible

## Current State
Phase 1 Complete: Infrastructure implemented and tested
Phase 2 Pending: Optional token optimization (hide routed tools from context)

Token impact:
- Current: ~42K tokens (all tools still registered)
- Potential: ~12K tokens (70% reduction with Phase 2)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 11:07:07 -05:00

35 KiB

MCP Tool Router Pattern Guide

A practical guide for building MCP servers with 50-500+ tools without destroying context windows or confusing the LLM.


The Problem

When your MCP server exposes too many tools:

  1. Token bloat: 50 tools ≈ 30-50K tokens consumed before the user says anything
  2. Selection errors: Claude sees add_component, place_component, add_footprint, place_footprint and guesses wrong
  3. Context starvation: Less room for actual conversation and results
  4. Accuracy degradation: More tools = more confusion about which to use

Real-world example: A KiCAD MCP server with 52 tools consumes ~40K tokens. An IDA Pro MCP server could easily hit 100+ tools.


Solution Overview

Implement a router pattern within your MCP server that:

  1. Exposes only essential tools directly (10-15 tools)
  2. Provides discovery tools for everything else
  3. Routes execution through a single execute_tool endpoint

Result: Claude sees ~18 tools instead of 100+, but can still access everything.


Architecture

┌─────────────────────────────────────────────────────────────┐
│                     MCP Client (Claude)                      │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      Your MCP Server                         │
│  ┌─────────────────────────────────────────────────────────┐│
│  │              DIRECT TOOLS (Always Visible)              ││
│  │  • create_project    • open_project    • save_project   ││
│  │  • add_component     • add_track       • get_info       ││
│  │  • (10-15 high-frequency tools)                         ││
│  └─────────────────────────────────────────────────────────┘│
│  ┌─────────────────────────────────────────────────────────┐│
│  │                    ROUTER TOOLS                         ││
│  │  • list_tool_categories   • get_category_tools          ││
│  │  • execute_tool           • search_tools                ││
│  └─────────────────────────────────────────────────────────┘│
│                              │                               │
│                              ▼                               │
│  ┌─────────────────────────────────────────────────────────┐│
│  │              ROUTED TOOLS (Hidden)                      ││
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐   ││
│  │  │  export  │ │   drc    │ │  zones   │ │ advanced │   ││
│  │  │ (8 tools)│ │(5 tools) │ │(6 tools) │ │(12 tools)│   ││
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────┘   ││
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐                ││
│  │  │schematic │ │  layers  │ │ graphics │  ... more      ││
│  │  │(15 tools)│ │(4 tools) │ │(10 tools)│                ││
│  │  └──────────┘ └──────────┘ └──────────┘                ││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘

Implementation

Step 1: Define Your Tool Registry

Create a central registry that organizes all tools by category.

// src/tools/registry.ts

export interface ToolDefinition {
  name: string;
  description: string;
  inputSchema: {
    type: "object";
    properties: Record<string, unknown>;
    required?: string[];
  };
  handler: (params: any) => Promise<any>;
}

export interface ToolCategory {
  name: string;
  description: string;
  tools: ToolDefinition[];
}

// Define all your categories
export const toolCategories: ToolCategory[] = [
  {
    name: "export",
    description: "Generate output files: Gerber, drill, BOM, PDF, 3D models (STEP/VRML), SVG",
    tools: [
      {
        name: "export_gerber",
        description: "Export Gerber files for PCB fabrication",
        inputSchema: {
          type: "object",
          properties: {
            output_dir: { 
              type: "string", 
              description: "Output directory path" 
            },
            layers: { 
              type: "array", 
              items: { type: "string" },
              description: "Layers to export (default: all copper + silkscreen + mask)"
            },
            format: {
              type: "string",
              enum: ["rs274x", "x2"],
              description: "Gerber format version"
            }
          },
          required: ["output_dir"]
        },
        handler: async (params) => {
          // Your implementation
          return { success: true, files: ["..."] };
        }
      },
      {
        name: "export_drill",
        description: "Export Excellon drill files",
        inputSchema: {
          type: "object",
          properties: {
            output_dir: { type: "string" },
            format: { type: "string", enum: ["excellon", "excellon2"] }
          },
          required: ["output_dir"]
        },
        handler: async (params) => { /* ... */ }
      },
      {
        name: "export_bom",
        description: "Export bill of materials as CSV or XML",
        inputSchema: { /* ... */ },
        handler: async (params) => { /* ... */ }
      },
      // ... more export tools
    ]
  },
  {
    name: "drc",
    description: "Design rule checking: clearance validation, electrical rules, manufacturing constraints",
    tools: [
      {
        name: "run_drc",
        description: "Run full design rule check on current board",
        inputSchema: {
          type: "object",
          properties: {
            report_all: { 
              type: "boolean", 
              description: "Report all violations or stop at first" 
            }
          }
        },
        handler: async (params) => { /* ... */ }
      },
      {
        name: "get_drc_errors",
        description: "Get current DRC violations without re-running check",
        inputSchema: { type: "object", properties: {} },
        handler: async (params) => { /* ... */ }
      },
      {
        name: "set_design_rules",
        description: "Configure design rules: clearance, track width, via size, etc.",
        inputSchema: {
          type: "object",
          properties: {
            min_clearance: { type: "number", description: "Minimum clearance in mm" },
            min_track_width: { type: "number", description: "Minimum track width in mm" },
            min_via_diameter: { type: "number", description: "Minimum via diameter in mm" },
            min_via_drill: { type: "number", description: "Minimum via drill size in mm" }
          }
        },
        handler: async (params) => { /* ... */ }
      }
    ]
  },
  {
    name: "zones",
    description: "Copper zones/pours: ground planes, power fills, keep-out areas",
    tools: [
      {
        name: "add_zone",
        description: "Add copper zone/pour to PCB",
        inputSchema: {
          type: "object",
          properties: {
            net_name: { type: "string", description: "Net to connect (e.g., 'GND', 'VCC')" },
            layer: { type: "string", description: "Layer name (e.g., 'F.Cu', 'B.Cu')" },
            points: {
              type: "array",
              items: {
                type: "object",
                properties: {
                  x: { type: "number" },
                  y: { type: "number" }
                }
              },
              description: "Polygon vertices in mm"
            },
            priority: { type: "number", description: "Fill priority (higher fills first)" }
          },
          required: ["net_name", "layer", "points"]
        },
        handler: async (params) => { /* ... */ }
      },
      {
        name: "fill_zones",
        description: "Recalculate and fill all copper zones",
        inputSchema: { type: "object", properties: {} },
        handler: async (params) => { /* ... */ }
      },
      {
        name: "remove_zone",
        description: "Remove a copper zone by ID",
        inputSchema: {
          type: "object",
          properties: {
            zone_id: { type: "string", description: "Zone identifier" }
          },
          required: ["zone_id"]
        },
        handler: async (params) => { /* ... */ }
      }
    ]
  },
  // Add more categories...
];

Step 2: Build Lookup Maps

Create efficient lookups for routing.

// src/tools/registry.ts (continued)

// Build lookup maps at module load time
const categoryMap = new Map<string, ToolCategory>();
const toolMap = new Map<string, { category: string; tool: ToolDefinition }>();

export function initializeRegistry() {
  for (const category of toolCategories) {
    categoryMap.set(category.name, category);
    for (const tool of category.tools) {
      toolMap.set(tool.name, { category: category.name, tool });
    }
  }
}

export function getCategory(name: string): ToolCategory | undefined {
  return categoryMap.get(name);
}

export function getTool(name: string): { category: string; tool: ToolDefinition } | undefined {
  return toolMap.get(name);
}

export function getAllCategories(): ToolCategory[] {
  return toolCategories;
}

export function searchTools(query: string): Array<{
  category: string;
  tool: string;
  description: string;
}> {
  const q = query.toLowerCase();
  const matches: Array<{ category: string; tool: string; description: string }> = [];

  for (const category of toolCategories) {
    for (const tool of category.tools) {
      if (
        tool.name.toLowerCase().includes(q) ||
        tool.description.toLowerCase().includes(q) ||
        category.name.toLowerCase().includes(q)
      ) {
        matches.push({
          category: category.name,
          tool: tool.name,
          description: tool.description
        });
      }
    }
  }

  return matches;
}

// Initialize on load
initializeRegistry();

Step 3: Create Router Tools

These are the tools that enable discovery and execution.

// src/tools/router.ts

import { 
  getAllCategories, 
  getCategory, 
  getTool, 
  searchTools 
} from "./registry.js";

export const routerTools = {
  list_tool_categories: {
    name: "list_tool_categories",
    description: 
      "List all available tool categories. Use this to discover what operations " +
      "are available beyond the basic tools exposed directly.",
    inputSchema: {
      type: "object" as const,
      properties: {},
      required: []
    },
    handler: async () => {
      const categories = getAllCategories();
      return {
        total_categories: categories.length,
        total_tools: categories.reduce((sum, c) => sum + c.tools.length, 0),
        categories: categories.map(c => ({
          name: c.name,
          description: c.description,
          tool_count: c.tools.length
        }))
      };
    }
  },

  get_category_tools: {
    name: "get_category_tools",
    description:
      "Get detailed information about tools in a specific category, " +
      "including their parameters. Use after list_tool_categories to " +
      "see what's available in a category.",
    inputSchema: {
      type: "object" as const,
      properties: {
        category: {
          type: "string",
          description: "Category name from list_tool_categories"
        }
      },
      required: ["category"]
    },
    handler: async (params: { category: string }) => {
      const category = getCategory(params.category);
      if (!category) {
        return {
          error: `Unknown category: ${params.category}`,
          available_categories: getAllCategories().map(c => c.name)
        };
      }
      return {
        category: category.name,
        description: category.description,
        tools: category.tools.map(t => ({
          name: t.name,
          description: t.description,
          parameters: t.inputSchema
        }))
      };
    }
  },

  execute_tool: {
    name: "execute_tool",
    description:
      "Execute a tool from any category. First use list_tool_categories " +
      "and get_category_tools to discover available tools and their parameters.",
    inputSchema: {
      type: "object" as const,
      properties: {
        tool_name: {
          type: "string",
          description: "Tool name (from get_category_tools)"
        },
        params: {
          type: "object",
          description: "Tool parameters (see get_category_tools for schema)"
        }
      },
      required: ["tool_name"]
    },
    handler: async (input: { tool_name: string; params?: Record<string, unknown> }) => {
      const entry = getTool(input.tool_name);
      if (!entry) {
        return {
          error: `Unknown tool: ${input.tool_name}`,
          hint: "Use list_tool_categories and get_category_tools to find available tools"
        };
      }

      try {
        const result = await entry.tool.handler(input.params || {});
        return {
          tool: input.tool_name,
          category: entry.category,
          result
        };
      } catch (err) {
        return {
          error: `Tool execution failed: ${(err as Error).message}`,
          tool: input.tool_name,
          category: entry.category
        };
      }
    }
  },

  search_tools: {
    name: "search_tools",
    description:
      "Search for tools by keyword across all categories. " +
      "Useful when you know what you want to do but not which category it's in.",
    inputSchema: {
      type: "object" as const,
      properties: {
        query: {
          type: "string",
          description: "Search term (e.g., 'gerber', 'zone', 'differential', 'export')"
        }
      },
      required: ["query"]
    },
    handler: async (params: { query: string }) => {
      const matches = searchTools(params.query);
      return {
        query: params.query,
        count: matches.length,
        matches: matches.slice(0, 20) // Limit results
      };
    }
  }
};

Step 4: Define Direct Tools

These are your high-frequency tools that stay visible always.

// src/tools/direct.ts

import { ToolDefinition } from "./registry.js";

// These tools are ALWAYS visible to Claude - no routing needed
// Pick your 10-15 most common operations

export const directTools: ToolDefinition[] = [
  // === PROJECT LIFECYCLE ===
  {
    name: "create_project",
    description: "Create a new project with initial files and configuration",
    inputSchema: {
      type: "object",
      properties: {
        name: { type: "string", description: "Project name" },
        path: { type: "string", description: "Directory path for project" },
        template: { 
          type: "string", 
          description: "Optional template to use",
          enum: ["blank", "arduino", "raspberry-pi"]
        }
      },
      required: ["name", "path"]
    },
    handler: async (params) => {
      // Implementation
      return { success: true, project_path: `${params.path}/${params.name}` };
    }
  },
  {
    name: "open_project",
    description: "Open an existing project",
    inputSchema: {
      type: "object",
      properties: {
        path: { type: "string", description: "Path to project file or directory" }
      },
      required: ["path"]
    },
    handler: async (params) => { /* ... */ }
  },
  {
    name: "save_project",
    description: "Save all project files",
    inputSchema: {
      type: "object",
      properties: {}
    },
    handler: async (params) => { /* ... */ }
  },
  {
    name: "get_project_info",
    description: "Get current project information: path, files, status",
    inputSchema: {
      type: "object",
      properties: {}
    },
    handler: async (params) => { /* ... */ }
  },

  // === PRIMARY OPERATIONS (your core workflow) ===
  {
    name: "add_component",
    description: "Add a component at specified position",
    inputSchema: {
      type: "object",
      properties: {
        type: { type: "string", description: "Component type or library reference" },
        reference: { type: "string", description: "Reference designator (e.g., R1, U1)" },
        x: { type: "number", description: "X position" },
        y: { type: "number", description: "Y position" },
        rotation: { type: "number", description: "Rotation in degrees", default: 0 }
      },
      required: ["type", "reference", "x", "y"]
    },
    handler: async (params) => { /* ... */ }
  },
  {
    name: "move_component",
    description: "Move a component to new position",
    inputSchema: {
      type: "object",
      properties: {
        reference: { type: "string", description: "Component reference (e.g., R1)" },
        x: { type: "number", description: "New X position" },
        y: { type: "number", description: "New Y position" }
      },
      required: ["reference", "x", "y"]
    },
    handler: async (params) => { /* ... */ }
  },
  {
    name: "list_components",
    description: "List all components with their positions and properties",
    inputSchema: {
      type: "object",
      properties: {
        filter: { type: "string", description: "Optional filter (e.g., 'R*' for resistors)" }
      }
    },
    handler: async (params) => { /* ... */ }
  },

  // === SECONDARY OPERATIONS (still common) ===
  {
    name: "add_connection",
    description: "Add a connection/trace between two points",
    inputSchema: {
      type: "object",
      properties: {
        start: {
          type: "object",
          properties: { x: { type: "number" }, y: { type: "number" } }
        },
        end: {
          type: "object",
          properties: { x: { type: "number" }, y: { type: "number" } }
        },
        net: { type: "string", description: "Net name" }
      },
      required: ["start", "end"]
    },
    handler: async (params) => { /* ... */ }
  },
  {
    name: "list_nets",
    description: "List all nets/connections",
    inputSchema: {
      type: "object",
      properties: {}
    },
    handler: async (params) => { /* ... */ }
  },
  {
    name: "get_info",
    description: "Get general information about current state",
    inputSchema: {
      type: "object",
      properties: {}
    },
    handler: async (params) => { /* ... */ }
  }
];

Step 5: Register with MCP Server

Wire everything together in your main server file.

// src/index.ts

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

import { directTools } from "./tools/direct.js";
import { routerTools } from "./tools/router.js";
import { initializeRegistry } from "./tools/registry.js";

// Initialize the tool registry
initializeRegistry();

const server = new Server(
  {
    name: "your-mcp-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// Combine all visible tools
const allVisibleTools = [
  ...directTools,
  ...Object.values(routerTools)
];

// Build a handler map for quick lookup
const toolHandlers = new Map<string, (params: any) => Promise<any>>();

for (const tool of directTools) {
  toolHandlers.set(tool.name, tool.handler);
}
for (const tool of Object.values(routerTools)) {
  toolHandlers.set(tool.name, tool.handler);
}

// List tools handler - returns only direct + router tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: allVisibleTools.map(tool => ({
      name: tool.name,
      description: tool.description,
      inputSchema: tool.inputSchema,
    })),
  };
});

// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  const handler = toolHandlers.get(name);
  if (!handler) {
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({
            error: `Unknown tool: ${name}`,
            hint: "Use list_tool_categories and search_tools to find available tools"
          })
        }
      ],
      isError: true,
    };
  }

  try {
    const result = await handler(args || {});
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(result, null, 2)
        }
      ],
    };
  } catch (error) {
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify({
            error: `Tool execution failed: ${(error as Error).message}`
          })
        }
      ],
      isError: true,
    };
  }
});

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP Server running on stdio");
}

main().catch(console.error);

Choosing Direct vs Routed Tools

Direct Tools (Always Visible)

Include tools that are:

  • Used in 80%+ of sessions
  • Essential for basic workflows
  • Required for project lifecycle (open, save, create)
  • Needed for the core operation loop

Examples by domain:

Domain Direct Tools
KiCAD create_project, open_project, save_project, add_component, move_component, add_track, list_components, list_nets, get_board_info
IDA Pro open_database, save_database, get_function, list_functions, add_comment, rename, get_xrefs, decompile
Git status, add, commit, push, pull, checkout, branch, log
Database connect, query, list_tables, describe_table

Routed Tools (Hidden Behind Router)

Include tools that are:

  • Used in <50% of sessions
  • Part of specialized workflows
  • End-of-workflow operations (export, report)
  • Advanced features
  • Bulk or batch operations

Examples:

Category Why Route It
export Only used at end of workflow
drc/validation Used during review phase
advanced_* Specialty operations
bulk_* Batch operations
config/settings One-time setup

Category Design Guidelines

Good Category Names

  • Short, memorable (1-2 words)
  • Verb-oriented or domain-specific
  • Clear scope
✓ export, import, drc, zones, routing, schematic, layers, analysis
✗ miscellaneous, utilities, other, stuff, tools2

Good Category Descriptions

Include:

  • What the category does
  • Key operations included
  • When you'd use it
// Good
{
  name: "export",
  description: "Generate manufacturing files: Gerber, drill, BOM, PDF, 3D models (STEP/VRML), SVG"
}

// Bad
{
  name: "export",
  description: "Export tools"
}

Suggested Category Structure (Generic)

const categories = [
  // Core operations (might be direct tools instead)
  { name: "project", description: "Project lifecycle: create, open, save, close" },
  
  // Domain-specific operations
  { name: "analysis", description: "Analyze and inspect: find patterns, validate, check" },
  { name: "modification", description: "Modify and transform: edit, rename, restructure" },
  { name: "navigation", description: "Navigate and search: find, list, filter, locate" },
  
  // Output operations
  { name: "export", description: "Export and generate: reports, files, documentation" },
  { name: "import", description: "Import from external sources: files, formats, APIs" },
  
  // Configuration
  { name: "config", description: "Configuration and settings: preferences, rules, templates" },
  
  // Advanced/specialized
  { name: "advanced", description: "Advanced operations: batch processing, automation, scripting" },
];

IDA Pro Example Structure

For an IDA Pro MCP server with 100+ tools:

Direct Tools (~12)

const directTools = [
  "open_database",      // Load IDB
  "save_database",      // Save IDB
  "get_function",       // Get function by address/name
  "list_functions",     // List all functions
  "decompile",          // Decompile function (Hex-Rays)
  "add_comment",        // Add comment at address
  "rename",             // Rename address/function
  "get_xrefs_to",       // Get cross-references to address
  "get_xrefs_from",     // Get cross-references from address
  "get_strings",        // List strings
  "search_bytes",       // Search for byte pattern
  "get_segments",       // List segments
];

Routed Categories

const categories = [
  {
    name: "disassembly",
    description: "Disassembly operations: undefine, make code/data, change types",
    tools: ["make_code", "make_data", "undefine", "set_type", "make_array", "make_struct"]
  },
  {
    name: "functions",
    description: "Function management: create, delete, modify boundaries, set types",
    tools: ["create_function", "delete_function", "set_func_end", "set_func_type", "add_func_arg"]
  },
  {
    name: "types",
    description: "Type system: structs, enums, typedefs, parse headers",
    tools: ["create_struct", "add_struct_member", "create_enum", "parse_header", "import_types"]
  },
  {
    name: "patching",
    description: "Binary patching: modify bytes, assemble, apply patches",
    tools: ["patch_bytes", "patch_word", "patch_dword", "assemble", "apply_patches"]
  },
  {
    name: "scripting",
    description: "IDAPython scripting: run scripts, evaluate expressions",
    tools: ["run_script", "eval_python", "get_global", "set_global"]
  },
  {
    name: "signatures",
    description: "Signatures and patterns: FLIRT, Lumina, create signatures",
    tools: ["apply_flirt", "query_lumina", "create_sig", "find_pattern"]
  },
  {
    name: "debugging",
    description: "Debugger control: breakpoints, stepping, memory",
    tools: ["set_breakpoint", "step_into", "step_over", "read_memory", "write_memory", "get_regs"]
  },
  {
    name: "export",
    description: "Export: ASM listing, pseudocode, database info, reports",
    tools: ["export_asm", "export_c", "export_json", "generate_report"]
  },
  {
    name: "import",
    description: "Import: symbols, types, comments from external sources",
    tools: ["import_symbols", "import_pdb", "import_map", "import_comments"]
  },
  {
    name: "analysis",
    description: "Analysis control: reanalyze, find patterns, auto-analysis settings",
    tools: ["reanalyze", "find_crypto", "find_strings", "analyze_calls", "set_analysis_options"]
  },
];

Usage Flow Examples

Example 1: User wants to export Gerbers

User: "Export gerbers for this board"

Claude's thinking:
1. Don't see an export_gerber tool directly
2. Call search_tools({ query: "gerber" })
   → Returns: { category: "export", tool: "export_gerber", ... }
3. Call execute_tool({ tool_name: "export_gerber", params: { output_dir: "./gerbers" }})
   → Returns: { success: true, files: [...] }

Claude: "I've exported the Gerber files to ./gerbers/"

Example 2: User wants to place a component

User: "Add a 0805 resistor at position 10,20"

Claude's thinking:
1. See add_component in direct tools
2. Call add_component({ type: "R_0805", reference: "R1", x: 10, y: 20 })
   → Returns: { success: true, reference: "R1" }

Claude: "Added R1 (0805 resistor) at position (10, 20)"

Example 3: User wants to do something unfamiliar

User: "I need to add length matching to these traces"

Claude's thinking:
1. Don't see length matching in direct tools
2. Call search_tools({ query: "length matching" })
   → Returns: { category: "advanced_routing", tool: "add_length_tuning", ... }
3. Call get_category_tools({ category: "advanced_routing" })
   → Returns full parameter schema for add_length_tuning
4. Call execute_tool({ tool_name: "add_length_tuning", params: {...} })

Claude: "I've added length tuning meanders to match the trace lengths"

Testing Your Router

Unit Tests

// tests/router.test.ts

import { describe, it, expect } from "vitest";
import { 
  searchTools, 
  getCategory, 
  getTool,
  getAllCategories 
} from "../src/tools/registry.js";
import { routerTools } from "../src/tools/router.js";

describe("Tool Registry", () => {
  it("should find tools by keyword", () => {
    const results = searchTools("export");
    expect(results.length).toBeGreaterThan(0);
    expect(results.some(r => r.tool.includes("export"))).toBe(true);
  });

  it("should return category info", () => {
    const category = getCategory("export");
    expect(category).toBeDefined();
    expect(category!.tools.length).toBeGreaterThan(0);
  });

  it("should return tool info with category", () => {
    const tool = getTool("export_gerber");
    expect(tool).toBeDefined();
    expect(tool!.category).toBe("export");
  });
});

describe("Router Tools", () => {
  it("list_tool_categories returns all categories", async () => {
    const result = await routerTools.list_tool_categories.handler({});
    expect(result.categories.length).toBe(getAllCategories().length);
  });

  it("get_category_tools returns tools for valid category", async () => {
    const result = await routerTools.get_category_tools.handler({ 
      category: "export" 
    });
    expect(result.tools).toBeDefined();
    expect(result.tools.length).toBeGreaterThan(0);
  });

  it("get_category_tools returns error for invalid category", async () => {
    const result = await routerTools.get_category_tools.handler({ 
      category: "nonexistent" 
    });
    expect(result.error).toBeDefined();
  });

  it("execute_tool runs valid tool", async () => {
    const result = await routerTools.execute_tool.handler({
      tool_name: "export_gerber",
      params: { output_dir: "/tmp/test" }
    });
    expect(result.error).toBeUndefined();
  });

  it("execute_tool returns error for invalid tool", async () => {
    const result = await routerTools.execute_tool.handler({
      tool_name: "nonexistent_tool",
      params: {}
    });
    expect(result.error).toBeDefined();
  });
});

Integration Test

Test with an actual MCP client:

# Using MCP Inspector
npx @modelcontextprotocol/inspector your-server

# Or with Claude Desktop - add to config
{
  "mcpServers": {
    "your-server": {
      "command": "node",
      "args": ["./dist/index.js"]
    }
  }
}

Advanced: Combining with Anthropic's Tool Search Tool

If you control the client (building your own app with Claude API), you can use Anthropic's native Tool Search Tool for even better results:

import anthropic

client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-sonnet-4-5-20250929",
    max_tokens=4096,
    betas=["advanced-tool-use-2025-11-20"],
    tools=[
        # Tool search tool for dynamic discovery
        {
            "type": "tool_search_tool_regex_20251119",
            "name": "tool_search_tool_regex"
        },
        # Always-loaded tools
        {
            "name": "create_project",
            "description": "Create new project",
            "input_schema": {...},
            "defer_loading": False  # Always visible
        },
        # Deferred tools - only loaded when searched
        {
            "name": "export_gerber",
            "description": "Export Gerber files for PCB fabrication",
            "input_schema": {...},
            "defer_loading": True  # Hidden until searched
        },
        # ... 100 more deferred tools
    ],
    messages=[{"role": "user", "content": "Export gerbers for my board"}]
)

This approach works at the API level rather than MCP level, giving you:

  • Native search with regex or BM25
  • Automatic tool expansion
  • Works with any MCP client

See Tool Search Tool Documentation for details.


References

MCP Documentation

Anthropic Advanced Tool Use

Example Implementations


Checklist

Before shipping your router-based MCP server:

  • Direct tools cover 80% of common use cases
  • Categories have clear, descriptive names
  • Category descriptions explain what's included
  • All tools have good descriptions
  • search_tools finds tools by common keywords
  • execute_tool handles errors gracefully
  • Unit tests pass for registry and router
  • Tested with actual MCP client (Claude Desktop, Cline, etc.)
  • README documents the router pattern for users

Summary

Before After
100 tools visible 15-18 tools visible
~60K+ tokens consumed ~10K tokens consumed
Tool selection confusion Clear categories
Context starvation Room for conversation

The router pattern gives you unlimited tool capacity while keeping Claude focused and efficient.