🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@sendly/mcp

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sendly/mcp - npm Package Compare versions

Comparing version
1.3.0
to
2.0.0
+940
-47
dist/index.js

@@ -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",