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.
- package/README.md +109 -69
- package/dist/index.js +2127 -1168
- 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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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/
|
|
294
|
-
import
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
this._startStableTimer();
|
|
495
|
-
}
|
|
496
|
-
get connected() {
|
|
497
|
-
return this._connected;
|
|
523
|
+
result.push(ch);
|
|
524
|
+
i++;
|
|
498
525
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
555
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
result,
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
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
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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 (
|
|
788
|
+
if (isJson) {
|
|
811
789
|
try {
|
|
812
|
-
|
|
790
|
+
result[key] = JSON.parse(rawVal);
|
|
813
791
|
} catch {
|
|
792
|
+
result[key] = rawVal;
|
|
814
793
|
}
|
|
815
|
-
|
|
794
|
+
} else {
|
|
795
|
+
result[key] = rawVal;
|
|
816
796
|
}
|
|
817
|
-
this._connected = false;
|
|
818
|
-
this.childPid = null;
|
|
819
797
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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 (
|
|
841
|
-
|
|
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
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
*
|
|
870
|
-
*
|
|
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
|
-
|
|
874
|
-
this.
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
-
|
|
1158
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
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
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
return
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
-
|
|
1235
|
-
|
|
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
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
-
|
|
1490
|
+
target.on("stderr", () => {
|
|
1491
|
+
});
|
|
1272
1492
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
result.
|
|
1315
|
-
|
|
1316
|
-
|
|
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({
|
|
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
|
|
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
|
-
|
|
1756
|
-
if (!trimmed || trimmed.startsWith("#"))
|
|
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(
|
|
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 (
|
|
1761
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
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
|
-
|
|
2094
|
-
|
|
2095
|
-
const
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2102
|
-
console.log(`
|
|
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
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
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((
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
3165
|
-
for (const [key, value] of Object.entries(
|
|
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(
|
|
3170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3515
|
-
"Command to spawn the server (e.g. 'node').
|
|
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
|
-
|
|
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(
|
|
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
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
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
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
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
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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-
|
|
3761
|
-
const
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
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("
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
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("
|
|
3775
|
-
const
|
|
3776
|
-
|
|
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("
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
)
|
|
3783
|
-
|
|
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("
|
|
3786
|
-
const
|
|
3787
|
-
|
|
3788
|
-
"
|
|
3789
|
-
|
|
3790
|
-
|
|
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.
|
|
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
|
|
3807
|
-
$ run-mcp node my-server.js
|
|
3808
|
-
$ run-mcp npx -y some-mcp-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
|
|
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
|
-
|
|
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);
|