Skip to content

Commit 55fc834

Browse files
committed
feat(satellite): add debug endpoint for comprehensive MCP server info
1 parent a9ed054 commit 55fc834

File tree

4 files changed

+354
-0
lines changed

4 files changed

+354
-0
lines changed

services/satellite/.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,12 @@ DEPLOYSTACK_REGISTRATION_TOKEN=
3535
# Status Display Configuration (optional)
3636
DEPLOYSTACK_STATUS_SHOW_UPTIME=true
3737
DEPLOYSTACK_STATUS_SHOW_VERSION=true
38+
39+
# Debug Endpoint Configuration (optional)
40+
# WARNING: This exposes detailed MCP server information including:
41+
# - All running processes with PIDs
42+
# - Configuration details
43+
# - Tool discovery information
44+
# - Team isolation details
45+
# Recommended: Set to false in production environments
46+
DEPLOYSTACK_STATUS_SHOW_MCP_DEBUG_ROUTE=true

services/satellite/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ npm run dev
3030
# The server will start on http://localhost:3001
3131
# API Documentation: http://localhost:3001/documentation (comming soon)
3232
# Backend Status: http://localhost:3001/api/status/backend
33+
# Debug Information: http://localhost:3001/api/status/debug (when enabled)
3334
```
3435

3536
### Production
@@ -95,6 +96,9 @@ DEPLOYSTACK_REGISTRATION_TOKEN=your_token_here # JWT registration token from ad
9596
# Status Display Configuration (Optional)
9697
DEPLOYSTACK_STATUS_SHOW_UPTIME=true # Show uptime in status endpoint (default: true)
9798
DEPLOYSTACK_STATUS_SHOW_VERSION=true # Show version info in status endpoint (default: true)
99+
100+
# Debug Endpoint Configuration (Optional)
101+
DEPLOYSTACK_STATUS_SHOW_MCP_DEBUG_ROUTE=true # Enable debug endpoint (default: true, disable in production)
98102
```
99103

100104
### Required Environment Variables

services/satellite/src/routes/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import messageRoute from './message';
66
import mcpRoute from './mcp';
77
import oauthDiscoveryRoutes from './oauth-discovery';
88
import { registerBackendStatusRoutes } from './status/backend';
9+
import { registerDebugRoutes } from './status/debug';
910
import { registerSatelliteStatusRoutes } from './root';
1011

1112
export const registerRoutes = (server: FastifyInstance): void => {
@@ -31,5 +32,8 @@ export const registerRoutes = (server: FastifyInstance): void => {
3132
server.register(async (statusInstance) => {
3233
// Backend connection status - GET /api/status/backend
3334
await registerBackendStatusRoutes(statusInstance);
35+
36+
// Debug information - GET /api/status/debug
37+
await registerDebugRoutes(statusInstance);
3438
});
3539
};
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2+
import { ProcessManager } from '../../process/manager';
3+
import { RuntimeState } from '../../process/runtime-state';
4+
import { UnifiedToolDiscoveryManager } from '../../services/unified-tool-discovery-manager';
5+
import { DynamicConfigManager } from '../../services/dynamic-config-manager';
6+
import { TeamIsolationService } from '../../services/team-isolation-service';
7+
8+
const debugSchema = {
9+
tags: ['Debug'],
10+
summary: 'Get comprehensive debug information',
11+
description: 'Returns detailed information about running MCP servers, discovered tools, team isolation, and system state. Requires DEPLOYSTACK_STATUS_SHOW_MCP_DEBUG_ROUTE=true.',
12+
response: {
13+
200: {
14+
type: 'object',
15+
properties: {
16+
timestamp: { type: 'string', format: 'date-time' },
17+
satellite_info: {
18+
type: 'object',
19+
properties: {
20+
satellite_id: { type: 'string' },
21+
version: { type: 'string' },
22+
uptime_ms: { type: 'number' }
23+
}
24+
},
25+
mcp_servers: {
26+
type: 'object',
27+
properties: {
28+
total_configured: { type: 'number' },
29+
total_running: { type: 'number' },
30+
servers: {
31+
type: 'array',
32+
items: {
33+
type: 'object',
34+
properties: {
35+
installation_name: { type: 'string' },
36+
installation_id: { type: 'string' },
37+
team_id: { type: 'string' },
38+
team_slug: { type: 'string' },
39+
server_slug: { type: 'string' },
40+
transport_type: { type: 'string' },
41+
status: { type: 'string' },
42+
pid: { type: 'number' },
43+
uptime_ms: { type: 'number' },
44+
message_count: { type: 'number' },
45+
error_count: { type: 'number' },
46+
command: { type: 'string' },
47+
args: { type: 'array', items: { type: 'string' } },
48+
url: { type: 'string' }
49+
}
50+
}
51+
}
52+
}
53+
},
54+
tools: {
55+
type: 'object',
56+
properties: {
57+
total_tools: { type: 'number' },
58+
tools_by_transport: {
59+
type: 'object',
60+
properties: {
61+
http: { type: 'number' },
62+
stdio: { type: 'number' }
63+
}
64+
},
65+
tools_by_server: {
66+
type: 'object',
67+
additionalProperties: { type: 'number' }
68+
},
69+
all_tools: {
70+
type: 'array',
71+
items: {
72+
type: 'object',
73+
properties: {
74+
namespaced_name: { type: 'string' },
75+
original_name: { type: 'string' },
76+
server_name: { type: 'string' },
77+
transport: { type: 'string' },
78+
description: { type: 'string' },
79+
discovered_at: { type: 'string' }
80+
}
81+
}
82+
}
83+
}
84+
},
85+
configuration: {
86+
type: 'object',
87+
properties: {
88+
total_servers_configured: { type: 'number' },
89+
enabled_servers: { type: 'number' },
90+
disabled_servers: { type: 'number' },
91+
servers_by_transport: {
92+
type: 'object',
93+
properties: {
94+
http: { type: 'number' },
95+
stdio: { type: 'number' }
96+
}
97+
}
98+
}
99+
}
100+
}
101+
},
102+
403: {
103+
type: 'object',
104+
properties: {
105+
error: { type: 'string' },
106+
message: { type: 'string' }
107+
}
108+
}
109+
}
110+
};
111+
112+
export async function registerDebugRoutes(server: FastifyInstance) {
113+
// Get services from server instance
114+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
115+
const processManager = (server as any).processManager as ProcessManager | undefined;
116+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
117+
const runtimeState = (server as any).runtimeState as RuntimeState | undefined;
118+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
119+
const toolDiscoveryManager = (server as any).toolDiscoveryManager as UnifiedToolDiscoveryManager | undefined;
120+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
121+
const dynamicConfigManager = (server as any).dynamicConfigManager as DynamicConfigManager | undefined;
122+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
123+
const teamIsolationService = (server as any).teamIsolationService as TeamIsolationService | undefined;
124+
125+
if (!processManager || !runtimeState || !toolDiscoveryManager || !dynamicConfigManager) {
126+
server.log.error('Required services not found on server instance (processManager, runtimeState, toolDiscoveryManager, or dynamicConfigManager)');
127+
throw new Error('Required services not initialized');
128+
}
129+
130+
server.get('/api/status/debug', {
131+
schema: debugSchema
132+
}, async (request: FastifyRequest, reply: FastifyReply) => {
133+
// Check if debug route is enabled via environment variable
134+
const debugRouteEnabled = process.env.DEPLOYSTACK_STATUS_SHOW_MCP_DEBUG_ROUTE === 'true';
135+
136+
if (!debugRouteEnabled) {
137+
request.log.warn({
138+
operation: 'debug_endpoint_disabled',
139+
client_ip: request.ip,
140+
endpoint: '/api/status/debug'
141+
}, 'Debug endpoint access denied - route disabled via environment variable');
142+
143+
return reply.code(403).send({
144+
error: 'Access Denied',
145+
message: 'Debug endpoint is disabled. Set DEPLOYSTACK_STATUS_SHOW_MCP_DEBUG_ROUTE=true to enable.'
146+
});
147+
}
148+
149+
request.log.info({
150+
operation: 'debug_endpoint_access',
151+
endpoint: '/api/status/debug'
152+
}, 'Debug information requested');
153+
154+
try {
155+
// Get all running processes
156+
const allProcesses = processManager.getAllProcesses();
157+
const allRuntimeProcesses = runtimeState.getAllProcesses();
158+
const runtimeStatus: Record<string, { status: string; restart_count?: number }> = {};
159+
allRuntimeProcesses.forEach(proc => {
160+
runtimeStatus[proc.installationName] = {
161+
status: proc.status,
162+
restart_count: 0 // RuntimeState doesn't track restart count
163+
};
164+
});
165+
166+
// Get current configuration
167+
const currentConfig = dynamicConfigManager.getCurrentConfiguration();
168+
const configStats = dynamicConfigManager.getStats();
169+
170+
// Get tool discovery stats
171+
const toolStats = toolDiscoveryManager.getStats();
172+
const allTools = toolDiscoveryManager.getAllTools();
173+
174+
// Build server information
175+
const servers = allProcesses.map(processInfo => {
176+
const status = runtimeStatus[processInfo.config.installation_name];
177+
let teamInfo = { serverSlug: 'unknown', teamSlug: 'unknown', installationId: 'unknown' };
178+
179+
try {
180+
if (teamIsolationService) {
181+
teamInfo = teamIsolationService.extractTeamInfo(processInfo.config.installation_name);
182+
}
183+
} catch (error) {
184+
request.log.debug({
185+
operation: 'debug_team_info_extraction_failed',
186+
installation_name: processInfo.config.installation_name,
187+
error: error instanceof Error ? error.message : String(error)
188+
}, 'Failed to extract team info');
189+
}
190+
191+
return {
192+
installation_name: processInfo.config.installation_name,
193+
installation_id: processInfo.config.installation_id,
194+
team_id: processInfo.config.team_id,
195+
team_slug: teamInfo.teamSlug,
196+
server_slug: teamInfo.serverSlug,
197+
transport_type: 'stdio', // ProcessManager only handles stdio processes
198+
status: status?.status || processInfo.status,
199+
pid: processInfo.process.pid,
200+
uptime_ms: Date.now() - processInfo.startTime,
201+
message_count: processInfo.messageCount,
202+
error_count: processInfo.errorCount,
203+
command: processInfo.config.command,
204+
args: processInfo.config.args,
205+
last_activity: new Date(processInfo.lastActivity).toISOString(),
206+
restart_count: status?.restart_count || 0
207+
};
208+
});
209+
210+
// Add configured but not running servers
211+
const runningNames = new Set(allProcesses.map(p => p.config.installation_name));
212+
const configuredServers = Object.entries(currentConfig.servers)
213+
.filter(([_, config]) => config.enabled !== false)
214+
.map(([serverName, serverConfig]) => {
215+
if (runningNames.has(serverName)) {
216+
return null; // Already included in running servers
217+
}
218+
219+
const status = runtimeStatus[serverName];
220+
let teamInfo = { serverSlug: 'unknown', teamSlug: 'unknown', installationId: 'unknown' };
221+
222+
try {
223+
if (teamIsolationService) {
224+
teamInfo = teamIsolationService.extractTeamInfo(serverName);
225+
}
226+
} catch {
227+
// Ignore extraction errors for non-running servers
228+
}
229+
230+
return {
231+
installation_name: serverName,
232+
installation_id: serverConfig.installation_id || 'unknown',
233+
team_id: serverConfig.team_id || 'unknown',
234+
team_slug: teamInfo.teamSlug,
235+
server_slug: teamInfo.serverSlug,
236+
transport_type: serverConfig.transport_type || serverConfig.type || 'unknown',
237+
status: status?.status || 'configured',
238+
uptime_ms: 0,
239+
message_count: 0,
240+
error_count: 0,
241+
command: serverConfig.command || serverConfig.url || '',
242+
args: serverConfig.args || [],
243+
url: serverConfig.url,
244+
restart_count: status?.restart_count || 0
245+
};
246+
})
247+
.filter(Boolean);
248+
249+
const allServers = [...servers, ...configuredServers];
250+
251+
// Build tools information
252+
const toolsData = allTools.map(tool => ({
253+
namespaced_name: tool.namespacedName,
254+
original_name: tool.originalName,
255+
server_name: tool.serverName,
256+
transport: tool.transport,
257+
description: tool.description,
258+
discovered_at: tool.discoveredAt?.toISOString()
259+
}));
260+
261+
// Count tools by transport
262+
const httpTools = allTools.filter(t => t.transport === 'http').length;
263+
const stdioTools = allTools.filter(t => t.transport === 'stdio').length;
264+
265+
// Count servers by transport type
266+
const httpServers = Object.values(currentConfig.servers)
267+
.filter(s => s.enabled !== false && (s.transport_type === 'http' || s.type === 'http')).length;
268+
const stdioServers = Object.values(currentConfig.servers)
269+
.filter(s => s.enabled !== false && (s.transport_type === 'stdio' || s.type === 'stdio')).length;
270+
271+
// Build response
272+
const debugInfo = {
273+
timestamp: new Date().toISOString(),
274+
satellite_info: {
275+
satellite_id: process.env.SATELLITE_ID || 'unknown',
276+
version: '1.0.0',
277+
uptime_ms: process.uptime() * 1000
278+
},
279+
mcp_servers: {
280+
total_configured: Object.keys(currentConfig.servers).filter(k =>
281+
currentConfig.servers[k].enabled !== false
282+
).length,
283+
total_running: allProcesses.length,
284+
servers: allServers
285+
},
286+
tools: {
287+
total_tools: allTools.length,
288+
tools_by_transport: {
289+
http: httpTools,
290+
stdio: stdioTools
291+
},
292+
tools_by_server: toolStats.stdio.tools_by_server,
293+
all_tools: toolsData
294+
},
295+
configuration: {
296+
total_servers_configured: configStats.total_servers,
297+
enabled_servers: configStats.enabled_servers,
298+
disabled_servers: configStats.disabled_servers,
299+
servers_by_transport: {
300+
http: httpServers,
301+
stdio: stdioServers
302+
},
303+
last_update: new Date().toISOString() // ConfigStats doesn't track last_update timestamp
304+
}
305+
};
306+
307+
request.log.info({
308+
operation: 'debug_endpoint_success',
309+
total_servers: allServers.length,
310+
running_servers: allProcesses.length,
311+
total_tools: allTools.length
312+
}, 'Debug information retrieved successfully');
313+
314+
return reply.code(200).send(debugInfo);
315+
316+
} catch (error) {
317+
const errorMessage = error instanceof Error ? error.message : String(error);
318+
319+
request.log.error({
320+
operation: 'debug_endpoint_error',
321+
error: errorMessage
322+
}, 'Failed to retrieve debug information');
323+
324+
return reply.code(500).send({
325+
error: 'Internal Server Error',
326+
message: errorMessage
327+
});
328+
}
329+
});
330+
331+
const debugRouteEnabled = process.env.DEPLOYSTACK_STATUS_SHOW_MCP_DEBUG_ROUTE === 'true';
332+
server.log.info({
333+
operation: 'routes_registered',
334+
routes: ['/api/status/debug'],
335+
enabled: debugRouteEnabled
336+
}, `Debug routes registered (${debugRouteEnabled ? 'enabled' : 'disabled'})`);
337+
}

0 commit comments

Comments
 (0)