Tools & approvals
Tools are functions a smith can call. They come in two kinds: hosted (Ingram Cloud executes, in-process) and MCP tools (your own server executes, spoken over the Model Context Protocol). Ingram Cloud is the MCP client: you point it at your MCP server and it discovers and calls your tools. MCP tools can additionally be gated behind a human approval.
Hosted vs MCP
| Hosted | MCP | |
|---|---|---|
| Runs on | Ingram Cloud, in-process | your MCP server |
| Examples | web_search, web_fetch, calculator, reasoning, read_budget, manage_schedules, file_search | anything: your DB, your APIs |
| Setup | enable in the agent (or a smith's config) | register one server URL per project |
| During a run | invisible: the run just continues | invisible: Ingram Cloud calls your server and continues |
Hosted tools: list the catalog at GET /v1/tenant/hosted_tools, enable them on
an agent or a smith via
enabled_hosted_tools. Their results just consume model tokens like any other
context. (The console labels these Built-in tools on the Tools page.)
A catalog entry is either a single function (web_search, web_fetch) or a
bundle — one name that lights up a family of related functions: enabling
calculator gives the smith arithmetic (add, subtract, multiply, divide,
powers, roots, …), and reasoning gives it a think/analyze scratchpad for
working through a problem before answering.
read_budget lets a smith read its own budgets for the month —
the limit, spend-to-date, and remaining for each scope that applies to it
(project, agent, smith, customer). It's read-only: the cap is enforced
server-side at run start, and reading it never changes a limit. Enable it when
you want a smith to pace itself against its cap or tell the user when it's
running low. Today these caps meter inference (model) cost.
manage_schedules lets a smith create, list, and delete its own
schedules — so "wake me up tomorrow at 8am"
becomes a real cron entry the smith sets for itself. Every action is scoped to
the calling smith: it can only ever see or change its own schedules, never
another smith's. On create it must pass an IANA timezone, and the tool pushes
the model to ask the user for theirs — a cron without the right zone fires at the
wrong hour. Off by default; enable it per agent when you want self-scheduling.
Cost is bounded the usual way, by the smith's and agent's budgets,
so there's no separate rate limit on the tool.
file_search is semantic search over the agent's attached
vector stores — its knowledge base. Enable the tool and set
vector_store_ids on the agent; the smith can then pull relevant passages from
your documents on any channel. A smith can only search tenant-wide stores or
stores owned by itself.
A third kind: client-side inline
tools. The hosted and MCP tools above are server-side — Ingram Cloud executes them and the run just continues. On the OpenAI-compatible surface you can instead send atoolsarray on the request and run a client-side function-call loop: the model's calls come back for you to execute, you feed the results in, IC runs nothing and gates nothing. It's the literal OpenAI contract, stateless (re-send the conversation each turn), and a turn that sendstoolsruns only those client tools (the agent still supplies instructions; its MCP/hosted tools and memory sit out that turn). Reach for it when the tool already lives in your process; reach for MCP (below) when the tool is shared, remote, or needs approval gating. Full reference: OpenAI-compatible API → Tools.
MCP tools are project-global: every tool a registered server advertises is offered to every smith in the project (subject to the allow-list). There is no per-smith or per-agent scoping yet. Use the tool's own description and the instructions to steer who calls what, or split products into separate projects. (Who a tool can act as is per-smith; see auth modes below.)
Two ways to add an MCP server
| Raw URL | Curated catalog | |
|---|---|---|
| You provide | a server URL + auth mode | one catalog slug |
| Best for | your own backend, or any third party | popular third parties (Stripe, GitHub, Notion) |
| Auth | you wire it | the OAuth endpoints, scopes, and a sane default policy ship pre-solved |
Both produce the same kind of MCP server resource — the catalog just pre-fills the fields (see catalog).
Registering an MCP server (raw URL)
You host an MCP server (Streamable HTTP, stateless) that exposes your tools.
Register its URL once per project; Ingram Cloud immediately discovers the tool
manifest (tools/list) and caches it:
# Authorization: tenant-admin token (server-side only)
curl -X PUT https://api.cloud.ingram.tech/v1/tenant/mcp/acme \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "url": "https://acme.example.com/mcp",
"auth": { "kind": "oauth", "provider": "acme" } }'
# → 200 { "name": "acme", "url": "…", "auth": { "kind": "oauth", "provider": "acme" },
# "tools": [ { "name": "book_expense", "requires_approval": true }, … ],
# "tools_discovered": 7 }
PUT is a full replace, so auth.kind is required unless a catalog preset
supplies it — a body without it is rejected (auth_required, 422) rather than
silently resetting a live registration to unauthenticated calls. Re-PUT (or
POST /v1/tenant/mcp/{name}/refresh) whenever your manifest changes. GET /v1/tenant/mcp lists; GET /v1/tenant/mcp/{name} shows one; DELETE /v1/tenant/mcp/{name} removes. Every register, replace, and delete lands in the
event log (mcp_server.created / mcp_server.updated /
mcp_server.deleted) with the auth transition, so a change to how Ingram Cloud
authenticates to your server is always auditable — and webhook-alertable.
When discovery fails, the server says so. Registration is never rolled back —
your server may simply not be reachable yet — but a failed tools/list (or a
stored static secret that can no longer be decoded) is recorded on the server:
status reads "degraded" and discovery_error carries the reason. The same is
true if the edge breaks at run time (e.g. a secret that stops decoding) — the
run loop skips only that server, keeps the rest, and flips it to degraded so the
list stops reporting a broken edge as active. A clean
POST /v1/tenant/mcp/{name}/refresh re-runs the probe end to end and clears it:
# Authorization: tenant-admin token (server-side only)
curl -X POST https://api.cloud.ingram.tech/v1/tenant/mcp/acme/refresh \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01"
# → 200 { "name": "acme", "status": "active", "discovery_error": null,
# "tools": [ … ], "tools_discovered": 7 }
# A still-broken edge instead returns the coded error `discovery_failed`.
Discovery self-heals — you rarely need to refresh by hand. Registering before
your server is live (the natural order when you provision with IaC) leaves the edge
degraded with no tools. You don't have to re-register once it comes up: a
degraded or empty server re-runs tools/list automatically at the start of the
next run that would use it, so the first run after your server is reachable picks up
its tools. The retry is throttled (at most once a minute per server) so a
persistently-down server never slows your runs. POST …/refresh is still there for
when you want to force it immediately. tools_refreshed_at records the last
successful discovery — an empty tools list with a set tools_refreshed_at means
"the server is up and exposes no tools", distinct from a discovery_error that
never succeeded.
Call failures are tracked separately from discovery. Discovery can succeed
while every tools/call fails — a common shape is an open tools/list in front
of an authenticated call path, where a wrong auth config 401s every call but
the registration still looks healthy. A tools/call that fails at the transport
level (network error or non-2xx HTTP status) is recorded on the server as
last_call_error (+ last_call_error_at) and flips status to "degraded",
so the break is visible on a GET instead of only surfacing as an agent whose
tools "error". A later successful call — or a re-PUT — clears it.
In the console: the Tools page lists your MCP servers alongside
the read-only built-in catalog. Register or edit one (URL + auth mode), and
the discovered tool count is shown after the save probes tools/list; use
Refresh to re-probe. A degraded server shows a red status badge and the
discovery_error or last_call_error reason inline, so a broken edge is visible
at a glance rather than surfacing as an agent that silently has no tools —
whether discovery or the calls themselves are failing. Because static secrets
are write-only, re-enter the shared secret whenever you save a static-auth
server.
Auth: how a tool call is authenticated
Each call Ingram Cloud makes to your server carries auth per the auth.kind you
register:
kind | Ingram Cloud sends | Use when |
|---|---|---|
none | nothing | public tools, no per-smith data |
static | Authorization: Bearer <secret> you set, plus X-IC-Smith-External-Id (the external_id you gave the smith), X-IC-Smith-Id, X-IC-Tenant | your server authorizes against your own store by user id |
oauth | Authorization: Bearer <the end-user's access token> from that smith's connection for provider (refreshed if stale) | the tool must run as the human, e.g. straight into a row-level-security context |
oauth is the strict per-smith model: the token is the end-user's own, so
the smith inherits exactly that human's permissions. No service-role token is
ever sent. The smith's token comes from a connection;
the cleanest way to obtain one is the hosted connect flow — IC drives the
consent dance and vaults the token for you (see
connecting an end-user's account).
If a smith has no connection yet, the tool call returns an error to the model
and Ingram Cloud fires a connection.required event carrying
a ready-to-use authorize_url so you can prompt them to connect; once connected,
submit the paused run to continue.
Whose OAuth client brokers consent is auth.client_mode: platform (an
Ingram-owned client — the default for catalog presets, so the consent screen
reads "Ingram Cloud") or tenant (your own client from
PUT /v1/tenant/providers/{provider}, so it carries your brand). Either way IC
owns the redirect and the token vault.
static is the cheap path: the person's identity rides in
X-IC-Smith-External-Id (the id you assigned), so your server maps it to your
own user without any token plumbing.
Curated catalog (preloaded integrations)
For popular third parties, Ingram Cloud maintains a small catalog of presets
so you don't re-solve the same OAuth + scope + risk config. GET /v1/catalog
lists them; each entry pre-fills a server's URL, auth mode, OAuth endpoints, a
default allow-list, and a default
approval policy. Enable one by passing its catalog slug
instead of a url:
# Authorization: tenant-admin token (server-side only)
curl -X PUT https://api.cloud.ingram.tech/v1/tenant/mcp/stripe \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "catalog": "stripe" }'
# → 200 { "name": "stripe", "origin": "catalog", "catalog_slug": "stripe",
# "auth": { "kind": "oauth", "provider": "stripe", "client_mode": "platform" },
# "tool_allowlist": ["list_charges", "get_balance", …],
# "approval_policy": [ { "match": "create_refund", "require": "approval" }, … ],
# "tools": [ … ], "tools_discovered": 7 }
The preset's fields are copied onto your server at enable time — editing the
catalog later never silently re-points your live integration. You can override
any copied field in the same PUT (e.g. send your own tool_allowlist,
approval_policy, or auth.client_mode). Catalog OAuth presets default to
client_mode: "platform", so a smith connects
with one call and no OAuth client of your own.
A catalog entry is curated by Ingram; the long tail of servers is covered by the raw-URL path. Both yield the same resource.
Tool allow-list (default-deny)
A server's manifest is what it advertises; the allow-list is what your
smiths may actually call. Set tool_allowlist to the exact tool names to
expose — anything else is denied, including tools the server adds later:
# Authorization: tenant-admin token (server-side only)
curl -X PUT https://api.cloud.ingram.tech/v1/tenant/mcp/stripe \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "catalog": "stripe", "tool_allowlist": ["get_balance", "list_charges"] }'
Omit tool_allowlist (or send null) to expose every discovered tool. This is
the supply-chain guard for third-party servers: because a server you don't own
can rename or add tools under you, a non-null allow-list means a new tool is
never auto-exposed to your smiths — you opt it in. In GET/PUT responses,
each tool carries an enabled flag reflecting the allow-list.
Scoping servers to agents
Registered servers are project-wide, and by default every agent's smiths load
all of them. To expose a server to only some agents, set mcp_servers on the
agent — the list of server names its smiths load:
# Authorization: tenant-admin token (server-side only)
curl -X PATCH https://api.cloud.ingram.tech/v1/agents/agt_123 \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "mcp_servers": ["sheets"] }'
null (the default) loads all registered servers; [] loads none. The value
is a behaviour-config field like enabled_hosted_tools: publishing freezes it
into the version, and a smith may override it (PATCH the smith; null clears
the override and re-inherits). A listed name that matches no active server is
reported in the run's tool inventory (run.metadata.tools.errors), not
silently skipped. The allow-list above still applies within each server.
The MCP loop
When a smith calls one of your tools, Ingram Cloud performs a live MCP
tools/call against your server, with the auth above, and feeds the result
(content / structuredContent) back to the model, all in-process, no pause.
A tool that returns an MCP error result is surfaced to the model as text, so it
can recover or explain rather than the run hanging.
Approval gating
A tool call can be gated behind a human approval two ways:
- The server's own hint — mark a tool
annotations.destructiveHint: truein yourtools/listresponse. Good for servers you own. - Your
approval_policy— a list of rules on the server resource, independent of what the server advertises. This is the path for third-party servers whose annotations you can't change:
# Authorization: tenant-admin token (server-side only)
curl -X PUT https://api.cloud.ingram.tech/v1/tenant/mcp/stripe \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "catalog": "stripe",
"approval_policy": [ { "match": "create_refund", "require": "approval" },
{ "match": "delete_*", "require": "approval" } ] }'
Each rule's match is a glob over the tool name; a match gates the call. The
effective gate is the server hint OR any policy match. (Arg-conditional
thresholds — e.g. "only refunds over $500" — are a planned extension; today
match is a name glob, and a rule with a when clause is rejected at register
time rather than silently ignored.)
Either way, every gated call pauses the run (paused_for_approval) and creates
an approval (a first-class resource that can outlive the run) before
Ingram Cloud calls your server:
- The stream emits
approval.required({ approval_id, tool, args, tool_call_id }), and the event feed + webhooks carry the same. GET /v1/approvals?status=pendinglists what's waiting, project-wide (a smith token sees only its own smith's). Each approval carries everything needed to act on it:
{ "id": "apr_…", "run_id": "run_…", "smith_id": "smt_…",
"tool_call_id": "tc_…", "tool": "book_expense", "args": { },
"status": "pending", "actor": null, "reason": null,
"created_at": "…", "resolved_at": null }
Resolve by submitting via /submit:
# Authorization: tenant-admin token (server-side only)
curl https://api.cloud.ingram.tech/v1/smiths/smt_…/runs/run_…/submit \
-H "Authorization: Bearer $IC_TOKEN" \
-H "IC-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{ "kind": "approval_decision", "approval_id": "apr_…",
"decision": "approve", "actor": "ops@example.com" }'
"approve" resumes the run and Ingram Cloud then calls your MCP server for that
tool. "reject" completes the run with stop_reason: "approval_rejected". The
tool never runs. Pass actor so the audit trail says who decided.
Who may approve: any token holding approvals:write and access to the
run, so a tenant-admin token always can, and a smith token can approve its own
smith's calls (that's deliberate: approval gating doubles as end-user consent,
e.g. the Telegram inline buttons). For operator-only gates, simply don't include
approvals:write in the smith tokens you mint.
The approvals queue in the console
Observe → Approvals is the human side of the same queue: pending calls with their arguments, one-click approve/reject, and a resolved log (tool, decision, actor, when). The sidebar badge counts pending approvals so a paused run is never invisible.