@buzzposter/mcp 0.1.1 → 0.1.4

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.
Files changed (2) hide show
  1. package/dist/index.js +1159 -19
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -163,6 +163,53 @@ var BuzzPosterClient = class {
163
163
  async getBrandVoice() {
164
164
  return this.request("GET", "/api/v1/brand-voice");
165
165
  }
166
+ // Audiences
167
+ async getAudience(id) {
168
+ return this.request("GET", `/api/v1/audiences/${id}`);
169
+ }
170
+ async getDefaultAudience() {
171
+ return this.request("GET", "/api/v1/audiences/default");
172
+ }
173
+ // Newsletter Templates
174
+ async getTemplate(id) {
175
+ if (id) return this.request("GET", `/api/v1/templates/${id}`);
176
+ return this.request("GET", "/api/v1/templates/default");
177
+ }
178
+ async listTemplates() {
179
+ return this.request("GET", "/api/v1/templates");
180
+ }
181
+ // Newsletter Archive
182
+ async listNewsletterArchive(params) {
183
+ return this.request("GET", "/api/v1/newsletter-archive", void 0, params);
184
+ }
185
+ async getArchivedNewsletter(id) {
186
+ return this.request("GET", `/api/v1/newsletter-archive/${id}`);
187
+ }
188
+ async saveNewsletterToArchive(data) {
189
+ return this.request("POST", "/api/v1/newsletter-archive", data);
190
+ }
191
+ // Calendar
192
+ async getCalendar(params) {
193
+ return this.request("GET", "/api/v1/calendar", void 0, params);
194
+ }
195
+ async rescheduleCalendarItem(id, scheduledFor) {
196
+ return this.request("PUT", `/api/v1/calendar/${id}`, {
197
+ scheduled_for: scheduledFor
198
+ });
199
+ }
200
+ async cancelCalendarItem(id) {
201
+ return this.request("DELETE", `/api/v1/calendar/${id}`);
202
+ }
203
+ // Queue
204
+ async getQueue() {
205
+ return this.request("GET", "/api/v1/queue");
206
+ }
207
+ async updateQueue(data) {
208
+ return this.request("PUT", "/api/v1/queue", data);
209
+ }
210
+ async getNextSlot() {
211
+ return this.request("GET", "/api/v1/queue/next");
212
+ }
166
213
  // RSS
167
214
  async fetchFeed(url, limit) {
168
215
  const params = { url };
@@ -172,6 +219,16 @@ var BuzzPosterClient = class {
172
219
  async fetchArticle(url) {
173
220
  return this.request("GET", "/api/v1/rss/article", void 0, { url });
174
221
  }
222
+ // Notifications
223
+ async getNotifications(params) {
224
+ return this.request("GET", "/api/v1/notifications", void 0, params);
225
+ }
226
+ async markNotificationRead(id) {
227
+ return this.request("PUT", `/api/v1/notifications/${id}/read`);
228
+ }
229
+ async markAllNotificationsRead() {
230
+ return this.request("PUT", "/api/v1/notifications/read-all");
231
+ }
175
232
  };
176
233
 
177
234
  // src/tools/posts.ts
@@ -179,16 +236,36 @@ import { z } from "zod";
179
236
  function registerPostTools(server2, client2) {
180
237
  server2.tool(
181
238
  "post",
182
- "Create and publish a post to one or more social media platforms. Supports Twitter, Instagram, LinkedIn, and Facebook with full platform-specific options.",
239
+ "Create and publish a post to one or more social media platforms. Supports Twitter, Instagram, LinkedIn, and Facebook with full platform-specific options. Requires confirmation before publishing.",
183
240
  {
184
241
  content: z.string().optional().describe("The text content of the post"),
185
242
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
186
243
  media_urls: z.array(z.string()).optional().describe("Public URLs of media to attach"),
187
244
  platform_specific: z.record(z.record(z.unknown())).optional().describe(
188
245
  "Platform-specific data keyed by platform name. E.g. { twitter: { threadItems: [...] }, instagram: { firstComment: '...' } }"
246
+ ),
247
+ confirmed: z.boolean().optional().describe(
248
+ "Set to true to confirm and publish. If false or missing, returns a preview for user approval."
189
249
  )
190
250
  },
251
+ {
252
+ title: "Publish Post Now",
253
+ readOnlyHint: false,
254
+ destructiveHint: false,
255
+ idempotentHint: false,
256
+ openWorldHint: true
257
+ },
191
258
  async (args) => {
259
+ if (!args.confirmed) {
260
+ const preview = `## Post Preview
261
+
262
+ **Content:** "${args.content ?? "(no text)"}"
263
+ **Platforms:** ${args.platforms.join(", ")}
264
+ ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
265
+ ` : "") + `
266
+ This will be published **immediately**. Call this tool again with confirmed=true to proceed.`;
267
+ return { content: [{ type: "text", text: preview }] };
268
+ }
192
269
  const platforms = args.platforms.map((platform) => {
193
270
  const entry = { platform };
194
271
  if (args.platform_specific?.[platform]) {
@@ -215,10 +292,20 @@ function registerPostTools(server2, client2) {
215
292
  );
216
293
  server2.tool(
217
294
  "cross_post",
218
- "Post the same content to all connected social media platforms at once.",
295
+ "Post the same content to all connected social media platforms at once. Requires confirmation before publishing.",
219
296
  {
220
297
  content: z.string().describe("The text content to post everywhere"),
221
- media_urls: z.array(z.string()).optional().describe("Public URLs of media to attach")
298
+ media_urls: z.array(z.string()).optional().describe("Public URLs of media to attach"),
299
+ confirmed: z.boolean().optional().describe(
300
+ "Set to true to confirm and publish. If false or missing, returns a preview for user approval."
301
+ )
302
+ },
303
+ {
304
+ title: "Cross-Post to All Platforms",
305
+ readOnlyHint: false,
306
+ destructiveHint: false,
307
+ idempotentHint: false,
308
+ openWorldHint: true
222
309
  },
223
310
  async (args) => {
224
311
  const accountsData = await client2.listAccounts();
@@ -235,6 +322,16 @@ function registerPostTools(server2, client2) {
235
322
  ]
236
323
  };
237
324
  }
325
+ if (!args.confirmed) {
326
+ const preview = `## Cross-Post Preview
327
+
328
+ **Content:** "${args.content}"
329
+ **Platforms:** ${platforms.join(", ")}
330
+ ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
331
+ ` : "") + `
332
+ This will be published **immediately** to all connected platforms. Call this tool again with confirmed=true to proceed.`;
333
+ return { content: [{ type: "text", text: preview }] };
334
+ }
238
335
  const body = {
239
336
  content: args.content,
240
337
  platforms: platforms.map((p) => ({ platform: p })),
@@ -254,16 +351,37 @@ function registerPostTools(server2, client2) {
254
351
  );
255
352
  server2.tool(
256
353
  "schedule_post",
257
- "Schedule a post for future publication.",
354
+ "Schedule a post for future publication. Requires confirmation before scheduling.",
258
355
  {
259
356
  content: z.string().optional().describe("The text content of the post"),
260
357
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
261
358
  scheduled_for: z.string().describe("ISO 8601 datetime for when to publish, e.g. 2024-01-16T12:00:00"),
262
359
  timezone: z.string().default("UTC").describe("Timezone for the scheduled time, e.g. America/New_York"),
263
360
  media_urls: z.array(z.string()).optional().describe("Media URLs to attach"),
264
- platform_specific: z.record(z.record(z.unknown())).optional().describe("Platform-specific data keyed by platform name")
361
+ platform_specific: z.record(z.record(z.unknown())).optional().describe("Platform-specific data keyed by platform name"),
362
+ confirmed: z.boolean().optional().describe(
363
+ "Set to true to confirm scheduling. If false or missing, returns a preview for user approval."
364
+ )
365
+ },
366
+ {
367
+ title: "Schedule Social Post",
368
+ readOnlyHint: false,
369
+ destructiveHint: false,
370
+ idempotentHint: false,
371
+ openWorldHint: true
265
372
  },
266
373
  async (args) => {
374
+ if (!args.confirmed) {
375
+ const preview = `## Schedule Preview
376
+
377
+ **Content:** "${args.content ?? "(no text)"}"
378
+ **Platforms:** ${args.platforms.join(", ")}
379
+ **Scheduled for:** ${args.scheduled_for} (${args.timezone})
380
+ ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
381
+ ` : "") + `
382
+ Call this tool again with confirmed=true to schedule.`;
383
+ return { content: [{ type: "text", text: preview }] };
384
+ }
267
385
  const platforms = args.platforms.map((platform) => {
268
386
  const entry = { platform };
269
387
  if (args.platform_specific?.[platform]) {
@@ -297,6 +415,13 @@ function registerPostTools(server2, client2) {
297
415
  platforms: z.array(z.string()).describe("Platforms this draft is intended for"),
298
416
  media_urls: z.array(z.string()).optional().describe("Media URLs to attach")
299
417
  },
418
+ {
419
+ title: "Create Draft Post",
420
+ readOnlyHint: false,
421
+ destructiveHint: false,
422
+ idempotentHint: false,
423
+ openWorldHint: false
424
+ },
300
425
  async (args) => {
301
426
  const body = {
302
427
  content: args.content,
@@ -322,6 +447,13 @@ function registerPostTools(server2, client2) {
322
447
  status: z.string().optional().describe("Filter by status: published, scheduled, draft, failed"),
323
448
  limit: z.string().optional().describe("Number of posts to return")
324
449
  },
450
+ {
451
+ title: "List Posts",
452
+ readOnlyHint: true,
453
+ destructiveHint: false,
454
+ idempotentHint: true,
455
+ openWorldHint: true
456
+ },
325
457
  async (args) => {
326
458
  const params = {};
327
459
  if (args.status) params.status = args.status;
@@ -338,6 +470,13 @@ function registerPostTools(server2, client2) {
338
470
  {
339
471
  post_id: z.string().describe("The ID of the post to retrieve")
340
472
  },
473
+ {
474
+ title: "Get Post Details",
475
+ readOnlyHint: true,
476
+ destructiveHint: false,
477
+ idempotentHint: true,
478
+ openWorldHint: true
479
+ },
341
480
  async (args) => {
342
481
  const result = await client2.getPost(args.post_id);
343
482
  return {
@@ -347,11 +486,48 @@ function registerPostTools(server2, client2) {
347
486
  );
348
487
  server2.tool(
349
488
  "retry_post",
350
- "Retry publishing a post that failed on one or more platforms.",
489
+ "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.",
351
490
  {
352
- post_id: z.string().describe("The ID of the post to retry")
491
+ post_id: z.string().describe("The ID of the post to retry"),
492
+ confirmed: z.boolean().optional().describe(
493
+ "Set to true to confirm retry. If false/missing, shows post details first."
494
+ )
495
+ },
496
+ {
497
+ title: "Retry Failed Post",
498
+ readOnlyHint: false,
499
+ destructiveHint: false,
500
+ idempotentHint: true,
501
+ openWorldHint: true
353
502
  },
354
503
  async (args) => {
504
+ if (!args.confirmed) {
505
+ const post = await client2.getPost(args.post_id);
506
+ const platforms = post.platforms ?? [];
507
+ const failed = platforms.filter((p) => p.status === "failed");
508
+ const published = platforms.filter((p) => p.status === "published");
509
+ let text = `## Retry Post Preview
510
+
511
+ `;
512
+ text += `**Post ID:** ${args.post_id}
513
+ `;
514
+ text += `**Content:** "${(post.content ?? "").substring(0, 100)}"
515
+ `;
516
+ text += `**Status:** ${post.status}
517
+
518
+ `;
519
+ if (published.length > 0) {
520
+ text += `**Already published on:** ${published.map((p) => p.platform).join(", ")}
521
+ `;
522
+ }
523
+ if (failed.length > 0) {
524
+ text += `**Failed on:** ${failed.map((p) => `${p.platform} (${p.error ?? "unknown error"})`).join(", ")}
525
+ `;
526
+ }
527
+ text += `
528
+ Retrying will only attempt the failed platforms. Call this tool again with confirmed=true to proceed.`;
529
+ return { content: [{ type: "text", text }] };
530
+ }
355
531
  const result = await client2.retryPost(args.post_id);
356
532
  return {
357
533
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
@@ -366,6 +542,13 @@ function registerAccountTools(server2, client2) {
366
542
  "list_accounts",
367
543
  "List all connected social media accounts with their platform, username, and connection status.",
368
544
  {},
545
+ {
546
+ title: "List Connected Accounts",
547
+ readOnlyHint: true,
548
+ destructiveHint: false,
549
+ idempotentHint: true,
550
+ openWorldHint: false
551
+ },
369
552
  async () => {
370
553
  const result = await client2.listAccounts();
371
554
  return {
@@ -375,12 +558,49 @@ function registerAccountTools(server2, client2) {
375
558
  );
376
559
  server2.tool(
377
560
  "check_accounts_health",
378
- "Check the health and connection status of all connected social media accounts.",
561
+ "Check the health status of all connected social media accounts. Shows which accounts are working, which have warnings, and which need reconnection. Call this before scheduling posts to make sure target platforms are healthy.",
379
562
  {},
563
+ {
564
+ title: "Check Account Health",
565
+ readOnlyHint: true,
566
+ destructiveHint: false,
567
+ idempotentHint: true,
568
+ openWorldHint: true
569
+ },
380
570
  async () => {
381
571
  const result = await client2.checkAccountsHealth();
572
+ const accounts = result.accounts ?? [];
573
+ const summary = result.summary;
574
+ if (accounts.length === 0) {
575
+ return {
576
+ content: [{
577
+ type: "text",
578
+ text: "## Account Health\n\nNo connected accounts found. Connect accounts first via the dashboard."
579
+ }]
580
+ };
581
+ }
582
+ const statusIcon = {
583
+ healthy: "\u2705",
584
+ warning: "\u26A0\uFE0F",
585
+ error: "\u274C"
586
+ };
587
+ const lines = accounts.map((a) => {
588
+ const icon = statusIcon[a.status] ?? "\u2753";
589
+ const name = a.username ? `@${a.username}` : a.platform;
590
+ const platform = a.platform.charAt(0).toUpperCase() + a.platform.slice(1);
591
+ const msg = a.message || (a.status === "healthy" ? "Healthy" : a.status);
592
+ return `${icon} **${platform}** (${name}) \u2014 ${msg}`;
593
+ });
594
+ let text = `## Account Health
595
+
596
+ ${lines.join("\n")}`;
597
+ if (summary) {
598
+ text += `
599
+
600
+ **Summary:** ${summary.total} total, ${summary.healthy} healthy, ${summary.warning} warning, ${summary.error} error`;
601
+ }
382
602
  return {
383
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
603
+ content: [{ type: "text", text }]
384
604
  };
385
605
  }
386
606
  );
@@ -397,6 +617,13 @@ function registerAnalyticsTools(server2, client2) {
397
617
  from_date: z2.string().optional().describe("Start date for analytics range (ISO 8601)"),
398
618
  to_date: z2.string().optional().describe("End date for analytics range (ISO 8601)")
399
619
  },
620
+ {
621
+ title: "Get Analytics",
622
+ readOnlyHint: true,
623
+ destructiveHint: false,
624
+ idempotentHint: true,
625
+ openWorldHint: true
626
+ },
400
627
  async (args) => {
401
628
  const params = {};
402
629
  if (args.platform) params.platform = args.platform;
@@ -419,6 +646,13 @@ function registerInboxTools(server2, client2) {
419
646
  {
420
647
  platform: z3.string().optional().describe("Filter by platform: twitter, instagram, linkedin, facebook")
421
648
  },
649
+ {
650
+ title: "List Conversations",
651
+ readOnlyHint: true,
652
+ destructiveHint: false,
653
+ idempotentHint: true,
654
+ openWorldHint: true
655
+ },
422
656
  async (args) => {
423
657
  const params = {};
424
658
  if (args.platform) params.platform = args.platform;
@@ -434,6 +668,13 @@ function registerInboxTools(server2, client2) {
434
668
  {
435
669
  conversation_id: z3.string().describe("The conversation ID")
436
670
  },
671
+ {
672
+ title: "Get Conversation Messages",
673
+ readOnlyHint: true,
674
+ destructiveHint: false,
675
+ idempotentHint: true,
676
+ openWorldHint: true
677
+ },
437
678
  async (args) => {
438
679
  const result = await client2.getConversation(args.conversation_id);
439
680
  return {
@@ -443,12 +684,29 @@ function registerInboxTools(server2, client2) {
443
684
  );
444
685
  server2.tool(
445
686
  "reply_to_conversation",
446
- "Send a reply message in a DM conversation.",
687
+ "Send a reply message in a DM conversation. Sends a message to an external platform on your behalf.",
447
688
  {
448
689
  conversation_id: z3.string().describe("The conversation ID to reply to"),
449
- message: z3.string().describe("The reply message text")
690
+ message: z3.string().describe("The reply message text"),
691
+ confirmed: z3.boolean().optional().describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
692
+ },
693
+ {
694
+ title: "Reply to Conversation",
695
+ readOnlyHint: false,
696
+ destructiveHint: false,
697
+ idempotentHint: false,
698
+ openWorldHint: true
450
699
  },
451
700
  async (args) => {
701
+ if (!args.confirmed) {
702
+ const preview = `## Reply Preview
703
+
704
+ **Conversation:** ${args.conversation_id}
705
+ **Message:** "${args.message}"
706
+
707
+ This will send a DM reply on the external platform. Call this tool again with confirmed=true to send.`;
708
+ return { content: [{ type: "text", text: preview }] };
709
+ }
452
710
  const result = await client2.replyToConversation(
453
711
  args.conversation_id,
454
712
  args.message
@@ -464,6 +722,13 @@ function registerInboxTools(server2, client2) {
464
722
  {
465
723
  post_id: z3.string().optional().describe("Filter comments by post ID")
466
724
  },
725
+ {
726
+ title: "List Comments",
727
+ readOnlyHint: true,
728
+ destructiveHint: false,
729
+ idempotentHint: true,
730
+ openWorldHint: true
731
+ },
467
732
  async (args) => {
468
733
  const params = {};
469
734
  if (args.post_id) params.postId = args.post_id;
@@ -475,12 +740,29 @@ function registerInboxTools(server2, client2) {
475
740
  );
476
741
  server2.tool(
477
742
  "reply_to_comment",
478
- "Reply to a comment on one of your posts.",
743
+ "Reply to a comment on one of your posts. Sends a reply on the external platform.",
479
744
  {
480
745
  comment_id: z3.string().describe("The comment ID to reply to"),
481
- message: z3.string().describe("The reply text")
746
+ message: z3.string().describe("The reply text"),
747
+ confirmed: z3.boolean().optional().describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
748
+ },
749
+ {
750
+ title: "Reply to Comment",
751
+ readOnlyHint: false,
752
+ destructiveHint: false,
753
+ idempotentHint: false,
754
+ openWorldHint: true
482
755
  },
483
756
  async (args) => {
757
+ if (!args.confirmed) {
758
+ const preview = `## Comment Reply Preview
759
+
760
+ **Comment ID:** ${args.comment_id}
761
+ **Reply:** "${args.message}"
762
+
763
+ This will post a public reply. Call this tool again with confirmed=true to send.`;
764
+ return { content: [{ type: "text", text: preview }] };
765
+ }
484
766
  const result = await client2.replyToComment(
485
767
  args.comment_id,
486
768
  args.message
@@ -494,6 +776,13 @@ function registerInboxTools(server2, client2) {
494
776
  "list_reviews",
495
777
  "List reviews on your Facebook page.",
496
778
  {},
779
+ {
780
+ title: "List Reviews",
781
+ readOnlyHint: true,
782
+ destructiveHint: false,
783
+ idempotentHint: true,
784
+ openWorldHint: true
785
+ },
497
786
  async () => {
498
787
  const result = await client2.listReviews();
499
788
  return {
@@ -503,12 +792,29 @@ function registerInboxTools(server2, client2) {
503
792
  );
504
793
  server2.tool(
505
794
  "reply_to_review",
506
- "Reply to a review on your Facebook page.",
795
+ "Reply to a review on your Facebook page. Sends a public reply on the external platform.",
507
796
  {
508
797
  review_id: z3.string().describe("The review ID to reply to"),
509
- message: z3.string().describe("The reply text")
798
+ message: z3.string().describe("The reply text"),
799
+ confirmed: z3.boolean().optional().describe("Set to true to confirm and send. If false or missing, returns a preview for user approval.")
800
+ },
801
+ {
802
+ title: "Reply to Review",
803
+ readOnlyHint: false,
804
+ destructiveHint: false,
805
+ idempotentHint: false,
806
+ openWorldHint: true
510
807
  },
511
808
  async (args) => {
809
+ if (!args.confirmed) {
810
+ const preview = `## Review Reply Preview
811
+
812
+ **Review ID:** ${args.review_id}
813
+ **Reply:** "${args.message}"
814
+
815
+ This will post a public reply to the review. Call this tool again with confirmed=true to send.`;
816
+ return { content: [{ type: "text", text: preview }] };
817
+ }
512
818
  const result = await client2.replyToReview(
513
819
  args.review_id,
514
820
  args.message
@@ -530,6 +836,13 @@ function registerMediaTools(server2, client2) {
530
836
  {
531
837
  file_path: z4.string().describe("Absolute path to the file on the local filesystem")
532
838
  },
839
+ {
840
+ title: "Upload Media File",
841
+ readOnlyHint: false,
842
+ destructiveHint: false,
843
+ idempotentHint: false,
844
+ openWorldHint: false
845
+ },
533
846
  async (args) => {
534
847
  const buffer = await readFile(args.file_path);
535
848
  const base64 = buffer.toString("base64");
@@ -562,6 +875,13 @@ function registerMediaTools(server2, client2) {
562
875
  data: z4.string().describe("Base64-encoded file data"),
563
876
  mime_type: z4.string().describe("MIME type of the file, e.g. image/jpeg, video/mp4")
564
877
  },
878
+ {
879
+ title: "Upload Media Base64",
880
+ readOnlyHint: false,
881
+ destructiveHint: false,
882
+ idempotentHint: false,
883
+ openWorldHint: false
884
+ },
565
885
  async (args) => {
566
886
  const result = await client2.uploadMedia(
567
887
  args.filename,
@@ -577,6 +897,13 @@ function registerMediaTools(server2, client2) {
577
897
  "list_media",
578
898
  "List all uploaded media files in your BuzzPoster media library.",
579
899
  {},
900
+ {
901
+ title: "List Media Library",
902
+ readOnlyHint: true,
903
+ destructiveHint: false,
904
+ idempotentHint: true,
905
+ openWorldHint: false
906
+ },
580
907
  async () => {
581
908
  const result = await client2.listMedia();
582
909
  return {
@@ -586,11 +913,27 @@ function registerMediaTools(server2, client2) {
586
913
  );
587
914
  server2.tool(
588
915
  "delete_media",
589
- "Delete a media file from your BuzzPoster media library.",
916
+ "Delete a media file from your BuzzPoster media library. This cannot be undone.",
590
917
  {
591
- key: z4.string().describe("The key/path of the media file to delete")
918
+ key: z4.string().describe("The key/path of the media file to delete"),
919
+ confirmed: z4.boolean().optional().describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
920
+ },
921
+ {
922
+ title: "Delete Media File",
923
+ readOnlyHint: false,
924
+ destructiveHint: true,
925
+ idempotentHint: true,
926
+ openWorldHint: false
592
927
  },
593
928
  async (args) => {
929
+ if (!args.confirmed) {
930
+ const preview = `## Delete Media Confirmation
931
+
932
+ **File:** ${args.key}
933
+
934
+ This will permanently delete this media file. Call this tool again with confirmed=true to proceed.`;
935
+ return { content: [{ type: "text", text: preview }] };
936
+ }
594
937
  await client2.deleteMedia(args.key);
595
938
  return {
596
939
  content: [{ type: "text", text: "Media deleted successfully." }]
@@ -609,6 +952,13 @@ function registerNewsletterTools(server2, client2) {
609
952
  page: z5.string().optional().describe("Page number for pagination"),
610
953
  per_page: z5.string().optional().describe("Number of subscribers per page")
611
954
  },
955
+ {
956
+ title: "List Subscribers",
957
+ readOnlyHint: true,
958
+ destructiveHint: false,
959
+ idempotentHint: true,
960
+ openWorldHint: true
961
+ },
612
962
  async (args) => {
613
963
  const params = {};
614
964
  if (args.page) params.page = args.page;
@@ -627,6 +977,13 @@ function registerNewsletterTools(server2, client2) {
627
977
  first_name: z5.string().optional().describe("Subscriber's first name"),
628
978
  last_name: z5.string().optional().describe("Subscriber's last name")
629
979
  },
980
+ {
981
+ title: "Add Subscriber",
982
+ readOnlyHint: false,
983
+ destructiveHint: false,
984
+ idempotentHint: false,
985
+ openWorldHint: true
986
+ },
630
987
  async (args) => {
631
988
  const result = await client2.addSubscriber({
632
989
  email: args.email,
@@ -646,6 +1003,13 @@ function registerNewsletterTools(server2, client2) {
646
1003
  content: z5.string().describe("HTML content of the newsletter"),
647
1004
  preview_text: z5.string().optional().describe("Preview text shown in email clients")
648
1005
  },
1006
+ {
1007
+ title: "Create Newsletter Draft",
1008
+ readOnlyHint: false,
1009
+ destructiveHint: false,
1010
+ idempotentHint: false,
1011
+ openWorldHint: true
1012
+ },
649
1013
  async (args) => {
650
1014
  const result = await client2.createBroadcast({
651
1015
  subject: args.subject,
@@ -666,6 +1030,13 @@ function registerNewsletterTools(server2, client2) {
666
1030
  content: z5.string().optional().describe("Updated HTML content"),
667
1031
  preview_text: z5.string().optional().describe("Updated preview text")
668
1032
  },
1033
+ {
1034
+ title: "Update Newsletter Draft",
1035
+ readOnlyHint: false,
1036
+ destructiveHint: false,
1037
+ idempotentHint: true,
1038
+ openWorldHint: true
1039
+ },
669
1040
  async (args) => {
670
1041
  const data = {};
671
1042
  if (args.subject) data.subject = args.subject;
@@ -679,11 +1050,31 @@ function registerNewsletterTools(server2, client2) {
679
1050
  );
680
1051
  server2.tool(
681
1052
  "send_newsletter",
682
- "Send a newsletter/broadcast to subscribers. This action cannot be undone.",
1053
+ "Send a newsletter/broadcast to subscribers. This action cannot be undone. Requires confirmation before sending.",
1054
+ {
1055
+ broadcast_id: z5.string().describe("The broadcast/newsletter ID to send"),
1056
+ confirmed: z5.boolean().optional().describe(
1057
+ "Set to true to confirm and send. If false or missing, returns a confirmation prompt."
1058
+ )
1059
+ },
683
1060
  {
684
- broadcast_id: z5.string().describe("The broadcast/newsletter ID to send")
1061
+ title: "Send Newsletter",
1062
+ readOnlyHint: false,
1063
+ destructiveHint: false,
1064
+ idempotentHint: false,
1065
+ openWorldHint: true
685
1066
  },
686
1067
  async (args) => {
1068
+ if (!args.confirmed) {
1069
+ const preview = `## Send Newsletter Confirmation
1070
+
1071
+ **Broadcast ID:** ${args.broadcast_id}
1072
+
1073
+ This will send the newsletter to your subscriber list. **This action cannot be undone.**
1074
+
1075
+ Call this tool again with confirmed=true to send.`;
1076
+ return { content: [{ type: "text", text: preview }] };
1077
+ }
687
1078
  await client2.sendBroadcast(args.broadcast_id);
688
1079
  return {
689
1080
  content: [{ type: "text", text: "Newsletter sent successfully." }]
@@ -696,6 +1087,13 @@ function registerNewsletterTools(server2, client2) {
696
1087
  {
697
1088
  page: z5.string().optional().describe("Page number for pagination")
698
1089
  },
1090
+ {
1091
+ title: "List Newsletters",
1092
+ readOnlyHint: true,
1093
+ destructiveHint: false,
1094
+ idempotentHint: true,
1095
+ openWorldHint: true
1096
+ },
699
1097
  async (args) => {
700
1098
  const params = {};
701
1099
  if (args.page) params.page = args.page;
@@ -709,6 +1107,13 @@ function registerNewsletterTools(server2, client2) {
709
1107
  "list_tags",
710
1108
  "List all subscriber tags from your email service provider.",
711
1109
  {},
1110
+ {
1111
+ title: "List Subscriber Tags",
1112
+ readOnlyHint: true,
1113
+ destructiveHint: false,
1114
+ idempotentHint: true,
1115
+ openWorldHint: true
1116
+ },
712
1117
  async () => {
713
1118
  const result = await client2.listTags();
714
1119
  return {
@@ -720,6 +1125,13 @@ function registerNewsletterTools(server2, client2) {
720
1125
  "list_sequences",
721
1126
  "List all email sequences/automations from your email service provider.",
722
1127
  {},
1128
+ {
1129
+ title: "List Email Sequences",
1130
+ readOnlyHint: true,
1131
+ destructiveHint: false,
1132
+ idempotentHint: true,
1133
+ openWorldHint: true
1134
+ },
723
1135
  async () => {
724
1136
  const result = await client2.listSequences();
725
1137
  return {
@@ -731,6 +1143,13 @@ function registerNewsletterTools(server2, client2) {
731
1143
  "list_forms",
732
1144
  "List all signup forms from your email service provider.",
733
1145
  {},
1146
+ {
1147
+ title: "List Signup Forms",
1148
+ readOnlyHint: true,
1149
+ destructiveHint: false,
1150
+ idempotentHint: true,
1151
+ openWorldHint: true
1152
+ },
734
1153
  async () => {
735
1154
  const result = await client2.listForms();
736
1155
  return {
@@ -750,6 +1169,13 @@ function registerRssTools(server2, client2) {
750
1169
  url: z6.string().describe("The RSS/Atom feed URL to fetch"),
751
1170
  limit: z6.number().optional().describe("Maximum number of entries to return (default 10, max 100)")
752
1171
  },
1172
+ {
1173
+ title: "Fetch RSS Feed",
1174
+ readOnlyHint: true,
1175
+ destructiveHint: false,
1176
+ idempotentHint: true,
1177
+ openWorldHint: true
1178
+ },
753
1179
  async (args) => {
754
1180
  const result = await client2.fetchFeed(args.url, args.limit);
755
1181
  return {
@@ -763,6 +1189,13 @@ function registerRssTools(server2, client2) {
763
1189
  {
764
1190
  url: z6.string().describe("The article URL to extract content from")
765
1191
  },
1192
+ {
1193
+ title: "Fetch Article Content",
1194
+ readOnlyHint: true,
1195
+ destructiveHint: false,
1196
+ idempotentHint: true,
1197
+ openWorldHint: true
1198
+ },
766
1199
  async (args) => {
767
1200
  const result = await client2.fetchArticle(args.url);
768
1201
  return {
@@ -778,6 +1211,13 @@ function registerAccountInfoTool(server2, client2) {
778
1211
  "get_account",
779
1212
  "Get your BuzzPoster account details including name, email, subscription status, ESP configuration, and connected platforms.",
780
1213
  {},
1214
+ {
1215
+ title: "Get Account Details",
1216
+ readOnlyHint: true,
1217
+ destructiveHint: false,
1218
+ idempotentHint: true,
1219
+ openWorldHint: false
1220
+ },
781
1221
  async () => {
782
1222
  const result = await client2.getAccount();
783
1223
  return {
@@ -793,6 +1233,13 @@ function registerBrandVoiceTools(server2, client2) {
793
1233
  "get_brand_voice",
794
1234
  "Get the customer's brand voice profile and writing rules. IMPORTANT: Call this tool BEFORE creating any post, draft, or content. Use the returned voice description, rules, and examples to match the customer's tone and style in everything you write for them.",
795
1235
  {},
1236
+ {
1237
+ title: "Get Brand Voice",
1238
+ readOnlyHint: true,
1239
+ destructiveHint: false,
1240
+ idempotentHint: true,
1241
+ openWorldHint: false
1242
+ },
796
1243
  async () => {
797
1244
  try {
798
1245
  const voice = await client2.getBrandVoice();
@@ -860,6 +1307,13 @@ function registerKnowledgeTools(server2, client2) {
860
1307
  "get_knowledge_base",
861
1308
  "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.",
862
1309
  {},
1310
+ {
1311
+ title: "Get Knowledge Base",
1312
+ readOnlyHint: true,
1313
+ destructiveHint: false,
1314
+ idempotentHint: true,
1315
+ openWorldHint: false
1316
+ },
863
1317
  async () => {
864
1318
  try {
865
1319
  const data = await client2.listKnowledge();
@@ -911,6 +1365,13 @@ function registerKnowledgeTools(server2, client2) {
911
1365
  "Search query - matches against tags first, then falls back to text search on title and content"
912
1366
  )
913
1367
  },
1368
+ {
1369
+ title: "Search Knowledge Base",
1370
+ readOnlyHint: true,
1371
+ destructiveHint: false,
1372
+ idempotentHint: true,
1373
+ openWorldHint: false
1374
+ },
914
1375
  async ({ query }) => {
915
1376
  try {
916
1377
  const data = await client2.listKnowledge({ tag: query });
@@ -955,6 +1416,13 @@ function registerKnowledgeTools(server2, client2) {
955
1416
  content: z7.string().describe("The content/text to save"),
956
1417
  tags: z7.array(z7.string()).optional().describe("Optional tags for categorization")
957
1418
  },
1419
+ {
1420
+ title: "Add Knowledge Item",
1421
+ readOnlyHint: false,
1422
+ destructiveHint: false,
1423
+ idempotentHint: false,
1424
+ openWorldHint: false
1425
+ },
958
1426
  async ({ title, content, tags }) => {
959
1427
  try {
960
1428
  await client2.createKnowledge({
@@ -979,6 +1447,674 @@ function registerKnowledgeTools(server2, client2) {
979
1447
  );
980
1448
  }
981
1449
 
1450
+ // src/tools/audience.ts
1451
+ import { z as z8 } from "zod";
1452
+ function registerAudienceTools(server2, client2) {
1453
+ server2.tool(
1454
+ "get_audience",
1455
+ "Get the target audience profile for content creation. Use this tool to understand WHO the content is for \u2014 their demographics, pain points, motivations, preferred platforms, and tone preferences. Call this BEFORE writing any post or content so you can tailor messaging to resonate with the intended audience.",
1456
+ {
1457
+ audienceId: z8.string().optional().describe("Specific audience profile ID. If omitted, returns the default audience.")
1458
+ },
1459
+ {
1460
+ title: "Get Audience Profile",
1461
+ readOnlyHint: true,
1462
+ destructiveHint: false,
1463
+ idempotentHint: true,
1464
+ openWorldHint: false
1465
+ },
1466
+ async ({ audienceId }) => {
1467
+ try {
1468
+ const audience = audienceId ? await client2.getAudience(audienceId) : await client2.getDefaultAudience();
1469
+ const lines = [];
1470
+ lines.push(`## Target Audience: ${audience.name}`);
1471
+ lines.push("");
1472
+ if (audience.description) {
1473
+ lines.push("### Description");
1474
+ lines.push(audience.description);
1475
+ lines.push("");
1476
+ }
1477
+ if (audience.demographics) {
1478
+ lines.push("### Demographics");
1479
+ lines.push(audience.demographics);
1480
+ lines.push("");
1481
+ }
1482
+ if (audience.painPoints && audience.painPoints.length > 0) {
1483
+ lines.push("### Pain Points");
1484
+ for (const point of audience.painPoints) {
1485
+ lines.push(`- ${point}`);
1486
+ }
1487
+ lines.push("");
1488
+ }
1489
+ if (audience.motivations && audience.motivations.length > 0) {
1490
+ lines.push("### Motivations");
1491
+ for (const motivation of audience.motivations) {
1492
+ lines.push(`- ${motivation}`);
1493
+ }
1494
+ lines.push("");
1495
+ }
1496
+ if (audience.platforms && audience.platforms.length > 0) {
1497
+ lines.push("### Active Platforms");
1498
+ lines.push(audience.platforms.join(", "));
1499
+ lines.push("");
1500
+ }
1501
+ if (audience.toneNotes) {
1502
+ lines.push("### Tone Notes");
1503
+ lines.push(audience.toneNotes);
1504
+ lines.push("");
1505
+ }
1506
+ if (audience.contentPreferences) {
1507
+ lines.push("### Content Preferences");
1508
+ lines.push(audience.contentPreferences);
1509
+ lines.push("");
1510
+ }
1511
+ return {
1512
+ content: [{ type: "text", text: lines.join("\n") }]
1513
+ };
1514
+ } catch (error) {
1515
+ const message = error instanceof Error ? error.message : "Unknown error";
1516
+ if (message.includes("404") || message.includes("No audience")) {
1517
+ return {
1518
+ content: [
1519
+ {
1520
+ type: "text",
1521
+ text: "No audience profile has been configured yet. The customer can set one up at their BuzzPoster dashboard under Audiences."
1522
+ }
1523
+ ]
1524
+ };
1525
+ }
1526
+ throw error;
1527
+ }
1528
+ }
1529
+ );
1530
+ }
1531
+
1532
+ // src/tools/newsletter-template.ts
1533
+ import { z as z9 } from "zod";
1534
+ function registerNewsletterTemplateTools(server2, client2) {
1535
+ server2.tool(
1536
+ "get_newsletter_template",
1537
+ "Get the customer's newsletter template -- the blueprint for how their newsletter is structured. IMPORTANT: Call this BEFORE writing any newsletter. The template defines the exact section order, what each section should contain, word counts, content sources, and styling. Follow the template structure precisely when generating newsletter content. If no template_id is specified, returns the default template.",
1538
+ {
1539
+ templateId: z9.string().optional().describe(
1540
+ "Specific template ID. If omitted, returns the default template."
1541
+ )
1542
+ },
1543
+ {
1544
+ title: "Get Newsletter Template",
1545
+ readOnlyHint: true,
1546
+ destructiveHint: false,
1547
+ idempotentHint: true,
1548
+ openWorldHint: false
1549
+ },
1550
+ async ({ templateId }) => {
1551
+ try {
1552
+ const template = await client2.getTemplate(templateId);
1553
+ const lines = [];
1554
+ lines.push(`## Newsletter Template: ${template.name}`);
1555
+ if (template.description) lines.push(template.description);
1556
+ lines.push("");
1557
+ if (template.audienceId)
1558
+ lines.push(`Audience ID: ${template.audienceId}`);
1559
+ if (template.sendCadence)
1560
+ lines.push(`Cadence: ${template.sendCadence}`);
1561
+ if (template.subjectPattern)
1562
+ lines.push(`Subject pattern: ${template.subjectPattern}`);
1563
+ if (template.previewTextPattern)
1564
+ lines.push(`Preview text pattern: ${template.previewTextPattern}`);
1565
+ lines.push("");
1566
+ if (template.sections && template.sections.length > 0) {
1567
+ lines.push("### Sections (in order):");
1568
+ lines.push("");
1569
+ for (let i = 0; i < template.sections.length; i++) {
1570
+ const s = template.sections[i];
1571
+ lines.push(`**${i + 1}. ${s.label}** (${s.type})`);
1572
+ if (s.instructions)
1573
+ lines.push(`Instructions: ${s.instructions}`);
1574
+ if (s.word_count)
1575
+ lines.push(
1576
+ `Word count: ${s.word_count.min}-${s.word_count.max} words`
1577
+ );
1578
+ if (s.tone_override) lines.push(`Tone: ${s.tone_override}`);
1579
+ if (s.content_source) {
1580
+ const src = s.content_source;
1581
+ if (src.type === "rss" && src.feed_urls?.length)
1582
+ lines.push(`Content source: RSS - ${src.feed_urls.join(", ")}`);
1583
+ else if (src.type === "knowledge" && src.knowledge_item_ids?.length)
1584
+ lines.push(
1585
+ `Content source: Knowledge items ${src.knowledge_item_ids.join(", ")}`
1586
+ );
1587
+ else lines.push(`Content source: ${src.type}`);
1588
+ }
1589
+ if (s.count) lines.push(`Count: ${s.count} items`);
1590
+ lines.push(
1591
+ `Required: ${s.required !== false ? "yes" : "no"}`
1592
+ );
1593
+ if (s.type === "cta") {
1594
+ if (s.button_text) lines.push(`Button text: ${s.button_text}`);
1595
+ if (s.button_url) lines.push(`Button URL: ${s.button_url}`);
1596
+ }
1597
+ if (s.type === "sponsor") {
1598
+ if (s.sponsor_name)
1599
+ lines.push(`Sponsor: ${s.sponsor_name}`);
1600
+ if (s.talking_points)
1601
+ lines.push(`Talking points: ${s.talking_points}`);
1602
+ }
1603
+ if (s.type === "header") {
1604
+ if (s.tagline) lines.push(`Tagline: ${s.tagline}`);
1605
+ }
1606
+ if (s.type === "signoff") {
1607
+ if (s.author_name)
1608
+ lines.push(`Author: ${s.author_name}`);
1609
+ if (s.author_title)
1610
+ lines.push(`Title: ${s.author_title}`);
1611
+ }
1612
+ lines.push("");
1613
+ }
1614
+ }
1615
+ if (template.style) {
1616
+ lines.push("### Style Guide:");
1617
+ const st = template.style;
1618
+ if (st.primary_color) lines.push(`Primary color: ${st.primary_color}`);
1619
+ if (st.accent_color) lines.push(`Accent color: ${st.accent_color}`);
1620
+ if (st.font_family) lines.push(`Font: ${st.font_family}`);
1621
+ if (st.heading_font) lines.push(`Heading font: ${st.heading_font}`);
1622
+ if (st.content_width)
1623
+ lines.push(`Content width: ${st.content_width}px`);
1624
+ if (st.text_color) lines.push(`Text color: ${st.text_color}`);
1625
+ if (st.link_color) lines.push(`Link color: ${st.link_color}`);
1626
+ if (st.background_color)
1627
+ lines.push(`Background: ${st.background_color}`);
1628
+ if (st.button_style)
1629
+ lines.push(
1630
+ `Button style: ${st.button_style.color} text on ${st.button_style.text_color}, ${st.button_style.shape}`
1631
+ );
1632
+ lines.push("");
1633
+ lines.push(
1634
+ "NOTE: Apply these style values as inline CSS when generating the newsletter HTML."
1635
+ );
1636
+ lines.push("");
1637
+ }
1638
+ if (template.rssFeedUrls && template.rssFeedUrls.length > 0) {
1639
+ lines.push("### Linked RSS Feeds:");
1640
+ for (const url of template.rssFeedUrls) {
1641
+ lines.push(`- ${url}`);
1642
+ }
1643
+ lines.push("");
1644
+ }
1645
+ if (template.knowledgeItemIds && template.knowledgeItemIds.length > 0) {
1646
+ lines.push("### Reference Material:");
1647
+ lines.push(
1648
+ `Knowledge item IDs: ${template.knowledgeItemIds.join(", ")}`
1649
+ );
1650
+ lines.push("");
1651
+ }
1652
+ return {
1653
+ content: [{ type: "text", text: lines.join("\n") }]
1654
+ };
1655
+ } catch (error) {
1656
+ const message = error instanceof Error ? error.message : "Unknown error";
1657
+ if (message.includes("404") || message.includes("No newsletter templates")) {
1658
+ return {
1659
+ content: [
1660
+ {
1661
+ type: "text",
1662
+ text: "No newsletter template has been configured yet. The customer can set one up at their BuzzPoster dashboard under Templates."
1663
+ }
1664
+ ]
1665
+ };
1666
+ }
1667
+ throw error;
1668
+ }
1669
+ }
1670
+ );
1671
+ server2.tool(
1672
+ "get_past_newsletters",
1673
+ "Retrieve the customer's past newsletters for reference. Use this to maintain continuity, match previous formatting, avoid repeating topics, and reference previous editions. Call this when writing a new newsletter to see what was covered recently.",
1674
+ {
1675
+ limit: z9.number().optional().describe("Number of past newsletters to retrieve. Default 5, max 50."),
1676
+ templateId: z9.string().optional().describe("Filter by template ID to see past newsletters from a specific template.")
1677
+ },
1678
+ {
1679
+ title: "Get Past Newsletters",
1680
+ readOnlyHint: true,
1681
+ destructiveHint: false,
1682
+ idempotentHint: true,
1683
+ openWorldHint: false
1684
+ },
1685
+ async ({ limit, templateId }) => {
1686
+ try {
1687
+ const params = {};
1688
+ if (limit) params.limit = String(Math.min(limit, 50));
1689
+ else params.limit = "5";
1690
+ if (templateId) params.template_id = templateId;
1691
+ const newsletters = await client2.listNewsletterArchive(
1692
+ params
1693
+ );
1694
+ if (!newsletters || newsletters.length === 0) {
1695
+ return {
1696
+ content: [
1697
+ {
1698
+ type: "text",
1699
+ text: "No past newsletters found. This will be the first edition!"
1700
+ }
1701
+ ]
1702
+ };
1703
+ }
1704
+ const lines = [];
1705
+ lines.push(
1706
+ `## Past Newsletters (${newsletters.length} most recent)`
1707
+ );
1708
+ lines.push("");
1709
+ for (const nl of newsletters) {
1710
+ lines.push(`### ${nl.subject} -- ${nl.sentAt || nl.createdAt}`);
1711
+ if (nl.notes) lines.push(`Notes: ${nl.notes}`);
1712
+ if (nl.metrics) {
1713
+ const m = nl.metrics;
1714
+ const parts = [];
1715
+ if (m.open_rate) parts.push(`Open rate: ${m.open_rate}`);
1716
+ if (m.click_rate) parts.push(`Click rate: ${m.click_rate}`);
1717
+ if (parts.length) lines.push(`Metrics: ${parts.join(", ")}`);
1718
+ }
1719
+ lines.push("---");
1720
+ const textContent = nl.contentHtml ? nl.contentHtml.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 500) : "(no content)";
1721
+ lines.push(textContent);
1722
+ lines.push(
1723
+ `[Full content available via get_past_newsletter tool with ID: ${nl.id}]`
1724
+ );
1725
+ lines.push("");
1726
+ }
1727
+ return {
1728
+ content: [{ type: "text", text: lines.join("\n") }]
1729
+ };
1730
+ } catch (error) {
1731
+ const message = error instanceof Error ? error.message : "Unknown error";
1732
+ throw new Error(`Failed to fetch past newsletters: ${message}`);
1733
+ }
1734
+ }
1735
+ );
1736
+ server2.tool(
1737
+ "get_past_newsletter",
1738
+ "Get the full content of a specific past newsletter by ID. Use when you need to reference the complete text of a previous edition, check exact formatting, or continue a series.",
1739
+ {
1740
+ newsletterId: z9.string().describe("The ID of the archived newsletter to retrieve.")
1741
+ },
1742
+ {
1743
+ title: "Get Past Newsletter Detail",
1744
+ readOnlyHint: true,
1745
+ destructiveHint: false,
1746
+ idempotentHint: true,
1747
+ openWorldHint: false
1748
+ },
1749
+ async ({ newsletterId }) => {
1750
+ try {
1751
+ const newsletter = await client2.getArchivedNewsletter(
1752
+ newsletterId
1753
+ );
1754
+ const lines = [];
1755
+ lines.push(`## ${newsletter.subject}`);
1756
+ if (newsletter.sentAt) lines.push(`Sent: ${newsletter.sentAt}`);
1757
+ if (newsletter.notes) lines.push(`Notes: ${newsletter.notes}`);
1758
+ lines.push("");
1759
+ lines.push("### Full HTML Content:");
1760
+ lines.push(newsletter.contentHtml);
1761
+ return {
1762
+ content: [{ type: "text", text: lines.join("\n") }]
1763
+ };
1764
+ } catch (error) {
1765
+ const message = error instanceof Error ? error.message : "Unknown error";
1766
+ if (message.includes("404")) {
1767
+ return {
1768
+ content: [
1769
+ {
1770
+ type: "text",
1771
+ text: "Newsletter not found. Check the ID and try again."
1772
+ }
1773
+ ]
1774
+ };
1775
+ }
1776
+ throw error;
1777
+ }
1778
+ }
1779
+ );
1780
+ server2.tool(
1781
+ "save_newsletter",
1782
+ "Save a generated newsletter to the archive. Call this after the customer approves a newsletter you've written, so it's stored for future reference. Include the subject line and full HTML content.",
1783
+ {
1784
+ subject: z9.string().describe("The newsletter subject line."),
1785
+ contentHtml: z9.string().describe("The full HTML content of the newsletter."),
1786
+ templateId: z9.string().optional().describe("The template ID used to generate this newsletter."),
1787
+ notes: z9.string().optional().describe(
1788
+ "Optional notes about this newsletter (what worked, theme, etc)."
1789
+ )
1790
+ },
1791
+ {
1792
+ title: "Save Newsletter to Archive",
1793
+ readOnlyHint: false,
1794
+ destructiveHint: false,
1795
+ idempotentHint: false,
1796
+ openWorldHint: false
1797
+ },
1798
+ async ({ subject, contentHtml, templateId, notes }) => {
1799
+ try {
1800
+ const data = {
1801
+ subject,
1802
+ contentHtml
1803
+ };
1804
+ if (templateId) data.templateId = Number(templateId);
1805
+ if (notes) data.notes = notes;
1806
+ await client2.saveNewsletterToArchive(data);
1807
+ return {
1808
+ content: [
1809
+ {
1810
+ type: "text",
1811
+ text: `Newsletter "${subject}" has been saved to the archive successfully.`
1812
+ }
1813
+ ]
1814
+ };
1815
+ } catch (error) {
1816
+ const message = error instanceof Error ? error.message : "Unknown error";
1817
+ throw new Error(`Failed to save newsletter: ${message}`);
1818
+ }
1819
+ }
1820
+ );
1821
+ }
1822
+
1823
+ // src/tools/calendar.ts
1824
+ import { z as z10 } from "zod";
1825
+ function registerCalendarTools(server2, client2) {
1826
+ server2.tool(
1827
+ "get_calendar",
1828
+ "View the customer's content calendar -- all scheduled, published, and draft posts across social media and newsletters. Use this to check what's already scheduled before creating new content, avoid posting conflicts, and maintain a balanced content mix.",
1829
+ {
1830
+ from: z10.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
1831
+ to: z10.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
1832
+ status: z10.string().optional().describe("Filter by status: scheduled, published, draft, failed"),
1833
+ type: z10.string().optional().describe("Filter by type: social_post or newsletter")
1834
+ },
1835
+ {
1836
+ title: "Get Content Calendar",
1837
+ readOnlyHint: true,
1838
+ destructiveHint: false,
1839
+ idempotentHint: true,
1840
+ openWorldHint: false
1841
+ },
1842
+ async (args) => {
1843
+ const params = {};
1844
+ if (args.from) params.from = args.from;
1845
+ if (args.to) params.to = args.to;
1846
+ if (args.status) params.status = args.status;
1847
+ if (args.type) params.type = args.type;
1848
+ const data = await client2.getCalendar(params);
1849
+ const items = data?.items ?? [];
1850
+ const scheduled = items.filter((i) => i.status === "scheduled");
1851
+ const published = items.filter((i) => i.status === "published");
1852
+ let text = "## Content Calendar\n\n";
1853
+ if (scheduled.length > 0) {
1854
+ text += "### Scheduled\n";
1855
+ for (const item of scheduled) {
1856
+ const date = item.scheduled_for ? new Date(item.scheduled_for).toLocaleString() : "TBD";
1857
+ if (item.type === "newsletter") {
1858
+ text += `\u{1F4E7} ${date} -- Newsletter: "${item.content_preview}"
1859
+ `;
1860
+ } else {
1861
+ const platforms = (item.platforms ?? []).join(", ");
1862
+ text += `\u{1F4C5} ${date} -- ${platforms} -- "${item.content_preview}"
1863
+ `;
1864
+ }
1865
+ }
1866
+ text += "\n";
1867
+ } else {
1868
+ text += "### Scheduled\nNo scheduled content.\n\n";
1869
+ }
1870
+ if (published.length > 0) {
1871
+ text += "### Recently Published\n";
1872
+ for (const item of published.slice(0, 10)) {
1873
+ const date = item.published_at ? new Date(item.published_at).toLocaleString() : "";
1874
+ if (item.type === "newsletter") {
1875
+ text += `\u2705 ${date} -- Newsletter: "${item.content_preview}"
1876
+ `;
1877
+ } else {
1878
+ const platforms = (item.platforms ?? []).join(", ");
1879
+ text += `\u2705 ${date} -- ${platforms} -- "${item.content_preview}"
1880
+ `;
1881
+ }
1882
+ }
1883
+ text += "\n";
1884
+ }
1885
+ try {
1886
+ const nextSlot = await client2.getNextSlot();
1887
+ if (nextSlot?.scheduledFor) {
1888
+ const slotDate = new Date(nextSlot.scheduledFor);
1889
+ const dayNames = [
1890
+ "Sunday",
1891
+ "Monday",
1892
+ "Tuesday",
1893
+ "Wednesday",
1894
+ "Thursday",
1895
+ "Friday",
1896
+ "Saturday"
1897
+ ];
1898
+ text += `### Queue: Next slot
1899
+ \u23ED\uFE0F ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
1900
+ `;
1901
+ }
1902
+ } catch {
1903
+ }
1904
+ return {
1905
+ content: [{ type: "text", text }]
1906
+ };
1907
+ }
1908
+ );
1909
+ server2.tool(
1910
+ "get_queue",
1911
+ "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.",
1912
+ {},
1913
+ {
1914
+ title: "Get Posting Queue",
1915
+ readOnlyHint: true,
1916
+ destructiveHint: false,
1917
+ idempotentHint: true,
1918
+ openWorldHint: false
1919
+ },
1920
+ async () => {
1921
+ const data = await client2.getQueue();
1922
+ const dayNames = [
1923
+ "Sunday",
1924
+ "Monday",
1925
+ "Tuesday",
1926
+ "Wednesday",
1927
+ "Thursday",
1928
+ "Friday",
1929
+ "Saturday"
1930
+ ];
1931
+ let text = "## Posting Queue\n\n";
1932
+ text += `Timezone: ${data?.timezone ?? "UTC"}
1933
+ `;
1934
+ text += `Active: ${data?.active ? "Yes" : "No"}
1935
+
1936
+ `;
1937
+ const slots = data?.slots ?? [];
1938
+ if (slots.length === 0) {
1939
+ text += "No queue slots configured.\n";
1940
+ } else {
1941
+ text += "### Weekly Schedule\n";
1942
+ for (const slot of slots) {
1943
+ const day = dayNames[slot.dayOfWeek] ?? `Day ${slot.dayOfWeek}`;
1944
+ const platforms = (slot.platforms ?? []).map((p) => `[${p}]`).join(" ");
1945
+ text += `${day}: ${slot.time} ${platforms}
1946
+ `;
1947
+ }
1948
+ }
1949
+ try {
1950
+ const nextSlot = await client2.getNextSlot();
1951
+ if (nextSlot?.scheduledFor) {
1952
+ const slotDate = new Date(nextSlot.scheduledFor);
1953
+ text += `
1954
+ Next slot: ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
1955
+ `;
1956
+ }
1957
+ } catch {
1958
+ }
1959
+ return {
1960
+ content: [{ type: "text", text }]
1961
+ };
1962
+ }
1963
+ );
1964
+ server2.tool(
1965
+ "schedule_to_queue",
1966
+ "Add a post to the customer's content queue. The post will be automatically scheduled to the next available time slot. Requires confirmation before scheduling.",
1967
+ {
1968
+ content: z10.string().describe("The text content of the post"),
1969
+ platforms: z10.array(z10.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
1970
+ media_urls: z10.array(z10.string()).optional().describe("Public URLs of media to attach"),
1971
+ confirmed: z10.boolean().optional().describe(
1972
+ "Set to true to confirm scheduling. If false or missing, returns a preview for user approval."
1973
+ )
1974
+ },
1975
+ {
1976
+ title: "Schedule Post to Queue",
1977
+ readOnlyHint: false,
1978
+ destructiveHint: false,
1979
+ idempotentHint: false,
1980
+ openWorldHint: true
1981
+ },
1982
+ async (args) => {
1983
+ const nextSlot = await client2.getNextSlot();
1984
+ if (!nextSlot?.scheduledFor) {
1985
+ return {
1986
+ content: [
1987
+ {
1988
+ type: "text",
1989
+ text: "No queue slots configured. Set up your posting queue first using the dashboard or ask to configure queue slots."
1990
+ }
1991
+ ]
1992
+ };
1993
+ }
1994
+ if (!args.confirmed) {
1995
+ const slotDate2 = new Date(nextSlot.scheduledFor);
1996
+ const preview = `## Queue Post Preview
1997
+
1998
+ **Content:** "${args.content}"
1999
+ **Platforms:** ${args.platforms.join(", ")}
2000
+ **Next queue slot:** ${slotDate2.toLocaleString()}
2001
+ ` + (args.media_urls?.length ? `**Media:** ${args.media_urls.length} file(s)
2002
+ ` : "") + `
2003
+ Call this tool again with confirmed=true to schedule.`;
2004
+ return { content: [{ type: "text", text: preview }] };
2005
+ }
2006
+ const body = {
2007
+ content: args.content,
2008
+ platforms: args.platforms.map((p) => ({ platform: p })),
2009
+ scheduledFor: nextSlot.scheduledFor,
2010
+ timezone: nextSlot.timezone ?? "UTC"
2011
+ };
2012
+ if (args.media_urls?.length) {
2013
+ body.mediaItems = args.media_urls.map((url) => ({
2014
+ type: url.match(/\.(mp4|mov|avi|webm)$/i) ? "video" : "image",
2015
+ url
2016
+ }));
2017
+ }
2018
+ const result = await client2.createPost(body);
2019
+ const slotDate = new Date(nextSlot.scheduledFor);
2020
+ return {
2021
+ content: [
2022
+ {
2023
+ type: "text",
2024
+ text: `Post scheduled to queue slot: ${slotDate.toLocaleString()}
2025
+
2026
+ ${JSON.stringify(result, null, 2)}`
2027
+ }
2028
+ ]
2029
+ };
2030
+ }
2031
+ );
2032
+ }
2033
+
2034
+ // src/tools/notifications.ts
2035
+ import { z as z11 } from "zod";
2036
+ function timeAgo(dateStr) {
2037
+ const diff = Date.now() - new Date(dateStr).getTime();
2038
+ const minutes = Math.floor(diff / 6e4);
2039
+ if (minutes < 1) return "just now";
2040
+ if (minutes < 60) return `${minutes}m ago`;
2041
+ const hours = Math.floor(minutes / 60);
2042
+ if (hours < 24) return `${hours}h ago`;
2043
+ const days = Math.floor(hours / 24);
2044
+ return `${days}d ago`;
2045
+ }
2046
+ function registerNotificationTools(server2, client2) {
2047
+ server2.tool(
2048
+ "get_notifications",
2049
+ "Get recent notifications including post publishing results, account health warnings, and errors. Use this to check if any posts failed or if any accounts need attention.",
2050
+ {
2051
+ unread_only: z11.boolean().optional().describe("If true, only show unread notifications. Default false."),
2052
+ limit: z11.number().optional().describe("Max notifications to return. Default 10.")
2053
+ },
2054
+ {
2055
+ title: "Get Notifications",
2056
+ readOnlyHint: true,
2057
+ destructiveHint: false,
2058
+ idempotentHint: true,
2059
+ openWorldHint: false
2060
+ },
2061
+ async (args) => {
2062
+ const params = {};
2063
+ if (args.unread_only) params.unread = "true";
2064
+ if (args.limit) params.limit = String(args.limit);
2065
+ else params.limit = "10";
2066
+ const result = await client2.getNotifications(params);
2067
+ const notifications = result.notifications ?? [];
2068
+ const unreadCount = result.unread_count ?? 0;
2069
+ if (notifications.length === 0) {
2070
+ return {
2071
+ content: [{
2072
+ type: "text",
2073
+ text: `## Notifications${args.unread_only ? " (unread)" : ""}
2074
+
2075
+ No notifications found. All clear!`
2076
+ }]
2077
+ };
2078
+ }
2079
+ const severityIcon = {
2080
+ success: "\u{1F7E2}",
2081
+ warning: "\u{1F7E1}",
2082
+ error: "\u{1F534}",
2083
+ info: "\u{1F535}"
2084
+ };
2085
+ const lines = notifications.map((n) => {
2086
+ const icon = severityIcon[n.severity] ?? "\u2B1C";
2087
+ const time = n.createdAt ? timeAgo(n.createdAt) : "";
2088
+ let line = `${icon} **${time}** \u2014 ${n.title}
2089
+ "${n.message}"`;
2090
+ if (n.actionUrl || n.actionLabel) {
2091
+ const action = n.actionLabel || "View";
2092
+ if (n.resourceType === "post" && n.resourceId) {
2093
+ line += `
2094
+ Action: ${action} \u2192 retry_post with post_id "${n.resourceId}"`;
2095
+ } else {
2096
+ line += `
2097
+ Action: ${action} \u2192 ${n.actionUrl}`;
2098
+ }
2099
+ }
2100
+ return line;
2101
+ });
2102
+ const header = `## Notifications (${unreadCount} unread)
2103
+
2104
+ `;
2105
+ const footer = `
2106
+
2107
+ Showing ${notifications.length} of ${result.unread_count !== void 0 ? "total" : ""} notifications.`;
2108
+ return {
2109
+ content: [{
2110
+ type: "text",
2111
+ text: header + lines.join("\n\n") + footer
2112
+ }]
2113
+ };
2114
+ }
2115
+ );
2116
+ }
2117
+
982
2118
  // src/index.ts
983
2119
  var apiKey = process.env.BUZZPOSTER_API_KEY;
984
2120
  var apiUrl = process.env.BUZZPOSTER_API_URL ?? "https://api.buzzposter.com";
@@ -1006,5 +2142,9 @@ registerRssTools(server, client);
1006
2142
  registerAccountInfoTool(server, client);
1007
2143
  registerBrandVoiceTools(server, client);
1008
2144
  registerKnowledgeTools(server, client);
2145
+ registerAudienceTools(server, client);
2146
+ registerNewsletterTemplateTools(server, client);
2147
+ registerCalendarTools(server, client);
2148
+ registerNotificationTools(server, client);
1009
2149
  var transport = new StdioServerTransport();
1010
2150
  await server.connect(transport);