@sendly/mcp 2.0.0 → 2.0.1
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 +1322 -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,1410 @@ 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
|
+
"import_contacts",
|
|
444
|
+
"Bulk import contacts from an array. Optionally add all imported contacts to a list. Returns created/updated/skipped counts.",
|
|
445
|
+
{
|
|
446
|
+
contacts: z.array(z.object({
|
|
447
|
+
phone: z.string().describe("Phone in E.164 format"),
|
|
448
|
+
name: z.string().optional().describe("Contact name"),
|
|
449
|
+
email: z.string().optional().describe("Contact email")
|
|
450
|
+
})).describe("Array of contacts to import (max 10000)"),
|
|
451
|
+
listId: z.string().optional().describe("Add all imported contacts to this list")
|
|
452
|
+
},
|
|
453
|
+
async ({ contacts, listId }) => {
|
|
454
|
+
try {
|
|
455
|
+
const body = { contacts };
|
|
456
|
+
if (listId) body.listId = listId;
|
|
457
|
+
return ok(await api2("POST", "/contacts/import", body));
|
|
458
|
+
} catch (e) {
|
|
459
|
+
return err(e);
|
|
460
|
+
}
|
|
526
461
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
if (listId) body.listId = listId;
|
|
544
|
-
return ok(await api("POST", "/contacts/import", body));
|
|
545
|
-
} catch (e) {
|
|
546
|
-
return err(e);
|
|
462
|
+
);
|
|
463
|
+
server2.tool(
|
|
464
|
+
"create_contact_list",
|
|
465
|
+
"Create a contact list for organizing contacts and targeting campaigns.",
|
|
466
|
+
{
|
|
467
|
+
name: z.string().describe("List name (e.g., 'VIP Customers', 'Newsletter')"),
|
|
468
|
+
description: z.string().optional().describe("List description")
|
|
469
|
+
},
|
|
470
|
+
async ({ name, description }) => {
|
|
471
|
+
try {
|
|
472
|
+
const body = { name };
|
|
473
|
+
if (description) body.description = description;
|
|
474
|
+
return ok(await api2("POST", "/contact-lists", body));
|
|
475
|
+
} catch (e) {
|
|
476
|
+
return err(e);
|
|
477
|
+
}
|
|
547
478
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const body = { name };
|
|
560
|
-
if (description) body.description = description;
|
|
561
|
-
return ok(await api("POST", "/contact-lists", body));
|
|
562
|
-
} catch (e) {
|
|
563
|
-
return err(e);
|
|
479
|
+
);
|
|
480
|
+
server2.tool(
|
|
481
|
+
"list_contact_lists",
|
|
482
|
+
"List all contact lists with their contact counts.",
|
|
483
|
+
{},
|
|
484
|
+
async () => {
|
|
485
|
+
try {
|
|
486
|
+
return ok(await api2("GET", "/contact-lists"));
|
|
487
|
+
} catch (e) {
|
|
488
|
+
return err(e);
|
|
489
|
+
}
|
|
564
490
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
491
|
+
);
|
|
492
|
+
server2.tool(
|
|
493
|
+
"get_contact_list",
|
|
494
|
+
"Get a contact list by ID with its members. Use limit/offset to paginate through members.",
|
|
495
|
+
{
|
|
496
|
+
listId: z.string().describe("The contact list ID"),
|
|
497
|
+
limit: z.number().optional().describe("Max contacts to include (default 50)"),
|
|
498
|
+
offset: z.number().optional().describe("Pagination offset for contacts")
|
|
499
|
+
},
|
|
500
|
+
async ({ listId, limit, offset }) => {
|
|
501
|
+
try {
|
|
502
|
+
return ok(
|
|
503
|
+
await api2("GET", `/contact-lists/${listId}`, void 0, {
|
|
504
|
+
limit: limit?.toString(),
|
|
505
|
+
offset: offset?.toString()
|
|
506
|
+
})
|
|
507
|
+
);
|
|
508
|
+
} catch (e) {
|
|
509
|
+
return err(e);
|
|
510
|
+
}
|
|
576
511
|
}
|
|
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);
|
|
512
|
+
);
|
|
513
|
+
server2.tool(
|
|
514
|
+
"update_contact_list",
|
|
515
|
+
"Update a contact list's name or description.",
|
|
516
|
+
{
|
|
517
|
+
listId: z.string().describe("The contact list ID"),
|
|
518
|
+
name: z.string().optional().describe("Updated name"),
|
|
519
|
+
description: z.string().optional().describe("Updated description")
|
|
520
|
+
},
|
|
521
|
+
async ({ listId, name, description }) => {
|
|
522
|
+
try {
|
|
523
|
+
const body = {};
|
|
524
|
+
if (name) body.name = name;
|
|
525
|
+
if (description !== void 0) body.description = description;
|
|
526
|
+
return ok(await api2("PATCH", `/contact-lists/${listId}`, body));
|
|
527
|
+
} catch (e) {
|
|
528
|
+
return err(e);
|
|
529
|
+
}
|
|
597
530
|
}
|
|
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);
|
|
531
|
+
);
|
|
532
|
+
server2.tool(
|
|
533
|
+
"delete_contact_list",
|
|
534
|
+
"Delete a contact list. Contacts in the list are not deleted, only the list grouping is removed.",
|
|
535
|
+
{ listId: z.string().describe("The contact list ID to delete") },
|
|
536
|
+
async ({ listId }) => {
|
|
537
|
+
try {
|
|
538
|
+
return ok(await api2("DELETE", `/contact-lists/${listId}`));
|
|
539
|
+
} catch (e) {
|
|
540
|
+
return err(e);
|
|
541
|
+
}
|
|
616
542
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
543
|
+
);
|
|
544
|
+
server2.tool(
|
|
545
|
+
"add_list_contacts",
|
|
546
|
+
"Add one or more contacts to a contact list.",
|
|
547
|
+
{
|
|
548
|
+
listId: z.string().describe("The contact list ID"),
|
|
549
|
+
contactIds: z.array(z.string()).describe("Array of contact IDs to add to the list")
|
|
550
|
+
},
|
|
551
|
+
async ({ listId, contactIds }) => {
|
|
552
|
+
try {
|
|
553
|
+
return ok(await api2("POST", `/contact-lists/${listId}/contacts`, { contact_ids: contactIds }));
|
|
554
|
+
} catch (e) {
|
|
555
|
+
return err(e);
|
|
556
|
+
}
|
|
628
557
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
558
|
+
);
|
|
559
|
+
server2.tool(
|
|
560
|
+
"remove_list_contact",
|
|
561
|
+
"Remove a single contact from a contact list. The contact itself is not deleted.",
|
|
562
|
+
{
|
|
563
|
+
listId: z.string().describe("The contact list ID"),
|
|
564
|
+
contactId: z.string().describe("The contact ID to remove from the list")
|
|
565
|
+
},
|
|
566
|
+
async ({ listId, contactId }) => {
|
|
567
|
+
try {
|
|
568
|
+
return ok(await api2("DELETE", `/contact-lists/${listId}/contacts/${contactId}`));
|
|
569
|
+
} catch (e) {
|
|
570
|
+
return err(e);
|
|
571
|
+
}
|
|
643
572
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
573
|
+
);
|
|
574
|
+
server2.tool(
|
|
575
|
+
"create_campaign",
|
|
576
|
+
"Create a campaign to send bulk SMS to contact lists. Created as a draft \u2014 preview and send separately. Supports {{variables}} in message text.",
|
|
577
|
+
{
|
|
578
|
+
name: z.string().describe("Campaign name"),
|
|
579
|
+
text: z.string().optional().describe("Message text with optional {{variables}}"),
|
|
580
|
+
templateId: z.string().optional().describe("Template ID to use instead of inline text"),
|
|
581
|
+
targetListId: z.string().optional().describe("Contact list ID to send to")
|
|
582
|
+
},
|
|
583
|
+
async ({ name, text, templateId, targetListId }) => {
|
|
584
|
+
try {
|
|
585
|
+
const body = { name };
|
|
586
|
+
if (text) body.messageText = text;
|
|
587
|
+
if (templateId) body.templateId = templateId;
|
|
588
|
+
if (targetListId) body.targetListId = targetListId;
|
|
589
|
+
return ok(await api2("POST", "/campaigns", body));
|
|
590
|
+
} catch (e) {
|
|
591
|
+
return err(e);
|
|
592
|
+
}
|
|
658
593
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
594
|
+
);
|
|
595
|
+
server2.tool(
|
|
596
|
+
"list_campaigns",
|
|
597
|
+
"List campaigns with optional filtering by status.",
|
|
598
|
+
{
|
|
599
|
+
limit: z.number().optional().describe("Campaigns to return (1-100, default 50)"),
|
|
600
|
+
offset: z.number().optional().describe("Pagination offset"),
|
|
601
|
+
status: z.enum(["draft", "scheduled", "sending", "completed", "cancelled", "failed"]).optional().describe("Filter by campaign status")
|
|
602
|
+
},
|
|
603
|
+
async ({ limit, offset, status }) => {
|
|
604
|
+
try {
|
|
605
|
+
return ok(
|
|
606
|
+
await api2("GET", "/campaigns", void 0, {
|
|
607
|
+
limit: limit?.toString(),
|
|
608
|
+
offset: offset?.toString(),
|
|
609
|
+
status
|
|
610
|
+
})
|
|
611
|
+
);
|
|
612
|
+
} catch (e) {
|
|
613
|
+
return err(e);
|
|
614
|
+
}
|
|
679
615
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
try {
|
|
692
|
-
return ok(
|
|
693
|
-
await api("GET", "/campaigns", void 0, {
|
|
694
|
-
limit: limit?.toString(),
|
|
695
|
-
offset: offset?.toString(),
|
|
696
|
-
status
|
|
697
|
-
})
|
|
698
|
-
);
|
|
699
|
-
} catch (e) {
|
|
700
|
-
return err(e);
|
|
616
|
+
);
|
|
617
|
+
server2.tool(
|
|
618
|
+
"get_campaign",
|
|
619
|
+
"Get a campaign by ID with delivery statistics (sent, delivered, failed counts).",
|
|
620
|
+
{ campaignId: z.string().describe("The campaign ID") },
|
|
621
|
+
async ({ campaignId }) => {
|
|
622
|
+
try {
|
|
623
|
+
return ok(await api2("GET", `/campaigns/${campaignId}`));
|
|
624
|
+
} catch (e) {
|
|
625
|
+
return err(e);
|
|
626
|
+
}
|
|
701
627
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
628
|
+
);
|
|
629
|
+
server2.tool(
|
|
630
|
+
"update_campaign",
|
|
631
|
+
"Update a campaign's name, text, or target list. Only draft and scheduled campaigns can be updated.",
|
|
632
|
+
{
|
|
633
|
+
campaignId: z.string().describe("The campaign ID"),
|
|
634
|
+
name: z.string().optional().describe("Updated campaign name"),
|
|
635
|
+
text: z.string().optional().describe("Updated message text"),
|
|
636
|
+
templateId: z.string().optional().describe("Updated template ID"),
|
|
637
|
+
targetListId: z.string().optional().describe("Updated target list ID")
|
|
638
|
+
},
|
|
639
|
+
async ({ campaignId, name, text, templateId, targetListId }) => {
|
|
640
|
+
try {
|
|
641
|
+
const body = {};
|
|
642
|
+
if (name) body.name = name;
|
|
643
|
+
if (text) body.messageText = text;
|
|
644
|
+
if (templateId) body.templateId = templateId;
|
|
645
|
+
if (targetListId) body.targetListId = targetListId;
|
|
646
|
+
return ok(await api2("PATCH", `/campaigns/${campaignId}`, body));
|
|
647
|
+
} catch (e) {
|
|
648
|
+
return err(e);
|
|
649
|
+
}
|
|
713
650
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
},
|
|
726
|
-
async ({ campaignId, name, text, templateId, targetListId }) => {
|
|
727
|
-
try {
|
|
728
|
-
const body = {};
|
|
729
|
-
if (name) body.name = name;
|
|
730
|
-
if (text) body.messageText = text;
|
|
731
|
-
if (templateId) body.templateId = templateId;
|
|
732
|
-
if (targetListId) body.targetListId = targetListId;
|
|
733
|
-
return ok(await api("PATCH", `/campaigns/${campaignId}`, body));
|
|
734
|
-
} catch (e) {
|
|
735
|
-
return err(e);
|
|
651
|
+
);
|
|
652
|
+
server2.tool(
|
|
653
|
+
"delete_campaign",
|
|
654
|
+
"Delete a campaign. Only draft and cancelled campaigns can be deleted.",
|
|
655
|
+
{ campaignId: z.string().describe("The campaign ID to delete") },
|
|
656
|
+
async ({ campaignId }) => {
|
|
657
|
+
try {
|
|
658
|
+
return ok(await api2("DELETE", `/campaigns/${campaignId}`));
|
|
659
|
+
} catch (e) {
|
|
660
|
+
return err(e);
|
|
661
|
+
}
|
|
736
662
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
663
|
+
);
|
|
664
|
+
server2.tool(
|
|
665
|
+
"preview_campaign",
|
|
666
|
+
"Preview a campaign before sending. Returns recipient count, estimated credit cost, and whether you have enough credits.",
|
|
667
|
+
{ campaignId: z.string().describe("The campaign ID to preview") },
|
|
668
|
+
async ({ campaignId }) => {
|
|
669
|
+
try {
|
|
670
|
+
return ok(await api2("GET", `/campaigns/${campaignId}/preview`));
|
|
671
|
+
} catch (e) {
|
|
672
|
+
return err(e);
|
|
673
|
+
}
|
|
748
674
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
675
|
+
);
|
|
676
|
+
server2.tool(
|
|
677
|
+
"send_campaign",
|
|
678
|
+
"Send a campaign immediately to all recipients in its target lists. Credits are deducted at send time. Preview first to check costs.",
|
|
679
|
+
{ campaignId: z.string().describe("The campaign ID to send") },
|
|
680
|
+
async ({ campaignId }) => {
|
|
681
|
+
try {
|
|
682
|
+
return ok(await api2("POST", `/campaigns/${campaignId}/send`));
|
|
683
|
+
} catch (e) {
|
|
684
|
+
return err(e);
|
|
685
|
+
}
|
|
760
686
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
687
|
+
);
|
|
688
|
+
server2.tool(
|
|
689
|
+
"schedule_campaign",
|
|
690
|
+
"Schedule a campaign for future delivery at a specific date and time.",
|
|
691
|
+
{
|
|
692
|
+
campaignId: z.string().describe("The campaign ID to schedule"),
|
|
693
|
+
scheduledAt: z.string().describe("ISO 8601 datetime for delivery"),
|
|
694
|
+
timezone: z.string().optional().describe("Timezone (e.g., 'America/New_York')")
|
|
695
|
+
},
|
|
696
|
+
async ({ campaignId, scheduledAt, timezone }) => {
|
|
697
|
+
try {
|
|
698
|
+
const body = { scheduledAt };
|
|
699
|
+
if (timezone) body.timezone = timezone;
|
|
700
|
+
return ok(await api2("POST", `/campaigns/${campaignId}/schedule`, body));
|
|
701
|
+
} catch (e) {
|
|
702
|
+
return err(e);
|
|
703
|
+
}
|
|
772
704
|
}
|
|
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);
|
|
705
|
+
);
|
|
706
|
+
server2.tool(
|
|
707
|
+
"cancel_campaign",
|
|
708
|
+
"Cancel a scheduled campaign before it sends. Credits reserved for the campaign are refunded.",
|
|
709
|
+
{ campaignId: z.string().describe("The campaign ID to cancel") },
|
|
710
|
+
async ({ campaignId }) => {
|
|
711
|
+
try {
|
|
712
|
+
return ok(await api2("POST", `/campaigns/${campaignId}/cancel`));
|
|
713
|
+
} catch (e) {
|
|
714
|
+
return err(e);
|
|
715
|
+
}
|
|
790
716
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
717
|
+
);
|
|
718
|
+
server2.tool(
|
|
719
|
+
"clone_campaign",
|
|
720
|
+
"Clone a campaign to create a new draft copy with the same settings. Useful for recurring or A/B campaigns.",
|
|
721
|
+
{ campaignId: z.string().describe("The campaign ID to clone") },
|
|
722
|
+
async ({ campaignId }) => {
|
|
723
|
+
try {
|
|
724
|
+
return ok(await api2("POST", `/campaigns/${campaignId}/clone`));
|
|
725
|
+
} catch (e) {
|
|
726
|
+
return err(e);
|
|
727
|
+
}
|
|
802
728
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
729
|
+
);
|
|
730
|
+
server2.tool(
|
|
731
|
+
"create_template",
|
|
732
|
+
"Create an SMS template with {{variable}} placeholders. Templates can be used with campaigns and the Verify API.",
|
|
733
|
+
{
|
|
734
|
+
name: z.string().describe("Template name"),
|
|
735
|
+
text: z.string().describe("Template text with {{variable}} placeholders")
|
|
736
|
+
},
|
|
737
|
+
async ({ name, text }) => {
|
|
738
|
+
try {
|
|
739
|
+
return ok(await api2("POST", "/templates", { name, text }));
|
|
740
|
+
} catch (e) {
|
|
741
|
+
return err(e);
|
|
742
|
+
}
|
|
814
743
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
744
|
+
);
|
|
745
|
+
server2.tool(
|
|
746
|
+
"list_templates",
|
|
747
|
+
"List all templates (custom and preset) with their status and variable definitions.",
|
|
748
|
+
{
|
|
749
|
+
limit: z.number().optional().describe("Templates to return (default 50)"),
|
|
750
|
+
offset: z.number().optional().describe("Pagination offset")
|
|
751
|
+
},
|
|
752
|
+
async ({ limit, offset }) => {
|
|
753
|
+
try {
|
|
754
|
+
return ok(
|
|
755
|
+
await api2("GET", "/templates", void 0, {
|
|
756
|
+
limit: limit?.toString(),
|
|
757
|
+
offset: offset?.toString()
|
|
758
|
+
})
|
|
759
|
+
);
|
|
760
|
+
} catch (e) {
|
|
761
|
+
return err(e);
|
|
762
|
+
}
|
|
829
763
|
}
|
|
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);
|
|
764
|
+
);
|
|
765
|
+
server2.tool(
|
|
766
|
+
"get_template",
|
|
767
|
+
"Get a template by ID including its variable definitions and publish status.",
|
|
768
|
+
{ templateId: z.string().describe("The template ID") },
|
|
769
|
+
async ({ templateId }) => {
|
|
770
|
+
try {
|
|
771
|
+
return ok(await api2("GET", `/templates/${templateId}`));
|
|
772
|
+
} catch (e) {
|
|
773
|
+
return err(e);
|
|
774
|
+
}
|
|
849
775
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
776
|
+
);
|
|
777
|
+
server2.tool(
|
|
778
|
+
"update_template",
|
|
779
|
+
"Update a template's name or text.",
|
|
780
|
+
{
|
|
781
|
+
templateId: z.string().describe("The template ID"),
|
|
782
|
+
name: z.string().optional().describe("Updated template name"),
|
|
783
|
+
text: z.string().optional().describe("Updated template text")
|
|
784
|
+
},
|
|
785
|
+
async ({ templateId, name, text }) => {
|
|
786
|
+
try {
|
|
787
|
+
const body = {};
|
|
788
|
+
if (name) body.name = name;
|
|
789
|
+
if (text) body.text = text;
|
|
790
|
+
return ok(await api2("PATCH", `/templates/${templateId}`, body));
|
|
791
|
+
} catch (e) {
|
|
792
|
+
return err(e);
|
|
793
|
+
}
|
|
861
794
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
try {
|
|
874
|
-
const body = {};
|
|
875
|
-
if (name) body.name = name;
|
|
876
|
-
if (text) body.text = text;
|
|
877
|
-
return ok(await api("PATCH", `/templates/${templateId}`, body));
|
|
878
|
-
} catch (e) {
|
|
879
|
-
return err(e);
|
|
795
|
+
);
|
|
796
|
+
server2.tool(
|
|
797
|
+
"delete_template",
|
|
798
|
+
"Delete a custom template. Preset templates cannot be deleted.",
|
|
799
|
+
{ templateId: z.string().describe("The template ID to delete") },
|
|
800
|
+
async ({ templateId }) => {
|
|
801
|
+
try {
|
|
802
|
+
return ok(await api2("DELETE", `/templates/${templateId}`));
|
|
803
|
+
} catch (e) {
|
|
804
|
+
return err(e);
|
|
805
|
+
}
|
|
880
806
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
807
|
+
);
|
|
808
|
+
server2.tool(
|
|
809
|
+
"publish_template",
|
|
810
|
+
"Publish a template, making it available for use with the Verify API and campaigns.",
|
|
811
|
+
{ templateId: z.string().describe("The template ID to publish") },
|
|
812
|
+
async ({ templateId }) => {
|
|
813
|
+
try {
|
|
814
|
+
return ok(await api2("POST", `/templates/${templateId}/publish`));
|
|
815
|
+
} catch (e) {
|
|
816
|
+
return err(e);
|
|
817
|
+
}
|
|
892
818
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
819
|
+
);
|
|
820
|
+
server2.tool(
|
|
821
|
+
"preview_template",
|
|
822
|
+
"Preview a template with sample variable values to see the interpolated result.",
|
|
823
|
+
{
|
|
824
|
+
templateId: z.string().describe("The template ID to preview"),
|
|
825
|
+
variables: z.record(z.string(), z.string()).optional().describe("Variable values (e.g., { app_name: 'MyApp', code: '123456' })")
|
|
826
|
+
},
|
|
827
|
+
async ({ templateId, variables }) => {
|
|
828
|
+
try {
|
|
829
|
+
const body = {};
|
|
830
|
+
if (variables) body.variables = variables;
|
|
831
|
+
return ok(await api2("POST", `/templates/${templateId}/preview`, body));
|
|
832
|
+
} catch (e) {
|
|
833
|
+
return err(e);
|
|
834
|
+
}
|
|
904
835
|
}
|
|
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);
|
|
836
|
+
);
|
|
837
|
+
server2.tool(
|
|
838
|
+
"list_template_presets",
|
|
839
|
+
"List system preset templates (OTP, 2FA, login, etc.) that can be used as-is or cloned.",
|
|
840
|
+
{},
|
|
841
|
+
async () => {
|
|
842
|
+
try {
|
|
843
|
+
return ok(await api2("GET", "/templates/presets"));
|
|
844
|
+
} catch (e) {
|
|
845
|
+
return err(e);
|
|
846
|
+
}
|
|
921
847
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
848
|
+
);
|
|
849
|
+
server2.tool(
|
|
850
|
+
"create_label",
|
|
851
|
+
"Create a label for categorizing conversations and messages. Labels have a name and optional color.",
|
|
852
|
+
{
|
|
853
|
+
name: z.string().describe("Label name (e.g., 'urgent', 'vip', 'follow-up')"),
|
|
854
|
+
color: z.string().optional().describe("Hex color code (default: #6b7280)"),
|
|
855
|
+
description: z.string().optional().describe("Label description")
|
|
856
|
+
},
|
|
857
|
+
async ({ name, color, description }) => {
|
|
858
|
+
try {
|
|
859
|
+
const body = { name };
|
|
860
|
+
if (color) body.color = color;
|
|
861
|
+
if (description) body.description = description;
|
|
862
|
+
return ok(await api2("POST", "/labels", body));
|
|
863
|
+
} catch (e) {
|
|
864
|
+
return err(e);
|
|
865
|
+
}
|
|
933
866
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
try {
|
|
946
|
-
const body = { name };
|
|
947
|
-
if (color) body.color = color;
|
|
948
|
-
if (description) body.description = description;
|
|
949
|
-
return ok(await api("POST", "/labels", body));
|
|
950
|
-
} catch (e) {
|
|
951
|
-
return err(e);
|
|
867
|
+
);
|
|
868
|
+
server2.tool(
|
|
869
|
+
"list_labels",
|
|
870
|
+
"List all labels available in your workspace.",
|
|
871
|
+
{},
|
|
872
|
+
async () => {
|
|
873
|
+
try {
|
|
874
|
+
return ok(await api2("GET", "/labels"));
|
|
875
|
+
} catch (e) {
|
|
876
|
+
return err(e);
|
|
877
|
+
}
|
|
952
878
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
879
|
+
);
|
|
880
|
+
server2.tool(
|
|
881
|
+
"add_conversation_label",
|
|
882
|
+
"Add one or more labels to a conversation for categorization.",
|
|
883
|
+
{
|
|
884
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
885
|
+
labelIds: z.array(z.string()).describe("Array of label IDs to add")
|
|
886
|
+
},
|
|
887
|
+
async ({ conversationId, labelIds }) => {
|
|
888
|
+
try {
|
|
889
|
+
return ok(await api2("POST", `/conversations/${conversationId}/labels`, { labelIds }));
|
|
890
|
+
} catch (e) {
|
|
891
|
+
return err(e);
|
|
892
|
+
}
|
|
964
893
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
894
|
+
);
|
|
895
|
+
server2.tool(
|
|
896
|
+
"remove_conversation_label",
|
|
897
|
+
"Remove a label from a conversation.",
|
|
898
|
+
{
|
|
899
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
900
|
+
labelId: z.string().describe("The label ID to remove")
|
|
901
|
+
},
|
|
902
|
+
async ({ conversationId, labelId }) => {
|
|
903
|
+
try {
|
|
904
|
+
return ok(await api2("DELETE", `/conversations/${conversationId}/labels/${labelId}`));
|
|
905
|
+
} catch (e) {
|
|
906
|
+
return err(e);
|
|
907
|
+
}
|
|
979
908
|
}
|
|
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);
|
|
909
|
+
);
|
|
910
|
+
server2.tool(
|
|
911
|
+
"list_rules",
|
|
912
|
+
"List auto-label rules that automatically tag conversations based on AI-detected intent and sentiment.",
|
|
913
|
+
{},
|
|
914
|
+
async () => {
|
|
915
|
+
try {
|
|
916
|
+
return ok(await api2("GET", "/rules"));
|
|
917
|
+
} catch (e) {
|
|
918
|
+
return err(e);
|
|
919
|
+
}
|
|
994
920
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
921
|
+
);
|
|
922
|
+
server2.tool(
|
|
923
|
+
"create_rule",
|
|
924
|
+
"Create an auto-label rule. Rules automatically apply labels to conversations when conditions match.",
|
|
925
|
+
{
|
|
926
|
+
name: z.string().describe("Rule name"),
|
|
927
|
+
conditions: z.object({
|
|
928
|
+
intent: z.union([z.string(), z.array(z.string())]).optional().describe("Intent(s) to match"),
|
|
929
|
+
sentiment: z.union([z.string(), z.array(z.string())]).optional().describe("Sentiment(s) to match")
|
|
930
|
+
}).describe("Conditions that trigger the rule"),
|
|
931
|
+
actions: z.object({
|
|
932
|
+
addLabels: z.array(z.string()).describe("Label IDs to add when rule matches"),
|
|
933
|
+
closeConversation: z.boolean().optional().describe("Automatically close the conversation")
|
|
934
|
+
}).describe("Actions to take when conditions match"),
|
|
935
|
+
priority: z.number().optional().describe("Rule priority (lower runs first)")
|
|
936
|
+
},
|
|
937
|
+
async ({ name, conditions, actions, priority }) => {
|
|
938
|
+
try {
|
|
939
|
+
const body = { name, conditions, actions };
|
|
940
|
+
if (priority !== void 0) body.priority = priority;
|
|
941
|
+
return ok(await api2("POST", "/rules", body));
|
|
942
|
+
} catch (e) {
|
|
943
|
+
return err(e);
|
|
944
|
+
}
|
|
1006
945
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
946
|
+
);
|
|
947
|
+
server2.tool(
|
|
948
|
+
"update_rule",
|
|
949
|
+
"Update an auto-label rule's name, conditions, actions, or enabled status.",
|
|
950
|
+
{
|
|
951
|
+
ruleId: z.string().describe("The rule ID"),
|
|
952
|
+
name: z.string().optional().describe("Updated rule name"),
|
|
953
|
+
conditions: z.object({
|
|
954
|
+
intent: z.union([z.string(), z.array(z.string())]).optional(),
|
|
955
|
+
sentiment: z.union([z.string(), z.array(z.string())]).optional()
|
|
956
|
+
}).optional().describe("Updated conditions"),
|
|
957
|
+
actions: z.object({
|
|
958
|
+
addLabels: z.array(z.string()),
|
|
959
|
+
closeConversation: z.boolean().optional()
|
|
960
|
+
}).optional().describe("Updated actions"),
|
|
961
|
+
enabled: z.boolean().optional().describe("Enable or disable the rule")
|
|
962
|
+
},
|
|
963
|
+
async ({ ruleId, name, conditions, actions, enabled }) => {
|
|
964
|
+
try {
|
|
965
|
+
const body = {};
|
|
966
|
+
if (name) body.name = name;
|
|
967
|
+
if (conditions) body.conditions = conditions;
|
|
968
|
+
if (actions) body.actions = actions;
|
|
969
|
+
if (enabled !== void 0) body.enabled = enabled;
|
|
970
|
+
return ok(await api2("PATCH", `/rules/${ruleId}`, body));
|
|
971
|
+
} catch (e) {
|
|
972
|
+
return err(e);
|
|
973
|
+
}
|
|
1031
974
|
}
|
|
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);
|
|
975
|
+
);
|
|
976
|
+
server2.tool(
|
|
977
|
+
"delete_rule",
|
|
978
|
+
"Delete an auto-label rule. Existing labels already applied by this rule are not removed.",
|
|
979
|
+
{ ruleId: z.string().describe("The rule ID to delete") },
|
|
980
|
+
async ({ ruleId }) => {
|
|
981
|
+
try {
|
|
982
|
+
return ok(await api2("DELETE", `/rules/${ruleId}`));
|
|
983
|
+
} catch (e) {
|
|
984
|
+
return err(e);
|
|
985
|
+
}
|
|
1060
986
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
987
|
+
);
|
|
988
|
+
server2.tool(
|
|
989
|
+
"create_draft",
|
|
990
|
+
"Create a message draft for human review before sending. The draft must be approved before it becomes a real SMS.",
|
|
991
|
+
{
|
|
992
|
+
conversationId: z.string().describe("The conversation ID"),
|
|
993
|
+
text: z.string().describe("Draft message text"),
|
|
994
|
+
source: z.string().optional().describe("Source of the draft (default: 'ai')")
|
|
995
|
+
},
|
|
996
|
+
async ({ conversationId, text, source }) => {
|
|
997
|
+
try {
|
|
998
|
+
const body = { conversationId, text };
|
|
999
|
+
if (source) body.source = source;
|
|
1000
|
+
return ok(await api2("POST", "/drafts", body));
|
|
1001
|
+
} catch (e) {
|
|
1002
|
+
return err(e);
|
|
1003
|
+
}
|
|
1072
1004
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1005
|
+
);
|
|
1006
|
+
server2.tool(
|
|
1007
|
+
"list_drafts",
|
|
1008
|
+
"List message drafts, optionally filtered by conversation or status.",
|
|
1009
|
+
{
|
|
1010
|
+
conversationId: z.string().optional().describe("Filter by conversation ID"),
|
|
1011
|
+
status: z.enum(["pending", "approved", "rejected", "sent", "failed"]).optional().describe("Filter by status")
|
|
1012
|
+
},
|
|
1013
|
+
async ({ conversationId, status }) => {
|
|
1014
|
+
try {
|
|
1015
|
+
return ok(
|
|
1016
|
+
await api2("GET", "/drafts", void 0, {
|
|
1017
|
+
conversation_id: conversationId,
|
|
1018
|
+
status
|
|
1019
|
+
})
|
|
1020
|
+
);
|
|
1021
|
+
} catch (e) {
|
|
1022
|
+
return err(e);
|
|
1023
|
+
}
|
|
1090
1024
|
}
|
|
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);
|
|
1025
|
+
);
|
|
1026
|
+
server2.tool(
|
|
1027
|
+
"approve_draft",
|
|
1028
|
+
"Approve a pending draft and send it as a real SMS message. Runs compliance checks and deducts credits at approval time.",
|
|
1029
|
+
{ draftId: z.string().describe("The draft ID to approve") },
|
|
1030
|
+
async ({ draftId }) => {
|
|
1031
|
+
try {
|
|
1032
|
+
return ok(await api2("POST", `/drafts/${draftId}/approve`));
|
|
1033
|
+
} catch (e) {
|
|
1034
|
+
return err(e);
|
|
1035
|
+
}
|
|
1110
1036
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1037
|
+
);
|
|
1038
|
+
server2.tool(
|
|
1039
|
+
"reject_draft",
|
|
1040
|
+
"Reject a pending draft with an optional reason. The message will not be sent.",
|
|
1041
|
+
{
|
|
1042
|
+
draftId: z.string().describe("The draft ID to reject"),
|
|
1043
|
+
reason: z.string().optional().describe("Reason for rejection")
|
|
1044
|
+
},
|
|
1045
|
+
async ({ draftId, reason }) => {
|
|
1046
|
+
try {
|
|
1047
|
+
const body = {};
|
|
1048
|
+
if (reason) body.reason = reason;
|
|
1049
|
+
return ok(await api2("POST", `/drafts/${draftId}/reject`, body));
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
return err(e);
|
|
1052
|
+
}
|
|
1122
1053
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1054
|
+
);
|
|
1055
|
+
server2.tool(
|
|
1056
|
+
"create_webhook",
|
|
1057
|
+
"Create a webhook endpoint to receive real-time event notifications. Returns a signing secret (shown only once) for verifying payloads.",
|
|
1058
|
+
{
|
|
1059
|
+
url: z.string().describe("HTTPS endpoint URL to receive events"),
|
|
1060
|
+
events: z.array(z.string()).describe("Event types to subscribe to (e.g., ['message.delivered', 'message.failed'])"),
|
|
1061
|
+
description: z.string().optional().describe("Description of this webhook")
|
|
1062
|
+
},
|
|
1063
|
+
async ({ url, events, description }) => {
|
|
1064
|
+
try {
|
|
1065
|
+
const body = { url, events };
|
|
1066
|
+
if (description) body.description = description;
|
|
1067
|
+
return ok(await api2("POST", "/webhooks", body));
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
return err(e);
|
|
1070
|
+
}
|
|
1139
1071
|
}
|
|
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);
|
|
1072
|
+
);
|
|
1073
|
+
server2.tool(
|
|
1074
|
+
"list_webhooks",
|
|
1075
|
+
"List all webhook endpoints with their status and event subscriptions.",
|
|
1076
|
+
{},
|
|
1077
|
+
async () => {
|
|
1078
|
+
try {
|
|
1079
|
+
return ok(await api2("GET", "/webhooks"));
|
|
1080
|
+
} catch (e) {
|
|
1081
|
+
return err(e);
|
|
1082
|
+
}
|
|
1157
1083
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1084
|
+
);
|
|
1085
|
+
server2.tool(
|
|
1086
|
+
"get_webhook",
|
|
1087
|
+
"Get a webhook by ID with delivery statistics.",
|
|
1088
|
+
{ webhookId: z.string().describe("The webhook ID") },
|
|
1089
|
+
async ({ webhookId }) => {
|
|
1090
|
+
try {
|
|
1091
|
+
return ok(await api2("GET", `/webhooks/${webhookId}`));
|
|
1092
|
+
} catch (e) {
|
|
1093
|
+
return err(e);
|
|
1094
|
+
}
|
|
1169
1095
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1096
|
+
);
|
|
1097
|
+
server2.tool(
|
|
1098
|
+
"update_webhook",
|
|
1099
|
+
"Update a webhook's URL, events, description, or active status.",
|
|
1100
|
+
{
|
|
1101
|
+
webhookId: z.string().describe("The webhook ID"),
|
|
1102
|
+
url: z.string().optional().describe("Updated HTTPS endpoint URL"),
|
|
1103
|
+
events: z.array(z.string()).optional().describe("Updated event types"),
|
|
1104
|
+
description: z.string().optional().describe("Updated description"),
|
|
1105
|
+
isActive: z.boolean().optional().describe("Enable or disable the webhook")
|
|
1106
|
+
},
|
|
1107
|
+
async ({ webhookId, url, events, description, isActive }) => {
|
|
1108
|
+
try {
|
|
1109
|
+
const body = {};
|
|
1110
|
+
if (url) body.url = url;
|
|
1111
|
+
if (events) body.events = events;
|
|
1112
|
+
if (description !== void 0) body.description = description;
|
|
1113
|
+
if (isActive !== void 0) body.is_active = isActive;
|
|
1114
|
+
return ok(await api2("PATCH", `/webhooks/${webhookId}`, body));
|
|
1115
|
+
} catch (e) {
|
|
1116
|
+
return err(e);
|
|
1117
|
+
}
|
|
1181
1118
|
}
|
|
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);
|
|
1119
|
+
);
|
|
1120
|
+
server2.tool(
|
|
1121
|
+
"delete_webhook",
|
|
1122
|
+
"Delete a webhook endpoint. Stops all future event deliveries to this URL.",
|
|
1123
|
+
{ webhookId: z.string().describe("The webhook ID to delete") },
|
|
1124
|
+
async ({ webhookId }) => {
|
|
1125
|
+
try {
|
|
1126
|
+
return ok(await api2("DELETE", `/webhooks/${webhookId}`));
|
|
1127
|
+
} catch (e) {
|
|
1128
|
+
return err(e);
|
|
1129
|
+
}
|
|
1204
1130
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1131
|
+
);
|
|
1132
|
+
server2.tool(
|
|
1133
|
+
"test_webhook",
|
|
1134
|
+
"Send a test event to a webhook endpoint to verify it is reachable. Returns response status and latency.",
|
|
1135
|
+
{ webhookId: z.string().describe("The webhook ID to test") },
|
|
1136
|
+
async ({ webhookId }) => {
|
|
1137
|
+
try {
|
|
1138
|
+
return ok(await api2("POST", `/webhooks/${webhookId}/test`));
|
|
1139
|
+
} catch (e) {
|
|
1140
|
+
return err(e);
|
|
1141
|
+
}
|
|
1216
1142
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1143
|
+
);
|
|
1144
|
+
server2.tool(
|
|
1145
|
+
"list_webhook_deliveries",
|
|
1146
|
+
"Get delivery history for a webhook showing recent attempts, statuses, and response times.",
|
|
1147
|
+
{ webhookId: z.string().describe("The webhook ID") },
|
|
1148
|
+
async ({ webhookId }) => {
|
|
1149
|
+
try {
|
|
1150
|
+
return ok(await api2("GET", `/webhooks/${webhookId}/deliveries`));
|
|
1151
|
+
} catch (e) {
|
|
1152
|
+
return err(e);
|
|
1153
|
+
}
|
|
1228
1154
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1155
|
+
);
|
|
1156
|
+
server2.tool(
|
|
1157
|
+
"rotate_webhook_secret",
|
|
1158
|
+
"Rotate a webhook's signing secret. The old secret stops working immediately.",
|
|
1159
|
+
{ webhookId: z.string().describe("The webhook ID") },
|
|
1160
|
+
async ({ webhookId }) => {
|
|
1161
|
+
try {
|
|
1162
|
+
return ok(await api2("POST", `/webhooks/${webhookId}/rotate-secret`));
|
|
1163
|
+
} catch (e) {
|
|
1164
|
+
return err(e);
|
|
1165
|
+
}
|
|
1240
1166
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1167
|
+
);
|
|
1168
|
+
server2.tool(
|
|
1169
|
+
"list_webhook_event_types",
|
|
1170
|
+
"List all available webhook event types that can be subscribed to.",
|
|
1171
|
+
{},
|
|
1172
|
+
async () => {
|
|
1173
|
+
try {
|
|
1174
|
+
return ok(await api2("GET", "/webhooks/event-types"));
|
|
1175
|
+
} catch (e) {
|
|
1176
|
+
return err(e);
|
|
1177
|
+
}
|
|
1252
1178
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1179
|
+
);
|
|
1180
|
+
server2.tool(
|
|
1181
|
+
"send_otp",
|
|
1182
|
+
"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.",
|
|
1183
|
+
{
|
|
1184
|
+
to: z.string().describe("Phone number to verify in E.164 format"),
|
|
1185
|
+
appName: z.string().optional().describe("Your app/brand name shown in the SMS"),
|
|
1186
|
+
codeLength: z.number().optional().describe("Digits in the code (default 6)"),
|
|
1187
|
+
timeoutSecs: z.number().optional().describe("Code validity in seconds (default 300)"),
|
|
1188
|
+
templateId: z.string().optional().describe("Custom OTP template ID")
|
|
1189
|
+
},
|
|
1190
|
+
async ({ to, appName, codeLength, timeoutSecs, templateId }) => {
|
|
1191
|
+
try {
|
|
1192
|
+
const body = { to };
|
|
1193
|
+
if (appName) body.app_name = appName;
|
|
1194
|
+
if (codeLength) body.code_length = codeLength;
|
|
1195
|
+
if (timeoutSecs) body.timeout_secs = timeoutSecs;
|
|
1196
|
+
if (templateId) body.template_id = templateId;
|
|
1197
|
+
return ok(await api2("POST", "/verify", body));
|
|
1198
|
+
} catch (e) {
|
|
1199
|
+
return err(e);
|
|
1200
|
+
}
|
|
1264
1201
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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);
|
|
1202
|
+
);
|
|
1203
|
+
server2.tool(
|
|
1204
|
+
"check_otp",
|
|
1205
|
+
"Verify an OTP code. Returns 'verified' (correct), 'invalid_code' (wrong), 'expired', or 'max_attempts_exceeded'.",
|
|
1206
|
+
{
|
|
1207
|
+
verificationId: z.string().describe("The verification ID from send_otp"),
|
|
1208
|
+
code: z.string().describe("The code entered by the user")
|
|
1209
|
+
},
|
|
1210
|
+
async ({ verificationId, code }) => {
|
|
1211
|
+
try {
|
|
1212
|
+
return ok(await api2("POST", `/verify/${verificationId}/check`, { code }));
|
|
1213
|
+
} catch (e) {
|
|
1214
|
+
return err(e);
|
|
1215
|
+
}
|
|
1287
1216
|
}
|
|
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);
|
|
1217
|
+
);
|
|
1218
|
+
server2.tool(
|
|
1219
|
+
"get_verification_status",
|
|
1220
|
+
"Check the current status of an OTP verification (pending, verified, expired, failed).",
|
|
1221
|
+
{ verificationId: z.string().describe("The verification ID") },
|
|
1222
|
+
async ({ verificationId }) => {
|
|
1223
|
+
try {
|
|
1224
|
+
return ok(await api2("GET", `/verify/${verificationId}`));
|
|
1225
|
+
} catch (e) {
|
|
1226
|
+
return err(e);
|
|
1227
|
+
}
|
|
1302
1228
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1229
|
+
);
|
|
1230
|
+
server2.tool(
|
|
1231
|
+
"resend_otp",
|
|
1232
|
+
"Resend an OTP verification code. Use when the original SMS was not received.",
|
|
1233
|
+
{ verificationId: z.string().describe("The verification ID from send_otp") },
|
|
1234
|
+
async ({ verificationId }) => {
|
|
1235
|
+
try {
|
|
1236
|
+
return ok(await api2("POST", `/verify/${verificationId}/resend`));
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
return err(e);
|
|
1239
|
+
}
|
|
1314
1240
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1241
|
+
);
|
|
1242
|
+
server2.tool(
|
|
1243
|
+
"list_verifications",
|
|
1244
|
+
"List recent OTP verifications with pagination.",
|
|
1245
|
+
{
|
|
1246
|
+
limit: z.number().optional().describe("Verifications to return (default 50)")
|
|
1247
|
+
},
|
|
1248
|
+
async ({ limit }) => {
|
|
1249
|
+
try {
|
|
1250
|
+
return ok(
|
|
1251
|
+
await api2("GET", "/verify", void 0, {
|
|
1252
|
+
limit: limit?.toString()
|
|
1253
|
+
})
|
|
1254
|
+
);
|
|
1255
|
+
} catch (e) {
|
|
1256
|
+
return err(e);
|
|
1257
|
+
}
|
|
1326
1258
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1259
|
+
);
|
|
1260
|
+
server2.tool(
|
|
1261
|
+
"create_verify_session",
|
|
1262
|
+
"Create a hosted verify session with a branded UI. Returns a URL to redirect the user to for phone verification. Zero frontend code needed.",
|
|
1263
|
+
{
|
|
1264
|
+
successUrl: z.string().describe("URL to redirect to after successful verification"),
|
|
1265
|
+
cancelUrl: z.string().optional().describe("URL to redirect to if user cancels"),
|
|
1266
|
+
brandName: z.string().optional().describe("Your brand name shown on the verify page"),
|
|
1267
|
+
brandColor: z.string().optional().describe("Brand color hex code (e.g., #4F46E5)"),
|
|
1268
|
+
metadata: z.record(z.string(), z.any()).optional().describe("Custom metadata to attach to the session")
|
|
1269
|
+
},
|
|
1270
|
+
async ({ successUrl, cancelUrl, brandName, brandColor, metadata }) => {
|
|
1271
|
+
try {
|
|
1272
|
+
const body = { success_url: successUrl };
|
|
1273
|
+
if (cancelUrl) body.cancel_url = cancelUrl;
|
|
1274
|
+
if (brandName) body.brand_name = brandName;
|
|
1275
|
+
if (brandColor) body.brand_color = brandColor;
|
|
1276
|
+
if (metadata) body.metadata = metadata;
|
|
1277
|
+
return ok(await api2("POST", "/verify/sessions", body));
|
|
1278
|
+
} catch (e) {
|
|
1279
|
+
return err(e);
|
|
1280
|
+
}
|
|
1346
1281
|
}
|
|
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);
|
|
1282
|
+
);
|
|
1283
|
+
server2.tool(
|
|
1284
|
+
"validate_verify_session",
|
|
1285
|
+
"Validate a session token returned after a user completes hosted verification. Returns the verified phone number.",
|
|
1286
|
+
{ token: z.string().describe("The session token from the success redirect URL") },
|
|
1287
|
+
async ({ token }) => {
|
|
1288
|
+
try {
|
|
1289
|
+
return ok(await api2("POST", "/verify/sessions/validate", { token }));
|
|
1290
|
+
} catch (e) {
|
|
1291
|
+
return err(e);
|
|
1292
|
+
}
|
|
1369
1293
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1294
|
+
);
|
|
1295
|
+
server2.tool(
|
|
1296
|
+
"get_credits",
|
|
1297
|
+
"Get current credit balance including reserved credits for scheduled messages.",
|
|
1298
|
+
{},
|
|
1299
|
+
async () => {
|
|
1300
|
+
try {
|
|
1301
|
+
return ok(await api2("GET", "/credits"));
|
|
1302
|
+
} catch (e) {
|
|
1303
|
+
return err(e);
|
|
1304
|
+
}
|
|
1381
1305
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1306
|
+
);
|
|
1307
|
+
server2.tool(
|
|
1308
|
+
"list_credit_transactions",
|
|
1309
|
+
"List credit transaction history showing purchases, usage, refunds, and transfers.",
|
|
1310
|
+
{
|
|
1311
|
+
limit: z.number().optional().describe("Transactions to return (default 50)")
|
|
1312
|
+
},
|
|
1313
|
+
async ({ limit }) => {
|
|
1314
|
+
try {
|
|
1315
|
+
return ok(
|
|
1316
|
+
await api2("GET", "/credits/transactions", void 0, {
|
|
1317
|
+
limit: limit?.toString()
|
|
1318
|
+
})
|
|
1319
|
+
);
|
|
1320
|
+
} catch (e) {
|
|
1321
|
+
return err(e);
|
|
1322
|
+
}
|
|
1393
1323
|
}
|
|
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);
|
|
1324
|
+
);
|
|
1325
|
+
server2.tool(
|
|
1326
|
+
"get_account",
|
|
1327
|
+
"Get account info: credit balance, phone number verification status, rate limits, and API key details.",
|
|
1328
|
+
{},
|
|
1329
|
+
async () => {
|
|
1330
|
+
try {
|
|
1331
|
+
return ok(await api2("GET", "/account"));
|
|
1332
|
+
} catch (e) {
|
|
1333
|
+
return err(e);
|
|
1334
|
+
}
|
|
1413
1335
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1336
|
+
);
|
|
1337
|
+
server2.tool(
|
|
1338
|
+
"generate_business_page",
|
|
1339
|
+
"Generate a hosted business landing page for verification. Returns a URL at sendly.live/biz/{slug}. Enterprise accounts only.",
|
|
1340
|
+
{
|
|
1341
|
+
businessName: z.string().describe("Business name"),
|
|
1342
|
+
useCase: z.string().optional().describe("Use case (e.g., Insurance Services, Appointment Reminders, 2FA)"),
|
|
1343
|
+
useCaseSummary: z.string().optional().describe("Brief description of what the business does"),
|
|
1344
|
+
contactEmail: z.string().optional().describe("Business contact email"),
|
|
1345
|
+
contactPhone: z.string().optional().describe("Business phone number"),
|
|
1346
|
+
businessAddress: z.string().optional().describe("City, State ZIP (e.g., Chicago, IL 60601)")
|
|
1347
|
+
},
|
|
1348
|
+
async (params) => {
|
|
1349
|
+
try {
|
|
1350
|
+
return ok(await api2("POST", "/enterprise/business-page/generate", params));
|
|
1351
|
+
} catch (e) {
|
|
1352
|
+
return err(e);
|
|
1353
|
+
}
|
|
1425
1354
|
}
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/index.ts
|
|
1359
|
+
var VERSION = "2.0.0";
|
|
1360
|
+
var API_KEY = process.env.SENDLY_API_KEY;
|
|
1361
|
+
var BASE_URL = process.env.SENDLY_BASE_URL || "https://sendly.live";
|
|
1362
|
+
if (!API_KEY) {
|
|
1363
|
+
process.stderr.write(
|
|
1364
|
+
"SENDLY_API_KEY environment variable is required.\nGet your API key at https://sendly.live \u2192 Settings \u2192 API Keys\n"
|
|
1365
|
+
);
|
|
1366
|
+
process.exit(1);
|
|
1367
|
+
}
|
|
1368
|
+
if (!BASE_URL.startsWith("https://") && !BASE_URL.startsWith("http://localhost") && !BASE_URL.startsWith("http://127.0.0.1")) {
|
|
1369
|
+
process.stderr.write(
|
|
1370
|
+
"SENDLY_BASE_URL must use HTTPS in production.\nHTTP is only allowed for localhost development.\n"
|
|
1371
|
+
);
|
|
1372
|
+
process.exit(1);
|
|
1373
|
+
}
|
|
1374
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1375
|
+
var RATE_LIMIT_MAX = 60;
|
|
1376
|
+
var rateLimitTokens = RATE_LIMIT_MAX;
|
|
1377
|
+
var rateLimitResetAt = Date.now() + RATE_LIMIT_WINDOW_MS;
|
|
1378
|
+
function checkRateLimit() {
|
|
1379
|
+
const now = Date.now();
|
|
1380
|
+
if (now >= rateLimitResetAt) {
|
|
1381
|
+
rateLimitTokens = RATE_LIMIT_MAX;
|
|
1382
|
+
rateLimitResetAt = now + RATE_LIMIT_WINDOW_MS;
|
|
1426
1383
|
}
|
|
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);
|
|
1384
|
+
if (rateLimitTokens <= 0) return false;
|
|
1385
|
+
rateLimitTokens--;
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1388
|
+
async function api(method, path, body, query) {
|
|
1389
|
+
if (!checkRateLimit()) {
|
|
1390
|
+
throw new Error("Rate limited \u2014 too many requests. Wait a moment and try again.");
|
|
1391
|
+
}
|
|
1392
|
+
const url = new URL(`/api/v1${path}`, BASE_URL);
|
|
1393
|
+
if (query) {
|
|
1394
|
+
for (const [k, v] of Object.entries(query)) {
|
|
1395
|
+
if (v !== void 0) url.searchParams.set(k, v);
|
|
1444
1396
|
}
|
|
1445
1397
|
}
|
|
1446
|
-
|
|
1398
|
+
const headers = {
|
|
1399
|
+
Authorization: `Bearer ${API_KEY}`
|
|
1400
|
+
};
|
|
1401
|
+
if (body) headers["Content-Type"] = "application/json";
|
|
1402
|
+
const res = await fetch(url.toString(), {
|
|
1403
|
+
method,
|
|
1404
|
+
headers,
|
|
1405
|
+
body: body ? JSON.stringify(body) : void 0
|
|
1406
|
+
});
|
|
1407
|
+
if (res.status === 204) return { success: true };
|
|
1408
|
+
if (res.status === 429) {
|
|
1409
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
1410
|
+
throw new Error(
|
|
1411
|
+
`Rate limited by API. ${retryAfter ? `Retry after ${retryAfter} seconds.` : "Wait a moment and try again."}`
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
const data = await res.json();
|
|
1415
|
+
if (!res.ok) {
|
|
1416
|
+
const msg = typeof data === "object" && data !== null ? data.error || data.message || JSON.stringify(data) : String(data);
|
|
1417
|
+
throw new Error(String(msg));
|
|
1418
|
+
}
|
|
1419
|
+
return data;
|
|
1420
|
+
}
|
|
1421
|
+
var server = new McpServer({
|
|
1422
|
+
name: "sendly",
|
|
1423
|
+
version: VERSION
|
|
1424
|
+
});
|
|
1425
|
+
registerAllTools(server, api);
|
|
1447
1426
|
var transport = new StdioServerTransport();
|
|
1448
1427
|
await server.connect(transport);
|