fix(mcp): include scope in clientMetadata and add callbackPort option

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:<callbackPort>/mcp/oauth/callback`.
   `redirectUri` still takes precedence if both are provided.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sebin Thomas 2026-05-22 11:58:19 +02:00
parent 76d9c2cd76
commit e9ca7d292b
3 changed files with 19 additions and 4 deletions

View file

@ -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).",
}),

View file

@ -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) => {

View file

@ -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 } : {}),
}
}