-
Notifications
You must be signed in to change notification settings - Fork 15.1k
Remote MCP servers with OAuth fail with 'No OAuth state saved for MCP server' #15546
Description
Description
Connecting to a remote OAuth MCP server that requires OAuth fails with "No OAuth state saved for MCP server: <name>" on first connection when no tokens have been previously saved. The server shows as status: "failed" instead of status: "needs_auth".
Root cause: There are two bugs in the MCP OAuth auto-connect flow:
Bug 1 — McpOAuthProvider.state() throws instead of generating a state
The MCP SDK's OAuthClientProvider interface defines state?() as a method that returns an OAuth2 state parameter. The SDK calls it during auth() at client/auth.js:588:
const state = provider.state ? await provider.state() : undefined;OpenCode's McpOAuthProvider.state() is implemented as a reader that throws if no state has been pre-saved:
async state(): Promise<string> {
const entry = await McpAuth.get(this.mcpName)
if (!entry?.oauthState) {
throw new Error(`No OAuth state saved for MCP server: ${this.mcpName}`)
}
return entry.oauthState
}The saveState() method is only called in startAuth() (the explicit auth flow), never during the automatic create() connect path. When create() triggers auto-auth via the SDK transport on a 401 response, state() is called but no state was ever saved — so it throws.
Bug 2 — create() only catches UnauthorizedError, not plain errors from the auth flow
When the SDK transport's _authThenStart() encounters an error from auth() (like the state() throw above), it re-throws it as a plain Error — not as UnauthorizedError. OpenCode's create() only checks error instanceof UnauthorizedError, so the error falls through to the generic handler that sets status: "failed" instead of status: "needs_auth".
This means the automatic connect-with-auth path in create() is broken for all OAuth MCP servers — not just ones that support dynamic client registration. Servers without DCR fail at the registration step (before state() is reached) with a different plain Error, hitting the same instanceof UnauthorizedError miss.
Why this hasn't been widely reported: Most OAuth MCP servers either (a) don't support dynamic client registration so they fail earlier with a different error, or (b) users run opencode mcp auth <name> explicitly, which uses startAuth() and pre-saves the state. The bug is only visible on the automatic first-connect path.
Plugins
None
OpenCode version
Latest (main branch at e1e18c7), and 1.2.10
Steps to reproduce
- Configure a remote MCP server with OAuth + dynamic client registration
- Start OpenCode — no prior auth tokens exist
- OpenCode tries to auto-connect, gets 401, SDK triggers auth flow
provider.state()throws"No OAuth state saved for MCP server: <name>"- Error propagates as plain
Error, notUnauthorizedError - Server shows
status: "failed"instead ofstatus: "needs_auth"
Screenshot and/or share link
Operating System
macOS 26.0.1
Terminal
zsh in VS Code