@evantahler/mcpx 0.20.1 → 0.21.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/package.json +1 -1
- package/src/client/elicitation.ts +81 -24
- package/src/client/manager.ts +3 -0
- package/src/commands/exec.ts +16 -3
- package/src/context.ts +2 -1
- package/src/output/formatter.ts +28 -3
package/package.json
CHANGED
|
@@ -283,16 +283,35 @@ async function promptMultiSelect(
|
|
|
283
283
|
// URL mode
|
|
284
284
|
// ---------------------------------------------------------------------------
|
|
285
285
|
|
|
286
|
-
async function handleUrlElicitation(
|
|
286
|
+
export async function handleUrlElicitation(
|
|
287
287
|
params: ElicitRequestURLParams,
|
|
288
288
|
options: ElicitationOptions,
|
|
289
289
|
): Promise<ElicitResult> {
|
|
290
290
|
if (options.json) {
|
|
291
291
|
return handleUrlJson(params);
|
|
292
292
|
}
|
|
293
|
+
if (options.noInteractive) {
|
|
294
|
+
printUrlElicitation(params);
|
|
295
|
+
return { action: "decline" };
|
|
296
|
+
}
|
|
293
297
|
return handleUrlInteractive(params);
|
|
294
298
|
}
|
|
295
299
|
|
|
300
|
+
function printUrlElicitation(params: ElicitRequestURLParams): void {
|
|
301
|
+
const domain = (() => {
|
|
302
|
+
try {
|
|
303
|
+
return new URL(params.url).hostname;
|
|
304
|
+
} catch {
|
|
305
|
+
return "unknown";
|
|
306
|
+
}
|
|
307
|
+
})();
|
|
308
|
+
|
|
309
|
+
logger.writeRaw(`\n${ansis.bold("Server requests URL interaction:")}\n`);
|
|
310
|
+
logger.writeRaw(` ${params.message}\n`);
|
|
311
|
+
logger.writeRaw(` ${ansis.yellow("Domain:")} ${domain}\n`);
|
|
312
|
+
logger.writeRaw(` ${ansis.yellow("URL:")} ${params.url}\n`);
|
|
313
|
+
}
|
|
314
|
+
|
|
296
315
|
async function handleUrlJson(params: ElicitRequestURLParams): Promise<ElicitResult> {
|
|
297
316
|
const request = {
|
|
298
317
|
type: "elicitation",
|
|
@@ -313,32 +332,70 @@ async function handleUrlJson(params: ElicitRequestURLParams): Promise<ElicitResu
|
|
|
313
332
|
}
|
|
314
333
|
|
|
315
334
|
async function handleUrlInteractive(params: ElicitRequestURLParams): Promise<ElicitResult> {
|
|
316
|
-
|
|
317
|
-
const question = (prompt: string): Promise<string> => new Promise((resolve) => rl.question(prompt, resolve));
|
|
318
|
-
|
|
319
|
-
try {
|
|
320
|
-
const domain = (() => {
|
|
321
|
-
try {
|
|
322
|
-
return new URL(params.url).hostname;
|
|
323
|
-
} catch {
|
|
324
|
-
return "unknown";
|
|
325
|
-
}
|
|
326
|
-
})();
|
|
335
|
+
printUrlElicitation(params);
|
|
327
336
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
337
|
+
const yes = await promptYesNo(` Open in browser? [y/n]: `);
|
|
338
|
+
if (yes) {
|
|
339
|
+
await openBrowser(params.url);
|
|
340
|
+
return { action: "accept" };
|
|
341
|
+
}
|
|
342
|
+
return { action: "decline" };
|
|
343
|
+
}
|
|
332
344
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Prompt for a yes/no answer.
|
|
347
|
+
* On a TTY, accepts a single keypress (y/Y/n/N/Enter/Esc) without requiring Enter.
|
|
348
|
+
* Off a TTY, falls back to line-buffered input so piped tests still work.
|
|
349
|
+
*/
|
|
350
|
+
function promptYesNo(prompt: string): Promise<boolean> {
|
|
351
|
+
logger.writeRaw(prompt);
|
|
352
|
+
const stdin = process.stdin;
|
|
353
|
+
|
|
354
|
+
if (!stdin.isTTY) {
|
|
355
|
+
return new Promise((resolve) => {
|
|
356
|
+
const rl = createInterface({ input: stdin });
|
|
357
|
+
rl.once("line", (line) => {
|
|
358
|
+
rl.close();
|
|
359
|
+
const ch = line.trim().toLowerCase();
|
|
360
|
+
resolve(ch === "y" || ch === "yes");
|
|
361
|
+
});
|
|
362
|
+
rl.once("close", () => resolve(false));
|
|
363
|
+
});
|
|
341
364
|
}
|
|
365
|
+
|
|
366
|
+
return new Promise((resolve) => {
|
|
367
|
+
stdin.setRawMode(true);
|
|
368
|
+
stdin.resume();
|
|
369
|
+
|
|
370
|
+
const cleanup = () => {
|
|
371
|
+
stdin.removeListener("data", onData);
|
|
372
|
+
stdin.setRawMode(false);
|
|
373
|
+
stdin.pause();
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const onData = (data: Buffer) => {
|
|
377
|
+
const key = data.toString();
|
|
378
|
+
// Ctrl+C
|
|
379
|
+
if (key === "\u0003") {
|
|
380
|
+
cleanup();
|
|
381
|
+
logger.writeRaw("\n");
|
|
382
|
+
process.exit(130);
|
|
383
|
+
}
|
|
384
|
+
const ch = key.toLowerCase();
|
|
385
|
+
if (ch === "y") {
|
|
386
|
+
cleanup();
|
|
387
|
+
logger.writeRaw("y\n");
|
|
388
|
+
resolve(true);
|
|
389
|
+
} else if (ch === "n" || key === "\u001b") {
|
|
390
|
+
cleanup();
|
|
391
|
+
logger.writeRaw("n\n");
|
|
392
|
+
resolve(false);
|
|
393
|
+
}
|
|
394
|
+
// Ignore other keys
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
stdin.on("data", onData);
|
|
398
|
+
});
|
|
342
399
|
}
|
|
343
400
|
|
|
344
401
|
// ---------------------------------------------------------------------------
|
package/src/client/manager.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
CallToolResultSchema,
|
|
14
14
|
ElicitRequestSchema,
|
|
15
15
|
LoggingMessageNotificationSchema,
|
|
16
|
+
UrlElicitationRequiredError,
|
|
16
17
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
17
18
|
import picomatch from "picomatch";
|
|
18
19
|
import pkg from "../../package.json";
|
|
@@ -305,6 +306,8 @@ export class ServerManager {
|
|
|
305
306
|
return await fn();
|
|
306
307
|
} catch (err) {
|
|
307
308
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
309
|
+
// Don't retry auth challenges — the user needs to authorize first
|
|
310
|
+
if (err instanceof UrlElicitationRequiredError) throw lastError;
|
|
308
311
|
if (attempt < this.maxRetries && serverName) {
|
|
309
312
|
// Clear cached client so next attempt reconnects fresh
|
|
310
313
|
try {
|
package/src/commands/exec.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { UrlElicitationRequiredError } from "@modelcontextprotocol/sdk/types.js";
|
|
1
2
|
import type { Command } from "commander";
|
|
3
|
+
import { handleUrlElicitation } from "../client/elicitation.ts";
|
|
2
4
|
import type { ServerManager } from "../client/manager.ts";
|
|
3
5
|
import { DEFAULTS } from "../constants.ts";
|
|
4
6
|
import { getContext } from "../context.ts";
|
|
@@ -108,7 +110,7 @@ export function registerExecCommand(program: Command) {
|
|
|
108
110
|
trailing: string[],
|
|
109
111
|
options: { file?: string; wait: boolean; ttl: string },
|
|
110
112
|
) => {
|
|
111
|
-
const { manager, formatOptions } = await getContext(program);
|
|
113
|
+
const { manager, formatOptions, noInteractive } = await getContext(program);
|
|
112
114
|
|
|
113
115
|
let resolved: ResolvedArgs;
|
|
114
116
|
try {
|
|
@@ -252,11 +254,22 @@ export function registerExecCommand(program: Command) {
|
|
|
252
254
|
} else {
|
|
253
255
|
// Standard synchronous tool call
|
|
254
256
|
const spinner = logger.startSpinner(`Executing ${server}/${tool}...`, formatOptions);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
+
let result: unknown;
|
|
258
|
+
try {
|
|
259
|
+
result = await manager.callTool(server, tool, args);
|
|
260
|
+
} finally {
|
|
261
|
+
spinner.stop();
|
|
262
|
+
}
|
|
257
263
|
console.log(formatCallResult(result, formatOptions));
|
|
258
264
|
}
|
|
259
265
|
} catch (err) {
|
|
266
|
+
if (err instanceof UrlElicitationRequiredError) {
|
|
267
|
+
const elicitOptions = { noInteractive, json: !!formatOptions.json };
|
|
268
|
+
for (const elicitation of err.elicitations) {
|
|
269
|
+
await handleUrlElicitation(elicitation, elicitOptions);
|
|
270
|
+
}
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
260
273
|
console.error(formatError(String(err), formatOptions));
|
|
261
274
|
process.exit(1);
|
|
262
275
|
} finally {
|
package/src/context.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface AppContext {
|
|
|
10
10
|
config: Config;
|
|
11
11
|
manager: ServerManager;
|
|
12
12
|
formatOptions: FormatOptions;
|
|
13
|
+
noInteractive: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/** Build the app context from the root commander program options */
|
|
@@ -67,5 +68,5 @@ export async function getContext(program: Command): Promise<AppContext> {
|
|
|
67
68
|
|
|
68
69
|
logger.configure(formatOptions);
|
|
69
70
|
|
|
70
|
-
return { config, manager, formatOptions };
|
|
71
|
+
return { config, manager, formatOptions, noInteractive };
|
|
71
72
|
}
|
package/src/output/formatter.ts
CHANGED
|
@@ -363,16 +363,25 @@ function formatCallResultAsMarkdown(result: unknown): string {
|
|
|
363
363
|
mimeType?: string;
|
|
364
364
|
uri?: string;
|
|
365
365
|
}>;
|
|
366
|
+
structuredContent?: unknown;
|
|
367
|
+
_meta?: unknown;
|
|
366
368
|
isError?: boolean;
|
|
367
369
|
};
|
|
368
370
|
|
|
369
|
-
|
|
371
|
+
const hasContent = Array.isArray(r.content) && r.content.length > 0;
|
|
372
|
+
const hasStructured = r.structuredContent !== undefined && r.structuredContent !== null;
|
|
373
|
+
const hasMeta =
|
|
374
|
+
r._meta !== undefined &&
|
|
375
|
+
r._meta !== null &&
|
|
376
|
+
!(typeof r._meta === "object" && Object.keys(r._meta as object).length === 0);
|
|
377
|
+
|
|
378
|
+
if (!hasContent && !hasStructured && !hasMeta) {
|
|
370
379
|
return renderMarkdownToAnsi(jsonToMarkdown(result));
|
|
371
380
|
}
|
|
372
381
|
|
|
373
382
|
const parts: string[] = [];
|
|
374
383
|
|
|
375
|
-
for (const block of r.content) {
|
|
384
|
+
for (const block of r.content ?? []) {
|
|
376
385
|
switch (block.type) {
|
|
377
386
|
case "text":
|
|
378
387
|
if (block.text !== undefined) {
|
|
@@ -390,15 +399,31 @@ function formatCallResultAsMarkdown(result: unknown): string {
|
|
|
390
399
|
`[image: ${block.mimeType ?? "unknown type"}, ${block.data ? Math.ceil((block.data.length * 3) / 4) : 0} bytes]`,
|
|
391
400
|
);
|
|
392
401
|
break;
|
|
402
|
+
case "audio":
|
|
403
|
+
parts.push(
|
|
404
|
+
`[audio: ${block.mimeType ?? "unknown type"}, ${block.data ? Math.ceil((block.data.length * 3) / 4) : 0} bytes]`,
|
|
405
|
+
);
|
|
406
|
+
break;
|
|
393
407
|
case "resource":
|
|
394
408
|
parts.push(`[resource: ${block.uri ?? "unknown"}]`);
|
|
395
409
|
break;
|
|
410
|
+
case "resource_link":
|
|
411
|
+
parts.push(`[resource_link: ${block.uri ?? "unknown"}]`);
|
|
412
|
+
break;
|
|
396
413
|
default:
|
|
397
|
-
parts.push(`[${block.type}]
|
|
414
|
+
parts.push(`[${block.type}]\n\n\`\`\`json\n${JSON.stringify(block, null, 2)}\n\`\`\``);
|
|
398
415
|
break;
|
|
399
416
|
}
|
|
400
417
|
}
|
|
401
418
|
|
|
419
|
+
if (hasStructured) {
|
|
420
|
+
parts.push(`**Structured Content:**\n\n${jsonToMarkdown(r.structuredContent)}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (hasMeta) {
|
|
424
|
+
parts.push(`**Meta:**\n\n${jsonToMarkdown(r._meta)}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
402
427
|
let output = parts.join("\n\n");
|
|
403
428
|
if (r.isError) {
|
|
404
429
|
output = `**error:** ${output}`;
|