Skip to content

Commit a65d849

Browse files
author
Lasim
committed
feat(gateway): add 'restart' command to gracefully restart the gateway server
1 parent 44af50e commit a65d849

File tree

6 files changed

+482
-7
lines changed

6 files changed

+482
-7
lines changed

services/gateway/README.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,18 @@ deploystack teams --switch 2
198198

199199
### `deploystack refresh`
200200

201-
Refresh MCP server configurations from cloud control plane.
201+
Refresh MCP server configurations from cloud control plane with automatic change detection and restart prompting.
202202

203203
**Options:**
204204

205205
- `--url <url>` - DeployStack backend URL (override stored URL)
206206

207+
**Features:**
208+
209+
- **Change Detection**: Automatically detects configuration changes (added/removed/modified servers)
210+
- **Interactive Restart**: Prompts to restart gateway when changes require it
211+
- **Smart Behavior**: Only prompts for restart if gateway is running and changes detected
212+
207213
**Examples:**
208214

209215
```bash
@@ -214,6 +220,21 @@ deploystack refresh
214220
deploystack refresh --url http://localhost:3000
215221
```
216222

223+
**Sample Output with Changes:**
224+
225+
```text
226+
🔄 Refreshing MCP configuration for team: My Team
227+
✅ MCP configuration refreshed (4 servers)
228+
229+
🔄 Configuration changes detected:
230+
• brightdata: Environment variables updated
231+
• github-server: Arguments changed
232+
• new-server: Added to team configuration
233+
234+
⚠️ Gateway restart required for changes to take effect.
235+
? Do you want to restart the DeployStack Gateway now? (Y/n)
236+
```
237+
217238
### `deploystack mcp`
218239

219240
Manage MCP server configurations and discover tools.
@@ -276,6 +297,35 @@ deploystack start --foreground
276297
🤖 Ready to serve 2 MCP servers for team: kaka
277298
```
278299

300+
### `deploystack restart`
301+
302+
Restart the gateway server with graceful shutdown and startup sequence.
303+
304+
**Options:**
305+
306+
- `-p, --port <port>` - Port to run the gateway on (default: 9095)
307+
- `-h, --host <host>` - Host to bind the gateway to (default: localhost)
308+
- `-f, --foreground` - Run in foreground instead of daemon mode
309+
310+
**Features:**
311+
312+
- **Graceful Shutdown**: Properly stops all MCP server processes before restart
313+
- **Smart Behavior**: If gateway isn't running, performs a start operation instead
314+
- **Configuration Reload**: Picks up any configuration changes after restart
315+
316+
**Examples:**
317+
318+
```bash
319+
# Restart gateway with current configuration
320+
deploystack restart
321+
322+
# Restart on custom port
323+
deploystack restart --port 8080
324+
325+
# Restart in foreground for debugging
326+
deploystack restart --foreground
327+
```
328+
279329
### `deploystack stop`
280330

281331
Stop the running gateway server.

services/gateway/src/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { registerTeamsCommand } from './teams';
55
export { registerMCPCommand } from './mcp';
66
export { registerRefreshCommand } from './refresh';
77
export { registerStartCommand } from './start';
8+
export { registerRestartCommand } from './restart';
89
export { registerStopCommand } from './stop';
910
export { registerStatusCommand } from './status';
1011
export { registerConfigCommand } from './config';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Command } from 'commander';
2+
import chalk from 'chalk';
3+
import { ServerRestartService } from '../services/server-restart-service';
4+
5+
export function registerRestartCommand(program: Command) {
6+
program
7+
.command('restart')
8+
.description('Restart the gateway server')
9+
.option('-p, --port <port>', 'Port to run the gateway on', '9095')
10+
.option('-h, --host <host>', 'Host to bind the gateway to', 'localhost')
11+
.option('-f, --foreground', 'Run in foreground (default is daemon mode)')
12+
.action(async (options) => {
13+
const restartService = new ServerRestartService();
14+
15+
try {
16+
const port = parseInt(options.port, 10);
17+
const host = options.host;
18+
const foreground = options.foreground || false;
19+
20+
const result = await restartService.restartGatewayServer({
21+
port,
22+
host,
23+
foreground
24+
});
25+
26+
if (result.restarted) {
27+
console.log(chalk.green('✅ Gateway restarted successfully'));
28+
} else {
29+
console.log(chalk.green('✅ Gateway started successfully'));
30+
}
31+
32+
if (foreground) {
33+
console.log(chalk.gray(' Press Ctrl+C to stop the server'));
34+
} else {
35+
console.log(chalk.gray(` PID: ${result.pid}`));
36+
console.log(chalk.gray(` SSE endpoint: ${result.endpoints?.sse}`));
37+
console.log(chalk.gray(` Messages: ${result.endpoints?.messages}`));
38+
console.log(chalk.gray(' Use "deploystack status" to check status'));
39+
console.log(chalk.gray(' Use "deploystack stop" to stop the server'));
40+
41+
if (result.mcpServersStarted !== undefined) {
42+
console.log(chalk.blue(`🤖 Ready to serve ${result.mcpServersStarted} MCP server${result.mcpServersStarted === 1 ? '' : 's'} for team: ${result.teamName}`));
43+
}
44+
}
45+
46+
} catch (error) {
47+
console.error(chalk.red('❌ Failed to restart gateway:'), error instanceof Error ? error.message : String(error));
48+
process.exit(1);
49+
}
50+
});
51+
}

services/gateway/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
registerMCPCommand,
1111
registerRefreshCommand,
1212
registerStartCommand,
13+
registerRestartCommand,
1314
registerStopCommand,
1415
registerStatusCommand,
1516
registerConfigCommand,
@@ -32,6 +33,7 @@ registerTeamsCommand(program);
3233
registerMCPCommand(program);
3334
registerRefreshCommand(program);
3435
registerStartCommand(program);
36+
registerRestartCommand(program);
3537
registerStopCommand(program);
3638
registerStatusCommand(program);
3739
registerConfigCommand(program);

services/gateway/src/services/refresh-service.ts

Lines changed: 187 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import chalk from 'chalk';
22
import ora from 'ora';
3+
import inquirer from 'inquirer';
34
import { CredentialStorage } from '../core/auth/storage';
45
import { DeployStackAPI } from '../core/auth/api-client';
56
import { MCPConfigService } from '../core/mcp';
7+
import { ServerRestartService } from './server-restart-service';
68
import { AuthenticationError } from '../types/auth';
9+
import { TeamMCPConfig, MCPServerConfig } from '../types/mcp';
710

811
export interface RefreshOptions {
912
url?: string;
@@ -12,10 +15,12 @@ export interface RefreshOptions {
1215
export class RefreshService {
1316
private storage: CredentialStorage;
1417
private mcpService: MCPConfigService;
18+
private restartService: ServerRestartService;
1519

1620
constructor() {
1721
this.storage = new CredentialStorage();
1822
this.mcpService = new MCPConfigService();
23+
this.restartService = new ServerRestartService();
1924
}
2025

2126
/**
@@ -51,25 +56,39 @@ export class RefreshService {
5156
const api = new DeployStackAPI(credentials, backendUrl);
5257

5358
console.log(chalk.blue(`🔄 Refreshing MCP configuration for team: ${chalk.cyan(credentials.selectedTeam.name)}`));
59+
60+
// Step 1: Get current configuration for comparison
61+
const oldConfig = await this.mcpService.getMCPConfig(credentials.selectedTeam.id);
62+
63+
// Step 2: Download new configuration
5464
spinner = ora('Downloading latest MCP configuration...').start();
5565

5666
try {
57-
const config = await this.mcpService.downloadAndStoreMCPConfig(
67+
const newConfig = await this.mcpService.downloadAndStoreMCPConfig(
5868
credentials.selectedTeam.id,
5969
credentials.selectedTeam.name,
6070
api,
6171
false
6272
);
6373

64-
spinner.succeed(`MCP configuration refreshed (${config.servers.length} server${config.servers.length === 1 ? '' : 's'})`);
74+
spinner.succeed(`MCP configuration refreshed (${newConfig.servers.length} server${newConfig.servers.length === 1 ? '' : 's'})`);
6575
console.log(chalk.green('✅ MCP configuration has been refreshed'));
6676

77+
// Step 3: Detect changes and prompt for restart if needed
78+
const changes = this.detectConfigurationChanges(oldConfig, newConfig);
79+
80+
if (changes.hasChanges) {
81+
await this.handleConfigurationChanges(changes);
82+
} else {
83+
console.log(chalk.gray('📋 No configuration changes detected'));
84+
}
85+
6786
// Show summary
6887
console.log(chalk.gray(`\n📊 Configuration Summary:`));
69-
console.log(chalk.gray(` Team: ${config.team_name}`));
70-
console.log(chalk.gray(` Installations: ${config.installations.length}`));
71-
console.log(chalk.gray(` Servers: ${config.servers.length}`));
72-
console.log(chalk.gray(` Last Updated: ${new Date(config.last_updated).toLocaleString()}`));
88+
console.log(chalk.gray(` Team: ${newConfig.team_name}`));
89+
console.log(chalk.gray(` Installations: ${newConfig.installations.length}`));
90+
console.log(chalk.gray(` Servers: ${newConfig.servers.length}`));
91+
console.log(chalk.gray(` Last Updated: ${new Date(newConfig.last_updated).toLocaleString()}`));
7392

7493
} catch (error) {
7594
spinner.fail('Failed to refresh MCP configuration');
@@ -96,4 +115,166 @@ export class RefreshService {
96115
process.exit(1);
97116
}
98117
}
118+
119+
/**
120+
* Detect changes between old and new MCP configurations
121+
*/
122+
private detectConfigurationChanges(oldConfig: TeamMCPConfig | null, newConfig: TeamMCPConfig): {
123+
hasChanges: boolean;
124+
changes: string[];
125+
addedServers: string[];
126+
removedServers: string[];
127+
modifiedServers: string[];
128+
} {
129+
const changes: string[] = [];
130+
const addedServers: string[] = [];
131+
const removedServers: string[] = [];
132+
const modifiedServers: string[] = [];
133+
134+
// If no old config, everything is new
135+
if (!oldConfig) {
136+
return {
137+
hasChanges: false, // Don't prompt for restart on first-time config
138+
changes: ['Initial configuration downloaded'],
139+
addedServers: newConfig.servers.map(s => s.installation_name),
140+
removedServers: [],
141+
modifiedServers: []
142+
};
143+
}
144+
145+
// Create maps for easier comparison
146+
const oldServers = new Map(oldConfig.servers.map(s => [s.installation_name, s]));
147+
const newServers = new Map(newConfig.servers.map(s => [s.installation_name, s]));
148+
149+
// Check for added servers
150+
for (const [name] of newServers) {
151+
if (!oldServers.has(name)) {
152+
addedServers.push(name);
153+
changes.push(`• ${chalk.green(name)}: Added to team configuration`);
154+
}
155+
}
156+
157+
// Check for removed servers
158+
for (const [name] of oldServers) {
159+
if (!newServers.has(name)) {
160+
removedServers.push(name);
161+
changes.push(`• ${chalk.red(name)}: Removed from team configuration`);
162+
}
163+
}
164+
165+
// Check for modified servers
166+
for (const [name, newServer] of newServers) {
167+
const oldServer = oldServers.get(name);
168+
if (oldServer && this.hasServerConfigChanged(oldServer, newServer)) {
169+
modifiedServers.push(name);
170+
const serverChanges = this.getServerChanges(oldServer, newServer);
171+
changes.push(`• ${chalk.yellow(name)}: ${serverChanges.join(', ')}`);
172+
}
173+
}
174+
175+
return {
176+
hasChanges: changes.length > 0,
177+
changes,
178+
addedServers,
179+
removedServers,
180+
modifiedServers
181+
};
182+
}
183+
184+
/**
185+
* Check if a server configuration has changed
186+
*/
187+
private hasServerConfigChanged(oldServer: MCPServerConfig, newServer: MCPServerConfig): boolean {
188+
// Check command and args
189+
if (oldServer.command !== newServer.command) return true;
190+
if (JSON.stringify(oldServer.args) !== JSON.stringify(newServer.args)) return true;
191+
192+
// Check environment variables
193+
if (JSON.stringify(oldServer.env) !== JSON.stringify(newServer.env)) return true;
194+
195+
// Check runtime
196+
if (oldServer.runtime !== newServer.runtime) return true;
197+
198+
return false;
199+
}
200+
201+
/**
202+
* Get specific changes for a server
203+
*/
204+
private getServerChanges(oldServer: MCPServerConfig, newServer: MCPServerConfig): string[] {
205+
const changes: string[] = [];
206+
207+
if (oldServer.command !== newServer.command) {
208+
changes.push('command updated');
209+
}
210+
211+
if (JSON.stringify(oldServer.args) !== JSON.stringify(newServer.args)) {
212+
changes.push('arguments changed');
213+
}
214+
215+
if (JSON.stringify(oldServer.env) !== JSON.stringify(newServer.env)) {
216+
changes.push('environment variables updated');
217+
}
218+
219+
if (oldServer.runtime !== newServer.runtime) {
220+
changes.push('runtime changed');
221+
}
222+
223+
return changes;
224+
}
225+
226+
/**
227+
* Handle configuration changes with interactive restart prompt
228+
*/
229+
private async handleConfigurationChanges(changeInfo: {
230+
hasChanges: boolean;
231+
changes: string[];
232+
addedServers: string[];
233+
removedServers: string[];
234+
modifiedServers: string[];
235+
}): Promise<void> {
236+
console.log(chalk.blue('\n🔄 Configuration changes detected:'));
237+
changeInfo.changes.forEach(change => console.log(` ${change}`));
238+
239+
console.log(chalk.yellow('\n⚠️ Gateway restart required for changes to take effect.'));
240+
241+
// Check if gateway is running
242+
const isRunning = this.restartService.isServerRunning();
243+
244+
if (!isRunning) {
245+
console.log(chalk.gray('💡 Gateway is not currently running. Changes will take effect when you start it.'));
246+
return;
247+
}
248+
249+
// Prompt user for restart
250+
const { shouldRestart } = await inquirer.prompt([
251+
{
252+
type: 'confirm',
253+
name: 'shouldRestart',
254+
message: 'Do you want to restart the DeployStack Gateway now?',
255+
default: true
256+
}
257+
]);
258+
259+
if (shouldRestart) {
260+
console.log(chalk.blue('\n🔄 Restarting gateway with updated configuration...'));
261+
262+
try {
263+
const result = await this.restartService.restartGatewayServer();
264+
265+
if (result.restarted) {
266+
console.log(chalk.green('✅ Gateway restarted successfully with new configuration'));
267+
268+
if (result.mcpServersStarted !== undefined) {
269+
console.log(chalk.blue(`🤖 Ready to serve ${result.mcpServersStarted} MCP server${result.mcpServersStarted === 1 ? '' : 's'}`));
270+
}
271+
}
272+
} catch (error) {
273+
console.log(chalk.red(`❌ Failed to restart gateway: ${error instanceof Error ? error.message : String(error)}`));
274+
console.log(chalk.gray('💡 You can restart manually with "deploystack restart"'));
275+
}
276+
} else {
277+
console.log(chalk.gray('💡 Configuration updated. Restart gateway manually with "deploystack restart" when ready.'));
278+
}
279+
}
99280
}

0 commit comments

Comments
 (0)