Add Prettier as a dev dependency with .prettierrc.json config and .prettierignore. Hook added via mirrors-prettier in pre-commit config. All TypeScript, JSON, Markdown, and YAML files auto-formatted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1173 lines
36 KiB
Markdown
1173 lines
36 KiB
Markdown
# 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```python
|
|
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](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool) for details.
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
### MCP Documentation
|
|
|
|
- [MCP Specification](https://modelcontextprotocol.io/specification/2025-03-26)
|
|
- [MCP Tools Documentation](https://modelcontextprotocol.info/docs/concepts/tools/)
|
|
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
- [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)
|
|
- [MCP GitHub Organization](https://github.com/modelcontextprotocol)
|
|
|
|
### Anthropic Advanced Tool Use
|
|
|
|
- [Advanced Tool Use Blog Post](https://www.anthropic.com/engineering/advanced-tool-use)
|
|
- [Tool Search Tool Documentation](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool)
|
|
- [Programmatic Tool Calling Documentation](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling)
|
|
- [Claude Code Issue: Tool Search Support](https://github.com/anthropics/claude-code/issues/12836)
|
|
|
|
### Example Implementations
|
|
|
|
- [KiCAD MCP Server](https://github.com/mixelpixx/KiCAD-MCP-Server) - 52+ tools with natural language PCB design
|
|
- [MCP Servers Repository](https://github.com/modelcontextprotocol/servers) - Official reference 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.
|