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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-03-21 15:14:40 +01:00
committed by GitHub
co-authored by Claude Opus 4.6
parent d61670231f
commit a1b33cbc6a
+24 -5
View File
@@ -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<Response> {
// ── 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<Response> {
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<string, unknown>)?.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, {}))