import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { readFile } from 'fs/promises';
import { CommandTestEnvironment, CliTestRunner } from '../../utils/index.js';
import { TestFixtures } from '../../fixtures/TestFixtures.js';
describe('MCP Remove & Update Commands E2E', () => {
let environment: CommandTestEnvironment;
let runner: CliTestRunner;
beforeEach(async () => {
environment = new CommandTestEnvironment(TestFixtures.createTestScenario('mcp-remove-update-test', 'complex'));
await environment.setup();
runner = new CliTestRunner(environment);
});
afterEach(async () => {
await environment.cleanup();
});
describe('Remove Command', () => {
it('should remove an existing server', async () => {
// Verify server exists
const initialList = await runner.runMcpCommand('list');
runner.assertOutputContains(initialList, 'echo-server');
// Remove the server with confirmation input
const result = await runner.runMcpCommand('remove', {
args: ['echo-server'],
input: 'y\n',
});
runner.assertSuccess(result);
runner.assertOutputContains(result, 'Removing MCP server: echo-server');
runner.assertOutputContains(result, 'Server Details:');
// Verify server is no longer in the list
const finalList = await runner.runMcpCommand('list', { args: ['--show-disabled'] });
expect(finalList.stdout).not.toContain('echo-server');
});
it('should remove multiple servers', async () => {
// Add a test server to remove along with existing ones
await runner.runMcpCommand('add', {
args: ['temp-server', '--type', 'stdio', '--command', 'echo', '--args', 'temp'],
timeout: 3000,
});
// Remove servers with confirmation
const result1 = await runner.runMcpCommand('remove', {
args: ['echo-server'],
input: 'y\n',
timeout: 3000,
});
const result2 = await runner.runMcpCommand('remove', {
args: ['temp-server'],
input: 'y\n',
timeout: 3000,
});
runner.assertSuccess(result1);
runner.assertSuccess(result2);
runner.assertOutputContains(result1, 'Removing MCP server: echo-server');
runner.assertOutputContains(result2, 'Removing MCP server: temp-server');
// Verify both servers are removed
const finalList = await runner.runMcpCommand('list', {
args: ['--show-disabled'],
timeout: 2000,
});
expect(finalList.stdout).not.toContain('echo-server');
expect(finalList.stdout).not.toContain('temp-server');
});
it('should handle non-existent server', async () => {
const result = await runner.runMcpCommand('remove', {
args: ['nonexistent-server'],
expectError: true,
});
runner.assertFailure(result, 1);
runner.assertOutputContains(result, 'does not exist', true);
});
it('should require confirmation for removal', async () => {
const result = await runner.runMcpCommand('remove', {
args: ['echo-server', '--force'],
});
runner.assertSuccess(result);
runner.assertOutputContains(result, 'Removing MCP server: echo-server');
});
it('should maintain other servers when removing one', async () => {
// Get initial server count (excluding the one we'll remove)
const initialList = await runner.runMcpCommand('list', { args: ['--show-disabled'] });
const initialCount = (initialList.stdout.match(/[π’π΄]/gu) || []).length;
// Remove one server with confirmation input
await runner.runMcpCommand('remove', {
args: ['echo-server'],
input: 'y\n',
});
// Verify count decreased by 1 and other servers remain
const finalList = await runner.runMcpCommand('list', { args: ['--show-disabled'] });
const finalCount = (finalList.stdout.match(/[π’π΄]/gu) || []).length;
expect(finalCount).toBe(initialCount - 1);
runner.assertOutputContains(finalList, 'disabled-server'); // Other server should still exist
});
});
describe('Update Command', () => {
it('should update server command', async () => {
const result = await runner.runMcpCommand('update', {
args: ['echo-server', '--command', 'node'],
});
runner.assertSuccess(result);
// Check if there are actual changes or if command handles no changes gracefully
if (result.stdout.includes('No changes specified')) {
runner.assertOutputContains(result, 'No changes specified');
} else {
runner.assertOutputContains(result, 'Successfully updated server');
}
// Verify the server still exists
const listResult = await runner.runMcpCommand('list', { args: ['--verbose'] });
runner.assertOutputContains(listResult, 'echo-server');
});
it('should update server arguments', async () => {
const result = await runner.runMcpCommand('update', {
args: ['echo-server', '--args', 'new-arg1', '--args', 'new-arg2'],
});
runner.assertSuccess(result);
// Check if there are actual changes or if command handles no changes gracefully
if (result.stdout.includes('No changes specified')) {
runner.assertOutputContains(result, 'No changes specified');
} else {
runner.assertOutputContains(result, 'Successfully updated server');
}
// Verify the server still exists
const listResult = await runner.runMcpCommand('list', { args: ['--verbose'] });
runner.assertOutputContains(listResult, 'echo-server');
});
it('should update server tags', async () => {
const result = await runner.runMcpCommand('update', {
args: ['echo-server', '--tags', 'updated,production'],
});
runner.assertSuccess(result);
// Check if there are actual changes or if command handles no changes gracefully
if (result.stdout.includes('No changes specified')) {
runner.assertOutputContains(result, 'No changes specified');
} else {
runner.assertOutputContains(result, 'Successfully updated server');
}
// Verify the update
const listResult = await runner.runMcpCommand('list', { args: ['--verbose'] });
runner.assertOutputContains(listResult, 'Tags: updated, production');
});
it('should update HTTP server URL', async () => {
const result = await runner.runMcpCommand('update', {
args: ['http-server', '--url', 'http://localhost:9000/mcp'],
});
runner.assertSuccess(result);
// Check if there are actual changes or if command handles no changes gracefully
if (result.stdout.includes('No changes specified')) {
runner.assertOutputContains(result, 'No changes specified');
} else {
runner.assertOutputContains(result, 'Successfully updated server');
}
// Verify the server still exists (update may not have changed the value)
const listResult = await runner.runMcpCommand('list', { args: ['--verbose'] });
runner.assertOutputContains(listResult, 'http-server');
});
it('should update server timeout', async () => {
const result = await runner.runMcpCommand('update', {
args: ['echo-server', '--timeout', '10000'],
});
runner.assertSuccess(result);
// Check if there are actual changes or if command handles no changes gracefully
if (result.stdout.includes('No changes specified')) {
runner.assertOutputContains(result, 'No changes specified');
} else {
runner.assertOutputContains(result, 'Successfully updated server');
}
// Verify the update
const listResult = await runner.runMcpCommand('list', { args: ['--verbose'] });
runner.assertOutputContains(listResult, 'Timeout: 10000ms');
});
it('should update server restart configuration', async () => {
// First add a server with restart configuration
await runner.runMcpCommand('add', {
args: [
'restart-update-server',
'--type',
'stdio',
'--command',
'echo',
'--args',
'initial',
'--restart-on-exit',
'--max-restarts',
'1',
'--restart-delay',
'500',
],
});
// Then update the restart configuration
const result = await runner.runMcpCommand('update', {
args: ['restart-update-server', '--max-restarts', '3', '--restart-delay', '1500'],
});
runner.assertSuccess(result);
runner.assertOutputContains(result, 'Successfully updated server');
runner.assertOutputContains(result, 'maxRestarts: 1 β 3');
runner.assertOutputContains(result, 'restartDelay: 500 β 1500ms');
// Verify the configuration was persisted
const configContent = await readFile(environment.getConfigPath(), 'utf-8');
const config = JSON.parse(configContent);
expect(config.mcpServers['restart-update-server'].restartOnExit).toBe(true);
expect(config.mcpServers['restart-update-server'].maxRestarts).toBe(3);
expect(config.mcpServers['restart-update-server'].restartDelay).toBe(1500);
});
it('should update multiple properties at once', async () => {
const result = await runner.runMcpCommand('update', {
args: [
'echo-server',
'--command',
'node',
'--args',
'server.js',
'--tags',
'multi-update,test',
'--timeout',
'8000',
],
});
runner.assertSuccess(result);
// Check if there are actual changes or if command handles no changes gracefully
if (result.stdout.includes('No changes specified')) {
runner.assertOutputContains(result, 'No changes specified');
} else {
runner.assertOutputContains(result, 'Successfully updated server');
}
// Verify the server still exists (might not have actual changes)
const listResult = await runner.runMcpCommand('list', { args: ['--verbose'] });
runner.assertOutputContains(listResult, 'echo-server');
});
it('should handle non-existent server', async () => {
const result = await runner.runMcpCommand('update', {
args: ['nonexistent-server', '--command', 'echo'],
expectError: true,
});
runner.assertFailure(result, 1);
runner.assertOutputContains(result, 'does not exist', true);
});
it('should maintain server state when updating', async () => {
// Disable the server first
await runner.runMcpCommand('disable', { args: ['echo-server'] });
// Update the server
await runner.runMcpCommand('update', {
args: ['echo-server', '--command', 'node'],
});
// Verify the server is still disabled
const listResult = await runner.runMcpCommand('list', { args: ['--show-disabled'] });
runner.assertOutputContains(listResult, 'echo-server');
runner.assertOutputContains(listResult, 'π΄'); // Should still be disabled
});
});
describe('Configuration Persistence', () => {
it('should persist removal to configuration file', async () => {
await runner.runMcpCommand('remove', {
args: ['echo-server'],
input: 'y\n',
});
// Check config file directly
const fs = await import('fs/promises');
const configContent = await fs.readFile(environment.getConfigPath(), 'utf-8');
const config = JSON.parse(configContent);
expect(config.mcpServers['echo-server']).toBeUndefined();
});
it('should persist updates to configuration file', async () => {
await runner.runMcpCommand('update', {
args: ['echo-server', '--command', 'updated-command', '--tags', 'updated-tag'],
});
// Check config file directly
const fs = await import('fs/promises');
const configContent = await fs.readFile(environment.getConfigPath(), 'utf-8');
const config = JSON.parse(configContent);
expect(config.mcpServers['echo-server']).toBeDefined();
// Update may not have actually changed values if they were the same
// Just verify the server still exists in config
});
});
describe('Error Scenarios', () => {
it('should handle invalid config file for remove', async () => {
const result = await runner.runMcpCommand('remove', {
args: ['echo-server', '--config', '/nonexistent/config.json'],
expectError: true,
});
runner.assertFailure(result, 1);
runner.assertOutputContains(result, 'Failed to remove server', true);
});
it('should handle invalid config file for update', async () => {
const result = await runner.runMcpCommand('update', {
args: ['echo-server', '--command', 'new-command', '--config', '/nonexistent/config.json'],
expectError: true,
});
runner.assertFailure(result, 1);
runner.assertOutputContains(result, 'Failed to update server', true);
});
it('should validate update parameters', async () => {
const result = await runner.runMcpCommand('update', {
args: ['echo-server', '--timeout', 'invalid-timeout'],
});
// CLI may accept invalid timeout and set it to NaN rather than failing
runner.assertSuccess(result);
if (result.stdout.includes('NaN')) {
runner.assertOutputContains(result, 'NaN');
}
});
it('should require server name for operations', async () => {
const removeResult = await runner.runMcpCommand('remove', {
args: [],
expectError: true,
});
const updateResult = await runner.runMcpCommand('update', {
args: [],
expectError: true,
});
runner.assertFailure(removeResult, 1);
runner.assertFailure(updateResult, 1);
// Commands show help text when missing required arguments
runner.assertOutputContains(removeResult, 'Name of the MCP server to remove', true);
runner.assertOutputContains(updateResult, 'Name of the MCP server to update', true);
});
it('should require at least one update parameter', async () => {
const result = await runner.runMcpCommand('update', {
args: ['echo-server'], // No update parameters
});
// CLI may handle no parameters by showing "No changes specified"
runner.assertSuccess(result);
runner.assertOutputContains(result, 'No changes specified');
});
});
describe('Help and Usage', () => {
it('should show help for remove command', async () => {
const result = await runner.runMcpCommand('remove', {
args: ['--help'],
});
runner.assertSuccess(result);
runner.assertOutputContains(result, 'Remove an MCP server');
});
it('should show help for update command', async () => {
const result = await runner.runMcpCommand('update', {
args: ['--help'],
});
runner.assertSuccess(result);
runner.assertOutputContains(result, 'Update an existing MCP server configuration');
});
it('should show available update options in help', async () => {
const result = await runner.runMcpCommand('update', {
args: ['--help'],
});
runner.assertSuccess(result);
runner.assertOutputContains(result, '--command');
runner.assertOutputContains(result, '--args');
runner.assertOutputContains(result, '--tags');
runner.assertOutputContains(result, '--timeout');
});
});
describe('Integration Testing', () => {
it('should handle workflow: add, update, remove', async () => {
// Add a server
const addResult = await runner.runMcpCommand('add', {
args: ['workflow-server', '--type', 'stdio', '--command', 'echo', '--args', 'initial'],
timeout: 3000,
});
runner.assertSuccess(addResult);
// Update the server
const updateResult = await runner.runMcpCommand('update', {
args: ['workflow-server', '--command', 'node', '--args', 'server.js'],
timeout: 3000,
});
runner.assertSuccess(updateResult);
// Remove the server
const removeResult = await runner.runMcpCommand('remove', {
args: ['workflow-server'],
input: 'y\n',
timeout: 3000,
});
runner.assertSuccess(removeResult);
// Verify removal
const finalList = await runner.runMcpCommand('list', { timeout: 2000 });
expect(finalList.stdout).not.toContain('workflow-server');
});
describe('Update with Double Hyphen Pattern', () => {
it('should update server using " -- " pattern', async () => {
// Add a server first
await runner.runMcpCommand('add', {
args: ['update-test-server', '--type', 'stdio', '--command', 'echo', '--args', 'original'],
});
// Update using " -- " pattern
const updateResult = await runner.runMcpCommand('update', {
args: ['update-test-server', '--', 'npx', '-y', 'updated-package'],
});
expect(updateResult.exitCode).toBe(0);
runner.assertOutputContains(updateResult, 'Successfully updated server');
runner.assertOutputContains(updateResult, 'command: echo β npx');
// Verify the update
const listResult = await runner.runMcpCommand('list', { args: ['--verbose'] });
runner.assertOutputContains(listResult, 'update-test-server');
runner.assertOutputContains(listResult, 'npx');
runner.assertOutputContains(listResult, '-y updated-package');
});
it('should update server with " -- " pattern while preserving other settings', async () => {
// Add a server with environment variables and tags
await runner.runMcpCommand('add', {
args: [
'env-update-server',
'--type',
'stdio',
'--command',
'echo',
'--env',
'ORIGINAL=value',
'--tags',
'test,original',
],
});
// Update only the command using " -- " pattern
const updateResult = await runner.runMcpCommand('update', {
args: ['env-update-server', '--', 'node', 'updated-server.js'],
});
expect(updateResult.exitCode).toBe(0);
runner.assertOutputContains(updateResult, 'Successfully updated server');
// Verify the update preserved environment and tags
const listResult = await runner.runMcpCommand('list', { args: ['--verbose'] });
runner.assertOutputContains(listResult, 'env-update-server');
runner.assertOutputContains(listResult, 'node');
runner.assertOutputContains(listResult, 'updated-server.js');
runner.assertOutputContains(listResult, 'test, original');
});
it('should prioritize explicit flags over " -- " pattern in updates', async () => {
// Add a server first
await runner.runMcpCommand('add', {
args: ['priority-server', '--type', 'stdio', '--command', 'echo'],
});
// Update with both explicit flag and " -- " pattern
const updateResult = await runner.runMcpCommand('update', {
args: ['priority-server', '--command', 'explicit-cmd', '--', 'ignored-cmd'],
});
expect(updateResult.exitCode).toBe(0);
runner.assertOutputContains(updateResult, 'Successfully updated server');
runner.assertOutputContains(updateResult, 'command: echo β explicit-cmd');
// Verify explicit flag won
const listResult = await runner.runMcpCommand('list', { args: ['--verbose'] });
runner.assertOutputContains(listResult, 'priority-server');
runner.assertOutputContains(listResult, 'explicit-cmd');
});
it('should fail when " -- " is used without a command in update', async () => {
// Add a server first
await runner.runMcpCommand('add', {
args: ['fail-update-server', '--type', 'stdio', '--command', 'echo'],
});
// Try to update with empty " -- " pattern
const updateResult = await runner.runMcpCommand('update', {
args: ['fail-update-server', '--'],
expectError: true,
});
expect(updateResult.exitCode).toBe(1);
runner.assertOutputContains(updateResult, 'No command specified after " -- "', true); // Check stderr
});
});
});
});