diff --git a/src/app/api/mcp/route.test.ts b/src/app/api/mcp/route.test.ts index 4b194b8..0bce544 100644 --- a/src/app/api/mcp/route.test.ts +++ b/src/app/api/mcp/route.test.ts @@ -288,6 +288,70 @@ describe("POST /api/mcp", () => { expect(body.error.message).toMatch(/Missing id/); }); + it("handles MCP initialize handshake and returns server info (regression: 2026-08-04)", async () => { + // Before this handler existed, /api/mcp returned HTTP 404 with + // "Method not found" on `initialize`, which made every spec-compliant + // MCP client (Hermes, Claude Desktop, Cursor, …) abort the session + // before `tools/list` ever ran. Spec-compliant clients expect HTTP 200 + // with a well-formed `result` containing `protocolVersion`, `capabilities`, + // and `serverInfo`. + const res = await POST( + mcpRequest({ + method: "initialize", + id: 100, + params: { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "test-client", version: "0.0.0" }, + }, + }) + ); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.jsonrpc).toBe("2.0"); + expect(body.id).toBe(100); + expect(body.error).toBeUndefined(); + expect(body.result.protocolVersion).toBe("2024-11-05"); + expect(body.result.capabilities).toEqual({ tools: {} }); + expect(body.result.serverInfo).toMatchObject({ name: "wfdash-mcp" }); + }); + + it("initialize echoes back an unrecognised protocolVersion so older clients keep working", async () => { + // The spec says servers MAY support older protocol versions. We + // echo whatever the client sent (rather than forcing 2024-11-05) + // so a client on 2025-03-26 doesn't immediately drop us. + const res = await POST( + mcpRequest({ + method: "initialize", + id: 101, + params: { protocolVersion: "2025-06-18-future-version" }, + }) + ); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.result.protocolVersion).toBe("2025-06-18-future-version"); + }); + + it("acknowledges notifications/initialized with HTTP 204 and no body", async () => { + // JSON-RPC notification: no `id`, server returns no JSON-RPC envelope. + // Clients shouldn't hang waiting for a reply that won't come. + const res = await POST( + mcpRequest({ method: "notifications/initialized" }) + ); + expect(res.status).toBe(204); + const text = await res.text(); + expect(text).toBe(""); + }); + + it("answers ping with an empty result (liveness probe)", async () => { + const res = await POST( + mcpRequest({ method: "ping", id: 102 }) + ); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.result).toEqual({}); + }); + it("rejects unknown tools with a JSON-RPC error", async () => { const res = await POST( mcpRequest({ diff --git a/src/app/api/mcp/route.ts b/src/app/api/mcp/route.ts index 838bb24..e0e1646 100644 --- a/src/app/api/mcp/route.ts +++ b/src/app/api/mcp/route.ts @@ -1309,6 +1309,47 @@ export async function POST(req: NextRequest) { assertAgentAuth(req); const body = await req.json(); + // MCP protocol handshake. The spec requires every client to send + // `initialize` before any other method; clients that don't get a + // well-formed response (incl. HTTP 200) abort the session before + // `tools/list` ever fires. This block is what makes WFDASH reachable + // as a real MCP server from any spec-compliant client (Hermes, + // Claude Desktop, Cursor, mcp-cli, …). Before this fix, clients + // got HTTP 404 "Method not found" on `initialize` and terminated + // the session — see docs/bugs/2026-08-04-mcp-initialize-handshake.md. + // + // Result shape: the MCP `initialize` response is a PLAIN object, + // not wrapped in the `content: [{type:"text",...}]` envelope that + // `jsonRpcOk` (used by tools/list and tools/call) wraps its result + // in. Using jsonRpcOk here would put the protocolVersion under + // `result.content[0].text` and clients would still drop us. + if (body?.method === "initialize") { + return NextResponse.json({ + jsonrpc: "2.0", + id: body.id ?? null, + result: { + protocolVersion: body?.params?.protocolVersion || "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "wfdash-mcp", version: "1.0.0" }, + }, + }); + } + // `notifications/initialized` is a JSON-RPC notification (no `id`, + // no response expected). We acknowledge with 204 so the client + // doesn't hang waiting for a reply that never comes. + if (body?.method === "notifications/initialized") { + return new NextResponse(null, { status: 204 }); + } + // Optional liveness probe — some clients send this before opening + // a real session. Result is an empty object per the spec. + if (body?.method === "ping") { + return NextResponse.json({ + jsonrpc: "2.0", + id: body.id ?? null, + result: {}, + }); + } + if (body?.method === "tools/list") { return withCors( NextResponse.json({ jsonrpc: "2.0", id: body.id ?? null, result: { tools } }),