Skip to main content

Tools

Capabilities an agent can call — an HTTP endpoint, an MCP server, your own client, or a pipeline of the three.

Overview​

A tool is what an agent calls out to. It is registered per project and attached to an agent through its tool_bindings — by reference, or inline for a one-off. Gate a tool with guardrails binds an http tool by reference.

typeWhat it calls
httpAn HTTP endpoint you name, with placeholders, headers and a body mode.
mcpAn MCP server, optionally scoped to an allowlist of its tools.
clientNothing server-side: the run pauses and your code supplies the result.
pipelineAn ordered sequence of other tools, wiring each step's input from earlier results.

This module is a verbatim mirror of the runtime: every field, method, status code and error shape is the runtime's own, re-rooted under the project in the path.

See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Tools.

Data Model​

Tool​

FieldTypeDescription
idstringPublic tool ID (tool_ prefix).
project_idstringThe owning project.
namestringHuman-readable label.
descriptionstring, nullable
typehttp | client | mcp | pipelineThe tool's execution kind. Defaults to http.
parametersobjectJSON-Schema-shaped parameters the tool accepts.
executeobject, nullableHTTP execution config (type: http only): url (required, may carry {param} placeholders), method, headers, body_mode.
output_mappingobject, nullableJSON Logic mapping applied to the raw result — see output_mapping.
preset_parametersobject, nullableParameters pinned on the tool, on every call it makes, whatever its type. A pinned key is removed from the schema the model sees, and its value wins over one the model or a direct caller supplies. Values accept {{context:<key>}} — see Per-call context in preset_parameters.
mcpobject, nullableMCP server config (type: mcp only).
actionsstring[], nullableAllowlist of MCP actions exposed (type: mcp only).
denied_actionsstring[], nullableDenylist of MCP actions (type: mcp only).
context_keysstring[], nullableWhich context keys may reach this tool as headers. null forwards every key the caller supplied. See Per-user credentials.
pipelineobject, nullableStep definition (type: pipeline only).
guardrail_idsstring[], nullableGuardrails attached at the tool scope, governing this tool wherever it is used.
created_atstring (date-time)
updated_atstring (date-time)

Key Concepts​

Secrets and per-call context in headers​

A tool's own headers — on execute for http, on mcp for MCP — accept two kinds of reference, resolved at call time rather than stored resolved: {{secret:<id>}} pulls a secret, and {{context:<key>}} pulls a value from the call's tool_context. A static credential belongs in the first; a per-user one in the second.

Per-call context in preset_parameters​

{{context:<key>}} also resolves inside preset_parameters values, not just headers — so a pin can be the caller's own value instead of one fixed when the tool was created. This is how a tool pins a parameter to "the one account this run may act on" rather than a value baked in at write time:

{ "preset_parameters": { "account_id": "{{context:accountId}}" } }

The resolved value is retyped to the parameter's declared schema type — a tool_context entry is always a string, but a parameter typed integer gets an integer. A key missing from the call's tool_context fails the call with MISSING_TOOL_CONTEXT_KEY, the same fail-closed behavior as a missing header reference, rather than sending the literal placeholder through.

{{secret:<id>}} is not resolved in preset_parameters — only in headers. A secret belongs in a header a tool's own endpoint reads, never in a parameter value the model or the tool call log can see.

Per-user credentials reach a tool as a header​

A tool endpoint often needs to act as the person the run is for, not as the project: one user's calendar, one user's orders, one user's account on a third-party API. Putting that credential in the tool's own headers cannot express it — those headers are fixed at write time and shared by every call.

The credential travels per call instead, in the tool_context a caller attaches when it starts work. The runtime forwards each entry as a request header on every http/mcp tool call the agents make, named X-Naturali-Context- followed by your key verbatim:

X-Naturali-Context-ocaToken: usr_token_V1StGXR8Z5jdHi6B

Two properties are worth reading twice. The key is appended verbatim — no character is re-cased and no separator is inserted, so ocaToken and oca_token are two different keys producing two different headers. And the prefix is always applied: a caller cannot name an arbitrary header, which is what stops caller-supplied data from overwriting a credential the tool configured for itself.

Read the header case-insensitively. HTTP field names are case-insensitive and HTTP/2 sends them lowercased, so your endpoint will usually see x-naturali-context-ocatoken however you spelled the key:

// Node / Express / Koa — incoming header names are lowercased for you
const ocaToken = req.headers['x-naturali-context-ocatoken'];

Landing the value in Authorization​

Most targets want the credential in the header they already use, not in a prefixed one. The tool says so, with a {{context:<key>}} reference in its own headers — the tool knows the header shape its endpoint expects, and the caller only supplies the value:

Authorization: 'Bearer {{context:ocaToken}}'

A reference is legal in headers and nowhere else, url included: a context value comes from the caller, so it must never steer the outbound request. If the key is missing from the tool_context at call time the tool call fails, rather than sending Bearer with nothing after it and coming back as an opaque 401 several steps from the mistake.

Confine it with context_keys​

By default every key in the bag goes to every tool the agent can call, including endpoints you do not control. context_keys is the allowlist that stops that: set it to the keys a tool legitimately needs, and a credential meant for one endpoint stops egressing to the rest of the tool set. [] forwards none, and null (the default) forwards all.

A key the tool consumes through a {{context:...}} reference in its own headers is substituted either way — the tool declared that header itself.

Create such a tool — a per-user credential in Authorization, and nothing else in the bag reaching it:

naturali create-tool \
--project-id proj_V1StGXR8Z5jdHi6B \
--name oca \
--type mcp \
--mcp '{"url":"https://mcp.example.com/sse","headers":{"Authorization":"Bearer {{context:ocaToken}}"}}' \
--context-keys ocaToken

Then supply the value when you start the work: tool_context is a field on a session (forwarded on every call in that session), on a single agent generation, on a direct call-tool request, and — write-only — on an eval run and on a trigger, which is what puts an agent built this way on a schedule: a firing has no caller to carry a credential, so the trigger holds the bag, and a {{secret:...}} reference in it keeps the value in the secret store.

The headers are worth trusting only if the endpoint also authenticates the request as coming from your naturali deployment — a shared secret in the tool's own headers, mTLS, or a network restriction. Anyone who can reach the endpoint can set an X-Naturali-Context-* header by hand.

When a tool does not resolve​

An mcp binding is resolved at the start of every turn by listing the server's tools. When that listing does not arrive — the server unreachable or refusing the credential, a {{secret:…}} in the URL or headers that does not resolve — or arrives and leaves nothing to attach, the binding is dropped and the turn runs without it. One flaky server does not take an agent down.

The generation still completes, carrying no error. The model is told which of its tools are unavailable for the turn, and instructed to say so plainly rather than claim it lacks the capability — but it is not told why. The reason is operator-grade and would become text the model may repeat to an end user, so it goes to the activity feed instead. An expired token therefore reads to your end user as a data source that could not be reached, not as a product limitation, and not as a cause the model guessed at.

The record is an activity entry of kind tool_resolution_failed, severity warning, naming the binding, the reason and the generation. Nothing else reports it: poll or alert on ?kind=tool_resolution_failed.

An actions allowlist that excludes every tool the server returns drops the binding too, and says so in detail.reason — a binding that legitimately matches nothing is indistinguishable from a broken one until you read it.

Zero tool calls is not proof a restriction held​

A generation that completes without calling a tool looks identical whether actions withheld the tool or the credential behind it was dead. Asking the agent for the forbidden thing and watching it decline does not tell the two apart. The activity feed does: an entry for that binding means it was dropped, and the refusal you observed says nothing about the allowlist. No entry means the binding attached what the allowlist leaves, and the model had those tools to call.

Calling a tool directly​

POST /v1/projects/{project_id}/tools/{tool_id}/call invokes a tool without an agent — the fastest way to check that a tool's URL, headers and mapping are right before binding it. http, mcp and pipeline tools are supported; a client tool answers 422, because there is no agent run to pause and nobody to hand the call to. For an mcp tool, action names the MCP tool to invoke, and an action outside the tool's actions allowlist is rejected before any outbound request is made. It is also what tells a broken tool from a model calling a working one badly — see Debug a failed run.

The request body also takes its own tool_context, narrowed by the tool's context_keys allowlist like any other caller — there is no session or generation behind this call, so session_id, actor_id and actor_external_id are dropped from the bag rather than forwarded.

A direct call is a run, like every tool call (Runs and the allowance). On Free, once the account has used its runs for the month, it answers 403 plan_limit_reached with resource: "runs" and calls nothing. A negative credit balance does not refuse it: a tool call spends no credit. Not in the generated spec.

client tools pause the run​

A client-type tool has no server-side execution: when the model calls it the generation stops with requires_action and the pending call on it. Your code does the work and posts the result back — to …/agents/{agent_id}/generate/{generation_id}/tool-outputs for a one-shot run, or …/sessions/{session_id}/tool-outputs inside a session — and the run continues from where it stopped. Run tools in your own code walks the pause, the submit and the resumed run.

output_mapping sees the result and the input​

output_mapping's JSON Logic is evaluated over { output: <raw result>, input: <merged input> }, for every tool type. So { "var": "output.text" } extracts a bare scalar instead of returning the whole envelope, and { "var": "input.title" } echoes back a field of the request that produced the response — which is how you carry a value through a call that does not return it. For a pipeline tool it runs after the pipeline's own output mapping, over that mapping's result.

http execution always sends a body, even for GET​

An http tool's resolved parameters are sent as the request body regardless of execute.method. Most servers ignore a body on a GET request, but some reject it outright; if a GET tool call fails against a target that behaves this way, that is usually why — switching the target's own route to accept POST, where one is available, sidesteps it.

Examples​

naturali create-tool \
--project-id proj_V1StGXR8Z5jdHi6B \
--name get-weather \
--type http