mcp-streamablehttp-proxy#
The divine bridge that wraps stdio-based MCP servers with StreamableHTTP endpoints, enabling web-based access to official MCP servers.
Quick Start#
Key Features:
Universal support for any stdio-based MCP server
No server modifications required
Session management with isolated subprocesses
Full MCP protocol support with JSON-RPC 2.0
Production-ready with health monitoring
Installation:
# Via pip
pip install mcp-streamablehttp-proxy
# Via pixi
pixi add --pypi mcp-streamablehttp-proxy
Basic Usage:
# Wrap a Python MCP server module
mcp-streamablehttp-proxy python -m mcp_server_fetch
# Wrap an executable MCP server
mcp-streamablehttp-proxy /usr/local/bin/mcp-server-filesystem --root /data
# Wrap an NPM-based MCP server
mcp-streamablehttp-proxy npx @modelcontextprotocol/server-memory
Table of Contents#
Overview#
mcp-streamablehttp-proxy
is a universal bridge that enables any stdio-based MCP (Model Context Protocol) server to be accessed via HTTP endpoints. This proxy is essential for integrating traditional MCP servers with web infrastructure, API gateways, and OAuth authentication systems. It manages sessions, spawns server subprocesses, and handles protocol translation transparently.
The proxy acts as a protocol translator, wrapping existing stdio MCP servers (like the official ones from modelcontextprotocol/servers) and exposing them via HTTP endpoints. This enables:
Web access to stdio-only MCP servers
OAuth authentication integration via gateway
Session management for stateful servers
Health monitoring for container orchestration
No modifications to existing MCP servers
Architecture#
Core Purpose#
┌──────────────┐ stdio ┌─────────────────────┐ HTTP ┌──────────────┐
│ MCP Server │ ←--------→ │ StreamableHTTP │ ←--------→ │ MCP Client │
│ (stdio) │ JSON-RPC │ Proxy │ /mcp │ (HTTP) │
└──────────────┘ └─────────────────────┘ └──────────────┘
Key Components#
mcp_streamablehttp_proxy/
├── __init__.py # Package initialization
├── __main__.py # Module entry point
├── cli.py # Command-line interface
├── server.py # Server runner
├── proxy.py # Main proxy implementation
├── session.py # Session management
└── models.py # Data models
Core Classes#
# Core components
StreamableHTTPProxy # Main FastAPI application
SessionManager # Client session tracking
ProcessManager # Subprocess lifecycle
StdioTransport # stdio communication
MessageRouter # Request/response routing
Installation#
Via pip#
pip install mcp-streamablehttp-proxy
Via pixi#
pixi add --pypi mcp-streamablehttp-proxy
Docker#
FROM python:3.11-slim
# Install proxy and desired MCP server
RUN pip install mcp-streamablehttp-proxy mcp-server-fetch
# Create non-root user
RUN useradd -m -s /bin/bash mcp
USER mcp
# Expose port
EXPOSE 3000
# Bind to 0.0.0.0 for container networking
CMD ["mcp-streamablehttp-proxy", "--host", "0.0.0.0", "python", "-m", "mcp_server_fetch"]
Usage#
Basic Usage#
Python MCP Server Module:
mcp-streamablehttp-proxy python -m mcp_server_fetch
Executable MCP Server:
mcp-streamablehttp-proxy /usr/local/bin/mcp-server-filesystem --root /data
NPM-based MCP Server:
mcp-streamablehttp-proxy npx @modelcontextprotocol/server-memory
Wrapping Official MCP Servers#
Fetch Server#
FROM python:3.12-slim
RUN pip install mcp-streamablehttp-proxy mcp-server-fetch
CMD ["mcp-streamablehttp-proxy", "--host", "0.0.0.0", "python", "-m", "mcp_server_fetch"]
Filesystem Server#
FROM python:3.12-slim
RUN pip install mcp-streamablehttp-proxy mcp-server-filesystem
CMD ["mcp-streamablehttp-proxy", "--host", "0.0.0.0", "mcp-server-filesystem", "--root", "/data", "--read-only"]
Time Server#
FROM python:3.12-slim
RUN pip install mcp-streamablehttp-proxy mcp-server-time
CMD ["mcp-streamablehttp-proxy", "--host", "0.0.0.0", "python", "-m", "mcp_server_time"]
Command Line Options#
mcp-streamablehttp-proxy [OPTIONS] <server_command> [server_args...]
Options:
--host TEXT Host to bind to (default: 127.0.0.1)
--port INTEGER Port to bind to (default: 3000)
--timeout INTEGER Session timeout in seconds (default: 300)
--log-level TEXT Log level: debug/info/warning/error (default: info)
--help Show this message and exit
Environment Variables#
| Variable | Description | Default |
|———-|————-|———||
| MCP_BIND_HOST
| Override bind host | 127.0.0.1 |
| MCP_PORT
| Override port | 3000 |
| MCP_SESSION_TIMEOUT
| Session timeout in seconds | 300 |
| MCP_LOG_LEVEL
| Logging level | INFO |
| LOG_FILE
| Enable file logging | None |
| MCP_SUBPROCESS_TIMEOUT
| Subprocess request timeout | 30 |
| MCP_STARTUP_TIMEOUT
| Startup timeout | 10 |
| MCP_MAX_SESSIONS
| Maximum concurrent sessions | 100 |
| MCP_READ_BUFFER_SIZE
| Read buffer size | 65536 |
| MCP_WRITE_BUFFER_SIZE
| Write buffer size | 65536 |
How It Works#
Request Flow#
HTTP Request: Client sends POST to
/mcp
Session Check:
First request: Create session, spawn subprocess
Subsequent: Route to existing session
Protocol Translation: HTTP JSON → stdio JSON-RPC
Server Processing: MCP server processes request
Response Translation: stdio JSON-RPC → HTTP JSON
Session Header: Response includes
Mcp-Session-Id
Session Lifecycle#
graph LR
A[Client Request] --> B{Session Exists?}
B -->|No| C[Create Session]
B -->|Yes| D[Route to Session]
C --> E[Spawn Subprocess]
E --> F[Initialize MCP]
F --> G[Process Request]
D --> G
G --> H[Return Response]
H --> I{Timeout?}
I -->|Yes| J[Cleanup Session]
I -->|No| K[Keep Alive]
API Reference#
HTTP Endpoints#
POST /mcp#
Single endpoint for all MCP protocol messages.
Headers:
Content-Type: application/json
(required)Mcp-Session-Id: <uuid>
(required after initialization)
Request Body: Standard JSON-RPC 2.0 message:
{
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "example-client",
"version": "1.0.0"
}
},
"id": 1
}
Response:
{
"jsonrpc": "2.0",
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
"resources": {}
},
"serverInfo": {
"name": "example-server",
"version": "1.0.0"
}
},
"id": 1
}
Response Headers:
Mcp-Session-Id: <uuid>
(on initialization)
Response (SSE stream alternative):
event: message
data: {"jsonrpc": "2.0", "result": {"protocolVersion": "2025-06-18", ...}, "id": 1}
event: done
data: {"status": "complete"}
GET /health#
Health check endpoint.
GET /health HTTP/1.1
Response:
{
"status": "healthy",
"subprocess": "running",
"sessions": 5,
"uptime": 3600
}
Error Responses#
Session Not Found:
{
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": "Session not found"
},
"id": 1
}
Request Timeout:
{
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": "Request timeout"
},
"id": 1
}
Session Management#
Session Data Structure#
{
"session_id": "uuid-v4",
"created_at": "2024-01-01T00:00:00Z",
"last_activity": "2024-01-01T00:01:00Z",
"process": subprocess.Popen,
"stdin": StreamWriter,
"stdout": StreamReader,
"stderr": StreamReader,
"pending_requests": {},
"initialized": False
}
Session Creation#
When a client first connects without a session ID:
# Proxy creates new session
session_id = generate_session_id()
subprocess = spawn_mcp_server()
sessions[session_id] = {
"process": subprocess,
"created": time.time(),
"last_activity": time.time()
}
Session Usage#
Subsequent requests include session ID:
POST /mcp
Mcp-Session-Id: session-123
Session Cleanup#
Sessions are cleaned up when:
Timeout: No activity for session_timeout seconds
Process Exit: MCP server terminates
Explicit Close: Client sends close notification
Error: Unrecoverable error occurs
Proxy Shutdown: Graceful termination
Resource Management#
One subprocess per session
Automatic process termination on cleanup
Bounded request queue per session
Memory-efficient streaming I/O
Subprocess Management#
Starting Subprocesses#
# Proxy spawns MCP server
process = subprocess.Popen(
[mcp_command] + mcp_args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=False # Binary mode for proper encoding
)
Health Monitoring#
The proxy monitors subprocess health:
Checks process is alive
Monitors stdio responsiveness
Restarts on crash (configurable)
Reports health via
/health
Graceful Shutdown#
On proxy shutdown:
Send shutdown to all subprocesses
Wait for graceful termination
Force kill if timeout exceeded
Clean up resources
Error Handling#
Common Errors and Solutions#
Error |
Cause |
Solution |
---|---|---|
Session not found |
Invalid/expired session |
Initialize new session |
Request timeout |
Server took >30s |
Check server health |
Subprocess died |
Server crashed |
Check server logs |
Invalid request |
Malformed JSON-RPC |
Verify request format |
Port already in use |
Another process on port |
Change port with –port |
Process crash |
Server died |
Restart or fail session |
Startup timeout |
Server slow/hung |
Kill and retry |
Communication error |
Broken pipe |
Terminate session |
Protocol Errors#
Error |
Response |
---|---|
Invalid JSON-RPC |
400 Bad Request |
Unknown session |
404 Not Found |
Server error |
500 with error details |
Docker Deployment#
Basic Dockerfile#
FROM python:3.11-slim
# Install proxy and MCP server
RUN pip install mcp-streamablehttp-proxy mcp-server-fetch
# Create non-root user
RUN useradd -m -s /bin/bash mcp
USER mcp
# Expose port
EXPOSE 3000
# Run proxy
CMD ["mcp-streamablehttp-proxy", "--host", "0.0.0.0", "--port", "3000", "python", "-m", "mcp_server_fetch"]
Docker Compose with Traefik#
services:
mcp-fetch:
build: ./mcp-streamablehttp-proxy
environment:
- MCP_BIND_HOST=0.0.0.0
- MCP_PORT=3000
- MCP_LOG_LEVEL=INFO
networks:
- internal
labels:
- "traefik.enable=true"
- "traefik.http.routers.mcp-fetch.rule=Host(`mcp-fetch.example.com`)"
- "traefik.http.services.mcp-fetch.loadbalancer.server.port=3000"
- "traefik.http.routers.mcp-fetch.middlewares=mcp-auth"
- "traefik.http.middlewares.mcp-auth.forwardauth.address=http://auth:8000/verify"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/mcp", "-X", "POST",
"-H", "Content-Type: application/json",
"-d", '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"healthcheck","version":"1.0"}},"id":1}']
interval: 30s
timeout: 5s
retries: 3
Multiple MCP Services#
services:
mcp-fetch:
build: .
command: ["mcp-streamablehttp-proxy", "python", "-m", "mcp_server_fetch"]
mcp-filesystem:
build: .
command: ["mcp-streamablehttp-proxy", "mcp-server-filesystem", "--root", "/data"]
mcp-memory:
build: .
command: ["mcp-streamablehttp-proxy", "npx", "@modelcontextprotocol/server-memory"]
Performance Tuning#
Connection Pooling#
# Reuse sessions where possible
connection_pool = {
"max_idle": 10,
"idle_timeout": 300
}
Buffer Sizes#
# Tune for large responses
MCP_READ_BUFFER_SIZE=65536
MCP_WRITE_BUFFER_SIZE=65536
Concurrent Sessions#
# Limit concurrent sessions
MCP_MAX_SESSIONS=100
MCP_SESSION_TIMEOUT=3600
Resource Usage#
Memory: ~10MB base + subprocess memory
CPU: Minimal overhead (I/O bound)
Connections: One subprocess per active session
File Descriptors: 3 per session (stdin/stdout/stderr)
Scaling Recommendations#
Horizontal Scaling: Run multiple proxy instances
Load Balancing: Use sticky sessions (session affinity)
Resource Limits: Set container memory/CPU limits
Session Timeout: Adjust based on usage patterns
Connection Pooling: Not applicable (one process per session)
Optimization Tips#
Use shorter session timeouts for high-traffic scenarios
Monitor subprocess resource usage
Implement health checks to detect unhealthy instances
Use process managers for automatic restart
Security Considerations#
Important Notes#
⚠️ This proxy provides NO authentication or authorization!
Always deploy behind an authenticating reverse proxy with:
OAuth2/JWT authentication
Rate limiting
Access control lists
HTTPS termination
Request validation
Security Best Practices#
Process Isolation: Each session gets its own subprocess
Resource Limits: Configure max sessions and timeouts
Input Validation: Validate all JSON-RPC messages
Subprocess Sandboxing: Use containers for isolation
Authentication: Handled by gateway layer (not proxy)
Network Isolation: Don’t expose directly to internet
Minimal Privileges: Run with minimal user permissions
Audit Logging: Log all session creation
Example Secure Deployment#
services:
traefik:
image: traefik:v3.0
command:
- "--providers.docker=true"
- "--entrypoints.websecure.address=:443"
ports:
- "443:443"
auth:
image: mcp-oauth-dynamicclient
environment:
- JWT_SECRET=${JWT_SECRET}
mcp-proxy:
image: mcp-streamablehttp-proxy
labels:
- "traefik.http.routers.mcp.middlewares=auth-forward"
- "traefik.http.middlewares.auth-forward.forwardauth.address=http://auth:8000/verify"
Monitoring and Health Checks#
Health Check Implementation#
async def health_check():
"""Check if proxy can spawn and communicate with MCP server."""
try:
# Create test session
session_id = await create_session()
# Send initialize request
response = await send_request(session_id, {
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "health", "version": "1.0"}
},
"id": 1
})
# Cleanup
await cleanup_session(session_id)
return response.get("result") is not None
except Exception:
return False
Metrics to Monitor#
Active session count
Session creation rate
Request latency
Error rate
Subprocess resource usage
Session duration
Subprocess CPU/memory usage
Logging#
# Structured logging
logger.info("session_created", extra={
"session_id": session_id,
"client_ip": client_ip,
"mcp_server": mcp_command
})
Troubleshooting#
Proxy Won’t Start#
Check Port Availability:
lsof -i :3000
Verify MCP Server:
# Test server directly
echo '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}' | python -m mcp_server_fetch
Check Permissions:
# Ensure executable permissions
which mcp-streamablehttp-proxy
ls -la $(which mcp-streamablehttp-proxy)
Sessions Immediately Timeout#
Increase Timeout:
mcp-streamablehttp-proxy --timeout 600 <server_command>
Check Server Startup Time:
Some servers take time to initialize
Monitor logs for startup messages
Verify JSON Output:
Server must output line-delimited JSON
No extra output to stdout
Requests Hang#
Enable Debug Logging:
mcp-streamablehttp-proxy --log-level debug <server_command>
Check Process State:
ps aux | grep mcp
Monitor Network:
netstat -an | grep 3000
Common Debug Checks#
Verify MCP server works standalone
Check subprocess output in logs
Monitor session creation/cleanup
Validate JSON-RPC format
Integration Patterns#
With MCP OAuth Gateway#
The proxy is designed for seamless integration:
Proxy exposes MCP servers as HTTP
Traefik provides routing and auth middleware
OAuth Gateway handles authentication
Clients access with Bearer tokens
With Traefik#
labels:
- "traefik.enable=true"
- "traefik.http.routers.service.rule=Host(`service.domain`)"
- "traefik.http.services.service.loadbalancer.server.port=3000"
With Logging#
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Best Practices#
One Proxy per Server Type: Don’t mix different MCP servers
Resource Limits: Set memory/CPU limits in production
Health Checks: Implement health endpoints
Logging: Centralize logs for debugging
Security: Always use authentication middleware
Updates: Keep both proxy and MCP servers updated
Session Management: Configure appropriate timeouts
Error Handling: Monitor and alert on errors
Advanced Configuration#
Custom Session Manager#
from mcp_streamablehttp_proxy import SessionManager
class CustomSessionManager(SessionManager):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.max_sessions = 100
async def create_session(self):
if len(self.sessions) >= self.max_sessions:
raise Exception("Max sessions reached")
return await super().create_session()
Request Interceptor#
async def request_interceptor(session_id: str, request: dict):
"""Modify requests before sending to MCP server."""
# Add custom headers
if request.get("method") == "tools/call":
request["params"]["custom"] = "value"
return request
Testing with curl#
Initialize Session:
# Start the proxy
mcp-streamablehttp-proxy python -m mcp_server_fetch
# Initialize and capture session ID
SESSION_ID=$(curl -s -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' \
-i | grep -i mcp-session-id | cut -d' ' -f2 | tr -d '\r')
echo "Session ID: $SESSION_ID"
List Tools:
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: $SESSION_ID" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":2}'
Call Tool:
curl -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: $SESSION_ID" \
-d '{
"jsonrpc":"2.0",
"method":"tools/call",
"params":{
"name":"fetch",
"arguments":{"url":"https://example.com"}
},
"id":3
}'
Limitations#
One Process per Session: Resource intensive for many sessions
No Request Queuing: Parallel requests within session not queued
30s Request Timeout: Not configurable per-request
stdio Only: Doesn’t support other MCP transports
No Built-in Auth: Requires external authentication
Session Affinity Required: Load balancers must use sticky sessions
No Session Persistence: Sessions lost on restart