Server tools
The platform's built-in tools — web search, URL fetch, code execution — and the contract they share.
A server tool runs on Inlay, not in your app. The agent calls it, the server executes it, and your client sees the call as an observability row — no handler, no pause. (The other kind — tools your app implements — is Client tools.)
Attach a server tool to an llm or for_each_llm node by name in its tools
list, exactly like a client tool. Three are built in:
| Tool | What it does | Metered as |
|---|---|---|
web.search | Web search (Tavily-backed), curated results | 1 credit per basic/fast/ultra-fast search, 2 per advanced |
url.fetch | Read one URL as clean text | 1 call |
code.exec | Run a Starlark snippet in a deny-all sandbox | Seconds (rounded up) |
Two more families are server tools too — MCP connectors (mcp.<alias>.<tool>)
and subagents-as-tools (agent.<alias>) — covered by
MCP connectors and the subagent picker on the canvas.
web.search
Searches the web and returns curated, capped results. Requires the deployment
to have a Tavily key configured (INLAY_TAVILY_API_KEY) — without it the tool
is not registered at all, so a graph referencing it fails at build with
unknown-tool rather than failing mid-run.
Args: query (required), max_results (1–20, default 5), search_depth
(basic default, advanced, fast, ultra-fast), topic
(general/news/finance), include_answer, include_domains,
exclude_domains. Results are aggressively bounded (per-result and total
content caps, a truncated marker when they bite) so a search cannot blow up
the model's context.
Node config (tool_configs):
{ "web.search": { "search_depth_cap": "basic" } }search_depth_cap: "basic" forbids the 2-credit advanced depth — the model
asking for it gets a teachable invalid-args error it can correct.
url.fetch
A reader, not an HTTP client: it fetches one absolute http(s) URL and
returns the page as clean text (HTML is converted, JSON pretty-printed, other
content types refused), delivered in a clearly-delimited untrusted-content
block. Args: url (required), max_chars (1–50,000, default 12,000). No
method, headers, or body — it cannot POST. No config surface.
It rides the strict egress guard (below) with no exceptions, ever.
code.exec
Runs a snippet in an embedded Starlark sandbox —
deterministic arithmetic, parsing, reshaping — with no filesystem,
network, or environment access. Args: source (required, ≤ 64 KiB),
input (a JSON string exposed as the INPUT global), language
("starlark" only). The script's top-level result variable is the return
value; stdout is capped and returned alongside.
Node config tightens the sandbox per node (defaults shown):
{ "code.exec": { "fuel_cap": 100000, "heap_bytes": 16777216, "wall_timeout_ms": 5000 } }Exhausting fuel/heap/time is a curated tool error the model can react to, not
a run failure. code.exec takes no secrets in v1 — a secret_ref argument is
rejected at validation.
tool_configs
Per-node configuration lives in the node's tool_configs — a map from tool
name to its config object:
{
"tools": ["web.search", "code.exec"],
"tool_configs": {
"web.search": { "search_depth_cap": "basic" },
"code.exec": { "wall_timeout_ms": 10000 }
}
}Configs are validated at graph build: an entry for a tool the node does
not attach, an unknown tool, a tool with no config surface, or a value outside
the schema all fail the build — a dead config cannot silently do nothing.
Configs never carry secret material; where a tool needs credentials it names a
secret by reference (secret_ref).
Today, configs are written in the graph JSON itself (the dashboard's schema-driven dialogs are follow-up work; the subagent picker's gear dialog is the one exception). Existing values round-trip through the canvas untouched.
Metering
Every successful tool call meters into the usage ledger with a unit —
web.search by credits (per the depth table above), url.fetch and the
connector/subagent bridges by calls, code.exec by seconds. Failed
dispatches meter zero. What a unit costs comes from the deployment's rate
card ([tools."web.search"] per_credit_usd = …); absent entries are free,
never blocked. Usage grouped by tool is on the operator usage route
(GET /v1/usage?group_by=tool) and flows into invoices as
web.search · 41 credits lines.
Those are metering and deployment-configuration rules, not an Inlay price list. Confirm commercial terms during onboarding.
The redaction contract
Server tool calls appear in your timeline as tool-call / tool-result row
pairs with requiresClientResponse: false. By default they are redacted:
the real tool name is visible, but arguments arrive as "{}" and the result
content is empty — so a private lookup never leaks its query into your
client's log, and your UI can still show that the agent searched. (The full
args and outcomes belong to the operator trace surface, not the public
stream. See Observability for access and capture limits.)
mcp.* and agent.* attachments can opt into full-fidelity rows per node
with wire_verbosity: "full" — for connectors you trust with your users'
eyes.
The egress guard
Anything that fetches a URL — url.fetch, MCP connectors — goes through one
guard: http(s) only; loopback, link-local (including the cloud metadata
address), and other non-public ranges blocked at the DNS layer so rebinding
tricks don't work; redirects re-validated per hop; response bodies capped.
MCP connectors to private network addresses are an explicit operator
opt-in per connector; url.fetch never lifts the floor.
Next
MCP connectors — connect tools from compatible Streamable HTTP servers.