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