callwright 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -4
- package/package.json +2 -1
- package/place_call.schema.json +28 -17
- package/server.js +19 -0
package/README.md
CHANGED
|
@@ -96,18 +96,35 @@ Callwright dials through [Retell](https://retellai.com), so you need a Retell ac
|
|
|
96
96
|
|
|
97
97
|
---
|
|
98
98
|
|
|
99
|
+
## Install
|
|
100
|
+
|
|
101
|
+
Published on npm as [`callwright`](https://www.npmjs.com/package/callwright). Install globally to get the `callwright` (server) and `callwright-init` (setup wizard) commands:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npm install -g callwright
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Or clone this repo and `npm install` to run from source (`node server.js`, `node init.js`).
|
|
108
|
+
|
|
109
|
+
Either way, callwright is a **self-hosted, single-user HTTP MCP server**: you run your own instance with your own [Retell](https://retellai.com) credentials (see [Retell setup](#retell-setup-prerequisites) above). Nothing is shared or multi-tenant.
|
|
110
|
+
|
|
99
111
|
## Run locally
|
|
100
112
|
|
|
113
|
+
Using the global install:
|
|
114
|
+
|
|
101
115
|
```bash
|
|
102
|
-
|
|
103
|
-
node init.js # one-time setup wizard (number, agent, your identity)
|
|
116
|
+
callwright-init # one-time setup wizard (number, agent, your identity)
|
|
104
117
|
export RETELL_API_KEY=key_xxx # [Environment]::SetEnvironmentVariable on Windows
|
|
105
|
-
|
|
118
|
+
callwright # MCP server on :8787 (POST /mcp)
|
|
106
119
|
```
|
|
107
120
|
|
|
108
|
-
|
|
121
|
+
From a cloned repo, use `node init.js` and `node server.js` instead.
|
|
122
|
+
|
|
123
|
+
Check setup anytime: `callwright-init status` (or `node init.js status`).
|
|
109
124
|
Place a call from the CLI (bypassing MCP): `node dispatch.js <job.json> --go`.
|
|
110
125
|
|
|
126
|
+
> For **local** use, `MCP_AUTH_TOKEN` is optional — with it unset the server accepts loopback connections only. Set it once you expose the server beyond localhost (see [Deploy hosted](#deploy-hosted-single-user)).
|
|
127
|
+
|
|
111
128
|
---
|
|
112
129
|
|
|
113
130
|
## Deploy hosted (single-user)
|
|
@@ -135,6 +152,12 @@ docker run -p 8787:8787 --env-file .env callwright
|
|
|
135
152
|
```
|
|
136
153
|
Or point the host at this repo (it auto-detects the Dockerfile). Mount a volume at the app dir so `config.json` / `scenario-profiles.json` persist across restarts.
|
|
137
154
|
|
|
155
|
+
No-Docker alternative: install from npm on the host and run the binary directly (point `CALLWRIGHT_DATA_DIR` at your persistent volume so runtime state survives restarts):
|
|
156
|
+
```bash
|
|
157
|
+
npm install -g callwright
|
|
158
|
+
CALLWRIGHT_DATA_DIR=/data callwright # reads RETELL_API_KEY / MCP_AUTH_TOKEN from env
|
|
159
|
+
```
|
|
160
|
+
|
|
138
161
|
### 4. First-run setup over MCP (no webpage needed)
|
|
139
162
|
Do setup **in chat**:
|
|
140
163
|
1. Connect the server (below).
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "callwright",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"mcpName": "io.github.topness-msft/callwright",
|
|
4
5
|
"description": "A voice agent that places phone calls (reservations, appointments, inquiries) on your behalf, shaped by LLM direction + grounding. Runs fire-and-forget on Retell; an MCP server lets an LLM shape the grounding before each call.",
|
|
5
6
|
"main": "server.js",
|
|
6
7
|
"bin": {
|
package/place_call.schema.json
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
"call_type": {
|
|
11
11
|
"type": "string",
|
|
12
|
+
"title": "Call type / scenario id",
|
|
13
|
+
"examples": ["haircut", "restaurant_reservation", "dentist_cleaning", "general_inquiry"],
|
|
12
14
|
"description": "Open-ended scenario id, lowercase snake/kebab (e.g. 'haircut', 'restaurant_reservation', 'dentist_cleaning', 'general_inquiry'). If a matching scenario profile exists, it supplies recommended fields, default flexibility, direction hints, and confirm items. Unknown types fall back to generic behavior."
|
|
13
15
|
},
|
|
14
16
|
|
|
@@ -18,13 +20,14 @@
|
|
|
18
20
|
"required": ["business_name", "phone_number"],
|
|
19
21
|
"additionalProperties": false,
|
|
20
22
|
"properties": {
|
|
21
|
-
"business_name": { "type": "string", "description": "Name of the business, used by the agent when speaking." },
|
|
23
|
+
"business_name": { "type": "string", "title": "Business name", "description": "Name of the business, used by the agent when speaking." },
|
|
22
24
|
"phone_number": {
|
|
23
25
|
"type": "string",
|
|
24
|
-
"
|
|
26
|
+
"title": "Business phone number (E.164)",
|
|
27
|
+
"examples": ["+15555550199", "+81312345678"],
|
|
25
28
|
"pattern": "^\\+[1-9]\\d{6,14}$"
|
|
26
29
|
},
|
|
27
|
-
"timezone": { "type": "string", "description": "IANA tz of the business, e.g. America/New_York. Infer from area code if not given, then confirm." }
|
|
30
|
+
"timezone": { "type": "string", "title": "Business timezone (IANA)", "examples": ["America/New_York", "Asia/Tokyo"], "description": "IANA tz of the business, e.g. America/New_York. Infer from area code if not given, then confirm." }
|
|
28
31
|
}
|
|
29
32
|
},
|
|
30
33
|
|
|
@@ -34,15 +37,19 @@
|
|
|
34
37
|
"required": [],
|
|
35
38
|
"additionalProperties": false,
|
|
36
39
|
"properties": {
|
|
37
|
-
"name": { "type": "string", "description": "Name to give for the reservation/appointment. OPTIONAL — omit for general inquiries where no booking/identity is involved, so the agent does not attach the principal's name to the call." },
|
|
40
|
+
"name": { "type": "string", "title": "Principal name", "x-pii": true, "description": "Name to give for the reservation/appointment. OPTIONAL — omit for general inquiries where no booking/identity is involved, so the agent does not attach the principal's name to the call." },
|
|
38
41
|
"anonymous": { "type": "boolean", "description": "Set true for a truly nameless call (general inquiries). Suppresses backfill of name/callback_number from config and places the call with NO principal identity. Default false (basics are backfilled for convenience)." },
|
|
39
42
|
"callback_number": {
|
|
40
43
|
"type": "string",
|
|
41
|
-
"
|
|
44
|
+
"title": "Callback number (E.164)",
|
|
45
|
+
"x-pii": true,
|
|
46
|
+
"examples": ["+15555550142"],
|
|
42
47
|
"pattern": "^\\+[1-9]\\d{6,14}$"
|
|
43
48
|
},
|
|
44
49
|
"facts": {
|
|
45
50
|
"type": "object",
|
|
51
|
+
"title": "Per-call PII subset",
|
|
52
|
+
"x-pii": true,
|
|
46
53
|
"description": "MINIMAL per-call subset of standing facts about the principal that the agent may share ONLY IF the business asks (never volunteer). Keys are snake_case labels the agent speaks humanized (e.g. service_address, account_number, date_of_birth, member_id, vehicle, insurance_provider, loyalty_number). Values are strings/numbers/booleans. DATA MINIMIZATION: include ONLY what THIS call_type plausibly needs — do NOT dump the user's full profile. The host LLM selects this subset from the user's standing profile and/or connected context MCPs (WorkIQ, Gmail, calendar, CRM). This is the right home for PII: anything identifying belongs here (only-if-asked + surfaced in the read-back), NOT in scenario_details. Distinct from request.scenario_details (non-PII call specifics).",
|
|
47
54
|
"additionalProperties": { "type": ["string", "number", "boolean"] }
|
|
48
55
|
}
|
|
@@ -57,10 +64,14 @@
|
|
|
57
64
|
"properties": {
|
|
58
65
|
"summary": {
|
|
59
66
|
"type": "string",
|
|
67
|
+
"title": "Request summary",
|
|
68
|
+
"examples": ["Dinner reservation for 4", "Routine dental cleaning"],
|
|
60
69
|
"description": "One sentence the agent can act on, e.g. 'Dinner reservation for 4'. For appointments, include the service, e.g. 'Routine dental cleaning'."
|
|
61
70
|
},
|
|
62
71
|
"opening_ask": {
|
|
63
72
|
"type": "string",
|
|
73
|
+
"title": "Opening ask (spoken)",
|
|
74
|
+
"examples": ["I'm calling to ask whether you have a table for two this Friday at 6 PM.", "金曜日18時に2名で空席があるか伺えますでしょうか。"],
|
|
64
75
|
"description": "OPTIONAL. The first thing the agent says after disclosing it's an AI — leads with the goal/need, e.g. \"I'm calling to confirm an appointment. Could you help me with that?\". If omitted, one is derived from `summary`. Do NOT phrase as 'is now a good time?'."
|
|
65
76
|
},
|
|
66
77
|
"party_size": { "type": "integer", "minimum": 1, "description": "Required for reservations. Number of people." },
|
|
@@ -71,8 +82,8 @@
|
|
|
71
82
|
"required": ["date", "time"],
|
|
72
83
|
"additionalProperties": false,
|
|
73
84
|
"properties": {
|
|
74
|
-
"date": { "type": "string", "format": "date", "description": "YYYY-MM-DD. Resolve relative dates ('Saturday') against the business timezone and confirm the absolute date." },
|
|
75
|
-
"time": { "type": "string", "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$", "description": "24h HH:MM." }
|
|
85
|
+
"date": { "type": "string", "format": "date", "examples": ["2026-07-10"], "description": "YYYY-MM-DD. Resolve relative dates ('Saturday') against the business timezone and confirm the absolute date." },
|
|
86
|
+
"time": { "type": "string", "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$", "examples": ["19:00"], "description": "24h HH:MM." }
|
|
76
87
|
}
|
|
77
88
|
},
|
|
78
89
|
"acceptable_windows": {
|
|
@@ -142,19 +153,19 @@
|
|
|
142
153
|
"description": "Optional per-call overrides of the direction template defaults. Omit to use template defaults.",
|
|
143
154
|
"additionalProperties": false,
|
|
144
155
|
"properties": {
|
|
145
|
-
"time_flexibility_minutes": { "type": "integer", "minimum": 0 },
|
|
146
|
-
"disclose_ai": { "type": "boolean", "description": "Default true. Do not set false without a legal reason." },
|
|
147
|
-
"record_audio": { "type": "boolean", "description": "Default false. If true, agent must speak a consent line." },
|
|
148
|
-
"max_call_duration_seconds": { "type": "integer", "minimum": 30 },
|
|
149
|
-
"on_voicemail": { "type": "string", "enum": ["leave_message", "hang_up"] },
|
|
156
|
+
"time_flexibility_minutes": { "type": "integer", "minimum": 0, "default": 45, "title": "Time flexibility (minutes)" },
|
|
157
|
+
"disclose_ai": { "type": "boolean", "default": true, "description": "Default true. Do not set false without a legal reason." },
|
|
158
|
+
"record_audio": { "type": "boolean", "default": false, "description": "Default false. If true, agent must speak a consent line." },
|
|
159
|
+
"max_call_duration_seconds": { "type": "integer", "minimum": 30, "title": "Max call duration (seconds)" },
|
|
160
|
+
"on_voicemail": { "type": "string", "enum": ["leave_message", "hang_up"], "default": "leave_message" },
|
|
150
161
|
"retry": {
|
|
151
162
|
"type": "object",
|
|
152
163
|
"description": "Retry-on-no-answer policy. Defaults to exactly 1 retry; never raise max_retries above 1 without an explicit user request (repeated calls are spammy).",
|
|
153
164
|
"additionalProperties": false,
|
|
154
165
|
"properties": {
|
|
155
|
-
"max_retries": { "type": "integer", "minimum": 0, "maximum": 5, "description": "Number of re-dials on no-answer. Default 1. >1 only on explicit user request." },
|
|
156
|
-
"on": { "type": "array", "items": { "type": "string", "enum": ["no_answer", "busy", "failed"] } },
|
|
157
|
-
"delay_seconds": { "type": "integer", "minimum": 0, "maximum": 600 }
|
|
166
|
+
"max_retries": { "type": "integer", "minimum": 0, "maximum": 5, "default": 1, "description": "Number of re-dials on no-answer. Default 1. >1 only on explicit user request." },
|
|
167
|
+
"on": { "type": "array", "default": ["no_answer", "busy", "failed"], "items": { "type": "string", "enum": ["no_answer", "busy", "failed"] } },
|
|
168
|
+
"delay_seconds": { "type": "integer", "minimum": 0, "maximum": 600, "default": 60 }
|
|
158
169
|
}
|
|
159
170
|
}
|
|
160
171
|
}
|
|
@@ -165,8 +176,8 @@
|
|
|
165
176
|
"description": "Optional per-call 'text me when it's done'. Off by default — results are pulled on demand via get_call_outcome. Only set sms:true if the user asks to be notified.",
|
|
166
177
|
"additionalProperties": false,
|
|
167
178
|
"properties": {
|
|
168
|
-
"sms": { "type": "boolean", "description": "True = send the user an SMS when this call completes." },
|
|
169
|
-
"to": { "type": "string", "description": "The user's own number to text (E.164). If omitted, uses the saved notify number; if none, ask the user." }
|
|
179
|
+
"sms": { "type": "boolean", "default": false, "description": "True = send the user an SMS when this call completes." },
|
|
180
|
+
"to": { "type": "string", "title": "Notify number (E.164)", "x-pii": true, "description": "The user's own number to text (E.164). If omitted, uses the saved notify number; if none, ask the user." }
|
|
170
181
|
}
|
|
171
182
|
}
|
|
172
183
|
}
|
package/server.js
CHANGED
|
@@ -117,6 +117,7 @@ function buildServer() {
|
|
|
117
117
|
"get_setup_status",
|
|
118
118
|
{
|
|
119
119
|
title: "Get setup status",
|
|
120
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
120
121
|
description: "Report what callwright has configured (from-number, agent, your name/callback, fact keys) and what's missing. Call this first if you're unsure whether setup is complete; if basics are missing, ask the user for them and call configure.",
|
|
121
122
|
inputSchema: {},
|
|
122
123
|
},
|
|
@@ -127,6 +128,7 @@ function buildServer() {
|
|
|
127
128
|
"configure",
|
|
128
129
|
{
|
|
129
130
|
title: "Configure principal + (optional) provision infra",
|
|
131
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
130
132
|
description: "Save the user's standing profile so calls need less per-call input. Set name and callback_number (the always-safe basics). Optionally save standing facts (durable PII like service_address, member_id — a STORE the LLM later draws minimal subsets from). This is also the SAVE TARGET for the lazy-save nudge: when place_call returns a save_suggestion (durable facts not yet stored), offer to save them and, if the user agrees, call configure with those facts so they're not re-asked next time. Set run_provisioning=true to verify the Retell key, pick the phone number, and create the generic agent if missing. Gather values from the user in chat before calling.",
|
|
131
133
|
inputSchema: {
|
|
132
134
|
name: z.string().optional().describe("Name used when a call becomes a booking."),
|
|
@@ -164,6 +166,7 @@ function buildServer() {
|
|
|
164
166
|
"list_scenarios",
|
|
165
167
|
{
|
|
166
168
|
title: "List scenario profiles",
|
|
169
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
167
170
|
description: "List known scenario profiles (recommended details, default flexibility, aliases). Use to learn what grounding a given call type benefits from before constructing place_call.",
|
|
168
171
|
inputSchema: {},
|
|
169
172
|
},
|
|
@@ -181,6 +184,7 @@ function buildServer() {
|
|
|
181
184
|
"place_call",
|
|
182
185
|
{
|
|
183
186
|
title: "Place an outbound phone call",
|
|
187
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
184
188
|
description:
|
|
185
189
|
"Place a call on the user's behalf. DRY-RUN by default: returns a read-back (including exactly which personal data types will be available to the agent) and does NOT dial. Review it with the user, then call again with confirm:true to actually place the call. DATA MINIMIZATION: in principal.facts include ONLY what THIS call needs; put PII in principal.facts (shared only if asked), not in scenario_details (spoken proactively). For a truly nameless general inquiry set principal.anonymous:true. LANGUAGE: the agent SPEAKS the text fields you provide verbatim. When the call is not in English (set lang accordingly), you MUST write every spoken field in that language — request.summary, request.opening_ask, scenario_details values, preferences, must_confirm, and any constraints. Do NOT write them in English for a non-English call. Fire-and-forget: returns a call_id; read the outcome later with get_call_outcome.",
|
|
186
190
|
inputSchema: {
|
|
@@ -306,6 +310,7 @@ function buildServer() {
|
|
|
306
310
|
"get_call_outcome",
|
|
307
311
|
{
|
|
308
312
|
title: "Get call outcome",
|
|
313
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
309
314
|
description: "Fetch a call's outcome, post-call analysis, and transcript by call_id. Use after place_call (give it ~30-60s to complete + analyze).",
|
|
310
315
|
inputSchema: { call_id: z.string() },
|
|
311
316
|
},
|
|
@@ -321,6 +326,7 @@ function buildServer() {
|
|
|
321
326
|
"list_recent_calls",
|
|
322
327
|
{
|
|
323
328
|
title: "List recent calls",
|
|
329
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
324
330
|
description: "List recent calls in this Retell workspace (most recent first).",
|
|
325
331
|
inputSchema: { limit: z.number().int().min(1).max(50).optional() },
|
|
326
332
|
},
|
|
@@ -341,6 +347,7 @@ function buildServer() {
|
|
|
341
347
|
"list_retries",
|
|
342
348
|
{
|
|
343
349
|
title: "List call retries",
|
|
350
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
344
351
|
description: "Show recent automatic call retries (no-answer/busy/failed re-dials), most recent first. Retries default to at most 1 per call and never auto-escalate. Each entry shows the original call, the attempt number/cap, the new call_id (if placed), and status (scheduled/placed/failed).",
|
|
345
352
|
inputSchema: { limit: z.number().int().min(1).max(100).optional() },
|
|
346
353
|
},
|
|
@@ -354,6 +361,7 @@ function buildServer() {
|
|
|
354
361
|
"configure_notifications",
|
|
355
362
|
{
|
|
356
363
|
title: "Configure SMS notifications",
|
|
364
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
357
365
|
description: "Set up the optional 'text me when the call is done' feature. Saves the user's mobile number as the default notify target, provisions the one-time SMS summary agent if needed, and (with test:true) sends a test SMS now so the user can confirm delivery. Per-call SMS is still opt-in via place_call notify.sms — this just stores the number + infra. NOTE: the Retell from-number must be SMS-capable (KYC-gated).",
|
|
358
366
|
inputSchema: {
|
|
359
367
|
sms_to: z.string().optional().describe("The user's mobile number (E.164) to text by default."),
|
|
@@ -403,6 +411,7 @@ function buildServer() {
|
|
|
403
411
|
"learn_from_call",
|
|
404
412
|
{
|
|
405
413
|
title: "Learn from a call",
|
|
414
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
406
415
|
description: "Improve the system from a completed call. If the call_type matches a profile, it's ENRICHED (deferred questions become recommended_details, value-baked ones rejected). If it has NO profile, the (generalized) scenario is STAGED as a candidate; once seen >=2 times it becomes a PROPOSAL you can review and create via create_profile. Creation is never silent — it's proposed for you to approve (or merge into an existing profile with add_scenario_alias).",
|
|
407
416
|
inputSchema: {
|
|
408
417
|
call_id: z.string(),
|
|
@@ -447,6 +456,7 @@ function buildServer() {
|
|
|
447
456
|
"list_candidates",
|
|
448
457
|
{
|
|
449
458
|
title: "List scenario candidates",
|
|
459
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
450
460
|
description: "Show staged scenario candidates (unmatched call types seen but not yet a profile) with their occurrence counts, variant names, and the questions agents had to defer. Candidates seen >=2 times are 'ready' to propose as a new profile (create_profile) or merge into an existing one (add_scenario_alias).",
|
|
451
461
|
inputSchema: {},
|
|
452
462
|
},
|
|
@@ -469,6 +479,7 @@ function buildServer() {
|
|
|
469
479
|
"create_profile",
|
|
470
480
|
{
|
|
471
481
|
title: "Create a scenario profile",
|
|
482
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
472
483
|
description: "Create a NEW scenario profile (propose-and-confirm — call this only after reviewing a learn_from_call proposal with the user). GUARDS: the name must be a GENERAL scenario (not instance-shaped like 'dinner_for_2'); recommended_details must be GENERALIZED field keys ('destination', not 'destination_haneda') — values belong in the per-call job, never the profile. If the scenario is really the same as an existing profile, use add_scenario_alias instead. Clears the matching candidate on success.",
|
|
473
484
|
inputSchema: {
|
|
474
485
|
name: z.string().describe("snake_case general scenario id, e.g. 'hotel_transport_inquiry'."),
|
|
@@ -504,6 +515,7 @@ function buildServer() {
|
|
|
504
515
|
"add_scenario_alias",
|
|
505
516
|
{
|
|
506
517
|
title: "Add an alias to a scenario profile",
|
|
518
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
507
519
|
description: "Merge a call_type into an EXISTING profile by adding it as an alias (the anti-splintering 'merge, don't create' path). Use when a candidate / new call_type is really the same scenario as a profile you already have. Clears the matching candidate.",
|
|
508
520
|
inputSchema: {
|
|
509
521
|
profile: z.string().describe("The existing profile to merge into (e.g. 'hotel_transport_inquiry')."),
|
|
@@ -532,6 +544,7 @@ function buildServer() {
|
|
|
532
544
|
"reject_candidate",
|
|
533
545
|
{
|
|
534
546
|
title: "Reject a scenario candidate",
|
|
547
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
535
548
|
description: "Discard a staged scenario candidate that is noise or a one-off (not a real recurring scenario). Removes it from the candidates store.",
|
|
536
549
|
inputSchema: { scenario: z.string().describe("The candidate scenario name to drop (from list_candidates).") },
|
|
537
550
|
},
|
|
@@ -549,6 +562,7 @@ function buildServer() {
|
|
|
549
562
|
"list_voices",
|
|
550
563
|
{
|
|
551
564
|
title: "List Retell voices",
|
|
565
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
552
566
|
description: "List available Retell voices (for picking a voice when adding a language). Optionally filter by accent (e.g. 'French', 'Japanese') or a free-text query (name/provider/accent). NOTE: Retell voices have an accent, not a hard language field, and most are multilingual — accent is a hint, not a guarantee. Validate the final choice with a verification call.",
|
|
553
567
|
inputSchema: {
|
|
554
568
|
accent: z.string().optional().describe("Filter by accent substring, e.g. 'French', 'Spanish', 'Japanese'."),
|
|
@@ -567,6 +581,7 @@ function buildServer() {
|
|
|
567
581
|
"list_languages",
|
|
568
582
|
{
|
|
569
583
|
title: "List call languages",
|
|
584
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
570
585
|
description: "List the call languages this server can conduct calls in: the built-in English base plus any added languages (with their agent, voice, Retell language code, and whether prompt/phrase assets are on the volume). Use to see what's available before place_call or add_language.",
|
|
571
586
|
inputSchema: {},
|
|
572
587
|
},
|
|
@@ -577,6 +592,7 @@ function buildServer() {
|
|
|
577
592
|
"add_language",
|
|
578
593
|
{
|
|
579
594
|
title: "Add a call language",
|
|
595
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
580
596
|
description: "Add a NEW call language at runtime (no redeploy). Two-phase: call with just lang + display_name to get back the base prompt + English phrase block to TRANSLATE; then call again with prompt_text + phrases (translated, preserving every {{variable}} and {placeholder}) to create the language agent. Pick a native-accent voice_id via list_voices (optional — defaults to the English house voice, which you should override for best quality). English cannot be added (it's the built-in base). After it succeeds, run a verification call before production.",
|
|
581
597
|
inputSchema: {
|
|
582
598
|
lang: z.string().regex(/^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/, "BCP-47 code, e.g. 'fr', 'es', 'pt-BR'").describe("BCP-47 language code to add (e.g. 'fr', 'es', 'de', 'pt-BR')."),
|
|
@@ -600,6 +616,7 @@ function buildServer() {
|
|
|
600
616
|
"verify_language",
|
|
601
617
|
{
|
|
602
618
|
title: "Verify a call language",
|
|
619
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
603
620
|
description: "Quality-gate a newly added language before production. DRY-RUN by default: returns a review card with the EXACT spoken opening (greeting + your ask + AI disclosure) and voicemail message — composed by the language's agent — plus a native-review checklist. To HEAR it live, pass to:<your phone> and confirm:true and the agent will call you so you can judge accent, etiquette, and the disclosure by ear. For the truest review, pass target-language sample_summary and sample_opening_ask.",
|
|
604
621
|
inputSchema: {
|
|
605
622
|
lang: z.string().describe("The registered language code to verify (e.g. 'fr')."),
|
|
@@ -663,6 +680,7 @@ function buildServer() {
|
|
|
663
680
|
"update_language",
|
|
664
681
|
{
|
|
665
682
|
title: "Update a call language",
|
|
683
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
666
684
|
description: "Change an already-added language: replace its prompt_text and/or phrases (re-translated), and/or change its voice_id, Retell language code, interruption_sensitivity, or stt_mode. Only the provided fields change. Use list_languages to see what's registered.",
|
|
667
685
|
inputSchema: {
|
|
668
686
|
lang: z.string().describe("The registered language code to update (e.g. 'fr')."),
|
|
@@ -685,6 +703,7 @@ function buildServer() {
|
|
|
685
703
|
"remove_language",
|
|
686
704
|
{
|
|
687
705
|
title: "Remove a call language",
|
|
706
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
688
707
|
description: "Unregister an added language and delete its volume prompt + phrase assets. By default the Retell agent is LEFT in your dashboard (set delete_agent:true to delete it too). English cannot be removed.",
|
|
689
708
|
inputSchema: {
|
|
690
709
|
lang: z.string().describe("The registered language code to remove (e.g. 'fr')."),
|