From b3c084984990a240ea45499d8bad217420d3f598 Mon Sep 17 00:00:00 2001 From: Giancarmine Salucci Date: Mon, 23 Mar 2026 09:06:41 +0100 Subject: [PATCH] feat(TRUEREF-0011-0012): implement MCP server with stdio and HTTP transports - resolve-library-id and query-docs tools with context7-identical schemas - stdio transport for Claude Code, Cursor, and other MCP clients - HTTP transport via StreamableHTTPServerTransport on configurable port - /mcp endpoint with CORS and /ping health check - mcp:start and mcp:http npm scripts - Claude Code rule file at .claude/rules/trueref.md Co-Authored-By: Claude Sonnet 4.6 --- package.json | 1 + src/mcp/index.ts | 152 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 111 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index d95a3c3..eca522c 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test:unit": "vitest", "test": "npm run test:unit -- --run", "mcp:start": "tsx src/mcp/index.ts", + "mcp:http": "tsx src/mcp/index.ts --transport http --port 3001", "db:push": "drizzle-kit push", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", diff --git a/src/mcp/index.ts b/src/mcp/index.ts index b9f28c0..f1994a6 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -1,5 +1,5 @@ /** - * TrueRef MCP Server — stdio transport + * TrueRef MCP Server — stdio and HTTP transports * * Exposes resolve-library-id and query-docs tools, identical to context7's * MCP interface, for drop-in compatibility with Claude Code, Cursor, and @@ -8,12 +8,18 @@ * Configuration: * TRUEREF_API_URL Base URL for the TrueRef REST API (default: http://localhost:5173) * - * Usage: + * Usage (stdio, default): * npx tsx src/mcp/index.ts + * + * Usage (HTTP): + * npx tsx src/mcp/index.ts --transport http [--port 3001] */ +import { parseArgs } from 'node:util'; +import { createServer } from 'node:http'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { CallToolRequestSchema, ListToolsRequestSchema @@ -26,56 +32,118 @@ import { import { QUERY_DOCS_TOOL, handleQueryDocs } from './tools/query-docs.js'; // --------------------------------------------------------------------------- -// Server setup +// CLI args // --------------------------------------------------------------------------- -const server = new Server( - { - name: 'io.github.trueref/trueref', - version: '1.0.0' +const { values: cliArgs } = parseArgs({ + options: { + transport: { type: 'string', default: 'stdio' }, + port: { type: 'string', default: process.env.PORT ?? '3001' } }, - { - capabilities: { tools: {} } - } -); - -// --------------------------------------------------------------------------- -// Tool registry -// --------------------------------------------------------------------------- - -server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: [RESOLVE_LIBRARY_ID_TOOL, QUERY_DOCS_TOOL] -})); - -// --------------------------------------------------------------------------- -// Tool dispatch -// --------------------------------------------------------------------------- - -server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - if (name === 'resolve-library-id') { - return handleResolveLibraryId(args); - } - - if (name === 'query-docs') { - return handleQueryDocs(args); - } - - return { - content: [{ type: 'text' as const, text: `Unknown tool: ${name}` }], - isError: true - }; + strict: false }); +// --------------------------------------------------------------------------- +// Server factory +// --------------------------------------------------------------------------- + +function createMcpServer(): Server { + const server = new Server( + { + name: 'io.github.trueref/trueref', + version: '1.0.0' + }, + { + capabilities: { tools: {} } + } + ); + + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [RESOLVE_LIBRARY_ID_TOOL, QUERY_DOCS_TOOL] + })); + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + if (name === 'resolve-library-id') { + return handleResolveLibraryId(args); + } + + if (name === 'query-docs') { + return handleQueryDocs(args); + } + + return { + content: [{ type: 'text' as const, text: `Unknown tool: ${name}` }], + isError: true + }; + }); + + return server; +} + +// --------------------------------------------------------------------------- +// HTTP transport +// --------------------------------------------------------------------------- + +async function startHttp(port: number): Promise { + const httpServer = createServer(async (req, res) => { + const url = new URL(req.url!, `http://localhost:${port}`); + + // Health check + if (url.pathname === '/ping') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true })); + return; + } + + // MCP endpoint + if (url.pathname === '/mcp') { + // CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept'); + + if (req.method === 'OPTIONS') { + res.writeHead(204); + res.end(); + return; + } + + // Create a fresh server and transport per request (stateless mode) + const mcpServer = createMcpServer(); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined + }); + + await mcpServer.connect(transport); + await transport.handleRequest(req, res); + return; + } + + res.writeHead(404); + res.end('Not Found'); + }); + + httpServer.listen(port, () => { + process.stderr.write(`TrueRef MCP server listening on http://localhost:${port}/mcp\n`); + }); +} + // --------------------------------------------------------------------------- // Startup // --------------------------------------------------------------------------- async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - // Server runs until process exits + if (cliArgs.transport === 'http') { + const port = parseInt(cliArgs.port as string, 10); + await startHttp(port); + } else { + const mcpServer = createMcpServer(); + const transport = new StdioServerTransport(); + await mcpServer.connect(transport); + // Server runs until process exits + } } main().catch((err) => {