Skip to content

Commit e5367c3

Browse files
author
Lasim
committed
feat(gateway): implement device detection and new MCP config endpoint
1 parent 12be78b commit e5367c3

File tree

5 files changed

+195
-42
lines changed

5 files changed

+195
-42
lines changed

services/gateway/src/commands/login.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DeployStackAPI } from '../core/auth/api-client';
77
import { MCPConfigService } from '../core/mcp';
88
import { AuthenticationError } from '../types/auth';
99
import { ServerStartService } from '../services/server-start-service';
10+
import { detectDeviceInfo } from '../utils/device-detection';
1011

1112
export function registerLoginCommand(program: Command) {
1213
program
@@ -61,11 +62,18 @@ export function registerLoginCommand(program: Command) {
6162
if (defaultTeam) {
6263
await storage.updateSelectedTeam(defaultTeam.id, defaultTeam.name);
6364

64-
// Download MCP configuration for the default team
65-
spinner.text = 'Downloading MCP server configurations...';
65+
// Detect device and download merged MCP configurations using new gateway endpoint
66+
spinner.text = 'Detecting device and downloading MCP configurations...';
6667
const mcpService = new MCPConfigService();
6768
try {
68-
await mcpService.downloadAndStoreMCPConfig(defaultTeam.id, defaultTeam.name, api, false);
69+
// Detect current device information
70+
const deviceInfo = await detectDeviceInfo();
71+
72+
// Register or update device with backend
73+
const device = await api.registerOrUpdateDevice(deviceInfo);
74+
75+
// Download merged configurations using the new gateway endpoint
76+
const gatewayConfig = await mcpService.downloadGatewayMCPConfig(device.id, api, false);
6977

7078
// Auto-start the gateway server after successful MCP config download
7179
spinner.text = 'Starting gateway server...';

services/gateway/src/core/auth/api-client.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,30 @@ export class DeployStackAPI {
9999
return response;
100100
}
101101

102+
/**
103+
* Get merged MCP configurations for gateway (NEW THREE-TIER ENDPOINT)
104+
* This endpoint merges Template + Team + User configurations and returns ready-to-use server configs
105+
* @param deviceId Device ID for device-specific user configurations
106+
* @returns Gateway MCP configurations response
107+
*/
108+
async getGatewayMCPConfigurations(hardwareId: string): Promise<{
109+
success: boolean;
110+
data: {
111+
servers: Array<{
112+
id: string;
113+
name: string;
114+
command: string;
115+
args: string[];
116+
env: Record<string, string>;
117+
status: 'ready' | 'invalid';
118+
}>;
119+
};
120+
}> {
121+
const endpoint = `${this.baseUrl}/api/gateway/me/mcp-configurations?hardware_id=${encodeURIComponent(hardwareId)}`;
122+
const response = await this.makeRequest(endpoint);
123+
return response;
124+
}
125+
102126
/**
103127
* Get device by hardware ID
104128
* @param hardwareId Hardware fingerprint

services/gateway/src/core/mcp/config-service.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,90 @@ export class MCPConfigService {
1414
}
1515

1616
/**
17-
* Download and store MCP configuration for a team
17+
* Download merged MCP configurations using the new gateway endpoint (THREE-TIER ARCHITECTURE)
18+
* This method uses the new /api/gateway/me/mcp-configurations endpoint that merges
19+
* Template + Team + User configurations and returns ready-to-use server configs
20+
* @param deviceId Device ID for device-specific user configurations
21+
* @param api DeployStack API client
22+
* @param showSpinner Whether to show progress spinner
23+
* @returns Gateway MCP configuration with ready-to-use servers
24+
*/
25+
async downloadGatewayMCPConfig(
26+
hardwareId: string,
27+
api?: DeployStackAPI,
28+
showSpinner: boolean = true
29+
): Promise<{
30+
servers: Array<{
31+
id: string;
32+
name: string;
33+
command: string;
34+
args: string[];
35+
env: Record<string, string>;
36+
status: 'ready' | 'invalid';
37+
}>;
38+
deviceId: string;
39+
lastUpdated: string;
40+
}> {
41+
let spinner: ReturnType<typeof ora> | null = null;
42+
43+
try {
44+
if (showSpinner) {
45+
spinner = ora('Downloading merged MCP configurations from gateway endpoint...').start();
46+
}
47+
48+
// Use provided API client or create one
49+
let apiClient = api;
50+
if (!apiClient) {
51+
const credentials = await this.storage.getCredentials();
52+
if (!credentials) {
53+
throw new AuthenticationError(
54+
AuthError.STORAGE_ERROR,
55+
'No authentication found - cannot download MCP config'
56+
);
57+
}
58+
apiClient = new DeployStackAPI(credentials, credentials.baseUrl);
59+
}
60+
61+
// Call the new gateway endpoint that merges all three tiers
62+
const response = await apiClient.getGatewayMCPConfigurations(hardwareId);
63+
64+
if (!response.success) {
65+
throw new AuthenticationError(
66+
AuthError.NETWORK_ERROR,
67+
'Failed to download merged MCP configurations from gateway endpoint'
68+
);
69+
}
70+
71+
const servers = response.data.servers || [];
72+
const readyServers = servers.filter(s => s.status === 'ready');
73+
const invalidServers = servers.filter(s => s.status === 'invalid');
74+
75+
if (showSpinner && spinner) {
76+
spinner.succeed(`Gateway MCP configurations downloaded (${readyServers.length} ready, ${invalidServers.length} invalid)`);
77+
}
78+
79+
if (invalidServers.length > 0) {
80+
console.log(chalk.yellow(`⚠️ ${invalidServers.length} server${invalidServers.length === 1 ? '' : 's'} marked as invalid (missing required user configurations)`));
81+
invalidServers.forEach(server => {
82+
console.log(chalk.gray(` • ${server.name} (${server.id})`));
83+
});
84+
}
85+
86+
return {
87+
servers,
88+
deviceId: hardwareId,
89+
lastUpdated: new Date().toISOString()
90+
};
91+
} catch (error) {
92+
if (spinner) {
93+
spinner.fail('Failed to download gateway MCP configurations');
94+
}
95+
throw error;
96+
}
97+
}
98+
99+
/**
100+
* Download and store MCP configuration for a team (LEGACY METHOD)
18101
* @param teamId Team ID
19102
* @param teamName Team name (optional, will be set from API if not provided)
20103
* @param api DeployStack API client

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

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { MCPConfigService } from '../core/mcp';
77
import { ServerRestartService } from './server-restart-service';
88
import { AuthenticationError } from '../types/auth';
99
import { TeamMCPConfig, MCPServerConfig } from '../types/mcp';
10+
import { detectDeviceInfo } from '../utils/device-detection';
1011

1112
export interface RefreshOptions {
1213
url?: string;
@@ -55,40 +56,79 @@ export class RefreshService {
5556
const backendUrl = options.url || credentials.baseUrl || 'https://cloud.deploystack.io';
5657
const api = new DeployStackAPI(credentials, backendUrl);
5758

58-
console.log(chalk.blue(`🔄 Refreshing MCP configuration for team: ${chalk.cyan(credentials.selectedTeam.name)}`));
59+
console.log(chalk.blue(`🔄 Refreshing MCP configuration using new gateway endpoint`));
5960

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
64-
spinner = ora('Downloading latest MCP configuration...').start();
61+
// Step 1: Detect device and get device ID
62+
spinner = ora('Detecting device and downloading latest MCP configurations...').start();
6563

6664
try {
67-
const newConfig = await this.mcpService.downloadAndStoreMCPConfig(
68-
credentials.selectedTeam.id,
69-
credentials.selectedTeam.name,
70-
api,
71-
false
72-
);
65+
// Detect current device information
66+
const deviceInfo = await detectDeviceInfo();
67+
68+
// Download merged configurations using the new gateway endpoint
69+
// Backend will automatically find device by hardware_id
70+
const gatewayConfig = await this.mcpService.downloadGatewayMCPConfig(deviceInfo.hardware_id, api, false);
71+
72+
const readyServers = gatewayConfig.servers.filter(s => s.status === 'ready');
73+
const invalidServers = gatewayConfig.servers.filter(s => s.status === 'invalid');
74+
75+
spinner.succeed(`Gateway MCP configurations refreshed (${readyServers.length} ready, ${invalidServers.length} invalid)`);
76+
console.log(chalk.green('✅ MCP configuration has been refreshed using new three-tier system'));
7377

74-
spinner.succeed(`MCP configuration refreshed (${newConfig.servers.length} server${newConfig.servers.length === 1 ? '' : 's'})`);
75-
console.log(chalk.green('✅ MCP configuration has been refreshed'));
78+
if (invalidServers.length > 0) {
79+
console.log(chalk.yellow(`\n⚠️ ${invalidServers.length} server${invalidServers.length === 1 ? '' : 's'} marked as invalid:`));
80+
invalidServers.forEach(server => {
81+
console.log(chalk.gray(` • ${server.name} - Missing required user configurations`));
82+
});
83+
console.log(chalk.gray(`💡 Configure these servers in the web UI to make them available`));
84+
}
7685

77-
// Step 3: Detect changes and prompt for restart if needed
78-
const changes = this.detectConfigurationChanges(oldConfig, newConfig);
86+
// Step 2: Check if gateway restart is needed
87+
const isRunning = this.restartService.isServerRunning();
7988

80-
if (changes.hasChanges) {
81-
await this.handleConfigurationChanges(changes);
89+
if (isRunning) {
90+
console.log(chalk.yellow('\n⚠️ Gateway restart required for changes to take effect.'));
91+
92+
// Prompt user for restart
93+
const { shouldRestart } = await inquirer.prompt([
94+
{
95+
type: 'confirm',
96+
name: 'shouldRestart',
97+
message: 'Do you want to restart the DeployStack Gateway now?',
98+
default: true
99+
}
100+
]);
101+
102+
if (shouldRestart) {
103+
console.log(chalk.blue('\n🔄 Restarting gateway with updated configuration...'));
104+
105+
try {
106+
const result = await this.restartService.restartGatewayServer();
107+
108+
if (result.restarted) {
109+
console.log(chalk.green('✅ Gateway restarted successfully with new configuration'));
110+
111+
if (result.mcpServersStarted !== undefined) {
112+
console.log(chalk.blue(`🤖 Ready to serve ${result.mcpServersStarted} MCP server${result.mcpServersStarted === 1 ? '' : 's'}`));
113+
}
114+
}
115+
} catch (error) {
116+
console.log(chalk.red(`❌ Failed to restart gateway: ${error instanceof Error ? error.message : String(error)}`));
117+
console.log(chalk.gray('💡 You can restart manually with "deploystack restart"'));
118+
}
119+
} else {
120+
console.log(chalk.gray('💡 Configuration updated. Restart gateway manually with "deploystack restart" when ready.'));
121+
}
82122
} else {
83-
console.log(chalk.gray('📋 No configuration changes detected'));
123+
console.log(chalk.gray('💡 Gateway is not currently running. Changes will take effect when you start it.'));
84124
}
85125

86126
// Show summary
87127
console.log(chalk.gray(`\n📊 Configuration Summary:`));
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()}`));
128+
console.log(chalk.gray(` Hardware ID: ${deviceInfo.hardware_id}`));
129+
console.log(chalk.gray(` Ready Servers: ${readyServers.length}`));
130+
console.log(chalk.gray(` Invalid Servers: ${invalidServers.length}`));
131+
console.log(chalk.gray(` Last Updated: ${new Date(gatewayConfig.lastUpdated).toLocaleString()}`));
92132

93133
} catch (error) {
94134
spinner.fail('Failed to refresh MCP configuration');

services/gateway/src/utils/device-detection.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,44 +59,42 @@ export async function generateHardwareFingerprint(): Promise<string> {
5959
.filter(iface => !iface?.internal && iface?.mac !== '00:00:00:00:00:00')
6060
.map(iface => iface?.mac)
6161
.filter(Boolean)
62-
.sort();
62+
.sort(); // Sort to ensure consistent ordering
6363

6464
// Get CPU information
6565
const cpus = os.cpus();
6666
const cpuModel = cpus.length > 0 ? cpus[0].model : 'unknown';
6767

68-
// Create fingerprint data
68+
// Create fingerprint data with consistent ordering
6969
const fingerprintData = {
70-
macs: macAddresses,
71-
hostname: os.hostname(),
72-
platform: os.platform(),
7370
arch: os.arch(),
7471
cpuModel: cpuModel,
75-
// Add some entropy from system info
76-
totalmem: os.totalmem(),
77-
homedir: os.homedir()
72+
homedir: os.homedir(),
73+
hostname: os.hostname(),
74+
macs: macAddresses,
75+
platform: os.platform(),
76+
totalmem: os.totalmem()
7877
};
7978

80-
// Generate SHA256 hash
79+
// Generate SHA256 hash with deterministic JSON serialization
8180
const fingerprint = crypto
8281
.createHash('sha256')
83-
.update(JSON.stringify(fingerprintData))
82+
.update(JSON.stringify(fingerprintData, Object.keys(fingerprintData).sort()))
8483
.digest('hex');
8584

8685
// Return first 32 characters for consistency
8786
return fingerprint.substring(0, 32);
8887
} catch {
89-
// Fallback fingerprint if hardware detection fails
88+
// Fallback fingerprint if hardware detection fails (deterministic)
9089
const fallbackData = {
91-
hostname: os.hostname(),
92-
platform: os.platform(),
9390
arch: os.arch(),
94-
timestamp: Date.now()
91+
hostname: os.hostname(),
92+
platform: os.platform()
9593
};
9694

9795
const fallbackFingerprint = crypto
9896
.createHash('sha256')
99-
.update(JSON.stringify(fallbackData))
97+
.update(JSON.stringify(fallbackData, Object.keys(fallbackData).sort()))
10098
.digest('hex');
10199

102100
return fallbackFingerprint.substring(0, 32);

0 commit comments

Comments
 (0)