@wabery/cli 0.15.2 → 0.16.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/api-client.d.ts +1 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +5 -4
- package/dist/api-client.js.map +1 -1
- package/dist/cli.js +174 -9
- package/dist/cli.js.map +1 -1
- package/dist/config-example.d.ts +1 -1
- package/dist/config-example.d.ts.map +1 -1
- package/dist/local-config.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +394 -14
- package/dist/mcp-server.js.map +1 -1
- package/package.json +2 -2
package/dist/mcp-server.js
CHANGED
|
@@ -5,6 +5,76 @@ import { createExampleConfig } from "./config-example.js";
|
|
|
5
5
|
import { checkWaberyConnection } from "./diagnostics.js";
|
|
6
6
|
const configSchema = z.record(z.string(), z.unknown());
|
|
7
7
|
const metadataSchema = z.record(z.string(), z.unknown()).optional();
|
|
8
|
+
const groupIdSchema = z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1)
|
|
11
|
+
.describe("Opaque WhatsApp group_id; never use a phone number here.");
|
|
12
|
+
const groupJoinRequestIdsSchema = z
|
|
13
|
+
.array(z.string().min(1))
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(8)
|
|
16
|
+
.describe("Bulk join-request IDs from wabery_list_group_join_requests.");
|
|
17
|
+
const groupParticipantSchema = z.object({
|
|
18
|
+
user: z
|
|
19
|
+
.string()
|
|
20
|
+
.min(1)
|
|
21
|
+
.describe("WhatsApp user ID, encoded in Meta's required { user } shape."),
|
|
22
|
+
});
|
|
23
|
+
const groupMessageBodySchema = z
|
|
24
|
+
.object({
|
|
25
|
+
text: z.string().min(1).max(4096).optional(),
|
|
26
|
+
media: z
|
|
27
|
+
.object({
|
|
28
|
+
type: z.enum(["image", "document", "video", "audio"]),
|
|
29
|
+
id: z.string().min(1).optional(),
|
|
30
|
+
link: z.string().url().optional(),
|
|
31
|
+
caption: z.string().max(1024).optional(),
|
|
32
|
+
filename: z.string().max(255).optional(),
|
|
33
|
+
})
|
|
34
|
+
.refine((media) => Boolean(media.id) !== Boolean(media.link), {
|
|
35
|
+
message: "Provide exactly one of media.id or media.link",
|
|
36
|
+
})
|
|
37
|
+
.optional(),
|
|
38
|
+
sticker: z
|
|
39
|
+
.object({
|
|
40
|
+
id: z.string().min(1).optional(),
|
|
41
|
+
link: z.string().url().optional(),
|
|
42
|
+
})
|
|
43
|
+
.refine((sticker) => Boolean(sticker.id) !== Boolean(sticker.link), {
|
|
44
|
+
message: "Provide exactly one of sticker.id or sticker.link",
|
|
45
|
+
})
|
|
46
|
+
.optional(),
|
|
47
|
+
template: z
|
|
48
|
+
.object({
|
|
49
|
+
id: z.string().min(1).optional(),
|
|
50
|
+
name: z.string().min(1).optional(),
|
|
51
|
+
language: z.string().min(2).max(10).optional(),
|
|
52
|
+
components: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
53
|
+
})
|
|
54
|
+
.refine((template) => Boolean(template.id) || Boolean(template.name && template.language), {
|
|
55
|
+
message: "Provide template.id or both template.name and template.language",
|
|
56
|
+
})
|
|
57
|
+
.optional(),
|
|
58
|
+
pin: z
|
|
59
|
+
.discriminatedUnion("type", [
|
|
60
|
+
z.object({
|
|
61
|
+
type: z.literal("pin"),
|
|
62
|
+
message_id: z.string().min(1),
|
|
63
|
+
expiration_days: z.number().int().min(1).max(30),
|
|
64
|
+
}),
|
|
65
|
+
z.object({
|
|
66
|
+
type: z.literal("unpin"),
|
|
67
|
+
message_id: z.string().min(1),
|
|
68
|
+
}),
|
|
69
|
+
])
|
|
70
|
+
.optional(),
|
|
71
|
+
reply_to: z.string().min(1).max(200).optional(),
|
|
72
|
+
idempotency_key: z.string().min(1).max(200),
|
|
73
|
+
})
|
|
74
|
+
.strict()
|
|
75
|
+
.refine((body) => [body.text, body.media, body.sticker, body.template, body.pin].filter(Boolean).length === 1, {
|
|
76
|
+
message: "Provide exactly one of text, media, sticker, template, or pin",
|
|
77
|
+
});
|
|
8
78
|
const readinessRequirementSchema = z.enum([
|
|
9
79
|
"sandbox_test",
|
|
10
80
|
"production_inbound",
|
|
@@ -80,22 +150,21 @@ const broadcastAudienceFilterSchema = z.object({
|
|
|
80
150
|
created_after: z.string().datetime().optional(),
|
|
81
151
|
created_before: z.string().datetime().optional(),
|
|
82
152
|
});
|
|
153
|
+
const MESSAGE_MUTATION_GUIDANCE = 'Inbound WhatsApp messages can use outer type "unsupported" when Cloud API does not provide the original body. Inspect unsupported.type: "revoke" and "edit" are message mutations, not unavailable media; never answer them or call wabery_send_message in response. Other unsupported subtypes are unavailable content and may be handled as such.';
|
|
83
154
|
const toolOutputSchema = {
|
|
84
155
|
result: z
|
|
85
156
|
.record(z.string(), z.unknown())
|
|
86
|
-
.describe(
|
|
157
|
+
.describe(`The structured Wabery API result object. Its operation-specific fields are described by the tool and Wabery API documentation. ${MESSAGE_MUTATION_GUIDANCE}`),
|
|
87
158
|
};
|
|
88
159
|
// Claude Code caps MCP server instructions at 2KB and silently truncates from
|
|
89
160
|
// the end (worse with multiple servers). Keep this text + WRITE_NOTE under
|
|
90
161
|
// 1900 UTF-8 bytes as a buffer, and put invariants first. Per-tool playbooks
|
|
91
162
|
// belong on tool descriptions, not in this handshake.
|
|
92
163
|
const SERVER_INSTRUCTIONS = `Wabery is a developer platform for building and operating WhatsApp AI agents. Start discovery with wabery_list_projects, select the project, then wabery_get_project_readiness; use channel readiness only for phone-number/provider detail.
|
|
93
|
-
Use Wabery for WhatsApp contacts, opted-in messaging, conversations and message history, approved templates, Flows, broadcasts, routing/human handoff, signed webhooks, hosted functions, and
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Broadcasts: approved templates, exactly one audience source, review recipients/exclusions, then confirm.
|
|
98
|
-
Use routing and thread-control tools for automation ownership and human handoff. Hosted function test/invoke is side-effecting. Obtain explicit user confirmation before any external or irreversible action (send, publish, delete, rotate secrets, update providers, run functions). After send, call wabery_get_message, wabery_get_dispatch, or wabery_get_broadcast for provider status; acceptance is not delivery.`;
|
|
164
|
+
Use Wabery for WhatsApp contacts, opted-in messaging, conversations and message history, approved templates, Flows, broadcasts, Groups, routing/human handoff, signed webhooks, hosted functions, and Business Agent configuration.
|
|
165
|
+
Sandbox is for controlled Draft tests; production inbound, proactive templates, and broadcasts need an eligible dedicated channel. Enroll only controlled or opted-in contacts and retain contact_id/channel_id. Live hosted-agent traffic is read-only; only Draft is editable. Config apply, validate, and preview never publish; publish in the Wabery AI builder.
|
|
166
|
+
Before replying, inspect message history. For inbound type unsupported, inspect unsupported.type: revoke/edit are mutations and must not be answered; other subtypes are unavailable content. Draft and validate Flows, then confirm publish or send separately. Broadcasts require approved templates, one reviewed audience source, and confirmation.
|
|
167
|
+
Use routing and thread-control tools for human handoff. Groups require an Official Business Account and Cloud API number; max 8 participants, 10k groups per number, one Cloud API business per group. Group commerce, interactive, calls, ephemeral, view-once, auth, edit, and delete messages are unsupported. Obtain explicit user confirmation before external or irreversible actions, including group sends/mutations, send, publish, delete, rotate, provider updates, and function runs. After a send, inspect provider status with wabery_get_message, wabery_get_dispatch, or wabery_get_broadcast; acceptance is not delivery.`;
|
|
99
168
|
// The write gate differs by transport, and the model has to be told the truth
|
|
100
169
|
// about the session it is actually in: a hosted OAuth client (ChatGPT, Claude)
|
|
101
170
|
// cannot set an env var, so advertising the env gate there makes it refuse to
|
|
@@ -212,6 +281,12 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
212
281
|
outputSchema: toolOutputSchema,
|
|
213
282
|
...config,
|
|
214
283
|
}, callback);
|
|
284
|
+
const groupPath = (channelId, groupId) => {
|
|
285
|
+
const base = `/channels/${encodeURIComponent(channelId)}/whatsapp-groups`;
|
|
286
|
+
return groupId === undefined
|
|
287
|
+
? base
|
|
288
|
+
: `${base}/${encodeURIComponent(groupId)}`;
|
|
289
|
+
};
|
|
215
290
|
const requireWrite = async (action, token, expectedToken) => {
|
|
216
291
|
const modeBlocked = checkWriteMode(action, undefined, undefined, writePolicy);
|
|
217
292
|
if (modeBlocked)
|
|
@@ -931,13 +1006,17 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
931
1006
|
}, async ({ project_id }) => jsonResult(await client.get(`/projects/${encodeURIComponent(project_id)}`)));
|
|
932
1007
|
registerTool("wabery_update_project", {
|
|
933
1008
|
title: "Update a Wabery project",
|
|
934
|
-
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.",
|
|
1009
|
+
description: "Use this when you need to overwrite project name, description, routing_mode, require_opt_in, group management webhook opt-in, or signed-webhook settings for a project_id from wabery_list_projects. Prefer wabery_apply_config for reviewed project-wide declarative changes.",
|
|
935
1010
|
inputSchema: {
|
|
936
1011
|
project_id: z.string().min(1),
|
|
937
1012
|
name: z.string().min(1).max(100).optional(),
|
|
938
1013
|
description: z.string().max(500).nullable().optional(),
|
|
939
1014
|
routing_mode: z.enum(["NONE", "FLOWS", "EXTERNAL"]).optional(),
|
|
940
1015
|
require_opt_in: z.boolean().optional(),
|
|
1016
|
+
group_webhook_events_enabled: z
|
|
1017
|
+
.boolean()
|
|
1018
|
+
.optional()
|
|
1019
|
+
.describe("Explicitly opt into the four new WhatsApp group management event names; defaults false."),
|
|
941
1020
|
webhook_url: z.string().url().nullable().optional(),
|
|
942
1021
|
webhook_signing_enabled: z.boolean().optional(),
|
|
943
1022
|
webhook_secret: z.string().min(16).max(256).optional(),
|
|
@@ -1467,6 +1546,10 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1467
1546
|
inputSchema: {
|
|
1468
1547
|
contact_id: z.string().optional(),
|
|
1469
1548
|
active: z.boolean().optional(),
|
|
1549
|
+
recipient_type: z
|
|
1550
|
+
.enum(["individual", "group", "all"])
|
|
1551
|
+
.optional()
|
|
1552
|
+
.describe("Defaults to individual for backward compatibility; use group or all explicitly."),
|
|
1470
1553
|
limit: z.number().int().min(1).max(100).optional(),
|
|
1471
1554
|
starting_after: z.string().optional(),
|
|
1472
1555
|
},
|
|
@@ -1485,6 +1568,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1485
1568
|
description: "Use this when you need one organization-scoped WhatsApp conversation by conversation_id from wabery_list_conversations.",
|
|
1486
1569
|
inputSchema: {
|
|
1487
1570
|
conversation_id: z.string().min(1),
|
|
1571
|
+
recipient_type: z.enum(["individual", "group"]).optional(),
|
|
1488
1572
|
},
|
|
1489
1573
|
annotations: {
|
|
1490
1574
|
readOnlyHint: true,
|
|
@@ -1492,12 +1576,13 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1492
1576
|
idempotentHint: true,
|
|
1493
1577
|
openWorldHint: false,
|
|
1494
1578
|
},
|
|
1495
|
-
}, async ({ conversation_id }) => jsonResult(await client.get(`/conversations/${conversation_id}
|
|
1579
|
+
}, async ({ conversation_id, ...query }) => jsonResult(await client.get(`/conversations/${conversation_id}`, query)));
|
|
1496
1580
|
registerTool("wabery_list_conversation_messages", {
|
|
1497
1581
|
title: "List Wabery conversation messages",
|
|
1498
|
-
description:
|
|
1582
|
+
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. ${MESSAGE_MUTATION_GUIDANCE} Direct type revoke/edit are also official coexistence delete/edit events; inspect original_message_id when present.`,
|
|
1499
1583
|
inputSchema: {
|
|
1500
1584
|
conversation_id: z.string().min(1),
|
|
1585
|
+
recipient_type: z.enum(["individual", "group"]).optional(),
|
|
1501
1586
|
limit: z.number().int().min(1).max(100).optional(),
|
|
1502
1587
|
starting_after: z.string().optional(),
|
|
1503
1588
|
order: z.enum(["asc", "desc"]).optional(),
|
|
@@ -1509,13 +1594,298 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1509
1594
|
openWorldHint: false,
|
|
1510
1595
|
},
|
|
1511
1596
|
}, async ({ conversation_id, ...query }) => jsonResult(await client.get(`/conversations/${conversation_id}/messages`, query)));
|
|
1597
|
+
registerTool("wabery_list_groups", {
|
|
1598
|
+
title: "List WhatsApp groups",
|
|
1599
|
+
description: "Use this when you need organization-scoped WhatsApp group_id values. A group_id is an opaque Meta ID, not a phone number or the messages.send to field. Groups require an eligible Official Business Account Cloud API channel. limit defaults to 25 and accepts 1–1024; starting_after/ending_before (or after/before) are opaque cursors.",
|
|
1600
|
+
inputSchema: {
|
|
1601
|
+
channel_id: z.string().min(1),
|
|
1602
|
+
limit: z.number().int().min(1).max(1024).optional(),
|
|
1603
|
+
starting_after: z.string().optional(),
|
|
1604
|
+
ending_before: z.string().optional(),
|
|
1605
|
+
after: z.string().optional(),
|
|
1606
|
+
before: z.string().optional(),
|
|
1607
|
+
},
|
|
1608
|
+
annotations: {
|
|
1609
|
+
readOnlyHint: true,
|
|
1610
|
+
destructiveHint: false,
|
|
1611
|
+
idempotentHint: true,
|
|
1612
|
+
openWorldHint: false,
|
|
1613
|
+
},
|
|
1614
|
+
}, async ({ channel_id, ...query }) => jsonResult(await client.get(groupPath(channel_id), query)));
|
|
1615
|
+
registerTool("wabery_get_group", {
|
|
1616
|
+
title: "Get WhatsApp group",
|
|
1617
|
+
description: "Use this when you need group settings, suspension state, participant count, or participant data for one opaque group_id from wabery_list_groups. The participant count excludes the business account.",
|
|
1618
|
+
inputSchema: {
|
|
1619
|
+
channel_id: z.string().min(1),
|
|
1620
|
+
group_id: groupIdSchema,
|
|
1621
|
+
},
|
|
1622
|
+
annotations: {
|
|
1623
|
+
readOnlyHint: true,
|
|
1624
|
+
destructiveHint: false,
|
|
1625
|
+
idempotentHint: true,
|
|
1626
|
+
openWorldHint: false,
|
|
1627
|
+
},
|
|
1628
|
+
}, async ({ channel_id, group_id }) => jsonResult(await client.get(groupPath(channel_id, group_id))));
|
|
1629
|
+
registerTool("wabery_create_group", {
|
|
1630
|
+
title: "Create WhatsApp group",
|
|
1631
|
+
description: "Use this when you need to create a WhatsApp group on an eligible Official Business Account Cloud API channel. Supply a caller-generated stable idempotency_key and reuse it for every retry. Meta limits groups to 8 participants and 10,000 groups per business number; explicit confirmation is required.",
|
|
1632
|
+
inputSchema: {
|
|
1633
|
+
channel_id: z.string().min(1),
|
|
1634
|
+
subject: z.string().min(1).max(128),
|
|
1635
|
+
description: z.string().max(2048).optional(),
|
|
1636
|
+
join_approval_mode: z
|
|
1637
|
+
.enum(["auto_approve", "approval_required"])
|
|
1638
|
+
.optional(),
|
|
1639
|
+
idempotency_key: z
|
|
1640
|
+
.string()
|
|
1641
|
+
.trim()
|
|
1642
|
+
.min(1)
|
|
1643
|
+
.max(200)
|
|
1644
|
+
.describe("Stable caller-generated key reused for every retry of this group creation."),
|
|
1645
|
+
confirmation_token: z.string().optional(),
|
|
1646
|
+
},
|
|
1647
|
+
annotations: {
|
|
1648
|
+
readOnlyHint: false,
|
|
1649
|
+
destructiveHint: true,
|
|
1650
|
+
idempotentHint: true,
|
|
1651
|
+
openWorldHint: true,
|
|
1652
|
+
},
|
|
1653
|
+
}, async ({ channel_id, confirmation_token, idempotency_key, ...body }) => {
|
|
1654
|
+
const blocked = await requireWrite("wabery_create_group", confirmation_token, "create_group");
|
|
1655
|
+
if (blocked)
|
|
1656
|
+
return blocked;
|
|
1657
|
+
return apiResult(client.post(groupPath(channel_id), body, idempotency_key));
|
|
1658
|
+
});
|
|
1659
|
+
registerTool("wabery_update_group_settings", {
|
|
1660
|
+
title: "Update WhatsApp group settings",
|
|
1661
|
+
description: "Use this when you need to change a group's subject or description. Meta reports the final result asynchronously through group.settings; explicit confirmation is required.",
|
|
1662
|
+
inputSchema: {
|
|
1663
|
+
channel_id: z.string().min(1),
|
|
1664
|
+
group_id: groupIdSchema,
|
|
1665
|
+
subject: z.string().min(1).max(128).optional(),
|
|
1666
|
+
description: z.string().max(2048).optional(),
|
|
1667
|
+
confirmation_token: z.string().optional(),
|
|
1668
|
+
},
|
|
1669
|
+
annotations: {
|
|
1670
|
+
readOnlyHint: false,
|
|
1671
|
+
destructiveHint: true,
|
|
1672
|
+
idempotentHint: true,
|
|
1673
|
+
openWorldHint: true,
|
|
1674
|
+
},
|
|
1675
|
+
}, async ({ channel_id, group_id, confirmation_token, ...body }) => {
|
|
1676
|
+
const blocked = await requireWrite("wabery_update_group_settings", confirmation_token, "update_group_settings");
|
|
1677
|
+
if (blocked)
|
|
1678
|
+
return blocked;
|
|
1679
|
+
return apiResult(client.post(groupPath(channel_id, group_id), body));
|
|
1680
|
+
});
|
|
1681
|
+
registerTool("wabery_update_group_profile_picture", {
|
|
1682
|
+
title: "Update WhatsApp group profile picture",
|
|
1683
|
+
description: "Use this when you need to upload a JPEG group profile picture as base64. Meta validates square aspect ratio and minimum dimensions and reports the final result asynchronously through group.settings. Explicit confirmation is required.",
|
|
1684
|
+
inputSchema: {
|
|
1685
|
+
channel_id: z.string().min(1),
|
|
1686
|
+
group_id: groupIdSchema,
|
|
1687
|
+
jpeg_base64: z.string().min(4).max(8_000_000),
|
|
1688
|
+
filename: z.string().min(1).max(240).optional(),
|
|
1689
|
+
subject: z.string().min(1).max(128).optional(),
|
|
1690
|
+
description: z.string().max(2048).optional(),
|
|
1691
|
+
confirmation_token: z.string().optional(),
|
|
1692
|
+
},
|
|
1693
|
+
annotations: {
|
|
1694
|
+
readOnlyHint: false,
|
|
1695
|
+
destructiveHint: true,
|
|
1696
|
+
idempotentHint: true,
|
|
1697
|
+
openWorldHint: true,
|
|
1698
|
+
},
|
|
1699
|
+
}, async ({ channel_id, group_id, jpeg_base64, filename, subject, description, confirmation_token, }) => {
|
|
1700
|
+
const blocked = await requireWrite("wabery_update_group_profile_picture", confirmation_token, "update_group_profile_picture");
|
|
1701
|
+
if (blocked)
|
|
1702
|
+
return blocked;
|
|
1703
|
+
const bytes = Buffer.from(jpeg_base64, "base64");
|
|
1704
|
+
if (bytes.length < 3 ||
|
|
1705
|
+
bytes[0] !== 0xff ||
|
|
1706
|
+
bytes[1] !== 0xd8 ||
|
|
1707
|
+
bytes[2] !== 0xff) {
|
|
1708
|
+
throw new Error("jpeg_base64 must decode to a JPEG file");
|
|
1709
|
+
}
|
|
1710
|
+
const form = new FormData();
|
|
1711
|
+
form.set("profile_picture_file", new Blob([bytes], { type: "image/jpeg" }), filename ?? "profile.jpg");
|
|
1712
|
+
if (subject !== undefined)
|
|
1713
|
+
form.set("subject", subject);
|
|
1714
|
+
if (description !== undefined)
|
|
1715
|
+
form.set("description", description);
|
|
1716
|
+
return apiResult(client.postForm(groupPath(channel_id, group_id), form));
|
|
1717
|
+
});
|
|
1718
|
+
registerTool("wabery_get_group_invite_link", {
|
|
1719
|
+
title: "Get WhatsApp group invite link",
|
|
1720
|
+
description: "Use this when you need the current chat.whatsapp.com invite link for a group_id. Reading the link is safe; share it only with the intended audience.",
|
|
1721
|
+
inputSchema: {
|
|
1722
|
+
channel_id: z.string().min(1),
|
|
1723
|
+
group_id: groupIdSchema,
|
|
1724
|
+
},
|
|
1725
|
+
annotations: {
|
|
1726
|
+
readOnlyHint: true,
|
|
1727
|
+
destructiveHint: false,
|
|
1728
|
+
idempotentHint: true,
|
|
1729
|
+
openWorldHint: false,
|
|
1730
|
+
},
|
|
1731
|
+
}, async ({ channel_id, group_id }) => jsonResult(await client.get(`${groupPath(channel_id, group_id)}/invite-link`)));
|
|
1732
|
+
registerTool("wabery_reset_group_invite_link", {
|
|
1733
|
+
title: "Reset WhatsApp group invite link",
|
|
1734
|
+
description: "Use this when you need Meta to invalidate the current group invite link and issue a new one. Existing links stop working; explicit confirmation is required.",
|
|
1735
|
+
inputSchema: {
|
|
1736
|
+
channel_id: z.string().min(1),
|
|
1737
|
+
group_id: groupIdSchema,
|
|
1738
|
+
confirmation_token: z.string().optional(),
|
|
1739
|
+
},
|
|
1740
|
+
annotations: {
|
|
1741
|
+
readOnlyHint: false,
|
|
1742
|
+
destructiveHint: true,
|
|
1743
|
+
idempotentHint: false,
|
|
1744
|
+
openWorldHint: true,
|
|
1745
|
+
},
|
|
1746
|
+
}, async ({ channel_id, group_id, confirmation_token }) => {
|
|
1747
|
+
const blocked = await requireWrite("wabery_reset_group_invite_link", confirmation_token, "reset_group_invite_link");
|
|
1748
|
+
if (blocked)
|
|
1749
|
+
return blocked;
|
|
1750
|
+
return apiResult(client.post(`${groupPath(channel_id, group_id)}/invite-link`));
|
|
1751
|
+
});
|
|
1752
|
+
registerTool("wabery_list_group_join_requests", {
|
|
1753
|
+
title: "List WhatsApp group join requests",
|
|
1754
|
+
description: "Use this when you need pending join-request IDs before approving or rejecting requests in bulk. limit defaults to 25 and accepts 1–1024; starting_after/ending_before (or after/before) are opaque cursors. Pass the returned opaque IDs to the corresponding mutation tool.",
|
|
1755
|
+
inputSchema: {
|
|
1756
|
+
channel_id: z.string().min(1),
|
|
1757
|
+
group_id: groupIdSchema,
|
|
1758
|
+
limit: z.number().int().min(1).max(1024).optional(),
|
|
1759
|
+
starting_after: z.string().optional(),
|
|
1760
|
+
ending_before: z.string().optional(),
|
|
1761
|
+
after: z.string().optional(),
|
|
1762
|
+
before: z.string().optional(),
|
|
1763
|
+
},
|
|
1764
|
+
annotations: {
|
|
1765
|
+
readOnlyHint: true,
|
|
1766
|
+
destructiveHint: false,
|
|
1767
|
+
idempotentHint: true,
|
|
1768
|
+
openWorldHint: false,
|
|
1769
|
+
},
|
|
1770
|
+
}, async ({ channel_id, group_id, ...query }) => jsonResult(await client.get(`${groupPath(channel_id, group_id)}/join-requests`, query)));
|
|
1771
|
+
registerTool("wabery_approve_group_join_requests", {
|
|
1772
|
+
title: "Approve WhatsApp group join requests",
|
|
1773
|
+
description: "Use this when you need to approve one or more pending group join-request IDs. This uses Meta's bulk join_requests contract and requires explicit confirmation.",
|
|
1774
|
+
inputSchema: {
|
|
1775
|
+
channel_id: z.string().min(1),
|
|
1776
|
+
group_id: groupIdSchema,
|
|
1777
|
+
join_request_ids: groupJoinRequestIdsSchema,
|
|
1778
|
+
confirmation_token: z.string().optional(),
|
|
1779
|
+
},
|
|
1780
|
+
annotations: {
|
|
1781
|
+
readOnlyHint: false,
|
|
1782
|
+
destructiveHint: true,
|
|
1783
|
+
idempotentHint: true,
|
|
1784
|
+
openWorldHint: true,
|
|
1785
|
+
},
|
|
1786
|
+
}, async ({ channel_id, group_id, join_request_ids, confirmation_token }) => {
|
|
1787
|
+
const blocked = await requireWrite("wabery_approve_group_join_requests", confirmation_token, "approve_group_join_requests");
|
|
1788
|
+
if (blocked)
|
|
1789
|
+
return blocked;
|
|
1790
|
+
return apiResult(client.post(`${groupPath(channel_id, group_id)}/join-requests`, {
|
|
1791
|
+
join_requests: join_request_ids,
|
|
1792
|
+
}));
|
|
1793
|
+
});
|
|
1794
|
+
registerTool("wabery_reject_group_join_requests", {
|
|
1795
|
+
title: "Reject WhatsApp group join requests",
|
|
1796
|
+
description: "Use this when you need to reject one or more pending group join-request IDs. This uses Meta's bulk join_requests contract and requires explicit confirmation.",
|
|
1797
|
+
inputSchema: {
|
|
1798
|
+
channel_id: z.string().min(1),
|
|
1799
|
+
group_id: groupIdSchema,
|
|
1800
|
+
join_request_ids: groupJoinRequestIdsSchema,
|
|
1801
|
+
confirmation_token: z.string().optional(),
|
|
1802
|
+
},
|
|
1803
|
+
annotations: {
|
|
1804
|
+
readOnlyHint: false,
|
|
1805
|
+
destructiveHint: true,
|
|
1806
|
+
idempotentHint: true,
|
|
1807
|
+
openWorldHint: true,
|
|
1808
|
+
},
|
|
1809
|
+
}, async ({ channel_id, group_id, join_request_ids, confirmation_token }) => {
|
|
1810
|
+
const blocked = await requireWrite("wabery_reject_group_join_requests", confirmation_token, "reject_group_join_requests");
|
|
1811
|
+
if (blocked)
|
|
1812
|
+
return blocked;
|
|
1813
|
+
return apiResult(client.delete(`${groupPath(channel_id, group_id)}/join-requests`, {
|
|
1814
|
+
join_requests: join_request_ids,
|
|
1815
|
+
}));
|
|
1816
|
+
});
|
|
1817
|
+
registerTool("wabery_remove_group_participants", {
|
|
1818
|
+
title: "Remove WhatsApp group participants",
|
|
1819
|
+
description: "Use this when you need to remove WhatsApp users from a group. Pass participant user IDs in Meta's required participants:[{user}] shape; partial failures are returned per participant. Explicit confirmation is required.",
|
|
1820
|
+
inputSchema: {
|
|
1821
|
+
channel_id: z.string().min(1),
|
|
1822
|
+
group_id: groupIdSchema,
|
|
1823
|
+
participants: z.array(groupParticipantSchema).min(1).max(8),
|
|
1824
|
+
confirmation_token: z.string().optional(),
|
|
1825
|
+
},
|
|
1826
|
+
annotations: {
|
|
1827
|
+
readOnlyHint: false,
|
|
1828
|
+
destructiveHint: true,
|
|
1829
|
+
idempotentHint: true,
|
|
1830
|
+
openWorldHint: true,
|
|
1831
|
+
},
|
|
1832
|
+
}, async ({ channel_id, group_id, participants, confirmation_token }) => {
|
|
1833
|
+
const blocked = await requireWrite("wabery_remove_group_participants", confirmation_token, "remove_group_participants");
|
|
1834
|
+
if (blocked)
|
|
1835
|
+
return blocked;
|
|
1836
|
+
return apiResult(client.delete(`${groupPath(channel_id, group_id)}/participants`, {
|
|
1837
|
+
participants: participants.map(({ user }) => user),
|
|
1838
|
+
}));
|
|
1839
|
+
});
|
|
1840
|
+
registerTool("wabery_delete_group", {
|
|
1841
|
+
title: "Delete WhatsApp group",
|
|
1842
|
+
description: "Use this when you need to permanently delete a Wabery-managed WhatsApp group. This is irreversible at the provider and requires explicit confirmation.",
|
|
1843
|
+
inputSchema: {
|
|
1844
|
+
channel_id: z.string().min(1),
|
|
1845
|
+
group_id: groupIdSchema,
|
|
1846
|
+
confirmation_token: z.string().optional(),
|
|
1847
|
+
},
|
|
1848
|
+
annotations: {
|
|
1849
|
+
readOnlyHint: false,
|
|
1850
|
+
destructiveHint: true,
|
|
1851
|
+
idempotentHint: true,
|
|
1852
|
+
openWorldHint: true,
|
|
1853
|
+
},
|
|
1854
|
+
}, async ({ channel_id, group_id, confirmation_token }) => {
|
|
1855
|
+
const blocked = await requireWrite("wabery_delete_group", confirmation_token, "delete_group");
|
|
1856
|
+
if (blocked)
|
|
1857
|
+
return blocked;
|
|
1858
|
+
return apiResult(client.delete(groupPath(channel_id, group_id)));
|
|
1859
|
+
});
|
|
1860
|
+
registerTool("wabery_send_group_message", {
|
|
1861
|
+
title: "Send WhatsApp group message",
|
|
1862
|
+
description: "Use this when you need to send a supported WhatsApp message to a group_id through POST /messages. group_id is an opaque Meta ID and is mutually exclusive with conversation_id/to. Groups support text, media, and text/media templates, but not commerce or interactive messages. Explicit confirmation is required.",
|
|
1863
|
+
inputSchema: {
|
|
1864
|
+
channel_id: z.string().min(1),
|
|
1865
|
+
group_id: groupIdSchema,
|
|
1866
|
+
body: groupMessageBodySchema.describe("Exactly one supported group content field plus a stable idempotency_key that must be reused for retries. reply_to is optional."),
|
|
1867
|
+
confirmation_token: z.string().optional(),
|
|
1868
|
+
},
|
|
1869
|
+
annotations: {
|
|
1870
|
+
readOnlyHint: false,
|
|
1871
|
+
destructiveHint: true,
|
|
1872
|
+
idempotentHint: false,
|
|
1873
|
+
openWorldHint: true,
|
|
1874
|
+
},
|
|
1875
|
+
}, async ({ channel_id, group_id, body, confirmation_token }) => {
|
|
1876
|
+
const blocked = await requireWrite("wabery_send_group_message", confirmation_token, "send_group_message");
|
|
1877
|
+
if (blocked)
|
|
1878
|
+
return blocked;
|
|
1879
|
+
const idempotencyKey = body.idempotency_key;
|
|
1880
|
+
return apiResult(client.post("/messages", { ...body, channel_id, group_id, idempotency_key: idempotencyKey }, idempotencyKey));
|
|
1881
|
+
});
|
|
1512
1882
|
registerTool("wabery_send_message", {
|
|
1513
1883
|
title: "Send WhatsApp message",
|
|
1514
|
-
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
|
|
1884
|
+
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 exactly one recipient: conversation_id, to, or opaque group_id. group_id is not a phone number; use wabery_send_group_message for the dedicated group-safe path. Include idempotency_key; explicit confirmation is required.",
|
|
1515
1885
|
inputSchema: {
|
|
1516
1886
|
body: z
|
|
1517
1887
|
.record(z.string(), z.unknown())
|
|
1518
|
-
.describe("POST /messages body. Include channel_id plus
|
|
1888
|
+
.describe("POST /messages body. Include channel_id plus exactly one of conversation_id, to, or group_id; include type-specific content and an idempotency_key for safe retries."),
|
|
1519
1889
|
confirmation_token: z
|
|
1520
1890
|
.string()
|
|
1521
1891
|
.optional()
|
|
@@ -1531,11 +1901,21 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1531
1901
|
const blocked = await requireWrite("wabery_send_message", confirmation_token, "send_message");
|
|
1532
1902
|
if (blocked)
|
|
1533
1903
|
return blocked;
|
|
1534
|
-
|
|
1904
|
+
const groupIdempotencyKey = typeof body.group_id === "string"
|
|
1905
|
+
? typeof body.idempotency_key === "string" && body.idempotency_key
|
|
1906
|
+
? body.idempotency_key
|
|
1907
|
+
: null
|
|
1908
|
+
: undefined;
|
|
1909
|
+
if (groupIdempotencyKey === null) {
|
|
1910
|
+
throw new Error("WhatsApp group sends require a stable idempotency_key that is reused for retries.");
|
|
1911
|
+
}
|
|
1912
|
+
return apiResult(client.post("/messages", groupIdempotencyKey
|
|
1913
|
+
? { ...body, idempotency_key: groupIdempotencyKey }
|
|
1914
|
+
: body, groupIdempotencyKey));
|
|
1535
1915
|
});
|
|
1536
1916
|
registerTool("wabery_get_message", {
|
|
1537
1917
|
title: "Get Wabery message",
|
|
1538
|
-
description:
|
|
1918
|
+
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. ${MESSAGE_MUTATION_GUIDANCE}`,
|
|
1539
1919
|
inputSchema: {
|
|
1540
1920
|
message_id: z.string().min(1),
|
|
1541
1921
|
},
|