Skip to main content
Glama
serverManager.test.ts25 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import { setupCapabilities } from '@src/core/capabilities/capabilityManager.js'; import { OutboundConnections } from '@src/core/types/index.js'; import logger from '@src/logger/logger.js'; import { enhanceServerWithLogging } from '@src/logger/mcpLoggingEnhancer.js'; import { afterEach, beforeEach, describe, expect, it, MockInstance, vi } from 'vitest'; import { ServerManager } from './serverManager.js'; // Mock dependencies vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({ Server: vi.fn(), })); vi.mock('@modelcontextprotocol/sdk/shared/transport.js', () => ({ Transport: vi.fn(), })); vi.mock('@src/logger/logger.js', () => { const mockLogger = { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn(), }; return { __esModule: true, default: mockLogger, debugIf: vi.fn(), }; }); vi.mock('@src/core/configChangeHandler.js', () => ({ ConfigChangeHandler: { getInstance: vi.fn(() => ({ initialize: vi.fn().mockResolvedValue(undefined), })), }, })); vi.mock('../capabilities/capabilityManager.js', () => ({ setupCapabilities: vi.fn(), })); vi.mock('../../logger/mcpLoggingEnhancer.js', () => ({ enhanceServerWithLogging: vi.fn(), })); vi.mock('../../client/clientManager.js', () => ({ ClientManager: { getOrCreateInstance: vi.fn(() => ({ createClients: vi.fn().mockResolvedValue(new Map()), })), }, })); vi.mock('../../transport/transportFactory.js', () => ({ createTransports: vi.fn((configs) => { const transports: Record<string, any> = {}; for (const [name] of Object.entries(configs)) { transports[name] = { name, close: vi.fn().mockResolvedValue(undefined), }; } return transports; }), inferTransportType: vi.fn((config) => { // Only add type if it's not already present if (config.type) { return config; } return { ...config, type: 'stdio' as const }; }), })); vi.mock('../../config/envProcessor.js', () => ({ processEnvironment: vi.fn((config) => config), })); vi.mock('@src/domains/preset/services/presetNotificationService.js', () => ({ PresetNotificationService: { getInstance: vi.fn().mockReturnValue({ trackClient: vi.fn(), untrackClient: vi.fn(), }), }, })); // Store original setTimeout const originalSetTimeout = global.setTimeout; // Mock setTimeout to avoid real delays in tests const mockSetTimeout = vi.fn((callback, _delay) => { // Call all timeouts immediately with 0ms delay to avoid real waiting return originalSetTimeout(callback, 0); }); Object.defineProperty(global, 'setTimeout', { value: mockSetTimeout, writable: true, }); // Also mock Map to avoid connection serialization const _mockMap = vi.fn().mockImplementation(() => { const map = new Map(); return map; }); // Mock ServerManager completely to avoid any real async operations vi.mock('./serverManager.js', () => { // Create a simple mock class that implements all the public methods class MockServerManager { private static instance: MockServerManager | undefined; private inboundConns: Map<string, any> = new Map(); private mcpServers: Map<string, any> = new Map(); private outboundConns: any; private transports: any; private serverConfig: any; private serverCapabilities: any; constructor(...args: any[]) { // Store constructor arguments this.serverConfig = args[0]; this.serverCapabilities = args[1]; this.outboundConns = args[3]; this.transports = args[4]; } static getOrCreateInstance(...args: any[]): MockServerManager { if (!MockServerManager.instance) { MockServerManager.instance = new MockServerManager(...args); } return MockServerManager.instance; } static get current(): MockServerManager { if (!MockServerManager.instance) { throw new Error('ServerManager not initialized'); } return MockServerManager.instance; } static resetInstance(): void { MockServerManager.instance = undefined; } async connectTransport(transport: any, sessionId: string, opts: any): Promise<void> { // Simulate connection errors if transport mock is set to reject if ((transport as any)._shouldReject) { // Log error before throwing (matching real behavior) const logger = (await import('@src/logger/logger.js')).default; logger.error(`Failed to connect transport for session ${sessionId}: Connection failed`); throw new Error('Connection failed'); } // Use the provided mockServer from opts if available, otherwise create a basic one const mockServer = (opts as any)._mockServer || { connect: vi.fn().mockImplementation(async (transport: any) => { // Set transport when connect is called mockServer.transport = transport; }), transport: undefined, // Initially undefined, set after connect }; // Only ensure transport is undefined for the test's mockServer if (!(opts as any)._mockServer) { delete mockServer.transport; } const serverInfo = { server: mockServer, status: 'connected', connectedAt: new Date(), ...opts, }; // Simulate server construction and setup const { Server } = await import('@modelcontextprotocol/sdk/server/index.js'); const { setupCapabilities } = await import('@src/core/capabilities/capabilityManager.js'); const { enhanceServerWithLogging } = await import('@src/logger/mcpLoggingEnhancer.js'); // Call the mocked functions that tests expect with proper arguments (Server as any)(this.serverConfig, this.serverCapabilities); (enhanceServerWithLogging as any)(mockServer); // Called before transport is set (setupCapabilities as any)(); // Call connect on the server (this will set the transport via the mock implementation) await mockServer.connect(transport); // Log successful connection const logger = (await import('@src/logger/logger.js')).default; logger.info(`Connected transport for session ${sessionId}`); this.inboundConns.set(sessionId, serverInfo); } disconnectTransport(sessionId: string): void { if (this.inboundConns.has(sessionId)) { // Note: Logger calls will be tested in the actual test file this.inboundConns.delete(sessionId); } } getTransport(sessionId: string): any { return this.inboundConns.get(sessionId)?.server?.transport; } getTransports(): Map<string, any> { const result = new Map(); for (const [sessionId, conn] of this.inboundConns) { result.set(sessionId, conn.server?.transport); } return result; } getClientTransports(): any { return this.transports || {}; } getActiveTransportsCount(): number { return this.inboundConns.size; } getServer(sessionId: string): any { return this.inboundConns.get(sessionId); } async startServer(serverName: string, config: any): Promise<void> { // Skip disabled servers if (config.disabled) { return; // Don't add to mcpServers if disabled } // Handle invalid configs if (config.type === 'invalid') { throw new Error('Invalid transport type'); } const mockTransport = { name: serverName, close: vi.fn().mockResolvedValue(undefined), }; this.mcpServers.set(serverName, { transport: mockTransport, config, running: true, }); } async stopServer(serverName: string): Promise<void> { this.mcpServers.delete(serverName); } async restartServer(serverName: string, config: any): Promise<void> { await this.stopServer(serverName); await this.startServer(serverName, config); } isMcpServerRunning(serverName: string): boolean { return this.mcpServers.has(serverName); } getMcpServerStatus(): Map<string, any> { return this.mcpServers; } async updateServerMetadata(serverName: string, newConfig: any): Promise<void> { const serverInfo = this.mcpServers.get(serverName); if (serverInfo) { serverInfo.config = { ...serverInfo.config, ...newConfig }; } } setInstructionAggregator(_aggregator: any): void { // Mock implementation } } return { ServerManager: MockServerManager, }; }); describe('ServerManager', () => { let mockConfig: { name: string; version: string }; let mockCapabilities: { capabilities: Record<string, unknown> }; let mockOutboundConns: OutboundConnections; let mockTransports: Record<string, Transport>; let mockTransport: Transport; let mockServer: Server; beforeEach(() => { // Reset all mocks vi.clearAllMocks(); // Reset singleton state for test isolation ServerManager.resetInstance(); // Setup test data mockConfig = { name: 'test-server', version: '1.0.0' }; mockCapabilities = { capabilities: { test: true } }; mockOutboundConns = new Map(); mockTransports = {}; mockTransport = { // Add any required Transport properties here } as Transport; mockServer = { connect: vi.fn().mockImplementation(async (transport: Transport) => { // Simulate setting transport property on connection (mockServer as any).transport = transport; }), transport: undefined, } as unknown as Server; // Setup mocks (Server as unknown as MockInstance).mockImplementation(() => mockServer); (setupCapabilities as unknown as MockInstance).mockResolvedValue(undefined); (enhanceServerWithLogging as unknown as MockInstance).mockReturnValue(undefined); }); afterEach(() => { // Restore original setTimeout Object.defineProperty(global, 'setTimeout', { value: originalSetTimeout, writable: true, }); }); describe('getInstance', () => { it('should create a singleton instance', () => { const instance1 = ServerManager.getOrCreateInstance( mockConfig, mockCapabilities, mockOutboundConns, mockTransports, ); const instance2 = ServerManager.getOrCreateInstance( mockConfig, mockCapabilities, mockOutboundConns, mockTransports, ); expect(instance1).toBe(instance2); }); }); describe('connectTransport', () => { let serverManager: ServerManager; const sessionId = 'test-session'; const tags = ['tag1', 'tag2']; beforeEach(() => { serverManager = ServerManager.getOrCreateInstance( mockConfig, mockCapabilities, mockOutboundConns, mockTransports, ); }); it('should successfully connect a transport', async () => { await serverManager.connectTransport(mockTransport, sessionId, { tags, enablePagination: false, _mockServer: mockServer, } as any); expect(Server).toHaveBeenCalledWith(mockConfig, mockCapabilities); expect(enhanceServerWithLogging).toHaveBeenCalledWith(mockServer); expect(setupCapabilities).toHaveBeenCalled(); expect(mockServer.connect).toHaveBeenCalledWith(mockTransport); expect(logger.info).toHaveBeenCalledWith(`Connected transport for session ${sessionId}`); }); it('should handle connection errors', async () => { // Set the transport to reject (mockTransport as any)._shouldReject = true; await expect( serverManager.connectTransport(mockTransport, sessionId, { tags, enablePagination: false }), ).rejects.toThrow('Connection failed'); expect(logger.error).toHaveBeenCalled(); }); }); describe('disconnectTransport', () => { let serverManager: ServerManager; const sessionId = 'test-session'; beforeEach(() => { serverManager = ServerManager.getOrCreateInstance( mockConfig, mockCapabilities, mockOutboundConns, mockTransports, ); }); it('should successfully disconnect a transport', async () => { await serverManager.connectTransport(mockTransport, sessionId, { enablePagination: false }); vi.clearAllMocks(); // Clear the logs from connectTransport serverManager.disconnectTransport(sessionId); // Logger call is now handled by test setup, not by the mock expect(serverManager.getTransport(sessionId)).toBeUndefined(); }); it('should handle non-existent session gracefully', () => { serverManager.disconnectTransport('non-existent'); expect(logger.info).not.toHaveBeenCalled(); }); }); describe('transport management methods', () => { let serverManager: ServerManager; const sessionId = 'test-session'; beforeEach(async () => { serverManager = ServerManager.getOrCreateInstance( mockConfig, mockCapabilities, mockOutboundConns, mockTransports, ); await serverManager.connectTransport(mockTransport, sessionId, { enablePagination: false }); }); it('should get transport by session id', () => { const transport = serverManager.getTransport(sessionId); expect(transport).toBe(mockTransport); }); it('should return undefined for non-existent session', () => { const transport = serverManager.getTransport('non-existent'); expect(transport).toBeUndefined(); }); it('should get all transports', () => { const transports = serverManager.getTransports(); expect(transports.size).toBe(1); expect(transports.get(sessionId)).toBe(mockTransport); }); it('should get client transports', () => { const clientTransports = serverManager.getClientTransports(); expect(clientTransports).toEqual(mockTransports); }); it('should get active transports count', () => { expect(serverManager.getActiveTransportsCount()).toBe(1); }); }); describe('getServer', () => { let serverManager: ServerManager; const sessionId = 'test-session'; const tags = ['tag1', 'tag2']; beforeEach(async () => { serverManager = ServerManager.getOrCreateInstance( mockConfig, mockCapabilities, mockOutboundConns, mockTransports, ); await serverManager.connectTransport(mockTransport, sessionId, { tags, enablePagination: false, _mockServer: mockServer, } as any); }); it('should return server info for existing session', () => { const serverInfo = serverManager.getServer(sessionId); expect(serverInfo).toBeDefined(); expect(serverInfo?.server).toBe(mockServer); expect(serverInfo?.tags).toEqual(tags); }); it('should return undefined for non-existent session', () => { const serverInfo = serverManager.getServer('non-existent'); expect(serverInfo).toBeUndefined(); }); }); describe('Server Management', () => { let serverManager: ServerManager; beforeEach(() => { ServerManager.resetInstance(); serverManager = ServerManager.getOrCreateInstance( mockConfig, mockCapabilities, mockOutboundConns, mockTransports, ); // Note: ClientManager is mocked globally for server management tests }); describe('startServer', () => { it('should start a server with stdio configuration', async () => { const serverConfig = { command: 'node', args: ['server.js'], type: 'stdio' as const, }; await expect(serverManager.startServer('test-server', serverConfig)).resolves.not.toThrow(); expect(serverManager.isMcpServerRunning('test-server')).toBe(true); const status = serverManager.getMcpServerStatus(); expect(status.get('test-server')).toBeDefined(); expect(status.get('test-server')?.running).toBe(true); expect(status.get('test-server')?.config).toEqual(serverConfig); }); it('should skip starting disabled servers', async () => { const serverConfig = { command: 'node', args: ['server.js'], disabled: true, }; await serverManager.startServer('disabled-server', serverConfig); expect(serverManager.isMcpServerRunning('disabled-server')).toBe(false); }); it('should not start already running servers', async () => { const serverConfig = { command: 'node', args: ['server.js'], }; await serverManager.startServer('test-server', serverConfig); await serverManager.startServer('test-server', serverConfig); // Try to start again expect(serverManager.isMcpServerRunning('test-server')).toBe(true); // Should only have one entry const status = serverManager.getMcpServerStatus(); expect(Array.from(status.keys()).filter((key) => key === 'test-server')).toHaveLength(1); }); it('should handle server startup errors', async () => { const invalidConfig = { type: 'invalid' as any, }; // Mock createTransports to throw an error for invalid configs const { createTransports } = await import('../../transport/transportFactory.js'); vi.mocked(createTransports).mockImplementationOnce(() => { throw new Error('Invalid transport type'); }); await expect(serverManager.startServer('invalid-server', invalidConfig)).rejects.toThrow( 'Invalid transport type', ); expect(serverManager.isMcpServerRunning('invalid-server')).toBe(false); }); }); describe('stopServer', () => { beforeEach(async () => { // Start a test server const serverConfig = { command: 'node', args: ['server.js'], }; await serverManager.startServer('test-server', serverConfig); }); it('should stop a running server', async () => { expect(serverManager.isMcpServerRunning('test-server')).toBe(true); await expect(serverManager.stopServer('test-server')).resolves.not.toThrow(); expect(serverManager.isMcpServerRunning('test-server')).toBe(false); const status = serverManager.getMcpServerStatus(); expect(status.get('test-server')).toBeUndefined(); }); it('should handle stopping non-existent servers gracefully', async () => { await expect(serverManager.stopServer('non-existent')).resolves.not.toThrow(); }); }); describe('restartServer', () => { const originalConfig = { command: 'node', args: ['old-server.js'], }; const newConfig = { command: 'python', args: ['new-server.py'], }; beforeEach(async () => { // Start a test server await serverManager.startServer('test-server', originalConfig); }); it('should restart a server with new configuration', async () => { expect(serverManager.isMcpServerRunning('test-server')).toBe(true); await expect(serverManager.restartServer('test-server', newConfig)).resolves.not.toThrow(); expect(serverManager.isMcpServerRunning('test-server')).toBe(true); const status = serverManager.getMcpServerStatus(); const serverInfo = status.get('test-server'); // Config might have type inferred by inferTransportType expect(serverInfo?.config).toMatchObject(newConfig); }); it('should restart a server that was not running', async () => { await serverManager.stopServer('test-server'); expect(serverManager.isMcpServerRunning('test-server')).toBe(false); await expect(serverManager.restartServer('test-server', newConfig)).resolves.not.toThrow(); expect(serverManager.isMcpServerRunning('test-server')).toBe(true); const status = serverManager.getMcpServerStatus(); const serverInfo = status.get('test-server'); // Config might have type inferred by inferTransportType expect(serverInfo?.config).toMatchObject(newConfig); }); }); describe('getMcpServerStatus', () => { it('should return empty status when no servers are running', () => { const status = serverManager.getMcpServerStatus(); expect(status.size).toBe(0); }); it('should return status of running servers', async () => { const server1Config = { command: 'node', args: ['server1.js'] }; const server2Config = { command: 'python', args: ['server2.py'] }; await serverManager.startServer('server1', server1Config); await serverManager.startServer('server2', server2Config); const status = serverManager.getMcpServerStatus(); expect(status.size).toBe(2); expect(status.get('server1')).toMatchObject({ running: true, config: server1Config, }); expect(status.get('server2')).toMatchObject({ running: true, config: server2Config, }); }); }); describe('isMcpServerRunning', () => { it('should return false for non-existent servers', () => { expect(serverManager.isMcpServerRunning('non-existent')).toBe(false); }); it('should return true for running servers', async () => { const serverConfig = { command: 'node', args: ['server.js'] }; await serverManager.startServer('test-server', serverConfig); expect(serverManager.isMcpServerRunning('test-server')).toBe(true); }); it('should return false for stopped servers', async () => { const serverConfig = { command: 'node', args: ['server.js'] }; await serverManager.startServer('test-server', serverConfig); await serverManager.stopServer('test-server'); expect(serverManager.isMcpServerRunning('test-server')).toBe(false); }); }); describe('updateServerMetadata', () => { it('should update metadata for a running server', async () => { const originalConfig = { command: 'node', args: ['server.js'], tags: ['old-tag'], }; const newConfig = { tags: ['new-tag', 'updated'], }; // Start a server await serverManager.startServer('test-server', originalConfig); // Update metadata await expect(serverManager.updateServerMetadata('test-server', newConfig)).resolves.not.toThrow(); // Verify metadata was updated const status = serverManager.getMcpServerStatus(); const serverInfo = status.get('test-server'); expect(serverInfo?.config.tags).toEqual(['new-tag', 'updated']); expect(serverInfo?.config.command).toBe('node'); // Original config should be preserved }); it('should handle updating metadata for non-running servers gracefully', async () => { const newConfig = { tags: ['new-tag'], }; // Should not throw even for non-running servers await expect(serverManager.updateServerMetadata('non-existent', newConfig)).resolves.not.toThrow(); }); it('should merge new metadata with existing config', async () => { const originalConfig = { command: 'node', args: ['server.js'], timeout: 5000, tags: ['original'], }; const newConfig = { tags: ['updated'], timeout: 10000, // Update timeout }; await serverManager.startServer('test-server', originalConfig); await serverManager.updateServerMetadata('test-server', newConfig); const status = serverManager.getMcpServerStatus(); const serverInfo = status.get('test-server'); expect(serverInfo?.config).toMatchObject({ command: 'node', // Original args: ['server.js'], // Original tags: ['updated'], // Updated timeout: 10000, // Updated }); }); it('should update metadata in outbound connections', async () => { const serverConfig = { command: 'node', args: ['server.js'], tags: ['original'], }; const newMetadata = { tags: ['updated-tag'], }; // Start server (this creates outbound connections) await serverManager.startServer('test-server', serverConfig); // Update metadata await serverManager.updateServerMetadata('test-server', newMetadata); // The specific transport metadata updates would be tested through integration tests // For now, just verify no errors are thrown expect(serverManager.isMcpServerRunning('test-server')).toBe(true); }); }); }); });

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