@buzzposter/mcp 0.1.4 → 0.1.6

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
@@ -101,12 +101,28 @@ var BuzzPosterClient = class {
101
101
  });
102
102
  }
103
103
  // Media
104
- async uploadMedia(filename, base64Data, mimeType) {
105
- return this.request("POST", "/api/v1/media/upload-base64", {
106
- filename,
107
- data: base64Data,
108
- mimeType
104
+ async uploadMediaMultipart(filename, buffer, mimeType) {
105
+ const formData = new FormData();
106
+ formData.append("file", new Blob([buffer], { type: mimeType }), filename);
107
+ const url = new URL(`${this.baseUrl}/api/v1/media/upload`);
108
+ const res = await fetch(url, {
109
+ method: "POST",
110
+ headers: { Authorization: `Bearer ${this.apiKey}` },
111
+ body: formData
109
112
  });
113
+ if (!res.ok) {
114
+ const errorBody = await res.json().catch(() => ({ message: res.statusText }));
115
+ const message = typeof errorBody === "object" && errorBody !== null && "message" in errorBody ? String(errorBody.message) : `API error (${res.status})`;
116
+ throw new Error(`BuzzPoster API error (${res.status}): ${message}`);
117
+ }
118
+ const text = await res.text();
119
+ return text ? JSON.parse(text) : void 0;
120
+ }
121
+ async uploadFromUrl(data) {
122
+ return this.request("POST", "/api/v1/media/upload-from-url", data);
123
+ }
124
+ async getUploadUrl(data) {
125
+ return this.request("POST", "/api/v1/media/presign", data);
110
126
  }
111
127
  async listMedia() {
112
128
  return this.request("GET", "/api/v1/media");
@@ -152,6 +168,79 @@ var BuzzPosterClient = class {
152
168
  async listForms() {
153
169
  return this.request("GET", "/api/v1/newsletters/forms");
154
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
+ }
155
244
  // Knowledge
156
245
  async listKnowledge(params) {
157
246
  return this.request("GET", "/api/v1/knowledge", void 0, params);
@@ -163,6 +252,9 @@ var BuzzPosterClient = class {
163
252
  async getBrandVoice() {
164
253
  return this.request("GET", "/api/v1/brand-voice");
165
254
  }
255
+ async updateBrandVoice(data) {
256
+ return this.request("PUT", "/api/v1/brand-voice", data);
257
+ }
166
258
  // Audiences
167
259
  async getAudience(id) {
168
260
  return this.request("GET", `/api/v1/audiences/${id}`);
@@ -170,6 +262,9 @@ var BuzzPosterClient = class {
170
262
  async getDefaultAudience() {
171
263
  return this.request("GET", "/api/v1/audiences/default");
172
264
  }
265
+ async updateAudience(id, data) {
266
+ return this.request("PUT", `/api/v1/audiences/${id}`, data);
267
+ }
173
268
  // Newsletter Templates
174
269
  async getTemplate(id) {
175
270
  if (id) return this.request("GET", `/api/v1/templates/${id}`);
@@ -229,6 +324,58 @@ var BuzzPosterClient = class {
229
324
  async markAllNotificationsRead() {
230
325
  return this.request("PUT", "/api/v1/notifications/read-all");
231
326
  }
327
+ // Content Sources
328
+ async listSources(params) {
329
+ return this.request("GET", "/api/v1/content-sources", void 0, params);
330
+ }
331
+ async createSource(data) {
332
+ return this.request("POST", "/api/v1/content-sources", data);
333
+ }
334
+ async updateSource(id, data) {
335
+ return this.request("PUT", `/api/v1/content-sources/${id}`, data);
336
+ }
337
+ async deleteSource(id) {
338
+ return this.request("DELETE", `/api/v1/content-sources/${id}`);
339
+ }
340
+ async checkSource(id) {
341
+ return this.request("POST", `/api/v1/content-sources/${id}/check`);
342
+ }
343
+ // Publishing Rules
344
+ async getPublishingRules() {
345
+ return this.request("GET", "/api/v1/publishing-rules");
346
+ }
347
+ // Audit Log
348
+ async getAuditLog(params) {
349
+ return this.request("GET", "/api/v1/audit-log", void 0, params);
350
+ }
351
+ // Newsletter Validation
352
+ async validateNewsletter(data) {
353
+ return this.request("POST", "/api/v1/newsletters/validate", data);
354
+ }
355
+ // Newsletter Schedule
356
+ async scheduleBroadcast(id, data) {
357
+ return this.request("POST", `/api/v1/newsletters/broadcasts/${id}/schedule`, data);
358
+ }
359
+ // Newsletter Test
360
+ async testBroadcast(id, data) {
361
+ return this.request("POST", `/api/v1/newsletters/broadcasts/${id}/test`, data);
362
+ }
363
+ // Carousel templates
364
+ async getCarouselTemplate() {
365
+ return this.request("GET", "/api/v1/carousel-templates");
366
+ }
367
+ async updateCarouselTemplate(data) {
368
+ return this.request("PUT", "/api/v1/carousel-templates", data);
369
+ }
370
+ async deleteCarouselTemplate() {
371
+ return this.request("DELETE", "/api/v1/carousel-templates");
372
+ }
373
+ async generateCarousel(data) {
374
+ return this.request("POST", "/api/v1/carousel-templates/generate", data);
375
+ }
376
+ async previewCarousel(data) {
377
+ return this.request("POST", "/api/v1/carousel-templates/preview", data);
378
+ }
232
379
  };
233
380
 
234
381
  // src/tools/posts.ts
@@ -236,7 +383,23 @@ import { z } from "zod";
236
383
  function registerPostTools(server2, client2) {
237
384
  server2.tool(
238
385
  "post",
239
- "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.",
386
+ `Create and publish a post to one or more social media platforms. Supports Twitter, Instagram, LinkedIn, and Facebook.
387
+
388
+ 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.
389
+
390
+ REQUIRED WORKFLOW:
391
+ 1. BEFORE creating any content, call get_brand_voice and get_audience to match the customer's tone
392
+ 2. BEFORE publishing, call get_publishing_rules to check safety settings
393
+ 3. ALWAYS set confirmed=false first to get a preview -- never set confirmed=true on the first call
394
+ 4. Show the user a visual preview of the post before confirming
395
+ 5. Only set confirmed=true after the user explicitly says to publish/post/send
396
+ 6. If publishing_rules.social_default_action is 'draft', create as draft unless the user explicitly asks to publish
397
+ 7. If publishing_rules.require_double_confirm_social is true, ask for confirmation twice before publishing
398
+ 8. NEVER publish immediately without user confirmation, even if the user says "post this" -- always show the preview first
399
+
400
+ 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.
401
+
402
+ 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.`,
240
403
  {
241
404
  content: z.string().optional().describe("The text content of the post"),
242
405
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -244,7 +407,7 @@ function registerPostTools(server2, client2) {
244
407
  platform_specific: z.record(z.record(z.unknown())).optional().describe(
245
408
  "Platform-specific data keyed by platform name. E.g. { twitter: { threadItems: [...] }, instagram: { firstComment: '...' } }"
246
409
  ),
247
- confirmed: z.boolean().optional().describe(
410
+ confirmed: z.boolean().default(false).describe(
248
411
  "Set to true to confirm and publish. If false or missing, returns a preview for user approval."
249
412
  )
250
413
  },
@@ -256,14 +419,52 @@ function registerPostTools(server2, client2) {
256
419
  openWorldHint: true
257
420
  },
258
421
  async (args) => {
259
- if (!args.confirmed) {
422
+ if (args.confirmed !== true) {
423
+ let rulesText = "";
424
+ try {
425
+ const rules = await client2.getPublishingRules();
426
+ const blockedWords = rules.blockedWords ?? [];
427
+ const foundBlocked = [];
428
+ if (args.content && blockedWords.length > 0) {
429
+ const lower = args.content.toLowerCase();
430
+ for (const word of blockedWords) {
431
+ if (lower.includes(word.toLowerCase())) foundBlocked.push(word);
432
+ }
433
+ }
434
+ rulesText = `
435
+ ### Safety Checks
436
+ `;
437
+ rulesText += `- Blocked words: ${foundBlocked.length > 0 ? `**FAILED** (found: ${foundBlocked.join(", ")})` : "PASS"}
438
+ `;
439
+ rulesText += `- Default action: ${rules.socialDefaultAction ?? "draft"}
440
+ `;
441
+ rulesText += `- Immediate publish allowed: ${rules.allowImmediatePublish ? "yes" : "no"}
442
+ `;
443
+ if (rules.maxPostsPerDay) rulesText += `- Daily post limit: ${rules.maxPostsPerDay}
444
+ `;
445
+ } catch {
446
+ }
447
+ const charCounts = {};
448
+ const limits = { twitter: 280, linkedin: 3e3, instagram: 2200, facebook: 63206, threads: 500 };
449
+ const contentLen = (args.content ?? "").length;
450
+ for (const p of args.platforms) {
451
+ const limit = limits[p.toLowerCase()] ?? 5e3;
452
+ charCounts[p] = { used: contentLen, limit, ok: contentLen <= limit };
453
+ }
454
+ let charText = "\n### Character Counts\n";
455
+ for (const [platform, counts] of Object.entries(charCounts)) {
456
+ charText += `- ${platform}: ${counts.used}/${counts.limit} ${counts.ok ? "" : "**OVER LIMIT**"}
457
+ `;
458
+ }
260
459
  const preview = `## Post Preview
261
460
 
262
461
  **Content:** "${args.content ?? "(no text)"}"
263
462
  **Platforms:** ${args.platforms.join(", ")}
264
463
  ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
265
- ` : "") + `
266
- This will be published **immediately**. Call this tool again with confirmed=true to proceed.`;
464
+ ` : "") + charText + rulesText + `
465
+ **Action:** This will be published **immediately** to ${args.platforms.length} platform(s).
466
+
467
+ Call this tool again with confirmed=true to proceed.`;
267
468
  return { content: [{ type: "text", text: preview }] };
268
469
  }
269
470
  const platforms = args.platforms.map((platform) => {
@@ -292,11 +493,23 @@ This will be published **immediately**. Call this tool again with confirmed=true
292
493
  );
293
494
  server2.tool(
294
495
  "cross_post",
295
- "Post the same content to all connected social media platforms at once. Requires confirmation before publishing.",
496
+ `Post the same content to all connected platforms at once.
497
+
498
+ 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.
499
+
500
+ REQUIRED WORKFLOW:
501
+ 1. BEFORE creating content, call get_brand_voice and get_audience
502
+ 2. BEFORE publishing, call get_publishing_rules
503
+ 3. ALWAYS set confirmed=false first to preview
504
+ 4. Show the user a visual preview showing how the post will appear on each platform
505
+ 5. This is a high-impact action (goes to ALL platforms) -- always require explicit confirmation
506
+ 6. If publishing_rules.require_double_confirm_social is true, ask twice
507
+
508
+ 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.`,
296
509
  {
297
510
  content: z.string().describe("The text content to post everywhere"),
298
511
  media_urls: z.array(z.string()).optional().describe("Public URLs of media to attach"),
299
- confirmed: z.boolean().optional().describe(
512
+ confirmed: z.boolean().default(false).describe(
300
513
  "Set to true to confirm and publish. If false or missing, returns a preview for user approval."
301
514
  )
302
515
  },
@@ -322,14 +535,38 @@ This will be published **immediately**. Call this tool again with confirmed=true
322
535
  ]
323
536
  };
324
537
  }
325
- if (!args.confirmed) {
538
+ if (args.confirmed !== true) {
539
+ let rulesText = "";
540
+ try {
541
+ const rules = await client2.getPublishingRules();
542
+ const blockedWords = rules.blockedWords ?? [];
543
+ const foundBlocked = [];
544
+ if (args.content && blockedWords.length > 0) {
545
+ const lower = args.content.toLowerCase();
546
+ for (const word of blockedWords) {
547
+ if (lower.includes(word.toLowerCase())) foundBlocked.push(word);
548
+ }
549
+ }
550
+ rulesText = `
551
+ ### Safety Checks
552
+ `;
553
+ rulesText += `- Blocked words: ${foundBlocked.length > 0 ? `**FAILED** (found: ${foundBlocked.join(", ")})` : "PASS"}
554
+ `;
555
+ rulesText += `- Default action: ${rules.socialDefaultAction ?? "draft"}
556
+ `;
557
+ rulesText += `- Double confirmation required: ${rules.requireDoubleConfirmSocial ? "yes" : "no"}
558
+ `;
559
+ } catch {
560
+ }
326
561
  const preview = `## Cross-Post Preview
327
562
 
328
563
  **Content:** "${args.content}"
329
- **Platforms:** ${platforms.join(", ")}
564
+ **Platforms:** ${platforms.join(", ")} (${platforms.length} platforms)
330
565
  ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
331
- ` : "") + `
332
- This will be published **immediately** to all connected platforms. Call this tool again with confirmed=true to proceed.`;
566
+ ` : "") + rulesText + `
567
+ **Action:** This will be published **immediately** to **all ${platforms.length} connected platforms**. This is a high-impact action.
568
+
569
+ Call this tool again with confirmed=true to proceed.`;
333
570
  return { content: [{ type: "text", text: preview }] };
334
571
  }
335
572
  const body = {
@@ -351,7 +588,21 @@ This will be published **immediately** to all connected platforms. Call this too
351
588
  );
352
589
  server2.tool(
353
590
  "schedule_post",
354
- "Schedule a post for future publication. Requires confirmation before scheduling.",
591
+ `Schedule a post for future publication.
592
+
593
+ 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.
594
+
595
+ REQUIRED WORKFLOW:
596
+ 1. BEFORE creating content, call get_brand_voice and get_audience
597
+ 2. BEFORE scheduling, call get_publishing_rules
598
+ 3. ALWAYS set confirmed=false first to preview
599
+ 4. Show the user a visual preview with the scheduled date/time before confirming
600
+ 5. Only confirm after explicit user approval
601
+ 6. If the user hasn't specified a time, suggest one based on get_queue time slots
602
+
603
+ 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.
604
+
605
+ 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.`,
355
606
  {
356
607
  content: z.string().optional().describe("The text content of the post"),
357
608
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -359,7 +610,7 @@ This will be published **immediately** to all connected platforms. Call this too
359
610
  timezone: z.string().default("UTC").describe("Timezone for the scheduled time, e.g. America/New_York"),
360
611
  media_urls: z.array(z.string()).optional().describe("Media URLs to attach"),
361
612
  platform_specific: z.record(z.record(z.unknown())).optional().describe("Platform-specific data keyed by platform name"),
362
- confirmed: z.boolean().optional().describe(
613
+ confirmed: z.boolean().default(false).describe(
363
614
  "Set to true to confirm scheduling. If false or missing, returns a preview for user approval."
364
615
  )
365
616
  },
@@ -371,14 +622,32 @@ This will be published **immediately** to all connected platforms. Call this too
371
622
  openWorldHint: true
372
623
  },
373
624
  async (args) => {
374
- if (!args.confirmed) {
625
+ if (args.confirmed !== true) {
626
+ let rulesText = "";
627
+ try {
628
+ const rules = await client2.getPublishingRules();
629
+ const blockedWords = rules.blockedWords ?? [];
630
+ const foundBlocked = [];
631
+ if (args.content && blockedWords.length > 0) {
632
+ const lower = args.content.toLowerCase();
633
+ for (const word of blockedWords) {
634
+ if (lower.includes(word.toLowerCase())) foundBlocked.push(word);
635
+ }
636
+ }
637
+ rulesText = `
638
+ ### Safety Checks
639
+ `;
640
+ rulesText += `- Blocked words: ${foundBlocked.length > 0 ? `**FAILED** (found: ${foundBlocked.join(", ")})` : "PASS"}
641
+ `;
642
+ } catch {
643
+ }
375
644
  const preview = `## Schedule Preview
376
645
 
377
646
  **Content:** "${args.content ?? "(no text)"}"
378
647
  **Platforms:** ${args.platforms.join(", ")}
379
648
  **Scheduled for:** ${args.scheduled_for} (${args.timezone})
380
649
  ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
381
- ` : "") + `
650
+ ` : "") + rulesText + `
382
651
  Call this tool again with confirmed=true to schedule.`;
383
652
  return { content: [{ type: "text", text: preview }] };
384
653
  }
@@ -409,7 +678,7 @@ Call this tool again with confirmed=true to schedule.`;
409
678
  );
410
679
  server2.tool(
411
680
  "create_draft",
412
- "Save a post as a draft without publishing.",
681
+ "Save a post as a draft without publishing. This NEVER publishes \u2014 it only saves. No confirmation needed since nothing goes live.",
413
682
  {
414
683
  content: z.string().optional().describe("The text content of the draft"),
415
684
  platforms: z.array(z.string()).describe("Platforms this draft is intended for"),
@@ -425,8 +694,7 @@ Call this tool again with confirmed=true to schedule.`;
425
694
  async (args) => {
426
695
  const body = {
427
696
  content: args.content,
428
- platforms: args.platforms.map((p) => ({ platform: p })),
429
- publishNow: false
697
+ platforms: args.platforms.map((p) => ({ platform: p }))
430
698
  };
431
699
  if (args.media_urls?.length) {
432
700
  body.mediaItems = args.media_urls.map((url) => ({
@@ -489,7 +757,7 @@ Call this tool again with confirmed=true to schedule.`;
489
757
  "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.",
490
758
  {
491
759
  post_id: z.string().describe("The ID of the post to retry"),
492
- confirmed: z.boolean().optional().describe(
760
+ confirmed: z.boolean().default(false).describe(
493
761
  "Set to true to confirm retry. If false/missing, shows post details first."
494
762
  )
495
763
  },
@@ -501,7 +769,7 @@ Call this tool again with confirmed=true to schedule.`;
501
769
  openWorldHint: true
502
770
  },
503
771
  async (args) => {
504
- if (!args.confirmed) {
772
+ if (args.confirmed !== true) {
505
773
  const post = await client2.getPost(args.post_id);
506
774
  const platforms = post.platforms ?? [];
507
775
  const failed = platforms.filter((p) => p.status === "failed");
@@ -540,7 +808,7 @@ Retrying will only attempt the failed platforms. Call this tool again with confi
540
808
  function registerAccountTools(server2, client2) {
541
809
  server2.tool(
542
810
  "list_accounts",
543
- "List all connected social media accounts with their platform, username, and connection status.",
811
+ "List all connected accounts \u2014 both social media platforms (Twitter, Instagram, etc.) and email service provider / ESP (Kit, Beehiiv, Mailchimp). Use this whenever the user asks about their accounts, connections, or integrations.",
544
812
  {},
545
813
  {
546
814
  title: "List Connected Accounts",
@@ -551,8 +819,44 @@ function registerAccountTools(server2, client2) {
551
819
  },
552
820
  async () => {
553
821
  const result = await client2.listAccounts();
822
+ const accounts = result.accounts ?? [];
823
+ const esp = result.esp;
824
+ if (accounts.length === 0 && !esp) {
825
+ return {
826
+ content: [{
827
+ type: "text",
828
+ text: "## Connected Accounts\n\nNo accounts connected. Connect social media accounts and/or an email service provider via the dashboard."
829
+ }]
830
+ };
831
+ }
832
+ let text = "## Connected Accounts\n\n";
833
+ text += "### Social Media\n";
834
+ if (accounts.length > 0) {
835
+ for (const a of accounts) {
836
+ const icon = a.isActive ? "\u2705" : "\u274C";
837
+ const platform = a.platform.charAt(0).toUpperCase() + a.platform.slice(1);
838
+ const name = a.username || a.displayName || a.platform;
839
+ text += `${icon} **${platform}** \u2014 @${name}
840
+ `;
841
+ }
842
+ } else {
843
+ text += "No social accounts connected.\n";
844
+ }
845
+ text += "\n### Email Service Provider (ESP)\n";
846
+ if (esp && esp.connected) {
847
+ const provider = esp.provider.charAt(0).toUpperCase() + esp.provider.slice(1);
848
+ text += `\u2705 **${provider}** \u2014 Connected`;
849
+ if (esp.publicationId) text += ` (Publication: ${esp.publicationId})`;
850
+ text += "\n";
851
+ } else if (esp) {
852
+ const provider = esp.provider.charAt(0).toUpperCase() + esp.provider.slice(1);
853
+ text += `\u26A0\uFE0F **${provider}** \u2014 Provider set but API key missing
854
+ `;
855
+ } else {
856
+ text += "No ESP configured.\n";
857
+ }
554
858
  return {
555
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
859
+ content: [{ type: "text", text }]
556
860
  };
557
861
  }
558
862
  );
@@ -684,11 +988,16 @@ function registerInboxTools(server2, client2) {
684
988
  );
685
989
  server2.tool(
686
990
  "reply_to_conversation",
687
- "Send a reply message in a DM conversation. Sends a message to an external platform on your behalf.",
991
+ `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.
992
+
993
+ REQUIRED WORKFLOW:
994
+ 1. ALWAYS set confirmed=false first to preview the reply
995
+ 2. Show the user the reply text and which platform/conversation it will be sent to
996
+ 3. Only confirm after explicit user approval`,
688
997
  {
689
998
  conversation_id: z3.string().describe("The conversation ID to reply to"),
690
999
  message: z3.string().describe("The reply message text"),
691
- confirmed: z3.boolean().optional().describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
1000
+ confirmed: z3.boolean().default(false).describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
692
1001
  },
693
1002
  {
694
1003
  title: "Reply to Conversation",
@@ -698,7 +1007,7 @@ function registerInboxTools(server2, client2) {
698
1007
  openWorldHint: true
699
1008
  },
700
1009
  async (args) => {
701
- if (!args.confirmed) {
1010
+ if (args.confirmed !== true) {
702
1011
  const preview = `## Reply Preview
703
1012
 
704
1013
  **Conversation:** ${args.conversation_id}
@@ -740,11 +1049,16 @@ This will send a DM reply on the external platform. Call this tool again with co
740
1049
  );
741
1050
  server2.tool(
742
1051
  "reply_to_comment",
743
- "Reply to a comment on one of your posts. Sends a reply on the external platform.",
1052
+ `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.
1053
+
1054
+ REQUIRED WORKFLOW:
1055
+ 1. ALWAYS set confirmed=false first to preview
1056
+ 2. Show the user the reply and the original comment for context
1057
+ 3. Only confirm after explicit user approval -- public replies are visible to everyone`,
744
1058
  {
745
1059
  comment_id: z3.string().describe("The comment ID to reply to"),
746
1060
  message: z3.string().describe("The reply text"),
747
- confirmed: z3.boolean().optional().describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
1061
+ confirmed: z3.boolean().default(false).describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
748
1062
  },
749
1063
  {
750
1064
  title: "Reply to Comment",
@@ -754,7 +1068,7 @@ This will send a DM reply on the external platform. Call this tool again with co
754
1068
  openWorldHint: true
755
1069
  },
756
1070
  async (args) => {
757
- if (!args.confirmed) {
1071
+ if (args.confirmed !== true) {
758
1072
  const preview = `## Comment Reply Preview
759
1073
 
760
1074
  **Comment ID:** ${args.comment_id}
@@ -792,11 +1106,16 @@ This will post a public reply. Call this tool again with confirmed=true to send.
792
1106
  );
793
1107
  server2.tool(
794
1108
  "reply_to_review",
795
- "Reply to a review on your Facebook page. Sends a public reply on the external platform.",
1109
+ `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.
1110
+
1111
+ REQUIRED WORKFLOW:
1112
+ 1. ALWAYS set confirmed=false first to preview
1113
+ 2. Show the user the reply, the original review, and the star rating for context
1114
+ 3. Only confirm after explicit user approval -- public replies represent the brand`,
796
1115
  {
797
1116
  review_id: z3.string().describe("The review ID to reply to"),
798
1117
  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.")
1118
+ confirmed: z3.boolean().default(false).describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
800
1119
  },
801
1120
  {
802
1121
  title: "Reply to Review",
@@ -806,7 +1125,7 @@ This will post a public reply. Call this tool again with confirmed=true to send.
806
1125
  openWorldHint: true
807
1126
  },
808
1127
  async (args) => {
809
- if (!args.confirmed) {
1128
+ if (args.confirmed !== true) {
810
1129
  const preview = `## Review Reply Preview
811
1130
 
812
1131
  **Review ID:** ${args.review_id}
@@ -844,8 +1163,7 @@ function registerMediaTools(server2, client2) {
844
1163
  openWorldHint: false
845
1164
  },
846
1165
  async (args) => {
847
- const buffer = await readFile(args.file_path);
848
- const base64 = buffer.toString("base64");
1166
+ const buffer = Buffer.from(await readFile(args.file_path));
849
1167
  const filename = args.file_path.split("/").pop() ?? "upload";
850
1168
  const ext = filename.split(".").pop()?.toLowerCase() ?? "";
851
1169
  const mimeMap = {
@@ -861,33 +1179,57 @@ function registerMediaTools(server2, client2) {
861
1179
  pdf: "application/pdf"
862
1180
  };
863
1181
  const mimeType = mimeMap[ext] ?? "application/octet-stream";
864
- const result = await client2.uploadMedia(filename, base64, mimeType);
1182
+ const result = await client2.uploadMediaMultipart(filename, buffer, mimeType);
865
1183
  return {
866
1184
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
867
1185
  };
868
1186
  }
869
1187
  );
870
1188
  server2.tool(
871
- "upload_media_base64",
872
- "Upload media from base64-encoded data. Returns a CDN URL that can be used in posts.",
1189
+ "upload_from_url",
1190
+ "Upload media from a public URL. The server fetches the image/video and uploads it to storage. Returns a CDN URL that can be used in posts. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM up to 25MB.",
873
1191
  {
874
- filename: z4.string().describe("The filename including extension"),
875
- data: z4.string().describe("Base64-encoded file data"),
876
- mime_type: z4.string().describe("MIME type of the file, e.g. image/jpeg, video/mp4")
1192
+ url: z4.string().url().describe("Public URL of the image or video to upload"),
1193
+ filename: z4.string().optional().describe("Optional filename override (including extension)"),
1194
+ folder: z4.string().optional().describe("Optional folder path within the customer's storage")
877
1195
  },
878
1196
  {
879
- title: "Upload Media Base64",
1197
+ title: "Upload Media from URL",
880
1198
  readOnlyHint: false,
881
1199
  destructiveHint: false,
882
1200
  idempotentHint: false,
883
1201
  openWorldHint: false
884
1202
  },
885
1203
  async (args) => {
886
- const result = await client2.uploadMedia(
887
- args.filename,
888
- args.data,
889
- args.mime_type
890
- );
1204
+ const result = await client2.uploadFromUrl({
1205
+ url: args.url,
1206
+ filename: args.filename,
1207
+ folder: args.folder
1208
+ });
1209
+ return {
1210
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1211
+ };
1212
+ }
1213
+ );
1214
+ server2.tool(
1215
+ "get_upload_url",
1216
+ "Get a pre-signed URL for direct client upload to storage. The URL is valid for 5 minutes. After uploading to the URL, the file will be available at the returned CDN URL.",
1217
+ {
1218
+ filename: z4.string().describe("The filename including extension (e.g. photo.jpg)"),
1219
+ content_type: z4.string().describe("MIME type of the file (e.g. image/jpeg, video/mp4)")
1220
+ },
1221
+ {
1222
+ title: "Get Upload URL",
1223
+ readOnlyHint: true,
1224
+ destructiveHint: false,
1225
+ idempotentHint: false,
1226
+ openWorldHint: false
1227
+ },
1228
+ async (args) => {
1229
+ const result = await client2.getUploadUrl({
1230
+ filename: args.filename,
1231
+ content_type: args.content_type
1232
+ });
891
1233
  return {
892
1234
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
893
1235
  };
@@ -913,10 +1255,15 @@ function registerMediaTools(server2, client2) {
913
1255
  );
914
1256
  server2.tool(
915
1257
  "delete_media",
916
- "Delete a media file from your BuzzPoster media library. This cannot be undone.",
1258
+ `Delete a media file from the customer's BuzzPoster media library. This cannot be undone.
1259
+
1260
+ REQUIRED WORKFLOW:
1261
+ 1. ALWAYS set confirmed=false first
1262
+ 2. Show the user which file will be deleted (filename, URL, size)
1263
+ 3. Only confirm after explicit approval`,
917
1264
  {
918
1265
  key: z4.string().describe("The key/path of the media file to delete"),
919
- confirmed: z4.boolean().optional().describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
1266
+ confirmed: z4.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
920
1267
  },
921
1268
  {
922
1269
  title: "Delete Media File",
@@ -926,7 +1273,7 @@ function registerMediaTools(server2, client2) {
926
1273
  openWorldHint: false
927
1274
  },
928
1275
  async (args) => {
929
- if (!args.confirmed) {
1276
+ if (args.confirmed !== true) {
930
1277
  const preview = `## Delete Media Confirmation
931
1278
 
932
1279
  **File:** ${args.key}
@@ -944,7 +1291,7 @@ This will permanently delete this media file. Call this tool again with confirme
944
1291
 
945
1292
  // src/tools/newsletter.ts
946
1293
  import { z as z5 } from "zod";
947
- function registerNewsletterTools(server2, client2) {
1294
+ function registerNewsletterTools(server2, client2, options = {}) {
948
1295
  server2.tool(
949
1296
  "list_subscribers",
950
1297
  "List email subscribers from your configured email service provider (Kit, Beehiiv, or Mailchimp).",
@@ -997,7 +1344,7 @@ function registerNewsletterTools(server2, client2) {
997
1344
  );
998
1345
  server2.tool(
999
1346
  "create_newsletter",
1000
- "Create a new newsletter/broadcast draft.",
1347
+ `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.`,
1001
1348
  {
1002
1349
  subject: z5.string().describe("Email subject line"),
1003
1350
  content: z5.string().describe("HTML content of the newsletter"),
@@ -1023,7 +1370,7 @@ function registerNewsletterTools(server2, client2) {
1023
1370
  );
1024
1371
  server2.tool(
1025
1372
  "update_newsletter",
1026
- "Update an existing newsletter/broadcast draft.",
1373
+ `Update an existing newsletter/broadcast draft. After updating, show the user a visual preview of the changes.`,
1027
1374
  {
1028
1375
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to update"),
1029
1376
  subject: z5.string().optional().describe("Updated subject line"),
@@ -1048,36 +1395,172 @@ function registerNewsletterTools(server2, client2) {
1048
1395
  };
1049
1396
  }
1050
1397
  );
1398
+ if (options.allowDirectSend) {
1399
+ server2.tool(
1400
+ "send_newsletter",
1401
+ `Send a newsletter/broadcast to subscribers. THIS ACTION CANNOT BE UNDONE. Once sent, the email goes to every subscriber on the list.
1402
+
1403
+ REQUIRED WORKFLOW:
1404
+ 1. ALWAYS call get_publishing_rules first
1405
+ 2. NEVER send without showing a full visual preview of the newsletter first
1406
+ 3. ALWAYS set confirmed=false first -- this returns a confirmation prompt, not a send
1407
+ 4. Tell the user exactly how many subscribers will receive this email
1408
+ 5. If publishing_rules.require_double_confirm_newsletter is true (default), require TWO explicit confirmations:
1409
+ - First: "Are you sure you want to send this to [X] subscribers?"
1410
+ - Second: "This is irreversible. Type 'send' to confirm."
1411
+ 6. If publishing_rules.allow_immediate_send is false and the user asks to send immediately, suggest scheduling instead
1412
+ 7. NEVER set confirmed=true without the user explicitly confirming after seeing the preview and subscriber count
1413
+
1414
+ 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.`,
1415
+ {
1416
+ broadcast_id: z5.string().describe("The broadcast/newsletter ID to send"),
1417
+ confirmed: z5.boolean().default(false).describe(
1418
+ "Set to true to confirm and send. If false or missing, returns a confirmation prompt."
1419
+ )
1420
+ },
1421
+ {
1422
+ title: "Send Newsletter",
1423
+ readOnlyHint: false,
1424
+ destructiveHint: false,
1425
+ idempotentHint: false,
1426
+ openWorldHint: true
1427
+ },
1428
+ async (args) => {
1429
+ if (args.confirmed !== true) {
1430
+ let subscriberCount = "unknown";
1431
+ let rulesText = "";
1432
+ try {
1433
+ const subData = await client2.listSubscribers({ perPage: "1" });
1434
+ subscriberCount = String(subData?.totalCount ?? subData?.total ?? subData?.subscribers?.length ?? "unknown");
1435
+ } catch {
1436
+ }
1437
+ try {
1438
+ const rules = await client2.getPublishingRules();
1439
+ rulesText = `
1440
+ ### Safety Checks
1441
+ `;
1442
+ rulesText += `- Double confirmation required: ${rules.requireDoubleConfirmNewsletter ? "**yes**" : "no"}
1443
+ `;
1444
+ rulesText += `- Immediate send allowed: ${rules.allowImmediateSend ? "yes" : "**no**"}
1445
+ `;
1446
+ if (rules.requiredDisclaimer) {
1447
+ rulesText += `- Required disclaimer: "${rules.requiredDisclaimer}"
1448
+ `;
1449
+ }
1450
+ } catch {
1451
+ }
1452
+ let espText = "";
1453
+ try {
1454
+ const account = await client2.getAccount();
1455
+ espText = account?.espProvider ? `**ESP:** ${account.espProvider}
1456
+ ` : "";
1457
+ } catch {
1458
+ }
1459
+ const preview = `## Send Newsletter Confirmation
1460
+
1461
+ **Broadcast ID:** ${args.broadcast_id}
1462
+ **Subscribers:** ~${subscriberCount} recipients
1463
+ ` + espText + rulesText + `
1464
+ **This action cannot be undone.** Once sent, the email goes to every subscriber.
1465
+
1466
+ Call this tool again with confirmed=true to send.`;
1467
+ return { content: [{ type: "text", text: preview }] };
1468
+ }
1469
+ await client2.sendBroadcast(args.broadcast_id);
1470
+ return {
1471
+ content: [{ type: "text", text: "Newsletter sent successfully." }]
1472
+ };
1473
+ }
1474
+ );
1475
+ }
1051
1476
  server2.tool(
1052
- "send_newsletter",
1053
- "Send a newsletter/broadcast to subscribers. This action cannot be undone. Requires confirmation before sending.",
1477
+ "schedule_newsletter",
1478
+ `Schedule a newsletter/broadcast to be sent at a future time.
1479
+
1480
+ REQUIRED WORKFLOW:
1481
+ 1. ALWAYS call get_publishing_rules first
1482
+ 2. NEVER schedule without showing a full visual preview of the newsletter first
1483
+ 3. ALWAYS set confirmed=false first -- this returns a confirmation prompt with details, not an actual schedule
1484
+ 4. Tell the user exactly how many subscribers will receive this email and the scheduled send time
1485
+ 5. If publishing_rules.require_double_confirm_newsletter is true (default), require TWO explicit confirmations:
1486
+ - First: "Are you sure you want to schedule this for [time] to [X] subscribers?"
1487
+ - Second: "Confirm schedule for [time]."
1488
+ 6. If publishing_rules.allow_immediate_send is false and the scheduled time is very soon, warn the user
1489
+ 7. NEVER set confirmed=true without the user explicitly confirming after seeing the preview, subscriber count, and scheduled time
1490
+
1491
+ Scheduling can typically be cancelled or rescheduled later, but the user should still verify the time is correct before confirming.
1492
+
1493
+ 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.`,
1054
1494
  {
1055
- broadcast_id: z5.string().describe("The broadcast/newsletter ID to send"),
1056
- confirmed: z5.boolean().optional().describe(
1057
- "Set to true to confirm and send. If false or missing, returns a confirmation prompt."
1495
+ broadcast_id: z5.string().describe("The broadcast/newsletter ID to schedule"),
1496
+ scheduled_for: z5.string().describe(
1497
+ "ISO 8601 datetime for when to send (e.g. '2026-02-25T14:00:00Z')"
1498
+ ),
1499
+ confirmed: z5.boolean().default(false).describe(
1500
+ "Set to true to confirm and schedule. If false or missing, returns a confirmation prompt."
1058
1501
  )
1059
1502
  },
1060
1503
  {
1061
- title: "Send Newsletter",
1504
+ title: "Schedule Newsletter",
1062
1505
  readOnlyHint: false,
1063
1506
  destructiveHint: false,
1064
1507
  idempotentHint: false,
1065
1508
  openWorldHint: true
1066
1509
  },
1067
1510
  async (args) => {
1068
- if (!args.confirmed) {
1069
- const preview = `## Send Newsletter Confirmation
1511
+ if (args.confirmed !== true) {
1512
+ let subscriberCount = "unknown";
1513
+ let rulesText = "";
1514
+ try {
1515
+ const subData = await client2.listSubscribers({ perPage: "1" });
1516
+ subscriberCount = String(
1517
+ subData?.totalCount ?? subData?.total ?? subData?.subscribers?.length ?? "unknown"
1518
+ );
1519
+ } catch {
1520
+ }
1521
+ try {
1522
+ const rules = await client2.getPublishingRules();
1523
+ rulesText = `
1524
+ ### Safety Checks
1525
+ `;
1526
+ rulesText += `- Double confirmation required: ${rules.requireDoubleConfirmNewsletter ? "**yes**" : "no"}
1527
+ `;
1528
+ rulesText += `- Immediate send allowed: ${rules.allowImmediateSend ? "yes" : "**no**"}
1529
+ `;
1530
+ if (rules.requiredDisclaimer) {
1531
+ rulesText += `- Required disclaimer: "${rules.requiredDisclaimer}"
1532
+ `;
1533
+ }
1534
+ } catch {
1535
+ }
1536
+ let espText = "";
1537
+ try {
1538
+ const account = await client2.getAccount();
1539
+ espText = account?.espProvider ? `**ESP:** ${account.espProvider}
1540
+ ` : "";
1541
+ } catch {
1542
+ }
1543
+ const preview = `## Schedule Newsletter Confirmation
1070
1544
 
1071
1545
  **Broadcast ID:** ${args.broadcast_id}
1546
+ **Scheduled for:** ${args.scheduled_for}
1547
+ **Subscribers:** ~${subscriberCount} recipients
1548
+ ` + espText + rulesText + `
1549
+ Scheduling can typically be cancelled or rescheduled later, but please verify the time is correct.
1072
1550
 
1073
- This will send the newsletter to your subscriber list. **This action cannot be undone.**
1074
-
1075
- Call this tool again with confirmed=true to send.`;
1551
+ Call this tool again with confirmed=true to schedule.`;
1076
1552
  return { content: [{ type: "text", text: preview }] };
1077
1553
  }
1078
- await client2.sendBroadcast(args.broadcast_id);
1554
+ await client2.scheduleBroadcast(args.broadcast_id, {
1555
+ scheduledFor: args.scheduled_for
1556
+ });
1079
1557
  return {
1080
- content: [{ type: "text", text: "Newsletter sent successfully." }]
1558
+ content: [
1559
+ {
1560
+ type: "text",
1561
+ text: `Newsletter scheduled successfully for ${args.scheduled_for}.`
1562
+ }
1563
+ ]
1081
1564
  };
1082
1565
  }
1083
1566
  );
@@ -1123,7 +1606,7 @@ Call this tool again with confirmed=true to send.`;
1123
1606
  );
1124
1607
  server2.tool(
1125
1608
  "list_sequences",
1126
- "List all email sequences/automations from your email service provider.",
1609
+ "List all email sequences/automations from your email service provider. On Beehiiv, this returns automations. For detailed automation data use list_automations instead.",
1127
1610
  {},
1128
1611
  {
1129
1612
  title: "List Email Sequences",
@@ -1164,7 +1647,7 @@ import { z as z6 } from "zod";
1164
1647
  function registerRssTools(server2, client2) {
1165
1648
  server2.tool(
1166
1649
  "fetch_feed",
1167
- "Fetch and parse entries from an RSS or Atom feed URL. Returns titles, links, descriptions, and dates.",
1650
+ "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.",
1168
1651
  {
1169
1652
  url: z6.string().describe("The RSS/Atom feed URL to fetch"),
1170
1653
  limit: z6.number().optional().describe("Maximum number of entries to return (default 10, max 100)")
@@ -1185,7 +1668,7 @@ function registerRssTools(server2, client2) {
1185
1668
  );
1186
1669
  server2.tool(
1187
1670
  "fetch_article",
1188
- "Extract the full article content from a URL as clean text/markdown. Useful for reading and summarizing articles.",
1671
+ "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.",
1189
1672
  {
1190
1673
  url: z6.string().describe("The article URL to extract content from")
1191
1674
  },
@@ -1228,10 +1711,11 @@ function registerAccountInfoTool(server2, client2) {
1228
1711
  }
1229
1712
 
1230
1713
  // src/tools/brand-voice.ts
1714
+ import { z as z7 } from "zod";
1231
1715
  function registerBrandVoiceTools(server2, client2) {
1232
1716
  server2.tool(
1233
1717
  "get_brand_voice",
1234
- "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.",
1718
+ "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.",
1235
1719
  {},
1236
1720
  {
1237
1721
  title: "Get Brand Voice",
@@ -1246,39 +1730,45 @@ function registerBrandVoiceTools(server2, client2) {
1246
1730
  const lines = [];
1247
1731
  lines.push(`## Brand Voice: ${voice.name || "My Brand"}`);
1248
1732
  lines.push("");
1249
- if (voice.description) {
1250
- lines.push("### Voice Description");
1251
- lines.push(voice.description);
1252
- lines.push("");
1253
- }
1733
+ lines.push("### Voice Description");
1734
+ lines.push(voice.description || "(not set)");
1735
+ lines.push("");
1736
+ lines.push("### Do's");
1254
1737
  if (voice.dos && voice.dos.length > 0) {
1255
- lines.push("### Do's");
1256
1738
  for (const rule of voice.dos) {
1257
1739
  lines.push(`- ${rule}`);
1258
1740
  }
1259
- lines.push("");
1741
+ } else {
1742
+ lines.push("(none)");
1260
1743
  }
1744
+ lines.push("");
1745
+ lines.push("### Don'ts");
1261
1746
  if (voice.donts && voice.donts.length > 0) {
1262
- lines.push("### Don'ts");
1263
1747
  for (const rule of voice.donts) {
1264
1748
  lines.push(`- ${rule}`);
1265
1749
  }
1266
- lines.push("");
1750
+ } else {
1751
+ lines.push("(none)");
1267
1752
  }
1753
+ lines.push("");
1754
+ lines.push("### Platform-Specific Rules");
1268
1755
  if (voice.platformRules && Object.keys(voice.platformRules).length > 0) {
1269
- lines.push("### Platform-Specific Rules");
1270
1756
  for (const [platform, rules] of Object.entries(voice.platformRules)) {
1271
1757
  lines.push(`**${platform}:** ${rules}`);
1272
1758
  }
1273
- lines.push("");
1759
+ } else {
1760
+ lines.push("(none)");
1274
1761
  }
1762
+ lines.push("");
1763
+ lines.push("### Example Posts");
1275
1764
  if (voice.examplePosts && voice.examplePosts.length > 0) {
1276
- lines.push("### Example Posts");
1277
1765
  voice.examplePosts.forEach((post, i) => {
1278
1766
  lines.push(`${i + 1}. ${post}`);
1279
1767
  });
1280
- lines.push("");
1768
+ } else {
1769
+ lines.push("(none)");
1281
1770
  }
1771
+ lines.push("");
1282
1772
  return {
1283
1773
  content: [{ type: "text", text: lines.join("\n") }]
1284
1774
  };
@@ -1298,10 +1788,125 @@ function registerBrandVoiceTools(server2, client2) {
1298
1788
  }
1299
1789
  }
1300
1790
  );
1791
+ server2.tool(
1792
+ "update_brand_voice",
1793
+ `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.
1794
+
1795
+ USAGE GUIDELINES:
1796
+ - Call get_brand_voice first to see the current state before making changes
1797
+ - Always set confirmed=false first to preview the changes, then confirmed=true after user approval
1798
+ - When adding rules to dos/donts, include the FULL array (existing + new items), not just the new ones
1799
+ - Platform rules use platform names as keys (e.g. twitter, linkedin, instagram)
1800
+ - Max limits: name 80 chars, description 16000 chars, dos/donts 50 items each, examplePosts 8 items`,
1801
+ {
1802
+ name: z7.string().max(80).optional().describe("Brand voice profile name (e.g. 'BOQtoday Voice', 'Lightbreak Editorial')"),
1803
+ description: z7.string().max(16e3).optional().describe("Overall voice description \u2014 tone, personality, style overview"),
1804
+ dos: z7.array(z7.string()).max(50).optional().describe("Writing rules to follow (array of do's). Send the FULL list, not just additions."),
1805
+ donts: z7.array(z7.string()).max(50).optional().describe("Things to avoid (array of don'ts). Send the FULL list, not just additions."),
1806
+ 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" }'),
1807
+ example_posts: z7.array(z7.string()).max(8).optional().describe("Example posts that demonstrate the desired voice (max 8)"),
1808
+ confirmed: z7.boolean().default(false).describe(
1809
+ "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
1810
+ )
1811
+ },
1812
+ {
1813
+ title: "Update Brand Voice",
1814
+ readOnlyHint: false,
1815
+ destructiveHint: false,
1816
+ idempotentHint: false,
1817
+ openWorldHint: false
1818
+ },
1819
+ async (args) => {
1820
+ const payload = {};
1821
+ if (args.name !== void 0) payload.name = args.name;
1822
+ if (args.description !== void 0) payload.description = args.description;
1823
+ if (args.dos !== void 0) payload.dos = args.dos;
1824
+ if (args.donts !== void 0) payload.donts = args.donts;
1825
+ if (args.platform_rules !== void 0) payload.platformRules = args.platform_rules;
1826
+ if (args.example_posts !== void 0) payload.examplePosts = args.example_posts;
1827
+ if (Object.keys(payload).length === 0) {
1828
+ return {
1829
+ content: [
1830
+ {
1831
+ type: "text",
1832
+ text: "No fields provided. Specify at least one field to update (name, description, dos, donts, platform_rules, example_posts)."
1833
+ }
1834
+ ],
1835
+ isError: true
1836
+ };
1837
+ }
1838
+ if (args.confirmed !== true) {
1839
+ const lines2 = [];
1840
+ lines2.push("## Brand Voice Update Preview");
1841
+ lines2.push("");
1842
+ if (payload.name) {
1843
+ lines2.push(`**Name:** ${payload.name}`);
1844
+ lines2.push("");
1845
+ }
1846
+ if (payload.description) {
1847
+ lines2.push("### Voice Description");
1848
+ lines2.push(String(payload.description));
1849
+ lines2.push("");
1850
+ }
1851
+ if (payload.dos) {
1852
+ const dos = payload.dos;
1853
+ lines2.push(`### Do's (${dos.length} rules)`);
1854
+ for (const rule of dos) {
1855
+ lines2.push(`- ${rule}`);
1856
+ }
1857
+ lines2.push("");
1858
+ }
1859
+ if (payload.donts) {
1860
+ const donts = payload.donts;
1861
+ lines2.push(`### Don'ts (${donts.length} rules)`);
1862
+ for (const rule of donts) {
1863
+ lines2.push(`- ${rule}`);
1864
+ }
1865
+ lines2.push("");
1866
+ }
1867
+ if (payload.platformRules) {
1868
+ const rules = payload.platformRules;
1869
+ lines2.push(`### Platform-Specific Rules (${Object.keys(rules).length} platforms)`);
1870
+ for (const [platform, rule] of Object.entries(rules)) {
1871
+ lines2.push(`**${platform}:** ${rule}`);
1872
+ }
1873
+ lines2.push("");
1874
+ }
1875
+ if (payload.examplePosts) {
1876
+ const posts = payload.examplePosts;
1877
+ lines2.push(`### Example Posts (${posts.length})`);
1878
+ posts.forEach((post, i) => {
1879
+ lines2.push(`${i + 1}. ${post}`);
1880
+ });
1881
+ lines2.push("");
1882
+ }
1883
+ lines2.push("---");
1884
+ lines2.push("Call this tool again with **confirmed=true** to save these changes.");
1885
+ return {
1886
+ content: [{ type: "text", text: lines2.join("\n") }]
1887
+ };
1888
+ }
1889
+ const result = await client2.updateBrandVoice(payload);
1890
+ const lines = [];
1891
+ lines.push(`Brand voice "${result.name || "My Brand"}" has been saved successfully.`);
1892
+ lines.push("");
1893
+ const summary = [];
1894
+ if (payload.name) summary.push("name");
1895
+ if (payload.description) summary.push("description");
1896
+ if (payload.dos) summary.push(`${payload.dos.length} do's`);
1897
+ if (payload.donts) summary.push(`${payload.donts.length} don'ts`);
1898
+ if (payload.platformRules) summary.push(`${Object.keys(payload.platformRules).length} platform rules`);
1899
+ if (payload.examplePosts) summary.push(`${payload.examplePosts.length} example posts`);
1900
+ lines.push(`**Updated:** ${summary.join(", ")}`);
1901
+ return {
1902
+ content: [{ type: "text", text: lines.join("\n") }]
1903
+ };
1904
+ }
1905
+ );
1301
1906
  }
1302
1907
 
1303
1908
  // src/tools/knowledge.ts
1304
- import { z as z7 } from "zod";
1909
+ import { z as z8 } from "zod";
1305
1910
  function registerKnowledgeTools(server2, client2) {
1306
1911
  server2.tool(
1307
1912
  "get_knowledge_base",
@@ -1361,7 +1966,7 @@ function registerKnowledgeTools(server2, client2) {
1361
1966
  "search_knowledge",
1362
1967
  "Search the customer's knowledge base by tag or keyword. Use this to find specific reference material when writing about a topic.",
1363
1968
  {
1364
- query: z7.string().describe(
1969
+ query: z8.string().describe(
1365
1970
  "Search query - matches against tags first, then falls back to text search on title and content"
1366
1971
  )
1367
1972
  },
@@ -1412,9 +2017,9 @@ function registerKnowledgeTools(server2, client2) {
1412
2017
  "add_knowledge",
1413
2018
  "Add a new item to the customer's knowledge base. Use this to save useful information the customer shares during conversation.",
1414
2019
  {
1415
- title: z7.string().describe("Title for the knowledge item"),
1416
- content: z7.string().describe("The content/text to save"),
1417
- tags: z7.array(z7.string()).optional().describe("Optional tags for categorization")
2020
+ title: z8.string().describe("Title for the knowledge item"),
2021
+ content: z8.string().describe("The content/text to save"),
2022
+ tags: z8.array(z8.string()).optional().describe("Optional tags for categorization")
1418
2023
  },
1419
2024
  {
1420
2025
  title: "Add Knowledge Item",
@@ -1448,13 +2053,13 @@ function registerKnowledgeTools(server2, client2) {
1448
2053
  }
1449
2054
 
1450
2055
  // src/tools/audience.ts
1451
- import { z as z8 } from "zod";
2056
+ import { z as z9 } from "zod";
1452
2057
  function registerAudienceTools(server2, client2) {
1453
2058
  server2.tool(
1454
2059
  "get_audience",
1455
2060
  "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.",
1456
2061
  {
1457
- audienceId: z8.string().optional().describe("Specific audience profile ID. If omitted, returns the default audience.")
2062
+ audienceId: z9.string().optional().describe("Specific audience profile ID. If omitted, returns the default audience.")
1458
2063
  },
1459
2064
  {
1460
2065
  title: "Get Audience Profile",
@@ -1527,48 +2132,212 @@ function registerAudienceTools(server2, client2) {
1527
2132
  }
1528
2133
  }
1529
2134
  );
1530
- }
1531
-
1532
- // src/tools/newsletter-template.ts
1533
- import { z as z9 } from "zod";
1534
- function registerNewsletterTemplateTools(server2, client2) {
1535
2135
  server2.tool(
1536
- "get_newsletter_template",
1537
- "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.",
2136
+ "update_audience",
2137
+ `Update an existing audience profile. Only the fields you provide will be changed; omitted fields are left as-is.
2138
+
2139
+ USAGE GUIDELINES:
2140
+ - Call get_audience first to see the current state before making changes
2141
+ - Always set confirmed=false first to preview the changes, then confirmed=true after user approval
2142
+ - When updating array fields (pain_points, motivations, preferred_platforms), include the FULL array (existing + new items), not just the new ones
2143
+ - Max limits: name 100 chars, description 500 chars`,
1538
2144
  {
1539
- templateId: z9.string().optional().describe(
1540
- "Specific template ID. If omitted, returns the default template."
2145
+ audience_id: z9.string().describe("The audience ID to update (required)"),
2146
+ name: z9.string().max(100).optional().describe("Audience profile name (e.g. 'SaaS Founders', 'Health-Conscious Millennials')"),
2147
+ description: z9.string().max(500).optional().describe("Brief description of who this audience is"),
2148
+ demographics: z9.string().optional().describe("Demographic details \u2014 age range, location, income level, education, etc."),
2149
+ pain_points: z9.array(z9.string()).optional().describe("Problems and frustrations the audience faces. Send the FULL list, not just additions."),
2150
+ motivations: z9.array(z9.string()).optional().describe("Goals, desires, and what drives the audience. Send the FULL list, not just additions."),
2151
+ 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."),
2152
+ tone_notes: z9.string().optional().describe("How to speak to this audience \u2014 formality level, jargon preferences, emotional tone"),
2153
+ content_preferences: z9.string().optional().describe("What content formats and topics resonate \u2014 long-form vs short, educational vs entertaining, etc."),
2154
+ is_default: z9.boolean().optional().describe("Set to true to make this the default audience profile"),
2155
+ confirmed: z9.boolean().default(false).describe(
2156
+ "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
1541
2157
  )
1542
2158
  },
1543
2159
  {
1544
- title: "Get Newsletter Template",
1545
- readOnlyHint: true,
2160
+ title: "Update Audience",
2161
+ readOnlyHint: false,
1546
2162
  destructiveHint: false,
1547
- idempotentHint: true,
2163
+ idempotentHint: false,
1548
2164
  openWorldHint: false
1549
2165
  },
1550
- async ({ templateId }) => {
1551
- try {
1552
- const template = await client2.getTemplate(templateId);
1553
- const lines = [];
1554
- lines.push(`## Newsletter Template: ${template.name}`);
1555
- if (template.description) lines.push(template.description);
1556
- lines.push("");
1557
- if (template.audienceId)
1558
- lines.push(`Audience ID: ${template.audienceId}`);
1559
- if (template.sendCadence)
1560
- lines.push(`Cadence: ${template.sendCadence}`);
1561
- if (template.subjectPattern)
1562
- lines.push(`Subject pattern: ${template.subjectPattern}`);
1563
- if (template.previewTextPattern)
1564
- lines.push(`Preview text pattern: ${template.previewTextPattern}`);
1565
- lines.push("");
1566
- if (template.sections && template.sections.length > 0) {
1567
- lines.push("### Sections (in order):");
1568
- lines.push("");
1569
- for (let i = 0; i < template.sections.length; i++) {
1570
- const s = template.sections[i];
1571
- lines.push(`**${i + 1}. ${s.label}** (${s.type})`);
2166
+ async (args) => {
2167
+ const payload = {};
2168
+ if (args.name !== void 0) payload.name = args.name;
2169
+ if (args.description !== void 0) payload.description = args.description;
2170
+ if (args.demographics !== void 0) payload.demographics = args.demographics;
2171
+ if (args.pain_points !== void 0) payload.painPoints = args.pain_points;
2172
+ if (args.motivations !== void 0) payload.motivations = args.motivations;
2173
+ if (args.preferred_platforms !== void 0) payload.platforms = args.preferred_platforms;
2174
+ if (args.tone_notes !== void 0) payload.toneNotes = args.tone_notes;
2175
+ if (args.content_preferences !== void 0) payload.contentPreferences = args.content_preferences;
2176
+ if (args.is_default !== void 0) payload.isDefault = args.is_default;
2177
+ if (Object.keys(payload).length === 0) {
2178
+ return {
2179
+ content: [
2180
+ {
2181
+ type: "text",
2182
+ 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)."
2183
+ }
2184
+ ],
2185
+ isError: true
2186
+ };
2187
+ }
2188
+ if (args.confirmed !== true) {
2189
+ const lines2 = [];
2190
+ lines2.push("## Audience Update Preview");
2191
+ lines2.push(`**Audience ID:** ${args.audience_id}`);
2192
+ lines2.push("");
2193
+ if (payload.name) {
2194
+ lines2.push(`**Name:** ${payload.name}`);
2195
+ lines2.push("");
2196
+ }
2197
+ if (payload.description) {
2198
+ lines2.push("### Description");
2199
+ lines2.push(String(payload.description));
2200
+ lines2.push("");
2201
+ }
2202
+ if (payload.demographics) {
2203
+ lines2.push("### Demographics");
2204
+ lines2.push(String(payload.demographics));
2205
+ lines2.push("");
2206
+ }
2207
+ if (payload.painPoints) {
2208
+ const points = payload.painPoints;
2209
+ lines2.push(`### Pain Points (${points.length})`);
2210
+ for (const point of points) {
2211
+ lines2.push(`- ${point}`);
2212
+ }
2213
+ lines2.push("");
2214
+ }
2215
+ if (payload.motivations) {
2216
+ const motivations = payload.motivations;
2217
+ lines2.push(`### Motivations (${motivations.length})`);
2218
+ for (const motivation of motivations) {
2219
+ lines2.push(`- ${motivation}`);
2220
+ }
2221
+ lines2.push("");
2222
+ }
2223
+ if (payload.platforms) {
2224
+ const platforms = payload.platforms;
2225
+ lines2.push(`### Preferred Platforms (${platforms.length})`);
2226
+ lines2.push(platforms.join(", "));
2227
+ lines2.push("");
2228
+ }
2229
+ if (payload.toneNotes) {
2230
+ lines2.push("### Tone Notes");
2231
+ lines2.push(String(payload.toneNotes));
2232
+ lines2.push("");
2233
+ }
2234
+ if (payload.contentPreferences) {
2235
+ lines2.push("### Content Preferences");
2236
+ lines2.push(String(payload.contentPreferences));
2237
+ lines2.push("");
2238
+ }
2239
+ if (payload.isDefault !== void 0) {
2240
+ lines2.push(`**Set as default:** ${payload.isDefault ? "Yes" : "No"}`);
2241
+ lines2.push("");
2242
+ }
2243
+ lines2.push("---");
2244
+ lines2.push("Call this tool again with **confirmed=true** to save these changes.");
2245
+ return {
2246
+ content: [{ type: "text", text: lines2.join("\n") }]
2247
+ };
2248
+ }
2249
+ const result = await client2.updateAudience(args.audience_id, payload);
2250
+ const lines = [];
2251
+ lines.push(`Audience "${result.name}" has been updated successfully.`);
2252
+ lines.push("");
2253
+ const summary = [];
2254
+ if (payload.name) summary.push("name");
2255
+ if (payload.description) summary.push("description");
2256
+ if (payload.demographics) summary.push("demographics");
2257
+ if (payload.painPoints) summary.push(`${payload.painPoints.length} pain points`);
2258
+ if (payload.motivations) summary.push(`${payload.motivations.length} motivations`);
2259
+ if (payload.platforms) summary.push(`${payload.platforms.length} platforms`);
2260
+ if (payload.toneNotes) summary.push("tone notes");
2261
+ if (payload.contentPreferences) summary.push("content preferences");
2262
+ if (payload.isDefault !== void 0) summary.push("default status");
2263
+ lines.push(`**Updated:** ${summary.join(", ")}`);
2264
+ return {
2265
+ content: [{ type: "text", text: lines.join("\n") }]
2266
+ };
2267
+ }
2268
+ );
2269
+ }
2270
+
2271
+ // src/tools/newsletter-template.ts
2272
+ import { z as z10 } from "zod";
2273
+ var NEWSLETTER_CRAFT_GUIDE = `## Newsletter Craft Guide
2274
+
2275
+ Follow these rules when drafting:
2276
+
2277
+ ### HTML Email Rules
2278
+ - Use table-based layouts, inline CSS only, 600px max width
2279
+ - Email-safe fonts only: Arial, Helvetica, Georgia, Verdana
2280
+ - Keep total email under 102KB (Gmail clips larger)
2281
+ - All images must use absolute URLs hosted on the customer's R2 CDN
2282
+ - Include alt text on every image
2283
+ - Use role="presentation" on layout tables
2284
+ - No JavaScript, no forms, no CSS grid/flexbox, no background images
2285
+
2286
+ ### Structure
2287
+ - Subject line: 6-10 words, specific not clickbaity
2288
+ - Preview text: complements the subject, doesn't repeat it
2289
+ - Open strong -- lead with the most interesting thing
2290
+ - One clear CTA per newsletter
2291
+ - Sign off should feel human, not corporate
2292
+
2293
+ ### Personalization
2294
+ - Kit: use {{ subscriber.first_name }}
2295
+ - Beehiiv: use {{email}} or {{subscriber_id}}
2296
+ - Mailchimp: use *NAME|*
2297
+
2298
+ ### Images
2299
+ - Upload to customer's R2 CDN via upload_media or upload_from_url
2300
+ - Reference with full CDN URLs in img tags
2301
+ - Always set width, height, and alt attributes
2302
+ - Use style="display:block;" to prevent phantom spacing`;
2303
+ function registerNewsletterTemplateTools(server2, client2) {
2304
+ server2.tool(
2305
+ "get_newsletter_template",
2306
+ "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.",
2307
+ {
2308
+ templateId: z10.string().optional().describe(
2309
+ "Specific template ID. If omitted, returns the default template."
2310
+ )
2311
+ },
2312
+ {
2313
+ title: "Get Newsletter Template",
2314
+ readOnlyHint: true,
2315
+ destructiveHint: false,
2316
+ idempotentHint: true,
2317
+ openWorldHint: false
2318
+ },
2319
+ async ({ templateId }) => {
2320
+ try {
2321
+ const template = await client2.getTemplate(templateId);
2322
+ const lines = [];
2323
+ lines.push(`## Newsletter Template: ${template.name}`);
2324
+ if (template.description) lines.push(template.description);
2325
+ lines.push("");
2326
+ if (template.audienceId)
2327
+ lines.push(`Audience ID: ${template.audienceId}`);
2328
+ if (template.sendCadence)
2329
+ lines.push(`Cadence: ${template.sendCadence}`);
2330
+ if (template.subjectPattern)
2331
+ lines.push(`Subject pattern: ${template.subjectPattern}`);
2332
+ if (template.previewTextPattern)
2333
+ lines.push(`Preview text pattern: ${template.previewTextPattern}`);
2334
+ lines.push("");
2335
+ if (template.sections && template.sections.length > 0) {
2336
+ lines.push("### Sections (in order):");
2337
+ lines.push("");
2338
+ for (let i = 0; i < template.sections.length; i++) {
2339
+ const s = template.sections[i];
2340
+ lines.push(`**${i + 1}. ${s.label}** (${s.type})`);
1572
2341
  if (s.instructions)
1573
2342
  lines.push(`Instructions: ${s.instructions}`);
1574
2343
  if (s.word_count)
@@ -1649,6 +2418,7 @@ function registerNewsletterTemplateTools(server2, client2) {
1649
2418
  );
1650
2419
  lines.push("");
1651
2420
  }
2421
+ lines.push(NEWSLETTER_CRAFT_GUIDE);
1652
2422
  return {
1653
2423
  content: [{ type: "text", text: lines.join("\n") }]
1654
2424
  };
@@ -1659,7 +2429,7 @@ function registerNewsletterTemplateTools(server2, client2) {
1659
2429
  content: [
1660
2430
  {
1661
2431
  type: "text",
1662
- text: "No newsletter template has been configured yet. The customer can set one up at their BuzzPoster dashboard under Templates."
2432
+ 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
1663
2433
  }
1664
2434
  ]
1665
2435
  };
@@ -1672,8 +2442,8 @@ function registerNewsletterTemplateTools(server2, client2) {
1672
2442
  "get_past_newsletters",
1673
2443
  "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.",
1674
2444
  {
1675
- limit: z9.number().optional().describe("Number of past newsletters to retrieve. Default 5, max 50."),
1676
- templateId: z9.string().optional().describe("Filter by template ID to see past newsletters from a specific template.")
2445
+ limit: z10.number().optional().describe("Number of past newsletters to retrieve. Default 5, max 50."),
2446
+ templateId: z10.string().optional().describe("Filter by template ID to see past newsletters from a specific template.")
1677
2447
  },
1678
2448
  {
1679
2449
  title: "Get Past Newsletters",
@@ -1737,7 +2507,7 @@ function registerNewsletterTemplateTools(server2, client2) {
1737
2507
  "get_past_newsletter",
1738
2508
  "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.",
1739
2509
  {
1740
- newsletterId: z9.string().describe("The ID of the archived newsletter to retrieve.")
2510
+ newsletterId: z10.string().describe("The ID of the archived newsletter to retrieve.")
1741
2511
  },
1742
2512
  {
1743
2513
  title: "Get Past Newsletter Detail",
@@ -1781,10 +2551,10 @@ function registerNewsletterTemplateTools(server2, client2) {
1781
2551
  "save_newsletter",
1782
2552
  "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.",
1783
2553
  {
1784
- subject: z9.string().describe("The newsletter subject line."),
1785
- contentHtml: z9.string().describe("The full HTML content of the newsletter."),
1786
- templateId: z9.string().optional().describe("The template ID used to generate this newsletter."),
1787
- notes: z9.string().optional().describe(
2554
+ subject: z10.string().describe("The newsletter subject line."),
2555
+ contentHtml: z10.string().describe("The full HTML content of the newsletter."),
2556
+ templateId: z10.string().optional().describe("The template ID used to generate this newsletter."),
2557
+ notes: z10.string().optional().describe(
1788
2558
  "Optional notes about this newsletter (what worked, theme, etc)."
1789
2559
  )
1790
2560
  },
@@ -1821,16 +2591,16 @@ function registerNewsletterTemplateTools(server2, client2) {
1821
2591
  }
1822
2592
 
1823
2593
  // src/tools/calendar.ts
1824
- import { z as z10 } from "zod";
2594
+ import { z as z11 } from "zod";
1825
2595
  function registerCalendarTools(server2, client2) {
1826
2596
  server2.tool(
1827
2597
  "get_calendar",
1828
2598
  "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.",
1829
2599
  {
1830
- from: z10.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
1831
- to: z10.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
1832
- status: z10.string().optional().describe("Filter by status: scheduled, published, draft, failed"),
1833
- type: z10.string().optional().describe("Filter by type: social_post or newsletter")
2600
+ from: z11.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
2601
+ to: z11.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
2602
+ status: z11.string().optional().describe("Filter by status: scheduled, published, draft, failed"),
2603
+ type: z11.string().optional().describe("Filter by type: social_post or newsletter")
1834
2604
  },
1835
2605
  {
1836
2606
  title: "Get Content Calendar",
@@ -1963,12 +2733,22 @@ Next slot: ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
1963
2733
  );
1964
2734
  server2.tool(
1965
2735
  "schedule_to_queue",
1966
- "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.",
2736
+ `Add a post to the customer's content queue for automatic scheduling.
2737
+
2738
+ 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.
2739
+
2740
+ REQUIRED WORKFLOW:
2741
+ 1. BEFORE creating content, call get_brand_voice and get_audience
2742
+ 2. ALWAYS set confirmed=false first to preview
2743
+ 3. Show the user a visual preview before confirming
2744
+ 4. Tell the user which queue slot the post will be assigned to
2745
+
2746
+ 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.`,
1967
2747
  {
1968
- content: z10.string().describe("The text content of the post"),
1969
- platforms: z10.array(z10.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
1970
- media_urls: z10.array(z10.string()).optional().describe("Public URLs of media to attach"),
1971
- confirmed: z10.boolean().optional().describe(
2748
+ content: z11.string().describe("The text content of the post"),
2749
+ platforms: z11.array(z11.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
2750
+ media_urls: z11.array(z11.string()).optional().describe("Public URLs of media to attach"),
2751
+ confirmed: z11.boolean().default(false).describe(
1972
2752
  "Set to true to confirm scheduling. If false or missing, returns a preview for user approval."
1973
2753
  )
1974
2754
  },
@@ -1991,15 +2771,33 @@ Next slot: ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
1991
2771
  ]
1992
2772
  };
1993
2773
  }
1994
- if (!args.confirmed) {
2774
+ if (args.confirmed !== true) {
1995
2775
  const slotDate2 = new Date(nextSlot.scheduledFor);
2776
+ let rulesText = "";
2777
+ try {
2778
+ const rules = await client2.getPublishingRules();
2779
+ const blockedWords = rules.blockedWords ?? [];
2780
+ const foundBlocked = [];
2781
+ if (args.content && blockedWords.length > 0) {
2782
+ const lower = args.content.toLowerCase();
2783
+ for (const word of blockedWords) {
2784
+ if (lower.includes(word.toLowerCase())) foundBlocked.push(word);
2785
+ }
2786
+ }
2787
+ rulesText = `
2788
+ ### Safety Checks
2789
+ `;
2790
+ rulesText += `- Blocked words: ${foundBlocked.length > 0 ? `**FAILED** (found: ${foundBlocked.join(", ")})` : "PASS"}
2791
+ `;
2792
+ } catch {
2793
+ }
1996
2794
  const preview = `## Queue Post Preview
1997
2795
 
1998
2796
  **Content:** "${args.content}"
1999
2797
  **Platforms:** ${args.platforms.join(", ")}
2000
2798
  **Next queue slot:** ${slotDate2.toLocaleString()}
2001
2799
  ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
2002
- ` : "") + `
2800
+ ` : "") + rulesText + `
2003
2801
  Call this tool again with confirmed=true to schedule.`;
2004
2802
  return { content: [{ type: "text", text: preview }] };
2005
2803
  }
@@ -2032,7 +2830,7 @@ ${JSON.stringify(result, null, 2)}`
2032
2830
  }
2033
2831
 
2034
2832
  // src/tools/notifications.ts
2035
- import { z as z11 } from "zod";
2833
+ import { z as z12 } from "zod";
2036
2834
  function timeAgo(dateStr) {
2037
2835
  const diff = Date.now() - new Date(dateStr).getTime();
2038
2836
  const minutes = Math.floor(diff / 6e4);
@@ -2048,8 +2846,8 @@ function registerNotificationTools(server2, client2) {
2048
2846
  "get_notifications",
2049
2847
  "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.",
2050
2848
  {
2051
- unread_only: z11.boolean().optional().describe("If true, only show unread notifications. Default false."),
2052
- limit: z11.number().optional().describe("Max notifications to return. Default 10.")
2849
+ unread_only: z12.boolean().optional().describe("If true, only show unread notifications. Default false."),
2850
+ limit: z12.number().optional().describe("Max notifications to return. Default 10.")
2053
2851
  },
2054
2852
  {
2055
2853
  title: "Get Notifications",
@@ -2115,29 +2913,1323 @@ Showing ${notifications.length} of ${result.unread_count !== void 0 ? "total" :
2115
2913
  );
2116
2914
  }
2117
2915
 
2118
- // src/index.ts
2119
- var apiKey = process.env.BUZZPOSTER_API_KEY;
2120
- var apiUrl = process.env.BUZZPOSTER_API_URL ?? "https://api.buzzposter.com";
2121
- if (!apiKey) {
2122
- console.error(
2123
- "Error: BUZZPOSTER_API_KEY environment variable is required."
2916
+ // src/tools/publishing-rules.ts
2917
+ function registerPublishingRulesTools(server2, client2) {
2918
+ server2.tool(
2919
+ "get_publishing_rules",
2920
+ `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.`,
2921
+ {},
2922
+ {
2923
+ title: "Get Publishing Rules",
2924
+ readOnlyHint: true,
2925
+ destructiveHint: false,
2926
+ idempotentHint: true,
2927
+ openWorldHint: false
2928
+ },
2929
+ async () => {
2930
+ const rules = await client2.getPublishingRules();
2931
+ let text = "## Publishing Rules\n\n";
2932
+ text += `Social media default: **${rules.socialDefaultAction ?? "draft"}**`;
2933
+ if (rules.socialDefaultAction === "draft") text += " (content is saved as draft, not published)";
2934
+ text += "\n";
2935
+ text += `Newsletter default: **${rules.newsletterDefaultAction ?? "draft"}**`;
2936
+ if (rules.newsletterDefaultAction === "draft") text += " (content is saved as draft, not sent)";
2937
+ text += "\n";
2938
+ text += `Preview required before publish: **${rules.requirePreviewBeforePublish ? "yes" : "no"}**
2939
+ `;
2940
+ text += `Double confirmation for newsletters: **${rules.requireDoubleConfirmNewsletter ? "yes" : "no"}**
2941
+ `;
2942
+ text += `Double confirmation for social: **${rules.requireDoubleConfirmSocial ? "yes" : "no"}**
2943
+ `;
2944
+ text += `Immediate publish allowed: **${rules.allowImmediatePublish ? "yes" : "no"}**
2945
+ `;
2946
+ text += `Immediate send allowed: **${rules.allowImmediateSend ? "yes" : "no"}**
2947
+ `;
2948
+ text += `Max posts per day: **${rules.maxPostsPerDay ?? "unlimited"}**
2949
+ `;
2950
+ if (rules.blockedWords && rules.blockedWords.length > 0) {
2951
+ text += `Blocked words: **${rules.blockedWords.join(", ")}**
2952
+ `;
2953
+ } else {
2954
+ text += "Blocked words: none\n";
2955
+ }
2956
+ if (rules.requiredDisclaimer) {
2957
+ text += `Required disclaimer: "${rules.requiredDisclaimer}"
2958
+ `;
2959
+ } else {
2960
+ text += "Required disclaimer: none\n";
2961
+ }
2962
+ return { content: [{ type: "text", text }] };
2963
+ }
2124
2964
  );
2125
- console.error(
2126
- "Set it in your Claude Desktop MCP config or export it in your shell."
2965
+ }
2966
+
2967
+ // src/tools/audit-log.ts
2968
+ import { z as z13 } from "zod";
2969
+ function registerAuditLogTools(server2, client2) {
2970
+ server2.tool(
2971
+ "get_audit_log",
2972
+ "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.",
2973
+ {
2974
+ limit: z13.number().optional().describe("Maximum number of entries to return (default 20, max 200)"),
2975
+ action: z13.string().optional().describe("Filter by action type prefix, e.g. 'post.published', 'newsletter.sent', 'post.' for all post actions"),
2976
+ resource_type: z13.string().optional().describe("Filter by resource type: post, newsletter, account, media, publishing_rules, brand_voice")
2977
+ },
2978
+ {
2979
+ title: "Get Audit Log",
2980
+ readOnlyHint: true,
2981
+ destructiveHint: false,
2982
+ idempotentHint: true,
2983
+ openWorldHint: false
2984
+ },
2985
+ async (args) => {
2986
+ const params = {};
2987
+ if (args.limit) params.limit = String(args.limit);
2988
+ if (args.action) params.action = args.action;
2989
+ if (args.resource_type) params.resource_type = args.resource_type;
2990
+ const data = await client2.getAuditLog(params);
2991
+ const entries = data?.entries ?? [];
2992
+ if (entries.length === 0) {
2993
+ return {
2994
+ content: [{ type: "text", text: "## Audit Log\n\nNo entries found." }]
2995
+ };
2996
+ }
2997
+ let text = `## Audit Log (${entries.length} of ${data?.total ?? entries.length} entries)
2998
+
2999
+ `;
3000
+ for (const entry of entries) {
3001
+ const date = entry.createdAt ? new Date(entry.createdAt).toLocaleString() : "unknown";
3002
+ const details = entry.details && Object.keys(entry.details).length > 0 ? ` \u2014 ${JSON.stringify(entry.details)}` : "";
3003
+ text += `- **${date}** | ${entry.action} | ${entry.resourceType}`;
3004
+ if (entry.resourceId) text += ` #${entry.resourceId}`;
3005
+ text += `${details}
3006
+ `;
3007
+ }
3008
+ return { content: [{ type: "text", text }] };
3009
+ }
2127
3010
  );
2128
- process.exit(1);
2129
3011
  }
2130
- var client = new BuzzPosterClient({ baseUrl: apiUrl, apiKey });
2131
- var server = new McpServer({
2132
- name: "buzzposter",
2133
- version: "0.1.0"
2134
- });
3012
+
3013
+ // src/tools/sources.ts
3014
+ import { z as z14 } from "zod";
3015
+ var TYPE_LABELS = {
3016
+ feed: "RSS Feed",
3017
+ website: "Website",
3018
+ youtube: "YouTube Channel",
3019
+ search: "Search Topic",
3020
+ podcast: "Podcast",
3021
+ reddit: "Reddit",
3022
+ social: "Social Profile"
3023
+ };
3024
+ var TYPE_HINTS = {
3025
+ feed: "Use fetch_feed to get latest entries",
3026
+ website: "Use fetch_article to extract content",
3027
+ youtube: "Use fetch_feed to get latest videos (YouTube RSS)",
3028
+ search: "Use web_search with the searchQuery",
3029
+ podcast: "Use fetch_feed to get latest episodes",
3030
+ reddit: "Use fetch_feed to get latest posts (Reddit RSS)",
3031
+ social: "Use web_search to check recent activity"
3032
+ };
3033
+ function registerSourceTools(server2, client2) {
3034
+ server2.tool(
3035
+ "get_sources",
3036
+ "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.",
3037
+ {
3038
+ type: z14.string().optional().describe(
3039
+ "Optional: filter by type (feed, website, youtube, search, podcast, reddit, social)"
3040
+ ),
3041
+ category: z14.string().optional().describe("Optional: filter by category")
3042
+ },
3043
+ {
3044
+ title: "Get Content Sources",
3045
+ readOnlyHint: true,
3046
+ destructiveHint: false,
3047
+ idempotentHint: true,
3048
+ openWorldHint: false
3049
+ },
3050
+ async (args) => {
3051
+ const params = {};
3052
+ if (args.type) params.type = args.type;
3053
+ if (args.category) params.category = args.category;
3054
+ const result = await client2.listSources(params);
3055
+ const sources = result.sources;
3056
+ if (sources.length === 0) {
3057
+ return {
3058
+ content: [
3059
+ {
3060
+ type: "text",
3061
+ 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."
3062
+ }
3063
+ ]
3064
+ };
3065
+ }
3066
+ const lines = [`## Content Sources (${sources.length})
3067
+ `];
3068
+ const byCategory = /* @__PURE__ */ new Map();
3069
+ for (const s of sources) {
3070
+ const cat = s.category || "Uncategorized";
3071
+ if (!byCategory.has(cat)) byCategory.set(cat, []);
3072
+ byCategory.get(cat).push(s);
3073
+ }
3074
+ for (const [cat, items] of byCategory) {
3075
+ lines.push(`### ${cat}
3076
+ `);
3077
+ for (const s of items) {
3078
+ const typeLabel = TYPE_LABELS[s.type] || s.type;
3079
+ const hint = TYPE_HINTS[s.type] || "";
3080
+ lines.push(`**${s.name}** (ID: ${s.id})`);
3081
+ lines.push(`- Type: ${typeLabel}`);
3082
+ if (s.url) lines.push(`- URL: ${s.url}`);
3083
+ if (s.searchQuery) lines.push(`- Search: "${s.searchQuery}"`);
3084
+ if (s.tags && s.tags.length > 0)
3085
+ lines.push(`- Tags: ${s.tags.join(", ")}`);
3086
+ if (s.notes) lines.push(`- Notes: ${s.notes}`);
3087
+ if (s.lastCheckedAt)
3088
+ lines.push(`- Last checked: ${s.lastCheckedAt}`);
3089
+ if (s.lastCheckedSummary)
3090
+ lines.push(`- Last result: ${s.lastCheckedSummary}`);
3091
+ if (hint) lines.push(`- How to check: ${hint}`);
3092
+ lines.push("");
3093
+ }
3094
+ }
3095
+ return {
3096
+ content: [{ type: "text", text: lines.join("\n") }]
3097
+ };
3098
+ }
3099
+ );
3100
+ server2.tool(
3101
+ "add_source",
3102
+ "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.",
3103
+ {
3104
+ name: z14.string().describe("Display name for the source"),
3105
+ url: z14.string().optional().describe(
3106
+ "URL of the source (RSS feed, website, YouTube channel, subreddit, or social profile URL). Not needed for 'search' type."
3107
+ ),
3108
+ type: z14.enum([
3109
+ "feed",
3110
+ "website",
3111
+ "youtube",
3112
+ "search",
3113
+ "podcast",
3114
+ "reddit",
3115
+ "social"
3116
+ ]).describe("Type of source. Determines how to fetch content from it."),
3117
+ category: z14.string().optional().describe(
3118
+ "Optional category for organization (e.g. 'competitors', 'industry', 'inspiration', 'my-content')"
3119
+ ),
3120
+ tags: z14.array(z14.string()).optional().describe("Optional tags for filtering"),
3121
+ notes: z14.string().optional().describe("Optional notes about why this source matters"),
3122
+ searchQuery: z14.string().optional().describe(
3123
+ "For 'search' type only: the keyword or topic to search for"
3124
+ )
3125
+ },
3126
+ {
3127
+ title: "Add Content Source",
3128
+ readOnlyHint: false,
3129
+ destructiveHint: false,
3130
+ idempotentHint: false,
3131
+ openWorldHint: false
3132
+ },
3133
+ async (args) => {
3134
+ const data = {
3135
+ name: args.name,
3136
+ type: args.type
3137
+ };
3138
+ if (args.url) data.url = args.url;
3139
+ if (args.category) data.category = args.category;
3140
+ if (args.tags) data.tags = args.tags;
3141
+ if (args.notes) data.notes = args.notes;
3142
+ if (args.searchQuery) data.searchQuery = args.searchQuery;
3143
+ const result = await client2.createSource(data);
3144
+ const typeLabel = TYPE_LABELS[result.type] || result.type;
3145
+ const lines = [
3146
+ `## Source Added
3147
+ `,
3148
+ `**${result.name}** (ID: ${result.id})`,
3149
+ `- Type: ${typeLabel}`
3150
+ ];
3151
+ if (result.url) lines.push(`- URL: ${result.url}`);
3152
+ if (result.searchQuery)
3153
+ lines.push(`- Search: "${result.searchQuery}"`);
3154
+ if (result.category) lines.push(`- Category: ${result.category}`);
3155
+ return {
3156
+ content: [{ type: "text", text: lines.join("\n") }]
3157
+ };
3158
+ }
3159
+ );
3160
+ server2.tool(
3161
+ "update_source",
3162
+ "Update a saved content source. Use when the customer wants to rename, recategorize, change the URL, add notes, or deactivate a source.",
3163
+ {
3164
+ source_id: z14.number().describe("The ID of the source to update"),
3165
+ name: z14.string().optional().describe("New display name"),
3166
+ url: z14.string().optional().describe("New URL"),
3167
+ type: z14.enum([
3168
+ "feed",
3169
+ "website",
3170
+ "youtube",
3171
+ "search",
3172
+ "podcast",
3173
+ "reddit",
3174
+ "social"
3175
+ ]).optional().describe("New source type"),
3176
+ category: z14.string().optional().describe("New category"),
3177
+ tags: z14.array(z14.string()).optional().describe("New tags"),
3178
+ notes: z14.string().optional().describe("New notes"),
3179
+ searchQuery: z14.string().optional().describe("New search query"),
3180
+ isActive: z14.boolean().optional().describe("Set to false to deactivate")
3181
+ },
3182
+ {
3183
+ title: "Update Content Source",
3184
+ readOnlyHint: false,
3185
+ destructiveHint: false,
3186
+ idempotentHint: true,
3187
+ openWorldHint: false
3188
+ },
3189
+ async (args) => {
3190
+ const { source_id, ...updates } = args;
3191
+ const data = {};
3192
+ for (const [k, v] of Object.entries(updates)) {
3193
+ if (v !== void 0) data[k] = v;
3194
+ }
3195
+ const result = await client2.updateSource(source_id, data);
3196
+ return {
3197
+ content: [
3198
+ {
3199
+ type: "text",
3200
+ text: `Source **${result.name}** (ID: ${result.id}) updated successfully.`
3201
+ }
3202
+ ]
3203
+ };
3204
+ }
3205
+ );
3206
+ server2.tool(
3207
+ "remove_source",
3208
+ `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.
3209
+
3210
+ REQUIRED WORKFLOW:
3211
+ 1. ALWAYS set confirmed=false first to preview which source will be deleted
3212
+ 2. Show the user the source details before confirming
3213
+ 3. Only confirm after explicit user approval`,
3214
+ {
3215
+ source_id: z14.number().describe("The ID of the source to remove"),
3216
+ confirmed: z14.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
3217
+ },
3218
+ {
3219
+ title: "Remove Content Source",
3220
+ readOnlyHint: false,
3221
+ destructiveHint: true,
3222
+ idempotentHint: false,
3223
+ openWorldHint: false
3224
+ },
3225
+ async (args) => {
3226
+ if (args.confirmed !== true) {
3227
+ const preview = `## Remove Source Confirmation
3228
+
3229
+ **Source ID:** ${args.source_id}
3230
+
3231
+ This will permanently remove this content source. Call this tool again with confirmed=true to proceed.`;
3232
+ return { content: [{ type: "text", text: preview }] };
3233
+ }
3234
+ await client2.deleteSource(args.source_id);
3235
+ return {
3236
+ content: [
3237
+ {
3238
+ type: "text",
3239
+ text: "Content source removed successfully."
3240
+ }
3241
+ ]
3242
+ };
3243
+ }
3244
+ );
3245
+ }
3246
+
3247
+ // src/tools/newsletter-advanced.ts
3248
+ import { z as z15 } from "zod";
3249
+ function registerNewsletterAdvancedTools(server2, client2) {
3250
+ server2.tool(
3251
+ "get_subscriber_by_email",
3252
+ "Look up a subscriber by their email address. Returns subscriber details including tags and custom fields.",
3253
+ {
3254
+ email: z15.string().describe("Email address to look up")
3255
+ },
3256
+ {
3257
+ title: "Get Subscriber by Email",
3258
+ readOnlyHint: true,
3259
+ destructiveHint: false,
3260
+ idempotentHint: true,
3261
+ openWorldHint: true
3262
+ },
3263
+ async (args) => {
3264
+ const result = await client2.getSubscriberByEmail(args.email);
3265
+ return {
3266
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3267
+ };
3268
+ }
3269
+ );
3270
+ server2.tool(
3271
+ "list_automations",
3272
+ "List all email automations/workflows from your ESP. On Beehiiv these are journeys; on Kit these are sequences.",
3273
+ {
3274
+ page: z15.string().optional().describe("Page number for pagination")
3275
+ },
3276
+ {
3277
+ title: "List Automations",
3278
+ readOnlyHint: true,
3279
+ destructiveHint: false,
3280
+ idempotentHint: true,
3281
+ openWorldHint: true
3282
+ },
3283
+ async (args) => {
3284
+ const params = {};
3285
+ if (args.page) params.page = args.page;
3286
+ const result = await client2.listAutomations(params);
3287
+ return {
3288
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3289
+ };
3290
+ }
3291
+ );
3292
+ server2.tool(
3293
+ "list_segments",
3294
+ "List subscriber segments. Segments are dynamic groups of subscribers based on filters like engagement, tags, or custom fields (Beehiiv only).",
3295
+ {
3296
+ page: z15.string().optional().describe("Page number"),
3297
+ type: z15.string().optional().describe("Filter by segment type"),
3298
+ status: z15.string().optional().describe("Filter by segment status"),
3299
+ expand: z15.string().optional().describe("Comma-separated expand fields (e.g. 'stats')")
3300
+ },
3301
+ {
3302
+ title: "List Segments",
3303
+ readOnlyHint: true,
3304
+ destructiveHint: false,
3305
+ idempotentHint: true,
3306
+ openWorldHint: true
3307
+ },
3308
+ async (args) => {
3309
+ const params = {};
3310
+ if (args.page) params.page = args.page;
3311
+ if (args.type) params.type = args.type;
3312
+ if (args.status) params.status = args.status;
3313
+ if (args.expand) params.expand = args.expand;
3314
+ const result = await client2.listSegments(params);
3315
+ return {
3316
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3317
+ };
3318
+ }
3319
+ );
3320
+ server2.tool(
3321
+ "get_segment",
3322
+ "Get details of a specific subscriber segment by ID.",
3323
+ {
3324
+ segment_id: z15.string().describe("The segment ID"),
3325
+ expand: z15.string().optional().describe("Comma-separated expand fields")
3326
+ },
3327
+ {
3328
+ title: "Get Segment",
3329
+ readOnlyHint: true,
3330
+ destructiveHint: false,
3331
+ idempotentHint: true,
3332
+ openWorldHint: true
3333
+ },
3334
+ async (args) => {
3335
+ const params = {};
3336
+ if (args.expand) params.expand = args.expand;
3337
+ const result = await client2.getSegment(args.segment_id, params);
3338
+ return {
3339
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3340
+ };
3341
+ }
3342
+ );
3343
+ server2.tool(
3344
+ "get_segment_members",
3345
+ "List subscribers who belong to a specific segment.",
3346
+ {
3347
+ segment_id: z15.string().describe("The segment ID"),
3348
+ page: z15.string().optional().describe("Page number")
3349
+ },
3350
+ {
3351
+ title: "Get Segment Members",
3352
+ readOnlyHint: true,
3353
+ destructiveHint: false,
3354
+ idempotentHint: true,
3355
+ openWorldHint: true
3356
+ },
3357
+ async (args) => {
3358
+ const params = {};
3359
+ if (args.page) params.page = args.page;
3360
+ const result = await client2.getSegmentMembers(args.segment_id, params);
3361
+ return {
3362
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3363
+ };
3364
+ }
3365
+ );
3366
+ server2.tool(
3367
+ "list_custom_fields",
3368
+ "List all custom subscriber fields defined in your ESP. Custom fields can store extra data like preferences, source, or company.",
3369
+ {},
3370
+ {
3371
+ title: "List Custom Fields",
3372
+ readOnlyHint: true,
3373
+ destructiveHint: false,
3374
+ idempotentHint: true,
3375
+ openWorldHint: true
3376
+ },
3377
+ async () => {
3378
+ const result = await client2.listCustomFields();
3379
+ return {
3380
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3381
+ };
3382
+ }
3383
+ );
3384
+ server2.tool(
3385
+ "get_referral_program",
3386
+ "Get your newsletter's referral program details including milestones and rewards (Beehiiv only).",
3387
+ {},
3388
+ {
3389
+ title: "Get Referral Program",
3390
+ readOnlyHint: true,
3391
+ destructiveHint: false,
3392
+ idempotentHint: true,
3393
+ openWorldHint: true
3394
+ },
3395
+ async () => {
3396
+ const result = await client2.getReferralProgram();
3397
+ return {
3398
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3399
+ };
3400
+ }
3401
+ );
3402
+ server2.tool(
3403
+ "list_tiers",
3404
+ "List all subscription tiers (free and premium) from your ESP (Beehiiv only).",
3405
+ {},
3406
+ {
3407
+ title: "List Tiers",
3408
+ readOnlyHint: true,
3409
+ destructiveHint: false,
3410
+ idempotentHint: true,
3411
+ openWorldHint: true
3412
+ },
3413
+ async () => {
3414
+ const result = await client2.listEspTiers();
3415
+ return {
3416
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3417
+ };
3418
+ }
3419
+ );
3420
+ server2.tool(
3421
+ "list_post_templates",
3422
+ "List all post/newsletter templates available in your ESP (Beehiiv only).",
3423
+ {},
3424
+ {
3425
+ title: "List Post Templates",
3426
+ readOnlyHint: true,
3427
+ destructiveHint: false,
3428
+ idempotentHint: true,
3429
+ openWorldHint: true
3430
+ },
3431
+ async () => {
3432
+ const result = await client2.listPostTemplates();
3433
+ return {
3434
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3435
+ };
3436
+ }
3437
+ );
3438
+ server2.tool(
3439
+ "get_post_aggregate_stats",
3440
+ "Get aggregate statistics across all posts/newsletters including total clicks, impressions, and click rates (Beehiiv only).",
3441
+ {},
3442
+ {
3443
+ title: "Get Post Aggregate Stats",
3444
+ readOnlyHint: true,
3445
+ destructiveHint: false,
3446
+ idempotentHint: true,
3447
+ openWorldHint: true
3448
+ },
3449
+ async () => {
3450
+ const result = await client2.getPostAggregateStats();
3451
+ return {
3452
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3453
+ };
3454
+ }
3455
+ );
3456
+ server2.tool(
3457
+ "list_esp_webhooks",
3458
+ "List all webhooks configured in your ESP for event notifications.",
3459
+ {},
3460
+ {
3461
+ title: "List ESP Webhooks",
3462
+ readOnlyHint: true,
3463
+ destructiveHint: false,
3464
+ idempotentHint: true,
3465
+ openWorldHint: true
3466
+ },
3467
+ async () => {
3468
+ const result = await client2.listEspWebhooks();
3469
+ return {
3470
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3471
+ };
3472
+ }
3473
+ );
3474
+ server2.tool(
3475
+ "tag_subscriber",
3476
+ "Add tags to a subscriber. Tags help categorize subscribers for targeted sends and automations.",
3477
+ {
3478
+ subscriber_id: z15.string().describe("The subscriber/subscription ID"),
3479
+ tags: z15.array(z15.string()).describe("Tags to add to the subscriber")
3480
+ },
3481
+ {
3482
+ title: "Tag Subscriber",
3483
+ readOnlyHint: false,
3484
+ destructiveHint: false,
3485
+ idempotentHint: true,
3486
+ openWorldHint: true
3487
+ },
3488
+ async (args) => {
3489
+ const result = await client2.tagSubscriber(args.subscriber_id, { tags: args.tags });
3490
+ return {
3491
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3492
+ };
3493
+ }
3494
+ );
3495
+ server2.tool(
3496
+ "update_subscriber",
3497
+ `Update a subscriber's details. On Beehiiv: update tier or custom fields. On Kit: update name or custom fields (tier not supported).
3498
+
3499
+ REQUIRED WORKFLOW:
3500
+ 1. ALWAYS set confirmed=false first to get a preview
3501
+ 2. Show the user what will happen before confirming
3502
+ 3. Only set confirmed=true after the user explicitly approves`,
3503
+ {
3504
+ subscriber_id: z15.string().describe("The subscriber/subscription ID"),
3505
+ tier: z15.string().optional().describe("New tier for the subscriber"),
3506
+ custom_fields: z15.record(z15.unknown()).optional().describe("Custom field values to set"),
3507
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm update")
3508
+ },
3509
+ {
3510
+ title: "Update Subscriber",
3511
+ readOnlyHint: false,
3512
+ destructiveHint: false,
3513
+ idempotentHint: true,
3514
+ openWorldHint: true
3515
+ },
3516
+ async (args) => {
3517
+ if (args.confirmed !== true) {
3518
+ const changes = [];
3519
+ if (args.tier) changes.push(`**Tier:** ${args.tier}`);
3520
+ if (args.custom_fields) changes.push(`**Custom fields:** ${Object.keys(args.custom_fields).join(", ")}`);
3521
+ return {
3522
+ content: [{
3523
+ type: "text",
3524
+ text: `## Update Subscriber
3525
+
3526
+ **Subscriber ID:** ${args.subscriber_id}
3527
+ ${changes.join("\n")}
3528
+
3529
+ Call again with confirmed=true to proceed.`
3530
+ }]
3531
+ };
3532
+ }
3533
+ const data = {};
3534
+ if (args.tier) data.tier = args.tier;
3535
+ if (args.custom_fields) data.customFields = args.custom_fields;
3536
+ const result = await client2.updateSubscriber(args.subscriber_id, data);
3537
+ return {
3538
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3539
+ };
3540
+ }
3541
+ );
3542
+ server2.tool(
3543
+ "create_custom_field",
3544
+ `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.
3545
+
3546
+ REQUIRED WORKFLOW:
3547
+ 1. ALWAYS set confirmed=false first to get a preview
3548
+ 2. Show the user what will happen before confirming
3549
+ 3. Only set confirmed=true after the user explicitly approves`,
3550
+ {
3551
+ name: z15.string().describe("Field name"),
3552
+ kind: z15.string().describe("Field type: string, integer, boolean, date, or enum"),
3553
+ options: z15.array(z15.string()).optional().describe("Options for enum-type fields"),
3554
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm creation")
3555
+ },
3556
+ {
3557
+ title: "Create Custom Field",
3558
+ readOnlyHint: false,
3559
+ destructiveHint: false,
3560
+ idempotentHint: false,
3561
+ openWorldHint: true
3562
+ },
3563
+ async (args) => {
3564
+ if (args.confirmed !== true) {
3565
+ const optionsLine = args.options ? `
3566
+ **Options:** ${args.options.join(", ")}` : "";
3567
+ return {
3568
+ content: [{
3569
+ type: "text",
3570
+ text: `## Create Custom Field
3571
+
3572
+ **Name:** ${args.name}
3573
+ **Kind:** ${args.kind}${optionsLine}
3574
+
3575
+ This creates a permanent schema-level field visible on all subscribers. Call again with confirmed=true to proceed.`
3576
+ }]
3577
+ };
3578
+ }
3579
+ const data = { name: args.name, kind: args.kind };
3580
+ if (args.options) data.options = args.options;
3581
+ const result = await client2.createCustomField(data);
3582
+ return {
3583
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3584
+ };
3585
+ }
3586
+ );
3587
+ server2.tool(
3588
+ "update_custom_field",
3589
+ `Update an existing custom subscriber field's name or kind. Renaming or rekeying a field can break automations that reference it.
3590
+
3591
+ REQUIRED WORKFLOW:
3592
+ 1. ALWAYS set confirmed=false first to get a preview
3593
+ 2. Show the user what will happen before confirming
3594
+ 3. Only set confirmed=true after the user explicitly approves`,
3595
+ {
3596
+ field_id: z15.string().describe("The custom field ID"),
3597
+ name: z15.string().optional().describe("New field name"),
3598
+ kind: z15.string().optional().describe("New field type"),
3599
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm update")
3600
+ },
3601
+ {
3602
+ title: "Update Custom Field",
3603
+ readOnlyHint: false,
3604
+ destructiveHint: false,
3605
+ idempotentHint: true,
3606
+ openWorldHint: true
3607
+ },
3608
+ async (args) => {
3609
+ if (args.confirmed !== true) {
3610
+ const changes = [];
3611
+ if (args.name) changes.push(`**Name:** ${args.name}`);
3612
+ if (args.kind) changes.push(`**Kind:** ${args.kind}`);
3613
+ return {
3614
+ content: [{
3615
+ type: "text",
3616
+ text: `## Update Custom Field
3617
+
3618
+ **Field ID:** ${args.field_id}
3619
+ ${changes.join("\n")}
3620
+
3621
+ Renaming or changing the type of a field can break automations that reference it. Call again with confirmed=true to proceed.`
3622
+ }]
3623
+ };
3624
+ }
3625
+ const data = {};
3626
+ if (args.name) data.name = args.name;
3627
+ if (args.kind) data.kind = args.kind;
3628
+ const result = await client2.updateCustomField(args.field_id, data);
3629
+ return {
3630
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3631
+ };
3632
+ }
3633
+ );
3634
+ server2.tool(
3635
+ "recalculate_segment",
3636
+ "Trigger a recalculation of a segment's membership. Useful after changing subscriber data or segment rules (Beehiiv only).",
3637
+ {
3638
+ segment_id: z15.string().describe("The segment ID to recalculate")
3639
+ },
3640
+ {
3641
+ title: "Recalculate Segment",
3642
+ readOnlyHint: false,
3643
+ destructiveHint: false,
3644
+ idempotentHint: true,
3645
+ openWorldHint: true
3646
+ },
3647
+ async (args) => {
3648
+ await client2.recalculateSegment(args.segment_id);
3649
+ return {
3650
+ content: [{ type: "text", text: "Segment recalculation triggered." }]
3651
+ };
3652
+ }
3653
+ );
3654
+ server2.tool(
3655
+ "enroll_in_automation",
3656
+ `Enroll a subscriber in an automation/journey. Provide either an email or subscription ID. On Kit, enrolls into a sequence.
3657
+
3658
+ REQUIRED WORKFLOW:
3659
+ 1. ALWAYS set confirmed=false first to get a preview
3660
+ 2. Show the user what will happen before confirming
3661
+ 3. Only set confirmed=true after the user explicitly approves`,
3662
+ {
3663
+ automation_id: z15.string().describe("The automation ID"),
3664
+ email: z15.string().optional().describe("Subscriber email to enroll"),
3665
+ subscription_id: z15.string().optional().describe("Subscription ID to enroll"),
3666
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm enrollment")
3667
+ },
3668
+ {
3669
+ title: "Enroll in Automation",
3670
+ readOnlyHint: false,
3671
+ destructiveHint: false,
3672
+ idempotentHint: false,
3673
+ openWorldHint: true
3674
+ },
3675
+ async (args) => {
3676
+ if (args.confirmed !== true) {
3677
+ const target = args.email ?? args.subscription_id ?? "unknown";
3678
+ return {
3679
+ content: [{
3680
+ type: "text",
3681
+ text: `## Enroll in Automation
3682
+
3683
+ **Automation ID:** ${args.automation_id}
3684
+ **Target:** ${target}
3685
+
3686
+ This will add the subscriber to the automation journey. Call again with confirmed=true to proceed.`
3687
+ }]
3688
+ };
3689
+ }
3690
+ const data = {};
3691
+ if (args.email) data.email = args.email;
3692
+ if (args.subscription_id) data.subscriptionId = args.subscription_id;
3693
+ await client2.enrollInAutomation(args.automation_id, data);
3694
+ return {
3695
+ content: [{ type: "text", text: "Subscriber enrolled in automation." }]
3696
+ };
3697
+ }
3698
+ );
3699
+ server2.tool(
3700
+ "bulk_add_subscribers",
3701
+ `Add multiple subscribers in a single request. Maximum 1000 subscribers per call.
3702
+
3703
+ REQUIRED WORKFLOW:
3704
+ 1. ALWAYS set confirmed=false first to get a preview
3705
+ 2. Show the user what will happen before confirming
3706
+ 3. Only set confirmed=true after the user explicitly approves`,
3707
+ {
3708
+ subscribers: z15.array(z15.object({
3709
+ email: z15.string().describe("Subscriber email"),
3710
+ data: z15.record(z15.unknown()).optional().describe("Additional subscriber data")
3711
+ })).describe("Array of subscribers to add"),
3712
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm bulk add")
3713
+ },
3714
+ {
3715
+ title: "Bulk Add Subscribers",
3716
+ readOnlyHint: false,
3717
+ destructiveHint: false,
3718
+ idempotentHint: false,
3719
+ openWorldHint: true
3720
+ },
3721
+ async (args) => {
3722
+ if (args.confirmed !== true) {
3723
+ return {
3724
+ content: [{
3725
+ type: "text",
3726
+ text: `## Bulk Add Subscribers
3727
+
3728
+ **Count:** ${args.subscribers.length} subscriber(s)
3729
+ **Sample:** ${args.subscribers.slice(0, 3).map((s) => s.email).join(", ")}${args.subscribers.length > 3 ? "..." : ""}
3730
+
3731
+ Call again with confirmed=true to proceed.`
3732
+ }]
3733
+ };
3734
+ }
3735
+ const result = await client2.bulkCreateSubscribers({ subscribers: args.subscribers });
3736
+ return {
3737
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3738
+ };
3739
+ }
3740
+ );
3741
+ server2.tool(
3742
+ "create_esp_webhook",
3743
+ `Create a webhook in your ESP to receive event notifications. Note: Kit only supports one event per webhook.
3744
+
3745
+ REQUIRED WORKFLOW:
3746
+ 1. ALWAYS set confirmed=false first to get a preview
3747
+ 2. Show the user what will happen before confirming
3748
+ 3. Only set confirmed=true after the user explicitly approves`,
3749
+ {
3750
+ url: z15.string().describe("Webhook URL to receive events"),
3751
+ event_types: z15.array(z15.string()).describe("Event types to subscribe to (e.g. 'subscription.created', 'post.sent')"),
3752
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm creation")
3753
+ },
3754
+ {
3755
+ title: "Create ESP Webhook",
3756
+ readOnlyHint: false,
3757
+ destructiveHint: false,
3758
+ idempotentHint: false,
3759
+ openWorldHint: true
3760
+ },
3761
+ async (args) => {
3762
+ if (args.confirmed !== true) {
3763
+ return {
3764
+ content: [{
3765
+ type: "text",
3766
+ text: `## Create ESP Webhook
3767
+
3768
+ **URL:** ${args.url}
3769
+ **Events:** ${args.event_types.join(", ")}
3770
+
3771
+ Call again with confirmed=true to create.`
3772
+ }]
3773
+ };
3774
+ }
3775
+ const result = await client2.createEspWebhook({ url: args.url, eventTypes: args.event_types });
3776
+ return {
3777
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3778
+ };
3779
+ }
3780
+ );
3781
+ server2.tool(
3782
+ "delete_broadcast",
3783
+ `Permanently delete a newsletter/broadcast. This action is permanent and cannot be undone.
3784
+
3785
+ REQUIRED WORKFLOW:
3786
+ 1. ALWAYS set confirmed=false first to get a preview
3787
+ 2. Show the user what will happen before confirming
3788
+ 3. Only set confirmed=true after the user explicitly approves`,
3789
+ {
3790
+ broadcast_id: z15.string().describe("The broadcast ID to delete"),
3791
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3792
+ },
3793
+ {
3794
+ title: "Delete Broadcast",
3795
+ readOnlyHint: false,
3796
+ destructiveHint: true,
3797
+ idempotentHint: true,
3798
+ openWorldHint: true
3799
+ },
3800
+ async (args) => {
3801
+ if (args.confirmed !== true) {
3802
+ return {
3803
+ content: [{
3804
+ type: "text",
3805
+ text: `## Delete Broadcast
3806
+
3807
+ **Broadcast ID:** ${args.broadcast_id}
3808
+
3809
+ **This action is permanent and cannot be undone.**
3810
+
3811
+ Call again with confirmed=true to delete.`
3812
+ }]
3813
+ };
3814
+ }
3815
+ await client2.deleteBroadcast(args.broadcast_id);
3816
+ return {
3817
+ content: [{ type: "text", text: "Broadcast deleted." }]
3818
+ };
3819
+ }
3820
+ );
3821
+ server2.tool(
3822
+ "delete_subscriber",
3823
+ `Remove a subscriber from your mailing list. On Beehiiv this permanently deletes the subscriber. On Kit this unsubscribes them (Kit has no hard delete).
3824
+
3825
+ REQUIRED WORKFLOW:
3826
+ 1. ALWAYS set confirmed=false first to get a preview
3827
+ 2. Show the user what will happen before confirming
3828
+ 3. Only set confirmed=true after the user explicitly approves`,
3829
+ {
3830
+ subscriber_id: z15.string().describe("The subscriber ID to delete"),
3831
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3832
+ },
3833
+ {
3834
+ title: "Delete Subscriber",
3835
+ readOnlyHint: false,
3836
+ destructiveHint: true,
3837
+ idempotentHint: true,
3838
+ openWorldHint: true
3839
+ },
3840
+ async (args) => {
3841
+ if (args.confirmed !== true) {
3842
+ return {
3843
+ content: [{
3844
+ type: "text",
3845
+ text: `## Delete Subscriber
3846
+
3847
+ **Subscriber ID:** ${args.subscriber_id}
3848
+
3849
+ **This action is permanent and cannot be undone.** The subscriber will be removed from all segments and automations.
3850
+
3851
+ Call again with confirmed=true to delete.`
3852
+ }]
3853
+ };
3854
+ }
3855
+ await client2.deleteSubscriber(args.subscriber_id);
3856
+ return {
3857
+ content: [{ type: "text", text: "Subscriber deleted." }]
3858
+ };
3859
+ }
3860
+ );
3861
+ server2.tool(
3862
+ "delete_segment",
3863
+ `Permanently delete a subscriber segment. This action is permanent and cannot be undone (Beehiiv only).
3864
+
3865
+ REQUIRED WORKFLOW:
3866
+ 1. ALWAYS set confirmed=false first to get a preview
3867
+ 2. Show the user what will happen before confirming
3868
+ 3. Only set confirmed=true after the user explicitly approves`,
3869
+ {
3870
+ segment_id: z15.string().describe("The segment ID to delete"),
3871
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3872
+ },
3873
+ {
3874
+ title: "Delete Segment",
3875
+ readOnlyHint: false,
3876
+ destructiveHint: true,
3877
+ idempotentHint: true,
3878
+ openWorldHint: true
3879
+ },
3880
+ async (args) => {
3881
+ if (args.confirmed !== true) {
3882
+ return {
3883
+ content: [{
3884
+ type: "text",
3885
+ text: `## Delete Segment
3886
+
3887
+ **Segment ID:** ${args.segment_id}
3888
+
3889
+ **This action is permanent and cannot be undone.** Subscribers in this segment will not be deleted, but the segment grouping will be removed.
3890
+
3891
+ Call again with confirmed=true to delete.`
3892
+ }]
3893
+ };
3894
+ }
3895
+ await client2.deleteSegment(args.segment_id);
3896
+ return {
3897
+ content: [{ type: "text", text: "Segment deleted." }]
3898
+ };
3899
+ }
3900
+ );
3901
+ server2.tool(
3902
+ "delete_custom_field",
3903
+ `Permanently delete a custom subscriber field and remove it from all subscribers. This action is permanent and cannot be undone.
3904
+
3905
+ REQUIRED WORKFLOW:
3906
+ 1. ALWAYS set confirmed=false first to get a preview
3907
+ 2. Show the user what will happen before confirming
3908
+ 3. Only set confirmed=true after the user explicitly approves`,
3909
+ {
3910
+ field_id: z15.string().describe("The custom field ID to delete"),
3911
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3912
+ },
3913
+ {
3914
+ title: "Delete Custom Field",
3915
+ readOnlyHint: false,
3916
+ destructiveHint: true,
3917
+ idempotentHint: true,
3918
+ openWorldHint: true
3919
+ },
3920
+ async (args) => {
3921
+ if (args.confirmed !== true) {
3922
+ return {
3923
+ content: [{
3924
+ type: "text",
3925
+ text: `## Delete Custom Field
3926
+
3927
+ **Field ID:** ${args.field_id}
3928
+
3929
+ **This action is permanent and cannot be undone.** The field and its data will be removed from all subscribers.
3930
+
3931
+ Call again with confirmed=true to delete.`
3932
+ }]
3933
+ };
3934
+ }
3935
+ await client2.deleteCustomField(args.field_id);
3936
+ return {
3937
+ content: [{ type: "text", text: "Custom field deleted." }]
3938
+ };
3939
+ }
3940
+ );
3941
+ server2.tool(
3942
+ "delete_esp_webhook",
3943
+ `Permanently delete a webhook from your ESP. This action is permanent and cannot be undone.
3944
+
3945
+ REQUIRED WORKFLOW:
3946
+ 1. ALWAYS set confirmed=false first to get a preview
3947
+ 2. Show the user what will happen before confirming
3948
+ 3. Only set confirmed=true after the user explicitly approves`,
3949
+ {
3950
+ webhook_id: z15.string().describe("The webhook ID to delete"),
3951
+ confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3952
+ },
3953
+ {
3954
+ title: "Delete ESP Webhook",
3955
+ readOnlyHint: false,
3956
+ destructiveHint: true,
3957
+ idempotentHint: true,
3958
+ openWorldHint: true
3959
+ },
3960
+ async (args) => {
3961
+ if (args.confirmed !== true) {
3962
+ return {
3963
+ content: [{
3964
+ type: "text",
3965
+ text: `## Delete ESP Webhook
3966
+
3967
+ **Webhook ID:** ${args.webhook_id}
3968
+
3969
+ **This action is permanent and cannot be undone.** You will stop receiving event notifications at this webhook URL.
3970
+
3971
+ Call again with confirmed=true to delete.`
3972
+ }]
3973
+ };
3974
+ }
3975
+ await client2.deleteEspWebhook(args.webhook_id);
3976
+ return {
3977
+ content: [{ type: "text", text: "Webhook deleted." }]
3978
+ };
3979
+ }
3980
+ );
3981
+ }
3982
+
3983
+ // src/tools/carousel.ts
3984
+ import { z as z16 } from "zod";
3985
+ function registerCarouselTools(server2, client2) {
3986
+ server2.tool(
3987
+ "get_carousel_template",
3988
+ "Get the customer's carousel template configuration. Returns brand colors, logo, CTA text, and other visual settings used to generate carousels from article URLs.",
3989
+ {},
3990
+ {
3991
+ title: "Get Carousel Template",
3992
+ readOnlyHint: true,
3993
+ destructiveHint: false,
3994
+ idempotentHint: true,
3995
+ openWorldHint: false
3996
+ },
3997
+ async () => {
3998
+ try {
3999
+ const template = await client2.getCarouselTemplate();
4000
+ const lines = [];
4001
+ lines.push(`## Carousel Template: ${template.name || "Not configured"}`);
4002
+ lines.push("");
4003
+ if (!template.id) {
4004
+ lines.push("No carousel template has been configured yet.");
4005
+ lines.push("");
4006
+ lines.push("Use `save_carousel_template` to set one up with your brand name, colors, and CTA text.");
4007
+ return {
4008
+ content: [{ type: "text", text: lines.join("\n") }]
4009
+ };
4010
+ }
4011
+ lines.push(`**Brand Name:** ${template.brandName || "\u2014"}`);
4012
+ if (template.logoUrl) lines.push(`**Logo:** ${template.logoUrl}`);
4013
+ lines.push(`**Primary Color:** ${template.colorPrimary || "#1a1a2e"}`);
4014
+ lines.push(`**Secondary Color:** ${template.colorSecondary || "#e94560"}`);
4015
+ lines.push(`**Accent Color:** ${template.colorAccent || "#ffffff"}`);
4016
+ lines.push(`**CTA Text:** ${template.ctaText || "Read the full story \u2192"}`);
4017
+ lines.push(`**Logo Placement:** ${template.logoPlacement || "top-left"}`);
4018
+ lines.push("");
4019
+ return {
4020
+ content: [{ type: "text", text: lines.join("\n") }]
4021
+ };
4022
+ } catch (error) {
4023
+ const message = error instanceof Error ? error.message : "Unknown error";
4024
+ if (message.includes("404")) {
4025
+ return {
4026
+ content: [
4027
+ {
4028
+ type: "text",
4029
+ text: "No carousel template has been configured yet. Use `save_carousel_template` to set one up."
4030
+ }
4031
+ ]
4032
+ };
4033
+ }
4034
+ throw error;
4035
+ }
4036
+ }
4037
+ );
4038
+ server2.tool(
4039
+ "save_carousel_template",
4040
+ `Create or update the customer's carousel template. This configures the visual style for auto-generated carousels (brand colors, logo, CTA text).
4041
+
4042
+ USAGE:
4043
+ - Set confirmed=false first to preview, then confirmed=true after user approval
4044
+ - brand_name is required
4045
+ - All color values should be hex codes (e.g. "#1a1a2e")
4046
+ - logo_placement: "top-left", "top-right", or "top-center"`,
4047
+ {
4048
+ brand_name: z16.string().max(255).describe("Brand name displayed on CTA slide"),
4049
+ color_primary: z16.string().max(20).optional().describe('Primary background color (hex, e.g. "#1a1a2e")'),
4050
+ color_secondary: z16.string().max(20).optional().describe('Secondary/accent color for buttons and highlights (hex, e.g. "#e94560")'),
4051
+ color_accent: z16.string().max(20).optional().describe('Text color (hex, e.g. "#ffffff")'),
4052
+ cta_text: z16.string().max(255).optional().describe('Call-to-action text on the final slide (e.g. "Read the full story \u2192")'),
4053
+ logo_placement: z16.enum(["top-left", "top-right", "top-center"]).optional().describe("Where to place the logo on slides"),
4054
+ confirmed: z16.boolean().default(false).describe("Set to true to confirm and save. If false, returns a preview.")
4055
+ },
4056
+ {
4057
+ title: "Save Carousel Template",
4058
+ readOnlyHint: false,
4059
+ destructiveHint: false,
4060
+ idempotentHint: false,
4061
+ openWorldHint: false
4062
+ },
4063
+ async (args) => {
4064
+ const payload = {
4065
+ brandName: args.brand_name
4066
+ };
4067
+ if (args.color_primary !== void 0) payload.colorPrimary = args.color_primary;
4068
+ if (args.color_secondary !== void 0) payload.colorSecondary = args.color_secondary;
4069
+ if (args.color_accent !== void 0) payload.colorAccent = args.color_accent;
4070
+ if (args.cta_text !== void 0) payload.ctaText = args.cta_text;
4071
+ if (args.logo_placement !== void 0) payload.logoPlacement = args.logo_placement;
4072
+ if (args.confirmed !== true) {
4073
+ const lines2 = [];
4074
+ lines2.push("## Carousel Template Preview");
4075
+ lines2.push("");
4076
+ lines2.push(`**Brand Name:** ${args.brand_name}`);
4077
+ if (args.color_primary) lines2.push(`**Primary Color:** ${args.color_primary}`);
4078
+ if (args.color_secondary) lines2.push(`**Secondary Color:** ${args.color_secondary}`);
4079
+ if (args.color_accent) lines2.push(`**Accent Color:** ${args.color_accent}`);
4080
+ if (args.cta_text) lines2.push(`**CTA Text:** ${args.cta_text}`);
4081
+ if (args.logo_placement) lines2.push(`**Logo Placement:** ${args.logo_placement}`);
4082
+ lines2.push("");
4083
+ lines2.push("---");
4084
+ lines2.push("Call this tool again with **confirmed=true** to save these settings.");
4085
+ return {
4086
+ content: [{ type: "text", text: lines2.join("\n") }]
4087
+ };
4088
+ }
4089
+ const result = await client2.updateCarouselTemplate(payload);
4090
+ const lines = [];
4091
+ lines.push(`Carousel template "${result.brandName || args.brand_name}" has been saved successfully.`);
4092
+ lines.push("");
4093
+ const updated = ["brand name"];
4094
+ if (args.color_primary) updated.push("primary color");
4095
+ if (args.color_secondary) updated.push("secondary color");
4096
+ if (args.color_accent) updated.push("accent color");
4097
+ if (args.cta_text) updated.push("CTA text");
4098
+ if (args.logo_placement) updated.push("logo placement");
4099
+ lines.push(`**Updated:** ${updated.join(", ")}`);
4100
+ return {
4101
+ content: [{ type: "text", text: lines.join("\n") }]
4102
+ };
4103
+ }
4104
+ );
4105
+ server2.tool(
4106
+ "generate_carousel",
4107
+ "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.",
4108
+ {
4109
+ url: z16.string().url().describe("The article URL to generate a carousel from")
4110
+ },
4111
+ {
4112
+ title: "Generate Carousel",
4113
+ readOnlyHint: false,
4114
+ destructiveHint: false,
4115
+ idempotentHint: false,
4116
+ openWorldHint: true
4117
+ },
4118
+ async (args) => {
4119
+ try {
4120
+ const result = await client2.generateCarousel({
4121
+ url: args.url
4122
+ });
4123
+ const lines = [];
4124
+ lines.push("## Carousel Generated");
4125
+ lines.push("");
4126
+ lines.push(`**Headline:** ${result.article.headline}`);
4127
+ lines.push(`**Source:** ${result.article.sourceDomain}`);
4128
+ lines.push(`**Slides:** ${result.slides.length}`);
4129
+ lines.push("");
4130
+ lines.push("### Key Points");
4131
+ for (const point of result.article.keyPoints) {
4132
+ lines.push(`- ${point}`);
4133
+ }
4134
+ lines.push("");
4135
+ lines.push("### Slide URLs");
4136
+ for (const slide of result.slides) {
4137
+ lines.push(`${slide.slideNumber}. [${slide.type}](${slide.cdnUrl})`);
4138
+ }
4139
+ lines.push("");
4140
+ lines.push("These URLs can be used as `media_urls` when creating a post.");
4141
+ return {
4142
+ content: [{ type: "text", text: lines.join("\n") }]
4143
+ };
4144
+ } catch (error) {
4145
+ const message = error instanceof Error ? error.message : "Unknown error";
4146
+ return {
4147
+ content: [
4148
+ {
4149
+ type: "text",
4150
+ text: `Failed to generate carousel: ${message}`
4151
+ }
4152
+ ],
4153
+ isError: true
4154
+ };
4155
+ }
4156
+ }
4157
+ );
4158
+ server2.tool(
4159
+ "preview_carousel",
4160
+ "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.",
4161
+ {
4162
+ url: z16.string().url().describe("The article URL to preview")
4163
+ },
4164
+ {
4165
+ title: "Preview Carousel",
4166
+ readOnlyHint: true,
4167
+ destructiveHint: false,
4168
+ idempotentHint: false,
4169
+ openWorldHint: true
4170
+ },
4171
+ async (args) => {
4172
+ try {
4173
+ const result = await client2.previewCarousel({
4174
+ url: args.url
4175
+ });
4176
+ const lines = [];
4177
+ lines.push("## Carousel Preview");
4178
+ lines.push("");
4179
+ lines.push(`**Headline:** ${result.article.headline}`);
4180
+ lines.push(`**Source:** ${result.article.sourceDomain}`);
4181
+ lines.push("");
4182
+ lines.push(`**Hero Slide:** ${result.cdnUrl}`);
4183
+ lines.push("");
4184
+ lines.push("Use `generate_carousel` to create the full carousel with all slides.");
4185
+ return {
4186
+ content: [{ type: "text", text: lines.join("\n") }]
4187
+ };
4188
+ } catch (error) {
4189
+ const message = error instanceof Error ? error.message : "Unknown error";
4190
+ return {
4191
+ content: [
4192
+ {
4193
+ type: "text",
4194
+ text: `Failed to preview carousel: ${message}`
4195
+ }
4196
+ ],
4197
+ isError: true
4198
+ };
4199
+ }
4200
+ }
4201
+ );
4202
+ }
4203
+
4204
+ // src/index.ts
4205
+ var apiKey = process.env.BUZZPOSTER_API_KEY;
4206
+ var apiUrl = process.env.BUZZPOSTER_API_URL ?? "https://api.buzzposter.com";
4207
+ if (!apiKey) {
4208
+ console.error(
4209
+ "Error: BUZZPOSTER_API_KEY environment variable is required."
4210
+ );
4211
+ console.error(
4212
+ "Set it in your Claude Desktop MCP config or export it in your shell."
4213
+ );
4214
+ process.exit(1);
4215
+ }
4216
+ var client = new BuzzPosterClient({ baseUrl: apiUrl, apiKey });
4217
+ var server = new McpServer({
4218
+ name: "buzzposter",
4219
+ version: "0.1.0"
4220
+ });
4221
+ var allowDirectSend = false;
4222
+ try {
4223
+ const account = await client.getAccount();
4224
+ allowDirectSend = account?.allowDirectSend === true;
4225
+ } catch {
4226
+ }
2135
4227
  registerPostTools(server, client);
2136
4228
  registerAccountTools(server, client);
2137
4229
  registerAnalyticsTools(server, client);
2138
4230
  registerInboxTools(server, client);
2139
4231
  registerMediaTools(server, client);
2140
- registerNewsletterTools(server, client);
4232
+ registerNewsletterTools(server, client, { allowDirectSend });
2141
4233
  registerRssTools(server, client);
2142
4234
  registerAccountInfoTool(server, client);
2143
4235
  registerBrandVoiceTools(server, client);
@@ -2146,5 +4238,10 @@ registerAudienceTools(server, client);
2146
4238
  registerNewsletterTemplateTools(server, client);
2147
4239
  registerCalendarTools(server, client);
2148
4240
  registerNotificationTools(server, client);
4241
+ registerPublishingRulesTools(server, client);
4242
+ registerAuditLogTools(server, client);
4243
+ registerSourceTools(server, client);
4244
+ registerNewsletterAdvancedTools(server, client);
4245
+ registerCarouselTools(server, client);
2149
4246
  var transport = new StdioServerTransport();
2150
4247
  await server.connect(transport);