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, reasoninganything: 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.

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; 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:

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.

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.