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.
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:
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 servehad grabbed 19876 days ago. Myopencode mcp auth Notionfailed every time with the CSRF error. Took an hour to trace.Why it matters
oauth.callbackPort,oauth.redirectUri) already exist onMcpOAuthConfigand are wired throughensureRunning. The user just has no way to know they need to set them.Proposed fix
Throw a clear error from
ensureRunningwhen the port is busy, naming the config knob: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.