@sendly/mcp
Advanced tools
+940
-47
@@ -7,3 +7,3 @@ #!/usr/bin/env node | ||
| import { z } from "zod"; | ||
| var VERSION = "1.2.0"; | ||
| var VERSION = "2.0.0"; | ||
| var API_KEY = process.env.SENDLY_API_KEY; | ||
@@ -24,3 +24,3 @@ var BASE_URL = process.env.SENDLY_BASE_URL || "https://sendly.live"; | ||
| var RATE_LIMIT_WINDOW_MS = 6e4; | ||
| var RATE_LIMIT_MAX = 30; | ||
| var RATE_LIMIT_MAX = 60; | ||
| var rateLimitTokens = RATE_LIMIT_MAX; | ||
@@ -109,9 +109,10 @@ var rateLimitResetAt = Date.now() + RATE_LIMIT_WINDOW_MS; | ||
| "list_messages", | ||
| "List sent and received SMS messages with pagination, ordered by creation date.", | ||
| "List sent and received SMS messages with pagination, ordered by creation date. Use q parameter for full-text search.", | ||
| { | ||
| limit: z.number().optional().describe("Messages to return (1-100, default 50)"), | ||
| offset: z.number().optional().describe("Pagination offset"), | ||
| status: z.enum(["queued", "sent", "delivered", "failed"]).optional().describe("Filter by delivery status") | ||
| status: z.enum(["queued", "sent", "delivered", "failed"]).optional().describe("Filter by delivery status"), | ||
| q: z.string().optional().describe("Full-text search query for message content") | ||
| }, | ||
| async ({ limit, offset, status }) => { | ||
| async ({ limit, offset, status, q }) => { | ||
| try { | ||
@@ -122,3 +123,4 @@ return ok( | ||
| offset: offset?.toString(), | ||
| status | ||
| status, | ||
| q | ||
| }) | ||
@@ -179,2 +181,118 @@ ); | ||
| server.tool( | ||
| "list_scheduled_messages", | ||
| "List all scheduled messages that haven't been sent yet.", | ||
| { | ||
| limit: z.number().optional().describe("Messages to return (1-100, default 50)"), | ||
| offset: z.number().optional().describe("Pagination offset") | ||
| }, | ||
| async ({ limit, offset }) => { | ||
| try { | ||
| return ok( | ||
| await api("GET", "/messages/scheduled", void 0, { | ||
| limit: limit?.toString(), | ||
| offset: offset?.toString() | ||
| }) | ||
| ); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "search_messages", | ||
| "Full-text search across all messages. Returns messages matching the query ranked by relevance.", | ||
| { | ||
| query: z.string().describe("Search query for message text"), | ||
| limit: z.number().optional().describe("Results to return (1-100, default 50)"), | ||
| offset: z.number().optional().describe("Pagination offset") | ||
| }, | ||
| async ({ query, limit, offset }) => { | ||
| try { | ||
| return ok( | ||
| await api("GET", "/messages", void 0, { | ||
| q: query, | ||
| limit: limit?.toString(), | ||
| offset: offset?.toString() | ||
| }) | ||
| ); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "send_batch", | ||
| "Send multiple SMS messages in a single batch (up to 1000). More efficient than individual sends for bulk messaging.", | ||
| { | ||
| messages: z.array(z.object({ | ||
| to: z.string().describe("Recipient phone number in E.164 format"), | ||
| text: z.string().describe("Message text") | ||
| })).describe("Array of messages to send (max 1000)"), | ||
| messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type for all messages (default: marketing)") | ||
| }, | ||
| async ({ messages, messageType }) => { | ||
| try { | ||
| const body = { messages }; | ||
| if (messageType) body.messageType = messageType; | ||
| return ok(await api("POST", "/messages/batch", body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "preview_batch", | ||
| "Preview a batch without sending. Returns credit cost estimate and validation results.", | ||
| { | ||
| messages: z.array(z.object({ | ||
| to: z.string().describe("Recipient phone number in E.164 format"), | ||
| text: z.string().describe("Message text") | ||
| })).describe("Array of messages to preview"), | ||
| messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)") | ||
| }, | ||
| async ({ messages, messageType }) => { | ||
| try { | ||
| const body = { messages }; | ||
| if (messageType) body.messageType = messageType; | ||
| return ok(await api("POST", "/messages/batch/preview", body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_batch", | ||
| "Get the status of a message batch including per-message delivery results.", | ||
| { | ||
| batchId: z.string().describe("The batch ID") | ||
| }, | ||
| async ({ batchId }) => { | ||
| try { | ||
| return ok(await api("GET", `/messages/batch/${batchId}`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_batches", | ||
| "List message batches with pagination.", | ||
| { | ||
| limit: z.number().optional().describe("Batches to return (1-100, default 50)"), | ||
| offset: z.number().optional().describe("Pagination offset") | ||
| }, | ||
| async ({ limit, offset }) => { | ||
| try { | ||
| return ok( | ||
| await api("GET", "/messages/batches", void 0, { | ||
| limit: limit?.toString(), | ||
| offset: offset?.toString() | ||
| }) | ||
| ); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_conversations", | ||
@@ -203,3 +321,3 @@ "List SMS conversation threads ordered by most recent activity. Each conversation groups all messages with a specific phone number.", | ||
| "get_conversation_context", | ||
| "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.", | ||
| "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.", | ||
| { | ||
@@ -254,5 +372,3 @@ conversationId: z.string().describe("The conversation ID"), | ||
| if (mediaUrls?.length) body.mediaUrls = mediaUrls; | ||
| return ok( | ||
| await api("POST", `/conversations/${conversationId}/messages`, body) | ||
| ); | ||
| return ok(await api("POST", `/conversations/${conversationId}/messages`, body)); | ||
| } catch (e) { | ||
@@ -285,5 +401,3 @@ return err(e); | ||
| "Close a conversation. Closed conversations auto-reopen when a new inbound message arrives.", | ||
| { | ||
| conversationId: z.string().describe("The conversation ID to close") | ||
| }, | ||
| { conversationId: z.string().describe("The conversation ID to close") }, | ||
| async ({ conversationId }) => { | ||
@@ -300,5 +414,3 @@ try { | ||
| "Reopen a previously closed conversation, setting its status back to active.", | ||
| { | ||
| conversationId: z.string().describe("The conversation ID to reopen") | ||
| }, | ||
| { conversationId: z.string().describe("The conversation ID to reopen") }, | ||
| async ({ conversationId }) => { | ||
@@ -315,9 +427,62 @@ try { | ||
| "Mark a conversation as read, resetting the unread count to zero.", | ||
| { conversationId: z.string().describe("The conversation ID") }, | ||
| async ({ conversationId }) => { | ||
| try { | ||
| return ok(await api("POST", `/conversations/${conversationId}/mark-read`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_suggested_replies", | ||
| "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).", | ||
| { conversationId: z.string().describe("The conversation ID to generate suggestions for") }, | ||
| async ({ conversationId }) => { | ||
| try { | ||
| return ok(await api("POST", `/conversations/${conversationId}/suggest-replies`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "create_contact", | ||
| "Create a contact with phone number and optional name, email, metadata. Contacts can be added to lists for campaigns.", | ||
| { | ||
| conversationId: z.string().describe("The conversation ID") | ||
| phoneNumber: z.string().describe("Phone number in E.164 format (+14155551234)"), | ||
| name: z.string().optional().describe("Contact name"), | ||
| email: z.string().optional().describe("Contact email address"), | ||
| metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata") | ||
| }, | ||
| async ({ conversationId }) => { | ||
| async ({ phoneNumber, name, email, metadata }) => { | ||
| try { | ||
| const body = { phone_number: phoneNumber }; | ||
| if (name) body.name = name; | ||
| if (email) body.email = email; | ||
| if (metadata) body.metadata = metadata; | ||
| return ok(await api("POST", "/contacts", body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_contacts", | ||
| "List contacts with optional search and pagination. Search matches name, email, and phone number.", | ||
| { | ||
| limit: z.number().optional().describe("Contacts to return (1-100, default 50)"), | ||
| offset: z.number().optional().describe("Pagination offset"), | ||
| search: z.string().optional().describe("Search by name, email, or phone number"), | ||
| listId: z.string().optional().describe("Filter by contact list ID") | ||
| }, | ||
| async ({ limit, offset, search, listId }) => { | ||
| try { | ||
| return ok( | ||
| await api("POST", `/conversations/${conversationId}/mark-read`) | ||
| await api("GET", "/contacts", void 0, { | ||
| limit: limit?.toString(), | ||
| offset: offset?.toString(), | ||
| search, | ||
| list_id: listId | ||
| }) | ||
| ); | ||
@@ -330,11 +495,111 @@ } catch (e) { | ||
| server.tool( | ||
| "get_suggested_replies", | ||
| "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).", | ||
| "get_contact", | ||
| "Get a contact by ID including their list memberships and metadata.", | ||
| { contactId: z.string().describe("The contact ID") }, | ||
| async ({ contactId }) => { | ||
| try { | ||
| return ok(await api("GET", `/contacts/${contactId}`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "update_contact", | ||
| "Update a contact's name, email, or metadata. Only provided fields are changed.", | ||
| { | ||
| conversationId: z.string().describe("The conversation ID to generate suggestions for") | ||
| contactId: z.string().describe("The contact ID"), | ||
| name: z.string().optional().describe("Updated name"), | ||
| email: z.string().optional().describe("Updated email"), | ||
| metadata: z.record(z.string(), z.any()).optional().describe("Updated metadata (replaces existing)") | ||
| }, | ||
| async ({ conversationId }) => { | ||
| async ({ contactId, name, email, metadata }) => { | ||
| try { | ||
| const body = {}; | ||
| if (name !== void 0) body.name = name; | ||
| if (email !== void 0) body.email = email; | ||
| if (metadata) body.metadata = metadata; | ||
| return ok(await api("PATCH", `/contacts/${contactId}`, body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "delete_contact", | ||
| "Delete a contact by ID. Removes the contact from all lists. Does not delete messages sent to this contact.", | ||
| { contactId: z.string().describe("The contact ID to delete") }, | ||
| async ({ contactId }) => { | ||
| try { | ||
| return ok(await api("DELETE", `/contacts/${contactId}`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "import_contacts", | ||
| "Bulk import contacts from an array. Optionally add all imported contacts to a list. Returns created/updated/skipped counts.", | ||
| { | ||
| contacts: z.array(z.object({ | ||
| phone_number: z.string().describe("Phone in E.164 format"), | ||
| name: z.string().optional().describe("Contact name"), | ||
| email: z.string().optional().describe("Contact email") | ||
| })).describe("Array of contacts to import (max 1000)"), | ||
| listId: z.string().optional().describe("Add all imported contacts to this list") | ||
| }, | ||
| async ({ contacts, listId }) => { | ||
| try { | ||
| const body = { contacts }; | ||
| if (listId) body.listId = listId; | ||
| return ok(await api("POST", "/contacts/import", body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "create_contact_list", | ||
| "Create a contact list for organizing contacts and targeting campaigns.", | ||
| { | ||
| name: z.string().describe("List name (e.g., 'VIP Customers', 'Newsletter')"), | ||
| description: z.string().optional().describe("List description") | ||
| }, | ||
| async ({ name, description }) => { | ||
| try { | ||
| const body = { name }; | ||
| if (description) body.description = description; | ||
| return ok(await api("POST", "/contact-lists", body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_contact_lists", | ||
| "List all contact lists with their contact counts.", | ||
| {}, | ||
| async () => { | ||
| try { | ||
| return ok(await api("GET", "/contact-lists")); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_contact_list", | ||
| "Get a contact list by ID with its members. Use limit/offset to paginate through members.", | ||
| { | ||
| listId: z.string().describe("The contact list ID"), | ||
| limit: z.number().optional().describe("Max contacts to include (default 50)"), | ||
| offset: z.number().optional().describe("Pagination offset for contacts") | ||
| }, | ||
| async ({ listId, limit, offset }) => { | ||
| try { | ||
| return ok( | ||
| await api("POST", `/conversations/${conversationId}/suggest-replies`) | ||
| await api("GET", `/contact-lists/${listId}`, void 0, { | ||
| limit: limit?.toString(), | ||
| offset: offset?.toString() | ||
| }) | ||
| ); | ||
@@ -347,2 +612,338 @@ } catch (e) { | ||
| server.tool( | ||
| "update_contact_list", | ||
| "Update a contact list's name or description.", | ||
| { | ||
| listId: z.string().describe("The contact list ID"), | ||
| name: z.string().optional().describe("Updated name"), | ||
| description: z.string().optional().describe("Updated description") | ||
| }, | ||
| async ({ listId, name, description }) => { | ||
| try { | ||
| const body = {}; | ||
| if (name) body.name = name; | ||
| if (description !== void 0) body.description = description; | ||
| return ok(await api("PATCH", `/contact-lists/${listId}`, body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "delete_contact_list", | ||
| "Delete a contact list. Contacts in the list are not deleted, only the list grouping is removed.", | ||
| { listId: z.string().describe("The contact list ID to delete") }, | ||
| async ({ listId }) => { | ||
| try { | ||
| return ok(await api("DELETE", `/contact-lists/${listId}`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "add_list_contacts", | ||
| "Add one or more contacts to a contact list.", | ||
| { | ||
| listId: z.string().describe("The contact list ID"), | ||
| contactIds: z.array(z.string()).describe("Array of contact IDs to add to the list") | ||
| }, | ||
| async ({ listId, contactIds }) => { | ||
| try { | ||
| return ok(await api("POST", `/contact-lists/${listId}/contacts`, { contactIds })); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "remove_list_contact", | ||
| "Remove a single contact from a contact list. The contact itself is not deleted.", | ||
| { | ||
| listId: z.string().describe("The contact list ID"), | ||
| contactId: z.string().describe("The contact ID to remove from the list") | ||
| }, | ||
| async ({ listId, contactId }) => { | ||
| try { | ||
| return ok(await api("DELETE", `/contact-lists/${listId}/contacts/${contactId}`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "create_campaign", | ||
| "Create a campaign to send bulk SMS to contact lists. Created as a draft \u2014 preview and send separately. Supports {{variables}} in message text.", | ||
| { | ||
| name: z.string().describe("Campaign name"), | ||
| text: z.string().optional().describe("Message text with optional {{variables}}"), | ||
| templateId: z.string().optional().describe("Template ID to use instead of inline text"), | ||
| targetListId: z.string().optional().describe("Contact list ID to send to") | ||
| }, | ||
| async ({ name, text, templateId, targetListId }) => { | ||
| try { | ||
| const body = { name }; | ||
| if (text) body.messageText = text; | ||
| if (templateId) body.templateId = templateId; | ||
| if (targetListId) body.targetListId = targetListId; | ||
| return ok(await api("POST", "/campaigns", body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_campaigns", | ||
| "List campaigns with optional filtering by status.", | ||
| { | ||
| limit: z.number().optional().describe("Campaigns to return (1-100, default 50)"), | ||
| offset: z.number().optional().describe("Pagination offset"), | ||
| status: z.enum(["draft", "scheduled", "sending", "completed", "cancelled", "failed"]).optional().describe("Filter by campaign status") | ||
| }, | ||
| async ({ limit, offset, status }) => { | ||
| try { | ||
| return ok( | ||
| await api("GET", "/campaigns", void 0, { | ||
| limit: limit?.toString(), | ||
| offset: offset?.toString(), | ||
| status | ||
| }) | ||
| ); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_campaign", | ||
| "Get a campaign by ID with delivery statistics (sent, delivered, failed counts).", | ||
| { campaignId: z.string().describe("The campaign ID") }, | ||
| async ({ campaignId }) => { | ||
| try { | ||
| return ok(await api("GET", `/campaigns/${campaignId}`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "update_campaign", | ||
| "Update a campaign's name, text, or target list. Only draft and scheduled campaigns can be updated.", | ||
| { | ||
| campaignId: z.string().describe("The campaign ID"), | ||
| name: z.string().optional().describe("Updated campaign name"), | ||
| text: z.string().optional().describe("Updated message text"), | ||
| templateId: z.string().optional().describe("Updated template ID"), | ||
| targetListId: z.string().optional().describe("Updated target list ID") | ||
| }, | ||
| async ({ campaignId, name, text, templateId, targetListId }) => { | ||
| try { | ||
| const body = {}; | ||
| if (name) body.name = name; | ||
| if (text) body.messageText = text; | ||
| if (templateId) body.templateId = templateId; | ||
| if (targetListId) body.targetListId = targetListId; | ||
| return ok(await api("PATCH", `/campaigns/${campaignId}`, body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "delete_campaign", | ||
| "Delete a campaign. Only draft and cancelled campaigns can be deleted.", | ||
| { campaignId: z.string().describe("The campaign ID to delete") }, | ||
| async ({ campaignId }) => { | ||
| try { | ||
| return ok(await api("DELETE", `/campaigns/${campaignId}`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "preview_campaign", | ||
| "Preview a campaign before sending. Returns recipient count, estimated credit cost, and whether you have enough credits.", | ||
| { campaignId: z.string().describe("The campaign ID to preview") }, | ||
| async ({ campaignId }) => { | ||
| try { | ||
| return ok(await api("GET", `/campaigns/${campaignId}/preview`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "send_campaign", | ||
| "Send a campaign immediately to all recipients in its target lists. Credits are deducted at send time. Preview first to check costs.", | ||
| { campaignId: z.string().describe("The campaign ID to send") }, | ||
| async ({ campaignId }) => { | ||
| try { | ||
| return ok(await api("POST", `/campaigns/${campaignId}/send`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "schedule_campaign", | ||
| "Schedule a campaign for future delivery at a specific date and time.", | ||
| { | ||
| campaignId: z.string().describe("The campaign ID to schedule"), | ||
| scheduledAt: z.string().describe("ISO 8601 datetime for delivery"), | ||
| timezone: z.string().optional().describe("Timezone (e.g., 'America/New_York')") | ||
| }, | ||
| async ({ campaignId, scheduledAt, timezone }) => { | ||
| try { | ||
| const body = { scheduledAt }; | ||
| if (timezone) body.timezone = timezone; | ||
| return ok(await api("POST", `/campaigns/${campaignId}/schedule`, body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "cancel_campaign", | ||
| "Cancel a scheduled campaign before it sends. Credits reserved for the campaign are refunded.", | ||
| { campaignId: z.string().describe("The campaign ID to cancel") }, | ||
| async ({ campaignId }) => { | ||
| try { | ||
| return ok(await api("POST", `/campaigns/${campaignId}/cancel`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "clone_campaign", | ||
| "Clone a campaign to create a new draft copy with the same settings. Useful for recurring or A/B campaigns.", | ||
| { campaignId: z.string().describe("The campaign ID to clone") }, | ||
| async ({ campaignId }) => { | ||
| try { | ||
| return ok(await api("POST", `/campaigns/${campaignId}/clone`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "create_template", | ||
| "Create an SMS template with {{variable}} placeholders. Templates can be used with campaigns and the Verify API.", | ||
| { | ||
| name: z.string().describe("Template name"), | ||
| text: z.string().describe("Template text with {{variable}} placeholders") | ||
| }, | ||
| async ({ name, text }) => { | ||
| try { | ||
| return ok(await api("POST", "/templates", { name, text })); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_templates", | ||
| "List all templates (custom and preset) with their status and variable definitions.", | ||
| { | ||
| limit: z.number().optional().describe("Templates to return (default 50)"), | ||
| offset: z.number().optional().describe("Pagination offset") | ||
| }, | ||
| async ({ limit, offset }) => { | ||
| try { | ||
| return ok( | ||
| await api("GET", "/templates", void 0, { | ||
| limit: limit?.toString(), | ||
| offset: offset?.toString() | ||
| }) | ||
| ); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_template", | ||
| "Get a template by ID including its variable definitions and publish status.", | ||
| { templateId: z.string().describe("The template ID") }, | ||
| async ({ templateId }) => { | ||
| try { | ||
| return ok(await api("GET", `/templates/${templateId}`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "update_template", | ||
| "Update a template's name or text.", | ||
| { | ||
| templateId: z.string().describe("The template ID"), | ||
| name: z.string().optional().describe("Updated template name"), | ||
| text: z.string().optional().describe("Updated template text") | ||
| }, | ||
| async ({ templateId, name, text }) => { | ||
| try { | ||
| const body = {}; | ||
| if (name) body.name = name; | ||
| if (text) body.text = text; | ||
| return ok(await api("PATCH", `/templates/${templateId}`, body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "delete_template", | ||
| "Delete a custom template. Preset templates cannot be deleted.", | ||
| { templateId: z.string().describe("The template ID to delete") }, | ||
| async ({ templateId }) => { | ||
| try { | ||
| return ok(await api("DELETE", `/templates/${templateId}`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "publish_template", | ||
| "Publish a template, making it available for use with the Verify API and campaigns.", | ||
| { templateId: z.string().describe("The template ID to publish") }, | ||
| async ({ templateId }) => { | ||
| try { | ||
| return ok(await api("POST", `/templates/${templateId}/publish`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "preview_template", | ||
| "Preview a template with sample variable values to see the interpolated result.", | ||
| { | ||
| templateId: z.string().describe("The template ID to preview"), | ||
| variables: z.record(z.string(), z.string()).optional().describe("Variable values (e.g., { app_name: 'MyApp', code: '123456' })") | ||
| }, | ||
| async ({ templateId, variables }) => { | ||
| try { | ||
| const body = {}; | ||
| if (variables) body.variables = variables; | ||
| return ok(await api("POST", `/templates/${templateId}/preview`, body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_template_presets", | ||
| "List system preset templates (OTP, 2FA, login, etc.) that can be used as-is or cloned.", | ||
| {}, | ||
| async () => { | ||
| try { | ||
| return ok(await api("GET", "/templates/presets")); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "create_label", | ||
@@ -387,7 +988,3 @@ "Create a label for categorizing conversations and messages. Labels have a name and optional color.", | ||
| try { | ||
| return ok( | ||
| await api("POST", `/conversations/${conversationId}/labels`, { | ||
| labelIds | ||
| }) | ||
| ); | ||
| return ok(await api("POST", `/conversations/${conversationId}/labels`, { labelIds })); | ||
| } catch (e) { | ||
@@ -407,5 +1004,3 @@ return err(e); | ||
| try { | ||
| return ok( | ||
| await api("DELETE", `/conversations/${conversationId}/labels/${labelId}`) | ||
| ); | ||
| return ok(await api("DELETE", `/conversations/${conversationId}/labels/${labelId}`)); | ||
| } catch (e) { | ||
@@ -417,2 +1012,80 @@ return err(e); | ||
| server.tool( | ||
| "list_rules", | ||
| "List auto-label rules that automatically tag conversations based on AI-detected intent and sentiment.", | ||
| {}, | ||
| async () => { | ||
| try { | ||
| return ok(await api("GET", "/rules")); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "create_rule", | ||
| "Create an auto-label rule. Rules automatically apply labels to conversations when conditions match.", | ||
| { | ||
| name: z.string().describe("Rule name"), | ||
| conditions: z.object({ | ||
| intent: z.union([z.string(), z.array(z.string())]).optional().describe("Intent(s) to match"), | ||
| sentiment: z.union([z.string(), z.array(z.string())]).optional().describe("Sentiment(s) to match") | ||
| }).describe("Conditions that trigger the rule"), | ||
| actions: z.object({ | ||
| addLabels: z.array(z.string()).describe("Label IDs to add when rule matches"), | ||
| closeConversation: z.boolean().optional().describe("Automatically close the conversation") | ||
| }).describe("Actions to take when conditions match"), | ||
| priority: z.number().optional().describe("Rule priority (lower runs first)") | ||
| }, | ||
| async ({ name, conditions, actions, priority }) => { | ||
| try { | ||
| const body = { name, conditions, actions }; | ||
| if (priority !== void 0) body.priority = priority; | ||
| return ok(await api("POST", "/rules", body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "update_rule", | ||
| "Update an auto-label rule's name, conditions, actions, or enabled status.", | ||
| { | ||
| ruleId: z.string().describe("The rule ID"), | ||
| name: z.string().optional().describe("Updated rule name"), | ||
| conditions: z.object({ | ||
| intent: z.union([z.string(), z.array(z.string())]).optional(), | ||
| sentiment: z.union([z.string(), z.array(z.string())]).optional() | ||
| }).optional().describe("Updated conditions"), | ||
| actions: z.object({ | ||
| addLabels: z.array(z.string()), | ||
| closeConversation: z.boolean().optional() | ||
| }).optional().describe("Updated actions"), | ||
| enabled: z.boolean().optional().describe("Enable or disable the rule") | ||
| }, | ||
| async ({ ruleId, name, conditions, actions, enabled }) => { | ||
| try { | ||
| const body = {}; | ||
| if (name) body.name = name; | ||
| if (conditions) body.conditions = conditions; | ||
| if (actions) body.actions = actions; | ||
| if (enabled !== void 0) body.enabled = enabled; | ||
| return ok(await api("PATCH", `/rules/${ruleId}`, body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "delete_rule", | ||
| "Delete an auto-label rule. Existing labels already applied by this rule are not removed.", | ||
| { ruleId: z.string().describe("The rule ID to delete") }, | ||
| async ({ ruleId }) => { | ||
| try { | ||
| return ok(await api("DELETE", `/rules/${ruleId}`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "create_draft", | ||
@@ -458,5 +1131,3 @@ "Create a message draft for human review before sending. The draft must be approved before it becomes a real SMS.", | ||
| "Approve a pending draft and send it as a real SMS message. Runs compliance checks and deducts credits at approval time.", | ||
| { | ||
| draftId: z.string().describe("The draft ID to approve") | ||
| }, | ||
| { draftId: z.string().describe("The draft ID to approve") }, | ||
| async ({ draftId }) => { | ||
@@ -488,4 +1159,129 @@ try { | ||
| server.tool( | ||
| "create_webhook", | ||
| "Create a webhook endpoint to receive real-time event notifications. Returns a signing secret (shown only once) for verifying payloads.", | ||
| { | ||
| url: z.string().describe("HTTPS endpoint URL to receive events"), | ||
| events: z.array(z.string()).describe("Event types to subscribe to (e.g., ['message.delivered', 'message.failed'])"), | ||
| description: z.string().optional().describe("Description of this webhook") | ||
| }, | ||
| async ({ url, events, description }) => { | ||
| try { | ||
| const body = { url, events }; | ||
| if (description) body.description = description; | ||
| return ok(await api("POST", "/webhooks", body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_webhooks", | ||
| "List all webhook endpoints with their status and event subscriptions.", | ||
| {}, | ||
| async () => { | ||
| try { | ||
| return ok(await api("GET", "/webhooks")); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_webhook", | ||
| "Get a webhook by ID with delivery statistics.", | ||
| { webhookId: z.string().describe("The webhook ID") }, | ||
| async ({ webhookId }) => { | ||
| try { | ||
| return ok(await api("GET", `/webhooks/${webhookId}`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "update_webhook", | ||
| "Update a webhook's URL, events, description, or active status.", | ||
| { | ||
| webhookId: z.string().describe("The webhook ID"), | ||
| url: z.string().optional().describe("Updated HTTPS endpoint URL"), | ||
| events: z.array(z.string()).optional().describe("Updated event types"), | ||
| description: z.string().optional().describe("Updated description"), | ||
| isActive: z.boolean().optional().describe("Enable or disable the webhook") | ||
| }, | ||
| async ({ webhookId, url, events, description, isActive }) => { | ||
| try { | ||
| const body = {}; | ||
| if (url) body.url = url; | ||
| if (events) body.events = events; | ||
| if (description !== void 0) body.description = description; | ||
| if (isActive !== void 0) body.is_active = isActive; | ||
| return ok(await api("PATCH", `/webhooks/${webhookId}`, body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "delete_webhook", | ||
| "Delete a webhook endpoint. Stops all future event deliveries to this URL.", | ||
| { webhookId: z.string().describe("The webhook ID to delete") }, | ||
| async ({ webhookId }) => { | ||
| try { | ||
| return ok(await api("DELETE", `/webhooks/${webhookId}`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "test_webhook", | ||
| "Send a test event to a webhook endpoint to verify it is reachable. Returns response status and latency.", | ||
| { webhookId: z.string().describe("The webhook ID to test") }, | ||
| async ({ webhookId }) => { | ||
| try { | ||
| return ok(await api("POST", `/webhooks/${webhookId}/test`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_webhook_deliveries", | ||
| "Get delivery history for a webhook showing recent attempts, statuses, and response times.", | ||
| { webhookId: z.string().describe("The webhook ID") }, | ||
| async ({ webhookId }) => { | ||
| try { | ||
| return ok(await api("GET", `/webhooks/${webhookId}/deliveries`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "rotate_webhook_secret", | ||
| "Rotate a webhook's signing secret. The old secret stops working immediately.", | ||
| { webhookId: z.string().describe("The webhook ID") }, | ||
| async ({ webhookId }) => { | ||
| try { | ||
| return ok(await api("POST", `/webhooks/${webhookId}/rotate-secret`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_webhook_event_types", | ||
| "List all available webhook event types that can be subscribed to.", | ||
| {}, | ||
| async () => { | ||
| try { | ||
| return ok(await api("GET", "/webhooks/event-types")); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "send_otp", | ||
| "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.", | ||
| "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.", | ||
| { | ||
@@ -495,5 +1291,6 @@ to: z.string().describe("Phone number to verify in E.164 format"), | ||
| codeLength: z.number().optional().describe("Digits in the code (default 6)"), | ||
| timeoutSecs: z.number().optional().describe("Code validity in seconds (default 300)") | ||
| timeoutSecs: z.number().optional().describe("Code validity in seconds (default 300)"), | ||
| templateId: z.string().optional().describe("Custom OTP template ID") | ||
| }, | ||
| async ({ to, appName, codeLength, timeoutSecs }) => { | ||
| async ({ to, appName, codeLength, timeoutSecs, templateId }) => { | ||
| try { | ||
@@ -504,2 +1301,3 @@ const body = { to }; | ||
| if (timeoutSecs) body.timeoutSecs = timeoutSecs; | ||
| if (templateId) body.templateId = templateId; | ||
| return ok(await api("POST", "/verify", body)); | ||
@@ -513,3 +1311,3 @@ } catch (e) { | ||
| "check_otp", | ||
| "Verify an OTP code. Returns the verification status: 'verified' (correct), 'invalid_code' (wrong code, shows remaining attempts), 'expired', or 'max_attempts_exceeded'.", | ||
| "Verify an OTP code. Returns 'verified' (correct), 'invalid_code' (wrong), 'expired', or 'max_attempts_exceeded'.", | ||
| { | ||
@@ -530,5 +1328,3 @@ verificationId: z.string().describe("The verification ID from send_otp"), | ||
| "Check the current status of an OTP verification (pending, verified, expired, failed).", | ||
| { | ||
| verificationId: z.string().describe("The verification ID") | ||
| }, | ||
| { verificationId: z.string().describe("The verification ID") }, | ||
| async ({ verificationId }) => { | ||
@@ -543,4 +1339,103 @@ try { | ||
| server.tool( | ||
| "resend_otp", | ||
| "Resend an OTP verification code. Use when the original SMS was not received.", | ||
| { verificationId: z.string().describe("The verification ID from send_otp") }, | ||
| async ({ verificationId }) => { | ||
| try { | ||
| return ok(await api("POST", `/verify/${verificationId}/resend`)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_verifications", | ||
| "List recent OTP verifications with optional status filtering.", | ||
| { | ||
| limit: z.number().optional().describe("Verifications to return (default 50)"), | ||
| status: z.enum(["pending", "verified", "expired", "failed"]).optional().describe("Filter by verification status") | ||
| }, | ||
| async ({ limit, status }) => { | ||
| try { | ||
| return ok( | ||
| await api("GET", "/verify", void 0, { | ||
| limit: limit?.toString(), | ||
| status | ||
| }) | ||
| ); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "create_verify_session", | ||
| "Create a hosted verify session with a branded UI. Returns a URL to redirect the user to for phone verification. Zero frontend code needed.", | ||
| { | ||
| successUrl: z.string().describe("URL to redirect to after successful verification"), | ||
| cancelUrl: z.string().optional().describe("URL to redirect to if user cancels"), | ||
| brandName: z.string().optional().describe("Your brand name shown on the verify page"), | ||
| brandColor: z.string().optional().describe("Brand color hex code (e.g., #4F46E5)"), | ||
| metadata: z.record(z.string(), z.any()).optional().describe("Custom metadata to attach to the session") | ||
| }, | ||
| async ({ successUrl, cancelUrl, brandName, brandColor, metadata }) => { | ||
| try { | ||
| const body = { successUrl }; | ||
| if (cancelUrl) body.cancelUrl = cancelUrl; | ||
| if (brandName) body.brandName = brandName; | ||
| if (brandColor) body.brandColor = brandColor; | ||
| if (metadata) body.metadata = metadata; | ||
| return ok(await api("POST", "/verify/sessions", body)); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "validate_verify_session", | ||
| "Validate a session token returned after a user completes hosted verification. Returns the verified phone number.", | ||
| { token: z.string().describe("The session token from the success redirect URL") }, | ||
| async ({ token }) => { | ||
| try { | ||
| return ok(await api("POST", "/verify/sessions/validate", { token })); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_credits", | ||
| "Get current credit balance including reserved credits for scheduled messages.", | ||
| {}, | ||
| async () => { | ||
| try { | ||
| return ok(await api("GET", "/credits")); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "list_credit_transactions", | ||
| "List credit transaction history showing purchases, usage, refunds, and transfers.", | ||
| { | ||
| limit: z.number().optional().describe("Transactions to return (default 50)"), | ||
| offset: z.number().optional().describe("Pagination offset") | ||
| }, | ||
| async ({ limit, offset }) => { | ||
| try { | ||
| return ok( | ||
| await api("GET", "/credits/transactions", void 0, { | ||
| limit: limit?.toString(), | ||
| offset: offset?.toString() | ||
| }) | ||
| ); | ||
| } catch (e) { | ||
| return err(e); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "get_account", | ||
| "Get account info: credit balance, phone number verification status, rate limits, and API key details. Check this to understand available credits and sending capabilities.", | ||
| "Get account info: credit balance, phone number verification status, rate limits, and API key details.", | ||
| {}, | ||
@@ -557,3 +1452,3 @@ async () => { | ||
| "generate_business_page", | ||
| "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.", | ||
| "Generate a hosted business landing page for verification. Returns a URL at sendly.live/biz/{slug}. Enterprise accounts only.", | ||
| { | ||
@@ -569,5 +1464,3 @@ businessName: z.string().describe("Business name"), | ||
| try { | ||
| return ok( | ||
| await api("POST", "/enterprise/business-page/generate", params) | ||
| ); | ||
| return ok(await api("POST", "/enterprise/business-page/generate", params)); | ||
| } catch (e) { | ||
@@ -574,0 +1467,0 @@ return err(e); |
+2
-2
| { | ||
| "name": "@sendly/mcp", | ||
| "version": "1.3.0", | ||
| "description": "Sendly MCP Server — SMS for AI agents. Send messages, manage conversations, verify phone numbers.", | ||
| "version": "2.0.0", | ||
| "description": "Sendly MCP Server — Full SMS platform for AI agents. Messaging, contacts, campaigns, templates, webhooks, OTP verification, and more.", | ||
| "type": "module", | ||
@@ -6,0 +6,0 @@ "main": "dist/index.js", |
50506
130.02%1448
160.9%