@damusix/ghost-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1395 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { attempt } from "@logosdx/utils";
6
+ import { FetchEngine } from "@logosdx/fetch";
7
+ import jwt from "jsonwebtoken";
8
+ //#region src/ghost-client.ts
9
+ const GHOST_URL = process.env.GHOST_URL || "";
10
+ const GHOST_ADMIN_API_KEY = process.env.GHOST_ADMIN_API_KEY || "";
11
+ const GHOST_CONTENT_API_KEY = process.env.GHOST_CONTENT_API_KEY || "";
12
+ const GHOST_API_VERSION = process.env.GHOST_API_VERSION || "v6.0";
13
+ function generateAdminToken() {
14
+ const [id, secret] = GHOST_ADMIN_API_KEY.split(":");
15
+ return jwt.sign({}, Buffer.from(secret, "hex"), {
16
+ keyid: id,
17
+ algorithm: "HS256",
18
+ expiresIn: "5m",
19
+ audience: "/admin/"
20
+ });
21
+ }
22
+ const adminApi = new FetchEngine({
23
+ baseUrl: `${GHOST_URL}/ghost/api/admin`,
24
+ defaultType: "json",
25
+ headers: { "Accept-Version": GHOST_API_VERSION },
26
+ onBeforeReq: async (opts) => {
27
+ if (opts.headers) opts.headers["Authorization"] = `Ghost ${generateAdminToken()}`;
28
+ }
29
+ });
30
+ const contentApi = new FetchEngine({
31
+ baseUrl: `${GHOST_URL}/ghost/api/content`,
32
+ defaultType: "json",
33
+ headers: { "Accept-Version": GHOST_API_VERSION },
34
+ params: { key: GHOST_CONTENT_API_KEY }
35
+ });
36
+ //#endregion
37
+ //#region src/actions/admin/posts.ts
38
+ const browseParams$12 = z.object({
39
+ include: z.string().optional().describe("Comma-separated list of related data to include (e.g. \"authors,tags\")"),
40
+ formats: z.string().optional().describe("Content formats to return: \"html\", \"lexical\", \"plaintext\" (comma-separated)"),
41
+ filter: z.string().optional().describe("NQL filter expression (e.g. \"status:published+tag:news\")"),
42
+ limit: z.union([z.number(), z.literal("all")]).optional().describe("Number of results per page (default: 15, or \"all\")"),
43
+ page: z.number().optional().describe("Page number for pagination"),
44
+ order: z.string().optional().describe("Sort order (e.g. \"published_at DESC\")"),
45
+ fields: z.string().optional().describe("Comma-separated list of fields to return")
46
+ });
47
+ const readParams$13 = z.object({
48
+ id: z.string().describe("Post ID"),
49
+ include: z.string().optional().describe("Comma-separated list of related data to include"),
50
+ formats: z.string().optional().describe("Content formats to return")
51
+ });
52
+ const readBySlugParams$5 = z.object({
53
+ slug: z.string().describe("Post slug"),
54
+ include: z.string().optional().describe("Comma-separated list of related data to include"),
55
+ formats: z.string().optional().describe("Content formats to return")
56
+ });
57
+ const postWriteFields = {
58
+ title: z.string().describe("Post title"),
59
+ lexical: z.string().optional().describe("Post content in Lexical JSON format"),
60
+ status: z.enum([
61
+ "published",
62
+ "draft",
63
+ "scheduled"
64
+ ]).optional().describe("Post status"),
65
+ tags: z.array(z.union([z.object({ id: z.string() }), z.object({ name: z.string() })])).optional().describe("Tags to assign (by id or name)"),
66
+ authors: z.array(z.object({ id: z.string() })).optional().describe("Authors to assign (by id)"),
67
+ featured: z.boolean().optional().describe("Whether the post is featured"),
68
+ visibility: z.string().optional().describe("Post visibility (public, members, paid, tiers)"),
69
+ published_at: z.string().optional().describe("Publication date (ISO 8601 format)"),
70
+ custom_excerpt: z.string().optional().describe("Custom excerpt for the post"),
71
+ meta_title: z.string().optional().describe("SEO meta title"),
72
+ meta_description: z.string().optional().describe("SEO meta description"),
73
+ og_image: z.string().optional().describe("Open Graph image URL"),
74
+ og_title: z.string().optional().describe("Open Graph title"),
75
+ og_description: z.string().optional().describe("Open Graph description"),
76
+ twitter_image: z.string().optional().describe("Twitter card image URL"),
77
+ twitter_title: z.string().optional().describe("Twitter card title"),
78
+ twitter_description: z.string().optional().describe("Twitter card description"),
79
+ codeinjection_head: z.string().optional().describe("Code injection for the post head"),
80
+ codeinjection_foot: z.string().optional().describe("Code injection for the post footer"),
81
+ canonical_url: z.string().optional().describe("Canonical URL for the post"),
82
+ feature_image: z.string().optional().describe("Feature image URL"),
83
+ feature_image_alt: z.string().optional().describe("Feature image alt text"),
84
+ feature_image_caption: z.string().optional().describe("Feature image caption (HTML)"),
85
+ custom_template: z.string().optional().describe("Custom template for the post"),
86
+ newsletter: z.object({ id: z.string() }).optional().describe("Newsletter to send the post to"),
87
+ email_subject: z.string().optional().describe("Custom email subject when sending as newsletter")
88
+ };
89
+ const addSchema$7 = z.object({
90
+ ...postWriteFields,
91
+ title: z.string().describe("Post title (required)")
92
+ });
93
+ const editSchema$7 = z.object({
94
+ id: z.string().describe("Post ID (required)"),
95
+ updated_at: z.string().describe("Last known updated_at value for collision detection (required)"),
96
+ ...postWriteFields,
97
+ title: z.string().optional().describe("Post title")
98
+ });
99
+ const copySchema$1 = z.object({ id: z.string().describe("Post ID to copy") });
100
+ const deleteSchema$3 = z.object({ id: z.string().describe("Post ID to delete") });
101
+ const adminPostActions = [
102
+ {
103
+ name: "posts.browse",
104
+ api: "admin",
105
+ method: "GET",
106
+ path: "/posts/",
107
+ inputSchema: browseParams$12,
108
+ description: "Browse all posts with filtering, pagination, and sorting",
109
+ example: {
110
+ filter: "status:published",
111
+ limit: 10,
112
+ include: "authors,tags"
113
+ }
114
+ },
115
+ {
116
+ name: "posts.read",
117
+ api: "admin",
118
+ method: "GET",
119
+ path: "/posts/{id}/",
120
+ inputSchema: readParams$13,
121
+ description: "Read a single post by ID",
122
+ example: {
123
+ id: "5ddc9141c35e7700383b2937",
124
+ include: "authors,tags",
125
+ formats: "html,lexical"
126
+ }
127
+ },
128
+ {
129
+ name: "posts.read_by_slug",
130
+ api: "admin",
131
+ method: "GET",
132
+ path: "/posts/slug/{slug}/",
133
+ inputSchema: readBySlugParams$5,
134
+ description: "Read a single post by slug",
135
+ example: {
136
+ slug: "my-post",
137
+ include: "authors,tags"
138
+ }
139
+ },
140
+ {
141
+ name: "posts.add",
142
+ api: "admin",
143
+ method: "POST",
144
+ path: "/posts/",
145
+ inputSchema: addSchema$7,
146
+ description: "Create a new post",
147
+ example: {
148
+ title: "My New Post",
149
+ status: "draft",
150
+ tags: [{ name: "News" }]
151
+ }
152
+ },
153
+ {
154
+ name: "posts.edit",
155
+ api: "admin",
156
+ method: "PUT",
157
+ path: "/posts/{id}/",
158
+ inputSchema: editSchema$7,
159
+ description: "Update an existing post. Requires updated_at for collision detection.",
160
+ example: {
161
+ id: "5ddc9141c35e7700383b2937",
162
+ updated_at: "2024-01-01T00:00:00.000Z",
163
+ title: "Updated Title"
164
+ }
165
+ },
166
+ {
167
+ name: "posts.copy",
168
+ api: "admin",
169
+ method: "POST",
170
+ path: "/posts/{id}/copy/",
171
+ inputSchema: copySchema$1,
172
+ description: "Copy an existing post",
173
+ example: { id: "5ddc9141c35e7700383b2937" }
174
+ },
175
+ {
176
+ name: "posts.delete",
177
+ api: "admin",
178
+ method: "DELETE",
179
+ path: "/posts/{id}/",
180
+ inputSchema: deleteSchema$3,
181
+ description: "Delete a post by ID",
182
+ example: { id: "5ddc9141c35e7700383b2937" }
183
+ }
184
+ ];
185
+ //#endregion
186
+ //#region src/actions/admin/pages.ts
187
+ const browseParams$11 = z.object({
188
+ include: z.string().optional().describe("Comma-separated list of related data to include (e.g. \"authors,tags\")"),
189
+ formats: z.string().optional().describe("Content formats to return: \"html\", \"lexical\", \"plaintext\" (comma-separated)"),
190
+ filter: z.string().optional().describe("NQL filter expression"),
191
+ limit: z.union([z.number(), z.literal("all")]).optional().describe("Number of results per page (default: 15, or \"all\")"),
192
+ page: z.number().optional().describe("Page number for pagination"),
193
+ order: z.string().optional().describe("Sort order (e.g. \"published_at DESC\")"),
194
+ fields: z.string().optional().describe("Comma-separated list of fields to return")
195
+ });
196
+ const readParams$12 = z.object({
197
+ id: z.string().describe("Page ID"),
198
+ include: z.string().optional().describe("Comma-separated list of related data to include"),
199
+ formats: z.string().optional().describe("Content formats to return")
200
+ });
201
+ const readBySlugParams$4 = z.object({
202
+ slug: z.string().describe("Page slug"),
203
+ include: z.string().optional().describe("Comma-separated list of related data to include"),
204
+ formats: z.string().optional().describe("Content formats to return")
205
+ });
206
+ const pageWriteFields = {
207
+ title: z.string().describe("Page title"),
208
+ lexical: z.string().optional().describe("Page content in Lexical JSON format"),
209
+ status: z.enum([
210
+ "published",
211
+ "draft",
212
+ "scheduled"
213
+ ]).optional().describe("Page status"),
214
+ tags: z.array(z.union([z.object({ id: z.string() }), z.object({ name: z.string() })])).optional().describe("Tags to assign (by id or name)"),
215
+ authors: z.array(z.object({ id: z.string() })).optional().describe("Authors to assign (by id)"),
216
+ featured: z.boolean().optional().describe("Whether the page is featured"),
217
+ visibility: z.string().optional().describe("Page visibility"),
218
+ published_at: z.string().optional().describe("Publication date (ISO 8601)"),
219
+ custom_excerpt: z.string().optional().describe("Custom excerpt"),
220
+ meta_title: z.string().optional().describe("SEO meta title"),
221
+ meta_description: z.string().optional().describe("SEO meta description"),
222
+ og_image: z.string().optional().describe("Open Graph image URL"),
223
+ og_title: z.string().optional().describe("Open Graph title"),
224
+ og_description: z.string().optional().describe("Open Graph description"),
225
+ twitter_image: z.string().optional().describe("Twitter card image URL"),
226
+ twitter_title: z.string().optional().describe("Twitter card title"),
227
+ twitter_description: z.string().optional().describe("Twitter card description"),
228
+ codeinjection_head: z.string().optional().describe("Code injection for head"),
229
+ codeinjection_foot: z.string().optional().describe("Code injection for footer"),
230
+ canonical_url: z.string().optional().describe("Canonical URL"),
231
+ feature_image: z.string().optional().describe("Feature image URL"),
232
+ feature_image_alt: z.string().optional().describe("Feature image alt text"),
233
+ feature_image_caption: z.string().optional().describe("Feature image caption (HTML)"),
234
+ custom_template: z.string().optional().describe("Custom template")
235
+ };
236
+ const addSchema$6 = z.object({
237
+ ...pageWriteFields,
238
+ title: z.string().describe("Page title (required)")
239
+ });
240
+ const editSchema$6 = z.object({
241
+ id: z.string().describe("Page ID (required)"),
242
+ updated_at: z.string().describe("Last known updated_at for collision detection (required)"),
243
+ ...pageWriteFields,
244
+ title: z.string().optional().describe("Page title")
245
+ });
246
+ const copySchema = z.object({ id: z.string().describe("Page ID to copy") });
247
+ const deleteSchema$2 = z.object({ id: z.string().describe("Page ID to delete") });
248
+ const adminPageActions = [
249
+ {
250
+ name: "pages.browse",
251
+ api: "admin",
252
+ method: "GET",
253
+ path: "/pages/",
254
+ inputSchema: browseParams$11,
255
+ description: "Browse all pages with filtering, pagination, and sorting",
256
+ example: {
257
+ filter: "status:published",
258
+ limit: 10
259
+ }
260
+ },
261
+ {
262
+ name: "pages.read",
263
+ api: "admin",
264
+ method: "GET",
265
+ path: "/pages/{id}/",
266
+ inputSchema: readParams$12,
267
+ description: "Read a single page by ID"
268
+ },
269
+ {
270
+ name: "pages.read_by_slug",
271
+ api: "admin",
272
+ method: "GET",
273
+ path: "/pages/slug/{slug}/",
274
+ inputSchema: readBySlugParams$4,
275
+ description: "Read a single page by slug"
276
+ },
277
+ {
278
+ name: "pages.add",
279
+ api: "admin",
280
+ method: "POST",
281
+ path: "/pages/",
282
+ inputSchema: addSchema$6,
283
+ description: "Create a new page",
284
+ example: {
285
+ title: "About Us",
286
+ status: "draft"
287
+ }
288
+ },
289
+ {
290
+ name: "pages.edit",
291
+ api: "admin",
292
+ method: "PUT",
293
+ path: "/pages/{id}/",
294
+ inputSchema: editSchema$6,
295
+ description: "Update an existing page. Requires updated_at for collision detection."
296
+ },
297
+ {
298
+ name: "pages.copy",
299
+ api: "admin",
300
+ method: "POST",
301
+ path: "/pages/{id}/copy/",
302
+ inputSchema: copySchema,
303
+ description: "Copy an existing page"
304
+ },
305
+ {
306
+ name: "pages.delete",
307
+ api: "admin",
308
+ method: "DELETE",
309
+ path: "/pages/{id}/",
310
+ inputSchema: deleteSchema$2,
311
+ description: "Delete a page by ID"
312
+ }
313
+ ];
314
+ //#endregion
315
+ //#region src/actions/admin/tags.ts
316
+ const browseParams$10 = z.object({
317
+ filter: z.string().optional().describe("NQL filter expression"),
318
+ include: z.string().optional().describe("Related data to include (e.g. \"count.posts\")"),
319
+ limit: z.union([z.number(), z.literal("all")]).optional().describe("Number of results per page"),
320
+ page: z.number().optional().describe("Page number"),
321
+ order: z.string().optional().describe("Sort order")
322
+ });
323
+ const readParams$11 = z.object({
324
+ id: z.string().describe("Tag ID"),
325
+ include: z.string().optional().describe("Related data to include")
326
+ });
327
+ const tagWriteFields = {
328
+ name: z.string().describe("Tag name"),
329
+ slug: z.string().optional().describe("Tag slug (auto-generated from name if omitted)"),
330
+ description: z.string().optional().describe("Tag description"),
331
+ feature_image: z.string().optional().describe("Feature image URL"),
332
+ visibility: z.string().optional().describe("Tag visibility (public or internal)"),
333
+ meta_title: z.string().optional().describe("SEO meta title"),
334
+ meta_description: z.string().optional().describe("SEO meta description"),
335
+ og_image: z.string().optional().describe("Open Graph image URL"),
336
+ og_title: z.string().optional().describe("Open Graph title"),
337
+ og_description: z.string().optional().describe("Open Graph description"),
338
+ twitter_image: z.string().optional().describe("Twitter card image URL"),
339
+ twitter_title: z.string().optional().describe("Twitter card title"),
340
+ twitter_description: z.string().optional().describe("Twitter card description"),
341
+ codeinjection_head: z.string().optional().describe("Code injection for head"),
342
+ codeinjection_foot: z.string().optional().describe("Code injection for footer"),
343
+ canonical_url: z.string().optional().describe("Canonical URL"),
344
+ accent_color: z.string().optional().describe("Accent color hex code")
345
+ };
346
+ const addSchema$5 = z.object({
347
+ ...tagWriteFields,
348
+ name: z.string().describe("Tag name (required)")
349
+ });
350
+ const editSchema$5 = z.object({
351
+ id: z.string().describe("Tag ID (required)"),
352
+ updated_at: z.string().describe("Last known updated_at for collision detection (required)"),
353
+ ...tagWriteFields,
354
+ name: z.string().optional().describe("Tag name")
355
+ });
356
+ const deleteSchema$1 = z.object({ id: z.string().describe("Tag ID to delete") });
357
+ const adminTagActions = [
358
+ {
359
+ name: "tags.browse",
360
+ api: "admin",
361
+ method: "GET",
362
+ path: "/tags/",
363
+ inputSchema: browseParams$10,
364
+ description: "Browse all tags with filtering and pagination",
365
+ example: {
366
+ limit: "all",
367
+ include: "count.posts"
368
+ }
369
+ },
370
+ {
371
+ name: "tags.read",
372
+ api: "admin",
373
+ method: "GET",
374
+ path: "/tags/{id}/",
375
+ inputSchema: readParams$11,
376
+ description: "Read a single tag by ID"
377
+ },
378
+ {
379
+ name: "tags.add",
380
+ api: "admin",
381
+ method: "POST",
382
+ path: "/tags/",
383
+ inputSchema: addSchema$5,
384
+ description: "Create a new tag",
385
+ example: {
386
+ name: "News",
387
+ description: "Latest news articles"
388
+ }
389
+ },
390
+ {
391
+ name: "tags.edit",
392
+ api: "admin",
393
+ method: "PUT",
394
+ path: "/tags/{id}/",
395
+ inputSchema: editSchema$5,
396
+ description: "Update an existing tag"
397
+ },
398
+ {
399
+ name: "tags.delete",
400
+ api: "admin",
401
+ method: "DELETE",
402
+ path: "/tags/{id}/",
403
+ inputSchema: deleteSchema$1,
404
+ description: "Delete a tag by ID"
405
+ }
406
+ ];
407
+ //#endregion
408
+ //#region src/actions/admin/tiers.ts
409
+ const browseParams$9 = z.object({
410
+ filter: z.string().optional().describe("NQL filter expression (e.g. \"type:paid+active:true\")"),
411
+ include: z.string().optional().describe("Related data to include (e.g. \"monthly_price,yearly_price,benefits\")"),
412
+ limit: z.union([z.number(), z.literal("all")]).optional().describe("Number of results per page"),
413
+ page: z.number().optional().describe("Page number"),
414
+ order: z.string().optional().describe("Sort order")
415
+ });
416
+ const readParams$10 = z.object({
417
+ id: z.string().describe("Tier ID"),
418
+ include: z.string().optional().describe("Related data to include")
419
+ });
420
+ const tierWriteFields = {
421
+ name: z.string().describe("Tier name"),
422
+ description: z.string().optional().describe("Tier description"),
423
+ welcome_page_url: z.string().optional().describe("URL of the welcome page for new subscribers"),
424
+ visibility: z.enum(["public", "none"]).optional().describe("Tier visibility"),
425
+ monthly_price: z.number().optional().describe("Monthly price in smallest currency unit (e.g. cents)"),
426
+ yearly_price: z.number().optional().describe("Yearly price in smallest currency unit"),
427
+ currency: z.string().optional().describe("Three-letter ISO currency code (e.g. \"usd\")"),
428
+ benefits: z.array(z.string()).optional().describe("List of benefits for this tier")
429
+ };
430
+ const addSchema$4 = z.object({
431
+ ...tierWriteFields,
432
+ name: z.string().describe("Tier name (required)")
433
+ });
434
+ const editSchema$4 = z.object({
435
+ id: z.string().describe("Tier ID (required)"),
436
+ ...tierWriteFields,
437
+ name: z.string().optional().describe("Tier name")
438
+ });
439
+ const adminTierActions = [
440
+ {
441
+ name: "tiers.browse",
442
+ api: "admin",
443
+ method: "GET",
444
+ path: "/tiers/",
445
+ inputSchema: browseParams$9,
446
+ description: "Browse all tiers with filtering and pagination",
447
+ example: {
448
+ filter: "type:paid+active:true",
449
+ include: "monthly_price,yearly_price,benefits"
450
+ }
451
+ },
452
+ {
453
+ name: "tiers.read",
454
+ api: "admin",
455
+ method: "GET",
456
+ path: "/tiers/{id}/",
457
+ inputSchema: readParams$10,
458
+ description: "Read a single tier by ID"
459
+ },
460
+ {
461
+ name: "tiers.add",
462
+ api: "admin",
463
+ method: "POST",
464
+ path: "/tiers/",
465
+ inputSchema: addSchema$4,
466
+ description: "Create a new tier",
467
+ example: {
468
+ name: "Premium",
469
+ monthly_price: 500,
470
+ yearly_price: 5e3,
471
+ currency: "usd"
472
+ }
473
+ },
474
+ {
475
+ name: "tiers.edit",
476
+ api: "admin",
477
+ method: "PUT",
478
+ path: "/tiers/{id}/",
479
+ inputSchema: editSchema$4,
480
+ description: "Update an existing tier"
481
+ }
482
+ ];
483
+ //#endregion
484
+ //#region src/actions/admin/newsletters.ts
485
+ const browseParams$8 = z.object({
486
+ limit: z.union([z.number(), z.literal("all")]).optional().describe("Number of results per page"),
487
+ page: z.number().optional().describe("Page number"),
488
+ order: z.string().optional().describe("Sort order")
489
+ });
490
+ const readParams$9 = z.object({ id: z.string().describe("Newsletter ID") });
491
+ const newsletterWriteFields = {
492
+ name: z.string().describe("Newsletter name"),
493
+ description: z.string().optional().describe("Newsletter description"),
494
+ slug: z.string().optional().describe("Newsletter slug"),
495
+ sender_name: z.string().optional().describe("Sender name in emails"),
496
+ sender_email: z.string().optional().describe("Sender email address (must be validated)"),
497
+ sender_reply_to: z.enum(["newsletter", "support"]).optional().describe("Reply-to address type"),
498
+ status: z.enum(["active", "archived"]).optional().describe("Newsletter status"),
499
+ visibility: z.string().optional().describe("Newsletter visibility"),
500
+ subscribe_on_signup: z.boolean().optional().describe("Auto-subscribe new members"),
501
+ sort_order: z.number().optional().describe("Sort order position"),
502
+ header_image: z.string().optional().describe("Header image URL"),
503
+ show_header_icon: z.boolean().optional().describe("Show site icon in header"),
504
+ show_header_title: z.boolean().optional().describe("Show site title in header"),
505
+ show_header_name: z.boolean().optional().describe("Show newsletter name in header"),
506
+ title_font_category: z.enum(["serif", "sans_serif"]).optional().describe("Title font category"),
507
+ title_alignment: z.enum(["left", "center"]).optional().describe("Title alignment"),
508
+ show_feature_image: z.boolean().optional().describe("Show feature image in emails"),
509
+ body_font_category: z.enum(["serif", "sans_serif"]).optional().describe("Body font category"),
510
+ footer_content: z.string().optional().describe("Footer content (HTML)"),
511
+ show_badge: z.boolean().optional().describe("Show Ghost badge in footer")
512
+ };
513
+ const addSchema$3 = z.object({
514
+ ...newsletterWriteFields,
515
+ name: z.string().describe("Newsletter name (required)")
516
+ });
517
+ const editSchema$3 = z.object({
518
+ id: z.string().describe("Newsletter ID (required)"),
519
+ ...newsletterWriteFields,
520
+ name: z.string().optional().describe("Newsletter name")
521
+ });
522
+ const adminNewsletterActions = [
523
+ {
524
+ name: "newsletters.browse",
525
+ api: "admin",
526
+ method: "GET",
527
+ path: "/newsletters/",
528
+ inputSchema: browseParams$8,
529
+ description: "Browse all newsletters",
530
+ example: { limit: "all" }
531
+ },
532
+ {
533
+ name: "newsletters.read",
534
+ api: "admin",
535
+ method: "GET",
536
+ path: "/newsletters/{id}/",
537
+ inputSchema: readParams$9,
538
+ description: "Read a single newsletter by ID"
539
+ },
540
+ {
541
+ name: "newsletters.add",
542
+ api: "admin",
543
+ method: "POST",
544
+ path: "/newsletters/",
545
+ inputSchema: addSchema$3,
546
+ description: "Create a new newsletter",
547
+ example: {
548
+ name: "Weekly Digest",
549
+ sender_name: "My Blog"
550
+ }
551
+ },
552
+ {
553
+ name: "newsletters.edit",
554
+ api: "admin",
555
+ method: "PUT",
556
+ path: "/newsletters/{id}/",
557
+ inputSchema: editSchema$3,
558
+ description: "Update an existing newsletter"
559
+ }
560
+ ];
561
+ //#endregion
562
+ //#region src/actions/admin/offers.ts
563
+ const browseParams$7 = z.object({}).describe("No parameters required");
564
+ const readParams$8 = z.object({ id: z.string().describe("Offer ID") });
565
+ const addSchema$2 = z.object({
566
+ name: z.string().describe("Internal name for the offer (required)"),
567
+ code: z.string().describe("Unique code for the offer URL (required)"),
568
+ display_title: z.string().describe("Title shown to users (required)"),
569
+ display_description: z.string().describe("Description shown to users (required)"),
570
+ type: z.enum(["percent", "fixed"]).describe("Discount type: percentage or fixed amount (required)"),
571
+ cadence: z.enum(["month", "year"]).describe("Billing cadence the offer applies to (required)"),
572
+ amount: z.number().describe("Discount amount: percentage (1-100) or fixed amount in cents (required)"),
573
+ duration: z.enum([
574
+ "once",
575
+ "forever",
576
+ "repeating"
577
+ ]).describe("How long the discount lasts (required)"),
578
+ duration_in_months: z.number().optional().describe("Number of months for repeating duration"),
579
+ currency_restriction: z.boolean().optional().describe("Whether the offer is restricted to a specific currency"),
580
+ currency: z.string().optional().describe("Three-letter ISO currency code (required for fixed type)"),
581
+ tier: z.object({ id: z.string().describe("Tier ID") }).describe("Tier this offer applies to (required)")
582
+ });
583
+ const editSchema$2 = z.object({
584
+ id: z.string().describe("Offer ID (required)"),
585
+ name: z.string().optional().describe("Internal name"),
586
+ code: z.string().optional().describe("Unique code for the offer URL"),
587
+ display_title: z.string().optional().describe("Title shown to users"),
588
+ display_description: z.string().optional().describe("Description shown to users")
589
+ });
590
+ const adminOfferActions = [
591
+ {
592
+ name: "offers.browse",
593
+ api: "admin",
594
+ method: "GET",
595
+ path: "/offers/",
596
+ inputSchema: browseParams$7,
597
+ description: "Browse all offers"
598
+ },
599
+ {
600
+ name: "offers.read",
601
+ api: "admin",
602
+ method: "GET",
603
+ path: "/offers/{id}/",
604
+ inputSchema: readParams$8,
605
+ description: "Read a single offer by ID"
606
+ },
607
+ {
608
+ name: "offers.add",
609
+ api: "admin",
610
+ method: "POST",
611
+ path: "/offers/",
612
+ inputSchema: addSchema$2,
613
+ description: "Create a new offer (discount code)",
614
+ example: {
615
+ name: "Black Friday",
616
+ code: "black-friday",
617
+ display_title: "20% Off",
618
+ display_description: "Black Friday special",
619
+ type: "percent",
620
+ cadence: "year",
621
+ amount: 20,
622
+ duration: "once",
623
+ tier: { id: "tier-id" }
624
+ }
625
+ },
626
+ {
627
+ name: "offers.edit",
628
+ api: "admin",
629
+ method: "PUT",
630
+ path: "/offers/{id}/",
631
+ inputSchema: editSchema$2,
632
+ description: "Update an existing offer (limited fields)"
633
+ }
634
+ ];
635
+ //#endregion
636
+ //#region src/actions/admin/members.ts
637
+ const browseParams$6 = z.object({
638
+ include: z.string().optional().describe("Related data to include (e.g. \"newsletters,labels\")"),
639
+ filter: z.string().optional().describe("NQL filter expression (e.g. \"status:paid\")"),
640
+ limit: z.union([z.number(), z.literal("all")]).optional().describe("Number of results per page"),
641
+ page: z.number().optional().describe("Page number"),
642
+ order: z.string().optional().describe("Sort order")
643
+ });
644
+ const readParams$7 = z.object({
645
+ id: z.string().describe("Member ID"),
646
+ include: z.string().optional().describe("Related data to include")
647
+ });
648
+ const memberWriteFields = {
649
+ email: z.string().describe("Member email address"),
650
+ name: z.string().optional().describe("Member name"),
651
+ note: z.string().optional().describe("Private note about the member"),
652
+ labels: z.array(z.union([z.object({ id: z.string() }), z.object({ name: z.string() })])).optional().describe("Labels to assign"),
653
+ newsletters: z.array(z.object({ id: z.string() })).optional().describe("Newsletters to subscribe the member to"),
654
+ comped: z.boolean().optional().describe("Whether the member has a complimentary subscription")
655
+ };
656
+ const addSchema$1 = z.object({
657
+ ...memberWriteFields,
658
+ email: z.string().describe("Member email address (required)")
659
+ });
660
+ const editSchema$1 = z.object({
661
+ id: z.string().describe("Member ID (required)"),
662
+ ...memberWriteFields,
663
+ email: z.string().optional().describe("Member email address")
664
+ });
665
+ const adminMemberActions = [
666
+ {
667
+ name: "members.browse",
668
+ api: "admin",
669
+ method: "GET",
670
+ path: "/members/",
671
+ inputSchema: browseParams$6,
672
+ description: "Browse all members with filtering and pagination",
673
+ example: {
674
+ filter: "status:paid",
675
+ include: "newsletters",
676
+ limit: 20
677
+ }
678
+ },
679
+ {
680
+ name: "members.read",
681
+ api: "admin",
682
+ method: "GET",
683
+ path: "/members/{id}/",
684
+ inputSchema: readParams$7,
685
+ description: "Read a single member by ID"
686
+ },
687
+ {
688
+ name: "members.add",
689
+ api: "admin",
690
+ method: "POST",
691
+ path: "/members/",
692
+ inputSchema: addSchema$1,
693
+ description: "Create a new member",
694
+ example: {
695
+ email: "member@example.com",
696
+ name: "New Member",
697
+ labels: [{ name: "VIP" }]
698
+ }
699
+ },
700
+ {
701
+ name: "members.edit",
702
+ api: "admin",
703
+ method: "PUT",
704
+ path: "/members/{id}/",
705
+ inputSchema: editSchema$1,
706
+ description: "Update an existing member"
707
+ }
708
+ ];
709
+ //#endregion
710
+ //#region src/actions/admin/users.ts
711
+ const browseParams$5 = z.object({
712
+ include: z.string().optional().describe("Related data to include (e.g. \"roles,count.posts\")"),
713
+ limit: z.union([z.number(), z.literal("all")]).optional().describe("Number of results per page"),
714
+ page: z.number().optional().describe("Page number"),
715
+ order: z.string().optional().describe("Sort order")
716
+ });
717
+ const readParams$6 = z.object({
718
+ id: z.string().describe("User ID"),
719
+ include: z.string().optional().describe("Related data to include")
720
+ });
721
+ const adminUserActions = [{
722
+ name: "users.browse",
723
+ api: "admin",
724
+ method: "GET",
725
+ path: "/users/",
726
+ inputSchema: browseParams$5,
727
+ description: "Browse all users (staff members)",
728
+ example: {
729
+ include: "roles",
730
+ limit: "all"
731
+ }
732
+ }, {
733
+ name: "users.read",
734
+ api: "admin",
735
+ method: "GET",
736
+ path: "/users/{id}/",
737
+ inputSchema: readParams$6,
738
+ description: "Read a single user by ID"
739
+ }];
740
+ const adminImageActions = [{
741
+ name: "images.upload",
742
+ api: "admin",
743
+ method: "POST",
744
+ path: "/images/upload/",
745
+ inputSchema: z.object({
746
+ file: z.string().describe("Image URL to download and upload, or base64-encoded image data"),
747
+ ref: z.string().optional().describe("Optional reference name for the uploaded image")
748
+ }),
749
+ description: "Upload an image to Ghost. Provide a URL (which will be downloaded) or base64-encoded image data.",
750
+ example: {
751
+ file: "https://example.com/photo.jpg",
752
+ ref: "hero-image"
753
+ }
754
+ }];
755
+ //#endregion
756
+ //#region src/actions/admin/themes.ts
757
+ const uploadSchema = z.object({ file: z.string().describe("URL to a theme ZIP file to download and upload") });
758
+ const activateSchema = z.object({ name: z.string().describe("Theme name to activate") });
759
+ const adminThemeActions = [{
760
+ name: "themes.upload",
761
+ api: "admin",
762
+ method: "POST",
763
+ path: "/themes/upload/",
764
+ inputSchema: uploadSchema,
765
+ description: "Upload a theme ZIP file to Ghost. Provide a URL to the ZIP file.",
766
+ example: { file: "https://example.com/theme.zip" }
767
+ }, {
768
+ name: "themes.activate",
769
+ api: "admin",
770
+ method: "PUT",
771
+ path: "/themes/{name}/activate/",
772
+ inputSchema: activateSchema,
773
+ description: "Activate an installed theme by name",
774
+ example: { name: "casper" }
775
+ }];
776
+ //#endregion
777
+ //#region src/actions/admin/webhooks.ts
778
+ const webhookEvents = z.enum([
779
+ "site.changed",
780
+ "post.added",
781
+ "post.deleted",
782
+ "post.edited",
783
+ "post.published",
784
+ "post.published.edited",
785
+ "post.unpublished",
786
+ "post.scheduled",
787
+ "post.unscheduled",
788
+ "post.rescheduled",
789
+ "page.added",
790
+ "page.deleted",
791
+ "page.edited",
792
+ "page.published",
793
+ "page.published.edited",
794
+ "page.unpublished",
795
+ "page.scheduled",
796
+ "page.unscheduled",
797
+ "page.rescheduled",
798
+ "tag.added",
799
+ "tag.edited",
800
+ "tag.deleted",
801
+ "post.tag.attached",
802
+ "post.tag.detached",
803
+ "page.tag.attached",
804
+ "page.tag.detached",
805
+ "member.added",
806
+ "member.edited",
807
+ "member.deleted"
808
+ ]);
809
+ const addSchema = z.object({
810
+ event: webhookEvents.describe("Webhook event to listen for (required)"),
811
+ target_url: z.string().describe("URL to receive webhook POST requests (required)"),
812
+ name: z.string().optional().describe("Human-readable name for the webhook"),
813
+ secret: z.string().optional().describe("Shared secret for HMAC signature verification"),
814
+ api_version: z.string().optional().describe("Target API version"),
815
+ integration_id: z.string().optional().describe("Associated integration ID")
816
+ });
817
+ const editSchema = z.object({
818
+ id: z.string().describe("Webhook ID (required)"),
819
+ event: webhookEvents.optional().describe("Webhook event"),
820
+ target_url: z.string().optional().describe("URL to receive webhook POST requests"),
821
+ name: z.string().optional().describe("Human-readable name"),
822
+ secret: z.string().optional().describe("Shared secret for HMAC verification"),
823
+ api_version: z.string().optional().describe("Target API version"),
824
+ integration_id: z.string().optional().describe("Associated integration ID")
825
+ });
826
+ const deleteSchema = z.object({ id: z.string().describe("Webhook ID to delete") });
827
+ const adminWebhookActions = [
828
+ {
829
+ name: "webhooks.add",
830
+ api: "admin",
831
+ method: "POST",
832
+ path: "/webhooks/",
833
+ inputSchema: addSchema,
834
+ description: "Create a new webhook",
835
+ example: {
836
+ event: "post.published",
837
+ target_url: "https://example.com/webhook",
838
+ name: "Post published"
839
+ }
840
+ },
841
+ {
842
+ name: "webhooks.edit",
843
+ api: "admin",
844
+ method: "PUT",
845
+ path: "/webhooks/{id}/",
846
+ inputSchema: editSchema,
847
+ description: "Update an existing webhook"
848
+ },
849
+ {
850
+ name: "webhooks.delete",
851
+ api: "admin",
852
+ method: "DELETE",
853
+ path: "/webhooks/{id}/",
854
+ inputSchema: deleteSchema,
855
+ description: "Delete a webhook by ID"
856
+ }
857
+ ];
858
+ const adminSiteActions = [{
859
+ name: "site.read",
860
+ api: "admin",
861
+ method: "GET",
862
+ path: "/site/",
863
+ inputSchema: z.object({}).describe("No parameters required"),
864
+ description: "Read site configuration and metadata"
865
+ }];
866
+ //#endregion
867
+ //#region src/actions/content/posts.ts
868
+ const browseParams$4 = z.object({
869
+ include: z.string().optional().describe("Comma-separated list of related data (e.g. \"authors,tags\")"),
870
+ formats: z.string().optional().describe("Content formats: \"html\", \"plaintext\" (comma-separated)"),
871
+ filter: z.string().optional().describe("NQL filter expression (e.g. \"tag:news+featured:true\")"),
872
+ limit: z.union([z.number(), z.literal("all")]).optional().describe("Number of results per page (default: 15, max: 100, or \"all\")"),
873
+ page: z.number().optional().describe("Page number for pagination"),
874
+ order: z.string().optional().describe("Sort order (e.g. \"published_at DESC\")"),
875
+ fields: z.string().optional().describe("Comma-separated list of fields to return")
876
+ });
877
+ const readParams$4 = z.object({
878
+ id: z.string().describe("Post ID"),
879
+ include: z.string().optional().describe("Related data to include"),
880
+ formats: z.string().optional().describe("Content formats to return")
881
+ });
882
+ const readBySlugParams$3 = z.object({
883
+ slug: z.string().describe("Post slug"),
884
+ include: z.string().optional().describe("Related data to include"),
885
+ formats: z.string().optional().describe("Content formats to return")
886
+ });
887
+ const contentPostActions = [
888
+ {
889
+ name: "posts.browse",
890
+ api: "content",
891
+ method: "GET",
892
+ path: "/posts/",
893
+ inputSchema: browseParams$4,
894
+ description: "Browse published posts (Content API — read-only)",
895
+ example: {
896
+ filter: "tag:news",
897
+ include: "authors,tags",
898
+ limit: 10
899
+ }
900
+ },
901
+ {
902
+ name: "posts.read",
903
+ api: "content",
904
+ method: "GET",
905
+ path: "/posts/{id}/",
906
+ inputSchema: readParams$4,
907
+ description: "Read a published post by ID (Content API)"
908
+ },
909
+ {
910
+ name: "posts.read_by_slug",
911
+ api: "content",
912
+ method: "GET",
913
+ path: "/posts/slug/{slug}/",
914
+ inputSchema: readBySlugParams$3,
915
+ description: "Read a published post by slug (Content API)"
916
+ }
917
+ ];
918
+ //#endregion
919
+ //#region src/actions/content/pages.ts
920
+ const browseParams$3 = z.object({
921
+ include: z.string().optional().describe("Comma-separated list of related data (e.g. \"authors,tags\")"),
922
+ formats: z.string().optional().describe("Content formats: \"html\", \"plaintext\" (comma-separated)"),
923
+ filter: z.string().optional().describe("NQL filter expression"),
924
+ limit: z.union([z.number(), z.literal("all")]).optional().describe("Number of results per page"),
925
+ page: z.number().optional().describe("Page number"),
926
+ order: z.string().optional().describe("Sort order"),
927
+ fields: z.string().optional().describe("Comma-separated list of fields to return")
928
+ });
929
+ const readParams$3 = z.object({
930
+ id: z.string().describe("Page ID"),
931
+ include: z.string().optional().describe("Related data to include"),
932
+ formats: z.string().optional().describe("Content formats to return")
933
+ });
934
+ const readBySlugParams$2 = z.object({
935
+ slug: z.string().describe("Page slug"),
936
+ include: z.string().optional().describe("Related data to include"),
937
+ formats: z.string().optional().describe("Content formats to return")
938
+ });
939
+ const contentPageActions = [
940
+ {
941
+ name: "pages.browse",
942
+ api: "content",
943
+ method: "GET",
944
+ path: "/pages/",
945
+ inputSchema: browseParams$3,
946
+ description: "Browse published pages (Content API — read-only)"
947
+ },
948
+ {
949
+ name: "pages.read",
950
+ api: "content",
951
+ method: "GET",
952
+ path: "/pages/{id}/",
953
+ inputSchema: readParams$3,
954
+ description: "Read a published page by ID (Content API)"
955
+ },
956
+ {
957
+ name: "pages.read_by_slug",
958
+ api: "content",
959
+ method: "GET",
960
+ path: "/pages/slug/{slug}/",
961
+ inputSchema: readBySlugParams$2,
962
+ description: "Read a published page by slug (Content API)"
963
+ }
964
+ ];
965
+ //#endregion
966
+ //#region src/actions/content/tags.ts
967
+ const browseParams$2 = z.object({
968
+ include: z.string().optional().describe("Related data to include (e.g. \"count.posts\")"),
969
+ filter: z.string().optional().describe("NQL filter expression"),
970
+ limit: z.union([z.number(), z.literal("all")]).optional().describe("Number of results per page"),
971
+ page: z.number().optional().describe("Page number"),
972
+ order: z.string().optional().describe("Sort order"),
973
+ fields: z.string().optional().describe("Comma-separated list of fields to return")
974
+ });
975
+ const readParams$2 = z.object({
976
+ id: z.string().describe("Tag ID"),
977
+ include: z.string().optional().describe("Related data to include")
978
+ });
979
+ const readBySlugParams$1 = z.object({
980
+ slug: z.string().describe("Tag slug"),
981
+ include: z.string().optional().describe("Related data to include")
982
+ });
983
+ const contentTagActions = [
984
+ {
985
+ name: "tags.browse",
986
+ api: "content",
987
+ method: "GET",
988
+ path: "/tags/",
989
+ inputSchema: browseParams$2,
990
+ description: "Browse all tags (Content API — read-only)",
991
+ example: {
992
+ include: "count.posts",
993
+ limit: "all"
994
+ }
995
+ },
996
+ {
997
+ name: "tags.read",
998
+ api: "content",
999
+ method: "GET",
1000
+ path: "/tags/{id}/",
1001
+ inputSchema: readParams$2,
1002
+ description: "Read a tag by ID (Content API)"
1003
+ },
1004
+ {
1005
+ name: "tags.read_by_slug",
1006
+ api: "content",
1007
+ method: "GET",
1008
+ path: "/tags/slug/{slug}/",
1009
+ inputSchema: readBySlugParams$1,
1010
+ description: "Read a tag by slug (Content API)"
1011
+ }
1012
+ ];
1013
+ //#endregion
1014
+ //#region src/actions/content/authors.ts
1015
+ const browseParams$1 = z.object({
1016
+ include: z.string().optional().describe("Related data to include (e.g. \"count.posts\")"),
1017
+ filter: z.string().optional().describe("NQL filter expression"),
1018
+ limit: z.union([z.number(), z.literal("all")]).optional().describe("Number of results per page"),
1019
+ page: z.number().optional().describe("Page number"),
1020
+ order: z.string().optional().describe("Sort order"),
1021
+ fields: z.string().optional().describe("Comma-separated list of fields to return")
1022
+ });
1023
+ const readParams$1 = z.object({
1024
+ id: z.string().describe("Author ID"),
1025
+ include: z.string().optional().describe("Related data to include")
1026
+ });
1027
+ const readBySlugParams = z.object({
1028
+ slug: z.string().describe("Author slug"),
1029
+ include: z.string().optional().describe("Related data to include")
1030
+ });
1031
+ const contentAuthorActions = [
1032
+ {
1033
+ name: "authors.browse",
1034
+ api: "content",
1035
+ method: "GET",
1036
+ path: "/authors/",
1037
+ inputSchema: browseParams$1,
1038
+ description: "Browse all authors (Content API — read-only)",
1039
+ example: { include: "count.posts" }
1040
+ },
1041
+ {
1042
+ name: "authors.read",
1043
+ api: "content",
1044
+ method: "GET",
1045
+ path: "/authors/{id}/",
1046
+ inputSchema: readParams$1,
1047
+ description: "Read an author by ID (Content API)"
1048
+ },
1049
+ {
1050
+ name: "authors.read_by_slug",
1051
+ api: "content",
1052
+ method: "GET",
1053
+ path: "/authors/slug/{slug}/",
1054
+ inputSchema: readBySlugParams,
1055
+ description: "Read an author by slug (Content API)"
1056
+ }
1057
+ ];
1058
+ const contentTierActions = [{
1059
+ name: "tiers.browse",
1060
+ api: "content",
1061
+ method: "GET",
1062
+ path: "/tiers/",
1063
+ inputSchema: z.object({
1064
+ include: z.string().optional().describe("Related data to include (e.g. \"monthly_price,yearly_price,benefits\")"),
1065
+ filter: z.string().optional().describe("NQL filter expression (e.g. \"type:paid+active:true+visibility:public\")"),
1066
+ limit: z.union([z.number(), z.literal("all")]).optional().describe("Number of results per page"),
1067
+ page: z.number().optional().describe("Page number")
1068
+ }),
1069
+ description: "Browse all tiers (Content API — read-only)",
1070
+ example: { include: "monthly_price,yearly_price,benefits" }
1071
+ }];
1072
+ const contentSettingsActions = [{
1073
+ name: "settings.read",
1074
+ api: "content",
1075
+ method: "GET",
1076
+ path: "/settings/",
1077
+ inputSchema: z.object({}).describe("No parameters required"),
1078
+ description: "Read site settings (Content API — read-only)"
1079
+ }];
1080
+ //#endregion
1081
+ //#region src/actions/registry.ts
1082
+ const registry = /* @__PURE__ */ new Map();
1083
+ function registerAction(action) {
1084
+ registry.set(`${action.api}:${action.name}`, action);
1085
+ }
1086
+ function registerActions(actions) {
1087
+ for (const action of actions) registerAction(action);
1088
+ }
1089
+ function getAction(name, api) {
1090
+ if (api) return registry.get(`${api}:${name}`);
1091
+ return registry.get(`admin:${name}`) || registry.get(`content:${name}`);
1092
+ }
1093
+ function listActions(api) {
1094
+ const actions = Array.from(registry.values());
1095
+ if (api) return actions.filter((a) => a.api === api);
1096
+ return actions;
1097
+ }
1098
+ function getActionHelp(name, api) {
1099
+ const action = getAction(name, api);
1100
+ if (!action) return;
1101
+ const schema = action.inputSchema;
1102
+ const lines = [
1103
+ `## ${action.name}`,
1104
+ "",
1105
+ action.description,
1106
+ "",
1107
+ `- **API:** ${action.api}`,
1108
+ `- **Method:** ${action.method}`,
1109
+ `- **Path:** ${action.path}`,
1110
+ ""
1111
+ ];
1112
+ if (schema instanceof z.ZodObject) {
1113
+ lines.push("### Parameters", "");
1114
+ const shape = schema.shape;
1115
+ for (const [key, field] of Object.entries(shape)) {
1116
+ const isOptional = field.isOptional();
1117
+ const desc = field.description || "";
1118
+ lines.push(`- **${key}**${isOptional ? " (optional)" : " (required)"}: ${desc}`);
1119
+ }
1120
+ lines.push("");
1121
+ }
1122
+ if (action.example) lines.push("### Example Payload", "", "```json", JSON.stringify(action.example, null, 2), "```", "");
1123
+ return lines.join("\n");
1124
+ }
1125
+ function initRegistry() {
1126
+ registerActions([
1127
+ ...adminPostActions,
1128
+ ...adminPageActions,
1129
+ ...adminTagActions,
1130
+ ...adminTierActions,
1131
+ ...adminNewsletterActions,
1132
+ ...adminOfferActions,
1133
+ ...adminMemberActions,
1134
+ ...adminUserActions,
1135
+ ...adminImageActions,
1136
+ ...adminThemeActions,
1137
+ ...adminWebhookActions,
1138
+ ...adminSiteActions,
1139
+ ...contentPostActions,
1140
+ ...contentPageActions,
1141
+ ...contentTagActions,
1142
+ ...contentAuthorActions,
1143
+ ...contentTierActions,
1144
+ ...contentSettingsActions
1145
+ ]);
1146
+ }
1147
+ initRegistry();
1148
+ //#endregion
1149
+ //#region src/tools/use-ghost-api.ts
1150
+ const useGhostApiSchema = z.object({
1151
+ api: z.enum(["admin", "content"]).describe("Which API to use: \"admin\" for full access, \"content\" for read-only public data"),
1152
+ action: z.string().describe("Action to execute (e.g. \"posts.browse\", \"members.add\")"),
1153
+ payload: z.record(z.unknown()).optional().describe("Action payload — fields depend on the action. Use ghost_api_help to see available fields.")
1154
+ });
1155
+ const PATH_PARAMS = [
1156
+ "id",
1157
+ "slug",
1158
+ "name"
1159
+ ];
1160
+ function buildPath(template, payload) {
1161
+ let path = template;
1162
+ for (const param of PATH_PARAMS) {
1163
+ const placeholder = `{${param}}`;
1164
+ if (path.includes(placeholder) && payload[param]) path = path.replace(placeholder, encodeURIComponent(String(payload[param])));
1165
+ }
1166
+ return path;
1167
+ }
1168
+ function extractQueryParams(payload) {
1169
+ const params = {};
1170
+ for (const [key, value] of Object.entries(payload)) {
1171
+ if (PATH_PARAMS.includes(key)) continue;
1172
+ if (value !== void 0 && value !== null) params[key] = String(value);
1173
+ }
1174
+ return params;
1175
+ }
1176
+ function extractBodyPayload(payload) {
1177
+ const body = {};
1178
+ for (const [key, value] of Object.entries(payload)) {
1179
+ if (PATH_PARAMS.includes(key)) continue;
1180
+ if (key === "updated_at") {
1181
+ body[key] = value;
1182
+ continue;
1183
+ }
1184
+ if (value !== void 0) body[key] = value;
1185
+ }
1186
+ return body;
1187
+ }
1188
+ async function handleUseGhostApi(input, mode) {
1189
+ const { api, action, payload = {} } = input;
1190
+ if (mode === "content" && api === "admin") return JSON.stringify({ error: "Admin API is not available in content mode. Set GHOST_API_MODE=admin and provide GHOST_ADMIN_API_KEY to use admin actions." });
1191
+ const actionDef = getAction(action, api);
1192
+ if (!actionDef) return JSON.stringify({ error: `Unknown action "${action}" for ${api} API. Use ghost_api_help to see available actions.` });
1193
+ const validation = actionDef.inputSchema.safeParse(payload);
1194
+ if (!validation.success) return JSON.stringify({
1195
+ error: "Invalid payload",
1196
+ details: validation.error.issues.map((i) => ({
1197
+ path: i.path.join("."),
1198
+ message: i.message
1199
+ }))
1200
+ });
1201
+ const validPayload = validation.data;
1202
+ const engine = api === "admin" ? adminApi : contentApi;
1203
+ const path = buildPath(actionDef.path, validPayload);
1204
+ if (actionDef.name === "images.upload" || actionDef.name === "themes.upload") return await handleFileUpload(actionDef.name, validPayload, path);
1205
+ if (actionDef.method === "GET") {
1206
+ const queryParams = extractQueryParams(validPayload);
1207
+ const [response, err] = await attempt(async () => engine.get(path, { params: queryParams }));
1208
+ if (err) return JSON.stringify({ error: err.message });
1209
+ return JSON.stringify(response);
1210
+ }
1211
+ if (actionDef.method === "DELETE") {
1212
+ const [response, err] = await attempt(async () => engine.delete(path));
1213
+ if (err) return JSON.stringify({ error: err.message });
1214
+ return JSON.stringify(response ?? { success: true });
1215
+ }
1216
+ const resourceKey = actionDef.name.split(".")[0];
1217
+ const body = extractBodyPayload(validPayload);
1218
+ const wrappedBody = { [resourceKey]: [body] };
1219
+ if (actionDef.method === "POST") {
1220
+ const [response, err] = await attempt(async () => engine.post(path, wrappedBody));
1221
+ if (err) return JSON.stringify({ error: err.message });
1222
+ return JSON.stringify(response);
1223
+ }
1224
+ if (actionDef.method === "PUT") {
1225
+ const [response, err] = await attempt(async () => engine.put(path, wrappedBody));
1226
+ if (err) return JSON.stringify({ error: err.message });
1227
+ return JSON.stringify(response);
1228
+ }
1229
+ return JSON.stringify({ error: `Unsupported method: ${actionDef.method}` });
1230
+ }
1231
+ async function handleFileUpload(actionName, payload, path) {
1232
+ const fileInput = payload.file;
1233
+ const ref = payload.ref;
1234
+ let fileBuffer;
1235
+ let filename;
1236
+ if (fileInput.startsWith("http://") || fileInput.startsWith("https://")) {
1237
+ const [response, err] = await attempt(async () => fetch(fileInput));
1238
+ if (err) return JSON.stringify({ error: `Failed to download file: ${err.message}` });
1239
+ if (!response.ok) return JSON.stringify({ error: `Failed to download file: ${response.status} ${response.statusText}` });
1240
+ fileBuffer = Buffer.from(await response.arrayBuffer());
1241
+ filename = new URL(fileInput).pathname.split("/").pop() || (actionName === "images.upload" ? "image.jpg" : "theme.zip");
1242
+ } else {
1243
+ fileBuffer = Buffer.from(fileInput, "base64");
1244
+ filename = actionName === "images.upload" ? "image.jpg" : "theme.zip";
1245
+ }
1246
+ const formData = new FormData();
1247
+ const blob = new Blob([new Uint8Array(fileBuffer)]);
1248
+ formData.append("file", blob, filename);
1249
+ if (ref) formData.append("ref", ref);
1250
+ const [response, err] = await attempt(async () => adminApi.post(path, formData, { headers: { "Content-Type": void 0 } }));
1251
+ if (err) return JSON.stringify({ error: err.message });
1252
+ return JSON.stringify(response);
1253
+ }
1254
+ //#endregion
1255
+ //#region src/tools/ghost-api-help.ts
1256
+ const ghostApiHelpSchema = z.object({
1257
+ action: z.string().optional().describe("Specific action name to get detailed help for (e.g. \"posts.add\"). Omit to see all available actions."),
1258
+ api: z.enum(["admin", "content"]).optional().describe("Filter actions by API type")
1259
+ });
1260
+ function handleGhostApiHelp(input) {
1261
+ const { action, api } = input;
1262
+ if (action) {
1263
+ const help = getActionHelp(action, api);
1264
+ if (!help) return `Unknown action "${action}". Use ghost_api_help without arguments to see all available actions.`;
1265
+ return help;
1266
+ }
1267
+ const actions = listActions(api);
1268
+ const grouped = {};
1269
+ for (const a of actions) {
1270
+ if (!grouped[a.api]) grouped[a.api] = {};
1271
+ const resource = a.name.split(".")[0];
1272
+ if (!grouped[a.api][resource]) grouped[a.api][resource] = [];
1273
+ grouped[a.api][resource].push(a);
1274
+ }
1275
+ const lines = ["# Ghost API Actions", ""];
1276
+ for (const [apiType, resources] of Object.entries(grouped)) {
1277
+ lines.push(`## ${apiType.charAt(0).toUpperCase() + apiType.slice(1)} API`, "");
1278
+ for (const [resource, resourceActions] of Object.entries(resources)) {
1279
+ lines.push(`### ${resource}`);
1280
+ for (const a of resourceActions) lines.push(`- **${a.name}** — ${a.description}`);
1281
+ lines.push("");
1282
+ }
1283
+ }
1284
+ lines.push("---", "", "Use `ghost_api_help` with `action` parameter for detailed schema info on any action.");
1285
+ return lines.join("\n");
1286
+ }
1287
+ //#endregion
1288
+ //#region src/tools/ghost-docs.ts
1289
+ const ghostDocsSchema = z.object({
1290
+ all: z.boolean().optional().describe("Return the full Ghost documentation (llms.txt)"),
1291
+ search: z.string().optional().describe("Case-insensitive substring search across the documentation"),
1292
+ regex: z.string().optional().describe("Regex pattern string to match (e.g. \"/pattern/i\")")
1293
+ });
1294
+ const GHOST_LLMS_TXT_URL = "https://docs.ghost.org/llms.txt";
1295
+ const CACHE_TTL = 900 * 1e3;
1296
+ let cachedContent = null;
1297
+ let cacheTimestamp = 0;
1298
+ async function fetchDocs() {
1299
+ const now = Date.now();
1300
+ if (cachedContent && now - cacheTimestamp < CACHE_TTL) return cachedContent;
1301
+ const [response, err] = await attempt(async () => fetch(GHOST_LLMS_TXT_URL));
1302
+ if (err) throw new Error(`Failed to fetch Ghost docs: ${err.message}`);
1303
+ if (!response.ok) throw new Error(`Failed to fetch Ghost docs: ${response.status}`);
1304
+ cachedContent = await response.text();
1305
+ cacheTimestamp = now;
1306
+ return cachedContent;
1307
+ }
1308
+ async function handleGhostDocs(input) {
1309
+ const { all, search, regex } = input;
1310
+ if (!all && !search && !regex) return "Provide one of: `all: true` to get full docs, `search` for text search, or `regex` for pattern matching.";
1311
+ let content;
1312
+ try {
1313
+ content = await fetchDocs();
1314
+ } catch (error) {
1315
+ return `Error: ${error.message}`;
1316
+ }
1317
+ if (all) return content;
1318
+ const lines = content.split("\n");
1319
+ const matchedLines = [];
1320
+ if (search) {
1321
+ const searchLower = search.toLowerCase();
1322
+ for (let i = 0; i < lines.length; i++) if (lines[i].toLowerCase().includes(searchLower)) {
1323
+ const start = Math.max(0, i - 1);
1324
+ const end = Math.min(lines.length - 1, i + 1);
1325
+ for (let j = start; j <= end; j++) {
1326
+ const line = `${j + 1}: ${lines[j]}`;
1327
+ if (!matchedLines.includes(line)) matchedLines.push(line);
1328
+ }
1329
+ }
1330
+ }
1331
+ if (regex) {
1332
+ let pattern;
1333
+ const regexMatch = regex.match(/^\/(.+)\/([gimsuy]*)$/);
1334
+ if (regexMatch) pattern = new RegExp(regexMatch[1], regexMatch[2]);
1335
+ else pattern = new RegExp(regex);
1336
+ for (let i = 0; i < lines.length; i++) if (pattern.test(lines[i])) {
1337
+ const start = Math.max(0, i - 1);
1338
+ const end = Math.min(lines.length - 1, i + 1);
1339
+ for (let j = start; j <= end; j++) {
1340
+ const line = `${j + 1}: ${lines[j]}`;
1341
+ if (!matchedLines.includes(line)) matchedLines.push(line);
1342
+ }
1343
+ }
1344
+ }
1345
+ if (matchedLines.length === 0) return "No matches found.";
1346
+ return matchedLines.join("\n");
1347
+ }
1348
+ //#endregion
1349
+ //#region src/index.ts
1350
+ const GHOST_API_MODE = process.env.GHOST_API_MODE || "admin";
1351
+ const server = new McpServer({
1352
+ name: "ghost-mcp",
1353
+ version: "0.1.0"
1354
+ });
1355
+ server.tool("use_ghost_api", "Execute a Ghost API action (browse, read, add, edit, delete posts, pages, tags, members, newsletters, and more)", useGhostApiSchema.shape, async ({ api, action, payload }) => {
1356
+ return { content: [{
1357
+ type: "text",
1358
+ text: await handleUseGhostApi({
1359
+ api,
1360
+ action,
1361
+ payload
1362
+ }, GHOST_API_MODE)
1363
+ }] };
1364
+ });
1365
+ server.tool("ghost_api_help", "Get help on available Ghost API actions — list all actions or get detailed schema info for a specific action", ghostApiHelpSchema.shape, async ({ action, api }) => {
1366
+ return { content: [{
1367
+ type: "text",
1368
+ text: handleGhostApiHelp({
1369
+ action,
1370
+ api
1371
+ })
1372
+ }] };
1373
+ });
1374
+ server.tool("ghost_docs", "Search Ghost CMS documentation — fetch full docs, search by text, or match with regex", ghostDocsSchema.shape, async ({ all, search, regex }) => {
1375
+ return { content: [{
1376
+ type: "text",
1377
+ text: await handleGhostDocs({
1378
+ all,
1379
+ search,
1380
+ regex
1381
+ })
1382
+ }] };
1383
+ });
1384
+ async function main() {
1385
+ const transport = new StdioServerTransport();
1386
+ await server.connect(transport);
1387
+ }
1388
+ main().catch((error) => {
1389
+ console.error("Fatal error:", error);
1390
+ process.exit(1);
1391
+ });
1392
+ //#endregion
1393
+ export {};
1394
+
1395
+ //# sourceMappingURL=index.mjs.map