Tools and MCP
Tier 0 — the tool catalog, invocation, registering MCP servers, and the allowlist.
import { Aside } from '@astrojs/starlight/components';
GET /v1/tools list the catalog
GET /v1/tools/{name} read one tool's schema
POST /v1/tools/invoke invoke
POST /v1/mcp/servers register an MCP server
GET /v1/mcp/servers list registered servers
DELETE /v1/mcp/servers/{id} deregister
POST /v1/mcp/servers/{id}/refresh re-read the server's tool list
Tier 0 runs registered tools and nothing else. The catalog is also reachable from tiers 1–4, so one definition of "what this agent may do" holds across every tier.
The catalog
curl -sS "$WB_BASE_URL/tools" -H "Authorization: Bearer $WB_API_KEY"
{
"object": "list",
"data": [
{
"name": "http.fetch",
"source": "builtin",
"description": "Perform an HTTP request to an allowlisted destination.",
"input_schema": {
"type": "object",
"required": ["url"],
"properties": {
"url": { "type": "string", "format": "uri" },
"method": { "type": "string", "enum": ["GET","POST","PUT","PATCH","DELETE"], "default": "GET" },
"headers": { "type": "object" },
"body": { "type": "string" }
}
},
"output_schema": { "type": "object", "properties": { "status": {"type":"integer"}, "body": {"type":"string"} } },
"side_effects": "network",
"avg_latency_ms": 180
},
{
"name": "crm.contact.get",
"source": "mcp:mcps_4Jd8Pq",
"description": "Look up a contact by email.",
"input_schema": { "type": "object", "required": ["email"], "properties": { "email": {"type":"string"} } },
"side_effects": "none",
"avg_latency_ms": 41
}
]
}
The catalog is designed to be passed straight to a model as tool definitions. input_schema is JSON
Schema, which is what every model tool-calling API expects.
side_effects is none, network, write or destructive. Use it to decide which tools need
confirmation before an agent may call them.
Invoke
curl -sS "$WB_BASE_URL/tools/invoke" \
-H "Authorization: Bearer $WB_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"tool": "crm.contact.get",
"arguments": { "email": "ada@example.com" },
"end_user_id": "eu_customer_4821",
"timeout_ms": 10000
}'
{
"id": "tcall_3Jd9Pq",
"object": "tool_call",
"tool": "crm.contact.get",
"status": "succeeded",
"result": { "id": "c_118", "name": "Ada Lovelace", "plan": "scale" },
"duration_ms": 41,
"usage": { "tool_call": 1, "tool_call_ms": 41 }
}
Send an idempotency key on any tool with side effects. A retried email.send without one sends a
second email.
From a higher tier
curl -sS "$WB_BASE_URL/sessions/$SESSION/exec" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{ "kind": "tool", "tool": "crm.contact.get", "arguments": { "email": "ada@example.com" } }'
The call is attributed to the session for tracing, but is still metered as tool_call rather than
session time. A tier 2 session calling a tool does not pay tier 2 rates for the tool's duration.
Built-in tools
| Tool | Side effects | Notes |
|---|---|---|
http.fetch | network | Subject to the session's egress policy |
search.web | network | Platform search provider |
storage.get, storage.put | write | Project-scoped object storage |
vector.query, vector.upsert | write | Project-scoped vector index |
email.send | destructive | Requires a verified sender domain |
schedule.callback | write | Calls a webhook at a future time |
Built-ins run in-process. http.fetch is still bound by the project's egress allowlist, which is
the point: it is not a bypass of the network policy, it is the same policy evaluated in a different
place.
Registering an MCP server
curl -sS "$WB_BASE_URL/mcp/servers" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{
"name": "crm",
"transport": "http",
"url": "https://mcp.internal.acme.com",
"auth": { "type": "bearer", "secret_ref": "sec_crm_token" },
"tool_prefix": "crm.",
"timeout_ms": 10000,
"max_concurrency": 20
}'
{
"id": "mcps_4Jd8Pq",
"name": "crm",
"state": "connected",
"tools_discovered": 14,
"tools": ["crm.contact.get", "crm.contact.update", "crm.deal.list", … ]
}
| Field | Notes |
|---|---|
transport | http (streamable HTTP) or sse |
url | Must be reachable from the platform; private servers need private connectivity |
auth | bearer, oauth2, none. Credentials are secret refs, never inline |
tool_prefix | Namespacing, so two servers can both expose contact.get |
timeout_ms | Per call; the circuit breaker opens after repeated timeouts |
max_concurrency | Ceiling on in-flight calls, so one server cannot exhaust the pool |
The platform holds the connection pool, the timeout, the retry policy and the circuit breaker. A slow MCP server degrades its own tools and nothing else.
POST /mcp/servers/{id}/refresh re-reads the tool list. Tools that disappear are marked
unavailable rather than removed, so a transient server problem does not silently shrink an agent's
capability set.
The tool allowlist
Which tools a session may invoke is policy, not catalog membership.
# policy: pol_support_agent
tool_allowlist:
mode: allowlist
allow:
- crm.contact.get
- crm.deal.list
- search.web
- "ticket.*" # prefix match
deny:
- crm.contact.delete # deny wins
require_confirmation:
- email.send # 202, waits for an approval callback
require_confirmation returns 202 with a confirmation ID instead of executing:
{
"id": "tcall_3Jd9Pq",
"status": "awaiting_confirmation",
"confirmation_id": "conf_8Pq2Vd",
"expires_at": "2026-10-06T09:34:11Z"
}
curl -sS "$WB_BASE_URL/tools/confirmations/conf_8Pq2Vd" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{ "decision": "approve" }'
This is the mechanism for human-in-the-loop on destructive tools, and it lives in the platform rather than in each customer's agent loop so it cannot be forgotten.
Writing a safe tool
Tier 0 has no sandbox. The handler is the boundary, so the handler's design is the security control.
| Do | Do not |
|---|---|
| Take typed, constrained arguments: an ID, an enum, a bounded string | Take a URL, a SQL string, a file path, a shell command |
| Enumerate what the tool can affect | Accept a resource identifier that can address anything |
Enforce authorization inside the handler, against end_user_id | Assume the caller already checked |
| Set a tight timeout and a small result size | Return unbounded data |
| Make the tool idempotent, or honour the idempotency key | Make a non-idempotent tool and hope retries do not happen |
A run_query tool that takes arbitrary SQL is tier 1 work with tier 0's boundary. If a tool needs
to express arbitrary computation, it belongs at tier 1, where there is a sandbox behind it.
Metering
| Meter | Unit |
|---|---|
tool_call | count, per invocation, success or failure |
tool_call_ms | handler wall time |
egress_gb | only when a built-in handler makes an outbound call |
No session_s — there is no session. That absence is the whole economic argument for tier 0.
Errors
| Code | Status | Meaning |
|---|---|---|
tool_not_found | 404 | Not in the catalog, or not visible to this project |
tool_not_allowed | 403 | Blocked by the allowlist; meta.rule names it |
tool_invalid_arguments | 422 | Schema validation failed; meta.errors lists the fields |
tool_timeout | 504 | Handler exceeded timeout_ms |
tool_unavailable | 503 | Circuit breaker open for this MCP server |
tool_error | 502 | Handler returned an error; meta.handler_error carries it |