@modootoday/pmcp 0.7.1 → 0.8.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 +40 -9
- package/dist/{catalog-5v7jrGwP.d.ts → catalog-CS0j6-0U.d.ts} +11 -11
- package/dist/{chunk-IJPOJ6PG.js → chunk-LUBYOET7.js} +6 -3
- package/dist/{chunk-GNJSZCRH.js → chunk-M6LBYJDS.js} +37 -2
- package/dist/{chunk-KP3TQ3QY.js → chunk-OPDVUHTK.js} +2 -2
- package/dist/{chunk-VBLAH6J2.js → chunk-WSTCBVXW.js} +2 -2
- package/dist/{chunk-C46YCY2I.js → chunk-ZDV75HZN.js} +823 -389
- package/dist/cli.js +50 -11
- package/dist/http.d.ts +10 -2
- package/dist/http.js +5 -4
- package/dist/index.d.ts +119 -44
- package/dist/index.js +51 -6
- package/dist/server-CLyCpwZb.d.ts +352 -0
- package/dist/stdio.d.ts +7 -3
- package/dist/stdio.js +2 -2
- package/package.json +1 -1
- package/dist/server-B9GDPRoE.d.ts +0 -217
|
@@ -3,6 +3,109 @@ function normaliseIntent(intent) {
|
|
|
3
3
|
return intent.toLowerCase().replaceAll(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/u).filter(Boolean).join(" ");
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
// src/frontmatter.ts
|
|
7
|
+
import { parse } from "yaml";
|
|
8
|
+
function frontmatterText(source) {
|
|
9
|
+
const text = source.replace(/^/u, "").replace(/\r\n/gu, "\n");
|
|
10
|
+
if (!text.startsWith("---\n") && text !== "---") return null;
|
|
11
|
+
const end = text.indexOf("\n---", 3);
|
|
12
|
+
if (end === -1) return null;
|
|
13
|
+
return text.slice(4, end + 1);
|
|
14
|
+
}
|
|
15
|
+
function bodyOf(source) {
|
|
16
|
+
const text = source.replace(/^/u, "").replace(/\r\n/gu, "\n");
|
|
17
|
+
if (!text.startsWith("---\n")) return text;
|
|
18
|
+
const end = text.indexOf("\n---", 3);
|
|
19
|
+
if (end === -1) return text;
|
|
20
|
+
return text.slice(end + 4).replace(/^\n+/u, "");
|
|
21
|
+
}
|
|
22
|
+
var asObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
23
|
+
function frontmatterObject(source) {
|
|
24
|
+
const text = frontmatterText(source);
|
|
25
|
+
if (text === null) return null;
|
|
26
|
+
try {
|
|
27
|
+
return asObject(parse(text));
|
|
28
|
+
} catch {
|
|
29
|
+
const quoted = text.split("\n").map((line) => {
|
|
30
|
+
const match = /^([A-Za-z][\w-]*):\s+(.*:\s.*)$/u.exec(line);
|
|
31
|
+
if (!match || /^["'|>[{]/u.test(match[2] ?? "")) return line;
|
|
32
|
+
return `${match[1]}: ${JSON.stringify(match[2])}`;
|
|
33
|
+
}).join("\n");
|
|
34
|
+
try {
|
|
35
|
+
return asObject(parse(quoted));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function stringFields(front) {
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const [key, value] of Object.entries(front ?? {})) {
|
|
44
|
+
if (typeof value === "string") out[key] = value.trim();
|
|
45
|
+
else if (typeof value === "number" || typeof value === "boolean")
|
|
46
|
+
out[key] = String(value);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function metadataOf(front) {
|
|
51
|
+
const out = {};
|
|
52
|
+
const meta = asObject(front?.["metadata"]);
|
|
53
|
+
for (const [key, value] of Object.entries(meta ?? {})) {
|
|
54
|
+
if (typeof value === "string") out[key] = value;
|
|
55
|
+
else if (typeof value === "number" || typeof value === "boolean")
|
|
56
|
+
out[key] = String(value);
|
|
57
|
+
else if (Array.isArray(value))
|
|
58
|
+
out[key] = value.filter((v) => typeof v === "string");
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
var SKILL_NAME = /^[a-z0-9](?:[a-z0-9]|-(?!-))*[a-z0-9]$|^[a-z0-9]$/u;
|
|
63
|
+
var NAME_MAX = 64;
|
|
64
|
+
var DESCRIPTION_MAX = 1024;
|
|
65
|
+
var COMPATIBILITY_MAX = 500;
|
|
66
|
+
function specProblems(front, directory) {
|
|
67
|
+
if (front === null) return ["frontmatter is missing or is not YAML"];
|
|
68
|
+
const problems = [];
|
|
69
|
+
const name = front["name"];
|
|
70
|
+
if (typeof name !== "string" || name === "") {
|
|
71
|
+
problems.push("name is required");
|
|
72
|
+
} else {
|
|
73
|
+
if (name.length > NAME_MAX)
|
|
74
|
+
problems.push(`name is longer than ${NAME_MAX} characters`);
|
|
75
|
+
if (!SKILL_NAME.test(name))
|
|
76
|
+
problems.push(
|
|
77
|
+
"name must be lowercase letters, digits and single hyphens, not starting or ending with one"
|
|
78
|
+
);
|
|
79
|
+
if (name !== directory)
|
|
80
|
+
problems.push(`name ${name} does not match its directory ${directory}`);
|
|
81
|
+
}
|
|
82
|
+
const description = front["description"];
|
|
83
|
+
if (typeof description !== "string" || description.trim() === "") {
|
|
84
|
+
problems.push("description is required");
|
|
85
|
+
} else if (description.length > DESCRIPTION_MAX) {
|
|
86
|
+
problems.push(`description is longer than ${DESCRIPTION_MAX} characters`);
|
|
87
|
+
}
|
|
88
|
+
const compatibility = front["compatibility"];
|
|
89
|
+
if (compatibility !== void 0) {
|
|
90
|
+
if (typeof compatibility !== "string")
|
|
91
|
+
problems.push("compatibility must be a string");
|
|
92
|
+
else if (compatibility.length > COMPATIBILITY_MAX)
|
|
93
|
+
problems.push(
|
|
94
|
+
`compatibility is longer than ${COMPATIBILITY_MAX} characters`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const license = front["license"];
|
|
98
|
+
if (license !== void 0 && typeof license !== "string")
|
|
99
|
+
problems.push("license must be a string");
|
|
100
|
+
const allowed = front["allowed-tools"];
|
|
101
|
+
if (allowed !== void 0 && typeof allowed !== "string")
|
|
102
|
+
problems.push("allowed-tools must be a space-separated string");
|
|
103
|
+
const metadata = front["metadata"];
|
|
104
|
+
if (metadata !== void 0 && asObject(metadata) === null)
|
|
105
|
+
problems.push("metadata must be a map");
|
|
106
|
+
return problems;
|
|
107
|
+
}
|
|
108
|
+
|
|
6
109
|
// src/marketplace.ts
|
|
7
110
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
8
111
|
import { basename as basename2, dirname as dirname2, join as join2, resolve } from "path";
|
|
@@ -23,24 +126,10 @@ var SKIPPED_DIRS = /* @__PURE__ */ new Set([
|
|
|
23
126
|
]);
|
|
24
127
|
var MAX_SKILL_DEPTH = 3;
|
|
25
128
|
function readFrontmatter(source) {
|
|
26
|
-
|
|
27
|
-
const end = source.indexOf("\n---", 3);
|
|
28
|
-
if (end === -1) return {};
|
|
29
|
-
const fields = {};
|
|
30
|
-
for (const line of source.slice(3, end).split("\n")) {
|
|
31
|
-
const match = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/u.exec(line);
|
|
32
|
-
if (!match) continue;
|
|
33
|
-
const value = (match[2] ?? "").trim();
|
|
34
|
-
if (value === "" || value.startsWith("#")) continue;
|
|
35
|
-
fields[match[1]] = value.replace(/^["'](.*)["']$/u, "$1");
|
|
36
|
-
}
|
|
37
|
-
return fields;
|
|
129
|
+
return stringFields(frontmatterObject(source));
|
|
38
130
|
}
|
|
39
131
|
function readBody(source) {
|
|
40
|
-
|
|
41
|
-
const end = source.indexOf("\n---", 3);
|
|
42
|
-
if (end === -1) return source;
|
|
43
|
-
return source.slice(end + 4).replace(/^(?:\r?\n)+/u, "");
|
|
132
|
+
return bodyOf(source);
|
|
44
133
|
}
|
|
45
134
|
function skillFiles(packageDir) {
|
|
46
135
|
const found = [];
|
|
@@ -98,8 +187,8 @@ function readManifest(dir) {
|
|
|
98
187
|
return null;
|
|
99
188
|
}
|
|
100
189
|
}
|
|
101
|
-
function workspacePatterns(
|
|
102
|
-
const field =
|
|
190
|
+
function workspacePatterns(manifest2) {
|
|
191
|
+
const field = manifest2["workspaces"];
|
|
103
192
|
const list = Array.isArray(field) ? field : field?.packages;
|
|
104
193
|
if (!Array.isArray(list)) return [];
|
|
105
194
|
return list.filter((p) => typeof p === "string");
|
|
@@ -155,11 +244,11 @@ function workspacePackageDirs(root) {
|
|
|
155
244
|
const expand = (dir) => {
|
|
156
245
|
if (visited.has(dir)) return;
|
|
157
246
|
visited.add(dir);
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
247
|
+
const manifest2 = readManifest(dir);
|
|
248
|
+
if (manifest2 === null) return;
|
|
160
249
|
const included = /* @__PURE__ */ new Set();
|
|
161
250
|
const excluded = /* @__PURE__ */ new Set();
|
|
162
|
-
for (const raw of workspacePatterns(
|
|
251
|
+
for (const raw of workspacePatterns(manifest2)) {
|
|
163
252
|
const negated = raw.startsWith("!");
|
|
164
253
|
const pattern = negated ? raw.slice(1) : raw;
|
|
165
254
|
for (const alternative of expandBraces(pattern)) {
|
|
@@ -215,20 +304,32 @@ function readCatalog(options) {
|
|
|
215
304
|
} catch {
|
|
216
305
|
continue;
|
|
217
306
|
}
|
|
218
|
-
const
|
|
307
|
+
const parsed = frontmatterObject(source);
|
|
308
|
+
const front = stringFields(parsed);
|
|
219
309
|
const description = front["description"] ?? "";
|
|
220
310
|
if (description === "") continue;
|
|
221
311
|
const dirName = basename(dirname(path));
|
|
222
312
|
const slug = front["name"] ?? (dirName === basename(packageDir) ? packageName : dirName);
|
|
223
313
|
const name = `${packageName}/${slug}`;
|
|
224
|
-
if (seen.has(name))
|
|
314
|
+
if (seen.has(name)) {
|
|
315
|
+
if (!path.includes("/node_modules/"))
|
|
316
|
+
options.onWarning?.({
|
|
317
|
+
path,
|
|
318
|
+
reason: `duplicate name ${name}; the first one found is served`
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
225
322
|
seen.add(name);
|
|
323
|
+
for (const problem of specProblems(parsed, dirName))
|
|
324
|
+
options.onWarning?.({ path, reason: problem });
|
|
325
|
+
const metadata = metadataOf(parsed);
|
|
226
326
|
found.push({
|
|
227
327
|
name,
|
|
228
328
|
package: packageName,
|
|
229
329
|
slug,
|
|
230
330
|
description,
|
|
231
|
-
path
|
|
331
|
+
path,
|
|
332
|
+
...Object.keys(metadata).length > 0 ? { metadata } : {}
|
|
232
333
|
});
|
|
233
334
|
}
|
|
234
335
|
}
|
|
@@ -242,6 +343,14 @@ function readCatalog(options) {
|
|
|
242
343
|
continue;
|
|
243
344
|
}
|
|
244
345
|
seen.add(entry.name);
|
|
346
|
+
if (kindOf(entry) === "skill" && basename(entry.path) === "SKILL.md") {
|
|
347
|
+
const parsed = frontmatterObject(readFileSync(entry.path, "utf8"));
|
|
348
|
+
for (const problem of specProblems(
|
|
349
|
+
parsed,
|
|
350
|
+
basename(dirname(entry.path))
|
|
351
|
+
))
|
|
352
|
+
options.onWarning?.({ path: entry.path, reason: problem });
|
|
353
|
+
}
|
|
245
354
|
found.push(entry);
|
|
246
355
|
}
|
|
247
356
|
}
|
|
@@ -259,38 +368,11 @@ function displayPath(root, entry) {
|
|
|
259
368
|
}
|
|
260
369
|
|
|
261
370
|
// src/marketplace.ts
|
|
262
|
-
function parseScalar(raw) {
|
|
263
|
-
const value = raw.trim();
|
|
264
|
-
if (value.startsWith("[") && value.endsWith("]")) {
|
|
265
|
-
return value.slice(1, -1).split(",").map((item) => item.trim().replace(/^["'](.*)["']$/u, "$1")).filter(Boolean);
|
|
266
|
-
}
|
|
267
|
-
return value.replace(/^["'](.*)["']$/u, "$1");
|
|
268
|
-
}
|
|
269
371
|
function readMetadata(source) {
|
|
270
|
-
|
|
271
|
-
const end = source.indexOf("\n---", 3);
|
|
272
|
-
if (end === -1) return {};
|
|
273
|
-
const out = {};
|
|
274
|
-
let inside = false;
|
|
275
|
-
for (const line of source.slice(3, end).split("\n")) {
|
|
276
|
-
if (/^metadata:\s*$/u.test(line)) {
|
|
277
|
-
inside = true;
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
if (!inside) continue;
|
|
281
|
-
if (!/^\s/u.test(line)) break;
|
|
282
|
-
const match = /^ {2}([A-Za-z][A-Za-z0-9_.-]*):\s*(.*)$/u.exec(line);
|
|
283
|
-
if (!match) continue;
|
|
284
|
-
const raw = match[2] ?? "";
|
|
285
|
-
if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
|
|
286
|
-
out[match[1]] = parseScalar(raw);
|
|
287
|
-
}
|
|
288
|
-
return out;
|
|
372
|
+
return metadataOf(frontmatterObject(source));
|
|
289
373
|
}
|
|
290
374
|
function readFrontmatterText(source) {
|
|
291
|
-
|
|
292
|
-
const end = source.indexOf("\n---", 3);
|
|
293
|
-
return end === -1 ? "" : source.slice(3, end).replace(/^\r?\n/u, "");
|
|
375
|
+
return frontmatterText(source) ?? "";
|
|
294
376
|
}
|
|
295
377
|
var asString = (value) => typeof value === "string" ? value : void 0;
|
|
296
378
|
var asList = (value) => {
|
|
@@ -392,8 +474,8 @@ function agentEntries(context) {
|
|
|
392
474
|
}
|
|
393
475
|
return found;
|
|
394
476
|
}
|
|
395
|
-
function hookEntries(context,
|
|
396
|
-
const declared = typeof
|
|
477
|
+
function hookEntries(context, manifest2) {
|
|
478
|
+
const declared = typeof manifest2.hooks === "string" ? manifest2.hooks : void 0;
|
|
397
479
|
const path = resolve(context.pluginDir, declared ?? "hooks/hooks.json");
|
|
398
480
|
if (!existsSync2(path)) return [];
|
|
399
481
|
const hooks = readJson(path);
|
|
@@ -451,9 +533,9 @@ function readMarketplace(dir, onReject) {
|
|
|
451
533
|
});
|
|
452
534
|
return [];
|
|
453
535
|
}
|
|
454
|
-
let
|
|
536
|
+
let manifest2;
|
|
455
537
|
try {
|
|
456
|
-
|
|
538
|
+
manifest2 = JSON.parse(
|
|
457
539
|
readFileSync2(manifestPath, "utf8")
|
|
458
540
|
);
|
|
459
541
|
} catch {
|
|
@@ -463,9 +545,9 @@ function readMarketplace(dir, onReject) {
|
|
|
463
545
|
});
|
|
464
546
|
return [];
|
|
465
547
|
}
|
|
466
|
-
const marketplace =
|
|
548
|
+
const marketplace = manifest2.name ?? "marketplace";
|
|
467
549
|
const found = [];
|
|
468
|
-
for (const plugin of
|
|
550
|
+
for (const plugin of manifest2.plugins ?? []) {
|
|
469
551
|
if (!plugin.name || !plugin.source) continue;
|
|
470
552
|
const pluginDir = resolve(dir, plugin.source);
|
|
471
553
|
if (!existsSync2(pluginDir)) continue;
|
|
@@ -477,7 +559,7 @@ function readMarketplace(dir, onReject) {
|
|
|
477
559
|
plugin: plugin.name,
|
|
478
560
|
pluginDir,
|
|
479
561
|
pluginDescription: typeof pluginManifest.description === "string" ? pluginManifest.description : void 0,
|
|
480
|
-
tier:
|
|
562
|
+
tier: manifest2.metadata?.tier,
|
|
481
563
|
onReject
|
|
482
564
|
};
|
|
483
565
|
found.push(
|
|
@@ -492,6 +574,316 @@ function readMarketplace(dir, onReject) {
|
|
|
492
574
|
}
|
|
493
575
|
var assetRoot = (entry) => entry.root ?? dirname2(entry.path);
|
|
494
576
|
|
|
577
|
+
// src/prompts.ts
|
|
578
|
+
import { z } from "zod";
|
|
579
|
+
var promptName = (entry) => entry.name.replace(/^@/u, "").replace(/[^A-Za-z0-9_-]+/gu, "-");
|
|
580
|
+
function messageText(tools, name, task) {
|
|
581
|
+
const found = tools.call(name);
|
|
582
|
+
if (!found) return `The skill ${name} is no longer in this catalog.`;
|
|
583
|
+
const files = found.files.map((file) => `- ${file.path}`);
|
|
584
|
+
const parts = [found.body];
|
|
585
|
+
if (files.length > 0)
|
|
586
|
+
parts.push(
|
|
587
|
+
`Files in skill ${found.name} (skill_read returns each by path):
|
|
588
|
+
${files.join("\n")}`
|
|
589
|
+
);
|
|
590
|
+
if (task) parts.push(`Task: ${task}`);
|
|
591
|
+
return parts.join("\n\n");
|
|
592
|
+
}
|
|
593
|
+
function registerSkillPrompts(server, tools) {
|
|
594
|
+
const taken = /* @__PURE__ */ new Set();
|
|
595
|
+
for (const entry of tools.skills()) {
|
|
596
|
+
if (kindOf(entry) !== "skill") continue;
|
|
597
|
+
const name = promptName(entry);
|
|
598
|
+
if (taken.has(name)) continue;
|
|
599
|
+
taken.add(name);
|
|
600
|
+
server.registerPrompt(
|
|
601
|
+
name,
|
|
602
|
+
{
|
|
603
|
+
title: entry.name,
|
|
604
|
+
description: entry.description,
|
|
605
|
+
argsSchema: z.object({
|
|
606
|
+
task: z.string().optional().describe("What to apply the skill to.")
|
|
607
|
+
})
|
|
608
|
+
},
|
|
609
|
+
({ task }) => ({
|
|
610
|
+
messages: [
|
|
611
|
+
{
|
|
612
|
+
role: "user",
|
|
613
|
+
content: {
|
|
614
|
+
type: "text",
|
|
615
|
+
text: messageText(tools, entry.name, task)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
]
|
|
619
|
+
})
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/files/list.ts
|
|
625
|
+
import { createHash } from "crypto";
|
|
626
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync3, statSync } from "fs";
|
|
627
|
+
import { basename as basename3, join as join3, relative as relative2, sep } from "path";
|
|
628
|
+
var FILE_LIST_LIMIT = 200;
|
|
629
|
+
var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
630
|
+
function describe(path, full) {
|
|
631
|
+
const bytes = readFileSync3(full);
|
|
632
|
+
return { path, bytes: bytes.length, sha256: sha256(bytes) };
|
|
633
|
+
}
|
|
634
|
+
function listSkillFiles(root, limit = FILE_LIST_LIMIT) {
|
|
635
|
+
if (statSync(root).isFile()) {
|
|
636
|
+
return { files: [describe(basename3(root), root)], truncated: false };
|
|
637
|
+
}
|
|
638
|
+
const files = [];
|
|
639
|
+
const stack = [root];
|
|
640
|
+
while (stack.length > 0) {
|
|
641
|
+
const dir = stack.pop();
|
|
642
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
643
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
644
|
+
const full = join3(dir, entry.name);
|
|
645
|
+
if (entry.isDirectory()) {
|
|
646
|
+
stack.push(full);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (!entry.isFile()) continue;
|
|
650
|
+
if (files.length >= limit) return { files, truncated: true };
|
|
651
|
+
files.push(describe(relative2(root, full).split(sep).join("/"), full));
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
655
|
+
return { files, truncated: false };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/mime.ts
|
|
659
|
+
var TYPES = {
|
|
660
|
+
".md": "text/markdown",
|
|
661
|
+
".markdown": "text/markdown",
|
|
662
|
+
".txt": "text/plain",
|
|
663
|
+
".json": "application/json",
|
|
664
|
+
".jsonl": "application/jsonl",
|
|
665
|
+
".yaml": "text/yaml",
|
|
666
|
+
".yml": "text/yaml",
|
|
667
|
+
".toml": "application/toml",
|
|
668
|
+
".csv": "text/csv",
|
|
669
|
+
".tsv": "text/tab-separated-values",
|
|
670
|
+
".xml": "application/xml",
|
|
671
|
+
".html": "text/html",
|
|
672
|
+
".htm": "text/html",
|
|
673
|
+
".css": "text/css",
|
|
674
|
+
".js": "text/javascript",
|
|
675
|
+
".mjs": "text/javascript",
|
|
676
|
+
".cjs": "text/javascript",
|
|
677
|
+
".jsx": "text/javascript",
|
|
678
|
+
".ts": "text/typescript",
|
|
679
|
+
".tsx": "text/typescript",
|
|
680
|
+
".mts": "text/typescript",
|
|
681
|
+
".cts": "text/typescript",
|
|
682
|
+
".py": "text/x-python",
|
|
683
|
+
".rb": "text/x-ruby",
|
|
684
|
+
".go": "text/x-go",
|
|
685
|
+
".rs": "text/x-rust",
|
|
686
|
+
".java": "text/x-java",
|
|
687
|
+
".sh": "text/x-shellscript",
|
|
688
|
+
".bash": "text/x-shellscript",
|
|
689
|
+
".zsh": "text/x-shellscript",
|
|
690
|
+
".ps1": "text/x-powershell",
|
|
691
|
+
".sql": "application/sql",
|
|
692
|
+
".ejs": "text/x-ejs",
|
|
693
|
+
".hbs": "text/x-handlebars-template",
|
|
694
|
+
".mustache": "text/x-mustache",
|
|
695
|
+
".svg": "image/svg+xml",
|
|
696
|
+
".png": "image/png",
|
|
697
|
+
".jpg": "image/jpeg",
|
|
698
|
+
".jpeg": "image/jpeg",
|
|
699
|
+
".gif": "image/gif",
|
|
700
|
+
".webp": "image/webp",
|
|
701
|
+
".avif": "image/avif",
|
|
702
|
+
".ico": "image/x-icon",
|
|
703
|
+
".bmp": "image/bmp",
|
|
704
|
+
".mp3": "audio/mpeg",
|
|
705
|
+
".wav": "audio/wav",
|
|
706
|
+
".ogg": "audio/ogg",
|
|
707
|
+
".m4a": "audio/mp4",
|
|
708
|
+
".flac": "audio/flac",
|
|
709
|
+
".mp4": "video/mp4",
|
|
710
|
+
".webm": "video/webm",
|
|
711
|
+
".pdf": "application/pdf",
|
|
712
|
+
".zip": "application/zip",
|
|
713
|
+
".gz": "application/gzip",
|
|
714
|
+
".tar": "application/x-tar",
|
|
715
|
+
".wasm": "application/wasm",
|
|
716
|
+
".woff": "font/woff",
|
|
717
|
+
".woff2": "font/woff2",
|
|
718
|
+
".ttf": "font/ttf",
|
|
719
|
+
".otf": "font/otf",
|
|
720
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
721
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
722
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
723
|
+
};
|
|
724
|
+
var TEXT_APPLICATION = /* @__PURE__ */ new Set([
|
|
725
|
+
"application/json",
|
|
726
|
+
"application/jsonl",
|
|
727
|
+
"application/toml",
|
|
728
|
+
"application/xml",
|
|
729
|
+
"application/sql",
|
|
730
|
+
"image/svg+xml"
|
|
731
|
+
]);
|
|
732
|
+
function mimeOf(path) {
|
|
733
|
+
const slash = path.lastIndexOf("/");
|
|
734
|
+
const dot = path.lastIndexOf(".");
|
|
735
|
+
if (dot === -1 || dot < slash) return null;
|
|
736
|
+
return TYPES[path.slice(dot).toLowerCase()] ?? null;
|
|
737
|
+
}
|
|
738
|
+
function isUtf8Text(bytes) {
|
|
739
|
+
if (bytes.includes(0)) return false;
|
|
740
|
+
try {
|
|
741
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
742
|
+
return true;
|
|
743
|
+
} catch {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function classify(path, bytes) {
|
|
748
|
+
const declared = mimeOf(path);
|
|
749
|
+
const textual = declared === null ? isUtf8Text(bytes) : declared.startsWith("text/") || TEXT_APPLICATION.has(declared);
|
|
750
|
+
if (textual && isUtf8Text(bytes)) {
|
|
751
|
+
return { kind: "text", mimeType: declared ?? "text/plain" };
|
|
752
|
+
}
|
|
753
|
+
if (declared?.startsWith("image/") && declared !== "image/svg+xml") {
|
|
754
|
+
return { kind: "image", mimeType: declared };
|
|
755
|
+
}
|
|
756
|
+
if (declared?.startsWith("audio/"))
|
|
757
|
+
return { kind: "audio", mimeType: declared };
|
|
758
|
+
return { kind: "blob", mimeType: declared ?? "application/octet-stream" };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/files/read.ts
|
|
762
|
+
import { readFileSync as readFileSync4, statSync as statSync3 } from "fs";
|
|
763
|
+
|
|
764
|
+
// src/files/resolve.ts
|
|
765
|
+
import { realpathSync, statSync as statSync2 } from "fs";
|
|
766
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
|
|
767
|
+
var ownFile = (entry) => relative3(assetRoot(entry), entry.path).split(sep2).join("/");
|
|
768
|
+
function resolveSkillFile(entry, wanted = ownFile(entry)) {
|
|
769
|
+
if (isAbsolute2(wanted) || wanted.split(/[\\/]/u).includes("..")) {
|
|
770
|
+
return { ok: false, error: "path_outside_skill", detail: wanted };
|
|
771
|
+
}
|
|
772
|
+
const scope = realpathSync(assetRoot(entry));
|
|
773
|
+
const single = statSync2(scope).isFile();
|
|
774
|
+
const root = single ? dirname3(scope) : scope;
|
|
775
|
+
let target;
|
|
776
|
+
try {
|
|
777
|
+
target = realpathSync(
|
|
778
|
+
single && wanted === "" ? scope : resolve2(root, wanted)
|
|
779
|
+
);
|
|
780
|
+
} catch {
|
|
781
|
+
return { ok: false, error: "no_such_file", detail: wanted };
|
|
782
|
+
}
|
|
783
|
+
const inside = single ? target === scope : target === root || target.startsWith(root + sep2);
|
|
784
|
+
if (!inside)
|
|
785
|
+
return { ok: false, error: "path_outside_skill", detail: wanted };
|
|
786
|
+
return {
|
|
787
|
+
ok: true,
|
|
788
|
+
target,
|
|
789
|
+
path: relative3(root, target).split(sep2).join("/")
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/files/read.ts
|
|
794
|
+
var READ_BUDGET_BYTES = 256 * 1024;
|
|
795
|
+
function readSkillFile(entry, wanted, budget = READ_BUDGET_BYTES) {
|
|
796
|
+
const resolved = resolveSkillFile(entry, wanted);
|
|
797
|
+
if (!resolved.ok) return resolved;
|
|
798
|
+
const size = statSync3(resolved.target).size;
|
|
799
|
+
if (size > budget) {
|
|
800
|
+
return {
|
|
801
|
+
ok: false,
|
|
802
|
+
error: "over_budget",
|
|
803
|
+
detail: `${size} bytes > ${budget}`
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const bytes = readFileSync4(resolved.target);
|
|
807
|
+
const { kind: kind2, mimeType } = classify(resolved.path, bytes);
|
|
808
|
+
return {
|
|
809
|
+
ok: true,
|
|
810
|
+
name: entry.name,
|
|
811
|
+
path: resolved.path,
|
|
812
|
+
bytes: bytes.length,
|
|
813
|
+
sha256: sha256(bytes),
|
|
814
|
+
mimeType,
|
|
815
|
+
kind: kind2,
|
|
816
|
+
...kind2 === "text" ? { text: bytes.toString("utf8") } : { blob: bytes.toString("base64") }
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/files/uri.ts
|
|
821
|
+
var SKILL_URI_AUTHORITY = "pmcp";
|
|
822
|
+
var PREFIX = `skill://${SKILL_URI_AUTHORITY}/`;
|
|
823
|
+
var skillFileUri = (entry, file = "SKILL.md") => `${PREFIX}${entry.name}/${file}`;
|
|
824
|
+
function resolveSkillFileUri(uri, entries) {
|
|
825
|
+
if (!uri.startsWith(PREFIX)) return null;
|
|
826
|
+
const rest = decodeURI(uri.slice(PREFIX.length));
|
|
827
|
+
let best = null;
|
|
828
|
+
for (const entry of entries) {
|
|
829
|
+
if (!rest.startsWith(`${entry.name}/`)) continue;
|
|
830
|
+
if (best === null || entry.name.length > best.name.length) best = entry;
|
|
831
|
+
}
|
|
832
|
+
if (best === null) return null;
|
|
833
|
+
const file = rest.slice(best.name.length + 1);
|
|
834
|
+
return file === "" ? null : { entry: best, file };
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/resources.ts
|
|
838
|
+
import { statSync as statSync4 } from "fs";
|
|
839
|
+
import {
|
|
840
|
+
ResourceNotFoundError,
|
|
841
|
+
ResourceTemplate
|
|
842
|
+
} from "@modelcontextprotocol/server";
|
|
843
|
+
var RESOURCE_READ_BUDGET = 16 * 1024 * 1024;
|
|
844
|
+
var COMPLETION_LIMIT = 100;
|
|
845
|
+
function listing(entries) {
|
|
846
|
+
return entries.filter((entry) => kindOf(entry) === "skill").map((entry) => ({
|
|
847
|
+
uri: skillFileUri(entry, ownFile(entry)),
|
|
848
|
+
name: entry.name,
|
|
849
|
+
description: entry.description,
|
|
850
|
+
mimeType: "text/markdown",
|
|
851
|
+
size: statSync4(entry.path).size
|
|
852
|
+
}));
|
|
853
|
+
}
|
|
854
|
+
function complete(value, entries) {
|
|
855
|
+
const owner = entries.find((entry) => value.startsWith(`${entry.name}/`));
|
|
856
|
+
if (!owner) {
|
|
857
|
+
return entries.map((entry) => `${entry.name}/`).filter((name) => name.startsWith(value)).slice(0, COMPLETION_LIMIT);
|
|
858
|
+
}
|
|
859
|
+
return listSkillFiles(assetRoot(owner)).files.map((file) => `${owner.name}/${file.path}`).filter((path) => path.startsWith(value)).slice(0, COMPLETION_LIMIT);
|
|
860
|
+
}
|
|
861
|
+
function registerSkillResources(server, skills) {
|
|
862
|
+
server.registerResource(
|
|
863
|
+
"skill-files",
|
|
864
|
+
new ResourceTemplate(`skill://${SKILL_URI_AUTHORITY}/{+path}`, {
|
|
865
|
+
list: () => ({ resources: listing(skills()) }),
|
|
866
|
+
complete: { path: (value) => complete(value, skills()) }
|
|
867
|
+
}),
|
|
868
|
+
{
|
|
869
|
+
title: "Skill files",
|
|
870
|
+
description: "Files of the skills this catalog serves: SKILL.md, references, scripts and assets."
|
|
871
|
+
},
|
|
872
|
+
async (uri) => {
|
|
873
|
+
const href = uri.toString();
|
|
874
|
+
const found = resolveSkillFileUri(href, skills());
|
|
875
|
+
if (!found) throw new ResourceNotFoundError(href);
|
|
876
|
+
const file = readSkillFile(found.entry, found.file, RESOURCE_READ_BUDGET);
|
|
877
|
+
if (!file.ok) throw new ResourceNotFoundError(href);
|
|
878
|
+
return {
|
|
879
|
+
contents: [
|
|
880
|
+
file.kind === "text" ? { uri: href, mimeType: file.mimeType, text: file.text ?? "" } : { uri: href, mimeType: file.mimeType, blob: file.blob ?? "" }
|
|
881
|
+
]
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
495
887
|
// src/find.ts
|
|
496
888
|
var DEFAULT_FIND_LIMIT = 5;
|
|
497
889
|
function tokens(text) {
|
|
@@ -575,25 +967,32 @@ function rank(matches2, limit) {
|
|
|
575
967
|
return matches2.filter((match) => match.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
|
|
576
968
|
}
|
|
577
969
|
|
|
970
|
+
// src/files/bundle.ts
|
|
971
|
+
import { readFileSync as readFileSync5, statSync as statSync5 } from "fs";
|
|
972
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
973
|
+
function bundleSkill(entry) {
|
|
974
|
+
const root = assetRoot(entry);
|
|
975
|
+
const base = statSync5(root).isFile() ? dirname4(root) : root;
|
|
976
|
+
const listing2 = listSkillFiles(root);
|
|
977
|
+
return {
|
|
978
|
+
name: entry.name,
|
|
979
|
+
truncated: listing2.truncated,
|
|
980
|
+
files: listing2.files.map((file) => ({
|
|
981
|
+
...file,
|
|
982
|
+
uri: skillFileUri(entry, file.path),
|
|
983
|
+
mimeType: classify(file.path, readFileSync5(join4(base, file.path))).mimeType
|
|
984
|
+
}))
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
578
988
|
// src/server.ts
|
|
579
|
-
import {
|
|
580
|
-
|
|
581
|
-
import {
|
|
582
|
-
basename as basename3,
|
|
583
|
-
dirname as dirname3,
|
|
584
|
-
isAbsolute as isAbsolute2,
|
|
585
|
-
join as join3,
|
|
586
|
-
relative as relative2,
|
|
587
|
-
resolve as resolve2,
|
|
588
|
-
sep
|
|
589
|
-
} from "path";
|
|
590
|
-
var READ_BUDGET_BYTES = 256 * 1024;
|
|
591
|
-
var DESCRIBE_FILE_LIMIT = 200;
|
|
989
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
990
|
+
var DESCRIBE_FILE_LIMIT = FILE_LIST_LIMIT;
|
|
592
991
|
var CATALOG_PAGE_SIZE = 8;
|
|
593
992
|
var CATALOG_PAGE_MAX = 20;
|
|
594
993
|
function matches(entry, filters) {
|
|
595
|
-
const
|
|
596
|
-
if (
|
|
994
|
+
const kind2 = filters?.kind ?? "skill";
|
|
995
|
+
if (kind2 !== "any" && kindOf(entry) !== kind2) return false;
|
|
597
996
|
if (!filters) return true;
|
|
598
997
|
const meta = entry.metadata ?? {};
|
|
599
998
|
const field = (key) => {
|
|
@@ -611,42 +1010,6 @@ function matches(entry, filters) {
|
|
|
611
1010
|
return false;
|
|
612
1011
|
return true;
|
|
613
1012
|
}
|
|
614
|
-
var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
615
|
-
function listFiles(root) {
|
|
616
|
-
const files = [];
|
|
617
|
-
if (statSync(root).isFile()) {
|
|
618
|
-
const bytes = readFileSync3(root);
|
|
619
|
-
const only = {
|
|
620
|
-
path: basename3(root),
|
|
621
|
-
bytes: bytes.length,
|
|
622
|
-
sha256: sha256(bytes)
|
|
623
|
-
};
|
|
624
|
-
return { files: [only], truncated: false };
|
|
625
|
-
}
|
|
626
|
-
const stack = [root];
|
|
627
|
-
while (stack.length > 0) {
|
|
628
|
-
const dir = stack.pop();
|
|
629
|
-
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
630
|
-
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
631
|
-
const full = join3(dir, entry.name);
|
|
632
|
-
if (entry.isDirectory()) {
|
|
633
|
-
stack.push(full);
|
|
634
|
-
continue;
|
|
635
|
-
}
|
|
636
|
-
if (!entry.isFile()) continue;
|
|
637
|
-
if (files.length >= DESCRIBE_FILE_LIMIT)
|
|
638
|
-
return { files, truncated: true };
|
|
639
|
-
const bytes = readFileSync3(full);
|
|
640
|
-
files.push({
|
|
641
|
-
path: relative2(root, full).split(sep).join("/"),
|
|
642
|
-
bytes: bytes.length,
|
|
643
|
-
sha256: sha256(bytes)
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
648
|
-
return { files, truncated: false };
|
|
649
|
-
}
|
|
650
1013
|
function catalogItem(entry) {
|
|
651
1014
|
return {
|
|
652
1015
|
name: entry.name,
|
|
@@ -665,10 +1028,12 @@ function createSkillTools(options) {
|
|
|
665
1028
|
marketplaces: options.marketplaces,
|
|
666
1029
|
packages: options.packages,
|
|
667
1030
|
workspaces: options.workspaces,
|
|
668
|
-
onReject: options.onReject
|
|
1031
|
+
onReject: options.onReject,
|
|
1032
|
+
onWarning: options.onWarning
|
|
669
1033
|
});
|
|
670
1034
|
return entries;
|
|
671
1035
|
};
|
|
1036
|
+
const byName = (name) => catalog().find((candidate) => candidate.name === name) ?? null;
|
|
672
1037
|
return {
|
|
673
1038
|
/**
|
|
674
1039
|
* Grouped, and without bodies. The cheap overview and the search's miss
|
|
@@ -693,13 +1058,13 @@ function createSkillTools(options) {
|
|
|
693
1058
|
CATALOG_PAGE_MAX
|
|
694
1059
|
);
|
|
695
1060
|
const totalPages = Math.max(1, Math.ceil(groups.length / size));
|
|
696
|
-
const
|
|
1061
|
+
const page2 = Math.min(Math.max(request.page ?? 1, 1), totalPages);
|
|
697
1062
|
return {
|
|
698
1063
|
count,
|
|
699
|
-
page,
|
|
1064
|
+
page: page2,
|
|
700
1065
|
totalPages,
|
|
701
|
-
hasNext:
|
|
702
|
-
packages: groups.slice((
|
|
1066
|
+
hasNext: page2 < totalPages,
|
|
1067
|
+
packages: groups.slice((page2 - 1) * size, page2 * size).map(([pkg, skills]) => ({ package: pkg, skills }))
|
|
703
1068
|
};
|
|
704
1069
|
},
|
|
705
1070
|
/** Ranked names. Returns; never loads a body. */
|
|
@@ -727,20 +1092,20 @@ function createSkillTools(options) {
|
|
|
727
1092
|
},
|
|
728
1093
|
/** Every property of one skill and the files it carries, without bodies. */
|
|
729
1094
|
describe(name) {
|
|
730
|
-
const entry =
|
|
1095
|
+
const entry = byName(name);
|
|
731
1096
|
if (!entry) return null;
|
|
732
|
-
const source =
|
|
733
|
-
const
|
|
1097
|
+
const source = readFileSync6(entry.path, "utf8");
|
|
1098
|
+
const listing2 = listSkillFiles(assetRoot(entry));
|
|
734
1099
|
return {
|
|
735
1100
|
name: entry.name,
|
|
736
1101
|
kind: kindOf(entry),
|
|
737
1102
|
package: entry.package,
|
|
738
1103
|
description: entry.description,
|
|
739
|
-
tier: entry.tier,
|
|
1104
|
+
...entry.tier === void 0 ? {} : { tier: entry.tier },
|
|
740
1105
|
metadata: entry.metadata ?? {},
|
|
741
1106
|
frontmatter: readFrontmatterText(source),
|
|
742
|
-
files:
|
|
743
|
-
filesTruncated:
|
|
1107
|
+
files: [...listing2.files],
|
|
1108
|
+
filesTruncated: listing2.truncated
|
|
744
1109
|
};
|
|
745
1110
|
},
|
|
746
1111
|
/**
|
|
@@ -748,61 +1113,30 @@ function createSkillTools(options) {
|
|
|
748
1113
|
* directly or through a link, are refused; nothing is ever executed.
|
|
749
1114
|
*/
|
|
750
1115
|
read(name, path) {
|
|
751
|
-
const entry =
|
|
1116
|
+
const entry = byName(name);
|
|
752
1117
|
if (!entry) return { ok: false, error: "unknown_skill", detail: name };
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
const root = single ? dirname3(scope) : scope;
|
|
760
|
-
let target;
|
|
761
|
-
try {
|
|
762
|
-
target = realpathSync(
|
|
763
|
-
single && wanted === "" ? scope : resolve2(root, wanted)
|
|
764
|
-
);
|
|
765
|
-
} catch {
|
|
766
|
-
return { ok: false, error: "no_such_file", detail: wanted };
|
|
767
|
-
}
|
|
768
|
-
const inside = single ? target === scope : target === root || target.startsWith(root + sep);
|
|
769
|
-
if (!inside) {
|
|
770
|
-
return { ok: false, error: "path_outside_skill", detail: wanted };
|
|
771
|
-
}
|
|
772
|
-
const size = statSync(target).size;
|
|
773
|
-
if (size > READ_BUDGET_BYTES) {
|
|
774
|
-
return {
|
|
775
|
-
ok: false,
|
|
776
|
-
error: "over_budget",
|
|
777
|
-
detail: `${size} bytes > ${READ_BUDGET_BYTES}`
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
const bytes = readFileSync3(target);
|
|
781
|
-
return {
|
|
782
|
-
ok: true,
|
|
783
|
-
name: entry.name,
|
|
784
|
-
path: relative2(root, target).split(sep).join("/"),
|
|
785
|
-
bytes: bytes.length,
|
|
786
|
-
sha256: sha256(bytes),
|
|
787
|
-
text: bytes.toString("utf8")
|
|
788
|
-
};
|
|
1118
|
+
return readSkillFile(entry, path);
|
|
1119
|
+
},
|
|
1120
|
+
/** Every file of one skill as links; the bytes stay out of the answer. */
|
|
1121
|
+
bundle(name) {
|
|
1122
|
+
const entry = byName(name);
|
|
1123
|
+
return entry ? bundleSkill(entry) : null;
|
|
789
1124
|
},
|
|
790
|
-
/** One body, at the moment it is needed. */
|
|
1125
|
+
/** One body, at the moment it is needed, and the other files it carries. */
|
|
791
1126
|
call(name) {
|
|
792
|
-
const entry =
|
|
1127
|
+
const entry = byName(name);
|
|
793
1128
|
if (!entry) return null;
|
|
794
|
-
const
|
|
795
|
-
const
|
|
796
|
-
const own = relative2(root, entry.path).split(sep).join("/");
|
|
1129
|
+
const bundle = bundleSkill(entry);
|
|
1130
|
+
const own = ownFile(entry);
|
|
797
1131
|
return {
|
|
798
1132
|
name: entry.name,
|
|
799
|
-
body: readBody(
|
|
800
|
-
files:
|
|
801
|
-
|
|
802
|
-
),
|
|
803
|
-
filesTruncated: listing.truncated
|
|
1133
|
+
body: readBody(readFileSync6(entry.path, "utf8")),
|
|
1134
|
+
files: bundle.files.filter((file) => file.path !== own),
|
|
1135
|
+
filesTruncated: bundle.truncated
|
|
804
1136
|
};
|
|
805
1137
|
},
|
|
1138
|
+
/** The catalog entry for a name, among those this caller may see. */
|
|
1139
|
+
entry: (name) => byName(name),
|
|
806
1140
|
/** Drops the memoised catalog, so the next call re-reads the tree. */
|
|
807
1141
|
refresh() {
|
|
808
1142
|
entries = null;
|
|
@@ -813,32 +1147,19 @@ function createSkillTools(options) {
|
|
|
813
1147
|
};
|
|
814
1148
|
}
|
|
815
1149
|
|
|
816
|
-
// src/
|
|
1150
|
+
// src/extension/entry.ts
|
|
817
1151
|
import { createHash as createHash2 } from "crypto";
|
|
818
|
-
import { readFileSync as
|
|
819
|
-
import { basename as basename4, dirname as
|
|
820
|
-
import {
|
|
821
|
-
ProtocolError,
|
|
822
|
-
ProtocolErrorCode,
|
|
823
|
-
ResourceNotFoundError,
|
|
824
|
-
ResourceTemplate
|
|
825
|
-
} from "@modelcontextprotocol/server";
|
|
826
|
-
import { parse as parseYaml } from "yaml";
|
|
827
|
-
import { z } from "zod";
|
|
828
|
-
var SKILLS_EXTENSION = "io.modelcontextprotocol/skills";
|
|
829
|
-
var AUTHORITY = "pmcp";
|
|
830
|
-
var SKILL_NAME = /^[a-z0-9](?:[a-z0-9]|-(?!-))*[a-z0-9]$|^[a-z0-9]$/u;
|
|
1152
|
+
import { readFileSync as readFileSync7, readdirSync as readdirSync4, statSync as statSync6 } from "fs";
|
|
1153
|
+
import { basename as basename4, dirname as dirname5, join as join5, relative as relative4, sep as sep3 } from "path";
|
|
831
1154
|
var MAX_RESOURCES = 512;
|
|
832
1155
|
var MAX_SKILL_BYTES = 16 * 1024 * 1024;
|
|
833
|
-
var PAGE_SIZE = 50;
|
|
834
|
-
var LIST_TTL_MS = 6e4;
|
|
835
1156
|
var digests = /* @__PURE__ */ new Map();
|
|
836
1157
|
function facts(path) {
|
|
837
|
-
const stat =
|
|
1158
|
+
const stat = statSync6(path);
|
|
838
1159
|
const key = `${stat.mtimeMs}:${stat.size}`;
|
|
839
1160
|
const cached = digests.get(path);
|
|
840
1161
|
if (cached?.key === key) return cached;
|
|
841
|
-
const digest = `sha256:${createHash2("sha256").update(
|
|
1162
|
+
const digest = `sha256:${createHash2("sha256").update(readFileSync7(path)).digest("hex")}`;
|
|
842
1163
|
const fresh = { key, digest, size: stat.size };
|
|
843
1164
|
digests.set(path, fresh);
|
|
844
1165
|
return fresh;
|
|
@@ -850,7 +1171,7 @@ function filesUnder(root) {
|
|
|
850
1171
|
const dir = stack.pop();
|
|
851
1172
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
852
1173
|
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
853
|
-
const full =
|
|
1174
|
+
const full = join5(dir, entry.name);
|
|
854
1175
|
if (entry.isDirectory()) stack.push(full);
|
|
855
1176
|
else if (entry.isFile()) found.push(full);
|
|
856
1177
|
if (found.length > MAX_RESOURCES) return null;
|
|
@@ -858,117 +1179,85 @@ function filesUnder(root) {
|
|
|
858
1179
|
}
|
|
859
1180
|
return found.sort();
|
|
860
1181
|
}
|
|
861
|
-
function frontmatterObject(source) {
|
|
862
|
-
if (!source.startsWith("---")) return null;
|
|
863
|
-
const end = source.indexOf("\n---", 3);
|
|
864
|
-
if (end === -1) return null;
|
|
865
|
-
try {
|
|
866
|
-
const parsed = parseYaml(source.slice(3, end));
|
|
867
|
-
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
868
|
-
} catch {
|
|
869
|
-
return null;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
var skillUri = (entry, file = "SKILL.md") => `skill://${AUTHORITY}/${entry.name}/${file}`;
|
|
873
1182
|
function extensionEntry(entry) {
|
|
874
1183
|
if (kindOf(entry) !== "skill" || basename4(entry.path) !== "SKILL.md")
|
|
875
1184
|
return null;
|
|
876
|
-
const frontmatter = frontmatterObject(
|
|
877
|
-
const
|
|
878
|
-
if (
|
|
879
|
-
|
|
880
|
-
if (basename4(dirname4(entry.path)) !== name) return null;
|
|
881
|
-
if (typeof frontmatter?.["description"] !== "string") return null;
|
|
882
|
-
const root = dirname4(entry.path);
|
|
1185
|
+
const frontmatter = frontmatterObject(readFileSync7(entry.path, "utf8"));
|
|
1186
|
+
const root = dirname5(entry.path);
|
|
1187
|
+
if (specProblems(frontmatter, basename4(root)).length > 0) return null;
|
|
1188
|
+
if (frontmatter?.["name"] !== entry.slug) return null;
|
|
883
1189
|
const files = filesUnder(root);
|
|
884
1190
|
if (files === null) return null;
|
|
885
1191
|
let total = 0;
|
|
886
1192
|
const resources = files.map((path) => {
|
|
887
1193
|
const fact = facts(path);
|
|
888
1194
|
total += fact.size;
|
|
889
|
-
const file =
|
|
890
|
-
return {
|
|
1195
|
+
const file = relative4(root, path).split(sep3).join("/");
|
|
1196
|
+
return {
|
|
1197
|
+
uri: skillFileUri(entry, file),
|
|
1198
|
+
digest: fact.digest,
|
|
1199
|
+
size: fact.size
|
|
1200
|
+
};
|
|
891
1201
|
});
|
|
892
1202
|
if (total > MAX_SKILL_BYTES) return null;
|
|
893
|
-
return { uri:
|
|
1203
|
+
return { uri: skillFileUri(entry), frontmatter, resources };
|
|
894
1204
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
".toml": "text/plain"
|
|
907
|
-
};
|
|
908
|
-
var mimeOf = (path) => {
|
|
909
|
-
const dot = path.lastIndexOf(".");
|
|
910
|
-
return dot === -1 ? void 0 : MIME[path.slice(dot).toLowerCase()];
|
|
911
|
-
};
|
|
912
|
-
function registerSkillsExtension(server, skills) {
|
|
1205
|
+
|
|
1206
|
+
// src/extension/register.ts
|
|
1207
|
+
import {
|
|
1208
|
+
ProtocolError,
|
|
1209
|
+
ProtocolErrorCode
|
|
1210
|
+
} from "@modelcontextprotocol/server";
|
|
1211
|
+
import { z as z2 } from "zod";
|
|
1212
|
+
var SKILLS_EXTENSION = "io.modelcontextprotocol/skills";
|
|
1213
|
+
var PAGE_SIZE = 50;
|
|
1214
|
+
var TTL_MS = 6e4;
|
|
1215
|
+
function entriesOf(skills) {
|
|
913
1216
|
let memo = null;
|
|
914
|
-
|
|
1217
|
+
return () => {
|
|
915
1218
|
const source = skills();
|
|
916
1219
|
if (memo?.source === source) return memo.entries;
|
|
917
1220
|
const map = /* @__PURE__ */ new Map();
|
|
918
1221
|
for (const entry of source) {
|
|
919
|
-
let served = null;
|
|
920
1222
|
try {
|
|
921
|
-
served = extensionEntry(entry);
|
|
1223
|
+
const served = extensionEntry(entry);
|
|
1224
|
+
if (served) map.set(served.uri, served);
|
|
922
1225
|
} catch {
|
|
923
1226
|
}
|
|
924
|
-
if (served) map.set(served.uri, served);
|
|
925
1227
|
}
|
|
926
1228
|
memo = { source, entries: map };
|
|
927
1229
|
return map;
|
|
928
1230
|
};
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
const prefix = skill.slice(0, -"SKILL.md".length);
|
|
940
|
-
if (uri.startsWith(prefix)) return join4(root, uri.slice(prefix.length));
|
|
941
|
-
}
|
|
942
|
-
return void 0;
|
|
1231
|
+
}
|
|
1232
|
+
function page(all, cursor) {
|
|
1233
|
+
const start = cursor ? Number.parseInt(cursor, 10) : 0;
|
|
1234
|
+
if (!Number.isInteger(start) || start < 0 || start > all.length) {
|
|
1235
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "invalid cursor");
|
|
1236
|
+
}
|
|
1237
|
+
const end = start + PAGE_SIZE;
|
|
1238
|
+
return {
|
|
1239
|
+
skills: all.slice(start, end),
|
|
1240
|
+
...end < all.length ? { nextCursor: String(end) } : {}
|
|
943
1241
|
};
|
|
1242
|
+
}
|
|
1243
|
+
function registerSkillsExtension(server, skills, options = {}) {
|
|
1244
|
+
const entries = entriesOf(skills);
|
|
1245
|
+
const cache = { ttlMs: TTL_MS, cacheScope: options.cacheScope ?? "public" };
|
|
944
1246
|
server.server.registerCapabilities({
|
|
945
1247
|
extensions: { [SKILLS_EXTENSION]: {} }
|
|
946
1248
|
});
|
|
947
1249
|
server.server.setRequestHandler(
|
|
948
1250
|
"skills/list",
|
|
949
|
-
{ params:
|
|
950
|
-
async (params) => {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
ProtocolErrorCode.InvalidParams,
|
|
956
|
-
"invalid cursor"
|
|
957
|
-
);
|
|
958
|
-
}
|
|
959
|
-
const end = start + PAGE_SIZE;
|
|
960
|
-
return {
|
|
961
|
-
resultType: "complete",
|
|
962
|
-
skills: all.slice(start, end),
|
|
963
|
-
...end < all.length ? { nextCursor: String(end) } : {},
|
|
964
|
-
ttlMs: LIST_TTL_MS,
|
|
965
|
-
cacheScope: "public"
|
|
966
|
-
};
|
|
967
|
-
}
|
|
1251
|
+
{ params: z2.object({ cursor: z2.string().optional() }).passthrough() },
|
|
1252
|
+
async (params) => ({
|
|
1253
|
+
resultType: "complete",
|
|
1254
|
+
...page([...entries().values()], params.cursor),
|
|
1255
|
+
...cache
|
|
1256
|
+
})
|
|
968
1257
|
);
|
|
969
1258
|
server.server.setRequestHandler(
|
|
970
1259
|
"skills/get",
|
|
971
|
-
{ params:
|
|
1260
|
+
{ params: z2.object({ uri: z2.string() }).passthrough() },
|
|
972
1261
|
async (params) => {
|
|
973
1262
|
const skill = entries().get(params.uri);
|
|
974
1263
|
if (!skill) {
|
|
@@ -977,39 +1266,104 @@ function registerSkillsExtension(server, skills) {
|
|
|
977
1266
|
`not a skill this server serves: ${params.uri}`
|
|
978
1267
|
);
|
|
979
1268
|
}
|
|
980
|
-
return { resultType: "complete", skill };
|
|
981
|
-
}
|
|
982
|
-
);
|
|
983
|
-
server.registerResource(
|
|
984
|
-
"skill-files",
|
|
985
|
-
new ResourceTemplate(`skill://${AUTHORITY}/{+path}`, { list: void 0 }),
|
|
986
|
-
{
|
|
987
|
-
title: "Skill files",
|
|
988
|
-
description: "Files of the skills listed by skills/list."
|
|
989
|
-
},
|
|
990
|
-
async (uri) => {
|
|
991
|
-
const href = uri.toString();
|
|
992
|
-
const listed = [...entries().values()].some(
|
|
993
|
-
(skill) => skill.resources.some((resource) => resource.uri === href)
|
|
994
|
-
);
|
|
995
|
-
const path = listed ? locate(href) : void 0;
|
|
996
|
-
if (path === void 0) throw new ResourceNotFoundError(href);
|
|
997
|
-
const bytes = readFileSync4(path);
|
|
998
|
-
const mimeType = mimeOf(path);
|
|
999
|
-
const text = mimeType !== void 0;
|
|
1000
|
-
return {
|
|
1001
|
-
contents: [
|
|
1002
|
-
text ? { uri: href, mimeType, text: bytes.toString("utf8") } : { uri: href, blob: bytes.toString("base64") }
|
|
1003
|
-
]
|
|
1004
|
-
};
|
|
1269
|
+
return { resultType: "complete", skill, ...cache };
|
|
1005
1270
|
}
|
|
1006
1271
|
);
|
|
1007
1272
|
}
|
|
1008
1273
|
|
|
1274
|
+
// src/tools/schemas.ts
|
|
1275
|
+
import { z as z3 } from "zod";
|
|
1276
|
+
var kind = z3.enum(ASSET_KINDS);
|
|
1277
|
+
var catalogItemSchema = z3.object({
|
|
1278
|
+
name: z3.string(),
|
|
1279
|
+
description: z3.string(),
|
|
1280
|
+
tier: z3.string().optional(),
|
|
1281
|
+
kind: kind.optional()
|
|
1282
|
+
});
|
|
1283
|
+
var catalogResponseSchema = z3.object({
|
|
1284
|
+
count: z3.number().int(),
|
|
1285
|
+
page: z3.number().int(),
|
|
1286
|
+
totalPages: z3.number().int(),
|
|
1287
|
+
hasNext: z3.boolean(),
|
|
1288
|
+
packages: z3.array(
|
|
1289
|
+
z3.object({ package: z3.string(), skills: z3.array(catalogItemSchema) })
|
|
1290
|
+
)
|
|
1291
|
+
});
|
|
1292
|
+
var findResultSchema = z3.object({
|
|
1293
|
+
ranking: z3.enum(["semantic", "lexical"]),
|
|
1294
|
+
matches: z3.array(
|
|
1295
|
+
z3.object({
|
|
1296
|
+
name: z3.string(),
|
|
1297
|
+
package: z3.string(),
|
|
1298
|
+
description: z3.string(),
|
|
1299
|
+
score: z3.number()
|
|
1300
|
+
})
|
|
1301
|
+
)
|
|
1302
|
+
});
|
|
1303
|
+
var skillFileSchema = z3.object({
|
|
1304
|
+
path: z3.string(),
|
|
1305
|
+
bytes: z3.number().int(),
|
|
1306
|
+
sha256: z3.string()
|
|
1307
|
+
});
|
|
1308
|
+
var describeResponseSchema = z3.object({
|
|
1309
|
+
name: z3.string(),
|
|
1310
|
+
kind,
|
|
1311
|
+
package: z3.string(),
|
|
1312
|
+
description: z3.string(),
|
|
1313
|
+
tier: z3.string().optional(),
|
|
1314
|
+
metadata: z3.record(
|
|
1315
|
+
z3.string(),
|
|
1316
|
+
z3.union([z3.string(), z3.array(z3.string()).readonly()])
|
|
1317
|
+
),
|
|
1318
|
+
frontmatter: z3.string(),
|
|
1319
|
+
files: z3.array(skillFileSchema),
|
|
1320
|
+
filesTruncated: z3.boolean()
|
|
1321
|
+
});
|
|
1322
|
+
var bundleResponseSchema = z3.object({
|
|
1323
|
+
name: z3.string(),
|
|
1324
|
+
truncated: z3.boolean(),
|
|
1325
|
+
files: z3.array(
|
|
1326
|
+
skillFileSchema.extend({ uri: z3.string(), mimeType: z3.string() })
|
|
1327
|
+
)
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1009
1330
|
// src/mcp.ts
|
|
1010
1331
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
1011
|
-
|
|
1012
|
-
|
|
1332
|
+
|
|
1333
|
+
// src/tools/bundle.ts
|
|
1334
|
+
import { z as z5 } from "zod";
|
|
1335
|
+
|
|
1336
|
+
// src/tools/content.ts
|
|
1337
|
+
function fileBlock(file) {
|
|
1338
|
+
if (file.kind === "text") {
|
|
1339
|
+
return { type: "text", text: file.text ?? "" };
|
|
1340
|
+
}
|
|
1341
|
+
if (file.kind === "image" || file.kind === "audio") {
|
|
1342
|
+
return {
|
|
1343
|
+
type: file.kind,
|
|
1344
|
+
data: file.blob ?? "",
|
|
1345
|
+
mimeType: file.mimeType
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
return {
|
|
1349
|
+
type: "resource",
|
|
1350
|
+
resource: {
|
|
1351
|
+
uri: skillFileUri({ name: file.name }, file.path),
|
|
1352
|
+
mimeType: file.mimeType,
|
|
1353
|
+
blob: file.blob ?? ""
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
var resourceLink = (file) => ({
|
|
1358
|
+
type: "resource_link",
|
|
1359
|
+
uri: file.uri,
|
|
1360
|
+
name: file.path,
|
|
1361
|
+
mimeType: file.mimeType,
|
|
1362
|
+
size: file.bytes
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
// src/tools/shared.ts
|
|
1366
|
+
import { z as z4 } from "zod";
|
|
1013
1367
|
var readOnly = {
|
|
1014
1368
|
readOnlyHint: true,
|
|
1015
1369
|
destructiveHint: false,
|
|
@@ -1020,147 +1374,212 @@ var readOnlyTool = (title) => ({
|
|
|
1020
1374
|
title,
|
|
1021
1375
|
annotations: { ...readOnly, title }
|
|
1022
1376
|
});
|
|
1023
|
-
var kindInput =
|
|
1377
|
+
var kindInput = z4.enum(["skill", "agent", "hook", "mcp", "any"]).optional().describe(
|
|
1024
1378
|
"skill (default), agent, hook, mcp, or any. Commands are skills; agents, hooks and MCP servers are installed with pmcp add rather than read."
|
|
1025
1379
|
);
|
|
1380
|
+
var tierInput = z4.enum(["open", "free", "paid", "internal"]).optional();
|
|
1381
|
+
var skillNameInput = z4.string().min(1).describe("A skill name as skill_find or skill_catalog returns it.");
|
|
1026
1382
|
var asText = (value) => ({
|
|
1027
1383
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
1028
1384
|
});
|
|
1029
|
-
|
|
1385
|
+
var structured = (value) => ({
|
|
1386
|
+
...asText(value),
|
|
1387
|
+
structuredContent: { ...value }
|
|
1388
|
+
});
|
|
1389
|
+
var unknownSkill = (name) => ({
|
|
1390
|
+
...asText({
|
|
1391
|
+
error: "unknown_skill",
|
|
1392
|
+
name,
|
|
1393
|
+
hint: "skill_catalog lists the names that exist."
|
|
1394
|
+
}),
|
|
1395
|
+
isError: true
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
// src/tools/bundle.ts
|
|
1399
|
+
function registerBundleTool(server, tools) {
|
|
1030
1400
|
server.registerTool(
|
|
1031
|
-
"
|
|
1401
|
+
"skill_bundle",
|
|
1032
1402
|
{
|
|
1033
|
-
...readOnlyTool("List
|
|
1034
|
-
description: "Every
|
|
1035
|
-
inputSchema:
|
|
1036
|
-
|
|
1037
|
-
pageSize: z2.number().int().min(1).max(20).optional(),
|
|
1038
|
-
tier: z2.enum(["open", "free", "paid", "internal"]).optional(),
|
|
1039
|
-
kind: kindInput
|
|
1040
|
-
})
|
|
1403
|
+
...readOnlyTool("List a skill's files"),
|
|
1404
|
+
description: "Every file of one skill with its path relative to the skill directory, size, media type and sha256, as resource links that resources/read returns. The bytes are not included in this result. Used to write a skill to a local skills directory.",
|
|
1405
|
+
inputSchema: z5.object({ name: skillNameInput }),
|
|
1406
|
+
outputSchema: bundleResponseSchema
|
|
1041
1407
|
},
|
|
1042
|
-
async (
|
|
1408
|
+
async ({ name }) => {
|
|
1409
|
+
const bundle = tools.bundle(name);
|
|
1410
|
+
if (!bundle) return unknownSkill(name);
|
|
1411
|
+
const value = { ...bundle, files: [...bundle.files] };
|
|
1412
|
+
return {
|
|
1413
|
+
content: [
|
|
1414
|
+
{ type: "text", text: JSON.stringify(value, null, 2) },
|
|
1415
|
+
...bundle.files.map(resourceLink)
|
|
1416
|
+
],
|
|
1417
|
+
structuredContent: value
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/tools/call.ts
|
|
1424
|
+
import { z as z6 } from "zod";
|
|
1425
|
+
function manifest(found) {
|
|
1426
|
+
const lines = found.files.map(
|
|
1427
|
+
(file) => `- ${file.path} (${file.bytes} bytes, ${file.mimeType}, sha256 ${file.sha256.slice(0, 12)})`
|
|
1043
1428
|
);
|
|
1429
|
+
if (found.filesTruncated)
|
|
1430
|
+
lines.push("- (list cut; skill_bundle has the rest)");
|
|
1431
|
+
return [
|
|
1432
|
+
`Files in skill ${found.name}. skill_read returns one by this name and its path; skill_bundle returns every file with its path:`,
|
|
1433
|
+
...lines
|
|
1434
|
+
].join("\n");
|
|
1435
|
+
}
|
|
1436
|
+
function registerCallTool(server, tools) {
|
|
1044
1437
|
server.registerTool(
|
|
1045
|
-
"
|
|
1438
|
+
"skill_call",
|
|
1046
1439
|
{
|
|
1047
|
-
...readOnlyTool("
|
|
1048
|
-
description: "
|
|
1049
|
-
inputSchema:
|
|
1050
|
-
|
|
1051
|
-
limit: z2.number().int().min(1).max(25).optional(),
|
|
1052
|
-
filters: z2.object({
|
|
1053
|
-
kind: kindInput,
|
|
1054
|
-
tier: z2.enum(["open", "free", "paid", "internal"]).optional(),
|
|
1055
|
-
package: z2.string().optional(),
|
|
1056
|
-
level: z2.string().optional(),
|
|
1057
|
-
domain: z2.string().optional(),
|
|
1058
|
-
medium: z2.string().optional()
|
|
1059
|
-
}).optional().describe("Exact-match filters over the catalog before ranking.")
|
|
1440
|
+
...readOnlyTool("Read a skill"),
|
|
1441
|
+
description: "The body of a single skill, by the name skill_find or skill_catalog returned, followed by the list of reference, script and asset files the skill carries, if any, as text and as resource links. This is the only call that costs a document.",
|
|
1442
|
+
inputSchema: z6.object({
|
|
1443
|
+
name: z6.string().min(1).describe("The skill name, as <package>/<slug>.")
|
|
1060
1444
|
})
|
|
1061
1445
|
},
|
|
1062
|
-
async ({
|
|
1446
|
+
async ({ name }) => {
|
|
1447
|
+
const found = tools.call(name);
|
|
1448
|
+
if (!found) return unknownSkill(name);
|
|
1449
|
+
const body = { type: "text", text: found.body };
|
|
1450
|
+
if (found.files.length === 0) return { content: [body] };
|
|
1451
|
+
return {
|
|
1452
|
+
content: [
|
|
1453
|
+
body,
|
|
1454
|
+
{ type: "text", text: manifest(found) },
|
|
1455
|
+
...found.files.map(resourceLink)
|
|
1456
|
+
]
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// src/tools/catalog.ts
|
|
1463
|
+
import { z as z7 } from "zod";
|
|
1464
|
+
function registerCatalogTool(server, tools) {
|
|
1465
|
+
server.registerTool(
|
|
1466
|
+
"skill_catalog",
|
|
1467
|
+
{
|
|
1468
|
+
...readOnlyTool("List skills"),
|
|
1469
|
+
description: "Every skill the installed packages and marketplaces ship, grouped by package or plugin, with names, one-line descriptions and tier, and no bodies. Paged by group. With kind it lists agents, hooks or MCP servers instead.",
|
|
1470
|
+
inputSchema: z7.object({
|
|
1471
|
+
page: z7.number().int().min(1).optional(),
|
|
1472
|
+
pageSize: z7.number().int().min(1).max(20).optional(),
|
|
1473
|
+
tier: tierInput,
|
|
1474
|
+
kind: kindInput
|
|
1475
|
+
}),
|
|
1476
|
+
outputSchema: catalogResponseSchema
|
|
1477
|
+
},
|
|
1478
|
+
async (request) => structured(tools.catalog(request))
|
|
1063
1479
|
);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// src/tools/describe.ts
|
|
1483
|
+
import { z as z8 } from "zod";
|
|
1484
|
+
function registerDescribeTool(server, tools) {
|
|
1064
1485
|
server.registerTool(
|
|
1065
1486
|
"skill_describe",
|
|
1066
1487
|
{
|
|
1067
1488
|
...readOnlyTool("Describe a skill"),
|
|
1068
|
-
description: "
|
|
1069
|
-
inputSchema:
|
|
1070
|
-
|
|
1071
|
-
})
|
|
1489
|
+
description: "One skill's frontmatter, metadata (tier, level, requirements, licence, approval), and the files it carries with sizes and sha256, without any body.",
|
|
1490
|
+
inputSchema: z8.object({ name: skillNameInput }),
|
|
1491
|
+
outputSchema: describeResponseSchema
|
|
1072
1492
|
},
|
|
1073
1493
|
async ({ name }) => {
|
|
1074
1494
|
const described = tools.describe(name);
|
|
1075
|
-
|
|
1076
|
-
return {
|
|
1077
|
-
...asText({
|
|
1078
|
-
error: "unknown_skill",
|
|
1079
|
-
name,
|
|
1080
|
-
hint: "Call skill_catalog for the names that exist."
|
|
1081
|
-
}),
|
|
1082
|
-
isError: true
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
return asText(described);
|
|
1495
|
+
return described ? structured(described) : unknownSkill(name);
|
|
1086
1496
|
}
|
|
1087
1497
|
);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/tools/find.ts
|
|
1501
|
+
import { z as z9 } from "zod";
|
|
1502
|
+
function registerFindTool(server, tools) {
|
|
1503
|
+
server.registerTool(
|
|
1504
|
+
"skill_find",
|
|
1505
|
+
{
|
|
1506
|
+
...readOnlyTool("Find skills"),
|
|
1507
|
+
description: "Skill names ranked against an intent, with descriptions and scores, and no bodies. The ranking compares the intent with skill descriptions, so a full sentence matches better than a single keyword.",
|
|
1508
|
+
inputSchema: z9.object({
|
|
1509
|
+
intent: z9.string().min(1).max(256).describe("What the task is, as a sentence."),
|
|
1510
|
+
limit: z9.number().int().min(1).max(25).optional(),
|
|
1511
|
+
filters: z9.object({
|
|
1512
|
+
kind: kindInput,
|
|
1513
|
+
tier: tierInput,
|
|
1514
|
+
package: z9.string().optional(),
|
|
1515
|
+
level: z9.string().optional(),
|
|
1516
|
+
domain: z9.string().optional(),
|
|
1517
|
+
medium: z9.string().optional()
|
|
1518
|
+
}).optional().describe("Exact-match filters over the catalog before ranking.")
|
|
1519
|
+
}),
|
|
1520
|
+
outputSchema: findResultSchema
|
|
1521
|
+
},
|
|
1522
|
+
async ({ intent, limit, filters }) => structured(await tools.find(intent, limit, filters))
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// src/tools/read.ts
|
|
1527
|
+
import { z as z10 } from "zod";
|
|
1528
|
+
function registerReadTool(server, tools) {
|
|
1088
1529
|
server.registerTool(
|
|
1089
1530
|
"skill_read",
|
|
1090
1531
|
{
|
|
1091
1532
|
...readOnlyTool("Read a skill file"),
|
|
1092
|
-
description: "
|
|
1093
|
-
inputSchema:
|
|
1094
|
-
name:
|
|
1095
|
-
path:
|
|
1096
|
-
"Path relative to the skill directory
|
|
1533
|
+
description: "One file inside a skill's directory in this catalog, with its sha256 and media type: text as text, images and audio as image and audio content, other binary files as an embedded resource. path is relative to the skill, as skill_call, skill_bundle and skill_describe list it (layout: https://pmcp.build). Scripts are returned as content, never run, and no external API is called. Files over 256 KiB and paths outside the skill are refused.",
|
|
1534
|
+
inputSchema: z10.object({
|
|
1535
|
+
name: skillNameInput,
|
|
1536
|
+
path: z10.string().min(1).optional().describe(
|
|
1537
|
+
"Path relative to the skill directory. Defaults to SKILL.md."
|
|
1097
1538
|
)
|
|
1098
1539
|
})
|
|
1099
1540
|
},
|
|
1100
1541
|
async ({ name, path }) => {
|
|
1101
1542
|
const result = tools.read(name, path);
|
|
1102
1543
|
if (!result.ok) return { ...asText(result), isError: true };
|
|
1103
|
-
const { text, ...header } = result;
|
|
1544
|
+
const { text: _text, blob: _blob, ...header } = result;
|
|
1104
1545
|
return {
|
|
1105
1546
|
content: [
|
|
1106
1547
|
{ type: "text", text: JSON.stringify(header) },
|
|
1107
|
-
|
|
1108
|
-
]
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
);
|
|
1112
|
-
server.registerTool(
|
|
1113
|
-
"skill_call",
|
|
1114
|
-
{
|
|
1115
|
-
...readOnlyTool("Read a skill"),
|
|
1116
|
-
description: "The body of a single skill, by the name skill_find or skill_catalog returned, followed by a list of the reference and script files the skill carries, if any. This is the only call that costs a document.",
|
|
1117
|
-
inputSchema: z2.object({
|
|
1118
|
-
name: z2.string().min(1).describe("The skill name, as <package>/<slug>.")
|
|
1119
|
-
})
|
|
1120
|
-
},
|
|
1121
|
-
async ({ name }) => {
|
|
1122
|
-
const found = tools.call(name);
|
|
1123
|
-
if (!found) {
|
|
1124
|
-
return {
|
|
1125
|
-
...asText({
|
|
1126
|
-
error: "unknown_skill",
|
|
1127
|
-
name,
|
|
1128
|
-
hint: "Call skill_catalog for the names that exist."
|
|
1129
|
-
}),
|
|
1130
|
-
isError: true
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
if (found.files.length === 0) {
|
|
1134
|
-
return { content: [{ type: "text", text: found.body }] };
|
|
1135
|
-
}
|
|
1136
|
-
const lines = found.files.map(
|
|
1137
|
-
(file) => `- ${file.path} (${file.bytes} bytes, sha256 ${file.sha256.slice(0, 12)})`
|
|
1138
|
-
);
|
|
1139
|
-
if (found.filesTruncated)
|
|
1140
|
-
lines.push("- (list cut; skill_describe has the rest)");
|
|
1141
|
-
const manifest = [
|
|
1142
|
-
`Files in skill ${found.name}, each returned by skill_read with this name and the path:`,
|
|
1143
|
-
...lines
|
|
1144
|
-
].join("\n");
|
|
1145
|
-
return {
|
|
1146
|
-
content: [
|
|
1147
|
-
{ type: "text", text: found.body },
|
|
1148
|
-
{ type: "text", text: manifest }
|
|
1548
|
+
fileBlock(result)
|
|
1149
1549
|
]
|
|
1150
1550
|
};
|
|
1151
1551
|
}
|
|
1152
1552
|
);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/mcp.ts
|
|
1556
|
+
var SERVER_NAME = "pmcp";
|
|
1557
|
+
var SERVER_VERSION = "0.8.0";
|
|
1558
|
+
function registerSkillTools(server, tools) {
|
|
1559
|
+
registerCatalogTool(server, tools);
|
|
1560
|
+
registerFindTool(server, tools);
|
|
1561
|
+
registerDescribeTool(server, tools);
|
|
1562
|
+
registerReadTool(server, tools);
|
|
1563
|
+
registerCallTool(server, tools);
|
|
1564
|
+
registerBundleTool(server, tools);
|
|
1153
1565
|
return server;
|
|
1154
1566
|
}
|
|
1155
1567
|
function createSkillServer(options) {
|
|
1156
|
-
const server = new McpServer({ name: SERVER_NAME, version:
|
|
1568
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
|
|
1157
1569
|
const tools = createSkillTools(options);
|
|
1158
|
-
registerSkillsExtension(server, tools.skills);
|
|
1570
|
+
registerSkillsExtension(server, tools.skills, options.skillsExtension);
|
|
1571
|
+
registerSkillResources(server, tools.skills);
|
|
1572
|
+
if (options.prompts) registerSkillPrompts(server, tools);
|
|
1159
1573
|
return registerSkillTools(server, tools);
|
|
1160
1574
|
}
|
|
1161
1575
|
|
|
1162
1576
|
export {
|
|
1163
1577
|
normaliseIntent,
|
|
1578
|
+
frontmatterObject,
|
|
1579
|
+
NAME_MAX,
|
|
1580
|
+
DESCRIPTION_MAX,
|
|
1581
|
+
COMPATIBILITY_MAX,
|
|
1582
|
+
specProblems,
|
|
1164
1583
|
readMetadata,
|
|
1165
1584
|
readFrontmatterText,
|
|
1166
1585
|
readMarketplace,
|
|
@@ -1174,21 +1593,36 @@ export {
|
|
|
1174
1593
|
readCatalog,
|
|
1175
1594
|
withoutBody,
|
|
1176
1595
|
displayPath,
|
|
1596
|
+
promptName,
|
|
1597
|
+
registerSkillPrompts,
|
|
1598
|
+
listSkillFiles,
|
|
1599
|
+
mimeOf,
|
|
1600
|
+
isUtf8Text,
|
|
1601
|
+
classify,
|
|
1602
|
+
READ_BUDGET_BYTES,
|
|
1603
|
+
readSkillFile,
|
|
1604
|
+
skillFileUri,
|
|
1605
|
+
resolveSkillFileUri,
|
|
1606
|
+
registerSkillResources,
|
|
1177
1607
|
DEFAULT_FIND_LIMIT,
|
|
1178
1608
|
lexicalScore,
|
|
1179
1609
|
cosine,
|
|
1180
1610
|
find,
|
|
1181
|
-
|
|
1611
|
+
bundleSkill,
|
|
1182
1612
|
DESCRIBE_FILE_LIMIT,
|
|
1183
1613
|
CATALOG_PAGE_SIZE,
|
|
1184
1614
|
CATALOG_PAGE_MAX,
|
|
1185
1615
|
createSkillTools,
|
|
1186
|
-
SKILLS_EXTENSION,
|
|
1187
|
-
skillUri,
|
|
1188
1616
|
extensionEntry,
|
|
1617
|
+
SKILLS_EXTENSION,
|
|
1189
1618
|
registerSkillsExtension,
|
|
1619
|
+
catalogResponseSchema,
|
|
1620
|
+
findResultSchema,
|
|
1621
|
+
describeResponseSchema,
|
|
1622
|
+
bundleResponseSchema,
|
|
1190
1623
|
SERVER_NAME,
|
|
1624
|
+
SERVER_VERSION,
|
|
1191
1625
|
registerSkillTools,
|
|
1192
1626
|
createSkillServer
|
|
1193
1627
|
};
|
|
1194
|
-
//# sourceMappingURL=chunk-
|
|
1628
|
+
//# sourceMappingURL=chunk-ZDV75HZN.js.map
|