run-mcp 1.3.2 → 1.5.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 (3) hide show
  1. package/README.md +50 -39
  2. package/dist/index.js +2297 -333
  3. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { program } from "commander";
6
6
  // src/repl.ts
7
7
  import { readFile } from "fs/promises";
8
8
  import { createInterface } from "readline";
9
+ import { checkbox, confirm, input, search } from "@inquirer/prompts";
9
10
  import pc from "picocolors";
10
11
 
11
12
  // src/interceptor.ts
@@ -134,14 +135,14 @@ var ResponseInterceptor = class {
134
135
  };
135
136
 
136
137
  // src/parsing.ts
137
- function parseCommandLine(input) {
138
- const spaceIdx = input.indexOf(" ");
138
+ function parseCommandLine(input3) {
139
+ const spaceIdx = input3.indexOf(" ");
139
140
  if (spaceIdx === -1) {
140
- return { cmd: input.toLowerCase(), rest: "" };
141
+ return { cmd: input3.toLowerCase(), rest: "" };
141
142
  }
142
143
  return {
143
- cmd: input.slice(0, spaceIdx).toLowerCase(),
144
- rest: input.slice(spaceIdx + 1)
144
+ cmd: input3.slice(0, spaceIdx).toLowerCase(),
145
+ rest: input3.slice(spaceIdx + 1)
145
146
  };
146
147
  }
147
148
  function parseCallArgs(rest) {
@@ -178,29 +179,187 @@ function levenshtein(a, b) {
178
179
  }
179
180
  return dp[m][n];
180
181
  }
181
- function suggestCommand(input, commands, threshold = 0.4) {
182
+ function suggestCommand(input3, commands, threshold = 0.4) {
182
183
  let best = null;
183
184
  let bestDist = Infinity;
184
185
  for (const cmd of commands) {
185
- const dist = levenshtein(input, cmd);
186
+ const dist = levenshtein(input3, cmd);
186
187
  if (dist < bestDist) {
187
188
  bestDist = dist;
188
189
  best = cmd;
189
190
  }
190
191
  }
191
- if (best && bestDist <= Math.ceil(input.length * threshold)) {
192
+ if (best && bestDist <= Math.ceil(input3.length * threshold)) {
192
193
  return best;
193
194
  }
194
195
  return null;
195
196
  }
197
+ function scaffoldArgs(schema) {
198
+ return JSON.stringify(scaffoldObject(schema), null, 2);
199
+ }
200
+ function scaffoldValue(prop) {
201
+ switch (prop.type) {
202
+ case "string":
203
+ return "<string>";
204
+ case "number":
205
+ case "integer":
206
+ return "<number>";
207
+ case "boolean":
208
+ return "<boolean>";
209
+ case "array": {
210
+ const items = prop.items;
211
+ return items ? [scaffoldValue(items)] : ["<item>"];
212
+ }
213
+ case "object":
214
+ return scaffoldObject(prop);
215
+ default:
216
+ return `<${prop.type ?? "unknown"}>`;
217
+ }
218
+ }
219
+ function scaffoldObject(schema) {
220
+ const properties = schema.properties;
221
+ if (!properties) return {};
222
+ const result = {};
223
+ for (const [key, prop] of Object.entries(properties)) {
224
+ result[key] = scaffoldValue(prop);
225
+ }
226
+ return result;
227
+ }
228
+ function formatToolDescription(tool) {
229
+ const lines = [];
230
+ lines.push(` ${tool.name}`);
231
+ if (tool.description) {
232
+ lines.push(` ${tool.description}`);
233
+ }
234
+ const schema = tool.inputSchema ?? {};
235
+ const properties = schema.properties;
236
+ const required = schema.required ?? [];
237
+ if (properties && Object.keys(properties).length > 0) {
238
+ lines.push("");
239
+ lines.push(" Arguments:");
240
+ const nameWidth = Math.max(6, ...Object.keys(properties).map((n) => n.length));
241
+ const typeWidth = Math.max(4, ...Object.values(properties).map((p) => typeLabel(p).length));
242
+ for (const [name, prop] of Object.entries(properties)) {
243
+ const type = typeLabel(prop);
244
+ const req = required.includes(name) ? "(required)" : "(optional)";
245
+ const desc = prop.description ?? "";
246
+ lines.push(
247
+ ` ${name.padEnd(nameWidth)} ${type.padEnd(typeWidth)} ${req.padEnd(10)} ${desc}`
248
+ );
249
+ }
250
+ } else {
251
+ lines.push("");
252
+ lines.push(" No arguments required.");
253
+ }
254
+ lines.push("");
255
+ lines.push(" Example:");
256
+ if (properties && Object.keys(properties).length > 0) {
257
+ const example = scaffoldObject(schema);
258
+ lines.push(` tools/call ${tool.name} ${JSON.stringify(example)}`);
259
+ } else {
260
+ lines.push(` tools/call ${tool.name}`);
261
+ }
262
+ if (tool.annotations && Object.keys(tool.annotations).length > 0) {
263
+ lines.push("");
264
+ lines.push(" Annotations:");
265
+ const annotationParts = [];
266
+ for (const [key, value] of Object.entries(tool.annotations)) {
267
+ annotationParts.push(`${key}: ${value}`);
268
+ }
269
+ lines.push(` ${annotationParts.join(", ")}`);
270
+ }
271
+ return lines.join("\n");
272
+ }
273
+ function typeLabel(prop) {
274
+ const type = prop.type;
275
+ if (!type) return "any";
276
+ if (type === "array") {
277
+ const items = prop.items;
278
+ return items ? `${typeLabel(items)}[]` : "array";
279
+ }
280
+ return type;
281
+ }
282
+ function groupToolsByPrefix(toolNames) {
283
+ const groups = /* @__PURE__ */ new Map();
284
+ for (const name of toolNames) {
285
+ const underscoreIdx = name.indexOf("_");
286
+ const prefix = underscoreIdx > 0 ? name.slice(0, underscoreIdx) : name;
287
+ const list = groups.get(prefix) ?? [];
288
+ list.push(name);
289
+ groups.set(prefix, list);
290
+ }
291
+ const meaningfulGroups = [...groups.entries()].filter(([, members]) => members.length >= 2);
292
+ if (meaningfulGroups.length < 2) {
293
+ const all = /* @__PURE__ */ new Map();
294
+ all.set("All", [...toolNames]);
295
+ return all;
296
+ }
297
+ const result = /* @__PURE__ */ new Map();
298
+ const other = [];
299
+ for (const [prefix, members] of groups) {
300
+ if (members.length >= 2) {
301
+ const label = prefix.charAt(0).toUpperCase() + prefix.slice(1);
302
+ result.set(label, members);
303
+ } else {
304
+ other.push(...members);
305
+ }
306
+ }
307
+ if (other.length > 0) {
308
+ result.set("Other", other);
309
+ }
310
+ return result;
311
+ }
312
+ var LOG_LEVELS = [
313
+ "debug",
314
+ "info",
315
+ "notice",
316
+ "warning",
317
+ "error",
318
+ "critical",
319
+ "alert",
320
+ "emergency"
321
+ ];
322
+ var ALIASES = {
323
+ tl: "tools/list",
324
+ td: "tools/describe",
325
+ tc: "tools/call",
326
+ ts: "tools/scaffold",
327
+ rl: "resources/list",
328
+ rr: "resources/read",
329
+ rt: "resources/templates",
330
+ rs: "resources/subscribe",
331
+ ru: "resources/unsubscribe",
332
+ pl: "prompts/list",
333
+ pg: "prompts/get"
334
+ };
335
+ function resolveAlias(input3) {
336
+ const spaceIdx = input3.indexOf(" ");
337
+ const token = spaceIdx === -1 ? input3 : input3.slice(0, spaceIdx);
338
+ const rest = spaceIdx === -1 ? "" : input3.slice(spaceIdx);
339
+ const expanded = ALIASES[token.toLowerCase()];
340
+ if (!expanded) return null;
341
+ return expanded + rest;
342
+ }
196
343
 
197
344
  // src/target-manager.ts
198
345
  import { EventEmitter } from "events";
199
346
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
347
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
200
348
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
349
+ import {
350
+ CreateMessageRequestSchema,
351
+ ElicitRequestSchema,
352
+ ListRootsRequestSchema,
353
+ LoggingMessageNotificationSchema,
354
+ PromptListChangedNotificationSchema,
355
+ ResourceListChangedNotificationSchema,
356
+ ResourceUpdatedNotificationSchema,
357
+ ToolListChangedNotificationSchema
358
+ } from "@modelcontextprotocol/sdk/types.js";
201
359
  var MIN_UPTIME_FOR_RESTART_MS = 5e3;
202
360
  var MAX_RECONNECT_ATTEMPTS = 3;
203
361
  var STABLE_CONNECTION_RESET_MS = 6e4;
362
+ var MAX_HISTORY = 100;
204
363
  var TargetManager = class _TargetManager extends EventEmitter {
205
364
  constructor(command, args) {
206
365
  super();
@@ -223,6 +382,14 @@ var TargetManager = class _TargetManager extends EventEmitter {
223
382
  _autoReconnect = false;
224
383
  _reconnecting = false;
225
384
  _intentionalClose = false;
385
+ // Request history
386
+ _history = [];
387
+ _historyIdCounter = 0;
388
+ // Notifications
389
+ _notifications = [];
390
+ static MAX_NOTIFICATIONS = 200;
391
+ // Roots
392
+ _roots = [];
226
393
  /**
227
394
  * Enable auto-reconnect behavior.
228
395
  * Only applies to interactive REPL mode — proxy mode manages its own lifecycle.
@@ -235,24 +402,126 @@ var TargetManager = class _TargetManager extends EventEmitter {
235
402
  * Stderr from the child process is emitted as 'stderr' events.
236
403
  */
237
404
  async connect() {
238
- this.transport = new StdioClientTransport({
239
- command: this.command,
240
- args: this.args,
241
- stderr: "pipe"
242
- });
243
- this.transport.stderr?.on("data", (chunk) => {
244
- const text = chunk.toString().trimEnd();
245
- if (text) {
246
- const lines = text.split("\n");
247
- this._stderrLineCount += lines.length;
248
- this._stderrLines.push(...lines);
249
- if (this._stderrLines.length > _TargetManager.MAX_STDERR_LINES) {
250
- this._stderrLines = this._stderrLines.slice(-_TargetManager.MAX_STDERR_LINES);
405
+ this._intentionalClose = false;
406
+ if (this.command.startsWith("http://") || this.command.startsWith("https://")) {
407
+ this.transport = new SSEClientTransport(new URL(this.command));
408
+ } else {
409
+ const stdioTransport = new StdioClientTransport({
410
+ command: this.command,
411
+ args: this.args,
412
+ stderr: "pipe"
413
+ });
414
+ stdioTransport.stderr?.on("data", (chunk) => {
415
+ const text = chunk.toString().trimEnd();
416
+ if (text) {
417
+ const lines = text.split("\n");
418
+ this._stderrLineCount += lines.length;
419
+ this._stderrLines.push(...lines);
420
+ if (this._stderrLines.length > _TargetManager.MAX_STDERR_LINES) {
421
+ this._stderrLines = this._stderrLines.slice(-_TargetManager.MAX_STDERR_LINES);
422
+ }
423
+ this.emit("stderr", text);
424
+ }
425
+ });
426
+ this.transport = stdioTransport;
427
+ }
428
+ this.client = new Client(
429
+ { name: "run-mcp", version: "1.4.0" },
430
+ {
431
+ capabilities: {
432
+ roots: { listChanged: true },
433
+ sampling: {},
434
+ elicitation: {}
251
435
  }
252
- this.emit("stderr", text);
253
436
  }
437
+ );
438
+ this.client.setNotificationHandler(
439
+ LoggingMessageNotificationSchema,
440
+ async (notification) => {
441
+ const record = {
442
+ method: "notifications/message",
443
+ params: notification.params,
444
+ timestamp: Date.now()
445
+ };
446
+ this._pushNotification(record);
447
+ this.emit("notification", record);
448
+ }
449
+ );
450
+ this.client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
451
+ const record = {
452
+ method: "notifications/tools/list_changed",
453
+ timestamp: Date.now()
454
+ };
455
+ this._pushNotification(record);
456
+ this.emit("notification", record);
457
+ });
458
+ this.client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
459
+ const record = {
460
+ method: "notifications/resources/list_changed",
461
+ timestamp: Date.now()
462
+ };
463
+ this._pushNotification(record);
464
+ this.emit("notification", record);
465
+ });
466
+ this.client.setNotificationHandler(
467
+ ResourceUpdatedNotificationSchema,
468
+ async (notification) => {
469
+ const record = {
470
+ method: "notifications/resources/updated",
471
+ params: notification.params,
472
+ timestamp: Date.now()
473
+ };
474
+ this._pushNotification(record);
475
+ this.emit("notification", record);
476
+ }
477
+ );
478
+ this.client.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
479
+ const record = {
480
+ method: "notifications/prompts/list_changed",
481
+ timestamp: Date.now()
482
+ };
483
+ this._pushNotification(record);
484
+ this.emit("notification", record);
485
+ });
486
+ this.client.setRequestHandler(CreateMessageRequestSchema, async (request) => {
487
+ return new Promise((resolve, reject) => {
488
+ const timeout = setTimeout(() => {
489
+ reject(new Error("Sampling request timed out (no response from user in 5 minutes)"));
490
+ }, 3e5);
491
+ this.emit("sampling_request", {
492
+ request: request.params,
493
+ respond: (result) => {
494
+ clearTimeout(timeout);
495
+ resolve(result);
496
+ },
497
+ reject: (err) => {
498
+ clearTimeout(timeout);
499
+ reject(err);
500
+ }
501
+ });
502
+ });
503
+ });
504
+ this.client.setRequestHandler(ElicitRequestSchema, async (request) => {
505
+ return new Promise((resolve, reject) => {
506
+ const timeout = setTimeout(() => {
507
+ reject(new Error("Elicitation request timed out (no response from user in 5 minutes)"));
508
+ }, 3e5);
509
+ this.emit("elicitation_request", {
510
+ request: request.params,
511
+ respond: (result) => {
512
+ clearTimeout(timeout);
513
+ resolve(result);
514
+ },
515
+ reject: (err) => {
516
+ clearTimeout(timeout);
517
+ reject(err);
518
+ }
519
+ });
520
+ });
521
+ });
522
+ this.client.setRequestHandler(ListRootsRequestSchema, async () => {
523
+ return { roots: this._roots };
254
524
  });
255
- this.client = new Client({ name: "run-mcp", version: "1.3.1" }, { capabilities: {} });
256
525
  this.client.onclose = () => {
257
526
  this._connected = false;
258
527
  this._clearStableTimer();
@@ -268,6 +537,8 @@ var TargetManager = class _TargetManager extends EventEmitter {
268
537
  const proc = this.transport._process;
269
538
  if (proc?.pid) {
270
539
  this.childPid = proc.pid;
540
+ } else {
541
+ this.childPid = null;
271
542
  }
272
543
  this.emit("connected");
273
544
  this._registerCleanup();
@@ -297,6 +568,26 @@ var TargetManager = class _TargetManager extends EventEmitter {
297
568
  getInstructions() {
298
569
  return this.client?.getInstructions();
299
570
  }
571
+ /**
572
+ * Returns the target server's name and version from the MCP handshake.
573
+ * Available after connect() completes.
574
+ */
575
+ getServerVersion() {
576
+ return this.client?.getServerVersion();
577
+ }
578
+ // ─── Ping ──────────────────────────────────────────────────────────────────
579
+ /**
580
+ * Send a ping to the target MCP server and return the round-trip time.
581
+ */
582
+ async ping() {
583
+ this._assertConnected();
584
+ const start = Date.now();
585
+ await this.client.ping();
586
+ const elapsed = Date.now() - start;
587
+ this.recordResponse();
588
+ this._addHistory("ping", void 0, { ok: true }, elapsed);
589
+ return elapsed;
590
+ }
300
591
  // ─── Tools ──────────────────────────────────────────────────────────────────
301
592
  /**
302
593
  * List all tools exposed by the target MCP server.
@@ -304,8 +595,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
304
595
  */
305
596
  async listTools(params) {
306
597
  this._assertConnected();
598
+ const start = Date.now();
307
599
  const result = await this.client.listTools(params);
308
600
  this.recordResponse();
601
+ this._addHistory("tools/list", params, result, Date.now() - start);
309
602
  return result;
310
603
  }
311
604
  /**
@@ -318,8 +611,14 @@ var TargetManager = class _TargetManager extends EventEmitter {
318
611
  async callTool(name, args = {}, _timeoutMs) {
319
612
  this._assertConnected();
320
613
  const requestOptions = { timeout: 36e5 * 10 };
321
- const result = await this.client.callTool({ name, arguments: args }, void 0, requestOptions);
614
+ const start = Date.now();
615
+ const result = await this.client.callTool(
616
+ { name, arguments: args },
617
+ void 0,
618
+ requestOptions
619
+ );
322
620
  this.recordResponse();
621
+ this._addHistory(`tools/call ${name}`, args, result, Date.now() - start);
323
622
  return result;
324
623
  }
325
624
  // ─── Resources ──────────────────────────────────────────────────────────────
@@ -329,8 +628,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
329
628
  */
330
629
  async listResources(params) {
331
630
  this._assertConnected();
631
+ const start = Date.now();
332
632
  const result = await this.client.listResources(params);
333
633
  this.recordResponse();
634
+ this._addHistory("resources/list", params, result, Date.now() - start);
334
635
  return result;
335
636
  }
336
637
  /**
@@ -339,8 +640,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
339
640
  */
340
641
  async listResourceTemplates(params) {
341
642
  this._assertConnected();
643
+ const start = Date.now();
342
644
  const result = await this.client.listResourceTemplates(params);
343
645
  this.recordResponse();
646
+ this._addHistory("resources/templates/list", params, result, Date.now() - start);
344
647
  return result;
345
648
  }
346
649
  /**
@@ -348,8 +651,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
348
651
  */
349
652
  async readResource(params) {
350
653
  this._assertConnected();
654
+ const start = Date.now();
351
655
  const result = await this.client.readResource(params);
352
656
  this.recordResponse();
657
+ this._addHistory(`resources/read ${params.uri}`, params, result, Date.now() - start);
353
658
  return result;
354
659
  }
355
660
  /**
@@ -357,8 +662,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
357
662
  */
358
663
  async subscribeResource(params) {
359
664
  this._assertConnected();
665
+ const start = Date.now();
360
666
  const result = await this.client.subscribeResource(params);
361
667
  this.recordResponse();
668
+ this._addHistory(`resources/subscribe ${params.uri}`, params, result, Date.now() - start);
362
669
  return result;
363
670
  }
364
671
  /**
@@ -366,8 +673,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
366
673
  */
367
674
  async unsubscribeResource(params) {
368
675
  this._assertConnected();
676
+ const start = Date.now();
369
677
  const result = await this.client.unsubscribeResource(params);
370
678
  this.recordResponse();
679
+ this._addHistory(`resources/unsubscribe ${params.uri}`, params, result, Date.now() - start);
371
680
  return result;
372
681
  }
373
682
  // ─── Prompts ────────────────────────────────────────────────────────────────
@@ -377,8 +686,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
377
686
  */
378
687
  async listPrompts(params) {
379
688
  this._assertConnected();
689
+ const start = Date.now();
380
690
  const result = await this.client.listPrompts(params);
381
691
  this.recordResponse();
692
+ this._addHistory("prompts/list", params, result, Date.now() - start);
382
693
  return result;
383
694
  }
384
695
  /**
@@ -386,8 +697,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
386
697
  */
387
698
  async getPrompt(params) {
388
699
  this._assertConnected();
700
+ const start = Date.now();
389
701
  const result = await this.client.getPrompt(params);
390
702
  this.recordResponse();
703
+ this._addHistory(`prompts/get ${params.name}`, params, result, Date.now() - start);
391
704
  return result;
392
705
  }
393
706
  // ─── Logging ────────────────────────────────────────────────────────────────
@@ -396,8 +709,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
396
709
  */
397
710
  async setLoggingLevel(level) {
398
711
  this._assertConnected();
712
+ const start = Date.now();
399
713
  const result = await this.client.setLoggingLevel(level);
400
714
  this.recordResponse();
715
+ this._addHistory(`logging/setLevel ${level}`, { level }, result, Date.now() - start);
401
716
  return result;
402
717
  }
403
718
  // ─── Completion ─────────────────────────────────────────────────────────────
@@ -406,10 +721,96 @@ var TargetManager = class _TargetManager extends EventEmitter {
406
721
  */
407
722
  async complete(params) {
408
723
  this._assertConnected();
724
+ const start = Date.now();
409
725
  const result = await this.client.complete(params);
410
726
  this.recordResponse();
727
+ this._addHistory("completion/complete", params, result, Date.now() - start);
411
728
  return result;
412
729
  }
730
+ // ─── Request History ────────────────────────────────────────────────────────
731
+ /**
732
+ * Get the request/response history.
733
+ * @param count - Number of recent records to return (default: all)
734
+ */
735
+ getHistory(count) {
736
+ if (!count || count >= this._history.length) return [...this._history];
737
+ return this._history.slice(-count);
738
+ }
739
+ /**
740
+ * Clear the history buffer.
741
+ */
742
+ clearHistory() {
743
+ this._history = [];
744
+ }
745
+ _addHistory(method, params, result, durationMs) {
746
+ const record = {
747
+ id: ++this._historyIdCounter,
748
+ method,
749
+ params,
750
+ result,
751
+ durationMs,
752
+ timestamp: Date.now()
753
+ };
754
+ this._history.push(record);
755
+ if (this._history.length > MAX_HISTORY) {
756
+ this._history = this._history.slice(-MAX_HISTORY);
757
+ }
758
+ }
759
+ // ─── Notification History ───────────────────────────────────────────────────
760
+ /**
761
+ * Get recent server notifications.
762
+ * @param count - Number of recent notifications to return (default: all)
763
+ */
764
+ getNotifications(count) {
765
+ if (!count || count >= this._notifications.length) return [...this._notifications];
766
+ return this._notifications.slice(-count);
767
+ }
768
+ /**
769
+ * Clear the notification buffer.
770
+ */
771
+ clearNotifications() {
772
+ this._notifications = [];
773
+ }
774
+ _pushNotification(record) {
775
+ this._notifications.push(record);
776
+ if (this._notifications.length > _TargetManager.MAX_NOTIFICATIONS) {
777
+ this._notifications = this._notifications.slice(-_TargetManager.MAX_NOTIFICATIONS);
778
+ }
779
+ }
780
+ // ─── Roots Management ─────────────────────────────────────────────────────
781
+ /**
782
+ * Get the current roots list that this client advertises.
783
+ */
784
+ getRoots() {
785
+ return [...this._roots];
786
+ }
787
+ /**
788
+ * Add a root and send notification to the server.
789
+ */
790
+ async addRoot(root) {
791
+ if (this._roots.some((r) => r.uri === root.uri)) return;
792
+ this._roots.push(root);
793
+ await this._sendRootsChanged();
794
+ }
795
+ /**
796
+ * Remove a root by URI and send notification to the server.
797
+ */
798
+ async removeRoot(uri) {
799
+ const before = this._roots.length;
800
+ this._roots = this._roots.filter((r) => r.uri !== uri);
801
+ if (this._roots.length < before) {
802
+ await this._sendRootsChanged();
803
+ return true;
804
+ }
805
+ return false;
806
+ }
807
+ async _sendRootsChanged() {
808
+ if (!this._connected || !this.client) return;
809
+ try {
810
+ await this.client.sendRootsListChanged();
811
+ } catch {
812
+ }
813
+ }
413
814
  // ─── Notification forwarding ────────────────────────────────────────────────
414
815
  /**
415
816
  * Access the underlying MCP client for advanced use cases like
@@ -566,18 +967,199 @@ var TargetManager = class _TargetManager extends EventEmitter {
566
967
 
567
968
  // src/repl.ts
568
969
  var KNOWN_COMMANDS = [
970
+ "explore",
971
+ "interactive",
569
972
  "tools/list",
570
973
  "tools/describe",
571
974
  "tools/call",
975
+ "tools/scaffold",
976
+ "tools/forget",
977
+ "resources/list",
978
+ "resources/read",
979
+ "resources/templates",
980
+ "resources/subscribe",
981
+ "resources/unsubscribe",
982
+ "prompts/list",
983
+ "prompts/get",
984
+ "ping",
985
+ "log-level",
986
+ "history",
987
+ "notifications",
988
+ "roots/list",
989
+ "roots/add",
990
+ "roots/remove",
991
+ "timing",
572
992
  "status",
993
+ "reconnect",
994
+ "!!",
995
+ "last",
573
996
  "help",
574
997
  "exit",
575
- "quit"
998
+ "quit",
999
+ // Short aliases
1000
+ "tl",
1001
+ "td",
1002
+ "tc",
1003
+ "ts",
1004
+ "rl",
1005
+ "rr",
1006
+ "rt",
1007
+ "rs",
1008
+ "ru",
1009
+ "pl",
1010
+ "pg"
576
1011
  ];
1012
+ var cachedToolNames = [];
1013
+ var cachedResourceUris = [];
1014
+ var cachedPromptNames = [];
1015
+ async function refreshCaches(target) {
1016
+ const caps = target.getServerCapabilities() ?? {};
1017
+ try {
1018
+ const { tools } = await target.listTools();
1019
+ cachedToolNames = tools.map((t) => t.name);
1020
+ } catch {
1021
+ }
1022
+ if (caps.resources) {
1023
+ try {
1024
+ const { resources } = await target.listResources();
1025
+ cachedResourceUris = resources.map((r) => r.uri);
1026
+ } catch {
1027
+ }
1028
+ }
1029
+ if (caps.prompts) {
1030
+ try {
1031
+ const { prompts } = await target.listPrompts();
1032
+ cachedPromptNames = prompts.map((p) => p.name);
1033
+ } catch {
1034
+ }
1035
+ }
1036
+ }
1037
+ var tabCycleState = null;
1038
+ function resetTabCycle() {
1039
+ tabCycleState = null;
1040
+ }
1041
+ function computeMatches(line) {
1042
+ const expanded = resolveAlias(line);
1043
+ const effective = expanded ?? line;
1044
+ for (const prefix of ["tools/call ", "tools/describe ", "tools/scaffold "]) {
1045
+ if (effective.startsWith(prefix)) {
1046
+ const partial = effective.slice(prefix.length).split(" ")[0];
1047
+ const matches2 = cachedToolNames.filter((n) => n.startsWith(partial));
1048
+ return [matches2.map((m) => `${prefix}${m}`), effective];
1049
+ }
1050
+ }
1051
+ if (effective.startsWith("resources/read ")) {
1052
+ const partial = effective.slice("resources/read ".length);
1053
+ const matches2 = cachedResourceUris.filter((u) => u.startsWith(partial));
1054
+ return [matches2.map((m) => `resources/read ${m}`), effective];
1055
+ }
1056
+ if (effective.startsWith("prompts/get ")) {
1057
+ const partial = effective.slice("prompts/get ".length).split(" ")[0];
1058
+ const matches2 = cachedPromptNames.filter((n) => n.startsWith(partial));
1059
+ return [matches2.map((m) => `prompts/get ${m}`), effective];
1060
+ }
1061
+ const matches = KNOWN_COMMANDS.filter((c) => c.startsWith(line));
1062
+ return [matches, line];
1063
+ }
1064
+ var completer = (line) => {
1065
+ if (tabCycleState) {
1066
+ const inCycle = line === tabCycleState.original || tabCycleState.matches.includes(line);
1067
+ if (inCycle) {
1068
+ tabCycleState.index = (tabCycleState.index + 1) % tabCycleState.matches.length;
1069
+ const next = tabCycleState.matches[tabCycleState.index];
1070
+ setImmediate(() => {
1071
+ if (activeRl) {
1072
+ activeRl.line = next;
1073
+ activeRl.cursor = next.length;
1074
+ activeRl._refreshLine();
1075
+ }
1076
+ });
1077
+ return [[], ""];
1078
+ }
1079
+ tabCycleState = null;
1080
+ }
1081
+ const [matches, matchLine] = computeMatches(line);
1082
+ if (matches.length > 1) {
1083
+ tabCycleState = { matches, index: -1, original: line };
1084
+ }
1085
+ return [matches, matchLine];
1086
+ };
1087
+ var callHistory = [];
1088
+ var lastToolArgsMap = /* @__PURE__ */ new Map();
1089
+ var replHistory = [];
1090
+ var lastCommand = null;
1091
+ var AbortFlowError = class extends Error {
1092
+ constructor() {
1093
+ super("Aborted by user.");
1094
+ this.name = "AbortFlowError";
1095
+ }
1096
+ };
1097
+ function isAbortError(err) {
1098
+ if (!err) return false;
1099
+ return err.name === "ExitPromptError" || err.name === "AbortError" || err.message === "Prompt was aborted" || typeof err.message === "string" && err.message.includes("User force closed");
1100
+ }
1101
+ var activeRl = null;
1102
+ var isScriptMode = false;
1103
+ var globalPauseReadlineClose = false;
1104
+ var deferNextPrompt = false;
1105
+ async function withSuspendedReadline(target, interceptor, fn) {
1106
+ const wasActive = !!activeRl;
1107
+ if (wasActive) {
1108
+ globalPauseReadlineClose = true;
1109
+ activeRl.close();
1110
+ activeRl = null;
1111
+ }
1112
+ try {
1113
+ return await fn();
1114
+ } finally {
1115
+ if (wasActive) {
1116
+ globalPauseReadlineClose = false;
1117
+ if (!isScriptMode) {
1118
+ deferNextPrompt = true;
1119
+ startReadlineLoop(target, interceptor);
1120
+ }
1121
+ }
1122
+ }
1123
+ }
1124
+ function getPrompt(target) {
1125
+ if (target.connected) return `${pc.green("\u2713")}${pc.cyan("> ")}`;
1126
+ return `${pc.red("\u2717")}${pc.cyan("> ")}`;
1127
+ }
1128
+ function printBanner(serverName, serverVersion, toolCount, resourceCount, promptCount) {
1129
+ const parts = [];
1130
+ parts.push(`${pc.bold(toolCount.toString())} tools`);
1131
+ if (resourceCount > 0) parts.push(`${pc.bold(resourceCount.toString())} resources`);
1132
+ if (promptCount > 0) parts.push(`${pc.bold(promptCount.toString())} prompts`);
1133
+ const title = serverVersion ? `${serverName} ${pc.dim(`v${serverVersion}`)}` : serverName;
1134
+ const BOX_WIDTH = 53;
1135
+ const padLine = (content) => {
1136
+ const visible = stripAnsi(content).length;
1137
+ const padding = Math.max(0, BOX_WIDTH - visible);
1138
+ return `${pc.cyan(" \u2502")}${content}${"".padEnd(padding)}${pc.cyan("\u2502")}`;
1139
+ };
1140
+ const partsStr = ` ${parts.join(" \u2022 ")}`;
1141
+ console.log();
1142
+ console.log(pc.cyan(` \u250C${"\u2500".repeat(BOX_WIDTH)}\u2510`));
1143
+ console.log(padLine(` ${title}`));
1144
+ console.log(padLine(partsStr));
1145
+ console.log(pc.cyan(` \u251C${"\u2500".repeat(BOX_WIDTH)}\u2524`));
1146
+ console.log(padLine(" Quick start:"));
1147
+ console.log(padLine(` ${pc.green("tools/list")} See all tools`));
1148
+ console.log(padLine(` ${pc.green("tools/call")} ${pc.dim("<name>")} Call a tool`));
1149
+ console.log(padLine(` ${pc.green("help")} All commands`));
1150
+ console.log(padLine(""));
1151
+ console.log(padLine(pc.dim(" Tab completion is active. Start typing to explore.")));
1152
+ console.log(pc.cyan(` \u2514${"\u2500".repeat(BOX_WIDTH)}\u2518`));
1153
+ console.log();
1154
+ }
1155
+ function stripAnsi(str) {
1156
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
1157
+ }
577
1158
  async function startRepl(targetCommand, opts) {
578
1159
  const [command, ...args] = targetCommand;
579
1160
  const target = new TargetManager(command, args);
580
1161
  const interceptor = new ResponseInterceptor({ outDir: opts.outDir });
1162
+ isScriptMode = !!opts.script;
581
1163
  target.on("stderr", (text) => {
582
1164
  for (const line of text.split("\n")) {
583
1165
  console.error(pc.dim(`[server] ${line}`));
@@ -600,7 +1182,7 @@ async function startRepl(targetCommand, opts) {
600
1182
  }
601
1183
  const status = target.getStatus();
602
1184
  console.log(pc.green(`\u2713 Connected (PID: ${status.pid})`));
603
- if (!opts.script) {
1185
+ if (!isScriptMode) {
604
1186
  target.enableAutoReconnect();
605
1187
  target.on(
606
1188
  "reconnecting",
@@ -611,9 +1193,10 @@ async function startRepl(targetCommand, opts) {
611
1193
  );
612
1194
  }
613
1195
  );
614
- target.on("reconnected", ({ attempt }) => {
1196
+ target.on("reconnected", async ({ attempt }) => {
615
1197
  const s = target.getStatus();
616
1198
  console.log(pc.green(`\u2713 Reconnected (PID: ${s.pid}, attempt ${attempt})`));
1199
+ await refreshCaches(target);
617
1200
  });
618
1201
  target.on("reconnect_failed", ({ reason, message }) => {
619
1202
  console.error(pc.red(`\u2717 ${message}`));
@@ -623,19 +1206,145 @@ async function startRepl(targetCommand, opts) {
623
1206
  );
624
1207
  }
625
1208
  });
1209
+ target.on("notification", (notification) => {
1210
+ const method = notification.method;
1211
+ if (method === "notifications/message") {
1212
+ const lvl = notification.params?.level ?? "info";
1213
+ const data = notification.params?.data ?? "";
1214
+ const text = typeof data === "string" ? data : JSON.stringify(data);
1215
+ console.log(pc.dim(`
1216
+ [${lvl}] ${text}`));
1217
+ } else if (method === "notifications/tools/list_changed") {
1218
+ console.log(pc.yellow("\n \u27F3 Server tools changed. Run tools/list to see updates."));
1219
+ refreshCaches(target).catch(() => {
1220
+ });
1221
+ } else if (method === "notifications/resources/list_changed") {
1222
+ console.log(pc.yellow("\n \u27F3 Server resources changed. Run resources/list to see."));
1223
+ refreshCaches(target).catch(() => {
1224
+ });
1225
+ } else if (method === "notifications/resources/updated") {
1226
+ const uri = notification.params?.uri ?? "unknown";
1227
+ console.log(pc.yellow(`
1228
+ \u27F3 Resource updated: ${uri}`));
1229
+ } else if (method === "notifications/prompts/list_changed") {
1230
+ console.log(pc.yellow("\n \u27F3 Server prompts changed. Run prompts/list to see."));
1231
+ refreshCaches(target).catch(() => {
1232
+ });
1233
+ }
1234
+ });
1235
+ target.on("sampling_request", async ({ request, respond, reject: rejectFn }) => {
1236
+ console.log(pc.magenta("\n \u2554\u2550\u2550 Sampling Request \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1237
+ const messages = request?.messages ?? [];
1238
+ for (const msg of messages) {
1239
+ const role = msg.role === "user" ? pc.blue("user") : pc.magenta("assistant");
1240
+ const text = msg.content?.text ?? JSON.stringify(msg.content);
1241
+ console.log(pc.magenta(` \u2551 ${role}: ${text}`));
1242
+ }
1243
+ console.log(pc.magenta(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1244
+ if (activeRl) {
1245
+ try {
1246
+ const answer = await question(activeRl, ` ${pc.bold("Approve? [y/N/text]:")} `);
1247
+ const trimmed = answer.trim().toLowerCase();
1248
+ if (trimmed === "y" || trimmed === "yes") {
1249
+ respond({
1250
+ model: "user-approved",
1251
+ role: "assistant",
1252
+ content: { type: "text", text: "Approved by user." }
1253
+ });
1254
+ } else if (trimmed === "n" || trimmed === "no" || trimmed === "") {
1255
+ rejectFn(new Error("Sampling request rejected by user"));
1256
+ } else {
1257
+ respond({
1258
+ model: "user-provided",
1259
+ role: "assistant",
1260
+ content: { type: "text", text: answer.trim() }
1261
+ });
1262
+ }
1263
+ } catch (err) {
1264
+ if (err instanceof AbortFlowError) {
1265
+ rejectFn(new Error("Sampling request rejected by user"));
1266
+ } else {
1267
+ throw err;
1268
+ }
1269
+ }
1270
+ } else {
1271
+ rejectFn(new Error("No interactive terminal available for sampling approval"));
1272
+ }
1273
+ });
1274
+ target.on("elicitation_request", async ({ request, respond, reject: rejectFn }) => {
1275
+ console.log(pc.cyan("\n \u2554\u2550\u2550 Elicitation Request \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1276
+ console.log(pc.cyan(` \u2551 ${request?.message ?? "Server requests input"}`));
1277
+ console.log(pc.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
1278
+ if (activeRl) {
1279
+ try {
1280
+ const answer = await question(
1281
+ activeRl,
1282
+ ` ${pc.bold("Your response (empty to decline):")} `
1283
+ );
1284
+ if (answer.trim() === "") {
1285
+ respond({ action: "decline" });
1286
+ } else {
1287
+ try {
1288
+ const parsed = JSON.parse(answer.trim());
1289
+ respond({ action: "accept", content: parsed });
1290
+ } catch {
1291
+ respond({ action: "accept", content: { value: answer.trim() } });
1292
+ }
1293
+ }
1294
+ } catch (err) {
1295
+ if (err instanceof AbortFlowError) {
1296
+ respond({ action: "decline" });
1297
+ } else {
1298
+ throw err;
1299
+ }
1300
+ }
1301
+ } else {
1302
+ rejectFn(new Error("No interactive terminal available for elicitation"));
1303
+ }
1304
+ });
626
1305
  }
1306
+ let toolCount = 0;
1307
+ let resourceCount = 0;
1308
+ let promptCount = 0;
627
1309
  try {
628
1310
  const { tools } = await target.listTools();
629
- console.log(
630
- pc.cyan(` ${tools.length} tool(s) available. Type ${pc.bold("help")} for commands.
631
- `)
632
- );
1311
+ toolCount = tools.length;
1312
+ const caps = target.getServerCapabilities() ?? {};
1313
+ if (caps.resources) {
1314
+ try {
1315
+ const { resources } = await target.listResources();
1316
+ resourceCount = resources.length;
1317
+ } catch {
1318
+ }
1319
+ }
1320
+ if (caps.prompts) {
1321
+ try {
1322
+ const { prompts } = await target.listPrompts();
1323
+ promptCount = prompts.length;
1324
+ } catch {
1325
+ }
1326
+ }
1327
+ const serverInfo = target.getServerVersion();
1328
+ const serverName = serverInfo?.name ?? "MCP Server";
1329
+ const serverVersion = serverInfo?.version ?? "";
1330
+ printBanner(serverName, serverVersion, toolCount, resourceCount, promptCount);
1331
+ if (toolCount >= 10) {
1332
+ const groups = groupToolsByPrefix(
1333
+ cachedToolNames.length > 0 ? cachedToolNames : tools.map((t) => t.name)
1334
+ );
1335
+ if (!groups.has("All")) {
1336
+ for (const [label, members] of groups) {
1337
+ console.log(` ${pc.bold(label.padEnd(16))} ${pc.dim(members.join(", "))}`);
1338
+ }
1339
+ console.log();
1340
+ }
1341
+ }
633
1342
  } catch (err) {
634
1343
  console.log(pc.yellow(` Warning: Could not list tools: ${err.message}
635
1344
  `));
636
1345
  }
637
- const isScript = !!opts.script;
638
- if (isScript) {
1346
+ await refreshCaches(target);
1347
+ if (isScriptMode) {
639
1348
  const lines = await readScriptLines(opts.script);
640
1349
  for (const line of lines) {
641
1350
  const trimmed = line.trim();
@@ -653,51 +1362,98 @@ async function startRepl(targetCommand, opts) {
653
1362
  await target.close();
654
1363
  process.exit(0);
655
1364
  } else {
656
- const rl = createInterface({
657
- input: process.stdin,
658
- output: process.stdout,
659
- prompt: pc.cyan("> "),
660
- terminal: true
1365
+ startReadlineLoop(target, interceptor);
1366
+ }
1367
+ }
1368
+ function startReadlineLoop(target, interceptor) {
1369
+ if (isScriptMode || activeRl) return;
1370
+ const rl = createInterface({
1371
+ input: process.stdin,
1372
+ output: process.stdout,
1373
+ prompt: getPrompt(target),
1374
+ terminal: true,
1375
+ completer,
1376
+ history: [...replHistory].reverse()
1377
+ // Node's readline history expects newest first
1378
+ });
1379
+ activeRl = rl;
1380
+ if (process.stdin.isTTY) {
1381
+ process.stdin.on("keypress", (_str, key) => {
1382
+ if (!key || key.name !== "tab") {
1383
+ resetTabCycle();
1384
+ }
661
1385
  });
1386
+ }
1387
+ if (!deferNextPrompt) {
662
1388
  rl.prompt();
663
- let processing = false;
664
- const queue = [];
665
- const processQueue = async () => {
666
- if (processing) return;
667
- processing = true;
668
- while (queue.length > 0) {
669
- const trimmed = queue.shift();
670
- try {
671
- await handleCommand(trimmed, target, interceptor);
672
- } catch (err) {
1389
+ }
1390
+ deferNextPrompt = false;
1391
+ let processing = false;
1392
+ let closed = false;
1393
+ const queue = [];
1394
+ const processQueue = async () => {
1395
+ if (processing) return;
1396
+ processing = true;
1397
+ while (queue.length > 0) {
1398
+ const trimmed = queue.shift();
1399
+ try {
1400
+ await handleCommand(trimmed, target, interceptor);
1401
+ } catch (err) {
1402
+ if (err instanceof AbortFlowError) {
1403
+ console.log(pc.yellow(" Aborted."));
1404
+ } else {
673
1405
  console.error(pc.red(`\u2717 Error: ${err.message}`));
674
1406
  }
675
- rl.prompt();
676
1407
  }
677
- processing = false;
678
- };
679
- rl.on("line", (line) => {
680
- const trimmed = line.trim();
681
- if (!trimmed || trimmed.startsWith("#")) {
682
- rl.prompt();
683
- return;
1408
+ if (activeRl) {
1409
+ setImmediate(() => {
1410
+ if (activeRl) {
1411
+ console.log();
1412
+ activeRl.setPrompt(getPrompt(target));
1413
+ activeRl.prompt();
1414
+ }
1415
+ });
684
1416
  }
685
- queue.push(trimmed);
686
- processQueue();
687
- });
688
- rl.on("close", async () => {
1417
+ }
1418
+ processing = false;
1419
+ };
1420
+ rl.on("line", (line) => {
1421
+ const trimmed = line.trim();
1422
+ if (!trimmed || trimmed.startsWith("#")) {
1423
+ if (!closed && activeRl) activeRl.prompt();
1424
+ return;
1425
+ }
1426
+ replHistory.push(trimmed);
1427
+ queue.push(trimmed);
1428
+ processQueue();
1429
+ });
1430
+ rl.on("close", async () => {
1431
+ closed = true;
1432
+ activeRl = null;
1433
+ if (!globalPauseReadlineClose) {
689
1434
  console.log(pc.dim("\nShutting down..."));
690
1435
  await target.close();
691
1436
  process.exit(0);
692
- });
693
- }
1437
+ }
1438
+ });
694
1439
  }
695
- async function handleCommand(input, target, interceptor) {
696
- const { cmd, rest } = parseCommandLine(input);
1440
+ async function handleCommand(input3, target, interceptor) {
1441
+ const expanded = resolveAlias(input3);
1442
+ const effective = expanded ?? input3;
1443
+ const { cmd, rest } = parseCommandLine(effective);
1444
+ if (cmd !== "!!" && cmd !== "last") {
1445
+ lastCommand = input3;
1446
+ }
697
1447
  switch (cmd) {
698
1448
  case "help":
699
1449
  printHelp();
700
1450
  return;
1451
+ case "explore":
1452
+ case "interactive":
1453
+ await withSuspendedReadline(target, interceptor, async () => {
1454
+ await cmdExplore(target, interceptor);
1455
+ });
1456
+ return;
701
1457
  case "tools/list":
702
1458
  await cmdToolsList(target);
703
1459
  return;
@@ -707,6 +1463,69 @@ async function handleCommand(input, target, interceptor) {
707
1463
  case "tools/call":
708
1464
  await cmdToolsCall(target, interceptor, rest);
709
1465
  return;
1466
+ case "tools/scaffold":
1467
+ await cmdToolsScaffold(target, rest);
1468
+ return;
1469
+ case "tools/forget":
1470
+ cmdToolsForget(rest);
1471
+ return;
1472
+ case "resources/list":
1473
+ await cmdResourcesList(target);
1474
+ return;
1475
+ case "resources/read":
1476
+ await cmdResourcesRead(target, rest);
1477
+ return;
1478
+ case "resources/templates":
1479
+ await cmdResourcesTemplates(target);
1480
+ return;
1481
+ case "prompts/list":
1482
+ await cmdPromptsList(target);
1483
+ return;
1484
+ case "prompts/get":
1485
+ await cmdPromptsGet(target, rest);
1486
+ return;
1487
+ case "timing":
1488
+ cmdTiming();
1489
+ return;
1490
+ case "ping":
1491
+ await cmdPing(target);
1492
+ return;
1493
+ case "log-level":
1494
+ await cmdLogLevel(target, rest);
1495
+ return;
1496
+ case "history":
1497
+ cmdHistory(target, rest);
1498
+ return;
1499
+ case "notifications":
1500
+ cmdNotifications(target, rest);
1501
+ return;
1502
+ case "resources/subscribe":
1503
+ await cmdResourcesSubscribe(target, rest);
1504
+ return;
1505
+ case "resources/unsubscribe":
1506
+ await cmdResourcesUnsubscribe(target, rest);
1507
+ return;
1508
+ case "roots/list":
1509
+ cmdRootsList(target);
1510
+ return;
1511
+ case "roots/add":
1512
+ await cmdRootsAdd(target, rest);
1513
+ return;
1514
+ case "roots/remove":
1515
+ await cmdRootsRemove(target, rest);
1516
+ return;
1517
+ case "reconnect":
1518
+ await cmdReconnect(target);
1519
+ return;
1520
+ case "!!":
1521
+ case "last":
1522
+ if (lastCommand) {
1523
+ console.log(pc.dim(` Re-running: ${lastCommand}`));
1524
+ await handleCommand(lastCommand, target, interceptor);
1525
+ } else {
1526
+ console.log(pc.yellow("No previous command to re-run."));
1527
+ }
1528
+ return;
710
1529
  case "status":
711
1530
  cmdStatus(target);
712
1531
  return;
@@ -717,7 +1536,22 @@ async function handleCommand(input, target, interceptor) {
717
1536
  default: {
718
1537
  const suggestion = suggestCommand(cmd, KNOWN_COMMANDS);
719
1538
  if (suggestion) {
720
- console.log(pc.yellow(`Unknown command: ${cmd}. Did you mean ${pc.bold(suggestion)}?`));
1539
+ console.log(pc.yellow(`Unknown command: ${cmd}.`));
1540
+ try {
1541
+ await withSuspendedReadline(target, interceptor, async () => {
1542
+ const runIt = await confirm({
1543
+ message: `Did you mean ${pc.bold(suggestion)}?`,
1544
+ default: true
1545
+ });
1546
+ if (runIt) {
1547
+ const rebuiltCommand = rest ? `${suggestion} ${rest}` : suggestion;
1548
+ await handleCommand(rebuiltCommand, target, interceptor);
1549
+ }
1550
+ });
1551
+ } catch (err) {
1552
+ if (!isAbortError(err)) throw err;
1553
+ throw new AbortFlowError();
1554
+ }
721
1555
  } else {
722
1556
  console.log(pc.yellow(`Unknown command: ${cmd}. Type ${pc.bold("help")} for usage.`));
723
1557
  }
@@ -739,32 +1573,71 @@ async function cmdToolsList(target) {
739
1573
  }
740
1574
  console.log(pc.dim(`
741
1575
  ${tools.length} tool(s) total.`));
1576
+ if (tools.length >= 10) {
1577
+ const groups = groupToolsByPrefix(tools.map((t) => t.name));
1578
+ if (!groups.has("All")) {
1579
+ console.log();
1580
+ console.log(pc.bold(" Groups:"));
1581
+ for (const [label, members] of groups) {
1582
+ console.log(` ${pc.cyan(label.padEnd(16))} ${pc.dim(members.join(", "))}`);
1583
+ }
1584
+ }
1585
+ }
742
1586
  }
743
1587
  async function cmdToolsDescribe(target, rest) {
744
1588
  const name = rest.trim();
745
1589
  if (!name) {
746
- console.log(pc.yellow("Usage: tools/describe <tool_name>"));
1590
+ console.log(pc.yellow(" Usage: tools/describe <name>"));
1591
+ if (cachedToolNames.length > 0) {
1592
+ const preview = cachedToolNames.slice(0, 6);
1593
+ const more = cachedToolNames.length > 6 ? `, ... (${cachedToolNames.length} total)` : "";
1594
+ console.log(pc.dim(`
1595
+ Available tools: ${preview.join(", ")}${more}`));
1596
+ console.log(pc.dim(` Type ${pc.bold("tools/list")} for all.`));
1597
+ }
747
1598
  return;
748
1599
  }
749
1600
  const { tools } = await target.listTools();
750
1601
  const tool = tools.find((t) => t.name === name);
751
1602
  if (!tool) {
752
1603
  console.log(pc.red(`Tool "${name}" not found.`));
753
- console.log(pc.dim(`Available: ${tools.map((t) => t.name).join(", ")}`));
1604
+ const suggestion = suggestCommand(
1605
+ name,
1606
+ tools.map((t) => t.name)
1607
+ );
1608
+ if (suggestion) {
1609
+ console.log(pc.yellow(`Did you mean ${pc.bold(suggestion)}?`));
1610
+ } else {
1611
+ console.log(pc.dim(`Available: ${tools.map((t) => t.name).join(", ")}`));
1612
+ }
754
1613
  return;
755
1614
  }
756
- console.log(pc.bold(`
757
- ${tool.name}`));
758
- if (tool.description) {
759
- console.log(pc.dim(` ${tool.description}`));
760
- }
761
- console.log(pc.cyan("\n Input Schema:"));
762
- console.log(formatJson(tool.inputSchema, 4));
1615
+ console.log();
1616
+ console.log(
1617
+ formatToolDescription({
1618
+ name: tool.name,
1619
+ description: tool.description,
1620
+ inputSchema: tool.inputSchema,
1621
+ annotations: tool.annotations
1622
+ })
1623
+ );
1624
+ console.log();
763
1625
  }
764
1626
  async function cmdToolsCall(target, interceptor, rest) {
765
- const { toolName, jsonArgs, timeoutMs } = parseCallArgs(rest);
1627
+ const clearPrevious = /\s--clear(\s|$)/.test(rest) || rest === "--clear";
1628
+ const cleanedRest = clearPrevious ? rest.replace(/\s*--clear/, "").trim() : rest;
1629
+ const { toolName, jsonArgs, timeoutMs } = parseCallArgs(cleanedRest);
766
1630
  if (!toolName) {
767
- console.log(pc.yellow("Usage: tools/call <tool_name> [json_args] [--timeout <ms>]"));
1631
+ console.log(pc.yellow(" Usage: tools/call <name> [json_args] [--timeout <ms>]"));
1632
+ if (cachedToolNames.length > 0) {
1633
+ const preview = cachedToolNames.slice(0, 6);
1634
+ const more = cachedToolNames.length > 6 ? `, ... (${cachedToolNames.length} total)` : "";
1635
+ console.log(pc.dim(`
1636
+ Available tools: ${preview.join(", ")}${more}`));
1637
+ console.log(
1638
+ pc.dim(` Run without args for ${pc.bold("interactive mode")}: tools/call <name>`)
1639
+ );
1640
+ }
768
1641
  return;
769
1642
  }
770
1643
  let args = {};
@@ -776,16 +1649,56 @@ async function cmdToolsCall(target, interceptor, rest) {
776
1649
  console.log(pc.dim(` Received: ${jsonArgs}`));
777
1650
  return;
778
1651
  }
1652
+ const { tools } = await target.listTools();
1653
+ const tool = tools.find((t) => t.name === toolName);
1654
+ if (tool) {
1655
+ const schema = tool.inputSchema;
1656
+ const required = schema.required ?? [];
1657
+ const missing = required.filter((r) => !(r in args));
1658
+ if (missing.length > 0) {
1659
+ console.log(pc.yellow(`
1660
+ Missing required arguments: ${missing.join(", ")}`));
1661
+ console.log();
1662
+ const scaffolded = scaffoldArgs(schema);
1663
+ console.log(pc.dim(" Try:"));
1664
+ console.log(` tools/call ${toolName} ${scaffolded}`);
1665
+ console.log();
1666
+ console.log(pc.dim(" Or run without args for interactive mode:"));
1667
+ console.log(` tools/call ${toolName}`);
1668
+ console.log();
1669
+ return;
1670
+ }
1671
+ }
1672
+ } else {
1673
+ const collectedArgs = await interactiveArgPrompt(target, interceptor, toolName, clearPrevious);
1674
+ if (collectedArgs === null) return;
1675
+ args = collectedArgs;
1676
+ if (!isScriptMode) {
1677
+ const fullCmd = `tools/call ${toolName} ${JSON.stringify(args)}`;
1678
+ replHistory.push(fullCmd);
1679
+ if (activeRl) {
1680
+ activeRl.history.unshift(fullCmd);
1681
+ }
1682
+ }
779
1683
  }
780
1684
  console.log(pc.dim(` Calling ${toolName}...`));
781
1685
  const startTime = Date.now();
782
1686
  const result = await interceptor.callTool(target, toolName, args, timeoutMs);
783
1687
  const elapsed = Date.now() - startTime;
1688
+ callHistory.push({ toolName, durationMs: elapsed, timestamp: startTime });
1689
+ lastToolArgsMap.set(toolName, { ...args });
1690
+ const width = 60;
1691
+ const isError = result.isError === true;
1692
+ const topText = isError ? "Error" : "Result";
1693
+ const resultLabel = isError ? pc.red("Error") : pc.green("Result");
1694
+ const topPads = Math.max(0, width - 4 - topText.length);
1695
+ console.log(`
1696
+ ${pc.dim("\u2500\u2500")} ${resultLabel} ${pc.dim("\u2500".repeat(topPads))}`);
784
1697
  const content = result.content;
785
1698
  if (Array.isArray(content)) {
786
1699
  for (const item of content) {
787
1700
  if (item.type === "text") {
788
- console.log(item.text);
1701
+ console.log(isError ? pc.red(` \u2717 ${item.text}`) : ` ${item.text}`);
789
1702
  } else {
790
1703
  console.log(formatJson(item, 2));
791
1704
  }
@@ -793,14 +1706,555 @@ async function cmdToolsCall(target, interceptor, rest) {
793
1706
  } else {
794
1707
  console.log(formatJson(result, 2));
795
1708
  }
796
- console.log(pc.dim(` (${elapsed}ms)`));
1709
+ if (isError) {
1710
+ console.log(
1711
+ pc.yellow(
1712
+ ` \u{1F4A1} Tip: Check the tool arguments via 'tools/describe ${toolName}'
1713
+ or view the raw server stderr above.`
1714
+ )
1715
+ );
1716
+ }
1717
+ const elapsedStr = `${elapsed}ms`;
1718
+ const bottomPads = Math.max(0, width - 4 - elapsedStr.length);
1719
+ console.log(` ${pc.dim("\u2500".repeat(bottomPads))} ${pc.dim(elapsedStr)} ${pc.dim("\u2500\u2500")}`);
797
1720
  }
798
- function cmdStatus(target) {
799
- const s = target.getStatus();
800
- const uptimeStr = s.uptime >= 60 ? `${Math.floor(s.uptime / 60)}m ${(s.uptime % 60).toFixed(0)}s` : `${s.uptime.toFixed(1)}s`;
801
- const lastRespStr = s.lastResponseTime ? `${((Date.now() - s.lastResponseTime) / 1e3).toFixed(1)}s ago` : "never";
802
- console.log(pc.bold("\n Target Server Status"));
803
- console.log(` ${pc.dim("Connected:")} ${s.connected ? pc.green("yes") : pc.red("no")}`);
1721
+ async function interactiveArgPrompt(target, interceptor, toolName, clearPrevious = false) {
1722
+ const { tools } = await target.listTools();
1723
+ const tool = tools.find((t) => t.name === toolName);
1724
+ if (!tool) {
1725
+ console.log(pc.red(`Tool "${toolName}" not found.`));
1726
+ const suggestion = suggestCommand(
1727
+ toolName,
1728
+ tools.map((t) => t.name)
1729
+ );
1730
+ if (suggestion) {
1731
+ console.log(pc.yellow(`Did you mean ${pc.bold(suggestion)}?`));
1732
+ } else {
1733
+ console.log(pc.dim(`Available: ${tools.map((t) => t.name).join(", ")}`));
1734
+ }
1735
+ return null;
1736
+ }
1737
+ const schema = tool.inputSchema;
1738
+ const properties = schema.properties;
1739
+ if (!properties || Object.keys(properties).length === 0) {
1740
+ return {};
1741
+ }
1742
+ if (isScriptMode) {
1743
+ console.log(pc.yellow(` Tool "${toolName}" requires arguments.`));
1744
+ const scaffolded = scaffoldArgs(schema);
1745
+ console.log(pc.dim(` Usage: tools/call ${toolName} ${scaffolded}`));
1746
+ return null;
1747
+ }
1748
+ const required = schema.required ?? [];
1749
+ const allProps = Object.entries(properties);
1750
+ const requiredProps = allProps.filter(([name]) => required.includes(name));
1751
+ const optionalProps = allProps.filter(([name]) => !required.includes(name));
1752
+ const previousArgs = clearPrevious ? void 0 : lastToolArgsMap.get(toolName);
1753
+ console.log();
1754
+ console.log(` ${pc.bold(tool.name)}${tool.description ? pc.dim(` \u2014 ${tool.description}`) : ""}`);
1755
+ if (previousArgs) {
1756
+ console.log(pc.dim(` Previous: ${JSON.stringify(previousArgs)}`));
1757
+ console.log(pc.dim(" Press Enter to reuse values, or type to override."));
1758
+ }
1759
+ console.log();
1760
+ const collectedArgs = {};
1761
+ return await withSuspendedReadline(target, interceptor, async () => {
1762
+ const abortController = new AbortController();
1763
+ const onData = (data) => {
1764
+ if (data.toString() === "\x1B") {
1765
+ abortController.abort();
1766
+ }
1767
+ };
1768
+ process.stdin.on("data", onData);
1769
+ try {
1770
+ for (const [name, prop] of requiredProps) {
1771
+ printJsonTemplate(collectedArgs, allProps, name);
1772
+ const typeStr = prop.type ?? "any";
1773
+ const desc = prop.description ?? "";
1774
+ const prevVal = previousArgs?.[name];
1775
+ const label = desc ? `${name} ${pc.dim(`(${typeStr})`)} ${pc.dim(desc)}` : `${name} ${pc.dim(`(${typeStr})`)}`;
1776
+ const answerStr = await input(
1777
+ { message: label, default: prevVal !== void 0 ? String(prevVal) : void 0 },
1778
+ { signal: abortController.signal }
1779
+ );
1780
+ collectedArgs[name] = coerceValue(answerStr, typeStr);
1781
+ }
1782
+ if (optionalProps.length > 0) {
1783
+ const previouslyUsedOptionals = previousArgs ? optionalProps.filter(([name]) => name in previousArgs).map(([name]) => name) : [];
1784
+ const selectedNames = await checkbox(
1785
+ {
1786
+ message: "Select optional arguments to provide:",
1787
+ choices: optionalProps.map(([name, prop]) => {
1788
+ const typeStr = prop.type ?? "any";
1789
+ const desc = prop.description ?? "";
1790
+ return {
1791
+ name: desc ? `${name} (${typeStr}) - ${desc}` : `${name} (${typeStr})`,
1792
+ value: name,
1793
+ checked: previouslyUsedOptionals.includes(name)
1794
+ };
1795
+ })
1796
+ },
1797
+ { signal: abortController.signal }
1798
+ );
1799
+ const selectedOptionals = optionalProps.filter(([name]) => selectedNames.includes(name));
1800
+ for (const [name, prop] of selectedOptionals) {
1801
+ printJsonTemplate(collectedArgs, allProps, name);
1802
+ const typeStr = prop.type ?? "any";
1803
+ const desc = prop.description ?? "";
1804
+ const prevVal = previousArgs?.[name];
1805
+ const label = desc ? `${name} ${pc.dim(`(${typeStr})`)} ${pc.dim(desc)}` : `${name} ${pc.dim(`(${typeStr})`)}`;
1806
+ const answerStr = await input(
1807
+ { message: label, default: prevVal !== void 0 ? String(prevVal) : void 0 },
1808
+ { signal: abortController.signal }
1809
+ );
1810
+ collectedArgs[name] = coerceValue(answerStr, typeStr);
1811
+ }
1812
+ }
1813
+ console.log();
1814
+ console.log(pc.dim(` ${JSON.stringify(collectedArgs)}`));
1815
+ const shouldExecute = await confirm(
1816
+ { message: "Execute?", default: true },
1817
+ { signal: abortController.signal }
1818
+ );
1819
+ if (!shouldExecute) return null;
1820
+ return collectedArgs;
1821
+ } catch (err) {
1822
+ if (!isAbortError(err)) {
1823
+ throw err;
1824
+ }
1825
+ return null;
1826
+ } finally {
1827
+ process.stdin.off("data", onData);
1828
+ }
1829
+ });
1830
+ }
1831
+ function printJsonTemplate(filled, allProps, currentProp) {
1832
+ const parts = [];
1833
+ for (const [name] of allProps) {
1834
+ if (name in filled) {
1835
+ parts.push(`"${name}": ${pc.green(JSON.stringify(filled[name]))}`);
1836
+ } else if (name === currentProp) {
1837
+ parts.push(`"${name}": ${pc.yellow("\u2592")}`);
1838
+ } else {
1839
+ parts.push(`"${name}": ${pc.dim("\u2592")}`);
1840
+ }
1841
+ }
1842
+ console.log(pc.dim(" { ") + parts.join(pc.dim(", ")) + pc.dim(" }"));
1843
+ console.log();
1844
+ }
1845
+ function coerceValue(input3, type) {
1846
+ switch (type) {
1847
+ case "number":
1848
+ case "integer": {
1849
+ const n = Number(input3);
1850
+ return Number.isNaN(n) ? input3 : n;
1851
+ }
1852
+ case "boolean":
1853
+ return input3.toLowerCase() === "true" || input3 === "1";
1854
+ default:
1855
+ return input3;
1856
+ }
1857
+ }
1858
+ function question(rl, prompt) {
1859
+ return new Promise((resolve, reject) => {
1860
+ let aborted = false;
1861
+ const onKeypress = (_str, key) => {
1862
+ if (key && key.name === "escape") {
1863
+ aborted = true;
1864
+ cleanup();
1865
+ process.stdout.write("\x1B[2K\r");
1866
+ rl.write("", { ctrl: true, name: "u" });
1867
+ rl.write("\n");
1868
+ }
1869
+ };
1870
+ const cleanup = () => {
1871
+ if (process.stdin.isTTY) {
1872
+ process.stdin.removeListener("keypress", onKeypress);
1873
+ }
1874
+ };
1875
+ if (process.stdin.isTTY) {
1876
+ process.stdin.on("keypress", onKeypress);
1877
+ }
1878
+ rl.question(prompt, (answer) => {
1879
+ cleanup();
1880
+ if (aborted) {
1881
+ reject(new AbortFlowError());
1882
+ } else {
1883
+ resolve(answer);
1884
+ }
1885
+ });
1886
+ });
1887
+ }
1888
+ async function cmdToolsScaffold(target, rest) {
1889
+ const name = rest.trim();
1890
+ if (!name) {
1891
+ console.log(pc.yellow(" Usage: tools/scaffold <name>"));
1892
+ if (cachedToolNames.length > 0) {
1893
+ const preview = cachedToolNames.slice(0, 6);
1894
+ const more = cachedToolNames.length > 6 ? `, ... (${cachedToolNames.length} total)` : "";
1895
+ console.log(pc.dim(`
1896
+ Available tools: ${preview.join(", ")}${more}`));
1897
+ }
1898
+ return;
1899
+ }
1900
+ const { tools } = await target.listTools();
1901
+ const tool = tools.find((t) => t.name === name);
1902
+ if (!tool) {
1903
+ console.log(pc.red(`Tool "${name}" not found.`));
1904
+ const suggestion = suggestCommand(
1905
+ name,
1906
+ tools.map((t) => t.name)
1907
+ );
1908
+ if (suggestion) {
1909
+ console.log(pc.yellow(`Did you mean ${pc.bold(suggestion)}?`));
1910
+ } else {
1911
+ console.log(pc.dim(`Available: ${tools.map((t) => t.name).join(", ")}`));
1912
+ }
1913
+ return;
1914
+ }
1915
+ const scaffolded = scaffoldArgs(tool.inputSchema);
1916
+ console.log(pc.cyan("\n Ready-to-paste command:"));
1917
+ console.log(` tools/call ${name} ${scaffolded}
1918
+ `);
1919
+ }
1920
+ async function cmdResourcesList(target) {
1921
+ const { resources } = await target.listResources();
1922
+ if (resources.length === 0) {
1923
+ console.log(pc.dim(" No resources available."));
1924
+ return;
1925
+ }
1926
+ const uriWidth = Math.max(6, ...resources.map((r) => r.uri.length));
1927
+ console.log(pc.bold(` ${"URI".padEnd(uriWidth)} Name`));
1928
+ console.log(pc.dim(` ${"\u2500".repeat(uriWidth)} ${"\u2500".repeat(40)}`));
1929
+ for (const r of resources) {
1930
+ const uri = r.uri;
1931
+ const name = r.name ?? pc.dim("(unnamed)");
1932
+ console.log(` ${pc.green(uri.padEnd(uriWidth))} ${name}`);
1933
+ }
1934
+ console.log(pc.dim(`
1935
+ ${resources.length} resource(s) total.`));
1936
+ }
1937
+ async function cmdResourcesRead(target, rest) {
1938
+ const uri = rest.trim();
1939
+ if (!uri) {
1940
+ console.log(pc.yellow(" Usage: resources/read <uri>"));
1941
+ if (cachedResourceUris.length > 0) {
1942
+ const preview = cachedResourceUris.slice(0, 5);
1943
+ const more = cachedResourceUris.length > 5 ? `, ... (${cachedResourceUris.length} total)` : "";
1944
+ console.log(pc.dim(`
1945
+ Available resources: ${preview.join(", ")}${more}`));
1946
+ }
1947
+ return;
1948
+ }
1949
+ const result = await target.readResource({ uri });
1950
+ for (const item of result.contents) {
1951
+ if (item.text !== void 0) {
1952
+ console.log(item.text);
1953
+ } else if (item.blob !== void 0) {
1954
+ const mimeType = item.mimeType ?? "application/octet-stream";
1955
+ const sizeBytes = Buffer.from(item.blob, "base64").length;
1956
+ console.log(pc.dim(`[Binary: ${mimeType}, ${sizeBytes} bytes]`));
1957
+ } else {
1958
+ console.log(formatJson(item, 2));
1959
+ }
1960
+ }
1961
+ }
1962
+ async function cmdResourcesTemplates(target) {
1963
+ const { resourceTemplates } = await target.listResourceTemplates();
1964
+ if (resourceTemplates.length === 0) {
1965
+ console.log(pc.dim(" No resource templates available."));
1966
+ return;
1967
+ }
1968
+ const uriWidth = Math.max(
1969
+ 12,
1970
+ ...resourceTemplates.map((t) => t.uriTemplate.length)
1971
+ );
1972
+ console.log(pc.bold(` ${"URI Template".padEnd(uriWidth)} Name`));
1973
+ console.log(pc.dim(` ${"\u2500".repeat(uriWidth)} ${"\u2500".repeat(40)}`));
1974
+ for (const t of resourceTemplates) {
1975
+ const uriTemplate = t.uriTemplate;
1976
+ const name = t.name ?? pc.dim("(unnamed)");
1977
+ console.log(` ${pc.green(uriTemplate.padEnd(uriWidth))} ${name}`);
1978
+ }
1979
+ console.log(pc.dim(`
1980
+ ${resourceTemplates.length} template(s) total.`));
1981
+ }
1982
+ async function cmdPromptsList(target) {
1983
+ const { prompts } = await target.listPrompts();
1984
+ if (prompts.length === 0) {
1985
+ console.log(pc.dim(" No prompts available."));
1986
+ return;
1987
+ }
1988
+ const nameWidth = Math.max(8, ...prompts.map((p) => p.name.length));
1989
+ console.log(pc.bold(` ${"Name".padEnd(nameWidth)} Description`));
1990
+ console.log(pc.dim(` ${"\u2500".repeat(nameWidth)} ${"\u2500".repeat(50)}`));
1991
+ for (const p of prompts) {
1992
+ const desc = p.description ? p.description.length > 60 ? `${p.description.slice(0, 57)}...` : p.description : pc.dim("(no description)");
1993
+ console.log(` ${pc.green(p.name.padEnd(nameWidth))} ${desc}`);
1994
+ }
1995
+ console.log(pc.dim(`
1996
+ ${prompts.length} prompt(s) total.`));
1997
+ }
1998
+ async function cmdPromptsGet(target, rest) {
1999
+ const { toolName: promptName, jsonArgs } = parseCallArgs(rest);
2000
+ if (!promptName) {
2001
+ console.log(pc.yellow(" Usage: prompts/get <name> [json_args]"));
2002
+ if (cachedPromptNames.length > 0) {
2003
+ console.log(pc.dim(`
2004
+ Available prompts: ${cachedPromptNames.join(", ")}`));
2005
+ }
2006
+ return;
2007
+ }
2008
+ let promptArgs = {};
2009
+ if (jsonArgs) {
2010
+ try {
2011
+ promptArgs = JSON.parse(jsonArgs);
2012
+ } catch (err) {
2013
+ console.error(pc.red(`Invalid JSON: ${err.message}`));
2014
+ console.log(pc.dim(` Received: ${jsonArgs}`));
2015
+ return;
2016
+ }
2017
+ }
2018
+ const result = await target.getPrompt({ name: promptName, arguments: promptArgs });
2019
+ for (const msg of result.messages) {
2020
+ const role = msg.role === "user" ? pc.blue("user") : pc.magenta("assistant");
2021
+ const text = msg.content.text ?? JSON.stringify(msg.content);
2022
+ console.log(` ${pc.bold(role)}: ${text}`);
2023
+ }
2024
+ }
2025
+ async function cmdPing(target) {
2026
+ try {
2027
+ const elapsed = await target.ping();
2028
+ console.log(pc.green(` \u2713 Pong! Round-trip: ${elapsed}ms`));
2029
+ } catch (err) {
2030
+ console.error(pc.red(` \u2717 Ping failed: ${err.message}`));
2031
+ }
2032
+ }
2033
+ async function cmdLogLevel(target, rest) {
2034
+ const level = rest.trim().toLowerCase();
2035
+ if (!level) {
2036
+ console.log(pc.yellow(" Usage: log-level <level>"));
2037
+ console.log(pc.dim(` Valid levels: ${LOG_LEVELS.join(", ")}`));
2038
+ return;
2039
+ }
2040
+ if (!LOG_LEVELS.includes(level)) {
2041
+ const suggestion = suggestCommand(level, [...LOG_LEVELS]);
2042
+ const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
2043
+ console.log(pc.red(` Unknown log level: "${level}".${hint}`));
2044
+ console.log(pc.dim(` Valid levels: ${LOG_LEVELS.join(", ")}`));
2045
+ return;
2046
+ }
2047
+ try {
2048
+ await target.setLoggingLevel(level);
2049
+ console.log(pc.green(` \u2713 Logging level set to: ${level}`));
2050
+ } catch (err) {
2051
+ console.error(pc.red(` \u2717 Failed to set log level: ${err.message}`));
2052
+ const caps = target.getServerCapabilities();
2053
+ if (!caps?.logging) {
2054
+ console.log(pc.dim(" The server does not advertise logging support."));
2055
+ }
2056
+ }
2057
+ }
2058
+ function cmdHistory(target, rest) {
2059
+ const arg = rest.trim();
2060
+ if (arg === "clear") {
2061
+ target.clearHistory();
2062
+ console.log(pc.dim(" History cleared."));
2063
+ return;
2064
+ }
2065
+ const count = arg ? Number.parseInt(arg, 10) : 20;
2066
+ const records = target.getHistory(Number.isNaN(count) ? 20 : count);
2067
+ if (records.length === 0) {
2068
+ console.log(pc.dim(" No request history yet."));
2069
+ return;
2070
+ }
2071
+ console.log(pc.bold("\n Request History"));
2072
+ console.log(pc.dim(` ${"\u2500".repeat(70)}`));
2073
+ for (const rec of records) {
2074
+ const time = new Date(rec.timestamp).toLocaleTimeString();
2075
+ const dur = `${rec.durationMs}ms`;
2076
+ const hasError = rec.error ? pc.red(" \u2717") : "";
2077
+ console.log(
2078
+ ` ${pc.dim(`#${rec.id}`)} ${pc.dim(time)} ${pc.green(rec.method.padEnd(30))} ${pc.cyan(dur.padStart(8))}${hasError}`
2079
+ );
2080
+ }
2081
+ const total = target.getHistory().length;
2082
+ console.log(
2083
+ pc.dim(`
2084
+ Showing ${records.length} of ${total} total. Use "history clear" to reset.`)
2085
+ );
2086
+ console.log();
2087
+ }
2088
+ function cmdNotifications(target, rest) {
2089
+ const arg = rest.trim();
2090
+ if (arg === "clear") {
2091
+ target.clearNotifications();
2092
+ console.log(pc.dim(" Notifications cleared."));
2093
+ return;
2094
+ }
2095
+ const count = arg ? Number.parseInt(arg, 10) : 20;
2096
+ const records = target.getNotifications(Number.isNaN(count) ? 20 : count);
2097
+ if (records.length === 0) {
2098
+ console.log(pc.dim(" No notifications received yet."));
2099
+ console.log(pc.dim(" Notifications appear inline as they arrive from the server."));
2100
+ return;
2101
+ }
2102
+ console.log(pc.bold("\n Server Notifications"));
2103
+ console.log(pc.dim(` ${"\u2500".repeat(70)}`));
2104
+ for (const n of records) {
2105
+ const time = new Date(n.timestamp).toLocaleTimeString();
2106
+ const params = n.params ? ` ${pc.dim(JSON.stringify(n.params))}` : "";
2107
+ console.log(` ${pc.dim(time)} ${pc.yellow(n.method)}${params}`);
2108
+ }
2109
+ const total = target.getNotifications().length;
2110
+ console.log(
2111
+ pc.dim(`
2112
+ Showing ${records.length} of ${total} total. Use "notifications clear" to reset.`)
2113
+ );
2114
+ console.log();
2115
+ }
2116
+ async function cmdResourcesSubscribe(target, rest) {
2117
+ const uri = rest.trim();
2118
+ if (!uri) {
2119
+ console.log(pc.yellow(" Usage: resources/subscribe <uri>"));
2120
+ if (cachedResourceUris.length > 0) {
2121
+ console.log(pc.dim(` Available: ${cachedResourceUris.join(", ")}`));
2122
+ }
2123
+ return;
2124
+ }
2125
+ try {
2126
+ await target.subscribeResource({ uri });
2127
+ console.log(pc.green(` \u2713 Subscribed to: ${uri}`));
2128
+ console.log(pc.dim(" You'll see notifications when this resource changes."));
2129
+ } catch (err) {
2130
+ console.error(pc.red(` \u2717 Subscribe failed: ${err.message}`));
2131
+ }
2132
+ }
2133
+ async function cmdResourcesUnsubscribe(target, rest) {
2134
+ const uri = rest.trim();
2135
+ if (!uri) {
2136
+ console.log(pc.yellow(" Usage: resources/unsubscribe <uri>"));
2137
+ return;
2138
+ }
2139
+ try {
2140
+ await target.unsubscribeResource({ uri });
2141
+ console.log(pc.green(` \u2713 Unsubscribed from: ${uri}`));
2142
+ } catch (err) {
2143
+ console.error(pc.red(` \u2717 Unsubscribe failed: ${err.message}`));
2144
+ }
2145
+ }
2146
+ function cmdRootsList(target) {
2147
+ const roots = target.getRoots();
2148
+ if (roots.length === 0) {
2149
+ console.log(pc.dim(" No roots configured."));
2150
+ console.log(pc.dim(" Use roots/add <uri> [name] to add one."));
2151
+ return;
2152
+ }
2153
+ console.log(pc.bold("\n Client Roots"));
2154
+ for (const r of roots) {
2155
+ const name = r.name ? ` (${r.name})` : "";
2156
+ console.log(` ${pc.green(r.uri)}${pc.dim(name)}`);
2157
+ }
2158
+ console.log();
2159
+ }
2160
+ async function cmdRootsAdd(target, rest) {
2161
+ const parts = rest.trim().split(/\s+/);
2162
+ const uri = parts[0];
2163
+ const name = parts.slice(1).join(" ") || void 0;
2164
+ if (!uri) {
2165
+ console.log(pc.yellow(" Usage: roots/add <uri> [name]"));
2166
+ console.log(pc.dim(' Example: roots/add file:///Users/me/project "My Project"'));
2167
+ return;
2168
+ }
2169
+ await target.addRoot({ uri, name });
2170
+ console.log(pc.green(` \u2713 Root added: ${uri}`));
2171
+ console.log(pc.dim(" Server has been notified of the change."));
2172
+ }
2173
+ async function cmdRootsRemove(target, rest) {
2174
+ const uri = rest.trim();
2175
+ if (!uri) {
2176
+ console.log(pc.yellow(" Usage: roots/remove <uri>"));
2177
+ const roots = target.getRoots();
2178
+ if (roots.length > 0) {
2179
+ console.log(pc.dim(` Current roots: ${roots.map((r) => r.uri).join(", ")}`));
2180
+ }
2181
+ return;
2182
+ }
2183
+ const removed = await target.removeRoot(uri);
2184
+ if (removed) {
2185
+ console.log(pc.green(` \u2713 Root removed: ${uri}`));
2186
+ } else {
2187
+ console.log(pc.yellow(` Root not found: ${uri}`));
2188
+ }
2189
+ }
2190
+ async function cmdReconnect(target) {
2191
+ console.log(pc.cyan("\u27F3 Disconnecting..."));
2192
+ await target.close();
2193
+ await new Promise((resolve) => setTimeout(resolve, 200));
2194
+ console.log(pc.cyan("\u27F3 Reconnecting..."));
2195
+ const { command, args } = target.getStatus();
2196
+ console.log(pc.dim(` Command: ${command} ${args.join(" ")}`));
2197
+ try {
2198
+ await target.connect();
2199
+ const s = target.getStatus();
2200
+ console.log(pc.green(`\u2713 Reconnected (PID: ${s.pid})`));
2201
+ const { tools } = await target.listTools();
2202
+ console.log(pc.cyan(` ${tools.length} tool(s) available.
2203
+ `));
2204
+ await refreshCaches(target);
2205
+ } catch (err) {
2206
+ console.error(pc.red(`\u2717 Failed to reconnect: ${err.message}`));
2207
+ }
2208
+ }
2209
+ function cmdTiming() {
2210
+ if (callHistory.length === 0) {
2211
+ console.log(pc.dim(" No tool calls recorded yet."));
2212
+ return;
2213
+ }
2214
+ const groups = /* @__PURE__ */ new Map();
2215
+ for (const record of callHistory) {
2216
+ const list = groups.get(record.toolName) ?? [];
2217
+ list.push(record.durationMs);
2218
+ groups.set(record.toolName, list);
2219
+ }
2220
+ const nameWidth = Math.max(8, ...[...groups.keys()].map((n) => n.length));
2221
+ console.log(pc.bold("\n Tool Call Performance"));
2222
+ console.log(pc.dim(` ${"\u2500".repeat(nameWidth + 50)}`));
2223
+ let totalCalls = 0;
2224
+ let totalMs = 0;
2225
+ let slowestName = "";
2226
+ let slowestMs = 0;
2227
+ for (const [name, durations] of groups) {
2228
+ const count = durations.length;
2229
+ totalCalls += count;
2230
+ const sorted = [...durations].sort((a, b) => a - b);
2231
+ const avg = Math.round(sorted.reduce((a, b) => a + b, 0) / count);
2232
+ const p95 = sorted[Math.floor(count * 0.95)];
2233
+ const max = sorted[sorted.length - 1];
2234
+ totalMs += sorted.reduce((a, b) => a + b, 0);
2235
+ if (max > slowestMs) {
2236
+ slowestMs = max;
2237
+ slowestName = name;
2238
+ }
2239
+ console.log(
2240
+ ` ${pc.green(name.padEnd(nameWidth))} \xD7 ${count} avg: ${avg}ms p95: ${p95}ms max: ${max}ms`
2241
+ );
2242
+ }
2243
+ const overallAvg = Math.round(totalMs / totalCalls);
2244
+ console.log(
2245
+ pc.dim(
2246
+ `
2247
+ Total: ${totalCalls} call(s), avg ${overallAvg}ms, slowest: ${slowestName} (${slowestMs}ms)`
2248
+ )
2249
+ );
2250
+ console.log();
2251
+ }
2252
+ function cmdStatus(target) {
2253
+ const s = target.getStatus();
2254
+ const uptimeStr = s.uptime >= 60 ? `${Math.floor(s.uptime / 60)}m ${(s.uptime % 60).toFixed(0)}s` : `${s.uptime.toFixed(1)}s`;
2255
+ const lastRespStr = s.lastResponseTime ? `${((Date.now() - s.lastResponseTime) / 1e3).toFixed(1)}s ago` : "never";
2256
+ console.log(pc.bold("\n Target Server Status"));
2257
+ console.log(` ${pc.dim("Connected:")} ${s.connected ? pc.green("yes") : pc.red("no")}`);
804
2258
  console.log(` ${pc.dim("PID:")} ${s.pid ?? "N/A"}`);
805
2259
  console.log(` ${pc.dim("Uptime:")} ${uptimeStr}`);
806
2260
  console.log(` ${pc.dim("Last response:")} ${lastRespStr}`);
@@ -811,56 +2265,486 @@ function cmdStatus(target) {
811
2265
  }
812
2266
  function printHelp() {
813
2267
  console.log(`
814
- ${pc.bold("Available Commands:")}
2268
+ ${pc.bold("Tool Commands:")}
815
2269
 
816
2270
  ${pc.green("tools/list")} List all available tools
817
2271
  ${pc.green("tools/describe")} <name> Show a tool's input schema
818
- ${pc.green("tools/call")} <name> <json> [opts] Call a tool with JSON arguments
2272
+ ${pc.green("tools/call")} <name> [json] [opts] Call a tool (interactive if no json)
819
2273
  Options: ${pc.dim("--timeout <ms>")} Override default timeout (60s)
2274
+ ${pc.dim("--clear")} Ignore remembered argument defaults
2275
+ ${pc.green("tools/scaffold")} <name> Generate a template for a tool's arguments
2276
+ ${pc.green("tools/forget")} [name] Clear remembered interactive defaults
2277
+
2278
+ ${pc.bold("Resource Commands:")}
2279
+
2280
+ ${pc.green("resources/list")} List all available resources
2281
+ ${pc.green("resources/read")} <uri> Read a resource by URI
2282
+ ${pc.green("resources/templates")} List resource templates
2283
+ ${pc.green("resources/subscribe")} <uri> Subscribe to resource changes
2284
+ ${pc.green("resources/unsubscribe")} <uri> Unsubscribe from resource changes
2285
+
2286
+ ${pc.bold("Prompt Commands:")}
2287
+
2288
+ ${pc.green("prompts/list")} List all available prompts
2289
+ ${pc.green("prompts/get")} <name> [json_args] Get a prompt with arguments
2290
+
2291
+ ${pc.bold("Protocol Commands:")}
2292
+
2293
+ ${pc.green("ping")} Verify connection, show round-trip time
2294
+ ${pc.green("log-level")} <level> Set server logging verbosity
2295
+ ${pc.green("history")} [count|clear] Show request/response history
2296
+ ${pc.green("notifications")} [count|clear] Show server notifications
2297
+
2298
+ ${pc.bold("Roots Management:")}
2299
+
2300
+ ${pc.green("roots/list")} Show configured client roots
2301
+ ${pc.green("roots/add")} <uri> [name] Add a root directory
2302
+ ${pc.green("roots/remove")} <uri> Remove a root directory
2303
+
2304
+ ${pc.bold("Session Commands:")}
2305
+
2306
+ ${pc.green("!!")} / ${pc.green("last")} Re-run the last command
2307
+ ${pc.green("reconnect")} Disconnect and reconnect to the server
2308
+ ${pc.green("timing")} Show tool call performance stats
820
2309
  ${pc.green("status")} Show target server status
821
2310
  ${pc.green("help")} Show this help
822
2311
  ${pc.green("exit")} / ${pc.green("quit")} Disconnect and exit
823
2312
 
2313
+ ${pc.bold("Shortcuts:")}
2314
+
2315
+ ${pc.green("tl")} tools/list ${pc.green("rl")} resources/list ${pc.green("pl")} prompts/list
2316
+ ${pc.green("td")} tools/describe ${pc.green("rr")} resources/read ${pc.green("pg")} prompts/get
2317
+ ${pc.green("tc")} tools/call ${pc.green("rt")} resources/templates
2318
+ ${pc.green("ts")} tools/scaffold ${pc.green("rs")} resources/subscribe
2319
+ ${pc.green("ru")} resources/unsubscribe
2320
+
824
2321
  ${pc.dim("Lines starting with # are treated as comments.")}
825
2322
  ${pc.dim('JSON arguments can contain spaces: tools/call say {"message": "hello world"}')}
2323
+ ${pc.dim("Run tools/call <name> without JSON for interactive argument prompting.")}
2324
+ ${pc.dim("Use tools/call <name> --clear to ignore remembered defaults.")}
826
2325
  `);
827
2326
  }
828
2327
  async function readScriptLines(filepath) {
829
2328
  const content = await readFile(filepath, "utf-8");
830
2329
  return content.split("\n");
831
2330
  }
2331
+ function cmdToolsForget(rest) {
2332
+ const toolName = rest.trim();
2333
+ if (toolName) {
2334
+ if (lastToolArgsMap.has(toolName)) {
2335
+ lastToolArgsMap.delete(toolName);
2336
+ console.log(pc.green(` Cleared remembered args for ${pc.bold(toolName)}.`));
2337
+ } else {
2338
+ console.log(pc.yellow(` No remembered args for "${toolName}".`));
2339
+ }
2340
+ } else {
2341
+ const count = lastToolArgsMap.size;
2342
+ lastToolArgsMap.clear();
2343
+ console.log(pc.green(` Cleared remembered args for ${count} tool${count === 1 ? "" : "s"}.`));
2344
+ }
2345
+ }
2346
+ async function cmdExplore(target, interceptor) {
2347
+ const choices = [];
2348
+ const caps = target.getServerCapabilities() ?? {};
2349
+ try {
2350
+ const { tools } = await target.listTools();
2351
+ for (const t of tools) {
2352
+ choices.push({
2353
+ name: `\u{1F6E0}\uFE0F Tool: ${t.name}`,
2354
+ value: { type: "tool", name: t.name },
2355
+ description: t.description || `Call the ${t.name} tool`
2356
+ });
2357
+ }
2358
+ } catch {
2359
+ }
2360
+ if (caps.resources) {
2361
+ try {
2362
+ const { resources } = await target.listResources();
2363
+ for (const r of resources) {
2364
+ choices.push({
2365
+ name: `\u{1F4C4} Resource: ${r.name || r.uri}`,
2366
+ value: { type: "resource", uri: r.uri },
2367
+ description: r.description || `Read resource ${r.uri}`
2368
+ });
2369
+ }
2370
+ } catch {
2371
+ }
2372
+ }
2373
+ if (caps.prompts) {
2374
+ try {
2375
+ const { prompts } = await target.listPrompts();
2376
+ for (const p of prompts) {
2377
+ choices.push({
2378
+ name: `\u{1F4AC} Prompt: ${p.name}`,
2379
+ value: { type: "prompt", name: p.name },
2380
+ description: p.description || `Get the ${p.name} prompt`
2381
+ });
2382
+ }
2383
+ } catch {
2384
+ }
2385
+ }
2386
+ if (choices.length === 0) {
2387
+ console.log(pc.yellow("No tools, resources, or prompts found."));
2388
+ return;
2389
+ }
2390
+ if (!process.stdin.isTTY) {
2391
+ console.log(pc.bold("\n Server Capabilities\n"));
2392
+ for (let i = 0; i < choices.length; i++) {
2393
+ console.log(
2394
+ ` ${pc.cyan(String(i + 1).padStart(2))}. ${choices[i].name} ${pc.dim(choices[i].description)}`
2395
+ );
2396
+ }
2397
+ console.log();
2398
+ console.log(
2399
+ pc.dim(" Non-interactive mode \u2014 use specific commands instead (e.g., tools/call <name>).")
2400
+ );
2401
+ return;
2402
+ }
2403
+ try {
2404
+ const answer = await search({
2405
+ message: "Explore server capabilities:",
2406
+ source: async (term) => {
2407
+ if (!term) return choices;
2408
+ const lower = term.toLowerCase();
2409
+ return choices.filter(
2410
+ (c) => c.name.toLowerCase().includes(lower) || c.description.toLowerCase().includes(lower)
2411
+ );
2412
+ }
2413
+ });
2414
+ if (answer.type === "tool") {
2415
+ await cmdToolsCall(target, interceptor, answer.name);
2416
+ } else if (answer.type === "resource") {
2417
+ await cmdResourcesRead(target, answer.uri);
2418
+ } else if (answer.type === "prompt") {
2419
+ await cmdPromptsGet(target, answer.name);
2420
+ }
2421
+ } catch (err) {
2422
+ if (!isAbortError(err)) {
2423
+ throw err;
2424
+ }
2425
+ }
2426
+ }
832
2427
 
833
2428
  // src/server.ts
2429
+ import { createHash } from "crypto";
834
2430
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
835
2431
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
836
2432
  import { z } from "zod";
2433
+
2434
+ // src/config-scanner.ts
2435
+ import { existsSync } from "fs";
2436
+ import { readFile as readFile2 } from "fs/promises";
2437
+ import { homedir } from "os";
2438
+ import path from "path";
2439
+ import process2 from "process";
2440
+ import { input as input2, select as select2 } from "@inquirer/prompts";
2441
+ function getConfigPaths() {
2442
+ const home = homedir();
2443
+ const cwd = process2.cwd();
2444
+ const isWin = process2.platform === "win32";
2445
+ const isMac = process2.platform === "darwin";
2446
+ const appData = process2.env.APPDATA || path.join(home, "AppData", "Roaming");
2447
+ const localAppData = process2.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
2448
+ let claudeDesktopGlob;
2449
+ if (isWin) {
2450
+ claudeDesktopGlob = path.join(appData, "Claude", "claude_desktop_config.json");
2451
+ } else if (isMac) {
2452
+ claudeDesktopGlob = path.join(
2453
+ home,
2454
+ "Library",
2455
+ "Application Support",
2456
+ "Claude",
2457
+ "claude_desktop_config.json"
2458
+ );
2459
+ } else {
2460
+ claudeDesktopGlob = path.join(home, ".config", "Claude", "claude_desktop_config.json");
2461
+ }
2462
+ return [
2463
+ { source: "Cursor (Global)", file: path.join(home, ".cursor", "mcp.json") },
2464
+ { source: "Cursor (Project)", file: path.join(cwd, ".cursor", "mcp.json") },
2465
+ { source: "Windsurf", file: path.join(home, ".codeium", "windsurf", "mcp_config.json") },
2466
+ { source: "Claude Desktop", file: claudeDesktopGlob },
2467
+ { source: "Cline", file: path.join(home, "Documents", "Cline", "MCP", "mcp.json") },
2468
+ { source: "VS Code (Project)", file: path.join(cwd, ".vscode", "mcp.json") },
2469
+ {
2470
+ source: "VS Code (Global)",
2471
+ file: path.join(
2472
+ isMac ? path.join(home, "Library", "Application Support") : isWin ? appData : path.join(home, ".config"),
2473
+ "Code",
2474
+ "User",
2475
+ "settings.json"
2476
+ )
2477
+ },
2478
+ { source: "Copilot CLI (Global)", file: path.join(home, ".copilot", "mcp-config.json") },
2479
+ { source: "Gemini CLI (Global)", file: path.join(home, ".gemini", "settings.json") },
2480
+ { source: "Gemini CLI (Project)", file: path.join(cwd, ".gemini", "settings.json") },
2481
+ { source: "Claude Code (Global)", file: path.join(home, ".claude.json") },
2482
+ { source: "Claude Code (Project)", file: path.join(cwd, ".mcp.json") },
2483
+ { source: "Antigravity", file: path.join(home, ".gemini", "antigravity", "mcp_config.json") }
2484
+ ];
2485
+ }
2486
+ async function discoverServers() {
2487
+ const servers = [];
2488
+ const paths = getConfigPaths();
2489
+ for (const { source, file } of paths) {
2490
+ if (!existsSync(file)) continue;
2491
+ try {
2492
+ const content = await readFile2(file, "utf8");
2493
+ const json = JSON.parse(content);
2494
+ let mcpServers;
2495
+ if (json.mcpServers && typeof json.mcpServers === "object") {
2496
+ mcpServers = json.mcpServers;
2497
+ } else if (json.mcp?.servers && typeof json.mcp.servers === "object") {
2498
+ mcpServers = json.mcp.servers;
2499
+ } else if (json.servers && typeof json.servers === "object") {
2500
+ mcpServers = json.servers;
2501
+ }
2502
+ if (mcpServers) {
2503
+ for (const [name, config] of Object.entries(mcpServers)) {
2504
+ if (config.command) {
2505
+ servers.push({ name, config, source });
2506
+ }
2507
+ }
2508
+ }
2509
+ } catch {
2510
+ }
2511
+ }
2512
+ return servers;
2513
+ }
2514
+ async function pickDiscoveredServer() {
2515
+ const servers = await discoverServers();
2516
+ if (servers.length === 0) {
2517
+ return null;
2518
+ }
2519
+ const uniqueServers = /* @__PURE__ */ new Map();
2520
+ for (const s of servers) {
2521
+ const key = `${s.name}::${s.config.command}::${(s.config.args || []).join(" ")}`;
2522
+ if (!uniqueServers.has(key)) {
2523
+ uniqueServers.set(key, s);
2524
+ } else {
2525
+ if (s.source.includes("Project")) {
2526
+ uniqueServers.set(key, s);
2527
+ }
2528
+ }
2529
+ }
2530
+ const choices = Array.from(uniqueServers.values()).map((s) => {
2531
+ return {
2532
+ name: `${s.name} (from ${s.source})`,
2533
+ value: s,
2534
+ description: `${s.config.command} ${(s.config.args || []).join(" ")}`
2535
+ };
2536
+ });
2537
+ choices.push({
2538
+ name: "Enter custom server command...",
2539
+ value: "CUSTOM",
2540
+ description: "Manually specify a command, e.g. 'npx foo' or 'python server.py'"
2541
+ });
2542
+ try {
2543
+ const answer = await select2({
2544
+ message: "Select an MCP server to launch:",
2545
+ choices,
2546
+ pageSize: 15
2547
+ });
2548
+ if (answer === "CUSTOM") {
2549
+ const customCommand = await input2({ message: "Command to spawn target MCP server:" });
2550
+ if (!customCommand.trim()) return null;
2551
+ const parts = customCommand.trim().match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((p) => p.replace(/^["']|["']$/g, ""));
2552
+ if (!parts || parts.length === 0) return null;
2553
+ return {
2554
+ name: "Custom",
2555
+ config: { command: parts[0], args: parts.slice(1) },
2556
+ source: "Manual"
2557
+ };
2558
+ }
2559
+ return answer;
2560
+ } catch {
2561
+ return null;
2562
+ }
2563
+ }
2564
+
2565
+ // src/server.ts
2566
+ function hashDefinition(obj) {
2567
+ return createHash("md5").update(JSON.stringify(obj)).digest("hex").slice(0, 12);
2568
+ }
2569
+ function computeDiff(prev, curr) {
2570
+ const prevMap = new Map(prev.map((p) => [p.name, p.hash]));
2571
+ const currMap = new Map(curr.map((c) => [c.name, c.hash]));
2572
+ const added = [];
2573
+ const removed = [];
2574
+ const modified = [];
2575
+ for (const [name, hash] of currMap) {
2576
+ if (!prevMap.has(name)) {
2577
+ added.push(name);
2578
+ } else if (prevMap.get(name) !== hash) {
2579
+ modified.push(name);
2580
+ }
2581
+ }
2582
+ for (const name of prevMap.keys()) {
2583
+ if (!currMap.has(name)) {
2584
+ removed.push(name);
2585
+ }
2586
+ }
2587
+ return { added, removed, modified };
2588
+ }
2589
+ function computeResourceDiff(prev, curr) {
2590
+ const prevUris = new Set(prev.map((r) => r.uri));
2591
+ const currUris = new Set(curr.map((r) => r.uri));
2592
+ const added = [...currUris].filter((u) => !prevUris.has(u));
2593
+ const removed = [...prevUris].filter((u) => !currUris.has(u));
2594
+ return { added, removed, modified: [] };
2595
+ }
2596
+ function formatDiffLine(label, diff) {
2597
+ const parts = [];
2598
+ if (diff.added.length > 0) parts.push(`+${diff.added.length} added`);
2599
+ if (diff.modified.length > 0) parts.push(`~${diff.modified.length} modified`);
2600
+ if (diff.removed.length > 0) parts.push(`-${diff.removed.length} removed`);
2601
+ if (parts.length === 0) return ` ${label}: unchanged`;
2602
+ const details = parts.join(", ");
2603
+ const names = [...diff.added, ...diff.modified, ...diff.removed];
2604
+ return ` ${label}: ${details} (${names.join(", ")})`;
2605
+ }
837
2606
  async function startServer(opts) {
838
2607
  let target = null;
2608
+ let previousSnapshot = null;
839
2609
  const interceptor = new ResponseInterceptor({
840
2610
  outDir: opts.outDir,
841
2611
  defaultTimeoutMs: opts.timeoutMs,
842
2612
  maxTextLength: opts.maxTextLength
843
2613
  });
844
2614
  const mcpServer = new McpServer(
845
- { name: "run-mcp", version: "1.3.1" },
2615
+ { name: "run-mcp", version: "1.4.0" },
846
2616
  {
847
2617
  capabilities: {
848
2618
  tools: {}
849
2619
  }
850
2620
  }
851
2621
  );
2622
+ async function takeSnapshot() {
2623
+ if (!target?.connected) return {};
2624
+ const snap = {};
2625
+ const caps = target.getServerCapabilities() ?? {};
2626
+ if (caps.tools) {
2627
+ try {
2628
+ const { tools } = await target.listTools();
2629
+ snap.tools = tools.map((t) => ({
2630
+ name: t.name,
2631
+ hash: hashDefinition({
2632
+ description: t.description,
2633
+ inputSchema: t.inputSchema
2634
+ })
2635
+ }));
2636
+ } catch {
2637
+ }
2638
+ }
2639
+ if (caps.resources) {
2640
+ try {
2641
+ const { resources } = await target.listResources();
2642
+ snap.resources = resources.map((r) => ({
2643
+ uri: r.uri,
2644
+ name: r.name ?? ""
2645
+ }));
2646
+ } catch {
2647
+ }
2648
+ }
2649
+ if (caps.prompts) {
2650
+ try {
2651
+ const { prompts } = await target.listPrompts();
2652
+ snap.prompts = prompts.map((p) => ({
2653
+ name: p.name,
2654
+ hash: hashDefinition({ description: p.description })
2655
+ }));
2656
+ } catch {
2657
+ }
2658
+ }
2659
+ return snap;
2660
+ }
2661
+ function computeSnapshotDiff(current) {
2662
+ if (!previousSnapshot) return [];
2663
+ const lines = ["", "Changes since last connection:"];
2664
+ if (current.tools && previousSnapshot.tools) {
2665
+ lines.push(formatDiffLine("Tools", computeDiff(previousSnapshot.tools, current.tools)));
2666
+ }
2667
+ if (current.resources && previousSnapshot.resources) {
2668
+ lines.push(
2669
+ formatDiffLine(
2670
+ "Resources",
2671
+ computeResourceDiff(previousSnapshot.resources, current.resources)
2672
+ )
2673
+ );
2674
+ }
2675
+ if (current.prompts && previousSnapshot.prompts) {
2676
+ lines.push(formatDiffLine("Prompts", computeDiff(previousSnapshot.prompts, current.prompts)));
2677
+ }
2678
+ const hasChanges = lines.some(
2679
+ (l) => l.includes("+") || l.includes("~") || l.includes("-removed")
2680
+ );
2681
+ if (!hasChanges) {
2682
+ return ["", "Changes since last connection: none"];
2683
+ }
2684
+ return lines;
2685
+ }
2686
+ async function ensureConnected(command, args, env) {
2687
+ if (target?.connected) return null;
2688
+ if (!command) {
2689
+ return "Not connected to a target server. Provide command/args to auto-connect, or call connect_to_mcp first.";
2690
+ }
2691
+ if (target) {
2692
+ await target.close();
2693
+ target = null;
2694
+ }
2695
+ if (env) {
2696
+ for (const [key, value] of Object.entries(env)) {
2697
+ process.env[key] = value;
2698
+ }
2699
+ }
2700
+ target = new TargetManager(command, args ?? []);
2701
+ await target.connect();
2702
+ return null;
2703
+ }
2704
+ async function buildIncludeData(include) {
2705
+ if (!target?.connected || include.length === 0) return [];
2706
+ const lines = [];
2707
+ if (include.includes("tools")) {
2708
+ try {
2709
+ const { tools } = await target.listTools();
2710
+ lines.push("", "--- Tools ---", JSON.stringify(tools, null, 2));
2711
+ } catch (err) {
2712
+ lines.push("", "--- Tools ---", `Error: ${err.message}`);
2713
+ }
2714
+ }
2715
+ if (include.includes("resources")) {
2716
+ try {
2717
+ const { resources } = await target.listResources();
2718
+ lines.push("", "--- Resources ---", JSON.stringify(resources, null, 2));
2719
+ } catch (err) {
2720
+ lines.push("", "--- Resources ---", `Error: ${err.message}`);
2721
+ }
2722
+ }
2723
+ if (include.includes("prompts")) {
2724
+ try {
2725
+ const { prompts } = await target.listPrompts();
2726
+ lines.push("", "--- Prompts ---", JSON.stringify(prompts, null, 2));
2727
+ } catch (err) {
2728
+ lines.push("", "--- Prompts ---", `Error: ${err.message}`);
2729
+ }
2730
+ }
2731
+ return lines;
2732
+ }
852
2733
  mcpServer.registerTool(
853
2734
  "connect_to_mcp",
854
2735
  {
855
2736
  title: "Connect to MCP Server",
856
- description: "Spawn and connect to a local MCP server process. Use this to test an MCP server you're building. Only one connection at a time \u2014 call disconnect_from_mcp first if already connected.",
2737
+ description: "Spawn and connect to a local MCP server process. Use this to test an MCP server you're building. Only one connection at a time \u2014 call disconnect_from_mcp first if already connected. Use the 'include' parameter to get tools/resources/prompts in the response, saving round trips.",
857
2738
  inputSchema: {
858
2739
  command: z.string().describe("Command to run (e.g. 'node', 'python', 'npx')"),
859
2740
  args: z.array(z.string()).optional().describe("Arguments to pass (e.g. ['src/index.js'] or ['-y', 'some-server'])"),
860
- env: z.record(z.string()).optional().describe("Extra environment variables for the child process")
2741
+ env: z.record(z.string()).optional().describe("Extra environment variables for the child process"),
2742
+ include: z.array(z.enum(["tools", "resources", "prompts"])).optional().describe(
2743
+ "Primitives to include in the response. Saves round trips vs calling list_mcp_primitives separately. On reconnect, also shows a diff of what changed since the last connection."
2744
+ )
861
2745
  }
862
2746
  },
863
- async ({ command, args, env }) => {
2747
+ async ({ command, args, env, include }) => {
864
2748
  if (target?.connected) {
865
2749
  return {
866
2750
  content: [
@@ -903,9 +2787,21 @@ async function startServer(opts) {
903
2787
  `Capabilities: ${capSummary.join(", ") || "none"}`,
904
2788
  `Tools available: ${toolCount}`,
905
2789
  "",
906
- "Use list_mcp_tools, call_mcp_tool, list_mcp_resources, etc. to interact with it.",
2790
+ "Use call_mcp_primitive to call tools, read resources, or get prompts.",
907
2791
  "Use disconnect_from_mcp when done, or to reconnect after code changes."
908
2792
  ];
2793
+ const currentSnapshot = await takeSnapshot();
2794
+ if (previousSnapshot && include && include.length > 0) {
2795
+ lines.push(...computeSnapshotDiff(currentSnapshot));
2796
+ }
2797
+ previousSnapshot = currentSnapshot;
2798
+ if (include && include.length > 0) {
2799
+ lines.push(...await buildIncludeData(include));
2800
+ }
2801
+ const instructions = target.getInstructions();
2802
+ if (instructions) {
2803
+ lines.push("", "--- Server Instructions ---", instructions);
2804
+ }
909
2805
  return { content: [{ type: "text", text: lines.join("\n") }] };
910
2806
  } catch (err) {
911
2807
  target = null;
@@ -981,12 +2877,20 @@ Check that the command is correct and the server starts without errors. You can
981
2877
  }
982
2878
  );
983
2879
  mcpServer.registerTool(
984
- "list_mcp_tools",
2880
+ "list_mcp_primitives",
985
2881
  {
986
- title: "List MCP Tools",
987
- description: "List all tools exposed by the connected MCP server, including descriptions, input schemas, and annotations."
2882
+ title: "List MCP Primitives",
2883
+ description: "List tools, resources, and/or prompts on the connected MCP server. Specify which types to include. Defaults to all available. Use 'name' to filter to a specific item (e.g. describe a single tool's schema).",
2884
+ inputSchema: {
2885
+ type: z.array(z.enum(["tools", "resources", "prompts"])).optional().describe(
2886
+ "Which primitives to list. Defaults to all that the server supports. Example: ['tools'] to list only tools."
2887
+ ),
2888
+ name: z.string().optional().describe(
2889
+ "Filter to a specific item by name. For tools: matches tool name. For resources: matches URI. For prompts: matches prompt name. Returns the full schema/details for just that item."
2890
+ )
2891
+ }
988
2892
  },
989
- async () => {
2893
+ async ({ type, name }) => {
990
2894
  if (!target?.connected) {
991
2895
  return {
992
2896
  content: [
@@ -998,239 +2902,272 @@ Check that the command is correct and the server starts without errors. You can
998
2902
  isError: true
999
2903
  };
1000
2904
  }
1001
- try {
1002
- const result = await target.listTools();
1003
- return {
1004
- content: [
1005
- {
1006
- type: "text",
1007
- text: JSON.stringify(result.tools, null, 2)
2905
+ const caps = target.getServerCapabilities() ?? {};
2906
+ const requested = type ?? ["tools", "resources", "prompts"];
2907
+ const sections = [];
2908
+ if (requested.includes("tools") && caps.tools) {
2909
+ try {
2910
+ const result = await target.listTools();
2911
+ let tools = result.tools;
2912
+ if (name) {
2913
+ tools = tools.filter((t) => t.name === name);
2914
+ if (tools.length === 0) {
2915
+ const available = result.tools.map((t) => t.name).join(", ");
2916
+ sections.push("--- Tools ---", `Tool "${name}" not found.
2917
+ Available: ${available}`);
2918
+ } else {
2919
+ sections.push("--- Tools ---", JSON.stringify(tools[0], null, 2));
1008
2920
  }
1009
- ]
1010
- };
1011
- } catch (err) {
1012
- return {
1013
- content: [{ type: "text", text: `Error listing tools: ${err.message}` }],
1014
- isError: true
1015
- };
1016
- }
1017
- }
1018
- );
1019
- mcpServer.registerTool(
1020
- "describe_mcp_tool",
1021
- {
1022
- title: "Describe MCP Tool",
1023
- description: "Get the description and input schema for a specific tool on the connected server.",
1024
- inputSchema: {
1025
- name: z.string().describe("Name of the tool to describe")
1026
- }
1027
- },
1028
- async ({ name }) => {
1029
- if (!target?.connected) {
1030
- return {
1031
- content: [
1032
- { type: "text", text: "No target server connected. Use connect_to_mcp first." }
1033
- ],
1034
- isError: true
1035
- };
2921
+ } else {
2922
+ sections.push("--- Tools ---", JSON.stringify(tools, null, 2));
2923
+ }
2924
+ } catch (err) {
2925
+ sections.push("--- Tools ---", `Error: ${err.message}`);
2926
+ }
1036
2927
  }
1037
- try {
1038
- const result = await target.listTools();
1039
- const tool = result.tools.find((t) => t.name === name);
1040
- if (!tool) {
1041
- const available = result.tools.map((t) => t.name).join(", ");
1042
- return {
1043
- content: [
1044
- { type: "text", text: `Tool "${name}" not found.
1045
- Available tools: ${available}` }
1046
- ],
1047
- isError: true
1048
- };
2928
+ if (requested.includes("resources") && caps.resources) {
2929
+ try {
2930
+ const result = await target.listResources();
2931
+ let resources = result.resources;
2932
+ if (name) {
2933
+ resources = resources.filter((r) => r.uri === name || r.name === name);
2934
+ if (resources.length === 0) {
2935
+ const available = result.resources.map((r) => r.uri).join(", ");
2936
+ sections.push(
2937
+ "--- Resources ---",
2938
+ `Resource "${name}" not found.
2939
+ Available: ${available}`
2940
+ );
2941
+ } else {
2942
+ sections.push("--- Resources ---", JSON.stringify(resources[0], null, 2));
2943
+ }
2944
+ } else {
2945
+ sections.push("--- Resources ---", JSON.stringify(resources, null, 2));
2946
+ }
2947
+ } catch (err) {
2948
+ sections.push("--- Resources ---", `Error: ${err.message}`);
1049
2949
  }
1050
- return {
1051
- content: [
1052
- { type: "text", text: JSON.stringify(tool, null, 2) }
1053
- ]
1054
- };
1055
- } catch (err) {
1056
- return {
1057
- content: [{ type: "text", text: `Error describing tool: ${err.message}` }],
1058
- isError: true
1059
- };
1060
2950
  }
1061
- }
1062
- );
1063
- mcpServer.registerTool(
1064
- "call_mcp_tool",
1065
- {
1066
- title: "Call MCP Tool",
1067
- description: "Call a tool on the connected MCP server. Responses go through the interceptor: images/audio are saved to disk, timeouts are enforced, and oversized text is truncated.",
1068
- inputSchema: {
1069
- name: z.string().describe("Name of the tool to call"),
1070
- arguments: z.record(z.unknown()).optional().describe("Arguments to pass to the tool (as a JSON object)"),
1071
- timeout_ms: z.number().optional().describe("Timeout for this specific call in milliseconds (overrides default)")
2951
+ if (requested.includes("prompts") && caps.prompts) {
2952
+ try {
2953
+ const result = await target.listPrompts();
2954
+ let prompts = result.prompts;
2955
+ if (name) {
2956
+ prompts = prompts.filter((p) => p.name === name);
2957
+ if (prompts.length === 0) {
2958
+ const available = result.prompts.map((p) => p.name).join(", ");
2959
+ sections.push(
2960
+ "--- Prompts ---",
2961
+ `Prompt "${name}" not found.
2962
+ Available: ${available}`
2963
+ );
2964
+ } else {
2965
+ sections.push("--- Prompts ---", JSON.stringify(prompts[0], null, 2));
2966
+ }
2967
+ } else {
2968
+ sections.push("--- Prompts ---", JSON.stringify(prompts, null, 2));
2969
+ }
2970
+ } catch (err) {
2971
+ sections.push("--- Prompts ---", `Error: ${err.message}`);
2972
+ }
1072
2973
  }
1073
- },
1074
- async ({ name, arguments: toolArgs, timeout_ms }) => {
1075
- if (!target?.connected) {
2974
+ if (sections.length === 0) {
1076
2975
  return {
1077
2976
  content: [
1078
2977
  {
1079
2978
  type: "text",
1080
- text: "No target server connected. Use connect_to_mcp first."
2979
+ text: "No matching primitives found. The server may not support the requested types."
1081
2980
  }
1082
- ],
1083
- isError: true
1084
- };
1085
- }
1086
- try {
1087
- const result = await interceptor.callTool(
1088
- target,
1089
- name,
1090
- toolArgs ?? {},
1091
- timeout_ms
1092
- );
1093
- return result;
1094
- } catch (err) {
1095
- return {
1096
- content: [{ type: "text", text: `Error: ${err.message}` }],
1097
- isError: true
2981
+ ]
1098
2982
  };
1099
2983
  }
2984
+ return {
2985
+ content: [{ type: "text", text: sections.join("\n") }]
2986
+ };
1100
2987
  }
1101
2988
  );
1102
2989
  mcpServer.registerTool(
1103
- "list_mcp_resources",
2990
+ "list_available_mcp_servers",
1104
2991
  {
1105
- title: "List MCP Resources",
1106
- description: "List all resources exposed by the connected MCP server."
2992
+ title: "List Available MCP Servers",
2993
+ description: "Scans common configuration files (VS Code, Claude Desktop, Cursor, etc.) and returns a list of local MCP servers that the user has configured on their machine. This is useful for discovering what other servers are available to connect to."
1107
2994
  },
1108
2995
  async () => {
1109
- if (!target?.connected) {
1110
- return {
1111
- content: [
1112
- {
1113
- type: "text",
1114
- text: "No target server connected. Use connect_to_mcp first."
1115
- }
1116
- ],
1117
- isError: true
1118
- };
1119
- }
1120
2996
  try {
1121
- const result = await target.listResources();
1122
- return {
1123
- content: [{ type: "text", text: JSON.stringify(result.resources, null, 2) }]
1124
- };
2997
+ const servers = await discoverServers();
2998
+ if (servers.length === 0) {
2999
+ return {
3000
+ content: [{ type: "text", text: "No configured MCP servers discovered in common locations." }]
3001
+ };
3002
+ }
3003
+ const lines = ["Discovered the following MCP server configurations:"];
3004
+ const uniqueServers = /* @__PURE__ */ new Map();
3005
+ for (const s of servers) {
3006
+ const key = `${s.name}::${s.config.command}::${(s.config.args || []).join(" ")}`;
3007
+ if (!uniqueServers.has(key)) {
3008
+ uniqueServers.set(key, s);
3009
+ } else if (s.source.includes("Project")) {
3010
+ uniqueServers.set(key, s);
3011
+ }
3012
+ }
3013
+ const list = Array.from(uniqueServers.values()).map((s) => ({
3014
+ name: s.name,
3015
+ source: s.source,
3016
+ command: s.config.command,
3017
+ args: s.config.args || []
3018
+ }));
3019
+ lines.push(JSON.stringify(list, null, 2));
3020
+ return { content: [{ type: "text", text: lines.join("\n\n") }] };
1125
3021
  } catch (err) {
1126
3022
  return {
1127
- content: [{ type: "text", text: `Error listing resources: ${err.message}` }],
3023
+ content: [{ type: "text", text: `Error discovering servers: ${err.message}` }],
1128
3024
  isError: true
1129
3025
  };
1130
3026
  }
1131
3027
  }
1132
3028
  );
1133
3029
  mcpServer.registerTool(
1134
- "read_mcp_resource",
3030
+ "call_mcp_primitive",
1135
3031
  {
1136
- title: "Read MCP Resource",
1137
- description: "Read a specific resource by URI from the connected MCP server.",
3032
+ title: "Call MCP Primitive",
3033
+ description: "Call a tool, read a resource, or get a prompt on a target MCP server. If not connected, provide command/args and a connection will be opened automatically. Use disconnect_after to tear down the connection when done, or leave it open (default) for subsequent calls.",
1138
3034
  inputSchema: {
1139
- uri: z.string().describe("URI of the resource to read (e.g. 'docs://readme')")
3035
+ // What to call
3036
+ type: z.enum(["tool", "resource", "prompt"]).describe("The MCP primitive type to invoke"),
3037
+ name: z.string().describe("Tool name, resource URI, or prompt name"),
3038
+ arguments: z.record(z.unknown()).optional().describe("Arguments for the tool or prompt (not used for resources)"),
3039
+ // Auto-connect params (only needed if not already connected)
3040
+ command: z.string().optional().describe(
3041
+ "Command to spawn the server (e.g. 'node'). Required if not already connected."
3042
+ ),
3043
+ args: z.array(z.string()).optional().describe("Arguments for the server command (e.g. ['src/index.js'])"),
3044
+ env: z.record(z.string()).optional().describe("Extra environment variables for the server process"),
3045
+ // Lifecycle
3046
+ disconnect_after: z.boolean().optional().describe("Tear down the connection after this call (default: false)"),
3047
+ timeout_ms: z.number().optional().describe("Timeout in ms (only applies to type: 'tool')")
1140
3048
  }
1141
3049
  },
1142
- async ({ uri }) => {
1143
- if (!target?.connected) {
1144
- return {
1145
- content: [
1146
- {
1147
- type: "text",
1148
- text: "No target server connected. Use connect_to_mcp first."
1149
- }
1150
- ],
1151
- isError: true
1152
- };
1153
- }
3050
+ async ({
3051
+ type: primitiveType,
3052
+ name,
3053
+ arguments: callArgs,
3054
+ command,
3055
+ args,
3056
+ env,
3057
+ disconnect_after,
3058
+ timeout_ms
3059
+ }) => {
1154
3060
  try {
1155
- const result = await target.readResource({ uri });
1156
- return {
1157
- content: [{ type: "text", text: JSON.stringify(result.contents, null, 2) }]
1158
- };
3061
+ const connectError = await ensureConnected(command, args, env);
3062
+ if (connectError) {
3063
+ return {
3064
+ content: [{ type: "text", text: connectError }],
3065
+ isError: true
3066
+ };
3067
+ }
1159
3068
  } catch (err) {
1160
- return {
1161
- content: [{ type: "text", text: `Error reading resource: ${err.message}` }],
1162
- isError: true
1163
- };
1164
- }
1165
- }
1166
- );
1167
- mcpServer.registerTool(
1168
- "list_mcp_prompts",
1169
- {
1170
- title: "List MCP Prompts",
1171
- description: "List all prompts exposed by the connected MCP server."
1172
- },
1173
- async () => {
1174
- if (!target?.connected) {
1175
3069
  return {
1176
3070
  content: [
1177
3071
  {
1178
3072
  type: "text",
1179
- text: "No target server connected. Use connect_to_mcp first."
3073
+ text: `Failed to auto-connect: ${err.message}
3074
+
3075
+ Check that the command is correct and the server starts without errors.`
1180
3076
  }
1181
3077
  ],
1182
3078
  isError: true
1183
3079
  };
1184
3080
  }
3081
+ let result;
1185
3082
  try {
1186
- const result = await target.listPrompts();
1187
- return {
1188
- content: [{ type: "text", text: JSON.stringify(result.prompts, null, 2) }]
1189
- };
1190
- } catch (err) {
1191
- return {
1192
- content: [{ type: "text", text: `Error listing prompts: ${err.message}` }],
1193
- isError: true
1194
- };
1195
- }
1196
- }
1197
- );
1198
- mcpServer.registerTool(
1199
- "get_mcp_prompt",
1200
- {
1201
- title: "Get MCP Prompt",
1202
- description: "Get a specific prompt by name from the connected MCP server.",
1203
- inputSchema: {
1204
- name: z.string().describe("Name of the prompt"),
1205
- arguments: z.record(z.string()).optional().describe("Arguments to pass to the prompt")
1206
- }
1207
- },
1208
- async ({ name, arguments: promptArgs }) => {
1209
- if (!target?.connected) {
1210
- return {
1211
- content: [
1212
- {
1213
- type: "text",
1214
- text: "No target server connected. Use connect_to_mcp first."
3083
+ switch (primitiveType) {
3084
+ case "tool": {
3085
+ try {
3086
+ const { tools } = await target.listTools();
3087
+ const toolNames = tools.map((t) => t.name);
3088
+ const matchedTool = tools.find((t) => t.name === name);
3089
+ if (!matchedTool) {
3090
+ const suggestion = suggestCommand(name, toolNames);
3091
+ const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
3092
+ return {
3093
+ content: [
3094
+ {
3095
+ type: "text",
3096
+ text: `Tool "${name}" not found.${hint}
3097
+ Available tools: ${toolNames.join(", ")}`
3098
+ }
3099
+ ],
3100
+ isError: true
3101
+ };
3102
+ }
3103
+ const schema = matchedTool.inputSchema;
3104
+ const requiredProps = schema?.required ?? [];
3105
+ const providedKeys = Object.keys(callArgs ?? {});
3106
+ const missingProps = requiredProps.filter((p) => !providedKeys.includes(p));
3107
+ if (missingProps.length > 0) {
3108
+ return {
3109
+ content: [
3110
+ {
3111
+ type: "text",
3112
+ text: `Tool "${name}" requires: ${missingProps.join(", ")}. Received: ${JSON.stringify(callArgs ?? {})}`
3113
+ }
3114
+ ],
3115
+ isError: true
3116
+ };
3117
+ }
3118
+ } catch {
1215
3119
  }
1216
- ],
1217
- isError: true
1218
- };
1219
- }
1220
- try {
1221
- const result = await target.getPrompt({
1222
- name,
1223
- arguments: promptArgs ?? {}
1224
- });
1225
- return {
1226
- content: [{ type: "text", text: JSON.stringify(result.messages, null, 2) }]
1227
- };
3120
+ const startMs = Date.now();
3121
+ result = await interceptor.callTool(
3122
+ target,
3123
+ name,
3124
+ callArgs ?? {},
3125
+ timeout_ms
3126
+ );
3127
+ const elapsedMs = Date.now() - startMs;
3128
+ const resultContent = result.content;
3129
+ if (Array.isArray(resultContent) && resultContent.length > 0) {
3130
+ const lastItem = resultContent[resultContent.length - 1];
3131
+ if (lastItem.type === "text") {
3132
+ lastItem.text += ` (${elapsedMs}ms)`;
3133
+ }
3134
+ }
3135
+ break;
3136
+ }
3137
+ case "resource": {
3138
+ const resourceResult = await target.readResource({ uri: name });
3139
+ result = {
3140
+ content: [
3141
+ { type: "text", text: JSON.stringify(resourceResult.contents, null, 2) }
3142
+ ]
3143
+ };
3144
+ break;
3145
+ }
3146
+ case "prompt": {
3147
+ const promptResult = await target.getPrompt({
3148
+ name,
3149
+ arguments: callArgs ?? {}
3150
+ });
3151
+ result = {
3152
+ content: [
3153
+ { type: "text", text: JSON.stringify(promptResult.messages, null, 2) }
3154
+ ]
3155
+ };
3156
+ break;
3157
+ }
3158
+ }
1228
3159
  } catch (err) {
1229
- return {
1230
- content: [{ type: "text", text: `Error getting prompt: ${err.message}` }],
3160
+ result = {
3161
+ content: [{ type: "text", text: `Error: ${err.message}` }],
1231
3162
  isError: true
1232
3163
  };
1233
3164
  }
3165
+ if (disconnect_after && target) {
3166
+ previousSnapshot = await takeSnapshot();
3167
+ await target.close();
3168
+ target = null;
3169
+ }
3170
+ return result;
1234
3171
  }
1235
3172
  );
1236
3173
  mcpServer.registerTool(
@@ -1273,72 +3210,99 @@ Available tools: ${available}` }
1273
3210
  };
1274
3211
  await mcpServer.connect(transport);
1275
3212
  process.stderr.write("[server] run-mcp test harness running on stdio.\n");
1276
- process.stderr.write("[server] Waiting for connect_to_mcp call...\n");
3213
+ process.stderr.write("[server] Waiting for connect_to_mcp or call_mcp_primitive call...\n");
1277
3214
  }
1278
3215
 
1279
3216
  // src/index.ts
1280
- program.name("run-mcp").enablePositionalOptions().description(
1281
- "A smart interactive REPL and live test harness for MCP servers.\n\nOperates in two modes:\n repl - Human-friendly CLI for testing MCP servers interactively\n server - MCP server that lets AI agents dynamically test local MCP servers"
1282
- ).version("1.3.1").addHelpText(
1283
- "after",
1284
- `
1285
- Examples:
1286
- $ run-mcp repl node my-server.js # Interactive testing (human)
1287
- $ run-mcp repl node my-server.js -s test.txt # Run a script
1288
- $ run-mcp server # Test harness (agent)
1289
- $ run-mcp repl npx -y some-mcp-server # Test an npx server
1290
-
1291
- Run 'run-mcp <command> --help' for detailed options.`
1292
- );
1293
- if (process.argv.length <= 2) {
1294
- program.outputHelp();
1295
- process.exit(0);
1296
- }
1297
- program.command("repl").description("Start an interactive REPL session with a target MCP server").passThroughOptions().allowUnknownOption().argument("<target_command...>", "Command to spawn the target MCP server").option("-s, --script <file>", "Read commands from a file instead of stdin").option("-o, --out-dir <path>", "Directory to save intercepted images").addHelpText(
1298
- "after",
1299
- `
1300
- Examples:
1301
- $ run-mcp repl node my-server.js
1302
- $ run-mcp repl node my-server.js --script verify.txt
1303
- $ run-mcp repl node my-server.js --out-dir ./screenshots
1304
-
1305
- REPL Commands (once connected):
1306
- tools/list List all available tools
1307
- tools/describe <name> Show a tool's input schema
1308
- tools/call <name> <json> [opts] Call a tool with JSON arguments
1309
- status Show target server status
1310
- help Show all commands`
1311
- ).action(async (targetCommand, opts) => {
1312
- await startRepl(targetCommand, opts);
1313
- });
1314
- program.command("server").description("Start as an MCP server that lets AI agents dynamically test local MCP servers").option("-o, --out-dir <path>", "Directory to save intercepted images and audio").option("-t, --timeout <ms>", "Default tool call timeout in milliseconds (default: 300000)").option("--max-text <chars>", "Max text response length before truncation (default: 50000)").addHelpText(
3217
+ program.name("run-mcp").description("A smart interactive REPL and live test harness for MCP servers").version("1.4.0").passThroughOptions().allowUnknownOption().argument(
3218
+ "[target_command...]",
3219
+ "Command to spawn the target MCP server (starts REPL if provided, Agent server otherwise)"
3220
+ ).option("-o, --out-dir <path>", "Directory to save intercepted images and audio").option(
3221
+ "-t, --timeout <ms>",
3222
+ "Default tool call timeout in milliseconds (default: 300000) (Agent Mode only)"
3223
+ ).option(
3224
+ "--max-text <chars>",
3225
+ "Max text response length before truncation (default: 50000) (Agent Mode only)"
3226
+ ).option("--mcp", "Force start Agent Server mode even if run interactively without arguments").option("-s, --script <file>", "Read commands from a file instead of stdin (REPL Mode only)").addHelpText(
1315
3227
  "after",
1316
3228
  `
1317
3229
  Examples:
1318
- $ run-mcp server
1319
- $ run-mcp server --out-dir ./test-output
1320
- $ run-mcp server --timeout 120000
3230
+ $ run-mcp # Test harness (agent mode)
3231
+ $ run-mcp node my-server.js # Interactive testing (human REPL mode)
3232
+ $ run-mcp node my-server.js -s test.txt # Run a script in REPL mode
3233
+ $ run-mcp npx -y some-mcp-server # Test an npx server
3234
+ $ run-mcp --out-dir ./test-output # Agent mode with options
3235
+ $ run-mcp --out-dir ./screenshots node srv.js # REPL mode with options
1321
3236
 
1322
- Add to your MCP client configuration:
3237
+ Agent Mode Configuration (mcp.json):
1323
3238
  {
1324
3239
  "mcpServers": {
1325
3240
  "run-mcp": {
1326
3241
  "command": "npx",
1327
- "args": ["-y", "run-mcp", "server"]
3242
+ "args": ["-y", "run-mcp"]
1328
3243
  }
1329
3244
  }
1330
3245
  }
1331
3246
 
1332
- Then use these tools from your agent:
1333
- connect_to_mcp \u2192 Spawn and connect to a local MCP server
1334
- list_mcp_tools \u2192 List tools on the connected server
1335
- call_mcp_tool \u2192 Call a tool (with interception)
1336
- disconnect_from_mcp \u2192 Tear down and reconnect after changes`
1337
- ).action(async (opts) => {
1338
- await startServer({
1339
- outDir: opts.outDir,
1340
- timeoutMs: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0,
1341
- maxTextLength: opts.maxText ? Number.parseInt(opts.maxText, 10) : void 0
1342
- });
1343
- });
3247
+ Agent Mode Tools:
3248
+ connect_to_mcp \u2192 Spawn and connect (use include to get tools/resources/prompts)
3249
+ call_mcp_primitive \u2192 Call a tool, read a resource, or get a prompt (auto-connects)
3250
+ list_mcp_primitives \u2192 List tools, resources, and/or prompts
3251
+ disconnect_from_mcp \u2192 Tear down and reconnect after changes
3252
+ mcp_server_status \u2192 Check connection status
3253
+ get_mcp_server_stderr \u2192 View target server stderr output
3254
+
3255
+ REPL Mode Commands (once connected):
3256
+ tools/list List all available tools
3257
+ tools/describe <name> Show a tool's input schema
3258
+ tools/call <name> [json] [opts] Call a tool (interactive if no json)
3259
+ tools/scaffold <name> Generate argument template for a tool
3260
+ resources/list List all available resources
3261
+ resources/read <uri> Read a resource by URI
3262
+ resources/templates List resource templates
3263
+ resources/subscribe <uri> Subscribe to resource changes
3264
+ resources/unsubscribe <uri> Unsubscribe from resource changes
3265
+ prompts/list List all available prompts
3266
+ prompts/get <name> [json_args] Get a prompt with arguments
3267
+ ping Verify connection, show round-trip time
3268
+ log-level <level> Set server logging verbosity
3269
+ history [count|clear] Show request/response history
3270
+ notifications [count|clear] Show server notifications
3271
+ roots/list Show configured client roots
3272
+ roots/add <uri> [name] Add a root directory
3273
+ roots/remove <uri> Remove a root directory
3274
+ !! / last Re-run the last command
3275
+ reconnect Disconnect and reconnect
3276
+ timing Show tool call performance stats
3277
+ status Show target server status
3278
+
3279
+ Shortcuts: tl td tc ts rl rr rt rs ru pl pg (see help for details)`
3280
+ ).action(
3281
+ async (targetCommand, opts) => {
3282
+ if (targetCommand && targetCommand.length > 0) {
3283
+ await startRepl(targetCommand, { script: opts.script, outDir: opts.outDir });
3284
+ } else {
3285
+ if (opts.mcp || !process.stdin.isTTY) {
3286
+ await startServer({
3287
+ outDir: opts.outDir,
3288
+ timeoutMs: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0,
3289
+ maxTextLength: opts.maxText ? Number.parseInt(opts.maxText, 10) : void 0
3290
+ });
3291
+ } else {
3292
+ const selected = await pickDiscoveredServer();
3293
+ if (!selected) {
3294
+ console.log("Run 'run-mcp --help' to see manual usage instructions.");
3295
+ return;
3296
+ }
3297
+ if (selected.config.env) {
3298
+ Object.assign(process.env, selected.config.env);
3299
+ }
3300
+ await startRepl([selected.config.command, ...selected.config.args || []], {
3301
+ script: opts.script,
3302
+ outDir: opts.outDir
3303
+ });
3304
+ }
3305
+ }
3306
+ }
3307
+ );
1344
3308
  program.parse();