No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
hijynx fd3d176071
All checks were successful
CI / Build, vet, test, lint (push) Successful in 49s
docs: refresh AGENTS.md layout and add stderr debugging gotcha (#5)
## 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]>
2026-09-29 12:18:30 -06:00
.forgejo/workflows ci: use golangci-lint-action for linting 2026-09-25 19:36:28 -06:00
.gitignore chore: rename project to mcp-ferryman 2026-09-25 19:04:50 -06:00
.golangci.yaml docs: add README, AGENTS.md, lint config and mise toolchain 2026-09-25 16:44:04 -06:00
AGENTS.md docs: refresh AGENTS.md layout and add stderr debugging gotcha (#5) 2026-09-29 12:18:30 -06:00
go.mod chore: rename project to mcp-ferryman 2026-09-25 19:04:50 -06:00
go.sum chore: scaffold Go module with go-sdk dependency 2026-09-25 16:44:04 -06:00
heartbeat.go feat: opt-in heartbeat liveness detection with fail-fast connection errors (#4) 2026-09-29 12:03:52 -06:00
main.go feat: opt-in heartbeat liveness detection with fail-fast connection errors (#4) 2026-09-29 12:03:52 -06:00
mise.toml test: add end-to-end smoke test 2026-09-25 19:19:01 -06:00
proxy.go feat: opt-in heartbeat liveness detection with fail-fast connection errors (#4) 2026-09-29 12:03:52 -06:00
proxy_test.go feat: opt-in heartbeat liveness detection with fail-fast connection errors (#4) 2026-09-29 12:03:52 -06:00
README.md feat: opt-in heartbeat liveness detection with fail-fast connection errors (#4) 2026-09-29 12:03:52 -06:00
reconnect.go feat: opt-in heartbeat liveness detection with fail-fast connection errors (#4) 2026-09-29 12:03:52 -06:00
transport.go feat: opt-in heartbeat liveness detection with fail-fast connection errors (#4) 2026-09-29 12:03:52 -06:00

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 stored initialize, sends notifications/initialized, then flushes stdin lines buffered while down
  • Debug logging to stderr
  • Opt-in heartbeat liveness (--heartbeat): idle ping, pong timeout → reconnect path
  • mise for 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-sdk jsonrpc codecs and transports