Skip to main content

Overview

The loaf tool system allows you to extend the CLI with custom functionality through a well-defined API. Tools are JavaScript modules that export a specific structure conforming to the ToolDefinition interface.

Core Types

ToolDefinition

The main interface that defines a tool’s structure and behavior.
type ToolDefinition<
  TInput extends ToolInput = ToolInput,
  TOutput extends JsonValue = JsonValue,
> = {
  name: string;
  description: string;
  inputSchema?: {
    type: "object";
    properties: Record<string, Record<string, unknown>>;
    required?: string[];
    additionalProperties?: boolean;
  };
  run: (input: TInput, context: ToolContext) => Promise<ToolResult<TOutput>> | ToolResult<TOutput>;
};
name
string
required
Unique identifier for the tool. Must match pattern: [a-zA-Z0-9_.:-]+
description
string
required
Human-readable description of what the tool does. Used for tool discovery and documentation.
inputSchema
object
JSON Schema definition for tool input validation.
inputSchema.type
string
required
Must be "object"
inputSchema.properties
Record<string, object>
required
Schema properties for each input parameter
inputSchema.required
string[]
Array of required parameter names
inputSchema.additionalProperties
boolean
Whether to allow properties not defined in the schema
run
function
required
The tool’s execution function. Receives input and context, returns a result.Signature:
(input: TInput, context: ToolContext) => Promise<ToolResult<TOutput>> | ToolResult<TOutput>

ToolInput

Input parameters passed to a tool.
type ToolInput = Record<string, JsonValue>;
type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
type JsonPrimitive = string | number | boolean | null;
Tools receive input as a key-value object where all values must be JSON-serializable.

ToolResult

The return type from a tool execution.
type ToolResult<TOutput extends JsonValue = JsonValue> = {
  ok: boolean;
  output: TOutput;
  error?: string;
};
ok
boolean
required
Indicates whether the tool executed successfully
output
JsonValue
required
The tool’s output data. Must be JSON-serializable.
error
string
Error message if ok is false

Export Formats

Loaf supports multiple export patterns for tool modules. Files must have extensions: .js, .mjs, or .cjs.

Direct Export

Export the tool object directly:
export const name = "my-tool";
export const description = "Does something useful";

export async function run(input, context) {
  return {
    ok: true,
    output: { result: "success" }
  };
}

Default Export

Export as default object:
export default {
  name: "my-tool",
  description: "Does something useful",
  run: async (input, context) => {
    return { ok: true, output: { result: "success" } };
  }
};

Tool Property

Nest the tool definition under a tool property:
export const tool = {
  name: "my-tool",
  description: "Does something useful",
  run: async (input, context) => {
    return { ok: true, output: { result: "success" } };
  }
};

Meta Export

Separate metadata from implementation:
export const meta = {
  name: "my-tool",
  description: "Does something useful",
  args: {
    type: "object",
    properties: {
      message: { type: "string" }
    }
  }
};

export async function run(input, context) {
  return { ok: true, output: { result: input.message } };
}

Input Schema

Define parameter schemas using JSON Schema format:
export const inputSchema = {
  type: "object",
  properties: {
    query: {
      type: "string",
      description: "Search query"
    },
    limit: {
      type: "number",
      description: "Maximum results",
      default: 10
    },
    tags: {
      type: "array",
      items: { type: "string" }
    }
  },
  required: ["query"],
  additionalProperties: false
};
You can use args as an alias for inputSchema in your exports.

Tool Registry

The ToolRegistry class manages tool registration and lookup.

Methods

register(tool)
method
Register a single tool. Throws if the tool name already exists.
register(tool: ToolDefinition): this
registerMany(tools)
method
Register multiple tools at once.
registerMany(tools: ToolDefinition[]): this
get(name)
method
Retrieve a tool by name.
get(name: string): ToolDefinition | undefined
has(name)
method
Check if a tool is registered.
has(name: string): boolean
unregister(name)
method
Remove a tool from the registry.
unregister(name: string): this
list()
method
Get all registered tools, sorted alphabetically by name.
list(): ToolDefinition[]
getModelManifest()
method
Get tool metadata for AI model consumption.
getModelManifest(): Array<{
  name: string;
  description: string;
  inputSchema?: ToolDefinition["inputSchema"];
}>

Usage Example

import { createToolRegistry } from "loaf/tools";

const registry = createToolRegistry();

registry.register({
  name: "hello",
  description: "Greets the user",
  run: async () => ({ ok: true, output: "Hello!" })
});

const tool = registry.get("hello");
if (tool) {
  const result = await tool.run({}, { now: new Date() });
  console.log(result.output); // "Hello!"
}

Tool Discovery

Loaf automatically discovers custom tools from the tools directory.

Discovery Process

import { discoverCustomTools } from "loaf/tools";

const result = await discoverCustomTools();

result.searchedDirectories; // Directories searched
result.loaded;              // Successfully loaded tools
result.errors;              // Any loading errors
searchedDirectories
string[]
List of directories that were searched for tools
loaded
array
Array of successfully loaded tools
loaded[].name
string
Tool name
loaded[].sourcePath
string
Absolute path to the tool file
loaded[].tool
ToolDefinition
The tool definition object
errors
string[]
Error messages for tools that failed to load

Custom Tools Directory

Tools are loaded from:
import { getCustomToolsDirectory } from "loaf/tools";

const toolsDir = getCustomToolsDirectory();
// Returns: <loaf-data-dir>/tools
The node_modules directory is automatically excluded from tool discovery.

Loading Individual Tools

You can load a single tool file programmatically:
import { loadCustomToolFile } from "loaf/tools";

const loaded = await loadCustomToolFile("/path/to/my-tool.js");

if (loaded) {
  console.log(loaded.name);       // Tool name
  console.log(loaded.sourcePath); // File path
  console.log(loaded.tool);       // ToolDefinition
}
Returns null if:
  • File extension is not supported
  • No valid tool export is found
Throws if:
  • Tool name is invalid
  • Module fails to load

Validation

Tool Name Validation

Tool names must match the pattern: [a-zA-Z0-9_.:-]+
import { isValidCustomToolName } from "loaf/tools";

isValidCustomToolName("my-tool");     // true
isValidCustomToolName("my_tool");     // true
isValidCustomToolName("my.tool");     // true
isValidCustomToolName("my:tool");     // true
isValidCustomToolName("my tool");     // false (spaces not allowed)
isValidCustomToolName("my/tool");     // false (slashes not allowed)

Result Normalization

Tools can return simplified values that are automatically normalized:
// Simple value
export async function run(input, context) {
  return "Hello";
}
// Normalized to: { ok: true, output: "Hello" }

// Explicit result
export async function run(input, context) {
  return { ok: true, output: "Hello" };
}
// Used as-is

// Error result
export async function run(input, context) {
  return { ok: false, output: null, error: "Something went wrong" };
}
All output values must be JSON-serializable. Non-serializable values (functions, symbols, etc.) will be converted to strings.

Build docs developers (and LLMs) love