@jgardner04/ghost-mcp-server 1.13.4 → 1.14.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.
Files changed (40) hide show
  1. package/README.md +68 -0
  2. package/package.json +7 -3
  3. package/src/__tests__/helpers/testUtils.js +15 -1
  4. package/src/__tests__/mcp_server.test.js +152 -1
  5. package/src/__tests__/mcp_server_pages.test.js +23 -6
  6. package/src/controllers/__tests__/imageController.test.js +2 -2
  7. package/src/controllers/imageController.js +11 -10
  8. package/src/mcp_server.js +647 -1203
  9. package/src/routes/__tests__/imageRoutes.test.js +2 -2
  10. package/src/schemas/__tests__/common.test.js +3 -3
  11. package/src/schemas/__tests__/pageSchemas.test.js +11 -2
  12. package/src/schemas/common.js +3 -2
  13. package/src/schemas/pageSchemas.js +1 -1
  14. package/src/schemas/postSchemas.js +1 -1
  15. package/src/services/__tests__/createResourceService.test.js +468 -0
  16. package/src/services/__tests__/ghostService.test.js +0 -19
  17. package/src/services/__tests__/ghostServiceImproved.members.test.js +0 -3
  18. package/src/services/__tests__/ghostServiceImproved.newsletters.test.js +2 -2
  19. package/src/services/__tests__/ghostServiceImproved.pages.test.js +0 -3
  20. package/src/services/__tests__/ghostServiceImproved.posts.test.js +0 -1
  21. package/src/services/__tests__/ghostServiceImproved.tags.test.js +0 -3
  22. package/src/services/__tests__/ghostServiceImproved.tiers.test.js +3 -5
  23. package/src/services/__tests__/imageProcessingService.test.js +148 -177
  24. package/src/services/__tests__/images.test.js +78 -0
  25. package/src/services/createResourceService.js +138 -0
  26. package/src/services/ghostApiClient.js +240 -0
  27. package/src/services/ghostService.js +1 -19
  28. package/src/services/ghostServiceImproved.js +76 -915
  29. package/src/services/imageProcessingService.js +100 -56
  30. package/src/services/images.js +54 -0
  31. package/src/services/members.js +127 -0
  32. package/src/services/newsletters.js +63 -0
  33. package/src/services/pageService.js +2 -2
  34. package/src/services/pages.js +116 -0
  35. package/src/services/posts.js +116 -0
  36. package/src/services/tags.js +118 -0
  37. package/src/services/tiers.js +72 -0
  38. package/src/services/validators.js +218 -0
  39. package/src/utils/__tests__/imageInputResolver.test.js +134 -0
  40. package/src/utils/imageInputResolver.js +127 -0
package/src/mcp_server.js CHANGED
@@ -11,6 +11,7 @@ import crypto from 'crypto';
11
11
  import { ValidationError } from './errors/index.js';
12
12
  import { validateToolInput } from './utils/validation.js';
13
13
  import { trackTempFile, cleanupTempFiles } from './utils/tempFileManager.js';
14
+ import { resolveLocalImagePath, decodeBase64ToTempFile } from './utils/imageInputResolver.js';
14
15
  import {
15
16
  createTagSchema,
16
17
  updateTagSchema,
@@ -83,6 +84,49 @@ const escapeNqlValue = (value) => {
83
84
  return value.replace(/'/g, "''");
84
85
  };
85
86
 
87
+ /**
88
+ * Higher-order function that wraps a tool handler with standardized
89
+ * input validation, service loading, and error handling.
90
+ *
91
+ * Note: Each registerTool call passes the schema twice — once as `inputSchema`
92
+ * (MCP protocol metadata exposed to clients) and once here for runtime input
93
+ * validation via validateToolInput. These serve different purposes and both
94
+ * are required.
95
+ *
96
+ * @param {string} toolName - The tool identifier (e.g., 'ghost_get_tags')
97
+ * @param {object} schema - Zod schema for input validation
98
+ * @param {Function} handler - Async function receiving validated input, returns MCP response
99
+ * @returns {Function} Wrapped async handler for server.registerTool
100
+ */
101
+ const withErrorHandling = (toolName, schema, handler) => {
102
+ const zodContext = toolName.replace('ghost_', '').replace(/_/g, ' ');
103
+ return async (rawInput) => {
104
+ console.error(`Executing tool: ${toolName}`);
105
+ const validation = validateToolInput(schema, rawInput, toolName);
106
+ if (!validation.success) {
107
+ return validation.errorResponse;
108
+ }
109
+
110
+ try {
111
+ await loadServices();
112
+ return await handler(validation.data);
113
+ } catch (error) {
114
+ console.error(`Error in ${toolName}:`, error);
115
+ if (error.name === 'ZodError') {
116
+ const validationError = ValidationError.fromZod(error, zodContext);
117
+ return {
118
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
119
+ isError: true,
120
+ };
121
+ }
122
+ return {
123
+ content: [{ type: 'text', text: `Error in ${toolName}: ${error.message}` }],
124
+ isError: true,
125
+ };
126
+ }
127
+ };
128
+ };
129
+
86
130
  // Create server instance with new API
87
131
  const server = new McpServer({
88
132
  name: 'ghost-mcp-server',
@@ -116,58 +160,32 @@ server.registerTool(
116
160
  'Retrieves a list of tags from Ghost CMS with pagination, filtering, sorting, and relation inclusion. Supports filtering by name, slug, visibility, or custom NQL filter expressions.',
117
161
  inputSchema: getTagsSchema,
118
162
  },
119
- async (rawInput) => {
120
- const validation = validateToolInput(getTagsSchema, rawInput, 'ghost_get_tags');
121
- if (!validation.success) {
122
- return validation.errorResponse;
123
- }
124
- const input = validation.data;
125
-
126
- console.error(`Executing tool: ghost_get_tags`);
127
- try {
128
- await loadServices();
129
-
130
- // Build options object with provided parameters
131
- const options = {};
132
- if (input.limit !== undefined) options.limit = input.limit;
133
- if (input.page !== undefined) options.page = input.page;
134
- if (input.order !== undefined) options.order = input.order;
135
- if (input.include !== undefined) options.include = input.include;
136
-
137
- // Build filter string from individual filter parameters
138
- const filters = [];
139
- if (input.name) filters.push(`name:'${escapeNqlValue(input.name)}'`);
140
- if (input.slug) filters.push(`slug:'${escapeNqlValue(input.slug)}'`);
141
- if (input.visibility) filters.push(`visibility:'${input.visibility}'`); // visibility is enum-validated, no escaping needed
142
- if (input.filter) filters.push(input.filter);
143
-
144
- if (filters.length > 0) {
145
- options.filter = filters.join('+');
146
- }
147
-
148
- const tags = await ghostService.getTags(options);
149
- console.error(`Retrieved ${tags.length} tags from Ghost.`);
150
-
151
- const result = tags;
152
-
153
- return {
154
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
155
- };
156
- } catch (error) {
157
- console.error(`Error in ghost_get_tags:`, error);
158
- if (error.name === 'ZodError') {
159
- const validationError = ValidationError.fromZod(error, 'Tags retrieval');
160
- return {
161
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
162
- isError: true,
163
- };
164
- }
165
- return {
166
- content: [{ type: 'text', text: `Error: ${error.message}` }],
167
- isError: true,
168
- };
169
- }
170
- }
163
+ withErrorHandling('ghost_get_tags', getTagsSchema, async (input) => {
164
+ // Build options object with provided parameters
165
+ const options = {};
166
+ if (input.limit !== undefined) options.limit = input.limit;
167
+ if (input.page !== undefined) options.page = input.page;
168
+ if (input.order !== undefined) options.order = input.order;
169
+ if (input.include !== undefined) options.include = input.include;
170
+
171
+ // Build filter string from individual filter parameters
172
+ const filters = [];
173
+ if (input.name) filters.push(`name:'${escapeNqlValue(input.name)}'`);
174
+ if (input.slug) filters.push(`slug:'${escapeNqlValue(input.slug)}'`);
175
+ if (input.visibility) filters.push(`visibility:'${input.visibility}'`); // visibility is enum-validated, no escaping needed
176
+ if (input.filter) filters.push(input.filter);
177
+
178
+ if (filters.length > 0) {
179
+ options.filter = filters.join('+');
180
+ }
181
+
182
+ const tags = await ghostService.getTags(options);
183
+ console.error(`Retrieved ${tags.length} tags from Ghost.`);
184
+
185
+ return {
186
+ content: [{ type: 'text', text: JSON.stringify(tags, null, 2) }],
187
+ };
188
+ })
171
189
  );
172
190
 
173
191
  // Create Tag Tool
@@ -177,37 +195,14 @@ server.registerTool(
177
195
  description: 'Creates a new tag in Ghost CMS.',
178
196
  inputSchema: createTagSchema,
179
197
  },
180
- async (rawInput) => {
181
- const validation = validateToolInput(createTagSchema, rawInput, 'ghost_create_tag');
182
- if (!validation.success) {
183
- return validation.errorResponse;
184
- }
185
- const input = validation.data;
198
+ withErrorHandling('ghost_create_tag', createTagSchema, async (input) => {
199
+ const createdTag = await ghostService.createTag(input);
200
+ console.error(`Tag created successfully. Tag ID: ${createdTag.id}`);
186
201
 
187
- console.error(`Executing tool: ghost_create_tag with name: ${input.name}`);
188
- try {
189
- await loadServices();
190
- const createdTag = await ghostService.createTag(input);
191
- console.error(`Tag created successfully. Tag ID: ${createdTag.id}`);
192
-
193
- return {
194
- content: [{ type: 'text', text: JSON.stringify(createdTag, null, 2) }],
195
- };
196
- } catch (error) {
197
- console.error(`Error in ghost_create_tag:`, error);
198
- if (error.name === 'ZodError') {
199
- const validationError = ValidationError.fromZod(error, 'Tag creation');
200
- return {
201
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
202
- isError: true,
203
- };
204
- }
205
- return {
206
- content: [{ type: 'text', text: `Error: ${error.message}` }],
207
- isError: true,
208
- };
209
- }
210
- }
202
+ return {
203
+ content: [{ type: 'text', text: JSON.stringify(createdTag, null, 2) }],
204
+ };
205
+ })
211
206
  );
212
207
 
213
208
  // Get Tag Tool
@@ -217,42 +212,22 @@ server.registerTool(
217
212
  description: 'Retrieves a single tag from Ghost CMS by ID or slug.',
218
213
  inputSchema: getTagSchema,
219
214
  },
220
- async (rawInput) => {
221
- const validation = validateToolInput(getTagSchema, rawInput, 'ghost_get_tag');
222
- if (!validation.success) {
223
- return validation.errorResponse;
224
- }
225
- const { id, slug, include } = validation.data;
226
-
227
- console.error(`Executing tool: ghost_get_tag`);
228
- try {
229
- await loadServices();
215
+ withErrorHandling('ghost_get_tag', getTagSchema, async (input) => {
216
+ const options = {};
217
+ if (input.include !== undefined) options.include = input.include;
230
218
 
231
- // If slug is provided, use the slug/slug-name format
232
- const identifier = slug ? `slug/${slug}` : id;
233
- const options = include ? { include } : {};
219
+ if (!input.id && !input.slug) {
220
+ throw new Error('Either id or slug is required');
221
+ }
222
+ const identifier = input.id || `slug/${input.slug}`;
234
223
 
235
- const tag = await ghostService.getTag(identifier, options);
236
- console.error(`Tag retrieved successfully. Tag ID: ${tag.id}`);
224
+ const tag = await ghostService.getTag(identifier, options);
225
+ console.error(`Retrieved tag: ${tag.name} (ID: ${tag.id})`);
237
226
 
238
- return {
239
- content: [{ type: 'text', text: JSON.stringify(tag, null, 2) }],
240
- };
241
- } catch (error) {
242
- console.error(`Error in ghost_get_tag:`, error);
243
- if (error.name === 'ZodError') {
244
- const validationError = ValidationError.fromZod(error, 'Tag retrieval');
245
- return {
246
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
247
- isError: true,
248
- };
249
- }
250
- return {
251
- content: [{ type: 'text', text: `Error: ${error.message}` }],
252
- isError: true,
253
- };
254
- }
255
- }
227
+ return {
228
+ content: [{ type: 'text', text: JSON.stringify(tag, null, 2) }],
229
+ };
230
+ })
256
231
  );
257
232
 
258
233
  // Update Tag Tool
@@ -262,184 +237,321 @@ server.registerTool(
262
237
  description: 'Updates an existing tag in Ghost CMS.',
263
238
  inputSchema: updateTagInputSchema,
264
239
  },
265
- async (rawInput) => {
266
- const validation = validateToolInput(updateTagInputSchema, rawInput, 'ghost_update_tag');
267
- if (!validation.success) {
268
- return validation.errorResponse;
240
+ withErrorHandling('ghost_update_tag', updateTagInputSchema, async (input) => {
241
+ const { id, ...updateData } = input;
242
+ const updatedTag = await ghostService.updateTag(id, updateData);
243
+ console.error(`Tag updated successfully. Tag ID: ${updatedTag.id}`);
244
+
245
+ return {
246
+ content: [{ type: 'text', text: JSON.stringify(updatedTag, null, 2) }],
247
+ };
248
+ })
249
+ );
250
+
251
+ // Delete Tag Tool
252
+ server.registerTool(
253
+ 'ghost_delete_tag',
254
+ {
255
+ description:
256
+ 'Deletes a tag from Ghost CMS by ID. This operation is permanent and cannot be undone.',
257
+ inputSchema: deleteTagSchema,
258
+ },
259
+ withErrorHandling('ghost_delete_tag', deleteTagSchema, async (input) => {
260
+ const { id } = input;
261
+ await ghostService.deleteTag(id);
262
+ console.error(`Tag deleted successfully. Tag ID: ${id}`);
263
+
264
+ return {
265
+ content: [{ type: 'text', text: `Tag ${id} has been successfully deleted.` }],
266
+ };
267
+ })
268
+ );
269
+
270
+ // --- Image Schema ---
271
+ // Base object (plain ZodObject — keeps `.shape` available for MCP schema
272
+ // introspection). Runtime XOR validation is applied at the tool level
273
+ // via a manual check so we never wrap with ZodEffects.
274
+ const imageInputFields = {
275
+ imageUrl: z.string().optional().meta({
276
+ description: 'The publicly accessible URL of the image to download and upload.',
277
+ }),
278
+ imagePath: z.string().optional().meta({
279
+ description:
280
+ 'Absolute path to a local image file. Only accepted when the GHOST_MCP_IMAGE_ROOT env var is set; paths must resolve inside that root.',
281
+ }),
282
+ imageBase64: z.string().optional().meta({
283
+ description:
284
+ 'Base64-encoded image bytes (with or without data: URI prefix). Decoded size capped at 5MB to respect MCP transport limits. Requires mimeType.',
285
+ }),
286
+ mimeType: z.string().optional().meta({
287
+ description:
288
+ 'MIME type for imageBase64 input (e.g. image/png, image/jpeg, image/svg+xml). Required when imageBase64 is used.',
289
+ }),
290
+ alt: z.string().optional().meta({
291
+ description:
292
+ 'Alt text for the image. If omitted, a default will be generated from the filename.',
293
+ }),
294
+ purpose: z.enum(['image', 'profile_image', 'icon']).optional().meta({
295
+ description:
296
+ 'Intended use. Ghost validates format/size per purpose (icon/profile_image must be square; icon also accepts ICO).',
297
+ }),
298
+ ref: z.string().max(200).optional().meta({
299
+ description:
300
+ 'Caller-supplied identifier (e.g. original filename). Ghost echoes it back in the response.',
301
+ }),
302
+ };
303
+
304
+ const uploadImageSchema = z.object(imageInputFields);
305
+
306
+ function validateImageInputXor(data) {
307
+ const count = Number(!!data.imageUrl) + Number(!!data.imagePath) + Number(!!data.imageBase64);
308
+ if (count !== 1) return 'Provide exactly one of imageUrl, imagePath, or imageBase64.';
309
+ if (data.imageBase64 && !data.mimeType) {
310
+ return 'mimeType is required when imageBase64 is provided.';
311
+ }
312
+ return null;
313
+ }
314
+
315
+ // Axios does not enforce `maxContentLength` for responseType: 'stream',
316
+ // so we cap downloads here by watching bytes on the response stream and
317
+ // destroying it on overflow. Mirrors the config value in urlValidator.js.
318
+ const DOWNLOAD_CAP_BYTES = 50 * 1024 * 1024;
319
+
320
+ async function acquireImageForUpload({ imageUrl, imagePath: localPath, imageBase64, mimeType }) {
321
+ const tempDir = os.tmpdir();
322
+
323
+ if (imageUrl) {
324
+ const urlValidation = urlValidator.validateImageUrl(imageUrl);
325
+ if (!urlValidation.isValid) {
326
+ throw new Error(`Invalid image URL: ${urlValidation.error}`);
269
327
  }
270
- const input = validation.data;
328
+ const axiosConfig = urlValidator.createSecureAxiosConfig(urlValidation.sanitizedUrl);
329
+ const response = await axios(axiosConfig);
271
330
 
272
- console.error(`Executing tool: ghost_update_tag for ID: ${input.id}`);
273
- try {
274
- if (!input.id) {
275
- throw new Error('Tag ID is required');
331
+ const declared = Number(response.headers['content-length']);
332
+ if (Number.isFinite(declared) && declared > DOWNLOAD_CAP_BYTES) {
333
+ response.data.destroy();
334
+ throw new Error(
335
+ `Image exceeds ${DOWNLOAD_CAP_BYTES} byte limit (server declared ${declared})`
336
+ );
337
+ }
338
+
339
+ const extension = path.extname(imageUrl.split('?')[0]) || '.tmp';
340
+ const filenameHint =
341
+ path.basename(imageUrl.split('?')[0]) || `image-${generateUuid()}${extension}`;
342
+ const downloadedPath = path.join(tempDir, `mcp-download-${generateUuid()}${extension}`);
343
+
344
+ let bytes = 0;
345
+ response.data.on('data', (chunk) => {
346
+ bytes += chunk.length;
347
+ if (bytes > DOWNLOAD_CAP_BYTES) {
348
+ response.data.destroy(new Error(`Image exceeds ${DOWNLOAD_CAP_BYTES} byte limit`));
276
349
  }
350
+ });
351
+
352
+ const writer = fs.createWriteStream(downloadedPath);
353
+ response.data.pipe(writer);
354
+ await new Promise((resolve, reject) => {
355
+ writer.on('finish', resolve);
356
+ writer.on('error', reject);
357
+ response.data.on('error', reject);
358
+ });
359
+ return { acquiredPath: downloadedPath, filenameHint, source: 'url' };
360
+ }
277
361
 
278
- await loadServices();
362
+ if (localPath) {
363
+ const resolved = await resolveLocalImagePath(localPath);
364
+ return { acquiredPath: resolved, filenameHint: path.basename(resolved), source: 'path' };
365
+ }
279
366
 
280
- // Build update data object with only provided fields (exclude id from update data)
281
- const { id, ...updateData } = input;
367
+ if (imageBase64) {
368
+ const decodedPath = await decodeBase64ToTempFile(imageBase64, mimeType);
369
+ return {
370
+ acquiredPath: decodedPath,
371
+ filenameHint: path.basename(decodedPath),
372
+ source: 'base64',
373
+ };
374
+ }
282
375
 
283
- const updatedTag = await ghostService.updateTag(id, updateData);
284
- console.error(`Tag updated successfully. Tag ID: ${updatedTag.id}`);
376
+ throw new Error('No image input provided'); // unreachable — schema enforces one
377
+ }
285
378
 
286
- return {
287
- content: [{ type: 'text', text: JSON.stringify(updatedTag, null, 2) }],
288
- };
289
- } catch (error) {
290
- console.error(`Error in ghost_update_tag:`, error);
291
- if (error.name === 'ZodError') {
292
- const validationError = ValidationError.fromZod(error, 'Tag update');
293
- return {
294
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
295
- isError: true,
296
- };
297
- }
298
- return {
299
- content: [{ type: 'text', text: `Error: ${error.message}` }],
300
- isError: true,
301
- };
302
- }
379
+ /**
380
+ * Acquire process → upload an image from a validated input. Owns its
381
+ * own temp-file lifecycle. Returns { uploadResult, filenameHint, finalAltText }.
382
+ * Does NOT delete the caller's imagePath file.
383
+ */
384
+ async function performImageUpload(data) {
385
+ await loadServices();
386
+
387
+ let acquiredPath = null;
388
+ let processedPath = null;
389
+
390
+ try {
391
+ const acquired = await acquireImageForUpload(data);
392
+ acquiredPath = acquired.acquiredPath;
393
+ if (acquired.source !== 'path') trackTempFile(acquiredPath);
394
+
395
+ processedPath = await imageProcessingService.processImage(
396
+ acquiredPath,
397
+ os.tmpdir(),
398
+ data.purpose ? { purpose: data.purpose } : {}
399
+ );
400
+ if (processedPath !== acquiredPath) trackTempFile(processedPath);
401
+
402
+ const uploadOpts = {};
403
+ if (data.purpose) uploadOpts.purpose = data.purpose;
404
+ if (data.ref) uploadOpts.ref = data.ref;
405
+ else if (acquired.filenameHint) uploadOpts.ref = acquired.filenameHint.slice(0, 200);
406
+
407
+ const uploadResult = await ghostService.uploadImage(processedPath, uploadOpts);
408
+ const finalAltText = data.alt || getDefaultAltText(acquired.filenameHint);
409
+
410
+ return { uploadResult, filenameHint: acquired.filenameHint, finalAltText };
411
+ } finally {
412
+ const toClean = [];
413
+ if (acquiredPath && !data.imagePath) toClean.push(acquiredPath);
414
+ if (processedPath && processedPath !== acquiredPath) toClean.push(processedPath);
415
+ if (toClean.length > 0) await cleanupTempFiles(toClean, console);
303
416
  }
304
- );
417
+ }
305
418
 
306
- // Delete Tag Tool
419
+ /**
420
+ * Runs the standard tool-input validation then the XOR image-input check.
421
+ * Returns { success: true, data } or { success: false, errorResponse }
422
+ * so both image tools can collapse their validation prelude to one call.
423
+ */
424
+ function validateAndXorImageInput(schema, rawInput, toolName) {
425
+ const validation = validateToolInput(schema, rawInput, toolName);
426
+ if (!validation.success) return validation;
427
+ const xorError = validateImageInputXor(validation.data);
428
+ if (xorError) {
429
+ return {
430
+ success: false,
431
+ errorResponse: { content: [{ type: 'text', text: xorError }], isError: true },
432
+ };
433
+ }
434
+ return validation;
435
+ }
436
+
437
+ // Upload Image Tool
307
438
  server.registerTool(
308
- 'ghost_delete_tag',
439
+ 'ghost_upload_image',
309
440
  {
310
- description: 'Deletes a tag from Ghost CMS by ID. This operation is permanent.',
311
- inputSchema: deleteTagSchema,
441
+ description:
442
+ 'Uploads an image to Ghost CMS. Accepts a remote URL, a local file path (when GHOST_MCP_IMAGE_ROOT is configured), or a base64 payload. Returns the Ghost image URL, alt text, and ref (when Ghost echoes it).',
443
+ inputSchema: uploadImageSchema,
312
444
  },
313
445
  async (rawInput) => {
314
- const validation = validateToolInput(deleteTagSchema, rawInput, 'ghost_delete_tag');
315
- if (!validation.success) {
316
- return validation.errorResponse;
317
- }
318
- const { id } = validation.data;
446
+ const validation = validateAndXorImageInput(uploadImageSchema, rawInput, 'ghost_upload_image');
447
+ if (!validation.success) return validation.errorResponse;
319
448
 
320
- console.error(`Executing tool: ghost_delete_tag for ID: ${id}`);
321
449
  try {
322
- if (!id) {
323
- throw new Error('Tag ID is required');
324
- }
325
-
326
- await loadServices();
327
-
328
- await ghostService.deleteTag(id);
329
- console.error(`Tag deleted successfully. Tag ID: ${id}`);
330
-
331
- return {
332
- content: [{ type: 'text', text: `Tag with ID ${id} has been successfully deleted.` }],
333
- };
450
+ const { uploadResult, finalAltText } = await performImageUpload(validation.data);
451
+ const result = { url: uploadResult.url, alt: finalAltText };
452
+ if (uploadResult.ref) result.ref = uploadResult.ref;
453
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
334
454
  } catch (error) {
335
- console.error(`Error in ghost_delete_tag:`, error);
336
- if (error.name === 'ZodError') {
337
- const validationError = ValidationError.fromZod(error, 'Tag deletion');
338
- return {
339
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
340
- isError: true,
341
- };
342
- }
455
+ console.error(`Error in ghost_upload_image:`, error);
343
456
  return {
344
- content: [{ type: 'text', text: `Error: ${error.message}` }],
457
+ content: [{ type: 'text', text: `Error uploading image: ${error.message}` }],
345
458
  isError: true,
346
459
  };
347
460
  }
348
461
  }
349
462
  );
350
463
 
351
- // --- Image Schema ---
352
- const uploadImageSchema = z.object({
353
- imageUrl: z.string().meta({ description: 'The publicly accessible URL of the image to upload.' }),
354
- alt: z.string().optional().meta({
355
- description:
356
- 'Alt text for the image. If omitted, a default will be generated from the filename.',
464
+ // --- Set Feature Image Tool ---
465
+ // Combined upload-and-assign flow. Ghost has no delete-image endpoint,
466
+ // so when the post/page update fails after a successful upload, the
467
+ // image is orphaned in Ghost's storage — we surface its URL in the
468
+ // error response so the caller can reuse or record it.
469
+ const setFeatureImageSchema = uploadImageSchema.extend({
470
+ type: z.enum(['post', 'page']).meta({
471
+ description: 'Which resource to attach the feature image to.',
472
+ }),
473
+ id: ghostIdSchema.meta({ description: 'ID of the post or page.' }),
474
+ caption: z.string().max(5000).optional().meta({
475
+ description: 'Optional HTML caption for the feature image (max 5000 chars).',
357
476
  }),
358
477
  });
359
478
 
360
- // Upload Image Tool
361
479
  server.registerTool(
362
- 'ghost_upload_image',
480
+ 'ghost_set_feature_image',
363
481
  {
364
482
  description:
365
- 'Downloads an image from a URL, processes it, uploads it to Ghost CMS, and returns the final Ghost image URL and alt text.',
366
- inputSchema: uploadImageSchema,
483
+ 'Uploads an image and assigns it as the feature image of a post or page (with optional alt text and caption) in one call. Accepts the same imageUrl/imagePath/imageBase64 input modes as ghost_upload_image. Returns the updated resource. If the update fails after the upload, the error response includes the orphaned image URL.',
484
+ inputSchema: setFeatureImageSchema,
367
485
  },
368
486
  async (rawInput) => {
369
- const validation = validateToolInput(uploadImageSchema, rawInput, 'ghost_upload_image');
370
- if (!validation.success) {
371
- return validation.errorResponse;
372
- }
373
- const { imageUrl, alt } = validation.data;
487
+ const validation = validateAndXorImageInput(
488
+ setFeatureImageSchema,
489
+ rawInput,
490
+ 'ghost_set_feature_image'
491
+ );
492
+ if (!validation.success) return validation.errorResponse;
374
493
 
375
- console.error(`Executing tool: ghost_upload_image for URL: ${imageUrl}`);
376
- let downloadedPath = null;
377
- let processedPath = null;
494
+ const { type, id, caption } = validation.data;
378
495
 
496
+ let uploadedUrl;
497
+ let uploadedRef;
498
+ let altText;
379
499
  try {
380
- await loadServices();
381
-
382
- // 1. Validate URL for SSRF protection
383
- const urlValidation = urlValidator.validateImageUrl(imageUrl);
384
- if (!urlValidation.isValid) {
385
- throw new Error(`Invalid image URL: ${urlValidation.error}`);
386
- }
387
-
388
- // 2. Download the image with security controls
389
- const axiosConfig = urlValidator.createSecureAxiosConfig(urlValidation.sanitizedUrl);
390
- const response = await axios(axiosConfig);
391
- const tempDir = os.tmpdir();
392
- const extension = path.extname(imageUrl.split('?')[0]) || '.tmp';
393
- const originalFilenameHint =
394
- path.basename(imageUrl.split('?')[0]) || `image-${generateUuid()}${extension}`;
395
- downloadedPath = path.join(tempDir, `mcp-download-${generateUuid()}${extension}`);
396
-
397
- const writer = fs.createWriteStream(downloadedPath);
398
- response.data.pipe(writer);
399
-
400
- await new Promise((resolve, reject) => {
401
- writer.on('finish', resolve);
402
- writer.on('error', reject);
403
- });
404
- // Track temp file for cleanup on process exit
405
- trackTempFile(downloadedPath);
406
- console.error(`Downloaded image to temporary path: ${downloadedPath}`);
407
-
408
- // 3. Process the image
409
- processedPath = await imageProcessingService.processImage(downloadedPath, tempDir);
410
- // Track processed file for cleanup on process exit
411
- if (processedPath !== downloadedPath) {
412
- trackTempFile(processedPath);
413
- }
414
- console.error(`Processed image path: ${processedPath}`);
415
-
416
- // 4. Determine Alt Text
417
- const defaultAlt = getDefaultAltText(originalFilenameHint);
418
- const finalAltText = alt || defaultAlt;
419
- console.error(`Using alt text: "${finalAltText}"`);
420
-
421
- // 5. Upload processed image to Ghost
422
- const uploadResult = await ghostService.uploadImage(processedPath);
423
- console.error(`Uploaded processed image to Ghost: ${uploadResult.url}`);
424
-
425
- // 6. Return result
426
- const result = {
427
- url: uploadResult.url,
428
- alt: finalAltText,
500
+ const { uploadResult, finalAltText } = await performImageUpload(validation.data);
501
+ uploadedUrl = uploadResult.url;
502
+ uploadedRef = uploadResult.ref;
503
+ altText = finalAltText;
504
+ } catch (error) {
505
+ console.error(`ghost_set_feature_image: upload failed`, error);
506
+ return {
507
+ content: [{ type: 'text', text: `Upload failed: ${error.message}` }],
508
+ isError: true,
429
509
  };
510
+ }
430
511
 
431
- return {
432
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
512
+ const updatePayload = {
513
+ feature_image: uploadedUrl,
514
+ feature_image_alt: altText,
515
+ };
516
+ if (caption !== undefined) updatePayload.feature_image_caption = caption;
517
+
518
+ try {
519
+ const updated =
520
+ type === 'post'
521
+ ? await ghostService.updatePost(id, updatePayload)
522
+ : await ghostService.updatePage(id, updatePayload);
523
+ console.error(`ghost_set_feature_image: ${type} ${id} updated with ${uploadedUrl}`);
524
+ return {
525
+ content: [
526
+ {
527
+ type: 'text',
528
+ text: JSON.stringify(
529
+ { uploaded: { url: uploadedUrl, ref: uploadedRef, alt: altText }, [type]: updated },
530
+ null,
531
+ 2
532
+ ),
533
+ },
534
+ ],
433
535
  };
434
536
  } catch (error) {
435
- console.error(`Error in ghost_upload_image:`, error);
436
- return {
437
- content: [{ type: 'text', text: `Error uploading image: ${error.message}` }],
537
+ console.error(`ghost_set_feature_image: update failed (orphaned ${uploadedUrl})`, error);
538
+ return {
539
+ content: [
540
+ {
541
+ type: 'text',
542
+ text: JSON.stringify(
543
+ {
544
+ error: `Upload succeeded but ${type} update failed: ${error.message}`,
545
+ orphanedImage: { url: uploadedUrl, ref: uploadedRef, alt: altText },
546
+ hint: 'Ghost does not expose a delete-image endpoint; reuse this URL or leave it orphaned.',
547
+ },
548
+ null,
549
+ 2
550
+ ),
551
+ },
552
+ ],
438
553
  isError: true,
439
554
  };
440
- } finally {
441
- // Cleanup temporary files with proper async/await
442
- await cleanupTempFiles([downloadedPath, processedPath], console);
443
555
  }
444
556
  }
445
557
  );
@@ -486,37 +598,14 @@ server.registerTool(
486
598
  description: 'Creates a new post in Ghost CMS.',
487
599
  inputSchema: createPostSchema,
488
600
  },
489
- async (rawInput) => {
490
- const validation = validateToolInput(createPostSchema, rawInput, 'ghost_create_post');
491
- if (!validation.success) {
492
- return validation.errorResponse;
493
- }
494
- const input = validation.data;
601
+ withErrorHandling('ghost_create_post', createPostSchema, async (input) => {
602
+ const createdPost = await postService.createPostService(input);
603
+ console.error(`Post created successfully. Post ID: ${createdPost.id}`);
495
604
 
496
- console.error(`Executing tool: ghost_create_post with title: ${input.title}`);
497
- try {
498
- await loadServices();
499
- const createdPost = await postService.createPostService(input);
500
- console.error(`Post created successfully. Post ID: ${createdPost.id}`);
501
-
502
- return {
503
- content: [{ type: 'text', text: JSON.stringify(createdPost, null, 2) }],
504
- };
505
- } catch (error) {
506
- console.error(`Error in ghost_create_post:`, error);
507
- if (error.name === 'ZodError') {
508
- const validationError = ValidationError.fromZod(error, 'Post creation');
509
- return {
510
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
511
- isError: true,
512
- };
513
- }
514
- return {
515
- content: [{ type: 'text', text: `Error creating post: ${error.message}` }],
516
- isError: true,
517
- };
518
- }
519
- }
605
+ return {
606
+ content: [{ type: 'text', text: JSON.stringify(createdPost, null, 2) }],
607
+ };
608
+ })
520
609
  );
521
610
 
522
611
  // Get Posts Tool
@@ -527,50 +616,26 @@ server.registerTool(
527
616
  'Retrieves a list of posts from Ghost CMS with pagination, filtering, and sorting options.',
528
617
  inputSchema: getPostsSchema,
529
618
  },
530
- async (rawInput) => {
531
- const validation = validateToolInput(getPostsSchema, rawInput, 'ghost_get_posts');
532
- if (!validation.success) {
533
- return validation.errorResponse;
534
- }
535
- const input = validation.data;
536
-
537
- console.error(`Executing tool: ghost_get_posts`);
538
- try {
539
- await loadServices();
540
-
541
- // Build options object with provided parameters
542
- const options = {};
543
- if (input.limit !== undefined) options.limit = input.limit;
544
- if (input.page !== undefined) options.page = input.page;
545
- if (input.status !== undefined) options.status = input.status;
546
- if (input.include !== undefined) options.include = input.include;
547
- if (input.filter !== undefined) options.filter = input.filter;
548
- if (input.order !== undefined) options.order = input.order;
549
- if (input.fields !== undefined) options.fields = input.fields;
550
- if (input.formats !== undefined) options.formats = input.formats;
551
-
552
- const posts = await ghostService.getPosts(options);
553
- console.error(`Retrieved ${posts.length} posts from Ghost.`);
554
-
555
- return {
556
- content: [{ type: 'text', text: JSON.stringify(posts, null, 2) }],
557
- };
558
- } catch (error) {
559
- console.error(`Error in ghost_get_posts:`, error);
560
- if (error.name === 'ZodError') {
561
- const validationError = ValidationError.fromZod(error, 'Posts retrieval');
562
- return {
563
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
564
- isError: true,
565
- };
566
- }
567
- return {
568
- content: [{ type: 'text', text: `Error retrieving posts: ${error.message}` }],
569
- isError: true,
570
- };
571
- }
572
- }
573
- );
619
+ withErrorHandling('ghost_get_posts', getPostsSchema, async (input) => {
620
+ // Build options object with provided parameters
621
+ const options = {};
622
+ if (input.limit !== undefined) options.limit = input.limit;
623
+ if (input.page !== undefined) options.page = input.page;
624
+ if (input.status !== undefined) options.status = input.status;
625
+ if (input.include !== undefined) options.include = input.include;
626
+ if (input.filter !== undefined) options.filter = input.filter;
627
+ if (input.order !== undefined) options.order = input.order;
628
+ if (input.fields !== undefined) options.fields = input.fields;
629
+ if (input.formats !== undefined) options.formats = input.formats;
630
+
631
+ const posts = await ghostService.getPosts(options);
632
+ console.error(`Retrieved ${posts.length} posts from Ghost.`);
633
+
634
+ return {
635
+ content: [{ type: 'text', text: JSON.stringify(posts, null, 2) }],
636
+ };
637
+ })
638
+ );
574
639
 
575
640
  // Get Post Tool
576
641
  server.registerTool(
@@ -579,45 +644,24 @@ server.registerTool(
579
644
  description: 'Retrieves a single post from Ghost CMS by ID or slug.',
580
645
  inputSchema: getPostSchema,
581
646
  },
582
- async (rawInput) => {
583
- const validation = validateToolInput(getPostSchema, rawInput, 'ghost_get_post');
584
- if (!validation.success) {
585
- return validation.errorResponse;
586
- }
587
- const input = validation.data;
588
-
589
- console.error(`Executing tool: ghost_get_post`);
590
- try {
591
- await loadServices();
647
+ withErrorHandling('ghost_get_post', getPostSchema, async (input) => {
648
+ // Build options object
649
+ const options = {};
650
+ if (input.include !== undefined) options.include = input.include;
592
651
 
593
- // Build options object
594
- const options = {};
595
- if (input.include !== undefined) options.include = input.include;
596
-
597
- // Determine identifier (prefer ID over slug)
598
- const identifier = input.id || `slug/${input.slug}`;
652
+ // Determine identifier (prefer ID over slug)
653
+ if (!input.id && !input.slug) {
654
+ throw new Error('Either id or slug is required');
655
+ }
656
+ const identifier = input.id || `slug/${input.slug}`;
599
657
 
600
- const post = await ghostService.getPost(identifier, options);
601
- console.error(`Retrieved post: ${post.title} (ID: ${post.id})`);
658
+ const post = await ghostService.getPost(identifier, options);
659
+ console.error(`Retrieved post: ${post.title} (ID: ${post.id})`);
602
660
 
603
- return {
604
- content: [{ type: 'text', text: JSON.stringify(post, null, 2) }],
605
- };
606
- } catch (error) {
607
- console.error(`Error in ghost_get_post:`, error);
608
- if (error.name === 'ZodError') {
609
- const validationError = ValidationError.fromZod(error, 'Post retrieval');
610
- return {
611
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
612
- isError: true,
613
- };
614
- }
615
- return {
616
- content: [{ type: 'text', text: `Error retrieving post: ${error.message}` }],
617
- isError: true,
618
- };
619
- }
620
- }
661
+ return {
662
+ content: [{ type: 'text', text: JSON.stringify(post, null, 2) }],
663
+ };
664
+ })
621
665
  );
622
666
 
623
667
  // Search Posts Tool
@@ -627,43 +671,19 @@ server.registerTool(
627
671
  description: 'Search for posts in Ghost CMS by query string with optional status filtering.',
628
672
  inputSchema: searchPostsSchema,
629
673
  },
630
- async (rawInput) => {
631
- const validation = validateToolInput(searchPostsSchema, rawInput, 'ghost_search_posts');
632
- if (!validation.success) {
633
- return validation.errorResponse;
634
- }
635
- const input = validation.data;
636
-
637
- console.error(`Executing tool: ghost_search_posts with query: ${input.query}`);
638
- try {
639
- await loadServices();
640
-
641
- // Build options object with provided parameters
642
- const options = {};
643
- if (input.status !== undefined) options.status = input.status;
644
- if (input.limit !== undefined) options.limit = input.limit;
645
-
646
- const posts = await ghostService.searchPosts(input.query, options);
647
- console.error(`Found ${posts.length} posts matching "${input.query}".`);
648
-
649
- return {
650
- content: [{ type: 'text', text: JSON.stringify(posts, null, 2) }],
651
- };
652
- } catch (error) {
653
- console.error(`Error in ghost_search_posts:`, error);
654
- if (error.name === 'ZodError') {
655
- const validationError = ValidationError.fromZod(error, 'Post search');
656
- return {
657
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
658
- isError: true,
659
- };
660
- }
661
- return {
662
- content: [{ type: 'text', text: `Error searching posts: ${error.message}` }],
663
- isError: true,
664
- };
665
- }
666
- }
674
+ withErrorHandling('ghost_search_posts', searchPostsSchema, async (input) => {
675
+ // Build options object with provided parameters
676
+ const options = {};
677
+ if (input.status !== undefined) options.status = input.status;
678
+ if (input.limit !== undefined) options.limit = input.limit;
679
+
680
+ const posts = await ghostService.searchPosts(input.query, options);
681
+ console.error(`Found ${posts.length} posts matching "${input.query}".`);
682
+
683
+ return {
684
+ content: [{ type: 'text', text: JSON.stringify(posts, null, 2) }],
685
+ };
686
+ })
667
687
  );
668
688
 
669
689
  // Update Post Tool
@@ -674,41 +694,17 @@ server.registerTool(
674
694
  'Updates an existing post in Ghost CMS. Can update title, content, status, tags, images, and SEO fields. Only the provided fields are changed; omitted fields remain unchanged. Note: tags and authors arrays are fully replaced, not merged with existing values.',
675
695
  inputSchema: updatePostInputSchema,
676
696
  },
677
- async (rawInput) => {
678
- const validation = validateToolInput(updatePostInputSchema, rawInput, 'ghost_update_post');
679
- if (!validation.success) {
680
- return validation.errorResponse;
681
- }
682
- const input = validation.data;
683
-
684
- console.error(`Executing tool: ghost_update_post for post ID: ${input.id}`);
685
- try {
686
- await loadServices();
687
-
688
- // Extract ID from input and build update data
689
- const { id, ...updateData } = input;
697
+ withErrorHandling('ghost_update_post', updatePostInputSchema, async (input) => {
698
+ // Extract ID from input and build update data
699
+ const { id, ...updateData } = input;
690
700
 
691
- const updatedPost = await ghostService.updatePost(id, updateData);
692
- console.error(`Post updated successfully. Post ID: ${updatedPost.id}`);
701
+ const updatedPost = await ghostService.updatePost(id, updateData);
702
+ console.error(`Post updated successfully. Post ID: ${updatedPost.id}`);
693
703
 
694
- return {
695
- content: [{ type: 'text', text: JSON.stringify(updatedPost, null, 2) }],
696
- };
697
- } catch (error) {
698
- console.error(`Error in ghost_update_post:`, error);
699
- if (error.name === 'ZodError') {
700
- const validationError = ValidationError.fromZod(error, 'Post update');
701
- return {
702
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
703
- isError: true,
704
- };
705
- }
706
- return {
707
- content: [{ type: 'text', text: `Error updating post: ${error.message}` }],
708
- isError: true,
709
- };
710
- }
711
- }
704
+ return {
705
+ content: [{ type: 'text', text: JSON.stringify(updatedPost, null, 2) }],
706
+ };
707
+ })
712
708
  );
713
709
 
714
710
  // Delete Post Tool
@@ -719,38 +715,15 @@ server.registerTool(
719
715
  'Deletes a post from Ghost CMS by ID. This operation is permanent and cannot be undone.',
720
716
  inputSchema: deletePostSchema,
721
717
  },
722
- async (rawInput) => {
723
- const validation = validateToolInput(deletePostSchema, rawInput, 'ghost_delete_post');
724
- if (!validation.success) {
725
- return validation.errorResponse;
726
- }
727
- const { id } = validation.data;
728
-
729
- console.error(`Executing tool: ghost_delete_post for post ID: ${id}`);
730
- try {
731
- await loadServices();
732
-
733
- await ghostService.deletePost(id);
734
- console.error(`Post deleted successfully. Post ID: ${id}`);
735
-
736
- return {
737
- content: [{ type: 'text', text: `Post ${id} has been successfully deleted.` }],
738
- };
739
- } catch (error) {
740
- console.error(`Error in ghost_delete_post:`, error);
741
- if (error.name === 'ZodError') {
742
- const validationError = ValidationError.fromZod(error, 'Post deletion');
743
- return {
744
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
745
- isError: true,
746
- };
747
- }
748
- return {
749
- content: [{ type: 'text', text: `Error deleting post: ${error.message}` }],
750
- isError: true,
751
- };
752
- }
753
- }
718
+ withErrorHandling('ghost_delete_post', deletePostSchema, async (input) => {
719
+ const { id } = input;
720
+ await ghostService.deletePost(id);
721
+ console.error(`Post deleted successfully. Post ID: ${id}`);
722
+
723
+ return {
724
+ content: [{ type: 'text', text: `Post ${id} has been successfully deleted.` }],
725
+ };
726
+ })
754
727
  );
755
728
 
756
729
  // =============================================================================
@@ -804,47 +777,23 @@ server.registerTool(
804
777
  'Retrieves a list of pages from Ghost CMS with pagination, filtering, and sorting options.',
805
778
  inputSchema: pageQuerySchema,
806
779
  },
807
- async (rawInput) => {
808
- const validation = validateToolInput(pageQuerySchema, rawInput, 'ghost_get_pages');
809
- if (!validation.success) {
810
- return validation.errorResponse;
811
- }
812
- const input = validation.data;
813
-
814
- console.error(`Executing tool: ghost_get_pages`);
815
- try {
816
- await loadServices();
817
-
818
- const options = {};
819
- if (input.limit !== undefined) options.limit = input.limit;
820
- if (input.page !== undefined) options.page = input.page;
821
- if (input.filter !== undefined) options.filter = input.filter;
822
- if (input.include !== undefined) options.include = input.include;
823
- if (input.fields !== undefined) options.fields = input.fields;
824
- if (input.formats !== undefined) options.formats = input.formats;
825
- if (input.order !== undefined) options.order = input.order;
826
-
827
- const pages = await ghostService.getPages(options);
828
- console.error(`Retrieved ${pages.length} pages from Ghost.`);
829
-
830
- return {
831
- content: [{ type: 'text', text: JSON.stringify(pages, null, 2) }],
832
- };
833
- } catch (error) {
834
- console.error(`Error in ghost_get_pages:`, error);
835
- if (error.name === 'ZodError') {
836
- const validationError = ValidationError.fromZod(error, 'Page query');
837
- return {
838
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
839
- isError: true,
840
- };
841
- }
842
- return {
843
- content: [{ type: 'text', text: `Error retrieving pages: ${error.message}` }],
844
- isError: true,
845
- };
846
- }
847
- }
780
+ withErrorHandling('ghost_get_pages', pageQuerySchema, async (input) => {
781
+ const options = {};
782
+ if (input.limit !== undefined) options.limit = input.limit;
783
+ if (input.page !== undefined) options.page = input.page;
784
+ if (input.filter !== undefined) options.filter = input.filter;
785
+ if (input.include !== undefined) options.include = input.include;
786
+ if (input.fields !== undefined) options.fields = input.fields;
787
+ if (input.formats !== undefined) options.formats = input.formats;
788
+ if (input.order !== undefined) options.order = input.order;
789
+
790
+ const pages = await ghostService.getPages(options);
791
+ console.error(`Retrieved ${pages.length} pages from Ghost.`);
792
+
793
+ return {
794
+ content: [{ type: 'text', text: JSON.stringify(pages, null, 2) }],
795
+ };
796
+ })
848
797
  );
849
798
 
850
799
  // Get Page Tool
@@ -854,43 +803,22 @@ server.registerTool(
854
803
  description: 'Retrieves a single page from Ghost CMS by ID or slug.',
855
804
  inputSchema: getPageSchema,
856
805
  },
857
- async (rawInput) => {
858
- const validation = validateToolInput(getPageSchema, rawInput, 'ghost_get_page');
859
- if (!validation.success) {
860
- return validation.errorResponse;
861
- }
862
- const input = validation.data;
863
-
864
- console.error(`Executing tool: ghost_get_page`);
865
- try {
866
- await loadServices();
867
-
868
- const options = {};
869
- if (input.include !== undefined) options.include = input.include;
806
+ withErrorHandling('ghost_get_page', getPageSchema, async (input) => {
807
+ const options = {};
808
+ if (input.include !== undefined) options.include = input.include;
870
809
 
871
- const identifier = input.id || `slug/${input.slug}`;
810
+ if (!input.id && !input.slug) {
811
+ throw new Error('Either id or slug is required');
812
+ }
813
+ const identifier = input.id || `slug/${input.slug}`;
872
814
 
873
- const page = await ghostService.getPage(identifier, options);
874
- console.error(`Retrieved page: ${page.title} (ID: ${page.id})`);
815
+ const page = await ghostService.getPage(identifier, options);
816
+ console.error(`Retrieved page: ${page.title} (ID: ${page.id})`);
875
817
 
876
- return {
877
- content: [{ type: 'text', text: JSON.stringify(page, null, 2) }],
878
- };
879
- } catch (error) {
880
- console.error(`Error in ghost_get_page:`, error);
881
- if (error.name === 'ZodError') {
882
- const validationError = ValidationError.fromZod(error, 'Get page');
883
- return {
884
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
885
- isError: true,
886
- };
887
- }
888
- return {
889
- content: [{ type: 'text', text: `Error retrieving page: ${error.message}` }],
890
- isError: true,
891
- };
892
- }
893
- }
818
+ return {
819
+ content: [{ type: 'text', text: JSON.stringify(page, null, 2) }],
820
+ };
821
+ })
894
822
  );
895
823
 
896
824
  // Create Page Tool
@@ -901,38 +829,14 @@ server.registerTool(
901
829
  'Creates a new page in Ghost CMS. Note: Pages do NOT typically use tags (unlike posts).',
902
830
  inputSchema: createPageSchema,
903
831
  },
904
- async (rawInput) => {
905
- const validation = validateToolInput(createPageSchema, rawInput, 'ghost_create_page');
906
- if (!validation.success) {
907
- return validation.errorResponse;
908
- }
909
- const input = validation.data;
910
-
911
- console.error(`Executing tool: ghost_create_page with title: ${input.title}`);
912
- try {
913
- await loadServices();
914
-
915
- const createdPage = await pageService.createPageService(input);
916
- console.error(`Page created successfully. Page ID: ${createdPage.id}`);
832
+ withErrorHandling('ghost_create_page', createPageSchema, async (input) => {
833
+ const createdPage = await pageService.createPageService(input);
834
+ console.error(`Page created successfully. Page ID: ${createdPage.id}`);
917
835
 
918
- return {
919
- content: [{ type: 'text', text: JSON.stringify(createdPage, null, 2) }],
920
- };
921
- } catch (error) {
922
- console.error(`Error in ghost_create_page:`, error);
923
- if (error.name === 'ZodError') {
924
- const validationError = ValidationError.fromZod(error, 'Page creation');
925
- return {
926
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
927
- isError: true,
928
- };
929
- }
930
- return {
931
- content: [{ type: 'text', text: `Error creating page: ${error.message}` }],
932
- isError: true,
933
- };
934
- }
935
- }
836
+ return {
837
+ content: [{ type: 'text', text: JSON.stringify(createdPage, null, 2) }],
838
+ };
839
+ })
936
840
  );
937
841
 
938
842
  // Update Page Tool
@@ -943,40 +847,16 @@ server.registerTool(
943
847
  'Updates an existing page in Ghost CMS. Can update title, content, status, images, and SEO fields. Only the provided fields are changed; omitted fields remain unchanged.',
944
848
  inputSchema: updatePageInputSchema,
945
849
  },
946
- async (rawInput) => {
947
- const validation = validateToolInput(updatePageInputSchema, rawInput, 'ghost_update_page');
948
- if (!validation.success) {
949
- return validation.errorResponse;
950
- }
951
- const input = validation.data;
952
-
953
- console.error(`Executing tool: ghost_update_page for page ID: ${input.id}`);
954
- try {
955
- await loadServices();
850
+ withErrorHandling('ghost_update_page', updatePageInputSchema, async (input) => {
851
+ const { id, ...updateData } = input;
956
852
 
957
- const { id, ...updateData } = input;
853
+ const updatedPage = await ghostService.updatePage(id, updateData);
854
+ console.error(`Page updated successfully. Page ID: ${updatedPage.id}`);
958
855
 
959
- const updatedPage = await ghostService.updatePage(id, updateData);
960
- console.error(`Page updated successfully. Page ID: ${updatedPage.id}`);
961
-
962
- return {
963
- content: [{ type: 'text', text: JSON.stringify(updatedPage, null, 2) }],
964
- };
965
- } catch (error) {
966
- console.error(`Error in ghost_update_page:`, error);
967
- if (error.name === 'ZodError') {
968
- const validationError = ValidationError.fromZod(error, 'Page update');
969
- return {
970
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
971
- isError: true,
972
- };
973
- }
974
- return {
975
- content: [{ type: 'text', text: `Error updating page: ${error.message}` }],
976
- isError: true,
977
- };
978
- }
979
- }
856
+ return {
857
+ content: [{ type: 'text', text: JSON.stringify(updatedPage, null, 2) }],
858
+ };
859
+ })
980
860
  );
981
861
 
982
862
  // Delete Page Tool
@@ -987,38 +867,15 @@ server.registerTool(
987
867
  'Deletes a page from Ghost CMS by ID. This operation is permanent and cannot be undone.',
988
868
  inputSchema: deletePageSchema,
989
869
  },
990
- async (rawInput) => {
991
- const validation = validateToolInput(deletePageSchema, rawInput, 'ghost_delete_page');
992
- if (!validation.success) {
993
- return validation.errorResponse;
994
- }
995
- const { id } = validation.data;
996
-
997
- console.error(`Executing tool: ghost_delete_page for page ID: ${id}`);
998
- try {
999
- await loadServices();
1000
-
1001
- await ghostService.deletePage(id);
1002
- console.error(`Page deleted successfully. Page ID: ${id}`);
1003
-
1004
- return {
1005
- content: [{ type: 'text', text: `Page ${id} has been successfully deleted.` }],
1006
- };
1007
- } catch (error) {
1008
- console.error(`Error in ghost_delete_page:`, error);
1009
- if (error.name === 'ZodError') {
1010
- const validationError = ValidationError.fromZod(error, 'Page deletion');
1011
- return {
1012
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1013
- isError: true,
1014
- };
1015
- }
1016
- return {
1017
- content: [{ type: 'text', text: `Error deleting page: ${error.message}` }],
1018
- isError: true,
1019
- };
1020
- }
1021
- }
870
+ withErrorHandling('ghost_delete_page', deletePageSchema, async (input) => {
871
+ const { id } = input;
872
+ await ghostService.deletePage(id);
873
+ console.error(`Page deleted successfully. Page ID: ${id}`);
874
+
875
+ return {
876
+ content: [{ type: 'text', text: `Page ${id} has been successfully deleted.` }],
877
+ };
878
+ })
1022
879
  );
1023
880
 
1024
881
  // Search Pages Tool
@@ -1028,42 +885,18 @@ server.registerTool(
1028
885
  description: 'Search for pages in Ghost CMS by query string with optional status filtering.',
1029
886
  inputSchema: searchPagesSchema,
1030
887
  },
1031
- async (rawInput) => {
1032
- const validation = validateToolInput(searchPagesSchema, rawInput, 'ghost_search_pages');
1033
- if (!validation.success) {
1034
- return validation.errorResponse;
1035
- }
1036
- const input = validation.data;
1037
-
1038
- console.error(`Executing tool: ghost_search_pages with query: ${input.query}`);
1039
- try {
1040
- await loadServices();
1041
-
1042
- const options = {};
1043
- if (input.status !== undefined) options.status = input.status;
1044
- if (input.limit !== undefined) options.limit = input.limit;
888
+ withErrorHandling('ghost_search_pages', searchPagesSchema, async (input) => {
889
+ const options = {};
890
+ if (input.status !== undefined) options.status = input.status;
891
+ if (input.limit !== undefined) options.limit = input.limit;
1045
892
 
1046
- const pages = await ghostService.searchPages(input.query, options);
1047
- console.error(`Found ${pages.length} pages matching "${input.query}".`);
893
+ const pages = await ghostService.searchPages(input.query, options);
894
+ console.error(`Found ${pages.length} pages matching "${input.query}".`);
1048
895
 
1049
- return {
1050
- content: [{ type: 'text', text: JSON.stringify(pages, null, 2) }],
1051
- };
1052
- } catch (error) {
1053
- console.error(`Error in ghost_search_pages:`, error);
1054
- if (error.name === 'ZodError') {
1055
- const validationError = ValidationError.fromZod(error, 'Page search');
1056
- return {
1057
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1058
- isError: true,
1059
- };
1060
- }
1061
- return {
1062
- content: [{ type: 'text', text: `Error searching pages: ${error.message}` }],
1063
- isError: true,
1064
- };
1065
- }
1066
- }
896
+ return {
897
+ content: [{ type: 'text', text: JSON.stringify(pages, null, 2) }],
898
+ };
899
+ })
1067
900
  );
1068
901
 
1069
902
  // =============================================================================
@@ -1104,38 +937,14 @@ server.registerTool(
1104
937
  description: 'Creates a new member (subscriber) in Ghost CMS.',
1105
938
  inputSchema: createMemberSchema,
1106
939
  },
1107
- async (rawInput) => {
1108
- const validation = validateToolInput(createMemberSchema, rawInput, 'ghost_create_member');
1109
- if (!validation.success) {
1110
- return validation.errorResponse;
1111
- }
1112
- const input = validation.data;
1113
-
1114
- console.error(`Executing tool: ghost_create_member with email: ${input.email}`);
1115
- try {
1116
- await loadServices();
940
+ withErrorHandling('ghost_create_member', createMemberSchema, async (input) => {
941
+ const createdMember = await ghostService.createMember(input);
942
+ console.error(`Member created successfully. Member ID: ${createdMember.id}`);
1117
943
 
1118
- const createdMember = await ghostService.createMember(input);
1119
- console.error(`Member created successfully. Member ID: ${createdMember.id}`);
1120
-
1121
- return {
1122
- content: [{ type: 'text', text: JSON.stringify(createdMember, null, 2) }],
1123
- };
1124
- } catch (error) {
1125
- console.error(`Error in ghost_create_member:`, error);
1126
- if (error.name === 'ZodError') {
1127
- const validationError = ValidationError.fromZod(error, 'Member creation');
1128
- return {
1129
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1130
- isError: true,
1131
- };
1132
- }
1133
- return {
1134
- content: [{ type: 'text', text: `Error creating member: ${error.message}` }],
1135
- isError: true,
1136
- };
1137
- }
1138
- }
944
+ return {
945
+ content: [{ type: 'text', text: JSON.stringify(createdMember, null, 2) }],
946
+ };
947
+ })
1139
948
  );
1140
949
 
1141
950
  // Update Member Tool
@@ -1145,40 +954,16 @@ server.registerTool(
1145
954
  description: 'Updates an existing member in Ghost CMS. All fields except id are optional.',
1146
955
  inputSchema: updateMemberInputSchema,
1147
956
  },
1148
- async (rawInput) => {
1149
- const validation = validateToolInput(updateMemberInputSchema, rawInput, 'ghost_update_member');
1150
- if (!validation.success) {
1151
- return validation.errorResponse;
1152
- }
1153
- const input = validation.data;
1154
-
1155
- console.error(`Executing tool: ghost_update_member for member ID: ${input.id}`);
1156
- try {
1157
- await loadServices();
1158
-
1159
- const { id, ...updateData } = input;
957
+ withErrorHandling('ghost_update_member', updateMemberInputSchema, async (input) => {
958
+ const { id, ...updateData } = input;
1160
959
 
1161
- const updatedMember = await ghostService.updateMember(id, updateData);
1162
- console.error(`Member updated successfully. Member ID: ${updatedMember.id}`);
960
+ const updatedMember = await ghostService.updateMember(id, updateData);
961
+ console.error(`Member updated successfully. Member ID: ${updatedMember.id}`);
1163
962
 
1164
- return {
1165
- content: [{ type: 'text', text: JSON.stringify(updatedMember, null, 2) }],
1166
- };
1167
- } catch (error) {
1168
- console.error(`Error in ghost_update_member:`, error);
1169
- if (error.name === 'ZodError') {
1170
- const validationError = ValidationError.fromZod(error, 'Member update');
1171
- return {
1172
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1173
- isError: true,
1174
- };
1175
- }
1176
- return {
1177
- content: [{ type: 'text', text: `Error updating member: ${error.message}` }],
1178
- isError: true,
1179
- };
1180
- }
1181
- }
963
+ return {
964
+ content: [{ type: 'text', text: JSON.stringify(updatedMember, null, 2) }],
965
+ };
966
+ })
1182
967
  );
1183
968
 
1184
969
  // Delete Member Tool
@@ -1189,38 +974,15 @@ server.registerTool(
1189
974
  'Deletes a member from Ghost CMS by ID. This operation is permanent and cannot be undone.',
1190
975
  inputSchema: deleteMemberSchema,
1191
976
  },
1192
- async (rawInput) => {
1193
- const validation = validateToolInput(deleteMemberSchema, rawInput, 'ghost_delete_member');
1194
- if (!validation.success) {
1195
- return validation.errorResponse;
1196
- }
1197
- const { id } = validation.data;
1198
-
1199
- console.error(`Executing tool: ghost_delete_member for member ID: ${id}`);
1200
- try {
1201
- await loadServices();
1202
-
1203
- await ghostService.deleteMember(id);
1204
- console.error(`Member deleted successfully. Member ID: ${id}`);
1205
-
1206
- return {
1207
- content: [{ type: 'text', text: `Member ${id} has been successfully deleted.` }],
1208
- };
1209
- } catch (error) {
1210
- console.error(`Error in ghost_delete_member:`, error);
1211
- if (error.name === 'ZodError') {
1212
- const validationError = ValidationError.fromZod(error, 'Member deletion');
1213
- return {
1214
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1215
- isError: true,
1216
- };
1217
- }
1218
- return {
1219
- content: [{ type: 'text', text: `Error deleting member: ${error.message}` }],
1220
- isError: true,
1221
- };
1222
- }
1223
- }
977
+ withErrorHandling('ghost_delete_member', deleteMemberSchema, async (input) => {
978
+ const { id } = input;
979
+ await ghostService.deleteMember(id);
980
+ console.error(`Member deleted successfully. Member ID: ${id}`);
981
+
982
+ return {
983
+ content: [{ type: 'text', text: `Member ${id} has been successfully deleted.` }],
984
+ };
985
+ })
1224
986
  );
1225
987
 
1226
988
  // Get Members Tool
@@ -1231,45 +993,21 @@ server.registerTool(
1231
993
  'Retrieves a list of members (subscribers) from Ghost CMS with optional filtering, pagination, and includes.',
1232
994
  inputSchema: getMembersSchema,
1233
995
  },
1234
- async (rawInput) => {
1235
- const validation = validateToolInput(getMembersSchema, rawInput, 'ghost_get_members');
1236
- if (!validation.success) {
1237
- return validation.errorResponse;
1238
- }
1239
- const input = validation.data;
1240
-
1241
- console.error(`Executing tool: ghost_get_members`);
1242
- try {
1243
- await loadServices();
1244
-
1245
- const options = {};
1246
- if (input.limit !== undefined) options.limit = input.limit;
1247
- if (input.page !== undefined) options.page = input.page;
1248
- if (input.filter !== undefined) options.filter = input.filter;
1249
- if (input.order !== undefined) options.order = input.order;
1250
- if (input.include !== undefined) options.include = input.include;
1251
-
1252
- const members = await ghostService.getMembers(options);
1253
- console.error(`Retrieved ${members.length} members from Ghost.`);
1254
-
1255
- return {
1256
- content: [{ type: 'text', text: JSON.stringify(members, null, 2) }],
1257
- };
1258
- } catch (error) {
1259
- console.error(`Error in ghost_get_members:`, error);
1260
- if (error.name === 'ZodError') {
1261
- const validationError = ValidationError.fromZod(error, 'Member query');
1262
- return {
1263
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1264
- isError: true,
1265
- };
1266
- }
1267
- return {
1268
- content: [{ type: 'text', text: `Error retrieving members: ${error.message}` }],
1269
- isError: true,
1270
- };
1271
- }
1272
- }
996
+ withErrorHandling('ghost_get_members', getMembersSchema, async (input) => {
997
+ const options = {};
998
+ if (input.limit !== undefined) options.limit = input.limit;
999
+ if (input.page !== undefined) options.page = input.page;
1000
+ if (input.filter !== undefined) options.filter = input.filter;
1001
+ if (input.order !== undefined) options.order = input.order;
1002
+ if (input.include !== undefined) options.include = input.include;
1003
+
1004
+ const members = await ghostService.getMembers(options);
1005
+ console.error(`Retrieved ${members.length} members from Ghost.`);
1006
+
1007
+ return {
1008
+ content: [{ type: 'text', text: JSON.stringify(members, null, 2) }],
1009
+ };
1010
+ })
1273
1011
  );
1274
1012
 
1275
1013
  // Get Member Tool
@@ -1280,38 +1018,15 @@ server.registerTool(
1280
1018
  'Retrieves a single member from Ghost CMS by ID or email. Provide either id OR email.',
1281
1019
  inputSchema: getMemberSchema,
1282
1020
  },
1283
- async (rawInput) => {
1284
- const validation = validateToolInput(getMemberSchema, rawInput, 'ghost_get_member');
1285
- if (!validation.success) {
1286
- return validation.errorResponse;
1287
- }
1288
- const { id, email } = validation.data;
1289
-
1290
- console.error(`Executing tool: ghost_get_member for ${id ? `ID: ${id}` : `email: ${email}`}`);
1291
- try {
1292
- await loadServices();
1293
-
1294
- const member = await ghostService.getMember({ id, email });
1295
- console.error(`Retrieved member: ${member.email} (ID: ${member.id})`);
1296
-
1297
- return {
1298
- content: [{ type: 'text', text: JSON.stringify(member, null, 2) }],
1299
- };
1300
- } catch (error) {
1301
- console.error(`Error in ghost_get_member:`, error);
1302
- if (error.name === 'ZodError') {
1303
- const validationError = ValidationError.fromZod(error, 'Member lookup');
1304
- return {
1305
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1306
- isError: true,
1307
- };
1308
- }
1309
- return {
1310
- content: [{ type: 'text', text: `Error retrieving member: ${error.message}` }],
1311
- isError: true,
1312
- };
1313
- }
1314
- }
1021
+ withErrorHandling('ghost_get_member', getMemberSchema, async (input) => {
1022
+ const { id, email } = input;
1023
+ const member = await ghostService.getMember({ id, email });
1024
+ console.error(`Retrieved member: ${member.email} (ID: ${member.id})`);
1025
+
1026
+ return {
1027
+ content: [{ type: 'text', text: JSON.stringify(member, null, 2) }],
1028
+ };
1029
+ })
1315
1030
  );
1316
1031
 
1317
1032
  // Search Members Tool
@@ -1321,41 +1036,18 @@ server.registerTool(
1321
1036
  description: 'Searches for members by name or email in Ghost CMS.',
1322
1037
  inputSchema: searchMembersSchema,
1323
1038
  },
1324
- async (rawInput) => {
1325
- const validation = validateToolInput(searchMembersSchema, rawInput, 'ghost_search_members');
1326
- if (!validation.success) {
1327
- return validation.errorResponse;
1328
- }
1329
- const { query, limit } = validation.data;
1330
-
1331
- console.error(`Executing tool: ghost_search_members with query: ${query}`);
1332
- try {
1333
- await loadServices();
1334
-
1335
- const options = {};
1336
- if (limit !== undefined) options.limit = limit;
1039
+ withErrorHandling('ghost_search_members', searchMembersSchema, async (input) => {
1040
+ const { query, limit } = input;
1041
+ const options = {};
1042
+ if (limit !== undefined) options.limit = limit;
1337
1043
 
1338
- const members = await ghostService.searchMembers(query, options);
1339
- console.error(`Found ${members.length} members matching "${query}".`);
1044
+ const members = await ghostService.searchMembers(query, options);
1045
+ console.error(`Found ${members.length} members matching "${query}".`);
1340
1046
 
1341
- return {
1342
- content: [{ type: 'text', text: JSON.stringify(members, null, 2) }],
1343
- };
1344
- } catch (error) {
1345
- console.error(`Error in ghost_search_members:`, error);
1346
- if (error.name === 'ZodError') {
1347
- const validationError = ValidationError.fromZod(error, 'Member search');
1348
- return {
1349
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1350
- isError: true,
1351
- };
1352
- }
1353
- return {
1354
- content: [{ type: 'text', text: `Error searching members: ${error.message}` }],
1355
- isError: true,
1356
- };
1357
- }
1358
- }
1047
+ return {
1048
+ content: [{ type: 'text', text: JSON.stringify(members, null, 2) }],
1049
+ };
1050
+ })
1359
1051
  );
1360
1052
 
1361
1053
  // =============================================================================
@@ -1374,44 +1066,20 @@ server.registerTool(
1374
1066
  description: 'Retrieves a list of newsletters from Ghost CMS with optional filtering.',
1375
1067
  inputSchema: newsletterQuerySchema,
1376
1068
  },
1377
- async (rawInput) => {
1378
- const validation = validateToolInput(newsletterQuerySchema, rawInput, 'ghost_get_newsletters');
1379
- if (!validation.success) {
1380
- return validation.errorResponse;
1381
- }
1382
- const input = validation.data;
1383
-
1384
- console.error(`Executing tool: ghost_get_newsletters`);
1385
- try {
1386
- await loadServices();
1387
-
1388
- const options = {};
1389
- if (input.limit !== undefined) options.limit = input.limit;
1390
- if (input.page !== undefined) options.page = input.page;
1391
- if (input.filter !== undefined) options.filter = input.filter;
1392
- if (input.order !== undefined) options.order = input.order;
1393
-
1394
- const newsletters = await ghostService.getNewsletters(options);
1395
- console.error(`Retrieved ${newsletters.length} newsletters from Ghost.`);
1396
-
1397
- return {
1398
- content: [{ type: 'text', text: JSON.stringify(newsletters, null, 2) }],
1399
- };
1400
- } catch (error) {
1401
- console.error(`Error in ghost_get_newsletters:`, error);
1402
- if (error.name === 'ZodError') {
1403
- const validationError = ValidationError.fromZod(error, 'Newsletter query');
1404
- return {
1405
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1406
- isError: true,
1407
- };
1408
- }
1409
- return {
1410
- content: [{ type: 'text', text: `Error retrieving newsletters: ${error.message}` }],
1411
- isError: true,
1412
- };
1413
- }
1414
- }
1069
+ withErrorHandling('ghost_get_newsletters', newsletterQuerySchema, async (input) => {
1070
+ const options = {};
1071
+ if (input.limit !== undefined) options.limit = input.limit;
1072
+ if (input.page !== undefined) options.page = input.page;
1073
+ if (input.filter !== undefined) options.filter = input.filter;
1074
+ if (input.order !== undefined) options.order = input.order;
1075
+
1076
+ const newsletters = await ghostService.getNewsletters(options);
1077
+ console.error(`Retrieved ${newsletters.length} newsletters from Ghost.`);
1078
+
1079
+ return {
1080
+ content: [{ type: 'text', text: JSON.stringify(newsletters, null, 2) }],
1081
+ };
1082
+ })
1415
1083
  );
1416
1084
 
1417
1085
  // Get Newsletter Tool
@@ -1421,38 +1089,15 @@ server.registerTool(
1421
1089
  description: 'Retrieves a single newsletter from Ghost CMS by ID.',
1422
1090
  inputSchema: getNewsletterSchema,
1423
1091
  },
1424
- async (rawInput) => {
1425
- const validation = validateToolInput(getNewsletterSchema, rawInput, 'ghost_get_newsletter');
1426
- if (!validation.success) {
1427
- return validation.errorResponse;
1428
- }
1429
- const { id } = validation.data;
1430
-
1431
- console.error(`Executing tool: ghost_get_newsletter for ID: ${id}`);
1432
- try {
1433
- await loadServices();
1434
-
1435
- const newsletter = await ghostService.getNewsletter(id);
1436
- console.error(`Retrieved newsletter: ${newsletter.name} (ID: ${newsletter.id})`);
1437
-
1438
- return {
1439
- content: [{ type: 'text', text: JSON.stringify(newsletter, null, 2) }],
1440
- };
1441
- } catch (error) {
1442
- console.error(`Error in ghost_get_newsletter:`, error);
1443
- if (error.name === 'ZodError') {
1444
- const validationError = ValidationError.fromZod(error, 'Newsletter retrieval');
1445
- return {
1446
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1447
- isError: true,
1448
- };
1449
- }
1450
- return {
1451
- content: [{ type: 'text', text: `Error retrieving newsletter: ${error.message}` }],
1452
- isError: true,
1453
- };
1454
- }
1455
- }
1092
+ withErrorHandling('ghost_get_newsletter', getNewsletterSchema, async (input) => {
1093
+ const { id } = input;
1094
+ const newsletter = await ghostService.getNewsletter(id);
1095
+ console.error(`Retrieved newsletter: ${newsletter.name} (ID: ${newsletter.id})`);
1096
+
1097
+ return {
1098
+ content: [{ type: 'text', text: JSON.stringify(newsletter, null, 2) }],
1099
+ };
1100
+ })
1456
1101
  );
1457
1102
 
1458
1103
  // Create Newsletter Tool
@@ -1463,42 +1108,14 @@ server.registerTool(
1463
1108
  'Creates a new newsletter in Ghost CMS with customizable sender settings and display options.',
1464
1109
  inputSchema: createNewsletterSchema,
1465
1110
  },
1466
- async (rawInput) => {
1467
- const validation = validateToolInput(
1468
- createNewsletterSchema,
1469
- rawInput,
1470
- 'ghost_create_newsletter'
1471
- );
1472
- if (!validation.success) {
1473
- return validation.errorResponse;
1474
- }
1475
- const input = validation.data;
1476
-
1477
- console.error(`Executing tool: ghost_create_newsletter with name: ${input.name}`);
1478
- try {
1479
- await loadServices();
1480
-
1481
- const createdNewsletter = await newsletterService.createNewsletterService(input);
1482
- console.error(`Newsletter created successfully. Newsletter ID: ${createdNewsletter.id}`);
1111
+ withErrorHandling('ghost_create_newsletter', createNewsletterSchema, async (input) => {
1112
+ const createdNewsletter = await newsletterService.createNewsletterService(input);
1113
+ console.error(`Newsletter created successfully. Newsletter ID: ${createdNewsletter.id}`);
1483
1114
 
1484
- return {
1485
- content: [{ type: 'text', text: JSON.stringify(createdNewsletter, null, 2) }],
1486
- };
1487
- } catch (error) {
1488
- console.error(`Error in ghost_create_newsletter:`, error);
1489
- if (error.name === 'ZodError') {
1490
- const validationError = ValidationError.fromZod(error, 'Newsletter creation');
1491
- return {
1492
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1493
- isError: true,
1494
- };
1495
- }
1496
- return {
1497
- content: [{ type: 'text', text: `Error creating newsletter: ${error.message}` }],
1498
- isError: true,
1499
- };
1500
- }
1501
- }
1115
+ return {
1116
+ content: [{ type: 'text', text: JSON.stringify(createdNewsletter, null, 2) }],
1117
+ };
1118
+ })
1502
1119
  );
1503
1120
 
1504
1121
  // Update Newsletter Tool
@@ -1509,44 +1126,16 @@ server.registerTool(
1509
1126
  'Updates an existing newsletter in Ghost CMS. Can update name, description, sender settings, and display options.',
1510
1127
  inputSchema: updateNewsletterInputSchema,
1511
1128
  },
1512
- async (rawInput) => {
1513
- const validation = validateToolInput(
1514
- updateNewsletterInputSchema,
1515
- rawInput,
1516
- 'ghost_update_newsletter'
1517
- );
1518
- if (!validation.success) {
1519
- return validation.errorResponse;
1520
- }
1521
- const input = validation.data;
1522
-
1523
- console.error(`Executing tool: ghost_update_newsletter for newsletter ID: ${input.id}`);
1524
- try {
1525
- await loadServices();
1526
-
1527
- const { id, ...updateData } = input;
1129
+ withErrorHandling('ghost_update_newsletter', updateNewsletterInputSchema, async (input) => {
1130
+ const { id, ...updateData } = input;
1528
1131
 
1529
- const updatedNewsletter = await ghostService.updateNewsletter(id, updateData);
1530
- console.error(`Newsletter updated successfully. Newsletter ID: ${updatedNewsletter.id}`);
1132
+ const updatedNewsletter = await ghostService.updateNewsletter(id, updateData);
1133
+ console.error(`Newsletter updated successfully. Newsletter ID: ${updatedNewsletter.id}`);
1531
1134
 
1532
- return {
1533
- content: [{ type: 'text', text: JSON.stringify(updatedNewsletter, null, 2) }],
1534
- };
1535
- } catch (error) {
1536
- console.error(`Error in ghost_update_newsletter:`, error);
1537
- if (error.name === 'ZodError') {
1538
- const validationError = ValidationError.fromZod(error, 'Newsletter update');
1539
- return {
1540
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1541
- isError: true,
1542
- };
1543
- }
1544
- return {
1545
- content: [{ type: 'text', text: `Error updating newsletter: ${error.message}` }],
1546
- isError: true,
1547
- };
1548
- }
1549
- }
1135
+ return {
1136
+ content: [{ type: 'text', text: JSON.stringify(updatedNewsletter, null, 2) }],
1137
+ };
1138
+ })
1550
1139
  );
1551
1140
 
1552
1141
  // Delete Newsletter Tool
@@ -1557,42 +1146,15 @@ server.registerTool(
1557
1146
  'Deletes a newsletter from Ghost CMS by ID. This operation is permanent and cannot be undone.',
1558
1147
  inputSchema: deleteNewsletterSchema,
1559
1148
  },
1560
- async (rawInput) => {
1561
- const validation = validateToolInput(
1562
- deleteNewsletterSchema,
1563
- rawInput,
1564
- 'ghost_delete_newsletter'
1565
- );
1566
- if (!validation.success) {
1567
- return validation.errorResponse;
1568
- }
1569
- const { id } = validation.data;
1570
-
1571
- console.error(`Executing tool: ghost_delete_newsletter for newsletter ID: ${id}`);
1572
- try {
1573
- await loadServices();
1574
-
1575
- await ghostService.deleteNewsletter(id);
1576
- console.error(`Newsletter deleted successfully. Newsletter ID: ${id}`);
1577
-
1578
- return {
1579
- content: [{ type: 'text', text: `Newsletter ${id} has been successfully deleted.` }],
1580
- };
1581
- } catch (error) {
1582
- console.error(`Error in ghost_delete_newsletter:`, error);
1583
- if (error.name === 'ZodError') {
1584
- const validationError = ValidationError.fromZod(error, 'Newsletter deletion');
1585
- return {
1586
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1587
- isError: true,
1588
- };
1589
- }
1590
- return {
1591
- content: [{ type: 'text', text: `Error deleting newsletter: ${error.message}` }],
1592
- isError: true,
1593
- };
1594
- }
1595
- }
1149
+ withErrorHandling('ghost_delete_newsletter', deleteNewsletterSchema, async (input) => {
1150
+ const { id } = input;
1151
+ await ghostService.deleteNewsletter(id);
1152
+ console.error(`Newsletter deleted successfully. Newsletter ID: ${id}`);
1153
+
1154
+ return {
1155
+ content: [{ type: 'text', text: `Newsletter ${id} has been successfully deleted.` }],
1156
+ };
1157
+ })
1596
1158
  );
1597
1159
 
1598
1160
  // --- Tier Tools ---
@@ -1610,38 +1172,14 @@ server.registerTool(
1610
1172
  'Retrieves a list of tiers (membership levels) from Ghost CMS with optional filtering by type (free/paid).',
1611
1173
  inputSchema: tierQuerySchema,
1612
1174
  },
1613
- async (rawInput) => {
1614
- const validation = validateToolInput(tierQuerySchema, rawInput, 'ghost_get_tiers');
1615
- if (!validation.success) {
1616
- return validation.errorResponse;
1617
- }
1618
- const input = validation.data;
1175
+ withErrorHandling('ghost_get_tiers', tierQuerySchema, async (input) => {
1176
+ const tiers = await ghostService.getTiers(input);
1177
+ console.error(`Retrieved ${tiers.length} tiers`);
1619
1178
 
1620
- console.error(`Executing tool: ghost_get_tiers`);
1621
- try {
1622
- await loadServices();
1623
-
1624
- const tiers = await ghostService.getTiers(input);
1625
- console.error(`Retrieved ${tiers.length} tiers`);
1626
-
1627
- return {
1628
- content: [{ type: 'text', text: JSON.stringify(tiers, null, 2) }],
1629
- };
1630
- } catch (error) {
1631
- console.error(`Error in ghost_get_tiers:`, error);
1632
- if (error.name === 'ZodError') {
1633
- const validationError = ValidationError.fromZod(error, 'Tier query');
1634
- return {
1635
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1636
- isError: true,
1637
- };
1638
- }
1639
- return {
1640
- content: [{ type: 'text', text: `Error getting tiers: ${error.message}` }],
1641
- isError: true,
1642
- };
1643
- }
1644
- }
1179
+ return {
1180
+ content: [{ type: 'text', text: JSON.stringify(tiers, null, 2) }],
1181
+ };
1182
+ })
1645
1183
  );
1646
1184
 
1647
1185
  // Get Tier Tool
@@ -1651,38 +1189,15 @@ server.registerTool(
1651
1189
  description: 'Retrieves a single tier (membership level) from Ghost CMS by ID.',
1652
1190
  inputSchema: getTierSchema,
1653
1191
  },
1654
- async (rawInput) => {
1655
- const validation = validateToolInput(getTierSchema, rawInput, 'ghost_get_tier');
1656
- if (!validation.success) {
1657
- return validation.errorResponse;
1658
- }
1659
- const { id } = validation.data;
1660
-
1661
- console.error(`Executing tool: ghost_get_tier for tier ID: ${id}`);
1662
- try {
1663
- await loadServices();
1664
-
1665
- const tier = await ghostService.getTier(id);
1666
- console.error(`Tier retrieved successfully. Tier ID: ${tier.id}`);
1667
-
1668
- return {
1669
- content: [{ type: 'text', text: JSON.stringify(tier, null, 2) }],
1670
- };
1671
- } catch (error) {
1672
- console.error(`Error in ghost_get_tier:`, error);
1673
- if (error.name === 'ZodError') {
1674
- const validationError = ValidationError.fromZod(error, 'Tier retrieval');
1675
- return {
1676
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1677
- isError: true,
1678
- };
1679
- }
1680
- return {
1681
- content: [{ type: 'text', text: `Error getting tier: ${error.message}` }],
1682
- isError: true,
1683
- };
1684
- }
1685
- }
1192
+ withErrorHandling('ghost_get_tier', getTierSchema, async (input) => {
1193
+ const { id } = input;
1194
+ const tier = await ghostService.getTier(id);
1195
+ console.error(`Tier retrieved successfully. Tier ID: ${tier.id}`);
1196
+
1197
+ return {
1198
+ content: [{ type: 'text', text: JSON.stringify(tier, null, 2) }],
1199
+ };
1200
+ })
1686
1201
  );
1687
1202
 
1688
1203
  // Create Tier Tool
@@ -1692,38 +1207,14 @@ server.registerTool(
1692
1207
  description: 'Creates a new tier (membership level) in Ghost CMS with pricing and benefits.',
1693
1208
  inputSchema: createTierSchema,
1694
1209
  },
1695
- async (rawInput) => {
1696
- const validation = validateToolInput(createTierSchema, rawInput, 'ghost_create_tier');
1697
- if (!validation.success) {
1698
- return validation.errorResponse;
1699
- }
1700
- const input = validation.data;
1701
-
1702
- console.error(`Executing tool: ghost_create_tier`);
1703
- try {
1704
- await loadServices();
1705
-
1706
- const tier = await ghostService.createTier(input);
1707
- console.error(`Tier created successfully. Tier ID: ${tier.id}`);
1210
+ withErrorHandling('ghost_create_tier', createTierSchema, async (input) => {
1211
+ const tier = await ghostService.createTier(input);
1212
+ console.error(`Tier created successfully. Tier ID: ${tier.id}`);
1708
1213
 
1709
- return {
1710
- content: [{ type: 'text', text: JSON.stringify(tier, null, 2) }],
1711
- };
1712
- } catch (error) {
1713
- console.error(`Error in ghost_create_tier:`, error);
1714
- if (error.name === 'ZodError') {
1715
- const validationError = ValidationError.fromZod(error, 'Tier creation');
1716
- return {
1717
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1718
- isError: true,
1719
- };
1720
- }
1721
- return {
1722
- content: [{ type: 'text', text: `Error creating tier: ${error.message}` }],
1723
- isError: true,
1724
- };
1725
- }
1726
- }
1214
+ return {
1215
+ content: [{ type: 'text', text: JSON.stringify(tier, null, 2) }],
1216
+ };
1217
+ })
1727
1218
  );
1728
1219
 
1729
1220
  // Update Tier Tool
@@ -1734,40 +1225,16 @@ server.registerTool(
1734
1225
  'Updates an existing tier (membership level) in Ghost CMS. Can update pricing, benefits, and other tier properties.',
1735
1226
  inputSchema: updateTierInputSchema,
1736
1227
  },
1737
- async (rawInput) => {
1738
- const validation = validateToolInput(updateTierInputSchema, rawInput, 'ghost_update_tier');
1739
- if (!validation.success) {
1740
- return validation.errorResponse;
1741
- }
1742
- const input = validation.data;
1743
-
1744
- console.error(`Executing tool: ghost_update_tier for tier ID: ${input.id}`);
1745
- try {
1746
- await loadServices();
1747
-
1748
- const { id, ...updateData } = input;
1228
+ withErrorHandling('ghost_update_tier', updateTierInputSchema, async (input) => {
1229
+ const { id, ...updateData } = input;
1749
1230
 
1750
- const updatedTier = await ghostService.updateTier(id, updateData);
1751
- console.error(`Tier updated successfully. Tier ID: ${updatedTier.id}`);
1231
+ const updatedTier = await ghostService.updateTier(id, updateData);
1232
+ console.error(`Tier updated successfully. Tier ID: ${updatedTier.id}`);
1752
1233
 
1753
- return {
1754
- content: [{ type: 'text', text: JSON.stringify(updatedTier, null, 2) }],
1755
- };
1756
- } catch (error) {
1757
- console.error(`Error in ghost_update_tier:`, error);
1758
- if (error.name === 'ZodError') {
1759
- const validationError = ValidationError.fromZod(error, 'Tier update');
1760
- return {
1761
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1762
- isError: true,
1763
- };
1764
- }
1765
- return {
1766
- content: [{ type: 'text', text: `Error updating tier: ${error.message}` }],
1767
- isError: true,
1768
- };
1769
- }
1770
- }
1234
+ return {
1235
+ content: [{ type: 'text', text: JSON.stringify(updatedTier, null, 2) }],
1236
+ };
1237
+ })
1771
1238
  );
1772
1239
 
1773
1240
  // Delete Tier Tool
@@ -1778,38 +1245,15 @@ server.registerTool(
1778
1245
  'Deletes a tier (membership level) from Ghost CMS by ID. This operation is permanent and cannot be undone.',
1779
1246
  inputSchema: deleteTierSchema,
1780
1247
  },
1781
- async (rawInput) => {
1782
- const validation = validateToolInput(deleteTierSchema, rawInput, 'ghost_delete_tier');
1783
- if (!validation.success) {
1784
- return validation.errorResponse;
1785
- }
1786
- const { id } = validation.data;
1787
-
1788
- console.error(`Executing tool: ghost_delete_tier for tier ID: ${id}`);
1789
- try {
1790
- await loadServices();
1791
-
1792
- await ghostService.deleteTier(id);
1793
- console.error(`Tier deleted successfully. Tier ID: ${id}`);
1794
-
1795
- return {
1796
- content: [{ type: 'text', text: `Tier ${id} has been successfully deleted.` }],
1797
- };
1798
- } catch (error) {
1799
- console.error(`Error in ghost_delete_tier:`, error);
1800
- if (error.name === 'ZodError') {
1801
- const validationError = ValidationError.fromZod(error, 'Tier deletion');
1802
- return {
1803
- content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1804
- isError: true,
1805
- };
1806
- }
1807
- return {
1808
- content: [{ type: 'text', text: `Error deleting tier: ${error.message}` }],
1809
- isError: true,
1810
- };
1811
- }
1812
- }
1248
+ withErrorHandling('ghost_delete_tier', deleteTierSchema, async (input) => {
1249
+ const { id } = input;
1250
+ await ghostService.deleteTier(id);
1251
+ console.error(`Tier deleted successfully. Tier ID: ${id}`);
1252
+
1253
+ return {
1254
+ content: [{ type: 'text', text: `Tier ${id} has been successfully deleted.` }],
1255
+ };
1256
+ })
1813
1257
  );
1814
1258
 
1815
1259
  // --- Main Entry Point ---