Skip to main content

Deploying and Operating MCP Servers

This guide explains hosting, scaling, maintaining compatibility, and operational policy for running MCP servers in production. Because the protocol became stateless in the 2026-07-28 revision, a remote MCP server can now be deployed and scaled the same way as an ordinary HTTP API. This document covers how to choose hosting on that premise, how to keep compatibility while legacy clients remain, and what to decide for operations. For the changes in the specification, see MCP 2026-07-28 Spec Changes and Migration; for observation and auditing, see MCP Observability and Auditing; for authorization, see MCP Authorization - OAuth 2.1 and Metadata Discovery.

1. What This Document Covers​

  • Deploying as stateless HTTP
  • Hosting on Cloudflare Workers
  • Other hosting options
  • Supporting old and new clients at the same time
  • Version operations
  • Gateways, WAFs, and rate limiting
  • Caching
  • Secret management
  • Long-running work
  • CI/CD and SLOs
  • Managing servers inside an organization

Deploying as stateless HTTP​

MCP in the 2026-07-28 revision is a stateless protocol. The initialize handshake, Mcp-Session-Id, and protocol-level sessions are removed from the core request path, and each request carries its own protocol version, client identity, and capabilities in _meta. Streamable HTTP becomes simple.

  • Every JSON-RPC request arrives as its own independent POST
  • The server answers with a JSON response or an SSE stream scoped to that request
  • There is no GET stream, no ending a session with DELETE, and no resumption with Last-Event-ID
  • Questions from the server to the client (elicitation, sampling, roots) are embedded in results (MRTR, input_required) rather than sent as independent requests
  • Change notifications arrive on the response stream of a POST to subscriptions/listen

As a result, you need neither sticky sessions nor held-open streams. A request can be handled by whichever instance it reaches, and adding and removing instances, rolling deploys, and blue-green deploys follow the same procedures as an ordinary Web API. The specification also says a server must not use connection or process identity as a substitute for a conversation or session.

"Stateless" does not, however, mean the application holds no state. State that spans multiple requests (a shopping cart, a workflow) is handled by the server issuing an explicit handle, the client passing it as an argument on every call, and the server saving the state in a shared store (a database or cache). State kept in an instance's memory is invisible to a request that reaches another instance. For handle design (unguessable, bound to the user, with an expiry), see "State handle hijacking" in MCP Security Best Practices - Attacks and Mitigations.

There are also HTTP transport requirements to keep in mind for operations.

  • Validate the Origin header. If present and invalid, respond 403. When running locally, bind only to localhost
  • Every POST carries MCP-Protocol-Version, Mcp-Method, and (where applicable) Mcp-Name. The server rejects a mismatch between headers and body with 400 and -32020
  • When opening an SSE stream, servers are encouraged to include X-Accel-Buffering: no, which keeps reverse proxies such as nginx from buffering the response and delaying event delivery
  • On long-lived streams (especially subscriptions/listen), servers are encouraged to send an SSE comment line (a line beginning with :) periodically, so intermediaries and client idle timeouts do not close the connection
  • Closing an SSE response stream is treated as cancellation of that request. If a load balancer timeout drops the connection, the processing is interrupted

Hosting on Cloudflare Workers​

Cloudflare Workers is a good place to host a stateless MCP server. A stateless MCP server can run in a single Worker, with no sticky routing and no session storage. createMcpHandler from the Cloudflare Agents SDK has graduated into the official MCP TypeScript SDK (the SDK moved from Node.js to Web Standards APIs, which improves support for Bun, Deno, and Workers).

A minimal server looks like this.

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

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

server.registerTool(
"hello",
{
description: "Return a greeting",
inputSchema: { name: z.string().optional() },
},
async ({ name }) => ({
content: [
{
type: "text",
text: `Hello, ${name ?? "World"}!`,
},
],
}),
);

return server;
}

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

There are two points to note.

  • Pass createMcpHandler a function that creates the server (a factory), not a server instance. The stateless handler creates one server per MCP request, so concurrent requests never share a connected server instance
  • Make the Worker's default export an object with a fetch method. If you make a function the default export, as in export default createMcpHandler(createServer), Wrangler treats it as a WorkerEntrypoint class

For authorization, the Workers OAuth Provider (@cloudflare/workers-oauth-provider) adds OAuth 2.1 authorization to a Worker. The Cloudflare blog shows a setup that protects requests to /mcp and gives a single Worker both the authorization server and resource server roles.

import { OAuthProvider } from "@cloudflare/workers-oauth-provider";

export default new OAuthProvider({
apiRoute: "/mcp",
apiHandler: mcpHandler,
defaultHandler: authorizationHandler,
authorizeEndpoint: "/authorize",
tokenEndpoint: "/oauth/token",
clientIdMetadataDocumentEnabled: true,
resourceMetadata: {
resource: "https://mcp.example.com/mcp",
authorization_servers: ["https://mcp.example.com"],
scopes_supported: ["mcp:read"],
},
});

Requests under apiRoute go to apiHandler, and everything else (such as the /authorize page) goes to defaultHandler. It also provides RFC 9728 protected resource metadata and a challenge for unauthenticated requests. Note the following.

  • To run as an authorization server, bind a KV namespace as OAUTH_KV. If it accepts Client ID Metadata Documents, it also needs the global_fetch_strictly_public compatibility flag
  • Tokens, codes, and secrets are stored only as hashes, and props are encrypted
  • The scopes you expose are advertised but not enforced. Your handler must verify ctx.auth.scope
  • The library's README describes putting the authorization server and resource server in the same Worker as the 0.x-style setup, and recommends a layout that splits the roles. Check the latest README before you design

Use Durable Objects only when you need state. McpAgent is no longer needed for the protocol; it is deprecated and feature-frozen (no removal version has been announced). Durable Objects remain the right choice for applications that need coordinated state.

For the basics of Workers, Wrangler, and deployment, see Cloudflare Workers - Edge Functions as the Azure Functions Equivalent.

Other hosting options​

Stateless MCP needs no special hosting. The selection criteria are the same as for an ordinary API.

AspectWhat to weigh
Distance to the existing backendIf it is on the same network and the same authentication platform, co-locate it in containers (Azure Container Apps, AKS)
Processing timeServerless for short request and response work. For long-running work, combine containers or a job queue
Reach of distributionTo run near users worldwide, use the edge (Cloudflare Workers)
Scaling profileServerless for bursty load and scale to zero. Containers for steady load
Network constraintsIf it touches private internal resources, place it inside the same virtual network

On Azure, the candidates are Container Apps (autoscaling on HTTP load), Functions (short request and response), and AKS (an existing Kubernetes platform). For how Azure and Cloudflare services correspond, see Azure vs Cloudflare Service Comparison.

Whichever host you use, check the following in common.

  • Proxies and gateways do not buffer SSE responses (X-Accel-Buffering: no)
  • Idle timeouts are not shorter than long streams such as subscriptions/listen, or you send periodic keep-alives
  • Health checks use a lightweight dedicated endpoint, not an MCP request (such as server/discover)
  • Horizontal scaling keeps no state in instance memory

A local stdio server runs on the user's machine, so it is a distribution problem rather than a deployment one. See MCP Registry and Distributing Servers and Operating Local MCP Servers Safely.

Supporting old and new clients at the same time​

Even after 2026-07-28 is published, legacy clients (2025-11-25 and earlier, using the initialize handshake) will remain for some time. The specification allows a server to implement dual-era support, accepting both old and new, and permits serving both on the same endpoint at the same time. The server chooses behavior by how the client opens.

  • A request carrying modern _meta is served statelessly according to this revision
  • An initialize request selects legacy semantics and follows the negotiated legacy revision (scoped to the session on HTTP)

The compatibility outcome depends on the combination.

ClientServerOutcome
ModernModernWorks
ModernLegacyFails (a legacy server either returns an error or stays silent, depending on the implementation)
Dual-eraModernWorks
Dual-eraLegacyWorks (after the modern request fails, it falls back to initialize)
LegacyModern onlyFails. The required headers are missing and the request is rejected with 400. A legacy client has no way to switch to the newer revision
LegacyDual-eraWorks

In other words, if you switch to a server that accepts only the modern era, every legacy client can no longer connect. A server that supports only the modern era is encouraged to include the protocol versions it supports in the error it returns to initialize, since for a legacy client that message may be the only diagnostic it can show the user.

Cloudflare's implementation provides migration stages as follows.

  • The same /mcp accepts both the new protocol and stateless requests from 2025-era Streamable HTTP clients. The default legacy: "stateless" of createMcpHandler serves ordinary tools, resources, and prompts to legacy clients too. In that case GET and DELETE return 405 and no session state persists. To make an endpoint modern-only, specify legacy: "reject"
  • A server that depends on legacy sessions, server-to-client requests, or standalone streams keeps a legacy path (a temporary "legacy lane" on the SDK 1.x line) beside the new one on the same URL, moves its features to the modern era, drains sessions, and removes the legacy path during the deprecation window
  • To run both on one URL, route with isLegacyRequest(). So that the stateless handler does not consume legacy requests, specify legacy: "reject" on it (see the code in "Code Examples")

Replace features that depend on sessions (questions from the server, standalone notification streams) with the modern mechanisms. A question from the server is returned as an input_required result, and the client retries the original operation. List-change notifications move to subscriptions/listen (a POST with an SSE response). Session IDs stored before the migration become invalid, and clients reconnect. If you had tied in-flight work to an old session, that work does not resume.

Version operations​

In the modern era, every request declares its own protocol version, and the server accepts or rejects each request independently. There is no negotiation handshake.

  • For a version the server does not support, return 400 and UnsupportedProtocolVersionError (-32022). Put the supported versions in data.supported and the requested version in data.requested. The client picks a common version from the list and retries
  • A server must implement server/discover. A client may call it first to learn the supported versions (not required)
  • In operation, monitor the count of -32022 and the distribution of MCP-Protocol-Version per request to see how many older clients remain (MCP Observability and Auditing)

There is also a policy for the deprecation period. Features are classified as Active, Deprecated, or Removed, and a deprecated feature stays in the specification for at least 12 months. In the 2026-07-28 revision, Roots, Sampling, Logging, and Dynamic Client Registration (DCR) were deprecated. The legacy HTTP+SSE transport was deprecated earlier, and under the SEP-2596 transition rule its earliest removal is set sooner: three months after SEP-2596 reaches Final. When you plan, think as follows.

  1. For 12 months from the publication date, deprecated features still exist in the specification. Before they become eligible for removal, check usage and replace them
  2. Close the legacy path only after metrics confirm that the share of legacy clients has fallen enough
  3. Allow a notice period for users before closing. After you close, a legacy client has no way to switch to the new revision
  4. Have a compatibility policy for changes to the API your own server provides (tool names, arguments, result structure), separate from the MCP version. For a change that breaks inputSchema, add a tool under a new name and deprecate the old one

Gateways, WAFs, and rate limiting​

Every POST in Streamable HTTP carries Mcp-Method, and tools/call, resources/read, and prompts/get carry Mcp-Name. This lets load balancers, gateways, and WAFs apply routing, rate limits, and allow or deny rules per method and per tool without parsing the JSON body.

  • Per-tool rate limits, routing only write tools to a separate backend, allowing particular tools only from particular networks
  • Parsing the body is done by the server. The server rejects a mismatch between headers and body with 400 and -32020. This requirement exists so that a gap between a gateway that decides on headers and a server that executes on the body cannot be exploited
  • Before applying header-based policy, a gateway confirms that MCP-Protocol-Version is a version that requires header and body validation. It rejects older versions and requests without the header
  • To use a tool argument as a routing key, mirror it into an Mcp-Param-{name} header with x-mcp-header. Do not use sensitive values
  • On a validation failure at a gateway, returning an appropriate HTTP error (such as 400) is enough; a JSON-RPC error body is not required

Use the limits at the gateway together with the limits inside the server (the specification requires servers to rate limit). For policy details, see "Rate limiting and anomaly detection" in MCP Observability and Auditing.

Caching​

A server must attach caching hints to results with resultType: "complete" from server/discover, tools/list, prompts/list, resources/list, resources/templates/list, and resources/read.

FieldMeaning
ttlMsThe number of milliseconds a client may consider the result fresh. Close to HTTP Cache-Control: max-age. 0 means immediately stale. Negative values are not allowed (the server returns 0 or more)
cacheScopepublic or private. Close to Cache-Control: public and private

The meaning of cacheScope matters in operation.

  • public: contains no user-specific data. Clients, shared gateways, and caching proxies may store it and serve it to any user
  • private: contains per-user content. It can be reused only within the same authorization context (the same access token). It must not go into a shared cache, and must not be shared across authorization contexts

Operational guidance follows.

  • A list of tools, prompts, and resource templates that is identical for everyone is public. A resources/read that depends on the authenticated user, or a filtered list, is private
  • Even a public result from an authenticated endpoint can be shared. For example, a public result of an authenticated tools/list can be reused by a user with a different access token. So do not use cacheScope as a substitute for access control. Enforce per-tool access control separately
  • A paginated list is cached page by page, independently. Keep cacheScope the same on all pages of the same list
  • Do not cache input_required results, nor the results of retries that carry inputResponses or requestState
  • list_changed notifications and TTLs can be used together. A notification acts as an immediate invalidation signal
  • ttlMs is a freshness hint, not a guarantee that data is unchanged. Make it shorter or longer to match how often the data changes

An MCP request is a POST, so CDNs and intermediate proxies do not necessarily cache it by default. These hints are first of all for clients (host applications) to follow. If you configure a shared gateway or CDN to cache public results keyed by method and body parameters, check separately how your product handles POST caching and key construction. The top priority is a setup that never puts private results into a shared cache.

Secret management​

The secrets an MCP server holds are mainly the following.

  • Credentials for downstream APIs (API keys, service accounts, client secrets)
  • Token signing keys and encryption keys
  • Client secrets, if it runs as an authorization server
  • Credentials for the deployment target's API, such as Cloudflare or Azure

The guidance is as follows.

  • Keep secrets out of code, repositories, images, and results returned to clients. Keep them out of logs, too
  • Use the runtime environment's secret mechanism. On Cloudflare Workers, register them with wrangler secret put and keep only non-sensitive values in [vars] of wrangler.toml (Wrangler CLI Guide). In containers, inject them at runtime from the platform's secret feature or a key vault
  • Do not pass a user's token straight through to a downstream API (token passthrough is forbidden). The server accepts only tokens issued to it, and uses its own credentials, or a separate token obtained through token exchange, downstream (MCP Authorization - OAuth 2.1 and Metadata Discovery)
  • Scope credentials narrowly, rotate them regularly, and be ready to revoke them immediately if they leak
  • Separate CI/CD secrets from runtime secrets. The token used for deployment is not needed by the running server

Long-running work​

Work that does not fit the request and response frame should not be a synchronous call. Use one of the following.

  • The Tasks extension (io.modelcontextprotocol/tasks): Tasks moved from the core to an extension in the 2026-07-28 revision. If the server declares this extension and a tool is called as a task, tools/call returns a task handle and the client polls with tasks/get. A completed task inlines its result. If more input is needed along the way, the task moves to an input_required state and the client returns the input responses with tasks/update. For the extension in detail, see MCP Extensions - Tasks, MCP Apps, and Skills
  • A job queue and an explicit handle: the tool submits a job and returns an ID, with separate tools to query the status and fetch the result. This is the natural design on a stateless protocol
  • Progress notifications: for synchronous work that takes tens of seconds, send progress notifications on the SSE stream and return the result at the end. Watch out for load balancer timeouts and buffering

The key to the design is to decide timeouts layer by layer. Align the client timeout, the gateway and load balancer idle timeouts, the server's processing timeout, and the downstream API timeout so that outer layers are longer and inner layers shorter. When an SSE response stream is closed, the request is treated as cancelled, so the server stops work as soon as it notices the cancellation and must not send any further messages. For work that does not fit a serverless execution limit, split it into a queue and workers.

CI/CD and SLOs​

Whatever kind of server it is, put the following stages in the deployment pipeline.

  1. Unit tests and schema validation (inputSchema / outputSchema)
  2. Protocol tests: run tools/list and representative tools/call calls with Inspector's CLI. Check schema portability with --strict and decide by exit code (Testing and Debugging MCP)
  3. Conformance tests: add the conformance suite to regression tests to find differences in interpretation of the specification early
  4. Authorization tests: check the paths for 401, 403, audience mismatch, and insufficient scope
  5. Deploy to staging and run a smoke test (the response of server/discover, the count from tools/list)
  6. Gradual deployment (canary, rolling). Because it is stateless, mixing old and new instances causes no session breakage. Note, however, that a change to tool definitions (names, schemas) leaves both old and new visible until clients' caches (ttlMs) expire
  7. Watch the error rate and latency in production metrics, and roll back automatically

As examples of SLOs, decide indicators like the following (choose the numbers to fit the nature of your service).

IndicatorWhat is measuredNotes
AvailabilityThe proportion of successful requests, excluding protocol-layer errors and 5xxTreat tool execution errors (isError) separately as user input mistakes versus downstream failures
Latencyp50, p95, p99 of tools/call (per tool)Obtainable per tool through Mcp-Name
Authorization success rateThe share of 401 and 403 for legitimate usersA sudden rise points to a configuration mistake or a failure on the token-issuing side
Share of older-version clientsDistribution of MCP-Protocol-Version, count of -32022Used to decide when to close the legacy path

Managing servers inside an organization​

To manage many MCP servers in an organization, provide approved servers from one place and prevent use of unapproved servers (Shadow MCP). A gateway such as MCP Server Portals aggregates servers inside the organization behind a single endpoint and centralizes authentication, policy, and auditing. The features and design cautions are summarized in "Gateways and MCP Server Portals" in MCP Authorization Extensions and Enterprise Adoption.

For distribution on the public side and approval inside the organization (allowlists, private registries, pre-adoption review), see MCP Registry and Distributing Servers.

2. Hosting Options​

OptionGood forNotes
Cloudflare WorkersStateless MCP servers, global distributionAdd Durable Objects if you need state
Containers (Azure Container Apps / AKS, etc.)Co-locating with an existing backend, long-running workDesign horizontal scaling and health checks
Serverless functionsMostly short request and responseExecution time and cold start limits
Local (stdio)Tools on a developer's machineDistribution, updates, and sandboxing challenges

3. Common Pitfalls for Developers​

Assuming sticky sessions​

Implementations up to the 2025 revision often relied on Mcp-Session-Id and per-instance in-memory session state. In the modern era, keep state in a shared store and pass it around with explicit handles. Sticky sessions are unnecessary, and bringing them in makes scaling and deploying needlessly hard.

Going modern-only and locking out legacy clients​

A modern-only server rejects initialize from legacy clients and leaves them unable to connect. Measure the client distribution, migrate with a dual lane, and close it only after the share has fallen.

Passing an instance to createMcpHandler​

Pass the factory (createServer). The SDK v1 style of passing the result of createServer() shares a server instance across concurrent requests. And do not make a function the default export, as in export default createMcpHandler(...).

SSE gets buffered and notifications do not arrive​

A reverse proxy accumulates the response. Add X-Accel-Buffering: no and also check the proxy's buffering settings. Send keep-alive comment lines on long streams.

Using cacheScope: public as a substitute for access control​

A public result can be reused for other users. Make content that differs per user private, and enforce per-tool authorization separately from caching.

Using an MCP request for health checks​

An MCP request, which needs headers and the required _meta fields, is too heavy for a health check. Provide a lightweight dedicated endpoint.

Putting secrets in plaintext environment configuration​

[vars] in wrangler.toml, or configuration baked into an image, is not a place for secrets. Use the platform's secret feature.

Thinking a deprecated feature disappears after 12 months​

Twelve months is the minimum period before it becomes eligible for removal, not the removal date. The timing of removal is decided in individual revisions of the specification. Update your plan by watching your metrics and the current state of the specification.

4. Code Examples​

A minimal stateless server on Cloudflare Workers​

As in the hello-server example above, pass the factory to createMcpHandler and export an object that has fetch.

Routing the old and new lanes on one URL​

When you run a stateless modern lane and a sessionful legacy lane on the same URL, route with isLegacyRequest(). Specify legacy: "reject" on the stateless handler so it does not consume legacy requests.

// Structure only (an excerpt from Cloudflare's migration guide). See the guide for how to build stateless and legacy
if (await isLegacyRequest(request)) {
return legacy.fetch(request, env, ctx);
}
return stateless(request, env, ctx);

To make a modern-only endpoint that does not accept legacy clients, also specify legacy: "reject". For how to pass the option, check the migration guide for your SDK version.

A gateway policy that assumes header validation (pseudocode)​

if request.header("MCP-Protocol-Version") is not a version that requires header validation:
reject 400
if request.header("Mcp-Method") == "tools/call":
tool = decode(request.header("Mcp-Name"))
if tool in write_tools:
rate_limit(key = user, limit = strict)
if tool in admin_tools and source not in internal_network:
reject 403
forward to MCP server

How to write this differs by gateway product. Only the order of decisions is shown here.

A smoke test after deployment​

# Check supported versions with server/discover (modern server)
curl -sS https://mcp.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientCapabilities":{}}}}'

# Confirm that the tool list is not empty (decide by exit code)
mcp-inspector --cli https://mcp.example.com/mcp --transport http \
--protocol-era modern --stored-auth-only --method tools/list --format json \
| jq -e '.result.tools | length > 0' > /dev/null

5. Summary​

  • MCP in the 2026-07-28 revision is stateless, and each request is an independent POST. You need neither sticky sessions nor held-open streams, and you can scale and do rolling deploys the same way as an ordinary Web API. If you need state, keep it with an explicit handle and a shared store
  • Cloudflare Workers suits a stateless MCP server that runs in a single Worker. Pass createMcpHandler a factory, not an instance, and export an object that has fetch. Use the Workers OAuth Provider for authorization, and Durable Objects only when you need state
  • Choose other hosting by distance to the existing backend, processing time, reach of distribution, and scaling profile. Check SSE buffering and idle timeouts
  • A modern-only server locks out legacy clients. Provide a lane that accepts both old and new on the same endpoint, and close it only after metrics confirm the share of legacy clients
  • A version mismatch comes back as -32022. Deprecated features stay in the specification for at least 12 months, but that is a minimum period, and you plan by usage
  • Mcp-Method and Mcp-Name let gateways and WAFs apply policy per method and tool without parsing the body. Trust only requests of versions that require header validation
  • ttlMs and cacheScope are caching hints. Do not put private in a shared cache, and do not use public as a substitute for access control
  • Manage secrets with the runtime environment's secret mechanism, and do not pass tokens through. Handle long-running work with the Tasks extension or a job queue and explicit handles, and align timeouts layer by layer
  • Put Inspector's CLI and conformance tests in CI/CD. Decide SLOs on availability, per-tool latency, the share of older-version clients, and similar indicators

References​