Skip to content

mcp: snake_case keys in a remote server's oauth block are silently dropped, so auth falls back to dynamic client registration with a misleading error #55335

Open

Description

@jethac

Description

In opencode.json, a remote MCP server's oauth block written with snake_case keys (client_id, client_secret, callback_port, redirect_uri) is silently dropped. The server resolves to oauth: {}. opencode then tries dynamic client registration. For servers that do not support it, login fails with an error that does not mention the config:

■  Authentication failed
■  Incompatible auth server: does not support dynamic client registration

The camelCase keys (clientId, callbackPort, redirectUri) work. The docs/schema mismatch is #52177. This issue is about the runtime: an oauth key it does not know is discarded with no diagnostic. opencode already logs configuration normalization diagnostic … action="omitted unsupported legacy setting" for other unsupported fields, for example a model's attachment and reasoning. That warning never appears for unknown oauth keys.

Steps to reproduce

  1. Add a remote server whose auth server requires a pre-registered client, for example Slack's MCP server:
    "slack": {
      "type": "remote",
      "url": "https://mcp.slack.com/mcp",
      "oauth": { "client_id": "<id>", "callback_port": 3118, "redirect_uri": "http://localhost:3118/callback" }
    }
  2. opencode debug config shows "oauth": {} for the server.
  3. opencode mcp auth slack fails with "does not support dynamic client registration". The log shows mcp oauth client registration selected … registration=dcr.
  4. Change the keys to camelCase (clientId, callbackPort, redirectUri). debug config now shows the values, and auth succeeds.

Expected

Either accept both spellings, or emit a configuration diagnostic and a CLI warning for unknown oauth keys. The failure should say that the client ID was ignored, not that the server is incompatible.

OpenCode version

2.0.28 (CLI, @opencode/cli), macOS (Apple Silicon)

Related

Activity

  1. opencode-agent commented on Oct 14, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I was able to reproduce this on 2.0.28, and it still happens on the latest dev (3a69bae).

    I used a local mock MCP server whose auth server has no dynamic client registration endpoint:

    • With the flat mcp.<name> entry and "oauth": {"client_id": "abc123", "callback_port": 3118}, opencode mcp auth fails with Incompatible auth server: does not support dynamic client registration, and the log shows registration=dcr. No configuration diagnostic mentions the ignored oauth keys.
    • With clientId / callbackPort, the same server gets client_id=abc123 in the authorize URL and registration=static.

    It looks like the flat (v1-style) mcp.<name> form only reads camelCase OAuth fields and drops the rest without a diagnostic. The v2 mcp.servers.<name>.oauth form uses the snake_case names.

    One side note: in my runs opencode debug config showed "oauth": {} even for camelCase config, so it may not be a reliable way to check these values.

    The script below reproduces it. Related: #52177 (docs/schema naming mismatch) and #50807 (MCP timeout silently dropped from the normalized config).

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduces anomalyco/opencode#55335: snake_case keys in a (flat, V1-style) remote MCP
    # server's `oauth` block are dropped without a diagnostic, so `opencode mcp auth` falls back to
    # dynamic client registration and fails with "Incompatible auth server".
    # Usage: ./repro.sh                          (uses `opencode` from PATH)
    #        OC_DEV=/path/to/checkout ./repro.sh (runs the CLI from a source checkout)
    set -u
    HERE=$(cd "$(dirname "$0")" && pwd)
    P=$(mktemp -d); mkdir -p "$P/proj"; cd "$P/proj"; git init -q
    # Mock MCP resource + OAuth authorization server that has NO registration_endpoint (pre-registered clients only).
    cat > "$P/mock.ts" <<'TS'
    const port = 4555, base = `http://127.0.0.1:${port}`
    Bun.serve({ port, fetch(req) {
      const u = new URL(req.url)
      if (u.pathname.startsWith("/.well-known/oauth-protected-resource"))
        return Response.json({ resource: `${base}/mcp`, authorization_servers: [base] })
      if (u.pathname.startsWith("/.well-known/oauth-authorization-server") || u.pathname.startsWith("/.well-known/openid-configuration"))
        return Response.json({ issuer: base, authorization_endpoint: `${base}/authorize`, token_endpoint: `${base}/token`,
          response_types_supported: ["code"], code_challenge_methods_supported: ["S256"] })
      if (u.pathname === "/mcp") return new Response("unauthorized", { status: 401,
        headers: { "www-authenticate": `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource/mcp"` } })
      return new Response("not found", { status: 404 })
    }})
    TS
    bun "$P/mock.ts" & MOCK=$!; trap 'kill $MOCK 2>/dev/null' EXIT; sleep 1
    if [ -n "${OC_DEV:-}" ]; then
      OC=(bun --conditions=browser --preload=$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js $OC_DEV/packages/cli/src/index.ts)
    else OC=(opencode); fi
    oc() { "${OC[@]}" "$@"; }
    restart_service() {
      kill $(pgrep -f "serve --servic[e]") 2>/dev/null; sleep 1
      # From source, the auto-spawned service lacks the preload, so start it by hand.
      if [ -n "${OC_DEV:-}" ]; then (cd "$P/proj" && "${OC[@]}" serve --service >"$P/service.log" 2>&1 &); sleep 8; fi
    }
    echo "version: $(oc --version)"
    i=0
    for O in '{"client_id":"abc123","callback_port":3118}' '{"clientId":"abc123","callbackPort":3118}'; do
      i=$((i+1)); name="srv$RANDOM$i"
      restart_service   # make sure the background service re-reads config
      echo "{\"mcp\":{\"$name\":{\"type\":\"remote\",\"url\":\"http://127.0.0.1:4555/mcp\",\"oauth\":$O}}}" > opencode.json
      echo; echo "=== oauth block: $O"
      BROWSER=true timeout 20 "${OC[@]}" mcp auth "$name" < /dev/null 2>&1 | grep -a -o -E "client_id=[A-Za-z0-9]+|Incompatible auth server.*|Authentication failed" | sort -u
      LOG=$(oc debug paths 2>/dev/null | awk '$1=="log"{print $2}')/opencode.log
      grep -a "server=$name" "$LOG" 2>/dev/null | grep -a -o "registration=[a-z]*" | tail -1
      grep -a -i "normalization diagnostic" "$LOG" 2>/dev/null | grep -a -i oauth | tail -1 || true
    done
    kill $(pgrep -f "serve --servic[e]") 2>/dev/null

    Run it: bash repro.sh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions