@sendly/mcp 1.0.0 → 1.2.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/dist/index.js +223 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
var VERSION = "1.2.0";
|
|
7
8
|
var API_KEY = process.env.SENDLY_API_KEY;
|
|
8
9
|
var BASE_URL = process.env.SENDLY_BASE_URL || "https://sendly.live";
|
|
9
10
|
if (!API_KEY) {
|
|
@@ -12,7 +13,30 @@ if (!API_KEY) {
|
|
|
12
13
|
);
|
|
13
14
|
process.exit(1);
|
|
14
15
|
}
|
|
16
|
+
if (!BASE_URL.startsWith("https://") && !BASE_URL.startsWith("http://localhost") && !BASE_URL.startsWith("http://127.0.0.1")) {
|
|
17
|
+
process.stderr.write(
|
|
18
|
+
"SENDLY_BASE_URL must use HTTPS in production.\nHTTP is only allowed for localhost development.\n"
|
|
19
|
+
);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
23
|
+
var RATE_LIMIT_MAX = 30;
|
|
24
|
+
var rateLimitTokens = RATE_LIMIT_MAX;
|
|
25
|
+
var rateLimitResetAt = Date.now() + RATE_LIMIT_WINDOW_MS;
|
|
26
|
+
function checkRateLimit() {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
if (now >= rateLimitResetAt) {
|
|
29
|
+
rateLimitTokens = RATE_LIMIT_MAX;
|
|
30
|
+
rateLimitResetAt = now + RATE_LIMIT_WINDOW_MS;
|
|
31
|
+
}
|
|
32
|
+
if (rateLimitTokens <= 0) return false;
|
|
33
|
+
rateLimitTokens--;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
15
36
|
async function api(method, path, body, query) {
|
|
37
|
+
if (!checkRateLimit()) {
|
|
38
|
+
throw new Error("Rate limited \u2014 too many requests. Wait a moment and try again.");
|
|
39
|
+
}
|
|
16
40
|
const url = new URL(`/api/v1${path}`, BASE_URL);
|
|
17
41
|
if (query) {
|
|
18
42
|
for (const [k, v] of Object.entries(query)) {
|
|
@@ -29,6 +53,12 @@ async function api(method, path, body, query) {
|
|
|
29
53
|
body: body ? JSON.stringify(body) : void 0
|
|
30
54
|
});
|
|
31
55
|
if (res.status === 204) return { success: true };
|
|
56
|
+
if (res.status === 429) {
|
|
57
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Rate limited by API. ${retryAfter ? `Retry after ${retryAfter} seconds.` : "Wait a moment and try again."}`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
32
62
|
const data = await res.json();
|
|
33
63
|
if (!res.ok) {
|
|
34
64
|
const msg = typeof data === "object" && data !== null ? data.error || data.message || JSON.stringify(data) : String(data);
|
|
@@ -50,7 +80,7 @@ function err(error) {
|
|
|
50
80
|
}
|
|
51
81
|
var server = new McpServer({
|
|
52
82
|
name: "sendly",
|
|
53
|
-
version:
|
|
83
|
+
version: VERSION
|
|
54
84
|
});
|
|
55
85
|
server.tool(
|
|
56
86
|
"send_sms",
|
|
@@ -163,6 +193,25 @@ server.tool(
|
|
|
163
193
|
}
|
|
164
194
|
}
|
|
165
195
|
);
|
|
196
|
+
server.tool(
|
|
197
|
+
"get_conversation_context",
|
|
198
|
+
"Get LLM-ready formatted conversation context. Returns a pre-formatted text string with timestamped messages, AI classification, and business context \u2014 ready to paste into a prompt. More efficient than get_conversation for AI agents.",
|
|
199
|
+
{
|
|
200
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
201
|
+
maxMessages: z.number().optional().describe("Max messages to include (default 20, max 50)")
|
|
202
|
+
},
|
|
203
|
+
async ({ conversationId, maxMessages }) => {
|
|
204
|
+
try {
|
|
205
|
+
return ok(
|
|
206
|
+
await api("GET", `/conversations/${conversationId}/context`, void 0, {
|
|
207
|
+
max_messages: maxMessages?.toString()
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return err(e);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
);
|
|
166
215
|
server.tool(
|
|
167
216
|
"get_conversation",
|
|
168
217
|
"Get a conversation thread by ID. Set includeMessages=true to load the message history.",
|
|
@@ -267,6 +316,158 @@ server.tool(
|
|
|
267
316
|
}
|
|
268
317
|
}
|
|
269
318
|
);
|
|
319
|
+
server.tool(
|
|
320
|
+
"get_suggested_replies",
|
|
321
|
+
"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).",
|
|
322
|
+
{
|
|
323
|
+
conversationId: z.string().describe("The conversation ID to generate suggestions for")
|
|
324
|
+
},
|
|
325
|
+
async ({ conversationId }) => {
|
|
326
|
+
try {
|
|
327
|
+
return ok(
|
|
328
|
+
await api("POST", `/conversations/${conversationId}/suggest-replies`)
|
|
329
|
+
);
|
|
330
|
+
} catch (e) {
|
|
331
|
+
return err(e);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
server.tool(
|
|
336
|
+
"create_label",
|
|
337
|
+
"Create a label for categorizing conversations and messages. Labels have a name and optional color.",
|
|
338
|
+
{
|
|
339
|
+
name: z.string().describe("Label name (e.g., 'urgent', 'vip', 'follow-up')"),
|
|
340
|
+
color: z.string().optional().describe("Hex color code (default: #6b7280)"),
|
|
341
|
+
description: z.string().optional().describe("Label description")
|
|
342
|
+
},
|
|
343
|
+
async ({ name, color, description }) => {
|
|
344
|
+
try {
|
|
345
|
+
const body = { name };
|
|
346
|
+
if (color) body.color = color;
|
|
347
|
+
if (description) body.description = description;
|
|
348
|
+
return ok(await api("POST", "/labels", body));
|
|
349
|
+
} catch (e) {
|
|
350
|
+
return err(e);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
server.tool(
|
|
355
|
+
"list_labels",
|
|
356
|
+
"List all labels available in your workspace.",
|
|
357
|
+
{},
|
|
358
|
+
async () => {
|
|
359
|
+
try {
|
|
360
|
+
return ok(await api("GET", "/labels"));
|
|
361
|
+
} catch (e) {
|
|
362
|
+
return err(e);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
server.tool(
|
|
367
|
+
"add_conversation_label",
|
|
368
|
+
"Add one or more labels to a conversation for categorization.",
|
|
369
|
+
{
|
|
370
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
371
|
+
labelIds: z.array(z.string()).describe("Array of label IDs to add")
|
|
372
|
+
},
|
|
373
|
+
async ({ conversationId, labelIds }) => {
|
|
374
|
+
try {
|
|
375
|
+
return ok(
|
|
376
|
+
await api("POST", `/conversations/${conversationId}/labels`, {
|
|
377
|
+
labelIds
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
} catch (e) {
|
|
381
|
+
return err(e);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
server.tool(
|
|
386
|
+
"remove_conversation_label",
|
|
387
|
+
"Remove a label from a conversation.",
|
|
388
|
+
{
|
|
389
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
390
|
+
labelId: z.string().describe("The label ID to remove")
|
|
391
|
+
},
|
|
392
|
+
async ({ conversationId, labelId }) => {
|
|
393
|
+
try {
|
|
394
|
+
return ok(
|
|
395
|
+
await api("DELETE", `/conversations/${conversationId}/labels/${labelId}`)
|
|
396
|
+
);
|
|
397
|
+
} catch (e) {
|
|
398
|
+
return err(e);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
server.tool(
|
|
403
|
+
"create_draft",
|
|
404
|
+
"Create a message draft for human review before sending. The draft must be approved before it becomes a real SMS.",
|
|
405
|
+
{
|
|
406
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
407
|
+
text: z.string().describe("Draft message text"),
|
|
408
|
+
source: z.string().optional().describe("Source of the draft (default: 'ai')")
|
|
409
|
+
},
|
|
410
|
+
async ({ conversationId, text, source }) => {
|
|
411
|
+
try {
|
|
412
|
+
const body = { conversationId, text };
|
|
413
|
+
if (source) body.source = source;
|
|
414
|
+
return ok(await api("POST", "/drafts", body));
|
|
415
|
+
} catch (e) {
|
|
416
|
+
return err(e);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
);
|
|
420
|
+
server.tool(
|
|
421
|
+
"list_drafts",
|
|
422
|
+
"List message drafts, optionally filtered by conversation or status.",
|
|
423
|
+
{
|
|
424
|
+
conversationId: z.string().optional().describe("Filter by conversation ID"),
|
|
425
|
+
status: z.enum(["pending", "approved", "rejected", "sent", "failed"]).optional().describe("Filter by status")
|
|
426
|
+
},
|
|
427
|
+
async ({ conversationId, status }) => {
|
|
428
|
+
try {
|
|
429
|
+
return ok(
|
|
430
|
+
await api("GET", "/drafts", void 0, {
|
|
431
|
+
conversation_id: conversationId,
|
|
432
|
+
status
|
|
433
|
+
})
|
|
434
|
+
);
|
|
435
|
+
} catch (e) {
|
|
436
|
+
return err(e);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
server.tool(
|
|
441
|
+
"approve_draft",
|
|
442
|
+
"Approve a pending draft and send it as a real SMS message. Runs compliance checks and deducts credits at approval time.",
|
|
443
|
+
{
|
|
444
|
+
draftId: z.string().describe("The draft ID to approve")
|
|
445
|
+
},
|
|
446
|
+
async ({ draftId }) => {
|
|
447
|
+
try {
|
|
448
|
+
return ok(await api("POST", `/drafts/${draftId}/approve`));
|
|
449
|
+
} catch (e) {
|
|
450
|
+
return err(e);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
server.tool(
|
|
455
|
+
"reject_draft",
|
|
456
|
+
"Reject a pending draft with an optional reason. The message will not be sent.",
|
|
457
|
+
{
|
|
458
|
+
draftId: z.string().describe("The draft ID to reject"),
|
|
459
|
+
reason: z.string().optional().describe("Reason for rejection")
|
|
460
|
+
},
|
|
461
|
+
async ({ draftId, reason }) => {
|
|
462
|
+
try {
|
|
463
|
+
const body = {};
|
|
464
|
+
if (reason) body.reason = reason;
|
|
465
|
+
return ok(await api("POST", `/drafts/${draftId}/reject`, body));
|
|
466
|
+
} catch (e) {
|
|
467
|
+
return err(e);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
);
|
|
270
471
|
server.tool(
|
|
271
472
|
"send_otp",
|
|
272
473
|
"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.",
|
|
@@ -329,5 +530,26 @@ server.tool(
|
|
|
329
530
|
}
|
|
330
531
|
}
|
|
331
532
|
);
|
|
533
|
+
server.tool(
|
|
534
|
+
"generate_business_page",
|
|
535
|
+
"Generate a hosted business landing page for verification. Use when a business doesn't have their own website. Returns a URL at sendly.live/biz/{slug} that satisfies carrier website requirements.",
|
|
536
|
+
{
|
|
537
|
+
businessName: z.string().describe("Business name"),
|
|
538
|
+
useCase: z.string().optional().describe("Use case (e.g., Insurance Services, Appointment Reminders, 2FA)"),
|
|
539
|
+
useCaseSummary: z.string().optional().describe("Brief description of what the business does"),
|
|
540
|
+
contactEmail: z.string().optional().describe("Business contact email"),
|
|
541
|
+
contactPhone: z.string().optional().describe("Business phone number"),
|
|
542
|
+
businessAddress: z.string().optional().describe("City, State ZIP (e.g., Chicago, IL 60601)")
|
|
543
|
+
},
|
|
544
|
+
async (params) => {
|
|
545
|
+
try {
|
|
546
|
+
return ok(
|
|
547
|
+
await api("POST", "/enterprise/business-page/generate", params)
|
|
548
|
+
);
|
|
549
|
+
} catch (e) {
|
|
550
|
+
return err(e);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
);
|
|
332
554
|
var transport = new StdioServerTransport();
|
|
333
555
|
await server.connect(transport);
|