@modootoday/pmcp 0.7.0 → 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,54 +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;
1129
+ const bundle = bundleSkill(entry);
1130
+ const own = ownFile(entry);
794
1131
  return {
795
1132
  name: entry.name,
796
- body: readBody(readFileSync3(entry.path, "utf8"))
1133
+ body: readBody(readFileSync6(entry.path, "utf8")),
1134
+ files: bundle.files.filter((file) => file.path !== own),
1135
+ filesTruncated: bundle.truncated
797
1136
  };
798
1137
  },
1138
+ /** The catalog entry for a name, among those this caller may see. */
1139
+ entry: (name) => byName(name),
799
1140
  /** Drops the memoised catalog, so the next call re-reads the tree. */
800
1141
  refresh() {
801
1142
  entries = null;
@@ -806,32 +1147,19 @@ function createSkillTools(options) {
806
1147
  };
807
1148
  }
808
1149
 
809
- // src/skills-extension.ts
1150
+ // src/extension/entry.ts
810
1151
  import { createHash as createHash2 } from "crypto";
811
- import { readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
812
- import { basename as basename4, dirname as dirname4, join as join4, relative as relative3, sep as sep2 } from "path";
813
- import {
814
- ProtocolError,
815
- ProtocolErrorCode,
816
- ResourceNotFoundError,
817
- ResourceTemplate
818
- } from "@modelcontextprotocol/server";
819
- import { parse as parseYaml } from "yaml";
820
- import { z } from "zod";
821
- var SKILLS_EXTENSION = "io.modelcontextprotocol/skills";
822
- var AUTHORITY = "pmcp";
823
- 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";
824
1154
  var MAX_RESOURCES = 512;
825
1155
  var MAX_SKILL_BYTES = 16 * 1024 * 1024;
826
- var PAGE_SIZE = 50;
827
- var LIST_TTL_MS = 6e4;
828
1156
  var digests = /* @__PURE__ */ new Map();
829
1157
  function facts(path) {
830
- const stat = statSync2(path);
1158
+ const stat = statSync6(path);
831
1159
  const key = `${stat.mtimeMs}:${stat.size}`;
832
1160
  const cached = digests.get(path);
833
1161
  if (cached?.key === key) return cached;
834
- const digest = `sha256:${createHash2("sha256").update(readFileSync4(path)).digest("hex")}`;
1162
+ const digest = `sha256:${createHash2("sha256").update(readFileSync7(path)).digest("hex")}`;
835
1163
  const fresh = { key, digest, size: stat.size };
836
1164
  digests.set(path, fresh);
837
1165
  return fresh;
@@ -843,7 +1171,7 @@ function filesUnder(root) {
843
1171
  const dir = stack.pop();
844
1172
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
845
1173
  if (entry.name === "node_modules" || entry.name === ".git") continue;
846
- const full = join4(dir, entry.name);
1174
+ const full = join5(dir, entry.name);
847
1175
  if (entry.isDirectory()) stack.push(full);
848
1176
  else if (entry.isFile()) found.push(full);
849
1177
  if (found.length > MAX_RESOURCES) return null;
@@ -851,117 +1179,85 @@ function filesUnder(root) {
851
1179
  }
852
1180
  return found.sort();
853
1181
  }
854
- function frontmatterObject(source) {
855
- if (!source.startsWith("---")) return null;
856
- const end = source.indexOf("\n---", 3);
857
- if (end === -1) return null;
858
- try {
859
- const parsed = parseYaml(source.slice(3, end));
860
- return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
861
- } catch {
862
- return null;
863
- }
864
- }
865
- var skillUri = (entry, file = "SKILL.md") => `skill://${AUTHORITY}/${entry.name}/${file}`;
866
1182
  function extensionEntry(entry) {
867
1183
  if (kindOf(entry) !== "skill" || basename4(entry.path) !== "SKILL.md")
868
1184
  return null;
869
- const frontmatter = frontmatterObject(readFileSync4(entry.path, "utf8"));
870
- const name = frontmatter?.["name"];
871
- if (typeof name !== "string" || name !== entry.slug || !SKILL_NAME.test(name))
872
- return null;
873
- if (basename4(dirname4(entry.path)) !== name) return null;
874
- if (typeof frontmatter?.["description"] !== "string") return null;
875
- 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;
876
1189
  const files = filesUnder(root);
877
1190
  if (files === null) return null;
878
1191
  let total = 0;
879
1192
  const resources = files.map((path) => {
880
1193
  const fact = facts(path);
881
1194
  total += fact.size;
882
- const file = relative3(root, path).split(sep2).join("/");
883
- 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
+ };
884
1201
  });
885
1202
  if (total > MAX_SKILL_BYTES) return null;
886
- return { uri: skillUri(entry), frontmatter, resources };
1203
+ return { uri: skillFileUri(entry), frontmatter, resources };
887
1204
  }
888
- var MIME = {
889
- ".md": "text/markdown",
890
- ".json": "application/json",
891
- ".txt": "text/plain",
892
- ".py": "text/x-python",
893
- ".js": "text/javascript",
894
- ".mjs": "text/javascript",
895
- ".ts": "text/typescript",
896
- ".sh": "text/x-shellscript",
897
- ".yaml": "text/yaml",
898
- ".yml": "text/yaml",
899
- ".toml": "text/plain"
900
- };
901
- var mimeOf = (path) => {
902
- const dot = path.lastIndexOf(".");
903
- return dot === -1 ? void 0 : MIME[path.slice(dot).toLowerCase()];
904
- };
905
- 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) {
906
1216
  let memo = null;
907
- const entries = () => {
1217
+ return () => {
908
1218
  const source = skills();
909
1219
  if (memo?.source === source) return memo.entries;
910
1220
  const map = /* @__PURE__ */ new Map();
911
1221
  for (const entry of source) {
912
- let served = null;
913
1222
  try {
914
- served = extensionEntry(entry);
1223
+ const served = extensionEntry(entry);
1224
+ if (served) map.set(served.uri, served);
915
1225
  } catch {
916
1226
  }
917
- if (served) map.set(served.uri, served);
918
1227
  }
919
1228
  memo = { source, entries: map };
920
1229
  return map;
921
1230
  };
922
- const pathOf = /* @__PURE__ */ new Map();
923
- const locate = (uri) => {
924
- if (pathOf.size === 0) {
925
- for (const entry of skills()) {
926
- if (kindOf(entry) === "skill" && basename4(entry.path) === "SKILL.md") {
927
- pathOf.set(skillUri(entry), dirname4(entry.path));
928
- }
929
- }
930
- }
931
- for (const [skill, root] of pathOf) {
932
- const prefix = skill.slice(0, -"SKILL.md".length);
933
- if (uri.startsWith(prefix)) return join4(root, uri.slice(prefix.length));
934
- }
935
- 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) } : {}
936
1241
  };
1242
+ }
1243
+ function registerSkillsExtension(server, skills, options = {}) {
1244
+ const entries = entriesOf(skills);
1245
+ const cache = { ttlMs: TTL_MS, cacheScope: options.cacheScope ?? "public" };
937
1246
  server.server.registerCapabilities({
938
1247
  extensions: { [SKILLS_EXTENSION]: {} }
939
1248
  });
940
1249
  server.server.setRequestHandler(
941
1250
  "skills/list",
942
- { params: z.object({ cursor: z.string().optional() }).passthrough() },
943
- async (params) => {
944
- const all = [...entries().values()];
945
- const start = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
946
- if (!Number.isInteger(start) || start < 0 || start > all.length) {
947
- throw new ProtocolError(
948
- ProtocolErrorCode.InvalidParams,
949
- "invalid cursor"
950
- );
951
- }
952
- const end = start + PAGE_SIZE;
953
- return {
954
- resultType: "complete",
955
- skills: all.slice(start, end),
956
- ...end < all.length ? { nextCursor: String(end) } : {},
957
- ttlMs: LIST_TTL_MS,
958
- cacheScope: "public"
959
- };
960
- }
1251
+ { params: z2.object({ cursor: z2.string().optional() }).passthrough() },
1252
+ async (params) => ({
1253
+ resultType: "complete",
1254
+ ...page([...entries().values()], params.cursor),
1255
+ ...cache
1256
+ })
961
1257
  );
962
1258
  server.server.setRequestHandler(
963
1259
  "skills/get",
964
- { params: z.object({ uri: z.string() }).passthrough() },
1260
+ { params: z2.object({ uri: z2.string() }).passthrough() },
965
1261
  async (params) => {
966
1262
  const skill = entries().get(params.uri);
967
1263
  if (!skill) {
@@ -970,39 +1266,104 @@ function registerSkillsExtension(server, skills) {
970
1266
  `not a skill this server serves: ${params.uri}`
971
1267
  );
972
1268
  }
973
- return { resultType: "complete", skill };
974
- }
975
- );
976
- server.registerResource(
977
- "skill-files",
978
- new ResourceTemplate(`skill://${AUTHORITY}/{+path}`, { list: void 0 }),
979
- {
980
- title: "Skill files",
981
- description: "Files of the skills listed by skills/list."
982
- },
983
- async (uri) => {
984
- const href = uri.toString();
985
- const listed = [...entries().values()].some(
986
- (skill) => skill.resources.some((resource) => resource.uri === href)
987
- );
988
- const path = listed ? locate(href) : void 0;
989
- if (path === void 0) throw new ResourceNotFoundError(href);
990
- const bytes = readFileSync4(path);
991
- const mimeType = mimeOf(path);
992
- const text = mimeType !== void 0;
993
- return {
994
- contents: [
995
- text ? { uri: href, mimeType, text: bytes.toString("utf8") } : { uri: href, blob: bytes.toString("base64") }
996
- ]
997
- };
1269
+ return { resultType: "complete", skill, ...cache };
998
1270
  }
999
1271
  );
1000
1272
  }
1001
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
+
1002
1330
  // src/mcp.ts
1003
1331
  import { McpServer } from "@modelcontextprotocol/server";
1004
- import { z as z2 } from "zod";
1005
- 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";
1006
1367
  var readOnly = {
1007
1368
  readOnlyHint: true,
1008
1369
  destructiveHint: false,
@@ -1013,130 +1374,212 @@ var readOnlyTool = (title) => ({
1013
1374
  title,
1014
1375
  annotations: { ...readOnly, title }
1015
1376
  });
1016
- var kindInput = z2.enum(["skill", "agent", "hook", "mcp", "any"]).optional().describe(
1377
+ var kindInput = z4.enum(["skill", "agent", "hook", "mcp", "any"]).optional().describe(
1017
1378
  "skill (default), agent, hook, mcp, or any. Commands are skills; agents, hooks and MCP servers are installed with pmcp add rather than read."
1018
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.");
1019
1382
  var asText = (value) => ({
1020
1383
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
1021
1384
  });
1022
- 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) {
1023
1400
  server.registerTool(
1024
- "skill_catalog",
1401
+ "skill_bundle",
1025
1402
  {
1026
- ...readOnlyTool("List skills"),
1027
- 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.",
1028
- inputSchema: z2.object({
1029
- page: z2.number().int().min(1).optional(),
1030
- pageSize: z2.number().int().min(1).max(20).optional(),
1031
- tier: z2.enum(["open", "free", "paid", "internal"]).optional(),
1032
- kind: kindInput
1033
- })
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
1034
1407
  },
1035
- 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)})`
1036
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) {
1037
1437
  server.registerTool(
1038
- "skill_find",
1438
+ "skill_call",
1039
1439
  {
1040
- ...readOnlyTool("Find skills"),
1041
- 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.",
1042
- inputSchema: z2.object({
1043
- intent: z2.string().min(1).max(256).describe("What you are trying to do, as an English sentence."),
1044
- limit: z2.number().int().min(1).max(25).optional(),
1045
- filters: z2.object({
1046
- kind: kindInput,
1047
- tier: z2.enum(["open", "free", "paid", "internal"]).optional(),
1048
- package: z2.string().optional(),
1049
- level: z2.string().optional(),
1050
- domain: z2.string().optional(),
1051
- medium: z2.string().optional()
1052
- }).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>.")
1053
1444
  })
1054
1445
  },
1055
- 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))
1056
1479
  );
1480
+ }
1481
+
1482
+ // src/tools/describe.ts
1483
+ import { z as z8 } from "zod";
1484
+ function registerDescribeTool(server, tools) {
1057
1485
  server.registerTool(
1058
1486
  "skill_describe",
1059
1487
  {
1060
1488
  ...readOnlyTool("Describe a skill"),
1061
- 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.",
1062
- inputSchema: z2.object({
1063
- name: z2.string().min(1).describe("The skill name skill_find or skill_catalog returned.")
1064
- })
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
1065
1492
  },
1066
1493
  async ({ name }) => {
1067
1494
  const described = tools.describe(name);
1068
- if (!described) {
1069
- return {
1070
- ...asText({
1071
- error: "unknown_skill",
1072
- name,
1073
- hint: "Call skill_catalog for the names that exist."
1074
- }),
1075
- isError: true
1076
- };
1077
- }
1078
- return asText(described);
1495
+ return described ? structured(described) : unknownSkill(name);
1079
1496
  }
1080
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) {
1081
1529
  server.registerTool(
1082
1530
  "skill_read",
1083
1531
  {
1084
1532
  ...readOnlyTool("Read a skill file"),
1085
- 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_describe lists it (layout: https://pmcp.build). Read only when the skill body points at the file. Scripts are returned, never run. Files over 256 KiB and paths outside the skill are refused.",
1086
- inputSchema: z2.object({
1087
- name: z2.string().min(1),
1088
- path: z2.string().min(1).optional().describe(
1089
- "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."
1090
1538
  )
1091
1539
  })
1092
1540
  },
1093
1541
  async ({ name, path }) => {
1094
1542
  const result = tools.read(name, path);
1095
1543
  if (!result.ok) return { ...asText(result), isError: true };
1096
- const { text, ...header } = result;
1544
+ const { text: _text, blob: _blob, ...header } = result;
1097
1545
  return {
1098
1546
  content: [
1099
1547
  { type: "text", text: JSON.stringify(header) },
1100
- { type: "text", text }
1548
+ fileBlock(result)
1101
1549
  ]
1102
1550
  };
1103
1551
  }
1104
1552
  );
1105
- server.registerTool(
1106
- "skill_call",
1107
- {
1108
- ...readOnlyTool("Read a skill"),
1109
- description: "The body of a single skill, by the name skill_find or skill_catalog returned. This is the only call that costs a document.",
1110
- inputSchema: z2.object({
1111
- name: z2.string().min(1).describe("The skill name, as <package>/<slug>.")
1112
- })
1113
- },
1114
- async ({ name }) => {
1115
- const found = tools.call(name);
1116
- if (!found) {
1117
- return {
1118
- ...asText({
1119
- error: "unknown_skill",
1120
- name,
1121
- hint: "Call skill_catalog for the names that exist."
1122
- }),
1123
- isError: true
1124
- };
1125
- }
1126
- return { content: [{ type: "text", text: found.body }] };
1127
- }
1128
- );
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);
1129
1565
  return server;
1130
1566
  }
1131
1567
  function createSkillServer(options) {
1132
- const server = new McpServer({ name: SERVER_NAME, version: "0.7.0" });
1568
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
1133
1569
  const tools = createSkillTools(options);
1134
- registerSkillsExtension(server, tools.skills);
1570
+ registerSkillsExtension(server, tools.skills, options.skillsExtension);
1571
+ registerSkillResources(server, tools.skills);
1572
+ if (options.prompts) registerSkillPrompts(server, tools);
1135
1573
  return registerSkillTools(server, tools);
1136
1574
  }
1137
1575
 
1138
1576
  export {
1139
1577
  normaliseIntent,
1578
+ frontmatterObject,
1579
+ NAME_MAX,
1580
+ DESCRIPTION_MAX,
1581
+ COMPATIBILITY_MAX,
1582
+ specProblems,
1140
1583
  readMetadata,
1141
1584
  readFrontmatterText,
1142
1585
  readMarketplace,
@@ -1150,21 +1593,36 @@ export {
1150
1593
  readCatalog,
1151
1594
  withoutBody,
1152
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,
1153
1607
  DEFAULT_FIND_LIMIT,
1154
1608
  lexicalScore,
1155
1609
  cosine,
1156
1610
  find,
1157
- READ_BUDGET_BYTES,
1611
+ bundleSkill,
1158
1612
  DESCRIBE_FILE_LIMIT,
1159
1613
  CATALOG_PAGE_SIZE,
1160
1614
  CATALOG_PAGE_MAX,
1161
1615
  createSkillTools,
1162
- SKILLS_EXTENSION,
1163
- skillUri,
1164
1616
  extensionEntry,
1617
+ SKILLS_EXTENSION,
1165
1618
  registerSkillsExtension,
1619
+ catalogResponseSchema,
1620
+ findResultSchema,
1621
+ describeResponseSchema,
1622
+ bundleResponseSchema,
1166
1623
  SERVER_NAME,
1624
+ SERVER_VERSION,
1167
1625
  registerSkillTools,
1168
1626
  createSkillServer
1169
1627
  };
1170
- //# sourceMappingURL=chunk-QHFORYIN.js.map
1628
+ //# sourceMappingURL=chunk-ZDV75HZN.js.map