@jgardner04/ghost-mcp-server 1.12.1 → 1.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp_server.js CHANGED
@@ -1,298 +1,359 @@
1
- import { MCPServer, Resource, Tool } from '@modelcontextprotocol/sdk/server/index.js';
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
2
5
  import dotenv from 'dotenv';
3
- import { createPostService } from './services/postService.js';
4
- import {
5
- uploadImage as uploadGhostImage,
6
- getTags as getGhostTags,
7
- createTag as createGhostTag,
8
- } from './services/ghostService.js';
9
- import { processImage } from './services/imageProcessingService.js';
10
6
  import axios from 'axios';
11
7
  import fs from 'fs';
12
8
  import path from 'path';
13
9
  import os from 'os';
14
- import { v4 as uuidv4 } from 'uuid';
15
- import { validateImageUrl, createSecureAxiosConfig } from './utils/urlValidator.js';
16
- import { createContextLogger } from './utils/logger.js';
10
+ import crypto from 'crypto';
11
+ import { ValidationError } from './errors/index.js';
12
+ import { validateToolInput } from './utils/validation.js';
17
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';
18
36
 
19
- // Load environment variables (might be redundant if loaded elsewhere, but safe)
37
+ // Load environment variables
20
38
  dotenv.config();
21
39
 
22
- // Initialize logger for MCP server
23
- const logger = createContextLogger('mcp-server');
40
+ // Lazy-loaded modules (to avoid Node.js v25 Buffer compatibility issues at startup)
41
+ let ghostService = null;
42
+ let postService = null;
43
+ let pageService = null;
44
+ let newsletterService = null;
45
+ let imageProcessingService = null;
46
+ let urlValidator = null;
24
47
 
25
- logger.info('Initializing MCP Server');
48
+ const loadServices = async () => {
49
+ if (!ghostService) {
50
+ ghostService = await import('./services/ghostServiceImproved.js');
51
+ postService = await import('./services/postService.js');
52
+ pageService = await import('./services/pageService.js');
53
+ newsletterService = await import('./services/newsletterService.js');
54
+ imageProcessingService = await import('./services/imageProcessingService.js');
55
+ urlValidator = await import('./utils/urlValidator.js');
56
+ }
57
+ };
26
58
 
27
- // Define the server instance
28
- const mcpServer = new MCPServer({
29
- metadata: {
30
- name: 'Ghost CMS Manager',
31
- description: 'MCP Server to manage a Ghost CMS instance using the Admin API.',
32
- // iconUrl: '...',
33
- },
34
- });
59
+ // Generate UUID without external dependency
60
+ const generateUuid = () => crypto.randomUUID();
35
61
 
36
- // --- Define Resources ---
37
-
38
- logger.info('Defining MCP Resources');
39
-
40
- // Ghost Tag Resource
41
- const ghostTagResource = new Resource({
42
- name: 'ghost/tag',
43
- description: 'Represents a tag in Ghost CMS.',
44
- schema: {
45
- type: 'object',
46
- properties: {
47
- id: { type: 'string', description: 'Unique ID of the tag' },
48
- name: { type: 'string', description: 'The name of the tag' },
49
- slug: { type: 'string', description: 'URL-friendly version of the name' },
50
- description: {
51
- type: ['string', 'null'],
52
- description: 'Optional description for the tag',
53
- },
54
- // Add other relevant tag fields if needed (e.g., feature_image, visibility)
55
- },
56
- required: ['id', 'name', 'slug'],
57
- },
58
- });
59
- mcpServer.addResource(ghostTagResource);
60
- logger.info('Added MCP Resource', { resourceName: ghostTagResource.name });
61
-
62
- // Ghost Post Resource
63
- const ghostPostResource = new Resource({
64
- name: 'ghost/post',
65
- description: 'Represents a post in Ghost CMS.',
66
- schema: {
67
- type: 'object',
68
- properties: {
69
- id: { type: 'string', description: 'Unique ID of the post' },
70
- uuid: { type: 'string', description: 'UUID of the post' },
71
- title: { type: 'string', description: 'The title of the post' },
72
- slug: {
73
- type: 'string',
74
- description: 'URL-friendly version of the title',
75
- },
76
- html: {
77
- type: ['string', 'null'],
78
- description: 'The post content as HTML',
79
- },
80
- plaintext: {
81
- type: ['string', 'null'],
82
- description: 'The post content as plain text',
83
- },
84
- feature_image: {
85
- type: ['string', 'null'],
86
- description: 'URL of the featured image',
87
- },
88
- feature_image_alt: {
89
- type: ['string', 'null'],
90
- description: 'Alt text for the featured image',
91
- },
92
- feature_image_caption: {
93
- type: ['string', 'null'],
94
- description: 'Caption for the featured image',
95
- },
96
- featured: {
97
- type: 'boolean',
98
- description: 'Whether the post is featured',
99
- },
100
- status: {
101
- type: 'string',
102
- enum: ['published', 'draft', 'scheduled'],
103
- description: 'Publication status',
104
- },
105
- visibility: {
106
- type: 'string',
107
- enum: ['public', 'members', 'paid'],
108
- description: 'Access level',
109
- },
110
- created_at: {
111
- type: 'string',
112
- format: 'date-time',
113
- description: 'Date/time post was created',
114
- },
115
- updated_at: {
116
- type: 'string',
117
- format: 'date-time',
118
- description: 'Date/time post was last updated',
119
- },
120
- published_at: {
121
- type: ['string', 'null'],
122
- format: 'date-time',
123
- description: 'Date/time post was published or scheduled',
124
- },
125
- custom_excerpt: {
126
- type: ['string', 'null'],
127
- description: 'Custom excerpt for the post',
128
- },
129
- meta_title: { type: ['string', 'null'], description: 'Custom SEO title' },
130
- meta_description: {
131
- type: ['string', 'null'],
132
- description: 'Custom SEO description',
133
- },
134
- tags: {
135
- type: 'array',
136
- description: 'Tags associated with the post',
137
- items: { $ref: '#/definitions/ghost/tag' }, // Reference the ghost/tag resource
138
- },
139
- // Add authors or other relevant fields if needed
140
- },
141
- required: ['id', 'uuid', 'title', 'slug', 'status', 'visibility', 'created_at', 'updated_at'],
142
- definitions: {
143
- // Make the referenced tag resource available within this schema's scope
144
- 'ghost/tag': ghostTagResource.schema,
145
- },
146
- },
62
+ // Helper function for default alt text
63
+ const getDefaultAltText = (filePath) => {
64
+ try {
65
+ const originalFilename = path.basename(filePath).split('.').slice(0, -1).join('.');
66
+ const nameWithoutIds = originalFilename
67
+ .replace(/^(processed-|mcp-download-|mcp-upload-)\d+-\d+-?/, '')
68
+ .replace(/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}-?/, '');
69
+ return nameWithoutIds.replace(/[-_]/g, ' ').trim() || 'Uploaded image';
70
+ } catch (_e) {
71
+ return 'Uploaded image';
72
+ }
73
+ };
74
+
75
+ // Create server instance with new API
76
+ const server = new McpServer({
77
+ name: 'ghost-mcp-server',
78
+ version: '1.0.0',
147
79
  });
148
- mcpServer.addResource(ghostPostResource);
149
- logger.info('Added MCP Resource', { resourceName: ghostPostResource.name });
150
-
151
- // --- Define Tools (Subtasks 8.4 - 8.7) ---
152
- // Placeholder comments for where tools will be added
153
-
154
- // --- End Resource/Tool Definitions ---
155
-
156
- logger.info('Defining MCP Tools');
157
-
158
- // Create Post Tool (Adding this missing tool)
159
- const createPostTool = new Tool({
160
- name: 'ghost_create_post',
161
- description:
162
- 'Creates a new post in Ghost CMS. Handles tag creation/lookup. Returns the created post data.',
163
- inputSchema: {
164
- type: 'object',
165
- properties: {
166
- title: { type: 'string', description: 'The title for the new post.' },
167
- html: {
168
- type: 'string',
169
- description: 'The HTML content for the new post.',
170
- },
171
- status: {
172
- type: 'string',
173
- enum: ['published', 'draft', 'scheduled'],
174
- default: 'draft',
175
- description: 'The status for the post (published, draft, scheduled). Defaults to draft.',
176
- },
177
- tags: {
178
- type: 'array',
179
- items: { type: 'string' },
180
- description: 'Optional: An array of tag names (strings) to associate with the post.',
181
- },
182
- published_at: {
183
- type: 'string',
184
- format: 'date-time',
185
- description:
186
- 'Optional: The ISO 8601 date/time for publishing or scheduling. Required if status is scheduled.',
187
- },
188
- custom_excerpt: {
189
- type: 'string',
190
- description: 'Optional: A custom short summary for the post.',
191
- },
192
- feature_image: {
193
- type: 'string',
194
- format: 'url',
195
- description:
196
- 'Optional: URL of the image (e.g., from ghost_upload_image tool) to use as the featured image.',
197
- },
198
- feature_image_alt: {
199
- type: 'string',
200
- description: 'Optional: Alt text for the featured image.',
201
- },
202
- feature_image_caption: {
203
- type: 'string',
204
- description: 'Optional: Caption for the featured image.',
205
- },
206
- meta_title: {
207
- type: 'string',
208
- description:
209
- 'Optional: Custom title for SEO (max 300 chars). Defaults to post title if omitted.',
210
- },
211
- meta_description: {
212
- type: 'string',
213
- description:
214
- 'Optional: Custom description for SEO (max 500 chars). Defaults to excerpt or generated summary if omitted.',
215
- },
216
- },
217
- required: ['title', 'html'],
218
- },
219
- outputSchema: {
220
- $ref: 'ghost/post#/schema',
221
- },
222
- implementation: async (input) => {
223
- logger.toolExecution(createPostTool.name, input);
224
- try {
225
- const createdPost = await createPostService(input);
226
- logger.toolSuccess(createPostTool.name, createdPost, { postId: createdPost.id });
227
- return createdPost;
228
- } catch (error) {
229
- logger.toolError(createPostTool.name, error);
230
- throw new Error(`Failed to create Ghost post: ${error.message}`);
231
- }
232
- },
80
+
81
+ // --- Register Tools ---
82
+
83
+ // --- Schema Definitions for Tools ---
84
+ const getTagsSchema = tagQuerySchema.partial();
85
+ const getTagSchema = z
86
+ .object({
87
+ id: ghostIdSchema.optional().describe('The ID of the tag to retrieve.'),
88
+ slug: z.string().optional().describe('The slug of the tag to retrieve.'),
89
+ include: z
90
+ .string()
91
+ .optional()
92
+ .describe('Additional resources to include (e.g., "count.posts").'),
93
+ })
94
+ .refine((data) => data.id || data.slug, {
95
+ message: 'Either id or slug is required to retrieve a tag',
96
+ });
97
+ const updateTagInputSchema = updateTagSchema.extend({ id: ghostIdSchema });
98
+ const deleteTagSchema = z.object({ id: ghostIdSchema });
99
+
100
+ // Get Tags Tool
101
+ server.tool(
102
+ 'ghost_get_tags',
103
+ 'Retrieves a list of tags from Ghost CMS. Can optionally filter by tag name.',
104
+ getTagsSchema,
105
+ async (rawInput) => {
106
+ const validation = validateToolInput(getTagsSchema, rawInput, 'ghost_get_tags');
107
+ if (!validation.success) {
108
+ return validation.errorResponse;
109
+ }
110
+ const input = validation.data;
111
+
112
+ console.error(`Executing tool: ghost_get_tags`);
113
+ try {
114
+ await loadServices();
115
+ const tags = await ghostService.getTags();
116
+ let result = tags;
117
+
118
+ if (input.name) {
119
+ result = tags.filter((tag) => tag.name.toLowerCase() === input.name.toLowerCase());
120
+ console.error(`Filtered tags by name "${input.name}". Found ${result.length} match(es).`);
121
+ } else {
122
+ console.error(`Retrieved ${tags.length} tags from Ghost.`);
123
+ }
124
+
125
+ return {
126
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
127
+ };
128
+ } catch (error) {
129
+ console.error(`Error in ghost_get_tags:`, error);
130
+ if (error.name === 'ZodError') {
131
+ const validationError = ValidationError.fromZod(error, 'Tags retrieval');
132
+ return {
133
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
134
+ isError: true,
135
+ };
136
+ }
137
+ return {
138
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
139
+ isError: true,
140
+ };
141
+ }
142
+ }
143
+ );
144
+
145
+ // Create Tag Tool
146
+ server.tool(
147
+ 'ghost_create_tag',
148
+ 'Creates a new tag in Ghost CMS.',
149
+ createTagSchema,
150
+ async (rawInput) => {
151
+ const validation = validateToolInput(createTagSchema, rawInput, 'ghost_create_tag');
152
+ if (!validation.success) {
153
+ return validation.errorResponse;
154
+ }
155
+ const input = validation.data;
156
+
157
+ console.error(`Executing tool: ghost_create_tag with name: ${input.name}`);
158
+ try {
159
+ await loadServices();
160
+ const createdTag = await ghostService.createTag(input);
161
+ console.error(`Tag created successfully. Tag ID: ${createdTag.id}`);
162
+
163
+ return {
164
+ content: [{ type: 'text', text: JSON.stringify(createdTag, null, 2) }],
165
+ };
166
+ } catch (error) {
167
+ console.error(`Error in ghost_create_tag:`, error);
168
+ if (error.name === 'ZodError') {
169
+ const validationError = ValidationError.fromZod(error, 'Tag creation');
170
+ return {
171
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
172
+ isError: true,
173
+ };
174
+ }
175
+ return {
176
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
177
+ isError: true,
178
+ };
179
+ }
180
+ }
181
+ );
182
+
183
+ // Get Tag Tool
184
+ server.tool(
185
+ 'ghost_get_tag',
186
+ 'Retrieves a single tag from Ghost CMS by ID or slug.',
187
+ getTagSchema,
188
+ async (rawInput) => {
189
+ const validation = validateToolInput(getTagSchema, rawInput, 'ghost_get_tag');
190
+ if (!validation.success) {
191
+ return validation.errorResponse;
192
+ }
193
+ const { id, slug, include } = validation.data;
194
+
195
+ console.error(`Executing tool: ghost_get_tag`);
196
+ try {
197
+ await loadServices();
198
+
199
+ // If slug is provided, use the slug/slug-name format
200
+ const identifier = slug ? `slug/${slug}` : id;
201
+ const options = include ? { include } : {};
202
+
203
+ const tag = await ghostService.getTag(identifier, options);
204
+ console.error(`Tag retrieved successfully. Tag ID: ${tag.id}`);
205
+
206
+ return {
207
+ content: [{ type: 'text', text: JSON.stringify(tag, null, 2) }],
208
+ };
209
+ } catch (error) {
210
+ console.error(`Error in ghost_get_tag:`, error);
211
+ if (error.name === 'ZodError') {
212
+ const validationError = ValidationError.fromZod(error, 'Tag retrieval');
213
+ return {
214
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
215
+ isError: true,
216
+ };
217
+ }
218
+ return {
219
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
220
+ isError: true,
221
+ };
222
+ }
223
+ }
224
+ );
225
+
226
+ // Update Tag Tool
227
+ server.tool(
228
+ 'ghost_update_tag',
229
+ 'Updates an existing tag in Ghost CMS.',
230
+ updateTagInputSchema,
231
+ async (rawInput) => {
232
+ const validation = validateToolInput(updateTagInputSchema, rawInput, 'ghost_update_tag');
233
+ if (!validation.success) {
234
+ return validation.errorResponse;
235
+ }
236
+ const input = validation.data;
237
+
238
+ console.error(`Executing tool: ghost_update_tag for ID: ${input.id}`);
239
+ try {
240
+ if (!input.id) {
241
+ throw new Error('Tag ID is required');
242
+ }
243
+
244
+ await loadServices();
245
+
246
+ // Build update data object with only provided fields (exclude id from update data)
247
+ const { id, ...updateData } = input;
248
+
249
+ const updatedTag = await ghostService.updateTag(id, updateData);
250
+ console.error(`Tag updated successfully. Tag ID: ${updatedTag.id}`);
251
+
252
+ return {
253
+ content: [{ type: 'text', text: JSON.stringify(updatedTag, null, 2) }],
254
+ };
255
+ } catch (error) {
256
+ console.error(`Error in ghost_update_tag:`, error);
257
+ if (error.name === 'ZodError') {
258
+ const validationError = ValidationError.fromZod(error, 'Tag update');
259
+ return {
260
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
261
+ isError: true,
262
+ };
263
+ }
264
+ return {
265
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
266
+ isError: true,
267
+ };
268
+ }
269
+ }
270
+ );
271
+
272
+ // Delete Tag Tool
273
+ server.tool(
274
+ 'ghost_delete_tag',
275
+ 'Deletes a tag from Ghost CMS by ID. This operation is permanent.',
276
+ deleteTagSchema,
277
+ async (rawInput) => {
278
+ const validation = validateToolInput(deleteTagSchema, rawInput, 'ghost_delete_tag');
279
+ if (!validation.success) {
280
+ return validation.errorResponse;
281
+ }
282
+ const { id } = validation.data;
283
+
284
+ console.error(`Executing tool: ghost_delete_tag for ID: ${id}`);
285
+ try {
286
+ if (!id) {
287
+ throw new Error('Tag ID is required');
288
+ }
289
+
290
+ await loadServices();
291
+
292
+ await ghostService.deleteTag(id);
293
+ console.error(`Tag deleted successfully. Tag ID: ${id}`);
294
+
295
+ return {
296
+ content: [{ type: 'text', text: `Tag with ID ${id} has been successfully deleted.` }],
297
+ };
298
+ } catch (error) {
299
+ console.error(`Error in ghost_delete_tag:`, error);
300
+ if (error.name === 'ZodError') {
301
+ const validationError = ValidationError.fromZod(error, 'Tag deletion');
302
+ return {
303
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
304
+ isError: true,
305
+ };
306
+ }
307
+ return {
308
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
309
+ isError: true,
310
+ };
311
+ }
312
+ }
313
+ );
314
+
315
+ // --- Image Schema ---
316
+ const uploadImageSchema = z.object({
317
+ imageUrl: z.string().describe('The publicly accessible URL of the image to upload.'),
318
+ alt: z
319
+ .string()
320
+ .optional()
321
+ .describe('Alt text for the image. If omitted, a default will be generated from the filename.'),
233
322
  });
234
- mcpServer.addTool(createPostTool);
235
- logger.info('Added MCP Tool', { toolName: createPostTool.name });
236
323
 
237
324
  // Upload Image Tool
238
- const uploadImageTool = new Tool({
239
- name: 'ghost_upload_image',
240
- description:
241
- 'Downloads an image from a URL, processes it, uploads it to Ghost CMS, and returns the final Ghost image URL and alt text.',
242
- inputSchema: {
243
- type: 'object',
244
- properties: {
245
- imageUrl: {
246
- type: 'string',
247
- format: 'url',
248
- description: 'The publicly accessible URL of the image to upload.',
249
- },
250
- alt: {
251
- type: 'string',
252
- description:
253
- 'Optional: Alt text for the image. If omitted, a default will be generated from the filename.',
254
- },
255
- // filenameHint: { type: 'string', description: 'Optional: A hint for the original filename, used for default alt text generation.' }
256
- },
257
- required: ['imageUrl'],
258
- },
259
- outputSchema: {
260
- type: 'object',
261
- properties: {
262
- url: {
263
- type: 'string',
264
- format: 'url',
265
- description: 'The final URL of the image hosted on Ghost.',
266
- },
267
- alt: {
268
- type: 'string',
269
- description: 'The alt text determined for the image.',
270
- },
271
- },
272
- required: ['url', 'alt'],
273
- },
274
- implementation: async (input) => {
275
- logger.toolExecution(uploadImageTool.name, { imageUrl: input.imageUrl });
276
- const { imageUrl, alt } = input;
325
+ server.tool(
326
+ 'ghost_upload_image',
327
+ 'Downloads an image from a URL, processes it, uploads it to Ghost CMS, and returns the final Ghost image URL and alt text.',
328
+ uploadImageSchema,
329
+ async (rawInput) => {
330
+ const validation = validateToolInput(uploadImageSchema, rawInput, 'ghost_upload_image');
331
+ if (!validation.success) {
332
+ return validation.errorResponse;
333
+ }
334
+ const { imageUrl, alt } = validation.data;
335
+
336
+ console.error(`Executing tool: ghost_upload_image for URL: ${imageUrl}`);
277
337
  let downloadedPath = null;
278
338
  let processedPath = null;
279
339
 
280
340
  try {
281
- // --- 1. Validate URL for SSRF protection ---
282
- const urlValidation = validateImageUrl(imageUrl);
341
+ await loadServices();
342
+
343
+ // 1. Validate URL for SSRF protection
344
+ const urlValidation = urlValidator.validateImageUrl(imageUrl);
283
345
  if (!urlValidation.isValid) {
284
346
  throw new Error(`Invalid image URL: ${urlValidation.error}`);
285
347
  }
286
348
 
287
- // --- 2. Download the image with security controls ---
288
- const axiosConfig = createSecureAxiosConfig(urlValidation.sanitizedUrl);
349
+ // 2. Download the image with security controls
350
+ const axiosConfig = urlValidator.createSecureAxiosConfig(urlValidation.sanitizedUrl);
289
351
  const response = await axios(axiosConfig);
290
- // Generate a unique temporary filename
291
352
  const tempDir = os.tmpdir();
292
- const extension = path.extname(imageUrl.split('?')[0]) || '.tmp'; // Basic extension extraction
353
+ const extension = path.extname(imageUrl.split('?')[0]) || '.tmp';
293
354
  const originalFilenameHint =
294
- path.basename(imageUrl.split('?')[0]) || `image-${uuidv4()}${extension}`;
295
- downloadedPath = path.join(tempDir, `mcp-download-${uuidv4()}${extension}`);
355
+ path.basename(imageUrl.split('?')[0]) || `image-${generateUuid()}${extension}`;
356
+ downloadedPath = path.join(tempDir, `mcp-download-${generateUuid()}${extension}`);
296
357
 
297
358
  const writer = fs.createWriteStream(downloadedPath);
298
359
  response.data.pipe(writer);
@@ -303,170 +364,1357 @@ const uploadImageTool = new Tool({
303
364
  });
304
365
  // Track temp file for cleanup on process exit
305
366
  trackTempFile(downloadedPath);
306
- logger.fileOperation('download', downloadedPath);
367
+ console.error(`Downloaded image to temporary path: ${downloadedPath}`);
307
368
 
308
- // --- 3. Process the image (Optional) ---
309
- // Using the service from subtask 4.2
310
- processedPath = await processImage(downloadedPath, tempDir);
369
+ // 3. Process the image
370
+ processedPath = await imageProcessingService.processImage(downloadedPath, tempDir);
311
371
  // Track processed file for cleanup on process exit
312
372
  if (processedPath !== downloadedPath) {
313
373
  trackTempFile(processedPath);
314
374
  }
315
- logger.fileOperation('process', processedPath);
375
+ console.error(`Processed image path: ${processedPath}`);
316
376
 
317
- // --- 4. Determine Alt Text ---
318
- // Using similar logic from subtask 4.4
377
+ // 4. Determine Alt Text
319
378
  const defaultAlt = getDefaultAltText(originalFilenameHint);
320
379
  const finalAltText = alt || defaultAlt;
321
- logger.debug('Generated alt text', { altText: finalAltText });
380
+ console.error(`Using alt text: "${finalAltText}"`);
322
381
 
323
- // --- 5. Upload processed image to Ghost ---
324
- const uploadResult = await uploadGhostImage(processedPath);
325
- logger.info('Image uploaded to Ghost', { ghostUrl: uploadResult.url });
382
+ // 5. Upload processed image to Ghost
383
+ const uploadResult = await ghostService.uploadImage(processedPath);
384
+ console.error(`Uploaded processed image to Ghost: ${uploadResult.url}`);
326
385
 
327
- // --- 6. Return result ---
328
- return {
386
+ // 6. Return result
387
+ const result = {
329
388
  url: uploadResult.url,
330
389
  alt: finalAltText,
331
390
  };
391
+
392
+ return {
393
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
394
+ };
332
395
  } catch (error) {
333
- logger.toolError(uploadImageTool.name, error, { imageUrl });
334
- // Add more specific error handling (download failed, processing failed, upload failed)
335
- throw new Error(`Failed to upload image from URL ${imageUrl}: ${error.message}`);
396
+ console.error(`Error in ghost_upload_image:`, error);
397
+ return {
398
+ content: [{ type: 'text', text: `Error uploading image: ${error.message}` }],
399
+ isError: true,
400
+ };
336
401
  } finally {
337
- // --- 7. Cleanup temporary files with proper async/await ---
338
- await cleanupTempFiles([downloadedPath, processedPath], logger);
402
+ // Cleanup temporary files with proper async/await
403
+ await cleanupTempFiles([downloadedPath, processedPath], console);
339
404
  }
340
- },
405
+ }
406
+ );
407
+
408
+ // --- Post Schema Definitions ---
409
+ const getPostsSchema = postQuerySchema.extend({
410
+ status: z
411
+ .enum(['published', 'draft', 'scheduled', 'all'])
412
+ .optional()
413
+ .describe('Filter posts by status. Options: published, draft, scheduled, all.'),
414
+ });
415
+ const getPostSchema = z
416
+ .object({
417
+ id: ghostIdSchema.optional().describe('The ID of the post to retrieve.'),
418
+ slug: z.string().optional().describe('The slug of the post to retrieve.'),
419
+ include: z
420
+ .string()
421
+ .optional()
422
+ .describe('Comma-separated list of relations to include (e.g., "tags,authors").'),
423
+ })
424
+ .refine((data) => data.id || data.slug, {
425
+ message: 'Either id or slug is required to retrieve a post',
426
+ });
427
+ const searchPostsSchema = z.object({
428
+ query: z.string().min(1).describe('Search query to find in post titles.'),
429
+ status: z
430
+ .enum(['published', 'draft', 'scheduled', 'all'])
431
+ .optional()
432
+ .describe('Filter by post status. Default searches all statuses.'),
433
+ limit: z
434
+ .number()
435
+ .int()
436
+ .min(1)
437
+ .max(50)
438
+ .optional()
439
+ .describe('Maximum number of results (1-50). Default is 15.'),
341
440
  });
441
+ const updatePostInputSchema = updatePostSchema.extend({ id: ghostIdSchema });
442
+ const deletePostSchema = z.object({ id: ghostIdSchema });
342
443
 
343
- // Helper function for default alt text (similar to imageController)
344
- const getDefaultAltText = (filePath) => {
345
- try {
346
- const originalFilename = path.basename(filePath).split('.').slice(0, -1).join('.');
347
- const nameWithoutIds = originalFilename
348
- .replace(/^(processed-|mcp-download-|mcp-upload-)\d+-\d+-?/, '')
349
- .replace(/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}-?/, ''); // Remove UUIDs too
350
- return nameWithoutIds.replace(/[-_]/g, ' ').trim() || 'Uploaded image';
351
- } catch (_e) {
352
- return 'Uploaded image';
444
+ // Create Post Tool
445
+ server.tool(
446
+ 'ghost_create_post',
447
+ 'Creates a new post in Ghost CMS.',
448
+ createPostSchema,
449
+ async (rawInput) => {
450
+ const validation = validateToolInput(createPostSchema, rawInput, 'ghost_create_post');
451
+ if (!validation.success) {
452
+ return validation.errorResponse;
453
+ }
454
+ const input = validation.data;
455
+
456
+ console.error(`Executing tool: ghost_create_post with title: ${input.title}`);
457
+ try {
458
+ await loadServices();
459
+ const createdPost = await postService.createPostService(input);
460
+ console.error(`Post created successfully. Post ID: ${createdPost.id}`);
461
+
462
+ return {
463
+ content: [{ type: 'text', text: JSON.stringify(createdPost, null, 2) }],
464
+ };
465
+ } catch (error) {
466
+ console.error(`Error in ghost_create_post:`, error);
467
+ if (error.name === 'ZodError') {
468
+ const validationError = ValidationError.fromZod(error, 'Post creation');
469
+ return {
470
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
471
+ isError: true,
472
+ };
473
+ }
474
+ return {
475
+ content: [{ type: 'text', text: `Error creating post: ${error.message}` }],
476
+ isError: true,
477
+ };
478
+ }
353
479
  }
354
- };
480
+ );
355
481
 
356
- mcpServer.addTool(uploadImageTool);
357
- logger.info('Added MCP Tool', { toolName: uploadImageTool.name });
482
+ // Get Posts Tool
483
+ server.tool(
484
+ 'ghost_get_posts',
485
+ 'Retrieves a list of posts from Ghost CMS with pagination, filtering, and sorting options.',
486
+ getPostsSchema,
487
+ async (rawInput) => {
488
+ const validation = validateToolInput(getPostsSchema, rawInput, 'ghost_get_posts');
489
+ if (!validation.success) {
490
+ return validation.errorResponse;
491
+ }
492
+ const input = validation.data;
358
493
 
359
- // Get Tags Tool
360
- const getTagsTool = new Tool({
361
- name: 'ghost_get_tags',
362
- description: 'Retrieves a list of tags from Ghost CMS. Can optionally filter by tag name.',
363
- inputSchema: {
364
- type: 'object',
365
- properties: {
366
- name: {
367
- type: 'string',
368
- description: 'Optional: The exact name of the tag to search for.',
369
- },
370
- },
371
- },
372
- outputSchema: {
373
- type: 'array',
374
- items: { $ref: 'ghost/tag#/schema' }, // Output is an array of ghost/tag resources
375
- },
376
- implementation: async (input) => {
377
- logger.toolExecution(getTagsTool.name, input);
378
- try {
379
- const tags = await getGhostTags(input?.name); // Pass name if provided
380
- logger.toolSuccess(getTagsTool.name, tags, { tagCount: tags.length });
381
- // TODO: Validate/map output against schema if necessary
382
- return tags;
383
- } catch (error) {
384
- logger.toolError(getTagsTool.name, error);
385
- throw new Error(`Failed to get Ghost tags: ${error.message}`);
386
- }
387
- },
388
- });
389
- mcpServer.addTool(getTagsTool);
390
- logger.info('Added MCP Tool', { toolName: getTagsTool.name });
494
+ console.error(`Executing tool: ghost_get_posts`);
495
+ try {
496
+ await loadServices();
391
497
 
392
- // Create Tag Tool
393
- const createTagTool = new Tool({
394
- name: 'ghost_create_tag',
395
- description: 'Creates a new tag in Ghost CMS. Returns the created tag.',
396
- inputSchema: {
397
- type: 'object',
398
- properties: {
399
- name: { type: 'string', description: 'The name for the new tag.' },
400
- description: {
401
- type: 'string',
402
- description: 'Optional: A description for the tag (max 500 chars).',
403
- },
404
- slug: {
405
- type: 'string',
406
- description:
407
- 'Optional: A URL-friendly slug. If omitted, Ghost generates one from the name.',
408
- },
409
- // Add other createable fields like color, feature_image etc. if needed
410
- },
411
- required: ['name'],
412
- },
413
- outputSchema: {
414
- $ref: 'ghost/tag#/schema', // Output is a single ghost/tag resource
415
- },
416
- implementation: async (input) => {
417
- logger.toolExecution(createTagTool.name, input);
418
- try {
419
- // Basic validation happens via inputSchema, more specific validation (like slug format) could be added here if not in service
420
- const newTag = await createGhostTag(input);
421
- logger.toolSuccess(createTagTool.name, newTag, { tagId: newTag.id });
422
- // TODO: Validate/map output against schema if necessary
423
- return newTag;
424
- } catch (error) {
425
- logger.toolError(createTagTool.name, error);
426
- throw new Error(`Failed to create Ghost tag: ${error.message}`);
427
- }
428
- },
498
+ // Build options object with provided parameters
499
+ const options = {};
500
+ if (input.limit !== undefined) options.limit = input.limit;
501
+ if (input.page !== undefined) options.page = input.page;
502
+ if (input.status !== undefined) options.status = input.status;
503
+ if (input.include !== undefined) options.include = input.include;
504
+ if (input.filter !== undefined) options.filter = input.filter;
505
+ if (input.order !== undefined) options.order = input.order;
506
+
507
+ const posts = await ghostService.getPosts(options);
508
+ console.error(`Retrieved ${posts.length} posts from Ghost.`);
509
+
510
+ return {
511
+ content: [{ type: 'text', text: JSON.stringify(posts, null, 2) }],
512
+ };
513
+ } catch (error) {
514
+ console.error(`Error in ghost_get_posts:`, error);
515
+ if (error.name === 'ZodError') {
516
+ const validationError = ValidationError.fromZod(error, 'Posts retrieval');
517
+ return {
518
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
519
+ isError: true,
520
+ };
521
+ }
522
+ return {
523
+ content: [{ type: 'text', text: `Error retrieving posts: ${error.message}` }],
524
+ isError: true,
525
+ };
526
+ }
527
+ }
528
+ );
529
+
530
+ // Get Post Tool
531
+ server.tool(
532
+ 'ghost_get_post',
533
+ 'Retrieves a single post from Ghost CMS by ID or slug.',
534
+ getPostSchema,
535
+ async (rawInput) => {
536
+ const validation = validateToolInput(getPostSchema, rawInput, 'ghost_get_post');
537
+ if (!validation.success) {
538
+ return validation.errorResponse;
539
+ }
540
+ const input = validation.data;
541
+
542
+ console.error(`Executing tool: ghost_get_post`);
543
+ try {
544
+ await loadServices();
545
+
546
+ // Build options object
547
+ const options = {};
548
+ if (input.include !== undefined) options.include = input.include;
549
+
550
+ // Determine identifier (prefer ID over slug)
551
+ const identifier = input.id || `slug/${input.slug}`;
552
+
553
+ const post = await ghostService.getPost(identifier, options);
554
+ console.error(`Retrieved post: ${post.title} (ID: ${post.id})`);
555
+
556
+ return {
557
+ content: [{ type: 'text', text: JSON.stringify(post, null, 2) }],
558
+ };
559
+ } catch (error) {
560
+ console.error(`Error in ghost_get_post:`, error);
561
+ if (error.name === 'ZodError') {
562
+ const validationError = ValidationError.fromZod(error, 'Post retrieval');
563
+ return {
564
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
565
+ isError: true,
566
+ };
567
+ }
568
+ return {
569
+ content: [{ type: 'text', text: `Error retrieving post: ${error.message}` }],
570
+ isError: true,
571
+ };
572
+ }
573
+ }
574
+ );
575
+
576
+ // Search Posts Tool
577
+ server.tool(
578
+ 'ghost_search_posts',
579
+ 'Search for posts in Ghost CMS by query string with optional status filtering.',
580
+ searchPostsSchema,
581
+ async (rawInput) => {
582
+ const validation = validateToolInput(searchPostsSchema, rawInput, 'ghost_search_posts');
583
+ if (!validation.success) {
584
+ return validation.errorResponse;
585
+ }
586
+ const input = validation.data;
587
+
588
+ console.error(`Executing tool: ghost_search_posts with query: ${input.query}`);
589
+ try {
590
+ await loadServices();
591
+
592
+ // Build options object with provided parameters
593
+ const options = {};
594
+ if (input.status !== undefined) options.status = input.status;
595
+ if (input.limit !== undefined) options.limit = input.limit;
596
+
597
+ const posts = await ghostService.searchPosts(input.query, options);
598
+ console.error(`Found ${posts.length} posts matching "${input.query}".`);
599
+
600
+ return {
601
+ content: [{ type: 'text', text: JSON.stringify(posts, null, 2) }],
602
+ };
603
+ } catch (error) {
604
+ console.error(`Error in ghost_search_posts:`, error);
605
+ if (error.name === 'ZodError') {
606
+ const validationError = ValidationError.fromZod(error, 'Post search');
607
+ return {
608
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
609
+ isError: true,
610
+ };
611
+ }
612
+ return {
613
+ content: [{ type: 'text', text: `Error searching posts: ${error.message}` }],
614
+ isError: true,
615
+ };
616
+ }
617
+ }
618
+ );
619
+
620
+ // Update Post Tool
621
+ server.tool(
622
+ 'ghost_update_post',
623
+ 'Updates an existing post in Ghost CMS. Can update title, content, status, tags, images, and SEO fields.',
624
+ updatePostInputSchema,
625
+ async (rawInput) => {
626
+ const validation = validateToolInput(updatePostInputSchema, rawInput, 'ghost_update_post');
627
+ if (!validation.success) {
628
+ return validation.errorResponse;
629
+ }
630
+ const input = validation.data;
631
+
632
+ console.error(`Executing tool: ghost_update_post for post ID: ${input.id}`);
633
+ try {
634
+ await loadServices();
635
+
636
+ // Extract ID from input and build update data
637
+ const { id, ...updateData } = input;
638
+
639
+ const updatedPost = await ghostService.updatePost(id, updateData);
640
+ console.error(`Post updated successfully. Post ID: ${updatedPost.id}`);
641
+
642
+ return {
643
+ content: [{ type: 'text', text: JSON.stringify(updatedPost, null, 2) }],
644
+ };
645
+ } catch (error) {
646
+ console.error(`Error in ghost_update_post:`, error);
647
+ if (error.name === 'ZodError') {
648
+ const validationError = ValidationError.fromZod(error, 'Post update');
649
+ return {
650
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
651
+ isError: true,
652
+ };
653
+ }
654
+ return {
655
+ content: [{ type: 'text', text: `Error updating post: ${error.message}` }],
656
+ isError: true,
657
+ };
658
+ }
659
+ }
660
+ );
661
+
662
+ // Delete Post Tool
663
+ server.tool(
664
+ 'ghost_delete_post',
665
+ 'Deletes a post from Ghost CMS by ID. This operation is permanent and cannot be undone.',
666
+ deletePostSchema,
667
+ async (rawInput) => {
668
+ const validation = validateToolInput(deletePostSchema, rawInput, 'ghost_delete_post');
669
+ if (!validation.success) {
670
+ return validation.errorResponse;
671
+ }
672
+ const { id } = validation.data;
673
+
674
+ console.error(`Executing tool: ghost_delete_post for post ID: ${id}`);
675
+ try {
676
+ await loadServices();
677
+
678
+ await ghostService.deletePost(id);
679
+ console.error(`Post deleted successfully. Post ID: ${id}`);
680
+
681
+ return {
682
+ content: [{ type: 'text', text: `Post ${id} has been successfully deleted.` }],
683
+ };
684
+ } catch (error) {
685
+ console.error(`Error in ghost_delete_post:`, error);
686
+ if (error.name === 'ZodError') {
687
+ const validationError = ValidationError.fromZod(error, 'Post deletion');
688
+ return {
689
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
690
+ isError: true,
691
+ };
692
+ }
693
+ return {
694
+ content: [{ type: 'text', text: `Error deleting post: ${error.message}` }],
695
+ isError: true,
696
+ };
697
+ }
698
+ }
699
+ );
700
+
701
+ // =============================================================================
702
+ // PAGE TOOLS
703
+ // Pages are similar to posts but do NOT support tags
704
+ // =============================================================================
705
+
706
+ // --- Page Schema Definitions ---
707
+ const getPageSchema = z
708
+ .object({
709
+ id: ghostIdSchema.optional().describe('The ID of the page to retrieve.'),
710
+ slug: z.string().optional().describe('The slug of the page to retrieve.'),
711
+ include: z
712
+ .string()
713
+ .optional()
714
+ .describe('Comma-separated list of relations to include (e.g., "authors").'),
715
+ })
716
+ .refine((data) => data.id || data.slug, {
717
+ message: 'Either id or slug is required to retrieve a page',
718
+ });
719
+ const updatePageInputSchema = z
720
+ .object({ id: ghostIdSchema.describe('The ID of the page to update.') })
721
+ .merge(updatePageSchema);
722
+ const deletePageSchema = z.object({ id: ghostIdSchema.describe('The ID of the page to delete.') });
723
+ const searchPagesSchema = z.object({
724
+ query: z
725
+ .string()
726
+ .min(1, 'Search query cannot be empty')
727
+ .describe('Search query to find in page titles.'),
728
+ status: z
729
+ .enum(['published', 'draft', 'scheduled', 'all'])
730
+ .optional()
731
+ .describe('Filter by page status. Default searches all statuses.'),
732
+ limit: z
733
+ .number()
734
+ .int()
735
+ .min(1)
736
+ .max(50)
737
+ .default(15)
738
+ .optional()
739
+ .describe('Maximum number of results (1-50). Default is 15.'),
429
740
  });
430
- mcpServer.addTool(createTagTool);
431
- logger.info('Added MCP Tool', { toolName: createTagTool.name });
432
741
 
433
- // --- End Tool Definitions ---
742
+ // Get Pages Tool
743
+ server.tool(
744
+ 'ghost_get_pages',
745
+ 'Retrieves a list of pages from Ghost CMS with pagination, filtering, and sorting options.',
746
+ pageQuerySchema,
747
+ async (rawInput) => {
748
+ const validation = validateToolInput(pageQuerySchema, rawInput, 'ghost_get_pages');
749
+ if (!validation.success) {
750
+ return validation.errorResponse;
751
+ }
752
+ const input = validation.data;
753
+
754
+ console.error(`Executing tool: ghost_get_pages`);
755
+ try {
756
+ await loadServices();
757
+
758
+ const options = {};
759
+ if (input.limit !== undefined) options.limit = input.limit;
760
+ if (input.page !== undefined) options.page = input.page;
761
+ if (input.filter !== undefined) options.filter = input.filter;
762
+ if (input.include !== undefined) options.include = input.include;
763
+ if (input.fields !== undefined) options.fields = input.fields;
764
+ if (input.formats !== undefined) options.formats = input.formats;
765
+ if (input.order !== undefined) options.order = input.order;
434
766
 
435
- // Function to start the MCP server
436
- // We might integrate this with the Express server later or run separately
437
- const startMCPServer = async (port = 3001) => {
438
- try {
439
- // Ensure resources/tools are added before starting
440
- logger.info('Starting MCP Server', { port });
441
- await mcpServer.listen({ port });
442
-
443
- const resources = mcpServer.listResources().map((r) => r.name);
444
- const tools = mcpServer.listTools().map((t) => t.name);
445
-
446
- logger.info('MCP Server started successfully', {
447
- port,
448
- resourceCount: resources.length,
449
- toolCount: tools.length,
450
- resources,
451
- tools,
452
- type: 'server_start',
453
- });
454
- } catch (error) {
455
- logger.error('Failed to start MCP Server', {
456
- port,
457
- error: error.message,
458
- stack: error.stack,
459
- type: 'server_start_error',
460
- });
461
- process.exit(1);
767
+ const pages = await ghostService.getPages(options);
768
+ console.error(`Retrieved ${pages.length} pages from Ghost.`);
769
+
770
+ return {
771
+ content: [{ type: 'text', text: JSON.stringify(pages, null, 2) }],
772
+ };
773
+ } catch (error) {
774
+ console.error(`Error in ghost_get_pages:`, error);
775
+ if (error.name === 'ZodError') {
776
+ const validationError = ValidationError.fromZod(error, 'Page query');
777
+ return {
778
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
779
+ isError: true,
780
+ };
781
+ }
782
+ return {
783
+ content: [{ type: 'text', text: `Error retrieving pages: ${error.message}` }],
784
+ isError: true,
785
+ };
786
+ }
462
787
  }
463
- };
788
+ );
789
+
790
+ // Get Page Tool
791
+ server.tool(
792
+ 'ghost_get_page',
793
+ 'Retrieves a single page from Ghost CMS by ID or slug.',
794
+ getPageSchema,
795
+ async (rawInput) => {
796
+ const validation = validateToolInput(getPageSchema, rawInput, 'ghost_get_page');
797
+ if (!validation.success) {
798
+ return validation.errorResponse;
799
+ }
800
+ const input = validation.data;
801
+
802
+ console.error(`Executing tool: ghost_get_page`);
803
+ try {
804
+ await loadServices();
805
+
806
+ const options = {};
807
+ if (input.include !== undefined) options.include = input.include;
808
+
809
+ const identifier = input.id || `slug/${input.slug}`;
810
+
811
+ const page = await ghostService.getPage(identifier, options);
812
+ console.error(`Retrieved page: ${page.title} (ID: ${page.id})`);
813
+
814
+ return {
815
+ content: [{ type: 'text', text: JSON.stringify(page, null, 2) }],
816
+ };
817
+ } catch (error) {
818
+ console.error(`Error in ghost_get_page:`, error);
819
+ if (error.name === 'ZodError') {
820
+ const validationError = ValidationError.fromZod(error, 'Get page');
821
+ return {
822
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
823
+ isError: true,
824
+ };
825
+ }
826
+ return {
827
+ content: [{ type: 'text', text: `Error retrieving page: ${error.message}` }],
828
+ isError: true,
829
+ };
830
+ }
831
+ }
832
+ );
833
+
834
+ // Create Page Tool
835
+ server.tool(
836
+ 'ghost_create_page',
837
+ 'Creates a new page in Ghost CMS. Note: Pages do NOT typically use tags (unlike posts).',
838
+ createPageSchema,
839
+ async (rawInput) => {
840
+ const validation = validateToolInput(createPageSchema, rawInput, 'ghost_create_page');
841
+ if (!validation.success) {
842
+ return validation.errorResponse;
843
+ }
844
+ const input = validation.data;
845
+
846
+ console.error(`Executing tool: ghost_create_page with title: ${input.title}`);
847
+ try {
848
+ await loadServices();
849
+
850
+ const createdPage = await pageService.createPageService(input);
851
+ console.error(`Page created successfully. Page ID: ${createdPage.id}`);
852
+
853
+ return {
854
+ content: [{ type: 'text', text: JSON.stringify(createdPage, null, 2) }],
855
+ };
856
+ } catch (error) {
857
+ console.error(`Error in ghost_create_page:`, error);
858
+ if (error.name === 'ZodError') {
859
+ const validationError = ValidationError.fromZod(error, 'Page creation');
860
+ return {
861
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
862
+ isError: true,
863
+ };
864
+ }
865
+ return {
866
+ content: [{ type: 'text', text: `Error creating page: ${error.message}` }],
867
+ isError: true,
868
+ };
869
+ }
870
+ }
871
+ );
872
+
873
+ // Update Page Tool
874
+ server.tool(
875
+ 'ghost_update_page',
876
+ 'Updates an existing page in Ghost CMS. Can update title, content, status, images, and SEO fields.',
877
+ updatePageInputSchema,
878
+ async (rawInput) => {
879
+ const validation = validateToolInput(updatePageInputSchema, rawInput, 'ghost_update_page');
880
+ if (!validation.success) {
881
+ return validation.errorResponse;
882
+ }
883
+ const input = validation.data;
884
+
885
+ console.error(`Executing tool: ghost_update_page for page ID: ${input.id}`);
886
+ try {
887
+ await loadServices();
888
+
889
+ const { id, ...updateData } = input;
464
890
 
465
- // Export the server instance and start function if needed elsewhere
466
- export { mcpServer, startMCPServer };
891
+ const updatedPage = await ghostService.updatePage(id, updateData);
892
+ console.error(`Page updated successfully. Page ID: ${updatedPage.id}`);
467
893
 
468
- // Optional: Automatically start if this file is run directly
469
- // This might conflict if we integrate with Express later
470
- // if (require.main === module) {
471
- // startMCPServer();
472
- // }
894
+ return {
895
+ content: [{ type: 'text', text: JSON.stringify(updatedPage, null, 2) }],
896
+ };
897
+ } catch (error) {
898
+ console.error(`Error in ghost_update_page:`, error);
899
+ if (error.name === 'ZodError') {
900
+ const validationError = ValidationError.fromZod(error, 'Page update');
901
+ return {
902
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
903
+ isError: true,
904
+ };
905
+ }
906
+ return {
907
+ content: [{ type: 'text', text: `Error updating page: ${error.message}` }],
908
+ isError: true,
909
+ };
910
+ }
911
+ }
912
+ );
913
+
914
+ // Delete Page Tool
915
+ server.tool(
916
+ 'ghost_delete_page',
917
+ 'Deletes a page from Ghost CMS by ID. This operation is permanent and cannot be undone.',
918
+ deletePageSchema,
919
+ async (rawInput) => {
920
+ const validation = validateToolInput(deletePageSchema, rawInput, 'ghost_delete_page');
921
+ if (!validation.success) {
922
+ return validation.errorResponse;
923
+ }
924
+ const { id } = validation.data;
925
+
926
+ console.error(`Executing tool: ghost_delete_page for page ID: ${id}`);
927
+ try {
928
+ await loadServices();
929
+
930
+ await ghostService.deletePage(id);
931
+ console.error(`Page deleted successfully. Page ID: ${id}`);
932
+
933
+ return {
934
+ content: [{ type: 'text', text: `Page ${id} has been successfully deleted.` }],
935
+ };
936
+ } catch (error) {
937
+ console.error(`Error in ghost_delete_page:`, error);
938
+ if (error.name === 'ZodError') {
939
+ const validationError = ValidationError.fromZod(error, 'Page deletion');
940
+ return {
941
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
942
+ isError: true,
943
+ };
944
+ }
945
+ return {
946
+ content: [{ type: 'text', text: `Error deleting page: ${error.message}` }],
947
+ isError: true,
948
+ };
949
+ }
950
+ }
951
+ );
952
+
953
+ // Search Pages Tool
954
+ server.tool(
955
+ 'ghost_search_pages',
956
+ 'Search for pages in Ghost CMS by query string with optional status filtering.',
957
+ searchPagesSchema,
958
+ async (rawInput) => {
959
+ const validation = validateToolInput(searchPagesSchema, rawInput, 'ghost_search_pages');
960
+ if (!validation.success) {
961
+ return validation.errorResponse;
962
+ }
963
+ const input = validation.data;
964
+
965
+ console.error(`Executing tool: ghost_search_pages with query: ${input.query}`);
966
+ try {
967
+ await loadServices();
968
+
969
+ const options = {};
970
+ if (input.status !== undefined) options.status = input.status;
971
+ if (input.limit !== undefined) options.limit = input.limit;
972
+
973
+ const pages = await ghostService.searchPages(input.query, options);
974
+ console.error(`Found ${pages.length} pages matching "${input.query}".`);
975
+
976
+ return {
977
+ content: [{ type: 'text', text: JSON.stringify(pages, null, 2) }],
978
+ };
979
+ } catch (error) {
980
+ console.error(`Error in ghost_search_pages:`, error);
981
+ if (error.name === 'ZodError') {
982
+ const validationError = ValidationError.fromZod(error, 'Page search');
983
+ return {
984
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
985
+ isError: true,
986
+ };
987
+ }
988
+ return {
989
+ content: [{ type: 'text', text: `Error searching pages: ${error.message}` }],
990
+ isError: true,
991
+ };
992
+ }
993
+ }
994
+ );
995
+
996
+ // =============================================================================
997
+ // MEMBER TOOLS
998
+ // Member management for Ghost CMS subscribers
999
+ // =============================================================================
1000
+
1001
+ // --- Member Schema Definitions ---
1002
+ const updateMemberInputSchema = z.object({ id: ghostIdSchema }).merge(updateMemberSchema);
1003
+ const deleteMemberSchema = z.object({ id: ghostIdSchema });
1004
+ const getMembersSchema = memberQuerySchema.omit({ search: true });
1005
+ const getMemberSchema = z
1006
+ .object({
1007
+ id: ghostIdSchema.optional().describe('The ID of the member to retrieve.'),
1008
+ email: emailSchema.optional().describe('The email of the member to retrieve.'),
1009
+ })
1010
+ .refine((data) => data.id || data.email, {
1011
+ message: 'Either id or email must be provided',
1012
+ });
1013
+ const searchMembersSchema = z.object({
1014
+ query: z.string().min(1).describe('Search query to match against member name or email.'),
1015
+ limit: z
1016
+ .number()
1017
+ .int()
1018
+ .min(1)
1019
+ .max(50)
1020
+ .optional()
1021
+ .describe('Maximum number of results to return (1-50). Default is 15.'),
1022
+ });
1023
+
1024
+ // Create Member Tool
1025
+ server.tool(
1026
+ 'ghost_create_member',
1027
+ 'Creates a new member (subscriber) in Ghost CMS.',
1028
+ createMemberSchema,
1029
+ async (rawInput) => {
1030
+ const validation = validateToolInput(createMemberSchema, rawInput, 'ghost_create_member');
1031
+ if (!validation.success) {
1032
+ return validation.errorResponse;
1033
+ }
1034
+ const input = validation.data;
1035
+
1036
+ console.error(`Executing tool: ghost_create_member with email: ${input.email}`);
1037
+ try {
1038
+ await loadServices();
1039
+
1040
+ const createdMember = await ghostService.createMember(input);
1041
+ console.error(`Member created successfully. Member ID: ${createdMember.id}`);
1042
+
1043
+ return {
1044
+ content: [{ type: 'text', text: JSON.stringify(createdMember, null, 2) }],
1045
+ };
1046
+ } catch (error) {
1047
+ console.error(`Error in ghost_create_member:`, error);
1048
+ if (error.name === 'ZodError') {
1049
+ const validationError = ValidationError.fromZod(error, 'Member creation');
1050
+ return {
1051
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1052
+ isError: true,
1053
+ };
1054
+ }
1055
+ return {
1056
+ content: [{ type: 'text', text: `Error creating member: ${error.message}` }],
1057
+ isError: true,
1058
+ };
1059
+ }
1060
+ }
1061
+ );
1062
+
1063
+ // Update Member Tool
1064
+ server.tool(
1065
+ 'ghost_update_member',
1066
+ 'Updates an existing member in Ghost CMS. All fields except id are optional.',
1067
+ updateMemberInputSchema,
1068
+ async (rawInput) => {
1069
+ const validation = validateToolInput(updateMemberInputSchema, rawInput, 'ghost_update_member');
1070
+ if (!validation.success) {
1071
+ return validation.errorResponse;
1072
+ }
1073
+ const input = validation.data;
1074
+
1075
+ console.error(`Executing tool: ghost_update_member for member ID: ${input.id}`);
1076
+ try {
1077
+ await loadServices();
1078
+
1079
+ const { id, ...updateData } = input;
1080
+
1081
+ const updatedMember = await ghostService.updateMember(id, updateData);
1082
+ console.error(`Member updated successfully. Member ID: ${updatedMember.id}`);
1083
+
1084
+ return {
1085
+ content: [{ type: 'text', text: JSON.stringify(updatedMember, null, 2) }],
1086
+ };
1087
+ } catch (error) {
1088
+ console.error(`Error in ghost_update_member:`, error);
1089
+ if (error.name === 'ZodError') {
1090
+ const validationError = ValidationError.fromZod(error, 'Member update');
1091
+ return {
1092
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1093
+ isError: true,
1094
+ };
1095
+ }
1096
+ return {
1097
+ content: [{ type: 'text', text: `Error updating member: ${error.message}` }],
1098
+ isError: true,
1099
+ };
1100
+ }
1101
+ }
1102
+ );
1103
+
1104
+ // Delete Member Tool
1105
+ server.tool(
1106
+ 'ghost_delete_member',
1107
+ 'Deletes a member from Ghost CMS by ID. This operation is permanent and cannot be undone.',
1108
+ deleteMemberSchema,
1109
+ async (rawInput) => {
1110
+ const validation = validateToolInput(deleteMemberSchema, rawInput, 'ghost_delete_member');
1111
+ if (!validation.success) {
1112
+ return validation.errorResponse;
1113
+ }
1114
+ const { id } = validation.data;
1115
+
1116
+ console.error(`Executing tool: ghost_delete_member for member ID: ${id}`);
1117
+ try {
1118
+ await loadServices();
1119
+
1120
+ await ghostService.deleteMember(id);
1121
+ console.error(`Member deleted successfully. Member ID: ${id}`);
1122
+
1123
+ return {
1124
+ content: [{ type: 'text', text: `Member ${id} has been successfully deleted.` }],
1125
+ };
1126
+ } catch (error) {
1127
+ console.error(`Error in ghost_delete_member:`, error);
1128
+ if (error.name === 'ZodError') {
1129
+ const validationError = ValidationError.fromZod(error, 'Member deletion');
1130
+ return {
1131
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1132
+ isError: true,
1133
+ };
1134
+ }
1135
+ return {
1136
+ content: [{ type: 'text', text: `Error deleting member: ${error.message}` }],
1137
+ isError: true,
1138
+ };
1139
+ }
1140
+ }
1141
+ );
1142
+
1143
+ // Get Members Tool
1144
+ server.tool(
1145
+ 'ghost_get_members',
1146
+ 'Retrieves a list of members (subscribers) from Ghost CMS with optional filtering, pagination, and includes.',
1147
+ getMembersSchema,
1148
+ async (rawInput) => {
1149
+ const validation = validateToolInput(getMembersSchema, rawInput, 'ghost_get_members');
1150
+ if (!validation.success) {
1151
+ return validation.errorResponse;
1152
+ }
1153
+ const input = validation.data;
1154
+
1155
+ console.error(`Executing tool: ghost_get_members`);
1156
+ try {
1157
+ await loadServices();
1158
+
1159
+ const options = {};
1160
+ if (input.limit !== undefined) options.limit = input.limit;
1161
+ if (input.page !== undefined) options.page = input.page;
1162
+ if (input.filter !== undefined) options.filter = input.filter;
1163
+ if (input.order !== undefined) options.order = input.order;
1164
+ if (input.include !== undefined) options.include = input.include;
1165
+
1166
+ const members = await ghostService.getMembers(options);
1167
+ console.error(`Retrieved ${members.length} members from Ghost.`);
1168
+
1169
+ return {
1170
+ content: [{ type: 'text', text: JSON.stringify(members, null, 2) }],
1171
+ };
1172
+ } catch (error) {
1173
+ console.error(`Error in ghost_get_members:`, error);
1174
+ if (error.name === 'ZodError') {
1175
+ const validationError = ValidationError.fromZod(error, 'Member query');
1176
+ return {
1177
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1178
+ isError: true,
1179
+ };
1180
+ }
1181
+ return {
1182
+ content: [{ type: 'text', text: `Error retrieving members: ${error.message}` }],
1183
+ isError: true,
1184
+ };
1185
+ }
1186
+ }
1187
+ );
1188
+
1189
+ // Get Member Tool
1190
+ server.tool(
1191
+ 'ghost_get_member',
1192
+ 'Retrieves a single member from Ghost CMS by ID or email. Provide either id OR email.',
1193
+ getMemberSchema,
1194
+ async (rawInput) => {
1195
+ const validation = validateToolInput(getMemberSchema, rawInput, 'ghost_get_member');
1196
+ if (!validation.success) {
1197
+ return validation.errorResponse;
1198
+ }
1199
+ const { id, email } = validation.data;
1200
+
1201
+ console.error(`Executing tool: ghost_get_member for ${id ? `ID: ${id}` : `email: ${email}`}`);
1202
+ try {
1203
+ await loadServices();
1204
+
1205
+ const member = await ghostService.getMember({ id, email });
1206
+ console.error(`Retrieved member: ${member.email} (ID: ${member.id})`);
1207
+
1208
+ return {
1209
+ content: [{ type: 'text', text: JSON.stringify(member, null, 2) }],
1210
+ };
1211
+ } catch (error) {
1212
+ console.error(`Error in ghost_get_member:`, error);
1213
+ if (error.name === 'ZodError') {
1214
+ const validationError = ValidationError.fromZod(error, 'Member lookup');
1215
+ return {
1216
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1217
+ isError: true,
1218
+ };
1219
+ }
1220
+ return {
1221
+ content: [{ type: 'text', text: `Error retrieving member: ${error.message}` }],
1222
+ isError: true,
1223
+ };
1224
+ }
1225
+ }
1226
+ );
1227
+
1228
+ // Search Members Tool
1229
+ server.tool(
1230
+ 'ghost_search_members',
1231
+ 'Searches for members by name or email in Ghost CMS.',
1232
+ searchMembersSchema,
1233
+ async (rawInput) => {
1234
+ const validation = validateToolInput(searchMembersSchema, rawInput, 'ghost_search_members');
1235
+ if (!validation.success) {
1236
+ return validation.errorResponse;
1237
+ }
1238
+ const { query, limit } = validation.data;
1239
+
1240
+ console.error(`Executing tool: ghost_search_members with query: ${query}`);
1241
+ try {
1242
+ await loadServices();
1243
+
1244
+ const options = {};
1245
+ if (limit !== undefined) options.limit = limit;
1246
+
1247
+ const members = await ghostService.searchMembers(query, options);
1248
+ console.error(`Found ${members.length} members matching "${query}".`);
1249
+
1250
+ return {
1251
+ content: [{ type: 'text', text: JSON.stringify(members, null, 2) }],
1252
+ };
1253
+ } catch (error) {
1254
+ console.error(`Error in ghost_search_members:`, error);
1255
+ if (error.name === 'ZodError') {
1256
+ const validationError = ValidationError.fromZod(error, 'Member search');
1257
+ return {
1258
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1259
+ isError: true,
1260
+ };
1261
+ }
1262
+ return {
1263
+ content: [{ type: 'text', text: `Error searching members: ${error.message}` }],
1264
+ isError: true,
1265
+ };
1266
+ }
1267
+ }
1268
+ );
1269
+
1270
+ // =============================================================================
1271
+ // NEWSLETTER TOOLS
1272
+ // =============================================================================
1273
+
1274
+ // --- Newsletter Schema Definitions ---
1275
+ const getNewsletterSchema = z.object({ id: ghostIdSchema });
1276
+ const updateNewsletterInputSchema = z.object({ id: ghostIdSchema }).merge(updateNewsletterSchema);
1277
+ const deleteNewsletterSchema = z.object({ id: ghostIdSchema });
1278
+
1279
+ // Get Newsletters Tool
1280
+ server.tool(
1281
+ 'ghost_get_newsletters',
1282
+ 'Retrieves a list of newsletters from Ghost CMS with optional filtering.',
1283
+ newsletterQuerySchema,
1284
+ async (rawInput) => {
1285
+ const validation = validateToolInput(newsletterQuerySchema, rawInput, 'ghost_get_newsletters');
1286
+ if (!validation.success) {
1287
+ return validation.errorResponse;
1288
+ }
1289
+ const input = validation.data;
1290
+
1291
+ console.error(`Executing tool: ghost_get_newsletters`);
1292
+ try {
1293
+ await loadServices();
1294
+
1295
+ const options = {};
1296
+ if (input.limit !== undefined) options.limit = input.limit;
1297
+ if (input.page !== undefined) options.page = input.page;
1298
+ if (input.filter !== undefined) options.filter = input.filter;
1299
+ if (input.order !== undefined) options.order = input.order;
1300
+
1301
+ const newsletters = await ghostService.getNewsletters(options);
1302
+ console.error(`Retrieved ${newsletters.length} newsletters from Ghost.`);
1303
+
1304
+ return {
1305
+ content: [{ type: 'text', text: JSON.stringify(newsletters, null, 2) }],
1306
+ };
1307
+ } catch (error) {
1308
+ console.error(`Error in ghost_get_newsletters:`, error);
1309
+ if (error.name === 'ZodError') {
1310
+ const validationError = ValidationError.fromZod(error, 'Newsletter query');
1311
+ return {
1312
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1313
+ isError: true,
1314
+ };
1315
+ }
1316
+ return {
1317
+ content: [{ type: 'text', text: `Error retrieving newsletters: ${error.message}` }],
1318
+ isError: true,
1319
+ };
1320
+ }
1321
+ }
1322
+ );
1323
+
1324
+ // Get Newsletter Tool
1325
+ server.tool(
1326
+ 'ghost_get_newsletter',
1327
+ 'Retrieves a single newsletter from Ghost CMS by ID.',
1328
+ getNewsletterSchema,
1329
+ async (rawInput) => {
1330
+ const validation = validateToolInput(getNewsletterSchema, rawInput, 'ghost_get_newsletter');
1331
+ if (!validation.success) {
1332
+ return validation.errorResponse;
1333
+ }
1334
+ const { id } = validation.data;
1335
+
1336
+ console.error(`Executing tool: ghost_get_newsletter for ID: ${id}`);
1337
+ try {
1338
+ await loadServices();
1339
+
1340
+ const newsletter = await ghostService.getNewsletter(id);
1341
+ console.error(`Retrieved newsletter: ${newsletter.name} (ID: ${newsletter.id})`);
1342
+
1343
+ return {
1344
+ content: [{ type: 'text', text: JSON.stringify(newsletter, null, 2) }],
1345
+ };
1346
+ } catch (error) {
1347
+ console.error(`Error in ghost_get_newsletter:`, error);
1348
+ if (error.name === 'ZodError') {
1349
+ const validationError = ValidationError.fromZod(error, 'Newsletter retrieval');
1350
+ return {
1351
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1352
+ isError: true,
1353
+ };
1354
+ }
1355
+ return {
1356
+ content: [{ type: 'text', text: `Error retrieving newsletter: ${error.message}` }],
1357
+ isError: true,
1358
+ };
1359
+ }
1360
+ }
1361
+ );
1362
+
1363
+ // Create Newsletter Tool
1364
+ server.tool(
1365
+ 'ghost_create_newsletter',
1366
+ 'Creates a new newsletter in Ghost CMS with customizable sender settings and display options.',
1367
+ createNewsletterSchema,
1368
+ async (rawInput) => {
1369
+ const validation = validateToolInput(
1370
+ createNewsletterSchema,
1371
+ rawInput,
1372
+ 'ghost_create_newsletter'
1373
+ );
1374
+ if (!validation.success) {
1375
+ return validation.errorResponse;
1376
+ }
1377
+ const input = validation.data;
1378
+
1379
+ console.error(`Executing tool: ghost_create_newsletter with name: ${input.name}`);
1380
+ try {
1381
+ await loadServices();
1382
+
1383
+ const createdNewsletter = await newsletterService.createNewsletterService(input);
1384
+ console.error(`Newsletter created successfully. Newsletter ID: ${createdNewsletter.id}`);
1385
+
1386
+ return {
1387
+ content: [{ type: 'text', text: JSON.stringify(createdNewsletter, null, 2) }],
1388
+ };
1389
+ } catch (error) {
1390
+ console.error(`Error in ghost_create_newsletter:`, error);
1391
+ if (error.name === 'ZodError') {
1392
+ const validationError = ValidationError.fromZod(error, 'Newsletter creation');
1393
+ return {
1394
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1395
+ isError: true,
1396
+ };
1397
+ }
1398
+ return {
1399
+ content: [{ type: 'text', text: `Error creating newsletter: ${error.message}` }],
1400
+ isError: true,
1401
+ };
1402
+ }
1403
+ }
1404
+ );
1405
+
1406
+ // Update Newsletter Tool
1407
+ server.tool(
1408
+ 'ghost_update_newsletter',
1409
+ 'Updates an existing newsletter in Ghost CMS. Can update name, description, sender settings, and display options.',
1410
+ updateNewsletterInputSchema,
1411
+ async (rawInput) => {
1412
+ const validation = validateToolInput(
1413
+ updateNewsletterInputSchema,
1414
+ rawInput,
1415
+ 'ghost_update_newsletter'
1416
+ );
1417
+ if (!validation.success) {
1418
+ return validation.errorResponse;
1419
+ }
1420
+ const input = validation.data;
1421
+
1422
+ console.error(`Executing tool: ghost_update_newsletter for newsletter ID: ${input.id}`);
1423
+ try {
1424
+ await loadServices();
1425
+
1426
+ const { id, ...updateData } = input;
1427
+
1428
+ const updatedNewsletter = await ghostService.updateNewsletter(id, updateData);
1429
+ console.error(`Newsletter updated successfully. Newsletter ID: ${updatedNewsletter.id}`);
1430
+
1431
+ return {
1432
+ content: [{ type: 'text', text: JSON.stringify(updatedNewsletter, null, 2) }],
1433
+ };
1434
+ } catch (error) {
1435
+ console.error(`Error in ghost_update_newsletter:`, error);
1436
+ if (error.name === 'ZodError') {
1437
+ const validationError = ValidationError.fromZod(error, 'Newsletter update');
1438
+ return {
1439
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1440
+ isError: true,
1441
+ };
1442
+ }
1443
+ return {
1444
+ content: [{ type: 'text', text: `Error updating newsletter: ${error.message}` }],
1445
+ isError: true,
1446
+ };
1447
+ }
1448
+ }
1449
+ );
1450
+
1451
+ // Delete Newsletter Tool
1452
+ server.tool(
1453
+ 'ghost_delete_newsletter',
1454
+ 'Deletes a newsletter from Ghost CMS by ID. This operation is permanent and cannot be undone.',
1455
+ deleteNewsletterSchema,
1456
+ async (rawInput) => {
1457
+ const validation = validateToolInput(
1458
+ deleteNewsletterSchema,
1459
+ rawInput,
1460
+ 'ghost_delete_newsletter'
1461
+ );
1462
+ if (!validation.success) {
1463
+ return validation.errorResponse;
1464
+ }
1465
+ const { id } = validation.data;
1466
+
1467
+ console.error(`Executing tool: ghost_delete_newsletter for newsletter ID: ${id}`);
1468
+ try {
1469
+ await loadServices();
1470
+
1471
+ await ghostService.deleteNewsletter(id);
1472
+ console.error(`Newsletter deleted successfully. Newsletter ID: ${id}`);
1473
+
1474
+ return {
1475
+ content: [{ type: 'text', text: `Newsletter ${id} has been successfully deleted.` }],
1476
+ };
1477
+ } catch (error) {
1478
+ console.error(`Error in ghost_delete_newsletter:`, error);
1479
+ if (error.name === 'ZodError') {
1480
+ const validationError = ValidationError.fromZod(error, 'Newsletter deletion');
1481
+ return {
1482
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1483
+ isError: true,
1484
+ };
1485
+ }
1486
+ return {
1487
+ content: [{ type: 'text', text: `Error deleting newsletter: ${error.message}` }],
1488
+ isError: true,
1489
+ };
1490
+ }
1491
+ }
1492
+ );
1493
+
1494
+ // --- Tier Tools ---
1495
+
1496
+ // --- Tier Schema Definitions ---
1497
+ const getTierSchema = z.object({ id: ghostIdSchema });
1498
+ const updateTierInputSchema = z.object({ id: ghostIdSchema }).merge(updateTierSchema);
1499
+ const deleteTierSchema = z.object({ id: ghostIdSchema });
1500
+
1501
+ // Get Tiers Tool
1502
+ server.tool(
1503
+ 'ghost_get_tiers',
1504
+ 'Retrieves a list of tiers (membership levels) from Ghost CMS with optional filtering by type (free/paid).',
1505
+ tierQuerySchema,
1506
+ async (rawInput) => {
1507
+ const validation = validateToolInput(tierQuerySchema, rawInput, 'ghost_get_tiers');
1508
+ if (!validation.success) {
1509
+ return validation.errorResponse;
1510
+ }
1511
+ const input = validation.data;
1512
+
1513
+ console.error(`Executing tool: ghost_get_tiers`);
1514
+ try {
1515
+ await loadServices();
1516
+
1517
+ const tiers = await ghostService.getTiers(input);
1518
+ console.error(`Retrieved ${tiers.length} tiers`);
1519
+
1520
+ return {
1521
+ content: [{ type: 'text', text: JSON.stringify(tiers, null, 2) }],
1522
+ };
1523
+ } catch (error) {
1524
+ console.error(`Error in ghost_get_tiers:`, error);
1525
+ if (error.name === 'ZodError') {
1526
+ const validationError = ValidationError.fromZod(error, 'Tier query');
1527
+ return {
1528
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1529
+ isError: true,
1530
+ };
1531
+ }
1532
+ return {
1533
+ content: [{ type: 'text', text: `Error getting tiers: ${error.message}` }],
1534
+ isError: true,
1535
+ };
1536
+ }
1537
+ }
1538
+ );
1539
+
1540
+ // Get Tier Tool
1541
+ server.tool(
1542
+ 'ghost_get_tier',
1543
+ 'Retrieves a single tier (membership level) from Ghost CMS by ID.',
1544
+ getTierSchema,
1545
+ async (rawInput) => {
1546
+ const validation = validateToolInput(getTierSchema, rawInput, 'ghost_get_tier');
1547
+ if (!validation.success) {
1548
+ return validation.errorResponse;
1549
+ }
1550
+ const { id } = validation.data;
1551
+
1552
+ console.error(`Executing tool: ghost_get_tier for tier ID: ${id}`);
1553
+ try {
1554
+ await loadServices();
1555
+
1556
+ const tier = await ghostService.getTier(id);
1557
+ console.error(`Tier retrieved successfully. Tier ID: ${tier.id}`);
1558
+
1559
+ return {
1560
+ content: [{ type: 'text', text: JSON.stringify(tier, null, 2) }],
1561
+ };
1562
+ } catch (error) {
1563
+ console.error(`Error in ghost_get_tier:`, error);
1564
+ if (error.name === 'ZodError') {
1565
+ const validationError = ValidationError.fromZod(error, 'Tier retrieval');
1566
+ return {
1567
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1568
+ isError: true,
1569
+ };
1570
+ }
1571
+ return {
1572
+ content: [{ type: 'text', text: `Error getting tier: ${error.message}` }],
1573
+ isError: true,
1574
+ };
1575
+ }
1576
+ }
1577
+ );
1578
+
1579
+ // Create Tier Tool
1580
+ server.tool(
1581
+ 'ghost_create_tier',
1582
+ 'Creates a new tier (membership level) in Ghost CMS with pricing and benefits.',
1583
+ createTierSchema,
1584
+ async (rawInput) => {
1585
+ const validation = validateToolInput(createTierSchema, rawInput, 'ghost_create_tier');
1586
+ if (!validation.success) {
1587
+ return validation.errorResponse;
1588
+ }
1589
+ const input = validation.data;
1590
+
1591
+ console.error(`Executing tool: ghost_create_tier`);
1592
+ try {
1593
+ await loadServices();
1594
+
1595
+ const tier = await ghostService.createTier(input);
1596
+ console.error(`Tier created successfully. Tier ID: ${tier.id}`);
1597
+
1598
+ return {
1599
+ content: [{ type: 'text', text: JSON.stringify(tier, null, 2) }],
1600
+ };
1601
+ } catch (error) {
1602
+ console.error(`Error in ghost_create_tier:`, error);
1603
+ if (error.name === 'ZodError') {
1604
+ const validationError = ValidationError.fromZod(error, 'Tier creation');
1605
+ return {
1606
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1607
+ isError: true,
1608
+ };
1609
+ }
1610
+ return {
1611
+ content: [{ type: 'text', text: `Error creating tier: ${error.message}` }],
1612
+ isError: true,
1613
+ };
1614
+ }
1615
+ }
1616
+ );
1617
+
1618
+ // Update Tier Tool
1619
+ server.tool(
1620
+ 'ghost_update_tier',
1621
+ 'Updates an existing tier (membership level) in Ghost CMS. Can update pricing, benefits, and other tier properties.',
1622
+ updateTierInputSchema,
1623
+ async (rawInput) => {
1624
+ const validation = validateToolInput(updateTierInputSchema, rawInput, 'ghost_update_tier');
1625
+ if (!validation.success) {
1626
+ return validation.errorResponse;
1627
+ }
1628
+ const input = validation.data;
1629
+
1630
+ console.error(`Executing tool: ghost_update_tier for tier ID: ${input.id}`);
1631
+ try {
1632
+ await loadServices();
1633
+
1634
+ const { id, ...updateData } = input;
1635
+
1636
+ const updatedTier = await ghostService.updateTier(id, updateData);
1637
+ console.error(`Tier updated successfully. Tier ID: ${updatedTier.id}`);
1638
+
1639
+ return {
1640
+ content: [{ type: 'text', text: JSON.stringify(updatedTier, null, 2) }],
1641
+ };
1642
+ } catch (error) {
1643
+ console.error(`Error in ghost_update_tier:`, error);
1644
+ if (error.name === 'ZodError') {
1645
+ const validationError = ValidationError.fromZod(error, 'Tier update');
1646
+ return {
1647
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1648
+ isError: true,
1649
+ };
1650
+ }
1651
+ return {
1652
+ content: [{ type: 'text', text: `Error updating tier: ${error.message}` }],
1653
+ isError: true,
1654
+ };
1655
+ }
1656
+ }
1657
+ );
1658
+
1659
+ // Delete Tier Tool
1660
+ server.tool(
1661
+ 'ghost_delete_tier',
1662
+ 'Deletes a tier (membership level) from Ghost CMS by ID. This operation is permanent and cannot be undone.',
1663
+ deleteTierSchema,
1664
+ async (rawInput) => {
1665
+ const validation = validateToolInput(deleteTierSchema, rawInput, 'ghost_delete_tier');
1666
+ if (!validation.success) {
1667
+ return validation.errorResponse;
1668
+ }
1669
+ const { id } = validation.data;
1670
+
1671
+ console.error(`Executing tool: ghost_delete_tier for tier ID: ${id}`);
1672
+ try {
1673
+ await loadServices();
1674
+
1675
+ await ghostService.deleteTier(id);
1676
+ console.error(`Tier deleted successfully. Tier ID: ${id}`);
1677
+
1678
+ return {
1679
+ content: [{ type: 'text', text: `Tier ${id} has been successfully deleted.` }],
1680
+ };
1681
+ } catch (error) {
1682
+ console.error(`Error in ghost_delete_tier:`, error);
1683
+ if (error.name === 'ZodError') {
1684
+ const validationError = ValidationError.fromZod(error, 'Tier deletion');
1685
+ return {
1686
+ content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
1687
+ isError: true,
1688
+ };
1689
+ }
1690
+ return {
1691
+ content: [{ type: 'text', text: `Error deleting tier: ${error.message}` }],
1692
+ isError: true,
1693
+ };
1694
+ }
1695
+ }
1696
+ );
1697
+
1698
+ // --- Main Entry Point ---
1699
+
1700
+ async function main() {
1701
+ console.error('Starting Ghost MCP Server...');
1702
+
1703
+ const transport = new StdioServerTransport();
1704
+ await server.connect(transport);
1705
+
1706
+ console.error('Ghost MCP Server running on stdio transport');
1707
+ console.error(
1708
+ 'Available tools: ghost_get_tags, ghost_create_tag, ghost_get_tag, ghost_update_tag, ghost_delete_tag, ghost_upload_image, ' +
1709
+ 'ghost_create_post, ghost_get_posts, ghost_get_post, ghost_search_posts, ghost_update_post, ghost_delete_post, ' +
1710
+ 'ghost_get_pages, ghost_get_page, ghost_create_page, ghost_update_page, ghost_delete_page, ghost_search_pages, ' +
1711
+ 'ghost_create_member, ghost_update_member, ghost_delete_member, ghost_get_members, ghost_get_member, ghost_search_members, ' +
1712
+ 'ghost_get_newsletters, ghost_get_newsletter, ghost_create_newsletter, ghost_update_newsletter, ghost_delete_newsletter, ' +
1713
+ 'ghost_get_tiers, ghost_get_tier, ghost_create_tier, ghost_update_tier, ghost_delete_tier'
1714
+ );
1715
+ }
1716
+
1717
+ main().catch((error) => {
1718
+ console.error('Fatal error starting MCP server:', error);
1719
+ process.exit(1);
1720
+ });