🚀 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
2.0.0
to
2.0.1
+1322
-1343
dist/index.js

@@ -6,65 +6,5 @@ #!/usr/bin/env node

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// src/tools.ts
import { z } from "zod";
var VERSION = "2.0.0";
var API_KEY = process.env.SENDLY_API_KEY;
var BASE_URL = process.env.SENDLY_BASE_URL || "https://sendly.live";
if (!API_KEY) {
process.stderr.write(
"SENDLY_API_KEY environment variable is required.\nGet your API key at https://sendly.live \u2192 Settings \u2192 API Keys\n"
);
process.exit(1);
}
if (!BASE_URL.startsWith("https://") && !BASE_URL.startsWith("http://localhost") && !BASE_URL.startsWith("http://127.0.0.1")) {
process.stderr.write(
"SENDLY_BASE_URL must use HTTPS in production.\nHTTP is only allowed for localhost development.\n"
);
process.exit(1);
}
var RATE_LIMIT_WINDOW_MS = 6e4;
var RATE_LIMIT_MAX = 60;
var rateLimitTokens = RATE_LIMIT_MAX;
var rateLimitResetAt = Date.now() + RATE_LIMIT_WINDOW_MS;
function checkRateLimit() {
const now = Date.now();
if (now >= rateLimitResetAt) {
rateLimitTokens = RATE_LIMIT_MAX;
rateLimitResetAt = now + RATE_LIMIT_WINDOW_MS;
}
if (rateLimitTokens <= 0) return false;
rateLimitTokens--;
return true;
}
async function api(method, path, body, query) {
if (!checkRateLimit()) {
throw new Error("Rate limited \u2014 too many requests. Wait a moment and try again.");
}
const url = new URL(`/api/v1${path}`, BASE_URL);
if (query) {
for (const [k, v] of Object.entries(query)) {
if (v !== void 0) url.searchParams.set(k, v);
}
}
const headers = {
Authorization: `Bearer ${API_KEY}`
};
if (body) headers["Content-Type"] = "application/json";
const res = await fetch(url.toString(), {
method,
headers,
body: body ? JSON.stringify(body) : void 0
});
if (res.status === 204) return { success: true };
if (res.status === 429) {
const retryAfter = res.headers.get("Retry-After");
throw new Error(
`Rate limited by API. ${retryAfter ? `Retry after ${retryAfter} seconds.` : "Wait a moment and try again."}`
);
}
const data = await res.json();
if (!res.ok) {
const msg = typeof data === "object" && data !== null ? data.error || data.message || JSON.stringify(data) : String(data);
throw new Error(String(msg));
}
return data;
}
function ok(data) {

@@ -82,1369 +22,1408 @@ return {

}
var server = new McpServer({
name: "sendly",
version: VERSION
});
server.tool(
"send_sms",
"Send an SMS message to a phone number. Returns the message with delivery status. Use 'transactional' for alerts/OTP (bypasses quiet hours), 'marketing' for promotions.",
{
to: z.string().describe("Recipient phone number in E.164 format (+14155551234)"),
text: z.string().describe("Message text content"),
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)"),
metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata to attach")
},
async ({ to, text, messageType, metadata }) => {
try {
const body = { to, text };
if (messageType) body.messageType = messageType;
if (metadata) body.metadata = metadata;
return ok(await api("POST", "/messages", body));
} catch (e) {
return err(e);
function registerAllTools(server2, api2) {
server2.tool(
"send_sms",
"Send an SMS message to a phone number. Returns the message with delivery status. Use 'transactional' for alerts/OTP (bypasses quiet hours), 'marketing' for promotions.",
{
to: z.string().describe("Recipient phone number in E.164 format (+14155551234)"),
text: z.string().describe("Message text content"),
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)"),
metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata to attach")
},
async ({ to, text, messageType, metadata }) => {
try {
const body = { to, text };
if (messageType) body.messageType = messageType;
if (metadata) body.metadata = metadata;
return ok(await api2("POST", "/messages", body));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"list_messages",
"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"),
q: z.string().optional().describe("Full-text search query for message content")
},
async ({ limit, offset, status, q }) => {
try {
return ok(
await api("GET", "/messages", void 0, {
limit: limit?.toString(),
offset: offset?.toString(),
status,
q
})
);
} catch (e) {
return err(e);
);
server2.tool(
"list_messages",
"List sent and received SMS messages with pagination. Use q parameter for full-text search across message content.",
{
limit: z.number().optional().describe("Messages to return (1-100, default 50)"),
offset: z.number().optional().describe("Pagination offset"),
q: z.string().optional().describe("Full-text search query for message content")
},
async ({ limit, offset, q }) => {
try {
return ok(
await api2("GET", "/messages", void 0, {
limit: limit?.toString(),
offset: offset?.toString(),
q
})
);
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"get_message",
"Get details of a specific SMS message including delivery status, timestamps, and metadata.",
{
messageId: z.string().describe("The message ID")
},
async ({ messageId }) => {
try {
return ok(await api("GET", `/messages/${messageId}`));
} catch (e) {
return err(e);
);
server2.tool(
"get_message",
"Get details of a specific SMS message including delivery status, timestamps, and metadata.",
{
messageId: z.string().describe("The message ID")
},
async ({ messageId }) => {
try {
return ok(await api2("GET", `/messages/${messageId}`));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"schedule_sms",
"Schedule an SMS for future delivery (5 minutes to 5 days from now). Credits are reserved immediately and refunded if cancelled.",
{
to: z.string().describe("Recipient phone number in E.164 format"),
text: z.string().describe("Message text content"),
scheduledAt: z.string().describe("ISO 8601 datetime for delivery (e.g., 2026-03-16T09:00:00Z)"),
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)")
},
async ({ to, text, scheduledAt, messageType }) => {
try {
const body = { to, text, scheduledAt };
if (messageType) body.messageType = messageType;
return ok(await api("POST", "/messages/schedule", body));
} catch (e) {
return err(e);
);
server2.tool(
"schedule_sms",
"Schedule an SMS for future delivery (5 minutes to 5 days from now). Credits are reserved immediately and refunded if cancelled.",
{
to: z.string().describe("Recipient phone number in E.164 format"),
text: z.string().describe("Message text content"),
scheduledAt: z.string().describe("ISO 8601 datetime for delivery (e.g., 2026-03-16T09:00:00Z)"),
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)")
},
async ({ to, text, scheduledAt, messageType }) => {
try {
const body = { to, text, scheduledAt };
if (messageType) body.messageType = messageType;
return ok(await api2("POST", "/messages/schedule", body));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"cancel_scheduled_message",
"Cancel a scheduled message before it sends. Credits are refunded automatically.",
{
messageId: z.string().describe("The scheduled message ID to cancel")
},
async ({ messageId }) => {
try {
return ok(await api("DELETE", `/messages/scheduled/${messageId}`));
} catch (e) {
return err(e);
);
server2.tool(
"cancel_scheduled_message",
"Cancel a scheduled message before it sends. Credits are refunded automatically.",
{
messageId: z.string().describe("The scheduled message ID to cancel")
},
async ({ messageId }) => {
try {
return ok(await api2("DELETE", `/messages/scheduled/${messageId}`));
} catch (e) {
return err(e);
}
}
}
);
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);
);
server2.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 api2("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);
);
server2.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 api2("POST", "/messages/batch", body));
} 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);
);
server2.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 api2("POST", "/messages/batch/preview", 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);
);
server2.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 api2("GET", `/messages/batch/${batchId}`));
} 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);
);
server2.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 api2("GET", "/messages/batches", void 0, {
limit: limit?.toString(),
offset: offset?.toString()
})
);
} 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);
);
server2.tool(
"list_conversations",
"List SMS conversation threads ordered by most recent activity. Each conversation groups all messages with a specific phone number.",
{
limit: z.number().optional().describe("Conversations to return (1-100, default 50)"),
offset: z.number().optional().describe("Pagination offset"),
status: z.enum(["active", "closed"]).optional().describe("Filter by conversation status")
},
async ({ limit, offset, status }) => {
try {
return ok(
await api2("GET", "/conversations", void 0, {
limit: limit?.toString(),
offset: offset?.toString(),
status
})
);
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"list_conversations",
"List SMS conversation threads ordered by most recent activity. Each conversation groups all messages with a specific phone number.",
{
limit: z.number().optional().describe("Conversations to return (1-100, default 50)"),
offset: z.number().optional().describe("Pagination offset"),
status: z.enum(["active", "closed"]).optional().describe("Filter by conversation status")
},
async ({ limit, offset, status }) => {
try {
return ok(
await api("GET", "/conversations", void 0, {
limit: limit?.toString(),
offset: offset?.toString(),
status
})
);
} catch (e) {
return err(e);
);
server2.tool(
"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.",
{
conversationId: z.string().describe("The conversation ID"),
maxMessages: z.number().optional().describe("Max messages to include (default 20, max 50)")
},
async ({ conversationId, maxMessages }) => {
try {
return ok(
await api2("GET", `/conversations/${conversationId}/context`, void 0, {
max_messages: maxMessages?.toString()
})
);
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"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.",
{
conversationId: z.string().describe("The conversation ID"),
maxMessages: z.number().optional().describe("Max messages to include (default 20, max 50)")
},
async ({ conversationId, maxMessages }) => {
try {
return ok(
await api("GET", `/conversations/${conversationId}/context`, void 0, {
max_messages: maxMessages?.toString()
})
);
} catch (e) {
return err(e);
);
server2.tool(
"get_conversation",
"Get a conversation thread by ID. Set includeMessages=true to load the message history.",
{
conversationId: z.string().describe("The conversation ID"),
includeMessages: z.boolean().optional().describe("Include message history (default false)"),
messageLimit: z.number().optional().describe("Number of messages to include (default 50)")
},
async ({ conversationId, includeMessages, messageLimit }) => {
try {
return ok(
await api2("GET", `/conversations/${conversationId}`, void 0, {
include_messages: includeMessages ? "true" : void 0,
message_limit: messageLimit?.toString()
})
);
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"get_conversation",
"Get a conversation thread by ID. Set includeMessages=true to load the message history.",
{
conversationId: z.string().describe("The conversation ID"),
includeMessages: z.boolean().optional().describe("Include message history (default false)"),
messageLimit: z.number().optional().describe("Number of messages to include (default 50)")
},
async ({ conversationId, includeMessages, messageLimit }) => {
try {
return ok(
await api("GET", `/conversations/${conversationId}`, void 0, {
include_messages: includeMessages ? "true" : void 0,
message_limit: messageLimit?.toString()
})
);
} catch (e) {
return err(e);
);
server2.tool(
"reply_to_conversation",
"Send a reply within an existing conversation. The recipient is automatically set from the conversation's phone number.",
{
conversationId: z.string().describe("The conversation ID to reply in"),
text: z.string().describe("Reply message text"),
mediaUrls: z.array(z.string()).optional().describe("Media URLs for MMS")
},
async ({ conversationId, text, mediaUrls }) => {
try {
const body = { text };
if (mediaUrls?.length) body.mediaUrls = mediaUrls;
return ok(await api2("POST", `/conversations/${conversationId}/messages`, body));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"reply_to_conversation",
"Send a reply within an existing conversation. The recipient is automatically set from the conversation's phone number.",
{
conversationId: z.string().describe("The conversation ID to reply in"),
text: z.string().describe("Reply message text"),
mediaUrls: z.array(z.string()).optional().describe("Media URLs for MMS")
},
async ({ conversationId, text, mediaUrls }) => {
try {
const body = { text };
if (mediaUrls?.length) body.mediaUrls = mediaUrls;
return ok(await api("POST", `/conversations/${conversationId}/messages`, body));
} catch (e) {
return err(e);
);
server2.tool(
"update_conversation",
"Update a conversation's metadata or tags. Use metadata for custom key-value data, tags for categorization.",
{
conversationId: z.string().describe("The conversation ID"),
metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata"),
tags: z.array(z.string()).optional().describe("Tags for categorization (replaces existing tags)")
},
async ({ conversationId, metadata, tags }) => {
try {
const body = {};
if (metadata) body.metadata = metadata;
if (tags) body.tags = tags;
return ok(await api2("PATCH", `/conversations/${conversationId}`, body));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"update_conversation",
"Update a conversation's metadata or tags. Use metadata for custom key-value data, tags for categorization.",
{
conversationId: z.string().describe("The conversation ID"),
metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata"),
tags: z.array(z.string()).optional().describe("Tags for categorization (replaces existing tags)")
},
async ({ conversationId, metadata, tags }) => {
try {
const body = {};
if (metadata) body.metadata = metadata;
if (tags) body.tags = tags;
return ok(await api("PATCH", `/conversations/${conversationId}`, body));
} catch (e) {
return err(e);
);
server2.tool(
"close_conversation",
"Close a conversation. Closed conversations auto-reopen when a new inbound message arrives.",
{ conversationId: z.string().describe("The conversation ID to close") },
async ({ conversationId }) => {
try {
return ok(await api2("POST", `/conversations/${conversationId}/close`));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"close_conversation",
"Close a conversation. Closed conversations auto-reopen when a new inbound message arrives.",
{ conversationId: z.string().describe("The conversation ID to close") },
async ({ conversationId }) => {
try {
return ok(await api("POST", `/conversations/${conversationId}/close`));
} catch (e) {
return err(e);
);
server2.tool(
"reopen_conversation",
"Reopen a previously closed conversation, setting its status back to active.",
{ conversationId: z.string().describe("The conversation ID to reopen") },
async ({ conversationId }) => {
try {
return ok(await api2("POST", `/conversations/${conversationId}/reopen`));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"reopen_conversation",
"Reopen a previously closed conversation, setting its status back to active.",
{ conversationId: z.string().describe("The conversation ID to reopen") },
async ({ conversationId }) => {
try {
return ok(await api("POST", `/conversations/${conversationId}/reopen`));
} catch (e) {
return err(e);
);
server2.tool(
"mark_conversation_read",
"Mark a conversation as read, resetting the unread count to zero.",
{ conversationId: z.string().describe("The conversation ID") },
async ({ conversationId }) => {
try {
return ok(await api2("POST", `/conversations/${conversationId}/mark-read`));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"mark_conversation_read",
"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);
);
server2.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 api2("POST", `/conversations/${conversationId}/suggest-replies`));
} 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);
);
server2.tool(
"create_contact",
"Create a contact with phone number and optional name, email, metadata. Contacts can be added to lists for campaigns.",
{
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 ({ 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 api2("POST", "/contacts", body));
} 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.",
{
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 ({ 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);
);
server2.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 api2("GET", "/contacts", void 0, {
limit: limit?.toString(),
offset: offset?.toString(),
search,
list_id: listId
})
);
} 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("GET", "/contacts", void 0, {
limit: limit?.toString(),
offset: offset?.toString(),
search,
list_id: listId
})
);
} catch (e) {
return err(e);
);
server2.tool(
"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 api2("GET", `/contacts/${contactId}`));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"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);
);
server2.tool(
"update_contact",
"Update a contact's name, email, or metadata. Only provided fields are changed.",
{
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 ({ 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 api2("PATCH", `/contacts/${contactId}`, body));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"update_contact",
"Update a contact's name, email, or metadata. Only provided fields are changed.",
{
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 ({ 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);
);
server2.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 api2("DELETE", `/contacts/${contactId}`));
} 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);
);
server2.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: 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 10000)"),
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 api2("POST", "/contacts/import", body));
} 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);
);
server2.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 api2("POST", "/contact-lists", 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);
);
server2.tool(
"list_contact_lists",
"List all contact lists with their contact counts.",
{},
async () => {
try {
return ok(await api2("GET", "/contact-lists"));
} 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);
);
server2.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 api2("GET", `/contact-lists/${listId}`, void 0, {
limit: limit?.toString(),
offset: offset?.toString()
})
);
} 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("GET", `/contact-lists/${listId}`, void 0, {
limit: limit?.toString(),
offset: offset?.toString()
})
);
} catch (e) {
return err(e);
);
server2.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 api2("PATCH", `/contact-lists/${listId}`, body));
} catch (e) {
return err(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);
);
server2.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 api2("DELETE", `/contact-lists/${listId}`));
} 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);
);
server2.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 api2("POST", `/contact-lists/${listId}/contacts`, { contact_ids: contactIds }));
} 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);
);
server2.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 api2("DELETE", `/contact-lists/${listId}/contacts/${contactId}`));
} 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);
);
server2.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 api2("POST", "/campaigns", body));
} 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);
);
server2.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 api2("GET", "/campaigns", void 0, {
limit: limit?.toString(),
offset: offset?.toString(),
status
})
);
} 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);
);
server2.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 api2("GET", `/campaigns/${campaignId}`));
} 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);
);
server2.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 api2("PATCH", `/campaigns/${campaignId}`, body));
} 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);
);
server2.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 api2("DELETE", `/campaigns/${campaignId}`));
} 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);
);
server2.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 api2("GET", `/campaigns/${campaignId}/preview`));
} 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);
);
server2.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 api2("POST", `/campaigns/${campaignId}/send`));
} 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);
);
server2.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 api2("POST", `/campaigns/${campaignId}/schedule`, body));
} 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);
);
server2.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 api2("POST", `/campaigns/${campaignId}/cancel`));
} 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);
);
server2.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 api2("POST", `/campaigns/${campaignId}/clone`));
} 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);
);
server2.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 api2("POST", "/templates", { name, text }));
} 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);
);
server2.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 api2("GET", "/templates", void 0, {
limit: limit?.toString(),
offset: offset?.toString()
})
);
} 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);
);
server2.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 api2("GET", `/templates/${templateId}`));
} 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);
);
server2.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 api2("PATCH", `/templates/${templateId}`, body));
} 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);
);
server2.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 api2("DELETE", `/templates/${templateId}`));
} 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);
);
server2.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 api2("POST", `/templates/${templateId}/publish`));
} 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);
);
server2.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 api2("POST", `/templates/${templateId}/preview`, body));
} 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);
);
server2.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 api2("GET", "/templates/presets"));
} 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);
);
server2.tool(
"create_label",
"Create a label for categorizing conversations and messages. Labels have a name and optional color.",
{
name: z.string().describe("Label name (e.g., 'urgent', 'vip', 'follow-up')"),
color: z.string().optional().describe("Hex color code (default: #6b7280)"),
description: z.string().optional().describe("Label description")
},
async ({ name, color, description }) => {
try {
const body = { name };
if (color) body.color = color;
if (description) body.description = description;
return ok(await api2("POST", "/labels", body));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"create_label",
"Create a label for categorizing conversations and messages. Labels have a name and optional color.",
{
name: z.string().describe("Label name (e.g., 'urgent', 'vip', 'follow-up')"),
color: z.string().optional().describe("Hex color code (default: #6b7280)"),
description: z.string().optional().describe("Label description")
},
async ({ name, color, description }) => {
try {
const body = { name };
if (color) body.color = color;
if (description) body.description = description;
return ok(await api("POST", "/labels", body));
} catch (e) {
return err(e);
);
server2.tool(
"list_labels",
"List all labels available in your workspace.",
{},
async () => {
try {
return ok(await api2("GET", "/labels"));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"list_labels",
"List all labels available in your workspace.",
{},
async () => {
try {
return ok(await api("GET", "/labels"));
} catch (e) {
return err(e);
);
server2.tool(
"add_conversation_label",
"Add one or more labels to a conversation for categorization.",
{
conversationId: z.string().describe("The conversation ID"),
labelIds: z.array(z.string()).describe("Array of label IDs to add")
},
async ({ conversationId, labelIds }) => {
try {
return ok(await api2("POST", `/conversations/${conversationId}/labels`, { labelIds }));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"add_conversation_label",
"Add one or more labels to a conversation for categorization.",
{
conversationId: z.string().describe("The conversation ID"),
labelIds: z.array(z.string()).describe("Array of label IDs to add")
},
async ({ conversationId, labelIds }) => {
try {
return ok(await api("POST", `/conversations/${conversationId}/labels`, { labelIds }));
} catch (e) {
return err(e);
);
server2.tool(
"remove_conversation_label",
"Remove a label from a conversation.",
{
conversationId: z.string().describe("The conversation ID"),
labelId: z.string().describe("The label ID to remove")
},
async ({ conversationId, labelId }) => {
try {
return ok(await api2("DELETE", `/conversations/${conversationId}/labels/${labelId}`));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"remove_conversation_label",
"Remove a label from a conversation.",
{
conversationId: z.string().describe("The conversation ID"),
labelId: z.string().describe("The label ID to remove")
},
async ({ conversationId, labelId }) => {
try {
return ok(await api("DELETE", `/conversations/${conversationId}/labels/${labelId}`));
} catch (e) {
return err(e);
);
server2.tool(
"list_rules",
"List auto-label rules that automatically tag conversations based on AI-detected intent and sentiment.",
{},
async () => {
try {
return ok(await api2("GET", "/rules"));
} catch (e) {
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);
);
server2.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 api2("POST", "/rules", body));
} 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);
);
server2.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 api2("PATCH", `/rules/${ruleId}`, 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);
);
server2.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 api2("DELETE", `/rules/${ruleId}`));
} 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);
);
server2.tool(
"create_draft",
"Create a message draft for human review before sending. The draft must be approved before it becomes a real SMS.",
{
conversationId: z.string().describe("The conversation ID"),
text: z.string().describe("Draft message text"),
source: z.string().optional().describe("Source of the draft (default: 'ai')")
},
async ({ conversationId, text, source }) => {
try {
const body = { conversationId, text };
if (source) body.source = source;
return ok(await api2("POST", "/drafts", body));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"create_draft",
"Create a message draft for human review before sending. The draft must be approved before it becomes a real SMS.",
{
conversationId: z.string().describe("The conversation ID"),
text: z.string().describe("Draft message text"),
source: z.string().optional().describe("Source of the draft (default: 'ai')")
},
async ({ conversationId, text, source }) => {
try {
const body = { conversationId, text };
if (source) body.source = source;
return ok(await api("POST", "/drafts", body));
} catch (e) {
return err(e);
);
server2.tool(
"list_drafts",
"List message drafts, optionally filtered by conversation or status.",
{
conversationId: z.string().optional().describe("Filter by conversation ID"),
status: z.enum(["pending", "approved", "rejected", "sent", "failed"]).optional().describe("Filter by status")
},
async ({ conversationId, status }) => {
try {
return ok(
await api2("GET", "/drafts", void 0, {
conversation_id: conversationId,
status
})
);
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"list_drafts",
"List message drafts, optionally filtered by conversation or status.",
{
conversationId: z.string().optional().describe("Filter by conversation ID"),
status: z.enum(["pending", "approved", "rejected", "sent", "failed"]).optional().describe("Filter by status")
},
async ({ conversationId, status }) => {
try {
return ok(
await api("GET", "/drafts", void 0, {
conversation_id: conversationId,
status
})
);
} catch (e) {
return err(e);
);
server2.tool(
"approve_draft",
"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") },
async ({ draftId }) => {
try {
return ok(await api2("POST", `/drafts/${draftId}/approve`));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"approve_draft",
"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") },
async ({ draftId }) => {
try {
return ok(await api("POST", `/drafts/${draftId}/approve`));
} catch (e) {
return err(e);
);
server2.tool(
"reject_draft",
"Reject a pending draft with an optional reason. The message will not be sent.",
{
draftId: z.string().describe("The draft ID to reject"),
reason: z.string().optional().describe("Reason for rejection")
},
async ({ draftId, reason }) => {
try {
const body = {};
if (reason) body.reason = reason;
return ok(await api2("POST", `/drafts/${draftId}/reject`, body));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"reject_draft",
"Reject a pending draft with an optional reason. The message will not be sent.",
{
draftId: z.string().describe("The draft ID to reject"),
reason: z.string().optional().describe("Reason for rejection")
},
async ({ draftId, reason }) => {
try {
const body = {};
if (reason) body.reason = reason;
return ok(await api("POST", `/drafts/${draftId}/reject`, body));
} catch (e) {
return err(e);
);
server2.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 api2("POST", "/webhooks", body));
} catch (e) {
return err(e);
}
}
}
);
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);
);
server2.tool(
"list_webhooks",
"List all webhook endpoints with their status and event subscriptions.",
{},
async () => {
try {
return ok(await api2("GET", "/webhooks"));
} 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);
);
server2.tool(
"get_webhook",
"Get a webhook by ID with delivery statistics.",
{ webhookId: z.string().describe("The webhook ID") },
async ({ webhookId }) => {
try {
return ok(await api2("GET", `/webhooks/${webhookId}`));
} 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);
);
server2.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 api2("PATCH", `/webhooks/${webhookId}`, body));
} 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);
);
server2.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 api2("DELETE", `/webhooks/${webhookId}`));
} 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);
);
server2.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 api2("POST", `/webhooks/${webhookId}/test`));
} 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);
);
server2.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 api2("GET", `/webhooks/${webhookId}/deliveries`));
} 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);
);
server2.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 api2("POST", `/webhooks/${webhookId}/rotate-secret`));
} 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);
);
server2.tool(
"list_webhook_event_types",
"List all available webhook event types that can be subscribed to.",
{},
async () => {
try {
return ok(await api2("GET", "/webhooks/event-types"));
} 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);
);
server2.tool(
"send_otp",
"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.",
{
to: z.string().describe("Phone number to verify in E.164 format"),
appName: z.string().optional().describe("Your app/brand name shown in the SMS"),
codeLength: z.number().optional().describe("Digits in the code (default 6)"),
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, templateId }) => {
try {
const body = { to };
if (appName) body.app_name = appName;
if (codeLength) body.code_length = codeLength;
if (timeoutSecs) body.timeout_secs = timeoutSecs;
if (templateId) body.template_id = templateId;
return ok(await api2("POST", "/verify", body));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"send_otp",
"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.",
{
to: z.string().describe("Phone number to verify in E.164 format"),
appName: z.string().optional().describe("Your app/brand name shown in the SMS"),
codeLength: z.number().optional().describe("Digits in the code (default 6)"),
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, templateId }) => {
try {
const body = { to };
if (appName) body.appName = appName;
if (codeLength) body.codeLength = codeLength;
if (timeoutSecs) body.timeoutSecs = timeoutSecs;
if (templateId) body.templateId = templateId;
return ok(await api("POST", "/verify", body));
} catch (e) {
return err(e);
);
server2.tool(
"check_otp",
"Verify an OTP code. Returns 'verified' (correct), 'invalid_code' (wrong), 'expired', or 'max_attempts_exceeded'.",
{
verificationId: z.string().describe("The verification ID from send_otp"),
code: z.string().describe("The code entered by the user")
},
async ({ verificationId, code }) => {
try {
return ok(await api2("POST", `/verify/${verificationId}/check`, { code }));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"check_otp",
"Verify an OTP code. Returns 'verified' (correct), 'invalid_code' (wrong), 'expired', or 'max_attempts_exceeded'.",
{
verificationId: z.string().describe("The verification ID from send_otp"),
code: z.string().describe("The code entered by the user")
},
async ({ verificationId, code }) => {
try {
return ok(await api("POST", `/verify/${verificationId}/check`, { code }));
} catch (e) {
return err(e);
);
server2.tool(
"get_verification_status",
"Check the current status of an OTP verification (pending, verified, expired, failed).",
{ verificationId: z.string().describe("The verification ID") },
async ({ verificationId }) => {
try {
return ok(await api2("GET", `/verify/${verificationId}`));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"get_verification_status",
"Check the current status of an OTP verification (pending, verified, expired, failed).",
{ verificationId: z.string().describe("The verification ID") },
async ({ verificationId }) => {
try {
return ok(await api("GET", `/verify/${verificationId}`));
} catch (e) {
return err(e);
);
server2.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 api2("POST", `/verify/${verificationId}/resend`));
} catch (e) {
return err(e);
}
}
}
);
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);
);
server2.tool(
"list_verifications",
"List recent OTP verifications with pagination.",
{
limit: z.number().optional().describe("Verifications to return (default 50)")
},
async ({ limit }) => {
try {
return ok(
await api2("GET", "/verify", void 0, {
limit: limit?.toString()
})
);
} 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);
);
server2.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 = { success_url: successUrl };
if (cancelUrl) body.cancel_url = cancelUrl;
if (brandName) body.brand_name = brandName;
if (brandColor) body.brand_color = brandColor;
if (metadata) body.metadata = metadata;
return ok(await api2("POST", "/verify/sessions", body));
} 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);
);
server2.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 api2("POST", "/verify/sessions/validate", { token }));
} 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);
);
server2.tool(
"get_credits",
"Get current credit balance including reserved credits for scheduled messages.",
{},
async () => {
try {
return ok(await api2("GET", "/credits"));
} 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);
);
server2.tool(
"list_credit_transactions",
"List credit transaction history showing purchases, usage, refunds, and transfers.",
{
limit: z.number().optional().describe("Transactions to return (default 50)")
},
async ({ limit }) => {
try {
return ok(
await api2("GET", "/credits/transactions", void 0, {
limit: limit?.toString()
})
);
} 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);
);
server2.tool(
"get_account",
"Get account info: credit balance, phone number verification status, rate limits, and API key details.",
{},
async () => {
try {
return ok(await api2("GET", "/account"));
} catch (e) {
return err(e);
}
}
}
);
server.tool(
"get_account",
"Get account info: credit balance, phone number verification status, rate limits, and API key details.",
{},
async () => {
try {
return ok(await api("GET", "/account"));
} catch (e) {
return err(e);
);
server2.tool(
"generate_business_page",
"Generate a hosted business landing page for verification. Returns a URL at sendly.live/biz/{slug}. Enterprise accounts only.",
{
businessName: z.string().describe("Business name"),
useCase: z.string().optional().describe("Use case (e.g., Insurance Services, Appointment Reminders, 2FA)"),
useCaseSummary: z.string().optional().describe("Brief description of what the business does"),
contactEmail: z.string().optional().describe("Business contact email"),
contactPhone: z.string().optional().describe("Business phone number"),
businessAddress: z.string().optional().describe("City, State ZIP (e.g., Chicago, IL 60601)")
},
async (params) => {
try {
return ok(await api2("POST", "/enterprise/business-page/generate", params));
} catch (e) {
return err(e);
}
}
);
}
// src/index.ts
var VERSION = "2.0.0";
var API_KEY = process.env.SENDLY_API_KEY;
var BASE_URL = process.env.SENDLY_BASE_URL || "https://sendly.live";
if (!API_KEY) {
process.stderr.write(
"SENDLY_API_KEY environment variable is required.\nGet your API key at https://sendly.live \u2192 Settings \u2192 API Keys\n"
);
process.exit(1);
}
if (!BASE_URL.startsWith("https://") && !BASE_URL.startsWith("http://localhost") && !BASE_URL.startsWith("http://127.0.0.1")) {
process.stderr.write(
"SENDLY_BASE_URL must use HTTPS in production.\nHTTP is only allowed for localhost development.\n"
);
process.exit(1);
}
var RATE_LIMIT_WINDOW_MS = 6e4;
var RATE_LIMIT_MAX = 60;
var rateLimitTokens = RATE_LIMIT_MAX;
var rateLimitResetAt = Date.now() + RATE_LIMIT_WINDOW_MS;
function checkRateLimit() {
const now = Date.now();
if (now >= rateLimitResetAt) {
rateLimitTokens = RATE_LIMIT_MAX;
rateLimitResetAt = now + RATE_LIMIT_WINDOW_MS;
}
);
server.tool(
"generate_business_page",
"Generate a hosted business landing page for verification. Returns a URL at sendly.live/biz/{slug}. Enterprise accounts only.",
{
businessName: z.string().describe("Business name"),
useCase: z.string().optional().describe("Use case (e.g., Insurance Services, Appointment Reminders, 2FA)"),
useCaseSummary: z.string().optional().describe("Brief description of what the business does"),
contactEmail: z.string().optional().describe("Business contact email"),
contactPhone: z.string().optional().describe("Business phone number"),
businessAddress: z.string().optional().describe("City, State ZIP (e.g., Chicago, IL 60601)")
},
async (params) => {
try {
return ok(await api("POST", "/enterprise/business-page/generate", params));
} catch (e) {
return err(e);
if (rateLimitTokens <= 0) return false;
rateLimitTokens--;
return true;
}
async function api(method, path, body, query) {
if (!checkRateLimit()) {
throw new Error("Rate limited \u2014 too many requests. Wait a moment and try again.");
}
const url = new URL(`/api/v1${path}`, BASE_URL);
if (query) {
for (const [k, v] of Object.entries(query)) {
if (v !== void 0) url.searchParams.set(k, v);
}
}
);
const headers = {
Authorization: `Bearer ${API_KEY}`
};
if (body) headers["Content-Type"] = "application/json";
const res = await fetch(url.toString(), {
method,
headers,
body: body ? JSON.stringify(body) : void 0
});
if (res.status === 204) return { success: true };
if (res.status === 429) {
const retryAfter = res.headers.get("Retry-After");
throw new Error(
`Rate limited by API. ${retryAfter ? `Retry after ${retryAfter} seconds.` : "Wait a moment and try again."}`
);
}
const data = await res.json();
if (!res.ok) {
const msg = typeof data === "object" && data !== null ? data.error || data.message || JSON.stringify(data) : String(data);
throw new Error(String(msg));
}
return data;
}
var server = new McpServer({
name: "sendly",
version: VERSION
});
registerAllTools(server, api);
var transport = new StdioServerTransport();
await server.connect(transport);
{
"name": "@sendly/mcp",
"version": "2.0.0",
"version": "2.0.1",
"description": "Sendly MCP Server — Full SMS platform for AI agents. Messaging, contacts, campaigns, templates, webhooks, OTP verification, and more.",

@@ -23,8 +23,19 @@ "type": "module",

"sms",
"sms-api",
"mcp",
"mcp-server",
"model-context-protocol",
"ai-agents",
"ai-tools",
"messaging",
"otp",
"verification"
"phone-verification",
"bulk-sms",
"twilio-alternative",
"text-message",
"claude-code",
"cursor",
"codex",
"windsurf",
"openclaw"
],

@@ -31,0 +42,0 @@ "author": "Sendly <support@sendly.live>",

+178
-32
# @sendly/mcp
SMS for AI agents — send messages, manage conversations, verify phone numbers via [Model Context Protocol](https://modelcontextprotocol.io).
Full SMS platform for AI agents — 82 tools for messaging, contacts, campaigns, templates, webhooks, OTP verification, conversations, labels, drafts, and more via [Model Context Protocol](https://modelcontextprotocol.io).
## Quick Setup
### Claude Code
```bash
claude mcp add --env SENDLY_API_KEY=sk_test_v1_your_key sendly -- npx -y @sendly/mcp
```
### Claude Desktop

@@ -18,3 +24,3 @@

"env": {
"SENDLY_API_KEY": "sk_test_v1_your_key_here"
"SENDLY_API_KEY": "sk_test_v1_your_key"
}

@@ -26,5 +32,5 @@ }

### Cursor
### Cursor / VS Code Copilot / Windsurf
Add to `.cursor/mcp.json` in your project:
Add to your MCP config (`.cursor/mcp.json`, `.vscode/mcp.json`, or `~/.codeium/windsurf/mcp_config.json`):

@@ -38,3 +44,3 @@ ```json

"env": {
"SENDLY_API_KEY": "sk_test_v1_your_key_here"
"SENDLY_API_KEY": "sk_test_v1_your_key"
}

@@ -46,44 +52,169 @@ }

### VS Code / Windsurf
### OpenClaw
Same pattern — set `SENDLY_API_KEY` in the environment and run `npx @sendly/mcp` as the MCP server command.
```bash
openclaw mcp set sendly '{"command":"npx","args":["-y","@sendly/mcp"],"env":{"SENDLY_API_KEY":"sk_test_v1_your_key"}}'
```
## Available Tools
Then ask your agent: *"Send a test SMS to +15005550000 saying Hello from my agent"*
### Messaging
## All 82 Tools
### Messaging (6)
| Tool | Description |
|------|-------------|
| `send_sms` | Send an SMS message to a phone number |
| `list_messages` | List sent and received messages with pagination |
| `get_message` | Get details of a specific message |
| `schedule_sms` | Schedule a message for future delivery |
| `cancel_scheduled_message` | Cancel a scheduled message (credits refunded) |
| `send_sms` | Send an SMS/MMS to a phone number |
| `list_messages` | List messages with full-text search (`q` param) |
| `get_message` | Get message details and delivery status |
| `schedule_sms` | Schedule for future delivery (5 min – 5 days) |
| `cancel_scheduled_message` | Cancel scheduled message, credits refunded |
| `list_scheduled_messages` | List pending scheduled messages |
### Conversations
### Batch Messaging (4)
| Tool | Description |
|------|-------------|
| `list_conversations` | List conversation threads by recent activity |
| `get_conversation` | Get a conversation with optional message history |
| `reply_to_conversation` | Reply within a conversation (auto-sets recipient) |
| `update_conversation` | Update metadata or tags on a conversation |
| `close_conversation` | Close a conversation (auto-reopens on new inbound) |
| `send_batch` | Send to multiple recipients in one call (up to 1,000) |
| `preview_batch` | Preview credit cost before sending |
| `get_batch` | Get batch status with per-message results |
| `list_batches` | List all batches |
### Conversations (9)
| Tool | Description |
|------|-------------|
| `list_conversations` | List threads by recent activity |
| `get_conversation` | Get conversation with optional message history |
| `get_conversation_context` | LLM-ready formatted context with AI annotations |
| `reply_to_conversation` | Reply in a conversation (auto-sets recipient) |
| `update_conversation` | Update metadata or tags |
| `close_conversation` | Close (auto-reopens on new inbound) |
| `reopen_conversation` | Reopen a closed conversation |
| `mark_conversation_read` | Reset unread count to zero |
| `get_suggested_replies` | AI-generated replies in 3 tones |
### OTP / Verification
### Contacts (6)
| Tool | Description |
|------|-------------|
| `send_otp` | Send a one-time password via SMS |
| `check_otp` | Verify an OTP code |
| `get_verification_status` | Check verification status |
| `create_contact` | Create with phone, name, email, metadata |
| `list_contacts` | List with search and pagination |
| `get_contact` | Get contact with list memberships |
| `update_contact` | Update name, email, or metadata |
| `delete_contact` | Delete (removes from all lists) |
| `import_contacts` | Bulk import up to 10,000 contacts |
### Account
### Contact Lists (7)
| Tool | Description |
|------|-------------|
| `get_account` | Get credit balance, verification status, rate limits |
| `create_contact_list` | Create a list for campaigns |
| `list_contact_lists` | List all lists with counts |
| `get_contact_list` | Get list with members |
| `update_contact_list` | Update name or description |
| `delete_contact_list` | Delete list (contacts preserved) |
| `add_list_contacts` | Add contacts to a list |
| `remove_list_contact` | Remove a contact from a list |
### Campaigns (10)
| Tool | Description |
|------|-------------|
| `create_campaign` | Create with `{{variable}}` personalization |
| `list_campaigns` | List with status filter |
| `get_campaign` | Get with delivery stats |
| `update_campaign` | Update draft or scheduled campaign |
| `delete_campaign` | Delete draft or cancelled campaign |
| `preview_campaign` | Preview recipient count and credit cost |
| `send_campaign` | Send immediately |
| `schedule_campaign` | Schedule for future delivery |
| `cancel_campaign` | Cancel scheduled campaign, credits refunded |
| `clone_campaign` | Clone as new draft |
### Templates (8)
| Tool | Description |
|------|-------------|
| `create_template` | Create with `{{variable}}` placeholders |
| `list_templates` | List custom and preset templates |
| `get_template` | Get template with variable definitions |
| `update_template` | Update name or text |
| `delete_template` | Delete custom template |
| `publish_template` | Publish for use with Verify API |
| `preview_template` | Preview with sample variable values |
| `list_template_presets` | List system OTP/2FA presets |
### Labels (4)
| Tool | Description |
|------|-------------|
| `create_label` | Create with name and color |
| `list_labels` | List all workspace labels |
| `add_conversation_label` | Add labels to a conversation |
| `remove_conversation_label` | Remove a label from a conversation |
### Auto-Label Rules (4)
| Tool | Description |
|------|-------------|
| `list_rules` | List auto-label rules |
| `create_rule` | Create rule (intent/sentiment → label) |
| `update_rule` | Update conditions, actions, or enabled state |
| `delete_rule` | Delete rule |
### Drafts (4)
| Tool | Description |
|------|-------------|
| `create_draft` | Create draft for human review |
| `list_drafts` | List drafts by conversation or status |
| `approve_draft` | Approve and send as real SMS |
| `reject_draft` | Reject with reason |
### Webhooks (9)
| Tool | Description |
|------|-------------|
| `create_webhook` | Create endpoint (returns signing secret) |
| `list_webhooks` | List all webhooks |
| `get_webhook` | Get webhook with delivery stats |
| `update_webhook` | Update URL, events, or active state |
| `delete_webhook` | Delete webhook |
| `test_webhook` | Send test event to verify endpoint |
| `list_webhook_deliveries` | Delivery history with statuses |
| `rotate_webhook_secret` | Rotate signing secret |
| `list_webhook_event_types` | List available event types |
### OTP / Verify (7)
| Tool | Description |
|------|-------------|
| `send_otp` | Send verification code via SMS |
| `check_otp` | Verify the code user entered |
| `get_verification_status` | Check verification state |
| `resend_otp` | Resend if original not received |
| `list_verifications` | List recent verifications |
| `create_verify_session` | Hosted verification UI (zero frontend code) |
| `validate_verify_session` | Validate token from hosted session |
### Credits (2)
| Tool | Description |
|------|-------------|
| `get_credits` | Current balance and reserved credits |
| `list_credit_transactions` | Transaction history |
### Account (1)
| Tool | Description |
|------|-------------|
| `get_account` | Credit balance, verification status, rate limits |
### Enterprise (1)
| Tool | Description |
|------|-------------|
| `generate_business_page` | Generate hosted business page for verification *(enterprise only)* |
## Authentication

@@ -93,7 +224,17 @@

- **Test keys** (`sk_test_v1_...`) — sandbox mode, messages simulated, OTP codes returned in response
- **Live keys** (`sk_live_v1_...`) — real SMS delivery on verified phone numbers
- **Test keys** (`sk_test_v1_...`) — sandbox mode, no real SMS, OTP codes in response
- **Live keys** (`sk_live_v1_...`) — real SMS delivery, requires verified phone number
Get your API key at [sendly.live](https://sendly.live) → Settings → API Keys.
Get your key at [sendly.live](https://sendly.live) → Settings → API Keys.
## Sandbox Testing
With test keys, use magic numbers:
| Number | Behavior |
|--------|----------|
| +15005550000 | Always succeeds |
| +15005550001 | Invalid number |
| +15005550006 | Carrier rejected |
## Environment Variables

@@ -104,8 +245,13 @@

| `SENDLY_API_KEY` | Yes | Your Sendly API key |
| `SENDLY_BASE_URL` | No | API base URL (default: `https://sendly.live`) |
| `SENDLY_BASE_URL` | No | API base (default: `https://sendly.live`) |
## Links
- [Documentation](https://sendly.live/docs)
- [MCP Tools Reference](https://sendly.live/docs/ai/mcp-tools) — all 82 tools with schemas
- [Agent Skills](https://sendly.live/skills) — SKILL.md files for 8 platforms
- [API Reference](https://sendly.live/docs/api)
- [Sendly Dashboard](https://sendly.live)
- [Sendly Dashboard](https://sendly.live/dashboard)
## License
MIT