Skip to main content
Glama

1MCP Server

mcpRegistryClient.test.ts11.1 kB
// Import axios after mocking import axios from 'axios'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { MCPRegistryClient } from './mcpRegistryClient.js'; import type { RegistryServer } from './types.js'; // Mock axios instance const mockAxiosInstance = { get: vi.fn(), }; // Mock axios module vi.mock('axios', () => ({ default: { create: vi.fn(() => mockAxiosInstance), get: vi.fn(), isAxiosError: vi.fn(), }, isAxiosError: vi.fn(), })); describe('MCPRegistryClient', () => { let client: MCPRegistryClient; let mockServers: RegistryServer[]; beforeEach(() => { client = new MCPRegistryClient({ baseUrl: 'https://registry.test.com', timeout: 5000, cache: { defaultTtl: 300, maxSize: 100, cleanupInterval: 10000, }, }); mockServers = [ { $schema: 'https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json', name: 'file-server', description: 'A file management server', status: 'active', repository: { url: 'https://github.com/test/file-server', source: 'github', }, version: '1.0.0', remotes: [ { type: 'streamable-http', url: 'npx @test/file-server', }, ], _meta: { 'io.modelcontextprotocol.registry/official': { serverId: 'file-server-1', versionId: 'v1.0.0', publishedAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', isLatest: true, status: 'active', }, }, }, { $schema: 'https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json', name: 'database-server', description: 'A database integration server', status: 'active', repository: { url: 'https://github.com/test/database-server', source: 'github', }, version: '2.1.0', remotes: [ { type: 'sse', url: 'https://database.example.com/sse', }, ], _meta: { 'io.modelcontextprotocol.registry/official': { serverId: 'database-server-1', versionId: 'v1.0.1', publishedAt: '2024-01-02T00:00:00Z', updatedAt: '2024-01-02T00:00:00Z', isLatest: true, status: 'active', }, }, }, ]; // Reset mocks vi.clearAllMocks(); mockAxiosInstance.get.mockReset(); }); afterEach(() => { client.destroy(); }); describe('getServers', () => { it('should fetch servers successfully', async () => { const mockServerResponses = mockServers.map((server) => ({ server, _meta: { 'io.modelcontextprotocol.registry/official': server._meta['io.modelcontextprotocol.registry/official'], }, })); mockAxiosInstance.get.mockResolvedValueOnce({ data: { servers: mockServerResponses, metadata: { count: 2 }, }, }); const result = await client.getServers(); expect(mockAxiosInstance.get).toHaveBeenCalledWith('https://registry.test.com/v0/servers'); expect(result).toEqual(mockServers); }); it('should handle query parameters', async () => { const mockServerResponses = mockServers.map((server) => ({ server, _meta: { 'io.modelcontextprotocol.registry/official': server._meta['io.modelcontextprotocol.registry/official'], }, })); mockAxiosInstance.get.mockResolvedValueOnce({ data: { servers: mockServerResponses, metadata: { count: 2 }, }, }); await client.getServers({ limit: 10, cursor: 'test-cursor-123' }); expect(mockAxiosInstance.get).toHaveBeenCalledWith( 'https://registry.test.com/v0/servers?limit=10&cursor=test-cursor-123', ); }); it('should use cache on second request', async () => { const mockServerResponses = mockServers.map((server) => ({ server, _meta: { 'io.modelcontextprotocol.registry/official': server._meta['io.modelcontextprotocol.registry/official'], }, })); mockAxiosInstance.get.mockResolvedValueOnce({ data: { servers: mockServerResponses, metadata: { count: 2 }, }, }); // First request const result1 = await client.getServers(); expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1); // Second request should use cache const result2 = await client.getServers(); expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1); // Still only called once expect(result2).toEqual(result1); }); it('should handle HTTP errors', async () => { const axiosError = { response: { status: 500, statusText: 'Internal Server Error', }, }; mockAxiosInstance.get.mockRejectedValueOnce(axiosError); vi.mocked(axios.isAxiosError).mockReturnValueOnce(true); await expect(client.getServers()).rejects.toThrow('Failed to fetch servers from registry'); }); it('should handle request timeout', async () => { const shortTimeoutClient = new MCPRegistryClient({ baseUrl: 'https://registry.test.com', timeout: 10, // 10ms timeout }); const timeoutError = { code: 'ECONNABORTED', }; mockAxiosInstance.get.mockRejectedValueOnce(timeoutError); vi.mocked(axios.isAxiosError).mockReturnValueOnce(true); await expect(shortTimeoutClient.getServers()).rejects.toThrow('Failed to fetch servers from registry'); shortTimeoutClient.destroy(); }); }); describe('getServerById', () => { it('should fetch server by ID successfully', async () => { mockAxiosInstance.get.mockResolvedValueOnce({ data: mockServers[0], }); const result = await client.getServerById('file-server-1'); expect(mockAxiosInstance.get).toHaveBeenCalledWith('https://registry.test.com/v0/servers/file-server-1'); expect(result).toEqual(mockServers[0]); }); it('should encode server ID in URL', async () => { mockAxiosInstance.get.mockResolvedValueOnce({ data: mockServers[0], }); await client.getServerById('server with spaces'); expect(mockAxiosInstance.get).toHaveBeenCalledWith('https://registry.test.com/v0/servers/server%20with%20spaces'); }); }); describe('searchServers', () => { it('should search servers with query parameters', async () => { const filteredServers = mockServers.filter((s) => s.name.includes('file')); const mockServerResponses = filteredServers.map((server) => ({ server, _meta: { 'io.modelcontextprotocol.registry/official': server._meta['io.modelcontextprotocol.registry/official'], }, })); mockAxiosInstance.get.mockResolvedValueOnce({ data: { servers: mockServerResponses, metadata: { count: 1 }, }, }); const result = await client.searchServers({ query: 'file', status: 'active', registry_type: 'npm', limit: 10, }); expect(mockAxiosInstance.get).toHaveBeenCalledWith('https://registry.test.com/v0/servers?limit=10&search=file'); expect(result).toHaveLength(1); expect(result[0].name).toBe('file-server'); }); it('should handle empty search results', async () => { mockAxiosInstance.get.mockResolvedValueOnce({ data: { servers: [], metadata: { count: 0 }, }, }); const result = await client.searchServers({ query: 'nonexistent' }); expect(result).toEqual([]); }); }); describe('getRegistryStatus', () => { it('should get basic registry status', async () => { mockAxiosInstance.get.mockResolvedValueOnce({ data: { status: 'ok', github_client_id: 'test-client-id' }, }); const result = await client.getRegistryStatus(); expect(result).toMatchObject({ available: true, url: 'https://registry.test.com', response_time_ms: expect.any(Number), last_updated: expect.any(String), github_client_id: 'test-client-id', }); expect(result.stats).toBeUndefined(); }); it('should get registry status with statistics', async () => { mockAxiosInstance.get .mockResolvedValueOnce({ data: { status: 'ok', github_client_id: 'test-client-id' }, }) .mockResolvedValueOnce({ data: { servers: mockServers.map((server) => ({ server, _meta: { 'io.modelcontextprotocol.registry/official': server._meta['io.modelcontextprotocol.registry/official'], }, })), metadata: { count: 2 }, }, }); const result = await client.getRegistryStatus(true); expect(result.stats).toBeDefined(); expect(result.stats).toMatchObject({ total_servers: 2, active_servers: 2, deprecated_servers: 0, by_registry_type: { unknown: 2 }, by_transport: { 'streamable-http': 1, sse: 1 }, }); }); it('should handle registry unavailable', async () => { mockAxiosInstance.get.mockRejectedValueOnce(new Error('Network error')); vi.mocked(axios.isAxiosError).mockReturnValueOnce(false); const result = await client.getRegistryStatus(); expect(result).toMatchObject({ available: false, url: 'https://registry.test.com', response_time_ms: expect.any(Number), last_updated: expect.any(String), }); }); }); describe('cache management', () => { it('should invalidate cache by pattern', async () => { mockAxiosInstance.get.mockResolvedValue({ data: { servers: mockServers, metadata: { count: 2 }, }, }); // Make initial request await client.getServers(); expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1); // Request should use cache await client.getServers(); expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1); // Invalidate cache await client.invalidateCache('/servers.*'); // New request should hit the server again await client.getServers(); expect(mockAxiosInstance.get).toHaveBeenCalledTimes(2); }); it('should provide cache statistics', async () => { mockAxiosInstance.get.mockResolvedValue({ data: { servers: mockServers, metadata: { count: 2 }, }, }); await client.getServers(); const stats = client.getCacheStats(); expect(stats).toMatchObject({ totalEntries: expect.any(Number), validEntries: expect.any(Number), maxSize: 100, }); }); }); });

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/1mcp-app/agent'

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