@buzzposter/mcp 0.1.13 → 0.1.15

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
@@ -65,9 +65,6 @@ var BuzzPosterClient = class {
65
65
  async deletePost(id) {
66
66
  return this.request("DELETE", `/api/v1/posts/${id}`);
67
67
  }
68
- async retryPost(id) {
69
- return this.request("POST", `/api/v1/posts/${id}/retry`);
70
- }
71
68
  // Analytics
72
69
  async getAnalytics(params) {
73
70
  return this.request("GET", "/api/v1/analytics", void 0, params);
@@ -101,32 +98,9 @@ var BuzzPosterClient = class {
101
98
  });
102
99
  }
103
100
  // Media
104
- async uploadMediaMultipart(filename, buffer, mimeType) {
105
- const formData = new FormData();
106
- formData.append("file", new Blob([buffer], { type: mimeType }), filename);
107
- const url = new URL(`${this.baseUrl}/api/v1/media/upload`);
108
- const res = await fetch(url, {
109
- method: "POST",
110
- headers: { Authorization: `Bearer ${this.apiKey}` },
111
- body: formData
112
- });
113
- if (!res.ok) {
114
- const errorBody = await res.json().catch(() => ({ message: res.statusText }));
115
- const message = typeof errorBody === "object" && errorBody !== null && "message" in errorBody ? String(errorBody.message) : `API error (${res.status})`;
116
- throw new Error(`BuzzPoster API error (${res.status}): ${message}`);
117
- }
118
- const text = await res.text();
119
- return text ? JSON.parse(text) : void 0;
120
- }
121
101
  async uploadFromUrl(data) {
122
102
  return this.request("POST", "/api/v1/media/upload-from-url", data);
123
103
  }
124
- async uploadBase64(data) {
125
- return this.request("POST", "/api/v1/media/upload-base64", data);
126
- }
127
- async getUploadUrl(data) {
128
- return this.request("POST", "/api/v1/media/presign", data);
129
- }
130
104
  async listMedia() {
131
105
  return this.request("GET", "/api/v1/media");
132
106
  }
@@ -165,9 +139,6 @@ var BuzzPosterClient = class {
165
139
  async listTags() {
166
140
  return this.request("GET", "/api/v1/newsletters/tags");
167
141
  }
168
- async listSequences() {
169
- return this.request("GET", "/api/v1/newsletters/sequences");
170
- }
171
142
  async listForms() {
172
143
  return this.request("GET", "/api/v1/newsletters/forms");
173
144
  }
@@ -178,36 +149,15 @@ var BuzzPosterClient = class {
178
149
  async listAutomations(params) {
179
150
  return this.request("GET", "/api/v1/newsletters/automations", void 0, params);
180
151
  }
181
- async enrollInAutomation(automationId, data) {
182
- return this.request("POST", `/api/v1/newsletters/automations/${automationId}/enroll`, data);
183
- }
184
152
  async listSegments(params) {
185
153
  return this.request("GET", "/api/v1/newsletters/segments", void 0, params);
186
154
  }
187
155
  async getSegment(id, params) {
188
156
  return this.request("GET", `/api/v1/newsletters/segments/${id}`, void 0, params);
189
157
  }
190
- async getSegmentMembers(segmentId, params) {
191
- return this.request("GET", `/api/v1/newsletters/segments/${segmentId}/members`, void 0, params);
192
- }
193
- async deleteSegment(id) {
194
- return this.request("DELETE", `/api/v1/newsletters/segments/${id}`);
195
- }
196
- async recalculateSegment(id) {
197
- return this.request("POST", `/api/v1/newsletters/segments/${id}/recalculate`);
198
- }
199
158
  async listCustomFields() {
200
159
  return this.request("GET", "/api/v1/newsletters/custom-fields");
201
160
  }
202
- async createCustomField(data) {
203
- return this.request("POST", "/api/v1/newsletters/custom-fields", data);
204
- }
205
- async updateCustomField(id, data) {
206
- return this.request("PUT", `/api/v1/newsletters/custom-fields/${id}`, data);
207
- }
208
- async deleteCustomField(id) {
209
- return this.request("DELETE", `/api/v1/newsletters/custom-fields/${id}`);
210
- }
211
161
  async tagSubscriber(subscriberId, data) {
212
162
  return this.request("POST", `/api/v1/newsletters/subscribers/${subscriberId}/tags`, data);
213
163
  }
@@ -217,33 +167,12 @@ var BuzzPosterClient = class {
217
167
  async deleteSubscriber(id) {
218
168
  return this.request("DELETE", `/api/v1/newsletters/subscribers/${id}`);
219
169
  }
220
- async bulkCreateSubscribers(data) {
221
- return this.request("POST", "/api/v1/newsletters/subscribers/bulk", data);
222
- }
223
- async getReferralProgram() {
224
- return this.request("GET", "/api/v1/newsletters/referral-program");
225
- }
226
- async listEspTiers() {
227
- return this.request("GET", "/api/v1/newsletters/tiers");
228
- }
229
170
  async listPostTemplates() {
230
171
  return this.request("GET", "/api/v1/newsletters/post-templates");
231
172
  }
232
173
  async getPostAggregateStats() {
233
174
  return this.request("GET", "/api/v1/newsletters/broadcasts/aggregate-stats");
234
175
  }
235
- async deleteBroadcast(id) {
236
- return this.request("DELETE", `/api/v1/newsletters/broadcasts/${id}`);
237
- }
238
- async listEspWebhooks() {
239
- return this.request("GET", "/api/v1/newsletters/webhooks");
240
- }
241
- async createEspWebhook(data) {
242
- return this.request("POST", "/api/v1/newsletters/webhooks", data);
243
- }
244
- async deleteEspWebhook(id) {
245
- return this.request("DELETE", `/api/v1/newsletters/webhooks/${id}`);
246
- }
247
176
  // Knowledge
248
177
  async listKnowledge(params) {
249
178
  return this.request("GET", "/api/v1/knowledge", void 0, params);
@@ -279,6 +208,18 @@ var BuzzPosterClient = class {
279
208
  async listTemplates() {
280
209
  return this.request("GET", "/api/v1/templates");
281
210
  }
211
+ async createTemplate(data) {
212
+ return this.request("POST", "/api/v1/templates", data);
213
+ }
214
+ async updateTemplate(id, data) {
215
+ return this.request("PUT", "/api/v1/templates/" + id, data);
216
+ }
217
+ async deleteTemplate(id) {
218
+ return this.request("DELETE", "/api/v1/templates/" + id);
219
+ }
220
+ async duplicateTemplate(id) {
221
+ return this.request("POST", "/api/v1/templates/" + id + "/duplicate");
222
+ }
282
223
  // Newsletter Archive
283
224
  async listNewsletterArchive(params) {
284
225
  return this.request("GET", "/api/v1/newsletter-archive", void 0, params);
@@ -350,10 +291,6 @@ var BuzzPosterClient = class {
350
291
  async getPublishingRules() {
351
292
  return this.request("GET", "/api/v1/publishing-rules");
352
293
  }
353
- // Audit Log
354
- async getAuditLog(params) {
355
- return this.request("GET", "/api/v1/audit-log", void 0, params);
356
- }
357
294
  // Newsletter Validation
358
295
  async validateNewsletter(data) {
359
296
  return this.request("POST", "/api/v1/newsletters/validate", data);
@@ -366,22 +303,6 @@ var BuzzPosterClient = class {
366
303
  async testBroadcast(id, data) {
367
304
  return this.request("POST", `/api/v1/newsletters/broadcasts/${id}/test`, data);
368
305
  }
369
- // Carousel templates
370
- async getCarouselTemplate() {
371
- return this.request("GET", "/api/v1/carousel-templates");
372
- }
373
- async updateCarouselTemplate(data) {
374
- return this.request("PUT", "/api/v1/carousel-templates", data);
375
- }
376
- async deleteCarouselTemplate() {
377
- return this.request("DELETE", "/api/v1/carousel-templates");
378
- }
379
- async generateCarousel(data) {
380
- return this.request("POST", "/api/v1/carousel-templates/generate", data);
381
- }
382
- async previewCarousel(data) {
383
- return this.request("POST", "/api/v1/carousel-templates/preview", data);
384
- }
385
306
  };
386
307
 
387
308
  // src/tools/posts.ts
@@ -389,23 +310,7 @@ import { z } from "zod";
389
310
  function registerPostTools(server2, client2) {
390
311
  server2.tool(
391
312
  "post",
392
- `Create and publish a post to one or more social media platforms. Supports Twitter, Instagram, LinkedIn, and Facebook.
393
-
394
- 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.
395
-
396
- REQUIRED WORKFLOW:
397
- 1. BEFORE creating any content, call get_brand_voice and get_audience to match the customer's tone
398
- 2. BEFORE publishing, call get_publishing_rules to check safety settings
399
- 3. ALWAYS set confirmed=false first to get a preview -- never set confirmed=true on the first call
400
- 4. Show the user a visual preview of the post before confirming
401
- 5. Only set confirmed=true after the user explicitly says to publish/post/send
402
- 6. If publishing_rules.social_default_action is 'draft', create as draft unless the user explicitly asks to publish
403
- 7. If publishing_rules.require_double_confirm_social is true, ask for confirmation twice before publishing
404
- 8. NEVER publish immediately without user confirmation, even if the user says "post this" -- always show the preview first
405
-
406
- 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.
407
-
408
- 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.`,
313
+ "Publish a post to one or more social platforms. Set confirmed=false to preview first.",
409
314
  {
410
315
  content: z.string().optional().describe("The text content of the post"),
411
316
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -497,118 +402,9 @@ Call this tool again with confirmed=true to proceed.`;
497
402
  };
498
403
  }
499
404
  );
500
- server2.tool(
501
- "cross_post",
502
- `Post the same content to all connected platforms at once.
503
-
504
- 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.
505
-
506
- REQUIRED WORKFLOW:
507
- 1. BEFORE creating content, call get_brand_voice and get_audience
508
- 2. BEFORE publishing, call get_publishing_rules
509
- 3. ALWAYS set confirmed=false first to preview
510
- 4. Show the user a visual preview showing how the post will appear on each platform
511
- 5. This is a high-impact action (goes to ALL platforms) -- always require explicit confirmation
512
- 6. If publishing_rules.require_double_confirm_social is true, ask twice
513
-
514
- 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.`,
515
- {
516
- content: z.string().describe("The text content to post everywhere"),
517
- media_urls: z.array(z.string()).optional().describe("Public URLs of media to attach"),
518
- confirmed: z.boolean().default(false).describe(
519
- "Set to true to confirm and publish. If false or missing, returns a preview for user approval."
520
- )
521
- },
522
- {
523
- title: "Cross-Post to All Platforms",
524
- readOnlyHint: false,
525
- destructiveHint: false,
526
- idempotentHint: false,
527
- openWorldHint: true
528
- },
529
- async (args) => {
530
- const accountsData = await client2.listAccounts();
531
- const platforms = (accountsData.accounts ?? []).map(
532
- (a) => a.platform
533
- );
534
- if (platforms.length === 0) {
535
- return {
536
- content: [
537
- {
538
- type: "text",
539
- text: "No connected social accounts found. Connect accounts first."
540
- }
541
- ]
542
- };
543
- }
544
- if (args.confirmed !== true) {
545
- let rulesText = "";
546
- try {
547
- const rules = await client2.getPublishingRules();
548
- const blockedWords = rules.blockedWords ?? [];
549
- const foundBlocked = [];
550
- if (args.content && blockedWords.length > 0) {
551
- const lower = args.content.toLowerCase();
552
- for (const word of blockedWords) {
553
- if (lower.includes(word.toLowerCase())) foundBlocked.push(word);
554
- }
555
- }
556
- rulesText = `
557
- ### Safety Checks
558
- `;
559
- rulesText += `- Blocked words: ${foundBlocked.length > 0 ? `**FAILED** (found: ${foundBlocked.join(", ")})` : "PASS"}
560
- `;
561
- rulesText += `- Default action: ${rules.socialDefaultAction ?? "draft"}
562
- `;
563
- rulesText += `- Double confirmation required: ${rules.requireDoubleConfirmSocial ? "yes" : "no"}
564
- `;
565
- } catch {
566
- }
567
- const preview = `## Cross-Post Preview
568
-
569
- **Content:** "${args.content}"
570
- **Platforms:** ${platforms.join(", ")} (${platforms.length} platforms)
571
- ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
572
- ` : "") + rulesText + `
573
- **Action:** This will be published **immediately** to **all ${platforms.length} connected platforms**. This is a high-impact action.
574
-
575
- Call this tool again with confirmed=true to proceed.`;
576
- return { content: [{ type: "text", text: preview }] };
577
- }
578
- const body = {
579
- content: args.content,
580
- platforms: platforms.map((p) => ({ platform: p })),
581
- publishNow: true
582
- };
583
- if (args.media_urls?.length) {
584
- body.mediaItems = args.media_urls.map((url) => ({
585
- type: url.match(/\.(mp4|mov|avi|webm)$/i) ? "video" : "image",
586
- url
587
- }));
588
- }
589
- const result = await client2.createPost(body);
590
- return {
591
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
592
- };
593
- }
594
- );
595
405
  server2.tool(
596
406
  "schedule_post",
597
- `Schedule a post for future publication.
598
-
599
- 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.
600
-
601
- REQUIRED WORKFLOW:
602
- 1. BEFORE creating content, call get_brand_voice and get_audience
603
- 2. BEFORE scheduling, call get_publishing_rules
604
- 3. ALWAYS set confirmed=false first to preview
605
- 4. Show the user a visual preview with the scheduled date/time before confirming
606
- 5. Only confirm after explicit user approval
607
- 6. If the user hasn't specified a time, suggest one based on get_queue time slots
608
-
609
- 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.
610
-
611
- 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.`,
407
+ "Schedule a post for future publication. Set confirmed=false to preview first.",
612
408
  {
613
409
  content: z.string().optional().describe("The text content of the post"),
614
410
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -684,7 +480,7 @@ Call this tool again with confirmed=true to schedule.`;
684
480
  );
685
481
  server2.tool(
686
482
  "create_draft",
687
- "Save a post as a draft without publishing. This NEVER publishes \u2014 it only saves. No confirmation needed since nothing goes live.",
483
+ "Save a post as a draft without publishing. No confirmation needed.",
688
484
  {
689
485
  content: z.string().optional().describe("The text content of the draft"),
690
486
  platforms: z.array(z.string()).describe("Platforms this draft is intended for"),
@@ -727,7 +523,7 @@ Call this tool again with confirmed=true to schedule.`;
727
523
  );
728
524
  server2.tool(
729
525
  "list_posts",
730
- "List recent posts with their status and platform details.",
526
+ "List recent posts with status and platform details.",
731
527
  {
732
528
  status: z.string().optional().describe("Filter by status: published, scheduled, draft, failed"),
733
529
  limit: z.string().optional().describe("Number of posts to return")
@@ -751,7 +547,7 @@ Call this tool again with confirmed=true to schedule.`;
751
547
  );
752
548
  server2.tool(
753
549
  "get_post",
754
- "Get detailed information about a specific post.",
550
+ "Get detailed information about a specific post by ID.",
755
551
  {
756
552
  post_id: z.string().describe("The ID of the post to retrieve")
757
553
  },
@@ -769,117 +565,13 @@ Call this tool again with confirmed=true to schedule.`;
769
565
  };
770
566
  }
771
567
  );
772
- server2.tool(
773
- "retry_post",
774
- "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.",
775
- {
776
- post_id: z.string().describe("The ID of the post to retry"),
777
- confirmed: z.boolean().default(false).describe(
778
- "Set to true to confirm retry. If false/missing, shows post details first."
779
- )
780
- },
781
- {
782
- title: "Retry Failed Post",
783
- readOnlyHint: false,
784
- destructiveHint: false,
785
- idempotentHint: true,
786
- openWorldHint: true
787
- },
788
- async (args) => {
789
- if (args.confirmed !== true) {
790
- const post = await client2.getPost(args.post_id);
791
- const platforms = post.platforms ?? [];
792
- const failed = platforms.filter((p) => p.status === "failed");
793
- const published = platforms.filter((p) => p.status === "published");
794
- let text = `## Retry Post Preview
795
-
796
- `;
797
- text += `**Post ID:** ${args.post_id}
798
- `;
799
- text += `**Content:** "${(post.content ?? "").substring(0, 100)}"
800
- `;
801
- text += `**Status:** ${post.status}
802
-
803
- `;
804
- if (published.length > 0) {
805
- text += `**Already published on:** ${published.map((p) => p.platform).join(", ")}
806
- `;
807
- }
808
- if (failed.length > 0) {
809
- text += `**Failed on:** ${failed.map((p) => `${p.platform} (${p.error ?? "unknown error"})`).join(", ")}
810
- `;
811
- }
812
- text += `
813
- Retrying will only attempt the failed platforms. Call this tool again with confirmed=true to proceed.`;
814
- return { content: [{ type: "text", text }] };
815
- }
816
- const result = await client2.retryPost(args.post_id);
817
- return {
818
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
819
- };
820
- }
821
- );
822
568
  }
823
569
 
824
570
  // src/tools/accounts.ts
825
571
  function registerAccountTools(server2, client2) {
826
- server2.tool(
827
- "list_accounts",
828
- "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.",
829
- {},
830
- {
831
- title: "List Connected Accounts",
832
- readOnlyHint: true,
833
- destructiveHint: false,
834
- idempotentHint: true,
835
- openWorldHint: false
836
- },
837
- async () => {
838
- const result = await client2.listAccounts();
839
- const accounts = result.accounts ?? [];
840
- const esp = result.esp;
841
- if (accounts.length === 0 && !esp) {
842
- return {
843
- content: [{
844
- type: "text",
845
- text: "## Connected Accounts\n\nNo accounts connected. Connect social media accounts and/or an email service provider via the dashboard."
846
- }]
847
- };
848
- }
849
- let text = "## Connected Accounts\n\n";
850
- text += "### Social Media\n";
851
- if (accounts.length > 0) {
852
- for (const a of accounts) {
853
- const icon = a.isActive ? "\u2705" : "\u274C";
854
- const platform = a.platform.charAt(0).toUpperCase() + a.platform.slice(1);
855
- const name = a.username || a.displayName || a.platform;
856
- text += `${icon} **${platform}** \u2014 @${name}
857
- `;
858
- }
859
- } else {
860
- text += "No social accounts connected.\n";
861
- }
862
- text += "\n### Email Service Provider (ESP)\n";
863
- if (esp && esp.connected) {
864
- const provider = esp.provider.charAt(0).toUpperCase() + esp.provider.slice(1);
865
- text += `\u2705 **${provider}** \u2014 Connected`;
866
- if (esp.publicationId) text += ` (Publication: ${esp.publicationId})`;
867
- text += "\n";
868
- } else if (esp) {
869
- const provider = esp.provider.charAt(0).toUpperCase() + esp.provider.slice(1);
870
- text += `\u26A0\uFE0F **${provider}** \u2014 Provider set but API key missing
871
- `;
872
- } else {
873
- text += "No ESP configured.\n";
874
- }
875
- return {
876
- content: [{ type: "text", text }]
877
- };
878
- }
879
- );
880
572
  server2.tool(
881
573
  "check_accounts_health",
882
- "Check the health status of all connected social media accounts. Shows which accounts are working, which have warnings, and which need attention. Call this before scheduling posts to make sure target platforms are healthy. If a token is expiring or an account shows an error, direct the user to reconnect via the BuzzPoster web UI -- reconnection cannot be done through MCP.",
574
+ "Check health status of connected social accounts. Direct user to BuzzPoster web UI to reconnect broken accounts.",
883
575
  {},
884
576
  {
885
577
  title: "Check Account Health",
@@ -932,7 +624,7 @@ import { z as z2 } from "zod";
932
624
  function registerAnalyticsTools(server2, client2) {
933
625
  server2.tool(
934
626
  "get_analytics",
935
- "Get performance analytics for your social media posts. Supports filtering by platform and date range.",
627
+ "Get social media post performance analytics. Filter by platform and date range.",
936
628
  {
937
629
  platform: z2.string().optional().describe("Filter by platform: twitter, instagram, linkedin, facebook"),
938
630
  from_date: z2.string().optional().describe("Start date for analytics range (ISO 8601)"),
@@ -963,7 +655,7 @@ import { z as z3 } from "zod";
963
655
  function registerInboxTools(server2, client2) {
964
656
  server2.tool(
965
657
  "list_conversations",
966
- "List DM conversations across all connected social media platforms.",
658
+ "List DM conversations across all connected platforms.",
967
659
  {
968
660
  platform: z3.string().optional().describe("Filter by platform: twitter, instagram, linkedin, facebook")
969
661
  },
@@ -1005,12 +697,7 @@ function registerInboxTools(server2, client2) {
1005
697
  );
1006
698
  server2.tool(
1007
699
  "reply_to_conversation",
1008
- `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.
1009
-
1010
- REQUIRED WORKFLOW:
1011
- 1. ALWAYS set confirmed=false first to preview the reply
1012
- 2. Show the user the reply text and which platform/conversation it will be sent to
1013
- 3. Only confirm after explicit user approval`,
700
+ "Send a DM reply. Set confirmed=false to preview first.",
1014
701
  {
1015
702
  conversation_id: z3.string().describe("The conversation ID to reply to"),
1016
703
  message: z3.string().describe("The reply message text"),
@@ -1066,12 +753,7 @@ This will send a DM reply on the external platform. Call this tool again with co
1066
753
  );
1067
754
  server2.tool(
1068
755
  "reply_to_comment",
1069
- `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.
1070
-
1071
- REQUIRED WORKFLOW:
1072
- 1. ALWAYS set confirmed=false first to preview
1073
- 2. Show the user the reply and the original comment for context
1074
- 3. Only confirm after explicit user approval -- public replies are visible to everyone`,
756
+ "Reply to a comment publicly. Set confirmed=false to preview first.",
1075
757
  {
1076
758
  comment_id: z3.string().describe("The comment ID to reply to"),
1077
759
  message: z3.string().describe("The reply text"),
@@ -1123,12 +805,7 @@ This will post a public reply. Call this tool again with confirmed=true to send.
1123
805
  );
1124
806
  server2.tool(
1125
807
  "reply_to_review",
1126
- `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.
1127
-
1128
- REQUIRED WORKFLOW:
1129
- 1. ALWAYS set confirmed=false first to preview
1130
- 2. Show the user the reply, the original review, and the star rating for context
1131
- 3. Only confirm after explicit user approval -- public replies represent the brand`,
808
+ "Reply to a Facebook review publicly. Set confirmed=false to preview first.",
1132
809
  {
1133
810
  review_id: z3.string().describe("The review ID to reply to"),
1134
811
  message: z3.string().describe("The reply text"),
@@ -1164,75 +841,10 @@ This will post a public reply to the review. Call this tool again with confirmed
1164
841
 
1165
842
  // src/tools/media.ts
1166
843
  import { z as z4 } from "zod";
1167
- import { readFile } from "fs/promises";
1168
844
  function registerMediaTools(server2, client2) {
1169
- server2.tool(
1170
- "upload_media",
1171
- "Upload an image or video file from a local file path. Returns a CDN URL that can be used in posts.",
1172
- {
1173
- file_path: z4.string().describe("Absolute path to the file on the local filesystem")
1174
- },
1175
- {
1176
- title: "Upload Media File",
1177
- readOnlyHint: false,
1178
- destructiveHint: false,
1179
- idempotentHint: false,
1180
- openWorldHint: false
1181
- },
1182
- async (args) => {
1183
- const buffer = Buffer.from(await readFile(args.file_path));
1184
- const filename = args.file_path.split("/").pop() ?? "upload";
1185
- const ext = filename.split(".").pop()?.toLowerCase() ?? "";
1186
- const mimeMap = {
1187
- jpg: "image/jpeg",
1188
- jpeg: "image/jpeg",
1189
- png: "image/png",
1190
- gif: "image/gif",
1191
- webp: "image/webp",
1192
- mp4: "video/mp4",
1193
- mov: "video/quicktime",
1194
- avi: "video/x-msvideo",
1195
- webm: "video/webm",
1196
- pdf: "application/pdf"
1197
- };
1198
- const mimeType = mimeMap[ext] ?? "application/octet-stream";
1199
- const result = await client2.uploadMediaMultipart(filename, buffer, mimeType);
1200
- return {
1201
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1202
- };
1203
- }
1204
- );
1205
- server2.tool(
1206
- "upload_from_claude",
1207
- "Upload an image that the user has dropped or pasted into Claude. When a user shares an image in the conversation, Claude can read it natively as base64. Pass the base64 data, filename, and content_type to upload it to the BuzzPoster media library and get a CDN URL back. Do NOT show the base64 data to the user -- just call this tool silently and return the CDN URL.",
1208
- {
1209
- data: z4.string().describe("Base64-encoded image data (no data URI prefix needed, just the raw base64 string)"),
1210
- filename: z4.string().describe("Filename including extension (e.g. photo.jpg)"),
1211
- content_type: z4.string().describe("MIME type (e.g. image/jpeg, image/png, image/webp, image/gif)"),
1212
- folder: z4.string().optional().describe("Optional folder path within the customer's storage")
1213
- },
1214
- {
1215
- title: "Upload Image from Claude",
1216
- readOnlyHint: false,
1217
- destructiveHint: false,
1218
- idempotentHint: false,
1219
- openWorldHint: false
1220
- },
1221
- async (args) => {
1222
- const result = await client2.uploadBase64({
1223
- data: args.data,
1224
- filename: args.filename,
1225
- content_type: args.content_type,
1226
- folder: args.folder
1227
- });
1228
- return {
1229
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1230
- };
1231
- }
1232
- );
1233
845
  server2.tool(
1234
846
  "upload_from_url",
1235
- "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.",
847
+ "Upload media from a public URL to storage. Returns a CDN URL for use in posts.",
1236
848
  {
1237
849
  url: z4.string().url().describe("Public URL of the image or video to upload"),
1238
850
  filename: z4.string().optional().describe("Optional filename override (including extension)"),
@@ -1251,38 +863,18 @@ function registerMediaTools(server2, client2) {
1251
863
  filename: args.filename,
1252
864
  folder: args.folder
1253
865
  });
866
+ const item = result;
1254
867
  return {
1255
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1256
- };
1257
- }
1258
- );
1259
- server2.tool(
1260
- "get_upload_url",
1261
- "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.",
1262
- {
1263
- filename: z4.string().describe("The filename including extension (e.g. photo.jpg)"),
1264
- content_type: z4.string().describe("MIME type of the file (e.g. image/jpeg, video/mp4)")
1265
- },
1266
- {
1267
- title: "Get Upload URL",
1268
- readOnlyHint: true,
1269
- destructiveHint: false,
1270
- idempotentHint: false,
1271
- openWorldHint: false
1272
- },
1273
- async (args) => {
1274
- const result = await client2.getUploadUrl({
1275
- filename: args.filename,
1276
- content_type: args.content_type
1277
- });
1278
- return {
1279
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
868
+ content: [
869
+ { type: "text", text: JSON.stringify(result, null, 2) },
870
+ ...item.type === "image" && item.url && item.mimeType && !item.mimeType.startsWith("video/") ? [{ type: "image", url: item.url, mimeType: item.mimeType }] : []
871
+ ]
1280
872
  };
1281
873
  }
1282
874
  );
1283
875
  server2.tool(
1284
876
  "list_media",
1285
- "List all uploaded media files in your BuzzPoster media library.",
877
+ "List all uploaded media files.",
1286
878
  {},
1287
879
  {
1288
880
  title: "List Media Library",
@@ -1293,19 +885,19 @@ function registerMediaTools(server2, client2) {
1293
885
  },
1294
886
  async () => {
1295
887
  const result = await client2.listMedia();
888
+ const media = Array.isArray(result) ? result : result?.media ?? [];
889
+ const imageBlocks = media.filter((m) => m.type === "image" && m.url && m.mimeType && !m.mimeType.startsWith("video/")).map((m) => ({ type: "image", url: m.url, mimeType: m.mimeType }));
1296
890
  return {
1297
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
891
+ content: [
892
+ { type: "text", text: JSON.stringify(result, null, 2) },
893
+ ...imageBlocks
894
+ ]
1298
895
  };
1299
896
  }
1300
897
  );
1301
898
  server2.tool(
1302
899
  "delete_media",
1303
- `Delete a media file from the customer's BuzzPoster media library. This cannot be undone.
1304
-
1305
- REQUIRED WORKFLOW:
1306
- 1. ALWAYS set confirmed=false first
1307
- 2. Show the user which file will be deleted (filename, URL, size)
1308
- 3. Only confirm after explicit approval`,
900
+ "Delete a media file. Set confirmed=false to preview first.",
1309
901
  {
1310
902
  key: z4.string().describe("The key/path of the media file to delete"),
1311
903
  confirmed: z4.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
@@ -1339,7 +931,7 @@ import { z as z5 } from "zod";
1339
931
  function registerNewsletterTools(server2, client2, options = {}) {
1340
932
  server2.tool(
1341
933
  "list_subscribers",
1342
- "List email subscribers from your configured email service provider (Kit, Beehiiv, or Mailchimp).",
934
+ "List email subscribers from the connected ESP.",
1343
935
  {
1344
936
  page: z5.string().optional().describe("Page number for pagination"),
1345
937
  per_page: z5.string().optional().describe("Number of subscribers per page")
@@ -1363,7 +955,7 @@ function registerNewsletterTools(server2, client2, options = {}) {
1363
955
  );
1364
956
  server2.tool(
1365
957
  "add_subscriber",
1366
- "Add a new email subscriber to your mailing list.",
958
+ "Add a new subscriber to the mailing list.",
1367
959
  {
1368
960
  email: z5.string().describe("Email address of the subscriber"),
1369
961
  first_name: z5.string().optional().describe("Subscriber's first name"),
@@ -1389,7 +981,7 @@ function registerNewsletterTools(server2, client2, options = {}) {
1389
981
  );
1390
982
  server2.tool(
1391
983
  "create_newsletter",
1392
- `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.`,
984
+ "Push a newsletter to the user's ESP as a draft. Show an HTML preview to the user before calling this.",
1393
985
  {
1394
986
  subject: z5.string().describe("Email subject line"),
1395
987
  content: z5.string().describe("HTML content of the newsletter"),
@@ -1409,13 +1001,17 @@ function registerNewsletterTools(server2, client2, options = {}) {
1409
1001
  previewText: args.preview_text
1410
1002
  });
1411
1003
  return {
1412
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1004
+ content: [
1005
+ { type: "text", text: JSON.stringify(result, null, 2) },
1006
+ ...args.content ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
1007
+ ${args.content}` }] : []
1008
+ ]
1413
1009
  };
1414
1010
  }
1415
1011
  );
1416
1012
  server2.tool(
1417
1013
  "update_newsletter",
1418
- `Update an existing newsletter/broadcast draft. After updating, show the user a visual preview of the changes.`,
1014
+ "Update an existing newsletter draft.",
1419
1015
  {
1420
1016
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to update"),
1421
1017
  subject: z5.string().optional().describe("Updated subject line"),
@@ -1436,27 +1032,18 @@ function registerNewsletterTools(server2, client2, options = {}) {
1436
1032
  if (args.preview_text) data.previewText = args.preview_text;
1437
1033
  const result = await client2.updateBroadcast(args.broadcast_id, data);
1438
1034
  return {
1439
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1035
+ content: [
1036
+ { type: "text", text: JSON.stringify(result, null, 2) },
1037
+ ...args.content ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
1038
+ ${args.content}` }] : []
1039
+ ]
1440
1040
  };
1441
1041
  }
1442
1042
  );
1443
1043
  if (options.allowDirectSend) {
1444
1044
  server2.tool(
1445
1045
  "send_newsletter",
1446
- `Send a newsletter/broadcast to subscribers. THIS ACTION CANNOT BE UNDONE. Once sent, the email goes to every subscriber on the list.
1447
-
1448
- REQUIRED WORKFLOW:
1449
- 1. ALWAYS call get_publishing_rules first
1450
- 2. NEVER send without showing a full visual preview of the newsletter first
1451
- 3. ALWAYS set confirmed=false first -- this returns a confirmation prompt, not a send
1452
- 4. Tell the user exactly how many subscribers will receive this email
1453
- 5. If publishing_rules.require_double_confirm_newsletter is true (default), require TWO explicit confirmations:
1454
- - First: "Are you sure you want to send this to [X] subscribers?"
1455
- - Second: "This is irreversible. Type 'send' to confirm."
1456
- 6. If publishing_rules.allow_immediate_send is false and the user asks to send immediately, suggest scheduling instead
1457
- 7. NEVER set confirmed=true without the user explicitly confirming after seeing the preview and subscriber count
1458
-
1459
- 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.`,
1046
+ "Send a newsletter to subscribers. This action cannot be undone. Set confirmed=false to preview first.",
1460
1047
  {
1461
1048
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to send"),
1462
1049
  confirmed: z5.boolean().default(false).describe(
@@ -1520,22 +1107,7 @@ Call this tool again with confirmed=true to send.`;
1520
1107
  }
1521
1108
  server2.tool(
1522
1109
  "schedule_newsletter",
1523
- `Schedule a newsletter/broadcast to be sent at a future time.
1524
-
1525
- REQUIRED WORKFLOW:
1526
- 1. ALWAYS call get_publishing_rules first
1527
- 2. NEVER schedule without showing a full visual preview of the newsletter first
1528
- 3. ALWAYS set confirmed=false first -- this returns a confirmation prompt with details, not an actual schedule
1529
- 4. Tell the user exactly how many subscribers will receive this email and the scheduled send time
1530
- 5. If publishing_rules.require_double_confirm_newsletter is true (default), require TWO explicit confirmations:
1531
- - First: "Are you sure you want to schedule this for [time] to [X] subscribers?"
1532
- - Second: "Confirm schedule for [time]."
1533
- 6. If publishing_rules.allow_immediate_send is false and the scheduled time is very soon, warn the user
1534
- 7. NEVER set confirmed=true without the user explicitly confirming after seeing the preview, subscriber count, and scheduled time
1535
-
1536
- Scheduling can typically be cancelled or rescheduled later, but the user should still verify the time is correct before confirming.
1537
-
1538
- 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.`,
1110
+ "Schedule a newsletter for future send. Set confirmed=false to preview first.",
1539
1111
  {
1540
1112
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to schedule"),
1541
1113
  scheduled_for: z5.string().describe(
@@ -1611,9 +1183,13 @@ Call this tool again with confirmed=true to schedule.`;
1611
1183
  );
1612
1184
  server2.tool(
1613
1185
  "list_newsletters",
1614
- "List all newsletters/broadcasts with their status.",
1186
+ "List newsletters with status, dates, and subject. Supports filtering by status, date range, and keyword.",
1615
1187
  {
1616
- page: z5.string().optional().describe("Page number for pagination")
1188
+ page: z5.string().optional().describe("Page number for pagination"),
1189
+ status: z5.enum(["draft", "sent", "scheduled"]).optional().describe("Filter by status: draft, sent, or scheduled"),
1190
+ from_date: z5.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
1191
+ to_date: z5.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
1192
+ subject: z5.string().optional().describe("Keyword to search in subject lines (case-insensitive)")
1617
1193
  },
1618
1194
  {
1619
1195
  title: "List Newsletters",
@@ -1625,15 +1201,37 @@ Call this tool again with confirmed=true to schedule.`;
1625
1201
  async (args) => {
1626
1202
  const params = {};
1627
1203
  if (args.page) params.page = args.page;
1204
+ if (args.status) params.status = args.status;
1205
+ if (args.from_date) params.fromDate = args.from_date;
1206
+ if (args.to_date) params.toDate = args.to_date;
1207
+ if (args.subject) params.subject = args.subject;
1628
1208
  const result = await client2.listBroadcasts(params);
1629
- return {
1630
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1209
+ const broadcasts = result?.broadcasts ?? [];
1210
+ if (broadcasts.length === 0) {
1211
+ return {
1212
+ content: [{ type: "text", text: "No newsletters found matching your filters." }]
1213
+ };
1214
+ }
1215
+ let text = `## Newsletters (${broadcasts.length}`;
1216
+ if (result?.totalCount != null) text += ` of ${result.totalCount}`;
1217
+ text += ")\n\n";
1218
+ for (const b of broadcasts) {
1219
+ const status = (b.status ?? "unknown").toUpperCase();
1220
+ const date = b.sentAt ? new Date(b.sentAt).toLocaleString() : b.createdAt ? new Date(b.createdAt).toLocaleString() : "";
1221
+ text += `- **[${status}]** "${b.subject ?? "(no subject)"}"
1222
+ `;
1223
+ text += ` ID: ${b.id}`;
1224
+ if (date) text += ` | ${b.sentAt ? "Sent" : "Created"}: ${date}`;
1225
+ text += "\n";
1226
+ }
1227
+ return {
1228
+ content: [{ type: "text", text }]
1631
1229
  };
1632
1230
  }
1633
1231
  );
1634
1232
  server2.tool(
1635
1233
  "list_tags",
1636
- "List all subscriber tags from your email service provider.",
1234
+ "List all subscriber tags.",
1637
1235
  {},
1638
1236
  {
1639
1237
  title: "List Subscriber Tags",
@@ -1649,27 +1247,9 @@ Call this tool again with confirmed=true to schedule.`;
1649
1247
  };
1650
1248
  }
1651
1249
  );
1652
- server2.tool(
1653
- "list_sequences",
1654
- "List all email sequences/automations from your email service provider. On Beehiiv, this returns automations. For detailed automation data use list_automations instead.",
1655
- {},
1656
- {
1657
- title: "List Email Sequences",
1658
- readOnlyHint: true,
1659
- destructiveHint: false,
1660
- idempotentHint: true,
1661
- openWorldHint: true
1662
- },
1663
- async () => {
1664
- const result = await client2.listSequences();
1665
- return {
1666
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1667
- };
1668
- }
1669
- );
1670
1250
  server2.tool(
1671
1251
  "list_forms",
1672
- "List all signup forms from your email service provider.",
1252
+ "List all signup forms.",
1673
1253
  {},
1674
1254
  {
1675
1255
  title: "List Signup Forms",
@@ -1692,7 +1272,7 @@ import { z as z6 } from "zod";
1692
1272
  function registerRssTools(server2, client2) {
1693
1273
  server2.tool(
1694
1274
  "fetch_feed",
1695
- "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.",
1275
+ "Fetch and parse entries from an RSS or Atom feed URL.",
1696
1276
  {
1697
1277
  url: z6.string().describe("The RSS/Atom feed URL to fetch"),
1698
1278
  limit: z6.number().optional().describe("Maximum number of entries to return (default 10, max 100)")
@@ -1713,7 +1293,7 @@ function registerRssTools(server2, client2) {
1713
1293
  );
1714
1294
  server2.tool(
1715
1295
  "fetch_article",
1716
- "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.",
1296
+ "Extract full article content from a URL as clean text.",
1717
1297
  {
1718
1298
  url: z6.string().describe("The article URL to extract content from")
1719
1299
  },
@@ -1737,7 +1317,7 @@ function registerRssTools(server2, client2) {
1737
1317
  function registerAccountInfoTool(server2, client2) {
1738
1318
  server2.tool(
1739
1319
  "get_account",
1740
- "Get your BuzzPoster account details including name, email, subscription status, ESP configuration, and connected platforms.",
1320
+ "Get account details, subscription status, ESP config, and all connected social/ESP accounts.",
1741
1321
  {},
1742
1322
  {
1743
1323
  title: "Get Account Details",
@@ -1747,9 +1327,15 @@ function registerAccountInfoTool(server2, client2) {
1747
1327
  openWorldHint: false
1748
1328
  },
1749
1329
  async () => {
1750
- const result = await client2.getAccount();
1330
+ const [account, accountsData] = await Promise.all([
1331
+ client2.getAccount(),
1332
+ client2.listAccounts()
1333
+ ]);
1751
1334
  return {
1752
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1335
+ content: [{
1336
+ type: "text",
1337
+ text: JSON.stringify({ account, connectedAccounts: accountsData }, null, 2)
1338
+ }]
1753
1339
  };
1754
1340
  }
1755
1341
  );
@@ -1760,7 +1346,7 @@ import { z as z7 } from "zod";
1760
1346
  function registerBrandVoiceTools(server2, client2) {
1761
1347
  server2.tool(
1762
1348
  "get_brand_voice",
1763
- "Get the customer's brand voice profile and writing rules. Returns: name, description, dos (array), donts (array), platformRules (per-platform object), and examplePosts (array). 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. This tool is read-only -- to update brand voice use update_brand_voice. Do NOT store brand voice data in the knowledge base. 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.",
1349
+ "Get the brand voice profile and writing rules. Call before creating content.",
1764
1350
  {},
1765
1351
  {
1766
1352
  title: "Get Brand Voice",
@@ -1835,21 +1421,14 @@ function registerBrandVoiceTools(server2, client2) {
1835
1421
  );
1836
1422
  server2.tool(
1837
1423
  "update_brand_voice",
1838
- `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.
1839
-
1840
- USAGE GUIDELINES:
1841
- - Call get_brand_voice first to see the current state before making changes
1842
- - Always set confirmed=false first to preview the changes, then confirmed=true after user approval
1843
- - When adding rules to dos/donts, include the FULL array (existing + new items), not just the new ones
1844
- - Platform rules use platform names as keys (e.g. twitter, linkedin, instagram)
1845
- - Max limits: name 80 chars, description 16000 chars, dos/donts 50 items each, examplePosts 8 items`,
1846
- {
1847
- name: z7.string().max(80).optional().describe("Brand voice profile name (e.g. 'BOQtoday Voice', 'Lightbreak Editorial')"),
1848
- description: z7.string().max(16e3).optional().describe("Overall voice description \u2014 tone, personality, style overview"),
1849
- dos: z7.array(z7.string()).max(50).optional().describe("Writing rules to follow (array of do's). Send the FULL list, not just additions."),
1850
- donts: z7.array(z7.string()).max(50).optional().describe("Things to avoid (array of don'ts). Send the FULL list, not just additions."),
1851
- 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" }'),
1852
- example_posts: z7.array(z7.string()).max(8).optional().describe("Example posts that demonstrate the desired voice (max 8)"),
1424
+ "Update the brand voice profile. Set confirmed=false to preview first.",
1425
+ {
1426
+ name: z7.string().max(80).optional().describe("Brand voice profile name"),
1427
+ description: z7.string().max(2e3).optional().describe("Overall voice description"),
1428
+ dos: z7.array(z7.string().max(200)).max(15).optional().describe("Writing rules to follow. Send the FULL list, not just additions."),
1429
+ donts: z7.array(z7.string().max(200)).max(15).optional().describe("Things to avoid. Send the FULL list, not just additions."),
1430
+ platform_rules: z7.record(z7.string().max(300)).optional().describe('Per-platform writing guidelines, e.g. { "twitter": "Keep under 200 chars" }'),
1431
+ example_posts: z7.array(z7.string().max(500)).max(5).optional().describe("Example posts that demonstrate the desired voice"),
1853
1432
  confirmed: z7.boolean().default(false).describe(
1854
1433
  "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
1855
1434
  )
@@ -1867,7 +1446,19 @@ USAGE GUIDELINES:
1867
1446
  if (args.description !== void 0) payload.description = args.description;
1868
1447
  if (args.dos !== void 0) payload.dos = args.dos;
1869
1448
  if (args.donts !== void 0) payload.donts = args.donts;
1870
- if (args.platform_rules !== void 0) payload.platformRules = args.platform_rules;
1449
+ if (args.platform_rules !== void 0) {
1450
+ const entries = Object.entries(args.platform_rules);
1451
+ if (entries.length > 5) {
1452
+ return {
1453
+ content: [{
1454
+ type: "text",
1455
+ text: "Maximum 5 platform rules allowed."
1456
+ }],
1457
+ isError: true
1458
+ };
1459
+ }
1460
+ payload.platformRules = args.platform_rules;
1461
+ }
1871
1462
  if (args.example_posts !== void 0) payload.examplePosts = args.example_posts;
1872
1463
  if (Object.keys(payload).length === 0) {
1873
1464
  return {
@@ -1955,7 +1546,7 @@ import { z as z8 } from "zod";
1955
1546
  function registerKnowledgeTools(server2, client2) {
1956
1547
  server2.tool(
1957
1548
  "get_knowledge_base",
1958
- "Get all items from the customer's knowledge base. Use this to access reference material about the business, products, team, competitors, and other context that helps you write better content. The knowledge base is ONLY for uploaded documents, URLs, and text references (company info, products, pricing, competitors, stats). Do NOT use this to store or retrieve brand voice profiles or audience definitions -- those have dedicated tools.",
1549
+ "Get all items from the knowledge base.",
1959
1550
  {},
1960
1551
  {
1961
1552
  title: "Get Knowledge Base",
@@ -2009,7 +1600,7 @@ function registerKnowledgeTools(server2, client2) {
2009
1600
  );
2010
1601
  server2.tool(
2011
1602
  "search_knowledge",
2012
- "Search the customer's knowledge base by tag or keyword. Use this to find specific reference material when writing about a topic. Search by tag first for best results, falls back to text search. Do NOT use this to find brand voice or audience data -- those have dedicated tools.",
1603
+ "Search the knowledge base by tag or keyword.",
2013
1604
  {
2014
1605
  query: z8.string().describe(
2015
1606
  "Search query - matches against tags first, then falls back to text search on title and content"
@@ -2060,7 +1651,7 @@ function registerKnowledgeTools(server2, client2) {
2060
1651
  );
2061
1652
  server2.tool(
2062
1653
  "add_knowledge",
2063
- "Add reference material to the customer's knowledge base. Use ONLY for: company info, product details, pricing, team bios, competitor notes, brand guidelines, and statistics. Do NOT add brand voice profiles, audience profiles, or content drafts here -- those have dedicated tools. Always include relevant tags to make items easier to find later.",
1654
+ "Add reference material to the knowledge base. Include tags for categorization.",
2064
1655
  {
2065
1656
  title: z8.string().describe("Title for the knowledge item"),
2066
1657
  content: z8.string().describe("The content/text to save"),
@@ -2102,10 +1693,8 @@ import { z as z9 } from "zod";
2102
1693
  function registerAudienceTools(server2, client2) {
2103
1694
  server2.tool(
2104
1695
  "get_audience",
2105
- "Get the target audience profile for content creation. Use this tool to understand WHO the content is for. Returns: name, description, demographics, painPoints (array), motivations (array), toneNotes, contentPreferences, preferredPlatforms (array), and isDefault. Call this BEFORE writing any post or content so you can tailor messaging to resonate with the intended audience. Pass an audienceId to fetch a specific profile, or omit it to get the default. Use list_audiences to see all available profiles. Do NOT store audience data in the knowledge base.",
2106
- {
2107
- audienceId: z9.string().optional().describe("Specific audience profile ID. If omitted, returns the default audience.")
2108
- },
1696
+ "Get the target audience profile. Call before creating content.",
1697
+ {},
2109
1698
  {
2110
1699
  title: "Get Audience Profile",
2111
1700
  readOnlyHint: true,
@@ -2113,9 +1702,9 @@ function registerAudienceTools(server2, client2) {
2113
1702
  idempotentHint: true,
2114
1703
  openWorldHint: false
2115
1704
  },
2116
- async ({ audienceId }) => {
1705
+ async () => {
2117
1706
  try {
2118
- const audience = audienceId ? await client2.getAudience(audienceId) : await client2.getDefaultAudience();
1707
+ const audience = await client2.getDefaultAudience();
2119
1708
  const lines = [];
2120
1709
  lines.push(`## Target Audience: ${audience.name}`);
2121
1710
  lines.push("");
@@ -2179,25 +1768,16 @@ function registerAudienceTools(server2, client2) {
2179
1768
  );
2180
1769
  server2.tool(
2181
1770
  "update_audience",
2182
- `Create or update a target audience profile. This is an upsert \u2014 if no audience_id is provided it will try to update the default audience, or create a new one if none exists. Only the fields you provide will be changed; omitted fields are left as-is.
2183
-
2184
- USAGE GUIDELINES:
2185
- - Call get_audience first to see the current state before making changes
2186
- - Always set confirmed=false first to preview the changes, then confirmed=true after user approval
2187
- - When updating array fields (pain_points, motivations, preferred_platforms), include the FULL array (existing + new items), not just the new ones
2188
- - Omit audience_id to update the default audience or create a new one if none exists
2189
- - Max limits: name 100 chars, description 500 chars`,
1771
+ "Update the audience profile. Set confirmed=false to preview first.",
2190
1772
  {
2191
- audience_id: z9.string().optional().describe("Audience ID to update. Omit to update the default audience, or create one if none exists."),
2192
- name: z9.string().max(100).optional().describe("Audience profile name (e.g. 'SaaS Founders', 'Health-Conscious Millennials'). Required when creating a new audience."),
1773
+ name: z9.string().max(100).optional().describe("Audience profile name. Required when creating a new audience."),
2193
1774
  description: z9.string().max(500).optional().describe("Brief description of who this audience is"),
2194
- demographics: z9.string().optional().describe("Demographic details \u2014 age range, location, income level, education, etc."),
2195
- pain_points: z9.array(z9.string()).optional().describe("Problems and frustrations the audience faces. Send the FULL list, not just additions."),
2196
- motivations: z9.array(z9.string()).optional().describe("Goals, desires, and what drives the audience. Send the FULL list, not just additions."),
2197
- 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."),
2198
- tone_notes: z9.string().optional().describe("How to speak to this audience \u2014 formality level, jargon preferences, emotional tone"),
2199
- content_preferences: z9.string().optional().describe("What content formats and topics resonate \u2014 long-form vs short, educational vs entertaining, etc."),
2200
- is_default: z9.boolean().optional().describe("Set to true to make this the default audience profile"),
1775
+ demographics: z9.string().max(500).optional().describe("Demographic details"),
1776
+ pain_points: z9.array(z9.string().max(200)).max(10).optional().describe("Problems the audience faces. Send the FULL list."),
1777
+ motivations: z9.array(z9.string().max(200)).max(10).optional().describe("Goals and desires. Send the FULL list."),
1778
+ preferred_platforms: z9.array(z9.string()).max(6).optional().describe("Social platforms the audience is most active on. Send the FULL list."),
1779
+ tone_notes: z9.string().max(500).optional().describe("How to speak to this audience"),
1780
+ content_preferences: z9.string().max(500).optional().describe("What content formats and topics resonate"),
2201
1781
  confirmed: z9.boolean().default(false).describe(
2202
1782
  "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
2203
1783
  )
@@ -2219,27 +1799,24 @@ USAGE GUIDELINES:
2219
1799
  if (args.preferred_platforms !== void 0) payload.platforms = args.preferred_platforms;
2220
1800
  if (args.tone_notes !== void 0) payload.toneNotes = args.tone_notes;
2221
1801
  if (args.content_preferences !== void 0) payload.contentPreferences = args.content_preferences;
2222
- if (args.is_default !== void 0) payload.isDefault = args.is_default;
2223
1802
  if (Object.keys(payload).length === 0) {
2224
1803
  return {
2225
1804
  content: [
2226
1805
  {
2227
1806
  type: "text",
2228
- 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)."
1807
+ text: "No fields provided. Specify at least one field to update."
2229
1808
  }
2230
1809
  ],
2231
1810
  isError: true
2232
1811
  };
2233
1812
  }
2234
- let targetId = args.audience_id;
1813
+ let targetId;
2235
1814
  let isCreating = false;
2236
- if (!targetId) {
2237
- try {
2238
- const defaultAudience = await client2.getDefaultAudience();
2239
- targetId = String(defaultAudience.id);
2240
- } catch {
2241
- isCreating = true;
2242
- }
1815
+ try {
1816
+ const defaultAudience = await client2.getDefaultAudience();
1817
+ targetId = String(defaultAudience.id);
1818
+ } catch {
1819
+ isCreating = true;
2243
1820
  }
2244
1821
  if (isCreating && !payload.name) {
2245
1822
  return {
@@ -2255,9 +1832,6 @@ USAGE GUIDELINES:
2255
1832
  if (args.confirmed !== true) {
2256
1833
  const lines2 = [];
2257
1834
  lines2.push(isCreating ? "## New Audience Preview" : "## Audience Update Preview");
2258
- if (!isCreating) {
2259
- lines2.push(`**Audience ID:** ${targetId}`);
2260
- }
2261
1835
  lines2.push("");
2262
1836
  if (payload.name) {
2263
1837
  lines2.push(`**Name:** ${payload.name}`);
@@ -2305,10 +1879,6 @@ USAGE GUIDELINES:
2305
1879
  lines2.push(String(payload.contentPreferences));
2306
1880
  lines2.push("");
2307
1881
  }
2308
- if (payload.isDefault !== void 0) {
2309
- lines2.push(`**Set as default:** ${payload.isDefault ? "Yes" : "No"}`);
2310
- lines2.push("");
2311
- }
2312
1882
  lines2.push("---");
2313
1883
  lines2.push("Call this tool again with **confirmed=true** to save these changes.");
2314
1884
  return {
@@ -2334,7 +1904,6 @@ USAGE GUIDELINES:
2334
1904
  if (payload.platforms) summary.push(`${payload.platforms.length} platforms`);
2335
1905
  if (payload.toneNotes) summary.push("tone notes");
2336
1906
  if (payload.contentPreferences) summary.push("content preferences");
2337
- if (payload.isDefault !== void 0) summary.push("default status");
2338
1907
  lines.push(`**${isCreating ? "Created with" : "Updated"}:** ${summary.join(", ")}`);
2339
1908
  return {
2340
1909
  content: [{ type: "text", text: lines.join("\n") }]
@@ -2378,7 +1947,7 @@ Follow these rules when drafting:
2378
1947
  function registerNewsletterTemplateTools(server2, client2) {
2379
1948
  server2.tool(
2380
1949
  "get_newsletter_template",
2381
- "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.",
1950
+ "Get a newsletter template's structure, sections, and HTML. Pass templateId or omit for default.",
2382
1951
  {
2383
1952
  templateId: z10.string().optional().describe(
2384
1953
  "Specific template ID. If omitted, returns the default template."
@@ -2479,6 +2048,15 @@ function registerNewsletterTemplateTools(server2, client2) {
2479
2048
  );
2480
2049
  lines.push("");
2481
2050
  }
2051
+ if (template.htmlContent) {
2052
+ lines.push("### HTML Template:");
2053
+ lines.push("The following HTML skeleton contains {{placeholder}} variables. Fill in the placeholders with real content when generating the newsletter.");
2054
+ lines.push("");
2055
+ lines.push("```html");
2056
+ lines.push(template.htmlContent);
2057
+ lines.push("```");
2058
+ lines.push("");
2059
+ }
2482
2060
  if (template.rssFeedUrls && template.rssFeedUrls.length > 0) {
2483
2061
  lines.push("### Linked RSS Feeds:");
2484
2062
  for (const url of template.rssFeedUrls) {
@@ -2515,9 +2093,10 @@ function registerNewsletterTemplateTools(server2, client2) {
2515
2093
  );
2516
2094
  server2.tool(
2517
2095
  "get_past_newsletters",
2518
- "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.",
2096
+ "Get past newsletters from the archive. Pass newsletterId to get full content of a specific edition.",
2519
2097
  {
2520
- limit: z10.number().optional().describe("Number of past newsletters to retrieve. Default 5, max 50."),
2098
+ newsletterId: z10.string().optional().describe("If provided, returns the full content of this specific newsletter. If omitted, returns the list."),
2099
+ limit: z10.number().optional().describe("Number of past newsletters to retrieve (when listing). Default 5, max 50."),
2521
2100
  templateId: z10.string().optional().describe("Filter by template ID to see past newsletters from a specific template.")
2522
2101
  },
2523
2102
  {
@@ -2527,7 +2106,41 @@ function registerNewsletterTemplateTools(server2, client2) {
2527
2106
  idempotentHint: true,
2528
2107
  openWorldHint: false
2529
2108
  },
2530
- async ({ limit, templateId }) => {
2109
+ async ({ newsletterId, limit, templateId }) => {
2110
+ if (newsletterId) {
2111
+ try {
2112
+ const newsletter = await client2.getArchivedNewsletter(
2113
+ newsletterId
2114
+ );
2115
+ const lines = [];
2116
+ lines.push(`## ${newsletter.subject}`);
2117
+ if (newsletter.sentAt) lines.push(`Sent: ${newsletter.sentAt}`);
2118
+ if (newsletter.notes) lines.push(`Notes: ${newsletter.notes}`);
2119
+ lines.push("");
2120
+ lines.push("### Full HTML Content:");
2121
+ lines.push(newsletter.contentHtml);
2122
+ return {
2123
+ content: [
2124
+ { type: "text", text: lines.join("\n") },
2125
+ ...newsletter.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2126
+ ${newsletter.contentHtml}` }] : []
2127
+ ]
2128
+ };
2129
+ } catch (error) {
2130
+ const message = error instanceof Error ? error.message : "Unknown error";
2131
+ if (message.includes("404")) {
2132
+ return {
2133
+ content: [
2134
+ {
2135
+ type: "text",
2136
+ text: "Newsletter not found. Check the ID and try again."
2137
+ }
2138
+ ]
2139
+ };
2140
+ }
2141
+ throw error;
2142
+ }
2143
+ }
2531
2144
  try {
2532
2145
  const params = {};
2533
2146
  if (limit) params.limit = String(Math.min(limit, 50));
@@ -2565,12 +2178,17 @@ function registerNewsletterTemplateTools(server2, client2) {
2565
2178
  const textContent = nl.contentHtml ? nl.contentHtml.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 500) : "(no content)";
2566
2179
  lines.push(textContent);
2567
2180
  lines.push(
2568
- `[Full content available via get_past_newsletter tool with ID: ${nl.id}]`
2181
+ `[Full content available by calling get_past_newsletters with newsletterId: ${nl.id}]`
2569
2182
  );
2570
2183
  lines.push("");
2571
2184
  }
2185
+ const mostRecent = newsletters[0];
2572
2186
  return {
2573
- content: [{ type: "text", text: lines.join("\n") }]
2187
+ content: [
2188
+ { type: "text", text: lines.join("\n") },
2189
+ ...mostRecent?.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2190
+ ${mostRecent.contentHtml}` }] : []
2191
+ ]
2574
2192
  };
2575
2193
  } catch (error) {
2576
2194
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -2579,87 +2197,247 @@ function registerNewsletterTemplateTools(server2, client2) {
2579
2197
  }
2580
2198
  );
2581
2199
  server2.tool(
2582
- "get_past_newsletter",
2583
- "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.",
2200
+ "save_newsletter",
2201
+ "Save an approved newsletter to the archive for future reference.",
2202
+ {
2203
+ subject: z10.string().describe("The newsletter subject line."),
2204
+ contentHtml: z10.string().describe("The full HTML content of the newsletter."),
2205
+ templateId: z10.string().optional().describe("The template ID used to generate this newsletter."),
2206
+ notes: z10.string().optional().describe(
2207
+ "Optional notes about this newsletter (what worked, theme, etc)."
2208
+ )
2209
+ },
2210
+ {
2211
+ title: "Save Newsletter to Archive",
2212
+ readOnlyHint: false,
2213
+ destructiveHint: false,
2214
+ idempotentHint: false,
2215
+ openWorldHint: false
2216
+ },
2217
+ async ({ subject, contentHtml, templateId, notes }) => {
2218
+ try {
2219
+ const data = {
2220
+ subject,
2221
+ contentHtml
2222
+ };
2223
+ if (templateId) data.templateId = Number(templateId);
2224
+ if (notes) data.notes = notes;
2225
+ await client2.saveNewsletterToArchive(data);
2226
+ return {
2227
+ content: [
2228
+ {
2229
+ type: "text",
2230
+ text: `Newsletter "${subject}" has been saved to the archive successfully.`
2231
+ },
2232
+ ...contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2233
+ ${contentHtml}` }] : []
2234
+ ]
2235
+ };
2236
+ } catch (error) {
2237
+ const message = error instanceof Error ? error.message : "Unknown error";
2238
+ throw new Error(`Failed to save newsletter: ${message}`);
2239
+ }
2240
+ }
2241
+ );
2242
+ server2.tool(
2243
+ "save_newsletter_template",
2244
+ "Create or update a newsletter template with HTML and section definitions.",
2584
2245
  {
2585
- newsletterId: z10.string().describe("The ID of the archived newsletter to retrieve.")
2246
+ template_id: z10.string().optional().describe(
2247
+ "If provided, updates this existing template. Otherwise creates a new one."
2248
+ ),
2249
+ name: z10.string().min(1).max(255).describe("Template name"),
2250
+ description: z10.string().max(2e3).optional().describe("What this template is for"),
2251
+ html_content: z10.string().min(1).max(1e5).describe(
2252
+ "Full HTML template with {{placeholder}} variables."
2253
+ ),
2254
+ sections: z10.array(
2255
+ z10.object({
2256
+ name: z10.string().describe("Section identifier, e.g. 'intro'"),
2257
+ label: z10.string().describe("Display label, e.g. 'Intro/Greeting'"),
2258
+ required: z10.boolean().optional().describe("Whether this section is required"),
2259
+ item_count: z10.number().optional().describe("Number of items in this section, if applicable")
2260
+ })
2261
+ ).max(20).optional().describe("Array of section metadata describing the template structure"),
2262
+ style_config: z10.object({
2263
+ primary_color: z10.string().optional(),
2264
+ accent_color: z10.string().optional(),
2265
+ link_color: z10.string().optional(),
2266
+ background_color: z10.string().optional(),
2267
+ font_family: z10.string().optional(),
2268
+ max_width: z10.string().optional()
2269
+ }).optional().describe("Colors, fonts, and brand configuration"),
2270
+ is_default: z10.boolean().optional().describe(
2271
+ "Set as the default template for this customer."
2272
+ )
2586
2273
  },
2587
2274
  {
2588
- title: "Get Past Newsletter Detail",
2589
- readOnlyHint: true,
2275
+ title: "Save Newsletter Template",
2276
+ readOnlyHint: false,
2590
2277
  destructiveHint: false,
2591
- idempotentHint: true,
2278
+ idempotentHint: false,
2592
2279
  openWorldHint: false
2593
2280
  },
2594
- async ({ newsletterId }) => {
2281
+ async ({
2282
+ template_id,
2283
+ name,
2284
+ description,
2285
+ html_content,
2286
+ sections,
2287
+ style_config,
2288
+ is_default
2289
+ }) => {
2595
2290
  try {
2596
- const newsletter = await client2.getArchivedNewsletter(
2597
- newsletterId
2598
- );
2599
- const lines = [];
2600
- lines.push(`## ${newsletter.subject}`);
2601
- if (newsletter.sentAt) lines.push(`Sent: ${newsletter.sentAt}`);
2602
- if (newsletter.notes) lines.push(`Notes: ${newsletter.notes}`);
2603
- lines.push("");
2604
- lines.push("### Full HTML Content:");
2605
- lines.push(newsletter.contentHtml);
2291
+ const data = {
2292
+ name,
2293
+ htmlContent: html_content
2294
+ };
2295
+ if (description !== void 0) data.description = description;
2296
+ if (sections !== void 0) {
2297
+ data.sections = sections.map((s) => ({
2298
+ id: s.name,
2299
+ type: "custom",
2300
+ label: s.label,
2301
+ required: s.required ?? true,
2302
+ count: s.item_count
2303
+ }));
2304
+ }
2305
+ if (style_config !== void 0) {
2306
+ data.style = {
2307
+ primary_color: style_config.primary_color,
2308
+ accent_color: style_config.accent_color,
2309
+ link_color: style_config.link_color,
2310
+ background_color: style_config.background_color,
2311
+ font_family: style_config.font_family,
2312
+ content_width: style_config.max_width
2313
+ };
2314
+ }
2315
+ if (is_default !== void 0) data.isDefault = is_default;
2316
+ let result;
2317
+ if (template_id) {
2318
+ result = await client2.updateTemplate(
2319
+ template_id,
2320
+ data
2321
+ );
2322
+ } else {
2323
+ result = await client2.createTemplate(data);
2324
+ }
2325
+ const action = template_id ? "updated" : "created";
2606
2326
  return {
2607
- content: [{ type: "text", text: lines.join("\n") }]
2327
+ content: [
2328
+ {
2329
+ type: "text",
2330
+ text: `Newsletter template "${result.name}" ${action} successfully (ID: ${result.id}).${result.isDefault ? " Set as default." : ""}`
2331
+ }
2332
+ ]
2608
2333
  };
2609
2334
  } catch (error) {
2610
2335
  const message = error instanceof Error ? error.message : "Unknown error";
2611
- if (message.includes("404")) {
2336
+ throw new Error(`Failed to save newsletter template: ${message}`);
2337
+ }
2338
+ }
2339
+ );
2340
+ server2.tool(
2341
+ "list_newsletter_templates",
2342
+ "List all saved newsletter templates.",
2343
+ {},
2344
+ {
2345
+ title: "List Newsletter Templates",
2346
+ readOnlyHint: true,
2347
+ destructiveHint: false,
2348
+ idempotentHint: true,
2349
+ openWorldHint: false
2350
+ },
2351
+ async () => {
2352
+ try {
2353
+ const templates = await client2.listTemplates();
2354
+ if (!templates || templates.length === 0) {
2612
2355
  return {
2613
2356
  content: [
2614
2357
  {
2615
2358
  type: "text",
2616
- text: "Newsletter not found. Check the ID and try again."
2359
+ text: "No newsletter templates found. Use save_newsletter_template to create one."
2617
2360
  }
2618
2361
  ]
2619
2362
  };
2620
2363
  }
2621
- throw error;
2364
+ const lines = [];
2365
+ lines.push(`## Newsletter Templates (${templates.length})`);
2366
+ lines.push("");
2367
+ for (const t of templates) {
2368
+ const badges = [];
2369
+ if (t.isDefault) badges.push("DEFAULT");
2370
+ if (t.htmlContent) badges.push("HAS HTML");
2371
+ const badgeStr = badges.length > 0 ? ` [${badges.join(", ")}]` : "";
2372
+ lines.push(`**${t.name}** (ID: ${t.id})${badgeStr}`);
2373
+ if (t.description) lines.push(` ${t.description}`);
2374
+ if (t.sections && t.sections.length > 0) {
2375
+ lines.push(
2376
+ ` Sections: ${t.sections.map((s) => s.label).join(", ")}`
2377
+ );
2378
+ }
2379
+ lines.push("");
2380
+ }
2381
+ return {
2382
+ content: [{ type: "text", text: lines.join("\n") }]
2383
+ };
2384
+ } catch (error) {
2385
+ const message = error instanceof Error ? error.message : "Unknown error";
2386
+ throw new Error(`Failed to list newsletter templates: ${message}`);
2622
2387
  }
2623
2388
  }
2624
2389
  );
2625
2390
  server2.tool(
2626
- "save_newsletter",
2627
- "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.",
2391
+ "delete_newsletter_template",
2392
+ "Delete a newsletter template. Set confirmed=true to proceed.",
2628
2393
  {
2629
- subject: z10.string().describe("The newsletter subject line."),
2630
- contentHtml: z10.string().describe("The full HTML content of the newsletter."),
2631
- templateId: z10.string().optional().describe("The template ID used to generate this newsletter."),
2632
- notes: z10.string().optional().describe(
2633
- "Optional notes about this newsletter (what worked, theme, etc)."
2394
+ template_id: z10.string().describe("The ID of the newsletter template to delete."),
2395
+ confirmed: z10.boolean().default(false).describe(
2396
+ "Must be true to actually delete. If false, returns a confirmation prompt."
2634
2397
  )
2635
2398
  },
2636
2399
  {
2637
- title: "Save Newsletter to Archive",
2400
+ title: "Delete Newsletter Template",
2638
2401
  readOnlyHint: false,
2639
- destructiveHint: false,
2402
+ destructiveHint: true,
2640
2403
  idempotentHint: false,
2641
2404
  openWorldHint: false
2642
2405
  },
2643
- async ({ subject, contentHtml, templateId, notes }) => {
2406
+ async ({ template_id, confirmed }) => {
2644
2407
  try {
2645
- const data = {
2646
- subject,
2647
- contentHtml
2648
- };
2649
- if (templateId) data.templateId = Number(templateId);
2650
- if (notes) data.notes = notes;
2651
- await client2.saveNewsletterToArchive(data);
2408
+ if (!confirmed) {
2409
+ const template = await client2.getTemplate(template_id);
2410
+ return {
2411
+ content: [
2412
+ {
2413
+ type: "text",
2414
+ text: `Are you sure you want to delete the template "${template.name}" (ID: ${template.id})? This action cannot be undone. Call delete_newsletter_template again with confirmed=true to proceed.`
2415
+ }
2416
+ ]
2417
+ };
2418
+ }
2419
+ await client2.deleteTemplate(template_id);
2652
2420
  return {
2653
2421
  content: [
2654
2422
  {
2655
2423
  type: "text",
2656
- text: `Newsletter "${subject}" has been saved to the archive successfully.`
2424
+ text: `Newsletter template (ID: ${template_id}) has been deleted.`
2657
2425
  }
2658
2426
  ]
2659
2427
  };
2660
2428
  } catch (error) {
2661
2429
  const message = error instanceof Error ? error.message : "Unknown error";
2662
- throw new Error(`Failed to save newsletter: ${message}`);
2430
+ if (message.includes("404")) {
2431
+ return {
2432
+ content: [
2433
+ {
2434
+ type: "text",
2435
+ text: "Template not found. It may have already been deleted."
2436
+ }
2437
+ ]
2438
+ };
2439
+ }
2440
+ throw new Error(`Failed to delete newsletter template: ${message}`);
2663
2441
  }
2664
2442
  }
2665
2443
  );
@@ -2670,7 +2448,7 @@ import { z as z11 } from "zod";
2670
2448
  function registerCalendarTools(server2, client2) {
2671
2449
  server2.tool(
2672
2450
  "get_calendar",
2673
- "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.",
2451
+ "View the content calendar with all scheduled, published, and draft posts.",
2674
2452
  {
2675
2453
  from: z11.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
2676
2454
  to: z11.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
@@ -2753,7 +2531,7 @@ function registerCalendarTools(server2, client2) {
2753
2531
  );
2754
2532
  server2.tool(
2755
2533
  "get_queue",
2756
- "View the customer's posting queue schedule -- their recurring weekly time slots for automatic post scheduling. Use this to understand when posts go out and to schedule content into the next available slot.",
2534
+ "View the posting queue schedule and recurring weekly time slots.",
2757
2535
  {},
2758
2536
  {
2759
2537
  title: "Get Posting Queue",
@@ -2787,7 +2565,8 @@ function registerCalendarTools(server2, client2) {
2787
2565
  for (const slot of slots) {
2788
2566
  const day = dayNames[slot.dayOfWeek] ?? `Day ${slot.dayOfWeek}`;
2789
2567
  const platforms = (slot.platforms ?? []).map((p) => `[${p}]`).join(" ");
2790
- text += `${day}: ${slot.time} ${platforms}
2568
+ const slotId = slot.id ?? slot._id ?? `${slot.dayOfWeek}_${slot.time}`;
2569
+ text += `${day}: ${slot.time} ${platforms} (id: ${slotId})
2791
2570
  `;
2792
2571
  }
2793
2572
  }
@@ -2808,17 +2587,7 @@ Next slot: ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
2808
2587
  );
2809
2588
  server2.tool(
2810
2589
  "schedule_to_queue",
2811
- `Add a post to the customer's content queue for automatic scheduling.
2812
-
2813
- Always show the user a preview of the content before calling this tool. Call get_brand_voice and get_audience before composing if not already loaded this session. Never call with confirmed=true without explicit user approval.
2814
-
2815
- REQUIRED WORKFLOW:
2816
- 1. BEFORE creating content, call get_brand_voice and get_audience
2817
- 2. ALWAYS set confirmed=false first to preview
2818
- 3. Show the user a visual preview before confirming
2819
- 4. Tell the user which queue slot the post will be assigned to
2820
-
2821
- 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.`,
2590
+ "Add a post to the next available queue slot. Check get_queue first. Set confirmed=false to preview first.",
2822
2591
  {
2823
2592
  content: z11.string().describe("The text content of the post"),
2824
2593
  platforms: z11.array(z11.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -2919,7 +2688,7 @@ function timeAgo(dateStr) {
2919
2688
  function registerNotificationTools(server2, client2) {
2920
2689
  server2.tool(
2921
2690
  "get_notifications",
2922
- "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.",
2691
+ "Get recent notifications including post results, account warnings, and errors.",
2923
2692
  {
2924
2693
  unread_only: z12.boolean().optional().describe("If true, only show unread notifications. Default false."),
2925
2694
  limit: z12.number().optional().describe("Max notifications to return. Default 10.")
@@ -2964,7 +2733,7 @@ No notifications found. All clear!`
2964
2733
  const action = n.actionLabel || "View";
2965
2734
  if (n.resourceType === "post" && n.resourceId) {
2966
2735
  line += `
2967
- Action: ${action} \u2192 retry_post with post_id "${n.resourceId}"`;
2736
+ Action: ${action} \u2192 get_post with post_id "${n.resourceId}"`;
2968
2737
  } else {
2969
2738
  line += `
2970
2739
  Action: ${action} \u2192 ${n.actionUrl}`;
@@ -2989,10 +2758,36 @@ Showing ${notifications.length} of ${result.unread_count !== void 0 ? "total" :
2989
2758
  }
2990
2759
 
2991
2760
  // src/tools/publishing-rules.ts
2761
+ var WORKFLOW = {
2762
+ before_writing_content: [
2763
+ "Call get_brand_voice to load tone and style rules",
2764
+ "Call get_audience to understand who the content is for",
2765
+ "For newsletters: also call get_newsletter_template and get_past_newsletters"
2766
+ ],
2767
+ before_publishing: [
2768
+ "Always set confirmed=false first to generate a preview",
2769
+ "Show the user a visual preview before confirming",
2770
+ "Never set confirmed=true without explicit user approval",
2771
+ "For newsletters: show subscriber count and send time before confirming",
2772
+ "If require_double_confirm is true, ask for confirmation twice"
2773
+ ],
2774
+ replies_and_engagement: [
2775
+ "Draft the reply and show it before sending",
2776
+ "Set confirmed=false first to preview",
2777
+ "Remind user that replies are public"
2778
+ ],
2779
+ newsletters: [
2780
+ "Always render newsletter as HTML artifact/preview before pushing to ESP",
2781
+ "Use table-based layouts, inline CSS only, 600px max width",
2782
+ "Email-safe fonts only: Arial, Helvetica, Georgia, Verdana",
2783
+ "All images must use absolute URLs",
2784
+ "Keep total email under 102KB to avoid Gmail clipping"
2785
+ ]
2786
+ };
2992
2787
  function registerPublishingRulesTools(server2, client2) {
2993
2788
  server2.tool(
2994
2789
  "get_publishing_rules",
2995
- `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.`,
2790
+ "Get publishing rules and the content creation workflow. Call this BEFORE publishing, scheduling, or sending any content.",
2996
2791
  {},
2997
2792
  {
2998
2793
  title: "Get Publishing Rules",
@@ -3003,90 +2798,30 @@ function registerPublishingRulesTools(server2, client2) {
3003
2798
  },
3004
2799
  async () => {
3005
2800
  const rules = await client2.getPublishingRules();
3006
- let text = "## Publishing Rules\n\n";
3007
- text += `Social media default: **${rules.socialDefaultAction ?? "draft"}**`;
3008
- if (rules.socialDefaultAction === "draft") text += " (content is saved as draft, not published)";
3009
- text += "\n";
3010
- text += `Newsletter default: **${rules.newsletterDefaultAction ?? "draft"}**`;
3011
- if (rules.newsletterDefaultAction === "draft") text += " (content is saved as draft, not sent)";
3012
- text += "\n";
3013
- text += `Preview required before publish: **${rules.requirePreviewBeforePublish ? "yes" : "no"}**
3014
- `;
3015
- text += `Double confirmation for newsletters: **${rules.requireDoubleConfirmNewsletter ? "yes" : "no"}**
3016
- `;
3017
- text += `Double confirmation for social: **${rules.requireDoubleConfirmSocial ? "yes" : "no"}**
3018
- `;
3019
- text += `Immediate publish allowed: **${rules.allowImmediatePublish ? "yes" : "no"}**
3020
- `;
3021
- text += `Immediate send allowed: **${rules.allowImmediateSend ? "yes" : "no"}**
3022
- `;
3023
- text += `Max posts per day: **${rules.maxPostsPerDay ?? "unlimited"}**
3024
- `;
3025
- if (rules.blockedWords && rules.blockedWords.length > 0) {
3026
- text += `Blocked words: **${rules.blockedWords.join(", ")}**
3027
- `;
3028
- } else {
3029
- text += "Blocked words: none\n";
3030
- }
3031
- if (rules.requiredDisclaimer) {
3032
- text += `Required disclaimer: "${rules.requiredDisclaimer}"
3033
- `;
3034
- } else {
3035
- text += "Required disclaimer: none\n";
3036
- }
3037
- return { content: [{ type: "text", text }] };
3038
- }
3039
- );
3040
- }
3041
-
3042
- // src/tools/audit-log.ts
3043
- import { z as z13 } from "zod";
3044
- function registerAuditLogTools(server2, client2) {
3045
- server2.tool(
3046
- "get_audit_log",
3047
- "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.",
3048
- {
3049
- limit: z13.number().optional().describe("Maximum number of entries to return (default 20, max 200)"),
3050
- action: z13.string().optional().describe("Filter by action type prefix, e.g. 'post.published', 'newsletter.sent', 'post.' for all post actions"),
3051
- resource_type: z13.string().optional().describe("Filter by resource type: post, newsletter, account, media, publishing_rules, brand_voice")
3052
- },
3053
- {
3054
- title: "Get Audit Log",
3055
- readOnlyHint: true,
3056
- destructiveHint: false,
3057
- idempotentHint: true,
3058
- openWorldHint: false
3059
- },
3060
- async (args) => {
3061
- const params = {};
3062
- if (args.limit) params.limit = String(args.limit);
3063
- if (args.action) params.action = args.action;
3064
- if (args.resource_type) params.resource_type = args.resource_type;
3065
- const data = await client2.getAuditLog(params);
3066
- const entries = data?.entries ?? [];
3067
- if (entries.length === 0) {
3068
- return {
3069
- content: [{ type: "text", text: "## Audit Log\n\nNo entries found." }]
3070
- };
3071
- }
3072
- let text = `## Audit Log (${entries.length} of ${data?.total ?? entries.length} entries)
3073
-
3074
- `;
3075
- for (const entry of entries) {
3076
- const date = entry.createdAt ? new Date(entry.createdAt).toLocaleString() : "unknown";
3077
- const details = entry.details && Object.keys(entry.details).length > 0 ? ` \u2014 ${JSON.stringify(entry.details)}` : "";
3078
- text += `- **${date}** | ${entry.action} | ${entry.resourceType}`;
3079
- if (entry.resourceId) text += ` #${entry.resourceId}`;
3080
- text += `${details}
3081
- `;
3082
- }
3083
- return { content: [{ type: "text", text }] };
2801
+ const response = {
2802
+ rules: {
2803
+ social_default_action: rules.socialDefaultAction ?? "draft",
2804
+ newsletter_default_action: rules.newsletterDefaultAction ?? "draft",
2805
+ require_preview_before_publish: rules.requirePreviewBeforePublish ?? true,
2806
+ require_double_confirm_newsletter: rules.requireDoubleConfirmNewsletter ?? true,
2807
+ require_double_confirm_social: rules.requireDoubleConfirmSocial ?? false,
2808
+ allow_immediate_publish: rules.allowImmediatePublish ?? false,
2809
+ allow_immediate_send: rules.allowImmediateSend ?? false,
2810
+ max_posts_per_day: rules.maxPostsPerDay ?? null,
2811
+ blocked_words: rules.blockedWords ?? [],
2812
+ required_disclaimer: rules.requiredDisclaimer ?? null
2813
+ },
2814
+ workflow: WORKFLOW
2815
+ };
2816
+ return {
2817
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
2818
+ };
3084
2819
  }
3085
2820
  );
3086
2821
  }
3087
2822
 
3088
2823
  // src/tools/sources.ts
3089
- import { z as z14 } from "zod";
2824
+ import { z as z13 } from "zod";
3090
2825
  var TYPE_LABELS = {
3091
2826
  feed: "RSS Feed",
3092
2827
  website: "Website",
@@ -3108,12 +2843,12 @@ var TYPE_HINTS = {
3108
2843
  function registerSourceTools(server2, client2) {
3109
2844
  server2.tool(
3110
2845
  "get_sources",
3111
- "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.",
2846
+ "Get saved content sources (RSS feeds, websites, YouTube channels, search topics).",
3112
2847
  {
3113
- type: z14.string().optional().describe(
2848
+ type: z13.string().optional().describe(
3114
2849
  "Optional: filter by type (feed, website, youtube, search, podcast, reddit, social)"
3115
2850
  ),
3116
- category: z14.string().optional().describe("Optional: filter by category")
2851
+ category: z13.string().optional().describe("Optional: filter by category")
3117
2852
  },
3118
2853
  {
3119
2854
  title: "Get Content Sources",
@@ -3174,13 +2909,13 @@ function registerSourceTools(server2, client2) {
3174
2909
  );
3175
2910
  server2.tool(
3176
2911
  "add_source",
3177
- "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.",
2912
+ "Save a new content source to monitor.",
3178
2913
  {
3179
- name: z14.string().describe("Display name for the source"),
3180
- url: z14.string().optional().describe(
3181
- "URL of the source (RSS feed, website, YouTube channel, subreddit, or social profile URL). Not needed for 'search' type."
2914
+ name: z13.string().describe("Display name for the source"),
2915
+ url: z13.string().optional().describe(
2916
+ "URL of the source. Not needed for 'search' type."
3182
2917
  ),
3183
- type: z14.enum([
2918
+ type: z13.enum([
3184
2919
  "feed",
3185
2920
  "website",
3186
2921
  "youtube",
@@ -3189,12 +2924,12 @@ function registerSourceTools(server2, client2) {
3189
2924
  "reddit",
3190
2925
  "social"
3191
2926
  ]).describe("Type of source. Determines how to fetch content from it."),
3192
- category: z14.string().optional().describe(
3193
- "Optional category for organization (e.g. 'competitors', 'industry', 'inspiration', 'my-content')"
2927
+ category: z13.string().optional().describe(
2928
+ "Optional category for organization (e.g. 'competitors', 'industry', 'inspiration')"
3194
2929
  ),
3195
- tags: z14.array(z14.string()).optional().describe("Optional tags for filtering"),
3196
- notes: z14.string().optional().describe("Optional notes about why this source matters"),
3197
- searchQuery: z14.string().optional().describe(
2930
+ tags: z13.array(z13.string()).optional().describe("Optional tags for filtering"),
2931
+ notes: z13.string().optional().describe("Optional notes about why this source matters"),
2932
+ searchQuery: z13.string().optional().describe(
3198
2933
  "For 'search' type only: the keyword or topic to search for"
3199
2934
  )
3200
2935
  },
@@ -3234,12 +2969,12 @@ function registerSourceTools(server2, client2) {
3234
2969
  );
3235
2970
  server2.tool(
3236
2971
  "update_source",
3237
- "Update a saved content source. Use when the customer wants to rename, recategorize, change the URL, add notes, or deactivate a source.",
2972
+ "Update a saved content source.",
3238
2973
  {
3239
- source_id: z14.number().describe("The ID of the source to update"),
3240
- name: z14.string().optional().describe("New display name"),
3241
- url: z14.string().optional().describe("New URL"),
3242
- type: z14.enum([
2974
+ source_id: z13.number().describe("The ID of the source to update"),
2975
+ name: z13.string().optional().describe("New display name"),
2976
+ url: z13.string().optional().describe("New URL"),
2977
+ type: z13.enum([
3243
2978
  "feed",
3244
2979
  "website",
3245
2980
  "youtube",
@@ -3248,11 +2983,11 @@ function registerSourceTools(server2, client2) {
3248
2983
  "reddit",
3249
2984
  "social"
3250
2985
  ]).optional().describe("New source type"),
3251
- category: z14.string().optional().describe("New category"),
3252
- tags: z14.array(z14.string()).optional().describe("New tags"),
3253
- notes: z14.string().optional().describe("New notes"),
3254
- searchQuery: z14.string().optional().describe("New search query"),
3255
- isActive: z14.boolean().optional().describe("Set to false to deactivate")
2986
+ category: z13.string().optional().describe("New category"),
2987
+ tags: z13.array(z13.string()).optional().describe("New tags"),
2988
+ notes: z13.string().optional().describe("New notes"),
2989
+ searchQuery: z13.string().optional().describe("New search query"),
2990
+ isActive: z13.boolean().optional().describe("Set to false to deactivate")
3256
2991
  },
3257
2992
  {
3258
2993
  title: "Update Content Source",
@@ -3280,15 +3015,10 @@ function registerSourceTools(server2, client2) {
3280
3015
  );
3281
3016
  server2.tool(
3282
3017
  "remove_source",
3283
- `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.
3284
-
3285
- REQUIRED WORKFLOW:
3286
- 1. ALWAYS set confirmed=false first to preview which source will be deleted
3287
- 2. Show the user the source details before confirming
3288
- 3. Only confirm after explicit user approval`,
3018
+ "Remove a content source. Set confirmed=false to preview first.",
3289
3019
  {
3290
- source_id: z14.number().describe("The ID of the source to remove"),
3291
- confirmed: z14.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
3020
+ source_id: z13.number().describe("The ID of the source to remove"),
3021
+ confirmed: z13.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
3292
3022
  },
3293
3023
  {
3294
3024
  title: "Remove Content Source",
@@ -3320,13 +3050,13 @@ This will permanently remove this content source. Call this tool again with conf
3320
3050
  }
3321
3051
 
3322
3052
  // src/tools/newsletter-advanced.ts
3323
- import { z as z15 } from "zod";
3053
+ import { z as z14 } from "zod";
3324
3054
  function registerNewsletterAdvancedTools(server2, client2) {
3325
3055
  server2.tool(
3326
3056
  "get_subscriber_by_email",
3327
- "Look up a subscriber by their email address. Returns subscriber details including tags and custom fields.",
3057
+ "Look up a subscriber by email address.",
3328
3058
  {
3329
- email: z15.string().describe("Email address to look up")
3059
+ email: z14.string().describe("Email address to look up")
3330
3060
  },
3331
3061
  {
3332
3062
  title: "Get Subscriber by Email",
@@ -3344,9 +3074,9 @@ function registerNewsletterAdvancedTools(server2, client2) {
3344
3074
  );
3345
3075
  server2.tool(
3346
3076
  "list_automations",
3347
- "List all email automations/workflows from your ESP. On Beehiiv these are journeys; on Kit these are sequences.",
3077
+ "List all email automations and sequences.",
3348
3078
  {
3349
- page: z15.string().optional().describe("Page number for pagination")
3079
+ page: z14.string().optional().describe("Page number for pagination")
3350
3080
  },
3351
3081
  {
3352
3082
  title: "List Automations",
@@ -3366,12 +3096,12 @@ function registerNewsletterAdvancedTools(server2, client2) {
3366
3096
  );
3367
3097
  server2.tool(
3368
3098
  "list_segments",
3369
- "List subscriber segments. Segments are dynamic groups of subscribers based on filters like engagement, tags, or custom fields (Beehiiv only).",
3099
+ "List subscriber segments.",
3370
3100
  {
3371
- page: z15.string().optional().describe("Page number"),
3372
- type: z15.string().optional().describe("Filter by segment type"),
3373
- status: z15.string().optional().describe("Filter by segment status"),
3374
- expand: z15.string().optional().describe("Comma-separated expand fields (e.g. 'stats')")
3101
+ page: z14.string().optional().describe("Page number"),
3102
+ type: z14.string().optional().describe("Filter by segment type"),
3103
+ status: z14.string().optional().describe("Filter by segment status"),
3104
+ expand: z14.string().optional().describe("Comma-separated expand fields (e.g. 'stats')")
3375
3105
  },
3376
3106
  {
3377
3107
  title: "List Segments",
@@ -3394,10 +3124,12 @@ function registerNewsletterAdvancedTools(server2, client2) {
3394
3124
  );
3395
3125
  server2.tool(
3396
3126
  "get_segment",
3397
- "Get details of a specific subscriber segment by ID.",
3127
+ "Get segment details. Set include_members=true to list members.",
3398
3128
  {
3399
- segment_id: z15.string().describe("The segment ID"),
3400
- expand: z15.string().optional().describe("Comma-separated expand fields")
3129
+ segment_id: z14.string().describe("The segment ID"),
3130
+ expand: z14.string().optional().describe("Comma-separated expand fields"),
3131
+ include_members: z14.boolean().optional().describe("Set to true to include the member list"),
3132
+ members_page: z14.string().optional().describe("Page number for members pagination")
3401
3133
  },
3402
3134
  {
3403
3135
  title: "Get Segment",
@@ -3409,92 +3141,49 @@ function registerNewsletterAdvancedTools(server2, client2) {
3409
3141
  async (args) => {
3410
3142
  const params = {};
3411
3143
  if (args.expand) params.expand = args.expand;
3412
- const result = await client2.getSegment(args.segment_id, params);
3144
+ const segment = await client2.getSegment(args.segment_id, params);
3145
+ if (!args.include_members) {
3146
+ return {
3147
+ content: [{ type: "text", text: JSON.stringify(segment, null, 2) }]
3148
+ };
3149
+ }
3150
+ const memberParams = {};
3151
+ if (args.members_page) memberParams.page = args.members_page;
3152
+ const members = await client2.request(
3153
+ "GET",
3154
+ `/api/v1/newsletters/segments/${args.segment_id}/members`,
3155
+ void 0,
3156
+ memberParams
3157
+ );
3413
3158
  return {
3414
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3159
+ content: [{
3160
+ type: "text",
3161
+ text: JSON.stringify({ segment, members }, null, 2)
3162
+ }]
3415
3163
  };
3416
3164
  }
3417
3165
  );
3418
3166
  server2.tool(
3419
- "get_segment_members",
3420
- "List subscribers who belong to a specific segment.",
3421
- {
3422
- segment_id: z15.string().describe("The segment ID"),
3423
- page: z15.string().optional().describe("Page number")
3424
- },
3167
+ "list_custom_fields",
3168
+ "List all custom subscriber fields defined in your ESP.",
3169
+ {},
3425
3170
  {
3426
- title: "Get Segment Members",
3171
+ title: "List Custom Fields",
3427
3172
  readOnlyHint: true,
3428
3173
  destructiveHint: false,
3429
3174
  idempotentHint: true,
3430
3175
  openWorldHint: true
3431
3176
  },
3432
- async (args) => {
3433
- const params = {};
3434
- if (args.page) params.page = args.page;
3435
- const result = await client2.getSegmentMembers(args.segment_id, params);
3177
+ async () => {
3178
+ const result = await client2.listCustomFields();
3436
3179
  return {
3437
3180
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3438
3181
  };
3439
3182
  }
3440
3183
  );
3441
3184
  server2.tool(
3442
- "list_custom_fields",
3443
- "List all custom subscriber fields defined in your ESP. Custom fields can store extra data like preferences, source, or company.",
3444
- {},
3445
- {
3446
- title: "List Custom Fields",
3447
- readOnlyHint: true,
3448
- destructiveHint: false,
3449
- idempotentHint: true,
3450
- openWorldHint: true
3451
- },
3452
- async () => {
3453
- const result = await client2.listCustomFields();
3454
- return {
3455
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3456
- };
3457
- }
3458
- );
3459
- server2.tool(
3460
- "get_referral_program",
3461
- "Get your newsletter's referral program details including milestones and rewards (Beehiiv only).",
3462
- {},
3463
- {
3464
- title: "Get Referral Program",
3465
- readOnlyHint: true,
3466
- destructiveHint: false,
3467
- idempotentHint: true,
3468
- openWorldHint: true
3469
- },
3470
- async () => {
3471
- const result = await client2.getReferralProgram();
3472
- return {
3473
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3474
- };
3475
- }
3476
- );
3477
- server2.tool(
3478
- "list_tiers",
3479
- "List all subscription tiers (free and premium) from your ESP (Beehiiv only).",
3480
- {},
3481
- {
3482
- title: "List Tiers",
3483
- readOnlyHint: true,
3484
- destructiveHint: false,
3485
- idempotentHint: true,
3486
- openWorldHint: true
3487
- },
3488
- async () => {
3489
- const result = await client2.listEspTiers();
3490
- return {
3491
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3492
- };
3493
- }
3494
- );
3495
- server2.tool(
3496
- "list_post_templates",
3497
- "List all post/newsletter templates available in your ESP (Beehiiv only).",
3185
+ "list_post_templates",
3186
+ "List all post/newsletter templates available in your ESP (Beehiiv only).",
3498
3187
  {},
3499
3188
  {
3500
3189
  title: "List Post Templates",
@@ -3512,7 +3201,7 @@ function registerNewsletterAdvancedTools(server2, client2) {
3512
3201
  );
3513
3202
  server2.tool(
3514
3203
  "get_post_aggregate_stats",
3515
- "Get aggregate statistics across all posts/newsletters including total clicks, impressions, and click rates (Beehiiv only).",
3204
+ "Get aggregate stats across all posts and newsletters.",
3516
3205
  {},
3517
3206
  {
3518
3207
  title: "Get Post Aggregate Stats",
@@ -3528,30 +3217,12 @@ function registerNewsletterAdvancedTools(server2, client2) {
3528
3217
  };
3529
3218
  }
3530
3219
  );
3531
- server2.tool(
3532
- "list_esp_webhooks",
3533
- "List all webhooks configured in your ESP for event notifications.",
3534
- {},
3535
- {
3536
- title: "List ESP Webhooks",
3537
- readOnlyHint: true,
3538
- destructiveHint: false,
3539
- idempotentHint: true,
3540
- openWorldHint: true
3541
- },
3542
- async () => {
3543
- const result = await client2.listEspWebhooks();
3544
- return {
3545
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3546
- };
3547
- }
3548
- );
3549
3220
  server2.tool(
3550
3221
  "tag_subscriber",
3551
- "Add tags to a subscriber. Tags help categorize subscribers for targeted sends and automations.",
3222
+ "Add tags to a subscriber.",
3552
3223
  {
3553
- subscriber_id: z15.string().describe("The subscriber/subscription ID"),
3554
- tags: z15.array(z15.string()).describe("Tags to add to the subscriber")
3224
+ subscriber_id: z14.string().describe("The subscriber/subscription ID"),
3225
+ tags: z14.array(z14.string()).describe("Tags to add to the subscriber")
3555
3226
  },
3556
3227
  {
3557
3228
  title: "Tag Subscriber",
@@ -3569,17 +3240,12 @@ function registerNewsletterAdvancedTools(server2, client2) {
3569
3240
  );
3570
3241
  server2.tool(
3571
3242
  "update_subscriber",
3572
- `Update a subscriber's details. On Beehiiv: update tier or custom fields. On Kit: update name or custom fields (tier not supported).
3573
-
3574
- REQUIRED WORKFLOW:
3575
- 1. ALWAYS set confirmed=false first to get a preview
3576
- 2. Show the user what will happen before confirming
3577
- 3. Only set confirmed=true after the user explicitly approves`,
3243
+ "Update subscriber details or custom fields. Set confirmed=false to preview first.",
3578
3244
  {
3579
- subscriber_id: z15.string().describe("The subscriber/subscription ID"),
3580
- tier: z15.string().optional().describe("New tier for the subscriber"),
3581
- custom_fields: z15.record(z15.unknown()).optional().describe("Custom field values to set"),
3582
- confirmed: z15.boolean().default(false).describe("Set to true to confirm update")
3245
+ subscriber_id: z14.string().describe("The subscriber/subscription ID"),
3246
+ tier: z14.string().optional().describe("New tier for the subscriber"),
3247
+ custom_fields: z14.record(z14.unknown()).optional().describe("Custom field values to set"),
3248
+ confirmed: z14.boolean().default(false).describe("Set to true to confirm update")
3583
3249
  },
3584
3250
  {
3585
3251
  title: "Update Subscriber",
@@ -3614,296 +3280,12 @@ Call again with confirmed=true to proceed.`
3614
3280
  };
3615
3281
  }
3616
3282
  );
3617
- server2.tool(
3618
- "create_custom_field",
3619
- `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.
3620
-
3621
- REQUIRED WORKFLOW:
3622
- 1. ALWAYS set confirmed=false first to get a preview
3623
- 2. Show the user what will happen before confirming
3624
- 3. Only set confirmed=true after the user explicitly approves`,
3625
- {
3626
- name: z15.string().describe("Field name"),
3627
- kind: z15.string().describe("Field type: string, integer, boolean, date, or enum"),
3628
- options: z15.array(z15.string()).optional().describe("Options for enum-type fields"),
3629
- confirmed: z15.boolean().default(false).describe("Set to true to confirm creation")
3630
- },
3631
- {
3632
- title: "Create Custom Field",
3633
- readOnlyHint: false,
3634
- destructiveHint: false,
3635
- idempotentHint: false,
3636
- openWorldHint: true
3637
- },
3638
- async (args) => {
3639
- if (args.confirmed !== true) {
3640
- const optionsLine = args.options ? `
3641
- **Options:** ${args.options.join(", ")}` : "";
3642
- return {
3643
- content: [{
3644
- type: "text",
3645
- text: `## Create Custom Field
3646
-
3647
- **Name:** ${args.name}
3648
- **Kind:** ${args.kind}${optionsLine}
3649
-
3650
- This creates a permanent schema-level field visible on all subscribers. Call again with confirmed=true to proceed.`
3651
- }]
3652
- };
3653
- }
3654
- const data = { name: args.name, kind: args.kind };
3655
- if (args.options) data.options = args.options;
3656
- const result = await client2.createCustomField(data);
3657
- return {
3658
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3659
- };
3660
- }
3661
- );
3662
- server2.tool(
3663
- "update_custom_field",
3664
- `Update an existing custom subscriber field's name or kind. Renaming or rekeying a field can break automations that reference it.
3665
-
3666
- REQUIRED WORKFLOW:
3667
- 1. ALWAYS set confirmed=false first to get a preview
3668
- 2. Show the user what will happen before confirming
3669
- 3. Only set confirmed=true after the user explicitly approves`,
3670
- {
3671
- field_id: z15.string().describe("The custom field ID"),
3672
- name: z15.string().optional().describe("New field name"),
3673
- kind: z15.string().optional().describe("New field type"),
3674
- confirmed: z15.boolean().default(false).describe("Set to true to confirm update")
3675
- },
3676
- {
3677
- title: "Update Custom Field",
3678
- readOnlyHint: false,
3679
- destructiveHint: false,
3680
- idempotentHint: true,
3681
- openWorldHint: true
3682
- },
3683
- async (args) => {
3684
- if (args.confirmed !== true) {
3685
- const changes = [];
3686
- if (args.name) changes.push(`**Name:** ${args.name}`);
3687
- if (args.kind) changes.push(`**Kind:** ${args.kind}`);
3688
- return {
3689
- content: [{
3690
- type: "text",
3691
- text: `## Update Custom Field
3692
-
3693
- **Field ID:** ${args.field_id}
3694
- ${changes.join("\n")}
3695
-
3696
- Renaming or changing the type of a field can break automations that reference it. Call again with confirmed=true to proceed.`
3697
- }]
3698
- };
3699
- }
3700
- const data = {};
3701
- if (args.name) data.name = args.name;
3702
- if (args.kind) data.kind = args.kind;
3703
- const result = await client2.updateCustomField(args.field_id, data);
3704
- return {
3705
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3706
- };
3707
- }
3708
- );
3709
- server2.tool(
3710
- "recalculate_segment",
3711
- "Trigger a recalculation of a segment's membership. Useful after changing subscriber data or segment rules (Beehiiv only).",
3712
- {
3713
- segment_id: z15.string().describe("The segment ID to recalculate")
3714
- },
3715
- {
3716
- title: "Recalculate Segment",
3717
- readOnlyHint: false,
3718
- destructiveHint: false,
3719
- idempotentHint: true,
3720
- openWorldHint: true
3721
- },
3722
- async (args) => {
3723
- await client2.recalculateSegment(args.segment_id);
3724
- return {
3725
- content: [{ type: "text", text: "Segment recalculation triggered." }]
3726
- };
3727
- }
3728
- );
3729
- server2.tool(
3730
- "enroll_in_automation",
3731
- `Enroll a subscriber in an automation/journey. Provide either an email or subscription ID. On Kit, enrolls into a sequence.
3732
-
3733
- REQUIRED WORKFLOW:
3734
- 1. ALWAYS set confirmed=false first to get a preview
3735
- 2. Show the user what will happen before confirming
3736
- 3. Only set confirmed=true after the user explicitly approves`,
3737
- {
3738
- automation_id: z15.string().describe("The automation ID"),
3739
- email: z15.string().optional().describe("Subscriber email to enroll"),
3740
- subscription_id: z15.string().optional().describe("Subscription ID to enroll"),
3741
- confirmed: z15.boolean().default(false).describe("Set to true to confirm enrollment")
3742
- },
3743
- {
3744
- title: "Enroll in Automation",
3745
- readOnlyHint: false,
3746
- destructiveHint: false,
3747
- idempotentHint: false,
3748
- openWorldHint: true
3749
- },
3750
- async (args) => {
3751
- if (args.confirmed !== true) {
3752
- const target = args.email ?? args.subscription_id ?? "unknown";
3753
- return {
3754
- content: [{
3755
- type: "text",
3756
- text: `## Enroll in Automation
3757
-
3758
- **Automation ID:** ${args.automation_id}
3759
- **Target:** ${target}
3760
-
3761
- This will add the subscriber to the automation journey. Call again with confirmed=true to proceed.`
3762
- }]
3763
- };
3764
- }
3765
- const data = {};
3766
- if (args.email) data.email = args.email;
3767
- if (args.subscription_id) data.subscriptionId = args.subscription_id;
3768
- await client2.enrollInAutomation(args.automation_id, data);
3769
- return {
3770
- content: [{ type: "text", text: "Subscriber enrolled in automation." }]
3771
- };
3772
- }
3773
- );
3774
- server2.tool(
3775
- "bulk_add_subscribers",
3776
- `Add multiple subscribers in a single request. Maximum 1000 subscribers per call.
3777
-
3778
- REQUIRED WORKFLOW:
3779
- 1. ALWAYS set confirmed=false first to get a preview
3780
- 2. Show the user what will happen before confirming
3781
- 3. Only set confirmed=true after the user explicitly approves`,
3782
- {
3783
- subscribers: z15.array(z15.object({
3784
- email: z15.string().describe("Subscriber email"),
3785
- data: z15.record(z15.unknown()).optional().describe("Additional subscriber data")
3786
- })).describe("Array of subscribers to add"),
3787
- confirmed: z15.boolean().default(false).describe("Set to true to confirm bulk add")
3788
- },
3789
- {
3790
- title: "Bulk Add Subscribers",
3791
- readOnlyHint: false,
3792
- destructiveHint: false,
3793
- idempotentHint: false,
3794
- openWorldHint: true
3795
- },
3796
- async (args) => {
3797
- if (args.confirmed !== true) {
3798
- return {
3799
- content: [{
3800
- type: "text",
3801
- text: `## Bulk Add Subscribers
3802
-
3803
- **Count:** ${args.subscribers.length} subscriber(s)
3804
- **Sample:** ${args.subscribers.slice(0, 3).map((s) => s.email).join(", ")}${args.subscribers.length > 3 ? "..." : ""}
3805
-
3806
- Call again with confirmed=true to proceed.`
3807
- }]
3808
- };
3809
- }
3810
- const result = await client2.bulkCreateSubscribers({ subscribers: args.subscribers });
3811
- return {
3812
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3813
- };
3814
- }
3815
- );
3816
- server2.tool(
3817
- "create_esp_webhook",
3818
- `Create a webhook in your ESP to receive event notifications. Note: Kit only supports one event per webhook.
3819
-
3820
- REQUIRED WORKFLOW:
3821
- 1. ALWAYS set confirmed=false first to get a preview
3822
- 2. Show the user what will happen before confirming
3823
- 3. Only set confirmed=true after the user explicitly approves`,
3824
- {
3825
- url: z15.string().describe("Webhook URL to receive events"),
3826
- event_types: z15.array(z15.string()).describe("Event types to subscribe to (e.g. 'subscription.created', 'post.sent')"),
3827
- confirmed: z15.boolean().default(false).describe("Set to true to confirm creation")
3828
- },
3829
- {
3830
- title: "Create ESP Webhook",
3831
- readOnlyHint: false,
3832
- destructiveHint: false,
3833
- idempotentHint: false,
3834
- openWorldHint: true
3835
- },
3836
- async (args) => {
3837
- if (args.confirmed !== true) {
3838
- return {
3839
- content: [{
3840
- type: "text",
3841
- text: `## Create ESP Webhook
3842
-
3843
- **URL:** ${args.url}
3844
- **Events:** ${args.event_types.join(", ")}
3845
-
3846
- Call again with confirmed=true to create.`
3847
- }]
3848
- };
3849
- }
3850
- const result = await client2.createEspWebhook({ url: args.url, eventTypes: args.event_types });
3851
- return {
3852
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3853
- };
3854
- }
3855
- );
3856
- server2.tool(
3857
- "delete_broadcast",
3858
- `Permanently delete a newsletter/broadcast. This action is permanent and cannot be undone.
3859
-
3860
- REQUIRED WORKFLOW:
3861
- 1. ALWAYS set confirmed=false first to get a preview
3862
- 2. Show the user what will happen before confirming
3863
- 3. Only set confirmed=true after the user explicitly approves`,
3864
- {
3865
- broadcast_id: z15.string().describe("The broadcast ID to delete"),
3866
- confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3867
- },
3868
- {
3869
- title: "Delete Broadcast",
3870
- readOnlyHint: false,
3871
- destructiveHint: true,
3872
- idempotentHint: true,
3873
- openWorldHint: true
3874
- },
3875
- async (args) => {
3876
- if (args.confirmed !== true) {
3877
- return {
3878
- content: [{
3879
- type: "text",
3880
- text: `## Delete Broadcast
3881
-
3882
- **Broadcast ID:** ${args.broadcast_id}
3883
-
3884
- **This action is permanent and cannot be undone.**
3885
-
3886
- Call again with confirmed=true to delete.`
3887
- }]
3888
- };
3889
- }
3890
- await client2.deleteBroadcast(args.broadcast_id);
3891
- return {
3892
- content: [{ type: "text", text: "Broadcast deleted." }]
3893
- };
3894
- }
3895
- );
3896
3283
  server2.tool(
3897
3284
  "delete_subscriber",
3898
- `Remove a subscriber from your mailing list. On Beehiiv this permanently deletes the subscriber. On Kit this unsubscribes them (Kit has no hard delete).
3899
-
3900
- REQUIRED WORKFLOW:
3901
- 1. ALWAYS set confirmed=false first to get a preview
3902
- 2. Show the user what will happen before confirming
3903
- 3. Only set confirmed=true after the user explicitly approves`,
3285
+ "Remove a subscriber. Set confirmed=false to preview first.",
3904
3286
  {
3905
- subscriber_id: z15.string().describe("The subscriber ID to delete"),
3906
- confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3287
+ subscriber_id: z14.string().describe("The subscriber ID to delete"),
3288
+ confirmed: z14.boolean().default(false).describe("Set to true to confirm deletion")
3907
3289
  },
3908
3290
  {
3909
3291
  title: "Delete Subscriber",
@@ -3933,347 +3315,6 @@ Call again with confirmed=true to delete.`
3933
3315
  };
3934
3316
  }
3935
3317
  );
3936
- server2.tool(
3937
- "delete_segment",
3938
- `Permanently delete a subscriber segment. This action is permanent and cannot be undone (Beehiiv only).
3939
-
3940
- REQUIRED WORKFLOW:
3941
- 1. ALWAYS set confirmed=false first to get a preview
3942
- 2. Show the user what will happen before confirming
3943
- 3. Only set confirmed=true after the user explicitly approves`,
3944
- {
3945
- segment_id: z15.string().describe("The segment ID to delete"),
3946
- confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3947
- },
3948
- {
3949
- title: "Delete Segment",
3950
- readOnlyHint: false,
3951
- destructiveHint: true,
3952
- idempotentHint: true,
3953
- openWorldHint: true
3954
- },
3955
- async (args) => {
3956
- if (args.confirmed !== true) {
3957
- return {
3958
- content: [{
3959
- type: "text",
3960
- text: `## Delete Segment
3961
-
3962
- **Segment ID:** ${args.segment_id}
3963
-
3964
- **This action is permanent and cannot be undone.** Subscribers in this segment will not be deleted, but the segment grouping will be removed.
3965
-
3966
- Call again with confirmed=true to delete.`
3967
- }]
3968
- };
3969
- }
3970
- await client2.deleteSegment(args.segment_id);
3971
- return {
3972
- content: [{ type: "text", text: "Segment deleted." }]
3973
- };
3974
- }
3975
- );
3976
- server2.tool(
3977
- "delete_custom_field",
3978
- `Permanently delete a custom subscriber field and remove it from all subscribers. This action is permanent and cannot be undone.
3979
-
3980
- REQUIRED WORKFLOW:
3981
- 1. ALWAYS set confirmed=false first to get a preview
3982
- 2. Show the user what will happen before confirming
3983
- 3. Only set confirmed=true after the user explicitly approves`,
3984
- {
3985
- field_id: z15.string().describe("The custom field ID to delete"),
3986
- confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
3987
- },
3988
- {
3989
- title: "Delete Custom Field",
3990
- readOnlyHint: false,
3991
- destructiveHint: true,
3992
- idempotentHint: true,
3993
- openWorldHint: true
3994
- },
3995
- async (args) => {
3996
- if (args.confirmed !== true) {
3997
- return {
3998
- content: [{
3999
- type: "text",
4000
- text: `## Delete Custom Field
4001
-
4002
- **Field ID:** ${args.field_id}
4003
-
4004
- **This action is permanent and cannot be undone.** The field and its data will be removed from all subscribers.
4005
-
4006
- Call again with confirmed=true to delete.`
4007
- }]
4008
- };
4009
- }
4010
- await client2.deleteCustomField(args.field_id);
4011
- return {
4012
- content: [{ type: "text", text: "Custom field deleted." }]
4013
- };
4014
- }
4015
- );
4016
- server2.tool(
4017
- "delete_esp_webhook",
4018
- `Permanently delete a webhook from your ESP. This action is permanent and cannot be undone.
4019
-
4020
- REQUIRED WORKFLOW:
4021
- 1. ALWAYS set confirmed=false first to get a preview
4022
- 2. Show the user what will happen before confirming
4023
- 3. Only set confirmed=true after the user explicitly approves`,
4024
- {
4025
- webhook_id: z15.string().describe("The webhook ID to delete"),
4026
- confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
4027
- },
4028
- {
4029
- title: "Delete ESP Webhook",
4030
- readOnlyHint: false,
4031
- destructiveHint: true,
4032
- idempotentHint: true,
4033
- openWorldHint: true
4034
- },
4035
- async (args) => {
4036
- if (args.confirmed !== true) {
4037
- return {
4038
- content: [{
4039
- type: "text",
4040
- text: `## Delete ESP Webhook
4041
-
4042
- **Webhook ID:** ${args.webhook_id}
4043
-
4044
- **This action is permanent and cannot be undone.** You will stop receiving event notifications at this webhook URL.
4045
-
4046
- Call again with confirmed=true to delete.`
4047
- }]
4048
- };
4049
- }
4050
- await client2.deleteEspWebhook(args.webhook_id);
4051
- return {
4052
- content: [{ type: "text", text: "Webhook deleted." }]
4053
- };
4054
- }
4055
- );
4056
- }
4057
-
4058
- // src/tools/carousel.ts
4059
- import { z as z16 } from "zod";
4060
- function registerCarouselTools(server2, client2) {
4061
- server2.tool(
4062
- "get_carousel_template",
4063
- "Get the customer's carousel template configuration. Returns brand colors, logo, CTA text, and other visual settings used to generate carousels from article URLs.",
4064
- {},
4065
- {
4066
- title: "Get Carousel Template",
4067
- readOnlyHint: true,
4068
- destructiveHint: false,
4069
- idempotentHint: true,
4070
- openWorldHint: false
4071
- },
4072
- async () => {
4073
- try {
4074
- const template = await client2.getCarouselTemplate();
4075
- const lines = [];
4076
- lines.push(`## Carousel Template: ${template.name || "Not configured"}`);
4077
- lines.push("");
4078
- if (!template.id) {
4079
- lines.push("No carousel template has been configured yet.");
4080
- lines.push("");
4081
- lines.push("Use `save_carousel_template` to set one up with your brand name, colors, and CTA text.");
4082
- return {
4083
- content: [{ type: "text", text: lines.join("\n") }]
4084
- };
4085
- }
4086
- lines.push(`**Brand Name:** ${template.brandName || "\u2014"}`);
4087
- if (template.logoUrl) lines.push(`**Logo:** ${template.logoUrl}`);
4088
- lines.push(`**Primary Color:** ${template.colorPrimary || "#1a1a2e"}`);
4089
- lines.push(`**Secondary Color:** ${template.colorSecondary || "#e94560"}`);
4090
- lines.push(`**Accent Color:** ${template.colorAccent || "#ffffff"}`);
4091
- lines.push(`**CTA Text:** ${template.ctaText || "Read the full story \u2192"}`);
4092
- lines.push(`**Logo Placement:** ${template.logoPlacement || "top-left"}`);
4093
- lines.push("");
4094
- return {
4095
- content: [{ type: "text", text: lines.join("\n") }]
4096
- };
4097
- } catch (error) {
4098
- const message = error instanceof Error ? error.message : "Unknown error";
4099
- if (message.includes("404")) {
4100
- return {
4101
- content: [
4102
- {
4103
- type: "text",
4104
- text: "No carousel template has been configured yet. Use `save_carousel_template` to set one up."
4105
- }
4106
- ]
4107
- };
4108
- }
4109
- throw error;
4110
- }
4111
- }
4112
- );
4113
- server2.tool(
4114
- "save_carousel_template",
4115
- `Create or update the customer's carousel template. This configures the visual style for auto-generated carousels (brand colors, logo, CTA text).
4116
-
4117
- USAGE:
4118
- - Set confirmed=false first to preview, then confirmed=true after user approval
4119
- - brand_name is required
4120
- - All color values should be hex codes (e.g. "#1a1a2e")
4121
- - logo_placement: "top-left", "top-right", or "top-center"`,
4122
- {
4123
- brand_name: z16.string().max(255).describe("Brand name displayed on CTA slide"),
4124
- color_primary: z16.string().max(20).optional().describe('Primary background color (hex, e.g. "#1a1a2e")'),
4125
- color_secondary: z16.string().max(20).optional().describe('Secondary/accent color for buttons and highlights (hex, e.g. "#e94560")'),
4126
- color_accent: z16.string().max(20).optional().describe('Text color (hex, e.g. "#ffffff")'),
4127
- cta_text: z16.string().max(255).optional().describe('Call-to-action text on the final slide (e.g. "Read the full story \u2192")'),
4128
- logo_placement: z16.enum(["top-left", "top-right", "top-center"]).optional().describe("Where to place the logo on slides"),
4129
- confirmed: z16.boolean().default(false).describe("Set to true to confirm and save. If false, returns a preview.")
4130
- },
4131
- {
4132
- title: "Save Carousel Template",
4133
- readOnlyHint: false,
4134
- destructiveHint: false,
4135
- idempotentHint: false,
4136
- openWorldHint: false
4137
- },
4138
- async (args) => {
4139
- const payload = {
4140
- brandName: args.brand_name
4141
- };
4142
- if (args.color_primary !== void 0) payload.colorPrimary = args.color_primary;
4143
- if (args.color_secondary !== void 0) payload.colorSecondary = args.color_secondary;
4144
- if (args.color_accent !== void 0) payload.colorAccent = args.color_accent;
4145
- if (args.cta_text !== void 0) payload.ctaText = args.cta_text;
4146
- if (args.logo_placement !== void 0) payload.logoPlacement = args.logo_placement;
4147
- if (args.confirmed !== true) {
4148
- const lines2 = [];
4149
- lines2.push("## Carousel Template Preview");
4150
- lines2.push("");
4151
- lines2.push(`**Brand Name:** ${args.brand_name}`);
4152
- if (args.color_primary) lines2.push(`**Primary Color:** ${args.color_primary}`);
4153
- if (args.color_secondary) lines2.push(`**Secondary Color:** ${args.color_secondary}`);
4154
- if (args.color_accent) lines2.push(`**Accent Color:** ${args.color_accent}`);
4155
- if (args.cta_text) lines2.push(`**CTA Text:** ${args.cta_text}`);
4156
- if (args.logo_placement) lines2.push(`**Logo Placement:** ${args.logo_placement}`);
4157
- lines2.push("");
4158
- lines2.push("---");
4159
- lines2.push("Call this tool again with **confirmed=true** to save these settings.");
4160
- return {
4161
- content: [{ type: "text", text: lines2.join("\n") }]
4162
- };
4163
- }
4164
- const result = await client2.updateCarouselTemplate(payload);
4165
- const lines = [];
4166
- lines.push(`Carousel template "${result.brandName || args.brand_name}" has been saved successfully.`);
4167
- lines.push("");
4168
- const updated = ["brand name"];
4169
- if (args.color_primary) updated.push("primary color");
4170
- if (args.color_secondary) updated.push("secondary color");
4171
- if (args.color_accent) updated.push("accent color");
4172
- if (args.cta_text) updated.push("CTA text");
4173
- if (args.logo_placement) updated.push("logo placement");
4174
- lines.push(`**Updated:** ${updated.join(", ")}`);
4175
- return {
4176
- content: [{ type: "text", text: lines.join("\n") }]
4177
- };
4178
- }
4179
- );
4180
- server2.tool(
4181
- "generate_carousel",
4182
- "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.",
4183
- {
4184
- url: z16.string().url().describe("The article URL to generate a carousel from")
4185
- },
4186
- {
4187
- title: "Generate Carousel",
4188
- readOnlyHint: false,
4189
- destructiveHint: false,
4190
- idempotentHint: false,
4191
- openWorldHint: true
4192
- },
4193
- async (args) => {
4194
- try {
4195
- const result = await client2.generateCarousel({
4196
- url: args.url
4197
- });
4198
- const lines = [];
4199
- lines.push("## Carousel Generated");
4200
- lines.push("");
4201
- lines.push(`**Headline:** ${result.article.headline}`);
4202
- lines.push(`**Source:** ${result.article.sourceDomain}`);
4203
- lines.push(`**Slides:** ${result.slides.length}`);
4204
- lines.push("");
4205
- lines.push("### Key Points");
4206
- for (const point of result.article.keyPoints) {
4207
- lines.push(`- ${point}`);
4208
- }
4209
- lines.push("");
4210
- lines.push("### Slide URLs");
4211
- for (const slide of result.slides) {
4212
- lines.push(`${slide.slideNumber}. [${slide.type}](${slide.cdnUrl})`);
4213
- }
4214
- lines.push("");
4215
- lines.push("These URLs can be used as `media_urls` when creating a post.");
4216
- return {
4217
- content: [{ type: "text", text: lines.join("\n") }]
4218
- };
4219
- } catch (error) {
4220
- const message = error instanceof Error ? error.message : "Unknown error";
4221
- return {
4222
- content: [
4223
- {
4224
- type: "text",
4225
- text: `Failed to generate carousel: ${message}`
4226
- }
4227
- ],
4228
- isError: true
4229
- };
4230
- }
4231
- }
4232
- );
4233
- server2.tool(
4234
- "preview_carousel",
4235
- "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.",
4236
- {
4237
- url: z16.string().url().describe("The article URL to preview")
4238
- },
4239
- {
4240
- title: "Preview Carousel",
4241
- readOnlyHint: true,
4242
- destructiveHint: false,
4243
- idempotentHint: false,
4244
- openWorldHint: true
4245
- },
4246
- async (args) => {
4247
- try {
4248
- const result = await client2.previewCarousel({
4249
- url: args.url
4250
- });
4251
- const lines = [];
4252
- lines.push("## Carousel Preview");
4253
- lines.push("");
4254
- lines.push(`**Headline:** ${result.article.headline}`);
4255
- lines.push(`**Source:** ${result.article.sourceDomain}`);
4256
- lines.push("");
4257
- lines.push(`**Hero Slide:** ${result.cdnUrl}`);
4258
- lines.push("");
4259
- lines.push("Use `generate_carousel` to create the full carousel with all slides.");
4260
- return {
4261
- content: [{ type: "text", text: lines.join("\n") }]
4262
- };
4263
- } catch (error) {
4264
- const message = error instanceof Error ? error.message : "Unknown error";
4265
- return {
4266
- content: [
4267
- {
4268
- type: "text",
4269
- text: `Failed to preview carousel: ${message}`
4270
- }
4271
- ],
4272
- isError: true
4273
- };
4274
- }
4275
- }
4276
- );
4277
3318
  }
4278
3319
 
4279
3320
  // src/index.ts
@@ -4289,10 +3330,15 @@ if (!apiKey) {
4289
3330
  process.exit(1);
4290
3331
  }
4291
3332
  var client = new BuzzPosterClient({ baseUrl: apiUrl, apiKey });
4292
- var server = new McpServer({
4293
- name: "buzzposter",
4294
- version: "0.1.0"
4295
- });
3333
+ var server = new McpServer(
3334
+ {
3335
+ name: "buzzposter",
3336
+ version: "0.1.0"
3337
+ },
3338
+ {
3339
+ instructions: `When a tool response contains \`NEWSLETTER_HTML_PREVIEW:\` followed by HTML content, immediately render that HTML as an artifact so the user sees a live visual preview of the newsletter. Do not ask the user if they want to see it -- just render it automatically.`
3340
+ }
3341
+ );
4296
3342
  var allowDirectSend = false;
4297
3343
  try {
4298
3344
  const account = await client.getAccount();
@@ -4314,9 +3360,7 @@ registerNewsletterTemplateTools(server, client);
4314
3360
  registerCalendarTools(server, client);
4315
3361
  registerNotificationTools(server, client);
4316
3362
  registerPublishingRulesTools(server, client);
4317
- registerAuditLogTools(server, client);
4318
3363
  registerSourceTools(server, client);
4319
3364
  registerNewsletterAdvancedTools(server, client);
4320
- registerCarouselTools(server, client);
4321
3365
  var transport = new StdioServerTransport();
4322
3366
  await server.connect(transport);