Skip to content

Remote MCP servers with OAuth fail with 'No OAuth state saved for MCP server' #15546

@domdomegg

Description

@domdomegg

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 Errornot 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

  1. Configure a remote MCP server with OAuth + dynamic client registration
  2. Start OpenCode — no prior auth tokens exist
  3. OpenCode tries to auto-connect, gets 401, SDK triggers auth flow
  4. provider.state() throws "No OAuth state saved for MCP server: <name>"
  5. Error propagates as plain Error, not UnauthorizedError
  6. Server shows status: "failed" instead of status: "needs_auth"

Screenshot and/or share link

Image Image

Operating System

macOS 26.0.1

Terminal

zsh in VS Code

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcoreAnything pertaining to core functionality of the application (opencode server stuff)

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions