@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,12 +1,12 @@
|
|
|
1
|
-
import sanitizeHtml from
|
|
2
|
-
import Joi from
|
|
3
|
-
import { createContextLogger } from
|
|
1
|
+
import sanitizeHtml from 'sanitize-html';
|
|
2
|
+
import Joi from 'joi';
|
|
3
|
+
import { createContextLogger } from '../utils/logger.js';
|
|
4
4
|
import {
|
|
5
5
|
createPost as createGhostPost,
|
|
6
6
|
getTags as getGhostTags,
|
|
7
7
|
createTag as createGhostTag,
|
|
8
8
|
// Import other necessary functions from ghostService later
|
|
9
|
-
} from
|
|
9
|
+
} from './ghostService.js'; // Note the relative path
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Helper to generate a simple meta description from HTML content.
|
|
@@ -16,21 +16,21 @@ import {
|
|
|
16
16
|
* @returns {string} A plain text truncated description.
|
|
17
17
|
*/
|
|
18
18
|
const generateSimpleMetaDescription = (htmlContent, maxLength = 500) => {
|
|
19
|
-
if (!htmlContent) return
|
|
20
|
-
|
|
19
|
+
if (!htmlContent) return '';
|
|
20
|
+
|
|
21
21
|
// Use sanitize-html to safely remove all HTML tags
|
|
22
22
|
// This prevents ReDoS attacks and properly handles malformed HTML
|
|
23
23
|
const textContent = sanitizeHtml(htmlContent, {
|
|
24
24
|
allowedTags: [], // Remove all HTML tags
|
|
25
25
|
allowedAttributes: {},
|
|
26
|
-
textFilter: function(text) {
|
|
26
|
+
textFilter: function (text) {
|
|
27
27
|
return text.replace(/\s\s+/g, ' ').trim();
|
|
28
|
-
}
|
|
28
|
+
},
|
|
29
29
|
});
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
// Truncate and add ellipsis if needed
|
|
32
32
|
return textContent.length > maxLength
|
|
33
|
-
? textContent.substring(0, maxLength - 3) +
|
|
33
|
+
? textContent.substring(0, maxLength - 3) + '...'
|
|
34
34
|
: textContent;
|
|
35
35
|
};
|
|
36
36
|
|
|
@@ -52,22 +52,22 @@ const postInputSchema = Joi.object({
|
|
|
52
52
|
feature_image_alt: Joi.string().max(255).optional(),
|
|
53
53
|
feature_image_caption: Joi.string().max(500).optional(),
|
|
54
54
|
meta_title: Joi.string().max(70).optional(),
|
|
55
|
-
meta_description: Joi.string().max(160).optional()
|
|
55
|
+
meta_description: Joi.string().max(160).optional(),
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
const createPostService = async (postInput) => {
|
|
59
59
|
const logger = createContextLogger('post-service');
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
// Validate input to prevent format string vulnerabilities
|
|
62
62
|
const { error, value: validatedInput } = postInputSchema.validate(postInput);
|
|
63
63
|
if (error) {
|
|
64
64
|
logger.error('Post input validation failed', {
|
|
65
65
|
error: error.details[0].message,
|
|
66
|
-
inputKeys: Object.keys(postInput)
|
|
66
|
+
inputKeys: Object.keys(postInput),
|
|
67
67
|
});
|
|
68
68
|
throw new Error(`Invalid post input: ${error.details[0].message}`);
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
|
|
71
71
|
const {
|
|
72
72
|
title,
|
|
73
73
|
html,
|
|
@@ -88,7 +88,7 @@ const createPostService = async (postInput) => {
|
|
|
88
88
|
logger.info('Resolving provided tag names', { tagCount: tags.length, tags });
|
|
89
89
|
resolvedTags = await Promise.all(
|
|
90
90
|
tags.map(async (tagName) => {
|
|
91
|
-
if (typeof tagName !==
|
|
91
|
+
if (typeof tagName !== 'string' || !tagName.trim()) {
|
|
92
92
|
logger.warn('Skipping invalid tag name', { tagName, type: typeof tagName });
|
|
93
93
|
return null; // Skip invalid entries
|
|
94
94
|
}
|
|
@@ -100,7 +100,7 @@ const createPostService = async (postInput) => {
|
|
|
100
100
|
if (existingTags && existingTags.length > 0) {
|
|
101
101
|
logger.debug('Found existing tag', {
|
|
102
102
|
tagName,
|
|
103
|
-
tagId: existingTags[0].id
|
|
103
|
+
tagId: existingTags[0].id,
|
|
104
104
|
});
|
|
105
105
|
// Use the existing tag (Ghost usually accepts name, slug, or id)
|
|
106
106
|
return { name: tagName }; // Or { id: existingTags[0].id } or { slug: existingTags[0].slug }
|
|
@@ -110,7 +110,7 @@ const createPostService = async (postInput) => {
|
|
|
110
110
|
const newTag = await createGhostTag({ name: tagName });
|
|
111
111
|
logger.info('Created new tag successfully', {
|
|
112
112
|
tagName,
|
|
113
|
-
tagId: newTag.id
|
|
113
|
+
tagId: newTag.id,
|
|
114
114
|
});
|
|
115
115
|
// Use the new tag
|
|
116
116
|
return { name: tagName }; // Or { id: newTag.id }
|
|
@@ -118,7 +118,7 @@ const createPostService = async (postInput) => {
|
|
|
118
118
|
} catch (tagError) {
|
|
119
119
|
logger.error('Error processing tag', {
|
|
120
120
|
tagName,
|
|
121
|
-
error: tagError.message
|
|
121
|
+
error: tagError.message,
|
|
122
122
|
});
|
|
123
123
|
return null; // Skip tags that cause errors during processing
|
|
124
124
|
}
|
|
@@ -128,7 +128,7 @@ const createPostService = async (postInput) => {
|
|
|
128
128
|
resolvedTags = resolvedTags.filter((tag) => tag !== null);
|
|
129
129
|
logger.debug('Resolved tags for API', {
|
|
130
130
|
resolvedTagCount: resolvedTags.length,
|
|
131
|
-
resolvedTags
|
|
131
|
+
resolvedTags,
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
134
|
// --- End Tag Resolution ---
|
|
@@ -140,7 +140,7 @@ const createPostService = async (postInput) => {
|
|
|
140
140
|
// Ensure description does not exceed limit even after defaulting
|
|
141
141
|
const truncatedMetaDescription =
|
|
142
142
|
finalMetaDescription.length > 500
|
|
143
|
-
? finalMetaDescription.substring(0, 497) +
|
|
143
|
+
? finalMetaDescription.substring(0, 497) + '...'
|
|
144
144
|
: finalMetaDescription;
|
|
145
145
|
// --- End Metadata Defaults ---
|
|
146
146
|
|
|
@@ -149,7 +149,7 @@ const createPostService = async (postInput) => {
|
|
|
149
149
|
title,
|
|
150
150
|
html,
|
|
151
151
|
custom_excerpt,
|
|
152
|
-
status: status ||
|
|
152
|
+
status: status || 'draft', // Default to draft
|
|
153
153
|
published_at,
|
|
154
154
|
tags: resolvedTags,
|
|
155
155
|
feature_image,
|
|
@@ -164,7 +164,7 @@ const createPostService = async (postInput) => {
|
|
|
164
164
|
title: postDataForApi.title,
|
|
165
165
|
status: postDataForApi.status,
|
|
166
166
|
tagCount: postDataForApi.tags?.length || 0,
|
|
167
|
-
hasFeatureImage: !!postDataForApi.feature_image
|
|
167
|
+
hasFeatureImage: !!postDataForApi.feature_image,
|
|
168
168
|
});
|
|
169
169
|
// Call the lower-level ghostService function
|
|
170
170
|
const newPost = await createGhostPost(postDataForApi);
|
package/src/utils/logger.js
CHANGED
|
@@ -12,7 +12,7 @@ const isDevelopment = process.env.NODE_ENV === 'development';
|
|
|
12
12
|
// Define custom log format
|
|
13
13
|
const logFormat = winston.format.combine(
|
|
14
14
|
winston.format.timestamp({
|
|
15
|
-
format: 'YYYY-MM-DD HH:mm:ss'
|
|
15
|
+
format: 'YYYY-MM-DD HH:mm:ss',
|
|
16
16
|
}),
|
|
17
17
|
winston.format.errors({ stack: true }),
|
|
18
18
|
winston.format.json(),
|
|
@@ -22,7 +22,7 @@ const logFormat = winston.format.combine(
|
|
|
22
22
|
// Development format (more readable)
|
|
23
23
|
const devFormat = winston.format.combine(
|
|
24
24
|
winston.format.timestamp({
|
|
25
|
-
format: 'HH:mm:ss'
|
|
25
|
+
format: 'HH:mm:ss',
|
|
26
26
|
}),
|
|
27
27
|
winston.format.colorize(),
|
|
28
28
|
winston.format.printf(({ level, message, timestamp, metadata }) => {
|
|
@@ -38,52 +38,52 @@ const devFormat = winston.format.combine(
|
|
|
38
38
|
const logger = winston.createLogger({
|
|
39
39
|
level: logLevel,
|
|
40
40
|
format: isDevelopment ? devFormat : logFormat,
|
|
41
|
-
defaultMeta: {
|
|
41
|
+
defaultMeta: {
|
|
42
42
|
service: 'ghost-mcp-server',
|
|
43
|
-
pid: process.pid
|
|
43
|
+
pid: process.pid,
|
|
44
44
|
},
|
|
45
45
|
transports: [
|
|
46
46
|
// Console output
|
|
47
47
|
new winston.transports.Console({
|
|
48
|
-
level: isDevelopment ? 'debug' : 'info'
|
|
49
|
-
})
|
|
48
|
+
level: isDevelopment ? 'debug' : 'info',
|
|
49
|
+
}),
|
|
50
50
|
],
|
|
51
51
|
// Handle uncaught exceptions
|
|
52
|
-
exceptionHandlers: [
|
|
53
|
-
new winston.transports.Console()
|
|
54
|
-
],
|
|
52
|
+
exceptionHandlers: [new winston.transports.Console()],
|
|
55
53
|
// Handle unhandled promise rejections
|
|
56
|
-
rejectionHandlers: [
|
|
57
|
-
new winston.transports.Console()
|
|
58
|
-
]
|
|
54
|
+
rejectionHandlers: [new winston.transports.Console()],
|
|
59
55
|
});
|
|
60
56
|
|
|
61
57
|
// Add file logging in production
|
|
62
58
|
if (!isDevelopment) {
|
|
63
59
|
const logDir = path.join(__dirname, '../../logs');
|
|
64
|
-
|
|
60
|
+
|
|
65
61
|
// Create logs directory if it doesn't exist
|
|
66
|
-
import('fs').then(fs => {
|
|
62
|
+
import('fs').then((fs) => {
|
|
67
63
|
if (!fs.existsSync(logDir)) {
|
|
68
64
|
fs.mkdirSync(logDir, { recursive: true });
|
|
69
65
|
}
|
|
70
66
|
});
|
|
71
|
-
|
|
67
|
+
|
|
72
68
|
// Add file transports for production
|
|
73
|
-
logger.add(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
69
|
+
logger.add(
|
|
70
|
+
new winston.transports.File({
|
|
71
|
+
filename: path.join(logDir, 'error.log'),
|
|
72
|
+
level: 'error',
|
|
73
|
+
maxsize: 10 * 1024 * 1024, // 10MB
|
|
74
|
+
maxFiles: 5,
|
|
75
|
+
tailable: true,
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
logger.add(
|
|
80
|
+
new winston.transports.File({
|
|
81
|
+
filename: path.join(logDir, 'combined.log'),
|
|
82
|
+
maxsize: 10 * 1024 * 1024, // 10MB
|
|
83
|
+
maxFiles: 10,
|
|
84
|
+
tailable: true,
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// Create helper functions for common logging patterns
|
|
@@ -93,41 +93,41 @@ const createContextLogger = (context) => {
|
|
|
93
93
|
info: (message, meta = {}) => logger.info(message, { ...meta, context }),
|
|
94
94
|
warn: (message, meta = {}) => logger.warn(message, { ...meta, context }),
|
|
95
95
|
error: (message, meta = {}) => logger.error(message, { ...meta, context }),
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
// Convenience methods for common patterns
|
|
98
|
-
apiRequest: (method, url, meta = {}) =>
|
|
98
|
+
apiRequest: (method, url, meta = {}) =>
|
|
99
99
|
logger.info(`${method} ${url}`, { ...meta, context, type: 'api_request' }),
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
apiResponse: (method, url, status, meta = {}) =>
|
|
102
102
|
logger.info(`${method} ${url} -> ${status}`, { ...meta, context, type: 'api_response' }),
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
apiError: (method, url, error, meta = {}) =>
|
|
105
|
-
logger.error(`${method} ${url} failed`, {
|
|
106
|
-
...meta,
|
|
107
|
-
context,
|
|
105
|
+
logger.error(`${method} ${url} failed`, {
|
|
106
|
+
...meta,
|
|
107
|
+
context,
|
|
108
108
|
type: 'api_error',
|
|
109
109
|
error: error.message,
|
|
110
|
-
stack: error.stack
|
|
110
|
+
stack: error.stack,
|
|
111
111
|
}),
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
toolExecution: (toolName, input, meta = {}) =>
|
|
114
|
-
logger.info(`Executing tool: ${toolName}`, {
|
|
115
|
-
...meta,
|
|
116
|
-
context,
|
|
114
|
+
logger.info(`Executing tool: ${toolName}`, {
|
|
115
|
+
...meta,
|
|
116
|
+
context,
|
|
117
117
|
type: 'tool_execution',
|
|
118
118
|
tool: toolName,
|
|
119
|
-
inputKeys: Object.keys(input || {})
|
|
119
|
+
inputKeys: Object.keys(input || {}),
|
|
120
120
|
}),
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
toolSuccess: (toolName, result, meta = {}) =>
|
|
123
123
|
logger.info(`Tool ${toolName} completed successfully`, {
|
|
124
124
|
...meta,
|
|
125
125
|
context,
|
|
126
126
|
type: 'tool_success',
|
|
127
127
|
tool: toolName,
|
|
128
|
-
resultType: typeof result
|
|
128
|
+
resultType: typeof result,
|
|
129
129
|
}),
|
|
130
|
-
|
|
130
|
+
|
|
131
131
|
toolError: (toolName, error, meta = {}) =>
|
|
132
132
|
logger.error(`Tool ${toolName} failed`, {
|
|
133
133
|
...meta,
|
|
@@ -135,19 +135,19 @@ const createContextLogger = (context) => {
|
|
|
135
135
|
type: 'tool_error',
|
|
136
136
|
tool: toolName,
|
|
137
137
|
error: error.message,
|
|
138
|
-
stack: error.stack
|
|
138
|
+
stack: error.stack,
|
|
139
139
|
}),
|
|
140
|
-
|
|
140
|
+
|
|
141
141
|
fileOperation: (operation, filePath, meta = {}) =>
|
|
142
142
|
logger.debug(`File operation: ${operation}`, {
|
|
143
143
|
...meta,
|
|
144
144
|
context,
|
|
145
145
|
type: 'file_operation',
|
|
146
146
|
operation,
|
|
147
|
-
file: path.basename(filePath)
|
|
148
|
-
})
|
|
147
|
+
file: path.basename(filePath),
|
|
148
|
+
}),
|
|
149
149
|
};
|
|
150
150
|
};
|
|
151
151
|
|
|
152
152
|
export default logger;
|
|
153
|
-
export { createContextLogger };
|
|
153
|
+
export { createContextLogger };
|
|
@@ -26,7 +26,7 @@ const ALLOWED_DOMAINS = [
|
|
|
26
26
|
'pexels.com',
|
|
27
27
|
'images.pexels.com',
|
|
28
28
|
'pixabay.com',
|
|
29
|
-
'cdn.pixabay.com'
|
|
29
|
+
'cdn.pixabay.com',
|
|
30
30
|
];
|
|
31
31
|
|
|
32
32
|
// Private/internal IP ranges to block
|
|
@@ -44,7 +44,7 @@ const BLOCKED_IP_PATTERNS = [
|
|
|
44
44
|
/^::/, // IPv6 unspecified
|
|
45
45
|
/^fc00:/, // IPv6 unique local
|
|
46
46
|
/^fe80:/, // IPv6 link local
|
|
47
|
-
/^ff00
|
|
47
|
+
/^ff00:/, // IPv6 multicast
|
|
48
48
|
];
|
|
49
49
|
|
|
50
50
|
/**
|
|
@@ -56,16 +56,16 @@ const isSafeHost = (hostname) => {
|
|
|
56
56
|
// Check if hostname is an IP address
|
|
57
57
|
const ipv4Pattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
|
58
58
|
const ipv6Pattern = /^[0-9a-f:]+$/i;
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
if (ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname)) {
|
|
61
61
|
// Check against blocked IP patterns
|
|
62
|
-
return !BLOCKED_IP_PATTERNS.some(pattern => pattern.test(hostname));
|
|
62
|
+
return !BLOCKED_IP_PATTERNS.some((pattern) => pattern.test(hostname));
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
// For domain names, check against allowlist
|
|
66
66
|
const normalizedHost = hostname.toLowerCase();
|
|
67
|
-
return ALLOWED_DOMAINS.some(
|
|
68
|
-
normalizedHost === allowed || normalizedHost.endsWith('.' + allowed)
|
|
67
|
+
return ALLOWED_DOMAINS.some(
|
|
68
|
+
(allowed) => normalizedHost === allowed || normalizedHost.endsWith('.' + allowed)
|
|
69
69
|
);
|
|
70
70
|
};
|
|
71
71
|
|
|
@@ -77,68 +77,71 @@ const isSafeHost = (hostname) => {
|
|
|
77
77
|
const validateImageUrl = (url) => {
|
|
78
78
|
try {
|
|
79
79
|
// Basic URL validation with Joi
|
|
80
|
-
const urlSchema = Joi.string()
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
const urlSchema = Joi.string()
|
|
81
|
+
.uri({
|
|
82
|
+
scheme: ['http', 'https'],
|
|
83
|
+
allowRelative: false,
|
|
84
|
+
})
|
|
85
|
+
.required();
|
|
86
|
+
|
|
85
87
|
const validation = urlSchema.validate(url);
|
|
86
88
|
if (validation.error) {
|
|
87
89
|
return {
|
|
88
90
|
isValid: false,
|
|
89
|
-
error: `Invalid URL format: ${validation.error.details[0].message}
|
|
91
|
+
error: `Invalid URL format: ${validation.error.details[0].message}`,
|
|
90
92
|
};
|
|
91
93
|
}
|
|
92
|
-
|
|
94
|
+
|
|
93
95
|
// Parse URL for additional security checks
|
|
94
96
|
const parsedUrl = new URL(url);
|
|
95
|
-
|
|
97
|
+
|
|
96
98
|
// Only allow HTTP/HTTPS
|
|
97
99
|
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
98
100
|
return {
|
|
99
101
|
isValid: false,
|
|
100
|
-
error: 'Only HTTP and HTTPS protocols are allowed'
|
|
102
|
+
error: 'Only HTTP and HTTPS protocols are allowed',
|
|
101
103
|
};
|
|
102
104
|
}
|
|
103
|
-
|
|
105
|
+
|
|
104
106
|
// Check if host is safe (not internal/private)
|
|
105
107
|
if (!isSafeHost(parsedUrl.hostname)) {
|
|
106
108
|
return {
|
|
107
109
|
isValid: false,
|
|
108
|
-
error: `Requests to ${parsedUrl.hostname} are not allowed for security reasons
|
|
110
|
+
error: `Requests to ${parsedUrl.hostname} are not allowed for security reasons`,
|
|
109
111
|
};
|
|
110
112
|
}
|
|
111
|
-
|
|
113
|
+
|
|
112
114
|
// Prevent requests to non-standard ports (common in SSRF attacks)
|
|
113
115
|
const port = parsedUrl.port;
|
|
114
116
|
if (port && !['80', '443', '8080', '8443'].includes(port)) {
|
|
115
117
|
return {
|
|
116
118
|
isValid: false,
|
|
117
|
-
error: `Requests to non-standard port ${port} are not allowed
|
|
119
|
+
error: `Requests to non-standard port ${port} are not allowed`,
|
|
118
120
|
};
|
|
119
121
|
}
|
|
120
|
-
|
|
122
|
+
|
|
121
123
|
// Additional checks for suspicious patterns
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
if (
|
|
125
|
+
parsedUrl.hostname.includes('localhost') ||
|
|
126
|
+
parsedUrl.hostname === '0.0.0.0' ||
|
|
127
|
+
parsedUrl.hostname.startsWith('192.168.') ||
|
|
128
|
+
parsedUrl.hostname.startsWith('10.') ||
|
|
129
|
+
parsedUrl.hostname.includes('.local')
|
|
130
|
+
) {
|
|
127
131
|
return {
|
|
128
132
|
isValid: false,
|
|
129
|
-
error: 'Requests to local/private addresses are not allowed'
|
|
133
|
+
error: 'Requests to local/private addresses are not allowed',
|
|
130
134
|
};
|
|
131
135
|
}
|
|
132
|
-
|
|
136
|
+
|
|
133
137
|
return {
|
|
134
138
|
isValid: true,
|
|
135
|
-
sanitizedUrl: parsedUrl.href
|
|
139
|
+
sanitizedUrl: parsedUrl.href,
|
|
136
140
|
};
|
|
137
|
-
|
|
138
141
|
} catch (error) {
|
|
139
142
|
return {
|
|
140
143
|
isValid: false,
|
|
141
|
-
error: `URL parsing failed: ${error.message}
|
|
144
|
+
error: `URL parsing failed: ${error.message}`,
|
|
142
145
|
};
|
|
143
146
|
}
|
|
144
147
|
};
|
|
@@ -157,13 +160,9 @@ const createSecureAxiosConfig = (url) => {
|
|
|
157
160
|
maxContentLength: 50 * 1024 * 1024, // 50MB max response
|
|
158
161
|
validateStatus: (status) => status >= 200 && status < 300, // Only accept 2xx
|
|
159
162
|
headers: {
|
|
160
|
-
'User-Agent': 'Ghost-MCP-Server/1.0'
|
|
161
|
-
}
|
|
163
|
+
'User-Agent': 'Ghost-MCP-Server/1.0',
|
|
164
|
+
},
|
|
162
165
|
};
|
|
163
166
|
};
|
|
164
167
|
|
|
165
|
-
export {
|
|
166
|
-
validateImageUrl,
|
|
167
|
-
createSecureAxiosConfig,
|
|
168
|
-
ALLOWED_DOMAINS
|
|
169
|
-
};
|
|
168
|
+
export { validateImageUrl, createSecureAxiosConfig, ALLOWED_DOMAINS };
|