@modelcontextprotocol/inspector 2.9.0 → 2.10.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 +9 -2
- package/clients/cli/build/index.js +14165 -10590
- package/clients/mcpdo/build/chunk-2E5EZYNU.js +21371 -0
- package/clients/mcpdo/build/mcp-bin.js +3990 -0
- package/clients/mcpdo/build/mcpdod.js +38 -0
- package/clients/tui/build/index.js +4702 -2501
- package/clients/web/build/index.js +498 -86
- package/clients/web/dist/assets/index-DgDEc2gL.js +1589 -0
- package/clients/web/dist/assets/{prism-light-leP1dqtE.js → prism-light-B0A727v7.js} +1 -1
- package/clients/web/dist/index.html +1 -1
- package/package.json +40 -11
- package/scripts/install-clients.mjs +1 -1
- package/skills/mcpdo/SKILL.md +165 -0
- package/clients/web/dist/assets/index-DgXruQhz.js +0 -1598
|
@@ -0,0 +1,3990 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AUTH_HELPER_COMMAND,
|
|
4
|
+
CallbackNavigation,
|
|
5
|
+
CliExitCodeError,
|
|
6
|
+
DAEMON_DIR_ENV,
|
|
7
|
+
DAEMON_TOKEN_ENV,
|
|
8
|
+
DEFAULT_MAX_FETCH_REQUESTS,
|
|
9
|
+
DEFAULT_TASK_TTL_MS,
|
|
10
|
+
EXIT_CODES,
|
|
11
|
+
InMemorySecretStore,
|
|
12
|
+
NodeOAuthStorage,
|
|
13
|
+
PENDING_AUTH_TTL_MS,
|
|
14
|
+
PLAIN,
|
|
15
|
+
assertSocketPathWithinLimit,
|
|
16
|
+
authorizeInFrontend,
|
|
17
|
+
awaitableError,
|
|
18
|
+
awaitableLog,
|
|
19
|
+
clearAllOAuthClientState,
|
|
20
|
+
clearEmaIdpSession,
|
|
21
|
+
createPrivateDaemonDir,
|
|
22
|
+
disallowMemorySecretStoreFallback,
|
|
23
|
+
emaLogin,
|
|
24
|
+
emaLogout,
|
|
25
|
+
encodeRequest,
|
|
26
|
+
ensureDaemonDir,
|
|
27
|
+
generateDaemonToken,
|
|
28
|
+
getDaemonDir,
|
|
29
|
+
getDaemonLogPath,
|
|
30
|
+
getDaemonSocketPath,
|
|
31
|
+
getDaemonTokenFromEnv,
|
|
32
|
+
getEmaIdpLoginState,
|
|
33
|
+
getEmaStatus,
|
|
34
|
+
getSecretStorageInfo,
|
|
35
|
+
getStateFilePath,
|
|
36
|
+
handleError,
|
|
37
|
+
idpOAuthStorageKey,
|
|
38
|
+
isCliAutoOpenForced,
|
|
39
|
+
isSafeLinkTarget,
|
|
40
|
+
loadEmaIdpConfig,
|
|
41
|
+
loadServerEntries,
|
|
42
|
+
normalizeIdpIssuer,
|
|
43
|
+
obtainPendingAuthUrl,
|
|
44
|
+
obtainPendingUrlForKey,
|
|
45
|
+
parseHeaderPair,
|
|
46
|
+
parseIdpOAuthStorageKey,
|
|
47
|
+
parseKeyValuePair,
|
|
48
|
+
pendingAuthMarkerPath,
|
|
49
|
+
readDaemonTokenFile,
|
|
50
|
+
readInspectorVersion,
|
|
51
|
+
readOAuthStore,
|
|
52
|
+
removeOwnPendingAuthMarker,
|
|
53
|
+
requireIdp,
|
|
54
|
+
resetNodeOAuthStorageCache,
|
|
55
|
+
resolveServerSource,
|
|
56
|
+
runAuthHelper,
|
|
57
|
+
runEmaIdpInteractiveFlow,
|
|
58
|
+
sanitizeDeep,
|
|
59
|
+
sanitizeText,
|
|
60
|
+
selectServerEntry,
|
|
61
|
+
setSecretStorageWarningsQuiet,
|
|
62
|
+
styleFromOpts,
|
|
63
|
+
warnAboutSecretStorage,
|
|
64
|
+
withDefaultCatalogPath,
|
|
65
|
+
writePendingAuthMarker
|
|
66
|
+
} from "./chunk-2E5EZYNU.js";
|
|
67
|
+
|
|
68
|
+
// src/mcp-bin.ts
|
|
69
|
+
import { realpathSync } from "fs";
|
|
70
|
+
import { resolve as resolve2 } from "path";
|
|
71
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
72
|
+
|
|
73
|
+
// src/connection/mcp.ts
|
|
74
|
+
import { Command } from "commander";
|
|
75
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
76
|
+
import path4 from "path";
|
|
77
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
78
|
+
import { getDefaultEnvironment } from "@modelcontextprotocol/client/stdio";
|
|
79
|
+
import { LoggingLevelSchema } from "@modelcontextprotocol/core";
|
|
80
|
+
|
|
81
|
+
// src/daemon/client.ts
|
|
82
|
+
import { randomUUID } from "crypto";
|
|
83
|
+
import * as net from "net";
|
|
84
|
+
import * as path from "path";
|
|
85
|
+
function daemonTokenDir(options) {
|
|
86
|
+
if (options.dir !== void 0) return options.dir;
|
|
87
|
+
if (options.socketPath !== void 0 && !options.socketPath.startsWith("\\\\.\\pipe\\")) {
|
|
88
|
+
return path.dirname(options.socketPath);
|
|
89
|
+
}
|
|
90
|
+
return getDaemonDir();
|
|
91
|
+
}
|
|
92
|
+
async function callDaemon(op, params, options = {}) {
|
|
93
|
+
const socketPath = options.socketPath ?? getDaemonSocketPath();
|
|
94
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
95
|
+
const id = randomUUID();
|
|
96
|
+
const token = options.token ?? getDaemonTokenFromEnv() ?? readDaemonTokenFile(daemonTokenDir(options));
|
|
97
|
+
const request = { id, op, params };
|
|
98
|
+
if (token !== void 0) request.token = token;
|
|
99
|
+
return new Promise((resolve3, reject) => {
|
|
100
|
+
let settled = false;
|
|
101
|
+
let buffer = "";
|
|
102
|
+
let queue = Promise.resolve();
|
|
103
|
+
let timer;
|
|
104
|
+
const socket = new net.Socket();
|
|
105
|
+
socket.setEncoding("utf8");
|
|
106
|
+
function settle(fn) {
|
|
107
|
+
if (settled) return;
|
|
108
|
+
settled = true;
|
|
109
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
110
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
111
|
+
socket.removeAllListeners();
|
|
112
|
+
socket.on("error", () => {
|
|
113
|
+
});
|
|
114
|
+
fn();
|
|
115
|
+
}
|
|
116
|
+
function onAbort() {
|
|
117
|
+
fail(
|
|
118
|
+
new CliExitCodeError(EXIT_CODES.USAGE, `'${op}' cancelled.`, {
|
|
119
|
+
code: "cancelled"
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
function fail(error) {
|
|
124
|
+
settle(() => {
|
|
125
|
+
socket.destroy();
|
|
126
|
+
reject(error);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function succeed(value) {
|
|
130
|
+
settle(() => {
|
|
131
|
+
socket.end();
|
|
132
|
+
resolve3(value);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function handleLine(line) {
|
|
136
|
+
const trimmed = line.trim();
|
|
137
|
+
if (!trimmed) return Promise.resolve();
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = JSON.parse(trimmed);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
fail(error);
|
|
143
|
+
return Promise.resolve();
|
|
144
|
+
}
|
|
145
|
+
if (parsed !== null && typeof parsed === "object" && "kind" in parsed && parsed.kind === "elicitation-request") {
|
|
146
|
+
return handleElicitationRequest(parsed);
|
|
147
|
+
}
|
|
148
|
+
handleResponse(parsed);
|
|
149
|
+
return Promise.resolve();
|
|
150
|
+
}
|
|
151
|
+
async function handleElicitationRequest(frame) {
|
|
152
|
+
if (frame.id !== id) return;
|
|
153
|
+
if (timer !== void 0) {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
timer = void 0;
|
|
156
|
+
}
|
|
157
|
+
const answer = options.onElicitation ? await options.onElicitation(frame) : {
|
|
158
|
+
id: frame.id,
|
|
159
|
+
kind: "elicitation-response",
|
|
160
|
+
elicitationId: frame.elicitationId,
|
|
161
|
+
action: "cancel"
|
|
162
|
+
};
|
|
163
|
+
if (settled || socket.destroyed) return;
|
|
164
|
+
socket.write(JSON.stringify(answer) + "\n");
|
|
165
|
+
}
|
|
166
|
+
function handleResponse(response) {
|
|
167
|
+
if (response.id !== id && response.id !== "?") {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
fail(
|
|
172
|
+
new CliExitCodeError(
|
|
173
|
+
response.error.exitCode ?? EXIT_CODES.USAGE,
|
|
174
|
+
// Daemon error text can embed server-supplied strings; sanitize
|
|
175
|
+
// before it reaches a terminal via the shared error handler.
|
|
176
|
+
sanitizeText(response.error.message),
|
|
177
|
+
{ code: response.error.code }
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
succeed(response.result);
|
|
183
|
+
}
|
|
184
|
+
socket.on("error", (err) => {
|
|
185
|
+
fail(
|
|
186
|
+
new CliExitCodeError(
|
|
187
|
+
EXIT_CODES.UNREACHABLE,
|
|
188
|
+
`Cannot reach connection daemon at ${socketPath}: ${err.message}`,
|
|
189
|
+
{ code: "daemon_unreachable" }
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
socket.on("close", () => {
|
|
194
|
+
if (!settled) {
|
|
195
|
+
fail(
|
|
196
|
+
new CliExitCodeError(
|
|
197
|
+
EXIT_CODES.UNREACHABLE,
|
|
198
|
+
`Connection daemon closed the connection during '${op}'`,
|
|
199
|
+
{ code: "daemon_unreachable" }
|
|
200
|
+
)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
timer = timeoutMs > 0 ? setTimeout(() => {
|
|
205
|
+
fail(
|
|
206
|
+
new CliExitCodeError(
|
|
207
|
+
EXIT_CODES.UNREACHABLE,
|
|
208
|
+
`Daemon request '${op}' timed out after ${timeoutMs}ms`,
|
|
209
|
+
{ code: "daemon_timeout" }
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
}, timeoutMs) : void 0;
|
|
213
|
+
if (options.signal?.aborted) {
|
|
214
|
+
onAbort();
|
|
215
|
+
} else {
|
|
216
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
217
|
+
}
|
|
218
|
+
socket.once("connect", () => {
|
|
219
|
+
socket.write(encodeRequest(request));
|
|
220
|
+
});
|
|
221
|
+
socket.on("data", (chunk) => {
|
|
222
|
+
buffer += String(chunk);
|
|
223
|
+
let idx;
|
|
224
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
225
|
+
const line = buffer.slice(0, idx);
|
|
226
|
+
buffer = buffer.slice(idx + 1);
|
|
227
|
+
queue = queue.then(() => handleLine(line)).catch((error) => fail(error));
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
if (!settled) socket.connect(socketPath);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/daemon/stream-client.ts
|
|
235
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
236
|
+
import * as net2 from "net";
|
|
237
|
+
async function streamDaemon(params, options) {
|
|
238
|
+
const socketPath = options.socketPath ?? getDaemonSocketPath();
|
|
239
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
240
|
+
const id = randomUUID2();
|
|
241
|
+
const token = options.token ?? getDaemonTokenFromEnv() ?? readDaemonTokenFile(daemonTokenDir(options));
|
|
242
|
+
const request = { id, op: "stream", params };
|
|
243
|
+
if (token !== void 0) request.token = token;
|
|
244
|
+
return new Promise((resolve3, reject) => {
|
|
245
|
+
let settled = false;
|
|
246
|
+
let buffer = "";
|
|
247
|
+
let streaming = false;
|
|
248
|
+
let pendingCallbacks = 0;
|
|
249
|
+
let timer;
|
|
250
|
+
const socket = new net2.Socket();
|
|
251
|
+
socket.setEncoding("utf8");
|
|
252
|
+
function settle(fn) {
|
|
253
|
+
if (settled) return;
|
|
254
|
+
settled = true;
|
|
255
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
256
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
257
|
+
socket.removeAllListeners();
|
|
258
|
+
socket.on("error", () => {
|
|
259
|
+
});
|
|
260
|
+
fn();
|
|
261
|
+
}
|
|
262
|
+
function fail(error) {
|
|
263
|
+
settle(() => {
|
|
264
|
+
socket.destroy();
|
|
265
|
+
reject(error);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function succeed() {
|
|
269
|
+
settle(() => {
|
|
270
|
+
socket.destroy();
|
|
271
|
+
resolve3();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function onAbort() {
|
|
275
|
+
succeed();
|
|
276
|
+
}
|
|
277
|
+
function handleLine(line) {
|
|
278
|
+
const trimmed = line.trim();
|
|
279
|
+
if (!trimmed) return;
|
|
280
|
+
if (!streaming) {
|
|
281
|
+
let response;
|
|
282
|
+
try {
|
|
283
|
+
response = JSON.parse(trimmed);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
fail(error);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (response.id !== id && response.id !== "?") return;
|
|
289
|
+
if (!response.ok) {
|
|
290
|
+
fail(
|
|
291
|
+
new CliExitCodeError(
|
|
292
|
+
response.error.exitCode ?? EXIT_CODES.USAGE,
|
|
293
|
+
sanitizeText(response.error.message),
|
|
294
|
+
{ code: response.error.code }
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
streaming = true;
|
|
300
|
+
if (timer !== void 0) {
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
timer = void 0;
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
let frame;
|
|
307
|
+
try {
|
|
308
|
+
frame = JSON.parse(trimmed);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
fail(error);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (frame.id !== id) return;
|
|
314
|
+
if (frame.stream === "data") {
|
|
315
|
+
const result = options.onData(frame.data);
|
|
316
|
+
if (result !== void 0 && typeof result.then === "function") {
|
|
317
|
+
pendingCallbacks++;
|
|
318
|
+
socket.pause();
|
|
319
|
+
void Promise.resolve(result).catch(() => {
|
|
320
|
+
}).finally(() => {
|
|
321
|
+
pendingCallbacks--;
|
|
322
|
+
if (pendingCallbacks === 0 && !settled) socket.resume();
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (frame.stream === "end") {
|
|
328
|
+
succeed();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
socket.on("error", (err) => {
|
|
332
|
+
if (streaming) {
|
|
333
|
+
fail(
|
|
334
|
+
new CliExitCodeError(
|
|
335
|
+
EXIT_CODES.UNREACHABLE,
|
|
336
|
+
`Connection daemon stream failed: ${err.message}`,
|
|
337
|
+
{ code: "daemon_unreachable" }
|
|
338
|
+
)
|
|
339
|
+
);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
fail(
|
|
343
|
+
new CliExitCodeError(
|
|
344
|
+
EXIT_CODES.UNREACHABLE,
|
|
345
|
+
`Cannot reach connection daemon at ${socketPath}: ${err.message}`,
|
|
346
|
+
{ code: "daemon_unreachable" }
|
|
347
|
+
)
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
socket.on("close", () => {
|
|
351
|
+
if (settled) return;
|
|
352
|
+
if (streaming) {
|
|
353
|
+
fail(
|
|
354
|
+
new CliExitCodeError(
|
|
355
|
+
EXIT_CODES.UNREACHABLE,
|
|
356
|
+
`Connection daemon closed the stream before it ended`,
|
|
357
|
+
{ code: "daemon_unreachable" }
|
|
358
|
+
)
|
|
359
|
+
);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
fail(
|
|
363
|
+
new CliExitCodeError(
|
|
364
|
+
EXIT_CODES.UNREACHABLE,
|
|
365
|
+
`Connection daemon closed the connection before the stream opened`,
|
|
366
|
+
{ code: "daemon_unreachable" }
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
timer = timeoutMs > 0 ? setTimeout(() => {
|
|
371
|
+
fail(
|
|
372
|
+
new CliExitCodeError(
|
|
373
|
+
EXIT_CODES.UNREACHABLE,
|
|
374
|
+
`Daemon stream open timed out after ${timeoutMs}ms`,
|
|
375
|
+
{ code: "daemon_timeout" }
|
|
376
|
+
)
|
|
377
|
+
);
|
|
378
|
+
}, timeoutMs) : void 0;
|
|
379
|
+
if (options.signal?.aborted) {
|
|
380
|
+
onAbort();
|
|
381
|
+
} else {
|
|
382
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
383
|
+
}
|
|
384
|
+
socket.once("connect", () => {
|
|
385
|
+
socket.write(encodeRequest(request));
|
|
386
|
+
});
|
|
387
|
+
socket.on("data", (chunk) => {
|
|
388
|
+
buffer += String(chunk);
|
|
389
|
+
let idx;
|
|
390
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
391
|
+
const line = buffer.slice(0, idx);
|
|
392
|
+
buffer = buffer.slice(idx + 1);
|
|
393
|
+
handleLine(line);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
if (!settled) socket.connect(socketPath);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/daemon/ensure.ts
|
|
401
|
+
import { spawn } from "child_process";
|
|
402
|
+
import * as fs from "fs";
|
|
403
|
+
import * as net3 from "net";
|
|
404
|
+
import * as path2 from "path";
|
|
405
|
+
import { fileURLToPath } from "url";
|
|
406
|
+
var READY_TIMEOUT_MS = 1e4;
|
|
407
|
+
var READY_POLL_MS = 50;
|
|
408
|
+
function resolveDaemonScriptPath() {
|
|
409
|
+
const here = path2.dirname(fileURLToPath(import.meta.url));
|
|
410
|
+
const candidates = [
|
|
411
|
+
path2.resolve(here, "mcpdod.js"),
|
|
412
|
+
path2.resolve(here, "../mcpdod.js"),
|
|
413
|
+
path2.resolve(here, "../../build/mcpdod.js"),
|
|
414
|
+
path2.resolve(here, "../build/mcpdod.js")
|
|
415
|
+
];
|
|
416
|
+
for (const candidate of candidates) {
|
|
417
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
418
|
+
}
|
|
419
|
+
throw new CliExitCodeError(
|
|
420
|
+
EXIT_CODES.USAGE,
|
|
421
|
+
`Connection daemon bundle not found (looked for mcpdod.js near ${here}). Run npm run build in clients/mcpdo.`,
|
|
422
|
+
{ code: "daemon_not_built" }
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
async function isDaemonReachable(socketPath) {
|
|
426
|
+
return new Promise((resolve3) => {
|
|
427
|
+
let settled = false;
|
|
428
|
+
const socket = new net3.Socket();
|
|
429
|
+
const done = (ok) => {
|
|
430
|
+
if (settled) return;
|
|
431
|
+
settled = true;
|
|
432
|
+
socket.removeAllListeners();
|
|
433
|
+
socket.on("error", () => {
|
|
434
|
+
});
|
|
435
|
+
socket.destroy();
|
|
436
|
+
resolve3(ok);
|
|
437
|
+
};
|
|
438
|
+
socket.on("error", () => done(false));
|
|
439
|
+
socket.setTimeout(500);
|
|
440
|
+
socket.once("connect", () => done(true));
|
|
441
|
+
socket.once("timeout", () => done(false));
|
|
442
|
+
socket.connect(socketPath);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
async function waitForDaemon(socketPath, token, logPath, opts) {
|
|
446
|
+
const deadline = Date.now() + (opts?.timeoutMs ?? READY_TIMEOUT_MS);
|
|
447
|
+
while (Date.now() < deadline) {
|
|
448
|
+
if (await isDaemonReachable(socketPath)) {
|
|
449
|
+
const effectiveToken = opts?.rereadTokenDir ? readDaemonTokenFile(opts.rereadTokenDir) ?? token : token;
|
|
450
|
+
try {
|
|
451
|
+
await callDaemon(
|
|
452
|
+
"ping",
|
|
453
|
+
{},
|
|
454
|
+
{ socketPath, timeoutMs: 2e3, token: effectiveToken }
|
|
455
|
+
);
|
|
456
|
+
return;
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
await new Promise((r) => setTimeout(r, READY_POLL_MS));
|
|
461
|
+
}
|
|
462
|
+
const logTail = readLogTail(logPath);
|
|
463
|
+
throw new CliExitCodeError(
|
|
464
|
+
EXIT_CODES.UNREACHABLE,
|
|
465
|
+
`Timed out waiting for connection daemon at ${socketPath}` + (logTail ? `
|
|
466
|
+
Daemon log (${logPath}):
|
|
467
|
+
${logTail}` : ""),
|
|
468
|
+
{ code: "daemon_start_timeout" }
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
function readLogTail(logPath, maxLines = 10) {
|
|
472
|
+
try {
|
|
473
|
+
const text = fs.readFileSync(logPath, "utf8");
|
|
474
|
+
return text.trimEnd().split("\n").slice(-maxLines).join("\n");
|
|
475
|
+
} catch {
|
|
476
|
+
return "";
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
var STOPPING_EXIT_TIMEOUT_MS = 1e4;
|
|
480
|
+
var STOPPING_POLL_MS = 100;
|
|
481
|
+
function pidAlive(pid) {
|
|
482
|
+
try {
|
|
483
|
+
process.kill(pid, 0);
|
|
484
|
+
return true;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
return error.code === "EPERM";
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function waitForDaemonExit(pid, socketPath, timeoutMs = STOPPING_EXIT_TIMEOUT_MS, pollMs = STOPPING_POLL_MS) {
|
|
490
|
+
const deadline = Date.now() + timeoutMs;
|
|
491
|
+
for (; ; ) {
|
|
492
|
+
const gone = pid !== void 0 ? !pidAlive(pid) : !await isDaemonReachable(socketPath);
|
|
493
|
+
if (gone) return;
|
|
494
|
+
if (Date.now() >= deadline) {
|
|
495
|
+
throw new CliExitCodeError(
|
|
496
|
+
EXIT_CODES.UNREACHABLE,
|
|
497
|
+
"Connection daemon is shutting down but did not exit in time; retry shortly.",
|
|
498
|
+
{ code: "daemon_stopping" }
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function ensureDaemon(options) {
|
|
505
|
+
const dir = options?.dir ?? getDaemonDir();
|
|
506
|
+
let token = options?.token ?? getDaemonTokenFromEnv();
|
|
507
|
+
ensureDaemonDir(dir);
|
|
508
|
+
const socketPath = getDaemonSocketPath(dir);
|
|
509
|
+
assertSocketPathWithinLimit(socketPath);
|
|
510
|
+
if (await isDaemonReachable(socketPath)) {
|
|
511
|
+
token ??= readDaemonTokenFile(dir);
|
|
512
|
+
const pong = await callDaemon("ping", {}, { socketPath, timeoutMs: 2e3, token });
|
|
513
|
+
if (pong?.stopping !== true) {
|
|
514
|
+
return { socketPath, spawned: false };
|
|
515
|
+
}
|
|
516
|
+
await waitForDaemonExit(pong.pid, socketPath);
|
|
517
|
+
}
|
|
518
|
+
const tokenWasGenerated = token === void 0;
|
|
519
|
+
token ??= generateDaemonToken();
|
|
520
|
+
const script = options?.daemonScript ?? resolveDaemonScriptPath();
|
|
521
|
+
const childEnv = {
|
|
522
|
+
...process.env,
|
|
523
|
+
// Pin the socket directory explicitly so parent and child agree even when
|
|
524
|
+
// MCP_STORAGE_DIR is unset (default ~/.mcp-inspector).
|
|
525
|
+
[DAEMON_DIR_ENV]: dir,
|
|
526
|
+
[DAEMON_TOKEN_ENV]: token
|
|
527
|
+
};
|
|
528
|
+
const logPath = getDaemonLogPath(dir);
|
|
529
|
+
let stderrTarget = "ignore";
|
|
530
|
+
try {
|
|
531
|
+
fs.rmSync(logPath, { force: true });
|
|
532
|
+
stderrTarget = fs.openSync(logPath, "ax", 384);
|
|
533
|
+
} catch {
|
|
534
|
+
}
|
|
535
|
+
const child = spawn(process.execPath, [script], {
|
|
536
|
+
detached: true,
|
|
537
|
+
stdio: ["ignore", "ignore", stderrTarget],
|
|
538
|
+
env: childEnv
|
|
539
|
+
});
|
|
540
|
+
child.unref();
|
|
541
|
+
if (typeof stderrTarget === "number") {
|
|
542
|
+
fs.closeSync(stderrTarget);
|
|
543
|
+
}
|
|
544
|
+
await waitForDaemon(socketPath, token, logPath, {
|
|
545
|
+
timeoutMs: options?.readyTimeoutMs,
|
|
546
|
+
rereadTokenDir: tokenWasGenerated ? dir : void 0
|
|
547
|
+
});
|
|
548
|
+
return { socketPath, spawned: true };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ../../core/cli/handlers/servers-list.ts
|
|
552
|
+
function resolveServerListSource(serverOptions = {}) {
|
|
553
|
+
const source = resolveServerSource(withDefaultCatalogPath(serverOptions));
|
|
554
|
+
if (!source) return null;
|
|
555
|
+
return { kind: source.writable ? "catalog" : "config", path: source.path };
|
|
556
|
+
}
|
|
557
|
+
function annotateServerEntriesWithConnections(entries, connections) {
|
|
558
|
+
if (connections.length === 0) return entries;
|
|
559
|
+
const byName = new Map(connections.map((s) => [s.name, s]));
|
|
560
|
+
return entries.map((entry) => {
|
|
561
|
+
const connection = byName.get(entry.name);
|
|
562
|
+
if (!connection) return entry;
|
|
563
|
+
return {
|
|
564
|
+
...entry,
|
|
565
|
+
connection: connection.name,
|
|
566
|
+
...connection.isMru === true ? { isMru: true } : {}
|
|
567
|
+
};
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
var REDACTED = "[redacted]";
|
|
571
|
+
function summarizeServerConfig(config) {
|
|
572
|
+
if (config.type === "sse" || config.type === "streamable-http") {
|
|
573
|
+
return { type: config.type, detail: config.url ?? "" };
|
|
574
|
+
}
|
|
575
|
+
const args = config.args?.length ? ` ${config.args.join(" ")}` : "";
|
|
576
|
+
return { type: "stdio", detail: `${config.command}${args}` };
|
|
577
|
+
}
|
|
578
|
+
async function listServerEntries(serverOptions = {}) {
|
|
579
|
+
const entries = await loadServerEntries({
|
|
580
|
+
...serverOptions,
|
|
581
|
+
secretStore: serverOptions.secretStore ?? new InMemorySecretStore()
|
|
582
|
+
});
|
|
583
|
+
return Object.entries(entries).map(([name, resolved]) => {
|
|
584
|
+
const { type, detail } = summarizeServerConfig(resolved.config);
|
|
585
|
+
return { name, type, detail };
|
|
586
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
587
|
+
}
|
|
588
|
+
async function showServerEntry(serverName, serverOptions = {}) {
|
|
589
|
+
const name = serverName.trim();
|
|
590
|
+
if (!name) {
|
|
591
|
+
throw new Error("servers/show requires a server name.");
|
|
592
|
+
}
|
|
593
|
+
const entries = await loadServerEntries(serverOptions);
|
|
594
|
+
const selected = selectServerEntry(entries, name);
|
|
595
|
+
const { type, detail } = summarizeServerConfig(selected.config);
|
|
596
|
+
const result = {
|
|
597
|
+
name,
|
|
598
|
+
type,
|
|
599
|
+
detail,
|
|
600
|
+
config: sanitizeServerConfig(selected.config)
|
|
601
|
+
};
|
|
602
|
+
if (selected.settings) {
|
|
603
|
+
result.settings = sanitizeServerSettings(selected.settings);
|
|
604
|
+
}
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
function sanitizeServerConfig(config) {
|
|
608
|
+
const out = { ...config };
|
|
609
|
+
if ("env" in config && config.env) {
|
|
610
|
+
out.env = redactStringRecord(config.env);
|
|
611
|
+
}
|
|
612
|
+
if ("requestInit" in config && isPlainObject(config.requestInit)) {
|
|
613
|
+
out.requestInit = sanitizeInitRecord(config.requestInit);
|
|
614
|
+
}
|
|
615
|
+
if ("eventSourceInit" in config && isPlainObject(config.eventSourceInit)) {
|
|
616
|
+
out.eventSourceInit = sanitizeInitRecord(config.eventSourceInit);
|
|
617
|
+
}
|
|
618
|
+
return out;
|
|
619
|
+
}
|
|
620
|
+
function sanitizeServerSettings(settings) {
|
|
621
|
+
const out = {
|
|
622
|
+
...settings,
|
|
623
|
+
headers: (settings.headers ?? []).map((h) => ({
|
|
624
|
+
key: h.key,
|
|
625
|
+
value: isSensitiveHeader(h.key) ? REDACTED : h.value
|
|
626
|
+
})),
|
|
627
|
+
// Metadata values may be any JSON (#1910), so the redaction has to descend:
|
|
628
|
+
// a top-level key that is not itself sensitive can hold a nested one that
|
|
629
|
+
// is. See `redactMetadataValue`.
|
|
630
|
+
metadata: redactMetadataValue(settings.metadata ?? {}),
|
|
631
|
+
env: (settings.env ?? []).map((e) => ({
|
|
632
|
+
key: e.key,
|
|
633
|
+
value: REDACTED
|
|
634
|
+
}))
|
|
635
|
+
};
|
|
636
|
+
if (settings.oauthClientSecret !== void 0) {
|
|
637
|
+
out.oauthClientSecret = REDACTED;
|
|
638
|
+
}
|
|
639
|
+
return out;
|
|
640
|
+
}
|
|
641
|
+
function redactStringRecord(record) {
|
|
642
|
+
return Object.fromEntries(Object.keys(record).map((key) => [key, REDACTED]));
|
|
643
|
+
}
|
|
644
|
+
function isPlainObject(value) {
|
|
645
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
646
|
+
}
|
|
647
|
+
function sanitizeInitRecord(init) {
|
|
648
|
+
const out = { ...init };
|
|
649
|
+
if (isPlainObject(init.headers)) {
|
|
650
|
+
out.headers = Object.fromEntries(
|
|
651
|
+
Object.entries(init.headers).map(([key, value]) => [
|
|
652
|
+
key,
|
|
653
|
+
isSensitiveHeader(key) ? REDACTED : value
|
|
654
|
+
])
|
|
655
|
+
);
|
|
656
|
+
} else if (Array.isArray(init.headers)) {
|
|
657
|
+
out.headers = init.headers.map((entry) => {
|
|
658
|
+
if (Array.isArray(entry) && entry.length >= 2 && typeof entry[0] === "string") {
|
|
659
|
+
const key = entry[0];
|
|
660
|
+
const value = entry[1];
|
|
661
|
+
return [key, isSensitiveHeader(key) ? REDACTED : value];
|
|
662
|
+
}
|
|
663
|
+
return entry;
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
function redactMetadataValue(value) {
|
|
669
|
+
if (Array.isArray(value)) {
|
|
670
|
+
return value.map(redactMetadataValue);
|
|
671
|
+
}
|
|
672
|
+
if (value !== null && typeof value === "object") {
|
|
673
|
+
return Object.fromEntries(
|
|
674
|
+
Object.entries(value).map(([key, member]) => [
|
|
675
|
+
key,
|
|
676
|
+
isSensitiveHeader(key) ? REDACTED : redactMetadataValue(member)
|
|
677
|
+
])
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
return value;
|
|
681
|
+
}
|
|
682
|
+
function isSensitiveHeader(key) {
|
|
683
|
+
const k = key.toLowerCase();
|
|
684
|
+
return k.includes("auth") || k.includes("cookie") || k.includes("secret") || k.includes("token") || k.includes("password") || k.includes("api-key") || k.includes("apikey");
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ../../core/cli/handlers/connect-timeout.ts
|
|
688
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 15e3;
|
|
689
|
+
function withConnectTimeout(settings, connectionTimeout) {
|
|
690
|
+
if (connectionTimeout === void 0) return settings;
|
|
691
|
+
if (settings) return { ...settings, connectionTimeout };
|
|
692
|
+
return {
|
|
693
|
+
headers: [],
|
|
694
|
+
metadata: {},
|
|
695
|
+
env: [],
|
|
696
|
+
connectionTimeout,
|
|
697
|
+
requestTimeout: 0,
|
|
698
|
+
taskTtl: DEFAULT_TASK_TTL_MS,
|
|
699
|
+
maxFetchRequests: DEFAULT_MAX_FETCH_REQUESTS,
|
|
700
|
+
autoRefreshOnListChanged: false,
|
|
701
|
+
paginatedLists: false,
|
|
702
|
+
roots: []
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ../../core/cli/handlers/method-types.ts
|
|
707
|
+
var CONNECTION_RPC_METHODS = [
|
|
708
|
+
"tools/list",
|
|
709
|
+
"tools/call",
|
|
710
|
+
"resources/list",
|
|
711
|
+
"resources/read",
|
|
712
|
+
"resources/templates/list",
|
|
713
|
+
"resources/directory/read",
|
|
714
|
+
"resources/subscribe",
|
|
715
|
+
"resources/unsubscribe",
|
|
716
|
+
"prompts/list",
|
|
717
|
+
"prompts/get",
|
|
718
|
+
"prompts/complete",
|
|
719
|
+
"logging/setLevel",
|
|
720
|
+
"logging/tail",
|
|
721
|
+
"tasks/list",
|
|
722
|
+
"tasks/get",
|
|
723
|
+
"tasks/cancel",
|
|
724
|
+
"tasks/result",
|
|
725
|
+
"tasks/update",
|
|
726
|
+
"roots/list",
|
|
727
|
+
"roots/set",
|
|
728
|
+
"skills/list",
|
|
729
|
+
"skills/get"
|
|
730
|
+
];
|
|
731
|
+
|
|
732
|
+
// src/connection/ema-login-helper.ts
|
|
733
|
+
var EMA_LOGIN_HELPER_COMMAND = "auth/complete-ema-login";
|
|
734
|
+
function emaLoginMarkerKey(issuer) {
|
|
735
|
+
return `ema-idp:${issuer}`;
|
|
736
|
+
}
|
|
737
|
+
async function startPendingEmaLogin(options) {
|
|
738
|
+
const { idp, enabled } = await loadEmaIdpConfig();
|
|
739
|
+
const active = requireIdp(idp, enabled);
|
|
740
|
+
const issuer = normalizeIdpIssuer(active.issuer);
|
|
741
|
+
const storage = new NodeOAuthStorage();
|
|
742
|
+
if (options?.relogin) {
|
|
743
|
+
await clearEmaIdpSession(storage, active.issuer);
|
|
744
|
+
} else if (await getEmaIdpLoginState(storage, active.issuer) === "logged_in") {
|
|
745
|
+
return { issuer, loginState: "logged_in", alreadyLoggedIn: true };
|
|
746
|
+
}
|
|
747
|
+
const authUrl = await obtainPendingUrlForKey(
|
|
748
|
+
emaLoginMarkerKey(issuer),
|
|
749
|
+
EMA_LOGIN_HELPER_COMMAND,
|
|
750
|
+
// The helper re-reads config itself; params document is empty.
|
|
751
|
+
{},
|
|
752
|
+
options
|
|
753
|
+
);
|
|
754
|
+
return {
|
|
755
|
+
issuer,
|
|
756
|
+
loginState: await getEmaIdpLoginState(storage, active.issuer),
|
|
757
|
+
alreadyLoggedIn: false,
|
|
758
|
+
pendingLogin: true,
|
|
759
|
+
authUrl
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
async function runEmaLoginHelper() {
|
|
763
|
+
const emit = (event) => {
|
|
764
|
+
try {
|
|
765
|
+
process.stdout.write(`${JSON.stringify(event)}
|
|
766
|
+
`);
|
|
767
|
+
} catch {
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
process.stdout.on("error", () => {
|
|
771
|
+
});
|
|
772
|
+
let markerPath;
|
|
773
|
+
try {
|
|
774
|
+
const { idp, enabled } = await loadEmaIdpConfig();
|
|
775
|
+
const active = requireIdp(idp, enabled);
|
|
776
|
+
const issuer = normalizeIdpIssuer(active.issuer);
|
|
777
|
+
const storage = new NodeOAuthStorage();
|
|
778
|
+
await runEmaIdpInteractiveFlow(
|
|
779
|
+
active,
|
|
780
|
+
storage,
|
|
781
|
+
new CallbackNavigation(async (url) => {
|
|
782
|
+
markerPath = pendingAuthMarkerPath(emaLoginMarkerKey(issuer));
|
|
783
|
+
writePendingAuthMarker(markerPath, {
|
|
784
|
+
url: url.href,
|
|
785
|
+
pid: process.pid,
|
|
786
|
+
expiresAt: Date.now() + PENDING_AUTH_TTL_MS
|
|
787
|
+
});
|
|
788
|
+
emit({ event: "auth_url", url: url.href });
|
|
789
|
+
})
|
|
790
|
+
);
|
|
791
|
+
emit({ event: "done" });
|
|
792
|
+
} catch (error) {
|
|
793
|
+
emit({
|
|
794
|
+
event: "error",
|
|
795
|
+
message: error instanceof Error ? error.message : String(error)
|
|
796
|
+
});
|
|
797
|
+
throw error;
|
|
798
|
+
} finally {
|
|
799
|
+
if (markerPath !== void 0) removeOwnPendingAuthMarker(markerPath);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/connection/parse-tool-args.ts
|
|
804
|
+
function assertJsonRoundTrips(value, context) {
|
|
805
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
806
|
+
throw new Error(
|
|
807
|
+
`Invalid ${context}: ${value} has no JSON representation (it would silently reach the tool as null).`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
if (Array.isArray(value)) {
|
|
811
|
+
for (const item of value) assertJsonRoundTrips(item, context);
|
|
812
|
+
} else if (value !== null && typeof value === "object") {
|
|
813
|
+
for (const item of Object.values(value)) {
|
|
814
|
+
assertJsonRoundTrips(item, context);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function parseToolCallPositionals(args) {
|
|
819
|
+
if (args.length === 0) return {};
|
|
820
|
+
const first = args[0];
|
|
821
|
+
if (first.startsWith("{") || first.startsWith("[")) {
|
|
822
|
+
if (args.length > 1) {
|
|
823
|
+
throw new Error(
|
|
824
|
+
"When using inline JSON, only one argument is allowed after the tool name."
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
let parsed;
|
|
828
|
+
try {
|
|
829
|
+
parsed = JSON.parse(first);
|
|
830
|
+
} catch (e) {
|
|
831
|
+
throw new Error(
|
|
832
|
+
`Invalid JSON tool arguments: ${e instanceof Error ? e.message : String(e)}`,
|
|
833
|
+
{ cause: e }
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
837
|
+
throw new Error("Inline JSON tool arguments must be a JSON object.");
|
|
838
|
+
}
|
|
839
|
+
assertJsonRoundTrips(parsed, "inline JSON tool arguments");
|
|
840
|
+
return parsed;
|
|
841
|
+
}
|
|
842
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
843
|
+
for (const pair of args) {
|
|
844
|
+
const sep = pair.indexOf(":=");
|
|
845
|
+
if (sep === -1) {
|
|
846
|
+
throw new Error(
|
|
847
|
+
`Invalid tool argument "${pair}". Use key:=value pairs or a JSON object.
|
|
848
|
+
Examples: message:=hello count:=10 '{"message":"hello"}'`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const key = pair.slice(0, sep);
|
|
852
|
+
const rawValue = pair.slice(sep + 2);
|
|
853
|
+
if (!key) {
|
|
854
|
+
throw new Error(
|
|
855
|
+
`Invalid tool argument "${pair}" \u2014 missing key before :=`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
out[key] = autoParseValue(rawValue, `tool argument "${pair}"`);
|
|
859
|
+
}
|
|
860
|
+
return out;
|
|
861
|
+
}
|
|
862
|
+
function autoParseValue(raw, context) {
|
|
863
|
+
let parsed;
|
|
864
|
+
try {
|
|
865
|
+
parsed = JSON.parse(raw);
|
|
866
|
+
} catch {
|
|
867
|
+
return raw;
|
|
868
|
+
}
|
|
869
|
+
assertJsonRoundTrips(parsed, context);
|
|
870
|
+
return parsed;
|
|
871
|
+
}
|
|
872
|
+
function resolveToolCallArgs(input) {
|
|
873
|
+
const flagArgs = input.toolArgFlag ?? {};
|
|
874
|
+
const hasFlagArgs = Object.keys(flagArgs).length > 0;
|
|
875
|
+
const hasJson = input.toolArgsJson !== void 0;
|
|
876
|
+
let toolName = input.toolNameFlag ?? input.toolNamePos;
|
|
877
|
+
let positionals = [...input.toolArgsPos ?? []];
|
|
878
|
+
if (input.toolNameFlag && input.toolNamePos) {
|
|
879
|
+
positionals = [input.toolNamePos, ...positionals];
|
|
880
|
+
toolName = input.toolNameFlag;
|
|
881
|
+
}
|
|
882
|
+
const hasPositionals = positionals.length > 0;
|
|
883
|
+
const styles = [hasPositionals, hasFlagArgs, hasJson].filter(Boolean).length;
|
|
884
|
+
if (styles > 1) {
|
|
885
|
+
throw new Error(
|
|
886
|
+
"Tool arguments must use one style: key:=value / JSON positionals, --tool-arg, or --tool-args-json."
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
if (hasJson) {
|
|
890
|
+
return {
|
|
891
|
+
toolName,
|
|
892
|
+
toolArg: parseJsonObject(input.toolArgsJson, "--tool-args-json")
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
if (hasPositionals) {
|
|
896
|
+
return { toolName, toolArg: parseToolCallPositionals(positionals) };
|
|
897
|
+
}
|
|
898
|
+
return { toolName, toolArg: flagArgs };
|
|
899
|
+
}
|
|
900
|
+
function parseJsonObject(raw, flag) {
|
|
901
|
+
let parsed;
|
|
902
|
+
try {
|
|
903
|
+
parsed = JSON.parse(raw);
|
|
904
|
+
} catch (e) {
|
|
905
|
+
throw new Error(
|
|
906
|
+
`${flag} is not valid JSON: ${e instanceof Error ? e.message : String(e)}`,
|
|
907
|
+
{ cause: e }
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
911
|
+
throw new Error(`${flag} must be a JSON object.`);
|
|
912
|
+
}
|
|
913
|
+
assertJsonRoundTrips(parsed, flag);
|
|
914
|
+
return parsed;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// src/connection/resolve-command.ts
|
|
918
|
+
import fs2 from "fs";
|
|
919
|
+
import path3 from "path";
|
|
920
|
+
function resolveCommandPath(command, env = process.env) {
|
|
921
|
+
if (!command || command.includes("/") || command.includes(path3.sep)) {
|
|
922
|
+
return command;
|
|
923
|
+
}
|
|
924
|
+
const pathVar = env.PATH ?? "";
|
|
925
|
+
const extensions = process.platform === "win32" ? (
|
|
926
|
+
// cmd.exe-like: an already-suffixed name ("node.exe") is tried as-is
|
|
927
|
+
// before PATHEXT variants — otherwise only "node.exe.EXE" etc. would
|
|
928
|
+
// be searched and resolution would silently fall to the daemon's PATH.
|
|
929
|
+
[
|
|
930
|
+
...path3.extname(command) ? [""] : [],
|
|
931
|
+
...(env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";")
|
|
932
|
+
]
|
|
933
|
+
) : [""];
|
|
934
|
+
for (const dir of pathVar.split(path3.delimiter)) {
|
|
935
|
+
for (const ext of extensions) {
|
|
936
|
+
const candidate = path3.resolve(dir === "" ? "." : dir, command + ext);
|
|
937
|
+
try {
|
|
938
|
+
const stat = fs2.statSync(candidate);
|
|
939
|
+
if (!stat.isFile()) continue;
|
|
940
|
+
fs2.accessSync(candidate, fs2.constants.X_OK);
|
|
941
|
+
return candidate;
|
|
942
|
+
} catch {
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return command;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/connection/form-schema.ts
|
|
950
|
+
function isRecord(value) {
|
|
951
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
952
|
+
}
|
|
953
|
+
function parseChoicesFromEnum(value) {
|
|
954
|
+
if (!Array.isArray(value) || value.length === 0 || value.some((v) => typeof v !== "string")) {
|
|
955
|
+
return void 0;
|
|
956
|
+
}
|
|
957
|
+
return value.map((v) => ({ value: v, label: v }));
|
|
958
|
+
}
|
|
959
|
+
function parseChoicesFromOneOf(value) {
|
|
960
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
961
|
+
const choices = [];
|
|
962
|
+
for (const entry of value) {
|
|
963
|
+
if (!isRecord(entry) || typeof entry.const !== "string") return void 0;
|
|
964
|
+
choices.push({
|
|
965
|
+
value: entry.const,
|
|
966
|
+
label: typeof entry.title === "string" ? entry.title : entry.const
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
return choices;
|
|
970
|
+
}
|
|
971
|
+
function isValidCount(value) {
|
|
972
|
+
return value === void 0 || Number.isInteger(value) && value >= 0;
|
|
973
|
+
}
|
|
974
|
+
function codePointLength(value) {
|
|
975
|
+
return [...value].length;
|
|
976
|
+
}
|
|
977
|
+
function isConsistent(field) {
|
|
978
|
+
switch (field.kind) {
|
|
979
|
+
case "boolean":
|
|
980
|
+
return true;
|
|
981
|
+
case "number":
|
|
982
|
+
if (field.minimum !== void 0 && field.maximum !== void 0 && field.minimum > field.maximum) {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
if (field.default !== void 0) {
|
|
986
|
+
if (field.integer && !Number.isInteger(field.default)) return false;
|
|
987
|
+
if (field.minimum !== void 0 && field.default < field.minimum)
|
|
988
|
+
return false;
|
|
989
|
+
if (field.maximum !== void 0 && field.default > field.maximum)
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
return true;
|
|
993
|
+
case "string":
|
|
994
|
+
if (!isValidCount(field.minLength) || !isValidCount(field.maxLength)) {
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
if (field.minLength !== void 0 && field.maxLength !== void 0 && field.minLength > field.maxLength) {
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
if (field.default !== void 0) {
|
|
1001
|
+
if (field.minLength !== void 0 && codePointLength(field.default) < field.minLength) {
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
if (field.maxLength !== void 0 && codePointLength(field.default) > field.maxLength) {
|
|
1005
|
+
return false;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return true;
|
|
1009
|
+
case "enum":
|
|
1010
|
+
return field.default === void 0 || field.choices.some((c) => c.value === field.default);
|
|
1011
|
+
case "multiselect": {
|
|
1012
|
+
if (!isValidCount(field.minItems) || !isValidCount(field.maxItems)) {
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
if (field.minItems !== void 0 && field.maxItems !== void 0 && field.minItems > field.maxItems) {
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
if (field.minItems !== void 0 && field.minItems > field.choices.length) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
const def = field.default;
|
|
1022
|
+
if (def !== void 0) {
|
|
1023
|
+
if (!def.every((v) => field.choices.some((c) => c.value === v))) {
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
if (field.minItems !== void 0 && def.length < field.minItems)
|
|
1027
|
+
return false;
|
|
1028
|
+
if (field.maxItems !== void 0 && def.length > field.maxItems)
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
return true;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
function parseField(prop) {
|
|
1036
|
+
const parsed = parseFieldShape(prop);
|
|
1037
|
+
if (!parsed || !isConsistent(parsed)) return null;
|
|
1038
|
+
return parsed;
|
|
1039
|
+
}
|
|
1040
|
+
function parseFieldShape(prop) {
|
|
1041
|
+
if (!isRecord(prop)) return null;
|
|
1042
|
+
const type = prop.type;
|
|
1043
|
+
if (type === "boolean") {
|
|
1044
|
+
return {
|
|
1045
|
+
kind: "boolean",
|
|
1046
|
+
default: typeof prop.default === "boolean" ? prop.default : void 0
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
if (type === "number" || type === "integer") {
|
|
1050
|
+
return {
|
|
1051
|
+
kind: "number",
|
|
1052
|
+
integer: type === "integer",
|
|
1053
|
+
minimum: typeof prop.minimum === "number" ? prop.minimum : void 0,
|
|
1054
|
+
maximum: typeof prop.maximum === "number" ? prop.maximum : void 0,
|
|
1055
|
+
default: typeof prop.default === "number" ? prop.default : void 0
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
if (type === "string") {
|
|
1059
|
+
if (prop.enum !== void 0) {
|
|
1060
|
+
const enumChoices = parseChoicesFromEnum(prop.enum);
|
|
1061
|
+
if (!enumChoices) return null;
|
|
1062
|
+
return {
|
|
1063
|
+
kind: "enum",
|
|
1064
|
+
choices: enumChoices,
|
|
1065
|
+
default: typeof prop.default === "string" ? prop.default : void 0
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
if (prop.oneOf !== void 0) {
|
|
1069
|
+
const oneOfChoices = parseChoicesFromOneOf(prop.oneOf);
|
|
1070
|
+
if (!oneOfChoices) return null;
|
|
1071
|
+
return {
|
|
1072
|
+
kind: "enum",
|
|
1073
|
+
choices: oneOfChoices,
|
|
1074
|
+
default: typeof prop.default === "string" ? prop.default : void 0
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
return {
|
|
1078
|
+
kind: "string",
|
|
1079
|
+
minLength: typeof prop.minLength === "number" ? prop.minLength : void 0,
|
|
1080
|
+
maxLength: typeof prop.maxLength === "number" ? prop.maxLength : void 0,
|
|
1081
|
+
format: typeof prop.format === "string" ? prop.format : void 0,
|
|
1082
|
+
default: typeof prop.default === "string" ? prop.default : void 0
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
if (type === "array") {
|
|
1086
|
+
const items = prop.items;
|
|
1087
|
+
if (!isRecord(items)) return null;
|
|
1088
|
+
const choices = parseChoicesFromEnum(items.enum) ?? parseChoicesFromOneOf(items.anyOf);
|
|
1089
|
+
if (!choices) return null;
|
|
1090
|
+
const defaultValue = Array.isArray(prop.default) && prop.default.every((v) => typeof v === "string") ? prop.default : void 0;
|
|
1091
|
+
return {
|
|
1092
|
+
kind: "multiselect",
|
|
1093
|
+
choices,
|
|
1094
|
+
minItems: typeof prop.minItems === "number" ? prop.minItems : void 0,
|
|
1095
|
+
maxItems: typeof prop.maxItems === "number" ? prop.maxItems : void 0,
|
|
1096
|
+
default: defaultValue
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
function parseFormSchema(schema) {
|
|
1102
|
+
if (!isRecord(schema)) return null;
|
|
1103
|
+
const properties = schema.properties;
|
|
1104
|
+
if (!isRecord(properties)) return null;
|
|
1105
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((v) => typeof v === "string") : [];
|
|
1106
|
+
const fields = [];
|
|
1107
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
1108
|
+
const parsed = parseField(prop);
|
|
1109
|
+
if (!parsed) return null;
|
|
1110
|
+
const title = isRecord(prop) && typeof prop.title === "string" ? prop.title : name;
|
|
1111
|
+
const description = isRecord(prop) && typeof prop.description === "string" ? prop.description : void 0;
|
|
1112
|
+
fields.push({
|
|
1113
|
+
name,
|
|
1114
|
+
required: required.includes(name),
|
|
1115
|
+
title,
|
|
1116
|
+
description,
|
|
1117
|
+
...parsed
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
return fields;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/connection/format-human.ts
|
|
1124
|
+
function asArray(value) {
|
|
1125
|
+
return Array.isArray(value) ? value : [];
|
|
1126
|
+
}
|
|
1127
|
+
function shortType(schema) {
|
|
1128
|
+
if (!schema || typeof schema !== "object") return "any";
|
|
1129
|
+
const s = schema;
|
|
1130
|
+
const t = s.type;
|
|
1131
|
+
if (t === "array") {
|
|
1132
|
+
if (s.items) return `[${shortType(s.items)}]`;
|
|
1133
|
+
return "[any]";
|
|
1134
|
+
}
|
|
1135
|
+
if (Array.isArray(t)) {
|
|
1136
|
+
const filtered = t.filter((x) => x !== "null");
|
|
1137
|
+
if (filtered.length === 1) return shortTypeName(String(filtered[0]));
|
|
1138
|
+
return filtered.map((x) => shortTypeName(String(x))).join(" | ");
|
|
1139
|
+
}
|
|
1140
|
+
if (Array.isArray(s.enum)) return "enum";
|
|
1141
|
+
if (typeof t === "string") return shortTypeName(t);
|
|
1142
|
+
return "any";
|
|
1143
|
+
}
|
|
1144
|
+
function shortTypeName(type) {
|
|
1145
|
+
const map = {
|
|
1146
|
+
string: "str",
|
|
1147
|
+
number: "num",
|
|
1148
|
+
integer: "int",
|
|
1149
|
+
boolean: "bool",
|
|
1150
|
+
object: "obj",
|
|
1151
|
+
array: "[any]"
|
|
1152
|
+
};
|
|
1153
|
+
return map[type] ?? type;
|
|
1154
|
+
}
|
|
1155
|
+
function formatToolParamsInline(schema) {
|
|
1156
|
+
if (!schema || typeof schema !== "object") return "()";
|
|
1157
|
+
const s = schema;
|
|
1158
|
+
const properties = s.properties;
|
|
1159
|
+
if (!properties || Object.keys(properties).length === 0) return "()";
|
|
1160
|
+
const required = new Set(asArray(s.required));
|
|
1161
|
+
const names = Object.keys(properties);
|
|
1162
|
+
const ordered = [
|
|
1163
|
+
...names.filter((n) => required.has(n)),
|
|
1164
|
+
...names.filter((n) => !required.has(n))
|
|
1165
|
+
];
|
|
1166
|
+
const shown = ordered.slice(0, 3);
|
|
1167
|
+
const hidden = ordered.length - shown.length;
|
|
1168
|
+
const parts = shown.map((name) => {
|
|
1169
|
+
const typeStr = shortType(properties[name]);
|
|
1170
|
+
return required.has(name) ? `${name}:${typeStr}` : `${name}?:${typeStr}`;
|
|
1171
|
+
});
|
|
1172
|
+
if (hidden > 0) parts.push("\u2026");
|
|
1173
|
+
return `(${parts.join(", ")})`;
|
|
1174
|
+
}
|
|
1175
|
+
function toolHints(tool) {
|
|
1176
|
+
const ann = tool.annotations;
|
|
1177
|
+
if (!ann) return void 0;
|
|
1178
|
+
const hints = [];
|
|
1179
|
+
if (ann.readOnlyHint === true) hints.push("read-only");
|
|
1180
|
+
if (ann.destructiveHint === true) hints.push("destructive");
|
|
1181
|
+
if (ann.idempotentHint === true) hints.push("idempotent");
|
|
1182
|
+
if (ann.openWorldHint === true) hints.push("open-world");
|
|
1183
|
+
return hints.length > 0 ? hints.join(", ") : void 0;
|
|
1184
|
+
}
|
|
1185
|
+
function code(style, name) {
|
|
1186
|
+
return `\`${style.bold(name)}\``;
|
|
1187
|
+
}
|
|
1188
|
+
function heading(style, text) {
|
|
1189
|
+
return style.bold(text);
|
|
1190
|
+
}
|
|
1191
|
+
function descSuffix(style, description) {
|
|
1192
|
+
if (typeof description !== "string" || !description.trim()) return "";
|
|
1193
|
+
return style.dim(` \u2014 ${description.trim().split("\n")[0]}`);
|
|
1194
|
+
}
|
|
1195
|
+
function formatUri(style, uri) {
|
|
1196
|
+
if (!uri) return uri;
|
|
1197
|
+
if (isSafeLinkTarget(uri)) return style.link(uri);
|
|
1198
|
+
return style.cyan(uri);
|
|
1199
|
+
}
|
|
1200
|
+
function colorLevel(style, level) {
|
|
1201
|
+
switch (level) {
|
|
1202
|
+
case "error":
|
|
1203
|
+
case "critical":
|
|
1204
|
+
case "alert":
|
|
1205
|
+
case "emergency":
|
|
1206
|
+
return style.red(level);
|
|
1207
|
+
case "warning":
|
|
1208
|
+
return style.yellow(level);
|
|
1209
|
+
case "debug":
|
|
1210
|
+
case "notice":
|
|
1211
|
+
return style.dim(level);
|
|
1212
|
+
default:
|
|
1213
|
+
return style.cyan(level);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
function formatToolsHuman(tools, style = PLAIN) {
|
|
1217
|
+
const lines = [heading(style, `Tools (${tools.length}):`)];
|
|
1218
|
+
for (const raw of tools) {
|
|
1219
|
+
const tool = raw;
|
|
1220
|
+
const name = String(tool.name ?? "?");
|
|
1221
|
+
const params = formatToolParamsInline(tool.inputSchema);
|
|
1222
|
+
const hints = toolHints(tool);
|
|
1223
|
+
const hintSuffix = hints ? style.dim(` [${hints}]`) : "";
|
|
1224
|
+
lines.push(
|
|
1225
|
+
`* \`${style.bold(name)}${style.cyan(params)}\`${hintSuffix}${descSuffix(style, tool.description)}`
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
if (tools.length === 0) lines.push(style.dim("(none)"));
|
|
1229
|
+
return lines.join("\n");
|
|
1230
|
+
}
|
|
1231
|
+
function formatResourcesHuman(resources, style = PLAIN) {
|
|
1232
|
+
const lines = [heading(style, `Resources (${resources.length}):`)];
|
|
1233
|
+
for (const raw of resources) {
|
|
1234
|
+
const r = raw;
|
|
1235
|
+
const name = typeof r.name === "string" ? r.name : String(r.uri ?? "?");
|
|
1236
|
+
const uri = typeof r.uri === "string" ? r.uri : "";
|
|
1237
|
+
const uriPart = uri ? ` (${formatUri(style, uri)})` : "";
|
|
1238
|
+
lines.push(
|
|
1239
|
+
`* ${code(style, name)}${uriPart}${descSuffix(style, r.description)}`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
if (resources.length === 0) lines.push(style.dim("(none)"));
|
|
1243
|
+
return lines.join("\n");
|
|
1244
|
+
}
|
|
1245
|
+
function formatResourceTemplatesHuman(templates, style = PLAIN) {
|
|
1246
|
+
const lines = [heading(style, `Resource templates (${templates.length}):`)];
|
|
1247
|
+
for (const raw of templates) {
|
|
1248
|
+
const t = raw;
|
|
1249
|
+
const name = String(t.name ?? "?");
|
|
1250
|
+
const uri = typeof t.uriTemplate === "string" ? t.uriTemplate : "";
|
|
1251
|
+
const uriPart = uri ? ` (${formatUri(style, uri)})` : "";
|
|
1252
|
+
lines.push(
|
|
1253
|
+
`* ${code(style, name)}${uriPart}${descSuffix(style, t.description)}`
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
if (templates.length === 0) lines.push(style.dim("(none)"));
|
|
1257
|
+
return lines.join("\n");
|
|
1258
|
+
}
|
|
1259
|
+
function formatPromptsHuman(prompts, style = PLAIN) {
|
|
1260
|
+
const lines = [heading(style, `Prompts (${prompts.length}):`)];
|
|
1261
|
+
for (const raw of prompts) {
|
|
1262
|
+
const p = raw;
|
|
1263
|
+
const name = String(p.name ?? "?");
|
|
1264
|
+
lines.push(`* ${code(style, name)}${descSuffix(style, p.description)}`);
|
|
1265
|
+
}
|
|
1266
|
+
if (prompts.length === 0) lines.push(style.dim("(none)"));
|
|
1267
|
+
return lines.join("\n");
|
|
1268
|
+
}
|
|
1269
|
+
function formatContentBlock(block, style) {
|
|
1270
|
+
const lines = [];
|
|
1271
|
+
switch (block.type) {
|
|
1272
|
+
case "text":
|
|
1273
|
+
lines.push("````");
|
|
1274
|
+
lines.push(String(block.text ?? ""));
|
|
1275
|
+
lines.push("````");
|
|
1276
|
+
break;
|
|
1277
|
+
case "resource_link":
|
|
1278
|
+
lines.push(heading(style, "Resource link"));
|
|
1279
|
+
lines.push(`* URI: ${formatUri(style, String(block.uri ?? ""))}`);
|
|
1280
|
+
if (block.name) lines.push(`* Name: ${String(block.name)}`);
|
|
1281
|
+
if (block.description)
|
|
1282
|
+
lines.push(`* Description: ${String(block.description)}`);
|
|
1283
|
+
if (block.mimeType) lines.push(`* MIME type: ${String(block.mimeType)}`);
|
|
1284
|
+
break;
|
|
1285
|
+
case "image":
|
|
1286
|
+
lines.push(
|
|
1287
|
+
style.dim(
|
|
1288
|
+
`[Image: ${String(block.mimeType ?? "unknown")}${typeof block.data === "string" ? `, ${block.data.length} chars base64` : ""}]`
|
|
1289
|
+
)
|
|
1290
|
+
);
|
|
1291
|
+
break;
|
|
1292
|
+
case "audio":
|
|
1293
|
+
lines.push(
|
|
1294
|
+
style.dim(
|
|
1295
|
+
`[Audio: ${String(block.mimeType ?? "unknown")}${typeof block.data === "string" ? `, ${block.data.length} chars base64` : ""}]`
|
|
1296
|
+
)
|
|
1297
|
+
);
|
|
1298
|
+
break;
|
|
1299
|
+
case "resource": {
|
|
1300
|
+
lines.push(heading(style, "Embedded resource"));
|
|
1301
|
+
const res = block.resource;
|
|
1302
|
+
if (res) {
|
|
1303
|
+
lines.push(`* URI: ${formatUri(style, String(res.uri ?? ""))}`);
|
|
1304
|
+
if (res.mimeType) lines.push(`* MIME type: ${String(res.mimeType)}`);
|
|
1305
|
+
if (typeof res.text === "string") {
|
|
1306
|
+
lines.push("````");
|
|
1307
|
+
lines.push(res.text);
|
|
1308
|
+
lines.push("````");
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
default:
|
|
1314
|
+
lines.push(JSON.stringify(block, null, 2));
|
|
1315
|
+
}
|
|
1316
|
+
return lines;
|
|
1317
|
+
}
|
|
1318
|
+
function findDuplicateTextBlocks(content, structuredContent) {
|
|
1319
|
+
const dupes = /* @__PURE__ */ new Set();
|
|
1320
|
+
const canonical = JSON.stringify(structuredContent);
|
|
1321
|
+
for (let i = 0; i < content.length; i++) {
|
|
1322
|
+
const block = content[i];
|
|
1323
|
+
if (!block || block.type !== "text" || typeof block.text !== "string")
|
|
1324
|
+
continue;
|
|
1325
|
+
try {
|
|
1326
|
+
const parsed = JSON.parse(block.text.trim());
|
|
1327
|
+
if (JSON.stringify(parsed) === canonical) dupes.add(i);
|
|
1328
|
+
} catch {
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return dupes;
|
|
1332
|
+
}
|
|
1333
|
+
function formatCallToolResultHuman(result, style = PLAIN) {
|
|
1334
|
+
const lines = [];
|
|
1335
|
+
if (result.isError === true) {
|
|
1336
|
+
lines.push(style.red(heading(style, "Tool error:")));
|
|
1337
|
+
}
|
|
1338
|
+
const sc = result.structuredContent;
|
|
1339
|
+
const hasStructuredContent = !!sc && Object.keys(sc).length > 0;
|
|
1340
|
+
const content = asArray(result.content);
|
|
1341
|
+
const skipIndices = hasStructuredContent ? findDuplicateTextBlocks(content, sc) : /* @__PURE__ */ new Set();
|
|
1342
|
+
const visible = content.filter((_, i) => !skipIndices.has(i));
|
|
1343
|
+
if (visible.length > 0) {
|
|
1344
|
+
lines.push(heading(style, "Content:"));
|
|
1345
|
+
for (let i = 0; i < visible.length; i++) {
|
|
1346
|
+
if (i > 0) lines.push("");
|
|
1347
|
+
lines.push(...formatContentBlock(visible[i], style));
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (hasStructuredContent) {
|
|
1351
|
+
if (lines.length > 0) lines.push("");
|
|
1352
|
+
lines.push(heading(style, "Structured content:"));
|
|
1353
|
+
lines.push(JSON.stringify(sc, null, 2));
|
|
1354
|
+
}
|
|
1355
|
+
const meta = result._meta;
|
|
1356
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
1357
|
+
if (lines.length > 0) lines.push("");
|
|
1358
|
+
lines.push(style.dim("Metadata:"));
|
|
1359
|
+
lines.push(style.dim(JSON.stringify(meta, null, 2)));
|
|
1360
|
+
}
|
|
1361
|
+
if (lines.length === 0) return style.dim("(no content)");
|
|
1362
|
+
return lines.join("\n");
|
|
1363
|
+
}
|
|
1364
|
+
function formatResourceReadHuman(result, style = PLAIN) {
|
|
1365
|
+
const contents = asArray(result.contents);
|
|
1366
|
+
if (contents.length === 0) return style.dim("(empty resource)");
|
|
1367
|
+
const lines = [
|
|
1368
|
+
heading(style, `Resource contents (${contents.length}):`)
|
|
1369
|
+
];
|
|
1370
|
+
for (const c of contents) {
|
|
1371
|
+
lines.push("");
|
|
1372
|
+
lines.push(`URI: ${formatUri(style, String(c.uri ?? ""))}`);
|
|
1373
|
+
if (c.mimeType) lines.push(style.dim(`MIME: ${String(c.mimeType)}`));
|
|
1374
|
+
if (typeof c.text === "string") {
|
|
1375
|
+
lines.push("````");
|
|
1376
|
+
lines.push(c.text);
|
|
1377
|
+
lines.push("````");
|
|
1378
|
+
} else if (typeof c.blob === "string") {
|
|
1379
|
+
lines.push(style.dim(`[Blob: ${c.blob.length} chars base64]`));
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return lines.join("\n");
|
|
1383
|
+
}
|
|
1384
|
+
function formatPromptResultHuman(result, style = PLAIN) {
|
|
1385
|
+
const description = typeof result.description === "string" ? result.description : void 0;
|
|
1386
|
+
const messages = asArray(result.messages);
|
|
1387
|
+
const lines = [];
|
|
1388
|
+
if (description) {
|
|
1389
|
+
lines.push(style.dim(description));
|
|
1390
|
+
lines.push("");
|
|
1391
|
+
}
|
|
1392
|
+
lines.push(heading(style, `Messages (${messages.length}):`));
|
|
1393
|
+
for (const msg of messages) {
|
|
1394
|
+
const role = String(msg.role ?? "?");
|
|
1395
|
+
lines.push("");
|
|
1396
|
+
lines.push(style.cyan(`[${role}]`));
|
|
1397
|
+
const content = msg.content;
|
|
1398
|
+
if (typeof content === "string") {
|
|
1399
|
+
lines.push("````");
|
|
1400
|
+
lines.push(content);
|
|
1401
|
+
lines.push("````");
|
|
1402
|
+
} else if (content && typeof content === "object") {
|
|
1403
|
+
if (Array.isArray(content)) {
|
|
1404
|
+
for (const block of content) {
|
|
1405
|
+
lines.push(...formatContentBlock(block, style));
|
|
1406
|
+
}
|
|
1407
|
+
} else {
|
|
1408
|
+
lines.push(...formatContentBlock(content, style));
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (messages.length === 0 && !description) return style.dim("(empty prompt)");
|
|
1413
|
+
return lines.join("\n");
|
|
1414
|
+
}
|
|
1415
|
+
function formatCompletionsHuman(result, style = PLAIN) {
|
|
1416
|
+
const values = asArray(result.values);
|
|
1417
|
+
const lines = [heading(style, `Completions (${values.length}):`)];
|
|
1418
|
+
for (const v of values) lines.push(`* ${v}`);
|
|
1419
|
+
if (values.length === 0) lines.push(style.dim("(none)"));
|
|
1420
|
+
if (result.hasMore === true) lines.push(style.dim("(more available)"));
|
|
1421
|
+
return lines.join("\n");
|
|
1422
|
+
}
|
|
1423
|
+
function formatTasksHuman(tasks, style = PLAIN) {
|
|
1424
|
+
const lines = [heading(style, `Tasks (${tasks.length}):`)];
|
|
1425
|
+
for (const raw of tasks) {
|
|
1426
|
+
const t = raw;
|
|
1427
|
+
const id = String(t.taskId ?? t.id ?? "?");
|
|
1428
|
+
const status = String(t.status ?? "?");
|
|
1429
|
+
const msg = typeof t.statusMessage === "string" ? style.dim(` \u2014 ${t.statusMessage}`) : "";
|
|
1430
|
+
lines.push(`* ${code(style, id)} ${status}${msg}`);
|
|
1431
|
+
}
|
|
1432
|
+
if (tasks.length === 0) lines.push(style.dim("(none)"));
|
|
1433
|
+
return lines.join("\n");
|
|
1434
|
+
}
|
|
1435
|
+
function formatTaskHuman(task, style = PLAIN) {
|
|
1436
|
+
const t = task ?? {};
|
|
1437
|
+
const lines = [
|
|
1438
|
+
`${heading(style, "Task:")} ${code(style, String(t.taskId ?? t.id ?? "?"))}`,
|
|
1439
|
+
`Status: ${String(t.status ?? "?")}`
|
|
1440
|
+
];
|
|
1441
|
+
if (typeof t.statusMessage === "string") {
|
|
1442
|
+
lines.push(`Message: ${t.statusMessage}`);
|
|
1443
|
+
}
|
|
1444
|
+
if (t.createdAt) lines.push(style.dim(`Created: ${String(t.createdAt)}`));
|
|
1445
|
+
if (t.lastUpdatedAt)
|
|
1446
|
+
lines.push(style.dim(`Updated: ${String(t.lastUpdatedAt)}`));
|
|
1447
|
+
return lines.join("\n");
|
|
1448
|
+
}
|
|
1449
|
+
function formatInitializeHuman(result, style = PLAIN) {
|
|
1450
|
+
const info = result.serverInfo ?? {};
|
|
1451
|
+
const lines = [
|
|
1452
|
+
`${heading(style, "Server:")} ${style.bold(String(info.name ?? "(unknown)"))}${info.version ? style.dim(` v${String(info.version)}`) : ""}`
|
|
1453
|
+
];
|
|
1454
|
+
if (result.protocolVersion) {
|
|
1455
|
+
lines.push(`Protocol: ${String(result.protocolVersion)}`);
|
|
1456
|
+
}
|
|
1457
|
+
if (typeof result.instructions === "string" && result.instructions.trim()) {
|
|
1458
|
+
lines.push("");
|
|
1459
|
+
lines.push(heading(style, "Instructions:"));
|
|
1460
|
+
lines.push(result.instructions.trim());
|
|
1461
|
+
}
|
|
1462
|
+
const caps = result.capabilities;
|
|
1463
|
+
if (caps && typeof caps === "object") {
|
|
1464
|
+
const keys = Object.keys(caps);
|
|
1465
|
+
if (keys.length > 0) {
|
|
1466
|
+
lines.push("");
|
|
1467
|
+
lines.push(`${heading(style, "Capabilities:")} ${keys.join(", ")}`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return lines.join("\n");
|
|
1471
|
+
}
|
|
1472
|
+
function formatRootsHuman(roots, style = PLAIN) {
|
|
1473
|
+
const lines = [heading(style, `Roots (${roots.length}):`)];
|
|
1474
|
+
for (const raw of roots) {
|
|
1475
|
+
const r = raw;
|
|
1476
|
+
const name = typeof r.name === "string" ? style.dim(` (${r.name})`) : "";
|
|
1477
|
+
lines.push(`* ${formatUri(style, String(r.uri ?? "?"))}${name}`);
|
|
1478
|
+
}
|
|
1479
|
+
if (roots.length === 0) lines.push(style.dim("(none)"));
|
|
1480
|
+
return lines.join("\n");
|
|
1481
|
+
}
|
|
1482
|
+
function formatAuthListHuman(list, style = PLAIN) {
|
|
1483
|
+
const servers = Array.isArray(list.servers) ? list.servers : [];
|
|
1484
|
+
const lines = [
|
|
1485
|
+
heading(style, `Stored auth (${servers.length}):`),
|
|
1486
|
+
style.dim(String(list.oauthStatePath ?? ""))
|
|
1487
|
+
];
|
|
1488
|
+
for (const raw of servers) {
|
|
1489
|
+
const s = raw;
|
|
1490
|
+
const flags = [];
|
|
1491
|
+
if (s.hasTokens === true) flags.push("tokens");
|
|
1492
|
+
if (s.hasRefreshToken === true) flags.push("refresh");
|
|
1493
|
+
const flagText = flags.length > 0 ? style.dim(` (${flags.join(", ")})`) : style.dim(" (no tokens)");
|
|
1494
|
+
if (s.idp === true && typeof s.issuer === "string") {
|
|
1495
|
+
lines.push(
|
|
1496
|
+
`* ${code(style, s.issuer)}${flagText}${style.dim(
|
|
1497
|
+
" enterprise IdP login"
|
|
1498
|
+
)}`
|
|
1499
|
+
);
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
const knownAs = Array.isArray(s.knownAs) ? s.knownAs.map((n) => String(n)) : [];
|
|
1503
|
+
const nameText = knownAs.length > 0 ? style.dim(` known as: ${knownAs.join(", ")}`) : style.dim(" (no local name)");
|
|
1504
|
+
const liveText = s.live === true ? ` ${style.green("\u25CF live")}` : "";
|
|
1505
|
+
lines.push(
|
|
1506
|
+
`* ${code(style, String(s.url))}${flagText}${nameText}${liveText}`
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
if (servers.length === 0) lines.push(style.dim("(none)"));
|
|
1510
|
+
return lines.join("\n");
|
|
1511
|
+
}
|
|
1512
|
+
function formatEmaStatusHuman(status, style = PLAIN) {
|
|
1513
|
+
const lines = [heading(style, "EMA (enterprise-managed auth):")];
|
|
1514
|
+
lines.push(style.dim(String(status.clientConfigPath ?? "")));
|
|
1515
|
+
if (status.configured !== true) {
|
|
1516
|
+
lines.push(
|
|
1517
|
+
"IdP: " + style.dim(
|
|
1518
|
+
"(not configured \u2014 set enterpriseManagedAuth in client.json or the web Inspector's Client Settings)"
|
|
1519
|
+
)
|
|
1520
|
+
);
|
|
1521
|
+
return lines.join("\n");
|
|
1522
|
+
}
|
|
1523
|
+
const client = status.clientId ? style.dim(` (client: ${status.clientId})`) : "";
|
|
1524
|
+
lines.push(`IdP: ${code(style, String(status.issuer ?? "?"))}${client}`);
|
|
1525
|
+
lines.push(`Enabled: ${status.enabled === true ? "yes" : style.dim("no")}`);
|
|
1526
|
+
const loginState = String(status.loginState ?? "none");
|
|
1527
|
+
const stateText = loginState === "logged_in" ? style.green(loginState) : style.dim(loginState);
|
|
1528
|
+
lines.push(`IdP session: ${stateText}`);
|
|
1529
|
+
return lines.join("\n");
|
|
1530
|
+
}
|
|
1531
|
+
function formatServersListHuman(servers, style = PLAIN, source) {
|
|
1532
|
+
const lines = [heading(style, `Servers (${servers.length}):`)];
|
|
1533
|
+
if (source) {
|
|
1534
|
+
lines.push(`Source: ${style.dim(`${source.kind} ${source.path}`)}`);
|
|
1535
|
+
}
|
|
1536
|
+
for (const raw of servers) {
|
|
1537
|
+
const s = raw;
|
|
1538
|
+
const connectionName = typeof s.connection === "string" && s.connection.length > 0 ? s.connection : void 0;
|
|
1539
|
+
const connectionMark = connectionName ? ` ${style.green(`@${connectionName}`)}${s.isMru === true ? style.green(" (MRU)") : ""}` : "";
|
|
1540
|
+
lines.push(
|
|
1541
|
+
`* ${code(style, String(s.name))} ${style.dim(`[${String(s.type)}]`)} ${style.dim(String(s.detail ?? ""))}${connectionMark}`
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
if (servers.length === 0) lines.push(style.dim("(none)"));
|
|
1545
|
+
return lines.join("\n");
|
|
1546
|
+
}
|
|
1547
|
+
function formatServerShowHuman(server, style = PLAIN, source) {
|
|
1548
|
+
const name = String(server.name ?? "?");
|
|
1549
|
+
const type = String(server.type ?? "?");
|
|
1550
|
+
const detail = String(server.detail ?? "");
|
|
1551
|
+
const header = `${heading(style, "Server")} ${code(style, name)} ${style.dim(`[${type}]`)}`;
|
|
1552
|
+
const body = {};
|
|
1553
|
+
if (server.config && typeof server.config === "object") {
|
|
1554
|
+
body.config = server.config;
|
|
1555
|
+
}
|
|
1556
|
+
if (server.settings && typeof server.settings === "object") {
|
|
1557
|
+
body.settings = server.settings;
|
|
1558
|
+
}
|
|
1559
|
+
return [
|
|
1560
|
+
header,
|
|
1561
|
+
detail ? style.dim(detail) : style.dim("(no detail)"),
|
|
1562
|
+
// Same provenance line as servers/list — which file the entry came from.
|
|
1563
|
+
...source ? [`Source: ${style.dim(`${source.kind} ${source.path}`)}`] : [],
|
|
1564
|
+
JSON.stringify(body, null, 2)
|
|
1565
|
+
].join("\n");
|
|
1566
|
+
}
|
|
1567
|
+
function formatElicitationPendingHuman(elicitation, style = PLAIN) {
|
|
1568
|
+
const id = elicitation.elicitationId;
|
|
1569
|
+
const mode = elicitation.mode === "url" ? "url" : "form";
|
|
1570
|
+
const message = elicitation.message;
|
|
1571
|
+
const lines = [
|
|
1572
|
+
`${heading(style, "Input required")} \u2014 ${code(style, elicitation.method)}${typeof elicitation.toolName === "string" ? ` (tool ${code(style, elicitation.toolName)})` : ""} on ${code(style, `@${elicitation.connection}`)} is waiting on the user:`,
|
|
1573
|
+
` ${message}`
|
|
1574
|
+
];
|
|
1575
|
+
if (mode === "url") {
|
|
1576
|
+
const url = typeof elicitation.url === "string" ? elicitation.url : "";
|
|
1577
|
+
lines.push(
|
|
1578
|
+
"",
|
|
1579
|
+
"The user needs to open this link and complete it:",
|
|
1580
|
+
// Only allowlisted schemes render as a clickable OSC 8 link; a server
|
|
1581
|
+
// supplying file:/custom-handler URLs gets plain text (see sanitize.ts).
|
|
1582
|
+
` ${isSafeLinkTarget(url) ? style.link(url, url) : url}`,
|
|
1583
|
+
"",
|
|
1584
|
+
`When they're done, run: ${code(style, `elicitation/respond ${id} --done`)}`,
|
|
1585
|
+
style.dim(`To give up instead: elicitation/respond ${id} --cancel`)
|
|
1586
|
+
);
|
|
1587
|
+
} else {
|
|
1588
|
+
const fields = parseFormSchema(elicitation.requestedSchema);
|
|
1589
|
+
if (fields && fields.length > 0) {
|
|
1590
|
+
lines.push("", heading(style, `Fields (${fields.length}):`));
|
|
1591
|
+
for (const field of fields) {
|
|
1592
|
+
const kind = field.kind === "number" && field.integer ? "integer" : field.kind;
|
|
1593
|
+
const flags = field.required ? `${kind}, required` : kind;
|
|
1594
|
+
const choices = field.kind === "enum" || field.kind === "multiselect" ? ` [${field.choices.map((c) => c.value).join(", ")}]` : "";
|
|
1595
|
+
lines.push(
|
|
1596
|
+
` ${style.bold(field.name)} (${flags})${choices}${descSuffix(style, field.description)}`
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
lines.push(
|
|
1601
|
+
"",
|
|
1602
|
+
`Answer with: ${code(style, `elicitation/respond ${id} field:=value ...`)}`,
|
|
1603
|
+
style.dim(`Or: elicitation/respond ${id} --decline | --cancel`)
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
const expiresAt = Number(elicitation.expiresAt ?? 0);
|
|
1607
|
+
if (Number.isFinite(expiresAt) && expiresAt > Date.now()) {
|
|
1608
|
+
const minutes = Math.max(1, Math.round((expiresAt - Date.now()) / 6e4));
|
|
1609
|
+
lines.push(
|
|
1610
|
+
style.dim(
|
|
1611
|
+
`Unanswered, this expires (auto-cancels) in about ${minutes} minute${minutes === 1 ? "" : "s"}.`
|
|
1612
|
+
)
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
return lines.join("\n");
|
|
1616
|
+
}
|
|
1617
|
+
function formatConnectionsListHuman(connections, style = PLAIN) {
|
|
1618
|
+
const lines = [heading(style, `Connections (${connections.length}):`)];
|
|
1619
|
+
for (const raw of connections) {
|
|
1620
|
+
const s = raw;
|
|
1621
|
+
const mru = s.isMru === true ? style.green(" (MRU)") : "";
|
|
1622
|
+
const era = s.protocolEra !== void 0 ? style.dim(` [${String(s.protocolEra)}]`) : "";
|
|
1623
|
+
const pending = s.pendingAuth === true ? s.pendingAuthSignedIn === true ? style.yellow(" (signed in \u2014 completing on next use)") : style.yellow(" (sign-in pending)") : "";
|
|
1624
|
+
lines.push(
|
|
1625
|
+
`* ${code(style, `@${String(s.name)}`)}${mru}${style.dim(` \u2014 ${String(s.serverIdentity ?? "")}`)}${era}${pending}`
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
if (connections.length === 0) lines.push(style.dim("(none \u2014 connect first)"));
|
|
1629
|
+
return lines.join("\n");
|
|
1630
|
+
}
|
|
1631
|
+
function formatConnectionInfoHuman(connection, style = PLAIN) {
|
|
1632
|
+
const mru = connection.isMru === true ? style.green(" (MRU)") : "";
|
|
1633
|
+
const lines = [
|
|
1634
|
+
`${heading(style, "Connection")} ${code(style, `@${String(connection.name)}`)}${mru}`,
|
|
1635
|
+
`Server: ${style.dim(String(connection.serverIdentity ?? ""))}`
|
|
1636
|
+
];
|
|
1637
|
+
const era = connection.protocolEra;
|
|
1638
|
+
const protocolVersion = connection.protocolVersion;
|
|
1639
|
+
if (era !== void 0 || protocolVersion !== void 0) {
|
|
1640
|
+
const versionSuffix = protocolVersion !== void 0 ? ` (${String(protocolVersion)})` : "";
|
|
1641
|
+
lines.push(
|
|
1642
|
+
`Era: ${style.dim(`${String(era ?? "unknown")}${versionSuffix}`)}`
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
const auth = connection.auth;
|
|
1646
|
+
if (auth !== void 0) {
|
|
1647
|
+
const method = auth.method === "ema" ? "EMA" : "OAuth";
|
|
1648
|
+
const parts = [auth.authorized === true ? "authorized" : "not authorized"];
|
|
1649
|
+
if (typeof auth.scope === "string" && auth.scope !== "") {
|
|
1650
|
+
parts.push(`scope: ${auth.scope}`);
|
|
1651
|
+
}
|
|
1652
|
+
if (typeof auth.clientId === "string" && auth.clientId !== "") {
|
|
1653
|
+
parts.push(`client: ${auth.clientId}`);
|
|
1654
|
+
}
|
|
1655
|
+
if (typeof auth.idpSession === "string") {
|
|
1656
|
+
parts.push(`IdP session: ${auth.idpSession}`);
|
|
1657
|
+
}
|
|
1658
|
+
lines.push(`Auth: ${method} ${style.dim(`(${parts.join("; ")})`)}`);
|
|
1659
|
+
}
|
|
1660
|
+
if (connection.pendingAuth === true) {
|
|
1661
|
+
lines.push(
|
|
1662
|
+
connection.pendingAuthSignedIn === true ? `Sign-in: ${style.green("completed")} ${style.dim("(connection finishes on next use)")}` : `Sign-in: ${style.yellow("pending")} ${style.dim("(completes automatically after the user signs in)")}`
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
if (typeof connection.transport === "string") {
|
|
1666
|
+
const detail = connection.transport === "dormant" ? "dormant (reconnects on next use)" : connection.transport;
|
|
1667
|
+
lines.push(`Transport: ${style.dim(detail)}`);
|
|
1668
|
+
}
|
|
1669
|
+
const serverInfo = connection.serverInfo;
|
|
1670
|
+
if (serverInfo?.name !== void 0) {
|
|
1671
|
+
const version = serverInfo.version !== void 0 ? ` v${String(serverInfo.version)}` : "";
|
|
1672
|
+
lines.push(
|
|
1673
|
+
`Server info: ${style.dim(`${String(serverInfo.name)}${version}`)}`
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
const capabilities = connection.capabilities;
|
|
1677
|
+
if (capabilities !== void 0) {
|
|
1678
|
+
const keys = Object.keys(capabilities);
|
|
1679
|
+
lines.push(
|
|
1680
|
+
`Capabilities: ${style.dim(keys.length > 0 ? keys.join(", ") : "(none)")}`
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
const supportedVersions = connection.supportedVersions;
|
|
1684
|
+
if (Array.isArray(supportedVersions) && supportedVersions.length > 0) {
|
|
1685
|
+
lines.push(
|
|
1686
|
+
`Supported versions: ${style.dim(supportedVersions.join(", "))}`
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
if (typeof connection.instructions === "string" && connection.instructions !== "") {
|
|
1690
|
+
lines.push(`Instructions: ${style.dim(connection.instructions)}`);
|
|
1691
|
+
}
|
|
1692
|
+
return lines.join("\n");
|
|
1693
|
+
}
|
|
1694
|
+
function formatAppInfoListHuman(lines, style = PLAIN) {
|
|
1695
|
+
const out = [heading(style, `App info (${lines.length} tools):`)];
|
|
1696
|
+
for (const raw of lines) {
|
|
1697
|
+
const info = raw;
|
|
1698
|
+
const name = String(info.toolName ?? "?");
|
|
1699
|
+
if (info.hasApp === true) {
|
|
1700
|
+
const uri = String(info.resourceUri ?? "ui://?");
|
|
1701
|
+
out.push(
|
|
1702
|
+
`* ${code(style, name)} \u2014 ${style.green("app")} (${formatUri(style, uri)})`
|
|
1703
|
+
);
|
|
1704
|
+
} else {
|
|
1705
|
+
const err = typeof info.resourceError === "string" ? style.dim(` \u2014 ${info.resourceError}`) : style.dim(" \u2014 no app");
|
|
1706
|
+
out.push(`* ${code(style, name)}${err}`);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
return out.join("\n");
|
|
1710
|
+
}
|
|
1711
|
+
function formatSkillsHuman(skills, style = PLAIN) {
|
|
1712
|
+
const lines = [heading(style, `Skills (${skills.length}):`)];
|
|
1713
|
+
for (const raw of skills) {
|
|
1714
|
+
lines.push(formatSkillEntryLine(raw, style));
|
|
1715
|
+
}
|
|
1716
|
+
if (skills.length === 0) lines.push(style.dim("(none)"));
|
|
1717
|
+
return lines.join("\n");
|
|
1718
|
+
}
|
|
1719
|
+
function formatSkillGetHuman(result, style = PLAIN) {
|
|
1720
|
+
const skill = result.skill ?? {};
|
|
1721
|
+
const lines = [heading(style, "Skill:"), formatSkillEntryLine(skill, style)];
|
|
1722
|
+
if (result.ttlMs !== void 0)
|
|
1723
|
+
lines.push(style.dim(`ttlMs: ${String(result.ttlMs)}`));
|
|
1724
|
+
if (result.cacheScope !== void 0)
|
|
1725
|
+
lines.push(style.dim(`cacheScope: ${String(result.cacheScope)}`));
|
|
1726
|
+
return lines.join("\n");
|
|
1727
|
+
}
|
|
1728
|
+
function formatSkillEntryLine(skill, style) {
|
|
1729
|
+
const frontmatter = skill.frontmatter ?? {};
|
|
1730
|
+
const name = String(frontmatter.name ?? "?");
|
|
1731
|
+
const uri = String(skill.uri ?? "");
|
|
1732
|
+
const resources = skill.resources;
|
|
1733
|
+
const manifest = resources === "dynamic" ? style.dim(" \u2014 dynamic resources") : Array.isArray(resources) ? style.dim(` \u2014 ${resources.length} file(s)`) : "";
|
|
1734
|
+
return `* ${code(style, name)} (${formatUri(style, uri)})${descSuffix(style, frontmatter.description)}${manifest}`;
|
|
1735
|
+
}
|
|
1736
|
+
function formatSkillVerifyListHuman(lines, style = PLAIN) {
|
|
1737
|
+
const out = [heading(style, `Skill verification (${lines.length}):`)];
|
|
1738
|
+
for (const raw of lines) {
|
|
1739
|
+
const report = raw;
|
|
1740
|
+
const name = String(report.name ?? "?");
|
|
1741
|
+
const uri = String(report.uri ?? "");
|
|
1742
|
+
const outcome = report.outcome;
|
|
1743
|
+
const conformance = asArray(report.conformance);
|
|
1744
|
+
const frontmatter = asArray(report.frontmatter);
|
|
1745
|
+
const files = asArray(report.files);
|
|
1746
|
+
const errorCount = [...conformance, ...frontmatter].filter(
|
|
1747
|
+
(issue) => issue.severity === "error"
|
|
1748
|
+
).length;
|
|
1749
|
+
const mismatchCount = files.filter(
|
|
1750
|
+
(file) => file.status === "mismatch" || file.status === "read-error"
|
|
1751
|
+
).length;
|
|
1752
|
+
const verdict = outcome === "verified" ? style.green("verified") : outcome === "incomplete" ? style.dim("incomplete") : outcome === "unverifiable" ? style.dim("unverifiable") : style.red("failed");
|
|
1753
|
+
const detail = outcome === "verified" ? "" : outcome === "incomplete" ? style.dim(
|
|
1754
|
+
` \u2014 ${String(report.incomplete ?? "read bounds cut the walk short")}`
|
|
1755
|
+
) : outcome === "unverifiable" ? (
|
|
1756
|
+
// Nothing was checked, so "0 issue(s)" would misread as a pass
|
|
1757
|
+
// narrowly missed; name the actual condition instead (#2405).
|
|
1758
|
+
style.dim(
|
|
1759
|
+
` \u2014 advertised no digests (resources: "dynamic"), so integrity was not checked`
|
|
1760
|
+
)
|
|
1761
|
+
) : style.dim(
|
|
1762
|
+
` \u2014 ${errorCount} issue(s), ${mismatchCount} file mismatch(es)`
|
|
1763
|
+
);
|
|
1764
|
+
out.push(
|
|
1765
|
+
`* ${code(style, name)} (${formatUri(style, uri)}) \u2014 ${verdict}${detail}`
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
return out.join("\n");
|
|
1769
|
+
}
|
|
1770
|
+
function formatAppInfoHuman(info, style = PLAIN) {
|
|
1771
|
+
const name = String(info.toolName ?? "?");
|
|
1772
|
+
if (info.hasApp === true) {
|
|
1773
|
+
const lines = [
|
|
1774
|
+
`Tool ${code(style, name)} ${style.green("has an MCP App")}`,
|
|
1775
|
+
`Resource: ${formatUri(style, String(info.resourceUri ?? ""))}`
|
|
1776
|
+
];
|
|
1777
|
+
if (info.csp) lines.push(style.dim(`CSP: ${JSON.stringify(info.csp)}`));
|
|
1778
|
+
return lines.join("\n");
|
|
1779
|
+
}
|
|
1780
|
+
const err = typeof info.resourceError === "string" ? info.resourceError : "No MCP App UI resource (_meta.ui.resourceUri).";
|
|
1781
|
+
return `Tool ${code(style, name)} ${style.red("has no MCP App")}
|
|
1782
|
+
${style.dim(err)}`;
|
|
1783
|
+
}
|
|
1784
|
+
function formatStreamEventHuman(data, style = PLAIN) {
|
|
1785
|
+
if (!data || typeof data !== "object") return String(data);
|
|
1786
|
+
const ev = data;
|
|
1787
|
+
if (ev.type === "subscribed") {
|
|
1788
|
+
return `${heading(style, "Subscribed:")} ${formatUri(style, String(ev.uri ?? ""))}`;
|
|
1789
|
+
}
|
|
1790
|
+
if (ev.type === "resources/updated") {
|
|
1791
|
+
return `${heading(style, "Resource updated:")} ${formatUri(style, String(ev.uri ?? ""))}`;
|
|
1792
|
+
}
|
|
1793
|
+
if (ev.direction === "notification" && ev.message) {
|
|
1794
|
+
const msg = ev.message;
|
|
1795
|
+
const params = msg.params ?? {};
|
|
1796
|
+
const level = String(params.level ?? "info");
|
|
1797
|
+
const logger = params.logger ? style.dim(` ${String(params.logger)}:`) : "";
|
|
1798
|
+
const raw = params.data ?? params.message ?? params;
|
|
1799
|
+
const text = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
1800
|
+
return `[${colorLevel(style, level)}]${logger} ${text}`;
|
|
1801
|
+
}
|
|
1802
|
+
return JSON.stringify(ev, null, 2);
|
|
1803
|
+
}
|
|
1804
|
+
function formatRpcResultHuman(method, result, style = PLAIN) {
|
|
1805
|
+
switch (method) {
|
|
1806
|
+
case "tools/list":
|
|
1807
|
+
return formatToolsHuman(asArray(result.tools), style);
|
|
1808
|
+
case "tools/call":
|
|
1809
|
+
return formatCallToolResultHuman(result, style);
|
|
1810
|
+
case "resources/list":
|
|
1811
|
+
return formatResourcesHuman(asArray(result.resources), style);
|
|
1812
|
+
case "resources/read":
|
|
1813
|
+
return formatResourceReadHuman(result, style);
|
|
1814
|
+
case "resources/templates/list":
|
|
1815
|
+
return formatResourceTemplatesHuman(
|
|
1816
|
+
asArray(result.resourceTemplates),
|
|
1817
|
+
style
|
|
1818
|
+
);
|
|
1819
|
+
case "resources/unsubscribe":
|
|
1820
|
+
return `${heading(style, "Unsubscribed:")} ${formatUri(style, String(result.uri ?? ""))}`;
|
|
1821
|
+
case "prompts/list":
|
|
1822
|
+
return formatPromptsHuman(asArray(result.prompts), style);
|
|
1823
|
+
case "prompts/get":
|
|
1824
|
+
return formatPromptResultHuman(result, style);
|
|
1825
|
+
case "prompts/complete":
|
|
1826
|
+
return formatCompletionsHuman(result, style);
|
|
1827
|
+
case "initialize":
|
|
1828
|
+
return formatInitializeHuman(result, style);
|
|
1829
|
+
case "logging/setLevel":
|
|
1830
|
+
return style.green("Logging level updated.");
|
|
1831
|
+
case "tasks/list":
|
|
1832
|
+
return formatTasksHuman(asArray(result.tasks), style);
|
|
1833
|
+
case "tasks/get":
|
|
1834
|
+
return formatTaskHuman(result.task, style);
|
|
1835
|
+
case "tasks/cancel":
|
|
1836
|
+
return `${heading(style, "Cancelled task:")} ${String(result.taskId ?? "")}`;
|
|
1837
|
+
case "tasks/result":
|
|
1838
|
+
return formatCallToolResultHuman(result, style);
|
|
1839
|
+
case "roots/list":
|
|
1840
|
+
case "roots/set":
|
|
1841
|
+
return formatRootsHuman(asArray(result.roots), style);
|
|
1842
|
+
case "skills/list":
|
|
1843
|
+
return formatSkillsHuman(asArray(result.skills), style);
|
|
1844
|
+
case "skills/get":
|
|
1845
|
+
return formatSkillGetHuman(result, style);
|
|
1846
|
+
default:
|
|
1847
|
+
return null;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// src/connection/format-connection.ts
|
|
1852
|
+
function formatConnectionJson(data) {
|
|
1853
|
+
return JSON.stringify(data, null, 2).replace(
|
|
1854
|
+
/[\u0080-\u009F]/g,
|
|
1855
|
+
(ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`
|
|
1856
|
+
) + "\n";
|
|
1857
|
+
}
|
|
1858
|
+
async function writeConnectionOutput(opts, payload) {
|
|
1859
|
+
const format = opts.format === "json" ? "json" : "text";
|
|
1860
|
+
const style = opts.style ?? PLAIN;
|
|
1861
|
+
const interactive = opts.interactive === true;
|
|
1862
|
+
if (format === "json") {
|
|
1863
|
+
await awaitableLog(formatConnectionJson(jsonPayload(payload)));
|
|
1864
|
+
await writeNdjsonSummary(payload);
|
|
1865
|
+
applyExitCodes(payload);
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
await awaitableLog(
|
|
1869
|
+
humanPayload(sanitizeDeep(payload), style, interactive) + "\n"
|
|
1870
|
+
);
|
|
1871
|
+
await writeNdjsonSummary(payload);
|
|
1872
|
+
applyExitCodes(payload);
|
|
1873
|
+
}
|
|
1874
|
+
async function writeNdjsonSummary(payload) {
|
|
1875
|
+
if (payload.kind === "ndjson" && payload.summary) {
|
|
1876
|
+
await awaitableError(`${sanitizeText(payload.summary)}
|
|
1877
|
+
`);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
function jsonPayload(payload) {
|
|
1881
|
+
switch (payload.kind) {
|
|
1882
|
+
case "rpc":
|
|
1883
|
+
return payload.result;
|
|
1884
|
+
case "ndjson":
|
|
1885
|
+
return payload.lines;
|
|
1886
|
+
case "stream-event":
|
|
1887
|
+
return payload.data;
|
|
1888
|
+
case "servers/list":
|
|
1889
|
+
return {
|
|
1890
|
+
servers: payload.servers,
|
|
1891
|
+
...payload.source && { source: payload.source }
|
|
1892
|
+
};
|
|
1893
|
+
case "servers/show":
|
|
1894
|
+
return payload.source ? { ...payload.server, source: payload.source } : payload.server;
|
|
1895
|
+
case "connections/list":
|
|
1896
|
+
return { connections: payload.connections };
|
|
1897
|
+
case "connection":
|
|
1898
|
+
return payload.authUrl !== void 0 ? { ...payload.connection, authUrl: payload.authUrl } : payload.connection;
|
|
1899
|
+
case "elicitation-pending":
|
|
1900
|
+
return { elicitationPending: payload.elicitation };
|
|
1901
|
+
case "disconnect":
|
|
1902
|
+
return {
|
|
1903
|
+
name: payload.name,
|
|
1904
|
+
...payload.clearedAuthUrl && {
|
|
1905
|
+
clearedAuthUrl: payload.clearedAuthUrl
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1908
|
+
case "daemon/status":
|
|
1909
|
+
return payload.status;
|
|
1910
|
+
case "daemon/stop":
|
|
1911
|
+
return payload.result;
|
|
1912
|
+
case "auth/list":
|
|
1913
|
+
return payload.list;
|
|
1914
|
+
case "auth/clear":
|
|
1915
|
+
return payload.result;
|
|
1916
|
+
case "auth/ema-status":
|
|
1917
|
+
return payload.status;
|
|
1918
|
+
case "auth/ema-login":
|
|
1919
|
+
return payload.result;
|
|
1920
|
+
case "auth/ema-logout":
|
|
1921
|
+
return payload.result;
|
|
1922
|
+
case "generic":
|
|
1923
|
+
return payload.data;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
function signInHeadline() {
|
|
1927
|
+
return "Sign-in required. Open this link in a browser to authenticate:";
|
|
1928
|
+
}
|
|
1929
|
+
function agentSignInBlock(url, subject, resumeCmd, finishPhrase) {
|
|
1930
|
+
return [
|
|
1931
|
+
`Sign-in required for ${subject}. This command exited 0, but ${subject} is not usable yet until the user signs in \u2014 this is not an error and not success.`,
|
|
1932
|
+
"",
|
|
1933
|
+
`Show the sign-in URL below to the user as literal plain text (make it the last thing in your reply), ask them to open it and sign in, and wait for them to confirm before continuing. Do not retry, poll, sleep, or run another command this turn \u2014 the user cannot see your reply until the turn ends.`,
|
|
1934
|
+
"",
|
|
1935
|
+
"Sign-in URL (show this to the user):",
|
|
1936
|
+
"",
|
|
1937
|
+
` ${url}`,
|
|
1938
|
+
"",
|
|
1939
|
+
`Once the user confirms they have signed in, run \`mcpdo ${resumeCmd}\` to ${finishPhrase}.`
|
|
1940
|
+
].join("\n");
|
|
1941
|
+
}
|
|
1942
|
+
function humanPayload(payload, style, interactive) {
|
|
1943
|
+
switch (payload.kind) {
|
|
1944
|
+
case "rpc": {
|
|
1945
|
+
if (asAppInfoProbe(payload.result)) {
|
|
1946
|
+
return formatAppInfoHuman(payload.result, style);
|
|
1947
|
+
}
|
|
1948
|
+
const formatted = formatRpcResultHuman(
|
|
1949
|
+
payload.method,
|
|
1950
|
+
payload.result,
|
|
1951
|
+
style
|
|
1952
|
+
);
|
|
1953
|
+
return formatted ?? JSON.stringify(payload.result, null, 2);
|
|
1954
|
+
}
|
|
1955
|
+
case "ndjson":
|
|
1956
|
+
return payload.variant === "skill-verify" ? formatSkillVerifyListHuman(payload.lines, style) : formatAppInfoListHuman(payload.lines, style);
|
|
1957
|
+
case "stream-event":
|
|
1958
|
+
return formatStreamEventHuman(payload.data, style);
|
|
1959
|
+
case "servers/list":
|
|
1960
|
+
return formatServersListHuman(payload.servers, style, payload.source);
|
|
1961
|
+
case "servers/show":
|
|
1962
|
+
return formatServerShowHuman(payload.server, style, payload.source);
|
|
1963
|
+
case "connections/list":
|
|
1964
|
+
return formatConnectionsListHuman(payload.connections, style);
|
|
1965
|
+
case "connection": {
|
|
1966
|
+
const info = formatConnectionInfoHuman(
|
|
1967
|
+
payload.connection,
|
|
1968
|
+
style
|
|
1969
|
+
);
|
|
1970
|
+
if (payload.authUrl === void 0) return info;
|
|
1971
|
+
const name = String(payload.connection.name ?? "");
|
|
1972
|
+
if (!interactive) {
|
|
1973
|
+
return [
|
|
1974
|
+
info,
|
|
1975
|
+
"",
|
|
1976
|
+
agentSignInBlock(
|
|
1977
|
+
payload.authUrl,
|
|
1978
|
+
`@${name}`,
|
|
1979
|
+
`connections/show @${name}`,
|
|
1980
|
+
"finish the connection"
|
|
1981
|
+
)
|
|
1982
|
+
].join("\n");
|
|
1983
|
+
}
|
|
1984
|
+
return [
|
|
1985
|
+
info,
|
|
1986
|
+
"",
|
|
1987
|
+
signInHeadline(),
|
|
1988
|
+
// The URL comes from server-controlled OAuth metadata: only
|
|
1989
|
+
// allowlisted schemes become clickable OSC 8 links (same gate as
|
|
1990
|
+
// every other server-supplied link — see sanitize.ts).
|
|
1991
|
+
` ${isSafeLinkTarget(payload.authUrl) ? style.link(payload.authUrl) : payload.authUrl}`,
|
|
1992
|
+
style.dim(
|
|
1993
|
+
`The connection completes automatically after sign-in \u2014 check with \`connections/show @${name}\`, or just run the next command.`
|
|
1994
|
+
)
|
|
1995
|
+
].join("\n");
|
|
1996
|
+
}
|
|
1997
|
+
case "elicitation-pending":
|
|
1998
|
+
return formatElicitationPendingHuman(payload.elicitation, style);
|
|
1999
|
+
case "disconnect": {
|
|
2000
|
+
const line = `${style.bold("Disconnected")} ${`\`${style.bold(`@${payload.name}`)}\``}`;
|
|
2001
|
+
if (!payload.clearedAuthUrl) return line;
|
|
2002
|
+
return [
|
|
2003
|
+
line,
|
|
2004
|
+
style.dim(
|
|
2005
|
+
`Cleared stored auth for ${payload.clearedAuthUrl} \u2014 the next connect will re-trigger sign-in.`
|
|
2006
|
+
)
|
|
2007
|
+
].join("\n");
|
|
2008
|
+
}
|
|
2009
|
+
case "daemon/status": {
|
|
2010
|
+
const s = payload.status;
|
|
2011
|
+
if (s.running === false) {
|
|
2012
|
+
return String(s.message ?? "Daemon is not running.");
|
|
2013
|
+
}
|
|
2014
|
+
const connections = Array.isArray(s.connections) ? s.connections : [];
|
|
2015
|
+
return [
|
|
2016
|
+
`${style.bold("Daemon")} pid ${String(s.pid)}` + (s.stopping === true ? ` ${style.yellow("(shutting down)")}` : ""),
|
|
2017
|
+
style.dim(`Socket: ${String(s.socketPath ?? "")}`),
|
|
2018
|
+
formatConnectionsListHuman(connections, style)
|
|
2019
|
+
].join("\n");
|
|
2020
|
+
}
|
|
2021
|
+
case "daemon/stop":
|
|
2022
|
+
if (payload.result.stopping === false) {
|
|
2023
|
+
return String(payload.result.message ?? "Daemon was not running.");
|
|
2024
|
+
}
|
|
2025
|
+
return style.green("Daemon stopping.");
|
|
2026
|
+
case "auth/list":
|
|
2027
|
+
return formatAuthListHuman(payload.list, style);
|
|
2028
|
+
case "auth/clear":
|
|
2029
|
+
if (payload.result.all === true) {
|
|
2030
|
+
return style.green(
|
|
2031
|
+
`Cleared ${String(payload.result.cleared ?? 0)} stored auth entr${payload.result.cleared === 1 ? "y" : "ies"}.`
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
return `${style.green("Cleared")} \`${style.bold(String(payload.result.url ?? ""))}\`${payload.result.clearedByName ? style.dim(` (${String(payload.result.clearedByName)})`) : ""}`;
|
|
2035
|
+
case "auth/ema-status":
|
|
2036
|
+
return formatEmaStatusHuman(payload.status, style);
|
|
2037
|
+
case "auth/ema-login":
|
|
2038
|
+
if (payload.result.alreadyLoggedIn) {
|
|
2039
|
+
return `${style.green("Already signed in")} to \`${style.bold(payload.result.issuer)}\` ${style.dim("(use auth/ema-login --relogin for a fresh connection)")}`;
|
|
2040
|
+
}
|
|
2041
|
+
if (payload.result.pendingLogin === true && payload.result.authUrl) {
|
|
2042
|
+
if (!interactive) {
|
|
2043
|
+
return agentSignInBlock(
|
|
2044
|
+
payload.result.authUrl,
|
|
2045
|
+
"enterprise IdP login",
|
|
2046
|
+
"auth/ema-status",
|
|
2047
|
+
"finish the login"
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
return [
|
|
2051
|
+
signInHeadline(),
|
|
2052
|
+
// Same OSC 8 allowlist gate as the connection authUrl above.
|
|
2053
|
+
` ${isSafeLinkTarget(payload.result.authUrl) ? style.link(payload.result.authUrl) : payload.result.authUrl}`,
|
|
2054
|
+
style.dim(
|
|
2055
|
+
"The sign-in completes in the background \u2014 check with `auth/ema-status` (loginState becomes logged_in)."
|
|
2056
|
+
)
|
|
2057
|
+
].join("\n");
|
|
2058
|
+
}
|
|
2059
|
+
return `${style.green("Signed in")} to \`${style.bold(payload.result.issuer)}\``;
|
|
2060
|
+
case "auth/ema-logout": {
|
|
2061
|
+
const signedOut = `${style.green("Signed out")} of \`${style.bold(payload.result.issuer)}\` ${style.dim("(EMA server tokens cleared)")}`;
|
|
2062
|
+
if (payload.result.endSessionUrl === void 0) return signedOut;
|
|
2063
|
+
return [
|
|
2064
|
+
signedOut,
|
|
2065
|
+
"To end your IdP browser session, navigate to:",
|
|
2066
|
+
// Same OSC 8 allowlist gate as the sign-in URLs above, so the logout
|
|
2067
|
+
// link renders clickable in a terminal instead of as plain text.
|
|
2068
|
+
` ${isSafeLinkTarget(payload.result.endSessionUrl) ? style.link(payload.result.endSessionUrl) : payload.result.endSessionUrl}`
|
|
2069
|
+
].join("\n");
|
|
2070
|
+
}
|
|
2071
|
+
case "generic": {
|
|
2072
|
+
if (payload.title) {
|
|
2073
|
+
return `${style.bold(payload.title)}
|
|
2074
|
+
${JSON.stringify(payload.data, null, 2)}`;
|
|
2075
|
+
}
|
|
2076
|
+
return JSON.stringify(payload.data, null, 2);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
function asAppInfoProbe(result) {
|
|
2081
|
+
if (typeof result.hasApp !== "boolean" || typeof result.toolName !== "string" || result.content !== void 0 || result.tools !== void 0) {
|
|
2082
|
+
return void 0;
|
|
2083
|
+
}
|
|
2084
|
+
return result;
|
|
2085
|
+
}
|
|
2086
|
+
function applyExitCodes(payload) {
|
|
2087
|
+
if (payload.kind === "ndjson" && payload.exitCode) {
|
|
2088
|
+
throw new CliExitCodeError(payload.exitCode, payload.summary ?? "", {
|
|
2089
|
+
code: payload.exitCode === EXIT_CODES.SKILL_INCOMPLETE ? "skills_incomplete" : "skills_nonconformant"
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
if (payload.kind === "rpc") {
|
|
2093
|
+
const info = asAppInfoProbe(payload.result);
|
|
2094
|
+
if (info) {
|
|
2095
|
+
if (!info.hasApp) {
|
|
2096
|
+
throw new CliExitCodeError(
|
|
2097
|
+
EXIT_CODES.NO_APP,
|
|
2098
|
+
// toolName echoes server-influenced text into a terminal-bound
|
|
2099
|
+
// error message; sanitize like the success path does.
|
|
2100
|
+
`Tool '${sanitizeText(info.toolName)}' has no MCP App UI resource (_meta.ui.resourceUri).`
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
if (payload.result.isError === true) {
|
|
2106
|
+
throw new CliExitCodeError(
|
|
2107
|
+
EXIT_CODES.TOOL_ERROR,
|
|
2108
|
+
`Tool '${sanitizeText(payload.toolName ?? "tool")}' returned isError:true.`,
|
|
2109
|
+
{ code: "tool_is_error" }
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// src/connection/form-prompt.ts
|
|
2116
|
+
function watchForClose(rl) {
|
|
2117
|
+
return new Promise((_, reject) => {
|
|
2118
|
+
rl.once(
|
|
2119
|
+
"close",
|
|
2120
|
+
() => reject(new Error("stdin closed before an answer was given"))
|
|
2121
|
+
);
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
function ask(rl, closed, prompt) {
|
|
2125
|
+
return Promise.race([rl.question(prompt), closed]);
|
|
2126
|
+
}
|
|
2127
|
+
function formatDefault(field) {
|
|
2128
|
+
if (field.default === void 0) return void 0;
|
|
2129
|
+
if (field.kind === "multiselect") {
|
|
2130
|
+
return field.default.join(", ");
|
|
2131
|
+
}
|
|
2132
|
+
return String(field.default);
|
|
2133
|
+
}
|
|
2134
|
+
function describeField(field, style) {
|
|
2135
|
+
const req = field.required ? style.yellow(" (required)") : "";
|
|
2136
|
+
const desc = field.description ? ` \u2014 ${sanitizeText(field.description)}` : "";
|
|
2137
|
+
const def = formatDefault(field);
|
|
2138
|
+
const defHint = def !== void 0 ? style.dim(` [default: ${sanitizeText(def)}]`) : "";
|
|
2139
|
+
return `${style.bold(sanitizeText(field.title))}${req}${desc}${defHint}`;
|
|
2140
|
+
}
|
|
2141
|
+
async function promptField(rl, closed, field, style) {
|
|
2142
|
+
for (; ; ) {
|
|
2143
|
+
if (field.kind === "boolean") {
|
|
2144
|
+
const def2 = field.default;
|
|
2145
|
+
const hint = def2 === void 0 ? "y/n" : def2 ? "Y/n" : "y/N";
|
|
2146
|
+
const raw2 = (await ask(rl, closed, `${describeField(field, style)}
|
|
2147
|
+
[${hint}]: `)).trim().toLowerCase();
|
|
2148
|
+
if (raw2 === "" && def2 !== void 0) return def2;
|
|
2149
|
+
if (raw2 === "y" || raw2 === "yes") return true;
|
|
2150
|
+
if (raw2 === "n" || raw2 === "no") return false;
|
|
2151
|
+
if (raw2 === "" && !field.required) return void 0;
|
|
2152
|
+
process.stderr.write(style.red(" Please answer y or n.\n"));
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
if (field.kind === "enum" || field.kind === "multiselect") {
|
|
2156
|
+
const lines = field.choices.map(
|
|
2157
|
+
(choice, i) => ` ${i + 1}. ${sanitizeText(choice.label)}`
|
|
2158
|
+
);
|
|
2159
|
+
const multi = field.kind === "multiselect";
|
|
2160
|
+
const prompt = multi ? "Enter one or more numbers separated by commas" : "Enter a number";
|
|
2161
|
+
const raw2 = (await ask(
|
|
2162
|
+
rl,
|
|
2163
|
+
closed,
|
|
2164
|
+
`${describeField(field, style)}
|
|
2165
|
+
${lines.join("\n")}
|
|
2166
|
+
${prompt}: `
|
|
2167
|
+
)).trim();
|
|
2168
|
+
if (raw2 === "") {
|
|
2169
|
+
if (field.default !== void 0) return field.default;
|
|
2170
|
+
if (!field.required) return void 0;
|
|
2171
|
+
if (multi) {
|
|
2172
|
+
const m = field;
|
|
2173
|
+
if ((m.minItems ?? 0) === 0) return [];
|
|
2174
|
+
process.stderr.write(style.red(` Select at least ${m.minItems}.
|
|
2175
|
+
`));
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
process.stderr.write(style.red(" This field is required.\n"));
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
const tokens = raw2.split(",").map((s) => s.trim());
|
|
2182
|
+
const indices = tokens.map(
|
|
2183
|
+
(s) => /^\d+$/.test(s) ? Number.parseInt(s, 10) : Number.NaN
|
|
2184
|
+
);
|
|
2185
|
+
if (indices.some(
|
|
2186
|
+
(n) => !Number.isInteger(n) || n < 1 || n > field.choices.length
|
|
2187
|
+
)) {
|
|
2188
|
+
process.stderr.write(
|
|
2189
|
+
style.red(
|
|
2190
|
+
` Enter a number between 1 and ${field.choices.length}.
|
|
2191
|
+
`
|
|
2192
|
+
)
|
|
2193
|
+
);
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
if (!multi && indices.length !== 1) {
|
|
2197
|
+
process.stderr.write(style.red(" Enter exactly one number.\n"));
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
const values = indices.map((n) => field.choices[n - 1].value);
|
|
2201
|
+
if (multi) {
|
|
2202
|
+
const m = field;
|
|
2203
|
+
if (m.minItems !== void 0 && values.length < m.minItems) {
|
|
2204
|
+
process.stderr.write(style.red(` Select at least ${m.minItems}.
|
|
2205
|
+
`));
|
|
2206
|
+
continue;
|
|
2207
|
+
}
|
|
2208
|
+
if (m.maxItems !== void 0 && values.length > m.maxItems) {
|
|
2209
|
+
process.stderr.write(style.red(` Select at most ${m.maxItems}.
|
|
2210
|
+
`));
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
return values;
|
|
2214
|
+
}
|
|
2215
|
+
return values[0];
|
|
2216
|
+
}
|
|
2217
|
+
if (field.kind === "number") {
|
|
2218
|
+
const def2 = field.default;
|
|
2219
|
+
const raw2 = (await ask(
|
|
2220
|
+
rl,
|
|
2221
|
+
closed,
|
|
2222
|
+
`${describeField(field, style)}
|
|
2223
|
+
${def2 !== void 0 ? `[${def2}]` : ""}: `
|
|
2224
|
+
)).trim();
|
|
2225
|
+
if (raw2 === "") {
|
|
2226
|
+
if (def2 !== void 0) return def2;
|
|
2227
|
+
if (!field.required) return void 0;
|
|
2228
|
+
process.stderr.write(style.red(" This field is required.\n"));
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
const n = Number(raw2);
|
|
2232
|
+
if (
|
|
2233
|
+
// isFinite (not isNaN): "Infinity" is not a valid JSON number and
|
|
2234
|
+
// would serialize as null in the response frame.
|
|
2235
|
+
!Number.isFinite(n) || field.integer && !Number.isInteger(n) || field.minimum !== void 0 && n < field.minimum || field.maximum !== void 0 && n > field.maximum
|
|
2236
|
+
) {
|
|
2237
|
+
const range = field.minimum !== void 0 || field.maximum !== void 0 ? ` (${field.minimum ?? "-\u221E"}..${field.maximum ?? "\u221E"})` : "";
|
|
2238
|
+
process.stderr.write(
|
|
2239
|
+
style.red(
|
|
2240
|
+
` Enter a valid ${field.integer ? "integer" : "number"}${range}.
|
|
2241
|
+
`
|
|
2242
|
+
)
|
|
2243
|
+
);
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
return n;
|
|
2247
|
+
}
|
|
2248
|
+
const def = field.default;
|
|
2249
|
+
const raw = await ask(
|
|
2250
|
+
rl,
|
|
2251
|
+
closed,
|
|
2252
|
+
`${describeField(field, style)}
|
|
2253
|
+
${def !== void 0 ? `[${sanitizeText(def)}]` : ""}: `
|
|
2254
|
+
);
|
|
2255
|
+
if (raw === "" && def !== void 0) return def;
|
|
2256
|
+
if (raw === "" && !field.required) return void 0;
|
|
2257
|
+
const value = raw;
|
|
2258
|
+
const length = codePointLength(value);
|
|
2259
|
+
if (field.minLength !== void 0 && length < field.minLength) {
|
|
2260
|
+
process.stderr.write(
|
|
2261
|
+
style.red(` Must be at least ${field.minLength} characters.
|
|
2262
|
+
`)
|
|
2263
|
+
);
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
if (field.maxLength !== void 0 && length > field.maxLength) {
|
|
2267
|
+
process.stderr.write(
|
|
2268
|
+
style.red(` Must be at most ${field.maxLength} characters.
|
|
2269
|
+
`)
|
|
2270
|
+
);
|
|
2271
|
+
continue;
|
|
2272
|
+
}
|
|
2273
|
+
return value;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
async function promptForm(rl, message, fields, style) {
|
|
2277
|
+
process.stderr.write(`
|
|
2278
|
+
${style.bold("Input requested: ")}${message}
|
|
2279
|
+
|
|
2280
|
+
`);
|
|
2281
|
+
const closed = watchForClose(rl);
|
|
2282
|
+
const values = /* @__PURE__ */ new Map();
|
|
2283
|
+
for (const field of fields) {
|
|
2284
|
+
values.set(field.name, await promptField(rl, closed, field, style));
|
|
2285
|
+
}
|
|
2286
|
+
for (; ; ) {
|
|
2287
|
+
process.stderr.write(`
|
|
2288
|
+
${style.bold("Review your answers:")}
|
|
2289
|
+
`);
|
|
2290
|
+
for (const field2 of fields) {
|
|
2291
|
+
const v = values.get(field2.name);
|
|
2292
|
+
const label = field2.title === field2.name ? field2.title : `${field2.title} (${field2.name})`;
|
|
2293
|
+
process.stderr.write(
|
|
2294
|
+
` ${sanitizeText(label)}: ${v === void 0 ? style.dim("(none)") : sanitizeText(String(v))}
|
|
2295
|
+
`
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
const answer = (await ask(
|
|
2299
|
+
rl,
|
|
2300
|
+
closed,
|
|
2301
|
+
"\nPress Enter to submit, type a field name to edit it, or 'c' to cancel: "
|
|
2302
|
+
)).trim();
|
|
2303
|
+
if (answer === "") {
|
|
2304
|
+
const content = /* @__PURE__ */ Object.create(null);
|
|
2305
|
+
for (const field2 of fields) {
|
|
2306
|
+
const v = values.get(field2.name);
|
|
2307
|
+
if (v !== void 0) content[field2.name] = v;
|
|
2308
|
+
}
|
|
2309
|
+
return { action: "accept", content };
|
|
2310
|
+
}
|
|
2311
|
+
if (answer.toLowerCase() === "c") {
|
|
2312
|
+
return { action: "cancel" };
|
|
2313
|
+
}
|
|
2314
|
+
const field = fields.find((f) => f.name === answer) ?? fields.find((f) => f.title === answer);
|
|
2315
|
+
if (!field) {
|
|
2316
|
+
process.stderr.write(
|
|
2317
|
+
style.red(` Unknown field "${answer}". Try again.
|
|
2318
|
+
`)
|
|
2319
|
+
);
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
values.set(field.name, await promptField(rl, closed, field, style));
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// src/connection/prompt-reader.ts
|
|
2327
|
+
import * as readline from "readline";
|
|
2328
|
+
function exhaustedError() {
|
|
2329
|
+
return new Error("stdin closed before an answer was given");
|
|
2330
|
+
}
|
|
2331
|
+
var PromptReader = class {
|
|
2332
|
+
rl;
|
|
2333
|
+
input;
|
|
2334
|
+
output;
|
|
2335
|
+
queued = [];
|
|
2336
|
+
waiter;
|
|
2337
|
+
closeListeners = [];
|
|
2338
|
+
eof = false;
|
|
2339
|
+
exhaustedNotified = false;
|
|
2340
|
+
constructor(input = process.stdin, output = process.stderr) {
|
|
2341
|
+
this.input = input;
|
|
2342
|
+
this.output = output;
|
|
2343
|
+
this.rl = readline.createInterface({ input, output });
|
|
2344
|
+
this.rl.on("line", (line) => {
|
|
2345
|
+
const waiter = this.waiter;
|
|
2346
|
+
if (waiter) {
|
|
2347
|
+
this.waiter = void 0;
|
|
2348
|
+
this.park();
|
|
2349
|
+
waiter.resolve(line);
|
|
2350
|
+
} else {
|
|
2351
|
+
this.queued.push(line);
|
|
2352
|
+
}
|
|
2353
|
+
});
|
|
2354
|
+
this.rl.once("close", () => {
|
|
2355
|
+
this.eof = true;
|
|
2356
|
+
const waiter = this.waiter;
|
|
2357
|
+
if (waiter) {
|
|
2358
|
+
this.waiter = void 0;
|
|
2359
|
+
this.notifyExhausted();
|
|
2360
|
+
waiter.reject(exhaustedError());
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
this.park();
|
|
2364
|
+
}
|
|
2365
|
+
/**
|
|
2366
|
+
* Write `prompt` and resolve with the next input line — a queued one
|
|
2367
|
+
* first, else the next to arrive. Rejects once input is exhausted (EOF
|
|
2368
|
+
* with nothing queued).
|
|
2369
|
+
*/
|
|
2370
|
+
question(prompt) {
|
|
2371
|
+
this.output.write(prompt);
|
|
2372
|
+
const queued = this.queued.shift();
|
|
2373
|
+
if (queued !== void 0) return Promise.resolve(queued);
|
|
2374
|
+
if (this.eof) {
|
|
2375
|
+
this.notifyExhausted();
|
|
2376
|
+
return Promise.reject(exhaustedError());
|
|
2377
|
+
}
|
|
2378
|
+
this.engage();
|
|
2379
|
+
return new Promise((resolve3, reject) => {
|
|
2380
|
+
this.waiter = { resolve: resolve3, reject };
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
/**
|
|
2384
|
+
* `close` here means input is exhausted: EOF *and* no queued line left to
|
|
2385
|
+
* answer with. A raw stream close while answers are still queued must not
|
|
2386
|
+
* cancel a form those answers can complete.
|
|
2387
|
+
*/
|
|
2388
|
+
once(event, listener) {
|
|
2389
|
+
if (event === "close") {
|
|
2390
|
+
if (this.exhaustedNotified) listener();
|
|
2391
|
+
else this.closeListeners.push(listener);
|
|
2392
|
+
}
|
|
2393
|
+
return this;
|
|
2394
|
+
}
|
|
2395
|
+
/** Tear down the underlying interface (tests / process cleanup). Restores
|
|
2396
|
+
* cooked mode first so a disposed/exiting reader never leaves the terminal
|
|
2397
|
+
* raw (where Ctrl-C would stay dead in the user's next shell). */
|
|
2398
|
+
dispose() {
|
|
2399
|
+
if (this.input.isTTY) this.input.setRawMode?.(false);
|
|
2400
|
+
this.rl.close();
|
|
2401
|
+
}
|
|
2402
|
+
notifyExhausted() {
|
|
2403
|
+
if (this.exhaustedNotified) return;
|
|
2404
|
+
this.exhaustedNotified = true;
|
|
2405
|
+
const listeners = this.closeListeners;
|
|
2406
|
+
this.closeListeners = [];
|
|
2407
|
+
for (const listener of listeners) listener();
|
|
2408
|
+
}
|
|
2409
|
+
/** Actively waiting for a line: let the stream flow and hold the loop. On a
|
|
2410
|
+
* TTY, re-enter raw mode so readline can do line editing while a question is
|
|
2411
|
+
* pending (the counterpart to {@link park} restoring cooked mode). */
|
|
2412
|
+
engage() {
|
|
2413
|
+
this.input.ref?.();
|
|
2414
|
+
if (this.input.isTTY) this.input.setRawMode?.(true);
|
|
2415
|
+
this.rl.resume();
|
|
2416
|
+
}
|
|
2417
|
+
/** Idle between questions: stop reading and release the event loop. On a TTY,
|
|
2418
|
+
* restore cooked mode so the terminal is not left raw between prompts — raw
|
|
2419
|
+
* mode disables the kernel's Ctrl-C (SIGINT), and a persistent reader that
|
|
2420
|
+
* stayed raw while parked left Ctrl-C dead in the gap between rounds. */
|
|
2421
|
+
park() {
|
|
2422
|
+
this.rl.pause();
|
|
2423
|
+
if (this.input.isTTY) this.input.setRawMode?.(false);
|
|
2424
|
+
this.input.unref?.();
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
var shared;
|
|
2428
|
+
function getSharedPromptReader() {
|
|
2429
|
+
shared ??= new PromptReader();
|
|
2430
|
+
return shared;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// src/connection/elicitation-prompt.ts
|
|
2434
|
+
function cancelResponse(frame) {
|
|
2435
|
+
return {
|
|
2436
|
+
id: frame.id,
|
|
2437
|
+
kind: "elicitation-response",
|
|
2438
|
+
elicitationId: frame.elicitationId,
|
|
2439
|
+
action: "cancel"
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
function declineResponse(frame) {
|
|
2443
|
+
return {
|
|
2444
|
+
id: frame.id,
|
|
2445
|
+
kind: "elicitation-response",
|
|
2446
|
+
elicitationId: frame.elicitationId,
|
|
2447
|
+
action: "decline"
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
async function promptElicitation(frame, opts) {
|
|
2451
|
+
const { style } = opts;
|
|
2452
|
+
const message = sanitizeText(frame.message);
|
|
2453
|
+
const url = frame.url === void 0 ? void 0 : sanitizeText(frame.url);
|
|
2454
|
+
if (frame.mode === "form") {
|
|
2455
|
+
const fields = parseFormSchema(frame.requestedSchema);
|
|
2456
|
+
if (!fields) {
|
|
2457
|
+
process.stderr.write(
|
|
2458
|
+
style.yellow(
|
|
2459
|
+
"This server's form request uses a schema mcpdo doesn't support \u2014 declining.\n"
|
|
2460
|
+
) + ` ${message}
|
|
2461
|
+
`
|
|
2462
|
+
);
|
|
2463
|
+
return declineResponse(frame);
|
|
2464
|
+
}
|
|
2465
|
+
if (!opts.interactive) {
|
|
2466
|
+
process.stderr.write(
|
|
2467
|
+
style.yellow(
|
|
2468
|
+
"This server is asking for form input, which isn't supported with --format json \u2014 declining.\n"
|
|
2469
|
+
) + ` ${message}
|
|
2470
|
+
`
|
|
2471
|
+
);
|
|
2472
|
+
return declineResponse(frame);
|
|
2473
|
+
}
|
|
2474
|
+
const rl2 = getSharedPromptReader();
|
|
2475
|
+
try {
|
|
2476
|
+
const outcome = await promptForm(rl2, message, fields, style);
|
|
2477
|
+
if (outcome.action === "accept") {
|
|
2478
|
+
return {
|
|
2479
|
+
id: frame.id,
|
|
2480
|
+
kind: "elicitation-response",
|
|
2481
|
+
elicitationId: frame.elicitationId,
|
|
2482
|
+
action: "accept",
|
|
2483
|
+
content: outcome.content
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
if (outcome.action === "decline") return declineResponse(frame);
|
|
2487
|
+
return cancelResponse(frame);
|
|
2488
|
+
} catch {
|
|
2489
|
+
return cancelResponse(frame);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
if (!opts.interactive) {
|
|
2493
|
+
process.stderr.write(
|
|
2494
|
+
style.yellow(
|
|
2495
|
+
"This server is asking for input via a URL (elicitation), which isn't supported with --format json \u2014 cancelling.\n"
|
|
2496
|
+
) + ` ${message}
|
|
2497
|
+
` + (url ? ` ${url}
|
|
2498
|
+
` : "")
|
|
2499
|
+
);
|
|
2500
|
+
return cancelResponse(frame);
|
|
2501
|
+
}
|
|
2502
|
+
process.stderr.write(
|
|
2503
|
+
"\n" + style.bold("Action required: ") + message + "\n " + // Only allowlisted schemes render as a clickable OSC 8 link; a server
|
|
2504
|
+
// supplying file:/custom-handler URLs gets plain text (see sanitize.ts).
|
|
2505
|
+
(url !== void 0 && isSafeLinkTarget(url) ? style.link(url, url) : url ?? "") + "\n\n"
|
|
2506
|
+
);
|
|
2507
|
+
const rl = getSharedPromptReader();
|
|
2508
|
+
try {
|
|
2509
|
+
const answer = await Promise.race([
|
|
2510
|
+
rl.question(
|
|
2511
|
+
"Open the URL above, complete it, then press Enter to continue (or type 'c' to cancel): "
|
|
2512
|
+
),
|
|
2513
|
+
watchForClose(rl)
|
|
2514
|
+
]);
|
|
2515
|
+
if (answer.trim().toLowerCase() === "c") {
|
|
2516
|
+
return cancelResponse(frame);
|
|
2517
|
+
}
|
|
2518
|
+
return {
|
|
2519
|
+
id: frame.id,
|
|
2520
|
+
kind: "elicitation-response",
|
|
2521
|
+
elicitationId: frame.elicitationId,
|
|
2522
|
+
action: "accept"
|
|
2523
|
+
};
|
|
2524
|
+
} catch {
|
|
2525
|
+
return cancelResponse(frame);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// src/connection/dispatch.ts
|
|
2530
|
+
var STREAM_METHODS = /* @__PURE__ */ new Set(["logging/tail", "resources/subscribe"]);
|
|
2531
|
+
var NDJSON_VARIANTS = /* @__PURE__ */ new Set(["skills/list", "skills/get"]);
|
|
2532
|
+
async function dispatchConnectionRpc(method, methodArgs, opts) {
|
|
2533
|
+
const format = opts.format ?? "text";
|
|
2534
|
+
const style = styleFromOpts({ plain: opts.plain, format });
|
|
2535
|
+
const params = {
|
|
2536
|
+
...methodArgs,
|
|
2537
|
+
// `format` stays frontend-only: forwarding it would make the daemon's
|
|
2538
|
+
// runMethod treat `format: "json"` tool calls as app-info requests and
|
|
2539
|
+
// issue a hidden extra resources/read whose result we discard. The
|
|
2540
|
+
// daemon also strips it defensively (see stripConnectionFields).
|
|
2541
|
+
method,
|
|
2542
|
+
name: stripAt(opts.connection),
|
|
2543
|
+
requireExplicit: opts.requireExplicit
|
|
2544
|
+
};
|
|
2545
|
+
const { socketPath } = await ensureDaemon();
|
|
2546
|
+
if (STREAM_METHODS.has(method)) {
|
|
2547
|
+
const ac2 = new AbortController();
|
|
2548
|
+
const onSignal2 = () => ac2.abort();
|
|
2549
|
+
process.on("SIGINT", onSignal2);
|
|
2550
|
+
process.on("SIGTERM", onSignal2);
|
|
2551
|
+
let writeChain = Promise.resolve();
|
|
2552
|
+
try {
|
|
2553
|
+
await streamDaemon(params, {
|
|
2554
|
+
socketPath,
|
|
2555
|
+
// Core enforces the configured MCP request timeout daemon-side; a
|
|
2556
|
+
// fixed local deadline would falsely fail stream setups (e.g. a
|
|
2557
|
+
// subscribe against a slow server) that are still valid.
|
|
2558
|
+
timeoutMs: 0,
|
|
2559
|
+
signal: ac2.signal,
|
|
2560
|
+
onData: (data) => {
|
|
2561
|
+
writeChain = writeChain.then(
|
|
2562
|
+
() => writeConnectionOutput(
|
|
2563
|
+
{ format, style },
|
|
2564
|
+
{
|
|
2565
|
+
kind: "stream-event",
|
|
2566
|
+
data
|
|
2567
|
+
}
|
|
2568
|
+
)
|
|
2569
|
+
).catch(() => {
|
|
2570
|
+
});
|
|
2571
|
+
return writeChain;
|
|
2572
|
+
}
|
|
2573
|
+
});
|
|
2574
|
+
} finally {
|
|
2575
|
+
process.off("SIGINT", onSignal2);
|
|
2576
|
+
process.off("SIGTERM", onSignal2);
|
|
2577
|
+
await writeChain.catch(() => {
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
const ac = new AbortController();
|
|
2583
|
+
const onSignal = () => ac.abort();
|
|
2584
|
+
process.on("SIGINT", onSignal);
|
|
2585
|
+
process.on("SIGTERM", onSignal);
|
|
2586
|
+
const interactive = format === "text" && (process.stdin.isTTY === true || process.stderr.isTTY === true);
|
|
2587
|
+
params.interactive = interactive;
|
|
2588
|
+
let outcome;
|
|
2589
|
+
try {
|
|
2590
|
+
outcome = await callDaemon("rpc", params, {
|
|
2591
|
+
socketPath,
|
|
2592
|
+
// Core enforces the configured MCP request timeout daemon-side; a
|
|
2593
|
+
// fixed local deadline would falsely fail long-running tool calls.
|
|
2594
|
+
timeoutMs: 0,
|
|
2595
|
+
signal: ac.signal,
|
|
2596
|
+
onElicitation: (frame) => promptElicitation(frame, {
|
|
2597
|
+
style,
|
|
2598
|
+
// Prompting only needs a readable stdin and a text-based reply
|
|
2599
|
+
// channel; parked (non-interactive) callers never receive frames,
|
|
2600
|
+
// so this only ever runs interactively.
|
|
2601
|
+
interactive
|
|
2602
|
+
})
|
|
2603
|
+
});
|
|
2604
|
+
} finally {
|
|
2605
|
+
process.off("SIGINT", onSignal);
|
|
2606
|
+
process.off("SIGTERM", onSignal);
|
|
2607
|
+
}
|
|
2608
|
+
await writeRpcOutcome(
|
|
2609
|
+
{ format, style },
|
|
2610
|
+
method,
|
|
2611
|
+
methodArgs.toolName,
|
|
2612
|
+
outcome
|
|
2613
|
+
);
|
|
2614
|
+
}
|
|
2615
|
+
async function writeRpcOutcome(out, method, toolName, outcome) {
|
|
2616
|
+
const { format, style } = out;
|
|
2617
|
+
if (outcome.kind === "elicitation-pending") {
|
|
2618
|
+
await writeConnectionOutput(
|
|
2619
|
+
{ format, style },
|
|
2620
|
+
{ kind: "elicitation-pending", elicitation: outcome.elicitation }
|
|
2621
|
+
);
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
if (outcome.kind === "ndjson") {
|
|
2625
|
+
await writeConnectionOutput(
|
|
2626
|
+
{ format, style },
|
|
2627
|
+
{
|
|
2628
|
+
kind: "ndjson",
|
|
2629
|
+
lines: outcome.lines,
|
|
2630
|
+
variant: NDJSON_VARIANTS.has(method) ? "skill-verify" : "app-info",
|
|
2631
|
+
summary: outcome.summary,
|
|
2632
|
+
exitCode: outcome.exitCode
|
|
2633
|
+
}
|
|
2634
|
+
);
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
await writeConnectionOutput(
|
|
2638
|
+
{ format, style },
|
|
2639
|
+
{
|
|
2640
|
+
kind: "rpc",
|
|
2641
|
+
method,
|
|
2642
|
+
result: outcome.result,
|
|
2643
|
+
appInfo: outcome.appInfo,
|
|
2644
|
+
toolName
|
|
2645
|
+
}
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
function stripAt(name) {
|
|
2649
|
+
if (!name) return void 0;
|
|
2650
|
+
return name.startsWith("@") ? name.slice(1) : name;
|
|
2651
|
+
}
|
|
2652
|
+
function requireExplicitConnection() {
|
|
2653
|
+
if (process.env.MCP_ALLOW_DEFAULT_CONNECTION === "1") return false;
|
|
2654
|
+
return process.stdin.isTTY !== true;
|
|
2655
|
+
}
|
|
2656
|
+
var VALUE_TAKING_GLOBALS = /* @__PURE__ */ new Set([
|
|
2657
|
+
"--format",
|
|
2658
|
+
"--connection",
|
|
2659
|
+
"--catalog",
|
|
2660
|
+
"--config"
|
|
2661
|
+
]);
|
|
2662
|
+
var AT_CONNECTION_RE = /^@[A-Za-z0-9_.-]+$/;
|
|
2663
|
+
function hoistAtConnection(argv) {
|
|
2664
|
+
const start = 2;
|
|
2665
|
+
for (let i = start; i < argv.length; i++) {
|
|
2666
|
+
const token = argv[i];
|
|
2667
|
+
if (token === "--") break;
|
|
2668
|
+
if (AT_CONNECTION_RE.test(token)) {
|
|
2669
|
+
return {
|
|
2670
|
+
argv: [...argv.slice(0, i), ...argv.slice(i + 1)],
|
|
2671
|
+
connectionFromAt: token.slice(1)
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
if (token.startsWith("-")) {
|
|
2675
|
+
if (!token.includes("=") && VALUE_TAKING_GLOBALS.has(token)) i++;
|
|
2676
|
+
continue;
|
|
2677
|
+
}
|
|
2678
|
+
break;
|
|
2679
|
+
}
|
|
2680
|
+
return { argv };
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
// src/connection/private-env.ts
|
|
2684
|
+
import { randomBytes } from "crypto";
|
|
2685
|
+
function createPrivateBinding() {
|
|
2686
|
+
const dir = createPrivateDaemonDir();
|
|
2687
|
+
const token = randomBytes(32).toString("base64url");
|
|
2688
|
+
return { dir, token };
|
|
2689
|
+
}
|
|
2690
|
+
function formatPrivateEnvExports(binding) {
|
|
2691
|
+
return [
|
|
2692
|
+
`export ${DAEMON_DIR_ENV}=${shellSingleQuote(binding.dir)}`,
|
|
2693
|
+
`export ${DAEMON_TOKEN_ENV}=${shellSingleQuote(binding.token)}`,
|
|
2694
|
+
""
|
|
2695
|
+
].join("\n");
|
|
2696
|
+
}
|
|
2697
|
+
function shellSingleQuote(value) {
|
|
2698
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
// src/connection/stored-auth.ts
|
|
2702
|
+
function normalizeServerUrl(serverUrl) {
|
|
2703
|
+
try {
|
|
2704
|
+
return new URL(serverUrl).href;
|
|
2705
|
+
} catch {
|
|
2706
|
+
return serverUrl;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
function tokenFlagsFromState(state) {
|
|
2710
|
+
if (state == null || typeof state !== "object") {
|
|
2711
|
+
return { hasTokens: false, hasRefreshToken: false };
|
|
2712
|
+
}
|
|
2713
|
+
const s = state;
|
|
2714
|
+
let hasTokens = Boolean(s.tokens?.access_token);
|
|
2715
|
+
let hasRefreshToken = hasTokens && Boolean(s.tokens?.refresh_token);
|
|
2716
|
+
for (const slot of Object.values(s.byIssuer ?? {})) {
|
|
2717
|
+
if (slot?.tokens?.access_token) {
|
|
2718
|
+
hasTokens = true;
|
|
2719
|
+
if (slot.tokens.refresh_token) hasRefreshToken = true;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return { hasTokens, hasRefreshToken };
|
|
2723
|
+
}
|
|
2724
|
+
async function readServersMap(statePath) {
|
|
2725
|
+
const snapshot = await readOAuthStore(statePath);
|
|
2726
|
+
if (snapshot?.servers && typeof snapshot.servers === "object") {
|
|
2727
|
+
return snapshot.servers;
|
|
2728
|
+
}
|
|
2729
|
+
return {};
|
|
2730
|
+
}
|
|
2731
|
+
async function listStoredAuth() {
|
|
2732
|
+
const oauthStatePath = getStateFilePath();
|
|
2733
|
+
const servers = await readServersMap(oauthStatePath);
|
|
2734
|
+
const entries = Object.keys(servers).sort((a, b) => a.localeCompare(b)).map((url) => ({
|
|
2735
|
+
url,
|
|
2736
|
+
...tokenFlagsFromState(servers[url])
|
|
2737
|
+
}));
|
|
2738
|
+
return { oauthStatePath, servers: entries };
|
|
2739
|
+
}
|
|
2740
|
+
async function resolveStoredAuthKey(key) {
|
|
2741
|
+
const trimmed = key.trim();
|
|
2742
|
+
if (!trimmed) {
|
|
2743
|
+
throw new CliExitCodeError(
|
|
2744
|
+
EXIT_CODES.USAGE,
|
|
2745
|
+
"auth/clear requires a server URL key (from auth/list) or --all",
|
|
2746
|
+
{ code: "usage" }
|
|
2747
|
+
);
|
|
2748
|
+
}
|
|
2749
|
+
const { servers } = await listStoredAuth();
|
|
2750
|
+
const urls = servers.map((s) => s.url);
|
|
2751
|
+
if (urls.includes(trimmed)) return trimmed;
|
|
2752
|
+
const normalized = normalizeServerUrl(trimmed);
|
|
2753
|
+
if (urls.includes(normalized)) return normalized;
|
|
2754
|
+
if (normalized !== trimmed || /^https?:\/\//i.test(trimmed)) {
|
|
2755
|
+
return normalized;
|
|
2756
|
+
}
|
|
2757
|
+
throw new CliExitCodeError(
|
|
2758
|
+
EXIT_CODES.USAGE,
|
|
2759
|
+
`No stored auth entry for '${trimmed}'. Use auth/list to see keys.`,
|
|
2760
|
+
{ code: "usage" }
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
async function clearStoredAuth(key) {
|
|
2764
|
+
const url = await resolveStoredAuthKey(key);
|
|
2765
|
+
const storage = new NodeOAuthStorage();
|
|
2766
|
+
await storage.clear(url);
|
|
2767
|
+
resetNodeOAuthStorageCache();
|
|
2768
|
+
return { url };
|
|
2769
|
+
}
|
|
2770
|
+
async function clearAllStoredAuth() {
|
|
2771
|
+
const before = await listStoredAuth();
|
|
2772
|
+
await clearAllOAuthClientState();
|
|
2773
|
+
resetNodeOAuthStorageCache();
|
|
2774
|
+
return { cleared: before.servers.length };
|
|
2775
|
+
}
|
|
2776
|
+
async function clearStoredAuthForRelogin(serverUrl) {
|
|
2777
|
+
if (!serverUrl?.trim()) return;
|
|
2778
|
+
const url = normalizeServerUrl(serverUrl.trim());
|
|
2779
|
+
const storage = new NodeOAuthStorage();
|
|
2780
|
+
await storage.clear(url);
|
|
2781
|
+
resetNodeOAuthStorageCache();
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// src/connection/auth-names.ts
|
|
2785
|
+
function urlIfHttp(value) {
|
|
2786
|
+
if (value && /^https?:\/\//i.test(value)) return normalizeServerUrl(value);
|
|
2787
|
+
return void 0;
|
|
2788
|
+
}
|
|
2789
|
+
function buildAuthNameIndex(entries, connections) {
|
|
2790
|
+
const urlToNames = /* @__PURE__ */ new Map();
|
|
2791
|
+
const nameToUrls = /* @__PURE__ */ new Map();
|
|
2792
|
+
const knownNames = /* @__PURE__ */ new Set();
|
|
2793
|
+
const add = (name, url, source, isLive) => {
|
|
2794
|
+
knownNames.add(name);
|
|
2795
|
+
if (!url) return;
|
|
2796
|
+
let urls = nameToUrls.get(name);
|
|
2797
|
+
if (!urls) {
|
|
2798
|
+
urls = /* @__PURE__ */ new Set();
|
|
2799
|
+
nameToUrls.set(name, urls);
|
|
2800
|
+
}
|
|
2801
|
+
urls.add(url);
|
|
2802
|
+
let refs = urlToNames.get(url);
|
|
2803
|
+
if (!refs) {
|
|
2804
|
+
refs = [];
|
|
2805
|
+
urlToNames.set(url, refs);
|
|
2806
|
+
}
|
|
2807
|
+
const existing = refs.find((r) => r.name === name);
|
|
2808
|
+
if (existing) {
|
|
2809
|
+
if (isLive) existing.isLive = true;
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
refs.push({ name, source, isLive });
|
|
2813
|
+
};
|
|
2814
|
+
for (const entry of entries) {
|
|
2815
|
+
add(entry.name, urlIfHttp(entry.detail), "catalog", false);
|
|
2816
|
+
}
|
|
2817
|
+
for (const conn of connections) {
|
|
2818
|
+
add(conn.name, urlIfHttp(conn.serverIdentity), "connection", true);
|
|
2819
|
+
}
|
|
2820
|
+
return index(urlToNames, nameToUrls, knownNames);
|
|
2821
|
+
}
|
|
2822
|
+
function index(urlToNames, nameToUrls, knownNames) {
|
|
2823
|
+
for (const refs of urlToNames.values()) {
|
|
2824
|
+
refs.sort(
|
|
2825
|
+
(a, b) => Number(b.isLive) - Number(a.isLive) || a.name.localeCompare(b.name)
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
return { urlToNames, nameToUrls, knownNames };
|
|
2829
|
+
}
|
|
2830
|
+
function resolveFriendlyName(name, index2) {
|
|
2831
|
+
const urls = index2.nameToUrls.get(name);
|
|
2832
|
+
if (urls && urls.size === 1) {
|
|
2833
|
+
return { kind: "url", url: [...urls][0] };
|
|
2834
|
+
}
|
|
2835
|
+
if (urls && urls.size > 1) {
|
|
2836
|
+
return { kind: "ambiguous", name, urls: [...urls].sort() };
|
|
2837
|
+
}
|
|
2838
|
+
if (index2.knownNames.has(name)) {
|
|
2839
|
+
return { kind: "no-url", name };
|
|
2840
|
+
}
|
|
2841
|
+
return { kind: "unknown", name };
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// src/connection/mcp.ts
|
|
2845
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
2846
|
+
function isDaemonUnreachable(error) {
|
|
2847
|
+
return error instanceof CliExitCodeError && error.envelope?.code === "daemon_unreachable";
|
|
2848
|
+
}
|
|
2849
|
+
async function resolveMruEntryName() {
|
|
2850
|
+
if (requireExplicitConnection()) {
|
|
2851
|
+
throw new CliExitCodeError(
|
|
2852
|
+
EXIT_CODES.USAGE,
|
|
2853
|
+
"servers/show requires an entry name in non-interactive mode. Pass one (see mcpdo servers/list).",
|
|
2854
|
+
{ code: "server_name_required" }
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
let connections = [];
|
|
2858
|
+
try {
|
|
2859
|
+
const result = await callDaemon(
|
|
2860
|
+
"connections/list",
|
|
2861
|
+
{}
|
|
2862
|
+
);
|
|
2863
|
+
connections = result.connections;
|
|
2864
|
+
} catch (error) {
|
|
2865
|
+
if (!isDaemonUnreachable(error)) throw error;
|
|
2866
|
+
}
|
|
2867
|
+
const mru = connections.find((c) => c.isMru);
|
|
2868
|
+
if (!mru) {
|
|
2869
|
+
throw new CliExitCodeError(
|
|
2870
|
+
EXIT_CODES.USAGE,
|
|
2871
|
+
"No entry name given and there is no most-recently-used connection to infer one from. Pass a catalog entry name (see mcpdo servers/list).",
|
|
2872
|
+
{ code: "server_name_required" }
|
|
2873
|
+
);
|
|
2874
|
+
}
|
|
2875
|
+
return mru.name;
|
|
2876
|
+
}
|
|
2877
|
+
function describeServerShowNotFound(error, entryName, inferredFromMru, source) {
|
|
2878
|
+
if (!(error instanceof Error) || !error.message.startsWith(`Server '${entryName}' not found`)) {
|
|
2879
|
+
return error;
|
|
2880
|
+
}
|
|
2881
|
+
const where = source ? `${source.kind} ${source.path}` : "the resolved server list";
|
|
2882
|
+
if (inferredFromMru) {
|
|
2883
|
+
return new CliExitCodeError(
|
|
2884
|
+
EXIT_CODES.USAGE,
|
|
2885
|
+
`The most-recently-used connection '${entryName}' has no entry in ${where}. Connections and catalog entries are separate \u2014 it may have been connected ad-hoc or from a different catalog. Pass an entry name (see mcpdo servers/list).`,
|
|
2886
|
+
{ code: "server_not_found" }
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
return new CliExitCodeError(EXIT_CODES.USAGE, `${error.message} (${where})`, {
|
|
2890
|
+
code: "server_not_found"
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
function isCommanderDisplayOnly(error) {
|
|
2894
|
+
if (error == null || typeof error !== "object") return false;
|
|
2895
|
+
const code2 = error.code;
|
|
2896
|
+
return code2 === "commander.help" || code2 === "commander.helpDisplayed" || code2 === "commander.version";
|
|
2897
|
+
}
|
|
2898
|
+
function outOpts(opts) {
|
|
2899
|
+
return {
|
|
2900
|
+
format: opts.format,
|
|
2901
|
+
style: styleFromOpts({ plain: opts.plain === true, format: opts.format }),
|
|
2902
|
+
// Matches dispatch.ts: a human is "present" only when output is text and a
|
|
2903
|
+
// TTY is attached. `--format json` or no TTY means an agent is reading, so
|
|
2904
|
+
// sign-in prompts switch to relay framing (see ConnectionWriteOpts).
|
|
2905
|
+
interactive: opts.format !== "json" && (process.stdin.isTTY === true || process.stderr.isTTY === true)
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
var validLogLevels = Object.values(LoggingLevelSchema.enum);
|
|
2909
|
+
function expandConnAlias(argv) {
|
|
2910
|
+
const sep = argv.indexOf("--");
|
|
2911
|
+
const end = sep === -1 ? argv.length : sep;
|
|
2912
|
+
return argv.map(
|
|
2913
|
+
(arg, i) => i >= end ? arg : arg === "--conn" ? "--connection" : arg.startsWith("--conn=") ? `--connection=${arg.slice("--conn=".length)}` : arg
|
|
2914
|
+
);
|
|
2915
|
+
}
|
|
2916
|
+
async function surfaceSecretStorageAtConnect() {
|
|
2917
|
+
const info = await getSecretStorageInfo();
|
|
2918
|
+
warnAboutSecretStorage(info, { force: true });
|
|
2919
|
+
if (info.kind === "memory") {
|
|
2920
|
+
process.stderr.write(
|
|
2921
|
+
`[mcpdo] MCP_INSPECTOR_SECRET_STORE=memory keeps secrets only in this process's memory. mcpdo runs the daemon and the OAuth sign-in helper as separate processes, so a token saved by one is invisible to the others and sign-in cannot complete (you will see a credential-refresh failure after signing in). Use "file" or "keyring" to persist across mcpdo's processes.
|
|
2922
|
+
`
|
|
2923
|
+
);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
function pinStdioConfigToCaller(config) {
|
|
2927
|
+
if (config.type === "sse" || config.type === "streamable-http") return config;
|
|
2928
|
+
const resolved = resolveCommandPath(config.command);
|
|
2929
|
+
return {
|
|
2930
|
+
...config,
|
|
2931
|
+
cwd: path4.resolve(config.cwd ?? process.cwd()),
|
|
2932
|
+
...resolved !== config.command ? { command: resolved } : {},
|
|
2933
|
+
// The SDK transport spawns with {...getDefaultEnvironment(), ...env}
|
|
2934
|
+
// evaluated in the DAEMON process; snapshotting the same default set
|
|
2935
|
+
// here makes those fallbacks the caller's.
|
|
2936
|
+
env: { ...getDefaultEnvironment(), ...config.env }
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
async function runMcp(argv) {
|
|
2940
|
+
const raw = argv ?? process.argv;
|
|
2941
|
+
const { argv: rewritten, connectionFromAt } = hoistAtConnection(
|
|
2942
|
+
expandConnAlias(raw)
|
|
2943
|
+
);
|
|
2944
|
+
const program = new Command();
|
|
2945
|
+
program.configureOutput({ outputError: () => {
|
|
2946
|
+
} });
|
|
2947
|
+
program.exitOverride((err) => {
|
|
2948
|
+
if (isCommanderDisplayOnly(err)) throw err;
|
|
2949
|
+
if (err.exitCode !== 0) {
|
|
2950
|
+
throw new CliExitCodeError(
|
|
2951
|
+
EXIT_CODES.USAGE,
|
|
2952
|
+
err.message.replace(/^error: /, ""),
|
|
2953
|
+
{ code: "usage" }
|
|
2954
|
+
);
|
|
2955
|
+
}
|
|
2956
|
+
});
|
|
2957
|
+
program.name("mcpdo").description(
|
|
2958
|
+
"MCP Inspector connection CLI \u2014 connect once, run many commands against a named connection.\n\nAgent skill for mcpdo: install with `npx skills add modelcontextprotocol/inspector --skill mcpdo`, or see `agent-help` below."
|
|
2959
|
+
).helpOption("-h, --help", "Display help for command").helpCommand("help [command]", "Display help for command").version(readInspectorVersion(import.meta.url), "--version").option(
|
|
2960
|
+
"--format <format>",
|
|
2961
|
+
"Output format: text (default; human-readable) or json (pretty-printed)",
|
|
2962
|
+
(v) => {
|
|
2963
|
+
if (v !== "text" && v !== "json") {
|
|
2964
|
+
throw new Error(`--format must be 'text' or 'json'.`);
|
|
2965
|
+
}
|
|
2966
|
+
return v;
|
|
2967
|
+
}
|
|
2968
|
+
).option(
|
|
2969
|
+
"--plain",
|
|
2970
|
+
"Disable ANSI styling (color, bold/dim, hyperlinks) in human text output"
|
|
2971
|
+
).option(
|
|
2972
|
+
"--connection <name>",
|
|
2973
|
+
"Connection name (without required @). Overrides MRU / positional @name. `--conn` is a supported shorthand."
|
|
2974
|
+
).option(
|
|
2975
|
+
"--catalog <path>",
|
|
2976
|
+
"Writable catalog file (default: ~/.mcp-inspector/mcp.json or MCP_CATALOG_PATH)"
|
|
2977
|
+
).option(
|
|
2978
|
+
"--config <path>",
|
|
2979
|
+
"Read-only connection config file (never written or seeded)"
|
|
2980
|
+
).option(
|
|
2981
|
+
"--stored-auth-only",
|
|
2982
|
+
"Never start interactive OAuth; use the shared store if present, otherwise fail."
|
|
2983
|
+
);
|
|
2984
|
+
if (connectionFromAt) {
|
|
2985
|
+
program.setOptionValue("connection", connectionFromAt);
|
|
2986
|
+
}
|
|
2987
|
+
program.command("servers/list").description(
|
|
2988
|
+
"List catalog/config server entries (marks live connections when the daemon is running; no MCP connection)"
|
|
2989
|
+
).action(async () => {
|
|
2990
|
+
const opts = program.opts();
|
|
2991
|
+
const envCatalog = process.env.MCP_CATALOG_PATH;
|
|
2992
|
+
const serverOptions = {
|
|
2993
|
+
catalogPath: opts.catalog?.trim() || envCatalog,
|
|
2994
|
+
configPath: opts.config?.trim() || void 0
|
|
2995
|
+
};
|
|
2996
|
+
const entries = await listServerEntries(serverOptions);
|
|
2997
|
+
const source = resolveServerListSource(serverOptions);
|
|
2998
|
+
let connections = [];
|
|
2999
|
+
try {
|
|
3000
|
+
const result = await callDaemon(
|
|
3001
|
+
"connections/list",
|
|
3002
|
+
{}
|
|
3003
|
+
);
|
|
3004
|
+
connections = result.connections;
|
|
3005
|
+
} catch (error) {
|
|
3006
|
+
if (!isDaemonUnreachable(error)) throw error;
|
|
3007
|
+
}
|
|
3008
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3009
|
+
kind: "servers/list",
|
|
3010
|
+
servers: annotateServerEntriesWithConnections(entries, connections),
|
|
3011
|
+
...source && { source }
|
|
3012
|
+
});
|
|
3013
|
+
});
|
|
3014
|
+
program.command("servers/show").description(
|
|
3015
|
+
"Show one catalog/config entry in detail (no MCP connection; secrets redacted)"
|
|
3016
|
+
).argument(
|
|
3017
|
+
"[name]",
|
|
3018
|
+
"Catalog entry name (defaults to the MRU connection's entry on an interactive TTY)"
|
|
3019
|
+
).action(async (name) => {
|
|
3020
|
+
const opts = program.opts();
|
|
3021
|
+
const envCatalog = process.env.MCP_CATALOG_PATH;
|
|
3022
|
+
const serverOptions = {
|
|
3023
|
+
catalogPath: opts.catalog?.trim() || envCatalog,
|
|
3024
|
+
configPath: opts.config?.trim() || void 0
|
|
3025
|
+
};
|
|
3026
|
+
const explicitName = stripAt(name?.trim() || void 0);
|
|
3027
|
+
const entryName = explicitName ?? await resolveMruEntryName();
|
|
3028
|
+
const source = resolveServerListSource(serverOptions);
|
|
3029
|
+
let entry;
|
|
3030
|
+
try {
|
|
3031
|
+
entry = await showServerEntry(entryName, serverOptions);
|
|
3032
|
+
} catch (error) {
|
|
3033
|
+
throw describeServerShowNotFound(
|
|
3034
|
+
error,
|
|
3035
|
+
entryName,
|
|
3036
|
+
explicitName === void 0,
|
|
3037
|
+
source
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
3040
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3041
|
+
kind: "servers/show",
|
|
3042
|
+
server: entry,
|
|
3043
|
+
...source && { source }
|
|
3044
|
+
});
|
|
3045
|
+
});
|
|
3046
|
+
registerConnect(program);
|
|
3047
|
+
registerConnectionAdmin(program);
|
|
3048
|
+
registerAuthCommands(program);
|
|
3049
|
+
registerRpcCommands(program);
|
|
3050
|
+
registerElicitationCommands(program);
|
|
3051
|
+
registerDaemonCommands(program);
|
|
3052
|
+
registerPrivateCommand(program);
|
|
3053
|
+
registerAgentHelpCommand(program);
|
|
3054
|
+
try {
|
|
3055
|
+
await program.parseAsync(rewritten);
|
|
3056
|
+
} catch (error) {
|
|
3057
|
+
if (isCommanderDisplayOnly(error)) return;
|
|
3058
|
+
throw error;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
function registerConnect(program) {
|
|
3062
|
+
program.command("connect").description(
|
|
3063
|
+
"Connect a catalog entry or ad-hoc target as a named connection"
|
|
3064
|
+
).argument(
|
|
3065
|
+
"[target...]",
|
|
3066
|
+
"Catalog entry name, or command/URL (use -- for command args). A single bare word is a catalog name; a URL, a path (contains / or starts with . or ~), multiple tokens, or --transport force an ad-hoc target."
|
|
3067
|
+
).option("--server <name>", "Server name from catalog/config").option(
|
|
3068
|
+
"-e <env>",
|
|
3069
|
+
"Environment variables for the server (KEY=VALUE)",
|
|
3070
|
+
parseKeyValuePair,
|
|
3071
|
+
{}
|
|
3072
|
+
).option("--cwd <path>", "Working directory for stdio server process").option(
|
|
3073
|
+
"--transport <type>",
|
|
3074
|
+
"Transport type (sse, http, or stdio)",
|
|
3075
|
+
(value) => {
|
|
3076
|
+
const valid = ["sse", "http", "stdio"];
|
|
3077
|
+
if (!valid.includes(value)) {
|
|
3078
|
+
throw new Error(`Invalid transport type: ${value}`);
|
|
3079
|
+
}
|
|
3080
|
+
return value;
|
|
3081
|
+
}
|
|
3082
|
+
).option("--server-url <url>", "Server URL for SSE/HTTP transport").option(
|
|
3083
|
+
"--header <headers...>",
|
|
3084
|
+
'HTTP headers as "HeaderName: Value" pairs',
|
|
3085
|
+
parseHeaderPair,
|
|
3086
|
+
{}
|
|
3087
|
+
).option(
|
|
3088
|
+
"--connect-timeout <ms>",
|
|
3089
|
+
`Connection timeout in ms (default ${DEFAULT_CONNECT_TIMEOUT_MS} for ad-hoc)`,
|
|
3090
|
+
(v) => {
|
|
3091
|
+
const n = Number(v);
|
|
3092
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
3093
|
+
throw new Error(`--connect-timeout must be a non-negative number.`);
|
|
3094
|
+
}
|
|
3095
|
+
return n;
|
|
3096
|
+
}
|
|
3097
|
+
).option(
|
|
3098
|
+
"--era <era>",
|
|
3099
|
+
"Protocol era to negotiate: legacy (default), auto, or modern. Overrides the catalog/config entry's protocolEra; the only way to set it for an ad-hoc target, which has no config entry of its own.",
|
|
3100
|
+
(value) => {
|
|
3101
|
+
const valid = ["legacy", "auto", "modern"];
|
|
3102
|
+
if (!valid.includes(value)) {
|
|
3103
|
+
throw new Error(
|
|
3104
|
+
`Invalid --era: ${value}. Use legacy, auto, or modern.`
|
|
3105
|
+
);
|
|
3106
|
+
}
|
|
3107
|
+
return value;
|
|
3108
|
+
}
|
|
3109
|
+
).option(
|
|
3110
|
+
"-r, --relogin",
|
|
3111
|
+
"Ignore stored OAuth for this connect (HTTP/SSE URL keys only); interactive login runs only if the server requires auth. No-op for stdio / servers with no stored entry"
|
|
3112
|
+
).option(
|
|
3113
|
+
"--elicit <mode>",
|
|
3114
|
+
"Elicitation capability to advertise: off, url, form, or both (default). Overrides the catalog/config entry's elicitCapability; the only way to set it for an ad-hoc target, which has no config entry of its own. Use off when the caller of mcpdo can't handle an elicitation request, so the server sees no elicitation capability and can fall back on its own.",
|
|
3115
|
+
(value) => {
|
|
3116
|
+
const valid = ["off", "url", "form", "both"];
|
|
3117
|
+
if (!valid.includes(value)) {
|
|
3118
|
+
throw new Error(
|
|
3119
|
+
`Invalid --elicit: ${value}. Use off, url, form, or both.`
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
return value;
|
|
3123
|
+
}
|
|
3124
|
+
).option(
|
|
3125
|
+
"--ema",
|
|
3126
|
+
"Treat the server as enterprise-managed (EMA): mint tokens from the signed-in enterprise IdP session instead of standard OAuth. Overrides the catalog/config entry's oauth.enterpriseManaged. Requires install-level IdP config (see auth/ema-status) and per-server OAuth client id/secret from the catalog entry, so it cannot be used with an ad-hoc target."
|
|
3127
|
+
).action(async (target, cmdOpts) => {
|
|
3128
|
+
const opts = program.opts();
|
|
3129
|
+
const { name: positionalConnection, rest } = splitConnectionTarget(target);
|
|
3130
|
+
const connectionName = stripAt(opts.connection) ?? positionalConnection ?? cmdOpts.server?.trim() ?? rest[0];
|
|
3131
|
+
if (!connectionName) {
|
|
3132
|
+
throw new CliExitCodeError(
|
|
3133
|
+
EXIT_CODES.USAGE,
|
|
3134
|
+
"connect requires a catalog entry name, --server <name>, or an ad-hoc target.",
|
|
3135
|
+
{ code: "usage" }
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
3138
|
+
await surfaceSecretStorageAtConnect();
|
|
3139
|
+
const relogin = cmdOpts.relogin === true;
|
|
3140
|
+
if (relogin && opts.storedAuthOnly) {
|
|
3141
|
+
throw new CliExitCodeError(
|
|
3142
|
+
EXIT_CODES.USAGE,
|
|
3143
|
+
"--relogin cannot be combined with --stored-auth-only",
|
|
3144
|
+
{ code: "usage" }
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
const adHoc = rest.length > 1 || Boolean(cmdOpts.transport) || Boolean(cmdOpts.serverUrl?.trim()) || rest.length === 1 && (looksLikeUrl(rest[0]) || looksLikePath(rest[0]));
|
|
3148
|
+
if (cmdOpts.ema === true && adHoc) {
|
|
3149
|
+
throw new CliExitCodeError(
|
|
3150
|
+
EXIT_CODES.USAGE,
|
|
3151
|
+
"--ema cannot be used with an ad-hoc target: EMA requires per-server OAuth client id/secret from a catalog entry. Add the server to a catalog with oauth.clientId and oauth.clientSecret (and oauth.enterpriseManaged), then connect by entry name.",
|
|
3152
|
+
{ code: "usage" }
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
const envCatalog = adHoc ? void 0 : process.env.MCP_CATALOG_PATH;
|
|
3156
|
+
const serverOptions = {
|
|
3157
|
+
catalogPath: opts.catalog?.trim() || envCatalog,
|
|
3158
|
+
configPath: opts.config?.trim() || void 0,
|
|
3159
|
+
target: adHoc ? rest.length > 0 ? rest : void 0 : void 0,
|
|
3160
|
+
transport: cmdOpts.transport,
|
|
3161
|
+
serverUrl: cmdOpts.serverUrl,
|
|
3162
|
+
// Resolve --cwd against the CALLER's working directory. The daemon
|
|
3163
|
+
// that spawns the stdio server inherits an unrelated cwd (see
|
|
3164
|
+
// daemon/run.ts), so a relative --cwd must be pinned here.
|
|
3165
|
+
cwd: cmdOpts.cwd ? path4.resolve(cmdOpts.cwd) : void 0,
|
|
3166
|
+
env: cmdOpts.e,
|
|
3167
|
+
headers: cmdOpts.header
|
|
3168
|
+
};
|
|
3169
|
+
const selectName = adHoc ? void 0 : cmdOpts.server?.trim() ?? rest[0];
|
|
3170
|
+
const entries = await loadServerEntries(serverOptions);
|
|
3171
|
+
const selected = selectServerEntry(entries, selectName);
|
|
3172
|
+
const serverConfig = pinStdioConfigToCaller(selected.config);
|
|
3173
|
+
const serverSettings = withEmaOverride(
|
|
3174
|
+
withElicitOverride(
|
|
3175
|
+
withEraOverride(
|
|
3176
|
+
withConnectTimeout(
|
|
3177
|
+
selected.settings,
|
|
3178
|
+
cmdOpts.connectTimeout ?? (adHoc ? DEFAULT_CONNECT_TIMEOUT_MS : void 0)
|
|
3179
|
+
),
|
|
3180
|
+
cmdOpts.era
|
|
3181
|
+
),
|
|
3182
|
+
cmdOpts.elicit
|
|
3183
|
+
),
|
|
3184
|
+
cmdOpts.ema === true ? true : void 0
|
|
3185
|
+
);
|
|
3186
|
+
const { detail } = summarizeServerConfig(serverConfig);
|
|
3187
|
+
const name = stripAt(connectionName);
|
|
3188
|
+
if (relogin && "url" in serverConfig && serverConfig.url) {
|
|
3189
|
+
await clearStoredAuthForRelogin(serverConfig.url);
|
|
3190
|
+
}
|
|
3191
|
+
const { socketPath } = await ensureDaemon();
|
|
3192
|
+
const connectParams = {
|
|
3193
|
+
name,
|
|
3194
|
+
serverConfig,
|
|
3195
|
+
serverSettings,
|
|
3196
|
+
serverIdentity: detail
|
|
3197
|
+
};
|
|
3198
|
+
let result;
|
|
3199
|
+
try {
|
|
3200
|
+
result = await callDaemon("connect", connectParams, {
|
|
3201
|
+
socketPath,
|
|
3202
|
+
// The daemon enforces the configured connect timeout (which may be
|
|
3203
|
+
// 0 = unlimited or exceed 60s); no fixed local deadline.
|
|
3204
|
+
timeoutMs: 0
|
|
3205
|
+
});
|
|
3206
|
+
} catch (error) {
|
|
3207
|
+
if (!(error instanceof CliExitCodeError) || error.envelope?.code !== "auth_required") {
|
|
3208
|
+
throw error;
|
|
3209
|
+
}
|
|
3210
|
+
if (opts.storedAuthOnly) {
|
|
3211
|
+
throw error;
|
|
3212
|
+
}
|
|
3213
|
+
const humanPresent = process.stdin.isTTY === true || process.stderr.isTTY === true;
|
|
3214
|
+
if (!humanPresent && !isCliAutoOpenForced()) {
|
|
3215
|
+
const authUrl = await obtainPendingAuthUrl(
|
|
3216
|
+
serverConfig,
|
|
3217
|
+
serverSettings
|
|
3218
|
+
);
|
|
3219
|
+
const { socketPath: pendingSocketPath } = await ensureDaemon();
|
|
3220
|
+
const pending = await callDaemon(
|
|
3221
|
+
"connect",
|
|
3222
|
+
{ ...connectParams, pendingOnAuthRequired: true },
|
|
3223
|
+
{ socketPath: pendingSocketPath, timeoutMs: 0 }
|
|
3224
|
+
);
|
|
3225
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3226
|
+
kind: "connection",
|
|
3227
|
+
connection: pending,
|
|
3228
|
+
...pending.pendingAuth === true && { authUrl }
|
|
3229
|
+
});
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
await authorizeInFrontend(serverConfig, serverSettings, {
|
|
3233
|
+
storedAuthOnly: false
|
|
3234
|
+
});
|
|
3235
|
+
const { socketPath: freshSocketPath } = await ensureDaemon();
|
|
3236
|
+
result = await callDaemon("connect", connectParams, {
|
|
3237
|
+
socketPath: freshSocketPath,
|
|
3238
|
+
timeoutMs: 0
|
|
3239
|
+
});
|
|
3240
|
+
}
|
|
3241
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3242
|
+
kind: "connection",
|
|
3243
|
+
connection: result
|
|
3244
|
+
});
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
async function collectAuthNameIndex(opts) {
|
|
3248
|
+
const envCatalog = process.env.MCP_CATALOG_PATH;
|
|
3249
|
+
const serverOptions = {
|
|
3250
|
+
catalogPath: opts.catalog?.trim() || envCatalog,
|
|
3251
|
+
configPath: opts.config?.trim() || void 0
|
|
3252
|
+
};
|
|
3253
|
+
const entries = await listServerEntries(serverOptions);
|
|
3254
|
+
let connections = [];
|
|
3255
|
+
try {
|
|
3256
|
+
const result = await callDaemon(
|
|
3257
|
+
"connections/list",
|
|
3258
|
+
{}
|
|
3259
|
+
);
|
|
3260
|
+
connections = result.connections;
|
|
3261
|
+
} catch (error) {
|
|
3262
|
+
if (!isDaemonUnreachable(error)) throw error;
|
|
3263
|
+
}
|
|
3264
|
+
return buildAuthNameIndex(entries, connections);
|
|
3265
|
+
}
|
|
3266
|
+
function registerAuthCommands(program) {
|
|
3267
|
+
program.command(AUTH_HELPER_COMMAND, { hidden: true }).description("Internal: complete an OAuth sign-in (params JSON on stdin)").action(async () => {
|
|
3268
|
+
await runAuthHelper();
|
|
3269
|
+
});
|
|
3270
|
+
program.command(EMA_LOGIN_HELPER_COMMAND, { hidden: true }).description("Internal: complete an EMA IdP sign-in").action(async () => {
|
|
3271
|
+
await runEmaLoginHelper();
|
|
3272
|
+
});
|
|
3273
|
+
program.command("auth/list").description(
|
|
3274
|
+
"List servers in the shared OAuth store, annotated with catalog/connection names"
|
|
3275
|
+
).action(async () => {
|
|
3276
|
+
const opts = program.opts();
|
|
3277
|
+
const list = await listStoredAuth();
|
|
3278
|
+
const index2 = await collectAuthNameIndex(opts);
|
|
3279
|
+
const servers = list.servers.map((s) => {
|
|
3280
|
+
const issuer = parseIdpOAuthStorageKey(s.url);
|
|
3281
|
+
if (issuer !== null) {
|
|
3282
|
+
return { ...s, idp: true, issuer };
|
|
3283
|
+
}
|
|
3284
|
+
const refs = index2.urlToNames.get(s.url) ?? [];
|
|
3285
|
+
return {
|
|
3286
|
+
...s,
|
|
3287
|
+
...refs.length > 0 && { knownAs: refs.map((r) => r.name) },
|
|
3288
|
+
...refs.some((r) => r.isLive) && { live: true }
|
|
3289
|
+
};
|
|
3290
|
+
});
|
|
3291
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3292
|
+
kind: "auth/list",
|
|
3293
|
+
list: { oauthStatePath: list.oauthStatePath, servers }
|
|
3294
|
+
});
|
|
3295
|
+
});
|
|
3296
|
+
program.command("auth/clear").description(
|
|
3297
|
+
"Clear stored OAuth state for one server (URL or catalog/connection name from auth/list) or all entries"
|
|
3298
|
+
).argument("[key]", "Server URL or catalog/connection name from auth/list").option("--all", "Clear every stored OAuth server entry").option("--yes", "Skip confirmation when using --all").action(async (key, cmdOpts) => {
|
|
3299
|
+
const opts = program.opts();
|
|
3300
|
+
const all = cmdOpts.all === true;
|
|
3301
|
+
if (all && key?.trim()) {
|
|
3302
|
+
throw new CliExitCodeError(
|
|
3303
|
+
EXIT_CODES.USAGE,
|
|
3304
|
+
"auth/clear: pass a key or --all, not both",
|
|
3305
|
+
{ code: "usage" }
|
|
3306
|
+
);
|
|
3307
|
+
}
|
|
3308
|
+
if (!all && !key?.trim()) {
|
|
3309
|
+
throw new CliExitCodeError(
|
|
3310
|
+
EXIT_CODES.USAGE,
|
|
3311
|
+
"auth/clear requires a server URL key (from auth/list) or --all",
|
|
3312
|
+
{ code: "usage" }
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
if (all) {
|
|
3316
|
+
if (!cmdOpts.yes) {
|
|
3317
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3318
|
+
throw new CliExitCodeError(
|
|
3319
|
+
EXIT_CODES.USAGE,
|
|
3320
|
+
"auth/clear --all requires --yes in non-interactive mode",
|
|
3321
|
+
{ code: "usage" }
|
|
3322
|
+
);
|
|
3323
|
+
}
|
|
3324
|
+
const rl = createInterface2({
|
|
3325
|
+
input: process.stdin,
|
|
3326
|
+
output: process.stderr
|
|
3327
|
+
});
|
|
3328
|
+
try {
|
|
3329
|
+
const answer = await rl.question(
|
|
3330
|
+
"Clear ALL stored OAuth credentials? [y/N] "
|
|
3331
|
+
);
|
|
3332
|
+
const ok = answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
3333
|
+
if (!ok) {
|
|
3334
|
+
throw new CliExitCodeError(
|
|
3335
|
+
EXIT_CODES.USAGE,
|
|
3336
|
+
"auth/clear --all cancelled",
|
|
3337
|
+
{ code: "usage" }
|
|
3338
|
+
);
|
|
3339
|
+
}
|
|
3340
|
+
} finally {
|
|
3341
|
+
rl.close();
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
const result = await clearAllStoredAuth();
|
|
3345
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3346
|
+
kind: "auth/clear",
|
|
3347
|
+
result: { all: true, cleared: result.cleared }
|
|
3348
|
+
});
|
|
3349
|
+
return;
|
|
3350
|
+
}
|
|
3351
|
+
const trimmed = key.trim();
|
|
3352
|
+
const storedKeys = (await listStoredAuth()).servers.map((s) => s.url);
|
|
3353
|
+
if (storedKeys.includes(trimmed)) {
|
|
3354
|
+
const result = await clearStoredAuth(trimmed);
|
|
3355
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3356
|
+
kind: "auth/clear",
|
|
3357
|
+
result: { url: result.url }
|
|
3358
|
+
});
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
3362
|
+
const idpKey = idpOAuthStorageKey(trimmed);
|
|
3363
|
+
const target = storedKeys.includes(idpKey) && !storedKeys.includes(normalizeServerUrl(trimmed)) ? idpKey : trimmed;
|
|
3364
|
+
const result = await clearStoredAuth(target);
|
|
3365
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3366
|
+
kind: "auth/clear",
|
|
3367
|
+
result: { url: result.url }
|
|
3368
|
+
});
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
const index2 = await collectAuthNameIndex(opts);
|
|
3372
|
+
const resolution = resolveFriendlyName(trimmed, index2);
|
|
3373
|
+
switch (resolution.kind) {
|
|
3374
|
+
case "no-url":
|
|
3375
|
+
throw new CliExitCodeError(
|
|
3376
|
+
EXIT_CODES.USAGE,
|
|
3377
|
+
`'${trimmed}' is a stdio server \u2014 it has no stored OAuth entry to clear.`,
|
|
3378
|
+
{ code: "usage" }
|
|
3379
|
+
);
|
|
3380
|
+
case "ambiguous":
|
|
3381
|
+
throw new CliExitCodeError(
|
|
3382
|
+
EXIT_CODES.USAGE,
|
|
3383
|
+
`'${trimmed}' maps to multiple server URLs (${resolution.urls.join(
|
|
3384
|
+
", "
|
|
3385
|
+
)}). Pass the explicit URL from auth/list.`,
|
|
3386
|
+
{ code: "usage" }
|
|
3387
|
+
);
|
|
3388
|
+
case "unknown":
|
|
3389
|
+
throw new CliExitCodeError(
|
|
3390
|
+
EXIT_CODES.USAGE,
|
|
3391
|
+
`No server named '${trimmed}'. Use auth/list or servers/list to see names, or pass a URL.`,
|
|
3392
|
+
{ code: "usage" }
|
|
3393
|
+
);
|
|
3394
|
+
case "url": {
|
|
3395
|
+
const result = await clearStoredAuth(resolution.url);
|
|
3396
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3397
|
+
kind: "auth/clear",
|
|
3398
|
+
result: { url: result.url, clearedByName: trimmed }
|
|
3399
|
+
});
|
|
3400
|
+
return;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
});
|
|
3404
|
+
program.command("auth/ema-status").description(
|
|
3405
|
+
"Show enterprise-managed auth (EMA) configuration and IdP login state"
|
|
3406
|
+
).action(async () => {
|
|
3407
|
+
const opts = program.opts();
|
|
3408
|
+
const status = await getEmaStatus();
|
|
3409
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3410
|
+
kind: "auth/ema-status",
|
|
3411
|
+
status
|
|
3412
|
+
});
|
|
3413
|
+
});
|
|
3414
|
+
program.command("auth/ema-login").description(
|
|
3415
|
+
"Sign in to the enterprise IdP (EMA); subsequent connects to EMA servers mint tokens silently from this connection"
|
|
3416
|
+
).option(
|
|
3417
|
+
"-r, --relogin",
|
|
3418
|
+
"Clear the existing IdP session (and EMA server tokens) and sign in fresh"
|
|
3419
|
+
).action(async (cmdOpts) => {
|
|
3420
|
+
const opts = program.opts();
|
|
3421
|
+
const relogin = cmdOpts.relogin === true;
|
|
3422
|
+
const humanPresent = process.stdin.isTTY === true || process.stderr.isTTY === true;
|
|
3423
|
+
const result = !humanPresent && !isCliAutoOpenForced() ? await startPendingEmaLogin({ relogin }) : await emaLogin({ relogin });
|
|
3424
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3425
|
+
kind: "auth/ema-login",
|
|
3426
|
+
result
|
|
3427
|
+
});
|
|
3428
|
+
});
|
|
3429
|
+
program.command("auth/ema-logout").description(
|
|
3430
|
+
"Sign out of the enterprise IdP locally and clear EMA-minted server tokens (prints the IdP end-session URL when available)"
|
|
3431
|
+
).action(async () => {
|
|
3432
|
+
const opts = program.opts();
|
|
3433
|
+
const result = await emaLogout();
|
|
3434
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3435
|
+
kind: "auth/ema-logout",
|
|
3436
|
+
result
|
|
3437
|
+
});
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
3440
|
+
function registerConnectionAdmin(program) {
|
|
3441
|
+
program.command("disconnect").description("Disconnect a connection (MRU when omitted on a TTY)").argument("[connection]", "Optional @name / name to disconnect").option(
|
|
3442
|
+
"-c, --clear-auth",
|
|
3443
|
+
"Also clear this server's stored OAuth tokens so the next connect re-triggers sign-in (HTTP/SSE URL keys only; no-op for stdio / no stored entry)"
|
|
3444
|
+
).action(async (connectionArg, cmdOpts) => {
|
|
3445
|
+
const opts = program.opts();
|
|
3446
|
+
const name = stripAt(opts.connection) ?? stripAt(connectionArg);
|
|
3447
|
+
const { socketPath } = await ensureDaemon();
|
|
3448
|
+
const result = await callDaemon(
|
|
3449
|
+
"disconnect",
|
|
3450
|
+
{
|
|
3451
|
+
name,
|
|
3452
|
+
requireExplicit: requireExplicitConnection()
|
|
3453
|
+
},
|
|
3454
|
+
{ socketPath }
|
|
3455
|
+
);
|
|
3456
|
+
let clearedAuthUrl;
|
|
3457
|
+
if (cmdOpts.clearAuth === true && result.serverUrl) {
|
|
3458
|
+
await clearStoredAuthForRelogin(result.serverUrl);
|
|
3459
|
+
clearedAuthUrl = result.serverUrl;
|
|
3460
|
+
}
|
|
3461
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3462
|
+
kind: "disconnect",
|
|
3463
|
+
name: result.name,
|
|
3464
|
+
...clearedAuthUrl && { clearedAuthUrl }
|
|
3465
|
+
});
|
|
3466
|
+
});
|
|
3467
|
+
program.command("connections/list").description("List open connections (marks MRU); does not start the daemon").action(async () => {
|
|
3468
|
+
const opts = program.opts();
|
|
3469
|
+
try {
|
|
3470
|
+
const result = await callDaemon(
|
|
3471
|
+
"connections/list",
|
|
3472
|
+
{}
|
|
3473
|
+
);
|
|
3474
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3475
|
+
kind: "connections/list",
|
|
3476
|
+
connections: result.connections
|
|
3477
|
+
});
|
|
3478
|
+
} catch (error) {
|
|
3479
|
+
if (isDaemonUnreachable(error)) {
|
|
3480
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3481
|
+
kind: "connections/list",
|
|
3482
|
+
connections: []
|
|
3483
|
+
});
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
throw error;
|
|
3487
|
+
}
|
|
3488
|
+
});
|
|
3489
|
+
program.command("connections/use").description("Set the MRU connection without an MCP RPC").argument("<connection>", "Connection @name / name").action(async (connectionArg) => {
|
|
3490
|
+
const opts = program.opts();
|
|
3491
|
+
const name = stripAt(opts.connection) ?? stripAt(connectionArg);
|
|
3492
|
+
if (!name) {
|
|
3493
|
+
throw new CliExitCodeError(
|
|
3494
|
+
EXIT_CODES.USAGE,
|
|
3495
|
+
"connections/use requires a connection name",
|
|
3496
|
+
{ code: "usage" }
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
const { socketPath } = await ensureDaemon();
|
|
3500
|
+
const result = await callDaemon(
|
|
3501
|
+
"connections/use",
|
|
3502
|
+
{ name },
|
|
3503
|
+
{ socketPath }
|
|
3504
|
+
);
|
|
3505
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3506
|
+
kind: "connection",
|
|
3507
|
+
connection: result
|
|
3508
|
+
});
|
|
3509
|
+
});
|
|
3510
|
+
program.command("connections/show").description(
|
|
3511
|
+
"Show connection + connection details: server info, capabilities, negotiated protocol era (defaults to MRU)"
|
|
3512
|
+
).argument("[connection]", "Connection @name / name (defaults to MRU)").action(async (connectionArg) => {
|
|
3513
|
+
const opts = program.opts();
|
|
3514
|
+
const name = stripAt(opts.connection) ?? stripAt(connectionArg);
|
|
3515
|
+
const { socketPath } = await ensureDaemon();
|
|
3516
|
+
const result = await callDaemon(
|
|
3517
|
+
"connections/show",
|
|
3518
|
+
{ name, requireExplicit: requireExplicitConnection() },
|
|
3519
|
+
{ socketPath }
|
|
3520
|
+
);
|
|
3521
|
+
const humanPresent = process.stdin.isTTY === true || process.stderr.isTTY === true;
|
|
3522
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3523
|
+
kind: "connection",
|
|
3524
|
+
connection: result,
|
|
3525
|
+
...!humanPresent && result.authUrl && { authUrl: result.authUrl }
|
|
3526
|
+
});
|
|
3527
|
+
});
|
|
3528
|
+
}
|
|
3529
|
+
function registerDaemonCommands(program) {
|
|
3530
|
+
const daemon = program.command("daemon").description("Daemon control");
|
|
3531
|
+
daemon.command("status").description("Show daemon pid, socket, and connections (does not start it)").action(async () => {
|
|
3532
|
+
const opts = program.opts();
|
|
3533
|
+
try {
|
|
3534
|
+
const result = await callDaemon("daemon/status", {});
|
|
3535
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3536
|
+
kind: "daemon/status",
|
|
3537
|
+
status: result
|
|
3538
|
+
});
|
|
3539
|
+
} catch (error) {
|
|
3540
|
+
if (isDaemonUnreachable(error)) {
|
|
3541
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3542
|
+
kind: "daemon/status",
|
|
3543
|
+
status: {
|
|
3544
|
+
running: false,
|
|
3545
|
+
message: "Daemon is not running."
|
|
3546
|
+
}
|
|
3547
|
+
});
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
throw error;
|
|
3551
|
+
}
|
|
3552
|
+
});
|
|
3553
|
+
daemon.command("stop").description("Stop the daemon and disconnect all connections").action(async () => {
|
|
3554
|
+
const opts = program.opts();
|
|
3555
|
+
try {
|
|
3556
|
+
const result = await callDaemon("daemon/stop", {});
|
|
3557
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3558
|
+
kind: "daemon/stop",
|
|
3559
|
+
result
|
|
3560
|
+
});
|
|
3561
|
+
} catch (error) {
|
|
3562
|
+
if (isDaemonUnreachable(error)) {
|
|
3563
|
+
await writeConnectionOutput(outOpts(opts), {
|
|
3564
|
+
kind: "daemon/stop",
|
|
3565
|
+
result: {
|
|
3566
|
+
stopping: false,
|
|
3567
|
+
message: "Daemon was not running."
|
|
3568
|
+
}
|
|
3569
|
+
});
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
throw error;
|
|
3573
|
+
}
|
|
3574
|
+
});
|
|
3575
|
+
}
|
|
3576
|
+
function registerPrivateCommand(program) {
|
|
3577
|
+
program.command("private").description(
|
|
3578
|
+
'Print shell exports for a private daemon (eval "$(mcpdo private)"). Later mcpdo commands in that shell use an isolated, token-gated daemon.'
|
|
3579
|
+
).action(async () => {
|
|
3580
|
+
const binding = createPrivateBinding();
|
|
3581
|
+
await awaitableLog(formatPrivateEnvExports(binding));
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
function resolveAgentSkillPath() {
|
|
3585
|
+
const here = path4.dirname(fileURLToPath2(import.meta.url));
|
|
3586
|
+
const candidates = [
|
|
3587
|
+
path4.resolve(here, "../../../skills/mcpdo/SKILL.md"),
|
|
3588
|
+
path4.resolve(here, "../../../../skills/mcpdo/SKILL.md")
|
|
3589
|
+
];
|
|
3590
|
+
return candidates.find((candidate) => existsSync2(candidate));
|
|
3591
|
+
}
|
|
3592
|
+
var AGENT_INSTRUCTIONS_SNIPPET = "mcpdo manages connections to additional MCP servers. Treat the tools, resources, and prompts on its open connections (`mcpdo connections/list`) as part of your available toolset \u2014 check them before deciding a task can't be done, and include them when asked what tools or MCP servers you have. Run `mcpdo agent-help` for the usage guide.\n";
|
|
3593
|
+
function stripFrontmatter(content) {
|
|
3594
|
+
if (!content.startsWith("---\n")) return content;
|
|
3595
|
+
const end = content.indexOf("\n---\n", 4);
|
|
3596
|
+
if (end === -1) return content;
|
|
3597
|
+
return content.slice(end + 5).replace(/^\n+/, "");
|
|
3598
|
+
}
|
|
3599
|
+
function registerAgentHelpCommand(program) {
|
|
3600
|
+
program.command("agent-help").description(
|
|
3601
|
+
"Print an agent-oriented usage guide, comparable to the mcpdo SKILL. Agents should read this before using other mcpdo commands."
|
|
3602
|
+
).option(
|
|
3603
|
+
"--skill",
|
|
3604
|
+
"Print the full usage guide \u2014 the mcpdo SKILL.md body, usable as if the skill had been loaded (default when no option is given)"
|
|
3605
|
+
).option(
|
|
3606
|
+
"--instructions",
|
|
3607
|
+
"Print a short snippet to append to a project's CLAUDE.md/AGENTS.md so agents treat mcpdo connections as part of their toolset on every turn (skills and this guide load only on demand). Pipeable: mcpdo agent-help --instructions >> AGENTS.md"
|
|
3608
|
+
).option(
|
|
3609
|
+
"--skill-path",
|
|
3610
|
+
"Print the path of the installable SKILL.md file \u2014 this guide plus its skill frontmatter \u2014 for skill runtimes (e.g. copy into ~/.claude/skills/mcpdo/)"
|
|
3611
|
+
).action(
|
|
3612
|
+
async (o) => {
|
|
3613
|
+
const picked = [o.skill, o.instructions, o.skillPath].filter(
|
|
3614
|
+
(v) => v === true
|
|
3615
|
+
).length;
|
|
3616
|
+
if (picked > 1) {
|
|
3617
|
+
throw new CliExitCodeError(
|
|
3618
|
+
EXIT_CODES.USAGE,
|
|
3619
|
+
"--skill, --instructions, and --skill-path are mutually exclusive.",
|
|
3620
|
+
{ code: "agent_help_flag_conflict" }
|
|
3621
|
+
);
|
|
3622
|
+
}
|
|
3623
|
+
if (o.instructions === true) {
|
|
3624
|
+
await awaitableLog(AGENT_INSTRUCTIONS_SNIPPET);
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
const skillPath = resolveAgentSkillPath();
|
|
3628
|
+
if (!skillPath) {
|
|
3629
|
+
throw new CliExitCodeError(
|
|
3630
|
+
EXIT_CODES.USAGE,
|
|
3631
|
+
"Could not locate skills/mcpdo/SKILL.md relative to this install.",
|
|
3632
|
+
{ code: "agent_help_not_found" }
|
|
3633
|
+
);
|
|
3634
|
+
}
|
|
3635
|
+
if (o.skillPath === true) {
|
|
3636
|
+
await awaitableLog(skillPath + "\n");
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
await awaitableLog(stripFrontmatter(readFileSync2(skillPath, "utf8")));
|
|
3640
|
+
}
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3643
|
+
function registerRpcCommands(program) {
|
|
3644
|
+
for (const method of CONNECTION_RPC_METHODS) {
|
|
3645
|
+
const cmd = program.command(method).description(`MCP ${method} against the current connection`);
|
|
3646
|
+
cmd.option(
|
|
3647
|
+
"--metadata <pairs...>",
|
|
3648
|
+
"General metadata as key=value pairs",
|
|
3649
|
+
parseKeyValue,
|
|
3650
|
+
{}
|
|
3651
|
+
);
|
|
3652
|
+
switch (method) {
|
|
3653
|
+
case "tools/list":
|
|
3654
|
+
cmd.option("--app-info", "Emit one NDJSON app-info line per tool");
|
|
3655
|
+
cmd.action(async (o) => {
|
|
3656
|
+
await runRpc(program, method, {
|
|
3657
|
+
appInfo: o.appInfo === true,
|
|
3658
|
+
metadata: o.metadata
|
|
3659
|
+
});
|
|
3660
|
+
});
|
|
3661
|
+
break;
|
|
3662
|
+
case "tools/call":
|
|
3663
|
+
cmd.argument("[toolName]", "Tool name").argument(
|
|
3664
|
+
"[toolArgs...]",
|
|
3665
|
+
"Arguments as key:=value pairs or a JSON object"
|
|
3666
|
+
).option("--tool-name <name>", "Tool name").option(
|
|
3667
|
+
"--tool-arg <pairs...>",
|
|
3668
|
+
"Tool argument as key=value pair (alternative to key:=value positionals)",
|
|
3669
|
+
parseKeyValue,
|
|
3670
|
+
{}
|
|
3671
|
+
).option(
|
|
3672
|
+
"--tool-args-json <json>",
|
|
3673
|
+
"Tool arguments as a JSON object (alternative to inline JSON positional)"
|
|
3674
|
+
).option(
|
|
3675
|
+
"--tool-metadata <pairs...>",
|
|
3676
|
+
"Tool-specific metadata",
|
|
3677
|
+
parseKeyValue,
|
|
3678
|
+
{}
|
|
3679
|
+
).option("--task", "Task-augmented tool call (callToolStream)").option("--app-info", "Probe MCP App metadata only");
|
|
3680
|
+
cmd.action(
|
|
3681
|
+
async (toolNamePos, toolArgsPos, o) => {
|
|
3682
|
+
const { toolName, toolArg } = resolveToolCallArgs({
|
|
3683
|
+
toolNameFlag: o.toolName,
|
|
3684
|
+
toolNamePos,
|
|
3685
|
+
toolArgsPos,
|
|
3686
|
+
toolArgFlag: o.toolArg ?? {},
|
|
3687
|
+
toolArgsJson: o.toolArgsJson
|
|
3688
|
+
});
|
|
3689
|
+
await runRpc(program, method, {
|
|
3690
|
+
toolName,
|
|
3691
|
+
toolArg,
|
|
3692
|
+
toolMeta: o.toolMetadata,
|
|
3693
|
+
metadata: o.metadata,
|
|
3694
|
+
task: o.task === true,
|
|
3695
|
+
appInfo: o.appInfo === true
|
|
3696
|
+
});
|
|
3697
|
+
}
|
|
3698
|
+
);
|
|
3699
|
+
break;
|
|
3700
|
+
case "resources/read":
|
|
3701
|
+
case "resources/subscribe":
|
|
3702
|
+
case "resources/unsubscribe":
|
|
3703
|
+
cmd.argument("[uri]", "Resource URI").option("--uri <uri>", "Resource URI");
|
|
3704
|
+
cmd.action(async (uriPos, o) => {
|
|
3705
|
+
await runRpc(program, method, {
|
|
3706
|
+
uri: o.uri ?? uriPos,
|
|
3707
|
+
metadata: o.metadata
|
|
3708
|
+
});
|
|
3709
|
+
});
|
|
3710
|
+
break;
|
|
3711
|
+
case "skills/list":
|
|
3712
|
+
cmd.option(
|
|
3713
|
+
"--verify",
|
|
3714
|
+
"Run the SEP-2640 conformance and digest checks over every skill returned"
|
|
3715
|
+
);
|
|
3716
|
+
cmd.action(async (o) => {
|
|
3717
|
+
await runRpc(program, method, {
|
|
3718
|
+
verify: o.verify === true,
|
|
3719
|
+
metadata: o.metadata
|
|
3720
|
+
});
|
|
3721
|
+
});
|
|
3722
|
+
break;
|
|
3723
|
+
case "skills/get":
|
|
3724
|
+
cmd.argument("[uri]", "Skill URI").option("--uri <uri>", "Skill URI").option(
|
|
3725
|
+
"--verify",
|
|
3726
|
+
"Run the SEP-2640 conformance and digest checks over this skill"
|
|
3727
|
+
);
|
|
3728
|
+
cmd.action(async (uriPos, o) => {
|
|
3729
|
+
await runRpc(program, method, {
|
|
3730
|
+
uri: o.uri ?? uriPos,
|
|
3731
|
+
verify: o.verify === true,
|
|
3732
|
+
metadata: o.metadata
|
|
3733
|
+
});
|
|
3734
|
+
});
|
|
3735
|
+
break;
|
|
3736
|
+
case "prompts/get":
|
|
3737
|
+
cmd.argument("[promptName]", "Prompt name").option("--prompt-name <name>", "Prompt name").option(
|
|
3738
|
+
"--prompt-args <pairs...>",
|
|
3739
|
+
"Prompt arguments",
|
|
3740
|
+
parseKeyValue,
|
|
3741
|
+
{}
|
|
3742
|
+
);
|
|
3743
|
+
cmd.action(async (promptPos, o) => {
|
|
3744
|
+
await runRpc(program, method, {
|
|
3745
|
+
promptName: o.promptName ?? promptPos,
|
|
3746
|
+
promptArgs: o.promptArgs ?? {},
|
|
3747
|
+
metadata: o.metadata
|
|
3748
|
+
});
|
|
3749
|
+
});
|
|
3750
|
+
break;
|
|
3751
|
+
case "prompts/complete":
|
|
3752
|
+
cmd.option("--complete-ref-type <type>", "ref/prompt or ref/resource").option("--complete-ref <ref>", "Prompt name or resource URI").option("--complete-arg-name <name>", "Argument name").option("--complete-arg-value <value>", "Partial value", "");
|
|
3753
|
+
cmd.action(async (o) => {
|
|
3754
|
+
const refType = o.completeRefType;
|
|
3755
|
+
if (refType !== "ref/prompt" && refType !== "ref/resource") {
|
|
3756
|
+
throw new CliExitCodeError(
|
|
3757
|
+
EXIT_CODES.USAGE,
|
|
3758
|
+
"prompts/complete requires --complete-ref-type ref/prompt|ref/resource",
|
|
3759
|
+
{ code: "usage" }
|
|
3760
|
+
);
|
|
3761
|
+
}
|
|
3762
|
+
await runRpc(program, method, {
|
|
3763
|
+
completeRefType: refType,
|
|
3764
|
+
completeRef: o.completeRef,
|
|
3765
|
+
completeArgName: o.completeArgName,
|
|
3766
|
+
completeArgValue: o.completeArgValue ?? "",
|
|
3767
|
+
metadata: o.metadata
|
|
3768
|
+
});
|
|
3769
|
+
});
|
|
3770
|
+
break;
|
|
3771
|
+
case "logging/setLevel":
|
|
3772
|
+
cmd.argument("[level]", "Logging level").option("--log-level <level>", "Logging level");
|
|
3773
|
+
cmd.action(async (levelPos, o) => {
|
|
3774
|
+
const level = o.logLevel ?? levelPos;
|
|
3775
|
+
if (level && !validLogLevels.includes(level)) {
|
|
3776
|
+
throw new Error(
|
|
3777
|
+
`Invalid log level: ${level}. Valid: ${validLogLevels.join(", ")}`
|
|
3778
|
+
);
|
|
3779
|
+
}
|
|
3780
|
+
await runRpc(program, method, {
|
|
3781
|
+
logLevel: level,
|
|
3782
|
+
metadata: o.metadata
|
|
3783
|
+
});
|
|
3784
|
+
});
|
|
3785
|
+
break;
|
|
3786
|
+
case "tasks/get":
|
|
3787
|
+
case "tasks/cancel":
|
|
3788
|
+
case "tasks/result":
|
|
3789
|
+
cmd.argument("[taskId]", "Task id").option("--task-id <id>", "Task id");
|
|
3790
|
+
cmd.action(async (taskPos, o) => {
|
|
3791
|
+
await runRpc(program, method, {
|
|
3792
|
+
taskId: o.taskId ?? taskPos,
|
|
3793
|
+
metadata: o.metadata
|
|
3794
|
+
});
|
|
3795
|
+
});
|
|
3796
|
+
break;
|
|
3797
|
+
case "tasks/update":
|
|
3798
|
+
cmd.argument("[taskId]", "Task id").option("--task-id <id>", "Task id").option(
|
|
3799
|
+
"--input-responses <json>",
|
|
3800
|
+
"JSON object keyed by the server's inputRequests id"
|
|
3801
|
+
);
|
|
3802
|
+
cmd.action(async (taskPos, o) => {
|
|
3803
|
+
await runRpc(program, method, {
|
|
3804
|
+
taskId: o.taskId ?? taskPos,
|
|
3805
|
+
inputResponsesJson: o.inputResponses,
|
|
3806
|
+
metadata: o.metadata
|
|
3807
|
+
});
|
|
3808
|
+
});
|
|
3809
|
+
break;
|
|
3810
|
+
case "roots/set":
|
|
3811
|
+
cmd.option("--roots-json <json>", "JSON array of {uri, name?}");
|
|
3812
|
+
cmd.action(async (o) => {
|
|
3813
|
+
await runRpc(program, method, {
|
|
3814
|
+
rootsJson: o.rootsJson,
|
|
3815
|
+
metadata: o.metadata
|
|
3816
|
+
});
|
|
3817
|
+
});
|
|
3818
|
+
break;
|
|
3819
|
+
default:
|
|
3820
|
+
cmd.action(async (o) => {
|
|
3821
|
+
await runRpc(program, method, {
|
|
3822
|
+
metadata: o.metadata
|
|
3823
|
+
});
|
|
3824
|
+
});
|
|
3825
|
+
break;
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
function registerElicitationCommands(program) {
|
|
3830
|
+
program.command("elicitation/respond").description(
|
|
3831
|
+
"Answer a pending server elicitation (from elicitationPending output): form answers as key:=value pairs / JSON, --done for URL mode, or --decline / --cancel"
|
|
3832
|
+
).argument("<elicitationId>", "Id from the elicitationPending payload").argument(
|
|
3833
|
+
"[fields...]",
|
|
3834
|
+
"Form answers as key:=value pairs or one JSON object (accepts)"
|
|
3835
|
+
).option(
|
|
3836
|
+
"--done",
|
|
3837
|
+
"URL mode: report the linked interaction as finished (accept)"
|
|
3838
|
+
).option("--decline", "Decline the request (form mode only)").option("--cancel", "Cancel the elicitation").action(async (elicitationId, fields, o) => {
|
|
3839
|
+
const opts = program.opts();
|
|
3840
|
+
const flags = [
|
|
3841
|
+
o.done === true && "--done",
|
|
3842
|
+
o.decline === true && "--decline",
|
|
3843
|
+
o.cancel === true && "--cancel"
|
|
3844
|
+
].filter(Boolean);
|
|
3845
|
+
const hasFields = (fields?.length ?? 0) > 0;
|
|
3846
|
+
if (flags.length > 1 || flags.length === 1 && hasFields) {
|
|
3847
|
+
throw new CliExitCodeError(
|
|
3848
|
+
EXIT_CODES.USAGE,
|
|
3849
|
+
`Provide field values, or exactly one of --done / --decline / --cancel \u2014 not ${[...hasFields ? ["field values"] : [], ...flags].join(" and ")}.`,
|
|
3850
|
+
{ code: "usage" }
|
|
3851
|
+
);
|
|
3852
|
+
}
|
|
3853
|
+
if (flags.length === 0 && !hasFields) {
|
|
3854
|
+
throw new CliExitCodeError(
|
|
3855
|
+
EXIT_CODES.USAGE,
|
|
3856
|
+
"Provide form answers as key:=value pairs (or one JSON object), or one of --done / --decline / --cancel.",
|
|
3857
|
+
{ code: "usage" }
|
|
3858
|
+
);
|
|
3859
|
+
}
|
|
3860
|
+
const params = o.cancel ? { elicitationId, action: "cancel" } : o.decline ? { elicitationId, action: "decline" } : hasFields ? {
|
|
3861
|
+
elicitationId,
|
|
3862
|
+
action: "accept",
|
|
3863
|
+
content: parseToolCallPositionals(fields)
|
|
3864
|
+
} : { elicitationId, action: "accept" };
|
|
3865
|
+
const { socketPath } = await ensureDaemon();
|
|
3866
|
+
const result = await callDaemon(
|
|
3867
|
+
"elicitation/respond",
|
|
3868
|
+
params,
|
|
3869
|
+
// The resumed call's duration is governed by MCP timeouts the
|
|
3870
|
+
// daemon enforces; a fixed local deadline would falsely fail it.
|
|
3871
|
+
{ socketPath, timeoutMs: 0 }
|
|
3872
|
+
);
|
|
3873
|
+
await writeRpcOutcome(
|
|
3874
|
+
outOpts(opts),
|
|
3875
|
+
result.method,
|
|
3876
|
+
result.toolName,
|
|
3877
|
+
result.outcome
|
|
3878
|
+
);
|
|
3879
|
+
});
|
|
3880
|
+
}
|
|
3881
|
+
async function runRpc(program, method, methodArgs) {
|
|
3882
|
+
const opts = program.opts();
|
|
3883
|
+
await dispatchConnectionRpc(method, methodArgs, {
|
|
3884
|
+
format: opts.format,
|
|
3885
|
+
plain: opts.plain === true,
|
|
3886
|
+
connection: opts.connection,
|
|
3887
|
+
requireExplicit: requireExplicitConnection()
|
|
3888
|
+
});
|
|
3889
|
+
}
|
|
3890
|
+
function withEraOverride(settings, era) {
|
|
3891
|
+
if (era === void 0) return settings;
|
|
3892
|
+
if (settings) return { ...settings, protocolEra: era };
|
|
3893
|
+
return {
|
|
3894
|
+
headers: [],
|
|
3895
|
+
metadata: {},
|
|
3896
|
+
env: [],
|
|
3897
|
+
connectionTimeout: DEFAULT_CONNECT_TIMEOUT_MS,
|
|
3898
|
+
requestTimeout: 0,
|
|
3899
|
+
taskTtl: DEFAULT_TASK_TTL_MS,
|
|
3900
|
+
maxFetchRequests: DEFAULT_MAX_FETCH_REQUESTS,
|
|
3901
|
+
autoRefreshOnListChanged: false,
|
|
3902
|
+
paginatedLists: false,
|
|
3903
|
+
roots: [],
|
|
3904
|
+
protocolEra: era
|
|
3905
|
+
};
|
|
3906
|
+
}
|
|
3907
|
+
function withElicitOverride(settings, elicit) {
|
|
3908
|
+
if (elicit === void 0) return settings;
|
|
3909
|
+
if (settings) return { ...settings, elicitCapability: elicit };
|
|
3910
|
+
return {
|
|
3911
|
+
headers: [],
|
|
3912
|
+
metadata: {},
|
|
3913
|
+
env: [],
|
|
3914
|
+
connectionTimeout: DEFAULT_CONNECT_TIMEOUT_MS,
|
|
3915
|
+
requestTimeout: 0,
|
|
3916
|
+
taskTtl: DEFAULT_TASK_TTL_MS,
|
|
3917
|
+
maxFetchRequests: DEFAULT_MAX_FETCH_REQUESTS,
|
|
3918
|
+
autoRefreshOnListChanged: false,
|
|
3919
|
+
paginatedLists: false,
|
|
3920
|
+
roots: [],
|
|
3921
|
+
elicitCapability: elicit
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
function withEmaOverride(settings, ema) {
|
|
3925
|
+
if (ema === void 0) return settings;
|
|
3926
|
+
if (settings) return { ...settings, enterpriseManaged: true };
|
|
3927
|
+
return {
|
|
3928
|
+
headers: [],
|
|
3929
|
+
metadata: {},
|
|
3930
|
+
env: [],
|
|
3931
|
+
connectionTimeout: DEFAULT_CONNECT_TIMEOUT_MS,
|
|
3932
|
+
requestTimeout: 0,
|
|
3933
|
+
taskTtl: DEFAULT_TASK_TTL_MS,
|
|
3934
|
+
maxFetchRequests: DEFAULT_MAX_FETCH_REQUESTS,
|
|
3935
|
+
autoRefreshOnListChanged: false,
|
|
3936
|
+
paginatedLists: false,
|
|
3937
|
+
roots: [],
|
|
3938
|
+
enterpriseManaged: true
|
|
3939
|
+
};
|
|
3940
|
+
}
|
|
3941
|
+
function parseKeyValue(value, previous = {}) {
|
|
3942
|
+
const parts = value.split("=");
|
|
3943
|
+
const key = parts[0];
|
|
3944
|
+
const val = parts.slice(1).join("=");
|
|
3945
|
+
if (!key || val === void 0 || val === "") {
|
|
3946
|
+
throw new Error(
|
|
3947
|
+
`Invalid parameter format: ${value}. Use key=value format.`
|
|
3948
|
+
);
|
|
3949
|
+
}
|
|
3950
|
+
let parsedValue;
|
|
3951
|
+
try {
|
|
3952
|
+
parsedValue = JSON.parse(val);
|
|
3953
|
+
} catch {
|
|
3954
|
+
parsedValue = val;
|
|
3955
|
+
}
|
|
3956
|
+
assertJsonRoundTrips(parsedValue, `parameter "${value}"`);
|
|
3957
|
+
return { ...previous, [key]: parsedValue };
|
|
3958
|
+
}
|
|
3959
|
+
function looksLikeUrl(value) {
|
|
3960
|
+
return /^https?:\/\//i.test(value);
|
|
3961
|
+
}
|
|
3962
|
+
function looksLikePath(value) {
|
|
3963
|
+
return value.includes("/") || value.includes("\\") || value.startsWith(".") || value.startsWith("~");
|
|
3964
|
+
}
|
|
3965
|
+
function splitConnectionTarget(target) {
|
|
3966
|
+
if (target.length > 0 && target[0].startsWith("@")) {
|
|
3967
|
+
return { name: stripAt(target[0]), rest: target.slice(1) };
|
|
3968
|
+
}
|
|
3969
|
+
return { name: void 0, rest: target };
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
// src/mcp-bin.ts
|
|
3973
|
+
disallowMemorySecretStoreFallback();
|
|
3974
|
+
setSecretStorageWarningsQuiet(true);
|
|
3975
|
+
var __filename = fileURLToPath3(import.meta.url);
|
|
3976
|
+
function isMainModule() {
|
|
3977
|
+
const entry = process.argv[1];
|
|
3978
|
+
if (entry === void 0) return false;
|
|
3979
|
+
try {
|
|
3980
|
+
return realpathSync(resolve2(entry)) === realpathSync(resolve2(__filename));
|
|
3981
|
+
} catch {
|
|
3982
|
+
return resolve2(entry) === resolve2(__filename);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
if (isMainModule()) {
|
|
3986
|
+
runMcp(process.argv).then(() => process.exit(0)).catch(handleError);
|
|
3987
|
+
}
|
|
3988
|
+
export {
|
|
3989
|
+
runMcp
|
|
3990
|
+
};
|