Skip to main content
Glama
config-hot-reload.test.ts11.6 kB
import { TestFixtures } from '@test/e2e/fixtures/TestFixtures.js'; import { CliTestRunner, CommandTestEnvironment } from '@test/e2e/utils/index.js'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; describe('Configuration Hot Reload Integration', () => { let environment: CommandTestEnvironment; let runner: CliTestRunner; beforeEach(async () => { environment = new CommandTestEnvironment(TestFixtures.createTestScenario('hot-reload-test', 'basic')); await environment.setup(); runner = new CliTestRunner(environment); }); afterEach(async () => { await environment.cleanup(); }); describe('Manual Config File Changes', () => { it('should handle manual server disable through config file', async () => { // Start with a running server await runner.runMcpCommand('add', { args: ['test-server', '--type', 'stdio', '--command', 'echo', '--args', 'hello'], }); // Verify server is running const initialList = await runner.runMcpCommand('list'); runner.assertOutputContains(initialList, 'test-server'); // Manually disable the server by modifying the config file const fs = await import('fs/promises'); const configPath = environment.getConfigPath(); // Read current config const configContent = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configContent); // Disable the server config.mcpServers['test-server'].disabled = true; await fs.writeFile(configPath, JSON.stringify(config, null, 2)); // Give it a moment for the hot reload to kick in await new Promise((resolve) => setTimeout(resolve, 1000)); // Verify server is no longer listed const afterDisable = await runner.runMcpCommand('list'); expect(afterDisable.stdout).not.toContain('test-server'); // Verify server is shown as disabled const disabledList = await runner.runMcpCommand('list', { args: ['--show-disabled'] }); runner.assertOutputContains(disabledList, 'test-server'); runner.assertOutputContains(disabledList, '🔴'); }); it('should handle manual server enable through config file', async () => { // Add a disabled server await runner.runMcpCommand('add', { args: ['disabled-test-server', '--type', 'stdio', '--command', 'echo', '--args', 'hello', '--disabled'], }); // Verify server is disabled const initialList = await runner.runMcpCommand('list', { args: ['--show-disabled'] }); runner.assertOutputContains(initialList, 'disabled-test-server'); runner.assertOutputContains(initialList, '🔴'); // Manually enable the server by modifying the config file const fs = await import('fs/promises'); const configPath = environment.getConfigPath(); // Read current config const configContent = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configContent); // Enable the server config.mcpServers['disabled-test-server'].disabled = false; await fs.writeFile(configPath, JSON.stringify(config, null, 2)); // Give it a moment for the hot reload to kick in await new Promise((resolve) => setTimeout(resolve, 1000)); // Verify server is now enabled and listed const afterEnable = await runner.runMcpCommand('list'); runner.assertOutputContains(afterEnable, 'disabled-test-server'); runner.assertOutputContains(afterEnable, '🟢'); }); it('should handle rapid config changes without errors', async () => { // Add a server await runner.runMcpCommand('add', { args: ['rapid-test-server', '--type', 'stdio', '--command', 'echo', '--args', 'test'], }); const fs = await import('fs/promises'); const configPath = environment.getConfigPath(); // Perform rapid changes for (let i = 0; i < 3; i++) { // Read current config const configContent = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configContent); // Toggle disabled state config.mcpServers['rapid-test-server'].disabled = i % 2 === 0; await fs.writeFile(configPath, JSON.stringify(config, null, 2)); // Small delay between changes await new Promise((resolve) => setTimeout(resolve, 200)); } // Give final change time to process await new Promise((resolve) => setTimeout(resolve, 500)); // Should not have any errors and server should be in the expected state const finalList = await runner.runMcpCommand('list', { args: ['--show-disabled'] }); runner.assertOutputContains(finalList, 'rapid-test-server'); }); it('should handle mixed changes across multiple servers', async () => { // Add multiple servers await runner.runMcpCommand('add', { args: ['multi-server-1', '--type', 'stdio', '--command', 'echo', '--args', 'server1'], }); await runner.runMcpCommand('add', { args: ['multi-server-2', '--type', 'stdio', '--command', 'echo', '--args', 'server2'], }); const fs = await import('fs/promises'); const configPath = environment.getConfigPath(); // Read current config const configContent = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configContent); // Make mixed changes to different servers config.mcpServers['multi-server-1'].disabled = true; // Disable first config.mcpServers['multi-server-2'].args = ['server2', 'updated']; // Modify second await fs.writeFile(configPath, JSON.stringify(config, null, 2)); // Give time for hot reload await new Promise((resolve) => setTimeout(resolve, 1000)); // Verify changes took effect const listResult = await runner.runMcpCommand('list', { args: ['--show-disabled'] }); // First server should be disabled expect(listResult.stdout).toContain('multi-server-1'); expect(listResult.stdout).toContain('🔴'); // Second server should still be enabled (it was modified but not disabled) expect(listResult.stdout).toContain('multi-server-2'); expect(listResult.stdout).toContain('🟢'); }); }); describe('Error Recovery', () => { it('should handle invalid config gracefully', async () => { // Add a server first await runner.runMcpCommand('add', { args: ['error-test-server', '--type', 'stdio', '--command', 'echo', '--args', 'hello'], }); const fs = await import('fs/promises'); const configPath = environment.getConfigPath(); // Write invalid JSON await fs.writeFile(configPath, '{ invalid json content }'); // Wait for error processing await new Promise((resolve) => setTimeout(resolve, 500)); // Restore valid config const validConfig = { mcpServers: { 'error-test-server': { command: 'echo', args: ['hello'], disabled: false, }, }, }; await fs.writeFile(configPath, JSON.stringify(validConfig, null, 2)); // Give time for recovery await new Promise((resolve) => setTimeout(resolve, 1000)); // Should recover and show the server const listResult = await runner.runMcpCommand('list'); runner.assertOutputContains(listResult, 'error-test-server'); }); it('should handle missing config file gracefully', async () => { const fs = await import('fs/promises'); const configPath = environment.getConfigPath(); // Remove the config file await fs.unlink(configPath); // Wait for processing await new Promise((resolve) => setTimeout(resolve, 500)); // The system should handle missing config gracefully // It may return an error code but shouldn't crash const listResult = await runner.runMcpCommand('list'); // Should either succeed (0) or return config error (1) expect([0, 1]).toContain(listResult.exitCode); }); }); describe('Configuration Changes While Server is Running', () => { it('should not affect other servers when one server is disabled', async () => { // Add multiple servers await runner.runMcpCommand('add', { args: ['stable-server-1', '--type', 'stdio', '--command', 'echo', '--args', 'stable1'], }); await runner.runMcpCommand('add', { args: ['stable-server-2', '--type', 'stdio', '--command', 'echo', '--args', 'stable2'], }); await runner.runMcpCommand('add', { args: ['unstable-server', '--type', 'stdio', '--command', 'echo', '--args', 'unstable'], }); // Verify all servers are initially running const initialList = await runner.runMcpCommand('list'); runner.assertOutputContains(initialList, 'stable-server-1'); runner.assertOutputContains(initialList, 'stable-server-2'); runner.assertOutputContains(initialList, 'unstable-server'); // Manually disable one server const fs = await import('fs/promises'); const configPath = environment.getConfigPath(); const configContent = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configContent); config.mcpServers['unstable-server'].disabled = true; await fs.writeFile(configPath, JSON.stringify(config, null, 2)); // Give time for hot reload await new Promise((resolve) => setTimeout(resolve, 1000)); // Verify only the unstable server is disabled const afterDisable = await runner.runMcpCommand('list'); runner.assertOutputContains(afterDisable, 'stable-server-1'); runner.assertOutputContains(afterDisable, 'stable-server-2'); expect(afterDisable.stdout).not.toContain('unstable-server'); }); }); describe('Performance and Scalability', () => { it('should handle multiple rapid config changes without performance degradation', async () => { // Add several servers const serverCount = 5; for (let i = 0; i < serverCount; i++) { await runner.runMcpCommand('add', { args: [`perf-server-${i}`, '--type', 'stdio', '--command', 'echo', '--args', `server${i}`], }); } const fs = await import('fs/promises'); const configPath = environment.getConfigPath(); // Measure time for rapid changes const startTime = Date.now(); for (let i = 0; i < 10; i++) { // Toggle disable state for all servers const configContent = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configContent); for (let j = 0; j < serverCount; j++) { config.mcpServers[`perf-server-${j}`].disabled = i % 2 === 0; } await fs.writeFile(configPath, JSON.stringify(config, null, 2)); await new Promise((resolve) => setTimeout(resolve, 50)); // 50ms between changes } const endTime = Date.now(); const totalTime = endTime - startTime; // Should complete within reasonable time (less than 5 seconds) expect(totalTime).toBeLessThan(5000); // Give final changes time to process await new Promise((resolve) => setTimeout(resolve, 500)); // Verify system is still responsive const listResult = await runner.runMcpCommand('list', { args: ['--show-disabled'] }); expect(listResult.exitCode).toBe(0); // All servers should be present (some enabled, some disabled) for (let i = 0; i < serverCount; i++) { expect(listResult.stdout).toContain(`perf-server-${i}`); } }); }); });

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