Skip to content

MCP OAuth "Sign in" fails with "MCP server probe returned HTTP 400" for stateful servers (Atlassian /v2/mcp) when a valid token is already stored #5014

Open

Description

@nischalpanwalametrc

Summary

Since runtime 1.0.90-0 (GitHub Copilot desktop app 1.1.24, Windows 11), the Atlassian MCP server shows Sign in in the MCP panel, and clicking it fails with:

Authentication failed
atlassian: RPC error -32603: Request session.mcp.oauth.login failed with message: MCP request failed: MCP server probe returned HTTP 400 Bad Request

The server itself works: regular sessions connect (server/discover returned an unusable JSON response; retrying with legacy initialize → Service initialized ... atlassian-mcp-server) and Jira/Confluence tools run fine with the stored token.

Config

"atlassian": { "type": "http", "url": "https://mcp.atlassian.com/v2/mcp", "tools": ["*"] }

OAuth via dynamic client registration; a valid access + refresh token is stored in ~/.copilot/mcp-oauth-config/.

Root cause (reproduced by hand)

The OAuth probe (remote_probe.rs) sends a JSON-RPC ping without an Mcp-Session-Id. Atlassian's endpoint is a stateful Streamable HTTP server:

POST to https://mcp.atlassian.com/v2/mcp Response
ping, no token 401 + WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource/v2/mcp"
ping, valid token (MCP-Protocol-Version 2026-07-28, 2025-11-25, or omitted) 400 {"jsonrpc":"2.0","error":{"code":-32600,"message":"Request must be an initialize request if no session ID is provided."}}
initialize, valid token 200 + Mcp-Session-Id

So sign-in fails only when the user is already authenticated: the probe passes auth and then hits the server's session check, and the 400 is treated as fatal instead of "authenticated".

curl -i -X POST https://mcp.atlassian.com/v2/mcp \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" -H "MCP-Protocol-Version: 2026-07-28" \
  -d '{"jsonrpc":"2.0","id":1,"method":"ping","params":{}}'

Why "Sign in" is shown in the first place

The app's MCP status session (global scope) never re-attaches the stored token for this third-party server. Its first connect gets 401, the host logs MCP OAuth non-first-party server; cancelling host-token and kicking daemon-owned reconnect so the status badge can resolve, and the reconnect gets 401 again, so the status stays "needs auth". Regular sessions in the same app handle the same initial 401 internally and connect. On 1.0.87-0 the same kick was followed by MCP refresh generation bumped and a token refresh within seconds.

Expected

  • The probe treats a 400 with JSON-RPC -32600 "initialize required / no session ID" (or any non-401/403 reply to an authenticated request) as authenticated, or probes with initialize and then DELETEs the session, matching the connector's server/discover → legacy initialize fallback.
  • The MCP status session reuses stored third-party OAuth tokens the way regular sessions do, so the badge doesn't ask for a sign-in that isn't needed.

Steps to reproduce

  1. Configure the Atlassian server as above and sign in once, so tokens are stored.
  2. Restart the app and open the MCP servers panel. Atlassian shows "Sign in", although tools work in sessions.
  3. Click Sign in. The error above appears.

Activity

  1. Ben-Home commented on Sep 30, 2026

    @Ben-Home

    The table in this report is the whole story: a valid token plus a non-initialize POST returns 400 -32600 "Request must be an initialize request if no session ID is provided.", and the probe reads that as not authenticated. The token proved out; the probe just asked the wrong question.

    Two decoupling points worth separating, because they fail independently:

    1. Liveness probe vs. auth state. ping is not a safe auth probe against a session-gated endpoint. The cleanest check is the same one your connector already negotiated for the data path: try initialize, accept 200 + Mcp-Session-Id as authenticated, then DELETE /mcp with Mcp-Session-Id to discard the probe session. That mirrors the server/discover → legacy initialize fallback you already rely on, so the probe and the real connection agree on what "reachable" means. If you would rather keep ping, the rule should be: only 401/403 means unauthenticated. Any other HTTP status from a request that carried a valid bearer token, 400, 405, 406, means the token was accepted and the request was rejected. Conflating those two is exactly how a working credential turns into a permanent "Sign in" badge.

    2. Session affinity on /v2/mcp. The 400 is really the server telling you it needs session establishment, which is a transport-state concern rather than an auth one. A stateful MCP host has three states per server, no token, token but no session, token and session, and only the first should surface a sign-in prompt. Worth persisting the returned Mcp-Session-Id alongside the token so a restart re-attaches to the existing session instead of forcing a fresh initialize.

    3. Status-session token reuse. The second half of your report is arguably the root: the global-scope status session kicks the daemon-owned reconnect but does not re-attach the stored third-party token, so it 401s forever while regular sessions succeed with the same credentials. If the status session reused the token the same way regular sessions do, the badge would never have flipped to "needs auth", and this 400 would have surfaced as a benign retry instead of a dead-end. Worth making token attachment a property of the credential store, not of the calling surface.

    On scope-challenge handling in the same family: when a server answers with WWW-Authenticate: Bearer error="insufficient_scope", the challenged scope parameter should be merged into the next authorization request rather than replayed from the previously held set. We hit that class on multi-source OAuth connectors at corpusiq.io, a connector holding four scopes never requests the fifth after a step-up challenge unless the authorization request is built from the challenge, not from the cache. Building it from the challenge makes it surface-independent.

    Small diagnostic for the report if it helps: the -32600 wording ("if no session ID is provided") is a server-specific string, but the shape is generic, a non-401/403 JSON-RPC error on an authenticated request should be classified as reachable, request-invalid and never mapped onto the auth badge.

  2. added
    area:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registry
    and removed on Oct 3, 2026
  3. patniko commented on Oct 26, 2026

    @patniko
    Collaborator

    Thanks for the report. We've shipped fixes in this area since this was filed, and we believe this may now be resolved. Could you retest on the latest release (Copilot CLI v1.0.103) and let us know whether you still see it? If we don't hear back in a couple of weeks, we may close this issue — you can always reopen it.

    Triaged as part of a Copilot SDK backlog review, with help from GitHub Copilot.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registrymore-info-needed

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions