Tools
External capabilities an agent can call — an HTTP endpoint or an MCP server.
Overview
A tool is what an agent of yours calls out to. For the opposite direction — connecting Claude, Cursor or another MCP client to your naturali account — see MCP Server.
A tool is a thin surface onto a runtime tool. What the tool is (name,
parameters, execution config) lives on the runtime and is read from there when
shaping responses; naturali stores nothing of its own about it. Attach tools to
an Agent via its tool_bindings.
See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Tools.
Data Model
Tool
| Field | Type | Description |
|---|---|---|
id | string | Public tool ID (tool_ prefix). |
project_id | string | The owning project. |
name | string | Human-readable label. |
description | string, nullable | |
type | http | mcp | The tool's execution kind. |
parameters | object | JSON-Schema-shaped parameters the tool accepts. |
execute | object, nullable | HTTP execution config (type: http only). |
output_mapping | object, nullable | How to map the raw response into a result. |
preset_parameters | object, nullable | Parameters fixed by the platform rather than the caller. |
mcp | object, nullable | MCP server config (type: mcp only). |
actions | string[], nullable | Allowlist of MCP actions exposed (type: mcp only). |
denied_actions | string[], nullable | Denylist of MCP actions (type: mcp only). |
context_keys | string[], nullable | Which context keys may reach this tool as headers. null forwards every key the caller supplied. See Per-user credentials. |
has_headers | boolean | Whether execute/MCP auth headers are configured — never returns the header values themselves. |
created_at | string (date-time) | |
updated_at | string (date-time) |
Key Concepts
Auth headers are write-only
Auth headers on a tool's execute/mcp config are accepted on write, never
returned — a read reports only has_headers.
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:
- CLI
- SDK
- curl
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
const { data: tool } = await naturali.tools.createTool({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: {
name: 'oca',
type: 'mcp',
mcp: {
url: 'https://mcp.example.com/sse',
headers: { Authorization: 'Bearer {{context:ocaToken}}' },
},
context_keys: ['ocaToken'],
},
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/tools \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"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 — see Orchestrations → Handing a run a per-user credential.
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.
Deleting a tool still in use
Deleting a tool still bound to an agent returns 409 tool_in_use. Detach it
from every agent's tool_bindings first.
output_mapping sees the raw response only
output_mapping's JSON Logic is evaluated against
the raw HTTP response body directly — a var path has no result. prefix
({ "var": "data.0.score" }, not { "var": "result.data.0.score" }; that
prefixed form is Boards' on_complete syntax, a different
evaluation context). It has no access to the call's own resolved
parameters/input_mapping either, so it cannot echo a request field back
into the result — only the target endpoint's response shape decides what
comes back. To carry a field through a call that does not return it, see
Boards → carrying a field past a column that doesn't return it.
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
- CLI
- SDK
- curl
naturali create-tool \
--project-id proj_V1StGXR8Z5jdHi6B \
--name get-weather \
--type http
const { data: tool } = await naturali.tools.createTool({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: { name: 'get-weather', type: 'http' },
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/tools \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "get-weather", "type": "http" }'