mcp-inspect 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/src/App.d.ts +7 -0
- package/dist/src/App.d.ts.map +1 -0
- package/dist/src/App.js +591 -0
- package/dist/src/App.js.map +1 -0
- package/dist/src/components/DetailsModal.d.ts +11 -0
- package/dist/src/components/DetailsModal.d.ts.map +1 -0
- package/dist/src/components/DetailsModal.js +50 -0
- package/dist/src/components/DetailsModal.js.map +1 -0
- package/dist/src/components/HistoryTab.d.ts +13 -0
- package/dist/src/components/HistoryTab.d.ts.map +1 -0
- package/dist/src/components/HistoryTab.js +122 -0
- package/dist/src/components/HistoryTab.js.map +1 -0
- package/dist/src/components/InfoTab.d.ts +13 -0
- package/dist/src/components/InfoTab.d.ts.map +1 -0
- package/dist/src/components/InfoTab.js +28 -0
- package/dist/src/components/InfoTab.js.map +1 -0
- package/dist/src/components/NotificationsTab.d.ts +13 -0
- package/dist/src/components/NotificationsTab.d.ts.map +1 -0
- package/dist/src/components/NotificationsTab.js +37 -0
- package/dist/src/components/NotificationsTab.js.map +1 -0
- package/dist/src/components/PromptsTab.d.ts +13 -0
- package/dist/src/components/PromptsTab.d.ts.map +1 -0
- package/dist/src/components/PromptsTab.js +60 -0
- package/dist/src/components/PromptsTab.js.map +1 -0
- package/dist/src/components/ResourcesTab.d.ts +13 -0
- package/dist/src/components/ResourcesTab.d.ts.map +1 -0
- package/dist/src/components/ResourcesTab.js +60 -0
- package/dist/src/components/ResourcesTab.js.map +1 -0
- package/dist/src/components/Tabs.d.ts +24 -0
- package/dist/src/components/Tabs.d.ts.map +1 -0
- package/dist/src/components/Tabs.js +22 -0
- package/dist/src/components/Tabs.js.map +1 -0
- package/dist/src/components/ToolTestModal.d.ts +11 -0
- package/dist/src/components/ToolTestModal.d.ts.map +1 -0
- package/dist/src/components/ToolTestModal.js +112 -0
- package/dist/src/components/ToolTestModal.js.map +1 -0
- package/dist/src/components/ToolsTab.d.ts +14 -0
- package/dist/src/components/ToolsTab.d.ts.map +1 -0
- package/dist/src/components/ToolsTab.js +76 -0
- package/dist/src/components/ToolsTab.js.map +1 -0
- package/dist/src/hooks/useMCPClient.d.ts +41 -0
- package/dist/src/hooks/useMCPClient.d.ts.map +1 -0
- package/dist/src/hooks/useMCPClient.js +179 -0
- package/dist/src/hooks/useMCPClient.js.map +1 -0
- package/dist/src/hooks/useMessageTracking.d.ts +9 -0
- package/dist/src/hooks/useMessageTracking.d.ts.map +1 -0
- package/dist/src/hooks/useMessageTracking.js +124 -0
- package/dist/src/hooks/useMessageTracking.js.map +1 -0
- package/dist/src/types/focus.d.ts +2 -0
- package/dist/src/types/focus.d.ts.map +1 -0
- package/dist/src/types/focus.js +2 -0
- package/dist/src/types/focus.js.map +1 -0
- package/dist/src/types/messages.d.ts +14 -0
- package/dist/src/types/messages.d.ts.map +1 -0
- package/dist/src/types/messages.js +2 -0
- package/dist/src/types/messages.js.map +1 -0
- package/dist/src/types.d.ts +48 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/schemaToForm.d.ts +9 -0
- package/dist/src/utils/schemaToForm.d.ts.map +1 -0
- package/dist/src/utils/schemaToForm.js +107 -0
- package/dist/src/utils/schemaToForm.js.map +1 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +43 -0
- package/dist/tui.js.map +1 -0
- package/package.json +58 -0
- package/screenshots/mcp-inspector.png +0 -0
package/dist/src/App.js
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo, useEffect, useCallback } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, join, resolve } from 'path';
|
|
7
|
+
import { useMCPClient, LoggingProxyTransport } from './hooks/useMCPClient.js';
|
|
8
|
+
import { useMessageTracking } from './hooks/useMessageTracking.js';
|
|
9
|
+
import { Tabs, tabs as tabList } from './components/Tabs.js';
|
|
10
|
+
import { InfoTab } from './components/InfoTab.js';
|
|
11
|
+
import { ResourcesTab } from './components/ResourcesTab.js';
|
|
12
|
+
import { PromptsTab } from './components/PromptsTab.js';
|
|
13
|
+
import { ToolsTab } from './components/ToolsTab.js';
|
|
14
|
+
import { NotificationsTab } from './components/NotificationsTab.js';
|
|
15
|
+
import { HistoryTab } from './components/HistoryTab.js';
|
|
16
|
+
import { ToolTestModal } from './components/ToolTestModal.js';
|
|
17
|
+
import { DetailsModal } from './components/DetailsModal.js';
|
|
18
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
19
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
20
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
21
|
+
import { Client as MCPClient } from '@modelcontextprotocol/sdk/client/index.js';
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = dirname(__filename);
|
|
24
|
+
// Read package.json to get project info
|
|
25
|
+
const packagePath = join(__dirname, '..', 'package.json');
|
|
26
|
+
const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
27
|
+
function App({ configFile, onExit }) {
|
|
28
|
+
const [selectedServer, setSelectedServer] = useState(null);
|
|
29
|
+
const [activeTab, setActiveTab] = useState('info');
|
|
30
|
+
const [focus, setFocus] = useState('serverList');
|
|
31
|
+
const [tabCounts, setTabCounts] = useState({});
|
|
32
|
+
// Tool test modal state
|
|
33
|
+
const [toolTestModal, setToolTestModal] = useState(null);
|
|
34
|
+
// Details modal state
|
|
35
|
+
const [detailsModal, setDetailsModal] = useState(null);
|
|
36
|
+
// Server state management - store state for all servers
|
|
37
|
+
const [serverStates, setServerStates] = useState({});
|
|
38
|
+
const [serverClients, setServerClients] = useState({});
|
|
39
|
+
// Message tracking
|
|
40
|
+
const { history: messageHistory, trackRequest, trackResponse, trackNotification, clearHistory } = useMessageTracking();
|
|
41
|
+
const [dimensions, setDimensions] = useState({
|
|
42
|
+
width: process.stdout.columns || 80,
|
|
43
|
+
height: process.stdout.rows || 24,
|
|
44
|
+
});
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const updateDimensions = () => {
|
|
47
|
+
setDimensions({
|
|
48
|
+
width: process.stdout.columns || 80,
|
|
49
|
+
height: process.stdout.rows || 24,
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
process.stdout.on('resize', updateDimensions);
|
|
53
|
+
return () => {
|
|
54
|
+
process.stdout.off('resize', updateDimensions);
|
|
55
|
+
};
|
|
56
|
+
}, []);
|
|
57
|
+
// Parse MCP configuration
|
|
58
|
+
const mcpConfig = useMemo(() => {
|
|
59
|
+
try {
|
|
60
|
+
const configPath = resolve(process.cwd(), configFile);
|
|
61
|
+
const configContent = readFileSync(configPath, 'utf-8');
|
|
62
|
+
const config = JSON.parse(configContent);
|
|
63
|
+
if (!config.mcpServers) {
|
|
64
|
+
throw new Error('Configuration file must contain an mcpServers element');
|
|
65
|
+
}
|
|
66
|
+
return config;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof Error) {
|
|
70
|
+
console.error(`Error loading configuration: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.error('Error loading configuration: Unknown error');
|
|
74
|
+
}
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}, [configFile]);
|
|
78
|
+
const serverNames = Object.keys(mcpConfig.mcpServers);
|
|
79
|
+
const selectedServerConfig = selectedServer ? mcpConfig.mcpServers[selectedServer] : null;
|
|
80
|
+
// Preselect the first server on mount
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (serverNames.length > 0 && selectedServer === null) {
|
|
83
|
+
setSelectedServer(serverNames[0]);
|
|
84
|
+
}
|
|
85
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
|
+
}, []);
|
|
87
|
+
// Initialize server states for all configured servers on mount
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const initialStates = {};
|
|
90
|
+
for (const serverName of serverNames) {
|
|
91
|
+
if (!(serverName in serverStates)) {
|
|
92
|
+
initialStates[serverName] = {
|
|
93
|
+
status: 'disconnected',
|
|
94
|
+
error: null,
|
|
95
|
+
capabilities: {},
|
|
96
|
+
serverInfo: undefined,
|
|
97
|
+
instructions: undefined,
|
|
98
|
+
resources: [],
|
|
99
|
+
prompts: [],
|
|
100
|
+
tools: [],
|
|
101
|
+
stderrLogs: [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (Object.keys(initialStates).length > 0) {
|
|
106
|
+
setServerStates(prev => ({ ...prev, ...initialStates }));
|
|
107
|
+
}
|
|
108
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
109
|
+
}, []);
|
|
110
|
+
// Memoize message tracking callbacks to prevent unnecessary re-renders
|
|
111
|
+
const messageTracking = useMemo(() => {
|
|
112
|
+
if (!selectedServer)
|
|
113
|
+
return undefined;
|
|
114
|
+
return {
|
|
115
|
+
trackRequest: (msg) => trackRequest(selectedServer, msg),
|
|
116
|
+
trackResponse: (msg) => trackResponse(selectedServer, msg),
|
|
117
|
+
trackNotification: (msg) => trackNotification(selectedServer, msg),
|
|
118
|
+
};
|
|
119
|
+
}, [selectedServer, trackRequest, trackResponse, trackNotification]);
|
|
120
|
+
// Get client for selected server (for connection management)
|
|
121
|
+
const { connection, connect: connectClient, disconnect: disconnectClient } = useMCPClient(selectedServer, selectedServerConfig, messageTracking);
|
|
122
|
+
// Helper function to determine transport type (defaults to stdio)
|
|
123
|
+
const getServerType = useCallback((config) => {
|
|
124
|
+
if ('type' in config) {
|
|
125
|
+
if (config.type === 'sse' || config.type === 'streamableHttp') {
|
|
126
|
+
return config.type;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return 'stdio';
|
|
130
|
+
}, []);
|
|
131
|
+
// Helper function to create the appropriate transport
|
|
132
|
+
const createTransport = useCallback((config, serverName) => {
|
|
133
|
+
const serverType = getServerType(config);
|
|
134
|
+
if (serverType === 'stdio') {
|
|
135
|
+
const stdioConfig = config;
|
|
136
|
+
const baseTransport = new StdioClientTransport({
|
|
137
|
+
command: stdioConfig.command,
|
|
138
|
+
args: stdioConfig.args || [],
|
|
139
|
+
env: stdioConfig.env,
|
|
140
|
+
cwd: stdioConfig.cwd,
|
|
141
|
+
stderr: 'pipe',
|
|
142
|
+
});
|
|
143
|
+
// Capture stderr from stdio transport - set up listener immediately after creation
|
|
144
|
+
if (baseTransport.stderr) {
|
|
145
|
+
baseTransport.stderr.on('data', (data) => {
|
|
146
|
+
const logEntry = data.toString().trim();
|
|
147
|
+
if (logEntry) {
|
|
148
|
+
setServerStates(prev => {
|
|
149
|
+
const existingState = prev[serverName];
|
|
150
|
+
if (!existingState) {
|
|
151
|
+
// Initialize state if it doesn't exist yet
|
|
152
|
+
return {
|
|
153
|
+
...prev,
|
|
154
|
+
[serverName]: {
|
|
155
|
+
status: 'connecting',
|
|
156
|
+
error: null,
|
|
157
|
+
capabilities: {},
|
|
158
|
+
serverInfo: undefined,
|
|
159
|
+
instructions: undefined,
|
|
160
|
+
resources: [],
|
|
161
|
+
prompts: [],
|
|
162
|
+
tools: [],
|
|
163
|
+
stderrLogs: [{ timestamp: new Date(), message: logEntry }],
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
...prev,
|
|
169
|
+
[serverName]: {
|
|
170
|
+
...existingState,
|
|
171
|
+
stderrLogs: [
|
|
172
|
+
...(existingState.stderrLogs || []),
|
|
173
|
+
{ timestamp: new Date(), message: logEntry }
|
|
174
|
+
].slice(-1000), // Keep last 1000 log entries
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return { transport: baseTransport, baseTransport };
|
|
182
|
+
}
|
|
183
|
+
else if (serverType === 'sse') {
|
|
184
|
+
const sseConfig = config;
|
|
185
|
+
const url = new URL(sseConfig.url);
|
|
186
|
+
// Merge headers and requestInit
|
|
187
|
+
const eventSourceInit = {
|
|
188
|
+
...sseConfig.eventSourceInit,
|
|
189
|
+
...(sseConfig.headers && { headers: sseConfig.headers }),
|
|
190
|
+
};
|
|
191
|
+
const requestInit = {
|
|
192
|
+
...sseConfig.requestInit,
|
|
193
|
+
...(sseConfig.headers && { headers: sseConfig.headers }),
|
|
194
|
+
};
|
|
195
|
+
const transport = new SSEClientTransport(url, {
|
|
196
|
+
eventSourceInit,
|
|
197
|
+
requestInit,
|
|
198
|
+
});
|
|
199
|
+
return { transport };
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// streamableHttp
|
|
203
|
+
const httpConfig = config;
|
|
204
|
+
const url = new URL(httpConfig.url);
|
|
205
|
+
// Merge headers and requestInit
|
|
206
|
+
const requestInit = {
|
|
207
|
+
...httpConfig.requestInit,
|
|
208
|
+
...(httpConfig.headers && { headers: httpConfig.headers }),
|
|
209
|
+
};
|
|
210
|
+
const transport = new StreamableHTTPClientTransport(url, {
|
|
211
|
+
requestInit,
|
|
212
|
+
});
|
|
213
|
+
return { transport };
|
|
214
|
+
}
|
|
215
|
+
}, [getServerType]);
|
|
216
|
+
// Connect handler - connects, gets capabilities, and queries resources/prompts/tools
|
|
217
|
+
const handleConnect = useCallback(async () => {
|
|
218
|
+
if (!selectedServer || !selectedServerConfig)
|
|
219
|
+
return;
|
|
220
|
+
// Capture server name immediately to avoid closure issues
|
|
221
|
+
const serverName = selectedServer;
|
|
222
|
+
const serverConfig = selectedServerConfig;
|
|
223
|
+
// Clear all data when connecting/reconnecting to start fresh
|
|
224
|
+
clearHistory(serverName);
|
|
225
|
+
// Clear stderr logs BEFORE connecting
|
|
226
|
+
setServerStates(prev => ({
|
|
227
|
+
...prev,
|
|
228
|
+
[serverName]: {
|
|
229
|
+
...(prev[serverName] || {
|
|
230
|
+
status: 'disconnected',
|
|
231
|
+
error: null,
|
|
232
|
+
capabilities: {},
|
|
233
|
+
resources: [],
|
|
234
|
+
prompts: [],
|
|
235
|
+
tools: [],
|
|
236
|
+
}),
|
|
237
|
+
status: 'connecting',
|
|
238
|
+
stderrLogs: [], // Clear logs before connecting
|
|
239
|
+
},
|
|
240
|
+
}));
|
|
241
|
+
// Create the appropriate transport
|
|
242
|
+
const { transport: baseTransport, baseTransport: stdioTransport } = createTransport(serverConfig, serverName);
|
|
243
|
+
// Wrap with proxy transport if message tracking is enabled
|
|
244
|
+
const transport = messageTracking
|
|
245
|
+
? new LoggingProxyTransport(baseTransport, messageTracking)
|
|
246
|
+
: baseTransport;
|
|
247
|
+
const client = new MCPClient({
|
|
248
|
+
name: 'mcp-inspect',
|
|
249
|
+
version: '1.0.0',
|
|
250
|
+
}, {
|
|
251
|
+
capabilities: {},
|
|
252
|
+
});
|
|
253
|
+
try {
|
|
254
|
+
await client.connect(transport);
|
|
255
|
+
// Store client immediately
|
|
256
|
+
setServerClients(prev => ({ ...prev, [serverName]: client }));
|
|
257
|
+
// Get server capabilities
|
|
258
|
+
const serverCapabilities = client.getServerCapabilities() || {};
|
|
259
|
+
const capabilities = {
|
|
260
|
+
resources: !!serverCapabilities.resources,
|
|
261
|
+
prompts: !!serverCapabilities.prompts,
|
|
262
|
+
tools: !!serverCapabilities.tools,
|
|
263
|
+
};
|
|
264
|
+
// Get server info (name, version) and instructions
|
|
265
|
+
const serverVersion = client.getServerVersion();
|
|
266
|
+
const serverInfo = serverVersion ? {
|
|
267
|
+
name: serverVersion.name,
|
|
268
|
+
version: serverVersion.version,
|
|
269
|
+
} : undefined;
|
|
270
|
+
const instructions = client.getInstructions();
|
|
271
|
+
// Query resources, prompts, and tools based on capabilities
|
|
272
|
+
let resources = [];
|
|
273
|
+
let prompts = [];
|
|
274
|
+
let tools = [];
|
|
275
|
+
if (capabilities.resources) {
|
|
276
|
+
try {
|
|
277
|
+
const result = await client.listResources();
|
|
278
|
+
resources = result.resources || [];
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
// Ignore errors, just leave empty
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (capabilities.prompts) {
|
|
285
|
+
try {
|
|
286
|
+
const result = await client.listPrompts();
|
|
287
|
+
prompts = result.prompts || [];
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
// Ignore errors, just leave empty
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (capabilities.tools) {
|
|
294
|
+
try {
|
|
295
|
+
const result = await client.listTools();
|
|
296
|
+
tools = result.tools || [];
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
// Ignore errors, just leave empty
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Update server state - use captured serverName to ensure we update the correct server
|
|
303
|
+
// Preserve stderrLogs that were captured during connection (after we cleared them before connecting)
|
|
304
|
+
setServerStates(prev => ({
|
|
305
|
+
...prev,
|
|
306
|
+
[serverName]: {
|
|
307
|
+
status: 'connected',
|
|
308
|
+
error: null,
|
|
309
|
+
capabilities,
|
|
310
|
+
serverInfo,
|
|
311
|
+
instructions,
|
|
312
|
+
resources,
|
|
313
|
+
prompts,
|
|
314
|
+
tools,
|
|
315
|
+
stderrLogs: prev[serverName]?.stderrLogs || [], // Preserve logs captured during connection
|
|
316
|
+
},
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
// Make sure we clean up the client on error
|
|
321
|
+
try {
|
|
322
|
+
await client.close();
|
|
323
|
+
}
|
|
324
|
+
catch (closeErr) {
|
|
325
|
+
// Ignore close errors
|
|
326
|
+
}
|
|
327
|
+
setServerStates(prev => ({
|
|
328
|
+
...prev,
|
|
329
|
+
[serverName]: {
|
|
330
|
+
...prev[serverName] || {
|
|
331
|
+
status: 'disconnected',
|
|
332
|
+
error: null,
|
|
333
|
+
capabilities: {},
|
|
334
|
+
resources: [],
|
|
335
|
+
prompts: [],
|
|
336
|
+
tools: [],
|
|
337
|
+
},
|
|
338
|
+
status: 'error',
|
|
339
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
340
|
+
},
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
}, [selectedServer, selectedServerConfig, messageTracking]);
|
|
344
|
+
// Disconnect handler
|
|
345
|
+
const handleDisconnect = useCallback(async () => {
|
|
346
|
+
if (!selectedServer)
|
|
347
|
+
return;
|
|
348
|
+
await disconnectClient();
|
|
349
|
+
setServerClients(prev => {
|
|
350
|
+
const newClients = { ...prev };
|
|
351
|
+
delete newClients[selectedServer];
|
|
352
|
+
return newClients;
|
|
353
|
+
});
|
|
354
|
+
// Preserve all data when disconnecting - only change status
|
|
355
|
+
setServerStates(prev => ({
|
|
356
|
+
...prev,
|
|
357
|
+
[selectedServer]: {
|
|
358
|
+
...prev[selectedServer],
|
|
359
|
+
status: 'disconnected',
|
|
360
|
+
error: null,
|
|
361
|
+
// Keep all existing data: capabilities, serverInfo, instructions, resources, prompts, tools, stderrLogs
|
|
362
|
+
},
|
|
363
|
+
}));
|
|
364
|
+
// Update tab counts based on preserved data
|
|
365
|
+
const preservedState = serverStates[selectedServer];
|
|
366
|
+
if (preservedState) {
|
|
367
|
+
setTabCounts(prev => ({
|
|
368
|
+
...prev,
|
|
369
|
+
resources: preservedState.resources?.length || 0,
|
|
370
|
+
prompts: preservedState.prompts?.length || 0,
|
|
371
|
+
tools: preservedState.tools?.length || 0,
|
|
372
|
+
messages: messageHistory[selectedServer]?.length || 0,
|
|
373
|
+
logging: preservedState.stderrLogs?.length || 0,
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
}, [selectedServer, disconnectClient, serverStates, messageHistory]);
|
|
377
|
+
const currentServerMessages = useMemo(() => selectedServer ? (messageHistory[selectedServer] || []) : [], [selectedServer, messageHistory]);
|
|
378
|
+
const currentServerState = useMemo(() => selectedServer ? (serverStates[selectedServer] || null) : null, [selectedServer, serverStates]);
|
|
379
|
+
const currentServerClient = useMemo(() => selectedServer ? (serverClients[selectedServer] || null) : null, [selectedServer, serverClients]);
|
|
380
|
+
// Helper functions to render details modal content
|
|
381
|
+
const renderResourceDetails = (resource) => (_jsxs(_Fragment, { children: [resource.description && (_jsx(_Fragment, { children: resource.description.split('\n').map((line, idx) => (_jsx(Box, { marginTop: idx === 0 ? 0 : 0, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: line }) }, `desc-${idx}`))) })), resource.uri && (_jsxs(Box, { marginTop: 1, flexShrink: 0, children: [_jsx(Text, { bold: true, children: "URI:" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: resource.uri }) })] })), resource.mimeType && (_jsxs(Box, { marginTop: 1, flexShrink: 0, children: [_jsx(Text, { bold: true, children: "MIME Type:" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: resource.mimeType }) })] })), _jsxs(Box, { marginTop: 1, flexShrink: 0, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Full JSON:" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: JSON.stringify(resource, null, 2) }) })] })] }));
|
|
382
|
+
const renderPromptDetails = (prompt) => (_jsxs(_Fragment, { children: [prompt.description && (_jsx(_Fragment, { children: prompt.description.split('\n').map((line, idx) => (_jsx(Box, { marginTop: idx === 0 ? 0 : 0, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: line }) }, `desc-${idx}`))) })), prompt.arguments && prompt.arguments.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, flexShrink: 0, children: _jsx(Text, { bold: true, children: "Arguments:" }) }), prompt.arguments.map((arg, idx) => (_jsx(Box, { marginTop: 1, paddingLeft: 2, flexShrink: 0, children: _jsxs(Text, { dimColor: true, children: ["- ", arg.name, ": ", arg.description || arg.type || 'string'] }) }, `arg-${idx}`)))] })), _jsxs(Box, { marginTop: 1, flexShrink: 0, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Full JSON:" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: JSON.stringify(prompt, null, 2) }) })] })] }));
|
|
383
|
+
const renderToolDetails = (tool) => (_jsxs(_Fragment, { children: [tool.description && (_jsx(_Fragment, { children: tool.description.split('\n').map((line, idx) => (_jsx(Box, { marginTop: idx === 0 ? 0 : 0, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: line }) }, `desc-${idx}`))) })), tool.inputSchema && (_jsxs(Box, { marginTop: 1, flexShrink: 0, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Input Schema:" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: JSON.stringify(tool.inputSchema, null, 2) }) })] })), _jsxs(Box, { marginTop: 1, flexShrink: 0, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Full JSON:" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: JSON.stringify(tool, null, 2) }) })] })] }));
|
|
384
|
+
const renderMessageDetails = (message) => (_jsxs(_Fragment, { children: [_jsx(Box, { flexShrink: 0, children: _jsxs(Text, { bold: true, children: ["Direction: ", message.direction] }) }), message.duration !== undefined && (_jsx(Box, { marginTop: 1, flexShrink: 0, children: _jsxs(Text, { dimColor: true, children: ["Duration: ", message.duration, "ms"] }) })), message.direction === 'request' ? (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, flexShrink: 0, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Request:" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: JSON.stringify(message.message, null, 2) }) })] }), message.response && (_jsxs(Box, { marginTop: 1, flexShrink: 0, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Response:" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: JSON.stringify(message.response, null, 2) }) })] }))] })) : (_jsxs(Box, { marginTop: 1, flexShrink: 0, flexDirection: "column", children: [_jsx(Text, { bold: true, children: message.direction === 'response' ? 'Response:' : 'Notification:' }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: JSON.stringify(message.message, null, 2) }) })] }))] }));
|
|
385
|
+
// Update tab counts when selected server changes
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
if (!selectedServer) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const serverState = serverStates[selectedServer];
|
|
391
|
+
if (serverState?.status === 'connected') {
|
|
392
|
+
setTabCounts({
|
|
393
|
+
resources: serverState.resources?.length || 0,
|
|
394
|
+
prompts: serverState.prompts?.length || 0,
|
|
395
|
+
tools: serverState.tools?.length || 0,
|
|
396
|
+
messages: messageHistory[selectedServer]?.length || 0,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
else if (serverState?.status !== 'connecting') {
|
|
400
|
+
// Reset counts for disconnected or error states
|
|
401
|
+
setTabCounts({
|
|
402
|
+
resources: 0,
|
|
403
|
+
prompts: 0,
|
|
404
|
+
tools: 0,
|
|
405
|
+
messages: messageHistory[selectedServer]?.length || 0,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}, [selectedServer, serverStates, messageHistory]);
|
|
409
|
+
// Keep focus state consistent when switching tabs
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
if (activeTab === 'messages') {
|
|
412
|
+
if (focus === 'tabContentList' || focus === 'tabContentDetails') {
|
|
413
|
+
setFocus('messagesList');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
if (focus === 'messagesList' || focus === 'messagesDetail') {
|
|
418
|
+
setFocus('tabContentList');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}, [activeTab]); // intentionally not depending on focus to avoid loops
|
|
422
|
+
// Switch away from logging tab if server is not stdio
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
if (activeTab === 'logging' && selectedServerConfig) {
|
|
425
|
+
const serverType = getServerType(selectedServerConfig);
|
|
426
|
+
if (serverType !== 'stdio') {
|
|
427
|
+
setActiveTab('info');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}, [selectedServerConfig, activeTab, getServerType]);
|
|
431
|
+
useInput((input, key) => {
|
|
432
|
+
// Don't process input when modal is open
|
|
433
|
+
if (toolTestModal || detailsModal) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (key.ctrl && input === 'c') {
|
|
437
|
+
if (onExit) {
|
|
438
|
+
onExit();
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
process.exit();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// Exit accelerators
|
|
445
|
+
if (key.escape) {
|
|
446
|
+
if (onExit) {
|
|
447
|
+
onExit(0);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
process.exit(0);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Tab switching with accelerator keys (first character of tab name)
|
|
454
|
+
const tabAccelerators = Object.fromEntries(tabList.map((tab) => [tab.accelerator, tab.id]));
|
|
455
|
+
if (tabAccelerators[input.toLowerCase()]) {
|
|
456
|
+
setActiveTab(tabAccelerators[input.toLowerCase()]);
|
|
457
|
+
setFocus('tabs');
|
|
458
|
+
}
|
|
459
|
+
else if (key.tab && !key.shift) {
|
|
460
|
+
// Flat focus order: servers -> tabs -> list -> details -> wrap to servers
|
|
461
|
+
const focusOrder = activeTab === 'messages'
|
|
462
|
+
? ['serverList', 'tabs', 'messagesList', 'messagesDetail']
|
|
463
|
+
: ['serverList', 'tabs', 'tabContentList', 'tabContentDetails'];
|
|
464
|
+
const currentIndex = focusOrder.indexOf(focus);
|
|
465
|
+
const nextIndex = (currentIndex + 1) % focusOrder.length;
|
|
466
|
+
setFocus(focusOrder[nextIndex]);
|
|
467
|
+
}
|
|
468
|
+
else if (key.tab && key.shift) {
|
|
469
|
+
// Reverse order: servers <- tabs <- list <- details <- wrap to servers
|
|
470
|
+
const focusOrder = activeTab === 'messages'
|
|
471
|
+
? ['serverList', 'tabs', 'messagesList', 'messagesDetail']
|
|
472
|
+
: ['serverList', 'tabs', 'tabContentList', 'tabContentDetails'];
|
|
473
|
+
const currentIndex = focusOrder.indexOf(focus);
|
|
474
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : focusOrder.length - 1;
|
|
475
|
+
setFocus(focusOrder[prevIndex]);
|
|
476
|
+
}
|
|
477
|
+
else if (key.upArrow || key.downArrow) {
|
|
478
|
+
// Arrow keys only work in the focused pane
|
|
479
|
+
if (focus === 'serverList') {
|
|
480
|
+
// Arrow key navigation for server list
|
|
481
|
+
if (key.upArrow) {
|
|
482
|
+
if (selectedServer === null) {
|
|
483
|
+
setSelectedServer(serverNames[serverNames.length - 1] || null);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
const currentIndex = serverNames.indexOf(selectedServer);
|
|
487
|
+
const newIndex = currentIndex > 0 ? currentIndex - 1 : serverNames.length - 1;
|
|
488
|
+
setSelectedServer(serverNames[newIndex] || null);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else if (key.downArrow) {
|
|
492
|
+
if (selectedServer === null) {
|
|
493
|
+
setSelectedServer(serverNames[0] || null);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
const currentIndex = serverNames.indexOf(selectedServer);
|
|
497
|
+
const newIndex = currentIndex < serverNames.length - 1 ? currentIndex + 1 : 0;
|
|
498
|
+
setSelectedServer(serverNames[newIndex] || null);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return; // Handled, don't let other handlers process
|
|
502
|
+
}
|
|
503
|
+
// If focus is on tabs, tabContentList, tabContentDetails, messagesList, or messagesDetail,
|
|
504
|
+
// arrow keys will be handled by those components - don't do anything here
|
|
505
|
+
}
|
|
506
|
+
else if (focus === 'tabs' && (key.leftArrow || key.rightArrow)) {
|
|
507
|
+
// Left/Right arrows switch tabs when tabs are focused
|
|
508
|
+
const tabs = ['info', 'resources', 'prompts', 'tools', 'messages', 'logging'];
|
|
509
|
+
const currentIndex = tabs.indexOf(activeTab);
|
|
510
|
+
if (key.leftArrow) {
|
|
511
|
+
const newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
|
|
512
|
+
setActiveTab(tabs[newIndex]);
|
|
513
|
+
}
|
|
514
|
+
else if (key.rightArrow) {
|
|
515
|
+
const newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;
|
|
516
|
+
setActiveTab(tabs[newIndex]);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Accelerator keys for connect/disconnect (work from anywhere)
|
|
520
|
+
if (selectedServer) {
|
|
521
|
+
const serverState = serverStates[selectedServer];
|
|
522
|
+
if (input.toLowerCase() === 'c' && (serverState?.status === 'disconnected' || serverState?.status === 'error')) {
|
|
523
|
+
handleConnect();
|
|
524
|
+
}
|
|
525
|
+
else if (input.toLowerCase() === 'd' && (serverState?.status === 'connected' || serverState?.status === 'connecting')) {
|
|
526
|
+
handleDisconnect();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
// Calculate layout dimensions
|
|
531
|
+
const headerHeight = 1;
|
|
532
|
+
const tabsHeight = 1;
|
|
533
|
+
// Server details will be flexible - calculate remaining space for content
|
|
534
|
+
const availableHeight = dimensions.height - headerHeight - tabsHeight;
|
|
535
|
+
// Reserve space for server details (will grow as needed, but we'll use flexGrow)
|
|
536
|
+
const serverDetailsMinHeight = 3;
|
|
537
|
+
const contentHeight = availableHeight - serverDetailsMinHeight;
|
|
538
|
+
const serverListWidth = Math.floor(dimensions.width * 0.3);
|
|
539
|
+
const contentWidth = dimensions.width - serverListWidth;
|
|
540
|
+
const getStatusColor = (status) => {
|
|
541
|
+
switch (status) {
|
|
542
|
+
case 'connected':
|
|
543
|
+
return 'green';
|
|
544
|
+
case 'connecting':
|
|
545
|
+
return 'yellow';
|
|
546
|
+
case 'error':
|
|
547
|
+
return 'red';
|
|
548
|
+
default:
|
|
549
|
+
return 'gray';
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
const getStatusSymbol = (status) => {
|
|
553
|
+
switch (status) {
|
|
554
|
+
case 'connected':
|
|
555
|
+
return '●';
|
|
556
|
+
case 'connecting':
|
|
557
|
+
return '◐';
|
|
558
|
+
case 'error':
|
|
559
|
+
return '✗';
|
|
560
|
+
default:
|
|
561
|
+
return '○';
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
return (_jsxs(Box, { flexDirection: "column", width: dimensions.width, height: dimensions.height, children: [_jsxs(Box, { width: dimensions.width, height: headerHeight, borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, paddingX: 1, justifyContent: "space-between", alignItems: "center", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: packageJson.name }), _jsxs(Text, { dimColor: true, children: [" - ", packageJson.description] })] }), _jsxs(Text, { dimColor: true, children: ["v", packageJson.version] })] }), _jsxs(Box, { flexDirection: "row", height: availableHeight + tabsHeight, width: dimensions.width, children: [_jsxs(Box, { width: serverListWidth, height: availableHeight + tabsHeight, borderStyle: "single", borderTop: false, borderBottom: false, borderLeft: false, borderRight: true, flexDirection: "column", paddingX: 1, children: [_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { bold: true, backgroundColor: focus === 'serverList' ? 'yellow' : undefined, children: "MCP Servers" }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: serverNames.map((serverName) => {
|
|
565
|
+
const isSelected = selectedServer === serverName;
|
|
566
|
+
return (_jsx(Box, { paddingY: 0, children: _jsxs(Text, { children: [isSelected ? '▶ ' : ' ', serverName] }) }, serverName));
|
|
567
|
+
}) }), _jsx(Box, { flexShrink: 0, height: 1, justifyContent: "center", backgroundColor: "gray", children: _jsx(Text, { bold: true, color: "white", children: "ESC to exit" }) })] }), _jsxs(Box, { flexGrow: 1, height: availableHeight + tabsHeight, flexDirection: "column", children: [_jsx(Box, { width: contentWidth, borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderBottom: true, paddingX: 1, paddingY: 1, flexDirection: "column", flexShrink: 0, children: _jsx(Box, { flexDirection: "column", children: _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", alignItems: "center", children: [_jsx(Text, { bold: true, color: "cyan", children: selectedServer }), _jsx(Box, { flexDirection: "row", alignItems: "center", children: currentServerState && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: getStatusColor(currentServerState.status), children: [getStatusSymbol(currentServerState.status), " ", currentServerState.status] }), _jsx(Text, { children: " " }), (currentServerState?.status === 'disconnected' || currentServerState?.status === 'error') && (_jsxs(Text, { color: "cyan", bold: true, children: ["[", _jsx(Text, { underline: true, children: "C" }), "onnect]"] })), (currentServerState?.status === 'connected' || currentServerState?.status === 'connecting') && (_jsxs(Text, { color: "red", bold: true, children: ["[", _jsx(Text, { underline: true, children: "D" }), "isconnect]"] }))] })) })] }) }) }), _jsx(Tabs, { activeTab: activeTab, onTabChange: setActiveTab, width: contentWidth, counts: tabCounts, focused: focus === 'tabs', showLogging: selectedServerConfig ? getServerType(selectedServerConfig) === 'stdio' : false }), _jsxs(Box, { flexGrow: 1, width: contentWidth, borderTop: false, borderLeft: false, borderRight: false, borderBottom: false, children: [activeTab === 'info' && (_jsx(InfoTab, { serverName: selectedServer, serverConfig: selectedServerConfig, serverState: currentServerState, width: contentWidth, height: contentHeight, focused: focus === 'tabContentList' || focus === 'tabContentDetails' })), currentServerState?.status === 'connected' && currentServerClient ? (_jsxs(_Fragment, { children: [activeTab === 'resources' && (_jsx(ResourcesTab, { resources: currentServerState.resources, client: currentServerClient, width: contentWidth, height: contentHeight, onCountChange: (count) => setTabCounts(prev => ({ ...prev, resources: count })), focusedPane: focus === 'tabContentDetails' ? 'details' : focus === 'tabContentList' ? 'list' : null, onViewDetails: (resource) => setDetailsModal({
|
|
568
|
+
title: `Resource: ${resource.name || resource.uri || 'Unknown'}`,
|
|
569
|
+
content: renderResourceDetails(resource)
|
|
570
|
+
}) }, `resources-${selectedServer}`)), activeTab === 'prompts' && (_jsx(PromptsTab, { prompts: currentServerState.prompts, client: currentServerClient, width: contentWidth, height: contentHeight, onCountChange: (count) => setTabCounts(prev => ({ ...prev, prompts: count })), focusedPane: focus === 'tabContentDetails' ? 'details' : focus === 'tabContentList' ? 'list' : null, onViewDetails: (prompt) => setDetailsModal({
|
|
571
|
+
title: `Prompt: ${prompt.name || 'Unknown'}`,
|
|
572
|
+
content: renderPromptDetails(prompt)
|
|
573
|
+
}) }, `prompts-${selectedServer}`)), activeTab === 'tools' && (_jsx(ToolsTab, { tools: currentServerState.tools, client: currentServerClient, width: contentWidth, height: contentHeight, onCountChange: (count) => setTabCounts(prev => ({ ...prev, tools: count })), focusedPane: focus === 'tabContentDetails' ? 'details' : focus === 'tabContentList' ? 'list' : null, onTestTool: (tool) => setToolTestModal({ tool, client: currentServerClient }), onViewDetails: (tool) => setDetailsModal({
|
|
574
|
+
title: `Tool: ${tool.name || 'Unknown'}`,
|
|
575
|
+
content: renderToolDetails(tool)
|
|
576
|
+
}) }, `tools-${selectedServer}`)), activeTab === 'messages' && (_jsx(HistoryTab, { serverName: selectedServer, messages: currentServerMessages, width: contentWidth, height: contentHeight, onCountChange: (count) => setTabCounts(prev => ({ ...prev, messages: count })), focusedPane: focus === 'messagesDetail' ? 'details' : focus === 'messagesList' ? 'messages' : null, onViewDetails: (message) => {
|
|
577
|
+
const label = message.direction === 'request' && 'method' in message.message
|
|
578
|
+
? message.message.method
|
|
579
|
+
: message.direction === 'response'
|
|
580
|
+
? 'Response'
|
|
581
|
+
: message.direction === 'notification' && 'method' in message.message
|
|
582
|
+
? message.message.method
|
|
583
|
+
: 'Message';
|
|
584
|
+
setDetailsModal({
|
|
585
|
+
title: `Message: ${label}`,
|
|
586
|
+
content: renderMessageDetails(message)
|
|
587
|
+
});
|
|
588
|
+
} })), activeTab === 'logging' && (_jsx(NotificationsTab, { client: currentServerClient, stderrLogs: currentServerState?.stderrLogs || [], width: contentWidth, height: contentHeight, onCountChange: (count) => setTabCounts(prev => ({ ...prev, logging: count })), focused: focus === 'tabContentList' || focus === 'tabContentDetails' }))] })) : activeTab !== 'info' && selectedServer ? (_jsx(Box, { paddingX: 1, paddingY: 1, children: _jsx(Text, { dimColor: true, children: "Server not connected" }) })) : null] })] })] }), toolTestModal && (_jsx(ToolTestModal, { tool: toolTestModal.tool, client: toolTestModal.client, width: dimensions.width, height: dimensions.height, onClose: () => setToolTestModal(null) })), detailsModal && (_jsx(DetailsModal, { title: detailsModal.title, content: detailsModal.content, width: dimensions.width, height: dimensions.height, onClose: () => setDetailsModal(null) }))] }));
|
|
589
|
+
}
|
|
590
|
+
export default App;
|
|
591
|
+
//# sourceMappingURL=App.js.map
|