@jgardner04/ghost-mcp-server 1.3.0 → 1.5.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.
@@ -0,0 +1,520 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ // Mock the McpServer to capture tool registrations
4
+ const mockTools = new Map();
5
+
6
+ vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => {
7
+ return {
8
+ McpServer: class MockMcpServer {
9
+ constructor(config) {
10
+ this.config = config;
11
+ }
12
+
13
+ tool(name, description, schema, handler) {
14
+ mockTools.set(name, { name, description, schema, handler });
15
+ }
16
+
17
+ connect(_transport) {
18
+ return Promise.resolve();
19
+ }
20
+ },
21
+ };
22
+ });
23
+
24
+ vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => {
25
+ return {
26
+ StdioServerTransport: class MockStdioServerTransport {},
27
+ };
28
+ });
29
+
30
+ // Mock dotenv
31
+ vi.mock('dotenv', () => ({
32
+ default: { config: vi.fn() },
33
+ }));
34
+
35
+ // Mock crypto
36
+ vi.mock('crypto', () => ({
37
+ default: { randomUUID: vi.fn().mockReturnValue('test-uuid-1234') },
38
+ }));
39
+
40
+ // Mock services for pages
41
+ const mockGetPages = vi.fn();
42
+ const mockGetPage = vi.fn();
43
+ const mockUpdatePage = vi.fn();
44
+ const mockDeletePage = vi.fn();
45
+ const mockSearchPages = vi.fn();
46
+ const mockCreatePageService = vi.fn();
47
+
48
+ // Also mock post and tag services to avoid errors
49
+ const mockGetPosts = vi.fn();
50
+ const mockGetPost = vi.fn();
51
+ const mockGetTags = vi.fn();
52
+ const mockCreateTag = vi.fn();
53
+ const mockUploadImage = vi.fn();
54
+ const mockCreatePostService = vi.fn();
55
+ const mockUpdatePost = vi.fn();
56
+ const mockDeletePost = vi.fn();
57
+ const mockSearchPosts = vi.fn();
58
+ const mockProcessImage = vi.fn();
59
+ const mockValidateImageUrl = vi.fn();
60
+ const mockCreateSecureAxiosConfig = vi.fn();
61
+
62
+ vi.mock('../services/ghostService.js', () => ({
63
+ getPosts: (...args) => mockGetPosts(...args),
64
+ getPost: (...args) => mockGetPost(...args),
65
+ getTags: (...args) => mockGetTags(...args),
66
+ createTag: (...args) => mockCreateTag(...args),
67
+ uploadImage: (...args) => mockUploadImage(...args),
68
+ }));
69
+
70
+ vi.mock('../services/postService.js', () => ({
71
+ createPostService: (...args) => mockCreatePostService(...args),
72
+ }));
73
+
74
+ vi.mock('../services/pageService.js', () => ({
75
+ createPageService: (...args) => mockCreatePageService(...args),
76
+ }));
77
+
78
+ vi.mock('../services/ghostServiceImproved.js', () => ({
79
+ updatePost: (...args) => mockUpdatePost(...args),
80
+ deletePost: (...args) => mockDeletePost(...args),
81
+ searchPosts: (...args) => mockSearchPosts(...args),
82
+ getPages: (...args) => mockGetPages(...args),
83
+ getPage: (...args) => mockGetPage(...args),
84
+ updatePage: (...args) => mockUpdatePage(...args),
85
+ deletePage: (...args) => mockDeletePage(...args),
86
+ searchPages: (...args) => mockSearchPages(...args),
87
+ }));
88
+
89
+ vi.mock('../services/imageProcessingService.js', () => ({
90
+ processImage: (...args) => mockProcessImage(...args),
91
+ }));
92
+
93
+ vi.mock('../utils/urlValidator.js', () => ({
94
+ validateImageUrl: (...args) => mockValidateImageUrl(...args),
95
+ createSecureAxiosConfig: (...args) => mockCreateSecureAxiosConfig(...args),
96
+ }));
97
+
98
+ // Mock axios
99
+ const mockAxios = vi.fn();
100
+ vi.mock('axios', () => ({
101
+ default: (...args) => mockAxios(...args),
102
+ }));
103
+
104
+ // Mock fs
105
+ const mockUnlink = vi.fn((path, cb) => cb(null));
106
+ const mockCreateWriteStream = vi.fn();
107
+ vi.mock('fs', () => ({
108
+ default: {
109
+ unlink: (...args) => mockUnlink(...args),
110
+ createWriteStream: (...args) => mockCreateWriteStream(...args),
111
+ },
112
+ }));
113
+
114
+ // Mock os
115
+ vi.mock('os', () => ({
116
+ default: { tmpdir: vi.fn().mockReturnValue('/tmp') },
117
+ }));
118
+
119
+ // Mock path
120
+ vi.mock('path', async () => {
121
+ const actual = await vi.importActual('path');
122
+ return {
123
+ default: actual,
124
+ ...actual,
125
+ };
126
+ });
127
+
128
+ describe('mcp_server_improved - ghost_get_pages tool', () => {
129
+ beforeEach(async () => {
130
+ vi.clearAllMocks();
131
+ // Import the module to register tools (only first time)
132
+ if (mockTools.size === 0) {
133
+ await import('../mcp_server_improved.js');
134
+ }
135
+ });
136
+
137
+ it('should register ghost_get_pages tool', () => {
138
+ expect(mockTools.has('ghost_get_pages')).toBe(true);
139
+ });
140
+
141
+ it('should have correct schema with all optional parameters', () => {
142
+ const tool = mockTools.get('ghost_get_pages');
143
+ expect(tool).toBeDefined();
144
+ expect(tool.description).toContain('pages');
145
+ expect(tool.schema).toBeDefined();
146
+ expect(tool.schema.limit).toBeDefined();
147
+ expect(tool.schema.page).toBeDefined();
148
+ expect(tool.schema.status).toBeDefined();
149
+ expect(tool.schema.include).toBeDefined();
150
+ expect(tool.schema.filter).toBeDefined();
151
+ expect(tool.schema.order).toBeDefined();
152
+ });
153
+
154
+ it('should retrieve pages with default options', async () => {
155
+ const mockPages = [
156
+ { id: '1', title: 'About Us', slug: 'about-us', status: 'published' },
157
+ { id: '2', title: 'Contact', slug: 'contact', status: 'draft' },
158
+ ];
159
+ mockGetPages.mockResolvedValue(mockPages);
160
+
161
+ const tool = mockTools.get('ghost_get_pages');
162
+ const result = await tool.handler({});
163
+
164
+ expect(mockGetPages).toHaveBeenCalledWith({});
165
+ expect(result.content[0].text).toContain('About Us');
166
+ expect(result.content[0].text).toContain('Contact');
167
+ });
168
+
169
+ it('should pass limit and page parameters', async () => {
170
+ const mockPages = [{ id: '1', title: 'Page 1', slug: 'page-1' }];
171
+ mockGetPages.mockResolvedValue(mockPages);
172
+
173
+ const tool = mockTools.get('ghost_get_pages');
174
+ await tool.handler({ limit: 10, page: 2 });
175
+
176
+ expect(mockGetPages).toHaveBeenCalledWith({ limit: 10, page: 2 });
177
+ });
178
+
179
+ it('should validate limit is between 1 and 100', () => {
180
+ const tool = mockTools.get('ghost_get_pages');
181
+ const schema = tool.schema;
182
+
183
+ expect(schema.limit).toBeDefined();
184
+ expect(() => schema.limit.parse(0)).toThrow();
185
+ expect(() => schema.limit.parse(101)).toThrow();
186
+ expect(schema.limit.parse(50)).toBe(50);
187
+ });
188
+
189
+ it('should pass status filter', async () => {
190
+ const mockPages = [{ id: '1', title: 'Published Page', status: 'published' }];
191
+ mockGetPages.mockResolvedValue(mockPages);
192
+
193
+ const tool = mockTools.get('ghost_get_pages');
194
+ await tool.handler({ status: 'published' });
195
+
196
+ expect(mockGetPages).toHaveBeenCalledWith({ status: 'published' });
197
+ });
198
+
199
+ it('should handle errors gracefully', async () => {
200
+ mockGetPages.mockRejectedValue(new Error('API error'));
201
+
202
+ const tool = mockTools.get('ghost_get_pages');
203
+ const result = await tool.handler({});
204
+
205
+ expect(result.isError).toBe(true);
206
+ expect(result.content[0].text).toContain('Error retrieving pages');
207
+ });
208
+ });
209
+
210
+ describe('mcp_server_improved - ghost_get_page tool', () => {
211
+ beforeEach(async () => {
212
+ vi.clearAllMocks();
213
+ if (mockTools.size === 0) {
214
+ await import('../mcp_server_improved.js');
215
+ }
216
+ });
217
+
218
+ it('should register ghost_get_page tool', () => {
219
+ expect(mockTools.has('ghost_get_page')).toBe(true);
220
+ });
221
+
222
+ it('should have correct schema with id and slug options', () => {
223
+ const tool = mockTools.get('ghost_get_page');
224
+ expect(tool).toBeDefined();
225
+ expect(tool.schema.id).toBeDefined();
226
+ expect(tool.schema.slug).toBeDefined();
227
+ expect(tool.schema.include).toBeDefined();
228
+ });
229
+
230
+ it('should retrieve page by ID', async () => {
231
+ const mockPage = { id: 'page-123', title: 'About Us', slug: 'about-us' };
232
+ mockGetPage.mockResolvedValue(mockPage);
233
+
234
+ const tool = mockTools.get('ghost_get_page');
235
+ const result = await tool.handler({ id: 'page-123' });
236
+
237
+ expect(mockGetPage).toHaveBeenCalledWith('page-123', {});
238
+ expect(result.content[0].text).toContain('About Us');
239
+ });
240
+
241
+ it('should retrieve page by slug', async () => {
242
+ const mockPage = { id: 'page-123', title: 'About Us', slug: 'about-us' };
243
+ mockGetPage.mockResolvedValue(mockPage);
244
+
245
+ const tool = mockTools.get('ghost_get_page');
246
+ const result = await tool.handler({ slug: 'about-us' });
247
+
248
+ expect(mockGetPage).toHaveBeenCalledWith('slug/about-us', {});
249
+ expect(result.content[0].text).toContain('About Us');
250
+ });
251
+
252
+ it('should require either id or slug', async () => {
253
+ const tool = mockTools.get('ghost_get_page');
254
+ const result = await tool.handler({});
255
+
256
+ expect(result.isError).toBe(true);
257
+ expect(result.content[0].text).toContain('Either id or slug is required');
258
+ });
259
+
260
+ it('should handle errors gracefully', async () => {
261
+ mockGetPage.mockRejectedValue(new Error('Page not found'));
262
+
263
+ const tool = mockTools.get('ghost_get_page');
264
+ const result = await tool.handler({ id: 'nonexistent' });
265
+
266
+ expect(result.isError).toBe(true);
267
+ expect(result.content[0].text).toContain('Error retrieving page');
268
+ });
269
+ });
270
+
271
+ describe('mcp_server_improved - ghost_create_page tool', () => {
272
+ beforeEach(async () => {
273
+ vi.clearAllMocks();
274
+ if (mockTools.size === 0) {
275
+ await import('../mcp_server_improved.js');
276
+ }
277
+ });
278
+
279
+ it('should register ghost_create_page tool', () => {
280
+ expect(mockTools.has('ghost_create_page')).toBe(true);
281
+ });
282
+
283
+ it('should have correct schema with required and optional fields', () => {
284
+ const tool = mockTools.get('ghost_create_page');
285
+ expect(tool).toBeDefined();
286
+ expect(tool.description).toContain('NOT support tags');
287
+ expect(tool.schema.title).toBeDefined();
288
+ expect(tool.schema.html).toBeDefined();
289
+ expect(tool.schema.status).toBeDefined();
290
+ expect(tool.schema.feature_image).toBeDefined();
291
+ expect(tool.schema.meta_title).toBeDefined();
292
+ expect(tool.schema.meta_description).toBeDefined();
293
+ // Should NOT have tags
294
+ expect(tool.schema.tags).toBeUndefined();
295
+ });
296
+
297
+ it('should create page with minimal input', async () => {
298
+ const createdPage = { id: 'page-123', title: 'New Page', status: 'draft' };
299
+ mockCreatePageService.mockResolvedValue(createdPage);
300
+
301
+ const tool = mockTools.get('ghost_create_page');
302
+ const result = await tool.handler({ title: 'New Page', html: '<p>Content</p>' });
303
+
304
+ expect(mockCreatePageService).toHaveBeenCalledWith({
305
+ title: 'New Page',
306
+ html: '<p>Content</p>',
307
+ });
308
+ expect(result.content[0].text).toContain('New Page');
309
+ });
310
+
311
+ it('should create page with all optional fields', async () => {
312
+ const fullInput = {
313
+ title: 'Complete Page',
314
+ html: '<p>Content</p>',
315
+ status: 'published',
316
+ custom_excerpt: 'Excerpt',
317
+ feature_image: 'https://example.com/image.jpg',
318
+ feature_image_alt: 'Alt text',
319
+ feature_image_caption: 'Caption',
320
+ meta_title: 'SEO Title',
321
+ meta_description: 'SEO Description',
322
+ };
323
+ const createdPage = { id: 'page-123', ...fullInput };
324
+ mockCreatePageService.mockResolvedValue(createdPage);
325
+
326
+ const tool = mockTools.get('ghost_create_page');
327
+ const result = await tool.handler(fullInput);
328
+
329
+ expect(mockCreatePageService).toHaveBeenCalledWith(fullInput);
330
+ expect(result.content[0].text).toContain('Complete Page');
331
+ });
332
+
333
+ it('should handle errors gracefully', async () => {
334
+ mockCreatePageService.mockRejectedValue(new Error('Invalid input'));
335
+
336
+ const tool = mockTools.get('ghost_create_page');
337
+ const result = await tool.handler({ title: 'Test', html: '<p>Content</p>' });
338
+
339
+ expect(result.isError).toBe(true);
340
+ expect(result.content[0].text).toContain('Error creating page');
341
+ });
342
+ });
343
+
344
+ describe('mcp_server_improved - ghost_update_page tool', () => {
345
+ beforeEach(async () => {
346
+ vi.clearAllMocks();
347
+ if (mockTools.size === 0) {
348
+ await import('../mcp_server_improved.js');
349
+ }
350
+ });
351
+
352
+ it('should register ghost_update_page tool', () => {
353
+ expect(mockTools.has('ghost_update_page')).toBe(true);
354
+ });
355
+
356
+ it('should have correct schema with id required and other fields optional', () => {
357
+ const tool = mockTools.get('ghost_update_page');
358
+ expect(tool).toBeDefined();
359
+ expect(tool.description).toContain('NOT support tags');
360
+ expect(tool.schema.id).toBeDefined();
361
+ expect(tool.schema.title).toBeDefined();
362
+ expect(tool.schema.html).toBeDefined();
363
+ expect(tool.schema.status).toBeDefined();
364
+ // Should NOT have tags
365
+ expect(tool.schema.tags).toBeUndefined();
366
+ });
367
+
368
+ it('should update page with new title', async () => {
369
+ const updatedPage = { id: 'page-123', title: 'Updated Title' };
370
+ mockUpdatePage.mockResolvedValue(updatedPage);
371
+
372
+ const tool = mockTools.get('ghost_update_page');
373
+ const result = await tool.handler({ id: 'page-123', title: 'Updated Title' });
374
+
375
+ expect(mockUpdatePage).toHaveBeenCalledWith('page-123', { title: 'Updated Title' });
376
+ expect(result.content[0].text).toContain('Updated Title');
377
+ });
378
+
379
+ it('should update page with multiple fields', async () => {
380
+ const updatedPage = { id: 'page-123', title: 'New Title', status: 'published' };
381
+ mockUpdatePage.mockResolvedValue(updatedPage);
382
+
383
+ const tool = mockTools.get('ghost_update_page');
384
+ const result = await tool.handler({
385
+ id: 'page-123',
386
+ title: 'New Title',
387
+ status: 'published',
388
+ html: '<p>Updated content</p>',
389
+ });
390
+
391
+ expect(mockUpdatePage).toHaveBeenCalledWith('page-123', {
392
+ title: 'New Title',
393
+ status: 'published',
394
+ html: '<p>Updated content</p>',
395
+ });
396
+ expect(result.content[0].text).toContain('New Title');
397
+ });
398
+
399
+ it('should handle errors gracefully', async () => {
400
+ mockUpdatePage.mockRejectedValue(new Error('Page not found'));
401
+
402
+ const tool = mockTools.get('ghost_update_page');
403
+ const result = await tool.handler({ id: 'nonexistent', title: 'Test' });
404
+
405
+ expect(result.isError).toBe(true);
406
+ expect(result.content[0].text).toContain('Error updating page');
407
+ });
408
+ });
409
+
410
+ describe('mcp_server_improved - ghost_delete_page tool', () => {
411
+ beforeEach(async () => {
412
+ vi.clearAllMocks();
413
+ if (mockTools.size === 0) {
414
+ await import('../mcp_server_improved.js');
415
+ }
416
+ });
417
+
418
+ it('should register ghost_delete_page tool', () => {
419
+ expect(mockTools.has('ghost_delete_page')).toBe(true);
420
+ });
421
+
422
+ it('should have correct schema with id required', () => {
423
+ const tool = mockTools.get('ghost_delete_page');
424
+ expect(tool).toBeDefined();
425
+ expect(tool.schema.id).toBeDefined();
426
+ expect(tool.description).toContain('permanent');
427
+ });
428
+
429
+ it('should delete page by ID', async () => {
430
+ mockDeletePage.mockResolvedValue({ id: 'page-123' });
431
+
432
+ const tool = mockTools.get('ghost_delete_page');
433
+ const result = await tool.handler({ id: 'page-123' });
434
+
435
+ expect(mockDeletePage).toHaveBeenCalledWith('page-123');
436
+ expect(result.content[0].text).toContain('successfully deleted');
437
+ });
438
+
439
+ it('should handle errors gracefully', async () => {
440
+ mockDeletePage.mockRejectedValue(new Error('Page not found'));
441
+
442
+ const tool = mockTools.get('ghost_delete_page');
443
+ const result = await tool.handler({ id: 'nonexistent' });
444
+
445
+ expect(result.isError).toBe(true);
446
+ expect(result.content[0].text).toContain('Error deleting page');
447
+ });
448
+ });
449
+
450
+ describe('mcp_server_improved - ghost_search_pages tool', () => {
451
+ beforeEach(async () => {
452
+ vi.clearAllMocks();
453
+ if (mockTools.size === 0) {
454
+ await import('../mcp_server_improved.js');
455
+ }
456
+ });
457
+
458
+ it('should register ghost_search_pages tool', () => {
459
+ expect(mockTools.has('ghost_search_pages')).toBe(true);
460
+ });
461
+
462
+ it('should have correct schema with query required', () => {
463
+ const tool = mockTools.get('ghost_search_pages');
464
+ expect(tool).toBeDefined();
465
+ expect(tool.schema.query).toBeDefined();
466
+ expect(tool.schema.status).toBeDefined();
467
+ expect(tool.schema.limit).toBeDefined();
468
+ });
469
+
470
+ it('should search pages with query', async () => {
471
+ const mockPages = [{ id: '1', title: 'About Us', slug: 'about-us' }];
472
+ mockSearchPages.mockResolvedValue(mockPages);
473
+
474
+ const tool = mockTools.get('ghost_search_pages');
475
+ const result = await tool.handler({ query: 'about' });
476
+
477
+ expect(mockSearchPages).toHaveBeenCalledWith('about', {});
478
+ expect(result.content[0].text).toContain('About Us');
479
+ });
480
+
481
+ it('should pass status filter', async () => {
482
+ const mockPages = [{ id: '1', title: 'Published Page' }];
483
+ mockSearchPages.mockResolvedValue(mockPages);
484
+
485
+ const tool = mockTools.get('ghost_search_pages');
486
+ await tool.handler({ query: 'test', status: 'published' });
487
+
488
+ expect(mockSearchPages).toHaveBeenCalledWith('test', { status: 'published' });
489
+ });
490
+
491
+ it('should pass limit option', async () => {
492
+ const mockPages = [];
493
+ mockSearchPages.mockResolvedValue(mockPages);
494
+
495
+ const tool = mockTools.get('ghost_search_pages');
496
+ await tool.handler({ query: 'test', limit: 5 });
497
+
498
+ expect(mockSearchPages).toHaveBeenCalledWith('test', { limit: 5 });
499
+ });
500
+
501
+ it('should validate limit is between 1 and 50', () => {
502
+ const tool = mockTools.get('ghost_search_pages');
503
+ const schema = tool.schema;
504
+
505
+ expect(schema.limit).toBeDefined();
506
+ expect(() => schema.limit.parse(0)).toThrow();
507
+ expect(() => schema.limit.parse(51)).toThrow();
508
+ expect(schema.limit.parse(25)).toBe(25);
509
+ });
510
+
511
+ it('should handle errors gracefully', async () => {
512
+ mockSearchPages.mockRejectedValue(new Error('Search failed'));
513
+
514
+ const tool = mockTools.get('ghost_search_pages');
515
+ const result = await tool.handler({ query: 'test' });
516
+
517
+ expect(result.isError).toBe(true);
518
+ expect(result.content[0].text).toContain('Error searching pages');
519
+ });
520
+ });