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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/dist/src/App.d.ts +7 -0
  4. package/dist/src/App.d.ts.map +1 -0
  5. package/dist/src/App.js +591 -0
  6. package/dist/src/App.js.map +1 -0
  7. package/dist/src/components/DetailsModal.d.ts +11 -0
  8. package/dist/src/components/DetailsModal.d.ts.map +1 -0
  9. package/dist/src/components/DetailsModal.js +50 -0
  10. package/dist/src/components/DetailsModal.js.map +1 -0
  11. package/dist/src/components/HistoryTab.d.ts +13 -0
  12. package/dist/src/components/HistoryTab.d.ts.map +1 -0
  13. package/dist/src/components/HistoryTab.js +122 -0
  14. package/dist/src/components/HistoryTab.js.map +1 -0
  15. package/dist/src/components/InfoTab.d.ts +13 -0
  16. package/dist/src/components/InfoTab.d.ts.map +1 -0
  17. package/dist/src/components/InfoTab.js +28 -0
  18. package/dist/src/components/InfoTab.js.map +1 -0
  19. package/dist/src/components/NotificationsTab.d.ts +13 -0
  20. package/dist/src/components/NotificationsTab.d.ts.map +1 -0
  21. package/dist/src/components/NotificationsTab.js +37 -0
  22. package/dist/src/components/NotificationsTab.js.map +1 -0
  23. package/dist/src/components/PromptsTab.d.ts +13 -0
  24. package/dist/src/components/PromptsTab.d.ts.map +1 -0
  25. package/dist/src/components/PromptsTab.js +60 -0
  26. package/dist/src/components/PromptsTab.js.map +1 -0
  27. package/dist/src/components/ResourcesTab.d.ts +13 -0
  28. package/dist/src/components/ResourcesTab.d.ts.map +1 -0
  29. package/dist/src/components/ResourcesTab.js +60 -0
  30. package/dist/src/components/ResourcesTab.js.map +1 -0
  31. package/dist/src/components/Tabs.d.ts +24 -0
  32. package/dist/src/components/Tabs.d.ts.map +1 -0
  33. package/dist/src/components/Tabs.js +22 -0
  34. package/dist/src/components/Tabs.js.map +1 -0
  35. package/dist/src/components/ToolTestModal.d.ts +11 -0
  36. package/dist/src/components/ToolTestModal.d.ts.map +1 -0
  37. package/dist/src/components/ToolTestModal.js +112 -0
  38. package/dist/src/components/ToolTestModal.js.map +1 -0
  39. package/dist/src/components/ToolsTab.d.ts +14 -0
  40. package/dist/src/components/ToolsTab.d.ts.map +1 -0
  41. package/dist/src/components/ToolsTab.js +76 -0
  42. package/dist/src/components/ToolsTab.js.map +1 -0
  43. package/dist/src/hooks/useMCPClient.d.ts +41 -0
  44. package/dist/src/hooks/useMCPClient.d.ts.map +1 -0
  45. package/dist/src/hooks/useMCPClient.js +179 -0
  46. package/dist/src/hooks/useMCPClient.js.map +1 -0
  47. package/dist/src/hooks/useMessageTracking.d.ts +9 -0
  48. package/dist/src/hooks/useMessageTracking.d.ts.map +1 -0
  49. package/dist/src/hooks/useMessageTracking.js +124 -0
  50. package/dist/src/hooks/useMessageTracking.js.map +1 -0
  51. package/dist/src/types/focus.d.ts +2 -0
  52. package/dist/src/types/focus.d.ts.map +1 -0
  53. package/dist/src/types/focus.js +2 -0
  54. package/dist/src/types/focus.js.map +1 -0
  55. package/dist/src/types/messages.d.ts +14 -0
  56. package/dist/src/types/messages.d.ts.map +1 -0
  57. package/dist/src/types/messages.js +2 -0
  58. package/dist/src/types/messages.js.map +1 -0
  59. package/dist/src/types.d.ts +48 -0
  60. package/dist/src/types.d.ts.map +1 -0
  61. package/dist/src/types.js +2 -0
  62. package/dist/src/types.js.map +1 -0
  63. package/dist/src/utils/schemaToForm.d.ts +9 -0
  64. package/dist/src/utils/schemaToForm.d.ts.map +1 -0
  65. package/dist/src/utils/schemaToForm.js +107 -0
  66. package/dist/src/utils/schemaToForm.js.map +1 -0
  67. package/dist/tui.d.ts +3 -0
  68. package/dist/tui.d.ts.map +1 -0
  69. package/dist/tui.js +43 -0
  70. package/dist/tui.js.map +1 -0
  71. package/package.json +58 -0
  72. package/screenshots/mcp-inspector.png +0 -0
@@ -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