run-mcp 1.3.4 → 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 +25 -9
  2. package/dist/index.js +2262 -298
  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,12 +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 };
614
+ const start = Date.now();
321
615
  const result = await this.client.callTool(
322
616
  { name, arguments: args },
323
617
  void 0,
324
618
  requestOptions
325
619
  );
326
620
  this.recordResponse();
621
+ this._addHistory(`tools/call ${name}`, args, result, Date.now() - start);
327
622
  return result;
328
623
  }
329
624
  // ─── Resources ──────────────────────────────────────────────────────────────
@@ -333,8 +628,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
333
628
  */
334
629
  async listResources(params) {
335
630
  this._assertConnected();
631
+ const start = Date.now();
336
632
  const result = await this.client.listResources(params);
337
633
  this.recordResponse();
634
+ this._addHistory("resources/list", params, result, Date.now() - start);
338
635
  return result;
339
636
  }
340
637
  /**
@@ -343,8 +640,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
343
640
  */
344
641
  async listResourceTemplates(params) {
345
642
  this._assertConnected();
643
+ const start = Date.now();
346
644
  const result = await this.client.listResourceTemplates(params);
347
645
  this.recordResponse();
646
+ this._addHistory("resources/templates/list", params, result, Date.now() - start);
348
647
  return result;
349
648
  }
350
649
  /**
@@ -352,8 +651,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
352
651
  */
353
652
  async readResource(params) {
354
653
  this._assertConnected();
654
+ const start = Date.now();
355
655
  const result = await this.client.readResource(params);
356
656
  this.recordResponse();
657
+ this._addHistory(`resources/read ${params.uri}`, params, result, Date.now() - start);
357
658
  return result;
358
659
  }
359
660
  /**
@@ -361,8 +662,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
361
662
  */
362
663
  async subscribeResource(params) {
363
664
  this._assertConnected();
665
+ const start = Date.now();
364
666
  const result = await this.client.subscribeResource(params);
365
667
  this.recordResponse();
668
+ this._addHistory(`resources/subscribe ${params.uri}`, params, result, Date.now() - start);
366
669
  return result;
367
670
  }
368
671
  /**
@@ -370,8 +673,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
370
673
  */
371
674
  async unsubscribeResource(params) {
372
675
  this._assertConnected();
676
+ const start = Date.now();
373
677
  const result = await this.client.unsubscribeResource(params);
374
678
  this.recordResponse();
679
+ this._addHistory(`resources/unsubscribe ${params.uri}`, params, result, Date.now() - start);
375
680
  return result;
376
681
  }
377
682
  // ─── Prompts ────────────────────────────────────────────────────────────────
@@ -381,8 +686,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
381
686
  */
382
687
  async listPrompts(params) {
383
688
  this._assertConnected();
689
+ const start = Date.now();
384
690
  const result = await this.client.listPrompts(params);
385
691
  this.recordResponse();
692
+ this._addHistory("prompts/list", params, result, Date.now() - start);
386
693
  return result;
387
694
  }
388
695
  /**
@@ -390,8 +697,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
390
697
  */
391
698
  async getPrompt(params) {
392
699
  this._assertConnected();
700
+ const start = Date.now();
393
701
  const result = await this.client.getPrompt(params);
394
702
  this.recordResponse();
703
+ this._addHistory(`prompts/get ${params.name}`, params, result, Date.now() - start);
395
704
  return result;
396
705
  }
397
706
  // ─── Logging ────────────────────────────────────────────────────────────────
@@ -400,8 +709,10 @@ var TargetManager = class _TargetManager extends EventEmitter {
400
709
  */
401
710
  async setLoggingLevel(level) {
402
711
  this._assertConnected();
712
+ const start = Date.now();
403
713
  const result = await this.client.setLoggingLevel(level);
404
714
  this.recordResponse();
715
+ this._addHistory(`logging/setLevel ${level}`, { level }, result, Date.now() - start);
405
716
  return result;
406
717
  }
407
718
  // ─── Completion ─────────────────────────────────────────────────────────────
@@ -410,10 +721,96 @@ var TargetManager = class _TargetManager extends EventEmitter {
410
721
  */
411
722
  async complete(params) {
412
723
  this._assertConnected();
724
+ const start = Date.now();
413
725
  const result = await this.client.complete(params);
414
726
  this.recordResponse();
727
+ this._addHistory("completion/complete", params, result, Date.now() - start);
415
728
  return result;
416
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
+ }
417
814
  // ─── Notification forwarding ────────────────────────────────────────────────
418
815
  /**
419
816
  * Access the underlying MCP client for advanced use cases like
@@ -570,18 +967,199 @@ var TargetManager = class _TargetManager extends EventEmitter {
570
967
 
571
968
  // src/repl.ts
572
969
  var KNOWN_COMMANDS = [
970
+ "explore",
971
+ "interactive",
573
972
  "tools/list",
574
973
  "tools/describe",
575
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",
576
992
  "status",
993
+ "reconnect",
994
+ "!!",
995
+ "last",
577
996
  "help",
578
997
  "exit",
579
- "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"
580
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
+ }
581
1158
  async function startRepl(targetCommand, opts) {
582
1159
  const [command, ...args] = targetCommand;
583
1160
  const target = new TargetManager(command, args);
584
1161
  const interceptor = new ResponseInterceptor({ outDir: opts.outDir });
1162
+ isScriptMode = !!opts.script;
585
1163
  target.on("stderr", (text) => {
586
1164
  for (const line of text.split("\n")) {
587
1165
  console.error(pc.dim(`[server] ${line}`));
@@ -604,7 +1182,7 @@ async function startRepl(targetCommand, opts) {
604
1182
  }
605
1183
  const status = target.getStatus();
606
1184
  console.log(pc.green(`\u2713 Connected (PID: ${status.pid})`));
607
- if (!opts.script) {
1185
+ if (!isScriptMode) {
608
1186
  target.enableAutoReconnect();
609
1187
  target.on(
610
1188
  "reconnecting",
@@ -615,9 +1193,10 @@ async function startRepl(targetCommand, opts) {
615
1193
  );
616
1194
  }
617
1195
  );
618
- target.on("reconnected", ({ attempt }) => {
1196
+ target.on("reconnected", async ({ attempt }) => {
619
1197
  const s = target.getStatus();
620
1198
  console.log(pc.green(`\u2713 Reconnected (PID: ${s.pid}, attempt ${attempt})`));
1199
+ await refreshCaches(target);
621
1200
  });
622
1201
  target.on("reconnect_failed", ({ reason, message }) => {
623
1202
  console.error(pc.red(`\u2717 ${message}`));
@@ -627,19 +1206,145 @@ async function startRepl(targetCommand, opts) {
627
1206
  );
628
1207
  }
629
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
+ });
630
1305
  }
1306
+ let toolCount = 0;
1307
+ let resourceCount = 0;
1308
+ let promptCount = 0;
631
1309
  try {
632
1310
  const { tools } = await target.listTools();
633
- console.log(
634
- pc.cyan(` ${tools.length} tool(s) available. Type ${pc.bold("help")} for commands.
635
- `)
636
- );
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
+ }
637
1342
  } catch (err) {
638
1343
  console.log(pc.yellow(` Warning: Could not list tools: ${err.message}
639
1344
  `));
640
1345
  }
641
- const isScript = !!opts.script;
642
- if (isScript) {
1346
+ await refreshCaches(target);
1347
+ if (isScriptMode) {
643
1348
  const lines = await readScriptLines(opts.script);
644
1349
  for (const line of lines) {
645
1350
  const trimmed = line.trim();
@@ -657,51 +1362,98 @@ async function startRepl(targetCommand, opts) {
657
1362
  await target.close();
658
1363
  process.exit(0);
659
1364
  } else {
660
- const rl = createInterface({
661
- input: process.stdin,
662
- output: process.stdout,
663
- prompt: pc.cyan("> "),
664
- 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
+ }
665
1385
  });
1386
+ }
1387
+ if (!deferNextPrompt) {
666
1388
  rl.prompt();
667
- let processing = false;
668
- const queue = [];
669
- const processQueue = async () => {
670
- if (processing) return;
671
- processing = true;
672
- while (queue.length > 0) {
673
- const trimmed = queue.shift();
674
- try {
675
- await handleCommand(trimmed, target, interceptor);
676
- } 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 {
677
1405
  console.error(pc.red(`\u2717 Error: ${err.message}`));
678
1406
  }
679
- rl.prompt();
680
1407
  }
681
- processing = false;
682
- };
683
- rl.on("line", (line) => {
684
- const trimmed = line.trim();
685
- if (!trimmed || trimmed.startsWith("#")) {
686
- rl.prompt();
687
- return;
1408
+ if (activeRl) {
1409
+ setImmediate(() => {
1410
+ if (activeRl) {
1411
+ console.log();
1412
+ activeRl.setPrompt(getPrompt(target));
1413
+ activeRl.prompt();
1414
+ }
1415
+ });
688
1416
  }
689
- queue.push(trimmed);
690
- processQueue();
691
- });
692
- 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) {
693
1434
  console.log(pc.dim("\nShutting down..."));
694
1435
  await target.close();
695
1436
  process.exit(0);
696
- });
697
- }
1437
+ }
1438
+ });
698
1439
  }
699
- async function handleCommand(input, target, interceptor) {
700
- 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
+ }
701
1447
  switch (cmd) {
702
1448
  case "help":
703
1449
  printHelp();
704
1450
  return;
1451
+ case "explore":
1452
+ case "interactive":
1453
+ await withSuspendedReadline(target, interceptor, async () => {
1454
+ await cmdExplore(target, interceptor);
1455
+ });
1456
+ return;
705
1457
  case "tools/list":
706
1458
  await cmdToolsList(target);
707
1459
  return;
@@ -711,6 +1463,69 @@ async function handleCommand(input, target, interceptor) {
711
1463
  case "tools/call":
712
1464
  await cmdToolsCall(target, interceptor, rest);
713
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;
714
1529
  case "status":
715
1530
  cmdStatus(target);
716
1531
  return;
@@ -721,7 +1536,22 @@ async function handleCommand(input, target, interceptor) {
721
1536
  default: {
722
1537
  const suggestion = suggestCommand(cmd, KNOWN_COMMANDS);
723
1538
  if (suggestion) {
724
- 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
+ }
725
1555
  } else {
726
1556
  console.log(pc.yellow(`Unknown command: ${cmd}. Type ${pc.bold("help")} for usage.`));
727
1557
  }
@@ -743,32 +1573,71 @@ async function cmdToolsList(target) {
743
1573
  }
744
1574
  console.log(pc.dim(`
745
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
+ }
746
1586
  }
747
1587
  async function cmdToolsDescribe(target, rest) {
748
1588
  const name = rest.trim();
749
1589
  if (!name) {
750
- 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
+ }
751
1598
  return;
752
1599
  }
753
1600
  const { tools } = await target.listTools();
754
1601
  const tool = tools.find((t) => t.name === name);
755
1602
  if (!tool) {
756
1603
  console.log(pc.red(`Tool "${name}" not found.`));
757
- 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
+ }
758
1613
  return;
759
1614
  }
760
- console.log(pc.bold(`
761
- ${tool.name}`));
762
- if (tool.description) {
763
- console.log(pc.dim(` ${tool.description}`));
764
- }
765
- console.log(pc.cyan("\n Input Schema:"));
766
- 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();
767
1625
  }
768
1626
  async function cmdToolsCall(target, interceptor, rest) {
769
- 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);
770
1630
  if (!toolName) {
771
- 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
+ }
772
1641
  return;
773
1642
  }
774
1643
  let args = {};
@@ -780,16 +1649,56 @@ async function cmdToolsCall(target, interceptor, rest) {
780
1649
  console.log(pc.dim(` Received: ${jsonArgs}`));
781
1650
  return;
782
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
+ }
783
1683
  }
784
1684
  console.log(pc.dim(` Calling ${toolName}...`));
785
1685
  const startTime = Date.now();
786
1686
  const result = await interceptor.callTool(target, toolName, args, timeoutMs);
787
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))}`);
788
1697
  const content = result.content;
789
1698
  if (Array.isArray(content)) {
790
1699
  for (const item of content) {
791
1700
  if (item.type === "text") {
792
- console.log(item.text);
1701
+ console.log(isError ? pc.red(` \u2717 ${item.text}`) : ` ${item.text}`);
793
1702
  } else {
794
1703
  console.log(formatJson(item, 2));
795
1704
  }
@@ -797,15 +1706,556 @@ async function cmdToolsCall(target, interceptor, rest) {
797
1706
  } else {
798
1707
  console.log(formatJson(result, 2));
799
1708
  }
800
- 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")}`);
801
1720
  }
802
- function cmdStatus(target) {
803
- const s = target.getStatus();
804
- const uptimeStr = s.uptime >= 60 ? `${Math.floor(s.uptime / 60)}m ${(s.uptime % 60).toFixed(0)}s` : `${s.uptime.toFixed(1)}s`;
805
- const lastRespStr = s.lastResponseTime ? `${((Date.now() - s.lastResponseTime) / 1e3).toFixed(1)}s ago` : "never";
806
- console.log(pc.bold("\n Target Server Status"));
807
- console.log(` ${pc.dim("Connected:")} ${s.connected ? pc.green("yes") : pc.red("no")}`);
808
- console.log(` ${pc.dim("PID:")} ${s.pid ?? "N/A"}`);
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")}`);
2258
+ console.log(` ${pc.dim("PID:")} ${s.pid ?? "N/A"}`);
809
2259
  console.log(` ${pc.dim("Uptime:")} ${uptimeStr}`);
810
2260
  console.log(` ${pc.dim("Last response:")} ${lastRespStr}`);
811
2261
  console.log(` ${pc.dim("Stderr lines:")} ${s.stderrLineCount.toLocaleString()}`);
@@ -815,56 +2265,486 @@ function cmdStatus(target) {
815
2265
  }
816
2266
  function printHelp() {
817
2267
  console.log(`
818
- ${pc.bold("Available Commands:")}
2268
+ ${pc.bold("Tool Commands:")}
819
2269
 
820
2270
  ${pc.green("tools/list")} List all available tools
821
2271
  ${pc.green("tools/describe")} <name> Show a tool's input schema
822
- ${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)
823
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
824
2309
  ${pc.green("status")} Show target server status
825
2310
  ${pc.green("help")} Show this help
826
2311
  ${pc.green("exit")} / ${pc.green("quit")} Disconnect and exit
827
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
+
828
2321
  ${pc.dim("Lines starting with # are treated as comments.")}
829
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.")}
830
2325
  `);
831
2326
  }
832
2327
  async function readScriptLines(filepath) {
833
2328
  const content = await readFile(filepath, "utf-8");
834
2329
  return content.split("\n");
835
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
+ }
836
2427
 
837
2428
  // src/server.ts
2429
+ import { createHash } from "crypto";
838
2430
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
839
2431
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
840
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
+ }
841
2606
  async function startServer(opts) {
842
2607
  let target = null;
2608
+ let previousSnapshot = null;
843
2609
  const interceptor = new ResponseInterceptor({
844
2610
  outDir: opts.outDir,
845
2611
  defaultTimeoutMs: opts.timeoutMs,
846
2612
  maxTextLength: opts.maxTextLength
847
2613
  });
848
2614
  const mcpServer = new McpServer(
849
- { name: "run-mcp", version: "1.3.1" },
2615
+ { name: "run-mcp", version: "1.4.0" },
850
2616
  {
851
2617
  capabilities: {
852
2618
  tools: {}
853
2619
  }
854
2620
  }
855
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
+ }
856
2733
  mcpServer.registerTool(
857
2734
  "connect_to_mcp",
858
2735
  {
859
2736
  title: "Connect to MCP Server",
860
- 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.",
861
2738
  inputSchema: {
862
2739
  command: z.string().describe("Command to run (e.g. 'node', 'python', 'npx')"),
863
2740
  args: z.array(z.string()).optional().describe("Arguments to pass (e.g. ['src/index.js'] or ['-y', 'some-server'])"),
864
- 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
+ )
865
2745
  }
866
2746
  },
867
- async ({ command, args, env }) => {
2747
+ async ({ command, args, env, include }) => {
868
2748
  if (target?.connected) {
869
2749
  return {
870
2750
  content: [
@@ -907,9 +2787,21 @@ async function startServer(opts) {
907
2787
  `Capabilities: ${capSummary.join(", ") || "none"}`,
908
2788
  `Tools available: ${toolCount}`,
909
2789
  "",
910
- "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.",
911
2791
  "Use disconnect_from_mcp when done, or to reconnect after code changes."
912
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
+ }
913
2805
  return { content: [{ type: "text", text: lines.join("\n") }] };
914
2806
  } catch (err) {
915
2807
  target = null;
@@ -985,12 +2877,20 @@ Check that the command is correct and the server starts without errors. You can
985
2877
  }
986
2878
  );
987
2879
  mcpServer.registerTool(
988
- "list_mcp_tools",
2880
+ "list_mcp_primitives",
989
2881
  {
990
- title: "List MCP Tools",
991
- 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
+ }
992
2892
  },
993
- async () => {
2893
+ async ({ type, name }) => {
994
2894
  if (!target?.connected) {
995
2895
  return {
996
2896
  content: [
@@ -1002,243 +2902,272 @@ Check that the command is correct and the server starts without errors. You can
1002
2902
  isError: true
1003
2903
  };
1004
2904
  }
1005
- try {
1006
- const result = await target.listTools();
1007
- return {
1008
- content: [
1009
- {
1010
- type: "text",
1011
- 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));
1012
2920
  }
1013
- ]
1014
- };
1015
- } catch (err) {
1016
- return {
1017
- content: [{ type: "text", text: `Error listing tools: ${err.message}` }],
1018
- isError: true
1019
- };
1020
- }
1021
- }
1022
- );
1023
- mcpServer.registerTool(
1024
- "describe_mcp_tool",
1025
- {
1026
- title: "Describe MCP Tool",
1027
- description: "Get the description and input schema for a specific tool on the connected server.",
1028
- inputSchema: {
1029
- name: z.string().describe("Name of the tool to describe")
2921
+ } else {
2922
+ sections.push("--- Tools ---", JSON.stringify(tools, null, 2));
2923
+ }
2924
+ } catch (err) {
2925
+ sections.push("--- Tools ---", `Error: ${err.message}`);
2926
+ }
1030
2927
  }
1031
- },
1032
- async ({ name }) => {
1033
- if (!target?.connected) {
1034
- return {
1035
- content: [
1036
- {
1037
- type: "text",
1038
- text: "No target server connected. Use connect_to_mcp first."
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));
1039
2943
  }
1040
- ],
1041
- isError: true
1042
- };
1043
- }
1044
- try {
1045
- const result = await target.listTools();
1046
- const tool = result.tools.find((t) => t.name === name);
1047
- if (!tool) {
1048
- const available = result.tools.map((t) => t.name).join(", ");
1049
- return {
1050
- content: [
1051
- {
1052
- type: "text",
1053
- text: `Tool "${name}" not found.
1054
- Available tools: ${available}`
1055
- }
1056
- ],
1057
- isError: true
1058
- };
2944
+ } else {
2945
+ sections.push("--- Resources ---", JSON.stringify(resources, null, 2));
2946
+ }
2947
+ } catch (err) {
2948
+ sections.push("--- Resources ---", `Error: ${err.message}`);
1059
2949
  }
1060
- return {
1061
- content: [{ type: "text", text: JSON.stringify(tool, null, 2) }]
1062
- };
1063
- } catch (err) {
1064
- return {
1065
- content: [{ type: "text", text: `Error describing tool: ${err.message}` }],
1066
- isError: true
1067
- };
1068
2950
  }
1069
- }
1070
- );
1071
- mcpServer.registerTool(
1072
- "call_mcp_tool",
1073
- {
1074
- title: "Call MCP Tool",
1075
- 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.",
1076
- inputSchema: {
1077
- name: z.string().describe("Name of the tool to call"),
1078
- arguments: z.record(z.unknown()).optional().describe("Arguments to pass to the tool (as a JSON object)"),
1079
- 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
+ }
1080
2973
  }
1081
- },
1082
- async ({ name, arguments: toolArgs, timeout_ms }) => {
1083
- if (!target?.connected) {
2974
+ if (sections.length === 0) {
1084
2975
  return {
1085
2976
  content: [
1086
2977
  {
1087
2978
  type: "text",
1088
- text: "No target server connected. Use connect_to_mcp first."
2979
+ text: "No matching primitives found. The server may not support the requested types."
1089
2980
  }
1090
- ],
1091
- isError: true
1092
- };
1093
- }
1094
- try {
1095
- const result = await interceptor.callTool(
1096
- target,
1097
- name,
1098
- toolArgs ?? {},
1099
- timeout_ms
1100
- );
1101
- return result;
1102
- } catch (err) {
1103
- return {
1104
- content: [{ type: "text", text: `Error: ${err.message}` }],
1105
- isError: true
2981
+ ]
1106
2982
  };
1107
2983
  }
2984
+ return {
2985
+ content: [{ type: "text", text: sections.join("\n") }]
2986
+ };
1108
2987
  }
1109
2988
  );
1110
2989
  mcpServer.registerTool(
1111
- "list_mcp_resources",
2990
+ "list_available_mcp_servers",
1112
2991
  {
1113
- title: "List MCP Resources",
1114
- 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."
1115
2994
  },
1116
2995
  async () => {
1117
- if (!target?.connected) {
1118
- return {
1119
- content: [
1120
- {
1121
- type: "text",
1122
- text: "No target server connected. Use connect_to_mcp first."
1123
- }
1124
- ],
1125
- isError: true
1126
- };
1127
- }
1128
2996
  try {
1129
- const result = await target.listResources();
1130
- return {
1131
- content: [{ type: "text", text: JSON.stringify(result.resources, null, 2) }]
1132
- };
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") }] };
1133
3021
  } catch (err) {
1134
3022
  return {
1135
- content: [{ type: "text", text: `Error listing resources: ${err.message}` }],
3023
+ content: [{ type: "text", text: `Error discovering servers: ${err.message}` }],
1136
3024
  isError: true
1137
3025
  };
1138
3026
  }
1139
3027
  }
1140
3028
  );
1141
3029
  mcpServer.registerTool(
1142
- "read_mcp_resource",
3030
+ "call_mcp_primitive",
1143
3031
  {
1144
- title: "Read MCP Resource",
1145
- 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.",
1146
3034
  inputSchema: {
1147
- 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')")
1148
3048
  }
1149
3049
  },
1150
- async ({ uri }) => {
1151
- if (!target?.connected) {
1152
- return {
1153
- content: [
1154
- {
1155
- type: "text",
1156
- text: "No target server connected. Use connect_to_mcp first."
1157
- }
1158
- ],
1159
- isError: true
1160
- };
1161
- }
3050
+ async ({
3051
+ type: primitiveType,
3052
+ name,
3053
+ arguments: callArgs,
3054
+ command,
3055
+ args,
3056
+ env,
3057
+ disconnect_after,
3058
+ timeout_ms
3059
+ }) => {
1162
3060
  try {
1163
- const result = await target.readResource({ uri });
1164
- return {
1165
- content: [{ type: "text", text: JSON.stringify(result.contents, null, 2) }]
1166
- };
3061
+ const connectError = await ensureConnected(command, args, env);
3062
+ if (connectError) {
3063
+ return {
3064
+ content: [{ type: "text", text: connectError }],
3065
+ isError: true
3066
+ };
3067
+ }
1167
3068
  } catch (err) {
1168
- return {
1169
- content: [{ type: "text", text: `Error reading resource: ${err.message}` }],
1170
- isError: true
1171
- };
1172
- }
1173
- }
1174
- );
1175
- mcpServer.registerTool(
1176
- "list_mcp_prompts",
1177
- {
1178
- title: "List MCP Prompts",
1179
- description: "List all prompts exposed by the connected MCP server."
1180
- },
1181
- async () => {
1182
- if (!target?.connected) {
1183
3069
  return {
1184
3070
  content: [
1185
3071
  {
1186
3072
  type: "text",
1187
- 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.`
1188
3076
  }
1189
3077
  ],
1190
3078
  isError: true
1191
3079
  };
1192
3080
  }
3081
+ let result;
1193
3082
  try {
1194
- const result = await target.listPrompts();
1195
- return {
1196
- content: [{ type: "text", text: JSON.stringify(result.prompts, null, 2) }]
1197
- };
1198
- } catch (err) {
1199
- return {
1200
- content: [{ type: "text", text: `Error listing prompts: ${err.message}` }],
1201
- isError: true
1202
- };
1203
- }
1204
- }
1205
- );
1206
- mcpServer.registerTool(
1207
- "get_mcp_prompt",
1208
- {
1209
- title: "Get MCP Prompt",
1210
- description: "Get a specific prompt by name from the connected MCP server.",
1211
- inputSchema: {
1212
- name: z.string().describe("Name of the prompt"),
1213
- arguments: z.record(z.string()).optional().describe("Arguments to pass to the prompt")
1214
- }
1215
- },
1216
- async ({ name, arguments: promptArgs }) => {
1217
- if (!target?.connected) {
1218
- return {
1219
- content: [
1220
- {
1221
- type: "text",
1222
- 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 {
1223
3119
  }
1224
- ],
1225
- isError: true
1226
- };
1227
- }
1228
- try {
1229
- const result = await target.getPrompt({
1230
- name,
1231
- arguments: promptArgs ?? {}
1232
- });
1233
- return {
1234
- content: [{ type: "text", text: JSON.stringify(result.messages, null, 2) }]
1235
- };
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
+ }
1236
3159
  } catch (err) {
1237
- return {
1238
- content: [{ type: "text", text: `Error getting prompt: ${err.message}` }],
3160
+ result = {
3161
+ content: [{ type: "text", text: `Error: ${err.message}` }],
1239
3162
  isError: true
1240
3163
  };
1241
3164
  }
3165
+ if (disconnect_after && target) {
3166
+ previousSnapshot = await takeSnapshot();
3167
+ await target.close();
3168
+ target = null;
3169
+ }
3170
+ return result;
1242
3171
  }
1243
3172
  );
1244
3173
  mcpServer.registerTool(
@@ -1281,11 +3210,11 @@ Available tools: ${available}`
1281
3210
  };
1282
3211
  await mcpServer.connect(transport);
1283
3212
  process.stderr.write("[server] run-mcp test harness running on stdio.\n");
1284
- 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");
1285
3214
  }
1286
3215
 
1287
3216
  // src/index.ts
1288
- program.name("run-mcp").description("A smart interactive REPL and live test harness for MCP servers").version("1.3.4").passThroughOptions().allowUnknownOption().argument(
3217
+ program.name("run-mcp").description("A smart interactive REPL and live test harness for MCP servers").version("1.4.0").passThroughOptions().allowUnknownOption().argument(
1289
3218
  "[target_command...]",
1290
3219
  "Command to spawn the target MCP server (starts REPL if provided, Agent server otherwise)"
1291
3220
  ).option("-o, --out-dir <path>", "Directory to save intercepted images and audio").option(
@@ -1294,7 +3223,7 @@ program.name("run-mcp").description("A smart interactive REPL and live test harn
1294
3223
  ).option(
1295
3224
  "--max-text <chars>",
1296
3225
  "Max text response length before truncation (default: 50000) (Agent Mode only)"
1297
- ).option("-s, --script <file>", "Read commands from a file instead of stdin (REPL Mode only)").addHelpText(
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(
1298
3227
  "after",
1299
3228
  `
1300
3229
  Examples:
@@ -1316,28 +3245,63 @@ Agent Mode Configuration (mcp.json):
1316
3245
  }
1317
3246
 
1318
3247
  Agent Mode Tools:
1319
- connect_to_mcp \u2192 Spawn and connect to a local MCP server
1320
- list_mcp_tools \u2192 List tools on the connected server
1321
- describe_mcp_tool \u2192 Show a tool's input schema
1322
- call_mcp_tool \u2192 Call a tool (with interception)
1323
- disconnect_from_mcp \u2192 Tear down and reconnect after changes
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
1324
3254
 
1325
3255
  REPL Mode Commands (once connected):
1326
3256
  tools/list List all available tools
1327
3257
  tools/describe <name> Show a tool's input schema
1328
- tools/call <name> <json> [opts] Call a tool with JSON arguments
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
1329
3277
  status Show target server status
1330
- help Show all commands`
3278
+
3279
+ Shortcuts: tl td tc ts rl rr rt rs ru pl pg (see help for details)`
1331
3280
  ).action(
1332
3281
  async (targetCommand, opts) => {
1333
3282
  if (targetCommand && targetCommand.length > 0) {
1334
3283
  await startRepl(targetCommand, { script: opts.script, outDir: opts.outDir });
1335
3284
  } else {
1336
- await startServer({
1337
- outDir: opts.outDir,
1338
- timeoutMs: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0,
1339
- maxTextLength: opts.maxText ? Number.parseInt(opts.maxText, 10) : void 0
1340
- });
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
+ }
1341
3305
  }
1342
3306
  }
1343
3307
  );