{"slug": "migrating-the-apify-mcp-server-to-the-new-stateless-spec", "title": "Migrating the Apify MCP Server to the new stateless spec", "summary": "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.", "body_md": "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`\n\nhandshake 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.\n\nThrough 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.\n\nBut 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.\n\n## What the 2026-07-28 revision changes\n\nWhen 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.\n\nTwo proposals carry the change:\n\n**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`\n\nhandshake is gone too (SEP-2575).`_meta`\n\nfield. Clients can probe a server with the new`server/discover`\n\nmethod, which servers must implement and clients may skip.\n\nFeatures that needed a persistent connection were redesigned:\n\n- Mid-call input became a multi-round-trip exchange.\n- List-change and resource-update notifications became an opt-in\n`subscriptions/listen`\n\nstream. - Long-running work moved to a tasks extension.\n\nTool lists now carry cache hints, and new HTTP headers let gateways route and rate-limit without parsing bodies.\n\nIn the legacy protocol, a session opened with an `initialize`\n\ncall, 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`\n\n:\n\n```\n// Legacy (2025-11-25 and earlier): a session starts with a handshake\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"method\": \"initialize\",\n  \"params\": {\n    \"protocolVersion\": \"2025-11-25\",\n    \"capabilities\": { \"tools\": {} },\n    \"clientInfo\": { \"name\": \"example-client\", \"version\": \"1.0.0\" }\n  }\n}\n// The server replies with its capabilities, and every request after\n// this one depends on the session the exchange created.\n// Modern (2026-07-28): no handshake; every request stands alone\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 7,\n  \"method\": \"tools/list\",\n  \"params\": {\n    \"_meta\": {\n      \"io.modelcontextprotocol/protocolVersion\": \"2026-07-28\",\n      \"io.modelcontextprotocol/clientCapabilities\": {},\n      \"io.modelcontextprotocol/clientInfo\": { \"name\": \"example-client\", \"version\": \"1.0.0\" }\n    }\n  }\n}\n// Any server instance can answer this request.\n```\n\nWith 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.\n\n**The spec's own**\n\n**Which version is which?**[era vocabulary](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning):\n\n-\n\n**revision 2026-07-28 and later**\n\n**Modern:****2025-11-25 and earlier**\n\n**- Legacy:****a server speaking both**\n\n**- 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.\n\n## So why not just a REST API?\n\nThe \"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.\n\nWe run the other case in production: a hosted server, and clients we don't control.\n\nRunning [mcp.apify.com](https://mcp.apify.com) on multiple nodes meant sessions lived in Redis:\n\n- A state store and a task store\n- An event store for resumability\n- Pub/sub for node coordination\n- Recovery for orphaned sessions\n\nAnd the channel needed constant attention:\n\n- Clients that never closed their sessions\n- Session floods we had to rate-limit\n- Memory leaks in session teardown\n- Cross-node state that silently went stale\n\nWe paid for all of it in infrastructure, and the new spec removes the entire class of problems.\n\nOur 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.\n\nWhat'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.\n\n## Why our migration was harder: the low-level SDK\n\nMost 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.\n\nFirst, tools were dynamic by design. An `add-actor`\n\ntool 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.\n\nBoth 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.\n\nOne removal was deliberate. The `add-actor`\n\ntool 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`\n\ntool now does the same job statelessly, reaching any Actor without adding it first.\n\n## Refactors before the feature\n\nA 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).\n\nThe 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.\n\nBehavior-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.\n\nCoding 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.\n\nThe 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.\n\n## Proving nothing broke\n\nNothing shipped until the old behavior was pinned:\n\n- 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.\n- 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 (\n[#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)).\n\n## The hosted server and the rollout\n\nOn 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.\n\nTwo things happen before auth, because the spec asks for them. `server/discover`\n\nanswers 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.\n\n[#1172](https://github.com/apify/apify-mcp-server/pull/1172) merged the evening of July 28, wiring the suite into CI for both eras.\n\nA late spec change moved server identity into each response's `_meta`\n\n, 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.\n\nv0.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.\n\n**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**\n\n**Three weeks in:**[mcp.apify.com](https://mcp.apify.com), with mcpc right behind.\n\n## What's next: tasks and resources\n\nJan 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).\n\nLong Actor runs already work without a persistent connection. `call-actor`\n\nwaits 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.\n\nAdopting the redesigned tasks extension comes next. The stateless path can't push notifications through `subscriptions/listen`\n\nyet, so the dataset-subscription demo stays on the roadmap.\n\n## Takeaways\n\nWhat we'd tell a team starting the same migration:\n\n- A prototype that doesn't merge is still useful work. Two throwaway branches exposed the wrong design early.\n- 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.\n- Refactoring first turns a risky feature into a small one. The stateless adapter landed as pure new behavior.\n- Pin the surface you promised not to break. Probes and byte-level diffs are cheap next to one client regression.\n- Run the official conformance suite before you release. It caught an auth-ordering bug the in-house tests didn't model.\n\n## Try it\n\nThe 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.\n\nYou can check it yourself. mcp.apify.com serves both eras on one URL: a modern request takes the stateless path, and a legacy `initialize`\n\ngets the same session behavior as before. mcpc v0.6.0, released on August 2, auto-negotiates 2026-07-28, and `mcpc server-discover`\n\nshows what the server supports. Issues go to [apify/apify-mcp-server](https://github.com/apify/apify-mcp-server).\n\n## FAQ\n\n### Does this break my existing setup?\n\nOne thing changed: we retired the `add-actor`\n\ntool on both eras (in v0.13.0), and `call-actor`\n\ncovers what it did. Everything else is dual-era: legacy clients keep the `initialize`\n\nhandshake against the same endpoint, pinned by roughly 70 integration tests and an end-to-end suite built on mcpc.\n\n### Is this \"MCP 2.0\"?\n\nOfficially, no. The protocol calls it revision 2026-07-28. The only official 2.0s are the SDK versions implementing it.\n\n### How do I test my client against the new revision?\n\nIf 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:\n\n- Put the protocol version and client capabilities in the namespaced\n`_meta`\n\nkeys (both required). - Send\n`MCP-Protocol-Version`\n\n(matching`_meta`\n\n) and`Mcp-Method`\n\nheaders on every Streamable HTTP POST. - Add\n`Mcp-Name`\n\non`tools/call`\n\n,`resources/read`\n\n, and`prompts/get`\n\n.\n\n### When do tasks land?\n\nTasks live in an official extension redesigned around polling, and adopting it is next on the Apify roadmap. Long Actor runs already work: `call-actor`\n\nwaits, then returns a run ID to poll. Dataset subscriptions arrive with `subscriptions/listen`\n\nsupport.", "url": "https://wpnews.pro/news/migrating-the-apify-mcp-server-to-the-new-stateless-spec", "canonical_source": "https://blog.apify.com/mcp-stateless-migration/", "published_at": "2026-08-18 14:23:26+00:00", "updated_at": "2026-08-18 14:42:17.461547+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure", "ai-agents"], "entities": ["Apify", "Model Context Protocol", "mcpc", "mcp.apify.com", "SEP-2567", "SEP-2575"], "alternates": {"html": "https://wpnews.pro/news/migrating-the-apify-mcp-server-to-the-new-stateless-spec", "markdown": "https://wpnews.pro/news/migrating-the-apify-mcp-server-to-the-new-stateless-spec.md", "text": "https://wpnews.pro/news/migrating-the-apify-mcp-server-to-the-new-stateless-spec.txt", "jsonld": "https://wpnews.pro/news/migrating-the-apify-mcp-server-to-the-new-stateless-spec.jsonld"}}