Skip to content

bug(core): MCP OAuth drops registrationUrl from WWW-Authenticate discovery, breaking Atlassian remote MCP #12165

Description

@BaraaJayousi

What happened?

Connecting to the Atlassian remote MCP server (https://mcp.atlassian.com/v2/mcp) with OAuth enabled is impossible: pressing Auth in /mcp fails immediately, before any browser is opened:

OAuth Authentication
Server: atlassian
Starting OAuth authentication for MCP server 'atlassian'...
No client ID provided and dynamic registration not supported

Root cause: MCPOAuthProvider.authenticate() discards the registration_endpoint returned by the WWW-Authenticate → protected-resource-metadata → authorization-server-metadata discovery chain, then falls back to an origin-only metadata lookup. For Atlassian, the registration endpoint only exists on a path-scoped authorization server (https://auth.atlassian.com/<tenant-id>), so the origin-only lookup finds nothing and the flow aborts.

The value is available — OAuthUtils.metadataToOAuthConfig() returns it, and the sibling discovery branch copies it. One branch just forgets the field.

Verified on main (8e9e9887d1), packages/core/src/mcp/oauth-provider.ts:

// lines 743-751 — WWW-Authenticate discovery branch. NOTE: no `registrationUrl`.
config = {
  ...config,
  authorizationUrl: discoveredConfig.authorizationUrl,
  tokenUrl: discoveredConfig.tokenUrl,
  scopes: discoveredConfig.scopes || config.scopes || [],
  // Preserve existing client credentials
  clientId: config.clientId,
  clientSecret: config.clientSecret,
};
// line 772 — the other discovery branch DOES copy it
registrationUrl: discoveredConfig.registrationUrl,
// lines 787-812 — fallback re-discovers from the bare origin, losing the tenant path
let registrationUrl = config.registrationUrl;
if (!registrationUrl) {
  const authUrl = new URL(config.authorizationUrl);
  const serverUrl = `${authUrl.protocol}//${authUrl.host}`;   // <-- path discarded
  const authServerMetadata =
    await OAuthUtils.discoverAuthorizationServerMetadata(serverUrl);
  registrationUrl = authServerMetadata.registration_endpoint; // <-- undefined for Atlassian
}
// line 830 — the observed error
throw new Error('No client ID provided and dynamic registration not supported');

Supporting code: packages/core/src/mcp/oauth-utils.ts — metadataToOAuthConfig() at line 213 (returns registrationUrl), discoverAuthorizationServerMetadata() at line 231, discoverOAuthFromWWWAuthenticate() at line 391.

Evidence from Atlassian

  1. MCP endpoint issues a resource_metadata challenge:
$ curl -sI https://mcp.atlassian.com/v2/mcp | grep -i www-authenticate
HTTP/2 401
www-authenticate: Bearer resource_metadata="https://mcp.atlassian.com/.well-known/oauth-protected-resource/v2/mcp"
  1. Resource metadata names a path-scoped authorization server:
$ curl -s https://mcp.atlassian.com/.well-known/oauth-protected-resource/v2/mcp
{"resource":"https://mcp.atlassian.com/v2/mcp",
 "authorization_servers":["https://auth.atlassian.com/VCeDsk8ZHncYF1g234fKtc4lNipbBhu3"],
 "scopes_supported":["read:me","read:account","offline_access", ... 34 scopes in total]}
  1. That authorization server does advertise DCR:
$ curl -s https://auth.atlassian.com/.well-known/oauth-authorization-server/VCeDsk8ZHncYF1g234fKtc4lNipbBhu3
{"authorization_endpoint":"https://auth.atlassian.com/authorize",
 "token_endpoint":"https://auth.atlassian.com/oauth/token",
 "registration_endpoint":"https://auth.atlassian.com/VCeDsk8ZHncYF1g234fKtc4lNipbBhu3/dcr/register",
 "code_challenge_methods_supported":["S256"],
 "token_endpoint_auth_methods_supported":["none","client_secret_post","client_secret_basic","private_key_jwt"]}
  1. The origin-only metadata the fallback uses has no registration_endpoint — this is the dead end:
$ curl -s https://auth.atlassian.com/.well-known/oauth-authorization-server \
    | python3 -c "import json,sys; print('registration_endpoint' in json.load(sys.stdin))"
False
  1. DCR at the tenant endpoint works unauthenticated, so the dropped URL is the only blocker:
$ curl -s -X POST https://auth.atlassian.com/VCeDsk8ZHncYF1g234fKtc4lNipbBhu3/dcr/register \
    -H 'Content-Type: application/json' \
    -d '{"client_name":"Qwen Code MCP Client","redirect_uris":["http://localhost:7777/oauth/callback"],"grant_types":["authorization_code","refresh_token"],"response_types":["code"],"token_endpoint_auth_method":"none","code_challenge_method":["S256"],"scope":""}'
HTTP 201
{"client_id":"<redacted>","client_id_issued_at":...,"client_name":"Qwen Code MCP Client",
 "client_profile":"ai.dcr3p","client_secret":"<redacted>","client_secret_expires_at":0,
 "redirect_uris":["http://localhost:7777/oauth/callback"],"token_endpoint_auth_method":"none"}
  1. Replaying the exact authorize URL the provider would build is accepted by Atlassian (client_id, redirect_uri, PKCE S256, all 34 scopes, and resource=https://mcp.atlassian.com/v2/mcp):
$ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' '<authorize-url built from the above>'
302 https://id.atlassian.com/login?continue=...

What did you expect to happen?

Auth should open the browser and reach Atlassian's consent screen with no extra configuration, because the server advertises a registration endpoint and supports unauthenticated DCR.

The WWW-Authenticate discovery path should carry registrationUrl exactly like the discoverOAuthFromMCPServer path does.

Steps to reproduce

  1. Configure the server:
{
  "mcpServers": {
    "atlassian": {
      "httpUrl": "https://mcp.atlassian.com/v2/mcp",
      "oauth": { "enabled": true }
    }
  }
}
  1. Run qwen, then /mcp → atlassian → Auth.
  2. Fails immediately with the error above; no browser is launched. Reproducible every time.

Workaround

Set the registration URL explicitly, which skips the broken origin fallback. The rest of the flow works as-is (verified: DCR 201, authorize URL 302 to Atlassian login):

"oauth": {
  "enabled": true,
  "registrationUrl": "https://auth.atlassian.com/VCeDsk8ZHncYF1g234fKtc4lNipbBhu3/dcr/register"
}

Suggested fix

Minimal: add the missing field to the WWW-Authenticate branch.

config = {
  ...config,
  authorizationUrl: discoveredConfig.authorizationUrl,
  tokenUrl: discoveredConfig.tokenUrl,
  scopes: discoveredConfig.scopes || config.scopes || [],
  registrationUrl: discoveredConfig.registrationUrl,   // <-- add
  clientId: config.clientId,
  clientSecret: config.clientSecret,
};

More robust, since the hand-copied field list is what caused this: merge the whole discovered object (config = { ...config, ...discoveredConfig, clientId: config.clientId, clientSecret: config.clientSecret }), or have the fallback at line ~787 consult the resource metadata's path-preserving authorization_servers[0] instead of new URL(config.authorizationUrl) origin. That would fix any spec-compliant authorization server whose metadata is path-scoped rather than host-rooted.

Related observation

Because the same branch does scopes: discoveredConfig.scopes || config.scopes || [], Atlassian's full 34-scope scopes_supported list overrides any oauth.scopes the user pins, so the consent screen always requests every scope. Probably worth a separate look.

Client information

Client Information
$ qwen /about
Qwen Code v0.24.0
Model: deepseek-v4-flash
Fast Model: not set
Auth: openai
Platform: linux x64 (6.6.87.2-microsoft-standard-WSL2)
Node.js: v22.23.2
Session: 70277a6d-123b-438a-80db-329efdb89997
Git commit: 56b003be07
LSP: disabled

Platform: Linux (WSL2). 0.24.0 is the latest published version on npm, and the bug is still present on main.

Login information

OpenAI-compatible provider (DeepSeek) via modelProviders / API key — unrelated to the MCP server's OAuth flow.

Anything else we need to know?

  • qwen mcp list loads the config fine; the server simply reports Disconnected since it can never authenticate.
  • Setting registrationUrl in the oauth block is an undocumented but supported key (MCPOAuthProvider reads it, and it survives config canonicalization), so the workaround is safe.
  • The Atlassian tenant ID is not personal — it's returned by Atlassian's public, unauthenticated protected-resource metadata. If Atlassian rotates it, it can be re-read from https://mcp.atlassian.com/.well-known/oauth-protected-resource/v2/mcp.

Activity

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

    category/coreCore engine and logicpriority/P2Medium - Moderately impactful, noticeable problemscope/mcpModel Context Protocolscope/oauthOAuth authentication flowstype/bugSomething isn't working as expected

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions