@sendly/mcp 1.3.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -32
- package/dist/index.js +1355 -483
- package/package.json +14 -3
package/dist/index.js
CHANGED
|
@@ -3,8 +3,1360 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// src/tools.ts
|
|
6
8
|
import { z } from "zod";
|
|
7
|
-
|
|
9
|
+
function ok(data) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function err(error) {
|
|
15
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16
|
+
return {
|
|
17
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
18
|
+
isError: true
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function registerAllTools(server2, api2) {
|
|
22
|
+
server2.tool(
|
|
23
|
+
"send_sms",
|
|
24
|
+
"Send an SMS message to a phone number. Returns the message with delivery status. Use 'transactional' for alerts/OTP (bypasses quiet hours), 'marketing' for promotions.",
|
|
25
|
+
{
|
|
26
|
+
to: z.string().describe("Recipient phone number in E.164 format (+14155551234)"),
|
|
27
|
+
text: z.string().describe("Message text content"),
|
|
28
|
+
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)"),
|
|
29
|
+
metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata to attach")
|
|
30
|
+
},
|
|
31
|
+
async ({ to, text, messageType, metadata }) => {
|
|
32
|
+
try {
|
|
33
|
+
const body = { to, text };
|
|
34
|
+
if (messageType) body.messageType = messageType;
|
|
35
|
+
if (metadata) body.metadata = metadata;
|
|
36
|
+
return ok(await api2("POST", "/messages", body));
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return err(e);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
server2.tool(
|
|
43
|
+
"list_messages",
|
|
44
|
+
"List sent and received SMS messages with pagination. Use q parameter for full-text search across message content.",
|
|
45
|
+
{
|
|
46
|
+
limit: z.number().optional().describe("Messages to return (1-100, default 50)"),
|
|
47
|
+
offset: z.number().optional().describe("Pagination offset"),
|
|
48
|
+
q: z.string().optional().describe("Full-text search query for message content")
|
|
49
|
+
},
|
|
50
|
+
async ({ limit, offset, q }) => {
|
|
51
|
+
try {
|
|
52
|
+
return ok(
|
|
53
|
+
await api2("GET", "/messages", void 0, {
|
|
54
|
+
limit: limit?.toString(),
|
|
55
|
+
offset: offset?.toString(),
|
|
56
|
+
q
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return err(e);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
server2.tool(
|
|
65
|
+
"get_message",
|
|
66
|
+
"Get details of a specific SMS message including delivery status, timestamps, and metadata.",
|
|
67
|
+
{
|
|
68
|
+
messageId: z.string().describe("The message ID")
|
|
69
|
+
},
|
|
70
|
+
async ({ messageId }) => {
|
|
71
|
+
try {
|
|
72
|
+
return ok(await api2("GET", `/messages/${messageId}`));
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return err(e);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
server2.tool(
|
|
79
|
+
"schedule_sms",
|
|
80
|
+
"Schedule an SMS for future delivery (5 minutes to 5 days from now). Credits are reserved immediately and refunded if cancelled.",
|
|
81
|
+
{
|
|
82
|
+
to: z.string().describe("Recipient phone number in E.164 format"),
|
|
83
|
+
text: z.string().describe("Message text content"),
|
|
84
|
+
scheduledAt: z.string().describe("ISO 8601 datetime for delivery (e.g., 2026-03-16T09:00:00Z)"),
|
|
85
|
+
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)")
|
|
86
|
+
},
|
|
87
|
+
async ({ to, text, scheduledAt, messageType }) => {
|
|
88
|
+
try {
|
|
89
|
+
const body = { to, text, scheduledAt };
|
|
90
|
+
if (messageType) body.messageType = messageType;
|
|
91
|
+
return ok(await api2("POST", "/messages/schedule", body));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return err(e);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
server2.tool(
|
|
98
|
+
"cancel_scheduled_message",
|
|
99
|
+
"Cancel a scheduled message before it sends. Credits are refunded automatically.",
|
|
100
|
+
{
|
|
101
|
+
messageId: z.string().describe("The scheduled message ID to cancel")
|
|
102
|
+
},
|
|
103
|
+
async ({ messageId }) => {
|
|
104
|
+
try {
|
|
105
|
+
return ok(await api2("DELETE", `/messages/scheduled/${messageId}`));
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return err(e);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
server2.tool(
|
|
112
|
+
"list_scheduled_messages",
|
|
113
|
+
"List all scheduled messages that haven't been sent yet.",
|
|
114
|
+
{
|
|
115
|
+
limit: z.number().optional().describe("Messages to return (1-100, default 50)"),
|
|
116
|
+
offset: z.number().optional().describe("Pagination offset")
|
|
117
|
+
},
|
|
118
|
+
async ({ limit, offset }) => {
|
|
119
|
+
try {
|
|
120
|
+
return ok(
|
|
121
|
+
await api2("GET", "/messages/scheduled", void 0, {
|
|
122
|
+
limit: limit?.toString(),
|
|
123
|
+
offset: offset?.toString()
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return err(e);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
server2.tool(
|
|
132
|
+
"send_batch",
|
|
133
|
+
"Send multiple SMS messages in a single batch (up to 1000). More efficient than individual sends for bulk messaging.",
|
|
134
|
+
{
|
|
135
|
+
messages: z.array(z.object({
|
|
136
|
+
to: z.string().describe("Recipient phone number in E.164 format"),
|
|
137
|
+
text: z.string().describe("Message text")
|
|
138
|
+
})).describe("Array of messages to send (max 1000)"),
|
|
139
|
+
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type for all messages (default: marketing)")
|
|
140
|
+
},
|
|
141
|
+
async ({ messages, messageType }) => {
|
|
142
|
+
try {
|
|
143
|
+
const body = { messages };
|
|
144
|
+
if (messageType) body.messageType = messageType;
|
|
145
|
+
return ok(await api2("POST", "/messages/batch", body));
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return err(e);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
server2.tool(
|
|
152
|
+
"preview_batch",
|
|
153
|
+
"Preview a batch without sending. Returns credit cost estimate and validation results.",
|
|
154
|
+
{
|
|
155
|
+
messages: z.array(z.object({
|
|
156
|
+
to: z.string().describe("Recipient phone number in E.164 format"),
|
|
157
|
+
text: z.string().describe("Message text")
|
|
158
|
+
})).describe("Array of messages to preview"),
|
|
159
|
+
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)")
|
|
160
|
+
},
|
|
161
|
+
async ({ messages, messageType }) => {
|
|
162
|
+
try {
|
|
163
|
+
const body = { messages };
|
|
164
|
+
if (messageType) body.messageType = messageType;
|
|
165
|
+
return ok(await api2("POST", "/messages/batch/preview", body));
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return err(e);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
server2.tool(
|
|
172
|
+
"get_batch",
|
|
173
|
+
"Get the status of a message batch including per-message delivery results.",
|
|
174
|
+
{
|
|
175
|
+
batchId: z.string().describe("The batch ID")
|
|
176
|
+
},
|
|
177
|
+
async ({ batchId }) => {
|
|
178
|
+
try {
|
|
179
|
+
return ok(await api2("GET", `/messages/batch/${batchId}`));
|
|
180
|
+
} catch (e) {
|
|
181
|
+
return err(e);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
server2.tool(
|
|
186
|
+
"list_batches",
|
|
187
|
+
"List message batches with pagination.",
|
|
188
|
+
{
|
|
189
|
+
limit: z.number().optional().describe("Batches to return (1-100, default 50)"),
|
|
190
|
+
offset: z.number().optional().describe("Pagination offset")
|
|
191
|
+
},
|
|
192
|
+
async ({ limit, offset }) => {
|
|
193
|
+
try {
|
|
194
|
+
return ok(
|
|
195
|
+
await api2("GET", "/messages/batches", void 0, {
|
|
196
|
+
limit: limit?.toString(),
|
|
197
|
+
offset: offset?.toString()
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return err(e);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
server2.tool(
|
|
206
|
+
"list_conversations",
|
|
207
|
+
"List SMS conversation threads ordered by most recent activity. Each conversation groups all messages with a specific phone number.",
|
|
208
|
+
{
|
|
209
|
+
limit: z.number().optional().describe("Conversations to return (1-100, default 50)"),
|
|
210
|
+
offset: z.number().optional().describe("Pagination offset"),
|
|
211
|
+
status: z.enum(["active", "closed"]).optional().describe("Filter by conversation status")
|
|
212
|
+
},
|
|
213
|
+
async ({ limit, offset, status }) => {
|
|
214
|
+
try {
|
|
215
|
+
return ok(
|
|
216
|
+
await api2("GET", "/conversations", void 0, {
|
|
217
|
+
limit: limit?.toString(),
|
|
218
|
+
offset: offset?.toString(),
|
|
219
|
+
status
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return err(e);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
server2.tool(
|
|
228
|
+
"get_conversation_context",
|
|
229
|
+
"Get LLM-ready formatted conversation context. Returns a pre-formatted text string with timestamped messages, AI classification, and business context \u2014 ready to paste into a prompt.",
|
|
230
|
+
{
|
|
231
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
232
|
+
maxMessages: z.number().optional().describe("Max messages to include (default 20, max 50)")
|
|
233
|
+
},
|
|
234
|
+
async ({ conversationId, maxMessages }) => {
|
|
235
|
+
try {
|
|
236
|
+
return ok(
|
|
237
|
+
await api2("GET", `/conversations/${conversationId}/context`, void 0, {
|
|
238
|
+
max_messages: maxMessages?.toString()
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
return err(e);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
server2.tool(
|
|
247
|
+
"get_conversation",
|
|
248
|
+
"Get a conversation thread by ID. Set includeMessages=true to load the message history.",
|
|
249
|
+
{
|
|
250
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
251
|
+
includeMessages: z.boolean().optional().describe("Include message history (default false)"),
|
|
252
|
+
messageLimit: z.number().optional().describe("Number of messages to include (default 50)")
|
|
253
|
+
},
|
|
254
|
+
async ({ conversationId, includeMessages, messageLimit }) => {
|
|
255
|
+
try {
|
|
256
|
+
return ok(
|
|
257
|
+
await api2("GET", `/conversations/${conversationId}`, void 0, {
|
|
258
|
+
include_messages: includeMessages ? "true" : void 0,
|
|
259
|
+
message_limit: messageLimit?.toString()
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
return err(e);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
server2.tool(
|
|
268
|
+
"reply_to_conversation",
|
|
269
|
+
"Send a reply within an existing conversation. The recipient is automatically set from the conversation's phone number.",
|
|
270
|
+
{
|
|
271
|
+
conversationId: z.string().describe("The conversation ID to reply in"),
|
|
272
|
+
text: z.string().describe("Reply message text"),
|
|
273
|
+
mediaUrls: z.array(z.string()).optional().describe("Media URLs for MMS")
|
|
274
|
+
},
|
|
275
|
+
async ({ conversationId, text, mediaUrls }) => {
|
|
276
|
+
try {
|
|
277
|
+
const body = { text };
|
|
278
|
+
if (mediaUrls?.length) body.mediaUrls = mediaUrls;
|
|
279
|
+
return ok(await api2("POST", `/conversations/${conversationId}/messages`, body));
|
|
280
|
+
} catch (e) {
|
|
281
|
+
return err(e);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
server2.tool(
|
|
286
|
+
"update_conversation",
|
|
287
|
+
"Update a conversation's metadata or tags. Use metadata for custom key-value data, tags for categorization.",
|
|
288
|
+
{
|
|
289
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
290
|
+
metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata"),
|
|
291
|
+
tags: z.array(z.string()).optional().describe("Tags for categorization (replaces existing tags)")
|
|
292
|
+
},
|
|
293
|
+
async ({ conversationId, metadata, tags }) => {
|
|
294
|
+
try {
|
|
295
|
+
const body = {};
|
|
296
|
+
if (metadata) body.metadata = metadata;
|
|
297
|
+
if (tags) body.tags = tags;
|
|
298
|
+
return ok(await api2("PATCH", `/conversations/${conversationId}`, body));
|
|
299
|
+
} catch (e) {
|
|
300
|
+
return err(e);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
server2.tool(
|
|
305
|
+
"close_conversation",
|
|
306
|
+
"Close a conversation. Closed conversations auto-reopen when a new inbound message arrives.",
|
|
307
|
+
{ conversationId: z.string().describe("The conversation ID to close") },
|
|
308
|
+
async ({ conversationId }) => {
|
|
309
|
+
try {
|
|
310
|
+
return ok(await api2("POST", `/conversations/${conversationId}/close`));
|
|
311
|
+
} catch (e) {
|
|
312
|
+
return err(e);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
server2.tool(
|
|
317
|
+
"reopen_conversation",
|
|
318
|
+
"Reopen a previously closed conversation, setting its status back to active.",
|
|
319
|
+
{ conversationId: z.string().describe("The conversation ID to reopen") },
|
|
320
|
+
async ({ conversationId }) => {
|
|
321
|
+
try {
|
|
322
|
+
return ok(await api2("POST", `/conversations/${conversationId}/reopen`));
|
|
323
|
+
} catch (e) {
|
|
324
|
+
return err(e);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
server2.tool(
|
|
329
|
+
"mark_conversation_read",
|
|
330
|
+
"Mark a conversation as read, resetting the unread count to zero.",
|
|
331
|
+
{ conversationId: z.string().describe("The conversation ID") },
|
|
332
|
+
async ({ conversationId }) => {
|
|
333
|
+
try {
|
|
334
|
+
return ok(await api2("POST", `/conversations/${conversationId}/mark-read`));
|
|
335
|
+
} catch (e) {
|
|
336
|
+
return err(e);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
server2.tool(
|
|
341
|
+
"get_suggested_replies",
|
|
342
|
+
"Get AI-generated reply suggestions for a conversation based on message history and context. Returns 2-3 suggested responses with different tones (professional, friendly, concise).",
|
|
343
|
+
{ conversationId: z.string().describe("The conversation ID to generate suggestions for") },
|
|
344
|
+
async ({ conversationId }) => {
|
|
345
|
+
try {
|
|
346
|
+
return ok(await api2("POST", `/conversations/${conversationId}/suggest-replies`));
|
|
347
|
+
} catch (e) {
|
|
348
|
+
return err(e);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
server2.tool(
|
|
353
|
+
"create_contact",
|
|
354
|
+
"Create a contact with phone number and optional name, email, metadata. Contacts can be added to lists for campaigns.",
|
|
355
|
+
{
|
|
356
|
+
phoneNumber: z.string().describe("Phone number in E.164 format (+14155551234)"),
|
|
357
|
+
name: z.string().optional().describe("Contact name"),
|
|
358
|
+
email: z.string().optional().describe("Contact email address"),
|
|
359
|
+
metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata")
|
|
360
|
+
},
|
|
361
|
+
async ({ phoneNumber, name, email, metadata }) => {
|
|
362
|
+
try {
|
|
363
|
+
const body = { phone_number: phoneNumber };
|
|
364
|
+
if (name) body.name = name;
|
|
365
|
+
if (email) body.email = email;
|
|
366
|
+
if (metadata) body.metadata = metadata;
|
|
367
|
+
return ok(await api2("POST", "/contacts", body));
|
|
368
|
+
} catch (e) {
|
|
369
|
+
return err(e);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
server2.tool(
|
|
374
|
+
"list_contacts",
|
|
375
|
+
"List contacts with optional search and pagination. Search matches name, email, and phone number.",
|
|
376
|
+
{
|
|
377
|
+
limit: z.number().optional().describe("Contacts to return (1-100, default 50)"),
|
|
378
|
+
offset: z.number().optional().describe("Pagination offset"),
|
|
379
|
+
search: z.string().optional().describe("Search by name, email, or phone number"),
|
|
380
|
+
listId: z.string().optional().describe("Filter by contact list ID")
|
|
381
|
+
},
|
|
382
|
+
async ({ limit, offset, search, listId }) => {
|
|
383
|
+
try {
|
|
384
|
+
return ok(
|
|
385
|
+
await api2("GET", "/contacts", void 0, {
|
|
386
|
+
limit: limit?.toString(),
|
|
387
|
+
offset: offset?.toString(),
|
|
388
|
+
search,
|
|
389
|
+
list_id: listId
|
|
390
|
+
})
|
|
391
|
+
);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
return err(e);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
server2.tool(
|
|
398
|
+
"get_contact",
|
|
399
|
+
"Get a contact by ID including their list memberships and metadata.",
|
|
400
|
+
{ contactId: z.string().describe("The contact ID") },
|
|
401
|
+
async ({ contactId }) => {
|
|
402
|
+
try {
|
|
403
|
+
return ok(await api2("GET", `/contacts/${contactId}`));
|
|
404
|
+
} catch (e) {
|
|
405
|
+
return err(e);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
server2.tool(
|
|
410
|
+
"update_contact",
|
|
411
|
+
"Update a contact's name, email, or metadata. Only provided fields are changed.",
|
|
412
|
+
{
|
|
413
|
+
contactId: z.string().describe("The contact ID"),
|
|
414
|
+
name: z.string().optional().describe("Updated name"),
|
|
415
|
+
email: z.string().optional().describe("Updated email"),
|
|
416
|
+
metadata: z.record(z.string(), z.any()).optional().describe("Updated metadata (replaces existing)")
|
|
417
|
+
},
|
|
418
|
+
async ({ contactId, name, email, metadata }) => {
|
|
419
|
+
try {
|
|
420
|
+
const body = {};
|
|
421
|
+
if (name !== void 0) body.name = name;
|
|
422
|
+
if (email !== void 0) body.email = email;
|
|
423
|
+
if (metadata) body.metadata = metadata;
|
|
424
|
+
return ok(await api2("PATCH", `/contacts/${contactId}`, body));
|
|
425
|
+
} catch (e) {
|
|
426
|
+
return err(e);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
server2.tool(
|
|
431
|
+
"delete_contact",
|
|
432
|
+
"Delete a contact by ID. Removes the contact from all lists. Does not delete messages sent to this contact.",
|
|
433
|
+
{ contactId: z.string().describe("The contact ID to delete") },
|
|
434
|
+
async ({ contactId }) => {
|
|
435
|
+
try {
|
|
436
|
+
return ok(await api2("DELETE", `/contacts/${contactId}`));
|
|
437
|
+
} catch (e) {
|
|
438
|
+
return err(e);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
server2.tool(
|
|
443
|
+
"import_contacts",
|
|
444
|
+
"Bulk import contacts from an array. Optionally add all imported contacts to a list. Returns created/updated/skipped counts.",
|
|
445
|
+
{
|
|
446
|
+
contacts: z.array(z.object({
|
|
447
|
+
phone: z.string().describe("Phone in E.164 format"),
|
|
448
|
+
name: z.string().optional().describe("Contact name"),
|
|
449
|
+
email: z.string().optional().describe("Contact email")
|
|
450
|
+
})).describe("Array of contacts to import (max 10000)"),
|
|
451
|
+
listId: z.string().optional().describe("Add all imported contacts to this list")
|
|
452
|
+
},
|
|
453
|
+
async ({ contacts, listId }) => {
|
|
454
|
+
try {
|
|
455
|
+
const body = { contacts };
|
|
456
|
+
if (listId) body.listId = listId;
|
|
457
|
+
return ok(await api2("POST", "/contacts/import", body));
|
|
458
|
+
} catch (e) {
|
|
459
|
+
return err(e);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
server2.tool(
|
|
464
|
+
"create_contact_list",
|
|
465
|
+
"Create a contact list for organizing contacts and targeting campaigns.",
|
|
466
|
+
{
|
|
467
|
+
name: z.string().describe("List name (e.g., 'VIP Customers', 'Newsletter')"),
|
|
468
|
+
description: z.string().optional().describe("List description")
|
|
469
|
+
},
|
|
470
|
+
async ({ name, description }) => {
|
|
471
|
+
try {
|
|
472
|
+
const body = { name };
|
|
473
|
+
if (description) body.description = description;
|
|
474
|
+
return ok(await api2("POST", "/contact-lists", body));
|
|
475
|
+
} catch (e) {
|
|
476
|
+
return err(e);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
);
|
|
480
|
+
server2.tool(
|
|
481
|
+
"list_contact_lists",
|
|
482
|
+
"List all contact lists with their contact counts.",
|
|
483
|
+
{},
|
|
484
|
+
async () => {
|
|
485
|
+
try {
|
|
486
|
+
return ok(await api2("GET", "/contact-lists"));
|
|
487
|
+
} catch (e) {
|
|
488
|
+
return err(e);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
server2.tool(
|
|
493
|
+
"get_contact_list",
|
|
494
|
+
"Get a contact list by ID with its members. Use limit/offset to paginate through members.",
|
|
495
|
+
{
|
|
496
|
+
listId: z.string().describe("The contact list ID"),
|
|
497
|
+
limit: z.number().optional().describe("Max contacts to include (default 50)"),
|
|
498
|
+
offset: z.number().optional().describe("Pagination offset for contacts")
|
|
499
|
+
},
|
|
500
|
+
async ({ listId, limit, offset }) => {
|
|
501
|
+
try {
|
|
502
|
+
return ok(
|
|
503
|
+
await api2("GET", `/contact-lists/${listId}`, void 0, {
|
|
504
|
+
limit: limit?.toString(),
|
|
505
|
+
offset: offset?.toString()
|
|
506
|
+
})
|
|
507
|
+
);
|
|
508
|
+
} catch (e) {
|
|
509
|
+
return err(e);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
server2.tool(
|
|
514
|
+
"update_contact_list",
|
|
515
|
+
"Update a contact list's name or description.",
|
|
516
|
+
{
|
|
517
|
+
listId: z.string().describe("The contact list ID"),
|
|
518
|
+
name: z.string().optional().describe("Updated name"),
|
|
519
|
+
description: z.string().optional().describe("Updated description")
|
|
520
|
+
},
|
|
521
|
+
async ({ listId, name, description }) => {
|
|
522
|
+
try {
|
|
523
|
+
const body = {};
|
|
524
|
+
if (name) body.name = name;
|
|
525
|
+
if (description !== void 0) body.description = description;
|
|
526
|
+
return ok(await api2("PATCH", `/contact-lists/${listId}`, body));
|
|
527
|
+
} catch (e) {
|
|
528
|
+
return err(e);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
server2.tool(
|
|
533
|
+
"delete_contact_list",
|
|
534
|
+
"Delete a contact list. Contacts in the list are not deleted, only the list grouping is removed.",
|
|
535
|
+
{ listId: z.string().describe("The contact list ID to delete") },
|
|
536
|
+
async ({ listId }) => {
|
|
537
|
+
try {
|
|
538
|
+
return ok(await api2("DELETE", `/contact-lists/${listId}`));
|
|
539
|
+
} catch (e) {
|
|
540
|
+
return err(e);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
server2.tool(
|
|
545
|
+
"add_list_contacts",
|
|
546
|
+
"Add one or more contacts to a contact list.",
|
|
547
|
+
{
|
|
548
|
+
listId: z.string().describe("The contact list ID"),
|
|
549
|
+
contactIds: z.array(z.string()).describe("Array of contact IDs to add to the list")
|
|
550
|
+
},
|
|
551
|
+
async ({ listId, contactIds }) => {
|
|
552
|
+
try {
|
|
553
|
+
return ok(await api2("POST", `/contact-lists/${listId}/contacts`, { contact_ids: contactIds }));
|
|
554
|
+
} catch (e) {
|
|
555
|
+
return err(e);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
server2.tool(
|
|
560
|
+
"remove_list_contact",
|
|
561
|
+
"Remove a single contact from a contact list. The contact itself is not deleted.",
|
|
562
|
+
{
|
|
563
|
+
listId: z.string().describe("The contact list ID"),
|
|
564
|
+
contactId: z.string().describe("The contact ID to remove from the list")
|
|
565
|
+
},
|
|
566
|
+
async ({ listId, contactId }) => {
|
|
567
|
+
try {
|
|
568
|
+
return ok(await api2("DELETE", `/contact-lists/${listId}/contacts/${contactId}`));
|
|
569
|
+
} catch (e) {
|
|
570
|
+
return err(e);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
server2.tool(
|
|
575
|
+
"create_campaign",
|
|
576
|
+
"Create a campaign to send bulk SMS to contact lists. Created as a draft \u2014 preview and send separately. Supports {{variables}} in message text.",
|
|
577
|
+
{
|
|
578
|
+
name: z.string().describe("Campaign name"),
|
|
579
|
+
text: z.string().optional().describe("Message text with optional {{variables}}"),
|
|
580
|
+
templateId: z.string().optional().describe("Template ID to use instead of inline text"),
|
|
581
|
+
targetListId: z.string().optional().describe("Contact list ID to send to")
|
|
582
|
+
},
|
|
583
|
+
async ({ name, text, templateId, targetListId }) => {
|
|
584
|
+
try {
|
|
585
|
+
const body = { name };
|
|
586
|
+
if (text) body.messageText = text;
|
|
587
|
+
if (templateId) body.templateId = templateId;
|
|
588
|
+
if (targetListId) body.targetListId = targetListId;
|
|
589
|
+
return ok(await api2("POST", "/campaigns", body));
|
|
590
|
+
} catch (e) {
|
|
591
|
+
return err(e);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
);
|
|
595
|
+
server2.tool(
|
|
596
|
+
"list_campaigns",
|
|
597
|
+
"List campaigns with optional filtering by status.",
|
|
598
|
+
{
|
|
599
|
+
limit: z.number().optional().describe("Campaigns to return (1-100, default 50)"),
|
|
600
|
+
offset: z.number().optional().describe("Pagination offset"),
|
|
601
|
+
status: z.enum(["draft", "scheduled", "sending", "completed", "cancelled", "failed"]).optional().describe("Filter by campaign status")
|
|
602
|
+
},
|
|
603
|
+
async ({ limit, offset, status }) => {
|
|
604
|
+
try {
|
|
605
|
+
return ok(
|
|
606
|
+
await api2("GET", "/campaigns", void 0, {
|
|
607
|
+
limit: limit?.toString(),
|
|
608
|
+
offset: offset?.toString(),
|
|
609
|
+
status
|
|
610
|
+
})
|
|
611
|
+
);
|
|
612
|
+
} catch (e) {
|
|
613
|
+
return err(e);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
);
|
|
617
|
+
server2.tool(
|
|
618
|
+
"get_campaign",
|
|
619
|
+
"Get a campaign by ID with delivery statistics (sent, delivered, failed counts).",
|
|
620
|
+
{ campaignId: z.string().describe("The campaign ID") },
|
|
621
|
+
async ({ campaignId }) => {
|
|
622
|
+
try {
|
|
623
|
+
return ok(await api2("GET", `/campaigns/${campaignId}`));
|
|
624
|
+
} catch (e) {
|
|
625
|
+
return err(e);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
server2.tool(
|
|
630
|
+
"update_campaign",
|
|
631
|
+
"Update a campaign's name, text, or target list. Only draft and scheduled campaigns can be updated.",
|
|
632
|
+
{
|
|
633
|
+
campaignId: z.string().describe("The campaign ID"),
|
|
634
|
+
name: z.string().optional().describe("Updated campaign name"),
|
|
635
|
+
text: z.string().optional().describe("Updated message text"),
|
|
636
|
+
templateId: z.string().optional().describe("Updated template ID"),
|
|
637
|
+
targetListId: z.string().optional().describe("Updated target list ID")
|
|
638
|
+
},
|
|
639
|
+
async ({ campaignId, name, text, templateId, targetListId }) => {
|
|
640
|
+
try {
|
|
641
|
+
const body = {};
|
|
642
|
+
if (name) body.name = name;
|
|
643
|
+
if (text) body.messageText = text;
|
|
644
|
+
if (templateId) body.templateId = templateId;
|
|
645
|
+
if (targetListId) body.targetListId = targetListId;
|
|
646
|
+
return ok(await api2("PATCH", `/campaigns/${campaignId}`, body));
|
|
647
|
+
} catch (e) {
|
|
648
|
+
return err(e);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
);
|
|
652
|
+
server2.tool(
|
|
653
|
+
"delete_campaign",
|
|
654
|
+
"Delete a campaign. Only draft and cancelled campaigns can be deleted.",
|
|
655
|
+
{ campaignId: z.string().describe("The campaign ID to delete") },
|
|
656
|
+
async ({ campaignId }) => {
|
|
657
|
+
try {
|
|
658
|
+
return ok(await api2("DELETE", `/campaigns/${campaignId}`));
|
|
659
|
+
} catch (e) {
|
|
660
|
+
return err(e);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
);
|
|
664
|
+
server2.tool(
|
|
665
|
+
"preview_campaign",
|
|
666
|
+
"Preview a campaign before sending. Returns recipient count, estimated credit cost, and whether you have enough credits.",
|
|
667
|
+
{ campaignId: z.string().describe("The campaign ID to preview") },
|
|
668
|
+
async ({ campaignId }) => {
|
|
669
|
+
try {
|
|
670
|
+
return ok(await api2("GET", `/campaigns/${campaignId}/preview`));
|
|
671
|
+
} catch (e) {
|
|
672
|
+
return err(e);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
);
|
|
676
|
+
server2.tool(
|
|
677
|
+
"send_campaign",
|
|
678
|
+
"Send a campaign immediately to all recipients in its target lists. Credits are deducted at send time. Preview first to check costs.",
|
|
679
|
+
{ campaignId: z.string().describe("The campaign ID to send") },
|
|
680
|
+
async ({ campaignId }) => {
|
|
681
|
+
try {
|
|
682
|
+
return ok(await api2("POST", `/campaigns/${campaignId}/send`));
|
|
683
|
+
} catch (e) {
|
|
684
|
+
return err(e);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
server2.tool(
|
|
689
|
+
"schedule_campaign",
|
|
690
|
+
"Schedule a campaign for future delivery at a specific date and time.",
|
|
691
|
+
{
|
|
692
|
+
campaignId: z.string().describe("The campaign ID to schedule"),
|
|
693
|
+
scheduledAt: z.string().describe("ISO 8601 datetime for delivery"),
|
|
694
|
+
timezone: z.string().optional().describe("Timezone (e.g., 'America/New_York')")
|
|
695
|
+
},
|
|
696
|
+
async ({ campaignId, scheduledAt, timezone }) => {
|
|
697
|
+
try {
|
|
698
|
+
const body = { scheduledAt };
|
|
699
|
+
if (timezone) body.timezone = timezone;
|
|
700
|
+
return ok(await api2("POST", `/campaigns/${campaignId}/schedule`, body));
|
|
701
|
+
} catch (e) {
|
|
702
|
+
return err(e);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
);
|
|
706
|
+
server2.tool(
|
|
707
|
+
"cancel_campaign",
|
|
708
|
+
"Cancel a scheduled campaign before it sends. Credits reserved for the campaign are refunded.",
|
|
709
|
+
{ campaignId: z.string().describe("The campaign ID to cancel") },
|
|
710
|
+
async ({ campaignId }) => {
|
|
711
|
+
try {
|
|
712
|
+
return ok(await api2("POST", `/campaigns/${campaignId}/cancel`));
|
|
713
|
+
} catch (e) {
|
|
714
|
+
return err(e);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
);
|
|
718
|
+
server2.tool(
|
|
719
|
+
"clone_campaign",
|
|
720
|
+
"Clone a campaign to create a new draft copy with the same settings. Useful for recurring or A/B campaigns.",
|
|
721
|
+
{ campaignId: z.string().describe("The campaign ID to clone") },
|
|
722
|
+
async ({ campaignId }) => {
|
|
723
|
+
try {
|
|
724
|
+
return ok(await api2("POST", `/campaigns/${campaignId}/clone`));
|
|
725
|
+
} catch (e) {
|
|
726
|
+
return err(e);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
);
|
|
730
|
+
server2.tool(
|
|
731
|
+
"create_template",
|
|
732
|
+
"Create an SMS template with {{variable}} placeholders. Templates can be used with campaigns and the Verify API.",
|
|
733
|
+
{
|
|
734
|
+
name: z.string().describe("Template name"),
|
|
735
|
+
text: z.string().describe("Template text with {{variable}} placeholders")
|
|
736
|
+
},
|
|
737
|
+
async ({ name, text }) => {
|
|
738
|
+
try {
|
|
739
|
+
return ok(await api2("POST", "/templates", { name, text }));
|
|
740
|
+
} catch (e) {
|
|
741
|
+
return err(e);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
);
|
|
745
|
+
server2.tool(
|
|
746
|
+
"list_templates",
|
|
747
|
+
"List all templates (custom and preset) with their status and variable definitions.",
|
|
748
|
+
{
|
|
749
|
+
limit: z.number().optional().describe("Templates to return (default 50)"),
|
|
750
|
+
offset: z.number().optional().describe("Pagination offset")
|
|
751
|
+
},
|
|
752
|
+
async ({ limit, offset }) => {
|
|
753
|
+
try {
|
|
754
|
+
return ok(
|
|
755
|
+
await api2("GET", "/templates", void 0, {
|
|
756
|
+
limit: limit?.toString(),
|
|
757
|
+
offset: offset?.toString()
|
|
758
|
+
})
|
|
759
|
+
);
|
|
760
|
+
} catch (e) {
|
|
761
|
+
return err(e);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
);
|
|
765
|
+
server2.tool(
|
|
766
|
+
"get_template",
|
|
767
|
+
"Get a template by ID including its variable definitions and publish status.",
|
|
768
|
+
{ templateId: z.string().describe("The template ID") },
|
|
769
|
+
async ({ templateId }) => {
|
|
770
|
+
try {
|
|
771
|
+
return ok(await api2("GET", `/templates/${templateId}`));
|
|
772
|
+
} catch (e) {
|
|
773
|
+
return err(e);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
);
|
|
777
|
+
server2.tool(
|
|
778
|
+
"update_template",
|
|
779
|
+
"Update a template's name or text.",
|
|
780
|
+
{
|
|
781
|
+
templateId: z.string().describe("The template ID"),
|
|
782
|
+
name: z.string().optional().describe("Updated template name"),
|
|
783
|
+
text: z.string().optional().describe("Updated template text")
|
|
784
|
+
},
|
|
785
|
+
async ({ templateId, name, text }) => {
|
|
786
|
+
try {
|
|
787
|
+
const body = {};
|
|
788
|
+
if (name) body.name = name;
|
|
789
|
+
if (text) body.text = text;
|
|
790
|
+
return ok(await api2("PATCH", `/templates/${templateId}`, body));
|
|
791
|
+
} catch (e) {
|
|
792
|
+
return err(e);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
);
|
|
796
|
+
server2.tool(
|
|
797
|
+
"delete_template",
|
|
798
|
+
"Delete a custom template. Preset templates cannot be deleted.",
|
|
799
|
+
{ templateId: z.string().describe("The template ID to delete") },
|
|
800
|
+
async ({ templateId }) => {
|
|
801
|
+
try {
|
|
802
|
+
return ok(await api2("DELETE", `/templates/${templateId}`));
|
|
803
|
+
} catch (e) {
|
|
804
|
+
return err(e);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
);
|
|
808
|
+
server2.tool(
|
|
809
|
+
"publish_template",
|
|
810
|
+
"Publish a template, making it available for use with the Verify API and campaigns.",
|
|
811
|
+
{ templateId: z.string().describe("The template ID to publish") },
|
|
812
|
+
async ({ templateId }) => {
|
|
813
|
+
try {
|
|
814
|
+
return ok(await api2("POST", `/templates/${templateId}/publish`));
|
|
815
|
+
} catch (e) {
|
|
816
|
+
return err(e);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
);
|
|
820
|
+
server2.tool(
|
|
821
|
+
"preview_template",
|
|
822
|
+
"Preview a template with sample variable values to see the interpolated result.",
|
|
823
|
+
{
|
|
824
|
+
templateId: z.string().describe("The template ID to preview"),
|
|
825
|
+
variables: z.record(z.string(), z.string()).optional().describe("Variable values (e.g., { app_name: 'MyApp', code: '123456' })")
|
|
826
|
+
},
|
|
827
|
+
async ({ templateId, variables }) => {
|
|
828
|
+
try {
|
|
829
|
+
const body = {};
|
|
830
|
+
if (variables) body.variables = variables;
|
|
831
|
+
return ok(await api2("POST", `/templates/${templateId}/preview`, body));
|
|
832
|
+
} catch (e) {
|
|
833
|
+
return err(e);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
);
|
|
837
|
+
server2.tool(
|
|
838
|
+
"list_template_presets",
|
|
839
|
+
"List system preset templates (OTP, 2FA, login, etc.) that can be used as-is or cloned.",
|
|
840
|
+
{},
|
|
841
|
+
async () => {
|
|
842
|
+
try {
|
|
843
|
+
return ok(await api2("GET", "/templates/presets"));
|
|
844
|
+
} catch (e) {
|
|
845
|
+
return err(e);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
server2.tool(
|
|
850
|
+
"create_label",
|
|
851
|
+
"Create a label for categorizing conversations and messages. Labels have a name and optional color.",
|
|
852
|
+
{
|
|
853
|
+
name: z.string().describe("Label name (e.g., 'urgent', 'vip', 'follow-up')"),
|
|
854
|
+
color: z.string().optional().describe("Hex color code (default: #6b7280)"),
|
|
855
|
+
description: z.string().optional().describe("Label description")
|
|
856
|
+
},
|
|
857
|
+
async ({ name, color, description }) => {
|
|
858
|
+
try {
|
|
859
|
+
const body = { name };
|
|
860
|
+
if (color) body.color = color;
|
|
861
|
+
if (description) body.description = description;
|
|
862
|
+
return ok(await api2("POST", "/labels", body));
|
|
863
|
+
} catch (e) {
|
|
864
|
+
return err(e);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
);
|
|
868
|
+
server2.tool(
|
|
869
|
+
"list_labels",
|
|
870
|
+
"List all labels available in your workspace.",
|
|
871
|
+
{},
|
|
872
|
+
async () => {
|
|
873
|
+
try {
|
|
874
|
+
return ok(await api2("GET", "/labels"));
|
|
875
|
+
} catch (e) {
|
|
876
|
+
return err(e);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
);
|
|
880
|
+
server2.tool(
|
|
881
|
+
"add_conversation_label",
|
|
882
|
+
"Add one or more labels to a conversation for categorization.",
|
|
883
|
+
{
|
|
884
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
885
|
+
labelIds: z.array(z.string()).describe("Array of label IDs to add")
|
|
886
|
+
},
|
|
887
|
+
async ({ conversationId, labelIds }) => {
|
|
888
|
+
try {
|
|
889
|
+
return ok(await api2("POST", `/conversations/${conversationId}/labels`, { labelIds }));
|
|
890
|
+
} catch (e) {
|
|
891
|
+
return err(e);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
server2.tool(
|
|
896
|
+
"remove_conversation_label",
|
|
897
|
+
"Remove a label from a conversation.",
|
|
898
|
+
{
|
|
899
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
900
|
+
labelId: z.string().describe("The label ID to remove")
|
|
901
|
+
},
|
|
902
|
+
async ({ conversationId, labelId }) => {
|
|
903
|
+
try {
|
|
904
|
+
return ok(await api2("DELETE", `/conversations/${conversationId}/labels/${labelId}`));
|
|
905
|
+
} catch (e) {
|
|
906
|
+
return err(e);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
);
|
|
910
|
+
server2.tool(
|
|
911
|
+
"list_rules",
|
|
912
|
+
"List auto-label rules that automatically tag conversations based on AI-detected intent and sentiment.",
|
|
913
|
+
{},
|
|
914
|
+
async () => {
|
|
915
|
+
try {
|
|
916
|
+
return ok(await api2("GET", "/rules"));
|
|
917
|
+
} catch (e) {
|
|
918
|
+
return err(e);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
);
|
|
922
|
+
server2.tool(
|
|
923
|
+
"create_rule",
|
|
924
|
+
"Create an auto-label rule. Rules automatically apply labels to conversations when conditions match.",
|
|
925
|
+
{
|
|
926
|
+
name: z.string().describe("Rule name"),
|
|
927
|
+
conditions: z.object({
|
|
928
|
+
intent: z.union([z.string(), z.array(z.string())]).optional().describe("Intent(s) to match"),
|
|
929
|
+
sentiment: z.union([z.string(), z.array(z.string())]).optional().describe("Sentiment(s) to match")
|
|
930
|
+
}).describe("Conditions that trigger the rule"),
|
|
931
|
+
actions: z.object({
|
|
932
|
+
addLabels: z.array(z.string()).describe("Label IDs to add when rule matches"),
|
|
933
|
+
closeConversation: z.boolean().optional().describe("Automatically close the conversation")
|
|
934
|
+
}).describe("Actions to take when conditions match"),
|
|
935
|
+
priority: z.number().optional().describe("Rule priority (lower runs first)")
|
|
936
|
+
},
|
|
937
|
+
async ({ name, conditions, actions, priority }) => {
|
|
938
|
+
try {
|
|
939
|
+
const body = { name, conditions, actions };
|
|
940
|
+
if (priority !== void 0) body.priority = priority;
|
|
941
|
+
return ok(await api2("POST", "/rules", body));
|
|
942
|
+
} catch (e) {
|
|
943
|
+
return err(e);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
);
|
|
947
|
+
server2.tool(
|
|
948
|
+
"update_rule",
|
|
949
|
+
"Update an auto-label rule's name, conditions, actions, or enabled status.",
|
|
950
|
+
{
|
|
951
|
+
ruleId: z.string().describe("The rule ID"),
|
|
952
|
+
name: z.string().optional().describe("Updated rule name"),
|
|
953
|
+
conditions: z.object({
|
|
954
|
+
intent: z.union([z.string(), z.array(z.string())]).optional(),
|
|
955
|
+
sentiment: z.union([z.string(), z.array(z.string())]).optional()
|
|
956
|
+
}).optional().describe("Updated conditions"),
|
|
957
|
+
actions: z.object({
|
|
958
|
+
addLabels: z.array(z.string()),
|
|
959
|
+
closeConversation: z.boolean().optional()
|
|
960
|
+
}).optional().describe("Updated actions"),
|
|
961
|
+
enabled: z.boolean().optional().describe("Enable or disable the rule")
|
|
962
|
+
},
|
|
963
|
+
async ({ ruleId, name, conditions, actions, enabled }) => {
|
|
964
|
+
try {
|
|
965
|
+
const body = {};
|
|
966
|
+
if (name) body.name = name;
|
|
967
|
+
if (conditions) body.conditions = conditions;
|
|
968
|
+
if (actions) body.actions = actions;
|
|
969
|
+
if (enabled !== void 0) body.enabled = enabled;
|
|
970
|
+
return ok(await api2("PATCH", `/rules/${ruleId}`, body));
|
|
971
|
+
} catch (e) {
|
|
972
|
+
return err(e);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
);
|
|
976
|
+
server2.tool(
|
|
977
|
+
"delete_rule",
|
|
978
|
+
"Delete an auto-label rule. Existing labels already applied by this rule are not removed.",
|
|
979
|
+
{ ruleId: z.string().describe("The rule ID to delete") },
|
|
980
|
+
async ({ ruleId }) => {
|
|
981
|
+
try {
|
|
982
|
+
return ok(await api2("DELETE", `/rules/${ruleId}`));
|
|
983
|
+
} catch (e) {
|
|
984
|
+
return err(e);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
);
|
|
988
|
+
server2.tool(
|
|
989
|
+
"create_draft",
|
|
990
|
+
"Create a message draft for human review before sending. The draft must be approved before it becomes a real SMS.",
|
|
991
|
+
{
|
|
992
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
993
|
+
text: z.string().describe("Draft message text"),
|
|
994
|
+
source: z.string().optional().describe("Source of the draft (default: 'ai')")
|
|
995
|
+
},
|
|
996
|
+
async ({ conversationId, text, source }) => {
|
|
997
|
+
try {
|
|
998
|
+
const body = { conversationId, text };
|
|
999
|
+
if (source) body.source = source;
|
|
1000
|
+
return ok(await api2("POST", "/drafts", body));
|
|
1001
|
+
} catch (e) {
|
|
1002
|
+
return err(e);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
);
|
|
1006
|
+
server2.tool(
|
|
1007
|
+
"list_drafts",
|
|
1008
|
+
"List message drafts, optionally filtered by conversation or status.",
|
|
1009
|
+
{
|
|
1010
|
+
conversationId: z.string().optional().describe("Filter by conversation ID"),
|
|
1011
|
+
status: z.enum(["pending", "approved", "rejected", "sent", "failed"]).optional().describe("Filter by status")
|
|
1012
|
+
},
|
|
1013
|
+
async ({ conversationId, status }) => {
|
|
1014
|
+
try {
|
|
1015
|
+
return ok(
|
|
1016
|
+
await api2("GET", "/drafts", void 0, {
|
|
1017
|
+
conversation_id: conversationId,
|
|
1018
|
+
status
|
|
1019
|
+
})
|
|
1020
|
+
);
|
|
1021
|
+
} catch (e) {
|
|
1022
|
+
return err(e);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
);
|
|
1026
|
+
server2.tool(
|
|
1027
|
+
"approve_draft",
|
|
1028
|
+
"Approve a pending draft and send it as a real SMS message. Runs compliance checks and deducts credits at approval time.",
|
|
1029
|
+
{ draftId: z.string().describe("The draft ID to approve") },
|
|
1030
|
+
async ({ draftId }) => {
|
|
1031
|
+
try {
|
|
1032
|
+
return ok(await api2("POST", `/drafts/${draftId}/approve`));
|
|
1033
|
+
} catch (e) {
|
|
1034
|
+
return err(e);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
);
|
|
1038
|
+
server2.tool(
|
|
1039
|
+
"reject_draft",
|
|
1040
|
+
"Reject a pending draft with an optional reason. The message will not be sent.",
|
|
1041
|
+
{
|
|
1042
|
+
draftId: z.string().describe("The draft ID to reject"),
|
|
1043
|
+
reason: z.string().optional().describe("Reason for rejection")
|
|
1044
|
+
},
|
|
1045
|
+
async ({ draftId, reason }) => {
|
|
1046
|
+
try {
|
|
1047
|
+
const body = {};
|
|
1048
|
+
if (reason) body.reason = reason;
|
|
1049
|
+
return ok(await api2("POST", `/drafts/${draftId}/reject`, body));
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
return err(e);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
);
|
|
1055
|
+
server2.tool(
|
|
1056
|
+
"create_webhook",
|
|
1057
|
+
"Create a webhook endpoint to receive real-time event notifications. Returns a signing secret (shown only once) for verifying payloads.",
|
|
1058
|
+
{
|
|
1059
|
+
url: z.string().describe("HTTPS endpoint URL to receive events"),
|
|
1060
|
+
events: z.array(z.string()).describe("Event types to subscribe to (e.g., ['message.delivered', 'message.failed'])"),
|
|
1061
|
+
description: z.string().optional().describe("Description of this webhook")
|
|
1062
|
+
},
|
|
1063
|
+
async ({ url, events, description }) => {
|
|
1064
|
+
try {
|
|
1065
|
+
const body = { url, events };
|
|
1066
|
+
if (description) body.description = description;
|
|
1067
|
+
return ok(await api2("POST", "/webhooks", body));
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
return err(e);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
);
|
|
1073
|
+
server2.tool(
|
|
1074
|
+
"list_webhooks",
|
|
1075
|
+
"List all webhook endpoints with their status and event subscriptions.",
|
|
1076
|
+
{},
|
|
1077
|
+
async () => {
|
|
1078
|
+
try {
|
|
1079
|
+
return ok(await api2("GET", "/webhooks"));
|
|
1080
|
+
} catch (e) {
|
|
1081
|
+
return err(e);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
);
|
|
1085
|
+
server2.tool(
|
|
1086
|
+
"get_webhook",
|
|
1087
|
+
"Get a webhook by ID with delivery statistics.",
|
|
1088
|
+
{ webhookId: z.string().describe("The webhook ID") },
|
|
1089
|
+
async ({ webhookId }) => {
|
|
1090
|
+
try {
|
|
1091
|
+
return ok(await api2("GET", `/webhooks/${webhookId}`));
|
|
1092
|
+
} catch (e) {
|
|
1093
|
+
return err(e);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
);
|
|
1097
|
+
server2.tool(
|
|
1098
|
+
"update_webhook",
|
|
1099
|
+
"Update a webhook's URL, events, description, or active status.",
|
|
1100
|
+
{
|
|
1101
|
+
webhookId: z.string().describe("The webhook ID"),
|
|
1102
|
+
url: z.string().optional().describe("Updated HTTPS endpoint URL"),
|
|
1103
|
+
events: z.array(z.string()).optional().describe("Updated event types"),
|
|
1104
|
+
description: z.string().optional().describe("Updated description"),
|
|
1105
|
+
isActive: z.boolean().optional().describe("Enable or disable the webhook")
|
|
1106
|
+
},
|
|
1107
|
+
async ({ webhookId, url, events, description, isActive }) => {
|
|
1108
|
+
try {
|
|
1109
|
+
const body = {};
|
|
1110
|
+
if (url) body.url = url;
|
|
1111
|
+
if (events) body.events = events;
|
|
1112
|
+
if (description !== void 0) body.description = description;
|
|
1113
|
+
if (isActive !== void 0) body.is_active = isActive;
|
|
1114
|
+
return ok(await api2("PATCH", `/webhooks/${webhookId}`, body));
|
|
1115
|
+
} catch (e) {
|
|
1116
|
+
return err(e);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
);
|
|
1120
|
+
server2.tool(
|
|
1121
|
+
"delete_webhook",
|
|
1122
|
+
"Delete a webhook endpoint. Stops all future event deliveries to this URL.",
|
|
1123
|
+
{ webhookId: z.string().describe("The webhook ID to delete") },
|
|
1124
|
+
async ({ webhookId }) => {
|
|
1125
|
+
try {
|
|
1126
|
+
return ok(await api2("DELETE", `/webhooks/${webhookId}`));
|
|
1127
|
+
} catch (e) {
|
|
1128
|
+
return err(e);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
);
|
|
1132
|
+
server2.tool(
|
|
1133
|
+
"test_webhook",
|
|
1134
|
+
"Send a test event to a webhook endpoint to verify it is reachable. Returns response status and latency.",
|
|
1135
|
+
{ webhookId: z.string().describe("The webhook ID to test") },
|
|
1136
|
+
async ({ webhookId }) => {
|
|
1137
|
+
try {
|
|
1138
|
+
return ok(await api2("POST", `/webhooks/${webhookId}/test`));
|
|
1139
|
+
} catch (e) {
|
|
1140
|
+
return err(e);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
);
|
|
1144
|
+
server2.tool(
|
|
1145
|
+
"list_webhook_deliveries",
|
|
1146
|
+
"Get delivery history for a webhook showing recent attempts, statuses, and response times.",
|
|
1147
|
+
{ webhookId: z.string().describe("The webhook ID") },
|
|
1148
|
+
async ({ webhookId }) => {
|
|
1149
|
+
try {
|
|
1150
|
+
return ok(await api2("GET", `/webhooks/${webhookId}/deliveries`));
|
|
1151
|
+
} catch (e) {
|
|
1152
|
+
return err(e);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
);
|
|
1156
|
+
server2.tool(
|
|
1157
|
+
"rotate_webhook_secret",
|
|
1158
|
+
"Rotate a webhook's signing secret. The old secret stops working immediately.",
|
|
1159
|
+
{ webhookId: z.string().describe("The webhook ID") },
|
|
1160
|
+
async ({ webhookId }) => {
|
|
1161
|
+
try {
|
|
1162
|
+
return ok(await api2("POST", `/webhooks/${webhookId}/rotate-secret`));
|
|
1163
|
+
} catch (e) {
|
|
1164
|
+
return err(e);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
);
|
|
1168
|
+
server2.tool(
|
|
1169
|
+
"list_webhook_event_types",
|
|
1170
|
+
"List all available webhook event types that can be subscribed to.",
|
|
1171
|
+
{},
|
|
1172
|
+
async () => {
|
|
1173
|
+
try {
|
|
1174
|
+
return ok(await api2("GET", "/webhooks/event-types"));
|
|
1175
|
+
} catch (e) {
|
|
1176
|
+
return err(e);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
);
|
|
1180
|
+
server2.tool(
|
|
1181
|
+
"send_otp",
|
|
1182
|
+
"Send an OTP verification code via SMS. Use for phone verification, 2FA, or identity confirmation. In sandbox mode (test API key), the code is returned in the response.",
|
|
1183
|
+
{
|
|
1184
|
+
to: z.string().describe("Phone number to verify in E.164 format"),
|
|
1185
|
+
appName: z.string().optional().describe("Your app/brand name shown in the SMS"),
|
|
1186
|
+
codeLength: z.number().optional().describe("Digits in the code (default 6)"),
|
|
1187
|
+
timeoutSecs: z.number().optional().describe("Code validity in seconds (default 300)"),
|
|
1188
|
+
templateId: z.string().optional().describe("Custom OTP template ID")
|
|
1189
|
+
},
|
|
1190
|
+
async ({ to, appName, codeLength, timeoutSecs, templateId }) => {
|
|
1191
|
+
try {
|
|
1192
|
+
const body = { to };
|
|
1193
|
+
if (appName) body.app_name = appName;
|
|
1194
|
+
if (codeLength) body.code_length = codeLength;
|
|
1195
|
+
if (timeoutSecs) body.timeout_secs = timeoutSecs;
|
|
1196
|
+
if (templateId) body.template_id = templateId;
|
|
1197
|
+
return ok(await api2("POST", "/verify", body));
|
|
1198
|
+
} catch (e) {
|
|
1199
|
+
return err(e);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
);
|
|
1203
|
+
server2.tool(
|
|
1204
|
+
"check_otp",
|
|
1205
|
+
"Verify an OTP code. Returns 'verified' (correct), 'invalid_code' (wrong), 'expired', or 'max_attempts_exceeded'.",
|
|
1206
|
+
{
|
|
1207
|
+
verificationId: z.string().describe("The verification ID from send_otp"),
|
|
1208
|
+
code: z.string().describe("The code entered by the user")
|
|
1209
|
+
},
|
|
1210
|
+
async ({ verificationId, code }) => {
|
|
1211
|
+
try {
|
|
1212
|
+
return ok(await api2("POST", `/verify/${verificationId}/check`, { code }));
|
|
1213
|
+
} catch (e) {
|
|
1214
|
+
return err(e);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
);
|
|
1218
|
+
server2.tool(
|
|
1219
|
+
"get_verification_status",
|
|
1220
|
+
"Check the current status of an OTP verification (pending, verified, expired, failed).",
|
|
1221
|
+
{ verificationId: z.string().describe("The verification ID") },
|
|
1222
|
+
async ({ verificationId }) => {
|
|
1223
|
+
try {
|
|
1224
|
+
return ok(await api2("GET", `/verify/${verificationId}`));
|
|
1225
|
+
} catch (e) {
|
|
1226
|
+
return err(e);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
);
|
|
1230
|
+
server2.tool(
|
|
1231
|
+
"resend_otp",
|
|
1232
|
+
"Resend an OTP verification code. Use when the original SMS was not received.",
|
|
1233
|
+
{ verificationId: z.string().describe("The verification ID from send_otp") },
|
|
1234
|
+
async ({ verificationId }) => {
|
|
1235
|
+
try {
|
|
1236
|
+
return ok(await api2("POST", `/verify/${verificationId}/resend`));
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
return err(e);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
);
|
|
1242
|
+
server2.tool(
|
|
1243
|
+
"list_verifications",
|
|
1244
|
+
"List recent OTP verifications with pagination.",
|
|
1245
|
+
{
|
|
1246
|
+
limit: z.number().optional().describe("Verifications to return (default 50)")
|
|
1247
|
+
},
|
|
1248
|
+
async ({ limit }) => {
|
|
1249
|
+
try {
|
|
1250
|
+
return ok(
|
|
1251
|
+
await api2("GET", "/verify", void 0, {
|
|
1252
|
+
limit: limit?.toString()
|
|
1253
|
+
})
|
|
1254
|
+
);
|
|
1255
|
+
} catch (e) {
|
|
1256
|
+
return err(e);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
);
|
|
1260
|
+
server2.tool(
|
|
1261
|
+
"create_verify_session",
|
|
1262
|
+
"Create a hosted verify session with a branded UI. Returns a URL to redirect the user to for phone verification. Zero frontend code needed.",
|
|
1263
|
+
{
|
|
1264
|
+
successUrl: z.string().describe("URL to redirect to after successful verification"),
|
|
1265
|
+
cancelUrl: z.string().optional().describe("URL to redirect to if user cancels"),
|
|
1266
|
+
brandName: z.string().optional().describe("Your brand name shown on the verify page"),
|
|
1267
|
+
brandColor: z.string().optional().describe("Brand color hex code (e.g., #4F46E5)"),
|
|
1268
|
+
metadata: z.record(z.string(), z.any()).optional().describe("Custom metadata to attach to the session")
|
|
1269
|
+
},
|
|
1270
|
+
async ({ successUrl, cancelUrl, brandName, brandColor, metadata }) => {
|
|
1271
|
+
try {
|
|
1272
|
+
const body = { success_url: successUrl };
|
|
1273
|
+
if (cancelUrl) body.cancel_url = cancelUrl;
|
|
1274
|
+
if (brandName) body.brand_name = brandName;
|
|
1275
|
+
if (brandColor) body.brand_color = brandColor;
|
|
1276
|
+
if (metadata) body.metadata = metadata;
|
|
1277
|
+
return ok(await api2("POST", "/verify/sessions", body));
|
|
1278
|
+
} catch (e) {
|
|
1279
|
+
return err(e);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
);
|
|
1283
|
+
server2.tool(
|
|
1284
|
+
"validate_verify_session",
|
|
1285
|
+
"Validate a session token returned after a user completes hosted verification. Returns the verified phone number.",
|
|
1286
|
+
{ token: z.string().describe("The session token from the success redirect URL") },
|
|
1287
|
+
async ({ token }) => {
|
|
1288
|
+
try {
|
|
1289
|
+
return ok(await api2("POST", "/verify/sessions/validate", { token }));
|
|
1290
|
+
} catch (e) {
|
|
1291
|
+
return err(e);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
);
|
|
1295
|
+
server2.tool(
|
|
1296
|
+
"get_credits",
|
|
1297
|
+
"Get current credit balance including reserved credits for scheduled messages.",
|
|
1298
|
+
{},
|
|
1299
|
+
async () => {
|
|
1300
|
+
try {
|
|
1301
|
+
return ok(await api2("GET", "/credits"));
|
|
1302
|
+
} catch (e) {
|
|
1303
|
+
return err(e);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
);
|
|
1307
|
+
server2.tool(
|
|
1308
|
+
"list_credit_transactions",
|
|
1309
|
+
"List credit transaction history showing purchases, usage, refunds, and transfers.",
|
|
1310
|
+
{
|
|
1311
|
+
limit: z.number().optional().describe("Transactions to return (default 50)")
|
|
1312
|
+
},
|
|
1313
|
+
async ({ limit }) => {
|
|
1314
|
+
try {
|
|
1315
|
+
return ok(
|
|
1316
|
+
await api2("GET", "/credits/transactions", void 0, {
|
|
1317
|
+
limit: limit?.toString()
|
|
1318
|
+
})
|
|
1319
|
+
);
|
|
1320
|
+
} catch (e) {
|
|
1321
|
+
return err(e);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
);
|
|
1325
|
+
server2.tool(
|
|
1326
|
+
"get_account",
|
|
1327
|
+
"Get account info: credit balance, phone number verification status, rate limits, and API key details.",
|
|
1328
|
+
{},
|
|
1329
|
+
async () => {
|
|
1330
|
+
try {
|
|
1331
|
+
return ok(await api2("GET", "/account"));
|
|
1332
|
+
} catch (e) {
|
|
1333
|
+
return err(e);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
);
|
|
1337
|
+
server2.tool(
|
|
1338
|
+
"generate_business_page",
|
|
1339
|
+
"Generate a hosted business landing page for verification. Returns a URL at sendly.live/biz/{slug}. Enterprise accounts only.",
|
|
1340
|
+
{
|
|
1341
|
+
businessName: z.string().describe("Business name"),
|
|
1342
|
+
useCase: z.string().optional().describe("Use case (e.g., Insurance Services, Appointment Reminders, 2FA)"),
|
|
1343
|
+
useCaseSummary: z.string().optional().describe("Brief description of what the business does"),
|
|
1344
|
+
contactEmail: z.string().optional().describe("Business contact email"),
|
|
1345
|
+
contactPhone: z.string().optional().describe("Business phone number"),
|
|
1346
|
+
businessAddress: z.string().optional().describe("City, State ZIP (e.g., Chicago, IL 60601)")
|
|
1347
|
+
},
|
|
1348
|
+
async (params) => {
|
|
1349
|
+
try {
|
|
1350
|
+
return ok(await api2("POST", "/enterprise/business-page/generate", params));
|
|
1351
|
+
} catch (e) {
|
|
1352
|
+
return err(e);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/index.ts
|
|
1359
|
+
var VERSION = "2.0.0";
|
|
8
1360
|
var API_KEY = process.env.SENDLY_API_KEY;
|
|
9
1361
|
var BASE_URL = process.env.SENDLY_BASE_URL || "https://sendly.live";
|
|
10
1362
|
if (!API_KEY) {
|
|
@@ -20,7 +1372,7 @@ if (!BASE_URL.startsWith("https://") && !BASE_URL.startsWith("http://localhost")
|
|
|
20
1372
|
process.exit(1);
|
|
21
1373
|
}
|
|
22
1374
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
23
|
-
var RATE_LIMIT_MAX =
|
|
1375
|
+
var RATE_LIMIT_MAX = 60;
|
|
24
1376
|
var rateLimitTokens = RATE_LIMIT_MAX;
|
|
25
1377
|
var rateLimitResetAt = Date.now() + RATE_LIMIT_WINDOW_MS;
|
|
26
1378
|
function checkRateLimit() {
|
|
@@ -66,490 +1418,10 @@ async function api(method, path, body, query) {
|
|
|
66
1418
|
}
|
|
67
1419
|
return data;
|
|
68
1420
|
}
|
|
69
|
-
function ok(data) {
|
|
70
|
-
return {
|
|
71
|
-
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
function err(error) {
|
|
75
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
-
return {
|
|
77
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
78
|
-
isError: true
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
1421
|
var server = new McpServer({
|
|
82
1422
|
name: "sendly",
|
|
83
1423
|
version: VERSION
|
|
84
1424
|
});
|
|
85
|
-
server
|
|
86
|
-
"send_sms",
|
|
87
|
-
"Send an SMS message to a phone number. Returns the message with delivery status. Use 'transactional' for alerts/OTP (bypasses quiet hours), 'marketing' for promotions.",
|
|
88
|
-
{
|
|
89
|
-
to: z.string().describe("Recipient phone number in E.164 format (+14155551234)"),
|
|
90
|
-
text: z.string().describe("Message text content"),
|
|
91
|
-
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)"),
|
|
92
|
-
metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata to attach")
|
|
93
|
-
},
|
|
94
|
-
async ({ to, text, messageType, metadata }) => {
|
|
95
|
-
try {
|
|
96
|
-
const body = { to, text };
|
|
97
|
-
if (messageType) body.messageType = messageType;
|
|
98
|
-
if (metadata) body.metadata = metadata;
|
|
99
|
-
return ok(await api("POST", "/messages", body));
|
|
100
|
-
} catch (e) {
|
|
101
|
-
return err(e);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
);
|
|
105
|
-
server.tool(
|
|
106
|
-
"list_messages",
|
|
107
|
-
"List sent and received SMS messages with pagination, ordered by creation date.",
|
|
108
|
-
{
|
|
109
|
-
limit: z.number().optional().describe("Messages to return (1-100, default 50)"),
|
|
110
|
-
offset: z.number().optional().describe("Pagination offset"),
|
|
111
|
-
status: z.enum(["queued", "sent", "delivered", "failed"]).optional().describe("Filter by delivery status")
|
|
112
|
-
},
|
|
113
|
-
async ({ limit, offset, status }) => {
|
|
114
|
-
try {
|
|
115
|
-
return ok(
|
|
116
|
-
await api("GET", "/messages", void 0, {
|
|
117
|
-
limit: limit?.toString(),
|
|
118
|
-
offset: offset?.toString(),
|
|
119
|
-
status
|
|
120
|
-
})
|
|
121
|
-
);
|
|
122
|
-
} catch (e) {
|
|
123
|
-
return err(e);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
);
|
|
127
|
-
server.tool(
|
|
128
|
-
"get_message",
|
|
129
|
-
"Get details of a specific SMS message including delivery status, timestamps, and metadata.",
|
|
130
|
-
{
|
|
131
|
-
messageId: z.string().describe("The message ID")
|
|
132
|
-
},
|
|
133
|
-
async ({ messageId }) => {
|
|
134
|
-
try {
|
|
135
|
-
return ok(await api("GET", `/messages/${messageId}`));
|
|
136
|
-
} catch (e) {
|
|
137
|
-
return err(e);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
);
|
|
141
|
-
server.tool(
|
|
142
|
-
"schedule_sms",
|
|
143
|
-
"Schedule an SMS for future delivery (5 minutes to 5 days from now). Credits are reserved immediately and refunded if cancelled.",
|
|
144
|
-
{
|
|
145
|
-
to: z.string().describe("Recipient phone number in E.164 format"),
|
|
146
|
-
text: z.string().describe("Message text content"),
|
|
147
|
-
scheduledAt: z.string().describe("ISO 8601 datetime for delivery (e.g., 2026-03-16T09:00:00Z)"),
|
|
148
|
-
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)")
|
|
149
|
-
},
|
|
150
|
-
async ({ to, text, scheduledAt, messageType }) => {
|
|
151
|
-
try {
|
|
152
|
-
const body = { to, text, scheduledAt };
|
|
153
|
-
if (messageType) body.messageType = messageType;
|
|
154
|
-
return ok(await api("POST", "/messages/schedule", body));
|
|
155
|
-
} catch (e) {
|
|
156
|
-
return err(e);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
);
|
|
160
|
-
server.tool(
|
|
161
|
-
"cancel_scheduled_message",
|
|
162
|
-
"Cancel a scheduled message before it sends. Credits are refunded automatically.",
|
|
163
|
-
{
|
|
164
|
-
messageId: z.string().describe("The scheduled message ID to cancel")
|
|
165
|
-
},
|
|
166
|
-
async ({ messageId }) => {
|
|
167
|
-
try {
|
|
168
|
-
return ok(await api("DELETE", `/messages/scheduled/${messageId}`));
|
|
169
|
-
} catch (e) {
|
|
170
|
-
return err(e);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
);
|
|
174
|
-
server.tool(
|
|
175
|
-
"list_conversations",
|
|
176
|
-
"List SMS conversation threads ordered by most recent activity. Each conversation groups all messages with a specific phone number.",
|
|
177
|
-
{
|
|
178
|
-
limit: z.number().optional().describe("Conversations to return (1-100, default 50)"),
|
|
179
|
-
offset: z.number().optional().describe("Pagination offset"),
|
|
180
|
-
status: z.enum(["active", "closed"]).optional().describe("Filter by conversation status")
|
|
181
|
-
},
|
|
182
|
-
async ({ limit, offset, status }) => {
|
|
183
|
-
try {
|
|
184
|
-
return ok(
|
|
185
|
-
await api("GET", "/conversations", void 0, {
|
|
186
|
-
limit: limit?.toString(),
|
|
187
|
-
offset: offset?.toString(),
|
|
188
|
-
status
|
|
189
|
-
})
|
|
190
|
-
);
|
|
191
|
-
} catch (e) {
|
|
192
|
-
return err(e);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
);
|
|
196
|
-
server.tool(
|
|
197
|
-
"get_conversation_context",
|
|
198
|
-
"Get LLM-ready formatted conversation context. Returns a pre-formatted text string with timestamped messages, AI classification, and business context \u2014 ready to paste into a prompt. More efficient than get_conversation for AI agents.",
|
|
199
|
-
{
|
|
200
|
-
conversationId: z.string().describe("The conversation ID"),
|
|
201
|
-
maxMessages: z.number().optional().describe("Max messages to include (default 20, max 50)")
|
|
202
|
-
},
|
|
203
|
-
async ({ conversationId, maxMessages }) => {
|
|
204
|
-
try {
|
|
205
|
-
return ok(
|
|
206
|
-
await api("GET", `/conversations/${conversationId}/context`, void 0, {
|
|
207
|
-
max_messages: maxMessages?.toString()
|
|
208
|
-
})
|
|
209
|
-
);
|
|
210
|
-
} catch (e) {
|
|
211
|
-
return err(e);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
);
|
|
215
|
-
server.tool(
|
|
216
|
-
"get_conversation",
|
|
217
|
-
"Get a conversation thread by ID. Set includeMessages=true to load the message history.",
|
|
218
|
-
{
|
|
219
|
-
conversationId: z.string().describe("The conversation ID"),
|
|
220
|
-
includeMessages: z.boolean().optional().describe("Include message history (default false)"),
|
|
221
|
-
messageLimit: z.number().optional().describe("Number of messages to include (default 50)")
|
|
222
|
-
},
|
|
223
|
-
async ({ conversationId, includeMessages, messageLimit }) => {
|
|
224
|
-
try {
|
|
225
|
-
return ok(
|
|
226
|
-
await api("GET", `/conversations/${conversationId}`, void 0, {
|
|
227
|
-
include_messages: includeMessages ? "true" : void 0,
|
|
228
|
-
message_limit: messageLimit?.toString()
|
|
229
|
-
})
|
|
230
|
-
);
|
|
231
|
-
} catch (e) {
|
|
232
|
-
return err(e);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
);
|
|
236
|
-
server.tool(
|
|
237
|
-
"reply_to_conversation",
|
|
238
|
-
"Send a reply within an existing conversation. The recipient is automatically set from the conversation's phone number.",
|
|
239
|
-
{
|
|
240
|
-
conversationId: z.string().describe("The conversation ID to reply in"),
|
|
241
|
-
text: z.string().describe("Reply message text"),
|
|
242
|
-
mediaUrls: z.array(z.string()).optional().describe("Media URLs for MMS")
|
|
243
|
-
},
|
|
244
|
-
async ({ conversationId, text, mediaUrls }) => {
|
|
245
|
-
try {
|
|
246
|
-
const body = { text };
|
|
247
|
-
if (mediaUrls?.length) body.mediaUrls = mediaUrls;
|
|
248
|
-
return ok(
|
|
249
|
-
await api("POST", `/conversations/${conversationId}/messages`, body)
|
|
250
|
-
);
|
|
251
|
-
} catch (e) {
|
|
252
|
-
return err(e);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
);
|
|
256
|
-
server.tool(
|
|
257
|
-
"update_conversation",
|
|
258
|
-
"Update a conversation's metadata or tags. Use metadata for custom key-value data, tags for categorization.",
|
|
259
|
-
{
|
|
260
|
-
conversationId: z.string().describe("The conversation ID"),
|
|
261
|
-
metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata"),
|
|
262
|
-
tags: z.array(z.string()).optional().describe("Tags for categorization (replaces existing tags)")
|
|
263
|
-
},
|
|
264
|
-
async ({ conversationId, metadata, tags }) => {
|
|
265
|
-
try {
|
|
266
|
-
const body = {};
|
|
267
|
-
if (metadata) body.metadata = metadata;
|
|
268
|
-
if (tags) body.tags = tags;
|
|
269
|
-
return ok(await api("PATCH", `/conversations/${conversationId}`, body));
|
|
270
|
-
} catch (e) {
|
|
271
|
-
return err(e);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
);
|
|
275
|
-
server.tool(
|
|
276
|
-
"close_conversation",
|
|
277
|
-
"Close a conversation. Closed conversations auto-reopen when a new inbound message arrives.",
|
|
278
|
-
{
|
|
279
|
-
conversationId: z.string().describe("The conversation ID to close")
|
|
280
|
-
},
|
|
281
|
-
async ({ conversationId }) => {
|
|
282
|
-
try {
|
|
283
|
-
return ok(await api("POST", `/conversations/${conversationId}/close`));
|
|
284
|
-
} catch (e) {
|
|
285
|
-
return err(e);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
);
|
|
289
|
-
server.tool(
|
|
290
|
-
"reopen_conversation",
|
|
291
|
-
"Reopen a previously closed conversation, setting its status back to active.",
|
|
292
|
-
{
|
|
293
|
-
conversationId: z.string().describe("The conversation ID to reopen")
|
|
294
|
-
},
|
|
295
|
-
async ({ conversationId }) => {
|
|
296
|
-
try {
|
|
297
|
-
return ok(await api("POST", `/conversations/${conversationId}/reopen`));
|
|
298
|
-
} catch (e) {
|
|
299
|
-
return err(e);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
);
|
|
303
|
-
server.tool(
|
|
304
|
-
"mark_conversation_read",
|
|
305
|
-
"Mark a conversation as read, resetting the unread count to zero.",
|
|
306
|
-
{
|
|
307
|
-
conversationId: z.string().describe("The conversation ID")
|
|
308
|
-
},
|
|
309
|
-
async ({ conversationId }) => {
|
|
310
|
-
try {
|
|
311
|
-
return ok(
|
|
312
|
-
await api("POST", `/conversations/${conversationId}/mark-read`)
|
|
313
|
-
);
|
|
314
|
-
} catch (e) {
|
|
315
|
-
return err(e);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
);
|
|
319
|
-
server.tool(
|
|
320
|
-
"get_suggested_replies",
|
|
321
|
-
"Get AI-generated reply suggestions for a conversation based on message history and context. Returns 2-3 suggested responses with different tones (professional, friendly, concise).",
|
|
322
|
-
{
|
|
323
|
-
conversationId: z.string().describe("The conversation ID to generate suggestions for")
|
|
324
|
-
},
|
|
325
|
-
async ({ conversationId }) => {
|
|
326
|
-
try {
|
|
327
|
-
return ok(
|
|
328
|
-
await api("POST", `/conversations/${conversationId}/suggest-replies`)
|
|
329
|
-
);
|
|
330
|
-
} catch (e) {
|
|
331
|
-
return err(e);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
);
|
|
335
|
-
server.tool(
|
|
336
|
-
"create_label",
|
|
337
|
-
"Create a label for categorizing conversations and messages. Labels have a name and optional color.",
|
|
338
|
-
{
|
|
339
|
-
name: z.string().describe("Label name (e.g., 'urgent', 'vip', 'follow-up')"),
|
|
340
|
-
color: z.string().optional().describe("Hex color code (default: #6b7280)"),
|
|
341
|
-
description: z.string().optional().describe("Label description")
|
|
342
|
-
},
|
|
343
|
-
async ({ name, color, description }) => {
|
|
344
|
-
try {
|
|
345
|
-
const body = { name };
|
|
346
|
-
if (color) body.color = color;
|
|
347
|
-
if (description) body.description = description;
|
|
348
|
-
return ok(await api("POST", "/labels", body));
|
|
349
|
-
} catch (e) {
|
|
350
|
-
return err(e);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
);
|
|
354
|
-
server.tool(
|
|
355
|
-
"list_labels",
|
|
356
|
-
"List all labels available in your workspace.",
|
|
357
|
-
{},
|
|
358
|
-
async () => {
|
|
359
|
-
try {
|
|
360
|
-
return ok(await api("GET", "/labels"));
|
|
361
|
-
} catch (e) {
|
|
362
|
-
return err(e);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
);
|
|
366
|
-
server.tool(
|
|
367
|
-
"add_conversation_label",
|
|
368
|
-
"Add one or more labels to a conversation for categorization.",
|
|
369
|
-
{
|
|
370
|
-
conversationId: z.string().describe("The conversation ID"),
|
|
371
|
-
labelIds: z.array(z.string()).describe("Array of label IDs to add")
|
|
372
|
-
},
|
|
373
|
-
async ({ conversationId, labelIds }) => {
|
|
374
|
-
try {
|
|
375
|
-
return ok(
|
|
376
|
-
await api("POST", `/conversations/${conversationId}/labels`, {
|
|
377
|
-
labelIds
|
|
378
|
-
})
|
|
379
|
-
);
|
|
380
|
-
} catch (e) {
|
|
381
|
-
return err(e);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
);
|
|
385
|
-
server.tool(
|
|
386
|
-
"remove_conversation_label",
|
|
387
|
-
"Remove a label from a conversation.",
|
|
388
|
-
{
|
|
389
|
-
conversationId: z.string().describe("The conversation ID"),
|
|
390
|
-
labelId: z.string().describe("The label ID to remove")
|
|
391
|
-
},
|
|
392
|
-
async ({ conversationId, labelId }) => {
|
|
393
|
-
try {
|
|
394
|
-
return ok(
|
|
395
|
-
await api("DELETE", `/conversations/${conversationId}/labels/${labelId}`)
|
|
396
|
-
);
|
|
397
|
-
} catch (e) {
|
|
398
|
-
return err(e);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
);
|
|
402
|
-
server.tool(
|
|
403
|
-
"create_draft",
|
|
404
|
-
"Create a message draft for human review before sending. The draft must be approved before it becomes a real SMS.",
|
|
405
|
-
{
|
|
406
|
-
conversationId: z.string().describe("The conversation ID"),
|
|
407
|
-
text: z.string().describe("Draft message text"),
|
|
408
|
-
source: z.string().optional().describe("Source of the draft (default: 'ai')")
|
|
409
|
-
},
|
|
410
|
-
async ({ conversationId, text, source }) => {
|
|
411
|
-
try {
|
|
412
|
-
const body = { conversationId, text };
|
|
413
|
-
if (source) body.source = source;
|
|
414
|
-
return ok(await api("POST", "/drafts", body));
|
|
415
|
-
} catch (e) {
|
|
416
|
-
return err(e);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
);
|
|
420
|
-
server.tool(
|
|
421
|
-
"list_drafts",
|
|
422
|
-
"List message drafts, optionally filtered by conversation or status.",
|
|
423
|
-
{
|
|
424
|
-
conversationId: z.string().optional().describe("Filter by conversation ID"),
|
|
425
|
-
status: z.enum(["pending", "approved", "rejected", "sent", "failed"]).optional().describe("Filter by status")
|
|
426
|
-
},
|
|
427
|
-
async ({ conversationId, status }) => {
|
|
428
|
-
try {
|
|
429
|
-
return ok(
|
|
430
|
-
await api("GET", "/drafts", void 0, {
|
|
431
|
-
conversation_id: conversationId,
|
|
432
|
-
status
|
|
433
|
-
})
|
|
434
|
-
);
|
|
435
|
-
} catch (e) {
|
|
436
|
-
return err(e);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
);
|
|
440
|
-
server.tool(
|
|
441
|
-
"approve_draft",
|
|
442
|
-
"Approve a pending draft and send it as a real SMS message. Runs compliance checks and deducts credits at approval time.",
|
|
443
|
-
{
|
|
444
|
-
draftId: z.string().describe("The draft ID to approve")
|
|
445
|
-
},
|
|
446
|
-
async ({ draftId }) => {
|
|
447
|
-
try {
|
|
448
|
-
return ok(await api("POST", `/drafts/${draftId}/approve`));
|
|
449
|
-
} catch (e) {
|
|
450
|
-
return err(e);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
);
|
|
454
|
-
server.tool(
|
|
455
|
-
"reject_draft",
|
|
456
|
-
"Reject a pending draft with an optional reason. The message will not be sent.",
|
|
457
|
-
{
|
|
458
|
-
draftId: z.string().describe("The draft ID to reject"),
|
|
459
|
-
reason: z.string().optional().describe("Reason for rejection")
|
|
460
|
-
},
|
|
461
|
-
async ({ draftId, reason }) => {
|
|
462
|
-
try {
|
|
463
|
-
const body = {};
|
|
464
|
-
if (reason) body.reason = reason;
|
|
465
|
-
return ok(await api("POST", `/drafts/${draftId}/reject`, body));
|
|
466
|
-
} catch (e) {
|
|
467
|
-
return err(e);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
);
|
|
471
|
-
server.tool(
|
|
472
|
-
"send_otp",
|
|
473
|
-
"Send an OTP verification code via SMS. Use for phone verification, 2FA, or identity confirmation. Returns a verification ID to check the code against. In sandbox mode (test API key), the code is returned in the response for testing.",
|
|
474
|
-
{
|
|
475
|
-
to: z.string().describe("Phone number to verify in E.164 format"),
|
|
476
|
-
appName: z.string().optional().describe("Your app/brand name shown in the SMS"),
|
|
477
|
-
codeLength: z.number().optional().describe("Digits in the code (default 6)"),
|
|
478
|
-
timeoutSecs: z.number().optional().describe("Code validity in seconds (default 300)")
|
|
479
|
-
},
|
|
480
|
-
async ({ to, appName, codeLength, timeoutSecs }) => {
|
|
481
|
-
try {
|
|
482
|
-
const body = { to };
|
|
483
|
-
if (appName) body.appName = appName;
|
|
484
|
-
if (codeLength) body.codeLength = codeLength;
|
|
485
|
-
if (timeoutSecs) body.timeoutSecs = timeoutSecs;
|
|
486
|
-
return ok(await api("POST", "/verify", body));
|
|
487
|
-
} catch (e) {
|
|
488
|
-
return err(e);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
);
|
|
492
|
-
server.tool(
|
|
493
|
-
"check_otp",
|
|
494
|
-
"Verify an OTP code. Returns the verification status: 'verified' (correct), 'invalid_code' (wrong code, shows remaining attempts), 'expired', or 'max_attempts_exceeded'.",
|
|
495
|
-
{
|
|
496
|
-
verificationId: z.string().describe("The verification ID from send_otp"),
|
|
497
|
-
code: z.string().describe("The code entered by the user")
|
|
498
|
-
},
|
|
499
|
-
async ({ verificationId, code }) => {
|
|
500
|
-
try {
|
|
501
|
-
return ok(await api("POST", `/verify/${verificationId}/check`, { code }));
|
|
502
|
-
} catch (e) {
|
|
503
|
-
return err(e);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
);
|
|
507
|
-
server.tool(
|
|
508
|
-
"get_verification_status",
|
|
509
|
-
"Check the current status of an OTP verification (pending, verified, expired, failed).",
|
|
510
|
-
{
|
|
511
|
-
verificationId: z.string().describe("The verification ID")
|
|
512
|
-
},
|
|
513
|
-
async ({ verificationId }) => {
|
|
514
|
-
try {
|
|
515
|
-
return ok(await api("GET", `/verify/${verificationId}`));
|
|
516
|
-
} catch (e) {
|
|
517
|
-
return err(e);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
);
|
|
521
|
-
server.tool(
|
|
522
|
-
"get_account",
|
|
523
|
-
"Get account info: credit balance, phone number verification status, rate limits, and API key details. Check this to understand available credits and sending capabilities.",
|
|
524
|
-
{},
|
|
525
|
-
async () => {
|
|
526
|
-
try {
|
|
527
|
-
return ok(await api("GET", "/account"));
|
|
528
|
-
} catch (e) {
|
|
529
|
-
return err(e);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
);
|
|
533
|
-
server.tool(
|
|
534
|
-
"generate_business_page",
|
|
535
|
-
"Generate a hosted business landing page for verification. Use when a business doesn't have their own website. Returns a URL at sendly.live/biz/{slug} that satisfies carrier website requirements.",
|
|
536
|
-
{
|
|
537
|
-
businessName: z.string().describe("Business name"),
|
|
538
|
-
useCase: z.string().optional().describe("Use case (e.g., Insurance Services, Appointment Reminders, 2FA)"),
|
|
539
|
-
useCaseSummary: z.string().optional().describe("Brief description of what the business does"),
|
|
540
|
-
contactEmail: z.string().optional().describe("Business contact email"),
|
|
541
|
-
contactPhone: z.string().optional().describe("Business phone number"),
|
|
542
|
-
businessAddress: z.string().optional().describe("City, State ZIP (e.g., Chicago, IL 60601)")
|
|
543
|
-
},
|
|
544
|
-
async (params) => {
|
|
545
|
-
try {
|
|
546
|
-
return ok(
|
|
547
|
-
await api("POST", "/enterprise/business-page/generate", params)
|
|
548
|
-
);
|
|
549
|
-
} catch (e) {
|
|
550
|
-
return err(e);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
);
|
|
1425
|
+
registerAllTools(server, api);
|
|
554
1426
|
var transport = new StdioServerTransport();
|
|
555
1427
|
await server.connect(transport);
|