@jgardner04/ghost-mcp-server 1.1.12 → 1.2.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/package.json +2 -1
- package/src/__tests__/helpers/mockExpress.js +38 -0
- package/src/__tests__/index.test.js +312 -0
- package/src/__tests__/mcp_server.test.js +381 -0
- package/src/__tests__/mcp_server_improved.test.js +440 -0
- package/src/config/__tests__/mcp-config.test.js +311 -0
- package/src/controllers/__tests__/imageController.test.js +572 -0
- package/src/controllers/__tests__/postController.test.js +236 -0
- package/src/controllers/__tests__/tagController.test.js +222 -0
- package/src/mcp_server_improved.js +105 -1
- package/src/middleware/__tests__/errorMiddleware.test.js +1113 -0
- package/src/resources/__tests__/ResourceManager.test.js +977 -0
- package/src/routes/__tests__/imageRoutes.test.js +117 -0
- package/src/routes/__tests__/postRoutes.test.js +262 -0
- package/src/routes/__tests__/tagRoutes.test.js +175 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock dotenv before importing config
|
|
4
|
+
vi.mock('dotenv', () => ({
|
|
5
|
+
default: { config: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
describe('mcp-config', () => {
|
|
9
|
+
const originalEnv = process.env;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.resetModules();
|
|
13
|
+
// Reset environment variables to a clean state
|
|
14
|
+
process.env = {
|
|
15
|
+
...originalEnv,
|
|
16
|
+
GHOST_ADMIN_API_URL: 'https://ghost.example.com',
|
|
17
|
+
GHOST_ADMIN_API_KEY: 'test-api-key',
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
process.env = originalEnv;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('mcpConfig object', () => {
|
|
26
|
+
describe('transport configuration', () => {
|
|
27
|
+
it('should use default transport type "http" when not specified', async () => {
|
|
28
|
+
delete process.env.MCP_TRANSPORT;
|
|
29
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
30
|
+
expect(mcpConfig.transport.type).toBe('http');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should use MCP_TRANSPORT env var when specified', async () => {
|
|
34
|
+
process.env.MCP_TRANSPORT = 'websocket';
|
|
35
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
36
|
+
expect(mcpConfig.transport.type).toBe('websocket');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should use default port 3001 when not specified', async () => {
|
|
40
|
+
delete process.env.MCP_PORT;
|
|
41
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
42
|
+
expect(mcpConfig.transport.port).toBe(3001);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should use MCP_PORT env var when specified', async () => {
|
|
46
|
+
process.env.MCP_PORT = '4000';
|
|
47
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
48
|
+
expect(mcpConfig.transport.port).toBe(4000);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should use default CORS "*" when not specified', async () => {
|
|
52
|
+
delete process.env.MCP_CORS;
|
|
53
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
54
|
+
expect(mcpConfig.transport.cors).toBe('*');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should use MCP_CORS env var when specified', async () => {
|
|
58
|
+
process.env.MCP_CORS = 'https://example.com';
|
|
59
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
60
|
+
expect(mcpConfig.transport.cors).toBe('https://example.com');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should use default SSE endpoint when not specified', async () => {
|
|
64
|
+
delete process.env.MCP_SSE_ENDPOINT;
|
|
65
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
66
|
+
expect(mcpConfig.transport.sseEndpoint).toBe('/mcp/sse');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should use default WebSocket path when not specified', async () => {
|
|
70
|
+
delete process.env.MCP_WS_PATH;
|
|
71
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
72
|
+
expect(mcpConfig.transport.wsPath).toBe('/');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should use default heartbeat interval when not specified', async () => {
|
|
76
|
+
delete process.env.MCP_WS_HEARTBEAT;
|
|
77
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
78
|
+
expect(mcpConfig.transport.wsHeartbeatInterval).toBe(30000);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('metadata configuration', () => {
|
|
83
|
+
it('should use default server name when not specified', async () => {
|
|
84
|
+
delete process.env.MCP_SERVER_NAME;
|
|
85
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
86
|
+
expect(mcpConfig.metadata.name).toBe('Ghost CMS Manager');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should use MCP_SERVER_NAME env var when specified', async () => {
|
|
90
|
+
process.env.MCP_SERVER_NAME = 'My Ghost Server';
|
|
91
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
92
|
+
expect(mcpConfig.metadata.name).toBe('My Ghost Server');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should use default description when not specified', async () => {
|
|
96
|
+
delete process.env.MCP_SERVER_DESC;
|
|
97
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
98
|
+
expect(mcpConfig.metadata.description).toContain('MCP Server to manage a Ghost CMS');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should use default version when not specified', async () => {
|
|
102
|
+
delete process.env.MCP_SERVER_VERSION;
|
|
103
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
104
|
+
expect(mcpConfig.metadata.version).toBe('1.0.0');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('errorHandling configuration', () => {
|
|
109
|
+
it('should not include stack trace in production', async () => {
|
|
110
|
+
process.env.NODE_ENV = 'production';
|
|
111
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
112
|
+
expect(mcpConfig.errorHandling.includeStackTrace).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should include stack trace in development', async () => {
|
|
116
|
+
process.env.NODE_ENV = 'development';
|
|
117
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
118
|
+
expect(mcpConfig.errorHandling.includeStackTrace).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should use default max retries when not specified', async () => {
|
|
122
|
+
delete process.env.MCP_MAX_RETRIES;
|
|
123
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
124
|
+
expect(mcpConfig.errorHandling.maxRetries).toBe(3);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should use default retry delay when not specified', async () => {
|
|
128
|
+
delete process.env.MCP_RETRY_DELAY;
|
|
129
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
130
|
+
expect(mcpConfig.errorHandling.retryDelay).toBe(1000);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('logging configuration', () => {
|
|
135
|
+
it('should use default log level when not specified', async () => {
|
|
136
|
+
delete process.env.MCP_LOG_LEVEL;
|
|
137
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
138
|
+
expect(mcpConfig.logging.level).toBe('info');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should use MCP_LOG_LEVEL env var when specified', async () => {
|
|
142
|
+
process.env.MCP_LOG_LEVEL = 'debug';
|
|
143
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
144
|
+
expect(mcpConfig.logging.level).toBe('debug');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should use default log format when not specified', async () => {
|
|
148
|
+
delete process.env.MCP_LOG_FORMAT;
|
|
149
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
150
|
+
expect(mcpConfig.logging.format).toBe('json');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('security configuration', () => {
|
|
155
|
+
it('should include API key when specified', async () => {
|
|
156
|
+
process.env.MCP_API_KEY = 'secret-key';
|
|
157
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
158
|
+
expect(mcpConfig.security.apiKey).toBe('secret-key');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should have undefined API key when not specified', async () => {
|
|
162
|
+
delete process.env.MCP_API_KEY;
|
|
163
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
164
|
+
expect(mcpConfig.security.apiKey).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should use default allowed origins when not specified', async () => {
|
|
168
|
+
delete process.env.MCP_ALLOWED_ORIGINS;
|
|
169
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
170
|
+
expect(mcpConfig.security.allowedOrigins).toEqual(['*']);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should parse comma-separated allowed origins', async () => {
|
|
174
|
+
process.env.MCP_ALLOWED_ORIGINS = 'https://a.com,https://b.com';
|
|
175
|
+
const { mcpConfig } = await import('../mcp-config.js');
|
|
176
|
+
expect(mcpConfig.security.allowedOrigins).toEqual(['https://a.com', 'https://b.com']);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('getTransportConfig', () => {
|
|
182
|
+
it('should return stdio config for stdio transport', async () => {
|
|
183
|
+
process.env.MCP_TRANSPORT = 'stdio';
|
|
184
|
+
const { getTransportConfig } = await import('../mcp-config.js');
|
|
185
|
+
const config = getTransportConfig();
|
|
186
|
+
expect(config).toEqual({ type: 'stdio' });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return SSE config for http transport', async () => {
|
|
190
|
+
process.env.MCP_TRANSPORT = 'http';
|
|
191
|
+
process.env.MCP_PORT = '3001';
|
|
192
|
+
const { getTransportConfig } = await import('../mcp-config.js');
|
|
193
|
+
const config = getTransportConfig();
|
|
194
|
+
expect(config.type).toBe('sse');
|
|
195
|
+
expect(config.port).toBe(3001);
|
|
196
|
+
expect(config.cors).toBeDefined();
|
|
197
|
+
expect(config.endpoint).toBeDefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should return SSE config for sse transport', async () => {
|
|
201
|
+
process.env.MCP_TRANSPORT = 'sse';
|
|
202
|
+
const { getTransportConfig } = await import('../mcp-config.js');
|
|
203
|
+
const config = getTransportConfig();
|
|
204
|
+
expect(config.type).toBe('sse');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should return websocket config for websocket transport', async () => {
|
|
208
|
+
process.env.MCP_TRANSPORT = 'websocket';
|
|
209
|
+
process.env.MCP_PORT = '3002';
|
|
210
|
+
process.env.MCP_WS_PATH = '/ws';
|
|
211
|
+
process.env.MCP_WS_HEARTBEAT = '15000';
|
|
212
|
+
const { getTransportConfig } = await import('../mcp-config.js');
|
|
213
|
+
const config = getTransportConfig();
|
|
214
|
+
expect(config).toEqual({
|
|
215
|
+
type: 'websocket',
|
|
216
|
+
port: 3002,
|
|
217
|
+
path: '/ws',
|
|
218
|
+
heartbeatInterval: 15000,
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should throw error for unknown transport type', async () => {
|
|
223
|
+
process.env.MCP_TRANSPORT = 'invalid-transport';
|
|
224
|
+
const { getTransportConfig } = await import('../mcp-config.js');
|
|
225
|
+
expect(() => getTransportConfig()).toThrow('Unknown transport type: invalid-transport');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('validateConfig', () => {
|
|
230
|
+
it('should return true when configuration is valid', async () => {
|
|
231
|
+
process.env.GHOST_ADMIN_API_URL = 'https://ghost.example.com';
|
|
232
|
+
process.env.GHOST_ADMIN_API_KEY = 'test-api-key';
|
|
233
|
+
process.env.MCP_TRANSPORT = 'http';
|
|
234
|
+
process.env.MCP_PORT = '3001';
|
|
235
|
+
const { validateConfig } = await import('../mcp-config.js');
|
|
236
|
+
expect(validateConfig()).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should throw error when GHOST_ADMIN_API_URL is missing', async () => {
|
|
240
|
+
delete process.env.GHOST_ADMIN_API_URL;
|
|
241
|
+
process.env.GHOST_ADMIN_API_KEY = 'test-api-key';
|
|
242
|
+
const { validateConfig } = await import('../mcp-config.js');
|
|
243
|
+
expect(() => validateConfig()).toThrow('Missing GHOST_ADMIN_API_URL');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should throw error when GHOST_ADMIN_API_KEY is missing', async () => {
|
|
247
|
+
process.env.GHOST_ADMIN_API_URL = 'https://ghost.example.com';
|
|
248
|
+
delete process.env.GHOST_ADMIN_API_KEY;
|
|
249
|
+
const { validateConfig } = await import('../mcp-config.js');
|
|
250
|
+
expect(() => validateConfig()).toThrow('Missing GHOST_ADMIN_API_KEY');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should throw error for invalid transport type', async () => {
|
|
254
|
+
process.env.MCP_TRANSPORT = 'invalid';
|
|
255
|
+
const { validateConfig } = await import('../mcp-config.js');
|
|
256
|
+
expect(() => validateConfig()).toThrow('Invalid transport type');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should throw error for invalid port (0)', async () => {
|
|
260
|
+
process.env.MCP_TRANSPORT = 'http';
|
|
261
|
+
process.env.MCP_PORT = '0';
|
|
262
|
+
const { validateConfig } = await import('../mcp-config.js');
|
|
263
|
+
expect(() => validateConfig()).toThrow('Invalid port');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should throw error for invalid port (negative)', async () => {
|
|
267
|
+
process.env.MCP_TRANSPORT = 'http';
|
|
268
|
+
process.env.MCP_PORT = '-1';
|
|
269
|
+
const { validateConfig } = await import('../mcp-config.js');
|
|
270
|
+
expect(() => validateConfig()).toThrow('Invalid port');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should throw error for invalid port (> 65535)', async () => {
|
|
274
|
+
process.env.MCP_TRANSPORT = 'http';
|
|
275
|
+
process.env.MCP_PORT = '70000';
|
|
276
|
+
const { validateConfig } = await import('../mcp-config.js');
|
|
277
|
+
expect(() => validateConfig()).toThrow('Invalid port');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should not validate port for stdio transport', async () => {
|
|
281
|
+
process.env.MCP_TRANSPORT = 'stdio';
|
|
282
|
+
process.env.MCP_PORT = '0'; // Invalid port, but not checked for stdio
|
|
283
|
+
const { validateConfig } = await import('../mcp-config.js');
|
|
284
|
+
expect(validateConfig()).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should accumulate multiple errors', async () => {
|
|
288
|
+
delete process.env.GHOST_ADMIN_API_URL;
|
|
289
|
+
delete process.env.GHOST_ADMIN_API_KEY;
|
|
290
|
+
process.env.MCP_TRANSPORT = 'invalid';
|
|
291
|
+
const { validateConfig } = await import('../mcp-config.js');
|
|
292
|
+
try {
|
|
293
|
+
validateConfig();
|
|
294
|
+
expect.fail('Should have thrown');
|
|
295
|
+
} catch (error) {
|
|
296
|
+
expect(error.message).toContain('Invalid transport type');
|
|
297
|
+
expect(error.message).toContain('GHOST_ADMIN_API_URL');
|
|
298
|
+
expect(error.message).toContain('GHOST_ADMIN_API_KEY');
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('default export', () => {
|
|
304
|
+
it('should export mcpConfig as default', async () => {
|
|
305
|
+
const module = await import('../mcp-config.js');
|
|
306
|
+
expect(module.default).toBeDefined();
|
|
307
|
+
expect(module.default).toHaveProperty('transport');
|
|
308
|
+
expect(module.default).toHaveProperty('metadata');
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|