Quilzomanual

MCP gateway: govern the tools AI agents call, with pinning and approvals

Agents call only the tools a person agreed to, as those tools were on the day they agreed, and other vendors' agents reach your tool servers only through Quilzo.

In the adminIntegrations

An MCP gateway sits between AI agents and MCP servers and decides each tool call: who may call which tool, how often, whether a person approves it first, and with which credential. Quilzo is one for its own agents, and offers the same to other vendors' agents at /mcp/gateway/NAME.

Declare a tool server

  1. Write the declaration

    Declarations live in integrations.json in the store. Each names one host, why this install reaches it, and the tools it may call there. Everything is off until somebody turns it on, so an example pasted from somewhere does nothing by accident.

    {
      "declared": [
        {
          "name": "tracker",
          "kind": "mcp",
          "enabled": true,
          "purpose": "File and read issues in the support tracker",
          "endpoint": "tracker.example.com",
          "uses": ["list_issues", "create_issue"],
          "writes": true,
          "secret": "tracker-token"
        }
      ]
    }
    FieldMeans
    endpointExactly one hostname, or an IPv4 address: no scheme, no wildcard. Quilzo always uses https.
    port, pathWhere on that host the server answers, when it is not port 443 and /. Most MCP servers answer at /mcp.
    usesThe tools this install may call there. Never "everything the server offers": a server can add or redefine a tool after the day you trusted it.
    writesThe tools change something on the far side, so a reviewer does not have to guess from their names.
    secretThe name of the credential, never its value: the store keeps history that cannot be deleted. Keep the value sealed with quilzo secret set tracker-token < file, or set QUILZO_SECRET_TRACKER_TOKEN (the name in capitals, with anything else turned into _), which wins when both are there.
    quilzo integrations list
    tracker  mcp  enabled
      File and read issues in the support tracker
      reaches tracker.example.com
      may call list_issues, create_issue
      changes something on the far side
  2. Look at what the server offers, and pin it

    quilzo integrations tools tracker asks the server what it offers and marks what you agreed to. The difference between the two lists is worth reading. When you are satisfied, pin the tools: Quilzo records a SHA-256 of each tool's definition (name, description and input schema) as the server gave it that day.

    quilzo integrations tools tracker
    quilzo integrations pin tracker list_issues create_issue

    A pinned tool whose definition later changes is refused until a person pins it again, and the shield is told. This is the answer to tool poisoning: a name staying the same is not the tool staying the same. Only a pinned tool can be chosen by a model.

  3. Check it in the admin

    Administration › Integrations shows each server, what it may call, how it authenticates, and whether it is offered through the gateway.

    The Integrations screen: tracker reaches tracker.example.com, may call list_issues and create_issue, is enabled and offered through the gateway to authors
    A declared tool server, offered through the gateway. The waiting call shown is sample data in the program's own format.

A REST API, from its OpenAPI description

An API with no MCP server is declared from its OpenAPI 3 document (JSON). Each operation becomes a tool by its name, with the same allow-list, pins, gateway and approvals as an MCP server's tools. Import once to see what is on offer, then again naming what this install may call:

quilzo integrations import billing-openapi.json
quilzo integrations import billing-openapi.json --uses getInvoice,listInvoices --secret billing-key

The declaration arrives switched off, with only the named operations, as kind http: each one's method, its path under the endpoint, and its path and query parameters. A bearer token, or an API key in a header the document names, comes from the secret, never from the agent.

An operation that spends money declares pays: where its amount, currency and payee are, so each payment is checked against the agent's mandates (see agents that pay).

A callIs
BuiltOnly from what its operation declares. An argument it does not declare is refused rather than passed on, and path values are escaped.
SentOver https, to the declared host and nothing else, with no redirects followed.
AnsweredUp to 32 KiB of the response, or the status and the start of the error.
ChangedAn operation edited after it was pinned is refused until somebody pins it again.

An import never fetches what a document refers to: a reference outside the document is named and left out. Header and cookie parameters, and an API key in a query string, are not imported. A REST API can be offered to other vendors' agents through the gateway like an MCP server.

An MCP server from the registry

A server's server.json from the MCP registry becomes a declaration of its Streamable HTTP address. Without --uses the server is asked what it offers and nothing is declared:

quilzo integrations import server.json
quilzo integrations import server.json --uses list_issues,create_issue --secret tracker-token

A server published only as a package runs on your side first; give its address with --endpoint.

Let an agent use a tool

An agent names each tool it needs, on its host, with a purpose, in its declaration. Tools that change something can ask first:

"tools": [
  { "name": "list_issues", "host": "tracker.example.com", "purpose": "see whether a problem is already reported" },
  { "name": "create_issue", "host": "tracker.example.com", "purpose": "report a problem it finds", "writes": true }
],
"ask_first": ["create_issue"]

A call is allowed only if the agent declares the tool, an enabled integration offers it, and it is pinned. Every call is recorded, and the exfiltration breaker stops a call when the run holds something private and has read somebody else's words (see Limits that hold). Try a call by hand with quilzo integrations call tracker list_issues.

A tool server on your own network

Quilzo refuses private, loopback and link-local addresses unless you say otherwise, so a declaration cannot be pointed at whatever else is inside your network. To use an MCP server that runs inside it, name the range it is in with reach:

{
  "name": "knowledge",
  "kind": "mcp",
  "enabled": true,
  "purpose": "Search the internal knowledge base",
  "endpoint": "kb.corp.example",
  "port": 8443,
  "path": "/mcp",
  "reach": ["10.20.0.0/16"],
  "uses": ["search"]
}
  • Every address the name resolves to must be inside a declared range. Quilzo checks this when it connects, so changing a DNS answer later does not let the declaration reach anything else.
  • Only private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, fc00::/7) and loopback can be named. Link-local, where cloud metadata lives, never can. Up to 8 ranges, each written from where it starts.
  • The server still needs https. If your organisation signs its own certificates, the machine running Quilzo has to trust that authority, through the system's certificate store or SSL_CERT_FILE.
  • In an offline deployment (network.mode set to offline) only loopback is reached, whatever is declared.

The Integrations screen and quilzo integrations list show the range next to the server, so a reviewer sees what was opened.

Offer your MCP servers to other agents

Another vendor's agent, or one your teams built, can reach a declared server through Quilzo instead of directly. It gets only the pinned tools, at the role and daily limit you set, with Quilzo's credential injected and never its own. Every call is checked, counted and recorded with a digest of its arguments.

  1. Turn the gateway on for one server

    quilzo integrations gateway tracker --role author --daily 200 --ask create_issue

    --role is the least role a caller needs (reader, author or publisher). --daily caps one caller's calls a day (500 if you name none). --ask lists tools a person approves call by call. --off stops offering it.

  2. Give the caller the address and a token

    The server is offered at https://YOUR-ADMIN/mcp/gateway/tracker. A caller authenticates with a Quilzo token or an OAuth connection (see Connect AI apps), and is limited by that principal's access.

  3. Decide the calls that wait

    A call to a tool under --ask waits under Calls waiting for a person. Approved, exactly that call goes through once, within the hour. Somebody other than the person it acts for must decide.

    Calls waiting for a person: create_issue on tracker for helpdesk-bot through the help desk agent, with its arguments, and Approve or Decline
    A call waiting for a second person (sample data).
    quilzo integrations held
    quilzo integrations approve gq_915959844d12
    quilzo integrations decline gq_915959844d12

Every caller of the gateway, and every tool server, also appears on the Fleet map.

Tell other systems when something happens

A webhook is a signed POST to a URL you choose when something is published, rolled-back, scheduled, submitted (a form) or proposed (a change waiting for approval). Only https endpoints are accepted.

quilzo webhook add https://deploy.example.com/hooks/quilzo

The signing key is printed once. Each delivery carries X-Quilzo-Event, X-Quilzo-Delivery, X-Quilzo-Timestamp and X-Quilzo-Signature. The signature is an HMAC-SHA256 over the timestamp and the body, each prefixed with its length, so the boundary between them cannot be moved. Reject anything more than five minutes old. A receiver in Python:

import hmac, hashlib, struct, time
def verify(key, timestamp, body, signature, now=None):
    if abs((now or time.time()) - int(timestamp)) > 300:
        return False                      # too old or from the future: a replay
    mac = hmac.new(key.encode(), digestmod=hashlib.sha256)
    for field in (timestamp.encode(), body):
        mac.update(struct.pack(">Q", len(field)) + field)
    return hmac.compare_digest("v1=" + mac.hexdigest(), signature)
The Webhooks section: one endpoint receiving every event, its key partly shown, with Turn off and Remove
Webhooks, under Integrations.

quilzo webhook list, webhook test and webhook remove ID manage them.

Traces and the audit log, to your own tools

Set telemetry.otlp_endpoint and every agent run is sent as OpenTelemetry spans using the GenAI semantic conventions (gen_ai.operation.name, gen_ai.tool.name and the rest) to the collector you already run.

The audit log exports to a SIEM as OCSF (the default), CEF or JSON Lines. --envelope writes every exported event's hash, in order, beside the export, so the receiving side can tell whether anything was added, removed or reordered:

quilzo siem ocsf --envelope envelope.json > audit.ocsf.jsonl
quilzo siem jsonl --envelope evidence.json > audit.jsonl
quilzo siem verify audit.jsonl --envelope evidence.json
verified  185 event(s), sequences 1-185
  nothing was added, removed, reordered or altered since export

An OCSF or CEF record carries its event's hash (metadata.uid, or cs3 in CEF) rather than the whole event, so compare those with the envelope. A JSON Lines export carries each event whole, which is what siem verify recomputes; keep one of those as evidence.

Next