Ingram Cloud

Documentation

Tools & approvals

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

HostedMCP
Runs onIngram Cloud, in-processyour MCP server
Examplesweb_search, web_fetch, calculator, reasoning, read_budget, manage_schedules, file_searchanything: your DB, your APIs
Setupenable in the agent (or a smith's config)register one server URL per project
During a runinvisible: the run just continuesinvisible: 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 a tools array 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 sends tools runs 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 URLCurated catalog
You providea server URL + auth modeone catalog slug
Best foryour own backend, or any third partypopular third parties (Stripe, GitHub, Notion)
Authyou wire itthe 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:

kindIngram Cloud sendsUse when
nonenothingpublic tools, no per-smith data
staticAuthorization: Bearer <secret> you set, plus X-IC-Smith-External-Id (the external_id you gave the smith), X-IC-Smith-Id, X-IC-Tenantyour server authorizes against your own store by user id
oauthAuthorization: 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:

  1. The server's own hint — mark a tool annotations.destructiveHint: true in your tools/list response. Good for servers you own.
  2. 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=pending lists 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.