@jgardner04/ghost-mcp-server 1.13.2 → 1.13.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.
Files changed (33) hide show
  1. package/package.json +5 -13
  2. package/src/__tests__/mcp_server.test.js +204 -117
  3. package/src/__tests__/mcp_server_pages.test.js +32 -18
  4. package/src/config/mcp-config.js +1 -1
  5. package/src/controllers/__tests__/tagController.test.js +12 -8
  6. package/src/controllers/tagController.js +2 -2
  7. package/src/errors/__tests__/index.test.js +3 -3
  8. package/src/errors/index.js +1 -1
  9. package/src/index.js +1 -1
  10. package/src/mcp_server.js +35 -31
  11. package/src/schemas/__tests__/postSchemas.test.js +19 -0
  12. package/src/schemas/__tests__/tagSchemas.test.js +1 -1
  13. package/src/schemas/common.js +2 -2
  14. package/src/schemas/memberSchemas.js +20 -8
  15. package/src/schemas/newsletterSchemas.js +10 -10
  16. package/src/schemas/pageSchemas.js +16 -11
  17. package/src/schemas/postSchemas.js +22 -15
  18. package/src/schemas/tagSchemas.js +12 -7
  19. package/src/schemas/tierSchemas.js +17 -8
  20. package/src/services/__tests__/ghostServiceImproved.members.test.js +7 -2
  21. package/src/services/__tests__/ghostServiceImproved.newsletters.test.js +10 -6
  22. package/src/services/__tests__/ghostServiceImproved.pages.test.js +4 -4
  23. package/src/services/__tests__/ghostServiceImproved.posts.test.js +4 -4
  24. package/src/services/__tests__/ghostServiceImproved.tags.test.js +2 -2
  25. package/src/services/__tests__/ghostServiceImproved.tiers.test.js +9 -14
  26. package/src/services/__tests__/memberService.test.js +0 -28
  27. package/src/services/__tests__/tierService.test.js +0 -28
  28. package/src/services/ghostServiceImproved.js +69 -217
  29. package/src/services/imageProcessingService.js +1 -1
  30. package/src/services/memberService.js +0 -13
  31. package/src/services/tierService.js +0 -13
  32. package/src/utils/__tests__/nqlSanitizer.test.js +38 -0
  33. package/src/utils/nqlSanitizer.js +11 -0
@@ -232,7 +232,7 @@ describe('mcp_server - ghost_get_posts tool', () => {
232
232
  const tool = mockTools.get('ghost_get_posts');
233
233
  const result = await tool.handler({});
234
234
 
235
- expect(mockGetPosts).toHaveBeenCalledWith({});
235
+ expect(mockGetPosts).toHaveBeenCalledWith(expect.objectContaining({}));
236
236
  expect(result.content[0].text).toContain('Post 1');
237
237
  expect(result.content[0].text).toContain('Post 2');
238
238
  });
@@ -244,7 +244,7 @@ describe('mcp_server - ghost_get_posts tool', () => {
244
244
  const tool = mockTools.get('ghost_get_posts');
245
245
  await tool.handler({ limit: 10, page: 2 });
246
246
 
247
- expect(mockGetPosts).toHaveBeenCalledWith({ limit: 10, page: 2 });
247
+ expect(mockGetPosts).toHaveBeenCalledWith(expect.objectContaining({ limit: 10, page: 2 }));
248
248
  });
249
249
 
250
250
  it('should validate limit is between 1 and 100', () => {
@@ -276,7 +276,7 @@ describe('mcp_server - ghost_get_posts tool', () => {
276
276
  const tool = mockTools.get('ghost_get_posts');
277
277
  await tool.handler({ status: 'published' });
278
278
 
279
- expect(mockGetPosts).toHaveBeenCalledWith({ status: 'published' });
279
+ expect(mockGetPosts).toHaveBeenCalledWith(expect.objectContaining({ status: 'published' }));
280
280
  });
281
281
 
282
282
  it('should validate status enum values', () => {
@@ -306,7 +306,7 @@ describe('mcp_server - ghost_get_posts tool', () => {
306
306
  const tool = mockTools.get('ghost_get_posts');
307
307
  await tool.handler({ include: 'tags,authors' });
308
308
 
309
- expect(mockGetPosts).toHaveBeenCalledWith({ include: 'tags,authors' });
309
+ expect(mockGetPosts).toHaveBeenCalledWith(expect.objectContaining({ include: 'tags,authors' }));
310
310
  });
311
311
 
312
312
  it('should pass filter parameter (NQL)', async () => {
@@ -316,7 +316,7 @@ describe('mcp_server - ghost_get_posts tool', () => {
316
316
  const tool = mockTools.get('ghost_get_posts');
317
317
  await tool.handler({ filter: 'featured:true' });
318
318
 
319
- expect(mockGetPosts).toHaveBeenCalledWith({ filter: 'featured:true' });
319
+ expect(mockGetPosts).toHaveBeenCalledWith(expect.objectContaining({ filter: 'featured:true' }));
320
320
  });
321
321
 
322
322
  it('should pass order parameter', async () => {
@@ -329,7 +329,9 @@ describe('mcp_server - ghost_get_posts tool', () => {
329
329
  const tool = mockTools.get('ghost_get_posts');
330
330
  await tool.handler({ order: 'published_at DESC' });
331
331
 
332
- expect(mockGetPosts).toHaveBeenCalledWith({ order: 'published_at DESC' });
332
+ expect(mockGetPosts).toHaveBeenCalledWith(
333
+ expect.objectContaining({ order: 'published_at DESC' })
334
+ );
333
335
  });
334
336
 
335
337
  it('should pass fields parameter', async () => {
@@ -339,7 +341,7 @@ describe('mcp_server - ghost_get_posts tool', () => {
339
341
  const tool = mockTools.get('ghost_get_posts');
340
342
  await tool.handler({ fields: 'id,title,slug' });
341
343
 
342
- expect(mockGetPosts).toHaveBeenCalledWith({ fields: 'id,title,slug' });
344
+ expect(mockGetPosts).toHaveBeenCalledWith(expect.objectContaining({ fields: 'id,title,slug' }));
343
345
  });
344
346
 
345
347
  it('should pass formats parameter', async () => {
@@ -349,7 +351,9 @@ describe('mcp_server - ghost_get_posts tool', () => {
349
351
  const tool = mockTools.get('ghost_get_posts');
350
352
  await tool.handler({ formats: 'html,plaintext' });
351
353
 
352
- expect(mockGetPosts).toHaveBeenCalledWith({ formats: 'html,plaintext' });
354
+ expect(mockGetPosts).toHaveBeenCalledWith(
355
+ expect.objectContaining({ formats: 'html,plaintext' })
356
+ );
353
357
  });
354
358
 
355
359
  it('should pass both fields and formats parameters', async () => {
@@ -359,7 +363,9 @@ describe('mcp_server - ghost_get_posts tool', () => {
359
363
  const tool = mockTools.get('ghost_get_posts');
360
364
  await tool.handler({ fields: 'id,title', formats: 'html' });
361
365
 
362
- expect(mockGetPosts).toHaveBeenCalledWith({ fields: 'id,title', formats: 'html' });
366
+ expect(mockGetPosts).toHaveBeenCalledWith(
367
+ expect.objectContaining({ fields: 'id,title', formats: 'html' })
368
+ );
363
369
  });
364
370
 
365
371
  it('should pass all parameters combined', async () => {
@@ -378,16 +384,18 @@ describe('mcp_server - ghost_get_posts tool', () => {
378
384
  formats: 'html,plaintext',
379
385
  });
380
386
 
381
- expect(mockGetPosts).toHaveBeenCalledWith({
382
- limit: 20,
383
- page: 1,
384
- status: 'published',
385
- include: 'tags,authors',
386
- filter: 'featured:true',
387
- order: 'published_at DESC',
388
- fields: 'id,title,slug',
389
- formats: 'html,plaintext',
390
- });
387
+ expect(mockGetPosts).toHaveBeenCalledWith(
388
+ expect.objectContaining({
389
+ limit: 20,
390
+ page: 1,
391
+ status: 'published',
392
+ include: 'tags,authors',
393
+ filter: 'featured:true',
394
+ order: 'published_at DESC',
395
+ fields: 'id,title,slug',
396
+ formats: 'html,plaintext',
397
+ })
398
+ );
391
399
  });
392
400
 
393
401
  it('should handle errors from ghostService', async () => {
@@ -461,7 +469,7 @@ describe('mcp_server - ghost_get_tags tool', () => {
461
469
  const tool = mockTools.get('ghost_get_tags');
462
470
  const result = await tool.handler({});
463
471
 
464
- expect(mockGetTags).toHaveBeenCalledWith({});
472
+ expect(mockGetTags).toHaveBeenCalledWith(expect.objectContaining({}));
465
473
  expect(result.content[0].text).toContain('Tag 1');
466
474
  expect(result.content[0].text).toContain('Tag 2');
467
475
  });
@@ -473,10 +481,12 @@ describe('mcp_server - ghost_get_tags tool', () => {
473
481
  const tool = mockTools.get('ghost_get_tags');
474
482
  await tool.handler({ limit: 10, page: 2 });
475
483
 
476
- expect(mockGetTags).toHaveBeenCalledWith({
477
- limit: 10,
478
- page: 2,
479
- });
484
+ expect(mockGetTags).toHaveBeenCalledWith(
485
+ expect.objectContaining({
486
+ limit: 10,
487
+ page: 2,
488
+ })
489
+ );
480
490
  });
481
491
 
482
492
  it('should pass order parameter', async () => {
@@ -486,9 +496,11 @@ describe('mcp_server - ghost_get_tags tool', () => {
486
496
  const tool = mockTools.get('ghost_get_tags');
487
497
  await tool.handler({ order: 'name ASC' });
488
498
 
489
- expect(mockGetTags).toHaveBeenCalledWith({
490
- order: 'name ASC',
491
- });
499
+ expect(mockGetTags).toHaveBeenCalledWith(
500
+ expect.objectContaining({
501
+ order: 'name ASC',
502
+ })
503
+ );
492
504
  });
493
505
 
494
506
  it('should pass include parameter', async () => {
@@ -498,9 +510,11 @@ describe('mcp_server - ghost_get_tags tool', () => {
498
510
  const tool = mockTools.get('ghost_get_tags');
499
511
  await tool.handler({ include: 'count.posts' });
500
512
 
501
- expect(mockGetTags).toHaveBeenCalledWith({
502
- include: 'count.posts',
503
- });
513
+ expect(mockGetTags).toHaveBeenCalledWith(
514
+ expect.objectContaining({
515
+ include: 'count.posts',
516
+ })
517
+ );
504
518
  });
505
519
 
506
520
  it('should filter by name parameter', async () => {
@@ -510,9 +524,11 @@ describe('mcp_server - ghost_get_tags tool', () => {
510
524
  const tool = mockTools.get('ghost_get_tags');
511
525
  await tool.handler({ name: 'Test Tag' });
512
526
 
513
- expect(mockGetTags).toHaveBeenCalledWith({
514
- filter: "name:'Test Tag'",
515
- });
527
+ expect(mockGetTags).toHaveBeenCalledWith(
528
+ expect.objectContaining({
529
+ filter: "name:'Test Tag'",
530
+ })
531
+ );
516
532
  });
517
533
 
518
534
  it('should filter by slug parameter', async () => {
@@ -522,9 +538,11 @@ describe('mcp_server - ghost_get_tags tool', () => {
522
538
  const tool = mockTools.get('ghost_get_tags');
523
539
  await tool.handler({ slug: 'test-tag' });
524
540
 
525
- expect(mockGetTags).toHaveBeenCalledWith({
526
- filter: "slug:'test-tag'",
527
- });
541
+ expect(mockGetTags).toHaveBeenCalledWith(
542
+ expect.objectContaining({
543
+ filter: "slug:'test-tag'",
544
+ })
545
+ );
528
546
  });
529
547
 
530
548
  it('should filter by visibility parameter', async () => {
@@ -534,9 +552,11 @@ describe('mcp_server - ghost_get_tags tool', () => {
534
552
  const tool = mockTools.get('ghost_get_tags');
535
553
  await tool.handler({ visibility: 'public' });
536
554
 
537
- expect(mockGetTags).toHaveBeenCalledWith({
538
- filter: "visibility:'public'",
539
- });
555
+ expect(mockGetTags).toHaveBeenCalledWith(
556
+ expect.objectContaining({
557
+ filter: "visibility:'public'",
558
+ })
559
+ );
540
560
  });
541
561
 
542
562
  it('should escape single quotes in name parameter', async () => {
@@ -546,9 +566,11 @@ describe('mcp_server - ghost_get_tags tool', () => {
546
566
  const tool = mockTools.get('ghost_get_tags');
547
567
  await tool.handler({ name: "O'Reilly" });
548
568
 
549
- expect(mockGetTags).toHaveBeenCalledWith({
550
- filter: "name:'O''Reilly'",
551
- });
569
+ expect(mockGetTags).toHaveBeenCalledWith(
570
+ expect.objectContaining({
571
+ filter: "name:'O''Reilly'",
572
+ })
573
+ );
552
574
  });
553
575
 
554
576
  it('should escape single quotes in slug parameter', async () => {
@@ -558,9 +580,11 @@ describe('mcp_server - ghost_get_tags tool', () => {
558
580
  const tool = mockTools.get('ghost_get_tags');
559
581
  await tool.handler({ slug: "test'slug" });
560
582
 
561
- expect(mockGetTags).toHaveBeenCalledWith({
562
- filter: "slug:'test''slug'",
563
- });
583
+ expect(mockGetTags).toHaveBeenCalledWith(
584
+ expect.objectContaining({
585
+ filter: "slug:'test''slug'",
586
+ })
587
+ );
564
588
  });
565
589
 
566
590
  it('should combine multiple filter parameters', async () => {
@@ -570,9 +594,11 @@ describe('mcp_server - ghost_get_tags tool', () => {
570
594
  const tool = mockTools.get('ghost_get_tags');
571
595
  await tool.handler({ name: 'News', visibility: 'public' });
572
596
 
573
- expect(mockGetTags).toHaveBeenCalledWith({
574
- filter: "name:'News'+visibility:'public'",
575
- });
597
+ expect(mockGetTags).toHaveBeenCalledWith(
598
+ expect.objectContaining({
599
+ filter: "name:'News'+visibility:'public'",
600
+ })
601
+ );
576
602
  });
577
603
 
578
604
  it('should combine individual filters with custom filter parameter', async () => {
@@ -582,9 +608,11 @@ describe('mcp_server - ghost_get_tags tool', () => {
582
608
  const tool = mockTools.get('ghost_get_tags');
583
609
  await tool.handler({ name: 'News', filter: 'featured:true' });
584
610
 
585
- expect(mockGetTags).toHaveBeenCalledWith({
586
- filter: "name:'News'+featured:true",
587
- });
611
+ expect(mockGetTags).toHaveBeenCalledWith(
612
+ expect.objectContaining({
613
+ filter: "name:'News'+featured:true",
614
+ })
615
+ );
588
616
  });
589
617
 
590
618
  it('should pass all parameters combined', async () => {
@@ -601,13 +629,15 @@ describe('mcp_server - ghost_get_tags tool', () => {
601
629
  visibility: 'public',
602
630
  });
603
631
 
604
- expect(mockGetTags).toHaveBeenCalledWith({
605
- limit: 20,
606
- page: 1,
607
- order: 'name ASC',
608
- include: 'count.posts',
609
- filter: "name:'News'+visibility:'public'",
610
- });
632
+ expect(mockGetTags).toHaveBeenCalledWith(
633
+ expect.objectContaining({
634
+ limit: 20,
635
+ page: 1,
636
+ order: 'name ASC',
637
+ include: 'count.posts',
638
+ filter: "name:'News'+visibility:'public'",
639
+ })
640
+ );
611
641
  });
612
642
 
613
643
  it('should handle service errors', async () => {
@@ -652,8 +682,8 @@ describe('mcp_server - ghost_get_post tool', () => {
652
682
  expect(tool).toBeDefined();
653
683
  expect(tool.description).toContain('post');
654
684
  expect(tool.schema).toBeDefined();
655
- // ghost_get_post uses a refined schema, access via _def.schema.shape
656
- const shape = tool.schema._def.schema.shape;
685
+ // In Zod v4, refined schemas expose .shape directly
686
+ const shape = tool.schema.shape;
657
687
  expect(shape.id).toBeDefined();
658
688
  expect(shape.slug).toBeDefined();
659
689
  expect(shape.include).toBeDefined();
@@ -672,7 +702,10 @@ describe('mcp_server - ghost_get_post tool', () => {
672
702
  const tool = mockTools.get('ghost_get_post');
673
703
  const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
674
704
 
675
- expect(mockGetPost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {});
705
+ expect(mockGetPost).toHaveBeenCalledWith(
706
+ '507f1f77bcf86cd799439011',
707
+ expect.objectContaining({})
708
+ );
676
709
  expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"');
677
710
  expect(result.content[0].text).toContain('"title": "Test Post"');
678
711
  });
@@ -690,7 +723,7 @@ describe('mcp_server - ghost_get_post tool', () => {
690
723
  const tool = mockTools.get('ghost_get_post');
691
724
  const result = await tool.handler({ slug: 'test-post' });
692
725
 
693
- expect(mockGetPost).toHaveBeenCalledWith('slug/test-post', {});
726
+ expect(mockGetPost).toHaveBeenCalledWith('slug/test-post', expect.objectContaining({}));
694
727
  expect(result.content[0].text).toContain('"title": "Test Post"');
695
728
  });
696
729
 
@@ -706,9 +739,12 @@ describe('mcp_server - ghost_get_post tool', () => {
706
739
  const tool = mockTools.get('ghost_get_post');
707
740
  await tool.handler({ id: '507f1f77bcf86cd799439011', include: 'tags,authors' });
708
741
 
709
- expect(mockGetPost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
710
- include: 'tags,authors',
711
- });
742
+ expect(mockGetPost).toHaveBeenCalledWith(
743
+ '507f1f77bcf86cd799439011',
744
+ expect.objectContaining({
745
+ include: 'tags,authors',
746
+ })
747
+ );
712
748
  });
713
749
 
714
750
  it('should pass include parameter with slug', async () => {
@@ -723,7 +759,10 @@ describe('mcp_server - ghost_get_post tool', () => {
723
759
  const tool = mockTools.get('ghost_get_post');
724
760
  await tool.handler({ slug: 'test-post', include: 'tags' });
725
761
 
726
- expect(mockGetPost).toHaveBeenCalledWith('slug/test-post', { include: 'tags' });
762
+ expect(mockGetPost).toHaveBeenCalledWith(
763
+ 'slug/test-post',
764
+ expect.objectContaining({ include: 'tags' })
765
+ );
727
766
  });
728
767
 
729
768
  it('should prefer ID over slug when both provided', async () => {
@@ -733,7 +772,10 @@ describe('mcp_server - ghost_get_post tool', () => {
733
772
  const tool = mockTools.get('ghost_get_post');
734
773
  await tool.handler({ id: '507f1f77bcf86cd799439011', slug: 'wrong-slug' });
735
774
 
736
- expect(mockGetPost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {});
775
+ expect(mockGetPost).toHaveBeenCalledWith(
776
+ '507f1f77bcf86cd799439011',
777
+ expect.objectContaining({})
778
+ );
737
779
  });
738
780
 
739
781
  it('should handle not found errors', async () => {
@@ -835,9 +877,12 @@ describe('mcp_server - ghost_update_post tool', () => {
835
877
  const tool = mockTools.get('ghost_update_post');
836
878
  const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'Updated Title' });
837
879
 
838
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
839
- title: 'Updated Title',
840
- });
880
+ expect(mockUpdatePost).toHaveBeenCalledWith(
881
+ '507f1f77bcf86cd799439011',
882
+ expect.objectContaining({
883
+ title: 'Updated Title',
884
+ })
885
+ );
841
886
  expect(result.content[0].text).toContain('"title": "Updated Title"');
842
887
  });
843
888
 
@@ -857,9 +902,12 @@ describe('mcp_server - ghost_update_post tool', () => {
857
902
  html: '<p>Updated content</p>',
858
903
  });
859
904
 
860
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
861
- html: '<p>Updated content</p>',
862
- });
905
+ expect(mockUpdatePost).toHaveBeenCalledWith(
906
+ '507f1f77bcf86cd799439011',
907
+ expect.objectContaining({
908
+ html: '<p>Updated content</p>',
909
+ })
910
+ );
863
911
  expect(result.content[0].text).toContain('Updated content');
864
912
  });
865
913
 
@@ -876,9 +924,12 @@ describe('mcp_server - ghost_update_post tool', () => {
876
924
  const tool = mockTools.get('ghost_update_post');
877
925
  const result = await tool.handler({ id: '507f1f77bcf86cd799439011', status: 'published' });
878
926
 
879
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
880
- status: 'published',
881
- });
927
+ expect(mockUpdatePost).toHaveBeenCalledWith(
928
+ '507f1f77bcf86cd799439011',
929
+ expect.objectContaining({
930
+ status: 'published',
931
+ })
932
+ );
882
933
  expect(result.content[0].text).toContain('"status": "published"');
883
934
  });
884
935
 
@@ -898,9 +949,12 @@ describe('mcp_server - ghost_update_post tool', () => {
898
949
  tags: ['tech', 'javascript'],
899
950
  });
900
951
 
901
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
902
- tags: ['tech', 'javascript'],
903
- });
952
+ expect(mockUpdatePost).toHaveBeenCalledWith(
953
+ '507f1f77bcf86cd799439011',
954
+ expect.objectContaining({
955
+ tags: ['tech', 'javascript'],
956
+ })
957
+ );
904
958
  expect(result.content[0].text).toContain('tech');
905
959
  expect(result.content[0].text).toContain('javascript');
906
960
  });
@@ -922,10 +976,13 @@ describe('mcp_server - ghost_update_post tool', () => {
922
976
  feature_image_alt: 'New image',
923
977
  });
924
978
 
925
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
926
- feature_image: 'https://example.com/new-image.jpg',
927
- feature_image_alt: 'New image',
928
- });
979
+ expect(mockUpdatePost).toHaveBeenCalledWith(
980
+ '507f1f77bcf86cd799439011',
981
+ expect.objectContaining({
982
+ feature_image: 'https://example.com/new-image.jpg',
983
+ feature_image_alt: 'New image',
984
+ })
985
+ );
929
986
  expect(result.content[0].text).toContain('new-image.jpg');
930
987
  });
931
988
 
@@ -946,10 +1003,13 @@ describe('mcp_server - ghost_update_post tool', () => {
946
1003
  meta_description: 'SEO Description',
947
1004
  });
948
1005
 
949
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
950
- meta_title: 'SEO Title',
951
- meta_description: 'SEO Description',
952
- });
1006
+ expect(mockUpdatePost).toHaveBeenCalledWith(
1007
+ '507f1f77bcf86cd799439011',
1008
+ expect.objectContaining({
1009
+ meta_title: 'SEO Title',
1010
+ meta_description: 'SEO Description',
1011
+ })
1012
+ );
953
1013
  expect(result.content[0].text).toContain('SEO Title');
954
1014
  expect(result.content[0].text).toContain('SEO Description');
955
1015
  });
@@ -974,12 +1034,15 @@ describe('mcp_server - ghost_update_post tool', () => {
974
1034
  tags: ['tech'],
975
1035
  });
976
1036
 
977
- expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
978
- title: 'Updated Title',
979
- html: '<p>Updated content</p>',
980
- status: 'published',
981
- tags: ['tech'],
982
- });
1037
+ expect(mockUpdatePost).toHaveBeenCalledWith(
1038
+ '507f1f77bcf86cd799439011',
1039
+ expect.objectContaining({
1040
+ title: 'Updated Title',
1041
+ html: '<p>Updated content</p>',
1042
+ status: 'published',
1043
+ tags: ['tech'],
1044
+ })
1045
+ );
983
1046
  expect(result.content[0].text).toContain('Updated Title');
984
1047
  });
985
1048
 
@@ -1151,7 +1214,7 @@ describe('mcp_server - ghost_search_posts tool', () => {
1151
1214
  const tool = mockTools.get('ghost_search_posts');
1152
1215
  const result = await tool.handler({ query: 'JavaScript' });
1153
1216
 
1154
- expect(mockSearchPosts).toHaveBeenCalledWith('JavaScript', {});
1217
+ expect(mockSearchPosts).toHaveBeenCalledWith('JavaScript', expect.objectContaining({}));
1155
1218
  expect(result.content[0].text).toContain('JavaScript Tips');
1156
1219
  expect(result.content[0].text).toContain('JavaScript Tricks');
1157
1220
  });
@@ -1165,7 +1228,10 @@ describe('mcp_server - ghost_search_posts tool', () => {
1165
1228
  const tool = mockTools.get('ghost_search_posts');
1166
1229
  await tool.handler({ query: 'test', status: 'published' });
1167
1230
 
1168
- expect(mockSearchPosts).toHaveBeenCalledWith('test', { status: 'published' });
1231
+ expect(mockSearchPosts).toHaveBeenCalledWith(
1232
+ 'test',
1233
+ expect.objectContaining({ status: 'published' })
1234
+ );
1169
1235
  });
1170
1236
 
1171
1237
  it('should search posts with query and limit', async () => {
@@ -1175,7 +1241,7 @@ describe('mcp_server - ghost_search_posts tool', () => {
1175
1241
  const tool = mockTools.get('ghost_search_posts');
1176
1242
  await tool.handler({ query: 'test', limit: 10 });
1177
1243
 
1178
- expect(mockSearchPosts).toHaveBeenCalledWith('test', { limit: 10 });
1244
+ expect(mockSearchPosts).toHaveBeenCalledWith('test', expect.objectContaining({ limit: 10 }));
1179
1245
  });
1180
1246
 
1181
1247
  it('should validate limit is between 1 and 50', () => {
@@ -1213,10 +1279,13 @@ describe('mcp_server - ghost_search_posts tool', () => {
1213
1279
  limit: 20,
1214
1280
  });
1215
1281
 
1216
- expect(mockSearchPosts).toHaveBeenCalledWith('JavaScript', {
1217
- status: 'published',
1218
- limit: 20,
1219
- });
1282
+ expect(mockSearchPosts).toHaveBeenCalledWith(
1283
+ 'JavaScript',
1284
+ expect.objectContaining({
1285
+ status: 'published',
1286
+ limit: 20,
1287
+ })
1288
+ );
1220
1289
  });
1221
1290
 
1222
1291
  it('should handle errors from searchPosts', async () => {
@@ -1286,8 +1355,8 @@ describe('ghost_get_tag', () => {
1286
1355
 
1287
1356
  it('should have correct schema with id and slug as optional', () => {
1288
1357
  const tool = mockTools.get('ghost_get_tag');
1289
- // ghost_get_tag uses a refined schema, access via _def.schema.shape
1290
- const shape = tool.schema._def.schema.shape;
1358
+ // In Zod v4, refined schemas expose .shape directly
1359
+ const shape = tool.schema.shape;
1291
1360
  expect(shape.id).toBeDefined();
1292
1361
  expect(shape.slug).toBeDefined();
1293
1362
  expect(shape.include).toBeDefined();
@@ -1305,7 +1374,10 @@ describe('ghost_get_tag', () => {
1305
1374
  const tool = mockTools.get('ghost_get_tag');
1306
1375
  const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
1307
1376
 
1308
- expect(mockGetTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {});
1377
+ expect(mockGetTag).toHaveBeenCalledWith(
1378
+ '507f1f77bcf86cd799439011',
1379
+ expect.objectContaining({})
1380
+ );
1309
1381
  expect(result.content).toBeDefined();
1310
1382
  expect(result.content[0].type).toBe('text');
1311
1383
  expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"');
@@ -1324,7 +1396,7 @@ describe('ghost_get_tag', () => {
1324
1396
  const tool = mockTools.get('ghost_get_tag');
1325
1397
  const result = await tool.handler({ slug: 'test-tag' });
1326
1398
 
1327
- expect(mockGetTag).toHaveBeenCalledWith('slug/test-tag', {});
1399
+ expect(mockGetTag).toHaveBeenCalledWith('slug/test-tag', expect.objectContaining({}));
1328
1400
  expect(result.content[0].text).toContain('"slug": "test-tag"');
1329
1401
  });
1330
1402
 
@@ -1340,7 +1412,10 @@ describe('ghost_get_tag', () => {
1340
1412
  const tool = mockTools.get('ghost_get_tag');
1341
1413
  const result = await tool.handler({ id: '507f1f77bcf86cd799439011', include: 'count.posts' });
1342
1414
 
1343
- expect(mockGetTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { include: 'count.posts' });
1415
+ expect(mockGetTag).toHaveBeenCalledWith(
1416
+ '507f1f77bcf86cd799439011',
1417
+ expect.objectContaining({ include: 'count.posts' })
1418
+ );
1344
1419
  expect(result.content[0].text).toContain('"count"');
1345
1420
  });
1346
1421
 
@@ -1401,7 +1476,10 @@ describe('ghost_update_tag', () => {
1401
1476
  const tool = mockTools.get('ghost_update_tag');
1402
1477
  const result = await tool.handler({ id: '507f1f77bcf86cd799439011', name: 'Updated Tag' });
1403
1478
 
1404
- expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { name: 'Updated Tag' });
1479
+ expect(mockUpdateTag).toHaveBeenCalledWith(
1480
+ '507f1f77bcf86cd799439011',
1481
+ expect.objectContaining({ name: 'Updated Tag' })
1482
+ );
1405
1483
  expect(result.content[0].text).toContain('"name": "Updated Tag"');
1406
1484
  });
1407
1485
 
@@ -1419,9 +1497,12 @@ describe('ghost_update_tag', () => {
1419
1497
  description: 'New description',
1420
1498
  });
1421
1499
 
1422
- expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
1423
- description: 'New description',
1424
- });
1500
+ expect(mockUpdateTag).toHaveBeenCalledWith(
1501
+ '507f1f77bcf86cd799439011',
1502
+ expect.objectContaining({
1503
+ description: 'New description',
1504
+ })
1505
+ );
1425
1506
  expect(result.content[0].text).toContain('"description": "New description"');
1426
1507
  });
1427
1508
 
@@ -1443,11 +1524,14 @@ describe('ghost_update_tag', () => {
1443
1524
  meta_title: 'Updated Meta',
1444
1525
  });
1445
1526
 
1446
- expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
1447
- name: 'Updated Tag',
1448
- description: 'Updated description',
1449
- meta_title: 'Updated Meta',
1450
- });
1527
+ expect(mockUpdateTag).toHaveBeenCalledWith(
1528
+ '507f1f77bcf86cd799439011',
1529
+ expect.objectContaining({
1530
+ name: 'Updated Tag',
1531
+ description: 'Updated description',
1532
+ meta_title: 'Updated Meta',
1533
+ })
1534
+ );
1451
1535
  });
1452
1536
 
1453
1537
  it('should update tag feature image', async () => {
@@ -1464,9 +1548,12 @@ describe('ghost_update_tag', () => {
1464
1548
  feature_image: 'https://example.com/image.jpg',
1465
1549
  });
1466
1550
 
1467
- expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
1468
- feature_image: 'https://example.com/image.jpg',
1469
- });
1551
+ expect(mockUpdateTag).toHaveBeenCalledWith(
1552
+ '507f1f77bcf86cd799439011',
1553
+ expect.objectContaining({
1554
+ feature_image: 'https://example.com/image.jpg',
1555
+ })
1556
+ );
1470
1557
  });
1471
1558
 
1472
1559
  it('should return error when id is missing', async () => {