@jgardner04/ghost-mcp-server 1.12.2 → 1.12.3

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.
@@ -1,1301 +0,0 @@
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 - will be lazy loaded
41
- const mockGetPosts = vi.fn();
42
- const mockGetPost = vi.fn();
43
- const mockGetTags = vi.fn();
44
- const mockGetTag = vi.fn();
45
- const mockCreateTag = vi.fn();
46
- const mockUpdateTag = vi.fn();
47
- const mockDeleteTag = vi.fn();
48
- const mockUploadImage = vi.fn();
49
- const mockCreatePostService = vi.fn();
50
- const mockProcessImage = vi.fn();
51
- const mockValidateImageUrl = vi.fn();
52
- const mockCreateSecureAxiosConfig = vi.fn();
53
- const mockUpdatePost = vi.fn();
54
- const mockDeletePost = vi.fn();
55
- const mockSearchPosts = vi.fn();
56
-
57
- // Page mocks
58
- const mockGetPages = vi.fn();
59
- const mockGetPage = vi.fn();
60
- const mockCreatePageService = vi.fn();
61
- const mockUpdatePage = vi.fn();
62
- const mockDeletePage = vi.fn();
63
- const mockSearchPages = vi.fn();
64
-
65
- // Member mocks
66
- const mockCreateMember = vi.fn();
67
- const mockUpdateMember = vi.fn();
68
- const mockDeleteMember = vi.fn();
69
- const mockGetMembers = vi.fn();
70
- const mockGetMember = vi.fn();
71
- const mockSearchMembers = vi.fn();
72
-
73
- // Newsletter mocks
74
- const mockGetNewsletters = vi.fn();
75
- const mockGetNewsletter = vi.fn();
76
- const mockCreateNewsletterService = vi.fn();
77
- const mockUpdateNewsletter = vi.fn();
78
- const mockDeleteNewsletter = vi.fn();
79
-
80
- // Tier mocks
81
- const mockGetTiers = vi.fn();
82
- const mockGetTier = vi.fn();
83
- const mockCreateTier = vi.fn();
84
- const mockUpdateTier = vi.fn();
85
- const mockDeleteTier = vi.fn();
86
-
87
- vi.mock('../services/postService.js', () => ({
88
- createPostService: (...args) => mockCreatePostService(...args),
89
- }));
90
-
91
- vi.mock('../services/pageService.js', () => ({
92
- createPageService: (...args) => mockCreatePageService(...args),
93
- }));
94
-
95
- vi.mock('../services/newsletterService.js', () => ({
96
- createNewsletterService: (...args) => mockCreateNewsletterService(...args),
97
- }));
98
-
99
- vi.mock('../services/ghostServiceImproved.js', () => ({
100
- // Posts
101
- getPosts: (...args) => mockGetPosts(...args),
102
- getPost: (...args) => mockGetPost(...args),
103
- updatePost: (...args) => mockUpdatePost(...args),
104
- deletePost: (...args) => mockDeletePost(...args),
105
- searchPosts: (...args) => mockSearchPosts(...args),
106
- // Tags
107
- getTags: (...args) => mockGetTags(...args),
108
- getTag: (...args) => mockGetTag(...args),
109
- createTag: (...args) => mockCreateTag(...args),
110
- updateTag: (...args) => mockUpdateTag(...args),
111
- deleteTag: (...args) => mockDeleteTag(...args),
112
- // Images
113
- uploadImage: (...args) => mockUploadImage(...args),
114
- // Pages
115
- getPages: (...args) => mockGetPages(...args),
116
- getPage: (...args) => mockGetPage(...args),
117
- updatePage: (...args) => mockUpdatePage(...args),
118
- deletePage: (...args) => mockDeletePage(...args),
119
- searchPages: (...args) => mockSearchPages(...args),
120
- // Members
121
- createMember: (...args) => mockCreateMember(...args),
122
- updateMember: (...args) => mockUpdateMember(...args),
123
- deleteMember: (...args) => mockDeleteMember(...args),
124
- getMembers: (...args) => mockGetMembers(...args),
125
- getMember: (...args) => mockGetMember(...args),
126
- searchMembers: (...args) => mockSearchMembers(...args),
127
- // Newsletters
128
- getNewsletters: (...args) => mockGetNewsletters(...args),
129
- getNewsletter: (...args) => mockGetNewsletter(...args),
130
- updateNewsletter: (...args) => mockUpdateNewsletter(...args),
131
- deleteNewsletter: (...args) => mockDeleteNewsletter(...args),
132
- // Tiers
133
- getTiers: (...args) => mockGetTiers(...args),
134
- getTier: (...args) => mockGetTier(...args),
135
- createTier: (...args) => mockCreateTier(...args),
136
- updateTier: (...args) => mockUpdateTier(...args),
137
- deleteTier: (...args) => mockDeleteTier(...args),
138
- }));
139
-
140
- vi.mock('../services/imageProcessingService.js', () => ({
141
- processImage: (...args) => mockProcessImage(...args),
142
- }));
143
-
144
- vi.mock('../utils/urlValidator.js', () => ({
145
- validateImageUrl: (...args) => mockValidateImageUrl(...args),
146
- createSecureAxiosConfig: (...args) => mockCreateSecureAxiosConfig(...args),
147
- }));
148
-
149
- // Mock axios
150
- const mockAxios = vi.fn();
151
- vi.mock('axios', () => ({
152
- default: (...args) => mockAxios(...args),
153
- }));
154
-
155
- // Mock fs
156
- const mockCreateWriteStream = vi.fn();
157
- vi.mock('fs', () => ({
158
- default: {
159
- createWriteStream: (...args) => mockCreateWriteStream(...args),
160
- },
161
- }));
162
-
163
- // Mock tempFileManager
164
- const mockTrackTempFile = vi.fn();
165
- const mockCleanupTempFiles = vi.fn().mockResolvedValue(undefined);
166
- vi.mock('../utils/tempFileManager.js', () => ({
167
- trackTempFile: (...args) => mockTrackTempFile(...args),
168
- cleanupTempFiles: (...args) => mockCleanupTempFiles(...args),
169
- }));
170
-
171
- // Mock os
172
- vi.mock('os', () => ({
173
- default: { tmpdir: vi.fn().mockReturnValue('/tmp') },
174
- }));
175
-
176
- // Mock path
177
- vi.mock('path', async () => {
178
- const actual = await vi.importActual('path');
179
- return {
180
- default: actual,
181
- ...actual,
182
- };
183
- });
184
-
185
- describe('mcp_server_improved - ghost_get_posts tool', () => {
186
- beforeEach(async () => {
187
- vi.clearAllMocks();
188
- // Don't clear mockTools - they're registered once on module load
189
- // Import the module to register tools (only first time)
190
- if (mockTools.size === 0) {
191
- await import('../mcp_server_improved.js');
192
- }
193
- });
194
-
195
- it('should register ghost_get_posts tool', () => {
196
- expect(mockTools.has('ghost_get_posts')).toBe(true);
197
- });
198
-
199
- it('should have correct schema with all optional parameters', () => {
200
- const tool = mockTools.get('ghost_get_posts');
201
- expect(tool).toBeDefined();
202
- expect(tool.description).toContain('posts');
203
- expect(tool.schema).toBeDefined();
204
- // Zod schemas store field definitions in schema.shape
205
- expect(tool.schema.shape.limit).toBeDefined();
206
- expect(tool.schema.shape.page).toBeDefined();
207
- expect(tool.schema.shape.status).toBeDefined();
208
- expect(tool.schema.shape.include).toBeDefined();
209
- expect(tool.schema.shape.filter).toBeDefined();
210
- expect(tool.schema.shape.order).toBeDefined();
211
- });
212
-
213
- it('should retrieve posts with default options', async () => {
214
- const mockPosts = [
215
- { id: '1', title: 'Post 1', slug: 'post-1', status: 'published' },
216
- { id: '2', title: 'Post 2', slug: 'post-2', status: 'draft' },
217
- ];
218
- mockGetPosts.mockResolvedValue(mockPosts);
219
-
220
- const tool = mockTools.get('ghost_get_posts');
221
- const result = await tool.handler({});
222
-
223
- expect(mockGetPosts).toHaveBeenCalledWith({});
224
- expect(result.content[0].text).toContain('Post 1');
225
- expect(result.content[0].text).toContain('Post 2');
226
- });
227
-
228
- it('should pass limit and page parameters', async () => {
229
- const mockPosts = [{ id: '1', title: 'Post 1', slug: 'post-1' }];
230
- mockGetPosts.mockResolvedValue(mockPosts);
231
-
232
- const tool = mockTools.get('ghost_get_posts');
233
- await tool.handler({ limit: 10, page: 2 });
234
-
235
- expect(mockGetPosts).toHaveBeenCalledWith({ limit: 10, page: 2 });
236
- });
237
-
238
- it('should validate limit is between 1 and 100', () => {
239
- const tool = mockTools.get('ghost_get_posts');
240
- // Zod schemas store field definitions in schema.shape
241
- const shape = tool.schema.shape;
242
-
243
- // Test that limit schema exists and has proper validation
244
- expect(shape.limit).toBeDefined();
245
- expect(() => shape.limit.parse(0)).toThrow();
246
- expect(() => shape.limit.parse(101)).toThrow();
247
- expect(shape.limit.parse(50)).toBe(50);
248
- });
249
-
250
- it('should validate page is at least 1', () => {
251
- const tool = mockTools.get('ghost_get_posts');
252
- // Zod schemas store field definitions in schema.shape
253
- const shape = tool.schema.shape;
254
-
255
- expect(shape.page).toBeDefined();
256
- expect(() => shape.page.parse(0)).toThrow();
257
- expect(shape.page.parse(1)).toBe(1);
258
- });
259
-
260
- it('should pass status filter', async () => {
261
- const mockPosts = [{ id: '1', title: 'Published Post', status: 'published' }];
262
- mockGetPosts.mockResolvedValue(mockPosts);
263
-
264
- const tool = mockTools.get('ghost_get_posts');
265
- await tool.handler({ status: 'published' });
266
-
267
- expect(mockGetPosts).toHaveBeenCalledWith({ status: 'published' });
268
- });
269
-
270
- it('should validate status enum values', () => {
271
- const tool = mockTools.get('ghost_get_posts');
272
- // Zod schemas store field definitions in schema.shape
273
- const shape = tool.schema.shape;
274
-
275
- expect(shape.status).toBeDefined();
276
- expect(() => shape.status.parse('invalid')).toThrow();
277
- expect(shape.status.parse('published')).toBe('published');
278
- expect(shape.status.parse('draft')).toBe('draft');
279
- expect(shape.status.parse('scheduled')).toBe('scheduled');
280
- expect(shape.status.parse('all')).toBe('all');
281
- });
282
-
283
- it('should pass include parameter', async () => {
284
- const mockPosts = [
285
- {
286
- id: '1',
287
- title: 'Post with tags',
288
- tags: [{ name: 'tech' }],
289
- authors: [{ name: 'John' }],
290
- },
291
- ];
292
- mockGetPosts.mockResolvedValue(mockPosts);
293
-
294
- const tool = mockTools.get('ghost_get_posts');
295
- await tool.handler({ include: 'tags,authors' });
296
-
297
- expect(mockGetPosts).toHaveBeenCalledWith({ include: 'tags,authors' });
298
- });
299
-
300
- it('should pass filter parameter (NQL)', async () => {
301
- const mockPosts = [{ id: '1', title: 'Featured Post', featured: true }];
302
- mockGetPosts.mockResolvedValue(mockPosts);
303
-
304
- const tool = mockTools.get('ghost_get_posts');
305
- await tool.handler({ filter: 'featured:true' });
306
-
307
- expect(mockGetPosts).toHaveBeenCalledWith({ filter: 'featured:true' });
308
- });
309
-
310
- it('should pass order parameter', async () => {
311
- const mockPosts = [
312
- { id: '1', title: 'Newest', published_at: '2025-12-10' },
313
- { id: '2', title: 'Older', published_at: '2025-12-01' },
314
- ];
315
- mockGetPosts.mockResolvedValue(mockPosts);
316
-
317
- const tool = mockTools.get('ghost_get_posts');
318
- await tool.handler({ order: 'published_at DESC' });
319
-
320
- expect(mockGetPosts).toHaveBeenCalledWith({ order: 'published_at DESC' });
321
- });
322
-
323
- it('should pass all parameters combined', async () => {
324
- const mockPosts = [{ id: '1', title: 'Test Post' }];
325
- mockGetPosts.mockResolvedValue(mockPosts);
326
-
327
- const tool = mockTools.get('ghost_get_posts');
328
- await tool.handler({
329
- limit: 20,
330
- page: 1,
331
- status: 'published',
332
- include: 'tags,authors',
333
- filter: 'featured:true',
334
- order: 'published_at DESC',
335
- });
336
-
337
- expect(mockGetPosts).toHaveBeenCalledWith({
338
- limit: 20,
339
- page: 1,
340
- status: 'published',
341
- include: 'tags,authors',
342
- filter: 'featured:true',
343
- order: 'published_at DESC',
344
- });
345
- });
346
-
347
- it('should handle errors from ghostService', async () => {
348
- mockGetPosts.mockRejectedValue(new Error('Ghost API error'));
349
-
350
- const tool = mockTools.get('ghost_get_posts');
351
- const result = await tool.handler({});
352
-
353
- expect(result.isError).toBe(true);
354
- expect(result.content[0].text).toContain('Ghost API error');
355
- });
356
-
357
- it('should return formatted JSON response', async () => {
358
- const mockPosts = [
359
- {
360
- id: '1',
361
- title: 'Test Post',
362
- slug: 'test-post',
363
- html: '<p>Content</p>',
364
- status: 'published',
365
- },
366
- ];
367
- mockGetPosts.mockResolvedValue(mockPosts);
368
-
369
- const tool = mockTools.get('ghost_get_posts');
370
- const result = await tool.handler({});
371
-
372
- expect(result.content).toBeDefined();
373
- expect(result.content[0].type).toBe('text');
374
- expect(result.content[0].text).toContain('"id": "1"');
375
- expect(result.content[0].text).toContain('"title": "Test Post"');
376
- });
377
- });
378
-
379
- describe('mcp_server_improved - ghost_get_post tool', () => {
380
- beforeEach(async () => {
381
- vi.clearAllMocks();
382
- // Don't clear mockTools - they're registered once on module load
383
- if (mockTools.size === 0) {
384
- await import('../mcp_server_improved.js');
385
- }
386
- });
387
-
388
- it('should register ghost_get_post tool', () => {
389
- expect(mockTools.has('ghost_get_post')).toBe(true);
390
- });
391
-
392
- it('should have correct schema requiring one of id or slug', () => {
393
- const tool = mockTools.get('ghost_get_post');
394
- expect(tool).toBeDefined();
395
- expect(tool.description).toContain('post');
396
- expect(tool.schema).toBeDefined();
397
- // ghost_get_post uses a refined schema, access via _def.schema.shape
398
- const shape = tool.schema._def.schema.shape;
399
- expect(shape.id).toBeDefined();
400
- expect(shape.slug).toBeDefined();
401
- expect(shape.include).toBeDefined();
402
- });
403
-
404
- it('should retrieve post by ID', async () => {
405
- const mockPost = {
406
- id: '507f1f77bcf86cd799439011',
407
- title: 'Test Post',
408
- slug: 'test-post',
409
- html: '<p>Content</p>',
410
- status: 'published',
411
- };
412
- mockGetPost.mockResolvedValue(mockPost);
413
-
414
- const tool = mockTools.get('ghost_get_post');
415
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
416
-
417
- expect(mockGetPost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {});
418
- expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"');
419
- expect(result.content[0].text).toContain('"title": "Test Post"');
420
- });
421
-
422
- it('should retrieve post by slug', async () => {
423
- const mockPost = {
424
- id: '507f1f77bcf86cd799439011',
425
- title: 'Test Post',
426
- slug: 'test-post',
427
- html: '<p>Content</p>',
428
- status: 'published',
429
- };
430
- mockGetPost.mockResolvedValue(mockPost);
431
-
432
- const tool = mockTools.get('ghost_get_post');
433
- const result = await tool.handler({ slug: 'test-post' });
434
-
435
- expect(mockGetPost).toHaveBeenCalledWith('slug/test-post', {});
436
- expect(result.content[0].text).toContain('"title": "Test Post"');
437
- });
438
-
439
- it('should pass include parameter with ID', async () => {
440
- const mockPost = {
441
- id: '507f1f77bcf86cd799439011',
442
- title: 'Post with relations',
443
- tags: [{ name: 'tech' }],
444
- authors: [{ name: 'John' }],
445
- };
446
- mockGetPost.mockResolvedValue(mockPost);
447
-
448
- const tool = mockTools.get('ghost_get_post');
449
- await tool.handler({ id: '507f1f77bcf86cd799439011', include: 'tags,authors' });
450
-
451
- expect(mockGetPost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
452
- include: 'tags,authors',
453
- });
454
- });
455
-
456
- it('should pass include parameter with slug', async () => {
457
- const mockPost = {
458
- id: '507f1f77bcf86cd799439011',
459
- title: 'Post with relations',
460
- slug: 'test-post',
461
- tags: [{ name: 'tech' }],
462
- };
463
- mockGetPost.mockResolvedValue(mockPost);
464
-
465
- const tool = mockTools.get('ghost_get_post');
466
- await tool.handler({ slug: 'test-post', include: 'tags' });
467
-
468
- expect(mockGetPost).toHaveBeenCalledWith('slug/test-post', { include: 'tags' });
469
- });
470
-
471
- it('should prefer ID over slug when both provided', async () => {
472
- const mockPost = { id: '507f1f77bcf86cd799439011', title: 'Test Post', slug: 'test-post' };
473
- mockGetPost.mockResolvedValue(mockPost);
474
-
475
- const tool = mockTools.get('ghost_get_post');
476
- await tool.handler({ id: '507f1f77bcf86cd799439011', slug: 'wrong-slug' });
477
-
478
- expect(mockGetPost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {});
479
- });
480
-
481
- it('should handle not found errors', async () => {
482
- mockGetPost.mockRejectedValue(new Error('Post not found'));
483
-
484
- const tool = mockTools.get('ghost_get_post');
485
- const result = await tool.handler({ id: '507f1f77bcf86cd799439099' });
486
-
487
- expect(result.isError).toBe(true);
488
- expect(result.content[0].text).toContain('Post not found');
489
- });
490
-
491
- it('should handle errors from ghostService', async () => {
492
- mockGetPost.mockRejectedValue(new Error('Ghost API error'));
493
-
494
- const tool = mockTools.get('ghost_get_post');
495
- const result = await tool.handler({ slug: 'test' });
496
-
497
- expect(result.isError).toBe(true);
498
- expect(result.content[0].text).toContain('Ghost API error');
499
- });
500
-
501
- it('should return formatted JSON response', async () => {
502
- const mockPost = {
503
- id: '507f1f77bcf86cd799439011',
504
- uuid: 'uuid-123',
505
- title: 'Test Post',
506
- slug: 'test-post',
507
- html: '<p>Content</p>',
508
- status: 'published',
509
- created_at: '2025-12-10T00:00:00.000Z',
510
- updated_at: '2025-12-10T00:00:00.000Z',
511
- };
512
- mockGetPost.mockResolvedValue(mockPost);
513
-
514
- const tool = mockTools.get('ghost_get_post');
515
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
516
-
517
- expect(result.content).toBeDefined();
518
- expect(result.content[0].type).toBe('text');
519
- expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"');
520
- expect(result.content[0].text).toContain('"title": "Test Post"');
521
- expect(result.content[0].text).toContain('"status": "published"');
522
- });
523
-
524
- it('should handle validation error when neither id nor slug provided', async () => {
525
- const tool = mockTools.get('ghost_get_post');
526
- const result = await tool.handler({});
527
-
528
- expect(result.isError).toBe(true);
529
- expect(result.content[0].text).toContain('Either id or slug is required');
530
- });
531
- });
532
-
533
- describe('mcp_server_improved - ghost_update_post tool', () => {
534
- beforeEach(async () => {
535
- vi.clearAllMocks();
536
- // Don't clear mockTools - they're registered once on module load
537
- if (mockTools.size === 0) {
538
- await import('../mcp_server_improved.js');
539
- }
540
- });
541
-
542
- it('should register ghost_update_post tool', () => {
543
- expect(mockTools.has('ghost_update_post')).toBe(true);
544
- });
545
-
546
- it('should have correct schema with required id field', () => {
547
- const tool = mockTools.get('ghost_update_post');
548
- expect(tool).toBeDefined();
549
- expect(tool.description).toContain('Updates an existing post');
550
- expect(tool.schema).toBeDefined();
551
- // Zod schemas store field definitions in schema.shape
552
- expect(tool.schema.shape.id).toBeDefined();
553
- expect(tool.schema.shape.title).toBeDefined();
554
- expect(tool.schema.shape.html).toBeDefined();
555
- expect(tool.schema.shape.status).toBeDefined();
556
- expect(tool.schema.shape.tags).toBeDefined();
557
- expect(tool.schema.shape.feature_image).toBeDefined();
558
- expect(tool.schema.shape.feature_image_alt).toBeDefined();
559
- expect(tool.schema.shape.feature_image_caption).toBeDefined();
560
- expect(tool.schema.shape.meta_title).toBeDefined();
561
- expect(tool.schema.shape.meta_description).toBeDefined();
562
- expect(tool.schema.shape.published_at).toBeDefined();
563
- expect(tool.schema.shape.custom_excerpt).toBeDefined();
564
- });
565
-
566
- it('should update post title', async () => {
567
- const mockUpdatedPost = {
568
- id: '507f1f77bcf86cd799439011',
569
- title: 'Updated Title',
570
- slug: 'test-post',
571
- html: '<p>Content</p>',
572
- status: 'published',
573
- updated_at: '2025-12-10T12:00:00.000Z',
574
- };
575
- mockUpdatePost.mockResolvedValue(mockUpdatedPost);
576
-
577
- const tool = mockTools.get('ghost_update_post');
578
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'Updated Title' });
579
-
580
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
581
- title: 'Updated Title',
582
- });
583
- expect(result.content[0].text).toContain('"title": "Updated Title"');
584
- });
585
-
586
- it('should update post content', async () => {
587
- const mockUpdatedPost = {
588
- id: '507f1f77bcf86cd799439011',
589
- title: 'Test Post',
590
- html: '<p>Updated content</p>',
591
- status: 'published',
592
- updated_at: '2025-12-10T12:00:00.000Z',
593
- };
594
- mockUpdatePost.mockResolvedValue(mockUpdatedPost);
595
-
596
- const tool = mockTools.get('ghost_update_post');
597
- const result = await tool.handler({
598
- id: '507f1f77bcf86cd799439011',
599
- html: '<p>Updated content</p>',
600
- });
601
-
602
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
603
- html: '<p>Updated content</p>',
604
- });
605
- expect(result.content[0].text).toContain('Updated content');
606
- });
607
-
608
- it('should update post status', async () => {
609
- const mockUpdatedPost = {
610
- id: '507f1f77bcf86cd799439011',
611
- title: 'Test Post',
612
- html: '<p>Content</p>',
613
- status: 'published',
614
- updated_at: '2025-12-10T12:00:00.000Z',
615
- };
616
- mockUpdatePost.mockResolvedValue(mockUpdatedPost);
617
-
618
- const tool = mockTools.get('ghost_update_post');
619
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011', status: 'published' });
620
-
621
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
622
- status: 'published',
623
- });
624
- expect(result.content[0].text).toContain('"status": "published"');
625
- });
626
-
627
- it('should update post tags', async () => {
628
- const mockUpdatedPost = {
629
- id: '507f1f77bcf86cd799439011',
630
- title: 'Test Post',
631
- html: '<p>Content</p>',
632
- tags: [{ name: 'tech' }, { name: 'javascript' }],
633
- updated_at: '2025-12-10T12:00:00.000Z',
634
- };
635
- mockUpdatePost.mockResolvedValue(mockUpdatedPost);
636
-
637
- const tool = mockTools.get('ghost_update_post');
638
- const result = await tool.handler({
639
- id: '507f1f77bcf86cd799439011',
640
- tags: ['tech', 'javascript'],
641
- });
642
-
643
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
644
- tags: ['tech', 'javascript'],
645
- });
646
- expect(result.content[0].text).toContain('tech');
647
- expect(result.content[0].text).toContain('javascript');
648
- });
649
-
650
- it('should update post featured image', async () => {
651
- const mockUpdatedPost = {
652
- id: '507f1f77bcf86cd799439011',
653
- title: 'Test Post',
654
- feature_image: 'https://example.com/new-image.jpg',
655
- feature_image_alt: 'New image',
656
- updated_at: '2025-12-10T12:00:00.000Z',
657
- };
658
- mockUpdatePost.mockResolvedValue(mockUpdatedPost);
659
-
660
- const tool = mockTools.get('ghost_update_post');
661
- const result = await tool.handler({
662
- id: '507f1f77bcf86cd799439011',
663
- feature_image: 'https://example.com/new-image.jpg',
664
- feature_image_alt: 'New image',
665
- });
666
-
667
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
668
- feature_image: 'https://example.com/new-image.jpg',
669
- feature_image_alt: 'New image',
670
- });
671
- expect(result.content[0].text).toContain('new-image.jpg');
672
- });
673
-
674
- it('should update SEO meta fields', async () => {
675
- const mockUpdatedPost = {
676
- id: '507f1f77bcf86cd799439011',
677
- title: 'Test Post',
678
- meta_title: 'SEO Title',
679
- meta_description: 'SEO Description',
680
- updated_at: '2025-12-10T12:00:00.000Z',
681
- };
682
- mockUpdatePost.mockResolvedValue(mockUpdatedPost);
683
-
684
- const tool = mockTools.get('ghost_update_post');
685
- const result = await tool.handler({
686
- id: '507f1f77bcf86cd799439011',
687
- meta_title: 'SEO Title',
688
- meta_description: 'SEO Description',
689
- });
690
-
691
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
692
- meta_title: 'SEO Title',
693
- meta_description: 'SEO Description',
694
- });
695
- expect(result.content[0].text).toContain('SEO Title');
696
- expect(result.content[0].text).toContain('SEO Description');
697
- });
698
-
699
- it('should update multiple fields at once', async () => {
700
- const mockUpdatedPost = {
701
- id: '507f1f77bcf86cd799439011',
702
- title: 'Updated Title',
703
- html: '<p>Updated content</p>',
704
- status: 'published',
705
- tags: [{ name: 'tech' }],
706
- updated_at: '2025-12-10T12:00:00.000Z',
707
- };
708
- mockUpdatePost.mockResolvedValue(mockUpdatedPost);
709
-
710
- const tool = mockTools.get('ghost_update_post');
711
- const result = await tool.handler({
712
- id: '507f1f77bcf86cd799439011',
713
- title: 'Updated Title',
714
- html: '<p>Updated content</p>',
715
- status: 'published',
716
- tags: ['tech'],
717
- });
718
-
719
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
720
- title: 'Updated Title',
721
- html: '<p>Updated content</p>',
722
- status: 'published',
723
- tags: ['tech'],
724
- });
725
- expect(result.content[0].text).toContain('Updated Title');
726
- });
727
-
728
- it('should handle not found errors', async () => {
729
- mockUpdatePost.mockRejectedValue(new Error('Post not found'));
730
-
731
- const tool = mockTools.get('ghost_update_post');
732
- const result = await tool.handler({ id: '507f1f77bcf86cd799439099', title: 'New Title' });
733
-
734
- expect(result.isError).toBe(true);
735
- expect(result.content[0].text).toContain('Post not found');
736
- });
737
-
738
- it('should handle validation errors', async () => {
739
- mockUpdatePost.mockRejectedValue(new Error('Validation failed: Title is required'));
740
-
741
- const tool = mockTools.get('ghost_update_post');
742
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: '' });
743
-
744
- expect(result.isError).toBe(true);
745
- expect(result.content[0].text).toContain('Validation failed');
746
- });
747
-
748
- it('should handle Ghost API errors', async () => {
749
- mockUpdatePost.mockRejectedValue(new Error('Ghost API error: Server timeout'));
750
-
751
- const tool = mockTools.get('ghost_update_post');
752
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'Updated' });
753
-
754
- expect(result.isError).toBe(true);
755
- expect(result.content[0].text).toContain('Ghost API error');
756
- });
757
-
758
- it('should return formatted JSON response', async () => {
759
- const mockUpdatedPost = {
760
- id: '507f1f77bcf86cd799439011',
761
- uuid: 'uuid-123',
762
- title: 'Updated Post',
763
- slug: 'updated-post',
764
- html: '<p>Updated content</p>',
765
- status: 'published',
766
- created_at: '2025-12-09T00:00:00.000Z',
767
- updated_at: '2025-12-10T12:00:00.000Z',
768
- };
769
- mockUpdatePost.mockResolvedValue(mockUpdatedPost);
770
-
771
- const tool = mockTools.get('ghost_update_post');
772
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'Updated Post' });
773
-
774
- expect(result.content).toBeDefined();
775
- expect(result.content[0].type).toBe('text');
776
- expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"');
777
- expect(result.content[0].text).toContain('"title": "Updated Post"');
778
- expect(result.content[0].text).toContain('"status": "published"');
779
- });
780
- });
781
-
782
- describe('mcp_server_improved - ghost_delete_post tool', () => {
783
- beforeEach(async () => {
784
- vi.clearAllMocks();
785
- // Don't clear mockTools - they're registered once on module load
786
- if (mockTools.size === 0) {
787
- await import('../mcp_server_improved.js');
788
- }
789
- });
790
-
791
- it('should register ghost_delete_post tool', () => {
792
- expect(mockTools.has('ghost_delete_post')).toBe(true);
793
- });
794
-
795
- it('should have correct schema with required id field', () => {
796
- const tool = mockTools.get('ghost_delete_post');
797
- expect(tool).toBeDefined();
798
- expect(tool.description).toContain('Deletes a post');
799
- expect(tool.description).toContain('permanent');
800
- expect(tool.schema).toBeDefined();
801
- // Zod schemas store field definitions in schema.shape
802
- expect(tool.schema.shape.id).toBeDefined();
803
- });
804
-
805
- it('should delete post by ID', async () => {
806
- mockDeletePost.mockResolvedValue({ deleted: true });
807
-
808
- const tool = mockTools.get('ghost_delete_post');
809
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
810
-
811
- expect(mockDeletePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011');
812
- expect(result.content[0].text).toContain(
813
- 'Post 507f1f77bcf86cd799439011 has been successfully deleted'
814
- );
815
- expect(result.isError).toBeUndefined();
816
- });
817
-
818
- it('should handle not found errors', async () => {
819
- mockDeletePost.mockRejectedValue(new Error('Post not found'));
820
-
821
- const tool = mockTools.get('ghost_delete_post');
822
- const result = await tool.handler({ id: '507f1f77bcf86cd799439099' });
823
-
824
- expect(result.isError).toBe(true);
825
- expect(result.content[0].text).toContain('Post not found');
826
- });
827
-
828
- it('should handle Ghost API errors', async () => {
829
- mockDeletePost.mockRejectedValue(new Error('Ghost API error: Permission denied'));
830
-
831
- const tool = mockTools.get('ghost_delete_post');
832
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
833
-
834
- expect(result.isError).toBe(true);
835
- expect(result.content[0].text).toContain('Ghost API error');
836
- });
837
-
838
- it('should return success message on successful deletion', async () => {
839
- mockDeletePost.mockResolvedValue({ deleted: true });
840
-
841
- const tool = mockTools.get('ghost_delete_post');
842
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
843
-
844
- expect(result.content).toBeDefined();
845
- expect(result.content[0].type).toBe('text');
846
- expect(result.content[0].text).toBe(
847
- 'Post 507f1f77bcf86cd799439011 has been successfully deleted.'
848
- );
849
- });
850
-
851
- it('should handle network errors', async () => {
852
- mockDeletePost.mockRejectedValue(new Error('Network error: Connection refused'));
853
-
854
- const tool = mockTools.get('ghost_delete_post');
855
- const result = await tool.handler({ id: '507f1f77bcf86cd799439012' });
856
-
857
- expect(result.isError).toBe(true);
858
- expect(result.content[0].text).toContain('Network error');
859
- });
860
- });
861
-
862
- describe('mcp_server_improved - ghost_search_posts tool', () => {
863
- beforeEach(async () => {
864
- vi.clearAllMocks();
865
- // Don't clear mockTools - they're registered once on module load
866
- if (mockTools.size === 0) {
867
- await import('../mcp_server_improved.js');
868
- }
869
- });
870
-
871
- it('should register ghost_search_posts tool', () => {
872
- expect(mockTools.has('ghost_search_posts')).toBe(true);
873
- });
874
-
875
- it('should have correct schema with required query and optional parameters', () => {
876
- const tool = mockTools.get('ghost_search_posts');
877
- expect(tool).toBeDefined();
878
- expect(tool.description).toContain('Search');
879
- expect(tool.schema).toBeDefined();
880
- // Zod schemas store field definitions in schema.shape
881
- expect(tool.schema.shape.query).toBeDefined();
882
- expect(tool.schema.shape.status).toBeDefined();
883
- expect(tool.schema.shape.limit).toBeDefined();
884
- });
885
-
886
- it('should search posts with query only', async () => {
887
- const mockPosts = [
888
- { id: '1', title: 'JavaScript Tips', slug: 'javascript-tips', status: 'published' },
889
- { id: '2', title: 'JavaScript Tricks', slug: 'javascript-tricks', status: 'published' },
890
- ];
891
- mockSearchPosts.mockResolvedValue(mockPosts);
892
-
893
- const tool = mockTools.get('ghost_search_posts');
894
- const result = await tool.handler({ query: 'JavaScript' });
895
-
896
- expect(mockSearchPosts).toHaveBeenCalledWith('JavaScript', {});
897
- expect(result.content[0].text).toContain('JavaScript Tips');
898
- expect(result.content[0].text).toContain('JavaScript Tricks');
899
- });
900
-
901
- it('should search posts with query and status filter', async () => {
902
- const mockPosts = [
903
- { id: '1', title: 'Published Post', slug: 'published-post', status: 'published' },
904
- ];
905
- mockSearchPosts.mockResolvedValue(mockPosts);
906
-
907
- const tool = mockTools.get('ghost_search_posts');
908
- await tool.handler({ query: 'test', status: 'published' });
909
-
910
- expect(mockSearchPosts).toHaveBeenCalledWith('test', { status: 'published' });
911
- });
912
-
913
- it('should search posts with query and limit', async () => {
914
- const mockPosts = [{ id: '1', title: 'Test Post', slug: 'test-post' }];
915
- mockSearchPosts.mockResolvedValue(mockPosts);
916
-
917
- const tool = mockTools.get('ghost_search_posts');
918
- await tool.handler({ query: 'test', limit: 10 });
919
-
920
- expect(mockSearchPosts).toHaveBeenCalledWith('test', { limit: 10 });
921
- });
922
-
923
- it('should validate limit is between 1 and 50', () => {
924
- const tool = mockTools.get('ghost_search_posts');
925
- // Zod schemas store field definitions in schema.shape
926
- const shape = tool.schema.shape;
927
-
928
- expect(shape.limit).toBeDefined();
929
- expect(() => shape.limit.parse(0)).toThrow();
930
- expect(() => shape.limit.parse(51)).toThrow();
931
- expect(shape.limit.parse(25)).toBe(25);
932
- });
933
-
934
- it('should validate status enum values', () => {
935
- const tool = mockTools.get('ghost_search_posts');
936
- // Zod schemas store field definitions in schema.shape
937
- const shape = tool.schema.shape;
938
-
939
- expect(shape.status).toBeDefined();
940
- expect(() => shape.status.parse('invalid')).toThrow();
941
- expect(shape.status.parse('published')).toBe('published');
942
- expect(shape.status.parse('draft')).toBe('draft');
943
- expect(shape.status.parse('scheduled')).toBe('scheduled');
944
- expect(shape.status.parse('all')).toBe('all');
945
- });
946
-
947
- it('should pass all parameters combined', async () => {
948
- const mockPosts = [{ id: '1', title: 'Test Post' }];
949
- mockSearchPosts.mockResolvedValue(mockPosts);
950
-
951
- const tool = mockTools.get('ghost_search_posts');
952
- await tool.handler({
953
- query: 'JavaScript',
954
- status: 'published',
955
- limit: 20,
956
- });
957
-
958
- expect(mockSearchPosts).toHaveBeenCalledWith('JavaScript', {
959
- status: 'published',
960
- limit: 20,
961
- });
962
- });
963
-
964
- it('should handle errors from searchPosts', async () => {
965
- // Empty query is now caught by Zod validation
966
- const tool = mockTools.get('ghost_search_posts');
967
- const result = await tool.handler({ query: '' });
968
-
969
- expect(result.isError).toBe(true);
970
- expect(result.content[0].text).toContain('VALIDATION_ERROR');
971
- });
972
-
973
- it('should handle Ghost API errors', async () => {
974
- mockSearchPosts.mockRejectedValue(new Error('Ghost API error'));
975
-
976
- const tool = mockTools.get('ghost_search_posts');
977
- const result = await tool.handler({ query: 'test' });
978
-
979
- expect(result.isError).toBe(true);
980
- expect(result.content[0].text).toContain('Ghost API error');
981
- });
982
-
983
- it('should return formatted JSON response', async () => {
984
- const mockPosts = [
985
- {
986
- id: '1',
987
- title: 'Test Post',
988
- slug: 'test-post',
989
- html: '<p>Content</p>',
990
- status: 'published',
991
- },
992
- ];
993
- mockSearchPosts.mockResolvedValue(mockPosts);
994
-
995
- const tool = mockTools.get('ghost_search_posts');
996
- const result = await tool.handler({ query: 'Test' });
997
-
998
- expect(result.content).toBeDefined();
999
- expect(result.content[0].type).toBe('text');
1000
- expect(result.content[0].text).toContain('"id": "1"');
1001
- expect(result.content[0].text).toContain('"title": "Test Post"');
1002
- });
1003
-
1004
- it('should return empty array when no results found', async () => {
1005
- mockSearchPosts.mockResolvedValue([]);
1006
-
1007
- const tool = mockTools.get('ghost_search_posts');
1008
- const result = await tool.handler({ query: 'nonexistent' });
1009
-
1010
- expect(result.content[0].text).toBe('[]');
1011
- expect(result.isError).toBeUndefined();
1012
- });
1013
- });
1014
-
1015
- describe('ghost_get_tag', () => {
1016
- beforeEach(() => {
1017
- vi.clearAllMocks();
1018
- });
1019
-
1020
- it('should be registered as a tool', () => {
1021
- expect(mockTools.has('ghost_get_tag')).toBe(true);
1022
- const tool = mockTools.get('ghost_get_tag');
1023
- expect(tool.name).toBe('ghost_get_tag');
1024
- expect(tool.description).toBeDefined();
1025
- expect(tool.schema).toBeDefined();
1026
- expect(tool.handler).toBeDefined();
1027
- });
1028
-
1029
- it('should have correct schema with id and slug as optional', () => {
1030
- const tool = mockTools.get('ghost_get_tag');
1031
- // ghost_get_tag uses a refined schema, access via _def.schema.shape
1032
- const shape = tool.schema._def.schema.shape;
1033
- expect(shape.id).toBeDefined();
1034
- expect(shape.slug).toBeDefined();
1035
- expect(shape.include).toBeDefined();
1036
- });
1037
-
1038
- it('should retrieve tag by ID', async () => {
1039
- const mockTag = {
1040
- id: '507f1f77bcf86cd799439011',
1041
- name: 'Test Tag',
1042
- slug: 'test-tag',
1043
- description: 'A test tag',
1044
- };
1045
- mockGetTag.mockResolvedValue(mockTag);
1046
-
1047
- const tool = mockTools.get('ghost_get_tag');
1048
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
1049
-
1050
- expect(mockGetTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {});
1051
- expect(result.content).toBeDefined();
1052
- expect(result.content[0].type).toBe('text');
1053
- expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"');
1054
- expect(result.content[0].text).toContain('"name": "Test Tag"');
1055
- });
1056
-
1057
- it('should retrieve tag by slug', async () => {
1058
- const mockTag = {
1059
- id: '507f1f77bcf86cd799439011',
1060
- name: 'Test Tag',
1061
- slug: 'test-tag',
1062
- description: 'A test tag',
1063
- };
1064
- mockGetTag.mockResolvedValue(mockTag);
1065
-
1066
- const tool = mockTools.get('ghost_get_tag');
1067
- const result = await tool.handler({ slug: 'test-tag' });
1068
-
1069
- expect(mockGetTag).toHaveBeenCalledWith('slug/test-tag', {});
1070
- expect(result.content[0].text).toContain('"slug": "test-tag"');
1071
- });
1072
-
1073
- it('should support include parameter for post count', async () => {
1074
- const mockTag = {
1075
- id: '507f1f77bcf86cd799439011',
1076
- name: 'Test Tag',
1077
- slug: 'test-tag',
1078
- count: { posts: 5 },
1079
- };
1080
- mockGetTag.mockResolvedValue(mockTag);
1081
-
1082
- const tool = mockTools.get('ghost_get_tag');
1083
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011', include: 'count.posts' });
1084
-
1085
- expect(mockGetTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { include: 'count.posts' });
1086
- expect(result.content[0].text).toContain('"count"');
1087
- });
1088
-
1089
- it('should return error when neither id nor slug provided', async () => {
1090
- const tool = mockTools.get('ghost_get_tag');
1091
- const result = await tool.handler({});
1092
-
1093
- expect(result.content[0].type).toBe('text');
1094
- expect(result.content[0].text).toContain('Either id or slug is required');
1095
- expect(result.isError).toBe(true);
1096
- });
1097
-
1098
- it('should handle not found error', async () => {
1099
- mockGetTag.mockRejectedValue(new Error('Tag not found'));
1100
-
1101
- const tool = mockTools.get('ghost_get_tag');
1102
- const result = await tool.handler({ id: '507f1f77bcf86cd799439099' });
1103
-
1104
- expect(result.isError).toBe(true);
1105
- expect(result.content[0].text).toContain('Tag not found');
1106
- });
1107
- });
1108
-
1109
- describe('ghost_update_tag', () => {
1110
- beforeEach(() => {
1111
- vi.clearAllMocks();
1112
- });
1113
-
1114
- it('should be registered as a tool', () => {
1115
- expect(mockTools.has('ghost_update_tag')).toBe(true);
1116
- const tool = mockTools.get('ghost_update_tag');
1117
- expect(tool.name).toBe('ghost_update_tag');
1118
- expect(tool.description).toBeDefined();
1119
- expect(tool.schema).toBeDefined();
1120
- expect(tool.handler).toBeDefined();
1121
- });
1122
-
1123
- it('should have correct schema with all update fields', () => {
1124
- const tool = mockTools.get('ghost_update_tag');
1125
- // Zod schemas store field definitions in schema.shape
1126
- expect(tool.schema.shape.id).toBeDefined();
1127
- expect(tool.schema.shape.name).toBeDefined();
1128
- expect(tool.schema.shape.slug).toBeDefined();
1129
- expect(tool.schema.shape.description).toBeDefined();
1130
- expect(tool.schema.shape.feature_image).toBeDefined();
1131
- expect(tool.schema.shape.meta_title).toBeDefined();
1132
- expect(tool.schema.shape.meta_description).toBeDefined();
1133
- });
1134
-
1135
- it('should update tag name', async () => {
1136
- const mockUpdatedTag = {
1137
- id: '507f1f77bcf86cd799439011',
1138
- name: 'Updated Tag',
1139
- slug: 'updated-tag',
1140
- };
1141
- mockUpdateTag.mockResolvedValue(mockUpdatedTag);
1142
-
1143
- const tool = mockTools.get('ghost_update_tag');
1144
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011', name: 'Updated Tag' });
1145
-
1146
- expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { name: 'Updated Tag' });
1147
- expect(result.content[0].text).toContain('"name": "Updated Tag"');
1148
- });
1149
-
1150
- it('should update tag description', async () => {
1151
- const mockUpdatedTag = {
1152
- id: '507f1f77bcf86cd799439011',
1153
- name: 'Test Tag',
1154
- description: 'New description',
1155
- };
1156
- mockUpdateTag.mockResolvedValue(mockUpdatedTag);
1157
-
1158
- const tool = mockTools.get('ghost_update_tag');
1159
- const result = await tool.handler({
1160
- id: '507f1f77bcf86cd799439011',
1161
- description: 'New description',
1162
- });
1163
-
1164
- expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
1165
- description: 'New description',
1166
- });
1167
- expect(result.content[0].text).toContain('"description": "New description"');
1168
- });
1169
-
1170
- it('should update multiple fields at once', async () => {
1171
- const mockUpdatedTag = {
1172
- id: '507f1f77bcf86cd799439011',
1173
- name: 'Updated Tag',
1174
- slug: 'updated-tag',
1175
- description: 'Updated description',
1176
- meta_title: 'Updated Meta',
1177
- };
1178
- mockUpdateTag.mockResolvedValue(mockUpdatedTag);
1179
-
1180
- const tool = mockTools.get('ghost_update_tag');
1181
- await tool.handler({
1182
- id: '507f1f77bcf86cd799439011',
1183
- name: 'Updated Tag',
1184
- description: 'Updated description',
1185
- meta_title: 'Updated Meta',
1186
- });
1187
-
1188
- expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
1189
- name: 'Updated Tag',
1190
- description: 'Updated description',
1191
- meta_title: 'Updated Meta',
1192
- });
1193
- });
1194
-
1195
- it('should update tag feature image', async () => {
1196
- const mockUpdatedTag = {
1197
- id: '507f1f77bcf86cd799439011',
1198
- name: 'Test Tag',
1199
- feature_image: 'https://example.com/image.jpg',
1200
- };
1201
- mockUpdateTag.mockResolvedValue(mockUpdatedTag);
1202
-
1203
- const tool = mockTools.get('ghost_update_tag');
1204
- await tool.handler({
1205
- id: '507f1f77bcf86cd799439011',
1206
- feature_image: 'https://example.com/image.jpg',
1207
- });
1208
-
1209
- expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
1210
- feature_image: 'https://example.com/image.jpg',
1211
- });
1212
- });
1213
-
1214
- it('should return error when id is missing', async () => {
1215
- const tool = mockTools.get('ghost_update_tag');
1216
- const result = await tool.handler({ name: 'Test' });
1217
-
1218
- expect(result.isError).toBe(true);
1219
- expect(result.content[0].text).toContain('VALIDATION_ERROR');
1220
- });
1221
-
1222
- it('should handle validation error', async () => {
1223
- mockUpdateTag.mockRejectedValue(new Error('Validation failed'));
1224
-
1225
- const tool = mockTools.get('ghost_update_tag');
1226
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011', name: '' });
1227
-
1228
- expect(result.isError).toBe(true);
1229
- expect(result.content[0].text).toContain('Validation failed');
1230
- });
1231
-
1232
- it('should handle not found error', async () => {
1233
- mockUpdateTag.mockRejectedValue(new Error('Tag not found'));
1234
-
1235
- const tool = mockTools.get('ghost_update_tag');
1236
- const result = await tool.handler({ id: '507f1f77bcf86cd799439099', name: 'Test' });
1237
-
1238
- expect(result.isError).toBe(true);
1239
- expect(result.content[0].text).toContain('Tag not found');
1240
- });
1241
- });
1242
-
1243
- describe('ghost_delete_tag', () => {
1244
- beforeEach(() => {
1245
- vi.clearAllMocks();
1246
- });
1247
-
1248
- it('should be registered as a tool', () => {
1249
- expect(mockTools.has('ghost_delete_tag')).toBe(true);
1250
- const tool = mockTools.get('ghost_delete_tag');
1251
- expect(tool.name).toBe('ghost_delete_tag');
1252
- expect(tool.description).toBeDefined();
1253
- expect(tool.schema).toBeDefined();
1254
- expect(tool.handler).toBeDefined();
1255
- });
1256
-
1257
- it('should have correct schema with id field', () => {
1258
- const tool = mockTools.get('ghost_delete_tag');
1259
- // Zod schemas store field definitions in schema.shape
1260
- expect(tool.schema.shape.id).toBeDefined();
1261
- });
1262
-
1263
- it('should delete tag successfully', async () => {
1264
- mockDeleteTag.mockResolvedValue({ success: true });
1265
-
1266
- const tool = mockTools.get('ghost_delete_tag');
1267
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
1268
-
1269
- expect(mockDeleteTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011');
1270
- expect(result.content[0].text).toContain('successfully deleted');
1271
- expect(result.isError).toBeUndefined();
1272
- });
1273
-
1274
- it('should return error when id is missing', async () => {
1275
- const tool = mockTools.get('ghost_delete_tag');
1276
- const result = await tool.handler({});
1277
-
1278
- expect(result.isError).toBe(true);
1279
- expect(result.content[0].text).toContain('VALIDATION_ERROR');
1280
- });
1281
-
1282
- it('should handle not found error', async () => {
1283
- mockDeleteTag.mockRejectedValue(new Error('Tag not found'));
1284
-
1285
- const tool = mockTools.get('ghost_delete_tag');
1286
- const result = await tool.handler({ id: '507f1f77bcf86cd799439099' });
1287
-
1288
- expect(result.isError).toBe(true);
1289
- expect(result.content[0].text).toContain('Tag not found');
1290
- });
1291
-
1292
- it('should handle deletion error', async () => {
1293
- mockDeleteTag.mockRejectedValue(new Error('Failed to delete tag'));
1294
-
1295
- const tool = mockTools.get('ghost_delete_tag');
1296
- const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
1297
-
1298
- expect(result.isError).toBe(true);
1299
- expect(result.content[0].text).toContain('Failed to delete tag');
1300
- });
1301
- });