spanly proxy
Standalone HTTP/SSE reverse proxy in front of an existing MCP server.
spanly proxy is for the cases where you can't wrap the MCP server as
a child process – typically:
- A third-party MCP service you don't control.
- A server already deployed behind k8s, where introducing a wrapping CLI is impractical.
- Network-level interception where the proxy sits between many clients and one server.
The proxy is HTTP-only (no stdio). Inbound traffic on <bind> is
forwarded to <upstream>; every JSON-RPC frame on inspected paths is
captured.
Quickstart
# 1. Set your API keyexport SPANLY_API_KEY="$SPANLY_API_KEY"# 2. Run the standalone proxy in front of an existing MCP server.# upstream -> the MCP server you can't / don't want to wrap# bind -> the address your MCP client should connect to insteadnpx -y @spanly/spanly proxy localhost:3000 localhost:3001Then point your MCP client at the bind address (localhost:3001)
instead of the upstream.
Anatomy
┌─────────────┐ ┌──────────────────┐ ┌────────────────┐
│ MCP client │ ───▶ │ spanly proxy │ ───▶ │ upstream MCP │
└─────────────┘ │ <bind> │ │ <upstream> │
└──────────────────┘ └────────────────┘
│
▼
Spanly ingestThe proxy is transparent at the HTTP layer:
- Status codes, headers, and bodies are passed through unchanged.
- SSE (
text/event-stream) streams flow through with response buffering disabled, so they stay live. - Non-MCP paths (anything not matching
--inspect-prefix, default/mcp,/sse) are forwarded without parsing.
Examples
# Loopback: monitor a server running on localhost:3000
spanly proxy localhost:3000 :3001
# In a Kubernetes Pod: front a sidecar MCP server
spanly proxy mcp:3000 0.0.0.0:3001
# Behind your own ingress / load balancer
spanly proxy upstream.svc.cluster.local:8080 :3001
# Multi-tenant tagging
spanly proxy --context-header=X-Tenant=environmentId \
localhost:3000 :3001SSE pass-through
MCP often uses text/event-stream for streaming notifications. The
proxy:
- Holds the upstream connection open for the duration of the SSE stream.
- Parses each
data:frame as a JSON-RPC packet and emits one telemetry event per frame. - Flushes immediately to the downstream client – there is no buffering between you and the upstream.
If you later put another reverse proxy (nginx, Caddy, Envoy) in front
of spanly proxy, configure it for SSE pass-through. See
Production for sample configs.
Per-request headers
Inbound headers recognized by the proxy:
| Header | Effect |
|---|---|
X-Spanly-Monitor-Id | Override spanlyMonitorId for this request. Useful for joining many requests under one logical session. |
Any header named via --context-header | Maps to the matching context field (environmentId, projectId, organisationId). |
Comparison: run vs proxy
spanly run | spanly proxy | |
|---|---|---|
| stdio support | yes | no |
| HTTP support | yes | yes |
| Wraps child process | yes | no |
| Affects MCP client URL | no | yes (point at bind) |
| Easiest to embed in MCP client config | yes | no |
| Best for third-party / unowned servers | no | yes |
If you can wrap, prefer run. Use proxy when you can't.
Flag reference
See the full flag reference for the complete list of
flags. proxy accepts the same flags as run minus --port and the
--child-* family.