Skip to main content
Glama
mcp_server_pages.test.js17.7 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 for pages const mockGetPages = vi.fn(); const mockGetPage = vi.fn(); const mockUpdatePage = vi.fn(); const mockDeletePage = vi.fn(); const mockSearchPages = vi.fn(); const mockCreatePageService = vi.fn(); // Also mock post and tag services to avoid errors const mockGetPosts = vi.fn(); const mockGetPost = vi.fn(); const mockGetTags = vi.fn(); const mockCreateTag = vi.fn(); const mockUploadImage = vi.fn(); const mockCreatePostService = vi.fn(); const mockUpdatePost = vi.fn(); const mockDeletePost = vi.fn(); const mockSearchPosts = vi.fn(); const mockProcessImage = vi.fn(); const mockValidateImageUrl = vi.fn(); const mockCreateSecureAxiosConfig = vi.fn(); vi.mock('../services/ghostService.js', () => ({ getPosts: (...args) => mockGetPosts(...args), getPost: (...args) => mockGetPost(...args), getTags: (...args) => mockGetTags(...args), createTag: (...args) => mockCreateTag(...args), uploadImage: (...args) => mockUploadImage(...args), })); vi.mock('../services/postService.js', () => ({ createPostService: (...args) => mockCreatePostService(...args), })); vi.mock('../services/pageService.js', () => ({ createPageService: (...args) => mockCreatePageService(...args), })); vi.mock('../services/ghostServiceImproved.js', () => ({ updatePost: (...args) => mockUpdatePost(...args), deletePost: (...args) => mockDeletePost(...args), searchPosts: (...args) => mockSearchPosts(...args), getPages: (...args) => mockGetPages(...args), getPage: (...args) => mockGetPage(...args), updatePage: (...args) => mockUpdatePage(...args), deletePage: (...args) => mockDeletePage(...args), searchPages: (...args) => mockSearchPages(...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 mockUnlink = vi.fn((path, cb) => cb(null)); const mockCreateWriteStream = vi.fn(); vi.mock('fs', () => ({ default: { unlink: (...args) => mockUnlink(...args), createWriteStream: (...args) => mockCreateWriteStream(...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_pages tool', () => { beforeEach(async () => { vi.clearAllMocks(); // Import the module to register tools (only first time) if (mockTools.size === 0) { await import('../mcp_server.js'); } }); it('should register ghost_get_pages tool', () => { expect(mockTools.has('ghost_get_pages')).toBe(true); }); it('should have correct schema with all optional parameters', () => { const tool = mockTools.get('ghost_get_pages'); expect(tool).toBeDefined(); expect(tool.description).toContain('pages'); expect(tool.schema).toBeDefined(); expect(tool.schema.shape.limit).toBeDefined(); expect(tool.schema.shape.page).toBeDefined(); expect(tool.schema.shape.include).toBeDefined(); expect(tool.schema.shape.filter).toBeDefined(); expect(tool.schema.shape.order).toBeDefined(); }); it('should retrieve pages with default options', async () => { const mockPages = [ { id: '1', title: 'About Us', slug: 'about-us', status: 'published' }, { id: '2', title: 'Contact', slug: 'contact', status: 'draft' }, ]; mockGetPages.mockResolvedValue(mockPages); const tool = mockTools.get('ghost_get_pages'); const result = await tool.handler({}); expect(mockGetPages).toHaveBeenCalledWith({}); expect(result.content[0].text).toContain('About Us'); expect(result.content[0].text).toContain('Contact'); }); it('should pass limit and page parameters', async () => { const mockPages = [{ id: '1', title: 'Page 1', slug: 'page-1' }]; mockGetPages.mockResolvedValue(mockPages); const tool = mockTools.get('ghost_get_pages'); await tool.handler({ limit: 10, page: 2 }); expect(mockGetPages).toHaveBeenCalledWith({ limit: 10, page: 2 }); }); it('should validate limit is between 1 and 100', () => { const tool = mockTools.get('ghost_get_pages'); const schema = tool.schema; expect(schema.shape.limit).toBeDefined(); expect(() => schema.shape.limit.parse(0)).toThrow(); expect(() => schema.shape.limit.parse(101)).toThrow(); expect(schema.shape.limit.parse(50)).toBe(50); }); it('should pass filter parameter', async () => { const mockPages = [{ id: '1', title: 'Published Page', status: 'published' }]; mockGetPages.mockResolvedValue(mockPages); const tool = mockTools.get('ghost_get_pages'); await tool.handler({ filter: 'status:published' }); expect(mockGetPages).toHaveBeenCalledWith({ filter: 'status:published' }); }); it('should handle errors gracefully', async () => { mockGetPages.mockRejectedValue(new Error('API error')); const tool = mockTools.get('ghost_get_pages'); const result = await tool.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Error retrieving pages'); }); }); describe('mcp_server - ghost_get_page tool', () => { beforeEach(async () => { vi.clearAllMocks(); if (mockTools.size === 0) { await import('../mcp_server.js'); } }); it('should register ghost_get_page tool', () => { expect(mockTools.has('ghost_get_page')).toBe(true); }); it('should have correct schema with id and slug options', () => { const tool = mockTools.get('ghost_get_page'); expect(tool).toBeDefined(); // ghost_get_page 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 page by ID', async () => { const mockPage = { id: '507f1f77bcf86cd799439011', title: 'About Us', slug: 'about-us' }; mockGetPage.mockResolvedValue(mockPage); const tool = mockTools.get('ghost_get_page'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011' }); expect(mockGetPage).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {}); expect(result.content[0].text).toContain('About Us'); }); it('should retrieve page by slug', async () => { const mockPage = { id: '507f1f77bcf86cd799439011', title: 'About Us', slug: 'about-us' }; mockGetPage.mockResolvedValue(mockPage); const tool = mockTools.get('ghost_get_page'); const result = await tool.handler({ slug: 'about-us' }); expect(mockGetPage).toHaveBeenCalledWith('slug/about-us', {}); expect(result.content[0].text).toContain('About Us'); }); it('should require either id or slug', () => { const tool = mockTools.get('ghost_get_page'); // Test schema validation - the refine check requires id or slug expect(() => tool.schema.parse({})).toThrow(); // Valid inputs should parse successfully (Ghost IDs are 24 hex chars) expect(() => tool.schema.parse({ id: '507f1f77bcf86cd799439011' })).not.toThrow(); expect(() => tool.schema.parse({ slug: 'about-us' })).not.toThrow(); }); it('should handle errors gracefully', async () => { mockGetPage.mockRejectedValue(new Error('Page not found')); const tool = mockTools.get('ghost_get_page'); const result = await tool.handler({ id: '507f1f77bcf86cd799439099' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Error retrieving page'); }); }); describe('mcp_server - ghost_create_page tool', () => { beforeEach(async () => { vi.clearAllMocks(); if (mockTools.size === 0) { await import('../mcp_server.js'); } }); it('should register ghost_create_page tool', () => { expect(mockTools.has('ghost_create_page')).toBe(true); }); it('should have correct schema with required and optional fields', () => { const tool = mockTools.get('ghost_create_page'); expect(tool).toBeDefined(); expect(tool.description).toContain('page'); expect(tool.schema.shape.title).toBeDefined(); expect(tool.schema.shape.html).toBeDefined(); expect(tool.schema.shape.status).toBeDefined(); expect(tool.schema.shape.feature_image).toBeDefined(); expect(tool.schema.shape.meta_title).toBeDefined(); expect(tool.schema.shape.meta_description).toBeDefined(); }); it('should create page with minimal input', async () => { const createdPage = { id: '507f1f77bcf86cd799439011', title: 'New Page', status: 'draft' }; mockCreatePageService.mockResolvedValue(createdPage); const tool = mockTools.get('ghost_create_page'); const result = await tool.handler({ title: 'New Page', html: '<p>Content</p>' }); // Schema adds default values, so use objectContaining for the key fields expect(mockCreatePageService).toHaveBeenCalledWith( expect.objectContaining({ title: 'New Page', html: '<p>Content</p>', }) ); expect(result.content[0].text).toContain('New Page'); }); it('should create page with all optional fields', async () => { const fullInput = { title: 'Complete Page', html: '<p>Content</p>', status: 'published', custom_excerpt: 'Excerpt', feature_image: 'https://example.com/image.jpg', feature_image_alt: 'Alt text', feature_image_caption: 'Caption', meta_title: 'SEO Title', meta_description: 'SEO Description', }; const createdPage = { id: '507f1f77bcf86cd799439011', ...fullInput }; mockCreatePageService.mockResolvedValue(createdPage); const tool = mockTools.get('ghost_create_page'); const result = await tool.handler(fullInput); // Schema adds default values, so use objectContaining for the key fields expect(mockCreatePageService).toHaveBeenCalledWith(expect.objectContaining(fullInput)); expect(result.content[0].text).toContain('Complete Page'); }); it('should handle errors gracefully', async () => { mockCreatePageService.mockRejectedValue(new Error('Invalid input')); const tool = mockTools.get('ghost_create_page'); const result = await tool.handler({ title: 'Test', html: '<p>Content</p>' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Error creating page'); }); }); describe('mcp_server - ghost_update_page tool', () => { beforeEach(async () => { vi.clearAllMocks(); if (mockTools.size === 0) { await import('../mcp_server.js'); } }); it('should register ghost_update_page tool', () => { expect(mockTools.has('ghost_update_page')).toBe(true); }); it('should have correct schema with id required and other fields optional', () => { const tool = mockTools.get('ghost_update_page'); expect(tool).toBeDefined(); expect(tool.description).toContain('page'); expect(tool.schema.shape.id).toBeDefined(); expect(tool.schema.shape.title).toBeDefined(); expect(tool.schema.shape.html).toBeDefined(); expect(tool.schema.shape.status).toBeDefined(); }); it('should update page with new title', async () => { const updatedPage = { id: '507f1f77bcf86cd799439011', title: 'Updated Title' }; mockUpdatePage.mockResolvedValue(updatedPage); const tool = mockTools.get('ghost_update_page'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'Updated Title' }); expect(mockUpdatePage).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { title: 'Updated Title', }); expect(result.content[0].text).toContain('Updated Title'); }); it('should update page with multiple fields', async () => { const updatedPage = { id: '507f1f77bcf86cd799439011', title: 'New Title', status: 'published' }; mockUpdatePage.mockResolvedValue(updatedPage); const tool = mockTools.get('ghost_update_page'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'New Title', status: 'published', html: '<p>Updated content</p>', }); expect(mockUpdatePage).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { title: 'New Title', status: 'published', html: '<p>Updated content</p>', }); expect(result.content[0].text).toContain('New Title'); }); it('should handle errors gracefully', async () => { mockUpdatePage.mockRejectedValue(new Error('Page not found')); const tool = mockTools.get('ghost_update_page'); const result = await tool.handler({ id: '507f1f77bcf86cd799439099', title: 'Test' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Error updating page'); }); }); describe('mcp_server - ghost_delete_page tool', () => { beforeEach(async () => { vi.clearAllMocks(); if (mockTools.size === 0) { await import('../mcp_server.js'); } }); it('should register ghost_delete_page tool', () => { expect(mockTools.has('ghost_delete_page')).toBe(true); }); it('should have correct schema with id required', () => { const tool = mockTools.get('ghost_delete_page'); expect(tool).toBeDefined(); expect(tool.schema.shape.id).toBeDefined(); expect(tool.description).toContain('permanent'); }); it('should delete page by ID', async () => { mockDeletePage.mockResolvedValue({ id: '507f1f77bcf86cd799439011' }); const tool = mockTools.get('ghost_delete_page'); const result = await tool.handler({ id: '507f1f77bcf86cd799439011' }); expect(mockDeletePage).toHaveBeenCalledWith('507f1f77bcf86cd799439011'); expect(result.content[0].text).toContain('successfully deleted'); }); it('should handle errors gracefully', async () => { mockDeletePage.mockRejectedValue(new Error('Page not found')); const tool = mockTools.get('ghost_delete_page'); const result = await tool.handler({ id: '507f1f77bcf86cd799439099' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Error deleting page'); }); }); describe('mcp_server - ghost_search_pages tool', () => { beforeEach(async () => { vi.clearAllMocks(); if (mockTools.size === 0) { await import('../mcp_server.js'); } }); it('should register ghost_search_pages tool', () => { expect(mockTools.has('ghost_search_pages')).toBe(true); }); it('should have correct schema with query required', () => { const tool = mockTools.get('ghost_search_pages'); expect(tool).toBeDefined(); expect(tool.schema.shape.query).toBeDefined(); expect(tool.schema.shape.status).toBeDefined(); expect(tool.schema.shape.limit).toBeDefined(); }); it('should search pages with query', async () => { const mockPages = [{ id: '1', title: 'About Us', slug: 'about-us' }]; mockSearchPages.mockResolvedValue(mockPages); const tool = mockTools.get('ghost_search_pages'); const result = await tool.handler({ query: 'about' }); expect(mockSearchPages).toHaveBeenCalledWith('about', {}); expect(result.content[0].text).toContain('About Us'); }); it('should pass status filter', async () => { const mockPages = [{ id: '1', title: 'Published Page' }]; mockSearchPages.mockResolvedValue(mockPages); const tool = mockTools.get('ghost_search_pages'); await tool.handler({ query: 'test', status: 'published' }); expect(mockSearchPages).toHaveBeenCalledWith('test', { status: 'published' }); }); it('should pass limit option', async () => { const mockPages = []; mockSearchPages.mockResolvedValue(mockPages); const tool = mockTools.get('ghost_search_pages'); await tool.handler({ query: 'test', limit: 5 }); expect(mockSearchPages).toHaveBeenCalledWith('test', { limit: 5 }); }); it('should validate limit is between 1 and 50', () => { const tool = mockTools.get('ghost_search_pages'); const schema = tool.schema; expect(schema.shape.limit).toBeDefined(); expect(() => schema.shape.limit.parse(0)).toThrow(); expect(() => schema.shape.limit.parse(51)).toThrow(); expect(schema.shape.limit.parse(25)).toBe(25); }); it('should handle errors gracefully', async () => { mockSearchPages.mockRejectedValue(new Error('Search failed')); const tool = mockTools.get('ghost_search_pages'); const result = await tool.handler({ query: 'test' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Error searching pages'); }); });

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