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
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>;
};
Unique identifier for the tool. Must match pattern: [a-zA-Z0-9_.:-]+
Human-readable description of what the tool does. Used for tool discovery and documentation.
JSON Schema definition for tool input validation.inputSchema.properties
Record<string, object>
required
Schema properties for each input parameter
Array of required parameter names
inputSchema.additionalProperties
Whether to allow properties not defined in the schema
The tool’s execution function. Receives input and context, returns a result.Signature:(input: TInput, context: ToolContext) => Promise<ToolResult<TOutput>> | ToolResult<TOutput>
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.
The return type from a tool execution.
type ToolResult<TOutput extends JsonValue = JsonValue> = {
ok: boolean;
output: TOutput;
error?: string;
};
Indicates whether the tool executed successfully
The tool’s output data. Must be JSON-serializable.
Error message if ok is false
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" } };
}
};
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" } };
}
};
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 } };
}
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.
The ToolRegistry class manages tool registration and lookup.
Methods
Register a single tool. Throws if the tool name already exists.register(tool: ToolDefinition): this
Register multiple tools at once.registerMany(tools: ToolDefinition[]): this
Retrieve a tool by name.get(name: string): ToolDefinition | undefined
Check if a tool is registered.has(name: string): boolean
Remove a tool from the registry.unregister(name: string): this
Get all registered tools, sorted alphabetically by name.
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!"
}
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
List of directories that were searched for tools
Array of successfully loaded toolsAbsolute path to the tool file
The tool definition object
Error messages for tools that failed to load
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.
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 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.