How to Add Custom Tools to the Lemon AI Agent Framework: A Complete Developer Guide
To add custom tools to the Lemon AI agent framework, create a JavaScript module in src/tools/ that exports an object matching the Tool interface defined in types/Tool.d.ts, implementing name, description, params, and an async execute function; the framework auto-discovers and registers the tool via src/tools/index.js without additional configuration.
The Lemon AI agent framework (hexdocom/lemonai) provides an extensible architecture that allows developers to augment LLM capabilities with custom business logic. By following the established Tool interface contract, you can integrate external APIs, databases, or proprietary algorithms while maintaining type safety and automatic discovery. This guide walks through the exact process for adding custom tools to the Lemon AI agent framework using the repository's native conventions.
Understanding the Tool Interface Contract
Before implementing, review the formal type definition in types/Tool.d.ts. This TypeScript declaration file specifies the mandatory properties every tool must expose:
- name: The unique identifier the LLM uses to invoke the tool
- description: A human-readable explanation of the tool's purpose
- params: A JSON-Schema-like object defining the expected arguments
- execute: An async function that receives parsed arguments and returns an
ActionResult - getActionDescription (optional): Returns a human-readable plan step for the agent's thought process
The execute function must return an object satisfying the ActionResult type, typically structured as { content: string, meta?: object } or a simple string that the framework normalizes.
Step-by-Step Implementation Process
Define the Tool Schema
Create a new JavaScript file in src/tools/ (for example, src/tools/calculator.js). The file must export a plain object conforming to the interface. Define the params object using JSON Schema conventions to help the LLM understand required versus optional arguments.
params: {
type: "object",
properties: {
operation: {
type: "string",
description: "One of: add, subtract, multiply, divide."
},
a: { type: "number", description: "First operand." },
b: { type: "number", description: "Second operand." }
},
required: ["operation", "a", "b"]
}
Implement the Execution Logic
Write an async execute function that accepts the parsed arguments object and returns an ActionResult. Handle errors gracefully within the function so the agent can recover and retry or report failures without crashing.
execute: async ({ operation, a, b }) => {
let result;
switch (operation) {
case "add": result = a + b; break;
case "subtract": result = a - b; break;
case "multiply": result = a * b; break;
case "divide":
if (b === 0) throw new Error("Division by zero");
result = a / b;
break;
default:
throw new Error(`Unsupported operation: ${operation}`);
}
return {
content: `Result: ${result}`,
meta: { operation, a, b }
};
}
Optional: Provide Action Descriptions
If your tool benefits from human-readable planning output, implement getActionDescription. This function receives the same arguments as execute and returns a string describing the intended action.
getActionDescription: async ({ operation, a, b }) => {
return `${operation} ${a} and ${b}`;
}
Leverage Auto-Discovery Registration
Place your completed file in src/tools/. The loader in src/tools/index.js automatically scans the directory for *.js files (excluding browser_use), imports each module, and adds it to the exported tools map using the object's name property as the key. No manual registration or import statements are required in other files.
Code Example: Building a Calculator Tool
Here is a complete, production-ready example following the framework's conventions:
File: src/tools/calculator.js
/** @type {import('types/Tool').Tool} */
const CalculatorTool = {
name: "calculator",
description: "Performs basic arithmetic operations (add, subtract, multiply, divide).",
params: {
type: "object",
properties: {
operation: {
type: "string",
description: "One of: add, subtract, multiply, divide."
},
a: {
type: "number",
description: "First operand."
},
b: {
type: "number",
description: "Second operand."
}
},
required: ["operation", "a", "b"]
},
getActionDescription: async ({ operation, a, b }) => {
return `${operation} ${a} and ${b}`;
},
execute: async ({ operation, a, b }) => {
let result;
switch (operation) {
case "add":
result = a + b;
break;
case "subtract":
result = a - b;
break;
case "multiply":
result = a * b;
break;
case "divide":
if (b === 0) throw new Error("Division by zero");
result = a / b;
break;
default:
throw new Error(`Unsupported operation: ${operation}`);
}
return {
content: `Result: ${result}`,
meta: { operation, a, b }
};
}
};
module.exports = CalculatorTool;
After saving this file, the framework immediately exposes the tool to the LLM. When the agent composes its system prompt via src/agent/prompt/tool.js, the calculator appears in the available tools list, enabling invocations like:
{
"name": "calculator",
"arguments": { "operation": "add", "a": 3, "b": 5 }
}
Advanced: MCP Server Integration
For scenarios requiring remote tool execution or distributed architectures, wrap your tool with the MCP (Model Context Protocol) definitions in src/mcp/tool.js. This allows external MCP servers to invoke your custom tools via standardized remote procedure calls. This step is only necessary if your deployment separates the agent core from tool execution environments; local tools in src/tools/ work without MCP configuration.
Testing and Validation
Verify your implementation using the existing test patterns in src/agent/prompt/tool.test.js. These unit tests confirm that:
- The tool loader correctly discovers your new file in
src/tools/ - The
resolveToolPromptfunction includes your tool's schema in the system prompt - The
executefunction returns properly structuredActionResultobjects
You can also test interactively through the agent UI by prompting the LLM to use your new tool and confirming the execution path routes through your execute function.
Key Source Files and Architecture
Understanding these core files helps debug and extend the tool system:
types/Tool.d.ts: The canonical TypeScript definition of theToolinterface andActionResulttypessrc/tools/index.js: Auto-loader that builds the tool registry by scanningsrc/tools/*.jssrc/tools/WebSearch.js: Reference implementation showing complex parameter handling and error managementsrc/agent/prompt/tool.js: Generates the system prompt snippet that enumerates available tools for the LLMsrc/mcp/tool.js: Wrapper utilities for exposing tools to remote MCP servers
Summary
- Create a new file in
src/tools/exporting an object that implements theToolinterface fromtypes/Tool.d.ts - Implement
name,description,params, and asyncexecutereturning anActionResultwith acontentstring - Auto-discovery happens automatically via
src/tools/index.js—no manual registration required - Handle errors inside
executeto maintain agent stability and enable graceful recovery - Test using
src/agent/prompt/tool.test.jsor interactive UI to verify system prompt inclusion - Use
src/mcp/tool.jsonly if exposing tools to remote MCP servers
Frequently Asked Questions
What programming language should I use to write custom tools?
Write custom tools in JavaScript (Node.js). The framework's auto-loader in src/tools/index.js specifically scans for *.js files and uses require() to import them. While the project uses TypeScript definitions (types/Tool.d.ts) for type checking, the runtime implementation uses plain JavaScript modules with JSDoc type hints.
Do I need to restart the server after adding a new tool file?
No manual restart is required for development in most configurations because src/tools/index.js loads tools at startup or on-demand depending on your deployment mode. However, for production deployments, a restart ensures the loader cache clears and picks up new files. The tool appears in the system prompt generated by src/agent/prompt/tool.js on the next agent initialization.
How does the LLM know what parameters to send to my tool?
The framework converts your params JSON Schema definition into a structured description within the system prompt. When src/agent/prompt/tool.js builds the prompt, it serializes the name, description, and params properties of every discovered tool. The LLM uses this schema to generate properly formatted tool calls with the correct argument types and required fields.
Can I make a tool's output persistent across conversations?
Yes, set the memorized property to true in your tool object definition. By default, memorized is false, meaning the output is ephemeral. When set to true, the framework stores the ActionResult in the agent's memory store, making it available for reference in later tasks or conversation turns. This is useful for tools that fetch reference data the agent needs to recall.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →