Summary
opencode mcp auth <server> registers a brand-new dynamic OAuth client (RFC 7591) on every invocation — producing a new client_id and a full interactive browser consent each time — instead of reusing the client registration already stored in mcp-auth.json. This happens even when a valid stored registration and a valid refresh token exist.
Environment
Root cause
startAuth() in packages/opencode/src/mcp/index.ts constructs a McpOAuthPendingProvider. Its clientInformation() (in packages/opencode/src/mcp/oauth-provider.ts) returns only the in-memory pendingClientInfo (which is undefined at the start of a run) and never falls back to the persisted clientInfo on disk — unlike the base McpOAuthProvider.clientInformation(), which reads it via getForUrl(...). Its tokens() is the same (returns in-memory pendingTokens only).
The MCP SDK's auth() / authInternal() registers a new client whenever provider.clientInformation() returns undefined (before it would ever attempt a refresh). So every mcp auth run dynamic-registers a fresh client.
Evidence
Server-side logs show a fresh POST /register (new client_id) followed by a full /authorize + consent + callback on every opencode mcp auth run, rather than a bare refresh_token grant against the existing client.
Impact
Suggested fix
Have McpOAuthPendingProvider.clientInformation() / tokens() fall back to the persisted values on disk (or seed the pending provider from stored state), so re-authentication reuses the existing registration and can refresh instead of re-registering.
Summary
opencode mcp auth <server>registers a brand-new dynamic OAuth client (RFC 7591) on every invocation — producing a newclient_idand a full interactive browser consent each time — instead of reusing the client registration already stored inmcp-auth.json. This happens even when a valid stored registration and a valid refresh token exist.Environment
1.17.14Root cause
startAuth()inpackages/opencode/src/mcp/index.tsconstructs aMcpOAuthPendingProvider. ItsclientInformation()(inpackages/opencode/src/mcp/oauth-provider.ts) returns only the in-memorypendingClientInfo(which isundefinedat the start of a run) and never falls back to the persistedclientInfoon disk — unlike the baseMcpOAuthProvider.clientInformation(), which reads it viagetForUrl(...). Itstokens()is the same (returns in-memorypendingTokensonly).The MCP SDK's
auth()/authInternal()registers a new client wheneverprovider.clientInformation()returnsundefined(before it would ever attempt a refresh). So everymcp authrun dynamic-registers a fresh client.Evidence
Server-side logs show a fresh
POST /register(newclient_id) followed by a full/authorize+ consent + callback on everyopencode mcp authrun, rather than a barerefresh_tokengrant against the existing client.Impact
client_idand full browser consent on every auth, even when silent refresh (or re-auth against the existing client) would suffice.patternfails the whole server connection (shown as "Authentication failed" for OAuth servers) #35624): whenever something downstream of the token exchange fails, the user re-auths, and each re-auth needlessly mints a new client.Suggested fix
Have
McpOAuthPendingProvider.clientInformation()/tokens()fall back to the persisted values on disk (or seed the pending provider from stored state), so re-authentication reuses the existing registration and can refresh instead of re-registering.