Build1 publisher2 min readPublished
MCP Python SDK's credential-theft fix waits on an issuer argument for two OAuth providers
MCP Python SDK releases 1.30.0 and 2.2.0 leave a credential-theft bug open for two OAuth providers unless their constructors pass an issuer. For unattended MCP clients the fix is a one-argument code change, and each team has to find and make it in its own source.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- A malicious MCP server can steer the SDK's OAuth discovery so the client secret, authorization code and PKCE code verifier go to an attacker's token endpoint.
- The advisory has no CVE, reports no known exploitation in the wild, and scores 7.5 under CVSS 3.1.
- The fixed releases reached PyPI on September 7, three weeks before the advisory itself was published on September 28.
- A registration stored without an issuer stays unbound after the upgrade, so stored OAuth client information has to be cleared once to force re-registration.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint A dependency audit that sees mcp>=1.30.0 will pass a client whose providers still lack issuer=, so signing off on this advisory requires a search of the source code.
- exposure Teams that upgraded as soon as the releases shipped, before any advisory existed, have unattended clients exactly as exposed as unpatched ones unless someone added the argument.
- cost Any client that may have reached an untrusted MCP server before the fix needs its secret rotated and its tokens revoked at the authorization server, work no package upgrade can do.
According to the post, the defect is a missing identity check [18]. On an affected client, discovery runs like this:
1. The SDK asks the MCP server where its authorization server lives [9]. 2. If that lookup returns a 404, the SDK falls back to an older discovery method [9]. 3. On affected versions, the fallback never confirms that the issuer it reaches is the issuer it expected [9].
The post says that from the client's side, everything looks normal [17]. It cites Cycode's write-up for the short version: the check does not fail, it never runs [10]. The advisory files the bug under CWE-345, insufficient verification of data authenticity, and CWE-522, insufficiently protected credentials [11].
The 404 fallback is one path of several. According to the post, the advisory says 1.x releases 1.9.1 through 1.29.1 lacked issuer validation on every path. It says that in 2.x releases 2.0.0a1 through 2.1.1, the missing validation extended to the 403 step-up path and to credential binding as well [12].
The patched SDK binds a registration to an issuer. For ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider, that binding is opt-in [6]. The advisory says upgrading "changes nothing until you also pass issuer=" [5]. The working construction in the post adds one argument [15]:
```python provider = ClientCredentialsOAuthProvider( client_id="my-client", client_secret="s3cret", issuer="https://auth.example.com", ) ```
Leave the argument out and the SDK emits a DeprecationWarning. On the 1.x patch line, Python hides that warning class by default [14]. An old-style constructor on 1.30.0 therefore runs silently with no issuer check [2]. I think a warning is a defensible choice for a library, since a required argument would break every existing call on upgrade day.
The post's audit script checks the installed version, then greps for constructor calls without issuer= [16]. Grep is a reasonable first pass for a bug that lives in a keyword argument, and the author calls it a quick script [16]. The grep matches any line where either class name is followed by an opening parenthesis. It drops any hit whose line contains the string "issuer" [16]. The post's own fixed example puts issuer= three lines below the class name [15], so the script would report that call as missing [3]. That error is a nuisance. A one-line call passing issuer=None is worse: it contains the string, so it would drop out of the report [4].
What to watch
- Whether a later SDK release makes issuer= required for ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider, so the version check covers the fix again.
- Whether GHSA-qx49-fqc8-xw99 is assigned a CVE, which would surface it in scanners that key on CVE IDs.
- Any report of exploitation against unattended MCP clients built without issuer= after the September 28 disclosure.