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 <noreply@anthropic.com>
This commit is contained in:
Giancarmine Salucci
2026-03-23 09:06:41 +01:00
parent 21f6acbfa3
commit b3c0849849
2 changed files with 111 additions and 42 deletions

View File

@@ -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",

View File

@@ -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,10 +32,23 @@ import {
import { QUERY_DOCS_TOOL, handleQueryDocs } from './tools/query-docs.js';
// ---------------------------------------------------------------------------
// Server setup
// CLI args
// ---------------------------------------------------------------------------
const server = new Server(
const { values: cliArgs } = parseArgs({
options: {
transport: { type: 'string', default: 'stdio' },
port: { type: 'string', default: process.env.PORT ?? '3001' }
},
strict: false
});
// ---------------------------------------------------------------------------
// Server factory
// ---------------------------------------------------------------------------
function createMcpServer(): Server {
const server = new Server(
{
name: 'io.github.trueref/trueref',
version: '1.0.0'
@@ -37,21 +56,13 @@ const server = new Server(
{
capabilities: { tools: {} }
}
);
);
// ---------------------------------------------------------------------------
// Tool registry
// ---------------------------------------------------------------------------
server.setRequestHandler(ListToolsRequestSchema, async () => ({
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [RESOLVE_LIBRARY_ID_TOOL, QUERY_DOCS_TOOL]
}));
}));
// ---------------------------------------------------------------------------
// Tool dispatch
// ---------------------------------------------------------------------------
server.setRequestHandler(CallToolRequestSchema, async (request) => {
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'resolve-library-id') {
@@ -66,16 +77,73 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
content: [{ type: 'text' as const, text: `Unknown tool: ${name}` }],
isError: true
};
});
});
return server;
}
// ---------------------------------------------------------------------------
// HTTP transport
// ---------------------------------------------------------------------------
async function startHttp(port: number): Promise<void> {
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() {
if (cliArgs.transport === 'http') {
const port = parseInt(cliArgs.port as string, 10);
await startHttp(port);
} else {
const mcpServer = createMcpServer();
const transport = new StdioServerTransport();
await server.connect(transport);
await mcpServer.connect(transport);
// Server runs until process exits
}
}
main().catch((err) => {