@ian-pascoe/pi-mcp 0.1.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/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/pi-mcp-cli.js +10948 -0
- package/package.json +64 -0
- package/src/index.ts +2 -0
- package/src/mcp-auth-store.ts +393 -0
- package/src/mcp-command.ts +893 -0
- package/src/mcp-content.ts +212 -0
- package/src/mcp-host.ts +971 -0
- package/src/mcp-oauth.ts +740 -0
- package/src/mcp-server-client.ts +375 -0
- package/src/mcp-session-files.ts +127 -0
- package/src/mcp-settings-store.ts +455 -0
- package/src/mcp-tool-catalog.ts +464 -0
- package/src/pi-mcp-cli.ts +507 -0
- package/src/pi-mcp-extension.ts +1013 -0
- package/src/pi-mcp-settings.ts +619 -0
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
/* oxlint-disable anti-slop/no-conditional-empty-object-spread -- Exact optional command fields are assembled only when their argv values are present. */
|
|
2
|
+
/* oxlint-disable anti-slop/no-runtime-typeof -- This module is the owning parser boundary for raw command-line strings and tagged parser results. */
|
|
3
|
+
|
|
4
|
+
/** JSON data returned by a command adapter and rendered by the shared command runner. */
|
|
5
|
+
export type McpCommandJsonValue =
|
|
6
|
+
| null
|
|
7
|
+
| boolean
|
|
8
|
+
| number
|
|
9
|
+
| string
|
|
10
|
+
| readonly McpCommandJsonValue[]
|
|
11
|
+
| { readonly [key: string]: McpCommandJsonValue };
|
|
12
|
+
|
|
13
|
+
/** Commands available through Pi's `/mcp` surface, in help order. */
|
|
14
|
+
export const MCP_COMMAND_NAMES = [
|
|
15
|
+
"list",
|
|
16
|
+
"add",
|
|
17
|
+
"remove",
|
|
18
|
+
"enable",
|
|
19
|
+
"disable",
|
|
20
|
+
"auth",
|
|
21
|
+
"logout",
|
|
22
|
+
"test",
|
|
23
|
+
"status",
|
|
24
|
+
"reconnect",
|
|
25
|
+
"prompt",
|
|
26
|
+
"subscribe",
|
|
27
|
+
"unsubscribe",
|
|
28
|
+
"logs",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
/** Persistent and offline commands available through the standalone executable. */
|
|
32
|
+
export const MCP_STANDALONE_COMMAND_NAMES = MCP_COMMAND_NAMES.slice(0, 8);
|
|
33
|
+
|
|
34
|
+
/** Command entrypoint that owns parsing and adapter execution. */
|
|
35
|
+
export type McpCommandSurface = "runtime" | "standalone";
|
|
36
|
+
|
|
37
|
+
/** Stable categories used for command results and process exit codes. */
|
|
38
|
+
export type McpCommandExitCategory =
|
|
39
|
+
| "success"
|
|
40
|
+
| "usage"
|
|
41
|
+
| "settings"
|
|
42
|
+
| "authentication"
|
|
43
|
+
| "connection"
|
|
44
|
+
| "runtime";
|
|
45
|
+
/** Settings layer targeted by a mutating command. */
|
|
46
|
+
export type McpCommandSettingsScope = "global" | "project";
|
|
47
|
+
|
|
48
|
+
/** Enabled Server Definition accepted by the `add` command. */
|
|
49
|
+
export type McpAddServerDefinition =
|
|
50
|
+
| {
|
|
51
|
+
readonly args: readonly string[];
|
|
52
|
+
readonly command: string;
|
|
53
|
+
readonly cwd?: string;
|
|
54
|
+
readonly enabled: true;
|
|
55
|
+
readonly environment: Readonly<Record<string, string>>;
|
|
56
|
+
readonly transport: "stdio";
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
readonly auth?:
|
|
60
|
+
| { readonly type: "none" }
|
|
61
|
+
| { readonly token: string; readonly type: "bearer" }
|
|
62
|
+
| {
|
|
63
|
+
readonly clientId?: string;
|
|
64
|
+
readonly clientSecret?: string;
|
|
65
|
+
readonly redirectUri?: string;
|
|
66
|
+
readonly scopes: readonly string[];
|
|
67
|
+
readonly type: "oauth";
|
|
68
|
+
};
|
|
69
|
+
readonly enabled: true;
|
|
70
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
71
|
+
readonly transport: "http" | "sse";
|
|
72
|
+
readonly url: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Parsed command and its normalized options. */
|
|
76
|
+
export type McpCommand =
|
|
77
|
+
| { readonly json: boolean; readonly kind: "list" }
|
|
78
|
+
| {
|
|
79
|
+
readonly definition: McpAddServerDefinition;
|
|
80
|
+
readonly kind: "add";
|
|
81
|
+
readonly name: string;
|
|
82
|
+
readonly scope: McpCommandSettingsScope;
|
|
83
|
+
}
|
|
84
|
+
| {
|
|
85
|
+
readonly kind: "remove";
|
|
86
|
+
readonly logout: boolean;
|
|
87
|
+
readonly name: string;
|
|
88
|
+
readonly scope: McpCommandSettingsScope;
|
|
89
|
+
}
|
|
90
|
+
| { readonly kind: "enable"; readonly name: string; readonly scope: McpCommandSettingsScope }
|
|
91
|
+
| { readonly kind: "disable"; readonly name: string; readonly scope: McpCommandSettingsScope }
|
|
92
|
+
| {
|
|
93
|
+
readonly callback?: string;
|
|
94
|
+
readonly code?: string;
|
|
95
|
+
readonly kind: "auth";
|
|
96
|
+
readonly noOpen: boolean;
|
|
97
|
+
readonly server: string;
|
|
98
|
+
readonly state?: string;
|
|
99
|
+
}
|
|
100
|
+
| {
|
|
101
|
+
readonly all: boolean;
|
|
102
|
+
readonly force: boolean;
|
|
103
|
+
readonly kind: "logout";
|
|
104
|
+
readonly server?: string;
|
|
105
|
+
}
|
|
106
|
+
| {
|
|
107
|
+
readonly all: boolean;
|
|
108
|
+
readonly json: boolean;
|
|
109
|
+
readonly kind: "test";
|
|
110
|
+
readonly server?: string;
|
|
111
|
+
}
|
|
112
|
+
| { readonly includeHelp: boolean; readonly kind: "status" }
|
|
113
|
+
| { readonly kind: "reconnect"; readonly server: string }
|
|
114
|
+
| {
|
|
115
|
+
readonly arguments: Readonly<Record<string, string>>;
|
|
116
|
+
readonly kind: "prompt";
|
|
117
|
+
readonly prompt: string;
|
|
118
|
+
readonly server: string;
|
|
119
|
+
}
|
|
120
|
+
| { readonly kind: "subscribe"; readonly server: string; readonly uri: string }
|
|
121
|
+
| { readonly kind: "unsubscribe"; readonly server: string; readonly uri: string }
|
|
122
|
+
| { readonly kind: "logs"; readonly level?: McpLoggingLevel; readonly server?: string };
|
|
123
|
+
|
|
124
|
+
/** Usage failure returned when command tokens cannot be parsed. */
|
|
125
|
+
export interface McpCommandParseFailure {
|
|
126
|
+
readonly category: "usage";
|
|
127
|
+
readonly message: string;
|
|
128
|
+
readonly ok: false;
|
|
129
|
+
readonly usage: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Successful parsed command or a usage failure. */
|
|
133
|
+
export type McpCommandParseResult =
|
|
134
|
+
| { readonly command: McpCommand; readonly ok: true }
|
|
135
|
+
| McpCommandParseFailure;
|
|
136
|
+
|
|
137
|
+
/** Successful adapter outcome and optional JSON payload. */
|
|
138
|
+
export interface McpCommandAdapterSuccess {
|
|
139
|
+
readonly data?: McpCommandJsonValue;
|
|
140
|
+
readonly message: string;
|
|
141
|
+
readonly ok: true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Adapter outcome for settings, authentication, connection, or runtime failure. */
|
|
145
|
+
export interface McpCommandAdapterFailure {
|
|
146
|
+
readonly category: Exclude<McpCommandExitCategory, "success" | "usage">;
|
|
147
|
+
readonly message: string;
|
|
148
|
+
readonly ok: false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Result returned by a command adapter. */
|
|
152
|
+
export type McpCommandAdapterResult = McpCommandAdapterSuccess | McpCommandAdapterFailure;
|
|
153
|
+
|
|
154
|
+
/** Options for one command variant, excluding its discriminant. */
|
|
155
|
+
export type McpCommandOptions<Kind extends McpCommand["kind"]> = Omit<
|
|
156
|
+
Extract<McpCommand, { kind: Kind }>,
|
|
157
|
+
"kind"
|
|
158
|
+
>;
|
|
159
|
+
|
|
160
|
+
/** Adapter implementations shared by runtime and standalone command surfaces. */
|
|
161
|
+
export interface McpCommandAdapters {
|
|
162
|
+
auth: {
|
|
163
|
+
authenticate(options: McpCommandOptions<"auth">): Promise<McpCommandAdapterResult>;
|
|
164
|
+
logout(options: McpCommandOptions<"logout">): Promise<McpCommandAdapterResult>;
|
|
165
|
+
};
|
|
166
|
+
live?: McpLiveCommandAdapter | undefined;
|
|
167
|
+
settings: {
|
|
168
|
+
add(options: McpCommandOptions<"add">): Promise<McpCommandAdapterResult>;
|
|
169
|
+
disable(options: McpCommandOptions<"disable">): Promise<McpCommandAdapterResult>;
|
|
170
|
+
enable(options: McpCommandOptions<"enable">): Promise<McpCommandAdapterResult>;
|
|
171
|
+
list(): Promise<McpCommandAdapterResult>;
|
|
172
|
+
remove(options: McpCommandOptions<"remove">): Promise<McpCommandAdapterResult>;
|
|
173
|
+
};
|
|
174
|
+
test: {
|
|
175
|
+
test(options: McpCommandOptions<"test">): Promise<McpCommandAdapterResult>;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Runtime-only adapter for live MCP Host operations. */
|
|
180
|
+
export interface McpLiveCommandAdapter {
|
|
181
|
+
connectInBackground(server: string): void;
|
|
182
|
+
disconnect(server: string): Promise<void>;
|
|
183
|
+
logs(options: McpCommandOptions<"logs">): Promise<McpCommandAdapterResult>;
|
|
184
|
+
prompt(options: McpCommandOptions<"prompt">): Promise<McpCommandAdapterResult>;
|
|
185
|
+
reconnect(server: string): Promise<McpCommandAdapterResult>;
|
|
186
|
+
status(): Promise<McpCommandAdapterResult>;
|
|
187
|
+
subscribe(options: McpCommandOptions<"subscribe">): Promise<McpCommandAdapterResult>;
|
|
188
|
+
unsubscribe(options: McpCommandOptions<"unsubscribe">): Promise<McpCommandAdapterResult>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Rendered command outcome with its process exit code. */
|
|
192
|
+
export interface McpCommandExecutionResult {
|
|
193
|
+
readonly category: McpCommandExitCategory;
|
|
194
|
+
readonly data?: McpCommandJsonValue;
|
|
195
|
+
readonly exitCode: number;
|
|
196
|
+
readonly ok: boolean;
|
|
197
|
+
readonly output: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const EXIT_CODES = {
|
|
201
|
+
authentication: 4,
|
|
202
|
+
connection: 5,
|
|
203
|
+
runtime: 6,
|
|
204
|
+
settings: 3,
|
|
205
|
+
success: 0,
|
|
206
|
+
usage: 2,
|
|
207
|
+
} as const satisfies Record<McpCommandExitCategory, number>;
|
|
208
|
+
|
|
209
|
+
const GENERAL_USAGE = `Usage: pi-mcp <command> [options]
|
|
210
|
+
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
|
+
const RUNTIME_HELP = `Commands: ${MCP_COMMAND_NAMES.join(", ")}`;
|
|
229
|
+
|
|
230
|
+
const COMMAND_USAGE = {
|
|
231
|
+
add: "Usage: pi-mcp add [-l|--local] <name> <url> [options]\n pi-mcp add [-l|--local] <name> [options] -- <command> [args...]",
|
|
232
|
+
auth: "Usage: pi-mcp auth <server> [--no-open] [--callback URL | --code CODE --state STATE]",
|
|
233
|
+
disable: "Usage: pi-mcp disable [-l|--local] <server>",
|
|
234
|
+
enable: "Usage: pi-mcp enable [-l|--local] <server>",
|
|
235
|
+
list: "Usage: pi-mcp list [--json]",
|
|
236
|
+
logout: "Usage: pi-mcp logout <server> | --all --force",
|
|
237
|
+
logs: "Usage: /mcp logs [server] [--level LEVEL]",
|
|
238
|
+
prompt: "Usage: /mcp prompt <server> <prompt> [--arg NAME=VALUE]...",
|
|
239
|
+
reconnect: "Usage: /mcp reconnect <server>",
|
|
240
|
+
remove: "Usage: pi-mcp remove [-l|--local] [--logout] <server>",
|
|
241
|
+
status: "Usage: /mcp status",
|
|
242
|
+
subscribe: "Usage: /mcp subscribe <server> <uri>",
|
|
243
|
+
test: "Usage: pi-mcp test <server> | --all [--json]",
|
|
244
|
+
unsubscribe: "Usage: /mcp unsubscribe <server> <uri>",
|
|
245
|
+
} as const satisfies Record<(typeof MCP_COMMAND_NAMES)[number], string>;
|
|
246
|
+
|
|
247
|
+
function usageFailure(
|
|
248
|
+
command: (typeof MCP_COMMAND_NAMES)[number] | undefined,
|
|
249
|
+
message: string,
|
|
250
|
+
): McpCommandParseFailure {
|
|
251
|
+
return {
|
|
252
|
+
category: "usage",
|
|
253
|
+
message,
|
|
254
|
+
ok: false,
|
|
255
|
+
usage: command === undefined ? GENERAL_USAGE : COMMAND_USAGE[command],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface ParsedOptions {
|
|
260
|
+
readonly flags: ReadonlySet<string>;
|
|
261
|
+
readonly positionals: readonly string[];
|
|
262
|
+
readonly tail: readonly string[] | undefined;
|
|
263
|
+
readonly values: ReadonlyMap<string, readonly string[]>;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function normalizeOptionName(rawName: string): string {
|
|
267
|
+
switch (rawName) {
|
|
268
|
+
case "-l":
|
|
269
|
+
case "--local":
|
|
270
|
+
return "local";
|
|
271
|
+
case "--env":
|
|
272
|
+
return "environment";
|
|
273
|
+
default:
|
|
274
|
+
return rawName.replace(/^--/, "");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function parseOptions(
|
|
279
|
+
tokens: readonly string[],
|
|
280
|
+
flagNames: ReadonlySet<string>,
|
|
281
|
+
valueNames: ReadonlySet<string>,
|
|
282
|
+
allowDelimiter = false,
|
|
283
|
+
): ParsedOptions | string {
|
|
284
|
+
const flags = new Set<string>();
|
|
285
|
+
const positionals: string[] = [];
|
|
286
|
+
const values = new Map<string, string[]>();
|
|
287
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
288
|
+
const token = tokens[index];
|
|
289
|
+
if (token === undefined) continue;
|
|
290
|
+
if (token === "--") {
|
|
291
|
+
if (!allowDelimiter) return "unexpected -- delimiter";
|
|
292
|
+
return { flags, positionals, tail: tokens.slice(index + 1), values };
|
|
293
|
+
}
|
|
294
|
+
if (!token.startsWith("-")) {
|
|
295
|
+
positionals.push(token);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const equalsIndex = token.indexOf("=");
|
|
299
|
+
const rawName = equalsIndex < 0 ? token : token.slice(0, equalsIndex);
|
|
300
|
+
const name = normalizeOptionName(rawName);
|
|
301
|
+
if (flagNames.has(name)) {
|
|
302
|
+
if (equalsIndex >= 0) return `option ${rawName} does not accept a value`;
|
|
303
|
+
flags.add(name);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (!valueNames.has(name)) return `unknown option ${rawName}`;
|
|
307
|
+
const value = equalsIndex < 0 ? tokens[index + 1] : token.slice(equalsIndex + 1);
|
|
308
|
+
if (value === undefined || (equalsIndex < 0 && value.startsWith("--"))) {
|
|
309
|
+
return `option ${rawName} requires a value`;
|
|
310
|
+
}
|
|
311
|
+
if (equalsIndex < 0) index += 1;
|
|
312
|
+
const existing = values.get(name) ?? [];
|
|
313
|
+
values.set(name, [...existing, value]);
|
|
314
|
+
}
|
|
315
|
+
return { flags, positionals, tail: undefined, values };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function oneValue(options: ParsedOptions, name: string): string | undefined {
|
|
319
|
+
return options.values.get(name)?.at(-1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function parseAssignments(
|
|
323
|
+
values: readonly string[],
|
|
324
|
+
kind: "environment" | "header" | "argument",
|
|
325
|
+
): Record<string, string> | string {
|
|
326
|
+
const result: Record<string, string> = {};
|
|
327
|
+
for (const value of values) {
|
|
328
|
+
let split = value.indexOf("=");
|
|
329
|
+
if (split < 0 && kind === "header") split = value.indexOf(":");
|
|
330
|
+
if (split <= 0) return `${kind} must use NAME=VALUE`;
|
|
331
|
+
const key = value.slice(0, split).trim();
|
|
332
|
+
const rawItem = value.slice(split + 1);
|
|
333
|
+
const item = kind === "header" ? rawItem.trim() : rawItem;
|
|
334
|
+
if (key.length === 0) return `${kind} name must not be empty`;
|
|
335
|
+
result[key] = item;
|
|
336
|
+
}
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function parseScope(options: ParsedOptions): McpCommandSettingsScope {
|
|
341
|
+
return options.flags.has("local") ? "project" : "global";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isHttpUrl(value: string): boolean {
|
|
345
|
+
try {
|
|
346
|
+
const url = new URL(value);
|
|
347
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
348
|
+
} catch {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function parseRemoteAuth(
|
|
354
|
+
options: ParsedOptions,
|
|
355
|
+
): McpAddServerDefinition extends infer _Definition
|
|
356
|
+
?
|
|
357
|
+
| Exclude<Extract<McpAddServerDefinition, { url: string }>["auth"], undefined>
|
|
358
|
+
| undefined
|
|
359
|
+
| string
|
|
360
|
+
: never {
|
|
361
|
+
const configuredType = oneValue(options, "auth");
|
|
362
|
+
const token = oneValue(options, "token");
|
|
363
|
+
const clientId = oneValue(options, "client-id");
|
|
364
|
+
const clientSecret = oneValue(options, "client-secret");
|
|
365
|
+
const redirectUri = oneValue(options, "redirect-uri");
|
|
366
|
+
const scopes = options.values.get("scope") ?? [];
|
|
367
|
+
const inferredType =
|
|
368
|
+
token !== undefined
|
|
369
|
+
? "bearer"
|
|
370
|
+
: clientId !== undefined ||
|
|
371
|
+
clientSecret !== undefined ||
|
|
372
|
+
redirectUri !== undefined ||
|
|
373
|
+
scopes.length > 0
|
|
374
|
+
? "oauth"
|
|
375
|
+
: undefined;
|
|
376
|
+
const type = configuredType ?? inferredType;
|
|
377
|
+
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
|
+
}
|
|
390
|
+
if (type === "bearer") {
|
|
391
|
+
if (token === undefined || token.length === 0) return "bearer auth requires --token";
|
|
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
|
+
}
|
|
400
|
+
return { token, type: "bearer" };
|
|
401
|
+
}
|
|
402
|
+
if (type !== "oauth") return "--auth must be none, bearer, or oauth";
|
|
403
|
+
if (token !== undefined) return "OAuth auth cannot include --token";
|
|
404
|
+
return {
|
|
405
|
+
...(clientId === undefined ? {} : { clientId }),
|
|
406
|
+
...(clientSecret === undefined ? {} : { clientSecret }),
|
|
407
|
+
...(redirectUri === undefined ? {} : { redirectUri }),
|
|
408
|
+
scopes: [...scopes],
|
|
409
|
+
type: "oauth",
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
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
|
+
);
|
|
431
|
+
if (typeof options === "string") return usageFailure("add", options);
|
|
432
|
+
const name = options.positionals[0];
|
|
433
|
+
if (name === undefined || name.length === 0)
|
|
434
|
+
return usageFailure("add", "server name is required");
|
|
435
|
+
if (options.tail !== undefined) {
|
|
436
|
+
if (options.positionals.length !== 1)
|
|
437
|
+
return usageFailure("add", "a local add cannot also include a URL");
|
|
438
|
+
const command = options.tail[0];
|
|
439
|
+
if (command === undefined || command.length === 0)
|
|
440
|
+
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
|
+
]) {
|
|
450
|
+
if (options.values.has(remoteOption))
|
|
451
|
+
return usageFailure("add", `local add cannot include --${remoteOption}`);
|
|
452
|
+
}
|
|
453
|
+
const transport = oneValue(options, "transport");
|
|
454
|
+
if (transport !== undefined && transport !== "stdio") {
|
|
455
|
+
return usageFailure("add", "local transport must be stdio");
|
|
456
|
+
}
|
|
457
|
+
const environment = parseAssignments(options.values.get("environment") ?? [], "environment");
|
|
458
|
+
if (typeof environment === "string") return usageFailure("add", environment);
|
|
459
|
+
const cwd = oneValue(options, "cwd");
|
|
460
|
+
return {
|
|
461
|
+
command: {
|
|
462
|
+
definition: {
|
|
463
|
+
args: options.tail.slice(1),
|
|
464
|
+
command,
|
|
465
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
466
|
+
enabled: true,
|
|
467
|
+
environment,
|
|
468
|
+
transport: "stdio",
|
|
469
|
+
},
|
|
470
|
+
kind: "add",
|
|
471
|
+
name,
|
|
472
|
+
scope: parseScope(options),
|
|
473
|
+
},
|
|
474
|
+
ok: true,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (options.positionals.length !== 2)
|
|
478
|
+
return usageFailure("add", "remote add requires a name and URL");
|
|
479
|
+
if (options.values.has("cwd") || options.values.has("environment")) {
|
|
480
|
+
return usageFailure("add", "remote add cannot include local process options");
|
|
481
|
+
}
|
|
482
|
+
const url = options.positionals[1] ?? "";
|
|
483
|
+
if (!isHttpUrl(url)) return usageFailure("add", "remote URL must be absolute HTTP or HTTPS");
|
|
484
|
+
const transport = oneValue(options, "transport") ?? "http";
|
|
485
|
+
if (transport !== "http" && transport !== "sse")
|
|
486
|
+
return usageFailure("add", "remote transport must be http or sse");
|
|
487
|
+
const headers = parseAssignments(options.values.get("header") ?? [], "header");
|
|
488
|
+
if (typeof headers === "string") return usageFailure("add", headers);
|
|
489
|
+
const auth = parseRemoteAuth(options);
|
|
490
|
+
if (typeof auth === "string") return usageFailure("add", auth);
|
|
491
|
+
return {
|
|
492
|
+
command: {
|
|
493
|
+
definition: {
|
|
494
|
+
...(auth === undefined ? {} : { auth }),
|
|
495
|
+
enabled: true,
|
|
496
|
+
headers,
|
|
497
|
+
transport,
|
|
498
|
+
url,
|
|
499
|
+
},
|
|
500
|
+
kind: "add",
|
|
501
|
+
name,
|
|
502
|
+
scope: parseScope(options),
|
|
503
|
+
},
|
|
504
|
+
ok: true,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function parseScopedServer(
|
|
509
|
+
kind: "remove" | "enable" | "disable",
|
|
510
|
+
args: readonly string[],
|
|
511
|
+
): McpCommandParseResult {
|
|
512
|
+
const options = parseOptions(
|
|
513
|
+
args,
|
|
514
|
+
new Set(kind === "remove" ? ["local", "logout"] : ["local"]),
|
|
515
|
+
new Set(),
|
|
516
|
+
);
|
|
517
|
+
if (typeof options === "string") return usageFailure(kind, options);
|
|
518
|
+
if (options.positionals.length !== 1)
|
|
519
|
+
return usageFailure(kind, "exactly one server name is required");
|
|
520
|
+
const name = options.positionals[0] ?? "";
|
|
521
|
+
const scope = parseScope(options);
|
|
522
|
+
if (kind === "remove") {
|
|
523
|
+
return { command: { kind, logout: options.flags.has("logout"), name, scope }, ok: true };
|
|
524
|
+
}
|
|
525
|
+
return kind === "enable"
|
|
526
|
+
? { command: { kind, name, scope }, ok: true }
|
|
527
|
+
: { command: { kind: "disable", name, scope }, ok: true };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Parse one tokenized command for the standalone executable or Pi runtime. */
|
|
531
|
+
export function parseMcpCommand(
|
|
532
|
+
args: readonly string[],
|
|
533
|
+
surface: McpCommandSurface,
|
|
534
|
+
): McpCommandParseResult {
|
|
535
|
+
const name = args[0];
|
|
536
|
+
if (name === undefined) {
|
|
537
|
+
return surface === "runtime"
|
|
538
|
+
? { command: { includeHelp: true, kind: "status" }, ok: true }
|
|
539
|
+
: usageFailure(undefined, "command is required");
|
|
540
|
+
}
|
|
541
|
+
if (!MCP_COMMAND_NAMES.some((candidate) => candidate === name))
|
|
542
|
+
return usageFailure(undefined, `unknown command ${name}`);
|
|
543
|
+
// SAFETY: The membership check immediately above proves `name` is an approved command name.
|
|
544
|
+
const commandName = name as (typeof MCP_COMMAND_NAMES)[number];
|
|
545
|
+
if (
|
|
546
|
+
surface === "standalone" &&
|
|
547
|
+
!MCP_STANDALONE_COMMAND_NAMES.some((candidate) => candidate === commandName)
|
|
548
|
+
) {
|
|
549
|
+
return usageFailure(undefined, `${commandName} is available only through /mcp`);
|
|
550
|
+
}
|
|
551
|
+
const rest = args.slice(1);
|
|
552
|
+
if (commandName === "add") return parseAdd(rest);
|
|
553
|
+
if (commandName === "remove" || commandName === "enable" || commandName === "disable")
|
|
554
|
+
return parseScopedServer(commandName, rest);
|
|
555
|
+
if (commandName === "list") {
|
|
556
|
+
const options = parseOptions(rest, new Set(["json"]), new Set());
|
|
557
|
+
if (typeof options === "string" || options.positionals.length > 0)
|
|
558
|
+
return usageFailure(
|
|
559
|
+
"list",
|
|
560
|
+
typeof options === "string" ? options : "list accepts no arguments",
|
|
561
|
+
);
|
|
562
|
+
if (surface === "runtime" && options.flags.has("json"))
|
|
563
|
+
return usageFailure("list", "--json is standalone-only");
|
|
564
|
+
return { command: { json: options.flags.has("json"), kind: "list" }, ok: true };
|
|
565
|
+
}
|
|
566
|
+
if (commandName === "auth") {
|
|
567
|
+
const options = parseOptions(
|
|
568
|
+
rest,
|
|
569
|
+
new Set(["no-open"]),
|
|
570
|
+
new Set(["callback", "code", "state"]),
|
|
571
|
+
);
|
|
572
|
+
if (typeof options === "string") return usageFailure("auth", options);
|
|
573
|
+
if (options.positionals.length !== 1)
|
|
574
|
+
return usageFailure(
|
|
575
|
+
"auth",
|
|
576
|
+
"exactly one server name is required; bare authorization codes are not accepted",
|
|
577
|
+
);
|
|
578
|
+
const callback = oneValue(options, "callback");
|
|
579
|
+
const code = oneValue(options, "code");
|
|
580
|
+
const state = oneValue(options, "state");
|
|
581
|
+
if ((code === undefined) !== (state === undefined))
|
|
582
|
+
return usageFailure("auth", "--code and --state must be supplied together");
|
|
583
|
+
if (callback !== undefined && code !== undefined)
|
|
584
|
+
return usageFailure("auth", "use either --callback or --code with --state");
|
|
585
|
+
return {
|
|
586
|
+
command: {
|
|
587
|
+
...(callback === undefined ? {} : { callback }),
|
|
588
|
+
...(code === undefined ? {} : { code }),
|
|
589
|
+
kind: "auth",
|
|
590
|
+
noOpen: options.flags.has("no-open"),
|
|
591
|
+
server: options.positionals[0] ?? "",
|
|
592
|
+
...(state === undefined ? {} : { state }),
|
|
593
|
+
},
|
|
594
|
+
ok: true,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
if (commandName === "logout") {
|
|
598
|
+
const options = parseOptions(rest, new Set(["all", "force"]), new Set());
|
|
599
|
+
if (typeof options === "string") return usageFailure("logout", options);
|
|
600
|
+
const all = options.flags.has("all");
|
|
601
|
+
const force = options.flags.has("force");
|
|
602
|
+
if (all || force) {
|
|
603
|
+
return all && force && options.positionals.length === 0
|
|
604
|
+
? { command: { all: true, force: true, kind: "logout" }, ok: true }
|
|
605
|
+
: usageFailure("logout", "auth-store reset requires exactly --all --force");
|
|
606
|
+
}
|
|
607
|
+
if (options.positionals.length !== 1)
|
|
608
|
+
return usageFailure("logout", "exactly one server name is required");
|
|
609
|
+
return {
|
|
610
|
+
command: { all: false, force: false, kind: "logout", server: options.positionals[0] ?? "" },
|
|
611
|
+
ok: true,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
if (commandName === "test") {
|
|
615
|
+
const options = parseOptions(rest, new Set(["all", "json"]), new Set());
|
|
616
|
+
if (typeof options === "string") return usageFailure("test", options);
|
|
617
|
+
if (surface === "runtime" && options.flags.has("json"))
|
|
618
|
+
return usageFailure("test", "--json is standalone-only");
|
|
619
|
+
const all = options.flags.has("all");
|
|
620
|
+
if ((all && options.positionals.length > 0) || (!all && options.positionals.length !== 1))
|
|
621
|
+
return usageFailure("test", "select one server or explicit --all");
|
|
622
|
+
return {
|
|
623
|
+
command: {
|
|
624
|
+
all,
|
|
625
|
+
json: options.flags.has("json"),
|
|
626
|
+
kind: "test",
|
|
627
|
+
...(all ? {} : { server: options.positionals[0] ?? "" }),
|
|
628
|
+
},
|
|
629
|
+
ok: true,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
if (commandName === "status") {
|
|
633
|
+
if (rest.length > 0) return usageFailure("status", "status accepts no arguments");
|
|
634
|
+
return { command: { includeHelp: false, kind: "status" }, ok: true };
|
|
635
|
+
}
|
|
636
|
+
if (commandName === "reconnect") {
|
|
637
|
+
if (rest.length !== 1) return usageFailure("reconnect", "exactly one server name is required");
|
|
638
|
+
return { command: { kind: "reconnect", server: rest[0] ?? "" }, ok: true };
|
|
639
|
+
}
|
|
640
|
+
if (commandName === "prompt") {
|
|
641
|
+
const options = parseOptions(rest, new Set(), new Set(["arg"]));
|
|
642
|
+
if (typeof options === "string") return usageFailure("prompt", options);
|
|
643
|
+
if (options.positionals.length !== 2)
|
|
644
|
+
return usageFailure("prompt", "server and prompt names are required");
|
|
645
|
+
const arguments_ = parseAssignments(options.values.get("arg") ?? [], "argument");
|
|
646
|
+
if (typeof arguments_ === "string") return usageFailure("prompt", arguments_);
|
|
647
|
+
return {
|
|
648
|
+
command: {
|
|
649
|
+
arguments: arguments_,
|
|
650
|
+
kind: "prompt",
|
|
651
|
+
prompt: options.positionals[1] ?? "",
|
|
652
|
+
server: options.positionals[0] ?? "",
|
|
653
|
+
},
|
|
654
|
+
ok: true,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
if (commandName === "subscribe" || commandName === "unsubscribe") {
|
|
658
|
+
if (rest.length !== 2) return usageFailure(commandName, "server and resource URI are required");
|
|
659
|
+
return { command: { kind: commandName, server: rest[0] ?? "", uri: rest[1] ?? "" }, ok: true };
|
|
660
|
+
}
|
|
661
|
+
const options = parseOptions(rest, new Set(), new Set(["level"]));
|
|
662
|
+
if (typeof options === "string" || options.positionals.length > 1)
|
|
663
|
+
return usageFailure(
|
|
664
|
+
"logs",
|
|
665
|
+
typeof options === "string" ? options : "logs accepts at most one server name",
|
|
666
|
+
);
|
|
667
|
+
const level = oneValue(options, "level");
|
|
668
|
+
if (level !== undefined && !isMcpLoggingLevel(level)) {
|
|
669
|
+
return usageFailure("logs", `unknown logging level ${level}`);
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
command: {
|
|
673
|
+
kind: "logs",
|
|
674
|
+
...(level === undefined ? {} : { level }),
|
|
675
|
+
...(options.positionals[0] === undefined ? {} : { server: options.positionals[0] }),
|
|
676
|
+
},
|
|
677
|
+
ok: true,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function adapterFailure(
|
|
682
|
+
category: Exclude<McpCommandExitCategory, "success">,
|
|
683
|
+
message: string,
|
|
684
|
+
usage?: string,
|
|
685
|
+
): McpCommandExecutionResult {
|
|
686
|
+
return {
|
|
687
|
+
category,
|
|
688
|
+
exitCode: EXIT_CODES[category],
|
|
689
|
+
ok: false,
|
|
690
|
+
output:
|
|
691
|
+
category === "usage"
|
|
692
|
+
? `Pi MCP: ${message}\n${usage ?? GENERAL_USAGE}\n`
|
|
693
|
+
: `Pi MCP: ${message}\n`,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function successResult(
|
|
698
|
+
result: McpCommandAdapterSuccess,
|
|
699
|
+
json: boolean,
|
|
700
|
+
suffix = "",
|
|
701
|
+
): McpCommandExecutionResult {
|
|
702
|
+
const output = json
|
|
703
|
+
? `${JSON.stringify(result.data ?? { message: result.message }, undefined, 2)}\n`
|
|
704
|
+
: `${result.message}${suffix}\n`;
|
|
705
|
+
return {
|
|
706
|
+
category: "success",
|
|
707
|
+
...(result.data === undefined ? {} : { data: result.data }),
|
|
708
|
+
exitCode: 0,
|
|
709
|
+
ok: true,
|
|
710
|
+
output,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function liveAdapter(
|
|
715
|
+
adapters: McpCommandAdapters,
|
|
716
|
+
): McpLiveCommandAdapter | McpCommandExecutionResult {
|
|
717
|
+
return adapters.live ?? adapterFailure("runtime", "live MCP Host is unavailable");
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Execute one parsed command through injected persistence, auth, test, and live-Host adapters. */
|
|
721
|
+
export async function executeMcpCommand(
|
|
722
|
+
command: McpCommand,
|
|
723
|
+
adapters: McpCommandAdapters,
|
|
724
|
+
surface: McpCommandSurface = "standalone",
|
|
725
|
+
): Promise<McpCommandExecutionResult> {
|
|
726
|
+
try {
|
|
727
|
+
let result: McpCommandAdapterResult;
|
|
728
|
+
switch (command.kind) {
|
|
729
|
+
case "list":
|
|
730
|
+
result = await adapters.settings.list();
|
|
731
|
+
break;
|
|
732
|
+
case "add":
|
|
733
|
+
result = await adapters.settings.add({
|
|
734
|
+
definition: command.definition,
|
|
735
|
+
name: command.name,
|
|
736
|
+
scope: command.scope,
|
|
737
|
+
});
|
|
738
|
+
if (result.ok && surface === "runtime") adapters.live?.connectInBackground(command.name);
|
|
739
|
+
break;
|
|
740
|
+
case "remove":
|
|
741
|
+
result = await adapters.settings.remove({
|
|
742
|
+
logout: command.logout,
|
|
743
|
+
name: command.name,
|
|
744
|
+
scope: command.scope,
|
|
745
|
+
});
|
|
746
|
+
if (result.ok && surface === "runtime") await adapters.live?.disconnect(command.name);
|
|
747
|
+
break;
|
|
748
|
+
case "enable":
|
|
749
|
+
result = await adapters.settings.enable({ name: command.name, scope: command.scope });
|
|
750
|
+
if (result.ok && surface === "runtime") adapters.live?.connectInBackground(command.name);
|
|
751
|
+
break;
|
|
752
|
+
case "disable":
|
|
753
|
+
result = await adapters.settings.disable({ name: command.name, scope: command.scope });
|
|
754
|
+
if (result.ok && surface === "runtime") await adapters.live?.disconnect(command.name);
|
|
755
|
+
break;
|
|
756
|
+
case "auth":
|
|
757
|
+
result = await adapters.auth.authenticate({
|
|
758
|
+
...(command.callback === undefined ? {} : { callback: command.callback }),
|
|
759
|
+
...(command.code === undefined ? {} : { code: command.code }),
|
|
760
|
+
noOpen: command.noOpen,
|
|
761
|
+
server: command.server,
|
|
762
|
+
...(command.state === undefined ? {} : { state: command.state }),
|
|
763
|
+
});
|
|
764
|
+
break;
|
|
765
|
+
case "logout":
|
|
766
|
+
result = await adapters.auth.logout({
|
|
767
|
+
all: command.all,
|
|
768
|
+
force: command.force,
|
|
769
|
+
...(command.server === undefined ? {} : { server: command.server }),
|
|
770
|
+
});
|
|
771
|
+
break;
|
|
772
|
+
case "test":
|
|
773
|
+
result = await adapters.test.test({
|
|
774
|
+
all: command.all,
|
|
775
|
+
json: command.json,
|
|
776
|
+
...(command.server === undefined ? {} : { server: command.server }),
|
|
777
|
+
});
|
|
778
|
+
break;
|
|
779
|
+
case "status": {
|
|
780
|
+
const live = liveAdapter(adapters);
|
|
781
|
+
if ("exitCode" in live) return live;
|
|
782
|
+
result = await live.status();
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
case "reconnect": {
|
|
786
|
+
const live = liveAdapter(adapters);
|
|
787
|
+
if ("exitCode" in live) return live;
|
|
788
|
+
result = await live.reconnect(command.server);
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
case "prompt": {
|
|
792
|
+
const live = liveAdapter(adapters);
|
|
793
|
+
if ("exitCode" in live) return live;
|
|
794
|
+
result = await live.prompt({
|
|
795
|
+
arguments: command.arguments,
|
|
796
|
+
prompt: command.prompt,
|
|
797
|
+
server: command.server,
|
|
798
|
+
});
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
case "subscribe": {
|
|
802
|
+
const live = liveAdapter(adapters);
|
|
803
|
+
if ("exitCode" in live) return live;
|
|
804
|
+
result = await live.subscribe({ server: command.server, uri: command.uri });
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
case "unsubscribe": {
|
|
808
|
+
const live = liveAdapter(adapters);
|
|
809
|
+
if ("exitCode" in live) return live;
|
|
810
|
+
result = await live.unsubscribe({ server: command.server, uri: command.uri });
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
case "logs": {
|
|
814
|
+
const live = liveAdapter(adapters);
|
|
815
|
+
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
|
+
});
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (!result.ok) return adapterFailure(result.category, result.message);
|
|
824
|
+
const json = (command.kind === "list" || command.kind === "test") && command.json;
|
|
825
|
+
const suffix = command.kind === "status" && command.includeHelp ? `\n\n${RUNTIME_HELP}` : "";
|
|
826
|
+
return successResult(result, json, suffix);
|
|
827
|
+
} catch {
|
|
828
|
+
return adapterFailure("runtime", "command failed unexpectedly");
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** Split a `/mcp` argument string without invoking a shell or expanding variables. */
|
|
833
|
+
export function tokenizeMcpCommandLine(line: string): string[] {
|
|
834
|
+
const tokens: string[] = [];
|
|
835
|
+
let current = "";
|
|
836
|
+
let quote: "'" | '"' | undefined;
|
|
837
|
+
let escaping = false;
|
|
838
|
+
let tokenStarted = false;
|
|
839
|
+
for (const character of line) {
|
|
840
|
+
if (escaping) {
|
|
841
|
+
current += character;
|
|
842
|
+
tokenStarted = true;
|
|
843
|
+
escaping = false;
|
|
844
|
+
} else if (character === "\\" && quote !== "'") {
|
|
845
|
+
escaping = true;
|
|
846
|
+
tokenStarted = true;
|
|
847
|
+
} else if (quote !== undefined) {
|
|
848
|
+
if (character === quote) quote = undefined;
|
|
849
|
+
else current += character;
|
|
850
|
+
tokenStarted = true;
|
|
851
|
+
} else if (character === "'" || character === '"') {
|
|
852
|
+
quote = character;
|
|
853
|
+
tokenStarted = true;
|
|
854
|
+
} else if (/\s/u.test(character)) {
|
|
855
|
+
if (tokenStarted) {
|
|
856
|
+
tokens.push(current);
|
|
857
|
+
current = "";
|
|
858
|
+
tokenStarted = false;
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
current += character;
|
|
862
|
+
tokenStarted = true;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (quote !== undefined) throw new Error("MCP command has an unterminated quote");
|
|
866
|
+
if (escaping) current += "\\";
|
|
867
|
+
if (tokenStarted) tokens.push(current);
|
|
868
|
+
return tokens;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/** Parse and execute one pre-tokenized MCP command without reinterpreting argument contents. */
|
|
872
|
+
export async function runMcpCommandTokens(
|
|
873
|
+
tokens: readonly string[],
|
|
874
|
+
surface: McpCommandSurface,
|
|
875
|
+
adapters: McpCommandAdapters,
|
|
876
|
+
): Promise<McpCommandExecutionResult> {
|
|
877
|
+
const parsed = parseMcpCommand(tokens, surface);
|
|
878
|
+
if (!parsed.ok) return adapterFailure("usage", parsed.message, parsed.usage);
|
|
879
|
+
return executeMcpCommand(parsed.command, adapters, surface);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Tokenize, parse, and execute one shared command line without throwing through its caller. */
|
|
883
|
+
export async function runMcpCommandLine(
|
|
884
|
+
line: string,
|
|
885
|
+
surface: McpCommandSurface,
|
|
886
|
+
adapters: McpCommandAdapters,
|
|
887
|
+
): Promise<McpCommandExecutionResult> {
|
|
888
|
+
try {
|
|
889
|
+
return runMcpCommandTokens(tokenizeMcpCommandLine(line), surface, adapters);
|
|
890
|
+
} catch {
|
|
891
|
+
return adapterFailure("usage", "invalid quoting", GENERAL_USAGE);
|
|
892
|
+
}
|
|
893
|
+
}
|