Gateway tunnels
Put MCP servers you run yourself behind approvals without exposing them or sharing their credentials.
Many MCP servers have no hosted address: you run them yourself, as a command such as npx or docker run, or on your private network. A tunnel lets the MCP Gateway use them. It runs inside your network, connects out to withHuman, and serves the servers you list in its configuration. Every call still goes through your approval pipeline first.
Nothing on your side is exposed. The tunnel opens one outbound HTTPS connection to withHuman and keeps it open; withHuman sends approved calls back down it. The servers' credentials stay in the tunnel's environment and are never sent to withHuman.
Loading diagram…
Diagram source
flowchart LR
agent["Agent"] -->|Tool call| gateway["withHuman gateway<br/>Approval pipeline"]
tunnel["Tunnel<br/>in your network"] -->|Connects out| gateway
gateway -.->|Approved calls| tunnel
tunnel --> server["Your MCP server"]Tunnels are part of the hosted edition.
Set up a tunnel
- On the Gateway page, choose Tunnels, then Set up a tunnel.
- Name it after where it runs, such as a cluster. withHuman shows its token once. Copy it.
- Write a
tunnel.yamllisting the servers it serves. - Run the tunnel where those servers can run. The page shows when it connects.
servers:
terraform_read:
command: ["terraform-mcp-server", "stdio"]
env:
TFE_TOKEN: ${TFE_READ_TOKEN}
terraform_write:
command: ["terraform-mcp-server", "stdio"]
env:
TFE_TOKEN: ${TFE_WRITE_TOKEN}
billing:
url: http://billing-mcp.internal:8080/mcp
headers:
Authorization: Bearer ${BILLING_MCP_TOKEN}Each server has a name, then either a command the tunnel starts or a url it reaches on your network. ${NAME} is read from the tunnel's environment, so keep secrets in your usual secret store. The tunnel refuses to start if a variable it needs is not set.
A command server gets only its own env plus basics such as PATH, HOME and proxy settings. It never sees the tunnel's token or another server's secrets.
Run it with Docker, giving it the token, the servers' secrets and the file:
docker run -d --name withhuman-tunnel --restart unless-stopped -e WITHHUMAN_TUNNEL_TOKEN -e TFE_READ_TOKEN -e TFE_WRITE_TOKEN -v "$PWD/tunnel.yaml:/etc/withhuman/tunnel.yaml:ro" quay.io/withhuman/tunnel:latestThe tunnel's page shows a Kubernetes manifest too. Run two copies to keep serving while one restarts.
The image includes Node and uv, so npx and uvx commands work as documented. A server that ships its own program, such as terraform-mcp-server above, needs adding to the image:
FROM quay.io/withhuman/tunnel:latest
COPY terraform-mcp-server /usr/local/bin/Or run the server in its own container on the same network and list it by url.
Check that every server starts and lists its tools before you connect:
docker run --rm -e TFE_READ_TOKEN -e TFE_WRITE_TOKEN -v "$PWD/tunnel.yaml:/etc/withhuman/tunnel.yaml:ro" quay.io/withhuman/tunnel:latest checkRegister a server through it
Register a server on the Gateway page as usual, and choose Through a tunnel. Pick the tunnel, then give each scope the name of the tunnel server it calls. Scopes through a tunnel store no credential in withHuman: the tunnel holds it.
To let reads through and send writes to a person, give two scopes servers with different tokens. For example, read calls terraform_read and write calls terraform_write. The service then enforces the difference, and your pipeline can match withhuman_scope as on any other server. See Scopes.
Everything else about the server works as on a server reached at a URL: tool access, activation and revisions.
What to expect
- While the tunnel is offline, calls through it fail straight away with a message that the tunnel is offline. No approval is requested. Its tools leave agents' tool lists until it reconnects.
- When withHuman restarts, tunnels reconnect on their own within seconds.
- Each call records which copy of the tunnel carried it, in the Audit log.
- The tunnel only runs what its configuration lists. withHuman can ask for a server by name, and nothing else.
Status and tokens
A tunnel's page shows whether it is online, what its live copies serve, the gateway servers that go through it, and recent connections with their host and version.
To rotate a token, create a new one, update the tunnel, then revoke the old one. Revoking a token disconnects copies using it within a minute. Archive tunnel revokes every token for good. A tunnel an active server still uses can't be archived: deactivate the server or point it elsewhere first.
Troubleshooting
| What you see | What to do |
|---|---|
| The tunnel stops with "the gateway refused the tunnel token" | The token was revoked, mistyped, or its tunnel archived. Create a new token on the tunnel's page. |
| The tunnel stops asking you to upgrade | Pull the latest quay.io/withhuman/tunnel image. |
| A scope shows Not served | The tunnel's configuration has no server by that name. Add it, or fix the name on the scope. |
| A server's tools don't list | Run check where the tunnel runs, and read the tunnel's logs for that server. |
| The tunnel can't connect | It needs outbound HTTPS to app.withhuman.ai. If your network uses a proxy, set HTTPS_PROXY. |