@buzzposter/mcp 0.1.17 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12,8 +12,8 @@ var BuzzPosterClient = class {
12
12
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
13
13
  this.apiKey = config.apiKey;
14
14
  }
15
- async request(method, path, body, query) {
16
- const url = new URL(`${this.baseUrl}${path}`);
15
+ async request(method, path2, body, query) {
16
+ const url = new URL(`${this.baseUrl}${path2}`);
17
17
  if (query) {
18
18
  for (const [k, v] of Object.entries(query)) {
19
19
  if (v !== void 0 && v !== "") url.searchParams.set(k, v);
@@ -170,15 +170,8 @@ var BuzzPosterClient = class {
170
170
  async listPostTemplates() {
171
171
  return this.request("GET", "/api/v1/newsletters/post-templates");
172
172
  }
173
- async getPostAggregateStats() {
174
- return this.request("GET", "/api/v1/newsletters/broadcasts/aggregate-stats");
175
- }
176
- // Knowledge
177
- async listKnowledge(params) {
178
- return this.request("GET", "/api/v1/knowledge", void 0, params);
179
- }
180
- async createKnowledge(data) {
181
- return this.request("POST", "/api/v1/knowledge", data);
173
+ async listEmailTemplates() {
174
+ return this.request("GET", "/api/v1/newsletters/email-templates");
182
175
  }
183
176
  // Brand Voice
184
177
  async getBrandVoice() {
@@ -188,9 +181,15 @@ var BuzzPosterClient = class {
188
181
  return this.request("PUT", "/api/v1/brand-voice", data);
189
182
  }
190
183
  // Audiences
184
+ async listAudiences() {
185
+ return this.request("GET", "/api/v1/audiences");
186
+ }
191
187
  async getAudience(id) {
192
188
  return this.request("GET", `/api/v1/audiences/${id}`);
193
189
  }
190
+ async getAudienceByName(name) {
191
+ return this.request("GET", `/api/v1/audiences/by-name/${encodeURIComponent(name)}`);
192
+ }
194
193
  async getDefaultAudience() {
195
194
  return this.request("GET", "/api/v1/audiences/default");
196
195
  }
@@ -200,6 +199,9 @@ var BuzzPosterClient = class {
200
199
  async updateAudience(id, data) {
201
200
  return this.request("PUT", `/api/v1/audiences/${id}`, data);
202
201
  }
202
+ async deleteAudience(id) {
203
+ return this.request("DELETE", `/api/v1/audiences/${id}`);
204
+ }
203
205
  // Newsletter Templates
204
206
  async getTemplate(id) {
205
207
  if (id) return this.request("GET", `/api/v1/templates/${id}`);
@@ -220,16 +222,6 @@ var BuzzPosterClient = class {
220
222
  async duplicateTemplate(id) {
221
223
  return this.request("POST", "/api/v1/templates/" + id + "/duplicate");
222
224
  }
223
- // Newsletter Archive
224
- async listNewsletterArchive(params) {
225
- return this.request("GET", "/api/v1/newsletter-archive", void 0, params);
226
- }
227
- async getArchivedNewsletter(id) {
228
- return this.request("GET", `/api/v1/newsletter-archive/${id}`);
229
- }
230
- async saveNewsletterToArchive(data) {
231
- return this.request("POST", "/api/v1/newsletter-archive", data);
232
- }
233
225
  // Calendar
234
226
  async getCalendar(params) {
235
227
  return this.request("GET", "/api/v1/calendar", void 0, params);
@@ -291,6 +283,37 @@ var BuzzPosterClient = class {
291
283
  async getPublishingRules() {
292
284
  return this.request("GET", "/api/v1/publishing-rules");
293
285
  }
286
+ // Ghost
287
+ async ghostSetup(data) {
288
+ return this.request("POST", "/api/v1/ghost/setup", data);
289
+ }
290
+ async ghostCreatePost(data) {
291
+ return this.request("POST", "/api/v1/ghost/posts", data);
292
+ }
293
+ async ghostUpdatePost(id, data) {
294
+ return this.request("PUT", `/api/v1/ghost/posts/${id}`, data);
295
+ }
296
+ async ghostGetPost(id) {
297
+ return this.request("GET", `/api/v1/ghost/posts/${id}`);
298
+ }
299
+ async ghostListPosts(params) {
300
+ return this.request("GET", "/api/v1/ghost/posts", void 0, params);
301
+ }
302
+ async ghostDeletePost(id) {
303
+ return this.request("DELETE", `/api/v1/ghost/posts/${id}`);
304
+ }
305
+ async ghostPublishPost(id, data) {
306
+ return this.request("POST", `/api/v1/ghost/posts/${id}/publish`, data);
307
+ }
308
+ async ghostSendNewsletter(id, data) {
309
+ return this.request("POST", `/api/v1/ghost/posts/${id}/newsletter`, data);
310
+ }
311
+ async ghostListMembers(params) {
312
+ return this.request("GET", "/api/v1/ghost/members", void 0, params);
313
+ }
314
+ async ghostListNewsletters() {
315
+ return this.request("GET", "/api/v1/ghost/newsletters");
316
+ }
294
317
  // Newsletter Validation
295
318
  async validateNewsletter(data) {
296
319
  return this.request("POST", "/api/v1/newsletters/validate", data);
@@ -303,6 +326,34 @@ var BuzzPosterClient = class {
303
326
  async testBroadcast(id, data) {
304
327
  return this.request("POST", `/api/v1/newsletters/broadcasts/${id}/test`, data);
305
328
  }
329
+ // WordPress
330
+ async wpCreatePost(data) {
331
+ return this.request("POST", "/api/v1/wordpress/posts", data);
332
+ }
333
+ async wpUpdatePost(id, data) {
334
+ return this.request("PUT", `/api/v1/wordpress/posts/${id}`, data);
335
+ }
336
+ async wpGetPost(id) {
337
+ return this.request("GET", `/api/v1/wordpress/posts/${id}`);
338
+ }
339
+ async wpListPosts(params) {
340
+ return this.request("GET", "/api/v1/wordpress/posts", void 0, params);
341
+ }
342
+ async wpDeletePost(id, params) {
343
+ return this.request("DELETE", `/api/v1/wordpress/posts/${id}`, void 0, params);
344
+ }
345
+ async wpUploadMedia(data) {
346
+ return this.request("POST", "/api/v1/wordpress/media", data);
347
+ }
348
+ async wpListCategories() {
349
+ return this.request("GET", "/api/v1/wordpress/categories");
350
+ }
351
+ async wpListTags() {
352
+ return this.request("GET", "/api/v1/wordpress/tags");
353
+ }
354
+ async wpCreatePage(data) {
355
+ return this.request("POST", "/api/v1/wordpress/pages", data);
356
+ }
306
357
  };
307
358
 
308
359
  // src/tools/posts.ts
@@ -310,7 +361,7 @@ import { z } from "zod";
310
361
  function registerPostTools(server2, client2) {
311
362
  server2.tool(
312
363
  "post",
313
- "Publish a post to one or more social platforms. Set confirmed=false to preview first.",
364
+ "Publish a post to one or more social platforms. Always call get_brand_voice and get_audience first to match tone. Always set confirmed=false first to generate a preview \u2014 never publish without explicit user approval.",
314
365
  {
315
366
  content: z.string().optional().describe("The text content of the post"),
316
367
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -404,7 +455,7 @@ Call this tool again with confirmed=true to proceed.`;
404
455
  );
405
456
  server2.tool(
406
457
  "schedule_post",
407
- "Schedule a post for future publication. Set confirmed=false to preview first.",
458
+ "Schedule a post for future publication. Always call get_brand_voice and get_audience first to match tone. Always set confirmed=false first to generate a preview \u2014 never schedule without explicit user approval.",
408
459
  {
409
460
  content: z.string().optional().describe("The text content of the post"),
410
461
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -497,8 +548,7 @@ Call this tool again with confirmed=true to schedule.`;
497
548
  const body = {
498
549
  content: args.content,
499
550
  platforms: args.platforms.map((p) => ({ platform: p })),
500
- publishNow: false,
501
- status: "draft"
551
+ isDraft: true
502
552
  };
503
553
  if (args.media_urls?.length) {
504
554
  body.mediaItems = args.media_urls.map((url) => ({
@@ -511,7 +561,7 @@ Call this tool again with confirmed=true to schedule.`;
511
561
  return {
512
562
  content: [{
513
563
  type: "text",
514
- text: `ERROR: Post was saved with status "${result.status}" instead of "draft". Post ID: ${result.id ?? "unknown"}. Check the Late dashboard immediately.`
564
+ text: `ERROR: Post was saved with status "${result.status}" instead of "draft". Post ID: ${result.id ?? "unknown"}. Check your BuzzPoster dashboard immediately.`
515
565
  }],
516
566
  isError: true
517
567
  };
@@ -841,7 +891,89 @@ This will post a public reply to the review. Call this tool again with confirmed
841
891
 
842
892
  // src/tools/media.ts
843
893
  import { z as z4 } from "zod";
894
+ var IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
895
+ "image/png",
896
+ "image/jpeg",
897
+ "image/gif",
898
+ "image/webp",
899
+ "image/svg+xml"
900
+ ]);
901
+ var MAX_THUMBNAIL_BYTES = 15e4;
902
+ async function fetchImageAsBase64(url) {
903
+ try {
904
+ const res = await fetch(url);
905
+ if (!res.ok) return null;
906
+ const contentType = res.headers.get("content-type")?.split(";")[0]?.trim() ?? "";
907
+ if (!IMAGE_MIME_TYPES.has(contentType)) return null;
908
+ const buffer = Buffer.from(await res.arrayBuffer());
909
+ if (buffer.byteLength > MAX_THUMBNAIL_BYTES) return null;
910
+ return {
911
+ data: buffer.toString("base64"),
912
+ mimeType: contentType
913
+ };
914
+ } catch {
915
+ return null;
916
+ }
917
+ }
844
918
  function registerMediaTools(server2, client2) {
919
+ server2.tool(
920
+ "get_assets",
921
+ "Get brand assets from the media library with visual thumbnails. Returns inline image previews plus full-resolution URLs for use in posts.",
922
+ {
923
+ limit: z4.number().optional().describe("Max number of assets to return with image previews. Default 10, max 20.")
924
+ },
925
+ {
926
+ title: "Get Brand Assets",
927
+ readOnlyHint: true,
928
+ destructiveHint: false,
929
+ idempotentHint: true,
930
+ openWorldHint: false
931
+ },
932
+ async ({ limit }) => {
933
+ const maxItems = Math.min(limit ?? 10, 20);
934
+ const result = await client2.listMedia();
935
+ const items = result?.media ?? [];
936
+ const images = items.filter((m) => IMAGE_MIME_TYPES.has(m.mimeType)).slice(0, maxItems);
937
+ if (images.length === 0) {
938
+ return {
939
+ content: [
940
+ {
941
+ type: "text",
942
+ text: "No image assets found in the media library. Upload images with upload_from_url first."
943
+ }
944
+ ]
945
+ };
946
+ }
947
+ const thumbnails = await Promise.all(
948
+ images.map((img) => fetchImageAsBase64(img.url))
949
+ );
950
+ const content = [];
951
+ content.push({
952
+ type: "text",
953
+ text: `## Brand Assets (${images.length} images)
954
+
955
+ Use the URLs below when attaching images to posts or newsletters.`
956
+ });
957
+ for (let i = 0; i < images.length; i++) {
958
+ const img = images[i];
959
+ const thumb = thumbnails[i];
960
+ content.push({
961
+ type: "text",
962
+ text: `### ${img.filename}
963
+ - **URL:** ${img.url}
964
+ - **Type:** ${img.mimeType} | **Size:** ${(img.size / 1024).toFixed(1)}KB`
965
+ });
966
+ if (thumb) {
967
+ content.push({
968
+ type: "image",
969
+ data: thumb.data,
970
+ mimeType: thumb.mimeType
971
+ });
972
+ }
973
+ }
974
+ return { content };
975
+ }
976
+ );
845
977
  server2.tool(
846
978
  "upload_from_url",
847
979
  "Upload media from a public URL to storage. Returns a CDN URL for use in posts.",
@@ -863,11 +995,9 @@ function registerMediaTools(server2, client2) {
863
995
  filename: args.filename,
864
996
  folder: args.folder
865
997
  });
866
- const item = result;
867
998
  return {
868
999
  content: [
869
- { type: "text", text: JSON.stringify(result, null, 2) },
870
- ...item.type === "image" && item.url && item.mimeType && !item.mimeType.startsWith("video/") ? [{ type: "image", url: item.url, mimeType: item.mimeType }] : []
1000
+ { type: "text", text: JSON.stringify(result, null, 2) }
871
1001
  ]
872
1002
  };
873
1003
  }
@@ -885,12 +1015,9 @@ function registerMediaTools(server2, client2) {
885
1015
  },
886
1016
  async () => {
887
1017
  const result = await client2.listMedia();
888
- const media = Array.isArray(result) ? result : result?.media ?? [];
889
- const imageBlocks = media.filter((m) => m.type === "image" && m.url && m.mimeType && !m.mimeType.startsWith("video/")).map((m) => ({ type: "image", url: m.url, mimeType: m.mimeType }));
890
1018
  return {
891
1019
  content: [
892
- { type: "text", text: JSON.stringify(result, null, 2) },
893
- ...imageBlocks
1020
+ { type: "text", text: JSON.stringify(result, null, 2) }
894
1021
  ]
895
1022
  };
896
1023
  }
@@ -928,7 +1055,37 @@ This will permanently delete this media file. Call this tool again with confirme
928
1055
 
929
1056
  // src/tools/newsletter.ts
930
1057
  import { z as z5 } from "zod";
1058
+ import {
1059
+ registerAppResource,
1060
+ registerAppTool,
1061
+ RESOURCE_MIME_TYPE
1062
+ } from "@modelcontextprotocol/ext-apps/server";
1063
+ import * as fs from "fs";
1064
+ import * as path from "path";
1065
+ import { fileURLToPath } from "url";
1066
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
1067
+ var NEWSLETTER_STUDIO_RESOURCE = "ui://newsletter-studio/app.html";
931
1068
  function registerNewsletterTools(server2, client2, options = {}) {
1069
+ registerAppResource(
1070
+ server2,
1071
+ NEWSLETTER_STUDIO_RESOURCE,
1072
+ NEWSLETTER_STUDIO_RESOURCE,
1073
+ { mimeType: RESOURCE_MIME_TYPE },
1074
+ async () => {
1075
+ const distDir = __dirname.endsWith("dist") ? __dirname : path.resolve(__dirname, "..", "dist");
1076
+ const htmlPath = path.join(distDir, "newsletter-studio.html");
1077
+ const html = fs.readFileSync(htmlPath, "utf-8");
1078
+ return {
1079
+ contents: [
1080
+ {
1081
+ uri: NEWSLETTER_STUDIO_RESOURCE,
1082
+ mimeType: RESOURCE_MIME_TYPE,
1083
+ text: html
1084
+ }
1085
+ ]
1086
+ };
1087
+ }
1088
+ );
932
1089
  server2.tool(
933
1090
  "list_subscribers",
934
1091
  "List email subscribers from the connected ESP.",
@@ -979,32 +1136,33 @@ function registerNewsletterTools(server2, client2, options = {}) {
979
1136
  };
980
1137
  }
981
1138
  );
982
- server2.tool(
1139
+ registerAppTool(
1140
+ server2,
983
1141
  "create_newsletter",
984
- "Push a newsletter to the user's ESP as a draft. Show an HTML preview to the user before calling this.",
985
- {
986
- subject: z5.string().describe("Email subject line"),
987
- content: z5.string().describe("HTML content of the newsletter"),
988
- preview_text: z5.string().optional().describe("Preview text shown in email clients")
989
- },
990
1142
  {
991
1143
  title: "Create Newsletter Draft",
992
- readOnlyHint: false,
993
- destructiveHint: false,
994
- idempotentHint: false,
995
- openWorldHint: true
1144
+ description: "Push a newsletter to the user's ESP as a draft. Always call get_brand_voice first. Show an HTML preview to the user before calling this. Use table-based layouts, inline CSS only, 600px max width, email-safe fonts only (Arial, Helvetica, Georgia, Verdana), all images must use absolute URLs, keep under 102KB to avoid Gmail clipping. Optionally pass template_id to use an ESP-native template. Use list_esp_templates to discover available templates.",
1145
+ inputSchema: {
1146
+ subject: z5.string().describe("Email subject line"),
1147
+ content: z5.string().describe("HTML content of the newsletter"),
1148
+ preview_text: z5.string().optional().describe("Preview text shown in email clients"),
1149
+ template_id: z5.string().optional().describe(
1150
+ "ESP-native template ID. For Kit: email_template_id (integer as string). For Beehiiv: post_template_id. Use list_esp_templates to find available templates."
1151
+ )
1152
+ },
1153
+ _meta: { ui: { resourceUri: NEWSLETTER_STUDIO_RESOURCE } }
996
1154
  },
997
1155
  async (args) => {
998
- const result = await client2.createBroadcast({
1156
+ const payload = {
999
1157
  subject: args.subject,
1000
1158
  content: args.content,
1001
1159
  previewText: args.preview_text
1002
- });
1160
+ };
1161
+ if (args.template_id) payload.templateId = args.template_id;
1162
+ const result = await client2.createBroadcast(payload);
1003
1163
  return {
1004
1164
  content: [
1005
- { type: "text", text: JSON.stringify(result, null, 2) },
1006
- ...args.content ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
1007
- ${args.content}` }] : []
1165
+ { type: "text", text: JSON.stringify(result, null, 2) }
1008
1166
  ]
1009
1167
  };
1010
1168
  }
@@ -1033,9 +1191,7 @@ ${args.content}` }] : []
1033
1191
  const result = await client2.updateBroadcast(args.broadcast_id, data);
1034
1192
  return {
1035
1193
  content: [
1036
- { type: "text", text: JSON.stringify(result, null, 2) },
1037
- ...args.content ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
1038
- ${args.content}` }] : []
1194
+ { type: "text", text: JSON.stringify(result, null, 2) }
1039
1195
  ]
1040
1196
  };
1041
1197
  }
@@ -1043,7 +1199,7 @@ ${args.content}` }] : []
1043
1199
  if (options.allowDirectSend) {
1044
1200
  server2.tool(
1045
1201
  "send_newsletter",
1046
- "Send a newsletter to subscribers. This action cannot be undone. Set confirmed=false to preview first.",
1202
+ "Send a newsletter to subscribers. This action cannot be undone. Always set confirmed=false first \u2014 never send without explicit user approval.",
1047
1203
  {
1048
1204
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to send"),
1049
1205
  confirmed: z5.boolean().default(false).describe(
@@ -1107,7 +1263,7 @@ Call this tool again with confirmed=true to send.`;
1107
1263
  }
1108
1264
  server2.tool(
1109
1265
  "schedule_newsletter",
1110
- "Schedule a newsletter for future send. Set confirmed=false to preview first.",
1266
+ "Schedule a newsletter for future send. Always set confirmed=false first \u2014 never schedule without explicit user approval.",
1111
1267
  {
1112
1268
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to schedule"),
1113
1269
  scheduled_for: z5.string().describe(
@@ -1272,10 +1428,10 @@ import { z as z6 } from "zod";
1272
1428
  function registerRssTools(server2, client2) {
1273
1429
  server2.tool(
1274
1430
  "fetch_feed",
1275
- "Fetch and parse entries from an RSS or Atom feed URL.",
1431
+ "Fetch and parse all available entries from an RSS or Atom feed URL. Returns up to 100 entries by default.",
1276
1432
  {
1277
1433
  url: z6.string().describe("The RSS/Atom feed URL to fetch"),
1278
- limit: z6.number().optional().describe("Maximum number of entries to return (default 10, max 100)")
1434
+ limit: z6.number().optional().describe("Maximum number of entries to return (default 100)")
1279
1435
  },
1280
1436
  {
1281
1437
  title: "Fetch RSS Feed",
@@ -1346,7 +1502,7 @@ import { z as z7 } from "zod";
1346
1502
  function registerBrandVoiceTools(server2, client2) {
1347
1503
  server2.tool(
1348
1504
  "get_brand_voice",
1349
- "Get the brand voice profile and writing rules. Call before creating content.",
1505
+ "Get the brand voice profile, writing rules, and platform-specific examples. Call this before creating any content to match the user's tone and style.",
1350
1506
  {},
1351
1507
  {
1352
1508
  title: "Get Brand Voice",
@@ -1391,6 +1547,18 @@ function registerBrandVoiceTools(server2, client2) {
1391
1547
  lines.push("(none)");
1392
1548
  }
1393
1549
  lines.push("");
1550
+ lines.push("### Platform-Specific Examples");
1551
+ if (voice.platformExamples && Object.keys(voice.platformExamples).length > 0) {
1552
+ for (const [platform, examples] of Object.entries(voice.platformExamples)) {
1553
+ lines.push(`**${platform}:**`);
1554
+ examples.forEach((ex, i) => {
1555
+ lines.push(` ${i + 1}. ${ex}`);
1556
+ });
1557
+ }
1558
+ } else {
1559
+ lines.push("(none)");
1560
+ }
1561
+ lines.push("");
1394
1562
  lines.push("### Example Posts");
1395
1563
  if (voice.examplePosts && voice.examplePosts.length > 0) {
1396
1564
  voice.examplePosts.forEach((post, i) => {
@@ -1400,6 +1568,9 @@ function registerBrandVoiceTools(server2, client2) {
1400
1568
  lines.push("(none)");
1401
1569
  }
1402
1570
  lines.push("");
1571
+ if (voice.updatedAt) {
1572
+ lines.push(`_Last updated: ${voice.updatedAt}_`);
1573
+ }
1403
1574
  return {
1404
1575
  content: [{ type: "text", text: lines.join("\n") }]
1405
1576
  };
@@ -1428,7 +1599,8 @@ function registerBrandVoiceTools(server2, client2) {
1428
1599
  dos: z7.array(z7.string().max(200)).max(15).optional().describe("Writing rules to follow. Send the FULL list, not just additions."),
1429
1600
  donts: z7.array(z7.string().max(200)).max(15).optional().describe("Things to avoid. Send the FULL list, not just additions."),
1430
1601
  platform_rules: z7.record(z7.string().max(300)).optional().describe('Per-platform writing guidelines, e.g. { "twitter": "Keep under 200 chars" }'),
1431
- example_posts: z7.array(z7.string().max(500)).max(5).optional().describe("Example posts that demonstrate the desired voice"),
1602
+ platform_examples: z7.record(z7.array(z7.string().max(500)).max(3)).optional().describe('Per-platform example posts, e.g. { "twitter": ["Example tweet 1", "Example tweet 2"], "linkedin": ["Example post"] }'),
1603
+ example_posts: z7.array(z7.string().max(500)).max(5).optional().describe("General example posts that demonstrate the desired voice"),
1432
1604
  confirmed: z7.boolean().default(false).describe(
1433
1605
  "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
1434
1606
  )
@@ -1459,13 +1631,26 @@ function registerBrandVoiceTools(server2, client2) {
1459
1631
  }
1460
1632
  payload.platformRules = args.platform_rules;
1461
1633
  }
1634
+ if (args.platform_examples !== void 0) {
1635
+ const entries = Object.entries(args.platform_examples);
1636
+ if (entries.length > 6) {
1637
+ return {
1638
+ content: [{
1639
+ type: "text",
1640
+ text: "Maximum 6 platforms for examples allowed."
1641
+ }],
1642
+ isError: true
1643
+ };
1644
+ }
1645
+ payload.platformExamples = args.platform_examples;
1646
+ }
1462
1647
  if (args.example_posts !== void 0) payload.examplePosts = args.example_posts;
1463
1648
  if (Object.keys(payload).length === 0) {
1464
1649
  return {
1465
1650
  content: [
1466
1651
  {
1467
1652
  type: "text",
1468
- text: "No fields provided. Specify at least one field to update (name, description, dos, donts, platform_rules, example_posts)."
1653
+ text: "No fields provided. Specify at least one field to update (name, description, dos, donts, platform_rules, platform_examples, example_posts)."
1469
1654
  }
1470
1655
  ],
1471
1656
  isError: true
@@ -1508,6 +1693,17 @@ function registerBrandVoiceTools(server2, client2) {
1508
1693
  }
1509
1694
  lines2.push("");
1510
1695
  }
1696
+ if (payload.platformExamples) {
1697
+ const examples = payload.platformExamples;
1698
+ lines2.push(`### Platform-Specific Examples (${Object.keys(examples).length} platforms)`);
1699
+ for (const [platform, exList] of Object.entries(examples)) {
1700
+ lines2.push(`**${platform}:**`);
1701
+ exList.forEach((ex, i) => {
1702
+ lines2.push(` ${i + 1}. ${ex}`);
1703
+ });
1704
+ }
1705
+ lines2.push("");
1706
+ }
1511
1707
  if (payload.examplePosts) {
1512
1708
  const posts = payload.examplePosts;
1513
1709
  lines2.push(`### Example Posts (${posts.length})`);
@@ -1532,6 +1728,7 @@ function registerBrandVoiceTools(server2, client2) {
1532
1728
  if (payload.dos) summary.push(`${payload.dos.length} do's`);
1533
1729
  if (payload.donts) summary.push(`${payload.donts.length} don'ts`);
1534
1730
  if (payload.platformRules) summary.push(`${Object.keys(payload.platformRules).length} platform rules`);
1731
+ if (payload.platformExamples) summary.push(`${Object.keys(payload.platformExamples).length} platform example sets`);
1535
1732
  if (payload.examplePosts) summary.push(`${payload.examplePosts.length} example posts`);
1536
1733
  lines.push(`**Updated:** ${summary.join(", ")}`);
1537
1734
  return {
@@ -1541,15 +1738,70 @@ function registerBrandVoiceTools(server2, client2) {
1541
1738
  );
1542
1739
  }
1543
1740
 
1544
- // src/tools/knowledge.ts
1741
+ // src/tools/audience.ts
1545
1742
  import { z as z8 } from "zod";
1546
- function registerKnowledgeTools(server2, client2) {
1743
+ function formatAudience(audience) {
1744
+ const lines = [];
1745
+ lines.push(`## ${audience.name}${audience.isDefault ? " (default)" : ""}`);
1746
+ lines.push("");
1747
+ if (audience.description) {
1748
+ lines.push("### Description");
1749
+ lines.push(audience.description);
1750
+ lines.push("");
1751
+ }
1752
+ if (audience.demographics) {
1753
+ lines.push("### Demographics");
1754
+ lines.push(audience.demographics);
1755
+ lines.push("");
1756
+ }
1757
+ if (audience.painPoints && audience.painPoints.length > 0) {
1758
+ lines.push("### Pain Points");
1759
+ for (const point of audience.painPoints) {
1760
+ lines.push(`- ${point}`);
1761
+ }
1762
+ lines.push("");
1763
+ }
1764
+ if (audience.motivations && audience.motivations.length > 0) {
1765
+ lines.push("### Motivations");
1766
+ for (const motivation of audience.motivations) {
1767
+ lines.push(`- ${motivation}`);
1768
+ }
1769
+ lines.push("");
1770
+ }
1771
+ if (audience.platforms && audience.platforms.length > 0) {
1772
+ lines.push("### Active Platforms");
1773
+ lines.push(audience.platforms.join(", "));
1774
+ lines.push("");
1775
+ }
1776
+ if (audience.toneNotes) {
1777
+ lines.push("### Tone Notes");
1778
+ lines.push(audience.toneNotes);
1779
+ lines.push("");
1780
+ }
1781
+ if (audience.contentPreferences) {
1782
+ lines.push("### Content Preferences");
1783
+ lines.push(audience.contentPreferences);
1784
+ lines.push("");
1785
+ }
1786
+ return lines.join("\n");
1787
+ }
1788
+ var audienceFields = {
1789
+ name: z8.string().max(100).describe("Audience name (must be unique per account)"),
1790
+ description: z8.string().max(500).optional().describe("Brief description of who this audience is"),
1791
+ demographics: z8.string().max(500).optional().describe("Demographic details"),
1792
+ pain_points: z8.array(z8.string().max(200)).max(10).optional().describe("Problems the audience faces. Send the FULL list."),
1793
+ motivations: z8.array(z8.string().max(200)).max(10).optional().describe("Goals and desires. Send the FULL list."),
1794
+ preferred_platforms: z8.array(z8.string()).max(6).optional().describe("Social platforms the audience is most active on. Send the FULL list."),
1795
+ tone_notes: z8.string().max(500).optional().describe("How to speak to this audience"),
1796
+ content_preferences: z8.string().max(500).optional().describe("What content formats and topics resonate")
1797
+ };
1798
+ function registerAudienceTools(server2, client2) {
1547
1799
  server2.tool(
1548
- "get_knowledge_base",
1549
- "Get all items from the knowledge base.",
1800
+ "list_audiences",
1801
+ "List all audience profiles. Call this to see which audiences are available before creating content.",
1550
1802
  {},
1551
1803
  {
1552
- title: "Get Knowledge Base",
1804
+ title: "List Audiences",
1553
1805
  readOnlyHint: true,
1554
1806
  destructiveHint: false,
1555
1807
  idempotentHint: true,
@@ -1557,230 +1809,171 @@ function registerKnowledgeTools(server2, client2) {
1557
1809
  },
1558
1810
  async () => {
1559
1811
  try {
1560
- const data = await client2.listKnowledge();
1561
- const items = data.items ?? [];
1562
- if (items.length === 0) {
1812
+ const audiences = await client2.listAudiences();
1813
+ if (!audiences || audiences.length === 0) {
1563
1814
  return {
1564
1815
  content: [
1565
1816
  {
1566
1817
  type: "text",
1567
- text: "The knowledge base is empty. The customer can add reference material at their BuzzPoster dashboard."
1818
+ text: "No audience profiles configured. Use create_audience to add one."
1568
1819
  }
1569
1820
  ]
1570
1821
  };
1571
1822
  }
1572
- const totalChars = items.reduce(
1573
- (sum, item) => sum + item.content.length,
1574
- 0
1575
- );
1576
- const shouldTruncate = totalChars > 1e4;
1577
1823
  const lines = [];
1578
- lines.push(`## Knowledge Base (${items.length} items)`);
1824
+ lines.push(`## Audience Profiles (${audiences.length})`);
1579
1825
  lines.push("");
1580
- for (const item of items) {
1581
- lines.push(`### ${item.title}`);
1582
- if (item.tags && item.tags.length > 0) {
1583
- lines.push(`Tags: ${item.tags.join(", ")}`);
1584
- }
1585
- if (shouldTruncate) {
1586
- lines.push(item.content.slice(0, 500) + " [truncated]");
1587
- } else {
1588
- lines.push(item.content);
1589
- }
1590
- lines.push("");
1826
+ for (const a of audiences) {
1827
+ lines.push(`- **${a.name}**${a.isDefault ? " (default)" : ""}: ${a.description || "(no description)"}`);
1591
1828
  }
1829
+ lines.push("");
1830
+ lines.push("Use get_audience with a name to see the full profile.");
1592
1831
  return {
1593
1832
  content: [{ type: "text", text: lines.join("\n") }]
1594
1833
  };
1595
1834
  } catch (error) {
1596
1835
  const message = error instanceof Error ? error.message : "Unknown error";
1597
- throw new Error(`Failed to get knowledge base: ${message}`);
1836
+ throw new Error(`Failed to list audiences: ${message}`);
1598
1837
  }
1599
1838
  }
1600
1839
  );
1601
1840
  server2.tool(
1602
- "search_knowledge",
1603
- "Search the knowledge base by tag or keyword.",
1841
+ "get_audience",
1842
+ "Get a specific audience profile by name. If no name is given, returns the default audience. Call before creating content to understand who you're writing for.",
1604
1843
  {
1605
- query: z8.string().describe(
1606
- "Search query - matches against tags first, then falls back to text search on title and content"
1607
- )
1844
+ name: z8.string().optional().describe("Audience name to look up. If omitted, returns the default audience.")
1608
1845
  },
1609
1846
  {
1610
- title: "Search Knowledge Base",
1847
+ title: "Get Audience Profile",
1611
1848
  readOnlyHint: true,
1612
1849
  destructiveHint: false,
1613
1850
  idempotentHint: true,
1614
1851
  openWorldHint: false
1615
1852
  },
1616
- async ({ query }) => {
1853
+ async ({ name }) => {
1617
1854
  try {
1618
- const data = await client2.listKnowledge({ tag: query });
1619
- const items = data.items ?? [];
1620
- if (items.length === 0) {
1855
+ let audience;
1856
+ if (name) {
1857
+ audience = await client2.getAudienceByName(name);
1858
+ } else {
1859
+ audience = await client2.getDefaultAudience();
1860
+ }
1861
+ return {
1862
+ content: [{ type: "text", text: formatAudience(audience) }]
1863
+ };
1864
+ } catch (error) {
1865
+ const message = error instanceof Error ? error.message : "Unknown error";
1866
+ if (message.includes("404") || message.includes("No audience") || message.includes("not found")) {
1621
1867
  return {
1622
1868
  content: [
1623
1869
  {
1624
1870
  type: "text",
1625
- text: `No knowledge base items found matching "${query}".`
1871
+ text: name ? `No audience named "${name}" found. Use list_audiences to see available profiles.` : "No audience profiles configured. Use create_audience to add one."
1626
1872
  }
1627
1873
  ]
1628
1874
  };
1629
1875
  }
1630
- const lines = [];
1631
- lines.push(
1632
- `## Knowledge Base Results for "${query}" (${items.length} items)`
1633
- );
1634
- lines.push("");
1635
- for (const item of items) {
1636
- lines.push(`### ${item.title}`);
1637
- if (item.tags && item.tags.length > 0) {
1638
- lines.push(`Tags: ${item.tags.join(", ")}`);
1639
- }
1640
- lines.push(item.content);
1641
- lines.push("");
1642
- }
1643
- return {
1644
- content: [{ type: "text", text: lines.join("\n") }]
1645
- };
1646
- } catch (error) {
1647
- const message = error instanceof Error ? error.message : "Unknown error";
1648
- throw new Error(`Failed to search knowledge base: ${message}`);
1876
+ throw error;
1649
1877
  }
1650
1878
  }
1651
1879
  );
1652
1880
  server2.tool(
1653
- "add_knowledge",
1654
- "Add reference material to the knowledge base. Include tags for categorization.",
1881
+ "create_audience",
1882
+ "Create a new audience profile. Set confirmed=false to preview first.",
1655
1883
  {
1656
- title: z8.string().describe("Title for the knowledge item"),
1657
- content: z8.string().describe("The content/text to save"),
1658
- tags: z8.array(z8.string()).optional().describe("Optional tags for categorization")
1884
+ ...audienceFields,
1885
+ is_default: z8.boolean().optional().describe("Set as the default audience profile"),
1886
+ confirmed: z8.boolean().default(false).describe("Set to true to confirm and save. If false, returns a preview.")
1659
1887
  },
1660
1888
  {
1661
- title: "Add Knowledge Item",
1889
+ title: "Create Audience",
1662
1890
  readOnlyHint: false,
1663
1891
  destructiveHint: false,
1664
1892
  idempotentHint: false,
1665
1893
  openWorldHint: false
1666
1894
  },
1667
- async ({ title, content, tags }) => {
1668
- try {
1669
- await client2.createKnowledge({
1670
- title,
1671
- content,
1672
- sourceType: "text",
1673
- tags: tags ?? []
1674
- });
1675
- return {
1676
- content: [
1677
- {
1678
- type: "text",
1679
- text: `Added "${title}" to the knowledge base.`
1680
- }
1681
- ]
1682
- };
1683
- } catch (error) {
1684
- const message = error instanceof Error ? error.message : "Unknown error";
1685
- throw new Error(`Failed to add knowledge item: ${message}`);
1686
- }
1687
- }
1688
- );
1689
- }
1690
-
1691
- // src/tools/audience.ts
1692
- import { z as z9 } from "zod";
1693
- function registerAudienceTools(server2, client2) {
1694
- server2.tool(
1695
- "get_audience",
1696
- "Get the target audience profile. Call before creating content.",
1697
- {},
1698
- {
1699
- title: "Get Audience Profile",
1700
- readOnlyHint: true,
1701
- destructiveHint: false,
1702
- idempotentHint: true,
1703
- openWorldHint: false
1704
- },
1705
- async () => {
1706
- try {
1707
- const audience = await client2.getDefaultAudience();
1895
+ async (args) => {
1896
+ const payload = { name: args.name };
1897
+ if (args.description !== void 0) payload.description = args.description;
1898
+ if (args.demographics !== void 0) payload.demographics = args.demographics;
1899
+ if (args.pain_points !== void 0) payload.painPoints = args.pain_points;
1900
+ if (args.motivations !== void 0) payload.motivations = args.motivations;
1901
+ if (args.preferred_platforms !== void 0) payload.platforms = args.preferred_platforms;
1902
+ if (args.tone_notes !== void 0) payload.toneNotes = args.tone_notes;
1903
+ if (args.content_preferences !== void 0) payload.contentPreferences = args.content_preferences;
1904
+ if (args.is_default !== void 0) payload.isDefault = args.is_default;
1905
+ if (args.confirmed !== true) {
1708
1906
  const lines = [];
1709
- lines.push(`## Target Audience: ${audience.name}`);
1907
+ lines.push("## New Audience Preview");
1710
1908
  lines.push("");
1711
- if (audience.description) {
1909
+ lines.push(`**Name:** ${args.name}`);
1910
+ if (args.is_default) lines.push("**Default:** yes");
1911
+ lines.push("");
1912
+ if (payload.description) {
1712
1913
  lines.push("### Description");
1713
- lines.push(audience.description);
1914
+ lines.push(String(payload.description));
1714
1915
  lines.push("");
1715
1916
  }
1716
- if (audience.demographics) {
1917
+ if (payload.demographics) {
1717
1918
  lines.push("### Demographics");
1718
- lines.push(audience.demographics);
1919
+ lines.push(String(payload.demographics));
1719
1920
  lines.push("");
1720
1921
  }
1721
- if (audience.painPoints && audience.painPoints.length > 0) {
1722
- lines.push("### Pain Points");
1723
- for (const point of audience.painPoints) {
1724
- lines.push(`- ${point}`);
1725
- }
1922
+ if (payload.painPoints) {
1923
+ lines.push(`### Pain Points (${payload.painPoints.length})`);
1924
+ for (const p of payload.painPoints) lines.push(`- ${p}`);
1726
1925
  lines.push("");
1727
1926
  }
1728
- if (audience.motivations && audience.motivations.length > 0) {
1729
- lines.push("### Motivations");
1730
- for (const motivation of audience.motivations) {
1731
- lines.push(`- ${motivation}`);
1732
- }
1927
+ if (payload.motivations) {
1928
+ lines.push(`### Motivations (${payload.motivations.length})`);
1929
+ for (const m of payload.motivations) lines.push(`- ${m}`);
1733
1930
  lines.push("");
1734
1931
  }
1735
- if (audience.platforms && audience.platforms.length > 0) {
1736
- lines.push("### Active Platforms");
1737
- lines.push(audience.platforms.join(", "));
1932
+ if (payload.platforms) {
1933
+ lines.push(`### Platforms`);
1934
+ lines.push(payload.platforms.join(", "));
1738
1935
  lines.push("");
1739
1936
  }
1740
- if (audience.toneNotes) {
1937
+ if (payload.toneNotes) {
1741
1938
  lines.push("### Tone Notes");
1742
- lines.push(audience.toneNotes);
1939
+ lines.push(String(payload.toneNotes));
1743
1940
  lines.push("");
1744
1941
  }
1745
- if (audience.contentPreferences) {
1942
+ if (payload.contentPreferences) {
1746
1943
  lines.push("### Content Preferences");
1747
- lines.push(audience.contentPreferences);
1944
+ lines.push(String(payload.contentPreferences));
1748
1945
  lines.push("");
1749
1946
  }
1750
- return {
1751
- content: [{ type: "text", text: lines.join("\n") }]
1752
- };
1753
- } catch (error) {
1754
- const message = error instanceof Error ? error.message : "Unknown error";
1755
- if (message.includes("404") || message.includes("No audience")) {
1756
- return {
1757
- content: [
1758
- {
1759
- type: "text",
1760
- text: "No audience profile has been configured yet. The customer can set one up at their BuzzPoster dashboard under Audiences."
1761
- }
1762
- ]
1763
- };
1764
- }
1765
- throw error;
1947
+ lines.push("---");
1948
+ lines.push("Call this tool again with **confirmed=true** to save.");
1949
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1766
1950
  }
1951
+ const result = await client2.createAudience(payload);
1952
+ return {
1953
+ content: [
1954
+ {
1955
+ type: "text",
1956
+ text: `Audience "${result.name}" created successfully (ID: ${result.id}).`
1957
+ }
1958
+ ]
1959
+ };
1767
1960
  }
1768
1961
  );
1769
1962
  server2.tool(
1770
1963
  "update_audience",
1771
- "Update the audience profile. Set confirmed=false to preview first.",
1772
- {
1773
- name: z9.string().max(100).optional().describe("Audience profile name. Required when creating a new audience."),
1774
- description: z9.string().max(500).optional().describe("Brief description of who this audience is"),
1775
- demographics: z9.string().max(500).optional().describe("Demographic details"),
1776
- pain_points: z9.array(z9.string().max(200)).max(10).optional().describe("Problems the audience faces. Send the FULL list."),
1777
- motivations: z9.array(z9.string().max(200)).max(10).optional().describe("Goals and desires. Send the FULL list."),
1778
- preferred_platforms: z9.array(z9.string()).max(6).optional().describe("Social platforms the audience is most active on. Send the FULL list."),
1779
- tone_notes: z9.string().max(500).optional().describe("How to speak to this audience"),
1780
- content_preferences: z9.string().max(500).optional().describe("What content formats and topics resonate"),
1781
- confirmed: z9.boolean().default(false).describe(
1782
- "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
1783
- )
1964
+ "Update an existing audience profile by name. Set confirmed=false to preview first.",
1965
+ {
1966
+ audience_name: z8.string().describe("Name of the audience to update"),
1967
+ name: z8.string().max(100).optional().describe("New name for the audience"),
1968
+ description: z8.string().max(500).optional().describe("Brief description of who this audience is"),
1969
+ demographics: z8.string().max(500).optional().describe("Demographic details"),
1970
+ pain_points: z8.array(z8.string().max(200)).max(10).optional().describe("Problems the audience faces. Send the FULL list."),
1971
+ motivations: z8.array(z8.string().max(200)).max(10).optional().describe("Goals and desires. Send the FULL list."),
1972
+ preferred_platforms: z8.array(z8.string()).max(6).optional().describe("Social platforms the audience is most active on. Send the FULL list."),
1973
+ tone_notes: z8.string().max(500).optional().describe("How to speak to this audience"),
1974
+ content_preferences: z8.string().max(500).optional().describe("What content formats and topics resonate"),
1975
+ is_default: z8.boolean().optional().describe("Set as the default audience profile"),
1976
+ confirmed: z8.boolean().default(false).describe("Set to true to confirm and save. If false, returns a preview.")
1784
1977
  },
1785
1978
  {
1786
1979
  title: "Update Audience",
@@ -1790,6 +1983,20 @@ function registerAudienceTools(server2, client2) {
1790
1983
  openWorldHint: false
1791
1984
  },
1792
1985
  async (args) => {
1986
+ let target;
1987
+ try {
1988
+ target = await client2.getAudienceByName(args.audience_name);
1989
+ } catch {
1990
+ return {
1991
+ content: [
1992
+ {
1993
+ type: "text",
1994
+ text: `Audience "${args.audience_name}" not found. Use list_audiences to see available profiles.`
1995
+ }
1996
+ ],
1997
+ isError: true
1998
+ };
1999
+ }
1793
2000
  const payload = {};
1794
2001
  if (args.name !== void 0) payload.name = args.name;
1795
2002
  if (args.description !== void 0) payload.description = args.description;
@@ -1799,661 +2006,187 @@ function registerAudienceTools(server2, client2) {
1799
2006
  if (args.preferred_platforms !== void 0) payload.platforms = args.preferred_platforms;
1800
2007
  if (args.tone_notes !== void 0) payload.toneNotes = args.tone_notes;
1801
2008
  if (args.content_preferences !== void 0) payload.contentPreferences = args.content_preferences;
2009
+ if (args.is_default !== void 0) payload.isDefault = args.is_default;
1802
2010
  if (Object.keys(payload).length === 0) {
1803
2011
  return {
1804
- content: [
1805
- {
1806
- type: "text",
1807
- text: "No fields provided. Specify at least one field to update."
1808
- }
1809
- ],
1810
- isError: true
1811
- };
1812
- }
1813
- let targetId;
1814
- let isCreating = false;
1815
- try {
1816
- const defaultAudience = await client2.getDefaultAudience();
1817
- targetId = String(defaultAudience.id);
1818
- } catch {
1819
- isCreating = true;
1820
- }
1821
- if (isCreating && !payload.name) {
1822
- return {
1823
- content: [
1824
- {
1825
- type: "text",
1826
- text: "No audience exists yet. Provide a **name** to create one (e.g. name: 'SaaS Founders')."
1827
- }
1828
- ],
2012
+ content: [{ type: "text", text: "No fields provided to update." }],
1829
2013
  isError: true
1830
2014
  };
1831
2015
  }
1832
2016
  if (args.confirmed !== true) {
1833
- const lines2 = [];
1834
- lines2.push(isCreating ? "## New Audience Preview" : "## Audience Update Preview");
1835
- lines2.push("");
2017
+ const lines = [];
2018
+ lines.push(`## Update Audience "${args.audience_name}" Preview`);
2019
+ lines.push("");
1836
2020
  if (payload.name) {
1837
- lines2.push(`**Name:** ${payload.name}`);
1838
- lines2.push("");
2021
+ lines.push(`**New Name:** ${payload.name}`);
2022
+ lines.push("");
1839
2023
  }
1840
2024
  if (payload.description) {
1841
- lines2.push("### Description");
1842
- lines2.push(String(payload.description));
1843
- lines2.push("");
2025
+ lines.push("### Description");
2026
+ lines.push(String(payload.description));
2027
+ lines.push("");
1844
2028
  }
1845
2029
  if (payload.demographics) {
1846
- lines2.push("### Demographics");
1847
- lines2.push(String(payload.demographics));
1848
- lines2.push("");
2030
+ lines.push("### Demographics");
2031
+ lines.push(String(payload.demographics));
2032
+ lines.push("");
1849
2033
  }
1850
2034
  if (payload.painPoints) {
1851
- const points = payload.painPoints;
1852
- lines2.push(`### Pain Points (${points.length})`);
1853
- for (const point of points) {
1854
- lines2.push(`- ${point}`);
1855
- }
1856
- lines2.push("");
2035
+ lines.push(`### Pain Points (${payload.painPoints.length})`);
2036
+ for (const p of payload.painPoints) lines.push(`- ${p}`);
2037
+ lines.push("");
1857
2038
  }
1858
2039
  if (payload.motivations) {
1859
- const motivations = payload.motivations;
1860
- lines2.push(`### Motivations (${motivations.length})`);
1861
- for (const motivation of motivations) {
1862
- lines2.push(`- ${motivation}`);
1863
- }
1864
- lines2.push("");
2040
+ lines.push(`### Motivations (${payload.motivations.length})`);
2041
+ for (const m of payload.motivations) lines.push(`- ${m}`);
2042
+ lines.push("");
1865
2043
  }
1866
2044
  if (payload.platforms) {
1867
- const platforms = payload.platforms;
1868
- lines2.push(`### Preferred Platforms (${platforms.length})`);
1869
- lines2.push(platforms.join(", "));
1870
- lines2.push("");
2045
+ lines.push(`### Platforms`);
2046
+ lines.push(payload.platforms.join(", "));
2047
+ lines.push("");
1871
2048
  }
1872
2049
  if (payload.toneNotes) {
1873
- lines2.push("### Tone Notes");
1874
- lines2.push(String(payload.toneNotes));
1875
- lines2.push("");
1876
- }
1877
- if (payload.contentPreferences) {
1878
- lines2.push("### Content Preferences");
1879
- lines2.push(String(payload.contentPreferences));
1880
- lines2.push("");
1881
- }
1882
- lines2.push("---");
1883
- lines2.push("Call this tool again with **confirmed=true** to save these changes.");
1884
- return {
1885
- content: [{ type: "text", text: lines2.join("\n") }]
1886
- };
1887
- }
1888
- let result;
1889
- if (isCreating) {
1890
- if (payload.isDefault === void 0) payload.isDefault = true;
1891
- result = await client2.createAudience(payload);
1892
- } else {
1893
- result = await client2.updateAudience(targetId, payload);
1894
- }
1895
- const lines = [];
1896
- lines.push(`Audience "${result.name}" has been ${isCreating ? "created" : "updated"} successfully.`);
1897
- lines.push("");
1898
- const summary = [];
1899
- if (payload.name) summary.push("name");
1900
- if (payload.description) summary.push("description");
1901
- if (payload.demographics) summary.push("demographics");
1902
- if (payload.painPoints) summary.push(`${payload.painPoints.length} pain points`);
1903
- if (payload.motivations) summary.push(`${payload.motivations.length} motivations`);
1904
- if (payload.platforms) summary.push(`${payload.platforms.length} platforms`);
1905
- if (payload.toneNotes) summary.push("tone notes");
1906
- if (payload.contentPreferences) summary.push("content preferences");
1907
- lines.push(`**${isCreating ? "Created with" : "Updated"}:** ${summary.join(", ")}`);
1908
- return {
1909
- content: [{ type: "text", text: lines.join("\n") }]
1910
- };
1911
- }
1912
- );
1913
- }
1914
-
1915
- // src/tools/newsletter-template.ts
1916
- import { z as z10 } from "zod";
1917
- var NEWSLETTER_CRAFT_GUIDE = `## Newsletter Craft Guide
1918
-
1919
- Follow these rules when drafting:
1920
-
1921
- ### HTML Email Rules
1922
- - Use table-based layouts, inline CSS only, 600px max width
1923
- - Email-safe fonts only: Arial, Helvetica, Georgia, Verdana
1924
- - Keep total email under 102KB (Gmail clips larger)
1925
- - All images must use absolute URLs hosted on the customer's R2 CDN
1926
- - Include alt text on every image
1927
- - Use role="presentation" on layout tables
1928
- - No JavaScript, no forms, no CSS grid/flexbox, no background images
1929
-
1930
- ### Structure
1931
- - Subject line: 6-10 words, specific not clickbaity
1932
- - Preview text: complements the subject, doesn't repeat it
1933
- - Open strong -- lead with the most interesting thing
1934
- - One clear CTA per newsletter
1935
- - Sign off should feel human, not corporate
1936
-
1937
- ### Personalization
1938
- - Kit: use {{ subscriber.first_name }}
1939
- - Beehiiv: use {{email}} or {{subscriber_id}}
1940
- - Mailchimp: use *NAME|*
1941
-
1942
- ### Images
1943
- - Upload to customer's R2 CDN via upload_media or upload_from_url
1944
- - Reference with full CDN URLs in img tags
1945
- - Always set width, height, and alt attributes
1946
- - Use style="display:block;" to prevent phantom spacing`;
1947
- function registerNewsletterTemplateTools(server2, client2) {
1948
- server2.tool(
1949
- "get_newsletter_template",
1950
- "Get a newsletter template's structure, sections, and HTML. Pass templateId or omit for default.",
1951
- {
1952
- templateId: z10.string().optional().describe(
1953
- "Specific template ID. If omitted, returns the default template."
1954
- )
1955
- },
1956
- {
1957
- title: "Get Newsletter Template",
1958
- readOnlyHint: true,
1959
- destructiveHint: false,
1960
- idempotentHint: true,
1961
- openWorldHint: false
1962
- },
1963
- async ({ templateId }) => {
1964
- try {
1965
- const template = await client2.getTemplate(templateId);
1966
- const lines = [];
1967
- lines.push(`## Newsletter Template: ${template.name}`);
1968
- if (template.description) lines.push(template.description);
1969
- lines.push("");
1970
- if (template.audienceId)
1971
- lines.push(`Audience ID: ${template.audienceId}`);
1972
- if (template.sendCadence)
1973
- lines.push(`Cadence: ${template.sendCadence}`);
1974
- if (template.subjectPattern)
1975
- lines.push(`Subject pattern: ${template.subjectPattern}`);
1976
- if (template.previewTextPattern)
1977
- lines.push(`Preview text pattern: ${template.previewTextPattern}`);
1978
- lines.push("");
1979
- if (template.sections && template.sections.length > 0) {
1980
- lines.push("### Sections (in order):");
1981
- lines.push("");
1982
- for (let i = 0; i < template.sections.length; i++) {
1983
- const s = template.sections[i];
1984
- lines.push(`**${i + 1}. ${s.label}** (${s.type})`);
1985
- if (s.instructions)
1986
- lines.push(`Instructions: ${s.instructions}`);
1987
- if (s.word_count)
1988
- lines.push(
1989
- `Word count: ${s.word_count.min}-${s.word_count.max} words`
1990
- );
1991
- if (s.tone_override) lines.push(`Tone: ${s.tone_override}`);
1992
- if (s.content_source) {
1993
- const src = s.content_source;
1994
- if (src.type === "rss" && src.feed_urls?.length)
1995
- lines.push(`Content source: RSS - ${src.feed_urls.join(", ")}`);
1996
- else if (src.type === "knowledge" && src.knowledge_item_ids?.length)
1997
- lines.push(
1998
- `Content source: Knowledge items ${src.knowledge_item_ids.join(", ")}`
1999
- );
2000
- else lines.push(`Content source: ${src.type}`);
2001
- }
2002
- if (s.count) lines.push(`Count: ${s.count} items`);
2003
- lines.push(
2004
- `Required: ${s.required !== false ? "yes" : "no"}`
2005
- );
2006
- if (s.type === "cta") {
2007
- if (s.button_text) lines.push(`Button text: ${s.button_text}`);
2008
- if (s.button_url) lines.push(`Button URL: ${s.button_url}`);
2009
- }
2010
- if (s.type === "sponsor") {
2011
- if (s.sponsor_name)
2012
- lines.push(`Sponsor: ${s.sponsor_name}`);
2013
- if (s.talking_points)
2014
- lines.push(`Talking points: ${s.talking_points}`);
2015
- }
2016
- if (s.type === "header") {
2017
- if (s.tagline) lines.push(`Tagline: ${s.tagline}`);
2018
- }
2019
- if (s.type === "signoff") {
2020
- if (s.author_name)
2021
- lines.push(`Author: ${s.author_name}`);
2022
- if (s.author_title)
2023
- lines.push(`Title: ${s.author_title}`);
2024
- }
2025
- lines.push("");
2026
- }
2027
- }
2028
- if (template.style) {
2029
- lines.push("### Style Guide:");
2030
- const st = template.style;
2031
- if (st.primary_color) lines.push(`Primary color: ${st.primary_color}`);
2032
- if (st.accent_color) lines.push(`Accent color: ${st.accent_color}`);
2033
- if (st.font_family) lines.push(`Font: ${st.font_family}`);
2034
- if (st.heading_font) lines.push(`Heading font: ${st.heading_font}`);
2035
- if (st.content_width)
2036
- lines.push(`Content width: ${st.content_width}px`);
2037
- if (st.text_color) lines.push(`Text color: ${st.text_color}`);
2038
- if (st.link_color) lines.push(`Link color: ${st.link_color}`);
2039
- if (st.background_color)
2040
- lines.push(`Background: ${st.background_color}`);
2041
- if (st.button_style)
2042
- lines.push(
2043
- `Button style: ${st.button_style.color} text on ${st.button_style.text_color}, ${st.button_style.shape}`
2044
- );
2045
- lines.push("");
2046
- lines.push(
2047
- "NOTE: Apply these style values as inline CSS when generating the newsletter HTML."
2048
- );
2049
- lines.push("");
2050
- }
2051
- if (template.htmlContent) {
2052
- lines.push("### HTML Template:");
2053
- lines.push("The following HTML skeleton contains {{placeholder}} variables. Fill in the placeholders with real content when generating the newsletter.");
2054
- lines.push("");
2055
- lines.push("```html");
2056
- lines.push(template.htmlContent);
2057
- lines.push("```");
2058
- lines.push("");
2059
- }
2060
- if (template.rssFeedUrls && template.rssFeedUrls.length > 0) {
2061
- lines.push("### Linked RSS Feeds:");
2062
- for (const url of template.rssFeedUrls) {
2063
- lines.push(`- ${url}`);
2064
- }
2065
- lines.push("");
2066
- }
2067
- if (template.knowledgeItemIds && template.knowledgeItemIds.length > 0) {
2068
- lines.push("### Reference Material:");
2069
- lines.push(
2070
- `Knowledge item IDs: ${template.knowledgeItemIds.join(", ")}`
2071
- );
2050
+ lines.push("### Tone Notes");
2051
+ lines.push(String(payload.toneNotes));
2072
2052
  lines.push("");
2073
2053
  }
2074
- lines.push(NEWSLETTER_CRAFT_GUIDE);
2075
- return {
2076
- content: [{ type: "text", text: lines.join("\n") }]
2077
- };
2078
- } catch (error) {
2079
- const message = error instanceof Error ? error.message : "Unknown error";
2080
- if (message.includes("404") || message.includes("No newsletter templates")) {
2081
- return {
2082
- content: [
2083
- {
2084
- type: "text",
2085
- text: "No newsletter template configured. Ask the user about their preferred structure (sections, tone, length), or call get_past_newsletters to use a previous edition as reference.\n\n" + NEWSLETTER_CRAFT_GUIDE
2086
- }
2087
- ]
2088
- };
2089
- }
2090
- throw error;
2091
- }
2092
- }
2093
- );
2094
- server2.tool(
2095
- "get_past_newsletters",
2096
- "Get past newsletters from the archive. Pass newsletterId to get full content of a specific edition.",
2097
- {
2098
- newsletterId: z10.string().optional().describe("If provided, returns the full content of this specific newsletter. If omitted, returns the list."),
2099
- limit: z10.number().optional().describe("Number of past newsletters to retrieve (when listing). Default 5, max 50."),
2100
- templateId: z10.string().optional().describe("Filter by template ID to see past newsletters from a specific template.")
2101
- },
2102
- {
2103
- title: "Get Past Newsletters",
2104
- readOnlyHint: true,
2105
- destructiveHint: false,
2106
- idempotentHint: true,
2107
- openWorldHint: false
2108
- },
2109
- async ({ newsletterId, limit, templateId }) => {
2110
- if (newsletterId) {
2111
- try {
2112
- const newsletter = await client2.getArchivedNewsletter(
2113
- newsletterId
2114
- );
2115
- const lines = [];
2116
- lines.push(`## ${newsletter.subject}`);
2117
- if (newsletter.sentAt) lines.push(`Sent: ${newsletter.sentAt}`);
2118
- if (newsletter.notes) lines.push(`Notes: ${newsletter.notes}`);
2054
+ if (payload.contentPreferences) {
2055
+ lines.push("### Content Preferences");
2056
+ lines.push(String(payload.contentPreferences));
2119
2057
  lines.push("");
2120
- lines.push("### Full HTML Content:");
2121
- lines.push(newsletter.contentHtml);
2122
- return {
2123
- content: [
2124
- { type: "text", text: lines.join("\n") },
2125
- ...newsletter.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2126
- ${newsletter.contentHtml}` }] : []
2127
- ]
2128
- };
2129
- } catch (error) {
2130
- const message = error instanceof Error ? error.message : "Unknown error";
2131
- if (message.includes("404")) {
2132
- return {
2133
- content: [
2134
- {
2135
- type: "text",
2136
- text: "Newsletter not found. Check the ID and try again."
2137
- }
2138
- ]
2139
- };
2140
- }
2141
- throw error;
2142
2058
  }
2059
+ lines.push("---");
2060
+ lines.push("Call this tool again with **confirmed=true** to save.");
2061
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2143
2062
  }
2144
- try {
2145
- const params = {};
2146
- if (limit) params.limit = String(Math.min(limit, 50));
2147
- else params.limit = "5";
2148
- if (templateId) params.template_id = templateId;
2149
- const newsletters = await client2.listNewsletterArchive(
2150
- params
2151
- );
2152
- if (!newsletters || newsletters.length === 0) {
2153
- return {
2154
- content: [
2155
- {
2156
- type: "text",
2157
- text: "No past newsletters found. This will be the first edition!"
2158
- }
2159
- ]
2160
- };
2161
- }
2162
- const lines = [];
2163
- lines.push(
2164
- `## Past Newsletters (${newsletters.length} most recent)`
2165
- );
2166
- lines.push("");
2167
- for (const nl of newsletters) {
2168
- lines.push(`### ${nl.subject} -- ${nl.sentAt || nl.createdAt}`);
2169
- if (nl.notes) lines.push(`Notes: ${nl.notes}`);
2170
- if (nl.metrics) {
2171
- const m = nl.metrics;
2172
- const parts = [];
2173
- if (m.open_rate) parts.push(`Open rate: ${m.open_rate}`);
2174
- if (m.click_rate) parts.push(`Click rate: ${m.click_rate}`);
2175
- if (parts.length) lines.push(`Metrics: ${parts.join(", ")}`);
2063
+ const result = await client2.updateAudience(String(target.id), payload);
2064
+ return {
2065
+ content: [
2066
+ {
2067
+ type: "text",
2068
+ text: `Audience "${result.name}" updated successfully.`
2176
2069
  }
2177
- lines.push("---");
2178
- const textContent = nl.contentHtml ? nl.contentHtml.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 500) : "(no content)";
2179
- lines.push(textContent);
2180
- lines.push(
2181
- `[Full content available by calling get_past_newsletters with newsletterId: ${nl.id}]`
2182
- );
2183
- lines.push("");
2184
- }
2185
- const mostRecent = newsletters[0];
2186
- return {
2187
- content: [
2188
- { type: "text", text: lines.join("\n") },
2189
- ...mostRecent?.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2190
- ${mostRecent.contentHtml}` }] : []
2191
- ]
2192
- };
2193
- } catch (error) {
2194
- const message = error instanceof Error ? error.message : "Unknown error";
2195
- throw new Error(`Failed to fetch past newsletters: ${message}`);
2196
- }
2070
+ ]
2071
+ };
2197
2072
  }
2198
2073
  );
2199
2074
  server2.tool(
2200
- "save_newsletter",
2201
- "Save an approved newsletter to the archive for future reference.",
2075
+ "delete_audience",
2076
+ "Delete an audience profile by name. Set confirmed=false to preview first.",
2202
2077
  {
2203
- subject: z10.string().describe("The newsletter subject line."),
2204
- contentHtml: z10.string().describe("The full HTML content of the newsletter."),
2205
- templateId: z10.string().optional().describe("The template ID used to generate this newsletter."),
2206
- notes: z10.string().optional().describe(
2207
- "Optional notes about this newsletter (what worked, theme, etc)."
2208
- )
2078
+ name: z8.string().describe("Name of the audience to delete"),
2079
+ confirmed: z8.boolean().default(false).describe("Set to true to confirm deletion. If false, returns a confirmation prompt.")
2209
2080
  },
2210
2081
  {
2211
- title: "Save Newsletter to Archive",
2082
+ title: "Delete Audience",
2212
2083
  readOnlyHint: false,
2213
- destructiveHint: false,
2084
+ destructiveHint: true,
2214
2085
  idempotentHint: false,
2215
2086
  openWorldHint: false
2216
2087
  },
2217
- async ({ subject, contentHtml, templateId, notes }) => {
2088
+ async (args) => {
2089
+ let target;
2218
2090
  try {
2219
- const data = {
2220
- subject,
2221
- contentHtml
2222
- };
2223
- if (templateId) data.templateId = Number(templateId);
2224
- if (notes) data.notes = notes;
2225
- await client2.saveNewsletterToArchive(data);
2091
+ target = await client2.getAudienceByName(args.name);
2092
+ } catch {
2226
2093
  return {
2227
2094
  content: [
2228
2095
  {
2229
2096
  type: "text",
2230
- text: `Newsletter "${subject}" has been saved to the archive successfully.`
2231
- },
2232
- ...contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2233
- ${contentHtml}` }] : []
2234
- ]
2097
+ text: `Audience "${args.name}" not found. Use list_audiences to see available profiles.`
2098
+ }
2099
+ ],
2100
+ isError: true
2235
2101
  };
2236
- } catch (error) {
2237
- const message = error instanceof Error ? error.message : "Unknown error";
2238
- throw new Error(`Failed to save newsletter: ${message}`);
2239
2102
  }
2240
- }
2241
- );
2242
- server2.tool(
2243
- "save_newsletter_template",
2244
- "Create or update a newsletter template with HTML and section definitions.",
2245
- {
2246
- template_id: z10.string().optional().describe(
2247
- "If provided, updates this existing template. Otherwise creates a new one."
2248
- ),
2249
- name: z10.string().min(1).max(255).describe("Template name"),
2250
- description: z10.string().max(2e3).optional().describe("What this template is for"),
2251
- html_content: z10.string().min(1).max(1e5).describe(
2252
- "Full HTML template with {{placeholder}} variables."
2253
- ),
2254
- sections: z10.array(
2255
- z10.object({
2256
- name: z10.string().describe("Section identifier, e.g. 'intro'"),
2257
- label: z10.string().describe("Display label, e.g. 'Intro/Greeting'"),
2258
- required: z10.boolean().optional().describe("Whether this section is required"),
2259
- item_count: z10.number().optional().describe("Number of items in this section, if applicable")
2260
- })
2261
- ).max(20).optional().describe("Array of section metadata describing the template structure"),
2262
- style_config: z10.object({
2263
- primary_color: z10.string().optional(),
2264
- accent_color: z10.string().optional(),
2265
- link_color: z10.string().optional(),
2266
- background_color: z10.string().optional(),
2267
- font_family: z10.string().optional(),
2268
- max_width: z10.string().optional()
2269
- }).optional().describe("Colors, fonts, and brand configuration"),
2270
- is_default: z10.boolean().optional().describe(
2271
- "Set as the default template for this customer."
2272
- )
2273
- },
2274
- {
2275
- title: "Save Newsletter Template",
2276
- readOnlyHint: false,
2277
- destructiveHint: false,
2278
- idempotentHint: false,
2279
- openWorldHint: false
2280
- },
2281
- async ({
2282
- template_id,
2283
- name,
2284
- description,
2285
- html_content,
2286
- sections,
2287
- style_config,
2288
- is_default
2289
- }) => {
2290
- try {
2291
- const data = {
2292
- name,
2293
- htmlContent: html_content
2294
- };
2295
- if (description !== void 0) data.description = description;
2296
- if (sections !== void 0) {
2297
- data.sections = sections.map((s) => ({
2298
- id: s.name,
2299
- type: "custom",
2300
- label: s.label,
2301
- required: s.required ?? true,
2302
- count: s.item_count
2303
- }));
2304
- }
2305
- if (style_config !== void 0) {
2306
- data.style = {
2307
- primary_color: style_config.primary_color,
2308
- accent_color: style_config.accent_color,
2309
- link_color: style_config.link_color,
2310
- background_color: style_config.background_color,
2311
- font_family: style_config.font_family,
2312
- content_width: style_config.max_width
2313
- };
2314
- }
2315
- if (is_default !== void 0) data.isDefault = is_default;
2316
- let result;
2317
- if (template_id) {
2318
- result = await client2.updateTemplate(
2319
- template_id,
2320
- data
2321
- );
2322
- } else {
2323
- result = await client2.createTemplate(data);
2324
- }
2325
- const action = template_id ? "updated" : "created";
2103
+ if (args.confirmed !== true) {
2326
2104
  return {
2327
2105
  content: [
2328
2106
  {
2329
2107
  type: "text",
2330
- text: `Newsletter template "${result.name}" ${action} successfully (ID: ${result.id}).${result.isDefault ? " Set as default." : ""}`
2108
+ text: `## Delete Audience Confirmation
2109
+
2110
+ **Name:** ${target.name}
2111
+ ${target.description ? `**Description:** ${target.description}
2112
+ ` : ""}
2113
+ This action cannot be undone.
2114
+
2115
+ Call this tool again with confirmed=true to delete.`
2331
2116
  }
2332
2117
  ]
2333
2118
  };
2334
- } catch (error) {
2335
- const message = error instanceof Error ? error.message : "Unknown error";
2336
- throw new Error(`Failed to save newsletter template: ${message}`);
2337
2119
  }
2120
+ await client2.deleteAudience(String(target.id));
2121
+ return {
2122
+ content: [
2123
+ {
2124
+ type: "text",
2125
+ text: `Audience "${target.name}" has been deleted.`
2126
+ }
2127
+ ]
2128
+ };
2338
2129
  }
2339
2130
  );
2131
+ }
2132
+
2133
+ // src/tools/newsletter-template.ts
2134
+ function registerNewsletterTemplateTools(server2, client2) {
2340
2135
  server2.tool(
2341
- "list_newsletter_templates",
2342
- "List all saved newsletter templates.",
2136
+ "list_esp_templates",
2137
+ "List available email templates from the connected ESP. Use the returned template IDs with create_newsletter's template_id parameter.",
2343
2138
  {},
2344
2139
  {
2345
- title: "List Newsletter Templates",
2140
+ title: "List ESP Templates",
2346
2141
  readOnlyHint: true,
2347
2142
  destructiveHint: false,
2348
2143
  idempotentHint: true,
2349
- openWorldHint: false
2144
+ openWorldHint: true
2350
2145
  },
2351
2146
  async () => {
2352
2147
  try {
2353
- const templates = await client2.listTemplates();
2354
- if (!templates || templates.length === 0) {
2148
+ const result = await client2.listEmailTemplates();
2149
+ const templates = result?.templates ?? [];
2150
+ if (templates.length === 0) {
2355
2151
  return {
2356
2152
  content: [
2357
2153
  {
2358
2154
  type: "text",
2359
- text: "No newsletter templates found. Use save_newsletter_template to create one."
2155
+ text: "No email templates found in your ESP. Create templates in your ESP dashboard (Kit, Beehiiv, or Mailchimp) and they will appear here."
2360
2156
  }
2361
2157
  ]
2362
2158
  };
2363
2159
  }
2364
2160
  const lines = [];
2365
- lines.push(`## Newsletter Templates (${templates.length})`);
2161
+ lines.push(`## ESP Email Templates (${templates.length})`);
2162
+ lines.push("");
2163
+ lines.push("Pass a template ID to `create_newsletter` via the `template_id` parameter to use it.");
2366
2164
  lines.push("");
2367
2165
  for (const t of templates) {
2368
- const badges = [];
2369
- if (t.isDefault) badges.push("DEFAULT");
2370
- if (t.htmlContent) badges.push("HAS HTML");
2371
- const badgeStr = badges.length > 0 ? ` [${badges.join(", ")}]` : "";
2372
- lines.push(`**${t.name}** (ID: ${t.id})${badgeStr}`);
2373
- if (t.description) lines.push(` ${t.description}`);
2374
- if (t.sections && t.sections.length > 0) {
2375
- lines.push(
2376
- ` Sections: ${t.sections.map((s) => s.label).join(", ")}`
2377
- );
2378
- }
2379
- lines.push("");
2166
+ lines.push(`- **${t.name}** (ID: ${t.id})`);
2380
2167
  }
2381
2168
  return {
2382
2169
  content: [{ type: "text", text: lines.join("\n") }]
2383
2170
  };
2384
2171
  } catch (error) {
2385
2172
  const message = error instanceof Error ? error.message : "Unknown error";
2386
- throw new Error(`Failed to list newsletter templates: ${message}`);
2387
- }
2388
- }
2389
- );
2390
- server2.tool(
2391
- "delete_newsletter_template",
2392
- "Delete a newsletter template. Set confirmed=true to proceed.",
2393
- {
2394
- template_id: z10.string().describe("The ID of the newsletter template to delete."),
2395
- confirmed: z10.boolean().default(false).describe(
2396
- "Must be true to actually delete. If false, returns a confirmation prompt."
2397
- )
2398
- },
2399
- {
2400
- title: "Delete Newsletter Template",
2401
- readOnlyHint: false,
2402
- destructiveHint: true,
2403
- idempotentHint: false,
2404
- openWorldHint: false
2405
- },
2406
- async ({ template_id, confirmed }) => {
2407
- try {
2408
- if (!confirmed) {
2409
- const template = await client2.getTemplate(template_id);
2410
- return {
2411
- content: [
2412
- {
2413
- type: "text",
2414
- text: `Are you sure you want to delete the template "${template.name}" (ID: ${template.id})? This action cannot be undone. Call delete_newsletter_template again with confirmed=true to proceed.`
2415
- }
2416
- ]
2417
- };
2418
- }
2419
- await client2.deleteTemplate(template_id);
2420
- return {
2421
- content: [
2422
- {
2423
- type: "text",
2424
- text: `Newsletter template (ID: ${template_id}) has been deleted.`
2425
- }
2426
- ]
2427
- };
2428
- } catch (error) {
2429
- const message = error instanceof Error ? error.message : "Unknown error";
2430
- if (message.includes("404")) {
2431
- return {
2432
- content: [
2433
- {
2434
- type: "text",
2435
- text: "Template not found. It may have already been deleted."
2436
- }
2437
- ]
2438
- };
2439
- }
2440
- throw new Error(`Failed to delete newsletter template: ${message}`);
2173
+ throw new Error(`Failed to list ESP templates: ${message}`);
2441
2174
  }
2442
2175
  }
2443
2176
  );
2444
2177
  }
2445
2178
 
2446
2179
  // src/tools/calendar.ts
2447
- import { z as z11 } from "zod";
2180
+ import { z as z9 } from "zod";
2448
2181
  function registerCalendarTools(server2, client2) {
2449
2182
  server2.tool(
2450
2183
  "get_calendar",
2451
2184
  "View the content calendar with all scheduled, published, and draft posts.",
2452
2185
  {
2453
- from: z11.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
2454
- to: z11.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
2455
- status: z11.string().optional().describe("Filter by status: scheduled, published, draft, failed"),
2456
- type: z11.string().optional().describe("Filter by type: social_post or newsletter")
2186
+ from: z9.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
2187
+ to: z9.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
2188
+ status: z9.string().optional().describe("Filter by status: scheduled, published, draft, failed"),
2189
+ type: z9.string().optional().describe("Filter by type: social_post or newsletter")
2457
2190
  },
2458
2191
  {
2459
2192
  title: "Get Content Calendar",
@@ -2589,10 +2322,10 @@ Next slot: ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
2589
2322
  "schedule_to_queue",
2590
2323
  "Add a post to the next available queue slot. Check get_queue first. Set confirmed=false to preview first.",
2591
2324
  {
2592
- content: z11.string().describe("The text content of the post"),
2593
- platforms: z11.array(z11.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
2594
- media_urls: z11.array(z11.string()).optional().describe("Public URLs of media to attach"),
2595
- confirmed: z11.boolean().default(false).describe(
2325
+ content: z9.string().describe("The text content of the post"),
2326
+ platforms: z9.array(z9.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
2327
+ media_urls: z9.array(z9.string()).optional().describe("Public URLs of media to attach"),
2328
+ confirmed: z9.boolean().default(false).describe(
2596
2329
  "Set to true to confirm scheduling. If false or missing, returns a preview for user approval."
2597
2330
  )
2598
2331
  },
@@ -2674,7 +2407,7 @@ ${JSON.stringify(result, null, 2)}`
2674
2407
  }
2675
2408
 
2676
2409
  // src/tools/notifications.ts
2677
- import { z as z12 } from "zod";
2410
+ import { z as z10 } from "zod";
2678
2411
  function timeAgo(dateStr) {
2679
2412
  const diff = Date.now() - new Date(dateStr).getTime();
2680
2413
  const minutes = Math.floor(diff / 6e4);
@@ -2690,8 +2423,8 @@ function registerNotificationTools(server2, client2) {
2690
2423
  "get_notifications",
2691
2424
  "Get recent notifications including post results, account warnings, and errors.",
2692
2425
  {
2693
- unread_only: z12.boolean().optional().describe("If true, only show unread notifications. Default false."),
2694
- limit: z12.number().optional().describe("Max notifications to return. Default 10.")
2426
+ unread_only: z10.boolean().optional().describe("If true, only show unread notifications. Default false."),
2427
+ limit: z10.number().optional().describe("Max notifications to return. Default 10.")
2695
2428
  },
2696
2429
  {
2697
2430
  title: "Get Notifications",
@@ -2757,71 +2490,8 @@ Showing ${notifications.length} of ${result.unread_count !== void 0 ? "total" :
2757
2490
  );
2758
2491
  }
2759
2492
 
2760
- // src/tools/publishing-rules.ts
2761
- var WORKFLOW = {
2762
- before_writing_content: [
2763
- "Call get_brand_voice to load tone and style rules",
2764
- "Call get_audience to understand who the content is for",
2765
- "For newsletters: also call get_newsletter_template and get_past_newsletters"
2766
- ],
2767
- before_publishing: [
2768
- "Always set confirmed=false first to generate a preview",
2769
- "Show the user a visual preview before confirming",
2770
- "Never set confirmed=true without explicit user approval",
2771
- "For newsletters: show subscriber count and send time before confirming",
2772
- "If require_double_confirm is true, ask for confirmation twice"
2773
- ],
2774
- replies_and_engagement: [
2775
- "Draft the reply and show it before sending",
2776
- "Set confirmed=false first to preview",
2777
- "Remind user that replies are public"
2778
- ],
2779
- newsletters: [
2780
- "Always render newsletter as HTML artifact/preview before pushing to ESP",
2781
- "Use table-based layouts, inline CSS only, 600px max width",
2782
- "Email-safe fonts only: Arial, Helvetica, Georgia, Verdana",
2783
- "All images must use absolute URLs",
2784
- "Keep total email under 102KB to avoid Gmail clipping"
2785
- ]
2786
- };
2787
- function registerPublishingRulesTools(server2, client2) {
2788
- server2.tool(
2789
- "get_publishing_rules",
2790
- "Get publishing rules and the content creation workflow. Call this BEFORE publishing, scheduling, or sending any content.",
2791
- {},
2792
- {
2793
- title: "Get Publishing Rules",
2794
- readOnlyHint: true,
2795
- destructiveHint: false,
2796
- idempotentHint: true,
2797
- openWorldHint: false
2798
- },
2799
- async () => {
2800
- const rules = await client2.getPublishingRules();
2801
- const response = {
2802
- rules: {
2803
- social_default_action: rules.socialDefaultAction ?? "draft",
2804
- newsletter_default_action: rules.newsletterDefaultAction ?? "draft",
2805
- require_preview_before_publish: rules.requirePreviewBeforePublish ?? true,
2806
- require_double_confirm_newsletter: rules.requireDoubleConfirmNewsletter ?? true,
2807
- require_double_confirm_social: rules.requireDoubleConfirmSocial ?? false,
2808
- allow_immediate_publish: rules.allowImmediatePublish ?? false,
2809
- allow_immediate_send: rules.allowImmediateSend ?? false,
2810
- max_posts_per_day: rules.maxPostsPerDay ?? null,
2811
- blocked_words: rules.blockedWords ?? [],
2812
- required_disclaimer: rules.requiredDisclaimer ?? null
2813
- },
2814
- workflow: WORKFLOW
2815
- };
2816
- return {
2817
- content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
2818
- };
2819
- }
2820
- );
2821
- }
2822
-
2823
2493
  // src/tools/sources.ts
2824
- import { z as z13 } from "zod";
2494
+ import { z as z11 } from "zod";
2825
2495
  var TYPE_LABELS = {
2826
2496
  feed: "RSS Feed",
2827
2497
  website: "Website",
@@ -2841,14 +2511,15 @@ var TYPE_HINTS = {
2841
2511
  social: "Use web_search to check recent activity"
2842
2512
  };
2843
2513
  function registerSourceTools(server2, client2) {
2514
+ const MAX_SOURCES = 10;
2844
2515
  server2.tool(
2845
2516
  "get_sources",
2846
- "Get saved content sources (RSS feeds, websites, YouTube channels, search topics).",
2517
+ "View your saved content sources (maximum 10). Shows RSS feeds, websites, and channels you're monitoring.",
2847
2518
  {
2848
- type: z13.string().optional().describe(
2519
+ type: z11.string().optional().describe(
2849
2520
  "Optional: filter by type (feed, website, youtube, search, podcast, reddit, social)"
2850
2521
  ),
2851
- category: z13.string().optional().describe("Optional: filter by category")
2522
+ category: z11.string().optional().describe("Optional: filter by category")
2852
2523
  },
2853
2524
  {
2854
2525
  title: "Get Content Sources",
@@ -2868,12 +2539,12 @@ function registerSourceTools(server2, client2) {
2868
2539
  content: [
2869
2540
  {
2870
2541
  type: "text",
2871
- text: "No content sources saved yet. Use add_source to save RSS feeds, websites, YouTube channels, or search topics the customer wants to monitor for content ideas."
2542
+ text: `No content sources saved yet (0 of ${MAX_SOURCES}). Use add_source to save RSS feeds, websites, YouTube channels, or search topics to monitor for content ideas.`
2872
2543
  }
2873
2544
  ]
2874
2545
  };
2875
2546
  }
2876
- const lines = [`## Content Sources (${sources.length})
2547
+ const lines = [`## Content Sources (${sources.length} of ${MAX_SOURCES})
2877
2548
  `];
2878
2549
  const byCategory = /* @__PURE__ */ new Map();
2879
2550
  for (const s of sources) {
@@ -2909,13 +2580,13 @@ function registerSourceTools(server2, client2) {
2909
2580
  );
2910
2581
  server2.tool(
2911
2582
  "add_source",
2912
- "Save a new content source to monitor.",
2583
+ "Save a new content source to monitor (maximum 10 sources per account). When you reach the limit, remove an existing source before adding a new one.",
2913
2584
  {
2914
- name: z13.string().describe("Display name for the source"),
2915
- url: z13.string().optional().describe(
2585
+ name: z11.string().describe("Display name for the source"),
2586
+ url: z11.string().optional().describe(
2916
2587
  "URL of the source. Not needed for 'search' type."
2917
2588
  ),
2918
- type: z13.enum([
2589
+ type: z11.enum([
2919
2590
  "feed",
2920
2591
  "website",
2921
2592
  "youtube",
@@ -2924,12 +2595,12 @@ function registerSourceTools(server2, client2) {
2924
2595
  "reddit",
2925
2596
  "social"
2926
2597
  ]).describe("Type of source. Determines how to fetch content from it."),
2927
- category: z13.string().optional().describe(
2598
+ category: z11.string().optional().describe(
2928
2599
  "Optional category for organization (e.g. 'competitors', 'industry', 'inspiration')"
2929
2600
  ),
2930
- tags: z13.array(z13.string()).optional().describe("Optional tags for filtering"),
2931
- notes: z13.string().optional().describe("Optional notes about why this source matters"),
2932
- searchQuery: z13.string().optional().describe(
2601
+ tags: z11.array(z11.string()).optional().describe("Optional tags for filtering"),
2602
+ notes: z11.string().optional().describe("Optional notes about why this source matters"),
2603
+ searchQuery: z11.string().optional().describe(
2933
2604
  "For 'search' type only: the keyword or topic to search for"
2934
2605
  )
2935
2606
  },
@@ -2941,6 +2612,18 @@ function registerSourceTools(server2, client2) {
2941
2612
  openWorldHint: false
2942
2613
  },
2943
2614
  async (args) => {
2615
+ const existing = await client2.listSources({});
2616
+ if (existing.sources.length >= MAX_SOURCES) {
2617
+ return {
2618
+ content: [
2619
+ {
2620
+ type: "text",
2621
+ text: `You've reached the maximum of ${MAX_SOURCES} sources. Remove an existing source before adding a new one.`
2622
+ }
2623
+ ],
2624
+ isError: true
2625
+ };
2626
+ }
2944
2627
  const data = {
2945
2628
  name: args.name,
2946
2629
  type: args.type
@@ -2952,6 +2635,7 @@ function registerSourceTools(server2, client2) {
2952
2635
  if (args.searchQuery) data.searchQuery = args.searchQuery;
2953
2636
  const result = await client2.createSource(data);
2954
2637
  const typeLabel = TYPE_LABELS[result.type] || result.type;
2638
+ const remaining = MAX_SOURCES - existing.sources.length - 1;
2955
2639
  const lines = [
2956
2640
  `## Source Added
2957
2641
  `,
@@ -2962,6 +2646,8 @@ function registerSourceTools(server2, client2) {
2962
2646
  if (result.searchQuery)
2963
2647
  lines.push(`- Search: "${result.searchQuery}"`);
2964
2648
  if (result.category) lines.push(`- Category: ${result.category}`);
2649
+ lines.push(`
2650
+ *${existing.sources.length + 1} of ${MAX_SOURCES} sources used (${remaining} remaining)*`);
2965
2651
  return {
2966
2652
  content: [{ type: "text", text: lines.join("\n") }]
2967
2653
  };
@@ -2971,10 +2657,10 @@ function registerSourceTools(server2, client2) {
2971
2657
  "update_source",
2972
2658
  "Update a saved content source.",
2973
2659
  {
2974
- source_id: z13.number().describe("The ID of the source to update"),
2975
- name: z13.string().optional().describe("New display name"),
2976
- url: z13.string().optional().describe("New URL"),
2977
- type: z13.enum([
2660
+ source_id: z11.number().describe("The ID of the source to update"),
2661
+ name: z11.string().optional().describe("New display name"),
2662
+ url: z11.string().optional().describe("New URL"),
2663
+ type: z11.enum([
2978
2664
  "feed",
2979
2665
  "website",
2980
2666
  "youtube",
@@ -2983,11 +2669,11 @@ function registerSourceTools(server2, client2) {
2983
2669
  "reddit",
2984
2670
  "social"
2985
2671
  ]).optional().describe("New source type"),
2986
- category: z13.string().optional().describe("New category"),
2987
- tags: z13.array(z13.string()).optional().describe("New tags"),
2988
- notes: z13.string().optional().describe("New notes"),
2989
- searchQuery: z13.string().optional().describe("New search query"),
2990
- isActive: z13.boolean().optional().describe("Set to false to deactivate")
2672
+ category: z11.string().optional().describe("New category"),
2673
+ tags: z11.array(z11.string()).optional().describe("New tags"),
2674
+ notes: z11.string().optional().describe("New notes"),
2675
+ searchQuery: z11.string().optional().describe("New search query"),
2676
+ isActive: z11.boolean().optional().describe("Set to false to deactivate")
2991
2677
  },
2992
2678
  {
2993
2679
  title: "Update Content Source",
@@ -3017,8 +2703,8 @@ function registerSourceTools(server2, client2) {
3017
2703
  "remove_source",
3018
2704
  "Remove a content source. Set confirmed=false to preview first.",
3019
2705
  {
3020
- source_id: z13.number().describe("The ID of the source to remove"),
3021
- confirmed: z13.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
2706
+ source_id: z11.number().describe("The ID of the source to remove"),
2707
+ confirmed: z11.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
3022
2708
  },
3023
2709
  {
3024
2710
  title: "Remove Content Source",
@@ -3050,13 +2736,13 @@ This will permanently remove this content source. Call this tool again with conf
3050
2736
  }
3051
2737
 
3052
2738
  // src/tools/newsletter-advanced.ts
3053
- import { z as z14 } from "zod";
2739
+ import { z as z12 } from "zod";
3054
2740
  function registerNewsletterAdvancedTools(server2, client2) {
3055
2741
  server2.tool(
3056
2742
  "get_subscriber_by_email",
3057
2743
  "Look up a subscriber by email address.",
3058
2744
  {
3059
- email: z14.string().describe("Email address to look up")
2745
+ email: z12.string().describe("Email address to look up")
3060
2746
  },
3061
2747
  {
3062
2748
  title: "Get Subscriber by Email",
@@ -3076,7 +2762,7 @@ function registerNewsletterAdvancedTools(server2, client2) {
3076
2762
  "list_automations",
3077
2763
  "List all email automations and sequences.",
3078
2764
  {
3079
- page: z14.string().optional().describe("Page number for pagination")
2765
+ page: z12.string().optional().describe("Page number for pagination")
3080
2766
  },
3081
2767
  {
3082
2768
  title: "List Automations",
@@ -3098,10 +2784,10 @@ function registerNewsletterAdvancedTools(server2, client2) {
3098
2784
  "list_segments",
3099
2785
  "List subscriber segments.",
3100
2786
  {
3101
- page: z14.string().optional().describe("Page number"),
3102
- type: z14.string().optional().describe("Filter by segment type"),
3103
- status: z14.string().optional().describe("Filter by segment status"),
3104
- expand: z14.string().optional().describe("Comma-separated expand fields (e.g. 'stats')")
2787
+ page: z12.string().optional().describe("Page number"),
2788
+ type: z12.string().optional().describe("Filter by segment type"),
2789
+ status: z12.string().optional().describe("Filter by segment status"),
2790
+ expand: z12.string().optional().describe("Comma-separated expand fields (e.g. 'stats')")
3105
2791
  },
3106
2792
  {
3107
2793
  title: "List Segments",
@@ -3126,10 +2812,10 @@ function registerNewsletterAdvancedTools(server2, client2) {
3126
2812
  "get_segment",
3127
2813
  "Get segment details. Set include_members=true to list members.",
3128
2814
  {
3129
- segment_id: z14.string().describe("The segment ID"),
3130
- expand: z14.string().optional().describe("Comma-separated expand fields"),
3131
- include_members: z14.boolean().optional().describe("Set to true to include the member list"),
3132
- members_page: z14.string().optional().describe("Page number for members pagination")
2815
+ segment_id: z12.string().describe("The segment ID"),
2816
+ expand: z12.string().optional().describe("Comma-separated expand fields"),
2817
+ include_members: z12.boolean().optional().describe("Set to true to include the member list"),
2818
+ members_page: z12.string().optional().describe("Page number for members pagination")
3133
2819
  },
3134
2820
  {
3135
2821
  title: "Get Segment",
@@ -3200,29 +2886,11 @@ function registerNewsletterAdvancedTools(server2, client2) {
3200
2886
  }
3201
2887
  );
3202
2888
  server2.tool(
3203
- "get_post_aggregate_stats",
3204
- "Get aggregate stats across all posts and newsletters.",
3205
- {},
2889
+ "tag_subscriber",
2890
+ "Add tags to a subscriber.",
3206
2891
  {
3207
- title: "Get Post Aggregate Stats",
3208
- readOnlyHint: true,
3209
- destructiveHint: false,
3210
- idempotentHint: true,
3211
- openWorldHint: true
3212
- },
3213
- async () => {
3214
- const result = await client2.getPostAggregateStats();
3215
- return {
3216
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3217
- };
3218
- }
3219
- );
3220
- server2.tool(
3221
- "tag_subscriber",
3222
- "Add tags to a subscriber.",
3223
- {
3224
- subscriber_id: z14.string().describe("The subscriber/subscription ID"),
3225
- tags: z14.array(z14.string()).describe("Tags to add to the subscriber")
2892
+ subscriber_id: z12.string().describe("The subscriber/subscription ID"),
2893
+ tags: z12.array(z12.string()).describe("Tags to add to the subscriber")
3226
2894
  },
3227
2895
  {
3228
2896
  title: "Tag Subscriber",
@@ -3242,10 +2910,10 @@ function registerNewsletterAdvancedTools(server2, client2) {
3242
2910
  "update_subscriber",
3243
2911
  "Update subscriber details or custom fields. Set confirmed=false to preview first.",
3244
2912
  {
3245
- subscriber_id: z14.string().describe("The subscriber/subscription ID"),
3246
- tier: z14.string().optional().describe("New tier for the subscriber"),
3247
- custom_fields: z14.record(z14.unknown()).optional().describe("Custom field values to set"),
3248
- confirmed: z14.boolean().default(false).describe("Set to true to confirm update")
2913
+ subscriber_id: z12.string().describe("The subscriber/subscription ID"),
2914
+ tier: z12.string().optional().describe("New tier for the subscriber"),
2915
+ custom_fields: z12.record(z12.unknown()).optional().describe("Custom field values to set"),
2916
+ confirmed: z12.boolean().default(false).describe("Set to true to confirm update")
3249
2917
  },
3250
2918
  {
3251
2919
  title: "Update Subscriber",
@@ -3284,8 +2952,8 @@ Call again with confirmed=true to proceed.`
3284
2952
  "delete_subscriber",
3285
2953
  "Remove a subscriber. Set confirmed=false to preview first.",
3286
2954
  {
3287
- subscriber_id: z14.string().describe("The subscriber ID to delete"),
3288
- confirmed: z14.boolean().default(false).describe("Set to true to confirm deletion")
2955
+ subscriber_id: z12.string().describe("The subscriber ID to delete"),
2956
+ confirmed: z12.boolean().default(false).describe("Set to true to confirm deletion")
3289
2957
  },
3290
2958
  {
3291
2959
  title: "Delete Subscriber",
@@ -3317,6 +2985,823 @@ Call again with confirmed=true to delete.`
3317
2985
  );
3318
2986
  }
3319
2987
 
2988
+ // src/tools/wordpress.ts
2989
+ import { z as z13 } from "zod";
2990
+ function registerWordPressTools(server2, client2) {
2991
+ server2.tool(
2992
+ "wp_create_post",
2993
+ "Create a new WordPress post. Defaults to draft status. Set confirmed=false to preview before publishing.",
2994
+ {
2995
+ title: z13.string().optional().describe("Post title"),
2996
+ content: z13.string().optional().describe("Post content (HTML)"),
2997
+ status: z13.enum(["draft", "publish", "pending", "private", "future"]).default("draft").describe("Post status. Use 'future' with date for scheduling."),
2998
+ categories: z13.array(z13.number()).optional().describe("Array of category IDs"),
2999
+ tags: z13.array(z13.number()).optional().describe("Array of tag IDs"),
3000
+ featured_media: z13.number().optional().describe("Featured image attachment ID"),
3001
+ excerpt: z13.string().optional().describe("Post excerpt"),
3002
+ date: z13.string().optional().describe(
3003
+ "Publication date in ISO 8601 format. Required when status is 'future'."
3004
+ ),
3005
+ slug: z13.string().optional().describe("URL slug for the post"),
3006
+ confirmed: z13.boolean().default(false).describe(
3007
+ "Set to true to confirm creation. If false, returns a preview for approval. Required when status is 'publish'."
3008
+ )
3009
+ },
3010
+ {
3011
+ title: "Create WordPress Post",
3012
+ readOnlyHint: false,
3013
+ destructiveHint: false,
3014
+ idempotentHint: false,
3015
+ openWorldHint: true
3016
+ },
3017
+ async (args) => {
3018
+ if (args.confirmed !== true && (args.status === "publish" || args.status === "future")) {
3019
+ const preview = `## WordPress Post Preview
3020
+
3021
+ **Title:** ${args.title ?? "(no title)"}
3022
+ **Status:** ${args.status}
3023
+ **Content length:** ${(args.content ?? "").length} characters
3024
+ ` + (args.categories?.length ? `**Categories:** ${args.categories.join(", ")}
3025
+ ` : "") + (args.tags?.length ? `**Tags:** ${args.tags.join(", ")}
3026
+ ` : "") + (args.date ? `**Date:** ${args.date}
3027
+ ` : "") + (args.slug ? `**Slug:** ${args.slug}
3028
+ ` : "") + (args.excerpt ? `**Excerpt:** ${args.excerpt.slice(0, 200)}
3029
+ ` : "") + `
3030
+ **Action:** This will ${args.status === "publish" ? "publish immediately" : "schedule"} on your WordPress site.
3031
+
3032
+ Call this tool again with confirmed=true to proceed.`;
3033
+ return { content: [{ type: "text", text: preview }] };
3034
+ }
3035
+ const body = {
3036
+ title: args.title,
3037
+ content: args.content,
3038
+ status: args.status
3039
+ };
3040
+ if (args.categories) body.categories = args.categories;
3041
+ if (args.tags) body.tags = args.tags;
3042
+ if (args.featured_media) body.featured_media = args.featured_media;
3043
+ if (args.excerpt) body.excerpt = args.excerpt;
3044
+ if (args.date) body.date = args.date;
3045
+ if (args.slug) body.slug = args.slug;
3046
+ const result = await client2.wpCreatePost(body);
3047
+ return {
3048
+ content: [
3049
+ { type: "text", text: JSON.stringify(result, null, 2) }
3050
+ ]
3051
+ };
3052
+ }
3053
+ );
3054
+ server2.tool(
3055
+ "wp_update_post",
3056
+ "Update an existing WordPress post by ID.",
3057
+ {
3058
+ post_id: z13.number().describe("The WordPress post ID to update"),
3059
+ title: z13.string().optional().describe("New post title"),
3060
+ content: z13.string().optional().describe("New post content (HTML)"),
3061
+ status: z13.enum(["draft", "publish", "pending", "private", "future"]).optional().describe("New post status"),
3062
+ categories: z13.array(z13.number()).optional().describe("New category IDs"),
3063
+ tags: z13.array(z13.number()).optional().describe("New tag IDs"),
3064
+ featured_media: z13.number().optional().describe("New featured image attachment ID"),
3065
+ excerpt: z13.string().optional().describe("New post excerpt"),
3066
+ date: z13.string().optional().describe("New publication date (ISO 8601)"),
3067
+ slug: z13.string().optional().describe("New URL slug")
3068
+ },
3069
+ {
3070
+ title: "Update WordPress Post",
3071
+ readOnlyHint: false,
3072
+ destructiveHint: false,
3073
+ idempotentHint: true,
3074
+ openWorldHint: true
3075
+ },
3076
+ async (args) => {
3077
+ const { post_id, ...data } = args;
3078
+ const result = await client2.wpUpdatePost(post_id, data);
3079
+ return {
3080
+ content: [
3081
+ { type: "text", text: JSON.stringify(result, null, 2) }
3082
+ ]
3083
+ };
3084
+ }
3085
+ );
3086
+ server2.tool(
3087
+ "wp_publish_post",
3088
+ "Change a WordPress post's status to 'publish'. Requires confirmation.",
3089
+ {
3090
+ post_id: z13.number().describe("The WordPress post ID to publish"),
3091
+ confirmed: z13.boolean().default(false).describe(
3092
+ "Set to true to confirm publishing. If false, returns a preview."
3093
+ )
3094
+ },
3095
+ {
3096
+ title: "Publish WordPress Post",
3097
+ readOnlyHint: false,
3098
+ destructiveHint: false,
3099
+ idempotentHint: true,
3100
+ openWorldHint: true
3101
+ },
3102
+ async (args) => {
3103
+ if (args.confirmed !== true) {
3104
+ try {
3105
+ const post = await client2.wpGetPost(args.post_id);
3106
+ const preview = `## Publish WordPress Post
3107
+
3108
+ **Post ID:** ${args.post_id}
3109
+ **Title:** ${post.title?.rendered ?? "(no title)"}
3110
+ **Current Status:** ${post.status ?? "unknown"}
3111
+
3112
+ This will publish this post on your WordPress site, making it publicly visible.
3113
+
3114
+ Call this tool again with confirmed=true to proceed.`;
3115
+ return { content: [{ type: "text", text: preview }] };
3116
+ } catch {
3117
+ const preview = `## Publish WordPress Post
3118
+
3119
+ **Post ID:** ${args.post_id}
3120
+
3121
+ This will publish this post. Call this tool again with confirmed=true to proceed.`;
3122
+ return { content: [{ type: "text", text: preview }] };
3123
+ }
3124
+ }
3125
+ const result = await client2.wpUpdatePost(args.post_id, {
3126
+ status: "publish"
3127
+ });
3128
+ return {
3129
+ content: [
3130
+ { type: "text", text: JSON.stringify(result, null, 2) }
3131
+ ]
3132
+ };
3133
+ }
3134
+ );
3135
+ server2.tool(
3136
+ "wp_list_posts",
3137
+ "List WordPress posts with optional filters.",
3138
+ {
3139
+ status: z13.string().optional().describe(
3140
+ "Filter by status: draft, publish, pending, private, future, trash"
3141
+ ),
3142
+ per_page: z13.string().optional().describe("Number of posts per page (default 10, max 100)"),
3143
+ page: z13.string().optional().describe("Page number for pagination"),
3144
+ search: z13.string().optional().describe("Search posts by keyword"),
3145
+ categories: z13.string().optional().describe("Filter by category ID (comma-separated)"),
3146
+ tags: z13.string().optional().describe("Filter by tag ID (comma-separated)")
3147
+ },
3148
+ {
3149
+ title: "List WordPress Posts",
3150
+ readOnlyHint: true,
3151
+ destructiveHint: false,
3152
+ idempotentHint: true,
3153
+ openWorldHint: true
3154
+ },
3155
+ async (args) => {
3156
+ const params = {};
3157
+ if (args.status) params.status = args.status;
3158
+ if (args.per_page) params.per_page = args.per_page;
3159
+ if (args.page) params.page = args.page;
3160
+ if (args.search) params.search = args.search;
3161
+ if (args.categories) params.categories = args.categories;
3162
+ if (args.tags) params.tags = args.tags;
3163
+ const result = await client2.wpListPosts(params);
3164
+ return {
3165
+ content: [
3166
+ { type: "text", text: JSON.stringify(result, null, 2) }
3167
+ ]
3168
+ };
3169
+ }
3170
+ );
3171
+ server2.tool(
3172
+ "wp_get_post",
3173
+ "Get detailed information about a specific WordPress post by ID.",
3174
+ {
3175
+ post_id: z13.number().describe("The WordPress post ID to retrieve")
3176
+ },
3177
+ {
3178
+ title: "Get WordPress Post",
3179
+ readOnlyHint: true,
3180
+ destructiveHint: false,
3181
+ idempotentHint: true,
3182
+ openWorldHint: true
3183
+ },
3184
+ async (args) => {
3185
+ const result = await client2.wpGetPost(args.post_id);
3186
+ return {
3187
+ content: [
3188
+ { type: "text", text: JSON.stringify(result, null, 2) }
3189
+ ]
3190
+ };
3191
+ }
3192
+ );
3193
+ server2.tool(
3194
+ "wp_delete_post",
3195
+ "Trash a WordPress post. Use force=true to permanently delete. Requires confirmation.",
3196
+ {
3197
+ post_id: z13.number().describe("The WordPress post ID to delete"),
3198
+ force: z13.boolean().default(false).describe("Set to true to permanently delete instead of trashing"),
3199
+ confirmed: z13.boolean().default(false).describe(
3200
+ "Set to true to confirm deletion. If false, returns a preview."
3201
+ )
3202
+ },
3203
+ {
3204
+ title: "Delete WordPress Post",
3205
+ readOnlyHint: false,
3206
+ destructiveHint: true,
3207
+ idempotentHint: true,
3208
+ openWorldHint: true
3209
+ },
3210
+ async (args) => {
3211
+ if (args.confirmed !== true) {
3212
+ const action = args.force ? "permanently delete" : "move to trash";
3213
+ const preview = `## Delete WordPress Post
3214
+
3215
+ **Post ID:** ${args.post_id}
3216
+ **Action:** ${action}
3217
+
3218
+ ${args.force ? "WARNING: This will permanently delete the post and cannot be undone.\n\n" : "The post will be moved to trash and can be restored later.\n\n"}Call this tool again with confirmed=true to proceed.`;
3219
+ return { content: [{ type: "text", text: preview }] };
3220
+ }
3221
+ const params = {};
3222
+ if (args.force) params.force = "true";
3223
+ const result = await client2.wpDeletePost(args.post_id, params);
3224
+ return {
3225
+ content: [
3226
+ {
3227
+ type: "text",
3228
+ text: args.force ? "Post permanently deleted." : JSON.stringify(result, null, 2)
3229
+ }
3230
+ ]
3231
+ };
3232
+ }
3233
+ );
3234
+ server2.tool(
3235
+ "wp_upload_media",
3236
+ "Upload an image from a URL to the WordPress media library.",
3237
+ {
3238
+ url: z13.string().url().describe("Public URL of the image to upload to WordPress"),
3239
+ filename: z13.string().optional().describe("Optional filename for the uploaded image")
3240
+ },
3241
+ {
3242
+ title: "Upload WordPress Media",
3243
+ readOnlyHint: false,
3244
+ destructiveHint: false,
3245
+ idempotentHint: false,
3246
+ openWorldHint: true
3247
+ },
3248
+ async (args) => {
3249
+ const result = await client2.wpUploadMedia({
3250
+ url: args.url,
3251
+ filename: args.filename
3252
+ });
3253
+ return {
3254
+ content: [
3255
+ { type: "text", text: JSON.stringify(result, null, 2) }
3256
+ ]
3257
+ };
3258
+ }
3259
+ );
3260
+ server2.tool(
3261
+ "wp_list_categories",
3262
+ "List all available WordPress categories.",
3263
+ {},
3264
+ {
3265
+ title: "List WordPress Categories",
3266
+ readOnlyHint: true,
3267
+ destructiveHint: false,
3268
+ idempotentHint: true,
3269
+ openWorldHint: true
3270
+ },
3271
+ async () => {
3272
+ const result = await client2.wpListCategories();
3273
+ return {
3274
+ content: [
3275
+ { type: "text", text: JSON.stringify(result, null, 2) }
3276
+ ]
3277
+ };
3278
+ }
3279
+ );
3280
+ server2.tool(
3281
+ "wp_list_tags",
3282
+ "List all available WordPress tags.",
3283
+ {},
3284
+ {
3285
+ title: "List WordPress Tags",
3286
+ readOnlyHint: true,
3287
+ destructiveHint: false,
3288
+ idempotentHint: true,
3289
+ openWorldHint: true
3290
+ },
3291
+ async () => {
3292
+ const result = await client2.wpListTags();
3293
+ return {
3294
+ content: [
3295
+ { type: "text", text: JSON.stringify(result, null, 2) }
3296
+ ]
3297
+ };
3298
+ }
3299
+ );
3300
+ server2.tool(
3301
+ "wp_create_page",
3302
+ "Create a new WordPress page. Defaults to draft status.",
3303
+ {
3304
+ title: z13.string().optional().describe("Page title"),
3305
+ content: z13.string().optional().describe("Page content (HTML)"),
3306
+ status: z13.enum(["draft", "publish", "pending", "private"]).default("draft").describe("Page status"),
3307
+ excerpt: z13.string().optional().describe("Page excerpt"),
3308
+ slug: z13.string().optional().describe("URL slug for the page"),
3309
+ featured_media: z13.number().optional().describe("Featured image attachment ID"),
3310
+ confirmed: z13.boolean().default(false).describe(
3311
+ "Set to true to confirm creation. Required when status is 'publish'."
3312
+ )
3313
+ },
3314
+ {
3315
+ title: "Create WordPress Page",
3316
+ readOnlyHint: false,
3317
+ destructiveHint: false,
3318
+ idempotentHint: false,
3319
+ openWorldHint: true
3320
+ },
3321
+ async (args) => {
3322
+ if (args.confirmed !== true && args.status === "publish") {
3323
+ const preview = `## WordPress Page Preview
3324
+
3325
+ **Title:** ${args.title ?? "(no title)"}
3326
+ **Status:** ${args.status}
3327
+ **Content length:** ${(args.content ?? "").length} characters
3328
+ ` + (args.slug ? `**Slug:** ${args.slug}
3329
+ ` : "") + `
3330
+ This will publish the page immediately on your WordPress site.
3331
+
3332
+ Call this tool again with confirmed=true to proceed.`;
3333
+ return { content: [{ type: "text", text: preview }] };
3334
+ }
3335
+ const body = {
3336
+ title: args.title,
3337
+ content: args.content,
3338
+ status: args.status
3339
+ };
3340
+ if (args.excerpt) body.excerpt = args.excerpt;
3341
+ if (args.slug) body.slug = args.slug;
3342
+ if (args.featured_media) body.featured_media = args.featured_media;
3343
+ const result = await client2.wpCreatePage(body);
3344
+ return {
3345
+ content: [
3346
+ { type: "text", text: JSON.stringify(result, null, 2) }
3347
+ ]
3348
+ };
3349
+ }
3350
+ );
3351
+ }
3352
+
3353
+ // src/tools/ghost.ts
3354
+ import { z as z14 } from "zod";
3355
+ function registerGhostTools(server2, client2) {
3356
+ server2.tool(
3357
+ "ghost_create_post",
3358
+ "Create a post on Ghost. Defaults to draft status. Set confirmed=false to preview before publishing.",
3359
+ {
3360
+ title: z14.string().describe("Post title"),
3361
+ html: z14.string().optional().describe("HTML content of the post"),
3362
+ status: z14.enum(["draft", "published", "scheduled"]).default("draft").describe("Post status: draft, published, or scheduled"),
3363
+ tags: z14.array(z14.string()).optional().describe("Tag names to apply to the post"),
3364
+ featured: z14.boolean().optional().describe("Whether the post is featured"),
3365
+ custom_excerpt: z14.string().optional().describe("Custom excerpt for the post"),
3366
+ feature_image: z14.string().optional().describe("URL of the feature image"),
3367
+ confirmed: z14.boolean().default(false).describe(
3368
+ "Set to true to confirm creation. Required when status is 'published'. Drafts are created immediately."
3369
+ )
3370
+ },
3371
+ {
3372
+ title: "Create Ghost Post",
3373
+ readOnlyHint: false,
3374
+ destructiveHint: false,
3375
+ idempotentHint: false,
3376
+ openWorldHint: true
3377
+ },
3378
+ async (args) => {
3379
+ const needsConfirmation = args.status !== "draft";
3380
+ if (needsConfirmation && args.confirmed !== true) {
3381
+ const preview = `## Ghost Post Preview
3382
+
3383
+ **Title:** ${args.title}
3384
+ **Status:** ${args.status}
3385
+ ` + (args.tags?.length ? `**Tags:** ${args.tags.join(", ")}
3386
+ ` : "") + (args.featured ? `**Featured:** yes
3387
+ ` : "") + (args.custom_excerpt ? `**Excerpt:** ${args.custom_excerpt}
3388
+ ` : "") + (args.feature_image ? `**Feature Image:** ${args.feature_image}
3389
+ ` : "") + (args.html ? `
3390
+ **Content preview:** ${args.html.replace(/<[^>]*>/g, "").slice(0, 200)}...
3391
+ ` : "") + `
3392
+ **Action:** This will be **${args.status}** on Ghost immediately.
3393
+
3394
+ Call this tool again with confirmed=true to proceed.`;
3395
+ return { content: [{ type: "text", text: preview }] };
3396
+ }
3397
+ const body = {
3398
+ title: args.title,
3399
+ status: args.status
3400
+ };
3401
+ if (args.html) body.html = args.html;
3402
+ if (args.tags) body.tags = args.tags.map((name) => ({ name }));
3403
+ if (args.featured !== void 0) body.featured = args.featured;
3404
+ if (args.custom_excerpt) body.custom_excerpt = args.custom_excerpt;
3405
+ if (args.feature_image) body.feature_image = args.feature_image;
3406
+ const result = await client2.ghostCreatePost(body);
3407
+ return {
3408
+ content: [
3409
+ { type: "text", text: JSON.stringify(result, null, 2) }
3410
+ ]
3411
+ };
3412
+ }
3413
+ );
3414
+ server2.tool(
3415
+ "ghost_update_post",
3416
+ "Update an existing Ghost post. Requires updated_at for collision detection.",
3417
+ {
3418
+ post_id: z14.string().describe("The Ghost post ID to update"),
3419
+ updated_at: z14.string().describe(
3420
+ "The updated_at value from the post (required for collision detection)"
3421
+ ),
3422
+ title: z14.string().optional().describe("Updated post title"),
3423
+ html: z14.string().optional().describe("Updated HTML content"),
3424
+ status: z14.enum(["draft", "published", "scheduled"]).optional().describe("Updated status"),
3425
+ tags: z14.array(z14.string()).optional().describe("Updated tag names"),
3426
+ featured: z14.boolean().optional().describe("Whether the post is featured"),
3427
+ custom_excerpt: z14.string().optional().describe("Updated custom excerpt"),
3428
+ feature_image: z14.string().optional().describe("Updated feature image URL")
3429
+ },
3430
+ {
3431
+ title: "Update Ghost Post",
3432
+ readOnlyHint: false,
3433
+ destructiveHint: false,
3434
+ idempotentHint: true,
3435
+ openWorldHint: true
3436
+ },
3437
+ async (args) => {
3438
+ const data = {
3439
+ updated_at: args.updated_at
3440
+ };
3441
+ if (args.title) data.title = args.title;
3442
+ if (args.html) data.html = args.html;
3443
+ if (args.status) data.status = args.status;
3444
+ if (args.tags) data.tags = args.tags.map((name) => ({ name }));
3445
+ if (args.featured !== void 0) data.featured = args.featured;
3446
+ if (args.custom_excerpt) data.custom_excerpt = args.custom_excerpt;
3447
+ if (args.feature_image) data.feature_image = args.feature_image;
3448
+ const result = await client2.ghostUpdatePost(args.post_id, data);
3449
+ return {
3450
+ content: [
3451
+ { type: "text", text: JSON.stringify(result, null, 2) }
3452
+ ]
3453
+ };
3454
+ }
3455
+ );
3456
+ server2.tool(
3457
+ "ghost_publish_post",
3458
+ "Publish a Ghost draft post. This makes the post live. Set confirmed=false to preview first.",
3459
+ {
3460
+ post_id: z14.string().describe("The Ghost post ID to publish"),
3461
+ updated_at: z14.string().describe(
3462
+ "The updated_at value from the post (required for collision detection)"
3463
+ ),
3464
+ confirmed: z14.boolean().default(false).describe(
3465
+ "Set to true to confirm publishing. If false or missing, returns a confirmation prompt."
3466
+ )
3467
+ },
3468
+ {
3469
+ title: "Publish Ghost Post",
3470
+ readOnlyHint: false,
3471
+ destructiveHint: false,
3472
+ idempotentHint: false,
3473
+ openWorldHint: true
3474
+ },
3475
+ async (args) => {
3476
+ if (args.confirmed !== true) {
3477
+ let postInfo = "";
3478
+ try {
3479
+ const post = await client2.ghostGetPost(args.post_id);
3480
+ postInfo = `**Title:** ${post?.title ?? "Unknown"}
3481
+ **Current Status:** ${post?.status ?? "Unknown"}
3482
+ ` + (post?.tags?.length ? `**Tags:** ${post.tags.map((t) => t.name).join(", ")}
3483
+ ` : "");
3484
+ } catch {
3485
+ postInfo = `**Post ID:** ${args.post_id}
3486
+ `;
3487
+ }
3488
+ const preview = `## Publish Ghost Post Confirmation
3489
+
3490
+ ` + postInfo + `
3491
+ **Action:** This will make the post **live** on your Ghost site.
3492
+
3493
+ Call this tool again with confirmed=true to publish.`;
3494
+ return { content: [{ type: "text", text: preview }] };
3495
+ }
3496
+ const result = await client2.ghostPublishPost(args.post_id, {
3497
+ updated_at: args.updated_at
3498
+ });
3499
+ return {
3500
+ content: [
3501
+ { type: "text", text: `Post published successfully.
3502
+
3503
+ ${JSON.stringify(result, null, 2)}` }
3504
+ ]
3505
+ };
3506
+ }
3507
+ );
3508
+ server2.tool(
3509
+ "ghost_list_posts",
3510
+ "List posts from Ghost with optional status filter.",
3511
+ {
3512
+ status: z14.enum(["draft", "published", "scheduled"]).optional().describe("Filter posts by status"),
3513
+ limit: z14.string().optional().describe("Number of posts to return (default: 15)"),
3514
+ page: z14.string().optional().describe("Page number for pagination")
3515
+ },
3516
+ {
3517
+ title: "List Ghost Posts",
3518
+ readOnlyHint: true,
3519
+ destructiveHint: false,
3520
+ idempotentHint: true,
3521
+ openWorldHint: true
3522
+ },
3523
+ async (args) => {
3524
+ const params = {};
3525
+ if (args.status) params.status = args.status;
3526
+ if (args.limit) params.limit = args.limit;
3527
+ if (args.page) params.page = args.page;
3528
+ const result = await client2.ghostListPosts(params);
3529
+ const posts = result?.posts ?? [];
3530
+ if (posts.length === 0) {
3531
+ return {
3532
+ content: [
3533
+ {
3534
+ type: "text",
3535
+ text: "No Ghost posts found matching your filters."
3536
+ }
3537
+ ]
3538
+ };
3539
+ }
3540
+ let text = `## Ghost Posts (${posts.length}`;
3541
+ if (result?.meta?.pagination?.total != null) {
3542
+ text += ` of ${result.meta.pagination.total}`;
3543
+ }
3544
+ text += ")\n\n";
3545
+ for (const p of posts) {
3546
+ const status = (p.status ?? "unknown").toUpperCase();
3547
+ const date = p.published_at ? new Date(p.published_at).toLocaleString() : p.created_at ? new Date(p.created_at).toLocaleString() : "";
3548
+ const tags = p.tags?.length > 0 ? ` | Tags: ${p.tags.map((t) => t.name).join(", ")}` : "";
3549
+ text += `- **[${status}]** "${p.title ?? "(no title)"}"
3550
+ `;
3551
+ text += ` ID: \`${p.id}\``;
3552
+ if (date) text += ` | ${p.published_at ? "Published" : "Created"}: ${date}`;
3553
+ text += tags;
3554
+ text += "\n";
3555
+ }
3556
+ return { content: [{ type: "text", text }] };
3557
+ }
3558
+ );
3559
+ server2.tool(
3560
+ "ghost_get_post",
3561
+ "Get detailed information about a specific Ghost post by ID.",
3562
+ {
3563
+ post_id: z14.string().describe("The Ghost post ID to retrieve")
3564
+ },
3565
+ {
3566
+ title: "Get Ghost Post",
3567
+ readOnlyHint: true,
3568
+ destructiveHint: false,
3569
+ idempotentHint: true,
3570
+ openWorldHint: true
3571
+ },
3572
+ async (args) => {
3573
+ const result = await client2.ghostGetPost(args.post_id);
3574
+ let text = `## Ghost Post Details
3575
+
3576
+ `;
3577
+ text += `**Title:** ${result?.title ?? "Unknown"}
3578
+ `;
3579
+ text += `**ID:** \`${result?.id}\`
3580
+ `;
3581
+ text += `**Status:** ${result?.status ?? "Unknown"}
3582
+ `;
3583
+ text += `**Slug:** ${result?.slug ?? "N/A"}
3584
+ `;
3585
+ if (result?.url) text += `**URL:** ${result.url}
3586
+ `;
3587
+ if (result?.custom_excerpt) text += `**Excerpt:** ${result.custom_excerpt}
3588
+ `;
3589
+ if (result?.feature_image) text += `**Feature Image:** ${result.feature_image}
3590
+ `;
3591
+ if (result?.featured) text += `**Featured:** yes
3592
+ `;
3593
+ if (result?.tags?.length > 0) {
3594
+ text += `**Tags:** ${result.tags.map((t) => t.name).join(", ")}
3595
+ `;
3596
+ }
3597
+ if (result?.authors?.length > 0) {
3598
+ text += `**Authors:** ${result.authors.map((a) => a.name).join(", ")}
3599
+ `;
3600
+ }
3601
+ if (result?.published_at) {
3602
+ text += `**Published:** ${new Date(result.published_at).toLocaleString()}
3603
+ `;
3604
+ }
3605
+ text += `**Created:** ${result?.created_at ? new Date(result.created_at).toLocaleString() : "N/A"}
3606
+ `;
3607
+ text += `**Updated:** ${result?.updated_at ?? "N/A"}
3608
+ `;
3609
+ text += `
3610
+ *Use updated_at value \`${result?.updated_at}\` when updating or publishing this post.*
3611
+ `;
3612
+ if (result?.html) {
3613
+ const plainText = result.html.replace(/<[^>]*>/g, "");
3614
+ const preview = plainText.length > 500 ? plainText.slice(0, 500) + "..." : plainText;
3615
+ text += `
3616
+ ### Content Preview
3617
+ ${preview}
3618
+ `;
3619
+ }
3620
+ return { content: [{ type: "text", text }] };
3621
+ }
3622
+ );
3623
+ server2.tool(
3624
+ "ghost_send_newsletter",
3625
+ "Send a Ghost post as a newsletter to subscribers. The post must exist as a draft. This action cannot be undone. Set confirmed=false to preview first.",
3626
+ {
3627
+ post_id: z14.string().describe("The Ghost post ID to send as newsletter"),
3628
+ updated_at: z14.string().describe("The updated_at value from the post"),
3629
+ newsletter_id: z14.string().describe(
3630
+ "The Ghost newsletter ID to send through. Use ghost_list_newsletters to find available newsletters."
3631
+ ),
3632
+ email_only: z14.boolean().default(false).describe(
3633
+ "If true, sends email only without publishing the post on the site"
3634
+ ),
3635
+ confirmed: z14.boolean().default(false).describe(
3636
+ "Set to true to confirm and send. If false or missing, returns a confirmation prompt."
3637
+ )
3638
+ },
3639
+ {
3640
+ title: "Send Ghost Newsletter",
3641
+ readOnlyHint: false,
3642
+ destructiveHint: false,
3643
+ idempotentHint: false,
3644
+ openWorldHint: true
3645
+ },
3646
+ async (args) => {
3647
+ if (args.confirmed !== true) {
3648
+ let postInfo = "";
3649
+ let memberCount = "unknown";
3650
+ let newsletterName = "unknown";
3651
+ try {
3652
+ const post = await client2.ghostGetPost(args.post_id);
3653
+ postInfo = `**Title:** ${post?.title ?? "Unknown"}
3654
+ **Current Status:** ${post?.status ?? "Unknown"}
3655
+ `;
3656
+ } catch {
3657
+ postInfo = `**Post ID:** ${args.post_id}
3658
+ `;
3659
+ }
3660
+ try {
3661
+ const members = await client2.ghostListMembers({ limit: "1" });
3662
+ memberCount = String(
3663
+ members?.meta?.pagination?.total ?? "unknown"
3664
+ );
3665
+ } catch {
3666
+ }
3667
+ try {
3668
+ const newsletters = await client2.ghostListNewsletters();
3669
+ const nl = (newsletters?.newsletters ?? []).find(
3670
+ (n) => n.id === args.newsletter_id
3671
+ );
3672
+ if (nl) newsletterName = nl.name;
3673
+ } catch {
3674
+ }
3675
+ const preview = `## Send Ghost Newsletter Confirmation
3676
+
3677
+ ` + postInfo + `**Newsletter:** ${newsletterName} (\`${args.newsletter_id}\`)
3678
+ **Members:** ~${memberCount} subscribers
3679
+ **Email Only:** ${args.email_only ? "yes (post will NOT be published on site)" : "no (post will also be published)"}
3680
+
3681
+ **This action cannot be undone.** Once sent, the email goes to all newsletter subscribers.
3682
+
3683
+ Call this tool again with confirmed=true to send.`;
3684
+ return { content: [{ type: "text", text: preview }] };
3685
+ }
3686
+ const result = await client2.ghostSendNewsletter(args.post_id, {
3687
+ updated_at: args.updated_at,
3688
+ newsletter_id: args.newsletter_id,
3689
+ email_only: args.email_only
3690
+ });
3691
+ return {
3692
+ content: [
3693
+ {
3694
+ type: "text",
3695
+ text: `Newsletter sent successfully.
3696
+
3697
+ ${JSON.stringify(result, null, 2)}`
3698
+ }
3699
+ ]
3700
+ };
3701
+ }
3702
+ );
3703
+ server2.tool(
3704
+ "ghost_list_members",
3705
+ "List Ghost newsletter members (subscribers).",
3706
+ {
3707
+ limit: z14.string().optional().describe("Number of members to return (default: 15)"),
3708
+ page: z14.string().optional().describe("Page number for pagination"),
3709
+ filter: z14.string().optional().describe(
3710
+ "Ghost NQL filter string, e.g. 'status:free' or 'subscribed:true'"
3711
+ )
3712
+ },
3713
+ {
3714
+ title: "List Ghost Members",
3715
+ readOnlyHint: true,
3716
+ destructiveHint: false,
3717
+ idempotentHint: true,
3718
+ openWorldHint: true
3719
+ },
3720
+ async (args) => {
3721
+ const params = {};
3722
+ if (args.limit) params.limit = args.limit;
3723
+ if (args.page) params.page = args.page;
3724
+ if (args.filter) params.filter = args.filter;
3725
+ const result = await client2.ghostListMembers(params);
3726
+ const members = result?.members ?? [];
3727
+ if (members.length === 0) {
3728
+ return {
3729
+ content: [
3730
+ {
3731
+ type: "text",
3732
+ text: "No Ghost members found matching your filters."
3733
+ }
3734
+ ]
3735
+ };
3736
+ }
3737
+ let text = `## Ghost Members (${members.length}`;
3738
+ if (result?.meta?.pagination?.total != null) {
3739
+ text += ` of ${result.meta.pagination.total}`;
3740
+ }
3741
+ text += ")\n\n";
3742
+ for (const m of members) {
3743
+ const status = m.status ?? "unknown";
3744
+ text += `- **${m.name || m.email}**`;
3745
+ if (m.name && m.email) text += ` (${m.email})`;
3746
+ text += ` | Status: ${status}`;
3747
+ if (m.created_at) {
3748
+ text += ` | Joined: ${new Date(m.created_at).toLocaleDateString()}`;
3749
+ }
3750
+ text += "\n";
3751
+ }
3752
+ return { content: [{ type: "text", text }] };
3753
+ }
3754
+ );
3755
+ server2.tool(
3756
+ "ghost_list_newsletters",
3757
+ "List configured newsletters in Ghost.",
3758
+ {},
3759
+ {
3760
+ title: "List Ghost Newsletters",
3761
+ readOnlyHint: true,
3762
+ destructiveHint: false,
3763
+ idempotentHint: true,
3764
+ openWorldHint: true
3765
+ },
3766
+ async () => {
3767
+ const result = await client2.ghostListNewsletters();
3768
+ const newsletters = result?.newsletters ?? [];
3769
+ if (newsletters.length === 0) {
3770
+ return {
3771
+ content: [
3772
+ {
3773
+ type: "text",
3774
+ text: "No newsletters configured in Ghost."
3775
+ }
3776
+ ]
3777
+ };
3778
+ }
3779
+ let text = `## Ghost Newsletters (${newsletters.length})
3780
+
3781
+ `;
3782
+ for (const nl of newsletters) {
3783
+ text += `- **${nl.name}**
3784
+ `;
3785
+ text += ` ID: \`${nl.id}\``;
3786
+ if (nl.description) text += ` | ${nl.description}`;
3787
+ text += `
3788
+ Status: ${nl.status ?? "active"}`;
3789
+ if (nl.subscribe_on_signup !== void 0) {
3790
+ text += ` | Subscribe on signup: ${nl.subscribe_on_signup ? "yes" : "no"}`;
3791
+ }
3792
+ if (nl.member_count != null) {
3793
+ text += ` | Members: ${nl.member_count}`;
3794
+ }
3795
+ text += "\n";
3796
+ }
3797
+ text += `
3798
+ *Use the newsletter ID when sending a post as a newsletter with ghost_send_newsletter.*
3799
+ `;
3800
+ return { content: [{ type: "text", text }] };
3801
+ }
3802
+ );
3803
+ }
3804
+
3320
3805
  // src/index.ts
3321
3806
  var apiKey = process.env.BUZZPOSTER_API_KEY;
3322
3807
  var apiUrl = process.env.BUZZPOSTER_API_URL ?? "https://api.buzzposter.com";
@@ -3325,7 +3810,7 @@ if (!apiKey) {
3325
3810
  "Error: BUZZPOSTER_API_KEY environment variable is required."
3326
3811
  );
3327
3812
  console.error(
3328
- "Set it in your Claude Desktop MCP config or export it in your shell."
3813
+ "Set it in your MCP config or export it in your shell."
3329
3814
  );
3330
3815
  process.exit(1);
3331
3816
  }
@@ -3333,11 +3818,21 @@ var client = new BuzzPosterClient({ baseUrl: apiUrl, apiKey });
3333
3818
  var server = new McpServer(
3334
3819
  {
3335
3820
  name: "buzzposter",
3336
- version: "0.1.0"
3821
+ version: "0.1.0",
3822
+ icons: [
3823
+ {
3824
+ src: "https://api.buzzposter.com/favicon.svg",
3825
+ mimeType: "image/svg+xml",
3826
+ sizes: ["any"]
3827
+ },
3828
+ {
3829
+ src: "https://api.buzzposter.com/favicon.ico",
3830
+ mimeType: "image/x-icon",
3831
+ sizes: ["48x48"]
3832
+ }
3833
+ ]
3337
3834
  },
3338
- {
3339
- instructions: `When a tool response contains \`NEWSLETTER_HTML_PREVIEW:\` followed by HTML content, immediately render that HTML as an artifact so the user sees a live visual preview of the newsletter. Do not ask the user if they want to see it -- just render it automatically.`
3340
- }
3835
+ {}
3341
3836
  );
3342
3837
  var allowDirectSend = false;
3343
3838
  try {
@@ -3354,13 +3849,13 @@ registerNewsletterTools(server, client, { allowDirectSend });
3354
3849
  registerRssTools(server, client);
3355
3850
  registerAccountInfoTool(server, client);
3356
3851
  registerBrandVoiceTools(server, client);
3357
- registerKnowledgeTools(server, client);
3358
3852
  registerAudienceTools(server, client);
3359
3853
  registerNewsletterTemplateTools(server, client);
3360
3854
  registerCalendarTools(server, client);
3361
3855
  registerNotificationTools(server, client);
3362
- registerPublishingRulesTools(server, client);
3363
3856
  registerSourceTools(server, client);
3364
3857
  registerNewsletterAdvancedTools(server, client);
3858
+ registerWordPressTools(server, client);
3859
+ registerGhostTools(server, client);
3365
3860
  var transport = new StdioServerTransport();
3366
3861
  await server.connect(transport);