Build

Tools

defineTool — flat builder for the Tool interface. JSON schema in, async execute out. Drop into agent.tool() or pull a whole MCP server's surface via agent.tools().

Your agent needs to look up an order. The LLM doesn't know your order database — it knows what you tell it via the tool's JSON schema. Tools are the contract between LLM intent ("call this with these args") and your code ("here's what happens"). Get the schema right and the LLM uses the tool well; get it wrong and you debug schema-vs-args mismatches at 2 AM.

What a tool is

A Tool has two parts:

{
  schema: { name, description, inputSchema },  // what the LLM sees
  execute: async (args) => result,             // what runs when LLM calls it
}

The inputSchema is a JSON Schema describing the args the LLM should produce. It's the LLM's contract — it generates well-formed args based on that shape.

defineTool — the flat builder

defineTool is a flatter helper that puts name + description + inputSchema at the top level instead of nested under schema:

import { defineTool } from 'agentfootprint';

const lookup = defineTool<{ orderId: string }, string>({
  name: 'lookup_order',
  description: 'Look up an order by ID',
  inputSchema: {
    type: 'object',
    properties: { orderId: { type: 'string' } },
    required: ['orderId'],
  },
  execute: async ({ orderId }) => `Order ${orderId}: shipped, $299`,
});

const agent = Agent.create({ provider }).tool(lookup).build();

Type parameters <TArgs, TResult> flow into execute so your handler is fully typed.

Example — agent + tool registered inline

const agent = Agent.create({  provider: provider ?? exampleProvider('feature', { respond: weatherRespond }),  model: 'mock',  maxIterations: 5,  // reactMode: 'dynamic-grouped' wraps the LLM turn in an sf-llm-call subflow,  // so Lens renders the agent's reasoning as an LLM group with its context  // slots (system-prompt / messages / tools) nested inside — the SAME shape  // the LLMCall primitive shows — instead of a bare "Final · RUNNER" card.  reactMode: 'dynamic-grouped',})  .system('You answer weather questions using the `weather` tool.')  .tool({    schema: {      name: 'weather',      description: 'Get current weather for a city.',      inputSchema: {        type: 'object',        properties: { city: { type: 'string' } },        required: ['city'],      },    },    execute: async (args) => `${(args as { city: string }).city}: sunny, 72°F`,  })  .build();

.tool(...) accepts both shapes (flat {schema, execute} or defineTool output) — the agent doesn't care which.

Bulk-register from MCP

Pull a whole MCP server's tool surface into the agent in one call:

// Connect once at startup. In production: use a real transport.const fileServer = await mcpClient({  name: 'file-server',  transport: { transport: 'stdio', command: 'npx', args: ['fake-mcp'] },  _client: fakeServer, // ← test injection; remove for real MCP});// Agent picks up all the server's tools at once.const agent = Agent.create({  provider: provider ?? mock({ reply: '/tmp/notes.md and /tmp/todo.txt are present.' }),  model: 'mock',  maxIterations: 1,})  .system('You answer file-system questions using the MCP tools provided.')  .tools(await fileServer.tools())  .build();

agent.tools(arr) is the bulk-register companion to agent.tool(t). Tool-name uniqueness is validated at registration time — .tool() (and .tools(), which calls it per entry) throws Agent.tool(): duplicate tool name '<name>' the moment a collision is registered, so an MCP-imported tool clashing with a manually-defined one fails fast. (Collisions between static tools and Skill-injected tools are caught later, at .build().)

For mock-first development of MCP integrations without spawning a subprocess, use mockMcpClient({ tools }) — same McpClient interface, in-memory implementation. See Tool discovery for the full MCP surface.

Tool errors

When execute throws, the framework catches it, reports it to the LLM as a tool error, and continues the loop. The LLM sees the error message and can decide to retry with different args, try a different tool, or surface to the user. See Error handling for the typed error contract + retry decorators.

Anti-patterns

  • Don't reach outside the args + ctx your tool is given. execute(args, ctx) receives the LLM-supplied args plus a ToolExecutionContext ({ toolCallId, iteration, signal? }) — use ctx.signal to honor cancellation, not ambient globals. (execute may return a value OR a Promise; the loop awaits either, so sync handlers are fine.)
  • Don't put validation logic in execute for things JSON Schema can express (type, required, enum). The LLM honors well-formed schemas; redundant runtime checks are noise.
  • Don't make tool descriptions ambiguous. "Get data" is bad. "Look up an order by ID; returns status + amount" is good. The LLM picks tools by description.

Next steps

On this page