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