# 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:

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

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:

``` json
{
  "mcpServers": {
    "orders": {
      "url": "https://mcp.example.com/orders/mcp",
      "headers": {}
    }
  }
}
```

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](https://www.powerduck.com/blog/mcp-authentication-oauth2-remote-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:

``` typescript
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());
}
```

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](https://www.powerduck.com/blog/openapi-to-mcp-server-step-by-step/), and the pattern of serving both humans and agents from one document is described in [one spec, two audiences](https://www.powerduck.com/blog/one-spec-two-audiences-humans-and-ai-agents/). The local build runs entirely in the [browser demo](https://www.powerduck.com/app/?ref=powerduck.com) if you want to see the stdio side without installing anything.

