@wabery/cli 0.11.5 → 0.13.0

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.
@@ -16,6 +16,11 @@ const readinessRequirementSchema = z.enum([
16
16
  ]);
17
17
  const optInSchema = z
18
18
  .object({
19
+ categories: z
20
+ .array(z.enum(["GENERAL", "MARKETING", "UTILITY", "AUTHENTICATION"]))
21
+ .min(1)
22
+ .max(4)
23
+ .optional(),
19
24
  source: z.string().min(1).max(120),
20
25
  method: z.string().min(1).max(80).optional(),
21
26
  occurred_at: z.string().datetime().optional(),
@@ -23,9 +28,62 @@ const optInSchema = z
23
28
  evidence: z.record(z.string(), z.unknown()).optional(),
24
29
  })
25
30
  .optional();
26
- const SERVER_INSTRUCTIONS = `Use Wabery tools as a developer platform for WhatsApp, Instagram, and Messenger messaging: channels, routing, contacts, registration intents, conversations, message history, outbound messages, inbound media metadata, WhatsApp Flows, templates, automations, webhooks, and Meta Business Agent extensions.
31
+ const broadcastStatusSchema = z.enum([
32
+ "draft",
33
+ "preparing",
34
+ "ready",
35
+ "scheduled",
36
+ "sending",
37
+ "completed",
38
+ "canceled",
39
+ "failed",
40
+ ]);
41
+ const broadcastRecipientStatusSchema = z.enum([
42
+ "pending",
43
+ "queued",
44
+ "sending",
45
+ "sent",
46
+ "delivered",
47
+ "read",
48
+ "failed",
49
+ "skipped",
50
+ "canceled",
51
+ ]);
52
+ const broadcastParameterMappingSchema = z.object({
53
+ component: z.enum(["header", "body", "button"]),
54
+ index: z.number().int().min(0),
55
+ button_index: z.number().int().min(0).optional(),
56
+ parameter_type: z
57
+ .enum(["text", "image", "document", "video"])
58
+ .default("text"),
59
+ source: z.enum([
60
+ "name",
61
+ "phone",
62
+ "preferred_language",
63
+ "metadata",
64
+ "constant",
65
+ ]),
66
+ metadata_key: z.string().min(1).max(120).optional(),
67
+ value: z.string().max(2000).optional(),
68
+ fallback: z.string().max(2000).optional(),
69
+ });
70
+ const broadcastVariantSchema = z.object({
71
+ template_id: z.string().min(1),
72
+ language: z.string().min(2).max(35),
73
+ parameter_mappings: z.array(broadcastParameterMappingSchema).max(100),
74
+ });
75
+ const broadcastAudienceFilterSchema = z.object({
76
+ search: z.string().max(120).optional(),
77
+ tags: z.array(z.string().min(1).max(60)).max(50).optional(),
78
+ languages: z.array(z.string().min(2).max(35)).max(50).optional(),
79
+ is_blocked: z.boolean().optional(),
80
+ created_after: z.string().datetime().optional(),
81
+ created_before: z.string().datetime().optional(),
82
+ });
83
+ const SERVER_INSTRUCTIONS = `Use Wabery tools as a developer platform for WhatsApp, Instagram, and Messenger messaging: channels, routing, contacts, registration intents, conversations, message history, outbound messages, inbound media metadata, WhatsApp Flows, templates, broadcasts, automations, webhooks, and Meta Business Agent extensions.
27
84
  Recommended order: read wabery_get_config_schema, select the project, call wabery_get_project_readiness, list channels only when more channel detail is needed, check Business Agent eligibility before adding business_agent config, create registration intents or enroll WhatsApp contacts as needed, store returned contact_id plus channel_id, validate and preview config diff, apply config only after review, publish flows only after explicit confirmation, then send messages or published flows with channel_id and contact_id/conversation_id.
28
85
  Always distinguish sandbox testing from production readiness: a project without its own connected phone number can often still be tested in the shared Wabery sandbox, but production inbound traffic requires a dedicated project channel. For WhatsApp templates and proactive reminders, inspect readiness before creation: template submission and proactive template sends require a dedicated non-sandbox WhatsApp channel, Meta business verification/account approval, a connected phone, and a Meta payment method. After sending a message, use wabery_get_message to inspect provider status and failures.
86
+ For broadcasts, use only approved WhatsApp templates on a dedicated channel and only contacts with valid category-specific consent. Create a draft, prepare exactly one audience source, poll wabery_get_broadcast until it is ready, inspect its counts and excluded recipients, and send or schedule only after explicit user confirmation. Preparation freezes the recipient snapshot; create a new draft when the intended audience changes. CSV files are uploaded through the dashboard or CLI; use wabery_list_contact_imports and wabery_get_contact_import to select a completed import.
29
87
  Use wabery_list_conversations, wabery_get_conversation, and wabery_list_conversation_messages to inspect history. Inbound WhatsApp media appears on message records/webhooks as media/assets metadata with Wabery signed URLs when retained; download files promptly because URLs are short-lived and retention is plan-based.
30
88
  Config apply creates or updates Wabery flows, automations, webhook settings, and eligible Meta Business Agent declarations. It never publishes flows to Meta. Automations and Business Agent connector tools can trigger flows through references to flow config keys.
31
89
  High-risk tools require confirmation_token values shown in each tool description.`;
@@ -89,11 +147,81 @@ function checkWriteMode(action, token, expectedToken, writePolicy = "env") {
89
147
  }
90
148
  export function createWaberyMcpServer(client = new WaberyApiClient(), options = {}) {
91
149
  const writePolicy = options.writePolicy ?? "env";
92
- const requireWrite = (action, token, expectedToken) => checkWriteMode(action, token, expectedToken, writePolicy);
93
150
  const server = new McpServer({
94
151
  name: "wabery",
95
152
  version: options.serverVersion ?? "0.1.0",
96
153
  }, { instructions: serverInstructions(writePolicy) });
154
+ const requireWrite = async (action, token, expectedToken) => {
155
+ const modeBlocked = checkWriteMode(action, undefined, undefined, writePolicy);
156
+ if (modeBlocked)
157
+ return modeBlocked;
158
+ if (!expectedToken)
159
+ return null;
160
+ if (server.server.getClientCapabilities()?.elicitation) {
161
+ const response = await server.server.elicitInput({
162
+ message: `Approve the high-risk Wabery action: ${action}`,
163
+ requestedSchema: {
164
+ type: "object",
165
+ properties: {
166
+ confirm: {
167
+ type: "boolean",
168
+ title: "Confirm action",
169
+ description: "Approve this external or destructive action",
170
+ default: false,
171
+ },
172
+ },
173
+ required: ["confirm"],
174
+ },
175
+ });
176
+ if (response.action === "accept" && response.content?.confirm === true) {
177
+ return null;
178
+ }
179
+ return errorResult({
180
+ action,
181
+ message: "The user declined or canceled the action.",
182
+ });
183
+ }
184
+ if (writePolicy === "enabled") {
185
+ return errorResult({
186
+ action,
187
+ message: "This hosted MCP client must support elicitation so Wabery can obtain real user approval.",
188
+ });
189
+ }
190
+ return checkWriteMode(action, token, expectedToken, writePolicy);
191
+ };
192
+ const requireBroadcastApproval = async (action, message, token, localConfirmationToken) => {
193
+ if (server.server.getClientCapabilities()?.elicitation) {
194
+ const response = await server.server.elicitInput({
195
+ message,
196
+ requestedSchema: {
197
+ type: "object",
198
+ properties: {
199
+ confirm: {
200
+ type: "boolean",
201
+ title: "Confirm broadcast action",
202
+ description: "Approve this external messaging action",
203
+ default: false,
204
+ },
205
+ },
206
+ required: ["confirm"],
207
+ },
208
+ });
209
+ if (response.action === "accept" && response.content?.confirm === true) {
210
+ return null;
211
+ }
212
+ return errorResult({
213
+ action,
214
+ message: "The user declined or canceled the broadcast action.",
215
+ });
216
+ }
217
+ if (writePolicy === "enabled") {
218
+ return errorResult({
219
+ action,
220
+ message: "This hosted MCP client must support elicitation so Wabery can obtain real user approval.",
221
+ });
222
+ }
223
+ return await requireWrite(action, token, localConfirmationToken);
224
+ };
97
225
  const resolveProjectId = async (projectId) => {
98
226
  if (projectId)
99
227
  return projectId;
@@ -258,7 +386,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
258
386
  },
259
387
  }, async (input) => {
260
388
  const { config, confirmation_token } = input;
261
- const blocked = requireWrite("wabery_apply_config", confirmation_token, "apply_config");
389
+ const blocked = await requireWrite("wabery_apply_config", confirmation_token, "apply_config");
262
390
  if (blocked)
263
391
  return blocked;
264
392
  try {
@@ -335,7 +463,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
335
463
  },
336
464
  }, async (input) => {
337
465
  const { confirmation_token, ...body } = input;
338
- const blocked = requireWrite("wabery_apply_business_agent", confirmation_token, "apply_business_agent");
466
+ const blocked = await requireWrite("wabery_apply_business_agent", confirmation_token, "apply_business_agent");
339
467
  if (blocked)
340
468
  return blocked;
341
469
  return jsonResult(await client.put("/business-agents", body));
@@ -376,7 +504,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
376
504
  },
377
505
  }, async (input) => {
378
506
  const { business_agent_id, confirmation_token, ...body } = input;
379
- const blocked = requireWrite("wabery_update_business_agent_settings", confirmation_token, "update_business_agent_settings");
507
+ const blocked = await requireWrite("wabery_update_business_agent_settings", confirmation_token, "update_business_agent_settings");
380
508
  if (blocked)
381
509
  return blocked;
382
510
  return jsonResult(await client.put(`/business-agents/${encodeURIComponent(business_agent_id)}/settings`, body));
@@ -418,7 +546,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
418
546
  },
419
547
  }, async (input) => {
420
548
  const { confirmation_token, ...body } = input;
421
- const blocked = requireWrite("wabery_create_business_agent_connector", confirmation_token, "create_business_agent_connector");
549
+ const blocked = await requireWrite("wabery_create_business_agent_connector", confirmation_token, "create_business_agent_connector");
422
550
  if (blocked)
423
551
  return blocked;
424
552
  return jsonResult(await client.post("/business-agents/connectors", body));
@@ -453,7 +581,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
453
581
  },
454
582
  }, async (input) => {
455
583
  const { connector_id, confirmation_token, ...body } = input;
456
- const blocked = requireWrite("wabery_create_business_agent_connector_tool", confirmation_token, "create_business_agent_connector_tool");
584
+ const blocked = await requireWrite("wabery_create_business_agent_connector_tool", confirmation_token, "create_business_agent_connector_tool");
457
585
  if (blocked)
458
586
  return blocked;
459
587
  return jsonResult(await client.post(`/business-agents/connectors/${encodeURIComponent(connector_id)}/tools`, body));
@@ -513,7 +641,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
513
641
  },
514
642
  }, async (input) => {
515
643
  const { connector_id, tool_id, confirmation_token, ...body } = input;
516
- const blocked = requireWrite("wabery_update_business_agent_connector_tool", confirmation_token, "update_business_agent_connector_tool");
644
+ const blocked = await requireWrite("wabery_update_business_agent_connector_tool", confirmation_token, "update_business_agent_connector_tool");
517
645
  if (blocked)
518
646
  return blocked;
519
647
  return jsonResult(await client.patch(`/business-agents/connectors/${encodeURIComponent(connector_id)}/tools/${encodeURIComponent(tool_id)}`, body));
@@ -536,7 +664,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
536
664
  openWorldHint: false,
537
665
  },
538
666
  }, async ({ connector_id, tool_id, confirmation_token }) => {
539
- const blocked = requireWrite("wabery_delete_business_agent_connector_tool", confirmation_token, "delete_business_agent_connector_tool");
667
+ const blocked = await requireWrite("wabery_delete_business_agent_connector_tool", confirmation_token, "delete_business_agent_connector_tool");
540
668
  if (blocked)
541
669
  return blocked;
542
670
  return jsonResult(await client.delete(`/business-agents/connectors/${encodeURIComponent(connector_id)}/tools/${encodeURIComponent(tool_id)}`));
@@ -565,7 +693,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
565
693
  },
566
694
  }, async (input) => {
567
695
  const { confirmation_token, ...body } = input;
568
- const blocked = requireWrite("wabery_business_agent_thread_control", confirmation_token, "thread_control");
696
+ const blocked = await requireWrite("wabery_business_agent_thread_control", confirmation_token, "thread_control");
569
697
  if (blocked)
570
698
  return blocked;
571
699
  return jsonResult(await client.post("/business-agents/thread-control", body));
@@ -633,7 +761,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
633
761
  openWorldHint: true,
634
762
  },
635
763
  }, async ({ confirmation_token, ...body }) => {
636
- const blocked = requireWrite("wabery_set_business_agent_skills", confirmation_token, "set_business_agent_skills");
764
+ const blocked = await requireWrite("wabery_set_business_agent_skills", confirmation_token, "set_business_agent_skills");
637
765
  if (blocked)
638
766
  return blocked;
639
767
  return jsonResult(await client.put("/business-agents/skills", body));
@@ -732,7 +860,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
732
860
  openWorldHint: false,
733
861
  },
734
862
  }, async ({ project_id, ...body }) => {
735
- const blocked = requireWrite("wabery_update_project");
863
+ const blocked = await requireWrite("wabery_update_project");
736
864
  if (blocked)
737
865
  return blocked;
738
866
  return jsonResult(await client.patch(`/projects/${encodeURIComponent(project_id)}`, body));
@@ -751,7 +879,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
751
879
  openWorldHint: false,
752
880
  },
753
881
  }, async ({ project_id, webhook_secret }) => {
754
- const blocked = requireWrite("wabery_rotate_webhook_secret");
882
+ const blocked = await requireWrite("wabery_rotate_webhook_secret");
755
883
  if (blocked)
756
884
  return blocked;
757
885
  return jsonResult(await client.post(`/projects/${encodeURIComponent(project_id)}/rotate-webhook-secret`, webhook_secret ? { webhook_secret } : undefined));
@@ -792,7 +920,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
792
920
  openWorldHint: false,
793
921
  },
794
922
  }, async ({ channel_id, routing_mode }) => {
795
- const blocked = requireWrite("wabery_update_channel");
923
+ const blocked = await requireWrite("wabery_update_channel");
796
924
  if (blocked)
797
925
  return blocked;
798
926
  return jsonResult(await client.patch(`/channels/${encodeURIComponent(channel_id)}`, {
@@ -821,7 +949,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
821
949
  openWorldHint: false,
822
950
  },
823
951
  }, async ({ project_id, customer_reference, metadata }) => {
824
- const blocked = requireWrite("wabery_create_registration_intent");
952
+ const blocked = await requireWrite("wabery_create_registration_intent");
825
953
  if (blocked)
826
954
  return blocked;
827
955
  return jsonResult(await client.post("/registration-intents", {
@@ -895,7 +1023,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
895
1023
  openWorldHint: true,
896
1024
  },
897
1025
  }, async ({ flow_id, confirmation_token }) => {
898
- const blocked = requireWrite("wabery_publish_flow", confirmation_token, `publish:${flow_id}`);
1026
+ const blocked = await requireWrite("wabery_publish_flow", confirmation_token, `publish:${flow_id}`);
899
1027
  if (blocked)
900
1028
  return blocked;
901
1029
  return apiResult(client.post(`/flows/${flow_id}/publish`));
@@ -919,7 +1047,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
919
1047
  .optional()
920
1048
  .describe("Your own user/customer id for correlation."),
921
1049
  metadata: metadataSchema,
922
- opt_in: optInSchema.describe("WhatsApp opt-in evidence. Include source, method, consent_text, and evidence where available."),
1050
+ opt_in: optInSchema.describe("WhatsApp opt-in evidence. For broadcasts include the applicable MARKETING or UTILITY category plus source, method, consent_text, and evidence."),
923
1051
  },
924
1052
  annotations: {
925
1053
  readOnlyHint: false,
@@ -928,7 +1056,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
928
1056
  openWorldHint: false,
929
1057
  },
930
1058
  }, async (input) => {
931
- const blocked = requireWrite("wabery_enroll_contact");
1059
+ const blocked = await requireWrite("wabery_enroll_contact");
932
1060
  if (blocked)
933
1061
  return blocked;
934
1062
  return jsonResult(await client.post("/contacts", input));
@@ -960,6 +1088,33 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
960
1088
  openWorldHint: false,
961
1089
  },
962
1090
  }, async ({ contact_id }) => jsonResult(await client.get(`/contacts/${contact_id}`)));
1091
+ server.registerTool("wabery_list_contact_imports", {
1092
+ title: "List Wabery contact imports",
1093
+ description: "List CSV contact imports and their processing counts. Upload files in the dashboard or CLI, then use a completed import_id as a broadcast audience source.",
1094
+ inputSchema: {
1095
+ limit: z.number().int().min(1).max(100).optional(),
1096
+ starting_after: z.string().optional(),
1097
+ },
1098
+ annotations: {
1099
+ readOnlyHint: true,
1100
+ destructiveHint: false,
1101
+ idempotentHint: true,
1102
+ openWorldHint: false,
1103
+ },
1104
+ }, async (query) => apiResult(client.get("/contact-imports", query)));
1105
+ server.registerTool("wabery_get_contact_import", {
1106
+ title: "Get Wabery contact import",
1107
+ description: "Inspect one CSV contact import's status and row counts. Wait for completed before using its import_id to prepare a broadcast.",
1108
+ inputSchema: {
1109
+ import_id: z.string().min(1),
1110
+ },
1111
+ annotations: {
1112
+ readOnlyHint: true,
1113
+ destructiveHint: false,
1114
+ idempotentHint: true,
1115
+ openWorldHint: false,
1116
+ },
1117
+ }, async ({ import_id }) => apiResult(client.get(`/contact-imports/${import_id}`)));
963
1118
  server.registerTool("wabery_unenroll_contact", {
964
1119
  title: "Unenroll Wabery contact",
965
1120
  description: "Remove a contact phone's sandbox authorization for this organization. Pass erase=true for irreversible org-scoped contact data deletion.",
@@ -978,11 +1133,233 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
978
1133
  openWorldHint: false,
979
1134
  },
980
1135
  }, async ({ contact_id, erase, confirmation_token }) => {
981
- const blocked = requireWrite("wabery_unenroll_contact", confirmation_token, erase ? `erase:${contact_id}` : undefined);
1136
+ const blocked = await requireWrite("wabery_unenroll_contact", confirmation_token, erase ? `erase:${contact_id}` : undefined);
982
1137
  if (blocked)
983
1138
  return blocked;
984
1139
  return jsonResult(await client.delete(`/contacts/${contact_id}${erase ? "?erase=true" : ""}`));
985
1140
  });
1141
+ server.registerTool("wabery_list_broadcasts", {
1142
+ title: "List Wabery broadcasts",
1143
+ description: "List broadcast campaigns and delivery counts. Filter by lifecycle status when monitoring preparation or delivery.",
1144
+ inputSchema: {
1145
+ limit: z.number().int().min(1).max(100).optional(),
1146
+ starting_after: z.string().optional(),
1147
+ status: broadcastStatusSchema.optional(),
1148
+ },
1149
+ annotations: {
1150
+ readOnlyHint: true,
1151
+ destructiveHint: false,
1152
+ idempotentHint: true,
1153
+ openWorldHint: false,
1154
+ },
1155
+ }, async (query) => apiResult(client.get("/broadcasts", query)));
1156
+ server.registerTool("wabery_get_broadcast", {
1157
+ title: "Get Wabery broadcast",
1158
+ description: "Fetch one broadcast with its lifecycle status and recipient counts. Poll while status is preparing, and review counts before requesting send or schedule confirmation.",
1159
+ inputSchema: {
1160
+ broadcast_id: z.string().min(1),
1161
+ },
1162
+ annotations: {
1163
+ readOnlyHint: true,
1164
+ destructiveHint: false,
1165
+ idempotentHint: true,
1166
+ openWorldHint: false,
1167
+ },
1168
+ }, async ({ broadcast_id }) => apiResult(client.get(`/broadcasts/${broadcast_id}`)));
1169
+ server.registerTool("wabery_create_broadcast", {
1170
+ title: "Create Wabery broadcast draft",
1171
+ description: "Create a draft from approved WhatsApp template variants. This does not select recipients or send messages. Use a dedicated WhatsApp channel and category-specific consent.",
1172
+ inputSchema: {
1173
+ name: z.string().min(1).max(120),
1174
+ channel_id: z.string().min(1),
1175
+ category: z.enum(["MARKETING", "UTILITY"]),
1176
+ template_name: z.string().min(1).max(512),
1177
+ fallback_language: z.string().min(2).max(35),
1178
+ variants: z.array(broadcastVariantSchema).min(1).max(50),
1179
+ },
1180
+ annotations: {
1181
+ readOnlyHint: false,
1182
+ destructiveHint: false,
1183
+ idempotentHint: false,
1184
+ openWorldHint: false,
1185
+ },
1186
+ }, async ({ name, channel_id, category, template_name, fallback_language, variants, }) => {
1187
+ const blocked = await requireWrite("wabery_create_broadcast");
1188
+ if (blocked)
1189
+ return blocked;
1190
+ return apiResult(client.post("/broadcasts", {
1191
+ name,
1192
+ channelId: channel_id,
1193
+ category,
1194
+ templateName: template_name,
1195
+ fallbackLanguage: fallback_language,
1196
+ variants: variants.map((variant) => ({
1197
+ templateId: variant.template_id,
1198
+ language: variant.language,
1199
+ parameterMappings: variant.parameter_mappings.map((mapping) => ({
1200
+ component: mapping.component,
1201
+ index: mapping.index,
1202
+ buttonIndex: mapping.button_index,
1203
+ parameterType: mapping.parameter_type,
1204
+ source: mapping.source,
1205
+ metadataKey: mapping.metadata_key,
1206
+ value: mapping.value,
1207
+ fallback: mapping.fallback,
1208
+ })),
1209
+ })),
1210
+ }));
1211
+ });
1212
+ server.registerTool("wabery_prepare_broadcast_audience", {
1213
+ title: "Prepare Wabery broadcast audience",
1214
+ description: "Resolve and freeze a draft broadcast's recipients from exactly one source: contact_ids, import_id, or filter. Preparation is asynchronous and does not send messages. Poll wabery_get_broadcast until ready, then inspect counts and recipients.",
1215
+ inputSchema: {
1216
+ broadcast_id: z.string().min(1),
1217
+ contact_ids: z.array(z.string().min(1)).max(1000).optional(),
1218
+ import_id: z.string().min(1).optional(),
1219
+ filter: broadcastAudienceFilterSchema.optional(),
1220
+ exclude_contact_ids: z.array(z.string().min(1)).max(1000).optional(),
1221
+ },
1222
+ annotations: {
1223
+ readOnlyHint: false,
1224
+ destructiveHint: false,
1225
+ idempotentHint: true,
1226
+ openWorldHint: false,
1227
+ },
1228
+ }, async ({ broadcast_id, contact_ids, import_id, filter, exclude_contact_ids, }) => {
1229
+ const blocked = await requireWrite("wabery_prepare_broadcast_audience");
1230
+ if (blocked)
1231
+ return blocked;
1232
+ const sources = [contact_ids, import_id, filter].filter((value) => value !== undefined);
1233
+ if (sources.length !== 1) {
1234
+ return {
1235
+ ...jsonResult({
1236
+ error: "invalid_audience_source",
1237
+ message: "Provide exactly one audience source: contact_ids, import_id, or filter.",
1238
+ }),
1239
+ isError: true,
1240
+ };
1241
+ }
1242
+ return apiResult(client.post(`/broadcasts/${broadcast_id}/audience`, {
1243
+ contactIds: contact_ids,
1244
+ importId: import_id,
1245
+ filter: filter
1246
+ ? {
1247
+ search: filter.search,
1248
+ tags: filter.tags,
1249
+ languages: filter.languages,
1250
+ isBlocked: filter.is_blocked,
1251
+ createdAfter: filter.created_after,
1252
+ createdBefore: filter.created_before,
1253
+ }
1254
+ : undefined,
1255
+ excludeContactIds: exclude_contact_ids ?? [],
1256
+ }));
1257
+ });
1258
+ server.registerTool("wabery_list_broadcast_recipients", {
1259
+ title: "List Wabery broadcast recipients",
1260
+ description: "Inspect the prepared recipient snapshot, language selection, skips, failures, and delivery state. Use before confirmation and while monitoring delivery.",
1261
+ inputSchema: {
1262
+ broadcast_id: z.string().min(1),
1263
+ limit: z.number().int().min(1).max(100).optional(),
1264
+ starting_after: z.string().optional(),
1265
+ status: broadcastRecipientStatusSchema.optional(),
1266
+ },
1267
+ annotations: {
1268
+ readOnlyHint: true,
1269
+ destructiveHint: false,
1270
+ idempotentHint: true,
1271
+ openWorldHint: false,
1272
+ },
1273
+ }, async ({ broadcast_id, ...query }) => apiResult(client.get(`/broadcasts/${broadcast_id}/recipients`, query)));
1274
+ server.registerTool("wabery_send_broadcast", {
1275
+ title: "Send Wabery broadcast",
1276
+ description: "Start sending a ready broadcast to its frozen recipient snapshot. This creates real external WhatsApp messages and requires explicit user confirmation with confirmation_token send_broadcast:<broadcast_id>.",
1277
+ inputSchema: {
1278
+ broadcast_id: z.string().min(1),
1279
+ confirmation_token: z
1280
+ .string()
1281
+ .optional()
1282
+ .describe("Required: send_broadcast:<broadcast_id>. Obtain explicit user confirmation after reviewing counts."),
1283
+ },
1284
+ annotations: {
1285
+ readOnlyHint: false,
1286
+ destructiveHint: true,
1287
+ idempotentHint: true,
1288
+ openWorldHint: true,
1289
+ },
1290
+ }, async ({ broadcast_id, confirmation_token }) => {
1291
+ const blocked = await requireBroadcastApproval("wabery_send_broadcast", `Send broadcast ${broadcast_id} to its prepared WhatsApp recipients now?`, confirmation_token, `send_broadcast:${broadcast_id}`);
1292
+ if (blocked)
1293
+ return blocked;
1294
+ return apiResult(client.post(`/broadcasts/${broadcast_id}/send`));
1295
+ });
1296
+ server.registerTool("wabery_schedule_broadcast", {
1297
+ title: "Schedule Wabery broadcast",
1298
+ description: "Schedule a ready broadcast for future delivery to its frozen recipient snapshot. Requires explicit user confirmation with confirmation_token schedule_broadcast:<broadcast_id>.",
1299
+ inputSchema: {
1300
+ broadcast_id: z.string().min(1),
1301
+ scheduled_at: z.string().datetime(),
1302
+ timezone: z.string().min(1).max(80),
1303
+ confirmation_token: z
1304
+ .string()
1305
+ .optional()
1306
+ .describe("Required: schedule_broadcast:<broadcast_id>. Obtain explicit user confirmation after reviewing counts and time."),
1307
+ },
1308
+ annotations: {
1309
+ readOnlyHint: false,
1310
+ destructiveHint: true,
1311
+ idempotentHint: true,
1312
+ openWorldHint: true,
1313
+ },
1314
+ }, async ({ broadcast_id, scheduled_at, timezone, confirmation_token }) => {
1315
+ const blocked = await requireBroadcastApproval("wabery_schedule_broadcast", `Schedule broadcast ${broadcast_id} for ${scheduled_at} (${timezone})?`, confirmation_token, `schedule_broadcast:${broadcast_id}`);
1316
+ if (blocked)
1317
+ return blocked;
1318
+ return apiResult(client.post(`/broadcasts/${broadcast_id}/schedule`, {
1319
+ scheduledAt: scheduled_at,
1320
+ timezone,
1321
+ }));
1322
+ });
1323
+ server.registerTool("wabery_cancel_broadcast", {
1324
+ title: "Cancel Wabery broadcast",
1325
+ description: "Cancel a draft, ready, scheduled, or sending broadcast and stop recipients that have not been dispatched. Already accepted messages cannot be recalled. Requires confirmation_token cancel_broadcast:<broadcast_id>.",
1326
+ inputSchema: {
1327
+ broadcast_id: z.string().min(1),
1328
+ confirmation_token: z
1329
+ .string()
1330
+ .optional()
1331
+ .describe("Required: cancel_broadcast:<broadcast_id>."),
1332
+ },
1333
+ annotations: {
1334
+ readOnlyHint: false,
1335
+ destructiveHint: true,
1336
+ idempotentHint: true,
1337
+ openWorldHint: false,
1338
+ },
1339
+ }, async ({ broadcast_id, confirmation_token }) => {
1340
+ const blocked = await requireBroadcastApproval("wabery_cancel_broadcast", `Cancel broadcast ${broadcast_id}? Messages already accepted by WhatsApp cannot be recalled.`, confirmation_token, `cancel_broadcast:${broadcast_id}`);
1341
+ if (blocked)
1342
+ return blocked;
1343
+ return apiResult(client.post(`/broadcasts/${broadcast_id}/cancel`));
1344
+ });
1345
+ server.registerTool("wabery_duplicate_broadcast", {
1346
+ title: "Duplicate Wabery broadcast",
1347
+ description: "Create a new draft with the source broadcast's channel, template variants, and parameter mappings. The recipient snapshot is not copied; prepare a new audience before sending.",
1348
+ inputSchema: {
1349
+ broadcast_id: z.string().min(1),
1350
+ },
1351
+ annotations: {
1352
+ readOnlyHint: false,
1353
+ destructiveHint: false,
1354
+ idempotentHint: false,
1355
+ openWorldHint: false,
1356
+ },
1357
+ }, async ({ broadcast_id }) => {
1358
+ const blocked = await requireWrite("wabery_duplicate_broadcast");
1359
+ if (blocked)
1360
+ return blocked;
1361
+ return apiResult(client.post(`/broadcasts/${broadcast_id}/duplicate`));
1362
+ });
986
1363
  server.registerTool("wabery_list_conversations", {
987
1364
  title: "List Wabery conversations",
988
1365
  description: "List organization-scoped conversations for inbox, support, or analytics tooling.",
@@ -1044,7 +1421,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1044
1421
  openWorldHint: true,
1045
1422
  },
1046
1423
  }, async ({ body }) => {
1047
- const blocked = requireWrite("wabery_send_message");
1424
+ const blocked = await requireWrite("wabery_send_message");
1048
1425
  if (blocked)
1049
1426
  return blocked;
1050
1427
  return apiResult(client.post("/messages", body));
@@ -1105,7 +1482,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1105
1482
  openWorldHint: true,
1106
1483
  },
1107
1484
  }, async ({ body }) => {
1108
- const blocked = requireWrite("wabery_create_template");
1485
+ const blocked = await requireWrite("wabery_create_template");
1109
1486
  if (blocked)
1110
1487
  return blocked;
1111
1488
  return apiResult(client.post("/templates", body));
@@ -1151,7 +1528,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1151
1528
  openWorldHint: true,
1152
1529
  },
1153
1530
  }, async ({ flow_id, ...body }) => {
1154
- const blocked = requireWrite("wabery_send_flow");
1531
+ const blocked = await requireWrite("wabery_send_flow");
1155
1532
  if (blocked)
1156
1533
  return blocked;
1157
1534
  return apiResult(client.post(`/flows/${flow_id}/send`, body));
@@ -1249,7 +1626,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1249
1626
  },
1250
1627
  }, async (input) => {
1251
1628
  const { confirmation_token, ...body } = input;
1252
- const blocked = requireWrite("wabery_create_function", confirmation_token, "create_function");
1629
+ const blocked = await requireWrite("wabery_create_function", confirmation_token, "create_function");
1253
1630
  if (blocked)
1254
1631
  return blocked;
1255
1632
  return jsonResult(await client.post("/functions", body));
@@ -1272,7 +1649,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1272
1649
  openWorldHint: false,
1273
1650
  },
1274
1651
  }, async ({ function_id, source, confirmation_token }) => {
1275
- const blocked = requireWrite("wabery_deploy_function", confirmation_token, "deploy_function");
1652
+ const blocked = await requireWrite("wabery_deploy_function", confirmation_token, "deploy_function");
1276
1653
  if (blocked)
1277
1654
  return blocked;
1278
1655
  return jsonResult(await client.post(`/functions/${function_id}/deploy`, { source }));
@@ -1295,7 +1672,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1295
1672
  openWorldHint: false,
1296
1673
  },
1297
1674
  }, async ({ function_id, text, confirmation_token }) => {
1298
- const blocked = requireWrite("wabery_test_function", confirmation_token, "test_function");
1675
+ const blocked = await requireWrite("wabery_test_function", confirmation_token, "test_function");
1299
1676
  if (blocked)
1300
1677
  return blocked;
1301
1678
  return jsonResult(await client.post(`/functions/${function_id}/test`, { text }));
@@ -1328,7 +1705,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1328
1705
  openWorldHint: false,
1329
1706
  },
1330
1707
  }, async ({ function_id, confirmation_token }) => {
1331
- const blocked = requireWrite("wabery_delete_function", confirmation_token, "delete_function");
1708
+ const blocked = await requireWrite("wabery_delete_function", confirmation_token, "delete_function");
1332
1709
  if (blocked)
1333
1710
  return blocked;
1334
1711
  return jsonResult(await client.delete(`/functions/${function_id}`));
@@ -1355,7 +1732,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1355
1732
  },
1356
1733
  }, async (input) => {
1357
1734
  const { function_id, confirmation_token, ...body } = input;
1358
- const blocked = requireWrite("wabery_update_function", confirmation_token, "update_function");
1735
+ const blocked = await requireWrite("wabery_update_function", confirmation_token, "update_function");
1359
1736
  if (blocked)
1360
1737
  return blocked;
1361
1738
  return jsonResult(await client.patch(`/functions/${function_id}`, body));
@@ -1379,7 +1756,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1379
1756
  openWorldHint: true,
1380
1757
  },
1381
1758
  }, async ({ function_id, arguments: args, contact_id, confirmation_token, }) => {
1382
- const blocked = requireWrite("wabery_invoke_function", confirmation_token, "invoke_function");
1759
+ const blocked = await requireWrite("wabery_invoke_function", confirmation_token, "invoke_function");
1383
1760
  if (blocked)
1384
1761
  return blocked;
1385
1762
  return jsonResult(await client.post(`/functions/${function_id}/invoke`, {