@jgardner04/ghost-mcp-server 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,657 +1,292 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- MCPServer,
4
- Resource,
5
- Tool,
6
- } from "@modelcontextprotocol/sdk/server/index.js";
7
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
9
- import { WebSocketServerTransport } from "@modelcontextprotocol/sdk/server/websocket.js";
10
- import dotenv from "dotenv";
11
- import { createPostService } from "./services/postService.js";
12
- import {
13
- uploadImage as uploadGhostImage,
14
- getTags as getGhostTags,
15
- createTag as createGhostTag,
16
- } from "./services/ghostService.js";
17
- import { processImage } from "./services/imageProcessingService.js";
18
- import axios from "axios";
19
- import { validateImageUrl, createSecureAxiosConfig } from "./utils/urlValidator.js";
20
- import fs from "fs";
21
- import path from "path";
22
- import os from "os";
23
- import { v4 as uuidv4 } from "uuid";
24
- import express from "express";
25
- import { WebSocketServer } from "ws";
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import dotenv from 'dotenv';
6
+ import axios from 'axios';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import os from 'os';
10
+ import crypto from 'crypto';
26
11
 
27
12
  // Load environment variables
28
13
  dotenv.config();
29
14
 
30
- console.log("Initializing MCP Server...");
15
+ // Lazy-loaded modules (to avoid Node.js v25 Buffer compatibility issues at startup)
16
+ let ghostService = null;
17
+ let postService = null;
18
+ let imageProcessingService = null;
19
+ let urlValidator = null;
31
20
 
32
- // Define the server instance
33
- const mcpServer = new MCPServer({
34
- metadata: {
35
- name: "Ghost CMS Manager",
36
- description:
37
- "MCP Server to manage a Ghost CMS instance using the Admin API.",
38
- version: "1.0.0",
39
- },
40
- });
41
-
42
- // --- Error Response Standardization ---
43
- class MCPError extends Error {
44
- constructor(message, code = "UNKNOWN_ERROR", details = {}) {
45
- super(message);
46
- this.code = code;
47
- this.details = details;
21
+ const loadServices = async () => {
22
+ if (!ghostService) {
23
+ ghostService = await import('./services/ghostService.js');
24
+ postService = await import('./services/postService.js');
25
+ imageProcessingService = await import('./services/imageProcessingService.js');
26
+ urlValidator = await import('./utils/urlValidator.js');
48
27
  }
49
- }
28
+ };
50
29
 
51
- const handleToolError = (error, toolName) => {
52
- console.error(`Error in tool ${toolName}:`, error);
53
-
54
- // Standardized error response
55
- return {
56
- error: {
57
- code: error.code || "TOOL_EXECUTION_ERROR",
58
- message: error.message || "An unexpected error occurred",
59
- tool: toolName,
60
- details: error.details || {},
61
- timestamp: new Date().toISOString(),
62
- }
63
- };
30
+ // Generate UUID without external dependency
31
+ const generateUuid = () => crypto.randomUUID();
32
+
33
+ // Helper function for default alt text
34
+ const getDefaultAltText = (filePath) => {
35
+ try {
36
+ const originalFilename = path.basename(filePath).split('.').slice(0, -1).join('.');
37
+ const nameWithoutIds = originalFilename
38
+ .replace(/^(processed-|mcp-download-|mcp-upload-)\d+-\d+-?/, '')
39
+ .replace(/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}-?/, '');
40
+ return nameWithoutIds.replace(/[-_]/g, ' ').trim() || 'Uploaded image';
41
+ } catch (_e) {
42
+ return 'Uploaded image';
43
+ }
64
44
  };
65
45
 
66
- // --- Define Resources ---
46
+ // Create server instance with new API
47
+ const server = new McpServer({
48
+ name: 'ghost-mcp-server',
49
+ version: '1.0.0',
50
+ });
67
51
 
68
- console.log("Defining MCP Resources...");
52
+ // --- Register Tools ---
69
53
 
70
- // Ghost Tag Resource
71
- const ghostTagResource = new Resource({
72
- name: "ghost/tag",
73
- description: "Represents a tag in Ghost CMS.",
74
- schema: {
75
- type: "object",
76
- properties: {
77
- id: { type: "string", description: "Unique ID of the tag" },
78
- name: { type: "string", description: "The name of the tag" },
79
- slug: { type: "string", description: "URL-friendly version of the name" },
80
- description: {
81
- type: ["string", "null"],
82
- description: "Optional description for the tag",
83
- },
84
- },
85
- required: ["id", "name", "slug"],
54
+ // Get Tags Tool
55
+ server.tool(
56
+ 'ghost_get_tags',
57
+ 'Retrieves a list of tags from Ghost CMS. Can optionally filter by tag name.',
58
+ {
59
+ name: z
60
+ .string()
61
+ .optional()
62
+ .describe('Filter tags by exact name. If omitted, all tags are returned.'),
86
63
  },
87
- // Resource fetching handler
88
- async fetch(uri) {
64
+ async ({ name }) => {
65
+ console.error(`Executing tool: ghost_get_tags`);
89
66
  try {
90
- // Extract tag ID from URI (e.g., "ghost/tag/123")
91
- const tagId = uri.split("/").pop();
92
- const tags = await getGhostTags();
93
- const tag = tags.find(t => t.id === tagId || t.slug === tagId);
94
-
95
- if (!tag) {
96
- throw new MCPError(`Tag not found: ${tagId}`, "RESOURCE_NOT_FOUND");
67
+ await loadServices();
68
+ const tags = await ghostService.getTags();
69
+ let result = tags;
70
+
71
+ if (name) {
72
+ result = tags.filter((tag) => tag.name.toLowerCase() === name.toLowerCase());
73
+ console.error(`Filtered tags by name "${name}". Found ${result.length} match(es).`);
74
+ } else {
75
+ console.error(`Retrieved ${tags.length} tags from Ghost.`);
97
76
  }
98
-
99
- return tag;
100
- } catch (error) {
101
- return handleToolError(error, "ghost_tag_fetch");
102
- }
103
- }
104
- });
105
- mcpServer.addResource(ghostTagResource);
106
- console.log(`Added Resource: ${ghostTagResource.name}`);
107
77
 
108
- // Ghost Post Resource
109
- const ghostPostResource = new Resource({
110
- name: "ghost/post",
111
- description: "Represents a post in Ghost CMS.",
112
- schema: {
113
- type: "object",
114
- properties: {
115
- id: { type: "string", description: "Unique ID of the post" },
116
- uuid: { type: "string", description: "UUID of the post" },
117
- title: { type: "string", description: "The title of the post" },
118
- slug: {
119
- type: "string",
120
- description: "URL-friendly version of the title",
121
- },
122
- html: {
123
- type: ["string", "null"],
124
- description: "The post content as HTML",
125
- },
126
- plaintext: {
127
- type: ["string", "null"],
128
- description: "The post content as plain text",
129
- },
130
- feature_image: {
131
- type: ["string", "null"],
132
- description: "URL of the featured image",
133
- },
134
- feature_image_alt: {
135
- type: ["string", "null"],
136
- description: "Alt text for the featured image",
137
- },
138
- feature_image_caption: {
139
- type: ["string", "null"],
140
- description: "Caption for the featured image",
141
- },
142
- featured: {
143
- type: "boolean",
144
- description: "Whether the post is featured",
145
- },
146
- status: {
147
- type: "string",
148
- enum: ["draft", "published", "scheduled"],
149
- description: "The status of the post",
150
- },
151
- visibility: {
152
- type: "string",
153
- enum: ["public", "members", "paid", "tiers"],
154
- description: "The visibility level of the post",
155
- },
156
- created_at: {
157
- type: "string",
158
- format: "date-time",
159
- description: "Date/time when the post was created",
160
- },
161
- updated_at: {
162
- type: "string",
163
- format: "date-time",
164
- description: "Date/time when the post was last updated",
165
- },
166
- published_at: {
167
- type: ["string", "null"],
168
- format: "date-time",
169
- description: "Date/time when the post was published",
170
- },
171
- custom_excerpt: {
172
- type: ["string", "null"],
173
- description: "Custom excerpt for the post",
174
- },
175
- tags: {
176
- type: "array",
177
- items: { $ref: "ghost/tag#/schema" },
178
- description: "Associated tags",
179
- },
180
- meta_title: {
181
- type: ["string", "null"],
182
- description: "Custom meta title for SEO",
183
- },
184
- meta_description: {
185
- type: ["string", "null"],
186
- description: "Custom meta description for SEO",
187
- },
188
- },
189
- required: ["id", "uuid", "title", "slug", "status"],
190
- },
191
- // Resource fetching handler
192
- async fetch(uri) {
193
- try {
194
- // Extract post ID from URI (e.g., "ghost/post/123")
195
- const postId = uri.split("/").pop();
196
-
197
- // You'll need to implement a getPost service method
198
- // For now, returning an error as this would require adding to ghostService.js
199
- throw new MCPError(
200
- "Post fetching not yet implemented",
201
- "NOT_IMPLEMENTED",
202
- { postId }
203
- );
78
+ return {
79
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
80
+ };
204
81
  } catch (error) {
205
- return handleToolError(error, "ghost_post_fetch");
82
+ console.error(`Error in ghost_get_tags:`, error);
83
+ return {
84
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
85
+ isError: true,
86
+ };
206
87
  }
207
88
  }
208
- });
209
- mcpServer.addResource(ghostPostResource);
210
- console.log(`Added Resource: ${ghostPostResource.name}`);
89
+ );
211
90
 
212
- // --- Define Tools (with improved error handling) ---
213
-
214
- console.log("Defining MCP Tools...");
215
-
216
- // Create Post Tool
217
- const createPostTool = new Tool({
218
- name: "ghost_create_post",
219
- description: "Creates a new post in Ghost CMS.",
220
- inputSchema: {
221
- type: "object",
222
- properties: {
223
- title: {
224
- type: "string",
225
- description: "The title of the post.",
226
- },
227
- html: {
228
- type: "string",
229
- description: "The HTML content of the post.",
230
- },
231
- status: {
232
- type: "string",
233
- enum: ["draft", "published", "scheduled"],
234
- default: "draft",
235
- description:
236
- "The status of the post. Use 'scheduled' with a future published_at date.",
237
- },
238
- tags: {
239
- type: "array",
240
- items: { type: "string" },
241
- description:
242
- "Optional: List of tag names to associate with the post. Tags will be created if they don't exist.",
243
- },
244
- published_at: {
245
- type: "string",
246
- format: "date-time",
247
- description:
248
- "Optional: ISO 8601 date/time to publish the post. Required if status is 'scheduled'.",
249
- },
250
- custom_excerpt: {
251
- type: "string",
252
- description: "Optional: A custom short summary for the post.",
253
- },
254
- feature_image: {
255
- type: "string",
256
- format: "url",
257
- description:
258
- "Optional: URL of the image (e.g., from ghost_upload_image tool) to use as the featured image.",
259
- },
260
- feature_image_alt: {
261
- type: "string",
262
- description: "Optional: Alt text for the featured image.",
263
- },
264
- feature_image_caption: {
265
- type: "string",
266
- description: "Optional: Caption for the featured image.",
267
- },
268
- meta_title: {
269
- type: "string",
270
- description:
271
- "Optional: Custom title for SEO (max 300 chars). Defaults to post title if omitted.",
272
- },
273
- meta_description: {
274
- type: "string",
275
- description:
276
- "Optional: Custom description for SEO (max 500 chars). Defaults to excerpt or generated summary if omitted.",
277
- },
278
- },
279
- required: ["title", "html"],
280
- },
281
- outputSchema: {
282
- $ref: "ghost/post#/schema",
91
+ // Create Tag Tool
92
+ server.tool(
93
+ 'ghost_create_tag',
94
+ 'Creates a new tag in Ghost CMS.',
95
+ {
96
+ name: z.string().describe('The name of the tag.'),
97
+ description: z.string().optional().describe('A description for the tag.'),
98
+ slug: z
99
+ .string()
100
+ .optional()
101
+ .describe(
102
+ 'A URL-friendly slug for the tag. Will be auto-generated from the name if omitted.'
103
+ ),
283
104
  },
284
- implementation: async (input) => {
285
- console.log(
286
- `Executing tool: ${createPostTool.name} with input keys:`,
287
- Object.keys(input)
288
- );
105
+ async ({ name, description, slug }) => {
106
+ console.error(`Executing tool: ghost_create_tag with name: ${name}`);
289
107
  try {
290
- const createdPost = await createPostService(input);
291
- console.log(
292
- `Tool ${createPostTool.name} executed successfully. Post ID: ${createdPost.id}`
293
- );
294
- return createdPost;
108
+ await loadServices();
109
+ const createdTag = await ghostService.createTag({ name, description, slug });
110
+ console.error(`Tag created successfully. Tag ID: ${createdTag.id}`);
111
+
112
+ return {
113
+ content: [{ type: 'text', text: JSON.stringify(createdTag, null, 2) }],
114
+ };
295
115
  } catch (error) {
296
- return handleToolError(error, createPostTool.name);
116
+ console.error(`Error in ghost_create_tag:`, error);
117
+ return {
118
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
119
+ isError: true,
120
+ };
297
121
  }
298
- },
299
- });
300
- mcpServer.addTool(createPostTool);
301
- console.log(`Added Tool: ${createPostTool.name}`);
122
+ }
123
+ );
302
124
 
303
125
  // Upload Image Tool
304
- const uploadImageTool = new Tool({
305
- name: "ghost_upload_image",
306
- description:
307
- "Downloads an image from a URL, processes it, uploads it to Ghost CMS, and returns the final Ghost image URL and alt text.",
308
- inputSchema: {
309
- type: "object",
310
- properties: {
311
- imageUrl: {
312
- type: "string",
313
- format: "url",
314
- description: "The publicly accessible URL of the image to upload.",
315
- },
316
- alt: {
317
- type: "string",
318
- description:
319
- "Optional: Alt text for the image. If omitted, a default will be generated from the filename.",
320
- },
321
- },
322
- required: ["imageUrl"],
323
- },
324
- outputSchema: {
325
- type: "object",
326
- properties: {
327
- url: {
328
- type: "string",
329
- format: "url",
330
- description: "The final URL of the image hosted on Ghost.",
331
- },
332
- alt: {
333
- type: "string",
334
- description: "The alt text determined for the image.",
335
- },
336
- },
337
- required: ["url", "alt"],
126
+ server.tool(
127
+ 'ghost_upload_image',
128
+ 'Downloads an image from a URL, processes it, uploads it to Ghost CMS, and returns the final Ghost image URL and alt text.',
129
+ {
130
+ imageUrl: z.string().describe('The publicly accessible URL of the image to upload.'),
131
+ alt: z
132
+ .string()
133
+ .optional()
134
+ .describe(
135
+ 'Alt text for the image. If omitted, a default will be generated from the filename.'
136
+ ),
338
137
  },
339
- implementation: async (input) => {
340
- console.log(
341
- `Executing tool: ${uploadImageTool.name} for URL:`,
342
- input.imageUrl
343
- );
344
- const { imageUrl, alt } = input;
138
+ async ({ imageUrl, alt }) => {
139
+ console.error(`Executing tool: ghost_upload_image for URL: ${imageUrl}`);
345
140
  let downloadedPath = null;
346
141
  let processedPath = null;
347
142
 
348
143
  try {
349
- // --- 1. Validate URL for SSRF protection ---
350
- const urlValidation = validateImageUrl(imageUrl);
144
+ await loadServices();
145
+
146
+ // 1. Validate URL for SSRF protection
147
+ const urlValidation = urlValidator.validateImageUrl(imageUrl);
351
148
  if (!urlValidation.isValid) {
352
149
  throw new Error(`Invalid image URL: ${urlValidation.error}`);
353
150
  }
354
-
355
- // --- 2. Download the image with security controls ---
356
- const axiosConfig = createSecureAxiosConfig(urlValidation.sanitizedUrl);
151
+
152
+ // 2. Download the image with security controls
153
+ const axiosConfig = urlValidator.createSecureAxiosConfig(urlValidation.sanitizedUrl);
357
154
  const response = await axios(axiosConfig);
358
155
  const tempDir = os.tmpdir();
359
- const extension = path.extname(imageUrl.split("?")[0]) || ".tmp";
156
+ const extension = path.extname(imageUrl.split('?')[0]) || '.tmp';
360
157
  const originalFilenameHint =
361
- path.basename(imageUrl.split("?")[0]) ||
362
- `image-${uuidv4()}${extension}`;
363
- downloadedPath = path.join(
364
- tempDir,
365
- `mcp-download-${uuidv4()}${extension}`
366
- );
158
+ path.basename(imageUrl.split('?')[0]) || `image-${generateUuid()}${extension}`;
159
+ downloadedPath = path.join(tempDir, `mcp-download-${generateUuid()}${extension}`);
367
160
 
368
161
  const writer = fs.createWriteStream(downloadedPath);
369
162
  response.data.pipe(writer);
370
163
 
371
164
  await new Promise((resolve, reject) => {
372
- writer.on("finish", resolve);
373
- writer.on("error", reject);
165
+ writer.on('finish', resolve);
166
+ writer.on('error', reject);
374
167
  });
375
- console.log(`Downloaded image to temporary path: ${downloadedPath}`);
168
+ console.error(`Downloaded image to temporary path: ${downloadedPath}`);
376
169
 
377
- // --- 2. Process the image ---
378
- processedPath = await processImage(downloadedPath, tempDir);
379
- console.log(`Processed image path: ${processedPath}`);
170
+ // 3. Process the image
171
+ processedPath = await imageProcessingService.processImage(downloadedPath, tempDir);
172
+ console.error(`Processed image path: ${processedPath}`);
380
173
 
381
- // --- 3. Determine Alt Text ---
174
+ // 4. Determine Alt Text
382
175
  const defaultAlt = getDefaultAltText(originalFilenameHint);
383
176
  const finalAltText = alt || defaultAlt;
384
- console.log(`Using alt text: "${finalAltText}"`);
177
+ console.error(`Using alt text: "${finalAltText}"`);
385
178
 
386
- // --- 4. Upload processed image to Ghost ---
387
- const uploadResult = await uploadGhostImage(processedPath);
388
- console.log(`Uploaded processed image to Ghost: ${uploadResult.url}`);
179
+ // 5. Upload processed image to Ghost
180
+ const uploadResult = await ghostService.uploadImage(processedPath);
181
+ console.error(`Uploaded processed image to Ghost: ${uploadResult.url}`);
389
182
 
390
- // --- 5. Return result ---
391
- return {
183
+ // 6. Return result
184
+ const result = {
392
185
  url: uploadResult.url,
393
186
  alt: finalAltText,
394
187
  };
188
+
189
+ return {
190
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
191
+ };
395
192
  } catch (error) {
396
- return handleToolError(
397
- new MCPError(
398
- `Failed to upload image from URL ${imageUrl}`,
399
- "IMAGE_UPLOAD_ERROR",
400
- { imageUrl, originalError: error.message }
401
- ),
402
- uploadImageTool.name
403
- );
193
+ console.error(`Error in ghost_upload_image:`, error);
194
+ return {
195
+ content: [{ type: 'text', text: `Error uploading image: ${error.message}` }],
196
+ isError: true,
197
+ };
404
198
  } finally {
405
- // --- 6. Cleanup temporary files ---
199
+ // Cleanup temporary files
406
200
  if (downloadedPath) {
407
201
  fs.unlink(downloadedPath, (err) => {
408
- if (err)
409
- console.error(
410
- "Error deleting temporary downloaded file:",
411
- downloadedPath,
412
- err
413
- );
202
+ if (err) console.error('Error deleting temporary downloaded file:', downloadedPath, err);
414
203
  });
415
204
  }
416
205
  if (processedPath && processedPath !== downloadedPath) {
417
206
  fs.unlink(processedPath, (err) => {
418
- if (err)
419
- console.error(
420
- "Error deleting temporary processed file:",
421
- processedPath,
422
- err
423
- );
207
+ if (err) console.error('Error deleting temporary processed file:', processedPath, err);
424
208
  });
425
209
  }
426
210
  }
427
- },
428
- });
429
-
430
- // Helper function for default alt text
431
- const getDefaultAltText = (filePath) => {
432
- try {
433
- const originalFilename = path
434
- .basename(filePath)
435
- .split(".")
436
- .slice(0, -1)
437
- .join(".");
438
- const nameWithoutIds = originalFilename
439
- .replace(/^(processed-|mcp-download-|mcp-upload-)\d+-\d+-?/, "")
440
- .replace(/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}-?/, "");
441
- return nameWithoutIds.replace(/[-_]/g, " ").trim() || "Uploaded image";
442
- } catch (e) {
443
- return "Uploaded image";
444
211
  }
445
- };
446
-
447
- mcpServer.addTool(uploadImageTool);
448
- console.log(`Added Tool: ${uploadImageTool.name}`);
212
+ );
449
213
 
450
- // Get Tags Tool
451
- const getTagsTool = new Tool({
452
- name: "ghost_get_tags",
453
- description:
454
- "Retrieves a list of tags from Ghost CMS. Can optionally filter by tag name.",
455
- inputSchema: {
456
- type: "object",
457
- properties: {
458
- name: {
459
- type: "string",
460
- description:
461
- "Optional: Filter tags by exact name. If omitted, all tags are returned.",
462
- },
463
- },
464
- },
465
- outputSchema: {
466
- type: "array",
467
- items: { $ref: "ghost/tag#/schema" },
214
+ // Create Post Tool
215
+ server.tool(
216
+ 'ghost_create_post',
217
+ 'Creates a new post in Ghost CMS.',
218
+ {
219
+ title: z.string().describe('The title of the post.'),
220
+ html: z.string().describe('The HTML content of the post.'),
221
+ status: z
222
+ .enum(['draft', 'published', 'scheduled'])
223
+ .optional()
224
+ .describe("The status of the post. Defaults to 'draft'."),
225
+ tags: z
226
+ .array(z.string())
227
+ .optional()
228
+ .describe(
229
+ "List of tag names to associate with the post. Tags will be created if they don't exist."
230
+ ),
231
+ published_at: z
232
+ .string()
233
+ .optional()
234
+ .describe("ISO 8601 date/time to publish the post. Required if status is 'scheduled'."),
235
+ custom_excerpt: z.string().optional().describe('A custom short summary for the post.'),
236
+ feature_image: z
237
+ .string()
238
+ .optional()
239
+ .describe(
240
+ 'URL of the image (e.g., from ghost_upload_image tool) to use as the featured image.'
241
+ ),
242
+ feature_image_alt: z.string().optional().describe('Alt text for the featured image.'),
243
+ feature_image_caption: z.string().optional().describe('Caption for the featured image.'),
244
+ meta_title: z
245
+ .string()
246
+ .optional()
247
+ .describe('Custom title for SEO (max 300 chars). Defaults to post title if omitted.'),
248
+ meta_description: z
249
+ .string()
250
+ .optional()
251
+ .describe(
252
+ 'Custom description for SEO (max 500 chars). Defaults to excerpt or generated summary if omitted.'
253
+ ),
468
254
  },
469
- implementation: async (input) => {
470
- console.log(`Executing tool: ${getTagsTool.name}`);
255
+ async (input) => {
256
+ console.error(`Executing tool: ghost_create_post with title: ${input.title}`);
471
257
  try {
472
- const tags = await getGhostTags();
473
- if (input.name) {
474
- const filteredTags = tags.filter(
475
- (tag) => tag.name.toLowerCase() === input.name.toLowerCase()
476
- );
477
- console.log(
478
- `Filtered tags by name "${input.name}". Found ${filteredTags.length} match(es).`
479
- );
480
- return filteredTags;
481
- }
482
- console.log(`Retrieved ${tags.length} tags from Ghost.`);
483
- return tags;
484
- } catch (error) {
485
- return handleToolError(error, getTagsTool.name);
486
- }
487
- },
488
- });
489
- mcpServer.addTool(getTagsTool);
490
- console.log(`Added Tool: ${getTagsTool.name}`);
258
+ await loadServices();
259
+ const createdPost = await postService.createPostService(input);
260
+ console.error(`Post created successfully. Post ID: ${createdPost.id}`);
491
261
 
492
- // Create Tag Tool
493
- const createTagTool = new Tool({
494
- name: "ghost_create_tag",
495
- description: "Creates a new tag in Ghost CMS.",
496
- inputSchema: {
497
- type: "object",
498
- properties: {
499
- name: {
500
- type: "string",
501
- description: "The name of the tag.",
502
- },
503
- description: {
504
- type: "string",
505
- description: "Optional: A description for the tag.",
506
- },
507
- slug: {
508
- type: "string",
509
- pattern: "^[a-z0-9\\-]+$",
510
- description:
511
- "Optional: A URL-friendly slug for the tag. Will be auto-generated from the name if omitted.",
512
- },
513
- },
514
- required: ["name"],
515
- },
516
- outputSchema: {
517
- $ref: "ghost/tag#/schema",
518
- },
519
- implementation: async (input) => {
520
- console.log(
521
- `Executing tool: ${createTagTool.name} with name:`,
522
- input.name
523
- );
524
- try {
525
- const createdTag = await createGhostTag(input);
526
- console.log(
527
- `Tool ${createTagTool.name} executed successfully. Tag ID: ${createdTag.id}`
528
- );
529
- return createdTag;
262
+ return {
263
+ content: [{ type: 'text', text: JSON.stringify(createdPost, null, 2) }],
264
+ };
530
265
  } catch (error) {
531
- return handleToolError(error, createTagTool.name);
532
- }
533
- },
534
- });
535
- mcpServer.addTool(createTagTool);
536
- console.log(`Added Tool: ${createTagTool.name}`);
537
-
538
- // --- Transport Configuration ---
539
-
540
- /**
541
- * Start MCP Server with specified transport
542
- * @param {string} transport - Transport type: 'stdio', 'http', 'websocket'
543
- * @param {object} options - Transport-specific options
544
- */
545
- const startMCPServer = async (transport = 'http', options = {}) => {
546
- try {
547
- console.log(`Starting MCP Server with ${transport} transport...`);
548
-
549
- switch (transport) {
550
- case 'stdio':
551
- // Standard I/O transport - best for CLI tools
552
- const stdioTransport = new StdioServerTransport();
553
- await mcpServer.connect(stdioTransport);
554
- console.log("MCP Server running on stdio transport");
555
- break;
556
-
557
- case 'http':
558
- case 'sse':
559
- // HTTP with Server-Sent Events - good for web clients
560
- const port = options.port || 3001;
561
- const app = express();
562
-
563
- // CORS configuration for web clients
564
- app.use((req, res, next) => {
565
- res.header('Access-Control-Allow-Origin', options.cors || '*');
566
- res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
567
- res.header('Access-Control-Allow-Headers', 'Content-Type');
568
- next();
569
- });
570
-
571
- // SSE endpoint
572
- const sseTransport = new SSEServerTransport();
573
- app.get('/mcp/sse', sseTransport.handler());
574
-
575
- // Health check
576
- app.get('/mcp/health', (req, res) => {
577
- res.json({
578
- status: 'ok',
579
- transport: 'sse',
580
- resources: mcpServer.listResources().map(r => r.name),
581
- tools: mcpServer.listTools().map(t => t.name),
582
- });
583
- });
584
-
585
- await mcpServer.connect(sseTransport);
586
-
587
- const server = app.listen(port, () => {
588
- console.log(`MCP Server (SSE) listening on port ${port}`);
589
- console.log(`SSE endpoint: http://localhost:${port}/mcp/sse`);
590
- console.log(`Health check: http://localhost:${port}/mcp/health`);
591
- });
592
-
593
- // Store server instance for cleanup
594
- mcpServer._httpServer = server;
595
- break;
596
-
597
- case 'websocket':
598
- // WebSocket transport - best for real-time bidirectional communication
599
- const wsPort = options.port || 3001;
600
- const wss = new WebSocketServer({ port: wsPort });
601
-
602
- wss.on('connection', async (ws) => {
603
- console.log('New WebSocket connection');
604
- const wsTransport = new WebSocketServerTransport(ws);
605
- await mcpServer.connect(wsTransport);
606
- });
607
-
608
- console.log(`MCP Server (WebSocket) listening on port ${wsPort}`);
609
- console.log(`WebSocket URL: ws://localhost:${wsPort}`);
610
-
611
- // Store WebSocket server instance for cleanup
612
- mcpServer._wss = wss;
613
- break;
614
-
615
- default:
616
- throw new Error(`Unknown transport type: ${transport}`);
266
+ console.error(`Error in ghost_create_post:`, error);
267
+ return {
268
+ content: [{ type: 'text', text: `Error creating post: ${error.message}` }],
269
+ isError: true,
270
+ };
617
271
  }
618
-
619
- console.log("Available Resources:", mcpServer.listResources().map(r => r.name));
620
- console.log("Available Tools:", mcpServer.listTools().map(t => t.name));
621
-
622
- } catch (error) {
623
- console.error("Failed to start MCP Server:", error);
624
- process.exit(1);
625
272
  }
626
- };
273
+ );
627
274
 
628
- // Graceful shutdown handler
629
- const shutdown = async () => {
630
- console.log("\nShutting down MCP Server...");
631
-
632
- if (mcpServer._httpServer) {
633
- mcpServer._httpServer.close();
634
- }
635
-
636
- if (mcpServer._wss) {
637
- mcpServer._wss.close();
638
- }
639
-
640
- await mcpServer.close();
641
- process.exit(0);
642
- };
275
+ // --- Main Entry Point ---
643
276
 
644
- process.on('SIGINT', shutdown);
645
- process.on('SIGTERM', shutdown);
277
+ async function main() {
278
+ console.error('Starting Ghost MCP Server...');
646
279
 
647
- // Export the server instance and start function
648
- export { mcpServer, startMCPServer, MCPError };
280
+ const transport = new StdioServerTransport();
281
+ await server.connect(transport);
649
282
 
650
- // If running directly, start with transport from environment or default to HTTP
651
- if (import.meta.url === `file://${process.argv[1]}`) {
652
- const transport = process.env.MCP_TRANSPORT || 'http';
653
- const port = parseInt(process.env.MCP_PORT || '3001');
654
- const cors = process.env.MCP_CORS || '*';
655
-
656
- startMCPServer(transport, { port, cors });
657
- }
283
+ console.error('Ghost MCP Server running on stdio transport');
284
+ console.error(
285
+ 'Available tools: ghost_get_tags, ghost_create_tag, ghost_upload_image, ghost_create_post'
286
+ );
287
+ }
288
+
289
+ main().catch((error) => {
290
+ console.error('Fatal error starting MCP server:', error);
291
+ process.exit(1);
292
+ });