From e9ca7d292b876a0b14db0d901859499880faa89d Mon Sep 17 00:00:00 2001 From: Sebin Thomas Date: Fri, 22 May 2026 11:58:19 +0200 Subject: [PATCH] fix(mcp): include scope in clientMetadata and add callbackPort option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related OAuth issues with remote MCP servers: 1. `scope` config field was accepted but never propagated to `clientMetadata`. The MCP SDK uses `clientMetadata.scope` as its last-resort fallback when neither the WWW-Authenticate header nor the Protected Resource Metadata (scopes_supported) advertise scopes. For servers whose metadata returns no scopes (e.g. AWS Bedrock AgentCore), the authorization request was sent with no scope parameter, causing IdPs such as Okta to reject with "No scopes were requested." 2. The callback port was hardcoded to 19876 with no way to override it short of providing a full `redirectUri`. Added `callbackPort` as a shorthand in the OAuth config — when set it constructs the redirect URI as `http://127.0.0.1:/mcp/oauth/callback`. `redirectUri` still takes precedence if both are provided. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- packages/opencode/src/config/mcp.ts | 4 ++++ packages/opencode/src/mcp/index.ts | 14 +++++++++++--- packages/opencode/src/mcp/oauth-provider.ts | 5 ++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/config/mcp.ts b/packages/opencode/src/config/mcp.ts index cf170b95fc..ae558a76b2 100644 --- a/packages/opencode/src/config/mcp.ts +++ b/packages/opencode/src/config/mcp.ts @@ -26,6 +26,10 @@ export const OAuth = Schema.Struct({ description: "OAuth client secret (if required by the authorization server)", }), scope: Schema.optional(Schema.String).annotate({ description: "OAuth scopes to request during authorization" }), + callbackPort: Schema.optional(PositiveInt).annotate({ + description: + "Port for the local OAuth callback server (default: 19876). Shorthand for redirectUri when only the port needs changing. Ignored if redirectUri is set.", + }), redirectUri: Schema.optional(Schema.String).annotate({ description: "OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback).", }), diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 70d1019573..e72b49f343 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -19,7 +19,7 @@ import { NamedError } from "@opencode-ai/core/util/error" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { withTimeout } from "@/util/timeout" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import { McpOAuthProvider } from "./oauth-provider" +import { McpOAuthProvider, OAUTH_CALLBACK_PATH } from "./oauth-provider" import { McpOAuthCallback } from "./oauth-callback" import { McpAuth } from "./auth" import { BusEvent } from "../bus/bus-event" @@ -318,6 +318,7 @@ export const layer = Layer.effect( clientId: oauthConfig?.clientId, clientSecret: oauthConfig?.clientSecret, scope: oauthConfig?.scope, + callbackPort: oauthConfig?.callbackPort, redirectUri: oauthConfig?.redirectUri, }, { @@ -769,8 +770,15 @@ export const layer = Layer.effect( // OAuth config is optional - if not provided, we'll use auto-discovery const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined + // Resolve effective redirect URI: explicit redirectUri > callbackPort shorthand > default + const effectiveRedirectUri = + oauthConfig?.redirectUri ?? + (oauthConfig?.callbackPort + ? `http://127.0.0.1:${oauthConfig.callbackPort}${OAUTH_CALLBACK_PATH}` + : undefined) + // Start the callback server with custom redirectUri if configured - yield* Effect.promise(() => McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri)) + yield* Effect.promise(() => McpOAuthCallback.ensureRunning(effectiveRedirectUri)) const oauthState = Array.from(crypto.getRandomValues(new Uint8Array(32))) .map((b) => b.toString(16).padStart(2, "0")) @@ -784,7 +792,7 @@ export const layer = Layer.effect( clientId: oauthConfig?.clientId, clientSecret: oauthConfig?.clientSecret, scope: oauthConfig?.scope, - redirectUri: oauthConfig?.redirectUri, + redirectUri: effectiveRedirectUri, }, { onRedirect: async (url) => { diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts index 45dcff50f0..c1a52639c8 100644 --- a/packages/opencode/src/mcp/oauth-provider.ts +++ b/packages/opencode/src/mcp/oauth-provider.ts @@ -18,6 +18,7 @@ export interface McpOAuthConfig { clientId?: string clientSecret?: string scope?: string + callbackPort?: number redirectUri?: string } @@ -38,7 +39,8 @@ export class McpOAuthProvider implements OAuthClientProvider { if (this.config.redirectUri) { return this.config.redirectUri } - return `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}` + const port = this.config.callbackPort ?? OAUTH_CALLBACK_PORT + return `http://127.0.0.1:${port}${OAUTH_CALLBACK_PATH}` } get clientMetadata(): OAuthClientMetadata { @@ -49,6 +51,7 @@ export class McpOAuthProvider implements OAuthClientProvider { grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: this.config.clientSecret ? "client_secret_post" : "none", + ...(this.config.scope ? { scope: this.config.scope } : {}), } }