Skip to content

Commit 62caf9c

Browse files
author
Lasim
committed
feat(gateway): implement configuration change detection and handling with restart prompts
1 parent 823af07 commit 62caf9c

File tree

3 files changed

+298
-168
lines changed

3 files changed

+298
-168
lines changed

services/gateway/src/commands/mcp.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { MCPConfigService } from '../core/mcp';
99
import { TableFormatter } from '../utils/table';
1010
import { AuthenticationError } from '../types/auth';
1111
import { RefreshService } from '../services/refresh-service';
12+
import { ConfigurationChangeService } from '../services/configuration-change-service';
1213

1314
// PID file location
1415
const PID_FILE = path.join(os.tmpdir(), 'deploystack-gateway.pid');
@@ -288,6 +289,84 @@ export function registerMCPCommand(program: Command) {
288289
console.log(chalk.gray('💡 Install MCP servers via the web interface'));
289290
}
290291

292+
// Check for remote configuration updates
293+
console.log(''); // Add spacing
294+
spinner = ora('Connecting to backend to check for configuration updates...').start();
295+
296+
try {
297+
const changeService = new ConfigurationChangeService();
298+
299+
spinner.text = 'Initializing API client...';
300+
// Use the same logic from RefreshService to check for changes
301+
const api = await import('../core/auth/api-client').then(m => new m.DeployStackAPI(credentials, backendUrl));
302+
303+
spinner.text = 'Detecting device information...';
304+
const deviceInfo = await import('../utils/device-detection').then(m => m.detectDeviceInfo());
305+
306+
spinner.text = 'Getting current configuration...';
307+
// Get existing configuration for change detection
308+
const oldConfig = await mcpService.getMCPConfig(credentials.selectedTeam.id);
309+
310+
spinner.text = 'Downloading latest configuration from cloud...';
311+
// Download latest configurations from cloud
312+
const gatewayConfig = await mcpService.downloadGatewayMCPConfig(deviceInfo.hardware_id, api, false);
313+
314+
spinner.text = 'Comparing configurations...';
315+
// Convert to team config format for comparison
316+
const newTeamConfig = mcpService.convertGatewayConfigToTeamConfig(
317+
credentials.selectedTeam.id,
318+
credentials.selectedTeam.name,
319+
gatewayConfig
320+
);
321+
322+
// Detect changes using the reusable service
323+
const changeInfo = changeService.detectConfigurationChanges(oldConfig, newTeamConfig);
324+
325+
spinner.succeed('Configuration check completed');
326+
327+
if (changeInfo.hasChanges) {
328+
// Handle configuration changes with custom restart logic
329+
await changeService.handleConfigurationChangesWithCustomRestart(changeInfo, async () => {
330+
// Store the new configuration first
331+
await mcpService.storeMCPConfig(newTeamConfig);
332+
333+
const ServerRestartService = await import('../services/server-restart-service').then(m => m.ServerRestartService);
334+
const restartService = new ServerRestartService();
335+
336+
try {
337+
const result = await restartService.restartGatewayServer();
338+
339+
if (result.restarted) {
340+
console.log(chalk.green('✅ Gateway restarted successfully with new configuration'));
341+
342+
if (result.mcpServersStarted !== undefined) {
343+
console.log(chalk.blue(`🤖 Ready to serve ${result.mcpServersStarted} MCP server${result.mcpServersStarted === 1 ? '' : 's'}`));
344+
}
345+
}
346+
} catch (restartError) {
347+
console.log(chalk.red(`❌ Failed to restart gateway: ${restartError instanceof Error ? restartError.message : String(restartError)}`));
348+
console.log(chalk.gray('💡 You can restart manually with "deploystack restart"'));
349+
}
350+
});
351+
352+
// Store the configuration even if user chose not to restart
353+
await mcpService.storeMCPConfig(newTeamConfig);
354+
} else {
355+
console.log(chalk.green('✅ Configuration is up to date'));
356+
}
357+
358+
} catch (refreshError) {
359+
if (spinner) {
360+
spinner.fail('Failed to check for configuration updates');
361+
}
362+
console.log(chalk.yellow('⚠️ Could not check for configuration updates'));
363+
if (refreshError instanceof Error) {
364+
console.log(chalk.gray(` Error: ${refreshError.message}`));
365+
}
366+
console.log(chalk.gray('💡 This may be due to network connectivity or backend issues'));
367+
console.log(chalk.gray('💡 Your local configuration is still available and functional'));
368+
}
369+
291370
} catch (error) {
292371
if (spinner) {
293372
spinner.fail('MCP operation failed');
@@ -333,6 +412,7 @@ function isGatewayRunning(): boolean {
333412
}
334413
}
335414

415+
336416
/**
337417
* Fetch tools from running gateway server via HTTP
338418
*/
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import chalk from 'chalk';
2+
import inquirer from 'inquirer';
3+
import { TeamMCPConfig, MCPServerConfig } from '../types/mcp';
4+
import { ServerRestartService } from './server-restart-service';
5+
6+
export interface ConfigurationChangeInfo {
7+
hasChanges: boolean;
8+
changes: string[];
9+
addedServers: string[];
10+
removedServers: string[];
11+
modifiedServers: string[];
12+
}
13+
14+
/**
15+
* Service for detecting and handling MCP configuration changes
16+
* Provides reusable logic for comparing configurations and managing restart prompts
17+
*/
18+
export class ConfigurationChangeService {
19+
private restartService: ServerRestartService;
20+
21+
constructor() {
22+
this.restartService = new ServerRestartService();
23+
}
24+
25+
/**
26+
* Detect changes between old and new MCP configurations
27+
*/
28+
detectConfigurationChanges(oldConfig: TeamMCPConfig | null, newConfig: TeamMCPConfig): ConfigurationChangeInfo {
29+
const changes: string[] = [];
30+
const addedServers: string[] = [];
31+
const removedServers: string[] = [];
32+
const modifiedServers: string[] = [];
33+
34+
// If no old config, everything is new
35+
if (!oldConfig) {
36+
return {
37+
hasChanges: false, // Don't prompt for restart on first-time config
38+
changes: ['Initial configuration downloaded'],
39+
addedServers: newConfig.servers.map(s => s.installation_name),
40+
removedServers: [],
41+
modifiedServers: []
42+
};
43+
}
44+
45+
// Create maps for easier comparison
46+
const oldServers = new Map(oldConfig.servers.map(s => [s.installation_name, s]));
47+
const newServers = new Map(newConfig.servers.map(s => [s.installation_name, s]));
48+
49+
// Check for added servers
50+
for (const [name] of newServers) {
51+
if (!oldServers.has(name)) {
52+
addedServers.push(name);
53+
changes.push(`• ${chalk.green(name)}: Added to team configuration`);
54+
}
55+
}
56+
57+
// Check for removed servers
58+
for (const [name] of oldServers) {
59+
if (!newServers.has(name)) {
60+
removedServers.push(name);
61+
changes.push(`• ${chalk.red(name)}: Removed from team configuration`);
62+
}
63+
}
64+
65+
// Check for modified servers
66+
for (const [name, newServer] of newServers) {
67+
const oldServer = oldServers.get(name);
68+
if (oldServer && this.hasServerConfigChanged(oldServer, newServer)) {
69+
modifiedServers.push(name);
70+
const serverChanges = this.getServerChanges(oldServer, newServer);
71+
changes.push(`• ${chalk.yellow(name)}: ${serverChanges.join(', ')}`);
72+
}
73+
}
74+
75+
return {
76+
hasChanges: changes.length > 0,
77+
changes,
78+
addedServers,
79+
removedServers,
80+
modifiedServers
81+
};
82+
}
83+
84+
/**
85+
* Check if a server configuration has changed
86+
*/
87+
hasServerConfigChanged(oldServer: MCPServerConfig, newServer: MCPServerConfig): boolean {
88+
// Check command and args
89+
if (oldServer.command !== newServer.command) return true;
90+
if (JSON.stringify(oldServer.args) !== JSON.stringify(newServer.args)) return true;
91+
92+
// Check environment variables
93+
if (JSON.stringify(oldServer.env) !== JSON.stringify(newServer.env)) return true;
94+
95+
// Check runtime
96+
if (oldServer.runtime !== newServer.runtime) return true;
97+
98+
return false;
99+
}
100+
101+
/**
102+
* Get specific changes for a server
103+
*/
104+
getServerChanges(oldServer: MCPServerConfig, newServer: MCPServerConfig): string[] {
105+
const changes: string[] = [];
106+
107+
if (oldServer.command !== newServer.command) {
108+
changes.push('command updated');
109+
}
110+
111+
if (JSON.stringify(oldServer.args) !== JSON.stringify(newServer.args)) {
112+
changes.push('arguments changed');
113+
}
114+
115+
if (JSON.stringify(oldServer.env) !== JSON.stringify(newServer.env)) {
116+
changes.push('environment variables updated');
117+
}
118+
119+
if (oldServer.runtime !== newServer.runtime) {
120+
changes.push('runtime changed');
121+
}
122+
123+
return changes;
124+
}
125+
126+
/**
127+
* Handle configuration changes with interactive restart prompt
128+
*/
129+
async handleConfigurationChanges(changeInfo: ConfigurationChangeInfo): Promise<void> {
130+
console.log(chalk.blue('\n🔄 Configuration changes detected:'));
131+
changeInfo.changes.forEach(change => console.log(` ${change}`));
132+
133+
console.log(chalk.yellow('\n⚠️ Gateway restart required for changes to take effect.'));
134+
135+
// Check if gateway is running
136+
const isRunning = this.restartService.isServerRunning();
137+
138+
if (!isRunning) {
139+
console.log(chalk.gray('💡 Gateway is not currently running. Changes will take effect when you start it.'));
140+
return;
141+
}
142+
143+
// Prompt user for restart
144+
const { shouldRestart } = await inquirer.prompt([
145+
{
146+
type: 'confirm',
147+
name: 'shouldRestart',
148+
message: 'Do you want to restart the DeployStack Gateway now?',
149+
default: true
150+
}
151+
]);
152+
153+
if (shouldRestart) {
154+
console.log(chalk.blue('\n🔄 Restarting gateway with updated configuration...'));
155+
156+
try {
157+
const result = await this.restartService.restartGatewayServer();
158+
159+
if (result.restarted) {
160+
console.log(chalk.green('✅ Gateway restarted successfully with new configuration'));
161+
162+
if (result.mcpServersStarted !== undefined) {
163+
console.log(chalk.blue(`🤖 Ready to serve ${result.mcpServersStarted} MCP server${result.mcpServersStarted === 1 ? '' : 's'}`));
164+
}
165+
}
166+
} catch (error) {
167+
console.log(chalk.red(`❌ Failed to restart gateway: ${error instanceof Error ? error.message : String(error)}`));
168+
console.log(chalk.gray('💡 You can restart manually with "deploystack restart"'));
169+
}
170+
} else {
171+
console.log(chalk.gray('💡 Configuration updated. Restart gateway manually with "deploystack restart" when ready.'));
172+
}
173+
}
174+
175+
/**
176+
* Handle configuration changes with custom restart logic (for MCP command)
177+
* This version allows the caller to handle configuration storage and restart logic
178+
*/
179+
async handleConfigurationChangesWithCustomRestart(
180+
changeInfo: ConfigurationChangeInfo,
181+
onRestart: () => Promise<void>
182+
): Promise<void> {
183+
console.log(chalk.blue('\n🔄 Configuration changes detected:'));
184+
changeInfo.changes.forEach(change => console.log(` ${change}`));
185+
186+
console.log(chalk.yellow('\n⚠️ Gateway restart required for changes to take effect.'));
187+
188+
// Check if gateway is running
189+
const isRunning = this.restartService.isServerRunning();
190+
191+
if (!isRunning) {
192+
console.log(chalk.gray('💡 Gateway is not currently running. Changes will take effect when you start it.'));
193+
return;
194+
}
195+
196+
// Prompt user for restart
197+
const { shouldRestart } = await inquirer.prompt([
198+
{
199+
type: 'confirm',
200+
name: 'shouldRestart',
201+
message: 'Do you want to restart the DeployStack Gateway now?',
202+
default: true
203+
}
204+
]);
205+
206+
if (shouldRestart) {
207+
console.log(chalk.blue('\n🔄 Restarting gateway with updated configuration...'));
208+
await onRestart();
209+
} else {
210+
console.log(chalk.gray('💡 Configuration updated. Restart gateway manually with "deploystack restart" when ready.'));
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)