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
- 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"
- 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]}
- 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"]}
- 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
- 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"}
- 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
- Configure the server:
{
"mcpServers": {
"atlassian": {
"httpUrl": "https://mcp.atlassian.com/v2/mcp",
"oauth": { "enabled": true }
}
}
}
- Run
qwen, then /mcp → atlassian → Auth.
- 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.
What happened?
Connecting to the Atlassian remote MCP server (
https://mcp.atlassian.com/v2/mcp) with OAuth enabled is impossible: pressing Auth in/mcpfails immediately, before any browser is opened:Root cause:
MCPOAuthProvider.authenticate()discards theregistration_endpointreturned by theWWW-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:Supporting code:
packages/core/src/mcp/oauth-utils.ts—metadataToOAuthConfig()at line 213 (returnsregistrationUrl),discoverAuthorizationServerMetadata()at line 231,discoverOAuthFromWWWAuthenticate()at line 391.Evidence from Atlassian
resource_metadatachallenge:registration_endpoint— this is the dead end:client_id,redirect_uri, PKCES256, all 34 scopes, andresource=https://mcp.atlassian.com/v2/mcp):What did you expect to happen?
Authshould 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-Authenticatediscovery path should carryregistrationUrlexactly like thediscoverOAuthFromMCPServerpath does.Steps to reproduce
{ "mcpServers": { "atlassian": { "httpUrl": "https://mcp.atlassian.com/v2/mcp", "oauth": { "enabled": true } } } }qwen, then/mcp→atlassian→ Auth.Workaround
Set the registration URL explicitly, which skips the broken origin fallback. The rest of the flow works as-is (verified: DCR
201, authorize URL302to Atlassian login):Suggested fix
Minimal: add the missing field to the
WWW-Authenticatebranch.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-preservingauthorization_servers[0]instead ofnew 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-scopescopes_supportedlist overrides anyoauth.scopesthe user pins, so the consent screen always requests every scope. Probably worth a separate look.Client information
Client Information
Platform: Linux (WSL2).
0.24.0is the latest published version on npm, and the bug is still present onmain.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 listloads the config fine; the server simply reportsDisconnectedsince it can never authenticate.registrationUrlin theoauthblock is an undocumented but supported key (MCPOAuthProviderreads it, and it survives config canonicalization), so the workaround is safe.https://mcp.atlassian.com/.well-known/oauth-protected-resource/v2/mcp.