@sendly/mcp
Advanced tools
+1322
-1343
@@ -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); |
+13
-2
| { | ||
| "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 |
57747
14.34%252
137.74%1425
-1.59%