@wabery/cli 0.13.0 → 0.14.1

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.
@@ -1,4 +1,4 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { McpServer, } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { WaberyApiClient, WaberyApiError } from "./api-client.js";
4
4
  import { createExampleConfig } from "./config-example.js";
@@ -80,13 +80,18 @@ const broadcastAudienceFilterSchema = z.object({
80
80
  created_after: z.string().datetime().optional(),
81
81
  created_before: z.string().datetime().optional(),
82
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.
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.
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.
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.
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.
89
- High-risk tools require confirmation_token values shown in each tool description.`;
83
+ const toolOutputSchema = {
84
+ result: z
85
+ .record(z.string(), z.unknown())
86
+ .describe("The structured Wabery API result object. Its operation-specific fields are described by the tool and Wabery API documentation."),
87
+ };
88
+ const SERVER_INSTRUCTIONS = `Wabery is a developer platform for building and operating WhatsApp AI agents. Start discovery with wabery_list_projects, select the intended project, then call wabery_get_project_readiness; use channel readiness only for phone-number/provider detail.
89
+ Use Wabery for WhatsApp contacts, opt-in, conversations and message history, approved templates, Flows, broadcasts, routing/human handoff, signed webhooks, hosted functions, and Meta Business Agent configuration.
90
+ Keep sandbox and production readiness distinct: the shared sandbox supports controlled tests; production inbound, proactive templates, and broadcasts require an eligible dedicated WhatsApp channel. Enroll only controlled or explicitly opted-in contacts and retain the returned contact_id and channel_id.
91
+ Read conversations and message history before replying. Validate and preview config before apply; config apply may replace resources but never publishes a Flow. Draft and validate Flows before separately confirming publish or send.
92
+ For broadcasts, create a draft with approved templates, prepare exactly one audience source, poll until ready, review recipients/exclusions, then obtain explicit confirmation before send or schedule. Preparation freezes the audience.
93
+ Use routing and thread-control tools for automation ownership and human handoff. Treat hosted function test/invoke as potentially side-effecting code.
94
+ Obtain explicit user confirmation immediately before any external or irreversible action, including sends, publishes, deletions, secret rotation, provider updates, or function execution. After sending, call wabery_get_message, wabery_get_dispatch, or wabery_get_broadcast to report provider status; acceptance is not delivery.`;
90
95
  // The write gate differs by transport, and the model has to be told the truth
91
96
  // about the session it is actually in: a hosted OAuth client (ChatGPT, Claude)
92
97
  // cannot set an env var, so advertising the env gate there makes it refuse to
@@ -98,10 +103,58 @@ const WRITE_NOTE = {
98
103
  function serverInstructions(writePolicy) {
99
104
  return `${SERVER_INSTRUCTIONS}\n${WRITE_NOTE[writePolicy]}`;
100
105
  }
106
+ const MCP_SENSITIVE_OUTPUT_KEY_SUFFIXES = [
107
+ "accesstoken",
108
+ "apikey",
109
+ "authorizationheader",
110
+ "authtoken",
111
+ "bearertoken",
112
+ "credential",
113
+ "credentials",
114
+ "idtoken",
115
+ "jwt",
116
+ "oauthtoken",
117
+ "password",
118
+ "passwordhash",
119
+ "privatekey",
120
+ "providertoken",
121
+ "refreshtoken",
122
+ "secret",
123
+ "serviceproof",
124
+ "sessiontoken",
125
+ "signingkey",
126
+ "stacktrace",
127
+ ];
128
+ const MCP_INTERNAL_OUTPUT_KEYS = new Set([
129
+ "debugid",
130
+ "internaldebugid",
131
+ "requestid",
132
+ "traceid",
133
+ ]);
134
+ function isSensitiveMcpOutputKey(key) {
135
+ const normalizedKey = key.replaceAll(/[^a-zA-Z0-9]/g, "").toLowerCase();
136
+ return (MCP_INTERNAL_OUTPUT_KEYS.has(normalizedKey) ||
137
+ MCP_SENSITIVE_OUTPUT_KEY_SUFFIXES.some((suffix) => normalizedKey.endsWith(suffix)));
138
+ }
139
+ function sanitizeMcpOutput(data) {
140
+ if (Array.isArray(data)) {
141
+ return data.map((item) => sanitizeMcpOutput(item));
142
+ }
143
+ if (!data || typeof data !== "object") {
144
+ return data;
145
+ }
146
+ return Object.fromEntries(Object.entries(data).flatMap(([key, value]) => isSensitiveMcpOutputKey(key) ? [] : [[key, sanitizeMcpOutput(value)]]));
147
+ }
101
148
  function jsonResult(data) {
149
+ const sanitizedData = sanitizeMcpOutput(data);
102
150
  return {
103
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
104
- structuredContent: { result: data },
151
+ content: [
152
+ {
153
+ type: "text",
154
+ text: JSON.stringify(sanitizedData, null, 2),
155
+ },
156
+ ],
157
+ structuredContent: { result: sanitizedData },
105
158
  };
106
159
  }
107
160
  function errorResult(data) {
@@ -151,6 +204,10 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
151
204
  name: "wabery",
152
205
  version: options.serverVersion ?? "0.1.0",
153
206
  }, { instructions: serverInstructions(writePolicy) });
207
+ const registerTool = (name, config, callback) => server.registerTool(name, {
208
+ outputSchema: toolOutputSchema,
209
+ ...config,
210
+ }, callback);
154
211
  const requireWrite = async (action, token, expectedToken) => {
155
212
  const modeBlocked = checkWriteMode(action, undefined, undefined, writePolicy);
156
213
  if (modeBlocked)
@@ -292,9 +349,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
292
349
  },
293
350
  ],
294
351
  }));
295
- server.registerTool("wabery_check_connection", {
352
+ registerTool("wabery_check_connection", {
296
353
  title: "Check Wabery connection",
297
- description: "Verify the configured API key can reach Wabery projects, channels, and the config schema before making changes.",
354
+ description: "Use this when you need to verify that the current credential can reach Wabery projects, WhatsApp channels, and the config schema before other work.",
298
355
  inputSchema: {},
299
356
  annotations: {
300
357
  readOnlyHint: true,
@@ -303,9 +360,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
303
360
  openWorldHint: false,
304
361
  },
305
362
  }, async () => jsonResult(await checkWaberyConnection(client)));
306
- server.registerTool("wabery_get_config_example", {
363
+ registerTool("wabery_get_config_example", {
307
364
  title: "Get example Wabery config",
308
- description: "Return a starter wabery.config.json with one flow and one automation that triggers it with a send_flow step.",
365
+ description: "Use this when you need a starter wabery.config.json with one WhatsApp Flow and one send_flow automation; optional project_id comes from wabery_list_projects.",
309
366
  inputSchema: {
310
367
  project_id: z
311
368
  .string()
@@ -320,9 +377,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
320
377
  openWorldHint: false,
321
378
  },
322
379
  }, async ({ project_id }) => jsonResult(createExampleConfig(project_id)));
323
- server.registerTool("wabery_get_config_schema", {
380
+ registerTool("wabery_get_config_schema", {
324
381
  title: "Get Wabery config schema",
325
- description: "Return the JSON Schema for wabery.config.json. Use this before generating flows, automations, webhooks, or business_agent declarations.",
382
+ description: "Use this when you need the current JSON Schema before generating a wabery.config.json with Flows, automations, webhooks, or business_agent declarations.",
326
383
  inputSchema: {},
327
384
  annotations: {
328
385
  readOnlyHint: true,
@@ -331,9 +388,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
331
388
  openWorldHint: false,
332
389
  },
333
390
  }, async () => jsonResult(await client.get("/config/schema")));
334
- server.registerTool("wabery_export_config", {
391
+ registerTool("wabery_export_config", {
335
392
  title: "Export Wabery config",
336
- description: "Export the organization's flows, automations, webhook settings, and Business Agent declarations as a config object.",
393
+ description: "Use this when you need the selected project's current Flows, automations, webhook settings, and Business Agent declarations as declarative config. Do not use it to inspect live provider status.",
337
394
  inputSchema: {},
338
395
  annotations: {
339
396
  readOnlyHint: true,
@@ -342,9 +399,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
342
399
  openWorldHint: false,
343
400
  },
344
401
  }, async () => jsonResult(await client.get("/config")));
345
- server.registerTool("wabery_validate_config", {
402
+ registerTool("wabery_validate_config", {
346
403
  title: "Validate Wabery config",
347
- description: "Validate a declarative config object without changing flows or automations.",
404
+ description: "Use this when you need to validate a complete declarative config without changing resources. Use wabery_preview_config_diff next to inspect effects.",
348
405
  inputSchema: {
349
406
  config: configSchema.describe("The full wabery.config.json object."),
350
407
  },
@@ -355,9 +412,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
355
412
  openWorldHint: false,
356
413
  },
357
414
  }, async ({ config }) => apiResult(client.post("/config/validate", config)));
358
- server.registerTool("wabery_preview_config_diff", {
415
+ registerTool("wabery_preview_config_diff", {
359
416
  title: "Preview Wabery config diff",
360
- description: "Preview which flows, automations, webhook settings, Business Agents, connectors, and tools would be created, updated, deleted, or left unchanged.",
417
+ description: "Use this when you need to preview what a complete config would create, update, delete, or leave unchanged without applying it. Use wabery_apply_config only after reviewing this diff.",
361
418
  inputSchema: {
362
419
  config: configSchema.describe("The full wabery.config.json object."),
363
420
  },
@@ -368,9 +425,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
368
425
  openWorldHint: false,
369
426
  },
370
427
  }, async ({ config }) => apiResult(client.post("/config/diff", config)));
371
- server.registerTool("wabery_apply_config", {
428
+ registerTool("wabery_apply_config", {
372
429
  title: "Apply Wabery config",
373
- description: "Create, update, or remove Wabery flows, automations, webhook settings, and eligible Business Agent declarations from config. This does not publish flows to Meta; call wabery_publish_flow explicitly after review.",
430
+ description: "Use this when you need to apply a reviewed complete config, creating, overwriting, or removing Wabery Flows, automations, webhooks, and eligible Business Agent resources. Do not use it for one resource update when a narrow update tool fits. It never publishes Flows; use wabery_publish_flow separately.",
374
431
  inputSchema: {
375
432
  config: configSchema.describe("The full wabery.config.json object."),
376
433
  confirmation_token: z
@@ -382,7 +439,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
382
439
  readOnlyHint: false,
383
440
  destructiveHint: true,
384
441
  idempotentHint: true,
385
- openWorldHint: false,
442
+ openWorldHint: true,
386
443
  },
387
444
  }, async (input) => {
388
445
  const { config, confirmation_token } = input;
@@ -410,9 +467,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
410
467
  throw error;
411
468
  }
412
469
  });
413
- server.registerTool("wabery_list_business_agents", {
470
+ registerTool("wabery_list_business_agents", {
414
471
  title: "List Meta Business Agents",
415
- description: "List Meta Business Agent records for the selected Wabery project.",
472
+ description: "Use this when you need the selected project's Meta Business Agent records and business_agent_id values. Do not use it for live settings; use wabery_get_business_agent_settings.",
416
473
  inputSchema: {
417
474
  channel_id: z.string().min(1).optional(),
418
475
  },
@@ -425,24 +482,24 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
425
482
  }, async ({ channel_id }) => jsonResult(await client.get("/business-agents", {
426
483
  channel_id,
427
484
  })));
428
- server.registerTool("wabery_check_business_agent_eligibility", {
485
+ registerTool("wabery_check_business_agent_eligibility", {
429
486
  title: "Check Meta Business Agent eligibility",
430
- description: "Check whether a WhatsApp channel is eligible for Meta Business Agent.",
487
+ description: "Use this when you need to query Meta eligibility for a WhatsApp channel_id from wabery_list_channels before provisioning a Business Agent. This also caches the provider result in Wabery.",
431
488
  inputSchema: {
432
489
  channel_id: z.string().min(1),
433
490
  },
434
491
  annotations: {
435
- readOnlyHint: true,
436
- destructiveHint: false,
492
+ readOnlyHint: false,
493
+ destructiveHint: true,
437
494
  idempotentHint: true,
438
- openWorldHint: false,
495
+ openWorldHint: true,
439
496
  },
440
497
  }, async ({ channel_id }) => jsonResult(await client.get("/business-agents/eligibility", {
441
498
  channel_id,
442
499
  })));
443
- server.registerTool("wabery_apply_business_agent", {
500
+ registerTool("wabery_apply_business_agent", {
444
501
  title: "Apply Meta Business Agent settings",
445
- description: "Provision or update a Meta Business Agent for a WhatsApp channel. Prefer wabery_apply_config when managing the whole project declaratively.",
502
+ description: "Use this when you need to provision or overwrite Meta Business Agent settings for a channel_id from wabery_list_channels. Prefer wabery_apply_config for reviewed project-wide declarative changes.",
446
503
  inputSchema: {
447
504
  channel_id: z.string().min(1),
448
505
  enabled: z.boolean().optional(),
@@ -457,9 +514,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
457
514
  },
458
515
  annotations: {
459
516
  readOnlyHint: false,
460
- destructiveHint: false,
517
+ destructiveHint: true,
461
518
  idempotentHint: true,
462
- openWorldHint: false,
519
+ openWorldHint: true,
463
520
  },
464
521
  }, async (input) => {
465
522
  const { confirmation_token, ...body } = input;
@@ -468,9 +525,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
468
525
  return blocked;
469
526
  return jsonResult(await client.put("/business-agents", body));
470
527
  });
471
- server.registerTool("wabery_get_business_agent_settings", {
528
+ registerTool("wabery_get_business_agent_settings", {
472
529
  title: "Get Meta Business Agent settings",
473
- description: "Retrieve live Meta Business Agent settings.",
530
+ description: "Use this when you need live Meta settings for a business_agent_id from wabery_list_business_agents. Do not use it for the cached Wabery record.",
474
531
  inputSchema: {
475
532
  business_agent_id: z.string().min(1),
476
533
  },
@@ -481,9 +538,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
481
538
  openWorldHint: true,
482
539
  },
483
540
  }, async ({ business_agent_id }) => jsonResult(await client.get(`/business-agents/${encodeURIComponent(business_agent_id)}/settings`)));
484
- server.registerTool("wabery_update_business_agent_settings", {
541
+ registerTool("wabery_update_business_agent_settings", {
485
542
  title: "Update Meta Business Agent settings",
486
- description: "Update live Meta Business Agent settings.",
543
+ description: "Use this when you need to overwrite live Meta settings for a business_agent_id from wabery_list_business_agents. Prefer wabery_apply_business_agent when provisioning by channel.",
487
544
  inputSchema: {
488
545
  business_agent_id: z.string().min(1),
489
546
  enabled: z.boolean().optional(),
@@ -498,7 +555,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
498
555
  },
499
556
  annotations: {
500
557
  readOnlyHint: false,
501
- destructiveHint: false,
558
+ destructiveHint: true,
502
559
  idempotentHint: true,
503
560
  openWorldHint: true,
504
561
  },
@@ -509,9 +566,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
509
566
  return blocked;
510
567
  return jsonResult(await client.put(`/business-agents/${encodeURIComponent(business_agent_id)}/settings`, body));
511
568
  });
512
- server.registerTool("wabery_list_business_agent_connectors", {
569
+ registerTool("wabery_list_business_agent_connectors", {
513
570
  title: "List Meta Business Agent connectors",
514
- description: "List connector records for Meta Business Agents in the selected Wabery project.",
571
+ description: "Use this when you need connector records and connector_id values for the selected project, optionally filtered by a business_agent_id from wabery_list_business_agents.",
515
572
  inputSchema: {
516
573
  business_agent_id: z.string().min(1).optional(),
517
574
  },
@@ -524,9 +581,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
524
581
  }, async ({ business_agent_id }) => jsonResult(await client.get("/business-agents/connectors", {
525
582
  business_agent_id,
526
583
  })));
527
- server.registerTool("wabery_create_business_agent_connector", {
584
+ registerTool("wabery_create_business_agent_connector", {
528
585
  title: "Create Meta Business Agent connector",
529
- description: "Create a connector on a provisioned Meta Business Agent. Prefer wabery_apply_config for declarative project-wide changes.",
586
+ description: "Use this when you need to create a connector in Meta for a provisioned business_agent_id from wabery_list_business_agents. Prefer wabery_apply_config for reviewed project-wide declarative changes.",
530
587
  inputSchema: {
531
588
  business_agent_id: z.string().min(1),
532
589
  name: z.string().min(1).max(120),
@@ -540,9 +597,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
540
597
  },
541
598
  annotations: {
542
599
  readOnlyHint: false,
543
- destructiveHint: false,
600
+ destructiveHint: true,
544
601
  idempotentHint: true,
545
- openWorldHint: false,
602
+ openWorldHint: true,
546
603
  },
547
604
  }, async (input) => {
548
605
  const { confirmation_token, ...body } = input;
@@ -551,9 +608,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
551
608
  return blocked;
552
609
  return jsonResult(await client.post("/business-agents/connectors", body));
553
610
  });
554
- server.registerTool("wabery_create_business_agent_connector_tool", {
611
+ registerTool("wabery_create_business_agent_connector_tool", {
555
612
  title: "Create Meta Business Agent connector tool",
556
- description: "Create a connector tool backed by a Wabery Flow, hosted function, API endpoint, or no-op placeholder.",
613
+ description: "Use this when you need to create a Meta connector tool for a connector_id from wabery_list_business_agent_connectors. backing_type is FLOW, HOSTED_FUNCTION, API_ENDPOINT, or NOOP; use the matching id from wabery_list_flows or wabery_list_functions.",
557
614
  inputSchema: {
558
615
  connector_id: z.string().min(1),
559
616
  name: z.string().min(1).max(120),
@@ -575,9 +632,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
575
632
  },
576
633
  annotations: {
577
634
  readOnlyHint: false,
578
- destructiveHint: false,
635
+ destructiveHint: true,
579
636
  idempotentHint: true,
580
- openWorldHint: false,
637
+ openWorldHint: true,
581
638
  },
582
639
  }, async (input) => {
583
640
  const { connector_id, confirmation_token, ...body } = input;
@@ -586,9 +643,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
586
643
  return blocked;
587
644
  return jsonResult(await client.post(`/business-agents/connectors/${encodeURIComponent(connector_id)}/tools`, body));
588
645
  });
589
- server.registerTool("wabery_list_business_agent_connector_tools", {
646
+ registerTool("wabery_list_business_agent_connector_tools", {
590
647
  title: "List Meta Business Agent connector tools",
591
- description: "List tools for a Meta Business Agent connector.",
648
+ description: "Use this when you need local connector-tool records and tool_id values for a connector_id from wabery_list_business_agent_connectors.",
592
649
  inputSchema: {
593
650
  connector_id: z.string().min(1),
594
651
  },
@@ -599,9 +656,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
599
656
  openWorldHint: false,
600
657
  },
601
658
  }, async ({ connector_id }) => jsonResult(await client.get(`/business-agents/connectors/${encodeURIComponent(connector_id)}/tools`)));
602
- server.registerTool("wabery_get_business_agent_connector_tool", {
659
+ registerTool("wabery_get_business_agent_connector_tool", {
603
660
  title: "Get Meta Business Agent connector tool",
604
- description: "Fetch one Meta Business Agent connector tool.",
661
+ description: "Use this when you need one connector tool by connector_id and tool_id from wabery_list_business_agent_connector_tools.",
605
662
  inputSchema: {
606
663
  connector_id: z.string().min(1),
607
664
  tool_id: z.string().min(1),
@@ -613,9 +670,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
613
670
  openWorldHint: false,
614
671
  },
615
672
  }, async ({ connector_id, tool_id }) => jsonResult(await client.get(`/business-agents/connectors/${encodeURIComponent(connector_id)}/tools/${encodeURIComponent(tool_id)}`)));
616
- server.registerTool("wabery_update_business_agent_connector_tool", {
673
+ registerTool("wabery_update_business_agent_connector_tool", {
617
674
  title: "Update Meta Business Agent connector tool",
618
- description: "Update one Meta Business Agent connector tool.",
675
+ description: "Use this when you need to overwrite a Meta connector tool identified by connector_id and tool_id from wabery_list_business_agent_connector_tools. Use create only for a new tool.",
619
676
  inputSchema: {
620
677
  connector_id: z.string().min(1),
621
678
  tool_id: z.string().min(1),
@@ -635,9 +692,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
635
692
  },
636
693
  annotations: {
637
694
  readOnlyHint: false,
638
- destructiveHint: false,
695
+ destructiveHint: true,
639
696
  idempotentHint: true,
640
- openWorldHint: false,
697
+ openWorldHint: true,
641
698
  },
642
699
  }, async (input) => {
643
700
  const { connector_id, tool_id, confirmation_token, ...body } = input;
@@ -646,9 +703,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
646
703
  return blocked;
647
704
  return jsonResult(await client.patch(`/business-agents/connectors/${encodeURIComponent(connector_id)}/tools/${encodeURIComponent(tool_id)}`, body));
648
705
  });
649
- server.registerTool("wabery_delete_business_agent_connector_tool", {
706
+ registerTool("wabery_delete_business_agent_connector_tool", {
650
707
  title: "Delete Meta Business Agent connector tool",
651
- description: "Delete one Meta Business Agent connector tool.",
708
+ description: "Use this when you need to permanently delete a Meta connector tool identified by connector_id and tool_id from wabery_list_business_agent_connector_tools.",
652
709
  inputSchema: {
653
710
  connector_id: z.string().min(1),
654
711
  tool_id: z.string().min(1),
@@ -661,7 +718,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
661
718
  readOnlyHint: false,
662
719
  destructiveHint: true,
663
720
  idempotentHint: true,
664
- openWorldHint: false,
721
+ openWorldHint: true,
665
722
  },
666
723
  }, async ({ connector_id, tool_id, confirmation_token }) => {
667
724
  const blocked = await requireWrite("wabery_delete_business_agent_connector_tool", confirmation_token, "delete_business_agent_connector_tool");
@@ -669,11 +726,30 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
669
726
  return blocked;
670
727
  return jsonResult(await client.delete(`/business-agents/connectors/${encodeURIComponent(connector_id)}/tools/${encodeURIComponent(tool_id)}`));
671
728
  });
672
- server.registerTool("wabery_business_agent_thread_control", {
729
+ registerTool("wabery_get_business_agent_thread_control", {
730
+ title: "Get Meta Business Agent thread control",
731
+ description: "Use this when you need to inspect current WhatsApp thread ownership for a channel_id from wabery_list_channels and contact_id from wabery_list_contacts. Use the set tool for handoff.",
732
+ inputSchema: {
733
+ channel_id: z.string().min(1),
734
+ contact_id: z.string().min(1),
735
+ business_agent_id: z.string().min(1).optional(),
736
+ thread_id: z.string().min(1).optional(),
737
+ },
738
+ annotations: {
739
+ readOnlyHint: true,
740
+ destructiveHint: false,
741
+ idempotentHint: true,
742
+ openWorldHint: true,
743
+ },
744
+ }, async (input) => jsonResult(await client.post("/business-agents/thread-control", {
745
+ ...input,
746
+ action: "get",
747
+ })));
748
+ registerTool("wabery_set_business_agent_thread_control", {
673
749
  title: "Set Meta Business Agent thread control",
674
- description: "Take, pass, or inspect WhatsApp thread control for a channel/contact pair.",
750
+ description: "Use this when you need to take or pass WhatsApp thread control for a channel_id and contact_id, changing automation or human ownership. Inspect first with wabery_get_business_agent_thread_control.",
675
751
  inputSchema: {
676
- action: z.enum(["take", "pass", "get"]),
752
+ action: z.enum(["take", "pass"]),
677
753
  channel_id: z.string().min(1),
678
754
  contact_id: z.string().min(1),
679
755
  business_agent_id: z.string().min(1).optional(),
@@ -687,35 +763,44 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
687
763
  },
688
764
  annotations: {
689
765
  readOnlyHint: false,
690
- destructiveHint: false,
766
+ destructiveHint: true,
691
767
  idempotentHint: true,
692
- openWorldHint: false,
768
+ openWorldHint: true,
693
769
  },
694
770
  }, async (input) => {
695
771
  const { confirmation_token, ...body } = input;
696
- const blocked = await requireWrite("wabery_business_agent_thread_control", confirmation_token, "thread_control");
772
+ const blocked = await requireWrite("wabery_set_business_agent_thread_control", confirmation_token, "thread_control");
697
773
  if (blocked)
698
774
  return blocked;
699
775
  return jsonResult(await client.post("/business-agents/thread-control", body));
700
776
  });
701
- server.registerTool("wabery_test_business_agent", {
777
+ registerTool("wabery_test_business_agent", {
702
778
  title: "Test Meta Business Agent",
703
- description: "Run a Meta Business Agent test scenario.",
779
+ description: "Use this when you need to submit a controlled test scenario to Meta for a business_agent_id from wabery_list_business_agents. This is a remote provider operation, not a read or a real-recipient message send.",
704
780
  inputSchema: {
705
781
  business_agent_id: z.string().min(1),
706
782
  input: z.string().min(1).optional(),
707
783
  scenario: z.record(z.string(), z.unknown()).optional(),
784
+ confirmation_token: z
785
+ .string()
786
+ .optional()
787
+ .describe("Required when write mode is enabled: test_business_agent"),
708
788
  },
709
789
  annotations: {
710
- readOnlyHint: true,
790
+ readOnlyHint: false,
711
791
  destructiveHint: false,
712
792
  idempotentHint: false,
713
793
  openWorldHint: true,
714
794
  },
715
- }, async (body) => jsonResult(await client.post("/business-agents/test", body)));
716
- server.registerTool("wabery_get_business_agent_eval", {
795
+ }, async ({ confirmation_token, ...body }) => {
796
+ const blocked = await requireWrite("wabery_test_business_agent", confirmation_token, "test_business_agent");
797
+ if (blocked)
798
+ return blocked;
799
+ return jsonResult(await client.post("/business-agents/test", body));
800
+ });
801
+ registerTool("wabery_get_business_agent_eval", {
717
802
  title: "Get Meta Business Agent eval summary",
718
- description: "Retrieve the Meta Business Agent eval summary.",
803
+ description: "Use this when you need the live Meta evaluation summary for a business_agent_id from wabery_list_business_agents. Do not use it to run a test scenario.",
719
804
  inputSchema: {
720
805
  business_agent_id: z.string().min(1),
721
806
  },
@@ -728,9 +813,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
728
813
  }, async ({ business_agent_id }) => jsonResult(await client.get("/business-agents/eval", {
729
814
  business_agent_id,
730
815
  })));
731
- server.registerTool("wabery_list_business_agent_skills", {
816
+ registerTool("wabery_list_business_agent_skills", {
732
817
  title: "List Meta Business Agent skills",
733
- description: "List configured Meta Business Agent skills.",
818
+ description: "Use this when you need the current Meta skill list for a business_agent_id from wabery_list_business_agents. Use the set tool only to replace the full list.",
734
819
  inputSchema: {
735
820
  business_agent_id: z.string().min(1),
736
821
  },
@@ -743,9 +828,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
743
828
  }, async ({ business_agent_id }) => jsonResult(await client.get("/business-agents/skills", {
744
829
  business_agent_id,
745
830
  })));
746
- server.registerTool("wabery_set_business_agent_skills", {
831
+ registerTool("wabery_set_business_agent_skills", {
747
832
  title: "Set Meta Business Agent skills",
748
- description: "Replace configured Meta Business Agent skills.",
833
+ description: "Use this when you need to replace the entire Meta skill list for a business_agent_id from wabery_list_business_agents. Do not use it to append one skill without first preserving existing entries.",
749
834
  inputSchema: {
750
835
  business_agent_id: z.string().min(1),
751
836
  skills: z.array(z.unknown()),
@@ -756,7 +841,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
756
841
  },
757
842
  annotations: {
758
843
  readOnlyHint: false,
759
- destructiveHint: false,
844
+ destructiveHint: true,
760
845
  idempotentHint: true,
761
846
  openWorldHint: true,
762
847
  },
@@ -766,9 +851,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
766
851
  return blocked;
767
852
  return jsonResult(await client.put("/business-agents/skills", body));
768
853
  });
769
- server.registerTool("wabery_list_projects", {
854
+ registerTool("wabery_list_projects", {
770
855
  title: "List Wabery projects",
771
- description: "List projects and their project_id values. Use project_id when creating flows or enrolling WhatsApp contacts.",
856
+ description: "Use this when you need to discover Wabery projects and their project_id values before selecting a project or passing an explicit project_id.",
772
857
  inputSchema: {},
773
858
  annotations: {
774
859
  readOnlyHint: true,
@@ -778,9 +863,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
778
863
  },
779
864
  }, async () => jsonResult(await client.get("/projects")));
780
865
  if (options.projectSelection) {
781
- server.registerTool("wabery_get_selected_project", {
866
+ registerTool("wabery_get_selected_project", {
782
867
  title: "Get selected Wabery project",
783
- description: "Return the project currently selected for this MCP session. If no project has been selected, Wabery uses the credential's default project.",
868
+ description: "Use this when you need to confirm which project this MCP session will use. A null selection means the credential's default project remains active.",
784
869
  inputSchema: {},
785
870
  annotations: {
786
871
  readOnlyHint: true,
@@ -793,14 +878,14 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
793
878
  selected_project_id: options.projectSelection?.get() ?? null,
794
879
  default_project_id: client.projectId ?? null,
795
880
  }));
796
- server.registerTool("wabery_select_project", {
881
+ registerTool("wabery_select_project", {
797
882
  title: "Select Wabery project",
798
- description: "Select the project that subsequent Wabery MCP tools should use in this session. Call wabery_list_projects first, then pass the project_id.",
883
+ description: "Use this when you need subsequent tools in this MCP session to target a project_id returned by wabery_list_projects. This changes session state, not the project itself.",
799
884
  inputSchema: {
800
885
  project_id: z.string().min(1),
801
886
  },
802
887
  annotations: {
803
- readOnlyHint: true,
888
+ readOnlyHint: false,
804
889
  destructiveHint: false,
805
890
  idempotentHint: true,
806
891
  openWorldHint: false,
@@ -815,9 +900,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
815
900
  });
816
901
  });
817
902
  }
818
- server.registerTool("wabery_get_project_readiness", {
903
+ registerTool("wabery_get_project_readiness", {
819
904
  title: "Get Wabery project readiness",
820
- description: "Explain what the selected project can do now: sandbox testing, production inbound, flow publish/send, templates, proactive messages, routing, opt-in, and submission handling. Call before building, after wabery_apply_config, before template/reminder work, and before the final creator summary.",
905
+ description: "Use this when you need capability readiness for the selected project or a project_id from wabery_list_projects: sandbox_test, production_inbound, flow_publish, flow_send, templates, proactive_messages, or submission_handling. Use channel readiness only for provider-specific phone details.",
821
906
  inputSchema: {
822
907
  project_id: z.string().min(1).optional(),
823
908
  requirements: z.array(readinessRequirementSchema).optional(),
@@ -829,9 +914,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
829
914
  openWorldHint: false,
830
915
  },
831
916
  }, async ({ project_id, requirements }) => jsonResult(await getReadiness(project_id, requirements)));
832
- server.registerTool("wabery_get_project", {
917
+ registerTool("wabery_get_project", {
833
918
  title: "Get a Wabery project",
834
- description: "Fetch a single project, including its webhook_secret (revealed only here, never in wabery_list_projects).",
919
+ description: "Use this when you need one project's user-safe settings by project_id from wabery_list_projects. Do not use it for capability readiness; use wabery_get_project_readiness.",
835
920
  inputSchema: { project_id: z.string().min(1) },
836
921
  annotations: {
837
922
  readOnlyHint: true,
@@ -840,9 +925,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
840
925
  openWorldHint: false,
841
926
  },
842
927
  }, async ({ project_id }) => jsonResult(await client.get(`/projects/${encodeURIComponent(project_id)}`)));
843
- server.registerTool("wabery_update_project", {
928
+ registerTool("wabery_update_project", {
844
929
  title: "Update a Wabery project",
845
- description: "Update name, description, routing_mode, require_opt_in, or webhook config (url, signing on/off, or a bring-your-own secret) of an existing project. Set require_opt_in=false on a dedicated production number so inbound senders can be replied to and messaged without the invite/opt-in step.",
930
+ description: "Use this when you need to overwrite project name, description, routing_mode, require_opt_in, or signed-webhook settings for a project_id from wabery_list_projects. Prefer wabery_apply_config for reviewed project-wide declarative changes.",
846
931
  inputSchema: {
847
932
  project_id: z.string().min(1),
848
933
  name: z.string().min(1).max(100).optional(),
@@ -852,25 +937,33 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
852
937
  webhook_url: z.string().url().nullable().optional(),
853
938
  webhook_signing_enabled: z.boolean().optional(),
854
939
  webhook_secret: z.string().min(16).max(256).optional(),
940
+ confirmation_token: z
941
+ .string()
942
+ .optional()
943
+ .describe("Required when write mode is enabled: update_project"),
855
944
  },
856
945
  annotations: {
857
946
  readOnlyHint: false,
858
- destructiveHint: false,
947
+ destructiveHint: true,
859
948
  idempotentHint: false,
860
- openWorldHint: false,
949
+ openWorldHint: true,
861
950
  },
862
- }, async ({ project_id, ...body }) => {
863
- const blocked = await requireWrite("wabery_update_project");
951
+ }, async ({ project_id, confirmation_token, ...body }) => {
952
+ const blocked = await requireWrite("wabery_update_project", confirmation_token, "update_project");
864
953
  if (blocked)
865
954
  return blocked;
866
955
  return jsonResult(await client.patch(`/projects/${encodeURIComponent(project_id)}`, body));
867
956
  });
868
- server.registerTool("wabery_rotate_webhook_secret", {
957
+ registerTool("wabery_rotate_webhook_secret", {
869
958
  title: "Rotate a project's webhook secret",
870
- description: "Set a new signing secret (server-generated, or a provided one) and return it once. Invalidates the previous secret.",
959
+ description: "Use this when you need to invalidate and replace the signed-webhook secret for a project_id from wabery_list_projects. The MCP result never returns the new secret; manage it in the Wabery dashboard.",
871
960
  inputSchema: {
872
961
  project_id: z.string().min(1),
873
962
  webhook_secret: z.string().min(16).max(256).optional(),
963
+ confirmation_token: z
964
+ .string()
965
+ .optional()
966
+ .describe("Required when write mode is enabled: rotate_webhook_secret"),
874
967
  },
875
968
  annotations: {
876
969
  readOnlyHint: false,
@@ -878,15 +971,15 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
878
971
  idempotentHint: false,
879
972
  openWorldHint: false,
880
973
  },
881
- }, async ({ project_id, webhook_secret }) => {
882
- const blocked = await requireWrite("wabery_rotate_webhook_secret");
974
+ }, async ({ project_id, webhook_secret, confirmation_token }) => {
975
+ const blocked = await requireWrite("wabery_rotate_webhook_secret", confirmation_token, "rotate_webhook_secret");
883
976
  if (blocked)
884
977
  return blocked;
885
978
  return jsonResult(await client.post(`/projects/${encodeURIComponent(project_id)}/rotate-webhook-secret`, webhook_secret ? { webhook_secret } : undefined));
886
979
  });
887
- server.registerTool("wabery_list_channels", {
980
+ registerTool("wabery_list_channels", {
888
981
  title: "List Wabery channels",
889
- description: "List connected messaging channels. Use a WhatsApp channel_id when sending a published flow. For WhatsApp templates or proactive template sends, inspect publish_readiness and tell the creator when a dedicated phone number, verified business, approved account, connected phone, or Meta payment method is missing.",
982
+ description: "Use this when you need WhatsApp channel_id values, connection state, routing warnings, or provider publish_readiness. Use project readiness first for capability-level discovery.",
890
983
  inputSchema: {},
891
984
  annotations: {
892
985
  readOnlyHint: true,
@@ -895,9 +988,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
895
988
  openWorldHint: false,
896
989
  },
897
990
  }, async () => jsonResult(await client.get("/channels")));
898
- server.registerTool("wabery_get_channel", {
991
+ registerTool("wabery_get_channel", {
899
992
  title: "Get a Wabery channel",
900
- description: "Fetch a single channel, including routing_warning and WhatsApp publish_readiness. If can_submit_templates is false, explain the blockers before attempting template creation or proactive template sends.",
993
+ description: "Use this when you need routing_warning and WhatsApp publish_readiness for a channel_id from wabery_list_channels. Do not use it as a substitute for project-level readiness.",
901
994
  inputSchema: { channel_id: z.string().min(1) },
902
995
  annotations: {
903
996
  readOnlyHint: true,
@@ -906,30 +999,34 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
906
999
  openWorldHint: false,
907
1000
  },
908
1001
  }, async ({ channel_id }) => jsonResult(await client.get(`/channels/${encodeURIComponent(channel_id)}`)));
909
- server.registerTool("wabery_update_channel", {
1002
+ registerTool("wabery_update_channel", {
910
1003
  title: "Update a Wabery channel's routing",
911
- description: "Set a dedicated channel's routing_mode (NONE | FLOWS | EXTERNAL). Use EXTERNAL so inbound to this channel is forwarded to the project webhook. The shared sandbox channel routes per-participant and cannot be updated this way.",
1004
+ description: "Use this when you need to overwrite a dedicated channel's routing_mode with NONE, FLOWS, or EXTERNAL using a channel_id from wabery_list_channels. Do not use it for the shared sandbox; update project routing instead.",
912
1005
  inputSchema: {
913
1006
  channel_id: z.string().min(1),
914
1007
  routing_mode: z.enum(["NONE", "FLOWS", "EXTERNAL"]),
1008
+ confirmation_token: z
1009
+ .string()
1010
+ .optional()
1011
+ .describe("Required when write mode is enabled: update_channel"),
915
1012
  },
916
1013
  annotations: {
917
1014
  readOnlyHint: false,
918
- destructiveHint: false,
1015
+ destructiveHint: true,
919
1016
  idempotentHint: true,
920
- openWorldHint: false,
1017
+ openWorldHint: true,
921
1018
  },
922
- }, async ({ channel_id, routing_mode }) => {
923
- const blocked = await requireWrite("wabery_update_channel");
1019
+ }, async ({ channel_id, routing_mode, confirmation_token }) => {
1020
+ const blocked = await requireWrite("wabery_update_channel", confirmation_token, "update_channel");
924
1021
  if (blocked)
925
1022
  return blocked;
926
1023
  return jsonResult(await client.patch(`/channels/${encodeURIComponent(channel_id)}`, {
927
1024
  routing_mode,
928
1025
  }));
929
1026
  });
930
- server.registerTool("wabery_create_registration_intent", {
1027
+ registerTool("wabery_create_registration_intent", {
931
1028
  title: "Create registration intent",
932
- description: "Mint a single-use wa.me registration link so an end-customer can connect over WhatsApp.",
1029
+ description: "Use this when you need a single-use wa.me link for an end customer to register with a project_id from wabery_list_projects. This creates the link but does not contact the customer.",
933
1030
  inputSchema: {
934
1031
  project_id: z
935
1032
  .string()
@@ -958,9 +1055,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
958
1055
  metadata,
959
1056
  }));
960
1057
  });
961
- server.registerTool("wabery_get_registration_intent", {
1058
+ registerTool("wabery_get_registration_intent", {
962
1059
  title: "Get registration intent",
963
- description: "Poll a registration intent's status. Works with a publishable key or a scoped secret key.",
1060
+ description: "Use this when you need to poll a registration intent by intent_id returned by wabery_create_registration_intent. Do not create another intent merely to check status.",
964
1061
  inputSchema: {
965
1062
  intent_id: z.string().min(1),
966
1063
  },
@@ -971,9 +1068,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
971
1068
  openWorldHint: false,
972
1069
  },
973
1070
  }, async ({ intent_id }) => jsonResult(await client.get(`/registration-intents/${encodeURIComponent(intent_id)}`)));
974
- server.registerTool("wabery_get_limits", {
1071
+ registerTool("wabery_get_limits", {
975
1072
  title: "Get Wabery API limits",
976
- description: "Return the enforced public API rate limit and monthly request quota for the configured API key.",
1073
+ description: "Use this when you need the current credential's enforced public API rate limit and monthly request quota. Do not use it for product-plan feature readiness.",
977
1074
  inputSchema: {},
978
1075
  annotations: {
979
1076
  readOnlyHint: true,
@@ -982,9 +1079,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
982
1079
  openWorldHint: false,
983
1080
  },
984
1081
  }, async () => jsonResult(await client.get("/limits")));
985
- server.registerTool("wabery_list_flows", {
1082
+ registerTool("wabery_list_flows", {
986
1083
  title: "List Wabery flows",
987
- description: "List flows, including ids, config keys, project ids, and Meta publish status.",
1084
+ description: "Use this when you need WhatsApp Flow ids, config keys, project ids, or Meta publish statuses before get, publish, send, dispatch, or submission operations.",
988
1085
  inputSchema: {},
989
1086
  annotations: {
990
1087
  readOnlyHint: true,
@@ -993,9 +1090,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
993
1090
  openWorldHint: false,
994
1091
  },
995
1092
  }, async () => jsonResult(await client.get("/flows")));
996
- server.registerTool("wabery_get_flow", {
1093
+ registerTool("wabery_get_flow", {
997
1094
  title: "Get Wabery flow",
998
- description: "Fetch one flow by id, including status and field schema.",
1095
+ description: "Use this when you need one WhatsApp Flow's draft, field schema, and publish status by flow_id from wabery_list_flows.",
999
1096
  inputSchema: {
1000
1097
  flow_id: z.string().min(1),
1001
1098
  },
@@ -1006,9 +1103,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1006
1103
  openWorldHint: false,
1007
1104
  },
1008
1105
  }, async ({ flow_id }) => jsonResult(await client.get(`/flows/${flow_id}`)));
1009
- server.registerTool("wabery_publish_flow", {
1106
+ registerTool("wabery_publish_flow", {
1010
1107
  title: "Publish Wabery flow",
1011
- description: "Publish a Wabery draft flow to Meta. Only call after validate/diff/apply and explicit user confirmation.",
1108
+ description: "Use this when you need to publish a reviewed draft WhatsApp Flow to Meta using a flow_id from wabery_list_flows. Do not use it to save a draft; use config apply, and obtain explicit confirmation first.",
1012
1109
  inputSchema: {
1013
1110
  flow_id: z.string().min(1),
1014
1111
  confirmation_token: z
@@ -1018,7 +1115,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1018
1115
  },
1019
1116
  annotations: {
1020
1117
  readOnlyHint: false,
1021
- destructiveHint: false,
1118
+ destructiveHint: true,
1022
1119
  idempotentHint: false,
1023
1120
  openWorldHint: true,
1024
1121
  },
@@ -1028,9 +1125,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1028
1125
  return blocked;
1029
1126
  return apiResult(client.post(`/flows/${flow_id}/publish`));
1030
1127
  });
1031
- server.registerTool("wabery_enroll_contact", {
1128
+ registerTool("wabery_enroll_contact", {
1032
1129
  title: "Enroll WhatsApp contact",
1033
- description: "Register a phone number against a Wabery project. Returns contact_id, which is the stable id used in flow submissions and future sends.",
1130
+ description: "Use this when you need to register a controlled or explicitly opted-in WhatsApp phone number for a project_id from wabery_list_projects. It returns stable contact_id and channel_id values for later sends.",
1034
1131
  inputSchema: {
1035
1132
  phone: z.string().min(5).describe("E.164 phone number."),
1036
1133
  project_id: z.string().min(1),
@@ -1051,7 +1148,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1051
1148
  },
1052
1149
  annotations: {
1053
1150
  readOnlyHint: false,
1054
- destructiveHint: false,
1151
+ destructiveHint: true,
1055
1152
  idempotentHint: true,
1056
1153
  openWorldHint: false,
1057
1154
  },
@@ -1061,9 +1158,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1061
1158
  return blocked;
1062
1159
  return jsonResult(await client.post("/contacts", input));
1063
1160
  });
1064
- server.registerTool("wabery_list_contacts", {
1161
+ registerTool("wabery_list_contacts", {
1065
1162
  title: "List Wabery contacts",
1066
- description: "List enrolled contacts and their stable contact_id values.",
1163
+ description: "Use this when you need enrolled WhatsApp contacts, stable contact_id values, and opt-in metadata before a targeted send or audience preparation.",
1067
1164
  inputSchema: {
1068
1165
  limit: z.number().int().min(1).max(100).optional(),
1069
1166
  starting_after: z.string().optional(),
@@ -1075,9 +1172,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1075
1172
  openWorldHint: false,
1076
1173
  },
1077
1174
  }, async ({ limit, starting_after }) => jsonResult(await client.get("/contacts", { limit, starting_after })));
1078
- server.registerTool("wabery_get_contact", {
1175
+ registerTool("wabery_get_contact", {
1079
1176
  title: "Get Wabery contact",
1080
- description: "Fetch one enrolled contact by stable contact_id before sending or unenrolling.",
1177
+ description: "Use this when you need one enrolled WhatsApp contact and consent state by contact_id from wabery_list_contacts or wabery_enroll_contact.",
1081
1178
  inputSchema: {
1082
1179
  contact_id: z.string().min(1),
1083
1180
  },
@@ -1088,9 +1185,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1088
1185
  openWorldHint: false,
1089
1186
  },
1090
1187
  }, async ({ contact_id }) => jsonResult(await client.get(`/contacts/${contact_id}`)));
1091
- server.registerTool("wabery_list_contact_imports", {
1188
+ registerTool("wabery_list_contact_imports", {
1092
1189
  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.",
1190
+ description: "Use this when you need CSV import_id values and processing counts after uploading through the dashboard or CLI. Only a completed import belongs in wabery_prepare_broadcast_audience.",
1094
1191
  inputSchema: {
1095
1192
  limit: z.number().int().min(1).max(100).optional(),
1096
1193
  starting_after: z.string().optional(),
@@ -1102,9 +1199,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1102
1199
  openWorldHint: false,
1103
1200
  },
1104
1201
  }, async (query) => apiResult(client.get("/contact-imports", query)));
1105
- server.registerTool("wabery_get_contact_import", {
1202
+ registerTool("wabery_get_contact_import", {
1106
1203
  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.",
1204
+ description: "Use this when you need status and row counts for an import_id from wabery_list_contact_imports. Wait for completed before broadcast preparation.",
1108
1205
  inputSchema: {
1109
1206
  import_id: z.string().min(1),
1110
1207
  },
@@ -1115,9 +1212,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1115
1212
  openWorldHint: false,
1116
1213
  },
1117
1214
  }, async ({ import_id }) => apiResult(client.get(`/contact-imports/${import_id}`)));
1118
- server.registerTool("wabery_unenroll_contact", {
1215
+ registerTool("wabery_unenroll_contact", {
1119
1216
  title: "Unenroll Wabery contact",
1120
- description: "Remove a contact phone's sandbox authorization for this organization. Pass erase=true for irreversible org-scoped contact data deletion.",
1217
+ description: "Use this when you need to remove sandbox authorization for a contact_id from wabery_list_contacts. Set erase=true only for confirmed, irreversible organization-scoped contact-data deletion.",
1121
1218
  inputSchema: {
1122
1219
  contact_id: z.string().min(1),
1123
1220
  erase: z.boolean().optional(),
@@ -1138,9 +1235,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1138
1235
  return blocked;
1139
1236
  return jsonResult(await client.delete(`/contacts/${contact_id}${erase ? "?erase=true" : ""}`));
1140
1237
  });
1141
- server.registerTool("wabery_list_broadcasts", {
1238
+ registerTool("wabery_list_broadcasts", {
1142
1239
  title: "List Wabery broadcasts",
1143
- description: "List broadcast campaigns and delivery counts. Filter by lifecycle status when monitoring preparation or delivery.",
1240
+ description: "Use this when you need WhatsApp broadcast_id values, lifecycle states, or aggregate delivery counts. Filter by status when monitoring preparation or delivery.",
1144
1241
  inputSchema: {
1145
1242
  limit: z.number().int().min(1).max(100).optional(),
1146
1243
  starting_after: z.string().optional(),
@@ -1153,9 +1250,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1153
1250
  openWorldHint: false,
1154
1251
  },
1155
1252
  }, async (query) => apiResult(client.get("/broadcasts", query)));
1156
- server.registerTool("wabery_get_broadcast", {
1253
+ registerTool("wabery_get_broadcast", {
1157
1254
  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.",
1255
+ description: "Use this when you need lifecycle status and recipient counts for a broadcast_id from wabery_list_broadcasts. Poll while preparing and review counts before send or schedule confirmation.",
1159
1256
  inputSchema: {
1160
1257
  broadcast_id: z.string().min(1),
1161
1258
  },
@@ -1166,9 +1263,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1166
1263
  openWorldHint: false,
1167
1264
  },
1168
1265
  }, async ({ broadcast_id }) => apiResult(client.get(`/broadcasts/${broadcast_id}`)));
1169
- server.registerTool("wabery_create_broadcast", {
1266
+ registerTool("wabery_create_broadcast", {
1170
1267
  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.",
1268
+ description: "Use this when you need a new WhatsApp broadcast draft from approved template variants and a channel_id from wabery_list_channels. This does not select recipients or send; use prepare next.",
1172
1269
  inputSchema: {
1173
1270
  name: z.string().min(1).max(120),
1174
1271
  channel_id: z.string().min(1),
@@ -1209,9 +1306,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1209
1306
  })),
1210
1307
  }));
1211
1308
  });
1212
- server.registerTool("wabery_prepare_broadcast_audience", {
1309
+ registerTool("wabery_prepare_broadcast_audience", {
1213
1310
  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.",
1311
+ description: "Use this when you need to start asynchronous audience resolution and overwrite the frozen recipient snapshot for a draft broadcast_id. Supply exactly one source: contact_ids from wabery_list_contacts, a completed import_id, or filter. This does not send; poll wabery_get_broadcast.",
1215
1312
  inputSchema: {
1216
1313
  broadcast_id: z.string().min(1),
1217
1314
  contact_ids: z.array(z.string().min(1)).max(1000).optional(),
@@ -1221,7 +1318,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1221
1318
  },
1222
1319
  annotations: {
1223
1320
  readOnlyHint: false,
1224
- destructiveHint: false,
1321
+ destructiveHint: true,
1225
1322
  idempotentHint: true,
1226
1323
  openWorldHint: false,
1227
1324
  },
@@ -1255,9 +1352,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1255
1352
  excludeContactIds: exclude_contact_ids ?? [],
1256
1353
  }));
1257
1354
  });
1258
- server.registerTool("wabery_list_broadcast_recipients", {
1355
+ registerTool("wabery_list_broadcast_recipients", {
1259
1356
  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.",
1357
+ description: "Use this when you need the frozen recipients, language selection, exclusions, failures, or delivery state for a prepared broadcast_id from wabery_list_broadcasts.",
1261
1358
  inputSchema: {
1262
1359
  broadcast_id: z.string().min(1),
1263
1360
  limit: z.number().int().min(1).max(100).optional(),
@@ -1271,9 +1368,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1271
1368
  openWorldHint: false,
1272
1369
  },
1273
1370
  }, async ({ broadcast_id, ...query }) => apiResult(client.get(`/broadcasts/${broadcast_id}/recipients`, query)));
1274
- server.registerTool("wabery_send_broadcast", {
1371
+ registerTool("wabery_send_broadcast", {
1275
1372
  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>.",
1373
+ description: "Use this when you need to start real WhatsApp delivery for a ready broadcast_id after reviewing its frozen audience and counts. Do not use it for preparation; explicit confirmation is required.",
1277
1374
  inputSchema: {
1278
1375
  broadcast_id: z.string().min(1),
1279
1376
  confirmation_token: z
@@ -1293,9 +1390,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1293
1390
  return blocked;
1294
1391
  return apiResult(client.post(`/broadcasts/${broadcast_id}/send`));
1295
1392
  });
1296
- server.registerTool("wabery_schedule_broadcast", {
1393
+ registerTool("wabery_schedule_broadcast", {
1297
1394
  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>.",
1395
+ description: "Use this when you need future WhatsApp delivery for a ready broadcast_id after reviewing its frozen audience, scheduled_at, and IANA timezone. Do not use it for immediate delivery; explicit confirmation is required.",
1299
1396
  inputSchema: {
1300
1397
  broadcast_id: z.string().min(1),
1301
1398
  scheduled_at: z.string().datetime(),
@@ -1320,9 +1417,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1320
1417
  timezone,
1321
1418
  }));
1322
1419
  });
1323
- server.registerTool("wabery_cancel_broadcast", {
1420
+ registerTool("wabery_cancel_broadcast", {
1324
1421
  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>.",
1422
+ description: "Use this when you need to cancel a broadcast_id and stop recipients not yet dispatched. Already accepted WhatsApp messages cannot be recalled; explicit confirmation is required.",
1326
1423
  inputSchema: {
1327
1424
  broadcast_id: z.string().min(1),
1328
1425
  confirmation_token: z
@@ -1342,9 +1439,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1342
1439
  return blocked;
1343
1440
  return apiResult(client.post(`/broadcasts/${broadcast_id}/cancel`));
1344
1441
  });
1345
- server.registerTool("wabery_duplicate_broadcast", {
1442
+ registerTool("wabery_duplicate_broadcast", {
1346
1443
  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.",
1444
+ description: "Use this when you need a new draft copied from a broadcast_id's channel, template variants, and mappings. The audience is never copied; prepare a new controlled audience.",
1348
1445
  inputSchema: {
1349
1446
  broadcast_id: z.string().min(1),
1350
1447
  },
@@ -1360,9 +1457,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1360
1457
  return blocked;
1361
1458
  return apiResult(client.post(`/broadcasts/${broadcast_id}/duplicate`));
1362
1459
  });
1363
- server.registerTool("wabery_list_conversations", {
1460
+ registerTool("wabery_list_conversations", {
1364
1461
  title: "List Wabery conversations",
1365
- description: "List organization-scoped conversations for inbox, support, or analytics tooling.",
1462
+ description: "Use this when you need organization-scoped WhatsApp conversation_id values for inbox review, support, or message-history lookup. Filter by contact_id when known.",
1366
1463
  inputSchema: {
1367
1464
  contact_id: z.string().optional(),
1368
1465
  active: z.boolean().optional(),
@@ -1379,9 +1476,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1379
1476
  ...query,
1380
1477
  active: query.active === undefined ? undefined : String(query.active),
1381
1478
  })));
1382
- server.registerTool("wabery_get_conversation", {
1479
+ registerTool("wabery_get_conversation", {
1383
1480
  title: "Get Wabery conversation",
1384
- description: "Fetch one organization-scoped conversation by id.",
1481
+ description: "Use this when you need one organization-scoped WhatsApp conversation by conversation_id from wabery_list_conversations.",
1385
1482
  inputSchema: {
1386
1483
  conversation_id: z.string().min(1),
1387
1484
  },
@@ -1392,9 +1489,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1392
1489
  openWorldHint: false,
1393
1490
  },
1394
1491
  }, async ({ conversation_id }) => jsonResult(await client.get(`/conversations/${conversation_id}`)));
1395
- server.registerTool("wabery_list_conversation_messages", {
1492
+ registerTool("wabery_list_conversation_messages", {
1396
1493
  title: "List Wabery conversation messages",
1397
- description: "Fetch message history for a conversation. Cached inbound WhatsApp media includes short-lived Wabery signed URLs; expires_at is the retention deadline.",
1494
+ description: "Use this when you need message history for a conversation_id from wabery_list_conversations. Retained inbound media may include short-lived signed URLs; expires_at is the retention deadline.",
1398
1495
  inputSchema: {
1399
1496
  conversation_id: z.string().min(1),
1400
1497
  limit: z.number().int().min(1).max(100).optional(),
@@ -1408,27 +1505,33 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1408
1505
  openWorldHint: false,
1409
1506
  },
1410
1507
  }, async ({ conversation_id, ...query }) => jsonResult(await client.get(`/conversations/${conversation_id}/messages`, query)));
1411
- server.registerTool("wabery_send_message", {
1412
- title: "Send Wabery message",
1413
- description: "Send a WhatsApp text, approved template, media, or interactive message. The body must match POST /messages and should include an idempotency_key for retries.",
1508
+ registerTool("wabery_send_message", {
1509
+ title: "Send WhatsApp message",
1510
+ description: "Use this when you need to send one real WhatsApp text, approved template, media, or interactive message. Do not use it to send a Flow; use wabery_send_flow. The body must identify channel and recipient using ids from list tools and should include idempotency_key; explicit confirmation is required.",
1414
1511
  inputSchema: {
1415
- body: z.record(z.string(), z.unknown()),
1512
+ body: z
1513
+ .record(z.string(), z.unknown())
1514
+ .describe("POST /messages body. Include channel_id plus contact_id, conversation_id, or to; include type-specific content and an idempotency_key for safe retries."),
1515
+ confirmation_token: z
1516
+ .string()
1517
+ .optional()
1518
+ .describe("Required when write mode is enabled: send_message"),
1416
1519
  },
1417
1520
  annotations: {
1418
1521
  readOnlyHint: false,
1419
- destructiveHint: false,
1522
+ destructiveHint: true,
1420
1523
  idempotentHint: false,
1421
1524
  openWorldHint: true,
1422
1525
  },
1423
- }, async ({ body }) => {
1424
- const blocked = await requireWrite("wabery_send_message");
1526
+ }, async ({ body, confirmation_token }) => {
1527
+ const blocked = await requireWrite("wabery_send_message", confirmation_token, "send_message");
1425
1528
  if (blocked)
1426
1529
  return blocked;
1427
1530
  return apiResult(client.post("/messages", body));
1428
1531
  });
1429
- server.registerTool("wabery_get_message", {
1532
+ registerTool("wabery_get_message", {
1430
1533
  title: "Get Wabery message",
1431
- description: "Fetch one message by message_id to inspect queued/provider status, delivery state, and Meta failure details after wabery_send_message.",
1534
+ description: "Use this when you need queued, provider, delivery, or failure status for a message_id returned by wabery_send_message or message history. An accepted send is not proof of delivery.",
1432
1535
  inputSchema: {
1433
1536
  message_id: z.string().min(1),
1434
1537
  },
@@ -1439,9 +1542,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1439
1542
  openWorldHint: false,
1440
1543
  },
1441
1544
  }, async ({ message_id }) => jsonResult(await client.get(`/messages/${message_id}`)));
1442
- server.registerTool("wabery_list_templates", {
1545
+ registerTool("wabery_list_templates", {
1443
1546
  title: "List WhatsApp templates",
1444
- description: "List WhatsApp message templates and approval statuses for dedicated WhatsApp channels.",
1547
+ description: "Use this when you need WhatsApp template_id values and approval states, optionally for a channel_id from wabery_list_channels, before template sends or broadcasts.",
1445
1548
  inputSchema: {
1446
1549
  channel_id: z.string().optional(),
1447
1550
  status: z.string().optional(),
@@ -1453,61 +1556,67 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1453
1556
  openWorldHint: false,
1454
1557
  },
1455
1558
  }, async (query) => jsonResult(await client.get("/templates", query)));
1456
- server.registerTool("wabery_get_template", {
1559
+ registerTool("wabery_get_template", {
1457
1560
  title: "Get WhatsApp template",
1458
- description: "Fetch one WhatsApp template and its Meta approval status. Set refresh=true to force a live Meta status check.",
1561
+ description: "Use this when you need one template by template_id from wabery_list_templates. Set refresh=true only for a live Meta check that also updates Wabery's cached status.",
1459
1562
  inputSchema: {
1460
1563
  template_id: z.string().min(1),
1461
1564
  refresh: z.boolean().optional(),
1462
1565
  },
1463
1566
  annotations: {
1464
- readOnlyHint: true,
1567
+ readOnlyHint: false,
1465
1568
  destructiveHint: false,
1466
1569
  idempotentHint: true,
1467
- openWorldHint: false,
1570
+ openWorldHint: true,
1468
1571
  },
1469
1572
  }, async ({ template_id, refresh }) => jsonResult(await client.get(`/templates/${template_id}`, {
1470
1573
  refresh: refresh ? "true" : undefined,
1471
1574
  })));
1472
- server.registerTool("wabery_create_template", {
1575
+ registerTool("wabery_create_template", {
1473
1576
  title: "Create WhatsApp template",
1474
- description: "Create and submit a WhatsApp message template to Meta for approval. Only dedicated, non-sandbox WhatsApp channels with Meta-verified business accounts and a Meta payment method can submit templates. Check channel publish_readiness first.",
1577
+ description: "Use this when you need to submit a new WhatsApp template to Meta for approval after checking channel publish_readiness. Do not use it to inspect or send an existing template; explicit confirmation is required.",
1475
1578
  inputSchema: {
1476
- body: z.record(z.string(), z.unknown()),
1579
+ body: z
1580
+ .record(z.string(), z.unknown())
1581
+ .describe("Template submission body including a dedicated WhatsApp channel_id, name, category, language, and components accepted by POST /templates."),
1582
+ confirmation_token: z
1583
+ .string()
1584
+ .optional()
1585
+ .describe("Required when write mode is enabled: create_template"),
1477
1586
  },
1478
1587
  annotations: {
1479
1588
  readOnlyHint: false,
1480
- destructiveHint: false,
1589
+ destructiveHint: true,
1481
1590
  idempotentHint: false,
1482
1591
  openWorldHint: true,
1483
1592
  },
1484
- }, async ({ body }) => {
1485
- const blocked = await requireWrite("wabery_create_template");
1593
+ }, async ({ body, confirmation_token }) => {
1594
+ const blocked = await requireWrite("wabery_create_template", confirmation_token, "create_template");
1486
1595
  if (blocked)
1487
1596
  return blocked;
1488
1597
  return apiResult(client.post("/templates", body));
1489
1598
  });
1490
- server.registerTool("wabery_wait_template", {
1599
+ registerTool("wabery_wait_template", {
1491
1600
  title: "Wait for WhatsApp template approval",
1492
- description: "Poll a WhatsApp template until Meta reports APPROVED. Fails if Meta rejects/disables it or if timeout_seconds elapses.",
1601
+ description: "Use this when you need to poll Meta for a template_id from wabery_list_templates until APPROVED or timeout. This repeatedly refreshes Wabery's cached provider status; do not use it for a single cached read.",
1493
1602
  inputSchema: {
1494
1603
  template_id: z.string().min(1),
1495
1604
  timeout_seconds: z.number().int().min(1).max(86_400).optional(),
1496
1605
  interval_seconds: z.number().int().min(1).max(300).optional(),
1497
1606
  },
1498
1607
  annotations: {
1499
- readOnlyHint: true,
1608
+ readOnlyHint: false,
1500
1609
  destructiveHint: false,
1501
1610
  idempotentHint: true,
1502
- openWorldHint: false,
1611
+ openWorldHint: true,
1503
1612
  },
1504
1613
  }, async ({ template_id, timeout_seconds, interval_seconds }) => jsonResult(await waitUntilTemplateApproved(client, template_id, {
1505
1614
  timeoutMs: (timeout_seconds ?? 86_400) * 1000,
1506
1615
  intervalMs: (interval_seconds ?? 60) * 1000,
1507
1616
  })));
1508
- server.registerTool("wabery_send_flow", {
1509
- title: "Send Wabery flow",
1510
- description: "Send a published WhatsApp Flow to an enrolled contact, existing conversation, or phone number. Returns flow_token and dispatch info.",
1617
+ registerTool("wabery_send_flow", {
1618
+ title: "Send WhatsApp Flow",
1619
+ description: "Use this when you need to send one published WhatsApp Flow using flow_id and channel_id from list tools plus exactly one controlled contact_id, conversation_id, or phone number. Do not use wabery_send_message for a Flow; explicit confirmation is required.",
1511
1620
  inputSchema: {
1512
1621
  flow_id: z.string().min(1),
1513
1622
  channel_id: z.string().min(1),
@@ -1520,22 +1629,26 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1520
1629
  footer_text: z.string().max(60).optional(),
1521
1630
  first_screen: z.string().optional(),
1522
1631
  screen_data: z.record(z.string(), z.unknown()).optional(),
1632
+ confirmation_token: z
1633
+ .string()
1634
+ .optional()
1635
+ .describe("Required when write mode is enabled: send_flow:<flow_id>"),
1523
1636
  },
1524
1637
  annotations: {
1525
1638
  readOnlyHint: false,
1526
- destructiveHint: false,
1639
+ destructiveHint: true,
1527
1640
  idempotentHint: false,
1528
1641
  openWorldHint: true,
1529
1642
  },
1530
- }, async ({ flow_id, ...body }) => {
1531
- const blocked = await requireWrite("wabery_send_flow");
1643
+ }, async ({ flow_id, confirmation_token, ...body }) => {
1644
+ const blocked = await requireWrite("wabery_send_flow", confirmation_token, `send_flow:${flow_id}`);
1532
1645
  if (blocked)
1533
1646
  return blocked;
1534
1647
  return apiResult(client.post(`/flows/${flow_id}/send`, body));
1535
1648
  });
1536
- server.registerTool("wabery_list_submissions", {
1649
+ registerTool("wabery_list_submissions", {
1537
1650
  title: "List Wabery submissions",
1538
- description: "List completed WhatsApp Flow submissions for the organization.",
1651
+ description: "Use this when you need completed WhatsApp Flow submissions, optionally filtered by project agent_id or flow_id from wabery_list_flows.",
1539
1652
  inputSchema: {
1540
1653
  agent_id: z.string().optional(),
1541
1654
  flow_id: z.string().optional(),
@@ -1549,9 +1662,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1549
1662
  openWorldHint: false,
1550
1663
  },
1551
1664
  }, async (query) => jsonResult(await client.get("/submissions", query)));
1552
- server.registerTool("wabery_list_dispatches", {
1665
+ registerTool("wabery_list_dispatches", {
1553
1666
  title: "List Wabery dispatches",
1554
- description: "List sent flow dispatches. Use this to inspect flow_token status across sends.",
1667
+ description: "Use this when you need sent WhatsApp Flow dispatches and flow_token values, optionally filtered by flow_id or contact_id. Use wabery_get_dispatch for one token.",
1555
1668
  inputSchema: {
1556
1669
  flow_id: z.string().optional(),
1557
1670
  contact_id: z.string().optional(),
@@ -1566,9 +1679,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1566
1679
  openWorldHint: false,
1567
1680
  },
1568
1681
  }, async (query) => jsonResult(await client.get("/dispatches", query)));
1569
- server.registerTool("wabery_get_dispatch", {
1682
+ registerTool("wabery_get_dispatch", {
1570
1683
  title: "Get Wabery dispatch",
1571
- description: "Get one flow dispatch by flow_token.",
1684
+ description: "Use this when you need provider and completion status for one WhatsApp Flow dispatch by flow_token returned by wabery_send_flow or wabery_list_dispatches.",
1572
1685
  inputSchema: {
1573
1686
  flow_token: z.string().min(1),
1574
1687
  },
@@ -1580,9 +1693,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1580
1693
  },
1581
1694
  }, async ({ flow_token }) => jsonResult(await client.get(`/dispatches/${flow_token}`)));
1582
1695
  // ─── Hosted functions (serverless logic the agent authors) ───────────
1583
- server.registerTool("wabery_list_functions", {
1696
+ registerTool("wabery_list_functions", {
1584
1697
  title: "List hosted functions",
1585
- description: "List the project's hosted functions (serverless handlers that run on send/tool triggers).",
1698
+ description: "Use this when you need hosted function_id values, deployment state, or exposure settings for the selected project before get, deploy, test, invoke, update, or delete operations.",
1586
1699
  inputSchema: {},
1587
1700
  annotations: {
1588
1701
  readOnlyHint: true,
@@ -1591,9 +1704,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1591
1704
  openWorldHint: false,
1592
1705
  },
1593
1706
  }, async () => jsonResult(await client.get("/functions")));
1594
- server.registerTool("wabery_get_function", {
1707
+ registerTool("wabery_get_function", {
1595
1708
  title: "Get a hosted function",
1596
- description: "Fetch one hosted function including its current source.",
1709
+ description: "Use this when you need metadata and current source for a function_id from wabery_list_functions. Do not use it to execute the function.",
1597
1710
  inputSchema: { function_id: z.string().min(1) },
1598
1711
  annotations: {
1599
1712
  readOnlyHint: true,
@@ -1602,9 +1715,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1602
1715
  openWorldHint: false,
1603
1716
  },
1604
1717
  }, async ({ function_id }) => jsonResult(await client.get(`/functions/${function_id}`)));
1605
- server.registerTool("wabery_create_function", {
1718
+ registerTool("wabery_create_function", {
1606
1719
  title: "Create a hosted function",
1607
- description: "Create a hosted function (metadata only). Deploy source separately with wabery_deploy_function. trigger_type is an AutomationTriggerType, e.g. KEYWORD, WELCOME, ANY_MESSAGE.",
1720
+ description: "Use this when you need a new hosted-function record without deploying source. trigger_type accepts values such as KEYWORD, WELCOME, or ANY_MESSAGE; use wabery_deploy_function next.",
1608
1721
  inputSchema: {
1609
1722
  slug: z
1610
1723
  .string()
@@ -1631,9 +1744,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1631
1744
  return blocked;
1632
1745
  return jsonResult(await client.post("/functions", body));
1633
1746
  });
1634
- server.registerTool("wabery_deploy_function", {
1747
+ registerTool("wabery_deploy_function", {
1635
1748
  title: "Deploy a hosted function",
1636
- description: "Bundle and upload TypeScript source for a hosted function. Returns { status, version_id?, diagnostics? } — read diagnostics on error, fix, and redeploy.",
1749
+ description: "Use this when you need to bundle and overwrite the active TypeScript deployment for a function_id from wabery_list_functions. Do not use it to execute code; use test or invoke after reviewing diagnostics.",
1637
1750
  inputSchema: {
1638
1751
  function_id: z.string().min(1),
1639
1752
  source: z.string().min(1),
@@ -1644,7 +1757,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1644
1757
  },
1645
1758
  annotations: {
1646
1759
  readOnlyHint: false,
1647
- destructiveHint: false,
1760
+ destructiveHint: true,
1648
1761
  idempotentHint: true,
1649
1762
  openWorldHint: false,
1650
1763
  },
@@ -1654,9 +1767,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1654
1767
  return blocked;
1655
1768
  return jsonResult(await client.post(`/functions/${function_id}/deploy`, { source }));
1656
1769
  });
1657
- server.registerTool("wabery_test_function", {
1770
+ registerTool("wabery_test_function", {
1658
1771
  title: "Test a hosted function",
1659
- description: "Invoke a hosted function with a sample message event (no real send) and return its result plus logs.",
1772
+ description: "Use this when you need to execute a function_id with a synthetic message event and inspect its result and logs. Do not use it as a side-effect-free preview: function code can persist data or call external APIs; use wabery_invoke_function for a real structured tool call.",
1660
1773
  inputSchema: {
1661
1774
  function_id: z.string().min(1),
1662
1775
  text: z.string().max(4000).optional(),
@@ -1667,9 +1780,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1667
1780
  },
1668
1781
  annotations: {
1669
1782
  readOnlyHint: false,
1670
- destructiveHint: false,
1671
- idempotentHint: true,
1672
- openWorldHint: false,
1783
+ destructiveHint: true,
1784
+ idempotentHint: false,
1785
+ openWorldHint: true,
1673
1786
  },
1674
1787
  }, async ({ function_id, text, confirmation_token }) => {
1675
1788
  const blocked = await requireWrite("wabery_test_function", confirmation_token, "test_function");
@@ -1677,9 +1790,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1677
1790
  return blocked;
1678
1791
  return jsonResult(await client.post(`/functions/${function_id}/test`, { text }));
1679
1792
  });
1680
- server.registerTool("wabery_tail_function_logs", {
1793
+ registerTool("wabery_tail_function_logs", {
1681
1794
  title: "Tail hosted function logs",
1682
- description: "Recent invocations of a hosted function (status, cpu, error, captured logs) for debugging.",
1795
+ description: "Use this when you need recent invocation status, CPU, errors, or captured logs for a function_id from wabery_list_functions. This does not execute the function.",
1683
1796
  inputSchema: { function_id: z.string().min(1) },
1684
1797
  annotations: {
1685
1798
  readOnlyHint: true,
@@ -1688,9 +1801,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1688
1801
  openWorldHint: false,
1689
1802
  },
1690
1803
  }, async ({ function_id }) => jsonResult(await client.get(`/functions/${function_id}/logs`)));
1691
- server.registerTool("wabery_delete_function", {
1804
+ registerTool("wabery_delete_function", {
1692
1805
  title: "Delete a hosted function",
1693
- description: "Delete a hosted function and its deployed Worker.",
1806
+ description: "Use this when you need to permanently delete a function_id from wabery_list_functions and its deployed Worker. Do not use update to simulate deletion.",
1694
1807
  inputSchema: {
1695
1808
  function_id: z.string().min(1),
1696
1809
  confirmation_token: z
@@ -1710,9 +1823,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1710
1823
  return blocked;
1711
1824
  return jsonResult(await client.delete(`/functions/${function_id}`));
1712
1825
  });
1713
- server.registerTool("wabery_update_function", {
1826
+ registerTool("wabery_update_function", {
1714
1827
  title: "Update a hosted function",
1715
- description: "Update a hosted function: rename, set expose_as_mcp_tool (make it callable by external agents), toggle is_active, or set input_schema (the tool schema used for agent/MCP calls).",
1828
+ description: "Use this when you need to overwrite metadata for a function_id from wabery_list_functions: name, expose_as_mcp_tool, is_active, or input_schema. Do not use it to deploy source or invoke code.",
1716
1829
  inputSchema: {
1717
1830
  function_id: z.string().min(1),
1718
1831
  name: z.string().min(1).max(120).optional(),
@@ -1726,7 +1839,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1726
1839
  },
1727
1840
  annotations: {
1728
1841
  readOnlyHint: false,
1729
- destructiveHint: false,
1842
+ destructiveHint: true,
1730
1843
  idempotentHint: true,
1731
1844
  openWorldHint: false,
1732
1845
  },
@@ -1737,9 +1850,9 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1737
1850
  return blocked;
1738
1851
  return jsonResult(await client.patch(`/functions/${function_id}`, body));
1739
1852
  });
1740
- server.registerTool("wabery_invoke_function", {
1853
+ registerTool("wabery_invoke_function", {
1741
1854
  title: "Invoke a hosted function as a tool",
1742
- description: "Call a hosted function as a tool (mcp_call) with structured arguments and return its result. Optionally target a specific contact.",
1855
+ description: "Use this when you need a real structured mcp_call to an exposed function_id from wabery_list_functions, optionally with contact_id from wabery_list_contacts. Do not use it for synthetic testing; arbitrary function code can persist data, call APIs, or send messages.",
1743
1856
  inputSchema: {
1744
1857
  function_id: z.string().min(1),
1745
1858
  arguments: z.record(z.string(), z.unknown()).optional(),
@@ -1751,8 +1864,8 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
1751
1864
  },
1752
1865
  annotations: {
1753
1866
  readOnlyHint: false,
1754
- destructiveHint: false,
1755
- idempotentHint: true,
1867
+ destructiveHint: true,
1868
+ idempotentHint: false,
1756
1869
  openWorldHint: true,
1757
1870
  },
1758
1871
  }, async ({ function_id, arguments: args, contact_id, confirmation_token, }) => {