@buzzposter/mcp 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -65,9 +65,6 @@ var BuzzPosterClient = class {
65
65
  async deletePost(id) {
66
66
  return this.request("DELETE", `/api/v1/posts/${id}`);
67
67
  }
68
- async retryPost(id) {
69
- return this.request("POST", `/api/v1/posts/${id}/retry`);
70
- }
71
68
  // Analytics
72
69
  async getAnalytics(params) {
73
70
  return this.request("GET", "/api/v1/analytics", void 0, params);
@@ -104,9 +101,6 @@ var BuzzPosterClient = class {
104
101
  async uploadFromUrl(data) {
105
102
  return this.request("POST", "/api/v1/media/upload-from-url", data);
106
103
  }
107
- async getUploadUrl(data) {
108
- return this.request("POST", "/api/v1/media/presign", data);
109
- }
110
104
  async listMedia() {
111
105
  return this.request("GET", "/api/v1/media");
112
106
  }
@@ -145,9 +139,6 @@ var BuzzPosterClient = class {
145
139
  async listTags() {
146
140
  return this.request("GET", "/api/v1/newsletters/tags");
147
141
  }
148
- async listSequences() {
149
- return this.request("GET", "/api/v1/newsletters/sequences");
150
- }
151
142
  async listForms() {
152
143
  return this.request("GET", "/api/v1/newsletters/forms");
153
144
  }
@@ -158,36 +149,15 @@ var BuzzPosterClient = class {
158
149
  async listAutomations(params) {
159
150
  return this.request("GET", "/api/v1/newsletters/automations", void 0, params);
160
151
  }
161
- async enrollInAutomation(automationId, data) {
162
- return this.request("POST", `/api/v1/newsletters/automations/${automationId}/enroll`, data);
163
- }
164
152
  async listSegments(params) {
165
153
  return this.request("GET", "/api/v1/newsletters/segments", void 0, params);
166
154
  }
167
155
  async getSegment(id, params) {
168
156
  return this.request("GET", `/api/v1/newsletters/segments/${id}`, void 0, params);
169
157
  }
170
- async getSegmentMembers(segmentId, params) {
171
- return this.request("GET", `/api/v1/newsletters/segments/${segmentId}/members`, void 0, params);
172
- }
173
- async deleteSegment(id) {
174
- return this.request("DELETE", `/api/v1/newsletters/segments/${id}`);
175
- }
176
- async recalculateSegment(id) {
177
- return this.request("POST", `/api/v1/newsletters/segments/${id}/recalculate`);
178
- }
179
158
  async listCustomFields() {
180
159
  return this.request("GET", "/api/v1/newsletters/custom-fields");
181
160
  }
182
- async createCustomField(data) {
183
- return this.request("POST", "/api/v1/newsletters/custom-fields", data);
184
- }
185
- async updateCustomField(id, data) {
186
- return this.request("PUT", `/api/v1/newsletters/custom-fields/${id}`, data);
187
- }
188
- async deleteCustomField(id) {
189
- return this.request("DELETE", `/api/v1/newsletters/custom-fields/${id}`);
190
- }
191
161
  async tagSubscriber(subscriberId, data) {
192
162
  return this.request("POST", `/api/v1/newsletters/subscribers/${subscriberId}/tags`, data);
193
163
  }
@@ -197,33 +167,12 @@ var BuzzPosterClient = class {
197
167
  async deleteSubscriber(id) {
198
168
  return this.request("DELETE", `/api/v1/newsletters/subscribers/${id}`);
199
169
  }
200
- async bulkCreateSubscribers(data) {
201
- return this.request("POST", "/api/v1/newsletters/subscribers/bulk", data);
202
- }
203
- async getReferralProgram() {
204
- return this.request("GET", "/api/v1/newsletters/referral-program");
205
- }
206
- async listEspTiers() {
207
- return this.request("GET", "/api/v1/newsletters/tiers");
208
- }
209
170
  async listPostTemplates() {
210
171
  return this.request("GET", "/api/v1/newsletters/post-templates");
211
172
  }
212
173
  async getPostAggregateStats() {
213
174
  return this.request("GET", "/api/v1/newsletters/broadcasts/aggregate-stats");
214
175
  }
215
- async deleteBroadcast(id) {
216
- return this.request("DELETE", `/api/v1/newsletters/broadcasts/${id}`);
217
- }
218
- async listEspWebhooks() {
219
- return this.request("GET", "/api/v1/newsletters/webhooks");
220
- }
221
- async createEspWebhook(data) {
222
- return this.request("POST", "/api/v1/newsletters/webhooks", data);
223
- }
224
- async deleteEspWebhook(id) {
225
- return this.request("DELETE", `/api/v1/newsletters/webhooks/${id}`);
226
- }
227
176
  // Knowledge
228
177
  async listKnowledge(params) {
229
178
  return this.request("GET", "/api/v1/knowledge", void 0, params);
@@ -251,12 +200,6 @@ var BuzzPosterClient = class {
251
200
  async updateAudience(id, data) {
252
201
  return this.request("PUT", `/api/v1/audiences/${id}`, data);
253
202
  }
254
- async listAudiences() {
255
- return this.request("GET", "/api/v1/audiences");
256
- }
257
- async deleteAudience(id) {
258
- return this.request("DELETE", `/api/v1/audiences/${id}`);
259
- }
260
203
  // Newsletter Templates
261
204
  async getTemplate(id) {
262
205
  if (id) return this.request("GET", `/api/v1/templates/${id}`);
@@ -348,10 +291,6 @@ var BuzzPosterClient = class {
348
291
  async getPublishingRules() {
349
292
  return this.request("GET", "/api/v1/publishing-rules");
350
293
  }
351
- // Audit Log
352
- async getAuditLog(params) {
353
- return this.request("GET", "/api/v1/audit-log", void 0, params);
354
- }
355
294
  // Newsletter Validation
356
295
  async validateNewsletter(data) {
357
296
  return this.request("POST", "/api/v1/newsletters/validate", data);
@@ -364,108 +303,6 @@ var BuzzPosterClient = class {
364
303
  async testBroadcast(id, data) {
365
304
  return this.request("POST", `/api/v1/newsletters/broadcasts/${id}/test`, data);
366
305
  }
367
- // Carousel templates
368
- async getCarouselTemplate() {
369
- return this.request("GET", "/api/v1/carousel-templates");
370
- }
371
- async updateCarouselTemplate(data) {
372
- return this.request("PUT", "/api/v1/carousel-templates", data);
373
- }
374
- async deleteCarouselTemplate() {
375
- return this.request("DELETE", "/api/v1/carousel-templates");
376
- }
377
- async generateCarousel(data) {
378
- return this.request("POST", "/api/v1/carousel-templates/generate", data);
379
- }
380
- async previewCarousel(data) {
381
- return this.request("POST", "/api/v1/carousel-templates/preview", data);
382
- }
383
- // Canva
384
- async getCanvaStatus() {
385
- return this.request("GET", "/api/v1/canva/status");
386
- }
387
- async canvaUploadAsset(data) {
388
- return this.request("POST", "/api/v1/canva/upload-asset", data);
389
- }
390
- async canvaCreateDesign(data) {
391
- return this.request("POST", "/api/v1/canva/designs", data);
392
- }
393
- async canvaExportDesign(data) {
394
- return this.request("POST", "/api/v1/canva/exports", data);
395
- }
396
- async canvaSearchDesigns(query) {
397
- return this.request("GET", "/api/v1/canva/designs", void 0, { query });
398
- }
399
- async canvaGetDesign(designId) {
400
- return this.request("GET", `/api/v1/canva/designs/${designId}`);
401
- }
402
- // SendGrid Newsletter (BuzzPoster-managed)
403
- async sgListLists() {
404
- return this.request("GET", "/api/v1/newsletter/lists");
405
- }
406
- async sgCreateList(data) {
407
- return this.request("POST", "/api/v1/newsletter/lists", data);
408
- }
409
- async sgUpdateList(id, data) {
410
- return this.request("PUT", `/api/v1/newsletter/lists/${id}`, data);
411
- }
412
- async sgDeleteList(id) {
413
- return this.request("DELETE", `/api/v1/newsletter/lists/${id}`);
414
- }
415
- async sgListSubscribers(params) {
416
- return this.request("GET", "/api/v1/newsletter/subscribers", void 0, params);
417
- }
418
- async sgAddSubscriber(data) {
419
- return this.request("POST", "/api/v1/newsletter/subscribers", data);
420
- }
421
- async sgGetSubscriber(id) {
422
- return this.request("GET", `/api/v1/newsletter/subscribers/${id}`);
423
- }
424
- async sgUpdateSubscriber(id, data) {
425
- return this.request("PUT", `/api/v1/newsletter/subscribers/${id}`, data);
426
- }
427
- async sgRemoveSubscriber(id) {
428
- return this.request("DELETE", `/api/v1/newsletter/subscribers/${id}`);
429
- }
430
- async sgBulkImportSubscribers(data) {
431
- return this.request("POST", "/api/v1/newsletter/subscribers/bulk", data);
432
- }
433
- async sgListSends(params) {
434
- return this.request("GET", "/api/v1/newsletter/sends", void 0, params);
435
- }
436
- async sgGetSend(id) {
437
- return this.request("GET", `/api/v1/newsletter/sends/${id}`);
438
- }
439
- async sgCreateSend(data) {
440
- return this.request("POST", "/api/v1/newsletter/sends", data);
441
- }
442
- async sgUpdateSend(id, data) {
443
- return this.request("PUT", `/api/v1/newsletter/sends/${id}`, data);
444
- }
445
- async sgDeleteSend(id) {
446
- return this.request("DELETE", `/api/v1/newsletter/sends/${id}`);
447
- }
448
- async sgSendNewsletter(id) {
449
- return this.request("POST", `/api/v1/newsletter/sends/${id}/send`);
450
- }
451
- async sgTestSend(id, data) {
452
- return this.request("POST", `/api/v1/newsletter/sends/${id}/test`, data);
453
- }
454
- async sgScheduleSend(id, data) {
455
- return this.request("POST", `/api/v1/newsletter/sends/${id}/schedule`, data);
456
- }
457
- async sgCancelSend(id) {
458
- return this.request("POST", `/api/v1/newsletter/sends/${id}/cancel`);
459
- }
460
- async sgAuthenticateDomain(domain) {
461
- return this.request("POST", "/api/v1/newsletter/domains/authenticate", { domain });
462
- }
463
- async sgValidateDomain(domainId) {
464
- return this.request("POST", `/api/v1/newsletter/domains/${domainId}/validate`);
465
- }
466
- async sgListDomains() {
467
- return this.request("GET", "/api/v1/newsletter/domains");
468
- }
469
306
  };
470
307
 
471
308
  // src/tools/posts.ts
@@ -473,23 +310,7 @@ import { z } from "zod";
473
310
  function registerPostTools(server2, client2) {
474
311
  server2.tool(
475
312
  "post",
476
- `Create and publish a post to one or more social media platforms. Supports Twitter, Instagram, LinkedIn, and Facebook.
477
-
478
- 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.
479
-
480
- REQUIRED WORKFLOW:
481
- 1. BEFORE creating any content, call get_brand_voice and get_audience to match the customer's tone
482
- 2. BEFORE publishing, call get_publishing_rules to check safety settings
483
- 3. ALWAYS set confirmed=false first to get a preview -- never set confirmed=true on the first call
484
- 4. Show the user a visual preview of the post before confirming
485
- 5. Only set confirmed=true after the user explicitly says to publish/post/send
486
- 6. If publishing_rules.social_default_action is 'draft', create as draft unless the user explicitly asks to publish
487
- 7. If publishing_rules.require_double_confirm_social is true, ask for confirmation twice before publishing
488
- 8. NEVER publish immediately without user confirmation, even if the user says "post this" -- always show the preview first
489
-
490
- 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.
491
-
492
- IMPORTANT: Always show the user a preview of the post content before publishing. Never publish without explicit user approval. If no preview has been shown yet, do NOT set confirmed=true -- render the preview first.`,
313
+ "Publish a post to one or more social platforms. Set confirmed=false to preview first.",
493
314
  {
494
315
  content: z.string().optional().describe("The text content of the post"),
495
316
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -581,118 +402,9 @@ Call this tool again with confirmed=true to proceed.`;
581
402
  };
582
403
  }
583
404
  );
584
- server2.tool(
585
- "cross_post",
586
- `Post the same content to all connected platforms at once.
587
-
588
- 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.
589
-
590
- REQUIRED WORKFLOW:
591
- 1. BEFORE creating content, call get_brand_voice and get_audience
592
- 2. BEFORE publishing, call get_publishing_rules
593
- 3. ALWAYS set confirmed=false first to preview
594
- 4. Show the user a visual preview showing how the post will appear on each platform
595
- 5. This is a high-impact action (goes to ALL platforms) -- always require explicit confirmation
596
- 6. If publishing_rules.require_double_confirm_social is true, ask twice
597
-
598
- 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.`,
599
- {
600
- content: z.string().describe("The text content to post everywhere"),
601
- media_urls: z.array(z.string()).optional().describe("Public URLs of media to attach"),
602
- confirmed: z.boolean().default(false).describe(
603
- "Set to true to confirm and publish. If false or missing, returns a preview for user approval."
604
- )
605
- },
606
- {
607
- title: "Cross-Post to All Platforms",
608
- readOnlyHint: false,
609
- destructiveHint: false,
610
- idempotentHint: false,
611
- openWorldHint: true
612
- },
613
- async (args) => {
614
- const accountsData = await client2.listAccounts();
615
- const platforms = (accountsData.accounts ?? []).map(
616
- (a) => a.platform
617
- );
618
- if (platforms.length === 0) {
619
- return {
620
- content: [
621
- {
622
- type: "text",
623
- text: "No connected social accounts found. Connect accounts first."
624
- }
625
- ]
626
- };
627
- }
628
- if (args.confirmed !== true) {
629
- let rulesText = "";
630
- try {
631
- const rules = await client2.getPublishingRules();
632
- const blockedWords = rules.blockedWords ?? [];
633
- const foundBlocked = [];
634
- if (args.content && blockedWords.length > 0) {
635
- const lower = args.content.toLowerCase();
636
- for (const word of blockedWords) {
637
- if (lower.includes(word.toLowerCase())) foundBlocked.push(word);
638
- }
639
- }
640
- rulesText = `
641
- ### Safety Checks
642
- `;
643
- rulesText += `- Blocked words: ${foundBlocked.length > 0 ? `**FAILED** (found: ${foundBlocked.join(", ")})` : "PASS"}
644
- `;
645
- rulesText += `- Default action: ${rules.socialDefaultAction ?? "draft"}
646
- `;
647
- rulesText += `- Double confirmation required: ${rules.requireDoubleConfirmSocial ? "yes" : "no"}
648
- `;
649
- } catch {
650
- }
651
- const preview = `## Cross-Post Preview
652
-
653
- **Content:** "${args.content}"
654
- **Platforms:** ${platforms.join(", ")} (${platforms.length} platforms)
655
- ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
656
- ` : "") + rulesText + `
657
- **Action:** This will be published **immediately** to **all ${platforms.length} connected platforms**. This is a high-impact action.
658
-
659
- Call this tool again with confirmed=true to proceed.`;
660
- return { content: [{ type: "text", text: preview }] };
661
- }
662
- const body = {
663
- content: args.content,
664
- platforms: platforms.map((p) => ({ platform: p })),
665
- publishNow: true
666
- };
667
- if (args.media_urls?.length) {
668
- body.mediaItems = args.media_urls.map((url) => ({
669
- type: url.match(/\.(mp4|mov|avi|webm)$/i) ? "video" : "image",
670
- url
671
- }));
672
- }
673
- const result = await client2.createPost(body);
674
- return {
675
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
676
- };
677
- }
678
- );
679
405
  server2.tool(
680
406
  "schedule_post",
681
- `Schedule a post for future publication.
682
-
683
- 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.
684
-
685
- REQUIRED WORKFLOW:
686
- 1. BEFORE creating content, call get_brand_voice and get_audience
687
- 2. BEFORE scheduling, call get_publishing_rules
688
- 3. ALWAYS set confirmed=false first to preview
689
- 4. Show the user a visual preview with the scheduled date/time before confirming
690
- 5. Only confirm after explicit user approval
691
- 6. If the user hasn't specified a time, suggest one based on get_queue time slots
692
-
693
- 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.
694
-
695
- IMPORTANT: Always show the user a preview of the post content before scheduling. Never schedule without explicit user approval. If no preview has been shown yet, do NOT set confirmed=true -- render the preview first.`,
407
+ "Schedule a post for future publication. Set confirmed=false to preview first.",
696
408
  {
697
409
  content: z.string().optional().describe("The text content of the post"),
698
410
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -768,7 +480,7 @@ Call this tool again with confirmed=true to schedule.`;
768
480
  );
769
481
  server2.tool(
770
482
  "create_draft",
771
- "Save a post as a draft without publishing. This NEVER publishes \u2014 it only saves. No confirmation needed since nothing goes live.",
483
+ "Save a post as a draft without publishing. No confirmation needed.",
772
484
  {
773
485
  content: z.string().optional().describe("The text content of the draft"),
774
486
  platforms: z.array(z.string()).describe("Platforms this draft is intended for"),
@@ -811,7 +523,7 @@ Call this tool again with confirmed=true to schedule.`;
811
523
  );
812
524
  server2.tool(
813
525
  "list_posts",
814
- "List recent posts with their status and platform details.",
526
+ "List recent posts with status and platform details.",
815
527
  {
816
528
  status: z.string().optional().describe("Filter by status: published, scheduled, draft, failed"),
817
529
  limit: z.string().optional().describe("Number of posts to return")
@@ -835,7 +547,7 @@ Call this tool again with confirmed=true to schedule.`;
835
547
  );
836
548
  server2.tool(
837
549
  "get_post",
838
- "Get detailed information about a specific post.",
550
+ "Get detailed information about a specific post by ID.",
839
551
  {
840
552
  post_id: z.string().describe("The ID of the post to retrieve")
841
553
  },
@@ -853,117 +565,13 @@ Call this tool again with confirmed=true to schedule.`;
853
565
  };
854
566
  }
855
567
  );
856
- server2.tool(
857
- "retry_post",
858
- "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.",
859
- {
860
- post_id: z.string().describe("The ID of the post to retry"),
861
- confirmed: z.boolean().default(false).describe(
862
- "Set to true to confirm retry. If false/missing, shows post details first."
863
- )
864
- },
865
- {
866
- title: "Retry Failed Post",
867
- readOnlyHint: false,
868
- destructiveHint: false,
869
- idempotentHint: true,
870
- openWorldHint: true
871
- },
872
- async (args) => {
873
- if (args.confirmed !== true) {
874
- const post = await client2.getPost(args.post_id);
875
- const platforms = post.platforms ?? [];
876
- const failed = platforms.filter((p) => p.status === "failed");
877
- const published = platforms.filter((p) => p.status === "published");
878
- let text = `## Retry Post Preview
879
-
880
- `;
881
- text += `**Post ID:** ${args.post_id}
882
- `;
883
- text += `**Content:** "${(post.content ?? "").substring(0, 100)}"
884
- `;
885
- text += `**Status:** ${post.status}
886
-
887
- `;
888
- if (published.length > 0) {
889
- text += `**Already published on:** ${published.map((p) => p.platform).join(", ")}
890
- `;
891
- }
892
- if (failed.length > 0) {
893
- text += `**Failed on:** ${failed.map((p) => `${p.platform} (${p.error ?? "unknown error"})`).join(", ")}
894
- `;
895
- }
896
- text += `
897
- Retrying will only attempt the failed platforms. Call this tool again with confirmed=true to proceed.`;
898
- return { content: [{ type: "text", text }] };
899
- }
900
- const result = await client2.retryPost(args.post_id);
901
- return {
902
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
903
- };
904
- }
905
- );
906
568
  }
907
569
 
908
570
  // src/tools/accounts.ts
909
571
  function registerAccountTools(server2, client2) {
910
- server2.tool(
911
- "list_accounts",
912
- "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.",
913
- {},
914
- {
915
- title: "List Connected Accounts",
916
- readOnlyHint: true,
917
- destructiveHint: false,
918
- idempotentHint: true,
919
- openWorldHint: false
920
- },
921
- async () => {
922
- const result = await client2.listAccounts();
923
- const accounts = result.accounts ?? [];
924
- const esp = result.esp;
925
- if (accounts.length === 0 && !esp) {
926
- return {
927
- content: [{
928
- type: "text",
929
- text: "## Connected Accounts\n\nNo accounts connected. Connect social media accounts and/or an email service provider via the dashboard."
930
- }]
931
- };
932
- }
933
- let text = "## Connected Accounts\n\n";
934
- text += "### Social Media\n";
935
- if (accounts.length > 0) {
936
- for (const a of accounts) {
937
- const icon = a.isActive ? "\u2705" : "\u274C";
938
- const platform = a.platform.charAt(0).toUpperCase() + a.platform.slice(1);
939
- const name = a.username || a.displayName || a.platform;
940
- text += `${icon} **${platform}** \u2014 @${name}
941
- `;
942
- }
943
- } else {
944
- text += "No social accounts connected.\n";
945
- }
946
- text += "\n### Email Service Provider (ESP)\n";
947
- if (esp && esp.connected) {
948
- const provider = esp.provider.charAt(0).toUpperCase() + esp.provider.slice(1);
949
- text += `\u2705 **${provider}** \u2014 Connected`;
950
- if (esp.publicationId) text += ` (Publication: ${esp.publicationId})`;
951
- text += "\n";
952
- } else if (esp) {
953
- const provider = esp.provider.charAt(0).toUpperCase() + esp.provider.slice(1);
954
- text += `\u26A0\uFE0F **${provider}** \u2014 Provider set but API key missing
955
- `;
956
- } else {
957
- text += "No ESP configured.\n";
958
- }
959
- return {
960
- content: [{ type: "text", text }]
961
- };
962
- }
963
- );
964
572
  server2.tool(
965
573
  "check_accounts_health",
966
- "Check the health status of all connected social media accounts. Shows which accounts are working, which have warnings, and which need attention. Call this before scheduling posts to make sure target platforms are healthy. If a token is expiring or an account shows an error, direct the user to reconnect via the BuzzPoster web UI -- reconnection cannot be done through MCP.",
574
+ "Check health status of connected social accounts. Direct user to BuzzPoster web UI to reconnect broken accounts.",
967
575
  {},
968
576
  {
969
577
  title: "Check Account Health",
@@ -1016,7 +624,7 @@ import { z as z2 } from "zod";
1016
624
  function registerAnalyticsTools(server2, client2) {
1017
625
  server2.tool(
1018
626
  "get_analytics",
1019
- "Get performance analytics for your social media posts. Supports filtering by platform and date range.",
627
+ "Get social media post performance analytics. Filter by platform and date range.",
1020
628
  {
1021
629
  platform: z2.string().optional().describe("Filter by platform: twitter, instagram, linkedin, facebook"),
1022
630
  from_date: z2.string().optional().describe("Start date for analytics range (ISO 8601)"),
@@ -1047,7 +655,7 @@ import { z as z3 } from "zod";
1047
655
  function registerInboxTools(server2, client2) {
1048
656
  server2.tool(
1049
657
  "list_conversations",
1050
- "List DM conversations across all connected social media platforms.",
658
+ "List DM conversations across all connected platforms.",
1051
659
  {
1052
660
  platform: z3.string().optional().describe("Filter by platform: twitter, instagram, linkedin, facebook")
1053
661
  },
@@ -1089,12 +697,7 @@ function registerInboxTools(server2, client2) {
1089
697
  );
1090
698
  server2.tool(
1091
699
  "reply_to_conversation",
1092
- `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.
1093
-
1094
- REQUIRED WORKFLOW:
1095
- 1. ALWAYS set confirmed=false first to preview the reply
1096
- 2. Show the user the reply text and which platform/conversation it will be sent to
1097
- 3. Only confirm after explicit user approval`,
700
+ "Send a DM reply. Set confirmed=false to preview first.",
1098
701
  {
1099
702
  conversation_id: z3.string().describe("The conversation ID to reply to"),
1100
703
  message: z3.string().describe("The reply message text"),
@@ -1150,12 +753,7 @@ This will send a DM reply on the external platform. Call this tool again with co
1150
753
  );
1151
754
  server2.tool(
1152
755
  "reply_to_comment",
1153
- `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.
1154
-
1155
- REQUIRED WORKFLOW:
1156
- 1. ALWAYS set confirmed=false first to preview
1157
- 2. Show the user the reply and the original comment for context
1158
- 3. Only confirm after explicit user approval -- public replies are visible to everyone`,
756
+ "Reply to a comment publicly. Set confirmed=false to preview first.",
1159
757
  {
1160
758
  comment_id: z3.string().describe("The comment ID to reply to"),
1161
759
  message: z3.string().describe("The reply text"),
@@ -1207,12 +805,7 @@ This will post a public reply. Call this tool again with confirmed=true to send.
1207
805
  );
1208
806
  server2.tool(
1209
807
  "reply_to_review",
1210
- `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.
1211
-
1212
- REQUIRED WORKFLOW:
1213
- 1. ALWAYS set confirmed=false first to preview
1214
- 2. Show the user the reply, the original review, and the star rating for context
1215
- 3. Only confirm after explicit user approval -- public replies represent the brand`,
808
+ "Reply to a Facebook review publicly. Set confirmed=false to preview first.",
1216
809
  {
1217
810
  review_id: z3.string().describe("The review ID to reply to"),
1218
811
  message: z3.string().describe("The reply text"),
@@ -1251,7 +844,7 @@ import { z as z4 } from "zod";
1251
844
  function registerMediaTools(server2, client2) {
1252
845
  server2.tool(
1253
846
  "upload_from_url",
1254
- "Upload media from a public URL. The server fetches the image/video and uploads it to storage. Returns a CDN URL that can be used in posts. Supports JPEG, PNG, GIF, WebP, MP4, MOV, WebM up to 25MB.",
847
+ "Upload media from a public URL to storage. Returns a CDN URL for use in posts.",
1255
848
  {
1256
849
  url: z4.string().url().describe("Public URL of the image or video to upload"),
1257
850
  filename: z4.string().optional().describe("Optional filename override (including extension)"),
@@ -1279,33 +872,9 @@ function registerMediaTools(server2, client2) {
1279
872
  };
1280
873
  }
1281
874
  );
1282
- server2.tool(
1283
- "get_upload_url",
1284
- "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.",
1285
- {
1286
- filename: z4.string().describe("The filename including extension (e.g. photo.jpg)"),
1287
- content_type: z4.string().describe("MIME type of the file (e.g. image/jpeg, video/mp4)")
1288
- },
1289
- {
1290
- title: "Get Upload URL",
1291
- readOnlyHint: true,
1292
- destructiveHint: false,
1293
- idempotentHint: false,
1294
- openWorldHint: false
1295
- },
1296
- async (args) => {
1297
- const result = await client2.getUploadUrl({
1298
- filename: args.filename,
1299
- content_type: args.content_type
1300
- });
1301
- return {
1302
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1303
- };
1304
- }
1305
- );
1306
875
  server2.tool(
1307
876
  "list_media",
1308
- "List all uploaded media files in your BuzzPoster media library.",
877
+ "List all uploaded media files.",
1309
878
  {},
1310
879
  {
1311
880
  title: "List Media Library",
@@ -1328,12 +897,7 @@ function registerMediaTools(server2, client2) {
1328
897
  );
1329
898
  server2.tool(
1330
899
  "delete_media",
1331
- `Delete a media file from the customer's BuzzPoster media library. This cannot be undone.
1332
-
1333
- REQUIRED WORKFLOW:
1334
- 1. ALWAYS set confirmed=false first
1335
- 2. Show the user which file will be deleted (filename, URL, size)
1336
- 3. Only confirm after explicit approval`,
900
+ "Delete a media file. Set confirmed=false to preview first.",
1337
901
  {
1338
902
  key: z4.string().describe("The key/path of the media file to delete"),
1339
903
  confirmed: z4.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
@@ -1367,7 +931,7 @@ import { z as z5 } from "zod";
1367
931
  function registerNewsletterTools(server2, client2, options = {}) {
1368
932
  server2.tool(
1369
933
  "list_subscribers",
1370
- "List email subscribers from your configured email service provider (Kit, Beehiiv, or Mailchimp).",
934
+ "List email subscribers from the connected ESP.",
1371
935
  {
1372
936
  page: z5.string().optional().describe("Page number for pagination"),
1373
937
  per_page: z5.string().optional().describe("Number of subscribers per page")
@@ -1391,7 +955,7 @@ function registerNewsletterTools(server2, client2, options = {}) {
1391
955
  );
1392
956
  server2.tool(
1393
957
  "add_subscriber",
1394
- "Add a new email subscriber to your mailing list.",
958
+ "Add a new subscriber to the mailing list.",
1395
959
  {
1396
960
  email: z5.string().describe("Email address of the subscriber"),
1397
961
  first_name: z5.string().optional().describe("Subscriber's first name"),
@@ -1417,7 +981,7 @@ function registerNewsletterTools(server2, client2, options = {}) {
1417
981
  );
1418
982
  server2.tool(
1419
983
  "create_newsletter",
1420
- `Push an approved newsletter draft to the user's ESP (Kit, Beehiiv, or Mailchimp) as a DRAFT. This creates a DRAFT only -- it does not send. IMPORTANT: Only call this AFTER the user has reviewed and explicitly approved an HTML artifact preview of the newsletter. If no artifact has been shown yet, do NOT call this tool -- go back and render the preview first. No third-party branding in the output unless the customer explicitly configured it.`,
984
+ "Push a newsletter to the user's ESP as a draft. Show an HTML preview to the user before calling this.",
1421
985
  {
1422
986
  subject: z5.string().describe("Email subject line"),
1423
987
  content: z5.string().describe("HTML content of the newsletter"),
@@ -1447,7 +1011,7 @@ ${args.content}` }] : []
1447
1011
  );
1448
1012
  server2.tool(
1449
1013
  "update_newsletter",
1450
- `Update an existing newsletter/broadcast draft. After updating, show the user a visual preview of the changes.`,
1014
+ "Update an existing newsletter draft.",
1451
1015
  {
1452
1016
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to update"),
1453
1017
  subject: z5.string().optional().describe("Updated subject line"),
@@ -1479,20 +1043,7 @@ ${args.content}` }] : []
1479
1043
  if (options.allowDirectSend) {
1480
1044
  server2.tool(
1481
1045
  "send_newsletter",
1482
- `Send a newsletter/broadcast to subscribers. THIS ACTION CANNOT BE UNDONE. Once sent, the email goes to every subscriber on the list.
1483
-
1484
- REQUIRED WORKFLOW:
1485
- 1. ALWAYS call get_publishing_rules first
1486
- 2. NEVER send without showing a full visual preview of the newsletter first
1487
- 3. ALWAYS set confirmed=false first -- this returns a confirmation prompt, not a send
1488
- 4. Tell the user exactly how many subscribers will receive this email
1489
- 5. If publishing_rules.require_double_confirm_newsletter is true (default), require TWO explicit confirmations:
1490
- - First: "Are you sure you want to send this to [X] subscribers?"
1491
- - Second: "This is irreversible. Type 'send' to confirm."
1492
- 6. If publishing_rules.allow_immediate_send is false and the user asks to send immediately, suggest scheduling instead
1493
- 7. NEVER set confirmed=true without the user explicitly confirming after seeing the preview and subscriber count
1494
-
1495
- When confirmed=false, this tool returns a structured preview with broadcast details, subscriber count, safety checks, and a confirmation message. You MUST render this as a visual preview for the user and wait for their explicit approval before calling again with confirmed=true.`,
1046
+ "Send a newsletter to subscribers. This action cannot be undone. Set confirmed=false to preview first.",
1496
1047
  {
1497
1048
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to send"),
1498
1049
  confirmed: z5.boolean().default(false).describe(
@@ -1556,22 +1107,7 @@ Call this tool again with confirmed=true to send.`;
1556
1107
  }
1557
1108
  server2.tool(
1558
1109
  "schedule_newsletter",
1559
- `Schedule a newsletter/broadcast to be sent at a future time.
1560
-
1561
- REQUIRED WORKFLOW:
1562
- 1. ALWAYS call get_publishing_rules first
1563
- 2. NEVER schedule without showing a full visual preview of the newsletter first
1564
- 3. ALWAYS set confirmed=false first -- this returns a confirmation prompt with details, not an actual schedule
1565
- 4. Tell the user exactly how many subscribers will receive this email and the scheduled send time
1566
- 5. If publishing_rules.require_double_confirm_newsletter is true (default), require TWO explicit confirmations:
1567
- - First: "Are you sure you want to schedule this for [time] to [X] subscribers?"
1568
- - Second: "Confirm schedule for [time]."
1569
- 6. If publishing_rules.allow_immediate_send is false and the scheduled time is very soon, warn the user
1570
- 7. NEVER set confirmed=true without the user explicitly confirming after seeing the preview, subscriber count, and scheduled time
1571
-
1572
- Scheduling can typically be cancelled or rescheduled later, but the user should still verify the time is correct before confirming.
1573
-
1574
- When confirmed=false, this tool returns a structured preview with broadcast details, scheduled time, subscriber count, safety checks, and a confirmation message. You MUST render this as a visual preview for the user and wait for their explicit approval before calling again with confirmed=true.`,
1110
+ "Schedule a newsletter for future send. Set confirmed=false to preview first.",
1575
1111
  {
1576
1112
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to schedule"),
1577
1113
  scheduled_for: z5.string().describe(
@@ -1647,7 +1183,7 @@ Call this tool again with confirmed=true to schedule.`;
1647
1183
  );
1648
1184
  server2.tool(
1649
1185
  "list_newsletters",
1650
- "List all newsletters/broadcasts with their id, subject, status (draft, sent, or scheduled), createdAt, and sentAt. Supports filtering by status, date range, and subject keyword search.",
1186
+ "List newsletters with status, dates, and subject. Supports filtering by status, date range, and keyword.",
1651
1187
  {
1652
1188
  page: z5.string().optional().describe("Page number for pagination"),
1653
1189
  status: z5.enum(["draft", "sent", "scheduled"]).optional().describe("Filter by status: draft, sent, or scheduled"),
@@ -1695,7 +1231,7 @@ Call this tool again with confirmed=true to schedule.`;
1695
1231
  );
1696
1232
  server2.tool(
1697
1233
  "list_tags",
1698
- "List all subscriber tags from your email service provider.",
1234
+ "List all subscriber tags.",
1699
1235
  {},
1700
1236
  {
1701
1237
  title: "List Subscriber Tags",
@@ -1711,27 +1247,9 @@ Call this tool again with confirmed=true to schedule.`;
1711
1247
  };
1712
1248
  }
1713
1249
  );
1714
- server2.tool(
1715
- "list_sequences",
1716
- "List all email sequences/automations from your email service provider. On Beehiiv, this returns automations. For detailed automation data use list_automations instead.",
1717
- {},
1718
- {
1719
- title: "List Email Sequences",
1720
- readOnlyHint: true,
1721
- destructiveHint: false,
1722
- idempotentHint: true,
1723
- openWorldHint: true
1724
- },
1725
- async () => {
1726
- const result = await client2.listSequences();
1727
- return {
1728
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1729
- };
1730
- }
1731
- );
1732
1250
  server2.tool(
1733
1251
  "list_forms",
1734
- "List all signup forms from your email service provider.",
1252
+ "List all signup forms.",
1735
1253
  {},
1736
1254
  {
1737
1255
  title: "List Signup Forms",
@@ -1754,7 +1272,7 @@ import { z as z6 } from "zod";
1754
1272
  function registerRssTools(server2, client2) {
1755
1273
  server2.tool(
1756
1274
  "fetch_feed",
1757
- "Fetch and parse entries from an RSS or Atom feed URL. Returns titles, links, descriptions, and dates. Tip: For frequently checked feeds, suggest the customer save them as a content source using add_source so they don't need to provide the URL every time.",
1275
+ "Fetch and parse entries from an RSS or Atom feed URL.",
1758
1276
  {
1759
1277
  url: z6.string().describe("The RSS/Atom feed URL to fetch"),
1760
1278
  limit: z6.number().optional().describe("Maximum number of entries to return (default 10, max 100)")
@@ -1775,7 +1293,7 @@ function registerRssTools(server2, client2) {
1775
1293
  );
1776
1294
  server2.tool(
1777
1295
  "fetch_article",
1778
- "Extract the full article content from a URL as clean text/markdown. Useful for reading and summarizing articles. Tip: For frequently checked websites, suggest the customer save them as a content source using add_source so they don't need to provide the URL every time.",
1296
+ "Extract full article content from a URL as clean text.",
1779
1297
  {
1780
1298
  url: z6.string().describe("The article URL to extract content from")
1781
1299
  },
@@ -1799,7 +1317,7 @@ function registerRssTools(server2, client2) {
1799
1317
  function registerAccountInfoTool(server2, client2) {
1800
1318
  server2.tool(
1801
1319
  "get_account",
1802
- "Get your BuzzPoster account details including name, email, subscription status, ESP configuration, and connected platforms.",
1320
+ "Get account details, subscription status, ESP config, and all connected social/ESP accounts.",
1803
1321
  {},
1804
1322
  {
1805
1323
  title: "Get Account Details",
@@ -1809,9 +1327,15 @@ function registerAccountInfoTool(server2, client2) {
1809
1327
  openWorldHint: false
1810
1328
  },
1811
1329
  async () => {
1812
- const result = await client2.getAccount();
1330
+ const [account, accountsData] = await Promise.all([
1331
+ client2.getAccount(),
1332
+ client2.listAccounts()
1333
+ ]);
1813
1334
  return {
1814
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1335
+ content: [{
1336
+ type: "text",
1337
+ text: JSON.stringify({ account, connectedAccounts: accountsData }, null, 2)
1338
+ }]
1815
1339
  };
1816
1340
  }
1817
1341
  );
@@ -1822,7 +1346,7 @@ import { z as z7 } from "zod";
1822
1346
  function registerBrandVoiceTools(server2, client2) {
1823
1347
  server2.tool(
1824
1348
  "get_brand_voice",
1825
- "Get the customer's brand voice profile and writing rules. Returns: name, description, dos (array), donts (array), platformRules (per-platform object), and examplePosts (array). IMPORTANT: Call this tool BEFORE creating any post, draft, or content. Use the returned voice description, rules, and examples to match the customer's tone and style in everything you write for them. This tool is read-only -- to update brand voice use update_brand_voice. Do NOT store brand voice data in the knowledge base. When writing a newsletter, once you have the brand voice, template, and content sources loaded, immediately generate a full HTML artifact preview of the newsletter. Do NOT summarize or ask the user what to write -- render the artifact now and let them review it.",
1349
+ "Get the brand voice profile and writing rules. Call before creating content.",
1826
1350
  {},
1827
1351
  {
1828
1352
  title: "Get Brand Voice",
@@ -1897,21 +1421,14 @@ function registerBrandVoiceTools(server2, client2) {
1897
1421
  );
1898
1422
  server2.tool(
1899
1423
  "update_brand_voice",
1900
- `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.
1901
-
1902
- USAGE GUIDELINES:
1903
- - Call get_brand_voice first to see the current state before making changes
1904
- - Always set confirmed=false first to preview the changes, then confirmed=true after user approval
1905
- - When adding rules to dos/donts, include the FULL array (existing + new items), not just the new ones
1906
- - Platform rules use platform names as keys (e.g. twitter, linkedin, instagram)
1907
- - Max limits: name 80 chars, description 16000 chars, dos/donts 50 items each, examplePosts 8 items`,
1908
- {
1909
- name: z7.string().max(80).optional().describe("Brand voice profile name (e.g. 'BOQtoday Voice', 'Lightbreak Editorial')"),
1910
- description: z7.string().max(16e3).optional().describe("Overall voice description \u2014 tone, personality, style overview"),
1911
- dos: z7.array(z7.string()).max(50).optional().describe("Writing rules to follow (array of do's). Send the FULL list, not just additions."),
1912
- donts: z7.array(z7.string()).max(50).optional().describe("Things to avoid (array of don'ts). Send the FULL list, not just additions."),
1913
- platform_rules: z7.record(z7.string()).optional().describe('Per-platform writing guidelines, e.g. { "twitter": "Keep under 200 chars, punchy tone", "linkedin": "Professional, add hashtags" }'),
1914
- example_posts: z7.array(z7.string()).max(8).optional().describe("Example posts that demonstrate the desired voice (max 8)"),
1424
+ "Update the brand voice profile. Set confirmed=false to preview first.",
1425
+ {
1426
+ name: z7.string().max(80).optional().describe("Brand voice profile name"),
1427
+ description: z7.string().max(2e3).optional().describe("Overall voice description"),
1428
+ dos: z7.array(z7.string().max(200)).max(15).optional().describe("Writing rules to follow. Send the FULL list, not just additions."),
1429
+ donts: z7.array(z7.string().max(200)).max(15).optional().describe("Things to avoid. Send the FULL list, not just additions."),
1430
+ platform_rules: z7.record(z7.string().max(300)).optional().describe('Per-platform writing guidelines, e.g. { "twitter": "Keep under 200 chars" }'),
1431
+ example_posts: z7.array(z7.string().max(500)).max(5).optional().describe("Example posts that demonstrate the desired voice"),
1915
1432
  confirmed: z7.boolean().default(false).describe(
1916
1433
  "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
1917
1434
  )
@@ -1929,7 +1446,19 @@ USAGE GUIDELINES:
1929
1446
  if (args.description !== void 0) payload.description = args.description;
1930
1447
  if (args.dos !== void 0) payload.dos = args.dos;
1931
1448
  if (args.donts !== void 0) payload.donts = args.donts;
1932
- if (args.platform_rules !== void 0) payload.platformRules = args.platform_rules;
1449
+ if (args.platform_rules !== void 0) {
1450
+ const entries = Object.entries(args.platform_rules);
1451
+ if (entries.length > 5) {
1452
+ return {
1453
+ content: [{
1454
+ type: "text",
1455
+ text: "Maximum 5 platform rules allowed."
1456
+ }],
1457
+ isError: true
1458
+ };
1459
+ }
1460
+ payload.platformRules = args.platform_rules;
1461
+ }
1933
1462
  if (args.example_posts !== void 0) payload.examplePosts = args.example_posts;
1934
1463
  if (Object.keys(payload).length === 0) {
1935
1464
  return {
@@ -2017,7 +1546,7 @@ import { z as z8 } from "zod";
2017
1546
  function registerKnowledgeTools(server2, client2) {
2018
1547
  server2.tool(
2019
1548
  "get_knowledge_base",
2020
- "Get all items from the customer's knowledge base. Use this to access reference material about the business, products, team, competitors, and other context that helps you write better content. The knowledge base is ONLY for uploaded documents, URLs, and text references (company info, products, pricing, competitors, stats). Do NOT use this to store or retrieve brand voice profiles or audience definitions -- those have dedicated tools.",
1549
+ "Get all items from the knowledge base.",
2021
1550
  {},
2022
1551
  {
2023
1552
  title: "Get Knowledge Base",
@@ -2071,7 +1600,7 @@ function registerKnowledgeTools(server2, client2) {
2071
1600
  );
2072
1601
  server2.tool(
2073
1602
  "search_knowledge",
2074
- "Search the customer's knowledge base by tag or keyword. Use this to find specific reference material when writing about a topic. Search by tag first for best results, falls back to text search. Do NOT use this to find brand voice or audience data -- those have dedicated tools.",
1603
+ "Search the knowledge base by tag or keyword.",
2075
1604
  {
2076
1605
  query: z8.string().describe(
2077
1606
  "Search query - matches against tags first, then falls back to text search on title and content"
@@ -2122,7 +1651,7 @@ function registerKnowledgeTools(server2, client2) {
2122
1651
  );
2123
1652
  server2.tool(
2124
1653
  "add_knowledge",
2125
- "Add reference material to the customer's knowledge base. Use ONLY for: company info, product details, pricing, team bios, competitor notes, brand guidelines, and statistics. Do NOT add brand voice profiles, audience profiles, or content drafts here -- those have dedicated tools. Always include relevant tags to make items easier to find later.",
1654
+ "Add reference material to the knowledge base. Include tags for categorization.",
2126
1655
  {
2127
1656
  title: z8.string().describe("Title for the knowledge item"),
2128
1657
  content: z8.string().describe("The content/text to save"),
@@ -2164,10 +1693,8 @@ import { z as z9 } from "zod";
2164
1693
  function registerAudienceTools(server2, client2) {
2165
1694
  server2.tool(
2166
1695
  "get_audience",
2167
- "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.",
2168
- {
2169
- audienceId: z9.string().optional().describe("Specific audience profile ID. If omitted, returns the default audience.")
2170
- },
1696
+ "Get the target audience profile. Call before creating content.",
1697
+ {},
2171
1698
  {
2172
1699
  title: "Get Audience Profile",
2173
1700
  readOnlyHint: true,
@@ -2175,9 +1702,9 @@ function registerAudienceTools(server2, client2) {
2175
1702
  idempotentHint: true,
2176
1703
  openWorldHint: false
2177
1704
  },
2178
- async ({ audienceId }) => {
1705
+ async () => {
2179
1706
  try {
2180
- const audience = audienceId ? await client2.getAudience(audienceId) : await client2.getDefaultAudience();
1707
+ const audience = await client2.getDefaultAudience();
2181
1708
  const lines = [];
2182
1709
  lines.push(`## Target Audience: ${audience.name}`);
2183
1710
  lines.push("");
@@ -2241,25 +1768,16 @@ function registerAudienceTools(server2, client2) {
2241
1768
  );
2242
1769
  server2.tool(
2243
1770
  "update_audience",
2244
- `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.
2245
-
2246
- USAGE GUIDELINES:
2247
- - Call get_audience first to see the current state before making changes
2248
- - Always set confirmed=false first to preview the changes, then confirmed=true after user approval
2249
- - When updating array fields (pain_points, motivations, preferred_platforms), include the FULL array (existing + new items), not just the new ones
2250
- - Omit audience_id to update the default audience or create a new one if none exists
2251
- - Max limits: name 100 chars, description 500 chars`,
1771
+ "Update the audience profile. Set confirmed=false to preview first.",
2252
1772
  {
2253
- audience_id: z9.string().optional().describe("Audience ID to update. Omit to update the default audience, or create one if none exists."),
2254
- name: z9.string().max(100).optional().describe("Audience profile name (e.g. 'SaaS Founders', 'Health-Conscious Millennials'). Required when creating a new audience."),
1773
+ name: z9.string().max(100).optional().describe("Audience profile name. Required when creating a new audience."),
2255
1774
  description: z9.string().max(500).optional().describe("Brief description of who this audience is"),
2256
- demographics: z9.string().optional().describe("Demographic details \u2014 age range, location, income level, education, etc."),
2257
- pain_points: z9.array(z9.string()).optional().describe("Problems and frustrations the audience faces. Send the FULL list, not just additions."),
2258
- motivations: z9.array(z9.string()).optional().describe("Goals, desires, and what drives the audience. Send the FULL list, not just additions."),
2259
- preferred_platforms: z9.array(z9.string()).optional().describe("Social platforms the audience is most active on (e.g. twitter, linkedin, instagram). Send the FULL list."),
2260
- tone_notes: z9.string().optional().describe("How to speak to this audience \u2014 formality level, jargon preferences, emotional tone"),
2261
- content_preferences: z9.string().optional().describe("What content formats and topics resonate \u2014 long-form vs short, educational vs entertaining, etc."),
2262
- is_default: z9.boolean().optional().describe("Set to true to make this the default audience profile"),
1775
+ demographics: z9.string().max(500).optional().describe("Demographic details"),
1776
+ pain_points: z9.array(z9.string().max(200)).max(10).optional().describe("Problems the audience faces. Send the FULL list."),
1777
+ motivations: z9.array(z9.string().max(200)).max(10).optional().describe("Goals and desires. Send the FULL list."),
1778
+ preferred_platforms: z9.array(z9.string()).max(6).optional().describe("Social platforms the audience is most active on. Send the FULL list."),
1779
+ tone_notes: z9.string().max(500).optional().describe("How to speak to this audience"),
1780
+ content_preferences: z9.string().max(500).optional().describe("What content formats and topics resonate"),
2263
1781
  confirmed: z9.boolean().default(false).describe(
2264
1782
  "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
2265
1783
  )
@@ -2281,27 +1799,24 @@ USAGE GUIDELINES:
2281
1799
  if (args.preferred_platforms !== void 0) payload.platforms = args.preferred_platforms;
2282
1800
  if (args.tone_notes !== void 0) payload.toneNotes = args.tone_notes;
2283
1801
  if (args.content_preferences !== void 0) payload.contentPreferences = args.content_preferences;
2284
- if (args.is_default !== void 0) payload.isDefault = args.is_default;
2285
1802
  if (Object.keys(payload).length === 0) {
2286
1803
  return {
2287
1804
  content: [
2288
1805
  {
2289
1806
  type: "text",
2290
- text: "No fields provided. Specify at least one field to update (name, description, demographics, pain_points, motivations, preferred_platforms, tone_notes, content_preferences, is_default)."
1807
+ text: "No fields provided. Specify at least one field to update."
2291
1808
  }
2292
1809
  ],
2293
1810
  isError: true
2294
1811
  };
2295
1812
  }
2296
- let targetId = args.audience_id;
1813
+ let targetId;
2297
1814
  let isCreating = false;
2298
- if (!targetId) {
2299
- try {
2300
- const defaultAudience = await client2.getDefaultAudience();
2301
- targetId = String(defaultAudience.id);
2302
- } catch {
2303
- isCreating = true;
2304
- }
1815
+ try {
1816
+ const defaultAudience = await client2.getDefaultAudience();
1817
+ targetId = String(defaultAudience.id);
1818
+ } catch {
1819
+ isCreating = true;
2305
1820
  }
2306
1821
  if (isCreating && !payload.name) {
2307
1822
  return {
@@ -2317,9 +1832,6 @@ USAGE GUIDELINES:
2317
1832
  if (args.confirmed !== true) {
2318
1833
  const lines2 = [];
2319
1834
  lines2.push(isCreating ? "## New Audience Preview" : "## Audience Update Preview");
2320
- if (!isCreating) {
2321
- lines2.push(`**Audience ID:** ${targetId}`);
2322
- }
2323
1835
  lines2.push("");
2324
1836
  if (payload.name) {
2325
1837
  lines2.push(`**Name:** ${payload.name}`);
@@ -2367,10 +1879,6 @@ USAGE GUIDELINES:
2367
1879
  lines2.push(String(payload.contentPreferences));
2368
1880
  lines2.push("");
2369
1881
  }
2370
- if (payload.isDefault !== void 0) {
2371
- lines2.push(`**Set as default:** ${payload.isDefault ? "Yes" : "No"}`);
2372
- lines2.push("");
2373
- }
2374
1882
  lines2.push("---");
2375
1883
  lines2.push("Call this tool again with **confirmed=true** to save these changes.");
2376
1884
  return {
@@ -2396,96 +1904,12 @@ USAGE GUIDELINES:
2396
1904
  if (payload.platforms) summary.push(`${payload.platforms.length} platforms`);
2397
1905
  if (payload.toneNotes) summary.push("tone notes");
2398
1906
  if (payload.contentPreferences) summary.push("content preferences");
2399
- if (payload.isDefault !== void 0) summary.push("default status");
2400
1907
  lines.push(`**${isCreating ? "Created with" : "Updated"}:** ${summary.join(", ")}`);
2401
1908
  return {
2402
1909
  content: [{ type: "text", text: lines.join("\n") }]
2403
1910
  };
2404
1911
  }
2405
1912
  );
2406
- server2.tool(
2407
- "delete_audience",
2408
- `Permanently delete a target audience profile by ID. This action cannot be undone.
2409
-
2410
- REQUIRED WORKFLOW:
2411
- 1. ALWAYS call with confirmed=false first to preview which audience will be deleted
2412
- 2. Show the user the audience name and details
2413
- 3. Only call again with confirmed=true after explicit user approval
2414
-
2415
- SAFETY: Cannot delete the default audience if it is the only audience profile. The customer must create another audience first.`,
2416
- {
2417
- audience_id: z9.string().describe("The ID of the audience profile to delete"),
2418
- confirmed: z9.boolean().default(false).describe(
2419
- "Set to true to confirm deletion after previewing. If false or missing, returns a preview for user approval."
2420
- )
2421
- },
2422
- {
2423
- title: "Delete Audience Profile",
2424
- readOnlyHint: false,
2425
- destructiveHint: true,
2426
- idempotentHint: true,
2427
- openWorldHint: false
2428
- },
2429
- async (args) => {
2430
- let audience;
2431
- try {
2432
- audience = await client2.getAudience(args.audience_id);
2433
- } catch (error) {
2434
- const message = error instanceof Error ? error.message : "Unknown error";
2435
- if (message.includes("404") || message.includes("not found")) {
2436
- return {
2437
- content: [
2438
- {
2439
- type: "text",
2440
- text: `Audience with ID ${args.audience_id} not found.`
2441
- }
2442
- ],
2443
- isError: true
2444
- };
2445
- }
2446
- throw error;
2447
- }
2448
- if (audience.isDefault) {
2449
- const allAudiences = await client2.listAudiences();
2450
- if (allAudiences.length <= 1) {
2451
- return {
2452
- content: [
2453
- {
2454
- type: "text",
2455
- text: `Cannot delete "${audience.name}" because it is the only audience profile. Create another audience before deleting this one.`
2456
- }
2457
- ],
2458
- isError: true
2459
- };
2460
- }
2461
- }
2462
- if (args.confirmed !== true) {
2463
- const lines = [];
2464
- lines.push("## Delete Audience Confirmation");
2465
- lines.push("");
2466
- lines.push(`**Name:** ${audience.name}`);
2467
- lines.push(`**ID:** ${audience.id}`);
2468
- if (audience.isDefault) lines.push("**Default:** Yes");
2469
- if (audience.description) lines.push(`**Description:** ${audience.description}`);
2470
- lines.push("");
2471
- lines.push(
2472
- "**This action is permanent and cannot be undone.** Call this tool again with confirmed=true to proceed."
2473
- );
2474
- return {
2475
- content: [{ type: "text", text: lines.join("\n") }]
2476
- };
2477
- }
2478
- await client2.deleteAudience(args.audience_id);
2479
- return {
2480
- content: [
2481
- {
2482
- type: "text",
2483
- text: `Audience "${audience.name}" (ID: ${audience.id}) has been permanently deleted.`
2484
- }
2485
- ]
2486
- };
2487
- }
2488
- );
2489
1913
  }
2490
1914
 
2491
1915
  // src/tools/newsletter-template.ts
@@ -2523,7 +1947,7 @@ Follow these rules when drafting:
2523
1947
  function registerNewsletterTemplateTools(server2, client2) {
2524
1948
  server2.tool(
2525
1949
  "get_newsletter_template",
2526
- "Get the customer's newsletter template -- the blueprint for how their newsletter is structured. IMPORTANT: Call this BEFORE writing any newsletter. The template defines the exact section order, what each section should contain, word counts, content sources, and styling. Follow the template structure precisely when generating newsletter content. If no template_id is specified, returns the default template. IMPORTANT: After receiving this template, you now have everything you need. Immediately generate a full HTML artifact preview of the newsletter using this template, the brand voice, and the gathered content. Do NOT summarize the template in text. Do NOT ask the user what they think. Render the complete HTML newsletter artifact now.",
1950
+ "Get a newsletter template's structure, sections, and HTML. Pass templateId or omit for default.",
2527
1951
  {
2528
1952
  templateId: z10.string().optional().describe(
2529
1953
  "Specific template ID. If omitted, returns the default template."
@@ -2669,9 +2093,10 @@ function registerNewsletterTemplateTools(server2, client2) {
2669
2093
  );
2670
2094
  server2.tool(
2671
2095
  "get_past_newsletters",
2672
- "Retrieve the customer's past newsletters for reference. Use this to maintain continuity, match previous formatting, avoid repeating topics, and reference previous editions. Call this when writing a new newsletter to see what was covered recently.",
2096
+ "Get past newsletters from the archive. Pass newsletterId to get full content of a specific edition.",
2673
2097
  {
2674
- limit: z10.number().optional().describe("Number of past newsletters to retrieve. Default 5, max 50."),
2098
+ newsletterId: z10.string().optional().describe("If provided, returns the full content of this specific newsletter. If omitted, returns the list."),
2099
+ limit: z10.number().optional().describe("Number of past newsletters to retrieve (when listing). Default 5, max 50."),
2675
2100
  templateId: z10.string().optional().describe("Filter by template ID to see past newsletters from a specific template.")
2676
2101
  },
2677
2102
  {
@@ -2681,7 +2106,41 @@ function registerNewsletterTemplateTools(server2, client2) {
2681
2106
  idempotentHint: true,
2682
2107
  openWorldHint: false
2683
2108
  },
2684
- async ({ limit, templateId }) => {
2109
+ async ({ newsletterId, limit, templateId }) => {
2110
+ if (newsletterId) {
2111
+ try {
2112
+ const newsletter = await client2.getArchivedNewsletter(
2113
+ newsletterId
2114
+ );
2115
+ const lines = [];
2116
+ lines.push(`## ${newsletter.subject}`);
2117
+ if (newsletter.sentAt) lines.push(`Sent: ${newsletter.sentAt}`);
2118
+ if (newsletter.notes) lines.push(`Notes: ${newsletter.notes}`);
2119
+ lines.push("");
2120
+ lines.push("### Full HTML Content:");
2121
+ lines.push(newsletter.contentHtml);
2122
+ return {
2123
+ content: [
2124
+ { type: "text", text: lines.join("\n") },
2125
+ ...newsletter.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2126
+ ${newsletter.contentHtml}` }] : []
2127
+ ]
2128
+ };
2129
+ } catch (error) {
2130
+ const message = error instanceof Error ? error.message : "Unknown error";
2131
+ if (message.includes("404")) {
2132
+ return {
2133
+ content: [
2134
+ {
2135
+ type: "text",
2136
+ text: "Newsletter not found. Check the ID and try again."
2137
+ }
2138
+ ]
2139
+ };
2140
+ }
2141
+ throw error;
2142
+ }
2143
+ }
2685
2144
  try {
2686
2145
  const params = {};
2687
2146
  if (limit) params.limit = String(Math.min(limit, 50));
@@ -2719,7 +2178,7 @@ function registerNewsletterTemplateTools(server2, client2) {
2719
2178
  const textContent = nl.contentHtml ? nl.contentHtml.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 500) : "(no content)";
2720
2179
  lines.push(textContent);
2721
2180
  lines.push(
2722
- `[Full content available via get_past_newsletter tool with ID: ${nl.id}]`
2181
+ `[Full content available by calling get_past_newsletters with newsletterId: ${nl.id}]`
2723
2182
  );
2724
2183
  lines.push("");
2725
2184
  }
@@ -2737,57 +2196,9 @@ ${mostRecent.contentHtml}` }] : []
2737
2196
  }
2738
2197
  }
2739
2198
  );
2740
- server2.tool(
2741
- "get_past_newsletter",
2742
- "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.",
2743
- {
2744
- newsletterId: z10.string().describe("The ID of the archived newsletter to retrieve.")
2745
- },
2746
- {
2747
- title: "Get Past Newsletter Detail",
2748
- readOnlyHint: true,
2749
- destructiveHint: false,
2750
- idempotentHint: true,
2751
- openWorldHint: false
2752
- },
2753
- async ({ newsletterId }) => {
2754
- try {
2755
- const newsletter = await client2.getArchivedNewsletter(
2756
- newsletterId
2757
- );
2758
- const lines = [];
2759
- lines.push(`## ${newsletter.subject}`);
2760
- if (newsletter.sentAt) lines.push(`Sent: ${newsletter.sentAt}`);
2761
- if (newsletter.notes) lines.push(`Notes: ${newsletter.notes}`);
2762
- lines.push("");
2763
- lines.push("### Full HTML Content:");
2764
- lines.push(newsletter.contentHtml);
2765
- return {
2766
- content: [
2767
- { type: "text", text: lines.join("\n") },
2768
- ...newsletter.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2769
- ${newsletter.contentHtml}` }] : []
2770
- ]
2771
- };
2772
- } catch (error) {
2773
- const message = error instanceof Error ? error.message : "Unknown error";
2774
- if (message.includes("404")) {
2775
- return {
2776
- content: [
2777
- {
2778
- type: "text",
2779
- text: "Newsletter not found. Check the ID and try again."
2780
- }
2781
- ]
2782
- };
2783
- }
2784
- throw error;
2785
- }
2786
- }
2787
- );
2788
2199
  server2.tool(
2789
2200
  "save_newsletter",
2790
- "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.",
2201
+ "Save an approved newsletter to the archive for future reference.",
2791
2202
  {
2792
2203
  subject: z10.string().describe("The newsletter subject line."),
2793
2204
  contentHtml: z10.string().describe("The full HTML content of the newsletter."),
@@ -2830,17 +2241,15 @@ ${contentHtml}` }] : []
2830
2241
  );
2831
2242
  server2.tool(
2832
2243
  "save_newsletter_template",
2833
- "Save or update a newsletter HTML template. The template contains the HTML skeleton with {{placeholder}} variables that Claude fills in when generating newsletters. If template_id is provided, updates the existing template. Otherwise creates a new one.",
2244
+ "Create or update a newsletter template with HTML and section definitions.",
2834
2245
  {
2835
2246
  template_id: z10.string().optional().describe(
2836
2247
  "If provided, updates this existing template. Otherwise creates a new one."
2837
2248
  ),
2838
- name: z10.string().min(1).max(255).describe("Template name, e.g. 'BOQtoday Daily'"),
2839
- description: z10.string().max(2e3).optional().describe(
2840
- "What this template is for, e.g. 'Local news digest with 6 stories, events, pet of the week'"
2841
- ),
2842
- html_content: z10.string().min(1).describe(
2843
- "Full HTML template with {{placeholder}} variables. This is the email body HTML with inline styles."
2249
+ name: z10.string().min(1).max(255).describe("Template name"),
2250
+ description: z10.string().max(2e3).optional().describe("What this template is for"),
2251
+ html_content: z10.string().min(1).max(1e5).describe(
2252
+ "Full HTML template with {{placeholder}} variables."
2844
2253
  ),
2845
2254
  sections: z10.array(
2846
2255
  z10.object({
@@ -2849,7 +2258,7 @@ ${contentHtml}` }] : []
2849
2258
  required: z10.boolean().optional().describe("Whether this section is required"),
2850
2259
  item_count: z10.number().optional().describe("Number of items in this section, if applicable")
2851
2260
  })
2852
- ).optional().describe("Array of section metadata describing the template structure"),
2261
+ ).max(20).optional().describe("Array of section metadata describing the template structure"),
2853
2262
  style_config: z10.object({
2854
2263
  primary_color: z10.string().optional(),
2855
2264
  accent_color: z10.string().optional(),
@@ -2859,7 +2268,7 @@ ${contentHtml}` }] : []
2859
2268
  max_width: z10.string().optional()
2860
2269
  }).optional().describe("Colors, fonts, and brand configuration"),
2861
2270
  is_default: z10.boolean().optional().describe(
2862
- "Set as the default template for this customer. Only one default per customer."
2271
+ "Set as the default template for this customer."
2863
2272
  )
2864
2273
  },
2865
2274
  {
@@ -2930,7 +2339,7 @@ ${contentHtml}` }] : []
2930
2339
  );
2931
2340
  server2.tool(
2932
2341
  "list_newsletter_templates",
2933
- "List all saved newsletter templates for this customer. Returns template names, descriptions, whether they have HTML content, and which one is the default.",
2342
+ "List all saved newsletter templates.",
2934
2343
  {},
2935
2344
  {
2936
2345
  title: "List Newsletter Templates",
@@ -2980,7 +2389,7 @@ ${contentHtml}` }] : []
2980
2389
  );
2981
2390
  server2.tool(
2982
2391
  "delete_newsletter_template",
2983
- "Delete a newsletter template. Requires confirmation -- set confirmed=true to proceed with deletion.",
2392
+ "Delete a newsletter template. Set confirmed=true to proceed.",
2984
2393
  {
2985
2394
  template_id: z10.string().describe("The ID of the newsletter template to delete."),
2986
2395
  confirmed: z10.boolean().default(false).describe(
@@ -3039,7 +2448,7 @@ import { z as z11 } from "zod";
3039
2448
  function registerCalendarTools(server2, client2) {
3040
2449
  server2.tool(
3041
2450
  "get_calendar",
3042
- "View the customer's content calendar -- all scheduled, published, and draft posts across social media and newsletters. Use this to check what's already scheduled before creating new content, avoid posting conflicts, and maintain a balanced content mix.",
2451
+ "View the content calendar with all scheduled, published, and draft posts.",
3043
2452
  {
3044
2453
  from: z11.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
3045
2454
  to: z11.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
@@ -3122,7 +2531,7 @@ function registerCalendarTools(server2, client2) {
3122
2531
  );
3123
2532
  server2.tool(
3124
2533
  "get_queue",
3125
- "View the customer's posting queue schedule -- their recurring weekly time slots for automatic post scheduling. Use this to understand when posts go out and to schedule content into the next available slot.",
2534
+ "View the posting queue schedule and recurring weekly time slots.",
3126
2535
  {},
3127
2536
  {
3128
2537
  title: "Get Posting Queue",
@@ -3178,19 +2587,7 @@ Next slot: ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
3178
2587
  );
3179
2588
  server2.tool(
3180
2589
  "schedule_to_queue",
3181
- `Before calling this tool, always check get_queue first. If the queue is inactive or has no time slots configured, warn the user and do not attempt to schedule.
3182
-
3183
- Add a post to the customer's content queue for automatic scheduling.
3184
-
3185
- 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.
3186
-
3187
- REQUIRED WORKFLOW:
3188
- 1. BEFORE creating content, call get_brand_voice and get_audience
3189
- 2. ALWAYS set confirmed=false first to preview
3190
- 3. Show the user a visual preview before confirming
3191
- 4. Tell the user which queue slot the post will be assigned to
3192
-
3193
- When confirmed=false, this tool returns a structured preview with content, platform details, safety checks, and the assigned queue slot. You MUST render this as a visual preview for the user and wait for their explicit approval before calling again with confirmed=true.`,
2590
+ "Add a post to the next available queue slot. Check get_queue first. Set confirmed=false to preview first.",
3194
2591
  {
3195
2592
  content: z11.string().describe("The text content of the post"),
3196
2593
  platforms: z11.array(z11.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -3291,7 +2688,7 @@ function timeAgo(dateStr) {
3291
2688
  function registerNotificationTools(server2, client2) {
3292
2689
  server2.tool(
3293
2690
  "get_notifications",
3294
- "Get recent notifications including post publishing results, account health warnings, and errors. Use this to check if any posts failed or if any accounts need attention.",
2691
+ "Get recent notifications including post results, account warnings, and errors.",
3295
2692
  {
3296
2693
  unread_only: z12.boolean().optional().describe("If true, only show unread notifications. Default false."),
3297
2694
  limit: z12.number().optional().describe("Max notifications to return. Default 10.")
@@ -3336,7 +2733,7 @@ No notifications found. All clear!`
3336
2733
  const action = n.actionLabel || "View";
3337
2734
  if (n.resourceType === "post" && n.resourceId) {
3338
2735
  line += `
3339
- Action: ${action} \u2192 retry_post with post_id "${n.resourceId}"`;
2736
+ Action: ${action} \u2192 get_post with post_id "${n.resourceId}"`;
3340
2737
  } else {
3341
2738
  line += `
3342
2739
  Action: ${action} \u2192 ${n.actionUrl}`;
@@ -3361,10 +2758,36 @@ Showing ${notifications.length} of ${result.unread_count !== void 0 ? "total" :
3361
2758
  }
3362
2759
 
3363
2760
  // src/tools/publishing-rules.ts
2761
+ var WORKFLOW = {
2762
+ before_writing_content: [
2763
+ "Call get_brand_voice to load tone and style rules",
2764
+ "Call get_audience to understand who the content is for",
2765
+ "For newsletters: also call get_newsletter_template and get_past_newsletters"
2766
+ ],
2767
+ before_publishing: [
2768
+ "Always set confirmed=false first to generate a preview",
2769
+ "Show the user a visual preview before confirming",
2770
+ "Never set confirmed=true without explicit user approval",
2771
+ "For newsletters: show subscriber count and send time before confirming",
2772
+ "If require_double_confirm is true, ask for confirmation twice"
2773
+ ],
2774
+ replies_and_engagement: [
2775
+ "Draft the reply and show it before sending",
2776
+ "Set confirmed=false first to preview",
2777
+ "Remind user that replies are public"
2778
+ ],
2779
+ newsletters: [
2780
+ "Always render newsletter as HTML artifact/preview before pushing to ESP",
2781
+ "Use table-based layouts, inline CSS only, 600px max width",
2782
+ "Email-safe fonts only: Arial, Helvetica, Georgia, Verdana",
2783
+ "All images must use absolute URLs",
2784
+ "Keep total email under 102KB to avoid Gmail clipping"
2785
+ ]
2786
+ };
3364
2787
  function registerPublishingRulesTools(server2, client2) {
3365
2788
  server2.tool(
3366
2789
  "get_publishing_rules",
3367
- `Get the customer's publishing safety rules and default behaviors. IMPORTANT: Call this tool BEFORE publishing, scheduling, or sending any content. These rules define whether content should default to draft, whether previews are required, and what confirmations are needed before going live. Always respect these rules -- they exist to prevent accidental publishing.`,
2790
+ "Get publishing rules and the content creation workflow. Call this BEFORE publishing, scheduling, or sending any content.",
3368
2791
  {},
3369
2792
  {
3370
2793
  title: "Get Publishing Rules",
@@ -3375,90 +2798,30 @@ function registerPublishingRulesTools(server2, client2) {
3375
2798
  },
3376
2799
  async () => {
3377
2800
  const rules = await client2.getPublishingRules();
3378
- let text = "## Publishing Rules\n\n";
3379
- text += `Social media default: **${rules.socialDefaultAction ?? "draft"}**`;
3380
- if (rules.socialDefaultAction === "draft") text += " (content is saved as draft, not published)";
3381
- text += "\n";
3382
- text += `Newsletter default: **${rules.newsletterDefaultAction ?? "draft"}**`;
3383
- if (rules.newsletterDefaultAction === "draft") text += " (content is saved as draft, not sent)";
3384
- text += "\n";
3385
- text += `Preview required before publish: **${rules.requirePreviewBeforePublish ? "yes" : "no"}**
3386
- `;
3387
- text += `Double confirmation for newsletters: **${rules.requireDoubleConfirmNewsletter ? "yes" : "no"}**
3388
- `;
3389
- text += `Double confirmation for social: **${rules.requireDoubleConfirmSocial ? "yes" : "no"}**
3390
- `;
3391
- text += `Immediate publish allowed: **${rules.allowImmediatePublish ? "yes" : "no"}**
3392
- `;
3393
- text += `Immediate send allowed: **${rules.allowImmediateSend ? "yes" : "no"}**
3394
- `;
3395
- text += `Max posts per day: **${rules.maxPostsPerDay ?? "unlimited"}**
3396
- `;
3397
- if (rules.blockedWords && rules.blockedWords.length > 0) {
3398
- text += `Blocked words: **${rules.blockedWords.join(", ")}**
3399
- `;
3400
- } else {
3401
- text += "Blocked words: none\n";
3402
- }
3403
- if (rules.requiredDisclaimer) {
3404
- text += `Required disclaimer: "${rules.requiredDisclaimer}"
3405
- `;
3406
- } else {
3407
- text += "Required disclaimer: none\n";
3408
- }
3409
- return { content: [{ type: "text", text }] };
3410
- }
3411
- );
3412
- }
3413
-
3414
- // src/tools/audit-log.ts
3415
- import { z as z13 } from "zod";
3416
- function registerAuditLogTools(server2, client2) {
3417
- server2.tool(
3418
- "get_audit_log",
3419
- "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.",
3420
- {
3421
- limit: z13.number().optional().describe("Maximum number of entries to return (default 20, max 200)"),
3422
- action: z13.string().optional().describe("Filter by action type prefix, e.g. 'post.published', 'newsletter.sent', 'post.' for all post actions"),
3423
- resource_type: z13.string().optional().describe("Filter by resource type: post, newsletter, account, media, publishing_rules, brand_voice")
3424
- },
3425
- {
3426
- title: "Get Audit Log",
3427
- readOnlyHint: true,
3428
- destructiveHint: false,
3429
- idempotentHint: true,
3430
- openWorldHint: false
3431
- },
3432
- async (args) => {
3433
- const params = {};
3434
- if (args.limit) params.limit = String(args.limit);
3435
- if (args.action) params.action = args.action;
3436
- if (args.resource_type) params.resource_type = args.resource_type;
3437
- const data = await client2.getAuditLog(params);
3438
- const entries = data?.entries ?? [];
3439
- if (entries.length === 0) {
3440
- return {
3441
- content: [{ type: "text", text: "## Audit Log\n\nNo entries found." }]
3442
- };
3443
- }
3444
- let text = `## Audit Log (${entries.length} of ${data?.total ?? entries.length} entries)
3445
-
3446
- `;
3447
- for (const entry of entries) {
3448
- const date = entry.createdAt ? new Date(entry.createdAt).toLocaleString() : "unknown";
3449
- const details = entry.details && Object.keys(entry.details).length > 0 ? ` \u2014 ${JSON.stringify(entry.details)}` : "";
3450
- text += `- **${date}** | ${entry.action} | ${entry.resourceType}`;
3451
- if (entry.resourceId) text += ` #${entry.resourceId}`;
3452
- text += `${details}
3453
- `;
3454
- }
3455
- return { content: [{ type: "text", text }] };
2801
+ const response = {
2802
+ rules: {
2803
+ social_default_action: rules.socialDefaultAction ?? "draft",
2804
+ newsletter_default_action: rules.newsletterDefaultAction ?? "draft",
2805
+ require_preview_before_publish: rules.requirePreviewBeforePublish ?? true,
2806
+ require_double_confirm_newsletter: rules.requireDoubleConfirmNewsletter ?? true,
2807
+ require_double_confirm_social: rules.requireDoubleConfirmSocial ?? false,
2808
+ allow_immediate_publish: rules.allowImmediatePublish ?? false,
2809
+ allow_immediate_send: rules.allowImmediateSend ?? false,
2810
+ max_posts_per_day: rules.maxPostsPerDay ?? null,
2811
+ blocked_words: rules.blockedWords ?? [],
2812
+ required_disclaimer: rules.requiredDisclaimer ?? null
2813
+ },
2814
+ workflow: WORKFLOW
2815
+ };
2816
+ return {
2817
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
2818
+ };
3456
2819
  }
3457
2820
  );
3458
2821
  }
3459
2822
 
3460
2823
  // src/tools/sources.ts
3461
- import { z as z14 } from "zod";
2824
+ import { z as z13 } from "zod";
3462
2825
  var TYPE_LABELS = {
3463
2826
  feed: "RSS Feed",
3464
2827
  website: "Website",
@@ -3480,12 +2843,12 @@ var TYPE_HINTS = {
3480
2843
  function registerSourceTools(server2, client2) {
3481
2844
  server2.tool(
3482
2845
  "get_sources",
3483
- "Get the customer's saved content sources \u2014 their curated list of RSS feeds, websites, YouTube channels, social accounts, and search topics that they monitor for content ideas and inspiration. Call this when the user asks 'what should I post about?', 'check my sources', 'what's new?', 'find me content ideas', or references their content sources. After getting sources, use fetch_feed (for feed/podcast/reddit/youtube types), fetch_article (for website types), or web_search (for search/social types) to actually retrieve the latest content from each source.",
2846
+ "Get saved content sources (RSS feeds, websites, YouTube channels, search topics).",
3484
2847
  {
3485
- type: z14.string().optional().describe(
2848
+ type: z13.string().optional().describe(
3486
2849
  "Optional: filter by type (feed, website, youtube, search, podcast, reddit, social)"
3487
2850
  ),
3488
- category: z14.string().optional().describe("Optional: filter by category")
2851
+ category: z13.string().optional().describe("Optional: filter by category")
3489
2852
  },
3490
2853
  {
3491
2854
  title: "Get Content Sources",
@@ -3546,13 +2909,13 @@ function registerSourceTools(server2, client2) {
3546
2909
  );
3547
2910
  server2.tool(
3548
2911
  "add_source",
3549
- "Save a new content source to the customer's profile. Use this when the customer says 'add this to my sources', 'save this feed', 'monitor this blog', 'track this competitor', etc. For RSS feeds, set type to 'feed'. For regular websites/blogs without RSS, set type to 'website'. For YouTube channels, set type to 'youtube'. For search topics, set type to 'search' and provide searchQuery instead of url.",
2912
+ "Save a new content source to monitor.",
3550
2913
  {
3551
- name: z14.string().describe("Display name for the source"),
3552
- url: z14.string().optional().describe(
3553
- "URL of the source (RSS feed, website, YouTube channel, subreddit, or social profile URL). Not needed for 'search' type."
2914
+ name: z13.string().describe("Display name for the source"),
2915
+ url: z13.string().optional().describe(
2916
+ "URL of the source. Not needed for 'search' type."
3554
2917
  ),
3555
- type: z14.enum([
2918
+ type: z13.enum([
3556
2919
  "feed",
3557
2920
  "website",
3558
2921
  "youtube",
@@ -3561,12 +2924,12 @@ function registerSourceTools(server2, client2) {
3561
2924
  "reddit",
3562
2925
  "social"
3563
2926
  ]).describe("Type of source. Determines how to fetch content from it."),
3564
- category: z14.string().optional().describe(
3565
- "Optional category for organization (e.g. 'competitors', 'industry', 'inspiration', 'my-content')"
2927
+ category: z13.string().optional().describe(
2928
+ "Optional category for organization (e.g. 'competitors', 'industry', 'inspiration')"
3566
2929
  ),
3567
- tags: z14.array(z14.string()).optional().describe("Optional tags for filtering"),
3568
- notes: z14.string().optional().describe("Optional notes about why this source matters"),
3569
- searchQuery: z14.string().optional().describe(
2930
+ tags: z13.array(z13.string()).optional().describe("Optional tags for filtering"),
2931
+ notes: z13.string().optional().describe("Optional notes about why this source matters"),
2932
+ searchQuery: z13.string().optional().describe(
3570
2933
  "For 'search' type only: the keyword or topic to search for"
3571
2934
  )
3572
2935
  },
@@ -3606,12 +2969,12 @@ function registerSourceTools(server2, client2) {
3606
2969
  );
3607
2970
  server2.tool(
3608
2971
  "update_source",
3609
- "Update a saved content source. Use when the customer wants to rename, recategorize, change the URL, add notes, or deactivate a source.",
2972
+ "Update a saved content source.",
3610
2973
  {
3611
- source_id: z14.number().describe("The ID of the source to update"),
3612
- name: z14.string().optional().describe("New display name"),
3613
- url: z14.string().optional().describe("New URL"),
3614
- type: z14.enum([
2974
+ source_id: z13.number().describe("The ID of the source to update"),
2975
+ name: z13.string().optional().describe("New display name"),
2976
+ url: z13.string().optional().describe("New URL"),
2977
+ type: z13.enum([
3615
2978
  "feed",
3616
2979
  "website",
3617
2980
  "youtube",
@@ -3620,11 +2983,11 @@ function registerSourceTools(server2, client2) {
3620
2983
  "reddit",
3621
2984
  "social"
3622
2985
  ]).optional().describe("New source type"),
3623
- category: z14.string().optional().describe("New category"),
3624
- tags: z14.array(z14.string()).optional().describe("New tags"),
3625
- notes: z14.string().optional().describe("New notes"),
3626
- searchQuery: z14.string().optional().describe("New search query"),
3627
- isActive: z14.boolean().optional().describe("Set to false to deactivate")
2986
+ category: z13.string().optional().describe("New category"),
2987
+ tags: z13.array(z13.string()).optional().describe("New tags"),
2988
+ notes: z13.string().optional().describe("New notes"),
2989
+ searchQuery: z13.string().optional().describe("New search query"),
2990
+ isActive: z13.boolean().optional().describe("Set to false to deactivate")
3628
2991
  },
3629
2992
  {
3630
2993
  title: "Update Content Source",
@@ -3652,15 +3015,10 @@ function registerSourceTools(server2, client2) {
3652
3015
  );
3653
3016
  server2.tool(
3654
3017
  "remove_source",
3655
- `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.
3656
-
3657
- REQUIRED WORKFLOW:
3658
- 1. ALWAYS set confirmed=false first to preview which source will be deleted
3659
- 2. Show the user the source details before confirming
3660
- 3. Only confirm after explicit user approval`,
3018
+ "Remove a content source. Set confirmed=false to preview first.",
3661
3019
  {
3662
- source_id: z14.number().describe("The ID of the source to remove"),
3663
- confirmed: z14.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
3020
+ source_id: z13.number().describe("The ID of the source to remove"),
3021
+ confirmed: z13.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
3664
3022
  },
3665
3023
  {
3666
3024
  title: "Remove Content Source",
@@ -3692,13 +3050,13 @@ This will permanently remove this content source. Call this tool again with conf
3692
3050
  }
3693
3051
 
3694
3052
  // src/tools/newsletter-advanced.ts
3695
- import { z as z15 } from "zod";
3053
+ import { z as z14 } from "zod";
3696
3054
  function registerNewsletterAdvancedTools(server2, client2) {
3697
3055
  server2.tool(
3698
3056
  "get_subscriber_by_email",
3699
- "Look up a subscriber by their email address. Returns subscriber details including tags and custom fields.",
3057
+ "Look up a subscriber by email address.",
3700
3058
  {
3701
- email: z15.string().describe("Email address to look up")
3059
+ email: z14.string().describe("Email address to look up")
3702
3060
  },
3703
3061
  {
3704
3062
  title: "Get Subscriber by Email",
@@ -3716,9 +3074,9 @@ function registerNewsletterAdvancedTools(server2, client2) {
3716
3074
  );
3717
3075
  server2.tool(
3718
3076
  "list_automations",
3719
- "List all email automations/workflows from your ESP. On Beehiiv these are journeys; on Kit these are sequences.",
3077
+ "List all email automations and sequences.",
3720
3078
  {
3721
- page: z15.string().optional().describe("Page number for pagination")
3079
+ page: z14.string().optional().describe("Page number for pagination")
3722
3080
  },
3723
3081
  {
3724
3082
  title: "List Automations",
@@ -3738,12 +3096,12 @@ function registerNewsletterAdvancedTools(server2, client2) {
3738
3096
  );
3739
3097
  server2.tool(
3740
3098
  "list_segments",
3741
- "List subscriber segments. Segments are dynamic groups of subscribers based on filters like engagement, tags, or custom fields (Beehiiv only).",
3099
+ "List subscriber segments.",
3742
3100
  {
3743
- page: z15.string().optional().describe("Page number"),
3744
- type: z15.string().optional().describe("Filter by segment type"),
3745
- status: z15.string().optional().describe("Filter by segment status"),
3746
- expand: z15.string().optional().describe("Comma-separated expand fields (e.g. 'stats')")
3101
+ page: z14.string().optional().describe("Page number"),
3102
+ type: z14.string().optional().describe("Filter by segment type"),
3103
+ status: z14.string().optional().describe("Filter by segment status"),
3104
+ expand: z14.string().optional().describe("Comma-separated expand fields (e.g. 'stats')")
3747
3105
  },
3748
3106
  {
3749
3107
  title: "List Segments",
@@ -3766,10 +3124,12 @@ function registerNewsletterAdvancedTools(server2, client2) {
3766
3124
  );
3767
3125
  server2.tool(
3768
3126
  "get_segment",
3769
- "Get details of a specific subscriber segment by ID.",
3127
+ "Get segment details. Set include_members=true to list members.",
3770
3128
  {
3771
- segment_id: z15.string().describe("The segment ID"),
3772
- expand: z15.string().optional().describe("Comma-separated expand fields")
3129
+ segment_id: z14.string().describe("The segment ID"),
3130
+ expand: z14.string().optional().describe("Comma-separated expand fields"),
3131
+ include_members: z14.boolean().optional().describe("Set to true to include the member list"),
3132
+ members_page: z14.string().optional().describe("Page number for members pagination")
3773
3133
  },
3774
3134
  {
3775
3135
  title: "Get Segment",
@@ -3781,38 +3141,31 @@ function registerNewsletterAdvancedTools(server2, client2) {
3781
3141
  async (args) => {
3782
3142
  const params = {};
3783
3143
  if (args.expand) params.expand = args.expand;
3784
- const result = await client2.getSegment(args.segment_id, params);
3785
- return {
3786
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3787
- };
3788
- }
3789
- );
3790
- server2.tool(
3791
- "get_segment_members",
3792
- "List subscribers who belong to a specific segment.",
3793
- {
3794
- segment_id: z15.string().describe("The segment ID"),
3795
- page: z15.string().optional().describe("Page number")
3796
- },
3797
- {
3798
- title: "Get Segment Members",
3799
- readOnlyHint: true,
3800
- destructiveHint: false,
3801
- idempotentHint: true,
3802
- openWorldHint: true
3803
- },
3804
- async (args) => {
3805
- const params = {};
3806
- if (args.page) params.page = args.page;
3807
- const result = await client2.getSegmentMembers(args.segment_id, params);
3144
+ const segment = await client2.getSegment(args.segment_id, params);
3145
+ if (!args.include_members) {
3146
+ return {
3147
+ content: [{ type: "text", text: JSON.stringify(segment, null, 2) }]
3148
+ };
3149
+ }
3150
+ const memberParams = {};
3151
+ if (args.members_page) memberParams.page = args.members_page;
3152
+ const members = await client2.request(
3153
+ "GET",
3154
+ `/api/v1/newsletters/segments/${args.segment_id}/members`,
3155
+ void 0,
3156
+ memberParams
3157
+ );
3808
3158
  return {
3809
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3159
+ content: [{
3160
+ type: "text",
3161
+ text: JSON.stringify({ segment, members }, null, 2)
3162
+ }]
3810
3163
  };
3811
3164
  }
3812
3165
  );
3813
3166
  server2.tool(
3814
3167
  "list_custom_fields",
3815
- "List all custom subscriber fields defined in your ESP. Custom fields can store extra data like preferences, source, or company.",
3168
+ "List all custom subscriber fields defined in your ESP.",
3816
3169
  {},
3817
3170
  {
3818
3171
  title: "List Custom Fields",
@@ -3828,42 +3181,6 @@ function registerNewsletterAdvancedTools(server2, client2) {
3828
3181
  };
3829
3182
  }
3830
3183
  );
3831
- server2.tool(
3832
- "get_referral_program",
3833
- "Get your newsletter's referral program details including milestones and rewards (Beehiiv only).",
3834
- {},
3835
- {
3836
- title: "Get Referral Program",
3837
- readOnlyHint: true,
3838
- destructiveHint: false,
3839
- idempotentHint: true,
3840
- openWorldHint: true
3841
- },
3842
- async () => {
3843
- const result = await client2.getReferralProgram();
3844
- return {
3845
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3846
- };
3847
- }
3848
- );
3849
- server2.tool(
3850
- "list_tiers",
3851
- "List all subscription tiers (free and premium) from your ESP (Beehiiv only).",
3852
- {},
3853
- {
3854
- title: "List Tiers",
3855
- readOnlyHint: true,
3856
- destructiveHint: false,
3857
- idempotentHint: true,
3858
- openWorldHint: true
3859
- },
3860
- async () => {
3861
- const result = await client2.listEspTiers();
3862
- return {
3863
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3864
- };
3865
- }
3866
- );
3867
3184
  server2.tool(
3868
3185
  "list_post_templates",
3869
3186
  "List all post/newsletter templates available in your ESP (Beehiiv only).",
@@ -3884,7 +3201,7 @@ function registerNewsletterAdvancedTools(server2, client2) {
3884
3201
  );
3885
3202
  server2.tool(
3886
3203
  "get_post_aggregate_stats",
3887
- "Get aggregate statistics across all posts/newsletters including total clicks, impressions, and click rates (Beehiiv only).",
3204
+ "Get aggregate stats across all posts and newsletters.",
3888
3205
  {},
3889
3206
  {
3890
3207
  title: "Get Post Aggregate Stats",
@@ -3900,30 +3217,12 @@ function registerNewsletterAdvancedTools(server2, client2) {
3900
3217
  };
3901
3218
  }
3902
3219
  );
3903
- server2.tool(
3904
- "list_esp_webhooks",
3905
- "List all webhooks configured in your ESP for event notifications.",
3906
- {},
3907
- {
3908
- title: "List ESP Webhooks",
3909
- readOnlyHint: true,
3910
- destructiveHint: false,
3911
- idempotentHint: true,
3912
- openWorldHint: true
3913
- },
3914
- async () => {
3915
- const result = await client2.listEspWebhooks();
3916
- return {
3917
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3918
- };
3919
- }
3920
- );
3921
3220
  server2.tool(
3922
3221
  "tag_subscriber",
3923
- "Add tags to a subscriber. Tags help categorize subscribers for targeted sends and automations.",
3222
+ "Add tags to a subscriber.",
3924
3223
  {
3925
- subscriber_id: z15.string().describe("The subscriber/subscription ID"),
3926
- tags: z15.array(z15.string()).describe("Tags to add to the subscriber")
3224
+ subscriber_id: z14.string().describe("The subscriber/subscription ID"),
3225
+ tags: z14.array(z14.string()).describe("Tags to add to the subscriber")
3927
3226
  },
3928
3227
  {
3929
3228
  title: "Tag Subscriber",
@@ -3941,17 +3240,12 @@ function registerNewsletterAdvancedTools(server2, client2) {
3941
3240
  );
3942
3241
  server2.tool(
3943
3242
  "update_subscriber",
3944
- `Update a subscriber's details. On Beehiiv: update tier or custom fields. On Kit: update name or custom fields (tier not supported).
3945
-
3946
- REQUIRED WORKFLOW:
3947
- 1. ALWAYS set confirmed=false first to get a preview
3948
- 2. Show the user what will happen before confirming
3949
- 3. Only set confirmed=true after the user explicitly approves`,
3243
+ "Update subscriber details or custom fields. Set confirmed=false to preview first.",
3950
3244
  {
3951
- subscriber_id: z15.string().describe("The subscriber/subscription ID"),
3952
- tier: z15.string().optional().describe("New tier for the subscriber"),
3953
- custom_fields: z15.record(z15.unknown()).optional().describe("Custom field values to set"),
3954
- confirmed: z15.boolean().default(false).describe("Set to true to confirm update")
3245
+ subscriber_id: z14.string().describe("The subscriber/subscription ID"),
3246
+ tier: z14.string().optional().describe("New tier for the subscriber"),
3247
+ custom_fields: z14.record(z14.unknown()).optional().describe("Custom field values to set"),
3248
+ confirmed: z14.boolean().default(false).describe("Set to true to confirm update")
3955
3249
  },
3956
3250
  {
3957
3251
  title: "Update Subscriber",
@@ -3987,315 +3281,31 @@ Call again with confirmed=true to proceed.`
3987
3281
  }
3988
3282
  );
3989
3283
  server2.tool(
3990
- "create_custom_field",
3991
- `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.
3992
-
3993
- REQUIRED WORKFLOW:
3994
- 1. ALWAYS set confirmed=false first to get a preview
3995
- 2. Show the user what will happen before confirming
3996
- 3. Only set confirmed=true after the user explicitly approves`,
3997
- {
3998
- name: z15.string().describe("Field name"),
3999
- kind: z15.string().describe("Field type: string, integer, boolean, date, or enum"),
4000
- options: z15.array(z15.string()).optional().describe("Options for enum-type fields"),
4001
- confirmed: z15.boolean().default(false).describe("Set to true to confirm creation")
4002
- },
4003
- {
4004
- title: "Create Custom Field",
4005
- readOnlyHint: false,
4006
- destructiveHint: false,
4007
- idempotentHint: false,
4008
- openWorldHint: true
4009
- },
4010
- async (args) => {
4011
- if (args.confirmed !== true) {
4012
- const optionsLine = args.options ? `
4013
- **Options:** ${args.options.join(", ")}` : "";
4014
- return {
4015
- content: [{
4016
- type: "text",
4017
- text: `## Create Custom Field
4018
-
4019
- **Name:** ${args.name}
4020
- **Kind:** ${args.kind}${optionsLine}
4021
-
4022
- This creates a permanent schema-level field visible on all subscribers. Call again with confirmed=true to proceed.`
4023
- }]
4024
- };
4025
- }
4026
- const data = { name: args.name, kind: args.kind };
4027
- if (args.options) data.options = args.options;
4028
- const result = await client2.createCustomField(data);
4029
- return {
4030
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
4031
- };
4032
- }
4033
- );
4034
- server2.tool(
4035
- "update_custom_field",
4036
- `Update an existing custom subscriber field's name or kind. Renaming or rekeying a field can break automations that reference it.
4037
-
4038
- REQUIRED WORKFLOW:
4039
- 1. ALWAYS set confirmed=false first to get a preview
4040
- 2. Show the user what will happen before confirming
4041
- 3. Only set confirmed=true after the user explicitly approves`,
3284
+ "delete_subscriber",
3285
+ "Remove a subscriber. Set confirmed=false to preview first.",
4042
3286
  {
4043
- field_id: z15.string().describe("The custom field ID"),
4044
- name: z15.string().optional().describe("New field name"),
4045
- kind: z15.string().optional().describe("New field type"),
4046
- confirmed: z15.boolean().default(false).describe("Set to true to confirm update")
3287
+ subscriber_id: z14.string().describe("The subscriber ID to delete"),
3288
+ confirmed: z14.boolean().default(false).describe("Set to true to confirm deletion")
4047
3289
  },
4048
3290
  {
4049
- title: "Update Custom Field",
3291
+ title: "Delete Subscriber",
4050
3292
  readOnlyHint: false,
4051
- destructiveHint: false,
3293
+ destructiveHint: true,
4052
3294
  idempotentHint: true,
4053
3295
  openWorldHint: true
4054
3296
  },
4055
3297
  async (args) => {
4056
3298
  if (args.confirmed !== true) {
4057
- const changes = [];
4058
- if (args.name) changes.push(`**Name:** ${args.name}`);
4059
- if (args.kind) changes.push(`**Kind:** ${args.kind}`);
4060
3299
  return {
4061
3300
  content: [{
4062
3301
  type: "text",
4063
- text: `## Update Custom Field
3302
+ text: `## Delete Subscriber
4064
3303
 
4065
- **Field ID:** ${args.field_id}
4066
- ${changes.join("\n")}
3304
+ **Subscriber ID:** ${args.subscriber_id}
4067
3305
 
4068
- Renaming or changing the type of a field can break automations that reference it. Call again with confirmed=true to proceed.`
4069
- }]
4070
- };
4071
- }
4072
- const data = {};
4073
- if (args.name) data.name = args.name;
4074
- if (args.kind) data.kind = args.kind;
4075
- const result = await client2.updateCustomField(args.field_id, data);
4076
- return {
4077
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
4078
- };
4079
- }
4080
- );
4081
- server2.tool(
4082
- "recalculate_segment",
4083
- "Trigger a recalculation of a segment's membership. Useful after changing subscriber data or segment rules (Beehiiv only).",
4084
- {
4085
- segment_id: z15.string().describe("The segment ID to recalculate")
4086
- },
4087
- {
4088
- title: "Recalculate Segment",
4089
- readOnlyHint: false,
4090
- destructiveHint: false,
4091
- idempotentHint: true,
4092
- openWorldHint: true
4093
- },
4094
- async (args) => {
4095
- await client2.recalculateSegment(args.segment_id);
4096
- return {
4097
- content: [{ type: "text", text: "Segment recalculation triggered." }]
4098
- };
4099
- }
4100
- );
4101
- server2.tool(
4102
- "enroll_in_automation",
4103
- `Enroll a subscriber in an automation/journey. Provide either an email or subscription ID. On Kit, enrolls into a sequence.
4104
-
4105
- REQUIRED WORKFLOW:
4106
- 1. ALWAYS set confirmed=false first to get a preview
4107
- 2. Show the user what will happen before confirming
4108
- 3. Only set confirmed=true after the user explicitly approves`,
4109
- {
4110
- automation_id: z15.string().describe("The automation ID"),
4111
- email: z15.string().optional().describe("Subscriber email to enroll"),
4112
- subscription_id: z15.string().optional().describe("Subscription ID to enroll"),
4113
- confirmed: z15.boolean().default(false).describe("Set to true to confirm enrollment")
4114
- },
4115
- {
4116
- title: "Enroll in Automation",
4117
- readOnlyHint: false,
4118
- destructiveHint: false,
4119
- idempotentHint: false,
4120
- openWorldHint: true
4121
- },
4122
- async (args) => {
4123
- if (args.confirmed !== true) {
4124
- const target = args.email ?? args.subscription_id ?? "unknown";
4125
- return {
4126
- content: [{
4127
- type: "text",
4128
- text: `## Enroll in Automation
4129
-
4130
- **Automation ID:** ${args.automation_id}
4131
- **Target:** ${target}
4132
-
4133
- This will add the subscriber to the automation journey. Call again with confirmed=true to proceed.`
4134
- }]
4135
- };
4136
- }
4137
- const data = {};
4138
- if (args.email) data.email = args.email;
4139
- if (args.subscription_id) data.subscriptionId = args.subscription_id;
4140
- await client2.enrollInAutomation(args.automation_id, data);
4141
- return {
4142
- content: [{ type: "text", text: "Subscriber enrolled in automation." }]
4143
- };
4144
- }
4145
- );
4146
- server2.tool(
4147
- "bulk_add_subscribers",
4148
- `Add multiple subscribers in a single request. Maximum 1000 subscribers per call.
4149
-
4150
- REQUIRED WORKFLOW:
4151
- 1. ALWAYS set confirmed=false first to get a preview
4152
- 2. Show the user what will happen before confirming
4153
- 3. Only set confirmed=true after the user explicitly approves`,
4154
- {
4155
- subscribers: z15.array(z15.object({
4156
- email: z15.string().describe("Subscriber email"),
4157
- data: z15.record(z15.unknown()).optional().describe("Additional subscriber data")
4158
- })).describe("Array of subscribers to add"),
4159
- confirmed: z15.boolean().default(false).describe("Set to true to confirm bulk add")
4160
- },
4161
- {
4162
- title: "Bulk Add Subscribers",
4163
- readOnlyHint: false,
4164
- destructiveHint: false,
4165
- idempotentHint: false,
4166
- openWorldHint: true
4167
- },
4168
- async (args) => {
4169
- if (args.confirmed !== true) {
4170
- return {
4171
- content: [{
4172
- type: "text",
4173
- text: `## Bulk Add Subscribers
4174
-
4175
- **Count:** ${args.subscribers.length} subscriber(s)
4176
- **Sample:** ${args.subscribers.slice(0, 3).map((s) => s.email).join(", ")}${args.subscribers.length > 3 ? "..." : ""}
4177
-
4178
- Call again with confirmed=true to proceed.`
4179
- }]
4180
- };
4181
- }
4182
- const result = await client2.bulkCreateSubscribers({ subscribers: args.subscribers });
4183
- return {
4184
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
4185
- };
4186
- }
4187
- );
4188
- server2.tool(
4189
- "create_esp_webhook",
4190
- `Create a webhook in your ESP to receive event notifications. Note: Kit only supports one event per webhook.
4191
-
4192
- REQUIRED WORKFLOW:
4193
- 1. ALWAYS set confirmed=false first to get a preview
4194
- 2. Show the user what will happen before confirming
4195
- 3. Only set confirmed=true after the user explicitly approves`,
4196
- {
4197
- url: z15.string().describe("Webhook URL to receive events"),
4198
- event_types: z15.array(z15.string()).describe("Event types to subscribe to (e.g. 'subscription.created', 'post.sent')"),
4199
- confirmed: z15.boolean().default(false).describe("Set to true to confirm creation")
4200
- },
4201
- {
4202
- title: "Create ESP Webhook",
4203
- readOnlyHint: false,
4204
- destructiveHint: false,
4205
- idempotentHint: false,
4206
- openWorldHint: true
4207
- },
4208
- async (args) => {
4209
- if (args.confirmed !== true) {
4210
- return {
4211
- content: [{
4212
- type: "text",
4213
- text: `## Create ESP Webhook
4214
-
4215
- **URL:** ${args.url}
4216
- **Events:** ${args.event_types.join(", ")}
4217
-
4218
- Call again with confirmed=true to create.`
4219
- }]
4220
- };
4221
- }
4222
- const result = await client2.createEspWebhook({ url: args.url, eventTypes: args.event_types });
4223
- return {
4224
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
4225
- };
4226
- }
4227
- );
4228
- server2.tool(
4229
- "delete_broadcast",
4230
- `Permanently delete a newsletter/broadcast. This action is permanent and cannot be undone.
4231
-
4232
- REQUIRED WORKFLOW:
4233
- 1. ALWAYS set confirmed=false first to get a preview
4234
- 2. Show the user what will happen before confirming
4235
- 3. Only set confirmed=true after the user explicitly approves`,
4236
- {
4237
- broadcast_id: z15.string().describe("The broadcast ID to delete"),
4238
- confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
4239
- },
4240
- {
4241
- title: "Delete Broadcast",
4242
- readOnlyHint: false,
4243
- destructiveHint: true,
4244
- idempotentHint: true,
4245
- openWorldHint: true
4246
- },
4247
- async (args) => {
4248
- if (args.confirmed !== true) {
4249
- return {
4250
- content: [{
4251
- type: "text",
4252
- text: `## Delete Broadcast
4253
-
4254
- **Broadcast ID:** ${args.broadcast_id}
4255
-
4256
- **This action is permanent and cannot be undone.**
4257
-
4258
- Call again with confirmed=true to delete.`
4259
- }]
4260
- };
4261
- }
4262
- await client2.deleteBroadcast(args.broadcast_id);
4263
- return {
4264
- content: [{ type: "text", text: "Broadcast deleted." }]
4265
- };
4266
- }
4267
- );
4268
- server2.tool(
4269
- "delete_subscriber",
4270
- `Remove a subscriber from your mailing list. On Beehiiv this permanently deletes the subscriber. On Kit this unsubscribes them (Kit has no hard delete).
4271
-
4272
- REQUIRED WORKFLOW:
4273
- 1. ALWAYS set confirmed=false first to get a preview
4274
- 2. Show the user what will happen before confirming
4275
- 3. Only set confirmed=true after the user explicitly approves`,
4276
- {
4277
- subscriber_id: z15.string().describe("The subscriber ID to delete"),
4278
- confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
4279
- },
4280
- {
4281
- title: "Delete Subscriber",
4282
- readOnlyHint: false,
4283
- destructiveHint: true,
4284
- idempotentHint: true,
4285
- openWorldHint: true
4286
- },
4287
- async (args) => {
4288
- if (args.confirmed !== true) {
4289
- return {
4290
- content: [{
4291
- type: "text",
4292
- text: `## Delete Subscriber
4293
-
4294
- **Subscriber ID:** ${args.subscriber_id}
4295
-
4296
- **This action is permanent and cannot be undone.** The subscriber will be removed from all segments and automations.
4297
-
4298
- Call again with confirmed=true to delete.`
3306
+ **This action is permanent and cannot be undone.** The subscriber will be removed from all segments and automations.
3307
+
3308
+ Call again with confirmed=true to delete.`
4299
3309
  }]
4300
3310
  };
4301
3311
  }
@@ -4305,1577 +3315,6 @@ Call again with confirmed=true to delete.`
4305
3315
  };
4306
3316
  }
4307
3317
  );
4308
- server2.tool(
4309
- "delete_segment",
4310
- `Permanently delete a subscriber segment. This action is permanent and cannot be undone (Beehiiv only).
4311
-
4312
- REQUIRED WORKFLOW:
4313
- 1. ALWAYS set confirmed=false first to get a preview
4314
- 2. Show the user what will happen before confirming
4315
- 3. Only set confirmed=true after the user explicitly approves`,
4316
- {
4317
- segment_id: z15.string().describe("The segment ID to delete"),
4318
- confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
4319
- },
4320
- {
4321
- title: "Delete Segment",
4322
- readOnlyHint: false,
4323
- destructiveHint: true,
4324
- idempotentHint: true,
4325
- openWorldHint: true
4326
- },
4327
- async (args) => {
4328
- if (args.confirmed !== true) {
4329
- return {
4330
- content: [{
4331
- type: "text",
4332
- text: `## Delete Segment
4333
-
4334
- **Segment ID:** ${args.segment_id}
4335
-
4336
- **This action is permanent and cannot be undone.** Subscribers in this segment will not be deleted, but the segment grouping will be removed.
4337
-
4338
- Call again with confirmed=true to delete.`
4339
- }]
4340
- };
4341
- }
4342
- await client2.deleteSegment(args.segment_id);
4343
- return {
4344
- content: [{ type: "text", text: "Segment deleted." }]
4345
- };
4346
- }
4347
- );
4348
- server2.tool(
4349
- "delete_custom_field",
4350
- `Permanently delete a custom subscriber field and remove it from all subscribers. This action is permanent and cannot be undone.
4351
-
4352
- REQUIRED WORKFLOW:
4353
- 1. ALWAYS set confirmed=false first to get a preview
4354
- 2. Show the user what will happen before confirming
4355
- 3. Only set confirmed=true after the user explicitly approves`,
4356
- {
4357
- field_id: z15.string().describe("The custom field ID to delete"),
4358
- confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
4359
- },
4360
- {
4361
- title: "Delete Custom Field",
4362
- readOnlyHint: false,
4363
- destructiveHint: true,
4364
- idempotentHint: true,
4365
- openWorldHint: true
4366
- },
4367
- async (args) => {
4368
- if (args.confirmed !== true) {
4369
- return {
4370
- content: [{
4371
- type: "text",
4372
- text: `## Delete Custom Field
4373
-
4374
- **Field ID:** ${args.field_id}
4375
-
4376
- **This action is permanent and cannot be undone.** The field and its data will be removed from all subscribers.
4377
-
4378
- Call again with confirmed=true to delete.`
4379
- }]
4380
- };
4381
- }
4382
- await client2.deleteCustomField(args.field_id);
4383
- return {
4384
- content: [{ type: "text", text: "Custom field deleted." }]
4385
- };
4386
- }
4387
- );
4388
- server2.tool(
4389
- "delete_esp_webhook",
4390
- `Permanently delete a webhook from your ESP. This action is permanent and cannot be undone.
4391
-
4392
- REQUIRED WORKFLOW:
4393
- 1. ALWAYS set confirmed=false first to get a preview
4394
- 2. Show the user what will happen before confirming
4395
- 3. Only set confirmed=true after the user explicitly approves`,
4396
- {
4397
- webhook_id: z15.string().describe("The webhook ID to delete"),
4398
- confirmed: z15.boolean().default(false).describe("Set to true to confirm deletion")
4399
- },
4400
- {
4401
- title: "Delete ESP Webhook",
4402
- readOnlyHint: false,
4403
- destructiveHint: true,
4404
- idempotentHint: true,
4405
- openWorldHint: true
4406
- },
4407
- async (args) => {
4408
- if (args.confirmed !== true) {
4409
- return {
4410
- content: [{
4411
- type: "text",
4412
- text: `## Delete ESP Webhook
4413
-
4414
- **Webhook ID:** ${args.webhook_id}
4415
-
4416
- **This action is permanent and cannot be undone.** You will stop receiving event notifications at this webhook URL.
4417
-
4418
- Call again with confirmed=true to delete.`
4419
- }]
4420
- };
4421
- }
4422
- await client2.deleteEspWebhook(args.webhook_id);
4423
- return {
4424
- content: [{ type: "text", text: "Webhook deleted." }]
4425
- };
4426
- }
4427
- );
4428
- }
4429
-
4430
- // src/tools/carousel.ts
4431
- import { z as z16 } from "zod";
4432
- function registerCarouselTools(server2, client2) {
4433
- server2.tool(
4434
- "get_carousel_template",
4435
- "Get the customer's carousel template configuration. Returns brand colors, logo, CTA text, and other visual settings used to generate carousels from article URLs.",
4436
- {},
4437
- {
4438
- title: "Get Carousel Template",
4439
- readOnlyHint: true,
4440
- destructiveHint: false,
4441
- idempotentHint: true,
4442
- openWorldHint: false
4443
- },
4444
- async () => {
4445
- try {
4446
- const template = await client2.getCarouselTemplate();
4447
- const lines = [];
4448
- lines.push(`## Carousel Template: ${template.name || "Not configured"}`);
4449
- lines.push("");
4450
- if (!template.id) {
4451
- lines.push("No carousel template has been configured yet.");
4452
- lines.push("");
4453
- lines.push("Use `save_carousel_template` to set one up with your brand name, colors, and CTA text.");
4454
- return {
4455
- content: [{ type: "text", text: lines.join("\n") }]
4456
- };
4457
- }
4458
- lines.push(`**Brand Name:** ${template.brandName || "\u2014"}`);
4459
- if (template.logoUrl) lines.push(`**Logo:** ${template.logoUrl}`);
4460
- lines.push(`**Primary Color:** ${template.colorPrimary || "#1a1a2e"}`);
4461
- lines.push(`**Secondary Color:** ${template.colorSecondary || "#e94560"}`);
4462
- lines.push(`**Accent Color:** ${template.colorAccent || "#ffffff"}`);
4463
- lines.push(`**CTA Text:** ${template.ctaText || "Read the full story \u2192"}`);
4464
- lines.push(`**Logo Placement:** ${template.logoPlacement || "top-left"}`);
4465
- lines.push("");
4466
- return {
4467
- content: [{ type: "text", text: lines.join("\n") }]
4468
- };
4469
- } catch (error) {
4470
- const message = error instanceof Error ? error.message : "Unknown error";
4471
- if (message.includes("404")) {
4472
- return {
4473
- content: [
4474
- {
4475
- type: "text",
4476
- text: "No carousel template has been configured yet. Use `save_carousel_template` to set one up."
4477
- }
4478
- ]
4479
- };
4480
- }
4481
- throw error;
4482
- }
4483
- }
4484
- );
4485
- server2.tool(
4486
- "save_carousel_template",
4487
- `Create or update the customer's carousel template. This configures the visual style for auto-generated carousels (brand colors, logo, CTA text).
4488
-
4489
- USAGE:
4490
- - Set confirmed=false first to preview, then confirmed=true after user approval
4491
- - brand_name is required
4492
- - All color values should be hex codes (e.g. "#1a1a2e")
4493
- - logo_placement: "top-left", "top-right", or "top-center"`,
4494
- {
4495
- brand_name: z16.string().max(255).describe("Brand name displayed on CTA slide"),
4496
- color_primary: z16.string().max(20).optional().describe('Primary background color (hex, e.g. "#1a1a2e")'),
4497
- color_secondary: z16.string().max(20).optional().describe('Secondary/accent color for buttons and highlights (hex, e.g. "#e94560")'),
4498
- color_accent: z16.string().max(20).optional().describe('Text color (hex, e.g. "#ffffff")'),
4499
- cta_text: z16.string().max(255).optional().describe('Call-to-action text on the final slide (e.g. "Read the full story \u2192")'),
4500
- logo_placement: z16.enum(["top-left", "top-right", "top-center"]).optional().describe("Where to place the logo on slides"),
4501
- confirmed: z16.boolean().default(false).describe("Set to true to confirm and save. If false, returns a preview.")
4502
- },
4503
- {
4504
- title: "Save Carousel Template",
4505
- readOnlyHint: false,
4506
- destructiveHint: false,
4507
- idempotentHint: false,
4508
- openWorldHint: false
4509
- },
4510
- async (args) => {
4511
- const payload = {
4512
- brandName: args.brand_name
4513
- };
4514
- if (args.color_primary !== void 0) payload.colorPrimary = args.color_primary;
4515
- if (args.color_secondary !== void 0) payload.colorSecondary = args.color_secondary;
4516
- if (args.color_accent !== void 0) payload.colorAccent = args.color_accent;
4517
- if (args.cta_text !== void 0) payload.ctaText = args.cta_text;
4518
- if (args.logo_placement !== void 0) payload.logoPlacement = args.logo_placement;
4519
- if (args.confirmed !== true) {
4520
- const lines2 = [];
4521
- lines2.push("## Carousel Template Preview");
4522
- lines2.push("");
4523
- lines2.push(`**Brand Name:** ${args.brand_name}`);
4524
- if (args.color_primary) lines2.push(`**Primary Color:** ${args.color_primary}`);
4525
- if (args.color_secondary) lines2.push(`**Secondary Color:** ${args.color_secondary}`);
4526
- if (args.color_accent) lines2.push(`**Accent Color:** ${args.color_accent}`);
4527
- if (args.cta_text) lines2.push(`**CTA Text:** ${args.cta_text}`);
4528
- if (args.logo_placement) lines2.push(`**Logo Placement:** ${args.logo_placement}`);
4529
- lines2.push("");
4530
- lines2.push("---");
4531
- lines2.push("Call this tool again with **confirmed=true** to save these settings.");
4532
- return {
4533
- content: [{ type: "text", text: lines2.join("\n") }]
4534
- };
4535
- }
4536
- const result = await client2.updateCarouselTemplate(payload);
4537
- const lines = [];
4538
- lines.push(`Carousel template "${result.brandName || args.brand_name}" has been saved successfully.`);
4539
- lines.push("");
4540
- const updated = ["brand name"];
4541
- if (args.color_primary) updated.push("primary color");
4542
- if (args.color_secondary) updated.push("secondary color");
4543
- if (args.color_accent) updated.push("accent color");
4544
- if (args.cta_text) updated.push("CTA text");
4545
- if (args.logo_placement) updated.push("logo placement");
4546
- lines.push(`**Updated:** ${updated.join(", ")}`);
4547
- return {
4548
- content: [{ type: "text", text: lines.join("\n") }]
4549
- };
4550
- }
4551
- );
4552
- server2.tool(
4553
- "generate_carousel",
4554
- "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.",
4555
- {
4556
- url: z16.string().url().describe("The article URL to generate a carousel from")
4557
- },
4558
- {
4559
- title: "Generate Carousel",
4560
- readOnlyHint: false,
4561
- destructiveHint: false,
4562
- idempotentHint: false,
4563
- openWorldHint: true
4564
- },
4565
- async (args) => {
4566
- try {
4567
- const result = await client2.generateCarousel({
4568
- url: args.url
4569
- });
4570
- const lines = [];
4571
- lines.push("## Carousel Generated");
4572
- lines.push("");
4573
- lines.push(`**Headline:** ${result.article.headline}`);
4574
- lines.push(`**Source:** ${result.article.sourceDomain}`);
4575
- lines.push(`**Slides:** ${result.slides.length}`);
4576
- lines.push("");
4577
- lines.push("### Key Points");
4578
- for (const point of result.article.keyPoints) {
4579
- lines.push(`- ${point}`);
4580
- }
4581
- lines.push("");
4582
- lines.push("### Slide URLs");
4583
- for (const slide of result.slides) {
4584
- lines.push(`${slide.slideNumber}. [${slide.type}](${slide.cdnUrl})`);
4585
- }
4586
- lines.push("");
4587
- lines.push("These URLs can be used as `media_urls` when creating a post.");
4588
- return {
4589
- content: [{ type: "text", text: lines.join("\n") }]
4590
- };
4591
- } catch (error) {
4592
- const message = error instanceof Error ? error.message : "Unknown error";
4593
- return {
4594
- content: [
4595
- {
4596
- type: "text",
4597
- text: `Failed to generate carousel: ${message}`
4598
- }
4599
- ],
4600
- isError: true
4601
- };
4602
- }
4603
- }
4604
- );
4605
- server2.tool(
4606
- "preview_carousel",
4607
- "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.",
4608
- {
4609
- url: z16.string().url().describe("The article URL to preview")
4610
- },
4611
- {
4612
- title: "Preview Carousel",
4613
- readOnlyHint: true,
4614
- destructiveHint: false,
4615
- idempotentHint: false,
4616
- openWorldHint: true
4617
- },
4618
- async (args) => {
4619
- try {
4620
- const result = await client2.previewCarousel({
4621
- url: args.url
4622
- });
4623
- const lines = [];
4624
- lines.push("## Carousel Preview");
4625
- lines.push("");
4626
- lines.push(`**Headline:** ${result.article.headline}`);
4627
- lines.push(`**Source:** ${result.article.sourceDomain}`);
4628
- lines.push("");
4629
- lines.push(`**Hero Slide:** ${result.cdnUrl}`);
4630
- lines.push("");
4631
- lines.push("Use `generate_carousel` to create the full carousel with all slides.");
4632
- return {
4633
- content: [{ type: "text", text: lines.join("\n") }]
4634
- };
4635
- } catch (error) {
4636
- const message = error instanceof Error ? error.message : "Unknown error";
4637
- return {
4638
- content: [
4639
- {
4640
- type: "text",
4641
- text: `Failed to preview carousel: ${message}`
4642
- }
4643
- ],
4644
- isError: true
4645
- };
4646
- }
4647
- }
4648
- );
4649
- }
4650
-
4651
- // src/tools/queue.ts
4652
- import { z as z17 } from "zod";
4653
- var DAY_NAMES = [
4654
- "sunday",
4655
- "monday",
4656
- "tuesday",
4657
- "wednesday",
4658
- "thursday",
4659
- "friday",
4660
- "saturday"
4661
- ];
4662
- var DAY_ENUM = z17.enum([
4663
- "monday",
4664
- "tuesday",
4665
- "wednesday",
4666
- "thursday",
4667
- "friday",
4668
- "saturday",
4669
- "sunday"
4670
- ]);
4671
- function dayNameToNumber(name) {
4672
- const idx = DAY_NAMES.indexOf(name.toLowerCase());
4673
- return idx === -1 ? 0 : idx;
4674
- }
4675
- function dayNumberToName(num) {
4676
- return (DAY_NAMES[num] ?? "sunday").charAt(0).toUpperCase() + (DAY_NAMES[num] ?? "sunday").slice(1);
4677
- }
4678
- function getSlotId(slot) {
4679
- if (slot.id) return String(slot.id);
4680
- if (slot._id) return String(slot._id);
4681
- return `${slot.dayOfWeek}_${slot.time}`;
4682
- }
4683
- function formatSlot(slot) {
4684
- const day = dayNumberToName(slot.dayOfWeek);
4685
- const platforms = (slot.platforms ?? []).join(", ");
4686
- return `${day} ${slot.time} [${platforms}]`;
4687
- }
4688
- function registerQueueTools(server2, client2) {
4689
- server2.tool(
4690
- "create_queue_slot",
4691
- "Create a new recurring time slot in the posting queue. Each slot fires once per week at the specified day and time. Use get_queue first to see existing slots and avoid duplicates.",
4692
- {
4693
- day_of_week: DAY_ENUM.describe("Day of the week for this slot"),
4694
- time: z17.string().regex(/^\d{2}:\d{2}$/, "Must be HH:MM format (24-hour)").describe("Time in HH:MM 24-hour format, e.g. '09:00' or '14:30'"),
4695
- timezone: z17.string().default("UTC").describe("IANA timezone, e.g. 'America/New_York'. Defaults to UTC."),
4696
- platforms: z17.array(z17.string()).min(1).describe('Platforms for this slot, e.g. ["twitter", "linkedin"]'),
4697
- confirmed: z17.boolean().default(false).describe(
4698
- "Set to true to confirm creation. If false, returns a preview."
4699
- )
4700
- },
4701
- {
4702
- title: "Create Queue Slot",
4703
- readOnlyHint: false,
4704
- destructiveHint: false,
4705
- idempotentHint: false,
4706
- openWorldHint: true
4707
- },
4708
- async (args) => {
4709
- const dayNum = dayNameToNumber(args.day_of_week);
4710
- if (args.confirmed !== true) {
4711
- let text = "## Create Queue Slot Preview\n\n";
4712
- text += `**Day:** ${dayNumberToName(dayNum)}
4713
- `;
4714
- text += `**Time:** ${args.time}
4715
- `;
4716
- text += `**Timezone:** ${args.timezone}
4717
- `;
4718
- text += `**Platforms:** ${args.platforms.join(", ")}
4719
-
4720
- `;
4721
- try {
4722
- const data2 = await client2.getQueue();
4723
- const slots = data2?.slots ?? [];
4724
- if (slots.length > 0) {
4725
- text += "### Existing slots\n";
4726
- for (const slot of slots) {
4727
- text += `- ${formatSlot(slot)}
4728
- `;
4729
- }
4730
- text += "\n";
4731
- }
4732
- } catch {
4733
- }
4734
- text += "Call this tool again with confirmed=true to create the slot.";
4735
- return { content: [{ type: "text", text }] };
4736
- }
4737
- const data = await client2.getQueue();
4738
- const existingSlots = data?.slots ?? [];
4739
- const duplicate = existingSlots.find(
4740
- (s) => s.dayOfWeek === dayNum && s.time === args.time
4741
- );
4742
- if (duplicate) {
4743
- return {
4744
- content: [
4745
- {
4746
- type: "text",
4747
- text: `A slot already exists for ${dayNumberToName(dayNum)} at ${args.time}. Use update_queue_slot to modify it.`
4748
- }
4749
- ]
4750
- };
4751
- }
4752
- const updatedSlots = [
4753
- ...existingSlots.map((s) => ({
4754
- dayOfWeek: s.dayOfWeek,
4755
- time: s.time,
4756
- platforms: s.platforms
4757
- })),
4758
- {
4759
- dayOfWeek: dayNum,
4760
- time: args.time,
4761
- platforms: args.platforms
4762
- }
4763
- ];
4764
- await client2.updateQueue({
4765
- slots: updatedSlots,
4766
- active: data?.active,
4767
- timezone: args.timezone
4768
- });
4769
- return {
4770
- content: [
4771
- {
4772
- type: "text",
4773
- text: `Queue slot created: ${dayNumberToName(dayNum)} at ${args.time} for ${args.platforms.join(", ")}. The queue now has ${updatedSlots.length} slot(s).`
4774
- }
4775
- ]
4776
- };
4777
- }
4778
- );
4779
- server2.tool(
4780
- "update_queue_slot",
4781
- "Update an existing queue slot. Use get_queue first to see slot IDs. Only the fields you provide will be changed; omitted fields keep their current values.",
4782
- {
4783
- slot_id: z17.string().describe(
4784
- "ID of the slot to update (shown by get_queue). Can be the Late API id or a dayOfWeek_time key like '1_09:00'."
4785
- ),
4786
- day_of_week: DAY_ENUM.optional().describe("New day of the week"),
4787
- time: z17.string().regex(/^\d{2}:\d{2}$/, "Must be HH:MM format (24-hour)").optional().describe("New time in HH:MM 24-hour format"),
4788
- timezone: z17.string().optional().describe("New IANA timezone for the queue"),
4789
- platforms: z17.array(z17.string()).min(1).optional().describe("New platforms list"),
4790
- confirmed: z17.boolean().default(false).describe(
4791
- "Set to true to confirm the update. If false, returns a preview."
4792
- )
4793
- },
4794
- {
4795
- title: "Update Queue Slot",
4796
- readOnlyHint: false,
4797
- destructiveHint: false,
4798
- idempotentHint: true,
4799
- openWorldHint: true
4800
- },
4801
- async (args) => {
4802
- const data = await client2.getQueue();
4803
- const slots = data?.slots ?? [];
4804
- const slotIndex = slots.findIndex(
4805
- (s) => getSlotId(s) === args.slot_id
4806
- );
4807
- if (slotIndex === -1) {
4808
- const available = slots.map((s) => `${getSlotId(s)} (${formatSlot(s)})`).join("\n- ");
4809
- return {
4810
- content: [
4811
- {
4812
- type: "text",
4813
- text: `Slot "${args.slot_id}" not found.
4814
-
4815
- Available slots:
4816
- - ${available || "(none)"}`
4817
- }
4818
- ]
4819
- };
4820
- }
4821
- const current = slots[slotIndex];
4822
- const newDay = args.day_of_week !== void 0 ? dayNameToNumber(args.day_of_week) : current.dayOfWeek;
4823
- const newTime = args.time ?? current.time;
4824
- const newPlatforms = args.platforms ?? current.platforms;
4825
- if (args.confirmed !== true) {
4826
- let text = "## Update Queue Slot Preview\n\n";
4827
- text += `**Current:** ${formatSlot(current)}
4828
- `;
4829
- text += `**Updated:** ${dayNumberToName(newDay)} ${newTime} [${(newPlatforms ?? []).join(", ")}]
4830
- `;
4831
- if (args.timezone) text += `**Timezone:** ${args.timezone}
4832
- `;
4833
- text += "\nCall this tool again with confirmed=true to apply the update.";
4834
- return { content: [{ type: "text", text }] };
4835
- }
4836
- const updatedSlots = slots.map((s, i) => {
4837
- if (i === slotIndex) {
4838
- return {
4839
- dayOfWeek: newDay,
4840
- time: newTime,
4841
- platforms: newPlatforms
4842
- };
4843
- }
4844
- return {
4845
- dayOfWeek: s.dayOfWeek,
4846
- time: s.time,
4847
- platforms: s.platforms
4848
- };
4849
- });
4850
- await client2.updateQueue({
4851
- slots: updatedSlots,
4852
- active: data?.active,
4853
- ...args.timezone ? { timezone: args.timezone } : {}
4854
- });
4855
- return {
4856
- content: [
4857
- {
4858
- type: "text",
4859
- text: `Slot updated: ${dayNumberToName(newDay)} at ${newTime} for ${(newPlatforms ?? []).join(", ")}.`
4860
- }
4861
- ]
4862
- };
4863
- }
4864
- );
4865
- server2.tool(
4866
- "delete_queue_slot",
4867
- "Delete a queue slot by ID. Use get_queue first to see slot IDs. This removes the recurring time slot -- any posts already scheduled for this slot are not affected.",
4868
- {
4869
- slot_id: z17.string().describe(
4870
- "ID of the slot to delete (shown by get_queue)."
4871
- ),
4872
- confirmed: z17.boolean().default(false).describe(
4873
- "Set to true to confirm deletion. If false, shows slot details for review."
4874
- )
4875
- },
4876
- {
4877
- title: "Delete Queue Slot",
4878
- readOnlyHint: false,
4879
- destructiveHint: true,
4880
- idempotentHint: true,
4881
- openWorldHint: true
4882
- },
4883
- async (args) => {
4884
- const data = await client2.getQueue();
4885
- const slots = data?.slots ?? [];
4886
- const slotIndex = slots.findIndex(
4887
- (s) => getSlotId(s) === args.slot_id
4888
- );
4889
- if (slotIndex === -1) {
4890
- const available = slots.map((s) => `${getSlotId(s)} (${formatSlot(s)})`).join("\n- ");
4891
- return {
4892
- content: [
4893
- {
4894
- type: "text",
4895
- text: `Slot "${args.slot_id}" not found.
4896
-
4897
- Available slots:
4898
- - ${available || "(none)"}`
4899
- }
4900
- ]
4901
- };
4902
- }
4903
- const target = slots[slotIndex];
4904
- if (args.confirmed !== true) {
4905
- let text = "## Delete Queue Slot\n\n";
4906
- text += `**Slot:** ${formatSlot(target)}
4907
- `;
4908
- text += `**ID:** ${getSlotId(target)}
4909
-
4910
- `;
4911
- text += `This will remove this recurring time slot. Posts already scheduled are not affected.
4912
-
4913
- `;
4914
- text += `The queue will have ${slots.length - 1} slot(s) remaining.
4915
-
4916
- `;
4917
- text += "Call this tool again with confirmed=true to delete.";
4918
- return { content: [{ type: "text", text }] };
4919
- }
4920
- const updatedSlots = slots.filter((_, i) => i !== slotIndex).map((s) => ({
4921
- dayOfWeek: s.dayOfWeek,
4922
- time: s.time,
4923
- platforms: s.platforms
4924
- }));
4925
- await client2.updateQueue({
4926
- slots: updatedSlots,
4927
- active: data?.active
4928
- });
4929
- return {
4930
- content: [
4931
- {
4932
- type: "text",
4933
- text: `Slot deleted: ${formatSlot(target)}. The queue now has ${updatedSlots.length} slot(s).`
4934
- }
4935
- ]
4936
- };
4937
- }
4938
- );
4939
- server2.tool(
4940
- "toggle_queue",
4941
- `Enable or disable the entire posting queue. When disabled, no posts will be auto-scheduled to queue slots. Existing scheduled posts are not affected.
4942
-
4943
- If the user tries to schedule_to_queue while the queue is inactive, warn them and suggest enabling it with this tool first.`,
4944
- {
4945
- active: z17.boolean().describe("true to enable the queue, false to disable it"),
4946
- confirmed: z17.boolean().default(false).describe(
4947
- "Set to true to confirm the change. If false, returns a preview."
4948
- )
4949
- },
4950
- {
4951
- title: "Toggle Queue",
4952
- readOnlyHint: false,
4953
- destructiveHint: false,
4954
- idempotentHint: true,
4955
- openWorldHint: true
4956
- },
4957
- async (args) => {
4958
- const data = await client2.getQueue();
4959
- const currentActive = data?.active ?? false;
4960
- const slots = data?.slots ?? [];
4961
- if (currentActive === args.active) {
4962
- return {
4963
- content: [
4964
- {
4965
- type: "text",
4966
- text: `The queue is already ${args.active ? "enabled" : "disabled"}. No change needed.`
4967
- }
4968
- ]
4969
- };
4970
- }
4971
- if (args.confirmed !== true) {
4972
- let text = `## Toggle Queue
4973
-
4974
- `;
4975
- text += `**Current status:** ${currentActive ? "Enabled" : "Disabled"}
4976
- `;
4977
- text += `**New status:** ${args.active ? "Enabled" : "Disabled"}
4978
- `;
4979
- text += `**Slots configured:** ${slots.length}
4980
-
4981
- `;
4982
- if (args.active && slots.length === 0) {
4983
- text += "**Warning:** Enabling the queue with no slots configured will have no effect. Create slots first with create_queue_slot.\n\n";
4984
- }
4985
- if (!args.active && slots.length > 0) {
4986
- text += "**Note:** Disabling the queue will stop auto-scheduling. Existing scheduled posts will still go out.\n\n";
4987
- }
4988
- text += "Call this tool again with confirmed=true to apply.";
4989
- return { content: [{ type: "text", text }] };
4990
- }
4991
- const updatedSlots = slots.map((s) => ({
4992
- dayOfWeek: s.dayOfWeek,
4993
- time: s.time,
4994
- platforms: s.platforms
4995
- }));
4996
- await client2.updateQueue({
4997
- slots: updatedSlots,
4998
- active: args.active
4999
- });
5000
- return {
5001
- content: [
5002
- {
5003
- type: "text",
5004
- text: `Queue ${args.active ? "enabled" : "disabled"} successfully.${args.active && slots.length === 0 ? " Note: no slots are configured yet -- use create_queue_slot to add time slots." : ""}`
5005
- }
5006
- ]
5007
- };
5008
- }
5009
- );
5010
- }
5011
-
5012
- // src/tools/canva.ts
5013
- import { z as z18 } from "zod";
5014
- var CANVA_NOT_CONNECTED = "Canva is not connected. Visit BuzzPoster settings to connect your Canva account.";
5015
- function isNotConnectedError(error) {
5016
- if (error instanceof Error) {
5017
- return error.message.includes("Canva is not connected") || error.message.includes("400");
5018
- }
5019
- return false;
5020
- }
5021
- function registerCanvaTools(server2, client2) {
5022
- server2.tool(
5023
- "canva_upload_asset",
5024
- "Upload an image to Canva from a URL. Returns the Canva asset ID and thumbnail URL. Use this to import images into Canva for use in designs.",
5025
- {
5026
- image_url: z18.string().url().describe("The URL of the image to upload to Canva."),
5027
- name: z18.string().min(1).max(255).describe("A name for the uploaded asset.")
5028
- },
5029
- {
5030
- title: "Upload Asset to Canva",
5031
- readOnlyHint: false,
5032
- destructiveHint: false,
5033
- idempotentHint: false,
5034
- openWorldHint: true
5035
- },
5036
- async ({ image_url, name }) => {
5037
- try {
5038
- const result = await client2.canvaUploadAsset({
5039
- imageUrl: image_url,
5040
- name
5041
- });
5042
- const lines = [`Asset uploaded to Canva successfully.`];
5043
- lines.push(`Asset ID: ${result.assetId}`);
5044
- if (result.thumbnailUrl) {
5045
- lines.push(`Thumbnail: ${result.thumbnailUrl}`);
5046
- }
5047
- return {
5048
- content: [{ type: "text", text: lines.join("\n") }]
5049
- };
5050
- } catch (error) {
5051
- if (isNotConnectedError(error)) {
5052
- return {
5053
- content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
5054
- };
5055
- }
5056
- const message = error instanceof Error ? error.message : "Unknown error";
5057
- throw new Error(`Failed to upload asset to Canva: ${message}`);
5058
- }
5059
- }
5060
- );
5061
- server2.tool(
5062
- "canva_create_design",
5063
- "Create a new Canva design. Returns the design ID and an edit URL the user can open to customize the design in Canva's editor.",
5064
- {
5065
- title: z18.string().min(1).max(255).describe("Title for the new design."),
5066
- design_type: z18.enum([
5067
- "instagram_post",
5068
- "facebook_post",
5069
- "presentation",
5070
- "poster",
5071
- "story"
5072
- ]).describe("The type of design to create, which determines dimensions."),
5073
- asset_id: z18.string().optional().describe(
5074
- "Optional Canva asset ID to include in the design."
5075
- )
5076
- },
5077
- {
5078
- title: "Create Canva Design",
5079
- readOnlyHint: false,
5080
- destructiveHint: false,
5081
- idempotentHint: false,
5082
- openWorldHint: true
5083
- },
5084
- async ({ title, design_type, asset_id }) => {
5085
- try {
5086
- const result = await client2.canvaCreateDesign({
5087
- title,
5088
- designType: design_type,
5089
- assetId: asset_id
5090
- });
5091
- const lines = [`Canva design created successfully.`];
5092
- lines.push(`Design ID: ${result.designId}`);
5093
- lines.push(`Edit URL: ${result.editUrl}`);
5094
- if (result.thumbnailUrl) {
5095
- lines.push(`Thumbnail: ${result.thumbnailUrl}`);
5096
- }
5097
- return {
5098
- content: [{ type: "text", text: lines.join("\n") }]
5099
- };
5100
- } catch (error) {
5101
- if (isNotConnectedError(error)) {
5102
- return {
5103
- content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
5104
- };
5105
- }
5106
- const message = error instanceof Error ? error.message : "Unknown error";
5107
- throw new Error(`Failed to create Canva design: ${message}`);
5108
- }
5109
- }
5110
- );
5111
- server2.tool(
5112
- "canva_export_design",
5113
- "Export a Canva design to a downloadable file. Polls until the export is complete and returns the download URL.",
5114
- {
5115
- design_id: z18.string().min(1).describe("The Canva design ID to export."),
5116
- format: z18.enum(["png", "pdf", "jpg"]).default("png").describe("Export format. Defaults to PNG.")
5117
- },
5118
- {
5119
- title: "Export Canva Design",
5120
- readOnlyHint: false,
5121
- destructiveHint: false,
5122
- idempotentHint: true,
5123
- openWorldHint: true
5124
- },
5125
- async ({ design_id, format }) => {
5126
- try {
5127
- const result = await client2.canvaExportDesign({
5128
- designId: design_id,
5129
- format
5130
- });
5131
- return {
5132
- content: [
5133
- {
5134
- type: "text",
5135
- text: `Design exported successfully.
5136
- Download URL: ${result.downloadUrl}`
5137
- }
5138
- ]
5139
- };
5140
- } catch (error) {
5141
- if (isNotConnectedError(error)) {
5142
- return {
5143
- content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
5144
- };
5145
- }
5146
- const message = error instanceof Error ? error.message : "Unknown error";
5147
- throw new Error(`Failed to export Canva design: ${message}`);
5148
- }
5149
- }
5150
- );
5151
- server2.tool(
5152
- "canva_search_designs",
5153
- "Search the customer's Canva designs by keyword. Returns matching designs with their IDs, titles, thumbnails, and edit URLs.",
5154
- {
5155
- query: z18.string().min(1).describe("Search keyword to find designs.")
5156
- },
5157
- {
5158
- title: "Search Canva Designs",
5159
- readOnlyHint: true,
5160
- destructiveHint: false,
5161
- idempotentHint: true,
5162
- openWorldHint: true
5163
- },
5164
- async ({ query }) => {
5165
- try {
5166
- const designs = await client2.canvaSearchDesigns(query);
5167
- if (!designs || designs.length === 0) {
5168
- return {
5169
- content: [
5170
- {
5171
- type: "text",
5172
- text: `No Canva designs found matching "${query}".`
5173
- }
5174
- ]
5175
- };
5176
- }
5177
- const lines = [`## Canva Designs (${designs.length} results)`];
5178
- lines.push("");
5179
- for (const d of designs) {
5180
- lines.push(`**${d.title}** (ID: ${d.id})`);
5181
- lines.push(` Edit: ${d.editUrl}`);
5182
- if (d.thumbnailUrl) lines.push(` Thumbnail: ${d.thumbnailUrl}`);
5183
- lines.push("");
5184
- }
5185
- return {
5186
- content: [{ type: "text", text: lines.join("\n") }]
5187
- };
5188
- } catch (error) {
5189
- if (isNotConnectedError(error)) {
5190
- return {
5191
- content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
5192
- };
5193
- }
5194
- const message = error instanceof Error ? error.message : "Unknown error";
5195
- throw new Error(`Failed to search Canva designs: ${message}`);
5196
- }
5197
- }
5198
- );
5199
- server2.tool(
5200
- "canva_get_design",
5201
- "Get details about a specific Canva design by ID. Returns the design's title, edit URL, thumbnail, and metadata.",
5202
- {
5203
- design_id: z18.string().min(1).describe("The Canva design ID to retrieve.")
5204
- },
5205
- {
5206
- title: "Get Canva Design",
5207
- readOnlyHint: true,
5208
- destructiveHint: false,
5209
- idempotentHint: true,
5210
- openWorldHint: true
5211
- },
5212
- async ({ design_id }) => {
5213
- try {
5214
- const design = await client2.canvaGetDesign(design_id);
5215
- const lines = [`## ${design.title}`];
5216
- lines.push(`Design ID: ${design.id}`);
5217
- lines.push(`Edit URL: ${design.editUrl}`);
5218
- lines.push(`View URL: ${design.viewUrl}`);
5219
- if (design.thumbnailUrl) {
5220
- lines.push(`Thumbnail: ${design.thumbnailUrl}`);
5221
- }
5222
- if (design.createdAt) lines.push(`Created: ${design.createdAt}`);
5223
- if (design.updatedAt) lines.push(`Updated: ${design.updatedAt}`);
5224
- return {
5225
- content: [{ type: "text", text: lines.join("\n") }]
5226
- };
5227
- } catch (error) {
5228
- if (isNotConnectedError(error)) {
5229
- return {
5230
- content: [{ type: "text", text: CANVA_NOT_CONNECTED }]
5231
- };
5232
- }
5233
- const message = error instanceof Error ? error.message : "Unknown error";
5234
- throw new Error(`Failed to get Canva design: ${message}`);
5235
- }
5236
- }
5237
- );
5238
- }
5239
-
5240
- // src/tools/sendgrid-newsletter.ts
5241
- import { z as z19 } from "zod";
5242
- function registerSendGridNewsletterTools(server2, client2, options) {
5243
- const allowDirectSend2 = options?.allowDirectSend ?? false;
5244
- server2.tool(
5245
- "sg_list_lists",
5246
- "List all SendGrid subscriber lists for the account. Returns list names, IDs, and descriptions.",
5247
- {},
5248
- {
5249
- title: "List Subscriber Lists",
5250
- readOnlyHint: true,
5251
- destructiveHint: false,
5252
- idempotentHint: true,
5253
- openWorldHint: false
5254
- },
5255
- async () => {
5256
- const lists = await client2.sgListLists();
5257
- if (!lists || lists.length === 0) {
5258
- return {
5259
- content: [
5260
- {
5261
- type: "text",
5262
- text: "No subscriber lists found. Create one with sg_create_list."
5263
- }
5264
- ]
5265
- };
5266
- }
5267
- const lines = [`## Subscriber Lists (${lists.length})`];
5268
- for (const l of lists) {
5269
- lines.push(`- **${l.name}** (ID: ${l.id})${l.description ? ` \u2014 ${l.description}` : ""}`);
5270
- }
5271
- return {
5272
- content: [{ type: "text", text: lines.join("\n") }]
5273
- };
5274
- }
5275
- );
5276
- server2.tool(
5277
- "sg_create_list",
5278
- "Create a new subscriber list for organizing newsletter recipients.",
5279
- {
5280
- name: z19.string().min(1).max(255).describe("Name of the subscriber list."),
5281
- description: z19.string().max(1e3).optional().describe("Optional description of the list.")
5282
- },
5283
- {
5284
- title: "Create Subscriber List",
5285
- readOnlyHint: false,
5286
- destructiveHint: false,
5287
- idempotentHint: false,
5288
- openWorldHint: false
5289
- },
5290
- async ({ name, description }) => {
5291
- const list = await client2.sgCreateList({ name, description });
5292
- return {
5293
- content: [
5294
- {
5295
- type: "text",
5296
- text: `Subscriber list created.
5297
- Name: ${list.name}
5298
- ID: ${list.id}`
5299
- }
5300
- ]
5301
- };
5302
- }
5303
- );
5304
- server2.tool(
5305
- "sg_delete_list",
5306
- "Delete a subscriber list. This removes the list and all memberships (subscribers themselves are not deleted). Requires confirmation.",
5307
- {
5308
- list_id: z19.number().int().describe("ID of the list to delete."),
5309
- confirmed: z19.boolean().describe(
5310
- "Must be true to confirm deletion. Show the list name to the user and ask for confirmation first."
5311
- )
5312
- },
5313
- {
5314
- title: "Delete Subscriber List",
5315
- readOnlyHint: false,
5316
- destructiveHint: true,
5317
- idempotentHint: false,
5318
- openWorldHint: false
5319
- },
5320
- async ({ list_id, confirmed }) => {
5321
- if (!confirmed) {
5322
- return {
5323
- content: [
5324
- {
5325
- type: "text",
5326
- text: "Deletion not confirmed. Set confirmed=true to proceed."
5327
- }
5328
- ]
5329
- };
5330
- }
5331
- await client2.sgDeleteList(list_id);
5332
- return {
5333
- content: [
5334
- {
5335
- type: "text",
5336
- text: `Subscriber list ${list_id} deleted successfully.`
5337
- }
5338
- ]
5339
- };
5340
- }
5341
- );
5342
- server2.tool(
5343
- "sg_list_subscribers",
5344
- "List SendGrid-managed subscribers. Can filter by list ID and status (active, unsubscribed, bounced, complained). Supports pagination.",
5345
- {
5346
- list_id: z19.number().int().optional().describe("Optional list ID to filter subscribers by list membership."),
5347
- status: z19.enum(["active", "unsubscribed", "bounced", "complained"]).optional().describe("Optional status filter."),
5348
- page: z19.number().int().min(1).default(1).describe("Page number."),
5349
- limit: z19.number().int().min(1).max(100).default(50).describe("Results per page.")
5350
- },
5351
- {
5352
- title: "List Subscribers",
5353
- readOnlyHint: true,
5354
- destructiveHint: false,
5355
- idempotentHint: true,
5356
- openWorldHint: false
5357
- },
5358
- async ({ list_id, status, page, limit }) => {
5359
- const params = {
5360
- page: String(page),
5361
- limit: String(limit)
5362
- };
5363
- if (list_id !== void 0) params.listId = String(list_id);
5364
- if (status) params.status = status;
5365
- const result = await client2.sgListSubscribers(params);
5366
- if (!result.subscribers || result.subscribers.length === 0) {
5367
- return {
5368
- content: [
5369
- {
5370
- type: "text",
5371
- text: "No subscribers found matching the criteria."
5372
- }
5373
- ]
5374
- };
5375
- }
5376
- const lines = [`## Subscribers (page ${result.page})`];
5377
- for (const s of result.subscribers) {
5378
- const name = [s.firstName, s.lastName].filter(Boolean).join(" ");
5379
- lines.push(
5380
- `- ${s.email}${name ? ` (${name})` : ""} \u2014 ${s.status} (ID: ${s.id})`
5381
- );
5382
- }
5383
- return {
5384
- content: [{ type: "text", text: lines.join("\n") }]
5385
- };
5386
- }
5387
- );
5388
- server2.tool(
5389
- "sg_add_subscriber",
5390
- "Add or update a subscriber in the SendGrid-managed subscriber list. Optionally assign to one or more lists.",
5391
- {
5392
- email: z19.string().email().describe("Subscriber's email address."),
5393
- first_name: z19.string().optional().describe("Subscriber's first name."),
5394
- last_name: z19.string().optional().describe("Subscriber's last name."),
5395
- list_ids: z19.array(z19.number().int()).optional().describe("Optional list IDs to add the subscriber to.")
5396
- },
5397
- {
5398
- title: "Add Subscriber",
5399
- readOnlyHint: false,
5400
- destructiveHint: false,
5401
- idempotentHint: true,
5402
- openWorldHint: false
5403
- },
5404
- async ({ email, first_name, last_name, list_ids }) => {
5405
- const subscriber = await client2.sgAddSubscriber({
5406
- email,
5407
- firstName: first_name,
5408
- lastName: last_name,
5409
- listIds: list_ids
5410
- });
5411
- return {
5412
- content: [
5413
- {
5414
- type: "text",
5415
- text: `Subscriber added/updated.
5416
- Email: ${subscriber.email}
5417
- ID: ${subscriber.id}`
5418
- }
5419
- ]
5420
- };
5421
- }
5422
- );
5423
- server2.tool(
5424
- "sg_remove_subscriber",
5425
- "Remove a subscriber (marks as unsubscribed). Requires confirmation.",
5426
- {
5427
- subscriber_id: z19.number().int().describe("ID of the subscriber to remove."),
5428
- confirmed: z19.boolean().describe(
5429
- "Must be true to confirm. Show the subscriber email to the user first."
5430
- )
5431
- },
5432
- {
5433
- title: "Remove Subscriber",
5434
- readOnlyHint: false,
5435
- destructiveHint: true,
5436
- idempotentHint: false,
5437
- openWorldHint: false
5438
- },
5439
- async ({ subscriber_id, confirmed }) => {
5440
- if (!confirmed) {
5441
- return {
5442
- content: [
5443
- {
5444
- type: "text",
5445
- text: "Removal not confirmed. Set confirmed=true to proceed."
5446
- }
5447
- ]
5448
- };
5449
- }
5450
- const result = await client2.sgRemoveSubscriber(subscriber_id);
5451
- return {
5452
- content: [
5453
- {
5454
- type: "text",
5455
- text: `Subscriber ${result.email} marked as unsubscribed.`
5456
- }
5457
- ]
5458
- };
5459
- }
5460
- );
5461
- server2.tool(
5462
- "sg_list_sends",
5463
- "List newsletter sends (drafts, scheduled, sent). Filter by status.",
5464
- {
5465
- status: z19.enum(["draft", "scheduled", "sending", "sent", "cancelled"]).optional().describe("Optional status filter."),
5466
- page: z19.number().int().min(1).default(1).describe("Page number."),
5467
- limit: z19.number().int().min(1).max(100).default(20).describe("Results per page.")
5468
- },
5469
- {
5470
- title: "List Newsletter Sends",
5471
- readOnlyHint: true,
5472
- destructiveHint: false,
5473
- idempotentHint: true,
5474
- openWorldHint: false
5475
- },
5476
- async ({ status, page, limit }) => {
5477
- const params = {
5478
- page: String(page),
5479
- limit: String(limit)
5480
- };
5481
- if (status) params.status = status;
5482
- const result = await client2.sgListSends(params);
5483
- if (!result.sends || result.sends.length === 0) {
5484
- return {
5485
- content: [
5486
- { type: "text", text: "No newsletter sends found." }
5487
- ]
5488
- };
5489
- }
5490
- const lines = [`## Newsletter Sends (${result.sends.length})`];
5491
- for (const s of result.sends) {
5492
- lines.push(
5493
- `- **${s.subject}** (ID: ${s.id}) \u2014 ${s.status}${s.sentAt ? `, sent ${s.sentAt}` : ""}${s.scheduledAt ? `, scheduled ${s.scheduledAt}` : ""}`
5494
- );
5495
- if (s.status === "sent") {
5496
- lines.push(` Delivered: ${s.delivered} | Opened: ${s.opened}`);
5497
- }
5498
- }
5499
- return {
5500
- content: [{ type: "text", text: lines.join("\n") }]
5501
- };
5502
- }
5503
- );
5504
- server2.tool(
5505
- "sg_get_send",
5506
- "Get details and delivery stats for a specific newsletter send.",
5507
- {
5508
- send_id: z19.number().int().describe("ID of the newsletter send.")
5509
- },
5510
- {
5511
- title: "Get Newsletter Send",
5512
- readOnlyHint: true,
5513
- destructiveHint: false,
5514
- idempotentHint: true,
5515
- openWorldHint: false
5516
- },
5517
- async ({ send_id }) => {
5518
- const send = await client2.sgGetSend(send_id);
5519
- const lines = [
5520
- `## ${send.subject}`,
5521
- `ID: ${send.id}`,
5522
- `From: ${send.fromName ? `${send.fromName} <${send.fromEmail}>` : send.fromEmail}`,
5523
- `Status: ${send.status}`
5524
- ];
5525
- if (send.sentAt) lines.push(`Sent: ${send.sentAt}`);
5526
- if (send.scheduledAt) lines.push(`Scheduled: ${send.scheduledAt}`);
5527
- lines.push("", "### Delivery Stats");
5528
- lines.push(`Delivered: ${send.delivered}`);
5529
- lines.push(`Opened: ${send.opened}`);
5530
- lines.push(`Clicked: ${send.clicked}`);
5531
- lines.push(`Bounced: ${send.bounced}`);
5532
- lines.push(`Complaints: ${send.complained}`);
5533
- lines.push(`Unsubscribed: ${send.unsubscribed}`);
5534
- lines.push("");
5535
- lines.push(`NEWSLETTER_HTML_PREVIEW: ${send.htmlContent}`);
5536
- return {
5537
- content: [{ type: "text", text: lines.join("\n") }]
5538
- };
5539
- }
5540
- );
5541
- server2.tool(
5542
- "sg_create_send",
5543
- "Create a new newsletter send draft. Must specify subject, HTML content, and from email.",
5544
- {
5545
- subject: z19.string().min(1).max(500).describe("Email subject line."),
5546
- html_content: z19.string().min(1).describe("HTML content of the newsletter."),
5547
- from_email: z19.string().email().describe("Sender email address (must be authenticated domain)."),
5548
- from_name: z19.string().optional().describe("Sender display name."),
5549
- list_id: z19.number().int().optional().describe("Optional list ID to send to. If omitted, sends to all active subscribers.")
5550
- },
5551
- {
5552
- title: "Create Newsletter Draft",
5553
- readOnlyHint: false,
5554
- destructiveHint: false,
5555
- idempotentHint: false,
5556
- openWorldHint: false
5557
- },
5558
- async ({ subject, html_content, from_email, from_name, list_id }) => {
5559
- const send = await client2.sgCreateSend({
5560
- subject,
5561
- htmlContent: html_content,
5562
- fromEmail: from_email,
5563
- fromName: from_name,
5564
- listId: list_id
5565
- });
5566
- return {
5567
- content: [
5568
- {
5569
- type: "text",
5570
- text: `Newsletter draft created.
5571
- ID: ${send.id}
5572
- Subject: ${send.subject}
5573
- Status: ${send.status}
5574
-
5575
- Use sg_test_send to preview, or sg_send_newsletter to send.`
5576
- }
5577
- ]
5578
- };
5579
- }
5580
- );
5581
- server2.tool(
5582
- "sg_update_send",
5583
- "Update a newsletter draft. Only drafts can be updated.",
5584
- {
5585
- send_id: z19.number().int().describe("ID of the send to update."),
5586
- subject: z19.string().min(1).max(500).optional().describe("New subject line."),
5587
- html_content: z19.string().min(1).optional().describe("New HTML content."),
5588
- from_email: z19.string().email().optional().describe("New sender email."),
5589
- from_name: z19.string().optional().describe("New sender name."),
5590
- list_id: z19.number().int().nullable().optional().describe("New list ID, or null to send to all.")
5591
- },
5592
- {
5593
- title: "Update Newsletter Draft",
5594
- readOnlyHint: false,
5595
- destructiveHint: false,
5596
- idempotentHint: true,
5597
- openWorldHint: false
5598
- },
5599
- async ({ send_id, subject, html_content, from_email, from_name, list_id }) => {
5600
- const data = {};
5601
- if (subject !== void 0) data.subject = subject;
5602
- if (html_content !== void 0) data.htmlContent = html_content;
5603
- if (from_email !== void 0) data.fromEmail = from_email;
5604
- if (from_name !== void 0) data.fromName = from_name;
5605
- if (list_id !== void 0) data.listId = list_id;
5606
- const send = await client2.sgUpdateSend(send_id, data);
5607
- return {
5608
- content: [
5609
- {
5610
- type: "text",
5611
- text: `Newsletter draft updated.
5612
- ID: ${send.id}
5613
- Subject: ${send.subject}`
5614
- }
5615
- ]
5616
- };
5617
- }
5618
- );
5619
- server2.tool(
5620
- "sg_delete_send",
5621
- "Delete a newsletter draft. Only drafts can be deleted. Requires confirmation.",
5622
- {
5623
- send_id: z19.number().int().describe("ID of the draft to delete."),
5624
- confirmed: z19.boolean().describe("Must be true to confirm deletion.")
5625
- },
5626
- {
5627
- title: "Delete Newsletter Draft",
5628
- readOnlyHint: false,
5629
- destructiveHint: true,
5630
- idempotentHint: false,
5631
- openWorldHint: false
5632
- },
5633
- async ({ send_id, confirmed }) => {
5634
- if (!confirmed) {
5635
- return {
5636
- content: [
5637
- {
5638
- type: "text",
5639
- text: "Deletion not confirmed. Set confirmed=true to proceed."
5640
- }
5641
- ]
5642
- };
5643
- }
5644
- await client2.sgDeleteSend(send_id);
5645
- return {
5646
- content: [
5647
- {
5648
- type: "text",
5649
- text: `Newsletter draft ${send_id} deleted.`
5650
- }
5651
- ]
5652
- };
5653
- }
5654
- );
5655
- server2.tool(
5656
- "sg_send_newsletter",
5657
- allowDirectSend2 ? "Send a newsletter immediately to all active subscribers (or subscribers in the specified list). This action cannot be undone. Requires confirmation." : "DISABLED: Direct send is not enabled for this account. Contact support.",
5658
- {
5659
- send_id: z19.number().int().describe("ID of the draft to send."),
5660
- confirmed: z19.boolean().describe(
5661
- "Must be true. Show the user the subject, from address, and recipient count before confirming."
5662
- )
5663
- },
5664
- {
5665
- title: "Send Newsletter Now",
5666
- readOnlyHint: false,
5667
- destructiveHint: true,
5668
- idempotentHint: false,
5669
- openWorldHint: true
5670
- },
5671
- async ({ send_id, confirmed }) => {
5672
- if (!allowDirectSend2) {
5673
- return {
5674
- content: [
5675
- {
5676
- type: "text",
5677
- text: "Direct send is not enabled for this account. Contact support to enable this feature."
5678
- }
5679
- ]
5680
- };
5681
- }
5682
- if (!confirmed) {
5683
- return {
5684
- content: [
5685
- {
5686
- type: "text",
5687
- text: "Send not confirmed. Review the newsletter details with sg_get_send first, then set confirmed=true."
5688
- }
5689
- ]
5690
- };
5691
- }
5692
- const result = await client2.sgSendNewsletter(send_id);
5693
- return {
5694
- content: [
5695
- {
5696
- type: "text",
5697
- text: `Newsletter sent successfully!
5698
- Subject: ${result.subject}
5699
- Recipients: ${result.recipientCount}
5700
- Status: ${result.status}`
5701
- }
5702
- ]
5703
- };
5704
- }
5705
- );
5706
- server2.tool(
5707
- "sg_test_send",
5708
- "Send a test email of a newsletter draft to a specific email address. Subject will be prefixed with [TEST].",
5709
- {
5710
- send_id: z19.number().int().describe("ID of the newsletter send."),
5711
- email: z19.string().email().describe("Email address to send the test to.")
5712
- },
5713
- {
5714
- title: "Send Test Email",
5715
- readOnlyHint: false,
5716
- destructiveHint: false,
5717
- idempotentHint: false,
5718
- openWorldHint: true
5719
- },
5720
- async ({ send_id, email }) => {
5721
- await client2.sgTestSend(send_id, { email });
5722
- return {
5723
- content: [
5724
- {
5725
- type: "text",
5726
- text: `Test email sent to ${email}. Check your inbox.`
5727
- }
5728
- ]
5729
- };
5730
- }
5731
- );
5732
- server2.tool(
5733
- "sg_schedule_send",
5734
- "Schedule a newsletter to be sent at a future date/time. Requires confirmation.",
5735
- {
5736
- send_id: z19.number().int().describe("ID of the draft to schedule."),
5737
- scheduled_at: z19.string().describe(
5738
- "ISO 8601 datetime for when to send (e.g., '2025-01-15T09:00:00Z'). Must be in the future."
5739
- ),
5740
- confirmed: z19.boolean().describe("Must be true to confirm scheduling.")
5741
- },
5742
- {
5743
- title: "Schedule Newsletter Send",
5744
- readOnlyHint: false,
5745
- destructiveHint: false,
5746
- idempotentHint: false,
5747
- openWorldHint: true
5748
- },
5749
- async ({ send_id, scheduled_at, confirmed }) => {
5750
- if (!confirmed) {
5751
- return {
5752
- content: [
5753
- {
5754
- type: "text",
5755
- text: "Scheduling not confirmed. Set confirmed=true to proceed."
5756
- }
5757
- ]
5758
- };
5759
- }
5760
- const result = await client2.sgScheduleSend(send_id, {
5761
- scheduledAt: scheduled_at
5762
- });
5763
- return {
5764
- content: [
5765
- {
5766
- type: "text",
5767
- text: `Newsletter scheduled.
5768
- Subject: ${result.subject}
5769
- Scheduled for: ${result.scheduledAt}
5770
- Recipients: ${result.recipientCount}`
5771
- }
5772
- ]
5773
- };
5774
- }
5775
- );
5776
- server2.tool(
5777
- "sg_authenticate_domain",
5778
- "Start domain authentication with SendGrid. Returns DNS records that need to be added to your domain's DNS settings.",
5779
- {
5780
- domain: z19.string().min(1).describe("Domain to authenticate (e.g., 'example.com').")
5781
- },
5782
- {
5783
- title: "Authenticate Domain",
5784
- readOnlyHint: false,
5785
- destructiveHint: false,
5786
- idempotentHint: false,
5787
- openWorldHint: true
5788
- },
5789
- async ({ domain }) => {
5790
- const result = await client2.sgAuthenticateDomain(domain);
5791
- const lines = [
5792
- `Domain authentication started for **${result.domain}**.`,
5793
- `Domain ID: ${result.id}`,
5794
- "",
5795
- "Add these DNS records to your domain:",
5796
- "```",
5797
- JSON.stringify(result.dnsRecords, null, 2),
5798
- "```",
5799
- "",
5800
- "After adding the records, use sg_validate_domain to verify."
5801
- ];
5802
- return {
5803
- content: [{ type: "text", text: lines.join("\n") }]
5804
- };
5805
- }
5806
- );
5807
- server2.tool(
5808
- "sg_validate_domain",
5809
- "Validate DNS records for a domain that was previously set up for authentication.",
5810
- {
5811
- domain_id: z19.number().int().describe("Domain ID from sg_authenticate_domain.")
5812
- },
5813
- {
5814
- title: "Validate Domain",
5815
- readOnlyHint: true,
5816
- destructiveHint: false,
5817
- idempotentHint: true,
5818
- openWorldHint: true
5819
- },
5820
- async ({ domain_id }) => {
5821
- const result = await client2.sgValidateDomain(domain_id);
5822
- if (result.valid) {
5823
- return {
5824
- content: [
5825
- {
5826
- type: "text",
5827
- text: "Domain validation successful! Your domain is now authenticated for sending."
5828
- }
5829
- ]
5830
- };
5831
- }
5832
- return {
5833
- content: [
5834
- {
5835
- type: "text",
5836
- text: `Domain validation failed. Check your DNS records.
5837
-
5838
- Results:
5839
- ${JSON.stringify(result.validationResults, null, 2)}`
5840
- }
5841
- ]
5842
- };
5843
- }
5844
- );
5845
- server2.tool(
5846
- "sg_list_domains",
5847
- "List all authenticated sending domains.",
5848
- {},
5849
- {
5850
- title: "List Authenticated Domains",
5851
- readOnlyHint: true,
5852
- destructiveHint: false,
5853
- idempotentHint: true,
5854
- openWorldHint: true
5855
- },
5856
- async () => {
5857
- const domains = await client2.sgListDomains();
5858
- if (!domains || domains.length === 0) {
5859
- return {
5860
- content: [
5861
- {
5862
- type: "text",
5863
- text: "No authenticated domains found. Use sg_authenticate_domain to set one up."
5864
- }
5865
- ]
5866
- };
5867
- }
5868
- const lines = [`## Authenticated Domains (${domains.length})`];
5869
- for (const d of domains) {
5870
- lines.push(
5871
- `- **${d.domain}** (ID: ${d.id}) \u2014 ${d.valid ? "Valid" : "Not verified"}`
5872
- );
5873
- }
5874
- return {
5875
- content: [{ type: "text", text: lines.join("\n") }]
5876
- };
5877
- }
5878
- );
5879
3318
  }
5880
3319
 
5881
3320
  // src/index.ts
@@ -5921,12 +3360,7 @@ registerNewsletterTemplateTools(server, client);
5921
3360
  registerCalendarTools(server, client);
5922
3361
  registerNotificationTools(server, client);
5923
3362
  registerPublishingRulesTools(server, client);
5924
- registerAuditLogTools(server, client);
5925
3363
  registerSourceTools(server, client);
5926
3364
  registerNewsletterAdvancedTools(server, client);
5927
- registerCarouselTools(server, client);
5928
- registerQueueTools(server, client);
5929
- registerCanvaTools(server, client);
5930
- registerSendGridNewsletterTools(server, client, { allowDirectSend });
5931
3365
  var transport = new StdioServerTransport();
5932
3366
  await server.connect(transport);