Claude Code
How to deploy a remote MCP server (Streamable HTTP): Cloudflare Workers, Vercel, Railway and more (2026)
How do I deploy an MCP server so remote clients can connect over HTTP?
Build your server with an official SDK's Streamable HTTP handler (createMcpHandler in the TypeScript SDK v2, or mcp.run(transport="streamable-http") or streamable_http_app() in the Python SDK v2), expose it at a single endpoint such as /mcp, and deploy it to any HTTPS host: Cloudflare Workers, a Next.js route on Vercel, or a web service on Railway that listens on 0.0.0.0:$PORT. Under the current spec (2026-07-28) each request is a self-contained POST with no session, so any instance can answer it. Before going live, add your public hostname to the SDK's Host/Origin allowlist and put token verification in front of the handler. Clients then connect with the URL, for example claude mcp add --transport http <name> <url>.
Claude Code settings change often
Get one email a week when a setting, flag or permission rule changes, checked against the vendor docs.
What a remote MCP server is
MCP defines two standard transports: stdio for a local subprocess and Streamable HTTP for a server that runs as its own process and handles many clients. A remote MCP server is one that speaks Streamable HTTP at a public HTTPS URL.
- The server exposes one HTTP endpoint, the MCP endpoint. The spec's example is https://example.com/mcp. The Python SDK serves it at /mcp by default and Cloudflare's examples use /mcp. The TypeScript SDK's handler answers on any path you mount it on (app.all('/mcp', ...) in the Express recipe). On Vercel with Next.js the route file decides the path, for example /api/mcp.
- The client sends every JSON-RPC request as its own HTTP POST. The server answers with a single JSON object or with an SSE stream scoped to that one request.
- The current spec revision is 2026-07-28. It removed protocol-level sessions (the Mcp-Session-Id header), the initialize handshake and the standalone GET stream. Each request now carries its protocol version, and the MCP-Protocol-Version header is required on every POST.
- The older HTTP+SSE transport from 2024-11-05 has been deprecated since 2025-03-26. New servers should not use it.
Revisions 2025-03-26 through 2025-11-25 also used Streamable HTTP, but with optional Mcp-Session-Id sessions, a GET stream and resumable streams. Both current SDKs (v2) still serve those older clients from the same handler.
Authentication options
Authorization is optional in MCP. When an HTTP server does use it, the spec says it should follow the MCP authorization spec, which is based on OAuth 2.1.
- OAuth (spec route): the MCP server acts as an OAuth 2.1 resource server. It must publish OAuth 2.0 Protected Resource Metadata (RFC 9728) so clients can find the authorization server, and it must check that each access token was issued for it (audience validation). Clients send Authorization: Bearer <token> on every request, never in the query string.
- Static token or API key: Claude Code and Cursor both let you attach custom headers, such as Authorization: Bearer or an API key header, to a remote server. This is simpler, but you have to verify the header in your own code.
- The TypeScript SDK handler does not verify tokens. It passes auth through, so put verification in front of it: Express has requireBearerAuth, Vercel's mcp-handler has withMcpAuth, and on Cloudflare you wrap the handler in the Workers OAuth Provider. The Python SDK verifies tokens when you pass MCPServer(..., token_verifier=YourVerifier(), auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])). You still write the TokenVerifier.
- A server must not pass the token it received from the MCP client on to upstream APIs.
Minimal server with the official SDKs
TypeScript SDK v2 (npm package @modelcontextprotocol/server) targets the 2026-07-28 spec. createMcpHandler takes a factory that builds a fresh McpServer for each request and returns a web-standard fetch handler, so the endpoint is stateless and scales horizontally as-is. In v1 you wired a StreamableHTTPServerTransport and called connect() for each request; createMcpHandler replaces that.
terminal (Node + Express)
npm install @modelcontextprotocol/server @modelcontextprotocol/express @modelcontextprotocol/node express
server.ts (from the SDK Express recipe)
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { toNodeHandler } from '@modelcontextprotocol/node';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const handler = createMcpHandler(() => {
const server = new McpServer({ name: 'notes', version: '1.0.0' });
server.registerTool('add-note', { description: 'Append a note', inputSchema: z.object({ text: z.string() }) }, async ({ text }) => ({
content: [{ type: 'text', text: `Saved: ${text}` }]
}));
return server;
});
const app = createMcpExpressApp();
const node = toNodeHandler(handler);
app.all('/mcp', (req, res) => void node(req, res, req.body));
app.listen(3000);terminal (test it)
curl -s -X POST http://127.0.0.1:3000/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'This smoke test, taken from the SDK docs, omits the MCP-Protocol-Version header, so the server handles it as a legacy request. Real 2026-07-28 clients also send MCP-Protocol-Version: 2026-07-28, Mcp-Method and the protocol version in _meta. Use the MCP Inspector to test the current protocol.
Python SDK v2 (package mcp, Python 3.11+) uses MCPServer. Passing transport="streamable-http" to run() starts a Starlette app on uvicorn. It listens on 127.0.0.1:8000 by default and serves the endpoint at /mcp.
terminal
uv add "mcp[cli]" # or: pip install "mcp[cli]"
server.py (from the Python SDK docs)
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool()
def search_books(query: str) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r}."
if __name__ == "__main__":
mcp.run(transport="streamable-http", port=3001)Transport options such as host, port, streamable_http_path and json_response go to run(), not to MCPServer(). If you need more control, build the ASGI app with mcp.streamable_http_app() and run it with any ASGI server.
Deploy on Cloudflare Workers
Cloudflare's Agents SDK recommends createMcpHandler for new servers. It is stateless. McpAgent, the older Durable Object approach, is deprecated and feature-frozen. Cloudflare notes that its quick-deploy templates still use the deprecated McpAgent path and says to start from its mcp-worker example for a new server.
src/index.ts (Cloudflare Agents SDK)
import { createMcpHandler } from "agents/mcp/server";
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({
name: "My MCP Server",
version: "1.0.0",
});
server.registerTool(
"hello",
{
description: "Returns a greeting message",
inputSchema: { name: z.string().optional() },
},
async ({ name }) => {
return {
content: [{ text: `Hello, ${name ?? "World"}!`, type: "text" }],
};
},
);
return server;
}
export default {
fetch(request, env, ctx) {
return createMcpHandler(createServer)(request, env, ctx);
},
};You can also deploy the plain TypeScript SDK handler: createMcpHandler from @modelcontextprotocol/server returns a { fetch } object, so export default handler is the whole Worker. Run wrangler dev server.ts to serve it on http://127.0.0.1:8787. That handler does no Host or Origin checks, so put hostHeaderValidationResponse and originValidationResponse in front of handler.fetch.
For a new server, start from Cloudflare's mcp-worker example and the createMcpHandler code above. To try a working deployment quickly, you can scaffold Cloudflare's authless template. It still uses the deprecated McpAgent path, so treat it as a demo and do not build on it:
terminal
npm create cloudflare@latest -- remote-mcp-server-authless --template=cloudflare/ai/demos/remote-mcp-authless cd remote-mcp-server-authless npm start # local: http://localhost:8788/mcp npx @modelcontextprotocol/inspector@latest # connect to the /mcp URL and list tools npx wrangler@latest deploy
- The deployed URL looks like https://remote-mcp-server-authless.your-account.workers.dev/mcp.
- For OAuth, use createMcpHandler as the apiHandler of the Workers OAuth Provider, with apiRoute set to "/mcp" (export default new OAuthProvider({ apiRoute: "/mcp", apiHandler: createMcpHandler(createServer), ... })). Cloudflare documents Cloudflare Access and third-party providers such as GitHub. Its GitHub template (cloudflare/ai/demos/remote-mcp-github-oauth) still uses apiHandler: MyMCP.serve("/mcp") on the deprecated McpAgent path, so swap in createMcpHandler(createServer). Secrets are set with npx wrangler secret put.
Deploy on Vercel (Next.js)
Vercel's docs use mcp-handler v2 inside a Next.js App Router app. You need Node.js 20 or later. Version 2 of mcp-handler is built on @modelcontextprotocol/server and drops the legacy HTTP+SSE transport and the Redis configuration.
terminal
npm i mcp-handler@2.1.1 @modelcontextprotocol/server@2 zod@4
app/api/mcp/route.ts (from Vercel docs)
import { createMcpHandler } from 'mcp-handler';
import { z } from 'zod';
const handler = createMcpHandler((server) => {
server.registerTool(
'roll_dice',
{
description: 'Roll an N-sided die',
inputSchema: z.object({ sides: z.number().int().min(2) }),
},
async ({ sides }) => {
const value = 1 + Math.floor(Math.random() * sides);
return {
content: [{ type: 'text', text: `You rolled a ${value}!` }],
};
},
);
});
export { handler as GET, handler as POST };- Clients connect to /api/mcp over Streamable HTTP. The route file sets the path, so mcp-handler needs no base path.
- To test locally, run next dev, then npx @modelcontextprotocol/inspector@latest. Pick Streamable HTTP, enter http://localhost:3000/api/mcp and paste the proxy session token.
- Deploy with Git or the CLI by running vercel from the project root (use vercel --prod for later production deploys). The production URL looks like https://my-mcp-server.vercel.app/api/mcp.
- For auth, wrap the handler with withMcpAuth(handler, verifyToken, { required: true, ... }). A request without a valid token gets 401, and a token missing a required scope gets 403. Serve the protected resource metadata with protectedResourceHandler at app/.well-known/oauth-protected-resource/route.ts.
Deploy on Railway, Fly.io and other container hosts
Railway runs your Node or Python server as an ordinary web service. Its docs say your server must bind to 0.0.0.0 and listen on the PORT environment variable, which Railway injects. If it does not, Railway's edge proxy returns 502 (Application failed to respond).
server.ts changes for Railway (TypeScript, Express)
// Binding 0.0.0.0 drops the SDK's localhost-only Host check, so list your public hostname.
const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['mcp.example.com'] });
// ... app.all('/mcp', ...) as above
const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0');server.py for Railway (Python)
from mcp.server import MCPServer
from mcp.server.transport_security import TransportSecuritySettings
mcp = MCPServer("Notes")
@mcp.tool()
def add_note(text: str) -> str:
"""Save a note."""
return f"Saved: {text}"
security = TransportSecuritySettings(
allowed_hosts=["mcp.example.com", "mcp.example.com:*"],
allowed_origins=["https://app.example.com"],
)
app = mcp.streamable_http_app(transport_security=security)
# start command: uvicorn server:app --host 0.0.0.0 --port $PORT --proxy-headers --forwarded-allow-ips='<proxy address>'- Deploy from the dashboard: New Project, then GitHub repo, then Deploy Now. Or use the CLI: run railway init, then railway up.
- Under the service's Public Networking settings, click Generate Domain to get a Railway domain. Railway issues and renews SSL certificates automatically.
- With the Python SDK, if the Host allowlist is missing, every request to the real hostname fails with 421 Misdirected Request. Setting host= alone does not allowlist a hostname.
- Fly.io: fly mcp launch deploys an existing stdio MCP server (run with npx, uvx, go run or a Docker image) to a Fly Machine and configures the client. It sets up bearer token auth by default. Example: fly mcp launch "uvx mcp-server-time" --claude --server time.
Connect clients
Claude Code
claude mcp add --transport http my-server https://mcp.example.com/mcp # with a static token claude mcp add --transport http my-server https://mcp.example.com/mcp \ --header "Authorization: Bearer your-token" # OAuth servers: add the server, then run /mcp inside Claude Code to log in
Cursor: .cursor/mcp.json (project) or ~/.cursor/mcp.json (global)
{
"mcpServers": {
"my-server": {
"url": "https://mcp.example.com/mcp",
"headers": {
"Authorization": "Bearer ${env:MY_SERVICE_TOKEN}"
}
}
}
}VS Code: .vscode/mcp.json
{
"servers": {
"my-server": {
"type": "http",
"url": "https://mcp.example.com/mcp"
}
}
}- Claude Code --scope options: local (the default), project (writes to .mcp.json) and user.
- VS Code tries Streamable HTTP first and falls back to SSE if the server does not support HTTP.
- For clients that only support local servers, Cloudflare documents the mcp-remote proxy: command npx with args ["mcp-remote", "<url>"].
Pre-launch checklist
- HTTPS: serve the endpoint over HTTPS. The auth spec requires HTTPS for all authorization server endpoints, and every redirect URI must be localhost or HTTPS.
- Origin validation: the spec requires servers to validate the Origin header on every incoming connection and to answer an invalid Origin with 403. On localhost, bind to 127.0.0.1, not 0.0.0.0.
- Host allowlist: in Python, streamable_http_app() accepts only localhost by default and answers every request to your real hostname with 421 until you pass transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...]). In TypeScript, createMcpExpressApp checks Host and Origin only on a localhost bind, and binding 0.0.0.0 drops that default, so pass allowedHosts. The bare createMcpHandler does no checks, so put hostHeaderValidationResponse and originValidationResponse in front of it.
- Auth: verify tokens before the MCP handler runs, check the audience, return 401 for invalid or expired tokens and 403 for insufficient scope, and publish /.well-known/oauth-protected-resource if you use OAuth.
- State: on 2026-07-28 there are no sessions, so any instance can serve any request and no sticky routing is needed. For older (2025-era) clients, the TypeScript handler is stateless by default. In Python, those clients get Mcp-Session-Id sessions held in one worker's memory, so use sticky sessions or set stateless_http=True.
- Multi-instance Python: share the same RequestStateSecurity keys and the same server name on every instance, or multi-round-trip tools fail across workers.
- TLS behind a proxy (Python): run uvicorn with --proxy-headers and --forwarded-allow-ips so redirects do not downgrade to http://.
- Do not build anything new on the deprecated HTTP+SSE transport.
Related on AgentAtlas
Get Claude Code settings changes by email
When a setting, flag or permission rule in this guide changes, it goes in the weekly AgentAtlas update, checked against the vendor docs. One email a week.
Sources
More on Claude Code
More Claude Code guides
- How to allow Claude Code to run all commands: permission modes, allow rules and skip permissions
- Claude Code permissions: allow, ask and deny rules, rule syntax and allowed tools
- Claude Code telemetry and data retention: what is sent and how to turn it off
- CLAUDE.md: where it goes, how it loads, and how it works with AGENTS.md
- Claude Code hooks: format on save, block risky edits, get notified
- Claude Code subagents: create one, limit its tools, and call it
- Add MCP servers to Claude Code (claude mcp add, .mcp.json, scopes)
- Claude Code custom slash commands and skills (SKILL.md)
- Claude Code settings.json: file locations, precedence, and key settings
- Claude Code plugins and marketplaces: install, create, and share
- Claude Code headless mode: claude -p, --output-format json, --bare and CI flags
- Claude Code statusline: custom status bar in settings.json with context %, cost and git branch
- Claude Code /model: switch models, aliases (opus, sonnet, haiku, opusplan), ANTHROPIC_MODEL and effort
- Claude Code /cost and /usage: check token spend, usage limits and reduce costs
- Claude Code VS Code extension: install, shortcuts, settings and /ide (works in Cursor too)
- MCP 2026-07-28 Migration Guide: Stateless Protocol, No Sessions, server/discover, MRTR, and SDK v2 Upgrade
- Subagents and Custom Agents Compared: Claude Code vs Codex vs Cursor vs Gemini CLI vs GitHub Copilot (2026)
- Allow all commands in any AI coding agent: Cursor, Claude Code, Codex, Gemini CLI, Copilot CLI and 7 more (2026)
- Claude Code sandbox: /sandbox, sandbox.enabled, allowedDomains, excludedCommands and strict mode
Other guides
Reviewed Oct 18, 2026. Settings change often; the linked vendor docs are the source of truth.