@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/tools.js CHANGED
@@ -59,9 +59,6 @@ var BuzzPosterClient = class {
59
59
  async deletePost(id) {
60
60
  return this.request("DELETE", `/api/v1/posts/${id}`);
61
61
  }
62
- async retryPost(id) {
63
- return this.request("POST", `/api/v1/posts/${id}/retry`);
64
- }
65
62
  // Analytics
66
63
  async getAnalytics(params) {
67
64
  return this.request("GET", "/api/v1/analytics", void 0, params);
@@ -95,32 +92,9 @@ var BuzzPosterClient = class {
95
92
  });
96
93
  }
97
94
  // Media
98
- async uploadMediaMultipart(filename, buffer, mimeType) {
99
- const formData = new FormData();
100
- formData.append("file", new Blob([buffer], { type: mimeType }), filename);
101
- const url = new URL(`${this.baseUrl}/api/v1/media/upload`);
102
- const res = await fetch(url, {
103
- method: "POST",
104
- headers: { Authorization: `Bearer ${this.apiKey}` },
105
- body: formData
106
- });
107
- if (!res.ok) {
108
- const errorBody = await res.json().catch(() => ({ message: res.statusText }));
109
- const message = typeof errorBody === "object" && errorBody !== null && "message" in errorBody ? String(errorBody.message) : `API error (${res.status})`;
110
- throw new Error(`BuzzPoster API error (${res.status}): ${message}`);
111
- }
112
- const text = await res.text();
113
- return text ? JSON.parse(text) : void 0;
114
- }
115
95
  async uploadFromUrl(data) {
116
96
  return this.request("POST", "/api/v1/media/upload-from-url", data);
117
97
  }
118
- async uploadBase64(data) {
119
- return this.request("POST", "/api/v1/media/upload-base64", data);
120
- }
121
- async getUploadUrl(data) {
122
- return this.request("POST", "/api/v1/media/presign", data);
123
- }
124
98
  async listMedia() {
125
99
  return this.request("GET", "/api/v1/media");
126
100
  }
@@ -159,9 +133,6 @@ var BuzzPosterClient = class {
159
133
  async listTags() {
160
134
  return this.request("GET", "/api/v1/newsletters/tags");
161
135
  }
162
- async listSequences() {
163
- return this.request("GET", "/api/v1/newsletters/sequences");
164
- }
165
136
  async listForms() {
166
137
  return this.request("GET", "/api/v1/newsletters/forms");
167
138
  }
@@ -172,36 +143,15 @@ var BuzzPosterClient = class {
172
143
  async listAutomations(params) {
173
144
  return this.request("GET", "/api/v1/newsletters/automations", void 0, params);
174
145
  }
175
- async enrollInAutomation(automationId, data) {
176
- return this.request("POST", `/api/v1/newsletters/automations/${automationId}/enroll`, data);
177
- }
178
146
  async listSegments(params) {
179
147
  return this.request("GET", "/api/v1/newsletters/segments", void 0, params);
180
148
  }
181
149
  async getSegment(id, params) {
182
150
  return this.request("GET", `/api/v1/newsletters/segments/${id}`, void 0, params);
183
151
  }
184
- async getSegmentMembers(segmentId, params) {
185
- return this.request("GET", `/api/v1/newsletters/segments/${segmentId}/members`, void 0, params);
186
- }
187
- async deleteSegment(id) {
188
- return this.request("DELETE", `/api/v1/newsletters/segments/${id}`);
189
- }
190
- async recalculateSegment(id) {
191
- return this.request("POST", `/api/v1/newsletters/segments/${id}/recalculate`);
192
- }
193
152
  async listCustomFields() {
194
153
  return this.request("GET", "/api/v1/newsletters/custom-fields");
195
154
  }
196
- async createCustomField(data) {
197
- return this.request("POST", "/api/v1/newsletters/custom-fields", data);
198
- }
199
- async updateCustomField(id, data) {
200
- return this.request("PUT", `/api/v1/newsletters/custom-fields/${id}`, data);
201
- }
202
- async deleteCustomField(id) {
203
- return this.request("DELETE", `/api/v1/newsletters/custom-fields/${id}`);
204
- }
205
155
  async tagSubscriber(subscriberId, data) {
206
156
  return this.request("POST", `/api/v1/newsletters/subscribers/${subscriberId}/tags`, data);
207
157
  }
@@ -211,33 +161,12 @@ var BuzzPosterClient = class {
211
161
  async deleteSubscriber(id) {
212
162
  return this.request("DELETE", `/api/v1/newsletters/subscribers/${id}`);
213
163
  }
214
- async bulkCreateSubscribers(data) {
215
- return this.request("POST", "/api/v1/newsletters/subscribers/bulk", data);
216
- }
217
- async getReferralProgram() {
218
- return this.request("GET", "/api/v1/newsletters/referral-program");
219
- }
220
- async listEspTiers() {
221
- return this.request("GET", "/api/v1/newsletters/tiers");
222
- }
223
164
  async listPostTemplates() {
224
165
  return this.request("GET", "/api/v1/newsletters/post-templates");
225
166
  }
226
167
  async getPostAggregateStats() {
227
168
  return this.request("GET", "/api/v1/newsletters/broadcasts/aggregate-stats");
228
169
  }
229
- async deleteBroadcast(id) {
230
- return this.request("DELETE", `/api/v1/newsletters/broadcasts/${id}`);
231
- }
232
- async listEspWebhooks() {
233
- return this.request("GET", "/api/v1/newsletters/webhooks");
234
- }
235
- async createEspWebhook(data) {
236
- return this.request("POST", "/api/v1/newsletters/webhooks", data);
237
- }
238
- async deleteEspWebhook(id) {
239
- return this.request("DELETE", `/api/v1/newsletters/webhooks/${id}`);
240
- }
241
170
  // Knowledge
242
171
  async listKnowledge(params) {
243
172
  return this.request("GET", "/api/v1/knowledge", void 0, params);
@@ -273,6 +202,18 @@ var BuzzPosterClient = class {
273
202
  async listTemplates() {
274
203
  return this.request("GET", "/api/v1/templates");
275
204
  }
205
+ async createTemplate(data) {
206
+ return this.request("POST", "/api/v1/templates", data);
207
+ }
208
+ async updateTemplate(id, data) {
209
+ return this.request("PUT", "/api/v1/templates/" + id, data);
210
+ }
211
+ async deleteTemplate(id) {
212
+ return this.request("DELETE", "/api/v1/templates/" + id);
213
+ }
214
+ async duplicateTemplate(id) {
215
+ return this.request("POST", "/api/v1/templates/" + id + "/duplicate");
216
+ }
276
217
  // Newsletter Archive
277
218
  async listNewsletterArchive(params) {
278
219
  return this.request("GET", "/api/v1/newsletter-archive", void 0, params);
@@ -344,10 +285,6 @@ var BuzzPosterClient = class {
344
285
  async getPublishingRules() {
345
286
  return this.request("GET", "/api/v1/publishing-rules");
346
287
  }
347
- // Audit Log
348
- async getAuditLog(params) {
349
- return this.request("GET", "/api/v1/audit-log", void 0, params);
350
- }
351
288
  // Newsletter Validation
352
289
  async validateNewsletter(data) {
353
290
  return this.request("POST", "/api/v1/newsletters/validate", data);
@@ -360,22 +297,6 @@ var BuzzPosterClient = class {
360
297
  async testBroadcast(id, data) {
361
298
  return this.request("POST", `/api/v1/newsletters/broadcasts/${id}/test`, data);
362
299
  }
363
- // Carousel templates
364
- async getCarouselTemplate() {
365
- return this.request("GET", "/api/v1/carousel-templates");
366
- }
367
- async updateCarouselTemplate(data) {
368
- return this.request("PUT", "/api/v1/carousel-templates", data);
369
- }
370
- async deleteCarouselTemplate() {
371
- return this.request("DELETE", "/api/v1/carousel-templates");
372
- }
373
- async generateCarousel(data) {
374
- return this.request("POST", "/api/v1/carousel-templates/generate", data);
375
- }
376
- async previewCarousel(data) {
377
- return this.request("POST", "/api/v1/carousel-templates/preview", data);
378
- }
379
300
  };
380
301
 
381
302
  // src/tools/posts.ts
@@ -383,23 +304,7 @@ import { z } from "zod";
383
304
  function registerPostTools(server, client) {
384
305
  server.tool(
385
306
  "post",
386
- `Create and publish a post to one or more social media platforms. Supports Twitter, Instagram, LinkedIn, and Facebook.
387
-
388
- Always show the user a preview of the content before calling this tool. Call get_brand_voice and get_audience before composing if not already loaded this session. Never call with confirmed=true without explicit user approval.
389
-
390
- REQUIRED WORKFLOW:
391
- 1. BEFORE creating any content, call get_brand_voice and get_audience to match the customer's tone
392
- 2. BEFORE publishing, call get_publishing_rules to check safety settings
393
- 3. ALWAYS set confirmed=false first to get a preview -- never set confirmed=true on the first call
394
- 4. Show the user a visual preview of the post before confirming
395
- 5. Only set confirmed=true after the user explicitly says to publish/post/send
396
- 6. If publishing_rules.social_default_action is 'draft', create as draft unless the user explicitly asks to publish
397
- 7. If publishing_rules.require_double_confirm_social is true, ask for confirmation twice before publishing
398
- 8. NEVER publish immediately without user confirmation, even if the user says "post this" -- always show the preview first
399
-
400
- When confirmed=false, this tool returns a structured preview with content, platform details, character counts, safety checks, and a confirmation message. You MUST render this as a visual preview for the user and wait for their explicit approval before calling again with confirmed=true.
401
-
402
- IMPORTANT: Always show the user a preview of the post content before publishing. Never publish without explicit user approval. If no preview has been shown yet, do NOT set confirmed=true -- render the preview first.`,
307
+ "Publish a post to one or more social platforms. Set confirmed=false to preview first.",
403
308
  {
404
309
  content: z.string().optional().describe("The text content of the post"),
405
310
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -491,118 +396,9 @@ Call this tool again with confirmed=true to proceed.`;
491
396
  };
492
397
  }
493
398
  );
494
- server.tool(
495
- "cross_post",
496
- `Post the same content to all connected platforms at once.
497
-
498
- Always show the user a preview of the content before calling this tool. Call get_brand_voice and get_audience before composing if not already loaded this session. Never call with confirmed=true without explicit user approval.
499
-
500
- REQUIRED WORKFLOW:
501
- 1. BEFORE creating content, call get_brand_voice and get_audience
502
- 2. BEFORE publishing, call get_publishing_rules
503
- 3. ALWAYS set confirmed=false first to preview
504
- 4. Show the user a visual preview showing how the post will appear on each platform
505
- 5. This is a high-impact action (goes to ALL platforms) -- always require explicit confirmation
506
- 6. If publishing_rules.require_double_confirm_social is true, ask twice
507
-
508
- When confirmed=false, this tool returns a structured preview with content, platform details, character counts, safety checks, and a confirmation message. You MUST render this as a visual preview for the user and wait for their explicit approval before calling again with confirmed=true.`,
509
- {
510
- content: z.string().describe("The text content to post everywhere"),
511
- media_urls: z.array(z.string()).optional().describe("Public URLs of media to attach"),
512
- confirmed: z.boolean().default(false).describe(
513
- "Set to true to confirm and publish. If false or missing, returns a preview for user approval."
514
- )
515
- },
516
- {
517
- title: "Cross-Post to All Platforms",
518
- readOnlyHint: false,
519
- destructiveHint: false,
520
- idempotentHint: false,
521
- openWorldHint: true
522
- },
523
- async (args) => {
524
- const accountsData = await client.listAccounts();
525
- const platforms = (accountsData.accounts ?? []).map(
526
- (a) => a.platform
527
- );
528
- if (platforms.length === 0) {
529
- return {
530
- content: [
531
- {
532
- type: "text",
533
- text: "No connected social accounts found. Connect accounts first."
534
- }
535
- ]
536
- };
537
- }
538
- if (args.confirmed !== true) {
539
- let rulesText = "";
540
- try {
541
- const rules = await client.getPublishingRules();
542
- const blockedWords = rules.blockedWords ?? [];
543
- const foundBlocked = [];
544
- if (args.content && blockedWords.length > 0) {
545
- const lower = args.content.toLowerCase();
546
- for (const word of blockedWords) {
547
- if (lower.includes(word.toLowerCase())) foundBlocked.push(word);
548
- }
549
- }
550
- rulesText = `
551
- ### Safety Checks
552
- `;
553
- rulesText += `- Blocked words: ${foundBlocked.length > 0 ? `**FAILED** (found: ${foundBlocked.join(", ")})` : "PASS"}
554
- `;
555
- rulesText += `- Default action: ${rules.socialDefaultAction ?? "draft"}
556
- `;
557
- rulesText += `- Double confirmation required: ${rules.requireDoubleConfirmSocial ? "yes" : "no"}
558
- `;
559
- } catch {
560
- }
561
- const preview = `## Cross-Post Preview
562
-
563
- **Content:** "${args.content}"
564
- **Platforms:** ${platforms.join(", ")} (${platforms.length} platforms)
565
- ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
566
- ` : "") + rulesText + `
567
- **Action:** This will be published **immediately** to **all ${platforms.length} connected platforms**. This is a high-impact action.
568
-
569
- Call this tool again with confirmed=true to proceed.`;
570
- return { content: [{ type: "text", text: preview }] };
571
- }
572
- const body = {
573
- content: args.content,
574
- platforms: platforms.map((p) => ({ platform: p })),
575
- publishNow: true
576
- };
577
- if (args.media_urls?.length) {
578
- body.mediaItems = args.media_urls.map((url) => ({
579
- type: url.match(/\.(mp4|mov|avi|webm)$/i) ? "video" : "image",
580
- url
581
- }));
582
- }
583
- const result = await client.createPost(body);
584
- return {
585
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
586
- };
587
- }
588
- );
589
399
  server.tool(
590
400
  "schedule_post",
591
- `Schedule a post for future publication.
592
-
593
- Always show the user a preview of the content before calling this tool. Call get_brand_voice and get_audience before composing if not already loaded this session. Never call with confirmed=true without explicit user approval.
594
-
595
- REQUIRED WORKFLOW:
596
- 1. BEFORE creating content, call get_brand_voice and get_audience
597
- 2. BEFORE scheduling, call get_publishing_rules
598
- 3. ALWAYS set confirmed=false first to preview
599
- 4. Show the user a visual preview with the scheduled date/time before confirming
600
- 5. Only confirm after explicit user approval
601
- 6. If the user hasn't specified a time, suggest one based on get_queue time slots
602
-
603
- When confirmed=false, this tool returns a structured preview with content, platform details, character counts, safety checks, and a confirmation message. You MUST render this as a visual preview for the user and wait for their explicit approval before calling again with confirmed=true.
604
-
605
- IMPORTANT: Always show the user a preview of the post content before scheduling. Never schedule without explicit user approval. If no preview has been shown yet, do NOT set confirmed=true -- render the preview first.`,
401
+ "Schedule a post for future publication. Set confirmed=false to preview first.",
606
402
  {
607
403
  content: z.string().optional().describe("The text content of the post"),
608
404
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -678,7 +474,7 @@ Call this tool again with confirmed=true to schedule.`;
678
474
  );
679
475
  server.tool(
680
476
  "create_draft",
681
- "Save a post as a draft without publishing. This NEVER publishes \u2014 it only saves. No confirmation needed since nothing goes live.",
477
+ "Save a post as a draft without publishing. No confirmation needed.",
682
478
  {
683
479
  content: z.string().optional().describe("The text content of the draft"),
684
480
  platforms: z.array(z.string()).describe("Platforms this draft is intended for"),
@@ -721,7 +517,7 @@ Call this tool again with confirmed=true to schedule.`;
721
517
  );
722
518
  server.tool(
723
519
  "list_posts",
724
- "List recent posts with their status and platform details.",
520
+ "List recent posts with status and platform details.",
725
521
  {
726
522
  status: z.string().optional().describe("Filter by status: published, scheduled, draft, failed"),
727
523
  limit: z.string().optional().describe("Number of posts to return")
@@ -745,7 +541,7 @@ Call this tool again with confirmed=true to schedule.`;
745
541
  );
746
542
  server.tool(
747
543
  "get_post",
748
- "Get detailed information about a specific post.",
544
+ "Get detailed information about a specific post by ID.",
749
545
  {
750
546
  post_id: z.string().describe("The ID of the post to retrieve")
751
547
  },
@@ -763,117 +559,13 @@ Call this tool again with confirmed=true to schedule.`;
763
559
  };
764
560
  }
765
561
  );
766
- server.tool(
767
- "retry_post",
768
- "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.",
769
- {
770
- post_id: z.string().describe("The ID of the post to retry"),
771
- confirmed: z.boolean().default(false).describe(
772
- "Set to true to confirm retry. If false/missing, shows post details first."
773
- )
774
- },
775
- {
776
- title: "Retry Failed Post",
777
- readOnlyHint: false,
778
- destructiveHint: false,
779
- idempotentHint: true,
780
- openWorldHint: true
781
- },
782
- async (args) => {
783
- if (args.confirmed !== true) {
784
- const post = await client.getPost(args.post_id);
785
- const platforms = post.platforms ?? [];
786
- const failed = platforms.filter((p) => p.status === "failed");
787
- const published = platforms.filter((p) => p.status === "published");
788
- let text = `## Retry Post Preview
789
-
790
- `;
791
- text += `**Post ID:** ${args.post_id}
792
- `;
793
- text += `**Content:** "${(post.content ?? "").substring(0, 100)}"
794
- `;
795
- text += `**Status:** ${post.status}
796
-
797
- `;
798
- if (published.length > 0) {
799
- text += `**Already published on:** ${published.map((p) => p.platform).join(", ")}
800
- `;
801
- }
802
- if (failed.length > 0) {
803
- text += `**Failed on:** ${failed.map((p) => `${p.platform} (${p.error ?? "unknown error"})`).join(", ")}
804
- `;
805
- }
806
- text += `
807
- Retrying will only attempt the failed platforms. Call this tool again with confirmed=true to proceed.`;
808
- return { content: [{ type: "text", text }] };
809
- }
810
- const result = await client.retryPost(args.post_id);
811
- return {
812
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
813
- };
814
- }
815
- );
816
562
  }
817
563
 
818
564
  // src/tools/accounts.ts
819
565
  function registerAccountTools(server, client) {
820
- server.tool(
821
- "list_accounts",
822
- "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.",
823
- {},
824
- {
825
- title: "List Connected Accounts",
826
- readOnlyHint: true,
827
- destructiveHint: false,
828
- idempotentHint: true,
829
- openWorldHint: false
830
- },
831
- async () => {
832
- const result = await client.listAccounts();
833
- const accounts = result.accounts ?? [];
834
- const esp = result.esp;
835
- if (accounts.length === 0 && !esp) {
836
- return {
837
- content: [{
838
- type: "text",
839
- text: "## Connected Accounts\n\nNo accounts connected. Connect social media accounts and/or an email service provider via the dashboard."
840
- }]
841
- };
842
- }
843
- let text = "## Connected Accounts\n\n";
844
- text += "### Social Media\n";
845
- if (accounts.length > 0) {
846
- for (const a of accounts) {
847
- const icon = a.isActive ? "\u2705" : "\u274C";
848
- const platform = a.platform.charAt(0).toUpperCase() + a.platform.slice(1);
849
- const name = a.username || a.displayName || a.platform;
850
- text += `${icon} **${platform}** \u2014 @${name}
851
- `;
852
- }
853
- } else {
854
- text += "No social accounts connected.\n";
855
- }
856
- text += "\n### Email Service Provider (ESP)\n";
857
- if (esp && esp.connected) {
858
- const provider = esp.provider.charAt(0).toUpperCase() + esp.provider.slice(1);
859
- text += `\u2705 **${provider}** \u2014 Connected`;
860
- if (esp.publicationId) text += ` (Publication: ${esp.publicationId})`;
861
- text += "\n";
862
- } else if (esp) {
863
- const provider = esp.provider.charAt(0).toUpperCase() + esp.provider.slice(1);
864
- text += `\u26A0\uFE0F **${provider}** \u2014 Provider set but API key missing
865
- `;
866
- } else {
867
- text += "No ESP configured.\n";
868
- }
869
- return {
870
- content: [{ type: "text", text }]
871
- };
872
- }
873
- );
874
566
  server.tool(
875
567
  "check_accounts_health",
876
- "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.",
568
+ "Check health status of connected social accounts. Direct user to BuzzPoster web UI to reconnect broken accounts.",
877
569
  {},
878
570
  {
879
571
  title: "Check Account Health",
@@ -926,7 +618,7 @@ import { z as z2 } from "zod";
926
618
  function registerAnalyticsTools(server, client) {
927
619
  server.tool(
928
620
  "get_analytics",
929
- "Get performance analytics for your social media posts. Supports filtering by platform and date range.",
621
+ "Get social media post performance analytics. Filter by platform and date range.",
930
622
  {
931
623
  platform: z2.string().optional().describe("Filter by platform: twitter, instagram, linkedin, facebook"),
932
624
  from_date: z2.string().optional().describe("Start date for analytics range (ISO 8601)"),
@@ -957,7 +649,7 @@ import { z as z3 } from "zod";
957
649
  function registerInboxTools(server, client) {
958
650
  server.tool(
959
651
  "list_conversations",
960
- "List DM conversations across all connected social media platforms.",
652
+ "List DM conversations across all connected platforms.",
961
653
  {
962
654
  platform: z3.string().optional().describe("Filter by platform: twitter, instagram, linkedin, facebook")
963
655
  },
@@ -999,12 +691,7 @@ function registerInboxTools(server, client) {
999
691
  );
1000
692
  server.tool(
1001
693
  "reply_to_conversation",
1002
- `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.
1003
-
1004
- REQUIRED WORKFLOW:
1005
- 1. ALWAYS set confirmed=false first to preview the reply
1006
- 2. Show the user the reply text and which platform/conversation it will be sent to
1007
- 3. Only confirm after explicit user approval`,
694
+ "Send a DM reply. Set confirmed=false to preview first.",
1008
695
  {
1009
696
  conversation_id: z3.string().describe("The conversation ID to reply to"),
1010
697
  message: z3.string().describe("The reply message text"),
@@ -1060,12 +747,7 @@ This will send a DM reply on the external platform. Call this tool again with co
1060
747
  );
1061
748
  server.tool(
1062
749
  "reply_to_comment",
1063
- `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.
1064
-
1065
- REQUIRED WORKFLOW:
1066
- 1. ALWAYS set confirmed=false first to preview
1067
- 2. Show the user the reply and the original comment for context
1068
- 3. Only confirm after explicit user approval -- public replies are visible to everyone`,
750
+ "Reply to a comment publicly. Set confirmed=false to preview first.",
1069
751
  {
1070
752
  comment_id: z3.string().describe("The comment ID to reply to"),
1071
753
  message: z3.string().describe("The reply text"),
@@ -1117,12 +799,7 @@ This will post a public reply. Call this tool again with confirmed=true to send.
1117
799
  );
1118
800
  server.tool(
1119
801
  "reply_to_review",
1120
- `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.
1121
-
1122
- REQUIRED WORKFLOW:
1123
- 1. ALWAYS set confirmed=false first to preview
1124
- 2. Show the user the reply, the original review, and the star rating for context
1125
- 3. Only confirm after explicit user approval -- public replies represent the brand`,
802
+ "Reply to a Facebook review publicly. Set confirmed=false to preview first.",
1126
803
  {
1127
804
  review_id: z3.string().describe("The review ID to reply to"),
1128
805
  message: z3.string().describe("The reply text"),
@@ -1158,75 +835,10 @@ This will post a public reply to the review. Call this tool again with confirmed
1158
835
 
1159
836
  // src/tools/media.ts
1160
837
  import { z as z4 } from "zod";
1161
- import { readFile } from "fs/promises";
1162
838
  function registerMediaTools(server, client) {
1163
- server.tool(
1164
- "upload_media",
1165
- "Upload an image or video file from a local file path. Returns a CDN URL that can be used in posts.",
1166
- {
1167
- file_path: z4.string().describe("Absolute path to the file on the local filesystem")
1168
- },
1169
- {
1170
- title: "Upload Media File",
1171
- readOnlyHint: false,
1172
- destructiveHint: false,
1173
- idempotentHint: false,
1174
- openWorldHint: false
1175
- },
1176
- async (args) => {
1177
- const buffer = Buffer.from(await readFile(args.file_path));
1178
- const filename = args.file_path.split("/").pop() ?? "upload";
1179
- const ext = filename.split(".").pop()?.toLowerCase() ?? "";
1180
- const mimeMap = {
1181
- jpg: "image/jpeg",
1182
- jpeg: "image/jpeg",
1183
- png: "image/png",
1184
- gif: "image/gif",
1185
- webp: "image/webp",
1186
- mp4: "video/mp4",
1187
- mov: "video/quicktime",
1188
- avi: "video/x-msvideo",
1189
- webm: "video/webm",
1190
- pdf: "application/pdf"
1191
- };
1192
- const mimeType = mimeMap[ext] ?? "application/octet-stream";
1193
- const result = await client.uploadMediaMultipart(filename, buffer, mimeType);
1194
- return {
1195
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1196
- };
1197
- }
1198
- );
1199
- server.tool(
1200
- "upload_from_claude",
1201
- "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.",
1202
- {
1203
- data: z4.string().describe("Base64-encoded image data (no data URI prefix needed, just the raw base64 string)"),
1204
- filename: z4.string().describe("Filename including extension (e.g. photo.jpg)"),
1205
- content_type: z4.string().describe("MIME type (e.g. image/jpeg, image/png, image/webp, image/gif)"),
1206
- folder: z4.string().optional().describe("Optional folder path within the customer's storage")
1207
- },
1208
- {
1209
- title: "Upload Image from Claude",
1210
- readOnlyHint: false,
1211
- destructiveHint: false,
1212
- idempotentHint: false,
1213
- openWorldHint: false
1214
- },
1215
- async (args) => {
1216
- const result = await client.uploadBase64({
1217
- data: args.data,
1218
- filename: args.filename,
1219
- content_type: args.content_type,
1220
- folder: args.folder
1221
- });
1222
- return {
1223
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1224
- };
1225
- }
1226
- );
1227
839
  server.tool(
1228
840
  "upload_from_url",
1229
- "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.",
841
+ "Upload media from a public URL to storage. Returns a CDN URL for use in posts.",
1230
842
  {
1231
843
  url: z4.string().url().describe("Public URL of the image or video to upload"),
1232
844
  filename: z4.string().optional().describe("Optional filename override (including extension)"),
@@ -1245,38 +857,18 @@ function registerMediaTools(server, client) {
1245
857
  filename: args.filename,
1246
858
  folder: args.folder
1247
859
  });
860
+ const item = result;
1248
861
  return {
1249
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1250
- };
1251
- }
1252
- );
1253
- server.tool(
1254
- "get_upload_url",
1255
- "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.",
1256
- {
1257
- filename: z4.string().describe("The filename including extension (e.g. photo.jpg)"),
1258
- content_type: z4.string().describe("MIME type of the file (e.g. image/jpeg, video/mp4)")
1259
- },
1260
- {
1261
- title: "Get Upload URL",
1262
- readOnlyHint: true,
1263
- destructiveHint: false,
1264
- idempotentHint: false,
1265
- openWorldHint: false
1266
- },
1267
- async (args) => {
1268
- const result = await client.getUploadUrl({
1269
- filename: args.filename,
1270
- content_type: args.content_type
1271
- });
1272
- return {
1273
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
862
+ content: [
863
+ { type: "text", text: JSON.stringify(result, null, 2) },
864
+ ...item.type === "image" && item.url && item.mimeType && !item.mimeType.startsWith("video/") ? [{ type: "image", url: item.url, mimeType: item.mimeType }] : []
865
+ ]
1274
866
  };
1275
867
  }
1276
868
  );
1277
869
  server.tool(
1278
870
  "list_media",
1279
- "List all uploaded media files in your BuzzPoster media library.",
871
+ "List all uploaded media files.",
1280
872
  {},
1281
873
  {
1282
874
  title: "List Media Library",
@@ -1287,19 +879,19 @@ function registerMediaTools(server, client) {
1287
879
  },
1288
880
  async () => {
1289
881
  const result = await client.listMedia();
882
+ const media = Array.isArray(result) ? result : result?.media ?? [];
883
+ 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 }));
1290
884
  return {
1291
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
885
+ content: [
886
+ { type: "text", text: JSON.stringify(result, null, 2) },
887
+ ...imageBlocks
888
+ ]
1292
889
  };
1293
890
  }
1294
891
  );
1295
892
  server.tool(
1296
893
  "delete_media",
1297
- `Delete a media file from the customer's BuzzPoster media library. This cannot be undone.
1298
-
1299
- REQUIRED WORKFLOW:
1300
- 1. ALWAYS set confirmed=false first
1301
- 2. Show the user which file will be deleted (filename, URL, size)
1302
- 3. Only confirm after explicit approval`,
894
+ "Delete a media file. Set confirmed=false to preview first.",
1303
895
  {
1304
896
  key: z4.string().describe("The key/path of the media file to delete"),
1305
897
  confirmed: z4.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
@@ -1333,7 +925,7 @@ import { z as z5 } from "zod";
1333
925
  function registerNewsletterTools(server, client, options = {}) {
1334
926
  server.tool(
1335
927
  "list_subscribers",
1336
- "List email subscribers from your configured email service provider (Kit, Beehiiv, or Mailchimp).",
928
+ "List email subscribers from the connected ESP.",
1337
929
  {
1338
930
  page: z5.string().optional().describe("Page number for pagination"),
1339
931
  per_page: z5.string().optional().describe("Number of subscribers per page")
@@ -1357,7 +949,7 @@ function registerNewsletterTools(server, client, options = {}) {
1357
949
  );
1358
950
  server.tool(
1359
951
  "add_subscriber",
1360
- "Add a new email subscriber to your mailing list.",
952
+ "Add a new subscriber to the mailing list.",
1361
953
  {
1362
954
  email: z5.string().describe("Email address of the subscriber"),
1363
955
  first_name: z5.string().optional().describe("Subscriber's first name"),
@@ -1383,7 +975,7 @@ function registerNewsletterTools(server, client, options = {}) {
1383
975
  );
1384
976
  server.tool(
1385
977
  "create_newsletter",
1386
- `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.`,
978
+ "Push a newsletter to the user's ESP as a draft. Show an HTML preview to the user before calling this.",
1387
979
  {
1388
980
  subject: z5.string().describe("Email subject line"),
1389
981
  content: z5.string().describe("HTML content of the newsletter"),
@@ -1403,13 +995,17 @@ function registerNewsletterTools(server, client, options = {}) {
1403
995
  previewText: args.preview_text
1404
996
  });
1405
997
  return {
1406
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
998
+ content: [
999
+ { type: "text", text: JSON.stringify(result, null, 2) },
1000
+ ...args.content ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
1001
+ ${args.content}` }] : []
1002
+ ]
1407
1003
  };
1408
1004
  }
1409
1005
  );
1410
1006
  server.tool(
1411
1007
  "update_newsletter",
1412
- `Update an existing newsletter/broadcast draft. After updating, show the user a visual preview of the changes.`,
1008
+ "Update an existing newsletter draft.",
1413
1009
  {
1414
1010
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to update"),
1415
1011
  subject: z5.string().optional().describe("Updated subject line"),
@@ -1430,27 +1026,18 @@ function registerNewsletterTools(server, client, options = {}) {
1430
1026
  if (args.preview_text) data.previewText = args.preview_text;
1431
1027
  const result = await client.updateBroadcast(args.broadcast_id, data);
1432
1028
  return {
1433
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1029
+ content: [
1030
+ { type: "text", text: JSON.stringify(result, null, 2) },
1031
+ ...args.content ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
1032
+ ${args.content}` }] : []
1033
+ ]
1434
1034
  };
1435
1035
  }
1436
1036
  );
1437
1037
  if (options.allowDirectSend) {
1438
1038
  server.tool(
1439
1039
  "send_newsletter",
1440
- `Send a newsletter/broadcast to subscribers. THIS ACTION CANNOT BE UNDONE. Once sent, the email goes to every subscriber on the list.
1441
-
1442
- REQUIRED WORKFLOW:
1443
- 1. ALWAYS call get_publishing_rules first
1444
- 2. NEVER send without showing a full visual preview of the newsletter first
1445
- 3. ALWAYS set confirmed=false first -- this returns a confirmation prompt, not a send
1446
- 4. Tell the user exactly how many subscribers will receive this email
1447
- 5. If publishing_rules.require_double_confirm_newsletter is true (default), require TWO explicit confirmations:
1448
- - First: "Are you sure you want to send this to [X] subscribers?"
1449
- - Second: "This is irreversible. Type 'send' to confirm."
1450
- 6. If publishing_rules.allow_immediate_send is false and the user asks to send immediately, suggest scheduling instead
1451
- 7. NEVER set confirmed=true without the user explicitly confirming after seeing the preview and subscriber count
1452
-
1453
- 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.`,
1040
+ "Send a newsletter to subscribers. This action cannot be undone. Set confirmed=false to preview first.",
1454
1041
  {
1455
1042
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to send"),
1456
1043
  confirmed: z5.boolean().default(false).describe(
@@ -1514,22 +1101,7 @@ Call this tool again with confirmed=true to send.`;
1514
1101
  }
1515
1102
  server.tool(
1516
1103
  "schedule_newsletter",
1517
- `Schedule a newsletter/broadcast to be sent at a future time.
1518
-
1519
- REQUIRED WORKFLOW:
1520
- 1. ALWAYS call get_publishing_rules first
1521
- 2. NEVER schedule without showing a full visual preview of the newsletter first
1522
- 3. ALWAYS set confirmed=false first -- this returns a confirmation prompt with details, not an actual schedule
1523
- 4. Tell the user exactly how many subscribers will receive this email and the scheduled send time
1524
- 5. If publishing_rules.require_double_confirm_newsletter is true (default), require TWO explicit confirmations:
1525
- - First: "Are you sure you want to schedule this for [time] to [X] subscribers?"
1526
- - Second: "Confirm schedule for [time]."
1527
- 6. If publishing_rules.allow_immediate_send is false and the scheduled time is very soon, warn the user
1528
- 7. NEVER set confirmed=true without the user explicitly confirming after seeing the preview, subscriber count, and scheduled time
1529
-
1530
- Scheduling can typically be cancelled or rescheduled later, but the user should still verify the time is correct before confirming.
1531
-
1532
- 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.`,
1104
+ "Schedule a newsletter for future send. Set confirmed=false to preview first.",
1533
1105
  {
1534
1106
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to schedule"),
1535
1107
  scheduled_for: z5.string().describe(
@@ -1605,9 +1177,13 @@ Call this tool again with confirmed=true to schedule.`;
1605
1177
  );
1606
1178
  server.tool(
1607
1179
  "list_newsletters",
1608
- "List all newsletters/broadcasts with their status.",
1180
+ "List newsletters with status, dates, and subject. Supports filtering by status, date range, and keyword.",
1609
1181
  {
1610
- page: z5.string().optional().describe("Page number for pagination")
1182
+ page: z5.string().optional().describe("Page number for pagination"),
1183
+ status: z5.enum(["draft", "sent", "scheduled"]).optional().describe("Filter by status: draft, sent, or scheduled"),
1184
+ from_date: z5.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
1185
+ to_date: z5.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
1186
+ subject: z5.string().optional().describe("Keyword to search in subject lines (case-insensitive)")
1611
1187
  },
1612
1188
  {
1613
1189
  title: "List Newsletters",
@@ -1619,15 +1195,37 @@ Call this tool again with confirmed=true to schedule.`;
1619
1195
  async (args) => {
1620
1196
  const params = {};
1621
1197
  if (args.page) params.page = args.page;
1198
+ if (args.status) params.status = args.status;
1199
+ if (args.from_date) params.fromDate = args.from_date;
1200
+ if (args.to_date) params.toDate = args.to_date;
1201
+ if (args.subject) params.subject = args.subject;
1622
1202
  const result = await client.listBroadcasts(params);
1623
- return {
1624
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1203
+ const broadcasts = result?.broadcasts ?? [];
1204
+ if (broadcasts.length === 0) {
1205
+ return {
1206
+ content: [{ type: "text", text: "No newsletters found matching your filters." }]
1207
+ };
1208
+ }
1209
+ let text = `## Newsletters (${broadcasts.length}`;
1210
+ if (result?.totalCount != null) text += ` of ${result.totalCount}`;
1211
+ text += ")\n\n";
1212
+ for (const b of broadcasts) {
1213
+ const status = (b.status ?? "unknown").toUpperCase();
1214
+ const date = b.sentAt ? new Date(b.sentAt).toLocaleString() : b.createdAt ? new Date(b.createdAt).toLocaleString() : "";
1215
+ text += `- **[${status}]** "${b.subject ?? "(no subject)"}"
1216
+ `;
1217
+ text += ` ID: ${b.id}`;
1218
+ if (date) text += ` | ${b.sentAt ? "Sent" : "Created"}: ${date}`;
1219
+ text += "\n";
1220
+ }
1221
+ return {
1222
+ content: [{ type: "text", text }]
1625
1223
  };
1626
1224
  }
1627
1225
  );
1628
1226
  server.tool(
1629
1227
  "list_tags",
1630
- "List all subscriber tags from your email service provider.",
1228
+ "List all subscriber tags.",
1631
1229
  {},
1632
1230
  {
1633
1231
  title: "List Subscriber Tags",
@@ -1643,27 +1241,9 @@ Call this tool again with confirmed=true to schedule.`;
1643
1241
  };
1644
1242
  }
1645
1243
  );
1646
- server.tool(
1647
- "list_sequences",
1648
- "List all email sequences/automations from your email service provider. On Beehiiv, this returns automations. For detailed automation data use list_automations instead.",
1649
- {},
1650
- {
1651
- title: "List Email Sequences",
1652
- readOnlyHint: true,
1653
- destructiveHint: false,
1654
- idempotentHint: true,
1655
- openWorldHint: true
1656
- },
1657
- async () => {
1658
- const result = await client.listSequences();
1659
- return {
1660
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1661
- };
1662
- }
1663
- );
1664
1244
  server.tool(
1665
1245
  "list_forms",
1666
- "List all signup forms from your email service provider.",
1246
+ "List all signup forms.",
1667
1247
  {},
1668
1248
  {
1669
1249
  title: "List Signup Forms",
@@ -1686,7 +1266,7 @@ import { z as z6 } from "zod";
1686
1266
  function registerNewsletterAdvancedTools(server, client) {
1687
1267
  server.tool(
1688
1268
  "get_subscriber_by_email",
1689
- "Look up a subscriber by their email address. Returns subscriber details including tags and custom fields.",
1269
+ "Look up a subscriber by email address.",
1690
1270
  {
1691
1271
  email: z6.string().describe("Email address to look up")
1692
1272
  },
@@ -1706,7 +1286,7 @@ function registerNewsletterAdvancedTools(server, client) {
1706
1286
  );
1707
1287
  server.tool(
1708
1288
  "list_automations",
1709
- "List all email automations/workflows from your ESP. On Beehiiv these are journeys; on Kit these are sequences.",
1289
+ "List all email automations and sequences.",
1710
1290
  {
1711
1291
  page: z6.string().optional().describe("Page number for pagination")
1712
1292
  },
@@ -1728,7 +1308,7 @@ function registerNewsletterAdvancedTools(server, client) {
1728
1308
  );
1729
1309
  server.tool(
1730
1310
  "list_segments",
1731
- "List subscriber segments. Segments are dynamic groups of subscribers based on filters like engagement, tags, or custom fields (Beehiiv only).",
1311
+ "List subscriber segments.",
1732
1312
  {
1733
1313
  page: z6.string().optional().describe("Page number"),
1734
1314
  type: z6.string().optional().describe("Filter by segment type"),
@@ -1756,10 +1336,12 @@ function registerNewsletterAdvancedTools(server, client) {
1756
1336
  );
1757
1337
  server.tool(
1758
1338
  "get_segment",
1759
- "Get details of a specific subscriber segment by ID.",
1339
+ "Get segment details. Set include_members=true to list members.",
1760
1340
  {
1761
1341
  segment_id: z6.string().describe("The segment ID"),
1762
- expand: z6.string().optional().describe("Comma-separated expand fields")
1342
+ expand: z6.string().optional().describe("Comma-separated expand fields"),
1343
+ include_members: z6.boolean().optional().describe("Set to true to include the member list"),
1344
+ members_page: z6.string().optional().describe("Page number for members pagination")
1763
1345
  },
1764
1346
  {
1765
1347
  title: "Get Segment",
@@ -1771,38 +1353,31 @@ function registerNewsletterAdvancedTools(server, client) {
1771
1353
  async (args) => {
1772
1354
  const params = {};
1773
1355
  if (args.expand) params.expand = args.expand;
1774
- const result = await client.getSegment(args.segment_id, params);
1775
- return {
1776
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1777
- };
1778
- }
1779
- );
1780
- server.tool(
1781
- "get_segment_members",
1782
- "List subscribers who belong to a specific segment.",
1783
- {
1784
- segment_id: z6.string().describe("The segment ID"),
1785
- page: z6.string().optional().describe("Page number")
1786
- },
1787
- {
1788
- title: "Get Segment Members",
1789
- readOnlyHint: true,
1790
- destructiveHint: false,
1791
- idempotentHint: true,
1792
- openWorldHint: true
1793
- },
1794
- async (args) => {
1795
- const params = {};
1796
- if (args.page) params.page = args.page;
1797
- const result = await client.getSegmentMembers(args.segment_id, params);
1356
+ const segment = await client.getSegment(args.segment_id, params);
1357
+ if (!args.include_members) {
1358
+ return {
1359
+ content: [{ type: "text", text: JSON.stringify(segment, null, 2) }]
1360
+ };
1361
+ }
1362
+ const memberParams = {};
1363
+ if (args.members_page) memberParams.page = args.members_page;
1364
+ const members = await client.request(
1365
+ "GET",
1366
+ `/api/v1/newsletters/segments/${args.segment_id}/members`,
1367
+ void 0,
1368
+ memberParams
1369
+ );
1798
1370
  return {
1799
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1371
+ content: [{
1372
+ type: "text",
1373
+ text: JSON.stringify({ segment, members }, null, 2)
1374
+ }]
1800
1375
  };
1801
1376
  }
1802
1377
  );
1803
1378
  server.tool(
1804
1379
  "list_custom_fields",
1805
- "List all custom subscriber fields defined in your ESP. Custom fields can store extra data like preferences, source, or company.",
1380
+ "List all custom subscriber fields defined in your ESP.",
1806
1381
  {},
1807
1382
  {
1808
1383
  title: "List Custom Fields",
@@ -1818,42 +1393,6 @@ function registerNewsletterAdvancedTools(server, client) {
1818
1393
  };
1819
1394
  }
1820
1395
  );
1821
- server.tool(
1822
- "get_referral_program",
1823
- "Get your newsletter's referral program details including milestones and rewards (Beehiiv only).",
1824
- {},
1825
- {
1826
- title: "Get Referral Program",
1827
- readOnlyHint: true,
1828
- destructiveHint: false,
1829
- idempotentHint: true,
1830
- openWorldHint: true
1831
- },
1832
- async () => {
1833
- const result = await client.getReferralProgram();
1834
- return {
1835
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1836
- };
1837
- }
1838
- );
1839
- server.tool(
1840
- "list_tiers",
1841
- "List all subscription tiers (free and premium) from your ESP (Beehiiv only).",
1842
- {},
1843
- {
1844
- title: "List Tiers",
1845
- readOnlyHint: true,
1846
- destructiveHint: false,
1847
- idempotentHint: true,
1848
- openWorldHint: true
1849
- },
1850
- async () => {
1851
- const result = await client.listEspTiers();
1852
- return {
1853
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1854
- };
1855
- }
1856
- );
1857
1396
  server.tool(
1858
1397
  "list_post_templates",
1859
1398
  "List all post/newsletter templates available in your ESP (Beehiiv only).",
@@ -1874,7 +1413,7 @@ function registerNewsletterAdvancedTools(server, client) {
1874
1413
  );
1875
1414
  server.tool(
1876
1415
  "get_post_aggregate_stats",
1877
- "Get aggregate statistics across all posts/newsletters including total clicks, impressions, and click rates (Beehiiv only).",
1416
+ "Get aggregate stats across all posts and newsletters.",
1878
1417
  {},
1879
1418
  {
1880
1419
  title: "Get Post Aggregate Stats",
@@ -1890,27 +1429,9 @@ function registerNewsletterAdvancedTools(server, client) {
1890
1429
  };
1891
1430
  }
1892
1431
  );
1893
- server.tool(
1894
- "list_esp_webhooks",
1895
- "List all webhooks configured in your ESP for event notifications.",
1896
- {},
1897
- {
1898
- title: "List ESP Webhooks",
1899
- readOnlyHint: true,
1900
- destructiveHint: false,
1901
- idempotentHint: true,
1902
- openWorldHint: true
1903
- },
1904
- async () => {
1905
- const result = await client.listEspWebhooks();
1906
- return {
1907
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1908
- };
1909
- }
1910
- );
1911
1432
  server.tool(
1912
1433
  "tag_subscriber",
1913
- "Add tags to a subscriber. Tags help categorize subscribers for targeted sends and automations.",
1434
+ "Add tags to a subscriber.",
1914
1435
  {
1915
1436
  subscriber_id: z6.string().describe("The subscriber/subscription ID"),
1916
1437
  tags: z6.array(z6.string()).describe("Tags to add to the subscriber")
@@ -1931,12 +1452,7 @@ function registerNewsletterAdvancedTools(server, client) {
1931
1452
  );
1932
1453
  server.tool(
1933
1454
  "update_subscriber",
1934
- `Update a subscriber's details. On Beehiiv: update tier or custom fields. On Kit: update name or custom fields (tier not supported).
1935
-
1936
- REQUIRED WORKFLOW:
1937
- 1. ALWAYS set confirmed=false first to get a preview
1938
- 2. Show the user what will happen before confirming
1939
- 3. Only set confirmed=true after the user explicitly approves`,
1455
+ "Update subscriber details or custom fields. Set confirmed=false to preview first.",
1940
1456
  {
1941
1457
  subscriber_id: z6.string().describe("The subscriber/subscription ID"),
1942
1458
  tier: z6.string().optional().describe("New tier for the subscriber"),
@@ -1976,293 +1492,9 @@ Call again with confirmed=true to proceed.`
1976
1492
  };
1977
1493
  }
1978
1494
  );
1979
- server.tool(
1980
- "create_custom_field",
1981
- `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.
1982
-
1983
- REQUIRED WORKFLOW:
1984
- 1. ALWAYS set confirmed=false first to get a preview
1985
- 2. Show the user what will happen before confirming
1986
- 3. Only set confirmed=true after the user explicitly approves`,
1987
- {
1988
- name: z6.string().describe("Field name"),
1989
- kind: z6.string().describe("Field type: string, integer, boolean, date, or enum"),
1990
- options: z6.array(z6.string()).optional().describe("Options for enum-type fields"),
1991
- confirmed: z6.boolean().default(false).describe("Set to true to confirm creation")
1992
- },
1993
- {
1994
- title: "Create Custom Field",
1995
- readOnlyHint: false,
1996
- destructiveHint: false,
1997
- idempotentHint: false,
1998
- openWorldHint: true
1999
- },
2000
- async (args) => {
2001
- if (args.confirmed !== true) {
2002
- const optionsLine = args.options ? `
2003
- **Options:** ${args.options.join(", ")}` : "";
2004
- return {
2005
- content: [{
2006
- type: "text",
2007
- text: `## Create Custom Field
2008
-
2009
- **Name:** ${args.name}
2010
- **Kind:** ${args.kind}${optionsLine}
2011
-
2012
- This creates a permanent schema-level field visible on all subscribers. Call again with confirmed=true to proceed.`
2013
- }]
2014
- };
2015
- }
2016
- const data = { name: args.name, kind: args.kind };
2017
- if (args.options) data.options = args.options;
2018
- const result = await client.createCustomField(data);
2019
- return {
2020
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2021
- };
2022
- }
2023
- );
2024
- server.tool(
2025
- "update_custom_field",
2026
- `Update an existing custom subscriber field's name or kind. Renaming or rekeying a field can break automations that reference it.
2027
-
2028
- REQUIRED WORKFLOW:
2029
- 1. ALWAYS set confirmed=false first to get a preview
2030
- 2. Show the user what will happen before confirming
2031
- 3. Only set confirmed=true after the user explicitly approves`,
2032
- {
2033
- field_id: z6.string().describe("The custom field ID"),
2034
- name: z6.string().optional().describe("New field name"),
2035
- kind: z6.string().optional().describe("New field type"),
2036
- confirmed: z6.boolean().default(false).describe("Set to true to confirm update")
2037
- },
2038
- {
2039
- title: "Update Custom Field",
2040
- readOnlyHint: false,
2041
- destructiveHint: false,
2042
- idempotentHint: true,
2043
- openWorldHint: true
2044
- },
2045
- async (args) => {
2046
- if (args.confirmed !== true) {
2047
- const changes = [];
2048
- if (args.name) changes.push(`**Name:** ${args.name}`);
2049
- if (args.kind) changes.push(`**Kind:** ${args.kind}`);
2050
- return {
2051
- content: [{
2052
- type: "text",
2053
- text: `## Update Custom Field
2054
-
2055
- **Field ID:** ${args.field_id}
2056
- ${changes.join("\n")}
2057
-
2058
- Renaming or changing the type of a field can break automations that reference it. Call again with confirmed=true to proceed.`
2059
- }]
2060
- };
2061
- }
2062
- const data = {};
2063
- if (args.name) data.name = args.name;
2064
- if (args.kind) data.kind = args.kind;
2065
- const result = await client.updateCustomField(args.field_id, data);
2066
- return {
2067
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2068
- };
2069
- }
2070
- );
2071
- server.tool(
2072
- "recalculate_segment",
2073
- "Trigger a recalculation of a segment's membership. Useful after changing subscriber data or segment rules (Beehiiv only).",
2074
- {
2075
- segment_id: z6.string().describe("The segment ID to recalculate")
2076
- },
2077
- {
2078
- title: "Recalculate Segment",
2079
- readOnlyHint: false,
2080
- destructiveHint: false,
2081
- idempotentHint: true,
2082
- openWorldHint: true
2083
- },
2084
- async (args) => {
2085
- await client.recalculateSegment(args.segment_id);
2086
- return {
2087
- content: [{ type: "text", text: "Segment recalculation triggered." }]
2088
- };
2089
- }
2090
- );
2091
- server.tool(
2092
- "enroll_in_automation",
2093
- `Enroll a subscriber in an automation/journey. Provide either an email or subscription ID. On Kit, enrolls into a sequence.
2094
-
2095
- REQUIRED WORKFLOW:
2096
- 1. ALWAYS set confirmed=false first to get a preview
2097
- 2. Show the user what will happen before confirming
2098
- 3. Only set confirmed=true after the user explicitly approves`,
2099
- {
2100
- automation_id: z6.string().describe("The automation ID"),
2101
- email: z6.string().optional().describe("Subscriber email to enroll"),
2102
- subscription_id: z6.string().optional().describe("Subscription ID to enroll"),
2103
- confirmed: z6.boolean().default(false).describe("Set to true to confirm enrollment")
2104
- },
2105
- {
2106
- title: "Enroll in Automation",
2107
- readOnlyHint: false,
2108
- destructiveHint: false,
2109
- idempotentHint: false,
2110
- openWorldHint: true
2111
- },
2112
- async (args) => {
2113
- if (args.confirmed !== true) {
2114
- const target = args.email ?? args.subscription_id ?? "unknown";
2115
- return {
2116
- content: [{
2117
- type: "text",
2118
- text: `## Enroll in Automation
2119
-
2120
- **Automation ID:** ${args.automation_id}
2121
- **Target:** ${target}
2122
-
2123
- This will add the subscriber to the automation journey. Call again with confirmed=true to proceed.`
2124
- }]
2125
- };
2126
- }
2127
- const data = {};
2128
- if (args.email) data.email = args.email;
2129
- if (args.subscription_id) data.subscriptionId = args.subscription_id;
2130
- await client.enrollInAutomation(args.automation_id, data);
2131
- return {
2132
- content: [{ type: "text", text: "Subscriber enrolled in automation." }]
2133
- };
2134
- }
2135
- );
2136
- server.tool(
2137
- "bulk_add_subscribers",
2138
- `Add multiple subscribers in a single request. Maximum 1000 subscribers per call.
2139
-
2140
- REQUIRED WORKFLOW:
2141
- 1. ALWAYS set confirmed=false first to get a preview
2142
- 2. Show the user what will happen before confirming
2143
- 3. Only set confirmed=true after the user explicitly approves`,
2144
- {
2145
- subscribers: z6.array(z6.object({
2146
- email: z6.string().describe("Subscriber email"),
2147
- data: z6.record(z6.unknown()).optional().describe("Additional subscriber data")
2148
- })).describe("Array of subscribers to add"),
2149
- confirmed: z6.boolean().default(false).describe("Set to true to confirm bulk add")
2150
- },
2151
- {
2152
- title: "Bulk Add Subscribers",
2153
- readOnlyHint: false,
2154
- destructiveHint: false,
2155
- idempotentHint: false,
2156
- openWorldHint: true
2157
- },
2158
- async (args) => {
2159
- if (args.confirmed !== true) {
2160
- return {
2161
- content: [{
2162
- type: "text",
2163
- text: `## Bulk Add Subscribers
2164
-
2165
- **Count:** ${args.subscribers.length} subscriber(s)
2166
- **Sample:** ${args.subscribers.slice(0, 3).map((s) => s.email).join(", ")}${args.subscribers.length > 3 ? "..." : ""}
2167
-
2168
- Call again with confirmed=true to proceed.`
2169
- }]
2170
- };
2171
- }
2172
- const result = await client.bulkCreateSubscribers({ subscribers: args.subscribers });
2173
- return {
2174
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2175
- };
2176
- }
2177
- );
2178
- server.tool(
2179
- "create_esp_webhook",
2180
- `Create a webhook in your ESP to receive event notifications. Note: Kit only supports one event per webhook.
2181
-
2182
- REQUIRED WORKFLOW:
2183
- 1. ALWAYS set confirmed=false first to get a preview
2184
- 2. Show the user what will happen before confirming
2185
- 3. Only set confirmed=true after the user explicitly approves`,
2186
- {
2187
- url: z6.string().describe("Webhook URL to receive events"),
2188
- event_types: z6.array(z6.string()).describe("Event types to subscribe to (e.g. 'subscription.created', 'post.sent')"),
2189
- confirmed: z6.boolean().default(false).describe("Set to true to confirm creation")
2190
- },
2191
- {
2192
- title: "Create ESP Webhook",
2193
- readOnlyHint: false,
2194
- destructiveHint: false,
2195
- idempotentHint: false,
2196
- openWorldHint: true
2197
- },
2198
- async (args) => {
2199
- if (args.confirmed !== true) {
2200
- return {
2201
- content: [{
2202
- type: "text",
2203
- text: `## Create ESP Webhook
2204
-
2205
- **URL:** ${args.url}
2206
- **Events:** ${args.event_types.join(", ")}
2207
-
2208
- Call again with confirmed=true to create.`
2209
- }]
2210
- };
2211
- }
2212
- const result = await client.createEspWebhook({ url: args.url, eventTypes: args.event_types });
2213
- return {
2214
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2215
- };
2216
- }
2217
- );
2218
- server.tool(
2219
- "delete_broadcast",
2220
- `Permanently delete a newsletter/broadcast. This action is permanent and cannot be undone.
2221
-
2222
- REQUIRED WORKFLOW:
2223
- 1. ALWAYS set confirmed=false first to get a preview
2224
- 2. Show the user what will happen before confirming
2225
- 3. Only set confirmed=true after the user explicitly approves`,
2226
- {
2227
- broadcast_id: z6.string().describe("The broadcast ID to delete"),
2228
- confirmed: z6.boolean().default(false).describe("Set to true to confirm deletion")
2229
- },
2230
- {
2231
- title: "Delete Broadcast",
2232
- readOnlyHint: false,
2233
- destructiveHint: true,
2234
- idempotentHint: true,
2235
- openWorldHint: true
2236
- },
2237
- async (args) => {
2238
- if (args.confirmed !== true) {
2239
- return {
2240
- content: [{
2241
- type: "text",
2242
- text: `## Delete Broadcast
2243
-
2244
- **Broadcast ID:** ${args.broadcast_id}
2245
-
2246
- **This action is permanent and cannot be undone.**
2247
-
2248
- Call again with confirmed=true to delete.`
2249
- }]
2250
- };
2251
- }
2252
- await client.deleteBroadcast(args.broadcast_id);
2253
- return {
2254
- content: [{ type: "text", text: "Broadcast deleted." }]
2255
- };
2256
- }
2257
- );
2258
1495
  server.tool(
2259
1496
  "delete_subscriber",
2260
- `Remove a subscriber from your mailing list. On Beehiiv this permanently deletes the subscriber. On Kit this unsubscribes them (Kit has no hard delete).
2261
-
2262
- REQUIRED WORKFLOW:
2263
- 1. ALWAYS set confirmed=false first to get a preview
2264
- 2. Show the user what will happen before confirming
2265
- 3. Only set confirmed=true after the user explicitly approves`,
1497
+ "Remove a subscriber. Set confirmed=false to preview first.",
2266
1498
  {
2267
1499
  subscriber_id: z6.string().describe("The subscriber ID to delete"),
2268
1500
  confirmed: z6.boolean().default(false).describe("Set to true to confirm deletion")
@@ -2295,126 +1527,6 @@ Call again with confirmed=true to delete.`
2295
1527
  };
2296
1528
  }
2297
1529
  );
2298
- server.tool(
2299
- "delete_segment",
2300
- `Permanently delete a subscriber segment. This action is permanent and cannot be undone (Beehiiv only).
2301
-
2302
- REQUIRED WORKFLOW:
2303
- 1. ALWAYS set confirmed=false first to get a preview
2304
- 2. Show the user what will happen before confirming
2305
- 3. Only set confirmed=true after the user explicitly approves`,
2306
- {
2307
- segment_id: z6.string().describe("The segment ID to delete"),
2308
- confirmed: z6.boolean().default(false).describe("Set to true to confirm deletion")
2309
- },
2310
- {
2311
- title: "Delete Segment",
2312
- readOnlyHint: false,
2313
- destructiveHint: true,
2314
- idempotentHint: true,
2315
- openWorldHint: true
2316
- },
2317
- async (args) => {
2318
- if (args.confirmed !== true) {
2319
- return {
2320
- content: [{
2321
- type: "text",
2322
- text: `## Delete Segment
2323
-
2324
- **Segment ID:** ${args.segment_id}
2325
-
2326
- **This action is permanent and cannot be undone.** Subscribers in this segment will not be deleted, but the segment grouping will be removed.
2327
-
2328
- Call again with confirmed=true to delete.`
2329
- }]
2330
- };
2331
- }
2332
- await client.deleteSegment(args.segment_id);
2333
- return {
2334
- content: [{ type: "text", text: "Segment deleted." }]
2335
- };
2336
- }
2337
- );
2338
- server.tool(
2339
- "delete_custom_field",
2340
- `Permanently delete a custom subscriber field and remove it from all subscribers. This action is permanent and cannot be undone.
2341
-
2342
- REQUIRED WORKFLOW:
2343
- 1. ALWAYS set confirmed=false first to get a preview
2344
- 2. Show the user what will happen before confirming
2345
- 3. Only set confirmed=true after the user explicitly approves`,
2346
- {
2347
- field_id: z6.string().describe("The custom field ID to delete"),
2348
- confirmed: z6.boolean().default(false).describe("Set to true to confirm deletion")
2349
- },
2350
- {
2351
- title: "Delete Custom Field",
2352
- readOnlyHint: false,
2353
- destructiveHint: true,
2354
- idempotentHint: true,
2355
- openWorldHint: true
2356
- },
2357
- async (args) => {
2358
- if (args.confirmed !== true) {
2359
- return {
2360
- content: [{
2361
- type: "text",
2362
- text: `## Delete Custom Field
2363
-
2364
- **Field ID:** ${args.field_id}
2365
-
2366
- **This action is permanent and cannot be undone.** The field and its data will be removed from all subscribers.
2367
-
2368
- Call again with confirmed=true to delete.`
2369
- }]
2370
- };
2371
- }
2372
- await client.deleteCustomField(args.field_id);
2373
- return {
2374
- content: [{ type: "text", text: "Custom field deleted." }]
2375
- };
2376
- }
2377
- );
2378
- server.tool(
2379
- "delete_esp_webhook",
2380
- `Permanently delete a webhook from your ESP. This action is permanent and cannot be undone.
2381
-
2382
- REQUIRED WORKFLOW:
2383
- 1. ALWAYS set confirmed=false first to get a preview
2384
- 2. Show the user what will happen before confirming
2385
- 3. Only set confirmed=true after the user explicitly approves`,
2386
- {
2387
- webhook_id: z6.string().describe("The webhook ID to delete"),
2388
- confirmed: z6.boolean().default(false).describe("Set to true to confirm deletion")
2389
- },
2390
- {
2391
- title: "Delete ESP Webhook",
2392
- readOnlyHint: false,
2393
- destructiveHint: true,
2394
- idempotentHint: true,
2395
- openWorldHint: true
2396
- },
2397
- async (args) => {
2398
- if (args.confirmed !== true) {
2399
- return {
2400
- content: [{
2401
- type: "text",
2402
- text: `## Delete ESP Webhook
2403
-
2404
- **Webhook ID:** ${args.webhook_id}
2405
-
2406
- **This action is permanent and cannot be undone.** You will stop receiving event notifications at this webhook URL.
2407
-
2408
- Call again with confirmed=true to delete.`
2409
- }]
2410
- };
2411
- }
2412
- await client.deleteEspWebhook(args.webhook_id);
2413
- return {
2414
- content: [{ type: "text", text: "Webhook deleted." }]
2415
- };
2416
- }
2417
- );
2418
1530
  }
2419
1531
 
2420
1532
  // src/tools/rss.ts
@@ -2422,7 +1534,7 @@ import { z as z7 } from "zod";
2422
1534
  function registerRssTools(server, client) {
2423
1535
  server.tool(
2424
1536
  "fetch_feed",
2425
- "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.",
1537
+ "Fetch and parse entries from an RSS or Atom feed URL.",
2426
1538
  {
2427
1539
  url: z7.string().describe("The RSS/Atom feed URL to fetch"),
2428
1540
  limit: z7.number().optional().describe("Maximum number of entries to return (default 10, max 100)")
@@ -2443,7 +1555,7 @@ function registerRssTools(server, client) {
2443
1555
  );
2444
1556
  server.tool(
2445
1557
  "fetch_article",
2446
- "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.",
1558
+ "Extract full article content from a URL as clean text.",
2447
1559
  {
2448
1560
  url: z7.string().describe("The article URL to extract content from")
2449
1561
  },
@@ -2467,7 +1579,7 @@ function registerRssTools(server, client) {
2467
1579
  function registerAccountInfoTool(server, client) {
2468
1580
  server.tool(
2469
1581
  "get_account",
2470
- "Get your BuzzPoster account details including name, email, subscription status, ESP configuration, and connected platforms.",
1582
+ "Get account details, subscription status, ESP config, and all connected social/ESP accounts.",
2471
1583
  {},
2472
1584
  {
2473
1585
  title: "Get Account Details",
@@ -2477,9 +1589,15 @@ function registerAccountInfoTool(server, client) {
2477
1589
  openWorldHint: false
2478
1590
  },
2479
1591
  async () => {
2480
- const result = await client.getAccount();
1592
+ const [account, accountsData] = await Promise.all([
1593
+ client.getAccount(),
1594
+ client.listAccounts()
1595
+ ]);
2481
1596
  return {
2482
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1597
+ content: [{
1598
+ type: "text",
1599
+ text: JSON.stringify({ account, connectedAccounts: accountsData }, null, 2)
1600
+ }]
2483
1601
  };
2484
1602
  }
2485
1603
  );
@@ -2490,7 +1608,7 @@ import { z as z8 } from "zod";
2490
1608
  function registerBrandVoiceTools(server, client) {
2491
1609
  server.tool(
2492
1610
  "get_brand_voice",
2493
- "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.",
1611
+ "Get the brand voice profile and writing rules. Call before creating content.",
2494
1612
  {},
2495
1613
  {
2496
1614
  title: "Get Brand Voice",
@@ -2565,21 +1683,14 @@ function registerBrandVoiceTools(server, client) {
2565
1683
  );
2566
1684
  server.tool(
2567
1685
  "update_brand_voice",
2568
- `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.
2569
-
2570
- USAGE GUIDELINES:
2571
- - Call get_brand_voice first to see the current state before making changes
2572
- - Always set confirmed=false first to preview the changes, then confirmed=true after user approval
2573
- - When adding rules to dos/donts, include the FULL array (existing + new items), not just the new ones
2574
- - Platform rules use platform names as keys (e.g. twitter, linkedin, instagram)
2575
- - Max limits: name 80 chars, description 16000 chars, dos/donts 50 items each, examplePosts 8 items`,
2576
- {
2577
- name: z8.string().max(80).optional().describe("Brand voice profile name (e.g. 'BOQtoday Voice', 'Lightbreak Editorial')"),
2578
- description: z8.string().max(16e3).optional().describe("Overall voice description \u2014 tone, personality, style overview"),
2579
- dos: z8.array(z8.string()).max(50).optional().describe("Writing rules to follow (array of do's). Send the FULL list, not just additions."),
2580
- donts: z8.array(z8.string()).max(50).optional().describe("Things to avoid (array of don'ts). Send the FULL list, not just additions."),
2581
- platform_rules: z8.record(z8.string()).optional().describe('Per-platform writing guidelines, e.g. { "twitter": "Keep under 200 chars, punchy tone", "linkedin": "Professional, add hashtags" }'),
2582
- example_posts: z8.array(z8.string()).max(8).optional().describe("Example posts that demonstrate the desired voice (max 8)"),
1686
+ "Update the brand voice profile. Set confirmed=false to preview first.",
1687
+ {
1688
+ name: z8.string().max(80).optional().describe("Brand voice profile name"),
1689
+ description: z8.string().max(2e3).optional().describe("Overall voice description"),
1690
+ dos: z8.array(z8.string().max(200)).max(15).optional().describe("Writing rules to follow. Send the FULL list, not just additions."),
1691
+ donts: z8.array(z8.string().max(200)).max(15).optional().describe("Things to avoid. Send the FULL list, not just additions."),
1692
+ platform_rules: z8.record(z8.string().max(300)).optional().describe('Per-platform writing guidelines, e.g. { "twitter": "Keep under 200 chars" }'),
1693
+ example_posts: z8.array(z8.string().max(500)).max(5).optional().describe("Example posts that demonstrate the desired voice"),
2583
1694
  confirmed: z8.boolean().default(false).describe(
2584
1695
  "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
2585
1696
  )
@@ -2597,7 +1708,19 @@ USAGE GUIDELINES:
2597
1708
  if (args.description !== void 0) payload.description = args.description;
2598
1709
  if (args.dos !== void 0) payload.dos = args.dos;
2599
1710
  if (args.donts !== void 0) payload.donts = args.donts;
2600
- if (args.platform_rules !== void 0) payload.platformRules = args.platform_rules;
1711
+ if (args.platform_rules !== void 0) {
1712
+ const entries = Object.entries(args.platform_rules);
1713
+ if (entries.length > 5) {
1714
+ return {
1715
+ content: [{
1716
+ type: "text",
1717
+ text: "Maximum 5 platform rules allowed."
1718
+ }],
1719
+ isError: true
1720
+ };
1721
+ }
1722
+ payload.platformRules = args.platform_rules;
1723
+ }
2601
1724
  if (args.example_posts !== void 0) payload.examplePosts = args.example_posts;
2602
1725
  if (Object.keys(payload).length === 0) {
2603
1726
  return {
@@ -2685,7 +1808,7 @@ import { z as z9 } from "zod";
2685
1808
  function registerKnowledgeTools(server, client) {
2686
1809
  server.tool(
2687
1810
  "get_knowledge_base",
2688
- "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.",
1811
+ "Get all items from the knowledge base.",
2689
1812
  {},
2690
1813
  {
2691
1814
  title: "Get Knowledge Base",
@@ -2739,7 +1862,7 @@ function registerKnowledgeTools(server, client) {
2739
1862
  );
2740
1863
  server.tool(
2741
1864
  "search_knowledge",
2742
- "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.",
1865
+ "Search the knowledge base by tag or keyword.",
2743
1866
  {
2744
1867
  query: z9.string().describe(
2745
1868
  "Search query - matches against tags first, then falls back to text search on title and content"
@@ -2790,7 +1913,7 @@ function registerKnowledgeTools(server, client) {
2790
1913
  );
2791
1914
  server.tool(
2792
1915
  "add_knowledge",
2793
- "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.",
1916
+ "Add reference material to the knowledge base. Include tags for categorization.",
2794
1917
  {
2795
1918
  title: z9.string().describe("Title for the knowledge item"),
2796
1919
  content: z9.string().describe("The content/text to save"),
@@ -2832,10 +1955,8 @@ import { z as z10 } from "zod";
2832
1955
  function registerAudienceTools(server, client) {
2833
1956
  server.tool(
2834
1957
  "get_audience",
2835
- "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.",
2836
- {
2837
- audienceId: z10.string().optional().describe("Specific audience profile ID. If omitted, returns the default audience.")
2838
- },
1958
+ "Get the target audience profile. Call before creating content.",
1959
+ {},
2839
1960
  {
2840
1961
  title: "Get Audience Profile",
2841
1962
  readOnlyHint: true,
@@ -2843,9 +1964,9 @@ function registerAudienceTools(server, client) {
2843
1964
  idempotentHint: true,
2844
1965
  openWorldHint: false
2845
1966
  },
2846
- async ({ audienceId }) => {
1967
+ async () => {
2847
1968
  try {
2848
- const audience = audienceId ? await client.getAudience(audienceId) : await client.getDefaultAudience();
1969
+ const audience = await client.getDefaultAudience();
2849
1970
  const lines = [];
2850
1971
  lines.push(`## Target Audience: ${audience.name}`);
2851
1972
  lines.push("");
@@ -2909,25 +2030,16 @@ function registerAudienceTools(server, client) {
2909
2030
  );
2910
2031
  server.tool(
2911
2032
  "update_audience",
2912
- `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.
2913
-
2914
- USAGE GUIDELINES:
2915
- - Call get_audience first to see the current state before making changes
2916
- - Always set confirmed=false first to preview the changes, then confirmed=true after user approval
2917
- - When updating array fields (pain_points, motivations, preferred_platforms), include the FULL array (existing + new items), not just the new ones
2918
- - Omit audience_id to update the default audience or create a new one if none exists
2919
- - Max limits: name 100 chars, description 500 chars`,
2033
+ "Update the audience profile. Set confirmed=false to preview first.",
2920
2034
  {
2921
- audience_id: z10.string().optional().describe("Audience ID to update. Omit to update the default audience, or create one if none exists."),
2922
- name: z10.string().max(100).optional().describe("Audience profile name (e.g. 'SaaS Founders', 'Health-Conscious Millennials'). Required when creating a new audience."),
2035
+ name: z10.string().max(100).optional().describe("Audience profile name. Required when creating a new audience."),
2923
2036
  description: z10.string().max(500).optional().describe("Brief description of who this audience is"),
2924
- demographics: z10.string().optional().describe("Demographic details \u2014 age range, location, income level, education, etc."),
2925
- pain_points: z10.array(z10.string()).optional().describe("Problems and frustrations the audience faces. Send the FULL list, not just additions."),
2926
- motivations: z10.array(z10.string()).optional().describe("Goals, desires, and what drives the audience. Send the FULL list, not just additions."),
2927
- preferred_platforms: z10.array(z10.string()).optional().describe("Social platforms the audience is most active on (e.g. twitter, linkedin, instagram). Send the FULL list."),
2928
- tone_notes: z10.string().optional().describe("How to speak to this audience \u2014 formality level, jargon preferences, emotional tone"),
2929
- content_preferences: z10.string().optional().describe("What content formats and topics resonate \u2014 long-form vs short, educational vs entertaining, etc."),
2930
- is_default: z10.boolean().optional().describe("Set to true to make this the default audience profile"),
2037
+ demographics: z10.string().max(500).optional().describe("Demographic details"),
2038
+ pain_points: z10.array(z10.string().max(200)).max(10).optional().describe("Problems the audience faces. Send the FULL list."),
2039
+ motivations: z10.array(z10.string().max(200)).max(10).optional().describe("Goals and desires. Send the FULL list."),
2040
+ preferred_platforms: z10.array(z10.string()).max(6).optional().describe("Social platforms the audience is most active on. Send the FULL list."),
2041
+ tone_notes: z10.string().max(500).optional().describe("How to speak to this audience"),
2042
+ content_preferences: z10.string().max(500).optional().describe("What content formats and topics resonate"),
2931
2043
  confirmed: z10.boolean().default(false).describe(
2932
2044
  "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
2933
2045
  )
@@ -2949,27 +2061,24 @@ USAGE GUIDELINES:
2949
2061
  if (args.preferred_platforms !== void 0) payload.platforms = args.preferred_platforms;
2950
2062
  if (args.tone_notes !== void 0) payload.toneNotes = args.tone_notes;
2951
2063
  if (args.content_preferences !== void 0) payload.contentPreferences = args.content_preferences;
2952
- if (args.is_default !== void 0) payload.isDefault = args.is_default;
2953
2064
  if (Object.keys(payload).length === 0) {
2954
2065
  return {
2955
2066
  content: [
2956
2067
  {
2957
2068
  type: "text",
2958
- 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)."
2069
+ text: "No fields provided. Specify at least one field to update."
2959
2070
  }
2960
2071
  ],
2961
2072
  isError: true
2962
2073
  };
2963
2074
  }
2964
- let targetId = args.audience_id;
2075
+ let targetId;
2965
2076
  let isCreating = false;
2966
- if (!targetId) {
2967
- try {
2968
- const defaultAudience = await client.getDefaultAudience();
2969
- targetId = String(defaultAudience.id);
2970
- } catch {
2971
- isCreating = true;
2972
- }
2077
+ try {
2078
+ const defaultAudience = await client.getDefaultAudience();
2079
+ targetId = String(defaultAudience.id);
2080
+ } catch {
2081
+ isCreating = true;
2973
2082
  }
2974
2083
  if (isCreating && !payload.name) {
2975
2084
  return {
@@ -2985,9 +2094,6 @@ USAGE GUIDELINES:
2985
2094
  if (args.confirmed !== true) {
2986
2095
  const lines2 = [];
2987
2096
  lines2.push(isCreating ? "## New Audience Preview" : "## Audience Update Preview");
2988
- if (!isCreating) {
2989
- lines2.push(`**Audience ID:** ${targetId}`);
2990
- }
2991
2097
  lines2.push("");
2992
2098
  if (payload.name) {
2993
2099
  lines2.push(`**Name:** ${payload.name}`);
@@ -3035,10 +2141,6 @@ USAGE GUIDELINES:
3035
2141
  lines2.push(String(payload.contentPreferences));
3036
2142
  lines2.push("");
3037
2143
  }
3038
- if (payload.isDefault !== void 0) {
3039
- lines2.push(`**Set as default:** ${payload.isDefault ? "Yes" : "No"}`);
3040
- lines2.push("");
3041
- }
3042
2144
  lines2.push("---");
3043
2145
  lines2.push("Call this tool again with **confirmed=true** to save these changes.");
3044
2146
  return {
@@ -3064,7 +2166,6 @@ USAGE GUIDELINES:
3064
2166
  if (payload.platforms) summary.push(`${payload.platforms.length} platforms`);
3065
2167
  if (payload.toneNotes) summary.push("tone notes");
3066
2168
  if (payload.contentPreferences) summary.push("content preferences");
3067
- if (payload.isDefault !== void 0) summary.push("default status");
3068
2169
  lines.push(`**${isCreating ? "Created with" : "Updated"}:** ${summary.join(", ")}`);
3069
2170
  return {
3070
2171
  content: [{ type: "text", text: lines.join("\n") }]
@@ -3108,7 +2209,7 @@ Follow these rules when drafting:
3108
2209
  function registerNewsletterTemplateTools(server, client) {
3109
2210
  server.tool(
3110
2211
  "get_newsletter_template",
3111
- "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.",
2212
+ "Get a newsletter template's structure, sections, and HTML. Pass templateId or omit for default.",
3112
2213
  {
3113
2214
  templateId: z11.string().optional().describe(
3114
2215
  "Specific template ID. If omitted, returns the default template."
@@ -3209,6 +2310,15 @@ function registerNewsletterTemplateTools(server, client) {
3209
2310
  );
3210
2311
  lines.push("");
3211
2312
  }
2313
+ if (template.htmlContent) {
2314
+ lines.push("### HTML Template:");
2315
+ lines.push("The following HTML skeleton contains {{placeholder}} variables. Fill in the placeholders with real content when generating the newsletter.");
2316
+ lines.push("");
2317
+ lines.push("```html");
2318
+ lines.push(template.htmlContent);
2319
+ lines.push("```");
2320
+ lines.push("");
2321
+ }
3212
2322
  if (template.rssFeedUrls && template.rssFeedUrls.length > 0) {
3213
2323
  lines.push("### Linked RSS Feeds:");
3214
2324
  for (const url of template.rssFeedUrls) {
@@ -3245,9 +2355,10 @@ function registerNewsletterTemplateTools(server, client) {
3245
2355
  );
3246
2356
  server.tool(
3247
2357
  "get_past_newsletters",
3248
- "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.",
2358
+ "Get past newsletters from the archive. Pass newsletterId to get full content of a specific edition.",
3249
2359
  {
3250
- limit: z11.number().optional().describe("Number of past newsletters to retrieve. Default 5, max 50."),
2360
+ newsletterId: z11.string().optional().describe("If provided, returns the full content of this specific newsletter. If omitted, returns the list."),
2361
+ limit: z11.number().optional().describe("Number of past newsletters to retrieve (when listing). Default 5, max 50."),
3251
2362
  templateId: z11.string().optional().describe("Filter by template ID to see past newsletters from a specific template.")
3252
2363
  },
3253
2364
  {
@@ -3257,7 +2368,41 @@ function registerNewsletterTemplateTools(server, client) {
3257
2368
  idempotentHint: true,
3258
2369
  openWorldHint: false
3259
2370
  },
3260
- async ({ limit, templateId }) => {
2371
+ async ({ newsletterId, limit, templateId }) => {
2372
+ if (newsletterId) {
2373
+ try {
2374
+ const newsletter = await client.getArchivedNewsletter(
2375
+ newsletterId
2376
+ );
2377
+ const lines = [];
2378
+ lines.push(`## ${newsletter.subject}`);
2379
+ if (newsletter.sentAt) lines.push(`Sent: ${newsletter.sentAt}`);
2380
+ if (newsletter.notes) lines.push(`Notes: ${newsletter.notes}`);
2381
+ lines.push("");
2382
+ lines.push("### Full HTML Content:");
2383
+ lines.push(newsletter.contentHtml);
2384
+ return {
2385
+ content: [
2386
+ { type: "text", text: lines.join("\n") },
2387
+ ...newsletter.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2388
+ ${newsletter.contentHtml}` }] : []
2389
+ ]
2390
+ };
2391
+ } catch (error) {
2392
+ const message = error instanceof Error ? error.message : "Unknown error";
2393
+ if (message.includes("404")) {
2394
+ return {
2395
+ content: [
2396
+ {
2397
+ type: "text",
2398
+ text: "Newsletter not found. Check the ID and try again."
2399
+ }
2400
+ ]
2401
+ };
2402
+ }
2403
+ throw error;
2404
+ }
2405
+ }
3261
2406
  try {
3262
2407
  const params = {};
3263
2408
  if (limit) params.limit = String(Math.min(limit, 50));
@@ -3295,101 +2440,266 @@ function registerNewsletterTemplateTools(server, client) {
3295
2440
  const textContent = nl.contentHtml ? nl.contentHtml.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 500) : "(no content)";
3296
2441
  lines.push(textContent);
3297
2442
  lines.push(
3298
- `[Full content available via get_past_newsletter tool with ID: ${nl.id}]`
2443
+ `[Full content available by calling get_past_newsletters with newsletterId: ${nl.id}]`
2444
+ );
2445
+ lines.push("");
2446
+ }
2447
+ const mostRecent = newsletters[0];
2448
+ return {
2449
+ content: [
2450
+ { type: "text", text: lines.join("\n") },
2451
+ ...mostRecent?.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2452
+ ${mostRecent.contentHtml}` }] : []
2453
+ ]
2454
+ };
2455
+ } catch (error) {
2456
+ const message = error instanceof Error ? error.message : "Unknown error";
2457
+ throw new Error(`Failed to fetch past newsletters: ${message}`);
2458
+ }
2459
+ }
2460
+ );
2461
+ server.tool(
2462
+ "save_newsletter",
2463
+ "Save an approved newsletter to the archive for future reference.",
2464
+ {
2465
+ subject: z11.string().describe("The newsletter subject line."),
2466
+ contentHtml: z11.string().describe("The full HTML content of the newsletter."),
2467
+ templateId: z11.string().optional().describe("The template ID used to generate this newsletter."),
2468
+ notes: z11.string().optional().describe(
2469
+ "Optional notes about this newsletter (what worked, theme, etc)."
2470
+ )
2471
+ },
2472
+ {
2473
+ title: "Save Newsletter to Archive",
2474
+ readOnlyHint: false,
2475
+ destructiveHint: false,
2476
+ idempotentHint: false,
2477
+ openWorldHint: false
2478
+ },
2479
+ async ({ subject, contentHtml, templateId, notes }) => {
2480
+ try {
2481
+ const data = {
2482
+ subject,
2483
+ contentHtml
2484
+ };
2485
+ if (templateId) data.templateId = Number(templateId);
2486
+ if (notes) data.notes = notes;
2487
+ await client.saveNewsletterToArchive(data);
2488
+ return {
2489
+ content: [
2490
+ {
2491
+ type: "text",
2492
+ text: `Newsletter "${subject}" has been saved to the archive successfully.`
2493
+ },
2494
+ ...contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2495
+ ${contentHtml}` }] : []
2496
+ ]
2497
+ };
2498
+ } catch (error) {
2499
+ const message = error instanceof Error ? error.message : "Unknown error";
2500
+ throw new Error(`Failed to save newsletter: ${message}`);
2501
+ }
2502
+ }
2503
+ );
2504
+ server.tool(
2505
+ "save_newsletter_template",
2506
+ "Create or update a newsletter template with HTML and section definitions.",
2507
+ {
2508
+ template_id: z11.string().optional().describe(
2509
+ "If provided, updates this existing template. Otherwise creates a new one."
2510
+ ),
2511
+ name: z11.string().min(1).max(255).describe("Template name"),
2512
+ description: z11.string().max(2e3).optional().describe("What this template is for"),
2513
+ html_content: z11.string().min(1).max(1e5).describe(
2514
+ "Full HTML template with {{placeholder}} variables."
2515
+ ),
2516
+ sections: z11.array(
2517
+ z11.object({
2518
+ name: z11.string().describe("Section identifier, e.g. 'intro'"),
2519
+ label: z11.string().describe("Display label, e.g. 'Intro/Greeting'"),
2520
+ required: z11.boolean().optional().describe("Whether this section is required"),
2521
+ item_count: z11.number().optional().describe("Number of items in this section, if applicable")
2522
+ })
2523
+ ).max(20).optional().describe("Array of section metadata describing the template structure"),
2524
+ style_config: z11.object({
2525
+ primary_color: z11.string().optional(),
2526
+ accent_color: z11.string().optional(),
2527
+ link_color: z11.string().optional(),
2528
+ background_color: z11.string().optional(),
2529
+ font_family: z11.string().optional(),
2530
+ max_width: z11.string().optional()
2531
+ }).optional().describe("Colors, fonts, and brand configuration"),
2532
+ is_default: z11.boolean().optional().describe(
2533
+ "Set as the default template for this customer."
2534
+ )
2535
+ },
2536
+ {
2537
+ title: "Save Newsletter Template",
2538
+ readOnlyHint: false,
2539
+ destructiveHint: false,
2540
+ idempotentHint: false,
2541
+ openWorldHint: false
2542
+ },
2543
+ async ({
2544
+ template_id,
2545
+ name,
2546
+ description,
2547
+ html_content,
2548
+ sections,
2549
+ style_config,
2550
+ is_default
2551
+ }) => {
2552
+ try {
2553
+ const data = {
2554
+ name,
2555
+ htmlContent: html_content
2556
+ };
2557
+ if (description !== void 0) data.description = description;
2558
+ if (sections !== void 0) {
2559
+ data.sections = sections.map((s) => ({
2560
+ id: s.name,
2561
+ type: "custom",
2562
+ label: s.label,
2563
+ required: s.required ?? true,
2564
+ count: s.item_count
2565
+ }));
2566
+ }
2567
+ if (style_config !== void 0) {
2568
+ data.style = {
2569
+ primary_color: style_config.primary_color,
2570
+ accent_color: style_config.accent_color,
2571
+ link_color: style_config.link_color,
2572
+ background_color: style_config.background_color,
2573
+ font_family: style_config.font_family,
2574
+ content_width: style_config.max_width
2575
+ };
2576
+ }
2577
+ if (is_default !== void 0) data.isDefault = is_default;
2578
+ let result;
2579
+ if (template_id) {
2580
+ result = await client.updateTemplate(
2581
+ template_id,
2582
+ data
3299
2583
  );
3300
- lines.push("");
2584
+ } else {
2585
+ result = await client.createTemplate(data);
3301
2586
  }
2587
+ const action = template_id ? "updated" : "created";
3302
2588
  return {
3303
- content: [{ type: "text", text: lines.join("\n") }]
2589
+ content: [
2590
+ {
2591
+ type: "text",
2592
+ text: `Newsletter template "${result.name}" ${action} successfully (ID: ${result.id}).${result.isDefault ? " Set as default." : ""}`
2593
+ }
2594
+ ]
3304
2595
  };
3305
2596
  } catch (error) {
3306
2597
  const message = error instanceof Error ? error.message : "Unknown error";
3307
- throw new Error(`Failed to fetch past newsletters: ${message}`);
2598
+ throw new Error(`Failed to save newsletter template: ${message}`);
3308
2599
  }
3309
2600
  }
3310
2601
  );
3311
2602
  server.tool(
3312
- "get_past_newsletter",
3313
- "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.",
3314
- {
3315
- newsletterId: z11.string().describe("The ID of the archived newsletter to retrieve.")
3316
- },
2603
+ "list_newsletter_templates",
2604
+ "List all saved newsletter templates.",
2605
+ {},
3317
2606
  {
3318
- title: "Get Past Newsletter Detail",
2607
+ title: "List Newsletter Templates",
3319
2608
  readOnlyHint: true,
3320
2609
  destructiveHint: false,
3321
2610
  idempotentHint: true,
3322
2611
  openWorldHint: false
3323
2612
  },
3324
- async ({ newsletterId }) => {
2613
+ async () => {
3325
2614
  try {
3326
- const newsletter = await client.getArchivedNewsletter(
3327
- newsletterId
3328
- );
3329
- const lines = [];
3330
- lines.push(`## ${newsletter.subject}`);
3331
- if (newsletter.sentAt) lines.push(`Sent: ${newsletter.sentAt}`);
3332
- if (newsletter.notes) lines.push(`Notes: ${newsletter.notes}`);
3333
- lines.push("");
3334
- lines.push("### Full HTML Content:");
3335
- lines.push(newsletter.contentHtml);
3336
- return {
3337
- content: [{ type: "text", text: lines.join("\n") }]
3338
- };
3339
- } catch (error) {
3340
- const message = error instanceof Error ? error.message : "Unknown error";
3341
- if (message.includes("404")) {
2615
+ const templates = await client.listTemplates();
2616
+ if (!templates || templates.length === 0) {
3342
2617
  return {
3343
2618
  content: [
3344
2619
  {
3345
2620
  type: "text",
3346
- text: "Newsletter not found. Check the ID and try again."
2621
+ text: "No newsletter templates found. Use save_newsletter_template to create one."
3347
2622
  }
3348
2623
  ]
3349
2624
  };
3350
2625
  }
3351
- throw error;
2626
+ const lines = [];
2627
+ lines.push(`## Newsletter Templates (${templates.length})`);
2628
+ lines.push("");
2629
+ for (const t of templates) {
2630
+ const badges = [];
2631
+ if (t.isDefault) badges.push("DEFAULT");
2632
+ if (t.htmlContent) badges.push("HAS HTML");
2633
+ const badgeStr = badges.length > 0 ? ` [${badges.join(", ")}]` : "";
2634
+ lines.push(`**${t.name}** (ID: ${t.id})${badgeStr}`);
2635
+ if (t.description) lines.push(` ${t.description}`);
2636
+ if (t.sections && t.sections.length > 0) {
2637
+ lines.push(
2638
+ ` Sections: ${t.sections.map((s) => s.label).join(", ")}`
2639
+ );
2640
+ }
2641
+ lines.push("");
2642
+ }
2643
+ return {
2644
+ content: [{ type: "text", text: lines.join("\n") }]
2645
+ };
2646
+ } catch (error) {
2647
+ const message = error instanceof Error ? error.message : "Unknown error";
2648
+ throw new Error(`Failed to list newsletter templates: ${message}`);
3352
2649
  }
3353
2650
  }
3354
2651
  );
3355
2652
  server.tool(
3356
- "save_newsletter",
3357
- "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.",
2653
+ "delete_newsletter_template",
2654
+ "Delete a newsletter template. Set confirmed=true to proceed.",
3358
2655
  {
3359
- subject: z11.string().describe("The newsletter subject line."),
3360
- contentHtml: z11.string().describe("The full HTML content of the newsletter."),
3361
- templateId: z11.string().optional().describe("The template ID used to generate this newsletter."),
3362
- notes: z11.string().optional().describe(
3363
- "Optional notes about this newsletter (what worked, theme, etc)."
2656
+ template_id: z11.string().describe("The ID of the newsletter template to delete."),
2657
+ confirmed: z11.boolean().default(false).describe(
2658
+ "Must be true to actually delete. If false, returns a confirmation prompt."
3364
2659
  )
3365
2660
  },
3366
2661
  {
3367
- title: "Save Newsletter to Archive",
2662
+ title: "Delete Newsletter Template",
3368
2663
  readOnlyHint: false,
3369
- destructiveHint: false,
2664
+ destructiveHint: true,
3370
2665
  idempotentHint: false,
3371
2666
  openWorldHint: false
3372
2667
  },
3373
- async ({ subject, contentHtml, templateId, notes }) => {
2668
+ async ({ template_id, confirmed }) => {
3374
2669
  try {
3375
- const data = {
3376
- subject,
3377
- contentHtml
3378
- };
3379
- if (templateId) data.templateId = Number(templateId);
3380
- if (notes) data.notes = notes;
3381
- await client.saveNewsletterToArchive(data);
2670
+ if (!confirmed) {
2671
+ const template = await client.getTemplate(template_id);
2672
+ return {
2673
+ content: [
2674
+ {
2675
+ type: "text",
2676
+ 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.`
2677
+ }
2678
+ ]
2679
+ };
2680
+ }
2681
+ await client.deleteTemplate(template_id);
3382
2682
  return {
3383
2683
  content: [
3384
2684
  {
3385
2685
  type: "text",
3386
- text: `Newsletter "${subject}" has been saved to the archive successfully.`
2686
+ text: `Newsletter template (ID: ${template_id}) has been deleted.`
3387
2687
  }
3388
2688
  ]
3389
2689
  };
3390
2690
  } catch (error) {
3391
2691
  const message = error instanceof Error ? error.message : "Unknown error";
3392
- throw new Error(`Failed to save newsletter: ${message}`);
2692
+ if (message.includes("404")) {
2693
+ return {
2694
+ content: [
2695
+ {
2696
+ type: "text",
2697
+ text: "Template not found. It may have already been deleted."
2698
+ }
2699
+ ]
2700
+ };
2701
+ }
2702
+ throw new Error(`Failed to delete newsletter template: ${message}`);
3393
2703
  }
3394
2704
  }
3395
2705
  );
@@ -3400,7 +2710,7 @@ import { z as z12 } from "zod";
3400
2710
  function registerCalendarTools(server, client) {
3401
2711
  server.tool(
3402
2712
  "get_calendar",
3403
- "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.",
2713
+ "View the content calendar with all scheduled, published, and draft posts.",
3404
2714
  {
3405
2715
  from: z12.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
3406
2716
  to: z12.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
@@ -3483,7 +2793,7 @@ function registerCalendarTools(server, client) {
3483
2793
  );
3484
2794
  server.tool(
3485
2795
  "get_queue",
3486
- "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.",
2796
+ "View the posting queue schedule and recurring weekly time slots.",
3487
2797
  {},
3488
2798
  {
3489
2799
  title: "Get Posting Queue",
@@ -3517,7 +2827,8 @@ function registerCalendarTools(server, client) {
3517
2827
  for (const slot of slots) {
3518
2828
  const day = dayNames[slot.dayOfWeek] ?? `Day ${slot.dayOfWeek}`;
3519
2829
  const platforms = (slot.platforms ?? []).map((p) => `[${p}]`).join(" ");
3520
- text += `${day}: ${slot.time} ${platforms}
2830
+ const slotId = slot.id ?? slot._id ?? `${slot.dayOfWeek}_${slot.time}`;
2831
+ text += `${day}: ${slot.time} ${platforms} (id: ${slotId})
3521
2832
  `;
3522
2833
  }
3523
2834
  }
@@ -3538,17 +2849,7 @@ Next slot: ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
3538
2849
  );
3539
2850
  server.tool(
3540
2851
  "schedule_to_queue",
3541
- `Add a post to the customer's content queue for automatic scheduling.
3542
-
3543
- 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.
3544
-
3545
- REQUIRED WORKFLOW:
3546
- 1. BEFORE creating content, call get_brand_voice and get_audience
3547
- 2. ALWAYS set confirmed=false first to preview
3548
- 3. Show the user a visual preview before confirming
3549
- 4. Tell the user which queue slot the post will be assigned to
3550
-
3551
- 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.`,
2852
+ "Add a post to the next available queue slot. Check get_queue first. Set confirmed=false to preview first.",
3552
2853
  {
3553
2854
  content: z12.string().describe("The text content of the post"),
3554
2855
  platforms: z12.array(z12.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -3649,7 +2950,7 @@ function timeAgo(dateStr) {
3649
2950
  function registerNotificationTools(server, client) {
3650
2951
  server.tool(
3651
2952
  "get_notifications",
3652
- "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.",
2953
+ "Get recent notifications including post results, account warnings, and errors.",
3653
2954
  {
3654
2955
  unread_only: z13.boolean().optional().describe("If true, only show unread notifications. Default false."),
3655
2956
  limit: z13.number().optional().describe("Max notifications to return. Default 10.")
@@ -3694,7 +2995,7 @@ No notifications found. All clear!`
3694
2995
  const action = n.actionLabel || "View";
3695
2996
  if (n.resourceType === "post" && n.resourceId) {
3696
2997
  line += `
3697
- Action: ${action} \u2192 retry_post with post_id "${n.resourceId}"`;
2998
+ Action: ${action} \u2192 get_post with post_id "${n.resourceId}"`;
3698
2999
  } else {
3699
3000
  line += `
3700
3001
  Action: ${action} \u2192 ${n.actionUrl}`;
@@ -3719,10 +3020,36 @@ Showing ${notifications.length} of ${result.unread_count !== void 0 ? "total" :
3719
3020
  }
3720
3021
 
3721
3022
  // src/tools/publishing-rules.ts
3023
+ var WORKFLOW = {
3024
+ before_writing_content: [
3025
+ "Call get_brand_voice to load tone and style rules",
3026
+ "Call get_audience to understand who the content is for",
3027
+ "For newsletters: also call get_newsletter_template and get_past_newsletters"
3028
+ ],
3029
+ before_publishing: [
3030
+ "Always set confirmed=false first to generate a preview",
3031
+ "Show the user a visual preview before confirming",
3032
+ "Never set confirmed=true without explicit user approval",
3033
+ "For newsletters: show subscriber count and send time before confirming",
3034
+ "If require_double_confirm is true, ask for confirmation twice"
3035
+ ],
3036
+ replies_and_engagement: [
3037
+ "Draft the reply and show it before sending",
3038
+ "Set confirmed=false first to preview",
3039
+ "Remind user that replies are public"
3040
+ ],
3041
+ newsletters: [
3042
+ "Always render newsletter as HTML artifact/preview before pushing to ESP",
3043
+ "Use table-based layouts, inline CSS only, 600px max width",
3044
+ "Email-safe fonts only: Arial, Helvetica, Georgia, Verdana",
3045
+ "All images must use absolute URLs",
3046
+ "Keep total email under 102KB to avoid Gmail clipping"
3047
+ ]
3048
+ };
3722
3049
  function registerPublishingRulesTools(server, client) {
3723
3050
  server.tool(
3724
3051
  "get_publishing_rules",
3725
- `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.`,
3052
+ "Get publishing rules and the content creation workflow. Call this BEFORE publishing, scheduling, or sending any content.",
3726
3053
  {},
3727
3054
  {
3728
3055
  title: "Get Publishing Rules",
@@ -3733,90 +3060,30 @@ function registerPublishingRulesTools(server, client) {
3733
3060
  },
3734
3061
  async () => {
3735
3062
  const rules = await client.getPublishingRules();
3736
- let text = "## Publishing Rules\n\n";
3737
- text += `Social media default: **${rules.socialDefaultAction ?? "draft"}**`;
3738
- if (rules.socialDefaultAction === "draft") text += " (content is saved as draft, not published)";
3739
- text += "\n";
3740
- text += `Newsletter default: **${rules.newsletterDefaultAction ?? "draft"}**`;
3741
- if (rules.newsletterDefaultAction === "draft") text += " (content is saved as draft, not sent)";
3742
- text += "\n";
3743
- text += `Preview required before publish: **${rules.requirePreviewBeforePublish ? "yes" : "no"}**
3744
- `;
3745
- text += `Double confirmation for newsletters: **${rules.requireDoubleConfirmNewsletter ? "yes" : "no"}**
3746
- `;
3747
- text += `Double confirmation for social: **${rules.requireDoubleConfirmSocial ? "yes" : "no"}**
3748
- `;
3749
- text += `Immediate publish allowed: **${rules.allowImmediatePublish ? "yes" : "no"}**
3750
- `;
3751
- text += `Immediate send allowed: **${rules.allowImmediateSend ? "yes" : "no"}**
3752
- `;
3753
- text += `Max posts per day: **${rules.maxPostsPerDay ?? "unlimited"}**
3754
- `;
3755
- if (rules.blockedWords && rules.blockedWords.length > 0) {
3756
- text += `Blocked words: **${rules.blockedWords.join(", ")}**
3757
- `;
3758
- } else {
3759
- text += "Blocked words: none\n";
3760
- }
3761
- if (rules.requiredDisclaimer) {
3762
- text += `Required disclaimer: "${rules.requiredDisclaimer}"
3763
- `;
3764
- } else {
3765
- text += "Required disclaimer: none\n";
3766
- }
3767
- return { content: [{ type: "text", text }] };
3768
- }
3769
- );
3770
- }
3771
-
3772
- // src/tools/audit-log.ts
3773
- import { z as z14 } from "zod";
3774
- function registerAuditLogTools(server, client) {
3775
- server.tool(
3776
- "get_audit_log",
3777
- "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.",
3778
- {
3779
- limit: z14.number().optional().describe("Maximum number of entries to return (default 20, max 200)"),
3780
- action: z14.string().optional().describe("Filter by action type prefix, e.g. 'post.published', 'newsletter.sent', 'post.' for all post actions"),
3781
- resource_type: z14.string().optional().describe("Filter by resource type: post, newsletter, account, media, publishing_rules, brand_voice")
3782
- },
3783
- {
3784
- title: "Get Audit Log",
3785
- readOnlyHint: true,
3786
- destructiveHint: false,
3787
- idempotentHint: true,
3788
- openWorldHint: false
3789
- },
3790
- async (args) => {
3791
- const params = {};
3792
- if (args.limit) params.limit = String(args.limit);
3793
- if (args.action) params.action = args.action;
3794
- if (args.resource_type) params.resource_type = args.resource_type;
3795
- const data = await client.getAuditLog(params);
3796
- const entries = data?.entries ?? [];
3797
- if (entries.length === 0) {
3798
- return {
3799
- content: [{ type: "text", text: "## Audit Log\n\nNo entries found." }]
3800
- };
3801
- }
3802
- let text = `## Audit Log (${entries.length} of ${data?.total ?? entries.length} entries)
3803
-
3804
- `;
3805
- for (const entry of entries) {
3806
- const date = entry.createdAt ? new Date(entry.createdAt).toLocaleString() : "unknown";
3807
- const details = entry.details && Object.keys(entry.details).length > 0 ? ` \u2014 ${JSON.stringify(entry.details)}` : "";
3808
- text += `- **${date}** | ${entry.action} | ${entry.resourceType}`;
3809
- if (entry.resourceId) text += ` #${entry.resourceId}`;
3810
- text += `${details}
3811
- `;
3812
- }
3813
- return { content: [{ type: "text", text }] };
3063
+ const response = {
3064
+ rules: {
3065
+ social_default_action: rules.socialDefaultAction ?? "draft",
3066
+ newsletter_default_action: rules.newsletterDefaultAction ?? "draft",
3067
+ require_preview_before_publish: rules.requirePreviewBeforePublish ?? true,
3068
+ require_double_confirm_newsletter: rules.requireDoubleConfirmNewsletter ?? true,
3069
+ require_double_confirm_social: rules.requireDoubleConfirmSocial ?? false,
3070
+ allow_immediate_publish: rules.allowImmediatePublish ?? false,
3071
+ allow_immediate_send: rules.allowImmediateSend ?? false,
3072
+ max_posts_per_day: rules.maxPostsPerDay ?? null,
3073
+ blocked_words: rules.blockedWords ?? [],
3074
+ required_disclaimer: rules.requiredDisclaimer ?? null
3075
+ },
3076
+ workflow: WORKFLOW
3077
+ };
3078
+ return {
3079
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
3080
+ };
3814
3081
  }
3815
3082
  );
3816
3083
  }
3817
3084
 
3818
3085
  // src/tools/sources.ts
3819
- import { z as z15 } from "zod";
3086
+ import { z as z14 } from "zod";
3820
3087
  var TYPE_LABELS = {
3821
3088
  feed: "RSS Feed",
3822
3089
  website: "Website",
@@ -3838,12 +3105,12 @@ var TYPE_HINTS = {
3838
3105
  function registerSourceTools(server, client) {
3839
3106
  server.tool(
3840
3107
  "get_sources",
3841
- "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.",
3108
+ "Get saved content sources (RSS feeds, websites, YouTube channels, search topics).",
3842
3109
  {
3843
- type: z15.string().optional().describe(
3110
+ type: z14.string().optional().describe(
3844
3111
  "Optional: filter by type (feed, website, youtube, search, podcast, reddit, social)"
3845
3112
  ),
3846
- category: z15.string().optional().describe("Optional: filter by category")
3113
+ category: z14.string().optional().describe("Optional: filter by category")
3847
3114
  },
3848
3115
  {
3849
3116
  title: "Get Content Sources",
@@ -3904,13 +3171,13 @@ function registerSourceTools(server, client) {
3904
3171
  );
3905
3172
  server.tool(
3906
3173
  "add_source",
3907
- "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.",
3174
+ "Save a new content source to monitor.",
3908
3175
  {
3909
- name: z15.string().describe("Display name for the source"),
3910
- url: z15.string().optional().describe(
3911
- "URL of the source (RSS feed, website, YouTube channel, subreddit, or social profile URL). Not needed for 'search' type."
3176
+ name: z14.string().describe("Display name for the source"),
3177
+ url: z14.string().optional().describe(
3178
+ "URL of the source. Not needed for 'search' type."
3912
3179
  ),
3913
- type: z15.enum([
3180
+ type: z14.enum([
3914
3181
  "feed",
3915
3182
  "website",
3916
3183
  "youtube",
@@ -3919,12 +3186,12 @@ function registerSourceTools(server, client) {
3919
3186
  "reddit",
3920
3187
  "social"
3921
3188
  ]).describe("Type of source. Determines how to fetch content from it."),
3922
- category: z15.string().optional().describe(
3923
- "Optional category for organization (e.g. 'competitors', 'industry', 'inspiration', 'my-content')"
3189
+ category: z14.string().optional().describe(
3190
+ "Optional category for organization (e.g. 'competitors', 'industry', 'inspiration')"
3924
3191
  ),
3925
- tags: z15.array(z15.string()).optional().describe("Optional tags for filtering"),
3926
- notes: z15.string().optional().describe("Optional notes about why this source matters"),
3927
- searchQuery: z15.string().optional().describe(
3192
+ tags: z14.array(z14.string()).optional().describe("Optional tags for filtering"),
3193
+ notes: z14.string().optional().describe("Optional notes about why this source matters"),
3194
+ searchQuery: z14.string().optional().describe(
3928
3195
  "For 'search' type only: the keyword or topic to search for"
3929
3196
  )
3930
3197
  },
@@ -3964,12 +3231,12 @@ function registerSourceTools(server, client) {
3964
3231
  );
3965
3232
  server.tool(
3966
3233
  "update_source",
3967
- "Update a saved content source. Use when the customer wants to rename, recategorize, change the URL, add notes, or deactivate a source.",
3234
+ "Update a saved content source.",
3968
3235
  {
3969
- source_id: z15.number().describe("The ID of the source to update"),
3970
- name: z15.string().optional().describe("New display name"),
3971
- url: z15.string().optional().describe("New URL"),
3972
- type: z15.enum([
3236
+ source_id: z14.number().describe("The ID of the source to update"),
3237
+ name: z14.string().optional().describe("New display name"),
3238
+ url: z14.string().optional().describe("New URL"),
3239
+ type: z14.enum([
3973
3240
  "feed",
3974
3241
  "website",
3975
3242
  "youtube",
@@ -3978,11 +3245,11 @@ function registerSourceTools(server, client) {
3978
3245
  "reddit",
3979
3246
  "social"
3980
3247
  ]).optional().describe("New source type"),
3981
- category: z15.string().optional().describe("New category"),
3982
- tags: z15.array(z15.string()).optional().describe("New tags"),
3983
- notes: z15.string().optional().describe("New notes"),
3984
- searchQuery: z15.string().optional().describe("New search query"),
3985
- isActive: z15.boolean().optional().describe("Set to false to deactivate")
3248
+ category: z14.string().optional().describe("New category"),
3249
+ tags: z14.array(z14.string()).optional().describe("New tags"),
3250
+ notes: z14.string().optional().describe("New notes"),
3251
+ searchQuery: z14.string().optional().describe("New search query"),
3252
+ isActive: z14.boolean().optional().describe("Set to false to deactivate")
3986
3253
  },
3987
3254
  {
3988
3255
  title: "Update Content Source",
@@ -4010,15 +3277,10 @@ function registerSourceTools(server, client) {
4010
3277
  );
4011
3278
  server.tool(
4012
3279
  "remove_source",
4013
- `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.
4014
-
4015
- REQUIRED WORKFLOW:
4016
- 1. ALWAYS set confirmed=false first to preview which source will be deleted
4017
- 2. Show the user the source details before confirming
4018
- 3. Only confirm after explicit user approval`,
3280
+ "Remove a content source. Set confirmed=false to preview first.",
4019
3281
  {
4020
- source_id: z15.number().describe("The ID of the source to remove"),
4021
- confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
3282
+ source_id: z14.number().describe("The ID of the source to remove"),
3283
+ confirmed: z14.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
4022
3284
  },
4023
3285
  {
4024
3286
  title: "Remove Content Source",
@@ -4048,237 +3310,14 @@ This will permanently remove this content source. Call this tool again with conf
4048
3310
  }
4049
3311
  );
4050
3312
  }
4051
-
4052
- // src/tools/carousel.ts
4053
- import { z as z16 } from "zod";
4054
- function registerCarouselTools(server, client) {
4055
- server.tool(
4056
- "get_carousel_template",
4057
- "Get the customer's carousel template configuration. Returns brand colors, logo, CTA text, and other visual settings used to generate carousels from article URLs.",
4058
- {},
4059
- {
4060
- title: "Get Carousel Template",
4061
- readOnlyHint: true,
4062
- destructiveHint: false,
4063
- idempotentHint: true,
4064
- openWorldHint: false
4065
- },
4066
- async () => {
4067
- try {
4068
- const template = await client.getCarouselTemplate();
4069
- const lines = [];
4070
- lines.push(`## Carousel Template: ${template.name || "Not configured"}`);
4071
- lines.push("");
4072
- if (!template.id) {
4073
- lines.push("No carousel template has been configured yet.");
4074
- lines.push("");
4075
- lines.push("Use `save_carousel_template` to set one up with your brand name, colors, and CTA text.");
4076
- return {
4077
- content: [{ type: "text", text: lines.join("\n") }]
4078
- };
4079
- }
4080
- lines.push(`**Brand Name:** ${template.brandName || "\u2014"}`);
4081
- if (template.logoUrl) lines.push(`**Logo:** ${template.logoUrl}`);
4082
- lines.push(`**Primary Color:** ${template.colorPrimary || "#1a1a2e"}`);
4083
- lines.push(`**Secondary Color:** ${template.colorSecondary || "#e94560"}`);
4084
- lines.push(`**Accent Color:** ${template.colorAccent || "#ffffff"}`);
4085
- lines.push(`**CTA Text:** ${template.ctaText || "Read the full story \u2192"}`);
4086
- lines.push(`**Logo Placement:** ${template.logoPlacement || "top-left"}`);
4087
- lines.push("");
4088
- return {
4089
- content: [{ type: "text", text: lines.join("\n") }]
4090
- };
4091
- } catch (error) {
4092
- const message = error instanceof Error ? error.message : "Unknown error";
4093
- if (message.includes("404")) {
4094
- return {
4095
- content: [
4096
- {
4097
- type: "text",
4098
- text: "No carousel template has been configured yet. Use `save_carousel_template` to set one up."
4099
- }
4100
- ]
4101
- };
4102
- }
4103
- throw error;
4104
- }
4105
- }
4106
- );
4107
- server.tool(
4108
- "save_carousel_template",
4109
- `Create or update the customer's carousel template. This configures the visual style for auto-generated carousels (brand colors, logo, CTA text).
4110
-
4111
- USAGE:
4112
- - Set confirmed=false first to preview, then confirmed=true after user approval
4113
- - brand_name is required
4114
- - All color values should be hex codes (e.g. "#1a1a2e")
4115
- - logo_placement: "top-left", "top-right", or "top-center"`,
4116
- {
4117
- brand_name: z16.string().max(255).describe("Brand name displayed on CTA slide"),
4118
- color_primary: z16.string().max(20).optional().describe('Primary background color (hex, e.g. "#1a1a2e")'),
4119
- color_secondary: z16.string().max(20).optional().describe('Secondary/accent color for buttons and highlights (hex, e.g. "#e94560")'),
4120
- color_accent: z16.string().max(20).optional().describe('Text color (hex, e.g. "#ffffff")'),
4121
- cta_text: z16.string().max(255).optional().describe('Call-to-action text on the final slide (e.g. "Read the full story \u2192")'),
4122
- logo_placement: z16.enum(["top-left", "top-right", "top-center"]).optional().describe("Where to place the logo on slides"),
4123
- confirmed: z16.boolean().default(false).describe("Set to true to confirm and save. If false, returns a preview.")
4124
- },
4125
- {
4126
- title: "Save Carousel Template",
4127
- readOnlyHint: false,
4128
- destructiveHint: false,
4129
- idempotentHint: false,
4130
- openWorldHint: false
4131
- },
4132
- async (args) => {
4133
- const payload = {
4134
- brandName: args.brand_name
4135
- };
4136
- if (args.color_primary !== void 0) payload.colorPrimary = args.color_primary;
4137
- if (args.color_secondary !== void 0) payload.colorSecondary = args.color_secondary;
4138
- if (args.color_accent !== void 0) payload.colorAccent = args.color_accent;
4139
- if (args.cta_text !== void 0) payload.ctaText = args.cta_text;
4140
- if (args.logo_placement !== void 0) payload.logoPlacement = args.logo_placement;
4141
- if (args.confirmed !== true) {
4142
- const lines2 = [];
4143
- lines2.push("## Carousel Template Preview");
4144
- lines2.push("");
4145
- lines2.push(`**Brand Name:** ${args.brand_name}`);
4146
- if (args.color_primary) lines2.push(`**Primary Color:** ${args.color_primary}`);
4147
- if (args.color_secondary) lines2.push(`**Secondary Color:** ${args.color_secondary}`);
4148
- if (args.color_accent) lines2.push(`**Accent Color:** ${args.color_accent}`);
4149
- if (args.cta_text) lines2.push(`**CTA Text:** ${args.cta_text}`);
4150
- if (args.logo_placement) lines2.push(`**Logo Placement:** ${args.logo_placement}`);
4151
- lines2.push("");
4152
- lines2.push("---");
4153
- lines2.push("Call this tool again with **confirmed=true** to save these settings.");
4154
- return {
4155
- content: [{ type: "text", text: lines2.join("\n") }]
4156
- };
4157
- }
4158
- const result = await client.updateCarouselTemplate(payload);
4159
- const lines = [];
4160
- lines.push(`Carousel template "${result.brandName || args.brand_name}" has been saved successfully.`);
4161
- lines.push("");
4162
- const updated = ["brand name"];
4163
- if (args.color_primary) updated.push("primary color");
4164
- if (args.color_secondary) updated.push("secondary color");
4165
- if (args.color_accent) updated.push("accent color");
4166
- if (args.cta_text) updated.push("CTA text");
4167
- if (args.logo_placement) updated.push("logo placement");
4168
- lines.push(`**Updated:** ${updated.join(", ")}`);
4169
- return {
4170
- content: [{ type: "text", text: lines.join("\n") }]
4171
- };
4172
- }
4173
- );
4174
- server.tool(
4175
- "generate_carousel",
4176
- "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.",
4177
- {
4178
- url: z16.string().url().describe("The article URL to generate a carousel from")
4179
- },
4180
- {
4181
- title: "Generate Carousel",
4182
- readOnlyHint: false,
4183
- destructiveHint: false,
4184
- idempotentHint: false,
4185
- openWorldHint: true
4186
- },
4187
- async (args) => {
4188
- try {
4189
- const result = await client.generateCarousel({
4190
- url: args.url
4191
- });
4192
- const lines = [];
4193
- lines.push("## Carousel Generated");
4194
- lines.push("");
4195
- lines.push(`**Headline:** ${result.article.headline}`);
4196
- lines.push(`**Source:** ${result.article.sourceDomain}`);
4197
- lines.push(`**Slides:** ${result.slides.length}`);
4198
- lines.push("");
4199
- lines.push("### Key Points");
4200
- for (const point of result.article.keyPoints) {
4201
- lines.push(`- ${point}`);
4202
- }
4203
- lines.push("");
4204
- lines.push("### Slide URLs");
4205
- for (const slide of result.slides) {
4206
- lines.push(`${slide.slideNumber}. [${slide.type}](${slide.cdnUrl})`);
4207
- }
4208
- lines.push("");
4209
- lines.push("These URLs can be used as `media_urls` when creating a post.");
4210
- return {
4211
- content: [{ type: "text", text: lines.join("\n") }]
4212
- };
4213
- } catch (error) {
4214
- const message = error instanceof Error ? error.message : "Unknown error";
4215
- return {
4216
- content: [
4217
- {
4218
- type: "text",
4219
- text: `Failed to generate carousel: ${message}`
4220
- }
4221
- ],
4222
- isError: true
4223
- };
4224
- }
4225
- }
4226
- );
4227
- server.tool(
4228
- "preview_carousel",
4229
- "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.",
4230
- {
4231
- url: z16.string().url().describe("The article URL to preview")
4232
- },
4233
- {
4234
- title: "Preview Carousel",
4235
- readOnlyHint: true,
4236
- destructiveHint: false,
4237
- idempotentHint: false,
4238
- openWorldHint: true
4239
- },
4240
- async (args) => {
4241
- try {
4242
- const result = await client.previewCarousel({
4243
- url: args.url
4244
- });
4245
- const lines = [];
4246
- lines.push("## Carousel Preview");
4247
- lines.push("");
4248
- lines.push(`**Headline:** ${result.article.headline}`);
4249
- lines.push(`**Source:** ${result.article.sourceDomain}`);
4250
- lines.push("");
4251
- lines.push(`**Hero Slide:** ${result.cdnUrl}`);
4252
- lines.push("");
4253
- lines.push("Use `generate_carousel` to create the full carousel with all slides.");
4254
- return {
4255
- content: [{ type: "text", text: lines.join("\n") }]
4256
- };
4257
- } catch (error) {
4258
- const message = error instanceof Error ? error.message : "Unknown error";
4259
- return {
4260
- content: [
4261
- {
4262
- type: "text",
4263
- text: `Failed to preview carousel: ${message}`
4264
- }
4265
- ],
4266
- isError: true
4267
- };
4268
- }
4269
- }
4270
- );
4271
- }
4272
3313
  export {
4273
3314
  BuzzPosterClient,
4274
3315
  registerAccountInfoTool,
4275
3316
  registerAccountTools,
4276
3317
  registerAnalyticsTools,
4277
3318
  registerAudienceTools,
4278
- registerAuditLogTools,
4279
3319
  registerBrandVoiceTools,
4280
3320
  registerCalendarTools,
4281
- registerCarouselTools,
4282
3321
  registerInboxTools,
4283
3322
  registerKnowledgeTools,
4284
3323
  registerMediaTools,