- Go 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
CI / Build, vet, test, lint (push) Successful in 49s
## Summary - Layout section now lists the actual flat files (`proxy.go`, `transport.go`, `reconnect.go`, `heartbeat.go`) instead of the stale `internal/` split that never happened. - SDK gotchas: debug a hanging bridge by running the binary directly with piped stdin and `--debug`, since MCP clients swallow child stderr. ## Why Retrospective findings from debugging the pit hang: the stale layout pointer and the missing stderr-visibility gotcha both slowed diagnosis. ## Verification `go build ./...`, `go vet ./...`, `golangci-lint run` (0 issues), `go test ./...` all pass. Docs-only change. Co-authored-by: w33ble <[email protected]> Reviewed-on: #5 Reviewed-by: w33ble <[email protected]> |
||
| .forgejo/workflows | ||
| .gitignore | ||
| .golangci.yaml | ||
| AGENTS.md | ||
| go.mod | ||
| go.sum | ||
| heartbeat.go | ||
| main.go | ||
| mise.toml | ||
| proxy.go | ||
| proxy_test.go | ||
| README.md | ||
| reconnect.go | ||
| transport.go | ||
mcp-ferryman
A Go clone of mcp-proxy: connects a STDIO-based MCP client (Claude Desktop, Codex, opencode, etc.) to a remote HTTP-based MCP server, with optional HTTP headers for auth.
{
"mcpServers": {
"my-server": {
"command": "/path/to/mcp-ferryman",
"args": [
"--header", "Authorization: Bearer $API_TOKEN",
"http://localhost:4000/tidewave/mcp"
]
}
}
}
Usage
# Streamable HTTP server (2025-03-26+)
mcp-ferryman http://localhost:4000/tidewave/mcp
# With auth headers
mcp-ferryman \
--header "Authorization: Bearer sk-..." \
--header "X-Custom: whatever" \
https://example.com/mcp
# Via env (instead of args)
MCP_BRIDGE_URL=https://example.com/mcp \
MCP_BRIDGE_HEADER="Authorization: Bearer sk-..." \
mcp-ferryman
Flags
| Flag | Env | Description |
|---|---|---|
URL (positional) |
MCP_BRIDGE_URL |
MCP server URL (streamable HTTP or SSE) |
--header (repeatable) |
MCP_BRIDGE_HEADER |
HTTP header sent with every remote request, Key: Value |
--max-disconnected-time |
MCP_BRIDGE_MAX_DISCONNECTED_TIME |
How long to keep retrying after the remote drops, as a Go duration (30s, 5m); empty or 0 retries forever |
--initial-retry-interval |
MCP_BRIDGE_INITIAL_RETRY_INTERVAL |
Wait before the first reconnect retry as a Go duration (default 5s); doubles each retry up to 8x |
--heartbeat |
MCP_BRIDGE_HEARTBEAT |
Opt-in liveness detection: true/1/t (or false/0/f), default off |
--heartbeat-interval |
MCP_BRIDGE_HEARTBEAT_INTERVAL |
Idle time with no traffic in either direction before pinging, as a Go duration (default 30s) |
--heartbeat-timeout |
MCP_BRIDGE_HEARTBEAT_TIMEOUT |
How long to await the ping response before treating the connection as down, as a Go duration (default 10s) |
--debug |
— | Debug logging to stderr |
When --heartbeat is on, the bridge sends an MCP ping after
--heartbeat-interval with no traffic in either direction; a pong missing
past --heartbeat-timeout drops the connection into the normal reconnect
path (same as any other stall). Ping responses are never forwarded to the
client. With --heartbeat off (the default), behavior is unchanged: no
ticker, no pings.
Transport is auto-detected: a POST probe to the URL returns 405 → SSE
(2024-11-05); a JSON/RPC-status response → streamable HTTP. Auth headers are
applied to every remote HTTP request (probe, initialize, session reuse), via
a header-injecting http.RoundTripper, so session and protocol headers from the
Go SDK still apply.
Releases
Pushing a v* tag triggers a Forgejo Actions workflow
(.forgejo/workflows/release.yaml) that cross-compiles linux/darwin binaries
(amd64/arm64), generates sha256 checksums, and publishes them to a Forgejo
release (tags containing a hyphen, e.g. v0.1.0-rc1, are marked prerelease).
What we're building
- STDIO ↔ remote HTTP proxy for MCP, in Go
- Streamable HTTP (official Go SDK
StreamableClientTransport) - SSE (official Go SDK
SSEClientTransport) - Auto-detect transport via POST probe
- Custom HTTP headers for auth (the Rust original has no header support)
- Reconnect with backoff (
--max-disconnected-time,--initial-retry-interval): replays the storedinitialize, sendsnotifications/initialized, then flushes stdin lines buffered while down - Debug logging to stderr
- Opt-in heartbeat liveness (
--heartbeat): idle ping, pong timeout → reconnect path misefor Go + golangci-lint; lint before commit
What we're skipping (vs mcp_proxy_rust)
--override-protocol-version: Go SDK handles protocol version negotiation- Exact rmcp framing internals: replaced by
github.com/modelcontextprotocol/go-sdkjsonrpccodecs and transports