@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.
- package/README.md +0 -3
- package/package.json +20 -2
- package/src/config/mcp-config.js +31 -22
- package/src/controllers/imageController.js +62 -62
- package/src/controllers/postController.js +8 -8
- package/src/controllers/tagController.js +17 -20
- package/src/errors/index.js +49 -44
- package/src/index.js +56 -50
- package/src/mcp_server.js +151 -178
- package/src/mcp_server_enhanced.js +265 -259
- package/src/mcp_server_improved.js +104 -71
- package/src/middleware/errorMiddleware.js +69 -70
- package/src/resources/ResourceManager.js +143 -134
- package/src/routes/imageRoutes.js +9 -9
- package/src/routes/postRoutes.js +22 -28
- package/src/routes/tagRoutes.js +12 -14
- package/src/services/__tests__/ghostService.test.js +20 -18
- package/src/services/ghostService.js +34 -46
- package/src/services/ghostServiceImproved.js +125 -109
- package/src/services/imageProcessingService.js +15 -15
- package/src/services/postService.js +22 -22
- package/src/utils/logger.js +50 -50
- package/src/utils/urlValidator.js +37 -38
package/src/mcp_server.js
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Tool,
|
|
5
|
-
} from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
-
import dotenv from "dotenv";
|
|
7
|
-
import { createPostService } from "./services/postService.js";
|
|
1
|
+
import { MCPServer, Resource, Tool } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import { createPostService } from './services/postService.js';
|
|
8
4
|
import {
|
|
9
5
|
uploadImage as uploadGhostImage,
|
|
10
6
|
getTags as getGhostTags,
|
|
11
7
|
createTag as createGhostTag,
|
|
12
|
-
} from
|
|
13
|
-
import { processImage } from
|
|
14
|
-
import axios from
|
|
15
|
-
import fs from
|
|
16
|
-
import path from
|
|
17
|
-
import os from
|
|
18
|
-
import { v4 as uuidv4 } from
|
|
19
|
-
import { validateImageUrl, createSecureAxiosConfig } from
|
|
20
|
-
import { createContextLogger } from
|
|
8
|
+
} from './services/ghostService.js';
|
|
9
|
+
import { processImage } from './services/imageProcessingService.js';
|
|
10
|
+
import axios from 'axios';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
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';
|
|
21
17
|
|
|
22
18
|
// Load environment variables (might be redundant if loaded elsewhere, but safe)
|
|
23
19
|
dotenv.config();
|
|
@@ -30,9 +26,8 @@ logger.info('Initializing MCP Server');
|
|
|
30
26
|
// Define the server instance
|
|
31
27
|
const mcpServer = new MCPServer({
|
|
32
28
|
metadata: {
|
|
33
|
-
name:
|
|
34
|
-
description:
|
|
35
|
-
"MCP Server to manage a Ghost CMS instance using the Admin API.",
|
|
29
|
+
name: 'Ghost CMS Manager',
|
|
30
|
+
description: 'MCP Server to manage a Ghost CMS instance using the Admin API.',
|
|
36
31
|
// iconUrl: '...',
|
|
37
32
|
},
|
|
38
33
|
});
|
|
@@ -43,21 +38,21 @@ logger.info('Defining MCP Resources');
|
|
|
43
38
|
|
|
44
39
|
// Ghost Tag Resource
|
|
45
40
|
const ghostTagResource = new Resource({
|
|
46
|
-
name:
|
|
47
|
-
description:
|
|
41
|
+
name: 'ghost/tag',
|
|
42
|
+
description: 'Represents a tag in Ghost CMS.',
|
|
48
43
|
schema: {
|
|
49
|
-
type:
|
|
44
|
+
type: 'object',
|
|
50
45
|
properties: {
|
|
51
|
-
id: { type:
|
|
52
|
-
name: { type:
|
|
53
|
-
slug: { type:
|
|
46
|
+
id: { type: 'string', description: 'Unique ID of the tag' },
|
|
47
|
+
name: { type: 'string', description: 'The name of the tag' },
|
|
48
|
+
slug: { type: 'string', description: 'URL-friendly version of the name' },
|
|
54
49
|
description: {
|
|
55
|
-
type: [
|
|
56
|
-
description:
|
|
50
|
+
type: ['string', 'null'],
|
|
51
|
+
description: 'Optional description for the tag',
|
|
57
52
|
},
|
|
58
53
|
// Add other relevant tag fields if needed (e.g., feature_image, visibility)
|
|
59
54
|
},
|
|
60
|
-
required: [
|
|
55
|
+
required: ['id', 'name', 'slug'],
|
|
61
56
|
},
|
|
62
57
|
});
|
|
63
58
|
mcpServer.addResource(ghostTagResource);
|
|
@@ -65,96 +60,87 @@ logger.info('Added MCP Resource', { resourceName: ghostTagResource.name });
|
|
|
65
60
|
|
|
66
61
|
// Ghost Post Resource
|
|
67
62
|
const ghostPostResource = new Resource({
|
|
68
|
-
name:
|
|
69
|
-
description:
|
|
63
|
+
name: 'ghost/post',
|
|
64
|
+
description: 'Represents a post in Ghost CMS.',
|
|
70
65
|
schema: {
|
|
71
|
-
type:
|
|
66
|
+
type: 'object',
|
|
72
67
|
properties: {
|
|
73
|
-
id: { type:
|
|
74
|
-
uuid: { type:
|
|
75
|
-
title: { type:
|
|
68
|
+
id: { type: 'string', description: 'Unique ID of the post' },
|
|
69
|
+
uuid: { type: 'string', description: 'UUID of the post' },
|
|
70
|
+
title: { type: 'string', description: 'The title of the post' },
|
|
76
71
|
slug: {
|
|
77
|
-
type:
|
|
78
|
-
description:
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'URL-friendly version of the title',
|
|
79
74
|
},
|
|
80
75
|
html: {
|
|
81
|
-
type: [
|
|
82
|
-
description:
|
|
76
|
+
type: ['string', 'null'],
|
|
77
|
+
description: 'The post content as HTML',
|
|
83
78
|
},
|
|
84
79
|
plaintext: {
|
|
85
|
-
type: [
|
|
86
|
-
description:
|
|
80
|
+
type: ['string', 'null'],
|
|
81
|
+
description: 'The post content as plain text',
|
|
87
82
|
},
|
|
88
83
|
feature_image: {
|
|
89
|
-
type: [
|
|
90
|
-
description:
|
|
84
|
+
type: ['string', 'null'],
|
|
85
|
+
description: 'URL of the featured image',
|
|
91
86
|
},
|
|
92
87
|
feature_image_alt: {
|
|
93
|
-
type: [
|
|
94
|
-
description:
|
|
88
|
+
type: ['string', 'null'],
|
|
89
|
+
description: 'Alt text for the featured image',
|
|
95
90
|
},
|
|
96
91
|
feature_image_caption: {
|
|
97
|
-
type: [
|
|
98
|
-
description:
|
|
92
|
+
type: ['string', 'null'],
|
|
93
|
+
description: 'Caption for the featured image',
|
|
99
94
|
},
|
|
100
95
|
featured: {
|
|
101
|
-
type:
|
|
102
|
-
description:
|
|
96
|
+
type: 'boolean',
|
|
97
|
+
description: 'Whether the post is featured',
|
|
103
98
|
},
|
|
104
99
|
status: {
|
|
105
|
-
type:
|
|
106
|
-
enum: [
|
|
107
|
-
description:
|
|
100
|
+
type: 'string',
|
|
101
|
+
enum: ['published', 'draft', 'scheduled'],
|
|
102
|
+
description: 'Publication status',
|
|
108
103
|
},
|
|
109
104
|
visibility: {
|
|
110
|
-
type:
|
|
111
|
-
enum: [
|
|
112
|
-
description:
|
|
105
|
+
type: 'string',
|
|
106
|
+
enum: ['public', 'members', 'paid'],
|
|
107
|
+
description: 'Access level',
|
|
113
108
|
},
|
|
114
109
|
created_at: {
|
|
115
|
-
type:
|
|
116
|
-
format:
|
|
117
|
-
description:
|
|
110
|
+
type: 'string',
|
|
111
|
+
format: 'date-time',
|
|
112
|
+
description: 'Date/time post was created',
|
|
118
113
|
},
|
|
119
114
|
updated_at: {
|
|
120
|
-
type:
|
|
121
|
-
format:
|
|
122
|
-
description:
|
|
115
|
+
type: 'string',
|
|
116
|
+
format: 'date-time',
|
|
117
|
+
description: 'Date/time post was last updated',
|
|
123
118
|
},
|
|
124
119
|
published_at: {
|
|
125
|
-
type: [
|
|
126
|
-
format:
|
|
127
|
-
description:
|
|
120
|
+
type: ['string', 'null'],
|
|
121
|
+
format: 'date-time',
|
|
122
|
+
description: 'Date/time post was published or scheduled',
|
|
128
123
|
},
|
|
129
124
|
custom_excerpt: {
|
|
130
|
-
type: [
|
|
131
|
-
description:
|
|
125
|
+
type: ['string', 'null'],
|
|
126
|
+
description: 'Custom excerpt for the post',
|
|
132
127
|
},
|
|
133
|
-
meta_title: { type: [
|
|
128
|
+
meta_title: { type: ['string', 'null'], description: 'Custom SEO title' },
|
|
134
129
|
meta_description: {
|
|
135
|
-
type: [
|
|
136
|
-
description:
|
|
130
|
+
type: ['string', 'null'],
|
|
131
|
+
description: 'Custom SEO description',
|
|
137
132
|
},
|
|
138
133
|
tags: {
|
|
139
|
-
type:
|
|
140
|
-
description:
|
|
141
|
-
items: { $ref:
|
|
134
|
+
type: 'array',
|
|
135
|
+
description: 'Tags associated with the post',
|
|
136
|
+
items: { $ref: '#/definitions/ghost/tag' }, // Reference the ghost/tag resource
|
|
142
137
|
},
|
|
143
138
|
// Add authors or other relevant fields if needed
|
|
144
139
|
},
|
|
145
|
-
required: [
|
|
146
|
-
"id",
|
|
147
|
-
"uuid",
|
|
148
|
-
"title",
|
|
149
|
-
"slug",
|
|
150
|
-
"status",
|
|
151
|
-
"visibility",
|
|
152
|
-
"created_at",
|
|
153
|
-
"updated_at",
|
|
154
|
-
],
|
|
140
|
+
required: ['id', 'uuid', 'title', 'slug', 'status', 'visibility', 'created_at', 'updated_at'],
|
|
155
141
|
definitions: {
|
|
156
142
|
// Make the referenced tag resource available within this schema's scope
|
|
157
|
-
|
|
143
|
+
'ghost/tag': ghostTagResource.schema,
|
|
158
144
|
},
|
|
159
145
|
},
|
|
160
146
|
});
|
|
@@ -170,69 +156,67 @@ logger.info('Defining MCP Tools');
|
|
|
170
156
|
|
|
171
157
|
// Create Post Tool (Adding this missing tool)
|
|
172
158
|
const createPostTool = new Tool({
|
|
173
|
-
name:
|
|
159
|
+
name: 'ghost_create_post',
|
|
174
160
|
description:
|
|
175
|
-
|
|
161
|
+
'Creates a new post in Ghost CMS. Handles tag creation/lookup. Returns the created post data.',
|
|
176
162
|
inputSchema: {
|
|
177
|
-
type:
|
|
163
|
+
type: 'object',
|
|
178
164
|
properties: {
|
|
179
|
-
title: { type:
|
|
165
|
+
title: { type: 'string', description: 'The title for the new post.' },
|
|
180
166
|
html: {
|
|
181
|
-
type:
|
|
182
|
-
description:
|
|
167
|
+
type: 'string',
|
|
168
|
+
description: 'The HTML content for the new post.',
|
|
183
169
|
},
|
|
184
170
|
status: {
|
|
185
|
-
type:
|
|
186
|
-
enum: [
|
|
187
|
-
default:
|
|
188
|
-
description:
|
|
189
|
-
"The status for the post (published, draft, scheduled). Defaults to draft.",
|
|
171
|
+
type: 'string',
|
|
172
|
+
enum: ['published', 'draft', 'scheduled'],
|
|
173
|
+
default: 'draft',
|
|
174
|
+
description: 'The status for the post (published, draft, scheduled). Defaults to draft.',
|
|
190
175
|
},
|
|
191
176
|
tags: {
|
|
192
|
-
type:
|
|
193
|
-
items: { type:
|
|
194
|
-
description:
|
|
195
|
-
"Optional: An array of tag names (strings) to associate with the post.",
|
|
177
|
+
type: 'array',
|
|
178
|
+
items: { type: 'string' },
|
|
179
|
+
description: 'Optional: An array of tag names (strings) to associate with the post.',
|
|
196
180
|
},
|
|
197
181
|
published_at: {
|
|
198
|
-
type:
|
|
199
|
-
format:
|
|
182
|
+
type: 'string',
|
|
183
|
+
format: 'date-time',
|
|
200
184
|
description:
|
|
201
|
-
|
|
185
|
+
'Optional: The ISO 8601 date/time for publishing or scheduling. Required if status is scheduled.',
|
|
202
186
|
},
|
|
203
187
|
custom_excerpt: {
|
|
204
|
-
type:
|
|
205
|
-
description:
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'Optional: A custom short summary for the post.',
|
|
206
190
|
},
|
|
207
191
|
feature_image: {
|
|
208
|
-
type:
|
|
209
|
-
format:
|
|
192
|
+
type: 'string',
|
|
193
|
+
format: 'url',
|
|
210
194
|
description:
|
|
211
|
-
|
|
195
|
+
'Optional: URL of the image (e.g., from ghost_upload_image tool) to use as the featured image.',
|
|
212
196
|
},
|
|
213
197
|
feature_image_alt: {
|
|
214
|
-
type:
|
|
215
|
-
description:
|
|
198
|
+
type: 'string',
|
|
199
|
+
description: 'Optional: Alt text for the featured image.',
|
|
216
200
|
},
|
|
217
201
|
feature_image_caption: {
|
|
218
|
-
type:
|
|
219
|
-
description:
|
|
202
|
+
type: 'string',
|
|
203
|
+
description: 'Optional: Caption for the featured image.',
|
|
220
204
|
},
|
|
221
205
|
meta_title: {
|
|
222
|
-
type:
|
|
206
|
+
type: 'string',
|
|
223
207
|
description:
|
|
224
|
-
|
|
208
|
+
'Optional: Custom title for SEO (max 300 chars). Defaults to post title if omitted.',
|
|
225
209
|
},
|
|
226
210
|
meta_description: {
|
|
227
|
-
type:
|
|
211
|
+
type: 'string',
|
|
228
212
|
description:
|
|
229
|
-
|
|
213
|
+
'Optional: Custom description for SEO (max 500 chars). Defaults to excerpt or generated summary if omitted.',
|
|
230
214
|
},
|
|
231
215
|
},
|
|
232
|
-
required: [
|
|
216
|
+
required: ['title', 'html'],
|
|
233
217
|
},
|
|
234
218
|
outputSchema: {
|
|
235
|
-
$ref:
|
|
219
|
+
$ref: 'ghost/post#/schema',
|
|
236
220
|
},
|
|
237
221
|
implementation: async (input) => {
|
|
238
222
|
logger.toolExecution(createPostTool.name, input);
|
|
@@ -251,40 +235,40 @@ logger.info('Added MCP Tool', { toolName: createPostTool.name });
|
|
|
251
235
|
|
|
252
236
|
// Upload Image Tool
|
|
253
237
|
const uploadImageTool = new Tool({
|
|
254
|
-
name:
|
|
238
|
+
name: 'ghost_upload_image',
|
|
255
239
|
description:
|
|
256
|
-
|
|
240
|
+
'Downloads an image from a URL, processes it, uploads it to Ghost CMS, and returns the final Ghost image URL and alt text.',
|
|
257
241
|
inputSchema: {
|
|
258
|
-
type:
|
|
242
|
+
type: 'object',
|
|
259
243
|
properties: {
|
|
260
244
|
imageUrl: {
|
|
261
|
-
type:
|
|
262
|
-
format:
|
|
263
|
-
description:
|
|
245
|
+
type: 'string',
|
|
246
|
+
format: 'url',
|
|
247
|
+
description: 'The publicly accessible URL of the image to upload.',
|
|
264
248
|
},
|
|
265
249
|
alt: {
|
|
266
|
-
type:
|
|
250
|
+
type: 'string',
|
|
267
251
|
description:
|
|
268
|
-
|
|
252
|
+
'Optional: Alt text for the image. If omitted, a default will be generated from the filename.',
|
|
269
253
|
},
|
|
270
254
|
// filenameHint: { type: 'string', description: 'Optional: A hint for the original filename, used for default alt text generation.' }
|
|
271
255
|
},
|
|
272
|
-
required: [
|
|
256
|
+
required: ['imageUrl'],
|
|
273
257
|
},
|
|
274
258
|
outputSchema: {
|
|
275
|
-
type:
|
|
259
|
+
type: 'object',
|
|
276
260
|
properties: {
|
|
277
261
|
url: {
|
|
278
|
-
type:
|
|
279
|
-
format:
|
|
280
|
-
description:
|
|
262
|
+
type: 'string',
|
|
263
|
+
format: 'url',
|
|
264
|
+
description: 'The final URL of the image hosted on Ghost.',
|
|
281
265
|
},
|
|
282
266
|
alt: {
|
|
283
|
-
type:
|
|
284
|
-
description:
|
|
267
|
+
type: 'string',
|
|
268
|
+
description: 'The alt text determined for the image.',
|
|
285
269
|
},
|
|
286
270
|
},
|
|
287
|
-
required: [
|
|
271
|
+
required: ['url', 'alt'],
|
|
288
272
|
},
|
|
289
273
|
implementation: async (input) => {
|
|
290
274
|
logger.toolExecution(uploadImageTool.name, { imageUrl: input.imageUrl });
|
|
@@ -298,27 +282,23 @@ const uploadImageTool = new Tool({
|
|
|
298
282
|
if (!urlValidation.isValid) {
|
|
299
283
|
throw new Error(`Invalid image URL: ${urlValidation.error}`);
|
|
300
284
|
}
|
|
301
|
-
|
|
285
|
+
|
|
302
286
|
// --- 2. Download the image with security controls ---
|
|
303
287
|
const axiosConfig = createSecureAxiosConfig(urlValidation.sanitizedUrl);
|
|
304
288
|
const response = await axios(axiosConfig);
|
|
305
289
|
// Generate a unique temporary filename
|
|
306
290
|
const tempDir = os.tmpdir();
|
|
307
|
-
const extension = path.extname(imageUrl.split(
|
|
291
|
+
const extension = path.extname(imageUrl.split('?')[0]) || '.tmp'; // Basic extension extraction
|
|
308
292
|
const originalFilenameHint =
|
|
309
|
-
path.basename(imageUrl.split(
|
|
310
|
-
|
|
311
|
-
downloadedPath = path.join(
|
|
312
|
-
tempDir,
|
|
313
|
-
`mcp-download-${uuidv4()}${extension}`
|
|
314
|
-
);
|
|
293
|
+
path.basename(imageUrl.split('?')[0]) || `image-${uuidv4()}${extension}`;
|
|
294
|
+
downloadedPath = path.join(tempDir, `mcp-download-${uuidv4()}${extension}`);
|
|
315
295
|
|
|
316
296
|
const writer = fs.createWriteStream(downloadedPath);
|
|
317
297
|
response.data.pipe(writer);
|
|
318
298
|
|
|
319
299
|
await new Promise((resolve, reject) => {
|
|
320
|
-
writer.on(
|
|
321
|
-
writer.on(
|
|
300
|
+
writer.on('finish', resolve);
|
|
301
|
+
writer.on('error', reject);
|
|
322
302
|
});
|
|
323
303
|
logger.fileOperation('download', downloadedPath);
|
|
324
304
|
|
|
@@ -345,9 +325,7 @@ const uploadImageTool = new Tool({
|
|
|
345
325
|
} catch (error) {
|
|
346
326
|
logger.toolError(uploadImageTool.name, error, { imageUrl });
|
|
347
327
|
// Add more specific error handling (download failed, processing failed, upload failed)
|
|
348
|
-
throw new Error(
|
|
349
|
-
`Failed to upload image from URL ${imageUrl}: ${error.message}`
|
|
350
|
-
);
|
|
328
|
+
throw new Error(`Failed to upload image from URL ${imageUrl}: ${error.message}`);
|
|
351
329
|
} finally {
|
|
352
330
|
// --- 7. Cleanup temporary files ---
|
|
353
331
|
if (downloadedPath) {
|
|
@@ -355,7 +333,7 @@ const uploadImageTool = new Tool({
|
|
|
355
333
|
if (err)
|
|
356
334
|
logger.warn('Failed to delete temporary downloaded file', {
|
|
357
335
|
file: path.basename(downloadedPath),
|
|
358
|
-
error: err.message
|
|
336
|
+
error: err.message,
|
|
359
337
|
});
|
|
360
338
|
});
|
|
361
339
|
}
|
|
@@ -364,7 +342,7 @@ const uploadImageTool = new Tool({
|
|
|
364
342
|
if (err)
|
|
365
343
|
logger.warn('Failed to delete temporary processed file', {
|
|
366
344
|
file: path.basename(processedPath),
|
|
367
|
-
error: err.message
|
|
345
|
+
error: err.message,
|
|
368
346
|
});
|
|
369
347
|
});
|
|
370
348
|
}
|
|
@@ -375,17 +353,13 @@ const uploadImageTool = new Tool({
|
|
|
375
353
|
// Helper function for default alt text (similar to imageController)
|
|
376
354
|
const getDefaultAltText = (filePath) => {
|
|
377
355
|
try {
|
|
378
|
-
const originalFilename = path
|
|
379
|
-
.basename(filePath)
|
|
380
|
-
.split(".")
|
|
381
|
-
.slice(0, -1)
|
|
382
|
-
.join(".");
|
|
356
|
+
const originalFilename = path.basename(filePath).split('.').slice(0, -1).join('.');
|
|
383
357
|
const nameWithoutIds = originalFilename
|
|
384
|
-
.replace(/^(processed-|mcp-download-|mcp-upload-)\d+-\d+-?/,
|
|
385
|
-
.replace(/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}-?/,
|
|
386
|
-
return nameWithoutIds.replace(/[-_]/g,
|
|
387
|
-
} catch (
|
|
388
|
-
return
|
|
358
|
+
.replace(/^(processed-|mcp-download-|mcp-upload-)\d+-\d+-?/, '')
|
|
359
|
+
.replace(/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}-?/, ''); // Remove UUIDs too
|
|
360
|
+
return nameWithoutIds.replace(/[-_]/g, ' ').trim() || 'Uploaded image';
|
|
361
|
+
} catch (_e) {
|
|
362
|
+
return 'Uploaded image';
|
|
389
363
|
}
|
|
390
364
|
};
|
|
391
365
|
|
|
@@ -394,21 +368,20 @@ logger.info('Added MCP Tool', { toolName: uploadImageTool.name });
|
|
|
394
368
|
|
|
395
369
|
// Get Tags Tool
|
|
396
370
|
const getTagsTool = new Tool({
|
|
397
|
-
name:
|
|
398
|
-
description:
|
|
399
|
-
"Retrieves a list of tags from Ghost CMS. Can optionally filter by tag name.",
|
|
371
|
+
name: 'ghost_get_tags',
|
|
372
|
+
description: 'Retrieves a list of tags from Ghost CMS. Can optionally filter by tag name.',
|
|
400
373
|
inputSchema: {
|
|
401
|
-
type:
|
|
374
|
+
type: 'object',
|
|
402
375
|
properties: {
|
|
403
376
|
name: {
|
|
404
|
-
type:
|
|
405
|
-
description:
|
|
377
|
+
type: 'string',
|
|
378
|
+
description: 'Optional: The exact name of the tag to search for.',
|
|
406
379
|
},
|
|
407
380
|
},
|
|
408
381
|
},
|
|
409
382
|
outputSchema: {
|
|
410
|
-
type:
|
|
411
|
-
items: { $ref:
|
|
383
|
+
type: 'array',
|
|
384
|
+
items: { $ref: 'ghost/tag#/schema' }, // Output is an array of ghost/tag resources
|
|
412
385
|
},
|
|
413
386
|
implementation: async (input) => {
|
|
414
387
|
logger.toolExecution(getTagsTool.name, input);
|
|
@@ -428,27 +401,27 @@ logger.info('Added MCP Tool', { toolName: getTagsTool.name });
|
|
|
428
401
|
|
|
429
402
|
// Create Tag Tool
|
|
430
403
|
const createTagTool = new Tool({
|
|
431
|
-
name:
|
|
432
|
-
description:
|
|
404
|
+
name: 'ghost_create_tag',
|
|
405
|
+
description: 'Creates a new tag in Ghost CMS. Returns the created tag.',
|
|
433
406
|
inputSchema: {
|
|
434
|
-
type:
|
|
407
|
+
type: 'object',
|
|
435
408
|
properties: {
|
|
436
|
-
name: { type:
|
|
409
|
+
name: { type: 'string', description: 'The name for the new tag.' },
|
|
437
410
|
description: {
|
|
438
|
-
type:
|
|
439
|
-
description:
|
|
411
|
+
type: 'string',
|
|
412
|
+
description: 'Optional: A description for the tag (max 500 chars).',
|
|
440
413
|
},
|
|
441
414
|
slug: {
|
|
442
|
-
type:
|
|
415
|
+
type: 'string',
|
|
443
416
|
description:
|
|
444
|
-
|
|
417
|
+
'Optional: A URL-friendly slug. If omitted, Ghost generates one from the name.',
|
|
445
418
|
},
|
|
446
419
|
// Add other createable fields like color, feature_image etc. if needed
|
|
447
420
|
},
|
|
448
|
-
required: [
|
|
421
|
+
required: ['name'],
|
|
449
422
|
},
|
|
450
423
|
outputSchema: {
|
|
451
|
-
$ref:
|
|
424
|
+
$ref: 'ghost/tag#/schema', // Output is a single ghost/tag resource
|
|
452
425
|
},
|
|
453
426
|
implementation: async (input) => {
|
|
454
427
|
logger.toolExecution(createTagTool.name, input);
|
|
@@ -476,24 +449,24 @@ const startMCPServer = async (port = 3001) => {
|
|
|
476
449
|
// Ensure resources/tools are added before starting
|
|
477
450
|
logger.info('Starting MCP Server', { port });
|
|
478
451
|
await mcpServer.listen({ port });
|
|
479
|
-
|
|
452
|
+
|
|
480
453
|
const resources = mcpServer.listResources().map((r) => r.name);
|
|
481
454
|
const tools = mcpServer.listTools().map((t) => t.name);
|
|
482
|
-
|
|
455
|
+
|
|
483
456
|
logger.info('MCP Server started successfully', {
|
|
484
457
|
port,
|
|
485
458
|
resourceCount: resources.length,
|
|
486
459
|
toolCount: tools.length,
|
|
487
460
|
resources,
|
|
488
461
|
tools,
|
|
489
|
-
type: 'server_start'
|
|
462
|
+
type: 'server_start',
|
|
490
463
|
});
|
|
491
464
|
} catch (error) {
|
|
492
465
|
logger.error('Failed to start MCP Server', {
|
|
493
466
|
port,
|
|
494
467
|
error: error.message,
|
|
495
468
|
stack: error.stack,
|
|
496
|
-
type: 'server_start_error'
|
|
469
|
+
type: 'server_start_error',
|
|
497
470
|
});
|
|
498
471
|
process.exit(1);
|
|
499
472
|
}
|