Lessons from rebuilding mcp-use v2
The new MCP specification is out. It is the largest protocol change since MCP launched, and it addresses a problem that became obvious as remote servers moved from demos into production: the protocol was carrying too much connection state.
The 2026-07-28 revision removes the initialize handshake and protocol-level sessions, including the Mcp-Session-Id header. Requests carry the information needed to process them, so any request can reach any server instance. The same release adds a formal extensions model, including MCP Apps and Tasks, cache hints, trace context, stronger OAuth requirements, and a deprecation policy.
We rebuilt mcp-use around the new specification rather than patching the old framework around it. The work surfaced six lessons for teams migrating MCP servers, clients, frameworks, and infrastructure to the stateless model.
1. Stateless MCP changes the infrastructure boundary
The old Streamable HTTP flow started with initialize. The server returned a session ID, and every later request had to carry it. In a single process this was simple enough. Behind a load balancer, the session tied a client to one replica.
That led to sticky routing, shared session storage, and gateways that had to understand more of the protocol than an HTTP gateway should need to know. A rolling deployment could also kill the process holding a client session and force the client to start over.
The new flow is closer to ordinary HTTP. The protocol version and client metadata travel with each request. Servers implementing the 2026-07-28 revision must support server/discover, which clients may call before any other request to select a version and discover capabilities. Required method and name headers let infrastructure route and observe requests without parsing JSON-RPC bodies.
In practical terms, a request now carries the context a server needs instead of relying on a connection that happened to be established earlier:
POST /mcp HTTP/1.1
Host: example.com
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json
{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "search", "arguments": { "query": "MCP" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": { "name": "example-client", "version": "1.0.0" }
}
}
}That is a small-looking change with a large operational consequence. A load balancer can treat the request like a request. It does not need to know which process owns a session before it can forward the call.
It also creates a boundary that gateways and servers must enforce. If MCP-Protocol-Version, Mcp-Method, Mcp-Name, or an Mcp-Param-* header disagrees with the JSON-RPC body, the server must reject the request with HeaderMismatch (-32020) rather than authorize one operation at the edge and execute another at the backend.
2. Application state has to become explicit
This does not make every application stateless. A shopping cart, browser session, or long-running job still needs state. The difference is where that state lives. The server can return an explicit handle and the client can pass it back on the next request. The state becomes part of the application contract instead of an invisible property of the transport.
There is a second important change for servers that need to ask the client for input. Multi Round-Trip Requests replace the old assumption that a server can keep a connection open while waiting. A server returns an input_required result whose inputRequests field describes the information it needs, together with an opaque requestState. The client gathers the input and retries the original request with inputResponses. The next retry can land on any instance.
For framework authors, requestState cannot become a disguised session identifier stored only in one process. It has to be treated as opaque and untrusted, then verified or exchanged for durable application state when the retry arrives. In mcp-use v2, that led us to add verified and tamper-protected request state and to test retries across instances rather than only inside one server process.
3. Compatibility needs an explicit migration path
Protocol revisions do not arrive everywhere at once. A new client may need to reach an older server, and a framework may need to support both the 2025 initialization flow and the 2026 request-scoped model during the migration period.
We did not try to patch the old framework around the new handshake. We rebuilt the TypeScript stack around the official MCP SDK v2 and made the protocol boundary explicit.
The server package now has a stateless request path designed for modern HTTP runtimes. The client negotiates the protocol version instead of assuming that every server speaks the same wire format. In automatic mode, it probes with server/discover and falls back to the older initialization flow when it needs to. That gives new servers the modern path without making an existing server upgrade on the same day as its client.
The official TypeScript SDK migration guide documents version negotiation and the changes required for request-scoped state. It also makes a point that is easy to miss: v2 packages can still speak the older protocol, and modern clients can negotiate down when they connect to an older server.
Compatibility tests should cover the boundary rather than only the happy path:
- A modern client probing a 2025-only server and falling back to initialize.
- A modern-only client rejecting a server that cannot negotiate a supported version.
- A 2026-07-28 server implementing server/discover and advertising the versions and capabilities it actually serves.
- Authentication, timeout, and infrastructure failures remaining failures rather than being mistaken for evidence that a server is legacy.
4. Package boundaries expose hidden responsibilities
A protocol rewrite also exposes assumptions hidden inside a combined framework surface. The package boundary becomes a practical way to separate server concerns from client behavior, agent workflows, and inspection tooling.
The package surface is split around the jobs people actually do:
- mcp-use for building servers
- @mcp-use/client for connecting to servers from Node, browsers, and React
- @mcp-use/agent for agent workflows
- @mcp-use/inspector for local inspection and testing
This made the framework easier to install and gave the client, server, agent, and Inspector code clearer boundaries. It also forced us to remove a lot of assumptions that were hidden by the old combined surface. We replaced preview-only SDK dependencies with registry packages, made the v2 packages installable under normal pnpm supply-chain settings, and kept the runtime usable in serverless and edge environments.
The transferable lesson is not that every framework needs these package names. It is that a stateless protocol makes ownership easier to see: transport and server dispatch, client negotiation, host behavior, and developer tooling should not depend on the same implicit connection state.
5. MCP Apps make the client boundary concrete
The client work in v2 also produced a set of primitives for building the application around an MCP connection.
A host for an MCP App has to do more than display an HTML string. It has to manage the View lifecycle, forward tool input and output, expose the host connection back to the app, enforce the resource's content-security policy, and handle host messages, display modes, downloads, and model context.
ViewRenderer is the clearest example in mcp-use v2. It takes either a live MCP resource or preloaded HTML and renders it in the host sandbox. That means a View is no longer something an application has to reconstruct from an HTML string and a handful of event listeners.
import { ViewRenderer } from "@mcp-use/client/react";
<ViewRenderer
viewId="search-results"
source={{
kind: "live",
connection,
resourceUri: "ui://search/results.html",
}}
toolName="search"
toolOutput={result}
displayMode="inline"
/>;The same separation applies beyond rendering. For applications with more than one server, McpClientProvider owns persistence, reconnects, OAuth state, notifications, and elicitation queues. The Inspector uses the same boundaries across chat, traces, prompts, and rendered Views, while the CLI, screenshot verification, and tunnel support the development loop from local inspection to a client connection.
MCP Apps are part of that same redesign. In v1, a widget involved a separate resource directory, URI indirection, build registration, and metadata wiring. In v2, a View is part of the application model. The tool and its UI have a typed boundary, and the build can discover and bundle the View without asking the developer to maintain the old chain of references.
6. The migration is wider than stateless requests
Removing sessions is the most visible part of the revision, but it is not the only operational change server and framework teams have to account for.
- The HTTP GET endpoint and resources/subscribe are replaced by subscriptions/listen, a long-lived POST-response stream that clients opt into for selected notification types.
- SSE resumability and message redelivery are removed. If a response stream breaks, the in-flight request is lost and the client must issue a new request with a new request ID.
- List and read results now require ttlMs and cacheScope, which makes cache policy part of the server contract rather than an implementation detail.
- Roots, Sampling, and Logging are deprecated for new implementations, while Tasks move out of the core protocol into an official extension.
The migration has involved more than changing imports. We added direct v2 elicitation flows, verified and tamper-protected request state, protocol-version metadata, and compatibility tests against both old and modern servers. We have also removed the temporary v1 compatibility facade from the v2 line. Historical v1 documentation remains available, but native v2 no longer pretends that the two APIs are the same thing.
The failure modes are as important as the feature paths. A migration test plan should send consecutive requests to different replicas, retry an input_required result on another instance, reject header and body mismatches, break an in-flight response stream, reconnect subscriptions/listen, and verify that cache hints are present on the required results.
What the benchmarks say
We ran the comparison against published packages, not local source builds. The full methodology and limits are in the mcp-use v2 benchmark report.
Against mcp-use v1, v2 beta.64 measured:
- 10,982 median operations per second versus 8,615
- 68.1 ms median cold launch versus 151.6 ms
- 74.4 MiB clean install versus 404.6 MiB
The throughput test used the same tools/list and tools/call workload across the targets. The launch numbers came from repeated fresh process starts. The install number is the actual dependency tree on disk, including peer dependencies.
The package-size comparison is easier to read on its own. A clean v1 install brought roughly 405 MiB of dependencies onto disk; v2 brought 74 MiB.
These are localhost measurements. They tell us that the rewrite removed real overhead from the framework and its dependency graph. The figures above compare mcp-use v2 with mcp-use v1 and do not predict a production server's latency or performance against other implementations. The important result is that the framework can add Views, an Inspector, a client, and deployment tooling without carrying the old package footprint forward.
For implementation details, see the mcp-use v2 documentation and migration guide.
Questions for a 2026-07-28 migration
- Which server behaviors still assume that two requests will reach the same process?
- Where does cross-call state live, and how is an explicit handle or requestState validated?
- Does server/discover advertise the protocol versions and capabilities the server actually supports?
- Do gateways and servers reject a mismatch between the MCP headers and the JSON-RPC body?
- Can an input_required retry land on any instance without losing context?
- What does the client do when an in-flight response stream breaks?
- Have subscriptions, cache hints, deprecated features, and task handling been updated as well as the request path?
Share
Author

Enrico Toniato
CTO, Manufact



