Three detailed implementation plans with TDD, exact code, and step-by-step tasks: - Image URL fetching: 4 tasks (schema, Zod, service, route) - Authentication: 9 tasks (tables, service, middleware, routes, frontend) - MCP server: 9 tasks (SDK, tools, resources, Hono integration) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
31 KiB
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
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:
import { describe, expect, test, beforeEach } from "bun:test";
import { createTestDb } from "../helpers/db";
import { registerItemTools } from "../../src/server/mcp/tools/items";
let db: ReturnType<typeof createTestDb>;
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:
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<ToolResult> {
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<ToolResult> {
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<ToolResult> {
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<ToolResult> {
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<ToolResult> {
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
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:
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<ToolResult> {
return textResult(getAllCategories(db));
},
async create_category(args: {
name: string;
icon?: string;
}): Promise<ToolResult> {
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
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:
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:
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<ToolResult> {
return textResult(getAllThreads(db, args.includeResolved));
},
async get_thread(args: { id: number }): Promise<ToolResult> {
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<ToolResult> {
return textResult(createThread(db, args));
},
async resolve_thread(args: {
threadId: number;
candidateId: number;
}): Promise<ToolResult> {
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<ToolResult> {
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<ToolResult> {
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<ToolResult> {
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
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:
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<ToolResult> {
return textResult(getAllSetups(db));
},
async get_setup(args: { id: number }): Promise<ToolResult> {
const setup = getSetupWithItems(db, args.id);
if (!setup) return errorResult("Setup not found");
return textResult(setup);
},
async create_setup(args: { name: string }): Promise<ToolResult> {
return textResult(createSetup(db, args));
},
async update_setup(args: {
id: number;
name?: string;
itemIds?: number[];
}): Promise<ToolResult> {
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:
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<ToolResult> {
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
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:
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<string, number> = {};
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
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:
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<string, SSEServerTransport>();
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:
import { mcpRoutes } from "./mcp/index.ts";
Add conditional MCP route registration after the other routes but before the static file serving:
// 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
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:
## 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": "<your-api-key>"
}
}
}
}
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"gearbox": {
"type": "sse",
"url": "http://localhost:3000/mcp/sse",
"headers": {
"X-API-Key": "<your-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: <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:
- GearBox tools appear in the tool list
list_itemsreturns current itemscreate_threadcreates a research threadadd_candidateadds candidates to threadsresolve_threadresolves and creates itemscollection-summaryresource provides context