@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.
@@ -14,7 +14,7 @@ export class BaseError extends Error {
14
14
  this.code = code;
15
15
  this.isOperational = isOperational;
16
16
  this.timestamp = new Date().toISOString();
17
-
17
+
18
18
  // Capture stack trace
19
19
  Error.captureStackTrace(this, this.constructor);
20
20
  }
@@ -26,7 +26,7 @@ export class BaseError extends Error {
26
26
  code: this.code,
27
27
  statusCode: this.statusCode,
28
28
  timestamp: this.timestamp,
29
- ...(process.env.NODE_ENV === 'development' && { stack: this.stack })
29
+ ...(process.env.NODE_ENV === 'development' && { stack: this.stack }),
30
30
  };
31
31
  }
32
32
  }
@@ -41,10 +41,10 @@ export class ValidationError extends BaseError {
41
41
  }
42
42
 
43
43
  static fromJoi(joiError) {
44
- const errors = joiError.details.map(detail => ({
44
+ const errors = joiError.details.map((detail) => ({
45
45
  field: detail.path.join('.'),
46
46
  message: detail.message,
47
- type: detail.type
47
+ type: detail.type,
48
48
  }));
49
49
  return new ValidationError('Validation failed', errors);
50
50
  }
@@ -118,7 +118,7 @@ export class GhostAPIError extends ExternalServiceError {
118
118
  super('Ghost API', originalError);
119
119
  this.operation = operation;
120
120
  this.ghostStatusCode = statusCode;
121
-
121
+
122
122
  // Map Ghost API status codes to our error types
123
123
  if (statusCode === 401) {
124
124
  this.statusCode = 401;
@@ -156,7 +156,7 @@ export class ToolExecutionError extends BaseError {
156
156
  this.toolName = toolName;
157
157
  this.originalError = originalError?.message || originalError;
158
158
  this.input = input;
159
-
159
+
160
160
  // Don't expose sensitive data in production
161
161
  if (process.env.NODE_ENV === 'production') {
162
162
  delete this.input.apiKey;
@@ -214,8 +214,8 @@ export class ErrorHandler {
214
214
  ...(toolName && { tool: toolName }),
215
215
  ...(error.errors && { validationErrors: error.errors }),
216
216
  ...(error.retryAfter && { retryAfter: error.retryAfter }),
217
- timestamp: error.timestamp
218
- }
217
+ timestamp: error.timestamp,
218
+ },
219
219
  };
220
220
  }
221
221
 
@@ -223,13 +223,12 @@ export class ErrorHandler {
223
223
  return {
224
224
  error: {
225
225
  code: 'UNKNOWN_ERROR',
226
- message: process.env.NODE_ENV === 'production'
227
- ? 'An unexpected error occurred'
228
- : error.message,
226
+ message:
227
+ process.env.NODE_ENV === 'production' ? 'An unexpected error occurred' : error.message,
229
228
  statusCode: 500,
230
229
  ...(toolName && { tool: toolName }),
231
- timestamp: new Date().toISOString()
232
- }
230
+ timestamp: new Date().toISOString(),
231
+ },
233
232
  };
234
233
  }
235
234
 
@@ -244,13 +243,13 @@ export class ErrorHandler {
244
243
  message: error.message,
245
244
  ...(error.errors && { errors: error.errors }),
246
245
  ...(error.retryAfter && { retryAfter: error.retryAfter }),
247
- ...(error.resource && { resource: error.resource })
248
- }
246
+ ...(error.resource && { resource: error.resource }),
247
+ },
249
248
  };
250
-
249
+
251
250
  return {
252
251
  statusCode: error.statusCode,
253
- body: response
252
+ body: response,
254
253
  };
255
254
  }
256
255
 
@@ -260,11 +259,10 @@ export class ErrorHandler {
260
259
  body: {
261
260
  error: {
262
261
  code: 'INTERNAL_ERROR',
263
- message: process.env.NODE_ENV === 'production'
264
- ? 'An internal error occurred'
265
- : error.message
266
- }
267
- }
262
+ message:
263
+ process.env.NODE_ENV === 'production' ? 'An internal error occurred' : error.message,
264
+ },
265
+ },
268
266
  };
269
267
  }
270
268
 
@@ -291,7 +289,7 @@ export class ErrorHandler {
291
289
  static fromGhostError(error, operation) {
292
290
  const statusCode = error.response?.status || error.statusCode;
293
291
  const message = error.response?.data?.errors?.[0]?.message || error.message;
294
-
292
+
295
293
  return new GhostAPIError(operation, message, statusCode);
296
294
  }
297
295
 
@@ -304,14 +302,16 @@ export class ErrorHandler {
304
302
  if (error instanceof GhostAPIError) {
305
303
  return [429, 502, 503, 504].includes(error.ghostStatusCode);
306
304
  }
307
-
305
+
308
306
  // Network errors
309
- if (error.code === 'ECONNREFUSED' ||
310
- error.code === 'ETIMEDOUT' ||
311
- error.code === 'ECONNRESET') {
307
+ if (
308
+ error.code === 'ECONNREFUSED' ||
309
+ error.code === 'ETIMEDOUT' ||
310
+ error.code === 'ECONNRESET'
311
+ ) {
312
312
  return true;
313
313
  }
314
-
314
+
315
315
  return false;
316
316
  }
317
317
 
@@ -322,15 +322,15 @@ export class ErrorHandler {
322
322
  if (error instanceof RateLimitError) {
323
323
  return error.retryAfter * 1000; // Convert to milliseconds
324
324
  }
325
-
325
+
326
326
  // Exponential backoff: 1s, 2s, 4s, 8s...
327
327
  const baseDelay = 1000;
328
328
  const maxDelay = 30000;
329
329
  const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
330
-
330
+
331
331
  // Add jitter to prevent thundering herd
332
332
  const jitter = Math.random() * 0.3 * delay;
333
-
333
+
334
334
  return Math.round(delay + jitter);
335
335
  }
336
336
  }
@@ -343,7 +343,7 @@ export class CircuitBreaker {
343
343
  this.failureThreshold = options.failureThreshold || 5;
344
344
  this.resetTimeout = options.resetTimeout || 60000; // 1 minute
345
345
  this.monitoringPeriod = options.monitoringPeriod || 10000; // 10 seconds
346
-
346
+
347
347
  this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
348
348
  this.failureCount = 0;
349
349
  this.lastFailureTime = null;
@@ -353,7 +353,10 @@ export class CircuitBreaker {
353
353
  async execute(fn, ...args) {
354
354
  if (this.state === 'OPEN') {
355
355
  if (Date.now() < this.nextAttempt) {
356
- throw new ExternalServiceError('Circuit breaker is OPEN', 'Service temporarily unavailable');
356
+ throw new ExternalServiceError(
357
+ 'Circuit breaker is OPEN',
358
+ 'Service temporarily unavailable'
359
+ );
357
360
  }
358
361
  this.state = 'HALF_OPEN';
359
362
  }
@@ -383,7 +386,9 @@ export class CircuitBreaker {
383
386
  if (this.failureCount >= this.failureThreshold) {
384
387
  this.state = 'OPEN';
385
388
  this.nextAttempt = Date.now() + this.resetTimeout;
386
- console.error(`Circuit breaker opened. Will retry at ${new Date(this.nextAttempt).toISOString()}`);
389
+ console.error(
390
+ `Circuit breaker opened. Will retry at ${new Date(this.nextAttempt).toISOString()}`
391
+ );
387
392
  }
388
393
  }
389
394
 
@@ -392,7 +397,7 @@ export class CircuitBreaker {
392
397
  state: this.state,
393
398
  failureCount: this.failureCount,
394
399
  lastFailureTime: this.lastFailureTime,
395
- nextAttempt: this.nextAttempt
400
+ nextAttempt: this.nextAttempt,
396
401
  };
397
402
  }
398
403
  }
@@ -403,27 +408,27 @@ export class CircuitBreaker {
403
408
  export async function retryWithBackoff(fn, options = {}) {
404
409
  const maxAttempts = options.maxAttempts || 3;
405
410
  const onRetry = options.onRetry || (() => {});
406
-
411
+
407
412
  let lastError;
408
-
413
+
409
414
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
410
415
  try {
411
416
  return await fn();
412
417
  } catch (error) {
413
418
  lastError = error;
414
-
419
+
415
420
  if (attempt === maxAttempts || !ErrorHandler.isRetryable(error)) {
416
421
  throw error;
417
422
  }
418
-
423
+
419
424
  const delay = ErrorHandler.getRetryDelay(attempt, error);
420
425
  console.log(`Retry attempt ${attempt}/${maxAttempts} after ${delay}ms`);
421
-
422
- await new Promise(resolve => setTimeout(resolve, delay));
426
+
427
+ await new Promise((resolve) => setTimeout(resolve, delay));
423
428
  onRetry(attempt, error);
424
429
  }
425
430
  }
426
-
431
+
427
432
  throw lastError;
428
433
  }
429
434
 
@@ -443,5 +448,5 @@ export default {
443
448
  ConfigurationError,
444
449
  ErrorHandler,
445
450
  CircuitBreaker,
446
- retryWithBackoff
447
- };
451
+ retryWithBackoff,
452
+ };
package/src/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import express from "express";
3
- import helmet from "helmet";
4
- import dotenv from "dotenv";
5
- import postRoutes from "./routes/postRoutes.js"; // Import post routes
6
- import imageRoutes from "./routes/imageRoutes.js"; // Import image routes
7
- import tagRoutes from "./routes/tagRoutes.js"; // Import tag routes
8
- import { startMCPServer } from "./mcp_server.js"; // Import MCP server start function
9
- import { createContextLogger } from "./utils/logger.js";
2
+ import express from 'express';
3
+ import helmet from 'helmet';
4
+ import dotenv from 'dotenv';
5
+ import postRoutes from './routes/postRoutes.js'; // Import post routes
6
+ import imageRoutes from './routes/imageRoutes.js'; // Import image routes
7
+ import tagRoutes from './routes/tagRoutes.js'; // Import tag routes
8
+ import { startMCPServer } from './mcp_server.js'; // Import MCP server start function
9
+ import { createContextLogger } from './utils/logger.js';
10
10
 
11
11
  // Load environment variables from .env file
12
12
  dotenv.config();
@@ -19,69 +19,75 @@ const restApiPort = process.env.PORT || 3000;
19
19
  const mcpPort = process.env.MCP_PORT || 3001; // Allow configuring MCP port
20
20
 
21
21
  // Apply security headers with Helmet (OWASP recommended)
22
- app.use(helmet({
23
- contentSecurityPolicy: {
24
- directives: {
25
- defaultSrc: ["'self'"],
26
- styleSrc: ["'self'", "'unsafe-inline'"],
27
- scriptSrc: ["'self'"],
28
- imgSrc: ["'self'", "data:", "https:"],
29
- connectSrc: ["'self'"],
30
- fontSrc: ["'self'"],
31
- objectSrc: ["'none'"],
32
- mediaSrc: ["'self'"],
33
- frameSrc: ["'none'"],
22
+ app.use(
23
+ helmet({
24
+ contentSecurityPolicy: {
25
+ directives: {
26
+ defaultSrc: ["'self'"],
27
+ styleSrc: ["'self'", "'unsafe-inline'"],
28
+ scriptSrc: ["'self'"],
29
+ imgSrc: ["'self'", 'data:', 'https:'],
30
+ connectSrc: ["'self'"],
31
+ fontSrc: ["'self'"],
32
+ objectSrc: ["'none'"],
33
+ mediaSrc: ["'self'"],
34
+ frameSrc: ["'none'"],
35
+ },
34
36
  },
35
- },
36
- hsts: {
37
- maxAge: 31536000,
38
- includeSubDomains: true,
39
- preload: true
40
- }
41
- }))
37
+ hsts: {
38
+ maxAge: 31536000,
39
+ includeSubDomains: true,
40
+ preload: true,
41
+ },
42
+ })
43
+ );
42
44
 
43
45
  // Middleware to parse JSON bodies with size limits
44
- app.use(express.json({
45
- limit: '1mb',
46
- strict: true,
47
- type: 'application/json'
48
- }));
46
+ app.use(
47
+ express.json({
48
+ limit: '1mb',
49
+ strict: true,
50
+ type: 'application/json',
51
+ })
52
+ );
49
53
  // Middleware to parse URL-encoded bodies with size limits
50
- app.use(express.urlencoded({
51
- extended: true,
52
- limit: '1mb',
53
- parameterLimit: 100
54
- }));
54
+ app.use(
55
+ express.urlencoded({
56
+ extended: true,
57
+ limit: '1mb',
58
+ parameterLimit: 100,
59
+ })
60
+ );
55
61
 
56
62
  // Health check endpoint
57
- app.get("/health", (req, res) => {
58
- res.status(200).json({ status: "ok", message: "Server is running" });
63
+ app.get('/health', (req, res) => {
64
+ res.status(200).json({ status: 'ok', message: 'Server is running' });
59
65
  });
60
66
 
61
67
  // Mount the post routes
62
- app.use("/api/posts", postRoutes); // All post routes will be prefixed with /api/posts
68
+ app.use('/api/posts', postRoutes); // All post routes will be prefixed with /api/posts
63
69
  // Mount the image routes
64
- app.use("/api/images", imageRoutes);
70
+ app.use('/api/images', imageRoutes);
65
71
  // Mount the tag routes
66
- app.use("/api/tags", tagRoutes);
72
+ app.use('/api/tags', tagRoutes);
67
73
 
68
74
  // Global error handler for Express
69
- app.use((err, req, res, next) => {
75
+ app.use((err, req, res, _next) => {
70
76
  const statusCode = err.statusCode || err.response?.status || 500;
71
-
77
+
72
78
  logger.error('Express error handler triggered', {
73
79
  error: err.message,
74
80
  statusCode,
75
81
  stack: err.stack,
76
82
  url: req.url,
77
83
  method: req.method,
78
- type: 'express_error'
84
+ type: 'express_error',
79
85
  });
80
-
86
+
81
87
  res.status(statusCode).json({
82
- message: err.message || "Internal Server Error",
88
+ message: err.message || 'Internal Server Error',
83
89
  // Optionally include stack trace in development
84
- stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
90
+ stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
85
91
  });
86
92
  });
87
93
 
@@ -92,7 +98,7 @@ const startServers = async () => {
92
98
  logger.info('Express REST API server started', {
93
99
  port: restApiPort,
94
100
  url: `http://localhost:${restApiPort}`,
95
- type: 'server_start'
101
+ type: 'server_start',
96
102
  });
97
103
  });
98
104
 
@@ -104,7 +110,7 @@ startServers().catch((error) => {
104
110
  logger.error('Failed to start servers', {
105
111
  error: error.message,
106
112
  stack: error.stack,
107
- type: 'server_start_error'
113
+ type: 'server_start_error',
108
114
  });
109
115
  process.exit(1);
110
116
  });