How would you design an MCP server that needs to run both locally (stdio) and remotely (HTTP) for different users?
Quick Answer
Structure the server so the transport is a thin adapter around a shared core: implement your tools/resources/prompts logic once, against the MCP SDK's transport-agnostic server object, and then wire that same object to either a stdio transport (for users running the server as a local subprocess) or a Streamable HTTP transport (for users connecting to a hosted deployment) at startup, typically chosen via a CLI flag or environment variable. The two deployment modes usually differ mainly in cross-cutting concerns layered around that shared core: local/stdio mode can skip authentication (the OS process boundary is the trust boundary) and skip session management (one process per client, by construction), while HTTP mode needs OAuth 2.1-based authorization, per-session state keyed by Mcp-Session-Id, and production concerns like TLS termination, rate limiting, and horizontal scaling across multiple server instances.
Detailed Answer
Most official MCP SDKs are built exactly to support this. The same server object can be handed to different transport implementations without any of your tool/resource code changing.
Shape of a dual-mode server (Python SDK, illustrative)
from mcp.server.fastmcp import FastMCP
import sys
mcp = FastMCP("orders-server")
@mcp.tool()
def get_order(order_id: str) -> str:
return fetch_order(order_id)
# Same tool definitions, different transport at the entry point
if __name__ == "__main__":
if "--http" in sys.argv:
mcp.run(transport="streamable-http", port=8080)
else:
mcp.run(transport="stdio")
The tool handlers, schemas, and business logic never reference the transport at all. That separation is exactly what "transport-agnostic core" means in practice.
What actually differs between the two modes
| Concern | stdio (local) | Streamable HTTP (remote) |
|---|---|---|
| Authentication | Usually none — OS process boundary is the trust line | OAuth 2.1 flow, token validation on every request |
| Session state | Implicit (1 process = 1 client) | Explicit, keyed by Mcp-Session-Id |
| Scaling | N/A — one process per user | Must handle concurrent sessions, possibly across multiple instances |
| Logging | stderr only | Standard structured logging/observability stack |
| Deployment | Shipped as a CLI/package the user runs locally | Deployed as a service (container, serverless) behind a load balancer |
Practical guidance
Keep authorization, session lookup, and rate-limiting as middleware/wrapper layers around the shared tool-handling core, rather than scattering if remote_mode: checks throughout individual tool implementations. This keeps the core testable in isolation — you can unit-test get_order without spinning up either transport. It also means adding a third transport later, if the spec or your needs evolve, doesn't require touching business logic at all.