Skip to main content
Glama
discoveryHandlers.test.ts20.2 kB
/** * Unit tests for discovery handlers * * This module tests the MCP discovery and information tool handlers * to ensure they return properly structured data matching their output schemas. */ import { FlagManager } from '@src/core/flags/flagManager.js'; import { AdapterFactory } from '@src/core/tools/internal/adapters/index.js'; import type { RegistryServer } from '@src/domains/registry/types.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { cleanupDiscoveryHandlers, handleMcpInfo, handleMcpRegistryInfo, handleMcpRegistryList, handleMcpRegistryStatus, handleMcpSearch, } from './discoveryHandlers.js'; import { McpInfoOutputSchema, McpRegistryInfoOutputSchema, McpRegistryListOutputSchema, McpRegistryStatusOutputSchema, McpSearchOutputSchema, } from './schemas/discovery.js'; // Mock dependencies vi.mock('@src/core/flags/flagManager.js'); vi.mock('@src/logger/logger.js', () => ({ default: { error: vi.fn(), }, debugIf: vi.fn(), infoIf: vi.fn(), warnIf: vi.fn(), errorIf: vi.fn(), logger: { error: vi.fn(), }, })); // Mock adapters vi.mock('@src/core/tools/internal/adapters/index.js', () => ({ AdapterFactory: { getDiscoveryAdapter: vi.fn(), cleanup: vi.fn(), reset: vi.fn(), }, })); describe('discoveryHandlers', () => { let flagManager: any; let mockDiscoveryAdapter: any; beforeEach(() => { vi.clearAllMocks(); flagManager = { isToolEnabled: vi.fn().mockReturnValue(true), } as any; (FlagManager.getInstance as any).mockReturnValue(flagManager); // Mock discovery adapter mockDiscoveryAdapter = { searchServers: vi.fn(), getServerById: vi.fn(), getRegistryStatus: vi.fn(), discoverInstalledApps: vi.fn(), discoverAppConfigs: vi.fn(), checkAppConsolidationStatus: vi.fn(), destroy: vi.fn(), }; (AdapterFactory.getDiscoveryAdapter as any).mockReturnValue(mockDiscoveryAdapter); }); afterEach(() => { cleanupDiscoveryHandlers(); vi.restoreAllMocks(); }); describe('handleMcpSearch', () => { it('should return structured data matching output schema', async () => { const mockServers: RegistryServer[] = [ { name: 'test-server', description: 'Test MCP server', version: '1.0.0', status: 'active', repository: { source: 'github', url: 'https://github.com/test/test-server', }, packages: [ { identifier: 'test-server', registryType: 'npm', transport: { type: 'stdio', }, }, ], _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: '2023-01-01T00:00:00Z', status: 'active', updatedAt: '2023-01-01T00:00:00Z', }, tags: ['test'], author: 'Test Author', downloads: 1000, }, }, ]; mockDiscoveryAdapter.searchServers.mockResolvedValue(mockServers); const args = { query: 'test', status: 'active' as const, limit: 10, cursor: undefined, transport: undefined, tags: undefined, category: undefined, format: 'json' as const, offset: undefined, type: undefined, }; const result = await handleMcpSearch(args); expect(result).toEqual({ results: expect.arrayContaining([ expect.objectContaining({ name: 'test-server', version: '1.0.0', description: 'Test MCP server', author: 'Test Author', tags: ['test'], transport: ['stdio'], registry: 'official', downloads: 1000, installationMethod: 'package', installationHint: 'Installable via npm packages', prerequisiteHint: '', hasEnvironmentVariables: false, hasPackageArguments: false, hasRuntimeArguments: false, installable: true, }), ]), total: 1, query: 'test', registry: 'official', }); // Verify schema validation const validated = McpSearchOutputSchema.parse(result); expect(validated).toBeDefined(); expect(mockDiscoveryAdapter.searchServers).toHaveBeenCalledWith('test', { limit: 10, cursor: undefined, transport: undefined, status: 'active', registry_type: undefined, }); }); it('should handle empty results', async () => { mockDiscoveryAdapter.searchServers.mockResolvedValue([]); const args = { query: 'nonexistent', status: 'active' as const, format: 'table' as const, limit: 20, offset: undefined, transport: undefined, tags: undefined, category: undefined, cursor: undefined, type: undefined, }; const result = await handleMcpSearch(args); expect(result).toEqual({ results: [], total: 0, query: 'nonexistent', registry: 'official', }); // Verify schema validation const validated = McpSearchOutputSchema.parse(result); expect(validated).toBeDefined(); }); it('should handle servers without packages', async () => { const mockServers: RegistryServer[] = [ { name: 'no-packages-server', description: 'Server without packages', version: '1.0.0', status: 'active', repository: { source: 'github', url: 'https://github.com/test/no-packages-server', }, _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: '2023-01-01T00:00:00Z', status: 'active', updatedAt: '2023-01-01T00:00:00Z', }, }, }, ]; mockDiscoveryAdapter.searchServers.mockResolvedValue(mockServers); const args = { query: 'no-packages', status: 'active' as const, format: 'table' as const, limit: 20, offset: undefined, transport: undefined, tags: undefined, category: undefined, cursor: undefined, type: undefined, }; const result = await handleMcpSearch(args); expect(result.results[0].transport).toEqual([]); }); it('should throw error on adapter failure', async () => { mockDiscoveryAdapter.searchServers.mockRejectedValue(new Error('Search failed')); const args = { query: 'test', status: 'active' as const, format: 'table' as const, limit: 20, offset: undefined, transport: undefined, tags: undefined, category: undefined, cursor: undefined, type: undefined, }; await expect(handleMcpSearch(args)).rejects.toThrow('Search failed: Search failed'); }); }); describe('handleMcpRegistryStatus', () => { it('should return structured data for online registry', async () => { const mockStatus = { available: true, url: 'https://registry.modelcontextprotocol.io', response_time_ms: 150, last_updated: '2023-01-01T00:00:00Z', stats: { total_servers: 100, active_servers: 80, }, }; mockDiscoveryAdapter.getRegistryStatus.mockResolvedValue(mockStatus); const args = { registry: 'official', includeStats: true, }; const result = await handleMcpRegistryStatus(args); expect(result).toEqual({ registry: 'official', status: 'online', responseTime: 150, lastCheck: '2023-01-01T00:00:00Z', error: undefined, metadata: { version: '1.0.0', supportedFormats: ['json', 'table'], totalServers: 100, active_servers: 80, }, }); // Verify schema validation const validated = McpRegistryStatusOutputSchema.parse(result); expect(validated).toBeDefined(); expect(mockDiscoveryAdapter.getRegistryStatus).toHaveBeenCalledWith(true); }); it('should return offline status for unavailable registry', async () => { const mockStatus = { available: false, url: 'https://registry.modelcontextprotocol.io', response_time_ms: 5000, last_updated: '2023-01-01T00:00:00Z', }; mockDiscoveryAdapter.getRegistryStatus.mockResolvedValue(mockStatus); const args = { registry: 'official', includeStats: false, }; const result = await handleMcpRegistryStatus(args); expect(result.status).toBe('offline'); expect(result.error).toBe('Registry unavailable'); }); it('should throw error on adapter failure', async () => { mockDiscoveryAdapter.getRegistryStatus.mockRejectedValue(new Error('Registry error')); const args = { registry: 'official', includeStats: false }; await expect(handleMcpRegistryStatus(args)).rejects.toThrow('Registry status check failed: Registry error'); }); }); describe('handleMcpRegistryInfo', () => { it('should return structured data with default registry', async () => { const args = { registry: 'official', }; const result = await handleMcpRegistryInfo(args); expect(result).toEqual({ name: 'official', url: 'https://registry.modelcontextprotocol.io', description: 'The official Model Context Protocol server registry', version: '1.0.0', supportedFormats: ['json', 'table'], features: ['search', 'get', 'list'], statistics: { totalPackages: 150, lastUpdated: expect.any(String), }, }); // Verify schema validation const validated = McpRegistryInfoOutputSchema.parse(result); expect(validated).toBeDefined(); }); it('should return structured data with custom registry', async () => { const args = { registry: 'custom', }; const result = await handleMcpRegistryInfo(args); expect(result.name).toBe('custom'); }); }); describe('handleMcpRegistryList', () => { it('should return structured data without stats', async () => { const args = { includeStats: false, }; const result = await handleMcpRegistryList(args); expect(result).toEqual({ registries: expect.arrayContaining([ { name: 'Official MCP Registry', url: 'https://registry.modelcontextprotocol.io', status: 'online', description: 'The official Model Context Protocol server registry', packageCount: undefined, }, { name: 'Community Registry', url: 'https://community-registry.modelcontextprotocol.io', status: 'online', description: 'Community-contributed MCP servers', packageCount: undefined, }, { name: 'Experimental Registry', url: 'https://experimental-registry.modelcontextprotocol.io', status: 'unknown', description: 'Experimental and cutting-edge MCP servers', packageCount: undefined, }, ]), total: 3, }); // Verify schema validation const validated = McpRegistryListOutputSchema.parse(result); expect(validated).toBeDefined(); }); it('should return structured data with stats', async () => { const args = { includeStats: true, }; const result = await handleMcpRegistryList(args); expect(result.registries[0].packageCount).toBe(150); expect(result.registries[1].packageCount).toBe(75); expect(result.registries[2].packageCount).toBe(25); }); it('should include all expected registries', async () => { const args = { includeStats: false, }; const result = await handleMcpRegistryList(args); const registryNames = result.registries.map((r) => r.name); expect(registryNames).toContain('Official MCP Registry'); expect(registryNames).toContain('Community Registry'); expect(registryNames).toContain('Experimental Registry'); }); }); describe('handleMcpInfo', () => { it('should return structured data for found server', async () => { const mockServer: RegistryServer = { name: 'test-server', description: 'Test MCP server', version: '1.0.0', status: 'active', repository: { source: 'github', url: 'https://github.com/test/test-server', }, packages: [ { identifier: 'test-server', registryType: 'npm', transport: { type: 'stdio', }, }, ], _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: '2023-01-01T00:00:00Z', status: 'active', updatedAt: '2023-01-01T00:00:00Z', }, tags: ['test', 'mcp'], capabilities: { tools: { count: 5, list: true, }, resources: { count: 3, subscribe: true, list: true, }, prompts: { count: 2, list: false, }, }, }, }; mockDiscoveryAdapter.getServerById.mockResolvedValue(mockServer); const args = { name: 'test-server', includeCapabilities: true, includeConfig: true, format: 'table' as const, }; const result = await handleMcpInfo(args); expect(result).toEqual({ server: { name: 'test-server', status: 'unknown', transport: 'stdio', }, configuration: { command: 'test-server', tags: ['test', 'mcp'], autoRestart: false, enabled: true, }, capabilities: { tools: expect.arrayContaining([ { name: 'tool_0', description: 'Tool 0' }, { name: 'tool_1', description: 'Tool 1' }, { name: 'tool_2', description: 'Tool 2' }, { name: 'tool_3', description: 'Tool 3' }, { name: 'tool_4', description: 'Tool 4' }, ]), resources: expect.arrayContaining([ { uri: 'resource://0', name: 'Resource 0' }, { uri: 'resource://1', name: 'Resource 1' }, { uri: 'resource://2', name: 'Resource 2' }, ]), prompts: expect.arrayContaining([ { name: 'prompt_0', description: 'Prompt 0' }, { name: 'prompt_1', description: 'Prompt 1' }, ]), }, health: { status: 'unknown', lastCheck: expect.any(String), }, }); // Verify schema validation const validated = McpInfoOutputSchema.parse(result); expect(validated).toBeDefined(); expect(mockDiscoveryAdapter.getServerById).toHaveBeenCalledWith('test-server', undefined); }); it('should return structured data for server not found', async () => { mockDiscoveryAdapter.getServerById.mockResolvedValue(null); const args = { name: 'nonexistent-server', includeCapabilities: true, includeConfig: true, format: 'table' as const, }; const result = await handleMcpInfo(args); expect(result).toEqual({ server: { name: 'nonexistent-server', status: 'unknown', transport: 'stdio', }, }); expect(mockDiscoveryAdapter.getServerById).toHaveBeenCalledWith('nonexistent-server', undefined); }); it('should handle server without capabilities', async () => { const mockServer: RegistryServer = { name: 'basic-server', description: 'Basic MCP server', version: '1.0.0', status: 'active', repository: { source: 'github', url: 'https://github.com/test/basic-server', }, _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: '2023-01-01T00:00:00Z', status: 'active', updatedAt: '2023-01-01T00:00:00Z', }, }, }; mockDiscoveryAdapter.getServerById.mockResolvedValue(mockServer); const args = { name: 'basic-server', includeCapabilities: false, includeConfig: false, format: 'table' as const, }; const result = await handleMcpInfo(args); expect(result.capabilities?.tools).toEqual([]); expect(result.capabilities?.resources).toEqual([]); expect(result.capabilities?.prompts).toEqual([]); }); it('should throw error on adapter failure', async () => { mockDiscoveryAdapter.getServerById.mockRejectedValue(new Error('Server lookup failed')); const args = { name: 'test-server', includeCapabilities: false, includeConfig: false, format: 'table' as const, }; await expect(handleMcpInfo(args)).rejects.toThrow('Server info check failed: Server lookup failed'); }); }); describe('Schema Validation', () => { it('should validate all handlers return data matching their schemas', async () => { // Mock all adapter calls for comprehensive validation const mockServer: RegistryServer = { name: 'test-server', description: 'Test MCP server', version: '1.0.0', status: 'active', repository: { source: 'github', url: 'https://github.com/test/test-server', }, packages: [ { identifier: 'test-server', registryType: 'npm', transport: { type: 'stdio', }, }, ], _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: '2023-01-01T00:00:00Z', status: 'active', updatedAt: '2023-01-01T00:00:00Z', }, }, }; const mockStatus = { available: true, url: 'https://registry.modelcontextprotocol.io', response_time_ms: 150, last_updated: '2023-01-01T00:00:00Z', }; mockDiscoveryAdapter.searchServers.mockResolvedValue([mockServer]); mockDiscoveryAdapter.getRegistryStatus.mockResolvedValue(mockStatus); mockDiscoveryAdapter.getServerById.mockResolvedValue(mockServer); // Test all handlers and validate schemas const searchResult = await handleMcpSearch({ query: 'test', status: 'all', limit: 10, format: 'table' }); const statusResult = await handleMcpRegistryStatus({ registry: 'official', includeStats: true }); const infoResult = await handleMcpRegistryInfo({ registry: 'official' }); const listResult = await handleMcpRegistryList({ includeStats: true }); const serverInfoResult = await handleMcpInfo({ name: 'test-server', includeCapabilities: true, includeConfig: true, format: 'table', }); // All these should not throw if schemas match expect(McpSearchOutputSchema.parse(searchResult)).toBeDefined(); expect(McpRegistryStatusOutputSchema.parse(statusResult)).toBeDefined(); expect(McpRegistryInfoOutputSchema.parse(infoResult)).toBeDefined(); expect(McpRegistryListOutputSchema.parse(listResult)).toBeDefined(); expect(McpInfoOutputSchema.parse(serverInfoResult)).toBeDefined(); }); }); describe('cleanupDiscoveryHandlers', () => { it('should cleanup without errors', () => { expect(() => cleanupDiscoveryHandlers()).not.toThrow(); }); }); });

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

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