@jgardner04/ghost-mcp-server 1.1.0 → 1.1.2

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,13 +1,13 @@
1
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";
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";
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';
11
11
 
12
12
  // Load environment variables
13
13
  dotenv.config();
@@ -20,10 +20,10 @@ let urlValidator = null;
20
20
 
21
21
  const loadServices = async () => {
22
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");
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');
27
27
  }
28
28
  };
29
29
 
@@ -33,34 +33,33 @@ const generateUuid = () => crypto.randomUUID();
33
33
  // Helper function for default alt text
34
34
  const getDefaultAltText = (filePath) => {
35
35
  try {
36
- const originalFilename = path
37
- .basename(filePath)
38
- .split(".")
39
- .slice(0, -1)
40
- .join(".");
36
+ const originalFilename = path.basename(filePath).split('.').slice(0, -1).join('.');
41
37
  const nameWithoutIds = originalFilename
42
- .replace(/^(processed-|mcp-download-|mcp-upload-)\d+-\d+-?/, "")
43
- .replace(/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}-?/, "");
44
- return nameWithoutIds.replace(/[-_]/g, " ").trim() || "Uploaded image";
45
- } catch (e) {
46
- return "Uploaded image";
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';
47
43
  }
48
44
  };
49
45
 
50
46
  // Create server instance with new API
51
47
  const server = new McpServer({
52
- name: "ghost-mcp-server",
53
- version: "1.0.0",
48
+ name: 'ghost-mcp-server',
49
+ version: '1.0.0',
54
50
  });
55
51
 
56
52
  // --- Register Tools ---
57
53
 
58
54
  // Get Tags Tool
59
55
  server.tool(
60
- "ghost_get_tags",
61
- "Retrieves a list of tags from Ghost CMS. Can optionally filter by tag name.",
56
+ 'ghost_get_tags',
57
+ 'Retrieves a list of tags from Ghost CMS. Can optionally filter by tag name.',
62
58
  {
63
- name: z.string().optional().describe("Filter tags by exact name. If omitted, all tags are returned."),
59
+ name: z
60
+ .string()
61
+ .optional()
62
+ .describe('Filter tags by exact name. If omitted, all tags are returned.'),
64
63
  },
65
64
  async ({ name }) => {
66
65
  console.error(`Executing tool: ghost_get_tags`);
@@ -70,21 +69,19 @@ server.tool(
70
69
  let result = tags;
71
70
 
72
71
  if (name) {
73
- result = tags.filter(
74
- (tag) => tag.name.toLowerCase() === name.toLowerCase()
75
- );
72
+ result = tags.filter((tag) => tag.name.toLowerCase() === name.toLowerCase());
76
73
  console.error(`Filtered tags by name "${name}". Found ${result.length} match(es).`);
77
74
  } else {
78
75
  console.error(`Retrieved ${tags.length} tags from Ghost.`);
79
76
  }
80
77
 
81
78
  return {
82
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
79
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
83
80
  };
84
81
  } catch (error) {
85
82
  console.error(`Error in ghost_get_tags:`, error);
86
83
  return {
87
- content: [{ type: "text", text: `Error: ${error.message}` }],
84
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
88
85
  isError: true,
89
86
  };
90
87
  }
@@ -93,12 +90,17 @@ server.tool(
93
90
 
94
91
  // Create Tag Tool
95
92
  server.tool(
96
- "ghost_create_tag",
97
- "Creates a new tag in Ghost CMS.",
93
+ 'ghost_create_tag',
94
+ 'Creates a new tag in Ghost CMS.',
98
95
  {
99
- name: z.string().describe("The name of the tag."),
100
- description: z.string().optional().describe("A description for the tag."),
101
- slug: z.string().optional().describe("A URL-friendly slug for the tag. Will be auto-generated from the name if omitted."),
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
+ ),
102
104
  },
103
105
  async ({ name, description, slug }) => {
104
106
  console.error(`Executing tool: ghost_create_tag with name: ${name}`);
@@ -108,12 +110,12 @@ server.tool(
108
110
  console.error(`Tag created successfully. Tag ID: ${createdTag.id}`);
109
111
 
110
112
  return {
111
- content: [{ type: "text", text: JSON.stringify(createdTag, null, 2) }],
113
+ content: [{ type: 'text', text: JSON.stringify(createdTag, null, 2) }],
112
114
  };
113
115
  } catch (error) {
114
116
  console.error(`Error in ghost_create_tag:`, error);
115
117
  return {
116
- content: [{ type: "text", text: `Error: ${error.message}` }],
118
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
117
119
  isError: true,
118
120
  };
119
121
  }
@@ -122,11 +124,16 @@ server.tool(
122
124
 
123
125
  // Upload Image Tool
124
126
  server.tool(
125
- "ghost_upload_image",
126
- "Downloads an image from a URL, processes it, uploads it to Ghost CMS, and returns the final Ghost image URL and alt text.",
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.',
127
129
  {
128
- imageUrl: z.string().describe("The publicly accessible URL of the image to upload."),
129
- alt: z.string().optional().describe("Alt text for the image. If omitted, a default will be generated from the filename."),
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
+ ),
130
137
  },
131
138
  async ({ imageUrl, alt }) => {
132
139
  console.error(`Executing tool: ghost_upload_image for URL: ${imageUrl}`);
@@ -146,17 +153,17 @@ server.tool(
146
153
  const axiosConfig = urlValidator.createSecureAxiosConfig(urlValidation.sanitizedUrl);
147
154
  const response = await axios(axiosConfig);
148
155
  const tempDir = os.tmpdir();
149
- const extension = path.extname(imageUrl.split("?")[0]) || ".tmp";
156
+ const extension = path.extname(imageUrl.split('?')[0]) || '.tmp';
150
157
  const originalFilenameHint =
151
- path.basename(imageUrl.split("?")[0]) || `image-${generateUuid()}${extension}`;
158
+ path.basename(imageUrl.split('?')[0]) || `image-${generateUuid()}${extension}`;
152
159
  downloadedPath = path.join(tempDir, `mcp-download-${generateUuid()}${extension}`);
153
160
 
154
161
  const writer = fs.createWriteStream(downloadedPath);
155
162
  response.data.pipe(writer);
156
163
 
157
164
  await new Promise((resolve, reject) => {
158
- writer.on("finish", resolve);
159
- writer.on("error", reject);
165
+ writer.on('finish', resolve);
166
+ writer.on('error', reject);
160
167
  });
161
168
  console.error(`Downloaded image to temporary path: ${downloadedPath}`);
162
169
 
@@ -180,24 +187,24 @@ server.tool(
180
187
  };
181
188
 
182
189
  return {
183
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
190
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
184
191
  };
185
192
  } catch (error) {
186
193
  console.error(`Error in ghost_upload_image:`, error);
187
194
  return {
188
- content: [{ type: "text", text: `Error uploading image: ${error.message}` }],
195
+ content: [{ type: 'text', text: `Error uploading image: ${error.message}` }],
189
196
  isError: true,
190
197
  };
191
198
  } finally {
192
199
  // Cleanup temporary files
193
200
  if (downloadedPath) {
194
201
  fs.unlink(downloadedPath, (err) => {
195
- if (err) console.error("Error deleting temporary downloaded file:", downloadedPath, err);
202
+ if (err) console.error('Error deleting temporary downloaded file:', downloadedPath, err);
196
203
  });
197
204
  }
198
205
  if (processedPath && processedPath !== downloadedPath) {
199
206
  fs.unlink(processedPath, (err) => {
200
- if (err) console.error("Error deleting temporary processed file:", processedPath, err);
207
+ if (err) console.error('Error deleting temporary processed file:', processedPath, err);
201
208
  });
202
209
  }
203
210
  }
@@ -206,20 +213,44 @@ server.tool(
206
213
 
207
214
  // Create Post Tool
208
215
  server.tool(
209
- "ghost_create_post",
210
- "Creates a new post in Ghost CMS.",
216
+ 'ghost_create_post',
217
+ 'Creates a new post in Ghost CMS.',
211
218
  {
212
- title: z.string().describe("The title of the post."),
213
- html: z.string().describe("The HTML content of the post."),
214
- status: z.enum(["draft", "published", "scheduled"]).optional().describe("The status of the post. Defaults to 'draft'."),
215
- tags: z.array(z.string()).optional().describe("List of tag names to associate with the post. Tags will be created if they don't exist."),
216
- published_at: z.string().optional().describe("ISO 8601 date/time to publish the post. Required if status is 'scheduled'."),
217
- custom_excerpt: z.string().optional().describe("A custom short summary for the post."),
218
- feature_image: z.string().optional().describe("URL of the image (e.g., from ghost_upload_image tool) to use as the featured image."),
219
- feature_image_alt: z.string().optional().describe("Alt text for the featured image."),
220
- feature_image_caption: z.string().optional().describe("Caption for the featured image."),
221
- meta_title: z.string().optional().describe("Custom title for SEO (max 300 chars). Defaults to post title if omitted."),
222
- meta_description: z.string().optional().describe("Custom description for SEO (max 500 chars). Defaults to excerpt or generated summary if omitted."),
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
+ ),
223
254
  },
224
255
  async (input) => {
225
256
  console.error(`Executing tool: ghost_create_post with title: ${input.title}`);
@@ -229,12 +260,12 @@ server.tool(
229
260
  console.error(`Post created successfully. Post ID: ${createdPost.id}`);
230
261
 
231
262
  return {
232
- content: [{ type: "text", text: JSON.stringify(createdPost, null, 2) }],
263
+ content: [{ type: 'text', text: JSON.stringify(createdPost, null, 2) }],
233
264
  };
234
265
  } catch (error) {
235
266
  console.error(`Error in ghost_create_post:`, error);
236
267
  return {
237
- content: [{ type: "text", text: `Error creating post: ${error.message}` }],
268
+ content: [{ type: 'text', text: `Error creating post: ${error.message}` }],
238
269
  isError: true,
239
270
  };
240
271
  }
@@ -244,16 +275,18 @@ server.tool(
244
275
  // --- Main Entry Point ---
245
276
 
246
277
  async function main() {
247
- console.error("Starting Ghost MCP Server...");
278
+ console.error('Starting Ghost MCP Server...');
248
279
 
249
280
  const transport = new StdioServerTransport();
250
281
  await server.connect(transport);
251
282
 
252
- console.error("Ghost MCP Server running on stdio transport");
253
- console.error("Available tools: ghost_get_tags, ghost_create_tag, ghost_upload_image, ghost_create_post");
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
+ );
254
287
  }
255
288
 
256
289
  main().catch((error) => {
257
- console.error("Fatal error starting MCP server:", error);
290
+ console.error('Fatal error starting MCP server:', error);
258
291
  process.exit(1);
259
292
  });