@jgardner04/ghost-mcp-server 1.11.0 → 1.12.1

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();
@@ -15,13 +40,17 @@ dotenv.config();
15
40
  // Lazy-loaded modules (to avoid Node.js v25 Buffer compatibility issues at startup)
16
41
  let ghostService = null;
17
42
  let postService = null;
43
+ let pageService = null;
44
+ let newsletterService = null;
18
45
  let imageProcessingService = null;
19
46
  let urlValidator = null;
20
47
 
21
48
  const loadServices = async () => {
22
49
  if (!ghostService) {
23
- ghostService = await import('./services/ghostService.js');
50
+ ghostService = await import('./services/ghostServiceImproved.js');
24
51
  postService = await import('./services/postService.js');
52
+ pageService = await import('./services/pageService.js');
53
+ newsletterService = await import('./services/newsletterService.js');
25
54
  imageProcessingService = await import('./services/imageProcessingService.js');
26
55
  urlValidator = await import('./utils/urlValidator.js');
27
56
  }
@@ -51,26 +80,37 @@ const server = new McpServer({
51
80
 
52
81
  // --- Register Tools ---
53
82
 
83
+ // --- Schema Definitions for Tools ---
84
+ const getTagsSchema = tagQuerySchema.partial();
85
+ const getTagSchema = z.object({
86
+ id: ghostIdSchema.optional().describe('The ID of the tag to retrieve.'),
87
+ slug: z.string().optional().describe('The slug of the tag to retrieve.'),
88
+ include: z.string().optional().describe('Additional resources to include (e.g., "count.posts").'),
89
+ });
90
+ const updateTagInputSchema = updateTagSchema.extend({ id: ghostIdSchema });
91
+ const deleteTagSchema = z.object({ id: ghostIdSchema });
92
+
54
93
  // Get Tags Tool
55
94
  server.tool(
56
95
  'ghost_get_tags',
57
96
  '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 }) => {
97
+ getTagsSchema,
98
+ async (rawInput) => {
99
+ const validation = validateToolInput(getTagsSchema, rawInput, 'ghost_get_tags');
100
+ if (!validation.success) {
101
+ return validation.errorResponse;
102
+ }
103
+ const input = validation.data;
104
+
65
105
  console.error(`Executing tool: ghost_get_tags`);
66
106
  try {
67
107
  await loadServices();
68
108
  const tags = await ghostService.getTags();
69
109
  let result = tags;
70
110
 
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).`);
111
+ if (input.name) {
112
+ result = tags.filter((tag) => tag.name.toLowerCase() === input.name.toLowerCase());
113
+ console.error(`Filtered tags by name "${input.name}". Found ${result.length} match(es).`);
74
114
  } else {
75
115
  console.error(`Retrieved ${tags.length} tags from Ghost.`);
76
116
  }
@@ -80,6 +120,13 @@ server.tool(
80
120
  };
81
121
  } catch (error) {
82
122
  console.error(`Error in ghost_get_tags:`, error);
123
+ if (error.name === 'ZodError') {
124
+ const validationError = ValidationError.fromZod(error, 'Tags retrieval');
125
+ return {
126
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
127
+ isError: true,
128
+ };
129
+ }
83
130
  return {
84
131
  content: [{ type: 'text', text: `Error: ${error.message}` }],
85
132
  isError: true,
@@ -92,21 +139,18 @@ server.tool(
92
139
  server.tool(
93
140
  'ghost_create_tag',
94
141
  '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}`);
142
+ createTagSchema,
143
+ async (rawInput) => {
144
+ const validation = validateToolInput(createTagSchema, rawInput, 'ghost_create_tag');
145
+ if (!validation.success) {
146
+ return validation.errorResponse;
147
+ }
148
+ const input = validation.data;
149
+
150
+ console.error(`Executing tool: ghost_create_tag with name: ${input.name}`);
107
151
  try {
108
152
  await loadServices();
109
- const createdTag = await ghostService.createTag({ name, description, slug });
153
+ const createdTag = await ghostService.createTag(input);
110
154
  console.error(`Tag created successfully. Tag ID: ${createdTag.id}`);
111
155
 
112
156
  return {
@@ -114,6 +158,13 @@ server.tool(
114
158
  };
115
159
  } catch (error) {
116
160
  console.error(`Error in ghost_create_tag:`, error);
161
+ if (error.name === 'ZodError') {
162
+ const validationError = ValidationError.fromZod(error, 'Tag creation');
163
+ return {
164
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
165
+ isError: true,
166
+ };
167
+ }
117
168
  return {
118
169
  content: [{ type: 'text', text: `Error: ${error.message}` }],
119
170
  isError: true,
@@ -126,15 +177,14 @@ server.tool(
126
177
  server.tool(
127
178
  'ghost_get_tag',
128
179
  '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 }) => {
180
+ getTagSchema,
181
+ async (rawInput) => {
182
+ const validation = validateToolInput(getTagSchema, rawInput, 'ghost_get_tag');
183
+ if (!validation.success) {
184
+ return validation.errorResponse;
185
+ }
186
+ const { id, slug, include } = validation.data;
187
+
138
188
  console.error(`Executing tool: ghost_get_tag`);
139
189
  try {
140
190
  if (!id && !slug) {
@@ -147,8 +197,7 @@ server.tool(
147
197
  const identifier = slug ? `slug/${slug}` : id;
148
198
  const options = include ? { include } : {};
149
199
 
150
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
151
- const tag = await ghostServiceImproved.getTag(identifier, options);
200
+ const tag = await ghostService.getTag(identifier, options);
152
201
  console.error(`Tag retrieved successfully. Tag ID: ${tag.id}`);
153
202
 
154
203
  return {
@@ -156,6 +205,13 @@ server.tool(
156
205
  };
157
206
  } catch (error) {
158
207
  console.error(`Error in ghost_get_tag:`, error);
208
+ if (error.name === 'ZodError') {
209
+ const validationError = ValidationError.fromZod(error, 'Tag retrieval');
210
+ return {
211
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
212
+ isError: true,
213
+ };
214
+ }
159
215
  return {
160
216
  content: [{ type: 'text', text: `Error: ${error.message}` }],
161
217
  isError: true,
@@ -168,35 +224,26 @@ server.tool(
168
224
  server.tool(
169
225
  'ghost_update_tag',
170
226
  '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}`);
227
+ updateTagInputSchema,
228
+ async (rawInput) => {
229
+ const validation = validateToolInput(updateTagInputSchema, rawInput, 'ghost_update_tag');
230
+ if (!validation.success) {
231
+ return validation.errorResponse;
232
+ }
233
+ const input = validation.data;
234
+
235
+ console.error(`Executing tool: ghost_update_tag for ID: ${input.id}`);
182
236
  try {
183
- if (!id) {
237
+ if (!input.id) {
184
238
  throw new Error('Tag ID is required');
185
239
  }
186
240
 
187
241
  await loadServices();
188
242
 
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;
197
-
198
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
199
- const updatedTag = await ghostServiceImproved.updateTag(id, updateData);
243
+ // Build update data object with only provided fields (exclude id from update data)
244
+ const { id, ...updateData } = input;
245
+
246
+ const updatedTag = await ghostService.updateTag(id, updateData);
200
247
  console.error(`Tag updated successfully. Tag ID: ${updatedTag.id}`);
201
248
 
202
249
  return {
@@ -204,6 +251,13 @@ server.tool(
204
251
  };
205
252
  } catch (error) {
206
253
  console.error(`Error in ghost_update_tag:`, error);
254
+ if (error.name === 'ZodError') {
255
+ const validationError = ValidationError.fromZod(error, 'Tag update');
256
+ return {
257
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
258
+ isError: true,
259
+ };
260
+ }
207
261
  return {
208
262
  content: [{ type: 'text', text: `Error: ${error.message}` }],
209
263
  isError: true,
@@ -216,10 +270,14 @@ server.tool(
216
270
  server.tool(
217
271
  'ghost_delete_tag',
218
272
  '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 }) => {
273
+ deleteTagSchema,
274
+ async (rawInput) => {
275
+ const validation = validateToolInput(deleteTagSchema, rawInput, 'ghost_delete_tag');
276
+ if (!validation.success) {
277
+ return validation.errorResponse;
278
+ }
279
+ const { id } = validation.data;
280
+
223
281
  console.error(`Executing tool: ghost_delete_tag for ID: ${id}`);
224
282
  try {
225
283
  if (!id) {
@@ -228,8 +286,7 @@ server.tool(
228
286
 
229
287
  await loadServices();
230
288
 
231
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
232
- await ghostServiceImproved.deleteTag(id);
289
+ await ghostService.deleteTag(id);
233
290
  console.error(`Tag deleted successfully. Tag ID: ${id}`);
234
291
 
235
292
  return {
@@ -237,6 +294,13 @@ server.tool(
237
294
  };
238
295
  } catch (error) {
239
296
  console.error(`Error in ghost_delete_tag:`, error);
297
+ if (error.name === 'ZodError') {
298
+ const validationError = ValidationError.fromZod(error, 'Tag deletion');
299
+ return {
300
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
301
+ isError: true,
302
+ };
303
+ }
240
304
  return {
241
305
  content: [{ type: 'text', text: `Error: ${error.message}` }],
242
306
  isError: true,
@@ -245,20 +309,27 @@ server.tool(
245
309
  }
246
310
  );
247
311
 
312
+ // --- Image Schema ---
313
+ const uploadImageSchema = z.object({
314
+ imageUrl: z.string().describe('The publicly accessible URL of the image to upload.'),
315
+ alt: z
316
+ .string()
317
+ .optional()
318
+ .describe('Alt text for the image. If omitted, a default will be generated from the filename.'),
319
+ });
320
+
248
321
  // Upload Image Tool
249
322
  server.tool(
250
323
  'ghost_upload_image',
251
324
  '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 }) => {
325
+ uploadImageSchema,
326
+ async (rawInput) => {
327
+ const validation = validateToolInput(uploadImageSchema, rawInput, 'ghost_upload_image');
328
+ if (!validation.success) {
329
+ return validation.errorResponse;
330
+ }
331
+ const { imageUrl, alt } = validation.data;
332
+
262
333
  console.error(`Executing tool: ghost_upload_image for URL: ${imageUrl}`);
263
334
  let downloadedPath = null;
264
335
  let processedPath = null;
@@ -288,10 +359,16 @@ server.tool(
288
359
  writer.on('finish', resolve);
289
360
  writer.on('error', reject);
290
361
  });
362
+ // Track temp file for cleanup on process exit
363
+ trackTempFile(downloadedPath);
291
364
  console.error(`Downloaded image to temporary path: ${downloadedPath}`);
292
365
 
293
366
  // 3. Process the image
294
367
  processedPath = await imageProcessingService.processImage(downloadedPath, tempDir);
368
+ // Track processed file for cleanup on process exit
369
+ if (processedPath !== downloadedPath) {
370
+ trackTempFile(processedPath);
371
+ }
295
372
  console.error(`Processed image path: ${processedPath}`);
296
373
 
297
374
  // 4. Determine Alt Text
@@ -319,63 +396,56 @@ server.tool(
319
396
  isError: true,
320
397
  };
321
398
  } 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
- }
399
+ // Cleanup temporary files with proper async/await
400
+ await cleanupTempFiles([downloadedPath, processedPath], console);
333
401
  }
334
402
  }
335
403
  );
336
404
 
405
+ // --- Post Schema Definitions ---
406
+ const getPostsSchema = postQuerySchema.extend({
407
+ status: z
408
+ .enum(['published', 'draft', 'scheduled', 'all'])
409
+ .optional()
410
+ .describe('Filter posts by status. Options: published, draft, scheduled, all.'),
411
+ });
412
+ const getPostSchema = z.object({
413
+ id: ghostIdSchema.optional().describe('The ID of the post to retrieve.'),
414
+ slug: z.string().optional().describe('The slug of the post to retrieve.'),
415
+ include: z
416
+ .string()
417
+ .optional()
418
+ .describe('Comma-separated list of relations to include (e.g., "tags,authors").'),
419
+ });
420
+ const searchPostsSchema = z.object({
421
+ query: z.string().min(1).describe('Search query to find in post titles.'),
422
+ status: z
423
+ .enum(['published', 'draft', 'scheduled', 'all'])
424
+ .optional()
425
+ .describe('Filter by post status. Default searches all statuses.'),
426
+ limit: z
427
+ .number()
428
+ .int()
429
+ .min(1)
430
+ .max(50)
431
+ .optional()
432
+ .describe('Maximum number of results (1-50). Default is 15.'),
433
+ });
434
+ const updatePostInputSchema = updatePostSchema.extend({ id: ghostIdSchema });
435
+ const deletePostSchema = z.object({ id: ghostIdSchema });
436
+
337
437
  // Create Post Tool
338
438
  server.tool(
339
439
  'ghost_create_post',
340
440
  '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) => {
441
+ createPostSchema,
442
+ async (rawInput) => {
443
+ const validation = validateToolInput(createPostSchema, rawInput, 'ghost_create_post');
444
+ if (!validation.success) {
445
+ return validation.errorResponse;
446
+ }
447
+ const input = validation.data;
448
+
379
449
  console.error(`Executing tool: ghost_create_post with title: ${input.title}`);
380
450
  try {
381
451
  await loadServices();
@@ -387,6 +457,13 @@ server.tool(
387
457
  };
388
458
  } catch (error) {
389
459
  console.error(`Error in ghost_create_post:`, error);
460
+ if (error.name === 'ZodError') {
461
+ const validationError = ValidationError.fromZod(error, 'Post creation');
462
+ return {
463
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
464
+ isError: true,
465
+ };
466
+ }
390
467
  return {
391
468
  content: [{ type: 'text', text: `Error creating post: ${error.message}` }],
392
469
  isError: true,
@@ -399,32 +476,14 @@ server.tool(
399
476
  server.tool(
400
477
  'ghost_get_posts',
401
478
  '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) => {
479
+ getPostsSchema,
480
+ async (rawInput) => {
481
+ const validation = validateToolInput(getPostsSchema, rawInput, 'ghost_get_posts');
482
+ if (!validation.success) {
483
+ return validation.errorResponse;
484
+ }
485
+ const input = validation.data;
486
+
428
487
  console.error(`Executing tool: ghost_get_posts`);
429
488
  try {
430
489
  await loadServices();
@@ -446,6 +505,13 @@ server.tool(
446
505
  };
447
506
  } catch (error) {
448
507
  console.error(`Error in ghost_get_posts:`, error);
508
+ if (error.name === 'ZodError') {
509
+ const validationError = ValidationError.fromZod(error, 'Posts retrieval');
510
+ return {
511
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
512
+ isError: true,
513
+ };
514
+ }
449
515
  return {
450
516
  content: [{ type: 'text', text: `Error retrieving posts: ${error.message}` }],
451
517
  isError: true,
@@ -458,15 +524,14 @@ server.tool(
458
524
  server.tool(
459
525
  'ghost_get_post',
460
526
  '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) => {
527
+ getPostSchema,
528
+ async (rawInput) => {
529
+ const validation = validateToolInput(getPostSchema, rawInput, 'ghost_get_post');
530
+ if (!validation.success) {
531
+ return validation.errorResponse;
532
+ }
533
+ const input = validation.data;
534
+
470
535
  console.error(`Executing tool: ghost_get_post`);
471
536
  try {
472
537
  // Validate that at least one of id or slug is provided
@@ -491,6 +556,13 @@ server.tool(
491
556
  };
492
557
  } catch (error) {
493
558
  console.error(`Error in ghost_get_post:`, error);
559
+ if (error.name === 'ZodError') {
560
+ const validationError = ValidationError.fromZod(error, 'Post retrieval');
561
+ return {
562
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
563
+ isError: true,
564
+ };
565
+ }
494
566
  return {
495
567
  content: [{ type: 'text', text: `Error retrieving post: ${error.message}` }],
496
568
  isError: true,
@@ -503,20 +575,14 @@ server.tool(
503
575
  server.tool(
504
576
  'ghost_search_posts',
505
577
  '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) => {
578
+ searchPostsSchema,
579
+ async (rawInput) => {
580
+ const validation = validateToolInput(searchPostsSchema, rawInput, 'ghost_search_posts');
581
+ if (!validation.success) {
582
+ return validation.errorResponse;
583
+ }
584
+ const input = validation.data;
585
+
520
586
  console.error(`Executing tool: ghost_search_posts with query: ${input.query}`);
521
587
  try {
522
588
  await loadServices();
@@ -526,9 +592,7 @@ server.tool(
526
592
  if (input.status !== undefined) options.status = input.status;
527
593
  if (input.limit !== undefined) options.limit = input.limit;
528
594
 
529
- // Search posts using ghostServiceImproved
530
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
531
- const posts = await ghostServiceImproved.searchPosts(input.query, options);
595
+ const posts = await ghostService.searchPosts(input.query, options);
532
596
  console.error(`Found ${posts.length} posts matching "${input.query}".`);
533
597
 
534
598
  return {
@@ -536,6 +600,13 @@ server.tool(
536
600
  };
537
601
  } catch (error) {
538
602
  console.error(`Error in ghost_search_posts:`, error);
603
+ if (error.name === 'ZodError') {
604
+ const validationError = ValidationError.fromZod(error, 'Post search');
605
+ return {
606
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
607
+ isError: true,
608
+ };
609
+ }
539
610
  return {
540
611
  content: [{ type: 'text', text: `Error searching posts: ${error.message}` }],
541
612
  isError: true,
@@ -548,35 +619,14 @@ server.tool(
548
619
  server.tool(
549
620
  'ghost_update_post',
550
621
  '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) => {
622
+ updatePostInputSchema,
623
+ async (rawInput) => {
624
+ const validation = validateToolInput(updatePostInputSchema, rawInput, 'ghost_update_post');
625
+ if (!validation.success) {
626
+ return validation.errorResponse;
627
+ }
628
+ const input = validation.data;
629
+
580
630
  console.error(`Executing tool: ghost_update_post for post ID: ${input.id}`);
581
631
  try {
582
632
  await loadServices();
@@ -584,9 +634,7 @@ server.tool(
584
634
  // Extract ID from input and build update data
585
635
  const { id, ...updateData } = input;
586
636
 
587
- // Update the post using ghostServiceImproved
588
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
589
- const updatedPost = await ghostServiceImproved.updatePost(id, updateData);
637
+ const updatedPost = await ghostService.updatePost(id, updateData);
590
638
  console.error(`Post updated successfully. Post ID: ${updatedPost.id}`);
591
639
 
592
640
  return {
@@ -594,6 +642,13 @@ server.tool(
594
642
  };
595
643
  } catch (error) {
596
644
  console.error(`Error in ghost_update_post:`, error);
645
+ if (error.name === 'ZodError') {
646
+ const validationError = ValidationError.fromZod(error, 'Post update');
647
+ return {
648
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
649
+ isError: true,
650
+ };
651
+ }
597
652
  return {
598
653
  content: [{ type: 'text', text: `Error updating post: ${error.message}` }],
599
654
  isError: true,
@@ -606,17 +661,19 @@ server.tool(
606
661
  server.tool(
607
662
  'ghost_delete_post',
608
663
  '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 }) => {
664
+ deletePostSchema,
665
+ async (rawInput) => {
666
+ const validation = validateToolInput(deletePostSchema, rawInput, 'ghost_delete_post');
667
+ if (!validation.success) {
668
+ return validation.errorResponse;
669
+ }
670
+ const { id } = validation.data;
671
+
613
672
  console.error(`Executing tool: ghost_delete_post for post ID: ${id}`);
614
673
  try {
615
674
  await loadServices();
616
675
 
617
- // Delete the post using ghostServiceImproved
618
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
619
- await ghostServiceImproved.deletePost(id);
676
+ await ghostService.deletePost(id);
620
677
  console.error(`Post deleted successfully. Post ID: ${id}`);
621
678
 
622
679
  return {
@@ -624,6 +681,13 @@ server.tool(
624
681
  };
625
682
  } catch (error) {
626
683
  console.error(`Error in ghost_delete_post:`, error);
684
+ if (error.name === 'ZodError') {
685
+ const validationError = ValidationError.fromZod(error, 'Post deletion');
686
+ return {
687
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
688
+ isError: true,
689
+ };
690
+ }
627
691
  return {
628
692
  content: [{ type: 'text', text: `Error deleting post: ${error.message}` }],
629
693
  isError: true,
@@ -637,30 +701,54 @@ server.tool(
637
701
  // Pages are similar to posts but do NOT support tags
638
702
  // =============================================================================
639
703
 
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.'),
704
+ // --- Page Schema Definitions ---
705
+ const getPageSchema = z
706
+ .object({
707
+ id: ghostIdSchema.optional().describe('The ID of the page to retrieve.'),
708
+ slug: z.string().optional().describe('The slug of the page to retrieve.'),
656
709
  include: z
657
710
  .string()
658
711
  .optional()
659
712
  .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) => {
713
+ })
714
+ .refine((data) => data.id || data.slug, {
715
+ message: 'Either id or slug is required to retrieve a page',
716
+ });
717
+ const updatePageInputSchema = z
718
+ .object({ id: ghostIdSchema.describe('The ID of the page to update.') })
719
+ .merge(updatePageSchema);
720
+ const deletePageSchema = z.object({ id: ghostIdSchema.describe('The ID of the page to delete.') });
721
+ const searchPagesSchema = z.object({
722
+ query: z
723
+ .string()
724
+ .min(1, 'Search query cannot be empty')
725
+ .describe('Search query to find in page titles.'),
726
+ status: z
727
+ .enum(['published', 'draft', 'scheduled', 'all'])
728
+ .optional()
729
+ .describe('Filter by page status. Default searches all statuses.'),
730
+ limit: z
731
+ .number()
732
+ .int()
733
+ .min(1)
734
+ .max(50)
735
+ .default(15)
736
+ .optional()
737
+ .describe('Maximum number of results (1-50). Default is 15.'),
738
+ });
739
+
740
+ // Get Pages Tool
741
+ server.tool(
742
+ 'ghost_get_pages',
743
+ 'Retrieves a list of pages from Ghost CMS with pagination, filtering, and sorting options.',
744
+ pageQuerySchema,
745
+ async (rawInput) => {
746
+ const validation = validateToolInput(pageQuerySchema, rawInput, 'ghost_get_pages');
747
+ if (!validation.success) {
748
+ return validation.errorResponse;
749
+ }
750
+ const input = validation.data;
751
+
664
752
  console.error(`Executing tool: ghost_get_pages`);
665
753
  try {
666
754
  await loadServices();
@@ -668,13 +756,13 @@ server.tool(
668
756
  const options = {};
669
757
  if (input.limit !== undefined) options.limit = input.limit;
670
758
  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
759
  if (input.filter !== undefined) options.filter = input.filter;
760
+ if (input.include !== undefined) options.include = input.include;
761
+ if (input.fields !== undefined) options.fields = input.fields;
762
+ if (input.formats !== undefined) options.formats = input.formats;
674
763
  if (input.order !== undefined) options.order = input.order;
675
764
 
676
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
677
- const pages = await ghostServiceImproved.getPages(options);
765
+ const pages = await ghostService.getPages(options);
678
766
  console.error(`Retrieved ${pages.length} pages from Ghost.`);
679
767
 
680
768
  return {
@@ -682,6 +770,13 @@ server.tool(
682
770
  };
683
771
  } catch (error) {
684
772
  console.error(`Error in ghost_get_pages:`, error);
773
+ if (error.name === 'ZodError') {
774
+ const validationError = ValidationError.fromZod(error, 'Page query');
775
+ return {
776
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
777
+ isError: true,
778
+ };
779
+ }
685
780
  return {
686
781
  content: [{ type: 'text', text: `Error retrieving pages: ${error.message}` }],
687
782
  isError: true,
@@ -694,21 +789,16 @@ server.tool(
694
789
  server.tool(
695
790
  'ghost_get_page',
696
791
  '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) => {
792
+ getPageSchema,
793
+ async (rawInput) => {
794
+ const validation = validateToolInput(getPageSchema, rawInput, 'ghost_get_page');
795
+ if (!validation.success) {
796
+ return validation.errorResponse;
797
+ }
798
+ const input = validation.data;
799
+
706
800
  console.error(`Executing tool: ghost_get_page`);
707
801
  try {
708
- if (!input.id && !input.slug) {
709
- throw new Error('Either id or slug is required to retrieve a page');
710
- }
711
-
712
802
  await loadServices();
713
803
 
714
804
  const options = {};
@@ -716,8 +806,7 @@ server.tool(
716
806
 
717
807
  const identifier = input.id || `slug/${input.slug}`;
718
808
 
719
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
720
- const page = await ghostServiceImproved.getPage(identifier, options);
809
+ const page = await ghostService.getPage(identifier, options);
721
810
  console.error(`Retrieved page: ${page.title} (ID: ${page.id})`);
722
811
 
723
812
  return {
@@ -725,6 +814,13 @@ server.tool(
725
814
  };
726
815
  } catch (error) {
727
816
  console.error(`Error in ghost_get_page:`, error);
817
+ if (error.name === 'ZodError') {
818
+ const validationError = ValidationError.fromZod(error, 'Get page');
819
+ return {
820
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
821
+ isError: true,
822
+ };
823
+ }
728
824
  return {
729
825
  content: [{ type: 'text', text: `Error retrieving page: ${error.message}` }],
730
826
  isError: true,
@@ -736,40 +832,19 @@ server.tool(
736
832
  // Create Page Tool
737
833
  server.tool(
738
834
  '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) => {
835
+ 'Creates a new page in Ghost CMS. Note: Pages do NOT typically use tags (unlike posts).',
836
+ createPageSchema,
837
+ async (rawInput) => {
838
+ const validation = validateToolInput(createPageSchema, rawInput, 'ghost_create_page');
839
+ if (!validation.success) {
840
+ return validation.errorResponse;
841
+ }
842
+ const input = validation.data;
843
+
768
844
  console.error(`Executing tool: ghost_create_page with title: ${input.title}`);
769
845
  try {
770
846
  await loadServices();
771
847
 
772
- const pageService = await import('./services/pageService.js');
773
848
  const createdPage = await pageService.createPageService(input);
774
849
  console.error(`Page created successfully. Page ID: ${createdPage.id}`);
775
850
 
@@ -778,6 +853,13 @@ server.tool(
778
853
  };
779
854
  } catch (error) {
780
855
  console.error(`Error in ghost_create_page:`, error);
856
+ if (error.name === 'ZodError') {
857
+ const validationError = ValidationError.fromZod(error, 'Page creation');
858
+ return {
859
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
860
+ isError: true,
861
+ };
862
+ }
781
863
  return {
782
864
  content: [{ type: 'text', text: `Error creating page: ${error.message}` }],
783
865
  isError: true,
@@ -789,33 +871,22 @@ server.tool(
789
871
  // Update Page Tool
790
872
  server.tool(
791
873
  '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) => {
874
+ 'Updates an existing page in Ghost CMS. Can update title, content, status, images, and SEO fields.',
875
+ updatePageInputSchema,
876
+ async (rawInput) => {
877
+ const validation = validateToolInput(updatePageInputSchema, rawInput, 'ghost_update_page');
878
+ if (!validation.success) {
879
+ return validation.errorResponse;
880
+ }
881
+ const input = validation.data;
882
+
811
883
  console.error(`Executing tool: ghost_update_page for page ID: ${input.id}`);
812
884
  try {
813
885
  await loadServices();
814
886
 
815
887
  const { id, ...updateData } = input;
816
888
 
817
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
818
- const updatedPage = await ghostServiceImproved.updatePage(id, updateData);
889
+ const updatedPage = await ghostService.updatePage(id, updateData);
819
890
  console.error(`Page updated successfully. Page ID: ${updatedPage.id}`);
820
891
 
821
892
  return {
@@ -823,6 +894,13 @@ server.tool(
823
894
  };
824
895
  } catch (error) {
825
896
  console.error(`Error in ghost_update_page:`, error);
897
+ if (error.name === 'ZodError') {
898
+ const validationError = ValidationError.fromZod(error, 'Page update');
899
+ return {
900
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
901
+ isError: true,
902
+ };
903
+ }
826
904
  return {
827
905
  content: [{ type: 'text', text: `Error updating page: ${error.message}` }],
828
906
  isError: true,
@@ -835,16 +913,19 @@ server.tool(
835
913
  server.tool(
836
914
  'ghost_delete_page',
837
915
  '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 }) => {
916
+ deletePageSchema,
917
+ async (rawInput) => {
918
+ const validation = validateToolInput(deletePageSchema, rawInput, 'ghost_delete_page');
919
+ if (!validation.success) {
920
+ return validation.errorResponse;
921
+ }
922
+ const { id } = validation.data;
923
+
842
924
  console.error(`Executing tool: ghost_delete_page for page ID: ${id}`);
843
925
  try {
844
926
  await loadServices();
845
927
 
846
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
847
- await ghostServiceImproved.deletePage(id);
928
+ await ghostService.deletePage(id);
848
929
  console.error(`Page deleted successfully. Page ID: ${id}`);
849
930
 
850
931
  return {
@@ -852,6 +933,13 @@ server.tool(
852
933
  };
853
934
  } catch (error) {
854
935
  console.error(`Error in ghost_delete_page:`, error);
936
+ if (error.name === 'ZodError') {
937
+ const validationError = ValidationError.fromZod(error, 'Page deletion');
938
+ return {
939
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
940
+ isError: true,
941
+ };
942
+ }
855
943
  return {
856
944
  content: [{ type: 'text', text: `Error deleting page: ${error.message}` }],
857
945
  isError: true,
@@ -864,20 +952,14 @@ server.tool(
864
952
  server.tool(
865
953
  'ghost_search_pages',
866
954
  '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) => {
955
+ searchPagesSchema,
956
+ async (rawInput) => {
957
+ const validation = validateToolInput(searchPagesSchema, rawInput, 'ghost_search_pages');
958
+ if (!validation.success) {
959
+ return validation.errorResponse;
960
+ }
961
+ const input = validation.data;
962
+
881
963
  console.error(`Executing tool: ghost_search_pages with query: ${input.query}`);
882
964
  try {
883
965
  await loadServices();
@@ -886,8 +968,7 @@ server.tool(
886
968
  if (input.status !== undefined) options.status = input.status;
887
969
  if (input.limit !== undefined) options.limit = input.limit;
888
970
 
889
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
890
- const pages = await ghostServiceImproved.searchPages(input.query, options);
971
+ const pages = await ghostService.searchPages(input.query, options);
891
972
  console.error(`Found ${pages.length} pages matching "${input.query}".`);
892
973
 
893
974
  return {
@@ -895,6 +976,13 @@ server.tool(
895
976
  };
896
977
  } catch (error) {
897
978
  console.error(`Error in ghost_search_pages:`, error);
979
+ if (error.name === 'ZodError') {
980
+ const validationError = ValidationError.fromZod(error, 'Page search');
981
+ return {
982
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
983
+ isError: true,
984
+ };
985
+ }
898
986
  return {
899
987
  content: [{ type: 'text', text: `Error searching pages: ${error.message}` }],
900
988
  isError: true,
@@ -908,31 +996,46 @@ server.tool(
908
996
  // Member management for Ghost CMS subscribers
909
997
  // =============================================================================
910
998
 
999
+ // --- Member Schema Definitions ---
1000
+ const updateMemberInputSchema = z.object({ id: ghostIdSchema }).merge(updateMemberSchema);
1001
+ const deleteMemberSchema = z.object({ id: ghostIdSchema });
1002
+ const getMembersSchema = memberQuerySchema.omit({ search: true });
1003
+ const getMemberSchema = z
1004
+ .object({
1005
+ id: ghostIdSchema.optional().describe('The ID of the member to retrieve.'),
1006
+ email: emailSchema.optional().describe('The email of the member to retrieve.'),
1007
+ })
1008
+ .refine((data) => data.id || data.email, {
1009
+ message: 'Either id or email must be provided',
1010
+ });
1011
+ const searchMembersSchema = z.object({
1012
+ query: z.string().min(1).describe('Search query to match against member name or email.'),
1013
+ limit: z
1014
+ .number()
1015
+ .int()
1016
+ .min(1)
1017
+ .max(50)
1018
+ .optional()
1019
+ .describe('Maximum number of results to return (1-50). Default is 15.'),
1020
+ });
1021
+
911
1022
  // Create Member Tool
912
1023
  server.tool(
913
1024
  'ghost_create_member',
914
1025
  '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) => {
1026
+ createMemberSchema,
1027
+ async (rawInput) => {
1028
+ const validation = validateToolInput(createMemberSchema, rawInput, 'ghost_create_member');
1029
+ if (!validation.success) {
1030
+ return validation.errorResponse;
1031
+ }
1032
+ const input = validation.data;
1033
+
930
1034
  console.error(`Executing tool: ghost_create_member with email: ${input.email}`);
931
1035
  try {
932
1036
  await loadServices();
933
1037
 
934
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
935
- const createdMember = await ghostServiceImproved.createMember(input);
1038
+ const createdMember = await ghostService.createMember(input);
936
1039
  console.error(`Member created successfully. Member ID: ${createdMember.id}`);
937
1040
 
938
1041
  return {
@@ -940,6 +1043,13 @@ server.tool(
940
1043
  };
941
1044
  } catch (error) {
942
1045
  console.error(`Error in ghost_create_member:`, error);
1046
+ if (error.name === 'ZodError') {
1047
+ const validationError = ValidationError.fromZod(error, 'Member creation');
1048
+ return {
1049
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1050
+ isError: true,
1051
+ };
1052
+ }
943
1053
  return {
944
1054
  content: [{ type: 'text', text: `Error creating member: ${error.message}` }],
945
1055
  isError: true,
@@ -952,29 +1062,21 @@ server.tool(
952
1062
  server.tool(
953
1063
  'ghost_update_member',
954
1064
  '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) => {
1065
+ updateMemberInputSchema,
1066
+ async (rawInput) => {
1067
+ const validation = validateToolInput(updateMemberInputSchema, rawInput, 'ghost_update_member');
1068
+ if (!validation.success) {
1069
+ return validation.errorResponse;
1070
+ }
1071
+ const input = validation.data;
1072
+
970
1073
  console.error(`Executing tool: ghost_update_member for member ID: ${input.id}`);
971
1074
  try {
972
1075
  await loadServices();
973
1076
 
974
1077
  const { id, ...updateData } = input;
975
1078
 
976
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
977
- const updatedMember = await ghostServiceImproved.updateMember(id, updateData);
1079
+ const updatedMember = await ghostService.updateMember(id, updateData);
978
1080
  console.error(`Member updated successfully. Member ID: ${updatedMember.id}`);
979
1081
 
980
1082
  return {
@@ -982,6 +1084,13 @@ server.tool(
982
1084
  };
983
1085
  } catch (error) {
984
1086
  console.error(`Error in ghost_update_member:`, error);
1087
+ if (error.name === 'ZodError') {
1088
+ const validationError = ValidationError.fromZod(error, 'Member update');
1089
+ return {
1090
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1091
+ isError: true,
1092
+ };
1093
+ }
985
1094
  return {
986
1095
  content: [{ type: 'text', text: `Error updating member: ${error.message}` }],
987
1096
  isError: true,
@@ -994,16 +1103,19 @@ server.tool(
994
1103
  server.tool(
995
1104
  'ghost_delete_member',
996
1105
  '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 }) => {
1106
+ deleteMemberSchema,
1107
+ async (rawInput) => {
1108
+ const validation = validateToolInput(deleteMemberSchema, rawInput, 'ghost_delete_member');
1109
+ if (!validation.success) {
1110
+ return validation.errorResponse;
1111
+ }
1112
+ const { id } = validation.data;
1113
+
1001
1114
  console.error(`Executing tool: ghost_delete_member for member ID: ${id}`);
1002
1115
  try {
1003
1116
  await loadServices();
1004
1117
 
1005
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1006
- await ghostServiceImproved.deleteMember(id);
1118
+ await ghostService.deleteMember(id);
1007
1119
  console.error(`Member deleted successfully. Member ID: ${id}`);
1008
1120
 
1009
1121
  return {
@@ -1011,6 +1123,13 @@ server.tool(
1011
1123
  };
1012
1124
  } catch (error) {
1013
1125
  console.error(`Error in ghost_delete_member:`, error);
1126
+ if (error.name === 'ZodError') {
1127
+ const validationError = ValidationError.fromZod(error, 'Member deletion');
1128
+ return {
1129
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1130
+ isError: true,
1131
+ };
1132
+ }
1014
1133
  return {
1015
1134
  content: [{ type: 'text', text: `Error deleting member: ${error.message}` }],
1016
1135
  isError: true,
@@ -1023,25 +1142,14 @@ server.tool(
1023
1142
  server.tool(
1024
1143
  'ghost_get_members',
1025
1144
  '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) => {
1145
+ getMembersSchema,
1146
+ async (rawInput) => {
1147
+ const validation = validateToolInput(getMembersSchema, rawInput, 'ghost_get_members');
1148
+ if (!validation.success) {
1149
+ return validation.errorResponse;
1150
+ }
1151
+ const input = validation.data;
1152
+
1045
1153
  console.error(`Executing tool: ghost_get_members`);
1046
1154
  try {
1047
1155
  await loadServices();
@@ -1053,8 +1161,7 @@ server.tool(
1053
1161
  if (input.order !== undefined) options.order = input.order;
1054
1162
  if (input.include !== undefined) options.include = input.include;
1055
1163
 
1056
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1057
- const members = await ghostServiceImproved.getMembers(options);
1164
+ const members = await ghostService.getMembers(options);
1058
1165
  console.error(`Retrieved ${members.length} members from Ghost.`);
1059
1166
 
1060
1167
  return {
@@ -1062,6 +1169,13 @@ server.tool(
1062
1169
  };
1063
1170
  } catch (error) {
1064
1171
  console.error(`Error in ghost_get_members:`, error);
1172
+ if (error.name === 'ZodError') {
1173
+ const validationError = ValidationError.fromZod(error, 'Member query');
1174
+ return {
1175
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1176
+ isError: true,
1177
+ };
1178
+ }
1065
1179
  return {
1066
1180
  content: [{ type: 'text', text: `Error retrieving members: ${error.message}` }],
1067
1181
  isError: true,
@@ -1074,17 +1188,19 @@ server.tool(
1074
1188
  server.tool(
1075
1189
  'ghost_get_member',
1076
1190
  '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 }) => {
1191
+ getMemberSchema,
1192
+ async (rawInput) => {
1193
+ const validation = validateToolInput(getMemberSchema, rawInput, 'ghost_get_member');
1194
+ if (!validation.success) {
1195
+ return validation.errorResponse;
1196
+ }
1197
+ const { id, email } = validation.data;
1198
+
1082
1199
  console.error(`Executing tool: ghost_get_member for ${id ? `ID: ${id}` : `email: ${email}`}`);
1083
1200
  try {
1084
1201
  await loadServices();
1085
1202
 
1086
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1087
- const member = await ghostServiceImproved.getMember({ id, email });
1203
+ const member = await ghostService.getMember({ id, email });
1088
1204
  console.error(`Retrieved member: ${member.email} (ID: ${member.id})`);
1089
1205
 
1090
1206
  return {
@@ -1092,6 +1208,13 @@ server.tool(
1092
1208
  };
1093
1209
  } catch (error) {
1094
1210
  console.error(`Error in ghost_get_member:`, error);
1211
+ if (error.name === 'ZodError') {
1212
+ const validationError = ValidationError.fromZod(error, 'Member lookup');
1213
+ return {
1214
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1215
+ isError: true,
1216
+ };
1217
+ }
1095
1218
  return {
1096
1219
  content: [{ type: 'text', text: `Error retrieving member: ${error.message}` }],
1097
1220
  isError: true,
@@ -1104,16 +1227,14 @@ server.tool(
1104
1227
  server.tool(
1105
1228
  'ghost_search_members',
1106
1229
  '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 }) => {
1230
+ searchMembersSchema,
1231
+ async (rawInput) => {
1232
+ const validation = validateToolInput(searchMembersSchema, rawInput, 'ghost_search_members');
1233
+ if (!validation.success) {
1234
+ return validation.errorResponse;
1235
+ }
1236
+ const { query, limit } = validation.data;
1237
+
1117
1238
  console.error(`Executing tool: ghost_search_members with query: ${query}`);
1118
1239
  try {
1119
1240
  await loadServices();
@@ -1121,8 +1242,7 @@ server.tool(
1121
1242
  const options = {};
1122
1243
  if (limit !== undefined) options.limit = limit;
1123
1244
 
1124
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1125
- const members = await ghostServiceImproved.searchMembers(query, options);
1245
+ const members = await ghostService.searchMembers(query, options);
1126
1246
  console.error(`Found ${members.length} members matching "${query}".`);
1127
1247
 
1128
1248
  return {
@@ -1130,6 +1250,13 @@ server.tool(
1130
1250
  };
1131
1251
  } catch (error) {
1132
1252
  console.error(`Error in ghost_search_members:`, error);
1253
+ if (error.name === 'ZodError') {
1254
+ const validationError = ValidationError.fromZod(error, 'Member search');
1255
+ return {
1256
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1257
+ isError: true,
1258
+ };
1259
+ }
1133
1260
  return {
1134
1261
  content: [{ type: 'text', text: `Error searching members: ${error.message}` }],
1135
1262
  isError: true,
@@ -1142,30 +1269,34 @@ server.tool(
1142
1269
  // NEWSLETTER TOOLS
1143
1270
  // =============================================================================
1144
1271
 
1272
+ // --- Newsletter Schema Definitions ---
1273
+ const getNewsletterSchema = z.object({ id: ghostIdSchema });
1274
+ const updateNewsletterInputSchema = z.object({ id: ghostIdSchema }).merge(updateNewsletterSchema);
1275
+ const deleteNewsletterSchema = z.object({ id: ghostIdSchema });
1276
+
1145
1277
  // Get Newsletters Tool
1146
1278
  server.tool(
1147
1279
  'ghost_get_newsletters',
1148
1280
  '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) => {
1281
+ newsletterQuerySchema,
1282
+ async (rawInput) => {
1283
+ const validation = validateToolInput(newsletterQuerySchema, rawInput, 'ghost_get_newsletters');
1284
+ if (!validation.success) {
1285
+ return validation.errorResponse;
1286
+ }
1287
+ const input = validation.data;
1288
+
1159
1289
  console.error(`Executing tool: ghost_get_newsletters`);
1160
1290
  try {
1161
1291
  await loadServices();
1162
1292
 
1163
1293
  const options = {};
1164
1294
  if (input.limit !== undefined) options.limit = input.limit;
1295
+ if (input.page !== undefined) options.page = input.page;
1165
1296
  if (input.filter !== undefined) options.filter = input.filter;
1297
+ if (input.order !== undefined) options.order = input.order;
1166
1298
 
1167
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1168
- const newsletters = await ghostServiceImproved.getNewsletters(options);
1299
+ const newsletters = await ghostService.getNewsletters(options);
1169
1300
  console.error(`Retrieved ${newsletters.length} newsletters from Ghost.`);
1170
1301
 
1171
1302
  return {
@@ -1173,6 +1304,13 @@ server.tool(
1173
1304
  };
1174
1305
  } catch (error) {
1175
1306
  console.error(`Error in ghost_get_newsletters:`, error);
1307
+ if (error.name === 'ZodError') {
1308
+ const validationError = ValidationError.fromZod(error, 'Newsletter query');
1309
+ return {
1310
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1311
+ isError: true,
1312
+ };
1313
+ }
1176
1314
  return {
1177
1315
  content: [{ type: 'text', text: `Error retrieving newsletters: ${error.message}` }],
1178
1316
  isError: true,
@@ -1185,16 +1323,19 @@ server.tool(
1185
1323
  server.tool(
1186
1324
  'ghost_get_newsletter',
1187
1325
  '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 }) => {
1326
+ getNewsletterSchema,
1327
+ async (rawInput) => {
1328
+ const validation = validateToolInput(getNewsletterSchema, rawInput, 'ghost_get_newsletter');
1329
+ if (!validation.success) {
1330
+ return validation.errorResponse;
1331
+ }
1332
+ const { id } = validation.data;
1333
+
1192
1334
  console.error(`Executing tool: ghost_get_newsletter for ID: ${id}`);
1193
1335
  try {
1194
1336
  await loadServices();
1195
1337
 
1196
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1197
- const newsletter = await ghostServiceImproved.getNewsletter(id);
1338
+ const newsletter = await ghostService.getNewsletter(id);
1198
1339
  console.error(`Retrieved newsletter: ${newsletter.name} (ID: ${newsletter.id})`);
1199
1340
 
1200
1341
  return {
@@ -1202,6 +1343,13 @@ server.tool(
1202
1343
  };
1203
1344
  } catch (error) {
1204
1345
  console.error(`Error in ghost_get_newsletter:`, error);
1346
+ if (error.name === 'ZodError') {
1347
+ const validationError = ValidationError.fromZod(error, 'Newsletter retrieval');
1348
+ return {
1349
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1350
+ isError: true,
1351
+ };
1352
+ }
1205
1353
  return {
1206
1354
  content: [{ type: 'text', text: `Error retrieving newsletter: ${error.message}` }],
1207
1355
  isError: true,
@@ -1214,38 +1362,22 @@ server.tool(
1214
1362
  server.tool(
1215
1363
  'ghost_create_newsletter',
1216
1364
  '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) => {
1365
+ createNewsletterSchema,
1366
+ async (rawInput) => {
1367
+ const validation = validateToolInput(
1368
+ createNewsletterSchema,
1369
+ rawInput,
1370
+ 'ghost_create_newsletter'
1371
+ );
1372
+ if (!validation.success) {
1373
+ return validation.errorResponse;
1374
+ }
1375
+ const input = validation.data;
1376
+
1244
1377
  console.error(`Executing tool: ghost_create_newsletter with name: ${input.name}`);
1245
1378
  try {
1246
1379
  await loadServices();
1247
1380
 
1248
- const newsletterService = await import('./services/newsletterService.js');
1249
1381
  const createdNewsletter = await newsletterService.createNewsletterService(input);
1250
1382
  console.error(`Newsletter created successfully. Newsletter ID: ${createdNewsletter.id}`);
1251
1383
 
@@ -1254,6 +1386,13 @@ server.tool(
1254
1386
  };
1255
1387
  } catch (error) {
1256
1388
  console.error(`Error in ghost_create_newsletter:`, error);
1389
+ if (error.name === 'ZodError') {
1390
+ const validationError = ValidationError.fromZod(error, 'Newsletter creation');
1391
+ return {
1392
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1393
+ isError: true,
1394
+ };
1395
+ }
1257
1396
  return {
1258
1397
  content: [{ type: 'text', text: `Error creating newsletter: ${error.message}` }],
1259
1398
  isError: true,
@@ -1266,26 +1405,25 @@ server.tool(
1266
1405
  server.tool(
1267
1406
  'ghost_update_newsletter',
1268
1407
  '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) => {
1408
+ updateNewsletterInputSchema,
1409
+ async (rawInput) => {
1410
+ const validation = validateToolInput(
1411
+ updateNewsletterInputSchema,
1412
+ rawInput,
1413
+ 'ghost_update_newsletter'
1414
+ );
1415
+ if (!validation.success) {
1416
+ return validation.errorResponse;
1417
+ }
1418
+ const input = validation.data;
1419
+
1281
1420
  console.error(`Executing tool: ghost_update_newsletter for newsletter ID: ${input.id}`);
1282
1421
  try {
1283
1422
  await loadServices();
1284
1423
 
1285
1424
  const { id, ...updateData } = input;
1286
1425
 
1287
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1288
- const updatedNewsletter = await ghostServiceImproved.updateNewsletter(id, updateData);
1426
+ const updatedNewsletter = await ghostService.updateNewsletter(id, updateData);
1289
1427
  console.error(`Newsletter updated successfully. Newsletter ID: ${updatedNewsletter.id}`);
1290
1428
 
1291
1429
  return {
@@ -1293,6 +1431,13 @@ server.tool(
1293
1431
  };
1294
1432
  } catch (error) {
1295
1433
  console.error(`Error in ghost_update_newsletter:`, error);
1434
+ if (error.name === 'ZodError') {
1435
+ const validationError = ValidationError.fromZod(error, 'Newsletter update');
1436
+ return {
1437
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1438
+ isError: true,
1439
+ };
1440
+ }
1296
1441
  return {
1297
1442
  content: [{ type: 'text', text: `Error updating newsletter: ${error.message}` }],
1298
1443
  isError: true,
@@ -1305,16 +1450,23 @@ server.tool(
1305
1450
  server.tool(
1306
1451
  'ghost_delete_newsletter',
1307
1452
  '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 }) => {
1453
+ deleteNewsletterSchema,
1454
+ async (rawInput) => {
1455
+ const validation = validateToolInput(
1456
+ deleteNewsletterSchema,
1457
+ rawInput,
1458
+ 'ghost_delete_newsletter'
1459
+ );
1460
+ if (!validation.success) {
1461
+ return validation.errorResponse;
1462
+ }
1463
+ const { id } = validation.data;
1464
+
1312
1465
  console.error(`Executing tool: ghost_delete_newsletter for newsletter ID: ${id}`);
1313
1466
  try {
1314
1467
  await loadServices();
1315
1468
 
1316
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1317
- await ghostServiceImproved.deleteNewsletter(id);
1469
+ await ghostService.deleteNewsletter(id);
1318
1470
  console.error(`Newsletter deleted successfully. Newsletter ID: ${id}`);
1319
1471
 
1320
1472
  return {
@@ -1322,6 +1474,13 @@ server.tool(
1322
1474
  };
1323
1475
  } catch (error) {
1324
1476
  console.error(`Error in ghost_delete_newsletter:`, error);
1477
+ if (error.name === 'ZodError') {
1478
+ const validationError = ValidationError.fromZod(error, 'Newsletter deletion');
1479
+ return {
1480
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1481
+ isError: true,
1482
+ };
1483
+ }
1325
1484
  return {
1326
1485
  content: [{ type: 'text', text: `Error deleting newsletter: ${error.message}` }],
1327
1486
  isError: true,
@@ -1332,27 +1491,28 @@ server.tool(
1332
1491
 
1333
1492
  // --- Tier Tools ---
1334
1493
 
1494
+ // --- Tier Schema Definitions ---
1495
+ const getTierSchema = z.object({ id: ghostIdSchema });
1496
+ const updateTierInputSchema = z.object({ id: ghostIdSchema }).merge(updateTierSchema);
1497
+ const deleteTierSchema = z.object({ id: ghostIdSchema });
1498
+
1335
1499
  // Get Tiers Tool
1336
1500
  server.tool(
1337
1501
  'ghost_get_tiers',
1338
1502
  '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) => {
1503
+ tierQuerySchema,
1504
+ async (rawInput) => {
1505
+ const validation = validateToolInput(tierQuerySchema, rawInput, 'ghost_get_tiers');
1506
+ if (!validation.success) {
1507
+ return validation.errorResponse;
1508
+ }
1509
+ const input = validation.data;
1510
+
1350
1511
  console.error(`Executing tool: ghost_get_tiers`);
1351
1512
  try {
1352
1513
  await loadServices();
1353
1514
 
1354
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1355
- const tiers = await ghostServiceImproved.getTiers(input);
1515
+ const tiers = await ghostService.getTiers(input);
1356
1516
  console.error(`Retrieved ${tiers.length} tiers`);
1357
1517
 
1358
1518
  return {
@@ -1360,6 +1520,13 @@ server.tool(
1360
1520
  };
1361
1521
  } catch (error) {
1362
1522
  console.error(`Error in ghost_get_tiers:`, error);
1523
+ if (error.name === 'ZodError') {
1524
+ const validationError = ValidationError.fromZod(error, 'Tier query');
1525
+ return {
1526
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1527
+ isError: true,
1528
+ };
1529
+ }
1363
1530
  return {
1364
1531
  content: [{ type: 'text', text: `Error getting tiers: ${error.message}` }],
1365
1532
  isError: true,
@@ -1372,16 +1539,19 @@ server.tool(
1372
1539
  server.tool(
1373
1540
  'ghost_get_tier',
1374
1541
  '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 }) => {
1542
+ getTierSchema,
1543
+ async (rawInput) => {
1544
+ const validation = validateToolInput(getTierSchema, rawInput, 'ghost_get_tier');
1545
+ if (!validation.success) {
1546
+ return validation.errorResponse;
1547
+ }
1548
+ const { id } = validation.data;
1549
+
1379
1550
  console.error(`Executing tool: ghost_get_tier for tier ID: ${id}`);
1380
1551
  try {
1381
1552
  await loadServices();
1382
1553
 
1383
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1384
- const tier = await ghostServiceImproved.getTier(id);
1554
+ const tier = await ghostService.getTier(id);
1385
1555
  console.error(`Tier retrieved successfully. Tier ID: ${tier.id}`);
1386
1556
 
1387
1557
  return {
@@ -1389,6 +1559,13 @@ server.tool(
1389
1559
  };
1390
1560
  } catch (error) {
1391
1561
  console.error(`Error in ghost_get_tier:`, error);
1562
+ if (error.name === 'ZodError') {
1563
+ const validationError = ValidationError.fromZod(error, 'Tier retrieval');
1564
+ return {
1565
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1566
+ isError: true,
1567
+ };
1568
+ }
1392
1569
  return {
1393
1570
  content: [{ type: 'text', text: `Error getting tier: ${error.message}` }],
1394
1571
  isError: true,
@@ -1401,32 +1578,19 @@ server.tool(
1401
1578
  server.tool(
1402
1579
  'ghost_create_tier',
1403
1580
  '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) => {
1581
+ createTierSchema,
1582
+ async (rawInput) => {
1583
+ const validation = validateToolInput(createTierSchema, rawInput, 'ghost_create_tier');
1584
+ if (!validation.success) {
1585
+ return validation.errorResponse;
1586
+ }
1587
+ const input = validation.data;
1588
+
1424
1589
  console.error(`Executing tool: ghost_create_tier`);
1425
1590
  try {
1426
1591
  await loadServices();
1427
1592
 
1428
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1429
- const tier = await ghostServiceImproved.createTier(input);
1593
+ const tier = await ghostService.createTier(input);
1430
1594
  console.error(`Tier created successfully. Tier ID: ${tier.id}`);
1431
1595
 
1432
1596
  return {
@@ -1434,6 +1598,13 @@ server.tool(
1434
1598
  };
1435
1599
  } catch (error) {
1436
1600
  console.error(`Error in ghost_create_tier:`, error);
1601
+ if (error.name === 'ZodError') {
1602
+ const validationError = ValidationError.fromZod(error, 'Tier creation');
1603
+ return {
1604
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1605
+ isError: true,
1606
+ };
1607
+ }
1437
1608
  return {
1438
1609
  content: [{ type: 'text', text: `Error creating tier: ${error.message}` }],
1439
1610
  isError: true,
@@ -1446,24 +1617,21 @@ server.tool(
1446
1617
  server.tool(
1447
1618
  'ghost_update_tier',
1448
1619
  '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) => {
1620
+ updateTierInputSchema,
1621
+ async (rawInput) => {
1622
+ const validation = validateToolInput(updateTierInputSchema, rawInput, 'ghost_update_tier');
1623
+ if (!validation.success) {
1624
+ return validation.errorResponse;
1625
+ }
1626
+ const input = validation.data;
1627
+
1459
1628
  console.error(`Executing tool: ghost_update_tier for tier ID: ${input.id}`);
1460
1629
  try {
1461
1630
  await loadServices();
1462
1631
 
1463
1632
  const { id, ...updateData } = input;
1464
1633
 
1465
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1466
- const updatedTier = await ghostServiceImproved.updateTier(id, updateData);
1634
+ const updatedTier = await ghostService.updateTier(id, updateData);
1467
1635
  console.error(`Tier updated successfully. Tier ID: ${updatedTier.id}`);
1468
1636
 
1469
1637
  return {
@@ -1471,6 +1639,13 @@ server.tool(
1471
1639
  };
1472
1640
  } catch (error) {
1473
1641
  console.error(`Error in ghost_update_tier:`, error);
1642
+ if (error.name === 'ZodError') {
1643
+ const validationError = ValidationError.fromZod(error, 'Tier update');
1644
+ return {
1645
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1646
+ isError: true,
1647
+ };
1648
+ }
1474
1649
  return {
1475
1650
  content: [{ type: 'text', text: `Error updating tier: ${error.message}` }],
1476
1651
  isError: true,
@@ -1483,16 +1658,19 @@ server.tool(
1483
1658
  server.tool(
1484
1659
  'ghost_delete_tier',
1485
1660
  '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 }) => {
1661
+ deleteTierSchema,
1662
+ async (rawInput) => {
1663
+ const validation = validateToolInput(deleteTierSchema, rawInput, 'ghost_delete_tier');
1664
+ if (!validation.success) {
1665
+ return validation.errorResponse;
1666
+ }
1667
+ const { id } = validation.data;
1668
+
1490
1669
  console.error(`Executing tool: ghost_delete_tier for tier ID: ${id}`);
1491
1670
  try {
1492
1671
  await loadServices();
1493
1672
 
1494
- const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
1495
- await ghostServiceImproved.deleteTier(id);
1673
+ await ghostService.deleteTier(id);
1496
1674
  console.error(`Tier deleted successfully. Tier ID: ${id}`);
1497
1675
 
1498
1676
  return {
@@ -1500,6 +1678,13 @@ server.tool(
1500
1678
  };
1501
1679
  } catch (error) {
1502
1680
  console.error(`Error in ghost_delete_tier:`, error);
1681
+ if (error.name === 'ZodError') {
1682
+ const validationError = ValidationError.fromZod(error, 'Tier deletion');
1683
+ return {
1684
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1685
+ isError: true,
1686
+ };
1687
+ }
1503
1688
  return {
1504
1689
  content: [{ type: 'text', text: `Error deleting tier: ${error.message}` }],
1505
1690
  isError: true,