DEV Community

Jeff
Jeff

Posted on Originally published at powerduck.com

MCP stdio vs Remote Transports: Run It Locally or Host It?

One of the most useful, and most confusing, properties of the Model Context Protocol is that the server code does not care how a client connects to it. A server exposes tools, resources, and prompts; the transport is a thin adapter underneath. Teams get stuck because the two transports in common use have almost nothing in common operationally: stdio is a child process with inherited credentials, Streamable HTTP is a hosted service with OAuth and uptime expectations.

This article is the decision guide I wish existed before we shipped internal MCP servers both ways.

The three transports, briefly

Transport Connection State Typical host
stdio stdin/stdout JSON-RPC, one client per process In-process, dies with the client Developer laptop
Streamable HTTP HTTP POST with optional SSE response stream Server-side, shared Container, VM, serverless
HTTP+SSE (legacy) Separate SSE and POST endpoints Deprecated by the 2025 spec revision Older servers only

New servers should implement stdio for local use and Streamable HTTP for hosted use. The legacy HTTP+SSE transport exists in older tutorials; treat it as a migration target, not a greenfield choice.

stdio: the server is a subprocess

Over stdio, the MCP client launches your server as a child process and speaks JSON-RPC over its standard streams. There is no port, no TLS, and no login prompt:

{
  "mcpServers": {
    "orders": {
      "command": "npx",
      "args": ["-y", "@acme/orders-mcp"],
      "env": {
        "ORDERS_DB_URL": "postgres://localhost:5432/orders",
        "NODE_ENV": "development"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Properties that fall out of this for free:

  • Authentication is ambient. The process inherits the user's shell environment, CLI tokens, and keychain. No OAuth, no client registration.
  • Isolation is per user. Two developers run two processes against two local databases; there is no shared state to corrupt.
  • Secrets never leave the machine. A stdio server reading local files or a local database has no network attack surface beyond what the tools themselves do.
  • Lifecycle is trivial. Closing the client kills the server; there is nothing to deploy or monitor.

The costs are equally direct. Nobody else can use your server. It cannot be called from CI, a browser-based agent, a phone, or a teammate's machine. Long-running work dies when the laptop sleeps. And every user needs the runtime installed (Node, Python, the JVM) unless you ship a binary.

Streamable HTTP: the server is infrastructure

The same tool implementations mounted on the HTTP transport become a hosted service. The client config shrinks to a URL:

{
  "mcpServers": {
    "orders": {
      "url": "https://mcp.example.com/orders/mcp",
      "headers": {}
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The first unauthenticated call returns metadata, the client runs the OAuth 2.1 flow in a browser, and subsequent JSON-RPC requests carry a bearer token. (The full dance is covered in MCP authentication: OAuth 2.1 for remote MCP servers.)

Hosting buys things stdio structurally cannot offer:

  • Shared access. An entire team, CI pipelines, and browser-based agents point at one URL.
  • Centralized data. The server can reach a production database or an internal network the client cannot.
  • Versioning and rollout. Upgrade the tools once; every caller gets the new behavior.
  • Observability. One place for logs, metrics, rate limits, and audit trails.

It also creates the obligations of any service: TLS, token lifetimes, scopes, uptime, capacity planning, and the question of what a tool is allowed to do on behalf of which user.

The same server, both transports

Most TypeScript MCP SDKs let you mount one server twice, which removes the temptation to fork the codebase:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createServer } from "node:http";

const server = new Server(
  { name: "orders", version: "1.4.0" },
  { capabilities: { tools: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, listTools);
server.setRequestHandler(CallToolRequestSchema, callTool);

if (process.env.MCP_TRANSPORT === "http") {
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: randomUUID });
  const http = createServer(async (req, res) => {
    if (req.url?.startsWith("/mcp")) await transport.handleRequest(req, res);
    else res.writeHead(404).end();
  });
  http.listen(8080);
} else {
  await server.connect(new StdioServerTransport());
}
Enter fullscreen mode Exit fullscreen mode

The tool handlers contain no transport-specific code. Auth middleware wraps the HTTP path; the stdio path has none, because it does not need it.

The decision framework

Run through these questions in order; the first "yes" decides it.

  1. Do the tools read local files, git repos, or a local database that exists only on the user's machine? stdio. Hosting would require uploading the data, which is often the exact thing users refuse to do.
  2. Does the caller need to be a teammate, a CI pipeline, a scheduled job, or a browser-based agent? Remote. A subprocess cannot be shared.
  3. Do the tools call systems inside a private network? Remote, hosted inside that network, so laptops never need VPN routes to ten databases.
  4. Is the toolset stable and team-wide, with an owner who can be on call? Remote. If it changes daily and only you use it, stdio.
  5. Are you prototyping? Always stdio first. It is the fastest path to a working tools/list, and the code ports to HTTP later without a rewrite.

A common mature setup runs both: engineers use the stdio server against local checkouts during development, and a hosted build of the same server serves staging data for design review and CI. The OpenAPI spec the tools are generated from is the same in both cases.

State and streaming differences that surprise people

A stdio server can keep everything in memory; requests are serialized over one pipe and the process is single-tenant. Do not carry that assumption into the HTTP transport:

  • The HTTP server is multi-tenant. Per-session state must be keyed by the MCP session ID (and, for security boundaries, by the authenticated user), never by a module-level variable.
  • Long tool calls stream progress over SSE on the HTTP response instead of simply blocking a pipe. Design tools to return a job reference for anything over a few seconds.
  • Clients reconnect. Tools that mutate state must be idempotent, because a retry after a dropped connection is normal, not exceptional.

How this maps to a spec-driven workflow

Generating an MCP server from an OpenAPI document makes the transport question cheaper, because the server is a build artifact rather than hand-maintained glue. In a local-first API workspace you run it over stdio against mocks and local services, with no credentials and no deployment. When the same spec is published to a hosted environment, the build target switches to Streamable HTTP, OAuth goes on at the edge, and the team gets a shared endpoint.

The mechanics of generating that server from a spec are in turning an OpenAPI spec into an MCP server, step by step, and the pattern of serving both humans and agents from one document is described in one spec, two audiences. The local build runs entirely in the browser demo if you want to see the stdio side without installing anything.

Top comments (0)