@sendly/mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # @sendly/mcp
2
+
3
+ SMS for AI agents — send messages, manage conversations, verify phone numbers via [Model Context Protocol](https://modelcontextprotocol.io).
4
+
5
+ ## Quick Setup
6
+
7
+ ### Claude Desktop
8
+
9
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "sendly": {
15
+ "command": "npx",
16
+ "args": ["-y", "@sendly/mcp"],
17
+ "env": {
18
+ "SENDLY_API_KEY": "sk_test_v1_your_key_here"
19
+ }
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ ### Cursor
26
+
27
+ Add to `.cursor/mcp.json` in your project:
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "sendly": {
33
+ "command": "npx",
34
+ "args": ["-y", "@sendly/mcp"],
35
+ "env": {
36
+ "SENDLY_API_KEY": "sk_test_v1_your_key_here"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ ### VS Code / Windsurf
44
+
45
+ Same pattern — set `SENDLY_API_KEY` in the environment and run `npx @sendly/mcp` as the MCP server command.
46
+
47
+ ## Available Tools
48
+
49
+ ### Messaging
50
+
51
+ | Tool | Description |
52
+ |------|-------------|
53
+ | `send_sms` | Send an SMS message to a phone number |
54
+ | `list_messages` | List sent and received messages with pagination |
55
+ | `get_message` | Get details of a specific message |
56
+ | `schedule_sms` | Schedule a message for future delivery |
57
+ | `cancel_scheduled_message` | Cancel a scheduled message (credits refunded) |
58
+
59
+ ### Conversations
60
+
61
+ | Tool | Description |
62
+ |------|-------------|
63
+ | `list_conversations` | List conversation threads by recent activity |
64
+ | `get_conversation` | Get a conversation with optional message history |
65
+ | `reply_to_conversation` | Reply within a conversation (auto-sets recipient) |
66
+ | `update_conversation` | Update metadata or tags on a conversation |
67
+ | `close_conversation` | Close a conversation (auto-reopens on new inbound) |
68
+ | `reopen_conversation` | Reopen a closed conversation |
69
+ | `mark_conversation_read` | Reset unread count to zero |
70
+
71
+ ### OTP / Verification
72
+
73
+ | Tool | Description |
74
+ |------|-------------|
75
+ | `send_otp` | Send a one-time password via SMS |
76
+ | `check_otp` | Verify an OTP code |
77
+ | `get_verification_status` | Check verification status |
78
+
79
+ ### Account
80
+
81
+ | Tool | Description |
82
+ |------|-------------|
83
+ | `get_account` | Get credit balance, verification status, rate limits |
84
+
85
+ ## Authentication
86
+
87
+ Set `SENDLY_API_KEY` as an environment variable:
88
+
89
+ - **Test keys** (`sk_test_v1_...`) — sandbox mode, messages simulated, OTP codes returned in response
90
+ - **Live keys** (`sk_live_v1_...`) — real SMS delivery on verified phone numbers
91
+
92
+ Get your API key at [sendly.live](https://sendly.live) → Settings → API Keys.
93
+
94
+ ## Environment Variables
95
+
96
+ | Variable | Required | Description |
97
+ |----------|----------|-------------|
98
+ | `SENDLY_API_KEY` | Yes | Your Sendly API key |
99
+ | `SENDLY_BASE_URL` | No | API base URL (default: `https://sendly.live`) |
100
+
101
+ ## Links
102
+
103
+ - [Documentation](https://sendly.live/docs)
104
+ - [API Reference](https://sendly.live/docs/api)
105
+ - [Sendly Dashboard](https://sendly.live)
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ var API_KEY = process.env.SENDLY_API_KEY;
8
+ var BASE_URL = process.env.SENDLY_BASE_URL || "https://sendly.live";
9
+ if (!API_KEY) {
10
+ process.stderr.write(
11
+ "SENDLY_API_KEY environment variable is required.\nGet your API key at https://sendly.live \u2192 Settings \u2192 API Keys\n"
12
+ );
13
+ process.exit(1);
14
+ }
15
+ async function api(method, path, body, query) {
16
+ const url = new URL(`/api/v1${path}`, BASE_URL);
17
+ if (query) {
18
+ for (const [k, v] of Object.entries(query)) {
19
+ if (v !== void 0) url.searchParams.set(k, v);
20
+ }
21
+ }
22
+ const headers = {
23
+ Authorization: `Bearer ${API_KEY}`
24
+ };
25
+ if (body) headers["Content-Type"] = "application/json";
26
+ const res = await fetch(url.toString(), {
27
+ method,
28
+ headers,
29
+ body: body ? JSON.stringify(body) : void 0
30
+ });
31
+ if (res.status === 204) return { success: true };
32
+ const data = await res.json();
33
+ if (!res.ok) {
34
+ const msg = typeof data === "object" && data !== null ? data.error || data.message || JSON.stringify(data) : String(data);
35
+ throw new Error(String(msg));
36
+ }
37
+ return data;
38
+ }
39
+ function ok(data) {
40
+ return {
41
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
42
+ };
43
+ }
44
+ function err(error) {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ return {
47
+ content: [{ type: "text", text: `Error: ${message}` }],
48
+ isError: true
49
+ };
50
+ }
51
+ var server = new McpServer({
52
+ name: "sendly",
53
+ version: "1.0.0"
54
+ });
55
+ server.tool(
56
+ "send_sms",
57
+ "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.",
58
+ {
59
+ to: z.string().describe("Recipient phone number in E.164 format (+14155551234)"),
60
+ text: z.string().describe("Message text content"),
61
+ messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)"),
62
+ metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata to attach")
63
+ },
64
+ async ({ to, text, messageType, metadata }) => {
65
+ try {
66
+ const body = { to, text };
67
+ if (messageType) body.messageType = messageType;
68
+ if (metadata) body.metadata = metadata;
69
+ return ok(await api("POST", "/messages", body));
70
+ } catch (e) {
71
+ return err(e);
72
+ }
73
+ }
74
+ );
75
+ server.tool(
76
+ "list_messages",
77
+ "List sent and received SMS messages with pagination, ordered by creation date.",
78
+ {
79
+ limit: z.number().optional().describe("Messages to return (1-100, default 50)"),
80
+ offset: z.number().optional().describe("Pagination offset"),
81
+ status: z.enum(["queued", "sent", "delivered", "failed"]).optional().describe("Filter by delivery status")
82
+ },
83
+ async ({ limit, offset, status }) => {
84
+ try {
85
+ return ok(
86
+ await api("GET", "/messages", void 0, {
87
+ limit: limit?.toString(),
88
+ offset: offset?.toString(),
89
+ status
90
+ })
91
+ );
92
+ } catch (e) {
93
+ return err(e);
94
+ }
95
+ }
96
+ );
97
+ server.tool(
98
+ "get_message",
99
+ "Get details of a specific SMS message including delivery status, timestamps, and metadata.",
100
+ {
101
+ messageId: z.string().describe("The message ID")
102
+ },
103
+ async ({ messageId }) => {
104
+ try {
105
+ return ok(await api("GET", `/messages/${messageId}`));
106
+ } catch (e) {
107
+ return err(e);
108
+ }
109
+ }
110
+ );
111
+ server.tool(
112
+ "schedule_sms",
113
+ "Schedule an SMS for future delivery (5 minutes to 5 days from now). Credits are reserved immediately and refunded if cancelled.",
114
+ {
115
+ to: z.string().describe("Recipient phone number in E.164 format"),
116
+ text: z.string().describe("Message text content"),
117
+ scheduledAt: z.string().describe("ISO 8601 datetime for delivery (e.g., 2026-03-16T09:00:00Z)"),
118
+ messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)")
119
+ },
120
+ async ({ to, text, scheduledAt, messageType }) => {
121
+ try {
122
+ const body = { to, text, scheduledAt };
123
+ if (messageType) body.messageType = messageType;
124
+ return ok(await api("POST", "/messages/schedule", body));
125
+ } catch (e) {
126
+ return err(e);
127
+ }
128
+ }
129
+ );
130
+ server.tool(
131
+ "cancel_scheduled_message",
132
+ "Cancel a scheduled message before it sends. Credits are refunded automatically.",
133
+ {
134
+ messageId: z.string().describe("The scheduled message ID to cancel")
135
+ },
136
+ async ({ messageId }) => {
137
+ try {
138
+ return ok(await api("DELETE", `/messages/scheduled/${messageId}`));
139
+ } catch (e) {
140
+ return err(e);
141
+ }
142
+ }
143
+ );
144
+ server.tool(
145
+ "list_conversations",
146
+ "List SMS conversation threads ordered by most recent activity. Each conversation groups all messages with a specific phone number.",
147
+ {
148
+ limit: z.number().optional().describe("Conversations to return (1-100, default 50)"),
149
+ offset: z.number().optional().describe("Pagination offset"),
150
+ status: z.enum(["active", "closed"]).optional().describe("Filter by conversation status")
151
+ },
152
+ async ({ limit, offset, status }) => {
153
+ try {
154
+ return ok(
155
+ await api("GET", "/conversations", void 0, {
156
+ limit: limit?.toString(),
157
+ offset: offset?.toString(),
158
+ status
159
+ })
160
+ );
161
+ } catch (e) {
162
+ return err(e);
163
+ }
164
+ }
165
+ );
166
+ server.tool(
167
+ "get_conversation",
168
+ "Get a conversation thread by ID. Set includeMessages=true to load the message history.",
169
+ {
170
+ conversationId: z.string().describe("The conversation ID"),
171
+ includeMessages: z.boolean().optional().describe("Include message history (default false)"),
172
+ messageLimit: z.number().optional().describe("Number of messages to include (default 50)")
173
+ },
174
+ async ({ conversationId, includeMessages, messageLimit }) => {
175
+ try {
176
+ return ok(
177
+ await api("GET", `/conversations/${conversationId}`, void 0, {
178
+ include_messages: includeMessages ? "true" : void 0,
179
+ message_limit: messageLimit?.toString()
180
+ })
181
+ );
182
+ } catch (e) {
183
+ return err(e);
184
+ }
185
+ }
186
+ );
187
+ server.tool(
188
+ "reply_to_conversation",
189
+ "Send a reply within an existing conversation. The recipient is automatically set from the conversation's phone number.",
190
+ {
191
+ conversationId: z.string().describe("The conversation ID to reply in"),
192
+ text: z.string().describe("Reply message text"),
193
+ mediaUrls: z.array(z.string()).optional().describe("Media URLs for MMS")
194
+ },
195
+ async ({ conversationId, text, mediaUrls }) => {
196
+ try {
197
+ const body = { text };
198
+ if (mediaUrls?.length) body.mediaUrls = mediaUrls;
199
+ return ok(
200
+ await api("POST", `/conversations/${conversationId}/messages`, body)
201
+ );
202
+ } catch (e) {
203
+ return err(e);
204
+ }
205
+ }
206
+ );
207
+ server.tool(
208
+ "update_conversation",
209
+ "Update a conversation's metadata or tags. Use metadata for custom key-value data, tags for categorization.",
210
+ {
211
+ conversationId: z.string().describe("The conversation ID"),
212
+ metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata"),
213
+ tags: z.array(z.string()).optional().describe("Tags for categorization (replaces existing tags)")
214
+ },
215
+ async ({ conversationId, metadata, tags }) => {
216
+ try {
217
+ const body = {};
218
+ if (metadata) body.metadata = metadata;
219
+ if (tags) body.tags = tags;
220
+ return ok(await api("PATCH", `/conversations/${conversationId}`, body));
221
+ } catch (e) {
222
+ return err(e);
223
+ }
224
+ }
225
+ );
226
+ server.tool(
227
+ "close_conversation",
228
+ "Close a conversation. Closed conversations auto-reopen when a new inbound message arrives.",
229
+ {
230
+ conversationId: z.string().describe("The conversation ID to close")
231
+ },
232
+ async ({ conversationId }) => {
233
+ try {
234
+ return ok(await api("POST", `/conversations/${conversationId}/close`));
235
+ } catch (e) {
236
+ return err(e);
237
+ }
238
+ }
239
+ );
240
+ server.tool(
241
+ "reopen_conversation",
242
+ "Reopen a previously closed conversation, setting its status back to active.",
243
+ {
244
+ conversationId: z.string().describe("The conversation ID to reopen")
245
+ },
246
+ async ({ conversationId }) => {
247
+ try {
248
+ return ok(await api("POST", `/conversations/${conversationId}/reopen`));
249
+ } catch (e) {
250
+ return err(e);
251
+ }
252
+ }
253
+ );
254
+ server.tool(
255
+ "mark_conversation_read",
256
+ "Mark a conversation as read, resetting the unread count to zero.",
257
+ {
258
+ conversationId: z.string().describe("The conversation ID")
259
+ },
260
+ async ({ conversationId }) => {
261
+ try {
262
+ return ok(
263
+ await api("POST", `/conversations/${conversationId}/mark-read`)
264
+ );
265
+ } catch (e) {
266
+ return err(e);
267
+ }
268
+ }
269
+ );
270
+ server.tool(
271
+ "send_otp",
272
+ "Send an OTP verification code via SMS. Use for phone verification, 2FA, or identity confirmation. Returns a verification ID to check the code against. In sandbox mode (test API key), the code is returned in the response for testing.",
273
+ {
274
+ to: z.string().describe("Phone number to verify in E.164 format"),
275
+ appName: z.string().optional().describe("Your app/brand name shown in the SMS"),
276
+ codeLength: z.number().optional().describe("Digits in the code (default 6)"),
277
+ timeoutSecs: z.number().optional().describe("Code validity in seconds (default 300)")
278
+ },
279
+ async ({ to, appName, codeLength, timeoutSecs }) => {
280
+ try {
281
+ const body = { to };
282
+ if (appName) body.appName = appName;
283
+ if (codeLength) body.codeLength = codeLength;
284
+ if (timeoutSecs) body.timeoutSecs = timeoutSecs;
285
+ return ok(await api("POST", "/verify", body));
286
+ } catch (e) {
287
+ return err(e);
288
+ }
289
+ }
290
+ );
291
+ server.tool(
292
+ "check_otp",
293
+ "Verify an OTP code. Returns the verification status: 'verified' (correct), 'invalid_code' (wrong code, shows remaining attempts), 'expired', or 'max_attempts_exceeded'.",
294
+ {
295
+ verificationId: z.string().describe("The verification ID from send_otp"),
296
+ code: z.string().describe("The code entered by the user")
297
+ },
298
+ async ({ verificationId, code }) => {
299
+ try {
300
+ return ok(await api("POST", `/verify/${verificationId}/check`, { code }));
301
+ } catch (e) {
302
+ return err(e);
303
+ }
304
+ }
305
+ );
306
+ server.tool(
307
+ "get_verification_status",
308
+ "Check the current status of an OTP verification (pending, verified, expired, failed).",
309
+ {
310
+ verificationId: z.string().describe("The verification ID")
311
+ },
312
+ async ({ verificationId }) => {
313
+ try {
314
+ return ok(await api("GET", `/verify/${verificationId}`));
315
+ } catch (e) {
316
+ return err(e);
317
+ }
318
+ }
319
+ );
320
+ server.tool(
321
+ "get_account",
322
+ "Get account info: credit balance, phone number verification status, rate limits, and API key details. Check this to understand available credits and sending capabilities.",
323
+ {},
324
+ async () => {
325
+ try {
326
+ return ok(await api("GET", "/account"));
327
+ } catch (e) {
328
+ return err(e);
329
+ }
330
+ }
331
+ );
332
+ var transport = new StdioServerTransport();
333
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@sendly/mcp",
3
+ "version": "1.0.0",
4
+ "description": "Sendly MCP Server — SMS for AI agents. Send messages, manage conversations, verify phone numbers.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "sendly-mcp": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "dev": "tsup --watch",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "sendly",
22
+ "sms",
23
+ "mcp",
24
+ "model-context-protocol",
25
+ "ai-agents",
26
+ "messaging",
27
+ "otp",
28
+ "verification"
29
+ ],
30
+ "author": "Sendly <support@sendly.live>",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/SendlyHQ/sendly-mcp.git"
35
+ },
36
+ "homepage": "https://sendly.live/docs",
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.12.0",
42
+ "zod": "^3.23.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20.0.0",
46
+ "tsup": "^8.0.0",
47
+ "typescript": "^5.0.0"
48
+ }
49
+ }