Migrating an MCP Server from Stdio to Remote Hosting one process to one service.
A stdio server lives inside one user's client. A remote Streamable HTTP server serves everyone — and it's the form an MCP server must take before it can charge per call.
Stdio is the right transport to start with: the client spawns your server as a child process, pipes JSON-RPC over stdin/stdout, and everything runs on the user's machine with the user's credentials. It is also a dead end for distribution — every user installs and runs their own copy, and there is no endpoint to meter, secure, or charge.
Migrating to remote hosting means swapping the transport to Streamable HTTP and rethinking three things stdio let you ignore: sessions, secrets, and exposure. Your tool logic — usually the bulk of the code — does not change.
Why migrate at all
Three reasons, in ascending order of consequence. Operations: one deployment you upgrade replaces N installs you can't. Reach: hosted agents and web-based clients can only call servers with a URL — a stdio-only server is invisible to them. Monetization: x402 is an HTTP flow, so a per-call price requires an HTTP endpoint; there is no way to gate a stdin pipe with a 402.
If you intend to ever sell calls, the migration is a prerequisite, not an optimization.
Swap the transport
In the TypeScript SDK, your McpServer and every tool registration stay untouched — you replace the StdioServerTransport with a StreamableHTTPServerTransport mounted on an HTTP route. The minimal stateless version is a few lines of Express:
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { buildServer } from "./mcp.js"; // your existing tools, unchanged
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
const server = buildServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless: fresh transport per request
});
res.on("close", () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(process.env.PORT ?? 3000);Decide: stateless or sessions
Stdio gave you a free assumption — one process, one user, state in module scope. Over HTTP that assumption is gone. The stateless pattern above creates a transport per request and works for most tool servers, since each tool call is self-contained; it also scales horizontally with zero coordination.
If your server genuinely needs continuity — subscriptions, long-running operations, per-client caches — use the SDK's session mode: generate a session ID, keep a transport map keyed by it, and route subsequent requests by the mcp-session-id header. Choose stateless unless you can name the state you need; sessions are where most migration bugs live.
Secrets and filesystem assumptions
A stdio server borrows the user's environment: their API keys, their home directory, their locale. Hosted, those become your problem. Move credentials into your deployment's secret store; anything that read local paths must either be dropped, scoped to explicit configuration, or redesigned around uploads. This audit — grep for env reads and filesystem access — usually takes longer than the transport swap itself.
Exposure changes too. You now need TLS, an auth story for who may call you, and logging. The security and deployment guides linked below cover that layer in depth.
After the migration: the endpoint can earn
With a public /mcp URL, two things become possible that stdio never allowed. First, listing: add the remote URL to your registry manifest and your Loomal listing, and hosted agents can connect directly. Second, pricing: gate the endpoint with x402 so agents pay per call — minimum $0.01 — with the payment verified before your handler runs and settlement in USDC on Base in about two seconds.
Many maintainers keep shipping the stdio package for local, single-user use and run the hosted endpoint alongside it. Same codebase, two transports, and only the hosted one carries a price.
FAQ
Do I have to rewrite my tools to migrate?
No. Tool handlers, schemas, and registrations are transport-agnostic in both the TypeScript and Python SDKs. The work is the HTTP mounting, the sessions decision, and the secrets audit — typically a day or two for a clean stdio server.
Should I drop stdio support after going remote?
Usually not. Local users — especially those whose tools touch their own filesystem — are better served by stdio, and it costs little to keep both entry points in one codebase. The common pattern is free local stdio plus a paid hosted endpoint.
What's the most common migration bug?
State leaking across users. Module-level caches, cwd assumptions, and ambient environment reads were invisible with one user per process and become cross-tenant bugs over HTTP. The stateless per-request pattern eliminates the whole class, which is why it's the right default.
Why is remote hosting required for x402 monetization?
Because x402 lives in the HTTP layer: the server answers an unpaid request with status 402 and a price, then verifies the signed USDC payment before executing. Stdio has no request/response boundary to gate, so there is nowhere for the payment handshake to happen.
Already hosted?
Browse remote servers on the Loomal Index to see live endpoints and pricing patterns.