Skip to main content
Glama
mcp_server.test.js44.2 kB
import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock the McpServer to capture tool registrations const mockTools = new Map(); vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => { return { McpServer: class MockMcpServer { constructor(config) { this.config = config; } tool(name, description, schema, handler) { mockTools.set(name, { name, description, schema, handler }); } connect(_transport) { return Promise.resolve(); } }, }; }); vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => { return { StdioServerTransport: class MockStdioServerTransport {}, }; }); // Mock dotenv vi.mock('dotenv', () => ({ default: { config: vi.fn() }, })); // Mock crypto vi.mock('crypto', () => ({ default: { randomUUID: vi.fn().mockReturnValue('test-uuid-1234') }, })); // Mock services - will be lazy loaded const mockGetPosts = vi.fn(); const mockGetPost = vi.fn(); const mockGetTags = vi.fn(); const mockGetTag = vi.fn(); const mockCreateTag = vi.fn(); const mockUpdateTag = vi.fn(); const mockDeleteTag = vi.fn(); const mockUploadImage = vi.fn(); const mockCreatePostService = vi.fn(); const mockProcessImage = vi.fn(); const mockValidateImageUrl = vi.fn(); const mockCreateSecureAxiosConfig = vi.fn(); const mockUpdatePost = vi.fn(); const mockDeletePost = vi.fn(); const mockSearchPosts = vi.fn(); // Page mocks const mockGetPages = vi.fn(); const mockGetPage = vi.fn(); const mockCreatePageService = vi.fn(); const mockUpdatePage = vi.fn(); const mockDeletePage = vi.fn(); const mockSearchPages = vi.fn(); // Member mocks const mockCreateMember = vi.fn(); const mockUpdateMember = vi.fn(); const mockDeleteMember = vi.fn(); const mockGetMembers = vi.fn(); const mockGetMember = vi.fn(); const mockSearchMembers = vi.fn(); // Newsletter mocks const mockGetNewsletters = vi.fn(); const mockGetNewsletter = vi.fn(); const mockCreateNewsletterService = vi.fn(); const mockUpdateNewsletter = vi.fn(); const mockDeleteNewsletter = vi.fn(); // Tier mocks const mockGetTiers = vi.fn(); const mockGetTier = vi.fn(); const mockCreateTier = vi.fn(); const mockUpdateTier = vi.fn(); const mockDeleteTier = vi.fn(); vi.mock('../services/postService.js', () => ({ createPostService: (...args) => mockCreatePostService(...args), })); vi.mock('../services/pageService.js', () => ({ createPageService: (...args) => mockCreatePageService(...args), })); vi.mock('../services/newsletterService.js', () => ({ createNewsletterService: (...args) => mockCreateNewsletterService(...args), })); vi.mock('../services/ghostServiceImproved.js', () => ({ // Posts getPosts: (...args) => mockGetPosts(...args), getPost: (...args) => mockGetPost(...args), updatePost: (...args) => mockUpdatePost(...args), deletePost: (...args) => mockDeletePost(...args), searchPosts: (...args) => mockSearchPosts(...args), // Tags getTags: (...args) => mockGetTags(...args), getTag: (...args) => mockGetTag(...args), createTag: (...args) => mockCreateTag(...args), updateTag: (...args) => mockUpdateTag(...args), deleteTag: (...args) => mockDeleteTag(...args), // Images uploadImage: (...args) => mockUploadImage(...args), // Pages getPages: (...args) => mockGetPages(...args), getPage: (...args) => mockGetPage(...args), updatePage: (...args) => mockUpdatePage(...args), deletePage: (...args) => mockDeletePage(...args), searchPages: (...args) => mockSearchPages(...args), // Members createMember: (...args) => mockCreateMember(...args), updateMember: (...args) => mockUpdateMember(...args), deleteMember: (...args) => mockDeleteMember(...args), getMembers: (...args) => mockGetMembers(...args), getMember: (...args) => mockGetMember(...args), searchMembers: (...args) => mockSearchMembers(...args), // Newsletters getNewsletters: (...args) => mockGetNewsletters(...args), getNewsletter: (...args) => mockGetNewsletter(...args), updateNewsletter: (...args) => mockUpdateNewsletter(...args), deleteNewsletter: (...args) => mockDeleteNewsletter(...args), // Tiers getTiers: (...args) => mockGetTiers(...args), getTier: (...args) => mockGetTier(...args), createTier: (...args) => mockCreateTier(...args), updateTier: (...args) => mockUpdateTier(...args), deleteTier: (...args) => mockDeleteTier(...args), })); vi.mock('../services/imageProcessingService.js', () => ({ processImage: (...args) => mockProcessImage(...args), })); vi.mock('../utils/urlValidator.js', () => ({ validateImageUrl: (...args) => mockValidateImageUrl(...args), createSecureAxiosConfig: (...args) => mockCreateSecureAxiosConfig(...args), })); // Mock axios const mockAxios = vi.fn(); vi.mock('axios', () => ({ default: (...args) => mockAxios(...args), })); // Mock fs const mockCreateWriteStream = vi.fn(); vi.mock('fs', () => ({ default: { createWriteStream: (...args) => mockCreateWriteStream(...args), }, })); // Mock tempFileManager const mockTrackTempFile = vi.fn(); const mockCleanupTempFiles = vi.fn().mockResolvedValue(undefined); vi.mock('../utils/tempFileManager.js', () => ({ trackTempFile: (...args) => mockTrackTempFile(...args), cleanupTempFiles: (...args) => mockCleanupTempFiles(...args), })); // Mock os vi.mock('os', () => ({ default: { tmpdir: vi.fn().mockReturnValue('/tmp') }, })); // Mock path vi.mock('path', async () => { const actual = await vi.importActual('path'); return { default: actual, ...actual, }; }); describe('mcp_server - ghost_get_posts tool', () => { beforeEach(async () => { vi.clearAllMocks(); // Don't clear mockTools - they're registered once on module load // Import the module to register tools (only first time) if (mockTools.size === 0) { await import('../mcp_server.js'); } }); it('should register ghost_get_posts tool', () => { expect(mockTools.has('ghost_get_posts')).toBe(true); }); it('should have correct schema with all optional parameters', () => { const tool = mockTools.get('ghost_get_posts'); expect(tool).toBeDefined(); expect(tool.description).toContain('posts'); expect(tool.schema).toBeDefined(); // Zod schemas store field definitions in schema.shape expect(tool.schema.shape.limit).toBeDefined(); expect(tool.schema.shape.page).toBeDefined(); expect(tool.schema.shape.status).toBeDefined(); expect(tool.schema.shape.include).toBeDefined(); expect(tool.schema.shape.filter).toBeDefined(); expect(tool.schema.shape.order).toBeDefined(); }); it('should retrieve posts with default options', async () => { const mockPosts = [ { id: '1', title: 'Post 1', slug: 'post-1', status: 'published' }, { id: '2', title: 'Post 2', slug: 'post-2', status: 'draft' }, ]; mockGetPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_get_posts'); const result = await tool.handler({}); expect(mockGetPosts).toHaveBeenCalledWith({}); expect(result.content[0].text).toContain('Post 1'); expect(result.content[0].text).toContain('Post 2'); }); it('should pass limit and page parameters', async () => { const mockPosts = [{ id: '1', title: 'Post 1', slug: 'post-1' }]; mockGetPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_get_posts'); await tool.handler({ limit: 10, page: 2 }); expect(mockGetPosts).toHaveBeenCalledWith({ limit: 10, page: 2 }); }); it('should validate limit is between 1 and 100', () => { const tool = mockTools.get('ghost_get_posts'); // Zod schemas store field definitions in schema.shape const shape = tool.schema.shape; // Test that limit schema exists and has proper validation expect(shape.limit).toBeDefined(); expect(() => shape.limit.parse(0)).toThrow(); expect(() => shape.limit.parse(101)).toThrow(); expect(shape.limit.parse(50)).toBe(50); }); it('should validate page is at least 1', () => { const tool = mockTools.get('ghost_get_posts'); // Zod schemas store field definitions in schema.shape const shape = tool.schema.shape; expect(shape.page).toBeDefined(); expect(() => shape.page.parse(0)).toThrow(); expect(shape.page.parse(1)).toBe(1); }); it('should pass status filter', async () => { const mockPosts = [{ id: '1', title: 'Published Post', status: 'published' }]; mockGetPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_get_posts'); await tool.handler({ status: 'published' }); expect(mockGetPosts).toHaveBeenCalledWith({ status: 'published' }); }); it('should validate status enum values', () => { const tool = mockTools.get('ghost_get_posts'); // Zod schemas store field definitions in schema.shape const shape = tool.schema.shape; expect(shape.status).toBeDefined(); expect(() => shape.status.parse('invalid')).toThrow(); expect(shape.status.parse('published')).toBe('published'); expect(shape.status.parse('draft')).toBe('draft'); expect(shape.status.parse('scheduled')).toBe('scheduled'); expect(shape.status.parse('all')).toBe('all'); }); it('should pass include parameter', async () => { const mockPosts = [ { id: '1', title: 'Post with tags', tags: [{ name: 'tech' }], authors: [{ name: 'John' }], }, ]; mockGetPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_get_posts'); await tool.handler({ include: 'tags,authors' }); expect(mockGetPosts).toHaveBeenCalledWith({ include: 'tags,authors' }); }); it('should pass filter parameter (NQL)', async () => { const mockPosts = [{ id: '1', title: 'Featured Post', featured: true }]; mockGetPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_get_posts'); await tool.handler({ filter: 'featured:true' }); expect(mockGetPosts).toHaveBeenCalledWith({ filter: 'featured:true' }); }); it('should pass order parameter', async () => { const mockPosts = [ { id: '1', title: 'Newest', published_at: '2025-12-10' }, { id: '2', title: 'Older', published_at: '2025-12-01' }, ]; mockGetPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_get_posts'); await tool.handler({ order: 'published_at DESC' }); expect(mockGetPosts).toHaveBeenCalledWith({ order: 'published_at DESC' }); }); it('should pass all parameters combined', async () => { const mockPosts = [{ id: '1', title: 'Test Post' }]; mockGetPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_get_posts'); await tool.handler({ limit: 20, page: 1, status: 'published', include: 'tags,authors', filter: 'featured:true', order: 'published_at DESC', }); expect(mockGetPosts).toHaveBeenCalledWith({ limit: 20, page: 1, status: 'published', include: 'tags,authors', filter: 'featured:true', order: 'published_at DESC', }); }); it('should handle errors from ghostService', async () => { mockGetPosts.mockRejectedValue(new Error('Ghost API error')); const tool = mockTools.get('ghost_get_posts'); const result = await tool.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Ghost API error'); }); it('should return formatted JSON response', async () => { const mockPosts = [ { id: '1', title: 'Test Post', slug: 'test-post', html: '<p>Content</p>', status: 'published', }, ]; mockGetPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_get_posts'); const result = await tool.handler({}); expect(result.content).toBeDefined(); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('"id": "1"'); expect(result.content[0].text).toContain('"title": "Test Post"'); }); }); describe('mcp_server - ghost_get_post tool', () => { beforeEach(async () => { vi.clearAllMocks(); // Don't clear mockTools - they're registered once on module load if (mockTools.size === 0) { await import('../mcp_server.js'); } }); it('should register ghost_get_post tool', () => { expect(mockTools.has('ghost_get_post')).toBe(true); }); it('should have correct schema requiring one of id or slug', () => { const tool = mockTools.get('ghost_get_post'); expect(tool).toBeDefined(); expect(tool.description).toContain('post'); expect(tool.schema).toBeDefined(); // ghost_get_post uses a refined schema, access via _def.schema.shape const shape = tool.schema._def.schema.shape; expect(shape.id).toBeDefined(); expect(shape.slug).toBeDefined(); expect(shape.include).toBeDefined(); }); it('should retrieve post by ID', async () => { const mockPost = { id: '507f1f77bcf86cd799439011', title: 'Test Post', slug: 'test-post', html: '<p>Content</p>', status: 'published', }; mockGetPost.mockResolvedValue(mockPost); const tool = mockTools.get('ghost_get_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011' }); expect(mockGetPost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {}); expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"'); expect(result.content[0].text).toContain('"title": "Test Post"'); }); it('should retrieve post by slug', async () => { const mockPost = { id: '507f1f77bcf86cd799439011', title: 'Test Post', slug: 'test-post', html: '<p>Content</p>', status: 'published', }; mockGetPost.mockResolvedValue(mockPost); const tool = mockTools.get('ghost_get_post'); const result = await tool.handler({ slug: 'test-post' }); expect(mockGetPost).toHaveBeenCalledWith('slug/test-post', {}); expect(result.content[0].text).toContain('"title": "Test Post"'); }); it('should pass include parameter with ID', async () => { const mockPost = { id: '507f1f77bcf86cd799439011', title: 'Post with relations', tags: [{ name: 'tech' }], authors: [{ name: 'John' }], }; mockGetPost.mockResolvedValue(mockPost); const tool = mockTools.get('ghost_get_post'); await tool.handler({ id: '507f1f77bcf86cd799439011', include: 'tags,authors' }); expect(mockGetPost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { include: 'tags,authors', }); }); it('should pass include parameter with slug', async () => { const mockPost = { id: '507f1f77bcf86cd799439011', title: 'Post with relations', slug: 'test-post', tags: [{ name: 'tech' }], }; mockGetPost.mockResolvedValue(mockPost); const tool = mockTools.get('ghost_get_post'); await tool.handler({ slug: 'test-post', include: 'tags' }); expect(mockGetPost).toHaveBeenCalledWith('slug/test-post', { include: 'tags' }); }); it('should prefer ID over slug when both provided', async () => { const mockPost = { id: '507f1f77bcf86cd799439011', title: 'Test Post', slug: 'test-post' }; mockGetPost.mockResolvedValue(mockPost); const tool = mockTools.get('ghost_get_post'); await tool.handler({ id: '507f1f77bcf86cd799439011', slug: 'wrong-slug' }); expect(mockGetPost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {}); }); it('should handle not found errors', async () => { mockGetPost.mockRejectedValue(new Error('Post not found')); const tool = mockTools.get('ghost_get_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439099' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Post not found'); }); it('should handle errors from ghostService', async () => { mockGetPost.mockRejectedValue(new Error('Ghost API error')); const tool = mockTools.get('ghost_get_post'); const result = await tool.handler({ slug: 'test' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Ghost API error'); }); it('should return formatted JSON response', async () => { const mockPost = { id: '507f1f77bcf86cd799439011', uuid: 'uuid-123', title: 'Test Post', slug: 'test-post', html: '<p>Content</p>', status: 'published', created_at: '2025-12-10T00:00:00.000Z', updated_at: '2025-12-10T00:00:00.000Z', }; mockGetPost.mockResolvedValue(mockPost); const tool = mockTools.get('ghost_get_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011' }); expect(result.content).toBeDefined(); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"'); expect(result.content[0].text).toContain('"title": "Test Post"'); expect(result.content[0].text).toContain('"status": "published"'); }); it('should handle validation error when neither id nor slug provided', async () => { const tool = mockTools.get('ghost_get_post'); const result = await tool.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Either id or slug is required'); }); }); describe('mcp_server - ghost_update_post tool', () => { beforeEach(async () => { vi.clearAllMocks(); // Don't clear mockTools - they're registered once on module load if (mockTools.size === 0) { await import('../mcp_server.js'); } }); it('should register ghost_update_post tool', () => { expect(mockTools.has('ghost_update_post')).toBe(true); }); it('should have correct schema with required id field', () => { const tool = mockTools.get('ghost_update_post'); expect(tool).toBeDefined(); expect(tool.description).toContain('Updates an existing post'); expect(tool.schema).toBeDefined(); // Zod schemas store field definitions in schema.shape expect(tool.schema.shape.id).toBeDefined(); expect(tool.schema.shape.title).toBeDefined(); expect(tool.schema.shape.html).toBeDefined(); expect(tool.schema.shape.status).toBeDefined(); expect(tool.schema.shape.tags).toBeDefined(); expect(tool.schema.shape.feature_image).toBeDefined(); expect(tool.schema.shape.feature_image_alt).toBeDefined(); expect(tool.schema.shape.feature_image_caption).toBeDefined(); expect(tool.schema.shape.meta_title).toBeDefined(); expect(tool.schema.shape.meta_description).toBeDefined(); expect(tool.schema.shape.published_at).toBeDefined(); expect(tool.schema.shape.custom_excerpt).toBeDefined(); }); it('should update post title', async () => { const mockUpdatedPost = { id: '507f1f77bcf86cd799439011', title: 'Updated Title', slug: 'test-post', html: '<p>Content</p>', status: 'published', updated_at: '2025-12-10T12:00:00.000Z', }; mockUpdatePost.mockResolvedValue(mockUpdatedPost); const tool = mockTools.get('ghost_update_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'Updated Title' }); expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { title: 'Updated Title', }); expect(result.content[0].text).toContain('"title": "Updated Title"'); }); it('should update post content', async () => { const mockUpdatedPost = { id: '507f1f77bcf86cd799439011', title: 'Test Post', html: '<p>Updated content</p>', status: 'published', updated_at: '2025-12-10T12:00:00.000Z', }; mockUpdatePost.mockResolvedValue(mockUpdatedPost); const tool = mockTools.get('ghost_update_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', html: '<p>Updated content</p>', }); expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { html: '<p>Updated content</p>', }); expect(result.content[0].text).toContain('Updated content'); }); it('should update post status', async () => { const mockUpdatedPost = { id: '507f1f77bcf86cd799439011', title: 'Test Post', html: '<p>Content</p>', status: 'published', updated_at: '2025-12-10T12:00:00.000Z', }; mockUpdatePost.mockResolvedValue(mockUpdatedPost); const tool = mockTools.get('ghost_update_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', status: 'published' }); expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { status: 'published', }); expect(result.content[0].text).toContain('"status": "published"'); }); it('should update post tags', async () => { const mockUpdatedPost = { id: '507f1f77bcf86cd799439011', title: 'Test Post', html: '<p>Content</p>', tags: [{ name: 'tech' }, { name: 'javascript' }], updated_at: '2025-12-10T12:00:00.000Z', }; mockUpdatePost.mockResolvedValue(mockUpdatedPost); const tool = mockTools.get('ghost_update_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', tags: ['tech', 'javascript'], }); expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { tags: ['tech', 'javascript'], }); expect(result.content[0].text).toContain('tech'); expect(result.content[0].text).toContain('javascript'); }); it('should update post featured image', async () => { const mockUpdatedPost = { id: '507f1f77bcf86cd799439011', title: 'Test Post', feature_image: 'https://example.com/new-image.jpg', feature_image_alt: 'New image', updated_at: '2025-12-10T12:00:00.000Z', }; mockUpdatePost.mockResolvedValue(mockUpdatedPost); const tool = mockTools.get('ghost_update_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', feature_image: 'https://example.com/new-image.jpg', feature_image_alt: 'New image', }); expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { feature_image: 'https://example.com/new-image.jpg', feature_image_alt: 'New image', }); expect(result.content[0].text).toContain('new-image.jpg'); }); it('should update SEO meta fields', async () => { const mockUpdatedPost = { id: '507f1f77bcf86cd799439011', title: 'Test Post', meta_title: 'SEO Title', meta_description: 'SEO Description', updated_at: '2025-12-10T12:00:00.000Z', }; mockUpdatePost.mockResolvedValue(mockUpdatedPost); const tool = mockTools.get('ghost_update_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', meta_title: 'SEO Title', meta_description: 'SEO Description', }); expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { meta_title: 'SEO Title', meta_description: 'SEO Description', }); expect(result.content[0].text).toContain('SEO Title'); expect(result.content[0].text).toContain('SEO Description'); }); it('should update multiple fields at once', async () => { const mockUpdatedPost = { id: '507f1f77bcf86cd799439011', title: 'Updated Title', html: '<p>Updated content</p>', status: 'published', tags: [{ name: 'tech' }], updated_at: '2025-12-10T12:00:00.000Z', }; mockUpdatePost.mockResolvedValue(mockUpdatedPost); const tool = mockTools.get('ghost_update_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'Updated Title', html: '<p>Updated content</p>', status: 'published', tags: ['tech'], }); expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { title: 'Updated Title', html: '<p>Updated content</p>', status: 'published', tags: ['tech'], }); expect(result.content[0].text).toContain('Updated Title'); }); it('should handle not found errors', async () => { mockUpdatePost.mockRejectedValue(new Error('Post not found')); const tool = mockTools.get('ghost_update_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439099', title: 'New Title' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Post not found'); }); it('should handle validation errors', async () => { mockUpdatePost.mockRejectedValue(new Error('Validation failed: Title is required')); const tool = mockTools.get('ghost_update_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: '' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Validation failed'); }); it('should handle Ghost API errors', async () => { mockUpdatePost.mockRejectedValue(new Error('Ghost API error: Server timeout')); const tool = mockTools.get('ghost_update_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'Updated' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Ghost API error'); }); it('should return formatted JSON response', async () => { const mockUpdatedPost = { id: '507f1f77bcf86cd799439011', uuid: 'uuid-123', title: 'Updated Post', slug: 'updated-post', html: '<p>Updated content</p>', status: 'published', created_at: '2025-12-09T00:00:00.000Z', updated_at: '2025-12-10T12:00:00.000Z', }; mockUpdatePost.mockResolvedValue(mockUpdatedPost); const tool = mockTools.get('ghost_update_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'Updated Post' }); expect(result.content).toBeDefined(); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"'); expect(result.content[0].text).toContain('"title": "Updated Post"'); expect(result.content[0].text).toContain('"status": "published"'); }); }); describe('mcp_server - ghost_delete_post tool', () => { beforeEach(async () => { vi.clearAllMocks(); // Don't clear mockTools - they're registered once on module load if (mockTools.size === 0) { await import('../mcp_server.js'); } }); it('should register ghost_delete_post tool', () => { expect(mockTools.has('ghost_delete_post')).toBe(true); }); it('should have correct schema with required id field', () => { const tool = mockTools.get('ghost_delete_post'); expect(tool).toBeDefined(); expect(tool.description).toContain('Deletes a post'); expect(tool.description).toContain('permanent'); expect(tool.schema).toBeDefined(); // Zod schemas store field definitions in schema.shape expect(tool.schema.shape.id).toBeDefined(); }); it('should delete post by ID', async () => { mockDeletePost.mockResolvedValue({ deleted: true }); const tool = mockTools.get('ghost_delete_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011' }); expect(mockDeletePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011'); expect(result.content[0].text).toContain( 'Post 507f1f77bcf86cd799439011 has been successfully deleted' ); expect(result.isError).toBeUndefined(); }); it('should handle not found errors', async () => { mockDeletePost.mockRejectedValue(new Error('Post not found')); const tool = mockTools.get('ghost_delete_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439099' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Post not found'); }); it('should handle Ghost API errors', async () => { mockDeletePost.mockRejectedValue(new Error('Ghost API error: Permission denied')); const tool = mockTools.get('ghost_delete_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Ghost API error'); }); it('should return success message on successful deletion', async () => { mockDeletePost.mockResolvedValue({ deleted: true }); const tool = mockTools.get('ghost_delete_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011' }); expect(result.content).toBeDefined(); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toBe( 'Post 507f1f77bcf86cd799439011 has been successfully deleted.' ); }); it('should handle network errors', async () => { mockDeletePost.mockRejectedValue(new Error('Network error: Connection refused')); const tool = mockTools.get('ghost_delete_post'); const result = await tool.handler({ id: '507f1f77bcf86cd799439012' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Network error'); }); }); describe('mcp_server - ghost_search_posts tool', () => { beforeEach(async () => { vi.clearAllMocks(); // Don't clear mockTools - they're registered once on module load if (mockTools.size === 0) { await import('../mcp_server.js'); } }); it('should register ghost_search_posts tool', () => { expect(mockTools.has('ghost_search_posts')).toBe(true); }); it('should have correct schema with required query and optional parameters', () => { const tool = mockTools.get('ghost_search_posts'); expect(tool).toBeDefined(); expect(tool.description).toContain('Search'); expect(tool.schema).toBeDefined(); // Zod schemas store field definitions in schema.shape expect(tool.schema.shape.query).toBeDefined(); expect(tool.schema.shape.status).toBeDefined(); expect(tool.schema.shape.limit).toBeDefined(); }); it('should search posts with query only', async () => { const mockPosts = [ { id: '1', title: 'JavaScript Tips', slug: 'javascript-tips', status: 'published' }, { id: '2', title: 'JavaScript Tricks', slug: 'javascript-tricks', status: 'published' }, ]; mockSearchPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_search_posts'); const result = await tool.handler({ query: 'JavaScript' }); expect(mockSearchPosts).toHaveBeenCalledWith('JavaScript', {}); expect(result.content[0].text).toContain('JavaScript Tips'); expect(result.content[0].text).toContain('JavaScript Tricks'); }); it('should search posts with query and status filter', async () => { const mockPosts = [ { id: '1', title: 'Published Post', slug: 'published-post', status: 'published' }, ]; mockSearchPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_search_posts'); await tool.handler({ query: 'test', status: 'published' }); expect(mockSearchPosts).toHaveBeenCalledWith('test', { status: 'published' }); }); it('should search posts with query and limit', async () => { const mockPosts = [{ id: '1', title: 'Test Post', slug: 'test-post' }]; mockSearchPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_search_posts'); await tool.handler({ query: 'test', limit: 10 }); expect(mockSearchPosts).toHaveBeenCalledWith('test', { limit: 10 }); }); it('should validate limit is between 1 and 50', () => { const tool = mockTools.get('ghost_search_posts'); // Zod schemas store field definitions in schema.shape const shape = tool.schema.shape; expect(shape.limit).toBeDefined(); expect(() => shape.limit.parse(0)).toThrow(); expect(() => shape.limit.parse(51)).toThrow(); expect(shape.limit.parse(25)).toBe(25); }); it('should validate status enum values', () => { const tool = mockTools.get('ghost_search_posts'); // Zod schemas store field definitions in schema.shape const shape = tool.schema.shape; expect(shape.status).toBeDefined(); expect(() => shape.status.parse('invalid')).toThrow(); expect(shape.status.parse('published')).toBe('published'); expect(shape.status.parse('draft')).toBe('draft'); expect(shape.status.parse('scheduled')).toBe('scheduled'); expect(shape.status.parse('all')).toBe('all'); }); it('should pass all parameters combined', async () => { const mockPosts = [{ id: '1', title: 'Test Post' }]; mockSearchPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_search_posts'); await tool.handler({ query: 'JavaScript', status: 'published', limit: 20, }); expect(mockSearchPosts).toHaveBeenCalledWith('JavaScript', { status: 'published', limit: 20, }); }); it('should handle errors from searchPosts', async () => { // Empty query is now caught by Zod validation const tool = mockTools.get('ghost_search_posts'); const result = await tool.handler({ query: '' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('VALIDATION_ERROR'); }); it('should handle Ghost API errors', async () => { mockSearchPosts.mockRejectedValue(new Error('Ghost API error')); const tool = mockTools.get('ghost_search_posts'); const result = await tool.handler({ query: 'test' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Ghost API error'); }); it('should return formatted JSON response', async () => { const mockPosts = [ { id: '1', title: 'Test Post', slug: 'test-post', html: '<p>Content</p>', status: 'published', }, ]; mockSearchPosts.mockResolvedValue(mockPosts); const tool = mockTools.get('ghost_search_posts'); const result = await tool.handler({ query: 'Test' }); expect(result.content).toBeDefined(); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('"id": "1"'); expect(result.content[0].text).toContain('"title": "Test Post"'); }); it('should return empty array when no results found', async () => { mockSearchPosts.mockResolvedValue([]); const tool = mockTools.get('ghost_search_posts'); const result = await tool.handler({ query: 'nonexistent' }); expect(result.content[0].text).toBe('[]'); expect(result.isError).toBeUndefined(); }); }); describe('ghost_get_tag', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should be registered as a tool', () => { expect(mockTools.has('ghost_get_tag')).toBe(true); const tool = mockTools.get('ghost_get_tag'); expect(tool.name).toBe('ghost_get_tag'); expect(tool.description).toBeDefined(); expect(tool.schema).toBeDefined(); expect(tool.handler).toBeDefined(); }); it('should have correct schema with id and slug as optional', () => { const tool = mockTools.get('ghost_get_tag'); // ghost_get_tag uses a refined schema, access via _def.schema.shape const shape = tool.schema._def.schema.shape; expect(shape.id).toBeDefined(); expect(shape.slug).toBeDefined(); expect(shape.include).toBeDefined(); }); it('should retrieve tag by ID', async () => { const mockTag = { id: '507f1f77bcf86cd799439011', name: 'Test Tag', slug: 'test-tag', description: 'A test tag', }; mockGetTag.mockResolvedValue(mockTag); const tool = mockTools.get('ghost_get_tag'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011' }); expect(mockGetTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {}); expect(result.content).toBeDefined(); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"'); expect(result.content[0].text).toContain('"name": "Test Tag"'); }); it('should retrieve tag by slug', async () => { const mockTag = { id: '507f1f77bcf86cd799439011', name: 'Test Tag', slug: 'test-tag', description: 'A test tag', }; mockGetTag.mockResolvedValue(mockTag); const tool = mockTools.get('ghost_get_tag'); const result = await tool.handler({ slug: 'test-tag' }); expect(mockGetTag).toHaveBeenCalledWith('slug/test-tag', {}); expect(result.content[0].text).toContain('"slug": "test-tag"'); }); it('should support include parameter for post count', async () => { const mockTag = { id: '507f1f77bcf86cd799439011', name: 'Test Tag', slug: 'test-tag', count: { posts: 5 }, }; mockGetTag.mockResolvedValue(mockTag); const tool = mockTools.get('ghost_get_tag'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', include: 'count.posts' }); expect(mockGetTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { include: 'count.posts' }); expect(result.content[0].text).toContain('"count"'); }); it('should return error when neither id nor slug provided', async () => { const tool = mockTools.get('ghost_get_tag'); const result = await tool.handler({}); expect(result.content[0].type).toBe('text'); expect(result.content[0].text).toContain('Either id or slug is required'); expect(result.isError).toBe(true); }); it('should handle not found error', async () => { mockGetTag.mockRejectedValue(new Error('Tag not found')); const tool = mockTools.get('ghost_get_tag'); const result = await tool.handler({ id: '507f1f77bcf86cd799439099' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Tag not found'); }); }); describe('ghost_update_tag', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should be registered as a tool', () => { expect(mockTools.has('ghost_update_tag')).toBe(true); const tool = mockTools.get('ghost_update_tag'); expect(tool.name).toBe('ghost_update_tag'); expect(tool.description).toBeDefined(); expect(tool.schema).toBeDefined(); expect(tool.handler).toBeDefined(); }); it('should have correct schema with all update fields', () => { const tool = mockTools.get('ghost_update_tag'); // Zod schemas store field definitions in schema.shape expect(tool.schema.shape.id).toBeDefined(); expect(tool.schema.shape.name).toBeDefined(); expect(tool.schema.shape.slug).toBeDefined(); expect(tool.schema.shape.description).toBeDefined(); expect(tool.schema.shape.feature_image).toBeDefined(); expect(tool.schema.shape.meta_title).toBeDefined(); expect(tool.schema.shape.meta_description).toBeDefined(); }); it('should update tag name', async () => { const mockUpdatedTag = { id: '507f1f77bcf86cd799439011', name: 'Updated Tag', slug: 'updated-tag', }; mockUpdateTag.mockResolvedValue(mockUpdatedTag); const tool = mockTools.get('ghost_update_tag'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', name: 'Updated Tag' }); expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { name: 'Updated Tag' }); expect(result.content[0].text).toContain('"name": "Updated Tag"'); }); it('should update tag description', async () => { const mockUpdatedTag = { id: '507f1f77bcf86cd799439011', name: 'Test Tag', description: 'New description', }; mockUpdateTag.mockResolvedValue(mockUpdatedTag); const tool = mockTools.get('ghost_update_tag'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', description: 'New description', }); expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { description: 'New description', }); expect(result.content[0].text).toContain('"description": "New description"'); }); it('should update multiple fields at once', async () => { const mockUpdatedTag = { id: '507f1f77bcf86cd799439011', name: 'Updated Tag', slug: 'updated-tag', description: 'Updated description', meta_title: 'Updated Meta', }; mockUpdateTag.mockResolvedValue(mockUpdatedTag); const tool = mockTools.get('ghost_update_tag'); await tool.handler({ id: '507f1f77bcf86cd799439011', name: 'Updated Tag', description: 'Updated description', meta_title: 'Updated Meta', }); expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { name: 'Updated Tag', description: 'Updated description', meta_title: 'Updated Meta', }); }); it('should update tag feature image', async () => { const mockUpdatedTag = { id: '507f1f77bcf86cd799439011', name: 'Test Tag', feature_image: 'https://example.com/image.jpg', }; mockUpdateTag.mockResolvedValue(mockUpdatedTag); const tool = mockTools.get('ghost_update_tag'); await tool.handler({ id: '507f1f77bcf86cd799439011', feature_image: 'https://example.com/image.jpg', }); expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { feature_image: 'https://example.com/image.jpg', }); }); it('should return error when id is missing', async () => { const tool = mockTools.get('ghost_update_tag'); const result = await tool.handler({ name: 'Test' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('VALIDATION_ERROR'); }); it('should handle validation error', async () => { mockUpdateTag.mockRejectedValue(new Error('Validation failed')); const tool = mockTools.get('ghost_update_tag'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', name: '' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Validation failed'); }); it('should handle not found error', async () => { mockUpdateTag.mockRejectedValue(new Error('Tag not found')); const tool = mockTools.get('ghost_update_tag'); const result = await tool.handler({ id: '507f1f77bcf86cd799439099', name: 'Test' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Tag not found'); }); }); describe('ghost_delete_tag', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should be registered as a tool', () => { expect(mockTools.has('ghost_delete_tag')).toBe(true); const tool = mockTools.get('ghost_delete_tag'); expect(tool.name).toBe('ghost_delete_tag'); expect(tool.description).toBeDefined(); expect(tool.schema).toBeDefined(); expect(tool.handler).toBeDefined(); }); it('should have correct schema with id field', () => { const tool = mockTools.get('ghost_delete_tag'); // Zod schemas store field definitions in schema.shape expect(tool.schema.shape.id).toBeDefined(); }); it('should delete tag successfully', async () => { mockDeleteTag.mockResolvedValue({ success: true }); const tool = mockTools.get('ghost_delete_tag'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011' }); expect(mockDeleteTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011'); expect(result.content[0].text).toContain('successfully deleted'); expect(result.isError).toBeUndefined(); }); it('should return error when id is missing', async () => { const tool = mockTools.get('ghost_delete_tag'); const result = await tool.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('VALIDATION_ERROR'); }); it('should handle not found error', async () => { mockDeleteTag.mockRejectedValue(new Error('Tag not found')); const tool = mockTools.get('ghost_delete_tag'); const result = await tool.handler({ id: '507f1f77bcf86cd799439099' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Tag not found'); }); it('should handle deletion error', async () => { mockDeleteTag.mockRejectedValue(new Error('Failed to delete tag')); const tool = mockTools.get('ghost_delete_tag'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Failed to delete tag'); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jgardner04/Ghost-MCP-Server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server