@wabery/cli 0.15.0 → 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/README.md +3 -3
- 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 +402 -18
- 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,18 +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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
Use
|
|
94
|
-
|
|
159
|
+
// Claude Code caps MCP server instructions at 2KB and silently truncates from
|
|
160
|
+
// the end (worse with multiple servers). Keep this text + WRITE_NOTE under
|
|
161
|
+
// 1900 UTF-8 bytes as a buffer, and put invariants first. Per-tool playbooks
|
|
162
|
+
// belong on tool descriptions, not in this handshake.
|
|
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.
|
|
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.`;
|
|
95
168
|
// The write gate differs by transport, and the model has to be told the truth
|
|
96
169
|
// about the session it is actually in: a hosted OAuth client (ChatGPT, Claude)
|
|
97
170
|
// cannot set an env var, so advertising the env gate there makes it refuse to
|
|
@@ -208,6 +281,12 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
208
281
|
outputSchema: toolOutputSchema,
|
|
209
282
|
...config,
|
|
210
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
|
+
};
|
|
211
290
|
const requireWrite = async (action, token, expectedToken) => {
|
|
212
291
|
const modeBlocked = checkWriteMode(action, undefined, undefined, writePolicy);
|
|
213
292
|
if (modeBlocked)
|
|
@@ -401,7 +480,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
401
480
|
}, async () => jsonResult(await client.get("/config")));
|
|
402
481
|
registerTool("wabery_validate_config", {
|
|
403
482
|
title: "Validate Wabery config",
|
|
404
|
-
description: "Use this when you need to validate a complete declarative config without changing resources. Use wabery_preview_config_diff next to inspect effects.",
|
|
483
|
+
description: "Use this when you need to validate a complete declarative config without changing resources. Hosted-agent fields are Draft-only; Live cannot be edited here. Use wabery_preview_config_diff next to inspect effects.",
|
|
405
484
|
inputSchema: {
|
|
406
485
|
config: configSchema.describe("The full wabery.config.json object."),
|
|
407
486
|
},
|
|
@@ -414,7 +493,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
414
493
|
}, async ({ config }) => apiResult(client.post("/config/validate", config)));
|
|
415
494
|
registerTool("wabery_preview_config_diff", {
|
|
416
495
|
title: "Preview Wabery config diff",
|
|
417
|
-
description: "Use this when you need to preview what a complete config would create, update, delete, or leave unchanged without applying it. Use wabery_apply_config only after reviewing this diff.",
|
|
496
|
+
description: "Use this when you need to preview what a complete config would create, update, delete, or leave unchanged without applying it. Hosted-agent changes land on Draft only; Live stays unchanged until someone uses Review & publish in Wabery. Use wabery_apply_config only after reviewing this diff.",
|
|
418
497
|
inputSchema: {
|
|
419
498
|
config: configSchema.describe("The full wabery.config.json object."),
|
|
420
499
|
},
|
|
@@ -427,7 +506,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
427
506
|
}, async ({ config }) => apiResult(client.post("/config/diff", config)));
|
|
428
507
|
registerTool("wabery_apply_config", {
|
|
429
508
|
title: "Apply Wabery config",
|
|
430
|
-
description: "Use this when you need to apply a reviewed complete config, creating
|
|
509
|
+
description: "Use this when you need to apply a reviewed complete config, creating or updating the hosted-agent Draft plus Flows, automations, webhooks, and eligible Business Agent resources. It cannot edit Live or publish the hosted agent to customers. Do not use it for one resource update when a narrow update tool fits. It never publishes Flows; use wabery_publish_flow separately. Hosted-agent publish happens in the Wabery AI builder after Sandbox testing.",
|
|
431
510
|
inputSchema: {
|
|
432
511
|
config: configSchema.describe("The full wabery.config.json object."),
|
|
433
512
|
confirmation_token: z
|
|
@@ -927,13 +1006,17 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
927
1006
|
}, async ({ project_id }) => jsonResult(await client.get(`/projects/${encodeURIComponent(project_id)}`)));
|
|
928
1007
|
registerTool("wabery_update_project", {
|
|
929
1008
|
title: "Update a Wabery project",
|
|
930
|
-
description: "Use this when you need to overwrite project name, description, routing_mode, require_opt_in, or signed-webhook settings for a project_id from wabery_list_projects. Prefer wabery_apply_config for reviewed project-wide declarative changes.",
|
|
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.",
|
|
931
1010
|
inputSchema: {
|
|
932
1011
|
project_id: z.string().min(1),
|
|
933
1012
|
name: z.string().min(1).max(100).optional(),
|
|
934
1013
|
description: z.string().max(500).nullable().optional(),
|
|
935
1014
|
routing_mode: z.enum(["NONE", "FLOWS", "EXTERNAL"]).optional(),
|
|
936
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."),
|
|
937
1020
|
webhook_url: z.string().url().nullable().optional(),
|
|
938
1021
|
webhook_signing_enabled: z.boolean().optional(),
|
|
939
1022
|
webhook_secret: z.string().min(16).max(256).optional(),
|
|
@@ -1463,6 +1546,10 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1463
1546
|
inputSchema: {
|
|
1464
1547
|
contact_id: z.string().optional(),
|
|
1465
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."),
|
|
1466
1553
|
limit: z.number().int().min(1).max(100).optional(),
|
|
1467
1554
|
starting_after: z.string().optional(),
|
|
1468
1555
|
},
|
|
@@ -1481,6 +1568,7 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1481
1568
|
description: "Use this when you need one organization-scoped WhatsApp conversation by conversation_id from wabery_list_conversations.",
|
|
1482
1569
|
inputSchema: {
|
|
1483
1570
|
conversation_id: z.string().min(1),
|
|
1571
|
+
recipient_type: z.enum(["individual", "group"]).optional(),
|
|
1484
1572
|
},
|
|
1485
1573
|
annotations: {
|
|
1486
1574
|
readOnlyHint: true,
|
|
@@ -1488,12 +1576,13 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1488
1576
|
idempotentHint: true,
|
|
1489
1577
|
openWorldHint: false,
|
|
1490
1578
|
},
|
|
1491
|
-
}, async ({ conversation_id }) => jsonResult(await client.get(`/conversations/${conversation_id}
|
|
1579
|
+
}, async ({ conversation_id, ...query }) => jsonResult(await client.get(`/conversations/${conversation_id}`, query)));
|
|
1492
1580
|
registerTool("wabery_list_conversation_messages", {
|
|
1493
1581
|
title: "List Wabery conversation messages",
|
|
1494
|
-
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.`,
|
|
1495
1583
|
inputSchema: {
|
|
1496
1584
|
conversation_id: z.string().min(1),
|
|
1585
|
+
recipient_type: z.enum(["individual", "group"]).optional(),
|
|
1497
1586
|
limit: z.number().int().min(1).max(100).optional(),
|
|
1498
1587
|
starting_after: z.string().optional(),
|
|
1499
1588
|
order: z.enum(["asc", "desc"]).optional(),
|
|
@@ -1505,13 +1594,298 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1505
1594
|
openWorldHint: false,
|
|
1506
1595
|
},
|
|
1507
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
|
+
});
|
|
1508
1882
|
registerTool("wabery_send_message", {
|
|
1509
1883
|
title: "Send WhatsApp message",
|
|
1510
|
-
description: "Use this when you need to send one real WhatsApp text, approved template, media, or interactive message. Do not use it to send a Flow; use wabery_send_flow. The body must identify channel and recipient
|
|
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.",
|
|
1511
1885
|
inputSchema: {
|
|
1512
1886
|
body: z
|
|
1513
1887
|
.record(z.string(), z.unknown())
|
|
1514
|
-
.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."),
|
|
1515
1889
|
confirmation_token: z
|
|
1516
1890
|
.string()
|
|
1517
1891
|
.optional()
|
|
@@ -1527,11 +1901,21 @@ export function createWaberyMcpServer(client = new WaberyApiClient(), options =
|
|
|
1527
1901
|
const blocked = await requireWrite("wabery_send_message", confirmation_token, "send_message");
|
|
1528
1902
|
if (blocked)
|
|
1529
1903
|
return blocked;
|
|
1530
|
-
|
|
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));
|
|
1531
1915
|
});
|
|
1532
1916
|
registerTool("wabery_get_message", {
|
|
1533
1917
|
title: "Get Wabery message",
|
|
1534
|
-
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}`,
|
|
1535
1919
|
inputSchema: {
|
|
1536
1920
|
message_id: z.string().min(1),
|
|
1537
1921
|
},
|