Skip to main content
Tools are the primary way your agent interacts with the outside world — fetching data, querying databases, calling APIs, and performing computations. Each tool you pass to defineAgent is advertised to the model, and the runtime handles the full call-validate-execute cycle automatically.

The AgentTool interface

Every tool you define implements this interface:
interface AgentTool<Input = unknown> {
  name: string;
  description: string;
  zodSchema?: z.ZodType<Input>;
  inputSchema?: JsonSchema;
  sideEffect?: boolean;
  execute: (input: Input, meta: ToolExecMeta) => Promise<unknown> | unknown;
}
name
string
required
A unique identifier for the tool. The model uses this name when requesting a call. Use snake_case — most model providers require it.
description
string
required
A plain-English description of what the tool does. The model reads this to decide when to call the tool, so write it from the model’s perspective: what information does the tool provide, and when should it be used?
zodSchema
z.ZodType<Input>
A Zod schema for the tool’s input. Preferred over inputSchema — the runtime converts it to JSON Schema automatically (to advertise to the model) and validates the model’s input against it before calling execute. If validation fails, the model receives a descriptive error result instead of a crash.
inputSchema
JsonSchema
A raw JSON Schema for the tool’s input. Use this if you cannot express the input as a Zod schema. If both zodSchema and inputSchema are present, zodSchema is used for validation and inputSchema is ignored.
sideEffect
boolean
Annotates the tool as one that mutates external state (sends an email, writes to a database, calls a payment API, etc.). This is a documentation signal — use it to communicate intent to other developers reading the agent definition.
execute
(input: Input, meta: ToolExecMeta) => Promise<unknown> | unknown
required
The async function that runs when the model calls this tool. Return any serializable value — it becomes the tool result fed back to the model. Throw an error to return an error result.

The ToolExecMeta argument

execute receives a second meta argument with runtime context:
meta.callId
string
The unique ID for this tool invocation, assigned by the model. Useful for correlating logs and traces.
meta.signal
AbortSignal | undefined
The request’s AbortSignal. Pass it to any fetch calls or async work inside the tool so that a client disconnect cancels in-flight I/O.
meta.requestApproval
(payload?: unknown) => never
Throw this to suspend the run and request human approval before proceeding (human-in-the-loop). The run is checkpointed and the client receives a suspended event. Resume with resumeAgent after the operator approves.

Complete tool example

import { defineAgent } from 'anvil/agent';
import { LlmClient, AnthropicDriver } from 'anvil/llm';
import { z } from 'zod';

const client = new LlmClient({
  drivers: [new AnthropicDriver({ apiKey: process.env.ANTHROPIC_API_KEY })],
  defaultModel: 'claude-opus-4-8',
});

export default defineAgent({
  client,
  system: 'You are a helpful assistant. Use the provided tools to answer questions.',
  maxIterations: 5,
  tools: [
    {
      name: 'search_products',
      description: 'Search the product catalog by keyword. Returns a list of matching products with name, price, and stock status.',
      zodSchema: z.object({
        query: z.string().describe('The search keyword or phrase'),
        limit: z.number().int().min(1).max(20).default(5).describe('Maximum results to return'),
      }),
      execute: async ({ query, limit }, meta) => {
        const results = await fetch(
          `/internal/products?q=${encodeURIComponent(query)}&limit=${limit}`,
          { signal: meta.signal },
        ).then((r) => r.json());
        return results;
      },
    },
    {
      name: 'place_order',
      description: 'Place an order for a product. Only call this after confirming the product ID and quantity with the user.',
      sideEffect: true, // annotates this tool as one that mutates external state
      zodSchema: z.object({
        productId: z.string(),
        quantity: z.number().int().min(1),
      }),
      execute: async ({ productId, quantity }, meta) => {
        // Request human approval before placing the order
        if (quantity > 10) {
          meta.requestApproval({ productId, quantity, reason: 'Large order requires approval' });
        }
        const order = await createOrder(productId, quantity);
        return { orderId: order.id, status: 'placed' };
      },
    },
  ],
});

Input validation

The runtime validates tool inputs against zodSchema before calling execute. You never need to validate inside the function body:
// The runtime already ensures `city` is a non-empty string when execute runs.
{
  name: 'get_weather',
  description: 'Get the current weather for a city',
  zodSchema: z.object({ city: z.string().min(1) }),
  execute: ({ city }) => fetchWeather(city),
}
If the model sends invalid input (a missing required field, a number where a string is expected), the runtime returns a descriptive Zod error message to the model as the tool result. The model can then correct its call on the next iteration.
Always use zodSchema for user-facing tools. Without it, malformed model output reaches execute unvalidated. Raw inputSchema provides no runtime protection — it only shapes what the model is told.

Preventing infinite loops with maxIterations

By default the agent runtime caps the model↔tool loop at 10 iterations. Each call to the model counts as one iteration, regardless of how many tools it calls in that turn. Set maxIterations on defineAgent to tighten or loosen this limit:
export default defineAgent({
  client,
  maxIterations: 3, // stop after at most 3 model calls
  tools: [...],
});
When the cap is reached the runtime emits a final event with whatever text the model produced last. The stoppedAtCap flag in AgentRunResult tells you whether the run ended this way.
Set maxIterations to a small value (2–3) for tools that should complete in one or two steps, and only raise it for complex multi-step workflows. Smaller caps reduce latency and cost when a runaway tool loop occurs.

Standalone tools and anvil mcp

Tools defined inline in defineAgent are only available to that agent route. If you also want a tool served via the MCP protocol (for use with Claude Desktop or other MCP clients), define it as a standalone tool in server/tools/:
// server/tools/wordCount.ts
import { z } from 'zod';

export const description = 'Count the words in a piece of text';
export const inputSchema = z.object({ text: z.string() });

export default async function wordCount(args: { text: string }) {
  const words = args.text.trim().split(/\s+/).filter(Boolean);
  return { words: words.length, characters: args.text.length };
}
Running anvil mcp serves both server/tools/*.ts files and any MCP-exposed route-derived tools from the same endpoint. Route-level tools defined in defineAgent are not automatically MCP-exposed — only server/tools/ files and routes with meta.mcp.expose: true appear there.