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:
@@ -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",
|
||||
|
||||
104
src/mcp/index.ts
104
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,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) => {
|
||||
|
||||
Reference in New Issue
Block a user