Build a Stateless MCP Server on Cloudflare Workers

Cloudflare Agents SDK v0.20.0 can run a stateless MCP server on Cloudflare without an initialization handshake, protocol session, or Durable Object. For an ordinary tool server, the upgrade is mostly subtraction—and protocols rarely get applause for deleting furniture.

This tutorial builds a temperature-conversion server against the published MCP 2026-07-28 specification, tests it with MCP Inspector, and deploys it to a public endpoint. The example is read-only and low-impact enough for a demo. Production exposure still needs usage controls; a tool that deletes data deserves considerably more ceremony.

What changed in MCP 2026-07-28

The MCP 2026-07-28 specification makes the protocol stateless at the transport layer. Earlier Streamable HTTP clients began with an initialize exchange, received an Mcp-Session-Id, and sent that identifier with later requests. Infrastructure then had to preserve or recreate that transport session.

The new path removes both the handshake and session header. Each request carries the protocol version, client identity, and capabilities it needs. A client may call server/discover to learn what the server supports, but it no longer has to establish a connection-wide identity before calling a tool.

That distinction matters. Stateless MCP does not require a stateless application. A shopping tool can return a basket_id, for example, then accept that ID on the next call while storing the basket in a database. State becomes an explicit part of the tool contract instead of luggage hidden inside the transport.

Protocol concernEarlier Streamable HTTP2026-07-28 candidate
Startupinitialize handshakeDirect request or server/discover
Identity and capabilitiesNegotiated onceSent with each request
Transport sessionMcp-Session-IdNone
Horizontal routingSession-awareAny compatible instance

Operationally, that makes remote MCP traffic look more like ordinary HTTP. A gateway can route, rate-limit, and trace an operation using request data instead of consulting sticky-session machinery. A failed Worker instance also does not strand a protocol session; a later self-contained request can land elsewhere. Durable tasks and application records still need durable designs, but simple tool discovery and invocation no longer drag a connection manager behind them.

If you want to see the other side of the connection, our guide to connecting Claude Code to a hosted MCP server covers the client workflow. Here, we are building the endpoint that clients call.

Prerequisites for a stateless MCP server on Cloudflare

You need Node.js, npm, a Cloudflare account, and Wrangler authenticated to that account. Start with an existing TypeScript Worker or create a fresh one:

npm create cloudflare@latest -- stateless-mcp-server
cd stateless-mcp-server
npx wrangler login

Choose a basic “Hello World” Worker when the setup wizard asks for a template. Then install the current Agents SDK, MCP SDK v2 server package, and Zod:

npm install agents@latest @modelcontextprotocol/server zod

Cloudflare’s v0.20.0 changelog introduced the server-factory API while the dated specification was still a release candidate. Use latest to reproduce the new path, then commit the lockfile before deployment. That turns a moving package label into a reviewable dependency set. Teams still comparing runtimes can start with our AI agent framework comparison.

Before changing code, run npm ls agents @modelcontextprotocol/server zod and save the output with your deployment notes. If a future package update changes the API, that one line tells you exactly which dependency set produced a working server. Protocol tutorials age much better when the version evidence survives the browser tab.

Build the stateless MCP server on Cloudflare

Register one low-risk tool

Replace src/index.ts with the following Worker. It creates a fresh MCP server, registers one validated tool, and hands the request to Cloudflare’s stateless handler.

import { McpServer } from "@modelcontextprotocol/server";
import { createMcpHandler } from "agents/mcp/server";
import { z } from "zod";

function createServer() {
  const server = new McpServer({
    name: "temperature-tools",
    version: "1.0.0",
  });

  server.registerTool(
    "convert_temperature",
    {
      description: "Convert a temperature between Celsius and Fahrenheit",
      inputSchema: {
        value: z.number(),
        from: z.enum(["celsius", "fahrenheit"]),
      },
    },
    async ({ value, from }) => {
      const converted =
        from === "celsius"
          ? (value * 9) / 5 + 32
          : ((value - 32) * 5) / 9;

      const to = from === "celsius" ? "Fahrenheit" : "Celsius";
      return {
        content: [
          {
            type: "text",
            text: `${value} degrees ${from} equals ${converted.toFixed(1)} degrees ${to}.`,
          },
        ],
      };
    },
  );

  return server;
}

export default {
  fetch(request, env, ctx) {
    return createMcpHandler(createServer)(request, env, ctx);
  },
} satisfies ExportedHandler;

The Cloudflare transport reference uses this same pairing: McpServer from @modelcontextprotocol/server and createMcpHandler from agents/mcp/server. Zod turns the input description into validation, so the handler rejects “moderately chilly” where the tool expects a number.

The tool callback returns MCP content rather than a bare JavaScript number. That content array is what clients present to a model or user. Keeping the description precise also matters: the model selects tools by their names, descriptions, and schemas, so “convert between Celsius and Fahrenheit” gives it a much better routing signal than the traditional developer favorite, “does stuff.”

This calculation is deterministic and has no side effects, making it a good first deployment. When you replace it with a real API call, validate every argument, set an upstream timeout, translate failures into useful tool errors, and avoid returning raw secrets or stack traces. Stateless transport removes session bookkeeping; it does not remove the need to engineer the tool.

Why the server factory matters

Pass the createServer function itself—not a global server instance—to createMcpHandler. Cloudflare creates an isolated server for each request. That mirrors the new protocol: the request arrives with its own context, executes, and finishes without leaving transport-session state behind.

The factory also keeps the stateless bundle focused. Cloudflare says the isolated entry point leaves legacy McpAgent, WorkerTransport, client transports, and SDK v1 modules out of that path. Less compatibility code in a new server is the pleasant kind of technical debt: somebody else already paid it.

Three isolated server nodes surrounded by separate teal request rings

Test locally, then deploy

Start the Worker and note its local port:

npm run dev

Your MCP endpoint will normally be http://localhost:8787/mcp, although Wrangler may choose another port. Opening that URL in a browser is not a protocol test; the endpoint expects MCP messages, not an enthusiastic human with a refresh button.

In a second terminal, launch the official interactive client:

npx @modelcontextprotocol/inspector@latest

Cloudflare’s remote MCP server guide recommends this exact test loop. In Inspector, enter the local /mcp URL, connect, choose List Tools, and invoke convert_temperature with value set to 32 and from set to fahrenheit. The result should report 0.0 degrees Celsius.

If the tool list is empty, first confirm Inspector is pointed at /mcp, not the Worker root. A connection failure usually means the local port differs from the example or Wrangler is not running. A schema error is better news: the server is reachable and Zod is correctly refusing the payload. Retest with the exact lowercase enum values from the schema.

Once the local call works, deploy and copy the Worker URL:

npx wrangler@latest deploy

Reconnect Inspector to https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev/mcp and repeat the call. An agent can now discover and invoke the same tool remotely—the basic primitive behind the more elaborate agent-as-tool workflow.

Migration and security: when one route is not enough

Cloudflare’s default handler accepts MCP 2026-07-28 clients and older clients that can make stateless requests. For ordinary tools, prompts, and resources, that means one route and one set of definitions. It does not magically convert session-dependent behavior.

Server situationRecommended path
New, ordinary tool serverUse createMcpHandler
Existing server without session dependenciesMove definitions into an SDK v2 factory
Uses RPC, replay, pushed requests, or protocol sessionsRun stateless and legacy routes during migration
Reads private data or changes systemsAdd OAuth and scoped authorization

McpAgent is now deprecated and feature-frozen. If an existing deployment depends on standalone streams, server-to-client pushes, replay, RPC, or session-scoped state, keep a temporary legacy lane while redesigning those features. Cloudflare documents routing old traffic with isLegacyRequest(), then letting existing sessions drain before removing the route.

The specification’s stateless MCP proposal makes protocol version and client capabilities per-request data. Use the same explicitness for application state: return a job or cart ID, accept it on later calls, and store the underlying record in KV, D1, or another durable system. Do not rebuild an invisible transport session under a more fashionable variable name.

Finally, keep public endpoints boring. You can leave this converter unauthenticated for a short test, but rate-limit it before treating the deployment as permanent. A tool that sends email, queries customer records, starts paid compute, or modifies infrastructure needs identity, scoped authorization, rate limits, and audit logs before an agent gets the URL.

  • Use explicit resource IDs rather than relying on a client’s previous connection.
  • Make mutations idempotent or require a confirmation token so retries do not repeat destructive work.
  • Log tool name, authenticated principal, result status, and trace identifier without logging sensitive arguments.

The state is dead; long live the state

The unresolved question is how long real client ecosystems will need the legacy initialization path after the published specification has moved on. A protocol can declare itself stateless; installed desktop clients and enterprise integrations do not upgrade by decree.

The deeper win is not that MCP has less state. It is that state now has to be explicit and architecturally honest: request context travels with the request, while business data lives behind an identifier and storage boundary the application actually owns.

The next major Cloudflare Agents SDK release is the catalyst to watch. Cloudflare has scheduled the SDK v1 createMcpHandler compatibility overload for removal in that major version; whether the ecosystem is ready when it arrives will show how quickly this freshly emptied room can lose its legacy furniture.

Get the Daily Pulse

Sharp analysis on what's actually moving in AI. No hype, no filler, no weekly digest.

Get the Daily Pulse

Sharp AI analysis, daily. Two minutes, every morning.

Get the Daily PulseTwo minutes, every morning