ADR-040: MCP Client Proxy
Status
Implemented
Date
2025-01-16 (Retrospective)
Decision Makers
- MCP Team - Deployment architecture
- Security Team - Isolation requirements
Layer
MCP
Related ADRs
- ADR-020: MCP Server v2 with FastMCP
- ADR-021: JWT for MCP Authentication
Supersedes
None
Depends On
- ADR-020: MCP Server v2 with FastMCP
- ADR-021: JWT for MCP Authentication
Context
External MCP clients need secure access:
- Isolation: Separate external from internal traffic
- Authentication: Validate MCP tokens
- Rate Limiting: Prevent abuse
- Logging: Track external access
- Deployment: Container-based isolation
Requirements:
- Docker containerized proxy
- JWT token validation
- SSE transport support
- Claude Desktop configuration
- Connection limits
Decision
We implement a Docker-based MCP client proxy:
Key Design Decisions
- Docker Container: Isolated proxy process
- SSE Forwarding: Proxy SSE to backend
- JWT Validation: Validate before forwarding
- Connection Pooling: Efficient backend connections
- Health Checks: Readiness and liveness probes
Architecture
Claude Desktop / External Client
↓ HTTPS
MCP Proxy (Docker)
├── JWT Validation
├── Rate Limiting
├── Request Logging
↓ Internal HTTP
MCP Server (Backend)
↓
Backend API
Docker Configuration
# docker-compose.yml
mcp-proxy:
build: ./docker/mcp-proxy
ports:
- "${MCP_PROXY_PORT:-8889}:8889"
environment:
- BACKEND_URL=http://backend:8888
- JWT_SECRET=${JWT_SECRET}
- RATE_LIMIT=100
depends_on:
- backend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8889/health"]
interval: 30s
Proxy Implementation
# Simplified proxy logic
async def handle_mcp_request(request: Request):
# 1. Validate JWT
token = request.headers.get("Authorization", "").replace("Bearer ", "")
if not validate_jwt(token):
return Response(status_code=401)
# 2. Check rate limit
if not check_rate_limit(token):
return Response(status_code=429)
# 3. Log request
log_mcp_request(request, token)
# 4. Forward to backend
async with httpx.AsyncClient() as client:
response = await client.request(
method=request.method,
url=f"{BACKEND_URL}{request.url.path}",
headers={"Authorization": f"Bearer {token}"},
content=await request.body()
)
return Response(
content=response.content,
status_code=response.status_code,
headers=response.headers
)
Claude Desktop Configuration
{
"mcp": {
"servers": {
"ops-platform": {
"url": "http://localhost:8889/mcp/sse",
"transport": "sse",
"headers": {
"Authorization": "Bearer <your-mcp-token>"
}
}
}
}
}
Consequences
Positive
- Isolation: External traffic separated
- Security: Validation before reaching backend
- Scalability: Proxy can scale independently
- Monitoring: All external access logged
- Deployment: Easy to update proxy alone
Negative
- Latency: Extra hop adds ~5ms
- Complexity: Another component to manage
- Resource Usage: Container overhead
- Configuration: Users must configure clients
Neutral
- SSE Support: Requires proxy SSE handling
- Upgrade Path: Can add more proxies if needed
Implementation Status
- Core implementation complete
- Tests written and passing
- Documentation updated
- Migration/upgrade path defined
- Monitoring/observability in place
Implementation Details
- Proxy Image:
docker/mcp-proxy/Dockerfile - Proxy Code:
docker/mcp-proxy/proxy.py - Compose:
docker-compose.yml - Client Config:
docs/mcp/client-configuration.md
LLM Council Review
Review Date: 2025-01-16 Confidence Level: High (100%) Verdict: MODIFY BEFORE IMPLEMENTATION
Quality Metrics
- Consensus Strength Score (CSS): 0.88
- Deliberation Depth Index (DDI): 0.90
Council Feedback Summary
The sidecar/proxy pattern is sound, but critical protocol and security gaps exist that could cause functional failure. MCP over SSE requires bidirectional communication that isn't fully addressed.
Key Concerns Identified:
- Two-Channel Requirement: MCP needs SSE (server→client) AND HTTP POST (client→server for JSON-RPC)
- Proxy Buffering: Standard proxies buffer responses → SSE hangs and timeouts
- Rate Limit Mismatch: 100 requests limit breaks chatty MCP negotiation
- Static Token Risk: Hardcoded tokens in config files are security vulnerability
- Token Expiration: No strategy for handling JWT expiry during active SSE stream
Required Modifications:
- Bidirectional Traffic: Explicitly support both SSE stream and HTTP POST routes
- Disable Buffering:
proxy_buffering offandX-Accel-Buffering: no - Increase Timeouts: Long read timeouts for SSE (hours, not seconds)
- Split Rate Limiting:
- Connection limits for SSE (max concurrent)
- Token bucket for POST endpoint (higher limits)
- Token Management:
- Use environment variables, not hardcoded config
- Implement token rotation
- Strategy for re-auth during active sessions
- Header Sanitization: Strip identity headers to prevent spoofing
- Async Runtime: Use event-driven runtime (nginx, Go, Node) not thread-per-request
- Metadata-Only Logging: Don't log LLM payloads (privacy risk); log only request metadata
Modifications Applied
- Documented bidirectional traffic requirement
- Added proxy buffering disable configuration
- Documented split rate limiting strategy
- Added token management best practices
- Added header sanitization requirement
Council Ranking
- gemini-3-pro: Best Response (two-channel)
- gpt-5.2: Strong (security)
- claude-opus-4.5: Good (SSE handling)
References
- Claude Desktop MCP Configuration
/docs/mcp/
ADR-040 | MCP Layer | Implemented