# Ship Day Was 7 September. The Advisory Was 21 Days Later.

> The primary is GitHub advisory GHSA-qx49-fqc8-xw99 from 28 September 2026. An affected MCP Python client can send its client secret, authorization code, and PKCE verifier to a token endpoint the server chooses. Versions 1.30.0 and 2.2.0 shipped the issuer check on 7 September as a behaviour change. Pass issuer on machine providers, then rotate any secret that already left.

Source: https://hackerlogs.com/blog/mcp-python-sdk-issuer-clock
Published: 2026-09-29

## Key takeaways

- The primary is GHSA-qx49-fqc8-xw99 on the mcp package. High is 7.5 for the two unattended providers. The interactive provider, which needs a person to start sign-in, is scored 6.5. The advisory lists no CVE.
- The package clock is 7 September. mcp 1.30.0 published at 14:03 UTC and 2.2.0 at 15:53 UTC. Both release notes file the issuer check under Behaviour changes, not as a security fix.
- The disclosure clock is 28 September, when the advisory and Cycode's write-up landed. The Hacker News ran it on 29 September. Twenty-one days of installs sat between the tag and the label.
- Upgrade is not the whole fix. ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider still follow the server until you pass issuer. On 1.30.0 that omission is a DeprecationWarning, which Python hides by default.
- The impact clock is the secret. The advisory does not report exploitation in the wild. A client secret that already left keeps working until you rotate it and revoke tokens at the login service.

On 28 September the maintainers of the official MCP Python SDK published [GHSA-qx49-fqc8-xw99](https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-qx49-fqc8-xw99). A malicious or compromised MCP server can choose where an affected HTTP client's OAuth credentials are sent. The client secret, the authorization code, and the PKCE verifier meant for a real login service can arrive at a token endpoint that server named. The fix tags, 1.30.0 and 2.2.0, had already been on PyPI for 21 days, filed as behaviour changes.

Hacker News on 29 September led with GPT-6.1 Sol, a privacy paper on conversational agents, and OpenAI's Dots launch. US X trends were baseball, a Senate fight, and fashion. Dots and GPT-6.1 Sol trended as product launches. The operator record is the advisory.

 Official client SDK, unattended OAuth, secret leaves toward a server-chosen issuer. High because the advisory scores the unattended providers at 7.5, not because an in-the-wild attack has been named.

<img src="/mcp-python-sdk-issuer-clock.svg" alt="Three panels for the MCP Python SDK OAuth advisory: a server-chosen login issuer, a 21-day gap between the 7 September tags and the 28 September advisory, and the issuer argument that upgrade alone does not set." width="1200" height="675" />

## Who the advisory says is affected.

The package is `mcp` on PyPI. Affected ranges on the advisory are 1.9.1 up to but not including 1.30.0, and 2.0.0a1 up to but not including 2.2.0. You are in scope only if both of these hold: the app uses the SDK as an MCP client over HTTP with `OAuthClientProvider`, `ClientCredentialsOAuthProvider`, `PrivateKeyJWTOAuthProvider`, or the deprecated 1.x `RFC7523OAuthClientProvider`, and it can connect to an MCP server you do not fully trust while holding credentials for a real login service.

The 1.x line, 1.9.1 through 1.29.1, had no issuer check and no credential binding on any path. On 2.0.0 through 2.1.1 the checks are missing when the server publishes no protected resource metadata, the legacy fallback, or when it answers `403 insufficient_scope`. On both lines, the two machine-to-machine providers had no way to name the authorization server their credentials belong to. That last gap survives a version bump until you pass `issuer`.

The advisory's own account of the miss is short. The server can name its own authorization server in protected resource metadata, or publish none and serve metadata that presents the user's real authorization server as the issuer. The client then sends the secret, the code, and the verifier, or a signed client assertion for `PrivateKeyJWTOAuthProvider`, to a token endpoint of the server's choosing. [Cycode](https://cycode.com/blog/mcp-python-sdk-oauth-account-takeover/), one of eight reporters on the advisory, says it completed that exchange in a test and that the resulting token carries the permissions the app was granted. This brief does not repeat the steps.

> The tag is the package clock. It is not the fix for the two providers that run with no person present. The advisory says upgrading changes nothing for them until you pass `issuer`. On 1.30.0 the missing argument is a `DeprecationWarning`, which Python hides by default. A quiet install log is not evidence the issuer is bound.

## The package clock and the impact clock.

[v1.30.0](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v1.30.0) published at 14:03 UTC on 7 September. [v2.2.0](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.2.0) published at 15:53 UTC the same day. Both notes lead with "Behaviour changes." The 1.x note says the OAuth client now rejects authorization-server metadata whose issuer does not match the server it was fetched for, and remembers registrations per issuer. The 2.x note is narrower, and you should read it that way: the protected-resource-metadata path has checked issuer since 2.0, and 2.2.0 extends the check to the legacy path. Both notes add `issuer=` on the two machine providers and say the old construction keeps working in this major version.

That is a maintenance heading. It is not a security advisory. The advisory itself is 28 September, the same day Cycode published. [The Hacker News](https://thehackernews.com/2026/09/official-mcp-python-sdk-flaw-can-let.html) ran the story on 29 September and said no CVE had been assigned. The advisory page still lists no CVE ID. Twenty-one days is the gap between a behaviour change a careful reader could have applied and a label that tells everyone else to look.

> The impact clock starts if an affected client already talked to a server you do not trust. The advisory's past-exposure line is to rotate the client secret and revoke tokens at the authorization server. Cycode's point, which matches that line, is that the client secret outlives the session. Pulling a new SDK build does not expire a secret that already left.

After the upgrade, stored registrations are a second clock. A registration stored without an issuer stays unbound. That includes everything 1.x stored before 1.30.0. Clear stored OAuth client information once so the client registers again. If you placed a pre-registered client in storage yourself, set `issuer` on that record instead of wiping it and hoping.

## What this is not.

It is not a bug in MCP servers you built with this SDK. It is not a stdio client. It is not a client that attaches its own bearer token and never runs these OAuth providers.

It is not CVE-2026-52869. That June advisory is session mixing on the server transports, fixed in 1.27.2. Patching it does not bind an OAuth issuer.

It is not a confirmed intrusion. The advisory does not describe an attack in the wild. The Hacker News says Cycode does not either. Absence of a named victim is not a reason to skip rotation if an untrusted server was already reachable.

It is also not a 2.x story in which nothing changed until 7 September. The 2.2.0 notes say the protected-resource-metadata path already checked issuer. The hole that remained was the legacy fallback, the 403 step-up, and machine credentials with no issuer of their own.

## What to do.

1. Find HTTP MCP clients on `mcp` that use one of the four OAuth providers and can reach a server you do not fully control. Stdio and "we attach the token ourselves" stay off this list.
2. Upgrade the 1.x line to 1.30.0 or later, and the 2.x line to 2.2.0 or later. Read the behaviour-change notes. The redirect and idle-session defaults shipped in the same tags.
3. Pass `issuer` on `ClientCredentialsOAuthProvider` and `PrivateKeyJWTOAuthProvider`, set to the login service those credentials belong to. Treat a missing argument as a review failure, including where Python swallows `DeprecationWarning`. Retire `RFC7523OAuthClientProvider`. It has no `issuer` argument.
4. Clear stored OAuth client registrations once, or set `issuer` on a record you inserted yourself. A pre-1.30.0 registration stays unbound if you leave it.
5. If that client may already have connected to an untrusted MCP server, rotate the client secret and revoke its tokens at the authorization server. Do this even when the new build is installed.
6. On versions before the fix, the advisory's only workaround is to connect OAuth clients only to MCP servers you trust. Do not treat that sentence as a substitute for the upgrade.

The package clock stopped on 7 September for anyone who read a behaviour change as a security fix and also passed `issuer`. Everyone else got the label on 28 September. The secret, if it left, is still on the impact clock.

## Sources

- [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) (2026-09-28)
- [mcp v1.30.0](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v1.30.0) (2026-09-07)
- [mcp v2.2.0](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.2.0) (2026-09-07)
- [Cycode Uncovers Account Takeover in Anthropic's MCP Python SDK](https://cycode.com/blog/mcp-python-sdk-oauth-account-takeover/) (2026-09-28)
- [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) (2026-09-29)
