run-mcp 1.6.0 → 1.6.1

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 +109 -69
  2. package/dist/index.js +2127 -1168
  3. package/package.json +10 -5
package/dist/index.js CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { program } from "commander";
5
+ import { createConnection, createServer } from "net";
6
+ import { existsSync as existsSync2 } from "fs";
7
+ import { mkdir as mkdir2, readFile as readFile3, rm, writeFile as writeFile2 } from "fs/promises";
8
+ import { join as join2, resolve } from "path";
9
+ import { tmpdir as tmpdir2 } from "os";
10
+ import { spawn } from "child_process";
5
11
 
6
12
  // src/config-scanner.ts
7
13
  import { existsSync } from "fs";
@@ -16,7 +22,6 @@ function getConfigPaths() {
16
22
  const isWin = process2.platform === "win32";
17
23
  const isMac = process2.platform === "darwin";
18
24
  const appData = process2.env.APPDATA || path.join(home, "AppData", "Roaming");
19
- const localAppData = process2.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
20
25
  let claudeDesktopGlob;
21
26
  if (isWin) {
22
27
  claudeDesktopGlob = path.join(appData, "Claude", "claude_desktop_config.json");
@@ -145,11 +150,13 @@ var ResponseInterceptor = class {
145
150
  outDir;
146
151
  defaultTimeoutMs;
147
152
  maxTextLength;
153
+ mediaThresholdKb;
148
154
  fileCounter = 0;
149
155
  constructor(opts = {}) {
150
156
  this.outDir = opts.outDir ?? join(tmpdir(), "run-mcp");
151
157
  this.defaultTimeoutMs = opts.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
152
158
  this.maxTextLength = opts.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH;
159
+ this.mediaThresholdKb = opts.mediaThresholdKb ?? 0;
153
160
  }
154
161
  /**
155
162
  * Call a tool on the target, applying timeout, media extraction, and truncation.
@@ -157,21 +164,101 @@ var ResponseInterceptor = class {
157
164
  * Returns the full result object as-is (including structuredContent, isError, _meta)
158
165
  * with only the content array items modified when interception is needed.
159
166
  */
160
- async callTool(target, name, args = {}, timeoutMs) {
161
- const { result } = await this._callToolInternal(target, name, args, timeoutMs);
167
+ async callTool(target, name, args = {}, timeoutMs, maxTextLength) {
168
+ const { result } = await this._callToolInternal(target, name, args, timeoutMs, maxTextLength);
162
169
  return result;
163
170
  }
164
171
  /**
165
172
  * Call a tool and return both the result and metadata about interception actions.
166
173
  * Used by the agent server when `include_metadata` is requested.
167
174
  */
168
- async callToolWithMetadata(target, name, args = {}, timeoutMs) {
169
- return this._callToolInternal(target, name, args, timeoutMs);
175
+ async callToolWithMetadata(target, name, args = {}, timeoutMs, maxTextLength) {
176
+ return this._callToolInternal(target, name, args, timeoutMs, maxTextLength);
177
+ }
178
+ /**
179
+ * Read a resource on the target, applying timeout, media extraction, and truncation.
180
+ */
181
+ async readResource(target, params, timeoutMs, maxTextLength) {
182
+ const timeout = timeoutMs ?? this.defaultTimeoutMs;
183
+ const metadata = {
184
+ truncated: false,
185
+ imagesSaved: 0,
186
+ audioSaved: 0,
187
+ originalSizeBytes: 0
188
+ };
189
+ const targetCall = target.readResource(params);
190
+ targetCall.catch(() => {
191
+ });
192
+ const result = await Promise.race([
193
+ targetCall,
194
+ this._timeout(timeout, `resource:${params.uri}`)
195
+ ]);
196
+ const contents = result.contents;
197
+ if (Array.isArray(contents)) {
198
+ for (const item of contents) {
199
+ if (item.text) {
200
+ metadata.originalSizeBytes += Buffer.byteLength(item.text, "utf8");
201
+ } else if (item.blob) {
202
+ metadata.originalSizeBytes += Buffer.byteLength(item.blob, "base64");
203
+ }
204
+ }
205
+ for (let i = 0; i < contents.length; i++) {
206
+ contents[i] = await this._processResourceItem(contents[i], metadata, maxTextLength);
207
+ }
208
+ }
209
+ return result;
210
+ }
211
+ /**
212
+ * Get a prompt on the target, applying timeout, media extraction, and truncation.
213
+ */
214
+ async getPrompt(target, params, timeoutMs, maxTextLength) {
215
+ const timeout = timeoutMs ?? this.defaultTimeoutMs;
216
+ const metadata = {
217
+ truncated: false,
218
+ imagesSaved: 0,
219
+ audioSaved: 0,
220
+ originalSizeBytes: 0
221
+ };
222
+ const targetCall = target.getPrompt(params);
223
+ targetCall.catch(() => {
224
+ });
225
+ const result = await Promise.race([
226
+ targetCall,
227
+ this._timeout(timeout, `prompt:${params.name}`)
228
+ ]);
229
+ const messages = result.messages;
230
+ if (Array.isArray(messages)) {
231
+ for (const msg of messages) {
232
+ const content = msg.content;
233
+ if (content) {
234
+ if (Array.isArray(content)) {
235
+ for (const item of content) {
236
+ if (item.type === "text" && item.text) {
237
+ metadata.originalSizeBytes += Buffer.byteLength(item.text, "utf8");
238
+ } else if ((item.type === "image" || item.type === "audio") && item.data) {
239
+ metadata.originalSizeBytes += Buffer.byteLength(item.data, "base64");
240
+ }
241
+ }
242
+ for (let i = 0; i < content.length; i++) {
243
+ content[i] = await this._processItem(content[i], metadata, maxTextLength);
244
+ }
245
+ } else if (typeof content === "object") {
246
+ if (content.type === "text" && content.text) {
247
+ metadata.originalSizeBytes += Buffer.byteLength(content.text, "utf8");
248
+ } else if ((content.type === "image" || content.type === "audio") && content.data) {
249
+ metadata.originalSizeBytes += Buffer.byteLength(content.data, "base64");
250
+ }
251
+ msg.content = await this._processItem(content, metadata, maxTextLength);
252
+ }
253
+ }
254
+ }
255
+ }
256
+ return result;
170
257
  }
171
258
  /**
172
259
  * Internal implementation shared by callTool and callToolWithMetadata.
173
260
  */
174
- async _callToolInternal(target, name, args = {}, timeoutMs) {
261
+ async _callToolInternal(target, name, args = {}, timeoutMs, maxTextLength) {
175
262
  const timeout = timeoutMs ?? this.defaultTimeoutMs;
176
263
  const metadata = {
177
264
  truncated: false,
@@ -193,7 +280,7 @@ var ResponseInterceptor = class {
193
280
  }
194
281
  }
195
282
  for (let i = 0; i < content.length; i++) {
196
- content[i] = await this._processItem(content[i], metadata);
283
+ content[i] = await this._processItem(content[i], metadata, maxTextLength);
197
284
  }
198
285
  }
199
286
  return { result, metadata };
@@ -203,25 +290,70 @@ var ResponseInterceptor = class {
203
290
  * Preserves all item properties not related to the intercepted data
204
291
  * (e.g., annotations, _meta).
205
292
  */
206
- async _processItem(item, metadata) {
293
+ async _processItem(item, metadata, maxTextLength) {
207
294
  if (item.type === "image" && item.data) {
295
+ const sizeKB = Buffer.byteLength(item.data, "base64") / 1024;
296
+ if (this.mediaThresholdKb === -1 || this.mediaThresholdKb > 0 && sizeKB <= this.mediaThresholdKb) {
297
+ return item;
298
+ }
208
299
  metadata.imagesSaved++;
209
300
  return this._saveMedia(item.data, item.mimeType ?? "image/png", "image");
210
301
  }
211
302
  if (item.type === "audio" && item.data) {
303
+ const sizeKB = Buffer.byteLength(item.data, "base64") / 1024;
304
+ if (this.mediaThresholdKb === -1 || this.mediaThresholdKb > 0 && sizeKB <= this.mediaThresholdKb) {
305
+ return item;
306
+ }
212
307
  metadata.audioSaved++;
213
308
  return this._saveMedia(item.data, item.mimeType ?? "audio/wav", "audio");
214
309
  }
215
310
  if (item.type === "text" && item.text && BASE64_PATTERN.test(item.text.trim())) {
311
+ const sizeKB = Buffer.byteLength(item.text.trim(), "base64") / 1024;
312
+ if (this.mediaThresholdKb === -1 || this.mediaThresholdKb > 0 && sizeKB <= this.mediaThresholdKb) {
313
+ return item;
314
+ }
216
315
  metadata.imagesSaved++;
217
316
  return this._saveMedia(item.text.trim(), "image/png", "image");
218
317
  }
219
- if (item.type === "text" && item.text && item.text.length > this.maxTextLength) {
318
+ const limit = maxTextLength ?? this.maxTextLength;
319
+ if (item.type === "text" && item.text && limit !== -1 && item.text.length > limit) {
320
+ const totalLength = item.text.length;
321
+ metadata.truncated = true;
322
+ return {
323
+ ...item,
324
+ text: item.text.slice(0, limit) + `
325
+ ... (truncated, ${totalLength.toLocaleString()} chars total)`
326
+ };
327
+ }
328
+ return item;
329
+ }
330
+ /**
331
+ * Process a single resource content item.
332
+ */
333
+ async _processResourceItem(item, metadata, maxTextLength) {
334
+ if (item.blob) {
335
+ const mime = item.mimeType ?? "image/png";
336
+ const isAudio = mime.startsWith("audio/");
337
+ const sizeKB = Buffer.byteLength(item.blob, "base64") / 1024;
338
+ if (this.mediaThresholdKb === -1 || this.mediaThresholdKb > 0 && sizeKB <= this.mediaThresholdKb) {
339
+ return item;
340
+ }
341
+ if (isAudio) metadata.audioSaved++;
342
+ else metadata.imagesSaved++;
343
+ const saved = await this._saveMedia(item.blob, mime, isAudio ? "audio" : "image");
344
+ return {
345
+ uri: item.uri,
346
+ mimeType: "text/plain",
347
+ text: saved.text
348
+ };
349
+ }
350
+ const limit = maxTextLength ?? this.maxTextLength;
351
+ if (item.text && limit !== -1 && item.text.length > limit) {
220
352
  const totalLength = item.text.length;
221
353
  metadata.truncated = true;
222
354
  return {
223
355
  ...item,
224
- text: item.text.slice(0, this.maxTextLength) + `
356
+ text: item.text.slice(0, limit) + `
225
357
  ... (truncated, ${totalLength.toLocaleString()} chars total)`
226
358
  };
227
359
  }
@@ -251,13 +383,14 @@ var ResponseInterceptor = class {
251
383
  /**
252
384
  * Returns a promise that rejects after the given timeout.
253
385
  */
254
- _timeout(ms, toolName) {
386
+ _timeout(ms, targetName) {
255
387
  return new Promise((_, reject) => {
256
388
  setTimeout(() => {
257
389
  const humanMs = ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${ms}ms`;
390
+ const typeLabel2 = targetName.includes(":") ? "Request" : "Tool";
258
391
  reject(
259
392
  new Error(
260
- `Tool "${toolName}" timed out after ${ms}ms (${humanMs}). Use --timeout <ms> to increase the limit.`
393
+ `${typeLabel2} "${targetName}" timed out after ${ms}ms (${humanMs}). Use --timeout <ms> to increase the limit.`
261
394
  )
262
395
  );
263
396
  }, ms);
@@ -290,1070 +423,1202 @@ var ResponseInterceptor = class {
290
423
  }
291
424
  };
292
425
 
293
- // src/target-manager.ts
294
- import { EventEmitter } from "events";
295
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
296
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
297
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
298
- import {
299
- CreateMessageRequestSchema,
300
- ElicitRequestSchema,
301
- ListRootsRequestSchema,
302
- LoggingMessageNotificationSchema,
303
- PromptListChangedNotificationSchema,
304
- ResourceListChangedNotificationSchema,
305
- ResourceUpdatedNotificationSchema,
306
- ToolListChangedNotificationSchema
307
- } from "@modelcontextprotocol/sdk/types.js";
308
- var MIN_UPTIME_FOR_RESTART_MS = 5e3;
309
- var MAX_RECONNECT_ATTEMPTS = 3;
310
- var STABLE_CONNECTION_RESET_MS = 6e4;
311
- var MAX_HISTORY = 100;
312
- var TargetManager = class _TargetManager extends EventEmitter {
313
- constructor(command, args) {
314
- super();
315
- this.command = command;
316
- this.args = args;
426
+ // src/parsing.ts
427
+ import pc from "picocolors";
428
+ function parseCommandLine(input3) {
429
+ const spaceIdx = input3.indexOf(" ");
430
+ if (spaceIdx === -1) {
431
+ return { cmd: input3.toLowerCase(), rest: "" };
317
432
  }
318
- client = null;
319
- transport = null;
320
- startTime = 0;
321
- childPid = null;
322
- _connected = false;
323
- // Enhanced status tracking
324
- _lastResponseTime = null;
325
- _stderrLineCount = 0;
326
- _stderrLines = [];
327
- static MAX_STDERR_LINES = 200;
328
- // Auto-reconnect state
329
- _reconnectAttempts = 0;
330
- _stableTimer = null;
331
- _autoReconnect = false;
332
- _reconnecting = false;
333
- _intentionalClose = false;
334
- // Request history
335
- _history = [];
336
- _historyIdCounter = 0;
337
- // Notifications
338
- _notifications = [];
339
- static MAX_NOTIFICATIONS = 200;
340
- // Roots
341
- _roots = [];
342
- /**
343
- * Enable auto-reconnect behavior.
344
- * Only applies to interactive REPL mode — proxy mode manages its own lifecycle.
345
- */
346
- enableAutoReconnect() {
347
- this._autoReconnect = true;
433
+ return {
434
+ cmd: input3.slice(0, spaceIdx).toLowerCase(),
435
+ rest: input3.slice(spaceIdx + 1)
436
+ };
437
+ }
438
+ function parseCallArgs(rest) {
439
+ const trimmed = rest.trim();
440
+ if (!trimmed) return { toolName: "", jsonArgs: "" };
441
+ const spaceIdx = trimmed.indexOf(" ");
442
+ if (spaceIdx === -1) {
443
+ return { toolName: trimmed, jsonArgs: "" };
348
444
  }
349
- /**
350
- * Spawn the target MCP server and establish the MCP client connection.
351
- * Stderr from the child process is emitted as 'stderr' events.
352
- */
353
- async connect() {
354
- this._intentionalClose = false;
355
- if (this.command.startsWith("http://") || this.command.startsWith("https://")) {
356
- this.transport = new SSEClientTransport(new URL(this.command));
357
- } else {
358
- const stdioTransport = new StdioClientTransport({
359
- command: this.command,
360
- args: this.args,
361
- stderr: "pipe"
362
- });
363
- stdioTransport.stderr?.on("data", (chunk) => {
364
- const text = chunk.toString().trimEnd();
365
- if (text) {
366
- const lines = text.split("\n");
367
- this._stderrLineCount += lines.length;
368
- this._stderrLines.push(...lines);
369
- if (this._stderrLines.length > _TargetManager.MAX_STDERR_LINES) {
370
- this._stderrLines = this._stderrLines.slice(-_TargetManager.MAX_STDERR_LINES);
371
- }
372
- this.emit("stderr", text);
373
- }
374
- });
375
- this.transport = stdioTransport;
376
- }
377
- this.client = new Client(
378
- { name: "run-mcp", version: "1.6.0" },
379
- {
380
- capabilities: {
381
- roots: { listChanged: true },
382
- sampling: {},
383
- elicitation: {}
384
- }
385
- }
386
- );
387
- this.client.setNotificationHandler(
388
- LoggingMessageNotificationSchema,
389
- async (notification) => {
390
- const record = {
391
- method: "notifications/message",
392
- params: notification.params,
393
- timestamp: Date.now()
394
- };
395
- this._pushNotification(record);
396
- this.emit("notification", record);
397
- }
398
- );
399
- this.client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
400
- const record = {
401
- method: "notifications/tools/list_changed",
402
- timestamp: Date.now()
403
- };
404
- this._pushNotification(record);
405
- this.emit("notification", record);
406
- });
407
- this.client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
408
- const record = {
409
- method: "notifications/resources/list_changed",
410
- timestamp: Date.now()
411
- };
412
- this._pushNotification(record);
413
- this.emit("notification", record);
414
- });
415
- this.client.setNotificationHandler(
416
- ResourceUpdatedNotificationSchema,
417
- async (notification) => {
418
- const record = {
419
- method: "notifications/resources/updated",
420
- params: notification.params,
421
- timestamp: Date.now()
422
- };
423
- this._pushNotification(record);
424
- this.emit("notification", record);
445
+ const toolName = trimmed.slice(0, spaceIdx);
446
+ let remainder = trimmed.slice(spaceIdx + 1).trim();
447
+ let timeoutMs;
448
+ const timeoutMatch = remainder.match(/\s--timeout\s+(\d+)\s*$/);
449
+ if (timeoutMatch) {
450
+ timeoutMs = parseInt(timeoutMatch[1], 10);
451
+ remainder = remainder.slice(0, timeoutMatch.index).trim();
452
+ }
453
+ return { toolName, jsonArgs: remainder, timeoutMs };
454
+ }
455
+ function formatJson(obj, indent = 2, colorize = false) {
456
+ const json = JSON.stringify(obj, null, indent);
457
+ const output = colorize ? colorizeJson(json) : json;
458
+ return output.split("\n").map((line) => " ".repeat(indent) + line).join("\n");
459
+ }
460
+ function colorizeJson(json) {
461
+ const result = [];
462
+ let i = 0;
463
+ let expectingValue = false;
464
+ while (i < json.length) {
465
+ const ch = json[i];
466
+ if (ch === '"') {
467
+ const str = consumeString(json, i);
468
+ if (expectingValue) {
469
+ result.push(pc.green(str));
470
+ expectingValue = false;
471
+ } else {
472
+ result.push(pc.cyan(str));
425
473
  }
426
- );
427
- this.client.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
428
- const record = {
429
- method: "notifications/prompts/list_changed",
430
- timestamp: Date.now()
431
- };
432
- this._pushNotification(record);
433
- this.emit("notification", record);
434
- });
435
- this.client.setRequestHandler(CreateMessageRequestSchema, async (request) => {
436
- return new Promise((resolve, reject) => {
437
- const timeout = setTimeout(() => {
438
- reject(new Error("Sampling request timed out (no response from user in 5 minutes)"));
439
- }, 3e5);
440
- this.emit("sampling_request", {
441
- request: request.params,
442
- respond: (result) => {
443
- clearTimeout(timeout);
444
- resolve(result);
445
- },
446
- reject: (err) => {
447
- clearTimeout(timeout);
448
- reject(err);
449
- }
450
- });
451
- });
452
- });
453
- this.client.setRequestHandler(ElicitRequestSchema, async (request) => {
454
- return new Promise((resolve, reject) => {
455
- const timeout = setTimeout(() => {
456
- reject(new Error("Elicitation request timed out (no response from user in 5 minutes)"));
457
- }, 3e5);
458
- this.emit("elicitation_request", {
459
- request: request.params,
460
- respond: (result) => {
461
- clearTimeout(timeout);
462
- resolve(result);
463
- },
464
- reject: (err) => {
465
- clearTimeout(timeout);
466
- reject(err);
467
- }
468
- });
469
- });
470
- });
471
- this.client.setRequestHandler(ListRootsRequestSchema, async () => {
472
- return { roots: this._roots };
473
- });
474
- this.client.onclose = () => {
475
- this._connected = false;
476
- this._clearStableTimer();
477
- if (this._intentionalClose) {
478
- return;
474
+ i += str.length;
475
+ continue;
476
+ }
477
+ if (ch === ":") {
478
+ result.push(ch);
479
+ expectingValue = true;
480
+ i++;
481
+ continue;
482
+ }
483
+ if (ch === "," || ch === "}" || ch === "]") {
484
+ result.push(ch);
485
+ expectingValue = false;
486
+ i++;
487
+ continue;
488
+ }
489
+ if (ch === "{" || ch === "[") {
490
+ result.push(ch);
491
+ if (ch === "[") expectingValue = true;
492
+ i++;
493
+ continue;
494
+ }
495
+ if (json.startsWith("true", i)) {
496
+ result.push(pc.magenta("true"));
497
+ expectingValue = false;
498
+ i += 4;
499
+ continue;
500
+ }
501
+ if (json.startsWith("false", i)) {
502
+ result.push(pc.magenta("false"));
503
+ expectingValue = false;
504
+ i += 5;
505
+ continue;
506
+ }
507
+ if (json.startsWith("null", i)) {
508
+ result.push(pc.dim("null"));
509
+ expectingValue = false;
510
+ i += 4;
511
+ continue;
512
+ }
513
+ if (ch === "-" || ch >= "0" && ch <= "9") {
514
+ let num = "";
515
+ while (i < json.length && /[0-9.eE+-]/.test(json[i])) {
516
+ num += json[i];
517
+ i++;
479
518
  }
480
- this.emit("disconnected");
481
- this._maybeReconnect();
482
- };
483
- await this.client.connect(this.transport);
484
- this._connected = true;
485
- this.startTime = Date.now();
486
- const proc = this.transport._process;
487
- if (proc?.pid) {
488
- this.childPid = proc.pid;
489
- } else {
490
- this.childPid = null;
519
+ result.push(pc.yellow(num));
520
+ expectingValue = false;
521
+ continue;
491
522
  }
492
- this.emit("connected");
493
- this._registerCleanup();
494
- this._startStableTimer();
495
- }
496
- get connected() {
497
- return this._connected;
523
+ result.push(ch);
524
+ i++;
498
525
  }
499
- /**
500
- * Record that a response was received (for status tracking).
501
- */
502
- recordResponse() {
503
- this._lastResponseTime = Date.now();
504
- }
505
- // ─── Server introspection ───────────────────────────────────────────────────
506
- /**
507
- * Returns the target server's advertised capabilities.
508
- * Available after connect() completes.
509
- */
510
- getServerCapabilities() {
511
- return this.client?.getServerCapabilities();
526
+ return result.join("");
527
+ }
528
+ function consumeString(json, start) {
529
+ let i = start + 1;
530
+ while (i < json.length) {
531
+ if (json[i] === "\\") {
532
+ i += 2;
533
+ continue;
534
+ }
535
+ if (json[i] === '"') {
536
+ return json.slice(start, i + 1);
537
+ }
538
+ i++;
512
539
  }
513
- /**
514
- * Returns the target server's instructions string (if any).
515
- * Agents may use this for system prompts or behavioral hints.
516
- */
517
- getInstructions() {
518
- return this.client?.getInstructions();
540
+ return json.slice(start);
541
+ }
542
+ function levenshtein(a, b) {
543
+ const m = a.length;
544
+ const n = b.length;
545
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
546
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
547
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
548
+ for (let i = 1; i <= m; i++) {
549
+ for (let j = 1; j <= n; j++) {
550
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
551
+ }
519
552
  }
520
- /**
521
- * Returns the target server's name and version from the MCP handshake.
522
- * Available after connect() completes.
523
- */
524
- getServerVersion() {
525
- return this.client?.getServerVersion();
553
+ return dp[m][n];
554
+ }
555
+ function suggestCommand(input3, commands, threshold = 0.4) {
556
+ let best = null;
557
+ let bestDist = Infinity;
558
+ for (const cmd of commands) {
559
+ const dist = levenshtein(input3, cmd);
560
+ if (dist < bestDist) {
561
+ bestDist = dist;
562
+ best = cmd;
563
+ }
526
564
  }
527
- // ─── Ping ──────────────────────────────────────────────────────────────────
528
- /**
529
- * Send a ping to the target MCP server and return the round-trip time.
530
- */
531
- async ping() {
532
- this._assertConnected();
533
- const start = Date.now();
534
- await this.client.ping();
535
- const elapsed = Date.now() - start;
536
- this.recordResponse();
537
- this._addHistory("ping", void 0, { ok: true }, elapsed);
538
- return elapsed;
565
+ if (best && bestDist <= Math.ceil(input3.length * threshold)) {
566
+ return best;
539
567
  }
540
- // ─── Tools ──────────────────────────────────────────────────────────────────
541
- /**
542
- * List all tools exposed by the target MCP server.
543
- * Supports cursor-based pagination via params.
544
- */
545
- async listTools(params) {
546
- this._assertConnected();
547
- const start = Date.now();
548
- const result = await this.client.listTools(params);
549
- this.recordResponse();
550
- this._addHistory("tools/list", params, result, Date.now() - start);
551
- return result;
568
+ return null;
569
+ }
570
+ function scaffoldArgs(schema) {
571
+ return JSON.stringify(scaffoldObject(schema), null, 2);
572
+ }
573
+ function scaffoldValue(prop) {
574
+ if (Array.isArray(prop.enum) && prop.enum.length > 0) {
575
+ return prop.enum[0];
552
576
  }
553
- /**
554
- * Call a tool on the target MCP server.
555
- * We apply a massive SDK-level timeout (e.g. 10 hours) because we want to handle
556
- * timeouts in the interceptor via Promise.race, and we DO NOT want to send
557
- * protocol-level cancellation requests to the target server if the agent gives up.
558
- * This allows long-running builds (like mobile app compiling) to finish in the background.
559
- */
560
- async callTool(name, args = {}, _timeoutMs) {
561
- this._assertConnected();
562
- const requestOptions = { timeout: 36e5 * 10 };
563
- const start = Date.now();
564
- const result = await this.client.callTool(
565
- { name, arguments: args },
566
- void 0,
567
- requestOptions
568
- );
569
- this.recordResponse();
570
- this._addHistory(`tools/call ${name}`, args, result, Date.now() - start);
571
- return result;
577
+ const variants = prop.anyOf ?? prop.oneOf;
578
+ if (Array.isArray(variants) && variants.length > 0) {
579
+ return scaffoldValue(variants[0]);
572
580
  }
573
- // ─── Resources ──────────────────────────────────────────────────────────────
574
- /**
575
- * List resources exposed by the target MCP server.
576
- * Supports cursor-based pagination.
577
- */
578
- async listResources(params) {
579
- this._assertConnected();
580
- const start = Date.now();
581
- const result = await this.client.listResources(params);
582
- this.recordResponse();
583
- this._addHistory("resources/list", params, result, Date.now() - start);
584
- return result;
581
+ switch (prop.type) {
582
+ case "string":
583
+ return "<string>";
584
+ case "number":
585
+ case "integer":
586
+ return "<number>";
587
+ case "boolean":
588
+ return "<boolean>";
589
+ case "array": {
590
+ const items = prop.items;
591
+ return items ? [scaffoldValue(items)] : ["<item>"];
592
+ }
593
+ case "object":
594
+ return scaffoldObject(prop);
595
+ default:
596
+ return `<${prop.type ?? "unknown"}>`;
585
597
  }
586
- /**
587
- * List resource templates exposed by the target MCP server.
588
- * Supports cursor-based pagination.
589
- */
590
- async listResourceTemplates(params) {
591
- this._assertConnected();
592
- const start = Date.now();
593
- const result = await this.client.listResourceTemplates(params);
594
- this.recordResponse();
595
- this._addHistory("resources/templates/list", params, result, Date.now() - start);
598
+ }
599
+ function scaffoldObject(schema) {
600
+ const properties = schema.properties;
601
+ if (properties) {
602
+ const result = {};
603
+ for (const [key, prop] of Object.entries(properties)) {
604
+ result[key] = scaffoldValue(prop);
605
+ }
596
606
  return result;
597
607
  }
598
- /**
599
- * Read a specific resource by URI from the target MCP server.
600
- */
601
- async readResource(params) {
602
- this._assertConnected();
603
- const start = Date.now();
604
- const result = await this.client.readResource(params);
605
- this.recordResponse();
606
- this._addHistory(`resources/read ${params.uri}`, params, result, Date.now() - start);
607
- return result;
608
+ const additionalProperties = schema.additionalProperties;
609
+ if (additionalProperties && typeof additionalProperties === "object") {
610
+ return { "<key>": scaffoldValue(additionalProperties) };
608
611
  }
609
- /**
610
- * Subscribe to resource updates on the target MCP server.
611
- */
612
- async subscribeResource(params) {
613
- this._assertConnected();
614
- const start = Date.now();
615
- const result = await this.client.subscribeResource(params);
616
- this.recordResponse();
617
- this._addHistory(`resources/subscribe ${params.uri}`, params, result, Date.now() - start);
618
- return result;
612
+ if (additionalProperties === true || schema.type === "object" && !properties) {
613
+ return { "<key>": "<value>" };
619
614
  }
620
- /**
621
- * Unsubscribe from resource updates on the target MCP server.
622
- */
623
- async unsubscribeResource(params) {
624
- this._assertConnected();
625
- const start = Date.now();
626
- const result = await this.client.unsubscribeResource(params);
627
- this.recordResponse();
628
- this._addHistory(`resources/unsubscribe ${params.uri}`, params, result, Date.now() - start);
629
- return result;
615
+ return {};
616
+ }
617
+ function formatToolDescription(tool) {
618
+ const lines = [];
619
+ lines.push(` ${tool.name}`);
620
+ if (tool.description) {
621
+ lines.push(` ${tool.description}`);
630
622
  }
631
- // ─── Prompts ────────────────────────────────────────────────────────────────
632
- /**
633
- * List prompts exposed by the target MCP server.
634
- * Supports cursor-based pagination.
635
- */
636
- async listPrompts(params) {
637
- this._assertConnected();
638
- const start = Date.now();
639
- const result = await this.client.listPrompts(params);
640
- this.recordResponse();
641
- this._addHistory("prompts/list", params, result, Date.now() - start);
642
- return result;
623
+ const schema = tool.inputSchema ?? {};
624
+ const properties = schema.properties;
625
+ const required = schema.required ?? [];
626
+ if (properties && Object.keys(properties).length > 0) {
627
+ lines.push("");
628
+ lines.push(" Arguments:");
629
+ const nameWidth = Math.max(6, ...Object.keys(properties).map((n) => n.length));
630
+ const typeWidth = Math.max(4, ...Object.values(properties).map((p) => typeLabel(p).length));
631
+ for (const [name, prop] of Object.entries(properties)) {
632
+ const type = typeLabel(prop);
633
+ const req = required.includes(name) ? "(required)" : "(optional)";
634
+ const desc = prop.description ?? "";
635
+ lines.push(
636
+ ` ${name.padEnd(nameWidth)} ${type.padEnd(typeWidth)} ${req.padEnd(10)} ${desc}`
637
+ );
638
+ }
639
+ } else {
640
+ lines.push("");
641
+ lines.push(" No arguments required.");
643
642
  }
644
- /**
645
- * Get a specific prompt by name from the target MCP server.
646
- */
647
- async getPrompt(params) {
648
- this._assertConnected();
649
- const start = Date.now();
650
- const result = await this.client.getPrompt(params);
651
- this.recordResponse();
652
- this._addHistory(`prompts/get ${params.name}`, params, result, Date.now() - start);
653
- return result;
643
+ lines.push("");
644
+ lines.push(" Example:");
645
+ if (properties && Object.keys(properties).length > 0) {
646
+ const example = scaffoldObject(schema);
647
+ lines.push(` tools/call ${tool.name} ${JSON.stringify(example)}`);
648
+ } else {
649
+ lines.push(` tools/call ${tool.name}`);
654
650
  }
655
- // ─── Logging ────────────────────────────────────────────────────────────────
656
- /**
657
- * Set the logging level on the target MCP server.
658
- */
659
- async setLoggingLevel(level) {
660
- this._assertConnected();
661
- const start = Date.now();
662
- const result = await this.client.setLoggingLevel(level);
663
- this.recordResponse();
664
- this._addHistory(`logging/setLevel ${level}`, { level }, result, Date.now() - start);
665
- return result;
651
+ if (tool.annotations) {
652
+ const entries = Object.entries(tool.annotations).filter(([key]) => key !== "title");
653
+ if (entries.length > 0) {
654
+ lines.push("");
655
+ lines.push(" Annotations:");
656
+ for (const [key, value] of entries) {
657
+ lines.push(` ${key}: ${value}`);
658
+ }
659
+ }
666
660
  }
667
- // ─── Completion ─────────────────────────────────────────────────────────────
668
- /**
669
- * Request completion from the target MCP server (for autocomplete UX).
670
- */
671
- async complete(params) {
672
- this._assertConnected();
673
- const start = Date.now();
674
- const result = await this.client.complete(params);
675
- this.recordResponse();
676
- this._addHistory("completion/complete", params, result, Date.now() - start);
677
- return result;
661
+ return lines.join("\n");
662
+ }
663
+ function typeLabel(prop) {
664
+ const type = prop.type;
665
+ if (!type) return "any";
666
+ if (type === "array") {
667
+ const items = prop.items;
668
+ return items ? `${typeLabel(items)}[]` : "array";
678
669
  }
679
- // ─── Request History ────────────────────────────────────────────────────────
680
- /**
681
- * Get the request/response history.
682
- * @param count - Number of recent records to return (default: all)
683
- */
684
- getHistory(count) {
685
- if (!count || count >= this._history.length) return [...this._history];
686
- return this._history.slice(-count);
670
+ return type;
671
+ }
672
+ function groupToolsByPrefix(toolNames) {
673
+ const groups = /* @__PURE__ */ new Map();
674
+ for (const name of toolNames) {
675
+ const underscoreIdx = name.indexOf("_");
676
+ const prefix = underscoreIdx > 0 ? name.slice(0, underscoreIdx) : name;
677
+ const list = groups.get(prefix) ?? [];
678
+ list.push(name);
679
+ groups.set(prefix, list);
687
680
  }
688
- /**
689
- * Clear the history buffer.
690
- */
691
- clearHistory() {
692
- this._history = [];
681
+ const meaningfulGroups = [...groups.entries()].filter(([, members]) => members.length >= 2);
682
+ if (meaningfulGroups.length < 2) {
683
+ const all = /* @__PURE__ */ new Map();
684
+ all.set("All", [...toolNames]);
685
+ return all;
693
686
  }
694
- _addHistory(method, params, result, durationMs) {
695
- const record = {
696
- id: ++this._historyIdCounter,
697
- method,
698
- params,
699
- result,
700
- durationMs,
701
- timestamp: Date.now()
702
- };
703
- this._history.push(record);
704
- if (this._history.length > MAX_HISTORY) {
705
- this._history = this._history.slice(-MAX_HISTORY);
687
+ const result = /* @__PURE__ */ new Map();
688
+ const other = [];
689
+ for (const [prefix, members] of groups) {
690
+ if (members.length >= 2) {
691
+ const label = prefix.charAt(0).toUpperCase() + prefix.slice(1);
692
+ result.set(label, members);
693
+ } else {
694
+ other.push(...members);
706
695
  }
707
696
  }
708
- // ─── Notification History ───────────────────────────────────────────────────
709
- /**
710
- * Get recent server notifications.
711
- * @param count - Number of recent notifications to return (default: all)
712
- */
713
- getNotifications(count) {
714
- if (!count || count >= this._notifications.length) return [...this._notifications];
715
- return this._notifications.slice(-count);
716
- }
717
- /**
718
- * Clear the notification buffer.
719
- */
720
- clearNotifications() {
721
- this._notifications = [];
697
+ if (other.length > 0) {
698
+ result.set("Other", other);
722
699
  }
723
- _pushNotification(record) {
724
- this._notifications.push(record);
725
- if (this._notifications.length > _TargetManager.MAX_NOTIFICATIONS) {
726
- this._notifications = this._notifications.slice(-_TargetManager.MAX_NOTIFICATIONS);
700
+ return result;
701
+ }
702
+ var LOG_LEVELS = [
703
+ "debug",
704
+ "info",
705
+ "notice",
706
+ "warning",
707
+ "error",
708
+ "critical",
709
+ "alert",
710
+ "emergency"
711
+ ];
712
+ var ALIASES = {
713
+ tl: "tools/list",
714
+ td: "tools/describe",
715
+ tc: "tools/call",
716
+ ts: "tools/scaffold",
717
+ rl: "resources/list",
718
+ rr: "resources/read",
719
+ rt: "resources/templates",
720
+ rs: "resources/subscribe",
721
+ ru: "resources/unsubscribe",
722
+ pl: "prompts/list",
723
+ pg: "prompts/get"
724
+ };
725
+ function resolveAlias(input3) {
726
+ const spaceIdx = input3.indexOf(" ");
727
+ const token = spaceIdx === -1 ? input3 : input3.slice(0, spaceIdx);
728
+ const rest = spaceIdx === -1 ? "" : input3.slice(spaceIdx);
729
+ const expanded = ALIASES[token.toLowerCase()];
730
+ if (!expanded) return null;
731
+ return expanded + rest;
732
+ }
733
+ function splitArgs(input3) {
734
+ const tokens = [];
735
+ let current = "";
736
+ let inDoubleQuote = false;
737
+ let inSingleQuote = false;
738
+ let escape = false;
739
+ for (let i = 0; i < input3.length; i++) {
740
+ const ch = input3[i];
741
+ if (escape) {
742
+ current += ch;
743
+ escape = false;
744
+ continue;
727
745
  }
728
- }
729
- // ─── Roots Management ─────────────────────────────────────────────────────
730
- /**
731
- * Get the current roots list that this client advertises.
732
- */
733
- getRoots() {
734
- return [...this._roots];
735
- }
736
- /**
737
- * Add a root and send notification to the server.
738
- */
739
- async addRoot(root) {
740
- if (this._roots.some((r) => r.uri === root.uri)) return;
741
- this._roots.push(root);
742
- await this._sendRootsChanged();
743
- }
744
- /**
745
- * Remove a root by URI and send notification to the server.
746
- */
747
- async removeRoot(uri) {
748
- const before = this._roots.length;
749
- this._roots = this._roots.filter((r) => r.uri !== uri);
750
- if (this._roots.length < before) {
751
- await this._sendRootsChanged();
752
- return true;
746
+ if (ch === "\\") {
747
+ escape = true;
748
+ continue;
753
749
  }
754
- return false;
755
- }
756
- async _sendRootsChanged() {
757
- if (!this._connected || !this.client) return;
758
- try {
759
- await this.client.sendRootsListChanged();
760
- } catch {
750
+ if (ch === '"' && !inSingleQuote) {
751
+ inDoubleQuote = !inDoubleQuote;
752
+ current += ch;
753
+ continue;
761
754
  }
755
+ if (ch === "'" && !inDoubleQuote) {
756
+ inSingleQuote = !inSingleQuote;
757
+ current += ch;
758
+ continue;
759
+ }
760
+ if (ch === " " && !inDoubleQuote && !inSingleQuote) {
761
+ if (current.trim()) {
762
+ tokens.push(current.trim());
763
+ }
764
+ current = "";
765
+ continue;
766
+ }
767
+ current += ch;
762
768
  }
763
- // ─── Notification forwarding ────────────────────────────────────────────────
764
- /**
765
- * Access the underlying MCP client for advanced use cases like
766
- * subscribing to notifications with proper SDK schemas.
767
- * Prefer the typed methods above when possible.
768
- */
769
- getRawClient() {
770
- return this.client;
771
- }
772
- // ─── Status & lifecycle ─────────────────────────────────────────────────────
773
- /**
774
- * Returns the last N lines of stderr output from the target server.
775
- * Useful for debugging crashes or unexpected behavior.
776
- */
777
- getStderrLines(count) {
778
- if (!count || count >= this._stderrLines.length) return [...this._stderrLines];
779
- return this._stderrLines.slice(-count);
780
- }
781
- /**
782
- * Returns current connection status, PID, uptime, and diagnostics.
783
- */
784
- getStatus() {
785
- return {
786
- pid: this.childPid,
787
- uptime: this._connected ? (Date.now() - this.startTime) / 1e3 : 0,
788
- connected: this._connected,
789
- command: this.command,
790
- args: this.args,
791
- lastResponseTime: this._lastResponseTime,
792
- stderrLineCount: this._stderrLineCount,
793
- reconnectAttempts: this._reconnectAttempts,
794
- maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS
795
- };
769
+ if (current.trim()) {
770
+ tokens.push(current.trim());
796
771
  }
797
- /**
798
- * Cleanly shut down the client connection and child process.
799
- */
800
- async close() {
801
- this._intentionalClose = true;
802
- this._clearStableTimer();
803
- if (this.client) {
804
- try {
805
- await this.client.close();
806
- } catch {
807
- }
808
- this.client = null;
772
+ return tokens;
773
+ }
774
+ function parseHttpieArgs(argsString) {
775
+ const result = {};
776
+ const trimmedArgs = argsString.trim();
777
+ if (!trimmedArgs) return result;
778
+ const tokens = splitArgs(trimmedArgs);
779
+ for (const token of tokens) {
780
+ const eqIdx = token.indexOf("=");
781
+ if (eqIdx === -1) continue;
782
+ const isJson = eqIdx > 0 && token[eqIdx - 1] === ":";
783
+ const key = isJson ? token.slice(0, eqIdx - 1).trim() : token.slice(0, eqIdx).trim();
784
+ let rawVal = token.slice(eqIdx + 1).trim();
785
+ if (rawVal.startsWith('"') && rawVal.endsWith('"') || rawVal.startsWith("'") && rawVal.endsWith("'")) {
786
+ rawVal = rawVal.slice(1, -1);
809
787
  }
810
- if (this.transport) {
788
+ if (isJson) {
811
789
  try {
812
- await this.transport.close();
790
+ result[key] = JSON.parse(rawVal);
813
791
  } catch {
792
+ result[key] = rawVal;
814
793
  }
815
- this.transport = null;
794
+ } else {
795
+ result[key] = rawVal;
816
796
  }
817
- this._connected = false;
818
- this.childPid = null;
819
797
  }
820
- // ─── Auto-reconnect logic ──────────────────────────────────────────────────
821
- /**
822
- * Decide whether to attempt auto-reconnect after a disconnect.
823
- *
824
- * Rules:
825
- * 1. Auto-reconnect must be enabled
826
- * 2. Server must have been alive for ≥5s (otherwise it's a startup bug)
827
- * 3. Must not exceed MAX_RECONNECT_ATTEMPTS consecutive retries
828
- * 4. Must not already be reconnecting
829
- */
830
- async _maybeReconnect() {
831
- if (!this._autoReconnect || this._reconnecting) return;
832
- const uptimeMs = Date.now() - this.startTime;
833
- if (uptimeMs < MIN_UPTIME_FOR_RESTART_MS) {
834
- this.emit("reconnect_failed", {
835
- reason: "startup_crash",
836
- message: `Server crashed after ${(uptimeMs / 1e3).toFixed(1)}s \u2014 too soon to be a transient failure (min ${MIN_UPTIME_FOR_RESTART_MS / 1e3}s). Not retrying.`
837
- });
838
- return;
798
+ return result;
799
+ }
800
+ function resolveJsonPath(obj, path2) {
801
+ const parts = path2.replace(/\["([^"]+)"\]/g, ".$1").replace(/\['([^']+)'\]/g, ".$1").replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
802
+ let current = obj;
803
+ for (const part of parts) {
804
+ if (current === void 0 || current === null) return void 0;
805
+ current = current[part];
806
+ }
807
+ return current;
808
+ }
809
+ function interpolateString(input3, context) {
810
+ const regex = /\$([a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])?((?:\.[a-zA-Z0-9_]+|\[\d+\]|\["[^"]+"\]|\['[^']+'\])*)/g;
811
+ return input3.replace(regex, (match, root, path2) => {
812
+ let baseName = root;
813
+ let fullPath = path2 || "";
814
+ if (baseName && baseName.startsWith("[")) {
815
+ fullPath = baseName + fullPath;
816
+ baseName = "LAST";
839
817
  }
840
- if (this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
841
- this.emit("reconnect_failed", {
842
- reason: "max_retries",
843
- message: `Server has crashed ${this._reconnectAttempts} times in a row. Giving up.`
844
- });
845
- return;
818
+ if (!baseName) {
819
+ baseName = "LAST";
846
820
  }
847
- this._reconnecting = true;
848
- this._reconnectAttempts++;
849
- this.emit("reconnecting", {
850
- attempt: this._reconnectAttempts,
851
- maxAttempts: MAX_RECONNECT_ATTEMPTS
852
- });
853
- this.client = null;
854
- this.transport = null;
855
- this.childPid = null;
856
- try {
857
- await this.connect();
858
- this.emit("reconnected", { attempt: this._reconnectAttempts });
859
- } catch (err) {
860
- this.emit("reconnect_failed", {
861
- reason: "connect_error",
862
- message: `Reconnect attempt ${this._reconnectAttempts} failed: ${err.message}`
863
- });
864
- } finally {
865
- this._reconnecting = false;
821
+ if (!(baseName in context)) {
822
+ return match;
823
+ }
824
+ const value = resolveJsonPath(context[baseName], fullPath);
825
+ if (value === void 0) {
826
+ return match;
866
827
  }
828
+ if (typeof value === "object") {
829
+ return JSON.stringify(value);
830
+ }
831
+ return String(value);
832
+ });
833
+ }
834
+
835
+ // src/target-manager.ts
836
+ import { EventEmitter } from "events";
837
+ import treeKill from "tree-kill";
838
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
839
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
840
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
841
+ import {
842
+ CreateMessageRequestSchema,
843
+ ElicitRequestSchema,
844
+ ListRootsRequestSchema,
845
+ LoggingMessageNotificationSchema,
846
+ PromptListChangedNotificationSchema,
847
+ ResourceListChangedNotificationSchema,
848
+ ResourceUpdatedNotificationSchema,
849
+ ToolListChangedNotificationSchema
850
+ } from "@modelcontextprotocol/sdk/types.js";
851
+ var MIN_UPTIME_FOR_RESTART_MS = 5e3;
852
+ var MAX_RECONNECT_ATTEMPTS = 3;
853
+ var STABLE_CONNECTION_RESET_MS = 6e4;
854
+ var MAX_HISTORY = 100;
855
+ var TargetManager = class _TargetManager extends EventEmitter {
856
+ constructor(command, args) {
857
+ super();
858
+ this.command = command;
859
+ this.args = args;
867
860
  }
861
+ client = null;
862
+ transport = null;
863
+ startTime = 0;
864
+ childPid = null;
865
+ _connected = false;
866
+ // Enhanced status tracking
867
+ _lastResponseTime = null;
868
+ _stderrLineCount = 0;
869
+ _stderrLines = [];
870
+ static MAX_STDERR_LINES = 200;
871
+ // Auto-reconnect state
872
+ _reconnectAttempts = 0;
873
+ _stableTimer = null;
874
+ _autoReconnect = false;
875
+ _reconnecting = false;
876
+ _intentionalClose = false;
877
+ _everConnected = false;
878
+ // Request history
879
+ _history = [];
880
+ _historyIdCounter = 0;
881
+ // Notifications
882
+ _notifications = [];
883
+ static MAX_NOTIFICATIONS = 200;
884
+ // Roots
885
+ _roots = [];
868
886
  /**
869
- * After STABLE_CONNECTION_RESET_MS of being connected, reset the retry counter.
870
- * This way, a server that crashes once after 10 minutes of stability
871
- * gets a fresh set of retries.
887
+ * Enable auto-reconnect behavior.
888
+ * Only applies to interactive REPL mode proxy mode manages its own lifecycle.
872
889
  */
873
- _startStableTimer() {
874
- this._clearStableTimer();
875
- this._stableTimer = setTimeout(() => {
876
- if (this._connected) {
877
- this._reconnectAttempts = 0;
878
- }
879
- }, STABLE_CONNECTION_RESET_MS);
880
- }
881
- _clearStableTimer() {
882
- if (this._stableTimer) {
883
- clearTimeout(this._stableTimer);
884
- this._stableTimer = null;
885
- }
886
- }
887
- // ─── Internal helpers ──────────────────────────────────────────────────────
888
- _assertConnected() {
889
- if (!this._connected || !this.client) {
890
- throw new Error("Not connected to target MCP server");
891
- }
890
+ enableAutoReconnect() {
891
+ this._autoReconnect = true;
892
892
  }
893
- static _cleanupRegistered = false;
894
- static _instances = /* @__PURE__ */ new Set();
895
- _registerCleanup() {
896
- _TargetManager._instances.add(this);
897
- if (_TargetManager._cleanupRegistered) return;
898
- _TargetManager._cleanupRegistered = true;
899
- const cleanupAll = () => {
900
- for (const instance of _TargetManager._instances) {
901
- instance.close().catch(() => {
893
+ /**
894
+ * Spawn the target MCP server and establish the MCP client connection.
895
+ * Stderr from the child process is emitted as 'stderr' events.
896
+ */
897
+ async connect() {
898
+ this._intentionalClose = false;
899
+ this._everConnected = false;
900
+ try {
901
+ if (this.command.startsWith("http://") || this.command.startsWith("https://")) {
902
+ this.transport = new SSEClientTransport(new URL(this.command));
903
+ } else {
904
+ const stdioTransport = new StdioClientTransport({
905
+ command: this.command,
906
+ args: this.args,
907
+ stderr: "pipe"
908
+ });
909
+ stdioTransport.stderr?.on("data", (chunk) => {
910
+ const text = chunk.toString().trimEnd();
911
+ if (text) {
912
+ const lines = text.split("\n");
913
+ this._stderrLineCount += lines.length;
914
+ this._stderrLines.push(...lines);
915
+ if (this._stderrLines.length > _TargetManager.MAX_STDERR_LINES) {
916
+ this._stderrLines = this._stderrLines.slice(-_TargetManager.MAX_STDERR_LINES);
917
+ }
918
+ this.emit("stderr", text);
919
+ }
902
920
  });
921
+ this.transport = stdioTransport;
903
922
  }
904
- };
905
- process.on("exit", cleanupAll);
906
- process.on("SIGINT", () => {
907
- cleanupAll();
908
- process.exit(130);
909
- });
910
- process.on("SIGTERM", () => {
911
- cleanupAll();
912
- process.exit(143);
913
- });
914
- }
915
- };
916
-
917
- // src/headless.ts
918
- var DEFAULT_HEADLESS_TIMEOUT_MS = 3e4;
919
- async function runHeadless(targetCommand, operation, opts = {}) {
920
- const [command, ...args] = targetCommand;
921
- const target = new TargetManager(command, args);
922
- const interceptor = new ResponseInterceptor({
923
- outDir: opts.outDir,
924
- defaultTimeoutMs: opts.timeoutMs ?? DEFAULT_HEADLESS_TIMEOUT_MS
925
- });
926
- target.on("stderr", () => {
927
- });
928
- try {
929
- process.stderr.write(`Connecting to ${targetCommand.join(" ")}...
930
- `);
931
- await target.connect();
932
- const status = target.getStatus();
933
- process.stderr.write(`Connected (PID: ${status.pid})
934
- `);
935
- const result = await executeOperation(target, interceptor, operation, opts);
936
- process.stdout.write(`${JSON.stringify(result, null, 2)}
937
- `);
938
- await target.close();
939
- process.exit(0);
940
- } catch (err) {
941
- const msg = err.message ?? String(err);
942
- if (msg.includes("ENOENT") || msg.includes("spawn")) {
943
- process.stderr.write(
944
- `Error: command "${command}" not found. Check that it is installed and in your PATH.
945
- `
923
+ this.client = new Client(
924
+ { name: "run-mcp", version: "1.6.1" },
925
+ {
926
+ capabilities: {
927
+ roots: { listChanged: true },
928
+ sampling: {},
929
+ elicitation: {}
930
+ }
931
+ }
946
932
  );
947
- } else if (msg.includes("timed out")) {
948
- process.stderr.write(`Error: ${msg}
949
- `);
950
- } else {
951
- process.stderr.write(`Error: ${msg}
952
- `);
953
- }
954
- await target.close().catch(() => {
955
- });
956
- process.exit(1);
957
- }
958
- }
959
- async function executeOperation(target, interceptor, operation, opts) {
960
- switch (operation.type) {
961
- case "call": {
962
- let parsedArgs = {};
963
- if (operation.args) {
964
- try {
965
- parsedArgs = JSON.parse(operation.args);
966
- } catch (err) {
967
- process.stderr.write(`Error: Invalid JSON arguments: ${err.message}
968
- `);
969
- process.stderr.write(` Received: ${operation.args}
970
- `);
971
- process.exit(2);
933
+ this.client.setNotificationHandler(
934
+ LoggingMessageNotificationSchema,
935
+ async (notification) => {
936
+ const record = {
937
+ method: "notifications/message",
938
+ params: notification.params,
939
+ timestamp: Date.now()
940
+ };
941
+ this._pushNotification(record);
942
+ this.emit("notification", record);
972
943
  }
973
- }
974
- const result = await interceptor.callTool(target, operation.tool, parsedArgs);
975
- if (result.isError) {
976
- const content = result.content;
977
- if (Array.isArray(content)) {
978
- const errorText = content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
979
- if (errorText) {
980
- process.stderr.write(`Tool error: ${errorText}
981
- `);
982
- }
944
+ );
945
+ this.client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
946
+ const record = {
947
+ method: "notifications/tools/list_changed",
948
+ timestamp: Date.now()
949
+ };
950
+ this._pushNotification(record);
951
+ this.emit("notification", record);
952
+ });
953
+ this.client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
954
+ const record = {
955
+ method: "notifications/resources/list_changed",
956
+ timestamp: Date.now()
957
+ };
958
+ this._pushNotification(record);
959
+ this.emit("notification", record);
960
+ });
961
+ this.client.setNotificationHandler(
962
+ ResourceUpdatedNotificationSchema,
963
+ async (notification) => {
964
+ const record = {
965
+ method: "notifications/resources/updated",
966
+ params: notification.params,
967
+ timestamp: Date.now()
968
+ };
969
+ this._pushNotification(record);
970
+ this.emit("notification", record);
983
971
  }
984
- if (opts.raw) return result;
985
- return result.content ?? result;
986
- }
987
- if (opts.raw) return result;
988
- return result.content ?? result;
989
- }
990
- case "list-tools": {
991
- const { tools } = await target.listTools();
992
- return tools;
993
- }
994
- case "list-resources": {
995
- const { resources } = await target.listResources();
996
- return resources;
997
- }
998
- case "list-prompts": {
999
- const { prompts } = await target.listPrompts();
1000
- return prompts;
1001
- }
1002
- case "read": {
1003
- const result = await target.readResource({ uri: operation.uri });
1004
- return result;
1005
- }
1006
- case "describe": {
1007
- const { tools } = await target.listTools();
1008
- const tool = tools.find((t) => t.name === operation.tool);
1009
- if (!tool) {
1010
- const available = tools.map((t) => t.name).join(", ");
1011
- process.stderr.write(
1012
- `Error: Tool "${operation.tool}" not found.
1013
- Available tools: ${available}
1014
- `
1015
- );
1016
- process.exit(1);
972
+ );
973
+ this.client.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
974
+ const record = {
975
+ method: "notifications/prompts/list_changed",
976
+ timestamp: Date.now()
977
+ };
978
+ this._pushNotification(record);
979
+ this.emit("notification", record);
980
+ });
981
+ this.client.setRequestHandler(CreateMessageRequestSchema, async (request) => {
982
+ return new Promise((resolve2, reject) => {
983
+ const timeout = setTimeout(() => {
984
+ reject(new Error("Sampling request timed out (no response from user in 5 minutes)"));
985
+ }, 3e5);
986
+ this.emit("sampling_request", {
987
+ request: request.params,
988
+ respond: (result) => {
989
+ clearTimeout(timeout);
990
+ resolve2(result);
991
+ },
992
+ reject: (err) => {
993
+ clearTimeout(timeout);
994
+ reject(err);
995
+ }
996
+ });
997
+ });
998
+ });
999
+ this.client.setRequestHandler(ElicitRequestSchema, async (request) => {
1000
+ return new Promise((resolve2, reject) => {
1001
+ const timeout = setTimeout(() => {
1002
+ reject(new Error("Elicitation request timed out (no response from user in 5 minutes)"));
1003
+ }, 3e5);
1004
+ this.emit("elicitation_request", {
1005
+ request: request.params,
1006
+ respond: (result) => {
1007
+ clearTimeout(timeout);
1008
+ resolve2(result);
1009
+ },
1010
+ reject: (err) => {
1011
+ clearTimeout(timeout);
1012
+ reject(err);
1013
+ }
1014
+ });
1015
+ });
1016
+ });
1017
+ this.client.setRequestHandler(ListRootsRequestSchema, async () => {
1018
+ return { roots: this._roots };
1019
+ });
1020
+ this.client.onclose = () => {
1021
+ this._connected = false;
1022
+ this._clearStableTimer();
1023
+ if (this._intentionalClose || !this._everConnected) {
1024
+ return;
1025
+ }
1026
+ this.emit("disconnected");
1027
+ this._maybeReconnect();
1028
+ };
1029
+ await this.client.connect(this.transport);
1030
+ this._connected = true;
1031
+ this._everConnected = true;
1032
+ this.startTime = Date.now();
1033
+ const proc = this.transport._process;
1034
+ if (proc?.pid) {
1035
+ this.childPid = proc.pid;
1036
+ } else {
1037
+ this.childPid = null;
1017
1038
  }
1018
- return tool;
1019
- }
1020
- case "get-prompt": {
1021
- let parsedArgs;
1022
- if (operation.args) {
1023
- try {
1024
- parsedArgs = JSON.parse(operation.args);
1025
- } catch (err) {
1026
- process.stderr.write(`Error: Invalid JSON arguments: ${err.message}
1027
- `);
1028
- process.stderr.write(` Received: ${operation.args}
1029
- `);
1030
- process.exit(2);
1031
- }
1032
- }
1033
- const result = await target.getPrompt({
1034
- name: operation.name,
1035
- arguments: parsedArgs
1036
- });
1037
- return result;
1039
+ this.emit("connected");
1040
+ this._registerCleanup();
1041
+ this._startStableTimer();
1042
+ } catch (err) {
1043
+ await this.close().catch(() => {
1044
+ });
1045
+ throw err;
1038
1046
  }
1039
1047
  }
1040
- }
1041
-
1042
- // src/repl.ts
1043
- import { readFile as readFile2 } from "fs/promises";
1044
- import { createInterface } from "readline";
1045
- import { checkbox, confirm, input as input2, search } from "@inquirer/prompts";
1046
- import pc2 from "picocolors";
1047
-
1048
- // src/parsing.ts
1049
- import pc from "picocolors";
1050
- function parseCommandLine(input3) {
1051
- const spaceIdx = input3.indexOf(" ");
1052
- if (spaceIdx === -1) {
1053
- return { cmd: input3.toLowerCase(), rest: "" };
1048
+ get connected() {
1049
+ return this._connected;
1054
1050
  }
1055
- return {
1056
- cmd: input3.slice(0, spaceIdx).toLowerCase(),
1057
- rest: input3.slice(spaceIdx + 1)
1058
- };
1059
- }
1060
- function parseCallArgs(rest) {
1061
- const trimmed = rest.trim();
1062
- if (!trimmed) return { toolName: "", jsonArgs: "" };
1063
- const spaceIdx = trimmed.indexOf(" ");
1064
- if (spaceIdx === -1) {
1065
- return { toolName: trimmed, jsonArgs: "" };
1051
+ /**
1052
+ * Record that a response was received (for status tracking).
1053
+ */
1054
+ recordResponse() {
1055
+ this._lastResponseTime = Date.now();
1066
1056
  }
1067
- const toolName = trimmed.slice(0, spaceIdx);
1068
- let remainder = trimmed.slice(spaceIdx + 1).trim();
1069
- let timeoutMs;
1070
- const timeoutMatch = remainder.match(/\s--timeout\s+(\d+)\s*$/);
1071
- if (timeoutMatch) {
1072
- timeoutMs = parseInt(timeoutMatch[1], 10);
1073
- remainder = remainder.slice(0, timeoutMatch.index).trim();
1057
+ // ─── Server introspection ───────────────────────────────────────────────────
1058
+ /**
1059
+ * Returns the target server's advertised capabilities.
1060
+ * Available after connect() completes.
1061
+ */
1062
+ getServerCapabilities() {
1063
+ return this.client?.getServerCapabilities();
1074
1064
  }
1075
- return { toolName, jsonArgs: remainder, timeoutMs };
1076
- }
1077
- function formatJson(obj, indent = 2, colorize = false) {
1078
- const json = JSON.stringify(obj, null, indent);
1079
- const output = colorize ? colorizeJson(json) : json;
1080
- return output.split("\n").map((line) => " ".repeat(indent) + line).join("\n");
1081
- }
1082
- function colorizeJson(json) {
1083
- const result = [];
1084
- let i = 0;
1085
- let expectingValue = false;
1086
- while (i < json.length) {
1087
- const ch = json[i];
1088
- if (ch === '"') {
1089
- const str = consumeString(json, i);
1090
- if (expectingValue) {
1091
- result.push(pc.green(str));
1092
- expectingValue = false;
1093
- } else {
1094
- result.push(pc.cyan(str));
1095
- }
1096
- i += str.length;
1097
- continue;
1098
- }
1099
- if (ch === ":") {
1100
- result.push(ch);
1101
- expectingValue = true;
1102
- i++;
1103
- continue;
1104
- }
1105
- if (ch === "," || ch === "}" || ch === "]") {
1106
- result.push(ch);
1107
- expectingValue = false;
1108
- i++;
1109
- continue;
1110
- }
1111
- if (ch === "{" || ch === "[") {
1112
- result.push(ch);
1113
- if (ch === "[") expectingValue = true;
1114
- i++;
1115
- continue;
1116
- }
1117
- if (json.startsWith("true", i)) {
1118
- result.push(pc.magenta("true"));
1119
- expectingValue = false;
1120
- i += 4;
1121
- continue;
1122
- }
1123
- if (json.startsWith("false", i)) {
1124
- result.push(pc.magenta("false"));
1125
- expectingValue = false;
1126
- i += 5;
1127
- continue;
1128
- }
1129
- if (json.startsWith("null", i)) {
1130
- result.push(pc.dim("null"));
1131
- expectingValue = false;
1132
- i += 4;
1133
- continue;
1134
- }
1135
- if (ch === "-" || ch >= "0" && ch <= "9") {
1136
- let num = "";
1137
- while (i < json.length && /[0-9.eE+-]/.test(json[i])) {
1138
- num += json[i];
1139
- i++;
1140
- }
1141
- result.push(pc.yellow(num));
1142
- expectingValue = false;
1143
- continue;
1144
- }
1145
- result.push(ch);
1146
- i++;
1065
+ /**
1066
+ * Returns the target server's instructions string (if any).
1067
+ * Agents may use this for system prompts or behavioral hints.
1068
+ */
1069
+ getInstructions() {
1070
+ return this.client?.getInstructions();
1147
1071
  }
1148
- return result.join("");
1149
- }
1150
- function consumeString(json, start) {
1151
- let i = start + 1;
1152
- while (i < json.length) {
1153
- if (json[i] === "\\") {
1154
- i += 2;
1155
- continue;
1072
+ /**
1073
+ * Returns the target server's name and version from the MCP handshake.
1074
+ * Available after connect() completes.
1075
+ */
1076
+ getServerVersion() {
1077
+ return this.client?.getServerVersion();
1078
+ }
1079
+ // ─── Ping ──────────────────────────────────────────────────────────────────
1080
+ /**
1081
+ * Send a ping to the target MCP server and return the round-trip time.
1082
+ */
1083
+ async ping() {
1084
+ this._assertConnected();
1085
+ const start = Date.now();
1086
+ await this.client.ping();
1087
+ const elapsed = Date.now() - start;
1088
+ this.recordResponse();
1089
+ this._addHistory("ping", void 0, { ok: true }, elapsed);
1090
+ return elapsed;
1091
+ }
1092
+ // ─── Tools ──────────────────────────────────────────────────────────────────
1093
+ /**
1094
+ * List all tools exposed by the target MCP server.
1095
+ * Supports cursor-based pagination via params.
1096
+ */
1097
+ async listTools(params) {
1098
+ this._assertConnected();
1099
+ const start = Date.now();
1100
+ const result = await this.client.listTools(params);
1101
+ this.recordResponse();
1102
+ this._addHistory("tools/list", params, result, Date.now() - start);
1103
+ return result;
1104
+ }
1105
+ /**
1106
+ * Call a tool on the target MCP server.
1107
+ * We apply a massive SDK-level timeout (e.g. 10 hours) because we want to handle
1108
+ * timeouts in the interceptor via Promise.race, and we DO NOT want to send
1109
+ * protocol-level cancellation requests to the target server if the agent gives up.
1110
+ * This allows long-running builds (like mobile app compiling) to finish in the background.
1111
+ */
1112
+ async callTool(name, args = {}, _timeoutMs) {
1113
+ this._assertConnected();
1114
+ const requestOptions = { timeout: 36e5 * 10 };
1115
+ const start = Date.now();
1116
+ const result = await this.client.callTool(
1117
+ { name, arguments: args },
1118
+ void 0,
1119
+ requestOptions
1120
+ );
1121
+ this.recordResponse();
1122
+ this._addHistory(`tools/call ${name}`, args, result, Date.now() - start);
1123
+ return result;
1124
+ }
1125
+ // ─── Resources ──────────────────────────────────────────────────────────────
1126
+ /**
1127
+ * List resources exposed by the target MCP server.
1128
+ * Supports cursor-based pagination.
1129
+ */
1130
+ async listResources(params) {
1131
+ this._assertConnected();
1132
+ const start = Date.now();
1133
+ const result = await this.client.listResources(params);
1134
+ this.recordResponse();
1135
+ this._addHistory("resources/list", params, result, Date.now() - start);
1136
+ return result;
1137
+ }
1138
+ /**
1139
+ * List resource templates exposed by the target MCP server.
1140
+ * Supports cursor-based pagination.
1141
+ */
1142
+ async listResourceTemplates(params) {
1143
+ this._assertConnected();
1144
+ const start = Date.now();
1145
+ const result = await this.client.listResourceTemplates(params);
1146
+ this.recordResponse();
1147
+ this._addHistory("resources/templates/list", params, result, Date.now() - start);
1148
+ return result;
1149
+ }
1150
+ /**
1151
+ * Read a specific resource by URI from the target MCP server.
1152
+ */
1153
+ async readResource(params) {
1154
+ this._assertConnected();
1155
+ const start = Date.now();
1156
+ const result = await this.client.readResource(params);
1157
+ this.recordResponse();
1158
+ this._addHistory(`resources/read ${params.uri}`, params, result, Date.now() - start);
1159
+ return result;
1160
+ }
1161
+ /**
1162
+ * Subscribe to resource updates on the target MCP server.
1163
+ */
1164
+ async subscribeResource(params) {
1165
+ this._assertConnected();
1166
+ const start = Date.now();
1167
+ const result = await this.client.subscribeResource(params);
1168
+ this.recordResponse();
1169
+ this._addHistory(`resources/subscribe ${params.uri}`, params, result, Date.now() - start);
1170
+ return result;
1171
+ }
1172
+ /**
1173
+ * Unsubscribe from resource updates on the target MCP server.
1174
+ */
1175
+ async unsubscribeResource(params) {
1176
+ this._assertConnected();
1177
+ const start = Date.now();
1178
+ const result = await this.client.unsubscribeResource(params);
1179
+ this.recordResponse();
1180
+ this._addHistory(`resources/unsubscribe ${params.uri}`, params, result, Date.now() - start);
1181
+ return result;
1182
+ }
1183
+ // ─── Prompts ────────────────────────────────────────────────────────────────
1184
+ /**
1185
+ * List prompts exposed by the target MCP server.
1186
+ * Supports cursor-based pagination.
1187
+ */
1188
+ async listPrompts(params) {
1189
+ this._assertConnected();
1190
+ const start = Date.now();
1191
+ const result = await this.client.listPrompts(params);
1192
+ this.recordResponse();
1193
+ this._addHistory("prompts/list", params, result, Date.now() - start);
1194
+ return result;
1195
+ }
1196
+ /**
1197
+ * Get a specific prompt by name from the target MCP server.
1198
+ */
1199
+ async getPrompt(params) {
1200
+ this._assertConnected();
1201
+ const start = Date.now();
1202
+ const result = await this.client.getPrompt(params);
1203
+ this.recordResponse();
1204
+ this._addHistory(`prompts/get ${params.name}`, params, result, Date.now() - start);
1205
+ return result;
1206
+ }
1207
+ // ─── Logging ────────────────────────────────────────────────────────────────
1208
+ /**
1209
+ * Set the logging level on the target MCP server.
1210
+ */
1211
+ async setLoggingLevel(level) {
1212
+ this._assertConnected();
1213
+ const start = Date.now();
1214
+ const result = await this.client.setLoggingLevel(level);
1215
+ this.recordResponse();
1216
+ this._addHistory(`logging/setLevel ${level}`, { level }, result, Date.now() - start);
1217
+ return result;
1218
+ }
1219
+ // ─── Completion ─────────────────────────────────────────────────────────────
1220
+ /**
1221
+ * Request completion from the target MCP server (for autocomplete UX).
1222
+ */
1223
+ async complete(params) {
1224
+ this._assertConnected();
1225
+ const start = Date.now();
1226
+ const result = await this.client.complete(params);
1227
+ this.recordResponse();
1228
+ this._addHistory("completion/complete", params, result, Date.now() - start);
1229
+ return result;
1230
+ }
1231
+ // ─── Request History ────────────────────────────────────────────────────────
1232
+ /**
1233
+ * Get the request/response history.
1234
+ * @param count - Number of recent records to return (default: all)
1235
+ */
1236
+ getHistory(count) {
1237
+ if (!count || count >= this._history.length) return [...this._history];
1238
+ return this._history.slice(-count);
1239
+ }
1240
+ /**
1241
+ * Clear the history buffer.
1242
+ */
1243
+ clearHistory() {
1244
+ this._history = [];
1245
+ }
1246
+ _addHistory(method, params, result, durationMs) {
1247
+ const record = {
1248
+ id: ++this._historyIdCounter,
1249
+ method,
1250
+ params,
1251
+ result,
1252
+ durationMs,
1253
+ timestamp: Date.now()
1254
+ };
1255
+ this._history.push(record);
1256
+ if (this._history.length > MAX_HISTORY) {
1257
+ this._history = this._history.slice(-MAX_HISTORY);
1156
1258
  }
1157
- if (json[i] === '"') {
1158
- return json.slice(start, i + 1);
1259
+ }
1260
+ // ─── Notification History ───────────────────────────────────────────────────
1261
+ /**
1262
+ * Get recent server notifications.
1263
+ * @param count - Number of recent notifications to return (default: all)
1264
+ */
1265
+ getNotifications(count) {
1266
+ if (!count || count >= this._notifications.length) return [...this._notifications];
1267
+ return this._notifications.slice(-count);
1268
+ }
1269
+ /**
1270
+ * Clear the notification buffer.
1271
+ */
1272
+ clearNotifications() {
1273
+ this._notifications = [];
1274
+ }
1275
+ _pushNotification(record) {
1276
+ this._notifications.push(record);
1277
+ if (this._notifications.length > _TargetManager.MAX_NOTIFICATIONS) {
1278
+ this._notifications = this._notifications.slice(-_TargetManager.MAX_NOTIFICATIONS);
1159
1279
  }
1160
- i++;
1161
1280
  }
1162
- return json.slice(start);
1163
- }
1164
- function levenshtein(a, b) {
1165
- const m = a.length;
1166
- const n = b.length;
1167
- const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
1168
- for (let i = 0; i <= m; i++) dp[i][0] = i;
1169
- for (let j = 0; j <= n; j++) dp[0][j] = j;
1170
- for (let i = 1; i <= m; i++) {
1171
- for (let j = 1; j <= n; j++) {
1172
- dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
1281
+ // ─── Roots Management ─────────────────────────────────────────────────────
1282
+ /**
1283
+ * Get the current roots list that this client advertises.
1284
+ */
1285
+ getRoots() {
1286
+ return [...this._roots];
1287
+ }
1288
+ /**
1289
+ * Add a root and send notification to the server.
1290
+ */
1291
+ async addRoot(root) {
1292
+ if (this._roots.some((r) => r.uri === root.uri)) return;
1293
+ this._roots.push(root);
1294
+ await this._sendRootsChanged();
1295
+ }
1296
+ /**
1297
+ * Remove a root by URI and send notification to the server.
1298
+ */
1299
+ async removeRoot(uri) {
1300
+ const before = this._roots.length;
1301
+ this._roots = this._roots.filter((r) => r.uri !== uri);
1302
+ if (this._roots.length < before) {
1303
+ await this._sendRootsChanged();
1304
+ return true;
1173
1305
  }
1306
+ return false;
1174
1307
  }
1175
- return dp[m][n];
1176
- }
1177
- function suggestCommand(input3, commands, threshold = 0.4) {
1178
- let best = null;
1179
- let bestDist = Infinity;
1180
- for (const cmd of commands) {
1181
- const dist = levenshtein(input3, cmd);
1182
- if (dist < bestDist) {
1183
- bestDist = dist;
1184
- best = cmd;
1308
+ async _sendRootsChanged() {
1309
+ if (!this._connected || !this.client) return;
1310
+ try {
1311
+ await this.client.sendRootsListChanged();
1312
+ } catch {
1185
1313
  }
1186
1314
  }
1187
- if (best && bestDist <= Math.ceil(input3.length * threshold)) {
1188
- return best;
1315
+ // ─── Notification forwarding ────────────────────────────────────────────────
1316
+ /**
1317
+ * Access the underlying MCP client for advanced use cases like
1318
+ * subscribing to notifications with proper SDK schemas.
1319
+ * Prefer the typed methods above when possible.
1320
+ */
1321
+ getRawClient() {
1322
+ return this.client;
1189
1323
  }
1190
- return null;
1191
- }
1192
- function scaffoldArgs(schema) {
1193
- return JSON.stringify(scaffoldObject(schema), null, 2);
1194
- }
1195
- function scaffoldValue(prop) {
1196
- if (Array.isArray(prop.enum) && prop.enum.length > 0) {
1197
- return prop.enum[0];
1324
+ // ─── Status & lifecycle ─────────────────────────────────────────────────────
1325
+ /**
1326
+ * Returns the last N lines of stderr output from the target server.
1327
+ * Useful for debugging crashes or unexpected behavior.
1328
+ */
1329
+ getStderrLines(count) {
1330
+ if (!count || count >= this._stderrLines.length) return [...this._stderrLines];
1331
+ return this._stderrLines.slice(-count);
1198
1332
  }
1199
- const variants = prop.anyOf ?? prop.oneOf;
1200
- if (Array.isArray(variants) && variants.length > 0) {
1201
- return scaffoldValue(variants[0]);
1333
+ /**
1334
+ * Returns current connection status, PID, uptime, and diagnostics.
1335
+ */
1336
+ getStatus() {
1337
+ return {
1338
+ pid: this.childPid,
1339
+ uptime: this._connected ? (Date.now() - this.startTime) / 1e3 : 0,
1340
+ connected: this._connected,
1341
+ command: this.command,
1342
+ args: this.args,
1343
+ lastResponseTime: this._lastResponseTime,
1344
+ stderrLineCount: this._stderrLineCount,
1345
+ reconnectAttempts: this._reconnectAttempts,
1346
+ maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS
1347
+ };
1202
1348
  }
1203
- switch (prop.type) {
1204
- case "string":
1205
- return "<string>";
1206
- case "number":
1207
- case "integer":
1208
- return "<number>";
1209
- case "boolean":
1210
- return "<boolean>";
1211
- case "array": {
1212
- const items = prop.items;
1213
- return items ? [scaffoldValue(items)] : ["<item>"];
1349
+ /**
1350
+ * Cleanly shut down the client connection and forcefully kill the child process tree.
1351
+ */
1352
+ async close() {
1353
+ this._intentionalClose = true;
1354
+ this._clearStableTimer();
1355
+ const pidToKill = this.childPid;
1356
+ if (this.client) {
1357
+ try {
1358
+ await this.client.close();
1359
+ } catch {
1360
+ }
1361
+ this.client = null;
1214
1362
  }
1215
- case "object":
1216
- return scaffoldObject(prop);
1217
- default:
1218
- return `<${prop.type ?? "unknown"}>`;
1219
- }
1220
- }
1221
- function scaffoldObject(schema) {
1222
- const properties = schema.properties;
1223
- if (properties) {
1224
- const result = {};
1225
- for (const [key, prop] of Object.entries(properties)) {
1226
- result[key] = scaffoldValue(prop);
1363
+ if (this.transport) {
1364
+ try {
1365
+ await this.transport.close();
1366
+ } catch {
1367
+ }
1368
+ this.transport = null;
1227
1369
  }
1228
- return result;
1370
+ if (pidToKill) {
1371
+ await new Promise((resolve2) => {
1372
+ treeKill(pidToKill, "SIGKILL", () => resolve2());
1373
+ });
1374
+ }
1375
+ this._connected = false;
1376
+ this.childPid = null;
1229
1377
  }
1230
- const additionalProperties = schema.additionalProperties;
1231
- if (additionalProperties && typeof additionalProperties === "object") {
1232
- return { "<key>": scaffoldValue(additionalProperties) };
1378
+ // ─── Auto-reconnect logic ──────────────────────────────────────────────────
1379
+ /**
1380
+ * Decide whether to attempt auto-reconnect after a disconnect.
1381
+ *
1382
+ * Rules:
1383
+ * 1. Auto-reconnect must be enabled
1384
+ * 2. Server must have been alive for ≥5s (otherwise it's a startup bug)
1385
+ * 3. Must not exceed MAX_RECONNECT_ATTEMPTS consecutive retries
1386
+ * 4. Must not already be reconnecting
1387
+ */
1388
+ async _maybeReconnect() {
1389
+ if (!this._autoReconnect || this._reconnecting) return;
1390
+ const uptimeMs = Date.now() - this.startTime;
1391
+ if (uptimeMs < MIN_UPTIME_FOR_RESTART_MS) {
1392
+ this.emit("reconnect_failed", {
1393
+ reason: "startup_crash",
1394
+ message: `Server crashed after ${(uptimeMs / 1e3).toFixed(1)}s \u2014 too soon to be a transient failure (min ${MIN_UPTIME_FOR_RESTART_MS / 1e3}s). Not retrying.`
1395
+ });
1396
+ return;
1397
+ }
1398
+ if (this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
1399
+ this.emit("reconnect_failed", {
1400
+ reason: "max_retries",
1401
+ message: `Server has crashed ${this._reconnectAttempts} times in a row. Giving up.`
1402
+ });
1403
+ return;
1404
+ }
1405
+ this._reconnecting = true;
1406
+ this._reconnectAttempts++;
1407
+ this.emit("reconnecting", {
1408
+ attempt: this._reconnectAttempts,
1409
+ maxAttempts: MAX_RECONNECT_ATTEMPTS
1410
+ });
1411
+ this.client = null;
1412
+ this.transport = null;
1413
+ this.childPid = null;
1414
+ try {
1415
+ await this.connect();
1416
+ this.emit("reconnected", { attempt: this._reconnectAttempts });
1417
+ } catch (err) {
1418
+ this.emit("reconnect_failed", {
1419
+ reason: "connect_error",
1420
+ message: `Reconnect attempt ${this._reconnectAttempts} failed: ${err.message}`
1421
+ });
1422
+ } finally {
1423
+ this._reconnecting = false;
1424
+ }
1233
1425
  }
1234
- if (additionalProperties === true || schema.type === "object" && !properties) {
1235
- return { "<key>": "<value>" };
1426
+ /**
1427
+ * After STABLE_CONNECTION_RESET_MS of being connected, reset the retry counter.
1428
+ * This way, a server that crashes once after 10 minutes of stability
1429
+ * gets a fresh set of retries.
1430
+ */
1431
+ _startStableTimer() {
1432
+ this._clearStableTimer();
1433
+ this._stableTimer = setTimeout(() => {
1434
+ if (this._connected) {
1435
+ this._reconnectAttempts = 0;
1436
+ }
1437
+ }, STABLE_CONNECTION_RESET_MS);
1236
1438
  }
1237
- return {};
1238
- }
1239
- function formatToolDescription(tool) {
1240
- const lines = [];
1241
- lines.push(` ${tool.name}`);
1242
- if (tool.description) {
1243
- lines.push(` ${tool.description}`);
1439
+ _clearStableTimer() {
1440
+ if (this._stableTimer) {
1441
+ clearTimeout(this._stableTimer);
1442
+ this._stableTimer = null;
1443
+ }
1244
1444
  }
1245
- const schema = tool.inputSchema ?? {};
1246
- const properties = schema.properties;
1247
- const required = schema.required ?? [];
1248
- if (properties && Object.keys(properties).length > 0) {
1249
- lines.push("");
1250
- lines.push(" Arguments:");
1251
- const nameWidth = Math.max(6, ...Object.keys(properties).map((n) => n.length));
1252
- const typeWidth = Math.max(4, ...Object.values(properties).map((p) => typeLabel(p).length));
1253
- for (const [name, prop] of Object.entries(properties)) {
1254
- const type = typeLabel(prop);
1255
- const req = required.includes(name) ? "(required)" : "(optional)";
1256
- const desc = prop.description ?? "";
1257
- lines.push(
1258
- ` ${name.padEnd(nameWidth)} ${type.padEnd(typeWidth)} ${req.padEnd(10)} ${desc}`
1259
- );
1445
+ // ─── Internal helpers ──────────────────────────────────────────────────────
1446
+ _assertConnected() {
1447
+ if (!this._connected || !this.client) {
1448
+ throw new Error("Not connected to target MCP server");
1260
1449
  }
1261
- } else {
1262
- lines.push("");
1263
- lines.push(" No arguments required.");
1264
1450
  }
1265
- lines.push("");
1266
- lines.push(" Example:");
1267
- if (properties && Object.keys(properties).length > 0) {
1268
- const example = scaffoldObject(schema);
1269
- lines.push(` tools/call ${tool.name} ${JSON.stringify(example)}`);
1451
+ static _cleanupRegistered = false;
1452
+ static _instances = /* @__PURE__ */ new Set();
1453
+ _registerCleanup() {
1454
+ _TargetManager._instances.add(this);
1455
+ if (_TargetManager._cleanupRegistered) return;
1456
+ _TargetManager._cleanupRegistered = true;
1457
+ const cleanupAll = () => {
1458
+ for (const instance of _TargetManager._instances) {
1459
+ instance.close().catch(() => {
1460
+ });
1461
+ }
1462
+ };
1463
+ process.on("exit", cleanupAll);
1464
+ process.on("SIGINT", () => {
1465
+ cleanupAll();
1466
+ process.exit(130);
1467
+ });
1468
+ process.on("SIGTERM", () => {
1469
+ cleanupAll();
1470
+ process.exit(143);
1471
+ });
1472
+ }
1473
+ };
1474
+
1475
+ // src/headless.ts
1476
+ var DEFAULT_HEADLESS_TIMEOUT_MS = 3e4;
1477
+ async function runHeadless(targetCommand, operation, opts = {}) {
1478
+ const [command, ...args] = targetCommand;
1479
+ const target = new TargetManager(command, args);
1480
+ const interceptor = new ResponseInterceptor({
1481
+ outDir: opts.outDir,
1482
+ defaultTimeoutMs: opts.timeoutMs ?? DEFAULT_HEADLESS_TIMEOUT_MS
1483
+ });
1484
+ if (opts.showStderr) {
1485
+ target.on("stderr", (text) => {
1486
+ process.stderr.write(`${text}
1487
+ `);
1488
+ });
1270
1489
  } else {
1271
- lines.push(` tools/call ${tool.name}`);
1490
+ target.on("stderr", () => {
1491
+ });
1272
1492
  }
1273
- if (tool.annotations) {
1274
- const entries = Object.entries(tool.annotations).filter(([key]) => key !== "title");
1275
- if (entries.length > 0) {
1276
- lines.push("");
1277
- lines.push(" Annotations:");
1278
- for (const [key, value] of entries) {
1279
- lines.push(` ${key}: ${value}`);
1280
- }
1493
+ try {
1494
+ process.stderr.write(`Connecting to ${targetCommand.join(" ")}...
1495
+ `);
1496
+ await target.connect();
1497
+ const status = target.getStatus();
1498
+ process.stderr.write(`Connected (PID: ${status.pid})
1499
+ `);
1500
+ const { result, hasError } = await executeOperation(target, interceptor, operation, opts);
1501
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
1502
+ `);
1503
+ await target.close();
1504
+ process.exit(hasError ? 1 : 0);
1505
+ } catch (err) {
1506
+ const msg = err.message ?? String(err);
1507
+ if (msg.includes("ENOENT") || msg.includes("spawn")) {
1508
+ process.stderr.write(
1509
+ `Error: command "${command}" not found. Check that it is installed and in your PATH.
1510
+ `
1511
+ );
1512
+ } else if (msg.includes("timed out")) {
1513
+ process.stderr.write(`Error: ${msg}
1514
+ `);
1515
+ } else {
1516
+ process.stderr.write(`Error: ${msg}
1517
+ `);
1281
1518
  }
1519
+ await target.close().catch(() => {
1520
+ });
1521
+ process.exit(1);
1282
1522
  }
1283
- return lines.join("\n");
1284
- }
1285
- function typeLabel(prop) {
1286
- const type = prop.type;
1287
- if (!type) return "any";
1288
- if (type === "array") {
1289
- const items = prop.items;
1290
- return items ? `${typeLabel(items)}[]` : "array";
1291
- }
1292
- return type;
1293
1523
  }
1294
- function groupToolsByPrefix(toolNames) {
1295
- const groups = /* @__PURE__ */ new Map();
1296
- for (const name of toolNames) {
1297
- const underscoreIdx = name.indexOf("_");
1298
- const prefix = underscoreIdx > 0 ? name.slice(0, underscoreIdx) : name;
1299
- const list = groups.get(prefix) ?? [];
1300
- list.push(name);
1301
- groups.set(prefix, list);
1302
- }
1303
- const meaningfulGroups = [...groups.entries()].filter(([, members]) => members.length >= 2);
1304
- if (meaningfulGroups.length < 2) {
1305
- const all = /* @__PURE__ */ new Map();
1306
- all.set("All", [...toolNames]);
1307
- return all;
1308
- }
1309
- const result = /* @__PURE__ */ new Map();
1310
- const other = [];
1311
- for (const [prefix, members] of groups) {
1312
- if (members.length >= 2) {
1313
- const label = prefix.charAt(0).toUpperCase() + prefix.slice(1);
1314
- result.set(label, members);
1315
- } else {
1316
- other.push(...members);
1524
+ async function executeOperation(target, interceptor, operation, opts) {
1525
+ switch (operation.type) {
1526
+ case "call": {
1527
+ let parsedArgs = {};
1528
+ if (operation.args) {
1529
+ const trimmed = operation.args.trim();
1530
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
1531
+ try {
1532
+ parsedArgs = JSON.parse(trimmed);
1533
+ } catch (err) {
1534
+ process.stderr.write(`Error: Invalid JSON arguments: ${err.message}
1535
+ `);
1536
+ process.stderr.write(` Received: ${operation.args}
1537
+ `);
1538
+ process.exit(2);
1539
+ }
1540
+ } else {
1541
+ parsedArgs = parseHttpieArgs(trimmed);
1542
+ }
1543
+ }
1544
+ const result = await interceptor.callTool(target, operation.tool, parsedArgs);
1545
+ if (result.isError) {
1546
+ const content = result.content;
1547
+ if (Array.isArray(content)) {
1548
+ const errorText = content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
1549
+ if (errorText) {
1550
+ process.stderr.write(`Tool error: ${errorText}
1551
+ `);
1552
+ }
1553
+ }
1554
+ if (opts.raw) return { result, hasError: true };
1555
+ return { result: result.content ?? result, hasError: true };
1556
+ }
1557
+ if (opts.raw) return { result, hasError: false };
1558
+ return { result: result.content ?? result, hasError: false };
1559
+ }
1560
+ case "list-tools": {
1561
+ const { tools } = await target.listTools();
1562
+ return { result: tools, hasError: false };
1563
+ }
1564
+ case "list-resources": {
1565
+ const { resources } = await target.listResources();
1566
+ return { result: resources, hasError: false };
1567
+ }
1568
+ case "list-prompts": {
1569
+ const { prompts } = await target.listPrompts();
1570
+ return { result: prompts, hasError: false };
1571
+ }
1572
+ case "read": {
1573
+ const result = await interceptor.readResource(target, { uri: operation.uri });
1574
+ return { result, hasError: false };
1575
+ }
1576
+ case "describe": {
1577
+ const { tools } = await target.listTools();
1578
+ const tool = tools.find((t) => t.name === operation.tool);
1579
+ if (!tool) {
1580
+ const available = tools.map((t) => t.name).join(", ");
1581
+ process.stderr.write(
1582
+ `Error: Tool "${operation.tool}" not found.
1583
+ Available tools: ${available}
1584
+ `
1585
+ );
1586
+ process.exit(1);
1587
+ }
1588
+ return { result: tool, hasError: false };
1589
+ }
1590
+ case "get-prompt": {
1591
+ let parsedArgs;
1592
+ if (operation.args) {
1593
+ const trimmed = operation.args.trim();
1594
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
1595
+ try {
1596
+ parsedArgs = JSON.parse(trimmed);
1597
+ } catch (err) {
1598
+ process.stderr.write(`Error: Invalid JSON arguments: ${err.message}
1599
+ `);
1600
+ process.stderr.write(` Received: ${operation.args}
1601
+ `);
1602
+ process.exit(2);
1603
+ }
1604
+ } else {
1605
+ parsedArgs = parseHttpieArgs(trimmed);
1606
+ }
1607
+ }
1608
+ const result = await interceptor.getPrompt(target, {
1609
+ name: operation.name,
1610
+ arguments: parsedArgs
1611
+ });
1612
+ return { result, hasError: false };
1317
1613
  }
1318
1614
  }
1319
- if (other.length > 0) {
1320
- result.set("Other", other);
1321
- }
1322
- return result;
1323
- }
1324
- var LOG_LEVELS = [
1325
- "debug",
1326
- "info",
1327
- "notice",
1328
- "warning",
1329
- "error",
1330
- "critical",
1331
- "alert",
1332
- "emergency"
1333
- ];
1334
- var ALIASES = {
1335
- tl: "tools/list",
1336
- td: "tools/describe",
1337
- tc: "tools/call",
1338
- ts: "tools/scaffold",
1339
- rl: "resources/list",
1340
- rr: "resources/read",
1341
- rt: "resources/templates",
1342
- rs: "resources/subscribe",
1343
- ru: "resources/unsubscribe",
1344
- pl: "prompts/list",
1345
- pg: "prompts/get"
1346
- };
1347
- function resolveAlias(input3) {
1348
- const spaceIdx = input3.indexOf(" ");
1349
- const token = spaceIdx === -1 ? input3 : input3.slice(0, spaceIdx);
1350
- const rest = spaceIdx === -1 ? "" : input3.slice(spaceIdx);
1351
- const expanded = ALIASES[token.toLowerCase()];
1352
- if (!expanded) return null;
1353
- return expanded + rest;
1354
1615
  }
1355
1616
 
1356
1617
  // src/repl.ts
1618
+ import { readFile as readFile2 } from "fs/promises";
1619
+ import { createInterface } from "readline";
1620
+ import { checkbox, confirm, input as input2, search } from "@inquirer/prompts";
1621
+ import pc2 from "picocolors";
1357
1622
  var KNOWN_COMMANDS = [
1358
1623
  "explore",
1359
1624
  "interactive",
@@ -1563,7 +1828,10 @@ function stripAnsi(str) {
1563
1828
  async function startRepl(targetCommand, opts) {
1564
1829
  const [command, ...args] = targetCommand;
1565
1830
  const target = new TargetManager(command, args);
1566
- const interceptor = new ResponseInterceptor({ outDir: opts.outDir });
1831
+ const interceptor = new ResponseInterceptor({
1832
+ outDir: opts.outDir,
1833
+ mediaThresholdKb: opts.mediaThresholdKb
1834
+ });
1567
1835
  isScriptMode = !!opts.script;
1568
1836
  target.on("stderr", (text) => {
1569
1837
  for (const line of text.split("\n")) {
@@ -1708,7 +1976,7 @@ async function startRepl(targetCommand, opts) {
1708
1976
  }
1709
1977
  });
1710
1978
  }
1711
- let toolCount = 0;
1979
+ let toolCount;
1712
1980
  let resourceCount = 0;
1713
1981
  let promptCount = 0;
1714
1982
  try {
@@ -1751,26 +2019,60 @@ async function startRepl(targetCommand, opts) {
1751
2019
  await refreshCaches(target);
1752
2020
  if (isScriptMode) {
1753
2021
  const lines = await readScriptLines(opts.script);
2022
+ const scriptContext = {};
2023
+ let expectError = false;
1754
2024
  for (const line of lines) {
1755
- const trimmed = line.trim();
1756
- if (!trimmed || trimmed.startsWith("#")) continue;
2025
+ let trimmed = line.trim();
2026
+ if (!trimmed || trimmed.startsWith("#")) {
2027
+ if (trimmed === "# @expect-error") {
2028
+ expectError = true;
2029
+ }
2030
+ continue;
2031
+ }
2032
+ if (trimmed.endsWith("# @expect-error")) {
2033
+ expectError = true;
2034
+ trimmed = trimmed.replace(/\s*#\s*@expect-error$/, "");
2035
+ }
2036
+ const interpolated = interpolateString(trimmed, scriptContext);
1757
2037
  try {
1758
- await handleCommand(trimmed, target, interceptor);
2038
+ const res = await handleCommand(interpolated, target, interceptor);
2039
+ if (res !== void 0) {
2040
+ scriptContext.LAST = res;
2041
+ }
2042
+ const isErrorRes = res && typeof res === "object" && res.isError === true;
2043
+ if (expectError && !isErrorRes) {
2044
+ console.error(pc2.red(`\u2717 Expected an error but the command succeeded.`));
2045
+ await target.close();
2046
+ process.exit(1);
2047
+ }
2048
+ if (!expectError && isErrorRes) {
2049
+ console.error(pc2.red(`\u2717 Command failed unexpectedly.`));
2050
+ await target.close();
2051
+ process.exit(1);
2052
+ }
2053
+ if (expectError && isErrorRes) {
2054
+ console.log(pc2.yellow(` \u2713 Expected error caught: tool returned isError: true`));
2055
+ }
1759
2056
  } catch (err) {
1760
- if (err?.message?.includes("-32601") || err?.code === -32601) {
1761
- let msg = "Server does not support this feature (Method not found)";
1762
- if (trimmed.startsWith("prompts/")) msg = "This server does not have any prompts.";
1763
- else if (trimmed.startsWith("resources/"))
1764
- msg = "This server does not have any resources.";
1765
- else if (trimmed.startsWith("tools/")) msg = "This server does not have any tools.";
1766
- console.log(pc2.yellow(` ${msg}`));
2057
+ if (expectError) {
2058
+ console.log(pc2.yellow(` \u2713 Expected error caught: ${err.message}`));
1767
2059
  } else {
1768
- console.error(pc2.red(`\u2717 Error: ${err.message}`));
2060
+ if (err?.message?.includes("-32601") || err?.code === -32601) {
2061
+ let msg = "Server does not support this feature (Method not found)";
2062
+ if (trimmed.startsWith("prompts/")) msg = "This server does not have any prompts.";
2063
+ else if (trimmed.startsWith("resources/"))
2064
+ msg = "This server does not have any resources.";
2065
+ else if (trimmed.startsWith("tools/")) msg = "This server does not have any tools.";
2066
+ console.log(pc2.yellow(` ${msg}`));
2067
+ } else {
2068
+ console.error(pc2.red(`\u2717 Error: ${err.message}`));
2069
+ }
2070
+ console.log(pc2.dim("\nShutting down..."));
2071
+ await target.close();
2072
+ process.exit(1);
1769
2073
  }
1770
- console.log(pc2.dim("\nShutting down..."));
1771
- await target.close();
1772
- process.exit(1);
1773
2074
  }
2075
+ expectError = false;
1774
2076
  }
1775
2077
  console.log(pc2.dim("\nShutting down..."));
1776
2078
  await target.close();
@@ -1885,8 +2187,7 @@ async function handleCommand(input3, target, interceptor) {
1885
2187
  await cmdToolsDescribe(target, rest);
1886
2188
  return;
1887
2189
  case "tools/call":
1888
- await cmdToolsCall(target, interceptor, rest);
1889
- return;
2190
+ return await cmdToolsCall(target, interceptor, rest);
1890
2191
  case "tools/scaffold":
1891
2192
  await cmdToolsScaffold(target, rest);
1892
2193
  return;
@@ -1897,8 +2198,7 @@ async function handleCommand(input3, target, interceptor) {
1897
2198
  await cmdResourcesList(target);
1898
2199
  return;
1899
2200
  case "resources/read":
1900
- await cmdResourcesRead(target, rest, interceptor);
1901
- return;
2201
+ return await cmdResourcesRead(target, rest, interceptor);
1902
2202
  case "resources/templates":
1903
2203
  await cmdResourcesTemplates(target);
1904
2204
  return;
@@ -1906,8 +2206,7 @@ async function handleCommand(input3, target, interceptor) {
1906
2206
  await cmdPromptsList(target);
1907
2207
  return;
1908
2208
  case "prompts/get":
1909
- await cmdPromptsGet(target, rest, interceptor);
1910
- return;
2209
+ return await cmdPromptsGet(target, rest, interceptor);
1911
2210
  case "timing":
1912
2211
  cmdTiming();
1913
2212
  return;
@@ -1945,7 +2244,7 @@ async function handleCommand(input3, target, interceptor) {
1945
2244
  case "last":
1946
2245
  if (lastCommand) {
1947
2246
  console.log(pc2.dim(` Re-running: ${lastCommand}`));
1948
- await handleCommand(lastCommand, target, interceptor);
2247
+ return await handleCommand(lastCommand, target, interceptor);
1949
2248
  } else {
1950
2249
  console.log(pc2.yellow("No previous command to re-run."));
1951
2250
  }
@@ -1961,6 +2260,9 @@ async function handleCommand(input3, target, interceptor) {
1961
2260
  return;
1962
2261
  }
1963
2262
  default: {
2263
+ if (cachedToolNames.includes(cmd)) {
2264
+ return await cmdToolsCall(target, interceptor, input3);
2265
+ }
1964
2266
  const suggestion = suggestCommand(cmd, getActiveCommands());
1965
2267
  if (suggestion) {
1966
2268
  console.log(pc2.yellow(`Unknown command: ${cmd}.`));
@@ -1972,7 +2274,7 @@ async function handleCommand(input3, target, interceptor) {
1972
2274
  });
1973
2275
  if (runIt) {
1974
2276
  const rebuiltCommand = rest ? `${suggestion} ${rest}` : suggestion;
1975
- await handleCommand(rebuiltCommand, target, interceptor);
2277
+ return await handleCommand(rebuiltCommand, target, interceptor);
1976
2278
  }
1977
2279
  });
1978
2280
  } catch (err) {
@@ -2080,32 +2382,54 @@ async function cmdToolsCall(target, interceptor, rest) {
2080
2382
  }
2081
2383
  let args = {};
2082
2384
  if (jsonArgs) {
2083
- try {
2084
- args = JSON.parse(jsonArgs);
2085
- } catch (err) {
2086
- console.error(pc2.red(`Invalid JSON: ${err.message}`));
2087
- console.log(pc2.dim(` Received: ${jsonArgs}`));
2088
- return;
2385
+ const trimmed = jsonArgs.trim();
2386
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
2387
+ try {
2388
+ args = JSON.parse(trimmed);
2389
+ } catch (err) {
2390
+ console.error(pc2.red(`Invalid JSON: ${err.message}`));
2391
+ console.log(pc2.dim(` Received: ${jsonArgs}`));
2392
+ return;
2393
+ }
2394
+ } else {
2395
+ try {
2396
+ args = parseHttpieArgs(trimmed);
2397
+ } catch (err) {
2398
+ console.error(pc2.red(`Invalid shorthand arguments: ${err.message}`));
2399
+ return;
2400
+ }
2089
2401
  }
2090
2402
  const { tools } = await target.listTools();
2091
2403
  const tool = tools.find((t) => t.name === toolName);
2092
- if (tool) {
2093
- const schema = tool.inputSchema;
2094
- const required = schema.required ?? [];
2095
- const missing = required.filter((r) => !(r in args));
2096
- if (missing.length > 0) {
2097
- console.log(pc2.yellow(`
2098
- Missing required arguments: ${missing.join(", ")}`));
2099
- console.log();
2100
- const scaffolded = scaffoldArgs(schema);
2101
- console.log(pc2.dim(" Try:"));
2102
- console.log(` tools/call ${toolName} ${scaffolded}`);
2103
- console.log();
2104
- console.log(pc2.dim(" Or run without args for interactive mode:"));
2105
- console.log(` tools/call ${toolName}`);
2106
- console.log();
2107
- return;
2404
+ if (!tool) {
2405
+ console.log(pc2.red(`
2406
+ \u2717 Tool "${toolName}" not found.`));
2407
+ const toolNames = tools.map((t) => t.name);
2408
+ const suggestion = suggestCommand(toolName, toolNames);
2409
+ if (suggestion) {
2410
+ console.log(pc2.yellow(` \u{1F4A1} Did you mean "${suggestion}"?`));
2411
+ } else {
2412
+ const preview = toolNames.slice(0, 6);
2413
+ const more = toolNames.length > 6 ? `, ... (${toolNames.length} total)` : "";
2414
+ console.log(pc2.dim(` Available tools: ${preview.join(", ")}${more}`));
2108
2415
  }
2416
+ return { isError: true, content: [{ type: "text", text: `Tool not found: ${toolName}` }] };
2417
+ }
2418
+ const schema = tool.inputSchema;
2419
+ const required = schema.required ?? [];
2420
+ const missing = required.filter((r) => !(r in args));
2421
+ if (missing.length > 0) {
2422
+ console.log(pc2.yellow(`
2423
+ Missing required arguments: ${missing.join(", ")}`));
2424
+ console.log();
2425
+ const scaffolded = scaffoldArgs(schema);
2426
+ console.log(pc2.dim(" Try:"));
2427
+ console.log(` tools/call ${toolName} ${scaffolded}`);
2428
+ console.log();
2429
+ console.log(pc2.dim(" Or run without args for interactive mode:"));
2430
+ console.log(` tools/call ${toolName}`);
2431
+ console.log();
2432
+ return;
2109
2433
  }
2110
2434
  } else {
2111
2435
  const collectedArgs = await interactiveArgPrompt(target, interceptor, toolName, clearPrevious);
@@ -2159,14 +2483,18 @@ async function cmdToolsCall(target, interceptor, rest) {
2159
2483
  console.log(formatJson(result, 2, true));
2160
2484
  }
2161
2485
  if (isError) {
2162
- console.log(
2163
- pc2.yellow(
2164
- ` \u{1F4A1} Tip: Check the tool arguments via 'tools/describe ${toolName}'
2486
+ const errText = Array.isArray(content) ? content.map((c) => c.text || "").join(" ").toLowerCase() : typeof content === "object" ? (content.text || "").toLowerCase() : "";
2487
+ if (errText.includes("argument") || errText.includes("validation") || errText.includes("schema") || errText.includes("missing") || errText.includes("invalid")) {
2488
+ console.log(
2489
+ pc2.yellow(
2490
+ ` \u{1F4A1} Tip: Check the tool arguments via 'tools/describe ${toolName}'
2165
2491
  or view the raw server stderr above.`
2166
- )
2167
- );
2492
+ )
2493
+ );
2494
+ }
2168
2495
  }
2169
2496
  console.log();
2497
+ return result;
2170
2498
  }
2171
2499
  async function interactiveArgPrompt(target, interceptor, toolName, clearPrevious = false) {
2172
2500
  const { tools } = await target.listTools();
@@ -2327,7 +2655,7 @@ function coerceValue(input3, type) {
2327
2655
  }
2328
2656
  }
2329
2657
  function question(rl, prompt) {
2330
- return new Promise((resolve, reject) => {
2658
+ return new Promise((resolve2, reject) => {
2331
2659
  let aborted = false;
2332
2660
  const onKeypress = (_str, key) => {
2333
2661
  if (key && key.name === "escape") {
@@ -2351,7 +2679,7 @@ function question(rl, prompt) {
2351
2679
  if (aborted) {
2352
2680
  reject(new AbortFlowError());
2353
2681
  } else {
2354
- resolve(answer);
2682
+ resolve2(answer);
2355
2683
  }
2356
2684
  });
2357
2685
  });
@@ -2458,6 +2786,7 @@ async function cmdResourcesRead(target, rest, interceptor) {
2458
2786
  }
2459
2787
  }
2460
2788
  console.log();
2789
+ return result;
2461
2790
  }
2462
2791
  async function cmdResourcesTemplates(target) {
2463
2792
  const { resourceTemplates } = await target.listResourceTemplates();
@@ -2500,9 +2829,9 @@ async function cmdPromptsGet(target, rest, interceptor) {
2500
2829
  if (!promptName) {
2501
2830
  if (!isScriptMode && cachedPromptNames.length > 0 && process.stdin.isTTY && interceptor) {
2502
2831
  const picked = await withSuspendedReadline(target, interceptor, async () => {
2503
- const { prompts } = await target.listPrompts();
2832
+ const { prompts: prompts2 } = await target.listPrompts();
2504
2833
  return pickInteractive(
2505
- prompts.map((p) => ({ name: p.name, description: p.description })),
2834
+ prompts2.map((p) => ({ name: p.name, description: p.description })),
2506
2835
  "Pick a prompt to get:"
2507
2836
  );
2508
2837
  });
@@ -2516,15 +2845,39 @@ async function cmdPromptsGet(target, rest, interceptor) {
2516
2845
  }
2517
2846
  return;
2518
2847
  }
2519
- let promptArgs = {};
2520
- if (jsonArgs) {
2521
- try {
2522
- promptArgs = JSON.parse(jsonArgs);
2523
- } catch (err) {
2524
- console.error(pc2.red(`Invalid JSON: ${err.message}`));
2525
- console.log(pc2.dim(` Received: ${jsonArgs}`));
2526
- return;
2848
+ let promptArgs = {};
2849
+ if (jsonArgs) {
2850
+ const trimmed = jsonArgs.trim();
2851
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
2852
+ try {
2853
+ promptArgs = JSON.parse(trimmed);
2854
+ } catch (err) {
2855
+ console.error(pc2.red(`Invalid JSON: ${err.message}`));
2856
+ console.log(pc2.dim(` Received: ${jsonArgs}`));
2857
+ return;
2858
+ }
2859
+ } else {
2860
+ try {
2861
+ promptArgs = parseHttpieArgs(trimmed);
2862
+ } catch (err) {
2863
+ console.error(pc2.red(`Invalid shorthand arguments: ${err.message}`));
2864
+ return;
2865
+ }
2866
+ }
2867
+ }
2868
+ const { prompts } = await target.listPrompts();
2869
+ const prompt = prompts.find((p) => p.name === promptName);
2870
+ if (!prompt) {
2871
+ console.log(pc2.red(`
2872
+ \u2717 Prompt "${promptName}" not found.`));
2873
+ const promptNames = prompts.map((p) => p.name);
2874
+ const suggestion = suggestCommand(promptName, promptNames);
2875
+ if (suggestion) {
2876
+ console.log(pc2.yellow(` \u{1F4A1} Did you mean "${suggestion}"?`));
2877
+ } else {
2878
+ console.log(pc2.dim(` Available prompts: ${promptNames.join(", ")}`));
2527
2879
  }
2880
+ return { isError: true, content: [{ type: "text", text: `Prompt not found: ${promptName}` }] };
2528
2881
  }
2529
2882
  const startTime = Date.now();
2530
2883
  const result = await target.getPrompt({ name: promptName, arguments: promptArgs });
@@ -2551,6 +2904,7 @@ async function cmdPromptsGet(target, rest, interceptor) {
2551
2904
  }
2552
2905
  }
2553
2906
  console.log();
2907
+ return result;
2554
2908
  }
2555
2909
  async function cmdPing(target) {
2556
2910
  try {
@@ -2724,7 +3078,7 @@ async function cmdRootsRemove(target, rest) {
2724
3078
  async function cmdReconnect(target) {
2725
3079
  console.log(pc2.cyan("\u27F3 Disconnecting..."));
2726
3080
  await target.close();
2727
- await new Promise((resolve) => setTimeout(resolve, 200));
3081
+ await new Promise((resolve2) => setTimeout(resolve2, 200));
2728
3082
  console.log(pc2.cyan("\u27F3 Reconnecting..."));
2729
3083
  const { command, args } = target.getStatus();
2730
3084
  console.log(pc2.dim(` Command: ${command} ${args.join(" ")}`));
@@ -3062,6 +3416,13 @@ function computeResourceDiff(prev, curr) {
3062
3416
  const removed = [...prevUris].filter((u) => !currUris.has(u));
3063
3417
  return { added, removed, modified: [] };
3064
3418
  }
3419
+ function computeResourceTemplateDiff(prev, curr) {
3420
+ const prevUris = new Set(prev.map((t) => t.uriTemplate));
3421
+ const currUris = new Set(curr.map((t) => t.uriTemplate));
3422
+ const added = [...currUris].filter((u) => !prevUris.has(u));
3423
+ const removed = [...prevUris].filter((u) => !currUris.has(u));
3424
+ return { added, removed, modified: [] };
3425
+ }
3065
3426
  function formatDiffLine(label, diff) {
3066
3427
  const parts = [];
3067
3428
  if (diff.added.length > 0) parts.push(`+${diff.added.length} added`);
@@ -3075,19 +3436,70 @@ function formatDiffLine(label, diff) {
3075
3436
  async function startServer(opts) {
3076
3437
  let target = null;
3077
3438
  let previousSnapshot = null;
3439
+ let cachedSpawnConfig = null;
3078
3440
  const interceptor = new ResponseInterceptor({
3079
3441
  outDir: opts.outDir,
3080
3442
  defaultTimeoutMs: opts.timeoutMs,
3081
- maxTextLength: opts.maxTextLength
3443
+ maxTextLength: opts.maxTextLength,
3444
+ mediaThresholdKb: opts.mediaThresholdKb
3082
3445
  });
3083
3446
  const mcpServer = new McpServer(
3084
- { name: "run-mcp", version: "1.6.0" },
3447
+ { name: "run-mcp", version: "1.6.1" },
3085
3448
  {
3086
3449
  capabilities: {
3087
- tools: {}
3450
+ tools: {},
3451
+ logging: {}
3088
3452
  }
3089
3453
  }
3090
3454
  );
3455
+ function setupTargetListeners(t) {
3456
+ t.on("stderr", (text) => {
3457
+ mcpServer.sendLoggingMessage({
3458
+ level: "info",
3459
+ logger: "target-stderr",
3460
+ data: text
3461
+ }).catch(() => {
3462
+ });
3463
+ });
3464
+ t.on("disconnected", () => {
3465
+ const pid = t.getStatus().pid;
3466
+ mcpServer.sendLoggingMessage({
3467
+ level: "error",
3468
+ logger: "run-mcp",
3469
+ data: `Target server disconnected unexpectedly! (PID: ${pid})`
3470
+ }).catch(() => {
3471
+ });
3472
+ });
3473
+ t.on("notification", (record) => {
3474
+ mcpServer.server.notification({
3475
+ method: record.method,
3476
+ params: record.params
3477
+ }).catch(() => {
3478
+ });
3479
+ });
3480
+ t.on("sampling_request", async ({ request, respond, reject }) => {
3481
+ try {
3482
+ const result = await mcpServer.server.request(
3483
+ { method: "sampling/createMessage", params: request },
3484
+ z.any()
3485
+ );
3486
+ respond(result);
3487
+ } catch (err) {
3488
+ reject(err);
3489
+ }
3490
+ });
3491
+ t.on("elicitation_request", async ({ request, respond, reject }) => {
3492
+ try {
3493
+ const result = await mcpServer.server.request(
3494
+ { method: "elicitation/create", params: request },
3495
+ z.any()
3496
+ );
3497
+ respond(result);
3498
+ } catch (err) {
3499
+ reject(err);
3500
+ }
3501
+ });
3502
+ }
3091
3503
  async function takeSnapshot() {
3092
3504
  if (!target?.connected) return {};
3093
3505
  const snap = {};
@@ -3114,6 +3526,14 @@ async function startServer(opts) {
3114
3526
  }));
3115
3527
  } catch {
3116
3528
  }
3529
+ try {
3530
+ const { resourceTemplates } = await target.listResourceTemplates();
3531
+ snap.resourceTemplates = resourceTemplates.map((t) => ({
3532
+ uriTemplate: t.uriTemplate,
3533
+ name: t.name ?? ""
3534
+ }));
3535
+ } catch {
3536
+ }
3117
3537
  }
3118
3538
  if (caps.prompts) {
3119
3539
  try {
@@ -3141,6 +3561,17 @@ async function startServer(opts) {
3141
3561
  )
3142
3562
  );
3143
3563
  }
3564
+ if (current.resourceTemplates && previousSnapshot.resourceTemplates) {
3565
+ lines.push(
3566
+ formatDiffLine(
3567
+ "Resource Templates",
3568
+ computeResourceTemplateDiff(
3569
+ previousSnapshot.resourceTemplates,
3570
+ current.resourceTemplates
3571
+ )
3572
+ )
3573
+ );
3574
+ }
3144
3575
  if (current.prompts && previousSnapshot.prompts) {
3145
3576
  lines.push(formatDiffLine("Prompts", computeDiff(previousSnapshot.prompts, current.prompts)));
3146
3577
  }
@@ -3154,29 +3585,59 @@ async function startServer(opts) {
3154
3585
  }
3155
3586
  async function ensureConnected(command, args, env) {
3156
3587
  if (target?.connected) return null;
3157
- if (!command) {
3588
+ let cmdToUse = command;
3589
+ let argsToUse = args;
3590
+ let envToUse = env;
3591
+ if (!cmdToUse && cachedSpawnConfig) {
3592
+ cmdToUse = cachedSpawnConfig.command;
3593
+ argsToUse = cachedSpawnConfig.args;
3594
+ envToUse = cachedSpawnConfig.env;
3595
+ }
3596
+ if (!cmdToUse) {
3158
3597
  return "Not connected to a target server. Provide command/args to auto-connect, or call connect_to_mcp first.";
3159
3598
  }
3160
3599
  if (target) {
3161
3600
  await target.close();
3162
3601
  target = null;
3163
3602
  }
3164
- if (env) {
3165
- for (const [key, value] of Object.entries(env)) {
3603
+ if (envToUse) {
3604
+ for (const [key, value] of Object.entries(envToUse)) {
3166
3605
  process.env[key] = value;
3167
3606
  }
3168
3607
  }
3169
- target = new TargetManager(command, args ?? []);
3170
- await target.connect();
3608
+ target = new TargetManager(cmdToUse, argsToUse ?? []);
3609
+ setupTargetListeners(target);
3610
+ try {
3611
+ await target.connect();
3612
+ } catch (err) {
3613
+ await target.close().catch(() => {
3614
+ });
3615
+ target = null;
3616
+ throw err;
3617
+ }
3618
+ cachedSpawnConfig = { command: cmdToUse, args: argsToUse ?? [], env: envToUse };
3171
3619
  return null;
3172
3620
  }
3173
- async function buildIncludeData(include) {
3621
+ async function buildIncludeData(include, summary = false) {
3174
3622
  if (!target?.connected || include.length === 0) return [];
3175
3623
  const lines = [];
3176
3624
  if (include.includes("tools")) {
3177
3625
  try {
3178
3626
  const { tools } = await target.listTools();
3179
- lines.push("", "--- Tools ---", JSON.stringify(tools, null, 2));
3627
+ let displayTools = summary ? tools.map((t) => ({ name: t.name, description: t.description })) : tools;
3628
+ let jsonStr = JSON.stringify(displayTools, null, 2);
3629
+ if (!summary && jsonStr.length > 2e4) {
3630
+ displayTools = tools.map((t) => ({ name: t.name, description: t.description }));
3631
+ jsonStr = JSON.stringify(displayTools, null, 2);
3632
+ lines.push(
3633
+ "",
3634
+ "--- Tools ---",
3635
+ jsonStr,
3636
+ "[Note: Full schemas omitted to protect context window. Use list_mcp_primitives with name='tool_name' to inspect schemas individually.]"
3637
+ );
3638
+ } else {
3639
+ lines.push("", "--- Tools ---", jsonStr);
3640
+ }
3180
3641
  } catch (err) {
3181
3642
  lines.push("", "--- Tools ---", `Error: ${err.message}`);
3182
3643
  }
@@ -3184,15 +3645,50 @@ async function startServer(opts) {
3184
3645
  if (include.includes("resources")) {
3185
3646
  try {
3186
3647
  const { resources } = await target.listResources();
3187
- lines.push("", "--- Resources ---", JSON.stringify(resources, null, 2));
3648
+ let displayResources = summary ? resources.map((r) => ({
3649
+ name: r.name,
3650
+ uri: r.uri,
3651
+ description: r.description
3652
+ })) : resources;
3653
+ let jsonStr = JSON.stringify(displayResources, null, 2);
3654
+ if (!summary && jsonStr.length > 2e4) {
3655
+ displayResources = resources.map((r) => ({
3656
+ name: r.name,
3657
+ uri: r.uri,
3658
+ description: r.description
3659
+ }));
3660
+ jsonStr = JSON.stringify(displayResources, null, 2);
3661
+ lines.push(
3662
+ "",
3663
+ "--- Resources ---",
3664
+ jsonStr,
3665
+ "[Note: Full schemas omitted to protect context window.]"
3666
+ );
3667
+ } else {
3668
+ lines.push("", "--- Resources ---", jsonStr);
3669
+ }
3188
3670
  } catch (err) {
3189
3671
  lines.push("", "--- Resources ---", `Error: ${err.message}`);
3190
3672
  }
3191
3673
  }
3674
+ if (include.includes("resource_templates")) {
3675
+ try {
3676
+ const { resourceTemplates } = await target.listResourceTemplates();
3677
+ const displayTemplates = summary ? resourceTemplates.map((t) => ({
3678
+ name: t.name,
3679
+ uriTemplate: t.uriTemplate,
3680
+ description: t.description
3681
+ })) : resourceTemplates;
3682
+ lines.push("", "--- Resource Templates ---", JSON.stringify(displayTemplates, null, 2));
3683
+ } catch (err) {
3684
+ lines.push("", "--- Resource Templates ---", `Error: ${err.message}`);
3685
+ }
3686
+ }
3192
3687
  if (include.includes("prompts")) {
3193
3688
  try {
3194
3689
  const { prompts } = await target.listPrompts();
3195
- lines.push("", "--- Prompts ---", JSON.stringify(prompts, null, 2));
3690
+ const displayPrompts = summary ? prompts.map((p) => ({ name: p.name, description: p.description })) : prompts;
3691
+ lines.push("", "--- Prompts ---", JSON.stringify(displayPrompts, null, 2));
3196
3692
  } catch (err) {
3197
3693
  lines.push("", "--- Prompts ---", `Error: ${err.message}`);
3198
3694
  }
@@ -3203,17 +3699,20 @@ async function startServer(opts) {
3203
3699
  "connect_to_mcp",
3204
3700
  {
3205
3701
  title: "Connect to MCP Server",
3206
- 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.",
3702
+ 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/resource_templates in the response, saving round trips.",
3207
3703
  inputSchema: {
3208
3704
  command: z.string().describe("Command to run (e.g. 'node', 'python', 'npx')"),
3209
3705
  args: z.array(z.string()).optional().describe("Arguments to pass (e.g. ['src/index.js'] or ['-y', 'some-server'])"),
3210
3706
  env: z.record(z.string()).optional().describe("Extra environment variables for the child process"),
3211
- include: z.array(z.enum(["tools", "resources", "prompts"])).optional().describe(
3707
+ include: z.array(z.enum(["tools", "resources", "resource_templates", "prompts"])).optional().describe(
3212
3708
  "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."
3709
+ ),
3710
+ summary: z.boolean().optional().describe(
3711
+ "If true, returns only the name and description of each primitive (omitting full schemas) when included to save tokens."
3213
3712
  )
3214
3713
  }
3215
3714
  },
3216
- async ({ command, args, env, include }) => {
3715
+ async ({ command, args, env, include, summary }) => {
3217
3716
  if (target?.connected) {
3218
3717
  return {
3219
3718
  content: [
@@ -3236,7 +3735,16 @@ async function startServer(opts) {
3236
3735
  }
3237
3736
  }
3238
3737
  target = new TargetManager(command, args ?? []);
3239
- await target.connect();
3738
+ setupTargetListeners(target);
3739
+ try {
3740
+ await target.connect();
3741
+ } catch (err) {
3742
+ await target.close().catch(() => {
3743
+ });
3744
+ target = null;
3745
+ throw err;
3746
+ }
3747
+ cachedSpawnConfig = { command, args: args ?? [], env };
3240
3748
  const status = target.getStatus();
3241
3749
  const caps = target.getServerCapabilities() ?? {};
3242
3750
  const capSummary = [];
@@ -3265,7 +3773,7 @@ async function startServer(opts) {
3265
3773
  }
3266
3774
  previousSnapshot = currentSnapshot;
3267
3775
  if (include && include.length > 0) {
3268
- lines.push(...await buildIncludeData(include));
3776
+ lines.push(...await buildIncludeData(include, summary));
3269
3777
  }
3270
3778
  const instructions = target.getInstructions();
3271
3779
  if (instructions) {
@@ -3349,17 +3857,21 @@ Check that the command is correct and the server starts without errors. You can
3349
3857
  "list_mcp_primitives",
3350
3858
  {
3351
3859
  title: "List MCP Primitives",
3352
- 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).",
3860
+ description: "List tools, resources, resource templates, 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).",
3353
3861
  inputSchema: {
3354
- type: z.array(z.enum(["tools", "resources", "prompts"])).optional().describe(
3862
+ type: z.array(z.enum(["tools", "resources", "resource_templates", "prompts"])).optional().describe(
3355
3863
  "Which primitives to list. Defaults to all that the server supports. Example: ['tools'] to list only tools."
3356
3864
  ),
3357
3865
  name: z.string().optional().describe(
3358
- "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."
3359
- )
3866
+ "Filter to a specific item by name. For tools: matches tool name. For resources: matches URI. For resource templates: matches URI template. For prompts: matches prompt name. Returns the full schema/details for just that item."
3867
+ ),
3868
+ summary: z.boolean().optional().describe(
3869
+ "If true, returns only the name and description of each primitive (omitting full schemas) to save tokens."
3870
+ ),
3871
+ cursor: z.string().optional().describe("Cursor for pagination (returned from a previous list call)")
3360
3872
  }
3361
3873
  },
3362
- async ({ type, name }) => {
3874
+ async ({ type, name, summary, cursor }) => {
3363
3875
  if (!target?.connected) {
3364
3876
  return {
3365
3877
  content: [
@@ -3372,11 +3884,11 @@ Check that the command is correct and the server starts without errors. You can
3372
3884
  };
3373
3885
  }
3374
3886
  const caps = target.getServerCapabilities() ?? {};
3375
- const requested = type ?? ["tools", "resources", "prompts"];
3887
+ const requested = type ?? ["tools", "resources", "resource_templates", "prompts"];
3376
3888
  const sections = [];
3377
3889
  if (requested.includes("tools") && caps.tools) {
3378
3890
  try {
3379
- const result = await target.listTools();
3891
+ const result = await target.listTools({ cursor });
3380
3892
  let tools = result.tools;
3381
3893
  if (name) {
3382
3894
  tools = tools.filter((t) => t.name === name);
@@ -3388,7 +3900,11 @@ Available: ${available}`);
3388
3900
  sections.push("--- Tools ---", JSON.stringify(tools[0], null, 2));
3389
3901
  }
3390
3902
  } else {
3391
- sections.push("--- Tools ---", JSON.stringify(tools, null, 2));
3903
+ const displayTools = summary ? tools.map((t) => ({ name: t.name, description: t.description })) : tools;
3904
+ sections.push("--- Tools ---", JSON.stringify(displayTools, null, 2));
3905
+ }
3906
+ if (result.nextCursor) {
3907
+ sections.push(`--- Tools Next Cursor: ${result.nextCursor} ---`);
3392
3908
  }
3393
3909
  } catch (err) {
3394
3910
  sections.push("--- Tools ---", `Error: ${err.message}`);
@@ -3396,7 +3912,7 @@ Available: ${available}`);
3396
3912
  }
3397
3913
  if (requested.includes("resources") && caps.resources) {
3398
3914
  try {
3399
- const result = await target.listResources();
3915
+ const result = await target.listResources({ cursor });
3400
3916
  let resources = result.resources;
3401
3917
  if (name) {
3402
3918
  resources = resources.filter((r) => r.uri === name || r.name === name);
@@ -3411,15 +3927,54 @@ Available: ${available}`
3411
3927
  sections.push("--- Resources ---", JSON.stringify(resources[0], null, 2));
3412
3928
  }
3413
3929
  } else {
3414
- sections.push("--- Resources ---", JSON.stringify(resources, null, 2));
3930
+ const displayResources = summary ? resources.map((r) => ({
3931
+ name: r.name,
3932
+ uri: r.uri,
3933
+ description: r.description
3934
+ })) : resources;
3935
+ sections.push("--- Resources ---", JSON.stringify(displayResources, null, 2));
3936
+ }
3937
+ if (result.nextCursor) {
3938
+ sections.push(`--- Resources Next Cursor: ${result.nextCursor} ---`);
3415
3939
  }
3416
3940
  } catch (err) {
3417
3941
  sections.push("--- Resources ---", `Error: ${err.message}`);
3418
3942
  }
3419
3943
  }
3944
+ if (requested.includes("resource_templates") && caps.resources) {
3945
+ try {
3946
+ const result = await target.listResourceTemplates({ cursor });
3947
+ let templates = result.resourceTemplates;
3948
+ if (name) {
3949
+ templates = templates.filter((t) => t.uriTemplate === name || t.name === name);
3950
+ if (templates.length === 0) {
3951
+ const available = result.resourceTemplates.map((t) => t.uriTemplate).join(", ");
3952
+ sections.push(
3953
+ "--- Resource Templates ---",
3954
+ `Resource Template "${name}" not found.
3955
+ Available: ${available}`
3956
+ );
3957
+ } else {
3958
+ sections.push("--- Resource Templates ---", JSON.stringify(templates[0], null, 2));
3959
+ }
3960
+ } else {
3961
+ const displayTemplates = summary ? templates.map((t) => ({
3962
+ name: t.name,
3963
+ uriTemplate: t.uriTemplate,
3964
+ description: t.description
3965
+ })) : templates;
3966
+ sections.push("--- Resource Templates ---", JSON.stringify(displayTemplates, null, 2));
3967
+ }
3968
+ if (result.nextCursor) {
3969
+ sections.push(`--- Resource Templates Next Cursor: ${result.nextCursor} ---`);
3970
+ }
3971
+ } catch (err) {
3972
+ sections.push("--- Resource Templates ---", `Error: ${err.message}`);
3973
+ }
3974
+ }
3420
3975
  if (requested.includes("prompts") && caps.prompts) {
3421
3976
  try {
3422
- const result = await target.listPrompts();
3977
+ const result = await target.listPrompts({ cursor });
3423
3978
  let prompts = result.prompts;
3424
3979
  if (name) {
3425
3980
  prompts = prompts.filter((p) => p.name === name);
@@ -3434,7 +3989,11 @@ Available: ${available}`
3434
3989
  sections.push("--- Prompts ---", JSON.stringify(prompts[0], null, 2));
3435
3990
  }
3436
3991
  } else {
3437
- sections.push("--- Prompts ---", JSON.stringify(prompts, null, 2));
3992
+ const displayPrompts = summary ? prompts.map((p) => ({ name: p.name, description: p.description })) : prompts;
3993
+ sections.push("--- Prompts ---", JSON.stringify(displayPrompts, null, 2));
3994
+ }
3995
+ if (result.nextCursor) {
3996
+ sections.push(`--- Prompts Next Cursor: ${result.nextCursor} ---`);
3438
3997
  }
3439
3998
  } catch (err) {
3440
3999
  sections.push("--- Prompts ---", `Error: ${err.message}`);
@@ -3511,16 +4070,21 @@ Available: ${available}`
3511
4070
  name: z.string().describe("Tool name, resource URI, or prompt name"),
3512
4071
  arguments: z.record(z.unknown()).optional().describe("Arguments for the tool or prompt (not used for resources)"),
3513
4072
  // Auto-connect params (only needed if not already connected)
3514
- command: z.string().optional().describe(
3515
- "Command to spawn the server (e.g. 'node'). Required if not already connected."
4073
+ auto_connect: z.object({
4074
+ command: z.string().describe("Command to spawn the server (e.g. 'node')."),
4075
+ args: z.array(z.string()).optional().describe("Arguments for the server command (e.g. ['src/index.js'])"),
4076
+ env: z.record(z.string()).optional().describe("Extra environment variables for the server process")
4077
+ }).optional().describe(
4078
+ "Provide this to automatically spawn and connect to a server if not already connected. Required if no active connection exists."
3516
4079
  ),
3517
- args: z.array(z.string()).optional().describe("Arguments for the server command (e.g. ['src/index.js'])"),
3518
- env: z.record(z.string()).optional().describe("Extra environment variables for the server process"),
3519
4080
  // Lifecycle
3520
4081
  disconnect_after: z.boolean().optional().describe("Tear down the connection after this call (default: false)"),
3521
4082
  timeout_ms: z.number().optional().describe("Timeout in ms (only applies to type: 'tool')"),
3522
4083
  include_metadata: z.boolean().optional().describe(
3523
4084
  "Include a structured metadata content item with latency, interception info, and content statistics. Useful for programmatic consumption."
4085
+ ),
4086
+ max_text_length: z.number().optional().describe(
4087
+ "Max text response length before truncation for this call. Use -1 to disable truncation."
3524
4088
  )
3525
4089
  }
3526
4090
  },
@@ -3528,15 +4092,18 @@ Available: ${available}`
3528
4092
  type: primitiveType,
3529
4093
  name,
3530
4094
  arguments: callArgs,
3531
- command,
3532
- args,
3533
- env,
4095
+ auto_connect,
3534
4096
  disconnect_after,
3535
4097
  timeout_ms,
3536
- include_metadata
4098
+ include_metadata,
4099
+ max_text_length
3537
4100
  }) => {
3538
4101
  try {
3539
- const connectError = await ensureConnected(command, args, env);
4102
+ const connectError = await ensureConnected(
4103
+ auto_connect?.command,
4104
+ auto_connect?.args,
4105
+ auto_connect?.env
4106
+ );
3540
4107
  if (connectError) {
3541
4108
  return {
3542
4109
  content: [{ type: "text", text: connectError }],
@@ -3602,7 +4169,8 @@ Available tools: ${toolNames.join(", ")}`
3602
4169
  target,
3603
4170
  name,
3604
4171
  callArgs ?? {},
3605
- timeout_ms
4172
+ timeout_ms,
4173
+ max_text_length
3606
4174
  );
3607
4175
  result = toolResult;
3608
4176
  interceptionMeta = metadata;
@@ -3611,17 +4179,12 @@ Available tools: ${toolNames.join(", ")}`
3611
4179
  target,
3612
4180
  name,
3613
4181
  callArgs ?? {},
3614
- timeout_ms
4182
+ timeout_ms,
4183
+ max_text_length
3615
4184
  );
3616
4185
  }
3617
4186
  const elapsedMs = Date.now() - startMs;
3618
4187
  const resultContent = result.content;
3619
- if (!include_metadata && Array.isArray(resultContent) && resultContent.length > 0) {
3620
- const lastItem = resultContent[resultContent.length - 1];
3621
- if (lastItem.type === "text") {
3622
- lastItem.text += ` (${elapsedMs}ms)`;
3623
- }
3624
- }
3625
4188
  if (include_metadata && Array.isArray(resultContent)) {
3626
4189
  const meta = {
3627
4190
  latency_ms: elapsedMs,
@@ -3643,24 +4206,102 @@ ${JSON.stringify(meta)}`
3643
4206
  break;
3644
4207
  }
3645
4208
  case "resource": {
3646
- const resourceResult = await target.readResource({ uri: name });
3647
- result = {
3648
- content: [
3649
- { type: "text", text: JSON.stringify(resourceResult.contents, null, 2) }
3650
- ]
3651
- };
4209
+ const startMs = Date.now();
4210
+ const resourceResult = await interceptor.readResource(
4211
+ target,
4212
+ { uri: name },
4213
+ timeout_ms,
4214
+ max_text_length
4215
+ );
4216
+ const elapsedMs = Date.now() - startMs;
4217
+ const contentItems = resourceResult.contents.map((c) => {
4218
+ if (c.text !== void 0) {
4219
+ return { type: "text", text: c.text };
4220
+ } else {
4221
+ return { type: "text", text: `[Resource blob: ${c.uri}]` };
4222
+ }
4223
+ });
4224
+ result = { content: contentItems };
4225
+ if (include_metadata) {
4226
+ const meta = {
4227
+ latency_ms: elapsedMs,
4228
+ content_items: contentItems.length,
4229
+ is_error: false
4230
+ };
4231
+ contentItems.unshift({
4232
+ type: "text",
4233
+ text: `--- metadata ---
4234
+ ${JSON.stringify(meta)}`
4235
+ });
4236
+ }
3652
4237
  break;
3653
4238
  }
3654
4239
  case "prompt": {
3655
- const promptResult = await target.getPrompt({
3656
- name,
3657
- arguments: callArgs ?? {}
3658
- });
3659
- result = {
3660
- content: [
3661
- { type: "text", text: JSON.stringify(promptResult.messages, null, 2) }
3662
- ]
3663
- };
4240
+ try {
4241
+ const { prompts } = await target.listPrompts();
4242
+ const promptNames = prompts.map((p) => p.name);
4243
+ const matchedPrompt = prompts.find((p) => p.name === name);
4244
+ if (!matchedPrompt) {
4245
+ const suggestion = suggestCommand(name, promptNames);
4246
+ const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
4247
+ return {
4248
+ content: [
4249
+ {
4250
+ type: "text",
4251
+ text: `Prompt "${name}" not found.${hint}
4252
+ Available prompts: ${promptNames.join(", ")}`
4253
+ }
4254
+ ],
4255
+ isError: true
4256
+ };
4257
+ }
4258
+ } catch {
4259
+ }
4260
+ const startMs = Date.now();
4261
+ const promptResult = await interceptor.getPrompt(
4262
+ target,
4263
+ {
4264
+ name,
4265
+ arguments: callArgs ?? {}
4266
+ },
4267
+ timeout_ms,
4268
+ max_text_length
4269
+ );
4270
+ const elapsedMs = Date.now() - startMs;
4271
+ const contentItems = [];
4272
+ for (const msg of promptResult.messages) {
4273
+ const role = msg.role;
4274
+ const content = msg.content;
4275
+ const prefix = `[${role.toUpperCase()} MESSAGE]`;
4276
+ if (content.type === "text") {
4277
+ contentItems.push({ type: "text", text: `${prefix}
4278
+ ${content.text}` });
4279
+ } else if (Array.isArray(content)) {
4280
+ for (const item of content) {
4281
+ if (item.type === "text") {
4282
+ contentItems.push({ type: "text", text: `${prefix}
4283
+ ${item.text}` });
4284
+ } else {
4285
+ contentItems.push(item);
4286
+ }
4287
+ }
4288
+ } else {
4289
+ contentItems.push(content);
4290
+ }
4291
+ }
4292
+ result = { content: contentItems };
4293
+ if (include_metadata) {
4294
+ const meta = {
4295
+ latency_ms: elapsedMs,
4296
+ content_items: contentItems.length,
4297
+ is_error: false
4298
+ };
4299
+ contentItems.unshift({
4300
+ type: "text",
4301
+ text: `--- metadata ---
4302
+ ${JSON.stringify(meta)}`
4303
+ });
4304
+ }
3664
4305
  break;
3665
4306
  }
3666
4307
  }
@@ -3722,74 +4363,376 @@ ${JSON.stringify(meta)}`
3722
4363
  }
3723
4364
 
3724
4365
  // src/index.ts
3725
- function extractTargetCommand(targetCommand) {
3726
- return targetCommand.filter((a) => a !== "--");
3727
- }
3728
4366
  function requireTargetCommand(targetCommand, subcommandUsage) {
3729
- const target = extractTargetCommand(targetCommand);
3730
- if (target.length === 0) {
3731
- process.stderr.write(`Error: No target server command provided after '--'.
4367
+ if (!activeTargetCommand) {
4368
+ process.stderr.write(`Error: Target server command must be separated by '--'.
4369
+ `);
4370
+ process.stderr.write(`This avoids option parsing conflicts.
4371
+
3732
4372
  `);
3733
4373
  process.stderr.write(`Usage: ${subcommandUsage}
3734
4374
  `);
3735
4375
  process.exit(2);
3736
4376
  }
3737
- return target;
4377
+ return activeTargetCommand;
4378
+ }
4379
+ var SESSION_DIR = join2(tmpdir2(), "run-mcp", "sessions");
4380
+ function getSessionPath(name) {
4381
+ return join2(SESSION_DIR, `${name}.json`);
4382
+ }
4383
+ async function getSession(name) {
4384
+ const path2 = getSessionPath(name);
4385
+ if (!existsSync2(path2)) return null;
4386
+ try {
4387
+ const data = await readFile3(path2, "utf8");
4388
+ const parsed = JSON.parse(data);
4389
+ try {
4390
+ process.kill(parsed.pid, 0);
4391
+ return parsed;
4392
+ } catch {
4393
+ await rm(path2, { force: true }).catch(() => {
4394
+ });
4395
+ return null;
4396
+ }
4397
+ } catch {
4398
+ return null;
4399
+ }
4400
+ }
4401
+ function sendDaemonRequest(port, request) {
4402
+ return new Promise((resolve2, reject) => {
4403
+ const socket = createConnection({ port });
4404
+ let buffer = "";
4405
+ socket.on("connect", () => {
4406
+ socket.write(JSON.stringify(request) + "\n");
4407
+ });
4408
+ socket.on("data", (data) => {
4409
+ buffer += data.toString();
4410
+ });
4411
+ socket.on("end", () => {
4412
+ try {
4413
+ const parsed = JSON.parse(buffer);
4414
+ if (parsed.error) {
4415
+ reject(new Error(parsed.error.message));
4416
+ } else {
4417
+ resolve2(parsed.result);
4418
+ }
4419
+ } catch (err) {
4420
+ reject(new Error(`Failed to parse daemon response: ${err}`));
4421
+ }
4422
+ });
4423
+ socket.on("error", (err) => {
4424
+ reject(err);
4425
+ });
4426
+ });
4427
+ }
4428
+ async function handleHeadlessSession(sessionName, targetCommand, operation, opts, subcommandUsage) {
4429
+ let session = await getSession(sessionName);
4430
+ if (!session) {
4431
+ if (!activeTargetCommand) {
4432
+ process.stderr.write(`Error: Session "${sessionName}" is not running.
4433
+ `);
4434
+ process.stderr.write(`Please provide a target command after '--' to start it.
4435
+
4436
+ `);
4437
+ process.stderr.write(`Usage: ${subcommandUsage}
4438
+ `);
4439
+ process.exit(2);
4440
+ }
4441
+ const target = activeTargetCommand;
4442
+ const binPath = resolve(import.meta.dirname, "./index.js");
4443
+ const daemonProcess = spawn("node", [binPath, "daemon", sessionName, ...target], {
4444
+ detached: true,
4445
+ stdio: "ignore"
4446
+ });
4447
+ daemonProcess.unref();
4448
+ let attempts = 0;
4449
+ while (attempts < 50) {
4450
+ session = await getSession(sessionName);
4451
+ if (session) break;
4452
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
4453
+ attempts++;
4454
+ }
4455
+ if (!session) {
4456
+ process.stderr.write(
4457
+ `Error: Failed to spawn background daemon for session "${sessionName}".
4458
+ `
4459
+ );
4460
+ process.exit(1);
4461
+ }
4462
+ }
4463
+ try {
4464
+ const response = await sendDaemonRequest(session.port, {
4465
+ jsonrpc: "2.0",
4466
+ method: "execute",
4467
+ params: { operation, opts },
4468
+ id: 1
4469
+ });
4470
+ process.stdout.write(`${JSON.stringify(response.result, null, 2)}
4471
+ `);
4472
+ process.exit(response.hasError ? 1 : 0);
4473
+ } catch (err) {
4474
+ process.stderr.write(`Error communicating with session daemon: ${err.message}
4475
+ `);
4476
+ process.exit(1);
4477
+ }
3738
4478
  }
3739
4479
  function parseHeadlessOpts(opts) {
3740
4480
  return {
3741
4481
  outDir: opts.outDir,
3742
4482
  timeoutMs: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0,
3743
- raw: opts.raw
4483
+ raw: opts.raw,
4484
+ showStderr: opts.showStderr,
4485
+ mediaThresholdKb: opts.mediaThreshold ? Number.parseInt(opts.mediaThreshold, 10) : void 0
3744
4486
  };
3745
4487
  }
4488
+ var activeTargetCommand;
4489
+ var argvToParse = process.argv;
4490
+ var dashDashIndex = process.argv.indexOf("--");
4491
+ if (dashDashIndex !== -1) {
4492
+ activeTargetCommand = process.argv.slice(dashDashIndex + 1);
4493
+ argvToParse = [...process.argv.slice(0, dashDashIndex)];
4494
+ }
3746
4495
  program.enablePositionalOptions();
3747
- program.command("call").argument("<tool>", "Tool name to call").argument("[json_args]", "JSON arguments for the tool").argument("[target_command...]", "Target server command (after --)").description("Call a tool on a target MCP server and print the result as JSON").option("-o, --out-dir <path>", "Output directory for saved media").option("-t, --timeout <ms>", "Timeout in milliseconds (default: 30000)").option("--raw", "Print the full result object including metadata").allowUnknownOption().action(
4496
+ program.command("call").argument("<tool>", "Tool name to call").argument("[json_args]", "JSON arguments for the tool").argument("[target_command...]", "Target server command (after --)").description("Call a tool on a target MCP server and print the result as JSON").option("-o, --out-dir <path>", "Output directory for saved media").option("-t, --timeout <ms>", "Timeout in milliseconds (default: 30000)").option(
4497
+ "-m, --media-threshold <kb>",
4498
+ "Media size threshold in KB to save to disk (0 to always save, -1 to keep inline)"
4499
+ ).option("--raw", "Print the full result object including metadata").option("--show-stderr", "Stream target server stderr to process stderr").option("--session <name>", "Persistent session name").allowUnknownOption().action(
3748
4500
  async (tool, jsonArgs, targetCommand, opts) => {
4501
+ const operation = { type: "call", tool, args: jsonArgs };
4502
+ const parsedOpts = parseHeadlessOpts(opts);
4503
+ if (opts.session) {
4504
+ await handleHeadlessSession(
4505
+ opts.session,
4506
+ targetCommand,
4507
+ operation,
4508
+ parsedOpts,
4509
+ "run-mcp call <tool> [json_args] -- <server_command...>"
4510
+ );
4511
+ } else {
4512
+ const target = requireTargetCommand(
4513
+ activeTargetCommand ?? targetCommand,
4514
+ "run-mcp call <tool> [json_args] -- <server_command...>"
4515
+ );
4516
+ await runHeadless(target, operation, parsedOpts);
4517
+ }
4518
+ }
4519
+ );
4520
+ program.command("list-tools").argument("[target_command...]", "Target server command (after --)").description("List all tools on a target MCP server as JSON").option("--show-stderr", "Stream target server stderr to process stderr").option("--session <name>", "Persistent session name").allowUnknownOption().action(async (targetCommand, opts) => {
4521
+ const operation = { type: "list-tools" };
4522
+ const parsedOpts = parseHeadlessOpts(opts);
4523
+ if (opts.session) {
4524
+ await handleHeadlessSession(
4525
+ opts.session,
4526
+ targetCommand,
4527
+ operation,
4528
+ parsedOpts,
4529
+ "run-mcp list-tools -- <server_command...>"
4530
+ );
4531
+ } else {
3749
4532
  const target = requireTargetCommand(
4533
+ activeTargetCommand ?? targetCommand,
4534
+ "run-mcp list-tools -- <server_command...>"
4535
+ );
4536
+ await runHeadless(target, operation, parsedOpts);
4537
+ }
4538
+ });
4539
+ program.command("list-resources").argument("[target_command...]", "Target server command (after --)").description("List all resources on a target MCP server as JSON").option("--show-stderr", "Stream target server stderr to process stderr").option("--session <name>", "Persistent session name").allowUnknownOption().action(async (targetCommand, opts) => {
4540
+ const operation = { type: "list-resources" };
4541
+ const parsedOpts = parseHeadlessOpts(opts);
4542
+ if (opts.session) {
4543
+ await handleHeadlessSession(
4544
+ opts.session,
3750
4545
  targetCommand,
3751
- "run-mcp call <tool> [json_args] -- <server_command...>"
4546
+ operation,
4547
+ parsedOpts,
4548
+ "run-mcp list-resources -- <server_command...>"
4549
+ );
4550
+ } else {
4551
+ const target = requireTargetCommand(
4552
+ activeTargetCommand ?? targetCommand,
4553
+ "run-mcp list-resources -- <server_command...>"
3752
4554
  );
3753
- await runHeadless(target, { type: "call", tool, args: jsonArgs }, parseHeadlessOpts(opts));
4555
+ await runHeadless(target, operation, parsedOpts);
3754
4556
  }
3755
- );
3756
- program.command("list-tools").argument("[target_command...]", "Target server command (after --)").description("List all tools on a target MCP server as JSON").allowUnknownOption().action(async (targetCommand) => {
3757
- const target = requireTargetCommand(targetCommand, "run-mcp list-tools -- <server_command...>");
3758
- await runHeadless(target, { type: "list-tools" });
3759
4557
  });
3760
- program.command("list-resources").argument("[target_command...]", "Target server command (after --)").description("List all resources on a target MCP server as JSON").allowUnknownOption().action(async (targetCommand) => {
3761
- const target = requireTargetCommand(
3762
- targetCommand,
3763
- "run-mcp list-resources -- <server_command...>"
3764
- );
3765
- await runHeadless(target, { type: "list-resources" });
4558
+ program.command("list-prompts").argument("[target_command...]", "Target server command (after --)").description("List all prompts on a target MCP server as JSON").option("--show-stderr", "Stream target server stderr to process stderr").option("--session <name>", "Persistent session name").allowUnknownOption().action(async (targetCommand, opts) => {
4559
+ const operation = { type: "list-prompts" };
4560
+ const parsedOpts = parseHeadlessOpts(opts);
4561
+ if (opts.session) {
4562
+ await handleHeadlessSession(
4563
+ opts.session,
4564
+ targetCommand,
4565
+ operation,
4566
+ parsedOpts,
4567
+ "run-mcp list-prompts -- <server_command...>"
4568
+ );
4569
+ } else {
4570
+ const target = requireTargetCommand(
4571
+ activeTargetCommand ?? targetCommand,
4572
+ "run-mcp list-prompts -- <server_command...>"
4573
+ );
4574
+ await runHeadless(target, operation, parsedOpts);
4575
+ }
3766
4576
  });
3767
- program.command("list-prompts").argument("[target_command...]", "Target server command (after --)").description("List all prompts on a target MCP server as JSON").allowUnknownOption().action(async (targetCommand) => {
3768
- const target = requireTargetCommand(
3769
- targetCommand,
3770
- "run-mcp list-prompts -- <server_command...>"
3771
- );
3772
- await runHeadless(target, { type: "list-prompts" });
4577
+ program.command("read").argument("<uri>", "Resource URI to read").argument("[target_command...]", "Target server command (after --)").description("Read a resource by URI from a target MCP server").option(
4578
+ "-m, --media-threshold <kb>",
4579
+ "Media size threshold in KB to save to disk (0 to always save, -1 to keep inline)"
4580
+ ).option("--show-stderr", "Stream target server stderr to process stderr").option("--session <name>", "Persistent session name").allowUnknownOption().action(async (uri, targetCommand, opts) => {
4581
+ const operation = { type: "read", uri };
4582
+ const parsedOpts = parseHeadlessOpts(opts);
4583
+ if (opts.session) {
4584
+ await handleHeadlessSession(
4585
+ opts.session,
4586
+ targetCommand,
4587
+ operation,
4588
+ parsedOpts,
4589
+ "run-mcp read <uri> -- <server_command...>"
4590
+ );
4591
+ } else {
4592
+ const target = requireTargetCommand(
4593
+ activeTargetCommand ?? targetCommand,
4594
+ "run-mcp read <uri> -- <server_command...>"
4595
+ );
4596
+ await runHeadless(target, operation, parsedOpts);
4597
+ }
3773
4598
  });
3774
- program.command("read").argument("<uri>", "Resource URI to read").argument("[target_command...]", "Target server command (after --)").description("Read a resource by URI from a target MCP server").allowUnknownOption().action(async (uri, targetCommand) => {
3775
- const target = requireTargetCommand(targetCommand, "run-mcp read <uri> -- <server_command...>");
3776
- await runHeadless(target, { type: "read", uri });
4599
+ program.command("describe").argument("<tool>", "Tool name to describe").argument("[target_command...]", "Target server command (after --)").description("Print a tool's full schema as JSON").option("--show-stderr", "Stream target server stderr to process stderr").option("--session <name>", "Persistent session name").allowUnknownOption().action(async (tool, targetCommand, opts) => {
4600
+ const operation = { type: "describe", tool };
4601
+ const parsedOpts = parseHeadlessOpts(opts);
4602
+ if (opts.session) {
4603
+ await handleHeadlessSession(
4604
+ opts.session,
4605
+ targetCommand,
4606
+ operation,
4607
+ parsedOpts,
4608
+ "run-mcp describe <tool> -- <server_command...>"
4609
+ );
4610
+ } else {
4611
+ const target = requireTargetCommand(
4612
+ activeTargetCommand ?? targetCommand,
4613
+ "run-mcp describe <tool> -- <server_command...>"
4614
+ );
4615
+ await runHeadless(target, operation, parsedOpts);
4616
+ }
3777
4617
  });
3778
- program.command("describe").argument("<tool>", "Tool name to describe").argument("[target_command...]", "Target server command (after --)").description("Print a tool's full schema as JSON").allowUnknownOption().action(async (tool, targetCommand) => {
3779
- const target = requireTargetCommand(
3780
- targetCommand,
3781
- "run-mcp describe <tool> -- <server_command...>"
3782
- );
3783
- await runHeadless(target, { type: "describe", tool });
4618
+ program.command("get-prompt").argument("<name>", "Prompt name").argument("[json_args]", "JSON arguments for the prompt").argument("[target_command...]", "Target server command (after --)").description("Get a prompt with optional arguments from a target MCP server").option(
4619
+ "-m, --media-threshold <kb>",
4620
+ "Media size threshold in KB to save to disk (0 to always save, -1 to keep inline)"
4621
+ ).option("--show-stderr", "Stream target server stderr to process stderr").option("--session <name>", "Persistent session name").allowUnknownOption().action(
4622
+ async (name, jsonArgs, targetCommand, opts) => {
4623
+ const operation = { type: "get-prompt", name, args: jsonArgs };
4624
+ const parsedOpts = parseHeadlessOpts(opts);
4625
+ if (opts.session) {
4626
+ await handleHeadlessSession(
4627
+ opts.session,
4628
+ targetCommand,
4629
+ operation,
4630
+ parsedOpts,
4631
+ "run-mcp get-prompt <name> [json_args] -- <server_command...>"
4632
+ );
4633
+ } else {
4634
+ const target = requireTargetCommand(
4635
+ activeTargetCommand ?? targetCommand,
4636
+ "run-mcp get-prompt <name> [json_args] -- <server_command...>"
4637
+ );
4638
+ await runHeadless(target, operation, parsedOpts);
4639
+ }
4640
+ }
4641
+ );
4642
+ program.command("daemon").argument("<session_name>", "Session name").argument("[target_command...]", "Target server command").description("Start run-mcp in background session daemon mode").allowUnknownOption().action(async (sessionName, targetCommand) => {
4643
+ const targetCmd = activeTargetCommand ?? targetCommand;
4644
+ if (!targetCmd || targetCmd.length === 0) {
4645
+ process.stderr.write("Error: No target command provided for daemon.\n");
4646
+ process.exit(1);
4647
+ }
4648
+ const server = createServer();
4649
+ server.listen(0, "127.0.0.1", async () => {
4650
+ const addr = server.address();
4651
+ const port = addr.port;
4652
+ const target = new TargetManager(targetCmd[0], targetCmd.slice(1));
4653
+ const interceptor = new ResponseInterceptor();
4654
+ try {
4655
+ await target.connect();
4656
+ } catch (err) {
4657
+ process.stderr.write(`Daemon failed to connect to target: ${err.message}
4658
+ `);
4659
+ process.exit(1);
4660
+ }
4661
+ await mkdir2(SESSION_DIR, { recursive: true });
4662
+ await writeFile2(
4663
+ getSessionPath(sessionName),
4664
+ JSON.stringify({ port, pid: process.pid }),
4665
+ "utf8"
4666
+ );
4667
+ server.on("connection", (socket) => {
4668
+ let buffer = "";
4669
+ socket.on("data", async (data) => {
4670
+ buffer += data.toString();
4671
+ const lines = buffer.split("\n");
4672
+ buffer = lines.pop() ?? "";
4673
+ for (const line of lines) {
4674
+ const trimmed = line.trim();
4675
+ if (!trimmed) continue;
4676
+ try {
4677
+ const req = JSON.parse(trimmed);
4678
+ if (req.method === "execute") {
4679
+ const { operation, opts } = req.params;
4680
+ const { result, hasError } = await executeOperation(
4681
+ target,
4682
+ interceptor,
4683
+ operation,
4684
+ opts
4685
+ );
4686
+ socket.write(
4687
+ JSON.stringify({ jsonrpc: "2.0", result: { result, hasError }, id: req.id }) + "\n"
4688
+ );
4689
+ socket.end();
4690
+ } else if (req.method === "close") {
4691
+ socket.write(
4692
+ JSON.stringify({ jsonrpc: "2.0", result: { ok: true }, id: req.id }) + "\n"
4693
+ );
4694
+ socket.end();
4695
+ await target.close().catch(() => {
4696
+ });
4697
+ await rm(getSessionPath(sessionName), { force: true }).catch(() => {
4698
+ });
4699
+ process.exit(0);
4700
+ }
4701
+ } catch (err) {
4702
+ socket.write(
4703
+ JSON.stringify({ jsonrpc: "2.0", error: { message: err.message }, id: 1 }) + "\n"
4704
+ );
4705
+ socket.end();
4706
+ }
4707
+ }
4708
+ });
4709
+ });
4710
+ });
3784
4711
  });
3785
- program.command("get-prompt").argument("<name>", "Prompt name").argument("[json_args]", "JSON arguments for the prompt").argument("[target_command...]", "Target server command (after --)").description("Get a prompt with optional arguments from a target MCP server").allowUnknownOption().action(async (name, jsonArgs, targetCommand) => {
3786
- const target = requireTargetCommand(
3787
- targetCommand,
3788
- "run-mcp get-prompt <name> [json_args] -- <server_command...>"
3789
- );
3790
- await runHeadless(target, { type: "get-prompt", name, args: jsonArgs });
4712
+ program.command("close-session").argument("<session_name>", "Session name").description("Stop a running session daemon").action(async (sessionName) => {
4713
+ const session = await getSession(sessionName);
4714
+ if (!session) {
4715
+ console.log(`Session "${sessionName}" is not running.`);
4716
+ return;
4717
+ }
4718
+ try {
4719
+ await sendDaemonRequest(session.port, {
4720
+ jsonrpc: "2.0",
4721
+ method: "close",
4722
+ params: {},
4723
+ id: 1
4724
+ });
4725
+ console.log(`Session "${sessionName}" stopped successfully.`);
4726
+ } catch {
4727
+ try {
4728
+ process.kill(session.pid, "SIGTERM");
4729
+ console.log(`Session "${sessionName}" stopped (SIGTERM).`);
4730
+ } catch {
4731
+ console.log(`Failed to stop session "${sessionName}".`);
4732
+ }
4733
+ }
3791
4734
  });
3792
- program.name("run-mcp").description("A smart interactive REPL and live test harness for MCP servers").version("1.6.0").passThroughOptions().allowUnknownOption().argument(
4735
+ program.name("run-mcp").description("A smart interactive REPL and live test harness for MCP servers").version("1.6.1").passThroughOptions().allowUnknownOption().argument(
3793
4736
  "[target_command...]",
3794
4737
  "Command to spawn the target MCP server (starts REPL if provided, Agent server otherwise)"
3795
4738
  ).option("-o, --out-dir <path>", "Directory to save intercepted images and audio").option(
@@ -3798,16 +4741,19 @@ program.name("run-mcp").description("A smart interactive REPL and live test harn
3798
4741
  ).option(
3799
4742
  "--max-text <chars>",
3800
4743
  "Max text response length before truncation (default: 50000) (Agent Mode only)"
4744
+ ).option(
4745
+ "-m, --media-threshold <kb>",
4746
+ "Media size threshold in KB to save to disk (0 to always save, -1 to keep inline)"
3801
4747
  ).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(
3802
4748
  "after",
3803
4749
  `
3804
4750
  Examples:
3805
4751
  $ run-mcp # Test harness (agent mode)
3806
- $ run-mcp node my-server.js # Interactive testing (human REPL mode)
3807
- $ run-mcp node my-server.js -s test.txt # Run a script in REPL mode
3808
- $ run-mcp npx -y some-mcp-server # Test an npx server
4752
+ $ run-mcp -- node my-server.js # Interactive testing (human REPL mode)
4753
+ $ run-mcp -s test.txt -- node my-server.js # Run a script in REPL mode
4754
+ $ run-mcp -- npx -y some-mcp-server # Test an npx server
3809
4755
  $ run-mcp --out-dir ./test-output # Agent mode with options
3810
- $ run-mcp --out-dir ./screenshots node srv.js # REPL mode with options
4756
+ $ run-mcp --out-dir ./screenshots -- node srv.js # REPL mode with options
3811
4757
 
3812
4758
  Headless Commands (pipe-friendly, JSON output):
3813
4759
  $ run-mcp call echo '{"text":"hi"}' -- node my-server.js
@@ -3863,14 +4809,26 @@ REPL Mode Commands (once connected):
3863
4809
  Shortcuts: tl td tc ts rl rr rt rs ru pl pg (see help for details)`
3864
4810
  ).action(
3865
4811
  async (targetCommand, opts) => {
3866
- if (targetCommand && targetCommand.length > 0) {
3867
- await startRepl(targetCommand, { script: opts.script, outDir: opts.outDir });
4812
+ if (targetCommand && targetCommand.length > 0 && !activeTargetCommand) {
4813
+ process.stderr.write(
4814
+ "Error: Target server command must be separated by '--'.\nThis avoids argument parsing ambiguity.\n\nExample:\n run-mcp -- node my-server.js\n run-mcp -s script.txt -- node my-server.js\n"
4815
+ );
4816
+ process.exit(1);
4817
+ }
4818
+ const target = activeTargetCommand ?? [];
4819
+ if (target && target.length > 0) {
4820
+ await startRepl(target, {
4821
+ script: opts.script,
4822
+ outDir: opts.outDir,
4823
+ mediaThresholdKb: opts.mediaThreshold ? Number.parseInt(opts.mediaThreshold, 10) : void 0
4824
+ });
3868
4825
  } else {
3869
4826
  if (opts.mcp || !process.stdin.isTTY) {
3870
4827
  await startServer({
3871
4828
  outDir: opts.outDir,
3872
4829
  timeoutMs: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0,
3873
- maxTextLength: opts.maxText ? Number.parseInt(opts.maxText, 10) : void 0
4830
+ maxTextLength: opts.maxText ? Number.parseInt(opts.maxText, 10) : void 0,
4831
+ mediaThresholdKb: opts.mediaThreshold ? Number.parseInt(opts.mediaThreshold, 10) : void 0
3874
4832
  });
3875
4833
  } else {
3876
4834
  const selected = await pickDiscoveredServer();
@@ -3883,10 +4841,11 @@ Shortcuts: tl td tc ts rl rr rt rs ru pl pg (see help for details)`
3883
4841
  }
3884
4842
  await startRepl([selected.config.command, ...selected.config.args || []], {
3885
4843
  script: opts.script,
3886
- outDir: opts.outDir
4844
+ outDir: opts.outDir,
4845
+ mediaThresholdKb: opts.mediaThreshold ? Number.parseInt(opts.mediaThreshold, 10) : void 0
3887
4846
  });
3888
4847
  }
3889
4848
  }
3890
4849
  }
3891
4850
  );
3892
- program.parse();
4851
+ program.parse(argvToParse);