Transparent MCP stdio-to-HTTP proxy and HTTP tunnel.
ferry can run in two modes:
- client mode, where it runs as a local stdio MCP server and forwards JSON-RPC traffic to a remote MCP server over HTTP
- serve mode, where it runs a local stdio MCP server or relays to another HTTP MCP server and exposes Streamable HTTP plus legacy SSE downstream
It prefers Streamable HTTP, can fall back to legacy SSE, and supports optional SOCKS5 proxying.
autotransport mode by default, probing Streamable HTTP first and falling back to legacy SSE- Streamable HTTP and legacy SSE support
servemode withstdioandrelaybackends- default downstream surfaces for
/mcp,/sse,/messages, and/health MCP-Session-IdandMCP-Protocol-Versionhandling for modern HTTP servers- Best-effort SSE resume support via
Last-Event-ID - Config from CLI, environment, and TOML with precedence: CLI > env > config > defaults
rustlsTLS and optionalsocks5h://proxy support- Nix-first build flow with static builds via
cargo-zigbuild
Minimal usage:
ferry --url https://mcp.example.com/mcpTypical MCP client config:
{
"mcpServers": {
"remote": {
"command": "ferry",
"args": ["--url", "https://mcp.example.com/mcp"]
}
}
}Legacy SSE only:
ferry --url https://old.example.com/sse --transport sseThrough a SOCKS5 proxy:
ferry --url http://mcp.internal/mcp --socks5 127.0.0.1:1080Use a config file explicitly:
ferry --config /path/to/ferry.tomlOr via env:
FERRY_CONFIG=/path/to/ferry.toml ferryExpose a local stdio MCP server over HTTP:
ferry serve --listen 127.0.0.1:9000 stdio -- my-mcp-server --stdioThat serves:
- Streamable HTTP on
/mcp - legacy SSE on
/sseplus/messages - health on
/health
Relay to another HTTP MCP server instead of spawning stdio:
ferry serve --listen 127.0.0.1:9001 relay --url http://127.0.0.1:9000/mcp --transport streamable-httpUse a config file instead of passing the backend on the command line:
ferry serve --config /path/to/ferry.tomlExample serve config:
[serve]
listen = "127.0.0.1:9000"
path = "/mcp"
allow_origins = ["https://app.example.com"]
[serve.auth]
bearer_token = "${FERRY_SHARED_TOKEN}"
[serve.stdio]
command = "my-mcp-server"
args = ["--stdio"]Or drive serve mode from env:
FERRY_SERVE_LISTEN=127.0.0.1:9001 \
FERRY_SERVE_BACKEND=relay \
FERRY_SERVE_RELAY_URL=http://127.0.0.1:9000/mcp \
FERRY_SERVE_RELAY_TRANSPORT=streamable-http \
ferry serveUse another ferry as a local stdio bridge into that relay:
ferry --url http://127.0.0.1:9001/mcpLoopback is the default bind target. Exposing serve mode remotely requires an explicit non-loopback --listen, for example:
ferry serve --listen 0.0.0.0:9000 stdio -- my-mcp-server --stdioCursor reads MCP config from .cursor/mcp.json in your repo or ~/.cursor/mcp.json globally.
Flags:
{
"mcpServers": {
"ferry": {
"command": "ferry",
"args": ["--url", "https://mcp.example.com/mcp", "-H", "Authorization=Bearer <token>"]
}
}
}Env vars:
{
"mcpServers": {
"ferry": {
"command": "ferry",
"env": {
"FERRY_URL": "https://mcp.example.com/mcp",
"FERRY_HEADER_AUTHORIZATION": "Bearer ${env:MCP_TOKEN}"
}
}
}
}Codex CLI reads MCP config from ~/.codex/config.toml.
Flags:
[mcp_servers.ferry]
command = "ferry"
args = ["--url", "https://mcp.example.com/mcp", "-H", "Authorization=Bearer <token>"]Env vars:
[mcp_servers.ferry]
command = "ferry"
[mcp_servers.ferry.env]
FERRY_URL = "https://mcp.example.com/mcp"
FERRY_HEADER_AUTHORIZATION = "Bearer <token>"Config file via env:
[mcp_servers.ferry]
command = "ferry"
[mcp_servers.ferry.env]
FERRY_CONFIG = "/absolute/path/to/ferry.toml"Claude Code supports .mcp.json in the project root or --mcp-config <file>.
Flags:
{
"mcpServers": {
"ferry": {
"command": "ferry",
"args": ["--url", "https://mcp.example.com/mcp"]
}
}
}Env vars:
{
"mcpServers": {
"ferry": {
"command": "ferry",
"env": {
"FERRY_URL": "https://mcp.example.com/mcp",
"FERRY_HEADER_AUTHORIZATION": "Bearer ${MCP_TOKEN}"
}
}
}
}Supported inputs:
- CLI flags such as
--url,--transport,--header,--timeout - Environment variables such as
FERRY_URL,FERRY_TRANSPORT,FERRY_TIMEOUT,FERRY_CONFIG - Header env vars of the form
FERRY_HEADER_<NAME>=<VALUE> - Serve mode env vars such as
FERRY_SERVE_LISTEN,FERRY_SERVE_BACKEND,FERRY_SERVE_RELAY_URL - Serve auth and relay header env vars of the form
FERRY_SERVE_AUTH_HEADER_<NAME>=<VALUE>andFERRY_SERVE_RELAY_HEADER_<NAME>=<VALUE> - TOML config via
--config,FERRY_CONFIG,./ferry.toml, or$XDG_CONFIG_HOME/ferry/config.toml
Example config:
url = "https://mcp.internal.example.com/mcp"
transport = "auto"
timeout = 30
[headers]
authorization = "Bearer ${MCP_TOKEN}"
[retry]
count = 3
base_delay_ms = 1000Serve mode config lives under [serve]:
[serve]
listen = "127.0.0.1:9000"
path = "/mcp"
legacy_sse_path = "/sse"
legacy_messages_path = "/messages"
health_path = "/health"
[serve.auth]
bearer_token = "${FERRY_SHARED_TOKEN}"
[serve.auth.headers]
x-ferry-auth = "expected"
[serve.relay]
url = "https://upstream.example.com/mcp"
transport = "auto"
timeout = 30
[serve.relay.headers]
authorization = "Bearer ${UPSTREAM_TOKEN}"
[serve.relay.retry]
count = 3
base_delay_ms = 1000Notes:
ferry servefollows the same precedence as client mode,CLI > env > config > defaultsFERRY_SERVE_STDIO_ARGSexpects a JSON string array, for exampleFERRY_SERVE_STDIO_ARGS='[\"--stdio\"]'
nix-shell # or direnv!
cargo test
cargo clippy --all --benches --tests --examples --all-features
build_static