From d61670231fb60f5d05373b9e78cf0dc43d37238c Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:47:47 +0100 Subject: [PATCH] fix: plain 401 for MCP OAuth discovery (#74) * fix: return plain 401 for MCP OAuth discovery and harden auth flow - Return plain HTTP 401 with WWW-Authenticate header (no JSON-RPC body) so Claude Desktop's MCP client can trigger OAuth discovery correctly - Remove unused apiKey import from authorize route - Remove stale codeChallengeMethod parameter from oauth-codes Co-Authored-By: Claude Opus 4.6 (1M context) * fix: add Retry-After header to 429 rate limit response Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- app/api/mcp-oauth/authorize/route.ts | 2 -- extensions/general/mcp-server/server.ts | 24 ++++++++++++++---------- lib/auth/oauth-codes.ts | 1 - 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/api/mcp-oauth/authorize/route.ts b/app/api/mcp-oauth/authorize/route.ts index d450f372..70c7da49 100644 --- a/app/api/mcp-oauth/authorize/route.ts +++ b/app/api/mcp-oauth/authorize/route.ts @@ -160,7 +160,6 @@ export async function POST(request: Request) { const redirectUri = url.searchParams.get('redirect_uri') const state = url.searchParams.get('state') const codeChallenge = url.searchParams.get('code_challenge') || '' - const codeChallengeMethod = url.searchParams.get('code_challenge_method') || 'S256' if (!redirectUri) { return NextResponse.json({ error: 'invalid_request' }, { status: 400 }) @@ -193,7 +192,6 @@ export async function POST(request: Request) { const code = createAuthCode({ userId: user.id, codeChallenge, - codeChallengeMethod, redirectUri, }) diff --git a/extensions/general/mcp-server/server.ts b/extensions/general/mcp-server/server.ts index af7dd7fd..382757a3 100644 --- a/extensions/general/mcp-server/server.ts +++ b/extensions/general/mcp-server/server.ts @@ -1234,21 +1234,25 @@ export async function handleMcpRequest(request: Request): Promise { const token = extractBearerToken(request) if (!token) { - return new Response( - JSON.stringify(jsonRpcError(null, -32000, 'Authorization required')), - { status: 401, headers: { 'Content-Type': 'application/json', 'WWW-Authenticate': wwwAuth } } - ) + return new Response('Unauthorized', { + status: 401, + headers: { 'WWW-Authenticate': wwwAuth }, + }) } const authResult = await validateApiKey(token) if ('error' in authResult) { const status = authResult.status - const headers: Record = { 'Content-Type': 'application/json' } - if (status === 401) headers['WWW-Authenticate'] = wwwAuth - return new Response( - JSON.stringify(jsonRpcError(null, -32000, authResult.error)), - { status, headers } - ) + if (status === 429) { + return new Response(authResult.error, { + status: 429, + headers: { 'Content-Type': 'text/plain', 'Retry-After': '60' }, + }) + } + return new Response('Unauthorized', { + status: 401, + headers: { 'WWW-Authenticate': wwwAuth }, + }) } const { userId } = authResult diff --git a/lib/auth/oauth-codes.ts b/lib/auth/oauth-codes.ts index 0f8a8e33..d6f1b365 100644 --- a/lib/auth/oauth-codes.ts +++ b/lib/auth/oauth-codes.ts @@ -21,7 +21,6 @@ function getEncryptionKey(): Buffer { export interface AuthCodePayload { userId: string codeChallenge: string - codeChallengeMethod: string redirectUri: string exp: number }