Show HN: mcp-gate – Ephemeral capability token proxy for LLM tool execution in Go
Skip to main content
Show HN: mcp-gate – Ephemeral capability token proxy for LLM tool execution in Go
Get link
Other Apps
August 23, 2026
I built mcp-gate, a small Go reverse proxy for a problem I keep coming back to with LLM agents:<br>How much authority should we actually give a model when it calls a tool?<br>Repo: https://github.com/ananthaprakashb/mcp-gate<br>A typical agent integration eventually ends up holding something powerful: an API key, service credential, OAuth token, or access to an MCP/tool server that can perform multiple operations.<br>Even when the model is supposed to perform one very specific action, the credential it indirectly controls may authorize far more.<br>I wanted the authorization boundary to look more like this:
The model never receives the upstream API credential.<br>Instead, the trusted orchestrator exchanges its gate credential for an ephemeral capability token authorizing one specific operation.<br>For example:<br>"route": "tickets",<br>"method": "POST",<br>"path": "/v1/tickets",<br>"ttl_seconds": 15
The returned bearer token is HMAC-signed and bound to:<br>a configured route
an HTTP method
an exact path
an expiration time
a unique token ID
It can then be used for that operation and, by default, only once.<br>Why single-use tokens?<br>Short expiration helps, but I don't think expiration alone is enough for agent tool calls.<br>Imagine issuing a 30-second token to create a ticket.<br>The intended authority is really:<br>Create this ticket once.<br>It isn't:<br>Create as many tickets as you can during the next 30 seconds.<br>So mcp-gate maintains replay state for the token ID. Once a valid request consumes the capability, replaying the same token fails.<br>The built-in store is process-local. There is also a ReplayStore interface intended for distributed implementations such as Redis/Valkey using an atomic SET ... NX ... EX ... style operation.<br>If the replay store fails, the proxy fails closed rather than silently disabling replay protection.<br>The arguments are part of the security boundary<br>Restricting the endpoint isn't sufficient if the model can invent additional JSON fields.<br>Suppose a tool is intended to expose:<br>"title": "Investigate alert",<br>"priority": "high"
but the underlying API also understands:<br>"title": "Investigate alert",<br>"priority": "high",<br>"admin": true
Depending entirely on prompt instructions to prevent that doesn't seem like a great security boundary.<br>mcp-gate therefore validates the body before forwarding it.<br>I deliberately implemented a small subset of JSON Schema rather than pulling in a large schema system. It currently supports things such as:<br>type<br>properties<br>required<br>items<br>enum<br>pattern<br>minLength / maxLength<br>minimum / maximum<br>minItems / maxItems
Objects are closed by default.<br>If a property wasn't declared in the policy, it isn't forwarded.<br>Importantly, schema validation happens before the capability is consumed. An agent can therefore correct malformed arguments without losing the one legitimate execution it was authorized to perform.<br>Upstream secrets stay upstream<br>Route configuration lives on mcp-gate, not inside the capability.<br>A route can inject things such as:<br>Bearer credentials<br>Basic Auth<br>fixed headers<br>fixed query parameters
So an agent might see:<br>POST /proxy/tickets/v1/tickets<br>Authorization: Bearer
while mcp-gate sends something like:<br>POST https://internal-api/v1/tickets<br>X-API-Key:
The actual upstream secret never needs to enter the model context.<br>I think this distinction becomes increasingly useful as agents interact with infrastructure, CI/CD, databases, ticketing systems, cloud APIs, internal admin tools, and eventually more destructive operations.<br>I also didn't want the proxy to become an information leak<br>Upstream failures are deliberately sanitized.<br>If the backend responds with something like:<br>database connection failed at postgres.internal.example<br>token=...<br>stack trace...
mcp-gate does not pass that body back to the model.<br>The caller gets a stable error instead.<br>Request and response sizes are bounded, caller-provided authentication headers aren't forwarded to the upstream service, and sensitive hop-by-hop headers are restricted.<br>Why Go?<br>There isn't much framework machinery here.<br>The project is intentionally small and uses Go's HTTP and crypto primitives. My goal is for the authorization path to remain understandable enough that someone can read it rather than trusting a large opaque security layer.<br>You can run it with Go 1.22+:<br>export GATE_SIGNING_KEY='replace-with-at-least-32-random-characters'<br>export GATE_ADMIN_KEY='orchestrator-to-gate-secret'<br>export GATE_ROUTES='[...]'
go run ./cmd/mcp-gate
There is also a Docker image setup and an end-to-end Docker Compose example with a mock ticket API and a small agent:<br>cd examples<br>docker compose up --build \<br>--abort-on-container-exit \<br>--exit-code-from agent
CI is set up to run tests with the race detector, go vet, and...