fix(mcp): return 405 (not 401) on GET /mcp to stop client re-auth storm (#747)

The Streamable HTTP GET handler returned 401 unconditionally. This server is
stateless and offers no server-initiated SSE stream, for which the MCP
Streamable HTTP spec requires 405 Method Not Allowed.

Returning 401 made spec-compliant clients (Claude connector, Claude Desktop,
Cursor) treat the SSE GET as an auth failure and enter a refresh-token →
re-open-GET → 401 retry loop. Across the active connector base this storms
/api/extensions/ext/mcp-server/mcp (observed ~steady GET→401 traffic on
app.gnubok.se) and churns OAuth API-key rotation — and tripped a Vercel
usage anomaly (edge requests + function invocations spiking ~16x).

OAuth discovery remains bootstrapped on the POST 401 (WWW-Authenticate +
.well-known/oauth-protected-resource); the POST JSON-RPC channel and the
POST-only npm bridge are unaffected.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-06-17 14:26:13 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent ca3ae65b12
commit 7aa37fd3b8
+10 -6
View File
@@ -36,14 +36,18 @@ export const mcpServerExtension: Extension = {
method: 'GET',
path: '/mcp',
skipAuth: true,
// This server is stateless and offers no server-initiated SSE stream, so
// the Streamable HTTP spec requires 405 Method Not Allowed here. Returning
// 401 (as we previously did) makes spec-compliant clients (Claude
// connector, Claude Desktop, Cursor) treat the SSE GET as an auth failure
// and retry-loop — refresh token → re-open GET → 401 → … — which storms
// the endpoint and churns OAuth key rotation. OAuth discovery is
// bootstrapped on the POST 401 (WWW-Authenticate), not here.
handler: async (request: Request) => {
if (isForbiddenOrigin(request)) return forbiddenOriginResponse()
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
return new Response('Authorization required', {
status: 401,
headers: {
'WWW-Authenticate': `Bearer resource_metadata="${appUrl}/.well-known/oauth-protected-resource"`,
},
return new Response('Method Not Allowed', {
status: 405,
headers: { Allow: 'POST, DELETE' },
})
},
},