Skip to content

Commit 925cd6e

Browse files
author
Lasim
committed
feat(gateway): add client notification functionality and tools refresh endpoint
1 parent 904e877 commit 925cd6e

File tree

9 files changed

+997
-5
lines changed

9 files changed

+997
-5
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { Command } from 'commander';
2+
import chalk from 'chalk';
3+
import { ClientStateCacheService } from '../core/client/client-state-cache';
4+
5+
export function registerClientsCommand(program: Command) {
6+
program
7+
.command('clients')
8+
.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);
13+
});
14+
}
15+
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> {
39+
try {
40+
const clientCache = new ClientStateCacheService();
41+
42+
// Get active clients from cache
43+
const activeClients = await clientCache.getActiveClients();
44+
const cacheSummary = await clientCache.getCacheSummary();
45+
46+
console.log(chalk.blue('Connected MCP Clients'));
47+
console.log(chalk.gray('═'.repeat(50)));
48+
49+
// Show summary
50+
if (activeClients.length === 0) {
51+
console.log(chalk.yellow('No clients currently connected'));
52+
console.log(chalk.gray('Clients will appear here when they connect to the gateway'));
53+
console.log(chalk.gray(' • SSE endpoint: http://localhost:9095/sse'));
54+
console.log(chalk.gray(' • MCP endpoint: http://localhost:9095/mcp'));
55+
56+
if (cacheSummary.exists && cacheSummary.disconnectedClients > 0) {
57+
console.log(chalk.gray(` • ${cacheSummary.disconnectedClients} recent disconnected client${cacheSummary.disconnectedClients === 1 ? '' : 's'} in cache`));
58+
}
59+
return;
60+
}
61+
62+
console.log(chalk.green(`Summary: ${activeClients.length} active connections`));
63+
console.log(chalk.gray(` • SSE connections: ${cacheSummary.sseClients}`));
64+
console.log(chalk.gray(` • Streamable HTTP connections: ${cacheSummary.streamableHttpClients}`));
65+
console.log(chalk.gray(` • Initialized clients: ${activeClients.filter(c => c.mcpInitialized).length}`));
66+
67+
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
68+
const recentlyActive = activeClients.filter(c => c.lastActivity > fiveMinutesAgo).length;
69+
console.log(chalk.gray(` • Active (last 5min): ${recentlyActive}`));
70+
console.log('');
71+
72+
// Show detailed client list
73+
console.log(chalk.blue('Client Details:'));
74+
console.log('');
75+
76+
// Sort by creation time (newest first)
77+
const sortedClients = activeClients.sort((a, b) => b.createdAt - a.createdAt);
78+
79+
for (const client of sortedClients) {
80+
const clientName = getClientDisplayName(client.clientInfo);
81+
const uptime = formatUptime(Date.now() - client.createdAt);
82+
const lastActivityAgo = formatUptime(Date.now() - client.lastActivity);
83+
84+
console.log(chalk.white(`${clientName}`));
85+
console.log(chalk.gray(` ID: ${client.id}`));
86+
console.log(chalk.gray(` Type: ${client.type} ${client.mcpInitialized ? 'Initialized' : 'Connecting'}`));
87+
console.log(chalk.gray(` Activity: ${lastActivityAgo} ago (${client.requestCount} requests)`));
88+
console.log(chalk.gray(` Uptime: ${uptime}`));
89+
90+
if (client.userAgent) {
91+
console.log(chalk.gray(` User Agent: ${client.userAgent}`));
92+
}
93+
94+
if (client.errorCount > 0) {
95+
console.log(chalk.red(` Errors: ${client.errorCount}`));
96+
}
97+
98+
console.log('');
99+
}
100+
101+
// Show connection endpoints
102+
console.log(chalk.blue('Connection Endpoints:'));
103+
console.log(chalk.gray(` • SSE: http://localhost:9095/sse`));
104+
console.log(chalk.gray(` • Messages: http://localhost:9095/message`));
105+
console.log(chalk.gray(` • MCP: http://localhost:9095/mcp`));
106+
console.log(chalk.gray(` • Health: http://localhost:9095/health`));
107+
108+
if (cacheSummary.lastUpdated) {
109+
const lastUpdated = new Date(cacheSummary.lastUpdated);
110+
console.log(chalk.gray(`\nCache last updated: ${lastUpdated.toLocaleString()}`));
111+
}
112+
113+
} catch (error) {
114+
console.error(chalk.red('Failed to retrieve client information:'));
115+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
116+
console.log(chalk.gray('Make sure the gateway has been started at least once to create the client cache'));
117+
process.exit(1);
118+
}
119+
}
120+
121+
/**
122+
* Get client display name from client info
123+
*/
124+
function getClientDisplayName(clientInfo?: { name: string; version: string }): string {
125+
if (!clientInfo) {
126+
return 'Unknown Client';
127+
}
128+
129+
return `${clientInfo.name} v${clientInfo.version}`;
130+
}
131+
132+
/**
133+
* Format uptime duration in human readable format
134+
*/
135+
function formatUptime(milliseconds: number): string {
136+
const seconds = Math.floor(milliseconds / 1000);
137+
const minutes = Math.floor(seconds / 60);
138+
const hours = Math.floor(minutes / 60);
139+
const days = Math.floor(hours / 24);
140+
141+
if (days > 0) {
142+
return `${days}d ${hours % 24}h ${minutes % 60}m`;
143+
} else if (hours > 0) {
144+
return `${hours}h ${minutes % 60}m`;
145+
} else if (minutes > 0) {
146+
return `${minutes}m ${seconds % 60}s`;
147+
} else {
148+
return `${seconds}s`;
149+
}
150+
}
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/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { registerWhoamiCommand } from './whoami';
44
export { registerTeamsCommand } from './teams';
55
export { registerMCPCommand } from './mcp';
66
export { registerRefreshCommand } from './refresh';
7+
export { registerClientsCommand } from './clients';
78
export { registerStartCommand } from './start';
89
export { registerRestartCommand } from './restart';
910
export { registerStopCommand } from './stop';

0 commit comments

Comments
 (0)