@buzzposter/mcp 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1777 -167
- package/dist/tools.d.ts +63 -8
- package/dist/tools.js +1757 -152
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -101,29 +101,9 @@ var BuzzPosterClient = class {
|
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
// Media
|
|
104
|
-
async uploadMediaMultipart(filename, buffer, mimeType) {
|
|
105
|
-
const formData = new FormData();
|
|
106
|
-
formData.append("file", new Blob([buffer], { type: mimeType }), filename);
|
|
107
|
-
const url = new URL(`${this.baseUrl}/api/v1/media/upload`);
|
|
108
|
-
const res = await fetch(url, {
|
|
109
|
-
method: "POST",
|
|
110
|
-
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
111
|
-
body: formData
|
|
112
|
-
});
|
|
113
|
-
if (!res.ok) {
|
|
114
|
-
const errorBody = await res.json().catch(() => ({ message: res.statusText }));
|
|
115
|
-
const message = typeof errorBody === "object" && errorBody !== null && "message" in errorBody ? String(errorBody.message) : `API error (${res.status})`;
|
|
116
|
-
throw new Error(`BuzzPoster API error (${res.status}): ${message}`);
|
|
117
|
-
}
|
|
118
|
-
const text = await res.text();
|
|
119
|
-
return text ? JSON.parse(text) : void 0;
|
|
120
|
-
}
|
|
121
104
|
async uploadFromUrl(data) {
|
|
122
105
|
return this.request("POST", "/api/v1/media/upload-from-url", data);
|
|
123
106
|
}
|
|
124
|
-
async uploadBase64(data) {
|
|
125
|
-
return this.request("POST", "/api/v1/media/upload-base64", data);
|
|
126
|
-
}
|
|
127
107
|
async getUploadUrl(data) {
|
|
128
108
|
return this.request("POST", "/api/v1/media/presign", data);
|
|
129
109
|
}
|
|
@@ -271,6 +251,12 @@ var BuzzPosterClient = class {
|
|
|
271
251
|
async updateAudience(id, data) {
|
|
272
252
|
return this.request("PUT", `/api/v1/audiences/${id}`, data);
|
|
273
253
|
}
|
|
254
|
+
async listAudiences() {
|
|
255
|
+
return this.request("GET", "/api/v1/audiences");
|
|
256
|
+
}
|
|
257
|
+
async deleteAudience(id) {
|
|
258
|
+
return this.request("DELETE", `/api/v1/audiences/${id}`);
|
|
259
|
+
}
|
|
274
260
|
// Newsletter Templates
|
|
275
261
|
async getTemplate(id) {
|
|
276
262
|
if (id) return this.request("GET", `/api/v1/templates/${id}`);
|
|
@@ -279,6 +265,18 @@ var BuzzPosterClient = class {
|
|
|
279
265
|
async listTemplates() {
|
|
280
266
|
return this.request("GET", "/api/v1/templates");
|
|
281
267
|
}
|
|
268
|
+
async createTemplate(data) {
|
|
269
|
+
return this.request("POST", "/api/v1/templates", data);
|
|
270
|
+
}
|
|
271
|
+
async updateTemplate(id, data) {
|
|
272
|
+
return this.request("PUT", "/api/v1/templates/" + id, data);
|
|
273
|
+
}
|
|
274
|
+
async deleteTemplate(id) {
|
|
275
|
+
return this.request("DELETE", "/api/v1/templates/" + id);
|
|
276
|
+
}
|
|
277
|
+
async duplicateTemplate(id) {
|
|
278
|
+
return this.request("POST", "/api/v1/templates/" + id + "/duplicate");
|
|
279
|
+
}
|
|
282
280
|
// Newsletter Archive
|
|
283
281
|
async listNewsletterArchive(params) {
|
|
284
282
|
return this.request("GET", "/api/v1/newsletter-archive", void 0, params);
|
|
@@ -382,6 +380,92 @@ var BuzzPosterClient = class {
|
|
|
382
380
|
async previewCarousel(data) {
|
|
383
381
|
return this.request("POST", "/api/v1/carousel-templates/preview", data);
|
|
384
382
|
}
|
|
383
|
+
// Canva
|
|
384
|
+
async getCanvaStatus() {
|
|
385
|
+
return this.request("GET", "/api/v1/canva/status");
|
|
386
|
+
}
|
|
387
|
+
async canvaUploadAsset(data) {
|
|
388
|
+
return this.request("POST", "/api/v1/canva/upload-asset", data);
|
|
389
|
+
}
|
|
390
|
+
async canvaCreateDesign(data) {
|
|
391
|
+
return this.request("POST", "/api/v1/canva/designs", data);
|
|
392
|
+
}
|
|
393
|
+
async canvaExportDesign(data) {
|
|
394
|
+
return this.request("POST", "/api/v1/canva/exports", data);
|
|
395
|
+
}
|
|
396
|
+
async canvaSearchDesigns(query) {
|
|
397
|
+
return this.request("GET", "/api/v1/canva/designs", void 0, { query });
|
|
398
|
+
}
|
|
399
|
+
async canvaGetDesign(designId) {
|
|
400
|
+
return this.request("GET", `/api/v1/canva/designs/${designId}`);
|
|
401
|
+
}
|
|
402
|
+
// SendGrid Newsletter (BuzzPoster-managed)
|
|
403
|
+
async sgListLists() {
|
|
404
|
+
return this.request("GET", "/api/v1/newsletter/lists");
|
|
405
|
+
}
|
|
406
|
+
async sgCreateList(data) {
|
|
407
|
+
return this.request("POST", "/api/v1/newsletter/lists", data);
|
|
408
|
+
}
|
|
409
|
+
async sgUpdateList(id, data) {
|
|
410
|
+
return this.request("PUT", `/api/v1/newsletter/lists/${id}`, data);
|
|
411
|
+
}
|
|
412
|
+
async sgDeleteList(id) {
|
|
413
|
+
return this.request("DELETE", `/api/v1/newsletter/lists/${id}`);
|
|
414
|
+
}
|
|
415
|
+
async sgListSubscribers(params) {
|
|
416
|
+
return this.request("GET", "/api/v1/newsletter/subscribers", void 0, params);
|
|
417
|
+
}
|
|
418
|
+
async sgAddSubscriber(data) {
|
|
419
|
+
return this.request("POST", "/api/v1/newsletter/subscribers", data);
|
|
420
|
+
}
|
|
421
|
+
async sgGetSubscriber(id) {
|
|
422
|
+
return this.request("GET", `/api/v1/newsletter/subscribers/${id}`);
|
|
423
|
+
}
|
|
424
|
+
async sgUpdateSubscriber(id, data) {
|
|
425
|
+
return this.request("PUT", `/api/v1/newsletter/subscribers/${id}`, data);
|
|
426
|
+
}
|
|
427
|
+
async sgRemoveSubscriber(id) {
|
|
428
|
+
return this.request("DELETE", `/api/v1/newsletter/subscribers/${id}`);
|
|
429
|
+
}
|
|
430
|
+
async sgBulkImportSubscribers(data) {
|
|
431
|
+
return this.request("POST", "/api/v1/newsletter/subscribers/bulk", data);
|
|
432
|
+
}
|
|
433
|
+
async sgListSends(params) {
|
|
434
|
+
return this.request("GET", "/api/v1/newsletter/sends", void 0, params);
|
|
435
|
+
}
|
|
436
|
+
async sgGetSend(id) {
|
|
437
|
+
return this.request("GET", `/api/v1/newsletter/sends/${id}`);
|
|
438
|
+
}
|
|
439
|
+
async sgCreateSend(data) {
|
|
440
|
+
return this.request("POST", "/api/v1/newsletter/sends", data);
|
|
441
|
+
}
|
|
442
|
+
async sgUpdateSend(id, data) {
|
|
443
|
+
return this.request("PUT", `/api/v1/newsletter/sends/${id}`, data);
|
|
444
|
+
}
|
|
445
|
+
async sgDeleteSend(id) {
|
|
446
|
+
return this.request("DELETE", `/api/v1/newsletter/sends/${id}`);
|
|
447
|
+
}
|
|
448
|
+
async sgSendNewsletter(id) {
|
|
449
|
+
return this.request("POST", `/api/v1/newsletter/sends/${id}/send`);
|
|
450
|
+
}
|
|
451
|
+
async sgTestSend(id, data) {
|
|
452
|
+
return this.request("POST", `/api/v1/newsletter/sends/${id}/test`, data);
|
|
453
|
+
}
|
|
454
|
+
async sgScheduleSend(id, data) {
|
|
455
|
+
return this.request("POST", `/api/v1/newsletter/sends/${id}/schedule`, data);
|
|
456
|
+
}
|
|
457
|
+
async sgCancelSend(id) {
|
|
458
|
+
return this.request("POST", `/api/v1/newsletter/sends/${id}/cancel`);
|
|
459
|
+
}
|
|
460
|
+
async sgAuthenticateDomain(domain) {
|
|
461
|
+
return this.request("POST", "/api/v1/newsletter/domains/authenticate", { domain });
|
|
462
|
+
}
|
|
463
|
+
async sgValidateDomain(domainId) {
|
|
464
|
+
return this.request("POST", `/api/v1/newsletter/domains/${domainId}/validate`);
|
|
465
|
+
}
|
|
466
|
+
async sgListDomains() {
|
|
467
|
+
return this.request("GET", "/api/v1/newsletter/domains");
|
|
468
|
+
}
|
|
385
469
|
};
|
|
386
470
|
|
|
387
471
|
// src/tools/posts.ts
|
|
@@ -1164,72 +1248,7 @@ This will post a public reply to the review. Call this tool again with confirmed
|
|
|
1164
1248
|
|
|
1165
1249
|
// src/tools/media.ts
|
|
1166
1250
|
import { z as z4 } from "zod";
|
|
1167
|
-
import { readFile } from "fs/promises";
|
|
1168
1251
|
function registerMediaTools(server2, client2) {
|
|
1169
|
-
server2.tool(
|
|
1170
|
-
"upload_media",
|
|
1171
|
-
"Upload an image or video file from a local file path. Returns a CDN URL that can be used in posts.",
|
|
1172
|
-
{
|
|
1173
|
-
file_path: z4.string().describe("Absolute path to the file on the local filesystem")
|
|
1174
|
-
},
|
|
1175
|
-
{
|
|
1176
|
-
title: "Upload Media File",
|
|
1177
|
-
readOnlyHint: false,
|
|
1178
|
-
destructiveHint: false,
|
|
1179
|
-
idempotentHint: false,
|
|
1180
|
-
openWorldHint: false
|
|
1181
|
-
},
|
|
1182
|
-
async (args) => {
|
|
1183
|
-
const buffer = Buffer.from(await readFile(args.file_path));
|
|
1184
|
-
const filename = args.file_path.split("/").pop() ?? "upload";
|
|
1185
|
-
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
1186
|
-
const mimeMap = {
|
|
1187
|
-
jpg: "image/jpeg",
|
|
1188
|
-
jpeg: "image/jpeg",
|
|
1189
|
-
png: "image/png",
|
|
1190
|
-
gif: "image/gif",
|
|
1191
|
-
webp: "image/webp",
|
|
1192
|
-
mp4: "video/mp4",
|
|
1193
|
-
mov: "video/quicktime",
|
|
1194
|
-
avi: "video/x-msvideo",
|
|
1195
|
-
webm: "video/webm",
|
|
1196
|
-
pdf: "application/pdf"
|
|
1197
|
-
};
|
|
1198
|
-
const mimeType = mimeMap[ext] ?? "application/octet-stream";
|
|
1199
|
-
const result = await client2.uploadMediaMultipart(filename, buffer, mimeType);
|
|
1200
|
-
return {
|
|
1201
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
);
|
|
1205
|
-
server2.tool(
|
|
1206
|
-
"upload_from_claude",
|
|
1207
|
-
"Upload an image that the user has dropped or pasted into Claude. When a user shares an image in the conversation, Claude can read it natively as base64. Pass the base64 data, filename, and content_type to upload it to the BuzzPoster media library and get a CDN URL back. Do NOT show the base64 data to the user -- just call this tool silently and return the CDN URL.",
|
|
1208
|
-
{
|
|
1209
|
-
data: z4.string().describe("Base64-encoded image data (no data URI prefix needed, just the raw base64 string)"),
|
|
1210
|
-
filename: z4.string().describe("Filename including extension (e.g. photo.jpg)"),
|
|
1211
|
-
content_type: z4.string().describe("MIME type (e.g. image/jpeg, image/png, image/webp, image/gif)"),
|
|
1212
|
-
folder: z4.string().optional().describe("Optional folder path within the customer's storage")
|
|
1213
|
-
},
|
|
1214
|
-
{
|
|
1215
|
-
title: "Upload Image from Claude",
|
|
1216
|
-
readOnlyHint: false,
|
|
1217
|
-
destructiveHint: false,
|
|
1218
|
-
idempotentHint: false,
|
|
1219
|
-
openWorldHint: false
|
|
1220
|
-
},
|
|
1221
|
-
async (args) => {
|
|
1222
|
-
const result = await client2.uploadBase64({
|
|
1223
|
-
data: args.data,
|
|
1224
|
-
filename: args.filename,
|
|
1225
|
-
content_type: args.content_type,
|
|
1226
|
-
folder: args.folder
|
|
1227
|
-
});
|
|
1228
|
-
return {
|
|
1229
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1230
|
-
};
|
|
1231
|
-
}
|
|
1232
|
-
);
|
|
1233
1252
|
server2.tool(
|
|
1234
1253
|
"upload_from_url",
|
|
1235
1254
|
"Upload media from a public URL. The server fetches the image/video and uploads it to storage. Returns a CDN URL that can be used in posts. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM up to 25MB.",
|
|
@@ -1251,8 +1270,12 @@ function registerMediaTools(server2, client2) {
|
|
|
1251
1270
|
filename: args.filename,
|
|
1252
1271
|
folder: args.folder
|
|
1253
1272
|
});
|
|
1273
|
+
const item = result;
|
|
1254
1274
|
return {
|
|
1255
|
-
content: [
|
|
1275
|
+
content: [
|
|
1276
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
1277
|
+
...item.type === "image" && item.url && item.mimeType && !item.mimeType.startsWith("video/") ? [{ type: "image", url: item.url, mimeType: item.mimeType }] : []
|
|
1278
|
+
]
|
|
1256
1279
|
};
|
|
1257
1280
|
}
|
|
1258
1281
|
);
|
|
@@ -1293,8 +1316,13 @@ function registerMediaTools(server2, client2) {
|
|
|
1293
1316
|
},
|
|
1294
1317
|
async () => {
|
|
1295
1318
|
const result = await client2.listMedia();
|
|
1319
|
+
const media = Array.isArray(result) ? result : result?.media ?? [];
|
|
1320
|
+
const imageBlocks = media.filter((m) => m.type === "image" && m.url && m.mimeType && !m.mimeType.startsWith("video/")).map((m) => ({ type: "image", url: m.url, mimeType: m.mimeType }));
|
|
1296
1321
|
return {
|
|
1297
|
-
content: [
|
|
1322
|
+
content: [
|
|
1323
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
1324
|
+
...imageBlocks
|
|
1325
|
+
]
|
|
1298
1326
|
};
|
|
1299
1327
|
}
|
|
1300
1328
|
);
|
|
@@ -1409,7 +1437,11 @@ function registerNewsletterTools(server2, client2, options = {}) {
|
|
|
1409
1437
|
previewText: args.preview_text
|
|
1410
1438
|
});
|
|
1411
1439
|
return {
|
|
1412
|
-
content: [
|
|
1440
|
+
content: [
|
|
1441
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
1442
|
+
...args.content ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
|
|
1443
|
+
${args.content}` }] : []
|
|
1444
|
+
]
|
|
1413
1445
|
};
|
|
1414
1446
|
}
|
|
1415
1447
|
);
|
|
@@ -1436,7 +1468,11 @@ function registerNewsletterTools(server2, client2, options = {}) {
|
|
|
1436
1468
|
if (args.preview_text) data.previewText = args.preview_text;
|
|
1437
1469
|
const result = await client2.updateBroadcast(args.broadcast_id, data);
|
|
1438
1470
|
return {
|
|
1439
|
-
content: [
|
|
1471
|
+
content: [
|
|
1472
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
1473
|
+
...args.content ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
|
|
1474
|
+
${args.content}` }] : []
|
|
1475
|
+
]
|
|
1440
1476
|
};
|
|
1441
1477
|
}
|
|
1442
1478
|
);
|
|
@@ -1611,9 +1647,13 @@ Call this tool again with confirmed=true to schedule.`;
|
|
|
1611
1647
|
);
|
|
1612
1648
|
server2.tool(
|
|
1613
1649
|
"list_newsletters",
|
|
1614
|
-
"List all newsletters/broadcasts with their status.",
|
|
1650
|
+
"List all newsletters/broadcasts with their id, subject, status (draft, sent, or scheduled), createdAt, and sentAt. Supports filtering by status, date range, and subject keyword search.",
|
|
1615
1651
|
{
|
|
1616
|
-
page: z5.string().optional().describe("Page number for pagination")
|
|
1652
|
+
page: z5.string().optional().describe("Page number for pagination"),
|
|
1653
|
+
status: z5.enum(["draft", "sent", "scheduled"]).optional().describe("Filter by status: draft, sent, or scheduled"),
|
|
1654
|
+
from_date: z5.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
|
|
1655
|
+
to_date: z5.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
|
|
1656
|
+
subject: z5.string().optional().describe("Keyword to search in subject lines (case-insensitive)")
|
|
1617
1657
|
},
|
|
1618
1658
|
{
|
|
1619
1659
|
title: "List Newsletters",
|
|
@@ -1625,9 +1665,31 @@ Call this tool again with confirmed=true to schedule.`;
|
|
|
1625
1665
|
async (args) => {
|
|
1626
1666
|
const params = {};
|
|
1627
1667
|
if (args.page) params.page = args.page;
|
|
1668
|
+
if (args.status) params.status = args.status;
|
|
1669
|
+
if (args.from_date) params.fromDate = args.from_date;
|
|
1670
|
+
if (args.to_date) params.toDate = args.to_date;
|
|
1671
|
+
if (args.subject) params.subject = args.subject;
|
|
1628
1672
|
const result = await client2.listBroadcasts(params);
|
|
1673
|
+
const broadcasts = result?.broadcasts ?? [];
|
|
1674
|
+
if (broadcasts.length === 0) {
|
|
1675
|
+
return {
|
|
1676
|
+
content: [{ type: "text", text: "No newsletters found matching your filters." }]
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
let text = `## Newsletters (${broadcasts.length}`;
|
|
1680
|
+
if (result?.totalCount != null) text += ` of ${result.totalCount}`;
|
|
1681
|
+
text += ")\n\n";
|
|
1682
|
+
for (const b of broadcasts) {
|
|
1683
|
+
const status = (b.status ?? "unknown").toUpperCase();
|
|
1684
|
+
const date = b.sentAt ? new Date(b.sentAt).toLocaleString() : b.createdAt ? new Date(b.createdAt).toLocaleString() : "";
|
|
1685
|
+
text += `- **[${status}]** "${b.subject ?? "(no subject)"}"
|
|
1686
|
+
`;
|
|
1687
|
+
text += ` ID: ${b.id}`;
|
|
1688
|
+
if (date) text += ` | ${b.sentAt ? "Sent" : "Created"}: ${date}`;
|
|
1689
|
+
text += "\n";
|
|
1690
|
+
}
|
|
1629
1691
|
return {
|
|
1630
|
-
content: [{ type: "text", text
|
|
1692
|
+
content: [{ type: "text", text }]
|
|
1631
1693
|
};
|
|
1632
1694
|
}
|
|
1633
1695
|
);
|
|
@@ -2341,6 +2403,89 @@ USAGE GUIDELINES:
|
|
|
2341
2403
|
};
|
|
2342
2404
|
}
|
|
2343
2405
|
);
|
|
2406
|
+
server2.tool(
|
|
2407
|
+
"delete_audience",
|
|
2408
|
+
`Permanently delete a target audience profile by ID. This action cannot be undone.
|
|
2409
|
+
|
|
2410
|
+
REQUIRED WORKFLOW:
|
|
2411
|
+
1. ALWAYS call with confirmed=false first to preview which audience will be deleted
|
|
2412
|
+
2. Show the user the audience name and details
|
|
2413
|
+
3. Only call again with confirmed=true after explicit user approval
|
|
2414
|
+
|
|
2415
|
+
SAFETY: Cannot delete the default audience if it is the only audience profile. The customer must create another audience first.`,
|
|
2416
|
+
{
|
|
2417
|
+
audience_id: z9.string().describe("The ID of the audience profile to delete"),
|
|
2418
|
+
confirmed: z9.boolean().default(false).describe(
|
|
2419
|
+
"Set to true to confirm deletion after previewing. If false or missing, returns a preview for user approval."
|
|
2420
|
+
)
|
|
2421
|
+
},
|
|
2422
|
+
{
|
|
2423
|
+
title: "Delete Audience Profile",
|
|
2424
|
+
readOnlyHint: false,
|
|
2425
|
+
destructiveHint: true,
|
|
2426
|
+
idempotentHint: true,
|
|
2427
|
+
openWorldHint: false
|
|
2428
|
+
},
|
|
2429
|
+
async (args) => {
|
|
2430
|
+
let audience;
|
|
2431
|
+
try {
|
|
2432
|
+
audience = await client2.getAudience(args.audience_id);
|
|
2433
|
+
} catch (error) {
|
|
2434
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2435
|
+
if (message.includes("404") || message.includes("not found")) {
|
|
2436
|
+
return {
|
|
2437
|
+
content: [
|
|
2438
|
+
{
|
|
2439
|
+
type: "text",
|
|
2440
|
+
text: `Audience with ID ${args.audience_id} not found.`
|
|
2441
|
+
}
|
|
2442
|
+
],
|
|
2443
|
+
isError: true
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
throw error;
|
|
2447
|
+
}
|
|
2448
|
+
if (audience.isDefault) {
|
|
2449
|
+
const allAudiences = await client2.listAudiences();
|
|
2450
|
+
if (allAudiences.length <= 1) {
|
|
2451
|
+
return {
|
|
2452
|
+
content: [
|
|
2453
|
+
{
|
|
2454
|
+
type: "text",
|
|
2455
|
+
text: `Cannot delete "${audience.name}" because it is the only audience profile. Create another audience before deleting this one.`
|
|
2456
|
+
}
|
|
2457
|
+
],
|
|
2458
|
+
isError: true
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
if (args.confirmed !== true) {
|
|
2463
|
+
const lines = [];
|
|
2464
|
+
lines.push("## Delete Audience Confirmation");
|
|
2465
|
+
lines.push("");
|
|
2466
|
+
lines.push(`**Name:** ${audience.name}`);
|
|
2467
|
+
lines.push(`**ID:** ${audience.id}`);
|
|
2468
|
+
if (audience.isDefault) lines.push("**Default:** Yes");
|
|
2469
|
+
if (audience.description) lines.push(`**Description:** ${audience.description}`);
|
|
2470
|
+
lines.push("");
|
|
2471
|
+
lines.push(
|
|
2472
|
+
"**This action is permanent and cannot be undone.** Call this tool again with confirmed=true to proceed."
|
|
2473
|
+
);
|
|
2474
|
+
return {
|
|
2475
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
await client2.deleteAudience(args.audience_id);
|
|
2479
|
+
return {
|
|
2480
|
+
content: [
|
|
2481
|
+
{
|
|
2482
|
+
type: "text",
|
|
2483
|
+
text: `Audience "${audience.name}" (ID: ${audience.id}) has been permanently deleted.`
|
|
2484
|
+
}
|
|
2485
|
+
]
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
);
|
|
2344
2489
|
}
|
|
2345
2490
|
|
|
2346
2491
|
// src/tools/newsletter-template.ts
|
|
@@ -2479,6 +2624,15 @@ function registerNewsletterTemplateTools(server2, client2) {
|
|
|
2479
2624
|
);
|
|
2480
2625
|
lines.push("");
|
|
2481
2626
|
}
|
|
2627
|
+
if (template.htmlContent) {
|
|
2628
|
+
lines.push("### HTML Template:");
|
|
2629
|
+
lines.push("The following HTML skeleton contains {{placeholder}} variables. Fill in the placeholders with real content when generating the newsletter.");
|
|
2630
|
+
lines.push("");
|
|
2631
|
+
lines.push("```html");
|
|
2632
|
+
lines.push(template.htmlContent);
|
|
2633
|
+
lines.push("```");
|
|
2634
|
+
lines.push("");
|
|
2635
|
+
}
|
|
2482
2636
|
if (template.rssFeedUrls && template.rssFeedUrls.length > 0) {
|
|
2483
2637
|
lines.push("### Linked RSS Feeds:");
|
|
2484
2638
|
for (const url of template.rssFeedUrls) {
|
|
@@ -2569,8 +2723,13 @@ function registerNewsletterTemplateTools(server2, client2) {
|
|
|
2569
2723
|
);
|
|
2570
2724
|
lines.push("");
|
|
2571
2725
|
}
|
|
2726
|
+
const mostRecent = newsletters[0];
|
|
2572
2727
|
return {
|
|
2573
|
-
content: [
|
|
2728
|
+
content: [
|
|
2729
|
+
{ type: "text", text: lines.join("\n") },
|
|
2730
|
+
...mostRecent?.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
|
|
2731
|
+
${mostRecent.contentHtml}` }] : []
|
|
2732
|
+
]
|
|
2574
2733
|
};
|
|
2575
2734
|
} catch (error) {
|
|
2576
2735
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -2604,7 +2763,11 @@ function registerNewsletterTemplateTools(server2, client2) {
|
|
|
2604
2763
|
lines.push("### Full HTML Content:");
|
|
2605
2764
|
lines.push(newsletter.contentHtml);
|
|
2606
2765
|
return {
|
|
2607
|
-
content: [
|
|
2766
|
+
content: [
|
|
2767
|
+
{ type: "text", text: lines.join("\n") },
|
|
2768
|
+
...newsletter.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
|
|
2769
|
+
${newsletter.contentHtml}` }] : []
|
|
2770
|
+
]
|
|
2608
2771
|
};
|
|
2609
2772
|
} catch (error) {
|
|
2610
2773
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -2654,7 +2817,9 @@ function registerNewsletterTemplateTools(server2, client2) {
|
|
|
2654
2817
|
{
|
|
2655
2818
|
type: "text",
|
|
2656
2819
|
text: `Newsletter "${subject}" has been saved to the archive successfully.`
|
|
2657
|
-
}
|
|
2820
|
+
},
|
|
2821
|
+
...contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
|
|
2822
|
+
${contentHtml}` }] : []
|
|
2658
2823
|
]
|
|
2659
2824
|
};
|
|
2660
2825
|
} catch (error) {
|
|
@@ -2663,72 +2828,276 @@ function registerNewsletterTemplateTools(server2, client2) {
|
|
|
2663
2828
|
}
|
|
2664
2829
|
}
|
|
2665
2830
|
);
|
|
2666
|
-
}
|
|
2667
|
-
|
|
2668
|
-
// src/tools/calendar.ts
|
|
2669
|
-
import { z as z11 } from "zod";
|
|
2670
|
-
function registerCalendarTools(server2, client2) {
|
|
2671
2831
|
server2.tool(
|
|
2672
|
-
"
|
|
2673
|
-
"
|
|
2832
|
+
"save_newsletter_template",
|
|
2833
|
+
"Save or update a newsletter HTML template. The template contains the HTML skeleton with {{placeholder}} variables that Claude fills in when generating newsletters. If template_id is provided, updates the existing template. Otherwise creates a new one.",
|
|
2674
2834
|
{
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2835
|
+
template_id: z10.string().optional().describe(
|
|
2836
|
+
"If provided, updates this existing template. Otherwise creates a new one."
|
|
2837
|
+
),
|
|
2838
|
+
name: z10.string().min(1).max(255).describe("Template name, e.g. 'BOQtoday Daily'"),
|
|
2839
|
+
description: z10.string().max(2e3).optional().describe(
|
|
2840
|
+
"What this template is for, e.g. 'Local news digest with 6 stories, events, pet of the week'"
|
|
2841
|
+
),
|
|
2842
|
+
html_content: z10.string().min(1).describe(
|
|
2843
|
+
"Full HTML template with {{placeholder}} variables. This is the email body HTML with inline styles."
|
|
2844
|
+
),
|
|
2845
|
+
sections: z10.array(
|
|
2846
|
+
z10.object({
|
|
2847
|
+
name: z10.string().describe("Section identifier, e.g. 'intro'"),
|
|
2848
|
+
label: z10.string().describe("Display label, e.g. 'Intro/Greeting'"),
|
|
2849
|
+
required: z10.boolean().optional().describe("Whether this section is required"),
|
|
2850
|
+
item_count: z10.number().optional().describe("Number of items in this section, if applicable")
|
|
2851
|
+
})
|
|
2852
|
+
).optional().describe("Array of section metadata describing the template structure"),
|
|
2853
|
+
style_config: z10.object({
|
|
2854
|
+
primary_color: z10.string().optional(),
|
|
2855
|
+
accent_color: z10.string().optional(),
|
|
2856
|
+
link_color: z10.string().optional(),
|
|
2857
|
+
background_color: z10.string().optional(),
|
|
2858
|
+
font_family: z10.string().optional(),
|
|
2859
|
+
max_width: z10.string().optional()
|
|
2860
|
+
}).optional().describe("Colors, fonts, and brand configuration"),
|
|
2861
|
+
is_default: z10.boolean().optional().describe(
|
|
2862
|
+
"Set as the default template for this customer. Only one default per customer."
|
|
2863
|
+
)
|
|
2679
2864
|
},
|
|
2680
2865
|
{
|
|
2681
|
-
title: "
|
|
2682
|
-
readOnlyHint:
|
|
2866
|
+
title: "Save Newsletter Template",
|
|
2867
|
+
readOnlyHint: false,
|
|
2683
2868
|
destructiveHint: false,
|
|
2684
|
-
idempotentHint:
|
|
2869
|
+
idempotentHint: false,
|
|
2685
2870
|
openWorldHint: false
|
|
2686
2871
|
},
|
|
2687
|
-
async (
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
}
|
|
2872
|
+
async ({
|
|
2873
|
+
template_id,
|
|
2874
|
+
name,
|
|
2875
|
+
description,
|
|
2876
|
+
html_content,
|
|
2877
|
+
sections,
|
|
2878
|
+
style_config,
|
|
2879
|
+
is_default
|
|
2880
|
+
}) => {
|
|
2881
|
+
try {
|
|
2882
|
+
const data = {
|
|
2883
|
+
name,
|
|
2884
|
+
htmlContent: html_content
|
|
2885
|
+
};
|
|
2886
|
+
if (description !== void 0) data.description = description;
|
|
2887
|
+
if (sections !== void 0) {
|
|
2888
|
+
data.sections = sections.map((s) => ({
|
|
2889
|
+
id: s.name,
|
|
2890
|
+
type: "custom",
|
|
2891
|
+
label: s.label,
|
|
2892
|
+
required: s.required ?? true,
|
|
2893
|
+
count: s.item_count
|
|
2894
|
+
}));
|
|
2710
2895
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
text += `\u2705 ${date} -- Newsletter: "${item.content_preview}"
|
|
2721
|
-
`;
|
|
2722
|
-
} else {
|
|
2723
|
-
const platforms = (item.platforms ?? []).join(", ");
|
|
2724
|
-
text += `\u2705 ${date} -- ${platforms} -- "${item.content_preview}"
|
|
2725
|
-
`;
|
|
2726
|
-
}
|
|
2896
|
+
if (style_config !== void 0) {
|
|
2897
|
+
data.style = {
|
|
2898
|
+
primary_color: style_config.primary_color,
|
|
2899
|
+
accent_color: style_config.accent_color,
|
|
2900
|
+
link_color: style_config.link_color,
|
|
2901
|
+
background_color: style_config.background_color,
|
|
2902
|
+
font_family: style_config.font_family,
|
|
2903
|
+
content_width: style_config.max_width
|
|
2904
|
+
};
|
|
2727
2905
|
}
|
|
2728
|
-
|
|
2906
|
+
if (is_default !== void 0) data.isDefault = is_default;
|
|
2907
|
+
let result;
|
|
2908
|
+
if (template_id) {
|
|
2909
|
+
result = await client2.updateTemplate(
|
|
2910
|
+
template_id,
|
|
2911
|
+
data
|
|
2912
|
+
);
|
|
2913
|
+
} else {
|
|
2914
|
+
result = await client2.createTemplate(data);
|
|
2915
|
+
}
|
|
2916
|
+
const action = template_id ? "updated" : "created";
|
|
2917
|
+
return {
|
|
2918
|
+
content: [
|
|
2919
|
+
{
|
|
2920
|
+
type: "text",
|
|
2921
|
+
text: `Newsletter template "${result.name}" ${action} successfully (ID: ${result.id}).${result.isDefault ? " Set as default." : ""}`
|
|
2922
|
+
}
|
|
2923
|
+
]
|
|
2924
|
+
};
|
|
2925
|
+
} catch (error) {
|
|
2926
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2927
|
+
throw new Error(`Failed to save newsletter template: ${message}`);
|
|
2729
2928
|
}
|
|
2929
|
+
}
|
|
2930
|
+
);
|
|
2931
|
+
server2.tool(
|
|
2932
|
+
"list_newsletter_templates",
|
|
2933
|
+
"List all saved newsletter templates for this customer. Returns template names, descriptions, whether they have HTML content, and which one is the default.",
|
|
2934
|
+
{},
|
|
2935
|
+
{
|
|
2936
|
+
title: "List Newsletter Templates",
|
|
2937
|
+
readOnlyHint: true,
|
|
2938
|
+
destructiveHint: false,
|
|
2939
|
+
idempotentHint: true,
|
|
2940
|
+
openWorldHint: false
|
|
2941
|
+
},
|
|
2942
|
+
async () => {
|
|
2730
2943
|
try {
|
|
2731
|
-
const
|
|
2944
|
+
const templates = await client2.listTemplates();
|
|
2945
|
+
if (!templates || templates.length === 0) {
|
|
2946
|
+
return {
|
|
2947
|
+
content: [
|
|
2948
|
+
{
|
|
2949
|
+
type: "text",
|
|
2950
|
+
text: "No newsletter templates found. Use save_newsletter_template to create one."
|
|
2951
|
+
}
|
|
2952
|
+
]
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
const lines = [];
|
|
2956
|
+
lines.push(`## Newsletter Templates (${templates.length})`);
|
|
2957
|
+
lines.push("");
|
|
2958
|
+
for (const t of templates) {
|
|
2959
|
+
const badges = [];
|
|
2960
|
+
if (t.isDefault) badges.push("DEFAULT");
|
|
2961
|
+
if (t.htmlContent) badges.push("HAS HTML");
|
|
2962
|
+
const badgeStr = badges.length > 0 ? ` [${badges.join(", ")}]` : "";
|
|
2963
|
+
lines.push(`**${t.name}** (ID: ${t.id})${badgeStr}`);
|
|
2964
|
+
if (t.description) lines.push(` ${t.description}`);
|
|
2965
|
+
if (t.sections && t.sections.length > 0) {
|
|
2966
|
+
lines.push(
|
|
2967
|
+
` Sections: ${t.sections.map((s) => s.label).join(", ")}`
|
|
2968
|
+
);
|
|
2969
|
+
}
|
|
2970
|
+
lines.push("");
|
|
2971
|
+
}
|
|
2972
|
+
return {
|
|
2973
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
2974
|
+
};
|
|
2975
|
+
} catch (error) {
|
|
2976
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2977
|
+
throw new Error(`Failed to list newsletter templates: ${message}`);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
);
|
|
2981
|
+
server2.tool(
|
|
2982
|
+
"delete_newsletter_template",
|
|
2983
|
+
"Delete a newsletter template. Requires confirmation -- set confirmed=true to proceed with deletion.",
|
|
2984
|
+
{
|
|
2985
|
+
template_id: z10.string().describe("The ID of the newsletter template to delete."),
|
|
2986
|
+
confirmed: z10.boolean().default(false).describe(
|
|
2987
|
+
"Must be true to actually delete. If false, returns a confirmation prompt."
|
|
2988
|
+
)
|
|
2989
|
+
},
|
|
2990
|
+
{
|
|
2991
|
+
title: "Delete Newsletter Template",
|
|
2992
|
+
readOnlyHint: false,
|
|
2993
|
+
destructiveHint: true,
|
|
2994
|
+
idempotentHint: false,
|
|
2995
|
+
openWorldHint: false
|
|
2996
|
+
},
|
|
2997
|
+
async ({ template_id, confirmed }) => {
|
|
2998
|
+
try {
|
|
2999
|
+
if (!confirmed) {
|
|
3000
|
+
const template = await client2.getTemplate(template_id);
|
|
3001
|
+
return {
|
|
3002
|
+
content: [
|
|
3003
|
+
{
|
|
3004
|
+
type: "text",
|
|
3005
|
+
text: `Are you sure you want to delete the template "${template.name}" (ID: ${template.id})? This action cannot be undone. Call delete_newsletter_template again with confirmed=true to proceed.`
|
|
3006
|
+
}
|
|
3007
|
+
]
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
await client2.deleteTemplate(template_id);
|
|
3011
|
+
return {
|
|
3012
|
+
content: [
|
|
3013
|
+
{
|
|
3014
|
+
type: "text",
|
|
3015
|
+
text: `Newsletter template (ID: ${template_id}) has been deleted.`
|
|
3016
|
+
}
|
|
3017
|
+
]
|
|
3018
|
+
};
|
|
3019
|
+
} catch (error) {
|
|
3020
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3021
|
+
if (message.includes("404")) {
|
|
3022
|
+
return {
|
|
3023
|
+
content: [
|
|
3024
|
+
{
|
|
3025
|
+
type: "text",
|
|
3026
|
+
text: "Template not found. It may have already been deleted."
|
|
3027
|
+
}
|
|
3028
|
+
]
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
throw new Error(`Failed to delete newsletter template: ${message}`);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
);
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// src/tools/calendar.ts
|
|
3038
|
+
import { z as z11 } from "zod";
|
|
3039
|
+
function registerCalendarTools(server2, client2) {
|
|
3040
|
+
server2.tool(
|
|
3041
|
+
"get_calendar",
|
|
3042
|
+
"View the customer's content calendar -- all scheduled, published, and draft posts across social media and newsletters. Use this to check what's already scheduled before creating new content, avoid posting conflicts, and maintain a balanced content mix.",
|
|
3043
|
+
{
|
|
3044
|
+
from: z11.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
|
|
3045
|
+
to: z11.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
|
|
3046
|
+
status: z11.string().optional().describe("Filter by status: scheduled, published, draft, failed"),
|
|
3047
|
+
type: z11.string().optional().describe("Filter by type: social_post or newsletter")
|
|
3048
|
+
},
|
|
3049
|
+
{
|
|
3050
|
+
title: "Get Content Calendar",
|
|
3051
|
+
readOnlyHint: true,
|
|
3052
|
+
destructiveHint: false,
|
|
3053
|
+
idempotentHint: true,
|
|
3054
|
+
openWorldHint: false
|
|
3055
|
+
},
|
|
3056
|
+
async (args) => {
|
|
3057
|
+
const params = {};
|
|
3058
|
+
if (args.from) params.from = args.from;
|
|
3059
|
+
if (args.to) params.to = args.to;
|
|
3060
|
+
if (args.status) params.status = args.status;
|
|
3061
|
+
if (args.type) params.type = args.type;
|
|
3062
|
+
const data = await client2.getCalendar(params);
|
|
3063
|
+
const items = data?.items ?? [];
|
|
3064
|
+
const scheduled = items.filter((i) => i.status === "scheduled");
|
|
3065
|
+
const published = items.filter((i) => i.status === "published");
|
|
3066
|
+
let text = "## Content Calendar\n\n";
|
|
3067
|
+
if (scheduled.length > 0) {
|
|
3068
|
+
text += "### Scheduled\n";
|
|
3069
|
+
for (const item of scheduled) {
|
|
3070
|
+
const date = item.scheduled_for ? new Date(item.scheduled_for).toLocaleString() : "TBD";
|
|
3071
|
+
if (item.type === "newsletter") {
|
|
3072
|
+
text += `\u{1F4E7} ${date} -- Newsletter: "${item.content_preview}"
|
|
3073
|
+
`;
|
|
3074
|
+
} else {
|
|
3075
|
+
const platforms = (item.platforms ?? []).join(", ");
|
|
3076
|
+
text += `\u{1F4C5} ${date} -- ${platforms} -- "${item.content_preview}"
|
|
3077
|
+
`;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
text += "\n";
|
|
3081
|
+
} else {
|
|
3082
|
+
text += "### Scheduled\nNo scheduled content.\n\n";
|
|
3083
|
+
}
|
|
3084
|
+
if (published.length > 0) {
|
|
3085
|
+
text += "### Recently Published\n";
|
|
3086
|
+
for (const item of published.slice(0, 10)) {
|
|
3087
|
+
const date = item.published_at ? new Date(item.published_at).toLocaleString() : "";
|
|
3088
|
+
if (item.type === "newsletter") {
|
|
3089
|
+
text += `\u2705 ${date} -- Newsletter: "${item.content_preview}"
|
|
3090
|
+
`;
|
|
3091
|
+
} else {
|
|
3092
|
+
const platforms = (item.platforms ?? []).join(", ");
|
|
3093
|
+
text += `\u2705 ${date} -- ${platforms} -- "${item.content_preview}"
|
|
3094
|
+
`;
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
text += "\n";
|
|
3098
|
+
}
|
|
3099
|
+
try {
|
|
3100
|
+
const nextSlot = await client2.getNextSlot();
|
|
2732
3101
|
if (nextSlot?.scheduledFor) {
|
|
2733
3102
|
const slotDate = new Date(nextSlot.scheduledFor);
|
|
2734
3103
|
const dayNames = [
|
|
@@ -2787,7 +3156,8 @@ function registerCalendarTools(server2, client2) {
|
|
|
2787
3156
|
for (const slot of slots) {
|
|
2788
3157
|
const day = dayNames[slot.dayOfWeek] ?? `Day ${slot.dayOfWeek}`;
|
|
2789
3158
|
const platforms = (slot.platforms ?? []).map((p) => `[${p}]`).join(" ");
|
|
2790
|
-
|
|
3159
|
+
const slotId = slot.id ?? slot._id ?? `${slot.dayOfWeek}_${slot.time}`;
|
|
3160
|
+
text += `${day}: ${slot.time} ${platforms} (id: ${slotId})
|
|
2791
3161
|
`;
|
|
2792
3162
|
}
|
|
2793
3163
|
}
|
|
@@ -2808,7 +3178,9 @@ Next slot: ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
|
|
|
2808
3178
|
);
|
|
2809
3179
|
server2.tool(
|
|
2810
3180
|
"schedule_to_queue",
|
|
2811
|
-
`
|
|
3181
|
+
`Before calling this tool, always check get_queue first. If the queue is inactive or has no time slots configured, warn the user and do not attempt to schedule.
|
|
3182
|
+
|
|
3183
|
+
Add a post to the customer's content queue for automatic scheduling.
|
|
2812
3184
|
|
|
2813
3185
|
Always show the user a preview of the content before calling this tool. Call get_brand_voice and get_audience before composing if not already loaded this session. Never call with confirmed=true without explicit user approval.
|
|
2814
3186
|
|
|
@@ -4276,23 +4648,1258 @@ USAGE:
|
|
|
4276
4648
|
);
|
|
4277
4649
|
}
|
|
4278
4650
|
|
|
4279
|
-
// src/
|
|
4280
|
-
|
|
4281
|
-
var
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4651
|
+
// src/tools/queue.ts
|
|
4652
|
+
import { z as z17 } from "zod";
|
|
4653
|
+
var DAY_NAMES = [
|
|
4654
|
+
"sunday",
|
|
4655
|
+
"monday",
|
|
4656
|
+
"tuesday",
|
|
4657
|
+
"wednesday",
|
|
4658
|
+
"thursday",
|
|
4659
|
+
"friday",
|
|
4660
|
+
"saturday"
|
|
4661
|
+
];
|
|
4662
|
+
var DAY_ENUM = z17.enum([
|
|
4663
|
+
"monday",
|
|
4664
|
+
"tuesday",
|
|
4665
|
+
"wednesday",
|
|
4666
|
+
"thursday",
|
|
4667
|
+
"friday",
|
|
4668
|
+
"saturday",
|
|
4669
|
+
"sunday"
|
|
4670
|
+
]);
|
|
4671
|
+
function dayNameToNumber(name) {
|
|
4672
|
+
const idx = DAY_NAMES.indexOf(name.toLowerCase());
|
|
4673
|
+
return idx === -1 ? 0 : idx;
|
|
4674
|
+
}
|
|
4675
|
+
function dayNumberToName(num) {
|
|
4676
|
+
return (DAY_NAMES[num] ?? "sunday").charAt(0).toUpperCase() + (DAY_NAMES[num] ?? "sunday").slice(1);
|
|
4677
|
+
}
|
|
4678
|
+
function getSlotId(slot) {
|
|
4679
|
+
if (slot.id) return String(slot.id);
|
|
4680
|
+
if (slot._id) return String(slot._id);
|
|
4681
|
+
return `${slot.dayOfWeek}_${slot.time}`;
|
|
4682
|
+
}
|
|
4683
|
+
function formatSlot(slot) {
|
|
4684
|
+
const day = dayNumberToName(slot.dayOfWeek);
|
|
4685
|
+
const platforms = (slot.platforms ?? []).join(", ");
|
|
4686
|
+
return `${day} ${slot.time} [${platforms}]`;
|
|
4687
|
+
}
|
|
4688
|
+
function registerQueueTools(server2, client2) {
|
|
4689
|
+
server2.tool(
|
|
4690
|
+
"create_queue_slot",
|
|
4691
|
+
"Create a new recurring time slot in the posting queue. Each slot fires once per week at the specified day and time. Use get_queue first to see existing slots and avoid duplicates.",
|
|
4692
|
+
{
|
|
4693
|
+
day_of_week: DAY_ENUM.describe("Day of the week for this slot"),
|
|
4694
|
+
time: z17.string().regex(/^\d{2}:\d{2}$/, "Must be HH:MM format (24-hour)").describe("Time in HH:MM 24-hour format, e.g. '09:00' or '14:30'"),
|
|
4695
|
+
timezone: z17.string().default("UTC").describe("IANA timezone, e.g. 'America/New_York'. Defaults to UTC."),
|
|
4696
|
+
platforms: z17.array(z17.string()).min(1).describe('Platforms for this slot, e.g. ["twitter", "linkedin"]'),
|
|
4697
|
+
confirmed: z17.boolean().default(false).describe(
|
|
4698
|
+
"Set to true to confirm creation. If false, returns a preview."
|
|
4699
|
+
)
|
|
4700
|
+
},
|
|
4701
|
+
{
|
|
4702
|
+
title: "Create Queue Slot",
|
|
4703
|
+
readOnlyHint: false,
|
|
4704
|
+
destructiveHint: false,
|
|
4705
|
+
idempotentHint: false,
|
|
4706
|
+
openWorldHint: true
|
|
4707
|
+
},
|
|
4708
|
+
async (args) => {
|
|
4709
|
+
const dayNum = dayNameToNumber(args.day_of_week);
|
|
4710
|
+
if (args.confirmed !== true) {
|
|
4711
|
+
let text = "## Create Queue Slot Preview\n\n";
|
|
4712
|
+
text += `**Day:** ${dayNumberToName(dayNum)}
|
|
4713
|
+
`;
|
|
4714
|
+
text += `**Time:** ${args.time}
|
|
4715
|
+
`;
|
|
4716
|
+
text += `**Timezone:** ${args.timezone}
|
|
4717
|
+
`;
|
|
4718
|
+
text += `**Platforms:** ${args.platforms.join(", ")}
|
|
4719
|
+
|
|
4720
|
+
`;
|
|
4721
|
+
try {
|
|
4722
|
+
const data2 = await client2.getQueue();
|
|
4723
|
+
const slots = data2?.slots ?? [];
|
|
4724
|
+
if (slots.length > 0) {
|
|
4725
|
+
text += "### Existing slots\n";
|
|
4726
|
+
for (const slot of slots) {
|
|
4727
|
+
text += `- ${formatSlot(slot)}
|
|
4728
|
+
`;
|
|
4729
|
+
}
|
|
4730
|
+
text += "\n";
|
|
4731
|
+
}
|
|
4732
|
+
} catch {
|
|
4733
|
+
}
|
|
4734
|
+
text += "Call this tool again with confirmed=true to create the slot.";
|
|
4735
|
+
return { content: [{ type: "text", text }] };
|
|
4736
|
+
}
|
|
4737
|
+
const data = await client2.getQueue();
|
|
4738
|
+
const existingSlots = data?.slots ?? [];
|
|
4739
|
+
const duplicate = existingSlots.find(
|
|
4740
|
+
(s) => s.dayOfWeek === dayNum && s.time === args.time
|
|
4741
|
+
);
|
|
4742
|
+
if (duplicate) {
|
|
4743
|
+
return {
|
|
4744
|
+
content: [
|
|
4745
|
+
{
|
|
4746
|
+
type: "text",
|
|
4747
|
+
text: `A slot already exists for ${dayNumberToName(dayNum)} at ${args.time}. Use update_queue_slot to modify it.`
|
|
4748
|
+
}
|
|
4749
|
+
]
|
|
4750
|
+
};
|
|
4751
|
+
}
|
|
4752
|
+
const updatedSlots = [
|
|
4753
|
+
...existingSlots.map((s) => ({
|
|
4754
|
+
dayOfWeek: s.dayOfWeek,
|
|
4755
|
+
time: s.time,
|
|
4756
|
+
platforms: s.platforms
|
|
4757
|
+
})),
|
|
4758
|
+
{
|
|
4759
|
+
dayOfWeek: dayNum,
|
|
4760
|
+
time: args.time,
|
|
4761
|
+
platforms: args.platforms
|
|
4762
|
+
}
|
|
4763
|
+
];
|
|
4764
|
+
await client2.updateQueue({
|
|
4765
|
+
slots: updatedSlots,
|
|
4766
|
+
active: data?.active,
|
|
4767
|
+
timezone: args.timezone
|
|
4768
|
+
});
|
|
4769
|
+
return {
|
|
4770
|
+
content: [
|
|
4771
|
+
{
|
|
4772
|
+
type: "text",
|
|
4773
|
+
text: `Queue slot created: ${dayNumberToName(dayNum)} at ${args.time} for ${args.platforms.join(", ")}. The queue now has ${updatedSlots.length} slot(s).`
|
|
4774
|
+
}
|
|
4775
|
+
]
|
|
4776
|
+
};
|
|
4777
|
+
}
|
|
4285
4778
|
);
|
|
4286
|
-
|
|
4287
|
-
"
|
|
4779
|
+
server2.tool(
|
|
4780
|
+
"update_queue_slot",
|
|
4781
|
+
"Update an existing queue slot. Use get_queue first to see slot IDs. Only the fields you provide will be changed; omitted fields keep their current values.",
|
|
4782
|
+
{
|
|
4783
|
+
slot_id: z17.string().describe(
|
|
4784
|
+
"ID of the slot to update (shown by get_queue). Can be the Late API id or a dayOfWeek_time key like '1_09:00'."
|
|
4785
|
+
),
|
|
4786
|
+
day_of_week: DAY_ENUM.optional().describe("New day of the week"),
|
|
4787
|
+
time: z17.string().regex(/^\d{2}:\d{2}$/, "Must be HH:MM format (24-hour)").optional().describe("New time in HH:MM 24-hour format"),
|
|
4788
|
+
timezone: z17.string().optional().describe("New IANA timezone for the queue"),
|
|
4789
|
+
platforms: z17.array(z17.string()).min(1).optional().describe("New platforms list"),
|
|
4790
|
+
confirmed: z17.boolean().default(false).describe(
|
|
4791
|
+
"Set to true to confirm the update. If false, returns a preview."
|
|
4792
|
+
)
|
|
4793
|
+
},
|
|
4794
|
+
{
|
|
4795
|
+
title: "Update Queue Slot",
|
|
4796
|
+
readOnlyHint: false,
|
|
4797
|
+
destructiveHint: false,
|
|
4798
|
+
idempotentHint: true,
|
|
4799
|
+
openWorldHint: true
|
|
4800
|
+
},
|
|
4801
|
+
async (args) => {
|
|
4802
|
+
const data = await client2.getQueue();
|
|
4803
|
+
const slots = data?.slots ?? [];
|
|
4804
|
+
const slotIndex = slots.findIndex(
|
|
4805
|
+
(s) => getSlotId(s) === args.slot_id
|
|
4806
|
+
);
|
|
4807
|
+
if (slotIndex === -1) {
|
|
4808
|
+
const available = slots.map((s) => `${getSlotId(s)} (${formatSlot(s)})`).join("\n- ");
|
|
4809
|
+
return {
|
|
4810
|
+
content: [
|
|
4811
|
+
{
|
|
4812
|
+
type: "text",
|
|
4813
|
+
text: `Slot "${args.slot_id}" not found.
|
|
4814
|
+
|
|
4815
|
+
Available slots:
|
|
4816
|
+
- ${available || "(none)"}`
|
|
4817
|
+
}
|
|
4818
|
+
]
|
|
4819
|
+
};
|
|
4820
|
+
}
|
|
4821
|
+
const current = slots[slotIndex];
|
|
4822
|
+
const newDay = args.day_of_week !== void 0 ? dayNameToNumber(args.day_of_week) : current.dayOfWeek;
|
|
4823
|
+
const newTime = args.time ?? current.time;
|
|
4824
|
+
const newPlatforms = args.platforms ?? current.platforms;
|
|
4825
|
+
if (args.confirmed !== true) {
|
|
4826
|
+
let text = "## Update Queue Slot Preview\n\n";
|
|
4827
|
+
text += `**Current:** ${formatSlot(current)}
|
|
4828
|
+
`;
|
|
4829
|
+
text += `**Updated:** ${dayNumberToName(newDay)} ${newTime} [${(newPlatforms ?? []).join(", ")}]
|
|
4830
|
+
`;
|
|
4831
|
+
if (args.timezone) text += `**Timezone:** ${args.timezone}
|
|
4832
|
+
`;
|
|
4833
|
+
text += "\nCall this tool again with confirmed=true to apply the update.";
|
|
4834
|
+
return { content: [{ type: "text", text }] };
|
|
4835
|
+
}
|
|
4836
|
+
const updatedSlots = slots.map((s, i) => {
|
|
4837
|
+
if (i === slotIndex) {
|
|
4838
|
+
return {
|
|
4839
|
+
dayOfWeek: newDay,
|
|
4840
|
+
time: newTime,
|
|
4841
|
+
platforms: newPlatforms
|
|
4842
|
+
};
|
|
4843
|
+
}
|
|
4844
|
+
return {
|
|
4845
|
+
dayOfWeek: s.dayOfWeek,
|
|
4846
|
+
time: s.time,
|
|
4847
|
+
platforms: s.platforms
|
|
4848
|
+
};
|
|
4849
|
+
});
|
|
4850
|
+
await client2.updateQueue({
|
|
4851
|
+
slots: updatedSlots,
|
|
4852
|
+
active: data?.active,
|
|
4853
|
+
...args.timezone ? { timezone: args.timezone } : {}
|
|
4854
|
+
});
|
|
4855
|
+
return {
|
|
4856
|
+
content: [
|
|
4857
|
+
{
|
|
4858
|
+
type: "text",
|
|
4859
|
+
text: `Slot updated: ${dayNumberToName(newDay)} at ${newTime} for ${(newPlatforms ?? []).join(", ")}.`
|
|
4860
|
+
}
|
|
4861
|
+
]
|
|
4862
|
+
};
|
|
4863
|
+
}
|
|
4288
4864
|
);
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4865
|
+
server2.tool(
|
|
4866
|
+
"delete_queue_slot",
|
|
4867
|
+
"Delete a queue slot by ID. Use get_queue first to see slot IDs. This removes the recurring time slot -- any posts already scheduled for this slot are not affected.",
|
|
4868
|
+
{
|
|
4869
|
+
slot_id: z17.string().describe(
|
|
4870
|
+
"ID of the slot to delete (shown by get_queue)."
|
|
4871
|
+
),
|
|
4872
|
+
confirmed: z17.boolean().default(false).describe(
|
|
4873
|
+
"Set to true to confirm deletion. If false, shows slot details for review."
|
|
4874
|
+
)
|
|
4875
|
+
},
|
|
4876
|
+
{
|
|
4877
|
+
title: "Delete Queue Slot",
|
|
4878
|
+
readOnlyHint: false,
|
|
4879
|
+
destructiveHint: true,
|
|
4880
|
+
idempotentHint: true,
|
|
4881
|
+
openWorldHint: true
|
|
4882
|
+
},
|
|
4883
|
+
async (args) => {
|
|
4884
|
+
const data = await client2.getQueue();
|
|
4885
|
+
const slots = data?.slots ?? [];
|
|
4886
|
+
const slotIndex = slots.findIndex(
|
|
4887
|
+
(s) => getSlotId(s) === args.slot_id
|
|
4888
|
+
);
|
|
4889
|
+
if (slotIndex === -1) {
|
|
4890
|
+
const available = slots.map((s) => `${getSlotId(s)} (${formatSlot(s)})`).join("\n- ");
|
|
4891
|
+
return {
|
|
4892
|
+
content: [
|
|
4893
|
+
{
|
|
4894
|
+
type: "text",
|
|
4895
|
+
text: `Slot "${args.slot_id}" not found.
|
|
4896
|
+
|
|
4897
|
+
Available slots:
|
|
4898
|
+
- ${available || "(none)"}`
|
|
4899
|
+
}
|
|
4900
|
+
]
|
|
4901
|
+
};
|
|
4902
|
+
}
|
|
4903
|
+
const target = slots[slotIndex];
|
|
4904
|
+
if (args.confirmed !== true) {
|
|
4905
|
+
let text = "## Delete Queue Slot\n\n";
|
|
4906
|
+
text += `**Slot:** ${formatSlot(target)}
|
|
4907
|
+
`;
|
|
4908
|
+
text += `**ID:** ${getSlotId(target)}
|
|
4909
|
+
|
|
4910
|
+
`;
|
|
4911
|
+
text += `This will remove this recurring time slot. Posts already scheduled are not affected.
|
|
4912
|
+
|
|
4913
|
+
`;
|
|
4914
|
+
text += `The queue will have ${slots.length - 1} slot(s) remaining.
|
|
4915
|
+
|
|
4916
|
+
`;
|
|
4917
|
+
text += "Call this tool again with confirmed=true to delete.";
|
|
4918
|
+
return { content: [{ type: "text", text }] };
|
|
4919
|
+
}
|
|
4920
|
+
const updatedSlots = slots.filter((_, i) => i !== slotIndex).map((s) => ({
|
|
4921
|
+
dayOfWeek: s.dayOfWeek,
|
|
4922
|
+
time: s.time,
|
|
4923
|
+
platforms: s.platforms
|
|
4924
|
+
}));
|
|
4925
|
+
await client2.updateQueue({
|
|
4926
|
+
slots: updatedSlots,
|
|
4927
|
+
active: data?.active
|
|
4928
|
+
});
|
|
4929
|
+
return {
|
|
4930
|
+
content: [
|
|
4931
|
+
{
|
|
4932
|
+
type: "text",
|
|
4933
|
+
text: `Slot deleted: ${formatSlot(target)}. The queue now has ${updatedSlots.length} slot(s).`
|
|
4934
|
+
}
|
|
4935
|
+
]
|
|
4936
|
+
};
|
|
4937
|
+
}
|
|
4938
|
+
);
|
|
4939
|
+
server2.tool(
|
|
4940
|
+
"toggle_queue",
|
|
4941
|
+
`Enable or disable the entire posting queue. When disabled, no posts will be auto-scheduled to queue slots. Existing scheduled posts are not affected.
|
|
4942
|
+
|
|
4943
|
+
If the user tries to schedule_to_queue while the queue is inactive, warn them and suggest enabling it with this tool first.`,
|
|
4944
|
+
{
|
|
4945
|
+
active: z17.boolean().describe("true to enable the queue, false to disable it"),
|
|
4946
|
+
confirmed: z17.boolean().default(false).describe(
|
|
4947
|
+
"Set to true to confirm the change. If false, returns a preview."
|
|
4948
|
+
)
|
|
4949
|
+
},
|
|
4950
|
+
{
|
|
4951
|
+
title: "Toggle Queue",
|
|
4952
|
+
readOnlyHint: false,
|
|
4953
|
+
destructiveHint: false,
|
|
4954
|
+
idempotentHint: true,
|
|
4955
|
+
openWorldHint: true
|
|
4956
|
+
},
|
|
4957
|
+
async (args) => {
|
|
4958
|
+
const data = await client2.getQueue();
|
|
4959
|
+
const currentActive = data?.active ?? false;
|
|
4960
|
+
const slots = data?.slots ?? [];
|
|
4961
|
+
if (currentActive === args.active) {
|
|
4962
|
+
return {
|
|
4963
|
+
content: [
|
|
4964
|
+
{
|
|
4965
|
+
type: "text",
|
|
4966
|
+
text: `The queue is already ${args.active ? "enabled" : "disabled"}. No change needed.`
|
|
4967
|
+
}
|
|
4968
|
+
]
|
|
4969
|
+
};
|
|
4970
|
+
}
|
|
4971
|
+
if (args.confirmed !== true) {
|
|
4972
|
+
let text = `## Toggle Queue
|
|
4973
|
+
|
|
4974
|
+
`;
|
|
4975
|
+
text += `**Current status:** ${currentActive ? "Enabled" : "Disabled"}
|
|
4976
|
+
`;
|
|
4977
|
+
text += `**New status:** ${args.active ? "Enabled" : "Disabled"}
|
|
4978
|
+
`;
|
|
4979
|
+
text += `**Slots configured:** ${slots.length}
|
|
4980
|
+
|
|
4981
|
+
`;
|
|
4982
|
+
if (args.active && slots.length === 0) {
|
|
4983
|
+
text += "**Warning:** Enabling the queue with no slots configured will have no effect. Create slots first with create_queue_slot.\n\n";
|
|
4984
|
+
}
|
|
4985
|
+
if (!args.active && slots.length > 0) {
|
|
4986
|
+
text += "**Note:** Disabling the queue will stop auto-scheduling. Existing scheduled posts will still go out.\n\n";
|
|
4987
|
+
}
|
|
4988
|
+
text += "Call this tool again with confirmed=true to apply.";
|
|
4989
|
+
return { content: [{ type: "text", text }] };
|
|
4990
|
+
}
|
|
4991
|
+
const updatedSlots = slots.map((s) => ({
|
|
4992
|
+
dayOfWeek: s.dayOfWeek,
|
|
4993
|
+
time: s.time,
|
|
4994
|
+
platforms: s.platforms
|
|
4995
|
+
}));
|
|
4996
|
+
await client2.updateQueue({
|
|
4997
|
+
slots: updatedSlots,
|
|
4998
|
+
active: args.active
|
|
4999
|
+
});
|
|
5000
|
+
return {
|
|
5001
|
+
content: [
|
|
5002
|
+
{
|
|
5003
|
+
type: "text",
|
|
5004
|
+
text: `Queue ${args.active ? "enabled" : "disabled"} successfully.${args.active && slots.length === 0 ? " Note: no slots are configured yet -- use create_queue_slot to add time slots." : ""}`
|
|
5005
|
+
}
|
|
5006
|
+
]
|
|
5007
|
+
};
|
|
5008
|
+
}
|
|
5009
|
+
);
|
|
5010
|
+
}
|
|
5011
|
+
|
|
5012
|
+
// src/tools/canva.ts
|
|
5013
|
+
import { z as z18 } from "zod";
|
|
5014
|
+
var CANVA_NOT_CONNECTED = "Canva is not connected. Visit BuzzPoster settings to connect your Canva account.";
|
|
5015
|
+
function isNotConnectedError(error) {
|
|
5016
|
+
if (error instanceof Error) {
|
|
5017
|
+
return error.message.includes("Canva is not connected") || error.message.includes("400");
|
|
5018
|
+
}
|
|
5019
|
+
return false;
|
|
5020
|
+
}
|
|
5021
|
+
function registerCanvaTools(server2, client2) {
|
|
5022
|
+
server2.tool(
|
|
5023
|
+
"canva_upload_asset",
|
|
5024
|
+
"Upload an image to Canva from a URL. Returns the Canva asset ID and thumbnail URL. Use this to import images into Canva for use in designs.",
|
|
5025
|
+
{
|
|
5026
|
+
image_url: z18.string().url().describe("The URL of the image to upload to Canva."),
|
|
5027
|
+
name: z18.string().min(1).max(255).describe("A name for the uploaded asset.")
|
|
5028
|
+
},
|
|
5029
|
+
{
|
|
5030
|
+
title: "Upload Asset to Canva",
|
|
5031
|
+
readOnlyHint: false,
|
|
5032
|
+
destructiveHint: false,
|
|
5033
|
+
idempotentHint: false,
|
|
5034
|
+
openWorldHint: true
|
|
5035
|
+
},
|
|
5036
|
+
async ({ image_url, name }) => {
|
|
5037
|
+
try {
|
|
5038
|
+
const result = await client2.canvaUploadAsset({
|
|
5039
|
+
imageUrl: image_url,
|
|
5040
|
+
name
|
|
5041
|
+
});
|
|
5042
|
+
const lines = [`Asset uploaded to Canva successfully.`];
|
|
5043
|
+
lines.push(`Asset ID: ${result.assetId}`);
|
|
5044
|
+
if (result.thumbnailUrl) {
|
|
5045
|
+
lines.push(`Thumbnail: ${result.thumbnailUrl}`);
|
|
5046
|
+
}
|
|
5047
|
+
return {
|
|
5048
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
5049
|
+
};
|
|
5050
|
+
} catch (error) {
|
|
5051
|
+
if (isNotConnectedError(error)) {
|
|
5052
|
+
return {
|
|
5053
|
+
content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
|
|
5054
|
+
};
|
|
5055
|
+
}
|
|
5056
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
5057
|
+
throw new Error(`Failed to upload asset to Canva: ${message}`);
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
);
|
|
5061
|
+
server2.tool(
|
|
5062
|
+
"canva_create_design",
|
|
5063
|
+
"Create a new Canva design. Returns the design ID and an edit URL the user can open to customize the design in Canva's editor.",
|
|
5064
|
+
{
|
|
5065
|
+
title: z18.string().min(1).max(255).describe("Title for the new design."),
|
|
5066
|
+
design_type: z18.enum([
|
|
5067
|
+
"instagram_post",
|
|
5068
|
+
"facebook_post",
|
|
5069
|
+
"presentation",
|
|
5070
|
+
"poster",
|
|
5071
|
+
"story"
|
|
5072
|
+
]).describe("The type of design to create, which determines dimensions."),
|
|
5073
|
+
asset_id: z18.string().optional().describe(
|
|
5074
|
+
"Optional Canva asset ID to include in the design."
|
|
5075
|
+
)
|
|
5076
|
+
},
|
|
5077
|
+
{
|
|
5078
|
+
title: "Create Canva Design",
|
|
5079
|
+
readOnlyHint: false,
|
|
5080
|
+
destructiveHint: false,
|
|
5081
|
+
idempotentHint: false,
|
|
5082
|
+
openWorldHint: true
|
|
5083
|
+
},
|
|
5084
|
+
async ({ title, design_type, asset_id }) => {
|
|
5085
|
+
try {
|
|
5086
|
+
const result = await client2.canvaCreateDesign({
|
|
5087
|
+
title,
|
|
5088
|
+
designType: design_type,
|
|
5089
|
+
assetId: asset_id
|
|
5090
|
+
});
|
|
5091
|
+
const lines = [`Canva design created successfully.`];
|
|
5092
|
+
lines.push(`Design ID: ${result.designId}`);
|
|
5093
|
+
lines.push(`Edit URL: ${result.editUrl}`);
|
|
5094
|
+
if (result.thumbnailUrl) {
|
|
5095
|
+
lines.push(`Thumbnail: ${result.thumbnailUrl}`);
|
|
5096
|
+
}
|
|
5097
|
+
return {
|
|
5098
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
5099
|
+
};
|
|
5100
|
+
} catch (error) {
|
|
5101
|
+
if (isNotConnectedError(error)) {
|
|
5102
|
+
return {
|
|
5103
|
+
content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
|
|
5104
|
+
};
|
|
5105
|
+
}
|
|
5106
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
5107
|
+
throw new Error(`Failed to create Canva design: ${message}`);
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
5110
|
+
);
|
|
5111
|
+
server2.tool(
|
|
5112
|
+
"canva_export_design",
|
|
5113
|
+
"Export a Canva design to a downloadable file. Polls until the export is complete and returns the download URL.",
|
|
5114
|
+
{
|
|
5115
|
+
design_id: z18.string().min(1).describe("The Canva design ID to export."),
|
|
5116
|
+
format: z18.enum(["png", "pdf", "jpg"]).default("png").describe("Export format. Defaults to PNG.")
|
|
5117
|
+
},
|
|
5118
|
+
{
|
|
5119
|
+
title: "Export Canva Design",
|
|
5120
|
+
readOnlyHint: false,
|
|
5121
|
+
destructiveHint: false,
|
|
5122
|
+
idempotentHint: true,
|
|
5123
|
+
openWorldHint: true
|
|
5124
|
+
},
|
|
5125
|
+
async ({ design_id, format }) => {
|
|
5126
|
+
try {
|
|
5127
|
+
const result = await client2.canvaExportDesign({
|
|
5128
|
+
designId: design_id,
|
|
5129
|
+
format
|
|
5130
|
+
});
|
|
5131
|
+
return {
|
|
5132
|
+
content: [
|
|
5133
|
+
{
|
|
5134
|
+
type: "text",
|
|
5135
|
+
text: `Design exported successfully.
|
|
5136
|
+
Download URL: ${result.downloadUrl}`
|
|
5137
|
+
}
|
|
5138
|
+
]
|
|
5139
|
+
};
|
|
5140
|
+
} catch (error) {
|
|
5141
|
+
if (isNotConnectedError(error)) {
|
|
5142
|
+
return {
|
|
5143
|
+
content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
|
|
5144
|
+
};
|
|
5145
|
+
}
|
|
5146
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
5147
|
+
throw new Error(`Failed to export Canva design: ${message}`);
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
);
|
|
5151
|
+
server2.tool(
|
|
5152
|
+
"canva_search_designs",
|
|
5153
|
+
"Search the customer's Canva designs by keyword. Returns matching designs with their IDs, titles, thumbnails, and edit URLs.",
|
|
5154
|
+
{
|
|
5155
|
+
query: z18.string().min(1).describe("Search keyword to find designs.")
|
|
5156
|
+
},
|
|
5157
|
+
{
|
|
5158
|
+
title: "Search Canva Designs",
|
|
5159
|
+
readOnlyHint: true,
|
|
5160
|
+
destructiveHint: false,
|
|
5161
|
+
idempotentHint: true,
|
|
5162
|
+
openWorldHint: true
|
|
5163
|
+
},
|
|
5164
|
+
async ({ query }) => {
|
|
5165
|
+
try {
|
|
5166
|
+
const designs = await client2.canvaSearchDesigns(query);
|
|
5167
|
+
if (!designs || designs.length === 0) {
|
|
5168
|
+
return {
|
|
5169
|
+
content: [
|
|
5170
|
+
{
|
|
5171
|
+
type: "text",
|
|
5172
|
+
text: `No Canva designs found matching "${query}".`
|
|
5173
|
+
}
|
|
5174
|
+
]
|
|
5175
|
+
};
|
|
5176
|
+
}
|
|
5177
|
+
const lines = [`## Canva Designs (${designs.length} results)`];
|
|
5178
|
+
lines.push("");
|
|
5179
|
+
for (const d of designs) {
|
|
5180
|
+
lines.push(`**${d.title}** (ID: ${d.id})`);
|
|
5181
|
+
lines.push(` Edit: ${d.editUrl}`);
|
|
5182
|
+
if (d.thumbnailUrl) lines.push(` Thumbnail: ${d.thumbnailUrl}`);
|
|
5183
|
+
lines.push("");
|
|
5184
|
+
}
|
|
5185
|
+
return {
|
|
5186
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
5187
|
+
};
|
|
5188
|
+
} catch (error) {
|
|
5189
|
+
if (isNotConnectedError(error)) {
|
|
5190
|
+
return {
|
|
5191
|
+
content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
|
|
5192
|
+
};
|
|
5193
|
+
}
|
|
5194
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
5195
|
+
throw new Error(`Failed to search Canva designs: ${message}`);
|
|
5196
|
+
}
|
|
5197
|
+
}
|
|
5198
|
+
);
|
|
5199
|
+
server2.tool(
|
|
5200
|
+
"canva_get_design",
|
|
5201
|
+
"Get details about a specific Canva design by ID. Returns the design's title, edit URL, thumbnail, and metadata.",
|
|
5202
|
+
{
|
|
5203
|
+
design_id: z18.string().min(1).describe("The Canva design ID to retrieve.")
|
|
5204
|
+
},
|
|
5205
|
+
{
|
|
5206
|
+
title: "Get Canva Design",
|
|
5207
|
+
readOnlyHint: true,
|
|
5208
|
+
destructiveHint: false,
|
|
5209
|
+
idempotentHint: true,
|
|
5210
|
+
openWorldHint: true
|
|
5211
|
+
},
|
|
5212
|
+
async ({ design_id }) => {
|
|
5213
|
+
try {
|
|
5214
|
+
const design = await client2.canvaGetDesign(design_id);
|
|
5215
|
+
const lines = [`## ${design.title}`];
|
|
5216
|
+
lines.push(`Design ID: ${design.id}`);
|
|
5217
|
+
lines.push(`Edit URL: ${design.editUrl}`);
|
|
5218
|
+
lines.push(`View URL: ${design.viewUrl}`);
|
|
5219
|
+
if (design.thumbnailUrl) {
|
|
5220
|
+
lines.push(`Thumbnail: ${design.thumbnailUrl}`);
|
|
5221
|
+
}
|
|
5222
|
+
if (design.createdAt) lines.push(`Created: ${design.createdAt}`);
|
|
5223
|
+
if (design.updatedAt) lines.push(`Updated: ${design.updatedAt}`);
|
|
5224
|
+
return {
|
|
5225
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
5226
|
+
};
|
|
5227
|
+
} catch (error) {
|
|
5228
|
+
if (isNotConnectedError(error)) {
|
|
5229
|
+
return {
|
|
5230
|
+
content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
|
|
5231
|
+
};
|
|
5232
|
+
}
|
|
5233
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
5234
|
+
throw new Error(`Failed to get Canva design: ${message}`);
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
5237
|
+
);
|
|
5238
|
+
}
|
|
5239
|
+
|
|
5240
|
+
// src/tools/sendgrid-newsletter.ts
|
|
5241
|
+
import { z as z19 } from "zod";
|
|
5242
|
+
function registerSendGridNewsletterTools(server2, client2, options) {
|
|
5243
|
+
const allowDirectSend2 = options?.allowDirectSend ?? false;
|
|
5244
|
+
server2.tool(
|
|
5245
|
+
"sg_list_lists",
|
|
5246
|
+
"List all SendGrid subscriber lists for the account. Returns list names, IDs, and descriptions.",
|
|
5247
|
+
{},
|
|
5248
|
+
{
|
|
5249
|
+
title: "List Subscriber Lists",
|
|
5250
|
+
readOnlyHint: true,
|
|
5251
|
+
destructiveHint: false,
|
|
5252
|
+
idempotentHint: true,
|
|
5253
|
+
openWorldHint: false
|
|
5254
|
+
},
|
|
5255
|
+
async () => {
|
|
5256
|
+
const lists = await client2.sgListLists();
|
|
5257
|
+
if (!lists || lists.length === 0) {
|
|
5258
|
+
return {
|
|
5259
|
+
content: [
|
|
5260
|
+
{
|
|
5261
|
+
type: "text",
|
|
5262
|
+
text: "No subscriber lists found. Create one with sg_create_list."
|
|
5263
|
+
}
|
|
5264
|
+
]
|
|
5265
|
+
};
|
|
5266
|
+
}
|
|
5267
|
+
const lines = [`## Subscriber Lists (${lists.length})`];
|
|
5268
|
+
for (const l of lists) {
|
|
5269
|
+
lines.push(`- **${l.name}** (ID: ${l.id})${l.description ? ` \u2014 ${l.description}` : ""}`);
|
|
5270
|
+
}
|
|
5271
|
+
return {
|
|
5272
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
5273
|
+
};
|
|
5274
|
+
}
|
|
5275
|
+
);
|
|
5276
|
+
server2.tool(
|
|
5277
|
+
"sg_create_list",
|
|
5278
|
+
"Create a new subscriber list for organizing newsletter recipients.",
|
|
5279
|
+
{
|
|
5280
|
+
name: z19.string().min(1).max(255).describe("Name of the subscriber list."),
|
|
5281
|
+
description: z19.string().max(1e3).optional().describe("Optional description of the list.")
|
|
5282
|
+
},
|
|
5283
|
+
{
|
|
5284
|
+
title: "Create Subscriber List",
|
|
5285
|
+
readOnlyHint: false,
|
|
5286
|
+
destructiveHint: false,
|
|
5287
|
+
idempotentHint: false,
|
|
5288
|
+
openWorldHint: false
|
|
5289
|
+
},
|
|
5290
|
+
async ({ name, description }) => {
|
|
5291
|
+
const list = await client2.sgCreateList({ name, description });
|
|
5292
|
+
return {
|
|
5293
|
+
content: [
|
|
5294
|
+
{
|
|
5295
|
+
type: "text",
|
|
5296
|
+
text: `Subscriber list created.
|
|
5297
|
+
Name: ${list.name}
|
|
5298
|
+
ID: ${list.id}`
|
|
5299
|
+
}
|
|
5300
|
+
]
|
|
5301
|
+
};
|
|
5302
|
+
}
|
|
5303
|
+
);
|
|
5304
|
+
server2.tool(
|
|
5305
|
+
"sg_delete_list",
|
|
5306
|
+
"Delete a subscriber list. This removes the list and all memberships (subscribers themselves are not deleted). Requires confirmation.",
|
|
5307
|
+
{
|
|
5308
|
+
list_id: z19.number().int().describe("ID of the list to delete."),
|
|
5309
|
+
confirmed: z19.boolean().describe(
|
|
5310
|
+
"Must be true to confirm deletion. Show the list name to the user and ask for confirmation first."
|
|
5311
|
+
)
|
|
5312
|
+
},
|
|
5313
|
+
{
|
|
5314
|
+
title: "Delete Subscriber List",
|
|
5315
|
+
readOnlyHint: false,
|
|
5316
|
+
destructiveHint: true,
|
|
5317
|
+
idempotentHint: false,
|
|
5318
|
+
openWorldHint: false
|
|
5319
|
+
},
|
|
5320
|
+
async ({ list_id, confirmed }) => {
|
|
5321
|
+
if (!confirmed) {
|
|
5322
|
+
return {
|
|
5323
|
+
content: [
|
|
5324
|
+
{
|
|
5325
|
+
type: "text",
|
|
5326
|
+
text: "Deletion not confirmed. Set confirmed=true to proceed."
|
|
5327
|
+
}
|
|
5328
|
+
]
|
|
5329
|
+
};
|
|
5330
|
+
}
|
|
5331
|
+
await client2.sgDeleteList(list_id);
|
|
5332
|
+
return {
|
|
5333
|
+
content: [
|
|
5334
|
+
{
|
|
5335
|
+
type: "text",
|
|
5336
|
+
text: `Subscriber list ${list_id} deleted successfully.`
|
|
5337
|
+
}
|
|
5338
|
+
]
|
|
5339
|
+
};
|
|
5340
|
+
}
|
|
5341
|
+
);
|
|
5342
|
+
server2.tool(
|
|
5343
|
+
"sg_list_subscribers",
|
|
5344
|
+
"List SendGrid-managed subscribers. Can filter by list ID and status (active, unsubscribed, bounced, complained). Supports pagination.",
|
|
5345
|
+
{
|
|
5346
|
+
list_id: z19.number().int().optional().describe("Optional list ID to filter subscribers by list membership."),
|
|
5347
|
+
status: z19.enum(["active", "unsubscribed", "bounced", "complained"]).optional().describe("Optional status filter."),
|
|
5348
|
+
page: z19.number().int().min(1).default(1).describe("Page number."),
|
|
5349
|
+
limit: z19.number().int().min(1).max(100).default(50).describe("Results per page.")
|
|
5350
|
+
},
|
|
5351
|
+
{
|
|
5352
|
+
title: "List Subscribers",
|
|
5353
|
+
readOnlyHint: true,
|
|
5354
|
+
destructiveHint: false,
|
|
5355
|
+
idempotentHint: true,
|
|
5356
|
+
openWorldHint: false
|
|
5357
|
+
},
|
|
5358
|
+
async ({ list_id, status, page, limit }) => {
|
|
5359
|
+
const params = {
|
|
5360
|
+
page: String(page),
|
|
5361
|
+
limit: String(limit)
|
|
5362
|
+
};
|
|
5363
|
+
if (list_id !== void 0) params.listId = String(list_id);
|
|
5364
|
+
if (status) params.status = status;
|
|
5365
|
+
const result = await client2.sgListSubscribers(params);
|
|
5366
|
+
if (!result.subscribers || result.subscribers.length === 0) {
|
|
5367
|
+
return {
|
|
5368
|
+
content: [
|
|
5369
|
+
{
|
|
5370
|
+
type: "text",
|
|
5371
|
+
text: "No subscribers found matching the criteria."
|
|
5372
|
+
}
|
|
5373
|
+
]
|
|
5374
|
+
};
|
|
5375
|
+
}
|
|
5376
|
+
const lines = [`## Subscribers (page ${result.page})`];
|
|
5377
|
+
for (const s of result.subscribers) {
|
|
5378
|
+
const name = [s.firstName, s.lastName].filter(Boolean).join(" ");
|
|
5379
|
+
lines.push(
|
|
5380
|
+
`- ${s.email}${name ? ` (${name})` : ""} \u2014 ${s.status} (ID: ${s.id})`
|
|
5381
|
+
);
|
|
5382
|
+
}
|
|
5383
|
+
return {
|
|
5384
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
5385
|
+
};
|
|
5386
|
+
}
|
|
5387
|
+
);
|
|
5388
|
+
server2.tool(
|
|
5389
|
+
"sg_add_subscriber",
|
|
5390
|
+
"Add or update a subscriber in the SendGrid-managed subscriber list. Optionally assign to one or more lists.",
|
|
5391
|
+
{
|
|
5392
|
+
email: z19.string().email().describe("Subscriber's email address."),
|
|
5393
|
+
first_name: z19.string().optional().describe("Subscriber's first name."),
|
|
5394
|
+
last_name: z19.string().optional().describe("Subscriber's last name."),
|
|
5395
|
+
list_ids: z19.array(z19.number().int()).optional().describe("Optional list IDs to add the subscriber to.")
|
|
5396
|
+
},
|
|
5397
|
+
{
|
|
5398
|
+
title: "Add Subscriber",
|
|
5399
|
+
readOnlyHint: false,
|
|
5400
|
+
destructiveHint: false,
|
|
5401
|
+
idempotentHint: true,
|
|
5402
|
+
openWorldHint: false
|
|
5403
|
+
},
|
|
5404
|
+
async ({ email, first_name, last_name, list_ids }) => {
|
|
5405
|
+
const subscriber = await client2.sgAddSubscriber({
|
|
5406
|
+
email,
|
|
5407
|
+
firstName: first_name,
|
|
5408
|
+
lastName: last_name,
|
|
5409
|
+
listIds: list_ids
|
|
5410
|
+
});
|
|
5411
|
+
return {
|
|
5412
|
+
content: [
|
|
5413
|
+
{
|
|
5414
|
+
type: "text",
|
|
5415
|
+
text: `Subscriber added/updated.
|
|
5416
|
+
Email: ${subscriber.email}
|
|
5417
|
+
ID: ${subscriber.id}`
|
|
5418
|
+
}
|
|
5419
|
+
]
|
|
5420
|
+
};
|
|
5421
|
+
}
|
|
5422
|
+
);
|
|
5423
|
+
server2.tool(
|
|
5424
|
+
"sg_remove_subscriber",
|
|
5425
|
+
"Remove a subscriber (marks as unsubscribed). Requires confirmation.",
|
|
5426
|
+
{
|
|
5427
|
+
subscriber_id: z19.number().int().describe("ID of the subscriber to remove."),
|
|
5428
|
+
confirmed: z19.boolean().describe(
|
|
5429
|
+
"Must be true to confirm. Show the subscriber email to the user first."
|
|
5430
|
+
)
|
|
5431
|
+
},
|
|
5432
|
+
{
|
|
5433
|
+
title: "Remove Subscriber",
|
|
5434
|
+
readOnlyHint: false,
|
|
5435
|
+
destructiveHint: true,
|
|
5436
|
+
idempotentHint: false,
|
|
5437
|
+
openWorldHint: false
|
|
5438
|
+
},
|
|
5439
|
+
async ({ subscriber_id, confirmed }) => {
|
|
5440
|
+
if (!confirmed) {
|
|
5441
|
+
return {
|
|
5442
|
+
content: [
|
|
5443
|
+
{
|
|
5444
|
+
type: "text",
|
|
5445
|
+
text: "Removal not confirmed. Set confirmed=true to proceed."
|
|
5446
|
+
}
|
|
5447
|
+
]
|
|
5448
|
+
};
|
|
5449
|
+
}
|
|
5450
|
+
const result = await client2.sgRemoveSubscriber(subscriber_id);
|
|
5451
|
+
return {
|
|
5452
|
+
content: [
|
|
5453
|
+
{
|
|
5454
|
+
type: "text",
|
|
5455
|
+
text: `Subscriber ${result.email} marked as unsubscribed.`
|
|
5456
|
+
}
|
|
5457
|
+
]
|
|
5458
|
+
};
|
|
5459
|
+
}
|
|
5460
|
+
);
|
|
5461
|
+
server2.tool(
|
|
5462
|
+
"sg_list_sends",
|
|
5463
|
+
"List newsletter sends (drafts, scheduled, sent). Filter by status.",
|
|
5464
|
+
{
|
|
5465
|
+
status: z19.enum(["draft", "scheduled", "sending", "sent", "cancelled"]).optional().describe("Optional status filter."),
|
|
5466
|
+
page: z19.number().int().min(1).default(1).describe("Page number."),
|
|
5467
|
+
limit: z19.number().int().min(1).max(100).default(20).describe("Results per page.")
|
|
5468
|
+
},
|
|
5469
|
+
{
|
|
5470
|
+
title: "List Newsletter Sends",
|
|
5471
|
+
readOnlyHint: true,
|
|
5472
|
+
destructiveHint: false,
|
|
5473
|
+
idempotentHint: true,
|
|
5474
|
+
openWorldHint: false
|
|
5475
|
+
},
|
|
5476
|
+
async ({ status, page, limit }) => {
|
|
5477
|
+
const params = {
|
|
5478
|
+
page: String(page),
|
|
5479
|
+
limit: String(limit)
|
|
5480
|
+
};
|
|
5481
|
+
if (status) params.status = status;
|
|
5482
|
+
const result = await client2.sgListSends(params);
|
|
5483
|
+
if (!result.sends || result.sends.length === 0) {
|
|
5484
|
+
return {
|
|
5485
|
+
content: [
|
|
5486
|
+
{ type: "text", text: "No newsletter sends found." }
|
|
5487
|
+
]
|
|
5488
|
+
};
|
|
5489
|
+
}
|
|
5490
|
+
const lines = [`## Newsletter Sends (${result.sends.length})`];
|
|
5491
|
+
for (const s of result.sends) {
|
|
5492
|
+
lines.push(
|
|
5493
|
+
`- **${s.subject}** (ID: ${s.id}) \u2014 ${s.status}${s.sentAt ? `, sent ${s.sentAt}` : ""}${s.scheduledAt ? `, scheduled ${s.scheduledAt}` : ""}`
|
|
5494
|
+
);
|
|
5495
|
+
if (s.status === "sent") {
|
|
5496
|
+
lines.push(` Delivered: ${s.delivered} | Opened: ${s.opened}`);
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5499
|
+
return {
|
|
5500
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
5501
|
+
};
|
|
5502
|
+
}
|
|
5503
|
+
);
|
|
5504
|
+
server2.tool(
|
|
5505
|
+
"sg_get_send",
|
|
5506
|
+
"Get details and delivery stats for a specific newsletter send.",
|
|
5507
|
+
{
|
|
5508
|
+
send_id: z19.number().int().describe("ID of the newsletter send.")
|
|
5509
|
+
},
|
|
5510
|
+
{
|
|
5511
|
+
title: "Get Newsletter Send",
|
|
5512
|
+
readOnlyHint: true,
|
|
5513
|
+
destructiveHint: false,
|
|
5514
|
+
idempotentHint: true,
|
|
5515
|
+
openWorldHint: false
|
|
5516
|
+
},
|
|
5517
|
+
async ({ send_id }) => {
|
|
5518
|
+
const send = await client2.sgGetSend(send_id);
|
|
5519
|
+
const lines = [
|
|
5520
|
+
`## ${send.subject}`,
|
|
5521
|
+
`ID: ${send.id}`,
|
|
5522
|
+
`From: ${send.fromName ? `${send.fromName} <${send.fromEmail}>` : send.fromEmail}`,
|
|
5523
|
+
`Status: ${send.status}`
|
|
5524
|
+
];
|
|
5525
|
+
if (send.sentAt) lines.push(`Sent: ${send.sentAt}`);
|
|
5526
|
+
if (send.scheduledAt) lines.push(`Scheduled: ${send.scheduledAt}`);
|
|
5527
|
+
lines.push("", "### Delivery Stats");
|
|
5528
|
+
lines.push(`Delivered: ${send.delivered}`);
|
|
5529
|
+
lines.push(`Opened: ${send.opened}`);
|
|
5530
|
+
lines.push(`Clicked: ${send.clicked}`);
|
|
5531
|
+
lines.push(`Bounced: ${send.bounced}`);
|
|
5532
|
+
lines.push(`Complaints: ${send.complained}`);
|
|
5533
|
+
lines.push(`Unsubscribed: ${send.unsubscribed}`);
|
|
5534
|
+
lines.push("");
|
|
5535
|
+
lines.push(`NEWSLETTER_HTML_PREVIEW: ${send.htmlContent}`);
|
|
5536
|
+
return {
|
|
5537
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
5538
|
+
};
|
|
5539
|
+
}
|
|
5540
|
+
);
|
|
5541
|
+
server2.tool(
|
|
5542
|
+
"sg_create_send",
|
|
5543
|
+
"Create a new newsletter send draft. Must specify subject, HTML content, and from email.",
|
|
5544
|
+
{
|
|
5545
|
+
subject: z19.string().min(1).max(500).describe("Email subject line."),
|
|
5546
|
+
html_content: z19.string().min(1).describe("HTML content of the newsletter."),
|
|
5547
|
+
from_email: z19.string().email().describe("Sender email address (must be authenticated domain)."),
|
|
5548
|
+
from_name: z19.string().optional().describe("Sender display name."),
|
|
5549
|
+
list_id: z19.number().int().optional().describe("Optional list ID to send to. If omitted, sends to all active subscribers.")
|
|
5550
|
+
},
|
|
5551
|
+
{
|
|
5552
|
+
title: "Create Newsletter Draft",
|
|
5553
|
+
readOnlyHint: false,
|
|
5554
|
+
destructiveHint: false,
|
|
5555
|
+
idempotentHint: false,
|
|
5556
|
+
openWorldHint: false
|
|
5557
|
+
},
|
|
5558
|
+
async ({ subject, html_content, from_email, from_name, list_id }) => {
|
|
5559
|
+
const send = await client2.sgCreateSend({
|
|
5560
|
+
subject,
|
|
5561
|
+
htmlContent: html_content,
|
|
5562
|
+
fromEmail: from_email,
|
|
5563
|
+
fromName: from_name,
|
|
5564
|
+
listId: list_id
|
|
5565
|
+
});
|
|
5566
|
+
return {
|
|
5567
|
+
content: [
|
|
5568
|
+
{
|
|
5569
|
+
type: "text",
|
|
5570
|
+
text: `Newsletter draft created.
|
|
5571
|
+
ID: ${send.id}
|
|
5572
|
+
Subject: ${send.subject}
|
|
5573
|
+
Status: ${send.status}
|
|
5574
|
+
|
|
5575
|
+
Use sg_test_send to preview, or sg_send_newsletter to send.`
|
|
5576
|
+
}
|
|
5577
|
+
]
|
|
5578
|
+
};
|
|
5579
|
+
}
|
|
5580
|
+
);
|
|
5581
|
+
server2.tool(
|
|
5582
|
+
"sg_update_send",
|
|
5583
|
+
"Update a newsletter draft. Only drafts can be updated.",
|
|
5584
|
+
{
|
|
5585
|
+
send_id: z19.number().int().describe("ID of the send to update."),
|
|
5586
|
+
subject: z19.string().min(1).max(500).optional().describe("New subject line."),
|
|
5587
|
+
html_content: z19.string().min(1).optional().describe("New HTML content."),
|
|
5588
|
+
from_email: z19.string().email().optional().describe("New sender email."),
|
|
5589
|
+
from_name: z19.string().optional().describe("New sender name."),
|
|
5590
|
+
list_id: z19.number().int().nullable().optional().describe("New list ID, or null to send to all.")
|
|
5591
|
+
},
|
|
5592
|
+
{
|
|
5593
|
+
title: "Update Newsletter Draft",
|
|
5594
|
+
readOnlyHint: false,
|
|
5595
|
+
destructiveHint: false,
|
|
5596
|
+
idempotentHint: true,
|
|
5597
|
+
openWorldHint: false
|
|
5598
|
+
},
|
|
5599
|
+
async ({ send_id, subject, html_content, from_email, from_name, list_id }) => {
|
|
5600
|
+
const data = {};
|
|
5601
|
+
if (subject !== void 0) data.subject = subject;
|
|
5602
|
+
if (html_content !== void 0) data.htmlContent = html_content;
|
|
5603
|
+
if (from_email !== void 0) data.fromEmail = from_email;
|
|
5604
|
+
if (from_name !== void 0) data.fromName = from_name;
|
|
5605
|
+
if (list_id !== void 0) data.listId = list_id;
|
|
5606
|
+
const send = await client2.sgUpdateSend(send_id, data);
|
|
5607
|
+
return {
|
|
5608
|
+
content: [
|
|
5609
|
+
{
|
|
5610
|
+
type: "text",
|
|
5611
|
+
text: `Newsletter draft updated.
|
|
5612
|
+
ID: ${send.id}
|
|
5613
|
+
Subject: ${send.subject}`
|
|
5614
|
+
}
|
|
5615
|
+
]
|
|
5616
|
+
};
|
|
5617
|
+
}
|
|
5618
|
+
);
|
|
5619
|
+
server2.tool(
|
|
5620
|
+
"sg_delete_send",
|
|
5621
|
+
"Delete a newsletter draft. Only drafts can be deleted. Requires confirmation.",
|
|
5622
|
+
{
|
|
5623
|
+
send_id: z19.number().int().describe("ID of the draft to delete."),
|
|
5624
|
+
confirmed: z19.boolean().describe("Must be true to confirm deletion.")
|
|
5625
|
+
},
|
|
5626
|
+
{
|
|
5627
|
+
title: "Delete Newsletter Draft",
|
|
5628
|
+
readOnlyHint: false,
|
|
5629
|
+
destructiveHint: true,
|
|
5630
|
+
idempotentHint: false,
|
|
5631
|
+
openWorldHint: false
|
|
5632
|
+
},
|
|
5633
|
+
async ({ send_id, confirmed }) => {
|
|
5634
|
+
if (!confirmed) {
|
|
5635
|
+
return {
|
|
5636
|
+
content: [
|
|
5637
|
+
{
|
|
5638
|
+
type: "text",
|
|
5639
|
+
text: "Deletion not confirmed. Set confirmed=true to proceed."
|
|
5640
|
+
}
|
|
5641
|
+
]
|
|
5642
|
+
};
|
|
5643
|
+
}
|
|
5644
|
+
await client2.sgDeleteSend(send_id);
|
|
5645
|
+
return {
|
|
5646
|
+
content: [
|
|
5647
|
+
{
|
|
5648
|
+
type: "text",
|
|
5649
|
+
text: `Newsletter draft ${send_id} deleted.`
|
|
5650
|
+
}
|
|
5651
|
+
]
|
|
5652
|
+
};
|
|
5653
|
+
}
|
|
5654
|
+
);
|
|
5655
|
+
server2.tool(
|
|
5656
|
+
"sg_send_newsletter",
|
|
5657
|
+
allowDirectSend2 ? "Send a newsletter immediately to all active subscribers (or subscribers in the specified list). This action cannot be undone. Requires confirmation." : "DISABLED: Direct send is not enabled for this account. Contact support.",
|
|
5658
|
+
{
|
|
5659
|
+
send_id: z19.number().int().describe("ID of the draft to send."),
|
|
5660
|
+
confirmed: z19.boolean().describe(
|
|
5661
|
+
"Must be true. Show the user the subject, from address, and recipient count before confirming."
|
|
5662
|
+
)
|
|
5663
|
+
},
|
|
5664
|
+
{
|
|
5665
|
+
title: "Send Newsletter Now",
|
|
5666
|
+
readOnlyHint: false,
|
|
5667
|
+
destructiveHint: true,
|
|
5668
|
+
idempotentHint: false,
|
|
5669
|
+
openWorldHint: true
|
|
5670
|
+
},
|
|
5671
|
+
async ({ send_id, confirmed }) => {
|
|
5672
|
+
if (!allowDirectSend2) {
|
|
5673
|
+
return {
|
|
5674
|
+
content: [
|
|
5675
|
+
{
|
|
5676
|
+
type: "text",
|
|
5677
|
+
text: "Direct send is not enabled for this account. Contact support to enable this feature."
|
|
5678
|
+
}
|
|
5679
|
+
]
|
|
5680
|
+
};
|
|
5681
|
+
}
|
|
5682
|
+
if (!confirmed) {
|
|
5683
|
+
return {
|
|
5684
|
+
content: [
|
|
5685
|
+
{
|
|
5686
|
+
type: "text",
|
|
5687
|
+
text: "Send not confirmed. Review the newsletter details with sg_get_send first, then set confirmed=true."
|
|
5688
|
+
}
|
|
5689
|
+
]
|
|
5690
|
+
};
|
|
5691
|
+
}
|
|
5692
|
+
const result = await client2.sgSendNewsletter(send_id);
|
|
5693
|
+
return {
|
|
5694
|
+
content: [
|
|
5695
|
+
{
|
|
5696
|
+
type: "text",
|
|
5697
|
+
text: `Newsletter sent successfully!
|
|
5698
|
+
Subject: ${result.subject}
|
|
5699
|
+
Recipients: ${result.recipientCount}
|
|
5700
|
+
Status: ${result.status}`
|
|
5701
|
+
}
|
|
5702
|
+
]
|
|
5703
|
+
};
|
|
5704
|
+
}
|
|
5705
|
+
);
|
|
5706
|
+
server2.tool(
|
|
5707
|
+
"sg_test_send",
|
|
5708
|
+
"Send a test email of a newsletter draft to a specific email address. Subject will be prefixed with [TEST].",
|
|
5709
|
+
{
|
|
5710
|
+
send_id: z19.number().int().describe("ID of the newsletter send."),
|
|
5711
|
+
email: z19.string().email().describe("Email address to send the test to.")
|
|
5712
|
+
},
|
|
5713
|
+
{
|
|
5714
|
+
title: "Send Test Email",
|
|
5715
|
+
readOnlyHint: false,
|
|
5716
|
+
destructiveHint: false,
|
|
5717
|
+
idempotentHint: false,
|
|
5718
|
+
openWorldHint: true
|
|
5719
|
+
},
|
|
5720
|
+
async ({ send_id, email }) => {
|
|
5721
|
+
await client2.sgTestSend(send_id, { email });
|
|
5722
|
+
return {
|
|
5723
|
+
content: [
|
|
5724
|
+
{
|
|
5725
|
+
type: "text",
|
|
5726
|
+
text: `Test email sent to ${email}. Check your inbox.`
|
|
5727
|
+
}
|
|
5728
|
+
]
|
|
5729
|
+
};
|
|
5730
|
+
}
|
|
5731
|
+
);
|
|
5732
|
+
server2.tool(
|
|
5733
|
+
"sg_schedule_send",
|
|
5734
|
+
"Schedule a newsletter to be sent at a future date/time. Requires confirmation.",
|
|
5735
|
+
{
|
|
5736
|
+
send_id: z19.number().int().describe("ID of the draft to schedule."),
|
|
5737
|
+
scheduled_at: z19.string().describe(
|
|
5738
|
+
"ISO 8601 datetime for when to send (e.g., '2025-01-15T09:00:00Z'). Must be in the future."
|
|
5739
|
+
),
|
|
5740
|
+
confirmed: z19.boolean().describe("Must be true to confirm scheduling.")
|
|
5741
|
+
},
|
|
5742
|
+
{
|
|
5743
|
+
title: "Schedule Newsletter Send",
|
|
5744
|
+
readOnlyHint: false,
|
|
5745
|
+
destructiveHint: false,
|
|
5746
|
+
idempotentHint: false,
|
|
5747
|
+
openWorldHint: true
|
|
5748
|
+
},
|
|
5749
|
+
async ({ send_id, scheduled_at, confirmed }) => {
|
|
5750
|
+
if (!confirmed) {
|
|
5751
|
+
return {
|
|
5752
|
+
content: [
|
|
5753
|
+
{
|
|
5754
|
+
type: "text",
|
|
5755
|
+
text: "Scheduling not confirmed. Set confirmed=true to proceed."
|
|
5756
|
+
}
|
|
5757
|
+
]
|
|
5758
|
+
};
|
|
5759
|
+
}
|
|
5760
|
+
const result = await client2.sgScheduleSend(send_id, {
|
|
5761
|
+
scheduledAt: scheduled_at
|
|
5762
|
+
});
|
|
5763
|
+
return {
|
|
5764
|
+
content: [
|
|
5765
|
+
{
|
|
5766
|
+
type: "text",
|
|
5767
|
+
text: `Newsletter scheduled.
|
|
5768
|
+
Subject: ${result.subject}
|
|
5769
|
+
Scheduled for: ${result.scheduledAt}
|
|
5770
|
+
Recipients: ${result.recipientCount}`
|
|
5771
|
+
}
|
|
5772
|
+
]
|
|
5773
|
+
};
|
|
5774
|
+
}
|
|
5775
|
+
);
|
|
5776
|
+
server2.tool(
|
|
5777
|
+
"sg_authenticate_domain",
|
|
5778
|
+
"Start domain authentication with SendGrid. Returns DNS records that need to be added to your domain's DNS settings.",
|
|
5779
|
+
{
|
|
5780
|
+
domain: z19.string().min(1).describe("Domain to authenticate (e.g., 'example.com').")
|
|
5781
|
+
},
|
|
5782
|
+
{
|
|
5783
|
+
title: "Authenticate Domain",
|
|
5784
|
+
readOnlyHint: false,
|
|
5785
|
+
destructiveHint: false,
|
|
5786
|
+
idempotentHint: false,
|
|
5787
|
+
openWorldHint: true
|
|
5788
|
+
},
|
|
5789
|
+
async ({ domain }) => {
|
|
5790
|
+
const result = await client2.sgAuthenticateDomain(domain);
|
|
5791
|
+
const lines = [
|
|
5792
|
+
`Domain authentication started for **${result.domain}**.`,
|
|
5793
|
+
`Domain ID: ${result.id}`,
|
|
5794
|
+
"",
|
|
5795
|
+
"Add these DNS records to your domain:",
|
|
5796
|
+
"```",
|
|
5797
|
+
JSON.stringify(result.dnsRecords, null, 2),
|
|
5798
|
+
"```",
|
|
5799
|
+
"",
|
|
5800
|
+
"After adding the records, use sg_validate_domain to verify."
|
|
5801
|
+
];
|
|
5802
|
+
return {
|
|
5803
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
5804
|
+
};
|
|
5805
|
+
}
|
|
5806
|
+
);
|
|
5807
|
+
server2.tool(
|
|
5808
|
+
"sg_validate_domain",
|
|
5809
|
+
"Validate DNS records for a domain that was previously set up for authentication.",
|
|
5810
|
+
{
|
|
5811
|
+
domain_id: z19.number().int().describe("Domain ID from sg_authenticate_domain.")
|
|
5812
|
+
},
|
|
5813
|
+
{
|
|
5814
|
+
title: "Validate Domain",
|
|
5815
|
+
readOnlyHint: true,
|
|
5816
|
+
destructiveHint: false,
|
|
5817
|
+
idempotentHint: true,
|
|
5818
|
+
openWorldHint: true
|
|
5819
|
+
},
|
|
5820
|
+
async ({ domain_id }) => {
|
|
5821
|
+
const result = await client2.sgValidateDomain(domain_id);
|
|
5822
|
+
if (result.valid) {
|
|
5823
|
+
return {
|
|
5824
|
+
content: [
|
|
5825
|
+
{
|
|
5826
|
+
type: "text",
|
|
5827
|
+
text: "Domain validation successful! Your domain is now authenticated for sending."
|
|
5828
|
+
}
|
|
5829
|
+
]
|
|
5830
|
+
};
|
|
5831
|
+
}
|
|
5832
|
+
return {
|
|
5833
|
+
content: [
|
|
5834
|
+
{
|
|
5835
|
+
type: "text",
|
|
5836
|
+
text: `Domain validation failed. Check your DNS records.
|
|
5837
|
+
|
|
5838
|
+
Results:
|
|
5839
|
+
${JSON.stringify(result.validationResults, null, 2)}`
|
|
5840
|
+
}
|
|
5841
|
+
]
|
|
5842
|
+
};
|
|
5843
|
+
}
|
|
5844
|
+
);
|
|
5845
|
+
server2.tool(
|
|
5846
|
+
"sg_list_domains",
|
|
5847
|
+
"List all authenticated sending domains.",
|
|
5848
|
+
{},
|
|
5849
|
+
{
|
|
5850
|
+
title: "List Authenticated Domains",
|
|
5851
|
+
readOnlyHint: true,
|
|
5852
|
+
destructiveHint: false,
|
|
5853
|
+
idempotentHint: true,
|
|
5854
|
+
openWorldHint: true
|
|
5855
|
+
},
|
|
5856
|
+
async () => {
|
|
5857
|
+
const domains = await client2.sgListDomains();
|
|
5858
|
+
if (!domains || domains.length === 0) {
|
|
5859
|
+
return {
|
|
5860
|
+
content: [
|
|
5861
|
+
{
|
|
5862
|
+
type: "text",
|
|
5863
|
+
text: "No authenticated domains found. Use sg_authenticate_domain to set one up."
|
|
5864
|
+
}
|
|
5865
|
+
]
|
|
5866
|
+
};
|
|
5867
|
+
}
|
|
5868
|
+
const lines = [`## Authenticated Domains (${domains.length})`];
|
|
5869
|
+
for (const d of domains) {
|
|
5870
|
+
lines.push(
|
|
5871
|
+
`- **${d.domain}** (ID: ${d.id}) \u2014 ${d.valid ? "Valid" : "Not verified"}`
|
|
5872
|
+
);
|
|
5873
|
+
}
|
|
5874
|
+
return {
|
|
5875
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
5876
|
+
};
|
|
5877
|
+
}
|
|
5878
|
+
);
|
|
5879
|
+
}
|
|
5880
|
+
|
|
5881
|
+
// src/index.ts
|
|
5882
|
+
var apiKey = process.env.BUZZPOSTER_API_KEY;
|
|
5883
|
+
var apiUrl = process.env.BUZZPOSTER_API_URL ?? "https://api.buzzposter.com";
|
|
5884
|
+
if (!apiKey) {
|
|
5885
|
+
console.error(
|
|
5886
|
+
"Error: BUZZPOSTER_API_KEY environment variable is required."
|
|
5887
|
+
);
|
|
5888
|
+
console.error(
|
|
5889
|
+
"Set it in your Claude Desktop MCP config or export it in your shell."
|
|
5890
|
+
);
|
|
5891
|
+
process.exit(1);
|
|
5892
|
+
}
|
|
5893
|
+
var client = new BuzzPosterClient({ baseUrl: apiUrl, apiKey });
|
|
5894
|
+
var server = new McpServer(
|
|
5895
|
+
{
|
|
5896
|
+
name: "buzzposter",
|
|
5897
|
+
version: "0.1.0"
|
|
5898
|
+
},
|
|
5899
|
+
{
|
|
5900
|
+
instructions: `When a tool response contains \`NEWSLETTER_HTML_PREVIEW:\` followed by HTML content, immediately render that HTML as an artifact so the user sees a live visual preview of the newsletter. Do not ask the user if they want to see it -- just render it automatically.`
|
|
5901
|
+
}
|
|
5902
|
+
);
|
|
4296
5903
|
var allowDirectSend = false;
|
|
4297
5904
|
try {
|
|
4298
5905
|
const account = await client.getAccount();
|
|
@@ -4318,5 +5925,8 @@ registerAuditLogTools(server, client);
|
|
|
4318
5925
|
registerSourceTools(server, client);
|
|
4319
5926
|
registerNewsletterAdvancedTools(server, client);
|
|
4320
5927
|
registerCarouselTools(server, client);
|
|
5928
|
+
registerQueueTools(server, client);
|
|
5929
|
+
registerCanvaTools(server, client);
|
|
5930
|
+
registerSendGridNewsletterTools(server, client, { allowDirectSend });
|
|
4321
5931
|
var transport = new StdioServerTransport();
|
|
4322
5932
|
await server.connect(transport);
|