# Tool Catalog

> Every assistant tool option — built-in tool-loop tools, MCP tools, and Site Control actions — and how to enable each per release.

A deployed assistant can do more than generate text. Divinci's tool surface spans
**three independent subsystems**, each with its own config and its own gate, and
**all three are off by default**:

| Subsystem | What it does | Configured via | Gate |
| --- | --- | --- | --- |
| **RP1 tool-loop** | In-chat tools the model calls during a send (search, fetch, RAG, media, CRM, drafts) | `Release.toolRouting` **+** server env | Double-gated (env + release) |
| **MCP tools** | The platform's full surface exposed to external MCP/agent clients | `Release.mcpConfig` | `mcpConfig.enabled` |
| **Site Control** | Navigate / submit forms on the embedded customer site | `Release.siteControl` | `siteControl.enabled` (THE WALL) |

<Aside type="caution" title="No CLI flags for these three">
  `divinci release update` has flags for RAG, cache, fallback, and more — but
  **not** for `toolRouting`, `mcpConfig`, or `siteControl`. Set those via the
  release-update API body (PATCH), the SDK (`client.releases.update*`), or MCP
  release tools. `clone` copies an existing `mcpConfig` forward but cannot set
  one fresh.
</Aside>

## RP1 tool-loop (in-chat tools)

**What it is.** When a chat turn is *routed*, the send path hands the message to a
tool-calling model that loops `call-model → execute-tool → feed-result` until the
model answers tool-free or a safety net trips. Use it when you want an in-chat
assistant that actually does things — searches the web, fetches a URL, retrieves
from RAG, generates media, or drafts an email/event/contact.

### The 11 tools

| Tool | Category | Side effect | In default set? |
| --- | --- | --- | --- |
| `webSearch` | read-only | none | ✅ |
| `fetchUrl` | read-only | none (SSRF-guarded) | ✅ |
| `ragSearch` | read-only | none | ✅ |
| `composeEmail` | draft | none — emits a deep-link draft | ✅ |
| `draftCalendarEvent` | draft | none — emits a deep-link draft | ✅ |
| `draftCrmContact` | draft | none — emits a deep-link draft | ✅ |
| `generateImage` | media | provider spend | ❌ opt-in |
| `generateVideo` | media | provider spend | ❌ opt-in |
| `generateDiagram` | media | provider spend | ❌ opt-in |
| `crmCreateContact` | mutation | creates/upserts a contact | ❌ opt-in |
| `crmLookup` | read-only | — (no implementation) | ❌ |

The three **draft tools** are read-only: they return a structured deep-link the
client turns into an "open in Gmail / Calendar / CRM" button — no server-side
write happens.

### Enabling it (double gate)

Routing requires **both** a server env gate **and** per-release config. Either one
alone does nothing.

```bash
# 1. Server env gate on the public-api service:
TOOL_ROUTING_ENABLED=true   # (or 1)
```

```bash
# 2. Per-release config (read-only default tool set):
curl -X PATCH https://api.stage.divinci.app/white-label/$WL/release/$RELEASE \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"toolRouting":{"enabled":true}}'
```

To opt media + CRM-create tools in (and accept their un-escrowed spend), set a
custom threshold, or pin the model, list them explicitly in `enabledTools`:

```bash
curl -X PATCH https://api.stage.divinci.app/white-label/$WL/release/$RELEASE \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"toolRouting":{
    "enabled":true,
    "threshold":0.5,
    "modelId":"@cf/moonshotai/kimi-k2.7-code",
    "enabledTools":[
      "webSearch","fetchUrl","ragSearch",
      "generateImage","generateDiagram","crmCreateContact",
      "composeEmail","draftCalendarEvent","draftCrmContact"
    ]
  }}'
```

SDK equivalent:

```typescript
await client.releases.updateInWorkspace(workspaceId, releaseId, {
  toolRouting: { enabled: true, enabledTools: ["webSearch", "ragSearch"] },
} as any); // set via the API body; not a typed CLI flag yet
```

### `toolRouting` fields and defaults

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `enabled` | boolean | `false` | Per-release gate (coerced to boolean by the API) |
| `threshold` | 0–1 | `0.6` | Tool-intent signal must cross this to route |
| `regexWeight` | 0–1 | `0.6` | Weight of the cheap regex prior in the signal |
| `modelId` | string | `@cf/moonshotai/kimi-k2.7-code` | Tool-calling model |
| `enabledTools` | string[] | the read-only `DEFAULT_ROUTED_TOOLS` set | Omit/empty = read-only set; list to add media/CRM |

`DEFAULT_ROUTED_TOOLS` = `webSearch`, `fetchUrl`, `ragSearch`, `composeEmail`,
`draftCalendarEvent`, `draftCrmContact` — i.e. **read-only / no un-escrowed
spend**. Media generators and `crmCreateContact` are excluded unless you add them.

### Tool-loop safety nets

The orchestrator enforces hard bounds you cannot relax via release config:

| Bound | Default |
| --- | --- |
| Max iterations | 8 |
| Max wall-clock | 90 000 ms |
| Repetition limit | 3 identical calls |
| Token budget | off (undefined) |
| Confirmation for mutations | always required |
| Per-tool-result size | bounded to 8000 chars |
| `ragSearch` topK | default 6, clamped to ≤ 20 |

### Gotchas

- **Double gate.** `TOOL_ROUTING_ENABLED=true` **and** `toolRouting.enabled=true`
  are both required. One without the other = no routing.
- **Default set is read-only only.** Media + `crmCreateContact` are opt-in because
  their provider cost currently **bypasses chat escrow** — enabling them means that
  spend is uncounted.
- **`crmLookup` has no implementation.** Even if advertised it always returns
  "CRM lookup is not available." Only create/upsert exists.
- **`crmCreateContact` needs an email.** If the model didn't supply one, the call
  short-circuits with a message rather than upserting an empty key.
- **Mutations always confirm.** `crmCreateContact` requires runtime confirmation
  regardless of config — `requireConfirmationForMutations` cannot be disabled.
- **`fetchUrl` is SSRF-guarded.** Private / loopback / link-local / metadata
  targets are rejected.
- **Routing reads a cheap signal, not a fresh classifier.** The send-time signal is
  the client-carried `toolLikelihood` or the regex prior — there is no per-turn 70B
  classifier call (removed for cost). A weak prompt won't route even with a low
  threshold.

## MCP tools

**What it is.** The MCP server (`workers/mcp-server`) exposes ~60+ named tools
across ~25 categories — chat, RAG, config, fine-tune, QA, analytics, terms, trust,
and more — to external MCP/agent clients. Use `mcpConfig` when you want to expose
some of that surface to an MCP client rather than to the in-chat model.

### Enabling it

```bash
curl -X PATCH https://api.stage.divinci.app/white-label/$WL/release/$RELEASE \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"mcpConfig":{
    "enabled":true,
    "exposedTools":["send_message","search_knowledge","get_transcript"],
    "allowAnonymousMcp":true,
    "maxSpendPerTokenCents":500,
    "mcpRateLimit":{"requestsPerMinute":30,"requestsPerDay":2000}
  }}'
```

### `mcpConfig` fields and defaults

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `enabled` | boolean | `false` | The MCP gate |
| `exposedTools` | string[] | empty = all release tools | Unknown names are **rejected** by the API |
| `allowedScopes` | string[] | — | Restricts callable scopes |
| `mcpRateLimit.requestsPerMinute` | number | `60` | Per-minute cap |
| `mcpRateLimit.requestsPerDay` | number | `10000` | Per-day cap |
| `allowAnonymousMcp` | boolean | `false` | Requires a positive `maxSpendPerTokenCents` |
| `maxSpendPerTokenCents` | cents | `1000` ($10) | Per-token spend cap |

### Gotchas

- **Unknown `exposedTools` are rejected.** Listing a tool name the server doesn't
  know fails validation (it would otherwise expose nothing).
- **Anonymous access needs a spend cap.** `allowAnonymousMcp: true` requires a
  positive `maxSpendPerTokenCents`.

## Site Control (navigation / action tools)

**What it is.** The crawler builds a `SiteManifest` of the embedded customer site;
Site Control turns that map into executable tools — a `navigate_site` action plus
per-form submit actions. Use it when you want the assistant to move around the
embedded site (and, opt-in, fill and submit forms with runtime confirmation).

<Aside type="caution" title="THE WALL — auto-activate ≠ auto-execute">
  The crawl-derived `siteManifest` is auto-activated, but the untrusted crawl can
  only *propose* capabilities — it can never *authorize* them. `deriveSiteActions`
  returns `[]` unless `siteControl.enabled` is true. `siteControl` is the separate
  authorization layer on top of the manifest.
</Aside>

### Enabling it

```bash
# Navigation only (THE WALL opt-in):
curl -X PATCH https://api.stage.divinci.app/white-label/$WL/release/$RELEASE \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"siteControl":{"enabled":true,"allowedCategories":["navigation"]}}'

# Add form submission (each submit action is confirmation-gated, always):
# -d '{"siteControl":{"enabled":true,"allowedCategories":["navigation","mutation"]}}'
```

### `siteControl` fields and defaults

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `enabled` | boolean | `false` | THE WALL gate |
| `allowedCategories` | `"navigation"` \| `"query"` \| `"mutation"` \| `"utility"`[] | `["navigation"]` | Which action categories are derived |
| `allowList` | string[] | — | Strictly validated |
| `blockList` | string[] | — | Strictly validated |

Manifest-derived limits: navigation route enum capped at 60, max 20 form actions,
action name length 48.

### Gotchas

- **Form submissions always confirm.** Site Control `mutation` / submit actions
  carry `requiresConfirmation: true`, which config cannot disable.
- **Sensitive fields are stripped.** Password / hidden inputs and any name matching
  `pass`/`pwd`/`card`/`cvv`/`ssn`/`secret`/`token`/`otp`/`pin`/`account-number`/`routing`
  never become fillable params.
- **Cross-origin hrefs are dropped** from navigation actions.

### Inspecting derived actions

```typescript


// Tool-loop: which tools a release advertises to the model
const toolDefs = selectToolDefinitions(
  release.toolRouting?.enabledTools?.length
    ? release.toolRouting.enabledTools
    : [...DEFAULT_ROUTED_TOOLS],
);

// Site Control: derived actions ([] unless siteControl.enabled)
const siteActions = deriveReleaseSiteActions(release);
```
