@codespar/mcp-whatsapp-cloud 0.1.0 → 0.2.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.
package/dist/index.js CHANGED
@@ -7,18 +7,29 @@
7
7
  * all sit on top of this. Target: merchants with an approved WhatsApp Business
8
8
  * Account (WABA) who want Meta-direct pricing and full control.
9
9
  *
10
- * Tools (11):
10
+ * Tools (22):
11
11
  * send_text_message — simple text message
12
12
  * send_template_message — approved template (required for 24h+ business-initiated)
13
13
  * send_media_message — image, video, document, or audio
14
- * send_interactive_message — buttons or list
14
+ * send_interactive_message — buttons or list (generic)
15
+ * send_interactive_cta_url — interactive CTA URL button
16
+ * send_interactive_flow — WhatsApp Flows message
15
17
  * send_location_message — latitude/longitude pin
18
+ * send_contacts_message — vCard-style contact cards
19
+ * send_reaction_message — emoji reaction to an inbound message
20
+ * send_typing_indicator — show typing indicator on a received message
16
21
  * mark_message_as_read — mark an incoming message as read
17
22
  * upload_media — upload a file and get a media_id (multipart)
18
23
  * retrieve_media_url — resolve a media_id to a downloadable URL
19
24
  * delete_media — delete an uploaded media asset
20
25
  * list_templates — list templates on the WABA
21
26
  * create_template — submit a new template for Meta review
27
+ * delete_template — delete a template by name
28
+ * get_business_profile — read business profile on the phone number
29
+ * update_business_profile — edit business profile fields
30
+ * list_phone_numbers — list phone numbers on the WABA
31
+ * request_verification_code — request SMS/voice code to verify a phone number
32
+ * verify_code — submit the received verification code
22
33
  *
23
34
  * Authentication
24
35
  * Bearer token (permanent system-user access token).
@@ -86,7 +97,7 @@ async function whatsappRequest(method, path, body, opts = {}) {
86
97
  const text = await res.text();
87
98
  return text ? JSON.parse(text) : {};
88
99
  }
89
- const server = new Server({ name: "mcp-whatsapp-cloud", version: "0.1.0" }, { capabilities: { tools: {} } });
100
+ const server = new Server({ name: "mcp-whatsapp-cloud", version: "0.2.0" }, { capabilities: { tools: {} } });
90
101
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
91
102
  tools: [
92
103
  {
@@ -151,6 +162,42 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
151
162
  required: ["to", "interactive"],
152
163
  },
153
164
  },
165
+ {
166
+ name: "send_interactive_cta_url",
167
+ description: "Send an interactive message with a single CTA URL button. Opens the URL when the recipient taps it. Available without template approval inside the 24h window.",
168
+ inputSchema: {
169
+ type: "object",
170
+ properties: {
171
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
172
+ body_text: { type: "string", description: "Main body text" },
173
+ button_text: { type: "string", description: "Label shown on the CTA button (max 20 chars)" },
174
+ url: { type: "string", description: "HTTPS URL the button opens" },
175
+ header_text: { type: "string", description: "Optional header text" },
176
+ footer_text: { type: "string", description: "Optional footer text" },
177
+ },
178
+ required: ["to", "body_text", "button_text", "url"],
179
+ },
180
+ },
181
+ {
182
+ name: "send_interactive_flow",
183
+ description: "Send a WhatsApp Flow message. Flows are Meta's structured UI experiences (forms, appointment booking, etc.) rendered inside WhatsApp.",
184
+ inputSchema: {
185
+ type: "object",
186
+ properties: {
187
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
188
+ flow_id: { type: "string", description: "ID of the approved Flow" },
189
+ flow_token: { type: "string", description: "Opaque token your server correlates with this send" },
190
+ flow_cta: { type: "string", description: "Label on the button that opens the flow (max 20 chars)" },
191
+ body_text: { type: "string", description: "Body text above the CTA" },
192
+ header_text: { type: "string", description: "Optional header text" },
193
+ footer_text: { type: "string", description: "Optional footer text" },
194
+ flow_action: { type: "string", enum: ["navigate", "data_exchange"], description: "Flow action type (default navigate)" },
195
+ flow_action_payload: { type: "object", description: "Optional action payload: { screen, data }. Required for navigate." },
196
+ mode: { type: "string", enum: ["draft", "published"], description: "Flow mode. Use draft while testing." },
197
+ },
198
+ required: ["to", "flow_id", "flow_token", "flow_cta", "body_text"],
199
+ },
200
+ },
154
201
  {
155
202
  name: "send_location_message",
156
203
  description: "Send a location pin with latitude/longitude and optional name/address.",
@@ -166,6 +213,46 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
166
213
  required: ["to", "latitude", "longitude"],
167
214
  },
168
215
  },
216
+ {
217
+ name: "send_contacts_message",
218
+ description: "Send one or more contact cards (vCard-like). Each contact includes name and at least one of phones, emails, addresses, urls, or org.",
219
+ inputSchema: {
220
+ type: "object",
221
+ properties: {
222
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
223
+ contacts: {
224
+ type: "array",
225
+ description: "Array of contact objects per Cloud API spec (name, phones, emails, addresses, org, urls, birthday).",
226
+ items: { type: "object" },
227
+ },
228
+ },
229
+ required: ["to", "contacts"],
230
+ },
231
+ },
232
+ {
233
+ name: "send_reaction_message",
234
+ description: "Send an emoji reaction on a previously received/sent message. Pass empty string for `emoji` to clear a reaction.",
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: {
238
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
239
+ message_id: { type: "string", description: "wamid of the message being reacted to" },
240
+ emoji: { type: "string", description: "Single unicode emoji. Empty string clears the reaction." },
241
+ },
242
+ required: ["to", "message_id", "emoji"],
243
+ },
244
+ },
245
+ {
246
+ name: "send_typing_indicator",
247
+ description: "Show a typing indicator on a received message. Also marks the message as read. Indicator auto-clears after ~25s or when you reply.",
248
+ inputSchema: {
249
+ type: "object",
250
+ properties: {
251
+ message_id: { type: "string", description: "wamid of the inbound message to show the indicator on" },
252
+ },
253
+ required: ["message_id"],
254
+ },
255
+ },
169
256
  {
170
257
  name: "mark_message_as_read",
171
258
  description: "Mark an incoming message as read so the sender sees the blue double-check. Uses the wamid from the inbound webhook.",
@@ -243,6 +330,76 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
243
330
  required: ["name", "language", "category", "components"],
244
331
  },
245
332
  },
333
+ {
334
+ name: "delete_template",
335
+ description: "Delete a message template from the WABA by name. Optionally scope by hsm_id when two templates share a name across languages.",
336
+ inputSchema: {
337
+ type: "object",
338
+ properties: {
339
+ name: { type: "string", description: "Template name to delete" },
340
+ hsm_id: { type: "string", description: "Optional template id (when multiple languages share a name and only one should be deleted)" },
341
+ },
342
+ required: ["name"],
343
+ },
344
+ },
345
+ {
346
+ name: "get_business_profile",
347
+ description: "Read the WhatsApp business profile (about, description, email, websites, vertical, address) for the configured phone number.",
348
+ inputSchema: {
349
+ type: "object",
350
+ properties: {
351
+ fields: { type: "string", description: "Comma-separated field list. Default: about,address,description,email,profile_picture_url,websites,vertical" },
352
+ },
353
+ },
354
+ },
355
+ {
356
+ name: "update_business_profile",
357
+ description: "Update the business profile on the configured phone number. Supply only the fields you want to change.",
358
+ inputSchema: {
359
+ type: "object",
360
+ properties: {
361
+ about: { type: "string", description: "About text (max 139 chars)" },
362
+ address: { type: "string", description: "Physical address" },
363
+ description: { type: "string", description: "Business description" },
364
+ email: { type: "string", description: "Contact email" },
365
+ vertical: { type: "string", description: "Business vertical (e.g. RETAIL, EDU, HEALTH, FINANCE, OTHER)" },
366
+ websites: { type: "array", description: "Up to two URLs", items: { type: "string" } },
367
+ },
368
+ },
369
+ },
370
+ {
371
+ name: "list_phone_numbers",
372
+ description: "List all phone numbers registered under the WhatsApp Business Account, including display name, quality rating, and verification status.",
373
+ inputSchema: {
374
+ type: "object",
375
+ properties: {
376
+ limit: { type: "number", description: "Max results per page (default 25)" },
377
+ },
378
+ },
379
+ },
380
+ {
381
+ name: "request_verification_code",
382
+ description: "Request Meta to send a verification code to the configured phone number via SMS or voice. Use before verify_code.",
383
+ inputSchema: {
384
+ type: "object",
385
+ properties: {
386
+ code_method: { type: "string", enum: ["SMS", "VOICE"], description: "Delivery method for the verification code" },
387
+ language: { type: "string", description: "BCP-47 language tag for the voice/SMS copy (e.g. en_US, pt_BR)" },
388
+ },
389
+ required: ["code_method", "language"],
390
+ },
391
+ },
392
+ {
393
+ name: "verify_code",
394
+ description: "Submit the verification code received via SMS/voice after request_verification_code. Completes registration.",
395
+ inputSchema: {
396
+ type: "object",
397
+ properties: {
398
+ code: { type: "string", description: "Verification code received (digits only, strip dashes)" },
399
+ },
400
+ required: ["code"],
401
+ },
402
+ },
246
403
  ],
247
404
  }));
248
405
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -308,6 +465,64 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
308
465
  };
309
466
  return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
310
467
  }
468
+ case "send_interactive_cta_url": {
469
+ const interactive = {
470
+ type: "cta_url",
471
+ body: { text: a.body_text },
472
+ action: {
473
+ name: "cta_url",
474
+ parameters: {
475
+ display_text: a.button_text,
476
+ url: a.url,
477
+ },
478
+ },
479
+ };
480
+ if (a.header_text)
481
+ interactive.header = { type: "text", text: a.header_text };
482
+ if (a.footer_text)
483
+ interactive.footer = { text: a.footer_text };
484
+ const body = {
485
+ messaging_product: "whatsapp",
486
+ recipient_type: "individual",
487
+ to: a.to,
488
+ type: "interactive",
489
+ interactive,
490
+ };
491
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
492
+ }
493
+ case "send_interactive_flow": {
494
+ const parameters = {
495
+ flow_message_version: "3",
496
+ flow_token: a.flow_token,
497
+ flow_id: a.flow_id,
498
+ flow_cta: a.flow_cta,
499
+ flow_action: a.flow_action ?? "navigate",
500
+ };
501
+ if (a.mode)
502
+ parameters.mode = a.mode;
503
+ if (a.flow_action_payload)
504
+ parameters.flow_action_payload = a.flow_action_payload;
505
+ const interactive = {
506
+ type: "flow",
507
+ body: { text: a.body_text },
508
+ action: {
509
+ name: "flow",
510
+ parameters,
511
+ },
512
+ };
513
+ if (a.header_text)
514
+ interactive.header = { type: "text", text: a.header_text };
515
+ if (a.footer_text)
516
+ interactive.footer = { text: a.footer_text };
517
+ const body = {
518
+ messaging_product: "whatsapp",
519
+ recipient_type: "individual",
520
+ to: a.to,
521
+ type: "interactive",
522
+ interactive,
523
+ };
524
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
525
+ }
311
526
  case "send_location_message": {
312
527
  const location = {
313
528
  latitude: a.latitude,
@@ -326,6 +541,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
326
541
  };
327
542
  return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
328
543
  }
544
+ case "send_contacts_message": {
545
+ const body = {
546
+ messaging_product: "whatsapp",
547
+ recipient_type: "individual",
548
+ to: a.to,
549
+ type: "contacts",
550
+ contacts: a.contacts,
551
+ };
552
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
553
+ }
554
+ case "send_reaction_message": {
555
+ const body = {
556
+ messaging_product: "whatsapp",
557
+ recipient_type: "individual",
558
+ to: a.to,
559
+ type: "reaction",
560
+ reaction: {
561
+ message_id: a.message_id,
562
+ emoji: a.emoji,
563
+ },
564
+ };
565
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
566
+ }
567
+ case "send_typing_indicator": {
568
+ const body = {
569
+ messaging_product: "whatsapp",
570
+ status: "read",
571
+ message_id: a.message_id,
572
+ typing_indicator: { type: "text" },
573
+ };
574
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
575
+ }
329
576
  case "mark_message_as_read": {
330
577
  const body = {
331
578
  messaging_product: "whatsapp",
@@ -373,6 +620,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
373
620
  body.allow_category_change = a.allow_category_change;
374
621
  return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${BUSINESS_ACCOUNT_ID}/message_templates`, body), null, 2) }] };
375
622
  }
623
+ case "delete_template": {
624
+ const q = new URLSearchParams();
625
+ q.set("name", String(a.name));
626
+ if (a.hsm_id)
627
+ q.set("hsm_id", String(a.hsm_id));
628
+ const path = `/${BUSINESS_ACCOUNT_ID}/message_templates?${q.toString()}`;
629
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("DELETE", path), null, 2) }] };
630
+ }
631
+ case "get_business_profile": {
632
+ const fields = a.fields ? String(a.fields) : "about,address,description,email,profile_picture_url,websites,vertical";
633
+ const path = `/${PHONE_NUMBER_ID}/whatsapp_business_profile?fields=${encodeURIComponent(fields)}`;
634
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("GET", path), null, 2) }] };
635
+ }
636
+ case "update_business_profile": {
637
+ const body = { messaging_product: "whatsapp" };
638
+ for (const k of ["about", "address", "description", "email", "vertical", "websites"]) {
639
+ if (a[k] !== undefined)
640
+ body[k] = a[k];
641
+ }
642
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/whatsapp_business_profile`, body), null, 2) }] };
643
+ }
644
+ case "list_phone_numbers": {
645
+ const q = new URLSearchParams();
646
+ if (a.limit !== undefined)
647
+ q.set("limit", String(a.limit));
648
+ const qs = q.toString();
649
+ const path = `/${BUSINESS_ACCOUNT_ID}/phone_numbers${qs ? `?${qs}` : ""}`;
650
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("GET", path), null, 2) }] };
651
+ }
652
+ case "request_verification_code": {
653
+ const body = {
654
+ code_method: a.code_method,
655
+ language: a.language,
656
+ };
657
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/request_code`, body), null, 2) }] };
658
+ }
659
+ case "verify_code": {
660
+ const body = { code: a.code };
661
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/verify_code`, body), null, 2) }] };
662
+ }
376
663
  default:
377
664
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
378
665
  }
@@ -399,7 +686,7 @@ async function main() {
399
686
  const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
400
687
  t.onclose = () => { if (t.sessionId)
401
688
  transports.delete(t.sessionId); };
402
- const s = new Server({ name: "mcp-whatsapp-cloud", version: "0.1.0" }, { capabilities: { tools: {} } });
689
+ const s = new Server({ name: "mcp-whatsapp-cloud", version: "0.2.0" }, { capabilities: { tools: {} } });
403
690
  server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
404
691
  server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
405
692
  await s.connect(t);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codespar/mcp-whatsapp-cloud",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for WhatsApp Cloud API (Meta direct) — official Graph API integration for messages, media, and templates",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/communication/whatsapp-cloud"
9
9
  },
10
- "version": "0.1.0",
10
+ "version": "0.2.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@codespar/mcp-whatsapp-cloud",
15
- "version": "0.1.0",
15
+ "version": "0.2.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
package/src/index.ts CHANGED
@@ -8,18 +8,29 @@
8
8
  * all sit on top of this. Target: merchants with an approved WhatsApp Business
9
9
  * Account (WABA) who want Meta-direct pricing and full control.
10
10
  *
11
- * Tools (11):
11
+ * Tools (22):
12
12
  * send_text_message — simple text message
13
13
  * send_template_message — approved template (required for 24h+ business-initiated)
14
14
  * send_media_message — image, video, document, or audio
15
- * send_interactive_message — buttons or list
15
+ * send_interactive_message — buttons or list (generic)
16
+ * send_interactive_cta_url — interactive CTA URL button
17
+ * send_interactive_flow — WhatsApp Flows message
16
18
  * send_location_message — latitude/longitude pin
19
+ * send_contacts_message — vCard-style contact cards
20
+ * send_reaction_message — emoji reaction to an inbound message
21
+ * send_typing_indicator — show typing indicator on a received message
17
22
  * mark_message_as_read — mark an incoming message as read
18
23
  * upload_media — upload a file and get a media_id (multipart)
19
24
  * retrieve_media_url — resolve a media_id to a downloadable URL
20
25
  * delete_media — delete an uploaded media asset
21
26
  * list_templates — list templates on the WABA
22
27
  * create_template — submit a new template for Meta review
28
+ * delete_template — delete a template by name
29
+ * get_business_profile — read business profile on the phone number
30
+ * update_business_profile — edit business profile fields
31
+ * list_phone_numbers — list phone numbers on the WABA
32
+ * request_verification_code — request SMS/voice code to verify a phone number
33
+ * verify_code — submit the received verification code
23
34
  *
24
35
  * Authentication
25
36
  * Bearer token (permanent system-user access token).
@@ -103,7 +114,7 @@ async function whatsappRequest(
103
114
  }
104
115
 
105
116
  const server = new Server(
106
- { name: "mcp-whatsapp-cloud", version: "0.1.0" },
117
+ { name: "mcp-whatsapp-cloud", version: "0.2.0" },
107
118
  { capabilities: { tools: {} } }
108
119
  );
109
120
 
@@ -171,6 +182,42 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
171
182
  required: ["to", "interactive"],
172
183
  },
173
184
  },
185
+ {
186
+ name: "send_interactive_cta_url",
187
+ description: "Send an interactive message with a single CTA URL button. Opens the URL when the recipient taps it. Available without template approval inside the 24h window.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
192
+ body_text: { type: "string", description: "Main body text" },
193
+ button_text: { type: "string", description: "Label shown on the CTA button (max 20 chars)" },
194
+ url: { type: "string", description: "HTTPS URL the button opens" },
195
+ header_text: { type: "string", description: "Optional header text" },
196
+ footer_text: { type: "string", description: "Optional footer text" },
197
+ },
198
+ required: ["to", "body_text", "button_text", "url"],
199
+ },
200
+ },
201
+ {
202
+ name: "send_interactive_flow",
203
+ description: "Send a WhatsApp Flow message. Flows are Meta's structured UI experiences (forms, appointment booking, etc.) rendered inside WhatsApp.",
204
+ inputSchema: {
205
+ type: "object",
206
+ properties: {
207
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
208
+ flow_id: { type: "string", description: "ID of the approved Flow" },
209
+ flow_token: { type: "string", description: "Opaque token your server correlates with this send" },
210
+ flow_cta: { type: "string", description: "Label on the button that opens the flow (max 20 chars)" },
211
+ body_text: { type: "string", description: "Body text above the CTA" },
212
+ header_text: { type: "string", description: "Optional header text" },
213
+ footer_text: { type: "string", description: "Optional footer text" },
214
+ flow_action: { type: "string", enum: ["navigate", "data_exchange"], description: "Flow action type (default navigate)" },
215
+ flow_action_payload: { type: "object", description: "Optional action payload: { screen, data }. Required for navigate." },
216
+ mode: { type: "string", enum: ["draft", "published"], description: "Flow mode. Use draft while testing." },
217
+ },
218
+ required: ["to", "flow_id", "flow_token", "flow_cta", "body_text"],
219
+ },
220
+ },
174
221
  {
175
222
  name: "send_location_message",
176
223
  description: "Send a location pin with latitude/longitude and optional name/address.",
@@ -186,6 +233,46 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
186
233
  required: ["to", "latitude", "longitude"],
187
234
  },
188
235
  },
236
+ {
237
+ name: "send_contacts_message",
238
+ description: "Send one or more contact cards (vCard-like). Each contact includes name and at least one of phones, emails, addresses, urls, or org.",
239
+ inputSchema: {
240
+ type: "object",
241
+ properties: {
242
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
243
+ contacts: {
244
+ type: "array",
245
+ description: "Array of contact objects per Cloud API spec (name, phones, emails, addresses, org, urls, birthday).",
246
+ items: { type: "object" },
247
+ },
248
+ },
249
+ required: ["to", "contacts"],
250
+ },
251
+ },
252
+ {
253
+ name: "send_reaction_message",
254
+ description: "Send an emoji reaction on a previously received/sent message. Pass empty string for `emoji` to clear a reaction.",
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ to: { type: "string", description: "Recipient phone number in E.164 without +" },
259
+ message_id: { type: "string", description: "wamid of the message being reacted to" },
260
+ emoji: { type: "string", description: "Single unicode emoji. Empty string clears the reaction." },
261
+ },
262
+ required: ["to", "message_id", "emoji"],
263
+ },
264
+ },
265
+ {
266
+ name: "send_typing_indicator",
267
+ description: "Show a typing indicator on a received message. Also marks the message as read. Indicator auto-clears after ~25s or when you reply.",
268
+ inputSchema: {
269
+ type: "object",
270
+ properties: {
271
+ message_id: { type: "string", description: "wamid of the inbound message to show the indicator on" },
272
+ },
273
+ required: ["message_id"],
274
+ },
275
+ },
189
276
  {
190
277
  name: "mark_message_as_read",
191
278
  description: "Mark an incoming message as read so the sender sees the blue double-check. Uses the wamid from the inbound webhook.",
@@ -263,6 +350,76 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
263
350
  required: ["name", "language", "category", "components"],
264
351
  },
265
352
  },
353
+ {
354
+ name: "delete_template",
355
+ description: "Delete a message template from the WABA by name. Optionally scope by hsm_id when two templates share a name across languages.",
356
+ inputSchema: {
357
+ type: "object",
358
+ properties: {
359
+ name: { type: "string", description: "Template name to delete" },
360
+ hsm_id: { type: "string", description: "Optional template id (when multiple languages share a name and only one should be deleted)" },
361
+ },
362
+ required: ["name"],
363
+ },
364
+ },
365
+ {
366
+ name: "get_business_profile",
367
+ description: "Read the WhatsApp business profile (about, description, email, websites, vertical, address) for the configured phone number.",
368
+ inputSchema: {
369
+ type: "object",
370
+ properties: {
371
+ fields: { type: "string", description: "Comma-separated field list. Default: about,address,description,email,profile_picture_url,websites,vertical" },
372
+ },
373
+ },
374
+ },
375
+ {
376
+ name: "update_business_profile",
377
+ description: "Update the business profile on the configured phone number. Supply only the fields you want to change.",
378
+ inputSchema: {
379
+ type: "object",
380
+ properties: {
381
+ about: { type: "string", description: "About text (max 139 chars)" },
382
+ address: { type: "string", description: "Physical address" },
383
+ description: { type: "string", description: "Business description" },
384
+ email: { type: "string", description: "Contact email" },
385
+ vertical: { type: "string", description: "Business vertical (e.g. RETAIL, EDU, HEALTH, FINANCE, OTHER)" },
386
+ websites: { type: "array", description: "Up to two URLs", items: { type: "string" } },
387
+ },
388
+ },
389
+ },
390
+ {
391
+ name: "list_phone_numbers",
392
+ description: "List all phone numbers registered under the WhatsApp Business Account, including display name, quality rating, and verification status.",
393
+ inputSchema: {
394
+ type: "object",
395
+ properties: {
396
+ limit: { type: "number", description: "Max results per page (default 25)" },
397
+ },
398
+ },
399
+ },
400
+ {
401
+ name: "request_verification_code",
402
+ description: "Request Meta to send a verification code to the configured phone number via SMS or voice. Use before verify_code.",
403
+ inputSchema: {
404
+ type: "object",
405
+ properties: {
406
+ code_method: { type: "string", enum: ["SMS", "VOICE"], description: "Delivery method for the verification code" },
407
+ language: { type: "string", description: "BCP-47 language tag for the voice/SMS copy (e.g. en_US, pt_BR)" },
408
+ },
409
+ required: ["code_method", "language"],
410
+ },
411
+ },
412
+ {
413
+ name: "verify_code",
414
+ description: "Submit the verification code received via SMS/voice after request_verification_code. Completes registration.",
415
+ inputSchema: {
416
+ type: "object",
417
+ properties: {
418
+ code: { type: "string", description: "Verification code received (digits only, strip dashes)" },
419
+ },
420
+ required: ["code"],
421
+ },
422
+ },
266
423
  ],
267
424
  }));
268
425
 
@@ -325,6 +482,58 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
325
482
  };
326
483
  return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
327
484
  }
485
+ case "send_interactive_cta_url": {
486
+ const interactive: Record<string, unknown> = {
487
+ type: "cta_url",
488
+ body: { text: a.body_text },
489
+ action: {
490
+ name: "cta_url",
491
+ parameters: {
492
+ display_text: a.button_text,
493
+ url: a.url,
494
+ },
495
+ },
496
+ };
497
+ if (a.header_text) interactive.header = { type: "text", text: a.header_text };
498
+ if (a.footer_text) interactive.footer = { text: a.footer_text };
499
+ const body = {
500
+ messaging_product: "whatsapp",
501
+ recipient_type: "individual",
502
+ to: a.to,
503
+ type: "interactive",
504
+ interactive,
505
+ };
506
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
507
+ }
508
+ case "send_interactive_flow": {
509
+ const parameters: Record<string, unknown> = {
510
+ flow_message_version: "3",
511
+ flow_token: a.flow_token,
512
+ flow_id: a.flow_id,
513
+ flow_cta: a.flow_cta,
514
+ flow_action: a.flow_action ?? "navigate",
515
+ };
516
+ if (a.mode) parameters.mode = a.mode;
517
+ if (a.flow_action_payload) parameters.flow_action_payload = a.flow_action_payload;
518
+ const interactive: Record<string, unknown> = {
519
+ type: "flow",
520
+ body: { text: a.body_text },
521
+ action: {
522
+ name: "flow",
523
+ parameters,
524
+ },
525
+ };
526
+ if (a.header_text) interactive.header = { type: "text", text: a.header_text };
527
+ if (a.footer_text) interactive.footer = { text: a.footer_text };
528
+ const body = {
529
+ messaging_product: "whatsapp",
530
+ recipient_type: "individual",
531
+ to: a.to,
532
+ type: "interactive",
533
+ interactive,
534
+ };
535
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
536
+ }
328
537
  case "send_location_message": {
329
538
  const location: Record<string, unknown> = {
330
539
  latitude: a.latitude,
@@ -341,6 +550,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
341
550
  };
342
551
  return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
343
552
  }
553
+ case "send_contacts_message": {
554
+ const body = {
555
+ messaging_product: "whatsapp",
556
+ recipient_type: "individual",
557
+ to: a.to,
558
+ type: "contacts",
559
+ contacts: a.contacts,
560
+ };
561
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
562
+ }
563
+ case "send_reaction_message": {
564
+ const body = {
565
+ messaging_product: "whatsapp",
566
+ recipient_type: "individual",
567
+ to: a.to,
568
+ type: "reaction",
569
+ reaction: {
570
+ message_id: a.message_id,
571
+ emoji: a.emoji,
572
+ },
573
+ };
574
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
575
+ }
576
+ case "send_typing_indicator": {
577
+ const body = {
578
+ messaging_product: "whatsapp",
579
+ status: "read",
580
+ message_id: a.message_id,
581
+ typing_indicator: { type: "text" },
582
+ };
583
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/messages`, body), null, 2) }] };
584
+ }
344
585
  case "mark_message_as_read": {
345
586
  const body = {
346
587
  messaging_product: "whatsapp",
@@ -384,6 +625,43 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
384
625
  if (a.allow_category_change !== undefined) body.allow_category_change = a.allow_category_change;
385
626
  return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${BUSINESS_ACCOUNT_ID}/message_templates`, body), null, 2) }] };
386
627
  }
628
+ case "delete_template": {
629
+ const q = new URLSearchParams();
630
+ q.set("name", String(a.name));
631
+ if (a.hsm_id) q.set("hsm_id", String(a.hsm_id));
632
+ const path = `/${BUSINESS_ACCOUNT_ID}/message_templates?${q.toString()}`;
633
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("DELETE", path), null, 2) }] };
634
+ }
635
+ case "get_business_profile": {
636
+ const fields = a.fields ? String(a.fields) : "about,address,description,email,profile_picture_url,websites,vertical";
637
+ const path = `/${PHONE_NUMBER_ID}/whatsapp_business_profile?fields=${encodeURIComponent(fields)}`;
638
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("GET", path), null, 2) }] };
639
+ }
640
+ case "update_business_profile": {
641
+ const body: Record<string, unknown> = { messaging_product: "whatsapp" };
642
+ for (const k of ["about", "address", "description", "email", "vertical", "websites"]) {
643
+ if (a[k] !== undefined) body[k] = a[k];
644
+ }
645
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/whatsapp_business_profile`, body), null, 2) }] };
646
+ }
647
+ case "list_phone_numbers": {
648
+ const q = new URLSearchParams();
649
+ if (a.limit !== undefined) q.set("limit", String(a.limit));
650
+ const qs = q.toString();
651
+ const path = `/${BUSINESS_ACCOUNT_ID}/phone_numbers${qs ? `?${qs}` : ""}`;
652
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("GET", path), null, 2) }] };
653
+ }
654
+ case "request_verification_code": {
655
+ const body = {
656
+ code_method: a.code_method,
657
+ language: a.language,
658
+ };
659
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/request_code`, body), null, 2) }] };
660
+ }
661
+ case "verify_code": {
662
+ const body = { code: a.code };
663
+ return { content: [{ type: "text", text: JSON.stringify(await whatsappRequest("POST", `/${PHONE_NUMBER_ID}/verify_code`, body), null, 2) }] };
664
+ }
387
665
  default:
388
666
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
389
667
  }
@@ -406,7 +684,7 @@ async function main() {
406
684
  if (!sid && isInitializeRequest(req.body)) {
407
685
  const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
408
686
  t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
409
- const s = new Server({ name: "mcp-whatsapp-cloud", version: "0.1.0" }, { capabilities: { tools: {} } });
687
+ const s = new Server({ name: "mcp-whatsapp-cloud", version: "0.2.0" }, { capabilities: { tools: {} } });
410
688
  (server as unknown as { _requestHandlers: Map<unknown, unknown> })._requestHandlers.forEach((v, k) => (s as unknown as { _requestHandlers: Map<unknown, unknown> })._requestHandlers.set(k, v));
411
689
  (server as unknown as { _notificationHandlers?: Map<unknown, unknown> })._notificationHandlers?.forEach((v, k) => (s as unknown as { _notificationHandlers: Map<unknown, unknown> })._notificationHandlers.set(k, v));
412
690
  await s.connect(t);