Architecture

Why We Built the ToolHost MCP Gateway in Go

5 min read ToolHost Engineering

Stack choices for infrastructure software are easier to justify when you do not oversell them. Go was the right choice for the ToolHost MCP gateway for specific, concrete reasons. There are also real trade-offs, and those are worth stating plainly.

MCP's message model maps directly to goroutines

The MCP protocol is fundamentally concurrent. A single gateway session can have multiple tool calls in flight simultaneously. The server must be able to route responses back to the right pending request, handle server-to-client push messages while client requests are being processed, and maintain multiple sessions concurrently across different clients.

This is a workload that goroutines handle well. The Go concurrency model — lightweight goroutines, channels for coordination, the net/http package's per-connection goroutine model — matches the shape of the problem without requiring an explicit async runtime. You write mostly-synchronous code that happens to run concurrently, which keeps the implementation straightforward to reason about.

An async/await model (Node, Python asyncio) works too, but it requires careful attention to blocking calls that can stall the event loop. A thread-per-connection model (traditional Java) works but pays the overhead of OS threads at the volume of MCP sessions a gateway sees. Goroutines sit in a useful middle ground: cheap enough to allocate per-session or per-request, cooperative enough to avoid the full OS scheduler overhead.

Single binary deployment

The ToolHost gateway deploys as a single binary. No runtime to install, no dependency management at deploy time, no version conflicts between the gateway and its runtime environment. The binary includes the embedded operator console as a static asset. docker cp, scp, or a standard package install is all it takes to get the gateway running on a new host.

This matters operationally. A gateway is infrastructure — it runs on servers managed by operators who may not want to deal with Node version managers or Python virtual environments. A binary with no external runtime dependencies is significantly easier to operate and audit.

Go's static linking (with CGO_ENABLED=0) also means the container image can be built FROM scratch — no base OS, no shell, no package manager attack surface. The container is the binary.

The go-sdk is the reference implementation

The MCP team at Anthropic maintains an official Go SDK: modelcontextprotocol/go-sdk, currently at v1.6.0 targeting the 2025-11-25 spec. This is not a community port — it is developed by the same team that writes the specification.

This matters for a production gateway. When the spec changes, the go-sdk tracks it. When there are ambiguities in the spec, the go-sdk's implementation reflects the authoritative interpretation. Building on the reference implementation means fewer cases where our gateway diverges from spec intent in ways that affect real clients.

The SDK provides the core types, transport handling, and session management we need. We build the control plane — policy enforcement, credential management, schema pinning, rate limiting, the admin API — on top of that foundation. The protocol layer is not something we had to implement from scratch.

Fast compile times for iteration

A gateway is not a finished product. The MCP spec is evolving, new protocol features arrive regularly, and operator requirements surface problems that require changes. Fast compile times matter when the development loop is tight.

Go's compilation speed is a real advantage here. The full gateway compiles in a few seconds on a modern machine. Integration tests that spin up the gateway, connect clients, and make tool calls complete in seconds rather than minutes. This keeps the development loop tight and makes it practical to test protocol-level changes end-to-end before shipping them.

The honest trade-off: library ecosystem

The TypeScript MCP ecosystem has more tools than Go. If you look at community-built MCP utilities, client libraries, and server toolkits, TypeScript has a significant lead. There are more examples, more Stack Overflow answers, and more community code to reference.

For a gateway, this matters less than it would for a tool server. The gateway does not implement tool logic — it routes, enforces policy, and manages credentials. The surface area where you might reach for an ecosystem library is smaller. But it is a real cost, particularly when researching edge cases in protocol behavior or debugging interop issues.

We accept that trade-off. The operational benefits of a single static binary, the concurrency model fit, and the reference SDK availability outweigh the library ecosystem gap for this specific workload.

Stability over novelty

Go v1 compatibility means the codebase we write today compiles unchanged on Go 1.25 when it ships. The language does not make breaking changes between minor versions. For infrastructure software with a long operational lifespan, this predictability has practical value.

The go-sdk at v1.6.0 is actively maintained, tracked to the spec, and used in production by teams building MCP infrastructure. That stability is more important for a gateway than it would be for an application — gateways are the thing that other things depend on. Churn in a gateway's dependencies propagates to everything it serves.

Go was not chosen because it is fashionable. It was chosen because its operational model, concurrency primitives, and the existence of a maintained reference SDK make it a good fit for the specific problem of building a production MCP gateway.