Skip to content

Commit faec3ca

Browse files
author
Lasim
committed
feat(gateway): add 'clients' command to display connected MCP clients with detailed information
1 parent 3cdfc02 commit faec3ca

File tree

5 files changed

+92
-139
lines changed

5 files changed

+92
-139
lines changed

services/gateway/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,62 @@ deploystack status --verbose
368368
deploystack status --json
369369
```
370370

371+
### `deploystack clients`
372+
373+
Show all connected MCP clients with detailed connection information.
374+
375+
**Examples:**
376+
377+
```bash
378+
# Show connected clients
379+
deploystack clients
380+
```
381+
382+
**Features:**
383+
384+
- **Real-time Status**: Shows currently connected MCP clients from cache
385+
- **Connection Types**: Displays both SSE and Streamable HTTP connections
386+
- **Activity Tracking**: Shows last activity time and request counts
387+
- **Client Information**: Displays client name, version, and user agent when available
388+
- **Connection Endpoints**: Lists all available gateway endpoints
389+
- **Cache Integration**: Uses client state cache for accurate connection tracking
390+
391+
**Sample Output:**
392+
393+
```text
394+
Connected MCP Clients
395+
══════════════════════════════════════════════════
396+
397+
Summary: 2 active connections
398+
• SSE connections: 1
399+
• Streamable HTTP connections: 1
400+
• Initialized clients: 2
401+
• Active (last 5min): 2
402+
403+
Client Details:
404+
405+
VS Code v1.95.0
406+
ID: sse_abc123
407+
Type: SSE Initialized
408+
Activity: 30s ago (15 requests)
409+
Uptime: 2h 15m
410+
User Agent: VSCode/1.95.0
411+
412+
Cursor v0.42.0
413+
ID: http_def456
414+
Type: Streamable HTTP Initialized
415+
Activity: 1m ago (8 requests)
416+
Uptime: 45m 30s
417+
418+
Connection Endpoints:
419+
• SSE: http://localhost:9095/sse
420+
• Messages: http://localhost:9095/message
421+
• MCP: http://localhost:9095/mcp
422+
• Health: http://localhost:9095/health
423+
424+
Cache last updated: 1/4/2025, 9:30:15 AM
425+
```
426+
371427
## 🛠️ Troubleshooting
372428

373429
### Common Issues

services/gateway/src/commands/clients.ts

Lines changed: 3 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,12 @@ export function registerClientsCommand(program: Command) {
66
program
77
.command('clients')
88
.description('Show all connected MCP clients')
9-
.option('--port <port>', 'Gateway port (default: 9095)', '9095')
10-
.option('--host <host>', 'Gateway host (default: localhost)', 'localhost')
11-
.action(async (options) => {
12-
await showConnectedClients(options);
9+
.action(async () => {
10+
await showConnectedClients();
1311
});
1412
}
1513

16-
interface ClientConnection {
17-
id: string;
18-
type: 'SSE' | 'Streamable HTTP';
19-
clientInfo?: {
20-
name: string;
21-
version: string;
22-
};
23-
createdAt: number;
24-
lastActivity: number;
25-
uptime: number;
26-
requestCount: number;
27-
errorCount: number;
28-
mcpInitialized: boolean;
29-
}
30-
31-
interface ClientStatus {
32-
totalConnections: number;
33-
sseConnections: number;
34-
streamableHttpConnections: number;
35-
clients: ClientConnection[];
36-
}
37-
38-
async function showConnectedClients(options: { port: string; host: string }): Promise<void> {
14+
async function showConnectedClients(): Promise<void> {
3915
try {
4016
const clientCache = new ClientStateCacheService();
4117

@@ -148,104 +124,3 @@ function formatUptime(milliseconds: number): string {
148124
return `${seconds}s`;
149125
}
150126
}
151-
152-
/**
153-
* Fetch gateway status via HTTP
154-
*/
155-
async function fetchGatewayStatus(gatewayUrl: string): Promise<any> {
156-
const fetch = (await import('node-fetch')).default;
157-
158-
// Create AbortController for timeout
159-
const controller = new AbortController();
160-
const timeoutId = setTimeout(() => controller.abort(), 5000);
161-
162-
try {
163-
const response = await fetch(`${gatewayUrl}/status`, {
164-
signal: controller.signal
165-
});
166-
167-
clearTimeout(timeoutId);
168-
169-
if (!response.ok) {
170-
throw new Error(`HTTP ${response.status}`);
171-
}
172-
173-
return await response.json();
174-
} catch (error) {
175-
clearTimeout(timeoutId);
176-
throw error;
177-
}
178-
}
179-
180-
/**
181-
* Extract client status from gateway status response
182-
*/
183-
function extractClientStatus(gatewayStatus: any): ClientStatus {
184-
const clients: ClientConnection[] = [];
185-
const now = Date.now();
186-
187-
// Extract SSE sessions
188-
if (gatewayStatus.clients?.sse?.sessions) {
189-
for (const session of gatewayStatus.clients.sse.sessions) {
190-
clients.push({
191-
id: session.id,
192-
type: 'SSE',
193-
clientInfo: session.clientInfo,
194-
createdAt: session.createdAt,
195-
lastActivity: session.lastActivity,
196-
uptime: session.uptime,
197-
requestCount: session.requestCount,
198-
errorCount: session.errorCount,
199-
mcpInitialized: session.mcpInitialized
200-
});
201-
}
202-
}
203-
204-
// Extract Streamable HTTP sessions
205-
if (gatewayStatus.clients?.streamableHttp?.sessions) {
206-
for (const session of gatewayStatus.clients.streamableHttp.sessions) {
207-
clients.push({
208-
id: session.id,
209-
type: 'Streamable HTTP',
210-
clientInfo: session.clientInfo,
211-
createdAt: session.createdAt,
212-
lastActivity: session.lastActivity,
213-
uptime: session.uptime,
214-
requestCount: session.requestCount,
215-
errorCount: session.errorCount,
216-
mcpInitialized: session.mcpInitialized
217-
});
218-
}
219-
}
220-
221-
// Sort by creation time (newest first)
222-
clients.sort((a, b) => b.createdAt - a.createdAt);
223-
224-
return {
225-
totalConnections: clients.length,
226-
sseConnections: gatewayStatus.clients?.sse?.activeCount || 0,
227-
streamableHttpConnections: gatewayStatus.clients?.streamableHttp?.activeSessionCount || 0,
228-
clients
229-
};
230-
}
231-
232-
/**
233-
* Calculate summary statistics
234-
*/
235-
function calculateSummary(status: ClientStatus): {
236-
total: number;
237-
sse: number;
238-
streamableHttp: number;
239-
initialized: number;
240-
active: number;
241-
} {
242-
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
243-
244-
return {
245-
total: status.totalConnections,
246-
sse: status.sseConnections,
247-
streamableHttp: status.streamableHttpConnections,
248-
initialized: status.clients.filter(c => c.mcpInitialized).length,
249-
active: status.clients.filter(c => c.lastActivity > fiveMinutesAgo).length
250-
};
251-
}

services/gateway/src/commands/stop.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ export function registerStopCommand(program: Command) {
1313

1414
try {
1515
const timeoutSeconds = parseInt(options.timeout, 10) || 30;
16-
console.log(chalk.blue('🛑 Stopping DeployStack Gateway...'));
16+
console.log(chalk.blue('Stopping DeployStack Gateway...'));
1717

1818
// Get current PID for progress messages
1919
const pid = stopService.getRunningPid();
2020

2121
if (options.force && pid) {
22-
console.log(chalk.yellow('⚠️ Force stopping gateway (MCP servers may not shutdown gracefully)'));
22+
console.log(chalk.yellow('Force stopping gateway (MCP servers may not shutdown gracefully)'));
2323
console.log(chalk.gray(` Sending SIGKILL to process ${pid}...`));
2424
} else if (pid) {
2525
console.log(chalk.gray(` Sending SIGTERM to process ${pid}...`));
@@ -35,21 +35,19 @@ export function registerStopCommand(program: Command) {
3535

3636
if (result.success) {
3737
if (result.wasRunning) {
38-
console.log(chalk.green(`${result.message}`));
39-
console.log(chalk.gray('💡 All MCP servers have been stopped along with the gateway'));
38+
console.log(chalk.green(`${result.message}`));
39+
console.log(chalk.gray('All MCP servers have been stopped along with the gateway'));
4040
} else {
41-
console.log(chalk.yellow(`⚠️ ${result.message}`));
41+
console.log(chalk.yellow(`${result.message}`));
4242
}
4343
} else {
44-
console.log(chalk.red(`${result.message}`));
44+
console.log(chalk.red(`${result.message}`));
4545
process.exit(1);
4646
}
4747

4848
} catch (error) {
49-
console.error(chalk.red('Failed to stop gateway:'), error instanceof Error ? error.message : String(error));
49+
console.error(chalk.red('Failed to stop gateway:'), error instanceof Error ? error.message : String(error));
5050
process.exit(1);
5151
}
5252
});
5353
}
54-
55-

services/gateway/src/core/client/client-state-cache.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,10 +368,10 @@ export class ClientStateCacheService {
368368
try {
369369
if (await fs.pathExists(this.cacheFile)) {
370370
await fs.remove(this.cacheFile);
371-
console.log(chalk.gray(`🗑️ Client state cache cleared`));
371+
console.log(chalk.gray(`Client state cache cleared`));
372372
}
373373
} catch (error) {
374-
console.warn(chalk.yellow(`⚠️ Failed to clear client cache:`, error));
374+
console.warn(chalk.yellow(`Failed to clear client cache:`, error));
375375
}
376376
}
377377
}

services/gateway/src/services/server-stop-service.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from 'fs';
22
import path from 'path';
33
import os from 'os';
4+
import { ClientStateCacheService } from '../core/client/client-state-cache';
45

56
// PID file location
67
const PID_FILE = path.join(os.tmpdir(), 'deploystack-gateway.pid');
@@ -17,6 +18,12 @@ export interface ServerStopResult {
1718
}
1819

1920
export class ServerStopService {
21+
private clientCacheService: ClientStateCacheService;
22+
23+
constructor() {
24+
this.clientCacheService = new ClientStateCacheService();
25+
}
26+
2027
/**
2128
* Stop the DeployStack Gateway server gracefully or forcefully
2229
*/
@@ -68,13 +75,15 @@ export class ServerStopService {
6875
try {
6976
process.kill(pid, 'SIGKILL');
7077
this.removePidFile();
78+
await this.cleanupClientCache();
7179
return {
7280
success: true,
7381
message: 'Gateway server force stopped (MCP servers may not have shutdown gracefully)',
7482
wasRunning: true
7583
};
7684
} catch {
7785
this.removePidFile();
86+
await this.cleanupClientCache();
7887
return {
7988
success: true,
8089
message: 'Process was already stopped - cleaned up PID file',
@@ -108,6 +117,7 @@ export class ServerStopService {
108117
};
109118
} else {
110119
this.removePidFile();
120+
await this.cleanupClientCache();
111121
return {
112122
success: true,
113123
message: `Gateway force stopped after ${timeoutSeconds}s timeout`,
@@ -116,6 +126,7 @@ export class ServerStopService {
116126
}
117127
} else {
118128
this.removePidFile();
129+
await this.cleanupClientCache();
119130
return {
120131
success: true,
121132
message: 'Gateway server stopped gracefully',
@@ -125,6 +136,7 @@ export class ServerStopService {
125136
} catch (error) {
126137
if ((error as NodeJS.ErrnoException).code === 'ESRCH') {
127138
this.removePidFile();
139+
await this.cleanupClientCache();
128140
return {
129141
success: true,
130142
message: 'Process was already stopped - cleaned up PID file',
@@ -203,4 +215,16 @@ export class ServerStopService {
203215
// Ignore file removal errors
204216
}
205217
}
218+
219+
/**
220+
* Clean up client state cache when gateway stops
221+
*/
222+
private async cleanupClientCache(): Promise<void> {
223+
try {
224+
await this.clientCacheService.clearCache();
225+
} catch (error) {
226+
// Don't fail the stop operation if cache cleanup fails
227+
console.warn('Failed to clear client cache:', error instanceof Error ? error.message : String(error));
228+
}
229+
}
206230
}

0 commit comments

Comments
 (0)