mcpstead
MCP Gateway that aggregates multiple upstream MCP servers into a single endpoint with persistent connections, tool registry, and authentication.
README
mcpstead
MCP Gateway
One downstream /mcp endpoint that fronts many upstream MCP servers. Persistent upstream connections with reconnect, tool registry with qualified names, JSON or SSE responses based on client Accept, per-upstream auth, Prometheus metrics.
Install
# npm (macOS, Linux, WSL)
npm i -g @ahkohd/mcpstead
# homebrew (macOS, Linux)
brew install ahkohd/tap/mcpstead
# cargo
cargo install mcpstead --locked --force
# verify
mcpstead --version
Quick start
# 1. write a config
mkdir -p ~/.config/mcpstead
cat > ~/.config/mcpstead/config.yaml <<'EOF'
host: 0.0.0.0
port: 8766
mcp:
auth:
mode: none
servers:
- name: example
url: http://127.0.0.1:3000/mcp
protocol: streamable
auth: none
EOF
# 2. run
mcpstead --config ~/.config/mcpstead/config.yaml
Then point any MCP client at http://127.0.0.1:8766/mcp.
Docker
docker build -t mcpstead .
docker run --rm \
-p 8766:8766 \
-v "$PWD/config:/etc/mcpstead:ro" \
mcpstead
The Dockerfile installs from crates.io.
HTTP API
| Method | Path | Purpose |
|---|---|---|
POST |
/mcp |
MCP over HTTP JSON-RPC |
GET |
/mcp |
returns 405 |
DELETE |
/mcp |
terminate downstream session |
GET |
/health |
upstream status, tool counts, last-seen, reconnects |
GET |
/metrics |
Prometheus text format |
POST |
/-/reload |
reload config without restart |
MCP client setup
Local, no-auth:
mcpstead:
url: http://127.0.0.1:8766/mcp
tools:
resources: false
prompts: false
Bearer auth:
mcpstead:
url: http://127.0.0.1:8766/mcp
headers:
Authorization: Bearer ${MCPSTEAD_BEARER_TOKEN}
tools:
resources: false
prompts: false
Tools appear with qualified names - <server>__<tool> - so multiple upstreams can ship overlapping tool names without collision.
Auth
Downstream (clients to mcpstead)
Default is no auth:
mcp:
auth:
mode: none
Bearer auth:
mcp:
auth:
mode: bearer
Token comes from env:
export MCPSTEAD_BEARER_TOKEN='replace-with-strong-secret'
mcpstead --config ~/.config/mcpstead/config.yaml
Clients send Authorization: Bearer <token>. Missing or wrong token returns 401. mcp.auth.bearer_token in config is rejected at startup.
/health and /metrics stay open regardless of auth mode - for monitoring without exposing the token.
Upstream (mcpstead to MCP servers)
Per-server in the servers list. Three modes:
servers:
- name: local
url: http://127.0.0.1:3000/mcp
auth: none
- name: workflow
url: https://workflow.example/mcp-server/http
auth:
type: bearer
token_env: WORKFLOW_TOKEN
- name: custom
url: https://api.example/mcp
headers:
X-API-Key: '${EXAMPLE_KEY}'
token_env resolves the named env var at startup and on config reload.
Config
Set the config path with --config <path> or the MCPSTEAD_CONFIG env var.
host: 0.0.0.0
port: 8766
mcp:
auth:
mode: none # none | bearer
session:
idle_ttl_seconds: 3600
gc_interval_seconds: 60
shutdown_grace_seconds: 5
servers:
- name: local
url: http://127.0.0.1:3000/mcp
protocol: streamable # streamable | sse | auto
required: false # if true, gateway won't start without this upstream
auth: none
reconnect:
max_attempts: 0 # 0 = infinite
backoff_base_ms: 1000
backoff_max_ms: 30000
tools:
ttl_seconds: 300
tls_skip_verify: false
quirks:
normalize_sse_events: true
inject_accept_header: 'application/json, text/event-stream'
metrics:
enabled: true
logging:
level: info
Hot reload
mcpstead reloads its config without restart on:
- SIGHUP (
systemctl reload mcpsteadorkill -HUP <pid>) POST /-/reload(gated by bearer auth in bearer mode)
Hot-reloadable:
- upstream list
- per-upstream auth, headers, quirks, reconnect, tools, URL, protocol, and TLS settings
mcp.auth.modeandMCPSTEAD_BEARER_TOKENmetrics.enabled
Restart required:
hostportlogging.levelmcp.session.*
Reload is best-effort. Bad config is rejected and ignored; the running config stays in place. Check logs and mcpstead_config_reloads_total{result="error"} for failures.
MCP session config keys
mcp.session.idle_ttl_seconds- evict inactive sessions after this many seconds (default3600)mcp.session.gc_interval_seconds- idle-session GC wake interval (default60)mcp.session.shutdown_grace_seconds- max shutdown accounting sweep time (default5)
Per-upstream config keys
name- required, used as tool prefixurl- required, MCP endpointprotocol-streamable | sse | auto(defaultauto)required- block startup if upstream fails to initialize (defaultfalse)auth-none,bearer(withtoken_env), orheadersmapreconnect.max_attempts-0= infinite (default)reconnect.backoff_base_ms/backoff_max_ms- exponential backoff boundstools.ttl_seconds- refresh cachedtools/listafter this intervaltls_skip_verify- disable TLS cert checks for this upstream (defaultfalse, use only for trusted local nets)quirks.normalize_sse_events- stripevent:lines from upstream SSE responsesquirks.inject_accept_header- override the Accept header sent upstream
Observability
Metrics
/metrics exposes Prometheus-format counters, gauges, and histograms. Label cardinality assumes a small bounded set of upstreams and tools; tool-call series are keyed by (server, tool).
mcpstead_build_info{version="...",rust_version="...",git_sha="..."}
mcpstead_start_time_seconds
mcpstead_uptime_seconds
mcpstead_process_resident_memory_bytes
mcpstead_process_virtual_memory_bytes
mcpstead_process_cpu_seconds_total
mcpstead_process_open_fds
mcpstead_process_max_fds
mcpstead_process_threads
mcpstead_upstream_connected{server="..."}
mcpstead_upstream_tools_count{server="..."}
mcpstead_upstream_reconnects_total{server="..."}
mcpstead_upstream_last_seen_seconds{server="..."}
mcpstead_upstream_initialize_total{server="...",result="success|error"}
mcpstead_upstream_initialize_duration_seconds_bucket{server="...",le="..."}
mcpstead_upstream_health_checks_total{server="...",result="success|failure"}
mcpstead_upstream_reconnect_attempts_total{server="...",result="success|error"}
mcpstead_upstream_backoff_seconds_total{server="..."}
mcpstead_upstream_in_backoff{server="..."}
mcpstead_upstream_current_backoff_seconds{server="..."}
mcpstead_upstream_session_resets_total{server="...",reason="unknown_session|expired|terminated"}
mcpstead_upstream_tools_refresh_total{server="...",result="success|error"}
mcpstead_upstream_tools_refresh_duration_seconds_bucket{server="...",le="..."}
mcpstead_upstream_tools_last_refresh_timestamp_seconds{server="..."}
mcpstead_upstream_bytes_total{server="...",direction="sent|received"}
mcpstead_downstream_sessions_active
mcpstead_downstream_sessions_total
mcpstead_downstream_session_duration_seconds_bucket{le="..."}
mcpstead_downstream_session_terminations_total{reason="..."}
mcpstead_mcp_requests_total{method="...",result="success|error"}
mcpstead_mcp_request_duration_seconds_bucket{method="...",le="..."}
mcpstead_mcp_auth_attempts_total{result="success|failure"}
mcpstead_mcp_auth_failures_total{reason="..."}
mcpstead_config_reloads_total{result="success|error"}
mcpstead_config_last_reload_timestamp_seconds
mcpstead_tool_calls_total{server="...",tool="..."}
mcpstead_tool_call_errors_total{server="...",tool="...",reason="..."}
mcpstead_tool_call_duration_seconds_bucket{server="...",tool="...",le="..."}
Scrape config
- job_name: mcpstead
metrics_path: /metrics
static_configs:
- targets: ['mcpstead:8766']
Health check
curl http://127.0.0.1:8766/health
Returns JSON: per-upstream connection state, tool counts, last successful contact, reconnect counts, last error.
Troubleshooting
- All tools list empty - at least one upstream failed to
initialize. Check/healthfor per-server status; check upstream URL, auth, and reachability. - Sporadic
SSE parse failed- upstream sends an SSE dialect mcpstead doesn't recognize. Tryquirks.normalize_sse_events: truefor that server, or setquirks.inject_accept_header: 'application/json'to force JSON. tools/callreturns auth error - upstream rejected the bearer token. Confirmtoken_envresolves to the right value at startup; check upstream's expected header name.- TLS handshake errors against a self-signed upstream - set
tls_skip_verify: trueon that server. Only safe on trusted local networks. - 401 from
/mcpwith mode bearer - client missing or sending wrongAuthorization: Bearer <token>. VerifyMCPSTEAD_BEARER_TOKENmatches what the client sends. - Upstream goes red in
/healthrepeatedly - checkmcpstead_upstream_reconnects_totalandmcpstead_upstream_last_seen_seconds. Tunereconnect.backoff_max_msif the upstream needs longer recovery.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.