callwright 1.0.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "callwright",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "mcpName": "io.github.topness-msft/callwright",
5
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.",
6
6
  "main": "server.js",
@@ -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
- "description": "E.164 format, e.g. +15555550199. If the user gives a local format, normalize it and confirm.",
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
- "description": "E.164. The agent only shares this if the business asks. Mark in do_not_volunteer if it should never be offered proactively.",
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')."),