TL;DR: The MCP protocol is identical over both transports — same JSON-RPC 2.0 messages, same capability handshake, same tools/resources/prompts. What changes is the channel, the deployment, and the trust boundary. Use stdio when the server needs your filesystem, a local database, or near-zero latency. Use Streamable HTTP when the server must be reachable by many users, hosted centrally, or kept off end-user machines. The 2026-07-28 spec revision made Streamable HTTP stateless — the biggest enabler for hosting MCP at scale [3][6].

The Transport Question

Every MCP server you’ve ever connected to made the same architectural decision before it could serve a single tool call: how do JSON-RPC messages physically travel between client and server? That decision — the transport — determines where the server runs, who can reach it, how you authenticate it, and how hard it is to scale. Get it wrong and you’ll fight your own architecture forever [2].

What a Transport Actually Is

MCP is an open JSON-RPC 2.0 protocol: a host runs one MCP client per server, performs a capability handshake, then discovers and calls that server’s tools, resources, and prompts. The transport is just the channel those messages travel over — not the message format and not the primitives [2]. Per the spec, there are two transports that matter today: stdio for local servers and Streamable HTTP for remote ones [5].

stdio Under the Hood

With stdio, the host spawns the server as a subprocess and pipes JSON-RPC messages over the process’s stdin and stdout. There is no port, no URL, and no network hop — the server lives and dies with the host that launched it [2]. This is the default for desktop integrations: a server configured in Claude Desktop’s config runs via npx or uvx, inherits environment variables (often containing API keys), and executes with your user’s privileges [2][5].

That privilege is the whole appeal and the whole risk. A stdio server can read your files, reach localhost services, and use any credential you handed it — which is exactly what makes filesystem, database, and dev-tooling servers so powerful. But “local” means a private transport, not safe code: a poisoned npm or PyPI package, or a server that quietly exfiltrates over an outbound call, operates with full local context [2]. Latency is negligible and your data never leaves the device [2].

Streamable HTTP Under the Hood

Streamable HTTP is a single HTTP endpoint — conventionally /mcp — that the client POSTs JSON-RPC requests to, with the server able to stream responses and server-initiated messages back using Server-Sent Events (SSE) when needed [2][3]. Clients must send an Accept header advertising both application/json and text/event-stream, so a server can answer a request either with a plain JSON response or an SSE stream [5].

The trust boundary inverts relative to stdio. Your machine’s filesystem and local secrets stay out of reach, but every request and every piece of data you pass to a tool crosses the network to a third party — and because clients re-fetch tool definitions per session rather than pinning a reviewed copy, a remote server can silently change its tools after you approved it. That’s the “rug-pull” problem, and it’s inherent to remote transports [2]. The older two-endpoint HTTP+SSE transport (a separate SSE channel plus a POST endpoint) is now legacy: the 2026-07-28 spec revision officially deprecated it with a twelve-month offramp, and new servers should use Streamable HTTP [3][6].

When to Use stdio

Choose stdio when the server needs your filesystem, a local database, or developer tooling; when latency must be near-zero; and when you want data to stay on the device [2]. It’s also the right call for single-user CLI integrations — a linter, a repo helper, a local vector index — where spawning a subprocess is simpler and more secure than standing up a network service. There’s no auth surface to manage, no URL to protect, and no rate limiter to tune. The cost is that the server is unreachable from web apps, mobile clients, or anyone else’s machine [1][2].

A stdio server in Claude Desktop looks like this — the host handles process management:

{
  "mcpServers": {
    "local-dev-tools": {
      "command": "npx",
      "args": ["-y", "@your-scope/local-dev-tools"]
    }
  }
}

When to Use Streamable HTTP

Choose Streamable HTTP when a server should be centrally hosted and updated, shared across a team or product, or kept off end-user machines — accepting that data now transits to a third party and that authentication and transport security become part of the threat model [2]. Cloudflare frames it as the shift from desktop software to web software: remote MCP is what lets everyday users “log in and have things just work” across devices, instead of installing and running servers locally [1]. It’s the model for hosted, multi-tenant MCP services, and it’s also what makes auditing tractable — a live URL can be probed, its handshake validated, and its real tools inspected, which is impossible for a subprocess with no network presence [2].

Production Patterns

Cloudflare: Workers + Durable Objects + OAuth 2.1

Cloudflare’s remote MCP stack handles the four hard parts of going online: transport, state, auth, and client compatibility. The McpAgent class in the Agents SDK implements remote transport for you, using Durable Objects behind the scenes to hold persistent connections open for SSE, so a minimal server is ~15 lines with no serialization or transport code [1]. Because each client session is backed by a Durable Object, MCP servers on Cloudflare can be genuinely stateful — games, checkout flows, persistent knowledge graphs — with per-session state persisted to a SQL database [1].

For auth, workers-oauth-provider makes your Worker an OAuth 2.1 provider, with Dynamic Client Registration (RFC 7591) and Authorization Server Metadata (RFC 8414) built in [1]. The pattern worth stealing: your server issues its own token to the MCP client, while the upstream provider token is stored encrypted in Workers KV and never exposed. A compromised client token only grants the limited tool surface you defined — a direct mitigation for OWASP’s “Excessive Agency” risk [1]. You can even gate individual tools on identity, adding an allowlisted generateImage tool only for specific users [1]. Finally, mcp-remote adapts remote servers for clients that only support local connections, so Claude Desktop, Cursor, and Windsurf users can connect today [1].

Railway: Stateless HTTP + Postgres

Railway’s guide is the cleanest minimal remote-server blueprint: an Express app exposing a single /mcp endpoint, with the Node SDK’s NodeStreamableHTTPServerTransport creating a fresh transport per request — stateless, per the 2026-07-28 spec [3]. Deploy to a public domain, and clients connect via claude mcp add --transport http my-server https://your-server.up.railway.app/mcp or a url entry in Cursor’s mcp.json [3].

Two details from that guide are easy to miss. First, host: "0.0.0.0" is required — the default localhost binding rejects requests arriving through a public domain via DNS rebinding protection [3]. Second, the protocol will not carry state for you: the in-memory Map in the tutorial is per-replica and wiped on every redeploy. State lives behind the server, in a database — swap the Map for Postgres queries and the tool definitions don’t change at all [3]. For private workloads, Railway’s private networking (http://<service>.railway.internal:<port>/mcp) keeps the server off the public internet entirely [3].

Lessons from Google’s AI Agent Clinic

Google’s production teardown of a brittle sales agent generalizes to MCP hosting: split monoliths into narrow, orchestrated components; keep state in external stores rather than hardcoded context; and treat observability as non-negotiable — OpenTelemetry traces plus an SSE streaming telemetry dashboard to debug component latencies and resolve “ground-truth disputes” [4]. The same post makes the case for framework-native circuit breakers: exponential backoff, timeout boundaries, and bounded retries instead of hand-rolled retry loops, because agentic loops burn tokens in minutes when a tool fails [4]. Apply that to your remote MCP server: rate-limit, trace, and bound retries at the transport layer.

The 2026 Spec Changes for Streamable HTTP

The 2026-07-28 specification revision is the biggest change to remote MCP since Streamable HTTP itself launched, and it directly reshapes hosting decisions [6]:

  • Stateless core. The initialize/initialized handshake and the Mcp-Session-Id header are retired. Every request is self-describing, carrying protocol version, client identity, and capabilities in _meta — so any request can land on any replica behind a plain round-robin load balancer with no shared storage [3][6].
  • Header-based routing. Requests must include Mcp-Method and Mcp-Name HTTP headers, so gateways, rate limiters, and WAFs can route and meter without parsing JSON bodies [6].
  • MRTR. Server-to-client requests (sampling, elicitation, roots) are redesigned as Multi Round-Trip Requests, removing the need for held-open bidirectional streams — a server returns resultType: "input_required" and the client retries with answers attached [6].
  • Cacheable catalogs. tools/list and prompts/list responses carry ttlMs and cacheScope hints, so clients can cache tool catalogs instead of re-fetching each session [6].
  • Auth hardening. Authorization servers must return iss per RFC 9207 and clients must validate it; Dynamic Client Registration is formally deprecated in favor of Client ID Metadata Documents (CIMD), with DCR kept for backward compatibility [6].
  • Deprecations. HTTP+SSE, Roots, Sampling, and Logging are deprecated with a guaranteed twelve-month minimum window [3][6].

If your server needs cross-call state, make it explicit: mint a handle from a tool and have the model pass it back as an argument, rather than relying on hidden transport state [6].

Code: Both Transports

A minimal stdio server in TypeScript — process I/O only, no network:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "local-dev-tools", version: "1.0.0" });

server.tool(
  "read_file",
  { path: z.string() },
  async ({ path }) => {
    // Runs with your user's privileges — full filesystem access
    const content = await fs.readFile(path, "utf-8");
    return { content: [{ type: "text", text: content }] };
  }
);

await server.connect(new StdioServerTransport());

The same server over Streamable HTTP, condensed from the Railway guide [3]:

import { createMcpExpressApp } from "@modelcontextprotocol/express";
import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";

const server = new McpServer({ name: "todo-mcp-server", version: "1.0.0" });

server.registerTool(
  "create_todo",
  { description: "Add a task", inputSchema: z.object({ title: z.string() }) },
  async ({ title }) => ({
    content: [{ type: "text", text: `Created: ${title}` }],
  })
);

const app = createMcpExpressApp({ host: "0.0.0.0" }); // required for public domains

app.post("/mcp", async (req, res) => {
  // Fresh transport per request: stateless, per the 2026-07-28 spec.
  // sessionIdGenerator: undefined opts out of legacy session mode.
  const transport = new NodeStreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(Number(process.env.PORT) || 3000, "0.0.0.0");

Clients connect either way — a URL for remote servers, or the mcp-remote adapter when your client only supports local connections [1][3]:

claude mcp add --transport http my-server https://your-server.up.railway.app/mcp
// .cursor/mcp.json
{
  "mcpServers": {
    "my-server": {
      "url": "https://your-server-production-xxxx.up.railway.app/mcp"
    }
  }
}
// Claude Desktop — via mcp-remote adapter
{
  "mcpServers": {
    "remote-example": {
      "command": "npx",
      "args": ["mcp-remote", "https://your-server.up.railway.app/mcp"]
    }
  }
}

Decision Matrix

Dimension stdio Streamable HTTP
Security boundary Your machine; server code gets full local privileges [2] Network edge; TLS + auth (OAuth 2.1 or bearer) are mandatory [1][2]
Latency Near-zero, no network hop [2] Network RTT per request; SSE streaming amortizes it [2]
Complexity No ports, URLs, or auth; trivial ops Endpoints, statelessness, auth, rate limiting, observability
Scaling One process per client, 1:1 Horizontal — any replica serves any request (post-2026-07-28) [3][6]
State Process memory / local filesystem External store: Postgres, KV, or Durable Objects [1][3]
Multi-user Single-user by construction [2] Built for shared, multi-tenant, team deployments [1][2]
Auditability Repo/package review only — no endpoint to probe [2] Live endpoint probing, handshake validation, gateways [2][4]
Best for Filesystem, local DB, dev tooling, CLI integrations [2] Web/mobile clients, hosted services, shared servers [1][2]

The One-Line Rule

Start with stdio for anything that only one user on one machine will touch. Move to Streamable HTTP the moment the server must be shared, remotely hosted, or reachable from web and mobile clients — and build it stateless from day one, because the 2026 spec revision made statelessness the native way to scale [3][6]. Treat every server — local or remote — as untrusted until audited: the transport tells you where the trust boundary is, not whether the code behind it is safe [2].

References

[1] Cloudflare Blog — Build and deploy Remote MCP servers to Cloudflare [2] CheckMCP — Local vs Remote MCP Servers (stdio vs Streamable HTTP) [3] Railway Docs — Build and Deploy Your Own MCP Server [4] Google Developers Blog — Production-Ready AI Agents: 5 Lessons from Refactoring a Monolith [5] MCP Specification 2025-03-26 — Transports [6] Model Context Protocol Blog — The 2026-07-28 Specification

  • Hermes Tutorials — Hermes Agent setup, configuration, and advanced workflows
  • ToolBrain — tool reviews, LLM comparisons, and AI workflow guides
  • CodeIntel Log — code quality, debugging, and software engineering benchmarks

Cross-links automatically generated from NiteAgent.

← Back to all posts