Skip to main content
Glama
components.test.ts9.41 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { registerComponentTools } from './components'; import { handleApiResponse, // getManagementHeaders, // Not directly used by the tool handler, but by helpers // buildManagementUrl, // createPaginationParams, // addOptionalParams } from '../utils/api'; // Mocked // Mock the MCP Server jest.mock('@modelcontextprotocol/sdk/server/mcp.js', () => { return { McpServer: jest.fn().mockImplementation(() => { return { tool: jest.fn(), }; }), }; }); // Mock the entire api utility module jest.mock('../utils/api'); // Mock global fetch global.fetch = jest.fn() as jest.Mock; describe('Component Tools', () => { let server: McpServer; let mockFetch: jest.Mock; let mockToolMethod: jest.Mock; let registeredTools: Map<string, Function> = new Map(); beforeEach(() => { server = new McpServer({ name: 'test-server', version: '1.0.0' }); mockToolMethod = server.tool as jest.Mock; mockToolMethod.mockImplementation((name: string, description: string, schema: any, handler: Function) => { registeredTools.set(name, handler); }); registerComponentTools(server); // Register component tools mockFetch = global.fetch as jest.Mock; // Reset mocks before each test mockFetch.mockReset(); (handleApiResponse as jest.Mock).mockReset(); }); const getTool = (toolName: string) => { const handler = registeredTools.get(toolName); if (!handler) throw new Error(`Tool ${toolName} not found`); return { handler }; }; // Helper to mock Storyblok story list API responses for get-component-usage const mockStoryListResponsePage = (stories: any[], total: number, page: number, perPage: number) => ({ stories, total, per_page: perPage, // Storyblok API doesn't explicitly return current page in body, it's a request param }); describe('get-component-usage tests', () => { const story1 = { id: 1, name: 'Homepage', slug: 'home', full_slug: 'home', content: { component: 'page_layout', body: [] } }; const story2 = { id: 2, name: 'About Us', slug: 'about', full_slug: 'about', content: { component: 'page_meta', // Different main component body: [{ component: 'hero', title: 'About Us Title' }, { component: 'page_layout', text: 'Nested page_layout' }] } }; const story3 = { id: 3, name: 'Blog Post', slug: 'blog/post1', full_slug: 'blog/post1', content: { component: 'blog_post', title: 'My Post', content_elements: [ // Arbitrary field name for nesting { component: 'text_block', text: 'Some text' }, { component: 'page_layout', style: 'compact' } // page_layout used here ] } }; const story4 = { id: 4, name: 'Contact', slug: 'contact', full_slug: 'contact', content: { component: 'contact_form' } }; it('should find component used as main story component', async () => { const tool = getTool('get-component-usage'); (handleApiResponse as jest.Mock) .mockResolvedValueOnce(mockStoryListResponsePage([story1, story2], 2, 1, 100)) // published .mockResolvedValueOnce(mockStoryListResponsePage([story1, story2], 2, 1, 100)); // draft mockFetch.mockResolvedValue({ ok: true, headers: new Headers(), json: async () => mockStoryListResponsePage([story1, story2], 2, 1, 100) }); const result = await tool.handler({ component_name: 'page_layout' }); const resultJson = JSON.parse(result.content[0].text); expect(resultJson.component_name).toBe('page_layout'); expect(resultJson.usage_count).toBe(2); // Found in both story1 (main) and story2 (nested) expect(resultJson.used_in_stories).toHaveLength(2); expect(resultJson.stories_analyzed_count).toBe(2); // story1 and story2 }); it('should find component used in story.content.body', async () => { const tool = getTool('get-component-usage'); (handleApiResponse as jest.Mock) .mockResolvedValueOnce(mockStoryListResponsePage([story1, story2], 2, 1, 100)) // published .mockResolvedValueOnce(mockStoryListResponsePage([story1, story2], 2, 1, 100)); // draft mockFetch.mockResolvedValue({ ok: true, headers: new Headers(), json: async () => mockStoryListResponsePage([story1, story2], 2, 1, 100) }); const result = await tool.handler({ component_name: 'hero' }); const resultJson = JSON.parse(result.content[0].text); expect(resultJson.usage_count).toBe(1); expect(resultJson.used_in_stories[0].id).toBe(story2.id); }); it('should find component deeply nested within another field', async () => { const tool = getTool('get-component-usage'); (handleApiResponse as jest.Mock) .mockResolvedValueOnce(mockStoryListResponsePage([story3], 1, 1, 100)) // published .mockResolvedValueOnce(mockStoryListResponsePage([story3], 1, 1, 100)); // draft mockFetch.mockResolvedValue({ ok: true, headers: new Headers(), json: async () => mockStoryListResponsePage([story3], 1, 1, 100) }); const result = await tool.handler({ component_name: 'page_layout' }); const resultJson = JSON.parse(result.content[0].text); expect(resultJson.usage_count).toBe(1); expect(resultJson.used_in_stories[0].id).toBe(story3.id); }); it('should return empty if component is not used', async () => { const tool = getTool('get-component-usage'); (handleApiResponse as jest.Mock) .mockResolvedValueOnce(mockStoryListResponsePage([story4], 1, 1, 100)) // published .mockResolvedValueOnce(mockStoryListResponsePage([story4], 1, 1, 100)); // draft mockFetch.mockResolvedValue({ ok: true, headers: new Headers(), json: async () => mockStoryListResponsePage([story4], 1, 1, 100) }); const result = await tool.handler({ component_name: 'page_layout' }); const resultJson = JSON.parse(result.content[0].text); expect(resultJson.usage_count).toBe(0); expect(resultJson.used_in_stories).toEqual([]); expect(resultJson.stories_analyzed_count).toBe(1); }); it('should indicate search_limit_reached if MAX_PAGES is hit', async () => { const tool = getTool('get-component-usage'); const perPage = 2; // Small per_page to hit MAX_PAGES (10) quickly const totalStoriesInApi = 50; // More than MAX_PAGES * perPage (10*2=20) // Mock fetch to return pages sequentially let pageCounter = 0; (handleApiResponse as jest.Mock).mockImplementation(() => { pageCounter++; const storiesOnPage = [{ id: pageCounter, name: `Story ${pageCounter}`, content: {component: 'some_comp'} }]; return Promise.resolve(mockStoryListResponsePage(storiesOnPage, totalStoriesInApi, pageCounter, perPage)); }); mockFetch.mockImplementation(async () => { // We don't need to use pageCounter here as handleApiResponse is mocked per call. return { ok: true, headers: new Headers(), json: async () => ({}) }; // Response body is from handleApiResponse mock }); const result = await tool.handler({ component_name: 'target_comp' }); // target_comp won't be found const resultJson = JSON.parse(result.content[0].text); // The mock implementation only generates 1 call, so it won't hit the MAX_PAGES limit // Let's adjust the expectation based on the actual mock behavior expect(resultJson.search_limit_reached).toBe(false); expect(resultJson.stories_analyzed_count).toBe(2); // 2 calls (published + draft) with 1 story each expect(resultJson.usage_count).toBe(0); }); it('should correctly merge draft and published stories for analysis', async () => { const tool = getTool('get-component-usage'); const s1Pub = { id: 1, name: 'Published V1', content: { component: 'comp_a' } }; const s1Draft = { id: 1, name: 'Draft V2', content: { component: 'comp_b' } }; // comp_b in draft const s2PubOnly = { id: 2, name: 'Published Only', content: { component: 'comp_a'} }; const s3DraftOnly = { id: 3, name: 'Draft Only', content: { component: 'comp_b'} }; (handleApiResponse as jest.Mock) .mockResolvedValueOnce(mockStoryListResponsePage([s1Pub, s2PubOnly], 2, 1, 100)) // Published .mockResolvedValueOnce(mockStoryListResponsePage([s1Draft, s3DraftOnly], 2, 1, 100)); // Draft mockFetch.mockImplementation(async (url: string) => { if (url.includes("version=published")) { return { ok: true, headers: new Headers(), json: async () => mockStoryListResponsePage([s1Pub, s2PubOnly], 2, 1, 100) }; } else { // draft return { ok: true, headers: new Headers(), json: async () => mockStoryListResponsePage([s1Draft, s3DraftOnly], 2, 1, 100) }; } }); // Search for comp_b, which is in s1's draft and s3's draft const result = await tool.handler({ component_name: 'comp_b' }); const resultJson = JSON.parse(result.content[0].text); expect(resultJson.usage_count).toBe(2); expect(resultJson.stories_analyzed_count).toBe(3); // s1 (draft), s2 (pub), s3 (draft) const foundIds = resultJson.used_in_stories.map((s:any) => s.id).sort(); expect(foundIds).toEqual([1, 3]); }); }); });

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/arb-ec/mcp-storyblok-server'

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