You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
OAuth handler doesn't support redirect URLs with params #2776
This assumes auth_endpoint has no existing query string. When it does (like Salesforce's ?prompt=select_account), you get ?prompt=select_account?response_type=code&... — double ?, broken URL.
Fix
Use urllib.parse to decompose and reconstruct:
fromurllib.parseimporturlparse, urlencode, parse_qs, urlunparseparsed=urlparse(auth_endpoint)
existing_params=parse_qs(parsed.query)
# Merge: existing params take precedence if there's a conflictmerged= {**auth_params, **existing_params}
new_query=urlencode(merged, doseq=True)
authorization_url=urlunparse(parsed._replace(query=new_query))
The merge order matters: if the authorization server already specified a prompt param, it probably intends that to be honored. Server params should win over client-added defaults.
Edge case: fragment handling
If the auth_endpoint somehow has a fragment (#), urlparse handles it correctly — fragments are preserved in parsed.fragment and carried through urlunparse.
Production pattern
At CorpusIQ (corpusiq.io), we hit the same bug with multiple OAuth providers (Salesforce, HubSpot, Zoho all use pre-parameterized auth URLs). We wrapped the URL construction in a build_authorization_url() helper with the urlparse + merge pattern above. It's been stable across 36+ OAuth connectors.
Happy to open a PR with this fix if that's helpful.
We ran into this exact issue when implementing OAuth 2.1 PKCE for 25+ business API connectors.
The problem isn't just the redirect handler — it's that many OAuth providers (Google, Shopify, QuickBooks) require specific redirect_uri registration and will reject any URI that doesn't match exactly. If the server sends redirect_uri=https://auth.example.com/callback?provider=shopify but the handler strips the query params during the authorize call, the state parameter gets lost and the OAuth flow breaks silently.
The fix we landed on:
Parse the redirect URL with urllib.parse.urlparse() to preserve existing query params
Merge auth_params into the existing query string instead of replacing it
Reconstruct with urlunparse() so the full redirect_uri (including provider-specific params) survives
Without this, multi-tenant OAuth setups where each provider needs its own callback path break entirely. The current implementation only works for the simple ?code=xxx&state=yyy case.
Without response_type, the page stops treating the request as an authorization request. It renders a plain sign-in form with no consent panel, and sign-in lands on /welcome. No authorization code ever reaches the callback listener.
TypeScript SDK clients reach the same endpoint through new URL() and searchParams.set(), so they keep the env parameter and work. Only Python SDK clients break, which makes this easy to miss during provider testing.
Railway's authorization server rejects this outright with invalid_request (unlike the Salesforce/NinjaTrader cases where a page still renders), so the flow fails immediately rather than dead-ending at a consent screen.
Secondary symptom worth knowing about: callback port collisions
Because the failure is instant, client applications that retry the flow spin up a second localhost callback listener while the first attempt's listener is still bound, yielding:
OAuth callback port 27890 is already in use ([Errno 98] Address already in use)
In practice the user-visible errors alternate between invalid_request and Address already in use, which makes this look like a local networking/port problem and sends debugging in the wrong direction. Worth keeping in mind when triaging duplicate reports — "port already in use during MCP OAuth" reports may be this bug.
Fix verification
I can confirm the minimal form of the fix resolves it against Railway's live endpoint. Patching line 415 to join with & when the endpoint already carries a query string:
completes the full authorization-code + PKCE flow against Railway successfully (consent page renders, callback receives the code, tokens issued). #2779's urlparse-based merge is the more correct approach — it would handle this case as well, since Railway's resource param and the flow params have no key conflicts. +1 for rebasing that branch.
Disclosure: investigated and drafted with AI assistance; I reproduced the failure and verified the fix against Railway's live endpoints myself.
Activity
fix(client): preserve existing query params on OAuth authorization_en…
fix(client): preserve existing query params on OAuth authorization_en…
Ben-Home commentedon Jun 13, 2026
Good catch. This is a classic URL construction bug —
urlencodeon the full URL breaks when the base URL already has query parameters.Root cause
The current code at
oauth2.py#L356-L357does:This assumes
auth_endpointhas no existing query string. When it does (like Salesforce's?prompt=select_account), you get?prompt=select_account?response_type=code&...— double?, broken URL.Fix
Use
urllib.parseto decompose and reconstruct:The merge order matters: if the authorization server already specified a
promptparam, it probably intends that to be honored. Server params should win over client-added defaults.Edge case: fragment handling
If the
auth_endpointsomehow has a fragment (#),urlparsehandles it correctly — fragments are preserved inparsed.fragmentand carried throughurlunparse.Production pattern
At CorpusIQ (corpusiq.io), we hit the same bug with multiple OAuth providers (Salesforce, HubSpot, Zoho all use pre-parameterized auth URLs). We wrapped the URL construction in a
build_authorization_url()helper with theurlparse+ merge pattern above. It's been stable across 36+ OAuth connectors.Happy to open a PR with this fix if that's helpful.
Ben-Home commentedon Jun 13, 2026
We ran into this exact issue when implementing OAuth 2.1 PKCE for 25+ business API connectors.
The problem isn't just the redirect handler — it's that many OAuth providers (Google, Shopify, QuickBooks) require specific redirect_uri registration and will reject any URI that doesn't match exactly. If the server sends
redirect_uri=https://auth.example.com/callback?provider=shopifybut the handler strips the query params during the authorize call, the state parameter gets lost and the OAuth flow breaks silently.The fix we landed on:
urllib.parse.urlparse()to preserve existing query paramsauth_paramsinto the existing query string instead of replacing iturlunparse()so the full redirect_uri (including provider-specific params) survivesQuick snippet of what worked:
Without this, multi-tenant OAuth setups where each provider needs its own callback path break entirely. The current implementation only works for the simple
?code=xxx&state=yyycase.fix(client): preserve existing query params on OAuth authorization_en…
fix(client): preserve existing query params on OAuth authorization_en…
fix(client): preserve existing query params on OAuth authorization_en…
josh-ninjatrader commentedon Jul 28, 2026
A second production provider hits this, so here's another data point for #2776 plus a verification of #2779.
Reproduction against NinjaTrader
NinjaTrader's MCP server advertises an
authorization_endpointwith a query component:The
envparameter selects the demo or the live backend. With mcp 1.26.0, the client opens this URL:The consent page then parses the query as:
envdemo?response_type=codeclient_id15218response_typeWithout
response_type, the page stops treating the request as an authorization request. It renders a plain sign-in form with no consent panel, and sign-in lands on/welcome. No authorization code ever reaches the callback listener.TypeScript SDK clients reach the same endpoint through
new URL()andsearchParams.set(), so they keep theenvparameter and work. Only Python SDK clients break, which makes this easy to miss during provider testing.Verification of #2779
Checking out #2779 and running
_build_authorization_urlagainst the live metadata gives:envpromptresponse_typehttps://web.ninjatrader.com/oauth?env=demodemocodehttps://web.ninjatrader.com/oauth?env=livelivecodehttps://test.salesforce.com/services/oauth2/authorize?prompt=select_accountselect_accountcodehttps://auth.example.com/authorizecodeuv run --frozen pytest tests/client/test_auth.pygives 137 passed and 1 xfailed on that branch.The merge behavior looks right for both providers. Preserving duplicate keys and dropping
Nonevalues covers more than the reported case does.One blocker
The #2779 branch sits 50 commits behind
mainand no longer applies cleanly:A rebase might be all that stands between the branch and a review.
Happy to help test a rebased branch against the NinjaTrader endpoints if that's useful.
Disclosure. I used AI tooling to help investigate and to run the preceding checks. I reviewed the findings and reproduced them myself.
braden-dev commentedon Aug 29, 2026
Railway's MCP server is a third production provider affected by this.
Reproduction against Railway
With mcp 2.0.0, the client opens:
Railway's authorization server rejects this outright with
invalid_request(unlike the Salesforce/NinjaTrader cases where a page still renders), so the flow fails immediately rather than dead-ending at a consent screen.Secondary symptom worth knowing about: callback port collisions
Because the failure is instant, client applications that retry the flow spin up a second localhost callback listener while the first attempt's listener is still bound, yielding:
In practice the user-visible errors alternate between
invalid_requestandAddress already in use, which makes this look like a local networking/port problem and sends debugging in the wrong direction. Worth keeping in mind when triaging duplicate reports — "port already in use during MCP OAuth" reports may be this bug.Fix verification
I can confirm the minimal form of the fix resolves it against Railway's live endpoint. Patching line 415 to join with
&when the endpoint already carries a query string:completes the full authorization-code + PKCE flow against Railway successfully (consent page renders, callback receives the code, tokens issued). #2779's
urlparse-based merge is the more correct approach — it would handle this case as well, since Railway'sresourceparam and the flow params have no key conflicts. +1 for rebasing that branch.Disclosure: investigated and drafted with AI assistance; I reproduced the failure and verified the fix against Railway's live endpoints myself.