Show HN: mcp-gate – Ephemeral capability token proxy for LLM tool execution in Go

I built mcp-gate, a small Go reverse proxy for a problem I keep coming back to with LLM agents:

How much authority should we actually give a model when it calls a tool?

Repo: ananthaprakashb/mcp-gate on GitHub

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. Even when the model is supposed to perform one very specific action, the credential it indirectly controls may authorize far more.

I wanted the authorization boundary to look more like this:

mcp-gate architecture diagram

The model never receives the upstream API credential. Instead, the trusted orchestrator exchanges its gate credential for an ephemeral capability token authorizing one specific operation.

For example:

{
  "route": "tickets",
  "method": "POST",
  "path": "/v1/tickets",
  "ttl_seconds": 15
}

The returned bearer token is HMAC-signed and bound to:

  • 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.

Why single-use tokens?

Short expiration helps, but expiration alone isn't enough for agent tool calls. Imagine issuing a 30-second token to create a ticket.

The intended authority is: "Create this ticket once."
It isn't: "Create as many tickets as you can during the next 30 seconds."

mcp-gate maintains replay state for the token ID. Once a valid request consumes the capability, replaying the same token fails.

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. If the replay store fails, the proxy fails closed rather than silently disabling replay protection.

The arguments are part of the security boundary

Restricting the endpoint isn't sufficient if the model can invent additional JSON fields.

Suppose a tool is intended to expose:

{
  "title": "Investigate alert",
  "priority": "high"
}

...but the underlying API also understands:

{
  "title": "Investigate alert",
  "priority": "high",
  "admin": true
}

Depending entirely on prompt instructions to prevent that isn't a solid security boundary. mcp-gate validates the body before forwarding it.

I deliberately implemented a small subset of JSON Schema rather than pulling in a large schema engine. It currently supports:

type, properties, required, items, enum, pattern, minLength / maxLength, minimum / maximum, and minItems / maxItems.

Objects are closed by default: if a property wasn't declared in the policy, it isn't forwarded.

Importantly, schema validation happens before the capability is consumed. An agent can correct malformed arguments without burning the single legitimate execution it was granted.

Upstream secrets stay upstream

Route configuration lives on mcp-gate, not inside the capability token. A route can inject:

  • Bearer credentials
  • Basic Auth
  • Fixed headers
  • Fixed query parameters

The agent sees:

POST /proxy/tickets/v1/tickets
Authorization: Bearer <ephemeral-capability>

While mcp-gate sends:

POST https://internal-api/v1/tickets
X-API-Key: <actual-secret>

The actual upstream secret never enters the model's context window. This boundary becomes critical as agents gain access to infrastructure, CI/CD, databases, ticketing systems, cloud APIs, and destructive operations.

Preventing information leaks

Upstream failures are deliberately sanitized. If the backend responds with internal debugging details:

database connection failed at postgres.internal.example
token=...
stack trace...

mcp-gate strips that body and returns a stable error response. Request and response payloads are strictly bounded, caller-provided authentication headers aren't forwarded upstream, and sensitive hop-by-hop headers are stripped.

Why Go?

There isn't heavy framework machinery here. The project uses Go's standard HTTP and crypto primitives so the authorization path remains small and auditable.

Running with Go 1.22+:

export GATE_SIGNING_KEY='replace-with-at-least-32-random-characters'
export GATE_ADMIN_KEY='orchestrator-to-gate-secret'
export GATE_ROUTES='[...]'

go run ./cmd/mcp-gate

Running the end-to-end Docker Compose setup (with a mock ticket API and agent):

cd examples
docker compose up --build \
  --abort-on-container-exit \
  --exit-code-from agent

CI runs tests with the race detector enabled, alongside go vet and govulncheck.

What mcp-gate is not

This is not an identity provider, and it is not trying to replace OAuth. It is also not a heavy enterprise policy engine. The goal is focused:

Turn broad application authority into a tiny, short-lived capability for a single LLM tool execution.

Roadmap areas currently being explored:

  • Distributed replay-store adapters
  • Asymmetric signing and key rotation
  • Richer policy constraints and argument-bound capabilities
  • Rate / budget limits
  • Audit logging and OpenTelemetry tracing
  • MCP-native integration examples
  • Kubernetes deployment templates

The core question

As agents become more autonomous, we need to clearly distinguish between:

"The agent is authenticated"

vs.

"The agent has authority to perform exactly this operation, with these arguments, once, for the next few seconds."

mcp-gate is an experiment around that second model. If you're building MCP servers, coding agents, internal workflows, or agent gateways, where would this capability model break down in your setup? What would you need before placing something like this in front of a production service?

Check out the code and discussion on GitHub: ananthaprakashb/mcp-gate.

Comments

Popular posts from this blog

The Secret Handshake (Identification vs. Authentication)

If Your Business Logic Knows AWS Exists, It Isn’t Business Log