Files
accounted/app/.well-known/oauth-authorization-server/route.ts
T
Jakob WennbergandClaude Fable 5 dc5aea4a35 feat(mcp): speak spec revision 2026-07-28 (stateless core) (#1277)
* feat(mcp): speak spec revision 2026-07-28 (stateless core)

Adopt the 2026-07-28 MCP spec revision on the connector endpoint while
keeping every handshake-era client (2025-06-18 and earlier) byte-identical:

- Accept per-request _meta protocol negotiation
  (io.modelcontextprotocol/protocolVersion); unsupported versions return
  UnsupportedProtocolVersionError (-32022) with the supported list.
- Implement server/discover (spec MUST): supported revisions, capabilities
  including the extensions field, identity, instructions, freshness hints.
- Decorate results for stateless clients: required resultType, serverInfo
  in _meta, and CacheableResult ttlMs/cacheScope on tools/list,
  prompts/list, resources/list, resources/read.
- Validate the standard Mcp-Method/Mcp-Name request headers when present
  (HeaderMismatchError -32020); absence stays accepted.
- Declare the ratified MCP Apps extension (io.modelcontextprotocol/ui) in
  capabilities; the widgets already use the ratified mime type and
  _meta.ui.resourceUri shape, so no widget changes are needed.
- OAuth: include the RFC 9207 iss parameter on every authorization
  response (success and error) and advertise
  authorization_response_iss_parameter_supported in RFC 8414 metadata.

Resource-not-found already used -32602 and tools/list ordering was already
deterministic; both are covered by the new test file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp): Mcp-Name covers params.uri, base64 sentinel, version-header consistency

Review follow-ups against the transport spec text: Mcp-Name mirrors
params.name OR params.uri (resources/read), values arrive base64-wrapped
in the =?base64?...?= sentinel and must be decoded before comparison, and
an MCP-Protocol-Version header that disagrees with the _meta protocol
version is a HeaderMismatch. Absence of any header stays accepted since
this server supports handshake-era clients (spec-sanctioned leniency).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:02:05 +02:00

37 lines
1.7 KiB
TypeScript

import { NextResponse } from 'next/server'
import { PUBLIC_OAUTH_METADATA_SCOPES } from '@/lib/auth/api-keys'
import { resolveDiscoveryBaseUrl } from '@/lib/api/v1/base-url'
/**
* RFC 8414: OAuth 2.0 Authorization Server Metadata.
* Tells MCP clients where the authorize/token endpoints are.
*
* The issuer reflects the (allowlisted) request host: clients that
* connected via the legacy app.gnubok.se domain must keep seeing a
* self-consistent issuer there, or their issuer validation breaks on
* re-auth after the app.accounted.se cutover.
*/
export async function GET(request: Request) {
const appUrl = resolveDiscoveryBaseUrl(request)
return NextResponse.json({
issuer: appUrl,
authorization_endpoint: `${appUrl}/api/mcp-oauth/authorize`,
token_endpoint: `${appUrl}/api/mcp-oauth/token`,
registration_endpoint: `${appUrl}/api/mcp-oauth/register`,
response_types_supported: ['code'],
grant_types_supported: ['authorization_code', 'refresh_token'],
code_challenge_methods_supported: ['S256'],
token_endpoint_auth_methods_supported: ['none', 'client_secret_post'],
// RFC 9207: the authorize endpoint includes `iss` in every authorization
// response (success and error) so clients can detect mix-up attacks.
authorization_response_iss_parameter_supported: true,
// Advertise only the safe read-only default scopes plus the coarse
// `mcp` marker. Destructive scopes (*:write, pending_operations:approve,
// bookkeeping:write) are still accepted by /authorize when requested
// explicitly, but enumerating them in public discovery aids
// scope-escalation reconnaissance (CC6.1, defense-in-depth).
scopes_supported: ['mcp', ...PUBLIC_OAUTH_METADATA_SCOPES],
})
}