@jgardner04/ghost-mcp-server 1.0.0 → 1.1.1
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 +21 -8
- 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 +217 -582
- 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 +118 -0
- 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
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ErrorHandler,
|
|
3
|
-
BaseError,
|
|
4
3
|
ValidationError,
|
|
5
|
-
NotFoundError,
|
|
6
4
|
AuthenticationError,
|
|
7
|
-
|
|
8
|
-
RateLimitError
|
|
5
|
+
RateLimitError,
|
|
9
6
|
} from '../errors/index.js';
|
|
10
7
|
import fs from 'fs/promises';
|
|
11
8
|
import path from 'path';
|
|
@@ -23,7 +20,7 @@ export class ErrorLogger {
|
|
|
23
20
|
this.maxLogSize = options.maxLogSize || 10 * 1024 * 1024; // 10MB
|
|
24
21
|
this.logLevel = options.logLevel || process.env.LOG_LEVEL || 'info';
|
|
25
22
|
this.enableFileLogging = options.enableFileLogging ?? true;
|
|
26
|
-
|
|
23
|
+
|
|
27
24
|
// Ensure log directory exists
|
|
28
25
|
if (this.enableFileLogging) {
|
|
29
26
|
this.ensureLogDirectory();
|
|
@@ -52,27 +49,29 @@ export class ErrorLogger {
|
|
|
52
49
|
const rotatedPath = filePath.replace('.log', `-${timestamp}.log`);
|
|
53
50
|
await fs.rename(filePath, rotatedPath);
|
|
54
51
|
}
|
|
55
|
-
} catch (
|
|
52
|
+
} catch (_error) {
|
|
56
53
|
// File doesn't exist yet, which is fine
|
|
57
54
|
}
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
formatLogEntry(level, message, meta = {}) {
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
return (
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
timestamp: new Date().toISOString(),
|
|
61
|
+
level,
|
|
62
|
+
message,
|
|
63
|
+
...meta,
|
|
64
|
+
environment: process.env.NODE_ENV || 'development',
|
|
65
|
+
pid: process.pid,
|
|
66
|
+
}) + '\n'
|
|
67
|
+
);
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
async writeToFile(type, entry) {
|
|
72
71
|
if (!this.enableFileLogging) return;
|
|
73
|
-
|
|
72
|
+
|
|
74
73
|
const filePath = this.getLogFilePath(type);
|
|
75
|
-
|
|
74
|
+
|
|
76
75
|
try {
|
|
77
76
|
await this.rotateLogIfNeeded(filePath);
|
|
78
77
|
await fs.appendFile(filePath, entry);
|
|
@@ -84,7 +83,7 @@ export class ErrorLogger {
|
|
|
84
83
|
async logError(error, context = {}) {
|
|
85
84
|
const isOperational = ErrorHandler.isOperationalError(error);
|
|
86
85
|
const level = isOperational ? 'error' : 'fatal';
|
|
87
|
-
|
|
86
|
+
|
|
88
87
|
const logData = {
|
|
89
88
|
name: error.name || 'Error',
|
|
90
89
|
message: error.message,
|
|
@@ -92,7 +91,7 @@ export class ErrorLogger {
|
|
|
92
91
|
statusCode: error.statusCode,
|
|
93
92
|
stack: error.stack,
|
|
94
93
|
isOperational,
|
|
95
|
-
...context
|
|
94
|
+
...context,
|
|
96
95
|
};
|
|
97
96
|
|
|
98
97
|
// Console logging
|
|
@@ -145,21 +144,22 @@ export class ErrorMetrics {
|
|
|
145
144
|
errorsByType: {},
|
|
146
145
|
errorsByStatusCode: {},
|
|
147
146
|
errorsByEndpoint: {},
|
|
148
|
-
lastReset: new Date().toISOString()
|
|
147
|
+
lastReset: new Date().toISOString(),
|
|
149
148
|
};
|
|
150
149
|
}
|
|
151
150
|
|
|
152
151
|
recordError(error, endpoint = null) {
|
|
153
152
|
this.metrics.totalErrors++;
|
|
154
|
-
|
|
153
|
+
|
|
155
154
|
// Count by error type
|
|
156
155
|
const errorType = error.constructor.name;
|
|
157
156
|
this.metrics.errorsByType[errorType] = (this.metrics.errorsByType[errorType] || 0) + 1;
|
|
158
|
-
|
|
157
|
+
|
|
159
158
|
// Count by status code
|
|
160
159
|
const statusCode = error.statusCode || 500;
|
|
161
|
-
this.metrics.errorsByStatusCode[statusCode] =
|
|
162
|
-
|
|
160
|
+
this.metrics.errorsByStatusCode[statusCode] =
|
|
161
|
+
(this.metrics.errorsByStatusCode[statusCode] || 0) + 1;
|
|
162
|
+
|
|
163
163
|
// Count by endpoint
|
|
164
164
|
if (endpoint) {
|
|
165
165
|
this.metrics.errorsByEndpoint[endpoint] = (this.metrics.errorsByEndpoint[endpoint] || 0) + 1;
|
|
@@ -171,7 +171,7 @@ export class ErrorMetrics {
|
|
|
171
171
|
...this.metrics,
|
|
172
172
|
uptime: process.uptime(),
|
|
173
173
|
memoryUsage: process.memoryUsage(),
|
|
174
|
-
timestamp: new Date().toISOString()
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -181,7 +181,7 @@ export class ErrorMetrics {
|
|
|
181
181
|
errorsByType: {},
|
|
182
182
|
errorsByStatusCode: {},
|
|
183
183
|
errorsByEndpoint: {},
|
|
184
|
-
lastReset: new Date().toISOString()
|
|
184
|
+
lastReset: new Date().toISOString(),
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
187
|
}
|
|
@@ -193,13 +193,13 @@ const errorMetrics = new ErrorMetrics();
|
|
|
193
193
|
/**
|
|
194
194
|
* Express Error Middleware
|
|
195
195
|
*/
|
|
196
|
-
export function expressErrorHandler(err, req, res,
|
|
196
|
+
export function expressErrorHandler(err, req, res, _next) {
|
|
197
197
|
// Log the error
|
|
198
198
|
errorLogger.logError(err, {
|
|
199
199
|
method: req.method,
|
|
200
200
|
url: req.url,
|
|
201
201
|
ip: req.ip,
|
|
202
|
-
userAgent: req.get('user-agent')
|
|
202
|
+
userAgent: req.get('user-agent'),
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
// Record metrics
|
|
@@ -207,12 +207,12 @@ export function expressErrorHandler(err, req, res, next) {
|
|
|
207
207
|
|
|
208
208
|
// Format response
|
|
209
209
|
const { statusCode, body } = ErrorHandler.formatHTTPError(err);
|
|
210
|
-
|
|
210
|
+
|
|
211
211
|
// Set security headers
|
|
212
212
|
res.set({
|
|
213
213
|
'X-Content-Type-Options': 'nosniff',
|
|
214
214
|
'X-Frame-Options': 'DENY',
|
|
215
|
-
'X-XSS-Protection': '1; mode=block'
|
|
215
|
+
'X-XSS-Protection': '1; mode=block',
|
|
216
216
|
});
|
|
217
217
|
|
|
218
218
|
// Send response
|
|
@@ -241,13 +241,15 @@ export function validateRequest(schema) {
|
|
|
241
241
|
if (validationResult && validationResult.error) {
|
|
242
242
|
throw new ValidationError(validationResult.error);
|
|
243
243
|
}
|
|
244
|
-
}
|
|
244
|
+
}
|
|
245
245
|
// If schema has a validate method (e.g., Joi schema)
|
|
246
246
|
else if (schema && typeof schema.validate === 'function') {
|
|
247
247
|
const { error } = schema.validate(req.body, { abortEarly: false });
|
|
248
248
|
if (error) {
|
|
249
249
|
// Create ValidationError from Joi error details
|
|
250
|
-
const errors = error.details
|
|
250
|
+
const errors = error.details
|
|
251
|
+
? error.details.map((detail) => detail.message)
|
|
252
|
+
: [error.message];
|
|
251
253
|
throw new ValidationError('Validation failed', errors);
|
|
252
254
|
}
|
|
253
255
|
}
|
|
@@ -266,7 +268,7 @@ export function validateRequest(schema) {
|
|
|
266
268
|
throw new ValidationError('Validation failed', errors);
|
|
267
269
|
}
|
|
268
270
|
}
|
|
269
|
-
|
|
271
|
+
|
|
270
272
|
next();
|
|
271
273
|
} catch (error) {
|
|
272
274
|
next(error);
|
|
@@ -288,33 +290,33 @@ export class RateLimiter {
|
|
|
288
290
|
return (req, res, next) => {
|
|
289
291
|
const key = req.ip;
|
|
290
292
|
const now = Date.now();
|
|
291
|
-
|
|
293
|
+
|
|
292
294
|
// Clean old entries
|
|
293
295
|
this.cleanup(now);
|
|
294
|
-
|
|
296
|
+
|
|
295
297
|
// Get or create request list for this IP
|
|
296
298
|
if (!this.requests.has(key)) {
|
|
297
299
|
this.requests.set(key, []);
|
|
298
300
|
}
|
|
299
|
-
|
|
301
|
+
|
|
300
302
|
const requestTimes = this.requests.get(key);
|
|
301
303
|
requestTimes.push(now);
|
|
302
|
-
|
|
304
|
+
|
|
303
305
|
if (requestTimes.length > this.maxRequests) {
|
|
304
306
|
const retryAfter = Math.ceil((this.windowMs - (now - requestTimes[0])) / 1000);
|
|
305
307
|
return next(new RateLimitError(retryAfter));
|
|
306
308
|
}
|
|
307
|
-
|
|
309
|
+
|
|
308
310
|
next();
|
|
309
311
|
};
|
|
310
312
|
}
|
|
311
313
|
|
|
312
314
|
cleanup(now) {
|
|
313
315
|
const cutoff = now - this.windowMs;
|
|
314
|
-
|
|
316
|
+
|
|
315
317
|
for (const [key, times] of this.requests.entries()) {
|
|
316
|
-
const filtered = times.filter(time => time > cutoff);
|
|
317
|
-
|
|
318
|
+
const filtered = times.filter((time) => time > cutoff);
|
|
319
|
+
|
|
318
320
|
if (filtered.length === 0) {
|
|
319
321
|
this.requests.delete(key);
|
|
320
322
|
} else {
|
|
@@ -329,29 +331,29 @@ export class RateLimiter {
|
|
|
329
331
|
*/
|
|
330
332
|
export function apiKeyAuth(apiKey) {
|
|
331
333
|
return (req, res, next) => {
|
|
332
|
-
const providedKey =
|
|
333
|
-
|
|
334
|
-
|
|
334
|
+
const providedKey =
|
|
335
|
+
req.headers['x-api-key'] || req.headers['authorization']?.replace('Bearer ', '');
|
|
336
|
+
|
|
335
337
|
if (!providedKey) {
|
|
336
338
|
return next(new AuthenticationError('API key is required'));
|
|
337
339
|
}
|
|
338
|
-
|
|
340
|
+
|
|
339
341
|
// Use timing-safe comparison to prevent timing attacks
|
|
340
342
|
const expectedKeyBuffer = Buffer.from(apiKey, 'utf8');
|
|
341
343
|
const providedKeyBuffer = Buffer.from(providedKey, 'utf8');
|
|
342
|
-
|
|
344
|
+
|
|
343
345
|
// Ensure buffers are same length to prevent timing attacks
|
|
344
346
|
if (expectedKeyBuffer.length !== providedKeyBuffer.length) {
|
|
345
347
|
return next(new AuthenticationError('Invalid API key'));
|
|
346
348
|
}
|
|
347
|
-
|
|
349
|
+
|
|
348
350
|
// Use constant-time comparison
|
|
349
351
|
const isValid = crypto.timingSafeEqual(expectedKeyBuffer, providedKeyBuffer);
|
|
350
|
-
|
|
352
|
+
|
|
351
353
|
if (!isValid) {
|
|
352
354
|
return next(new AuthenticationError('Invalid API key'));
|
|
353
355
|
}
|
|
354
|
-
|
|
356
|
+
|
|
355
357
|
next();
|
|
356
358
|
};
|
|
357
359
|
}
|
|
@@ -362,18 +364,18 @@ export function apiKeyAuth(apiKey) {
|
|
|
362
364
|
export function mcpCors(allowedOrigins = ['*']) {
|
|
363
365
|
return (req, res, next) => {
|
|
364
366
|
const origin = req.headers.origin;
|
|
365
|
-
|
|
367
|
+
|
|
366
368
|
if (allowedOrigins.includes('*') || allowedOrigins.includes(origin)) {
|
|
367
369
|
res.header('Access-Control-Allow-Origin', origin || '*');
|
|
368
370
|
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
369
371
|
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
|
|
370
372
|
res.header('Access-Control-Max-Age', '86400');
|
|
371
373
|
}
|
|
372
|
-
|
|
374
|
+
|
|
373
375
|
if (req.method === 'OPTIONS') {
|
|
374
376
|
return res.sendStatus(204);
|
|
375
377
|
}
|
|
376
|
-
|
|
378
|
+
|
|
377
379
|
next();
|
|
378
380
|
};
|
|
379
381
|
}
|
|
@@ -386,22 +388,22 @@ export function healthCheck(ghostService) {
|
|
|
386
388
|
try {
|
|
387
389
|
const health = await ghostService.checkHealth();
|
|
388
390
|
const metrics = errorMetrics.getMetrics();
|
|
389
|
-
|
|
391
|
+
|
|
390
392
|
const status = health.status === 'healthy' ? 200 : 503;
|
|
391
|
-
|
|
393
|
+
|
|
392
394
|
res.status(status).json({
|
|
393
395
|
...health,
|
|
394
396
|
metrics: {
|
|
395
397
|
errors: metrics.totalErrors,
|
|
396
398
|
uptime: metrics.uptime,
|
|
397
|
-
memory: metrics.memoryUsage
|
|
398
|
-
}
|
|
399
|
+
memory: metrics.memoryUsage,
|
|
400
|
+
},
|
|
399
401
|
});
|
|
400
402
|
} catch (error) {
|
|
401
403
|
res.status(503).json({
|
|
402
404
|
status: 'unhealthy',
|
|
403
405
|
error: error.message,
|
|
404
|
-
timestamp: new Date().toISOString()
|
|
406
|
+
timestamp: new Date().toISOString(),
|
|
405
407
|
});
|
|
406
408
|
}
|
|
407
409
|
};
|
|
@@ -428,12 +430,12 @@ export class GracefulShutdown {
|
|
|
428
430
|
res.status(503).json({
|
|
429
431
|
error: {
|
|
430
432
|
code: 'SERVER_SHUTTING_DOWN',
|
|
431
|
-
message: 'Server is shutting down'
|
|
432
|
-
}
|
|
433
|
+
message: 'Server is shutting down',
|
|
434
|
+
},
|
|
433
435
|
});
|
|
434
436
|
return;
|
|
435
437
|
}
|
|
436
|
-
|
|
438
|
+
|
|
437
439
|
// Track the connection
|
|
438
440
|
this.trackConnection(req.socket);
|
|
439
441
|
next();
|
|
@@ -442,36 +444,33 @@ export class GracefulShutdown {
|
|
|
442
444
|
|
|
443
445
|
async shutdown(server) {
|
|
444
446
|
if (this.isShuttingDown) return;
|
|
445
|
-
|
|
447
|
+
|
|
446
448
|
this.isShuttingDown = true;
|
|
447
449
|
console.log('Graceful shutdown initiated...');
|
|
448
|
-
|
|
450
|
+
|
|
449
451
|
// Stop accepting new connections
|
|
450
452
|
server.close(() => {
|
|
451
453
|
console.log('Server closed to new connections');
|
|
452
454
|
});
|
|
453
|
-
|
|
455
|
+
|
|
454
456
|
// Close existing connections
|
|
455
457
|
for (const connection of this.connections) {
|
|
456
458
|
connection.end();
|
|
457
459
|
}
|
|
458
|
-
|
|
460
|
+
|
|
459
461
|
// Force close after timeout
|
|
460
462
|
setTimeout(() => {
|
|
461
463
|
for (const connection of this.connections) {
|
|
462
464
|
connection.destroy();
|
|
463
465
|
}
|
|
464
466
|
}, 10000);
|
|
465
|
-
|
|
467
|
+
|
|
466
468
|
// Log final metrics
|
|
467
469
|
await errorLogger.logInfo('Shutdown metrics', errorMetrics.getMetrics());
|
|
468
470
|
}
|
|
469
471
|
}
|
|
470
472
|
|
|
471
|
-
export {
|
|
472
|
-
errorLogger,
|
|
473
|
-
errorMetrics
|
|
474
|
-
};
|
|
473
|
+
export { errorLogger, errorMetrics };
|
|
475
474
|
|
|
476
475
|
export default {
|
|
477
476
|
expressErrorHandler,
|
|
@@ -485,5 +484,5 @@ export default {
|
|
|
485
484
|
ErrorLogger,
|
|
486
485
|
ErrorMetrics,
|
|
487
486
|
errorLogger,
|
|
488
|
-
errorMetrics
|
|
489
|
-
};
|
|
487
|
+
errorMetrics,
|
|
488
|
+
};
|