{"slug": "three-mcp-python-sdk-advisories-and-the-one-an-upgrade-will-not-fix", "title": "Three MCP Python SDK Advisories, and the One an Upgrade Will Not Fix", "summary": "GitHub's Advisory Database added three Moderate-severity advisories on October 27, 2026 for the MCP Python SDK (the `mcp` PyPI package), all fixed in versions 1.30.0 and 2.2.0, which shipped September 7 before the advisories became public. The flaws cover missing token audience validation on the server side (CVE-2026-116684), cross-origin redirects leaking custom headers and replaying request bodies in the Streamable HTTP and SSE client transports (CVE-2026-116685), and client-side `$ref` resolution that could fetch arbitrary http, https or file URLs and block the event loop (CVE-2026-116686). The audience-validation fix requires opting in via the new `AuthSettings.validate_token_resource` flag, which defaults to off until version 3.0, so upgrading alone does not close that issue.", "body_md": "*Originally published at [x1y1.co](https://x1y1.co/journal/mcp-python-sdk-advisories-upgrade-checklist/).*\n\nOn October 27, 2026, GitHub added three advisories for the MCP Python SDK (the `mcp` package on PyPI) to its Advisory Database, and one of them is not fixed by running `pip install --upgrade`. If your team runs an MCP server or an agent client on this SDK, you have a version check to do and a config change to make. The version check takes a minute. The config change needs a decision from whoever owns your authorization server.\n\nAll three are rated Moderate. All three are fixed in `mcp` 1.30.0 (1.x line) and 2.2.0 (2.x line), which [went out on September 7](https://github.com/modelcontextprotocol/python-sdk/releases) according to the release notes and the [PyPI upload times](https://pypi.org/project/mcp/). The repository advisories carry earlier dates than the database listing (September 28, October 2 and October 5), so the fixes shipped before the advisories were public. PyPI currently shows 2.3.0 as the latest release. We could not find any statement in the advisories about exploitation in the wild, and we are not claiming any.\n\n[CVE-2026-116684](https://github.com/advisories/GHSA-w4fh-qvv9-3v23) (CVSS 6.8) is about the server side. The SDK's bearer authentication could not check the audience of a token. If your authorization server issues tokens for several services, a valid token meant for a different service was accepted by your MCP server. It affects `mcp` 1.10.0 through 1.29.1 and 2.0.0 through 2.1.1, and it matters only if you use `auth=AuthSettings(...)` with a `token_verifier` that does not check the `aud` claim itself.\n\n[CVE-2026-116685](https://github.com/advisories/GHSA-5h93-6whr-6q8j) (CVSS 5.9) is about the client side. The Streamable HTTP and SSE client transports followed redirects to any host. If the endpoint you configured redirected to another origin, custom headers such as `X-API-Key` went along, and on 307 and 308 responses the request body was sent again too. The advisory says a lone `Authorization` bearer header was not exposed, because the underlying HTTP library already drops it on cross-origin redirects. The SDK's OAuth providers had the same behavior on discovery, registration and token requests. Affected: 1.8.0 through 1.29.1 and 2.0.0 through 2.1.1.\n\n[CVE-2026-116686](https://github.com/advisories/GHSA-rwrf-2pqf-9j8j) (CVSS 6.5) is also client side. When a server declared an `outputSchema` for a tool, the client validated each `call_tool` result with the `jsonschema` default resolver, which fetches any `$ref` it cannot resolve locally. A server you connect to could therefore make your client request an `http://`, `https://` or `file://` URL on every call. The fetched content was used only as a schema and never sent back to the server. The fetch also ran synchronously on the event loop with no timeout, so a target that accepted the connection and went silent could freeze every session in the process. Affected: 1.10.0 through 1.29.1 and 2.0.0 through 2.1.1.\n\nThe release notes list these changes as behavior changes (same-origin redirects, a new `validate_token_resource` option, `$ref` resolution limited to the schema document). They have no security heading, so a team that skims release notes for the word \"security\" would not have flagged the upgrade.\n\nThis is the one that needs your attention. The patched releases add `AuthSettings.validate_token_resource`, and it defaults to off. The advisory says upgrading alone does not fix the issue, and that the default will flip to `True` in version 3.0. Turning it on takes two changes made together: set `validate_token_resource=True` with `resource_server_url` set to the audience your authorization server puts in tokens for your server, and return that audience from `verify_token()` as `AccessToken.resource`. The advisory is explicit that doing only the first change gives you a 401 on every request.\n\nHere is how we would wire it. This example is ours, written against the 1.x API shape the advisory describes, and it uses PyJWT with a JWKS endpoint. Check the import paths against the version you pin.\n\n``` python\nimport jwt  # PyJWT\nfrom jwt import PyJWKClient\nfrom mcp.server.auth.provider import AccessToken\nfrom mcp.server.auth.settings import AuthSettings\nfrom mcp.server.fastmcp import FastMCP\n\nISSUER = \"https://auth.example.com\"\nAUDIENCE = \"https://mcp.example.com\"  # what your IdP puts in aud for THIS server\njwks = PyJWKClient(f\"{ISSUER}/.well-known/jwks.json\")\n\nclass JwtVerifier:\n    async def verify_token(self, token: str) -> AccessToken | None:\n        try:\n            key = jwks.get_signing_key_from_jwt(token).key\n            claims = jwt.decode(\n                token,\n                key,\n                algorithms=[\"RS256\"],\n                audience=AUDIENCE,  # rejects tokens minted for other services\n                issuer=ISSUER,\n                options={\"require\": [\"exp\", \"aud\", \"iss\"]},\n            )\n        except jwt.PyJWTError:\n            return None\n        return AccessToken(\n            token=token,\n            client_id=claims.get(\"azp\", claims[\"sub\"]),\n            scopes=claims.get(\"scope\", \"\").split(),\n            expires_at=claims[\"exp\"],\n            resource=AUDIENCE,  # the SDK compares this to resource_server_url\n        )\n\nmcp = FastMCP(\n    \"orders\",\n    token_verifier=JwtVerifier(),\n    auth=AuthSettings(\n        issuer_url=ISSUER,\n        resource_server_url=AUDIENCE,\n        validate_token_resource=True,\n    ),\n)\n```\n\nPassing `audience=` to `jwt.decode` is the workaround the advisory itself gives for versions you cannot upgrade: reject any token whose `aud` does not include your server inside `verify_token()`. It closes the hole on every affected version. If your verifier already did this before the advisory, set `validate_token_resource=False` explicitly to silence the deprecation warning, and write down why.\n\nThe deployment shape to worry about is one authorization server in front of many internal services, each with its own scopes. A token minted for the billing service should not open the MCP server that can write to your CRM, and before this fix the SDK would not stop it unless your verifier did.\n\nIf your agent talks to an MCP endpoint behind a load balancer, a vanity domain or a migration redirect, the redirect advisory applies to you. Before the fix, a redirect to another origin carried your `X-API-Key` with it. After the fix, a cross-origin redirect fails with an error: `httpx.HTTPStatusError` in 1.30.0, `MCPError` in 2.2.0, where the session stays usable. Any client that quietly depended on a cross-host redirect will stop connecting after the upgrade. The advisory's answer is to set the endpoint URL to the redirect target, and that is the right fix. It is also a change you want to find in staging, so grep your client configs for URLs that answer with a 301, 302, 307 or 308.\n\n```\n# list MCP endpoints that redirect (our script; feed it your own URL list)\nwhile read -r url; do\n  code=$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' \"$url\")\n  echo \"$url -> $code\"\ndone < mcp-endpoints.txt | grep -E ' 30[1278] '\n```\n\nIf you cannot upgrade yet, the advisory's stopgap is to pass your own HTTP client with `follow_redirects` left at `False`. With the 1.x client transport that looks like this (ours, and it assumes the `httpx_client_factory` hook on `streamablehttp_client`):\n\n``` python\nimport httpx\nfrom mcp.client.streamable_http import streamablehttp_client\n\ndef no_redirects(headers=None, timeout=None, auth=None) -> httpx.AsyncClient:\n    return httpx.AsyncClient(\n        headers=headers,\n        timeout=timeout or httpx.Timeout(30.0),\n        auth=auth,\n        follow_redirects=False,\n    )\n\nasync def connect(url: str, api_key: str):\n    return streamablehttp_client(\n        url,\n        headers={\"X-API-Key\": api_key},\n        httpx_client_factory=no_redirects,\n    )\n```\n\nThe advisory adds one more instruction that people skip. If a client may already have followed a redirect to a host you do not control, rotate the credentials it carried. If your API keys travel in custom headers, that means every key held by an agent whose endpoint ever redirected off-origin.\n\nThe third advisory only affects you if you use the SDK as a client against servers you do not fully control, which describes most agent platforms that let users add their own MCP servers. The patched client resolves `$ref` only inside the schema the server sent, plus the standard JSON Schema metaschemas, so validation touches no network and no files. A tool whose output schema points outside its own document now fails at call time with `RuntimeError: Invalid schema for tool ...`. In-document references such as `#/$defs/Order` keep working.\n\nThe advisory's partial stopgap is to skip any tool whose `outputSchema` contains an `$id`, or a `$ref` or `$dynamicRef` that does not start with `#`. That filter is short enough to run on every `list_tools` response:\n\n``` php\ndef schema_is_local(node) -> bool:\n    \"\"\"True if every $ref/$dynamicRef is in-document and no $id is present.\"\"\"\n    if isinstance(node, dict):\n        if \"$id\" in node:\n            return False\n        for key in (\"$ref\", \"$dynamicRef\"):\n            ref = node.get(key)\n            if isinstance(ref, str) and not ref.startswith(\"#\"):\n                return False\n        return all(schema_is_local(v) for v in node.values())\n    if isinstance(node, list):\n        return all(schema_is_local(v) for v in node)\n    return True\n\nasync def safe_tools(session):\n    result = await session.list_tools()\n    return [t for t in result.tools if t.outputSchema is None or schema_is_local(t.outputSchema)]\n```\n\nA server with a hostile schema can still be a bad neighbor in other ways, so this does not make an untrusted server safe. It closes the one path the advisory describes.\n\nThe 1.30.0 release notes call it a maintenance release and say 2.x is the current line. If you are still on 1.x and plan to move to 2.x, do that as its own change, since we have not tested your code against 2.x. We have not run any of the examples above against a live authorization server, so treat them as a starting point to adapt and test. And we only read the advisories and release notes, not the diffs, so the behavior described here is the maintainers' account of the fix.\n\nRun `pip show mcp` (or `uv pip list | grep mcp`) in every service image and every agent runtime, and pin `mcp>=1.30.0,<2` or `mcp>=2.2.0` in your constraints file. For each MCP server that uses `AuthSettings`, ask your identity owner what `aud` your tokens carry for that server, then ship the two-part change above in staging and confirm that a token minted for a different service now gets a 401. For each client, run the redirect check against your endpoint list, update any URL that redirects, and rotate any key that was sent in a custom header to an endpoint that has ever redirected off-origin. Finally, add a line to your dependency review that treats release notes mentioning new auth options, redirect policy or schema resolution as security-relevant even when the word is missing.", "url": "https://wpnews.pro/news/three-mcp-python-sdk-advisories-and-the-one-an-upgrade-will-not-fix", "canonical_source": "https://dev.to/x1y1co/three-mcp-python-sdk-advisories-and-the-one-an-upgrade-will-not-fix-2dp2", "published_at": "2026-11-06 08:17:56+00:00", "updated_at": "2026-11-06 08:24:53.858980+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-tools", "ai-safety", "developer-tools"], "entities": ["MCP Python SDK", "GitHub Advisory Database", "PyPI", "Model Context Protocol", "CVE-2026-116684", "CVE-2026-116685", "CVE-2026-116686"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/three-mcp-python-sdk-advisories-and-the-one-an-upgrade-will-not-fix", "markdown": "https://wpnews.pro/news/three-mcp-python-sdk-advisories-and-the-one-an-upgrade-will-not-fix.md", "text": "https://wpnews.pro/news/three-mcp-python-sdk-advisories-and-the-one-an-upgrade-will-not-fix.txt", "jsonld": "https://wpnews.pro/news/three-mcp-python-sdk-advisories-and-the-one-an-upgrade-will-not-fix.jsonld"}}