@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
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/* oxlint-disable anti-slop/no-conditional-empty-object-spread -- Exact optional completion descriptions are included only when their source supplies one. */
|
|
2
|
+
import {
|
|
3
|
+
MCP_ADD_LOCAL_VALUE_OPTIONS,
|
|
4
|
+
MCP_ADD_OAUTH_VALUE_OPTIONS,
|
|
5
|
+
MCP_ADD_REMOTE_VALUE_OPTIONS,
|
|
6
|
+
MCP_COMMAND_NAMES,
|
|
7
|
+
MCP_COMMAND_OPTIONS,
|
|
8
|
+
classifyMcpAddAuthentication,
|
|
9
|
+
classifyMcpAddTransportMode,
|
|
10
|
+
scanMcpCommandOptions,
|
|
11
|
+
tokenizeMcpCommandCompletionPrefix,
|
|
12
|
+
type McpAddAuthenticationMode,
|
|
13
|
+
type McpAddTransportMode,
|
|
14
|
+
type McpCommandCompletionPrefix,
|
|
15
|
+
type McpCommandName,
|
|
16
|
+
type McpCommandScannedOptions,
|
|
17
|
+
} from "./mcp-command.js";
|
|
18
|
+
import type { McpHost } from "./mcp-host.js";
|
|
19
|
+
|
|
20
|
+
const MAX_MCP_COMMAND_COMPLETIONS = 100;
|
|
21
|
+
|
|
22
|
+
const COMMAND_DESCRIPTIONS = {
|
|
23
|
+
add: "Add an MCP Server Definition",
|
|
24
|
+
auth: "Authenticate an MCP Server",
|
|
25
|
+
disable: "Disable an MCP Server",
|
|
26
|
+
enable: "Enable an MCP Server",
|
|
27
|
+
help: "Show MCP command help",
|
|
28
|
+
list: "List configured MCP Servers",
|
|
29
|
+
logout: "Remove stored authentication",
|
|
30
|
+
logs: "Read retained Server logs",
|
|
31
|
+
prompt: "Run an MCP Prompt",
|
|
32
|
+
reconnect: "Reconnect an MCP Server",
|
|
33
|
+
remove: "Remove an MCP Server Definition",
|
|
34
|
+
status: "Show live MCP Server status",
|
|
35
|
+
subscribe: "Subscribe to an MCP Resource",
|
|
36
|
+
test: "Test an MCP Server connection",
|
|
37
|
+
unsubscribe: "Unsubscribe from an MCP Resource",
|
|
38
|
+
} as const satisfies Record<(typeof MCP_COMMAND_NAMES)[number], string>;
|
|
39
|
+
|
|
40
|
+
const OPTION_DESCRIPTIONS = {
|
|
41
|
+
"--all": "Target every configured Server",
|
|
42
|
+
"--arg": "Set a Prompt argument",
|
|
43
|
+
"--auth": "Select remote authentication",
|
|
44
|
+
"--callback": "Complete OAuth from a callback URL",
|
|
45
|
+
"--client-id": "Set an OAuth client ID",
|
|
46
|
+
"--client-secret": "Set an OAuth client secret",
|
|
47
|
+
"--code": "Complete OAuth with a code",
|
|
48
|
+
"--cwd": "Set the stdio working directory",
|
|
49
|
+
"--environment": "Set a stdio environment variable",
|
|
50
|
+
"--force": "Confirm removal of all stored authentication",
|
|
51
|
+
"--header": "Set an HTTP request header",
|
|
52
|
+
"--local": "Write project MCP settings",
|
|
53
|
+
"--logout": "Remove stored authentication too",
|
|
54
|
+
"--no-open": "Do not open the authorization URL",
|
|
55
|
+
"--redirect-uri": "Set the OAuth redirect URI",
|
|
56
|
+
"--scope": "Add an OAuth scope",
|
|
57
|
+
"--state": "Complete OAuth with matching state",
|
|
58
|
+
"--token": "Set a bearer token",
|
|
59
|
+
"--transport": "Select the MCP transport",
|
|
60
|
+
"--": "Start the stdio command",
|
|
61
|
+
} as const satisfies Readonly<Record<string, string>>;
|
|
62
|
+
|
|
63
|
+
const REPEATABLE_OPTIONS = new Set(["arg", "environment", "header", "scope"]);
|
|
64
|
+
const REMOTE_ADD_OPTIONS = new Set<string>(MCP_ADD_REMOTE_VALUE_OPTIONS);
|
|
65
|
+
const LOCAL_ADD_OPTIONS = new Set<string>(MCP_ADD_LOCAL_VALUE_OPTIONS);
|
|
66
|
+
|
|
67
|
+
/** Host reads used to complete `/mcp` commands without changing MCP state. */
|
|
68
|
+
export type McpCommandCompletionHost = Pick<
|
|
69
|
+
McpHost,
|
|
70
|
+
"completePromptArgument" | "listPrompts" | "listResources" | "listStatuses" | "listSubscriptions"
|
|
71
|
+
>;
|
|
72
|
+
|
|
73
|
+
/** One Pi autocomplete row whose value replaces the complete `/mcp` argument prefix. */
|
|
74
|
+
export interface McpCommandCompletionItem {
|
|
75
|
+
/** Optional explanatory text shown beside the candidate. */
|
|
76
|
+
readonly description?: string;
|
|
77
|
+
/** Human-readable candidate text. */
|
|
78
|
+
readonly label: string;
|
|
79
|
+
/** Complete `/mcp` argument prefix inserted by Pi. */
|
|
80
|
+
readonly value: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface CompletionCandidate {
|
|
84
|
+
readonly description?: string;
|
|
85
|
+
readonly label: string;
|
|
86
|
+
readonly suffix?: "none" | "space";
|
|
87
|
+
readonly token?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function quoteMcpCommandToken(value: string, preferred: "'" | '"' | undefined): string {
|
|
91
|
+
if (preferred === "'" && !value.includes("'")) return `'${value}'`;
|
|
92
|
+
if (preferred === '"' || /[\s'"\\]/u.test(value)) {
|
|
93
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function completionItems(
|
|
99
|
+
prefix: McpCommandCompletionPrefix,
|
|
100
|
+
candidates: readonly CompletionCandidate[],
|
|
101
|
+
query = prefix.current.value,
|
|
102
|
+
): McpCommandCompletionItem[] | null {
|
|
103
|
+
const normalizedQuery = query.toLocaleLowerCase();
|
|
104
|
+
const items = candidates
|
|
105
|
+
.filter(({ label }) => label.toLocaleLowerCase().startsWith(normalizedQuery))
|
|
106
|
+
.sort(
|
|
107
|
+
({ label: left }, { label: right }) =>
|
|
108
|
+
left.toLocaleLowerCase().localeCompare(right.toLocaleLowerCase()) ||
|
|
109
|
+
left.localeCompare(right),
|
|
110
|
+
)
|
|
111
|
+
.slice(0, MAX_MCP_COMMAND_COMPLETIONS)
|
|
112
|
+
.map(({ description, label, suffix = "space", token = label }) => ({
|
|
113
|
+
...(description === undefined ? {} : { description }),
|
|
114
|
+
label,
|
|
115
|
+
value: `${prefix.beforeCurrent}${quoteMcpCommandToken(token, prefix.current.quote)}${suffix === "space" ? " " : ""}`,
|
|
116
|
+
}));
|
|
117
|
+
return items.length === 0 ? null : items;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function optionCandidate(name: string, suffix: "none" | "space" = "space"): CompletionCandidate {
|
|
121
|
+
const token = `--${name}`;
|
|
122
|
+
// SAFETY: Unknown option names intentionally produce no description; known names index this closed presentation map.
|
|
123
|
+
const description = OPTION_DESCRIPTIONS[token as keyof typeof OPTION_DESCRIPTIONS];
|
|
124
|
+
return { ...(description === undefined ? {} : { description }), label: token, suffix };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function availableOptionCandidates(
|
|
128
|
+
command: McpCommandName,
|
|
129
|
+
parsed: McpCommandScannedOptions,
|
|
130
|
+
allowed?: ReadonlySet<string>,
|
|
131
|
+
flagSuffix: "none" | "space" = "space",
|
|
132
|
+
): CompletionCandidate[] {
|
|
133
|
+
const valueNames = new Set<string>(MCP_COMMAND_OPTIONS[command].values);
|
|
134
|
+
return [...MCP_COMMAND_OPTIONS[command].flags, ...MCP_COMMAND_OPTIONS[command].values]
|
|
135
|
+
.filter((name) => allowed === undefined || allowed.has(name))
|
|
136
|
+
.filter(
|
|
137
|
+
(name) =>
|
|
138
|
+
REPEATABLE_OPTIONS.has(name) || (!parsed.flags.has(name) && !parsed.values.has(name)),
|
|
139
|
+
)
|
|
140
|
+
.map((name) => optionCandidate(name, valueNames.has(name) ? "space" : flagSuffix));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function serverCandidates(
|
|
144
|
+
host: McpCommandCompletionHost,
|
|
145
|
+
predicate: (state: string, serverId: string) => boolean = () => true,
|
|
146
|
+
suffix: "none" | "space" = "space",
|
|
147
|
+
): CompletionCandidate[] {
|
|
148
|
+
return [...host.listStatuses()]
|
|
149
|
+
.filter(([serverId, status]) => predicate(status.state, serverId))
|
|
150
|
+
.map(([serverId, status]) => ({
|
|
151
|
+
description: status.state.replaceAll("_", " "),
|
|
152
|
+
label: serverId,
|
|
153
|
+
suffix,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function completeServerAndOptions(
|
|
158
|
+
prefix: McpCommandCompletionPrefix,
|
|
159
|
+
host: McpCommandCompletionHost,
|
|
160
|
+
command: "auth" | "disable" | "enable" | "remove",
|
|
161
|
+
parsed: McpCommandScannedOptions,
|
|
162
|
+
options?: readonly CompletionCandidate[],
|
|
163
|
+
): McpCommandCompletionItem[] | null {
|
|
164
|
+
if (parsed.pendingValue !== undefined || parsed.positionals.length > 1) return null;
|
|
165
|
+
return completionItems(prefix, [
|
|
166
|
+
...(options ??
|
|
167
|
+
availableOptionCandidates(
|
|
168
|
+
command,
|
|
169
|
+
parsed,
|
|
170
|
+
undefined,
|
|
171
|
+
parsed.positionals.length === 0 ? "space" : "none",
|
|
172
|
+
)),
|
|
173
|
+
...(parsed.positionals.length === 0 ? serverCandidates(host, () => true, "none") : []),
|
|
174
|
+
]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function enumCompletion(
|
|
178
|
+
prefix: McpCommandCompletionPrefix,
|
|
179
|
+
option: string,
|
|
180
|
+
values: readonly CompletionCandidate[],
|
|
181
|
+
): McpCommandCompletionItem[] | null {
|
|
182
|
+
const inlinePrefix = `--${option}=`;
|
|
183
|
+
if (prefix.current.value.startsWith(inlinePrefix)) {
|
|
184
|
+
return completionItems(
|
|
185
|
+
{
|
|
186
|
+
...prefix,
|
|
187
|
+
current: { ...prefix.current, quote: undefined },
|
|
188
|
+
},
|
|
189
|
+
values.map((candidate) => ({ ...candidate, token: `${inlinePrefix}${candidate.label}` })),
|
|
190
|
+
prefix.current.value.slice(inlinePrefix.length),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return completionItems(prefix, values);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function addOptionCandidates(
|
|
197
|
+
parsed: McpCommandScannedOptions,
|
|
198
|
+
mode: Exclude<McpAddTransportMode, "invalid">,
|
|
199
|
+
auth: McpAddAuthenticationMode,
|
|
200
|
+
): CompletionCandidate[] {
|
|
201
|
+
const allowed = new Set<string>(["local", "transport"]);
|
|
202
|
+
if (mode !== "remote") for (const name of LOCAL_ADD_OPTIONS) allowed.add(name);
|
|
203
|
+
if (mode !== "local") for (const name of REMOTE_ADD_OPTIONS) allowed.add(name);
|
|
204
|
+
if (auth === "none") {
|
|
205
|
+
for (const name of MCP_ADD_OAUTH_VALUE_OPTIONS) allowed.delete(name);
|
|
206
|
+
allowed.delete("token");
|
|
207
|
+
} else if (auth === "bearer") {
|
|
208
|
+
for (const name of MCP_ADD_OAUTH_VALUE_OPTIONS) allowed.delete(name);
|
|
209
|
+
} else if (auth === "oauth") {
|
|
210
|
+
allowed.delete("token");
|
|
211
|
+
}
|
|
212
|
+
const candidates = availableOptionCandidates(
|
|
213
|
+
"add",
|
|
214
|
+
parsed,
|
|
215
|
+
allowed,
|
|
216
|
+
mode === "remote" && parsed.positionals.length > 1 ? "none" : "space",
|
|
217
|
+
);
|
|
218
|
+
if (parsed.positionals.length === 1 && mode !== "remote") {
|
|
219
|
+
candidates.push({ description: OPTION_DESCRIPTIONS["--"], label: "--" });
|
|
220
|
+
}
|
|
221
|
+
return candidates;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function completeAddCommand(
|
|
225
|
+
prefix: McpCommandCompletionPrefix,
|
|
226
|
+
parsed: McpCommandScannedOptions,
|
|
227
|
+
): McpCommandCompletionItem[] | null {
|
|
228
|
+
if (parsed.tail !== undefined) return null;
|
|
229
|
+
const mode = classifyMcpAddTransportMode(parsed);
|
|
230
|
+
const authentication = classifyMcpAddAuthentication(parsed, "completion");
|
|
231
|
+
if (mode === "invalid" || !authentication.ok) return null;
|
|
232
|
+
const auth = authentication.type;
|
|
233
|
+
const pending = parsed.pendingValue;
|
|
234
|
+
const inline = prefix.current.value.match(/^--(auth|transport)=(.*)$/u);
|
|
235
|
+
if (pending === "auth" || inline?.[1] === "auth") {
|
|
236
|
+
if (parsed.values.has("auth")) return null;
|
|
237
|
+
const finalSuffix: "none" | "space" =
|
|
238
|
+
mode === "remote" && parsed.positionals.length > 1 ? "none" : "space";
|
|
239
|
+
const candidates = [
|
|
240
|
+
{
|
|
241
|
+
description: "Bearer token authentication",
|
|
242
|
+
label: "bearer" as const,
|
|
243
|
+
suffix: parsed.values.has("token") ? finalSuffix : ("space" as const),
|
|
244
|
+
},
|
|
245
|
+
{ description: "No authentication", label: "none" as const, suffix: finalSuffix },
|
|
246
|
+
{ description: "OAuth authentication", label: "oauth" as const, suffix: finalSuffix },
|
|
247
|
+
];
|
|
248
|
+
return enumCompletion(
|
|
249
|
+
prefix,
|
|
250
|
+
"auth",
|
|
251
|
+
auth === undefined ? candidates : candidates.filter(({ label }) => label === auth),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (pending === "transport" || inline?.[1] === "transport") {
|
|
255
|
+
if (parsed.values.has("transport")) return null;
|
|
256
|
+
const finalSuffix: "none" | "space" = parsed.positionals.length > 1 ? "none" : "space";
|
|
257
|
+
const transports =
|
|
258
|
+
mode === "local"
|
|
259
|
+
? [{ description: "Local stdio transport", label: "stdio", suffix: "space" as const }]
|
|
260
|
+
: mode === "remote"
|
|
261
|
+
? [
|
|
262
|
+
{ description: "Streamable HTTP transport", label: "http", suffix: finalSuffix },
|
|
263
|
+
{ description: "Server-sent events transport", label: "sse", suffix: finalSuffix },
|
|
264
|
+
]
|
|
265
|
+
: [
|
|
266
|
+
{ description: "Streamable HTTP transport", label: "http", suffix: finalSuffix },
|
|
267
|
+
{ description: "Server-sent events transport", label: "sse", suffix: finalSuffix },
|
|
268
|
+
{
|
|
269
|
+
description: "Local stdio transport",
|
|
270
|
+
label: "stdio",
|
|
271
|
+
suffix: "space" as const,
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
return enumCompletion(prefix, "transport", transports);
|
|
275
|
+
}
|
|
276
|
+
if (pending !== undefined) return null;
|
|
277
|
+
return completionItems(prefix, addOptionCandidates(parsed, mode, auth));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function completeAuthCommand(
|
|
281
|
+
prefix: McpCommandCompletionPrefix,
|
|
282
|
+
host: McpCommandCompletionHost,
|
|
283
|
+
parsed: McpCommandScannedOptions,
|
|
284
|
+
): McpCommandCompletionItem[] | null {
|
|
285
|
+
if (parsed.pendingValue !== undefined) return null;
|
|
286
|
+
let allowed = new Set<string>(["callback", "code", "no-open", "state"]);
|
|
287
|
+
if (parsed.values.has("callback")) {
|
|
288
|
+
allowed.delete("callback");
|
|
289
|
+
allowed.delete("code");
|
|
290
|
+
allowed.delete("state");
|
|
291
|
+
} else if (parsed.values.has("code") || parsed.values.has("state")) {
|
|
292
|
+
allowed.delete("callback");
|
|
293
|
+
if (!parsed.values.has("state")) allowed = new Set(["no-open", "state"]);
|
|
294
|
+
else if (!parsed.values.has("code")) allowed = new Set(["code", "no-open"]);
|
|
295
|
+
}
|
|
296
|
+
return completeServerAndOptions(
|
|
297
|
+
prefix,
|
|
298
|
+
host,
|
|
299
|
+
"auth",
|
|
300
|
+
parsed,
|
|
301
|
+
availableOptionCandidates(
|
|
302
|
+
"auth",
|
|
303
|
+
parsed,
|
|
304
|
+
allowed,
|
|
305
|
+
parsed.positionals.length === 0 ? "space" : "none",
|
|
306
|
+
),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function completeLogoutCommand(
|
|
311
|
+
prefix: McpCommandCompletionPrefix,
|
|
312
|
+
host: McpCommandCompletionHost,
|
|
313
|
+
parsed: McpCommandScannedOptions,
|
|
314
|
+
): McpCommandCompletionItem[] | null {
|
|
315
|
+
if (parsed.pendingValue !== undefined || parsed.positionals.length > 1) return null;
|
|
316
|
+
if (parsed.positionals.length === 1) return null;
|
|
317
|
+
if (parsed.flags.has("all") && parsed.flags.has("force")) return null;
|
|
318
|
+
if (parsed.flags.has("all") || parsed.flags.has("force")) {
|
|
319
|
+
const missing = parsed.flags.has("all") ? "force" : "all";
|
|
320
|
+
return completionItems(prefix, [optionCandidate(missing, "none")]);
|
|
321
|
+
}
|
|
322
|
+
return completionItems(prefix, [
|
|
323
|
+
optionCandidate("all"),
|
|
324
|
+
...serverCandidates(host, () => true, "none"),
|
|
325
|
+
]);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function completeTestCommand(
|
|
329
|
+
prefix: McpCommandCompletionPrefix,
|
|
330
|
+
host: McpCommandCompletionHost,
|
|
331
|
+
parsed: McpCommandScannedOptions,
|
|
332
|
+
): McpCommandCompletionItem[] | null {
|
|
333
|
+
if (
|
|
334
|
+
parsed.pendingValue !== undefined ||
|
|
335
|
+
parsed.positionals.length > 0 ||
|
|
336
|
+
parsed.flags.has("json")
|
|
337
|
+
)
|
|
338
|
+
return null;
|
|
339
|
+
if (parsed.flags.has("all")) return null;
|
|
340
|
+
return completionItems(prefix, [
|
|
341
|
+
optionCandidate("all", "none"),
|
|
342
|
+
...serverCandidates(host, () => true, "none"),
|
|
343
|
+
]);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function quietMcpCompletionRead<Value>(
|
|
347
|
+
read: () => Promise<Value>,
|
|
348
|
+
): Promise<Value | undefined> {
|
|
349
|
+
try {
|
|
350
|
+
return await read();
|
|
351
|
+
} catch {
|
|
352
|
+
// Catalog and protocol completion failures are expected while live Server state changes.
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function promptDefinition(
|
|
358
|
+
host: McpCommandCompletionHost,
|
|
359
|
+
serverId: string,
|
|
360
|
+
promptName: string,
|
|
361
|
+
) {
|
|
362
|
+
const prompts = await quietMcpCompletionRead(() => host.listPrompts(serverId));
|
|
363
|
+
if (prompts === undefined) return undefined;
|
|
364
|
+
return {
|
|
365
|
+
definition: prompts.find(({ prompt }) => prompt.name === promptName)?.prompt,
|
|
366
|
+
prompts,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function completePromptArgument(
|
|
371
|
+
prefix: McpCommandCompletionPrefix,
|
|
372
|
+
host: McpCommandCompletionHost,
|
|
373
|
+
serverId: string,
|
|
374
|
+
promptName: string,
|
|
375
|
+
rawArgument: string,
|
|
376
|
+
syntax: "inline" | "separate",
|
|
377
|
+
): Promise<McpCommandCompletionItem[] | null> {
|
|
378
|
+
const separator = rawArgument.indexOf("=");
|
|
379
|
+
if (separator < 0) {
|
|
380
|
+
const catalog = await promptDefinition(host, serverId, promptName);
|
|
381
|
+
if (catalog === undefined) return null;
|
|
382
|
+
return completionItems(
|
|
383
|
+
prefix,
|
|
384
|
+
(catalog.definition?.arguments ?? []).map(({ description, name }) => ({
|
|
385
|
+
suffix: "none",
|
|
386
|
+
...(description === undefined ? {} : { description }),
|
|
387
|
+
label: name,
|
|
388
|
+
token: `${syntax === "inline" ? "--arg=" : ""}${name}=`,
|
|
389
|
+
})),
|
|
390
|
+
rawArgument,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const argumentName = rawArgument.slice(0, separator);
|
|
394
|
+
const valuePrefix = rawArgument.slice(separator + 1);
|
|
395
|
+
const completion = await quietMcpCompletionRead(() =>
|
|
396
|
+
host.completePromptArgument(serverId, promptName, argumentName, valuePrefix),
|
|
397
|
+
);
|
|
398
|
+
if (completion === undefined) return null;
|
|
399
|
+
return completionItems(
|
|
400
|
+
prefix,
|
|
401
|
+
completion.values.map((value) => ({
|
|
402
|
+
label: value,
|
|
403
|
+
suffix: "none",
|
|
404
|
+
token: `${syntax === "inline" ? "--arg=" : ""}${argumentName}=${value}`,
|
|
405
|
+
})),
|
|
406
|
+
valuePrefix,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function completePromptCommand(
|
|
411
|
+
prefix: McpCommandCompletionPrefix,
|
|
412
|
+
host: McpCommandCompletionHost,
|
|
413
|
+
parsed: McpCommandScannedOptions,
|
|
414
|
+
): Promise<McpCommandCompletionItem[] | null> {
|
|
415
|
+
if (parsed.positionals.length === 0) {
|
|
416
|
+
if (parsed.pendingValue !== undefined) return null;
|
|
417
|
+
return completionItems(
|
|
418
|
+
prefix,
|
|
419
|
+
serverCandidates(host, (state) => state === "connected"),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const serverId = parsed.positionals[0] ?? "";
|
|
423
|
+
if (parsed.positionals.length === 1 && parsed.pendingValue === undefined) {
|
|
424
|
+
const prompts = await quietMcpCompletionRead(() => host.listPrompts(serverId));
|
|
425
|
+
if (prompts === undefined) return null;
|
|
426
|
+
return completionItems(
|
|
427
|
+
prefix,
|
|
428
|
+
prompts.map(({ prompt }) => ({
|
|
429
|
+
...(prompt.description === undefined ? {} : { description: prompt.description }),
|
|
430
|
+
label: prompt.name,
|
|
431
|
+
suffix: "none",
|
|
432
|
+
})),
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const promptName = parsed.positionals[1];
|
|
436
|
+
if (promptName === undefined || parsed.positionals.length > 2) return null;
|
|
437
|
+
const inlineMarker = "--arg=";
|
|
438
|
+
if (prefix.current.value.startsWith(inlineMarker)) {
|
|
439
|
+
return completePromptArgument(
|
|
440
|
+
prefix,
|
|
441
|
+
host,
|
|
442
|
+
serverId,
|
|
443
|
+
promptName,
|
|
444
|
+
prefix.current.value.slice(inlineMarker.length),
|
|
445
|
+
"inline",
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
if (parsed.pendingValue === "arg") {
|
|
449
|
+
return completePromptArgument(
|
|
450
|
+
prefix,
|
|
451
|
+
host,
|
|
452
|
+
serverId,
|
|
453
|
+
promptName,
|
|
454
|
+
prefix.current.value,
|
|
455
|
+
"separate",
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
if (parsed.pendingValue !== undefined) return null;
|
|
459
|
+
const catalog = await promptDefinition(host, serverId, promptName);
|
|
460
|
+
if (catalog === undefined) return null;
|
|
461
|
+
return completionItems(
|
|
462
|
+
prefix,
|
|
463
|
+
catalog.definition?.arguments === undefined || catalog.definition.arguments.length === 0
|
|
464
|
+
? []
|
|
465
|
+
: [optionCandidate("arg")],
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function completeResourceCommand(
|
|
470
|
+
prefix: McpCommandCompletionPrefix,
|
|
471
|
+
host: McpCommandCompletionHost,
|
|
472
|
+
command: "subscribe" | "unsubscribe",
|
|
473
|
+
parsed: McpCommandScannedOptions,
|
|
474
|
+
): Promise<McpCommandCompletionItem[] | null> {
|
|
475
|
+
if (parsed.pendingValue !== undefined || parsed.positionals.length > 2) return null;
|
|
476
|
+
if (parsed.positionals.length === 0) {
|
|
477
|
+
const subscriptions = host.listSubscriptions();
|
|
478
|
+
return completionItems(
|
|
479
|
+
prefix,
|
|
480
|
+
command === "subscribe"
|
|
481
|
+
? serverCandidates(host, (state) => state === "connected")
|
|
482
|
+
: serverCandidates(host, (_state, serverId) =>
|
|
483
|
+
subscriptions.some((subscription) => subscription.serverId === serverId),
|
|
484
|
+
),
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
if (parsed.positionals.length > 1) return null;
|
|
488
|
+
const serverId = parsed.positionals[0] ?? "";
|
|
489
|
+
if (command === "unsubscribe") {
|
|
490
|
+
return completionItems(
|
|
491
|
+
prefix,
|
|
492
|
+
host
|
|
493
|
+
.listSubscriptions()
|
|
494
|
+
.filter((subscription) => subscription.serverId === serverId)
|
|
495
|
+
.map(({ uri }) => ({ description: "Subscribed Resource", label: uri, suffix: "none" })),
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
const resources = await quietMcpCompletionRead(() => host.listResources(serverId));
|
|
499
|
+
if (resources === undefined) return null;
|
|
500
|
+
return completionItems(
|
|
501
|
+
prefix,
|
|
502
|
+
resources.map(({ resource }) => ({
|
|
503
|
+
...(resource.name === undefined ? {} : { description: resource.name }),
|
|
504
|
+
label: resource.uri,
|
|
505
|
+
suffix: "none",
|
|
506
|
+
})),
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function completeSingleServerCommand(
|
|
511
|
+
prefix: McpCommandCompletionPrefix,
|
|
512
|
+
host: McpCommandCompletionHost,
|
|
513
|
+
parsed: McpCommandScannedOptions,
|
|
514
|
+
): McpCommandCompletionItem[] | null {
|
|
515
|
+
if (parsed.pendingValue !== undefined || parsed.positionals.length > 0) return null;
|
|
516
|
+
return completionItems(
|
|
517
|
+
prefix,
|
|
518
|
+
serverCandidates(host, () => true, "none"),
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Complete one full `/mcp` argument prefix using local Host state and selected live catalogs. */
|
|
523
|
+
export async function completeMcpCommandArguments(
|
|
524
|
+
line: string,
|
|
525
|
+
host: McpCommandCompletionHost,
|
|
526
|
+
): Promise<McpCommandCompletionItem[] | null> {
|
|
527
|
+
const prefix = tokenizeMcpCommandCompletionPrefix(line);
|
|
528
|
+
if (prefix.completed.length === 0) {
|
|
529
|
+
return completionItems(
|
|
530
|
+
prefix,
|
|
531
|
+
MCP_COMMAND_NAMES.map((name) => ({
|
|
532
|
+
suffix:
|
|
533
|
+
name === "help" || name === "list" || name === "logs" || name === "status"
|
|
534
|
+
? "none"
|
|
535
|
+
: "space",
|
|
536
|
+
description: COMMAND_DESCRIPTIONS[name],
|
|
537
|
+
label: name,
|
|
538
|
+
})),
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
const command = prefix.completed[0];
|
|
542
|
+
if (!MCP_COMMAND_NAMES.some((name) => name === command)) return null;
|
|
543
|
+
// SAFETY: The membership check above narrows the runtime string to an MCP command name.
|
|
544
|
+
const commandName = command as (typeof MCP_COMMAND_NAMES)[number];
|
|
545
|
+
if (commandName === "help" || commandName === "list" || commandName === "status") return null;
|
|
546
|
+
const scanned = scanMcpCommandOptions(prefix.completed.slice(1), commandName, "completion");
|
|
547
|
+
if (!scanned.ok) return null;
|
|
548
|
+
const parsed = scanned.options;
|
|
549
|
+
|
|
550
|
+
switch (commandName) {
|
|
551
|
+
case "add":
|
|
552
|
+
return completeAddCommand(prefix, parsed);
|
|
553
|
+
case "auth":
|
|
554
|
+
return completeAuthCommand(prefix, host, parsed);
|
|
555
|
+
case "disable":
|
|
556
|
+
case "enable":
|
|
557
|
+
case "remove":
|
|
558
|
+
return completeServerAndOptions(prefix, host, commandName, parsed);
|
|
559
|
+
case "logout":
|
|
560
|
+
return completeLogoutCommand(prefix, host, parsed);
|
|
561
|
+
case "test":
|
|
562
|
+
return completeTestCommand(prefix, host, parsed);
|
|
563
|
+
case "prompt":
|
|
564
|
+
return completePromptCommand(prefix, host, parsed);
|
|
565
|
+
case "subscribe":
|
|
566
|
+
case "unsubscribe":
|
|
567
|
+
return completeResourceCommand(prefix, host, commandName, parsed);
|
|
568
|
+
case "logs":
|
|
569
|
+
case "reconnect":
|
|
570
|
+
return completeSingleServerCommand(prefix, host, parsed);
|
|
571
|
+
}
|
|
572
|
+
}
|