Agent Office v0.27.0 · POC Get started
Menu: Guides

Guides

HTTP APIs as tools

Connect any REST API without an MCP server. Declare its endpoints, or import them from an OpenAPI file, and each becomes a tool the office calls with a key the agent never sees.

Many services have a plain HTTP API but no MCP server: your billing system, a CRM, an internal inventory service. An HTTP API connection turns the endpoints you choose into agent tools. You declare them in connection.yaml, or pick them from an OpenAPI file. The office adds the API key to each request on its side, so the agent's sandbox never holds it.

Each endpoint is a normal connection tool. Grants, pin, args and approve work exactly as for any other service, and every call is ledgered as connection.call.

How it works#

  1. The agent calls billing__list_invoices with arguments, like any connection tool.
  2. The office checks the grant, sets pinned arguments and refuses values outside args. If the tool is in approve, it files a request on Needs you first.
  3. It builds the request on the connection's base URL and adds the credential from the secret store. Then it sends the request through the office's network guard, which refuses private addresses.
  4. The agent gets the status and the body back. JSON is pretty-printed, long responses are cut, and any echo of the key is replaced by [redacted].

1. Declare the endpoints#

Connections → Add connection → HTTP API prefills a form: the base URL, where the key goes, and the endpoints as YAML. It opens a pull request adding connections/<name>/connection.yaml:

title: Acme Billing
description: Invoices and refunds.
transport: http-api
url: https://billing.example.com/v2        # the base URL; every endpoint path goes under it
identity: office                           # office (one key) or agent (a key per agent)
http_api:
  auth:
    type: header                           # bearer | header | query | basic | none
    name: X-Api-Key                        # the header or query parameter (header and query only)
  headers:                                 # optional fixed headers, never secrets
    Accept: application/json
  max_response_chars: 20000                # default 20,000; at most 200,000
  timeout_seconds: 30                      # default 30; at most 120
  endpoints:
    list_invoices:                         # the tool name: lowercase and underscores
      method: GET
      path: /invoices
      description: List invoices, newest first.
      params:
        status: { in: query, enum: [open, paid, void] }
        customer_id: { in: query }
        limit: { in: query, type: integer, description: At most 100 }
    get_invoice:
      method: GET
      path: /invoices/{invoice_id}         # {placeholders} are path params
      description: One invoice with its lines.
      params:
        invoice_id: { in: path }
    refund_invoice:
      method: POST
      path: /invoices/{invoice_id}/refunds
      description: Refund all or part of an invoice.
      params:
        invoice_id: { in: path }
        amount_cents: { in: body, type: integer, required: true }
        reason: { in: body }
Param fieldMeaning
inpath (a {placeholder}), query (default) or body (a field of the JSON body)
typestring (default), integer, number, boolean, array (with items) or object
requiredThe agent must pass it. Path params are always required.
enumThe only values allowed
descriptionShown to the agent in the tool's schema
nameThe name on the wire, when it isn't a plain identifier (page[size], X-Request-Id)
as_bodyFor one in: body param of type object or array: its value is the whole JSON body

The office refuses arguments the endpoint doesn't declare, wrong types and values outside enum. Path values are URL-encoded, and . or .. are refused, so a request can't leave the base URL. GET endpoints are marked read-only in the tool list and DELETE endpoints destructive.

2. Or import from an OpenAPI file#

Put the API's OpenAPI 3 file (JSON or YAML, up to 2 MB) next to connection.yaml in the same pull request, and name the operations to keep:

http_api:
  auth: { type: bearer }
  openapi:
    file: openapi.yaml                     # connections/<name>/openapi.yaml
    operations:                            # operationIds, or "METHOD /path"
      - listInvoices
      - getInvoice
      - POST /invoices/{id}/refunds
  endpoints:                               # optional: these win over imported ones with the same name
    get_invoice:
      method: GET
      path: /invoices/{id}
      description: One invoice. Amounts are in cents.
      params:
        id: { in: path }

Only the operations you list become tools. A whole public API never becomes callable by accident. The import reads path and query parameters (including shared ones and $refs), and turns a JSON request body's top-level properties into in: body params. readOnly properties are skipped. Tool names come from the operationId in snake case (listInvoices → list_invoices), or from the method and path. An operation that needs a header or cookie parameter, or has a body that isn't JSON, is not imported. The policy check says why. The base URL is always the connection's url, never the file's servers.

3. Store the credential#

Once merged, open the connection and paste the credential on its Overview tab:

auth.typeSecretSent as
bearerAPI tokenAuthorization: Bearer <token>
headerAPI key<name>: <key>
queryAPI key?<name>=<key> (left out of what the agent sees)
basicUsername and passwordAuthorization: Basic …
nonenothingnothing

With identity: agent each agent gets its own key: grant the agent first, then paste its key. You can also store keys from the office host with officectl connections secret <name> [--agent <slug>] --key token (or --key username / --key password). An optional expiry date gets the same warnings as other tokens.

4. Grant the tools#

capabilities:
  - connections:
      billing:
        tools: [list_invoices, get_invoice, refund_invoice]
        pin: { customer_id: cus_acme_eu }              # the office sets it; the agent can't change it
        args: { status: [open, paid] }                  # only these values
        approve: [refund_invoice]                       # a person approves each refund first

Responses#

The agent gets the method and path, the HTTP status and content type, then the body:

  • JSON is pretty-printed.
  • Text (text/*, XML, CSV) is shown as it is.
  • Binary bodies are described, not shown: (binary image/png, 48213 bytes; not shown).
  • Everything is cut at max_response_chars, with a note asking the agent to request less, such as a smaller page. The office reads at most 2 MB of any response.
  • Status 400 and above come back as tool errors, so the agent sees that the call failed.
  • Redirects are not followed. The agent sees the Location, so a credential is never sent to a second host.

Internal APIs#

By default the office reaches public addresses only. Requests to loopback, private (10/8, 172.16/12, 192.168/16, IPv6 ULA), link-local and other reserved addresses are refused. That includes host names that resolve to them, and IPv6 forms that embed a private IPv4 address. The check runs when the connection opens, so DNS rebinding can't slip past it. The agent gets Blocked: … is on a private network address.

To connect an internal API, set allow_private: true. Plain http:// URLs are then allowed too:

url: http://inventory.acme.internal/api
http_api:
  auth: { type: bearer }
  allow_private: true

A pull request that turns allow_private on needs an access approver, as for any connection, and it must also be merged by an admin or the owner. The pull request page says so. Allowing it points the office at its own network, so keep these connections narrow: few endpoints, read-only where you can, and approve on anything that changes data.

Security notes#

  • The credential stays in the encrypted secret store and is added by the office for each request. It never reaches the agent's sandbox, and it is redacted from responses, the ledger and the logs.
  • No credentials in the policy: url can't carry a user or password, and headers can't set Authorization, Cookie or Host.
  • Only the endpoints you declare exist. The agent can't call other paths, methods or hosts.
  • Response text comes from outside the office. The agent is told to treat it as data, not instructions.