Don't Let an MCP Server Choose Your OAuth Issuer
TLDR;
The maintainers of the official MCP Python SDK published an advisory covering its OAuth client support. In affected versions, the MCP server could influence where a client’s OAuth credentials were sent. A malicious or compromised server could direct the client to an attacker-controlled token endpoint and collect the client secret, authorization code, and PKCE code_verifier.
Cycode, which reported the issue, says it demonstrated a full exchange for a valid access token. The advisory rates the issue High: 7.5 for unattended providers and 6.5 for the interactive one. As of the September 29 news report, no CVE had been assigned.
My view is simple: the client should know its OAuth issuer before it talks to the MCP server. Don’t let the server nominate the identity provider that receives your credentials.
The Server Chose the Token Endpoint
The normal flow sounds reasonable. The client asks the MCP server which authorization server handles login, fetches that server’s metadata, then checks whether the metadata issuer matches the expected issuer. That comparison is the control that matters.
According to Cycode, one fallback path skipped it. If the server returned 404 to the first discovery request, the SDK switched to a legacy method and asked the server directly. There was no expected URL to compare with, so validation never happened.
The advisory describes another route. A server can omit resource metadata, present metadata naming the user’s real authorization server as the issuer, and point the token endpoint somewhere else.
That second route exposes the underlying design problem. Stored or pre-provisioned credentials were not bound to the authorization server they belonged to. A client could hold a secret for Okta, for example, without recording that the secret was only valid for Okta.
Picture a server that merely seems unreliable. You sign in through the genuine login page, the MCP connection fails, and you move on. Cycode says the victim sees that failure while the attacker receives a token. That’s exactly the sort of failure people stop investigating after the second attempt.
Check Whether Your Client Is Exposed
The advisory says you are affected when both conditions apply:
- You use the SDK as an HTTP MCP client with
OAuthClientProvider,ClientCredentialsOAuthProvider,PrivateKeyJWTOAuthProvider, or the deprecated 1.xRFC7523OAuthClientProvider. - The client may connect to a server you do not fully trust while it holds credentials for a legitimate authorization server.
| Line | Affected | Fixed in |
|---|---|---|
| 1.x | 1.9.1 through 1.29.1 | 1.30.0 |
| 2.x | 2.0.0 through 2.1.1 | 2.2.0 |
The advisory excludes SDK-built servers, stdio clients, and clients that attach their own tokens or headers.
That scope is worth reading carefully. This is a client-side OAuth problem. A server can be the hostile party even when its advertised identity flow looks normal.
What I’d Patch First
- Upgrade to 1.30.0 or 2.2.0 or later.
- For
ClientCredentialsOAuthProviderorPrivateKeyJWTOAuthProvider, passissuer=with your authorization server’s issuer URL. The advisory is explicit: upgrading alone does not fix this configuration. Withoutissuer=, those providers still follow what the server advertises. - Move off
RFC7523OAuthClientProvider, which has no issuer option. - Clear stored OAuth client registrations once. Registrations saved without an issuer remain unbound, including registrations stored by 1.x before 1.30.0.
- If the client may have connected to an untrusted server, rotate its client secret and revoke its tokens at the authorization server.
I’d focus on step 2 before treating the upgrade as complete. The version change gives the client a safer path; your configuration tells it where that path should lead.
On 1.30.0, the missing-issuer warning is a DeprecationWarning, which Python hides by default. Run the test suite with warnings visible, or treat them as errors, so an omitted issuer= does not disappear into routine test output.
Discovery Is Not a Trust Decision
The fixed client determines the expected issuer before fetching metadata, rejects a mismatch with OAuthFlowError, and binds registrations to that issuer. That’s the design I’d want.
Metadata from the MCP server is still useful for discovery. It is not a trust anchor. The server is making a claim about where your client should send a secret, and a self-consistent claim is still only a claim.
This flaw is a good example of why fallback paths deserve the same review as the main path. The normal issuer check existed. The fallback path had no expected issuer, so there was nothing meaningful to validate against.
The interactive flow does not save you here. Cycode says the page the user approves is the genuine one. The problem comes afterward, when the client sends material to the token endpoint selected through server-controlled metadata.
For older versions, the advisory says there is no workaround beyond connecting OAuth-enabled clients only to servers you trust.
Version 1.30.0 also changed redirect handling so clients follow redirects only within the endpoint’s origin. Sensible hardening.
Final Thought
Anything that determines where a secret goes should come from your configuration. Discovery can help a client find endpoints; it should never decide who gets trusted with credentials.
References and Further Reading
-
GitHub Security Advisory — OAuth client could send credentials to an authorization server chosen by the MCP server https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-qx49-fqc8-xw99
-
Cycode — Cycode Uncovers Account Takeover in MCP Python SDK https://cycode.com/blog/mcp-python-sdk-oauth-account-takeover
-
GitHub — Release v1.30.0, modelcontextprotocol/python-sdk https://github.com/modelcontextprotocol/python-sdk/releases/tag/v1.30.0
-
The Hacker News — Official MCP Python SDK Flaw Can Let Malicious Servers Steal OAuth Credentials https://thehackernews.com/2026/09/official-mcp-python-sdk-flaw-can-let.html
