@ian-pascoe/pi-mcp 0.1.0 → 0.2.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 +16 -3
- package/dist/pi-mcp-cli.js +137 -101
- package/package.json +8 -8
- package/src/mcp-command-completion.ts +572 -0
- package/src/mcp-command.ts +272 -131
- package/src/mcp-host.ts +56 -49
- package/src/mcp-observer-ui.ts +195 -0
- package/src/mcp-presentation.ts +660 -0
- package/src/mcp-session-files.ts +15 -6
- package/src/mcp-tool-catalog.ts +59 -44
- package/src/pi-mcp-cli.ts +7 -4
- package/src/pi-mcp-extension.ts +369 -310
- package/src/pi-mcp-settings.ts +23 -7
package/src/mcp-command.ts
CHANGED
|
@@ -26,8 +26,56 @@ export const MCP_COMMAND_NAMES = [
|
|
|
26
26
|
"subscribe",
|
|
27
27
|
"unsubscribe",
|
|
28
28
|
"logs",
|
|
29
|
+
"help",
|
|
29
30
|
] as const;
|
|
30
31
|
|
|
32
|
+
/** One command name accepted by the MCP command parser. */
|
|
33
|
+
export type McpCommandName = (typeof MCP_COMMAND_NAMES)[number];
|
|
34
|
+
|
|
35
|
+
/** Value options valid only for local stdio Server Definitions. */
|
|
36
|
+
export const MCP_ADD_LOCAL_VALUE_OPTIONS = ["cwd", "environment"] as const;
|
|
37
|
+
|
|
38
|
+
/** OAuth credential options accepted by remote Server Definitions. */
|
|
39
|
+
export const MCP_ADD_OAUTH_VALUE_OPTIONS = [
|
|
40
|
+
"client-id",
|
|
41
|
+
"client-secret",
|
|
42
|
+
"redirect-uri",
|
|
43
|
+
"scope",
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
/** Value options valid only for remote HTTP or SSE Server Definitions. */
|
|
47
|
+
export const MCP_ADD_REMOTE_VALUE_OPTIONS = [
|
|
48
|
+
"auth",
|
|
49
|
+
...MCP_ADD_OAUTH_VALUE_OPTIONS,
|
|
50
|
+
"header",
|
|
51
|
+
"token",
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
/** Option names accepted by the MCP command parser, without leading dashes. */
|
|
55
|
+
export const MCP_COMMAND_OPTIONS = {
|
|
56
|
+
add: {
|
|
57
|
+
flags: ["local"],
|
|
58
|
+
values: [...MCP_ADD_LOCAL_VALUE_OPTIONS, ...MCP_ADD_REMOTE_VALUE_OPTIONS, "transport"],
|
|
59
|
+
},
|
|
60
|
+
auth: { flags: ["no-open"], values: ["callback", "code", "state"] },
|
|
61
|
+
disable: { flags: ["local"], values: [] },
|
|
62
|
+
enable: { flags: ["local"], values: [] },
|
|
63
|
+
help: { flags: [], values: [] },
|
|
64
|
+
list: { flags: ["json"], values: [] },
|
|
65
|
+
logout: { flags: ["all", "force"], values: [] },
|
|
66
|
+
logs: { flags: [], values: [] },
|
|
67
|
+
prompt: { flags: [], values: ["arg"] },
|
|
68
|
+
reconnect: { flags: [], values: [] },
|
|
69
|
+
remove: { flags: ["local", "logout"], values: [] },
|
|
70
|
+
status: { flags: [], values: [] },
|
|
71
|
+
subscribe: { flags: [], values: [] },
|
|
72
|
+
test: { flags: ["all", "json"], values: [] },
|
|
73
|
+
unsubscribe: { flags: [], values: [] },
|
|
74
|
+
} as const satisfies Record<
|
|
75
|
+
McpCommandName,
|
|
76
|
+
{ readonly flags: readonly string[]; readonly values: readonly string[] }
|
|
77
|
+
>;
|
|
78
|
+
|
|
31
79
|
/** Persistent and offline commands available through the standalone executable. */
|
|
32
80
|
export const MCP_STANDALONE_COMMAND_NAMES = MCP_COMMAND_NAMES.slice(0, 8);
|
|
33
81
|
|
|
@@ -119,7 +167,8 @@ export type McpCommand =
|
|
|
119
167
|
}
|
|
120
168
|
| { readonly kind: "subscribe"; readonly server: string; readonly uri: string }
|
|
121
169
|
| { readonly kind: "unsubscribe"; readonly server: string; readonly uri: string }
|
|
122
|
-
| { readonly kind: "logs"; readonly
|
|
170
|
+
| { readonly kind: "logs"; readonly server?: string }
|
|
171
|
+
| { readonly kind: "help" };
|
|
123
172
|
|
|
124
173
|
/** Usage failure returned when command tokens cannot be parsed. */
|
|
125
174
|
export interface McpCommandParseFailure {
|
|
@@ -208,23 +257,6 @@ const EXIT_CODES = {
|
|
|
208
257
|
|
|
209
258
|
const GENERAL_USAGE = `Usage: pi-mcp <command> [options]
|
|
210
259
|
Commands: ${MCP_STANDALONE_COMMAND_NAMES.join(", ")}`;
|
|
211
|
-
const MCP_LOG_LEVELS = [
|
|
212
|
-
"debug",
|
|
213
|
-
"info",
|
|
214
|
-
"notice",
|
|
215
|
-
"warning",
|
|
216
|
-
"error",
|
|
217
|
-
"critical",
|
|
218
|
-
"alert",
|
|
219
|
-
"emergency",
|
|
220
|
-
] as const;
|
|
221
|
-
|
|
222
|
-
/** MCP logging levels accepted by the shared `/mcp logs` grammar. */
|
|
223
|
-
export type McpLoggingLevel = (typeof MCP_LOG_LEVELS)[number];
|
|
224
|
-
|
|
225
|
-
function isMcpLoggingLevel(value: string): value is McpLoggingLevel {
|
|
226
|
-
return MCP_LOG_LEVELS.some((candidate) => candidate === value);
|
|
227
|
-
}
|
|
228
260
|
const RUNTIME_HELP = `Commands: ${MCP_COMMAND_NAMES.join(", ")}`;
|
|
229
261
|
|
|
230
262
|
const COMMAND_USAGE = {
|
|
@@ -234,7 +266,8 @@ const COMMAND_USAGE = {
|
|
|
234
266
|
enable: "Usage: pi-mcp enable [-l|--local] <server>",
|
|
235
267
|
list: "Usage: pi-mcp list [--json]",
|
|
236
268
|
logout: "Usage: pi-mcp logout <server> | --all --force",
|
|
237
|
-
logs: "Usage: /mcp logs [server]
|
|
269
|
+
logs: "Usage: /mcp logs [server]",
|
|
270
|
+
help: "Usage: /mcp help",
|
|
238
271
|
prompt: "Usage: /mcp prompt <server> <prompt> [--arg NAME=VALUE]...",
|
|
239
272
|
reconnect: "Usage: /mcp reconnect <server>",
|
|
240
273
|
remove: "Usage: pi-mcp remove [-l|--local] [--logout] <server>",
|
|
@@ -256,13 +289,39 @@ function usageFailure(
|
|
|
256
289
|
};
|
|
257
290
|
}
|
|
258
291
|
|
|
259
|
-
|
|
292
|
+
/** Tokenized command options, including an unfinished completion value when requested. */
|
|
293
|
+
export interface McpCommandScannedOptions {
|
|
294
|
+
/** Normalized flag names present before the stdio delimiter. */
|
|
260
295
|
readonly flags: ReadonlySet<string>;
|
|
296
|
+
/** Value option awaiting the current completion token. */
|
|
297
|
+
readonly pendingValue?: string;
|
|
298
|
+
/** Non-option tokens present before the stdio delimiter. */
|
|
261
299
|
readonly positionals: readonly string[];
|
|
300
|
+
/** Unparsed stdio command tokens after `--`, or `undefined` when no delimiter exists. */
|
|
262
301
|
readonly tail: readonly string[] | undefined;
|
|
302
|
+
/** Normalized value-option names and their supplied values. */
|
|
263
303
|
readonly values: ReadonlyMap<string, readonly string[]>;
|
|
264
304
|
}
|
|
265
305
|
|
|
306
|
+
/** Failed command option scan before semantic validation. */
|
|
307
|
+
export interface McpCommandOptionScanFailure {
|
|
308
|
+
/** Human-readable grammar failure. */
|
|
309
|
+
readonly message: string;
|
|
310
|
+
/** Discriminant for a failed scan. */
|
|
311
|
+
readonly ok: false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Successful command option scan before semantic validation. */
|
|
315
|
+
export interface McpCommandOptionScanSuccess {
|
|
316
|
+
/** Discriminant for a successful scan. */
|
|
317
|
+
readonly ok: true;
|
|
318
|
+
/** Parsed option structure. */
|
|
319
|
+
readonly options: McpCommandScannedOptions;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Result of scanning command options before command-specific semantic validation. */
|
|
323
|
+
export type McpCommandOptionScanResult = McpCommandOptionScanFailure | McpCommandOptionScanSuccess;
|
|
324
|
+
|
|
266
325
|
function normalizeOptionName(rawName: string): string {
|
|
267
326
|
switch (rawName) {
|
|
268
327
|
case "-l":
|
|
@@ -275,21 +334,24 @@ function normalizeOptionName(rawName: string): string {
|
|
|
275
334
|
}
|
|
276
335
|
}
|
|
277
336
|
|
|
278
|
-
|
|
337
|
+
/** Scan MCP command options in strict parser mode or tolerant completion mode. */
|
|
338
|
+
export function scanMcpCommandOptions(
|
|
279
339
|
tokens: readonly string[],
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
): ParsedOptions | string {
|
|
340
|
+
command: McpCommandName,
|
|
341
|
+
mode: "completion" | "strict",
|
|
342
|
+
): McpCommandOptionScanResult {
|
|
284
343
|
const flags = new Set<string>();
|
|
285
344
|
const positionals: string[] = [];
|
|
286
345
|
const values = new Map<string, string[]>();
|
|
346
|
+
const accepted = MCP_COMMAND_OPTIONS[command];
|
|
347
|
+
const flagNames = new Set<string>(accepted.flags);
|
|
348
|
+
const valueNames = new Set<string>(accepted.values);
|
|
287
349
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
288
350
|
const token = tokens[index];
|
|
289
351
|
if (token === undefined) continue;
|
|
290
352
|
if (token === "--") {
|
|
291
|
-
if (
|
|
292
|
-
return { flags, positionals, tail: tokens.slice(index + 1), values };
|
|
353
|
+
if (command !== "add") return { message: "unexpected -- delimiter", ok: false };
|
|
354
|
+
return { ok: true, options: { flags, positionals, tail: tokens.slice(index + 1), values } };
|
|
293
355
|
}
|
|
294
356
|
if (!token.startsWith("-")) {
|
|
295
357
|
positionals.push(token);
|
|
@@ -299,23 +361,37 @@ function parseOptions(
|
|
|
299
361
|
const rawName = equalsIndex < 0 ? token : token.slice(0, equalsIndex);
|
|
300
362
|
const name = normalizeOptionName(rawName);
|
|
301
363
|
if (flagNames.has(name)) {
|
|
302
|
-
if (equalsIndex >= 0)
|
|
364
|
+
if (equalsIndex >= 0)
|
|
365
|
+
return { message: `option ${rawName} does not accept a value`, ok: false };
|
|
303
366
|
flags.add(name);
|
|
304
367
|
continue;
|
|
305
368
|
}
|
|
306
|
-
if (!valueNames.has(name)) return `unknown option ${rawName}
|
|
369
|
+
if (!valueNames.has(name)) return { message: `unknown option ${rawName}`, ok: false };
|
|
307
370
|
const value = equalsIndex < 0 ? tokens[index + 1] : token.slice(equalsIndex + 1);
|
|
308
371
|
if (value === undefined || (equalsIndex < 0 && value.startsWith("--"))) {
|
|
309
|
-
return
|
|
372
|
+
return mode === "completion" && value === undefined
|
|
373
|
+
? {
|
|
374
|
+
ok: true,
|
|
375
|
+
options: { flags, pendingValue: name, positionals, tail: undefined, values },
|
|
376
|
+
}
|
|
377
|
+
: { message: `option ${rawName} requires a value`, ok: false };
|
|
310
378
|
}
|
|
311
379
|
if (equalsIndex < 0) index += 1;
|
|
312
380
|
const existing = values.get(name) ?? [];
|
|
313
381
|
values.set(name, [...existing, value]);
|
|
314
382
|
}
|
|
315
|
-
return { flags, positionals, tail: undefined, values };
|
|
383
|
+
return { ok: true, options: { flags, positionals, tail: undefined, values } };
|
|
316
384
|
}
|
|
317
385
|
|
|
318
|
-
function
|
|
386
|
+
function parseOptions(
|
|
387
|
+
tokens: readonly string[],
|
|
388
|
+
command: McpCommandName,
|
|
389
|
+
): McpCommandScannedOptions | string {
|
|
390
|
+
const result = scanMcpCommandOptions(tokens, command, "strict");
|
|
391
|
+
return result.ok ? result.options : result.message;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function oneValue(options: McpCommandScannedOptions, name: string): string | undefined {
|
|
319
395
|
return options.values.get(name)?.at(-1);
|
|
320
396
|
}
|
|
321
397
|
|
|
@@ -337,7 +413,7 @@ function parseAssignments(
|
|
|
337
413
|
return result;
|
|
338
414
|
}
|
|
339
415
|
|
|
340
|
-
function parseScope(options:
|
|
416
|
+
function parseScope(options: McpCommandScannedOptions): McpCommandSettingsScope {
|
|
341
417
|
return options.flags.has("local") ? "project" : "global";
|
|
342
418
|
}
|
|
343
419
|
|
|
@@ -350,57 +426,107 @@ function isHttpUrl(value: string): boolean {
|
|
|
350
426
|
}
|
|
351
427
|
}
|
|
352
428
|
|
|
429
|
+
/** Authentication mode inferred from `add` options. */
|
|
430
|
+
export type McpAddAuthenticationMode = "bearer" | "none" | "oauth" | undefined;
|
|
431
|
+
|
|
432
|
+
/** Invalid combination of MCP `add` authentication options. */
|
|
433
|
+
export interface McpAddAuthenticationFailure {
|
|
434
|
+
/** Parser-compatible explanation of the incompatible options. */
|
|
435
|
+
readonly message: string;
|
|
436
|
+
/** Discriminant for incompatible authentication options. */
|
|
437
|
+
readonly ok: false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Compatible MCP `add` authentication options. */
|
|
441
|
+
export interface McpAddAuthenticationSuccess {
|
|
442
|
+
/** Discriminant for compatible authentication options. */
|
|
443
|
+
readonly ok: true;
|
|
444
|
+
/** Explicit or inferred authentication mode. */
|
|
445
|
+
readonly type: McpAddAuthenticationMode;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Compatibility result for MCP `add` authentication options. */
|
|
449
|
+
export type McpAddAuthenticationResult = McpAddAuthenticationFailure | McpAddAuthenticationSuccess;
|
|
450
|
+
|
|
451
|
+
/** Classify MCP `add` authentication options for strict parsing or partial completion. */
|
|
452
|
+
export function classifyMcpAddAuthentication(
|
|
453
|
+
options: McpCommandScannedOptions,
|
|
454
|
+
mode: "completion" | "strict",
|
|
455
|
+
): McpAddAuthenticationResult {
|
|
456
|
+
const configuredType = oneValue(options, "auth");
|
|
457
|
+
const token = oneValue(options, "token");
|
|
458
|
+
const oauth = MCP_ADD_OAUTH_VALUE_OPTIONS.some((name) => options.values.has(name));
|
|
459
|
+
const type = configuredType ?? (token !== undefined ? "bearer" : oauth ? "oauth" : undefined);
|
|
460
|
+
if (type === undefined) return { ok: true, type };
|
|
461
|
+
if (type === "none") {
|
|
462
|
+
return token === undefined && !oauth
|
|
463
|
+
? { ok: true, type }
|
|
464
|
+
: { message: "auth type none cannot include credential options", ok: false };
|
|
465
|
+
}
|
|
466
|
+
if (type === "bearer") {
|
|
467
|
+
if (mode === "strict" && (token === undefined || token.length === 0))
|
|
468
|
+
return { message: "bearer auth requires --token", ok: false };
|
|
469
|
+
if (oauth) return { message: "bearer auth cannot include OAuth options", ok: false };
|
|
470
|
+
return { ok: true, type };
|
|
471
|
+
}
|
|
472
|
+
if (type === "oauth") {
|
|
473
|
+
return token === undefined
|
|
474
|
+
? { ok: true, type }
|
|
475
|
+
: { message: "OAuth auth cannot include --token", ok: false };
|
|
476
|
+
}
|
|
477
|
+
return { message: "--auth must be none, bearer, or oauth", ok: false };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Local, remote, undecided, or incompatible MCP `add` transport form. */
|
|
481
|
+
export type McpAddTransportMode = "both" | "invalid" | "local" | "remote";
|
|
482
|
+
|
|
483
|
+
/** Classify local and remote MCP `add` option signals without performing I/O. */
|
|
484
|
+
export function classifyMcpAddTransportMode(
|
|
485
|
+
options: McpCommandScannedOptions,
|
|
486
|
+
): McpAddTransportMode {
|
|
487
|
+
const transport = oneValue(options, "transport");
|
|
488
|
+
if (
|
|
489
|
+
transport !== undefined &&
|
|
490
|
+
transport !== "http" &&
|
|
491
|
+
transport !== "sse" &&
|
|
492
|
+
transport !== "stdio"
|
|
493
|
+
)
|
|
494
|
+
return "invalid";
|
|
495
|
+
if (options.positionals.length > 2) return "invalid";
|
|
496
|
+
const local =
|
|
497
|
+
options.tail !== undefined ||
|
|
498
|
+
transport === "stdio" ||
|
|
499
|
+
MCP_ADD_LOCAL_VALUE_OPTIONS.some((name) => options.values.has(name));
|
|
500
|
+
const remote =
|
|
501
|
+
options.positionals.length > 1 ||
|
|
502
|
+
transport === "http" ||
|
|
503
|
+
transport === "sse" ||
|
|
504
|
+
MCP_ADD_REMOTE_VALUE_OPTIONS.some((name) => options.values.has(name));
|
|
505
|
+
return local && remote ? "invalid" : local ? "local" : remote ? "remote" : "both";
|
|
506
|
+
}
|
|
507
|
+
|
|
353
508
|
function parseRemoteAuth(
|
|
354
|
-
options:
|
|
509
|
+
options: McpCommandScannedOptions,
|
|
355
510
|
): McpAddServerDefinition extends infer _Definition
|
|
356
511
|
?
|
|
357
512
|
| Exclude<Extract<McpAddServerDefinition, { url: string }>["auth"], undefined>
|
|
358
513
|
| undefined
|
|
359
514
|
| string
|
|
360
515
|
: never {
|
|
361
|
-
const configuredType = oneValue(options, "auth");
|
|
362
516
|
const token = oneValue(options, "token");
|
|
363
517
|
const clientId = oneValue(options, "client-id");
|
|
364
518
|
const clientSecret = oneValue(options, "client-secret");
|
|
365
519
|
const redirectUri = oneValue(options, "redirect-uri");
|
|
366
520
|
const scopes = options.values.get("scope") ?? [];
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
: clientId !== undefined ||
|
|
371
|
-
clientSecret !== undefined ||
|
|
372
|
-
redirectUri !== undefined ||
|
|
373
|
-
scopes.length > 0
|
|
374
|
-
? "oauth"
|
|
375
|
-
: undefined;
|
|
376
|
-
const type = configuredType ?? inferredType;
|
|
521
|
+
const compatibility = classifyMcpAddAuthentication(options, "strict");
|
|
522
|
+
if (!compatibility.ok) return compatibility.message;
|
|
523
|
+
const type = compatibility.type;
|
|
377
524
|
if (type === undefined) return undefined;
|
|
378
|
-
if (type === "none") {
|
|
379
|
-
if (
|
|
380
|
-
token !== undefined ||
|
|
381
|
-
clientId !== undefined ||
|
|
382
|
-
clientSecret !== undefined ||
|
|
383
|
-
redirectUri !== undefined ||
|
|
384
|
-
scopes.length > 0
|
|
385
|
-
) {
|
|
386
|
-
return "auth type none cannot include credential options";
|
|
387
|
-
}
|
|
388
|
-
return { type: "none" };
|
|
389
|
-
}
|
|
525
|
+
if (type === "none") return { type: "none" };
|
|
390
526
|
if (type === "bearer") {
|
|
391
|
-
if (token === undefined
|
|
392
|
-
if (
|
|
393
|
-
clientId !== undefined ||
|
|
394
|
-
clientSecret !== undefined ||
|
|
395
|
-
redirectUri !== undefined ||
|
|
396
|
-
scopes.length > 0
|
|
397
|
-
) {
|
|
398
|
-
return "bearer auth cannot include OAuth options";
|
|
399
|
-
}
|
|
527
|
+
if (token === undefined) return "bearer auth requires --token";
|
|
400
528
|
return { token, type: "bearer" };
|
|
401
529
|
}
|
|
402
|
-
if (type !== "oauth") return "--auth must be none, bearer, or oauth";
|
|
403
|
-
if (token !== undefined) return "OAuth auth cannot include --token";
|
|
404
530
|
return {
|
|
405
531
|
...(clientId === undefined ? {} : { clientId }),
|
|
406
532
|
...(clientSecret === undefined ? {} : { clientSecret }),
|
|
@@ -411,47 +537,24 @@ function parseRemoteAuth(
|
|
|
411
537
|
}
|
|
412
538
|
|
|
413
539
|
function parseAdd(args: readonly string[]): McpCommandParseResult {
|
|
414
|
-
const options = parseOptions(
|
|
415
|
-
args,
|
|
416
|
-
new Set(["local"]),
|
|
417
|
-
new Set([
|
|
418
|
-
"auth",
|
|
419
|
-
"client-id",
|
|
420
|
-
"client-secret",
|
|
421
|
-
"cwd",
|
|
422
|
-
"environment",
|
|
423
|
-
"header",
|
|
424
|
-
"redirect-uri",
|
|
425
|
-
"scope",
|
|
426
|
-
"token",
|
|
427
|
-
"transport",
|
|
428
|
-
]),
|
|
429
|
-
true,
|
|
430
|
-
);
|
|
540
|
+
const options = parseOptions(args, "add");
|
|
431
541
|
if (typeof options === "string") return usageFailure("add", options);
|
|
432
542
|
const name = options.positionals[0];
|
|
433
543
|
if (name === undefined || name.length === 0)
|
|
434
544
|
return usageFailure("add", "server name is required");
|
|
545
|
+
const transportMode = classifyMcpAddTransportMode(options);
|
|
435
546
|
if (options.tail !== undefined) {
|
|
436
547
|
if (options.positionals.length !== 1)
|
|
437
548
|
return usageFailure("add", "a local add cannot also include a URL");
|
|
438
549
|
const command = options.tail[0];
|
|
439
550
|
if (command === undefined || command.length === 0)
|
|
440
551
|
return usageFailure("add", "local command is required after --");
|
|
441
|
-
for (const remoteOption of
|
|
442
|
-
"auth",
|
|
443
|
-
"client-id",
|
|
444
|
-
"client-secret",
|
|
445
|
-
"header",
|
|
446
|
-
"redirect-uri",
|
|
447
|
-
"scope",
|
|
448
|
-
"token",
|
|
449
|
-
]) {
|
|
552
|
+
for (const remoteOption of MCP_ADD_REMOTE_VALUE_OPTIONS) {
|
|
450
553
|
if (options.values.has(remoteOption))
|
|
451
554
|
return usageFailure("add", `local add cannot include --${remoteOption}`);
|
|
452
555
|
}
|
|
453
556
|
const transport = oneValue(options, "transport");
|
|
454
|
-
if (transport !== undefined &&
|
|
557
|
+
if (transport !== undefined && transportMode !== "local") {
|
|
455
558
|
return usageFailure("add", "local transport must be stdio");
|
|
456
559
|
}
|
|
457
560
|
const environment = parseAssignments(options.values.get("environment") ?? [], "environment");
|
|
@@ -476,13 +579,13 @@ function parseAdd(args: readonly string[]): McpCommandParseResult {
|
|
|
476
579
|
}
|
|
477
580
|
if (options.positionals.length !== 2)
|
|
478
581
|
return usageFailure("add", "remote add requires a name and URL");
|
|
479
|
-
if (
|
|
582
|
+
if (MCP_ADD_LOCAL_VALUE_OPTIONS.some((name) => options.values.has(name))) {
|
|
480
583
|
return usageFailure("add", "remote add cannot include local process options");
|
|
481
584
|
}
|
|
482
585
|
const url = options.positionals[1] ?? "";
|
|
483
586
|
if (!isHttpUrl(url)) return usageFailure("add", "remote URL must be absolute HTTP or HTTPS");
|
|
484
587
|
const transport = oneValue(options, "transport") ?? "http";
|
|
485
|
-
if (transport !== "http" && transport !== "sse")
|
|
588
|
+
if (transportMode !== "remote" || (transport !== "http" && transport !== "sse"))
|
|
486
589
|
return usageFailure("add", "remote transport must be http or sse");
|
|
487
590
|
const headers = parseAssignments(options.values.get("header") ?? [], "header");
|
|
488
591
|
if (typeof headers === "string") return usageFailure("add", headers);
|
|
@@ -509,11 +612,7 @@ function parseScopedServer(
|
|
|
509
612
|
kind: "remove" | "enable" | "disable",
|
|
510
613
|
args: readonly string[],
|
|
511
614
|
): McpCommandParseResult {
|
|
512
|
-
const options = parseOptions(
|
|
513
|
-
args,
|
|
514
|
-
new Set(kind === "remove" ? ["local", "logout"] : ["local"]),
|
|
515
|
-
new Set(),
|
|
516
|
-
);
|
|
615
|
+
const options = parseOptions(args, kind);
|
|
517
616
|
if (typeof options === "string") return usageFailure(kind, options);
|
|
518
617
|
if (options.positionals.length !== 1)
|
|
519
618
|
return usageFailure(kind, "exactly one server name is required");
|
|
@@ -553,7 +652,7 @@ export function parseMcpCommand(
|
|
|
553
652
|
if (commandName === "remove" || commandName === "enable" || commandName === "disable")
|
|
554
653
|
return parseScopedServer(commandName, rest);
|
|
555
654
|
if (commandName === "list") {
|
|
556
|
-
const options = parseOptions(rest,
|
|
655
|
+
const options = parseOptions(rest, "list");
|
|
557
656
|
if (typeof options === "string" || options.positionals.length > 0)
|
|
558
657
|
return usageFailure(
|
|
559
658
|
"list",
|
|
@@ -564,11 +663,7 @@ export function parseMcpCommand(
|
|
|
564
663
|
return { command: { json: options.flags.has("json"), kind: "list" }, ok: true };
|
|
565
664
|
}
|
|
566
665
|
if (commandName === "auth") {
|
|
567
|
-
const options = parseOptions(
|
|
568
|
-
rest,
|
|
569
|
-
new Set(["no-open"]),
|
|
570
|
-
new Set(["callback", "code", "state"]),
|
|
571
|
-
);
|
|
666
|
+
const options = parseOptions(rest, "auth");
|
|
572
667
|
if (typeof options === "string") return usageFailure("auth", options);
|
|
573
668
|
if (options.positionals.length !== 1)
|
|
574
669
|
return usageFailure(
|
|
@@ -595,7 +690,7 @@ export function parseMcpCommand(
|
|
|
595
690
|
};
|
|
596
691
|
}
|
|
597
692
|
if (commandName === "logout") {
|
|
598
|
-
const options = parseOptions(rest,
|
|
693
|
+
const options = parseOptions(rest, "logout");
|
|
599
694
|
if (typeof options === "string") return usageFailure("logout", options);
|
|
600
695
|
const all = options.flags.has("all");
|
|
601
696
|
const force = options.flags.has("force");
|
|
@@ -612,7 +707,7 @@ export function parseMcpCommand(
|
|
|
612
707
|
};
|
|
613
708
|
}
|
|
614
709
|
if (commandName === "test") {
|
|
615
|
-
const options = parseOptions(rest,
|
|
710
|
+
const options = parseOptions(rest, "test");
|
|
616
711
|
if (typeof options === "string") return usageFailure("test", options);
|
|
617
712
|
if (surface === "runtime" && options.flags.has("json"))
|
|
618
713
|
return usageFailure("test", "--json is standalone-only");
|
|
@@ -633,12 +728,16 @@ export function parseMcpCommand(
|
|
|
633
728
|
if (rest.length > 0) return usageFailure("status", "status accepts no arguments");
|
|
634
729
|
return { command: { includeHelp: false, kind: "status" }, ok: true };
|
|
635
730
|
}
|
|
731
|
+
if (commandName === "help") {
|
|
732
|
+
if (rest.length > 0) return usageFailure("help", "help accepts no arguments");
|
|
733
|
+
return { command: { kind: "help" }, ok: true };
|
|
734
|
+
}
|
|
636
735
|
if (commandName === "reconnect") {
|
|
637
736
|
if (rest.length !== 1) return usageFailure("reconnect", "exactly one server name is required");
|
|
638
737
|
return { command: { kind: "reconnect", server: rest[0] ?? "" }, ok: true };
|
|
639
738
|
}
|
|
640
739
|
if (commandName === "prompt") {
|
|
641
|
-
const options = parseOptions(rest,
|
|
740
|
+
const options = parseOptions(rest, "prompt");
|
|
642
741
|
if (typeof options === "string") return usageFailure("prompt", options);
|
|
643
742
|
if (options.positionals.length !== 2)
|
|
644
743
|
return usageFailure("prompt", "server and prompt names are required");
|
|
@@ -658,20 +757,15 @@ export function parseMcpCommand(
|
|
|
658
757
|
if (rest.length !== 2) return usageFailure(commandName, "server and resource URI are required");
|
|
659
758
|
return { command: { kind: commandName, server: rest[0] ?? "", uri: rest[1] ?? "" }, ok: true };
|
|
660
759
|
}
|
|
661
|
-
const options = parseOptions(rest,
|
|
760
|
+
const options = parseOptions(rest, "logs");
|
|
662
761
|
if (typeof options === "string" || options.positionals.length > 1)
|
|
663
762
|
return usageFailure(
|
|
664
763
|
"logs",
|
|
665
764
|
typeof options === "string" ? options : "logs accepts at most one server name",
|
|
666
765
|
);
|
|
667
|
-
const level = oneValue(options, "level");
|
|
668
|
-
if (level !== undefined && !isMcpLoggingLevel(level)) {
|
|
669
|
-
return usageFailure("logs", `unknown logging level ${level}`);
|
|
670
|
-
}
|
|
671
766
|
return {
|
|
672
767
|
command: {
|
|
673
768
|
kind: "logs",
|
|
674
|
-
...(level === undefined ? {} : { level }),
|
|
675
769
|
...(options.positionals[0] === undefined ? {} : { server: options.positionals[0] }),
|
|
676
770
|
},
|
|
677
771
|
ok: true,
|
|
@@ -813,12 +907,11 @@ export async function executeMcpCommand(
|
|
|
813
907
|
case "logs": {
|
|
814
908
|
const live = liveAdapter(adapters);
|
|
815
909
|
if ("exitCode" in live) return live;
|
|
816
|
-
result = await live.logs({
|
|
817
|
-
...(command.level === undefined ? {} : { level: command.level }),
|
|
818
|
-
...(command.server === undefined ? {} : { server: command.server }),
|
|
819
|
-
});
|
|
910
|
+
result = await live.logs(command.server === undefined ? {} : { server: command.server });
|
|
820
911
|
break;
|
|
821
912
|
}
|
|
913
|
+
case "help":
|
|
914
|
+
return successResult({ message: RUNTIME_HELP, ok: true }, false);
|
|
822
915
|
}
|
|
823
916
|
if (!result.ok) return adapterFailure(result.category, result.message);
|
|
824
917
|
const json = (command.kind === "list" || command.kind === "test") && command.json;
|
|
@@ -829,43 +922,91 @@ export async function executeMcpCommand(
|
|
|
829
922
|
}
|
|
830
923
|
}
|
|
831
924
|
|
|
832
|
-
/**
|
|
833
|
-
export
|
|
834
|
-
|
|
925
|
+
/** Current token and replacement boundary for an unfinished MCP command. */
|
|
926
|
+
export interface McpCommandCompletionToken {
|
|
927
|
+
/** Opening quote style to preserve when replacing the token. */
|
|
928
|
+
readonly quote: "'" | '"' | undefined;
|
|
929
|
+
/** Zero-based source offset where replacement begins. */
|
|
930
|
+
readonly start: number;
|
|
931
|
+
/** Decoded token value before the cursor. */
|
|
932
|
+
readonly value: string;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** Tolerant tokenization of a complete or unfinished MCP command prefix. */
|
|
936
|
+
export interface McpCommandCompletionPrefix {
|
|
937
|
+
/** Source text preserved before replacing the current token. */
|
|
938
|
+
readonly beforeCurrent: string;
|
|
939
|
+
/** Decoded tokens completed before the current token. */
|
|
940
|
+
readonly completed: readonly string[];
|
|
941
|
+
/** Token being completed at the cursor. */
|
|
942
|
+
readonly current: McpCommandCompletionToken;
|
|
943
|
+
/** Whether the source ends inside a token rather than after whitespace. */
|
|
944
|
+
readonly currentStarted: boolean;
|
|
945
|
+
/** Whether the source ends before its opening quote closes. */
|
|
946
|
+
readonly incompleteQuote: boolean;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** Tokenize an unfinished MCP command while retaining its current replacement boundary. */
|
|
950
|
+
export function tokenizeMcpCommandCompletionPrefix(line: string): McpCommandCompletionPrefix {
|
|
951
|
+
const tokens: McpCommandCompletionToken[] = [];
|
|
835
952
|
let current = "";
|
|
836
953
|
let quote: "'" | '"' | undefined;
|
|
954
|
+
let quoteAtStart: "'" | '"' | undefined;
|
|
837
955
|
let escaping = false;
|
|
956
|
+
let start = line.length;
|
|
838
957
|
let tokenStarted = false;
|
|
839
|
-
for (
|
|
958
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
959
|
+
const character = line[index] ?? "";
|
|
840
960
|
if (escaping) {
|
|
841
961
|
current += character;
|
|
842
|
-
tokenStarted = true;
|
|
843
962
|
escaping = false;
|
|
844
963
|
} else if (character === "\\" && quote !== "'") {
|
|
964
|
+
if (!tokenStarted) start = index;
|
|
845
965
|
escaping = true;
|
|
846
966
|
tokenStarted = true;
|
|
847
967
|
} else if (quote !== undefined) {
|
|
848
968
|
if (character === quote) quote = undefined;
|
|
849
969
|
else current += character;
|
|
850
|
-
tokenStarted = true;
|
|
851
970
|
} else if (character === "'" || character === '"') {
|
|
971
|
+
if (!tokenStarted) {
|
|
972
|
+
quoteAtStart = character;
|
|
973
|
+
start = index;
|
|
974
|
+
}
|
|
852
975
|
quote = character;
|
|
853
976
|
tokenStarted = true;
|
|
854
977
|
} else if (/\s/u.test(character)) {
|
|
855
978
|
if (tokenStarted) {
|
|
856
|
-
tokens.push(current);
|
|
979
|
+
tokens.push({ quote: quoteAtStart, start, value: current });
|
|
857
980
|
current = "";
|
|
981
|
+
quoteAtStart = undefined;
|
|
858
982
|
tokenStarted = false;
|
|
859
983
|
}
|
|
860
984
|
} else {
|
|
985
|
+
if (!tokenStarted) start = index;
|
|
861
986
|
current += character;
|
|
862
987
|
tokenStarted = true;
|
|
863
988
|
}
|
|
864
989
|
}
|
|
865
|
-
if (quote !== undefined) throw new Error("MCP command has an unterminated quote");
|
|
866
990
|
if (escaping) current += "\\";
|
|
867
|
-
if (tokenStarted) tokens.push(current);
|
|
868
|
-
|
|
991
|
+
if (tokenStarted) tokens.push({ quote: quoteAtStart, start, value: current });
|
|
992
|
+
else tokens.push({ quote: undefined, start: line.length, value: "" });
|
|
993
|
+
const active = tokens.at(-1) ?? { quote: undefined, start: line.length, value: "" };
|
|
994
|
+
return {
|
|
995
|
+
beforeCurrent: line.slice(0, active.start),
|
|
996
|
+
completed: tokens.slice(0, -1).map(({ value }) => value),
|
|
997
|
+
current: active,
|
|
998
|
+
currentStarted: tokenStarted,
|
|
999
|
+
incompleteQuote: quote !== undefined,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/** Split a `/mcp` argument string without invoking a shell or expanding variables. */
|
|
1004
|
+
export function tokenizeMcpCommandLine(line: string): string[] {
|
|
1005
|
+
const prefix = tokenizeMcpCommandCompletionPrefix(line);
|
|
1006
|
+
if (prefix.incompleteQuote) throw new Error("MCP command has an unterminated quote");
|
|
1007
|
+
return prefix.currentStarted
|
|
1008
|
+
? [...prefix.completed, prefix.current.value]
|
|
1009
|
+
: [...prefix.completed];
|
|
869
1010
|
}
|
|
870
1011
|
|
|
871
1012
|
/** Parse and execute one pre-tokenized MCP command without reinterpreting argument contents. */
|