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