@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,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced MCP Server with Advanced Resource Management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
MCPServer,
|
|
7
|
+
Tool,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/server/index.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
11
|
+
import { WebSocketServerTransport } from "@modelcontextprotocol/sdk/server/websocket.js";
|
|
12
|
+
import dotenv from "dotenv";
|
|
13
|
+
import express from "express";
|
|
14
|
+
import { WebSocketServer } from "ws";
|
|
15
|
+
|
|
16
|
+
// Import services
|
|
17
|
+
import ghostService from "./services/ghostServiceImproved.js";
|
|
18
|
+
import { ResourceManager } from "./resources/ResourceManager.js";
|
|
19
|
+
import { ErrorHandler, ValidationError } from "./errors/index.js";
|
|
20
|
+
import {
|
|
21
|
+
errorLogger,
|
|
22
|
+
mcpCors,
|
|
23
|
+
RateLimiter,
|
|
24
|
+
healthCheck,
|
|
25
|
+
GracefulShutdown
|
|
26
|
+
} from "./middleware/errorMiddleware.js";
|
|
27
|
+
|
|
28
|
+
dotenv.config();
|
|
29
|
+
|
|
30
|
+
console.log("Initializing Enhanced MCP Server...");
|
|
31
|
+
|
|
32
|
+
// Initialize components
|
|
33
|
+
const resourceManager = new ResourceManager(ghostService);
|
|
34
|
+
const rateLimiter = new RateLimiter();
|
|
35
|
+
const gracefulShutdown = new GracefulShutdown();
|
|
36
|
+
|
|
37
|
+
// Create MCP Server
|
|
38
|
+
const mcpServer = new MCPServer({
|
|
39
|
+
metadata: {
|
|
40
|
+
name: "Ghost CMS Manager",
|
|
41
|
+
description: "Enhanced MCP Server for Ghost CMS with advanced resource management",
|
|
42
|
+
version: "2.0.0",
|
|
43
|
+
capabilities: {
|
|
44
|
+
resources: true,
|
|
45
|
+
tools: true,
|
|
46
|
+
subscriptions: true,
|
|
47
|
+
batch: true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// --- Register Resources with Enhanced Fetching ---
|
|
53
|
+
|
|
54
|
+
console.log("Registering enhanced resources...");
|
|
55
|
+
|
|
56
|
+
// Ghost Post Resource
|
|
57
|
+
const postResource = resourceManager.registerResource(
|
|
58
|
+
"ghost/post",
|
|
59
|
+
{
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
id: { type: "string" },
|
|
63
|
+
uuid: { type: "string" },
|
|
64
|
+
title: { type: "string" },
|
|
65
|
+
slug: { type: "string" },
|
|
66
|
+
html: { type: ["string", "null"] },
|
|
67
|
+
status: {
|
|
68
|
+
type: "string",
|
|
69
|
+
enum: ["draft", "published", "scheduled"]
|
|
70
|
+
},
|
|
71
|
+
feature_image: { type: ["string", "null"] },
|
|
72
|
+
published_at: {
|
|
73
|
+
type: ["string", "null"],
|
|
74
|
+
format: "date-time"
|
|
75
|
+
},
|
|
76
|
+
tags: {
|
|
77
|
+
type: "array",
|
|
78
|
+
items: { $ref: "ghost/tag#/schema" }
|
|
79
|
+
},
|
|
80
|
+
meta_title: { type: ["string", "null"] },
|
|
81
|
+
meta_description: { type: ["string", "null"] }
|
|
82
|
+
},
|
|
83
|
+
required: ["id", "uuid", "title", "slug", "status"]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
description: "Ghost blog post with support for multiple identifier types",
|
|
87
|
+
examples: [
|
|
88
|
+
"ghost/post/123",
|
|
89
|
+
"ghost/post/slug:my-awesome-post",
|
|
90
|
+
"ghost/post/uuid:550e8400-e29b-41d4-a716-446655440000",
|
|
91
|
+
"ghost/posts?status=published&limit=10&page=1"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
mcpServer.addResource(postResource);
|
|
97
|
+
|
|
98
|
+
// Ghost Tag Resource
|
|
99
|
+
const tagResource = resourceManager.registerResource(
|
|
100
|
+
"ghost/tag",
|
|
101
|
+
{
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
id: { type: "string" },
|
|
105
|
+
name: { type: "string" },
|
|
106
|
+
slug: { type: "string" },
|
|
107
|
+
description: { type: ["string", "null"] },
|
|
108
|
+
feature_image: { type: ["string", "null"] },
|
|
109
|
+
visibility: {
|
|
110
|
+
type: "string",
|
|
111
|
+
enum: ["public", "internal"]
|
|
112
|
+
},
|
|
113
|
+
meta_title: { type: ["string", "null"] },
|
|
114
|
+
meta_description: { type: ["string", "null"] }
|
|
115
|
+
},
|
|
116
|
+
required: ["id", "name", "slug"]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
description: "Ghost tag for categorizing posts",
|
|
120
|
+
examples: [
|
|
121
|
+
"ghost/tag/technology",
|
|
122
|
+
"ghost/tag/slug:web-development",
|
|
123
|
+
"ghost/tag/name:JavaScript",
|
|
124
|
+
"ghost/tags?limit=20"
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
mcpServer.addResource(tagResource);
|
|
130
|
+
|
|
131
|
+
// --- Enhanced Tools ---
|
|
132
|
+
|
|
133
|
+
console.log("Registering enhanced tools...");
|
|
134
|
+
|
|
135
|
+
// Batch Operations Tool
|
|
136
|
+
const batchOperationsTool = new Tool({
|
|
137
|
+
name: "ghost_batch_operations",
|
|
138
|
+
description: "Execute multiple Ghost operations in a single request",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
operations: {
|
|
143
|
+
type: "array",
|
|
144
|
+
items: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
id: { type: "string", description: "Operation ID for reference" },
|
|
148
|
+
type: {
|
|
149
|
+
type: "string",
|
|
150
|
+
enum: ["create_post", "update_post", "create_tag", "fetch_resource"]
|
|
151
|
+
},
|
|
152
|
+
data: { type: "object", description: "Operation-specific data" }
|
|
153
|
+
},
|
|
154
|
+
required: ["id", "type", "data"]
|
|
155
|
+
},
|
|
156
|
+
minItems: 1,
|
|
157
|
+
maxItems: 10
|
|
158
|
+
},
|
|
159
|
+
stopOnError: {
|
|
160
|
+
type: "boolean",
|
|
161
|
+
default: false,
|
|
162
|
+
description: "Stop processing on first error"
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
required: ["operations"]
|
|
166
|
+
},
|
|
167
|
+
outputSchema: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: {
|
|
170
|
+
results: {
|
|
171
|
+
type: "object",
|
|
172
|
+
additionalProperties: {
|
|
173
|
+
type: "object",
|
|
174
|
+
properties: {
|
|
175
|
+
success: { type: "boolean" },
|
|
176
|
+
data: { type: "object" },
|
|
177
|
+
error: { type: "object" }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
implementation: async (input) => {
|
|
184
|
+
const results = {};
|
|
185
|
+
|
|
186
|
+
for (const operation of input.operations) {
|
|
187
|
+
try {
|
|
188
|
+
let result;
|
|
189
|
+
|
|
190
|
+
switch (operation.type) {
|
|
191
|
+
case 'create_post':
|
|
192
|
+
result = await ghostService.createPost(operation.data);
|
|
193
|
+
break;
|
|
194
|
+
case 'update_post':
|
|
195
|
+
result = await ghostService.updatePost(
|
|
196
|
+
operation.data.id,
|
|
197
|
+
operation.data
|
|
198
|
+
);
|
|
199
|
+
break;
|
|
200
|
+
case 'create_tag':
|
|
201
|
+
result = await ghostService.createTag(operation.data);
|
|
202
|
+
break;
|
|
203
|
+
case 'fetch_resource':
|
|
204
|
+
result = await resourceManager.fetchResource(operation.data.uri);
|
|
205
|
+
break;
|
|
206
|
+
default:
|
|
207
|
+
throw new ValidationError(`Unknown operation type: ${operation.type}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
results[operation.id] = {
|
|
211
|
+
success: true,
|
|
212
|
+
data: result
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
} catch (error) {
|
|
216
|
+
results[operation.id] = {
|
|
217
|
+
success: false,
|
|
218
|
+
error: ErrorHandler.formatMCPError(error)
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (input.stopOnError) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { results };
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
mcpServer.addTool(batchOperationsTool);
|
|
232
|
+
|
|
233
|
+
// Resource Search Tool
|
|
234
|
+
const searchResourcesTool = new Tool({
|
|
235
|
+
name: "ghost_search_resources",
|
|
236
|
+
description: "Search for Ghost resources with advanced filtering",
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
resourceType: {
|
|
241
|
+
type: "string",
|
|
242
|
+
enum: ["posts", "tags"],
|
|
243
|
+
description: "Type of resource to search"
|
|
244
|
+
},
|
|
245
|
+
query: {
|
|
246
|
+
type: "string",
|
|
247
|
+
description: "Search query"
|
|
248
|
+
},
|
|
249
|
+
filters: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
status: { type: "string" },
|
|
253
|
+
visibility: { type: "string" },
|
|
254
|
+
tag: { type: "string" },
|
|
255
|
+
author: { type: "string" },
|
|
256
|
+
published_after: { type: "string", format: "date-time" },
|
|
257
|
+
published_before: { type: "string", format: "date-time" }
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
sort: {
|
|
261
|
+
type: "string",
|
|
262
|
+
default: "published_at desc",
|
|
263
|
+
description: "Sort order"
|
|
264
|
+
},
|
|
265
|
+
limit: {
|
|
266
|
+
type: "integer",
|
|
267
|
+
minimum: 1,
|
|
268
|
+
maximum: 100,
|
|
269
|
+
default: 15
|
|
270
|
+
},
|
|
271
|
+
page: {
|
|
272
|
+
type: "integer",
|
|
273
|
+
minimum: 1,
|
|
274
|
+
default: 1
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
required: ["resourceType"]
|
|
278
|
+
},
|
|
279
|
+
implementation: async (input) => {
|
|
280
|
+
const { resourceType, query, filters = {}, sort, limit, page } = input;
|
|
281
|
+
|
|
282
|
+
// Build Ghost filter string
|
|
283
|
+
let filterParts = [];
|
|
284
|
+
|
|
285
|
+
if (query) {
|
|
286
|
+
filterParts.push(`title:~'${query}'`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (filters.status) {
|
|
290
|
+
filterParts.push(`status:${filters.status}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (filters.visibility) {
|
|
294
|
+
filterParts.push(`visibility:${filters.visibility}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (filters.tag) {
|
|
298
|
+
filterParts.push(`tag:${filters.tag}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (filters.author) {
|
|
302
|
+
filterParts.push(`author:${filters.author}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (filters.published_after) {
|
|
306
|
+
filterParts.push(`published_at:>='${filters.published_after}'`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (filters.published_before) {
|
|
310
|
+
filterParts.push(`published_at:<='${filters.published_before}'`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const ghostFilter = filterParts.join('+');
|
|
314
|
+
|
|
315
|
+
// Build resource URI
|
|
316
|
+
const uri = `ghost/${resourceType}?${new URLSearchParams({
|
|
317
|
+
filter: ghostFilter,
|
|
318
|
+
order: sort,
|
|
319
|
+
limit,
|
|
320
|
+
page
|
|
321
|
+
}).toString()}`;
|
|
322
|
+
|
|
323
|
+
// Fetch using ResourceManager
|
|
324
|
+
return await resourceManager.fetchResource(uri);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
mcpServer.addTool(searchResourcesTool);
|
|
329
|
+
|
|
330
|
+
// Cache Management Tool
|
|
331
|
+
const cacheManagementTool = new Tool({
|
|
332
|
+
name: "ghost_cache_management",
|
|
333
|
+
description: "Manage resource cache",
|
|
334
|
+
inputSchema: {
|
|
335
|
+
type: "object",
|
|
336
|
+
properties: {
|
|
337
|
+
action: {
|
|
338
|
+
type: "string",
|
|
339
|
+
enum: ["invalidate", "stats", "prefetch"],
|
|
340
|
+
description: "Cache action to perform"
|
|
341
|
+
},
|
|
342
|
+
pattern: {
|
|
343
|
+
type: "string",
|
|
344
|
+
description: "Pattern for invalidation (optional)"
|
|
345
|
+
},
|
|
346
|
+
uris: {
|
|
347
|
+
type: "array",
|
|
348
|
+
items: { type: "string" },
|
|
349
|
+
description: "URIs to prefetch (for prefetch action)"
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
required: ["action"]
|
|
353
|
+
},
|
|
354
|
+
implementation: async (input) => {
|
|
355
|
+
switch (input.action) {
|
|
356
|
+
case 'invalidate':
|
|
357
|
+
resourceManager.invalidateCache(input.pattern);
|
|
358
|
+
return {
|
|
359
|
+
success: true,
|
|
360
|
+
message: `Cache invalidated${input.pattern ? ` for pattern: ${input.pattern}` : ''}`
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
case 'stats':
|
|
364
|
+
return resourceManager.getCacheStats();
|
|
365
|
+
|
|
366
|
+
case 'prefetch':
|
|
367
|
+
if (!input.uris || input.uris.length === 0) {
|
|
368
|
+
throw new ValidationError('URIs required for prefetch action');
|
|
369
|
+
}
|
|
370
|
+
return await resourceManager.prefetch(input.uris);
|
|
371
|
+
|
|
372
|
+
default:
|
|
373
|
+
throw new ValidationError(`Unknown action: ${input.action}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
mcpServer.addTool(cacheManagementTool);
|
|
379
|
+
|
|
380
|
+
// Resource Subscription Tool
|
|
381
|
+
const subscriptionTool = new Tool({
|
|
382
|
+
name: "ghost_subscribe",
|
|
383
|
+
description: "Subscribe to resource changes",
|
|
384
|
+
inputSchema: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: {
|
|
387
|
+
action: {
|
|
388
|
+
type: "string",
|
|
389
|
+
enum: ["subscribe", "unsubscribe"],
|
|
390
|
+
description: "Subscription action"
|
|
391
|
+
},
|
|
392
|
+
uri: {
|
|
393
|
+
type: "string",
|
|
394
|
+
description: "Resource URI to subscribe to (required for subscribe action)"
|
|
395
|
+
},
|
|
396
|
+
subscriptionId: {
|
|
397
|
+
type: "string",
|
|
398
|
+
description: "Subscription ID (required for unsubscribe action)"
|
|
399
|
+
},
|
|
400
|
+
options: {
|
|
401
|
+
type: "object",
|
|
402
|
+
properties: {
|
|
403
|
+
pollingInterval: {
|
|
404
|
+
type: "integer",
|
|
405
|
+
minimum: 5000,
|
|
406
|
+
default: 30000,
|
|
407
|
+
description: "Polling interval in milliseconds"
|
|
408
|
+
},
|
|
409
|
+
enablePolling: {
|
|
410
|
+
type: "boolean",
|
|
411
|
+
default: false,
|
|
412
|
+
description: "Enable automatic polling"
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
required: ["action"],
|
|
418
|
+
// Add conditional validation using JSON Schema if/then/else
|
|
419
|
+
if: {
|
|
420
|
+
properties: { action: { const: "subscribe" } }
|
|
421
|
+
},
|
|
422
|
+
then: {
|
|
423
|
+
required: ["uri"]
|
|
424
|
+
},
|
|
425
|
+
else: {
|
|
426
|
+
if: {
|
|
427
|
+
properties: { action: { const: "unsubscribe" } }
|
|
428
|
+
},
|
|
429
|
+
then: {
|
|
430
|
+
required: ["subscriptionId"]
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
implementation: async (input) => {
|
|
435
|
+
if (input.action === 'subscribe') {
|
|
436
|
+
if (!input.uri) {
|
|
437
|
+
throw new ValidationError('URI required for subscribe action');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const subscriptionId = resourceManager.subscribe(
|
|
441
|
+
input.uri,
|
|
442
|
+
(event) => {
|
|
443
|
+
// In a real implementation, this would send events to the client
|
|
444
|
+
console.log('Resource update:', event);
|
|
445
|
+
},
|
|
446
|
+
input.options || {}
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
success: true,
|
|
451
|
+
subscriptionId,
|
|
452
|
+
message: `Subscribed to ${input.uri}`
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
} else if (input.action === 'unsubscribe') {
|
|
456
|
+
if (!input.subscriptionId) {
|
|
457
|
+
throw new ValidationError('Subscription ID required for unsubscribe action');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
resourceManager.unsubscribe(input.subscriptionId);
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
success: true,
|
|
464
|
+
message: `Unsubscribed from ${input.subscriptionId}`
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
mcpServer.addTool(subscriptionTool);
|
|
471
|
+
|
|
472
|
+
// Keep existing tools (create_post, upload_image, etc.) from previous implementation
|
|
473
|
+
// ... (include the tools from mcp_server_improved.js)
|
|
474
|
+
|
|
475
|
+
// --- Enhanced Transport with Middleware ---
|
|
476
|
+
|
|
477
|
+
const startEnhancedMCPServer = async (transport = 'http', options = {}) => {
|
|
478
|
+
try {
|
|
479
|
+
console.log(`Starting Enhanced MCP Server with ${transport} transport...`);
|
|
480
|
+
|
|
481
|
+
switch (transport) {
|
|
482
|
+
case 'stdio':
|
|
483
|
+
const stdioTransport = new StdioServerTransport();
|
|
484
|
+
await mcpServer.connect(stdioTransport);
|
|
485
|
+
console.log("Enhanced MCP Server running on stdio transport");
|
|
486
|
+
break;
|
|
487
|
+
|
|
488
|
+
case 'http':
|
|
489
|
+
case 'sse':
|
|
490
|
+
const port = options.port || 3001;
|
|
491
|
+
const app = express();
|
|
492
|
+
|
|
493
|
+
// Apply middleware
|
|
494
|
+
app.use(gracefulShutdown.middleware());
|
|
495
|
+
app.use(rateLimiter.middleware());
|
|
496
|
+
app.use(mcpCors(options.allowedOrigins));
|
|
497
|
+
|
|
498
|
+
// Health check with Ghost status
|
|
499
|
+
app.get('/health', healthCheck(ghostService));
|
|
500
|
+
|
|
501
|
+
// Resource endpoints
|
|
502
|
+
app.get('/resources', async (req, res) => {
|
|
503
|
+
try {
|
|
504
|
+
const resources = resourceManager.listResources(req.query);
|
|
505
|
+
res.json(resources);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
const formatted = ErrorHandler.formatHTTPError(error);
|
|
508
|
+
res.status(formatted.statusCode).json(formatted.body);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
app.get('/resources/*', async (req, res) => {
|
|
513
|
+
try {
|
|
514
|
+
const uri = req.params[0];
|
|
515
|
+
|
|
516
|
+
// Validate and sanitize the URI to prevent path traversal
|
|
517
|
+
if (!uri || typeof uri !== 'string') {
|
|
518
|
+
throw new ValidationError('Invalid resource URI');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Ensure the URI doesn't contain path traversal attempts
|
|
522
|
+
if (uri.includes('..') || uri.includes('//') || uri.includes('\\')) {
|
|
523
|
+
throw new ValidationError('Invalid resource URI: path traversal detected');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Only allow specific URI patterns for Ghost resources
|
|
527
|
+
const validPatterns = /^ghost\/(post|posts|tag|tags|author|authors|page|pages)/;
|
|
528
|
+
if (!validPatterns.test(uri)) {
|
|
529
|
+
throw new ValidationError('Invalid resource type');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const result = await resourceManager.fetchResource(uri);
|
|
533
|
+
res.json(result);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
const formatted = ErrorHandler.formatHTTPError(error);
|
|
536
|
+
res.status(formatted.statusCode).json(formatted.body);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Batch endpoint
|
|
541
|
+
app.post('/batch', async (req, res) => {
|
|
542
|
+
try {
|
|
543
|
+
const result = await resourceManager.batchFetch(req.body.uris);
|
|
544
|
+
res.json(result);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
const formatted = ErrorHandler.formatHTTPError(error);
|
|
547
|
+
res.status(formatted.statusCode).json(formatted.body);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Cache stats endpoint
|
|
552
|
+
app.get('/cache/stats', (req, res) => {
|
|
553
|
+
res.json(resourceManager.getCacheStats());
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// SSE endpoint for MCP
|
|
557
|
+
const sseTransport = new SSEServerTransport();
|
|
558
|
+
app.get('/mcp/sse', sseTransport.handler());
|
|
559
|
+
|
|
560
|
+
await mcpServer.connect(sseTransport);
|
|
561
|
+
|
|
562
|
+
const server = app.listen(port, () => {
|
|
563
|
+
console.log(`Enhanced MCP Server (SSE) listening on port ${port}`);
|
|
564
|
+
console.log(`Health: http://localhost:${port}/health`);
|
|
565
|
+
console.log(`Resources: http://localhost:${port}/resources`);
|
|
566
|
+
console.log(`SSE: http://localhost:${port}/mcp/sse`);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
mcpServer._httpServer = server;
|
|
570
|
+
|
|
571
|
+
// Track connections for graceful shutdown
|
|
572
|
+
server.on('connection', (connection) => {
|
|
573
|
+
gracefulShutdown.trackConnection(connection);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
break;
|
|
577
|
+
|
|
578
|
+
case 'websocket':
|
|
579
|
+
const wsPort = options.port || 3001;
|
|
580
|
+
const wss = new WebSocketServer({ port: wsPort });
|
|
581
|
+
|
|
582
|
+
wss.on('connection', async (ws) => {
|
|
583
|
+
console.log('New WebSocket connection');
|
|
584
|
+
|
|
585
|
+
const wsTransport = new WebSocketServerTransport(ws);
|
|
586
|
+
await mcpServer.connect(wsTransport);
|
|
587
|
+
|
|
588
|
+
// Handle subscriptions over WebSocket
|
|
589
|
+
ws.on('message', async (data) => {
|
|
590
|
+
try {
|
|
591
|
+
const message = JSON.parse(data);
|
|
592
|
+
|
|
593
|
+
if (message.type === 'subscribe') {
|
|
594
|
+
const subscriptionId = resourceManager.subscribe(
|
|
595
|
+
message.uri,
|
|
596
|
+
(event) => {
|
|
597
|
+
ws.send(JSON.stringify({
|
|
598
|
+
type: 'subscription_update',
|
|
599
|
+
...event
|
|
600
|
+
}));
|
|
601
|
+
},
|
|
602
|
+
message.options || {}
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
ws.send(JSON.stringify({
|
|
606
|
+
type: 'subscription_created',
|
|
607
|
+
subscriptionId
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
610
|
+
} catch (error) {
|
|
611
|
+
ws.send(JSON.stringify({
|
|
612
|
+
type: 'error',
|
|
613
|
+
error: error.message
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
console.log(`Enhanced MCP Server (WebSocket) listening on port ${wsPort}`);
|
|
620
|
+
mcpServer._wss = wss;
|
|
621
|
+
break;
|
|
622
|
+
|
|
623
|
+
default:
|
|
624
|
+
throw new Error(`Unknown transport type: ${transport}`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Log capabilities
|
|
628
|
+
console.log("Server Capabilities:");
|
|
629
|
+
console.log("- Resources:", mcpServer.listResources().map(r => r.name));
|
|
630
|
+
console.log("- Tools:", mcpServer.listTools().map(t => t.name));
|
|
631
|
+
console.log("- Cache enabled with LRU eviction");
|
|
632
|
+
console.log("- Subscription support for real-time updates");
|
|
633
|
+
console.log("- Batch operations for efficiency");
|
|
634
|
+
|
|
635
|
+
} catch (error) {
|
|
636
|
+
errorLogger.logError(error);
|
|
637
|
+
console.error("Failed to start Enhanced MCP Server:", error);
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// Graceful shutdown
|
|
643
|
+
const shutdown = async () => {
|
|
644
|
+
console.log("\nShutting down Enhanced MCP Server...");
|
|
645
|
+
|
|
646
|
+
// Clear all subscriptions
|
|
647
|
+
resourceManager.subscriptionManager.subscriptions.clear();
|
|
648
|
+
|
|
649
|
+
// Close servers
|
|
650
|
+
if (mcpServer._httpServer) {
|
|
651
|
+
await gracefulShutdown.shutdown(mcpServer._httpServer);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (mcpServer._wss) {
|
|
655
|
+
mcpServer._wss.close();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
await mcpServer.close();
|
|
659
|
+
process.exit(0);
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
process.on('SIGINT', shutdown);
|
|
663
|
+
process.on('SIGTERM', shutdown);
|
|
664
|
+
|
|
665
|
+
// Export
|
|
666
|
+
export { mcpServer, startEnhancedMCPServer, resourceManager };
|
|
667
|
+
|
|
668
|
+
// If running directly
|
|
669
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
670
|
+
const transport = process.env.MCP_TRANSPORT || 'http';
|
|
671
|
+
const port = parseInt(process.env.MCP_PORT || '3001');
|
|
672
|
+
const allowedOrigins = process.env.MCP_ALLOWED_ORIGINS?.split(',') || ['*'];
|
|
673
|
+
|
|
674
|
+
startEnhancedMCPServer(transport, { port, allowedOrigins });
|
|
675
|
+
}
|