> ## Documentation Index
> Fetch the complete documentation index at: https://docs.leanmcp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools

> Create actions AI agents can perform

# Tools

Tools are actions AI can perform — the primary way AI interacts with your system. When an AI agent needs to do something (send email, create task, query database), it calls a tool.

<Note>
  **Tools are triggered by the AI agent, not the user.** The LLM decides when to call a tool based on the user's request and the tool's description. Users don't directly invoke tools — they ask the AI, and the AI decides which tools to use.
</Note>

### Testing Your Tools

Since tools are AI-triggered, you'll need a way to test them:

* **MCP Inspector** — `npx @modelcontextprotocol/inspector http://localhost:3001/mcp`
* **Claude Desktop / Cursor** — Connect your MCP and chat with it
* **LeanMCP Sandbox** — Test tools directly in the browser
* **Postman** — Send raw MCP requests to your server

<Tip>
  Every tool you add increases token usage. Keep descriptions concise and only expose tools the AI actually needs. See [Reducing Tokens in MCPs](/guides/reducing-tokens) for optimization strategies.
</Tip>

## Tool Structure

```typescript theme={null}
import { Tool, SchemaConstraint, Optional } from "@leanmcp/core";

// 1. Define input schema with descriptions for AI
class YourToolInput {
  @SchemaConstraint({ description: "Describe what this param is for" })
  requiredParam!: string;

  @Optional()
  @SchemaConstraint({ description: "Optional param", default: 10 })
  optionalParam?: number;
}

// 2. Create service class
export class YourService {
  // 3. Decorate method with @Tool + inputClass
  @Tool({ 
    description: "Clear description of what this tool does",
    inputClass: YourToolInput 
  })
  async yourToolName(input: YourToolInput) {
    // 4. Implementation
    return { success: true, data: "result" };
  }
}
```

## Full Example: Task Manager

A complete task management service with CRUD operations:

```typescript theme={null}
// mcp/tasks/index.ts
import { Tool, Resource, SchemaConstraint, Optional } from "@leanmcp/core";

// In-memory store (replace with database in production)
interface Task {
  id: string;
  title: string;
  description: string;
  status: "todo" | "in_progress" | "done";
  priority: "low" | "medium" | "high";
  createdAt: Date;
  updatedAt: Date;
}

const tasks: Map<string, Task> = new Map();

// --- Input Schemas ---

class CreateTaskInput {
  @SchemaConstraint({ description: "Task title" })
  title!: string;

  @Optional()
  @SchemaConstraint({ description: "Task description" })
  description?: string;

  @Optional()
  @SchemaConstraint({ 
    description: "Priority level", 
    enum: ["low", "medium", "high"], 
    default: "medium" 
  })
  priority?: "low" | "medium" | "high";
}

class UpdateTaskInput {
  @SchemaConstraint({ description: "Task ID to update" })
  id!: string;

  @Optional()
  @SchemaConstraint({ description: "New title" })
  title?: string;

  @Optional()
  @SchemaConstraint({ description: "New status", enum: ["todo", "in_progress", "done"] })
  status?: "todo" | "in_progress" | "done";

  @Optional()
  @SchemaConstraint({ description: "New priority", enum: ["low", "medium", "high"] })
  priority?: "low" | "medium" | "high";
}

class DeleteTaskInput {
  @SchemaConstraint({ description: "Task ID to delete" })
  id!: string;
}

class ListTasksInput {
  @Optional()
  @SchemaConstraint({ description: "Filter by status", enum: ["todo", "in_progress", "done"] })
  status?: "todo" | "in_progress" | "done";

  @Optional()
  @SchemaConstraint({ description: "Filter by priority", enum: ["low", "medium", "high"] })
  priority?: "low" | "medium" | "high";
}

// --- Service ---

export class TaskService {
  
  @Tool({ description: "Create a new task", inputClass: CreateTaskInput })
  createTask(input: CreateTaskInput) {
    const id = `task_${Date.now()}`;
    const now = new Date();
    
    const task: Task = {
      id,
      title: input.title,
      description: input.description || "",
      status: "todo",
      priority: input.priority || "medium",
      createdAt: now,
      updatedAt: now
    };
    
    tasks.set(id, task);
    
    return {
      created: true,
      task: { id: task.id, title: task.title, status: task.status, priority: task.priority }
    };
  }

  @Tool({ description: "Update an existing task", inputClass: UpdateTaskInput })
  updateTask(input: UpdateTaskInput) {
    const task = tasks.get(input.id);
    if (!task) throw new Error(`Task ${input.id} not found`);
    
    if (input.title) task.title = input.title;
    if (input.status) task.status = input.status;
    if (input.priority) task.priority = input.priority;
    task.updatedAt = new Date();
    
    tasks.set(input.id, task);
    
    return {
      updated: true,
      task: { id: task.id, title: task.title, status: task.status, priority: task.priority }
    };
  }

  @Tool({ description: "Delete a task", inputClass: DeleteTaskInput })
  deleteTask(input: DeleteTaskInput) {
    const task = tasks.get(input.id);
    if (!task) throw new Error(`Task ${input.id} not found`);
    
    tasks.delete(input.id);
    return { deleted: true, id: input.id, title: task.title };
  }

  @Tool({ description: "List tasks with optional filters", inputClass: ListTasksInput })
  listTasks(input: ListTasksInput) {
    let result = Array.from(tasks.values());
    
    if (input.status) result = result.filter(t => t.status === input.status);
    if (input.priority) result = result.filter(t => t.priority === input.priority);
    
    return {
      count: result.length,
      tasks: result.map(t => ({ id: t.id, title: t.title, status: t.status, priority: t.priority }))
    };
  }

  @Resource({ description: "Task statistics", mimeType: "application/json" })
  getStats() {
    const all = Array.from(tasks.values());
    return {
      total: all.length,
      todo: all.filter(t => t.status === "todo").length,
      inProgress: all.filter(t => t.status === "in_progress").length,
      done: all.filter(t => t.status === "done").length
    };
  }
}
```

**How AI uses this:**

* "Create a task called 'Review PR #42' with high priority"
* "List all tasks that are in progress"
* "Mark task\_1234 as complete"
* "Delete the task about reviewing PR"

## Schema Validation

Use `@SchemaConstraint` for input validation and better AI understanding:

```typescript theme={null}
import { Tool, SchemaConstraint, Optional } from "@leanmcp/core";

class SendEmailInput {
  @SchemaConstraint({ description: "Recipient email address", format: "email" })
  to!: string;

  @SchemaConstraint({ description: "Email subject line" })
  subject!: string;

  @SchemaConstraint({ description: "Email body content" })
  body!: string;

  @Optional()
  @SchemaConstraint({ 
    description: "Priority", 
    enum: ["low", "normal", "high"], 
    default: "normal" 
  })
  priority?: string;
}

export class EmailService {
  @Tool({ description: "Send an email to a recipient", inputClass: SendEmailInput })
  async sendEmail(input: SendEmailInput) {
    // Send email implementation
    return { sent: true, messageId: `msg_${Date.now()}` };
  }
}
```

## Return Types

Tools can return different types:

<Tabs>
  <Tab title="Objects">
    ```typescript theme={null}
    @Tool({ description: "Get user" })
    getUser(input: { id: string }) {
      return { name: "John", email: "john@example.com" };
    }
    ```
  </Tab>

  <Tab title="Text">
    ```typescript theme={null}
    @Tool({ description: "Generate report" })
    generateReport(input: { type: string }) {
      return "Report generated successfully";
    }
    ```
  </Tab>

  <Tab title="Images">
    ```typescript theme={null}
    @Tool({ description: "Get image" })
    getImage(input: { id: string }) {
      return {
        content: [
          { type: "image", data: base64Data, mimeType: "image/png" }
        ]
      };
    }
    ```
  </Tab>
</Tabs>

## Async Tools

Tools can be async for API calls, database queries, file operations:

```typescript theme={null}
@Tool({ description: "Search database" })
async search(input: { query: string }) {
  const results = await db.query(input.query);
  return { results, count: results.length };
}
```

## Error Handling

Throw errors — they're caught and returned properly to the AI:

```typescript theme={null}
@Tool({ description: "Delete user" })
async deleteUser(input: { id: string }) {
  const user = await db.users.find(input.id);
  if (!user) {
    throw new Error(`User ${input.id} not found`);
  }
  await db.users.delete(input.id);
  return { deleted: true };
}
```

## Organizing Services

Group related tools into services. One file per domain:

```
mcp/
├── email/index.ts       # EmailService - send, draft, search
├── calendar/index.ts    # CalendarService - create, list, update events
├── contacts/index.ts    # ContactsService - lookup, create, update
└── analytics/index.ts   # AnalyticsService - reports, metrics
```

## Best Practices

<AccordionGroup>
  <Accordion title="Write clear descriptions">
    AI uses descriptions to understand when to use tools.

    **Bad:** `"Process data"`

    **Good:** `"Search customer orders by date range and status"`
  </Accordion>

  <Accordion title="Design for minimal tool calls">
    Every tool call costs tokens and time. Design tools that accomplish goals in **one call**, not a chain of 7-8 calls.

    **❌ BAD: Hotel booking with 7+ tool calls**

    ```mermaid theme={null}
    flowchart LR
      A[searchCities] --> B[getLocations]
      B --> C[listHotels]
      C --> D[getRoomTypes]
      D --> E[getQuote]
      E --> F[holdRoom]
      F --> G[processPayment]
      G --> H[issueConfirmation]
    ```

    Each call: AI generates request → waits for response → processes → generates next request. **7 round trips, thousands of tokens.**

    **✅ GOOD: One tool that does it all**

    ```typescript theme={null}
    class BookHotelInput {
      @SchemaConstraint({ description: "City name" })
      city!: string;
      
      @SchemaConstraint({ description: "Check-in date (YYYY-MM-DD)" })
      checkIn!: string;
      
      @SchemaConstraint({ description: "Check-out date (YYYY-MM-DD)" })
      checkOut!: string;
      
      @SchemaConstraint({ description: "Number of guests" })
      guests!: number;
      
      @Optional()
      @SchemaConstraint({ description: "Max price per night in USD" })
      maxPrice?: number;
    }

    @Tool({ 
      description: "Book a hotel room. Returns available options with prices, or confirms booking if user approves.", 
      inputClass: BookHotelInput 
    })
    async bookHotel(input: BookHotelInput) {
      // All logic happens server-side in one call
      const options = await this.findAndPriceRooms(input);
      return { 
        options: options.slice(0, 3),  // Top 3 choices
        message: "Reply with option number to confirm booking"
      };
    }
    ```

    **One call, one response.** The server handles the complexity, not the AI.
  </Accordion>

  <Accordion title="Keep tools focused">
    One tool, one job. Don't combine unrelated actions.

    **Bad:** `"manageUser"` (create, update, delete in one)

    **Good:** `"createUser"`, `"updateUser"`, `"deleteUser"`
  </Accordion>

  <Accordion title="Validate all inputs">
    Use `@SchemaConstraint` for all inputs. AI makes fewer mistakes with validated schemas.
  </Accordion>

  <Accordion title="Return meaningful data">
    Return useful data AI can use in follow-up actions.

    **Bad:** `return { success: true }`

    **Good:** `return { created: true, id: "123", name: "John" }`
  </Accordion>

  <Accordion title="Handle errors gracefully">
    Throw clear errors. AI can explain issues to users.
  </Accordion>
</AccordionGroup>

## The Three MCP Primitives

Understanding when to use each:

| Primitive     | Control            | Purpose                 | Example                            |
| ------------- | ------------------ | ----------------------- | ---------------------------------- |
| **Tools**     | Model-driven       | Actions the AI performs | Send email, create task, query API |
| **Resources** | Application-driven | Context data for AI     | Files, preferences, schedules      |
| **Prompts**   | User-driven        | Templates users invoke  | `/analyze`, `/review`, `/support`  |

## Next Steps

<CardGroup cols={2}>
  <Card title="Resources" icon="database" href="/core-concepts/resources">
    Expose data to AI agents
  </Card>

  <Card title="Prompts" icon="message" href="/core-concepts/prompts">
    Template prompts for AI
  </Card>
</CardGroup>
