# Adding a New Tool

How to add a new tool to the ows-coda AI agent. Tools are the primary mechanism for Claude to interact with backend services, execute queries, and perform actions on behalf of the user.

## Prerequisites

- Read `apps/server/src/ai/tools/handler-utils.ts` — the `ToolHandler` and `ToolHandlerObject` types
- Read an existing tool definition (e.g., `apps/server/src/ai/tools/account/definitions.ts`)
- Read the deferred loading config (`apps/server/src/ai/tools/deferred.ts`)

## Tool definition

Every tool is defined as a `ToolDefinition` object with a name, description, input schema, and metadata:

```typescript
// apps/server/src/ai/tools/account/definitions.ts
export const ACCOUNT_TOOLS: ToolDefinition[] = [
  {
    name: "search_accounts",
    description:
      "Searches for accounts by name. Returns a paginated list of matching " +
      "accounts with their IDs and names.",
    inputSchema: z.object({
      search_term: z.string().describe("Partial or full account name"),
      limit: z.number().int().optional().default(20),
    }),
    domain: "account",
    permission: "tools.account.query",
    label: "Looking up account",
    source: { type: "static", href: "/accounts", title: "Account Search" },
    core: true,
    hint: "Find accounts by name when you don't have an account ID.",
    examples: [
      { label: "Search by name", input: { search_term: "Sony Music" } },
    ],
  },
];
```

Key fields:

| Field         | Required | Description                                                                                           |
| ------------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `name`        | Yes      | Unique tool name (snake_case)                                                                         |
| `description` | Yes      | Claude sees this — be specific about what the tool does and when to use it                            |
| `inputSchema` | Yes      | Zod schema for input validation. Use `.describe()` on each field for Claude                           |
| `domain`      | Yes      | Tool category (e.g., `account`, `royalties`, `snowflake`)                                             |
| `permission`  | No       | Platform permission required (e.g., `tools.snowflake.query`)                                          |
| `label`       | No       | Progress step label shown to the user during execution                                                |
| `source`      | No       | Source link template for deep-linking to the platform UI                                              |
| `core`        | No       | If `true`, always sent to the model. If omitted, the tool is deferred (discovered via `search_tools`) |
| `hint`        | No       | Short usage hint for the tool catalog search                                                          |
| `examples`    | No       | Example inputs shown in the tool catalog                                                              |

## Handler implementation

### Plain `ToolHandler` (always available)

Use for tools whose backing service is always configured:

```typescript
// apps/server/src/ai/tools/account/handlers.ts
export function buildAccountHandlers(
  deps: HttpClientDeps,
): Record<string, ToolHandler> {
  return {
    search_accounts: async (input, headers) => {
      const { search_term, limit } = input as {
        search_term: string;
        limit?: number;
      };
      return safeGet(
        deps,
        `/search?query=${encodeURIComponent(search_term)}&limit=${limit ?? 20}`,
        headers,
      );
    },
    get_account_payee: async (input, headers) => {
      return getById(deps, input, "account_id", "/payee-info", headers);
    },
  };
}
```

### `ToolHandlerObject` (conditional availability)

Use when the tool depends on optional infrastructure (Snowflake pool, GraphQL gateway, Notion OAuth):

```typescript
export function buildSnowflakeHandlers(
  pool: SnowflakePool | null,
): Record<string, AnyToolHandler> {
  if (!pool) {
    // Return disabled stubs — tools won't appear in the model's tool list
    return {
      query_snowflake: {
        enabled: () => false,
        disabledReason: "Snowflake pool is not configured",
        execute: async () => ({ data: null, error: "Snowflake not available" }),
      },
    };
  }

  return {
    query_snowflake: {
      enabled: () => true,
      execute: async (input, headers) => {
        // ... handler logic using pool
      },
    },
  };
}
```

The `enabled()` predicate is checked at both surfacing time (catalog/deferred) and execution time (registry), so disabled tools are never sent to the model.

## Core vs deferred tools

To reduce prompt size and cost, tools are split into two tiers:

- **Core tools** (`core: true`) — always included in every request to Claude. Use for the most frequently needed tools and meta-tools like `search_tools`.
- **Deferred tools** (no `core` flag) — discovered via the `search_tools` meta-tool. When Claude calls `search_tools` with a query, matching tool definitions are added to the next round's tool config.

Most new tools should be **deferred** unless they're needed in the majority of conversations.

## Steps

### 1. Create a tool directory

Create `apps/server/src/ai/tools/<category>/` with `definitions.ts` and `handlers.ts`. Use an existing category if it fits.

### 2. Write definitions

Export a `ToolDefinition[]` array. Write clear descriptions — Claude relies on them to choose the right tool.

### 3. Implement the handler

Return `{ data, error }` from every handler. Use `safeGet`/`safePost` from `apps/server/src/services/http-client.ts` for downstream HTTP calls — they handle auth header forwarding, error sanitization, and timeout.

### 4. Register the handler

Add definitions to the barrel export in `apps/server/src/ai/tools/definitions.ts`. Register handlers in `apps/server/src/ai/tools/registry/handlers.ts` by adding them to the appropriate handler map (`buildDomainHandlers` for always-available, `buildSnowflakeHandlers`/`buildNotionHandlers` for conditional).

### 5. Update the system prompt

Edit `apps/server/src/ai/system-prompt.md` if the tool changes how Claude should reason about or present data. Not needed for straightforward CRUD tools.

### 6. Add tests

Test the handler logic. For tools with `enabled()` predicates, test both enabled and disabled states:

```typescript
it("returns disabled handlers when pool is null", () => {
  const handlers = buildSnowflakeHandlers(null);
  expect(isHandlerEnabled(handlers.query_snowflake)).toBe(false);
});
```

### 7. Update documentation

Add the tool to the [Tool Catalog](../api/tool-catalog.md).

## Checklist

- [ ] `definitions.ts` with `ToolDefinition[]` array
- [ ] `handlers.ts` with handler functions
- [ ] Handlers registered in `registry.ts`
- [ ] Definitions exported from `definitions.ts` barrel
- [ ] `core: true` set only if the tool is needed in most conversations
- [ ] `permission` field set if platform permissions are required
- [ ] `domain` field set for MCP domain filtering
- [ ] `source` field set if deep-links to the platform UI are applicable
- [ ] Tests for handler logic (both success and error cases)
- [ ] Tests for enabled/disabled states (if using `ToolHandlerObject`)
- [ ] System prompt updated (if applicable)
- [ ] Tool added to the [Tool Catalog](../api/tool-catalog.md)

## See also

- [CONTRIBUTING.md](../../CONTRIBUTING.md) for the full contribution workflow, PR process, and code conventions
- [Architecture](../architecture/server.md#tool-system) for the tool system architecture
- [Tool Catalog](../api/tool-catalog.md) for the complete list of existing tools
