import { describe, it, expect, vi } from 'vitest'; import Fastify from 'fastify'; import { registerMcpEndpoint } from '../src/http/mcp-endpoint.js'; import type { McpRouter } from '../src/router.js'; import type { JsonRpcRequest, JsonRpcResponse } from '../src/types.js'; /** * End-to-end over the Streamable HTTP transport: the /mcp endpoint must serve * OpenAI-safe tool names on tools/list and map them back to the router's * canonical `server/tool` names on tools/call (the librechat fetch_content * incident, 2026-08-25). */ function parseSse(body: string): JsonRpcResponse { const dataLine = body.split('\n').find((l) => l.startsWith('data: ')); if (!dataLine) throw new Error(`no SSE data line in: ${body}`); return JSON.parse(dataLine.slice('data: '.length)) as JsonRpcResponse; } describe('registerMcpEndpoint wire names', () => { it('lists wire-safe names and routes calls back to canonical names', async () => { const routed: JsonRpcRequest[] = []; const fakeRouter = { route: vi.fn(async (req: JsonRpcRequest): Promise => { routed.push(req); switch (req.method) { case 'initialize': return { jsonrpc: '2.0', id: req.id, result: { protocolVersion: '2024-11-05', serverInfo: { name: 'test', version: '0' }, capabilities: { tools: {} }, }, }; case 'tools/list': return { jsonrpc: '2.0', id: req.id, result: { tools: [{ name: 'websearch/fetch_content', inputSchema: { type: 'object' } }] }, }; case 'tools/call': return { jsonrpc: '2.0', id: req.id, result: { content: [{ type: 'text', text: 'ok' }] }, }; default: return { jsonrpc: '2.0', id: req.id, result: {} }; } }), } as unknown as McpRouter; const app = Fastify(); registerMcpEndpoint(app, fakeRouter); await app.ready(); try { const headers = { 'content-type': 'application/json', accept: 'application/json, text/event-stream', }; const init = await app.inject({ method: 'POST', url: '/mcp', headers, payload: { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 't', version: '0' } } }, }); expect(init.statusCode).toBe(200); const sessionId = init.headers['mcp-session-id'] as string; expect(sessionId).toBeTruthy(); const sessionHeaders = { ...headers, 'mcp-session-id': sessionId }; const list = await app.inject({ method: 'POST', url: '/mcp', headers: sessionHeaders, payload: { jsonrpc: '2.0', id: 2, method: 'tools/list' }, }); const listResponse = parseSse(list.body); const tools = (listResponse.result as { tools: Array<{ name: string }> }).tools; expect(tools.map((t) => t.name)).toEqual(['websearch_fetch_content']); const call = await app.inject({ method: 'POST', url: '/mcp', headers: sessionHeaders, payload: { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'websearch_fetch_content', arguments: { url: 'https://x' } } }, }); const callResponse = parseSse(call.body); expect(callResponse.error).toBeUndefined(); const routedCall = routed.find((r) => r.method === 'tools/call'); expect(routedCall?.params?.['name']).toBe('websearch/fetch_content'); } finally { await app.close(); } }); });