From a1b33cbc6a9d279334a5cd7e6cf925ea131c0de1 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Sat, 21 Mar 2026 15:14:40 +0100 Subject: [PATCH] fix: MCP notification 202 and protocol version echo (#75) * fix: return 202 for MCP notifications and echo client protocol version - notifications/initialized returns 202 Accepted per MCP Streamable HTTP spec (was 204 which may prevent tool discovery) - Echo client's protocolVersion in initialize response - Add instructions field for server description Co-Authored-By: Claude Opus 4.6 (1M context) * fix: validate protocol version and handle notifications pre-auth - Validate protocolVersion against supported set instead of blindly echoing (prevents false protocol agreement with unknown versions) - Handle notifications/initialized before auth check so fire-and-forget notifications don't get 401 responses that confuse MCP clients Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- extensions/general/mcp-server/server.ts | 29 ++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/extensions/general/mcp-server/server.ts b/extensions/general/mcp-server/server.ts index 382757a3..8701befd 100644 --- a/extensions/general/mcp-server/server.ts +++ b/extensions/general/mcp-server/server.ts @@ -1228,10 +1228,23 @@ function jsonRpcError( * Auth is done via Bearer API key (extension route has skipAuth: true). */ export async function handleMcpRequest(request: Request): Promise { - // ── Auth ── const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' const wwwAuth = `Bearer resource_metadata="${appUrl}/.well-known/oauth-protected-resource"` + // ── Pre-auth: handle fire-and-forget notifications before auth check ── + // MCP notifications have no id and don't expect error responses. + // Checking auth on them would return 401 which confuses clients. + const clonedRequest = request.clone() + try { + const peek = await clonedRequest.json() + if (peek.method === 'notifications/initialized') { + return new Response(null, { status: 202 }) + } + } catch { + // Not valid JSON — fall through to auth + parse below + } + + // ── Auth ── const token = extractBearerToken(request) if (!token) { return new Response('Unauthorized', { @@ -1280,20 +1293,26 @@ export async function handleMcpRequest(request: Request): Promise { const { method, id, params } = body switch (method) { - case 'initialize': + case 'initialize': { + const SUPPORTED_VERSIONS = new Set(['2025-03-26', '2024-11-05']) + const clientVersion = (params as Record)?.protocolVersion as string | undefined + const negotiatedVersion = + clientVersion && SUPPORTED_VERSIONS.has(clientVersion) ? clientVersion : PROTOCOL_VERSION return NextResponse.json( jsonRpc(id ?? null, { - protocolVersion: PROTOCOL_VERSION, + protocolVersion: negotiatedVersion, capabilities: { tools: { listChanged: false }, }, serverInfo: SERVER_INFO, + instructions: 'gnubok — Swedish bookkeeping via conversation. List transactions, categorize, create invoices, view reports.', }) ) + } case 'notifications/initialized': - // Client acknowledgement — no response needed for notifications - return new Response(null, { status: 204 }) + // Handled pre-auth above, but if it somehow reaches here, still return 202 + return new Response(null, { status: 202 }) case 'ping': return NextResponse.json(jsonRpc(id ?? null, {}))