Tools
Tools are how your app exposes functionality to the AI. They're functions that the orchestrator can discover and call based on what users ask for.
What's a Tool?
A tool is a function with a contract. You define:
- What it's called and what it does
- What parameters it accepts
- What it returns
- What capabilities it needs
The orchestrator reads these definitions and figures out when to use each tool.
Defining Tools
Tools are defined in tools.json:
[
{
"name": "create_note",
"description": "Creates a new note with a title and content",
"capabilities": ["storage"],
"input_schema": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The note title"
},
"content": {
"type": "string",
"description": "The note content"
}
},
"required": ["title", "content"]
},
"output_schema": {
"type": "object",
"properties": {
"noteId": { "type": "string" },
"success": { "type": "boolean" }
}
}
}
]
Tool Fields
| Field | Required | What It Does |
|---|---|---|
name |
Yes | Identifier for the tool (use snake_case) |
description |
Yes | Explains what the tool does - the AI reads this |
capabilities |
Yes | What resources the tool needs |
input_schema |
Yes | JSON Schema for input parameters |
output_schema |
Yes | JSON Schema for return value |
input_tokens |
No | Tokens required to call this tool |
output_tokens |
No | Tokens this tool can create |
Writing Good Descriptions
The description field matters. The AI uses it to decide whether to call your tool.
Be specific:
"description": "Lists the 10 most recent emails from the user's Gmail inbox"
Not vague:
"description": "Gets emails"
The more context you provide, the better the AI can match user intent to your tool.
Capabilities
Capabilities declare what resources your tool needs:
| Capability | What It Provides |
|---|---|
storage |
Read/write to persistent storage |
token |
Sign and verify tokens |
environment |
Access environment variables |
event |
Subscribe to and publish events |
tool |
Call other tools |
ai |
Access AI completions |
object |
Create visual components |
Only request what you need. A tool with "capabilities": [] can still do useful work - it just can't access external resources.
Implementing Tools
Create a TypeScript file matching your tool name:
// src/tools/create_note.ts
import { Input, Output, Capabilities } from './types/create_note.js';
export default async function create_note(
input: Input,
capabilities: Capabilities
): Promise<Output> {
const storage = capabilities.storage.use('@my-org/notes');
const noteId = crypto.randomUUID();
await storage.put(`/notes/${noteId}.json`, {
title: input.title,
content: input.content,
created: Date.now()
});
return { noteId, success: true };
}
The Input, Output, and Capabilities types are auto-generated from your schemas. Run yarn generate after updating tools.json.
Requiring Tokens
Some tools should only work for authenticated users or users with specific permissions. Use input_tokens:
{
"name": "list_emails",
"description": "Lists emails from the user's Gmail inbox",
"input_tokens": {
"@malv/auth": {
"required": ["account"]
},
"@malv/gmail": {
"required": ["gmail_access"]
}
}
}
This tool requires:
- An
accounttoken from@malv/auth(user is logged in) - A
gmail_accesstoken from@malv/gmail(user connected Gmail)
If the user doesn't have these tokens, the tool won't appear in the orchestrator's available options.
Creating Tokens
Tools can issue tokens using output_tokens:
{
"name": "login_gmail",
"description": "Connects the user's Gmail account",
"output_tokens": {
"@malv/gmail": {
"required": ["gmail_access"]
}
}
}
The orchestrator uses this to understand dependencies. If a user needs gmail_access to list emails, and login_gmail can provide it, the orchestrator knows to suggest logging in first.
Expired Token Handling
Sometimes you want a tool to work even when tokens have expired. This is useful for auto-refresh flows:
"input_tokens": {
"@malv/gmail": {
"required": ["gmail_access"],
"allow_expired": ["gmail_access"]
}
}
With allow_expired, the tool receives the expired token and can refresh it internally (e.g., using an OAuth refresh token) without requiring the user to re-authenticate.
Complex Input Types
For advanced schemas, you can use $defs and $ref:
{
"name": "process_questions",
"$defs": {
"question": {
"type": "object",
"properties": {
"text": { "type": "string" },
"type": { "type": "string", "enum": ["open", "rating"] }
},
"required": ["text", "type"]
}
},
"input_schema": {
"type": "object",
"properties": {
"questions": {
"type": "array",
"items": { "$ref": "#/$defs/question" }
}
}
}
}
This generates proper TypeScript types with reusable interfaces.
Best Practices
One thing per tool - A tool should do one clear thing. Instead of manage_notes, create create_note, edit_note, delete_note.
Descriptive names - send_email is better than email. list_recent_orders is better than get_orders.
Minimal capabilities - Only request what you actually use. More capabilities means more security surface.
Handle errors gracefully - Return meaningful error messages in your output schema so the AI can explain what went wrong.