@buzzposter/mcp 0.1.5 → 0.1.7

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 CHANGED
@@ -168,6 +168,79 @@ var BuzzPosterClient = class {
168
168
  async listForms() {
169
169
  return this.request("GET", "/api/v1/newsletters/forms");
170
170
  }
171
+ // Newsletter Advanced
172
+ async getSubscriberByEmail(email) {
173
+ return this.request("GET", `/api/v1/newsletters/subscribers/by-email/${encodeURIComponent(email)}`);
174
+ }
175
+ async listAutomations(params) {
176
+ return this.request("GET", "/api/v1/newsletters/automations", void 0, params);
177
+ }
178
+ async enrollInAutomation(automationId, data) {
179
+ return this.request("POST", `/api/v1/newsletters/automations/${automationId}/enroll`, data);
180
+ }
181
+ async listSegments(params) {
182
+ return this.request("GET", "/api/v1/newsletters/segments", void 0, params);
183
+ }
184
+ async getSegment(id, params) {
185
+ return this.request("GET", `/api/v1/newsletters/segments/${id}`, void 0, params);
186
+ }
187
+ async getSegmentMembers(segmentId, params) {
188
+ return this.request("GET", `/api/v1/newsletters/segments/${segmentId}/members`, void 0, params);
189
+ }
190
+ async deleteSegment(id) {
191
+ return this.request("DELETE", `/api/v1/newsletters/segments/${id}`);
192
+ }
193
+ async recalculateSegment(id) {
194
+ return this.request("POST", `/api/v1/newsletters/segments/${id}/recalculate`);
195
+ }
196
+ async listCustomFields() {
197
+ return this.request("GET", "/api/v1/newsletters/custom-fields");
198
+ }
199
+ async createCustomField(data) {
200
+ return this.request("POST", "/api/v1/newsletters/custom-fields", data);
201
+ }
202
+ async updateCustomField(id, data) {
203
+ return this.request("PUT", `/api/v1/newsletters/custom-fields/${id}`, data);
204
+ }
205
+ async deleteCustomField(id) {
206
+ return this.request("DELETE", `/api/v1/newsletters/custom-fields/${id}`);
207
+ }
208
+ async tagSubscriber(subscriberId, data) {
209
+ return this.request("POST", `/api/v1/newsletters/subscribers/${subscriberId}/tags`, data);
210
+ }
211
+ async updateSubscriber(id, data) {
212
+ return this.request("PUT", `/api/v1/newsletters/subscribers/${id}`, data);
213
+ }
214
+ async deleteSubscriber(id) {
215
+ return this.request("DELETE", `/api/v1/newsletters/subscribers/${id}`);
216
+ }
217
+ async bulkCreateSubscribers(data) {
218
+ return this.request("POST", "/api/v1/newsletters/subscribers/bulk", data);
219
+ }
220
+ async getReferralProgram() {
221
+ return this.request("GET", "/api/v1/newsletters/referral-program");
222
+ }
223
+ async listEspTiers() {
224
+ return this.request("GET", "/api/v1/newsletters/tiers");
225
+ }
226
+ async listPostTemplates() {
227
+ return this.request("GET", "/api/v1/newsletters/post-templates");
228
+ }
229
+ async getPostAggregateStats() {
230
+ return this.request("GET", "/api/v1/newsletters/broadcasts/aggregate-stats");
231
+ }
232
+ async deleteBroadcast(id) {
233
+ return this.request("DELETE", `/api/v1/newsletters/broadcasts/${id}`);
234
+ }
235
+ async listEspWebhooks() {
236
+ return this.request("GET", "/api/v1/newsletters/webhooks");
237
+ }
238
+ async createEspWebhook(data) {
239
+ return this.request("POST", "/api/v1/newsletters/webhooks", data);
240
+ }
241
+ async deleteEspWebhook(id) {
242
+ return this.request("DELETE", `/api/v1/newsletters/webhooks/${id}`);
243
+ }
171
244
  // Knowledge
172
245
  async listKnowledge(params) {
173
246
  return this.request("GET", "/api/v1/knowledge", void 0, params);
@@ -179,6 +252,9 @@ var BuzzPosterClient = class {
179
252
  async getBrandVoice() {
180
253
  return this.request("GET", "/api/v1/brand-voice");
181
254
  }
255
+ async updateBrandVoice(data) {
256
+ return this.request("PUT", "/api/v1/brand-voice", data);
257
+ }
182
258
  // Audiences
183
259
  async getAudience(id) {
184
260
  return this.request("GET", `/api/v1/audiences/${id}`);
@@ -186,6 +262,12 @@ var BuzzPosterClient = class {
186
262
  async getDefaultAudience() {
187
263
  return this.request("GET", "/api/v1/audiences/default");
188
264
  }
265
+ async createAudience(data) {
266
+ return this.request("POST", "/api/v1/audiences", data);
267
+ }
268
+ async updateAudience(id, data) {
269
+ return this.request("PUT", `/api/v1/audiences/${id}`, data);
270
+ }
189
271
  // Newsletter Templates
190
272
  async getTemplate(id) {
191
273
  if (id) return this.request("GET", `/api/v1/templates/${id}`);
@@ -245,6 +327,58 @@ var BuzzPosterClient = class {
245
327
  async markAllNotificationsRead() {
246
328
  return this.request("PUT", "/api/v1/notifications/read-all");
247
329
  }
330
+ // Content Sources
331
+ async listSources(params) {
332
+ return this.request("GET", "/api/v1/content-sources", void 0, params);
333
+ }
334
+ async createSource(data) {
335
+ return this.request("POST", "/api/v1/content-sources", data);
336
+ }
337
+ async updateSource(id, data) {
338
+ return this.request("PUT", `/api/v1/content-sources/${id}`, data);
339
+ }
340
+ async deleteSource(id) {
341
+ return this.request("DELETE", `/api/v1/content-sources/${id}`);
342
+ }
343
+ async checkSource(id) {
344
+ return this.request("POST", `/api/v1/content-sources/${id}/check`);
345
+ }
346
+ // Publishing Rules
347
+ async getPublishingRules() {
348
+ return this.request("GET", "/api/v1/publishing-rules");
349
+ }
350
+ // Audit Log
351
+ async getAuditLog(params) {
352
+ return this.request("GET", "/api/v1/audit-log", void 0, params);
353
+ }
354
+ // Newsletter Validation
355
+ async validateNewsletter(data) {
356
+ return this.request("POST", "/api/v1/newsletters/validate", data);
357
+ }
358
+ // Newsletter Schedule
359
+ async scheduleBroadcast(id, data) {
360
+ return this.request("POST", `/api/v1/newsletters/broadcasts/${id}/schedule`, data);
361
+ }
362
+ // Newsletter Test
363
+ async testBroadcast(id, data) {
364
+ return this.request("POST", `/api/v1/newsletters/broadcasts/${id}/test`, data);
365
+ }
366
+ // Carousel templates
367
+ async getCarouselTemplate() {
368
+ return this.request("GET", "/api/v1/carousel-templates");
369
+ }
370
+ async updateCarouselTemplate(data) {
371
+ return this.request("PUT", "/api/v1/carousel-templates", data);
372
+ }
373
+ async deleteCarouselTemplate() {
374
+ return this.request("DELETE", "/api/v1/carousel-templates");
375
+ }
376
+ async generateCarousel(data) {
377
+ return this.request("POST", "/api/v1/carousel-templates/generate", data);
378
+ }
379
+ async previewCarousel(data) {
380
+ return this.request("POST", "/api/v1/carousel-templates/preview", data);
381
+ }
248
382
  };
249
383
 
250
384
  // src/tools/posts.ts
@@ -252,7 +386,23 @@ import { z } from "zod";
252
386
  function registerPostTools(server2, client2) {
253
387
  server2.tool(
254
388
  "post",
255
- "Create and publish a post to one or more social media platforms. Supports Twitter, Instagram, LinkedIn, and Facebook with full platform-specific options. Requires confirmation before publishing.",
389
+ `Create and publish a post to one or more social media platforms. Supports Twitter, Instagram, LinkedIn, and Facebook.
390
+
391
+ 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.
392
+
393
+ REQUIRED WORKFLOW:
394
+ 1. BEFORE creating any content, call get_brand_voice and get_audience to match the customer's tone
395
+ 2. BEFORE publishing, call get_publishing_rules to check safety settings
396
+ 3. ALWAYS set confirmed=false first to get a preview -- never set confirmed=true on the first call
397
+ 4. Show the user a visual preview of the post before confirming
398
+ 5. Only set confirmed=true after the user explicitly says to publish/post/send
399
+ 6. If publishing_rules.social_default_action is 'draft', create as draft unless the user explicitly asks to publish
400
+ 7. If publishing_rules.require_double_confirm_social is true, ask for confirmation twice before publishing
401
+ 8. NEVER publish immediately without user confirmation, even if the user says "post this" -- always show the preview first
402
+
403
+ When confirmed=false, this tool returns a structured preview with content, platform details, character counts, safety checks, and a confirmation message. You MUST render this as a visual preview for the user and wait for their explicit approval before calling again with confirmed=true.
404
+
405
+ IMPORTANT: Always show the user a preview of the post content before publishing. Never publish without explicit user approval. If no preview has been shown yet, do NOT set confirmed=true -- render the preview first.`,
256
406
  {
257
407
  content: z.string().optional().describe("The text content of the post"),
258
408
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -260,7 +410,7 @@ function registerPostTools(server2, client2) {
260
410
  platform_specific: z.record(z.record(z.unknown())).optional().describe(
261
411
  "Platform-specific data keyed by platform name. E.g. { twitter: { threadItems: [...] }, instagram: { firstComment: '...' } }"
262
412
  ),
263
- confirmed: z.boolean().optional().describe(
413
+ confirmed: z.boolean().default(false).describe(
264
414
  "Set to true to confirm and publish. If false or missing, returns a preview for user approval."
265
415
  )
266
416
  },
@@ -272,14 +422,52 @@ function registerPostTools(server2, client2) {
272
422
  openWorldHint: true
273
423
  },
274
424
  async (args) => {
275
- if (!args.confirmed) {
425
+ if (args.confirmed !== true) {
426
+ let rulesText = "";
427
+ try {
428
+ const rules = await client2.getPublishingRules();
429
+ const blockedWords = rules.blockedWords ?? [];
430
+ const foundBlocked = [];
431
+ if (args.content && blockedWords.length > 0) {
432
+ const lower = args.content.toLowerCase();
433
+ for (const word of blockedWords) {
434
+ if (lower.includes(word.toLowerCase())) foundBlocked.push(word);
435
+ }
436
+ }
437
+ rulesText = `
438
+ ### Safety Checks
439
+ `;
440
+ rulesText += `- Blocked words: ${foundBlocked.length > 0 ? `**FAILED** (found: ${foundBlocked.join(", ")})` : "PASS"}
441
+ `;
442
+ rulesText += `- Default action: ${rules.socialDefaultAction ?? "draft"}
443
+ `;
444
+ rulesText += `- Immediate publish allowed: ${rules.allowImmediatePublish ? "yes" : "no"}
445
+ `;
446
+ if (rules.maxPostsPerDay) rulesText += `- Daily post limit: ${rules.maxPostsPerDay}
447
+ `;
448
+ } catch {
449
+ }
450
+ const charCounts = {};
451
+ const limits = { twitter: 280, linkedin: 3e3, instagram: 2200, facebook: 63206, threads: 500 };
452
+ const contentLen = (args.content ?? "").length;
453
+ for (const p of args.platforms) {
454
+ const limit = limits[p.toLowerCase()] ?? 5e3;
455
+ charCounts[p] = { used: contentLen, limit, ok: contentLen <= limit };
456
+ }
457
+ let charText = "\n### Character Counts\n";
458
+ for (const [platform, counts] of Object.entries(charCounts)) {
459
+ charText += `- ${platform}: ${counts.used}/${counts.limit} ${counts.ok ? "" : "**OVER LIMIT**"}
460
+ `;
461
+ }
276
462
  const preview = `## Post Preview
277
463
 
278
464
  **Content:** "${args.content ?? "(no text)"}"
279
465
  **Platforms:** ${args.platforms.join(", ")}
280
466
  ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
281
- ` : "") + `
282
- This will be published **immediately**. Call this tool again with confirmed=true to proceed.`;
467
+ ` : "") + charText + rulesText + `
468
+ **Action:** This will be published **immediately** to ${args.platforms.length} platform(s).
469
+
470
+ Call this tool again with confirmed=true to proceed.`;
283
471
  return { content: [{ type: "text", text: preview }] };
284
472
  }
285
473
  const platforms = args.platforms.map((platform) => {
@@ -308,11 +496,23 @@ This will be published **immediately**. Call this tool again with confirmed=true
308
496
  );
309
497
  server2.tool(
310
498
  "cross_post",
311
- "Post the same content to all connected social media platforms at once. Requires confirmation before publishing.",
499
+ `Post the same content to all connected platforms at once.
500
+
501
+ 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.
502
+
503
+ REQUIRED WORKFLOW:
504
+ 1. BEFORE creating content, call get_brand_voice and get_audience
505
+ 2. BEFORE publishing, call get_publishing_rules
506
+ 3. ALWAYS set confirmed=false first to preview
507
+ 4. Show the user a visual preview showing how the post will appear on each platform
508
+ 5. This is a high-impact action (goes to ALL platforms) -- always require explicit confirmation
509
+ 6. If publishing_rules.require_double_confirm_social is true, ask twice
510
+
511
+ When confirmed=false, this tool returns a structured preview with content, platform details, character counts, safety checks, and a confirmation message. You MUST render this as a visual preview for the user and wait for their explicit approval before calling again with confirmed=true.`,
312
512
  {
313
513
  content: z.string().describe("The text content to post everywhere"),
314
514
  media_urls: z.array(z.string()).optional().describe("Public URLs of media to attach"),
315
- confirmed: z.boolean().optional().describe(
515
+ confirmed: z.boolean().default(false).describe(
316
516
  "Set to true to confirm and publish. If false or missing, returns a preview for user approval."
317
517
  )
318
518
  },
@@ -338,14 +538,38 @@ This will be published **immediately**. Call this tool again with confirmed=true
338
538
  ]
339
539
  };
340
540
  }
341
- if (!args.confirmed) {
541
+ if (args.confirmed !== true) {
542
+ let rulesText = "";
543
+ try {
544
+ const rules = await client2.getPublishingRules();
545
+ const blockedWords = rules.blockedWords ?? [];
546
+ const foundBlocked = [];
547
+ if (args.content && blockedWords.length > 0) {
548
+ const lower = args.content.toLowerCase();
549
+ for (const word of blockedWords) {
550
+ if (lower.includes(word.toLowerCase())) foundBlocked.push(word);
551
+ }
552
+ }
553
+ rulesText = `
554
+ ### Safety Checks
555
+ `;
556
+ rulesText += `- Blocked words: ${foundBlocked.length > 0 ? `**FAILED** (found: ${foundBlocked.join(", ")})` : "PASS"}
557
+ `;
558
+ rulesText += `- Default action: ${rules.socialDefaultAction ?? "draft"}
559
+ `;
560
+ rulesText += `- Double confirmation required: ${rules.requireDoubleConfirmSocial ? "yes" : "no"}
561
+ `;
562
+ } catch {
563
+ }
342
564
  const preview = `## Cross-Post Preview
343
565
 
344
566
  **Content:** "${args.content}"
345
- **Platforms:** ${platforms.join(", ")}
567
+ **Platforms:** ${platforms.join(", ")} (${platforms.length} platforms)
346
568
  ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
347
- ` : "") + `
348
- This will be published **immediately** to all connected platforms. Call this tool again with confirmed=true to proceed.`;
569
+ ` : "") + rulesText + `
570
+ **Action:** This will be published **immediately** to **all ${platforms.length} connected platforms**. This is a high-impact action.
571
+
572
+ Call this tool again with confirmed=true to proceed.`;
349
573
  return { content: [{ type: "text", text: preview }] };
350
574
  }
351
575
  const body = {
@@ -367,7 +591,21 @@ This will be published **immediately** to all connected platforms. Call this too
367
591
  );
368
592
  server2.tool(
369
593
  "schedule_post",
370
- "Schedule a post for future publication. Requires confirmation before scheduling.",
594
+ `Schedule a post for future publication.
595
+
596
+ 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.
597
+
598
+ REQUIRED WORKFLOW:
599
+ 1. BEFORE creating content, call get_brand_voice and get_audience
600
+ 2. BEFORE scheduling, call get_publishing_rules
601
+ 3. ALWAYS set confirmed=false first to preview
602
+ 4. Show the user a visual preview with the scheduled date/time before confirming
603
+ 5. Only confirm after explicit user approval
604
+ 6. If the user hasn't specified a time, suggest one based on get_queue time slots
605
+
606
+ When confirmed=false, this tool returns a structured preview with content, platform details, character counts, safety checks, and a confirmation message. You MUST render this as a visual preview for the user and wait for their explicit approval before calling again with confirmed=true.
607
+
608
+ IMPORTANT: Always show the user a preview of the post content before scheduling. Never schedule without explicit user approval. If no preview has been shown yet, do NOT set confirmed=true -- render the preview first.`,
371
609
  {
372
610
  content: z.string().optional().describe("The text content of the post"),
373
611
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -375,7 +613,7 @@ This will be published **immediately** to all connected platforms. Call this too
375
613
  timezone: z.string().default("UTC").describe("Timezone for the scheduled time, e.g. America/New_York"),
376
614
  media_urls: z.array(z.string()).optional().describe("Media URLs to attach"),
377
615
  platform_specific: z.record(z.record(z.unknown())).optional().describe("Platform-specific data keyed by platform name"),
378
- confirmed: z.boolean().optional().describe(
616
+ confirmed: z.boolean().default(false).describe(
379
617
  "Set to true to confirm scheduling. If false or missing, returns a preview for user approval."
380
618
  )
381
619
  },
@@ -387,14 +625,32 @@ This will be published **immediately** to all connected platforms. Call this too
387
625
  openWorldHint: true
388
626
  },
389
627
  async (args) => {
390
- if (!args.confirmed) {
628
+ if (args.confirmed !== true) {
629
+ let rulesText = "";
630
+ try {
631
+ const rules = await client2.getPublishingRules();
632
+ const blockedWords = rules.blockedWords ?? [];
633
+ const foundBlocked = [];
634
+ if (args.content && blockedWords.length > 0) {
635
+ const lower = args.content.toLowerCase();
636
+ for (const word of blockedWords) {
637
+ if (lower.includes(word.toLowerCase())) foundBlocked.push(word);
638
+ }
639
+ }
640
+ rulesText = `
641
+ ### Safety Checks
642
+ `;
643
+ rulesText += `- Blocked words: ${foundBlocked.length > 0 ? `**FAILED** (found: ${foundBlocked.join(", ")})` : "PASS"}
644
+ `;
645
+ } catch {
646
+ }
391
647
  const preview = `## Schedule Preview
392
648
 
393
649
  **Content:** "${args.content ?? "(no text)"}"
394
650
  **Platforms:** ${args.platforms.join(", ")}
395
651
  **Scheduled for:** ${args.scheduled_for} (${args.timezone})
396
652
  ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
397
- ` : "") + `
653
+ ` : "") + rulesText + `
398
654
  Call this tool again with confirmed=true to schedule.`;
399
655
  return { content: [{ type: "text", text: preview }] };
400
656
  }
@@ -425,7 +681,7 @@ Call this tool again with confirmed=true to schedule.`;
425
681
  );
426
682
  server2.tool(
427
683
  "create_draft",
428
- "Save a post as a draft without publishing.",
684
+ "Save a post as a draft without publishing. This NEVER publishes \u2014 it only saves. No confirmation needed since nothing goes live.",
429
685
  {
430
686
  content: z.string().optional().describe("The text content of the draft"),
431
687
  platforms: z.array(z.string()).describe("Platforms this draft is intended for"),
@@ -441,8 +697,7 @@ Call this tool again with confirmed=true to schedule.`;
441
697
  async (args) => {
442
698
  const body = {
443
699
  content: args.content,
444
- platforms: args.platforms.map((p) => ({ platform: p })),
445
- publishNow: false
700
+ platforms: args.platforms.map((p) => ({ platform: p }))
446
701
  };
447
702
  if (args.media_urls?.length) {
448
703
  body.mediaItems = args.media_urls.map((url) => ({
@@ -505,7 +760,7 @@ Call this tool again with confirmed=true to schedule.`;
505
760
  "Retry a post that failed to publish on one or more platforms. Only retries the failed platforms -- already-published platforms are not affected. Use this when a post shows 'failed' or 'partial' status.",
506
761
  {
507
762
  post_id: z.string().describe("The ID of the post to retry"),
508
- confirmed: z.boolean().optional().describe(
763
+ confirmed: z.boolean().default(false).describe(
509
764
  "Set to true to confirm retry. If false/missing, shows post details first."
510
765
  )
511
766
  },
@@ -517,7 +772,7 @@ Call this tool again with confirmed=true to schedule.`;
517
772
  openWorldHint: true
518
773
  },
519
774
  async (args) => {
520
- if (!args.confirmed) {
775
+ if (args.confirmed !== true) {
521
776
  const post = await client2.getPost(args.post_id);
522
777
  const platforms = post.platforms ?? [];
523
778
  const failed = platforms.filter((p) => p.status === "failed");
@@ -736,11 +991,16 @@ function registerInboxTools(server2, client2) {
736
991
  );
737
992
  server2.tool(
738
993
  "reply_to_conversation",
739
- "Send a reply message in a DM conversation. Sends a message to an external platform on your behalf.",
994
+ `Send a reply message in a DM conversation. This sends a message on an external platform on the customer's behalf. Always show draft reply to the user before calling with confirmed=true.
995
+
996
+ REQUIRED WORKFLOW:
997
+ 1. ALWAYS set confirmed=false first to preview the reply
998
+ 2. Show the user the reply text and which platform/conversation it will be sent to
999
+ 3. Only confirm after explicit user approval`,
740
1000
  {
741
1001
  conversation_id: z3.string().describe("The conversation ID to reply to"),
742
1002
  message: z3.string().describe("The reply message text"),
743
- confirmed: z3.boolean().optional().describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
1003
+ confirmed: z3.boolean().default(false).describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
744
1004
  },
745
1005
  {
746
1006
  title: "Reply to Conversation",
@@ -750,7 +1010,7 @@ function registerInboxTools(server2, client2) {
750
1010
  openWorldHint: true
751
1011
  },
752
1012
  async (args) => {
753
- if (!args.confirmed) {
1013
+ if (args.confirmed !== true) {
754
1014
  const preview = `## Reply Preview
755
1015
 
756
1016
  **Conversation:** ${args.conversation_id}
@@ -792,11 +1052,16 @@ This will send a DM reply on the external platform. Call this tool again with co
792
1052
  );
793
1053
  server2.tool(
794
1054
  "reply_to_comment",
795
- "Reply to a comment on one of your posts. Sends a reply on the external platform.",
1055
+ `Reply to a comment on one of the customer's posts. This sends a public reply on the external platform. Sends a message on the user's behalf on an external platform. Always show draft reply to the user before calling with confirmed=true.
1056
+
1057
+ REQUIRED WORKFLOW:
1058
+ 1. ALWAYS set confirmed=false first to preview
1059
+ 2. Show the user the reply and the original comment for context
1060
+ 3. Only confirm after explicit user approval -- public replies are visible to everyone`,
796
1061
  {
797
1062
  comment_id: z3.string().describe("The comment ID to reply to"),
798
1063
  message: z3.string().describe("The reply text"),
799
- confirmed: z3.boolean().optional().describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
1064
+ confirmed: z3.boolean().default(false).describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
800
1065
  },
801
1066
  {
802
1067
  title: "Reply to Comment",
@@ -806,7 +1071,7 @@ This will send a DM reply on the external platform. Call this tool again with co
806
1071
  openWorldHint: true
807
1072
  },
808
1073
  async (args) => {
809
- if (!args.confirmed) {
1074
+ if (args.confirmed !== true) {
810
1075
  const preview = `## Comment Reply Preview
811
1076
 
812
1077
  **Comment ID:** ${args.comment_id}
@@ -844,11 +1109,16 @@ This will post a public reply. Call this tool again with confirmed=true to send.
844
1109
  );
845
1110
  server2.tool(
846
1111
  "reply_to_review",
847
- "Reply to a review on your Facebook page. Sends a public reply on the external platform.",
1112
+ `Reply to a review on the customer's Facebook page. This sends a public reply on the external platform. Sends a message on the user's behalf on an external platform. Always show draft reply to the user before calling with confirmed=true.
1113
+
1114
+ REQUIRED WORKFLOW:
1115
+ 1. ALWAYS set confirmed=false first to preview
1116
+ 2. Show the user the reply, the original review, and the star rating for context
1117
+ 3. Only confirm after explicit user approval -- public replies represent the brand`,
848
1118
  {
849
1119
  review_id: z3.string().describe("The review ID to reply to"),
850
1120
  message: z3.string().describe("The reply text"),
851
- confirmed: z3.boolean().optional().describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
1121
+ confirmed: z3.boolean().default(false).describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
852
1122
  },
853
1123
  {
854
1124
  title: "Reply to Review",
@@ -858,7 +1128,7 @@ This will post a public reply. Call this tool again with confirmed=true to send.
858
1128
  openWorldHint: true
859
1129
  },
860
1130
  async (args) => {
861
- if (!args.confirmed) {
1131
+ if (args.confirmed !== true) {
862
1132
  const preview = `## Review Reply Preview
863
1133
 
864
1134
  **Review ID:** ${args.review_id}
@@ -988,10 +1258,15 @@ function registerMediaTools(server2, client2) {
988
1258
  );
989
1259
  server2.tool(
990
1260
  "delete_media",
991
- "Delete a media file from your BuzzPoster media library. This cannot be undone.",
1261
+ `Delete a media file from the customer's BuzzPoster media library. This cannot be undone.
1262
+
1263
+ REQUIRED WORKFLOW:
1264
+ 1. ALWAYS set confirmed=false first
1265
+ 2. Show the user which file will be deleted (filename, URL, size)
1266
+ 3. Only confirm after explicit approval`,
992
1267
  {
993
1268
  key: z4.string().describe("The key/path of the media file to delete"),
994
- confirmed: z4.boolean().optional().describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
1269
+ confirmed: z4.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
995
1270
  },
996
1271
  {
997
1272
  title: "Delete Media File",
@@ -1001,7 +1276,7 @@ function registerMediaTools(server2, client2) {
1001
1276
  openWorldHint: false
1002
1277
  },
1003
1278
  async (args) => {
1004
- if (!args.confirmed) {
1279
+ if (args.confirmed !== true) {
1005
1280
  const preview = `## Delete Media Confirmation
1006
1281
 
1007
1282
  **File:** ${args.key}
@@ -1019,7 +1294,7 @@ This will permanently delete this media file. Call this tool again with confirme
1019
1294
 
1020
1295
  // src/tools/newsletter.ts
1021
1296
  import { z as z5 } from "zod";
1022
- function registerNewsletterTools(server2, client2) {
1297
+ function registerNewsletterTools(server2, client2, options = {}) {
1023
1298
  server2.tool(
1024
1299
  "list_subscribers",
1025
1300
  "List email subscribers from your configured email service provider (Kit, Beehiiv, or Mailchimp).",
@@ -1072,7 +1347,7 @@ function registerNewsletterTools(server2, client2) {
1072
1347
  );
1073
1348
  server2.tool(
1074
1349
  "create_newsletter",
1075
- "Create a new newsletter/broadcast draft.",
1350
+ `Push an approved newsletter draft to the user's ESP (Kit, Beehiiv, or Mailchimp) as a DRAFT. This creates a DRAFT only -- it does not send. IMPORTANT: Only call this AFTER the user has reviewed and explicitly approved an HTML artifact preview of the newsletter. If no artifact has been shown yet, do NOT call this tool -- go back and render the preview first. No third-party branding in the output unless the customer explicitly configured it.`,
1076
1351
  {
1077
1352
  subject: z5.string().describe("Email subject line"),
1078
1353
  content: z5.string().describe("HTML content of the newsletter"),
@@ -1098,7 +1373,7 @@ function registerNewsletterTools(server2, client2) {
1098
1373
  );
1099
1374
  server2.tool(
1100
1375
  "update_newsletter",
1101
- "Update an existing newsletter/broadcast draft.",
1376
+ `Update an existing newsletter/broadcast draft. After updating, show the user a visual preview of the changes.`,
1102
1377
  {
1103
1378
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to update"),
1104
1379
  subject: z5.string().optional().describe("Updated subject line"),
@@ -1123,36 +1398,172 @@ function registerNewsletterTools(server2, client2) {
1123
1398
  };
1124
1399
  }
1125
1400
  );
1401
+ if (options.allowDirectSend) {
1402
+ server2.tool(
1403
+ "send_newsletter",
1404
+ `Send a newsletter/broadcast to subscribers. THIS ACTION CANNOT BE UNDONE. Once sent, the email goes to every subscriber on the list.
1405
+
1406
+ REQUIRED WORKFLOW:
1407
+ 1. ALWAYS call get_publishing_rules first
1408
+ 2. NEVER send without showing a full visual preview of the newsletter first
1409
+ 3. ALWAYS set confirmed=false first -- this returns a confirmation prompt, not a send
1410
+ 4. Tell the user exactly how many subscribers will receive this email
1411
+ 5. If publishing_rules.require_double_confirm_newsletter is true (default), require TWO explicit confirmations:
1412
+ - First: "Are you sure you want to send this to [X] subscribers?"
1413
+ - Second: "This is irreversible. Type 'send' to confirm."
1414
+ 6. If publishing_rules.allow_immediate_send is false and the user asks to send immediately, suggest scheduling instead
1415
+ 7. NEVER set confirmed=true without the user explicitly confirming after seeing the preview and subscriber count
1416
+
1417
+ When confirmed=false, this tool returns a structured preview with broadcast details, subscriber count, safety checks, and a confirmation message. You MUST render this as a visual preview for the user and wait for their explicit approval before calling again with confirmed=true.`,
1418
+ {
1419
+ broadcast_id: z5.string().describe("The broadcast/newsletter ID to send"),
1420
+ confirmed: z5.boolean().default(false).describe(
1421
+ "Set to true to confirm and send. If false or missing, returns a confirmation prompt."
1422
+ )
1423
+ },
1424
+ {
1425
+ title: "Send Newsletter",
1426
+ readOnlyHint: false,
1427
+ destructiveHint: false,
1428
+ idempotentHint: false,
1429
+ openWorldHint: true
1430
+ },
1431
+ async (args) => {
1432
+ if (args.confirmed !== true) {
1433
+ let subscriberCount = "unknown";
1434
+ let rulesText = "";
1435
+ try {
1436
+ const subData = await client2.listSubscribers({ perPage: "1" });
1437
+ subscriberCount = String(subData?.totalCount ?? subData?.total ?? subData?.subscribers?.length ?? "unknown");
1438
+ } catch {
1439
+ }
1440
+ try {
1441
+ const rules = await client2.getPublishingRules();
1442
+ rulesText = `
1443
+ ### Safety Checks
1444
+ `;
1445
+ rulesText += `- Double confirmation required: ${rules.requireDoubleConfirmNewsletter ? "**yes**" : "no"}
1446
+ `;
1447
+ rulesText += `- Immediate send allowed: ${rules.allowImmediateSend ? "yes" : "**no**"}
1448
+ `;
1449
+ if (rules.requiredDisclaimer) {
1450
+ rulesText += `- Required disclaimer: "${rules.requiredDisclaimer}"
1451
+ `;
1452
+ }
1453
+ } catch {
1454
+ }
1455
+ let espText = "";
1456
+ try {
1457
+ const account = await client2.getAccount();
1458
+ espText = account?.espProvider ? `**ESP:** ${account.espProvider}
1459
+ ` : "";
1460
+ } catch {
1461
+ }
1462
+ const preview = `## Send Newsletter Confirmation
1463
+
1464
+ **Broadcast ID:** ${args.broadcast_id}
1465
+ **Subscribers:** ~${subscriberCount} recipients
1466
+ ` + espText + rulesText + `
1467
+ **This action cannot be undone.** Once sent, the email goes to every subscriber.
1468
+
1469
+ Call this tool again with confirmed=true to send.`;
1470
+ return { content: [{ type: "text", text: preview }] };
1471
+ }
1472
+ await client2.sendBroadcast(args.broadcast_id);
1473
+ return {
1474
+ content: [{ type: "text", text: "Newsletter sent successfully." }]
1475
+ };
1476
+ }
1477
+ );
1478
+ }
1126
1479
  server2.tool(
1127
- "send_newsletter",
1128
- "Send a newsletter/broadcast to subscribers. This action cannot be undone. Requires confirmation before sending.",
1480
+ "schedule_newsletter",
1481
+ `Schedule a newsletter/broadcast to be sent at a future time.
1482
+
1483
+ REQUIRED WORKFLOW:
1484
+ 1. ALWAYS call get_publishing_rules first
1485
+ 2. NEVER schedule without showing a full visual preview of the newsletter first
1486
+ 3. ALWAYS set confirmed=false first -- this returns a confirmation prompt with details, not an actual schedule
1487
+ 4. Tell the user exactly how many subscribers will receive this email and the scheduled send time
1488
+ 5. If publishing_rules.require_double_confirm_newsletter is true (default), require TWO explicit confirmations:
1489
+ - First: "Are you sure you want to schedule this for [time] to [X] subscribers?"
1490
+ - Second: "Confirm schedule for [time]."
1491
+ 6. If publishing_rules.allow_immediate_send is false and the scheduled time is very soon, warn the user
1492
+ 7. NEVER set confirmed=true without the user explicitly confirming after seeing the preview, subscriber count, and scheduled time
1493
+
1494
+ Scheduling can typically be cancelled or rescheduled later, but the user should still verify the time is correct before confirming.
1495
+
1496
+ When confirmed=false, this tool returns a structured preview with broadcast details, scheduled time, subscriber count, safety checks, and a confirmation message. You MUST render this as a visual preview for the user and wait for their explicit approval before calling again with confirmed=true.`,
1129
1497
  {
1130
- broadcast_id: z5.string().describe("The broadcast/newsletter ID to send"),
1131
- confirmed: z5.boolean().optional().describe(
1132
- "Set to true to confirm and send. If false or missing, returns a confirmation prompt."
1498
+ broadcast_id: z5.string().describe("The broadcast/newsletter ID to schedule"),
1499
+ scheduled_for: z5.string().describe(
1500
+ "ISO 8601 datetime for when to send (e.g. '2026-02-25T14:00:00Z')"
1501
+ ),
1502
+ confirmed: z5.boolean().default(false).describe(
1503
+ "Set to true to confirm and schedule. If false or missing, returns a confirmation prompt."
1133
1504
  )
1134
1505
  },
1135
1506
  {
1136
- title: "Send Newsletter",
1507
+ title: "Schedule Newsletter",
1137
1508
  readOnlyHint: false,
1138
1509
  destructiveHint: false,
1139
1510
  idempotentHint: false,
1140
1511
  openWorldHint: true
1141
1512
  },
1142
1513
  async (args) => {
1143
- if (!args.confirmed) {
1144
- const preview = `## Send Newsletter Confirmation
1514
+ if (args.confirmed !== true) {
1515
+ let subscriberCount = "unknown";
1516
+ let rulesText = "";
1517
+ try {
1518
+ const subData = await client2.listSubscribers({ perPage: "1" });
1519
+ subscriberCount = String(
1520
+ subData?.totalCount ?? subData?.total ?? subData?.subscribers?.length ?? "unknown"
1521
+ );
1522
+ } catch {
1523
+ }
1524
+ try {
1525
+ const rules = await client2.getPublishingRules();
1526
+ rulesText = `
1527
+ ### Safety Checks
1528
+ `;
1529
+ rulesText += `- Double confirmation required: ${rules.requireDoubleConfirmNewsletter ? "**yes**" : "no"}
1530
+ `;
1531
+ rulesText += `- Immediate send allowed: ${rules.allowImmediateSend ? "yes" : "**no**"}
1532
+ `;
1533
+ if (rules.requiredDisclaimer) {
1534
+ rulesText += `- Required disclaimer: "${rules.requiredDisclaimer}"
1535
+ `;
1536
+ }
1537
+ } catch {
1538
+ }
1539
+ let espText = "";
1540
+ try {
1541
+ const account = await client2.getAccount();
1542
+ espText = account?.espProvider ? `**ESP:** ${account.espProvider}
1543
+ ` : "";
1544
+ } catch {
1545
+ }
1546
+ const preview = `## Schedule Newsletter Confirmation
1145
1547
 
1146
1548
  **Broadcast ID:** ${args.broadcast_id}
1549
+ **Scheduled for:** ${args.scheduled_for}
1550
+ **Subscribers:** ~${subscriberCount} recipients
1551
+ ` + espText + rulesText + `
1552
+ Scheduling can typically be cancelled or rescheduled later, but please verify the time is correct.
1147
1553
 
1148
- This will send the newsletter to your subscriber list. **This action cannot be undone.**
1149
-
1150
- Call this tool again with confirmed=true to send.`;
1554
+ Call this tool again with confirmed=true to schedule.`;
1151
1555
  return { content: [{ type: "text", text: preview }] };
1152
1556
  }
1153
- await client2.sendBroadcast(args.broadcast_id);
1557
+ await client2.scheduleBroadcast(args.broadcast_id, {
1558
+ scheduledFor: args.scheduled_for
1559
+ });
1154
1560
  return {
1155
- content: [{ type: "text", text: "Newsletter sent successfully." }]
1561
+ content: [
1562
+ {
1563
+ type: "text",
1564
+ text: `Newsletter scheduled successfully for ${args.scheduled_for}.`
1565
+ }
1566
+ ]
1156
1567
  };
1157
1568
  }
1158
1569
  );
@@ -1198,7 +1609,7 @@ Call this tool again with confirmed=true to send.`;
1198
1609
  );
1199
1610
  server2.tool(
1200
1611
  "list_sequences",
1201
- "List all email sequences/automations from your email service provider.",
1612
+ "List all email sequences/automations from your email service provider. On Beehiiv, this returns automations. For detailed automation data use list_automations instead.",
1202
1613
  {},
1203
1614
  {
1204
1615
  title: "List Email Sequences",
@@ -1239,7 +1650,7 @@ import { z as z6 } from "zod";
1239
1650
  function registerRssTools(server2, client2) {
1240
1651
  server2.tool(
1241
1652
  "fetch_feed",
1242
- "Fetch and parse entries from an RSS or Atom feed URL. Returns titles, links, descriptions, and dates.",
1653
+ "Fetch and parse entries from an RSS or Atom feed URL. Returns titles, links, descriptions, and dates. Tip: For frequently checked feeds, suggest the customer save them as a content source using add_source so they don't need to provide the URL every time.",
1243
1654
  {
1244
1655
  url: z6.string().describe("The RSS/Atom feed URL to fetch"),
1245
1656
  limit: z6.number().optional().describe("Maximum number of entries to return (default 10, max 100)")
@@ -1260,7 +1671,7 @@ function registerRssTools(server2, client2) {
1260
1671
  );
1261
1672
  server2.tool(
1262
1673
  "fetch_article",
1263
- "Extract the full article content from a URL as clean text/markdown. Useful for reading and summarizing articles.",
1674
+ "Extract the full article content from a URL as clean text/markdown. Useful for reading and summarizing articles. Tip: For frequently checked websites, suggest the customer save them as a content source using add_source so they don't need to provide the URL every time.",
1264
1675
  {
1265
1676
  url: z6.string().describe("The article URL to extract content from")
1266
1677
  },
@@ -1303,10 +1714,11 @@ function registerAccountInfoTool(server2, client2) {
1303
1714
  }
1304
1715
 
1305
1716
  // src/tools/brand-voice.ts
1717
+ import { z as z7 } from "zod";
1306
1718
  function registerBrandVoiceTools(server2, client2) {
1307
1719
  server2.tool(
1308
1720
  "get_brand_voice",
1309
- "Get the customer's brand voice profile and writing rules. IMPORTANT: Call this tool BEFORE creating any post, draft, or content. Use the returned voice description, rules, and examples to match the customer's tone and style in everything you write for them.",
1721
+ "Get the customer's brand voice profile and writing rules. IMPORTANT: Call this tool BEFORE creating any post, draft, or content. Use the returned voice description, rules, and examples to match the customer's tone and style in everything you write for them. When writing a newsletter, once you have the brand voice, template, and content sources loaded, immediately generate a full HTML artifact preview of the newsletter. Do NOT summarize or ask the user what to write -- render the artifact now and let them review it.",
1310
1722
  {},
1311
1723
  {
1312
1724
  title: "Get Brand Voice",
@@ -1321,39 +1733,45 @@ function registerBrandVoiceTools(server2, client2) {
1321
1733
  const lines = [];
1322
1734
  lines.push(`## Brand Voice: ${voice.name || "My Brand"}`);
1323
1735
  lines.push("");
1324
- if (voice.description) {
1325
- lines.push("### Voice Description");
1326
- lines.push(voice.description);
1327
- lines.push("");
1328
- }
1736
+ lines.push("### Voice Description");
1737
+ lines.push(voice.description || "(not set)");
1738
+ lines.push("");
1739
+ lines.push("### Do's");
1329
1740
  if (voice.dos && voice.dos.length > 0) {
1330
- lines.push("### Do's");
1331
1741
  for (const rule of voice.dos) {
1332
1742
  lines.push(`- ${rule}`);
1333
1743
  }
1334
- lines.push("");
1744
+ } else {
1745
+ lines.push("(none)");
1335
1746
  }
1747
+ lines.push("");
1748
+ lines.push("### Don'ts");
1336
1749
  if (voice.donts && voice.donts.length > 0) {
1337
- lines.push("### Don'ts");
1338
1750
  for (const rule of voice.donts) {
1339
1751
  lines.push(`- ${rule}`);
1340
1752
  }
1341
- lines.push("");
1753
+ } else {
1754
+ lines.push("(none)");
1342
1755
  }
1756
+ lines.push("");
1757
+ lines.push("### Platform-Specific Rules");
1343
1758
  if (voice.platformRules && Object.keys(voice.platformRules).length > 0) {
1344
- lines.push("### Platform-Specific Rules");
1345
1759
  for (const [platform, rules] of Object.entries(voice.platformRules)) {
1346
1760
  lines.push(`**${platform}:** ${rules}`);
1347
1761
  }
1348
- lines.push("");
1762
+ } else {
1763
+ lines.push("(none)");
1349
1764
  }
1765
+ lines.push("");
1766
+ lines.push("### Example Posts");
1350
1767
  if (voice.examplePosts && voice.examplePosts.length > 0) {
1351
- lines.push("### Example Posts");
1352
1768
  voice.examplePosts.forEach((post, i) => {
1353
1769
  lines.push(`${i + 1}. ${post}`);
1354
1770
  });
1355
- lines.push("");
1771
+ } else {
1772
+ lines.push("(none)");
1356
1773
  }
1774
+ lines.push("");
1357
1775
  return {
1358
1776
  content: [{ type: "text", text: lines.join("\n") }]
1359
1777
  };
@@ -1373,10 +1791,125 @@ function registerBrandVoiceTools(server2, client2) {
1373
1791
  }
1374
1792
  }
1375
1793
  );
1794
+ server2.tool(
1795
+ "update_brand_voice",
1796
+ `Create or update the customer's brand voice profile. This is an upsert \u2014 if no brand voice exists it will be created, otherwise the existing one is updated. Only the fields you provide will be changed; omitted fields are left as-is.
1797
+
1798
+ USAGE GUIDELINES:
1799
+ - Call get_brand_voice first to see the current state before making changes
1800
+ - Always set confirmed=false first to preview the changes, then confirmed=true after user approval
1801
+ - When adding rules to dos/donts, include the FULL array (existing + new items), not just the new ones
1802
+ - Platform rules use platform names as keys (e.g. twitter, linkedin, instagram)
1803
+ - Max limits: name 80 chars, description 16000 chars, dos/donts 50 items each, examplePosts 8 items`,
1804
+ {
1805
+ name: z7.string().max(80).optional().describe("Brand voice profile name (e.g. 'BOQtoday Voice', 'Lightbreak Editorial')"),
1806
+ description: z7.string().max(16e3).optional().describe("Overall voice description \u2014 tone, personality, style overview"),
1807
+ dos: z7.array(z7.string()).max(50).optional().describe("Writing rules to follow (array of do's). Send the FULL list, not just additions."),
1808
+ donts: z7.array(z7.string()).max(50).optional().describe("Things to avoid (array of don'ts). Send the FULL list, not just additions."),
1809
+ platform_rules: z7.record(z7.string()).optional().describe('Per-platform writing guidelines, e.g. { "twitter": "Keep under 200 chars, punchy tone", "linkedin": "Professional, add hashtags" }'),
1810
+ example_posts: z7.array(z7.string()).max(8).optional().describe("Example posts that demonstrate the desired voice (max 8)"),
1811
+ confirmed: z7.boolean().default(false).describe(
1812
+ "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
1813
+ )
1814
+ },
1815
+ {
1816
+ title: "Update Brand Voice",
1817
+ readOnlyHint: false,
1818
+ destructiveHint: false,
1819
+ idempotentHint: false,
1820
+ openWorldHint: false
1821
+ },
1822
+ async (args) => {
1823
+ const payload = {};
1824
+ if (args.name !== void 0) payload.name = args.name;
1825
+ if (args.description !== void 0) payload.description = args.description;
1826
+ if (args.dos !== void 0) payload.dos = args.dos;
1827
+ if (args.donts !== void 0) payload.donts = args.donts;
1828
+ if (args.platform_rules !== void 0) payload.platformRules = args.platform_rules;
1829
+ if (args.example_posts !== void 0) payload.examplePosts = args.example_posts;
1830
+ if (Object.keys(payload).length === 0) {
1831
+ return {
1832
+ content: [
1833
+ {
1834
+ type: "text",
1835
+ text: "No fields provided. Specify at least one field to update (name, description, dos, donts, platform_rules, example_posts)."
1836
+ }
1837
+ ],
1838
+ isError: true
1839
+ };
1840
+ }
1841
+ if (args.confirmed !== true) {
1842
+ const lines2 = [];
1843
+ lines2.push("## Brand Voice Update Preview");
1844
+ lines2.push("");
1845
+ if (payload.name) {
1846
+ lines2.push(`**Name:** ${payload.name}`);
1847
+ lines2.push("");
1848
+ }
1849
+ if (payload.description) {
1850
+ lines2.push("### Voice Description");
1851
+ lines2.push(String(payload.description));
1852
+ lines2.push("");
1853
+ }
1854
+ if (payload.dos) {
1855
+ const dos = payload.dos;
1856
+ lines2.push(`### Do's (${dos.length} rules)`);
1857
+ for (const rule of dos) {
1858
+ lines2.push(`- ${rule}`);
1859
+ }
1860
+ lines2.push("");
1861
+ }
1862
+ if (payload.donts) {
1863
+ const donts = payload.donts;
1864
+ lines2.push(`### Don'ts (${donts.length} rules)`);
1865
+ for (const rule of donts) {
1866
+ lines2.push(`- ${rule}`);
1867
+ }
1868
+ lines2.push("");
1869
+ }
1870
+ if (payload.platformRules) {
1871
+ const rules = payload.platformRules;
1872
+ lines2.push(`### Platform-Specific Rules (${Object.keys(rules).length} platforms)`);
1873
+ for (const [platform, rule] of Object.entries(rules)) {
1874
+ lines2.push(`**${platform}:** ${rule}`);
1875
+ }
1876
+ lines2.push("");
1877
+ }
1878
+ if (payload.examplePosts) {
1879
+ const posts = payload.examplePosts;
1880
+ lines2.push(`### Example Posts (${posts.length})`);
1881
+ posts.forEach((post, i) => {
1882
+ lines2.push(`${i + 1}. ${post}`);
1883
+ });
1884
+ lines2.push("");
1885
+ }
1886
+ lines2.push("---");
1887
+ lines2.push("Call this tool again with **confirmed=true** to save these changes.");
1888
+ return {
1889
+ content: [{ type: "text", text: lines2.join("\n") }]
1890
+ };
1891
+ }
1892
+ const result = await client2.updateBrandVoice(payload);
1893
+ const lines = [];
1894
+ lines.push(`Brand voice "${result.name || "My Brand"}" has been saved successfully.`);
1895
+ lines.push("");
1896
+ const summary = [];
1897
+ if (payload.name) summary.push("name");
1898
+ if (payload.description) summary.push("description");
1899
+ if (payload.dos) summary.push(`${payload.dos.length} do's`);
1900
+ if (payload.donts) summary.push(`${payload.donts.length} don'ts`);
1901
+ if (payload.platformRules) summary.push(`${Object.keys(payload.platformRules).length} platform rules`);
1902
+ if (payload.examplePosts) summary.push(`${payload.examplePosts.length} example posts`);
1903
+ lines.push(`**Updated:** ${summary.join(", ")}`);
1904
+ return {
1905
+ content: [{ type: "text", text: lines.join("\n") }]
1906
+ };
1907
+ }
1908
+ );
1376
1909
  }
1377
1910
 
1378
1911
  // src/tools/knowledge.ts
1379
- import { z as z7 } from "zod";
1912
+ import { z as z8 } from "zod";
1380
1913
  function registerKnowledgeTools(server2, client2) {
1381
1914
  server2.tool(
1382
1915
  "get_knowledge_base",
@@ -1436,7 +1969,7 @@ function registerKnowledgeTools(server2, client2) {
1436
1969
  "search_knowledge",
1437
1970
  "Search the customer's knowledge base by tag or keyword. Use this to find specific reference material when writing about a topic.",
1438
1971
  {
1439
- query: z7.string().describe(
1972
+ query: z8.string().describe(
1440
1973
  "Search query - matches against tags first, then falls back to text search on title and content"
1441
1974
  )
1442
1975
  },
@@ -1487,9 +2020,9 @@ function registerKnowledgeTools(server2, client2) {
1487
2020
  "add_knowledge",
1488
2021
  "Add a new item to the customer's knowledge base. Use this to save useful information the customer shares during conversation.",
1489
2022
  {
1490
- title: z7.string().describe("Title for the knowledge item"),
1491
- content: z7.string().describe("The content/text to save"),
1492
- tags: z7.array(z7.string()).optional().describe("Optional tags for categorization")
2023
+ title: z8.string().describe("Title for the knowledge item"),
2024
+ content: z8.string().describe("The content/text to save"),
2025
+ tags: z8.array(z8.string()).optional().describe("Optional tags for categorization")
1493
2026
  },
1494
2027
  {
1495
2028
  title: "Add Knowledge Item",
@@ -1523,13 +2056,13 @@ function registerKnowledgeTools(server2, client2) {
1523
2056
  }
1524
2057
 
1525
2058
  // src/tools/audience.ts
1526
- import { z as z8 } from "zod";
2059
+ import { z as z9 } from "zod";
1527
2060
  function registerAudienceTools(server2, client2) {
1528
2061
  server2.tool(
1529
2062
  "get_audience",
1530
2063
  "Get the target audience profile for content creation. Use this tool to understand WHO the content is for \u2014 their demographics, pain points, motivations, preferred platforms, and tone preferences. Call this BEFORE writing any post or content so you can tailor messaging to resonate with the intended audience.",
1531
2064
  {
1532
- audienceId: z8.string().optional().describe("Specific audience profile ID. If omitted, returns the default audience.")
2065
+ audienceId: z9.string().optional().describe("Specific audience profile ID. If omitted, returns the default audience.")
1533
2066
  },
1534
2067
  {
1535
2068
  title: "Get Audience Profile",
@@ -1602,16 +2135,210 @@ function registerAudienceTools(server2, client2) {
1602
2135
  }
1603
2136
  }
1604
2137
  );
2138
+ server2.tool(
2139
+ "update_audience",
2140
+ `Create or update a target audience profile. This is an upsert \u2014 if no audience_id is provided it will try to update the default audience, or create a new one if none exists. Only the fields you provide will be changed; omitted fields are left as-is.
2141
+
2142
+ USAGE GUIDELINES:
2143
+ - Call get_audience first to see the current state before making changes
2144
+ - Always set confirmed=false first to preview the changes, then confirmed=true after user approval
2145
+ - When updating array fields (pain_points, motivations, preferred_platforms), include the FULL array (existing + new items), not just the new ones
2146
+ - Omit audience_id to update the default audience or create a new one if none exists
2147
+ - Max limits: name 100 chars, description 500 chars`,
2148
+ {
2149
+ audience_id: z9.string().optional().describe("Audience ID to update. Omit to update the default audience, or create one if none exists."),
2150
+ name: z9.string().max(100).optional().describe("Audience profile name (e.g. 'SaaS Founders', 'Health-Conscious Millennials'). Required when creating a new audience."),
2151
+ description: z9.string().max(500).optional().describe("Brief description of who this audience is"),
2152
+ demographics: z9.string().optional().describe("Demographic details \u2014 age range, location, income level, education, etc."),
2153
+ pain_points: z9.array(z9.string()).optional().describe("Problems and frustrations the audience faces. Send the FULL list, not just additions."),
2154
+ motivations: z9.array(z9.string()).optional().describe("Goals, desires, and what drives the audience. Send the FULL list, not just additions."),
2155
+ preferred_platforms: z9.array(z9.string()).optional().describe("Social platforms the audience is most active on (e.g. twitter, linkedin, instagram). Send the FULL list."),
2156
+ tone_notes: z9.string().optional().describe("How to speak to this audience \u2014 formality level, jargon preferences, emotional tone"),
2157
+ content_preferences: z9.string().optional().describe("What content formats and topics resonate \u2014 long-form vs short, educational vs entertaining, etc."),
2158
+ is_default: z9.boolean().optional().describe("Set to true to make this the default audience profile"),
2159
+ confirmed: z9.boolean().default(false).describe(
2160
+ "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
2161
+ )
2162
+ },
2163
+ {
2164
+ title: "Update Audience",
2165
+ readOnlyHint: false,
2166
+ destructiveHint: false,
2167
+ idempotentHint: false,
2168
+ openWorldHint: false
2169
+ },
2170
+ async (args) => {
2171
+ const payload = {};
2172
+ if (args.name !== void 0) payload.name = args.name;
2173
+ if (args.description !== void 0) payload.description = args.description;
2174
+ if (args.demographics !== void 0) payload.demographics = args.demographics;
2175
+ if (args.pain_points !== void 0) payload.painPoints = args.pain_points;
2176
+ if (args.motivations !== void 0) payload.motivations = args.motivations;
2177
+ if (args.preferred_platforms !== void 0) payload.platforms = args.preferred_platforms;
2178
+ if (args.tone_notes !== void 0) payload.toneNotes = args.tone_notes;
2179
+ if (args.content_preferences !== void 0) payload.contentPreferences = args.content_preferences;
2180
+ if (args.is_default !== void 0) payload.isDefault = args.is_default;
2181
+ if (Object.keys(payload).length === 0) {
2182
+ return {
2183
+ content: [
2184
+ {
2185
+ type: "text",
2186
+ text: "No fields provided. Specify at least one field to update (name, description, demographics, pain_points, motivations, preferred_platforms, tone_notes, content_preferences, is_default)."
2187
+ }
2188
+ ],
2189
+ isError: true
2190
+ };
2191
+ }
2192
+ let targetId = args.audience_id;
2193
+ let isCreating = false;
2194
+ if (!targetId) {
2195
+ try {
2196
+ const defaultAudience = await client2.getDefaultAudience();
2197
+ targetId = String(defaultAudience.id);
2198
+ } catch {
2199
+ isCreating = true;
2200
+ }
2201
+ }
2202
+ if (isCreating && !payload.name) {
2203
+ return {
2204
+ content: [
2205
+ {
2206
+ type: "text",
2207
+ text: "No audience exists yet. Provide a **name** to create one (e.g. name: 'SaaS Founders')."
2208
+ }
2209
+ ],
2210
+ isError: true
2211
+ };
2212
+ }
2213
+ if (args.confirmed !== true) {
2214
+ const lines2 = [];
2215
+ lines2.push(isCreating ? "## New Audience Preview" : "## Audience Update Preview");
2216
+ if (!isCreating) {
2217
+ lines2.push(`**Audience ID:** ${targetId}`);
2218
+ }
2219
+ lines2.push("");
2220
+ if (payload.name) {
2221
+ lines2.push(`**Name:** ${payload.name}`);
2222
+ lines2.push("");
2223
+ }
2224
+ if (payload.description) {
2225
+ lines2.push("### Description");
2226
+ lines2.push(String(payload.description));
2227
+ lines2.push("");
2228
+ }
2229
+ if (payload.demographics) {
2230
+ lines2.push("### Demographics");
2231
+ lines2.push(String(payload.demographics));
2232
+ lines2.push("");
2233
+ }
2234
+ if (payload.painPoints) {
2235
+ const points = payload.painPoints;
2236
+ lines2.push(`### Pain Points (${points.length})`);
2237
+ for (const point of points) {
2238
+ lines2.push(`- ${point}`);
2239
+ }
2240
+ lines2.push("");
2241
+ }
2242
+ if (payload.motivations) {
2243
+ const motivations = payload.motivations;
2244
+ lines2.push(`### Motivations (${motivations.length})`);
2245
+ for (const motivation of motivations) {
2246
+ lines2.push(`- ${motivation}`);
2247
+ }
2248
+ lines2.push("");
2249
+ }
2250
+ if (payload.platforms) {
2251
+ const platforms = payload.platforms;
2252
+ lines2.push(`### Preferred Platforms (${platforms.length})`);
2253
+ lines2.push(platforms.join(", "));
2254
+ lines2.push("");
2255
+ }
2256
+ if (payload.toneNotes) {
2257
+ lines2.push("### Tone Notes");
2258
+ lines2.push(String(payload.toneNotes));
2259
+ lines2.push("");
2260
+ }
2261
+ if (payload.contentPreferences) {
2262
+ lines2.push("### Content Preferences");
2263
+ lines2.push(String(payload.contentPreferences));
2264
+ lines2.push("");
2265
+ }
2266
+ if (payload.isDefault !== void 0) {
2267
+ lines2.push(`**Set as default:** ${payload.isDefault ? "Yes" : "No"}`);
2268
+ lines2.push("");
2269
+ }
2270
+ lines2.push("---");
2271
+ lines2.push("Call this tool again with **confirmed=true** to save these changes.");
2272
+ return {
2273
+ content: [{ type: "text", text: lines2.join("\n") }]
2274
+ };
2275
+ }
2276
+ let result;
2277
+ if (isCreating) {
2278
+ if (payload.isDefault === void 0) payload.isDefault = true;
2279
+ result = await client2.createAudience(payload);
2280
+ } else {
2281
+ result = await client2.updateAudience(targetId, payload);
2282
+ }
2283
+ const lines = [];
2284
+ lines.push(`Audience "${result.name}" has been ${isCreating ? "created" : "updated"} successfully.`);
2285
+ lines.push("");
2286
+ const summary = [];
2287
+ if (payload.name) summary.push("name");
2288
+ if (payload.description) summary.push("description");
2289
+ if (payload.demographics) summary.push("demographics");
2290
+ if (payload.painPoints) summary.push(`${payload.painPoints.length} pain points`);
2291
+ if (payload.motivations) summary.push(`${payload.motivations.length} motivations`);
2292
+ if (payload.platforms) summary.push(`${payload.platforms.length} platforms`);
2293
+ if (payload.toneNotes) summary.push("tone notes");
2294
+ if (payload.contentPreferences) summary.push("content preferences");
2295
+ if (payload.isDefault !== void 0) summary.push("default status");
2296
+ lines.push(`**${isCreating ? "Created with" : "Updated"}:** ${summary.join(", ")}`);
2297
+ return {
2298
+ content: [{ type: "text", text: lines.join("\n") }]
2299
+ };
2300
+ }
2301
+ );
1605
2302
  }
1606
2303
 
1607
2304
  // src/tools/newsletter-template.ts
1608
- import { z as z9 } from "zod";
2305
+ import { z as z10 } from "zod";
2306
+ var NEWSLETTER_CRAFT_GUIDE = `## Newsletter Craft Guide
2307
+
2308
+ Follow these rules when drafting:
2309
+
2310
+ ### HTML Email Rules
2311
+ - Use table-based layouts, inline CSS only, 600px max width
2312
+ - Email-safe fonts only: Arial, Helvetica, Georgia, Verdana
2313
+ - Keep total email under 102KB (Gmail clips larger)
2314
+ - All images must use absolute URLs hosted on the customer's R2 CDN
2315
+ - Include alt text on every image
2316
+ - Use role="presentation" on layout tables
2317
+ - No JavaScript, no forms, no CSS grid/flexbox, no background images
2318
+
2319
+ ### Structure
2320
+ - Subject line: 6-10 words, specific not clickbaity
2321
+ - Preview text: complements the subject, doesn't repeat it
2322
+ - Open strong -- lead with the most interesting thing
2323
+ - One clear CTA per newsletter
2324
+ - Sign off should feel human, not corporate
2325
+
2326
+ ### Personalization
2327
+ - Kit: use {{ subscriber.first_name }}
2328
+ - Beehiiv: use {{email}} or {{subscriber_id}}
2329
+ - Mailchimp: use *NAME|*
2330
+
2331
+ ### Images
2332
+ - Upload to customer's R2 CDN via upload_media or upload_from_url
2333
+ - Reference with full CDN URLs in img tags
2334
+ - Always set width, height, and alt attributes
2335
+ - Use style="display:block;" to prevent phantom spacing`;
1609
2336
  function registerNewsletterTemplateTools(server2, client2) {
1610
2337
  server2.tool(
1611
2338
  "get_newsletter_template",
1612
- "Get the customer's newsletter template -- the blueprint for how their newsletter is structured. IMPORTANT: Call this BEFORE writing any newsletter. The template defines the exact section order, what each section should contain, word counts, content sources, and styling. Follow the template structure precisely when generating newsletter content. If no template_id is specified, returns the default template.",
2339
+ "Get the customer's newsletter template -- the blueprint for how their newsletter is structured. IMPORTANT: Call this BEFORE writing any newsletter. The template defines the exact section order, what each section should contain, word counts, content sources, and styling. Follow the template structure precisely when generating newsletter content. If no template_id is specified, returns the default template. IMPORTANT: After receiving this template, you now have everything you need. Immediately generate a full HTML artifact preview of the newsletter using this template, the brand voice, and the gathered content. Do NOT summarize the template in text. Do NOT ask the user what they think. Render the complete HTML newsletter artifact now.",
1613
2340
  {
1614
- templateId: z9.string().optional().describe(
2341
+ templateId: z10.string().optional().describe(
1615
2342
  "Specific template ID. If omitted, returns the default template."
1616
2343
  )
1617
2344
  },
@@ -1724,6 +2451,7 @@ function registerNewsletterTemplateTools(server2, client2) {
1724
2451
  );
1725
2452
  lines.push("");
1726
2453
  }
2454
+ lines.push(NEWSLETTER_CRAFT_GUIDE);
1727
2455
  return {
1728
2456
  content: [{ type: "text", text: lines.join("\n") }]
1729
2457
  };
@@ -1734,7 +2462,7 @@ function registerNewsletterTemplateTools(server2, client2) {
1734
2462
  content: [
1735
2463
  {
1736
2464
  type: "text",
1737
- text: "No newsletter template has been configured yet. The customer can set one up at their BuzzPoster dashboard under Templates."
2465
+ text: "No newsletter template configured. Ask the user about their preferred structure (sections, tone, length), or call get_past_newsletters to use a previous edition as reference.\n\n" + NEWSLETTER_CRAFT_GUIDE
1738
2466
  }
1739
2467
  ]
1740
2468
  };
@@ -1747,8 +2475,8 @@ function registerNewsletterTemplateTools(server2, client2) {
1747
2475
  "get_past_newsletters",
1748
2476
  "Retrieve the customer's past newsletters for reference. Use this to maintain continuity, match previous formatting, avoid repeating topics, and reference previous editions. Call this when writing a new newsletter to see what was covered recently.",
1749
2477
  {
1750
- limit: z9.number().optional().describe("Number of past newsletters to retrieve. Default 5, max 50."),
1751
- templateId: z9.string().optional().describe("Filter by template ID to see past newsletters from a specific template.")
2478
+ limit: z10.number().optional().describe("Number of past newsletters to retrieve. Default 5, max 50."),
2479
+ templateId: z10.string().optional().describe("Filter by template ID to see past newsletters from a specific template.")
1752
2480
  },
1753
2481
  {
1754
2482
  title: "Get Past Newsletters",
@@ -1812,7 +2540,7 @@ function registerNewsletterTemplateTools(server2, client2) {
1812
2540
  "get_past_newsletter",
1813
2541
  "Get the full content of a specific past newsletter by ID. Use when you need to reference the complete text of a previous edition, check exact formatting, or continue a series.",
1814
2542
  {
1815
- newsletterId: z9.string().describe("The ID of the archived newsletter to retrieve.")
2543
+ newsletterId: z10.string().describe("The ID of the archived newsletter to retrieve.")
1816
2544
  },
1817
2545
  {
1818
2546
  title: "Get Past Newsletter Detail",
@@ -1856,10 +2584,10 @@ function registerNewsletterTemplateTools(server2, client2) {
1856
2584
  "save_newsletter",
1857
2585
  "Save a generated newsletter to the archive. Call this after the customer approves a newsletter you've written, so it's stored for future reference. Include the subject line and full HTML content.",
1858
2586
  {
1859
- subject: z9.string().describe("The newsletter subject line."),
1860
- contentHtml: z9.string().describe("The full HTML content of the newsletter."),
1861
- templateId: z9.string().optional().describe("The template ID used to generate this newsletter."),
1862
- notes: z9.string().optional().describe(
2587
+ subject: z10.string().describe("The newsletter subject line."),
2588
+ contentHtml: z10.string().describe("The full HTML content of the newsletter."),
2589
+ templateId: z10.string().optional().describe("The template ID used to generate this newsletter."),
2590
+ notes: z10.string().optional().describe(
1863
2591
  "Optional notes about this newsletter (what worked, theme, etc)."
1864
2592
  )
1865
2593
  },
@@ -1896,16 +2624,16 @@ function registerNewsletterTemplateTools(server2, client2) {
1896
2624
  }
1897
2625
 
1898
2626
  // src/tools/calendar.ts
1899
- import { z as z10 } from "zod";
2627
+ import { z as z11 } from "zod";
1900
2628
  function registerCalendarTools(server2, client2) {
1901
2629
  server2.tool(
1902
2630
  "get_calendar",
1903
2631
  "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.",
1904
2632
  {
1905
- from: z10.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
1906
- to: z10.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
1907
- status: z10.string().optional().describe("Filter by status: scheduled, published, draft, failed"),
1908
- type: z10.string().optional().describe("Filter by type: social_post or newsletter")
2633
+ from: z11.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
2634
+ to: z11.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
2635
+ status: z11.string().optional().describe("Filter by status: scheduled, published, draft, failed"),
2636
+ type: z11.string().optional().describe("Filter by type: social_post or newsletter")
1909
2637
  },
1910
2638
  {
1911
2639
  title: "Get Content Calendar",
@@ -2038,12 +2766,22 @@ Next slot: ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
2038
2766
  );
2039
2767
  server2.tool(
2040
2768
  "schedule_to_queue",
2041
- "Add a post to the customer's content queue. The post will be automatically scheduled to the next available time slot. Requires confirmation before scheduling.",
2769
+ `Add a post to the customer's content queue for automatic scheduling.
2770
+
2771
+ 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.
2772
+
2773
+ REQUIRED WORKFLOW:
2774
+ 1. BEFORE creating content, call get_brand_voice and get_audience
2775
+ 2. ALWAYS set confirmed=false first to preview
2776
+ 3. Show the user a visual preview before confirming
2777
+ 4. Tell the user which queue slot the post will be assigned to
2778
+
2779
+ When confirmed=false, this tool returns a structured preview with content, platform details, safety checks, and the assigned queue slot. You MUST render this as a visual preview for the user and wait for their explicit approval before calling again with confirmed=true.`,
2042
2780
  {
2043
- content: z10.string().describe("The text content of the post"),
2044
- platforms: z10.array(z10.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
2045
- media_urls: z10.array(z10.string()).optional().describe("Public URLs of media to attach"),
2046
- confirmed: z10.boolean().optional().describe(
2781
+ content: z11.string().describe("The text content of the post"),
2782
+ platforms: z11.array(z11.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
2783
+ media_urls: z11.array(z11.string()).optional().describe("Public URLs of media to attach"),
2784
+ confirmed: z11.boolean().default(false).describe(
2047
2785
  "Set to true to confirm scheduling. If false or missing, returns a preview for user approval."
2048
2786
  )
2049
2787
  },
@@ -2066,15 +2804,33 @@ Next slot: ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
2066
2804
  ]
2067
2805
  };
2068
2806
  }
2069
- if (!args.confirmed) {
2807
+ if (args.confirmed !== true) {
2070
2808
  const slotDate2 = new Date(nextSlot.scheduledFor);
2809
+ let rulesText = "";
2810
+ try {
2811
+ const rules = await client2.getPublishingRules();
2812
+ const blockedWords = rules.blockedWords ?? [];
2813
+ const foundBlocked = [];
2814
+ if (args.content && blockedWords.length > 0) {
2815
+ const lower = args.content.toLowerCase();
2816
+ for (const word of blockedWords) {
2817
+ if (lower.includes(word.toLowerCase())) foundBlocked.push(word);
2818
+ }
2819
+ }
2820
+ rulesText = `
2821
+ ### Safety Checks
2822
+ `;
2823
+ rulesText += `- Blocked words: ${foundBlocked.length > 0 ? `**FAILED** (found: ${foundBlocked.join(", ")})` : "PASS"}
2824
+ `;
2825
+ } catch {
2826
+ }
2071
2827
  const preview = `## Queue Post Preview
2072
2828
 
2073
2829
  **Content:** "${args.content}"
2074
2830
  **Platforms:** ${args.platforms.join(", ")}
2075
2831
  **Next queue slot:** ${slotDate2.toLocaleString()}
2076
2832
  ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
2077
- ` : "") + `
2833
+ ` : "") + rulesText + `
2078
2834
  Call this tool again with confirmed=true to schedule.`;
2079
2835
  return { content: [{ type: "text", text: preview }] };
2080
2836
  }
@@ -2107,7 +2863,7 @@ ${JSON.stringify(result, null, 2)}`
2107
2863
  }
2108
2864
 
2109
2865
  // src/tools/notifications.ts
2110
- import { z as z11 } from "zod";
2866
+ import { z as z12 } from "zod";
2111
2867
  function timeAgo(dateStr) {
2112
2868
  const diff = Date.now() - new Date(dateStr).getTime();
2113
2869
  const minutes = Math.floor(diff / 6e4);
@@ -2123,8 +2879,8 @@ function registerNotificationTools(server2, client2) {
2123
2879
  "get_notifications",
2124
2880
  "Get recent notifications including post publishing results, account health warnings, and errors. Use this to check if any posts failed or if any accounts need attention.",
2125
2881
  {
2126
- unread_only: z11.boolean().optional().describe("If true, only show unread notifications. Default false."),
2127
- limit: z11.number().optional().describe("Max notifications to return. Default 10.")
2882
+ unread_only: z12.boolean().optional().describe("If true, only show unread notifications. Default false."),
2883
+ limit: z12.number().optional().describe("Max notifications to return. Default 10.")
2128
2884
  },
2129
2885
  {
2130
2886
  title: "Get Notifications",
@@ -2190,29 +2946,1323 @@ Showing ${notifications.length} of ${result.unread_count !== void 0 ? "total" :
2190
2946
  );
2191
2947
  }
2192
2948
 
2193
- // src/index.ts
2194
- var apiKey = process.env.BUZZPOSTER_API_KEY;
2195
- var apiUrl = process.env.BUZZPOSTER_API_URL ?? "https://api.buzzposter.com";
2196
- if (!apiKey) {
2197
- console.error(
2198
- "Error: BUZZPOSTER_API_KEY environment variable is required."
2199
- );
2200
- console.error(
2201
- "Set it in your Claude Desktop MCP config or export it in your shell."
2202
- );
2203
- process.exit(1);
2204
- }
2949
+ // src/tools/publishing-rules.ts
2950
+ function registerPublishingRulesTools(server2, client2) {
2951
+ server2.tool(
2952
+ "get_publishing_rules",
2953
+ `Get the customer's publishing safety rules and default behaviors. IMPORTANT: Call this tool BEFORE publishing, scheduling, or sending any content. These rules define whether content should default to draft, whether previews are required, and what confirmations are needed before going live. Always respect these rules -- they exist to prevent accidental publishing.`,
2954
+ {},
2955
+ {
2956
+ title: "Get Publishing Rules",
2957
+ readOnlyHint: true,
2958
+ destructiveHint: false,
2959
+ idempotentHint: true,
2960
+ openWorldHint: false
2961
+ },
2962
+ async () => {
2963
+ const rules = await client2.getPublishingRules();
2964
+ let text = "## Publishing Rules\n\n";
2965
+ text += `Social media default: **${rules.socialDefaultAction ?? "draft"}**`;
2966
+ if (rules.socialDefaultAction === "draft") text += " (content is saved as draft, not published)";
2967
+ text += "\n";
2968
+ text += `Newsletter default: **${rules.newsletterDefaultAction ?? "draft"}**`;
2969
+ if (rules.newsletterDefaultAction === "draft") text += " (content is saved as draft, not sent)";
2970
+ text += "\n";
2971
+ text += `Preview required before publish: **${rules.requirePreviewBeforePublish ? "yes" : "no"}**
2972
+ `;
2973
+ text += `Double confirmation for newsletters: **${rules.requireDoubleConfirmNewsletter ? "yes" : "no"}**
2974
+ `;
2975
+ text += `Double confirmation for social: **${rules.requireDoubleConfirmSocial ? "yes" : "no"}**
2976
+ `;
2977
+ text += `Immediate publish allowed: **${rules.allowImmediatePublish ? "yes" : "no"}**
2978
+ `;
2979
+ text += `Immediate send allowed: **${rules.allowImmediateSend ? "yes" : "no"}**
2980
+ `;
2981
+ text += `Max posts per day: **${rules.maxPostsPerDay ?? "unlimited"}**
2982
+ `;
2983
+ if (rules.blockedWords && rules.blockedWords.length > 0) {
2984
+ text += `Blocked words: **${rules.blockedWords.join(", ")}**
2985
+ `;
2986
+ } else {
2987
+ text += "Blocked words: none\n";
2988
+ }
2989
+ if (rules.requiredDisclaimer) {
2990
+ text += `Required disclaimer: "${rules.requiredDisclaimer}"
2991
+ `;
2992
+ } else {
2993
+ text += "Required disclaimer: none\n";
2994
+ }
2995
+ return { content: [{ type: "text", text }] };
2996
+ }
2997
+ );
2998
+ }
2999
+
3000
+ // src/tools/audit-log.ts
3001
+ import { z as z13 } from "zod";
3002
+ function registerAuditLogTools(server2, client2) {
3003
+ server2.tool(
3004
+ "get_audit_log",
3005
+ "View the history of all publish, send, schedule, and delete actions taken on the customer's account. Use this to review what was published recently, check activity, and troubleshoot issues.",
3006
+ {
3007
+ limit: z13.number().optional().describe("Maximum number of entries to return (default 20, max 200)"),
3008
+ action: z13.string().optional().describe("Filter by action type prefix, e.g. 'post.published', 'newsletter.sent', 'post.' for all post actions"),
3009
+ resource_type: z13.string().optional().describe("Filter by resource type: post, newsletter, account, media, publishing_rules, brand_voice")
3010
+ },
3011
+ {
3012
+ title: "Get Audit Log",
3013
+ readOnlyHint: true,
3014
+ destructiveHint: false,
3015
+ idempotentHint: true,
3016
+ openWorldHint: false
3017
+ },
3018
+ async (args) => {
3019
+ const params = {};
3020
+ if (args.limit) params.limit = String(args.limit);
3021
+ if (args.action) params.action = args.action;
3022
+ if (args.resource_type) params.resource_type = args.resource_type;
3023
+ const data = await client2.getAuditLog(params);
3024
+ const entries = data?.entries ?? [];
3025
+ if (entries.length === 0) {
3026
+ return {
3027
+ content: [{ type: "text", text: "## Audit Log\n\nNo entries found." }]
3028
+ };
3029
+ }
3030
+ let text = `## Audit Log (${entries.length} of ${data?.total ?? entries.length} entries)
3031
+
3032
+ `;
3033
+ for (const entry of entries) {
3034
+ const date = entry.createdAt ? new Date(entry.createdAt).toLocaleString() : "unknown";
3035
+ const details = entry.details && Object.keys(entry.details).length > 0 ? ` \u2014 ${JSON.stringify(entry.details)}` : "";
3036
+ text += `- **${date}** | ${entry.action} | ${entry.resourceType}`;
3037
+ if (entry.resourceId) text += ` #${entry.resourceId}`;
3038
+ text += `${details}
3039
+ `;
3040
+ }
3041
+ return { content: [{ type: "text", text }] };
3042
+ }
3043
+ );
3044
+ }
3045
+
3046
+ // src/tools/sources.ts
3047
+ import { z as z14 } from "zod";
3048
+ var TYPE_LABELS = {
3049
+ feed: "RSS Feed",
3050
+ website: "Website",
3051
+ youtube: "YouTube Channel",
3052
+ search: "Search Topic",
3053
+ podcast: "Podcast",
3054
+ reddit: "Reddit",
3055
+ social: "Social Profile"
3056
+ };
3057
+ var TYPE_HINTS = {
3058
+ feed: "Use fetch_feed to get latest entries",
3059
+ website: "Use fetch_article to extract content",
3060
+ youtube: "Use fetch_feed to get latest videos (YouTube RSS)",
3061
+ search: "Use web_search with the searchQuery",
3062
+ podcast: "Use fetch_feed to get latest episodes",
3063
+ reddit: "Use fetch_feed to get latest posts (Reddit RSS)",
3064
+ social: "Use web_search to check recent activity"
3065
+ };
3066
+ function registerSourceTools(server2, client2) {
3067
+ server2.tool(
3068
+ "get_sources",
3069
+ "Get the customer's saved content sources \u2014 their curated list of RSS feeds, websites, YouTube channels, social accounts, and search topics that they monitor for content ideas and inspiration. Call this when the user asks 'what should I post about?', 'check my sources', 'what's new?', 'find me content ideas', or references their content sources. After getting sources, use fetch_feed (for feed/podcast/reddit/youtube types), fetch_article (for website types), or web_search (for search/social types) to actually retrieve the latest content from each source.",
3070
+ {
3071
+ type: z14.string().optional().describe(
3072
+ "Optional: filter by type (feed, website, youtube, search, podcast, reddit, social)"
3073
+ ),
3074
+ category: z14.string().optional().describe("Optional: filter by category")
3075
+ },
3076
+ {
3077
+ title: "Get Content Sources",
3078
+ readOnlyHint: true,
3079
+ destructiveHint: false,
3080
+ idempotentHint: true,
3081
+ openWorldHint: false
3082
+ },
3083
+ async (args) => {
3084
+ const params = {};
3085
+ if (args.type) params.type = args.type;
3086
+ if (args.category) params.category = args.category;
3087
+ const result = await client2.listSources(params);
3088
+ const sources = result.sources;
3089
+ if (sources.length === 0) {
3090
+ return {
3091
+ content: [
3092
+ {
3093
+ type: "text",
3094
+ text: "No content sources saved yet. Use add_source to save RSS feeds, websites, YouTube channels, or search topics the customer wants to monitor for content ideas."
3095
+ }
3096
+ ]
3097
+ };
3098
+ }
3099
+ const lines = [`## Content Sources (${sources.length})
3100
+ `];
3101
+ const byCategory = /* @__PURE__ */ new Map();
3102
+ for (const s of sources) {
3103
+ const cat = s.category || "Uncategorized";
3104
+ if (!byCategory.has(cat)) byCategory.set(cat, []);
3105
+ byCategory.get(cat).push(s);
3106
+ }
3107
+ for (const [cat, items] of byCategory) {
3108
+ lines.push(`### ${cat}
3109
+ `);
3110
+ for (const s of items) {
3111
+ const typeLabel = TYPE_LABELS[s.type] || s.type;
3112
+ const hint = TYPE_HINTS[s.type] || "";
3113
+ lines.push(`**${s.name}** (ID: ${s.id})`);
3114
+ lines.push(`- Type: ${typeLabel}`);
3115
+ if (s.url) lines.push(`- URL: ${s.url}`);
3116
+ if (s.searchQuery) lines.push(`- Search: "${s.searchQuery}"`);
3117
+ if (s.tags && s.tags.length > 0)
3118
+ lines.push(`- Tags: ${s.tags.join(", ")}`);
3119
+ if (s.notes) lines.push(`- Notes: ${s.notes}`);
3120
+ if (s.lastCheckedAt)
3121
+ lines.push(`- Last checked: ${s.lastCheckedAt}`);
3122
+ if (s.lastCheckedSummary)
3123
+ lines.push(`- Last result: ${s.lastCheckedSummary}`);
3124
+ if (hint) lines.push(`- How to check: ${hint}`);
3125
+ lines.push("");
3126
+ }
3127
+ }
3128
+ return {
3129
+ content: [{ type: "text", text: lines.join("\n") }]
3130
+ };
3131
+ }
3132
+ );
3133
+ server2.tool(
3134
+ "add_source",
3135
+ "Save a new content source to the customer's profile. Use this when the customer says 'add this to my sources', 'save this feed', 'monitor this blog', 'track this competitor', etc. For RSS feeds, set type to 'feed'. For regular websites/blogs without RSS, set type to 'website'. For YouTube channels, set type to 'youtube'. For search topics, set type to 'search' and provide searchQuery instead of url.",
3136
+ {
3137
+ name: z14.string().describe("Display name for the source"),
3138
+ url: z14.string().optional().describe(
3139
+ "URL of the source (RSS feed, website, YouTube channel, subreddit, or social profile URL). Not needed for 'search' type."
3140
+ ),
3141
+ type: z14.enum([
3142
+ "feed",
3143
+ "website",
3144
+ "youtube",
3145
+ "search",
3146
+ "podcast",
3147
+ "reddit",
3148
+ "social"
3149
+ ]).describe("Type of source. Determines how to fetch content from it."),
3150
+ category: z14.string().optional().describe(
3151
+ "Optional category for organization (e.g. 'competitors', 'industry', 'inspiration', 'my-content')"
3152
+ ),
3153
+ tags: z14.array(z14.string()).optional().describe("Optional tags for filtering"),
3154
+ notes: z14.string().optional().describe("Optional notes about why this source matters"),
3155
+ searchQuery: z14.string().optional().describe(
3156
+ "For 'search' type only: the keyword or topic to search for"
3157
+ )
3158
+ },
3159
+ {
3160
+ title: "Add Content Source",
3161
+ readOnlyHint: false,
3162
+ destructiveHint: false,
3163
+ idempotentHint: false,
3164
+ openWorldHint: false
3165
+ },
3166
+ async (args) => {
3167
+ const data = {
3168
+ name: args.name,
3169
+ type: args.type
3170
+ };
3171
+ if (args.url) data.url = args.url;
3172
+ if (args.category) data.category = args.category;
3173
+ if (args.tags) data.tags = args.tags;
3174
+ if (args.notes) data.notes = args.notes;
3175
+ if (args.searchQuery) data.searchQuery = args.searchQuery;
3176
+ const result = await client2.createSource(data);
3177
+ const typeLabel = TYPE_LABELS[result.type] || result.type;
3178
+ const lines = [
3179
+ `## Source Added
3180
+ `,
3181
+ `**${result.name}** (ID: ${result.id})`,
3182
+ `- Type: ${typeLabel}`
3183
+ ];
3184
+ if (result.url) lines.push(`- URL: ${result.url}`);
3185
+ if (result.searchQuery)
3186
+ lines.push(`- Search: "${result.searchQuery}"`);
3187
+ if (result.category) lines.push(`- Category: ${result.category}`);
3188
+ return {
3189
+ content: [{ type: "text", text: lines.join("\n") }]
3190
+ };
3191
+ }
3192
+ );
3193
+ server2.tool(
3194
+ "update_source",
3195
+ "Update a saved content source. Use when the customer wants to rename, recategorize, change the URL, add notes, or deactivate a source.",
3196
+ {
3197
+ source_id: z14.number().describe("The ID of the source to update"),
3198
+ name: z14.string().optional().describe("New display name"),
3199
+ url: z14.string().optional().describe("New URL"),
3200
+ type: z14.enum([
3201
+ "feed",
3202
+ "website",
3203
+ "youtube",
3204
+ "search",
3205
+ "podcast",
3206
+ "reddit",
3207
+ "social"
3208
+ ]).optional().describe("New source type"),
3209
+ category: z14.string().optional().describe("New category"),
3210
+ tags: z14.array(z14.string()).optional().describe("New tags"),
3211
+ notes: z14.string().optional().describe("New notes"),
3212
+ searchQuery: z14.string().optional().describe("New search query"),
3213
+ isActive: z14.boolean().optional().describe("Set to false to deactivate")
3214
+ },
3215
+ {
3216
+ title: "Update Content Source",
3217
+ readOnlyHint: false,
3218
+ destructiveHint: false,
3219
+ idempotentHint: true,
3220
+ openWorldHint: false
3221
+ },
3222
+ async (args) => {
3223
+ const { source_id, ...updates } = args;
3224
+ const data = {};
3225
+ for (const [k, v] of Object.entries(updates)) {
3226
+ if (v !== void 0) data[k] = v;
3227
+ }
3228
+ const result = await client2.updateSource(source_id, data);
3229
+ return {
3230
+ content: [
3231
+ {
3232
+ type: "text",
3233
+ text: `Source **${result.name}** (ID: ${result.id}) updated successfully.`
3234
+ }
3235
+ ]
3236
+ };
3237
+ }
3238
+ );
3239
+ server2.tool(
3240
+ "remove_source",
3241
+ `Remove a content source from the customer's saved sources. Use when the customer says 'remove that source', 'stop tracking X', 'delete that feed', etc.
3242
+
3243
+ REQUIRED WORKFLOW:
3244
+ 1. ALWAYS set confirmed=false first to preview which source will be deleted
3245
+ 2. Show the user the source details before confirming
3246
+ 3. Only confirm after explicit user approval`,
3247
+ {
3248
+ source_id: z14.number().describe("The ID of the source to remove"),
3249
+ confirmed: z14.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
3250
+ },
3251
+ {
3252
+ title: "Remove Content Source",
3253
+ readOnlyHint: false,
3254
+ destructiveHint: true,
3255
+ idempotentHint: false,
3256
+ openWorldHint: false
3257
+ },
3258
+ async (args) => {
3259
+ if (args.confirmed !== true) {
3260
+ const preview = `## Remove Source Confirmation
3261
+
3262
+ **Source ID:** ${args.source_id}
3263
+
3264
+ This will permanently remove this content source. Call this tool again with confirmed=true to proceed.`;
3265
+ return { content: [{ type: "text", text: preview }] };
3266
+ }
3267
+ await client2.deleteSource(args.source_id);
3268
+ return {
3269
+ content: [
3270
+ {
3271
+ type: "text",
3272
+ text: "Content source removed successfully."
3273
+ }
3274
+ ]
3275
+ };
3276
+ }
3277
+ );
3278
+ }
3279
+
3280
+ // src/tools/newsletter-advanced.ts
3281
+ import { z as z15 } from "zod";
3282
+ function registerNewsletterAdvancedTools(server2, client2) {
3283
+ server2.tool(
3284
+ "get_subscriber_by_email",
3285
+ "Look up a subscriber by their email address. Returns subscriber details including tags and custom fields.",
3286
+ {
3287
+ email: z15.string().describe("Email address to look up")
3288
+ },
3289
+ {
3290
+ title: "Get Subscriber by Email",
3291
+ readOnlyHint: true,
3292
+ destructiveHint: false,
3293
+ idempotentHint: true,
3294
+ openWorldHint: true
3295
+ },
3296
+ async (args) => {
3297
+ const result = await client2.getSubscriberByEmail(args.email);
3298
+ return {
3299
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3300
+ };
3301
+ }
3302
+ );
3303
+ server2.tool(
3304
+ "list_automations",
3305
+ "List all email automations/workflows from your ESP. On Beehiiv these are journeys; on Kit these are sequences.",
3306
+ {
3307
+ page: z15.string().optional().describe("Page number for pagination")
3308
+ },
3309
+ {
3310
+ title: "List Automations",
3311
+ readOnlyHint: true,
3312
+ destructiveHint: false,
3313
+ idempotentHint: true,
3314
+ openWorldHint: true
3315
+ },
3316
+ async (args) => {
3317
+ const params = {};
3318
+ if (args.page) params.page = args.page;
3319
+ const result = await client2.listAutomations(params);
3320
+ return {
3321
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3322
+ };
3323
+ }
3324
+ );
3325
+ server2.tool(
3326
+ "list_segments",
3327
+ "List subscriber segments. Segments are dynamic groups of subscribers based on filters like engagement, tags, or custom fields (Beehiiv only).",
3328
+ {
3329
+ page: z15.string().optional().describe("Page number"),
3330
+ type: z15.string().optional().describe("Filter by segment type"),
3331
+ status: z15.string().optional().describe("Filter by segment status"),
3332
+ expand: z15.string().optional().describe("Comma-separated expand fields (e.g. 'stats')")
3333
+ },
3334
+ {
3335
+ title: "List Segments",
3336
+ readOnlyHint: true,
3337
+ destructiveHint: false,
3338
+ idempotentHint: true,
3339
+ openWorldHint: true
3340
+ },
3341
+ async (args) => {
3342
+ const params = {};
3343
+ if (args.page) params.page = args.page;
3344
+ if (args.type) params.type = args.type;
3345
+ if (args.status) params.status = args.status;
3346
+ if (args.expand) params.expand = args.expand;
3347
+ const result = await client2.listSegments(params);
3348
+ return {
3349
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3350
+ };
3351
+ }
3352
+ );
3353
+ server2.tool(
3354
+ "get_segment",
3355
+ "Get details of a specific subscriber segment by ID.",
3356
+ {
3357
+ segment_id: z15.string().describe("The segment ID"),
3358
+ expand: z15.string().optional().describe("Comma-separated expand fields")
3359
+ },
3360
+ {
3361
+ title: "Get Segment",
3362
+ readOnlyHint: true,
3363
+ destructiveHint: false,
3364
+ idempotentHint: true,
3365
+ openWorldHint: true
3366
+ },
3367
+ async (args) => {
3368
+ const params = {};
3369
+ if (args.expand) params.expand = args.expand;
3370
+ const result = await client2.getSegment(args.segment_id, params);
3371
+ return {
3372
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3373
+ };
3374
+ }
3375
+ );
3376
+ server2.tool(
3377
+ "get_segment_members",
3378
+ "List subscribers who belong to a specific segment.",
3379
+ {
3380
+ segment_id: z15.string().describe("The segment ID"),
3381
+ page: z15.string().optional().describe("Page number")
3382
+ },
3383
+ {
3384
+ title: "Get Segment Members",
3385
+ readOnlyHint: true,
3386
+ destructiveHint: false,
3387
+ idempotentHint: true,
3388
+ openWorldHint: true
3389
+ },
3390
+ async (args) => {
3391
+ const params = {};
3392
+ if (args.page) params.page = args.page;
3393
+ const result = await client2.getSegmentMembers(args.segment_id, params);
3394
+ return {
3395
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3396
+ };
3397
+ }
3398
+ );
3399
+ server2.tool(
3400
+ "list_custom_fields",
3401
+ "List all custom subscriber fields defined in your ESP. Custom fields can store extra data like preferences, source, or company.",
3402
+ {},
3403
+ {
3404
+ title: "List Custom Fields",
3405
+ readOnlyHint: true,
3406
+ destructiveHint: false,
3407
+ idempotentHint: true,
3408
+ openWorldHint: true
3409
+ },
3410
+ async () => {
3411
+ const result = await client2.listCustomFields();
3412
+ return {
3413
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3414
+ };
3415
+ }
3416
+ );
3417
+ server2.tool(
3418
+ "get_referral_program",
3419
+ "Get your newsletter's referral program details including milestones and rewards (Beehiiv only).",
3420
+ {},
3421
+ {
3422
+ title: "Get Referral Program",
3423
+ readOnlyHint: true,
3424
+ destructiveHint: false,
3425
+ idempotentHint: true,
3426
+ openWorldHint: true
3427
+ },
3428
+ async () => {
3429
+ const result = await client2.getReferralProgram();
3430
+ return {
3431
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3432
+ };
3433
+ }
3434
+ );
3435
+ server2.tool(
3436
+ "list_tiers",
3437
+ "List all subscription tiers (free and premium) from your ESP (Beehiiv only).",
3438
+ {},
3439
+ {
3440
+ title: "List Tiers",
3441
+ readOnlyHint: true,
3442
+ destructiveHint: false,
3443
+ idempotentHint: true,
3444
+ openWorldHint: true
3445
+ },
3446
+ async () => {
3447
+ const result = await client2.listEspTiers();
3448
+ return {
3449
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3450
+ };
3451
+ }
3452
+ );
3453
+ server2.tool(
3454
+ "list_post_templates",
3455
+ "List all post/newsletter templates available in your ESP (Beehiiv only).",
3456
+ {},
3457
+ {
3458
+ title: "List Post Templates",
3459
+ readOnlyHint: true,
3460
+ destructiveHint: false,
3461
+ idempotentHint: true,
3462
+ openWorldHint: true
3463
+ },
3464
+ async () => {
3465
+ const result = await client2.listPostTemplates();
3466
+ return {
3467
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3468
+ };
3469
+ }
3470
+ );
3471
+ server2.tool(
3472
+ "get_post_aggregate_stats",
3473
+ "Get aggregate statistics across all posts/newsletters including total clicks, impressions, and click rates (Beehiiv only).",
3474
+ {},
3475
+ {
3476
+ title: "Get Post Aggregate Stats",
3477
+ readOnlyHint: true,
3478
+ destructiveHint: false,
3479
+ idempotentHint: true,
3480
+ openWorldHint: true
3481
+ },
3482
+ async () => {
3483
+ const result = await client2.getPostAggregateStats();
3484
+ return {
3485
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3486
+ };
3487
+ }
3488
+ );
3489
+ server2.tool(
3490
+ "list_esp_webhooks",
3491
+ "List all webhooks configured in your ESP for event notifications.",
3492
+ {},
3493
+ {
3494
+ title: "List ESP Webhooks",
3495
+ readOnlyHint: true,
3496
+ destructiveHint: false,
3497
+ idempotentHint: true,
3498
+ openWorldHint: true
3499
+ },
3500
+ async () => {
3501
+ const result = await client2.listEspWebhooks();
3502
+ return {
3503
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3504
+ };
3505
+ }
3506
+ );
3507
+ server2.tool(
3508
+ "tag_subscriber",
3509
+ "Add tags to a subscriber. Tags help categorize subscribers for targeted sends and automations.",
3510
+ {
3511
+ subscriber_id: z15.string().describe("The subscriber/subscription ID"),
3512
+ tags: z15.array(z15.string()).describe("Tags to add to the subscriber")
3513
+ },
3514
+ {
3515
+ title: "Tag Subscriber",
3516
+ readOnlyHint: false,
3517
+ destructiveHint: false,
3518
+ idempotentHint: true,
3519
+ openWorldHint: true
3520
+ },
3521
+ async (args) => {
3522
+ const result = await client2.tagSubscriber(args.subscriber_id, { tags: args.tags });
3523
+ return {
3524
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3525
+ };
3526
+ }
3527
+ );
3528
+ server2.tool(
3529
+ "update_subscriber",
3530
+ `Update a subscriber's details. On Beehiiv: update tier or custom fields. On Kit: update name or custom fields (tier not supported).
3531
+
3532
+ REQUIRED WORKFLOW:
3533
+ 1. ALWAYS set confirmed=false first to get a preview
3534
+ 2. Show the user what will happen before confirming
3535
+ 3. Only set confirmed=true after the user explicitly approves`,
3536
+ {
3537
+ subscriber_id: z15.string().describe("The subscriber/subscription ID"),
3538
+ tier: z15.string().optional().describe("New tier for the subscriber"),
3539
+ custom_fields: z15.record(z15.unknown()).optional().describe("Custom field values to set"),
3540
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm update")
3541
+ },
3542
+ {
3543
+ title: "Update Subscriber",
3544
+ readOnlyHint: false,
3545
+ destructiveHint: false,
3546
+ idempotentHint: true,
3547
+ openWorldHint: true
3548
+ },
3549
+ async (args) => {
3550
+ if (args.confirmed !== true) {
3551
+ const changes = [];
3552
+ if (args.tier) changes.push(`**Tier:** ${args.tier}`);
3553
+ if (args.custom_fields) changes.push(`**Custom fields:** ${Object.keys(args.custom_fields).join(", ")}`);
3554
+ return {
3555
+ content: [{
3556
+ type: "text",
3557
+ text: `## Update Subscriber
3558
+
3559
+ **Subscriber ID:** ${args.subscriber_id}
3560
+ ${changes.join("\n")}
3561
+
3562
+ Call again with confirmed=true to proceed.`
3563
+ }]
3564
+ };
3565
+ }
3566
+ const data = {};
3567
+ if (args.tier) data.tier = args.tier;
3568
+ if (args.custom_fields) data.customFields = args.custom_fields;
3569
+ const result = await client2.updateSubscriber(args.subscriber_id, data);
3570
+ return {
3571
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3572
+ };
3573
+ }
3574
+ );
3575
+ server2.tool(
3576
+ "create_custom_field",
3577
+ `Create a new custom subscriber field. On Beehiiv: kind can be 'string', 'integer', 'boolean', 'date', or 'enum'. On Kit: all fields are string-typed (kind is ignored). Creates a permanent schema-level field that affects all subscribers.
3578
+
3579
+ REQUIRED WORKFLOW:
3580
+ 1. ALWAYS set confirmed=false first to get a preview
3581
+ 2. Show the user what will happen before confirming
3582
+ 3. Only set confirmed=true after the user explicitly approves`,
3583
+ {
3584
+ name: z15.string().describe("Field name"),
3585
+ kind: z15.string().describe("Field type: string, integer, boolean, date, or enum"),
3586
+ options: z15.array(z15.string()).optional().describe("Options for enum-type fields"),
3587
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm creation")
3588
+ },
3589
+ {
3590
+ title: "Create Custom Field",
3591
+ readOnlyHint: false,
3592
+ destructiveHint: false,
3593
+ idempotentHint: false,
3594
+ openWorldHint: true
3595
+ },
3596
+ async (args) => {
3597
+ if (args.confirmed !== true) {
3598
+ const optionsLine = args.options ? `
3599
+ **Options:** ${args.options.join(", ")}` : "";
3600
+ return {
3601
+ content: [{
3602
+ type: "text",
3603
+ text: `## Create Custom Field
3604
+
3605
+ **Name:** ${args.name}
3606
+ **Kind:** ${args.kind}${optionsLine}
3607
+
3608
+ This creates a permanent schema-level field visible on all subscribers. Call again with confirmed=true to proceed.`
3609
+ }]
3610
+ };
3611
+ }
3612
+ const data = { name: args.name, kind: args.kind };
3613
+ if (args.options) data.options = args.options;
3614
+ const result = await client2.createCustomField(data);
3615
+ return {
3616
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3617
+ };
3618
+ }
3619
+ );
3620
+ server2.tool(
3621
+ "update_custom_field",
3622
+ `Update an existing custom subscriber field's name or kind. Renaming or rekeying a field can break automations that reference it.
3623
+
3624
+ REQUIRED WORKFLOW:
3625
+ 1. ALWAYS set confirmed=false first to get a preview
3626
+ 2. Show the user what will happen before confirming
3627
+ 3. Only set confirmed=true after the user explicitly approves`,
3628
+ {
3629
+ field_id: z15.string().describe("The custom field ID"),
3630
+ name: z15.string().optional().describe("New field name"),
3631
+ kind: z15.string().optional().describe("New field type"),
3632
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm update")
3633
+ },
3634
+ {
3635
+ title: "Update Custom Field",
3636
+ readOnlyHint: false,
3637
+ destructiveHint: false,
3638
+ idempotentHint: true,
3639
+ openWorldHint: true
3640
+ },
3641
+ async (args) => {
3642
+ if (args.confirmed !== true) {
3643
+ const changes = [];
3644
+ if (args.name) changes.push(`**Name:** ${args.name}`);
3645
+ if (args.kind) changes.push(`**Kind:** ${args.kind}`);
3646
+ return {
3647
+ content: [{
3648
+ type: "text",
3649
+ text: `## Update Custom Field
3650
+
3651
+ **Field ID:** ${args.field_id}
3652
+ ${changes.join("\n")}
3653
+
3654
+ Renaming or changing the type of a field can break automations that reference it. Call again with confirmed=true to proceed.`
3655
+ }]
3656
+ };
3657
+ }
3658
+ const data = {};
3659
+ if (args.name) data.name = args.name;
3660
+ if (args.kind) data.kind = args.kind;
3661
+ const result = await client2.updateCustomField(args.field_id, data);
3662
+ return {
3663
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3664
+ };
3665
+ }
3666
+ );
3667
+ server2.tool(
3668
+ "recalculate_segment",
3669
+ "Trigger a recalculation of a segment's membership. Useful after changing subscriber data or segment rules (Beehiiv only).",
3670
+ {
3671
+ segment_id: z15.string().describe("The segment ID to recalculate")
3672
+ },
3673
+ {
3674
+ title: "Recalculate Segment",
3675
+ readOnlyHint: false,
3676
+ destructiveHint: false,
3677
+ idempotentHint: true,
3678
+ openWorldHint: true
3679
+ },
3680
+ async (args) => {
3681
+ await client2.recalculateSegment(args.segment_id);
3682
+ return {
3683
+ content: [{ type: "text", text: "Segment recalculation triggered." }]
3684
+ };
3685
+ }
3686
+ );
3687
+ server2.tool(
3688
+ "enroll_in_automation",
3689
+ `Enroll a subscriber in an automation/journey. Provide either an email or subscription ID. On Kit, enrolls into a sequence.
3690
+
3691
+ REQUIRED WORKFLOW:
3692
+ 1. ALWAYS set confirmed=false first to get a preview
3693
+ 2. Show the user what will happen before confirming
3694
+ 3. Only set confirmed=true after the user explicitly approves`,
3695
+ {
3696
+ automation_id: z15.string().describe("The automation ID"),
3697
+ email: z15.string().optional().describe("Subscriber email to enroll"),
3698
+ subscription_id: z15.string().optional().describe("Subscription ID to enroll"),
3699
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm enrollment")
3700
+ },
3701
+ {
3702
+ title: "Enroll in Automation",
3703
+ readOnlyHint: false,
3704
+ destructiveHint: false,
3705
+ idempotentHint: false,
3706
+ openWorldHint: true
3707
+ },
3708
+ async (args) => {
3709
+ if (args.confirmed !== true) {
3710
+ const target = args.email ?? args.subscription_id ?? "unknown";
3711
+ return {
3712
+ content: [{
3713
+ type: "text",
3714
+ text: `## Enroll in Automation
3715
+
3716
+ **Automation ID:** ${args.automation_id}
3717
+ **Target:** ${target}
3718
+
3719
+ This will add the subscriber to the automation journey. Call again with confirmed=true to proceed.`
3720
+ }]
3721
+ };
3722
+ }
3723
+ const data = {};
3724
+ if (args.email) data.email = args.email;
3725
+ if (args.subscription_id) data.subscriptionId = args.subscription_id;
3726
+ await client2.enrollInAutomation(args.automation_id, data);
3727
+ return {
3728
+ content: [{ type: "text", text: "Subscriber enrolled in automation." }]
3729
+ };
3730
+ }
3731
+ );
3732
+ server2.tool(
3733
+ "bulk_add_subscribers",
3734
+ `Add multiple subscribers in a single request. Maximum 1000 subscribers per call.
3735
+
3736
+ REQUIRED WORKFLOW:
3737
+ 1. ALWAYS set confirmed=false first to get a preview
3738
+ 2. Show the user what will happen before confirming
3739
+ 3. Only set confirmed=true after the user explicitly approves`,
3740
+ {
3741
+ subscribers: z15.array(z15.object({
3742
+ email: z15.string().describe("Subscriber email"),
3743
+ data: z15.record(z15.unknown()).optional().describe("Additional subscriber data")
3744
+ })).describe("Array of subscribers to add"),
3745
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm bulk add")
3746
+ },
3747
+ {
3748
+ title: "Bulk Add Subscribers",
3749
+ readOnlyHint: false,
3750
+ destructiveHint: false,
3751
+ idempotentHint: false,
3752
+ openWorldHint: true
3753
+ },
3754
+ async (args) => {
3755
+ if (args.confirmed !== true) {
3756
+ return {
3757
+ content: [{
3758
+ type: "text",
3759
+ text: `## Bulk Add Subscribers
3760
+
3761
+ **Count:** ${args.subscribers.length} subscriber(s)
3762
+ **Sample:** ${args.subscribers.slice(0, 3).map((s) => s.email).join(", ")}${args.subscribers.length > 3 ? "..." : ""}
3763
+
3764
+ Call again with confirmed=true to proceed.`
3765
+ }]
3766
+ };
3767
+ }
3768
+ const result = await client2.bulkCreateSubscribers({ subscribers: args.subscribers });
3769
+ return {
3770
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3771
+ };
3772
+ }
3773
+ );
3774
+ server2.tool(
3775
+ "create_esp_webhook",
3776
+ `Create a webhook in your ESP to receive event notifications. Note: Kit only supports one event per webhook.
3777
+
3778
+ REQUIRED WORKFLOW:
3779
+ 1. ALWAYS set confirmed=false first to get a preview
3780
+ 2. Show the user what will happen before confirming
3781
+ 3. Only set confirmed=true after the user explicitly approves`,
3782
+ {
3783
+ url: z15.string().describe("Webhook URL to receive events"),
3784
+ event_types: z15.array(z15.string()).describe("Event types to subscribe to (e.g. 'subscription.created', 'post.sent')"),
3785
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm creation")
3786
+ },
3787
+ {
3788
+ title: "Create ESP Webhook",
3789
+ readOnlyHint: false,
3790
+ destructiveHint: false,
3791
+ idempotentHint: false,
3792
+ openWorldHint: true
3793
+ },
3794
+ async (args) => {
3795
+ if (args.confirmed !== true) {
3796
+ return {
3797
+ content: [{
3798
+ type: "text",
3799
+ text: `## Create ESP Webhook
3800
+
3801
+ **URL:** ${args.url}
3802
+ **Events:** ${args.event_types.join(", ")}
3803
+
3804
+ Call again with confirmed=true to create.`
3805
+ }]
3806
+ };
3807
+ }
3808
+ const result = await client2.createEspWebhook({ url: args.url, eventTypes: args.event_types });
3809
+ return {
3810
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3811
+ };
3812
+ }
3813
+ );
3814
+ server2.tool(
3815
+ "delete_broadcast",
3816
+ `Permanently delete a newsletter/broadcast. This action is permanent and cannot be undone.
3817
+
3818
+ REQUIRED WORKFLOW:
3819
+ 1. ALWAYS set confirmed=false first to get a preview
3820
+ 2. Show the user what will happen before confirming
3821
+ 3. Only set confirmed=true after the user explicitly approves`,
3822
+ {
3823
+ broadcast_id: z15.string().describe("The broadcast ID to delete"),
3824
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3825
+ },
3826
+ {
3827
+ title: "Delete Broadcast",
3828
+ readOnlyHint: false,
3829
+ destructiveHint: true,
3830
+ idempotentHint: true,
3831
+ openWorldHint: true
3832
+ },
3833
+ async (args) => {
3834
+ if (args.confirmed !== true) {
3835
+ return {
3836
+ content: [{
3837
+ type: "text",
3838
+ text: `## Delete Broadcast
3839
+
3840
+ **Broadcast ID:** ${args.broadcast_id}
3841
+
3842
+ **This action is permanent and cannot be undone.**
3843
+
3844
+ Call again with confirmed=true to delete.`
3845
+ }]
3846
+ };
3847
+ }
3848
+ await client2.deleteBroadcast(args.broadcast_id);
3849
+ return {
3850
+ content: [{ type: "text", text: "Broadcast deleted." }]
3851
+ };
3852
+ }
3853
+ );
3854
+ server2.tool(
3855
+ "delete_subscriber",
3856
+ `Remove a subscriber from your mailing list. On Beehiiv this permanently deletes the subscriber. On Kit this unsubscribes them (Kit has no hard delete).
3857
+
3858
+ REQUIRED WORKFLOW:
3859
+ 1. ALWAYS set confirmed=false first to get a preview
3860
+ 2. Show the user what will happen before confirming
3861
+ 3. Only set confirmed=true after the user explicitly approves`,
3862
+ {
3863
+ subscriber_id: z15.string().describe("The subscriber ID to delete"),
3864
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3865
+ },
3866
+ {
3867
+ title: "Delete Subscriber",
3868
+ readOnlyHint: false,
3869
+ destructiveHint: true,
3870
+ idempotentHint: true,
3871
+ openWorldHint: true
3872
+ },
3873
+ async (args) => {
3874
+ if (args.confirmed !== true) {
3875
+ return {
3876
+ content: [{
3877
+ type: "text",
3878
+ text: `## Delete Subscriber
3879
+
3880
+ **Subscriber ID:** ${args.subscriber_id}
3881
+
3882
+ **This action is permanent and cannot be undone.** The subscriber will be removed from all segments and automations.
3883
+
3884
+ Call again with confirmed=true to delete.`
3885
+ }]
3886
+ };
3887
+ }
3888
+ await client2.deleteSubscriber(args.subscriber_id);
3889
+ return {
3890
+ content: [{ type: "text", text: "Subscriber deleted." }]
3891
+ };
3892
+ }
3893
+ );
3894
+ server2.tool(
3895
+ "delete_segment",
3896
+ `Permanently delete a subscriber segment. This action is permanent and cannot be undone (Beehiiv only).
3897
+
3898
+ REQUIRED WORKFLOW:
3899
+ 1. ALWAYS set confirmed=false first to get a preview
3900
+ 2. Show the user what will happen before confirming
3901
+ 3. Only set confirmed=true after the user explicitly approves`,
3902
+ {
3903
+ segment_id: z15.string().describe("The segment ID to delete"),
3904
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3905
+ },
3906
+ {
3907
+ title: "Delete Segment",
3908
+ readOnlyHint: false,
3909
+ destructiveHint: true,
3910
+ idempotentHint: true,
3911
+ openWorldHint: true
3912
+ },
3913
+ async (args) => {
3914
+ if (args.confirmed !== true) {
3915
+ return {
3916
+ content: [{
3917
+ type: "text",
3918
+ text: `## Delete Segment
3919
+
3920
+ **Segment ID:** ${args.segment_id}
3921
+
3922
+ **This action is permanent and cannot be undone.** Subscribers in this segment will not be deleted, but the segment grouping will be removed.
3923
+
3924
+ Call again with confirmed=true to delete.`
3925
+ }]
3926
+ };
3927
+ }
3928
+ await client2.deleteSegment(args.segment_id);
3929
+ return {
3930
+ content: [{ type: "text", text: "Segment deleted." }]
3931
+ };
3932
+ }
3933
+ );
3934
+ server2.tool(
3935
+ "delete_custom_field",
3936
+ `Permanently delete a custom subscriber field and remove it from all subscribers. This action is permanent and cannot be undone.
3937
+
3938
+ REQUIRED WORKFLOW:
3939
+ 1. ALWAYS set confirmed=false first to get a preview
3940
+ 2. Show the user what will happen before confirming
3941
+ 3. Only set confirmed=true after the user explicitly approves`,
3942
+ {
3943
+ field_id: z15.string().describe("The custom field ID to delete"),
3944
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3945
+ },
3946
+ {
3947
+ title: "Delete Custom Field",
3948
+ readOnlyHint: false,
3949
+ destructiveHint: true,
3950
+ idempotentHint: true,
3951
+ openWorldHint: true
3952
+ },
3953
+ async (args) => {
3954
+ if (args.confirmed !== true) {
3955
+ return {
3956
+ content: [{
3957
+ type: "text",
3958
+ text: `## Delete Custom Field
3959
+
3960
+ **Field ID:** ${args.field_id}
3961
+
3962
+ **This action is permanent and cannot be undone.** The field and its data will be removed from all subscribers.
3963
+
3964
+ Call again with confirmed=true to delete.`
3965
+ }]
3966
+ };
3967
+ }
3968
+ await client2.deleteCustomField(args.field_id);
3969
+ return {
3970
+ content: [{ type: "text", text: "Custom field deleted." }]
3971
+ };
3972
+ }
3973
+ );
3974
+ server2.tool(
3975
+ "delete_esp_webhook",
3976
+ `Permanently delete a webhook from your ESP. This action is permanent and cannot be undone.
3977
+
3978
+ REQUIRED WORKFLOW:
3979
+ 1. ALWAYS set confirmed=false first to get a preview
3980
+ 2. Show the user what will happen before confirming
3981
+ 3. Only set confirmed=true after the user explicitly approves`,
3982
+ {
3983
+ webhook_id: z15.string().describe("The webhook ID to delete"),
3984
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3985
+ },
3986
+ {
3987
+ title: "Delete ESP Webhook",
3988
+ readOnlyHint: false,
3989
+ destructiveHint: true,
3990
+ idempotentHint: true,
3991
+ openWorldHint: true
3992
+ },
3993
+ async (args) => {
3994
+ if (args.confirmed !== true) {
3995
+ return {
3996
+ content: [{
3997
+ type: "text",
3998
+ text: `## Delete ESP Webhook
3999
+
4000
+ **Webhook ID:** ${args.webhook_id}
4001
+
4002
+ **This action is permanent and cannot be undone.** You will stop receiving event notifications at this webhook URL.
4003
+
4004
+ Call again with confirmed=true to delete.`
4005
+ }]
4006
+ };
4007
+ }
4008
+ await client2.deleteEspWebhook(args.webhook_id);
4009
+ return {
4010
+ content: [{ type: "text", text: "Webhook deleted." }]
4011
+ };
4012
+ }
4013
+ );
4014
+ }
4015
+
4016
+ // src/tools/carousel.ts
4017
+ import { z as z16 } from "zod";
4018
+ function registerCarouselTools(server2, client2) {
4019
+ server2.tool(
4020
+ "get_carousel_template",
4021
+ "Get the customer's carousel template configuration. Returns brand colors, logo, CTA text, and other visual settings used to generate carousels from article URLs.",
4022
+ {},
4023
+ {
4024
+ title: "Get Carousel Template",
4025
+ readOnlyHint: true,
4026
+ destructiveHint: false,
4027
+ idempotentHint: true,
4028
+ openWorldHint: false
4029
+ },
4030
+ async () => {
4031
+ try {
4032
+ const template = await client2.getCarouselTemplate();
4033
+ const lines = [];
4034
+ lines.push(`## Carousel Template: ${template.name || "Not configured"}`);
4035
+ lines.push("");
4036
+ if (!template.id) {
4037
+ lines.push("No carousel template has been configured yet.");
4038
+ lines.push("");
4039
+ lines.push("Use `save_carousel_template` to set one up with your brand name, colors, and CTA text.");
4040
+ return {
4041
+ content: [{ type: "text", text: lines.join("\n") }]
4042
+ };
4043
+ }
4044
+ lines.push(`**Brand Name:** ${template.brandName || "\u2014"}`);
4045
+ if (template.logoUrl) lines.push(`**Logo:** ${template.logoUrl}`);
4046
+ lines.push(`**Primary Color:** ${template.colorPrimary || "#1a1a2e"}`);
4047
+ lines.push(`**Secondary Color:** ${template.colorSecondary || "#e94560"}`);
4048
+ lines.push(`**Accent Color:** ${template.colorAccent || "#ffffff"}`);
4049
+ lines.push(`**CTA Text:** ${template.ctaText || "Read the full story \u2192"}`);
4050
+ lines.push(`**Logo Placement:** ${template.logoPlacement || "top-left"}`);
4051
+ lines.push("");
4052
+ return {
4053
+ content: [{ type: "text", text: lines.join("\n") }]
4054
+ };
4055
+ } catch (error) {
4056
+ const message = error instanceof Error ? error.message : "Unknown error";
4057
+ if (message.includes("404")) {
4058
+ return {
4059
+ content: [
4060
+ {
4061
+ type: "text",
4062
+ text: "No carousel template has been configured yet. Use `save_carousel_template` to set one up."
4063
+ }
4064
+ ]
4065
+ };
4066
+ }
4067
+ throw error;
4068
+ }
4069
+ }
4070
+ );
4071
+ server2.tool(
4072
+ "save_carousel_template",
4073
+ `Create or update the customer's carousel template. This configures the visual style for auto-generated carousels (brand colors, logo, CTA text).
4074
+
4075
+ USAGE:
4076
+ - Set confirmed=false first to preview, then confirmed=true after user approval
4077
+ - brand_name is required
4078
+ - All color values should be hex codes (e.g. "#1a1a2e")
4079
+ - logo_placement: "top-left", "top-right", or "top-center"`,
4080
+ {
4081
+ brand_name: z16.string().max(255).describe("Brand name displayed on CTA slide"),
4082
+ color_primary: z16.string().max(20).optional().describe('Primary background color (hex, e.g. "#1a1a2e")'),
4083
+ color_secondary: z16.string().max(20).optional().describe('Secondary/accent color for buttons and highlights (hex, e.g. "#e94560")'),
4084
+ color_accent: z16.string().max(20).optional().describe('Text color (hex, e.g. "#ffffff")'),
4085
+ cta_text: z16.string().max(255).optional().describe('Call-to-action text on the final slide (e.g. "Read the full story \u2192")'),
4086
+ logo_placement: z16.enum(["top-left", "top-right", "top-center"]).optional().describe("Where to place the logo on slides"),
4087
+ confirmed: z16.boolean().default(false).describe("Set to true to confirm and save. If false, returns a preview.")
4088
+ },
4089
+ {
4090
+ title: "Save Carousel Template",
4091
+ readOnlyHint: false,
4092
+ destructiveHint: false,
4093
+ idempotentHint: false,
4094
+ openWorldHint: false
4095
+ },
4096
+ async (args) => {
4097
+ const payload = {
4098
+ brandName: args.brand_name
4099
+ };
4100
+ if (args.color_primary !== void 0) payload.colorPrimary = args.color_primary;
4101
+ if (args.color_secondary !== void 0) payload.colorSecondary = args.color_secondary;
4102
+ if (args.color_accent !== void 0) payload.colorAccent = args.color_accent;
4103
+ if (args.cta_text !== void 0) payload.ctaText = args.cta_text;
4104
+ if (args.logo_placement !== void 0) payload.logoPlacement = args.logo_placement;
4105
+ if (args.confirmed !== true) {
4106
+ const lines2 = [];
4107
+ lines2.push("## Carousel Template Preview");
4108
+ lines2.push("");
4109
+ lines2.push(`**Brand Name:** ${args.brand_name}`);
4110
+ if (args.color_primary) lines2.push(`**Primary Color:** ${args.color_primary}`);
4111
+ if (args.color_secondary) lines2.push(`**Secondary Color:** ${args.color_secondary}`);
4112
+ if (args.color_accent) lines2.push(`**Accent Color:** ${args.color_accent}`);
4113
+ if (args.cta_text) lines2.push(`**CTA Text:** ${args.cta_text}`);
4114
+ if (args.logo_placement) lines2.push(`**Logo Placement:** ${args.logo_placement}`);
4115
+ lines2.push("");
4116
+ lines2.push("---");
4117
+ lines2.push("Call this tool again with **confirmed=true** to save these settings.");
4118
+ return {
4119
+ content: [{ type: "text", text: lines2.join("\n") }]
4120
+ };
4121
+ }
4122
+ const result = await client2.updateCarouselTemplate(payload);
4123
+ const lines = [];
4124
+ lines.push(`Carousel template "${result.brandName || args.brand_name}" has been saved successfully.`);
4125
+ lines.push("");
4126
+ const updated = ["brand name"];
4127
+ if (args.color_primary) updated.push("primary color");
4128
+ if (args.color_secondary) updated.push("secondary color");
4129
+ if (args.color_accent) updated.push("accent color");
4130
+ if (args.cta_text) updated.push("CTA text");
4131
+ if (args.logo_placement) updated.push("logo placement");
4132
+ lines.push(`**Updated:** ${updated.join(", ")}`);
4133
+ return {
4134
+ content: [{ type: "text", text: lines.join("\n") }]
4135
+ };
4136
+ }
4137
+ );
4138
+ server2.tool(
4139
+ "generate_carousel",
4140
+ "Generate a full multi-slide carousel from an article URL. Extracts headline and key points using AI, renders branded slides (hero + key points + CTA), and uploads them to CDN. Returns an array of CDN URLs for the rendered slides. Requires a carousel template to be configured first via save_carousel_template.",
4141
+ {
4142
+ url: z16.string().url().describe("The article URL to generate a carousel from")
4143
+ },
4144
+ {
4145
+ title: "Generate Carousel",
4146
+ readOnlyHint: false,
4147
+ destructiveHint: false,
4148
+ idempotentHint: false,
4149
+ openWorldHint: true
4150
+ },
4151
+ async (args) => {
4152
+ try {
4153
+ const result = await client2.generateCarousel({
4154
+ url: args.url
4155
+ });
4156
+ const lines = [];
4157
+ lines.push("## Carousel Generated");
4158
+ lines.push("");
4159
+ lines.push(`**Headline:** ${result.article.headline}`);
4160
+ lines.push(`**Source:** ${result.article.sourceDomain}`);
4161
+ lines.push(`**Slides:** ${result.slides.length}`);
4162
+ lines.push("");
4163
+ lines.push("### Key Points");
4164
+ for (const point of result.article.keyPoints) {
4165
+ lines.push(`- ${point}`);
4166
+ }
4167
+ lines.push("");
4168
+ lines.push("### Slide URLs");
4169
+ for (const slide of result.slides) {
4170
+ lines.push(`${slide.slideNumber}. [${slide.type}](${slide.cdnUrl})`);
4171
+ }
4172
+ lines.push("");
4173
+ lines.push("These URLs can be used as `media_urls` when creating a post.");
4174
+ return {
4175
+ content: [{ type: "text", text: lines.join("\n") }]
4176
+ };
4177
+ } catch (error) {
4178
+ const message = error instanceof Error ? error.message : "Unknown error";
4179
+ return {
4180
+ content: [
4181
+ {
4182
+ type: "text",
4183
+ text: `Failed to generate carousel: ${message}`
4184
+ }
4185
+ ],
4186
+ isError: true
4187
+ };
4188
+ }
4189
+ }
4190
+ );
4191
+ server2.tool(
4192
+ "preview_carousel",
4193
+ "Generate a quick preview of the carousel hero slide from an article URL. Useful for checking how the template looks before generating the full carousel. Requires a carousel template to be configured first.",
4194
+ {
4195
+ url: z16.string().url().describe("The article URL to preview")
4196
+ },
4197
+ {
4198
+ title: "Preview Carousel",
4199
+ readOnlyHint: true,
4200
+ destructiveHint: false,
4201
+ idempotentHint: false,
4202
+ openWorldHint: true
4203
+ },
4204
+ async (args) => {
4205
+ try {
4206
+ const result = await client2.previewCarousel({
4207
+ url: args.url
4208
+ });
4209
+ const lines = [];
4210
+ lines.push("## Carousel Preview");
4211
+ lines.push("");
4212
+ lines.push(`**Headline:** ${result.article.headline}`);
4213
+ lines.push(`**Source:** ${result.article.sourceDomain}`);
4214
+ lines.push("");
4215
+ lines.push(`**Hero Slide:** ${result.cdnUrl}`);
4216
+ lines.push("");
4217
+ lines.push("Use `generate_carousel` to create the full carousel with all slides.");
4218
+ return {
4219
+ content: [{ type: "text", text: lines.join("\n") }]
4220
+ };
4221
+ } catch (error) {
4222
+ const message = error instanceof Error ? error.message : "Unknown error";
4223
+ return {
4224
+ content: [
4225
+ {
4226
+ type: "text",
4227
+ text: `Failed to preview carousel: ${message}`
4228
+ }
4229
+ ],
4230
+ isError: true
4231
+ };
4232
+ }
4233
+ }
4234
+ );
4235
+ }
4236
+
4237
+ // src/index.ts
4238
+ var apiKey = process.env.BUZZPOSTER_API_KEY;
4239
+ var apiUrl = process.env.BUZZPOSTER_API_URL ?? "https://api.buzzposter.com";
4240
+ if (!apiKey) {
4241
+ console.error(
4242
+ "Error: BUZZPOSTER_API_KEY environment variable is required."
4243
+ );
4244
+ console.error(
4245
+ "Set it in your Claude Desktop MCP config or export it in your shell."
4246
+ );
4247
+ process.exit(1);
4248
+ }
2205
4249
  var client = new BuzzPosterClient({ baseUrl: apiUrl, apiKey });
2206
4250
  var server = new McpServer({
2207
4251
  name: "buzzposter",
2208
4252
  version: "0.1.0"
2209
4253
  });
4254
+ var allowDirectSend = false;
4255
+ try {
4256
+ const account = await client.getAccount();
4257
+ allowDirectSend = account?.allowDirectSend === true;
4258
+ } catch {
4259
+ }
2210
4260
  registerPostTools(server, client);
2211
4261
  registerAccountTools(server, client);
2212
4262
  registerAnalyticsTools(server, client);
2213
4263
  registerInboxTools(server, client);
2214
4264
  registerMediaTools(server, client);
2215
- registerNewsletterTools(server, client);
4265
+ registerNewsletterTools(server, client, { allowDirectSend });
2216
4266
  registerRssTools(server, client);
2217
4267
  registerAccountInfoTool(server, client);
2218
4268
  registerBrandVoiceTools(server, client);
@@ -2221,5 +4271,10 @@ registerAudienceTools(server, client);
2221
4271
  registerNewsletterTemplateTools(server, client);
2222
4272
  registerCalendarTools(server, client);
2223
4273
  registerNotificationTools(server, client);
4274
+ registerPublishingRulesTools(server, client);
4275
+ registerAuditLogTools(server, client);
4276
+ registerSourceTools(server, client);
4277
+ registerNewsletterAdvancedTools(server, client);
4278
+ registerCarouselTools(server, client);
2224
4279
  var transport = new StdioServerTransport();
2225
4280
  await server.connect(transport);