@sendly/mcp 2.0.1 → 2.3.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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +143 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @sendly/mcp
2
2
 
3
- 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).
3
+ Full SMS platform for AI agents — 85 tools for messaging, contacts, campaigns, templates, webhooks, OTP verification, conversations, labels, drafts, and more via [Model Context Protocol](https://modelcontextprotocol.io).
4
4
 
5
5
  ## Quick Setup
6
6
 
@@ -241,7 +241,7 @@ With test keys, use magic numbers:
241
241
 
242
242
  ## Links
243
243
 
244
- - [MCP Tools Reference](https://sendly.live/docs/ai/mcp-tools) — all 82 tools with schemas
244
+ - [MCP Tools Reference](https://sendly.live/docs/ai/mcp-tools) — all 85 tools with schemas
245
245
  - [Agent Skills](https://sendly.live/skills) — SKILL.md files for 8 platforms
246
246
  - [API Reference](https://sendly.live/docs/api)
247
247
  - [Sendly Dashboard](https://sendly.live/dashboard)
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@
3
3
  // src/index.ts
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { readFileSync } from "fs";
7
+ import { dirname, resolve } from "path";
8
+ import { fileURLToPath } from "url";
6
9
 
7
10
  // src/tools.ts
8
11
  import { z } from "zod";
@@ -439,6 +442,67 @@ function registerAllTools(server2, api2) {
439
442
  }
440
443
  }
441
444
  );
445
+ server2.tool(
446
+ "mark_contact_valid",
447
+ "Clear the invalid flag on a contact so future campaigns include it again. Contacts get auto-flagged as invalid when a send fails with a terminal bad-number error (landline, invalid number) or when a carrier lookup reports they can't receive SMS. Use this when you disagree with the auto-flag.",
448
+ {
449
+ contactId: z.string().describe("The contact ID to clear the invalid flag on")
450
+ },
451
+ async ({ contactId }) => {
452
+ try {
453
+ return ok(await api2("POST", `/contacts/${contactId}/mark-valid`));
454
+ } catch (e) {
455
+ return err(e);
456
+ }
457
+ }
458
+ );
459
+ server2.tool(
460
+ "check_contact_numbers",
461
+ "Trigger a background carrier lookup across contacts. Landlines and other non-SMS-capable numbers are auto-excluded from future campaigns. The lookup runs asynchronously (1-5 minutes). Results populate line_type, carrier_name, and invalid_reason fields on affected contacts. Idempotent: re-triggering while a lookup is running for the same scope is a no-op \u2014 response carries alreadyRunning: true in that case.",
462
+ {
463
+ listId: z.string().optional().describe("Scope the lookup to a single contact list"),
464
+ force: z.boolean().optional().describe("Re-check contacts even if already looked up (default: false)")
465
+ },
466
+ async ({ listId, force }) => {
467
+ try {
468
+ return ok(
469
+ await api2("POST", "/contacts/lookup", {
470
+ listId: listId ?? null,
471
+ force: force ?? false
472
+ })
473
+ );
474
+ } catch (e) {
475
+ return err(e);
476
+ }
477
+ }
478
+ );
479
+ server2.tool(
480
+ "bulk_mark_contacts_valid",
481
+ "Clear the invalid flag on many contacts at once (up to 10,000 per call). Pass either an explicit id array OR a listId \u2014 not both. Foreign ids silently no-op via the per-organization filter. Returns { cleared } \u2014 the number of contacts whose flag was actually cleared.",
482
+ {
483
+ ids: z.array(z.string()).max(1e4).optional().describe("Explicit contact ids to clear (max 10,000)"),
484
+ listId: z.string().optional().describe("Clear every flagged member of this list")
485
+ },
486
+ async ({ ids, listId }) => {
487
+ if (!ids && !listId) {
488
+ return err("bulk_mark_contacts_valid requires either 'ids' or 'listId'");
489
+ }
490
+ if (ids && listId) {
491
+ return err("bulk_mark_contacts_valid accepts 'ids' OR 'listId', not both");
492
+ }
493
+ try {
494
+ return ok(
495
+ await api2(
496
+ "POST",
497
+ "/contacts/bulk-mark-valid",
498
+ ids ? { ids } : { listId }
499
+ )
500
+ );
501
+ } catch (e) {
502
+ return err(e);
503
+ }
504
+ }
505
+ );
442
506
  server2.tool(
443
507
  "import_contacts",
444
508
  "Bulk import contacts from an array. Optionally add all imported contacts to a list. Returns created/updated/skipped counts.",
@@ -1177,6 +1241,78 @@ function registerAllTools(server2, api2) {
1177
1241
  }
1178
1242
  }
1179
1243
  );
1244
+ server2.tool(
1245
+ "reset_webhook_circuit",
1246
+ "Reset the circuit breaker for a webhook so deliveries resume immediately instead of waiting for the automatic 5-minute recovery.",
1247
+ { webhookId: z.string().describe("The webhook ID") },
1248
+ async ({ webhookId }) => {
1249
+ try {
1250
+ return ok(await api2("POST", `/webhooks/${webhookId}/reset-circuit`));
1251
+ } catch (e) {
1252
+ return err(e);
1253
+ }
1254
+ }
1255
+ );
1256
+ server2.tool(
1257
+ "redeliver_webhook",
1258
+ "Replay failed or cancelled webhook deliveries from the audit log for a time window. Use after a customer endpoint has recovered from an outage. Reset the circuit first if it is open. Each replay preserves the original event_id for dedup.",
1259
+ {
1260
+ webhookId: z.string().describe("The webhook ID"),
1261
+ since: z.string().optional().describe(
1262
+ "Earliest delivery time, ISO-8601. Defaults to 24h ago."
1263
+ ),
1264
+ until: z.string().optional().describe("Latest delivery time, ISO-8601. Defaults to now."),
1265
+ eventTypes: z.array(z.string()).optional().describe("Filter by event type. Defaults to all."),
1266
+ statuses: z.array(z.string()).optional().describe(
1267
+ "Replay deliveries in any of these statuses. Defaults to ['failed', 'cancelled']."
1268
+ ),
1269
+ limit: z.number().int().positive().max(1e4).optional().describe("Max deliveries to requeue. Defaults to 1000, max 10000.")
1270
+ },
1271
+ async ({ webhookId, since, until, eventTypes, statuses, limit }) => {
1272
+ try {
1273
+ const body = {};
1274
+ if (since !== void 0) body.since = since;
1275
+ if (until !== void 0) body.until = until;
1276
+ if (eventTypes !== void 0) body.event_types = eventTypes;
1277
+ if (statuses !== void 0) body.statuses = statuses;
1278
+ if (limit !== void 0) body.limit = limit;
1279
+ return ok(
1280
+ await api2("POST", `/webhooks/${webhookId}/redeliver`, body)
1281
+ );
1282
+ } catch (e) {
1283
+ return err(e);
1284
+ }
1285
+ }
1286
+ );
1287
+ server2.tool(
1288
+ "backfill_webhook",
1289
+ "Synthesize webhook events from the underlying message log for events that have no audit row (the case redeliver_webhook cannot recover). Use this when a circuit-breaker outage left events with no delivery record. Synthesized events have fresh IDs \u2014 clients should dedupe by event.data.object.id (the message ID). Reset the circuit first if it is open.",
1290
+ {
1291
+ webhookId: z.string().describe("The webhook ID"),
1292
+ since: z.string().optional().describe(
1293
+ "Earliest message time, ISO-8601. Defaults to 24h ago."
1294
+ ),
1295
+ until: z.string().optional().describe("Latest message time, ISO-8601. Defaults to now."),
1296
+ eventTypes: z.array(z.string()).optional().describe(
1297
+ "Filter by event type. Defaults to subscribed message events."
1298
+ ),
1299
+ limit: z.number().int().positive().max(1e4).optional().describe("Max events to synthesize. Defaults to 1000, max 10000.")
1300
+ },
1301
+ async ({ webhookId, since, until, eventTypes, limit }) => {
1302
+ try {
1303
+ const body = {};
1304
+ if (since !== void 0) body.since = since;
1305
+ if (until !== void 0) body.until = until;
1306
+ if (eventTypes !== void 0) body.event_types = eventTypes;
1307
+ if (limit !== void 0) body.limit = limit;
1308
+ return ok(
1309
+ await api2("POST", `/webhooks/${webhookId}/backfill`, body)
1310
+ );
1311
+ } catch (e) {
1312
+ return err(e);
1313
+ }
1314
+ }
1315
+ );
1180
1316
  server2.tool(
1181
1317
  "send_otp",
1182
1318
  "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.",
@@ -1356,7 +1492,11 @@ function registerAllTools(server2, api2) {
1356
1492
  }
1357
1493
 
1358
1494
  // src/index.ts
1359
- var VERSION = "2.0.0";
1495
+ var __dirname = dirname(fileURLToPath(import.meta.url));
1496
+ var pkg = JSON.parse(
1497
+ readFileSync(resolve(__dirname, "../package.json"), "utf8")
1498
+ );
1499
+ var VERSION = pkg.version;
1360
1500
  var API_KEY = process.env.SENDLY_API_KEY;
1361
1501
  var BASE_URL = process.env.SENDLY_BASE_URL || "https://sendly.live";
1362
1502
  if (!API_KEY) {
@@ -1396,7 +1536,8 @@ async function api(method, path, body, query) {
1396
1536
  }
1397
1537
  }
1398
1538
  const headers = {
1399
- Authorization: `Bearer ${API_KEY}`
1539
+ Authorization: `Bearer ${API_KEY}`,
1540
+ "User-Agent": `@sendly/mcp/${VERSION}`
1400
1541
  };
1401
1542
  if (body) headers["Content-Type"] = "application/json";
1402
1543
  const res = await fetch(url.toString(), {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendly/mcp",
3
- "version": "2.0.1",
3
+ "version": "2.3.0",
4
4
  "description": "Sendly MCP Server — Full SMS platform for AI agents. Messaging, contacts, campaigns, templates, webhooks, OTP verification, and more.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",