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