Migrating the Apify MCP Server to the new stateless spec Apify merged support for the Model Context Protocol's 2026-07-28 stateless revision on July 28, 2025, released a working stateless server the next day, and has run it in production at mcp.apify.com since July 30. The revision eliminates sessions and the initialize handshake, with each request carrying its own protocol version, client info, and capabilities in the _meta field, and SEP-2567's authors found that 90% of a random 1,000-repo sample of open-source servers never referenced the session ID. Apify's migration, which began on January 2, 2025, relied heavily on its mcpc command-line MCP client, and existing setups continue to work with one deliberate exception. We started building the Apify MCP Server on January 2, 2025, a few weeks after MCP launched. MCP revision 2026-07-28 https://blog.modelcontextprotocol.io/posts/2026-07-28/ is the "largest revision of the protocol since launch": sessions and the initialize handshake are gone, and every request now carries everything the server needs to answer it, which the specification https://modelcontextprotocol.io/specification/2026-07-28 sets out in full. We merged dual-era support on July 28, the morning the final spec was published, released a working stateless server the day after, and have run the stateless path in production at mcp.apify.com https://mcp.apify.com since July 30. Existing setups keep working, with one deliberate exception covered below. Through spring 2026, plenty of people wrote MCP's obituary: it was dead, and the CLI had won. The tool we leaned on hardest through this migration was mcpc https://github.com/apify/mcpc , a command-line MCP client, which is a strange thing to exist if one of them killed the other. We keep improving the Apify CLI to treat agents as first-class citizens, too. But every major AI platform offers MCP connectors as the primary way to integrate an external service, and none of them ships a CLI connector. The skeptics do hold one point: a single agent calling a single API you control does fine with plain REST. What the 2026-07-28 revision changes When the Model Context Protocol launched, its pitch was easy to explain: write one adapter instead of bespoke glue for every AI client, and get a live, two-way connection between agent and service. The new revision keeps the one adapter and drops the live connection, along with the session state that connection carried. Two proposals carry the change: Sessions are gone SEP-2567 . The proposal's authors ran an automated survey of a random 1,000-repo sample of open-source servers: 90% never referenced the session ID. The Every request declares its protocol version, client info, and capabilities in a initialize handshake is gone too SEP-2575 . meta field. Clients can probe a server with the new server/discover method, which servers must implement and clients may skip. Features that needed a persistent connection were redesigned: - Mid-call input became a multi-round-trip exchange. - List-change and resource-update notifications became an opt-in subscriptions/listen stream. - Long-running work moved to a tasks extension. Tool lists now carry cache hints, and new HTTP headers let gateways route and rate-limit without parsing bodies. In the legacy protocol, a session opened with an initialize call, and every subsequent request depended on it. In the modern one, there's no handshake, and each request carries its own protocol version, client info, and capabilities in meta : // Legacy 2025-11-25 and earlier : a session starts with a handshake { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": { "tools": {} }, "clientInfo": { "name": "example-client", "version": "1.0.0" } } } // The server replies with its capabilities, and every request after // this one depends on the session the exchange created. // Modern 2026-07-28 : no handshake; every request stands alone { "jsonrpc": "2.0", "id": 7, "method": "tools/list", "params": { " meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {}, "io.modelcontextprotocol/clientInfo": { "name": "example-client", "version": "1.0.0" } } } } // Any server instance can answer this request. With sessions, a request could land on any node, so every node has to be able to reconstruct the session on arrival: session state in Redis, plus pub/sub channels to reach the node holding a client's live stream for cancellations and task progress. That's a lot of moving parts to make a stateless load balancer look stateful. The stateless protocol removes them: each request carries what it needs, and any node can answer it with nothing shared behind the scenes. The spec's own Which version is which? era vocabulary https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning : - revision 2026-07-28 and later Modern: 2025-11-25 and earlier - Legacy: a server speaking both - Dual-era: The official SDKs implementing it are versioned 2.0.0. Internally we called it "MCP V2," a nickname the spec never uses, so official names it is. So why not just a REST API? The "just use a REST API" argument is right about one case: a single agent calling a single API you control. MCP adds protocol overhead there, so let the agent call the API directly. We run the other case in production: a hosted server, and clients we don't control. Running mcp.apify.com https://mcp.apify.com on multiple nodes meant sessions lived in Redis: - A state store and a task store - An event store for resumability - Pub/sub for node coordination - Recovery for orphaned sessions And the channel needed constant attention: - Clients that never closed their sessions - Session floods we had to rate-limit - Memory leaks in session teardown - Cross-node state that silently went stale We paid for all of it in infrastructure, and the new spec removes the entire class of problems. Our tech lead Jiří https://www.linkedin.com/in/jiri-spilka/ sums it up: "Instead of the server holding a million clients and their state, it's now offloaded entirely to the client." State that spans requests now lives in explicit identifiers the client passes back. What's left is the part a REST API never standardized. Tool discovery is machine-readable JSON Schema, and OAuth lives in the spec instead of being reinvented per integration. One server implementation works for Claude, ChatGPT, Cursor, and anything else, and it gets standard ways to expose prompts and resources, ask a human for structured input, and render interactive app UIs MCP Apps . Statelessness removed MCP's cost and kept everything else. Why our migration was harder: the low-level SDK Most MCP servers use the SDK's high-level helpers: declare your tools, let the SDK handle the protocol. We couldn't. The Apify MCP Server is built on the SDK's low-level API, for two reasons. First, tools were dynamic by design. An add-actor tool let agents pull any of the thousands of Actors https://apify.com/actors serverless cloud programs for web automation in Apify Store https://apify.com/store into the session at runtime. Second, the server is hierarchical: any Actor on the Apify platform can itself be an MCP server, and mcp.apify.com https://mcp.apify.com attaches it and proxies its tools. Both features needed custom request handlers, tool registries, and session state the high-level API never sees. Much of that code assumed a persistent session, and switching to the high-level API mid-migration would only have made the job bigger, so we stayed low-level and removed those assumptions instead. One removal was deliberate. The add-actor tool had to go, because its whole job was mutating the session's tool list, and the new spec requires tool lists to be session-independent. "From back in the day, we thought it's a great idea. It was not," our AI engineer Jakub https://www.linkedin.com/in/doas-jakub-kopecky/ says. The call-actor tool now does the same job statelessly, reaching any Actor without adding it first. Refactors before the feature A few weeks of refactors came first: retiring the legacy SSE transport and hardening session handling, then a two-week migration push, tracked at the end under umbrella issue 1128 https://github.com/apify/apify-mcp-server/issues/1128 . The first agent-led attempts never merged. One prototype cloned hundreds of lines of orchestration per protocol era, and another made the new code depend on the legacy session server. We kept both as reference, and settled on doing the refactors before the feature. Behavior-preserving refactors separated shared server logic from protocol-specific code. All the sessionful SDK code moved into a private legacy adapter, and both protocol paths now consume the same shared core through small, one-directional interfaces. Tool lists are composed per request, because a stateless request has no handshake to remember anything from. That left the stateless adapter to go in as pure new behavior. Coding agents wrote practically every line of this migration, and we spent our time on the designs and the diffs. Most of the review went to making those diffs smaller: reusing services that already existed, adjusting instead of adding, deleting instead of guarding. Agents write fast, and they write too much. The hardest parts ran through shepherd https://github.com/apify/shepherd , the Claude Code plugin we built internally, dogfooded on this migration, and have since made public. Proving nothing broke Nothing shipped until the old behavior was pinned: - Ahead of the first refactor, an agent pinned existing behavior, quirks included, as roughly 70 integration tests. Those ran against the server directly, with no MCP client in the loop. - An end-to-end suite then locked the contract with a real client. We used mcpc to capture the old build's live traffic and diff it byte for byte against each refactored build, and a harness fired about 280 probes at the legacy path 1168 https://github.com/apify/apify-mcp-server/pull/1168 . The pins and diffs caught rewrites that changed behavior they were meant to preserve. The harness was meant to be disposable, kept out of CI and slated for deletion, and Jiří's comment on the PR read: "I know we discussed not to do it : " Its case table now runs against production 1180 https://github.com/apify/apify-mcp-server/pull/1180 . The hosted server and the rollout On the hosted side we changed as little as possible. The legacy path keeps its Redis stores and its multi-node coordination, while modern requests fork at the front door onto a fresh server that's torn down after the response. Since no instance holds session state, any instance can answer any request. We wrote the multi-node tests anyway. Two things happen before auth, because the spec asks for them. server/discover answers without a token, since a new client has to learn what the endpoint speaks before it has credentials, and it stays rate-limited. Malformed protocol headers get a 400 without credentials too. Our first cut had the ordering wrong, and the conformance suite caught it before merge. 1172 https://github.com/apify/apify-mcp-server/pull/1172 merged the evening of July 28, wiring the suite into CI for both eras. A late spec change moved server identity into each response's meta , and the beta SDK still expected the older shape, so it would have rejected exactly the clients that conformed. The stable 2.0.0 SDK landed at 23:55 UTC on July 27, and 1165 https://github.com/apify/apify-mcp-server/pull/1165 bumped to it the following morning, which broke our own rule against dependencies younger than three days. We took a hand inspection and an extra day of rollout testing instead of the wait. v0.14.0 went out on July 29, v0.14.1 followed, and the stateless path went live on July 30, a day later than planned. Live traffic was running on it within 24 hours, rejecting malformed requests the way the conformance suite required. about one client in eight already takes the stateless path, up from 1% just a week ago. Claude, Claude Code, Codex, and Google Antigravity already negotiate 2026-07-28 against Three weeks in: mcp.apify.com https://mcp.apify.com , with mcpc right behind. What's next: tasks and resources Jan Curn, Apify's CEO, skips past the plumbing: "Tasks and resources on our side, that's something we really use, and it's pretty cool." The mapping is direct: an Actor run is work that outlives a single request the tasks extension , and a dataset is data an agent reads and watches a resource . Long Actor runs already work without a persistent connection. call-actor waits up to 45 seconds for a run to finish, 30 by default, then returns a run ID with polling instructions, and the run itself outlives any connection. Adopting the redesigned tasks extension comes next. The stateless path can't push notifications through subscriptions/listen yet, so the dataset-subscription demo stays on the roadmap. Takeaways What we'd tell a team starting the same migration: - A prototype that doesn't merge is still useful work. Two throwaway branches exposed the wrong design early. - AI agents wrote the code, and keeping it small was our job. Their default was to add and duplicate, so most of the human time went to review. - Refactoring first turns a risky feature into a small one. The stateless adapter landed as pure new behavior. - Pin the surface you promised not to break. Probes and byte-level diffs are cheap next to one client regression. - Run the official conformance suite before you release. It caught an auth-ordering bug the in-house tests didn't model. Try it The Apify MCP Server passed 100,000 monthly users in July 2026, and that number is why we expect most SaaS products to end up headless, driven through API, CLI, and MCP, with UI-centric dashboards as the nice-to-have. We built session replication in Redis, fought its leaks and floods, and then the protocol deleted the need for it, without any of those users noticing. On whether MCP is still worth the trouble, that's the best argument we have. You can check it yourself. mcp.apify.com serves both eras on one URL: a modern request takes the stateless path, and a legacy initialize gets the same session behavior as before. mcpc v0.6.0, released on August 2, auto-negotiates 2026-07-28, and mcpc server-discover shows what the server supports. Issues go to apify/apify-mcp-server https://github.com/apify/apify-mcp-server . FAQ Does this break my existing setup? One thing changed: we retired the add-actor tool on both eras in v0.13.0 , and call-actor covers what it did. Everything else is dual-era: legacy clients keep the initialize handshake against the same endpoint, pinned by roughly 70 integration tests and an end-to-end suite built on mcpc. Is this "MCP 2.0"? Officially, no. The protocol calls it revision 2026-07-28. The only official 2.0s are the SDK versions implementing it. How do I test my client against the new revision? If you use a standard MCP client Claude, ChatGPT, Cursor, Codex, and the rest , there's nothing to set up: the same URL serves both eras, and your client switches to the stateless path on its own once it supports the new revision. To test a client you're building yourself, the quick path is mcpc v0.6.0, which auto-negotiates it. To build a request by hand: - Put the protocol version and client capabilities in the namespaced meta keys both required . - Send MCP-Protocol-Version matching meta and Mcp-Method headers on every Streamable HTTP POST. - Add Mcp-Name on tools/call , resources/read , and prompts/get . When do tasks land? Tasks live in an official extension redesigned around polling, and adopting it is next on the Apify roadmap. Long Actor runs already work: call-actor waits, then returns a run ID to poll. Dataset subscriptions arrive with subscriptions/listen support.