@ikuma.cloud/pix-mcp 0.0.8 → 0.0.9
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 +110 -39
- package/mcp.schema.json +1 -1
- package/package.json +4 -2
- package/src/client.ts +335 -9
- package/src/command.ts +69 -0
- package/src/config.ts +1 -1
- package/src/index.ts +262 -13
- package/src/output.ts +9 -10
- package/src/prompts.ts +18 -70
- package/src/resource-picker.ts +157 -0
- package/src/resources.ts +769 -0
package/src/index.ts
CHANGED
|
@@ -5,7 +5,12 @@ import type {
|
|
|
5
5
|
ExtensionCommandContext,
|
|
6
6
|
ExtensionContext,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
Prompt,
|
|
10
|
+
Resource,
|
|
11
|
+
ResourceTemplate,
|
|
12
|
+
Tool,
|
|
13
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
14
|
import { compact, entry, search, summary, type Entry } from "./catalog.ts";
|
|
10
15
|
import { Connection } from "./client.ts";
|
|
11
16
|
import { readConfig, type ConfigIssue, type ServerConfig } from "./config.ts";
|
|
@@ -15,6 +20,21 @@ import {
|
|
|
15
20
|
removeEditablePromptGuard,
|
|
16
21
|
} from "./output.ts";
|
|
17
22
|
import { pickPrompt } from "./prompt-picker.ts";
|
|
23
|
+
import { pickResource } from "./resource-picker.ts";
|
|
24
|
+
import {
|
|
25
|
+
formatResourceList,
|
|
26
|
+
formatResourceResult,
|
|
27
|
+
parseResourceCommand,
|
|
28
|
+
resourceCompletions,
|
|
29
|
+
resourceEntryKey,
|
|
30
|
+
resourceKey,
|
|
31
|
+
resourceTemplateVariables,
|
|
32
|
+
resolveResourceUri,
|
|
33
|
+
type ResourceArgumentToken,
|
|
34
|
+
type ResourceCommand,
|
|
35
|
+
type ResourceEntry,
|
|
36
|
+
type TemplateResourceEntry,
|
|
37
|
+
} from "./resources.ts";
|
|
18
38
|
import {
|
|
19
39
|
formatPromptList,
|
|
20
40
|
formatPromptResult,
|
|
@@ -44,17 +64,23 @@ interface PromptSelection {
|
|
|
44
64
|
item: PromptEntry;
|
|
45
65
|
}
|
|
46
66
|
|
|
67
|
+
interface ResourceSelection {
|
|
68
|
+
command: ResourceCommand;
|
|
69
|
+
item: ResourceEntry;
|
|
70
|
+
}
|
|
71
|
+
|
|
47
72
|
interface State {
|
|
48
73
|
alive: boolean;
|
|
49
74
|
status: string;
|
|
50
75
|
entries: Map<string, Entry>;
|
|
51
76
|
prompts: Map<string, PromptEntry>;
|
|
77
|
+
resources: Map<string, ResourceEntry>;
|
|
52
78
|
loaded: Map<string, string>;
|
|
53
79
|
connections: Map<string, Connection>;
|
|
54
80
|
rejected: Map<string, Rejections>;
|
|
55
81
|
configIssues: ConfigIssue[];
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
draftCleanups: Set<() => Promise<void>>;
|
|
83
|
+
draftGuards: Set<string>;
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
const discoveryParameters = Type.Object(
|
|
@@ -178,8 +204,8 @@ export default function mcp(pi: ExtensionAPI) {
|
|
|
178
204
|
ensureSelectedPromptCurrent();
|
|
179
205
|
ctx.signal?.throwIfAborted();
|
|
180
206
|
ctx.ui.pasteToEditor(draft.text);
|
|
181
|
-
owner.
|
|
182
|
-
if (draft.guard) owner.
|
|
207
|
+
owner.draftCleanups.add(draft.cleanup);
|
|
208
|
+
if (draft.guard) owner.draftGuards.add(draft.guard);
|
|
183
209
|
} catch (error) {
|
|
184
210
|
await draft.cleanup();
|
|
185
211
|
throw error;
|
|
@@ -202,6 +228,197 @@ export default function mcp(pi: ExtensionAPI) {
|
|
|
202
228
|
},
|
|
203
229
|
});
|
|
204
230
|
|
|
231
|
+
pi.registerCommand("mcp-resource", {
|
|
232
|
+
description: "Select, list, or read a user-controlled MCP resource",
|
|
233
|
+
getArgumentCompletions: (prefix) =>
|
|
234
|
+
resourceCompletions(
|
|
235
|
+
prefix,
|
|
236
|
+
[...(state?.resources.values() ?? [])],
|
|
237
|
+
state?.connections.keys() ?? [],
|
|
238
|
+
),
|
|
239
|
+
async handler(input, ctx) {
|
|
240
|
+
try {
|
|
241
|
+
const owner = state;
|
|
242
|
+
if (!owner) throw new Error("MCP session has not started.");
|
|
243
|
+
current(owner);
|
|
244
|
+
const editBeforeSending = ctx.mode === "tui" && input.trim() === "";
|
|
245
|
+
const selection = editBeforeSending
|
|
246
|
+
? await chooseResource(owner, ctx)
|
|
247
|
+
: undefined;
|
|
248
|
+
const command =
|
|
249
|
+
selection?.command ??
|
|
250
|
+
(editBeforeSending ? undefined : parseResourceCommand(input));
|
|
251
|
+
if (!command) return;
|
|
252
|
+
if (command.action === "list") {
|
|
253
|
+
if (
|
|
254
|
+
command.server &&
|
|
255
|
+
!owner.connections.has(command.server) &&
|
|
256
|
+
!owner.configIssues.some((issue) => issue.name === command.server)
|
|
257
|
+
)
|
|
258
|
+
throw new Error("Unknown MCP server.");
|
|
259
|
+
const resourceStatus = [
|
|
260
|
+
...owner.configIssues
|
|
261
|
+
.filter(
|
|
262
|
+
(issue) => !command.server || issue.name === command.server,
|
|
263
|
+
)
|
|
264
|
+
.map((issue) => `${issue.name}: ${issue.message}`),
|
|
265
|
+
...[...owner.connections.values()]
|
|
266
|
+
.filter(
|
|
267
|
+
(connection) =>
|
|
268
|
+
!command.server || connection.config.name === command.server,
|
|
269
|
+
)
|
|
270
|
+
.map(
|
|
271
|
+
(connection) =>
|
|
272
|
+
`${connection.config.name}: ${connection.resourceStatus}`,
|
|
273
|
+
),
|
|
274
|
+
].join("\n");
|
|
275
|
+
const text = `${resourceStatus}${resourceStatus ? "\n\n" : ""}${formatResourceList(
|
|
276
|
+
[...owner.resources.values()],
|
|
277
|
+
command.server,
|
|
278
|
+
)}`;
|
|
279
|
+
if (!ctx.hasUI) throw new Error(text);
|
|
280
|
+
ctx.ui.notify(text, "info");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const direct = owner.resources.get(
|
|
285
|
+
resourceKey(command.server, "resource", command.target),
|
|
286
|
+
);
|
|
287
|
+
const template = owner.resources.get(
|
|
288
|
+
resourceKey(command.server, "template", command.target),
|
|
289
|
+
);
|
|
290
|
+
const item =
|
|
291
|
+
selection?.item ??
|
|
292
|
+
(command.argumentTokens.length > 0 ? template : (direct ?? template));
|
|
293
|
+
const selectedKey = item ? resourceEntryKey(item) : undefined;
|
|
294
|
+
const ensureSelectedResourceCurrent = () => {
|
|
295
|
+
if (
|
|
296
|
+
selection &&
|
|
297
|
+
selectedKey &&
|
|
298
|
+
owner.resources.get(selectedKey) !== item
|
|
299
|
+
)
|
|
300
|
+
throw new Error(
|
|
301
|
+
"MCP resource catalog changed. Open the picker again.",
|
|
302
|
+
);
|
|
303
|
+
};
|
|
304
|
+
ensureSelectedResourceCurrent();
|
|
305
|
+
const templateEntry =
|
|
306
|
+
item?.kind === "template"
|
|
307
|
+
? item
|
|
308
|
+
: template?.kind === "template"
|
|
309
|
+
? template
|
|
310
|
+
: undefined;
|
|
311
|
+
const uri = resolveResourceUri(
|
|
312
|
+
command.target,
|
|
313
|
+
templateEntry,
|
|
314
|
+
command.argumentTokens,
|
|
315
|
+
);
|
|
316
|
+
const connection = owner.connections.get(command.server);
|
|
317
|
+
if (!connection) throw new Error("MCP connection unavailable.");
|
|
318
|
+
if (ctx.mode === "tui")
|
|
319
|
+
ctx.ui.setStatus(
|
|
320
|
+
"mcp-resource",
|
|
321
|
+
`Loading ${command.server} / ${compact(uri, 120)}…`,
|
|
322
|
+
);
|
|
323
|
+
try {
|
|
324
|
+
const result = await connection.readResource(uri, ctx.signal);
|
|
325
|
+
current(owner);
|
|
326
|
+
ensureSelectedResourceCurrent();
|
|
327
|
+
const formatted = await formatResourceResult(command.server, result);
|
|
328
|
+
current(owner);
|
|
329
|
+
ensureSelectedResourceCurrent();
|
|
330
|
+
ctx.signal?.throwIfAborted();
|
|
331
|
+
if (editBeforeSending) {
|
|
332
|
+
const draft = await formatEditableContent(formatted.content);
|
|
333
|
+
try {
|
|
334
|
+
current(owner);
|
|
335
|
+
ensureSelectedResourceCurrent();
|
|
336
|
+
ctx.signal?.throwIfAborted();
|
|
337
|
+
ctx.ui.pasteToEditor(draft.text);
|
|
338
|
+
owner.draftCleanups.add(draft.cleanup);
|
|
339
|
+
if (draft.guard) owner.draftGuards.add(draft.guard);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
await draft.cleanup();
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
pi.sendUserMessage(
|
|
346
|
+
formatted.content,
|
|
347
|
+
ctx.isIdle() ? undefined : { deliverAs: "followUp" },
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
} finally {
|
|
351
|
+
if (ctx.mode === "tui") ctx.ui.setStatus("mcp-resource", undefined);
|
|
352
|
+
}
|
|
353
|
+
} catch (error) {
|
|
354
|
+
const message =
|
|
355
|
+
error instanceof Error ? error.message : "MCP resource failed.";
|
|
356
|
+
if (!ctx.hasUI) throw new Error(message);
|
|
357
|
+
ctx.ui.notify(message, "error");
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
async function chooseResource(
|
|
363
|
+
owner: State,
|
|
364
|
+
ctx: ExtensionCommandContext,
|
|
365
|
+
): Promise<ResourceSelection | undefined> {
|
|
366
|
+
const entries = [...owner.resources.values()].sort(
|
|
367
|
+
(a, b) =>
|
|
368
|
+
a.server.localeCompare(b.server, "en") ||
|
|
369
|
+
resourceEntryKey(a).localeCompare(resourceEntryKey(b), "en"),
|
|
370
|
+
);
|
|
371
|
+
if (entries.length === 0) {
|
|
372
|
+
ctx.ui.notify("No MCP resources are available.", "warning");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const item = await pickResource(entries, ctx);
|
|
377
|
+
if (!item) return;
|
|
378
|
+
ctx.signal?.throwIfAborted();
|
|
379
|
+
|
|
380
|
+
const key = resourceEntryKey(item);
|
|
381
|
+
const ensureCurrent = () => {
|
|
382
|
+
current(owner);
|
|
383
|
+
if (owner.resources.get(key) !== item)
|
|
384
|
+
throw new Error("MCP resource catalog changed. Open the picker again.");
|
|
385
|
+
};
|
|
386
|
+
ensureCurrent();
|
|
387
|
+
|
|
388
|
+
const dialogOptions = ctx.signal ? { signal: ctx.signal } : undefined;
|
|
389
|
+
const argumentTokens: ResourceArgumentToken[] = [];
|
|
390
|
+
if (item.kind === "template") {
|
|
391
|
+
for (const variable of item.variables) {
|
|
392
|
+
const value = await ctx.ui.input(
|
|
393
|
+
`${variable} (optional)`,
|
|
394
|
+
"Leave empty to omit",
|
|
395
|
+
dialogOptions,
|
|
396
|
+
);
|
|
397
|
+
if (value === undefined) return;
|
|
398
|
+
ctx.signal?.throwIfAborted();
|
|
399
|
+
ensureCurrent();
|
|
400
|
+
if (value !== "")
|
|
401
|
+
argumentTokens.push({
|
|
402
|
+
value: `${variable}=${value}`,
|
|
403
|
+
separator: variable.length,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
command: {
|
|
410
|
+
action: "read",
|
|
411
|
+
server: item.server,
|
|
412
|
+
target:
|
|
413
|
+
item.kind === "resource"
|
|
414
|
+
? item.resource.uri
|
|
415
|
+
: item.template.uriTemplate,
|
|
416
|
+
argumentTokens,
|
|
417
|
+
},
|
|
418
|
+
item,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
205
422
|
async function choosePrompt(
|
|
206
423
|
owner: State,
|
|
207
424
|
ctx: ExtensionCommandContext,
|
|
@@ -441,6 +658,35 @@ export default function mcp(pi: ExtensionAPI) {
|
|
|
441
658
|
}
|
|
442
659
|
}
|
|
443
660
|
|
|
661
|
+
function synchronizeResources(
|
|
662
|
+
owner: State,
|
|
663
|
+
config: ServerConfig,
|
|
664
|
+
resources: Resource[],
|
|
665
|
+
templates: ResourceTemplate[],
|
|
666
|
+
) {
|
|
667
|
+
if (!owner.alive || state !== owner) return;
|
|
668
|
+
for (const [key, item] of owner.resources) {
|
|
669
|
+
if (item.server === config.name) owner.resources.delete(key);
|
|
670
|
+
}
|
|
671
|
+
for (const resource of resources) {
|
|
672
|
+
const item: ResourceEntry = {
|
|
673
|
+
kind: "resource",
|
|
674
|
+
server: config.name,
|
|
675
|
+
resource,
|
|
676
|
+
};
|
|
677
|
+
owner.resources.set(resourceEntryKey(item), item);
|
|
678
|
+
}
|
|
679
|
+
for (const template of templates) {
|
|
680
|
+
const item: TemplateResourceEntry = {
|
|
681
|
+
kind: "template",
|
|
682
|
+
server: config.name,
|
|
683
|
+
template,
|
|
684
|
+
variables: resourceTemplateVariables(template.uriTemplate),
|
|
685
|
+
};
|
|
686
|
+
owner.resources.set(resourceEntryKey(item), item);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
444
690
|
function synchronize(owner: State, config: ServerConfig, tools: Tool[]) {
|
|
445
691
|
if (!owner.alive || state !== owner) return;
|
|
446
692
|
const previous = new Map(
|
|
@@ -551,14 +797,14 @@ export default function mcp(pi: ExtensionAPI) {
|
|
|
551
797
|
owner.alive = false;
|
|
552
798
|
state = undefined;
|
|
553
799
|
deactivate(owned);
|
|
554
|
-
const
|
|
555
|
-
owner.
|
|
556
|
-
owner.
|
|
800
|
+
const draftCleanups = [...owner.draftCleanups];
|
|
801
|
+
owner.draftCleanups.clear();
|
|
802
|
+
owner.draftGuards.clear();
|
|
557
803
|
await Promise.all([
|
|
558
804
|
...[...owner.connections.values()].map((connection) =>
|
|
559
805
|
connection.close(),
|
|
560
806
|
),
|
|
561
|
-
...
|
|
807
|
+
...draftCleanups.map((cleanup) => cleanup()),
|
|
562
808
|
]);
|
|
563
809
|
}
|
|
564
810
|
|
|
@@ -569,12 +815,13 @@ export default function mcp(pi: ExtensionAPI) {
|
|
|
569
815
|
status: "No MCP servers configured.",
|
|
570
816
|
entries: new Map(),
|
|
571
817
|
prompts: new Map(),
|
|
818
|
+
resources: new Map(),
|
|
572
819
|
loaded: new Map(),
|
|
573
820
|
connections: new Map(),
|
|
574
821
|
rejected: new Map(),
|
|
575
822
|
configIssues: [],
|
|
576
|
-
|
|
577
|
-
|
|
823
|
+
draftCleanups: new Set(),
|
|
824
|
+
draftGuards: new Set(),
|
|
578
825
|
};
|
|
579
826
|
state = owner;
|
|
580
827
|
try {
|
|
@@ -606,6 +853,8 @@ export default function mcp(pi: ExtensionAPI) {
|
|
|
606
853
|
server,
|
|
607
854
|
(tools) => synchronize(owner, server, tools),
|
|
608
855
|
(prompts) => synchronizePrompts(owner, server, prompts),
|
|
856
|
+
(resources, templates) =>
|
|
857
|
+
synchronizeResources(owner, server, resources, templates),
|
|
609
858
|
),
|
|
610
859
|
);
|
|
611
860
|
}
|
|
@@ -637,7 +886,7 @@ export default function mcp(pi: ExtensionAPI) {
|
|
|
637
886
|
const consumed = new Set<string>();
|
|
638
887
|
const removeKnownGuards = (text: string) => {
|
|
639
888
|
let next = text;
|
|
640
|
-
for (const guard of owner.
|
|
889
|
+
for (const guard of owner.draftGuards) {
|
|
641
890
|
if (!next.includes(guard)) continue;
|
|
642
891
|
next = removeEditablePromptGuard(next, guard);
|
|
643
892
|
consumed.add(guard);
|
|
@@ -654,7 +903,7 @@ export default function mcp(pi: ExtensionAPI) {
|
|
|
654
903
|
: block,
|
|
655
904
|
);
|
|
656
905
|
}
|
|
657
|
-
for (const guard of consumed) owner.
|
|
906
|
+
for (const guard of consumed) owner.draftGuards.delete(guard);
|
|
658
907
|
if (consumed.size > 0) return { message: { ...event.message, content } };
|
|
659
908
|
});
|
|
660
909
|
|
package/src/output.ts
CHANGED
|
@@ -29,6 +29,7 @@ interface ContentOptions {
|
|
|
29
29
|
structuredContent?: unknown;
|
|
30
30
|
preserveErrorImages?: boolean;
|
|
31
31
|
preserveOrder?: boolean;
|
|
32
|
+
artifactRequired?: boolean;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export async function formatContent(
|
|
@@ -83,6 +84,7 @@ export async function formatContent(
|
|
|
83
84
|
textTruncated ||
|
|
84
85
|
omitted ||
|
|
85
86
|
largeDetails ||
|
|
87
|
+
options.artifactRequired === true ||
|
|
86
88
|
(options.preserveErrorImages === true && images.length > 0);
|
|
87
89
|
let fullOutputPath: string | undefined;
|
|
88
90
|
if (truncated) {
|
|
@@ -141,7 +143,7 @@ export async function formatContent(
|
|
|
141
143
|
};
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
function
|
|
146
|
+
function createEditableContentGuard(): string {
|
|
145
147
|
const bytes = randomBytes(8);
|
|
146
148
|
let guard = "\u2060\u2063";
|
|
147
149
|
for (const byte of bytes)
|
|
@@ -154,12 +156,12 @@ export function removeEditablePromptGuard(text: string, guard: string): string {
|
|
|
154
156
|
return text.replaceAll(guard, "");
|
|
155
157
|
}
|
|
156
158
|
|
|
157
|
-
function
|
|
159
|
+
function prepareEditableContent(text: string): {
|
|
158
160
|
text: string;
|
|
159
161
|
guard?: string;
|
|
160
162
|
} {
|
|
161
163
|
// The TUI interprets leading / and ! as commands. An invisible guard keeps
|
|
162
|
-
// untrusted
|
|
164
|
+
// untrusted staged text inert in the editor and is removed at message_end.
|
|
163
165
|
const sanitized = text
|
|
164
166
|
.replace(/\r\n?/gu, "\n")
|
|
165
167
|
.replace(/\p{Cc}/gu, (character) =>
|
|
@@ -167,7 +169,7 @@ function prepareEditablePrompt(text: string): {
|
|
|
167
169
|
)
|
|
168
170
|
.replace(/[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/gu, "");
|
|
169
171
|
if (!/^[!/]/u.test(sanitized.trimStart())) return { text: sanitized };
|
|
170
|
-
const guard =
|
|
172
|
+
const guard = createEditableContentGuard();
|
|
171
173
|
return { text: `${guard}${sanitized}`, guard };
|
|
172
174
|
}
|
|
173
175
|
|
|
@@ -190,12 +192,9 @@ export async function formatEditableContent(
|
|
|
190
192
|
text += block.text;
|
|
191
193
|
continue;
|
|
192
194
|
}
|
|
193
|
-
imageDirectory ??= await mkdtemp(join(tmpdir(), "pix-mcp-
|
|
195
|
+
imageDirectory ??= await mkdtemp(join(tmpdir(), "pix-mcp-draft-"));
|
|
194
196
|
const extension = imageExtensions[block.mimeType] ?? "img";
|
|
195
|
-
const path = join(
|
|
196
|
-
imageDirectory,
|
|
197
|
-
`prompt-image-${++imageIndex}.${extension}`,
|
|
198
|
-
);
|
|
197
|
+
const path = join(imageDirectory, `image-${++imageIndex}.${extension}`);
|
|
199
198
|
await writeFile(path, Buffer.from(block.data, "base64"), { mode: 0o600 });
|
|
200
199
|
const separator = text.endsWith("\n\n")
|
|
201
200
|
? ""
|
|
@@ -210,7 +209,7 @@ export async function formatEditableContent(
|
|
|
210
209
|
throw error;
|
|
211
210
|
}
|
|
212
211
|
return {
|
|
213
|
-
...
|
|
212
|
+
...prepareEditableContent(text),
|
|
214
213
|
cleanup: async () => {
|
|
215
214
|
if (imageDirectory)
|
|
216
215
|
await rm(imageDirectory, { recursive: true, force: true });
|
package/src/prompts.ts
CHANGED
|
@@ -4,6 +4,12 @@ import type {
|
|
|
4
4
|
Prompt,
|
|
5
5
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import { compact } from "./catalog.ts";
|
|
7
|
+
import {
|
|
8
|
+
quoteCommandArgument,
|
|
9
|
+
renderCommandToken,
|
|
10
|
+
tokenizeCommand,
|
|
11
|
+
type CommandToken,
|
|
12
|
+
} from "./command.ts";
|
|
7
13
|
import { formatContent } from "./output.ts";
|
|
8
14
|
|
|
9
15
|
export interface PromptEntry {
|
|
@@ -11,10 +17,7 @@ export interface PromptEntry {
|
|
|
11
17
|
prompt: Prompt;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
export
|
|
15
|
-
value: string;
|
|
16
|
-
separator?: number;
|
|
17
|
-
}
|
|
20
|
+
export type PromptArgumentToken = CommandToken;
|
|
18
21
|
|
|
19
22
|
export type PromptCommand =
|
|
20
23
|
| { action: "list"; server?: string }
|
|
@@ -30,7 +33,7 @@ export function promptKey(server: string, name: string): string {
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export function parsePromptCommand(input: string): PromptCommand {
|
|
33
|
-
const tokens =
|
|
36
|
+
const tokens = tokenizeCommand(input, "prompt");
|
|
34
37
|
const action = tokens.shift()?.value;
|
|
35
38
|
if (action === "list") {
|
|
36
39
|
if (tokens.length > 1) throw new Error("Usage: /mcp-prompt list [server]");
|
|
@@ -120,15 +123,15 @@ export function formatPromptList(
|
|
|
120
123
|
const argumentsHint = (entry.prompt.arguments ?? [])
|
|
121
124
|
.map((argument) =>
|
|
122
125
|
argument.required
|
|
123
|
-
? `<${
|
|
124
|
-
: `[${
|
|
126
|
+
? `<${quoteCommandArgument(argument.name)}>`
|
|
127
|
+
: `[${quoteCommandArgument(argument.name)}]`,
|
|
125
128
|
)
|
|
126
129
|
.join(" ");
|
|
127
130
|
const description = compact(
|
|
128
131
|
entry.prompt.description ?? entry.prompt.title ?? "",
|
|
129
132
|
160,
|
|
130
133
|
);
|
|
131
|
-
const line = `${entry.server} ${
|
|
134
|
+
const line = `${entry.server} ${quoteCommandArgument(entry.prompt.name)}${argumentsHint ? ` ${argumentsHint}` : ""}${description ? ` — ${description}` : ""}`;
|
|
132
135
|
if (shown >= 100 || lines.join("\n").length + line.length > 15_000) {
|
|
133
136
|
lines.push(`${selected.length - shown} additional prompts omitted.`);
|
|
134
137
|
break;
|
|
@@ -149,7 +152,7 @@ export function promptCompletions(
|
|
|
149
152
|
const trailingSpace = /\s$/.test(prefix);
|
|
150
153
|
let tokens: PromptArgumentToken[];
|
|
151
154
|
try {
|
|
152
|
-
tokens =
|
|
155
|
+
tokens = tokenizeCommand(prefix);
|
|
153
156
|
} catch {
|
|
154
157
|
return null;
|
|
155
158
|
}
|
|
@@ -191,7 +194,7 @@ export function promptCompletions(
|
|
|
191
194
|
)
|
|
192
195
|
.sort((a, b) => a.prompt.name.localeCompare(b.prompt.name, "en"));
|
|
193
196
|
return prompts.map((entry) => ({
|
|
194
|
-
value: `run ${
|
|
197
|
+
value: `run ${quoteCommandArgument(server)} ${quoteCommandArgument(entry.prompt.name)}`,
|
|
195
198
|
label: entry.prompt.name,
|
|
196
199
|
...(entry.prompt.description || entry.prompt.title
|
|
197
200
|
? {
|
|
@@ -237,9 +240,9 @@ export function promptCompletions(
|
|
|
237
240
|
}
|
|
238
241
|
const base = [
|
|
239
242
|
"run",
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
...supplied.map(
|
|
243
|
+
quoteCommandArgument(server),
|
|
244
|
+
quoteCommandArgument(selected.prompt.name),
|
|
245
|
+
...supplied.map(renderCommandToken),
|
|
243
246
|
].join(" ");
|
|
244
247
|
const arguments_ = definitions.filter(
|
|
245
248
|
(argument) =>
|
|
@@ -247,7 +250,7 @@ export function promptCompletions(
|
|
|
247
250
|
);
|
|
248
251
|
return arguments_.length > 0
|
|
249
252
|
? arguments_.map((argument) => ({
|
|
250
|
-
value: `${base} ${
|
|
253
|
+
value: `${base} ${quoteCommandArgument(argument.name)}=`,
|
|
251
254
|
label: `${argument.name}=`,
|
|
252
255
|
...(argument.description
|
|
253
256
|
? { description: compact(argument.description, 100) }
|
|
@@ -262,66 +265,11 @@ function completionItems(
|
|
|
262
265
|
label: (value: string) => string,
|
|
263
266
|
) {
|
|
264
267
|
return values.map((value) => ({
|
|
265
|
-
value: `${prefix}${
|
|
268
|
+
value: `${prefix}${quoteCommandArgument(value)}`,
|
|
266
269
|
label: label(value),
|
|
267
270
|
}));
|
|
268
271
|
}
|
|
269
272
|
|
|
270
|
-
function quote(value: string): string {
|
|
271
|
-
return /^[A-Za-z0-9_.:/-]+$/.test(value)
|
|
272
|
-
? value
|
|
273
|
-
: `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function tokenize(input: string): PromptArgumentToken[] {
|
|
277
|
-
const tokens: PromptArgumentToken[] = [];
|
|
278
|
-
let token = "";
|
|
279
|
-
let separator: number | undefined;
|
|
280
|
-
let quote: '"' | "'" | undefined;
|
|
281
|
-
let escaped = false;
|
|
282
|
-
let started = false;
|
|
283
|
-
for (const character of input) {
|
|
284
|
-
if (escaped) {
|
|
285
|
-
token += character;
|
|
286
|
-
escaped = false;
|
|
287
|
-
started = true;
|
|
288
|
-
} else if (character === "\\" && quote !== "'") {
|
|
289
|
-
escaped = true;
|
|
290
|
-
started = true;
|
|
291
|
-
} else if (quote) {
|
|
292
|
-
if (character === quote) quote = undefined;
|
|
293
|
-
else token += character;
|
|
294
|
-
started = true;
|
|
295
|
-
} else if (character === '"' || character === "'") {
|
|
296
|
-
quote = character;
|
|
297
|
-
started = true;
|
|
298
|
-
} else if (/\s/u.test(character)) {
|
|
299
|
-
if (started) {
|
|
300
|
-
tokens.push({
|
|
301
|
-
value: token,
|
|
302
|
-
...(separator !== undefined ? { separator } : {}),
|
|
303
|
-
});
|
|
304
|
-
token = "";
|
|
305
|
-
separator = undefined;
|
|
306
|
-
started = false;
|
|
307
|
-
}
|
|
308
|
-
} else {
|
|
309
|
-
if (character === "=" && separator === undefined)
|
|
310
|
-
separator = token.length;
|
|
311
|
-
token += character;
|
|
312
|
-
started = true;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
if (quote) throw new Error("Unterminated quote in prompt arguments.");
|
|
316
|
-
if (escaped) throw new Error("Trailing escape in prompt arguments.");
|
|
317
|
-
if (started)
|
|
318
|
-
tokens.push({
|
|
319
|
-
value: token,
|
|
320
|
-
...(separator !== undefined ? { separator } : {}),
|
|
321
|
-
});
|
|
322
|
-
return tokens;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
273
|
function hasUsablePromptContent(
|
|
326
274
|
content: GetPromptResult["messages"][number]["content"],
|
|
327
275
|
): boolean {
|