From bce6aa89a450e2a3dd2a7bd735c7dc951fb14ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Ara=C3=BAjo?= Date: Fri, 10 Jul 2026 18:08:28 +0000 Subject: [PATCH] fix(mcp): included OAuth scopes in dynamic client registration Clerk and similar providers bind DCR clients to only the scopes declared at registration. Authorize then requests scopes_supported (including openid), which rejects with "client is not allowed to request scope 'openid'". Match Claude Code by sending config.scopes as RFC 7591 scope on the DCR body. --- packages/coding-agent/CHANGELOG.md | 4 +++ packages/coding-agent/src/mcp/oauth-flow.ts | 25 ++++++++++++------ packages/coding-agent/test/oauth-flow.test.ts | 26 +++++++++++++++++++ 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 04ae4eb4d..412d37450 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed MCP OAuth dynamic client registration omitting discovered scopes on the RFC 7591 registration body. Providers such as Clerk bind DCR-created clients to only the scopes declared at registration, then reject the subsequent authorize request when it asks for `openid` (from `scopes_supported`). Registration now includes `config.scopes` when present, matching Claude Code and the scopes already sent on authorize. + ## [16.4.0] - 2026-07-10 ### Breaking Changes diff --git a/packages/coding-agent/src/mcp/oauth-flow.ts b/packages/coding-agent/src/mcp/oauth-flow.ts index 36e6f9bf7..a456ec41c 100644 --- a/packages/coding-agent/src/mcp/oauth-flow.ts +++ b/packages/coding-agent/src/mcp/oauth-flow.ts @@ -555,12 +555,28 @@ export class MCPOAuthFlow extends OAuthCallbackFlow { * "Only clients listed in the Figma MCP Catalog can connect"), the fallback * probe surfaces a message that names the endpoint and status instead of * the historical opaque "OAuth provider requires client_id". + * + * Includes {@link MCPOAuthConfig.scopes} as RFC 7591 `scope` when set so + * providers that bind DCR clients to registered scopes only (e.g. Clerk) + * accept the later authorize request for the same scope set. */ async #tryRegisterClient(redirectUri: string): Promise { const registrationEndpoint = await this.#resolveRegistrationEndpoint(); if (!registrationEndpoint) return; try { + const registrationBody: Record = { + client_name: "oh-my-pi", + redirect_uris: [redirectUri], + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + token_endpoint_auth_method: "none", + application_type: "native", + }; + const scope = this.config.scopes?.trim(); + if (scope) { + registrationBody.scope = scope; + } const response = await this.#fetch(registrationEndpoint, { method: "POST", headers: { @@ -568,14 +584,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow { Accept: "application/json", }, signal: this.ctrl.signal, - body: JSON.stringify({ - client_name: "oh-my-pi", - redirect_uris: [redirectUri], - grant_types: ["authorization_code", "refresh_token"], - response_types: ["code"], - token_endpoint_auth_method: "none", - application_type: "native", - }), + body: JSON.stringify(registrationBody), }); if (!response.ok) { diff --git a/packages/coding-agent/test/oauth-flow.test.ts b/packages/coding-agent/test/oauth-flow.test.ts index d022e3ad4..ade93f5b0 100644 --- a/packages/coding-agent/test/oauth-flow.test.ts +++ b/packages/coding-agent/test/oauth-flow.test.ts @@ -80,10 +80,36 @@ describe("mcp oauth flow", () => { expect(registrationPayload).not.toBeNull(); expect((registrationPayload as { client_name?: string } | null)?.client_name).toBe("oh-my-pi"); + expect((registrationPayload as { scope?: string } | null)?.scope).toBeUndefined(); expect(authUrl.searchParams.get("client_id")).toBe("registered-client-id"); expect(authUrl.searchParams.get("state")).toBe("test-state"); }); + it("includes discovered scopes in dynamic client registration", async () => { + let registrationPayload: Record | null = null; + const scopes = "openid profile email offline_access"; + + const flow = new MCPOAuthFlow( + { + authorizationUrl: "https://www.figma.com/oauth/mcp", + tokenUrl: "https://api.figma.com/v1/oauth/token", + scopes, + fetch: mockFigmaRegistration(payload => { + registrationPayload = payload; + }), + }, + {}, + ); + + const { url } = await flow.generateAuthUrl("test-state", "http://127.0.0.1:53173/callback"); + const authUrl = new URL(url); + + expect(registrationPayload).not.toBeNull(); + expect((registrationPayload as { scope?: string } | null)?.scope).toBe(scopes); + expect(authUrl.searchParams.get("scope")).toBe(scopes); + expect(authUrl.searchParams.get("client_id")).toBe("registered-client-id"); + }); + it("omits prompt by default so provider-specific reauth pages can use returning grants", async () => { const flow = new MCPOAuthFlow( {