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

# Voice tool dispatcher

# Voice Tool Dispatcher

## Overview

`POST /api/incoming-call-webhook-tool/[name]` is the dynamic catch-all route
ElevenLabs calls for every voice tool. The tool name is the URL slug — e.g.
`/api/incoming-call-webhook-tool/skills`. The route dispatches through
the shared `executeTool()` registry in
`app/api/claude/tools/executors/index.ts`, which means voice picks up the
same tools, truncation rules, and logging as chat / SMS / email.

This **coexists with** the 42 legacy static routes (`.../get-financial-metrics/`,
`.../get-crypto-holdings/`, etc.). Next.js resolves static routes before
dynamic ones, so the existing per-tool files keep working unchanged — the
`[name]` route only handles slugs with no static file (e.g. `skills`,
`execute_sql`). Once Phase 4 deletes the 42 static directories, `[name]`
absorbs every call and ElevenLabs keeps hitting the same base URL the whole
time. No URL migration, no rollback risk.

## Request contract

ElevenLabs sends JSON to `POST /api/incoming-call-webhook-tool/<tool_name>`:

```
POST /api/incoming-call-webhook-tool/skills
x-tool-secret: <ELEVENLABS_TOOL_SECRET>
Content-Type: application/json

{
  "user_id": "user_abc123",
  "op": "search",
  "command": "cat /mnt/s3files/shared/silvia-database-skills/SKILL.md"
}
```

| Location | Field | Required | Notes |
| - | - | - | - |
| URL slug | `<tool_name>` | yes | Executor name in kebab or snake case. `get-current-time` and `get_current_time` both work. |
| Body | `user_id` | yes | Clerk user ID; becomes `context.clerkId` inside the executor. |
| Body | tool-specific args | varies | Everything in the body except `user_id` / `prompt` is passed as the tool's `input`. |
| Headers | `x-tool-secret` | yes | Must match `ELEVENLABS_TOOL_SECRET`. |

### Response

Success (200):

```json theme={null}
{
  "tool": "skills",
  "result": { "...": "tool-specific JSON or {\"raw\": \"...\"} if non-JSON" }
}
```

Failure:

* `400` — malformed JSON body, missing tool name in URL, or missing/invalid `user_id` in body
* `401` — bad or missing `x-tool-secret`
* `403` — tool is not opted into the `voice` surface
* `500` — `ELEVENLABS_TOOL_SECRET` not configured, or the dispatcher itself threw before reaching the executor

Executor-level failures (database errors, bad SQL, Finnhub 4xx, etc.) come
back **in-band** as a `200` with the executor's own error JSON in the
`result` field — matching the shape chat/SMS/email see. The `500` path is
reserved for the dispatcher itself misbehaving.

## Surface eligibility

A tool is only callable over voice if it's listed in the local
`VOICE_ALLOWED_TOOLS` set at the top of the dispatcher route. Tools not
opted in return 403 even if the executor exists — this keeps chat-only
tools (writes, PDF generators) from leaking to telephony.

Current voice-enabled tools. Each must exist in the shared executor registry
(`toolRegistry` in `app/api/claude/tools/executors/index.ts`); the dispatcher
allowlist is a strict subset of it, so a tool that was removed or renamed
upstream must also be dropped here (otherwise it 403-passes and comes back as
an in-band "Unknown tool"):

* `get_current_time`
* `skills` (POSIX shell over the skills filesystem + persistent personal-memory writes — previously unreachable from voice; voice has the same `op="search" | "write" | "delete"` access chat does, since memory is shared across surfaces)
* `send_email` (parity with chat/SMS/email; voice already supports email via the legacy `app/api/incoming-call-webhook-send-email-tool` endpoint that the ElevenLabs system prompt drives — keeping it on the dispatcher avoids a regression once Phase 4 deletes the legacy routes)
* `execute_sql` (read financial data — radar queries and the financial-statement schedule are now expressed as SQL rather than dedicated tools)
* `execute_python` (Lambda microVM sandbox — Finnhub SDK, pandas, numpy, requests, S3 session save/load; also serves web search and historical/deep research, which no longer have standalone tools)

Specialized read tools voice used to call directly (`search_web`,
`get_radar_events` / `get_radar_event_by_id` / `get_radar_results`,
`get_current_financial_statement_schedule`, `fetch_historical_deep_research`)
were consolidated into `execute_sql` / `execute_python` upstream and are no
longer separate registry entries — so they're intentionally absent here.

Adding a new tool to voice:

1. Append the snake\_case name to `VOICE_ALLOWED_TOOLS` in the dispatcher.
2. Register a Server Tool with the same name in the ElevenLabs dashboard.

A follow-up PR will consolidate this local allowlist with SMS/email's
inline exclusion arrays into a single per-surface map.

## ElevenLabs agent configuration

In the ElevenLabs dashboard, create one **Server Tool** per voice-enabled
tool. Each tool gets its own URL slug; the body just carries the tool's own
params plus `user_id`.

Example — the `skills` server tool:

* **Method**: `POST`
* **URL**: `https://<app-host>/api/incoming-call-webhook-tool/skills`
* **Headers**:
  * `Content-Type: application/json`
  * `x-tool-secret: {{ELEVENLABS_TOOL_SECRET}}` (from agent secret)
* **Body template** (JSON):
  ```json theme={null}
  {
    "user_id": "{{user_id}}",
    "op": "{{op}}",
    "command": "{{command}}",
    "filename": "{{filename}}",
    "content": "{{content}}"
  }
  ```
  Same input shape as the chat tool: `op` is `search | write | delete`,
  `command` is required for search, `filename` for write/delete,
  `content` for write. The dispatcher only checks the tool name —
  per-op argument validation happens inside the executor.
* **Description / parameter schema**: copy verbatim from the matching entry
  in `regularTools` in `app/api/claude/tools/definitions.ts` so the voice
  LLM sees the same tool docs the chat LLM gets.

Repeat once per voice-enabled tool. Each has its own URL, natural body
shape, and same auth header.

The full, authoritative agent system prompt lives in
[`./voice-system-prompt.md`](./voice-system-prompt.md). It currently targets
the legacy 42-route architecture — a follow-up PR will update it to teach
the voice model about the new dispatcher tools alongside (or in place of)
the legacy ones.

## Debugging

Every dispatch is logged:

* `[Voice Tool] Dispatching <tool_name>` — accepted request (no clerkId in
  the dispatcher's own log line to keep raw user identifiers out of every
  request log; per-user tracing flows through the shared tool-logger
  instead, same as chat/SMS/email)
* `[Tool Execute] <tool_name>:` — underlying executor logging (same as chat)
* `[Voice Tool] Unauthorized dispatch attempt` — 401
* `[Voice Tool] Tool "<name>" is not enabled for voice surface` — 403
* `[Voice Tool] Dispatcher error: <error>` — server-side-only log of the
  full exception; the client always receives a generic 500 body

Tool outputs flow into the shared `logToolSuccess` / `logToolError`
helpers, so voice traffic is visible in the same place as chat/SMS/email.
(`logs/tool-calls.log` is dev-only — file writes are a no-op in production.)

## Migration plan

The 42 legacy static routes under `app/api/incoming-call-webhook-tool/*`
predate the shared executor registry. They each do a hand-rolled auth check
and call one specific data-cache function — no skills access, no shared
logging, no truncation.

1. **Phase 2 (shipped)** — dynamic `[name]` route added under the same
   directory. Static routes keep serving the 42 existing URLs; the dynamic
   route handles any new slug that has no static file.
2. **Phase 3** — configure new ElevenLabs server tools at the new slugs
   (`/skills`, `/execute_sql`, etc.). Update the agent system prompt
   in the ElevenLabs dashboard to match the latest
   [`./voice-system-prompt.md`](./voice-system-prompt.md) once it's been
   updated to reference the new dispatcher tools. Staging canary first.
   No change to the 42 legacy tools during this phase — they keep working,
   giving us a risk-free rollback path.
3. **Phase 4** — once production has baked on the new tools, delete the 42
   static route directories. The dynamic `[name]` route handles everything
   that remains. ElevenLabs URLs are unchanged from Phase 3.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.