Skip to content

feat(discovery): negotiate grant types and scopes from server metadata (CL-8796) - #12

Merged
TheGreatAxios merged 4 commits into
mainfrom
cl-8796-negotiate-grants-scopes
Sep 22, 2026
Merged

TheGreatAxios merged 4 commits into
mainfrom
cl-8796-negotiate-grants-scopes

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

MCP OAuth discovery and dynamic registration now negotiate grant types, scopes, and PKCE support from server metadata instead of hard-coding them.

  • Discovery (discoverMcpLoginEntry / McpLoginEntry / McpAuthorizationServer): the returned entry now carries the authorization server's grantTypesSupported, scopesSupported, tokenEndpointAuthMethodsSupported, and codeChallengeMethodsSupported, plus the protected resource's own resourceScopesSupported (RFC 9728 §2 scopes_supported).
  • PKCE: discovery now refuses (OAuthDiscoveryError) when code_challenge_methods_supported is absent or doesn't include S256 — MCP spec, "Security Considerations" → "Authorization Code Protection": "If code_challenge_methods_supported is absent, the authorization server does not support PKCE and MCP clients MUST refuse to proceed", and "MCP clients MUST use the S256 code challenge method when technically capable".
  • Public-client refusal: discovery now refuses when token_endpoint_auth_methods_supported is advertised but excludes "none" — this package only ever registers a public loopback client.
  • Registration (registerMcpClient): requests grant_types: ["authorization_code", "refresh_token"] when the AS's grant_types_supported includes refresh_token or wasn't advertised at all, otherwise just ["authorization_code"]; always response_types: ["code"] and token_endpoint_auth_method: "none"; forwards scope when given (RFC 7591 §2/§3.2.1). It now returns McpClientRegistration (clientId, grantTypes, scope?) reflecting what the server echoed back as granted, falling back to what was requested when the response is silent — RFC 7591 §3.2.1 lets a server narrow the request, and a host persisting "what's actually usable" needs the echoed value.
  • Scope selection: new exported selectMcpScopes(opts) implements the spec's "Scope Selection Strategy" priority — (1) a 401 WWW-Authenticate challenge's scope parameter if supplied, (2) the protected resource's scopes_supported, (3) omit the scope parameter — as small named steps. It also layers on offline_access (spec "Refresh Tokens" section) when the caller wants a refresh token and the AS's own scopes_supported lists offline_access. wantRefresh defaults to true in both selectMcpScopes and mcpClientConfig, matching registerMcpClient's default request of refresh_token — a host that passes no options still gets offline_access when the AS supports it (this was the Granola case the ticket was written for); pass wantRefresh: false to opt out. mcpClientConfig uses selectMcpScopes by default; an explicit scopes option overrides it entirely.

Spec sections followed

  • MCP Authorization spec (https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization): "Scope Selection Strategy", "Refresh Tokens".
  • MCP Authorization spec, "Security Considerations": "Authorization Code Protection" (PKCE/S256 refusal rules).
  • RFC 7591 §2, §3.2.1 (dynamic client registration request/response fields, including the server echoing back what was granted).
  • RFC 8414 §2 / RFC 9728 §2 (the new metadata fields parsed: grant_types_supported, scopes_supported, token_endpoint_auth_methods_supported, code_challenge_methods_supported).

Fixtures

Test fixtures use real, publicly-checkable authorization-server metadata shapes from two live MCP servers (checked 2026-09-22) to keep the negotiation logic honest against real-world variance — no provider name or behavior lives in src.

Test plan

  • bun run check (typecheck, lint, format, test) passes
  • New tests confirmed red against the pre-negotiation discovery.ts, then green against this implementation
  • Follow-up: wantRefresh default flipped to true (was false, disagreeing with registerMcpClient's own default); confirmed red before the fix, green after — 54 pass / 0 fail across the package

Fixes CL-8796

@TheGreatAxios TheGreatAxios left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review · Approve

Discovery negotiates grant types, scopes, and the PKCE challenge method from RFC 8414/9728 server metadata instead of hard-coding them, refuses authorization servers that don't advertise S256 or a public-client auth method, and mcpClientConfig/registerMcpClient request refresh_token/offline_access by default when the server supports them.

No findings. bun run check (typecheck, lint, format, 54 tests) passes clean; the new asserts (assertSupportsS256, assertSupportsPublicClients) run on both the protected-resource and direct-AS-metadata discovery paths, and the grant/scope negotiation is exercised against realistic Linear/Granola metadata shapes.

Notes

  • discovery.ts:364 — registerMcpClient's return type grew from { clientId } to McpClientRegistration and mcpClientConfig's wantRefresh default flipped from false to true; both are intentional per the commit messages and covered by tests, flagging only because they're behavior changes for existing consumers.
  • Two subject lines (feat(discovery): negotiate grant types and scopes from server metadata (CL-8796), test(discovery): negotiate grant types and scopes from server metadata (CL-8796)) run to 80 chars, over the usual 72-char guideline.

@TheGreatAxios
TheGreatAxios merged commit dd59fd9 into main Sep 22, 2026
1 check passed
@TheGreatAxios
TheGreatAxios deleted the cl-8796-negotiate-grants-scopes branch September 22, 2026 05:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant