You built an MCP server, it works great in Claude Desktop, and now three teammates want it. Suddenly you are writing install docs, debugging someone's Node version, and rotating an API key that got committed to a dotfiles repo.
That is the moment to stop running it locally over stdio and deploy it as a remote MCP server. One URL, one deployment, every client connects to the same thing.
Why remote beats stdio for anything shared
Stdio is the default MCP transport: the client launches your server as a subprocess and pipes JSON-RPC through stdin and stdout. For a personal tool on your own machine, it is fine.
It falls apart the moment more than one person needs the server:
- Every user installs and updates their own copy.
- Secrets (database credentials, API keys) live on every laptop.
- You cannot ship a fix; you can only ask people to upgrade.
- Web clients like claude.ai cannot spawn a subprocess at all.
A remote server flips all of that. You deploy once, clients connect over HTTPS, secrets stay on the server, and claude.ai, ChatGPT connectors, Claude Code, and Cursor can all reach it.
The transport: Streamable HTTP, not SSE
The MCP spec (revision 2025-03-26 and later) defines one remote transport: Streamable HTTP. It replaced the older HTTP+SSE transport, which needed two endpoints and a long-lived event stream just to exist.
Streamable HTTP is much simpler to host:
- The client POSTs each JSON-RPC message to a single endpoint, for example
https://mcp.example.com/. - The server replies with
application/jsonfor a plain response, or upgrades that same response totext/event-streamwhen it wants to stream progress. - An optional
Mcp-Session-Idheader carries session state. If your tools are stateless, you can skip sessions entirely and treat every request independently.
That last point is the practical win. A stateless Streamable HTTP server is just a regular HTTPS endpoint. It works behind Cloudflare, scales horizontally, and survives a redeploy mid-conversation because there is no in-memory session to lose.
If you are choosing today: build Streamable HTTP, and only add SSE fallback if you must support clients frozen on the old transport.
A minimal deployable server
Here is the shape of a stateless remote server using the official TypeScript SDK. This is the whole trick: one POST handler, new transport per request.
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
const app = express();
app.use(express.json());
app.post("/", async (req, res) => {
const server = new McpServer({ name: "my-remote-server", version: "1.0.0" });
server.tool("ping", { host: z.string() }, async ({ host }) => ({
content: [{ type: "text", text: `pong from ${host}` }],
}));
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless mode
});
res.on("close", () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000);
Put that behind TLS and you have a remote MCP server. Python users get the same result from FastMCP with mcp.run(transport="http").
Where to host it
Anywhere that serves HTTPS works. The real differences are cold starts, streaming support, and how much you want to manage.
| Option | Good for | Watch out for |
|---|---|---|
| Cloudflare Workers | Low latency, free tier, built-in OAuth helpers via workers-oauth-provider |
CPU time limits on heavy tools |
| Fly.io / Railway / Render | Containers, long-running work, databases nearby | Cold starts on cheap tiers can trip client timeouts |
| A VPS behind nginx or Caddy | Full control, predictable cost | You own patching, TLS renewal, and uptime |
Two proxy details bite people constantly. First, make sure your reverse proxy does not buffer responses, or SSE streaming breaks silently. Second, validate the Origin header on requests; the spec calls this out because a browser tab on a malicious site can otherwise poke at DNS-rebound local endpoints.
Auth: the fork in the road
This is where remote MCP gets opinionated. You have two realistic options, and which one you need depends on your clients.
API key in a header
The simple path: issue each user a key, require it in a header, check it on every request. Claude Code sets this up in one line:
claude mcp add --transport http myserver https://mcp.example.com/ \
--header "x-api-key: sk_your_key"
This is perfect for developer tools. Keys map cleanly to billing and rate limits, revocation is a database update, and there is no consent screen to build.
The limitation: some hosted clients, notably the claude.ai custom connector UI, are built around OAuth and have no field for a custom header. Key-based auth works great in Claude Desktop, Claude Code, and Cursor, and that covers most developer audiences today.
OAuth 2.1
The MCP authorization spec makes your server an OAuth 2.1 resource server. In practice that means:
- Serve protected resource metadata (RFC 9728) at
/.well-known/oauth-protected-resourcepointing at your authorization server. - Support Dynamic Client Registration, because MCP clients register themselves; there is no dashboard where users paste a client ID.
- Require PKCE, since clients are public.
- Return
401with aWWW-Authenticateheader so clients know where to start the flow.
My honest advice: do not hand-roll this. Use Cloudflare's workers-oauth-provider, or delegate to Auth0, WorkOS, Clerk, or Stytch, all of which now ship MCP-specific flows. OAuth is the right answer when non-technical users connect through a web UI and you need per-user identity rather than a shared team key. It is overkill for an internal tool five engineers use.
A live remote server to compare against
It helps to poke a working deployment while building your own. link.sc runs its MCP server remotely at https://mcp.link.sc/ using exactly this recipe: Streamable HTTP, stateless, API key auth. Listing tools requires no key:
curl -X POST https://mcp.link.sc/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
You get back search and fetch tool definitions as plain JSON-RPC. Calling a tool adds one header, x-api-key, with a key from link.sc/dashboard/keys. That request-response symmetry is what you are aiming for: if curl can exercise your server, any MCP client can. The full walkthrough for connecting it to Claude is in Connect Claude to the Web with the link.sc MCP Server.
A pre-launch checklist
Before you hand out the URL:
- Statelessness verified. Kill the server mid-session and confirm clients recover on the next request.
- Auth on
tools/call, not necessarily ontools/list. Letting anyone list tools is free marketing; letting anyone run them is a bill. - Rate limits per key or per user. An agent in a loop will happily call your tool 400 times a minute.
- Timeouts under 60 seconds per tool call. Clients give up; long jobs should return early and expose a status tool.
- Logging with request IDs. When a user says "Claude said the tool failed," you want to find that exact call.
If MCP itself is still fuzzy, back up to What Is the Model Context Protocol and Why It Matters. And if your remote server needs live web data behind its tools, the link.sc API gives you fetch and search as clean markdown with one key.
Want a remote MCP server without deploying anything? Create a free link.sc account and point your client at mcp.link.sc.