@sendly/mcp 2.0.0 → 2.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/README.md +178 -32
- package/dist/index.js +1384 -1343
- package/package.json +13 -2
package/dist/index.js
CHANGED
|
@@ -3,69 +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
|
+
|
|
7
|
+
// src/tools.ts
|
|
6
8
|
import { z } from "zod";
|
|
7
|
-
var VERSION = "2.0.0";
|
|
8
|
-
var API_KEY = process.env.SENDLY_API_KEY;
|
|
9
|
-
var BASE_URL = process.env.SENDLY_BASE_URL || "https://sendly.live";
|
|
10
|
-
if (!API_KEY) {
|
|
11
|
-
process.stderr.write(
|
|
12
|
-
"SENDLY_API_KEY environment variable is required.\nGet your API key at https://sendly.live \u2192 Settings \u2192 API Keys\n"
|
|
13
|
-
);
|
|
14
|
-
process.exit(1);
|
|
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 = 60;
|
|
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
|
-
}
|
|
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
|
-
}
|
|
40
|
-
const url = new URL(`/api/v1${path}`, BASE_URL);
|
|
41
|
-
if (query) {
|
|
42
|
-
for (const [k, v] of Object.entries(query)) {
|
|
43
|
-
if (v !== void 0) url.searchParams.set(k, v);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
const headers = {
|
|
47
|
-
Authorization: `Bearer ${API_KEY}`
|
|
48
|
-
};
|
|
49
|
-
if (body) headers["Content-Type"] = "application/json";
|
|
50
|
-
const res = await fetch(url.toString(), {
|
|
51
|
-
method,
|
|
52
|
-
headers,
|
|
53
|
-
body: body ? JSON.stringify(body) : void 0
|
|
54
|
-
});
|
|
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
|
-
}
|
|
62
|
-
const data = await res.json();
|
|
63
|
-
if (!res.ok) {
|
|
64
|
-
const msg = typeof data === "object" && data !== null ? data.error || data.message || JSON.stringify(data) : String(data);
|
|
65
|
-
throw new Error(String(msg));
|
|
66
|
-
}
|
|
67
|
-
return data;
|
|
68
|
-
}
|
|
69
9
|
function ok(data) {
|
|
70
10
|
return {
|
|
71
11
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
@@ -78,1371 +18,1472 @@ function err(error) {
|
|
|
78
18
|
isError: true
|
|
79
19
|
};
|
|
80
20
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
} catch (e) {
|
|
101
|
-
return err(e);
|
|
21
|
+
function registerAllTools(server2, api2) {
|
|
22
|
+
server2.tool(
|
|
23
|
+
"send_sms",
|
|
24
|
+
"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.",
|
|
25
|
+
{
|
|
26
|
+
to: z.string().describe("Recipient phone number in E.164 format (+14155551234)"),
|
|
27
|
+
text: z.string().describe("Message text content"),
|
|
28
|
+
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)"),
|
|
29
|
+
metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata to attach")
|
|
30
|
+
},
|
|
31
|
+
async ({ to, text, messageType, metadata }) => {
|
|
32
|
+
try {
|
|
33
|
+
const body = { to, text };
|
|
34
|
+
if (messageType) body.messageType = messageType;
|
|
35
|
+
if (metadata) body.metadata = metadata;
|
|
36
|
+
return ok(await api2("POST", "/messages", body));
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return err(e);
|
|
39
|
+
}
|
|
102
40
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
} catch (e) {
|
|
125
|
-
return err(e);
|
|
41
|
+
);
|
|
42
|
+
server2.tool(
|
|
43
|
+
"list_messages",
|
|
44
|
+
"List sent and received SMS messages with pagination. Use q parameter for full-text search across message content.",
|
|
45
|
+
{
|
|
46
|
+
limit: z.number().optional().describe("Messages to return (1-100, default 50)"),
|
|
47
|
+
offset: z.number().optional().describe("Pagination offset"),
|
|
48
|
+
q: z.string().optional().describe("Full-text search query for message content")
|
|
49
|
+
},
|
|
50
|
+
async ({ limit, offset, q }) => {
|
|
51
|
+
try {
|
|
52
|
+
return ok(
|
|
53
|
+
await api2("GET", "/messages", void 0, {
|
|
54
|
+
limit: limit?.toString(),
|
|
55
|
+
offset: offset?.toString(),
|
|
56
|
+
q
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return err(e);
|
|
61
|
+
}
|
|
126
62
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
63
|
+
);
|
|
64
|
+
server2.tool(
|
|
65
|
+
"get_message",
|
|
66
|
+
"Get details of a specific SMS message including delivery status, timestamps, and metadata.",
|
|
67
|
+
{
|
|
68
|
+
messageId: z.string().describe("The message ID")
|
|
69
|
+
},
|
|
70
|
+
async ({ messageId }) => {
|
|
71
|
+
try {
|
|
72
|
+
return ok(await api2("GET", `/messages/${messageId}`));
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return err(e);
|
|
75
|
+
}
|
|
140
76
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
77
|
+
);
|
|
78
|
+
server2.tool(
|
|
79
|
+
"schedule_sms",
|
|
80
|
+
"Schedule an SMS for future delivery (5 minutes to 5 days from now). Credits are reserved immediately and refunded if cancelled.",
|
|
81
|
+
{
|
|
82
|
+
to: z.string().describe("Recipient phone number in E.164 format"),
|
|
83
|
+
text: z.string().describe("Message text content"),
|
|
84
|
+
scheduledAt: z.string().describe("ISO 8601 datetime for delivery (e.g., 2026-03-16T09:00:00Z)"),
|
|
85
|
+
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)")
|
|
86
|
+
},
|
|
87
|
+
async ({ to, text, scheduledAt, messageType }) => {
|
|
88
|
+
try {
|
|
89
|
+
const body = { to, text, scheduledAt };
|
|
90
|
+
if (messageType) body.messageType = messageType;
|
|
91
|
+
return ok(await api2("POST", "/messages/schedule", body));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return err(e);
|
|
94
|
+
}
|
|
159
95
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
96
|
+
);
|
|
97
|
+
server2.tool(
|
|
98
|
+
"cancel_scheduled_message",
|
|
99
|
+
"Cancel a scheduled message before it sends. Credits are refunded automatically.",
|
|
100
|
+
{
|
|
101
|
+
messageId: z.string().describe("The scheduled message ID to cancel")
|
|
102
|
+
},
|
|
103
|
+
async ({ messageId }) => {
|
|
104
|
+
try {
|
|
105
|
+
return ok(await api2("DELETE", `/messages/scheduled/${messageId}`));
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return err(e);
|
|
108
|
+
}
|
|
173
109
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
|
|
110
|
+
);
|
|
111
|
+
server2.tool(
|
|
112
|
+
"list_scheduled_messages",
|
|
113
|
+
"List all scheduled messages that haven't been sent yet.",
|
|
114
|
+
{
|
|
115
|
+
limit: z.number().optional().describe("Messages to return (1-100, default 50)"),
|
|
116
|
+
offset: z.number().optional().describe("Pagination offset")
|
|
117
|
+
},
|
|
118
|
+
async ({ limit, offset }) => {
|
|
119
|
+
try {
|
|
120
|
+
return ok(
|
|
121
|
+
await api2("GET", "/messages/scheduled", void 0, {
|
|
122
|
+
limit: limit?.toString(),
|
|
123
|
+
offset: offset?.toString()
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return err(e);
|
|
128
|
+
}
|
|
193
129
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
} catch (e) {
|
|
214
|
-
return err(e);
|
|
130
|
+
);
|
|
131
|
+
server2.tool(
|
|
132
|
+
"send_batch",
|
|
133
|
+
"Send multiple SMS messages in a single batch (up to 1000). More efficient than individual sends for bulk messaging.",
|
|
134
|
+
{
|
|
135
|
+
messages: z.array(z.object({
|
|
136
|
+
to: z.string().describe("Recipient phone number in E.164 format"),
|
|
137
|
+
text: z.string().describe("Message text")
|
|
138
|
+
})).describe("Array of messages to send (max 1000)"),
|
|
139
|
+
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type for all messages (default: marketing)")
|
|
140
|
+
},
|
|
141
|
+
async ({ messages, messageType }) => {
|
|
142
|
+
try {
|
|
143
|
+
const body = { messages };
|
|
144
|
+
if (messageType) body.messageType = messageType;
|
|
145
|
+
return ok(await api2("POST", "/messages/batch", body));
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return err(e);
|
|
148
|
+
}
|
|
215
149
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
150
|
+
);
|
|
151
|
+
server2.tool(
|
|
152
|
+
"preview_batch",
|
|
153
|
+
"Preview a batch without sending. Returns credit cost estimate and validation results.",
|
|
154
|
+
{
|
|
155
|
+
messages: z.array(z.object({
|
|
156
|
+
to: z.string().describe("Recipient phone number in E.164 format"),
|
|
157
|
+
text: z.string().describe("Message text")
|
|
158
|
+
})).describe("Array of messages to preview"),
|
|
159
|
+
messageType: z.enum(["marketing", "transactional"]).optional().describe("Message type (default: marketing)")
|
|
160
|
+
},
|
|
161
|
+
async ({ messages, messageType }) => {
|
|
162
|
+
try {
|
|
163
|
+
const body = { messages };
|
|
164
|
+
if (messageType) body.messageType = messageType;
|
|
165
|
+
return ok(await api2("POST", "/messages/batch/preview", body));
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return err(e);
|
|
168
|
+
}
|
|
235
169
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
const body = { messages };
|
|
251
|
-
if (messageType) body.messageType = messageType;
|
|
252
|
-
return ok(await api("POST", "/messages/batch/preview", body));
|
|
253
|
-
} catch (e) {
|
|
254
|
-
return err(e);
|
|
170
|
+
);
|
|
171
|
+
server2.tool(
|
|
172
|
+
"get_batch",
|
|
173
|
+
"Get the status of a message batch including per-message delivery results.",
|
|
174
|
+
{
|
|
175
|
+
batchId: z.string().describe("The batch ID")
|
|
176
|
+
},
|
|
177
|
+
async ({ batchId }) => {
|
|
178
|
+
try {
|
|
179
|
+
return ok(await api2("GET", `/messages/batch/${batchId}`));
|
|
180
|
+
} catch (e) {
|
|
181
|
+
return err(e);
|
|
182
|
+
}
|
|
255
183
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
184
|
+
);
|
|
185
|
+
server2.tool(
|
|
186
|
+
"list_batches",
|
|
187
|
+
"List message batches with pagination.",
|
|
188
|
+
{
|
|
189
|
+
limit: z.number().optional().describe("Batches to return (1-100, default 50)"),
|
|
190
|
+
offset: z.number().optional().describe("Pagination offset")
|
|
191
|
+
},
|
|
192
|
+
async ({ limit, offset }) => {
|
|
193
|
+
try {
|
|
194
|
+
return ok(
|
|
195
|
+
await api2("GET", "/messages/batches", void 0, {
|
|
196
|
+
limit: limit?.toString(),
|
|
197
|
+
offset: offset?.toString()
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return err(e);
|
|
202
|
+
}
|
|
269
203
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
204
|
+
);
|
|
205
|
+
server2.tool(
|
|
206
|
+
"list_conversations",
|
|
207
|
+
"List SMS conversation threads ordered by most recent activity. Each conversation groups all messages with a specific phone number.",
|
|
208
|
+
{
|
|
209
|
+
limit: z.number().optional().describe("Conversations to return (1-100, default 50)"),
|
|
210
|
+
offset: z.number().optional().describe("Pagination offset"),
|
|
211
|
+
status: z.enum(["active", "closed"]).optional().describe("Filter by conversation status")
|
|
212
|
+
},
|
|
213
|
+
async ({ limit, offset, status }) => {
|
|
214
|
+
try {
|
|
215
|
+
return ok(
|
|
216
|
+
await api2("GET", "/conversations", void 0, {
|
|
217
|
+
limit: limit?.toString(),
|
|
218
|
+
offset: offset?.toString(),
|
|
219
|
+
status
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return err(e);
|
|
224
|
+
}
|
|
289
225
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
);
|
|
309
|
-
} catch (e) {
|
|
310
|
-
return err(e);
|
|
226
|
+
);
|
|
227
|
+
server2.tool(
|
|
228
|
+
"get_conversation_context",
|
|
229
|
+
"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.",
|
|
230
|
+
{
|
|
231
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
232
|
+
maxMessages: z.number().optional().describe("Max messages to include (default 20, max 50)")
|
|
233
|
+
},
|
|
234
|
+
async ({ conversationId, maxMessages }) => {
|
|
235
|
+
try {
|
|
236
|
+
return ok(
|
|
237
|
+
await api2("GET", `/conversations/${conversationId}/context`, void 0, {
|
|
238
|
+
max_messages: maxMessages?.toString()
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
return err(e);
|
|
243
|
+
}
|
|
311
244
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
245
|
+
);
|
|
246
|
+
server2.tool(
|
|
247
|
+
"get_conversation",
|
|
248
|
+
"Get a conversation thread by ID. Set includeMessages=true to load the message history.",
|
|
249
|
+
{
|
|
250
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
251
|
+
includeMessages: z.boolean().optional().describe("Include message history (default false)"),
|
|
252
|
+
messageLimit: z.number().optional().describe("Number of messages to include (default 50)")
|
|
253
|
+
},
|
|
254
|
+
async ({ conversationId, includeMessages, messageLimit }) => {
|
|
255
|
+
try {
|
|
256
|
+
return ok(
|
|
257
|
+
await api2("GET", `/conversations/${conversationId}`, void 0, {
|
|
258
|
+
include_messages: includeMessages ? "true" : void 0,
|
|
259
|
+
message_limit: messageLimit?.toString()
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
return err(e);
|
|
264
|
+
}
|
|
330
265
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
await
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
);
|
|
349
|
-
} catch (e) {
|
|
350
|
-
return err(e);
|
|
266
|
+
);
|
|
267
|
+
server2.tool(
|
|
268
|
+
"reply_to_conversation",
|
|
269
|
+
"Send a reply within an existing conversation. The recipient is automatically set from the conversation's phone number.",
|
|
270
|
+
{
|
|
271
|
+
conversationId: z.string().describe("The conversation ID to reply in"),
|
|
272
|
+
text: z.string().describe("Reply message text"),
|
|
273
|
+
mediaUrls: z.array(z.string()).optional().describe("Media URLs for MMS")
|
|
274
|
+
},
|
|
275
|
+
async ({ conversationId, text, mediaUrls }) => {
|
|
276
|
+
try {
|
|
277
|
+
const body = { text };
|
|
278
|
+
if (mediaUrls?.length) body.mediaUrls = mediaUrls;
|
|
279
|
+
return ok(await api2("POST", `/conversations/${conversationId}/messages`, body));
|
|
280
|
+
} catch (e) {
|
|
281
|
+
return err(e);
|
|
282
|
+
}
|
|
351
283
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
284
|
+
);
|
|
285
|
+
server2.tool(
|
|
286
|
+
"update_conversation",
|
|
287
|
+
"Update a conversation's metadata or tags. Use metadata for custom key-value data, tags for categorization.",
|
|
288
|
+
{
|
|
289
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
290
|
+
metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata"),
|
|
291
|
+
tags: z.array(z.string()).optional().describe("Tags for categorization (replaces existing tags)")
|
|
292
|
+
},
|
|
293
|
+
async ({ conversationId, metadata, tags }) => {
|
|
294
|
+
try {
|
|
295
|
+
const body = {};
|
|
296
|
+
if (metadata) body.metadata = metadata;
|
|
297
|
+
if (tags) body.tags = tags;
|
|
298
|
+
return ok(await api2("PATCH", `/conversations/${conversationId}`, body));
|
|
299
|
+
} catch (e) {
|
|
300
|
+
return err(e);
|
|
301
|
+
}
|
|
369
302
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
try {
|
|
382
|
-
const body = {};
|
|
383
|
-
if (metadata) body.metadata = metadata;
|
|
384
|
-
if (tags) body.tags = tags;
|
|
385
|
-
return ok(await api("PATCH", `/conversations/${conversationId}`, body));
|
|
386
|
-
} catch (e) {
|
|
387
|
-
return err(e);
|
|
303
|
+
);
|
|
304
|
+
server2.tool(
|
|
305
|
+
"close_conversation",
|
|
306
|
+
"Close a conversation. Closed conversations auto-reopen when a new inbound message arrives.",
|
|
307
|
+
{ conversationId: z.string().describe("The conversation ID to close") },
|
|
308
|
+
async ({ conversationId }) => {
|
|
309
|
+
try {
|
|
310
|
+
return ok(await api2("POST", `/conversations/${conversationId}/close`));
|
|
311
|
+
} catch (e) {
|
|
312
|
+
return err(e);
|
|
313
|
+
}
|
|
388
314
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
315
|
+
);
|
|
316
|
+
server2.tool(
|
|
317
|
+
"reopen_conversation",
|
|
318
|
+
"Reopen a previously closed conversation, setting its status back to active.",
|
|
319
|
+
{ conversationId: z.string().describe("The conversation ID to reopen") },
|
|
320
|
+
async ({ conversationId }) => {
|
|
321
|
+
try {
|
|
322
|
+
return ok(await api2("POST", `/conversations/${conversationId}/reopen`));
|
|
323
|
+
} catch (e) {
|
|
324
|
+
return err(e);
|
|
325
|
+
}
|
|
400
326
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
327
|
+
);
|
|
328
|
+
server2.tool(
|
|
329
|
+
"mark_conversation_read",
|
|
330
|
+
"Mark a conversation as read, resetting the unread count to zero.",
|
|
331
|
+
{ conversationId: z.string().describe("The conversation ID") },
|
|
332
|
+
async ({ conversationId }) => {
|
|
333
|
+
try {
|
|
334
|
+
return ok(await api2("POST", `/conversations/${conversationId}/mark-read`));
|
|
335
|
+
} catch (e) {
|
|
336
|
+
return err(e);
|
|
337
|
+
}
|
|
412
338
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
339
|
+
);
|
|
340
|
+
server2.tool(
|
|
341
|
+
"get_suggested_replies",
|
|
342
|
+
"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).",
|
|
343
|
+
{ conversationId: z.string().describe("The conversation ID to generate suggestions for") },
|
|
344
|
+
async ({ conversationId }) => {
|
|
345
|
+
try {
|
|
346
|
+
return ok(await api2("POST", `/conversations/${conversationId}/suggest-replies`));
|
|
347
|
+
} catch (e) {
|
|
348
|
+
return err(e);
|
|
349
|
+
}
|
|
424
350
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
|
|
351
|
+
);
|
|
352
|
+
server2.tool(
|
|
353
|
+
"create_contact",
|
|
354
|
+
"Create a contact with phone number and optional name, email, metadata. Contacts can be added to lists for campaigns.",
|
|
355
|
+
{
|
|
356
|
+
phoneNumber: z.string().describe("Phone number in E.164 format (+14155551234)"),
|
|
357
|
+
name: z.string().optional().describe("Contact name"),
|
|
358
|
+
email: z.string().optional().describe("Contact email address"),
|
|
359
|
+
metadata: z.record(z.string(), z.any()).optional().describe("Custom key-value metadata")
|
|
360
|
+
},
|
|
361
|
+
async ({ phoneNumber, name, email, metadata }) => {
|
|
362
|
+
try {
|
|
363
|
+
const body = { phone_number: phoneNumber };
|
|
364
|
+
if (name) body.name = name;
|
|
365
|
+
if (email) body.email = email;
|
|
366
|
+
if (metadata) body.metadata = metadata;
|
|
367
|
+
return ok(await api2("POST", "/contacts", body));
|
|
368
|
+
} catch (e) {
|
|
369
|
+
return err(e);
|
|
370
|
+
}
|
|
436
371
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
372
|
+
);
|
|
373
|
+
server2.tool(
|
|
374
|
+
"list_contacts",
|
|
375
|
+
"List contacts with optional search and pagination. Search matches name, email, and phone number.",
|
|
376
|
+
{
|
|
377
|
+
limit: z.number().optional().describe("Contacts to return (1-100, default 50)"),
|
|
378
|
+
offset: z.number().optional().describe("Pagination offset"),
|
|
379
|
+
search: z.string().optional().describe("Search by name, email, or phone number"),
|
|
380
|
+
listId: z.string().optional().describe("Filter by contact list ID")
|
|
381
|
+
},
|
|
382
|
+
async ({ limit, offset, search, listId }) => {
|
|
383
|
+
try {
|
|
384
|
+
return ok(
|
|
385
|
+
await api2("GET", "/contacts", void 0, {
|
|
386
|
+
limit: limit?.toString(),
|
|
387
|
+
offset: offset?.toString(),
|
|
388
|
+
search,
|
|
389
|
+
list_id: listId
|
|
390
|
+
})
|
|
391
|
+
);
|
|
392
|
+
} catch (e) {
|
|
393
|
+
return err(e);
|
|
394
|
+
}
|
|
457
395
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
async ({ limit, offset, search, listId }) => {
|
|
470
|
-
try {
|
|
471
|
-
return ok(
|
|
472
|
-
await api("GET", "/contacts", void 0, {
|
|
473
|
-
limit: limit?.toString(),
|
|
474
|
-
offset: offset?.toString(),
|
|
475
|
-
search,
|
|
476
|
-
list_id: listId
|
|
477
|
-
})
|
|
478
|
-
);
|
|
479
|
-
} catch (e) {
|
|
480
|
-
return err(e);
|
|
396
|
+
);
|
|
397
|
+
server2.tool(
|
|
398
|
+
"get_contact",
|
|
399
|
+
"Get a contact by ID including their list memberships and metadata.",
|
|
400
|
+
{ contactId: z.string().describe("The contact ID") },
|
|
401
|
+
async ({ contactId }) => {
|
|
402
|
+
try {
|
|
403
|
+
return ok(await api2("GET", `/contacts/${contactId}`));
|
|
404
|
+
} catch (e) {
|
|
405
|
+
return err(e);
|
|
406
|
+
}
|
|
481
407
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
|
|
408
|
+
);
|
|
409
|
+
server2.tool(
|
|
410
|
+
"update_contact",
|
|
411
|
+
"Update a contact's name, email, or metadata. Only provided fields are changed.",
|
|
412
|
+
{
|
|
413
|
+
contactId: z.string().describe("The contact ID"),
|
|
414
|
+
name: z.string().optional().describe("Updated name"),
|
|
415
|
+
email: z.string().optional().describe("Updated email"),
|
|
416
|
+
metadata: z.record(z.string(), z.any()).optional().describe("Updated metadata (replaces existing)")
|
|
417
|
+
},
|
|
418
|
+
async ({ contactId, name, email, metadata }) => {
|
|
419
|
+
try {
|
|
420
|
+
const body = {};
|
|
421
|
+
if (name !== void 0) body.name = name;
|
|
422
|
+
if (email !== void 0) body.email = email;
|
|
423
|
+
if (metadata) body.metadata = metadata;
|
|
424
|
+
return ok(await api2("PATCH", `/contacts/${contactId}`, body));
|
|
425
|
+
} catch (e) {
|
|
426
|
+
return err(e);
|
|
427
|
+
}
|
|
493
428
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
async ({ contactId, name, email, metadata }) => {
|
|
506
|
-
try {
|
|
507
|
-
const body = {};
|
|
508
|
-
if (name !== void 0) body.name = name;
|
|
509
|
-
if (email !== void 0) body.email = email;
|
|
510
|
-
if (metadata) body.metadata = metadata;
|
|
511
|
-
return ok(await api("PATCH", `/contacts/${contactId}`, body));
|
|
512
|
-
} catch (e) {
|
|
513
|
-
return err(e);
|
|
429
|
+
);
|
|
430
|
+
server2.tool(
|
|
431
|
+
"delete_contact",
|
|
432
|
+
"Delete a contact by ID. Removes the contact from all lists. Does not delete messages sent to this contact.",
|
|
433
|
+
{ contactId: z.string().describe("The contact ID to delete") },
|
|
434
|
+
async ({ contactId }) => {
|
|
435
|
+
try {
|
|
436
|
+
return ok(await api2("DELETE", `/contacts/${contactId}`));
|
|
437
|
+
} catch (e) {
|
|
438
|
+
return err(e);
|
|
439
|
+
}
|
|
514
440
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
441
|
+
);
|
|
442
|
+
server2.tool(
|
|
443
|
+
"mark_contact_valid",
|
|
444
|
+
"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.",
|
|
445
|
+
{
|
|
446
|
+
contactId: z.string().describe("The contact ID to clear the invalid flag on")
|
|
447
|
+
},
|
|
448
|
+
async ({ contactId }) => {
|
|
449
|
+
try {
|
|
450
|
+
return ok(await api2("POST", `/contacts/${contactId}/mark-valid`));
|
|
451
|
+
} catch (e) {
|
|
452
|
+
return err(e);
|
|
453
|
+
}
|
|
526
454
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
return err(e);
|
|
455
|
+
);
|
|
456
|
+
server2.tool(
|
|
457
|
+
"check_contact_numbers",
|
|
458
|
+
"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.",
|
|
459
|
+
{
|
|
460
|
+
listId: z.string().optional().describe("Scope the lookup to a single contact list"),
|
|
461
|
+
force: z.boolean().optional().describe("Re-check contacts even if already looked up (default: false)")
|
|
462
|
+
},
|
|
463
|
+
async ({ listId, force }) => {
|
|
464
|
+
try {
|
|
465
|
+
return ok(
|
|
466
|
+
await api2("POST", "/contacts/lookup", {
|
|
467
|
+
listId: listId ?? null,
|
|
468
|
+
force: force ?? false
|
|
469
|
+
})
|
|
470
|
+
);
|
|
471
|
+
} catch (e) {
|
|
472
|
+
return err(e);
|
|
473
|
+
}
|
|
547
474
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
475
|
+
);
|
|
476
|
+
server2.tool(
|
|
477
|
+
"bulk_mark_contacts_valid",
|
|
478
|
+
"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.",
|
|
479
|
+
{
|
|
480
|
+
ids: z.array(z.string()).max(1e4).optional().describe("Explicit contact ids to clear (max 10,000)"),
|
|
481
|
+
listId: z.string().optional().describe("Clear every flagged member of this list")
|
|
482
|
+
},
|
|
483
|
+
async ({ ids, listId }) => {
|
|
484
|
+
if (!ids && !listId) {
|
|
485
|
+
return err("bulk_mark_contacts_valid requires either 'ids' or 'listId'");
|
|
486
|
+
}
|
|
487
|
+
if (ids && listId) {
|
|
488
|
+
return err("bulk_mark_contacts_valid accepts 'ids' OR 'listId', not both");
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
return ok(
|
|
492
|
+
await api2(
|
|
493
|
+
"POST",
|
|
494
|
+
"/contacts/bulk-mark-valid",
|
|
495
|
+
ids ? { ids } : { listId }
|
|
496
|
+
)
|
|
497
|
+
);
|
|
498
|
+
} catch (e) {
|
|
499
|
+
return err(e);
|
|
500
|
+
}
|
|
564
501
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
502
|
+
);
|
|
503
|
+
server2.tool(
|
|
504
|
+
"import_contacts",
|
|
505
|
+
"Bulk import contacts from an array. Optionally add all imported contacts to a list. Returns created/updated/skipped counts.",
|
|
506
|
+
{
|
|
507
|
+
contacts: z.array(z.object({
|
|
508
|
+
phone: z.string().describe("Phone in E.164 format"),
|
|
509
|
+
name: z.string().optional().describe("Contact name"),
|
|
510
|
+
email: z.string().optional().describe("Contact email")
|
|
511
|
+
})).describe("Array of contacts to import (max 10000)"),
|
|
512
|
+
listId: z.string().optional().describe("Add all imported contacts to this list")
|
|
513
|
+
},
|
|
514
|
+
async ({ contacts, listId }) => {
|
|
515
|
+
try {
|
|
516
|
+
const body = { contacts };
|
|
517
|
+
if (listId) body.listId = listId;
|
|
518
|
+
return ok(await api2("POST", "/contacts/import", body));
|
|
519
|
+
} catch (e) {
|
|
520
|
+
return err(e);
|
|
521
|
+
}
|
|
576
522
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
})
|
|
594
|
-
);
|
|
595
|
-
} catch (e) {
|
|
596
|
-
return err(e);
|
|
523
|
+
);
|
|
524
|
+
server2.tool(
|
|
525
|
+
"create_contact_list",
|
|
526
|
+
"Create a contact list for organizing contacts and targeting campaigns.",
|
|
527
|
+
{
|
|
528
|
+
name: z.string().describe("List name (e.g., 'VIP Customers', 'Newsletter')"),
|
|
529
|
+
description: z.string().optional().describe("List description")
|
|
530
|
+
},
|
|
531
|
+
async ({ name, description }) => {
|
|
532
|
+
try {
|
|
533
|
+
const body = { name };
|
|
534
|
+
if (description) body.description = description;
|
|
535
|
+
return ok(await api2("POST", "/contact-lists", body));
|
|
536
|
+
} catch (e) {
|
|
537
|
+
return err(e);
|
|
538
|
+
}
|
|
597
539
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
try {
|
|
610
|
-
const body = {};
|
|
611
|
-
if (name) body.name = name;
|
|
612
|
-
if (description !== void 0) body.description = description;
|
|
613
|
-
return ok(await api("PATCH", `/contact-lists/${listId}`, body));
|
|
614
|
-
} catch (e) {
|
|
615
|
-
return err(e);
|
|
540
|
+
);
|
|
541
|
+
server2.tool(
|
|
542
|
+
"list_contact_lists",
|
|
543
|
+
"List all contact lists with their contact counts.",
|
|
544
|
+
{},
|
|
545
|
+
async () => {
|
|
546
|
+
try {
|
|
547
|
+
return ok(await api2("GET", "/contact-lists"));
|
|
548
|
+
} catch (e) {
|
|
549
|
+
return err(e);
|
|
550
|
+
}
|
|
616
551
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
552
|
+
);
|
|
553
|
+
server2.tool(
|
|
554
|
+
"get_contact_list",
|
|
555
|
+
"Get a contact list by ID with its members. Use limit/offset to paginate through members.",
|
|
556
|
+
{
|
|
557
|
+
listId: z.string().describe("The contact list ID"),
|
|
558
|
+
limit: z.number().optional().describe("Max contacts to include (default 50)"),
|
|
559
|
+
offset: z.number().optional().describe("Pagination offset for contacts")
|
|
560
|
+
},
|
|
561
|
+
async ({ listId, limit, offset }) => {
|
|
562
|
+
try {
|
|
563
|
+
return ok(
|
|
564
|
+
await api2("GET", `/contact-lists/${listId}`, void 0, {
|
|
565
|
+
limit: limit?.toString(),
|
|
566
|
+
offset: offset?.toString()
|
|
567
|
+
})
|
|
568
|
+
);
|
|
569
|
+
} catch (e) {
|
|
570
|
+
return err(e);
|
|
571
|
+
}
|
|
628
572
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
573
|
+
);
|
|
574
|
+
server2.tool(
|
|
575
|
+
"update_contact_list",
|
|
576
|
+
"Update a contact list's name or description.",
|
|
577
|
+
{
|
|
578
|
+
listId: z.string().describe("The contact list ID"),
|
|
579
|
+
name: z.string().optional().describe("Updated name"),
|
|
580
|
+
description: z.string().optional().describe("Updated description")
|
|
581
|
+
},
|
|
582
|
+
async ({ listId, name, description }) => {
|
|
583
|
+
try {
|
|
584
|
+
const body = {};
|
|
585
|
+
if (name) body.name = name;
|
|
586
|
+
if (description !== void 0) body.description = description;
|
|
587
|
+
return ok(await api2("PATCH", `/contact-lists/${listId}`, body));
|
|
588
|
+
} catch (e) {
|
|
589
|
+
return err(e);
|
|
590
|
+
}
|
|
643
591
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
return ok(await api("DELETE", `/contact-lists/${listId}/contacts/${contactId}`));
|
|
656
|
-
} catch (e) {
|
|
657
|
-
return err(e);
|
|
592
|
+
);
|
|
593
|
+
server2.tool(
|
|
594
|
+
"delete_contact_list",
|
|
595
|
+
"Delete a contact list. Contacts in the list are not deleted, only the list grouping is removed.",
|
|
596
|
+
{ listId: z.string().describe("The contact list ID to delete") },
|
|
597
|
+
async ({ listId }) => {
|
|
598
|
+
try {
|
|
599
|
+
return ok(await api2("DELETE", `/contact-lists/${listId}`));
|
|
600
|
+
} catch (e) {
|
|
601
|
+
return err(e);
|
|
602
|
+
}
|
|
658
603
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (text) body.messageText = text;
|
|
674
|
-
if (templateId) body.templateId = templateId;
|
|
675
|
-
if (targetListId) body.targetListId = targetListId;
|
|
676
|
-
return ok(await api("POST", "/campaigns", body));
|
|
677
|
-
} catch (e) {
|
|
678
|
-
return err(e);
|
|
604
|
+
);
|
|
605
|
+
server2.tool(
|
|
606
|
+
"add_list_contacts",
|
|
607
|
+
"Add one or more contacts to a contact list.",
|
|
608
|
+
{
|
|
609
|
+
listId: z.string().describe("The contact list ID"),
|
|
610
|
+
contactIds: z.array(z.string()).describe("Array of contact IDs to add to the list")
|
|
611
|
+
},
|
|
612
|
+
async ({ listId, contactIds }) => {
|
|
613
|
+
try {
|
|
614
|
+
return ok(await api2("POST", `/contact-lists/${listId}/contacts`, { contact_ids: contactIds }));
|
|
615
|
+
} catch (e) {
|
|
616
|
+
return err(e);
|
|
617
|
+
}
|
|
679
618
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
limit: limit?.toString(),
|
|
695
|
-
offset: offset?.toString(),
|
|
696
|
-
status
|
|
697
|
-
})
|
|
698
|
-
);
|
|
699
|
-
} catch (e) {
|
|
700
|
-
return err(e);
|
|
619
|
+
);
|
|
620
|
+
server2.tool(
|
|
621
|
+
"remove_list_contact",
|
|
622
|
+
"Remove a single contact from a contact list. The contact itself is not deleted.",
|
|
623
|
+
{
|
|
624
|
+
listId: z.string().describe("The contact list ID"),
|
|
625
|
+
contactId: z.string().describe("The contact ID to remove from the list")
|
|
626
|
+
},
|
|
627
|
+
async ({ listId, contactId }) => {
|
|
628
|
+
try {
|
|
629
|
+
return ok(await api2("DELETE", `/contact-lists/${listId}/contacts/${contactId}`));
|
|
630
|
+
} catch (e) {
|
|
631
|
+
return err(e);
|
|
632
|
+
}
|
|
701
633
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
634
|
+
);
|
|
635
|
+
server2.tool(
|
|
636
|
+
"create_campaign",
|
|
637
|
+
"Create a campaign to send bulk SMS to contact lists. Created as a draft \u2014 preview and send separately. Supports {{variables}} in message text.",
|
|
638
|
+
{
|
|
639
|
+
name: z.string().describe("Campaign name"),
|
|
640
|
+
text: z.string().optional().describe("Message text with optional {{variables}}"),
|
|
641
|
+
templateId: z.string().optional().describe("Template ID to use instead of inline text"),
|
|
642
|
+
targetListId: z.string().optional().describe("Contact list ID to send to")
|
|
643
|
+
},
|
|
644
|
+
async ({ name, text, templateId, targetListId }) => {
|
|
645
|
+
try {
|
|
646
|
+
const body = { name };
|
|
647
|
+
if (text) body.messageText = text;
|
|
648
|
+
if (templateId) body.templateId = templateId;
|
|
649
|
+
if (targetListId) body.targetListId = targetListId;
|
|
650
|
+
return ok(await api2("POST", "/campaigns", body));
|
|
651
|
+
} catch (e) {
|
|
652
|
+
return err(e);
|
|
653
|
+
}
|
|
713
654
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
return err(e);
|
|
655
|
+
);
|
|
656
|
+
server2.tool(
|
|
657
|
+
"list_campaigns",
|
|
658
|
+
"List campaigns with optional filtering by status.",
|
|
659
|
+
{
|
|
660
|
+
limit: z.number().optional().describe("Campaigns to return (1-100, default 50)"),
|
|
661
|
+
offset: z.number().optional().describe("Pagination offset"),
|
|
662
|
+
status: z.enum(["draft", "scheduled", "sending", "completed", "cancelled", "failed"]).optional().describe("Filter by campaign status")
|
|
663
|
+
},
|
|
664
|
+
async ({ limit, offset, status }) => {
|
|
665
|
+
try {
|
|
666
|
+
return ok(
|
|
667
|
+
await api2("GET", "/campaigns", void 0, {
|
|
668
|
+
limit: limit?.toString(),
|
|
669
|
+
offset: offset?.toString(),
|
|
670
|
+
status
|
|
671
|
+
})
|
|
672
|
+
);
|
|
673
|
+
} catch (e) {
|
|
674
|
+
return err(e);
|
|
675
|
+
}
|
|
736
676
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
677
|
+
);
|
|
678
|
+
server2.tool(
|
|
679
|
+
"get_campaign",
|
|
680
|
+
"Get a campaign by ID with delivery statistics (sent, delivered, failed counts).",
|
|
681
|
+
{ campaignId: z.string().describe("The campaign ID") },
|
|
682
|
+
async ({ campaignId }) => {
|
|
683
|
+
try {
|
|
684
|
+
return ok(await api2("GET", `/campaigns/${campaignId}`));
|
|
685
|
+
} catch (e) {
|
|
686
|
+
return err(e);
|
|
687
|
+
}
|
|
748
688
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
689
|
+
);
|
|
690
|
+
server2.tool(
|
|
691
|
+
"update_campaign",
|
|
692
|
+
"Update a campaign's name, text, or target list. Only draft and scheduled campaigns can be updated.",
|
|
693
|
+
{
|
|
694
|
+
campaignId: z.string().describe("The campaign ID"),
|
|
695
|
+
name: z.string().optional().describe("Updated campaign name"),
|
|
696
|
+
text: z.string().optional().describe("Updated message text"),
|
|
697
|
+
templateId: z.string().optional().describe("Updated template ID"),
|
|
698
|
+
targetListId: z.string().optional().describe("Updated target list ID")
|
|
699
|
+
},
|
|
700
|
+
async ({ campaignId, name, text, templateId, targetListId }) => {
|
|
701
|
+
try {
|
|
702
|
+
const body = {};
|
|
703
|
+
if (name) body.name = name;
|
|
704
|
+
if (text) body.messageText = text;
|
|
705
|
+
if (templateId) body.templateId = templateId;
|
|
706
|
+
if (targetListId) body.targetListId = targetListId;
|
|
707
|
+
return ok(await api2("PATCH", `/campaigns/${campaignId}`, body));
|
|
708
|
+
} catch (e) {
|
|
709
|
+
return err(e);
|
|
710
|
+
}
|
|
760
711
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
712
|
+
);
|
|
713
|
+
server2.tool(
|
|
714
|
+
"delete_campaign",
|
|
715
|
+
"Delete a campaign. Only draft and cancelled campaigns can be deleted.",
|
|
716
|
+
{ campaignId: z.string().describe("The campaign ID to delete") },
|
|
717
|
+
async ({ campaignId }) => {
|
|
718
|
+
try {
|
|
719
|
+
return ok(await api2("DELETE", `/campaigns/${campaignId}`));
|
|
720
|
+
} catch (e) {
|
|
721
|
+
return err(e);
|
|
722
|
+
}
|
|
772
723
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
try {
|
|
785
|
-
const body = { scheduledAt };
|
|
786
|
-
if (timezone) body.timezone = timezone;
|
|
787
|
-
return ok(await api("POST", `/campaigns/${campaignId}/schedule`, body));
|
|
788
|
-
} catch (e) {
|
|
789
|
-
return err(e);
|
|
724
|
+
);
|
|
725
|
+
server2.tool(
|
|
726
|
+
"preview_campaign",
|
|
727
|
+
"Preview a campaign before sending. Returns recipient count, estimated credit cost, and whether you have enough credits.",
|
|
728
|
+
{ campaignId: z.string().describe("The campaign ID to preview") },
|
|
729
|
+
async ({ campaignId }) => {
|
|
730
|
+
try {
|
|
731
|
+
return ok(await api2("GET", `/campaigns/${campaignId}/preview`));
|
|
732
|
+
} catch (e) {
|
|
733
|
+
return err(e);
|
|
734
|
+
}
|
|
790
735
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
736
|
+
);
|
|
737
|
+
server2.tool(
|
|
738
|
+
"send_campaign",
|
|
739
|
+
"Send a campaign immediately to all recipients in its target lists. Credits are deducted at send time. Preview first to check costs.",
|
|
740
|
+
{ campaignId: z.string().describe("The campaign ID to send") },
|
|
741
|
+
async ({ campaignId }) => {
|
|
742
|
+
try {
|
|
743
|
+
return ok(await api2("POST", `/campaigns/${campaignId}/send`));
|
|
744
|
+
} catch (e) {
|
|
745
|
+
return err(e);
|
|
746
|
+
}
|
|
802
747
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
748
|
+
);
|
|
749
|
+
server2.tool(
|
|
750
|
+
"schedule_campaign",
|
|
751
|
+
"Schedule a campaign for future delivery at a specific date and time.",
|
|
752
|
+
{
|
|
753
|
+
campaignId: z.string().describe("The campaign ID to schedule"),
|
|
754
|
+
scheduledAt: z.string().describe("ISO 8601 datetime for delivery"),
|
|
755
|
+
timezone: z.string().optional().describe("Timezone (e.g., 'America/New_York')")
|
|
756
|
+
},
|
|
757
|
+
async ({ campaignId, scheduledAt, timezone }) => {
|
|
758
|
+
try {
|
|
759
|
+
const body = { scheduledAt };
|
|
760
|
+
if (timezone) body.timezone = timezone;
|
|
761
|
+
return ok(await api2("POST", `/campaigns/${campaignId}/schedule`, body));
|
|
762
|
+
} catch (e) {
|
|
763
|
+
return err(e);
|
|
764
|
+
}
|
|
814
765
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
return ok(await api("POST", "/templates", { name, text }));
|
|
827
|
-
} catch (e) {
|
|
828
|
-
return err(e);
|
|
766
|
+
);
|
|
767
|
+
server2.tool(
|
|
768
|
+
"cancel_campaign",
|
|
769
|
+
"Cancel a scheduled campaign before it sends. Credits reserved for the campaign are refunded.",
|
|
770
|
+
{ campaignId: z.string().describe("The campaign ID to cancel") },
|
|
771
|
+
async ({ campaignId }) => {
|
|
772
|
+
try {
|
|
773
|
+
return ok(await api2("POST", `/campaigns/${campaignId}/cancel`));
|
|
774
|
+
} catch (e) {
|
|
775
|
+
return err(e);
|
|
776
|
+
}
|
|
829
777
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
return ok(
|
|
842
|
-
await api("GET", "/templates", void 0, {
|
|
843
|
-
limit: limit?.toString(),
|
|
844
|
-
offset: offset?.toString()
|
|
845
|
-
})
|
|
846
|
-
);
|
|
847
|
-
} catch (e) {
|
|
848
|
-
return err(e);
|
|
778
|
+
);
|
|
779
|
+
server2.tool(
|
|
780
|
+
"clone_campaign",
|
|
781
|
+
"Clone a campaign to create a new draft copy with the same settings. Useful for recurring or A/B campaigns.",
|
|
782
|
+
{ campaignId: z.string().describe("The campaign ID to clone") },
|
|
783
|
+
async ({ campaignId }) => {
|
|
784
|
+
try {
|
|
785
|
+
return ok(await api2("POST", `/campaigns/${campaignId}/clone`));
|
|
786
|
+
} catch (e) {
|
|
787
|
+
return err(e);
|
|
788
|
+
}
|
|
849
789
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
790
|
+
);
|
|
791
|
+
server2.tool(
|
|
792
|
+
"create_template",
|
|
793
|
+
"Create an SMS template with {{variable}} placeholders. Templates can be used with campaigns and the Verify API.",
|
|
794
|
+
{
|
|
795
|
+
name: z.string().describe("Template name"),
|
|
796
|
+
text: z.string().describe("Template text with {{variable}} placeholders")
|
|
797
|
+
},
|
|
798
|
+
async ({ name, text }) => {
|
|
799
|
+
try {
|
|
800
|
+
return ok(await api2("POST", "/templates", { name, text }));
|
|
801
|
+
} catch (e) {
|
|
802
|
+
return err(e);
|
|
803
|
+
}
|
|
861
804
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
805
|
+
);
|
|
806
|
+
server2.tool(
|
|
807
|
+
"list_templates",
|
|
808
|
+
"List all templates (custom and preset) with their status and variable definitions.",
|
|
809
|
+
{
|
|
810
|
+
limit: z.number().optional().describe("Templates to return (default 50)"),
|
|
811
|
+
offset: z.number().optional().describe("Pagination offset")
|
|
812
|
+
},
|
|
813
|
+
async ({ limit, offset }) => {
|
|
814
|
+
try {
|
|
815
|
+
return ok(
|
|
816
|
+
await api2("GET", "/templates", void 0, {
|
|
817
|
+
limit: limit?.toString(),
|
|
818
|
+
offset: offset?.toString()
|
|
819
|
+
})
|
|
820
|
+
);
|
|
821
|
+
} catch (e) {
|
|
822
|
+
return err(e);
|
|
823
|
+
}
|
|
880
824
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
825
|
+
);
|
|
826
|
+
server2.tool(
|
|
827
|
+
"get_template",
|
|
828
|
+
"Get a template by ID including its variable definitions and publish status.",
|
|
829
|
+
{ templateId: z.string().describe("The template ID") },
|
|
830
|
+
async ({ templateId }) => {
|
|
831
|
+
try {
|
|
832
|
+
return ok(await api2("GET", `/templates/${templateId}`));
|
|
833
|
+
} catch (e) {
|
|
834
|
+
return err(e);
|
|
835
|
+
}
|
|
892
836
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
837
|
+
);
|
|
838
|
+
server2.tool(
|
|
839
|
+
"update_template",
|
|
840
|
+
"Update a template's name or text.",
|
|
841
|
+
{
|
|
842
|
+
templateId: z.string().describe("The template ID"),
|
|
843
|
+
name: z.string().optional().describe("Updated template name"),
|
|
844
|
+
text: z.string().optional().describe("Updated template text")
|
|
845
|
+
},
|
|
846
|
+
async ({ templateId, name, text }) => {
|
|
847
|
+
try {
|
|
848
|
+
const body = {};
|
|
849
|
+
if (name) body.name = name;
|
|
850
|
+
if (text) body.text = text;
|
|
851
|
+
return ok(await api2("PATCH", `/templates/${templateId}`, body));
|
|
852
|
+
} catch (e) {
|
|
853
|
+
return err(e);
|
|
854
|
+
}
|
|
904
855
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
const body = {};
|
|
917
|
-
if (variables) body.variables = variables;
|
|
918
|
-
return ok(await api("POST", `/templates/${templateId}/preview`, body));
|
|
919
|
-
} catch (e) {
|
|
920
|
-
return err(e);
|
|
856
|
+
);
|
|
857
|
+
server2.tool(
|
|
858
|
+
"delete_template",
|
|
859
|
+
"Delete a custom template. Preset templates cannot be deleted.",
|
|
860
|
+
{ templateId: z.string().describe("The template ID to delete") },
|
|
861
|
+
async ({ templateId }) => {
|
|
862
|
+
try {
|
|
863
|
+
return ok(await api2("DELETE", `/templates/${templateId}`));
|
|
864
|
+
} catch (e) {
|
|
865
|
+
return err(e);
|
|
866
|
+
}
|
|
921
867
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
868
|
+
);
|
|
869
|
+
server2.tool(
|
|
870
|
+
"publish_template",
|
|
871
|
+
"Publish a template, making it available for use with the Verify API and campaigns.",
|
|
872
|
+
{ templateId: z.string().describe("The template ID to publish") },
|
|
873
|
+
async ({ templateId }) => {
|
|
874
|
+
try {
|
|
875
|
+
return ok(await api2("POST", `/templates/${templateId}/publish`));
|
|
876
|
+
} catch (e) {
|
|
877
|
+
return err(e);
|
|
878
|
+
}
|
|
933
879
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
} catch (e) {
|
|
951
|
-
return err(e);
|
|
880
|
+
);
|
|
881
|
+
server2.tool(
|
|
882
|
+
"preview_template",
|
|
883
|
+
"Preview a template with sample variable values to see the interpolated result.",
|
|
884
|
+
{
|
|
885
|
+
templateId: z.string().describe("The template ID to preview"),
|
|
886
|
+
variables: z.record(z.string(), z.string()).optional().describe("Variable values (e.g., { app_name: 'MyApp', code: '123456' })")
|
|
887
|
+
},
|
|
888
|
+
async ({ templateId, variables }) => {
|
|
889
|
+
try {
|
|
890
|
+
const body = {};
|
|
891
|
+
if (variables) body.variables = variables;
|
|
892
|
+
return ok(await api2("POST", `/templates/${templateId}/preview`, body));
|
|
893
|
+
} catch (e) {
|
|
894
|
+
return err(e);
|
|
895
|
+
}
|
|
952
896
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
897
|
+
);
|
|
898
|
+
server2.tool(
|
|
899
|
+
"list_template_presets",
|
|
900
|
+
"List system preset templates (OTP, 2FA, login, etc.) that can be used as-is or cloned.",
|
|
901
|
+
{},
|
|
902
|
+
async () => {
|
|
903
|
+
try {
|
|
904
|
+
return ok(await api2("GET", "/templates/presets"));
|
|
905
|
+
} catch (e) {
|
|
906
|
+
return err(e);
|
|
907
|
+
}
|
|
964
908
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
909
|
+
);
|
|
910
|
+
server2.tool(
|
|
911
|
+
"create_label",
|
|
912
|
+
"Create a label for categorizing conversations and messages. Labels have a name and optional color.",
|
|
913
|
+
{
|
|
914
|
+
name: z.string().describe("Label name (e.g., 'urgent', 'vip', 'follow-up')"),
|
|
915
|
+
color: z.string().optional().describe("Hex color code (default: #6b7280)"),
|
|
916
|
+
description: z.string().optional().describe("Label description")
|
|
917
|
+
},
|
|
918
|
+
async ({ name, color, description }) => {
|
|
919
|
+
try {
|
|
920
|
+
const body = { name };
|
|
921
|
+
if (color) body.color = color;
|
|
922
|
+
if (description) body.description = description;
|
|
923
|
+
return ok(await api2("POST", "/labels", body));
|
|
924
|
+
} catch (e) {
|
|
925
|
+
return err(e);
|
|
926
|
+
}
|
|
979
927
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
return ok(await api("DELETE", `/conversations/${conversationId}/labels/${labelId}`));
|
|
992
|
-
} catch (e) {
|
|
993
|
-
return err(e);
|
|
928
|
+
);
|
|
929
|
+
server2.tool(
|
|
930
|
+
"list_labels",
|
|
931
|
+
"List all labels available in your workspace.",
|
|
932
|
+
{},
|
|
933
|
+
async () => {
|
|
934
|
+
try {
|
|
935
|
+
return ok(await api2("GET", "/labels"));
|
|
936
|
+
} catch (e) {
|
|
937
|
+
return err(e);
|
|
938
|
+
}
|
|
994
939
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
940
|
+
);
|
|
941
|
+
server2.tool(
|
|
942
|
+
"add_conversation_label",
|
|
943
|
+
"Add one or more labels to a conversation for categorization.",
|
|
944
|
+
{
|
|
945
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
946
|
+
labelIds: z.array(z.string()).describe("Array of label IDs to add")
|
|
947
|
+
},
|
|
948
|
+
async ({ conversationId, labelIds }) => {
|
|
949
|
+
try {
|
|
950
|
+
return ok(await api2("POST", `/conversations/${conversationId}/labels`, { labelIds }));
|
|
951
|
+
} catch (e) {
|
|
952
|
+
return err(e);
|
|
953
|
+
}
|
|
1006
954
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}).describe("Actions to take when conditions match"),
|
|
1022
|
-
priority: z.number().optional().describe("Rule priority (lower runs first)")
|
|
1023
|
-
},
|
|
1024
|
-
async ({ name, conditions, actions, priority }) => {
|
|
1025
|
-
try {
|
|
1026
|
-
const body = { name, conditions, actions };
|
|
1027
|
-
if (priority !== void 0) body.priority = priority;
|
|
1028
|
-
return ok(await api("POST", "/rules", body));
|
|
1029
|
-
} catch (e) {
|
|
1030
|
-
return err(e);
|
|
955
|
+
);
|
|
956
|
+
server2.tool(
|
|
957
|
+
"remove_conversation_label",
|
|
958
|
+
"Remove a label from a conversation.",
|
|
959
|
+
{
|
|
960
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
961
|
+
labelId: z.string().describe("The label ID to remove")
|
|
962
|
+
},
|
|
963
|
+
async ({ conversationId, labelId }) => {
|
|
964
|
+
try {
|
|
965
|
+
return ok(await api2("DELETE", `/conversations/${conversationId}/labels/${labelId}`));
|
|
966
|
+
} catch (e) {
|
|
967
|
+
return err(e);
|
|
968
|
+
}
|
|
1031
969
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
}).optional().describe("Updated conditions"),
|
|
1044
|
-
actions: z.object({
|
|
1045
|
-
addLabels: z.array(z.string()),
|
|
1046
|
-
closeConversation: z.boolean().optional()
|
|
1047
|
-
}).optional().describe("Updated actions"),
|
|
1048
|
-
enabled: z.boolean().optional().describe("Enable or disable the rule")
|
|
1049
|
-
},
|
|
1050
|
-
async ({ ruleId, name, conditions, actions, enabled }) => {
|
|
1051
|
-
try {
|
|
1052
|
-
const body = {};
|
|
1053
|
-
if (name) body.name = name;
|
|
1054
|
-
if (conditions) body.conditions = conditions;
|
|
1055
|
-
if (actions) body.actions = actions;
|
|
1056
|
-
if (enabled !== void 0) body.enabled = enabled;
|
|
1057
|
-
return ok(await api("PATCH", `/rules/${ruleId}`, body));
|
|
1058
|
-
} catch (e) {
|
|
1059
|
-
return err(e);
|
|
970
|
+
);
|
|
971
|
+
server2.tool(
|
|
972
|
+
"list_rules",
|
|
973
|
+
"List auto-label rules that automatically tag conversations based on AI-detected intent and sentiment.",
|
|
974
|
+
{},
|
|
975
|
+
async () => {
|
|
976
|
+
try {
|
|
977
|
+
return ok(await api2("GET", "/rules"));
|
|
978
|
+
} catch (e) {
|
|
979
|
+
return err(e);
|
|
980
|
+
}
|
|
1060
981
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
982
|
+
);
|
|
983
|
+
server2.tool(
|
|
984
|
+
"create_rule",
|
|
985
|
+
"Create an auto-label rule. Rules automatically apply labels to conversations when conditions match.",
|
|
986
|
+
{
|
|
987
|
+
name: z.string().describe("Rule name"),
|
|
988
|
+
conditions: z.object({
|
|
989
|
+
intent: z.union([z.string(), z.array(z.string())]).optional().describe("Intent(s) to match"),
|
|
990
|
+
sentiment: z.union([z.string(), z.array(z.string())]).optional().describe("Sentiment(s) to match")
|
|
991
|
+
}).describe("Conditions that trigger the rule"),
|
|
992
|
+
actions: z.object({
|
|
993
|
+
addLabels: z.array(z.string()).describe("Label IDs to add when rule matches"),
|
|
994
|
+
closeConversation: z.boolean().optional().describe("Automatically close the conversation")
|
|
995
|
+
}).describe("Actions to take when conditions match"),
|
|
996
|
+
priority: z.number().optional().describe("Rule priority (lower runs first)")
|
|
997
|
+
},
|
|
998
|
+
async ({ name, conditions, actions, priority }) => {
|
|
999
|
+
try {
|
|
1000
|
+
const body = { name, conditions, actions };
|
|
1001
|
+
if (priority !== void 0) body.priority = priority;
|
|
1002
|
+
return ok(await api2("POST", "/rules", body));
|
|
1003
|
+
} catch (e) {
|
|
1004
|
+
return err(e);
|
|
1005
|
+
}
|
|
1072
1006
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1007
|
+
);
|
|
1008
|
+
server2.tool(
|
|
1009
|
+
"update_rule",
|
|
1010
|
+
"Update an auto-label rule's name, conditions, actions, or enabled status.",
|
|
1011
|
+
{
|
|
1012
|
+
ruleId: z.string().describe("The rule ID"),
|
|
1013
|
+
name: z.string().optional().describe("Updated rule name"),
|
|
1014
|
+
conditions: z.object({
|
|
1015
|
+
intent: z.union([z.string(), z.array(z.string())]).optional(),
|
|
1016
|
+
sentiment: z.union([z.string(), z.array(z.string())]).optional()
|
|
1017
|
+
}).optional().describe("Updated conditions"),
|
|
1018
|
+
actions: z.object({
|
|
1019
|
+
addLabels: z.array(z.string()),
|
|
1020
|
+
closeConversation: z.boolean().optional()
|
|
1021
|
+
}).optional().describe("Updated actions"),
|
|
1022
|
+
enabled: z.boolean().optional().describe("Enable or disable the rule")
|
|
1023
|
+
},
|
|
1024
|
+
async ({ ruleId, name, conditions, actions, enabled }) => {
|
|
1025
|
+
try {
|
|
1026
|
+
const body = {};
|
|
1027
|
+
if (name) body.name = name;
|
|
1028
|
+
if (conditions) body.conditions = conditions;
|
|
1029
|
+
if (actions) body.actions = actions;
|
|
1030
|
+
if (enabled !== void 0) body.enabled = enabled;
|
|
1031
|
+
return ok(await api2("PATCH", `/rules/${ruleId}`, body));
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
return err(e);
|
|
1034
|
+
}
|
|
1090
1035
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
return ok(
|
|
1103
|
-
await api("GET", "/drafts", void 0, {
|
|
1104
|
-
conversation_id: conversationId,
|
|
1105
|
-
status
|
|
1106
|
-
})
|
|
1107
|
-
);
|
|
1108
|
-
} catch (e) {
|
|
1109
|
-
return err(e);
|
|
1036
|
+
);
|
|
1037
|
+
server2.tool(
|
|
1038
|
+
"delete_rule",
|
|
1039
|
+
"Delete an auto-label rule. Existing labels already applied by this rule are not removed.",
|
|
1040
|
+
{ ruleId: z.string().describe("The rule ID to delete") },
|
|
1041
|
+
async ({ ruleId }) => {
|
|
1042
|
+
try {
|
|
1043
|
+
return ok(await api2("DELETE", `/rules/${ruleId}`));
|
|
1044
|
+
} catch (e) {
|
|
1045
|
+
return err(e);
|
|
1046
|
+
}
|
|
1110
1047
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1048
|
+
);
|
|
1049
|
+
server2.tool(
|
|
1050
|
+
"create_draft",
|
|
1051
|
+
"Create a message draft for human review before sending. The draft must be approved before it becomes a real SMS.",
|
|
1052
|
+
{
|
|
1053
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
1054
|
+
text: z.string().describe("Draft message text"),
|
|
1055
|
+
source: z.string().optional().describe("Source of the draft (default: 'ai')")
|
|
1056
|
+
},
|
|
1057
|
+
async ({ conversationId, text, source }) => {
|
|
1058
|
+
try {
|
|
1059
|
+
const body = { conversationId, text };
|
|
1060
|
+
if (source) body.source = source;
|
|
1061
|
+
return ok(await api2("POST", "/drafts", body));
|
|
1062
|
+
} catch (e) {
|
|
1063
|
+
return err(e);
|
|
1064
|
+
}
|
|
1122
1065
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1066
|
+
);
|
|
1067
|
+
server2.tool(
|
|
1068
|
+
"list_drafts",
|
|
1069
|
+
"List message drafts, optionally filtered by conversation or status.",
|
|
1070
|
+
{
|
|
1071
|
+
conversationId: z.string().optional().describe("Filter by conversation ID"),
|
|
1072
|
+
status: z.enum(["pending", "approved", "rejected", "sent", "failed"]).optional().describe("Filter by status")
|
|
1073
|
+
},
|
|
1074
|
+
async ({ conversationId, status }) => {
|
|
1075
|
+
try {
|
|
1076
|
+
return ok(
|
|
1077
|
+
await api2("GET", "/drafts", void 0, {
|
|
1078
|
+
conversation_id: conversationId,
|
|
1079
|
+
status
|
|
1080
|
+
})
|
|
1081
|
+
);
|
|
1082
|
+
} catch (e) {
|
|
1083
|
+
return err(e);
|
|
1084
|
+
}
|
|
1139
1085
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
try {
|
|
1152
|
-
const body = { url, events };
|
|
1153
|
-
if (description) body.description = description;
|
|
1154
|
-
return ok(await api("POST", "/webhooks", body));
|
|
1155
|
-
} catch (e) {
|
|
1156
|
-
return err(e);
|
|
1086
|
+
);
|
|
1087
|
+
server2.tool(
|
|
1088
|
+
"approve_draft",
|
|
1089
|
+
"Approve a pending draft and send it as a real SMS message. Runs compliance checks and deducts credits at approval time.",
|
|
1090
|
+
{ draftId: z.string().describe("The draft ID to approve") },
|
|
1091
|
+
async ({ draftId }) => {
|
|
1092
|
+
try {
|
|
1093
|
+
return ok(await api2("POST", `/drafts/${draftId}/approve`));
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
return err(e);
|
|
1096
|
+
}
|
|
1157
1097
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1098
|
+
);
|
|
1099
|
+
server2.tool(
|
|
1100
|
+
"reject_draft",
|
|
1101
|
+
"Reject a pending draft with an optional reason. The message will not be sent.",
|
|
1102
|
+
{
|
|
1103
|
+
draftId: z.string().describe("The draft ID to reject"),
|
|
1104
|
+
reason: z.string().optional().describe("Reason for rejection")
|
|
1105
|
+
},
|
|
1106
|
+
async ({ draftId, reason }) => {
|
|
1107
|
+
try {
|
|
1108
|
+
const body = {};
|
|
1109
|
+
if (reason) body.reason = reason;
|
|
1110
|
+
return ok(await api2("POST", `/drafts/${draftId}/reject`, body));
|
|
1111
|
+
} catch (e) {
|
|
1112
|
+
return err(e);
|
|
1113
|
+
}
|
|
1169
1114
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1115
|
+
);
|
|
1116
|
+
server2.tool(
|
|
1117
|
+
"create_webhook",
|
|
1118
|
+
"Create a webhook endpoint to receive real-time event notifications. Returns a signing secret (shown only once) for verifying payloads.",
|
|
1119
|
+
{
|
|
1120
|
+
url: z.string().describe("HTTPS endpoint URL to receive events"),
|
|
1121
|
+
events: z.array(z.string()).describe("Event types to subscribe to (e.g., ['message.delivered', 'message.failed'])"),
|
|
1122
|
+
description: z.string().optional().describe("Description of this webhook")
|
|
1123
|
+
},
|
|
1124
|
+
async ({ url, events, description }) => {
|
|
1125
|
+
try {
|
|
1126
|
+
const body = { url, events };
|
|
1127
|
+
if (description) body.description = description;
|
|
1128
|
+
return ok(await api2("POST", "/webhooks", body));
|
|
1129
|
+
} catch (e) {
|
|
1130
|
+
return err(e);
|
|
1131
|
+
}
|
|
1181
1132
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
},
|
|
1194
|
-
async ({ webhookId, url, events, description, isActive }) => {
|
|
1195
|
-
try {
|
|
1196
|
-
const body = {};
|
|
1197
|
-
if (url) body.url = url;
|
|
1198
|
-
if (events) body.events = events;
|
|
1199
|
-
if (description !== void 0) body.description = description;
|
|
1200
|
-
if (isActive !== void 0) body.is_active = isActive;
|
|
1201
|
-
return ok(await api("PATCH", `/webhooks/${webhookId}`, body));
|
|
1202
|
-
} catch (e) {
|
|
1203
|
-
return err(e);
|
|
1133
|
+
);
|
|
1134
|
+
server2.tool(
|
|
1135
|
+
"list_webhooks",
|
|
1136
|
+
"List all webhook endpoints with their status and event subscriptions.",
|
|
1137
|
+
{},
|
|
1138
|
+
async () => {
|
|
1139
|
+
try {
|
|
1140
|
+
return ok(await api2("GET", "/webhooks"));
|
|
1141
|
+
} catch (e) {
|
|
1142
|
+
return err(e);
|
|
1143
|
+
}
|
|
1204
1144
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1145
|
+
);
|
|
1146
|
+
server2.tool(
|
|
1147
|
+
"get_webhook",
|
|
1148
|
+
"Get a webhook by ID with delivery statistics.",
|
|
1149
|
+
{ webhookId: z.string().describe("The webhook ID") },
|
|
1150
|
+
async ({ webhookId }) => {
|
|
1151
|
+
try {
|
|
1152
|
+
return ok(await api2("GET", `/webhooks/${webhookId}`));
|
|
1153
|
+
} catch (e) {
|
|
1154
|
+
return err(e);
|
|
1155
|
+
}
|
|
1216
1156
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1157
|
+
);
|
|
1158
|
+
server2.tool(
|
|
1159
|
+
"update_webhook",
|
|
1160
|
+
"Update a webhook's URL, events, description, or active status.",
|
|
1161
|
+
{
|
|
1162
|
+
webhookId: z.string().describe("The webhook ID"),
|
|
1163
|
+
url: z.string().optional().describe("Updated HTTPS endpoint URL"),
|
|
1164
|
+
events: z.array(z.string()).optional().describe("Updated event types"),
|
|
1165
|
+
description: z.string().optional().describe("Updated description"),
|
|
1166
|
+
isActive: z.boolean().optional().describe("Enable or disable the webhook")
|
|
1167
|
+
},
|
|
1168
|
+
async ({ webhookId, url, events, description, isActive }) => {
|
|
1169
|
+
try {
|
|
1170
|
+
const body = {};
|
|
1171
|
+
if (url) body.url = url;
|
|
1172
|
+
if (events) body.events = events;
|
|
1173
|
+
if (description !== void 0) body.description = description;
|
|
1174
|
+
if (isActive !== void 0) body.is_active = isActive;
|
|
1175
|
+
return ok(await api2("PATCH", `/webhooks/${webhookId}`, body));
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
return err(e);
|
|
1178
|
+
}
|
|
1228
1179
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1180
|
+
);
|
|
1181
|
+
server2.tool(
|
|
1182
|
+
"delete_webhook",
|
|
1183
|
+
"Delete a webhook endpoint. Stops all future event deliveries to this URL.",
|
|
1184
|
+
{ webhookId: z.string().describe("The webhook ID to delete") },
|
|
1185
|
+
async ({ webhookId }) => {
|
|
1186
|
+
try {
|
|
1187
|
+
return ok(await api2("DELETE", `/webhooks/${webhookId}`));
|
|
1188
|
+
} catch (e) {
|
|
1189
|
+
return err(e);
|
|
1190
|
+
}
|
|
1240
1191
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1192
|
+
);
|
|
1193
|
+
server2.tool(
|
|
1194
|
+
"test_webhook",
|
|
1195
|
+
"Send a test event to a webhook endpoint to verify it is reachable. Returns response status and latency.",
|
|
1196
|
+
{ webhookId: z.string().describe("The webhook ID to test") },
|
|
1197
|
+
async ({ webhookId }) => {
|
|
1198
|
+
try {
|
|
1199
|
+
return ok(await api2("POST", `/webhooks/${webhookId}/test`));
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
return err(e);
|
|
1202
|
+
}
|
|
1252
1203
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1204
|
+
);
|
|
1205
|
+
server2.tool(
|
|
1206
|
+
"list_webhook_deliveries",
|
|
1207
|
+
"Get delivery history for a webhook showing recent attempts, statuses, and response times.",
|
|
1208
|
+
{ webhookId: z.string().describe("The webhook ID") },
|
|
1209
|
+
async ({ webhookId }) => {
|
|
1210
|
+
try {
|
|
1211
|
+
return ok(await api2("GET", `/webhooks/${webhookId}/deliveries`));
|
|
1212
|
+
} catch (e) {
|
|
1213
|
+
return err(e);
|
|
1214
|
+
}
|
|
1264
1215
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
},
|
|
1277
|
-
async ({ to, appName, codeLength, timeoutSecs, templateId }) => {
|
|
1278
|
-
try {
|
|
1279
|
-
const body = { to };
|
|
1280
|
-
if (appName) body.appName = appName;
|
|
1281
|
-
if (codeLength) body.codeLength = codeLength;
|
|
1282
|
-
if (timeoutSecs) body.timeoutSecs = timeoutSecs;
|
|
1283
|
-
if (templateId) body.templateId = templateId;
|
|
1284
|
-
return ok(await api("POST", "/verify", body));
|
|
1285
|
-
} catch (e) {
|
|
1286
|
-
return err(e);
|
|
1216
|
+
);
|
|
1217
|
+
server2.tool(
|
|
1218
|
+
"rotate_webhook_secret",
|
|
1219
|
+
"Rotate a webhook's signing secret. The old secret stops working immediately.",
|
|
1220
|
+
{ webhookId: z.string().describe("The webhook ID") },
|
|
1221
|
+
async ({ webhookId }) => {
|
|
1222
|
+
try {
|
|
1223
|
+
return ok(await api2("POST", `/webhooks/${webhookId}/rotate-secret`));
|
|
1224
|
+
} catch (e) {
|
|
1225
|
+
return err(e);
|
|
1226
|
+
}
|
|
1287
1227
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
return ok(await api("POST", `/verify/${verificationId}/check`, { code }));
|
|
1300
|
-
} catch (e) {
|
|
1301
|
-
return err(e);
|
|
1228
|
+
);
|
|
1229
|
+
server2.tool(
|
|
1230
|
+
"list_webhook_event_types",
|
|
1231
|
+
"List all available webhook event types that can be subscribed to.",
|
|
1232
|
+
{},
|
|
1233
|
+
async () => {
|
|
1234
|
+
try {
|
|
1235
|
+
return ok(await api2("GET", "/webhooks/event-types"));
|
|
1236
|
+
} catch (e) {
|
|
1237
|
+
return err(e);
|
|
1238
|
+
}
|
|
1302
1239
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1240
|
+
);
|
|
1241
|
+
server2.tool(
|
|
1242
|
+
"send_otp",
|
|
1243
|
+
"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.",
|
|
1244
|
+
{
|
|
1245
|
+
to: z.string().describe("Phone number to verify in E.164 format"),
|
|
1246
|
+
appName: z.string().optional().describe("Your app/brand name shown in the SMS"),
|
|
1247
|
+
codeLength: z.number().optional().describe("Digits in the code (default 6)"),
|
|
1248
|
+
timeoutSecs: z.number().optional().describe("Code validity in seconds (default 300)"),
|
|
1249
|
+
templateId: z.string().optional().describe("Custom OTP template ID")
|
|
1250
|
+
},
|
|
1251
|
+
async ({ to, appName, codeLength, timeoutSecs, templateId }) => {
|
|
1252
|
+
try {
|
|
1253
|
+
const body = { to };
|
|
1254
|
+
if (appName) body.app_name = appName;
|
|
1255
|
+
if (codeLength) body.code_length = codeLength;
|
|
1256
|
+
if (timeoutSecs) body.timeout_secs = timeoutSecs;
|
|
1257
|
+
if (templateId) body.template_id = templateId;
|
|
1258
|
+
return ok(await api2("POST", "/verify", body));
|
|
1259
|
+
} catch (e) {
|
|
1260
|
+
return err(e);
|
|
1261
|
+
}
|
|
1314
1262
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1263
|
+
);
|
|
1264
|
+
server2.tool(
|
|
1265
|
+
"check_otp",
|
|
1266
|
+
"Verify an OTP code. Returns 'verified' (correct), 'invalid_code' (wrong), 'expired', or 'max_attempts_exceeded'.",
|
|
1267
|
+
{
|
|
1268
|
+
verificationId: z.string().describe("The verification ID from send_otp"),
|
|
1269
|
+
code: z.string().describe("The code entered by the user")
|
|
1270
|
+
},
|
|
1271
|
+
async ({ verificationId, code }) => {
|
|
1272
|
+
try {
|
|
1273
|
+
return ok(await api2("POST", `/verify/${verificationId}/check`, { code }));
|
|
1274
|
+
} catch (e) {
|
|
1275
|
+
return err(e);
|
|
1276
|
+
}
|
|
1326
1277
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
return ok(
|
|
1339
|
-
await api("GET", "/verify", void 0, {
|
|
1340
|
-
limit: limit?.toString(),
|
|
1341
|
-
status
|
|
1342
|
-
})
|
|
1343
|
-
);
|
|
1344
|
-
} catch (e) {
|
|
1345
|
-
return err(e);
|
|
1278
|
+
);
|
|
1279
|
+
server2.tool(
|
|
1280
|
+
"get_verification_status",
|
|
1281
|
+
"Check the current status of an OTP verification (pending, verified, expired, failed).",
|
|
1282
|
+
{ verificationId: z.string().describe("The verification ID") },
|
|
1283
|
+
async ({ verificationId }) => {
|
|
1284
|
+
try {
|
|
1285
|
+
return ok(await api2("GET", `/verify/${verificationId}`));
|
|
1286
|
+
} catch (e) {
|
|
1287
|
+
return err(e);
|
|
1288
|
+
}
|
|
1346
1289
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
},
|
|
1359
|
-
async ({ successUrl, cancelUrl, brandName, brandColor, metadata }) => {
|
|
1360
|
-
try {
|
|
1361
|
-
const body = { successUrl };
|
|
1362
|
-
if (cancelUrl) body.cancelUrl = cancelUrl;
|
|
1363
|
-
if (brandName) body.brandName = brandName;
|
|
1364
|
-
if (brandColor) body.brandColor = brandColor;
|
|
1365
|
-
if (metadata) body.metadata = metadata;
|
|
1366
|
-
return ok(await api("POST", "/verify/sessions", body));
|
|
1367
|
-
} catch (e) {
|
|
1368
|
-
return err(e);
|
|
1290
|
+
);
|
|
1291
|
+
server2.tool(
|
|
1292
|
+
"resend_otp",
|
|
1293
|
+
"Resend an OTP verification code. Use when the original SMS was not received.",
|
|
1294
|
+
{ verificationId: z.string().describe("The verification ID from send_otp") },
|
|
1295
|
+
async ({ verificationId }) => {
|
|
1296
|
+
try {
|
|
1297
|
+
return ok(await api2("POST", `/verify/${verificationId}/resend`));
|
|
1298
|
+
} catch (e) {
|
|
1299
|
+
return err(e);
|
|
1300
|
+
}
|
|
1369
1301
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1302
|
+
);
|
|
1303
|
+
server2.tool(
|
|
1304
|
+
"list_verifications",
|
|
1305
|
+
"List recent OTP verifications with pagination.",
|
|
1306
|
+
{
|
|
1307
|
+
limit: z.number().optional().describe("Verifications to return (default 50)")
|
|
1308
|
+
},
|
|
1309
|
+
async ({ limit }) => {
|
|
1310
|
+
try {
|
|
1311
|
+
return ok(
|
|
1312
|
+
await api2("GET", "/verify", void 0, {
|
|
1313
|
+
limit: limit?.toString()
|
|
1314
|
+
})
|
|
1315
|
+
);
|
|
1316
|
+
} catch (e) {
|
|
1317
|
+
return err(e);
|
|
1318
|
+
}
|
|
1381
1319
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1320
|
+
);
|
|
1321
|
+
server2.tool(
|
|
1322
|
+
"create_verify_session",
|
|
1323
|
+
"Create a hosted verify session with a branded UI. Returns a URL to redirect the user to for phone verification. Zero frontend code needed.",
|
|
1324
|
+
{
|
|
1325
|
+
successUrl: z.string().describe("URL to redirect to after successful verification"),
|
|
1326
|
+
cancelUrl: z.string().optional().describe("URL to redirect to if user cancels"),
|
|
1327
|
+
brandName: z.string().optional().describe("Your brand name shown on the verify page"),
|
|
1328
|
+
brandColor: z.string().optional().describe("Brand color hex code (e.g., #4F46E5)"),
|
|
1329
|
+
metadata: z.record(z.string(), z.any()).optional().describe("Custom metadata to attach to the session")
|
|
1330
|
+
},
|
|
1331
|
+
async ({ successUrl, cancelUrl, brandName, brandColor, metadata }) => {
|
|
1332
|
+
try {
|
|
1333
|
+
const body = { success_url: successUrl };
|
|
1334
|
+
if (cancelUrl) body.cancel_url = cancelUrl;
|
|
1335
|
+
if (brandName) body.brand_name = brandName;
|
|
1336
|
+
if (brandColor) body.brand_color = brandColor;
|
|
1337
|
+
if (metadata) body.metadata = metadata;
|
|
1338
|
+
return ok(await api2("POST", "/verify/sessions", body));
|
|
1339
|
+
} catch (e) {
|
|
1340
|
+
return err(e);
|
|
1341
|
+
}
|
|
1393
1342
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
return ok(
|
|
1406
|
-
await api("GET", "/credits/transactions", void 0, {
|
|
1407
|
-
limit: limit?.toString(),
|
|
1408
|
-
offset: offset?.toString()
|
|
1409
|
-
})
|
|
1410
|
-
);
|
|
1411
|
-
} catch (e) {
|
|
1412
|
-
return err(e);
|
|
1343
|
+
);
|
|
1344
|
+
server2.tool(
|
|
1345
|
+
"validate_verify_session",
|
|
1346
|
+
"Validate a session token returned after a user completes hosted verification. Returns the verified phone number.",
|
|
1347
|
+
{ token: z.string().describe("The session token from the success redirect URL") },
|
|
1348
|
+
async ({ token }) => {
|
|
1349
|
+
try {
|
|
1350
|
+
return ok(await api2("POST", "/verify/sessions/validate", { token }));
|
|
1351
|
+
} catch (e) {
|
|
1352
|
+
return err(e);
|
|
1353
|
+
}
|
|
1413
1354
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1355
|
+
);
|
|
1356
|
+
server2.tool(
|
|
1357
|
+
"get_credits",
|
|
1358
|
+
"Get current credit balance including reserved credits for scheduled messages.",
|
|
1359
|
+
{},
|
|
1360
|
+
async () => {
|
|
1361
|
+
try {
|
|
1362
|
+
return ok(await api2("GET", "/credits"));
|
|
1363
|
+
} catch (e) {
|
|
1364
|
+
return err(e);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
);
|
|
1368
|
+
server2.tool(
|
|
1369
|
+
"list_credit_transactions",
|
|
1370
|
+
"List credit transaction history showing purchases, usage, refunds, and transfers.",
|
|
1371
|
+
{
|
|
1372
|
+
limit: z.number().optional().describe("Transactions to return (default 50)")
|
|
1373
|
+
},
|
|
1374
|
+
async ({ limit }) => {
|
|
1375
|
+
try {
|
|
1376
|
+
return ok(
|
|
1377
|
+
await api2("GET", "/credits/transactions", void 0, {
|
|
1378
|
+
limit: limit?.toString()
|
|
1379
|
+
})
|
|
1380
|
+
);
|
|
1381
|
+
} catch (e) {
|
|
1382
|
+
return err(e);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
);
|
|
1386
|
+
server2.tool(
|
|
1387
|
+
"get_account",
|
|
1388
|
+
"Get account info: credit balance, phone number verification status, rate limits, and API key details.",
|
|
1389
|
+
{},
|
|
1390
|
+
async () => {
|
|
1391
|
+
try {
|
|
1392
|
+
return ok(await api2("GET", "/account"));
|
|
1393
|
+
} catch (e) {
|
|
1394
|
+
return err(e);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
);
|
|
1398
|
+
server2.tool(
|
|
1399
|
+
"generate_business_page",
|
|
1400
|
+
"Generate a hosted business landing page for verification. Returns a URL at sendly.live/biz/{slug}. Enterprise accounts only.",
|
|
1401
|
+
{
|
|
1402
|
+
businessName: z.string().describe("Business name"),
|
|
1403
|
+
useCase: z.string().optional().describe("Use case (e.g., Insurance Services, Appointment Reminders, 2FA)"),
|
|
1404
|
+
useCaseSummary: z.string().optional().describe("Brief description of what the business does"),
|
|
1405
|
+
contactEmail: z.string().optional().describe("Business contact email"),
|
|
1406
|
+
contactPhone: z.string().optional().describe("Business phone number"),
|
|
1407
|
+
businessAddress: z.string().optional().describe("City, State ZIP (e.g., Chicago, IL 60601)")
|
|
1408
|
+
},
|
|
1409
|
+
async (params) => {
|
|
1410
|
+
try {
|
|
1411
|
+
return ok(await api2("POST", "/enterprise/business-page/generate", params));
|
|
1412
|
+
} catch (e) {
|
|
1413
|
+
return err(e);
|
|
1414
|
+
}
|
|
1425
1415
|
}
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/index.ts
|
|
1420
|
+
var VERSION = "2.0.0";
|
|
1421
|
+
var API_KEY = process.env.SENDLY_API_KEY;
|
|
1422
|
+
var BASE_URL = process.env.SENDLY_BASE_URL || "https://sendly.live";
|
|
1423
|
+
if (!API_KEY) {
|
|
1424
|
+
process.stderr.write(
|
|
1425
|
+
"SENDLY_API_KEY environment variable is required.\nGet your API key at https://sendly.live \u2192 Settings \u2192 API Keys\n"
|
|
1426
|
+
);
|
|
1427
|
+
process.exit(1);
|
|
1428
|
+
}
|
|
1429
|
+
if (!BASE_URL.startsWith("https://") && !BASE_URL.startsWith("http://localhost") && !BASE_URL.startsWith("http://127.0.0.1")) {
|
|
1430
|
+
process.stderr.write(
|
|
1431
|
+
"SENDLY_BASE_URL must use HTTPS in production.\nHTTP is only allowed for localhost development.\n"
|
|
1432
|
+
);
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
}
|
|
1435
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1436
|
+
var RATE_LIMIT_MAX = 60;
|
|
1437
|
+
var rateLimitTokens = RATE_LIMIT_MAX;
|
|
1438
|
+
var rateLimitResetAt = Date.now() + RATE_LIMIT_WINDOW_MS;
|
|
1439
|
+
function checkRateLimit() {
|
|
1440
|
+
const now = Date.now();
|
|
1441
|
+
if (now >= rateLimitResetAt) {
|
|
1442
|
+
rateLimitTokens = RATE_LIMIT_MAX;
|
|
1443
|
+
rateLimitResetAt = now + RATE_LIMIT_WINDOW_MS;
|
|
1426
1444
|
}
|
|
1427
|
-
);
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
async (params) => {
|
|
1440
|
-
try {
|
|
1441
|
-
return ok(await api("POST", "/enterprise/business-page/generate", params));
|
|
1442
|
-
} catch (e) {
|
|
1443
|
-
return err(e);
|
|
1445
|
+
if (rateLimitTokens <= 0) return false;
|
|
1446
|
+
rateLimitTokens--;
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
async function api(method, path, body, query) {
|
|
1450
|
+
if (!checkRateLimit()) {
|
|
1451
|
+
throw new Error("Rate limited \u2014 too many requests. Wait a moment and try again.");
|
|
1452
|
+
}
|
|
1453
|
+
const url = new URL(`/api/v1${path}`, BASE_URL);
|
|
1454
|
+
if (query) {
|
|
1455
|
+
for (const [k, v] of Object.entries(query)) {
|
|
1456
|
+
if (v !== void 0) url.searchParams.set(k, v);
|
|
1444
1457
|
}
|
|
1445
1458
|
}
|
|
1446
|
-
|
|
1459
|
+
const headers = {
|
|
1460
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
1461
|
+
"User-Agent": "@sendly/mcp/2.0.2"
|
|
1462
|
+
};
|
|
1463
|
+
if (body) headers["Content-Type"] = "application/json";
|
|
1464
|
+
const res = await fetch(url.toString(), {
|
|
1465
|
+
method,
|
|
1466
|
+
headers,
|
|
1467
|
+
body: body ? JSON.stringify(body) : void 0
|
|
1468
|
+
});
|
|
1469
|
+
if (res.status === 204) return { success: true };
|
|
1470
|
+
if (res.status === 429) {
|
|
1471
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
1472
|
+
throw new Error(
|
|
1473
|
+
`Rate limited by API. ${retryAfter ? `Retry after ${retryAfter} seconds.` : "Wait a moment and try again."}`
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
const data = await res.json();
|
|
1477
|
+
if (!res.ok) {
|
|
1478
|
+
const msg = typeof data === "object" && data !== null ? data.error || data.message || JSON.stringify(data) : String(data);
|
|
1479
|
+
throw new Error(String(msg));
|
|
1480
|
+
}
|
|
1481
|
+
return data;
|
|
1482
|
+
}
|
|
1483
|
+
var server = new McpServer({
|
|
1484
|
+
name: "sendly",
|
|
1485
|
+
version: VERSION
|
|
1486
|
+
});
|
|
1487
|
+
registerAllTools(server, api);
|
|
1447
1488
|
var transport = new StdioServerTransport();
|
|
1448
1489
|
await server.connect(transport);
|