MCP Protocol Architecture explains the two transport mechanisms conceptually. This note covers the practical configuration — how to set up stdio for development and streamable HTTP for production, and when to switch between them.
stdio for Local Development
Standard I/O transport runs the server as a subprocess. The host application spawns the server process, and all communication happens through stdin/stdout pipes.
Server-side, the configuration is one line:
if __name__ == "__main__": mcp.run(transport="stdio")Client-side for Claude Desktop (claude_desktop_config.json):
{ "mcpServers": { "my-data-server": { "command": "uv", "args": ["run", "/path/to/server.py"], "env": { "DATABASE_URL": "postgresql://localhost/analytics" } } }}Client-side for Claude Code:
claude mcp add my-data-server -- uv run /path/to/server.pyWhy stdio Is the Default
stdio gives you several things for free:
- Zero network configuration. No ports to open, no TLS certificates, no DNS resolution.
- Credential isolation. Credentials pass through environment variables, never touching the network. They stay in the process environment.
- Process isolation. Each client gets its own server instance. One client’s crash doesn’t affect another’s.
- Simple debugging. Server logs go to stderr, which the client can capture and display. No log aggregation needed.
For individual development and internal tools used by one person at a time, stdio is often all you need. Many data engineering teams never move past it.
Streamable HTTP for Production
When you need a server that multiple clients share — across a team, across environments, or deployed to cloud infrastructure — HTTP transport is the right choice.
Server-side:
mcp = FastMCP("RemoteDataServer", stateless_http=True)
if __name__ == "__main__": mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)Client-side configuration:
{ "mcpServers": { "remote-data-server": { "type": "http", "url": "https://mcp.yourcompany.com", "headers": { "Authorization": "Bearer ${MCP_API_KEY}" } } }}What HTTP Adds
HTTP transport requires more infrastructure but enables:
- Multi-client access. One server instance serves multiple users. Useful for shared team servers or centralized data access.
- Authentication. OAuth 2.1 for production deployments. The Security Posture for AI Agents note covers the security principles; HTTP transport is where you implement them.
- Load balancing. Deploy behind standard load balancers for high availability.
- Server-Sent Events. Streaming responses for long-running operations. The client receives incremental updates as the server processes a request.
- Centralized deployment. The server runs in your cloud environment, close to the data, rather than on each developer’s machine.
The stateless_http Flag
The stateless_http=True parameter in the FastMCP constructor tells the server not to maintain session state between requests. This is important for production deployments behind load balancers — any request can go to any server instance. If your server needs to track state across calls (rare for data engineering use cases), omit this flag and ensure sticky sessions in your load balancer.
When to Switch
The progression for most teams:
-
Start with stdio. Build the server, test with MCP Inspector, connect Claude Desktop or Claude Code. This covers individual use.
-
Stay with stdio if the server is used by one person at a time, the data sources are accessible from the developer’s machine, and there’s no compliance requirement for centralized access logging.
-
Move to HTTP when you need multiple people accessing the same server, the data sources are only reachable from a cloud environment, security requires centralized authentication and audit logging, or you want the server running 24/7 without a developer’s laptop being open.
The same Python or TypeScript code works with both transports. The only change is the mcp.run() call and client configuration. You don’t rewrite tools, resources, or prompts when switching transport.
Environment Variable Patterns
Both transports use environment variables for credentials, but the patterns differ:
stdio: Set env vars in the client configuration. Each client instance gets its own environment. Simple and isolated.
{ "env": { "DATABASE_URL": "postgresql://localhost/analytics", "API_KEY": "secret" }}HTTP: Set env vars on the server host (container env, systemd environment file, cloud provider secrets manager). All clients share the same server environment.
# In your deployment configurationDATABASE_URL=postgresql://prod-host/analyticsAPI_KEY=secretFor HTTP deployments, prefer a secrets manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) over plaintext environment variables. The server reads credentials at startup, so the secrets manager only needs to be accessible during initialization.