@wabery/cli 0.15.2 → 0.17.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/README.md +2 -0
- 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 +8 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +425 -19
- package/dist/mcp-server.js.map +1 -1
- package/package.json +4 -3
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
|
-
const SERVER_INSTRUCTIONS = `Wabery is a developer platform for
|
|
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.`;
|
|
163
|
+
const SERVER_INSTRUCTIONS = `Wabery is a developer platform for WhatsApp AI agents. Start with wabery_list_projects, select a project, then wabery_get_project_readiness; channel readiness is for provider detail.
|
|
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
|
|
@@ -208,10 +277,24 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
208
277
|
name: "wabery",
|
|
209
278
|
version: options.serverVersion ?? "0.1.0",
|
|
210
279
|
}, { instructions: serverInstructions(writePolicy) });
|
|
211
|
-
const registerTool = (name, config, callback) =>
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
280
|
+
const registerTool = (name, config, callback) => {
|
|
281
|
+
options.onToolRegistered?.({
|
|
282
|
+
name,
|
|
283
|
+
title: config.title,
|
|
284
|
+
description: config.description,
|
|
285
|
+
readOnly: Boolean(config.annotations.readOnlyHint),
|
|
286
|
+
});
|
|
287
|
+
return server.registerTool(name, {
|
|
288
|
+
outputSchema: toolOutputSchema,
|
|
289
|
+
...config,
|
|
290
|
+
}, callback);
|
|
291
|
+
};
|
|
292
|
+
const groupPath = (channelId, groupId) => {
|
|
293
|
+
const base = `/channels/${encodeURIComponent(channelId)}/whatsapp-groups`;
|
|
294
|
+
return groupId === undefined
|
|
295
|
+
? base
|
|
296
|
+
: `${base}/${encodeURIComponent(groupId)}`;
|
|
297
|
+
};
|
|
215
298
|
const requireWrite = async (action, token, expectedToken) => {
|
|
216
299
|
const modeBlocked = checkWriteMode(action, undefined, undefined, writePolicy);
|
|
217
300
|
if (modeBlocked)
|
|
@@ -931,13 +1014,17 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
931
1014
|
}, async ({ project_id }) => jsonResult(await client.get(`/projects/${encodeURIComponent(project_id)}`)));
|
|
932
1015
|
registerTool("wabery_update_project", {
|
|
933
1016
|
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.",
|
|
1017
|
+
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
1018
|
inputSchema: {
|
|
936
1019
|
project_id: z.string().min(1),
|
|
937
1020
|
name: z.string().min(1).max(100).optional(),
|
|
938
1021
|
description: z.string().max(500).nullable().optional(),
|
|
939
1022
|
routing_mode: z.enum(["NONE", "FLOWS", "EXTERNAL"]).optional(),
|
|
940
1023
|
require_opt_in: z.boolean().optional(),
|
|
1024
|
+
group_webhook_events_enabled: z
|
|
1025
|
+
.boolean()
|
|
1026
|
+
.optional()
|
|
1027
|
+
.describe("Explicitly opt into the four new WhatsApp group management event names; defaults false."),
|
|
941
1028
|
webhook_url: z.string().url().nullable().optional(),
|
|
942
1029
|
webhook_signing_enabled: z.boolean().optional(),
|
|
943
1030
|
webhook_secret: z.string().min(16).max(256).optional(),
|
|
@@ -1467,6 +1554,10 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1467
1554
|
inputSchema: {
|
|
1468
1555
|
contact_id: z.string().optional(),
|
|
1469
1556
|
active: z.boolean().optional(),
|
|
1557
|
+
recipient_type: z
|
|
1558
|
+
.enum(["individual", "group", "all"])
|
|
1559
|
+
.optional()
|
|
1560
|
+
.describe("Defaults to individual for backward compatibility; use group or all explicitly."),
|
|
1470
1561
|
limit: z.number().int().min(1).max(100).optional(),
|
|
1471
1562
|
starting_after: z.string().optional(),
|
|
1472
1563
|
},
|
|
@@ -1485,6 +1576,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1485
1576
|
description: "Use this when you need one organization-scoped WhatsApp conversation by conversation_id from wabery_list_conversations.",
|
|
1486
1577
|
inputSchema: {
|
|
1487
1578
|
conversation_id: z.string().min(1),
|
|
1579
|
+
recipient_type: z.enum(["individual", "group"]).optional(),
|
|
1488
1580
|
},
|
|
1489
1581
|
annotations: {
|
|
1490
1582
|
readOnlyHint: true,
|
|
@@ -1492,12 +1584,13 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1492
1584
|
idempotentHint: true,
|
|
1493
1585
|
openWorldHint: false,
|
|
1494
1586
|
},
|
|
1495
|
-
}, async ({ conversation_id }) => jsonResult(await client.get(`/conversations/${conversation_id}
|
|
1587
|
+
}, async ({ conversation_id, ...query }) => jsonResult(await client.get(`/conversations/${conversation_id}`, query)));
|
|
1496
1588
|
registerTool("wabery_list_conversation_messages", {
|
|
1497
1589
|
title: "List Wabery conversation messages",
|
|
1498
|
-
description:
|
|
1590
|
+
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
1591
|
inputSchema: {
|
|
1500
1592
|
conversation_id: z.string().min(1),
|
|
1593
|
+
recipient_type: z.enum(["individual", "group"]).optional(),
|
|
1501
1594
|
limit: z.number().int().min(1).max(100).optional(),
|
|
1502
1595
|
starting_after: z.string().optional(),
|
|
1503
1596
|
order: z.enum(["asc", "desc"]).optional(),
|
|
@@ -1509,13 +1602,298 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1509
1602
|
openWorldHint: false,
|
|
1510
1603
|
},
|
|
1511
1604
|
}, async ({ conversation_id, ...query }) => jsonResult(await client.get(`/conversations/${conversation_id}/messages`, query)));
|
|
1605
|
+
registerTool("wabery_list_groups", {
|
|
1606
|
+
title: "List WhatsApp groups",
|
|
1607
|
+
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.",
|
|
1608
|
+
inputSchema: {
|
|
1609
|
+
channel_id: z.string().min(1),
|
|
1610
|
+
limit: z.number().int().min(1).max(1024).optional(),
|
|
1611
|
+
starting_after: z.string().optional(),
|
|
1612
|
+
ending_before: z.string().optional(),
|
|
1613
|
+
after: z.string().optional(),
|
|
1614
|
+
before: z.string().optional(),
|
|
1615
|
+
},
|
|
1616
|
+
annotations: {
|
|
1617
|
+
readOnlyHint: true,
|
|
1618
|
+
destructiveHint: false,
|
|
1619
|
+
idempotentHint: true,
|
|
1620
|
+
openWorldHint: false,
|
|
1621
|
+
},
|
|
1622
|
+
}, async ({ channel_id, ...query }) => jsonResult(await client.get(groupPath(channel_id), query)));
|
|
1623
|
+
registerTool("wabery_get_group", {
|
|
1624
|
+
title: "Get WhatsApp group",
|
|
1625
|
+
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.",
|
|
1626
|
+
inputSchema: {
|
|
1627
|
+
channel_id: z.string().min(1),
|
|
1628
|
+
group_id: groupIdSchema,
|
|
1629
|
+
},
|
|
1630
|
+
annotations: {
|
|
1631
|
+
readOnlyHint: true,
|
|
1632
|
+
destructiveHint: false,
|
|
1633
|
+
idempotentHint: true,
|
|
1634
|
+
openWorldHint: false,
|
|
1635
|
+
},
|
|
1636
|
+
}, async ({ channel_id, group_id }) => jsonResult(await client.get(groupPath(channel_id, group_id))));
|
|
1637
|
+
registerTool("wabery_create_group", {
|
|
1638
|
+
title: "Create WhatsApp group",
|
|
1639
|
+
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.",
|
|
1640
|
+
inputSchema: {
|
|
1641
|
+
channel_id: z.string().min(1),
|
|
1642
|
+
subject: z.string().min(1).max(128),
|
|
1643
|
+
description: z.string().max(2048).optional(),
|
|
1644
|
+
join_approval_mode: z
|
|
1645
|
+
.enum(["auto_approve", "approval_required"])
|
|
1646
|
+
.optional(),
|
|
1647
|
+
idempotency_key: z
|
|
1648
|
+
.string()
|
|
1649
|
+
.trim()
|
|
1650
|
+
.min(1)
|
|
1651
|
+
.max(200)
|
|
1652
|
+
.describe("Stable caller-generated key reused for every retry of this group creation."),
|
|
1653
|
+
confirmation_token: z.string().optional(),
|
|
1654
|
+
},
|
|
1655
|
+
annotations: {
|
|
1656
|
+
readOnlyHint: false,
|
|
1657
|
+
destructiveHint: true,
|
|
1658
|
+
idempotentHint: true,
|
|
1659
|
+
openWorldHint: true,
|
|
1660
|
+
},
|
|
1661
|
+
}, async ({ channel_id, confirmation_token, idempotency_key, ...body }) => {
|
|
1662
|
+
const blocked = await requireWrite("wabery_create_group", confirmation_token, "create_group");
|
|
1663
|
+
if (blocked)
|
|
1664
|
+
return blocked;
|
|
1665
|
+
return apiResult(client.post(groupPath(channel_id), body, idempotency_key));
|
|
1666
|
+
});
|
|
1667
|
+
registerTool("wabery_update_group_settings", {
|
|
1668
|
+
title: "Update WhatsApp group settings",
|
|
1669
|
+
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.",
|
|
1670
|
+
inputSchema: {
|
|
1671
|
+
channel_id: z.string().min(1),
|
|
1672
|
+
group_id: groupIdSchema,
|
|
1673
|
+
subject: z.string().min(1).max(128).optional(),
|
|
1674
|
+
description: z.string().max(2048).optional(),
|
|
1675
|
+
confirmation_token: z.string().optional(),
|
|
1676
|
+
},
|
|
1677
|
+
annotations: {
|
|
1678
|
+
readOnlyHint: false,
|
|
1679
|
+
destructiveHint: true,
|
|
1680
|
+
idempotentHint: true,
|
|
1681
|
+
openWorldHint: true,
|
|
1682
|
+
},
|
|
1683
|
+
}, async ({ channel_id, group_id, confirmation_token, ...body }) => {
|
|
1684
|
+
const blocked = await requireWrite("wabery_update_group_settings", confirmation_token, "update_group_settings");
|
|
1685
|
+
if (blocked)
|
|
1686
|
+
return blocked;
|
|
1687
|
+
return apiResult(client.post(groupPath(channel_id, group_id), body));
|
|
1688
|
+
});
|
|
1689
|
+
registerTool("wabery_update_group_profile_picture", {
|
|
1690
|
+
title: "Update WhatsApp group profile picture",
|
|
1691
|
+
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.",
|
|
1692
|
+
inputSchema: {
|
|
1693
|
+
channel_id: z.string().min(1),
|
|
1694
|
+
group_id: groupIdSchema,
|
|
1695
|
+
jpeg_base64: z.string().min(4).max(8_000_000),
|
|
1696
|
+
filename: z.string().min(1).max(240).optional(),
|
|
1697
|
+
subject: z.string().min(1).max(128).optional(),
|
|
1698
|
+
description: z.string().max(2048).optional(),
|
|
1699
|
+
confirmation_token: z.string().optional(),
|
|
1700
|
+
},
|
|
1701
|
+
annotations: {
|
|
1702
|
+
readOnlyHint: false,
|
|
1703
|
+
destructiveHint: true,
|
|
1704
|
+
idempotentHint: true,
|
|
1705
|
+
openWorldHint: true,
|
|
1706
|
+
},
|
|
1707
|
+
}, async ({ channel_id, group_id, jpeg_base64, filename, subject, description, confirmation_token, }) => {
|
|
1708
|
+
const blocked = await requireWrite("wabery_update_group_profile_picture", confirmation_token, "update_group_profile_picture");
|
|
1709
|
+
if (blocked)
|
|
1710
|
+
return blocked;
|
|
1711
|
+
const bytes = Buffer.from(jpeg_base64, "base64");
|
|
1712
|
+
if (bytes.length < 3 ||
|
|
1713
|
+
bytes[0] !== 0xff ||
|
|
1714
|
+
bytes[1] !== 0xd8 ||
|
|
1715
|
+
bytes[2] !== 0xff) {
|
|
1716
|
+
throw new Error("jpeg_base64 must decode to a JPEG file");
|
|
1717
|
+
}
|
|
1718
|
+
const form = new FormData();
|
|
1719
|
+
form.set("profile_picture_file", new Blob([bytes], { type: "image/jpeg" }), filename ?? "profile.jpg");
|
|
1720
|
+
if (subject !== undefined)
|
|
1721
|
+
form.set("subject", subject);
|
|
1722
|
+
if (description !== undefined)
|
|
1723
|
+
form.set("description", description);
|
|
1724
|
+
return apiResult(client.postForm(groupPath(channel_id, group_id), form));
|
|
1725
|
+
});
|
|
1726
|
+
registerTool("wabery_get_group_invite_link", {
|
|
1727
|
+
title: "Get WhatsApp group invite link",
|
|
1728
|
+
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.",
|
|
1729
|
+
inputSchema: {
|
|
1730
|
+
channel_id: z.string().min(1),
|
|
1731
|
+
group_id: groupIdSchema,
|
|
1732
|
+
},
|
|
1733
|
+
annotations: {
|
|
1734
|
+
readOnlyHint: true,
|
|
1735
|
+
destructiveHint: false,
|
|
1736
|
+
idempotentHint: true,
|
|
1737
|
+
openWorldHint: false,
|
|
1738
|
+
},
|
|
1739
|
+
}, async ({ channel_id, group_id }) => jsonResult(await client.get(`${groupPath(channel_id, group_id)}/invite-link`)));
|
|
1740
|
+
registerTool("wabery_reset_group_invite_link", {
|
|
1741
|
+
title: "Reset WhatsApp group invite link",
|
|
1742
|
+
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.",
|
|
1743
|
+
inputSchema: {
|
|
1744
|
+
channel_id: z.string().min(1),
|
|
1745
|
+
group_id: groupIdSchema,
|
|
1746
|
+
confirmation_token: z.string().optional(),
|
|
1747
|
+
},
|
|
1748
|
+
annotations: {
|
|
1749
|
+
readOnlyHint: false,
|
|
1750
|
+
destructiveHint: true,
|
|
1751
|
+
idempotentHint: false,
|
|
1752
|
+
openWorldHint: true,
|
|
1753
|
+
},
|
|
1754
|
+
}, async ({ channel_id, group_id, confirmation_token }) => {
|
|
1755
|
+
const blocked = await requireWrite("wabery_reset_group_invite_link", confirmation_token, "reset_group_invite_link");
|
|
1756
|
+
if (blocked)
|
|
1757
|
+
return blocked;
|
|
1758
|
+
return apiResult(client.post(`${groupPath(channel_id, group_id)}/invite-link`));
|
|
1759
|
+
});
|
|
1760
|
+
registerTool("wabery_list_group_join_requests", {
|
|
1761
|
+
title: "List WhatsApp group join requests",
|
|
1762
|
+
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.",
|
|
1763
|
+
inputSchema: {
|
|
1764
|
+
channel_id: z.string().min(1),
|
|
1765
|
+
group_id: groupIdSchema,
|
|
1766
|
+
limit: z.number().int().min(1).max(1024).optional(),
|
|
1767
|
+
starting_after: z.string().optional(),
|
|
1768
|
+
ending_before: z.string().optional(),
|
|
1769
|
+
after: z.string().optional(),
|
|
1770
|
+
before: z.string().optional(),
|
|
1771
|
+
},
|
|
1772
|
+
annotations: {
|
|
1773
|
+
readOnlyHint: true,
|
|
1774
|
+
destructiveHint: false,
|
|
1775
|
+
idempotentHint: true,
|
|
1776
|
+
openWorldHint: false,
|
|
1777
|
+
},
|
|
1778
|
+
}, async ({ channel_id, group_id, ...query }) => jsonResult(await client.get(`${groupPath(channel_id, group_id)}/join-requests`, query)));
|
|
1779
|
+
registerTool("wabery_approve_group_join_requests", {
|
|
1780
|
+
title: "Approve WhatsApp group join requests",
|
|
1781
|
+
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.",
|
|
1782
|
+
inputSchema: {
|
|
1783
|
+
channel_id: z.string().min(1),
|
|
1784
|
+
group_id: groupIdSchema,
|
|
1785
|
+
join_request_ids: groupJoinRequestIdsSchema,
|
|
1786
|
+
confirmation_token: z.string().optional(),
|
|
1787
|
+
},
|
|
1788
|
+
annotations: {
|
|
1789
|
+
readOnlyHint: false,
|
|
1790
|
+
destructiveHint: true,
|
|
1791
|
+
idempotentHint: true,
|
|
1792
|
+
openWorldHint: true,
|
|
1793
|
+
},
|
|
1794
|
+
}, async ({ channel_id, group_id, join_request_ids, confirmation_token }) => {
|
|
1795
|
+
const blocked = await requireWrite("wabery_approve_group_join_requests", confirmation_token, "approve_group_join_requests");
|
|
1796
|
+
if (blocked)
|
|
1797
|
+
return blocked;
|
|
1798
|
+
return apiResult(client.post(`${groupPath(channel_id, group_id)}/join-requests`, {
|
|
1799
|
+
join_requests: join_request_ids,
|
|
1800
|
+
}));
|
|
1801
|
+
});
|
|
1802
|
+
registerTool("wabery_reject_group_join_requests", {
|
|
1803
|
+
title: "Reject WhatsApp group join requests",
|
|
1804
|
+
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.",
|
|
1805
|
+
inputSchema: {
|
|
1806
|
+
channel_id: z.string().min(1),
|
|
1807
|
+
group_id: groupIdSchema,
|
|
1808
|
+
join_request_ids: groupJoinRequestIdsSchema,
|
|
1809
|
+
confirmation_token: z.string().optional(),
|
|
1810
|
+
},
|
|
1811
|
+
annotations: {
|
|
1812
|
+
readOnlyHint: false,
|
|
1813
|
+
destructiveHint: true,
|
|
1814
|
+
idempotentHint: true,
|
|
1815
|
+
openWorldHint: true,
|
|
1816
|
+
},
|
|
1817
|
+
}, async ({ channel_id, group_id, join_request_ids, confirmation_token }) => {
|
|
1818
|
+
const blocked = await requireWrite("wabery_reject_group_join_requests", confirmation_token, "reject_group_join_requests");
|
|
1819
|
+
if (blocked)
|
|
1820
|
+
return blocked;
|
|
1821
|
+
return apiResult(client.delete(`${groupPath(channel_id, group_id)}/join-requests`, {
|
|
1822
|
+
join_requests: join_request_ids,
|
|
1823
|
+
}));
|
|
1824
|
+
});
|
|
1825
|
+
registerTool("wabery_remove_group_participants", {
|
|
1826
|
+
title: "Remove WhatsApp group participants",
|
|
1827
|
+
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.",
|
|
1828
|
+
inputSchema: {
|
|
1829
|
+
channel_id: z.string().min(1),
|
|
1830
|
+
group_id: groupIdSchema,
|
|
1831
|
+
participants: z.array(groupParticipantSchema).min(1).max(8),
|
|
1832
|
+
confirmation_token: z.string().optional(),
|
|
1833
|
+
},
|
|
1834
|
+
annotations: {
|
|
1835
|
+
readOnlyHint: false,
|
|
1836
|
+
destructiveHint: true,
|
|
1837
|
+
idempotentHint: true,
|
|
1838
|
+
openWorldHint: true,
|
|
1839
|
+
},
|
|
1840
|
+
}, async ({ channel_id, group_id, participants, confirmation_token }) => {
|
|
1841
|
+
const blocked = await requireWrite("wabery_remove_group_participants", confirmation_token, "remove_group_participants");
|
|
1842
|
+
if (blocked)
|
|
1843
|
+
return blocked;
|
|
1844
|
+
return apiResult(client.delete(`${groupPath(channel_id, group_id)}/participants`, {
|
|
1845
|
+
participants: participants.map(({ user }) => user),
|
|
1846
|
+
}));
|
|
1847
|
+
});
|
|
1848
|
+
registerTool("wabery_delete_group", {
|
|
1849
|
+
title: "Delete WhatsApp group",
|
|
1850
|
+
description: "Use this when you need to permanently delete a Wabery-managed WhatsApp group. This is irreversible at the provider and requires explicit confirmation.",
|
|
1851
|
+
inputSchema: {
|
|
1852
|
+
channel_id: z.string().min(1),
|
|
1853
|
+
group_id: groupIdSchema,
|
|
1854
|
+
confirmation_token: z.string().optional(),
|
|
1855
|
+
},
|
|
1856
|
+
annotations: {
|
|
1857
|
+
readOnlyHint: false,
|
|
1858
|
+
destructiveHint: true,
|
|
1859
|
+
idempotentHint: true,
|
|
1860
|
+
openWorldHint: true,
|
|
1861
|
+
},
|
|
1862
|
+
}, async ({ channel_id, group_id, confirmation_token }) => {
|
|
1863
|
+
const blocked = await requireWrite("wabery_delete_group", confirmation_token, "delete_group");
|
|
1864
|
+
if (blocked)
|
|
1865
|
+
return blocked;
|
|
1866
|
+
return apiResult(client.delete(groupPath(channel_id, group_id)));
|
|
1867
|
+
});
|
|
1868
|
+
registerTool("wabery_send_group_message", {
|
|
1869
|
+
title: "Send WhatsApp group message",
|
|
1870
|
+
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.",
|
|
1871
|
+
inputSchema: {
|
|
1872
|
+
channel_id: z.string().min(1),
|
|
1873
|
+
group_id: groupIdSchema,
|
|
1874
|
+
body: groupMessageBodySchema.describe("Exactly one supported group content field plus a stable idempotency_key that must be reused for retries. reply_to is optional."),
|
|
1875
|
+
confirmation_token: z.string().optional(),
|
|
1876
|
+
},
|
|
1877
|
+
annotations: {
|
|
1878
|
+
readOnlyHint: false,
|
|
1879
|
+
destructiveHint: true,
|
|
1880
|
+
idempotentHint: false,
|
|
1881
|
+
openWorldHint: true,
|
|
1882
|
+
},
|
|
1883
|
+
}, async ({ channel_id, group_id, body, confirmation_token }) => {
|
|
1884
|
+
const blocked = await requireWrite("wabery_send_group_message", confirmation_token, "send_group_message");
|
|
1885
|
+
if (blocked)
|
|
1886
|
+
return blocked;
|
|
1887
|
+
const idempotencyKey = body.idempotency_key;
|
|
1888
|
+
return apiResult(client.post("/messages", { ...body, channel_id, group_id, idempotency_key: idempotencyKey }, idempotencyKey));
|
|
1889
|
+
});
|
|
1512
1890
|
registerTool("wabery_send_message", {
|
|
1513
1891
|
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
|
|
1892
|
+
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
1893
|
inputSchema: {
|
|
1516
1894
|
body: z
|
|
1517
1895
|
.record(z.string(), z.unknown())
|
|
1518
|
-
.describe("POST /messages body. Include channel_id plus
|
|
1896
|
+
.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
1897
|
confirmation_token: z
|
|
1520
1898
|
.string()
|
|
1521
1899
|
.optional()
|
|
@@ -1531,11 +1909,21 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1531
1909
|
const blocked = await requireWrite("wabery_send_message", confirmation_token, "send_message");
|
|
1532
1910
|
if (blocked)
|
|
1533
1911
|
return blocked;
|
|
1534
|
-
|
|
1912
|
+
const groupIdempotencyKey = typeof body.group_id === "string"
|
|
1913
|
+
? typeof body.idempotency_key === "string" && body.idempotency_key
|
|
1914
|
+
? body.idempotency_key
|
|
1915
|
+
: null
|
|
1916
|
+
: undefined;
|
|
1917
|
+
if (groupIdempotencyKey === null) {
|
|
1918
|
+
throw new Error("WhatsApp group sends require a stable idempotency_key that is reused for retries.");
|
|
1919
|
+
}
|
|
1920
|
+
return apiResult(client.post("/messages", groupIdempotencyKey
|
|
1921
|
+
? { ...body, idempotency_key: groupIdempotencyKey }
|
|
1922
|
+
: body, groupIdempotencyKey));
|
|
1535
1923
|
});
|
|
1536
1924
|
registerTool("wabery_get_message", {
|
|
1537
1925
|
title: "Get Wabery message",
|
|
1538
|
-
description:
|
|
1926
|
+
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
1927
|
inputSchema: {
|
|
1540
1928
|
message_id: z.string().min(1),
|
|
1541
1929
|
},
|
|
@@ -1904,6 +2292,24 @@ async function waitUntilTemplateApproved(client, templateId, { timeoutMs, interv
|
|
|
1904
2292
|
await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, Math.max(0, deadline - Date.now()))));
|
|
1905
2293
|
}
|
|
1906
2294
|
}
|
|
2295
|
+
let cachedPublicMcpTools = null;
|
|
2296
|
+
export function listPublicMcpTools() {
|
|
2297
|
+
if (cachedPublicMcpTools)
|
|
2298
|
+
return cachedPublicMcpTools;
|
|
2299
|
+
const tools = [];
|
|
2300
|
+
createWaberyMcpServer(new WaberyApiClient({
|
|
2301
|
+
apiKey: "public-catalog",
|
|
2302
|
+
baseUrl: "https://api.wabery.com/v1",
|
|
2303
|
+
}), {
|
|
2304
|
+
writePolicy: "enabled",
|
|
2305
|
+
serverVersion: "catalog",
|
|
2306
|
+
onToolRegistered: (tool) => {
|
|
2307
|
+
tools.push(tool);
|
|
2308
|
+
},
|
|
2309
|
+
});
|
|
2310
|
+
cachedPublicMcpTools = tools;
|
|
2311
|
+
return tools;
|
|
2312
|
+
}
|
|
1907
2313
|
export async function runMcpServer(client = new WaberyApiClient()) {
|
|
1908
2314
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1909
2315
|
const server = createWaberyMcpServer(client);
|