Summary In affected versions, when a connected MCP server declared an outputSchema for a tool, the SDK's client validated each calltool result with jsonschema's default resolver, which fetches any $ref it cannot resolve locally by opening the URL. A server could therefore put an http://, https:// or file:// reference in a tool's output schema and make the client:
- request that URL, or read that local file, from the client's own network position on every call of the tool (the content was used only as a schema and never sent back to the server), and - stall: the fetch ran synchronously on the event loop with no timeout, so a target that accepted the connection and then went quiet froze every session in the process for as long as it held the connection.
Am I affected? Yes, if your application uses the SDK as an MCP client (ClientSession over any transport, ClientSessionGroup, or the 2.x Client) and calls tools on servers you do not fully control. There was no setting that turned the validation off.
Affected versions: 1.10.0 through 1.29.1, and 2.0.0 through 2.1.1.
Not affected: applications that only build servers. If nothing in your code uses ClientSession, ClientSessionGroup or the 2.x Client class, you are not affected.
Fix Upgrade to mcp 1.30.0 (1.x) or 2.2.0 (2.x) or later. The client now resolves references only within the schema document the server sent (plus the standard JSON Schema metaschemas), so result validation performs no network or file access. A tool whose output schema points outside its own document fails at call time with RuntimeError (Invalid schema for tool ...); in-document references such as #/$defs/... work as before.
Until you can upgrade, call tools only on servers you trust. As a partial stopgap, skip tools whose outputSchema contains an $id, or a $ref / $dynamicRef that does not start with #.
Summary In affected versions, a Streamable HTTP server in stateful mode (the default) never reclaimed session state:
- sessions did not expire; - a client DELETE ended the session but left its entry registered; - a session whose first request was refused stayed registered with a running task.
Each session also kept its parsed initialize parameters, and the number of sessions was unlimited. A client that could reach the endpoint could grow server memory without bound using ordinary short requests; only a restart reclaimed it.
Am I affected? Yes, if you serve MCP over Streamable HTTP in stateful mode and clients you do not fully trust can reach the endpoint. Stateful mode is the default for:
- FastMCP (1.x) - MCPServer and Server.streamablehttpapp() (2.x) - StreamableHTTPSessionManager
Affected versions: 1.8.0 through 1.29.1, and 2.0.0 through 2.1.1.
Not affected:
- servers on 1.12.0 or later passing statelesshttp=True (no sessions are kept) - servers using only the stdio or SSE transports
Authentication limits who can open sessions, not how many a token holder can open.
Fix Upgrade to mcp 1.30.0 (1.x) or 2.2.0 (2.x) or later. In those versions:
- a session with no request in flight for sessionidletimeout seconds (default 1800) is closed and its ID then answers 404; open streams and in-progress requests keep it alive; - at most maxsessions sessions exist at once (default 10000); a request that would open another is answered 503; - a session is discarded as soon as it ends, whether by DELETE, expiry, or a refused opening request.
Both settings are keyword arguments on FastMCP(...) (1.x), MCPServer.run() / streamablehttpapp() (2.x) and StreamableHTTPSessionManager; None disables them.
This bounds retention rather than eliminating it: a live session still holds its initialize parameters (up to roughly twice the 4 MiB body limit), and an initialize with invalid parameters still opens a session that expires like any other. On a small host, lower sessionidletimeout, maxsessions and maxrequestbodysize to fit.
If you cannot upgrade yet, pass statelesshttp=True (1.12.0 or later) where you need no per-session state, or restrict who can reach the endpoint.
The Model Context Protocol Python SDK is the official Python SDK for Model Context Protocol servers and clients. In mcp versions before 1.29.1 and from 2.0.0a1 through 2.0.1, the Streamable HTTP session manager, SSE message endpoint, and OAuth authorization-server routes buffer complete HTTP request bodies without enforcing a maximum size. An unauthenticated remote client able to reach an affected endpoint can send a large declared or streamed request body that consumes multiple times the body size in process memory and can repeatedly exhaust memory or terminate the server. Stdio-only servers and deployments with an effective upstream request-size limit are not affected, and applications that use StreamableHTTPServerTransport directly remain responsible for enforcing their own limit. The standard SDK server applications and session managers are fixed in versions 1.29.1 and 2.1.0.
Summary In affected versions, the SDK's server-side bearer authentication had no way to check that a token was issued for your server (its audience). Where one authorization server issues tokens for several services, a valid token for another service was also accepted by your MCP server.
Am I affected? Yes, if your HTTP server uses the SDK's bearer authentication (auth=AuthSettings(...) with tokenverifier=), its authorization server also issues tokens for other services, and your TokenVerifier does not check the token's audience (aud) itself.
Affected versions:
- mcp 1.10.0 through 1.29.1 - mcp 2.0.0 through 2.1.1
Fix Upgrade to:
- 1.x: mcp 1.30.0 or later - 2.x: mcp 2.2.0 or later
Upgrading alone changes nothing. The patched versions add a validatetokenresource setting that is off unless you set it. To turn the check on, make both of these changes together:
1. Set validatetokenresource=True in AuthSettings, with resourceserverurl set to the audience your authorization server puts in tokens for your server. 2. Return the token's audience from verifytoken() as AccessToken.resource.
A token for another service then gets 401. With only the first change, every request gets 401.
If your TokenVerifier already checks the audience, set validatetokenresource=False to silence the deprecation warning. The setting will default to True in 3.0.
If you cannot upgrade yet, reject any token whose aud does not include your server inside verifytoken(). That closes the issue on every version.
Summary In affected versions, the SDK's Streamable HTTP and SSE client transports followed HTTP redirects to any host. If the configured MCP endpoint answered with a redirect to a different origin:
- custom headers set on the client (for example an X-API-Key credential) were sent to that origin, and - on a 307 or 308 redirect the request body was re-sent there too.
The SDK's OAuth client providers followed redirects on their own discovery, registration and token requests in the same way.
A lone Authorization bearer header was not exposed; the HTTP library already drops it on cross-origin redirects.
Anyone able to make an endpoint you trust redirect elsewhere (a stale redirect to a lapsed domain, a misrouted proxy rule, redirect logic an attacker can influence) could capture that material. A server that is itself malicious gains nothing this way; it already receives your requests directly.
Am I affected? Yes, if your code uses the SDK as an MCP client over HTTP through any of:
- streamablehttpclient (on 2.x also Client given an http:// or https:// URL) - streamablehttpclient or sseclient - ClientSessionGroup with HTTP server parameters - the internal createmcphttpclient helper
This includes an HTTP client you passed in yourself with followredirects=True.
Affected versions: 1.8.0 through 1.29.1, and 2.0.0 through 2.1.1.
Not affected:
- MCP servers - stdio clients - a client you supplied with followredirects left at its default of False
Fix Upgrade to mcp 1.30.0 (1.x) or 2.2.0 (2.x) or later; no code changes are needed.
The transports, and the OAuth providers' own requests, now follow a redirect only when it stays on the endpoint's origin (same scheme, host and port, or an http to https upgrade on the same host) and keeps the request method. Any other redirect is not followed and the request fails with an error naming the target (MCPError on 2.x, httpx.HTTPStatusError on 1.x), whichever HTTP client the transport uses. The internal createmcphttpclient helper no longer turns redirect following on, so code calling it directly now receives the redirect response instead.
If your client currently reaches its server through a redirect to another host, including a server that needs no credentials, it will stop connecting after the upgrade: set the endpoint URL to the redirect's target.
Until you can upgrade, supply your own HTTP client with followredirects left at False (httpclient= on streamablehttpclient; httpxclientfactory= on sseclient and streamablehttpclient; ClientSessionGroup has no such parameter). If a client may already have followed a redirect to a host you do not control, rotate the credentials it carried.
<details> <summary>If you genuinely need the old behaviour (unsupported)</summary>
This restores what this advisory fixes: redirects to any host are followed again, carrying your custom headers and, on 307/308, the request body, OAuth token requests included. Prefer setting the endpoint URL to the redirect's target. If you cannot, and you accept forwarding credentials to the redirect target, give the transport an HTTP client that follows redirects regardless of what the SDK asks for:
python import httpx2 # on 1.x: import httpx and subclass httpx.AsyncClient instead
class FollowRedirectsAnyway(httpx2.AsyncClient): """Follows redirects to ANY origin. Deliberately gives up the SDK's same-origin protection."""
async def send(self, request, , followredirects=False, kwargs): return await super().send(request, followredirects=True, kwargs)
httpclient = FollowRedirectsAnyway(timeout=httpx2.Timeout(30, read=300)) streamablehttpclient(url, httpclient=httpclient)
sseclient / streamablehttpclient take a factory instead: def clientfactory(headers=None, timeout=None, auth=None): return FollowRedirectsAnyway(headers=headers, auth=auth, timeout=timeout if timeout is not None else httpx2.Timeout(30, read=300)) sseclient(url, httpxclientfactory=clientfactory)
The redirect handling is then the HTTP library's own, as before the fix: a lone Authorization header is still dropped on cross-origin hops and 301/302/303 become GET. It is not something the SDK supports or tests.
</details>
---
The Model Context Protocol Python SDK is the official Python SDK for Model Context Protocol servers and clients. In mcp versions before 1.29.1 and from 2.0.0a1 through 2.0.1, the Streamable HTTP session manager, SSE message endpoint, and OAuth authorization-server routes buffer complete HTTP request bodies without enforcing a maximum size. An unauthenticated remote client able to reach an affected endpoint can send a large declared or streamed request body that consumes multiple times the body size in process memory and can repeatedly exhaust memory or terminate the server. Stdio-only servers and deployments with an effective upstream request-size limit are not affected, and applications that use StreamableHTTPServerTransport directly remain responsible for enforcing their own limit. The standard SDK server applications and session managers are fixed in versions 1.29.1 and 2.1.0.
Summary In affected versions, the default request handlers installed by the experimental tasks feature (server.experimental.enabletasks()) did not check which session created a task before acting on it. On a server with more than one connected client, any client could observe, read results from, and cancel tasks belonging to other clients.
Am I affected? Only if the developer's application server calls server.experimental.enabletasks(). If grep -r enabletasks over their codebase finds nothing, the application is not affected.
Details When tasks support is enabled on the low-level server, default handlers are registered for tasks/list, tasks/get, tasks/result, and tasks/cancel. These handlers operated on the task identifier alone and kept no record of the session that created each task. Because tasks/list returned every task in the store, a connected client did not need to know any identifiers in advance: it could enumerate all tasks, read any task's status and result via tasks/get and tasks/result, retrieve queued task messages — such as elicitation requests intended for the task's creator, which are removed from the queue on delivery, so the intended recipient never receives them — and cancel any task via tasks/cancel.
Impact Servers that call server.experimental.enabletasks() and serve multiple clients are affected: one client can read other clients' task results and elicitation payloads, consume messages meant for them, and cancel their tasks. The feature is experimental and opt-in, so servers that never enable it are unaffected. Servers that registered their own task handlers instead of the defaults are affected only if those handlers have the same omission.
Mitigation Upgrade to version 1.27.2 or later, in which task IDs generated by runtask() embed an opaque per-session marker and the default handlers restrict each session to its own tasks: requests for another session's task receive "task not found", and tasks/list returns only the requesting session's tasks. Tasks created with explicitly chosen IDs or written directly through a TaskStore remain reachable by ID but are not listed. Alternatively, leave the experimental tasks feature disabled, or register task handlers that validate session ownership.
Summary In affected versions, the SSE and Streamable HTTP server transports routed incoming requests to an existing session based only on the session identifier, without verifying that the request was authenticated as the same principal that created the session. Anyone who learned or guessed a session ID could send JSON-RPC messages on that session, regardless of which bearer token the request carried.
Am I affected? Only if a developer's application server uses an HTTP transport (SSE, or Streamable HTTP in stateful mode) and authenticates requests. Servers on stdio, stateless Streamable HTTP, or with no authentication configured are not affected.
Details Both transports look up the target session by its identifier alone — the sessionid query parameter for SSE (mcp.server.sse.SseServerTransport) and the Mcp-Session-Id header for Streamable HTTP (mcp.server.streamablehttpmanager.StreamableHTTPSessionManager). Once the lookup succeeded, the request was handled on that session without comparing its authentication context to the credentials presented when the session was created, so a request authenticated as a different OAuth client could inject messages into the session. On the SSE transport the response is delivered to the original client's event stream; on the Streamable HTTP transport it is returned on the injecting request, so the injecting client can also read the result. The SSE transport has been affected since the first release; the Streamable HTTP transport since version 1.8.0.
Impact Servers using either HTTP transport together with the SDK's built-in bearer-token authentication are affected: the per-client isolation that authentication provides can be bypassed for any session whose ID is known. Session IDs are randomly generated UUIDs, so exploitation requires obtaining one out of band (logs, network observation). Servers that do not enable bearer-token authentication have no per-client isolation to bypass and are not addressed by this advisory, and stateless Streamable HTTP deployments do not maintain sessions and are unaffected.
Mitigation Upgrade to version 1.27.2 or later, which records the authenticated principal that created each session — the OAuth client ID together with the token's issuer and subject when the token verifier supplies them — and answers requests presenting a different principal with the same 404 response as for an unknown session.
Deployments where many end users share a single OAuth client (hosted MCP clients, gateways) should ensure their token verifier populates AccessToken.subject (e.g. from the token's sub claim) so sessions are isolated per user rather than per client. Deployments using a custom authentication backend other than the built-in BearerAuthBackend should enforce an equivalent check themselves.