@buzzposter/mcp 0.1.16 → 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/tools.js CHANGED
@@ -6,8 +6,8 @@ var BuzzPosterClient = class {
6
6
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
7
7
  this.apiKey = config.apiKey;
8
8
  }
9
- async request(method, path, body, query) {
10
- const url = new URL(`${this.baseUrl}${path}`);
9
+ async request(method, path2, body, query) {
10
+ const url = new URL(`${this.baseUrl}${path2}`);
11
11
  if (query) {
12
12
  for (const [k, v] of Object.entries(query)) {
13
13
  if (v !== void 0 && v !== "") url.searchParams.set(k, v);
@@ -164,15 +164,8 @@ var BuzzPosterClient = class {
164
164
  async listPostTemplates() {
165
165
  return this.request("GET", "/api/v1/newsletters/post-templates");
166
166
  }
167
- async getPostAggregateStats() {
168
- return this.request("GET", "/api/v1/newsletters/broadcasts/aggregate-stats");
169
- }
170
- // Knowledge
171
- async listKnowledge(params) {
172
- return this.request("GET", "/api/v1/knowledge", void 0, params);
173
- }
174
- async createKnowledge(data) {
175
- return this.request("POST", "/api/v1/knowledge", data);
167
+ async listEmailTemplates() {
168
+ return this.request("GET", "/api/v1/newsletters/email-templates");
176
169
  }
177
170
  // Brand Voice
178
171
  async getBrandVoice() {
@@ -182,9 +175,15 @@ var BuzzPosterClient = class {
182
175
  return this.request("PUT", "/api/v1/brand-voice", data);
183
176
  }
184
177
  // Audiences
178
+ async listAudiences() {
179
+ return this.request("GET", "/api/v1/audiences");
180
+ }
185
181
  async getAudience(id) {
186
182
  return this.request("GET", `/api/v1/audiences/${id}`);
187
183
  }
184
+ async getAudienceByName(name) {
185
+ return this.request("GET", `/api/v1/audiences/by-name/${encodeURIComponent(name)}`);
186
+ }
188
187
  async getDefaultAudience() {
189
188
  return this.request("GET", "/api/v1/audiences/default");
190
189
  }
@@ -194,6 +193,9 @@ var BuzzPosterClient = class {
194
193
  async updateAudience(id, data) {
195
194
  return this.request("PUT", `/api/v1/audiences/${id}`, data);
196
195
  }
196
+ async deleteAudience(id) {
197
+ return this.request("DELETE", `/api/v1/audiences/${id}`);
198
+ }
197
199
  // Newsletter Templates
198
200
  async getTemplate(id) {
199
201
  if (id) return this.request("GET", `/api/v1/templates/${id}`);
@@ -214,16 +216,6 @@ var BuzzPosterClient = class {
214
216
  async duplicateTemplate(id) {
215
217
  return this.request("POST", "/api/v1/templates/" + id + "/duplicate");
216
218
  }
217
- // Newsletter Archive
218
- async listNewsletterArchive(params) {
219
- return this.request("GET", "/api/v1/newsletter-archive", void 0, params);
220
- }
221
- async getArchivedNewsletter(id) {
222
- return this.request("GET", `/api/v1/newsletter-archive/${id}`);
223
- }
224
- async saveNewsletterToArchive(data) {
225
- return this.request("POST", "/api/v1/newsletter-archive", data);
226
- }
227
219
  // Calendar
228
220
  async getCalendar(params) {
229
221
  return this.request("GET", "/api/v1/calendar", void 0, params);
@@ -285,6 +277,37 @@ var BuzzPosterClient = class {
285
277
  async getPublishingRules() {
286
278
  return this.request("GET", "/api/v1/publishing-rules");
287
279
  }
280
+ // Ghost
281
+ async ghostSetup(data) {
282
+ return this.request("POST", "/api/v1/ghost/setup", data);
283
+ }
284
+ async ghostCreatePost(data) {
285
+ return this.request("POST", "/api/v1/ghost/posts", data);
286
+ }
287
+ async ghostUpdatePost(id, data) {
288
+ return this.request("PUT", `/api/v1/ghost/posts/${id}`, data);
289
+ }
290
+ async ghostGetPost(id) {
291
+ return this.request("GET", `/api/v1/ghost/posts/${id}`);
292
+ }
293
+ async ghostListPosts(params) {
294
+ return this.request("GET", "/api/v1/ghost/posts", void 0, params);
295
+ }
296
+ async ghostDeletePost(id) {
297
+ return this.request("DELETE", `/api/v1/ghost/posts/${id}`);
298
+ }
299
+ async ghostPublishPost(id, data) {
300
+ return this.request("POST", `/api/v1/ghost/posts/${id}/publish`, data);
301
+ }
302
+ async ghostSendNewsletter(id, data) {
303
+ return this.request("POST", `/api/v1/ghost/posts/${id}/newsletter`, data);
304
+ }
305
+ async ghostListMembers(params) {
306
+ return this.request("GET", "/api/v1/ghost/members", void 0, params);
307
+ }
308
+ async ghostListNewsletters() {
309
+ return this.request("GET", "/api/v1/ghost/newsletters");
310
+ }
288
311
  // Newsletter Validation
289
312
  async validateNewsletter(data) {
290
313
  return this.request("POST", "/api/v1/newsletters/validate", data);
@@ -297,6 +320,34 @@ var BuzzPosterClient = class {
297
320
  async testBroadcast(id, data) {
298
321
  return this.request("POST", `/api/v1/newsletters/broadcasts/${id}/test`, data);
299
322
  }
323
+ // WordPress
324
+ async wpCreatePost(data) {
325
+ return this.request("POST", "/api/v1/wordpress/posts", data);
326
+ }
327
+ async wpUpdatePost(id, data) {
328
+ return this.request("PUT", `/api/v1/wordpress/posts/${id}`, data);
329
+ }
330
+ async wpGetPost(id) {
331
+ return this.request("GET", `/api/v1/wordpress/posts/${id}`);
332
+ }
333
+ async wpListPosts(params) {
334
+ return this.request("GET", "/api/v1/wordpress/posts", void 0, params);
335
+ }
336
+ async wpDeletePost(id, params) {
337
+ return this.request("DELETE", `/api/v1/wordpress/posts/${id}`, void 0, params);
338
+ }
339
+ async wpUploadMedia(data) {
340
+ return this.request("POST", "/api/v1/wordpress/media", data);
341
+ }
342
+ async wpListCategories() {
343
+ return this.request("GET", "/api/v1/wordpress/categories");
344
+ }
345
+ async wpListTags() {
346
+ return this.request("GET", "/api/v1/wordpress/tags");
347
+ }
348
+ async wpCreatePage(data) {
349
+ return this.request("POST", "/api/v1/wordpress/pages", data);
350
+ }
300
351
  };
301
352
 
302
353
  // src/tools/posts.ts
@@ -304,7 +355,7 @@ import { z } from "zod";
304
355
  function registerPostTools(server, client) {
305
356
  server.tool(
306
357
  "post",
307
- "Publish a post to one or more social platforms. Set confirmed=false to preview first.",
358
+ "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.",
308
359
  {
309
360
  content: z.string().optional().describe("The text content of the post"),
310
361
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -398,7 +449,7 @@ Call this tool again with confirmed=true to proceed.`;
398
449
  );
399
450
  server.tool(
400
451
  "schedule_post",
401
- "Schedule a post for future publication. Set confirmed=false to preview first.",
452
+ "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.",
402
453
  {
403
454
  content: z.string().optional().describe("The text content of the post"),
404
455
  platforms: z.array(z.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
@@ -491,8 +542,7 @@ Call this tool again with confirmed=true to schedule.`;
491
542
  const body = {
492
543
  content: args.content,
493
544
  platforms: args.platforms.map((p) => ({ platform: p })),
494
- publishNow: false,
495
- status: "draft"
545
+ isDraft: true
496
546
  };
497
547
  if (args.media_urls?.length) {
498
548
  body.mediaItems = args.media_urls.map((url) => ({
@@ -505,7 +555,7 @@ Call this tool again with confirmed=true to schedule.`;
505
555
  return {
506
556
  content: [{
507
557
  type: "text",
508
- text: `ERROR: Post was saved with status "${result.status}" instead of "draft". Post ID: ${result.id ?? "unknown"}. Check the Late dashboard immediately.`
558
+ text: `ERROR: Post was saved with status "${result.status}" instead of "draft". Post ID: ${result.id ?? "unknown"}. Check your BuzzPoster dashboard immediately.`
509
559
  }],
510
560
  isError: true
511
561
  };
@@ -835,7 +885,89 @@ This will post a public reply to the review. Call this tool again with confirmed
835
885
 
836
886
  // src/tools/media.ts
837
887
  import { z as z4 } from "zod";
888
+ var IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
889
+ "image/png",
890
+ "image/jpeg",
891
+ "image/gif",
892
+ "image/webp",
893
+ "image/svg+xml"
894
+ ]);
895
+ var MAX_THUMBNAIL_BYTES = 15e4;
896
+ async function fetchImageAsBase64(url) {
897
+ try {
898
+ const res = await fetch(url);
899
+ if (!res.ok) return null;
900
+ const contentType = res.headers.get("content-type")?.split(";")[0]?.trim() ?? "";
901
+ if (!IMAGE_MIME_TYPES.has(contentType)) return null;
902
+ const buffer = Buffer.from(await res.arrayBuffer());
903
+ if (buffer.byteLength > MAX_THUMBNAIL_BYTES) return null;
904
+ return {
905
+ data: buffer.toString("base64"),
906
+ mimeType: contentType
907
+ };
908
+ } catch {
909
+ return null;
910
+ }
911
+ }
838
912
  function registerMediaTools(server, client) {
913
+ server.tool(
914
+ "get_assets",
915
+ "Get brand assets from the media library with visual thumbnails. Returns inline image previews plus full-resolution URLs for use in posts.",
916
+ {
917
+ limit: z4.number().optional().describe("Max number of assets to return with image previews. Default 10, max 20.")
918
+ },
919
+ {
920
+ title: "Get Brand Assets",
921
+ readOnlyHint: true,
922
+ destructiveHint: false,
923
+ idempotentHint: true,
924
+ openWorldHint: false
925
+ },
926
+ async ({ limit }) => {
927
+ const maxItems = Math.min(limit ?? 10, 20);
928
+ const result = await client.listMedia();
929
+ const items = result?.media ?? [];
930
+ const images = items.filter((m) => IMAGE_MIME_TYPES.has(m.mimeType)).slice(0, maxItems);
931
+ if (images.length === 0) {
932
+ return {
933
+ content: [
934
+ {
935
+ type: "text",
936
+ text: "No image assets found in the media library. Upload images with upload_from_url first."
937
+ }
938
+ ]
939
+ };
940
+ }
941
+ const thumbnails = await Promise.all(
942
+ images.map((img) => fetchImageAsBase64(img.url))
943
+ );
944
+ const content = [];
945
+ content.push({
946
+ type: "text",
947
+ text: `## Brand Assets (${images.length} images)
948
+
949
+ Use the URLs below when attaching images to posts or newsletters.`
950
+ });
951
+ for (let i = 0; i < images.length; i++) {
952
+ const img = images[i];
953
+ const thumb = thumbnails[i];
954
+ content.push({
955
+ type: "text",
956
+ text: `### ${img.filename}
957
+ - **URL:** ${img.url}
958
+ - **Type:** ${img.mimeType} | **Size:** ${(img.size / 1024).toFixed(1)}KB`
959
+ });
960
+ if (thumb) {
961
+ content.push({
962
+ type: "image",
963
+ data: thumb.data,
964
+ mimeType: thumb.mimeType
965
+ });
966
+ }
967
+ }
968
+ return { content };
969
+ }
970
+ );
839
971
  server.tool(
840
972
  "upload_from_url",
841
973
  "Upload media from a public URL to storage. Returns a CDN URL for use in posts.",
@@ -857,11 +989,9 @@ function registerMediaTools(server, client) {
857
989
  filename: args.filename,
858
990
  folder: args.folder
859
991
  });
860
- const item = result;
861
992
  return {
862
993
  content: [
863
- { type: "text", text: JSON.stringify(result, null, 2) },
864
- ...item.type === "image" && item.url && item.mimeType && !item.mimeType.startsWith("video/") ? [{ type: "image", url: item.url, mimeType: item.mimeType }] : []
994
+ { type: "text", text: JSON.stringify(result, null, 2) }
865
995
  ]
866
996
  };
867
997
  }
@@ -879,12 +1009,9 @@ function registerMediaTools(server, client) {
879
1009
  },
880
1010
  async () => {
881
1011
  const result = await client.listMedia();
882
- const media = Array.isArray(result) ? result : result?.media ?? [];
883
- const imageBlocks = media.filter((m) => m.type === "image" && m.url && m.mimeType && !m.mimeType.startsWith("video/")).map((m) => ({ type: "image", url: m.url, mimeType: m.mimeType }));
884
1012
  return {
885
1013
  content: [
886
- { type: "text", text: JSON.stringify(result, null, 2) },
887
- ...imageBlocks
1014
+ { type: "text", text: JSON.stringify(result, null, 2) }
888
1015
  ]
889
1016
  };
890
1017
  }
@@ -922,7 +1049,37 @@ This will permanently delete this media file. Call this tool again with confirme
922
1049
 
923
1050
  // src/tools/newsletter.ts
924
1051
  import { z as z5 } from "zod";
1052
+ import {
1053
+ registerAppResource,
1054
+ registerAppTool,
1055
+ RESOURCE_MIME_TYPE
1056
+ } from "@modelcontextprotocol/ext-apps/server";
1057
+ import * as fs from "fs";
1058
+ import * as path from "path";
1059
+ import { fileURLToPath } from "url";
1060
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
1061
+ var NEWSLETTER_STUDIO_RESOURCE = "ui://newsletter-studio/app.html";
925
1062
  function registerNewsletterTools(server, client, options = {}) {
1063
+ registerAppResource(
1064
+ server,
1065
+ NEWSLETTER_STUDIO_RESOURCE,
1066
+ NEWSLETTER_STUDIO_RESOURCE,
1067
+ { mimeType: RESOURCE_MIME_TYPE },
1068
+ async () => {
1069
+ const distDir = __dirname.endsWith("dist") ? __dirname : path.resolve(__dirname, "..", "dist");
1070
+ const htmlPath = path.join(distDir, "newsletter-studio.html");
1071
+ const html = fs.readFileSync(htmlPath, "utf-8");
1072
+ return {
1073
+ contents: [
1074
+ {
1075
+ uri: NEWSLETTER_STUDIO_RESOURCE,
1076
+ mimeType: RESOURCE_MIME_TYPE,
1077
+ text: html
1078
+ }
1079
+ ]
1080
+ };
1081
+ }
1082
+ );
926
1083
  server.tool(
927
1084
  "list_subscribers",
928
1085
  "List email subscribers from the connected ESP.",
@@ -973,32 +1130,33 @@ function registerNewsletterTools(server, client, options = {}) {
973
1130
  };
974
1131
  }
975
1132
  );
976
- server.tool(
1133
+ registerAppTool(
1134
+ server,
977
1135
  "create_newsletter",
978
- "Push a newsletter to the user's ESP as a draft. Show an HTML preview to the user before calling this.",
979
- {
980
- subject: z5.string().describe("Email subject line"),
981
- content: z5.string().describe("HTML content of the newsletter"),
982
- preview_text: z5.string().optional().describe("Preview text shown in email clients")
983
- },
984
1136
  {
985
1137
  title: "Create Newsletter Draft",
986
- readOnlyHint: false,
987
- destructiveHint: false,
988
- idempotentHint: false,
989
- openWorldHint: true
1138
+ 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.",
1139
+ inputSchema: {
1140
+ subject: z5.string().describe("Email subject line"),
1141
+ content: z5.string().describe("HTML content of the newsletter"),
1142
+ preview_text: z5.string().optional().describe("Preview text shown in email clients"),
1143
+ template_id: z5.string().optional().describe(
1144
+ "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."
1145
+ )
1146
+ },
1147
+ _meta: { ui: { resourceUri: NEWSLETTER_STUDIO_RESOURCE } }
990
1148
  },
991
1149
  async (args) => {
992
- const result = await client.createBroadcast({
1150
+ const payload = {
993
1151
  subject: args.subject,
994
1152
  content: args.content,
995
1153
  previewText: args.preview_text
996
- });
1154
+ };
1155
+ if (args.template_id) payload.templateId = args.template_id;
1156
+ const result = await client.createBroadcast(payload);
997
1157
  return {
998
1158
  content: [
999
- { type: "text", text: JSON.stringify(result, null, 2) },
1000
- ...args.content ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
1001
- ${args.content}` }] : []
1159
+ { type: "text", text: JSON.stringify(result, null, 2) }
1002
1160
  ]
1003
1161
  };
1004
1162
  }
@@ -1027,9 +1185,7 @@ ${args.content}` }] : []
1027
1185
  const result = await client.updateBroadcast(args.broadcast_id, data);
1028
1186
  return {
1029
1187
  content: [
1030
- { type: "text", text: JSON.stringify(result, null, 2) },
1031
- ...args.content ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
1032
- ${args.content}` }] : []
1188
+ { type: "text", text: JSON.stringify(result, null, 2) }
1033
1189
  ]
1034
1190
  };
1035
1191
  }
@@ -1037,7 +1193,7 @@ ${args.content}` }] : []
1037
1193
  if (options.allowDirectSend) {
1038
1194
  server.tool(
1039
1195
  "send_newsletter",
1040
- "Send a newsletter to subscribers. This action cannot be undone. Set confirmed=false to preview first.",
1196
+ "Send a newsletter to subscribers. This action cannot be undone. Always set confirmed=false first \u2014 never send without explicit user approval.",
1041
1197
  {
1042
1198
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to send"),
1043
1199
  confirmed: z5.boolean().default(false).describe(
@@ -1101,7 +1257,7 @@ Call this tool again with confirmed=true to send.`;
1101
1257
  }
1102
1258
  server.tool(
1103
1259
  "schedule_newsletter",
1104
- "Schedule a newsletter for future send. Set confirmed=false to preview first.",
1260
+ "Schedule a newsletter for future send. Always set confirmed=false first \u2014 never schedule without explicit user approval.",
1105
1261
  {
1106
1262
  broadcast_id: z5.string().describe("The broadcast/newsletter ID to schedule"),
1107
1263
  scheduled_for: z5.string().describe(
@@ -1411,24 +1567,6 @@ function registerNewsletterAdvancedTools(server, client) {
1411
1567
  };
1412
1568
  }
1413
1569
  );
1414
- server.tool(
1415
- "get_post_aggregate_stats",
1416
- "Get aggregate stats across all posts and newsletters.",
1417
- {},
1418
- {
1419
- title: "Get Post Aggregate Stats",
1420
- readOnlyHint: true,
1421
- destructiveHint: false,
1422
- idempotentHint: true,
1423
- openWorldHint: true
1424
- },
1425
- async () => {
1426
- const result = await client.getPostAggregateStats();
1427
- return {
1428
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1429
- };
1430
- }
1431
- );
1432
1570
  server.tool(
1433
1571
  "tag_subscriber",
1434
1572
  "Add tags to a subscriber.",
@@ -1534,10 +1672,10 @@ import { z as z7 } from "zod";
1534
1672
  function registerRssTools(server, client) {
1535
1673
  server.tool(
1536
1674
  "fetch_feed",
1537
- "Fetch and parse entries from an RSS or Atom feed URL.",
1675
+ "Fetch and parse all available entries from an RSS or Atom feed URL. Returns up to 100 entries by default.",
1538
1676
  {
1539
1677
  url: z7.string().describe("The RSS/Atom feed URL to fetch"),
1540
- limit: z7.number().optional().describe("Maximum number of entries to return (default 10, max 100)")
1678
+ limit: z7.number().optional().describe("Maximum number of entries to return (default 100)")
1541
1679
  },
1542
1680
  {
1543
1681
  title: "Fetch RSS Feed",
@@ -1608,7 +1746,7 @@ import { z as z8 } from "zod";
1608
1746
  function registerBrandVoiceTools(server, client) {
1609
1747
  server.tool(
1610
1748
  "get_brand_voice",
1611
- "Get the brand voice profile and writing rules. Call before creating content.",
1749
+ "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.",
1612
1750
  {},
1613
1751
  {
1614
1752
  title: "Get Brand Voice",
@@ -1653,6 +1791,18 @@ function registerBrandVoiceTools(server, client) {
1653
1791
  lines.push("(none)");
1654
1792
  }
1655
1793
  lines.push("");
1794
+ lines.push("### Platform-Specific Examples");
1795
+ if (voice.platformExamples && Object.keys(voice.platformExamples).length > 0) {
1796
+ for (const [platform, examples] of Object.entries(voice.platformExamples)) {
1797
+ lines.push(`**${platform}:**`);
1798
+ examples.forEach((ex, i) => {
1799
+ lines.push(` ${i + 1}. ${ex}`);
1800
+ });
1801
+ }
1802
+ } else {
1803
+ lines.push("(none)");
1804
+ }
1805
+ lines.push("");
1656
1806
  lines.push("### Example Posts");
1657
1807
  if (voice.examplePosts && voice.examplePosts.length > 0) {
1658
1808
  voice.examplePosts.forEach((post, i) => {
@@ -1662,6 +1812,9 @@ function registerBrandVoiceTools(server, client) {
1662
1812
  lines.push("(none)");
1663
1813
  }
1664
1814
  lines.push("");
1815
+ if (voice.updatedAt) {
1816
+ lines.push(`_Last updated: ${voice.updatedAt}_`);
1817
+ }
1665
1818
  return {
1666
1819
  content: [{ type: "text", text: lines.join("\n") }]
1667
1820
  };
@@ -1690,7 +1843,8 @@ function registerBrandVoiceTools(server, client) {
1690
1843
  dos: z8.array(z8.string().max(200)).max(15).optional().describe("Writing rules to follow. Send the FULL list, not just additions."),
1691
1844
  donts: z8.array(z8.string().max(200)).max(15).optional().describe("Things to avoid. Send the FULL list, not just additions."),
1692
1845
  platform_rules: z8.record(z8.string().max(300)).optional().describe('Per-platform writing guidelines, e.g. { "twitter": "Keep under 200 chars" }'),
1693
- example_posts: z8.array(z8.string().max(500)).max(5).optional().describe("Example posts that demonstrate the desired voice"),
1846
+ platform_examples: z8.record(z8.array(z8.string().max(500)).max(3)).optional().describe('Per-platform example posts, e.g. { "twitter": ["Example tweet 1", "Example tweet 2"], "linkedin": ["Example post"] }'),
1847
+ example_posts: z8.array(z8.string().max(500)).max(5).optional().describe("General example posts that demonstrate the desired voice"),
1694
1848
  confirmed: z8.boolean().default(false).describe(
1695
1849
  "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
1696
1850
  )
@@ -1721,13 +1875,26 @@ function registerBrandVoiceTools(server, client) {
1721
1875
  }
1722
1876
  payload.platformRules = args.platform_rules;
1723
1877
  }
1878
+ if (args.platform_examples !== void 0) {
1879
+ const entries = Object.entries(args.platform_examples);
1880
+ if (entries.length > 6) {
1881
+ return {
1882
+ content: [{
1883
+ type: "text",
1884
+ text: "Maximum 6 platforms for examples allowed."
1885
+ }],
1886
+ isError: true
1887
+ };
1888
+ }
1889
+ payload.platformExamples = args.platform_examples;
1890
+ }
1724
1891
  if (args.example_posts !== void 0) payload.examplePosts = args.example_posts;
1725
1892
  if (Object.keys(payload).length === 0) {
1726
1893
  return {
1727
1894
  content: [
1728
1895
  {
1729
1896
  type: "text",
1730
- text: "No fields provided. Specify at least one field to update (name, description, dos, donts, platform_rules, example_posts)."
1897
+ text: "No fields provided. Specify at least one field to update (name, description, dos, donts, platform_rules, platform_examples, example_posts)."
1731
1898
  }
1732
1899
  ],
1733
1900
  isError: true
@@ -1770,6 +1937,17 @@ function registerBrandVoiceTools(server, client) {
1770
1937
  }
1771
1938
  lines2.push("");
1772
1939
  }
1940
+ if (payload.platformExamples) {
1941
+ const examples = payload.platformExamples;
1942
+ lines2.push(`### Platform-Specific Examples (${Object.keys(examples).length} platforms)`);
1943
+ for (const [platform, exList] of Object.entries(examples)) {
1944
+ lines2.push(`**${platform}:**`);
1945
+ exList.forEach((ex, i) => {
1946
+ lines2.push(` ${i + 1}. ${ex}`);
1947
+ });
1948
+ }
1949
+ lines2.push("");
1950
+ }
1773
1951
  if (payload.examplePosts) {
1774
1952
  const posts = payload.examplePosts;
1775
1953
  lines2.push(`### Example Posts (${posts.length})`);
@@ -1794,6 +1972,7 @@ function registerBrandVoiceTools(server, client) {
1794
1972
  if (payload.dos) summary.push(`${payload.dos.length} do's`);
1795
1973
  if (payload.donts) summary.push(`${payload.donts.length} don'ts`);
1796
1974
  if (payload.platformRules) summary.push(`${Object.keys(payload.platformRules).length} platform rules`);
1975
+ if (payload.platformExamples) summary.push(`${Object.keys(payload.platformExamples).length} platform example sets`);
1797
1976
  if (payload.examplePosts) summary.push(`${payload.examplePosts.length} example posts`);
1798
1977
  lines.push(`**Updated:** ${summary.join(", ")}`);
1799
1978
  return {
@@ -1803,15 +1982,70 @@ function registerBrandVoiceTools(server, client) {
1803
1982
  );
1804
1983
  }
1805
1984
 
1806
- // src/tools/knowledge.ts
1985
+ // src/tools/audience.ts
1807
1986
  import { z as z9 } from "zod";
1808
- function registerKnowledgeTools(server, client) {
1987
+ function formatAudience(audience) {
1988
+ const lines = [];
1989
+ lines.push(`## ${audience.name}${audience.isDefault ? " (default)" : ""}`);
1990
+ lines.push("");
1991
+ if (audience.description) {
1992
+ lines.push("### Description");
1993
+ lines.push(audience.description);
1994
+ lines.push("");
1995
+ }
1996
+ if (audience.demographics) {
1997
+ lines.push("### Demographics");
1998
+ lines.push(audience.demographics);
1999
+ lines.push("");
2000
+ }
2001
+ if (audience.painPoints && audience.painPoints.length > 0) {
2002
+ lines.push("### Pain Points");
2003
+ for (const point of audience.painPoints) {
2004
+ lines.push(`- ${point}`);
2005
+ }
2006
+ lines.push("");
2007
+ }
2008
+ if (audience.motivations && audience.motivations.length > 0) {
2009
+ lines.push("### Motivations");
2010
+ for (const motivation of audience.motivations) {
2011
+ lines.push(`- ${motivation}`);
2012
+ }
2013
+ lines.push("");
2014
+ }
2015
+ if (audience.platforms && audience.platforms.length > 0) {
2016
+ lines.push("### Active Platforms");
2017
+ lines.push(audience.platforms.join(", "));
2018
+ lines.push("");
2019
+ }
2020
+ if (audience.toneNotes) {
2021
+ lines.push("### Tone Notes");
2022
+ lines.push(audience.toneNotes);
2023
+ lines.push("");
2024
+ }
2025
+ if (audience.contentPreferences) {
2026
+ lines.push("### Content Preferences");
2027
+ lines.push(audience.contentPreferences);
2028
+ lines.push("");
2029
+ }
2030
+ return lines.join("\n");
2031
+ }
2032
+ var audienceFields = {
2033
+ name: z9.string().max(100).describe("Audience name (must be unique per account)"),
2034
+ description: z9.string().max(500).optional().describe("Brief description of who this audience is"),
2035
+ demographics: z9.string().max(500).optional().describe("Demographic details"),
2036
+ pain_points: z9.array(z9.string().max(200)).max(10).optional().describe("Problems the audience faces. Send the FULL list."),
2037
+ motivations: z9.array(z9.string().max(200)).max(10).optional().describe("Goals and desires. Send the FULL list."),
2038
+ preferred_platforms: z9.array(z9.string()).max(6).optional().describe("Social platforms the audience is most active on. Send the FULL list."),
2039
+ tone_notes: z9.string().max(500).optional().describe("How to speak to this audience"),
2040
+ content_preferences: z9.string().max(500).optional().describe("What content formats and topics resonate")
2041
+ };
2042
+ function registerAudienceTools(server, client) {
1809
2043
  server.tool(
1810
- "get_knowledge_base",
1811
- "Get all items from the knowledge base.",
2044
+ "list_audiences",
2045
+ "List all audience profiles. Call this to see which audiences are available before creating content.",
1812
2046
  {},
1813
2047
  {
1814
- title: "Get Knowledge Base",
2048
+ title: "List Audiences",
1815
2049
  readOnlyHint: true,
1816
2050
  destructiveHint: false,
1817
2051
  idempotentHint: true,
@@ -1819,230 +2053,171 @@ function registerKnowledgeTools(server, client) {
1819
2053
  },
1820
2054
  async () => {
1821
2055
  try {
1822
- const data = await client.listKnowledge();
1823
- const items = data.items ?? [];
1824
- if (items.length === 0) {
2056
+ const audiences = await client.listAudiences();
2057
+ if (!audiences || audiences.length === 0) {
1825
2058
  return {
1826
2059
  content: [
1827
2060
  {
1828
2061
  type: "text",
1829
- text: "The knowledge base is empty. The customer can add reference material at their BuzzPoster dashboard."
2062
+ text: "No audience profiles configured. Use create_audience to add one."
1830
2063
  }
1831
2064
  ]
1832
2065
  };
1833
2066
  }
1834
- const totalChars = items.reduce(
1835
- (sum, item) => sum + item.content.length,
1836
- 0
1837
- );
1838
- const shouldTruncate = totalChars > 1e4;
1839
2067
  const lines = [];
1840
- lines.push(`## Knowledge Base (${items.length} items)`);
2068
+ lines.push(`## Audience Profiles (${audiences.length})`);
1841
2069
  lines.push("");
1842
- for (const item of items) {
1843
- lines.push(`### ${item.title}`);
1844
- if (item.tags && item.tags.length > 0) {
1845
- lines.push(`Tags: ${item.tags.join(", ")}`);
1846
- }
1847
- if (shouldTruncate) {
1848
- lines.push(item.content.slice(0, 500) + " [truncated]");
1849
- } else {
1850
- lines.push(item.content);
1851
- }
1852
- lines.push("");
2070
+ for (const a of audiences) {
2071
+ lines.push(`- **${a.name}**${a.isDefault ? " (default)" : ""}: ${a.description || "(no description)"}`);
1853
2072
  }
2073
+ lines.push("");
2074
+ lines.push("Use get_audience with a name to see the full profile.");
1854
2075
  return {
1855
2076
  content: [{ type: "text", text: lines.join("\n") }]
1856
2077
  };
1857
2078
  } catch (error) {
1858
2079
  const message = error instanceof Error ? error.message : "Unknown error";
1859
- throw new Error(`Failed to get knowledge base: ${message}`);
2080
+ throw new Error(`Failed to list audiences: ${message}`);
1860
2081
  }
1861
2082
  }
1862
2083
  );
1863
2084
  server.tool(
1864
- "search_knowledge",
1865
- "Search the knowledge base by tag or keyword.",
2085
+ "get_audience",
2086
+ "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.",
1866
2087
  {
1867
- query: z9.string().describe(
1868
- "Search query - matches against tags first, then falls back to text search on title and content"
1869
- )
2088
+ name: z9.string().optional().describe("Audience name to look up. If omitted, returns the default audience.")
1870
2089
  },
1871
2090
  {
1872
- title: "Search Knowledge Base",
2091
+ title: "Get Audience Profile",
1873
2092
  readOnlyHint: true,
1874
2093
  destructiveHint: false,
1875
2094
  idempotentHint: true,
1876
2095
  openWorldHint: false
1877
2096
  },
1878
- async ({ query }) => {
2097
+ async ({ name }) => {
1879
2098
  try {
1880
- const data = await client.listKnowledge({ tag: query });
1881
- const items = data.items ?? [];
1882
- if (items.length === 0) {
2099
+ let audience;
2100
+ if (name) {
2101
+ audience = await client.getAudienceByName(name);
2102
+ } else {
2103
+ audience = await client.getDefaultAudience();
2104
+ }
2105
+ return {
2106
+ content: [{ type: "text", text: formatAudience(audience) }]
2107
+ };
2108
+ } catch (error) {
2109
+ const message = error instanceof Error ? error.message : "Unknown error";
2110
+ if (message.includes("404") || message.includes("No audience") || message.includes("not found")) {
1883
2111
  return {
1884
2112
  content: [
1885
2113
  {
1886
2114
  type: "text",
1887
- text: `No knowledge base items found matching "${query}".`
2115
+ text: name ? `No audience named "${name}" found. Use list_audiences to see available profiles.` : "No audience profiles configured. Use create_audience to add one."
1888
2116
  }
1889
2117
  ]
1890
2118
  };
1891
2119
  }
1892
- const lines = [];
1893
- lines.push(
1894
- `## Knowledge Base Results for "${query}" (${items.length} items)`
1895
- );
1896
- lines.push("");
1897
- for (const item of items) {
1898
- lines.push(`### ${item.title}`);
1899
- if (item.tags && item.tags.length > 0) {
1900
- lines.push(`Tags: ${item.tags.join(", ")}`);
1901
- }
1902
- lines.push(item.content);
1903
- lines.push("");
1904
- }
1905
- return {
1906
- content: [{ type: "text", text: lines.join("\n") }]
1907
- };
1908
- } catch (error) {
1909
- const message = error instanceof Error ? error.message : "Unknown error";
1910
- throw new Error(`Failed to search knowledge base: ${message}`);
2120
+ throw error;
1911
2121
  }
1912
2122
  }
1913
2123
  );
1914
2124
  server.tool(
1915
- "add_knowledge",
1916
- "Add reference material to the knowledge base. Include tags for categorization.",
2125
+ "create_audience",
2126
+ "Create a new audience profile. Set confirmed=false to preview first.",
1917
2127
  {
1918
- title: z9.string().describe("Title for the knowledge item"),
1919
- content: z9.string().describe("The content/text to save"),
1920
- tags: z9.array(z9.string()).optional().describe("Optional tags for categorization")
2128
+ ...audienceFields,
2129
+ is_default: z9.boolean().optional().describe("Set as the default audience profile"),
2130
+ confirmed: z9.boolean().default(false).describe("Set to true to confirm and save. If false, returns a preview.")
1921
2131
  },
1922
2132
  {
1923
- title: "Add Knowledge Item",
2133
+ title: "Create Audience",
1924
2134
  readOnlyHint: false,
1925
2135
  destructiveHint: false,
1926
2136
  idempotentHint: false,
1927
2137
  openWorldHint: false
1928
2138
  },
1929
- async ({ title, content, tags }) => {
1930
- try {
1931
- await client.createKnowledge({
1932
- title,
1933
- content,
1934
- sourceType: "text",
1935
- tags: tags ?? []
1936
- });
1937
- return {
1938
- content: [
1939
- {
1940
- type: "text",
1941
- text: `Added "${title}" to the knowledge base.`
1942
- }
1943
- ]
1944
- };
1945
- } catch (error) {
1946
- const message = error instanceof Error ? error.message : "Unknown error";
1947
- throw new Error(`Failed to add knowledge item: ${message}`);
1948
- }
1949
- }
1950
- );
1951
- }
1952
-
1953
- // src/tools/audience.ts
1954
- import { z as z10 } from "zod";
1955
- function registerAudienceTools(server, client) {
1956
- server.tool(
1957
- "get_audience",
1958
- "Get the target audience profile. Call before creating content.",
1959
- {},
1960
- {
1961
- title: "Get Audience Profile",
1962
- readOnlyHint: true,
1963
- destructiveHint: false,
1964
- idempotentHint: true,
1965
- openWorldHint: false
1966
- },
1967
- async () => {
1968
- try {
1969
- const audience = await client.getDefaultAudience();
2139
+ async (args) => {
2140
+ const payload = { name: args.name };
2141
+ if (args.description !== void 0) payload.description = args.description;
2142
+ if (args.demographics !== void 0) payload.demographics = args.demographics;
2143
+ if (args.pain_points !== void 0) payload.painPoints = args.pain_points;
2144
+ if (args.motivations !== void 0) payload.motivations = args.motivations;
2145
+ if (args.preferred_platforms !== void 0) payload.platforms = args.preferred_platforms;
2146
+ if (args.tone_notes !== void 0) payload.toneNotes = args.tone_notes;
2147
+ if (args.content_preferences !== void 0) payload.contentPreferences = args.content_preferences;
2148
+ if (args.is_default !== void 0) payload.isDefault = args.is_default;
2149
+ if (args.confirmed !== true) {
1970
2150
  const lines = [];
1971
- lines.push(`## Target Audience: ${audience.name}`);
2151
+ lines.push("## New Audience Preview");
1972
2152
  lines.push("");
1973
- if (audience.description) {
2153
+ lines.push(`**Name:** ${args.name}`);
2154
+ if (args.is_default) lines.push("**Default:** yes");
2155
+ lines.push("");
2156
+ if (payload.description) {
1974
2157
  lines.push("### Description");
1975
- lines.push(audience.description);
2158
+ lines.push(String(payload.description));
1976
2159
  lines.push("");
1977
2160
  }
1978
- if (audience.demographics) {
2161
+ if (payload.demographics) {
1979
2162
  lines.push("### Demographics");
1980
- lines.push(audience.demographics);
2163
+ lines.push(String(payload.demographics));
1981
2164
  lines.push("");
1982
2165
  }
1983
- if (audience.painPoints && audience.painPoints.length > 0) {
1984
- lines.push("### Pain Points");
1985
- for (const point of audience.painPoints) {
1986
- lines.push(`- ${point}`);
1987
- }
2166
+ if (payload.painPoints) {
2167
+ lines.push(`### Pain Points (${payload.painPoints.length})`);
2168
+ for (const p of payload.painPoints) lines.push(`- ${p}`);
1988
2169
  lines.push("");
1989
2170
  }
1990
- if (audience.motivations && audience.motivations.length > 0) {
1991
- lines.push("### Motivations");
1992
- for (const motivation of audience.motivations) {
1993
- lines.push(`- ${motivation}`);
1994
- }
2171
+ if (payload.motivations) {
2172
+ lines.push(`### Motivations (${payload.motivations.length})`);
2173
+ for (const m of payload.motivations) lines.push(`- ${m}`);
1995
2174
  lines.push("");
1996
2175
  }
1997
- if (audience.platforms && audience.platforms.length > 0) {
1998
- lines.push("### Active Platforms");
1999
- lines.push(audience.platforms.join(", "));
2176
+ if (payload.platforms) {
2177
+ lines.push(`### Platforms`);
2178
+ lines.push(payload.platforms.join(", "));
2000
2179
  lines.push("");
2001
2180
  }
2002
- if (audience.toneNotes) {
2181
+ if (payload.toneNotes) {
2003
2182
  lines.push("### Tone Notes");
2004
- lines.push(audience.toneNotes);
2183
+ lines.push(String(payload.toneNotes));
2005
2184
  lines.push("");
2006
2185
  }
2007
- if (audience.contentPreferences) {
2186
+ if (payload.contentPreferences) {
2008
2187
  lines.push("### Content Preferences");
2009
- lines.push(audience.contentPreferences);
2188
+ lines.push(String(payload.contentPreferences));
2010
2189
  lines.push("");
2011
2190
  }
2012
- return {
2013
- content: [{ type: "text", text: lines.join("\n") }]
2014
- };
2015
- } catch (error) {
2016
- const message = error instanceof Error ? error.message : "Unknown error";
2017
- if (message.includes("404") || message.includes("No audience")) {
2018
- return {
2019
- content: [
2020
- {
2021
- type: "text",
2022
- text: "No audience profile has been configured yet. The customer can set one up at their BuzzPoster dashboard under Audiences."
2023
- }
2024
- ]
2025
- };
2026
- }
2027
- throw error;
2191
+ lines.push("---");
2192
+ lines.push("Call this tool again with **confirmed=true** to save.");
2193
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2028
2194
  }
2195
+ const result = await client.createAudience(payload);
2196
+ return {
2197
+ content: [
2198
+ {
2199
+ type: "text",
2200
+ text: `Audience "${result.name}" created successfully (ID: ${result.id}).`
2201
+ }
2202
+ ]
2203
+ };
2029
2204
  }
2030
2205
  );
2031
2206
  server.tool(
2032
2207
  "update_audience",
2033
- "Update the audience profile. Set confirmed=false to preview first.",
2034
- {
2035
- name: z10.string().max(100).optional().describe("Audience profile name. Required when creating a new audience."),
2036
- description: z10.string().max(500).optional().describe("Brief description of who this audience is"),
2037
- demographics: z10.string().max(500).optional().describe("Demographic details"),
2038
- pain_points: z10.array(z10.string().max(200)).max(10).optional().describe("Problems the audience faces. Send the FULL list."),
2039
- motivations: z10.array(z10.string().max(200)).max(10).optional().describe("Goals and desires. Send the FULL list."),
2040
- preferred_platforms: z10.array(z10.string()).max(6).optional().describe("Social platforms the audience is most active on. Send the FULL list."),
2041
- tone_notes: z10.string().max(500).optional().describe("How to speak to this audience"),
2042
- content_preferences: z10.string().max(500).optional().describe("What content formats and topics resonate"),
2043
- confirmed: z10.boolean().default(false).describe(
2044
- "Set to true to confirm and save. If false or missing, returns a preview of what will be saved."
2045
- )
2208
+ "Update an existing audience profile by name. Set confirmed=false to preview first.",
2209
+ {
2210
+ audience_name: z9.string().describe("Name of the audience to update"),
2211
+ name: z9.string().max(100).optional().describe("New name for the audience"),
2212
+ description: z9.string().max(500).optional().describe("Brief description of who this audience is"),
2213
+ demographics: z9.string().max(500).optional().describe("Demographic details"),
2214
+ pain_points: z9.array(z9.string().max(200)).max(10).optional().describe("Problems the audience faces. Send the FULL list."),
2215
+ motivations: z9.array(z9.string().max(200)).max(10).optional().describe("Goals and desires. Send the FULL list."),
2216
+ preferred_platforms: z9.array(z9.string()).max(6).optional().describe("Social platforms the audience is most active on. Send the FULL list."),
2217
+ tone_notes: z9.string().max(500).optional().describe("How to speak to this audience"),
2218
+ content_preferences: z9.string().max(500).optional().describe("What content formats and topics resonate"),
2219
+ is_default: z9.boolean().optional().describe("Set as the default audience profile"),
2220
+ confirmed: z9.boolean().default(false).describe("Set to true to confirm and save. If false, returns a preview.")
2046
2221
  },
2047
2222
  {
2048
2223
  title: "Update Audience",
@@ -2052,6 +2227,20 @@ function registerAudienceTools(server, client) {
2052
2227
  openWorldHint: false
2053
2228
  },
2054
2229
  async (args) => {
2230
+ let target;
2231
+ try {
2232
+ target = await client.getAudienceByName(args.audience_name);
2233
+ } catch {
2234
+ return {
2235
+ content: [
2236
+ {
2237
+ type: "text",
2238
+ text: `Audience "${args.audience_name}" not found. Use list_audiences to see available profiles.`
2239
+ }
2240
+ ],
2241
+ isError: true
2242
+ };
2243
+ }
2055
2244
  const payload = {};
2056
2245
  if (args.name !== void 0) payload.name = args.name;
2057
2246
  if (args.description !== void 0) payload.description = args.description;
@@ -2061,661 +2250,187 @@ function registerAudienceTools(server, client) {
2061
2250
  if (args.preferred_platforms !== void 0) payload.platforms = args.preferred_platforms;
2062
2251
  if (args.tone_notes !== void 0) payload.toneNotes = args.tone_notes;
2063
2252
  if (args.content_preferences !== void 0) payload.contentPreferences = args.content_preferences;
2253
+ if (args.is_default !== void 0) payload.isDefault = args.is_default;
2064
2254
  if (Object.keys(payload).length === 0) {
2065
2255
  return {
2066
- content: [
2067
- {
2068
- type: "text",
2069
- text: "No fields provided. Specify at least one field to update."
2070
- }
2071
- ],
2072
- isError: true
2073
- };
2074
- }
2075
- let targetId;
2076
- let isCreating = false;
2077
- try {
2078
- const defaultAudience = await client.getDefaultAudience();
2079
- targetId = String(defaultAudience.id);
2080
- } catch {
2081
- isCreating = true;
2082
- }
2083
- if (isCreating && !payload.name) {
2084
- return {
2085
- content: [
2086
- {
2087
- type: "text",
2088
- text: "No audience exists yet. Provide a **name** to create one (e.g. name: 'SaaS Founders')."
2089
- }
2090
- ],
2256
+ content: [{ type: "text", text: "No fields provided to update." }],
2091
2257
  isError: true
2092
2258
  };
2093
2259
  }
2094
2260
  if (args.confirmed !== true) {
2095
- const lines2 = [];
2096
- lines2.push(isCreating ? "## New Audience Preview" : "## Audience Update Preview");
2097
- lines2.push("");
2261
+ const lines = [];
2262
+ lines.push(`## Update Audience "${args.audience_name}" Preview`);
2263
+ lines.push("");
2098
2264
  if (payload.name) {
2099
- lines2.push(`**Name:** ${payload.name}`);
2100
- lines2.push("");
2265
+ lines.push(`**New Name:** ${payload.name}`);
2266
+ lines.push("");
2101
2267
  }
2102
2268
  if (payload.description) {
2103
- lines2.push("### Description");
2104
- lines2.push(String(payload.description));
2105
- lines2.push("");
2269
+ lines.push("### Description");
2270
+ lines.push(String(payload.description));
2271
+ lines.push("");
2106
2272
  }
2107
2273
  if (payload.demographics) {
2108
- lines2.push("### Demographics");
2109
- lines2.push(String(payload.demographics));
2110
- lines2.push("");
2111
- }
2112
- if (payload.painPoints) {
2113
- const points = payload.painPoints;
2114
- lines2.push(`### Pain Points (${points.length})`);
2115
- for (const point of points) {
2116
- lines2.push(`- ${point}`);
2117
- }
2118
- lines2.push("");
2119
- }
2120
- if (payload.motivations) {
2121
- const motivations = payload.motivations;
2122
- lines2.push(`### Motivations (${motivations.length})`);
2123
- for (const motivation of motivations) {
2124
- lines2.push(`- ${motivation}`);
2125
- }
2126
- lines2.push("");
2127
- }
2128
- if (payload.platforms) {
2129
- const platforms = payload.platforms;
2130
- lines2.push(`### Preferred Platforms (${platforms.length})`);
2131
- lines2.push(platforms.join(", "));
2132
- lines2.push("");
2133
- }
2134
- if (payload.toneNotes) {
2135
- lines2.push("### Tone Notes");
2136
- lines2.push(String(payload.toneNotes));
2137
- lines2.push("");
2138
- }
2139
- if (payload.contentPreferences) {
2140
- lines2.push("### Content Preferences");
2141
- lines2.push(String(payload.contentPreferences));
2142
- lines2.push("");
2143
- }
2144
- lines2.push("---");
2145
- lines2.push("Call this tool again with **confirmed=true** to save these changes.");
2146
- return {
2147
- content: [{ type: "text", text: lines2.join("\n") }]
2148
- };
2149
- }
2150
- let result;
2151
- if (isCreating) {
2152
- if (payload.isDefault === void 0) payload.isDefault = true;
2153
- result = await client.createAudience(payload);
2154
- } else {
2155
- result = await client.updateAudience(targetId, payload);
2156
- }
2157
- const lines = [];
2158
- lines.push(`Audience "${result.name}" has been ${isCreating ? "created" : "updated"} successfully.`);
2159
- lines.push("");
2160
- const summary = [];
2161
- if (payload.name) summary.push("name");
2162
- if (payload.description) summary.push("description");
2163
- if (payload.demographics) summary.push("demographics");
2164
- if (payload.painPoints) summary.push(`${payload.painPoints.length} pain points`);
2165
- if (payload.motivations) summary.push(`${payload.motivations.length} motivations`);
2166
- if (payload.platforms) summary.push(`${payload.platforms.length} platforms`);
2167
- if (payload.toneNotes) summary.push("tone notes");
2168
- if (payload.contentPreferences) summary.push("content preferences");
2169
- lines.push(`**${isCreating ? "Created with" : "Updated"}:** ${summary.join(", ")}`);
2170
- return {
2171
- content: [{ type: "text", text: lines.join("\n") }]
2172
- };
2173
- }
2174
- );
2175
- }
2176
-
2177
- // src/tools/newsletter-template.ts
2178
- import { z as z11 } from "zod";
2179
- var NEWSLETTER_CRAFT_GUIDE = `## Newsletter Craft Guide
2180
-
2181
- Follow these rules when drafting:
2182
-
2183
- ### HTML Email Rules
2184
- - Use table-based layouts, inline CSS only, 600px max width
2185
- - Email-safe fonts only: Arial, Helvetica, Georgia, Verdana
2186
- - Keep total email under 102KB (Gmail clips larger)
2187
- - All images must use absolute URLs hosted on the customer's R2 CDN
2188
- - Include alt text on every image
2189
- - Use role="presentation" on layout tables
2190
- - No JavaScript, no forms, no CSS grid/flexbox, no background images
2191
-
2192
- ### Structure
2193
- - Subject line: 6-10 words, specific not clickbaity
2194
- - Preview text: complements the subject, doesn't repeat it
2195
- - Open strong -- lead with the most interesting thing
2196
- - One clear CTA per newsletter
2197
- - Sign off should feel human, not corporate
2198
-
2199
- ### Personalization
2200
- - Kit: use {{ subscriber.first_name }}
2201
- - Beehiiv: use {{email}} or {{subscriber_id}}
2202
- - Mailchimp: use *NAME|*
2203
-
2204
- ### Images
2205
- - Upload to customer's R2 CDN via upload_media or upload_from_url
2206
- - Reference with full CDN URLs in img tags
2207
- - Always set width, height, and alt attributes
2208
- - Use style="display:block;" to prevent phantom spacing`;
2209
- function registerNewsletterTemplateTools(server, client) {
2210
- server.tool(
2211
- "get_newsletter_template",
2212
- "Get a newsletter template's structure, sections, and HTML. Pass templateId or omit for default.",
2213
- {
2214
- templateId: z11.string().optional().describe(
2215
- "Specific template ID. If omitted, returns the default template."
2216
- )
2217
- },
2218
- {
2219
- title: "Get Newsletter Template",
2220
- readOnlyHint: true,
2221
- destructiveHint: false,
2222
- idempotentHint: true,
2223
- openWorldHint: false
2224
- },
2225
- async ({ templateId }) => {
2226
- try {
2227
- const template = await client.getTemplate(templateId);
2228
- const lines = [];
2229
- lines.push(`## Newsletter Template: ${template.name}`);
2230
- if (template.description) lines.push(template.description);
2231
- lines.push("");
2232
- if (template.audienceId)
2233
- lines.push(`Audience ID: ${template.audienceId}`);
2234
- if (template.sendCadence)
2235
- lines.push(`Cadence: ${template.sendCadence}`);
2236
- if (template.subjectPattern)
2237
- lines.push(`Subject pattern: ${template.subjectPattern}`);
2238
- if (template.previewTextPattern)
2239
- lines.push(`Preview text pattern: ${template.previewTextPattern}`);
2240
- lines.push("");
2241
- if (template.sections && template.sections.length > 0) {
2242
- lines.push("### Sections (in order):");
2274
+ lines.push("### Demographics");
2275
+ lines.push(String(payload.demographics));
2243
2276
  lines.push("");
2244
- for (let i = 0; i < template.sections.length; i++) {
2245
- const s = template.sections[i];
2246
- lines.push(`**${i + 1}. ${s.label}** (${s.type})`);
2247
- if (s.instructions)
2248
- lines.push(`Instructions: ${s.instructions}`);
2249
- if (s.word_count)
2250
- lines.push(
2251
- `Word count: ${s.word_count.min}-${s.word_count.max} words`
2252
- );
2253
- if (s.tone_override) lines.push(`Tone: ${s.tone_override}`);
2254
- if (s.content_source) {
2255
- const src = s.content_source;
2256
- if (src.type === "rss" && src.feed_urls?.length)
2257
- lines.push(`Content source: RSS - ${src.feed_urls.join(", ")}`);
2258
- else if (src.type === "knowledge" && src.knowledge_item_ids?.length)
2259
- lines.push(
2260
- `Content source: Knowledge items ${src.knowledge_item_ids.join(", ")}`
2261
- );
2262
- else lines.push(`Content source: ${src.type}`);
2263
- }
2264
- if (s.count) lines.push(`Count: ${s.count} items`);
2265
- lines.push(
2266
- `Required: ${s.required !== false ? "yes" : "no"}`
2267
- );
2268
- if (s.type === "cta") {
2269
- if (s.button_text) lines.push(`Button text: ${s.button_text}`);
2270
- if (s.button_url) lines.push(`Button URL: ${s.button_url}`);
2271
- }
2272
- if (s.type === "sponsor") {
2273
- if (s.sponsor_name)
2274
- lines.push(`Sponsor: ${s.sponsor_name}`);
2275
- if (s.talking_points)
2276
- lines.push(`Talking points: ${s.talking_points}`);
2277
- }
2278
- if (s.type === "header") {
2279
- if (s.tagline) lines.push(`Tagline: ${s.tagline}`);
2280
- }
2281
- if (s.type === "signoff") {
2282
- if (s.author_name)
2283
- lines.push(`Author: ${s.author_name}`);
2284
- if (s.author_title)
2285
- lines.push(`Title: ${s.author_title}`);
2286
- }
2287
- lines.push("");
2288
- }
2289
2277
  }
2290
- if (template.style) {
2291
- lines.push("### Style Guide:");
2292
- const st = template.style;
2293
- if (st.primary_color) lines.push(`Primary color: ${st.primary_color}`);
2294
- if (st.accent_color) lines.push(`Accent color: ${st.accent_color}`);
2295
- if (st.font_family) lines.push(`Font: ${st.font_family}`);
2296
- if (st.heading_font) lines.push(`Heading font: ${st.heading_font}`);
2297
- if (st.content_width)
2298
- lines.push(`Content width: ${st.content_width}px`);
2299
- if (st.text_color) lines.push(`Text color: ${st.text_color}`);
2300
- if (st.link_color) lines.push(`Link color: ${st.link_color}`);
2301
- if (st.background_color)
2302
- lines.push(`Background: ${st.background_color}`);
2303
- if (st.button_style)
2304
- lines.push(
2305
- `Button style: ${st.button_style.color} text on ${st.button_style.text_color}, ${st.button_style.shape}`
2306
- );
2307
- lines.push("");
2308
- lines.push(
2309
- "NOTE: Apply these style values as inline CSS when generating the newsletter HTML."
2310
- );
2278
+ if (payload.painPoints) {
2279
+ lines.push(`### Pain Points (${payload.painPoints.length})`);
2280
+ for (const p of payload.painPoints) lines.push(`- ${p}`);
2311
2281
  lines.push("");
2312
2282
  }
2313
- if (template.htmlContent) {
2314
- lines.push("### HTML Template:");
2315
- lines.push("The following HTML skeleton contains {{placeholder}} variables. Fill in the placeholders with real content when generating the newsletter.");
2316
- lines.push("");
2317
- lines.push("```html");
2318
- lines.push(template.htmlContent);
2319
- lines.push("```");
2283
+ if (payload.motivations) {
2284
+ lines.push(`### Motivations (${payload.motivations.length})`);
2285
+ for (const m of payload.motivations) lines.push(`- ${m}`);
2320
2286
  lines.push("");
2321
2287
  }
2322
- if (template.rssFeedUrls && template.rssFeedUrls.length > 0) {
2323
- lines.push("### Linked RSS Feeds:");
2324
- for (const url of template.rssFeedUrls) {
2325
- lines.push(`- ${url}`);
2326
- }
2288
+ if (payload.platforms) {
2289
+ lines.push(`### Platforms`);
2290
+ lines.push(payload.platforms.join(", "));
2327
2291
  lines.push("");
2328
2292
  }
2329
- if (template.knowledgeItemIds && template.knowledgeItemIds.length > 0) {
2330
- lines.push("### Reference Material:");
2331
- lines.push(
2332
- `Knowledge item IDs: ${template.knowledgeItemIds.join(", ")}`
2333
- );
2293
+ if (payload.toneNotes) {
2294
+ lines.push("### Tone Notes");
2295
+ lines.push(String(payload.toneNotes));
2334
2296
  lines.push("");
2335
2297
  }
2336
- lines.push(NEWSLETTER_CRAFT_GUIDE);
2337
- return {
2338
- content: [{ type: "text", text: lines.join("\n") }]
2339
- };
2340
- } catch (error) {
2341
- const message = error instanceof Error ? error.message : "Unknown error";
2342
- if (message.includes("404") || message.includes("No newsletter templates")) {
2343
- return {
2344
- content: [
2345
- {
2346
- type: "text",
2347
- 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
2348
- }
2349
- ]
2350
- };
2351
- }
2352
- throw error;
2353
- }
2354
- }
2355
- );
2356
- server.tool(
2357
- "get_past_newsletters",
2358
- "Get past newsletters from the archive. Pass newsletterId to get full content of a specific edition.",
2359
- {
2360
- newsletterId: z11.string().optional().describe("If provided, returns the full content of this specific newsletter. If omitted, returns the list."),
2361
- limit: z11.number().optional().describe("Number of past newsletters to retrieve (when listing). Default 5, max 50."),
2362
- templateId: z11.string().optional().describe("Filter by template ID to see past newsletters from a specific template.")
2363
- },
2364
- {
2365
- title: "Get Past Newsletters",
2366
- readOnlyHint: true,
2367
- destructiveHint: false,
2368
- idempotentHint: true,
2369
- openWorldHint: false
2370
- },
2371
- async ({ newsletterId, limit, templateId }) => {
2372
- if (newsletterId) {
2373
- try {
2374
- const newsletter = await client.getArchivedNewsletter(
2375
- newsletterId
2376
- );
2377
- const lines = [];
2378
- lines.push(`## ${newsletter.subject}`);
2379
- if (newsletter.sentAt) lines.push(`Sent: ${newsletter.sentAt}`);
2380
- if (newsletter.notes) lines.push(`Notes: ${newsletter.notes}`);
2298
+ if (payload.contentPreferences) {
2299
+ lines.push("### Content Preferences");
2300
+ lines.push(String(payload.contentPreferences));
2381
2301
  lines.push("");
2382
- lines.push("### Full HTML Content:");
2383
- lines.push(newsletter.contentHtml);
2384
- return {
2385
- content: [
2386
- { type: "text", text: lines.join("\n") },
2387
- ...newsletter.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2388
- ${newsletter.contentHtml}` }] : []
2389
- ]
2390
- };
2391
- } catch (error) {
2392
- const message = error instanceof Error ? error.message : "Unknown error";
2393
- if (message.includes("404")) {
2394
- return {
2395
- content: [
2396
- {
2397
- type: "text",
2398
- text: "Newsletter not found. Check the ID and try again."
2399
- }
2400
- ]
2401
- };
2402
- }
2403
- throw error;
2404
2302
  }
2303
+ lines.push("---");
2304
+ lines.push("Call this tool again with **confirmed=true** to save.");
2305
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2405
2306
  }
2406
- try {
2407
- const params = {};
2408
- if (limit) params.limit = String(Math.min(limit, 50));
2409
- else params.limit = "5";
2410
- if (templateId) params.template_id = templateId;
2411
- const newsletters = await client.listNewsletterArchive(
2412
- params
2413
- );
2414
- if (!newsletters || newsletters.length === 0) {
2415
- return {
2416
- content: [
2417
- {
2418
- type: "text",
2419
- text: "No past newsletters found. This will be the first edition!"
2420
- }
2421
- ]
2422
- };
2423
- }
2424
- const lines = [];
2425
- lines.push(
2426
- `## Past Newsletters (${newsletters.length} most recent)`
2427
- );
2428
- lines.push("");
2429
- for (const nl of newsletters) {
2430
- lines.push(`### ${nl.subject} -- ${nl.sentAt || nl.createdAt}`);
2431
- if (nl.notes) lines.push(`Notes: ${nl.notes}`);
2432
- if (nl.metrics) {
2433
- const m = nl.metrics;
2434
- const parts = [];
2435
- if (m.open_rate) parts.push(`Open rate: ${m.open_rate}`);
2436
- if (m.click_rate) parts.push(`Click rate: ${m.click_rate}`);
2437
- if (parts.length) lines.push(`Metrics: ${parts.join(", ")}`);
2307
+ const result = await client.updateAudience(String(target.id), payload);
2308
+ return {
2309
+ content: [
2310
+ {
2311
+ type: "text",
2312
+ text: `Audience "${result.name}" updated successfully.`
2438
2313
  }
2439
- lines.push("---");
2440
- const textContent = nl.contentHtml ? nl.contentHtml.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 500) : "(no content)";
2441
- lines.push(textContent);
2442
- lines.push(
2443
- `[Full content available by calling get_past_newsletters with newsletterId: ${nl.id}]`
2444
- );
2445
- lines.push("");
2446
- }
2447
- const mostRecent = newsletters[0];
2448
- return {
2449
- content: [
2450
- { type: "text", text: lines.join("\n") },
2451
- ...mostRecent?.contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2452
- ${mostRecent.contentHtml}` }] : []
2453
- ]
2454
- };
2455
- } catch (error) {
2456
- const message = error instanceof Error ? error.message : "Unknown error";
2457
- throw new Error(`Failed to fetch past newsletters: ${message}`);
2458
- }
2314
+ ]
2315
+ };
2459
2316
  }
2460
2317
  );
2461
2318
  server.tool(
2462
- "save_newsletter",
2463
- "Save an approved newsletter to the archive for future reference.",
2319
+ "delete_audience",
2320
+ "Delete an audience profile by name. Set confirmed=false to preview first.",
2464
2321
  {
2465
- subject: z11.string().describe("The newsletter subject line."),
2466
- contentHtml: z11.string().describe("The full HTML content of the newsletter."),
2467
- templateId: z11.string().optional().describe("The template ID used to generate this newsletter."),
2468
- notes: z11.string().optional().describe(
2469
- "Optional notes about this newsletter (what worked, theme, etc)."
2470
- )
2322
+ name: z9.string().describe("Name of the audience to delete"),
2323
+ confirmed: z9.boolean().default(false).describe("Set to true to confirm deletion. If false, returns a confirmation prompt.")
2471
2324
  },
2472
2325
  {
2473
- title: "Save Newsletter to Archive",
2326
+ title: "Delete Audience",
2474
2327
  readOnlyHint: false,
2475
- destructiveHint: false,
2328
+ destructiveHint: true,
2476
2329
  idempotentHint: false,
2477
2330
  openWorldHint: false
2478
2331
  },
2479
- async ({ subject, contentHtml, templateId, notes }) => {
2332
+ async (args) => {
2333
+ let target;
2480
2334
  try {
2481
- const data = {
2482
- subject,
2483
- contentHtml
2484
- };
2485
- if (templateId) data.templateId = Number(templateId);
2486
- if (notes) data.notes = notes;
2487
- await client.saveNewsletterToArchive(data);
2335
+ target = await client.getAudienceByName(args.name);
2336
+ } catch {
2488
2337
  return {
2489
2338
  content: [
2490
2339
  {
2491
2340
  type: "text",
2492
- text: `Newsletter "${subject}" has been saved to the archive successfully.`
2493
- },
2494
- ...contentHtml ? [{ type: "text", text: `NEWSLETTER_HTML_PREVIEW:
2495
- ${contentHtml}` }] : []
2496
- ]
2341
+ text: `Audience "${args.name}" not found. Use list_audiences to see available profiles.`
2342
+ }
2343
+ ],
2344
+ isError: true
2497
2345
  };
2498
- } catch (error) {
2499
- const message = error instanceof Error ? error.message : "Unknown error";
2500
- throw new Error(`Failed to save newsletter: ${message}`);
2501
2346
  }
2502
- }
2503
- );
2504
- server.tool(
2505
- "save_newsletter_template",
2506
- "Create or update a newsletter template with HTML and section definitions.",
2507
- {
2508
- template_id: z11.string().optional().describe(
2509
- "If provided, updates this existing template. Otherwise creates a new one."
2510
- ),
2511
- name: z11.string().min(1).max(255).describe("Template name"),
2512
- description: z11.string().max(2e3).optional().describe("What this template is for"),
2513
- html_content: z11.string().min(1).max(1e5).describe(
2514
- "Full HTML template with {{placeholder}} variables."
2515
- ),
2516
- sections: z11.array(
2517
- z11.object({
2518
- name: z11.string().describe("Section identifier, e.g. 'intro'"),
2519
- label: z11.string().describe("Display label, e.g. 'Intro/Greeting'"),
2520
- required: z11.boolean().optional().describe("Whether this section is required"),
2521
- item_count: z11.number().optional().describe("Number of items in this section, if applicable")
2522
- })
2523
- ).max(20).optional().describe("Array of section metadata describing the template structure"),
2524
- style_config: z11.object({
2525
- primary_color: z11.string().optional(),
2526
- accent_color: z11.string().optional(),
2527
- link_color: z11.string().optional(),
2528
- background_color: z11.string().optional(),
2529
- font_family: z11.string().optional(),
2530
- max_width: z11.string().optional()
2531
- }).optional().describe("Colors, fonts, and brand configuration"),
2532
- is_default: z11.boolean().optional().describe(
2533
- "Set as the default template for this customer."
2534
- )
2535
- },
2536
- {
2537
- title: "Save Newsletter Template",
2538
- readOnlyHint: false,
2539
- destructiveHint: false,
2540
- idempotentHint: false,
2541
- openWorldHint: false
2542
- },
2543
- async ({
2544
- template_id,
2545
- name,
2546
- description,
2547
- html_content,
2548
- sections,
2549
- style_config,
2550
- is_default
2551
- }) => {
2552
- try {
2553
- const data = {
2554
- name,
2555
- htmlContent: html_content
2556
- };
2557
- if (description !== void 0) data.description = description;
2558
- if (sections !== void 0) {
2559
- data.sections = sections.map((s) => ({
2560
- id: s.name,
2561
- type: "custom",
2562
- label: s.label,
2563
- required: s.required ?? true,
2564
- count: s.item_count
2565
- }));
2566
- }
2567
- if (style_config !== void 0) {
2568
- data.style = {
2569
- primary_color: style_config.primary_color,
2570
- accent_color: style_config.accent_color,
2571
- link_color: style_config.link_color,
2572
- background_color: style_config.background_color,
2573
- font_family: style_config.font_family,
2574
- content_width: style_config.max_width
2575
- };
2576
- }
2577
- if (is_default !== void 0) data.isDefault = is_default;
2578
- let result;
2579
- if (template_id) {
2580
- result = await client.updateTemplate(
2581
- template_id,
2582
- data
2583
- );
2584
- } else {
2585
- result = await client.createTemplate(data);
2586
- }
2587
- const action = template_id ? "updated" : "created";
2347
+ if (args.confirmed !== true) {
2588
2348
  return {
2589
2349
  content: [
2590
2350
  {
2591
2351
  type: "text",
2592
- text: `Newsletter template "${result.name}" ${action} successfully (ID: ${result.id}).${result.isDefault ? " Set as default." : ""}`
2352
+ text: `## Delete Audience Confirmation
2353
+
2354
+ **Name:** ${target.name}
2355
+ ${target.description ? `**Description:** ${target.description}
2356
+ ` : ""}
2357
+ This action cannot be undone.
2358
+
2359
+ Call this tool again with confirmed=true to delete.`
2593
2360
  }
2594
2361
  ]
2595
2362
  };
2596
- } catch (error) {
2597
- const message = error instanceof Error ? error.message : "Unknown error";
2598
- throw new Error(`Failed to save newsletter template: ${message}`);
2599
2363
  }
2364
+ await client.deleteAudience(String(target.id));
2365
+ return {
2366
+ content: [
2367
+ {
2368
+ type: "text",
2369
+ text: `Audience "${target.name}" has been deleted.`
2370
+ }
2371
+ ]
2372
+ };
2600
2373
  }
2601
2374
  );
2375
+ }
2376
+
2377
+ // src/tools/newsletter-template.ts
2378
+ function registerNewsletterTemplateTools(server, client) {
2602
2379
  server.tool(
2603
- "list_newsletter_templates",
2604
- "List all saved newsletter templates.",
2380
+ "list_esp_templates",
2381
+ "List available email templates from the connected ESP. Use the returned template IDs with create_newsletter's template_id parameter.",
2605
2382
  {},
2606
2383
  {
2607
- title: "List Newsletter Templates",
2384
+ title: "List ESP Templates",
2608
2385
  readOnlyHint: true,
2609
2386
  destructiveHint: false,
2610
2387
  idempotentHint: true,
2611
- openWorldHint: false
2388
+ openWorldHint: true
2612
2389
  },
2613
2390
  async () => {
2614
2391
  try {
2615
- const templates = await client.listTemplates();
2616
- if (!templates || templates.length === 0) {
2392
+ const result = await client.listEmailTemplates();
2393
+ const templates = result?.templates ?? [];
2394
+ if (templates.length === 0) {
2617
2395
  return {
2618
2396
  content: [
2619
2397
  {
2620
2398
  type: "text",
2621
- text: "No newsletter templates found. Use save_newsletter_template to create one."
2399
+ text: "No email templates found in your ESP. Create templates in your ESP dashboard (Kit, Beehiiv, or Mailchimp) and they will appear here."
2622
2400
  }
2623
2401
  ]
2624
2402
  };
2625
2403
  }
2626
2404
  const lines = [];
2627
- lines.push(`## Newsletter Templates (${templates.length})`);
2405
+ lines.push(`## ESP Email Templates (${templates.length})`);
2406
+ lines.push("");
2407
+ lines.push("Pass a template ID to `create_newsletter` via the `template_id` parameter to use it.");
2628
2408
  lines.push("");
2629
2409
  for (const t of templates) {
2630
- const badges = [];
2631
- if (t.isDefault) badges.push("DEFAULT");
2632
- if (t.htmlContent) badges.push("HAS HTML");
2633
- const badgeStr = badges.length > 0 ? ` [${badges.join(", ")}]` : "";
2634
- lines.push(`**${t.name}** (ID: ${t.id})${badgeStr}`);
2635
- if (t.description) lines.push(` ${t.description}`);
2636
- if (t.sections && t.sections.length > 0) {
2637
- lines.push(
2638
- ` Sections: ${t.sections.map((s) => s.label).join(", ")}`
2639
- );
2640
- }
2641
- lines.push("");
2410
+ lines.push(`- **${t.name}** (ID: ${t.id})`);
2642
2411
  }
2643
2412
  return {
2644
2413
  content: [{ type: "text", text: lines.join("\n") }]
2645
2414
  };
2646
2415
  } catch (error) {
2647
2416
  const message = error instanceof Error ? error.message : "Unknown error";
2648
- throw new Error(`Failed to list newsletter templates: ${message}`);
2649
- }
2650
- }
2651
- );
2652
- server.tool(
2653
- "delete_newsletter_template",
2654
- "Delete a newsletter template. Set confirmed=true to proceed.",
2655
- {
2656
- template_id: z11.string().describe("The ID of the newsletter template to delete."),
2657
- confirmed: z11.boolean().default(false).describe(
2658
- "Must be true to actually delete. If false, returns a confirmation prompt."
2659
- )
2660
- },
2661
- {
2662
- title: "Delete Newsletter Template",
2663
- readOnlyHint: false,
2664
- destructiveHint: true,
2665
- idempotentHint: false,
2666
- openWorldHint: false
2667
- },
2668
- async ({ template_id, confirmed }) => {
2669
- try {
2670
- if (!confirmed) {
2671
- const template = await client.getTemplate(template_id);
2672
- return {
2673
- content: [
2674
- {
2675
- type: "text",
2676
- text: `Are you sure you want to delete the template "${template.name}" (ID: ${template.id})? This action cannot be undone. Call delete_newsletter_template again with confirmed=true to proceed.`
2677
- }
2678
- ]
2679
- };
2680
- }
2681
- await client.deleteTemplate(template_id);
2682
- return {
2683
- content: [
2684
- {
2685
- type: "text",
2686
- text: `Newsletter template (ID: ${template_id}) has been deleted.`
2687
- }
2688
- ]
2689
- };
2690
- } catch (error) {
2691
- const message = error instanceof Error ? error.message : "Unknown error";
2692
- if (message.includes("404")) {
2693
- return {
2694
- content: [
2695
- {
2696
- type: "text",
2697
- text: "Template not found. It may have already been deleted."
2698
- }
2699
- ]
2700
- };
2701
- }
2702
- throw new Error(`Failed to delete newsletter template: ${message}`);
2417
+ throw new Error(`Failed to list ESP templates: ${message}`);
2703
2418
  }
2704
2419
  }
2705
2420
  );
2706
2421
  }
2707
2422
 
2708
2423
  // src/tools/calendar.ts
2709
- import { z as z12 } from "zod";
2424
+ import { z as z10 } from "zod";
2710
2425
  function registerCalendarTools(server, client) {
2711
2426
  server.tool(
2712
2427
  "get_calendar",
2713
2428
  "View the content calendar with all scheduled, published, and draft posts.",
2714
2429
  {
2715
- from: z12.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
2716
- to: z12.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
2717
- status: z12.string().optional().describe("Filter by status: scheduled, published, draft, failed"),
2718
- type: z12.string().optional().describe("Filter by type: social_post or newsletter")
2430
+ from: z10.string().optional().describe("ISO date to filter from, e.g. 2024-01-01"),
2431
+ to: z10.string().optional().describe("ISO date to filter to, e.g. 2024-01-31"),
2432
+ status: z10.string().optional().describe("Filter by status: scheduled, published, draft, failed"),
2433
+ type: z10.string().optional().describe("Filter by type: social_post or newsletter")
2719
2434
  },
2720
2435
  {
2721
2436
  title: "Get Content Calendar",
@@ -2851,10 +2566,10 @@ Next slot: ${slotDate.toLocaleString()} (${dayNames[slotDate.getDay()]})
2851
2566
  "schedule_to_queue",
2852
2567
  "Add a post to the next available queue slot. Check get_queue first. Set confirmed=false to preview first.",
2853
2568
  {
2854
- content: z12.string().describe("The text content of the post"),
2855
- platforms: z12.array(z12.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
2856
- media_urls: z12.array(z12.string()).optional().describe("Public URLs of media to attach"),
2857
- confirmed: z12.boolean().default(false).describe(
2569
+ content: z10.string().describe("The text content of the post"),
2570
+ platforms: z10.array(z10.string()).describe('Platforms to post to, e.g. ["twitter", "linkedin"]'),
2571
+ media_urls: z10.array(z10.string()).optional().describe("Public URLs of media to attach"),
2572
+ confirmed: z10.boolean().default(false).describe(
2858
2573
  "Set to true to confirm scheduling. If false or missing, returns a preview for user approval."
2859
2574
  )
2860
2575
  },
@@ -2936,7 +2651,7 @@ ${JSON.stringify(result, null, 2)}`
2936
2651
  }
2937
2652
 
2938
2653
  // src/tools/notifications.ts
2939
- import { z as z13 } from "zod";
2654
+ import { z as z11 } from "zod";
2940
2655
  function timeAgo(dateStr) {
2941
2656
  const diff = Date.now() - new Date(dateStr).getTime();
2942
2657
  const minutes = Math.floor(diff / 6e4);
@@ -2952,8 +2667,8 @@ function registerNotificationTools(server, client) {
2952
2667
  "get_notifications",
2953
2668
  "Get recent notifications including post results, account warnings, and errors.",
2954
2669
  {
2955
- unread_only: z13.boolean().optional().describe("If true, only show unread notifications. Default false."),
2956
- limit: z13.number().optional().describe("Max notifications to return. Default 10.")
2670
+ unread_only: z11.boolean().optional().describe("If true, only show unread notifications. Default false."),
2671
+ limit: z11.number().optional().describe("Max notifications to return. Default 10.")
2957
2672
  },
2958
2673
  {
2959
2674
  title: "Get Notifications",
@@ -3019,71 +2734,8 @@ Showing ${notifications.length} of ${result.unread_count !== void 0 ? "total" :
3019
2734
  );
3020
2735
  }
3021
2736
 
3022
- // src/tools/publishing-rules.ts
3023
- var WORKFLOW = {
3024
- before_writing_content: [
3025
- "Call get_brand_voice to load tone and style rules",
3026
- "Call get_audience to understand who the content is for",
3027
- "For newsletters: also call get_newsletter_template and get_past_newsletters"
3028
- ],
3029
- before_publishing: [
3030
- "Always set confirmed=false first to generate a preview",
3031
- "Show the user a visual preview before confirming",
3032
- "Never set confirmed=true without explicit user approval",
3033
- "For newsletters: show subscriber count and send time before confirming",
3034
- "If require_double_confirm is true, ask for confirmation twice"
3035
- ],
3036
- replies_and_engagement: [
3037
- "Draft the reply and show it before sending",
3038
- "Set confirmed=false first to preview",
3039
- "Remind user that replies are public"
3040
- ],
3041
- newsletters: [
3042
- "Always render newsletter as HTML artifact/preview before pushing to ESP",
3043
- "Use table-based layouts, inline CSS only, 600px max width",
3044
- "Email-safe fonts only: Arial, Helvetica, Georgia, Verdana",
3045
- "All images must use absolute URLs",
3046
- "Keep total email under 102KB to avoid Gmail clipping"
3047
- ]
3048
- };
3049
- function registerPublishingRulesTools(server, client) {
3050
- server.tool(
3051
- "get_publishing_rules",
3052
- "Get publishing rules and the content creation workflow. Call this BEFORE publishing, scheduling, or sending any content.",
3053
- {},
3054
- {
3055
- title: "Get Publishing Rules",
3056
- readOnlyHint: true,
3057
- destructiveHint: false,
3058
- idempotentHint: true,
3059
- openWorldHint: false
3060
- },
3061
- async () => {
3062
- const rules = await client.getPublishingRules();
3063
- const response = {
3064
- rules: {
3065
- social_default_action: rules.socialDefaultAction ?? "draft",
3066
- newsletter_default_action: rules.newsletterDefaultAction ?? "draft",
3067
- require_preview_before_publish: rules.requirePreviewBeforePublish ?? true,
3068
- require_double_confirm_newsletter: rules.requireDoubleConfirmNewsletter ?? true,
3069
- require_double_confirm_social: rules.requireDoubleConfirmSocial ?? false,
3070
- allow_immediate_publish: rules.allowImmediatePublish ?? false,
3071
- allow_immediate_send: rules.allowImmediateSend ?? false,
3072
- max_posts_per_day: rules.maxPostsPerDay ?? null,
3073
- blocked_words: rules.blockedWords ?? [],
3074
- required_disclaimer: rules.requiredDisclaimer ?? null
3075
- },
3076
- workflow: WORKFLOW
3077
- };
3078
- return {
3079
- content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
3080
- };
3081
- }
3082
- );
3083
- }
3084
-
3085
2737
  // src/tools/sources.ts
3086
- import { z as z14 } from "zod";
2738
+ import { z as z12 } from "zod";
3087
2739
  var TYPE_LABELS = {
3088
2740
  feed: "RSS Feed",
3089
2741
  website: "Website",
@@ -3103,14 +2755,15 @@ var TYPE_HINTS = {
3103
2755
  social: "Use web_search to check recent activity"
3104
2756
  };
3105
2757
  function registerSourceTools(server, client) {
2758
+ const MAX_SOURCES = 10;
3106
2759
  server.tool(
3107
2760
  "get_sources",
3108
- "Get saved content sources (RSS feeds, websites, YouTube channels, search topics).",
2761
+ "View your saved content sources (maximum 10). Shows RSS feeds, websites, and channels you're monitoring.",
3109
2762
  {
3110
- type: z14.string().optional().describe(
2763
+ type: z12.string().optional().describe(
3111
2764
  "Optional: filter by type (feed, website, youtube, search, podcast, reddit, social)"
3112
2765
  ),
3113
- category: z14.string().optional().describe("Optional: filter by category")
2766
+ category: z12.string().optional().describe("Optional: filter by category")
3114
2767
  },
3115
2768
  {
3116
2769
  title: "Get Content Sources",
@@ -3130,12 +2783,12 @@ function registerSourceTools(server, client) {
3130
2783
  content: [
3131
2784
  {
3132
2785
  type: "text",
3133
- 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."
2786
+ 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.`
3134
2787
  }
3135
2788
  ]
3136
2789
  };
3137
2790
  }
3138
- const lines = [`## Content Sources (${sources.length})
2791
+ const lines = [`## Content Sources (${sources.length} of ${MAX_SOURCES})
3139
2792
  `];
3140
2793
  const byCategory = /* @__PURE__ */ new Map();
3141
2794
  for (const s of sources) {
@@ -3171,13 +2824,13 @@ function registerSourceTools(server, client) {
3171
2824
  );
3172
2825
  server.tool(
3173
2826
  "add_source",
3174
- "Save a new content source to monitor.",
2827
+ "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.",
3175
2828
  {
3176
- name: z14.string().describe("Display name for the source"),
3177
- url: z14.string().optional().describe(
2829
+ name: z12.string().describe("Display name for the source"),
2830
+ url: z12.string().optional().describe(
3178
2831
  "URL of the source. Not needed for 'search' type."
3179
2832
  ),
3180
- type: z14.enum([
2833
+ type: z12.enum([
3181
2834
  "feed",
3182
2835
  "website",
3183
2836
  "youtube",
@@ -3186,12 +2839,12 @@ function registerSourceTools(server, client) {
3186
2839
  "reddit",
3187
2840
  "social"
3188
2841
  ]).describe("Type of source. Determines how to fetch content from it."),
3189
- category: z14.string().optional().describe(
2842
+ category: z12.string().optional().describe(
3190
2843
  "Optional category for organization (e.g. 'competitors', 'industry', 'inspiration')"
3191
2844
  ),
3192
- tags: z14.array(z14.string()).optional().describe("Optional tags for filtering"),
3193
- notes: z14.string().optional().describe("Optional notes about why this source matters"),
3194
- searchQuery: z14.string().optional().describe(
2845
+ tags: z12.array(z12.string()).optional().describe("Optional tags for filtering"),
2846
+ notes: z12.string().optional().describe("Optional notes about why this source matters"),
2847
+ searchQuery: z12.string().optional().describe(
3195
2848
  "For 'search' type only: the keyword or topic to search for"
3196
2849
  )
3197
2850
  },
@@ -3203,8 +2856,20 @@ function registerSourceTools(server, client) {
3203
2856
  openWorldHint: false
3204
2857
  },
3205
2858
  async (args) => {
3206
- const data = {
3207
- name: args.name,
2859
+ const existing = await client.listSources({});
2860
+ if (existing.sources.length >= MAX_SOURCES) {
2861
+ return {
2862
+ content: [
2863
+ {
2864
+ type: "text",
2865
+ text: `You've reached the maximum of ${MAX_SOURCES} sources. Remove an existing source before adding a new one.`
2866
+ }
2867
+ ],
2868
+ isError: true
2869
+ };
2870
+ }
2871
+ const data = {
2872
+ name: args.name,
3208
2873
  type: args.type
3209
2874
  };
3210
2875
  if (args.url) data.url = args.url;
@@ -3214,6 +2879,7 @@ function registerSourceTools(server, client) {
3214
2879
  if (args.searchQuery) data.searchQuery = args.searchQuery;
3215
2880
  const result = await client.createSource(data);
3216
2881
  const typeLabel = TYPE_LABELS[result.type] || result.type;
2882
+ const remaining = MAX_SOURCES - existing.sources.length - 1;
3217
2883
  const lines = [
3218
2884
  `## Source Added
3219
2885
  `,
@@ -3224,6 +2890,8 @@ function registerSourceTools(server, client) {
3224
2890
  if (result.searchQuery)
3225
2891
  lines.push(`- Search: "${result.searchQuery}"`);
3226
2892
  if (result.category) lines.push(`- Category: ${result.category}`);
2893
+ lines.push(`
2894
+ *${existing.sources.length + 1} of ${MAX_SOURCES} sources used (${remaining} remaining)*`);
3227
2895
  return {
3228
2896
  content: [{ type: "text", text: lines.join("\n") }]
3229
2897
  };
@@ -3233,10 +2901,10 @@ function registerSourceTools(server, client) {
3233
2901
  "update_source",
3234
2902
  "Update a saved content source.",
3235
2903
  {
3236
- source_id: z14.number().describe("The ID of the source to update"),
3237
- name: z14.string().optional().describe("New display name"),
3238
- url: z14.string().optional().describe("New URL"),
3239
- type: z14.enum([
2904
+ source_id: z12.number().describe("The ID of the source to update"),
2905
+ name: z12.string().optional().describe("New display name"),
2906
+ url: z12.string().optional().describe("New URL"),
2907
+ type: z12.enum([
3240
2908
  "feed",
3241
2909
  "website",
3242
2910
  "youtube",
@@ -3245,11 +2913,11 @@ function registerSourceTools(server, client) {
3245
2913
  "reddit",
3246
2914
  "social"
3247
2915
  ]).optional().describe("New source type"),
3248
- category: z14.string().optional().describe("New category"),
3249
- tags: z14.array(z14.string()).optional().describe("New tags"),
3250
- notes: z14.string().optional().describe("New notes"),
3251
- searchQuery: z14.string().optional().describe("New search query"),
3252
- isActive: z14.boolean().optional().describe("Set to false to deactivate")
2916
+ category: z12.string().optional().describe("New category"),
2917
+ tags: z12.array(z12.string()).optional().describe("New tags"),
2918
+ notes: z12.string().optional().describe("New notes"),
2919
+ searchQuery: z12.string().optional().describe("New search query"),
2920
+ isActive: z12.boolean().optional().describe("Set to false to deactivate")
3253
2921
  },
3254
2922
  {
3255
2923
  title: "Update Content Source",
@@ -3279,8 +2947,8 @@ function registerSourceTools(server, client) {
3279
2947
  "remove_source",
3280
2948
  "Remove a content source. Set confirmed=false to preview first.",
3281
2949
  {
3282
- source_id: z14.number().describe("The ID of the source to remove"),
3283
- confirmed: z14.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
2950
+ source_id: z12.number().describe("The ID of the source to remove"),
2951
+ confirmed: z12.boolean().default(false).describe("Set to true to confirm deletion. If false or missing, returns a preview for user approval.")
3284
2952
  },
3285
2953
  {
3286
2954
  title: "Remove Content Source",
@@ -3310,6 +2978,823 @@ This will permanently remove this content source. Call this tool again with conf
3310
2978
  }
3311
2979
  );
3312
2980
  }
2981
+
2982
+ // src/tools/wordpress.ts
2983
+ import { z as z13 } from "zod";
2984
+ function registerWordPressTools(server, client) {
2985
+ server.tool(
2986
+ "wp_create_post",
2987
+ "Create a new WordPress post. Defaults to draft status. Set confirmed=false to preview before publishing.",
2988
+ {
2989
+ title: z13.string().optional().describe("Post title"),
2990
+ content: z13.string().optional().describe("Post content (HTML)"),
2991
+ status: z13.enum(["draft", "publish", "pending", "private", "future"]).default("draft").describe("Post status. Use 'future' with date for scheduling."),
2992
+ categories: z13.array(z13.number()).optional().describe("Array of category IDs"),
2993
+ tags: z13.array(z13.number()).optional().describe("Array of tag IDs"),
2994
+ featured_media: z13.number().optional().describe("Featured image attachment ID"),
2995
+ excerpt: z13.string().optional().describe("Post excerpt"),
2996
+ date: z13.string().optional().describe(
2997
+ "Publication date in ISO 8601 format. Required when status is 'future'."
2998
+ ),
2999
+ slug: z13.string().optional().describe("URL slug for the post"),
3000
+ confirmed: z13.boolean().default(false).describe(
3001
+ "Set to true to confirm creation. If false, returns a preview for approval. Required when status is 'publish'."
3002
+ )
3003
+ },
3004
+ {
3005
+ title: "Create WordPress Post",
3006
+ readOnlyHint: false,
3007
+ destructiveHint: false,
3008
+ idempotentHint: false,
3009
+ openWorldHint: true
3010
+ },
3011
+ async (args) => {
3012
+ if (args.confirmed !== true && (args.status === "publish" || args.status === "future")) {
3013
+ const preview = `## WordPress Post Preview
3014
+
3015
+ **Title:** ${args.title ?? "(no title)"}
3016
+ **Status:** ${args.status}
3017
+ **Content length:** ${(args.content ?? "").length} characters
3018
+ ` + (args.categories?.length ? `**Categories:** ${args.categories.join(", ")}
3019
+ ` : "") + (args.tags?.length ? `**Tags:** ${args.tags.join(", ")}
3020
+ ` : "") + (args.date ? `**Date:** ${args.date}
3021
+ ` : "") + (args.slug ? `**Slug:** ${args.slug}
3022
+ ` : "") + (args.excerpt ? `**Excerpt:** ${args.excerpt.slice(0, 200)}
3023
+ ` : "") + `
3024
+ **Action:** This will ${args.status === "publish" ? "publish immediately" : "schedule"} on your WordPress site.
3025
+
3026
+ Call this tool again with confirmed=true to proceed.`;
3027
+ return { content: [{ type: "text", text: preview }] };
3028
+ }
3029
+ const body = {
3030
+ title: args.title,
3031
+ content: args.content,
3032
+ status: args.status
3033
+ };
3034
+ if (args.categories) body.categories = args.categories;
3035
+ if (args.tags) body.tags = args.tags;
3036
+ if (args.featured_media) body.featured_media = args.featured_media;
3037
+ if (args.excerpt) body.excerpt = args.excerpt;
3038
+ if (args.date) body.date = args.date;
3039
+ if (args.slug) body.slug = args.slug;
3040
+ const result = await client.wpCreatePost(body);
3041
+ return {
3042
+ content: [
3043
+ { type: "text", text: JSON.stringify(result, null, 2) }
3044
+ ]
3045
+ };
3046
+ }
3047
+ );
3048
+ server.tool(
3049
+ "wp_update_post",
3050
+ "Update an existing WordPress post by ID.",
3051
+ {
3052
+ post_id: z13.number().describe("The WordPress post ID to update"),
3053
+ title: z13.string().optional().describe("New post title"),
3054
+ content: z13.string().optional().describe("New post content (HTML)"),
3055
+ status: z13.enum(["draft", "publish", "pending", "private", "future"]).optional().describe("New post status"),
3056
+ categories: z13.array(z13.number()).optional().describe("New category IDs"),
3057
+ tags: z13.array(z13.number()).optional().describe("New tag IDs"),
3058
+ featured_media: z13.number().optional().describe("New featured image attachment ID"),
3059
+ excerpt: z13.string().optional().describe("New post excerpt"),
3060
+ date: z13.string().optional().describe("New publication date (ISO 8601)"),
3061
+ slug: z13.string().optional().describe("New URL slug")
3062
+ },
3063
+ {
3064
+ title: "Update WordPress Post",
3065
+ readOnlyHint: false,
3066
+ destructiveHint: false,
3067
+ idempotentHint: true,
3068
+ openWorldHint: true
3069
+ },
3070
+ async (args) => {
3071
+ const { post_id, ...data } = args;
3072
+ const result = await client.wpUpdatePost(post_id, data);
3073
+ return {
3074
+ content: [
3075
+ { type: "text", text: JSON.stringify(result, null, 2) }
3076
+ ]
3077
+ };
3078
+ }
3079
+ );
3080
+ server.tool(
3081
+ "wp_publish_post",
3082
+ "Change a WordPress post's status to 'publish'. Requires confirmation.",
3083
+ {
3084
+ post_id: z13.number().describe("The WordPress post ID to publish"),
3085
+ confirmed: z13.boolean().default(false).describe(
3086
+ "Set to true to confirm publishing. If false, returns a preview."
3087
+ )
3088
+ },
3089
+ {
3090
+ title: "Publish WordPress Post",
3091
+ readOnlyHint: false,
3092
+ destructiveHint: false,
3093
+ idempotentHint: true,
3094
+ openWorldHint: true
3095
+ },
3096
+ async (args) => {
3097
+ if (args.confirmed !== true) {
3098
+ try {
3099
+ const post = await client.wpGetPost(args.post_id);
3100
+ const preview = `## Publish WordPress Post
3101
+
3102
+ **Post ID:** ${args.post_id}
3103
+ **Title:** ${post.title?.rendered ?? "(no title)"}
3104
+ **Current Status:** ${post.status ?? "unknown"}
3105
+
3106
+ This will publish this post on your WordPress site, making it publicly visible.
3107
+
3108
+ Call this tool again with confirmed=true to proceed.`;
3109
+ return { content: [{ type: "text", text: preview }] };
3110
+ } catch {
3111
+ const preview = `## Publish WordPress Post
3112
+
3113
+ **Post ID:** ${args.post_id}
3114
+
3115
+ This will publish this post. Call this tool again with confirmed=true to proceed.`;
3116
+ return { content: [{ type: "text", text: preview }] };
3117
+ }
3118
+ }
3119
+ const result = await client.wpUpdatePost(args.post_id, {
3120
+ status: "publish"
3121
+ });
3122
+ return {
3123
+ content: [
3124
+ { type: "text", text: JSON.stringify(result, null, 2) }
3125
+ ]
3126
+ };
3127
+ }
3128
+ );
3129
+ server.tool(
3130
+ "wp_list_posts",
3131
+ "List WordPress posts with optional filters.",
3132
+ {
3133
+ status: z13.string().optional().describe(
3134
+ "Filter by status: draft, publish, pending, private, future, trash"
3135
+ ),
3136
+ per_page: z13.string().optional().describe("Number of posts per page (default 10, max 100)"),
3137
+ page: z13.string().optional().describe("Page number for pagination"),
3138
+ search: z13.string().optional().describe("Search posts by keyword"),
3139
+ categories: z13.string().optional().describe("Filter by category ID (comma-separated)"),
3140
+ tags: z13.string().optional().describe("Filter by tag ID (comma-separated)")
3141
+ },
3142
+ {
3143
+ title: "List WordPress Posts",
3144
+ readOnlyHint: true,
3145
+ destructiveHint: false,
3146
+ idempotentHint: true,
3147
+ openWorldHint: true
3148
+ },
3149
+ async (args) => {
3150
+ const params = {};
3151
+ if (args.status) params.status = args.status;
3152
+ if (args.per_page) params.per_page = args.per_page;
3153
+ if (args.page) params.page = args.page;
3154
+ if (args.search) params.search = args.search;
3155
+ if (args.categories) params.categories = args.categories;
3156
+ if (args.tags) params.tags = args.tags;
3157
+ const result = await client.wpListPosts(params);
3158
+ return {
3159
+ content: [
3160
+ { type: "text", text: JSON.stringify(result, null, 2) }
3161
+ ]
3162
+ };
3163
+ }
3164
+ );
3165
+ server.tool(
3166
+ "wp_get_post",
3167
+ "Get detailed information about a specific WordPress post by ID.",
3168
+ {
3169
+ post_id: z13.number().describe("The WordPress post ID to retrieve")
3170
+ },
3171
+ {
3172
+ title: "Get WordPress Post",
3173
+ readOnlyHint: true,
3174
+ destructiveHint: false,
3175
+ idempotentHint: true,
3176
+ openWorldHint: true
3177
+ },
3178
+ async (args) => {
3179
+ const result = await client.wpGetPost(args.post_id);
3180
+ return {
3181
+ content: [
3182
+ { type: "text", text: JSON.stringify(result, null, 2) }
3183
+ ]
3184
+ };
3185
+ }
3186
+ );
3187
+ server.tool(
3188
+ "wp_delete_post",
3189
+ "Trash a WordPress post. Use force=true to permanently delete. Requires confirmation.",
3190
+ {
3191
+ post_id: z13.number().describe("The WordPress post ID to delete"),
3192
+ force: z13.boolean().default(false).describe("Set to true to permanently delete instead of trashing"),
3193
+ confirmed: z13.boolean().default(false).describe(
3194
+ "Set to true to confirm deletion. If false, returns a preview."
3195
+ )
3196
+ },
3197
+ {
3198
+ title: "Delete WordPress Post",
3199
+ readOnlyHint: false,
3200
+ destructiveHint: true,
3201
+ idempotentHint: true,
3202
+ openWorldHint: true
3203
+ },
3204
+ async (args) => {
3205
+ if (args.confirmed !== true) {
3206
+ const action = args.force ? "permanently delete" : "move to trash";
3207
+ const preview = `## Delete WordPress Post
3208
+
3209
+ **Post ID:** ${args.post_id}
3210
+ **Action:** ${action}
3211
+
3212
+ ${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.`;
3213
+ return { content: [{ type: "text", text: preview }] };
3214
+ }
3215
+ const params = {};
3216
+ if (args.force) params.force = "true";
3217
+ const result = await client.wpDeletePost(args.post_id, params);
3218
+ return {
3219
+ content: [
3220
+ {
3221
+ type: "text",
3222
+ text: args.force ? "Post permanently deleted." : JSON.stringify(result, null, 2)
3223
+ }
3224
+ ]
3225
+ };
3226
+ }
3227
+ );
3228
+ server.tool(
3229
+ "wp_upload_media",
3230
+ "Upload an image from a URL to the WordPress media library.",
3231
+ {
3232
+ url: z13.string().url().describe("Public URL of the image to upload to WordPress"),
3233
+ filename: z13.string().optional().describe("Optional filename for the uploaded image")
3234
+ },
3235
+ {
3236
+ title: "Upload WordPress Media",
3237
+ readOnlyHint: false,
3238
+ destructiveHint: false,
3239
+ idempotentHint: false,
3240
+ openWorldHint: true
3241
+ },
3242
+ async (args) => {
3243
+ const result = await client.wpUploadMedia({
3244
+ url: args.url,
3245
+ filename: args.filename
3246
+ });
3247
+ return {
3248
+ content: [
3249
+ { type: "text", text: JSON.stringify(result, null, 2) }
3250
+ ]
3251
+ };
3252
+ }
3253
+ );
3254
+ server.tool(
3255
+ "wp_list_categories",
3256
+ "List all available WordPress categories.",
3257
+ {},
3258
+ {
3259
+ title: "List WordPress Categories",
3260
+ readOnlyHint: true,
3261
+ destructiveHint: false,
3262
+ idempotentHint: true,
3263
+ openWorldHint: true
3264
+ },
3265
+ async () => {
3266
+ const result = await client.wpListCategories();
3267
+ return {
3268
+ content: [
3269
+ { type: "text", text: JSON.stringify(result, null, 2) }
3270
+ ]
3271
+ };
3272
+ }
3273
+ );
3274
+ server.tool(
3275
+ "wp_list_tags",
3276
+ "List all available WordPress tags.",
3277
+ {},
3278
+ {
3279
+ title: "List WordPress Tags",
3280
+ readOnlyHint: true,
3281
+ destructiveHint: false,
3282
+ idempotentHint: true,
3283
+ openWorldHint: true
3284
+ },
3285
+ async () => {
3286
+ const result = await client.wpListTags();
3287
+ return {
3288
+ content: [
3289
+ { type: "text", text: JSON.stringify(result, null, 2) }
3290
+ ]
3291
+ };
3292
+ }
3293
+ );
3294
+ server.tool(
3295
+ "wp_create_page",
3296
+ "Create a new WordPress page. Defaults to draft status.",
3297
+ {
3298
+ title: z13.string().optional().describe("Page title"),
3299
+ content: z13.string().optional().describe("Page content (HTML)"),
3300
+ status: z13.enum(["draft", "publish", "pending", "private"]).default("draft").describe("Page status"),
3301
+ excerpt: z13.string().optional().describe("Page excerpt"),
3302
+ slug: z13.string().optional().describe("URL slug for the page"),
3303
+ featured_media: z13.number().optional().describe("Featured image attachment ID"),
3304
+ confirmed: z13.boolean().default(false).describe(
3305
+ "Set to true to confirm creation. Required when status is 'publish'."
3306
+ )
3307
+ },
3308
+ {
3309
+ title: "Create WordPress Page",
3310
+ readOnlyHint: false,
3311
+ destructiveHint: false,
3312
+ idempotentHint: false,
3313
+ openWorldHint: true
3314
+ },
3315
+ async (args) => {
3316
+ if (args.confirmed !== true && args.status === "publish") {
3317
+ const preview = `## WordPress Page Preview
3318
+
3319
+ **Title:** ${args.title ?? "(no title)"}
3320
+ **Status:** ${args.status}
3321
+ **Content length:** ${(args.content ?? "").length} characters
3322
+ ` + (args.slug ? `**Slug:** ${args.slug}
3323
+ ` : "") + `
3324
+ This will publish the page immediately on your WordPress site.
3325
+
3326
+ Call this tool again with confirmed=true to proceed.`;
3327
+ return { content: [{ type: "text", text: preview }] };
3328
+ }
3329
+ const body = {
3330
+ title: args.title,
3331
+ content: args.content,
3332
+ status: args.status
3333
+ };
3334
+ if (args.excerpt) body.excerpt = args.excerpt;
3335
+ if (args.slug) body.slug = args.slug;
3336
+ if (args.featured_media) body.featured_media = args.featured_media;
3337
+ const result = await client.wpCreatePage(body);
3338
+ return {
3339
+ content: [
3340
+ { type: "text", text: JSON.stringify(result, null, 2) }
3341
+ ]
3342
+ };
3343
+ }
3344
+ );
3345
+ }
3346
+
3347
+ // src/tools/ghost.ts
3348
+ import { z as z14 } from "zod";
3349
+ function registerGhostTools(server, client) {
3350
+ server.tool(
3351
+ "ghost_create_post",
3352
+ "Create a post on Ghost. Defaults to draft status. Set confirmed=false to preview before publishing.",
3353
+ {
3354
+ title: z14.string().describe("Post title"),
3355
+ html: z14.string().optional().describe("HTML content of the post"),
3356
+ status: z14.enum(["draft", "published", "scheduled"]).default("draft").describe("Post status: draft, published, or scheduled"),
3357
+ tags: z14.array(z14.string()).optional().describe("Tag names to apply to the post"),
3358
+ featured: z14.boolean().optional().describe("Whether the post is featured"),
3359
+ custom_excerpt: z14.string().optional().describe("Custom excerpt for the post"),
3360
+ feature_image: z14.string().optional().describe("URL of the feature image"),
3361
+ confirmed: z14.boolean().default(false).describe(
3362
+ "Set to true to confirm creation. Required when status is 'published'. Drafts are created immediately."
3363
+ )
3364
+ },
3365
+ {
3366
+ title: "Create Ghost Post",
3367
+ readOnlyHint: false,
3368
+ destructiveHint: false,
3369
+ idempotentHint: false,
3370
+ openWorldHint: true
3371
+ },
3372
+ async (args) => {
3373
+ const needsConfirmation = args.status !== "draft";
3374
+ if (needsConfirmation && args.confirmed !== true) {
3375
+ const preview = `## Ghost Post Preview
3376
+
3377
+ **Title:** ${args.title}
3378
+ **Status:** ${args.status}
3379
+ ` + (args.tags?.length ? `**Tags:** ${args.tags.join(", ")}
3380
+ ` : "") + (args.featured ? `**Featured:** yes
3381
+ ` : "") + (args.custom_excerpt ? `**Excerpt:** ${args.custom_excerpt}
3382
+ ` : "") + (args.feature_image ? `**Feature Image:** ${args.feature_image}
3383
+ ` : "") + (args.html ? `
3384
+ **Content preview:** ${args.html.replace(/<[^>]*>/g, "").slice(0, 200)}...
3385
+ ` : "") + `
3386
+ **Action:** This will be **${args.status}** on Ghost immediately.
3387
+
3388
+ Call this tool again with confirmed=true to proceed.`;
3389
+ return { content: [{ type: "text", text: preview }] };
3390
+ }
3391
+ const body = {
3392
+ title: args.title,
3393
+ status: args.status
3394
+ };
3395
+ if (args.html) body.html = args.html;
3396
+ if (args.tags) body.tags = args.tags.map((name) => ({ name }));
3397
+ if (args.featured !== void 0) body.featured = args.featured;
3398
+ if (args.custom_excerpt) body.custom_excerpt = args.custom_excerpt;
3399
+ if (args.feature_image) body.feature_image = args.feature_image;
3400
+ const result = await client.ghostCreatePost(body);
3401
+ return {
3402
+ content: [
3403
+ { type: "text", text: JSON.stringify(result, null, 2) }
3404
+ ]
3405
+ };
3406
+ }
3407
+ );
3408
+ server.tool(
3409
+ "ghost_update_post",
3410
+ "Update an existing Ghost post. Requires updated_at for collision detection.",
3411
+ {
3412
+ post_id: z14.string().describe("The Ghost post ID to update"),
3413
+ updated_at: z14.string().describe(
3414
+ "The updated_at value from the post (required for collision detection)"
3415
+ ),
3416
+ title: z14.string().optional().describe("Updated post title"),
3417
+ html: z14.string().optional().describe("Updated HTML content"),
3418
+ status: z14.enum(["draft", "published", "scheduled"]).optional().describe("Updated status"),
3419
+ tags: z14.array(z14.string()).optional().describe("Updated tag names"),
3420
+ featured: z14.boolean().optional().describe("Whether the post is featured"),
3421
+ custom_excerpt: z14.string().optional().describe("Updated custom excerpt"),
3422
+ feature_image: z14.string().optional().describe("Updated feature image URL")
3423
+ },
3424
+ {
3425
+ title: "Update Ghost Post",
3426
+ readOnlyHint: false,
3427
+ destructiveHint: false,
3428
+ idempotentHint: true,
3429
+ openWorldHint: true
3430
+ },
3431
+ async (args) => {
3432
+ const data = {
3433
+ updated_at: args.updated_at
3434
+ };
3435
+ if (args.title) data.title = args.title;
3436
+ if (args.html) data.html = args.html;
3437
+ if (args.status) data.status = args.status;
3438
+ if (args.tags) data.tags = args.tags.map((name) => ({ name }));
3439
+ if (args.featured !== void 0) data.featured = args.featured;
3440
+ if (args.custom_excerpt) data.custom_excerpt = args.custom_excerpt;
3441
+ if (args.feature_image) data.feature_image = args.feature_image;
3442
+ const result = await client.ghostUpdatePost(args.post_id, data);
3443
+ return {
3444
+ content: [
3445
+ { type: "text", text: JSON.stringify(result, null, 2) }
3446
+ ]
3447
+ };
3448
+ }
3449
+ );
3450
+ server.tool(
3451
+ "ghost_publish_post",
3452
+ "Publish a Ghost draft post. This makes the post live. Set confirmed=false to preview first.",
3453
+ {
3454
+ post_id: z14.string().describe("The Ghost post ID to publish"),
3455
+ updated_at: z14.string().describe(
3456
+ "The updated_at value from the post (required for collision detection)"
3457
+ ),
3458
+ confirmed: z14.boolean().default(false).describe(
3459
+ "Set to true to confirm publishing. If false or missing, returns a confirmation prompt."
3460
+ )
3461
+ },
3462
+ {
3463
+ title: "Publish Ghost Post",
3464
+ readOnlyHint: false,
3465
+ destructiveHint: false,
3466
+ idempotentHint: false,
3467
+ openWorldHint: true
3468
+ },
3469
+ async (args) => {
3470
+ if (args.confirmed !== true) {
3471
+ let postInfo = "";
3472
+ try {
3473
+ const post = await client.ghostGetPost(args.post_id);
3474
+ postInfo = `**Title:** ${post?.title ?? "Unknown"}
3475
+ **Current Status:** ${post?.status ?? "Unknown"}
3476
+ ` + (post?.tags?.length ? `**Tags:** ${post.tags.map((t) => t.name).join(", ")}
3477
+ ` : "");
3478
+ } catch {
3479
+ postInfo = `**Post ID:** ${args.post_id}
3480
+ `;
3481
+ }
3482
+ const preview = `## Publish Ghost Post Confirmation
3483
+
3484
+ ` + postInfo + `
3485
+ **Action:** This will make the post **live** on your Ghost site.
3486
+
3487
+ Call this tool again with confirmed=true to publish.`;
3488
+ return { content: [{ type: "text", text: preview }] };
3489
+ }
3490
+ const result = await client.ghostPublishPost(args.post_id, {
3491
+ updated_at: args.updated_at
3492
+ });
3493
+ return {
3494
+ content: [
3495
+ { type: "text", text: `Post published successfully.
3496
+
3497
+ ${JSON.stringify(result, null, 2)}` }
3498
+ ]
3499
+ };
3500
+ }
3501
+ );
3502
+ server.tool(
3503
+ "ghost_list_posts",
3504
+ "List posts from Ghost with optional status filter.",
3505
+ {
3506
+ status: z14.enum(["draft", "published", "scheduled"]).optional().describe("Filter posts by status"),
3507
+ limit: z14.string().optional().describe("Number of posts to return (default: 15)"),
3508
+ page: z14.string().optional().describe("Page number for pagination")
3509
+ },
3510
+ {
3511
+ title: "List Ghost Posts",
3512
+ readOnlyHint: true,
3513
+ destructiveHint: false,
3514
+ idempotentHint: true,
3515
+ openWorldHint: true
3516
+ },
3517
+ async (args) => {
3518
+ const params = {};
3519
+ if (args.status) params.status = args.status;
3520
+ if (args.limit) params.limit = args.limit;
3521
+ if (args.page) params.page = args.page;
3522
+ const result = await client.ghostListPosts(params);
3523
+ const posts = result?.posts ?? [];
3524
+ if (posts.length === 0) {
3525
+ return {
3526
+ content: [
3527
+ {
3528
+ type: "text",
3529
+ text: "No Ghost posts found matching your filters."
3530
+ }
3531
+ ]
3532
+ };
3533
+ }
3534
+ let text = `## Ghost Posts (${posts.length}`;
3535
+ if (result?.meta?.pagination?.total != null) {
3536
+ text += ` of ${result.meta.pagination.total}`;
3537
+ }
3538
+ text += ")\n\n";
3539
+ for (const p of posts) {
3540
+ const status = (p.status ?? "unknown").toUpperCase();
3541
+ const date = p.published_at ? new Date(p.published_at).toLocaleString() : p.created_at ? new Date(p.created_at).toLocaleString() : "";
3542
+ const tags = p.tags?.length > 0 ? ` | Tags: ${p.tags.map((t) => t.name).join(", ")}` : "";
3543
+ text += `- **[${status}]** "${p.title ?? "(no title)"}"
3544
+ `;
3545
+ text += ` ID: \`${p.id}\``;
3546
+ if (date) text += ` | ${p.published_at ? "Published" : "Created"}: ${date}`;
3547
+ text += tags;
3548
+ text += "\n";
3549
+ }
3550
+ return { content: [{ type: "text", text }] };
3551
+ }
3552
+ );
3553
+ server.tool(
3554
+ "ghost_get_post",
3555
+ "Get detailed information about a specific Ghost post by ID.",
3556
+ {
3557
+ post_id: z14.string().describe("The Ghost post ID to retrieve")
3558
+ },
3559
+ {
3560
+ title: "Get Ghost Post",
3561
+ readOnlyHint: true,
3562
+ destructiveHint: false,
3563
+ idempotentHint: true,
3564
+ openWorldHint: true
3565
+ },
3566
+ async (args) => {
3567
+ const result = await client.ghostGetPost(args.post_id);
3568
+ let text = `## Ghost Post Details
3569
+
3570
+ `;
3571
+ text += `**Title:** ${result?.title ?? "Unknown"}
3572
+ `;
3573
+ text += `**ID:** \`${result?.id}\`
3574
+ `;
3575
+ text += `**Status:** ${result?.status ?? "Unknown"}
3576
+ `;
3577
+ text += `**Slug:** ${result?.slug ?? "N/A"}
3578
+ `;
3579
+ if (result?.url) text += `**URL:** ${result.url}
3580
+ `;
3581
+ if (result?.custom_excerpt) text += `**Excerpt:** ${result.custom_excerpt}
3582
+ `;
3583
+ if (result?.feature_image) text += `**Feature Image:** ${result.feature_image}
3584
+ `;
3585
+ if (result?.featured) text += `**Featured:** yes
3586
+ `;
3587
+ if (result?.tags?.length > 0) {
3588
+ text += `**Tags:** ${result.tags.map((t) => t.name).join(", ")}
3589
+ `;
3590
+ }
3591
+ if (result?.authors?.length > 0) {
3592
+ text += `**Authors:** ${result.authors.map((a) => a.name).join(", ")}
3593
+ `;
3594
+ }
3595
+ if (result?.published_at) {
3596
+ text += `**Published:** ${new Date(result.published_at).toLocaleString()}
3597
+ `;
3598
+ }
3599
+ text += `**Created:** ${result?.created_at ? new Date(result.created_at).toLocaleString() : "N/A"}
3600
+ `;
3601
+ text += `**Updated:** ${result?.updated_at ?? "N/A"}
3602
+ `;
3603
+ text += `
3604
+ *Use updated_at value \`${result?.updated_at}\` when updating or publishing this post.*
3605
+ `;
3606
+ if (result?.html) {
3607
+ const plainText = result.html.replace(/<[^>]*>/g, "");
3608
+ const preview = plainText.length > 500 ? plainText.slice(0, 500) + "..." : plainText;
3609
+ text += `
3610
+ ### Content Preview
3611
+ ${preview}
3612
+ `;
3613
+ }
3614
+ return { content: [{ type: "text", text }] };
3615
+ }
3616
+ );
3617
+ server.tool(
3618
+ "ghost_send_newsletter",
3619
+ "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.",
3620
+ {
3621
+ post_id: z14.string().describe("The Ghost post ID to send as newsletter"),
3622
+ updated_at: z14.string().describe("The updated_at value from the post"),
3623
+ newsletter_id: z14.string().describe(
3624
+ "The Ghost newsletter ID to send through. Use ghost_list_newsletters to find available newsletters."
3625
+ ),
3626
+ email_only: z14.boolean().default(false).describe(
3627
+ "If true, sends email only without publishing the post on the site"
3628
+ ),
3629
+ confirmed: z14.boolean().default(false).describe(
3630
+ "Set to true to confirm and send. If false or missing, returns a confirmation prompt."
3631
+ )
3632
+ },
3633
+ {
3634
+ title: "Send Ghost Newsletter",
3635
+ readOnlyHint: false,
3636
+ destructiveHint: false,
3637
+ idempotentHint: false,
3638
+ openWorldHint: true
3639
+ },
3640
+ async (args) => {
3641
+ if (args.confirmed !== true) {
3642
+ let postInfo = "";
3643
+ let memberCount = "unknown";
3644
+ let newsletterName = "unknown";
3645
+ try {
3646
+ const post = await client.ghostGetPost(args.post_id);
3647
+ postInfo = `**Title:** ${post?.title ?? "Unknown"}
3648
+ **Current Status:** ${post?.status ?? "Unknown"}
3649
+ `;
3650
+ } catch {
3651
+ postInfo = `**Post ID:** ${args.post_id}
3652
+ `;
3653
+ }
3654
+ try {
3655
+ const members = await client.ghostListMembers({ limit: "1" });
3656
+ memberCount = String(
3657
+ members?.meta?.pagination?.total ?? "unknown"
3658
+ );
3659
+ } catch {
3660
+ }
3661
+ try {
3662
+ const newsletters = await client.ghostListNewsletters();
3663
+ const nl = (newsletters?.newsletters ?? []).find(
3664
+ (n) => n.id === args.newsletter_id
3665
+ );
3666
+ if (nl) newsletterName = nl.name;
3667
+ } catch {
3668
+ }
3669
+ const preview = `## Send Ghost Newsletter Confirmation
3670
+
3671
+ ` + postInfo + `**Newsletter:** ${newsletterName} (\`${args.newsletter_id}\`)
3672
+ **Members:** ~${memberCount} subscribers
3673
+ **Email Only:** ${args.email_only ? "yes (post will NOT be published on site)" : "no (post will also be published)"}
3674
+
3675
+ **This action cannot be undone.** Once sent, the email goes to all newsletter subscribers.
3676
+
3677
+ Call this tool again with confirmed=true to send.`;
3678
+ return { content: [{ type: "text", text: preview }] };
3679
+ }
3680
+ const result = await client.ghostSendNewsletter(args.post_id, {
3681
+ updated_at: args.updated_at,
3682
+ newsletter_id: args.newsletter_id,
3683
+ email_only: args.email_only
3684
+ });
3685
+ return {
3686
+ content: [
3687
+ {
3688
+ type: "text",
3689
+ text: `Newsletter sent successfully.
3690
+
3691
+ ${JSON.stringify(result, null, 2)}`
3692
+ }
3693
+ ]
3694
+ };
3695
+ }
3696
+ );
3697
+ server.tool(
3698
+ "ghost_list_members",
3699
+ "List Ghost newsletter members (subscribers).",
3700
+ {
3701
+ limit: z14.string().optional().describe("Number of members to return (default: 15)"),
3702
+ page: z14.string().optional().describe("Page number for pagination"),
3703
+ filter: z14.string().optional().describe(
3704
+ "Ghost NQL filter string, e.g. 'status:free' or 'subscribed:true'"
3705
+ )
3706
+ },
3707
+ {
3708
+ title: "List Ghost Members",
3709
+ readOnlyHint: true,
3710
+ destructiveHint: false,
3711
+ idempotentHint: true,
3712
+ openWorldHint: true
3713
+ },
3714
+ async (args) => {
3715
+ const params = {};
3716
+ if (args.limit) params.limit = args.limit;
3717
+ if (args.page) params.page = args.page;
3718
+ if (args.filter) params.filter = args.filter;
3719
+ const result = await client.ghostListMembers(params);
3720
+ const members = result?.members ?? [];
3721
+ if (members.length === 0) {
3722
+ return {
3723
+ content: [
3724
+ {
3725
+ type: "text",
3726
+ text: "No Ghost members found matching your filters."
3727
+ }
3728
+ ]
3729
+ };
3730
+ }
3731
+ let text = `## Ghost Members (${members.length}`;
3732
+ if (result?.meta?.pagination?.total != null) {
3733
+ text += ` of ${result.meta.pagination.total}`;
3734
+ }
3735
+ text += ")\n\n";
3736
+ for (const m of members) {
3737
+ const status = m.status ?? "unknown";
3738
+ text += `- **${m.name || m.email}**`;
3739
+ if (m.name && m.email) text += ` (${m.email})`;
3740
+ text += ` | Status: ${status}`;
3741
+ if (m.created_at) {
3742
+ text += ` | Joined: ${new Date(m.created_at).toLocaleDateString()}`;
3743
+ }
3744
+ text += "\n";
3745
+ }
3746
+ return { content: [{ type: "text", text }] };
3747
+ }
3748
+ );
3749
+ server.tool(
3750
+ "ghost_list_newsletters",
3751
+ "List configured newsletters in Ghost.",
3752
+ {},
3753
+ {
3754
+ title: "List Ghost Newsletters",
3755
+ readOnlyHint: true,
3756
+ destructiveHint: false,
3757
+ idempotentHint: true,
3758
+ openWorldHint: true
3759
+ },
3760
+ async () => {
3761
+ const result = await client.ghostListNewsletters();
3762
+ const newsletters = result?.newsletters ?? [];
3763
+ if (newsletters.length === 0) {
3764
+ return {
3765
+ content: [
3766
+ {
3767
+ type: "text",
3768
+ text: "No newsletters configured in Ghost."
3769
+ }
3770
+ ]
3771
+ };
3772
+ }
3773
+ let text = `## Ghost Newsletters (${newsletters.length})
3774
+
3775
+ `;
3776
+ for (const nl of newsletters) {
3777
+ text += `- **${nl.name}**
3778
+ `;
3779
+ text += ` ID: \`${nl.id}\``;
3780
+ if (nl.description) text += ` | ${nl.description}`;
3781
+ text += `
3782
+ Status: ${nl.status ?? "active"}`;
3783
+ if (nl.subscribe_on_signup !== void 0) {
3784
+ text += ` | Subscribe on signup: ${nl.subscribe_on_signup ? "yes" : "no"}`;
3785
+ }
3786
+ if (nl.member_count != null) {
3787
+ text += ` | Members: ${nl.member_count}`;
3788
+ }
3789
+ text += "\n";
3790
+ }
3791
+ text += `
3792
+ *Use the newsletter ID when sending a post as a newsletter with ghost_send_newsletter.*
3793
+ `;
3794
+ return { content: [{ type: "text", text }] };
3795
+ }
3796
+ );
3797
+ }
3313
3798
  export {
3314
3799
  BuzzPosterClient,
3315
3800
  registerAccountInfoTool,
@@ -3318,15 +3803,15 @@ export {
3318
3803
  registerAudienceTools,
3319
3804
  registerBrandVoiceTools,
3320
3805
  registerCalendarTools,
3806
+ registerGhostTools,
3321
3807
  registerInboxTools,
3322
- registerKnowledgeTools,
3323
3808
  registerMediaTools,
3324
3809
  registerNewsletterAdvancedTools,
3325
3810
  registerNewsletterTemplateTools,
3326
3811
  registerNewsletterTools,
3327
3812
  registerNotificationTools,
3328
3813
  registerPostTools,
3329
- registerPublishingRulesTools,
3330
3814
  registerRssTools,
3331
- registerSourceTools
3815
+ registerSourceTools,
3816
+ registerWordPressTools
3332
3817
  };