Get Started

Tools

A standard interface for agent capabilities. Built-in tools cover common tasks out of the box. Register custom tools via webhook to extend agents with your own APIs. For connecting to external MCP servers, see MCP.

Built-in tools

Pass any built-in tool name in the tools array when creating an agent. No configuration required.

tools restricts the agent. An agent created with tools: ['read_file'] can read files and cannot write them or execute code — the restriction is enforced when the tool is called, not only in what the model is offered. Omit tools to give the agent every built-in and every custom tool you have registered. See Restricting an agent's tools.

exec_codelanguage?: 'python' | 'bash', code: string

Execute Python code in an isolated sandbox. Returns stdout.

write_filepath: string, content: string

Write content to a file in the sandbox.

read_filepath: string

Read content from a file in the sandbox.

list_filesdirectory?: string

List files in a directory.

search_knowledge is attached conditionally — The agent is attached to a knowledge base. Anything beyond this list is either a custom tool you register or a tool discovered from a connected MCP server.

Using built-in tools

agent-with-tools.ts
import { Theazo } from 'theazo'

const theazo = new Theazo({ apiKey: 'th_live_...' })
const session = await theazo.sessions.forUser('user_123')

const agent = await session.agents.create({
  tools: ['exec_code', 'read_file', 'write_file', 'list_files'],
})

const result = await agent.run(
  'Read /data/yc-batch.csv, compare the companies by funding stage, ' +
  'and write a comparison report to /output/yc-analysis.md'
)

console.log(result.output)
console.log(result.artifacts)  // ['/output/yc-analysis.md']
console.log(result.cost)       // { amount: 189, currency: 'usd' }

Restricting an agent's tools

Use tools to limit what an agent can do — for example a review agent that may read a codebase but never modify it.

// Read-only agent — cannot write files or execute code
const reviewer = await session.agents.create({
  tools: ['read_file', 'list_files'],
})

// Unrestricted — every built-in plus every custom tool you've registered
const worker = await session.agents.create({})

The restriction applies to built-in tools and to custom tools you register — the shared pools every agent draws from. It does not apply to capabilities you attach to an agent individually: MCP servers (mcp), skills, and knowledge (search_knowledge) are already scoped by their own attachment, so listing tools never silently removes them.

If the agent calls a tool it doesn't have, the call is refused and the agent is told so — it can adapt rather than failing the run. Naming a tool that doesn't exist is logged and ignored.

Enforcement happens when the tool is called, not just in the list of tools the model is shown. A model that names a tool it was never offered — whether by mistake or because instructions were injected through tool output — still cannot execute it.

Custom tools

Register a custom tool by providing a name, description, parameter schema, and a webhook URL. When an agent calls the tool, Theazo POSTs the arguments to your URL and returns the response to the agent.

register-tool.ts
const tool = await theazo.tools.register({
  name:        'lookup_customer',
  description: 'Look up a customer by email and return their account details and subscription status.',
  parameters: {
    type:       'object',
    properties: {
      email:  { type: 'string',  description: 'Customer email address' },
      fields: { type: 'array',   items: { type: 'string' }, description: 'Fields to include' },
    },
    required: ['email'],
  },
  handler: {
    type: 'webhook',
    url:  'https://your-api.com/tools/lookup-customer',
  },
  requiresApproval: false,
})

console.log(tool.name)             // 'lookup_customer'
console.log(tool.builtin)          // false
console.log(tool.requiresApproval) // false

Register options

The options accepted by tools.register(). This table is generated from the @theazo/contracts schema — it can't drift from the SDK.

FieldTypeRequiredDescription
namestringyesUnique tool name within the platform.
descriptionstringyesWhat the tool does; shown to the model when it decides to call it.
parametersRecord<string, unknown>yesJSON Schema describing the tool input.
handlerobjectyesHow the tool is executed.
requiresApprovalbooleanGate every call behind a human approval. Default false.

Implementing the tool handler

Theazo POSTs to your webhook URL with the agent-provided arguments. Return a JSON response — the agent receives it as the tool result.

Request signing (an X-Theazo-Signature header) is not available yet — secure your endpoint with an unguessable URL or a shared token you check yourself until it ships.

tool-handler.ts
// Your API endpoint
export async function POST(req: Request) {
  const { email, fields } = await req.json()

  // Your business logic
  const customer = await db.customers.findByEmail(email)
  if (!customer) {
    return Response.json({ found: false })
  }

  return Response.json({
    found:        true,
    id:           customer.id,
    email:        customer.email,
    plan:         customer.plan,
    mrr:          customer.mrr,
    createdAt:    customer.createdAt,
    // Only include requested fields if specified
  })
}
Tool handler responses must be JSON-serializable. Keep responses focused — agents perform better when tool output is concise and structured. Avoid returning large blobs of text or binary data.

Tools that require approval

Set requiresApproval: true to gate a custom tool through the Approvals system. The agent will pause when it tries to call the tool.

await theazo.tools.register({
  name:        'delete_account',
  description: 'Permanently delete a customer account and all associated data.',
  parameters: {
    type:       'object',
    properties: {
      customerId: { type: 'string', description: 'Customer ID to delete' },
      reason:     { type: 'string', description: 'Reason for deletion' },
    },
    required: ['customerId', 'reason'],
  },
  handler: {
    type: 'webhook',
    url:  'https://your-api.com/tools/delete-account',
  },
  requiresApproval: true,   // human must approve before handler is called
})

Listing and testing tools

List tools

const tools = await theazo.tools.list()

// tools = [
//   { name: 'exec_code',        builtin: true,   requiresApproval: false },
//   { name: 'write_file',       builtin: true,   requiresApproval: false },
//   { name: 'lookup_customer',  builtin: false,  requiresApproval: false,
//     handler: { type: 'webhook', url: 'https://your-api.com/tools/lookup-customer' } },
// ]

Test a tool

Call theazo.tools.test() to invoke a tool directly without running an agent. Useful for verifying your webhook handler responds correctly.

const result = await theazo.tools.test('lookup_customer', {
  email:  'alice@acme.com',
  fields: ['id', 'plan', 'mrr'],
})

console.log(result.output)   // the tool's output (string)
console.log(!result.error)   // true when the tool succeeded (ToolResult has no `success`)

Delete a tool

await theazo.tools.delete('lookup_customer')
// Agents that have this tool in their config will error if they try to call it

Agents with custom tools

agent-custom-tools.ts
import { Theazo } from 'theazo'

const theazo = new Theazo({ apiKey: 'th_live_...' })
const session = await theazo.sessions.forUser('user_123')

// Register custom tool
await theazo.tools.register({
  name:        'get_pipeline_status',
  description: 'Get the current status of a sales pipeline for a given account.',
  parameters: {
    type:       'object',
    properties: { accountId: { type: 'string' } },
    required:   ['accountId'],
  },
  handler: { type: 'webhook', url: 'https://api.yourcrm.com/tools/pipeline-status' },
  requiresApproval: false,
})

// Create an agent that uses both built-in and custom tools
const agent = await session.agents.create({
  tools: ['exec_code', 'write_file', 'get_pipeline_status'],
  approvals: {
    require:       ['get_pipeline_status'],
    notifyVia:     'webhook',
    timeout:       '4h',
    defaultAction: 'deny',
  },
})

const result = await agent.run(
  'Check the pipeline status for account ACC_789 and write a summary ' +
  'of the open opportunities to /output/acc-789.md'
)

console.log(result.output)
console.log(result.cost)  // { amount: 87, currency: 'usd' }

API reference

theazo.tools.register(config)Promise<Tool>Register a custom tool with a webhook handler. Built-in tools cannot be registered.
theazo.tools.list()Promise<Tool[]>List all tools available: built-in, custom, and MCP-sourced.
theazo.tools.test(name, input)Promise<ToolResult>Invoke a tool directly. Returns { tool, input, output, error?, duration }.
theazo.tools.delete(name)Promise<void>Delete a custom tool. Built-in tools cannot be deleted.

MCP stdio transport

MCP servers can run as local processes — not just over HTTP. Theazo spawns the process using StdioClientTransport from @modelcontextprotocol/sdk and communicates over stdin/stdout. Tools are discovered live at agent run start, so the tool list always reflects the server's current capabilities.

Theazo tracks process health automatically. If a stdio server crashes twice, it is marked unavailable and agents will skip it until it is re-registered or manually re-enabled.

Registering a stdio MCP server

mcp-stdio.ts
import { Theazo } from 'theazo'

const theazo = new Theazo({ apiKey: 'th_live_...' })

// Connect an MCP server running as a local process
await theazo.mcp.connect({
  name:      'filesystem',
  transport: 'stdio',
  command:   'node',
  args:      ['mcp-filesystem-server.js'],
})

// Tools are discovered automatically at agent run start
const agent = await session.agents.create({
  name:  'file-worker',
  tools: ['filesystem_readFile', 'filesystem_writeFile'],
})

const result = await agent.run('List all .ts files in /src and summarize them')
console.log(result.output)
Stdio transport is ideal for local development and self-hosted deployments where the MCP server runs on the same machine as the Theazo worker. For remote servers, use the HTTP transport described in the MCP docs.

Tool namespacing

When multiple MCP servers are connected, tool names are automatically namespaced to prevent collisions. The format is <serverName>_<toolName> — for example, a tool called readFile on a server named filesystem becomes filesystem_readFile.

Built-in tools (like exec_code and write_file) keep their original names with no prefix. Only MCP-sourced tools are namespaced. The agent model sees the namespaced names in the tool definitions it receives, so prompts and tool calls always use the full namespaced name.

namespacing-example.ts
// Connect two MCP servers
await theazo.mcp.connect({ name: 'filesystem', transport: 'stdio', command: 'node', args: ['fs-server.js'] })
await theazo.mcp.connect({ name: 'github',     transport: 'stdio', command: 'node', args: ['gh-server.js'] })

const tools = await theazo.tools.list()
tools-response.json
// tools = [
//   { name: 'exec_code',             builtin: true },   // no prefix
//   { name: 'write_file',            builtin: true },   // no prefix
//   { name: 'filesystem_readFile',   builtin: false },  // namespaced MCP tool
//   { name: 'filesystem_writeFile',  builtin: false },
//   { name: 'github_createIssue',    builtin: false },
//   { name: 'github_listPRs',        builtin: false },
// ]

Per-user MCP credentials

Some MCP servers require per-user authentication — for example, an agent acting on behalf of a user needs that user's own GitHub token or Slack OAuth token. Theazo stores these credentials in the encrypted secrets vault, scoped per user so each user's tokens are isolated.

Set credentialsType: 'per_user' on the MCP connection. When the agent runs, Theazo resolves the correct credentials for the session's user. For OAuth2 providers, Theazo handles token refresh automatically — you only need to store the initial tokens.

Setting up per-user credentials

per-user-mcp.ts
import { Theazo } from 'theazo'

const theazo = new Theazo({ apiKey: 'th_live_...' })

// Register an MCP server that requires per-user auth
const github = await theazo.mcp.connect({
  name:            'github',
  transport:       'stdio',
  command:         'node',
  args:            ['mcp-github-server.js'],
  credentialsType: 'per_user',
})

// Store a user's token as their per-user auth header for this connection
await theazo.mcp.setUserCredentials(github.id, 'user_123', {
  headers: { 'Authorization': 'Bearer gho_xxxxxxxxxxxx' },
})

// Agent runs with the user's own GitHub credentials
const session = await theazo.sessions.forUser('user_123')
const agent = await session.agents.create({
  mcp: ['github'],
})

const result = await agent.run('Create an issue for the login bug in acme/frontend')
console.log(result.output)
Per-user credentials are encrypted at rest with AES-256-GCM and scoped by user ID. Agents can only access credentials for the user associated with their session. OAuth2 tokens are refreshed automatically when they expire — your MCP server always receives a valid token.
Was this page helpful?