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