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:

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:

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.