@buzzposter/mcp 0.1.2 → 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 +1364 -19
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -152,6 +152,64 @@ var BuzzPosterClient = class {
152
152
  async listForms() {
153
153
  return this.request("GET", "/api/v1/newsletters/forms");
154
154
  }
155
+ // Knowledge
156
+ async listKnowledge(params) {
157
+ return this.request("GET", "/api/v1/knowledge", void 0, params);
158
+ }
159
+ async createKnowledge(data) {
160
+ return this.request("POST", "/api/v1/knowledge", data);
161
+ }
162
+ // Brand Voice
163
+ async getBrandVoice() {
164
+ return this.request("GET", "/api/v1/brand-voice");
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
+ }
155
213
  // RSS
156
214
  async fetchFeed(url, limit) {
157
215
  const params = { url };
@@ -161,6 +219,16 @@ var BuzzPosterClient = class {
161
219
  async fetchArticle(url) {
162
220
  return this.request("GET", "/api/v1/rss/article", void 0, { url });
163
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
+ }
164
232
  };
165
233
 
166
234
  // src/tools/posts.ts
@@ -168,16 +236,36 @@ import { z } from "zod";
168
236
  function registerPostTools(server2, client2) {
169
237
  server2.tool(
170
238
  "post",
171
- "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.",
172
240
  {
173
241
  content: z.string().optional().describe("The text content of the post"),
174
242
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
175
243
  media_urls: z.array(z.string()).optional().describe("Public URLs of media to attach"),
176
244
  platform_specific: z.record(z.record(z.unknown())).optional().describe(
177
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."
178
249
  )
179
250
  },
251
+ {
252
+ title: "Publish Post Now",
253
+ readOnlyHint: false,
254
+ destructiveHint: false,
255
+ idempotentHint: false,
256
+ openWorldHint: true
257
+ },
180
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
+ }
181
269
  const platforms = args.platforms.map((platform) => {
182
270
  const entry = { platform };
183
271
  if (args.platform_specific?.[platform]) {
@@ -204,10 +292,20 @@ function registerPostTools(server2, client2) {
204
292
  );
205
293
  server2.tool(
206
294
  "cross_post",
207
- "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.",
208
296
  {
209
297
  content: z.string().describe("The text content to post everywhere"),
210
- 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
211
309
  },
212
310
  async (args) => {
213
311
  const accountsData = await client2.listAccounts();
@@ -224,6 +322,16 @@ function registerPostTools(server2, client2) {
224
322
  ]
225
323
  };
226
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
+ }
227
335
  const body = {
228
336
  content: args.content,
229
337
  platforms: platforms.map((p) => ({ platform: p })),
@@ -243,16 +351,37 @@ function registerPostTools(server2, client2) {
243
351
  );
244
352
  server2.tool(
245
353
  "schedule_post",
246
- "Schedule a post for future publication.",
354
+ "Schedule a post for future publication. Requires confirmation before scheduling.",
247
355
  {
248
356
  content: z.string().optional().describe("The text content of the post"),
249
357
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
250
358
  scheduled_for: z.string().describe("ISO 8601 datetime for when to publish, e.g. 2024-01-16T12:00:00"),
251
359
  timezone: z.string().default("UTC").describe("Timezone for the scheduled time, e.g. America/New_York"),
252
360
  media_urls: z.array(z.string()).optional().describe("Media URLs to attach"),
253
- 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
254
372
  },
255
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
+ }
256
385
  const platforms = args.platforms.map((platform) => {
257
386
  const entry = { platform };
258
387
  if (args.platform_specific?.[platform]) {
@@ -286,6 +415,13 @@ function registerPostTools(server2, client2) {
286
415
  platforms: z.array(z.string()).describe("Platforms this draft is intended for"),
287
416
  media_urls: z.array(z.string()).optional().describe("Media URLs to attach")
288
417
  },
418
+ {
419
+ title: "Create Draft Post",
420
+ readOnlyHint: false,
421
+ destructiveHint: false,
422
+ idempotentHint: false,
423
+ openWorldHint: false
424
+ },
289
425
  async (args) => {
290
426
  const body = {
291
427
  content: args.content,
@@ -311,6 +447,13 @@ function registerPostTools(server2, client2) {
311
447
  status: z.string().optional().describe("Filter by status: published, scheduled, draft, failed"),
312
448
  limit: z.string().optional().describe("Number of posts to return")
313
449
  },
450
+ {
451
+ title: "List Posts",
452
+ readOnlyHint: true,
453
+ destructiveHint: false,
454
+ idempotentHint: true,
455
+ openWorldHint: true
456
+ },
314
457
  async (args) => {
315
458
  const params = {};
316
459
  if (args.status) params.status = args.status;
@@ -327,6 +470,13 @@ function registerPostTools(server2, client2) {
327
470
  {
328
471
  post_id: z.string().describe("The ID of the post to retrieve")
329
472
  },
473
+ {
474
+ title: "Get Post Details",
475
+ readOnlyHint: true,
476
+ destructiveHint: false,
477
+ idempotentHint: true,
478
+ openWorldHint: true
479
+ },
330
480
  async (args) => {
331
481
  const result = await client2.getPost(args.post_id);
332
482
  return {
@@ -336,11 +486,48 @@ function registerPostTools(server2, client2) {
336
486
  );
337
487
  server2.tool(
338
488
  "retry_post",
339
- "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.",
340
490
  {
341
- 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
342
502
  },
343
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
+ }
344
531
  const result = await client2.retryPost(args.post_id);
345
532
  return {
346
533
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
@@ -355,6 +542,13 @@ function registerAccountTools(server2, client2) {
355
542
  "list_accounts",
356
543
  "List all connected social media accounts with their platform, username, and connection status.",
357
544
  {},
545
+ {
546
+ title: "List Connected Accounts",
547
+ readOnlyHint: true,
548
+ destructiveHint: false,
549
+ idempotentHint: true,
550
+ openWorldHint: false
551
+ },
358
552
  async () => {
359
553
  const result = await client2.listAccounts();
360
554
  return {
@@ -364,12 +558,49 @@ function registerAccountTools(server2, client2) {
364
558
  );
365
559
  server2.tool(
366
560
  "check_accounts_health",
367
- "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.",
368
562
  {},
563
+ {
564
+ title: "Check Account Health",
565
+ readOnlyHint: true,
566
+ destructiveHint: false,
567
+ idempotentHint: true,
568
+ openWorldHint: true
569
+ },
369
570
  async () => {
370
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
+ }
371
602
  return {
372
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
603
+ content: [{ type: "text", text }]
373
604
  };
374
605
  }
375
606
  );
@@ -386,6 +617,13 @@ function registerAnalyticsTools(server2, client2) {
386
617
  from_date: z2.string().optional().describe("Start date for analytics range (ISO 8601)"),
387
618
  to_date: z2.string().optional().describe("End date for analytics range (ISO 8601)")
388
619
  },
620
+ {
621
+ title: "Get Analytics",
622
+ readOnlyHint: true,
623
+ destructiveHint: false,
624
+ idempotentHint: true,
625
+ openWorldHint: true
626
+ },
389
627
  async (args) => {
390
628
  const params = {};
391
629
  if (args.platform) params.platform = args.platform;
@@ -408,6 +646,13 @@ function registerInboxTools(server2, client2) {
408
646
  {
409
647
  platform: z3.string().optional().describe("Filter by platform: twitter, instagram, linkedin, facebook")
410
648
  },
649
+ {
650
+ title: "List Conversations",
651
+ readOnlyHint: true,
652
+ destructiveHint: false,
653
+ idempotentHint: true,
654
+ openWorldHint: true
655
+ },
411
656
  async (args) => {
412
657
  const params = {};
413
658
  if (args.platform) params.platform = args.platform;
@@ -423,6 +668,13 @@ function registerInboxTools(server2, client2) {
423
668
  {
424
669
  conversation_id: z3.string().describe("The conversation ID")
425
670
  },
671
+ {
672
+ title: "Get Conversation Messages",
673
+ readOnlyHint: true,
674
+ destructiveHint: false,
675
+ idempotentHint: true,
676
+ openWorldHint: true
677
+ },
426
678
  async (args) => {
427
679
  const result = await client2.getConversation(args.conversation_id);
428
680
  return {
@@ -432,12 +684,29 @@ function registerInboxTools(server2, client2) {
432
684
  );
433
685
  server2.tool(
434
686
  "reply_to_conversation",
435
- "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.",
436
688
  {
437
689
  conversation_id: z3.string().describe("The conversation ID to reply to"),
438
- 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
439
699
  },
440
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
+ }
441
710
  const result = await client2.replyToConversation(
442
711
  args.conversation_id,
443
712
  args.message
@@ -453,6 +722,13 @@ function registerInboxTools(server2, client2) {
453
722
  {
454
723
  post_id: z3.string().optional().describe("Filter comments by post ID")
455
724
  },
725
+ {
726
+ title: "List Comments",
727
+ readOnlyHint: true,
728
+ destructiveHint: false,
729
+ idempotentHint: true,
730
+ openWorldHint: true
731
+ },
456
732
  async (args) => {
457
733
  const params = {};
458
734
  if (args.post_id) params.postId = args.post_id;
@@ -464,12 +740,29 @@ function registerInboxTools(server2, client2) {
464
740
  );
465
741
  server2.tool(
466
742
  "reply_to_comment",
467
- "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.",
468
744
  {
469
745
  comment_id: z3.string().describe("The comment ID to reply to"),
470
- 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
471
755
  },
472
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
+ }
473
766
  const result = await client2.replyToComment(
474
767
  args.comment_id,
475
768
  args.message
@@ -483,6 +776,13 @@ function registerInboxTools(server2, client2) {
483
776
  "list_reviews",
484
777
  "List reviews on your Facebook page.",
485
778
  {},
779
+ {
780
+ title: "List Reviews",
781
+ readOnlyHint: true,
782
+ destructiveHint: false,
783
+ idempotentHint: true,
784
+ openWorldHint: true
785
+ },
486
786
  async () => {
487
787
  const result = await client2.listReviews();
488
788
  return {
@@ -492,12 +792,29 @@ function registerInboxTools(server2, client2) {
492
792
  );
493
793
  server2.tool(
494
794
  "reply_to_review",
495
- "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.",
496
796
  {
497
797
  review_id: z3.string().describe("The review ID to reply to"),
498
- 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
499
807
  },
500
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
+ }
501
818
  const result = await client2.replyToReview(
502
819
  args.review_id,
503
820
  args.message
@@ -519,6 +836,13 @@ function registerMediaTools(server2, client2) {
519
836
  {
520
837
  file_path: z4.string().describe("Absolute path to the file on the local filesystem")
521
838
  },
839
+ {
840
+ title: "Upload Media File",
841
+ readOnlyHint: false,
842
+ destructiveHint: false,
843
+ idempotentHint: false,
844
+ openWorldHint: false
845
+ },
522
846
  async (args) => {
523
847
  const buffer = await readFile(args.file_path);
524
848
  const base64 = buffer.toString("base64");
@@ -551,6 +875,13 @@ function registerMediaTools(server2, client2) {
551
875
  data: z4.string().describe("Base64-encoded file data"),
552
876
  mime_type: z4.string().describe("MIME type of the file, e.g. image/jpeg, video/mp4")
553
877
  },
878
+ {
879
+ title: "Upload Media Base64",
880
+ readOnlyHint: false,
881
+ destructiveHint: false,
882
+ idempotentHint: false,
883
+ openWorldHint: false
884
+ },
554
885
  async (args) => {
555
886
  const result = await client2.uploadMedia(
556
887
  args.filename,
@@ -566,6 +897,13 @@ function registerMediaTools(server2, client2) {
566
897
  "list_media",
567
898
  "List all uploaded media files in your BuzzPoster media library.",
568
899
  {},
900
+ {
901
+ title: "List Media Library",
902
+ readOnlyHint: true,
903
+ destructiveHint: false,
904
+ idempotentHint: true,
905
+ openWorldHint: false
906
+ },
569
907
  async () => {
570
908
  const result = await client2.listMedia();
571
909
  return {
@@ -575,11 +913,27 @@ function registerMediaTools(server2, client2) {
575
913
  );
576
914
  server2.tool(
577
915
  "delete_media",
578
- "Delete a media file from your BuzzPoster media library.",
916
+ "Delete a media file from your BuzzPoster media library. This cannot be undone.",
579
917
  {
580
- 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
581
927
  },
582
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
+ }
583
937
  await client2.deleteMedia(args.key);
584
938
  return {
585
939
  content: [{ type: "text", text: "Media deleted successfully." }]
@@ -598,6 +952,13 @@ function registerNewsletterTools(server2, client2) {
598
952
  page: z5.string().optional().describe("Page number for pagination"),
599
953
  per_page: z5.string().optional().describe("Number of subscribers per page")
600
954
  },
955
+ {
956
+ title: "List Subscribers",
957
+ readOnlyHint: true,
958
+ destructiveHint: false,
959
+ idempotentHint: true,
960
+ openWorldHint: true
961
+ },
601
962
  async (args) => {
602
963
  const params = {};
603
964
  if (args.page) params.page = args.page;
@@ -616,6 +977,13 @@ function registerNewsletterTools(server2, client2) {
616
977
  first_name: z5.string().optional().describe("Subscriber's first name"),
617
978
  last_name: z5.string().optional().describe("Subscriber's last name")
618
979
  },
980
+ {
981
+ title: "Add Subscriber",
982
+ readOnlyHint: false,
983
+ destructiveHint: false,
984
+ idempotentHint: false,
985
+ openWorldHint: true
986
+ },
619
987
  async (args) => {
620
988
  const result = await client2.addSubscriber({
621
989
  email: args.email,
@@ -635,6 +1003,13 @@ function registerNewsletterTools(server2, client2) {
635
1003
  content: z5.string().describe("HTML content of the newsletter"),
636
1004
  preview_text: z5.string().optional().describe("Preview text shown in email clients")
637
1005
  },
1006
+ {
1007
+ title: "Create Newsletter Draft",
1008
+ readOnlyHint: false,
1009
+ destructiveHint: false,
1010
+ idempotentHint: false,
1011
+ openWorldHint: true
1012
+ },
638
1013
  async (args) => {
639
1014
  const result = await client2.createBroadcast({
640
1015
  subject: args.subject,
@@ -655,6 +1030,13 @@ function registerNewsletterTools(server2, client2) {
655
1030
  content: z5.string().optional().describe("Updated HTML content"),
656
1031
  preview_text: z5.string().optional().describe("Updated preview text")
657
1032
  },
1033
+ {
1034
+ title: "Update Newsletter Draft",
1035
+ readOnlyHint: false,
1036
+ destructiveHint: false,
1037
+ idempotentHint: true,
1038
+ openWorldHint: true
1039
+ },
658
1040
  async (args) => {
659
1041
  const data = {};
660
1042
  if (args.subject) data.subject = args.subject;
@@ -668,11 +1050,31 @@ function registerNewsletterTools(server2, client2) {
668
1050
  );
669
1051
  server2.tool(
670
1052
  "send_newsletter",
671
- "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.",
672
1054
  {
673
- broadcast_id: z5.string().describe("The broadcast/newsletter ID to send")
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
+ },
1060
+ {
1061
+ title: "Send Newsletter",
1062
+ readOnlyHint: false,
1063
+ destructiveHint: false,
1064
+ idempotentHint: false,
1065
+ openWorldHint: true
674
1066
  },
675
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
+ }
676
1078
  await client2.sendBroadcast(args.broadcast_id);
677
1079
  return {
678
1080
  content: [{ type: "text", text: "Newsletter sent successfully." }]
@@ -685,6 +1087,13 @@ function registerNewsletterTools(server2, client2) {
685
1087
  {
686
1088
  page: z5.string().optional().describe("Page number for pagination")
687
1089
  },
1090
+ {
1091
+ title: "List Newsletters",
1092
+ readOnlyHint: true,
1093
+ destructiveHint: false,
1094
+ idempotentHint: true,
1095
+ openWorldHint: true
1096
+ },
688
1097
  async (args) => {
689
1098
  const params = {};
690
1099
  if (args.page) params.page = args.page;
@@ -698,6 +1107,13 @@ function registerNewsletterTools(server2, client2) {
698
1107
  "list_tags",
699
1108
  "List all subscriber tags from your email service provider.",
700
1109
  {},
1110
+ {
1111
+ title: "List Subscriber Tags",
1112
+ readOnlyHint: true,
1113
+ destructiveHint: false,
1114
+ idempotentHint: true,
1115
+ openWorldHint: true
1116
+ },
701
1117
  async () => {
702
1118
  const result = await client2.listTags();
703
1119
  return {
@@ -709,6 +1125,13 @@ function registerNewsletterTools(server2, client2) {
709
1125
  "list_sequences",
710
1126
  "List all email sequences/automations from your email service provider.",
711
1127
  {},
1128
+ {
1129
+ title: "List Email Sequences",
1130
+ readOnlyHint: true,
1131
+ destructiveHint: false,
1132
+ idempotentHint: true,
1133
+ openWorldHint: true
1134
+ },
712
1135
  async () => {
713
1136
  const result = await client2.listSequences();
714
1137
  return {
@@ -720,6 +1143,13 @@ function registerNewsletterTools(server2, client2) {
720
1143
  "list_forms",
721
1144
  "List all signup forms from your email service provider.",
722
1145
  {},
1146
+ {
1147
+ title: "List Signup Forms",
1148
+ readOnlyHint: true,
1149
+ destructiveHint: false,
1150
+ idempotentHint: true,
1151
+ openWorldHint: true
1152
+ },
723
1153
  async () => {
724
1154
  const result = await client2.listForms();
725
1155
  return {
@@ -739,6 +1169,13 @@ function registerRssTools(server2, client2) {
739
1169
  url: z6.string().describe("The RSS/Atom feed URL to fetch"),
740
1170
  limit: z6.number().optional().describe("Maximum number of entries to return (default 10, max 100)")
741
1171
  },
1172
+ {
1173
+ title: "Fetch RSS Feed",
1174
+ readOnlyHint: true,
1175
+ destructiveHint: false,
1176
+ idempotentHint: true,
1177
+ openWorldHint: true
1178
+ },
742
1179
  async (args) => {
743
1180
  const result = await client2.fetchFeed(args.url, args.limit);
744
1181
  return {
@@ -752,6 +1189,13 @@ function registerRssTools(server2, client2) {
752
1189
  {
753
1190
  url: z6.string().describe("The article URL to extract content from")
754
1191
  },
1192
+ {
1193
+ title: "Fetch Article Content",
1194
+ readOnlyHint: true,
1195
+ destructiveHint: false,
1196
+ idempotentHint: true,
1197
+ openWorldHint: true
1198
+ },
755
1199
  async (args) => {
756
1200
  const result = await client2.fetchArticle(args.url);
757
1201
  return {
@@ -767,6 +1211,13 @@ function registerAccountInfoTool(server2, client2) {
767
1211
  "get_account",
768
1212
  "Get your BuzzPoster account details including name, email, subscription status, ESP configuration, and connected platforms.",
769
1213
  {},
1214
+ {
1215
+ title: "Get Account Details",
1216
+ readOnlyHint: true,
1217
+ destructiveHint: false,
1218
+ idempotentHint: true,
1219
+ openWorldHint: false
1220
+ },
770
1221
  async () => {
771
1222
  const result = await client2.getAccount();
772
1223
  return {
@@ -776,6 +1227,894 @@ function registerAccountInfoTool(server2, client2) {
776
1227
  );
777
1228
  }
778
1229
 
1230
+ // src/tools/brand-voice.ts
1231
+ function registerBrandVoiceTools(server2, client2) {
1232
+ server2.tool(
1233
+ "get_brand_voice",
1234
+ "Get the customer's brand voice profile and writing rules. IMPORTANT: Call this tool BEFORE creating any post, draft, or content. Use the returned voice description, rules, and examples to match the customer's tone and style in everything you write for them.",
1235
+ {},
1236
+ {
1237
+ title: "Get Brand Voice",
1238
+ readOnlyHint: true,
1239
+ destructiveHint: false,
1240
+ idempotentHint: true,
1241
+ openWorldHint: false
1242
+ },
1243
+ async () => {
1244
+ try {
1245
+ const voice = await client2.getBrandVoice();
1246
+ const lines = [];
1247
+ lines.push(`## Brand Voice: ${voice.name || "My Brand"}`);
1248
+ lines.push("");
1249
+ if (voice.description) {
1250
+ lines.push("### Voice Description");
1251
+ lines.push(voice.description);
1252
+ lines.push("");
1253
+ }
1254
+ if (voice.dos && voice.dos.length > 0) {
1255
+ lines.push("### Do's");
1256
+ for (const rule of voice.dos) {
1257
+ lines.push(`- ${rule}`);
1258
+ }
1259
+ lines.push("");
1260
+ }
1261
+ if (voice.donts && voice.donts.length > 0) {
1262
+ lines.push("### Don'ts");
1263
+ for (const rule of voice.donts) {
1264
+ lines.push(`- ${rule}`);
1265
+ }
1266
+ lines.push("");
1267
+ }
1268
+ if (voice.platformRules && Object.keys(voice.platformRules).length > 0) {
1269
+ lines.push("### Platform-Specific Rules");
1270
+ for (const [platform, rules] of Object.entries(voice.platformRules)) {
1271
+ lines.push(`**${platform}:** ${rules}`);
1272
+ }
1273
+ lines.push("");
1274
+ }
1275
+ if (voice.examplePosts && voice.examplePosts.length > 0) {
1276
+ lines.push("### Example Posts");
1277
+ voice.examplePosts.forEach((post, i) => {
1278
+ lines.push(`${i + 1}. ${post}`);
1279
+ });
1280
+ lines.push("");
1281
+ }
1282
+ return {
1283
+ content: [{ type: "text", text: lines.join("\n") }]
1284
+ };
1285
+ } catch (error) {
1286
+ const message = error instanceof Error ? error.message : "Unknown error";
1287
+ if (message.includes("404") || message.includes("No brand voice")) {
1288
+ return {
1289
+ content: [
1290
+ {
1291
+ type: "text",
1292
+ text: "No brand voice has been configured yet. The customer can set one up at their BuzzPoster dashboard."
1293
+ }
1294
+ ]
1295
+ };
1296
+ }
1297
+ throw error;
1298
+ }
1299
+ }
1300
+ );
1301
+ }
1302
+
1303
+ // src/tools/knowledge.ts
1304
+ import { z as z7 } from "zod";
1305
+ function registerKnowledgeTools(server2, client2) {
1306
+ server2.tool(
1307
+ "get_knowledge_base",
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.",
1309
+ {},
1310
+ {
1311
+ title: "Get Knowledge Base",
1312
+ readOnlyHint: true,
1313
+ destructiveHint: false,
1314
+ idempotentHint: true,
1315
+ openWorldHint: false
1316
+ },
1317
+ async () => {
1318
+ try {
1319
+ const data = await client2.listKnowledge();
1320
+ const items = data.items ?? [];
1321
+ if (items.length === 0) {
1322
+ return {
1323
+ content: [
1324
+ {
1325
+ type: "text",
1326
+ text: "The knowledge base is empty. The customer can add reference material at their BuzzPoster dashboard."
1327
+ }
1328
+ ]
1329
+ };
1330
+ }
1331
+ const totalChars = items.reduce(
1332
+ (sum, item) => sum + item.content.length,
1333
+ 0
1334
+ );
1335
+ const shouldTruncate = totalChars > 1e4;
1336
+ const lines = [];
1337
+ lines.push(`## Knowledge Base (${items.length} items)`);
1338
+ lines.push("");
1339
+ for (const item of items) {
1340
+ lines.push(`### ${item.title}`);
1341
+ if (item.tags && item.tags.length > 0) {
1342
+ lines.push(`Tags: ${item.tags.join(", ")}`);
1343
+ }
1344
+ if (shouldTruncate) {
1345
+ lines.push(item.content.slice(0, 500) + " [truncated]");
1346
+ } else {
1347
+ lines.push(item.content);
1348
+ }
1349
+ lines.push("");
1350
+ }
1351
+ return {
1352
+ content: [{ type: "text", text: lines.join("\n") }]
1353
+ };
1354
+ } catch (error) {
1355
+ const message = error instanceof Error ? error.message : "Unknown error";
1356
+ throw new Error(`Failed to get knowledge base: ${message}`);
1357
+ }
1358
+ }
1359
+ );
1360
+ server2.tool(
1361
+ "search_knowledge",
1362
+ "Search the customer's knowledge base by tag or keyword. Use this to find specific reference material when writing about a topic.",
1363
+ {
1364
+ query: z7.string().describe(
1365
+ "Search query - matches against tags first, then falls back to text search on title and content"
1366
+ )
1367
+ },
1368
+ {
1369
+ title: "Search Knowledge Base",
1370
+ readOnlyHint: true,
1371
+ destructiveHint: false,
1372
+ idempotentHint: true,
1373
+ openWorldHint: false
1374
+ },
1375
+ async ({ query }) => {
1376
+ try {
1377
+ const data = await client2.listKnowledge({ tag: query });
1378
+ const items = data.items ?? [];
1379
+ if (items.length === 0) {
1380
+ return {
1381
+ content: [
1382
+ {
1383
+ type: "text",
1384
+ text: `No knowledge base items found matching "${query}".`
1385
+ }
1386
+ ]
1387
+ };
1388
+ }
1389
+ const lines = [];
1390
+ lines.push(
1391
+ `## Knowledge Base Results for "${query}" (${items.length} items)`
1392
+ );
1393
+ lines.push("");
1394
+ for (const item of items) {
1395
+ lines.push(`### ${item.title}`);
1396
+ if (item.tags && item.tags.length > 0) {
1397
+ lines.push(`Tags: ${item.tags.join(", ")}`);
1398
+ }
1399
+ lines.push(item.content);
1400
+ lines.push("");
1401
+ }
1402
+ return {
1403
+ content: [{ type: "text", text: lines.join("\n") }]
1404
+ };
1405
+ } catch (error) {
1406
+ const message = error instanceof Error ? error.message : "Unknown error";
1407
+ throw new Error(`Failed to search knowledge base: ${message}`);
1408
+ }
1409
+ }
1410
+ );
1411
+ server2.tool(
1412
+ "add_knowledge",
1413
+ "Add a new item to the customer's knowledge base. Use this to save useful information the customer shares during conversation.",
1414
+ {
1415
+ title: z7.string().describe("Title for the knowledge item"),
1416
+ content: z7.string().describe("The content/text to save"),
1417
+ tags: z7.array(z7.string()).optional().describe("Optional tags for categorization")
1418
+ },
1419
+ {
1420
+ title: "Add Knowledge Item",
1421
+ readOnlyHint: false,
1422
+ destructiveHint: false,
1423
+ idempotentHint: false,
1424
+ openWorldHint: false
1425
+ },
1426
+ async ({ title, content, tags }) => {
1427
+ try {
1428
+ await client2.createKnowledge({
1429
+ title,
1430
+ content,
1431
+ sourceType: "text",
1432
+ tags: tags ?? []
1433
+ });
1434
+ return {
1435
+ content: [
1436
+ {
1437
+ type: "text",
1438
+ text: `Added "${title}" to the knowledge base.`
1439
+ }
1440
+ ]
1441
+ };
1442
+ } catch (error) {
1443
+ const message = error instanceof Error ? error.message : "Unknown error";
1444
+ throw new Error(`Failed to add knowledge item: ${message}`);
1445
+ }
1446
+ }
1447
+ );
1448
+ }
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
+
779
2118
  // src/index.ts
780
2119
  var apiKey = process.env.BUZZPOSTER_API_KEY;
781
2120
  var apiUrl = process.env.BUZZPOSTER_API_URL ?? "https://api.buzzposter.com";
@@ -801,5 +2140,11 @@ registerMediaTools(server, client);
801
2140
  registerNewsletterTools(server, client);
802
2141
  registerRssTools(server, client);
803
2142
  registerAccountInfoTool(server, client);
2143
+ registerBrandVoiceTools(server, client);
2144
+ registerKnowledgeTools(server, client);
2145
+ registerAudienceTools(server, client);
2146
+ registerNewsletterTemplateTools(server, client);
2147
+ registerCalendarTools(server, client);
2148
+ registerNotificationTools(server, client);
804
2149
  var transport = new StdioServerTransport();
805
2150
  await server.connect(transport);