@jgardner04/ghost-mcp-server 1.11.0 → 1.12.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.
@@ -8,6 +8,31 @@ import fs from 'fs';
8
8
  import path from 'path';
9
9
  import os from 'os';
10
10
  import crypto from 'crypto';
11
+ import { ValidationError } from './errors/index.js';
12
+ import { validateToolInput } from './utils/validation.js';
13
+ import { trackTempFile, cleanupTempFiles } from './utils/tempFileManager.js';
14
+ import {
15
+ createTagSchema,
16
+ updateTagSchema,
17
+ tagQuerySchema,
18
+ ghostIdSchema,
19
+ emailSchema,
20
+ createPostSchema,
21
+ updatePostSchema,
22
+ postQuerySchema,
23
+ createMemberSchema,
24
+ updateMemberSchema,
25
+ memberQuerySchema,
26
+ createTierSchema,
27
+ updateTierSchema,
28
+ tierQuerySchema,
29
+ createNewsletterSchema,
30
+ updateNewsletterSchema,
31
+ newsletterQuerySchema,
32
+ createPageSchema,
33
+ updatePageSchema,
34
+ pageQuerySchema,
35
+ } from './schemas/index.js';
11
36
 
12
37
  // Load environment variables
13
38
  dotenv.config();
@@ -51,26 +76,37 @@ const server = new McpServer({
51
76
 
52
77
  // --- Register Tools ---
53
78
 
79
+ // --- Schema Definitions for Tools ---
80
+ const getTagsSchema = tagQuerySchema.partial();
81
+ const getTagSchema = z.object({
82
+ id: ghostIdSchema.optional().describe('The ID of the tag to retrieve.'),
83
+ slug: z.string().optional().describe('The slug of the tag to retrieve.'),
84
+ include: z.string().optional().describe('Additional resources to include (e.g., "count.posts").'),
85
+ });
86
+ const updateTagInputSchema = updateTagSchema.extend({ id: ghostIdSchema });
87
+ const deleteTagSchema = z.object({ id: ghostIdSchema });
88
+
54
89
  // Get Tags Tool
55
90
  server.tool(
56
91
  'ghost_get_tags',
57
92
  'Retrieves a list of tags from Ghost CMS. Can optionally filter by tag name.',
58
- {
59
- name: z
60
- .string()
61
- .optional()
62
- .describe('Filter tags by exact name. If omitted, all tags are returned.'),
63
- },
64
- async ({ name }) => {
93
+ getTagsSchema,
94
+ async (rawInput) => {
95
+ const validation = validateToolInput(getTagsSchema, rawInput, 'ghost_get_tags');
96
+ if (!validation.success) {
97
+ return validation.errorResponse;
98
+ }
99
+ const input = validation.data;
100
+
65
101
  console.error(`Executing tool: ghost_get_tags`);
66
102
  try {
67
103
  await loadServices();
68
104
  const tags = await ghostService.getTags();
69
105
  let result = tags;
70
106
 
71
- if (name) {
72
- result = tags.filter((tag) => tag.name.toLowerCase() === name.toLowerCase());
73
- console.error(`Filtered tags by name "${name}". Found ${result.length} match(es).`);
107
+ if (input.name) {
108
+ result = tags.filter((tag) => tag.name.toLowerCase() === input.name.toLowerCase());
109
+ console.error(`Filtered tags by name "${input.name}". Found ${result.length} match(es).`);
74
110
  } else {
75
111
  console.error(`Retrieved ${tags.length} tags from Ghost.`);
76
112
  }
@@ -80,6 +116,13 @@ server.tool(
80
116
  };
81
117
  } catch (error) {
82
118
  console.error(`Error in ghost_get_tags:`, error);
119
+ if (error.name === 'ZodError') {
120
+ const validationError = ValidationError.fromZod(error, 'Tags retrieval');
121
+ return {
122
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
123
+ isError: true,
124
+ };
125
+ }
83
126
  return {
84
127
  content: [{ type: 'text', text: `Error: ${error.message}` }],
85
128
  isError: true,
@@ -92,21 +135,18 @@ server.tool(
92
135
  server.tool(
93
136
  'ghost_create_tag',
94
137
  'Creates a new tag in Ghost CMS.',
95
- {
96
- name: z.string().describe('The name of the tag.'),
97
- description: z.string().optional().describe('A description for the tag.'),
98
- slug: z
99
- .string()
100
- .optional()
101
- .describe(
102
- 'A URL-friendly slug for the tag. Will be auto-generated from the name if omitted.'
103
- ),
104
- },
105
- async ({ name, description, slug }) => {
106
- console.error(`Executing tool: ghost_create_tag with name: ${name}`);
138
+ createTagSchema,
139
+ async (rawInput) => {
140
+ const validation = validateToolInput(createTagSchema, rawInput, 'ghost_create_tag');
141
+ if (!validation.success) {
142
+ return validation.errorResponse;
143
+ }
144
+ const input = validation.data;
145
+
146
+ console.error(`Executing tool: ghost_create_tag with name: ${input.name}`);
107
147
  try {
108
148
  await loadServices();
109
- const createdTag = await ghostService.createTag({ name, description, slug });
149
+ const createdTag = await ghostService.createTag(input);
110
150
  console.error(`Tag created successfully. Tag ID: ${createdTag.id}`);
111
151
 
112
152
  return {
@@ -114,6 +154,13 @@ server.tool(
114
154
  };
115
155
  } catch (error) {
116
156
  console.error(`Error in ghost_create_tag:`, error);
157
+ if (error.name === 'ZodError') {
158
+ const validationError = ValidationError.fromZod(error, 'Tag creation');
159
+ return {
160
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
161
+ isError: true,
162
+ };
163
+ }
117
164
  return {
118
165
  content: [{ type: 'text', text: `Error: ${error.message}` }],
119
166
  isError: true,
@@ -126,15 +173,14 @@ server.tool(
126
173
  server.tool(
127
174
  'ghost_get_tag',
128
175
  'Retrieves a single tag from Ghost CMS by ID or slug.',
129
- {
130
- id: z.string().optional().describe('The ID of the tag to retrieve.'),
131
- slug: z.string().optional().describe('The slug of the tag to retrieve.'),
132
- include: z
133
- .string()
134
- .optional()
135
- .describe('Additional resources to include (e.g., "count.posts").'),
136
- },
137
- async ({ id, slug, include }) => {
176
+ getTagSchema,
177
+ async (rawInput) => {
178
+ const validation = validateToolInput(getTagSchema, rawInput, 'ghost_get_tag');
179
+ if (!validation.success) {
180
+ return validation.errorResponse;
181
+ }
182
+ const { id, slug, include } = validation.data;
183
+
138
184
  console.error(`Executing tool: ghost_get_tag`);
139
185
  try {
140
186
  if (!id && !slug) {
@@ -156,6 +202,13 @@ server.tool(
156
202
  };
157
203
  } catch (error) {
158
204
  console.error(`Error in ghost_get_tag:`, error);
205
+ if (error.name === 'ZodError') {
206
+ const validationError = ValidationError.fromZod(error, 'Tag retrieval');
207
+ return {
208
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
209
+ isError: true,
210
+ };
211
+ }
159
212
  return {
160
213
  content: [{ type: 'text', text: `Error: ${error.message}` }],
161
214
  isError: true,
@@ -168,32 +221,24 @@ server.tool(
168
221
  server.tool(
169
222
  'ghost_update_tag',
170
223
  'Updates an existing tag in Ghost CMS.',
171
- {
172
- id: z.string().describe('The ID of the tag to update.'),
173
- name: z.string().optional().describe('The new name for the tag.'),
174
- slug: z.string().optional().describe('The new slug for the tag.'),
175
- description: z.string().optional().describe('The new description for the tag.'),
176
- feature_image: z.string().url().optional().describe('URL of the feature image for the tag.'),
177
- meta_title: z.string().optional().describe('SEO meta title for the tag.'),
178
- meta_description: z.string().optional().describe('SEO meta description for the tag.'),
179
- },
180
- async ({ id, name, slug, description, feature_image, meta_title, meta_description }) => {
181
- console.error(`Executing tool: ghost_update_tag for ID: ${id}`);
224
+ updateTagInputSchema,
225
+ async (rawInput) => {
226
+ const validation = validateToolInput(updateTagInputSchema, rawInput, 'ghost_update_tag');
227
+ if (!validation.success) {
228
+ return validation.errorResponse;
229
+ }
230
+ const input = validation.data;
231
+
232
+ console.error(`Executing tool: ghost_update_tag for ID: ${input.id}`);
182
233
  try {
183
- if (!id) {
234
+ if (!input.id) {
184
235
  throw new Error('Tag ID is required');
185
236
  }
186
237
 
187
238
  await loadServices();
188
239
 
189
- // Build update data object with only provided fields
190
- const updateData = {};
191
- if (name !== undefined) updateData.name = name;
192
- if (slug !== undefined) updateData.slug = slug;
193
- if (description !== undefined) updateData.description = description;
194
- if (feature_image !== undefined) updateData.feature_image = feature_image;
195
- if (meta_title !== undefined) updateData.meta_title = meta_title;
196
- if (meta_description !== undefined) updateData.meta_description = meta_description;
240
+ // Build update data object with only provided fields (exclude id from update data)
241
+ const { id, ...updateData } = input;
197
242
 
198
243
  const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
199
244
  const updatedTag = await ghostServiceImproved.updateTag(id, updateData);
@@ -204,6 +249,13 @@ server.tool(
204
249
  };
205
250
  } catch (error) {
206
251
  console.error(`Error in ghost_update_tag:`, error);
252
+ if (error.name === 'ZodError') {
253
+ const validationError = ValidationError.fromZod(error, 'Tag update');
254
+ return {
255
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
256
+ isError: true,
257
+ };
258
+ }
207
259
  return {
208
260
  content: [{ type: 'text', text: `Error: ${error.message}` }],
209
261
  isError: true,
@@ -216,10 +268,14 @@ server.tool(
216
268
  server.tool(
217
269
  'ghost_delete_tag',
218
270
  'Deletes a tag from Ghost CMS by ID. This operation is permanent.',
219
- {
220
- id: z.string().describe('The ID of the tag to delete.'),
221
- },
222
- async ({ id }) => {
271
+ deleteTagSchema,
272
+ async (rawInput) => {
273
+ const validation = validateToolInput(deleteTagSchema, rawInput, 'ghost_delete_tag');
274
+ if (!validation.success) {
275
+ return validation.errorResponse;
276
+ }
277
+ const { id } = validation.data;
278
+
223
279
  console.error(`Executing tool: ghost_delete_tag for ID: ${id}`);
224
280
  try {
225
281
  if (!id) {
@@ -237,6 +293,13 @@ server.tool(
237
293
  };
238
294
  } catch (error) {
239
295
  console.error(`Error in ghost_delete_tag:`, error);
296
+ if (error.name === 'ZodError') {
297
+ const validationError = ValidationError.fromZod(error, 'Tag deletion');
298
+ return {
299
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
300
+ isError: true,
301
+ };
302
+ }
240
303
  return {
241
304
  content: [{ type: 'text', text: `Error: ${error.message}` }],
242
305
  isError: true,
@@ -245,20 +308,27 @@ server.tool(
245
308
  }
246
309
  );
247
310
 
311
+ // --- Image Schema ---
312
+ const uploadImageSchema = z.object({
313
+ imageUrl: z.string().describe('The publicly accessible URL of the image to upload.'),
314
+ alt: z
315
+ .string()
316
+ .optional()
317
+ .describe('Alt text for the image. If omitted, a default will be generated from the filename.'),
318
+ });
319
+
248
320
  // Upload Image Tool
249
321
  server.tool(
250
322
  'ghost_upload_image',
251
323
  'Downloads an image from a URL, processes it, uploads it to Ghost CMS, and returns the final Ghost image URL and alt text.',
252
- {
253
- imageUrl: z.string().describe('The publicly accessible URL of the image to upload.'),
254
- alt: z
255
- .string()
256
- .optional()
257
- .describe(
258
- 'Alt text for the image. If omitted, a default will be generated from the filename.'
259
- ),
260
- },
261
- async ({ imageUrl, alt }) => {
324
+ uploadImageSchema,
325
+ async (rawInput) => {
326
+ const validation = validateToolInput(uploadImageSchema, rawInput, 'ghost_upload_image');
327
+ if (!validation.success) {
328
+ return validation.errorResponse;
329
+ }
330
+ const { imageUrl, alt } = validation.data;
331
+
262
332
  console.error(`Executing tool: ghost_upload_image for URL: ${imageUrl}`);
263
333
  let downloadedPath = null;
264
334
  let processedPath = null;
@@ -288,10 +358,16 @@ server.tool(
288
358
  writer.on('finish', resolve);
289
359
  writer.on('error', reject);
290
360
  });
361
+ // Track temp file for cleanup on process exit
362
+ trackTempFile(downloadedPath);
291
363
  console.error(`Downloaded image to temporary path: ${downloadedPath}`);
292
364
 
293
365
  // 3. Process the image
294
366
  processedPath = await imageProcessingService.processImage(downloadedPath, tempDir);
367
+ // Track processed file for cleanup on process exit
368
+ if (processedPath !== downloadedPath) {
369
+ trackTempFile(processedPath);
370
+ }
295
371
  console.error(`Processed image path: ${processedPath}`);
296
372
 
297
373
  // 4. Determine Alt Text
@@ -319,63 +395,56 @@ server.tool(
319
395
  isError: true,
320
396
  };
321
397
  } finally {
322
- // Cleanup temporary files
323
- if (downloadedPath) {
324
- fs.unlink(downloadedPath, (err) => {
325
- if (err) console.error('Error deleting temporary downloaded file:', downloadedPath, err);
326
- });
327
- }
328
- if (processedPath && processedPath !== downloadedPath) {
329
- fs.unlink(processedPath, (err) => {
330
- if (err) console.error('Error deleting temporary processed file:', processedPath, err);
331
- });
332
- }
398
+ // Cleanup temporary files with proper async/await
399
+ await cleanupTempFiles([downloadedPath, processedPath], console);
333
400
  }
334
401
  }
335
402
  );
336
403
 
404
+ // --- Post Schema Definitions ---
405
+ const getPostsSchema = postQuerySchema.extend({
406
+ status: z
407
+ .enum(['published', 'draft', 'scheduled', 'all'])
408
+ .optional()
409
+ .describe('Filter posts by status. Options: published, draft, scheduled, all.'),
410
+ });
411
+ const getPostSchema = z.object({
412
+ id: ghostIdSchema.optional().describe('The ID of the post to retrieve.'),
413
+ slug: z.string().optional().describe('The slug of the post to retrieve.'),
414
+ include: z
415
+ .string()
416
+ .optional()
417
+ .describe('Comma-separated list of relations to include (e.g., "tags,authors").'),
418
+ });
419
+ const searchPostsSchema = z.object({
420
+ query: z.string().min(1).describe('Search query to find in post titles.'),
421
+ status: z
422
+ .enum(['published', 'draft', 'scheduled', 'all'])
423
+ .optional()
424
+ .describe('Filter by post status. Default searches all statuses.'),
425
+ limit: z
426
+ .number()
427
+ .int()
428
+ .min(1)
429
+ .max(50)
430
+ .optional()
431
+ .describe('Maximum number of results (1-50). Default is 15.'),
432
+ });
433
+ const updatePostInputSchema = updatePostSchema.extend({ id: ghostIdSchema });
434
+ const deletePostSchema = z.object({ id: ghostIdSchema });
435
+
337
436
  // Create Post Tool
338
437
  server.tool(
339
438
  'ghost_create_post',
340
439
  'Creates a new post in Ghost CMS.',
341
- {
342
- title: z.string().describe('The title of the post.'),
343
- html: z.string().describe('The HTML content of the post.'),
344
- status: z
345
- .enum(['draft', 'published', 'scheduled'])
346
- .optional()
347
- .describe("The status of the post. Defaults to 'draft'."),
348
- tags: z
349
- .array(z.string())
350
- .optional()
351
- .describe(
352
- "List of tag names to associate with the post. Tags will be created if they don't exist."
353
- ),
354
- published_at: z
355
- .string()
356
- .optional()
357
- .describe("ISO 8601 date/time to publish the post. Required if status is 'scheduled'."),
358
- custom_excerpt: z.string().optional().describe('A custom short summary for the post.'),
359
- feature_image: z
360
- .string()
361
- .optional()
362
- .describe(
363
- 'URL of the image (e.g., from ghost_upload_image tool) to use as the featured image.'
364
- ),
365
- feature_image_alt: z.string().optional().describe('Alt text for the featured image.'),
366
- feature_image_caption: z.string().optional().describe('Caption for the featured image.'),
367
- meta_title: z
368
- .string()
369
- .optional()
370
- .describe('Custom title for SEO (max 300 chars). Defaults to post title if omitted.'),
371
- meta_description: z
372
- .string()
373
- .optional()
374
- .describe(
375
- 'Custom description for SEO (max 500 chars). Defaults to excerpt or generated summary if omitted.'
376
- ),
377
- },
378
- async (input) => {
440
+ createPostSchema,
441
+ async (rawInput) => {
442
+ const validation = validateToolInput(createPostSchema, rawInput, 'ghost_create_post');
443
+ if (!validation.success) {
444
+ return validation.errorResponse;
445
+ }
446
+ const input = validation.data;
447
+
379
448
  console.error(`Executing tool: ghost_create_post with title: ${input.title}`);
380
449
  try {
381
450
  await loadServices();
@@ -387,6 +456,13 @@ server.tool(
387
456
  };
388
457
  } catch (error) {
389
458
  console.error(`Error in ghost_create_post:`, error);
459
+ if (error.name === 'ZodError') {
460
+ const validationError = ValidationError.fromZod(error, 'Post creation');
461
+ return {
462
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
463
+ isError: true,
464
+ };
465
+ }
390
466
  return {
391
467
  content: [{ type: 'text', text: `Error creating post: ${error.message}` }],
392
468
  isError: true,
@@ -399,32 +475,14 @@ server.tool(
399
475
  server.tool(
400
476
  'ghost_get_posts',
401
477
  'Retrieves a list of posts from Ghost CMS with pagination, filtering, and sorting options.',
402
- {
403
- limit: z
404
- .number()
405
- .min(1)
406
- .max(100)
407
- .optional()
408
- .describe('Number of posts to retrieve (1-100). Default is 15.'),
409
- page: z.number().min(1).optional().describe('Page number for pagination. Default is 1.'),
410
- status: z
411
- .enum(['published', 'draft', 'scheduled', 'all'])
412
- .optional()
413
- .describe('Filter posts by status. Options: published, draft, scheduled, all.'),
414
- include: z
415
- .string()
416
- .optional()
417
- .describe('Comma-separated list of relations to include (e.g., "tags,authors").'),
418
- filter: z
419
- .string()
420
- .optional()
421
- .describe('Ghost NQL filter string for advanced filtering (e.g., "featured:true").'),
422
- order: z
423
- .string()
424
- .optional()
425
- .describe('Sort order for results (e.g., "published_at DESC", "title ASC").'),
426
- },
427
- async (input) => {
478
+ getPostsSchema,
479
+ async (rawInput) => {
480
+ const validation = validateToolInput(getPostsSchema, rawInput, 'ghost_get_posts');
481
+ if (!validation.success) {
482
+ return validation.errorResponse;
483
+ }
484
+ const input = validation.data;
485
+
428
486
  console.error(`Executing tool: ghost_get_posts`);
429
487
  try {
430
488
  await loadServices();
@@ -446,6 +504,13 @@ server.tool(
446
504
  };
447
505
  } catch (error) {
448
506
  console.error(`Error in ghost_get_posts:`, error);
507
+ if (error.name === 'ZodError') {
508
+ const validationError = ValidationError.fromZod(error, 'Posts retrieval');
509
+ return {
510
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
511
+ isError: true,
512
+ };
513
+ }
449
514
  return {
450
515
  content: [{ type: 'text', text: `Error retrieving posts: ${error.message}` }],
451
516
  isError: true,
@@ -458,15 +523,14 @@ server.tool(
458
523
  server.tool(
459
524
  'ghost_get_post',
460
525
  'Retrieves a single post from Ghost CMS by ID or slug.',
461
- {
462
- id: z.string().optional().describe('The ID of the post to retrieve.'),
463
- slug: z.string().optional().describe('The slug of the post to retrieve.'),
464
- include: z
465
- .string()
466
- .optional()
467
- .describe('Comma-separated list of relations to include (e.g., "tags,authors").'),
468
- },
469
- async (input) => {
526
+ getPostSchema,
527
+ async (rawInput) => {
528
+ const validation = validateToolInput(getPostSchema, rawInput, 'ghost_get_post');
529
+ if (!validation.success) {
530
+ return validation.errorResponse;
531
+ }
532
+ const input = validation.data;
533
+
470
534
  console.error(`Executing tool: ghost_get_post`);
471
535
  try {
472
536
  // Validate that at least one of id or slug is provided
@@ -491,6 +555,13 @@ server.tool(
491
555
  };
492
556
  } catch (error) {
493
557
  console.error(`Error in ghost_get_post:`, error);
558
+ if (error.name === 'ZodError') {
559
+ const validationError = ValidationError.fromZod(error, 'Post retrieval');
560
+ return {
561
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
562
+ isError: true,
563
+ };
564
+ }
494
565
  return {
495
566
  content: [{ type: 'text', text: `Error retrieving post: ${error.message}` }],
496
567
  isError: true,
@@ -503,20 +574,14 @@ server.tool(
503
574
  server.tool(
504
575
  'ghost_search_posts',
505
576
  'Search for posts in Ghost CMS by query string with optional status filtering.',
506
- {
507
- query: z.string().describe('Search query to find in post titles.'),
508
- status: z
509
- .enum(['published', 'draft', 'scheduled', 'all'])
510
- .optional()
511
- .describe('Filter by post status. Default searches all statuses.'),
512
- limit: z
513
- .number()
514
- .min(1)
515
- .max(50)
516
- .optional()
517
- .describe('Maximum number of results (1-50). Default is 15.'),
518
- },
519
- async (input) => {
577
+ searchPostsSchema,
578
+ async (rawInput) => {
579
+ const validation = validateToolInput(searchPostsSchema, rawInput, 'ghost_search_posts');
580
+ if (!validation.success) {
581
+ return validation.errorResponse;
582
+ }
583
+ const input = validation.data;
584
+
520
585
  console.error(`Executing tool: ghost_search_posts with query: ${input.query}`);
521
586
  try {
522
587
  await loadServices();
@@ -536,6 +601,13 @@ server.tool(
536
601
  };
537
602
  } catch (error) {
538
603
  console.error(`Error in ghost_search_posts:`, error);
604
+ if (error.name === 'ZodError') {
605
+ const validationError = ValidationError.fromZod(error, 'Post search');
606
+ return {
607
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
608
+ isError: true,
609
+ };
610
+ }
539
611
  return {
540
612
  content: [{ type: 'text', text: `Error searching posts: ${error.message}` }],
541
613
  isError: true,
@@ -548,35 +620,14 @@ server.tool(
548
620
  server.tool(
549
621
  'ghost_update_post',
550
622
  'Updates an existing post in Ghost CMS. Can update title, content, status, tags, images, and SEO fields.',
551
- {
552
- id: z.string().describe('The ID of the post to update.'),
553
- title: z.string().optional().describe('New title for the post.'),
554
- html: z.string().optional().describe('New HTML content for the post.'),
555
- status: z
556
- .enum(['draft', 'published', 'scheduled'])
557
- .optional()
558
- .describe('New status for the post.'),
559
- tags: z
560
- .array(z.string())
561
- .optional()
562
- .describe('New list of tag names to associate with the post.'),
563
- feature_image: z.string().optional().describe('New featured image URL.'),
564
- feature_image_alt: z.string().optional().describe('New alt text for the featured image.'),
565
- feature_image_caption: z.string().optional().describe('New caption for the featured image.'),
566
- meta_title: z.string().optional().describe('New custom title for SEO (max 300 chars).'),
567
- meta_description: z
568
- .string()
569
- .optional()
570
- .describe('New custom description for SEO (max 500 chars).'),
571
- published_at: z
572
- .string()
573
- .optional()
574
- .describe(
575
- "New publication date/time in ISO 8601 format. Required if changing status to 'scheduled'."
576
- ),
577
- custom_excerpt: z.string().optional().describe('New custom short summary for the post.'),
578
- },
579
- async (input) => {
623
+ updatePostInputSchema,
624
+ async (rawInput) => {
625
+ const validation = validateToolInput(updatePostInputSchema, rawInput, 'ghost_update_post');
626
+ if (!validation.success) {
627
+ return validation.errorResponse;
628
+ }
629
+ const input = validation.data;
630
+
580
631
  console.error(`Executing tool: ghost_update_post for post ID: ${input.id}`);
581
632
  try {
582
633
  await loadServices();
@@ -594,6 +645,13 @@ server.tool(
594
645
  };
595
646
  } catch (error) {
596
647
  console.error(`Error in ghost_update_post:`, error);
648
+ if (error.name === 'ZodError') {
649
+ const validationError = ValidationError.fromZod(error, 'Post update');
650
+ return {
651
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
652
+ isError: true,
653
+ };
654
+ }
597
655
  return {
598
656
  content: [{ type: 'text', text: `Error updating post: ${error.message}` }],
599
657
  isError: true,
@@ -606,10 +664,14 @@ server.tool(
606
664
  server.tool(
607
665
  'ghost_delete_post',
608
666
  'Deletes a post from Ghost CMS by ID. This operation is permanent and cannot be undone.',
609
- {
610
- id: z.string().describe('The ID of the post to delete.'),
611
- },
612
- async ({ id }) => {
667
+ deletePostSchema,
668
+ async (rawInput) => {
669
+ const validation = validateToolInput(deletePostSchema, rawInput, 'ghost_delete_post');
670
+ if (!validation.success) {
671
+ return validation.errorResponse;
672
+ }
673
+ const { id } = validation.data;
674
+
613
675
  console.error(`Executing tool: ghost_delete_post for post ID: ${id}`);
614
676
  try {
615
677
  await loadServices();
@@ -624,6 +686,13 @@ server.tool(
624
686
  };
625
687
  } catch (error) {
626
688
  console.error(`Error in ghost_delete_post:`, error);
689
+ if (error.name === 'ZodError') {
690
+ const validationError = ValidationError.fromZod(error, 'Post deletion');
691
+ return {
692
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
693
+ isError: true,
694
+ };
695
+ }
627
696
  return {
628
697
  content: [{ type: 'text', text: `Error deleting post: ${error.message}` }],
629
698
  isError: true,
@@ -637,30 +706,54 @@ server.tool(
637
706
  // Pages are similar to posts but do NOT support tags
638
707
  // =============================================================================
639
708
 
640
- // Get Pages Tool
641
- server.tool(
642
- 'ghost_get_pages',
643
- 'Retrieves a list of pages from Ghost CMS with pagination, filtering, and sorting options.',
644
- {
645
- limit: z
646
- .number()
647
- .min(1)
648
- .max(100)
649
- .optional()
650
- .describe('Number of pages to retrieve (1-100). Default is 15.'),
651
- page: z.number().min(1).optional().describe('Page number for pagination. Default is 1.'),
652
- status: z
653
- .enum(['published', 'draft', 'scheduled', 'all'])
654
- .optional()
655
- .describe('Filter pages by status.'),
709
+ // --- Page Schema Definitions ---
710
+ const getPageSchema = z
711
+ .object({
712
+ id: ghostIdSchema.optional().describe('The ID of the page to retrieve.'),
713
+ slug: z.string().optional().describe('The slug of the page to retrieve.'),
656
714
  include: z
657
715
  .string()
658
716
  .optional()
659
717
  .describe('Comma-separated list of relations to include (e.g., "authors").'),
660
- filter: z.string().optional().describe('Ghost NQL filter string for advanced filtering.'),
661
- order: z.string().optional().describe('Sort order for results (e.g., "published_at DESC").'),
662
- },
663
- async (input) => {
718
+ })
719
+ .refine((data) => data.id || data.slug, {
720
+ message: 'Either id or slug is required to retrieve a page',
721
+ });
722
+ const updatePageInputSchema = z
723
+ .object({ id: ghostIdSchema.describe('The ID of the page to update.') })
724
+ .merge(updatePageSchema);
725
+ const deletePageSchema = z.object({ id: ghostIdSchema.describe('The ID of the page to delete.') });
726
+ const searchPagesSchema = z.object({
727
+ query: z
728
+ .string()
729
+ .min(1, 'Search query cannot be empty')
730
+ .describe('Search query to find in page titles.'),
731
+ status: z
732
+ .enum(['published', 'draft', 'scheduled', 'all'])
733
+ .optional()
734
+ .describe('Filter by page status. Default searches all statuses.'),
735
+ limit: z
736
+ .number()
737
+ .int()
738
+ .min(1)
739
+ .max(50)
740
+ .default(15)
741
+ .optional()
742
+ .describe('Maximum number of results (1-50). Default is 15.'),
743
+ });
744
+
745
+ // Get Pages Tool
746
+ server.tool(
747
+ 'ghost_get_pages',
748
+ 'Retrieves a list of pages from Ghost CMS with pagination, filtering, and sorting options.',
749
+ pageQuerySchema,
750
+ async (rawInput) => {
751
+ const validation = validateToolInput(pageQuerySchema, rawInput, 'ghost_get_pages');
752
+ if (!validation.success) {
753
+ return validation.errorResponse;
754
+ }
755
+ const input = validation.data;
756
+
664
757
  console.error(`Executing tool: ghost_get_pages`);
665
758
  try {
666
759
  await loadServices();
@@ -668,9 +761,10 @@ server.tool(
668
761
  const options = {};
669
762
  if (input.limit !== undefined) options.limit = input.limit;
670
763
  if (input.page !== undefined) options.page = input.page;
671
- if (input.status !== undefined) options.status = input.status;
672
- if (input.include !== undefined) options.include = input.include;
673
764
  if (input.filter !== undefined) options.filter = input.filter;
765
+ if (input.include !== undefined) options.include = input.include;
766
+ if (input.fields !== undefined) options.fields = input.fields;
767
+ if (input.formats !== undefined) options.formats = input.formats;
674
768
  if (input.order !== undefined) options.order = input.order;
675
769
 
676
770
  const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
@@ -682,6 +776,13 @@ server.tool(
682
776
  };
683
777
  } catch (error) {
684
778
  console.error(`Error in ghost_get_pages:`, error);
779
+ if (error.name === 'ZodError') {
780
+ const validationError = ValidationError.fromZod(error, 'Page query');
781
+ return {
782
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
783
+ isError: true,
784
+ };
785
+ }
685
786
  return {
686
787
  content: [{ type: 'text', text: `Error retrieving pages: ${error.message}` }],
687
788
  isError: true,
@@ -694,21 +795,16 @@ server.tool(
694
795
  server.tool(
695
796
  'ghost_get_page',
696
797
  'Retrieves a single page from Ghost CMS by ID or slug.',
697
- {
698
- id: z.string().optional().describe('The ID of the page to retrieve.'),
699
- slug: z.string().optional().describe('The slug of the page to retrieve.'),
700
- include: z
701
- .string()
702
- .optional()
703
- .describe('Comma-separated list of relations to include (e.g., "authors").'),
704
- },
705
- async (input) => {
798
+ getPageSchema,
799
+ async (rawInput) => {
800
+ const validation = validateToolInput(getPageSchema, rawInput, 'ghost_get_page');
801
+ if (!validation.success) {
802
+ return validation.errorResponse;
803
+ }
804
+ const input = validation.data;
805
+
706
806
  console.error(`Executing tool: ghost_get_page`);
707
807
  try {
708
- if (!input.id && !input.slug) {
709
- throw new Error('Either id or slug is required to retrieve a page');
710
- }
711
-
712
808
  await loadServices();
713
809
 
714
810
  const options = {};
@@ -725,6 +821,13 @@ server.tool(
725
821
  };
726
822
  } catch (error) {
727
823
  console.error(`Error in ghost_get_page:`, error);
824
+ if (error.name === 'ZodError') {
825
+ const validationError = ValidationError.fromZod(error, 'Get page');
826
+ return {
827
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
828
+ isError: true,
829
+ };
830
+ }
728
831
  return {
729
832
  content: [{ type: 'text', text: `Error retrieving page: ${error.message}` }],
730
833
  isError: true,
@@ -736,35 +839,15 @@ server.tool(
736
839
  // Create Page Tool
737
840
  server.tool(
738
841
  'ghost_create_page',
739
- 'Creates a new page in Ghost CMS. Note: Pages do NOT support tags (unlike posts).',
740
- {
741
- title: z.string().describe('The title of the page.'),
742
- html: z.string().describe('The HTML content of the page.'),
743
- status: z
744
- .enum(['draft', 'published', 'scheduled'])
745
- .optional()
746
- .describe("The status of the page. Defaults to 'draft'."),
747
- // NO tags parameter - pages don't support tags
748
- published_at: z
749
- .string()
750
- .optional()
751
- .describe("ISO 8601 date/time to publish the page. Required if status is 'scheduled'."),
752
- custom_excerpt: z.string().optional().describe('A custom short summary for the page.'),
753
- feature_image: z.string().optional().describe('URL of the image to use as the featured image.'),
754
- feature_image_alt: z.string().optional().describe('Alt text for the featured image.'),
755
- feature_image_caption: z.string().optional().describe('Caption for the featured image.'),
756
- meta_title: z
757
- .string()
758
- .optional()
759
- .describe('Custom title for SEO (max 70 chars). Defaults to page title if omitted.'),
760
- meta_description: z
761
- .string()
762
- .optional()
763
- .describe(
764
- 'Custom description for SEO (max 160 chars). Defaults to excerpt or generated summary if omitted.'
765
- ),
766
- },
767
- async (input) => {
842
+ 'Creates a new page in Ghost CMS. Note: Pages do NOT typically use tags (unlike posts).',
843
+ createPageSchema,
844
+ async (rawInput) => {
845
+ const validation = validateToolInput(createPageSchema, rawInput, 'ghost_create_page');
846
+ if (!validation.success) {
847
+ return validation.errorResponse;
848
+ }
849
+ const input = validation.data;
850
+
768
851
  console.error(`Executing tool: ghost_create_page with title: ${input.title}`);
769
852
  try {
770
853
  await loadServices();
@@ -778,6 +861,13 @@ server.tool(
778
861
  };
779
862
  } catch (error) {
780
863
  console.error(`Error in ghost_create_page:`, error);
864
+ if (error.name === 'ZodError') {
865
+ const validationError = ValidationError.fromZod(error, 'Page creation');
866
+ return {
867
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
868
+ isError: true,
869
+ };
870
+ }
781
871
  return {
782
872
  content: [{ type: 'text', text: `Error creating page: ${error.message}` }],
783
873
  isError: true,
@@ -789,25 +879,15 @@ server.tool(
789
879
  // Update Page Tool
790
880
  server.tool(
791
881
  'ghost_update_page',
792
- 'Updates an existing page in Ghost CMS. Can update title, content, status, images, and SEO fields. Note: Pages do NOT support tags.',
793
- {
794
- id: z.string().describe('The ID of the page to update.'),
795
- title: z.string().optional().describe('New title for the page.'),
796
- html: z.string().optional().describe('New HTML content for the page.'),
797
- status: z
798
- .enum(['draft', 'published', 'scheduled'])
799
- .optional()
800
- .describe('New status for the page.'),
801
- // NO tags parameter - pages don't support tags
802
- feature_image: z.string().optional().describe('New featured image URL.'),
803
- feature_image_alt: z.string().optional().describe('New alt text for the featured image.'),
804
- feature_image_caption: z.string().optional().describe('New caption for the featured image.'),
805
- meta_title: z.string().optional().describe('New custom title for SEO.'),
806
- meta_description: z.string().optional().describe('New custom description for SEO.'),
807
- published_at: z.string().optional().describe('New publication date/time in ISO 8601 format.'),
808
- custom_excerpt: z.string().optional().describe('New custom short summary for the page.'),
809
- },
810
- async (input) => {
882
+ 'Updates an existing page in Ghost CMS. Can update title, content, status, images, and SEO fields.',
883
+ updatePageInputSchema,
884
+ async (rawInput) => {
885
+ const validation = validateToolInput(updatePageInputSchema, rawInput, 'ghost_update_page');
886
+ if (!validation.success) {
887
+ return validation.errorResponse;
888
+ }
889
+ const input = validation.data;
890
+
811
891
  console.error(`Executing tool: ghost_update_page for page ID: ${input.id}`);
812
892
  try {
813
893
  await loadServices();
@@ -823,6 +903,13 @@ server.tool(
823
903
  };
824
904
  } catch (error) {
825
905
  console.error(`Error in ghost_update_page:`, error);
906
+ if (error.name === 'ZodError') {
907
+ const validationError = ValidationError.fromZod(error, 'Page update');
908
+ return {
909
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
910
+ isError: true,
911
+ };
912
+ }
826
913
  return {
827
914
  content: [{ type: 'text', text: `Error updating page: ${error.message}` }],
828
915
  isError: true,
@@ -835,10 +922,14 @@ server.tool(
835
922
  server.tool(
836
923
  'ghost_delete_page',
837
924
  'Deletes a page from Ghost CMS by ID. This operation is permanent and cannot be undone.',
838
- {
839
- id: z.string().describe('The ID of the page to delete.'),
840
- },
841
- async ({ id }) => {
925
+ deletePageSchema,
926
+ async (rawInput) => {
927
+ const validation = validateToolInput(deletePageSchema, rawInput, 'ghost_delete_page');
928
+ if (!validation.success) {
929
+ return validation.errorResponse;
930
+ }
931
+ const { id } = validation.data;
932
+
842
933
  console.error(`Executing tool: ghost_delete_page for page ID: ${id}`);
843
934
  try {
844
935
  await loadServices();
@@ -852,6 +943,13 @@ server.tool(
852
943
  };
853
944
  } catch (error) {
854
945
  console.error(`Error in ghost_delete_page:`, error);
946
+ if (error.name === 'ZodError') {
947
+ const validationError = ValidationError.fromZod(error, 'Page deletion');
948
+ return {
949
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
950
+ isError: true,
951
+ };
952
+ }
855
953
  return {
856
954
  content: [{ type: 'text', text: `Error deleting page: ${error.message}` }],
857
955
  isError: true,
@@ -864,20 +962,14 @@ server.tool(
864
962
  server.tool(
865
963
  'ghost_search_pages',
866
964
  'Search for pages in Ghost CMS by query string with optional status filtering.',
867
- {
868
- query: z.string().describe('Search query to find in page titles.'),
869
- status: z
870
- .enum(['published', 'draft', 'scheduled', 'all'])
871
- .optional()
872
- .describe('Filter by page status. Default searches all statuses.'),
873
- limit: z
874
- .number()
875
- .min(1)
876
- .max(50)
877
- .optional()
878
- .describe('Maximum number of results (1-50). Default is 15.'),
879
- },
880
- async (input) => {
965
+ searchPagesSchema,
966
+ async (rawInput) => {
967
+ const validation = validateToolInput(searchPagesSchema, rawInput, 'ghost_search_pages');
968
+ if (!validation.success) {
969
+ return validation.errorResponse;
970
+ }
971
+ const input = validation.data;
972
+
881
973
  console.error(`Executing tool: ghost_search_pages with query: ${input.query}`);
882
974
  try {
883
975
  await loadServices();
@@ -895,6 +987,13 @@ server.tool(
895
987
  };
896
988
  } catch (error) {
897
989
  console.error(`Error in ghost_search_pages:`, error);
990
+ if (error.name === 'ZodError') {
991
+ const validationError = ValidationError.fromZod(error, 'Page search');
992
+ return {
993
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
994
+ isError: true,
995
+ };
996
+ }
898
997
  return {
899
998
  content: [{ type: 'text', text: `Error searching pages: ${error.message}` }],
900
999
  isError: true,
@@ -908,25 +1007,41 @@ server.tool(
908
1007
  // Member management for Ghost CMS subscribers
909
1008
  // =============================================================================
910
1009
 
1010
+ // --- Member Schema Definitions ---
1011
+ const updateMemberInputSchema = z.object({ id: ghostIdSchema }).merge(updateMemberSchema);
1012
+ const deleteMemberSchema = z.object({ id: ghostIdSchema });
1013
+ const getMembersSchema = memberQuerySchema.omit({ search: true });
1014
+ const getMemberSchema = z
1015
+ .object({
1016
+ id: ghostIdSchema.optional().describe('The ID of the member to retrieve.'),
1017
+ email: emailSchema.optional().describe('The email of the member to retrieve.'),
1018
+ })
1019
+ .refine((data) => data.id || data.email, {
1020
+ message: 'Either id or email must be provided',
1021
+ });
1022
+ const searchMembersSchema = z.object({
1023
+ query: z.string().min(1).describe('Search query to match against member name or email.'),
1024
+ limit: z
1025
+ .number()
1026
+ .int()
1027
+ .min(1)
1028
+ .max(50)
1029
+ .optional()
1030
+ .describe('Maximum number of results to return (1-50). Default is 15.'),
1031
+ });
1032
+
911
1033
  // Create Member Tool
912
1034
  server.tool(
913
1035
  'ghost_create_member',
914
1036
  'Creates a new member (subscriber) in Ghost CMS.',
915
- {
916
- email: z.string().email().describe('The email address of the member (required).'),
917
- name: z.string().optional().describe('The name of the member.'),
918
- note: z.string().optional().describe('A note about the member.'),
919
- labels: z.array(z.string()).optional().describe('List of label names to assign to the member.'),
920
- newsletters: z
921
- .array(z.object({ id: z.string() }))
922
- .optional()
923
- .describe('List of newsletter objects with id field to subscribe the member to.'),
924
- subscribed: z
925
- .boolean()
926
- .optional()
927
- .describe('Whether the member is subscribed to emails. Defaults to true.'),
928
- },
929
- async (input) => {
1037
+ createMemberSchema,
1038
+ async (rawInput) => {
1039
+ const validation = validateToolInput(createMemberSchema, rawInput, 'ghost_create_member');
1040
+ if (!validation.success) {
1041
+ return validation.errorResponse;
1042
+ }
1043
+ const input = validation.data;
1044
+
930
1045
  console.error(`Executing tool: ghost_create_member with email: ${input.email}`);
931
1046
  try {
932
1047
  await loadServices();
@@ -940,6 +1055,13 @@ server.tool(
940
1055
  };
941
1056
  } catch (error) {
942
1057
  console.error(`Error in ghost_create_member:`, error);
1058
+ if (error.name === 'ZodError') {
1059
+ const validationError = ValidationError.fromZod(error, 'Member creation');
1060
+ return {
1061
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1062
+ isError: true,
1063
+ };
1064
+ }
943
1065
  return {
944
1066
  content: [{ type: 'text', text: `Error creating member: ${error.message}` }],
945
1067
  isError: true,
@@ -952,21 +1074,14 @@ server.tool(
952
1074
  server.tool(
953
1075
  'ghost_update_member',
954
1076
  'Updates an existing member in Ghost CMS. All fields except id are optional.',
955
- {
956
- id: z.string().describe('The ID of the member to update.'),
957
- email: z.string().email().optional().describe('New email address for the member.'),
958
- name: z.string().optional().describe('New name for the member.'),
959
- note: z.string().optional().describe('New note about the member.'),
960
- labels: z
961
- .array(z.string())
962
- .optional()
963
- .describe('New list of label names to assign to the member.'),
964
- newsletters: z
965
- .array(z.object({ id: z.string() }))
966
- .optional()
967
- .describe('New list of newsletter objects with id field to subscribe the member to.'),
968
- },
969
- async (input) => {
1077
+ updateMemberInputSchema,
1078
+ async (rawInput) => {
1079
+ const validation = validateToolInput(updateMemberInputSchema, rawInput, 'ghost_update_member');
1080
+ if (!validation.success) {
1081
+ return validation.errorResponse;
1082
+ }
1083
+ const input = validation.data;
1084
+
970
1085
  console.error(`Executing tool: ghost_update_member for member ID: ${input.id}`);
971
1086
  try {
972
1087
  await loadServices();
@@ -982,6 +1097,13 @@ server.tool(
982
1097
  };
983
1098
  } catch (error) {
984
1099
  console.error(`Error in ghost_update_member:`, error);
1100
+ if (error.name === 'ZodError') {
1101
+ const validationError = ValidationError.fromZod(error, 'Member update');
1102
+ return {
1103
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1104
+ isError: true,
1105
+ };
1106
+ }
985
1107
  return {
986
1108
  content: [{ type: 'text', text: `Error updating member: ${error.message}` }],
987
1109
  isError: true,
@@ -994,10 +1116,14 @@ server.tool(
994
1116
  server.tool(
995
1117
  'ghost_delete_member',
996
1118
  'Deletes a member from Ghost CMS by ID. This operation is permanent and cannot be undone.',
997
- {
998
- id: z.string().describe('The ID of the member to delete.'),
999
- },
1000
- async ({ id }) => {
1119
+ deleteMemberSchema,
1120
+ async (rawInput) => {
1121
+ const validation = validateToolInput(deleteMemberSchema, rawInput, 'ghost_delete_member');
1122
+ if (!validation.success) {
1123
+ return validation.errorResponse;
1124
+ }
1125
+ const { id } = validation.data;
1126
+
1001
1127
  console.error(`Executing tool: ghost_delete_member for member ID: ${id}`);
1002
1128
  try {
1003
1129
  await loadServices();
@@ -1011,6 +1137,13 @@ server.tool(
1011
1137
  };
1012
1138
  } catch (error) {
1013
1139
  console.error(`Error in ghost_delete_member:`, error);
1140
+ if (error.name === 'ZodError') {
1141
+ const validationError = ValidationError.fromZod(error, 'Member deletion');
1142
+ return {
1143
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1144
+ isError: true,
1145
+ };
1146
+ }
1014
1147
  return {
1015
1148
  content: [{ type: 'text', text: `Error deleting member: ${error.message}` }],
1016
1149
  isError: true,
@@ -1023,25 +1156,14 @@ server.tool(
1023
1156
  server.tool(
1024
1157
  'ghost_get_members',
1025
1158
  'Retrieves a list of members (subscribers) from Ghost CMS with optional filtering, pagination, and includes.',
1026
- {
1027
- limit: z
1028
- .number()
1029
- .min(1)
1030
- .max(100)
1031
- .optional()
1032
- .describe('Number of members to retrieve (1-100). Default is 15.'),
1033
- page: z.number().min(1).optional().describe('Page number for pagination (starts at 1).'),
1034
- filter: z
1035
- .string()
1036
- .optional()
1037
- .describe('Ghost NQL filter string (e.g., "status:free", "status:paid", "subscribed:true").'),
1038
- order: z.string().optional().describe('Order string (e.g., "created_at desc", "email asc").'),
1039
- include: z
1040
- .string()
1041
- .optional()
1042
- .describe('Comma-separated list of related data to include (e.g., "labels,newsletters").'),
1043
- },
1044
- async (input) => {
1159
+ getMembersSchema,
1160
+ async (rawInput) => {
1161
+ const validation = validateToolInput(getMembersSchema, rawInput, 'ghost_get_members');
1162
+ if (!validation.success) {
1163
+ return validation.errorResponse;
1164
+ }
1165
+ const input = validation.data;
1166
+
1045
1167
  console.error(`Executing tool: ghost_get_members`);
1046
1168
  try {
1047
1169
  await loadServices();
@@ -1062,6 +1184,13 @@ server.tool(
1062
1184
  };
1063
1185
  } catch (error) {
1064
1186
  console.error(`Error in ghost_get_members:`, error);
1187
+ if (error.name === 'ZodError') {
1188
+ const validationError = ValidationError.fromZod(error, 'Member query');
1189
+ return {
1190
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1191
+ isError: true,
1192
+ };
1193
+ }
1065
1194
  return {
1066
1195
  content: [{ type: 'text', text: `Error retrieving members: ${error.message}` }],
1067
1196
  isError: true,
@@ -1074,11 +1203,14 @@ server.tool(
1074
1203
  server.tool(
1075
1204
  'ghost_get_member',
1076
1205
  'Retrieves a single member from Ghost CMS by ID or email. Provide either id OR email.',
1077
- {
1078
- id: z.string().optional().describe('The ID of the member to retrieve.'),
1079
- email: z.string().email().optional().describe('The email of the member to retrieve.'),
1080
- },
1081
- async ({ id, email }) => {
1206
+ getMemberSchema,
1207
+ async (rawInput) => {
1208
+ const validation = validateToolInput(getMemberSchema, rawInput, 'ghost_get_member');
1209
+ if (!validation.success) {
1210
+ return validation.errorResponse;
1211
+ }
1212
+ const { id, email } = validation.data;
1213
+
1082
1214
  console.error(`Executing tool: ghost_get_member for ${id ? `ID: ${id}` : `email: ${email}`}`);
1083
1215
  try {
1084
1216
  await loadServices();
@@ -1092,6 +1224,13 @@ server.tool(
1092
1224
  };
1093
1225
  } catch (error) {
1094
1226
  console.error(`Error in ghost_get_member:`, error);
1227
+ if (error.name === 'ZodError') {
1228
+ const validationError = ValidationError.fromZod(error, 'Member lookup');
1229
+ return {
1230
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1231
+ isError: true,
1232
+ };
1233
+ }
1095
1234
  return {
1096
1235
  content: [{ type: 'text', text: `Error retrieving member: ${error.message}` }],
1097
1236
  isError: true,
@@ -1104,16 +1243,14 @@ server.tool(
1104
1243
  server.tool(
1105
1244
  'ghost_search_members',
1106
1245
  'Searches for members by name or email in Ghost CMS.',
1107
- {
1108
- query: z.string().min(1).describe('Search query to match against member name or email.'),
1109
- limit: z
1110
- .number()
1111
- .min(1)
1112
- .max(50)
1113
- .optional()
1114
- .describe('Maximum number of results to return (1-50). Default is 15.'),
1115
- },
1116
- async ({ query, limit }) => {
1246
+ searchMembersSchema,
1247
+ async (rawInput) => {
1248
+ const validation = validateToolInput(searchMembersSchema, rawInput, 'ghost_search_members');
1249
+ if (!validation.success) {
1250
+ return validation.errorResponse;
1251
+ }
1252
+ const { query, limit } = validation.data;
1253
+
1117
1254
  console.error(`Executing tool: ghost_search_members with query: ${query}`);
1118
1255
  try {
1119
1256
  await loadServices();
@@ -1130,6 +1267,13 @@ server.tool(
1130
1267
  };
1131
1268
  } catch (error) {
1132
1269
  console.error(`Error in ghost_search_members:`, error);
1270
+ if (error.name === 'ZodError') {
1271
+ const validationError = ValidationError.fromZod(error, 'Member search');
1272
+ return {
1273
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1274
+ isError: true,
1275
+ };
1276
+ }
1133
1277
  return {
1134
1278
  content: [{ type: 'text', text: `Error searching members: ${error.message}` }],
1135
1279
  isError: true,
@@ -1142,27 +1286,32 @@ server.tool(
1142
1286
  // NEWSLETTER TOOLS
1143
1287
  // =============================================================================
1144
1288
 
1289
+ // --- Newsletter Schema Definitions ---
1290
+ const getNewsletterSchema = z.object({ id: ghostIdSchema });
1291
+ const updateNewsletterInputSchema = z.object({ id: ghostIdSchema }).merge(updateNewsletterSchema);
1292
+ const deleteNewsletterSchema = z.object({ id: ghostIdSchema });
1293
+
1145
1294
  // Get Newsletters Tool
1146
1295
  server.tool(
1147
1296
  'ghost_get_newsletters',
1148
1297
  'Retrieves a list of newsletters from Ghost CMS with optional filtering.',
1149
- {
1150
- limit: z
1151
- .number()
1152
- .min(1)
1153
- .max(100)
1154
- .optional()
1155
- .describe('Number of newsletters to retrieve (1-100). Default is all.'),
1156
- filter: z.string().optional().describe('Ghost NQL filter string for advanced filtering.'),
1157
- },
1158
- async (input) => {
1298
+ newsletterQuerySchema,
1299
+ async (rawInput) => {
1300
+ const validation = validateToolInput(newsletterQuerySchema, rawInput, 'ghost_get_newsletters');
1301
+ if (!validation.success) {
1302
+ return validation.errorResponse;
1303
+ }
1304
+ const input = validation.data;
1305
+
1159
1306
  console.error(`Executing tool: ghost_get_newsletters`);
1160
1307
  try {
1161
1308
  await loadServices();
1162
1309
 
1163
1310
  const options = {};
1164
1311
  if (input.limit !== undefined) options.limit = input.limit;
1312
+ if (input.page !== undefined) options.page = input.page;
1165
1313
  if (input.filter !== undefined) options.filter = input.filter;
1314
+ if (input.order !== undefined) options.order = input.order;
1166
1315
 
1167
1316
  const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1168
1317
  const newsletters = await ghostServiceImproved.getNewsletters(options);
@@ -1173,6 +1322,13 @@ server.tool(
1173
1322
  };
1174
1323
  } catch (error) {
1175
1324
  console.error(`Error in ghost_get_newsletters:`, error);
1325
+ if (error.name === 'ZodError') {
1326
+ const validationError = ValidationError.fromZod(error, 'Newsletter query');
1327
+ return {
1328
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1329
+ isError: true,
1330
+ };
1331
+ }
1176
1332
  return {
1177
1333
  content: [{ type: 'text', text: `Error retrieving newsletters: ${error.message}` }],
1178
1334
  isError: true,
@@ -1185,10 +1341,14 @@ server.tool(
1185
1341
  server.tool(
1186
1342
  'ghost_get_newsletter',
1187
1343
  'Retrieves a single newsletter from Ghost CMS by ID.',
1188
- {
1189
- id: z.string().describe('The ID of the newsletter to retrieve.'),
1190
- },
1191
- async ({ id }) => {
1344
+ getNewsletterSchema,
1345
+ async (rawInput) => {
1346
+ const validation = validateToolInput(getNewsletterSchema, rawInput, 'ghost_get_newsletter');
1347
+ if (!validation.success) {
1348
+ return validation.errorResponse;
1349
+ }
1350
+ const { id } = validation.data;
1351
+
1192
1352
  console.error(`Executing tool: ghost_get_newsletter for ID: ${id}`);
1193
1353
  try {
1194
1354
  await loadServices();
@@ -1202,6 +1362,13 @@ server.tool(
1202
1362
  };
1203
1363
  } catch (error) {
1204
1364
  console.error(`Error in ghost_get_newsletter:`, error);
1365
+ if (error.name === 'ZodError') {
1366
+ const validationError = ValidationError.fromZod(error, 'Newsletter retrieval');
1367
+ return {
1368
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1369
+ isError: true,
1370
+ };
1371
+ }
1205
1372
  return {
1206
1373
  content: [{ type: 'text', text: `Error retrieving newsletter: ${error.message}` }],
1207
1374
  isError: true,
@@ -1214,33 +1381,18 @@ server.tool(
1214
1381
  server.tool(
1215
1382
  'ghost_create_newsletter',
1216
1383
  'Creates a new newsletter in Ghost CMS with customizable sender settings and display options.',
1217
- {
1218
- name: z.string().describe('The name of the newsletter.'),
1219
- description: z.string().optional().describe('A description for the newsletter.'),
1220
- sender_name: z.string().optional().describe('The sender name for newsletter emails.'),
1221
- sender_email: z
1222
- .string()
1223
- .email()
1224
- .optional()
1225
- .describe('The sender email address for newsletter emails.'),
1226
- sender_reply_to: z
1227
- .enum(['newsletter', 'support'])
1228
- .optional()
1229
- .describe('Reply-to address setting. Options: newsletter, support.'),
1230
- subscribe_on_signup: z
1231
- .boolean()
1232
- .optional()
1233
- .describe('Whether new members are automatically subscribed to this newsletter on signup.'),
1234
- show_header_icon: z
1235
- .boolean()
1236
- .optional()
1237
- .describe('Whether to show the site icon in the newsletter header.'),
1238
- show_header_title: z
1239
- .boolean()
1240
- .optional()
1241
- .describe('Whether to show the site title in the newsletter header.'),
1242
- },
1243
- async (input) => {
1384
+ createNewsletterSchema,
1385
+ async (rawInput) => {
1386
+ const validation = validateToolInput(
1387
+ createNewsletterSchema,
1388
+ rawInput,
1389
+ 'ghost_create_newsletter'
1390
+ );
1391
+ if (!validation.success) {
1392
+ return validation.errorResponse;
1393
+ }
1394
+ const input = validation.data;
1395
+
1244
1396
  console.error(`Executing tool: ghost_create_newsletter with name: ${input.name}`);
1245
1397
  try {
1246
1398
  await loadServices();
@@ -1254,6 +1406,13 @@ server.tool(
1254
1406
  };
1255
1407
  } catch (error) {
1256
1408
  console.error(`Error in ghost_create_newsletter:`, error);
1409
+ if (error.name === 'ZodError') {
1410
+ const validationError = ValidationError.fromZod(error, 'Newsletter creation');
1411
+ return {
1412
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1413
+ isError: true,
1414
+ };
1415
+ }
1257
1416
  return {
1258
1417
  content: [{ type: 'text', text: `Error creating newsletter: ${error.message}` }],
1259
1418
  isError: true,
@@ -1266,18 +1425,18 @@ server.tool(
1266
1425
  server.tool(
1267
1426
  'ghost_update_newsletter',
1268
1427
  'Updates an existing newsletter in Ghost CMS. Can update name, description, sender settings, and display options.',
1269
- {
1270
- id: z.string().describe('The ID of the newsletter to update.'),
1271
- name: z.string().optional().describe('New name for the newsletter.'),
1272
- description: z.string().optional().describe('New description for the newsletter.'),
1273
- sender_name: z.string().optional().describe('New sender name for newsletter emails.'),
1274
- sender_email: z.string().email().optional().describe('New sender email address.'),
1275
- subscribe_on_signup: z
1276
- .boolean()
1277
- .optional()
1278
- .describe('Whether new members are automatically subscribed to this newsletter on signup.'),
1279
- },
1280
- async (input) => {
1428
+ updateNewsletterInputSchema,
1429
+ async (rawInput) => {
1430
+ const validation = validateToolInput(
1431
+ updateNewsletterInputSchema,
1432
+ rawInput,
1433
+ 'ghost_update_newsletter'
1434
+ );
1435
+ if (!validation.success) {
1436
+ return validation.errorResponse;
1437
+ }
1438
+ const input = validation.data;
1439
+
1281
1440
  console.error(`Executing tool: ghost_update_newsletter for newsletter ID: ${input.id}`);
1282
1441
  try {
1283
1442
  await loadServices();
@@ -1293,6 +1452,13 @@ server.tool(
1293
1452
  };
1294
1453
  } catch (error) {
1295
1454
  console.error(`Error in ghost_update_newsletter:`, error);
1455
+ if (error.name === 'ZodError') {
1456
+ const validationError = ValidationError.fromZod(error, 'Newsletter update');
1457
+ return {
1458
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1459
+ isError: true,
1460
+ };
1461
+ }
1296
1462
  return {
1297
1463
  content: [{ type: 'text', text: `Error updating newsletter: ${error.message}` }],
1298
1464
  isError: true,
@@ -1305,10 +1471,18 @@ server.tool(
1305
1471
  server.tool(
1306
1472
  'ghost_delete_newsletter',
1307
1473
  'Deletes a newsletter from Ghost CMS by ID. This operation is permanent and cannot be undone.',
1308
- {
1309
- id: z.string().describe('The ID of the newsletter to delete.'),
1310
- },
1311
- async ({ id }) => {
1474
+ deleteNewsletterSchema,
1475
+ async (rawInput) => {
1476
+ const validation = validateToolInput(
1477
+ deleteNewsletterSchema,
1478
+ rawInput,
1479
+ 'ghost_delete_newsletter'
1480
+ );
1481
+ if (!validation.success) {
1482
+ return validation.errorResponse;
1483
+ }
1484
+ const { id } = validation.data;
1485
+
1312
1486
  console.error(`Executing tool: ghost_delete_newsletter for newsletter ID: ${id}`);
1313
1487
  try {
1314
1488
  await loadServices();
@@ -1322,6 +1496,13 @@ server.tool(
1322
1496
  };
1323
1497
  } catch (error) {
1324
1498
  console.error(`Error in ghost_delete_newsletter:`, error);
1499
+ if (error.name === 'ZodError') {
1500
+ const validationError = ValidationError.fromZod(error, 'Newsletter deletion');
1501
+ return {
1502
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1503
+ isError: true,
1504
+ };
1505
+ }
1325
1506
  return {
1326
1507
  content: [{ type: 'text', text: `Error deleting newsletter: ${error.message}` }],
1327
1508
  isError: true,
@@ -1332,21 +1513,23 @@ server.tool(
1332
1513
 
1333
1514
  // --- Tier Tools ---
1334
1515
 
1516
+ // --- Tier Schema Definitions ---
1517
+ const getTierSchema = z.object({ id: ghostIdSchema });
1518
+ const updateTierInputSchema = z.object({ id: ghostIdSchema }).merge(updateTierSchema);
1519
+ const deleteTierSchema = z.object({ id: ghostIdSchema });
1520
+
1335
1521
  // Get Tiers Tool
1336
1522
  server.tool(
1337
1523
  'ghost_get_tiers',
1338
1524
  'Retrieves a list of tiers (membership levels) from Ghost CMS with optional filtering by type (free/paid).',
1339
- {
1340
- limit: z
1341
- .number()
1342
- .int()
1343
- .min(1)
1344
- .max(100)
1345
- .optional()
1346
- .describe('Number of tiers to return (1-100, default 15)'),
1347
- filter: z.string().optional().describe('NQL filter string (e.g., "type:paid" or "type:free")'),
1348
- },
1349
- async (input) => {
1525
+ tierQuerySchema,
1526
+ async (rawInput) => {
1527
+ const validation = validateToolInput(tierQuerySchema, rawInput, 'ghost_get_tiers');
1528
+ if (!validation.success) {
1529
+ return validation.errorResponse;
1530
+ }
1531
+ const input = validation.data;
1532
+
1350
1533
  console.error(`Executing tool: ghost_get_tiers`);
1351
1534
  try {
1352
1535
  await loadServices();
@@ -1360,6 +1543,13 @@ server.tool(
1360
1543
  };
1361
1544
  } catch (error) {
1362
1545
  console.error(`Error in ghost_get_tiers:`, error);
1546
+ if (error.name === 'ZodError') {
1547
+ const validationError = ValidationError.fromZod(error, 'Tier query');
1548
+ return {
1549
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1550
+ isError: true,
1551
+ };
1552
+ }
1363
1553
  return {
1364
1554
  content: [{ type: 'text', text: `Error getting tiers: ${error.message}` }],
1365
1555
  isError: true,
@@ -1372,10 +1562,14 @@ server.tool(
1372
1562
  server.tool(
1373
1563
  'ghost_get_tier',
1374
1564
  'Retrieves a single tier (membership level) from Ghost CMS by ID.',
1375
- {
1376
- id: z.string().describe('The ID of the tier to retrieve.'),
1377
- },
1378
- async ({ id }) => {
1565
+ getTierSchema,
1566
+ async (rawInput) => {
1567
+ const validation = validateToolInput(getTierSchema, rawInput, 'ghost_get_tier');
1568
+ if (!validation.success) {
1569
+ return validation.errorResponse;
1570
+ }
1571
+ const { id } = validation.data;
1572
+
1379
1573
  console.error(`Executing tool: ghost_get_tier for tier ID: ${id}`);
1380
1574
  try {
1381
1575
  await loadServices();
@@ -1389,6 +1583,13 @@ server.tool(
1389
1583
  };
1390
1584
  } catch (error) {
1391
1585
  console.error(`Error in ghost_get_tier:`, error);
1586
+ if (error.name === 'ZodError') {
1587
+ const validationError = ValidationError.fromZod(error, 'Tier retrieval');
1588
+ return {
1589
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1590
+ isError: true,
1591
+ };
1592
+ }
1392
1593
  return {
1393
1594
  content: [{ type: 'text', text: `Error getting tier: ${error.message}` }],
1394
1595
  isError: true,
@@ -1401,26 +1602,14 @@ server.tool(
1401
1602
  server.tool(
1402
1603
  'ghost_create_tier',
1403
1604
  'Creates a new tier (membership level) in Ghost CMS with pricing and benefits.',
1404
- {
1405
- name: z.string().describe('Tier name (required)'),
1406
- description: z.string().optional().describe('Tier description'),
1407
- monthly_price: z
1408
- .number()
1409
- .int()
1410
- .min(0)
1411
- .optional()
1412
- .describe('Monthly price in cents (e.g., 500 = $5.00)'),
1413
- yearly_price: z
1414
- .number()
1415
- .int()
1416
- .min(0)
1417
- .optional()
1418
- .describe('Yearly price in cents (e.g., 5000 = $50.00)'),
1419
- currency: z.string().length(3).optional().describe('Currency code (e.g., "USD", "EUR")'),
1420
- benefits: z.array(z.string()).optional().describe('Array of benefit descriptions'),
1421
- welcome_page_url: z.string().url().optional().describe('Welcome page URL for new subscribers'),
1422
- },
1423
- async (input) => {
1605
+ createTierSchema,
1606
+ async (rawInput) => {
1607
+ const validation = validateToolInput(createTierSchema, rawInput, 'ghost_create_tier');
1608
+ if (!validation.success) {
1609
+ return validation.errorResponse;
1610
+ }
1611
+ const input = validation.data;
1612
+
1424
1613
  console.error(`Executing tool: ghost_create_tier`);
1425
1614
  try {
1426
1615
  await loadServices();
@@ -1434,6 +1623,13 @@ server.tool(
1434
1623
  };
1435
1624
  } catch (error) {
1436
1625
  console.error(`Error in ghost_create_tier:`, error);
1626
+ if (error.name === 'ZodError') {
1627
+ const validationError = ValidationError.fromZod(error, 'Tier creation');
1628
+ return {
1629
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1630
+ isError: true,
1631
+ };
1632
+ }
1437
1633
  return {
1438
1634
  content: [{ type: 'text', text: `Error creating tier: ${error.message}` }],
1439
1635
  isError: true,
@@ -1446,16 +1642,14 @@ server.tool(
1446
1642
  server.tool(
1447
1643
  'ghost_update_tier',
1448
1644
  'Updates an existing tier (membership level) in Ghost CMS. Can update pricing, benefits, and other tier properties.',
1449
- {
1450
- id: z.string().describe('The ID of the tier to update (required)'),
1451
- name: z.string().optional().describe('Updated tier name'),
1452
- description: z.string().optional().describe('Updated description'),
1453
- monthly_price: z.number().int().min(0).optional().describe('Updated monthly price in cents'),
1454
- yearly_price: z.number().int().min(0).optional().describe('Updated yearly price in cents'),
1455
- currency: z.string().length(3).optional().describe('Updated currency code'),
1456
- benefits: z.array(z.string()).optional().describe('Updated array of benefit descriptions'),
1457
- },
1458
- async (input) => {
1645
+ updateTierInputSchema,
1646
+ async (rawInput) => {
1647
+ const validation = validateToolInput(updateTierInputSchema, rawInput, 'ghost_update_tier');
1648
+ if (!validation.success) {
1649
+ return validation.errorResponse;
1650
+ }
1651
+ const input = validation.data;
1652
+
1459
1653
  console.error(`Executing tool: ghost_update_tier for tier ID: ${input.id}`);
1460
1654
  try {
1461
1655
  await loadServices();
@@ -1471,6 +1665,13 @@ server.tool(
1471
1665
  };
1472
1666
  } catch (error) {
1473
1667
  console.error(`Error in ghost_update_tier:`, error);
1668
+ if (error.name === 'ZodError') {
1669
+ const validationError = ValidationError.fromZod(error, 'Tier update');
1670
+ return {
1671
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1672
+ isError: true,
1673
+ };
1674
+ }
1474
1675
  return {
1475
1676
  content: [{ type: 'text', text: `Error updating tier: ${error.message}` }],
1476
1677
  isError: true,
@@ -1483,10 +1684,14 @@ server.tool(
1483
1684
  server.tool(
1484
1685
  'ghost_delete_tier',
1485
1686
  'Deletes a tier (membership level) from Ghost CMS by ID. This operation is permanent and cannot be undone.',
1486
- {
1487
- id: z.string().describe('The ID of the tier to delete.'),
1488
- },
1489
- async ({ id }) => {
1687
+ deleteTierSchema,
1688
+ async (rawInput) => {
1689
+ const validation = validateToolInput(deleteTierSchema, rawInput, 'ghost_delete_tier');
1690
+ if (!validation.success) {
1691
+ return validation.errorResponse;
1692
+ }
1693
+ const { id } = validation.data;
1694
+
1490
1695
  console.error(`Executing tool: ghost_delete_tier for tier ID: ${id}`);
1491
1696
  try {
1492
1697
  await loadServices();
@@ -1500,6 +1705,13 @@ server.tool(
1500
1705
  };
1501
1706
  } catch (error) {
1502
1707
  console.error(`Error in ghost_delete_tier:`, error);
1708
+ if (error.name === 'ZodError') {
1709
+ const validationError = ValidationError.fromZod(error, 'Tier deletion');
1710
+ return {
1711
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1712
+ isError: true,
1713
+ };
1714
+ }
1503
1715
  return {
1504
1716
  content: [{ type: 'text', text: `Error deleting tier: ${error.message}` }],
1505
1717
  isError: true,