# Tools Development Guide

This guide provides detailed information for developing tools in IntentKit.

## Overview

Tools are in the `intentkit/tools/` folder. Each folder is a category. Each tool category can contain multiple tools. A category can be a theme or a brand.

There are two ways to create a tool category:
1. **Native tools** — implement tools directly in Python (see [Tool Category Structure](#tool-category-structure))
2. **MCP-wrapped tools** — wrap a remote MCP server as a tool category with minimal code (see [MCP Tool Category Integration](#mcp-tool-category-integration))

## Dependency Rules

To avoid circular dependencies, Tools can only depend on the contents of models, abstracts, utils, and clients.

## Tool Category Structure

The necessary elements in a tool category folder are as follows. For the paradigm of each element, you can refer to existing tools, such as `tools/twitter`:

### 1. Base Class (`base.py`)

Base class inherit `IntentKitTool`. If there are functions that are common to this category, they can also be written in BaseClass. A common example is `get_api_key`.

### 2. Individual Tool Files

Each tool should have its own file, with the same name as the tool. Key points:

- **Class Inheritance**: The tool class inherit BaseClass created in `base.py`

- **Name Attribute**: The `name` attribute needs a same prefix as the category name, such as `twitter_`, for uniqueness in the system.

- **Description Attribute**: The `description` attribute is the description of the tool, which will be used in LLM to select the tool.

- **Args Schema**: The `args_schema` attribute is the pydantic model for the tool arguments.

- **Main Logic (`_arun` method)**: The `_arun` method is the main logic of the tool.
  - There is special parameter `config: RunnableConfig`, which is used to pass the LangChain runnable config.
  - There is function `context_from_config` in IntentKitTool, can be used to get the context from the runnable config.
  - In the `_arun` method, if there is any exception, just raise it, and the exception will be handled by the Agent.
  - If the return value is not a string, you can document it in the description attribute.

### 3. Initialization (`__init__.py`)

The `__init__.py` must have the function:
```python
async def get_tools(
    config: "Config",
    is_private: bool,
    **_,
) -> list[OpenAIBaseTool]
```

- **Config**: Config is inherit from `ToolsetConfig`, and the `states` is a dict, key is the tool name, value is the tool state. If the tool category have any other config fields need agent creator to set, they can be added to Config.

- **Caching**: If the tool is stateless, you can add a global `_cache` for it, to avoid re-create the tool object every time.

- **Availability Check**: The `__init__.py` must also have the function:
```python
def available() -> bool:
    """Check if this tool category is available based on system config."""
```
This function checks if all required system configuration variables exist. If the tool requires a platform-hosted API key (e.g., `config.tavily_api_key`), return whether that key is present. If the tool has no system config dependencies (e.g., only uses agent-owner provided keys), return `True`.


### 4. Visual Assets

A square image (icon/logo) is needed in the category folder. The `schema.json` must reference it via the `x-icon` field:
```json
"x-icon": "/tools/{category_name}/{icon_filename}.{ext}"
```
Supported formats: SVG, PNG, JPEG, WebP. Icons are served by the API at `GET /tools/{category}/{icon_name}.{ext}`.

### 5. Configuration Schema (`schema.json`)

Add `schema.json` file for the config, since the Config inherit from `ToolsetConfig`, you can check examples in exists tool category to find out the pattern.

The `x-tags` in schema should be in this list: AI, Analytics, Audio, Communication, Crypto, DeFi, Developer Tools, Entertainment, Identity, Image, Infrastructure, Knowledge Base, NFT, Search, Social

## Exception Handling

There is no need to catch exceptions in tools, because the agent has a dedicated module to catch tool exceptions. If you need to add more information to the exception, you can catch it and re-throw the appropriate exception.

---

## MCP Tool Category Integration

You can wrap any remote MCP (Model Context Protocol) server as an IntentKit tool category. The MCP framework handles tool discovery, schema generation, and runtime invocation automatically — you only need to register the server and run a sync script.

### Architecture

```
intentkit/clients/mcp/          # MCP protocol client (clients layer)
├── registry.py                 # Server definitions (McpServerDef)
└── client.py                   # HTTP client (SSE / Streamable HTTP transport)

intentkit/tools/mcp/            # MCP → IntentKit tool adapters (tools layer)
├── wrapper.py                  # McpCategoryModule — provides get_tools/available/Config
└── tool.py                     # McpToolTool — wraps individual MCP tools as IntentKit tools

intentkit/tools/mcp_{name}/    # Generated per-server tool category
├── __init__.py                 # Thin wrapper (auto-generated by sync script)
├── schema.json                 # Tool states + config (auto-generated by sync script)
└── {name}.{ext}                # Icon (manually added)

scripts/sync_mcp_schemas.py     # Generates the fixed schema.json + __init__.py boilerplate
```

> **Coarse, drift-proof config.** Remote MCP servers own their tool list and
> can change it at any time, so an MCP category does **not** snapshot or toggle
> individual tools. Its `schema.json` carries a single server-level visibility
> control (keyed by the server name); when enabled, the agent gets whatever
> tools the server currently offers, discovered live at runtime. Because the
> schema never enumerates tools, it can't go stale — no re-sync is needed when
> the server changes. The trade-off is no per-tool on/off in the UI. If you
> need per-tool control (or depend heavily on a provider's data), write a
> native tool category whose schema is version-controlled alongside the code.

### Step-by-Step: Adding a New MCP Server

#### 1. Add API key config (if needed)

If the MCP server requires an API key, add it to `intentkit/config/config.py`:
```python
self.my_service_api_key: str | None = self.load("MY_SERVICE_API_KEY")
```

#### 2. Register the server in `intentkit/clients/mcp/registry.py`

Add an entry to the `MCP_SERVERS` dict:
```python
"mcp_myservice": McpServerDef(
    name="mcp_myservice",               # Must match the dict key and tools/ folder name
    display_name="My Service",           # Human-readable name for UI
    description="What this service does",
    url="https://mcp.myservice.com/sse", # MCP server endpoint
    transport="sse",                     # "sse" or "streamable_http"
    api_key_config_attr="my_service_api_key",  # Attribute name in config.py (or None)
    api_key_header="Authorization",      # HTTP header for the key (or None)
    api_key_prefix="Bearer",             # Key prefix (or None for raw key)
    tags=["Developer Tools"],            # From the x-tags list above
),
```

Key fields:
- `name` — must be `mcp_{service}` and match the `MCP_SERVERS` dict key
- `transport` — `"sse"` for Server-Sent Events, `"streamable_http"` for HTTP streaming
- `api_key_config_attr` — set to `None` if the server needs no auth
- `api_key_prefix` — set to `None` to send the raw key without prefix

#### 3. Run the sync script

```bash
source .venv/bin/activate
python scripts/sync_mcp_schemas.py
```

This generates the boilerplate (the schema shape comes from the server
definition, not from the live tool list — the script only probes the server
for an informational reachability check):
- `intentkit/tools/mcp_myservice/__init__.py` — thin wrapper delegating to `McpCategoryModule`
- `intentkit/tools/mcp_myservice/schema.json` — `enabled`, the single server-level visibility toggle, and the optional `api_key` field

The sync script preserves any manually added `x-icon` field in existing schema.json files.

#### 4. Add an icon

Download the service's official logo (square, SVG/PNG/JPEG/WebP) into the tool folder:
```
intentkit/tools/mcp_myservice/myservice.svg
```

Then add the `x-icon` field to `schema.json` (after `"title"`):
```json
"x-icon": "/tools/mcp_myservice/myservice.svg",
```

#### 5. Verify

- The tool category is auto-discovered by the executor via `importlib.import_module(f"intentkit.tools.{k}")` — no manual registration needed.
- The `available()` check returns `True` if no API key is required, or if the system-level key is configured.
- Agent owners can also provide per-agent API keys via the `api_key` field in tool config.

### How It Works at Runtime

1. **Gating**: `McpCategoryModule.get_tools()` reads the single server-level visibility (`states[server_name]`). If it is `"public"`, or `"private"` and the caller is the owner, the toolset is on; otherwise it returns nothing.
2. **Discovery**: when on, the server is queried for its current tools (cached for 1 hour) and **all** of them are exposed — there is no per-tool filtering.
3. **Execution**: `McpToolTool._arun()` calls `call_mcp_tool()` which opens an MCP session, invokes the tool by its original (un-prefixed) name, and returns the text result.
4. **API key resolution**: Per-agent key (from tool config) takes priority over the system key (from env/config).
