Skip to content

fix(mcp): silent fallback when OAuth callback port is in use surfaces as misleading CSRF error #30888

@WUKUNTAI-0211

Description

@WUKUNTAI-0211

Summary

When 127.0.0.1:19876 (the hardcoded OAuth callback port) is already in use, McpOAuthCallback.ensureRunning() silently returns without starting our callback server:

https://github.com/sst/opencode/blob/dev/packages/opencode/src/mcp/oauth-callback.ts#L153-L157

The OAuth flow then opens the browser anyway and waits for a callback. The browser hits the other process listening on 19876, which either 404s or — if it's another opencode instance — sees a state it doesn't know about and shows:

Authorization Failed
Invalid or expired state parameter - potential CSRF attack

That message points the user at a CSRF / cookie / browser issue. The real cause is "the callback port is occupied".

How I hit it

Shared dev host with two users. The other user's long-running bun dev serve had grabbed 19876 days ago. My opencode mcp auth Notion failed every time with the CSRF error. Took an hour to trace.

Why it matters

  • The fields to fix it (oauth.callbackPort, oauth.redirectUri) already exist on McpOAuthConfig and are wired through ensureRunning. The user just has no way to know they need to set them.
  • Anyone on a shared host, dev container, GitHub Codespaces, or a machine where 19876 happens to be taken (it's not in IANA's well-known range but it's not reserved either) gets the same misleading error.

Proposed fix

Throw a clear error from ensureRunning when the port is busy, naming the config knob:

OAuth callback port 19876 is already in use. Set "oauth.callbackPort"
(or "oauth.redirectUri") on the MCP server entry in your opencode config
to use a different port.

This changes one branch from "silent return" to "throw", so it's a small behaviour change. The only thing it breaks is the (apparently undocumented) ability for two opencode instances on the same machine to share one callback server — which is racy anyway (states are kept in the first instance's memory, so the second's auth would silently fail).

Happy to PR — will post one shortly.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions