Skip to content

Remote OAuth MCP servers fail with invalid_token when defined in config.json (no OAuth provider attached to config-file transport) #106

Description

@spokvulcan

Summary

A remote MCP server that requires OAuth — for example the official Atlassian Rovo server (https://mcp.atlassian.com/v1/mcp, which provides Jira/Confluence/Compass) — cannot be used from ZCode when it is declared in ~/.zcode/cli/config.json. The server fails at connect time with invalid_token / "Missing or invalid access token", and no Authenticate / "Sign in" affordance is offered in Settings → MCP.

The same server URL works in Claude Code, because Claude Code's MCP host attaches an OAuth client provider to HTTP transports automatically and runs the OAuth + PKCE browser flow on the first 401. ZCode already contains that OAuth machinery, but it is not wired into the config-file MCP path.

Environment

  • ZCode: 0.15.0
  • OS: macOS (Darwin 25.5.0, arm64)
  • Config scope: user (~/.zcode/cli/config.json)

Configuration

{
  "mcp": {
    "servers": {
      "atlassian": {
        "type": "http",
        "url": "https://mcp.atlassian.com/v1/mcp",
        "enabled": true,
        "timeoutMs": 60000
      }
    }
  }
}

Repro

  1. Add the config above to ~/.zcode/cli/config.json.
  2. Restart ZCode (so the MCP subsystem reconnects).
  3. Open Settings → MCP.

Actual

  • Server atlassian shows as failed:
    Streamable HTTP error: Error POSTing to endpoint: {"error":"invalid_token","error_description":"Missing or invalid access token"}
  • The entry shows 0 tools and no Authenticate / Connect / Sign-in button.
  • The ZCode log (~/.zcode/cli/log/zcode-YYYY-MM-DD.jsonl) shows:
{"event":"mcp.server.failed",
 "context":{"mcpServerName":"atlassian","transport":"http",
   "error":"Streamable HTTP error: Error POSTing to endpoint:
   {\"error\":\"invalid_token\",\"error_description\":\"Missing or invalid access token\"}"}}```

- Crucially, the logs contain **no OAuth activity** at all — no `www-authenticate` handling, no authorization-server discovery, no PKCE, no token request. The request is sent with no `Authorization` header.

## Expected

On the first 401 from an OAuth-protected server, ZCode should follow the MCP authorization flow (discover protected-resource metadata from the `WWW-Authenticate` header, run Authorization Code + PKCE, open a browser for login, then retry with a Bearer token) — and expose an "Authenticate" affordance on the server entry, as the in-app strings ("Authenticate with OAuth", "Please authenticate", "Open in browser?") imply it is meant to.

## Root-cause analysis (from inspecting the app bundle)

ZCode bundles a full MCP OAuth implementation: `OAuthClientProvider`, dynamic client registration, PKCE (`code_verifier` / `code_challenge`, S256), `WWW-Authenticate` parsing, token refresh, and the GUI strings referenced above. However, the OAuth branch in the Streamable HTTP transport is gated on an auth provider being present:

```js
if (response.status === 401 && this._authProvider) { /* run OAuth flow */ }

For servers defined via config.json, the transport is constructed without an _authProvider, so the 401 falls straight through to the StreamableHTTPError(401, ...) path and no OAuth flow is ever attempted.

Compounding this, the mcp.servers schema is strict (an unknown key causes the server to be dropped) and exposes no auth / oauth field a user could set to opt in. So there is currently no file-level way to make an OAuth-protected remote server work.

Suggested fix (either or both)

  1. Preferred: when constructing the Streamable HTTP / SSE transport for a config-file MCP server, attach a default OAuthClientProvider (the capability already exists in-app), so remote OAuth servers work with no extra config — matching the behavior in Claude Code.
  2. Alternatively / additionally: add a recognized auth (or oauth) field to the mcp.servers schema, e.g. {"auth": {"method": "oauth"}}, letting users opt a server into the OAuth provider. This keeps the strict-schema behavior while making OAuth explicitly configurable.

Impact / context

This blocks any remote MCP server that uses OAuth (Atlassian Rovo is the common case) when configured via the standard config file — which is the documented primary path. A workaround (running a local stdio server that authenticates with a static API token) exists but gives a worse experience (API token to manage, Jira-only, no Confluence/Compass) and diverges from the seamless OAuth flow available in peer tools.

I'd like ZCode + GLM to be a drop-in replacement for Claude Code for this class of integration; wiring OAuth into the config-file path would close that gap.

Supporting evidence available

  • Full ZCode connection logs (the mcp.server.connect.started / mcp.server.failed events, with zero OAuth entries).
  • The app-bundle strings confirming the OAuth implementation and the _authProvider gate.
  • Side-by-side: identical server URL works in Claude Code (OAuth login completes in-browser on first connect).

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Fields

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