Skip to main content
Glama
serverInstallationService.test.ts36.2 kB
import { getAllServers, getInstallationMetadata, getServer, setServer, } from '@src/commands/mcp/utils/mcpServerConfig.js'; import type { RegistryServer } from '@src/domains/registry/types.js'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createRegistryClient } from '../registry/mcpRegistryClient.js'; import { getProgressTrackingService } from './progressTrackingService.js'; import { createServerInstallationService, ServerInstallationService } from './serverInstallationService.js'; import { compareVersions, getUpdateType } from './services/versionResolver.js'; // Helper function to create mock RegistryServer objects with correct interface function _createMockRegistryServer(overrides: Partial<RegistryServer> = {}): RegistryServer { return { name: 'test-server', description: 'Test server description', status: 'active' as const, version: '1.0.0', repository: { source: 'github', url: 'https://github.com/test/test-server', }, _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: new Date().toISOString(), status: 'active' as const, updatedAt: new Date().toISOString(), }, }, ...overrides, } as RegistryServer; } // Mock all external dependencies vi.mock('@src/commands/mcp/utils/mcpServerConfig.js', () => ({ getAllServers: vi.fn(), getInstallationMetadata: vi.fn(), getServer: vi.fn(), setServer: vi.fn(), })); vi.mock('../registry/mcpRegistryClient.js', () => ({ createRegistryClient: vi.fn(() => ({ getServerById: vi.fn(), searchServers: vi.fn(), })), })); vi.mock('./services/versionResolver.js', () => ({ compareVersions: vi.fn(), getUpdateType: vi.fn(), })); vi.mock('./progressTrackingService.js', () => ({ getProgressTrackingService: vi.fn(() => ({ startOperation: vi.fn(), updateProgress: vi.fn(), completeOperation: vi.fn(), failOperation: vi.fn(), })), })); vi.mock('@src/logger/logger.js', () => ({ default: { info: vi.fn(), error: vi.fn(), debug: vi.fn(), warn: vi.fn(), }, })); vi.mock('@src/constants', () => ({ MCP_SERVER_VERSION: '0.27.3-test', MCP_CONFIG_FILE: 'mcp.json', MCP_INSTRUCTIONS_TEMPLATE_FILE: 'instructions-template.md', MCP_SERVER_NAME: '1mcp', MCP_URI_SEPARATOR: '_1mcp_', MCP_SERVER_CAPABILITIES: { completions: {}, resources: { listChanged: true }, tools: { listChanged: true }, prompts: { listChanged: true }, logging: {}, }, MCP_CLIENT_CAPABILITIES: { roots: { listChanged: false }, sampling: { listChanged: false }, elicitation: { listChanged: false }, }, PORT: 3050, HOST: '127.0.0.1', SSE_ENDPOINT: '/sse', MESSAGES_ENDPOINT: '/messages', STREAMABLE_HTTP_ENDPOINT: '/mcp', HEALTH_ENDPOINT: '/health', CONNECTION_RETRY: { MAX_ATTEMPTS: 3, INITIAL_DELAY_MS: 1000, }, AUTH_CONFIG: { SERVER: { DEFAULT_ENABLED: false, STORAGE: { DIR: 'sessions', FILE_EXTENSION: '.json' }, SESSION: { TTL_MINUTES: 1440, ID_PREFIX: 'sess-', FILE_PREFIX: 'session_', SUBDIR: 'server' }, AUTH_CODE: { TTL_MS: 60000, ID_PREFIX: 'code-', FILE_PREFIX: 'auth_code_', SUBDIR: 'server' }, AUTH_REQUEST: { TTL_MS: 600000, ID_PREFIX: 'code-', FILE_PREFIX: 'auth_request_', SUBDIR: 'server' }, TOKEN: { TTL_MS: 86400000, ID_PREFIX: 'tk-' }, STREAMABLE_SESSION: { TTL_MS: 86400000, ID_PREFIX: 'stream-', FILE_PREFIX: 'streamable_session_', SUBDIR: 'transport', SAVE_POLICY: { REQUESTS: 100, INTERVAL_MS: 300000, FLUSH_INTERVAL_MS: 60000 }, }, CLIENT: { ID_PREFIX: 'client-', FILE_PREFIX: 'session_cli_', SUBDIR: 'server' }, }, CLIENT: { OAUTH: { TTL_MS: 2592000000, CODE_VERIFIER_TTL_MS: 600000, STATE_TTL_MS: 600000, DEFAULT_TOKEN_EXPIRY_SECONDS: 3600, DEFAULT_CALLBACK_PATH: '/oauth/callback', DEFAULT_SCOPES: [], }, SESSION: { TTL_MS: 2592000000, ID_PREFIX: 'oauth_', FILE_PREFIX: '', SUBDIR: 'client' }, PREFIXES: { CLIENT: 'cli_', TOKENS: 'tok_', VERIFIER: 'ver_', STATE: 'sta_' }, }, }, STORAGE_SUBDIRS: { SERVER: 'server', CLIENT: 'client', TRANSPORT: 'transport' }, FILE_PREFIX_MAPPING: { SERVER: ['session_', 'auth_code_', 'auth_request_'], CLIENT: ['oauth_', 'cli_', 'tok_', 'ver_', 'sta_'], TRANSPORT: ['streamable_session_'], }, RATE_LIMIT_CONFIG: { OAUTH: { WINDOW_MS: 900000, MAX: 100, MESSAGE: { error: 'Too many requests, please try again later.' } }, }, CONFIG_DIR_NAME: '1mcp', BACKUP_DIR_NAME: 'backups', DEFAULT_CONFIG: { mcpServers: {} }, getGlobalConfigDir: vi.fn(() => '/test/config'), getConfigDir: vi.fn(() => '/test/config'), getGlobalConfigPath: vi.fn(() => '/test/config/mcp.json'), getConfigPath: vi.fn(() => '/test/config/mcp.json'), getGlobalBackupDir: vi.fn(() => '/test/config/backups'), getAppBackupDir: vi.fn(() => '/test/config/backups/test'), getDefaultInstructionsTemplatePath: vi.fn(() => '/test/config/instructions-template.md'), })); describe('ServerInstallationService', () => { let service: ServerInstallationService; let mockRegistryClient: any; let mockRegistryClientInstance: any; let mockConfigUtils: any; let mockVersionResolver: any; beforeEach(() => { vi.clearAllMocks(); // Setup mock references - create mock instance and configure factory function mockRegistryClientInstance = { getServerById: vi.fn(), searchServers: vi.fn(), }; mockRegistryClient = vi.mocked(createRegistryClient); mockRegistryClient.mockReturnValue(mockRegistryClientInstance as any); // Create service after setting up mocks service = new ServerInstallationService(); mockConfigUtils = { getAllServers: vi.mocked(getAllServers), getServer: vi.mocked(getServer), setServer: vi.mocked(setServer), getInstallationMetadata: vi.mocked(getInstallationMetadata), }; mockVersionResolver = { compareVersions: vi.mocked(compareVersions), getUpdateType: vi.mocked(getUpdateType), }; // Initialize progress tracking service getProgressTrackingService(); }); describe('constructor', () => { it('should initialize with registry client and progress tracker', () => { // Clear mock counts before this specific test vi.clearAllMocks(); new ServerInstallationService(); expect(createRegistryClient).toHaveBeenCalledTimes(1); // Once for service only expect(getProgressTrackingService).toHaveBeenCalledTimes(1); }); }); describe('installServer', () => { const mockRegistryServer: RegistryServer = { name: 'test-server', version: '1.0.0', description: 'Test server', status: 'active', repository: { source: 'test', url: 'https://github.com/test/test-server', }, remotes: [ { type: 'streamable-http', url: 'npx:test-package', }, ], _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: '2024-01-01T00:00:00Z', status: 'active', updatedAt: '2024-01-01T00:00:00Z', }, }, }; it('should install server successfully with direct lookup', async () => { mockRegistryClientInstance.getServerById.mockResolvedValue(mockRegistryServer); const result = await service.installServer('test-server', '1.0.0'); expect(result.success).toBe(true); expect(result.serverName).toBe('test-server'); expect(result.version).toBe('1.0.0'); expect(result.operationId).toMatch(/^op_\d+_[a-z0-9]{7}$/); expect(result.installedAt).toBeInstanceOf(Date); expect(result.config).toEqual({ type: 'stdio', command: 'npx:test-package', tags: ['test-server'], // Deduplicated tags: [localServerName, registryServerId] }); }); it('should install server with search-based ID resolution fallback', async () => { // Direct lookup fails mockRegistryClientInstance.getServerById.mockRejectedValueOnce(new Error('Not found')); // Search succeeds mockRegistryClientInstance.searchServers.mockResolvedValue([ { name: 'io.github/user/test-server', version: '1.0.0' } as RegistryServer, ]); // Second direct lookup with full ID succeeds mockRegistryClientInstance.getServerById.mockResolvedValueOnce(mockRegistryServer); const result = await service.installServer('test-server'); expect(result.success).toBe(true); expect(result.serverName).toBe('test-server'); expect(mockRegistryClientInstance.searchServers).toHaveBeenCalledWith({ query: 'test-server', limit: 10, }); }); it('should prioritize streamable-http remote endpoint', async () => { const serverWithMultipleRemotes: RegistryServer = { name: 'multi-remote-server', version: '1.0.0', description: 'Server with multiple remotes', status: 'active', repository: { source: 'test', url: 'https://github.com/test/multi-remote-server', }, remotes: [ { type: 'docker', url: 'docker://some-image', }, { type: 'streamable-http', url: 'npx:test-package', }, { type: 'stdio', url: 'local://path', }, ], _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: '2024-01-01T00:00:00Z', status: 'active', updatedAt: '2024-01-01T00:00:00Z', }, }, } as RegistryServer; mockRegistryClientInstance.getServerById.mockResolvedValue(serverWithMultipleRemotes); const result = await service.installServer('multi-remote-server'); expect(result.success).toBe(true); expect(result.config?.command).toBe('npx:test-package'); }); it('should fallback to first available remote when no streamable-http', async () => { const serverWithoutStreamable: RegistryServer = { name: 'fallback-server', version: '1.0.0', description: 'Server without streamable-http', remotes: [ { type: 'docker', url: 'docker://some-image', }, { type: 'stdio', url: 'local://path', }, ], status: 'active', repository: { source: 'github', url: 'https://github.com/test/fallback-server' }, _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: new Date().toISOString(), status: 'active' as const, updatedAt: new Date().toISOString(), }, }, } as RegistryServer; mockRegistryClientInstance.getServerById.mockResolvedValue(serverWithoutStreamable); const result = await service.installServer('fallback-server'); expect(result.success).toBe(true); expect(result.config?.command).toBe('docker://some-image'); }); it('should handle server without remotes gracefully', async () => { const serverWithoutRemotes: RegistryServer = { name: 'no-remote-server', version: '1.0.0', description: 'Server without remotes', remotes: [], status: 'active', repository: { source: 'github', url: 'https://github.com/test/no-remote-server' } as any, _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: new Date().toISOString(), status: 'active' as const, updatedAt: new Date().toISOString(), }, } as any, } as any; mockRegistryClientInstance.getServerById.mockResolvedValue(serverWithoutRemotes); await expect(service.installServer('no-remote-server')).rejects.toThrow( 'No compatible installation method found for no-remote-server', ); }); it('should handle server not found in registry', async () => { mockRegistryClientInstance.getServerById.mockRejectedValue(new Error('Server not found')); mockRegistryClientInstance.searchServers.mockResolvedValue([]); await expect(service.installServer('non-existent-server')).rejects.toThrow( "Server 'non-existent-server' not found in registry. Suggestions:\n1. Check spelling: non-existent-server\n2. Search for available servers: 1mcp registry search non-existent-server\n3. Use interactive mode: 1mcp mcp install --interactive\n4. Use full registry ID (e.g., 'io.github.username/server-name')", ); }); it('should handle search failure with helpful error message', async () => { mockRegistryClientInstance.getServerById.mockRejectedValue(new Error('Server not found')); mockRegistryClientInstance.searchServers.mockRejectedValue(new Error('Search failed')); await expect(service.installServer('search-fail-server')).rejects.toThrow( `Server 'search-fail-server' not found in registry. Suggestions: 1. Check spelling: search-fail-server 2. Search for available servers: 1mcp registry search search-fail-server 3. Use interactive mode: 1mcp mcp install --interactive 4. Use full registry ID (e.g., 'io.github.username/server-name')`, ); }); it('should handle registry client errors', async () => { const networkError = new Error('Network error'); mockRegistryClientInstance.getServerById.mockRejectedValue(networkError); mockRegistryClientInstance.searchServers.mockRejectedValue(networkError); await expect(service.installServer('network-error-server')).rejects.toThrow('network-error-server'); }); it('should search with exact name match priority', async () => { const searchResults = [ { name: 'some-prefix-test-server-suffix', version: '1.0.0' }, { name: 'exact-test-server', version: '1.0.0' }, { name: 'test-server-related', version: '1.0.0' }, ]; mockRegistryClientInstance.getServerById.mockRejectedValueOnce(new Error('Not found')); mockRegistryClientInstance.searchServers.mockResolvedValue(searchResults); mockRegistryClientInstance.getServerById.mockResolvedValueOnce({ name: 'exact-test-server', version: '1.0.0', remotes: [{ type: 'streamable-http', url: 'npx:test-package' }], }); const result = await service.installServer('test-server'); expect(result.success).toBe(true); expect(result.serverName).toBe('test-server'); }); it('should search with partial match when no exact match', async () => { const searchResults = [ { name: 'io.github/user/test-server', version: '1.0.0' } as RegistryServer, { name: 'different-server', version: '1.0.0' }, ]; mockRegistryClientInstance.getServerById.mockRejectedValueOnce(new Error('Not found')); mockRegistryClientInstance.searchServers.mockResolvedValue(searchResults); mockRegistryClientInstance.getServerById.mockResolvedValueOnce({ name: 'io.github/user/test-server', version: '1.0.0', remotes: [{ type: 'streamable-http', url: 'npx:test-package' }], status: 'active', repository: { type: 'git', url: 'https://github.com/test/io.github-user-test-server' }, _meta: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), lastSync: new Date().toISOString(), source: 'official', verified: true, downloads: 0, stars: 0, }, } as any); const result = await service.installServer('test-server'); expect(result.success).toBe(true); expect(result.serverName).toBe('test-server'); }); it('should install server without specifying version', async () => { mockRegistryClientInstance.getServerById.mockResolvedValue(mockRegistryServer); const result = await service.installServer('test-server'); expect(result.success).toBe(true); expect(result.version).toBe('1.0.0'); expect(mockRegistryClientInstance.getServerById).toHaveBeenCalledWith('test-server', undefined); }); it('should generate unique operation IDs for multiple installations', async () => { mockRegistryClientInstance.getServerById.mockResolvedValue(mockRegistryServer); const result1 = await service.installServer('server1'); const result2 = await service.installServer('server2'); expect(result1.operationId).not.toBe(result2.operationId); expect(result1.operationId).toMatch(/^op_\d+_[a-z0-9]{7}$/); expect(result2.operationId).toMatch(/^op_\d+_[a-z0-9]{7}$/); }); }); describe('updateServer', () => { const mockCurrentConfig = { type: 'stdio', command: 'npx:test-package@1.0.0', }; it('should update server successfully', async () => { const updatedRegistryServer: RegistryServer = { name: 'test-server', version: '2.0.0', description: 'Updated test server', remotes: [{ type: 'streamable-http', url: 'npx:test-package@2.0.0' }], status: 'active', repository: { type: 'git', url: 'https://github.com/test/test-server' }, _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: new Date().toISOString(), status: 'active' as const, updatedAt: new Date().toISOString(), }, } as any, } as any; mockConfigUtils.getServer.mockReturnValue(mockCurrentConfig); mockRegistryClientInstance.getServerById.mockResolvedValue(updatedRegistryServer); const result = await service.updateServer('test-server', '2.0.0'); expect(result.success).toBe(true); expect(result.serverName).toBe('test-server'); expect(result.newVersion).toBe('2.0.0'); expect(result.updatedAt).toBeInstanceOf(Date); expect(result.operationId).toMatch(/^op_\d+_[a-z0-9]{7}$/); expect(mockConfigUtils.setServer).toHaveBeenCalledWith('test-server', mockCurrentConfig); }); it('should update server to latest version when no version specified', async () => { const latestRegistryServer: RegistryServer = { name: 'test-server', version: '2.1.0', description: 'Latest test server', remotes: [{ type: 'streamable-http', url: 'npx:test-package@2.1.0' }], status: 'active', repository: { source: 'github', url: 'https://github.com/test/test-server' } as any, _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: new Date().toISOString(), status: 'active' as const, updatedAt: new Date().toISOString(), }, } as any, } as any; mockConfigUtils.getServer.mockReturnValue(mockCurrentConfig); mockRegistryClientInstance.getServerById.mockResolvedValue(latestRegistryServer); const result = await service.updateServer('test-server'); expect(result.success).toBe(true); expect(result.newVersion).toBe('2.1.0'); expect(mockRegistryClientInstance.getServerById).toHaveBeenCalledWith('test-server', 'latest'); }); it('should handle update when server not found in configuration', async () => { mockConfigUtils.getServer.mockReturnValue(null); const result = await service.updateServer('non-existent-server'); expect(result.success).toBe(false); expect(result.errors).toContain("Server 'non-existent-server' not found in configuration"); }); it('should handle update when server not found in registry', async () => { mockConfigUtils.getServer.mockReturnValue(mockCurrentConfig); mockRegistryClientInstance.getServerById.mockRejectedValue( new Error("Server 'test-server' not found in registry"), ); const result = await service.updateServer('test-server'); expect(result.success).toBe(false); expect(result.errors).toContain("Server 'test-server' not found in registry"); }); it('should handle registry errors during update', async () => { mockConfigUtils.getServer.mockReturnValue(mockCurrentConfig); mockRegistryClientInstance.getServerById.mockRejectedValue(new Error('Network error')); const result = await service.updateServer('test-server'); expect(result.success).toBe(false); expect(result.errors[0]).toContain('Network error'); }); it('should generate unique operation IDs for updates', async () => { mockConfigUtils.getServer.mockReturnValue(mockCurrentConfig); mockRegistryClientInstance.getServerById.mockResolvedValue({ name: 'test-server', version: '2.0.0', remotes: [{ type: 'streamable-http', url: 'npx:test-package' }], }); const result1 = await service.updateServer('server1'); const result2 = await service.updateServer('server2'); expect(result1.operationId).not.toBe(result2.operationId); }); }); describe('uninstallServer', () => { it('should uninstall server successfully', async () => { const result = await service.uninstallServer('test-server'); expect(result.success).toBe(true); expect(result.serverName).toBe('test-server'); expect(result.removedAt).toBeInstanceOf(Date); expect(result.configRemoved).toBe(true); expect(result.operationId).toMatch(/^op_\d+_[a-z0-9]{7}$/); expect(result.warnings).toEqual([]); expect(result.errors).toEqual([]); }); it('should generate unique operation IDs for uninstalls', async () => { const result1 = await service.uninstallServer('server1'); const result2 = await service.uninstallServer('server2'); expect(result1.operationId).not.toBe(result2.operationId); expect(result1.operationId).toMatch(/^op_\d+_[a-z0-9]{7}$/); }); it('should handle uninstall with options', async () => { const options = { force: true, backup: true, removeConfig: false, verbose: true, }; const result = await service.uninstallServer('test-server', options); expect(result.success).toBe(true); // Note: Current implementation doesn't use options, but test verifies structure expect(result.configRemoved).toBe(true); // Default behavior }); }); describe('checkForUpdates', () => { beforeEach(() => { mockConfigUtils.getAllServers.mockReturnValue({ server1: { type: 'stdio' }, server2: { type: 'stdio' }, server3: { type: 'stdio' }, }); }); it('should check updates for all servers when no names provided', async () => { const registryServers = [ { name: 'server1', version: '1.1.0' }, { name: 'server2', version: '2.0.0' }, { name: 'server3', version: '1.0.0' }, ]; mockConfigUtils.getInstallationMetadata.mockImplementation((serverName: string) => { if (serverName === 'server1') return { version: '1.0.0' }; if (serverName === 'server2') return { version: '2.0.0' }; return null; // server3 has no metadata }); mockRegistryClientInstance.getServerById.mockImplementation((serverName: string) => { return registryServers.find((s) => s.name === serverName); }); mockVersionResolver.compareVersions.mockImplementation((newVersion: string, oldVersion: string) => { if (newVersion === '1.1.0' && oldVersion === '1.0.0') return 1; if (newVersion === '2.0.0' && oldVersion === '2.0.0') return 0; if (newVersion === '1.0.0' && oldVersion === '2.0.0') return -1; return 0; }); mockVersionResolver.getUpdateType.mockReturnValue('minor'); const results = await service.checkForUpdates(); expect(results).toHaveLength(3); expect(results[0]).toEqual({ serverName: 'server1', currentVersion: '1.0.0', latestVersion: '1.1.0', hasUpdate: true, updateAvailable: true, updateType: 'minor', }); expect(results[1]).toEqual({ serverName: 'server2', currentVersion: '2.0.0', latestVersion: '2.0.0', hasUpdate: false, updateAvailable: false, updateType: 'minor', }); expect(results[2]).toEqual({ serverName: 'server3', currentVersion: 'unknown', latestVersion: '1.0.0', hasUpdate: false, updateAvailable: false, updateType: undefined, }); }); it('should check updates for specific servers when names provided', async () => { mockConfigUtils.getInstallationMetadata.mockReturnValue({ version: '1.0.0' }); mockRegistryClientInstance.getServerById.mockResolvedValue({ name: 'server1', version: '1.1.0', description: 'Test server 1', status: 'active', repository: { type: 'git', url: 'https://github.com/test/server1' }, _meta: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), lastSync: new Date().toISOString(), source: 'official', verified: true, downloads: 0, stars: 0, }, } as any); mockVersionResolver.compareVersions.mockReturnValue(1); mockVersionResolver.getUpdateType.mockReturnValue('patch'); const results = await service.checkForUpdates(['server1']); expect(results).toHaveLength(1); expect(results[0].serverName).toBe('server1'); expect(mockRegistryClientInstance.getServerById).toHaveBeenCalledTimes(1); }); it('should handle servers with unknown version gracefully', async () => { mockConfigUtils.getInstallationMetadata.mockReturnValue(null); mockRegistryClientInstance.getServerById.mockResolvedValue({ name: 'server1', version: '1.0.0', description: 'Test server 1', status: 'active', repository: { type: 'git', url: 'https://github.com/test/server1' }, _meta: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), lastSync: new Date().toISOString(), source: 'official', verified: true, downloads: 0, stars: 0, }, } as any); const results = await service.checkForUpdates(['server1']); expect(results[0]).toEqual({ serverName: 'server1', currentVersion: 'unknown', latestVersion: '1.0.0', hasUpdate: false, updateAvailable: false, updateType: undefined, }); }); it('should handle servers not found in registry', async () => { mockConfigUtils.getInstallationMetadata.mockReturnValue({ version: '1.0.0' }); mockRegistryClientInstance.getServerById.mockRejectedValue(new Error('Server not found')); const results = await service.checkForUpdates(['missing-server']); expect(results).toHaveLength(0); // Silently skipped }); it('should handle version comparison correctly', async () => { mockConfigUtils.getInstallationMetadata.mockReturnValue({ version: '1.0.0' }); const testCases = [ { latest: '2.0.0', hasUpdate: true }, { latest: '1.1.0', hasUpdate: true }, { latest: '1.0.1', hasUpdate: true }, { latest: '1.0.0', hasUpdate: false }, { latest: '0.9.0', hasUpdate: false }, ]; for (const testCase of testCases) { mockRegistryClientInstance.getServerById.mockResolvedValue({ name: 'test-server', version: testCase.latest, description: 'Test server for version comparison', status: 'active', repository: { type: 'git', url: 'https://github.com/test/test-server' }, _meta: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), lastSync: new Date().toISOString(), source: 'official', verified: true, downloads: 0, stars: 0, }, } as any); mockVersionResolver.compareVersions.mockReturnValue( testCase.latest === '1.0.0' ? 0 : testCase.latest > '1.0.0' ? 1 : -1, ); const results = await service.checkForUpdates(['test-server']); expect(results[0].hasUpdate).toBe(testCase.hasUpdate); expect(results[0].updateAvailable).toBe(testCase.hasUpdate); } }); it('should skip servers that cannot be checked without failing', async () => { mockConfigUtils.getAllServers.mockReturnValue({ 'working-server': { type: 'stdio' }, 'failing-server': { type: 'stdio' }, 'another-working': { type: 'stdio' }, }); mockConfigUtils.getInstallationMetadata.mockImplementation((_serverName: string) => ({ version: '1.0.0', })); mockRegistryClientInstance.getServerById.mockImplementation((currentServerName: string) => { if (currentServerName === 'failing-server') { throw new Error(`Network error for ${currentServerName}`); } return Promise.resolve({ name: currentServerName, version: '1.0.0', description: `Test server ${currentServerName}`, status: 'active', repository: { type: 'git', url: `https://github.com/test/${currentServerName}` }, _meta: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), lastSync: new Date().toISOString(), source: 'official', verified: true, downloads: 0, stars: 0, }, } as any); }); mockVersionResolver.compareVersions.mockReturnValue(0); const results = await service.checkForUpdates(); expect(results).toHaveLength(2); // Only working servers expect(results.map((r) => r.serverName)).toEqual(['working-server', 'another-working']); }); }); describe('listInstalledServers', () => { const mockServers = { server1: { type: 'stdio', disabled: false }, server2: { type: 'stdio', disabled: true }, server3: { type: 'stdio', disabled: false }, }; beforeEach(() => { mockConfigUtils.getAllServers.mockReturnValue(mockServers); }); it('should list all installed servers', async () => { const result = await service.listInstalledServers(); expect(result).toEqual(['server1', 'server2', 'server3']); expect(mockConfigUtils.getAllServers).toHaveBeenCalled(); }); it('should filter only active servers when requested', async () => { const result = await service.listInstalledServers({ filterActive: true }); expect(result).toEqual(['server1', 'server3']); // Excludes disabled server2 }); it('should handle empty server list', async () => { mockConfigUtils.getAllServers.mockReturnValue({}); const result = await service.listInstalledServers(); expect(result).toEqual([]); }); it('should handle servers without disabled property', async () => { const serversWithoutDisabled = { server1: { type: 'stdio' }, server2: { type: 'stdio' }, }; mockConfigUtils.getAllServers.mockReturnValue(serversWithoutDisabled); const result = await service.listInstalledServers({ filterActive: true }); expect(result).toEqual(['server1', 'server2']); // All treated as active }); it('should handle additional list options', async () => { const options = { filterActive: true, includeDisabled: false, includeOutdated: false, }; const result = await service.listInstalledServers(options); expect(result).toEqual(['server1', 'server3']); }); }); describe('integration scenarios', () => { it('should handle complex installation workflow', async () => { const complexServer: RegistryServer = { name: 'complex-server', version: '1.0.0', description: 'Complex server with configuration', remotes: [ { type: 'docker', url: 'docker://complex-server:1.0.0' }, { type: 'streamable-http', url: 'npx:complex-server@1.0.0' }, ], status: 'active', repository: { source: 'github', url: 'https://github.com/test/complex-server' } as any, _meta: { 'io.modelcontextprotocol.registry/official': { isLatest: true, publishedAt: new Date().toISOString(), status: 'active' as const, updatedAt: new Date().toISOString(), }, } as any, } as any; // Simulate search-based resolution mockRegistryClientInstance.getServerById.mockRejectedValueOnce(new Error('Not found')); mockRegistryClientInstance.searchServers.mockResolvedValue([ { name: 'io.github/user/complex-server', version: '1.0.0' }, ]); mockRegistryClientInstance.getServerById.mockResolvedValueOnce(complexServer); const result = await service.installServer('complex-server'); expect(result.success).toBe(true); expect(result.config?.command).toBe('npx:complex-server@1.0.0'); // Prioritized streamable-http expect(mockRegistryClientInstance.searchServers).toHaveBeenCalled(); }); it('should handle error recovery scenarios', async () => { // Test network error handling with search fallback mockRegistryClientInstance.getServerById.mockRejectedValueOnce(new Error('Network timeout')); mockRegistryClientInstance.searchServers.mockRejectedValueOnce(new Error('Search service unavailable')); await expect(service.installServer('unreachable-server')).rejects.toThrow( "Server 'unreachable-server' not found in registry. Suggestions:", ); // Verify both direct lookup and search were attempted expect(mockRegistryClientInstance.getServerById).toHaveBeenCalledTimes(1); expect(mockRegistryClientInstance.searchServers).toHaveBeenCalledTimes(1); }); it('should maintain operation ID consistency across methods', async () => { mockConfigUtils.getAllServers.mockReturnValue({ 'test-server': { type: 'stdio' }, }); mockConfigUtils.getInstallationMetadata.mockReturnValue({ version: '1.0.0' }); mockRegistryClientInstance.getServerById.mockResolvedValue({ name: 'test-server', version: '1.0.0', remotes: [{ type: 'streamable-http', url: 'npx:test' }], }); mockConfigUtils.getServer.mockReturnValue({ type: 'stdio' }); const installResult = await service.installServer('test-server'); const updateResult = await service.updateServer('test-server'); const uninstallResult = await service.uninstallServer('test-server'); await service.checkForUpdates(['test-server']); expect(installResult.operationId).toMatch(/^op_\d+_[a-z0-9]{7}$/); expect(updateResult.operationId).toMatch(/^op_\d+_[a-z0-9]{7}$/); expect(uninstallResult.operationId).toMatch(/^op_\d+_[a-z0-9]{7}$/); // All operation IDs should be different const operationIds = [installResult.operationId, updateResult.operationId, uninstallResult.operationId]; const uniqueIds = new Set(operationIds); expect(uniqueIds.size).toBe(3); }); }); }); describe('createServerInstallationService', () => { it('should create a new ServerInstallationService instance', () => { const service = createServerInstallationService(); expect(service).toBeInstanceOf(ServerInstallationService); }); it('should create separate instances', () => { const service1 = createServerInstallationService(); const service2 = createServerInstallationService(); expect(service1).not.toBe(service2); expect(service1).toBeInstanceOf(ServerInstallationService); expect(service2).toBeInstanceOf(ServerInstallationService); }); });

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