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

Other guides

How to allow Cursor to run all commands: Run Everything, allowlist and sandboxCodex CLI: approval and sandbox modesGemini CLI: YOLO mode, auto_edit and allowing specific shell commandsCursor Privacy Mode: telemetry, training and data retentionGemini CLI: turn off usage statistics and telemetryAdd MCP servers to Cursor with mcp.json: Keep files out of Cursor with .cursorignore: Configure Codex with config.toml: Configure Gemini CLI with settings.json: GitHub Copilot custom instructions: Adding MCP servers to GitHub Copilot: Gemini CLI extensions and custom commands: Configuring OpenCode with opencode.json: Cline Rules: workspace, global, and conditional rulesKiro steering files: .kiro/steering, inclusion modes, and AGENTS.mdZed agent: instruction files, MCP servers, tool permissions, and ACP agentsCursor CLI: install, headless mode, permissions and CIAider: CONVENTIONS.md, .aider.conf.yml and API keysJunie guidelines, MCP and the Action Allowlist: Amp AGENTS.md, settings.json and MCP: Configure Goose extensions, hints, recipes and permissions: Customize OpenHands with skills, AGENTS.md, setup.sh and MCP: Configure Factory Droid CLI: AGENTS.md, settings.json, autonomy, custom droids and MCPConfigure Qwen Code: settings.json, providers, QWEN.md, MCP and approval modesCursor hooks: hooks.json, beforeShellExecution, afterFileEdit and exit codesCursor sandbox: sandbox.json, network allowlist and run modesCursor settings: settings.json, Cursor Settings and the ~/.cursor config filesCursor browser agent: @browser, browser automation and approval modesCursor subagents: .cursor/agents, frontmatter, built-in Explore and BrowserCursor telemetry and OpenTelemetry export (Privacy Mode, OTLP, Analytics API): Cursor rules setup: project, user and Team Rules, .mdc frontmatter, globs and AGENTS.mdCodex AGENTS.md: global, project and override instruction files explainedCodex MCP servers: codex mcp add and [mcp_servers] in config.tomlCodex sandbox modes: read-only, workspace-write, danger-full-access and network accesscodex exec: run Codex non-interactively in scripts and CI with JSON outputCodex skills: create SKILL.md, skill folder locations and $skill invocationGEMINI.md in Gemini CLI: context file locations, imports and /memoryGemini CLI MCP servers: gemini mcp add and mcpServers in settings.jsonGemini CLI custom slash commands: TOML files, {{args}}, !{shell} and @{file}Gemini CLI headless mode: gemini -p, JSON output and exit codes for scripts and CIGemini CLI sandbox: enable Docker, Podman, macOS Seatbelt or gVisorcopilot-setup-steps.yml: set up the GitHub Copilot cloudGitHub Copilot CLI permissions: --allow-tool, --deny-tool, --allow-all-tools and trusted directoriesGitHub Copilot custom agents: .github/agents/*.agent.md profiles, tools and frontmatterGitHub Copilot code review settings: automatic PR reviews, rulesets, effort level and approvalsOpenCode MCP Server Setup: Local and Remote Servers in opencode.jsonOpenCode Permissions: Allow, Ask or Deny Bash, Edit and WebfetchOpenCode Custom Agents and Subagents: Markdown Files, Modes and @ MentionsCline MCP Setup: cline_mcp_settings.json for Local and Remote ServersCline Auto Approve and YOLO Mode: Run Commands and Edits Without PromptsCline Skills: SKILL.md Location, Format and Slash Command UseAmp MCP Setup: amp mcp add, amp.mcpServers and Workspace ApprovalAmp AGENTS.md: File Locations, @ Mentions and Glob-Scoped GuidanceAmp Skills: SKILL.md Folders, amp skill add and Search OrderKiro MCP Setup: .kiro/settings/mcp.json, autoApprove and OAuthKiro Hooks: .kiro/hooks JSON, PreToolUse, PostFileSave and StopKiro Permissions: permissions.yaml Allow, Ask and Deny RulesFactory Droid MCP Setup: droid mcp add, mcp.json Locations and /mcpFactory Droid Hooks: hooks.json, PreToolUse Matchers and Exit CodesFactory Droid Custom Droids (Subagents): .factory/droids Markdown FilesJunie MCP Setup: .junie/mcp/mcp.json for CLI and /mcp CommandJunie Brave Mode and Action Allowlist: Auto-Approve Commands in Junie CLIJunie Custom Slash Commands: .junie/commands Markdown Files and Argumentsgoose .goosehints and AGENTS.md: Global and Project Instructionsgoose Permission Modes: GOOSE_MODE auto, approve, smart_approve and chatgoose Recipes: recipe.yaml Format, Parameters and goose run --recipeZed Agent Tool Permissions: Auto-Approve Terminal Commands with tool_permissionsUse Claude, Codex and Gemini CLI in Zed: External Agents and agent_serversZed with Ollama, LM Studio and llama.cpp: Local Model Setup and Context LengthAdd an MCP server to any AI coding agent: Claude Code, Cursor, Codex, Gemini CLI, VS Code and 8 moreSettings file locations for every AI coding agent: Claude Code, Cursor, Codex, Gemini CLI, OpenCode, Qwen Code, Amp and Factory DroidRules and instruction files in every AI coding agent: .cursor/rules, CLAUDE.md, AGENTS.md, GEMINI.md, Copilot instructions and moreGemini CLI hooks: settings.json, BeforeTool, AfterTool, exit codes and /hooksGitHub Copilot hooks: .github/hooks/*.json for Copilot CLI and the cloud agentHooks in every AI coding agent: Claude Code, Cursor, Gemini CLI, GitHub Copilot, Kiro and Factory DroidAgent Skills (SKILL.md) in every AI coding agent: Claude Code, Codex, Cline, Amp and OpenHandsHeadless mode in every AI coding agent: claude -p, codex exec, gemini -p, Cursor agent -p and copilot -p for scripts and CISandbox modes in every AI coding agent: Claude Code, Cursor, Codex and Gemini CLI compared

Reviewed Oct 18, 2026. Settings change often; the linked vendor docs are the source of truth.