@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.
@@ -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
- if (!source.startsWith("---")) return {};
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
- if (!source.startsWith("---")) return source;
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(manifest) {
102
- const field = manifest["workspaces"];
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 manifest = readManifest(dir);
159
- if (manifest === null) return;
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(manifest)) {
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 front = readFrontmatter(source);
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)) continue;
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
- if (!source.startsWith("---")) return {};
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
- if (!source.startsWith("---")) return "";
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, manifest) {
396
- const declared = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
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 manifest;
536
+ let manifest2;
455
537
  try {
456
- manifest = JSON.parse(
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 = manifest.name ?? "marketplace";
548
+ const marketplace = manifest2.name ?? "marketplace";
467
549
  const found = [];
468
- for (const plugin of manifest.plugins ?? []) {
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: manifest.metadata?.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 { createHash } from "crypto";
580
- import { readFileSync as readFileSync3, readdirSync as readdirSync3, realpathSync, statSync } from "fs";
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 kind = filters?.kind ?? "skill";
596
- if (kind !== "any" && kindOf(entry) !== kind) return false;
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 page = Math.min(Math.max(request.page ?? 1, 1), totalPages);
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: page < totalPages,
702
- packages: groups.slice((page - 1) * size, page * size).map(([pkg, skills]) => ({ package: pkg, skills }))
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 = catalog().find((candidate) => candidate.name === name);
1095
+ const entry = byName(name);
731
1096
  if (!entry) return null;
732
- const source = readFileSync3(entry.path, "utf8");
733
- const listing = listFiles(assetRoot(entry));
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: listing.files,
743
- filesTruncated: listing.truncated
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 = catalog().find((candidate) => candidate.name === name);
1116
+ const entry = byName(name);
752
1117
  if (!entry) return { ok: false, error: "unknown_skill", detail: name };
753
- const wanted = path ?? relative2(assetRoot(entry), entry.path);
754
- if (isAbsolute2(wanted) || wanted.split(/[\\/]/u).includes("..")) {
755
- return { ok: false, error: "path_outside_skill", detail: wanted };
756
- }
757
- const scope = realpathSync(assetRoot(entry));
758
- const single = statSync(scope).isFile();
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 = catalog().find((candidate) => candidate.name === name);
1127
+ const entry = byName(name);
793
1128
  if (!entry) return null;
794
- const root = assetRoot(entry);
795
- const listing = listFiles(root);
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(readFileSync3(entry.path, "utf8")),
800
- files: listing.files.filter(
801
- (file) => file.path !== own && file.path !== basename3(entry.path)
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/skills-extension.ts
1150
+ // src/extension/entry.ts
817
1151
  import { createHash as createHash2 } from "crypto";
818
- import { readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
819
- import { basename as basename4, dirname as dirname4, join as join4, relative as relative3, sep as sep2 } from "path";
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 = statSync2(path);
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(readFileSync4(path)).digest("hex")}`;
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 = join4(dir, entry.name);
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(readFileSync4(entry.path, "utf8"));
877
- const name = frontmatter?.["name"];
878
- if (typeof name !== "string" || name !== entry.slug || !SKILL_NAME.test(name))
879
- return null;
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 = relative3(root, path).split(sep2).join("/");
890
- return { uri: skillUri(entry, file), digest: fact.digest, size: fact.size };
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: skillUri(entry), frontmatter, resources };
1203
+ return { uri: skillFileUri(entry), frontmatter, resources };
894
1204
  }
895
- var MIME = {
896
- ".md": "text/markdown",
897
- ".json": "application/json",
898
- ".txt": "text/plain",
899
- ".py": "text/x-python",
900
- ".js": "text/javascript",
901
- ".mjs": "text/javascript",
902
- ".ts": "text/typescript",
903
- ".sh": "text/x-shellscript",
904
- ".yaml": "text/yaml",
905
- ".yml": "text/yaml",
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
- const entries = () => {
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
- const pathOf = /* @__PURE__ */ new Map();
930
- const locate = (uri) => {
931
- if (pathOf.size === 0) {
932
- for (const entry of skills()) {
933
- if (kindOf(entry) === "skill" && basename4(entry.path) === "SKILL.md") {
934
- pathOf.set(skillUri(entry), dirname4(entry.path));
935
- }
936
- }
937
- }
938
- for (const [skill, root] of pathOf) {
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: z.object({ cursor: z.string().optional() }).passthrough() },
950
- async (params) => {
951
- const all = [...entries().values()];
952
- const start = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
953
- if (!Number.isInteger(start) || start < 0 || start > all.length) {
954
- throw new ProtocolError(
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: z.object({ uri: z.string() }).passthrough() },
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
- import { z as z2 } from "zod";
1012
- var SERVER_NAME = "pmcp";
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 = z2.enum(["skill", "agent", "hook", "mcp", "any"]).optional().describe(
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
- function registerSkillTools(server, tools) {
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
- "skill_catalog",
1401
+ "skill_bundle",
1032
1402
  {
1033
- ...readOnlyTool("List skills"),
1034
- 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. Use it to see what exists, or when skill_find returns nothing. Pass kind to list agents, hooks or MCP servers instead.",
1035
- inputSchema: z2.object({
1036
- page: z2.number().int().min(1).optional(),
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 (request) => asText(tools.catalog(request))
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
- "skill_find",
1438
+ "skill_call",
1046
1439
  {
1047
- ...readOnlyTool("Find skills"),
1048
- description: "Ranked skill names for an intent. Describe the problem in a full sentence in English rather than a keyword: a single word matches far worse, and the ranking is over descriptions. Returns names only -- read one with skill_call.",
1049
- inputSchema: z2.object({
1050
- intent: z2.string().min(1).max(256).describe("What you are trying to do, as an English sentence."),
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 ({ intent, limit, filters }) => asText(await tools.find(intent, limit, filters))
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: "Frontmatter, metadata (tier, level, requirements, licence, approval), and the files the skill carries with sizes and sha256, without any body. Use it to decide between candidates before skill_call.",
1069
- inputSchema: z2.object({
1070
- name: z2.string().min(1).describe("The skill name skill_find or skill_catalog returned.")
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
- if (!described) {
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: "A reference or script file inside one skill's directory in this catalog, as text, with its sha256. It reads only files the catalog ships and calls no external API; path is relative to the skill, as skill_call and skill_describe list it (layout: https://pmcp.build). Scripts are returned as text, never run. Files over 256 KiB and paths outside the skill are refused.",
1093
- inputSchema: z2.object({
1094
- name: z2.string().min(1),
1095
- path: z2.string().min(1).optional().describe(
1096
- "Path relative to the skill directory, as skill_describe lists it. Defaults to SKILL.md."
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
- { type: "text", text }
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: "0.7.1" });
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
- READ_BUDGET_BYTES,
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-C46YCY2I.js.map
1628
+ //# sourceMappingURL=chunk-ZDV75HZN.js.map