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 | 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.
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; 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.
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`.
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 reason inline, so a broken edge is visible at a glance rather
than surfacing as an agent that silently has no tools. 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.
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.