# How to Add Custom Tools to the Lemon AI Agent Framework: A Complete Developer Guide

> Easily add custom tools to the Lemon AI agent framework. Follow this guide to create JavaScript modules that integrate seamlessly with the agent for enhanced functionality. Start building now.

- Repository: [hexdocom/lemonai](https://github.com/hexdocom/lemonai)
- Tags: how-to-guide
- Published: 2026-03-03

---

**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`](https://github.com/hexdocom/lemonai/blob/main/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`](https://github.com/hexdocom/lemonai/blob/main/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`](https://github.com/hexdocom/lemonai/blob/main/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`](https://github.com/hexdocom/lemonai/blob/main/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.

```javascript
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.

```javascript
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.

```javascript
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`](https://github.com/hexdocom/lemonai/blob/main/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`](https://github.com/hexdocom/lemonai/blob/main/src/tools/calculator.js)

```javascript
/** @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`](https://github.com/hexdocom/lemonai/blob/main/src/agent/prompt/tool.js), the calculator appears in the available tools list, enabling invocations like:

```json
{
  "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`](https://github.com/hexdocom/lemonai/blob/main/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`](https://github.com/hexdocom/lemonai/blob/main/src/agent/prompt/tool.test.js). These unit tests confirm that:

1. The tool loader correctly discovers your new file in `src/tools/`
2. The `resolveToolPrompt` function includes your tool's schema in the system prompt
3. The `execute` function returns properly structured `ActionResult` objects

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`](https://github.com/hexdocom/lemonai/blob/main/types/Tool.d.ts)**: The canonical TypeScript definition of the `Tool` interface and `ActionResult` types
- **[`src/tools/index.js`](https://github.com/hexdocom/lemonai/blob/main/src/tools/index.js)**: Auto-loader that builds the tool registry by scanning `src/tools/*.js`
- **[`src/tools/WebSearch.js`](https://github.com/hexdocom/lemonai/blob/main/src/tools/WebSearch.js)**: Reference implementation showing complex parameter handling and error management
- **[`src/agent/prompt/tool.js`](https://github.com/hexdocom/lemonai/blob/main/src/agent/prompt/tool.js)**: Generates the system prompt snippet that enumerates available tools for the LLM
- **[`src/mcp/tool.js`](https://github.com/hexdocom/lemonai/blob/main/src/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 the `Tool` interface from [`types/Tool.d.ts`](https://github.com/hexdocom/lemonai/blob/main/types/Tool.d.ts)
- **Implement `name`, `description`, `params`, and async `execute`** returning an `ActionResult` with a `content` string
- **Auto-discovery happens automatically** via [`src/tools/index.js`](https://github.com/hexdocom/lemonai/blob/main/src/tools/index.js)—no manual registration required
- **Handle errors inside `execute`** to maintain agent stability and enable graceful recovery
- **Test using [`src/agent/prompt/tool.test.js`](https://github.com/hexdocom/lemonai/blob/main/src/agent/prompt/tool.test.js)** or interactive UI to verify system prompt inclusion
- **Use [`src/mcp/tool.js`](https://github.com/hexdocom/lemonai/blob/main/src/mcp/tool.js)** only 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`](https://github.com/hexdocom/lemonai/blob/main/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`](https://github.com/hexdocom/lemonai/blob/main/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`](https://github.com/hexdocom/lemonai/blob/main/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`](https://github.com/hexdocom/lemonai/blob/main/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`](https://github.com/hexdocom/lemonai/blob/main/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.