@buzzposter/mcp 0.1.12 → 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/tools.js CHANGED
@@ -95,23 +95,6 @@ var BuzzPosterClient = class {
95
95
  });
96
96
  }
97
97
  // Media
98
- async uploadMediaMultipart(filename, buffer, mimeType) {
99
- const formData = new FormData();
100
- formData.append("file", new Blob([buffer], { type: mimeType }), filename);
101
- const url = new URL(`${this.baseUrl}/api/v1/media/upload`);
102
- const res = await fetch(url, {
103
- method: "POST",
104
- headers: { Authorization: `Bearer ${this.apiKey}` },
105
- body: formData
106
- });
107
- if (!res.ok) {
108
- const errorBody = await res.json().catch(() => ({ message: res.statusText }));
109
- const message = typeof errorBody === "object" && errorBody !== null && "message" in errorBody ? String(errorBody.message) : `API error (${res.status})`;
110
- throw new Error(`BuzzPoster API error (${res.status}): ${message}`);
111
- }
112
- const text = await res.text();
113
- return text ? JSON.parse(text) : void 0;
114
- }
115
98
  async uploadFromUrl(data) {
116
99
  return this.request("POST", "/api/v1/media/upload-from-url", data);
117
100
  }
@@ -262,6 +245,12 @@ var BuzzPosterClient = class {
262
245
  async updateAudience(id, data) {
263
246
  return this.request("PUT", `/api/v1/audiences/${id}`, data);
264
247
  }
248
+ async listAudiences() {
249
+ return this.request("GET", "/api/v1/audiences");
250
+ }
251
+ async deleteAudience(id) {
252
+ return this.request("DELETE", `/api/v1/audiences/${id}`);
253
+ }
265
254
  // Newsletter Templates
266
255
  async getTemplate(id) {
267
256
  if (id) return this.request("GET", `/api/v1/templates/${id}`);
@@ -270,6 +259,18 @@ var BuzzPosterClient = class {
270
259
  async listTemplates() {
271
260
  return this.request("GET", "/api/v1/templates");
272
261
  }
262
+ async createTemplate(data) {
263
+ return this.request("POST", "/api/v1/templates", data);
264
+ }
265
+ async updateTemplate(id, data) {
266
+ return this.request("PUT", "/api/v1/templates/" + id, data);
267
+ }
268
+ async deleteTemplate(id) {
269
+ return this.request("DELETE", "/api/v1/templates/" + id);
270
+ }
271
+ async duplicateTemplate(id) {
272
+ return this.request("POST", "/api/v1/templates/" + id + "/duplicate");
273
+ }
273
274
  // Newsletter Archive
274
275
  async listNewsletterArchive(params) {
275
276
  return this.request("GET", "/api/v1/newsletter-archive", void 0, params);
@@ -373,6 +374,92 @@ var BuzzPosterClient = class {
373
374
  async previewCarousel(data) {
374
375
  return this.request("POST", "/api/v1/carousel-templates/preview", data);
375
376
  }
377
+ // Canva
378
+ async getCanvaStatus() {
379
+ return this.request("GET", "/api/v1/canva/status");
380
+ }
381
+ async canvaUploadAsset(data) {
382
+ return this.request("POST", "/api/v1/canva/upload-asset", data);
383
+ }
384
+ async canvaCreateDesign(data) {
385
+ return this.request("POST", "/api/v1/canva/designs", data);
386
+ }
387
+ async canvaExportDesign(data) {
388
+ return this.request("POST", "/api/v1/canva/exports", data);
389
+ }
390
+ async canvaSearchDesigns(query) {
391
+ return this.request("GET", "/api/v1/canva/designs", void 0, { query });
392
+ }
393
+ async canvaGetDesign(designId) {
394
+ return this.request("GET", `/api/v1/canva/designs/${designId}`);
395
+ }
396
+ // SendGrid Newsletter (BuzzPoster-managed)
397
+ async sgListLists() {
398
+ return this.request("GET", "/api/v1/newsletter/lists");
399
+ }
400
+ async sgCreateList(data) {
401
+ return this.request("POST", "/api/v1/newsletter/lists", data);
402
+ }
403
+ async sgUpdateList(id, data) {
404
+ return this.request("PUT", `/api/v1/newsletter/lists/${id}`, data);
405
+ }
406
+ async sgDeleteList(id) {
407
+ return this.request("DELETE", `/api/v1/newsletter/lists/${id}`);
408
+ }
409
+ async sgListSubscribers(params) {
410
+ return this.request("GET", "/api/v1/newsletter/subscribers", void 0, params);
411
+ }
412
+ async sgAddSubscriber(data) {
413
+ return this.request("POST", "/api/v1/newsletter/subscribers", data);
414
+ }
415
+ async sgGetSubscriber(id) {
416
+ return this.request("GET", `/api/v1/newsletter/subscribers/${id}`);
417
+ }
418
+ async sgUpdateSubscriber(id, data) {
419
+ return this.request("PUT", `/api/v1/newsletter/subscribers/${id}`, data);
420
+ }
421
+ async sgRemoveSubscriber(id) {
422
+ return this.request("DELETE", `/api/v1/newsletter/subscribers/${id}`);
423
+ }
424
+ async sgBulkImportSubscribers(data) {
425
+ return this.request("POST", "/api/v1/newsletter/subscribers/bulk", data);
426
+ }
427
+ async sgListSends(params) {
428
+ return this.request("GET", "/api/v1/newsletter/sends", void 0, params);
429
+ }
430
+ async sgGetSend(id) {
431
+ return this.request("GET", `/api/v1/newsletter/sends/${id}`);
432
+ }
433
+ async sgCreateSend(data) {
434
+ return this.request("POST", "/api/v1/newsletter/sends", data);
435
+ }
436
+ async sgUpdateSend(id, data) {
437
+ return this.request("PUT", `/api/v1/newsletter/sends/${id}`, data);
438
+ }
439
+ async sgDeleteSend(id) {
440
+ return this.request("DELETE", `/api/v1/newsletter/sends/${id}`);
441
+ }
442
+ async sgSendNewsletter(id) {
443
+ return this.request("POST", `/api/v1/newsletter/sends/${id}/send`);
444
+ }
445
+ async sgTestSend(id, data) {
446
+ return this.request("POST", `/api/v1/newsletter/sends/${id}/test`, data);
447
+ }
448
+ async sgScheduleSend(id, data) {
449
+ return this.request("POST", `/api/v1/newsletter/sends/${id}/schedule`, data);
450
+ }
451
+ async sgCancelSend(id) {
452
+ return this.request("POST", `/api/v1/newsletter/sends/${id}/cancel`);
453
+ }
454
+ async sgAuthenticateDomain(domain) {
455
+ return this.request("POST", "/api/v1/newsletter/domains/authenticate", { domain });
456
+ }
457
+ async sgValidateDomain(domainId) {
458
+ return this.request("POST", `/api/v1/newsletter/domains/${domainId}/validate`);
459
+ }
460
+ async sgListDomains() {
461
+ return this.request("GET", "/api/v1/newsletter/domains");
462
+ }
376
463
  };
377
464
 
378
465
  // src/tools/posts.ts
@@ -1155,44 +1242,7 @@ This will post a public reply to the review. Call this tool again with confirmed
1155
1242
 
1156
1243
  // src/tools/media.ts
1157
1244
  import { z as z4 } from "zod";
1158
- import { readFile } from "fs/promises";
1159
1245
  function registerMediaTools(server, client) {
1160
- server.tool(
1161
- "upload_media",
1162
- "Upload an image or video file from a local file path. Returns a CDN URL that can be used in posts.",
1163
- {
1164
- file_path: z4.string().describe("Absolute path to the file on the local filesystem")
1165
- },
1166
- {
1167
- title: "Upload Media File",
1168
- readOnlyHint: false,
1169
- destructiveHint: false,
1170
- idempotentHint: false,
1171
- openWorldHint: false
1172
- },
1173
- async (args) => {
1174
- const buffer = Buffer.from(await readFile(args.file_path));
1175
- const filename = args.file_path.split("/").pop() ?? "upload";
1176
- const ext = filename.split(".").pop()?.toLowerCase() ?? "";
1177
- const mimeMap = {
1178
- jpg: "image/jpeg",
1179
- jpeg: "image/jpeg",
1180
- png: "image/png",
1181
- gif: "image/gif",
1182
- webp: "image/webp",
1183
- mp4: "video/mp4",
1184
- mov: "video/quicktime",
1185
- avi: "video/x-msvideo",
1186
- webm: "video/webm",
1187
- pdf: "application/pdf"
1188
- };
1189
- const mimeType = mimeMap[ext] ?? "application/octet-stream";
1190
- const result = await client.uploadMediaMultipart(filename, buffer, mimeType);
1191
- return {
1192
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1193
- };
1194
- }
1195
- );
1196
1246
  server.tool(
1197
1247
  "upload_from_url",
1198
1248
  "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.",
@@ -1214,8 +1264,12 @@ function registerMediaTools(server, client) {
1214
1264
  filename: args.filename,
1215
1265
  folder: args.folder
1216
1266
  });
1267
+ const item = result;
1217
1268
  return {
1218
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1269
+ content: [
1270
+ { type: "text", text: JSON.stringify(result, null, 2) },
1271
+ ...item.type === "image" && item.url && item.mimeType && !item.mimeType.startsWith("video/") ? [{ type: "image", url: item.url, mimeType: item.mimeType }] : []
1272
+ ]
1219
1273
  };
1220
1274
  }
1221
1275
  );
@@ -1256,8 +1310,13 @@ function registerMediaTools(server, client) {
1256
1310
  },
1257
1311
  async () => {
1258
1312
  const result = await client.listMedia();
1313
+ const media = Array.isArray(result) ? result : result?.media ?? [];
1314
+ 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 }));
1259
1315
  return {
1260
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1316
+ content: [
1317
+ { type: "text", text: JSON.stringify(result, null, 2) },
1318
+ ...imageBlocks
1319
+ ]
1261
1320
  };
1262
1321
  }
1263
1322
  );
@@ -1372,7 +1431,11 @@ function registerNewsletterTools(server, client, options = {}) {
1372
1431
  previewText: args.preview_text
1373
1432
  });
1374
1433
  return {
1375
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1434
+ content: [
1435
+ { type: "text", text: JSON.stringify(result, null, 2) },
1436
+ ...args.content ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
1437
+ ${args.content}` }] : []
1438
+ ]
1376
1439
  };
1377
1440
  }
1378
1441
  );
@@ -1399,7 +1462,11 @@ function registerNewsletterTools(server, client, options = {}) {
1399
1462
  if (args.preview_text) data.previewText = args.preview_text;
1400
1463
  const result = await client.updateBroadcast(args.broadcast_id, data);
1401
1464
  return {
1402
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1465
+ content: [
1466
+ { type: "text", text: JSON.stringify(result, null, 2) },
1467
+ ...args.content ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
1468
+ ${args.content}` }] : []
1469
+ ]
1403
1470
  };
1404
1471
  }
1405
1472
  );
@@ -1574,9 +1641,13 @@ Call this tool again with confirmed=true to schedule.`;
1574
1641
  );
1575
1642
  server.tool(
1576
1643
  "list_newsletters",
1577
- "List all newsletters/broadcasts with their status.",
1644
+ "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.",
1578
1645
  {
1579
- page: z5.string().optional().describe("Page number for pagination")
1646
+ page: z5.string().optional().describe("Page number for pagination"),
1647
+ status: z5.enum(["draft", "sent", "scheduled"]).optional().describe("Filter by status: draft, sent, or scheduled"),
1648
+ from_date: z5.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
1649
+ to_date: z5.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
1650
+ subject: z5.string().optional().describe("Keyword to search in subject lines (case-insensitive)")
1580
1651
  },
1581
1652
  {
1582
1653
  title: "List Newsletters",
@@ -1588,9 +1659,31 @@ Call this tool again with confirmed=true to schedule.`;
1588
1659
  async (args) => {
1589
1660
  const params = {};
1590
1661
  if (args.page) params.page = args.page;
1662
+ if (args.status) params.status = args.status;
1663
+ if (args.from_date) params.fromDate = args.from_date;
1664
+ if (args.to_date) params.toDate = args.to_date;
1665
+ if (args.subject) params.subject = args.subject;
1591
1666
  const result = await client.listBroadcasts(params);
1667
+ const broadcasts = result?.broadcasts ?? [];
1668
+ if (broadcasts.length === 0) {
1669
+ return {
1670
+ content: [{ type: "text", text: "No newsletters found matching your filters." }]
1671
+ };
1672
+ }
1673
+ let text = `## Newsletters (${broadcasts.length}`;
1674
+ if (result?.totalCount != null) text += ` of ${result.totalCount}`;
1675
+ text += ")\n\n";
1676
+ for (const b of broadcasts) {
1677
+ const status = (b.status ?? "unknown").toUpperCase();
1678
+ const date = b.sentAt ? new Date(b.sentAt).toLocaleString() : b.createdAt ? new Date(b.createdAt).toLocaleString() : "";
1679
+ text += `- **[${status}]** "${b.subject ?? "(no subject)"}"
1680
+ `;
1681
+ text += ` ID: ${b.id}`;
1682
+ if (date) text += ` | ${b.sentAt ? "Sent" : "Created"}: ${date}`;
1683
+ text += "\n";
1684
+ }
1592
1685
  return {
1593
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1686
+ content: [{ type: "text", text }]
1594
1687
  };
1595
1688
  }
1596
1689
  );
@@ -3040,6 +3133,89 @@ USAGE GUIDELINES:
3040
3133
  };
3041
3134
  }
3042
3135
  );
3136
+ server.tool(
3137
+ "delete_audience",
3138
+ `Permanently delete a target audience profile by ID. This action cannot be undone.
3139
+
3140
+ REQUIRED WORKFLOW:
3141
+ 1. ALWAYS call with confirmed=false first to preview which audience will be deleted
3142
+ 2. Show the user the audience name and details
3143
+ 3. Only call again with confirmed=true after explicit user approval
3144
+
3145
+ SAFETY: Cannot delete the default audience if it is the only audience profile. The customer must create another audience first.`,
3146
+ {
3147
+ audience_id: z10.string().describe("The ID of the audience profile to delete"),
3148
+ confirmed: z10.boolean().default(false).describe(
3149
+ "Set to true to confirm deletion after previewing. If false or missing, returns a preview for user approval."
3150
+ )
3151
+ },
3152
+ {
3153
+ title: "Delete Audience Profile",
3154
+ readOnlyHint: false,
3155
+ destructiveHint: true,
3156
+ idempotentHint: true,
3157
+ openWorldHint: false
3158
+ },
3159
+ async (args) => {
3160
+ let audience;
3161
+ try {
3162
+ audience = await client.getAudience(args.audience_id);
3163
+ } catch (error) {
3164
+ const message = error instanceof Error ? error.message : "Unknown error";
3165
+ if (message.includes("404") || message.includes("not found")) {
3166
+ return {
3167
+ content: [
3168
+ {
3169
+ type: "text",
3170
+ text: `Audience with ID ${args.audience_id} not found.`
3171
+ }
3172
+ ],
3173
+ isError: true
3174
+ };
3175
+ }
3176
+ throw error;
3177
+ }
3178
+ if (audience.isDefault) {
3179
+ const allAudiences = await client.listAudiences();
3180
+ if (allAudiences.length <= 1) {
3181
+ return {
3182
+ content: [
3183
+ {
3184
+ type: "text",
3185
+ text: `Cannot delete "${audience.name}" because it is the only audience profile. Create another audience before deleting this one.`
3186
+ }
3187
+ ],
3188
+ isError: true
3189
+ };
3190
+ }
3191
+ }
3192
+ if (args.confirmed !== true) {
3193
+ const lines = [];
3194
+ lines.push("## Delete Audience Confirmation");
3195
+ lines.push("");
3196
+ lines.push(`**Name:** ${audience.name}`);
3197
+ lines.push(`**ID:** ${audience.id}`);
3198
+ if (audience.isDefault) lines.push("**Default:** Yes");
3199
+ if (audience.description) lines.push(`**Description:** ${audience.description}`);
3200
+ lines.push("");
3201
+ lines.push(
3202
+ "**This action is permanent and cannot be undone.** Call this tool again with confirmed=true to proceed."
3203
+ );
3204
+ return {
3205
+ content: [{ type: "text", text: lines.join("\n") }]
3206
+ };
3207
+ }
3208
+ await client.deleteAudience(args.audience_id);
3209
+ return {
3210
+ content: [
3211
+ {
3212
+ type: "text",
3213
+ text: `Audience "${audience.name}" (ID: ${audience.id}) has been permanently deleted.`
3214
+ }
3215
+ ]
3216
+ };
3217
+ }
3218
+ );
3043
3219
  }
3044
3220
 
3045
3221
  // src/tools/newsletter-template.ts
@@ -3178,6 +3354,15 @@ function registerNewsletterTemplateTools(server, client) {
3178
3354
  );
3179
3355
  lines.push("");
3180
3356
  }
3357
+ if (template.htmlContent) {
3358
+ lines.push("### HTML Template:");
3359
+ lines.push("The following HTML skeleton contains {{placeholder}} variables. Fill in the placeholders with real content when generating the newsletter.");
3360
+ lines.push("");
3361
+ lines.push("```html");
3362
+ lines.push(template.htmlContent);
3363
+ lines.push("```");
3364
+ lines.push("");
3365
+ }
3181
3366
  if (template.rssFeedUrls && template.rssFeedUrls.length > 0) {
3182
3367
  lines.push("### Linked RSS Feeds:");
3183
3368
  for (const url of template.rssFeedUrls) {
@@ -3268,8 +3453,13 @@ function registerNewsletterTemplateTools(server, client) {
3268
3453
  );
3269
3454
  lines.push("");
3270
3455
  }
3456
+ const mostRecent = newsletters[0];
3271
3457
  return {
3272
- content: [{ type: "text", text: lines.join("\n") }]
3458
+ content: [
3459
+ { type: "text", text: lines.join("\n") },
3460
+ ...mostRecent?.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
3461
+ ${mostRecent.contentHtml}` }] : []
3462
+ ]
3273
3463
  };
3274
3464
  } catch (error) {
3275
3465
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -3303,7 +3493,11 @@ function registerNewsletterTemplateTools(server, client) {
3303
3493
  lines.push("### Full HTML Content:");
3304
3494
  lines.push(newsletter.contentHtml);
3305
3495
  return {
3306
- content: [{ type: "text", text: lines.join("\n") }]
3496
+ content: [
3497
+ { type: "text", text: lines.join("\n") },
3498
+ ...newsletter.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
3499
+ ${newsletter.contentHtml}` }] : []
3500
+ ]
3307
3501
  };
3308
3502
  } catch (error) {
3309
3503
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -3353,7 +3547,9 @@ function registerNewsletterTemplateTools(server, client) {
3353
3547
  {
3354
3548
  type: "text",
3355
3549
  text: `Newsletter "${subject}" has been saved to the archive successfully.`
3356
- }
3550
+ },
3551
+ ...contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
3552
+ ${contentHtml}` }] : []
3357
3553
  ]
3358
3554
  };
3359
3555
  } catch (error) {
@@ -3362,6 +3558,210 @@ function registerNewsletterTemplateTools(server, client) {
3362
3558
  }
3363
3559
  }
3364
3560
  );
3561
+ server.tool(
3562
+ "save_newsletter_template",
3563
+ "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.",
3564
+ {
3565
+ template_id: z11.string().optional().describe(
3566
+ "If provided, updates this existing template. Otherwise creates a new one."
3567
+ ),
3568
+ name: z11.string().min(1).max(255).describe("Template name, e.g. 'BOQtoday Daily'"),
3569
+ description: z11.string().max(2e3).optional().describe(
3570
+ "What this template is for, e.g. 'Local news digest with 6 stories, events, pet of the week'"
3571
+ ),
3572
+ html_content: z11.string().min(1).describe(
3573
+ "Full HTML template with {{placeholder}} variables. This is the email body HTML with inline styles."
3574
+ ),
3575
+ sections: z11.array(
3576
+ z11.object({
3577
+ name: z11.string().describe("Section identifier, e.g. 'intro'"),
3578
+ label: z11.string().describe("Display label, e.g. 'Intro/Greeting'"),
3579
+ required: z11.boolean().optional().describe("Whether this section is required"),
3580
+ item_count: z11.number().optional().describe("Number of items in this section, if applicable")
3581
+ })
3582
+ ).optional().describe("Array of section metadata describing the template structure"),
3583
+ style_config: z11.object({
3584
+ primary_color: z11.string().optional(),
3585
+ accent_color: z11.string().optional(),
3586
+ link_color: z11.string().optional(),
3587
+ background_color: z11.string().optional(),
3588
+ font_family: z11.string().optional(),
3589
+ max_width: z11.string().optional()
3590
+ }).optional().describe("Colors, fonts, and brand configuration"),
3591
+ is_default: z11.boolean().optional().describe(
3592
+ "Set as the default template for this customer. Only one default per customer."
3593
+ )
3594
+ },
3595
+ {
3596
+ title: "Save Newsletter Template",
3597
+ readOnlyHint: false,
3598
+ destructiveHint: false,
3599
+ idempotentHint: false,
3600
+ openWorldHint: false
3601
+ },
3602
+ async ({
3603
+ template_id,
3604
+ name,
3605
+ description,
3606
+ html_content,
3607
+ sections,
3608
+ style_config,
3609
+ is_default
3610
+ }) => {
3611
+ try {
3612
+ const data = {
3613
+ name,
3614
+ htmlContent: html_content
3615
+ };
3616
+ if (description !== void 0) data.description = description;
3617
+ if (sections !== void 0) {
3618
+ data.sections = sections.map((s) => ({
3619
+ id: s.name,
3620
+ type: "custom",
3621
+ label: s.label,
3622
+ required: s.required ?? true,
3623
+ count: s.item_count
3624
+ }));
3625
+ }
3626
+ if (style_config !== void 0) {
3627
+ data.style = {
3628
+ primary_color: style_config.primary_color,
3629
+ accent_color: style_config.accent_color,
3630
+ link_color: style_config.link_color,
3631
+ background_color: style_config.background_color,
3632
+ font_family: style_config.font_family,
3633
+ content_width: style_config.max_width
3634
+ };
3635
+ }
3636
+ if (is_default !== void 0) data.isDefault = is_default;
3637
+ let result;
3638
+ if (template_id) {
3639
+ result = await client.updateTemplate(
3640
+ template_id,
3641
+ data
3642
+ );
3643
+ } else {
3644
+ result = await client.createTemplate(data);
3645
+ }
3646
+ const action = template_id ? "updated" : "created";
3647
+ return {
3648
+ content: [
3649
+ {
3650
+ type: "text",
3651
+ text: `Newsletter template "${result.name}" ${action} successfully (ID: ${result.id}).${result.isDefault ? " Set as default." : ""}`
3652
+ }
3653
+ ]
3654
+ };
3655
+ } catch (error) {
3656
+ const message = error instanceof Error ? error.message : "Unknown error";
3657
+ throw new Error(`Failed to save newsletter template: ${message}`);
3658
+ }
3659
+ }
3660
+ );
3661
+ server.tool(
3662
+ "list_newsletter_templates",
3663
+ "List all saved newsletter templates for this customer. Returns template names, descriptions, whether they have HTML content, and which one is the default.",
3664
+ {},
3665
+ {
3666
+ title: "List Newsletter Templates",
3667
+ readOnlyHint: true,
3668
+ destructiveHint: false,
3669
+ idempotentHint: true,
3670
+ openWorldHint: false
3671
+ },
3672
+ async () => {
3673
+ try {
3674
+ const templates = await client.listTemplates();
3675
+ if (!templates || templates.length === 0) {
3676
+ return {
3677
+ content: [
3678
+ {
3679
+ type: "text",
3680
+ text: "No newsletter templates found. Use save_newsletter_template to create one."
3681
+ }
3682
+ ]
3683
+ };
3684
+ }
3685
+ const lines = [];
3686
+ lines.push(`## Newsletter Templates (${templates.length})`);
3687
+ lines.push("");
3688
+ for (const t of templates) {
3689
+ const badges = [];
3690
+ if (t.isDefault) badges.push("DEFAULT");
3691
+ if (t.htmlContent) badges.push("HAS HTML");
3692
+ const badgeStr = badges.length > 0 ? ` [${badges.join(", ")}]` : "";
3693
+ lines.push(`**${t.name}** (ID: ${t.id})${badgeStr}`);
3694
+ if (t.description) lines.push(` ${t.description}`);
3695
+ if (t.sections && t.sections.length > 0) {
3696
+ lines.push(
3697
+ ` Sections: ${t.sections.map((s) => s.label).join(", ")}`
3698
+ );
3699
+ }
3700
+ lines.push("");
3701
+ }
3702
+ return {
3703
+ content: [{ type: "text", text: lines.join("\n") }]
3704
+ };
3705
+ } catch (error) {
3706
+ const message = error instanceof Error ? error.message : "Unknown error";
3707
+ throw new Error(`Failed to list newsletter templates: ${message}`);
3708
+ }
3709
+ }
3710
+ );
3711
+ server.tool(
3712
+ "delete_newsletter_template",
3713
+ "Delete a newsletter template. Requires confirmation -- set confirmed=true to proceed with deletion.",
3714
+ {
3715
+ template_id: z11.string().describe("The ID of the newsletter template to delete."),
3716
+ confirmed: z11.boolean().default(false).describe(
3717
+ "Must be true to actually delete. If false, returns a confirmation prompt."
3718
+ )
3719
+ },
3720
+ {
3721
+ title: "Delete Newsletter Template",
3722
+ readOnlyHint: false,
3723
+ destructiveHint: true,
3724
+ idempotentHint: false,
3725
+ openWorldHint: false
3726
+ },
3727
+ async ({ template_id, confirmed }) => {
3728
+ try {
3729
+ if (!confirmed) {
3730
+ const template = await client.getTemplate(template_id);
3731
+ return {
3732
+ content: [
3733
+ {
3734
+ type: "text",
3735
+ 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.`
3736
+ }
3737
+ ]
3738
+ };
3739
+ }
3740
+ await client.deleteTemplate(template_id);
3741
+ return {
3742
+ content: [
3743
+ {
3744
+ type: "text",
3745
+ text: `Newsletter template (ID: ${template_id}) has been deleted.`
3746
+ }
3747
+ ]
3748
+ };
3749
+ } catch (error) {
3750
+ const message = error instanceof Error ? error.message : "Unknown error";
3751
+ if (message.includes("404")) {
3752
+ return {
3753
+ content: [
3754
+ {
3755
+ type: "text",
3756
+ text: "Template not found. It may have already been deleted."
3757
+ }
3758
+ ]
3759
+ };
3760
+ }
3761
+ throw new Error(`Failed to delete newsletter template: ${message}`);
3762
+ }
3763
+ }
3764
+ );
3365
3765
  }
3366
3766
 
3367
3767
  // src/tools/calendar.ts
@@ -3486,7 +3886,8 @@ function registerCalendarTools(server, client) {
3486
3886
  for (const slot of slots) {
3487
3887
  const day = dayNames[slot.dayOfWeek] ?? `Day ${slot.dayOfWeek}`;
3488
3888
  const platforms = (slot.platforms ?? []).map((p) => `[${p}]`).join(" ");
3489
- text += `${day}: ${slot.time} ${platforms}
3889
+ const slotId = slot.id ?? slot._id ?? `${slot.dayOfWeek}_${slot.time}`;
3890
+ text += `${day}: ${slot.time} ${platforms} (id: ${slotId})
3490
3891
  `;
3491
3892
  }
3492
3893
  }
@@ -3507,7 +3908,9 @@ Next slot: ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
3507
3908
  );
3508
3909
  server.tool(
3509
3910
  "schedule_to_queue",
3510
- `Add a post to the customer's content queue for automatic scheduling.
3911
+ `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.
3912
+
3913
+ Add a post to the customer's content queue for automatic scheduling.
3511
3914
 
3512
3915
  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.
3513
3916
 
@@ -4238,6 +4641,1236 @@ USAGE:
4238
4641
  }
4239
4642
  );
4240
4643
  }
4644
+
4645
+ // src/tools/queue.ts
4646
+ import { z as z17 } from "zod";
4647
+ var DAY_NAMES = [
4648
+ "sunday",
4649
+ "monday",
4650
+ "tuesday",
4651
+ "wednesday",
4652
+ "thursday",
4653
+ "friday",
4654
+ "saturday"
4655
+ ];
4656
+ var DAY_ENUM = z17.enum([
4657
+ "monday",
4658
+ "tuesday",
4659
+ "wednesday",
4660
+ "thursday",
4661
+ "friday",
4662
+ "saturday",
4663
+ "sunday"
4664
+ ]);
4665
+ function dayNameToNumber(name) {
4666
+ const idx = DAY_NAMES.indexOf(name.toLowerCase());
4667
+ return idx === -1 ? 0 : idx;
4668
+ }
4669
+ function dayNumberToName(num) {
4670
+ return (DAY_NAMES[num] ?? "sunday").charAt(0).toUpperCase() + (DAY_NAMES[num] ?? "sunday").slice(1);
4671
+ }
4672
+ function getSlotId(slot) {
4673
+ if (slot.id) return String(slot.id);
4674
+ if (slot._id) return String(slot._id);
4675
+ return `${slot.dayOfWeek}_${slot.time}`;
4676
+ }
4677
+ function formatSlot(slot) {
4678
+ const day = dayNumberToName(slot.dayOfWeek);
4679
+ const platforms = (slot.platforms ?? []).join(", ");
4680
+ return `${day} ${slot.time} [${platforms}]`;
4681
+ }
4682
+ function registerQueueTools(server, client) {
4683
+ server.tool(
4684
+ "create_queue_slot",
4685
+ "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.",
4686
+ {
4687
+ day_of_week: DAY_ENUM.describe("Day of the week for this slot"),
4688
+ 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'"),
4689
+ timezone: z17.string().default("UTC").describe("IANA timezone, e.g. 'America/New_York'. Defaults to UTC."),
4690
+ platforms: z17.array(z17.string()).min(1).describe('Platforms for this slot, e.g. ["twitter", "linkedin"]'),
4691
+ confirmed: z17.boolean().default(false).describe(
4692
+ "Set to true to confirm creation. If false, returns a preview."
4693
+ )
4694
+ },
4695
+ {
4696
+ title: "Create Queue Slot",
4697
+ readOnlyHint: false,
4698
+ destructiveHint: false,
4699
+ idempotentHint: false,
4700
+ openWorldHint: true
4701
+ },
4702
+ async (args) => {
4703
+ const dayNum = dayNameToNumber(args.day_of_week);
4704
+ if (args.confirmed !== true) {
4705
+ let text = "## Create Queue Slot Preview\n\n";
4706
+ text += `**Day:** ${dayNumberToName(dayNum)}
4707
+ `;
4708
+ text += `**Time:** ${args.time}
4709
+ `;
4710
+ text += `**Timezone:** ${args.timezone}
4711
+ `;
4712
+ text += `**Platforms:** ${args.platforms.join(", ")}
4713
+
4714
+ `;
4715
+ try {
4716
+ const data2 = await client.getQueue();
4717
+ const slots = data2?.slots ?? [];
4718
+ if (slots.length > 0) {
4719
+ text += "### Existing slots\n";
4720
+ for (const slot of slots) {
4721
+ text += `- ${formatSlot(slot)}
4722
+ `;
4723
+ }
4724
+ text += "\n";
4725
+ }
4726
+ } catch {
4727
+ }
4728
+ text += "Call this tool again with confirmed=true to create the slot.";
4729
+ return { content: [{ type: "text", text }] };
4730
+ }
4731
+ const data = await client.getQueue();
4732
+ const existingSlots = data?.slots ?? [];
4733
+ const duplicate = existingSlots.find(
4734
+ (s) => s.dayOfWeek === dayNum && s.time === args.time
4735
+ );
4736
+ if (duplicate) {
4737
+ return {
4738
+ content: [
4739
+ {
4740
+ type: "text",
4741
+ text: `A slot already exists for ${dayNumberToName(dayNum)} at ${args.time}. Use update_queue_slot to modify it.`
4742
+ }
4743
+ ]
4744
+ };
4745
+ }
4746
+ const updatedSlots = [
4747
+ ...existingSlots.map((s) => ({
4748
+ dayOfWeek: s.dayOfWeek,
4749
+ time: s.time,
4750
+ platforms: s.platforms
4751
+ })),
4752
+ {
4753
+ dayOfWeek: dayNum,
4754
+ time: args.time,
4755
+ platforms: args.platforms
4756
+ }
4757
+ ];
4758
+ await client.updateQueue({
4759
+ slots: updatedSlots,
4760
+ active: data?.active,
4761
+ timezone: args.timezone
4762
+ });
4763
+ return {
4764
+ content: [
4765
+ {
4766
+ type: "text",
4767
+ text: `Queue slot created: ${dayNumberToName(dayNum)} at ${args.time} for ${args.platforms.join(", ")}. The queue now has ${updatedSlots.length} slot(s).`
4768
+ }
4769
+ ]
4770
+ };
4771
+ }
4772
+ );
4773
+ server.tool(
4774
+ "update_queue_slot",
4775
+ "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.",
4776
+ {
4777
+ slot_id: z17.string().describe(
4778
+ "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'."
4779
+ ),
4780
+ day_of_week: DAY_ENUM.optional().describe("New day of the week"),
4781
+ 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"),
4782
+ timezone: z17.string().optional().describe("New IANA timezone for the queue"),
4783
+ platforms: z17.array(z17.string()).min(1).optional().describe("New platforms list"),
4784
+ confirmed: z17.boolean().default(false).describe(
4785
+ "Set to true to confirm the update. If false, returns a preview."
4786
+ )
4787
+ },
4788
+ {
4789
+ title: "Update Queue Slot",
4790
+ readOnlyHint: false,
4791
+ destructiveHint: false,
4792
+ idempotentHint: true,
4793
+ openWorldHint: true
4794
+ },
4795
+ async (args) => {
4796
+ const data = await client.getQueue();
4797
+ const slots = data?.slots ?? [];
4798
+ const slotIndex = slots.findIndex(
4799
+ (s) => getSlotId(s) === args.slot_id
4800
+ );
4801
+ if (slotIndex === -1) {
4802
+ const available = slots.map((s) => `${getSlotId(s)} (${formatSlot(s)})`).join("\n- ");
4803
+ return {
4804
+ content: [
4805
+ {
4806
+ type: "text",
4807
+ text: `Slot "${args.slot_id}" not found.
4808
+
4809
+ Available slots:
4810
+ - ${available || "(none)"}`
4811
+ }
4812
+ ]
4813
+ };
4814
+ }
4815
+ const current = slots[slotIndex];
4816
+ const newDay = args.day_of_week !== void 0 ? dayNameToNumber(args.day_of_week) : current.dayOfWeek;
4817
+ const newTime = args.time ?? current.time;
4818
+ const newPlatforms = args.platforms ?? current.platforms;
4819
+ if (args.confirmed !== true) {
4820
+ let text = "## Update Queue Slot Preview\n\n";
4821
+ text += `**Current:** ${formatSlot(current)}
4822
+ `;
4823
+ text += `**Updated:** ${dayNumberToName(newDay)} ${newTime} [${(newPlatforms ?? []).join(", ")}]
4824
+ `;
4825
+ if (args.timezone) text += `**Timezone:** ${args.timezone}
4826
+ `;
4827
+ text += "\nCall this tool again with confirmed=true to apply the update.";
4828
+ return { content: [{ type: "text", text }] };
4829
+ }
4830
+ const updatedSlots = slots.map((s, i) => {
4831
+ if (i === slotIndex) {
4832
+ return {
4833
+ dayOfWeek: newDay,
4834
+ time: newTime,
4835
+ platforms: newPlatforms
4836
+ };
4837
+ }
4838
+ return {
4839
+ dayOfWeek: s.dayOfWeek,
4840
+ time: s.time,
4841
+ platforms: s.platforms
4842
+ };
4843
+ });
4844
+ await client.updateQueue({
4845
+ slots: updatedSlots,
4846
+ active: data?.active,
4847
+ ...args.timezone ? { timezone: args.timezone } : {}
4848
+ });
4849
+ return {
4850
+ content: [
4851
+ {
4852
+ type: "text",
4853
+ text: `Slot updated: ${dayNumberToName(newDay)} at ${newTime} for ${(newPlatforms ?? []).join(", ")}.`
4854
+ }
4855
+ ]
4856
+ };
4857
+ }
4858
+ );
4859
+ server.tool(
4860
+ "delete_queue_slot",
4861
+ "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.",
4862
+ {
4863
+ slot_id: z17.string().describe(
4864
+ "ID of the slot to delete (shown by get_queue)."
4865
+ ),
4866
+ confirmed: z17.boolean().default(false).describe(
4867
+ "Set to true to confirm deletion. If false, shows slot details for review."
4868
+ )
4869
+ },
4870
+ {
4871
+ title: "Delete Queue Slot",
4872
+ readOnlyHint: false,
4873
+ destructiveHint: true,
4874
+ idempotentHint: true,
4875
+ openWorldHint: true
4876
+ },
4877
+ async (args) => {
4878
+ const data = await client.getQueue();
4879
+ const slots = data?.slots ?? [];
4880
+ const slotIndex = slots.findIndex(
4881
+ (s) => getSlotId(s) === args.slot_id
4882
+ );
4883
+ if (slotIndex === -1) {
4884
+ const available = slots.map((s) => `${getSlotId(s)} (${formatSlot(s)})`).join("\n- ");
4885
+ return {
4886
+ content: [
4887
+ {
4888
+ type: "text",
4889
+ text: `Slot "${args.slot_id}" not found.
4890
+
4891
+ Available slots:
4892
+ - ${available || "(none)"}`
4893
+ }
4894
+ ]
4895
+ };
4896
+ }
4897
+ const target = slots[slotIndex];
4898
+ if (args.confirmed !== true) {
4899
+ let text = "## Delete Queue Slot\n\n";
4900
+ text += `**Slot:** ${formatSlot(target)}
4901
+ `;
4902
+ text += `**ID:** ${getSlotId(target)}
4903
+
4904
+ `;
4905
+ text += `This will remove this recurring time slot. Posts already scheduled are not affected.
4906
+
4907
+ `;
4908
+ text += `The queue will have ${slots.length - 1} slot(s) remaining.
4909
+
4910
+ `;
4911
+ text += "Call this tool again with confirmed=true to delete.";
4912
+ return { content: [{ type: "text", text }] };
4913
+ }
4914
+ const updatedSlots = slots.filter((_, i) => i !== slotIndex).map((s) => ({
4915
+ dayOfWeek: s.dayOfWeek,
4916
+ time: s.time,
4917
+ platforms: s.platforms
4918
+ }));
4919
+ await client.updateQueue({
4920
+ slots: updatedSlots,
4921
+ active: data?.active
4922
+ });
4923
+ return {
4924
+ content: [
4925
+ {
4926
+ type: "text",
4927
+ text: `Slot deleted: ${formatSlot(target)}. The queue now has ${updatedSlots.length} slot(s).`
4928
+ }
4929
+ ]
4930
+ };
4931
+ }
4932
+ );
4933
+ server.tool(
4934
+ "toggle_queue",
4935
+ `Enable or disable the entire posting queue. When disabled, no posts will be auto-scheduled to queue slots. Existing scheduled posts are not affected.
4936
+
4937
+ If the user tries to schedule_to_queue while the queue is inactive, warn them and suggest enabling it with this tool first.`,
4938
+ {
4939
+ active: z17.boolean().describe("true to enable the queue, false to disable it"),
4940
+ confirmed: z17.boolean().default(false).describe(
4941
+ "Set to true to confirm the change. If false, returns a preview."
4942
+ )
4943
+ },
4944
+ {
4945
+ title: "Toggle Queue",
4946
+ readOnlyHint: false,
4947
+ destructiveHint: false,
4948
+ idempotentHint: true,
4949
+ openWorldHint: true
4950
+ },
4951
+ async (args) => {
4952
+ const data = await client.getQueue();
4953
+ const currentActive = data?.active ?? false;
4954
+ const slots = data?.slots ?? [];
4955
+ if (currentActive === args.active) {
4956
+ return {
4957
+ content: [
4958
+ {
4959
+ type: "text",
4960
+ text: `The queue is already ${args.active ? "enabled" : "disabled"}. No change needed.`
4961
+ }
4962
+ ]
4963
+ };
4964
+ }
4965
+ if (args.confirmed !== true) {
4966
+ let text = `## Toggle Queue
4967
+
4968
+ `;
4969
+ text += `**Current status:** ${currentActive ? "Enabled" : "Disabled"}
4970
+ `;
4971
+ text += `**New status:** ${args.active ? "Enabled" : "Disabled"}
4972
+ `;
4973
+ text += `**Slots configured:** ${slots.length}
4974
+
4975
+ `;
4976
+ if (args.active && slots.length === 0) {
4977
+ text += "**Warning:** Enabling the queue with no slots configured will have no effect. Create slots first with create_queue_slot.\n\n";
4978
+ }
4979
+ if (!args.active && slots.length > 0) {
4980
+ text += "**Note:** Disabling the queue will stop auto-scheduling. Existing scheduled posts will still go out.\n\n";
4981
+ }
4982
+ text += "Call this tool again with confirmed=true to apply.";
4983
+ return { content: [{ type: "text", text }] };
4984
+ }
4985
+ const updatedSlots = slots.map((s) => ({
4986
+ dayOfWeek: s.dayOfWeek,
4987
+ time: s.time,
4988
+ platforms: s.platforms
4989
+ }));
4990
+ await client.updateQueue({
4991
+ slots: updatedSlots,
4992
+ active: args.active
4993
+ });
4994
+ return {
4995
+ content: [
4996
+ {
4997
+ type: "text",
4998
+ 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." : ""}`
4999
+ }
5000
+ ]
5001
+ };
5002
+ }
5003
+ );
5004
+ }
5005
+
5006
+ // src/tools/canva.ts
5007
+ import { z as z18 } from "zod";
5008
+ var CANVA_NOT_CONNECTED = "Canva is not connected. Visit BuzzPoster settings to connect your Canva account.";
5009
+ function isNotConnectedError(error) {
5010
+ if (error instanceof Error) {
5011
+ return error.message.includes("Canva is not connected") || error.message.includes("400");
5012
+ }
5013
+ return false;
5014
+ }
5015
+ function registerCanvaTools(server, client) {
5016
+ server.tool(
5017
+ "canva_upload_asset",
5018
+ "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.",
5019
+ {
5020
+ image_url: z18.string().url().describe("The URL of the image to upload to Canva."),
5021
+ name: z18.string().min(1).max(255).describe("A name for the uploaded asset.")
5022
+ },
5023
+ {
5024
+ title: "Upload Asset to Canva",
5025
+ readOnlyHint: false,
5026
+ destructiveHint: false,
5027
+ idempotentHint: false,
5028
+ openWorldHint: true
5029
+ },
5030
+ async ({ image_url, name }) => {
5031
+ try {
5032
+ const result = await client.canvaUploadAsset({
5033
+ imageUrl: image_url,
5034
+ name
5035
+ });
5036
+ const lines = [`Asset uploaded to Canva successfully.`];
5037
+ lines.push(`Asset ID: ${result.assetId}`);
5038
+ if (result.thumbnailUrl) {
5039
+ lines.push(`Thumbnail: ${result.thumbnailUrl}`);
5040
+ }
5041
+ return {
5042
+ content: [{ type: "text", text: lines.join("\n") }]
5043
+ };
5044
+ } catch (error) {
5045
+ if (isNotConnectedError(error)) {
5046
+ return {
5047
+ content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
5048
+ };
5049
+ }
5050
+ const message = error instanceof Error ? error.message : "Unknown error";
5051
+ throw new Error(`Failed to upload asset to Canva: ${message}`);
5052
+ }
5053
+ }
5054
+ );
5055
+ server.tool(
5056
+ "canva_create_design",
5057
+ "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.",
5058
+ {
5059
+ title: z18.string().min(1).max(255).describe("Title for the new design."),
5060
+ design_type: z18.enum([
5061
+ "instagram_post",
5062
+ "facebook_post",
5063
+ "presentation",
5064
+ "poster",
5065
+ "story"
5066
+ ]).describe("The type of design to create, which determines dimensions."),
5067
+ asset_id: z18.string().optional().describe(
5068
+ "Optional Canva asset ID to include in the design."
5069
+ )
5070
+ },
5071
+ {
5072
+ title: "Create Canva Design",
5073
+ readOnlyHint: false,
5074
+ destructiveHint: false,
5075
+ idempotentHint: false,
5076
+ openWorldHint: true
5077
+ },
5078
+ async ({ title, design_type, asset_id }) => {
5079
+ try {
5080
+ const result = await client.canvaCreateDesign({
5081
+ title,
5082
+ designType: design_type,
5083
+ assetId: asset_id
5084
+ });
5085
+ const lines = [`Canva design created successfully.`];
5086
+ lines.push(`Design ID: ${result.designId}`);
5087
+ lines.push(`Edit URL: ${result.editUrl}`);
5088
+ if (result.thumbnailUrl) {
5089
+ lines.push(`Thumbnail: ${result.thumbnailUrl}`);
5090
+ }
5091
+ return {
5092
+ content: [{ type: "text", text: lines.join("\n") }]
5093
+ };
5094
+ } catch (error) {
5095
+ if (isNotConnectedError(error)) {
5096
+ return {
5097
+ content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
5098
+ };
5099
+ }
5100
+ const message = error instanceof Error ? error.message : "Unknown error";
5101
+ throw new Error(`Failed to create Canva design: ${message}`);
5102
+ }
5103
+ }
5104
+ );
5105
+ server.tool(
5106
+ "canva_export_design",
5107
+ "Export a Canva design to a downloadable file. Polls until the export is complete and returns the download URL.",
5108
+ {
5109
+ design_id: z18.string().min(1).describe("The Canva design ID to export."),
5110
+ format: z18.enum(["png", "pdf", "jpg"]).default("png").describe("Export format. Defaults to PNG.")
5111
+ },
5112
+ {
5113
+ title: "Export Canva Design",
5114
+ readOnlyHint: false,
5115
+ destructiveHint: false,
5116
+ idempotentHint: true,
5117
+ openWorldHint: true
5118
+ },
5119
+ async ({ design_id, format }) => {
5120
+ try {
5121
+ const result = await client.canvaExportDesign({
5122
+ designId: design_id,
5123
+ format
5124
+ });
5125
+ return {
5126
+ content: [
5127
+ {
5128
+ type: "text",
5129
+ text: `Design exported successfully.
5130
+ Download URL: ${result.downloadUrl}`
5131
+ }
5132
+ ]
5133
+ };
5134
+ } catch (error) {
5135
+ if (isNotConnectedError(error)) {
5136
+ return {
5137
+ content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
5138
+ };
5139
+ }
5140
+ const message = error instanceof Error ? error.message : "Unknown error";
5141
+ throw new Error(`Failed to export Canva design: ${message}`);
5142
+ }
5143
+ }
5144
+ );
5145
+ server.tool(
5146
+ "canva_search_designs",
5147
+ "Search the customer's Canva designs by keyword. Returns matching designs with their IDs, titles, thumbnails, and edit URLs.",
5148
+ {
5149
+ query: z18.string().min(1).describe("Search keyword to find designs.")
5150
+ },
5151
+ {
5152
+ title: "Search Canva Designs",
5153
+ readOnlyHint: true,
5154
+ destructiveHint: false,
5155
+ idempotentHint: true,
5156
+ openWorldHint: true
5157
+ },
5158
+ async ({ query }) => {
5159
+ try {
5160
+ const designs = await client.canvaSearchDesigns(query);
5161
+ if (!designs || designs.length === 0) {
5162
+ return {
5163
+ content: [
5164
+ {
5165
+ type: "text",
5166
+ text: `No Canva designs found matching "${query}".`
5167
+ }
5168
+ ]
5169
+ };
5170
+ }
5171
+ const lines = [`## Canva Designs (${designs.length} results)`];
5172
+ lines.push("");
5173
+ for (const d of designs) {
5174
+ lines.push(`**${d.title}** (ID: ${d.id})`);
5175
+ lines.push(` Edit: ${d.editUrl}`);
5176
+ if (d.thumbnailUrl) lines.push(` Thumbnail: ${d.thumbnailUrl}`);
5177
+ lines.push("");
5178
+ }
5179
+ return {
5180
+ content: [{ type: "text", text: lines.join("\n") }]
5181
+ };
5182
+ } catch (error) {
5183
+ if (isNotConnectedError(error)) {
5184
+ return {
5185
+ content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
5186
+ };
5187
+ }
5188
+ const message = error instanceof Error ? error.message : "Unknown error";
5189
+ throw new Error(`Failed to search Canva designs: ${message}`);
5190
+ }
5191
+ }
5192
+ );
5193
+ server.tool(
5194
+ "canva_get_design",
5195
+ "Get details about a specific Canva design by ID. Returns the design's title, edit URL, thumbnail, and metadata.",
5196
+ {
5197
+ design_id: z18.string().min(1).describe("The Canva design ID to retrieve.")
5198
+ },
5199
+ {
5200
+ title: "Get Canva Design",
5201
+ readOnlyHint: true,
5202
+ destructiveHint: false,
5203
+ idempotentHint: true,
5204
+ openWorldHint: true
5205
+ },
5206
+ async ({ design_id }) => {
5207
+ try {
5208
+ const design = await client.canvaGetDesign(design_id);
5209
+ const lines = [`## ${design.title}`];
5210
+ lines.push(`Design ID: ${design.id}`);
5211
+ lines.push(`Edit URL: ${design.editUrl}`);
5212
+ lines.push(`View URL: ${design.viewUrl}`);
5213
+ if (design.thumbnailUrl) {
5214
+ lines.push(`Thumbnail: ${design.thumbnailUrl}`);
5215
+ }
5216
+ if (design.createdAt) lines.push(`Created: ${design.createdAt}`);
5217
+ if (design.updatedAt) lines.push(`Updated: ${design.updatedAt}`);
5218
+ return {
5219
+ content: [{ type: "text", text: lines.join("\n") }]
5220
+ };
5221
+ } catch (error) {
5222
+ if (isNotConnectedError(error)) {
5223
+ return {
5224
+ content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
5225
+ };
5226
+ }
5227
+ const message = error instanceof Error ? error.message : "Unknown error";
5228
+ throw new Error(`Failed to get Canva design: ${message}`);
5229
+ }
5230
+ }
5231
+ );
5232
+ }
5233
+
5234
+ // src/tools/sendgrid-newsletter.ts
5235
+ import { z as z19 } from "zod";
5236
+ function registerSendGridNewsletterTools(server, client, options) {
5237
+ const allowDirectSend = options?.allowDirectSend ?? false;
5238
+ server.tool(
5239
+ "sg_list_lists",
5240
+ "List all SendGrid subscriber lists for the account. Returns list names, IDs, and descriptions.",
5241
+ {},
5242
+ {
5243
+ title: "List Subscriber Lists",
5244
+ readOnlyHint: true,
5245
+ destructiveHint: false,
5246
+ idempotentHint: true,
5247
+ openWorldHint: false
5248
+ },
5249
+ async () => {
5250
+ const lists = await client.sgListLists();
5251
+ if (!lists || lists.length === 0) {
5252
+ return {
5253
+ content: [
5254
+ {
5255
+ type: "text",
5256
+ text: "No subscriber lists found. Create one with sg_create_list."
5257
+ }
5258
+ ]
5259
+ };
5260
+ }
5261
+ const lines = [`## Subscriber Lists (${lists.length})`];
5262
+ for (const l of lists) {
5263
+ lines.push(`- **${l.name}** (ID: ${l.id})${l.description ? ` \u2014 ${l.description}` : ""}`);
5264
+ }
5265
+ return {
5266
+ content: [{ type: "text", text: lines.join("\n") }]
5267
+ };
5268
+ }
5269
+ );
5270
+ server.tool(
5271
+ "sg_create_list",
5272
+ "Create a new subscriber list for organizing newsletter recipients.",
5273
+ {
5274
+ name: z19.string().min(1).max(255).describe("Name of the subscriber list."),
5275
+ description: z19.string().max(1e3).optional().describe("Optional description of the list.")
5276
+ },
5277
+ {
5278
+ title: "Create Subscriber List",
5279
+ readOnlyHint: false,
5280
+ destructiveHint: false,
5281
+ idempotentHint: false,
5282
+ openWorldHint: false
5283
+ },
5284
+ async ({ name, description }) => {
5285
+ const list = await client.sgCreateList({ name, description });
5286
+ return {
5287
+ content: [
5288
+ {
5289
+ type: "text",
5290
+ text: `Subscriber list created.
5291
+ Name: ${list.name}
5292
+ ID: ${list.id}`
5293
+ }
5294
+ ]
5295
+ };
5296
+ }
5297
+ );
5298
+ server.tool(
5299
+ "sg_delete_list",
5300
+ "Delete a subscriber list. This removes the list and all memberships (subscribers themselves are not deleted). Requires confirmation.",
5301
+ {
5302
+ list_id: z19.number().int().describe("ID of the list to delete."),
5303
+ confirmed: z19.boolean().describe(
5304
+ "Must be true to confirm deletion. Show the list name to the user and ask for confirmation first."
5305
+ )
5306
+ },
5307
+ {
5308
+ title: "Delete Subscriber List",
5309
+ readOnlyHint: false,
5310
+ destructiveHint: true,
5311
+ idempotentHint: false,
5312
+ openWorldHint: false
5313
+ },
5314
+ async ({ list_id, confirmed }) => {
5315
+ if (!confirmed) {
5316
+ return {
5317
+ content: [
5318
+ {
5319
+ type: "text",
5320
+ text: "Deletion not confirmed. Set confirmed=true to proceed."
5321
+ }
5322
+ ]
5323
+ };
5324
+ }
5325
+ await client.sgDeleteList(list_id);
5326
+ return {
5327
+ content: [
5328
+ {
5329
+ type: "text",
5330
+ text: `Subscriber list ${list_id} deleted successfully.`
5331
+ }
5332
+ ]
5333
+ };
5334
+ }
5335
+ );
5336
+ server.tool(
5337
+ "sg_list_subscribers",
5338
+ "List SendGrid-managed subscribers. Can filter by list ID and status (active, unsubscribed, bounced, complained). Supports pagination.",
5339
+ {
5340
+ list_id: z19.number().int().optional().describe("Optional list ID to filter subscribers by list membership."),
5341
+ status: z19.enum(["active", "unsubscribed", "bounced", "complained"]).optional().describe("Optional status filter."),
5342
+ page: z19.number().int().min(1).default(1).describe("Page number."),
5343
+ limit: z19.number().int().min(1).max(100).default(50).describe("Results per page.")
5344
+ },
5345
+ {
5346
+ title: "List Subscribers",
5347
+ readOnlyHint: true,
5348
+ destructiveHint: false,
5349
+ idempotentHint: true,
5350
+ openWorldHint: false
5351
+ },
5352
+ async ({ list_id, status, page, limit }) => {
5353
+ const params = {
5354
+ page: String(page),
5355
+ limit: String(limit)
5356
+ };
5357
+ if (list_id !== void 0) params.listId = String(list_id);
5358
+ if (status) params.status = status;
5359
+ const result = await client.sgListSubscribers(params);
5360
+ if (!result.subscribers || result.subscribers.length === 0) {
5361
+ return {
5362
+ content: [
5363
+ {
5364
+ type: "text",
5365
+ text: "No subscribers found matching the criteria."
5366
+ }
5367
+ ]
5368
+ };
5369
+ }
5370
+ const lines = [`## Subscribers (page ${result.page})`];
5371
+ for (const s of result.subscribers) {
5372
+ const name = [s.firstName, s.lastName].filter(Boolean).join(" ");
5373
+ lines.push(
5374
+ `- ${s.email}${name ? ` (${name})` : ""} \u2014 ${s.status} (ID: ${s.id})`
5375
+ );
5376
+ }
5377
+ return {
5378
+ content: [{ type: "text", text: lines.join("\n") }]
5379
+ };
5380
+ }
5381
+ );
5382
+ server.tool(
5383
+ "sg_add_subscriber",
5384
+ "Add or update a subscriber in the SendGrid-managed subscriber list. Optionally assign to one or more lists.",
5385
+ {
5386
+ email: z19.string().email().describe("Subscriber's email address."),
5387
+ first_name: z19.string().optional().describe("Subscriber's first name."),
5388
+ last_name: z19.string().optional().describe("Subscriber's last name."),
5389
+ list_ids: z19.array(z19.number().int()).optional().describe("Optional list IDs to add the subscriber to.")
5390
+ },
5391
+ {
5392
+ title: "Add Subscriber",
5393
+ readOnlyHint: false,
5394
+ destructiveHint: false,
5395
+ idempotentHint: true,
5396
+ openWorldHint: false
5397
+ },
5398
+ async ({ email, first_name, last_name, list_ids }) => {
5399
+ const subscriber = await client.sgAddSubscriber({
5400
+ email,
5401
+ firstName: first_name,
5402
+ lastName: last_name,
5403
+ listIds: list_ids
5404
+ });
5405
+ return {
5406
+ content: [
5407
+ {
5408
+ type: "text",
5409
+ text: `Subscriber added/updated.
5410
+ Email: ${subscriber.email}
5411
+ ID: ${subscriber.id}`
5412
+ }
5413
+ ]
5414
+ };
5415
+ }
5416
+ );
5417
+ server.tool(
5418
+ "sg_remove_subscriber",
5419
+ "Remove a subscriber (marks as unsubscribed). Requires confirmation.",
5420
+ {
5421
+ subscriber_id: z19.number().int().describe("ID of the subscriber to remove."),
5422
+ confirmed: z19.boolean().describe(
5423
+ "Must be true to confirm. Show the subscriber email to the user first."
5424
+ )
5425
+ },
5426
+ {
5427
+ title: "Remove Subscriber",
5428
+ readOnlyHint: false,
5429
+ destructiveHint: true,
5430
+ idempotentHint: false,
5431
+ openWorldHint: false
5432
+ },
5433
+ async ({ subscriber_id, confirmed }) => {
5434
+ if (!confirmed) {
5435
+ return {
5436
+ content: [
5437
+ {
5438
+ type: "text",
5439
+ text: "Removal not confirmed. Set confirmed=true to proceed."
5440
+ }
5441
+ ]
5442
+ };
5443
+ }
5444
+ const result = await client.sgRemoveSubscriber(subscriber_id);
5445
+ return {
5446
+ content: [
5447
+ {
5448
+ type: "text",
5449
+ text: `Subscriber ${result.email} marked as unsubscribed.`
5450
+ }
5451
+ ]
5452
+ };
5453
+ }
5454
+ );
5455
+ server.tool(
5456
+ "sg_list_sends",
5457
+ "List newsletter sends (drafts, scheduled, sent). Filter by status.",
5458
+ {
5459
+ status: z19.enum(["draft", "scheduled", "sending", "sent", "cancelled"]).optional().describe("Optional status filter."),
5460
+ page: z19.number().int().min(1).default(1).describe("Page number."),
5461
+ limit: z19.number().int().min(1).max(100).default(20).describe("Results per page.")
5462
+ },
5463
+ {
5464
+ title: "List Newsletter Sends",
5465
+ readOnlyHint: true,
5466
+ destructiveHint: false,
5467
+ idempotentHint: true,
5468
+ openWorldHint: false
5469
+ },
5470
+ async ({ status, page, limit }) => {
5471
+ const params = {
5472
+ page: String(page),
5473
+ limit: String(limit)
5474
+ };
5475
+ if (status) params.status = status;
5476
+ const result = await client.sgListSends(params);
5477
+ if (!result.sends || result.sends.length === 0) {
5478
+ return {
5479
+ content: [
5480
+ { type: "text", text: "No newsletter sends found." }
5481
+ ]
5482
+ };
5483
+ }
5484
+ const lines = [`## Newsletter Sends (${result.sends.length})`];
5485
+ for (const s of result.sends) {
5486
+ lines.push(
5487
+ `- **${s.subject}** (ID: ${s.id}) \u2014 ${s.status}${s.sentAt ? `, sent ${s.sentAt}` : ""}${s.scheduledAt ? `, scheduled ${s.scheduledAt}` : ""}`
5488
+ );
5489
+ if (s.status === "sent") {
5490
+ lines.push(` Delivered: ${s.delivered} | Opened: ${s.opened}`);
5491
+ }
5492
+ }
5493
+ return {
5494
+ content: [{ type: "text", text: lines.join("\n") }]
5495
+ };
5496
+ }
5497
+ );
5498
+ server.tool(
5499
+ "sg_get_send",
5500
+ "Get details and delivery stats for a specific newsletter send.",
5501
+ {
5502
+ send_id: z19.number().int().describe("ID of the newsletter send.")
5503
+ },
5504
+ {
5505
+ title: "Get Newsletter Send",
5506
+ readOnlyHint: true,
5507
+ destructiveHint: false,
5508
+ idempotentHint: true,
5509
+ openWorldHint: false
5510
+ },
5511
+ async ({ send_id }) => {
5512
+ const send = await client.sgGetSend(send_id);
5513
+ const lines = [
5514
+ `## ${send.subject}`,
5515
+ `ID: ${send.id}`,
5516
+ `From: ${send.fromName ? `${send.fromName} <${send.fromEmail}>` : send.fromEmail}`,
5517
+ `Status: ${send.status}`
5518
+ ];
5519
+ if (send.sentAt) lines.push(`Sent: ${send.sentAt}`);
5520
+ if (send.scheduledAt) lines.push(`Scheduled: ${send.scheduledAt}`);
5521
+ lines.push("", "### Delivery Stats");
5522
+ lines.push(`Delivered: ${send.delivered}`);
5523
+ lines.push(`Opened: ${send.opened}`);
5524
+ lines.push(`Clicked: ${send.clicked}`);
5525
+ lines.push(`Bounced: ${send.bounced}`);
5526
+ lines.push(`Complaints: ${send.complained}`);
5527
+ lines.push(`Unsubscribed: ${send.unsubscribed}`);
5528
+ lines.push("");
5529
+ lines.push(`NEWSLETTER_HTML_PREVIEW: ${send.htmlContent}`);
5530
+ return {
5531
+ content: [{ type: "text", text: lines.join("\n") }]
5532
+ };
5533
+ }
5534
+ );
5535
+ server.tool(
5536
+ "sg_create_send",
5537
+ "Create a new newsletter send draft. Must specify subject, HTML content, and from email.",
5538
+ {
5539
+ subject: z19.string().min(1).max(500).describe("Email subject line."),
5540
+ html_content: z19.string().min(1).describe("HTML content of the newsletter."),
5541
+ from_email: z19.string().email().describe("Sender email address (must be authenticated domain)."),
5542
+ from_name: z19.string().optional().describe("Sender display name."),
5543
+ list_id: z19.number().int().optional().describe("Optional list ID to send to. If omitted, sends to all active subscribers.")
5544
+ },
5545
+ {
5546
+ title: "Create Newsletter Draft",
5547
+ readOnlyHint: false,
5548
+ destructiveHint: false,
5549
+ idempotentHint: false,
5550
+ openWorldHint: false
5551
+ },
5552
+ async ({ subject, html_content, from_email, from_name, list_id }) => {
5553
+ const send = await client.sgCreateSend({
5554
+ subject,
5555
+ htmlContent: html_content,
5556
+ fromEmail: from_email,
5557
+ fromName: from_name,
5558
+ listId: list_id
5559
+ });
5560
+ return {
5561
+ content: [
5562
+ {
5563
+ type: "text",
5564
+ text: `Newsletter draft created.
5565
+ ID: ${send.id}
5566
+ Subject: ${send.subject}
5567
+ Status: ${send.status}
5568
+
5569
+ Use sg_test_send to preview, or sg_send_newsletter to send.`
5570
+ }
5571
+ ]
5572
+ };
5573
+ }
5574
+ );
5575
+ server.tool(
5576
+ "sg_update_send",
5577
+ "Update a newsletter draft. Only drafts can be updated.",
5578
+ {
5579
+ send_id: z19.number().int().describe("ID of the send to update."),
5580
+ subject: z19.string().min(1).max(500).optional().describe("New subject line."),
5581
+ html_content: z19.string().min(1).optional().describe("New HTML content."),
5582
+ from_email: z19.string().email().optional().describe("New sender email."),
5583
+ from_name: z19.string().optional().describe("New sender name."),
5584
+ list_id: z19.number().int().nullable().optional().describe("New list ID, or null to send to all.")
5585
+ },
5586
+ {
5587
+ title: "Update Newsletter Draft",
5588
+ readOnlyHint: false,
5589
+ destructiveHint: false,
5590
+ idempotentHint: true,
5591
+ openWorldHint: false
5592
+ },
5593
+ async ({ send_id, subject, html_content, from_email, from_name, list_id }) => {
5594
+ const data = {};
5595
+ if (subject !== void 0) data.subject = subject;
5596
+ if (html_content !== void 0) data.htmlContent = html_content;
5597
+ if (from_email !== void 0) data.fromEmail = from_email;
5598
+ if (from_name !== void 0) data.fromName = from_name;
5599
+ if (list_id !== void 0) data.listId = list_id;
5600
+ const send = await client.sgUpdateSend(send_id, data);
5601
+ return {
5602
+ content: [
5603
+ {
5604
+ type: "text",
5605
+ text: `Newsletter draft updated.
5606
+ ID: ${send.id}
5607
+ Subject: ${send.subject}`
5608
+ }
5609
+ ]
5610
+ };
5611
+ }
5612
+ );
5613
+ server.tool(
5614
+ "sg_delete_send",
5615
+ "Delete a newsletter draft. Only drafts can be deleted. Requires confirmation.",
5616
+ {
5617
+ send_id: z19.number().int().describe("ID of the draft to delete."),
5618
+ confirmed: z19.boolean().describe("Must be true to confirm deletion.")
5619
+ },
5620
+ {
5621
+ title: "Delete Newsletter Draft",
5622
+ readOnlyHint: false,
5623
+ destructiveHint: true,
5624
+ idempotentHint: false,
5625
+ openWorldHint: false
5626
+ },
5627
+ async ({ send_id, confirmed }) => {
5628
+ if (!confirmed) {
5629
+ return {
5630
+ content: [
5631
+ {
5632
+ type: "text",
5633
+ text: "Deletion not confirmed. Set confirmed=true to proceed."
5634
+ }
5635
+ ]
5636
+ };
5637
+ }
5638
+ await client.sgDeleteSend(send_id);
5639
+ return {
5640
+ content: [
5641
+ {
5642
+ type: "text",
5643
+ text: `Newsletter draft ${send_id} deleted.`
5644
+ }
5645
+ ]
5646
+ };
5647
+ }
5648
+ );
5649
+ server.tool(
5650
+ "sg_send_newsletter",
5651
+ allowDirectSend ? "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.",
5652
+ {
5653
+ send_id: z19.number().int().describe("ID of the draft to send."),
5654
+ confirmed: z19.boolean().describe(
5655
+ "Must be true. Show the user the subject, from address, and recipient count before confirming."
5656
+ )
5657
+ },
5658
+ {
5659
+ title: "Send Newsletter Now",
5660
+ readOnlyHint: false,
5661
+ destructiveHint: true,
5662
+ idempotentHint: false,
5663
+ openWorldHint: true
5664
+ },
5665
+ async ({ send_id, confirmed }) => {
5666
+ if (!allowDirectSend) {
5667
+ return {
5668
+ content: [
5669
+ {
5670
+ type: "text",
5671
+ text: "Direct send is not enabled for this account. Contact support to enable this feature."
5672
+ }
5673
+ ]
5674
+ };
5675
+ }
5676
+ if (!confirmed) {
5677
+ return {
5678
+ content: [
5679
+ {
5680
+ type: "text",
5681
+ text: "Send not confirmed. Review the newsletter details with sg_get_send first, then set confirmed=true."
5682
+ }
5683
+ ]
5684
+ };
5685
+ }
5686
+ const result = await client.sgSendNewsletter(send_id);
5687
+ return {
5688
+ content: [
5689
+ {
5690
+ type: "text",
5691
+ text: `Newsletter sent successfully!
5692
+ Subject: ${result.subject}
5693
+ Recipients: ${result.recipientCount}
5694
+ Status: ${result.status}`
5695
+ }
5696
+ ]
5697
+ };
5698
+ }
5699
+ );
5700
+ server.tool(
5701
+ "sg_test_send",
5702
+ "Send a test email of a newsletter draft to a specific email address. Subject will be prefixed with [TEST].",
5703
+ {
5704
+ send_id: z19.number().int().describe("ID of the newsletter send."),
5705
+ email: z19.string().email().describe("Email address to send the test to.")
5706
+ },
5707
+ {
5708
+ title: "Send Test Email",
5709
+ readOnlyHint: false,
5710
+ destructiveHint: false,
5711
+ idempotentHint: false,
5712
+ openWorldHint: true
5713
+ },
5714
+ async ({ send_id, email }) => {
5715
+ await client.sgTestSend(send_id, { email });
5716
+ return {
5717
+ content: [
5718
+ {
5719
+ type: "text",
5720
+ text: `Test email sent to ${email}. Check your inbox.`
5721
+ }
5722
+ ]
5723
+ };
5724
+ }
5725
+ );
5726
+ server.tool(
5727
+ "sg_schedule_send",
5728
+ "Schedule a newsletter to be sent at a future date/time. Requires confirmation.",
5729
+ {
5730
+ send_id: z19.number().int().describe("ID of the draft to schedule."),
5731
+ scheduled_at: z19.string().describe(
5732
+ "ISO 8601 datetime for when to send (e.g., '2025-01-15T09:00:00Z'). Must be in the future."
5733
+ ),
5734
+ confirmed: z19.boolean().describe("Must be true to confirm scheduling.")
5735
+ },
5736
+ {
5737
+ title: "Schedule Newsletter Send",
5738
+ readOnlyHint: false,
5739
+ destructiveHint: false,
5740
+ idempotentHint: false,
5741
+ openWorldHint: true
5742
+ },
5743
+ async ({ send_id, scheduled_at, confirmed }) => {
5744
+ if (!confirmed) {
5745
+ return {
5746
+ content: [
5747
+ {
5748
+ type: "text",
5749
+ text: "Scheduling not confirmed. Set confirmed=true to proceed."
5750
+ }
5751
+ ]
5752
+ };
5753
+ }
5754
+ const result = await client.sgScheduleSend(send_id, {
5755
+ scheduledAt: scheduled_at
5756
+ });
5757
+ return {
5758
+ content: [
5759
+ {
5760
+ type: "text",
5761
+ text: `Newsletter scheduled.
5762
+ Subject: ${result.subject}
5763
+ Scheduled for: ${result.scheduledAt}
5764
+ Recipients: ${result.recipientCount}`
5765
+ }
5766
+ ]
5767
+ };
5768
+ }
5769
+ );
5770
+ server.tool(
5771
+ "sg_authenticate_domain",
5772
+ "Start domain authentication with SendGrid. Returns DNS records that need to be added to your domain's DNS settings.",
5773
+ {
5774
+ domain: z19.string().min(1).describe("Domain to authenticate (e.g., 'example.com').")
5775
+ },
5776
+ {
5777
+ title: "Authenticate Domain",
5778
+ readOnlyHint: false,
5779
+ destructiveHint: false,
5780
+ idempotentHint: false,
5781
+ openWorldHint: true
5782
+ },
5783
+ async ({ domain }) => {
5784
+ const result = await client.sgAuthenticateDomain(domain);
5785
+ const lines = [
5786
+ `Domain authentication started for **${result.domain}**.`,
5787
+ `Domain ID: ${result.id}`,
5788
+ "",
5789
+ "Add these DNS records to your domain:",
5790
+ "```",
5791
+ JSON.stringify(result.dnsRecords, null, 2),
5792
+ "```",
5793
+ "",
5794
+ "After adding the records, use sg_validate_domain to verify."
5795
+ ];
5796
+ return {
5797
+ content: [{ type: "text", text: lines.join("\n") }]
5798
+ };
5799
+ }
5800
+ );
5801
+ server.tool(
5802
+ "sg_validate_domain",
5803
+ "Validate DNS records for a domain that was previously set up for authentication.",
5804
+ {
5805
+ domain_id: z19.number().int().describe("Domain ID from sg_authenticate_domain.")
5806
+ },
5807
+ {
5808
+ title: "Validate Domain",
5809
+ readOnlyHint: true,
5810
+ destructiveHint: false,
5811
+ idempotentHint: true,
5812
+ openWorldHint: true
5813
+ },
5814
+ async ({ domain_id }) => {
5815
+ const result = await client.sgValidateDomain(domain_id);
5816
+ if (result.valid) {
5817
+ return {
5818
+ content: [
5819
+ {
5820
+ type: "text",
5821
+ text: "Domain validation successful! Your domain is now authenticated for sending."
5822
+ }
5823
+ ]
5824
+ };
5825
+ }
5826
+ return {
5827
+ content: [
5828
+ {
5829
+ type: "text",
5830
+ text: `Domain validation failed. Check your DNS records.
5831
+
5832
+ Results:
5833
+ ${JSON.stringify(result.validationResults, null, 2)}`
5834
+ }
5835
+ ]
5836
+ };
5837
+ }
5838
+ );
5839
+ server.tool(
5840
+ "sg_list_domains",
5841
+ "List all authenticated sending domains.",
5842
+ {},
5843
+ {
5844
+ title: "List Authenticated Domains",
5845
+ readOnlyHint: true,
5846
+ destructiveHint: false,
5847
+ idempotentHint: true,
5848
+ openWorldHint: true
5849
+ },
5850
+ async () => {
5851
+ const domains = await client.sgListDomains();
5852
+ if (!domains || domains.length === 0) {
5853
+ return {
5854
+ content: [
5855
+ {
5856
+ type: "text",
5857
+ text: "No authenticated domains found. Use sg_authenticate_domain to set one up."
5858
+ }
5859
+ ]
5860
+ };
5861
+ }
5862
+ const lines = [`## Authenticated Domains (${domains.length})`];
5863
+ for (const d of domains) {
5864
+ lines.push(
5865
+ `- **${d.domain}** (ID: ${d.id}) \u2014 ${d.valid ? "Valid" : "Not verified"}`
5866
+ );
5867
+ }
5868
+ return {
5869
+ content: [{ type: "text", text: lines.join("\n") }]
5870
+ };
5871
+ }
5872
+ );
5873
+ }
4241
5874
  export {
4242
5875
  BuzzPosterClient,
4243
5876
  registerAccountInfoTool,
@@ -4247,6 +5880,7 @@ export {
4247
5880
  registerAuditLogTools,
4248
5881
  registerBrandVoiceTools,
4249
5882
  registerCalendarTools,
5883
+ registerCanvaTools,
4250
5884
  registerCarouselTools,
4251
5885
  registerInboxTools,
4252
5886
  registerKnowledgeTools,
@@ -4257,6 +5891,8 @@ export {
4257
5891
  registerNotificationTools,
4258
5892
  registerPostTools,
4259
5893
  registerPublishingRulesTools,
5894
+ registerQueueTools,
4260
5895
  registerRssTools,
5896
+ registerSendGridNewsletterTools,
4261
5897
  registerSourceTools
4262
5898
  };