# MCP Server Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build a built-in MCP server that exposes GearBox tools for managing gear collections via Claude Code and Claude Desktop, with workflow guidance emphasizing research threads. **Architecture:** MCP server runs inside the Hono process using the `@modelcontextprotocol/sdk` package with SSE/Streamable HTTP transport at `/mcp`. Tools call service functions directly (not via HTTP). Authenticated via `X-API-Key` header. Enabled by default, disabled with `GEARBOX_MCP=false`. **Tech Stack:** `@modelcontextprotocol/sdk`, Hono, existing GearBox services, Zod for tool input schemas --- ## File Structure | Action | Path | Responsibility | |--------|------|----------------| | Create | `src/server/mcp/index.ts` | MCP server setup, tool/resource registration, Hono route handler | | Create | `src/server/mcp/tools/items.ts` | Item CRUD tool definitions and handlers | | Create | `src/server/mcp/tools/categories.ts` | Category tool definitions and handlers | | Create | `src/server/mcp/tools/threads.ts` | Thread + candidate tool definitions and handlers | | Create | `src/server/mcp/tools/setups.ts` | Setup tool definitions and handlers | | Create | `src/server/mcp/tools/images.ts` | Image URL fetch tool definition and handler | | Create | `src/server/mcp/resources/collection.ts` | Collection summary resource | | Modify | `src/server/index.ts` | Mount MCP route conditionally | | Create | `tests/mcp/tools.test.ts` | MCP tool handler tests | --- ### Task 1: Install MCP SDK **Files:** - Modify: `package.json` - [ ] **Step 1: Install the MCP SDK** Run: `bun add @modelcontextprotocol/sdk` - [ ] **Step 2: Verify installation** Run: `bun run build` Expected: Build succeeds. If there are type issues, check the SDK version supports Bun. - [ ] **Step 3: Commit** ```bash git add package.json bun.lock git commit -m "chore: install @modelcontextprotocol/sdk" ``` --- ### Task 2: Create Item Tools **Files:** - Create: `src/server/mcp/tools/items.ts` - Create: `tests/mcp/tools.test.ts` - [ ] **Step 1: Write failing test for item tools** Create `tests/mcp/tools.test.ts`: ```typescript import { describe, expect, test, beforeEach } from "bun:test"; import { createTestDb } from "../helpers/db"; import { registerItemTools } from "../../src/server/mcp/tools/items"; let db: ReturnType; beforeEach(() => { db = createTestDb(); }); describe("item tools", () => { test("list_items returns all items", async () => { const tools = registerItemTools(db); const result = await tools.list_items({}); const items = JSON.parse(result.content[0].text); expect(Array.isArray(items)).toBe(true); }); test("create_item creates an item", async () => { const tools = registerItemTools(db); const result = await tools.create_item({ name: "Test Item", categoryId: 1, weightGrams: 100, priceCents: 2500, }); const item = JSON.parse(result.content[0].text); expect(item.name).toBe("Test Item"); expect(item.id).toBeDefined(); }); test("get_item retrieves an item by ID", async () => { const tools = registerItemTools(db); const created = await tools.create_item({ name: "Test", categoryId: 1, }); const id = JSON.parse(created.content[0].text).id; const result = await tools.get_item({ id }); const item = JSON.parse(result.content[0].text); expect(item.name).toBe("Test"); }); test("delete_item removes an item", async () => { const tools = registerItemTools(db); const created = await tools.create_item({ name: "To Delete", categoryId: 1, }); const id = JSON.parse(created.content[0].text).id; const result = await tools.delete_item({ id }); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(true); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `bun test tests/mcp/tools.test.ts` Expected: FAIL — module not found. - [ ] **Step 3: Implement item tools** Create `src/server/mcp/tools/items.ts`: ```typescript import type { db as prodDb } from "../../../db/index.ts"; import { getAllItems, getItemById, createItem, updateItem, deleteItem, } from "../../services/item.service.ts"; type Db = typeof prodDb; interface ToolResult { content: Array<{ type: "text"; text: string }>; } function textResult(data: unknown): ToolResult { return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } function errorResult(message: string): ToolResult { return { content: [{ type: "text", text: JSON.stringify({ error: message }) }] }; } export function registerItemTools(db: Db) { return { async list_items(args: { categoryId?: number }): Promise { const items = getAllItems(db); if (args.categoryId) { return textResult(items.filter((i) => i.categoryId === args.categoryId)); } return textResult(items); }, async get_item(args: { id: number }): Promise { const item = getItemById(db, args.id); if (!item) return errorResult("Item not found"); return textResult(item); }, async create_item(args: { name: string; categoryId: number; weightGrams?: number; priceCents?: number; notes?: string; productUrl?: string; imageFilename?: string; imageSourceUrl?: string; }): Promise { const item = createItem(db, args); return textResult(item); }, async update_item(args: { id: number; name?: string; categoryId?: number; weightGrams?: number; priceCents?: number; notes?: string; productUrl?: string; imageFilename?: string; imageSourceUrl?: string; }): Promise { const { id, ...data } = args; const item = updateItem(db, id, data); if (!item) return errorResult("Item not found"); return textResult(item); }, async delete_item(args: { id: number }): Promise { const item = deleteItem(db, args.id); if (!item) return errorResult("Item not found"); return textResult({ success: true, deleted: item.name }); }, }; } export const itemToolDefinitions = [ { name: "list_items", description: "List all items in the gear collection. Optionally filter by category.", inputSchema: { type: "object" as const, properties: { categoryId: { type: "number", description: "Filter by category ID", }, }, }, }, { name: "get_item", description: "Get details of a specific item by ID.", inputSchema: { type: "object" as const, properties: { id: { type: "number", description: "Item ID" }, }, required: ["id"], }, }, { name: "create_item", description: "Add a new item to the gear collection. Use this for items you've already decided on. For items you're still researching, use create_thread instead to compare candidates.", inputSchema: { type: "object" as const, properties: { name: { type: "string", description: "Item name" }, categoryId: { type: "number", description: "Category ID" }, weightGrams: { type: "number", description: "Weight in grams" }, priceCents: { type: "number", description: "Price in cents (e.g. 2500 = $25.00)", }, notes: { type: "string", description: "Notes about the item" }, productUrl: { type: "string", description: "URL to product page" }, }, required: ["name", "categoryId"], }, }, { name: "update_item", description: "Update an existing item's details.", inputSchema: { type: "object" as const, properties: { id: { type: "number", description: "Item ID" }, name: { type: "string" }, categoryId: { type: "number" }, weightGrams: { type: "number" }, priceCents: { type: "number" }, notes: { type: "string" }, productUrl: { type: "string" }, }, required: ["id"], }, }, { name: "delete_item", description: "Remove an item from the collection.", inputSchema: { type: "object" as const, properties: { id: { type: "number", description: "Item ID" }, }, required: ["id"], }, }, ]; ``` - [ ] **Step 4: Run tests** Run: `bun test tests/mcp/tools.test.ts` Expected: All tests pass. - [ ] **Step 5: Commit** ```bash git add src/server/mcp/tools/items.ts tests/mcp/tools.test.ts git commit -m "feat: add MCP item tools with tests" ``` --- ### Task 3: Create Category Tools **Files:** - Create: `src/server/mcp/tools/categories.ts` - [ ] **Step 1: Implement category tools** Create `src/server/mcp/tools/categories.ts`: ```typescript import type { db as prodDb } from "../../../db/index.ts"; import { getAllCategories, createCategory, } from "../../services/category.service.ts"; type Db = typeof prodDb; interface ToolResult { content: Array<{ type: "text"; text: string }>; } function textResult(data: unknown): ToolResult { return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } export function registerCategoryTools(db: Db) { return { async list_categories(): Promise { return textResult(getAllCategories(db)); }, async create_category(args: { name: string; icon?: string; }): Promise { const category = createCategory(db, args); return textResult(category); }, }; } export const categoryToolDefinitions = [ { name: "list_categories", description: "List all gear categories.", inputSchema: { type: "object" as const, properties: {} }, }, { name: "create_category", description: "Create a new category for organizing gear.", inputSchema: { type: "object" as const, properties: { name: { type: "string", description: "Category name" }, icon: { type: "string", description: "Lucide icon name (e.g. 'tent', 'bike', 'monitor'). Defaults to 'package'.", }, }, required: ["name"], }, }, ]; ``` - [ ] **Step 2: Commit** ```bash git add src/server/mcp/tools/categories.ts git commit -m "feat: add MCP category tools" ``` --- ### Task 4: Create Thread and Candidate Tools **Files:** - Create: `src/server/mcp/tools/threads.ts` - [ ] **Step 1: Write failing test for thread tools** Add to `tests/mcp/tools.test.ts`: ```typescript import { registerThreadTools } from "../../src/server/mcp/tools/threads"; describe("thread tools", () => { test("create_thread starts a research thread", async () => { const tools = registerThreadTools(db); const result = await tools.create_thread({ name: "Best handlebar bag", categoryId: 1, }); const thread = JSON.parse(result.content[0].text); expect(thread.name).toBe("Best handlebar bag"); expect(thread.status).toBe("active"); }); test("add_candidate adds a candidate to thread", async () => { const tools = registerThreadTools(db); const threadResult = await tools.create_thread({ name: "Handlebar bags", categoryId: 1, }); const threadId = JSON.parse(threadResult.content[0].text).id; const result = await tools.add_candidate({ threadId, name: "Apidura Racing", categoryId: 1, weightGrams: 130, priceCents: 6500, pros: "Lightweight, aerodynamic", cons: "Small capacity", }); const candidate = JSON.parse(result.content[0].text); expect(candidate.name).toBe("Apidura Racing"); }); test("resolve_thread picks a winner and creates item", async () => { const tools = registerThreadTools(db); const threadResult = await tools.create_thread({ name: "Handlebar bags", categoryId: 1, }); const threadId = JSON.parse(threadResult.content[0].text).id; const candidateResult = await tools.add_candidate({ threadId, name: "Winner Bag", categoryId: 1, priceCents: 5000, }); const candidateId = JSON.parse(candidateResult.content[0].text).id; const result = await tools.resolve_thread({ threadId, candidateId }); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(true); expect(response.item.name).toBe("Winner Bag"); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: `bun test tests/mcp/tools.test.ts` Expected: FAIL — module not found for threads tools. - [ ] **Step 3: Implement thread tools** Create `src/server/mcp/tools/threads.ts`: ```typescript import type { db as prodDb } from "../../../db/index.ts"; import { createThread, getAllThreads, getThreadWithCandidates, updateThread, deleteThread, createCandidate, updateCandidate, deleteCandidate, resolveThread, } from "../../services/thread.service.ts"; type Db = typeof prodDb; interface ToolResult { content: Array<{ type: "text"; text: string }>; } function textResult(data: unknown): ToolResult { return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } function errorResult(message: string): ToolResult { return { content: [{ type: "text", text: JSON.stringify({ error: message }) }] }; } export function registerThreadTools(db: Db) { return { async list_threads(args: { includeResolved?: boolean; }): Promise { return textResult(getAllThreads(db, args.includeResolved)); }, async get_thread(args: { id: number }): Promise { const thread = getThreadWithCandidates(db, args.id); if (!thread) return errorResult("Thread not found"); return textResult(thread); }, async create_thread(args: { name: string; categoryId: number; }): Promise { return textResult(createThread(db, args)); }, async resolve_thread(args: { threadId: number; candidateId: number; }): Promise { const result = resolveThread(db, args.threadId, args.candidateId); if (!result.success) return errorResult(result.error ?? "Resolution failed"); return textResult(result); }, async add_candidate(args: { threadId: number; name: string; categoryId: number; weightGrams?: number; priceCents?: number; notes?: string; productUrl?: string; imageFilename?: string; pros?: string; cons?: string; }): Promise { const { threadId, ...data } = args; return textResult(createCandidate(db, threadId, data)); }, async update_candidate(args: { id: number; name?: string; weightGrams?: number; priceCents?: number; notes?: string; productUrl?: string; pros?: string; cons?: string; }): Promise { const { id, ...data } = args; const candidate = updateCandidate(db, id, data); if (!candidate) return errorResult("Candidate not found"); return textResult(candidate); }, async remove_candidate(args: { id: number }): Promise { const candidate = deleteCandidate(db, args.id); if (!candidate) return errorResult("Candidate not found"); return textResult({ success: true, deleted: candidate.name }); }, }; } export const threadToolDefinitions = [ { name: "list_threads", description: "List all research threads. Threads are the recommended way to evaluate gear purchases — create a thread, add candidates, compare them, then resolve to pick a winner.", inputSchema: { type: "object" as const, properties: { includeResolved: { type: "boolean", description: "Include resolved threads (default: false)", }, }, }, }, { name: "get_thread", description: "Get a thread with all its candidates and comparison data.", inputSchema: { type: "object" as const, properties: { id: { type: "number", description: "Thread ID" }, }, required: ["id"], }, }, { name: "create_thread", description: "Start a new research thread for evaluating a gear purchase. This is the preferred workflow: create a thread describing what you need, add candidate products, compare specs/weight/price, then resolve when you've decided.", inputSchema: { type: "object" as const, properties: { name: { type: "string", description: "Thread name (e.g. 'Best handlebar bag for bikepacking')", }, categoryId: { type: "number", description: "Category ID" }, }, required: ["name", "categoryId"], }, }, { name: "resolve_thread", description: "Resolve a thread by picking the winning candidate. This adds the winner to your collection as a new item and marks the thread as resolved.", inputSchema: { type: "object" as const, properties: { threadId: { type: "number", description: "Thread ID" }, candidateId: { type: "number", description: "ID of the winning candidate", }, }, required: ["threadId", "candidateId"], }, }, { name: "add_candidate", description: "Add a candidate product to a research thread. Include weight, price, pros, cons, and optionally an image URL.", inputSchema: { type: "object" as const, properties: { threadId: { type: "number", description: "Thread ID to add to" }, name: { type: "string", description: "Product name" }, categoryId: { type: "number", description: "Category ID" }, weightGrams: { type: "number", description: "Weight in grams" }, priceCents: { type: "number", description: "Price in cents (e.g. 6500 = $65.00)", }, notes: { type: "string", description: "Notes" }, productUrl: { type: "string", description: "URL to product page" }, pros: { type: "string", description: "Pros / advantages of this candidate", }, cons: { type: "string", description: "Cons / disadvantages of this candidate", }, }, required: ["threadId", "name", "categoryId"], }, }, { name: "update_candidate", description: "Update a candidate's details — weight, price, pros, cons, etc.", inputSchema: { type: "object" as const, properties: { id: { type: "number", description: "Candidate ID" }, name: { type: "string" }, weightGrams: { type: "number" }, priceCents: { type: "number" }, notes: { type: "string" }, productUrl: { type: "string" }, pros: { type: "string" }, cons: { type: "string" }, }, required: ["id"], }, }, { name: "remove_candidate", description: "Remove a candidate from a research thread.", inputSchema: { type: "object" as const, properties: { id: { type: "number", description: "Candidate ID" }, }, required: ["id"], }, }, ]; ``` - [ ] **Step 4: Run tests** Run: `bun test tests/mcp/tools.test.ts` Expected: All tests pass. - [ ] **Step 5: Commit** ```bash git add src/server/mcp/tools/threads.ts tests/mcp/tools.test.ts git commit -m "feat: add MCP thread and candidate tools with tests" ``` --- ### Task 5: Create Setup and Image Tools **Files:** - Create: `src/server/mcp/tools/setups.ts` - Create: `src/server/mcp/tools/images.ts` - [ ] **Step 1: Implement setup tools** Create `src/server/mcp/tools/setups.ts`: ```typescript import type { db as prodDb } from "../../../db/index.ts"; import { getAllSetups, getSetupWithItems, createSetup, updateSetup, syncSetupItems, } from "../../services/setup.service.ts"; type Db = typeof prodDb; interface ToolResult { content: Array<{ type: "text"; text: string }>; } function textResult(data: unknown): ToolResult { return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } function errorResult(message: string): ToolResult { return { content: [{ type: "text", text: JSON.stringify({ error: message }) }] }; } export function registerSetupTools(db: Db) { return { async list_setups(): Promise { return textResult(getAllSetups(db)); }, async get_setup(args: { id: number }): Promise { const setup = getSetupWithItems(db, args.id); if (!setup) return errorResult("Setup not found"); return textResult(setup); }, async create_setup(args: { name: string }): Promise { return textResult(createSetup(db, args)); }, async update_setup(args: { id: number; name?: string; itemIds?: number[]; }): Promise { const { id, name, itemIds } = args; if (name) { const updated = updateSetup(db, id, { name }); if (!updated) return errorResult("Setup not found"); } if (itemIds) { syncSetupItems(db, id, itemIds); } const setup = getSetupWithItems(db, id); return textResult(setup); }, }; } export const setupToolDefinitions = [ { name: "list_setups", description: "List all gear setups (named configurations of items for different trips/activities).", inputSchema: { type: "object" as const, properties: {} }, }, { name: "get_setup", description: "Get a setup with all its items, total weight, and total cost.", inputSchema: { type: "object" as const, properties: { id: { type: "number", description: "Setup ID" }, }, required: ["id"], }, }, { name: "create_setup", description: "Create a new gear setup.", inputSchema: { type: "object" as const, properties: { name: { type: "string", description: "Setup name (e.g. 'Summer Bikepacking')", }, }, required: ["name"], }, }, { name: "update_setup", description: "Update a setup's name and/or items. Pass itemIds to replace all items in the setup.", inputSchema: { type: "object" as const, properties: { id: { type: "number", description: "Setup ID" }, name: { type: "string", description: "New name" }, itemIds: { type: "array", items: { type: "number" }, description: "Array of item IDs to include in the setup", }, }, required: ["id"], }, }, ]; ``` - [ ] **Step 2: Implement image tools** Create `src/server/mcp/tools/images.ts`: ```typescript import { fetchImageFromUrl } from "../../services/image.service.ts"; interface ToolResult { content: Array<{ type: "text"; text: string }>; } function textResult(data: unknown): ToolResult { return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } function errorResult(message: string): ToolResult { return { content: [{ type: "text", text: JSON.stringify({ error: message }) }] }; } export function registerImageTools() { return { async upload_image_from_url(args: { url: string }): Promise { try { const result = await fetchImageFromUrl(args.url); return textResult(result); } catch (err) { return errorResult((err as Error).message); } }, }; } export const imageToolDefinitions = [ { name: "upload_image_from_url", description: "Fetch an image from a URL and save it locally. Returns the filename to use when creating/updating items or candidates.", inputSchema: { type: "object" as const, properties: { url: { type: "string", description: "URL of the image to fetch", }, }, required: ["url"], }, }, ]; ``` - [ ] **Step 3: Commit** ```bash git add src/server/mcp/tools/setups.ts src/server/mcp/tools/images.ts git commit -m "feat: add MCP setup and image tools" ``` --- ### Task 6: Create Collection Summary Resource **Files:** - Create: `src/server/mcp/resources/collection.ts` - [ ] **Step 1: Implement collection resource** Create `src/server/mcp/resources/collection.ts`: ```typescript import type { db as prodDb } from "../../../db/index.ts"; import { getGlobalTotals } from "../../services/totals.service.ts"; import { getAllThreads } from "../../services/thread.service.ts"; import { getAllSetups } from "../../services/setup.service.ts"; import { getAllCategories } from "../../services/category.service.ts"; import { getAllItems } from "../../services/item.service.ts"; type Db = typeof prodDb; export function getCollectionSummary(db: Db) { const totals = getGlobalTotals(db); const activeThreads = getAllThreads(db, false); const setups = getAllSetups(db); const categories = getAllCategories(db); const items = getAllItems(db); // Count items per category const itemsByCategory: Record = {}; for (const item of items) { const name = item.categoryName; itemsByCategory[name] = (itemsByCategory[name] ?? 0) + 1; } return { overview: { totalItems: totals.itemCount, totalWeightGrams: totals.totalWeight, totalCostCents: totals.totalCost, categoryCount: categories.length, setupCount: setups.length, activeThreadCount: activeThreads.length, }, itemsByCategory, activeThreads: activeThreads.map((t) => ({ id: t.id, name: t.name, candidateCount: t.candidateCount, category: t.categoryName, })), }; } ``` - [ ] **Step 2: Commit** ```bash git add src/server/mcp/resources/collection.ts git commit -m "feat: add collection summary MCP resource" ``` --- ### Task 7: Create MCP Server Entry Point and Hono Integration **Files:** - Create: `src/server/mcp/index.ts` - Modify: `src/server/index.ts` - [ ] **Step 1: Implement MCP server with Hono SSE bridge** Create `src/server/mcp/index.ts`: ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { Hono } from "hono"; import { db as prodDb } from "../../db/index.ts"; import { verifyApiKey } from "../services/auth.service.ts"; import { itemToolDefinitions, registerItemTools } from "./tools/items.ts"; import { categoryToolDefinitions, registerCategoryTools, } from "./tools/categories.ts"; import { threadToolDefinitions, registerThreadTools, } from "./tools/threads.ts"; import { setupToolDefinitions, registerSetupTools } from "./tools/setups.ts"; import { imageToolDefinitions, registerImageTools } from "./tools/images.ts"; import { getCollectionSummary } from "./resources/collection.ts"; const app = new Hono(); // Store active transports for SSE connections const transports = new Map(); function createMcpServer(db: typeof prodDb) { const server = new McpServer({ name: "GearBox", version: "1.0.0", }); // Register all tools const itemTools = registerItemTools(db); const categoryTools = registerCategoryTools(db); const threadTools = registerThreadTools(db); const setupTools = registerSetupTools(db); const imageTools = registerImageTools(); // Items for (const def of itemToolDefinitions) { server.tool(def.name, def.description, def.inputSchema, async (args) => { const handler = itemTools[def.name as keyof typeof itemTools]; return handler(args as any); }); } // Categories for (const def of categoryToolDefinitions) { server.tool(def.name, def.description, def.inputSchema, async (args) => { const handler = categoryTools[def.name as keyof typeof categoryTools]; return handler(args as any); }); } // Threads for (const def of threadToolDefinitions) { server.tool(def.name, def.description, def.inputSchema, async (args) => { const handler = threadTools[def.name as keyof typeof threadTools]; return handler(args as any); }); } // Setups for (const def of setupToolDefinitions) { server.tool(def.name, def.description, def.inputSchema, async (args) => { const handler = setupTools[def.name as keyof typeof setupTools]; return handler(args as any); }); } // Images for (const def of imageToolDefinitions) { server.tool(def.name, def.description, def.inputSchema, async (args) => { const handler = imageTools[def.name as keyof typeof imageTools]; return handler(args as any); }); } // Collection summary resource server.resource( "collection-summary", "gearbox://collection-summary", { description: "Overview of the gear collection: totals, items per category, active research threads, and setups.", mimeType: "application/json", }, async () => ({ contents: [ { uri: "gearbox://collection-summary", mimeType: "application/json", text: JSON.stringify(getCollectionSummary(db), null, 2), }, ], }), ); return server; } // SSE endpoint for MCP connections app.get("/sse", async (c) => { // Auth check const apiKey = c.req.header("X-API-Key"); if (apiKey) { const db = c.get("db") ?? prodDb; const valid = await verifyApiKey(db, apiKey); if (!valid) { return c.json({ error: "Invalid API key" }, 401); } } const db = c.get("db") ?? prodDb; const server = createMcpServer(db); const transport = new SSEServerTransport("/mcp/messages", c.res); const sessionId = transport.sessionId; transports.set(sessionId, transport); c.header("Content-Type", "text/event-stream"); c.header("Cache-Control", "no-cache"); c.header("Connection", "keep-alive"); await server.connect(transport); // Clean up on disconnect c.req.raw.signal.addEventListener("abort", () => { transports.delete(sessionId); }); return c.body(null); }); // Message endpoint for MCP app.post("/messages", async (c) => { const sessionId = c.req.query("sessionId"); if (!sessionId) { return c.json({ error: "Missing sessionId" }, 400); } const transport = transports.get(sessionId); if (!transport) { return c.json({ error: "Session not found" }, 404); } const body = await c.req.json(); await transport.handlePostMessage(body); return c.json({ ok: true }); }); export { app as mcpRoutes }; ``` **Important note:** The exact SSE/transport API may differ based on the MCP SDK version installed. The implementation above follows the SDK's documented SSE transport pattern. During implementation, check the actual SDK exports and adjust if needed. The newer MCP SDK versions may use `StreamableHTTPServerTransport` instead of `SSEServerTransport` — use whichever is available. - [ ] **Step 2: Mount MCP routes in server index** In `src/server/index.ts`, add import: ```typescript import { mcpRoutes } from "./mcp/index.ts"; ``` Add conditional MCP route registration after the other routes but before the static file serving: ```typescript // MCP server (enabled by default, disable with GEARBOX_MCP=false) if (process.env.GEARBOX_MCP !== "false") { app.route("/mcp", mcpRoutes); } ``` - [ ] **Step 3: Run all tests** Run: `bun test` Expected: All tests pass. - [ ] **Step 4: Commit** ```bash git add src/server/mcp/index.ts src/server/index.ts git commit -m "feat: add MCP server with SSE transport at /mcp" ``` --- ### Task 8: Add MCP Client Configuration Examples **Files:** - Modify: `CLAUDE.md` (add MCP configuration section) - [ ] **Step 1: Add MCP configuration section to CLAUDE.md** Add a new section to `CLAUDE.md`: ```markdown ## MCP Server GearBox includes a built-in MCP server for integration with Claude Code and Claude Desktop. ### Configuration **Claude Code** (`.claude/settings.json`): ```json { "mcpServers": { "gearbox": { "type": "sse", "url": "http://localhost:3000/mcp/sse", "headers": { "X-API-Key": "" } } } } ``` **Claude Desktop** (`claude_desktop_config.json`): ```json { "mcpServers": { "gearbox": { "type": "sse", "url": "http://localhost:3000/mcp/sse", "headers": { "X-API-Key": "" } } } } ``` ### Disabling Set `GEARBOX_MCP=false` environment variable to disable the MCP server. ``` - [ ] **Step 2: Commit** ```bash git add CLAUDE.md git commit -m "docs: add MCP server configuration to CLAUDE.md" ``` --- ### Task 9: Full Test Suite and Manual Verification **Files:** None (verification only) - [ ] **Step 1: Run all tests** Run: `bun test` Expected: All tests pass. - [ ] **Step 2: Run linter** Run: `bun run lint` Expected: No errors. - [ ] **Step 3: Manual verification — start the server** Run: `bun run dev:server` Check that the server starts without errors and logs no MCP-related issues. - [ ] **Step 4: Test SSE endpoint** Run: `curl -N -H "X-API-Key: " http://localhost:3000/mcp/sse` Expected: SSE connection opens, receives initial MCP handshake. - [ ] **Step 5: Test with Claude Code** Add MCP server config to `.claude/settings.json` with a valid API key. Start a new Claude Code session. Verify: 1. GearBox tools appear in the tool list 2. `list_items` returns current items 3. `create_thread` creates a research thread 4. `add_candidate` adds candidates to threads 5. `resolve_thread` resolves and creates items 6. `collection-summary` resource provides context