@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,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTags as getGhostTags,
|
|
3
|
+
createTag as createGhostTag,
|
|
4
|
+
} from "../services/ghostService.js";
|
|
5
|
+
import { createContextLogger } from "../utils/logger.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Controller to handle fetching tags.
|
|
9
|
+
* Can optionally filter by tag name via query parameter.
|
|
10
|
+
*/
|
|
11
|
+
const getTags = async (req, res, next) => {
|
|
12
|
+
const logger = createContextLogger('tag-controller');
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const { name } = req.query; // Get name from query params like /api/tags?name=some-tag
|
|
16
|
+
logger.info('Fetching tags', {
|
|
17
|
+
filtered: !!name,
|
|
18
|
+
filterName: name
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const tags = await getGhostTags(name);
|
|
22
|
+
|
|
23
|
+
logger.info('Tags retrieved successfully', {
|
|
24
|
+
count: tags.length,
|
|
25
|
+
filtered: !!name
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
res.status(200).json(tags);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
logger.error('Get tags failed', {
|
|
31
|
+
error: error.message,
|
|
32
|
+
filterName: req.query?.name
|
|
33
|
+
});
|
|
34
|
+
next(error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Controller to handle creating a new tag.
|
|
40
|
+
*/
|
|
41
|
+
const createTag = async (req, res, next) => {
|
|
42
|
+
const logger = createContextLogger('tag-controller');
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Basic validation (more could be added via express-validator)
|
|
46
|
+
const { name, description, slug, ...otherData } = req.body;
|
|
47
|
+
if (!name) {
|
|
48
|
+
logger.warn('Tag creation attempted without name');
|
|
49
|
+
return res.status(400).json({ message: "Tag name is required." });
|
|
50
|
+
}
|
|
51
|
+
const tagData = { name, description, slug, ...otherData };
|
|
52
|
+
|
|
53
|
+
logger.info('Creating tag', {
|
|
54
|
+
name,
|
|
55
|
+
hasDescription: !!description,
|
|
56
|
+
hasSlug: !!slug
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const newTag = await createGhostTag(tagData);
|
|
60
|
+
|
|
61
|
+
logger.info('Tag created successfully', {
|
|
62
|
+
tagId: newTag.id,
|
|
63
|
+
name: newTag.name,
|
|
64
|
+
slug: newTag.slug
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
res.status(201).json(newTag);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
logger.error('Tag creation failed', {
|
|
70
|
+
error: error.message,
|
|
71
|
+
tagName: req.body?.name
|
|
72
|
+
});
|
|
73
|
+
next(error);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Add controllers for other CRUD operations (getTagById, updateTag, deleteTag) if needed later
|
|
78
|
+
|
|
79
|
+
export { getTags, createTag };
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive Error Handling System for MCP Server
|
|
3
|
+
* Following best practices for error handling in Node.js applications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base error class with structured error information
|
|
8
|
+
*/
|
|
9
|
+
export class BaseError extends Error {
|
|
10
|
+
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR', isOperational = true) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = this.constructor.name;
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.isOperational = isOperational;
|
|
16
|
+
this.timestamp = new Date().toISOString();
|
|
17
|
+
|
|
18
|
+
// Capture stack trace
|
|
19
|
+
Error.captureStackTrace(this, this.constructor);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toJSON() {
|
|
23
|
+
return {
|
|
24
|
+
name: this.name,
|
|
25
|
+
message: this.message,
|
|
26
|
+
code: this.code,
|
|
27
|
+
statusCode: this.statusCode,
|
|
28
|
+
timestamp: this.timestamp,
|
|
29
|
+
...(process.env.NODE_ENV === 'development' && { stack: this.stack })
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validation Error - 400
|
|
36
|
+
*/
|
|
37
|
+
export class ValidationError extends BaseError {
|
|
38
|
+
constructor(message, errors = []) {
|
|
39
|
+
super(message, 400, 'VALIDATION_ERROR');
|
|
40
|
+
this.errors = errors;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static fromJoi(joiError) {
|
|
44
|
+
const errors = joiError.details.map(detail => ({
|
|
45
|
+
field: detail.path.join('.'),
|
|
46
|
+
message: detail.message,
|
|
47
|
+
type: detail.type
|
|
48
|
+
}));
|
|
49
|
+
return new ValidationError('Validation failed', errors);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Authentication Error - 401
|
|
55
|
+
*/
|
|
56
|
+
export class AuthenticationError extends BaseError {
|
|
57
|
+
constructor(message = 'Authentication failed') {
|
|
58
|
+
super(message, 401, 'AUTHENTICATION_ERROR');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Authorization Error - 403
|
|
64
|
+
*/
|
|
65
|
+
export class AuthorizationError extends BaseError {
|
|
66
|
+
constructor(message = 'Access denied') {
|
|
67
|
+
super(message, 403, 'AUTHORIZATION_ERROR');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Not Found Error - 404
|
|
73
|
+
*/
|
|
74
|
+
export class NotFoundError extends BaseError {
|
|
75
|
+
constructor(resource, identifier) {
|
|
76
|
+
super(`${resource} not found: ${identifier}`, 404, 'NOT_FOUND');
|
|
77
|
+
this.resource = resource;
|
|
78
|
+
this.identifier = identifier;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Conflict Error - 409
|
|
84
|
+
*/
|
|
85
|
+
export class ConflictError extends BaseError {
|
|
86
|
+
constructor(message, resource) {
|
|
87
|
+
super(message, 409, 'CONFLICT');
|
|
88
|
+
this.resource = resource;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Rate Limit Error - 429
|
|
94
|
+
*/
|
|
95
|
+
export class RateLimitError extends BaseError {
|
|
96
|
+
constructor(retryAfter = 60) {
|
|
97
|
+
super('Rate limit exceeded', 429, 'RATE_LIMIT_EXCEEDED');
|
|
98
|
+
this.retryAfter = retryAfter;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* External Service Error - 502
|
|
104
|
+
*/
|
|
105
|
+
export class ExternalServiceError extends BaseError {
|
|
106
|
+
constructor(service, originalError) {
|
|
107
|
+
super(`External service error: ${service}`, 502, 'EXTERNAL_SERVICE_ERROR');
|
|
108
|
+
this.service = service;
|
|
109
|
+
this.originalError = originalError?.message || originalError;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Ghost API specific errors
|
|
115
|
+
*/
|
|
116
|
+
export class GhostAPIError extends ExternalServiceError {
|
|
117
|
+
constructor(operation, originalError, statusCode) {
|
|
118
|
+
super('Ghost API', originalError);
|
|
119
|
+
this.operation = operation;
|
|
120
|
+
this.ghostStatusCode = statusCode;
|
|
121
|
+
|
|
122
|
+
// Map Ghost API status codes to our error types
|
|
123
|
+
if (statusCode === 401) {
|
|
124
|
+
this.statusCode = 401;
|
|
125
|
+
this.code = 'GHOST_AUTH_ERROR';
|
|
126
|
+
} else if (statusCode === 404) {
|
|
127
|
+
this.statusCode = 404;
|
|
128
|
+
this.code = 'GHOST_NOT_FOUND';
|
|
129
|
+
} else if (statusCode === 422) {
|
|
130
|
+
this.statusCode = 400;
|
|
131
|
+
this.code = 'GHOST_VALIDATION_ERROR';
|
|
132
|
+
} else if (statusCode === 429) {
|
|
133
|
+
this.statusCode = 429;
|
|
134
|
+
this.code = 'GHOST_RATE_LIMIT';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* MCP Protocol Error
|
|
141
|
+
*/
|
|
142
|
+
export class MCPProtocolError extends BaseError {
|
|
143
|
+
constructor(message, details = {}) {
|
|
144
|
+
super(message, 400, 'MCP_PROTOCOL_ERROR');
|
|
145
|
+
this.details = details;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Tool Execution Error
|
|
151
|
+
*/
|
|
152
|
+
export class ToolExecutionError extends BaseError {
|
|
153
|
+
constructor(toolName, originalError, input = {}) {
|
|
154
|
+
const message = `Tool execution failed: ${toolName}`;
|
|
155
|
+
super(message, 500, 'TOOL_EXECUTION_ERROR');
|
|
156
|
+
this.toolName = toolName;
|
|
157
|
+
this.originalError = originalError?.message || originalError;
|
|
158
|
+
this.input = input;
|
|
159
|
+
|
|
160
|
+
// Don't expose sensitive data in production
|
|
161
|
+
if (process.env.NODE_ENV === 'production') {
|
|
162
|
+
delete this.input.apiKey;
|
|
163
|
+
delete this.input.password;
|
|
164
|
+
delete this.input.token;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Image Processing Error
|
|
171
|
+
*/
|
|
172
|
+
export class ImageProcessingError extends BaseError {
|
|
173
|
+
constructor(operation, originalError) {
|
|
174
|
+
super(`Image processing failed: ${operation}`, 422, 'IMAGE_PROCESSING_ERROR');
|
|
175
|
+
this.operation = operation;
|
|
176
|
+
this.originalError = originalError?.message || originalError;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Configuration Error
|
|
182
|
+
*/
|
|
183
|
+
export class ConfigurationError extends BaseError {
|
|
184
|
+
constructor(message, missingFields = []) {
|
|
185
|
+
super(message, 500, 'CONFIGURATION_ERROR', false);
|
|
186
|
+
this.missingFields = missingFields;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Error handler utility functions
|
|
192
|
+
*/
|
|
193
|
+
export class ErrorHandler {
|
|
194
|
+
/**
|
|
195
|
+
* Determine if error is operational (expected) or programming error
|
|
196
|
+
*/
|
|
197
|
+
static isOperationalError(error) {
|
|
198
|
+
if (error instanceof BaseError) {
|
|
199
|
+
return error.isOperational;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Format error for MCP response
|
|
206
|
+
*/
|
|
207
|
+
static formatMCPError(error, toolName = null) {
|
|
208
|
+
if (error instanceof BaseError) {
|
|
209
|
+
return {
|
|
210
|
+
error: {
|
|
211
|
+
code: error.code,
|
|
212
|
+
message: error.message,
|
|
213
|
+
statusCode: error.statusCode,
|
|
214
|
+
...(toolName && { tool: toolName }),
|
|
215
|
+
...(error.errors && { validationErrors: error.errors }),
|
|
216
|
+
...(error.retryAfter && { retryAfter: error.retryAfter }),
|
|
217
|
+
timestamp: error.timestamp
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Unknown error - be careful not to leak sensitive info
|
|
223
|
+
return {
|
|
224
|
+
error: {
|
|
225
|
+
code: 'UNKNOWN_ERROR',
|
|
226
|
+
message: process.env.NODE_ENV === 'production'
|
|
227
|
+
? 'An unexpected error occurred'
|
|
228
|
+
: error.message,
|
|
229
|
+
statusCode: 500,
|
|
230
|
+
...(toolName && { tool: toolName }),
|
|
231
|
+
timestamp: new Date().toISOString()
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Format error for HTTP response
|
|
238
|
+
*/
|
|
239
|
+
static formatHTTPError(error) {
|
|
240
|
+
if (error instanceof BaseError) {
|
|
241
|
+
const response = {
|
|
242
|
+
error: {
|
|
243
|
+
code: error.code,
|
|
244
|
+
message: error.message,
|
|
245
|
+
...(error.errors && { errors: error.errors }),
|
|
246
|
+
...(error.retryAfter && { retryAfter: error.retryAfter }),
|
|
247
|
+
...(error.resource && { resource: error.resource })
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
statusCode: error.statusCode,
|
|
253
|
+
body: response
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Unknown error
|
|
258
|
+
return {
|
|
259
|
+
statusCode: 500,
|
|
260
|
+
body: {
|
|
261
|
+
error: {
|
|
262
|
+
code: 'INTERNAL_ERROR',
|
|
263
|
+
message: process.env.NODE_ENV === 'production'
|
|
264
|
+
? 'An internal error occurred'
|
|
265
|
+
: error.message
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Wrap async functions with error handling
|
|
273
|
+
*/
|
|
274
|
+
static asyncWrapper(fn) {
|
|
275
|
+
return async (...args) => {
|
|
276
|
+
try {
|
|
277
|
+
return await fn(...args);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (!ErrorHandler.isOperationalError(error)) {
|
|
280
|
+
// Log programming errors
|
|
281
|
+
console.error('Unexpected error:', error);
|
|
282
|
+
}
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Create error from Ghost API response
|
|
290
|
+
*/
|
|
291
|
+
static fromGhostError(error, operation) {
|
|
292
|
+
const statusCode = error.response?.status || error.statusCode;
|
|
293
|
+
const message = error.response?.data?.errors?.[0]?.message || error.message;
|
|
294
|
+
|
|
295
|
+
return new GhostAPIError(operation, message, statusCode);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check if error is retryable
|
|
300
|
+
*/
|
|
301
|
+
static isRetryable(error) {
|
|
302
|
+
if (error instanceof RateLimitError) return true;
|
|
303
|
+
if (error instanceof ExternalServiceError) return true;
|
|
304
|
+
if (error instanceof GhostAPIError) {
|
|
305
|
+
return [429, 502, 503, 504].includes(error.ghostStatusCode);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Network errors
|
|
309
|
+
if (error.code === 'ECONNREFUSED' ||
|
|
310
|
+
error.code === 'ETIMEDOUT' ||
|
|
311
|
+
error.code === 'ECONNRESET') {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Calculate retry delay with exponential backoff
|
|
320
|
+
*/
|
|
321
|
+
static getRetryDelay(attempt, error) {
|
|
322
|
+
if (error instanceof RateLimitError) {
|
|
323
|
+
return error.retryAfter * 1000; // Convert to milliseconds
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Exponential backoff: 1s, 2s, 4s, 8s...
|
|
327
|
+
const baseDelay = 1000;
|
|
328
|
+
const maxDelay = 30000;
|
|
329
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
330
|
+
|
|
331
|
+
// Add jitter to prevent thundering herd
|
|
332
|
+
const jitter = Math.random() * 0.3 * delay;
|
|
333
|
+
|
|
334
|
+
return Math.round(delay + jitter);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Circuit Breaker for external services
|
|
340
|
+
*/
|
|
341
|
+
export class CircuitBreaker {
|
|
342
|
+
constructor(options = {}) {
|
|
343
|
+
this.failureThreshold = options.failureThreshold || 5;
|
|
344
|
+
this.resetTimeout = options.resetTimeout || 60000; // 1 minute
|
|
345
|
+
this.monitoringPeriod = options.monitoringPeriod || 10000; // 10 seconds
|
|
346
|
+
|
|
347
|
+
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
|
|
348
|
+
this.failureCount = 0;
|
|
349
|
+
this.lastFailureTime = null;
|
|
350
|
+
this.nextAttempt = null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async execute(fn, ...args) {
|
|
354
|
+
if (this.state === 'OPEN') {
|
|
355
|
+
if (Date.now() < this.nextAttempt) {
|
|
356
|
+
throw new ExternalServiceError('Circuit breaker is OPEN', 'Service temporarily unavailable');
|
|
357
|
+
}
|
|
358
|
+
this.state = 'HALF_OPEN';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const result = await fn(...args);
|
|
363
|
+
this.onSuccess();
|
|
364
|
+
return result;
|
|
365
|
+
} catch (error) {
|
|
366
|
+
this.onFailure();
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
onSuccess() {
|
|
372
|
+
if (this.state === 'HALF_OPEN') {
|
|
373
|
+
this.state = 'CLOSED';
|
|
374
|
+
}
|
|
375
|
+
this.failureCount = 0;
|
|
376
|
+
this.lastFailureTime = null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
onFailure() {
|
|
380
|
+
this.failureCount++;
|
|
381
|
+
this.lastFailureTime = Date.now();
|
|
382
|
+
|
|
383
|
+
if (this.failureCount >= this.failureThreshold) {
|
|
384
|
+
this.state = 'OPEN';
|
|
385
|
+
this.nextAttempt = Date.now() + this.resetTimeout;
|
|
386
|
+
console.error(`Circuit breaker opened. Will retry at ${new Date(this.nextAttempt).toISOString()}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
getState() {
|
|
391
|
+
return {
|
|
392
|
+
state: this.state,
|
|
393
|
+
failureCount: this.failureCount,
|
|
394
|
+
lastFailureTime: this.lastFailureTime,
|
|
395
|
+
nextAttempt: this.nextAttempt
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Retry mechanism with exponential backoff
|
|
402
|
+
*/
|
|
403
|
+
export async function retryWithBackoff(fn, options = {}) {
|
|
404
|
+
const maxAttempts = options.maxAttempts || 3;
|
|
405
|
+
const onRetry = options.onRetry || (() => {});
|
|
406
|
+
|
|
407
|
+
let lastError;
|
|
408
|
+
|
|
409
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
410
|
+
try {
|
|
411
|
+
return await fn();
|
|
412
|
+
} catch (error) {
|
|
413
|
+
lastError = error;
|
|
414
|
+
|
|
415
|
+
if (attempt === maxAttempts || !ErrorHandler.isRetryable(error)) {
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const delay = ErrorHandler.getRetryDelay(attempt, error);
|
|
420
|
+
console.log(`Retry attempt ${attempt}/${maxAttempts} after ${delay}ms`);
|
|
421
|
+
|
|
422
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
423
|
+
onRetry(attempt, error);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
throw lastError;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export default {
|
|
431
|
+
BaseError,
|
|
432
|
+
ValidationError,
|
|
433
|
+
AuthenticationError,
|
|
434
|
+
AuthorizationError,
|
|
435
|
+
NotFoundError,
|
|
436
|
+
ConflictError,
|
|
437
|
+
RateLimitError,
|
|
438
|
+
ExternalServiceError,
|
|
439
|
+
GhostAPIError,
|
|
440
|
+
MCPProtocolError,
|
|
441
|
+
ToolExecutionError,
|
|
442
|
+
ImageProcessingError,
|
|
443
|
+
ConfigurationError,
|
|
444
|
+
ErrorHandler,
|
|
445
|
+
CircuitBreaker,
|
|
446
|
+
retryWithBackoff
|
|
447
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
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";
|
|
10
|
+
|
|
11
|
+
// Load environment variables from .env file
|
|
12
|
+
dotenv.config();
|
|
13
|
+
|
|
14
|
+
// Initialize logger for main server
|
|
15
|
+
const logger = createContextLogger('main');
|
|
16
|
+
|
|
17
|
+
const app = express();
|
|
18
|
+
const restApiPort = process.env.PORT || 3000;
|
|
19
|
+
const mcpPort = process.env.MCP_PORT || 3001; // Allow configuring MCP port
|
|
20
|
+
|
|
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'"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
hsts: {
|
|
37
|
+
maxAge: 31536000,
|
|
38
|
+
includeSubDomains: true,
|
|
39
|
+
preload: true
|
|
40
|
+
}
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
// Middleware to parse JSON bodies with size limits
|
|
44
|
+
app.use(express.json({
|
|
45
|
+
limit: '1mb',
|
|
46
|
+
strict: true,
|
|
47
|
+
type: 'application/json'
|
|
48
|
+
}));
|
|
49
|
+
// Middleware to parse URL-encoded bodies with size limits
|
|
50
|
+
app.use(express.urlencoded({
|
|
51
|
+
extended: true,
|
|
52
|
+
limit: '1mb',
|
|
53
|
+
parameterLimit: 100
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
// Health check endpoint
|
|
57
|
+
app.get("/health", (req, res) => {
|
|
58
|
+
res.status(200).json({ status: "ok", message: "Server is running" });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Mount the post routes
|
|
62
|
+
app.use("/api/posts", postRoutes); // All post routes will be prefixed with /api/posts
|
|
63
|
+
// Mount the image routes
|
|
64
|
+
app.use("/api/images", imageRoutes);
|
|
65
|
+
// Mount the tag routes
|
|
66
|
+
app.use("/api/tags", tagRoutes);
|
|
67
|
+
|
|
68
|
+
// Global error handler for Express
|
|
69
|
+
app.use((err, req, res, next) => {
|
|
70
|
+
const statusCode = err.statusCode || err.response?.status || 500;
|
|
71
|
+
|
|
72
|
+
logger.error('Express error handler triggered', {
|
|
73
|
+
error: err.message,
|
|
74
|
+
statusCode,
|
|
75
|
+
stack: err.stack,
|
|
76
|
+
url: req.url,
|
|
77
|
+
method: req.method,
|
|
78
|
+
type: 'express_error'
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
res.status(statusCode).json({
|
|
82
|
+
message: err.message || "Internal Server Error",
|
|
83
|
+
// Optionally include stack trace in development
|
|
84
|
+
stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Start both servers
|
|
89
|
+
const startServers = async () => {
|
|
90
|
+
// Start Express server
|
|
91
|
+
app.listen(restApiPort, () => {
|
|
92
|
+
logger.info('Express REST API server started', {
|
|
93
|
+
port: restApiPort,
|
|
94
|
+
url: `http://localhost:${restApiPort}`,
|
|
95
|
+
type: 'server_start'
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Start MCP server
|
|
100
|
+
await startMCPServer(mcpPort);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
startServers().catch((error) => {
|
|
104
|
+
logger.error('Failed to start servers', {
|
|
105
|
+
error: error.message,
|
|
106
|
+
stack: error.stack,
|
|
107
|
+
type: 'server_start_error'
|
|
108
|
+
});
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|