mcp-pentester-cli 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 (40) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +327 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +183 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/mcp-client.d.ts +23 -0
  8. package/dist/mcp-client.d.ts.map +1 -0
  9. package/dist/mcp-client.js +163 -0
  10. package/dist/mcp-client.js.map +1 -0
  11. package/dist/transport/base.d.ts +18 -0
  12. package/dist/transport/base.d.ts.map +1 -0
  13. package/dist/transport/base.js +64 -0
  14. package/dist/transport/base.js.map +1 -0
  15. package/dist/transport/http.d.ts +14 -0
  16. package/dist/transport/http.d.ts.map +1 -0
  17. package/dist/transport/http.js +137 -0
  18. package/dist/transport/http.js.map +1 -0
  19. package/dist/transport/stdio.d.ts +15 -0
  20. package/dist/transport/stdio.d.ts.map +1 -0
  21. package/dist/transport/stdio.js +89 -0
  22. package/dist/transport/stdio.js.map +1 -0
  23. package/dist/transport/websocket.d.ts +15 -0
  24. package/dist/transport/websocket.d.ts.map +1 -0
  25. package/dist/transport/websocket.js +109 -0
  26. package/dist/transport/websocket.js.map +1 -0
  27. package/dist/types.d.ts +103 -0
  28. package/dist/types.d.ts.map +1 -0
  29. package/dist/types.js +3 -0
  30. package/dist/types.js.map +1 -0
  31. package/dist/ui/tui.d.ts +43 -0
  32. package/dist/ui/tui.d.ts.map +1 -0
  33. package/dist/ui/tui.js +872 -0
  34. package/dist/ui/tui.js.map +1 -0
  35. package/examples/http-burp-config.json +9 -0
  36. package/examples/https-burp-config.json +13 -0
  37. package/examples/stdio-config.json +10 -0
  38. package/examples/tor-config.json +9 -0
  39. package/examples/websocket-config.json +9 -0
  40. package/package.json +44 -0
package/dist/ui/tui.js ADDED
@@ -0,0 +1,872 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TUI = void 0;
7
+ const blessed_1 = __importDefault(require("blessed"));
8
+ class TUI {
9
+ constructor() {
10
+ this.currentView = 'tools';
11
+ this.trafficLines = [];
12
+ this.trafficPairs = [];
13
+ this.activePanel = 'sidebar';
14
+ this.pendingRequests = new Map(); // Track requests waiting for responses
15
+ this.currentPopupContent = null;
16
+ this.showRawInPopup = false;
17
+ this.screen = blessed_1.default.screen({
18
+ smartCSR: true,
19
+ title: 'MCP Pentester CLI - IntegSec',
20
+ });
21
+ this.layout = this.createLayout();
22
+ this.setupKeyBindings();
23
+ }
24
+ showSplashScreen() {
25
+ const splash = blessed_1.default.box({
26
+ parent: this.screen,
27
+ top: 'center',
28
+ left: 'center',
29
+ width: 70,
30
+ height: 18,
31
+ border: { type: 'line' },
32
+ style: {
33
+ border: { fg: 'cyan', bold: true },
34
+ },
35
+ content: `{center}{cyan-fg}{bold}
36
+ ██▓ ███▄ █ ▄▄▄█████▓▓█████ ▄████ ██████ ▓█████ ▄████▄
37
+ ▓██▒ ██ ▀█ █ ▓ ██▒ ▓▒▓█ ▀ ██▒ ▀█▒▒██ ▒ ▓█ ▀ ▒██▀ ▀█
38
+ ▒██▒▓██ ▀█ ██▒▒ ▓██░ ▒░▒███ ▒██░▄▄▄░░ ▓██▄ ▒███ ▒▓█ ▄
39
+ ░██░▓██▒ ▐▌██▒░ ▓██▓ ░ ▒▓█ ▄ ░▓█ ██▓ ▒ ██▒▒▓█ ▄ ▒▓▓▄ ▄██▒
40
+ ░██░▒██░ ▓██░ ▒██▒ ░ ░▒████▒░▒▓███▀▒▒██████▒▒░▒████▒▒ ▓███▀ ░
41
+ ░▓ ░ ▒░ ▒ ▒ ▒ ░░ ░░ ▒░ ░ ░▒ ▒ ▒ ▒▓▒ ▒ ░░░ ▒░ ░░ ░▒ ▒ ░
42
+ ▒ ░░ ░░ ░ ▒░ ░ ░ ░ ░ ░ ░ ░ ░▒ ░ ░ ░ ░ ░ ░ ▒
43
+ ▒ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
44
+ ░ ░ ░ ░ ░ ░ ░ ░░ ░
45
+ {/bold}{/cyan-fg}
46
+ {bold}{yellow-fg}MCP Pentester CLI v1.0.0{/yellow-fg}{/bold}
47
+ {green-fg}{bold}integsec.com{/bold}{/green-fg} {gray-fg}|{/gray-fg} {white-fg}Security Testing{/white-fg}
48
+ {gray-fg}© 2025 IntegSec - All Rights Reserved{/gray-fg}
49
+ {green-fg}Press any key (auto-closes in 2s)...{/green-fg}{/center}`,
50
+ tags: true,
51
+ });
52
+ this.screen.render();
53
+ const closeHandler = () => {
54
+ if (!splash.detached) {
55
+ splash.destroy();
56
+ this.screen.render();
57
+ }
58
+ };
59
+ // Auto-close after 2 seconds
60
+ setTimeout(closeHandler, 2000);
61
+ // Or close on any keypress
62
+ this.screen.once('keypress', closeHandler);
63
+ }
64
+ createLayout() {
65
+ // IntegSec Logo (top right)
66
+ const logo = blessed_1.default.box({
67
+ parent: this.screen,
68
+ top: 0,
69
+ right: 0,
70
+ width: 20,
71
+ height: 5,
72
+ tags: true,
73
+ content: '{right}{cyan-fg}{bold}IntegSec{/bold}\n{gray-fg}Security\nTesting{/gray-fg}{/cyan-fg}{/right}',
74
+ style: {
75
+ fg: 'cyan',
76
+ },
77
+ });
78
+ // Sidebar (left panel) - Navigation
79
+ const sidebar = blessed_1.default.list({
80
+ parent: this.screen,
81
+ label: ' Navigation ',
82
+ tags: true,
83
+ top: 0,
84
+ left: 0,
85
+ width: '20%',
86
+ height: '70%',
87
+ border: { type: 'line' },
88
+ style: {
89
+ border: { fg: 'cyan' },
90
+ selected: { bg: 'blue', fg: 'white' },
91
+ item: { fg: 'white' },
92
+ },
93
+ keys: true,
94
+ vi: true,
95
+ mouse: true,
96
+ items: [
97
+ '{cyan-fg}Tools{/cyan-fg}',
98
+ '{cyan-fg}Resources{/cyan-fg}',
99
+ '{cyan-fg}Prompts{/cyan-fg}',
100
+ '{cyan-fg}Traffic Log{/cyan-fg}',
101
+ ],
102
+ });
103
+ // Main panel (center) - Content display (using list for interactivity)
104
+ const main = blessed_1.default.list({
105
+ parent: this.screen,
106
+ label: ' Content ',
107
+ tags: true,
108
+ top: 0,
109
+ left: '20%',
110
+ width: '80%',
111
+ height: '70%',
112
+ border: { type: 'line' },
113
+ style: {
114
+ border: { fg: 'cyan' },
115
+ selected: { bg: 'blue', fg: 'white' },
116
+ item: { fg: 'white' },
117
+ },
118
+ scrollable: true,
119
+ alwaysScroll: true,
120
+ keys: true,
121
+ vi: true,
122
+ mouse: true,
123
+ scrollbar: {
124
+ ch: ' ',
125
+ style: { bg: 'cyan' },
126
+ },
127
+ interactive: true,
128
+ });
129
+ // Status bar
130
+ const status = blessed_1.default.box({
131
+ parent: this.screen,
132
+ top: '70%',
133
+ left: 0,
134
+ width: '100%',
135
+ height: 3,
136
+ tags: true,
137
+ border: { type: 'line' },
138
+ style: {
139
+ border: { fg: 'yellow' },
140
+ },
141
+ content: '{yellow-fg}Status:{/yellow-fg} Disconnected',
142
+ });
143
+ // Traffic log (bottom panel)
144
+ const traffic = blessed_1.default.box({
145
+ parent: this.screen,
146
+ label: ' Traffic Log ',
147
+ tags: true,
148
+ top: '73%',
149
+ left: 0,
150
+ width: '100%',
151
+ height: '24%',
152
+ border: { type: 'line' },
153
+ style: {
154
+ border: { fg: 'green' },
155
+ },
156
+ scrollable: true,
157
+ alwaysScroll: true,
158
+ keys: true,
159
+ vi: true,
160
+ mouse: true,
161
+ scrollbar: {
162
+ ch: ' ',
163
+ style: { bg: 'green' },
164
+ },
165
+ content: '',
166
+ });
167
+ // Input box (hidden by default)
168
+ const input = blessed_1.default.textbox({
169
+ parent: this.screen,
170
+ label: ' Input ',
171
+ top: 'center',
172
+ left: 'center',
173
+ width: '60%',
174
+ height: 3,
175
+ border: { type: 'line' },
176
+ style: {
177
+ border: { fg: 'magenta' },
178
+ focus: { border: { fg: 'blue' } },
179
+ },
180
+ hidden: true,
181
+ inputOnFocus: true,
182
+ keys: true,
183
+ mouse: true,
184
+ });
185
+ return { sidebar, main, status, traffic, input };
186
+ }
187
+ setupKeyBindings() {
188
+ // F10 - Quit application
189
+ this.screen.key(['f10', 'C-c'], () => {
190
+ return process.exit(0);
191
+ });
192
+ // Navigate sidebar
193
+ this.layout.sidebar.on('select', (item, index) => {
194
+ switch (index) {
195
+ case 0:
196
+ this.showTools();
197
+ break;
198
+ case 1:
199
+ this.showResources();
200
+ break;
201
+ case 2:
202
+ this.showPrompts();
203
+ break;
204
+ case 3:
205
+ this.showTrafficLog();
206
+ break;
207
+ }
208
+ // Auto-focus the content pane after selecting from sidebar
209
+ this.activePanel = 'main';
210
+ this.updatePanelHighlight();
211
+ this.layout.main.focus();
212
+ this.screen.render();
213
+ });
214
+ // F5 - Refresh current view
215
+ this.screen.key(['f5'], async () => {
216
+ if (this.client) {
217
+ await this.client.refreshAll();
218
+ this.updateCurrentView();
219
+ }
220
+ });
221
+ // F1 - Focus sidebar
222
+ this.screen.key(['f1'], () => {
223
+ this.activePanel = 'sidebar';
224
+ this.updatePanelHighlight();
225
+ this.layout.sidebar.focus();
226
+ this.screen.render();
227
+ });
228
+ // F2 - Focus main panel
229
+ this.screen.key(['f2'], () => {
230
+ this.activePanel = 'main';
231
+ this.updatePanelHighlight();
232
+ this.layout.main.focus();
233
+ this.screen.render();
234
+ });
235
+ // F3 - Focus traffic panel
236
+ this.screen.key(['f3'], () => {
237
+ this.activePanel = 'traffic';
238
+ this.updatePanelHighlight();
239
+ this.layout.traffic.focus();
240
+ this.screen.render();
241
+ });
242
+ // Handle selection in main panel
243
+ this.layout.main.key(['enter'], async () => {
244
+ await this.handleMainSelection();
245
+ });
246
+ // Clear traffic log with 'c' when focused
247
+ this.layout.traffic.key(['c'], () => {
248
+ if (this.client) {
249
+ this.client.clearTrafficLog();
250
+ this.trafficLines = [];
251
+ this.layout.traffic.setContent('');
252
+ this.screen.render();
253
+ }
254
+ });
255
+ }
256
+ setClient(client) {
257
+ this.client = client;
258
+ // Set up event handlers
259
+ client.on('connected', (result) => {
260
+ const serverName = result.serverInfo?.name || 'Unknown';
261
+ const serverVersion = result.serverInfo?.version || '?';
262
+ this.layout.status.setContent(`{green-fg}Connected{/green-fg} to ${this.escapeBlessedTags(serverName)} v${serverVersion} | ` +
263
+ `{cyan-fg}F1{/cyan-fg}=Nav {cyan-fg}F2{/cyan-fg}=Content {cyan-fg}F3{/cyan-fg}=Traffic {cyan-fg}F4{/cyan-fg}=Close {cyan-fg}F5{/cyan-fg}=Refresh {cyan-fg}F10{/cyan-fg}=Quit | ` +
264
+ `{bold}{cyan-fg}IntegSec{/cyan-fg}{/bold} {gray-fg}({green-fg}integsec.com{/green-fg}) - Need pentesting? Contact us!{/gray-fg}`);
265
+ this.updateCurrentView();
266
+ this.screen.render();
267
+ });
268
+ client.on('disconnected', () => {
269
+ this.layout.status.setContent('{red-fg}Status:{/red-fg} Disconnected');
270
+ this.screen.render();
271
+ });
272
+ client.on('error', (error) => {
273
+ this.addTrafficLine(`{red-fg}ERROR:{/red-fg} ${error.message}`);
274
+ this.screen.render();
275
+ });
276
+ client.on('traffic', ({ direction, data }) => {
277
+ const timestamp = new Date().toISOString().substr(11, 12);
278
+ if (direction === 'sent' && 'method' in data) {
279
+ // This is a request
280
+ const method = data.method;
281
+ let details = '';
282
+ if (data.method === 'tools/call' && data.params) {
283
+ details = ` tool=${data.params.name}`;
284
+ if (data.params.arguments) {
285
+ const argKeys = Object.keys(data.params.arguments);
286
+ if (argKeys.length > 0) {
287
+ details += ` args=${argKeys.join(',')}`;
288
+ }
289
+ }
290
+ }
291
+ else if (data.method === 'resources/read' && data.params) {
292
+ details = ` uri=${data.params.uri}`;
293
+ }
294
+ else if (data.method === 'prompts/get' && data.params) {
295
+ details = ` prompt=${data.params.name}`;
296
+ }
297
+ const requestLine = `{cyan-fg}[${timestamp}]{/cyan-fg} {yellow-fg}>>>{/yellow-fg} ${this.escapeBlessedTags(method + details)}`;
298
+ if ('id' in data && data.id !== undefined) {
299
+ // Store request, waiting for response - store full data for detail view
300
+ this.pendingRequests.set(data.id, { line: requestLine, data, timestamp: new Date() });
301
+ }
302
+ else {
303
+ // Notification (no response expected)
304
+ this.addTrafficLine(requestLine);
305
+ }
306
+ }
307
+ else if (direction === 'received') {
308
+ // This is a response
309
+ const responseId = ('id' in data) ? data.id : null;
310
+ let responseLine = '';
311
+ if ('error' in data && data.error) {
312
+ const errorMsg = this.escapeBlessedTags(String(data.error.message || 'Unknown error'));
313
+ responseLine = `{cyan-fg}[${timestamp}]{/cyan-fg} {red-fg}<<<{/red-fg} ERROR: ${errorMsg}`;
314
+ }
315
+ else {
316
+ const resultPreview = ('result' in data && data.result)
317
+ ? this.formatResultPreview(data.result)
318
+ : 'OK';
319
+ responseLine = `{cyan-fg}[${timestamp}]{/cyan-fg} {green-fg}<<<{/green-fg} ${resultPreview}`;
320
+ }
321
+ if (responseId !== null && this.pendingRequests.has(responseId)) {
322
+ // Found matching request - show summary in log, store full data for detail view
323
+ const { line: requestLine, data: requestData, timestamp: reqTime } = this.pendingRequests.get(responseId);
324
+ this.pendingRequests.delete(responseId);
325
+ // Add summary to traffic log
326
+ this.addTrafficPair(requestLine, responseLine);
327
+ // Store full data for detail view
328
+ this.trafficPairs.unshift({ request: requestData, response: data, timestamp: reqTime });
329
+ if (this.trafficPairs.length > 50) {
330
+ this.trafficPairs = this.trafficPairs.slice(0, 50);
331
+ }
332
+ }
333
+ else {
334
+ // No matching request or notification
335
+ this.addTrafficLine(responseLine);
336
+ }
337
+ }
338
+ this.screen.render();
339
+ });
340
+ }
341
+ updatePanelHighlight() {
342
+ // Reset all borders to normal
343
+ this.layout.sidebar.style.border = { fg: 'cyan' };
344
+ this.layout.main.style.border = { fg: 'cyan' };
345
+ this.layout.traffic.style.border = { fg: 'green' };
346
+ // Highlight active panel with bold yellow border
347
+ switch (this.activePanel) {
348
+ case 'sidebar':
349
+ this.layout.sidebar.style.border = { fg: 'yellow', bold: true };
350
+ break;
351
+ case 'main':
352
+ this.layout.main.style.border = { fg: 'yellow', bold: true };
353
+ break;
354
+ case 'traffic':
355
+ this.layout.traffic.style.border = { fg: 'yellow', bold: true };
356
+ break;
357
+ }
358
+ }
359
+ showTools() {
360
+ this.currentView = 'tools';
361
+ this.updateCurrentView();
362
+ }
363
+ showResources() {
364
+ this.currentView = 'resources';
365
+ this.updateCurrentView();
366
+ }
367
+ showPrompts() {
368
+ this.currentView = 'prompts';
369
+ this.updateCurrentView();
370
+ }
371
+ showTrafficLog() {
372
+ this.currentView = 'traffic';
373
+ this.updateCurrentView();
374
+ }
375
+ updateCurrentView() {
376
+ if (!this.client)
377
+ return;
378
+ const state = this.client.getState();
379
+ switch (this.currentView) {
380
+ case 'tools':
381
+ this.displayTools(state.tools);
382
+ break;
383
+ case 'resources':
384
+ this.displayResources(state.resources);
385
+ break;
386
+ case 'prompts':
387
+ this.displayPrompts(state.prompts);
388
+ break;
389
+ case 'traffic':
390
+ this.displayTraffic();
391
+ break;
392
+ }
393
+ }
394
+ displayTools(tools) {
395
+ this.layout.main.setLabel(` Tools (${tools.length}) - Press Enter to execute `);
396
+ if (tools.length === 0) {
397
+ this.layout.main.setItems(['{yellow-fg}No tools available - Press F5 to refresh{/yellow-fg}']);
398
+ }
399
+ else {
400
+ const items = tools.map((tool, idx) => {
401
+ let label = `${idx + 1}. {bold}${tool.name}{/bold}`;
402
+ if (tool.description) {
403
+ label += ` - ${tool.description}`;
404
+ }
405
+ return label;
406
+ });
407
+ this.layout.main.setItems(items);
408
+ }
409
+ this.screen.render();
410
+ }
411
+ displayResources(resources) {
412
+ this.layout.main.setLabel(` Resources (${resources.length}) - Press Enter to read `);
413
+ if (resources.length === 0) {
414
+ this.layout.main.setItems(['{yellow-fg}No resources available - Press F5 to refresh{/yellow-fg}']);
415
+ }
416
+ else {
417
+ const items = resources.map((resource, idx) => {
418
+ let label = `${idx + 1}. {bold}${resource.name}{/bold} - {gray-fg}${resource.uri}{/gray-fg}`;
419
+ if (resource.description) {
420
+ label += ` - ${resource.description}`;
421
+ }
422
+ return label;
423
+ });
424
+ this.layout.main.setItems(items);
425
+ }
426
+ this.screen.render();
427
+ }
428
+ displayPrompts(prompts) {
429
+ this.layout.main.setLabel(` Prompts (${prompts.length}) - Press Enter to use `);
430
+ if (prompts.length === 0) {
431
+ this.layout.main.setItems(['{yellow-fg}No prompts available - Press F5 to refresh{/yellow-fg}']);
432
+ }
433
+ else {
434
+ const items = prompts.map((prompt, idx) => {
435
+ let label = `${idx + 1}. {bold}${prompt.name}{/bold}`;
436
+ if (prompt.description) {
437
+ label += ` - ${prompt.description}`;
438
+ }
439
+ return label;
440
+ });
441
+ this.layout.main.setItems(items);
442
+ }
443
+ this.screen.render();
444
+ }
445
+ displayTraffic() {
446
+ if (!this.client)
447
+ return;
448
+ const logs = this.client.getTrafficLog();
449
+ this.layout.main.setLabel(` Traffic Details (${logs.length}) - Full Request/Response `);
450
+ if (logs.length === 0) {
451
+ this.layout.main.setItems(['{yellow-fg}No traffic logged yet{/yellow-fg}']);
452
+ }
453
+ else {
454
+ // Build full request/response pairs with details
455
+ const items = [];
456
+ const processedIds = new Set();
457
+ // Process in reverse order (most recent first)
458
+ for (let i = 0; i < Math.min(logs.length, 20); i++) {
459
+ const log = logs[i];
460
+ // Skip if we already processed this request/response pair
461
+ if ('id' in log.data) {
462
+ const logId = log.data.id;
463
+ if (processedIds.has(logId))
464
+ continue;
465
+ processedIds.add(logId);
466
+ }
467
+ // Find matching request/response pair
468
+ let requestLog = log;
469
+ let responseLog = null;
470
+ if (log.direction === 'received' && 'id' in log.data) {
471
+ const requestId = log.data.id;
472
+ // Find the request
473
+ for (let j = i + 1; j < logs.length; j++) {
474
+ if (logs[j].direction === 'sent' && 'id' in logs[j].data &&
475
+ logs[j].data.id === requestId) {
476
+ requestLog = logs[j];
477
+ responseLog = log;
478
+ break;
479
+ }
480
+ }
481
+ }
482
+ else if (log.direction === 'sent' && 'id' in log.data) {
483
+ const requestId = log.data.id;
484
+ // Find the response
485
+ for (let j = i - 1; j >= 0; j--) {
486
+ if (logs[j].direction === 'received' && 'id' in logs[j].data &&
487
+ logs[j].data.id === requestId) {
488
+ responseLog = logs[j];
489
+ break;
490
+ }
491
+ }
492
+ }
493
+ // Format as compact single-line entry with timestamp and method
494
+ const timestamp = requestLog.timestamp.toISOString().substr(11, 12);
495
+ const method = ('method' in requestLog.data) ? requestLog.data.method : 'unknown';
496
+ const respStatus = responseLog
497
+ ? ('error' in responseLog.data ? '{red-fg}ERROR{/red-fg}' : '{green-fg}OK{/green-fg}')
498
+ : '{gray-fg}pending{/gray-fg}';
499
+ items.push(`{gray-fg}[${timestamp}]{/gray-fg} {yellow-fg}${this.escapeBlessedTags(method)}{/yellow-fg} → ${respStatus}`);
500
+ }
501
+ this.layout.main.setItems(items);
502
+ }
503
+ this.screen.render();
504
+ }
505
+ escapeBlessedTags(text) {
506
+ // Escape curly braces so blessed doesn't try to parse them as tags
507
+ return text.replace(/{/g, '\\{').replace(/}/g, '\\}');
508
+ }
509
+ formatResultPreview(result) {
510
+ let preview;
511
+ if (typeof result === 'string') {
512
+ preview = result.length > 50 ? result.substring(0, 47) + '...' : result;
513
+ }
514
+ else if (Array.isArray(result)) {
515
+ preview = `Array[${result.length}]`;
516
+ }
517
+ else if (typeof result === 'object' && result !== null) {
518
+ const keys = Object.keys(result);
519
+ if (keys.length === 0)
520
+ return '{}';
521
+ preview = `{${keys.slice(0, 3).join(',')}}`;
522
+ }
523
+ else {
524
+ preview = String(result);
525
+ }
526
+ return this.escapeBlessedTags(preview);
527
+ }
528
+ addTrafficPair(requestLine, responseLine) {
529
+ // Add simple summary lines to the small traffic log
530
+ this.trafficLines.unshift(responseLine);
531
+ this.trafficLines.unshift(requestLine);
532
+ // Keep only last 100 lines
533
+ if (this.trafficLines.length > 100) {
534
+ this.trafficLines = this.trafficLines.slice(0, 100);
535
+ }
536
+ // Update the traffic box content
537
+ this.layout.traffic.setContent(this.trafficLines.join('\n'));
538
+ this.layout.traffic.setScrollPerc(0);
539
+ }
540
+ addTrafficLine(line) {
541
+ this.trafficLines.unshift(line);
542
+ if (this.trafficLines.length > 100) {
543
+ this.trafficLines = this.trafficLines.slice(0, 100);
544
+ }
545
+ this.layout.traffic.setContent(this.trafficLines.join('\n'));
546
+ }
547
+ render() {
548
+ // Set up the main UI first
549
+ this.activePanel = 'sidebar';
550
+ this.updatePanelHighlight();
551
+ this.layout.sidebar.focus();
552
+ this.screen.render();
553
+ // Show splash screen as overlay (auto-closes)
554
+ this.showSplashScreen();
555
+ }
556
+ async prompt(label) {
557
+ return new Promise((resolve) => {
558
+ this.layout.input.setLabel(` ${label} `);
559
+ this.layout.input.show();
560
+ this.layout.input.focus();
561
+ this.layout.input.on('submit', (value) => {
562
+ this.layout.input.hide();
563
+ this.layout.input.clearValue();
564
+ this.layout.sidebar.focus();
565
+ this.screen.render();
566
+ resolve(value || '');
567
+ });
568
+ this.layout.input.on('cancel', () => {
569
+ this.layout.input.hide();
570
+ this.layout.input.clearValue();
571
+ this.layout.sidebar.focus();
572
+ this.screen.render();
573
+ resolve('');
574
+ });
575
+ this.screen.render();
576
+ });
577
+ }
578
+ async handleMainSelection() {
579
+ if (!this.client)
580
+ return;
581
+ const selectedIndex = this.layout.main.selected || 0;
582
+ const state = this.client.getState();
583
+ try {
584
+ switch (this.currentView) {
585
+ case 'tools':
586
+ if (selectedIndex < state.tools.length) {
587
+ await this.executeTool(state.tools[selectedIndex]);
588
+ }
589
+ break;
590
+ case 'resources':
591
+ if (selectedIndex < state.resources.length) {
592
+ await this.readResource(state.resources[selectedIndex]);
593
+ }
594
+ break;
595
+ case 'prompts':
596
+ if (selectedIndex < state.prompts.length) {
597
+ await this.usePrompt(state.prompts[selectedIndex]);
598
+ }
599
+ break;
600
+ case 'traffic':
601
+ const logs = this.client.getTrafficLog().slice(-50);
602
+ if (selectedIndex < logs.length) {
603
+ await this.showTrafficDetail(logs[selectedIndex]);
604
+ }
605
+ break;
606
+ }
607
+ }
608
+ catch (error) {
609
+ this.showMessage('Error', error.message || String(error), 'red');
610
+ }
611
+ }
612
+ async executeTool(tool) {
613
+ // Build arguments
614
+ const args = {};
615
+ if (tool.inputSchema?.properties) {
616
+ const props = tool.inputSchema.properties;
617
+ const required = tool.inputSchema.required || [];
618
+ for (const paramName of Object.keys(props)) {
619
+ const isRequired = required.includes(paramName);
620
+ const prompt = `${paramName}${isRequired ? ' (required)' : ' (optional)'}:`;
621
+ const value = await this.prompt(prompt);
622
+ if (value || isRequired) {
623
+ // Try to parse as JSON for complex types
624
+ try {
625
+ args[paramName] = JSON.parse(value);
626
+ }
627
+ catch {
628
+ args[paramName] = value;
629
+ }
630
+ }
631
+ }
632
+ }
633
+ const result = await this.client.callTool(tool.name, args);
634
+ const formatted = this.formatForDisplay(result);
635
+ this.showMessage(`Tool Result: ${tool.name}`, formatted, 'green');
636
+ }
637
+ async readResource(resource) {
638
+ let uri = resource.uri;
639
+ // Check if URI contains parameters like {id}, {path}, etc.
640
+ const paramMatches = uri.match(/\{([^}]+)\}/g);
641
+ if (paramMatches) {
642
+ // Extract parameter names and prompt for values
643
+ for (const paramMatch of paramMatches) {
644
+ const paramName = paramMatch.slice(1, -1); // Remove { and }
645
+ const value = await this.prompt(`Enter value for {${paramName}}:`);
646
+ if (!value) {
647
+ this.showMessage('Error', `Parameter {${paramName}} is required`, 'red');
648
+ return;
649
+ }
650
+ // Replace the parameter in the URI
651
+ uri = uri.replace(paramMatch, value);
652
+ }
653
+ }
654
+ const result = await this.client.readResource(uri);
655
+ const formatted = this.formatForDisplay(result);
656
+ this.showMessage(`Resource: ${resource.name}`, formatted, 'cyan');
657
+ }
658
+ async usePrompt(prompt) {
659
+ const args = {};
660
+ if (prompt.arguments && prompt.arguments.length > 0) {
661
+ for (const arg of prompt.arguments) {
662
+ const isRequired = arg.required || false;
663
+ const promptText = `${arg.name}${isRequired ? ' (required)' : ' (optional)'}:`;
664
+ const value = await this.prompt(promptText);
665
+ if (value || isRequired) {
666
+ args[arg.name] = value;
667
+ }
668
+ }
669
+ }
670
+ const result = await this.client.getPrompt(prompt.name, args);
671
+ const formatted = this.formatForDisplay(result);
672
+ this.showMessage(`Prompt: ${prompt.name}`, formatted, 'magenta');
673
+ }
674
+ async showTrafficDetail(log) {
675
+ // Find matching request/response pair
676
+ const logs = this.client.getTrafficLog();
677
+ const logIndex = logs.indexOf(log);
678
+ let requestLog = log;
679
+ let responseLog = null;
680
+ // If this is a response, find its request
681
+ if (log.direction === 'received' && 'id' in log.data) {
682
+ const requestId = log.data.id;
683
+ if (requestId !== undefined && requestId !== null) {
684
+ // Look backwards for the request with matching ID
685
+ for (let i = logIndex + 1; i < logs.length; i++) {
686
+ const logData = logs[i].data;
687
+ if (logs[i].direction === 'sent' && 'id' in logs[i].data &&
688
+ logData.id !== undefined && logData.id === requestId) {
689
+ requestLog = logs[i];
690
+ responseLog = log;
691
+ break;
692
+ }
693
+ }
694
+ }
695
+ }
696
+ // If this is a request, find its response
697
+ else if (log.direction === 'sent' && 'id' in log.data) {
698
+ const requestId = log.data.id;
699
+ if (requestId !== undefined && requestId !== null) {
700
+ // Look backwards for response with matching ID
701
+ for (let i = logIndex - 1; i >= 0; i--) {
702
+ const logData = logs[i].data;
703
+ if (logs[i].direction === 'received' && 'id' in logs[i].data &&
704
+ logData.id !== undefined && logData.id === requestId) {
705
+ responseLog = logs[i];
706
+ break;
707
+ }
708
+ }
709
+ }
710
+ }
711
+ // Format request and response JSON (already formatted with indentation)
712
+ const requestJson = this.formatForDisplay(requestLog.data);
713
+ const responseJson = responseLog
714
+ ? this.formatForDisplay(responseLog.data)
715
+ : '(pending - no response yet)';
716
+ // Split into lines for side-by-side display
717
+ const requestLines = requestJson.split('\n');
718
+ const responseLines = responseJson.split('\n');
719
+ const maxLines = Math.max(requestLines.length, responseLines.length);
720
+ // Calculate column width (half of 90% screen width, minus separator)
721
+ const colWidth = 60;
722
+ // Build side-by-side display
723
+ const lines = [];
724
+ lines.push(`{bold}{yellow-fg}REQUEST{/yellow-fg}{/bold} - ${requestLog.timestamp.toISOString()}`.padEnd(colWidth) +
725
+ ' {gray-fg}│{/gray-fg} ' +
726
+ `{bold}{green-fg}RESPONSE{/green-fg}{/bold}${responseLog ? ' - ' + responseLog.timestamp.toISOString() : ''}`);
727
+ lines.push(`{gray-fg}${'─'.repeat(colWidth)}{/gray-fg} {gray-fg}┼{/gray-fg} {gray-fg}${'─'.repeat(colWidth)}{/gray-fg}`);
728
+ for (let i = 0; i < maxLines; i++) {
729
+ const reqLine = requestLines[i] || '';
730
+ const respLine = responseLines[i] || '';
731
+ // Pad request line to column width
732
+ const paddedReq = reqLine.padEnd(colWidth).substring(0, colWidth);
733
+ lines.push(paddedReq + ' {gray-fg}│{/gray-fg} ' + respLine);
734
+ }
735
+ this.showMessage('Traffic Detail - Request/Response Pair', lines.join('\n'), 'cyan');
736
+ }
737
+ formatXML(xml) {
738
+ // Simple XML formatter with indentation
739
+ let formatted = '';
740
+ let indent = 0;
741
+ const tab = ' ';
742
+ xml.split(/>\s*</).forEach((node, index) => {
743
+ // Add back the angle brackets
744
+ if (index > 0)
745
+ node = '<' + node;
746
+ if (index < xml.split(/>\s*</).length - 1)
747
+ node = node + '>';
748
+ // Check if it's a closing tag
749
+ if (node.match(/^<\/\w/)) {
750
+ indent--;
751
+ }
752
+ // Add the indented line
753
+ formatted += tab.repeat(Math.max(0, indent)) + node + '\n';
754
+ // Check if it's an opening tag (not self-closing and not closing)
755
+ if (node.match(/^<\w[^>]*[^\/]>$/)) {
756
+ indent++;
757
+ }
758
+ });
759
+ // Add syntax highlighting for XML
760
+ return formatted
761
+ .replace(/<(\/?[\w:]+)/g, '{cyan-fg}<$1{/cyan-fg}') // Tag names
762
+ .replace(/([\w:]+)=/g, '{yellow-fg}$1{/yellow-fg}=') // Attributes
763
+ .replace(/="([^"]*)"/g, '="{green-fg}$1{/green-fg}"') // Attribute values
764
+ .replace(/>/g, '{cyan-fg}>{/cyan-fg}'); // Closing brackets
765
+ }
766
+ formatForDisplay(data) {
767
+ // Smart formatter for better readability
768
+ if (typeof data === 'string') {
769
+ // Check if it's XML
770
+ if (data.trim().startsWith('<') && data.trim().includes('</')) {
771
+ try {
772
+ return this.formatXML(data);
773
+ }
774
+ catch {
775
+ // If XML formatting fails, continue to JSON attempt
776
+ }
777
+ }
778
+ // Try to parse as JSON for better formatting
779
+ try {
780
+ const parsed = JSON.parse(data);
781
+ const json = JSON.stringify(parsed, null, 2);
782
+ // Add color hints for better readability
783
+ return json
784
+ .replace(/"([^"]+)":/g, '{cyan-fg}"$1"{/cyan-fg}:') // Property names
785
+ .replace(/: "([^"]*?)"/g, ': {green-fg}"$1"{/green-fg}') // String values
786
+ .replace(/: (\d+)/g, ': {yellow-fg}$1{/yellow-fg}') // Numbers
787
+ .replace(/: (true|false|null)/g, ': {magenta-fg}$1{/magenta-fg}'); // Keywords
788
+ }
789
+ catch {
790
+ // Not JSON, return as-is
791
+ return data;
792
+ }
793
+ }
794
+ if (typeof data === 'object' && data !== null) {
795
+ // Format objects with syntax highlighting hints
796
+ const json = JSON.stringify(data, null, 2);
797
+ // Add color hints for better readability
798
+ return json
799
+ .replace(/"([^"]+)":/g, '{cyan-fg}"$1"{/cyan-fg}:') // Property names
800
+ .replace(/: "([^"]*?)"/g, ': {green-fg}"$1"{/green-fg}') // String values
801
+ .replace(/: (\d+)/g, ': {yellow-fg}$1{/yellow-fg}') // Numbers
802
+ .replace(/: (true|false|null)/g, ': {magenta-fg}$1{/magenta-fg}'); // Keywords
803
+ }
804
+ return String(data);
805
+ }
806
+ showMessage(title, content, color) {
807
+ // Store both raw and rendered versions
808
+ const rawContent = content;
809
+ const renderedContent = content.replace(/\\n/g, '\n').replace(/\\t/g, ' ');
810
+ // Store for toggling
811
+ this.currentPopupContent = { raw: rawContent, rendered: renderedContent, title, color };
812
+ this.showRawInPopup = false;
813
+ const msg = blessed_1.default.box({
814
+ parent: this.screen,
815
+ top: 'center',
816
+ left: 'center',
817
+ width: '90%',
818
+ height: '85%',
819
+ label: ` ${title} - F4/ESC=close W=toggle PgUp/PgDn=scroll `,
820
+ tags: true,
821
+ border: { type: 'line' },
822
+ style: {
823
+ border: { fg: color, bold: true },
824
+ },
825
+ scrollable: true,
826
+ alwaysScroll: true,
827
+ keys: true,
828
+ vi: true,
829
+ mouse: true,
830
+ scrollbar: {
831
+ ch: '█',
832
+ track: {
833
+ ch: '░',
834
+ },
835
+ style: {
836
+ fg: color,
837
+ bg: 'black',
838
+ },
839
+ },
840
+ content: this.showRawInPopup ? rawContent : renderedContent,
841
+ });
842
+ // Page Up/Page Down for scrolling
843
+ msg.key(['pageup'], () => {
844
+ msg.scroll(-10);
845
+ this.screen.render();
846
+ });
847
+ msg.key(['pagedown'], () => {
848
+ msg.scroll(10);
849
+ this.screen.render();
850
+ });
851
+ // W to toggle between raw and rendered
852
+ msg.key(['w', 'W'], () => {
853
+ this.showRawInPopup = !this.showRawInPopup;
854
+ if (this.currentPopupContent) {
855
+ msg.setContent(this.showRawInPopup ? this.currentPopupContent.raw : this.currentPopupContent.rendered);
856
+ msg.setLabel(` ${this.currentPopupContent.title} - Press F4/ESC to close, W to toggle raw/rendered ${this.showRawInPopup ? '(RAW)' : '(RENDERED)'} `);
857
+ this.screen.render();
858
+ }
859
+ });
860
+ // F4 or ESC to close
861
+ msg.key(['f4', 'escape', 'enter'], () => {
862
+ this.currentPopupContent = null;
863
+ this.showRawInPopup = false;
864
+ msg.destroy();
865
+ this.screen.render();
866
+ });
867
+ msg.focus();
868
+ this.screen.render();
869
+ }
870
+ }
871
+ exports.TUI = TUI;
872
+ //# sourceMappingURL=tui.js.map