MCP Python SDK Draws a Harder HTTP Boundary

MCP Python SDK 2.2.0 turns remote MCP deployment assumptions into stricter defaults for redirects, OAuth discovery, token audience checks, and legacy Streamable HTTP sessions.

MCP Python SDK 2.2.0, released on GitHub on September 7, 2026 and uploaded to PyPI the same day, is not a flashy protocol announcement. That is exactly why it is worth reading. The release changes the behavior of the Python implementation around the parts of MCP that become uncomfortable in production: HTTP redirects, OAuth discovery, token resource validation, and the lifetime of legacy Streamable HTTP sessions.

The important signal is that remote MCP servers are no longer being treated like convenient local adapters with a web server attached. They are network services that sit behind reverse proxies, receive credentials, serve multiple client generations, and can be exhausted or confused by ordinary infrastructure mistakes. SDK defaults should reflect that reality.

What Happened

The Model Context Protocol Python SDK shipped v2.2.0 on September 7, 2026. The same project also shipped v1.30.0 as a maintenance release for the 1.x line, backporting the main hardening changes while naming 2.x as the current line. PyPI shows the mcp 2.2.0 wheel and source distribution uploaded on September 7, with Trusted Publishing and provenance metadata tied to the modelcontextprotocol/python-sdk repository.

The release makes four behavior changes that matter for engineers operating MCP clients or servers over HTTP.

First, HTTP clients now follow redirects only inside the configured MCP endpoint's origin, with a narrow exception for upgrading from http to https on the same host. That applies to the high-level Client("https://...") path, streamable_http_client, sse_client, and OAuth provider requests. A redirect to another host, scheme, or port is no longer hidden control flow. It fails, and the caller needs to configure the intended endpoint directly.

Second, legacy Streamable HTTP sessions now have a default lifetime and a default ceiling. For clients using the pre-2026-07-28 handshake era, a stateful session with nothing in flight expires after 30 minutes. The next request sees 404 Session not found and must initialize again. A server process also holds at most 10,000 such sessions by default; once that limit is reached, new sessions receive a 503 rather than accumulating without bound.

Third, OAuth discovery got stricter on the legacy path. If a server does not expose protected resource metadata and the discovered authorization server metadata has an issuer that does not match the expected server origin, the client rejects it. The release also changes fallback behavior: a 403 that is not an insufficient_scope challenge is returned instead of retried, and a failure to fetch protected resource metadata due to 5xx or 429 stops the flow instead of falling back to legacy endpoints.

Fourth, the SDK adds AuthSettings.validate_token_resource and new deprecation warnings around missing issuer and token resource validation configuration. In 2.x, these warnings preserve compatibility. They also make the intended 3.0 direction clear: MCP servers should know which authorization server issued a token and whether the token was issued for this resource server.

Why It Matters

MCP's center of gravity has moved from desktop-local helper processes to remote tool services. That changes the failure model.

When an MCP server is a local subprocess launched over stdio, redirect policy is irrelevant. When an MCP client connects to https://tools.example.com/mcp through corporate proxies, ingress controllers, OAuth middleware, and environment-specific hostnames, redirect policy becomes part of the security boundary. If a client silently follows a cross-origin redirect, the endpoint URL stops being the authority the operator reviewed. The new default makes that handoff explicit. Same-origin trailing-slash redirects still work. Cross-origin moves require a deliberate configuration change.

The session changes matter for a different reason: old clients are not free to support forever. The current MCP transport model is sessionless for 2026-07-28 HTTP connections. The legacy model established a connection-scoped session with an initialize handshake and server-to-client request channels. The Python SDK serves both eras from the same Streamable HTTP app, routing by protocol version before user code sees the request.

That compatibility has an operational cost. The SDK documentation is blunt about the legacy session record: it is in-process state, not a distributed session store. If you run more than one worker, a legacy request carrying Mcp-Session-Id has to land on the worker that created it. Otherwise the worker correctly returns 404 Session not found. If you keep sessions enabled, you need sticky routing. If you refuse to pay that cost, stateless_http=True avoids retained sessions for the legacy leg, but it gives up the back-channel those clients need for server-initiated requests and notifications.

The old default of effectively unbounded retained sessions was a bad fit for long-running public or internal services. Abandoned clients, failed opening requests, and idle browser-adjacent workflows can turn into retained server state. A 30-minute idle timeout and a per-process ceiling do not solve distributed state, but they make the resource model finite.

The OAuth changes are part of the same theme. A tool server is often an authority bridge: it can read tickets, fetch documents, query databases, or mutate cloud resources. It is not enough for a client to get some bearer token from some discovered authorization server. The client needs to bind authorization metadata to the expected issuer, and the server needs a path toward checking that a token was issued for its own resource. MCP's 2026-07-28 authorization spec already points implementers toward protected resource metadata, issuer validation, and resource indicators. The Python SDK release tightens how the implementation behaves around those ideas.

How It Works

There are two layers to keep separate: protocol semantics and SDK deployment behavior.

At the protocol level, MCP uses JSON-RPC messages and defines transports as bindings. Streamable HTTP carries each client message as an HTTP POST to an MCP endpoint, with replies delivered as JSON or a request-scoped SSE stream. The transport can mirror selected metadata into HTTP headers so intermediaries can route or inspect requests, but the message body remains the source of truth.

At the SDK level, one Python server can serve both modern and legacy HTTP clients. A modern 2026-07-28 connection is sessionless. Each request stands alone. The server does not issue Mcp-Session-Id, and load balancers do not need to keep subsequent requests on the same worker for MCP session lookup.

A legacy connection is different. During initialize, the SDK creates a session record, returns an Mcp-Session-Id header, and keeps live state for later requests. That state includes negotiated version, open streams, and background session machinery. In v2.2.0, that record expires when the session has had nothing in flight for session_idle_timeout seconds, which defaults to 1800. An open GET stream counts as in flight, as does a request still being answered. A long-running tool call is not interrupted merely because it exceeds the idle timeout; the timer starts after active work finishes.

The ceiling is controlled by max_sessions, defaulting to 10,000 per process. The release notes and docs both make clear that this is not eviction. Existing sessions are not displaced. New sessions are refused once the process is at the ceiling.

Redirect handling is now pinned to endpoint origin. If a client was configured with https://tools.example.com/mcp, a redirect that stays within that same scheme, host, and port can be followed. An http URL can upgrade to https on the same host. A redirect to https://other.example.com/mcp, or to the same host on an unexpected port, fails. The useful operational consequence is simple: canonicalize your MCP URLs in configuration instead of relying on redirect chains.

OAuth discovery is also biased toward fail-closed behavior. The 2026-07-28 MCP authorization specification requires servers to implement OAuth 2.0 Protected Resource Metadata and clients to use it for authorization server discovery. For older or incomplete deployments, the SDK still has compatibility paths, but v2.2.0 stops accepting issuer mismatches on the legacy route and stops falling back when protected resource metadata is temporarily unavailable due to 5xx or rate limiting. That may expose broken deployments, but it avoids turning transient metadata failure into weaker discovery semantics.

Example

Suppose you operate an internal MCP server that exposes deployment, incident, and cloud-account tools to coding agents. It runs behind https://mcp.internal.example.com/mcp, with OAuth in front and two application workers behind a load balancer.

On the client side, v2.2.0 pushes you to configure the endpoint you actually trust:

from mcp import Client


async def list_internal_tools() -> list[str]:
    async with Client("https://mcp.internal.example.com/mcp") as client:
        result = await client.list_tools()
        return [tool.name for tool in result.tools]

If the service has moved to https://mcp-tools.internal.example.com/mcp, update the URL. Do not depend on the old endpoint redirecting agent traffic across origins. That redirect now fails, which is the correct failure mode for an authenticated tool channel.

On the server side, the defaults are now reasonable for many deployments, but they should still be conscious settings:

from mcp.server import MCPServer

mcp = MCPServer("internal-ops")


@mcp.tool()
async def deployment_status(service: str) -> str:
    return f"{service}: query your real status backend here"


if __name__ == "__main__":
    mcp.run(
        transport="streamable-http",
        session_idle_timeout=1800,
        max_sessions=10_000,
    )

If you support legacy clients and keep sessions enabled, route by Mcp-Session-Id or another sticky-load-balancing mechanism that reliably returns a session to its owning worker. The SDK does not provide a distributed session store behind event_store; that feature is about replaying missed events for a reconnecting client, not making a session visible from another process.

If your tools never ask the client questions and never rely on server-to-client notifications for legacy clients, you can consider the more scalable legacy tradeoff:

app = mcp.streamable_http_app(stateless_http=True)

That setting removes retained sessions from the legacy leg. It also removes the back-channel those legacy clients need for server-initiated requests. For a read-only catalog of simple tools, this can be acceptable. For interactive tools that use elicitation or live notifications, it is probably the wrong trade.

My Take

This is the kind of release that makes an ecosystem more deployable without announcing a new protocol revision. The changes are small individually, but they line up around a serious position: remote MCP is production HTTP infrastructure, not a localhost convenience with TLS added later.

The redirect change is the easiest to defend. Cross-origin redirects in authenticated API clients are rarely what operators want. They hide authority changes in infrastructure behavior, make incident analysis harder, and interact badly with credentials, cookies, and OAuth flows. If another origin is the real server, put that origin in configuration.

The session ceiling is also overdue. Compatibility code tends to become permanent infrastructure. Once a server can support old clients automatically, teams stop seeing the cost until memory pressure or load-balancer behavior makes it visible. v2.2.0 puts a price tag on that support: 30 minutes of idle state, 10,000 sessions per process, sticky routing if you keep stateful legacy behavior, and explicit breakage if you choose stateless mode but still expect back-channels.

The OAuth changes are slightly more painful and more important. Engineers should expect some staging environments to break after upgrade because issuer metadata, protected resource metadata, proxy origins, or error semantics were sloppy. That is not a reason to avoid the upgrade. It is a reason to test it with real auth flows before rolling it through every agent host. A tool server that can act on behalf of users should fail on ambiguous authorization metadata.

One caveat: these changes do not make an MCP deployment safe by themselves. They do not replace tool-level authorization, audit logging, allowlisted egress, secret scoping, or human approval for dangerous operations. They do remove several bad ambient assumptions from the SDK. That is a useful foundation.

Conclusion

MCP Python SDK 2.2.0 is a practical hardening release for teams running MCP over HTTP. It bounds retained legacy session state, refuses cross-origin redirect surprises, validates OAuth issuer assumptions more consistently, and starts moving token resource validation toward a stricter default.

For experienced engineers, the lesson is not "upgrade and forget it." The lesson is to treat MCP servers like any other sensitive backend service. Pin the endpoint you trust. Understand which clients are modern and which are legacy. Decide whether sticky sessions or stateless legacy behavior is the right operational trade. Fix OAuth metadata rather than leaning on fallback paths. Then upgrade deliberately.

Sources

Enjoying this article?

Subscribe to get new NestJS, Node.js, and backend engineering posts directly in your inbox.

Subscribe for free