Private betaRequest early access →

Sign in

MCP tool gateway for agents

Agents do their riskiest work when they call tools. The MCP gateway puts Perihelium's authorization engine between your agent and every MCP tool server: each tools/call is individually authorized, budgeted, and logged before it leaves.

Availability: the gateway is enabled per installation. While it is off, its endpoints — /api/v1/mcp/servers and /api/v1/mcp/call/* — return 404.

Not to be confused with the other two MCP surfaces. Three different things share the mcp name:

SurfacePathWhat it is
This gateway/api/v1/mcp/servers, /api/v1/mcp/call/*Zynth authorizing your agents' outbound calls to your MCP tool servers. Feature-flagged.
Zynth's own MCP serverPOST /api/v1/mcpAn agent operating Perihelium itself over MCP. Always on; agent tokens only.
Zynth as your MCP authorization server/api/v1/oauth/*Minting audience-bound tokens your MCP server verifies.

Probing POST /api/v1/mcp therefore tells you nothing about this gateway — that path is the first-party MCP server and answers whatever the flag is set to.

Setup (admin)

Everything in this section can also be done from the console's MCP registry page (register a server, set the per-agent call caps, retire a server) — the API calls below are what that page uses.

  1. Register the tool server (requires mcp:servers:manage):

    Shell
    curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
      -d '{"name": "github", "upstream_url": "https://mcp.example.com/rpc",
           "max_calls_per_minute": 30, "max_calls_per_day": 500}' \
      https://auth.example.com/api/v1/mcp/servers
    

    The upstream host must also be permitted by the deployment's egress allowlist — two independent chokepoints, both admin-controlled.

  2. Allowlist each tool with a reviewed risk score (1–20) and, for a tool that only reads, "read_only": true. Tools not on the list cannot be called — or even seen — by agents:

    Shell
    curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
      -d '{"tool_name": "search", "risk_score": 3, "read_only": true}' \
      https://auth.example.com/api/v1/mcp/servers/$SERVER_ID/tools
    

    read_only defaults to false: a tool you have not declared counts as a write against the agent's hourly write budget (the blast-radius breaker that contains a runaway agent). Declare your reads so a read-heavy agent never spends that budget on search; protocol frames (initialize, tools/list) never do. The flag changes what a call costs the agent, never whether it is allowed — a read-only tool still needs to be on the allowlist, inside the delegation scope and under the agent's risk ceiling.

  3. Delegate mcp:call to the agent, optionally scoped to specific servers/tools with glob patterns:

    Shell
    curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
      -d '{"delegate_id": "'$AGENT_ID'", "delegate_type": "agent",
           "permissions": ["mcp:call"], "oversight_mode": "AUTONOMOUS",
           "scope": {"mcp_servers": ["github"], "mcp_tools": ["search", "read_*"]}}' \
      https://auth.example.com/api/v1/authz/delegations
    

Calling tools (agent)

The agent sends ordinary JSON-RPC 2.0 to the gateway with its agent token — never to the tool server directly:

Shell
curl -X POST -H "Authorization: Bearer $AGENT_TOKEN" -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
       "params": {"name": "search", "arguments": {"q": "zynth"}}}' \
  https://auth.example.com/api/v1/mcp/call/github

Supported methods: initialize, tools/list (filtered to the allowlist), and tools/call.

There is a request budget, and it is generous. Every frame counts — initialize and tools/list as well as tool calls, and a call to a server that does not exist — against a per-agent ceiling, your organization's fleet allowance, and a per-source-host floor. This is the gateway's own budget: separate from the first-party MCP server's and from the per-server max_calls_* caps you configure (those refuse with 403). Over the limit you get 429 with a computed Retry-After and RateLimit-Limit/-Remaining/-Reset describing the budget that bit — honour Retry-After rather than retrying immediately. One agent exhausting its own ceiling does not spend your other agents' allowance. A 429 with no RateLimit-* headers and Retry-After: 30 means the limit could not be computed and the gateway refused deliberately rather than wave requests through; retry shortly.

Every tools/call is checked, in order: tool on the allowlist → delegation scope covers this server+tool → the agent's rate/budget caps → the full authorization engine (kill switch, risk ceiling vs. the tool's score, ABAC policies — and, for supervised agents, a human approval bound to these exact arguments). A denial is a 403 with the reason (or the pending approval_request_id); an unreachable tool server is a structured 502 — the gateway never fails open.

A 503 with Retry-After on a call means it did not run. The gateway writes the decision to allow a call into the audit trail, and commits it, before it reaches your tool server. If that record cannot be written the call is refused 503 and nothing is forwarded, so retrying after Retry-After is safe. The converse holds too: once your tool has run, the gateway returns its reply (or the 502 for its failure) and never a 503 for an audit reason, so a tool that is not idempotent is never invoked twice by a retry the gateway asked for.

What you get

  • Kill switch that actually stops tool use — engaged mid-session, the agent's very next call is denied.
  • No scope escalation across chained calls — authority is evaluated fresh per call from delegation scope; there is no session-wide tool grant.
  • Budgets — per-agent, per-server call caps deny (never queue) when exceeded, under a platform-wide per-agent / per-organization / per-host request budget that fails closed.
  • A write budget that meters writes — only calls to tools you have not declared read_only spend the agent's hourly write budget; handshakes and declared reads are free.
  • A complete decision log — every allowed and denied call lands in the tamper-evident audit trail with the argument fingerprint, and streams live in the Command Center. An allowed call is recorded before it is forwarded (mcp.call_allowed), so a call whose tool server then failed carries both that record and an mcp.call_upstream_error. An approval an agent holds is spent in the same step as that record, so a call refused with a temporary 503 can be retried with the same approval.