Files
accounted/app/.well-known/oauth-protected-resource/[...path]/route.ts
T
1307d4db2e fix(oauth): serve RFC 9728 resource metadata at the path-based locations Claude.ai fetches (#1915)
Claude.ai's connector setup derives the protected-resource metadata URL
from the MCP server URL and fetches it before any 401 challenge:
  /.well-known/oauth-protected-resource/api/extensions/ext/mcp-server/mcp
  /api/extensions/ext/mcp-server/mcp/.well-known/oauth-protected-resource
Both were 404 (only the root document our WWW-Authenticate header points
at existed), which the dialog reported as "Authorization with Accounted
failed". One shared builder now serves all three locations; the
path-based route answers 404 for any path other than the MCP endpoint.


Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 09:24:11 +02:00

28 lines
1.0 KiB
TypeScript

import { NextResponse } from 'next/server'
import {
MCP_RESOURCE_PATH,
buildProtectedResourceMetadata,
} from '@/lib/auth/protected-resource-metadata'
/**
* RFC 9728 §3.1 path-based Protected Resource Metadata:
* `/.well-known/oauth-protected-resource{resource-path}`.
*
* Claude.ai's connector setup derives this URL from the MCP server URL and
* fetches it before any 401 challenge; without it the dialog reports
* "Authorization with Accounted failed". Only the MCP endpoint is a protected
* resource here, so every other path is a 404 rather than a generic answer
* that would advertise resources this server does not serve.
*/
export async function GET(
request: Request,
context: { params: Promise<{ path: string[] }> }
) {
const { path } = await context.params
const resourcePath = '/' + (path ?? []).join('/')
if (resourcePath !== MCP_RESOURCE_PATH) {
return NextResponse.json({ error: 'not_found' }, { status: 404 })
}
return NextResponse.json(buildProtectedResourceMetadata(request))
}