If someone has built an AI agent or an internal tool for your business in Python, and that tool connects to MCP servers over the internet, this is for you. MCP, the Model Context Protocol, is the standard way AI agents plug into outside tools: a CRM, a calendar, a ticketing system, a database. The official Python library for it, published on PyPI as mcp, was downloaded about 219 million times in the last month according to PyPI Stats. Most of that is automated builds, not separate businesses, but it gives a sense of how much software sits on it.
The short version: on affected versions, when an agent built with the library needed to log in somewhere on behalf of an MCP server, it asked that server where to log in and did not always check the answer. A malicious server could name a login address it controlled and collect the agent’s credentials. The fix is out, it is three weeks old, and for the two classes that log a machine in with a stored secret or key, installing it is not enough on its own.
Everything below was checked on September 30, 2026. Where a fact comes from the maintainers or the researchers, we say so. The reproduction, the release count and the warning test are ours, and each is written so you can repeat it.
What was published
The advisory is GHSA-qx49-fqc8-xw99, “OAuth client could send credentials to an authorization server chosen by the MCP server”, published on the SDK’s GitHub repository on September 28, 2026. The maintainers’ own summary:
In affected versions, the SDK’s OAuth client support (
mcp.client.auth) let the MCP server a client connected to decide where the client’s OAuth credentials were sent
It gives two causes: the authorization server metadata’s issuer was not validated on every discovery path, and stored or pre-provisioned client credentials were not bound to the authorization server they belong to. What can leak is the client secret, an authorization code, the PKCE code verifier that exists specifically to stop a stolen code being used, or a signed client assertion. With those, an attacker can ask the real login service for a valid access token.
| Package | mcp on PyPI |
| Affected | 1.9.1 up to (not including) 1.30.0, and 2.0.0a1 up to (not including) 2.2.0 |
| Fixed | 1.30.0 and 2.2.0, both published September 7, 2026 |
| Severity | High, CVSS 3.1 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N); the maintainers put the interactive login flow at 6.5 because a person has to approve a login page |
| CVE | None assigned at the time of writing |
| Weaknesses | CWE-345 (insufficient verification of data authenticity), CWE-522 (insufficiently protected credentials) |
| Reported by | Eight researchers are credited, among them Yuval Elbar of Cycode, whose write-up was published the same day |
| Exploited? | Neither the advisory nor Cycode reports any attack using it |
The classes affected are the four OAuth handlers an MCP client uses over HTTP: OAuthClientProvider (a person logs in through a browser), ClientCredentialsOAuthProvider (a machine logs in with a client ID and secret), PrivateKeyJWTOAuthProvider (a machine logs in with a signed key), and the deprecated 1.x RFC7523OAuthClientProvider.
The maintainers are just as clear about who is not affected: MCP servers built with the SDK, clients that talk to a local tool over stdio, and clients that attach their own tokens or headers rather than using the SDK’s OAuth support. If your agent only runs local tools, or only connects to MCP servers with a fixed API key header, this advisory does not describe you.
How the attack worked
An MCP server that needs a login answers the agent’s first request with “401, you need a token”. The agent then has to find out who issues tokens for this server. The current way is to fetch a small file from the server, the protected resource metadata, which names the login service. The agent then fetches the login service’s own metadata, which lists its token address.
Cycode’s write-up walks through what happens when the malicious server simply returns 404 for the first file. The SDK falls back to an older method and asks the MCP server itself for the login metadata. On that fallback path there was no expected issuer to compare against, so the check was skipped. The attacker’s server can return metadata whose issuer field names the victim’s real login service, which is what a quick look would expect to see, while the token_endpoint field points at an address the attacker controls. The agent sends its credentials there.
On the 1.x line up to 1.29.1, the advisory says neither protection existed on any path. On 2.0.0 to 2.1.1, both were missing only on that fallback and on a 403 insufficient_scope step up. For the two machine login classes, the advisory adds a third point that applies to every version: there was no way to tell them which login service their credentials belong to at all.
This is not an exotic setup. The MCP specification released on July 28, 2026 already required both rules. The maintainers’ announcement for it reads: “Client credentials are bound to the issuer that minted them. No reuse across authorization servers (SEP-2352).” The Python SDK fix is the library catching up with its own specification.
We ran it against the vulnerable and fixed versions
We wanted to know two things the write-ups do not answer directly: does upgrading alone stop the attack, and will a developer notice if they have not finished the job. So we installed mcp 1.29.1, 1.30.0, 2.1.1 and 2.2.0 from PyPI into separate environments and pointed a ClientCredentialsOAuthProvider with the client ID acme-agent and a dummy secret at a simulated malicious MCP server. The server ran inside the test through httpx’s MockTransport, so nothing left the machine. It answers every unauthenticated request with a 401, returns 404 for the protected resource metadata, and serves login metadata whose token address is its own.
We ran two versions of the malicious server:
- Scenario A, the lie. The metadata claims the issuer is the real login service,
https://auth.example.com, but the token address is the attacker’s. This is the attack in the advisory. - Scenario B, the honest attacker. The metadata names the attacker’s own server as the issuer, and its own token address. Nothing is spoofed. The server just says “log in with me”.
For each, we ran the provider as most existing code constructs it (no issuer=), and with the new issuer="https://auth.example.com" argument.
| Version | A, no issuer= |
A, with issuer= |
B, no issuer= |
B, with issuer= |
|---|---|---|---|---|
| 1.29.1 | Secret sent to attacker | argument does not exist | Secret sent to attacker | argument does not exist |
| 2.1.1 | Secret sent to attacker | argument does not exist | Secret sent to attacker | argument does not exist |
| 1.30.0 | Blocked: OAuthFlowError |
Blocked | Secret sent to attacker | Blocked |
| 2.2.0 | Blocked: OAuthFlowError |
Blocked | Secret sent to attacker | Blocked |
On the vulnerable versions, the attacker’s token address received the HTTP Basic header decoding to acme-agent:<secret> in every case. On both fixed versions, the lie is caught with the error Authorization server metadata issuer mismatch, which is the check the advisory describes.
Scenario B is the column to look at. On 1.30.0 and 2.2.0, a server that names its own login service still receives the client secret unless the code passes issuer=. That matches the advisory, which says that without the argument these providers “continue following whichever authorization server the MCP server advertises”, but it is easy to read past. For a machine login with a long lived client secret, it is the difference between fixed and not fixed. The rule to take away: a pre-provisioned secret should only ever be offered to the service that issued it, and after this release, only your code can say which service that is.
The warning you may never see
The fixed versions do warn when issuer= is missing. Whether you see it depends on the version line and on how Python treats warnings.
On 1.30.0 the warning is a DeprecationWarning. Python hides those by default unless they are triggered by code in the script you ran directly. We built the provider in two ways: once in the main script, and once inside a helper module that the main script imports, which is how nearly every real application is laid out. Under Python 3.11’s default settings, the first printed the warning and the second printed nothing. On 2.2.0 the warning is an MCPDeprecationWarning, which the SDK defines as a subclass of UserWarning, and it was printed in both cases.
So a team that upgraded a 1.x integration to 1.30.0 and saw clean logs has no reason to think anything is left to do. To surface it, run your tests with warnings turned into errors:
python -W error::DeprecationWarning -m pytest
How long the window was
We read the release history from PyPI. 1.9.1, the first affected version, was published on May 22, 2025. The fixes arrived on September 7, 2026, 473 days later. In between, 50 of the package’s 72 releases are in the affected ranges, including every 2.x release before 2.2.0, pre-releases included.
The fix itself was not labelled as a security fix when it shipped. The 1.30.0 release notes list “The OAuth client checks the authorization server’s issuer” under Behaviour Changes, next to a new redirect restriction and idle session limits, and the new issuer= argument under New Features. The advisory followed 21 days later. That is a normal and defensible way to handle a fix before disclosure, and it has a practical consequence: anyone who skipped 1.30.0 because the notes looked like routine maintenance was exposed for three more weeks without knowing it, and anyone whose dependency scanner only reads CVE identifiers still sees nothing, because there is none.
What to check
If you have a Python agent, chatbot back end or automation that uses MCP, these are the questions to ask, or to put to whoever built it.
-
Which version is installed? Run
pip show mcpin the environment the tool actually runs in, or look formcpinrequirements.txt,pyproject.tomlor the lock file. Anything from 1.9.1 to 1.29.1, or any 2.x before 2.2.0, is affected. -
Does it use the SDK’s OAuth support at all? Search the code for
OAuthClientProvider,ClientCredentialsOAuthProvider,PrivateKeyJWTOAuthProviderandRFC7523OAuthClientProvider. If none appear, the advisory does not apply, though upgrading is still sensible. -
Upgrade to 1.30.0 on the 1.x line or 2.2.0 on the 2.x line.
-
Pass
issuer=to everyClientCredentialsOAuthProviderandPrivateKeyJWTOAuthProvider, set to the issuer URL of the login service that gave you the credentials. It looks like this:provider = ClientCredentialsOAuthProvider( server_url="https://mcp.example-crm.com/mcp", storage=token_storage, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, issuer="https://auth.example-crm.com", )The maintainers say the argument becomes required in version 3.0. Replace any
RFC7523OAuthClientProvider, which has noissueroption. -
Clear stored registrations. The advisory notes that client registrations saved before the fix, which on 1.x means all of them, are not bound to an issuer. Clear the stored OAuth client information so the client registers again, or set
issueron pre-registered records. -
Rotate if it ever connected to a server you do not control. If an affected version was used against any MCP server that was not yours or a vendor you trust, rotate the client secret and revoke its tokens at the login service. There is no log on your side that will tell you whether it was taken.
The maintainers say there is no workaround for older versions other than connecting only to MCP servers you trust.
The part to remember
AI agents are sold on the idea that you can plug them into anything. Each thing you plug in gets to talk to the agent, and until September 7 an MCP server could also tell the official Python library where to send the agent’s keys. The library now checks. It still cannot know which login service your secret belongs to unless you tell it, and on the 1.x line it tells you that in a warning Python normally hides. One argument closes it.
This is the same lesson as the SalesBleed Agentforce flaws from the other direction: there the untrusted input was a lead typed into a web form, here it is the server on the other end of the connection. We covered the general rule for agents in our piece on prompt injection, and what happens when secrets meant for one service end up somewhere else in our Stripe API key post.
If someone built an AI integration for you and you would like a second pair of eyes on how it logs in to things, get in touch. We will tell you what we find, including when the answer is that you are fine.
Sources
- OAuth client could send credentials to an authorization server chosen by the MCP server (GHSA-qx49-fqc8-xw99), modelcontextprotocol/python-sdk, published September 28, 2026, retrieved September 30, 2026, for the quoted summary, the two causes, the affected and patched ranges, the CVSS 3.1 vector and the 6.5 score for the interactive provider, the four affected classes, who is not affected, the
issuer=requirement and its 3.0 plan, the quoted “continue following whichever authorization server the MCP server advertises”, stored registrations, rotation, the absence of a workaround, CWE-345 and CWE-522, the eight credited reporters, and the absence of a CVE - Cycode Uncovers Account Takeover in Anthropic’s MCP Python SDK, Yuval Elbar, Cycode, September 28, 2026, updated September 29, retrieved September 30, 2026, for the 404 fallback sequence, the metadata naming the real issuer while pointing the token endpoint elsewhere, and the absence of reported exploitation
- Official MCP Python SDK Flaw Can Let Malicious Servers Steal OAuth Credentials, The Hacker News, September 29, 2026, for the absence of a CVE identifier as of that date
- Release v1.30.0, modelcontextprotocol/python-sdk, September 7, 2026, for the “Behaviour Changes” and “New Features” headings and the issuer check listed under the first
- The 2026-07-28 Specification, Model Context Protocol blog, David Soria Parra and Den Delimarsky, July 28, 2026, for the quoted SEP-2352 sentence and SEP-2468
- MCP joins the Agentic AI Foundation, Model Context Protocol blog, December 9, 2025, for MCP’s move from Anthropic to the Linux Foundation’s Agentic AI Foundation
- mcp on PyPI, release history from the JSON API, retrieved September 30, 2026. The upload dates for 1.9.1 (May 22, 2025) and for 1.30.0 and 2.2.0 (September 7, 2026) are PyPI’s; the count of 50 affected releases out of 72, and the 473 and 21 day intervals, are ours
- PyPI Stats for mcp, retrieved September 30, 2026, for 219,043,071 downloads in the last month
- The reproduction, the table of results and the warning test are ours:
mcp1.29.1, 1.30.0, 2.1.1 and 2.2.0 installed from PyPI under Python 3.11.15,ClientCredentialsOAuthProviderdriven throughhttpx.MockTransport(httpx2on the 2.x line, which the SDK switched to) against a simulated server with no network access, andDeprecationWarningvisibility checked with Python’s default warning filters