Your MCP client hands OAuth secrets to any server that asks
The scene most MCP developers will run in the next month goes like this. Your agent is wired up to your own auth stack, an internal client_id and client_secret already registered against your identity provider, and today you add a third-party integration you found in a directory. The MCP client library is the official mcp package on PyPI. On the first tool call the third-party server answers 401 Unauthorized, the client does its OAuth dance, and your agent starts working. What you did not see is that the same handshake handed the third-party server your identity provider secret and the authorization code the browser round-trip produced. That is the whole of GHSA-qx49-fqc8-xw99, disclosed September 28, 2026 against the official Anthropic MCP Python SDK.
What the handshake was supposed to be
The MCP server is the OAuth protected resource; a separate authorization server hands out the token. The mcp.client.auth module follows a four step choreography every time a request comes back with 401. It reads the WWW-Authenticate header, fetches the RFC 9728 Protected Resource Metadata document at /.well-known/oauth-protected-resource, and picks out the authorization server it names. It fetches the RFC 8414 Authorization Server Metadata document from that authorization server, which lists the token_endpoint, registration_endpoint, and supported grants. If nothing is cached, it dynamically registers via RFC 7591 to obtain a client_id and client_secret. It generates a PKCE code_verifier and code_challenge, redirects the browser to the authorization endpoint, receives an authorization code on the redirect_uri, and POSTs code plus verifier plus client secret to the token_endpoint. The returned access token is stored and replayed on every subsequent request.
That last POST is the interesting one. Whatever host the client believes owns the token_endpoint receives everything the token exchange contains.
Where the client stopped trusting
The SDK had two gaps that together let the MCP server decide where that POST landed, and the advisory names both. The first gap: the authorization server metadata issuer value was not validated on every discovery path. RFC 8414 says the metadata document must include an issuer string that names, exactly, the authorization server it describes; a conforming client fetches the document, checks that issuer matches the URL it fetched it from, and refuses if the two disagree. On some code paths in mcp.client.auth that check was missing, so a malicious MCP server could publish a metadata document that named the victim's real identity provider as issuer while pointing token_endpoint, authorization_endpoint, and registration_endpoint at attacker infrastructure. The second gap: stored or pre-provisioned client credentials were not bound to the authorization server they belong to. A registration cached under one issuer could be replayed against a completely different one, because the storage layer never recorded which authorization server the credential was meant for.
Two bindings that a hardened OAuth client always makes, this one did not. The MCP server was in a position to break both.
What a malicious server actually walks away with
code_verifier posted with the code. With PrivateKeyJWTOAuthProvider, a signed JWT client assertion. Any of those alone is enough to mint tokens the victim's identity provider will accept.
Two attack shapes fall out of that. In the first, the MCP server names its own authorization server in the protected resource metadata; discovery walks straight into attacker infrastructure and the whole handshake completes with the attacker in the token endpoint seat. In the second, the metadata still lists the victim's real issuer and real authorization_endpoint, so the browser tab looks correct, but the token_endpoint points at attacker control. The user completes the browser round-trip against the real IdP, the redirect lands normally, and the client POSTs the code, verifier, and client secret to the attacker. The attacker forwards to the real token endpoint, receives the access token, and returns it to the victim client. Nothing looks wrong; the agent begins tool calls with a token the attacker also holds.
The ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider paths do not need the browser round-trip at all. They skip registration and go straight to a token endpoint, so a bad metadata document silently reroutes the client secret or JWT client assertion on the very first call. Those two providers stay exposed after you upgrade unless you also pass an explicit issuer parameter, which is the piece of the fix most likely to get missed in a rush.
Why a CVE first queue misranks this
The advisory is a GitHub Security Advisory. No CVE ID had been assigned as of September 29, 2026. A queue that filters by CVE or NVD publication produces zero findings today in a shop that pulled mcp==1.29.1 into a dozen agent projects last week. The CVSS the maintainers assigned is 7.5, comfortably below the threshold most patch programs use to trigger urgency, and confidentiality scores High while integrity scores None because the score assumes the attacker only reads the exchange. In practice, credentials read out of the exchange are credentials that mint tokens, and the integrity impact on any resource trusting those tokens is total. The score is not wrong, it just does not include the second hop.
TRIS layer walk on this finding
TRIS, the Threat and Risk Intelligence Scoring engine inside CVEasy AI, does not stop at the base score. It walks the finding across the layers that decide it against your inventory.
Layer one, exposure. Any Python service in the inventory that imports mcp.client.auth and can be pointed at an untrusted MCP server carries this exposure. Internal-only agents that talk to a single first-party MCP server land in a different band than an agent runtime that lets end users add their own MCP endpoints from a marketplace or a config field.
Layer two, exploitability. The attack requires no user vulnerability beyond connecting the MCP client to a server the attacker controls. Dynamic client registration and PKCE, the ceremony that made this handshake feel safe, do the work of moving credentials to the wrong host. There is no memory corruption, no clever primitive. It is one metadata document.
Layer three, blast. The credentials that leak are the ones that mint tokens at your identity provider. Anything those tokens can reach is now reachable by whoever operates the malicious MCP server: email, drive, chat, ticketing, whatever else you granted the agent.
Layer four, exploitation status. No public confirmed abuse in the wild as of September 29, 2026. The class is trivial to exploit against any client that connects to attacker infrastructure, so a public PoC is a matter of days. TRIS keeps proof of concept separate from confirmed exploitation and does not promote one to the other. In your environment this scores top band on any host that lets a user or a marketplace supply the MCP endpoint URL, and lower on hosts pinned to a first-party server list.
Same GHSA, different score by real exposure.
The upgrade is not the whole fix
Upgrade first. mcp==1.30.0 for the 1.x line, mcp==2.2.0 for the 2.x line. Fixed versions add RFC 9207 issuer validation and refuse metadata documents whose issuer does not match the URL they were fetched from, closing the OAuthClientProvider path outright.
ClientCredentialsOAuthProvider or PrivateKeyJWTOAuthProvider. Both providers stay exposed until you pass an explicit issuer= argument at construction time. Grep every provider construction site in your codebase and add the parameter. The advisory says the argument becomes required in 3.0; a deprecation warning fires today when it is omitted.
Clear stored OAuth client registrations after the upgrade so pre-1.30.0 unbound records force a fresh registration bound to the correct issuer. Rotate any client_secret or JWT signing key ever handed to an MCP server outside your own perimeter, and revoke sessions issued against those clients. Audit logs for the affected agents: any client_secret_post to an unfamiliar host is a signal to hunt. The Python SDK OAuth clients reference now shows the issuer= parameter as part of the standard construction pattern.
How CVEasy AI surfaces this
CVEasy AI, the number one local-first CTEM platform, ingests GitHub Security Advisories alongside NVD and vendor feeds, so an advisory without a CVE ID lands in the queue the day it publishes. TRIS scores the finding against the inventory on your own hardware, weighs which Python services actually import mcp.client.auth, and separates services pinned to first-party MCP endpoints from agent runtimes that accept user supplied MCP URLs. The remediation workflow flags the two providers that need the issuer= argument in addition to the version bump, so the fix that would otherwise stop at pip install mcp==2.2.0 carries through to the code change that actually closes the credential redirection path. Your Python inventory, your identity provider metadata, and your agent runtime configuration never leave your hardware.
Sources: GHSA-qx49-fqc8-xw99 advisory (modelcontextprotocol/python-sdk), MCP Python SDK v1.30.0 release notes, MCP Python SDK OAuth clients reference.