@modootoday/pmcp 0.4.0 → 0.5.1

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.
@@ -5,11 +5,13 @@ function normaliseIntent(intent) {
5
5
 
6
6
  // src/marketplace.ts
7
7
  import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
8
- import { join as join2, resolve } from "path";
8
+ import { basename as basename2, dirname as dirname2, join as join2, resolve } from "path";
9
9
 
10
10
  // src/catalog.ts
11
11
  import { existsSync, readFileSync, readdirSync } from "fs";
12
12
  import { basename, dirname, isAbsolute, join, relative } from "path";
13
+ var ASSET_KINDS = ["skill", "agent", "hook", "mcp"];
14
+ var kindOf = (entry) => entry.kind ?? "skill";
13
15
  var SKIPPED_DIRS = /* @__PURE__ */ new Set([
14
16
  "node_modules",
15
17
  ".git",
@@ -295,6 +297,151 @@ var asList = (value) => {
295
297
  if (value === void 0) return [];
296
298
  return typeof value === "string" ? [value] : value;
297
299
  };
300
+ function readJson(path) {
301
+ try {
302
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
303
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
304
+ } catch {
305
+ return null;
306
+ }
307
+ }
308
+ function markdownEntry(context, path, fallbackSlug, shape) {
309
+ const source = readFileSync2(path, "utf8");
310
+ const front = readFrontmatter(source);
311
+ const description = front["description"] ?? "";
312
+ if (description === "") {
313
+ context.onReject?.({ path, reason: "no description" });
314
+ return null;
315
+ }
316
+ const metadata = readMetadata(source);
317
+ const ownTier = asString(metadata["tier"]);
318
+ if (context.tier !== void 0 && ownTier !== void 0 && ownTier !== context.tier) {
319
+ context.onReject?.({
320
+ path,
321
+ reason: `metadata.tier ${ownTier} in a ${context.tier} marketplace`
322
+ });
323
+ return null;
324
+ }
325
+ const slug = front["name"] ?? fallbackSlug;
326
+ const prefix = `${context.marketplace}/${context.plugin}`;
327
+ return {
328
+ name: shape.namespace ? `${prefix}/${shape.namespace}/${slug}` : `${prefix}/${slug}`,
329
+ package: prefix,
330
+ slug,
331
+ description,
332
+ path,
333
+ tier: ownTier ?? context.tier,
334
+ metadata,
335
+ keywords: asList(metadata["keywords"]),
336
+ ...shape.kind ? { kind: shape.kind } : {},
337
+ ...shape.root ? { root: shape.root } : {}
338
+ };
339
+ }
340
+ function skillEntries(context) {
341
+ const dir = join2(context.pluginDir, "skills");
342
+ if (!existsSync2(dir)) return [];
343
+ const found = [];
344
+ for (const child of readdirSync2(dir, { withFileTypes: true })) {
345
+ if (!child.isDirectory()) continue;
346
+ const path = join2(dir, child.name, "SKILL.md");
347
+ if (!existsSync2(path)) continue;
348
+ const entry = markdownEntry(context, path, child.name, {});
349
+ if (entry) found.push(entry);
350
+ }
351
+ return found;
352
+ }
353
+ function commandEntries(context) {
354
+ const dir = join2(context.pluginDir, "commands");
355
+ if (!existsSync2(dir)) return [];
356
+ const found = [];
357
+ for (const child of readdirSync2(dir, { withFileTypes: true })) {
358
+ if (!child.isFile() || !child.name.endsWith(".md")) continue;
359
+ const path = join2(dir, child.name);
360
+ const entry = markdownEntry(context, path, basename2(child.name, ".md"), {
361
+ root: path
362
+ });
363
+ if (entry) found.push(entry);
364
+ }
365
+ return found;
366
+ }
367
+ function agentEntries(context) {
368
+ const dir = join2(context.pluginDir, "agents");
369
+ if (!existsSync2(dir)) return [];
370
+ const found = [];
371
+ for (const child of readdirSync2(dir, { withFileTypes: true })) {
372
+ let entry = null;
373
+ if (child.isFile() && child.name.endsWith(".md")) {
374
+ const path = join2(dir, child.name);
375
+ entry = markdownEntry(context, path, basename2(child.name, ".md"), {
376
+ kind: "agent",
377
+ root: path,
378
+ namespace: "agents"
379
+ });
380
+ } else if (child.isDirectory() && existsSync2(join2(dir, child.name, "agent.md"))) {
381
+ entry = markdownEntry(
382
+ context,
383
+ join2(dir, child.name, "agent.md"),
384
+ child.name,
385
+ {
386
+ kind: "agent",
387
+ namespace: "agents"
388
+ }
389
+ );
390
+ }
391
+ if (entry) found.push(entry);
392
+ }
393
+ return found;
394
+ }
395
+ function hookEntries(context, manifest) {
396
+ const declared = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
397
+ const path = resolve(context.pluginDir, declared ?? "hooks/hooks.json");
398
+ if (!existsSync2(path)) return [];
399
+ const hooks = readJson(path);
400
+ if (hooks === null) {
401
+ context.onReject?.({ path, reason: "hooks.json is not a JSON object" });
402
+ return [];
403
+ }
404
+ const own = typeof hooks["description"] === "string" ? hooks["description"] : void 0;
405
+ const description = own ?? context.pluginDescription ?? "";
406
+ if (description === "") {
407
+ context.onReject?.({ path, reason: "no description" });
408
+ return [];
409
+ }
410
+ const prefix = `${context.marketplace}/${context.plugin}`;
411
+ return [
412
+ {
413
+ name: `${prefix}/hooks`,
414
+ package: prefix,
415
+ slug: "hooks",
416
+ description,
417
+ path,
418
+ tier: context.tier,
419
+ kind: "hook",
420
+ root: context.pluginDir
421
+ }
422
+ ];
423
+ }
424
+ function mcpEntries(context) {
425
+ const path = join2(context.pluginDir, ".mcp.json");
426
+ if (!existsSync2(path)) return [];
427
+ const config = readJson(path);
428
+ const servers = config?.["mcpServers"];
429
+ if (servers === null || typeof servers !== "object" || Array.isArray(servers)) {
430
+ context.onReject?.({ path, reason: ".mcp.json has no mcpServers object" });
431
+ return [];
432
+ }
433
+ const prefix = `${context.marketplace}/${context.plugin}`;
434
+ return Object.keys(servers).map((server) => ({
435
+ name: `${prefix}/mcp/${server}`,
436
+ package: prefix,
437
+ slug: server,
438
+ description: context.pluginDescription ? `MCP server ${server}. ${context.pluginDescription}` : `MCP server ${server} from plugin ${context.plugin}.`,
439
+ path,
440
+ tier: context.tier,
441
+ kind: "mcp",
442
+ root: path
443
+ }));
444
+ }
298
445
  function readMarketplace(dir, onReject) {
299
446
  const manifestPath = join2(dir, ".claude-plugin", "marketplace.json");
300
447
  if (!existsSync2(manifestPath)) {
@@ -317,47 +464,33 @@ function readMarketplace(dir, onReject) {
317
464
  return [];
318
465
  }
319
466
  const marketplace = manifest.name ?? "marketplace";
320
- const tier = manifest.metadata?.tier;
321
467
  const found = [];
322
468
  for (const plugin of manifest.plugins ?? []) {
323
469
  if (!plugin.name || !plugin.source) continue;
324
- const skillsDir = join2(resolve(dir, plugin.source), "skills");
325
- if (!existsSync2(skillsDir)) continue;
326
- for (const child of readdirSync2(skillsDir, { withFileTypes: true })) {
327
- if (!child.isDirectory()) continue;
328
- const path = join2(skillsDir, child.name, "SKILL.md");
329
- if (!existsSync2(path)) continue;
330
- const source = readFileSync2(path, "utf8");
331
- const front = readFrontmatter(source);
332
- const description = front["description"] ?? "";
333
- if (description === "") {
334
- onReject?.({ path, reason: "no description" });
335
- continue;
336
- }
337
- const metadata = readMetadata(source);
338
- const ownTier = asString(metadata["tier"]);
339
- if (tier !== void 0 && ownTier !== void 0 && ownTier !== tier) {
340
- onReject?.({
341
- path,
342
- reason: `metadata.tier ${ownTier} in a ${tier} marketplace`
343
- });
344
- continue;
345
- }
346
- const slug = front["name"] ?? child.name;
347
- found.push({
348
- name: `${marketplace}/${plugin.name}/${slug}`,
349
- package: `${marketplace}/${plugin.name}`,
350
- slug,
351
- description,
352
- path,
353
- tier: ownTier ?? tier,
354
- metadata,
355
- keywords: asList(metadata["keywords"])
356
- });
357
- }
470
+ const pluginDir = resolve(dir, plugin.source);
471
+ if (!existsSync2(pluginDir)) continue;
472
+ const pluginManifest = readJson(
473
+ join2(pluginDir, ".claude-plugin", "plugin.json")
474
+ ) ?? {};
475
+ const context = {
476
+ marketplace,
477
+ plugin: plugin.name,
478
+ pluginDir,
479
+ pluginDescription: typeof pluginManifest.description === "string" ? pluginManifest.description : void 0,
480
+ tier: manifest.metadata?.tier,
481
+ onReject
482
+ };
483
+ found.push(
484
+ ...skillEntries(context),
485
+ ...commandEntries(context),
486
+ ...agentEntries(context),
487
+ ...hookEntries(context, pluginManifest),
488
+ ...mcpEntries(context)
489
+ );
358
490
  }
359
491
  return found;
360
492
  }
493
+ var assetRoot = (entry) => entry.root ?? dirname2(entry.path);
361
494
 
362
495
  // src/find.ts
363
496
  var DEFAULT_FIND_LIMIT = 5;
@@ -445,12 +578,22 @@ function rank(matches2, limit) {
445
578
  // src/server.ts
446
579
  import { createHash } from "crypto";
447
580
  import { readFileSync as readFileSync3, readdirSync as readdirSync3, realpathSync, statSync } from "fs";
448
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve2, sep } from "path";
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";
449
590
  var READ_BUDGET_BYTES = 256 * 1024;
450
591
  var DESCRIBE_FILE_LIMIT = 200;
451
592
  var CATALOG_PAGE_SIZE = 8;
452
593
  var CATALOG_PAGE_MAX = 20;
453
594
  function matches(entry, filters) {
595
+ const kind = filters?.kind ?? "skill";
596
+ if (kind !== "any" && kindOf(entry) !== kind) return false;
454
597
  if (!filters) return true;
455
598
  const meta = entry.metadata ?? {};
456
599
  const field = (key) => {
@@ -471,6 +614,15 @@ function matches(entry, filters) {
471
614
  var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
472
615
  function listFiles(root) {
473
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
+ }
474
626
  const stack = [root];
475
627
  while (stack.length > 0) {
476
628
  const dir = stack.pop();
@@ -495,6 +647,14 @@ function listFiles(root) {
495
647
  files.sort((a, b) => a.path.localeCompare(b.path));
496
648
  return { files, truncated: false };
497
649
  }
650
+ function catalogItem(entry) {
651
+ return {
652
+ name: entry.name,
653
+ description: entry.description,
654
+ ...entry.tier === void 0 ? {} : { tier: entry.tier },
655
+ ...kindOf(entry) === "skill" ? {} : { kind: kindOf(entry) }
656
+ };
657
+ }
498
658
  function createSkillTools(options) {
499
659
  const load = options.loadCatalog ?? readCatalog;
500
660
  let entries = null;
@@ -518,16 +678,11 @@ function createSkillTools(options) {
518
678
  const byPackage = /* @__PURE__ */ new Map();
519
679
  let count = 0;
520
680
  for (const entry of catalog()) {
521
- if (!matches(entry, { tier: request.tier })) continue;
681
+ if (!matches(entry, { tier: request.tier, kind: request.kind }))
682
+ continue;
522
683
  count += 1;
523
684
  const list = byPackage.get(entry.package) ?? [];
524
- list.push(
525
- entry.tier === void 0 ? { name: entry.name, description: entry.description } : {
526
- name: entry.name,
527
- description: entry.description,
528
- tier: entry.tier
529
- }
530
- );
685
+ list.push(catalogItem(entry));
531
686
  byPackage.set(entry.package, list);
532
687
  }
533
688
  const groups = [...byPackage.entries()].sort(
@@ -575,9 +730,10 @@ function createSkillTools(options) {
575
730
  const entry = catalog().find((candidate) => candidate.name === name);
576
731
  if (!entry) return null;
577
732
  const source = readFileSync3(entry.path, "utf8");
578
- const listing = listFiles(dirname2(entry.path));
733
+ const listing = listFiles(assetRoot(entry));
579
734
  return {
580
735
  name: entry.name,
736
+ kind: kindOf(entry),
581
737
  package: entry.package,
582
738
  description: entry.description,
583
739
  tier: entry.tier,
@@ -591,21 +747,27 @@ function createSkillTools(options) {
591
747
  * One file inside a skill's directory. Paths that leave the directory,
592
748
  * directly or through a link, are refused; nothing is ever executed.
593
749
  */
594
- read(name, path = "SKILL.md") {
750
+ read(name, path) {
595
751
  const entry = catalog().find((candidate) => candidate.name === name);
596
752
  if (!entry) return { ok: false, error: "unknown_skill", detail: name };
597
- if (isAbsolute2(path) || path.split(/[\\/]/u).includes("..")) {
598
- return { ok: false, error: "path_outside_skill", detail: path };
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 };
599
756
  }
600
- const root = realpathSync(dirname2(entry.path));
757
+ const scope = realpathSync(assetRoot(entry));
758
+ const single = statSync(scope).isFile();
759
+ const root = single ? dirname3(scope) : scope;
601
760
  let target;
602
761
  try {
603
- target = realpathSync(resolve2(root, path));
762
+ target = realpathSync(
763
+ single && wanted === "" ? scope : resolve2(root, wanted)
764
+ );
604
765
  } catch {
605
- return { ok: false, error: "no_such_file", detail: path };
766
+ return { ok: false, error: "no_such_file", detail: wanted };
606
767
  }
607
- if (target !== root && !target.startsWith(root + sep)) {
608
- return { ok: false, error: "path_outside_skill", detail: path };
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 };
609
771
  }
610
772
  const size = statSync(target).size;
611
773
  if (size > READ_BUDGET_BYTES) {
@@ -638,13 +800,208 @@ function createSkillTools(options) {
638
800
  refresh() {
639
801
  entries = null;
640
802
  },
641
- entries: () => catalog().map(withoutBody)
803
+ entries: () => catalog().map(withoutBody),
804
+ /** The catalog with paths, for the Skills extension. The same array until refresh. */
805
+ skills: () => catalog()
642
806
  };
643
807
  }
644
808
 
809
+ // src/skills-extension.ts
810
+ 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;
824
+ var MAX_RESOURCES = 512;
825
+ var MAX_SKILL_BYTES = 16 * 1024 * 1024;
826
+ var PAGE_SIZE = 50;
827
+ var LIST_TTL_MS = 6e4;
828
+ var digests = /* @__PURE__ */ new Map();
829
+ function facts(path) {
830
+ const stat = statSync2(path);
831
+ const key = `${stat.mtimeMs}:${stat.size}`;
832
+ const cached = digests.get(path);
833
+ if (cached?.key === key) return cached;
834
+ const digest = `sha256:${createHash2("sha256").update(readFileSync4(path)).digest("hex")}`;
835
+ const fresh = { key, digest, size: stat.size };
836
+ digests.set(path, fresh);
837
+ return fresh;
838
+ }
839
+ function filesUnder(root) {
840
+ const found = [];
841
+ const stack = [root];
842
+ while (stack.length > 0) {
843
+ const dir = stack.pop();
844
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
845
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
846
+ const full = join4(dir, entry.name);
847
+ if (entry.isDirectory()) stack.push(full);
848
+ else if (entry.isFile()) found.push(full);
849
+ if (found.length > MAX_RESOURCES) return null;
850
+ }
851
+ }
852
+ return found.sort();
853
+ }
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
+ function extensionEntry(entry) {
867
+ if (kindOf(entry) !== "skill" || basename4(entry.path) !== "SKILL.md")
868
+ 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);
876
+ const files = filesUnder(root);
877
+ if (files === null) return null;
878
+ let total = 0;
879
+ const resources = files.map((path) => {
880
+ const fact = facts(path);
881
+ total += fact.size;
882
+ const file = relative3(root, path).split(sep2).join("/");
883
+ return { uri: skillUri(entry, file), digest: fact.digest, size: fact.size };
884
+ });
885
+ if (total > MAX_SKILL_BYTES) return null;
886
+ return { uri: skillUri(entry), frontmatter, resources };
887
+ }
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) {
906
+ let memo = null;
907
+ const entries = () => {
908
+ const source = skills();
909
+ if (memo?.source === source) return memo.entries;
910
+ const map = /* @__PURE__ */ new Map();
911
+ for (const entry of source) {
912
+ let served = null;
913
+ try {
914
+ served = extensionEntry(entry);
915
+ } catch {
916
+ }
917
+ if (served) map.set(served.uri, served);
918
+ }
919
+ memo = { source, entries: map };
920
+ return map;
921
+ };
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;
936
+ };
937
+ server.server.registerCapabilities({
938
+ extensions: { [SKILLS_EXTENSION]: {} }
939
+ });
940
+ server.server.setRequestHandler(
941
+ "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
+ }
961
+ );
962
+ server.server.setRequestHandler(
963
+ "skills/get",
964
+ { params: z.object({ uri: z.string() }).passthrough() },
965
+ async (params) => {
966
+ const skill = entries().get(params.uri);
967
+ if (!skill) {
968
+ throw new ProtocolError(
969
+ ProtocolErrorCode.InvalidParams,
970
+ `not a skill this server serves: ${params.uri}`
971
+ );
972
+ }
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
+ };
998
+ }
999
+ );
1000
+ }
1001
+
645
1002
  // src/mcp.ts
646
1003
  import { McpServer } from "@modelcontextprotocol/server";
647
- import { z } from "zod";
1004
+ import { z as z2 } from "zod";
648
1005
  var SERVER_NAME = "pmcp";
649
1006
  var readOnly = {
650
1007
  readOnlyHint: true,
@@ -652,6 +1009,13 @@ var readOnly = {
652
1009
  idempotentHint: true,
653
1010
  openWorldHint: false
654
1011
  };
1012
+ var readOnlyTool = (title) => ({
1013
+ title,
1014
+ annotations: { ...readOnly, title }
1015
+ });
1016
+ var kindInput = z2.enum(["skill", "agent", "hook", "mcp", "any"]).optional().describe(
1017
+ "skill (default), agent, hook, mcp, or any. Commands are skills; agents, hooks and MCP servers are installed with pmcp add rather than read."
1018
+ );
655
1019
  var asText = (value) => ({
656
1020
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
657
1021
  });
@@ -659,46 +1023,45 @@ function registerSkillTools(server, tools) {
659
1023
  server.registerTool(
660
1024
  "skill_catalog",
661
1025
  {
662
- title: "List installed skills without their bodies",
663
- 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.",
664
- inputSchema: z.object({
665
- page: z.number().int().min(1).optional(),
666
- pageSize: z.number().int().min(1).max(20).optional(),
667
- tier: z.enum(["open", "free", "paid", "internal"]).optional()
668
- }),
669
- annotations: readOnly
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
+ })
670
1034
  },
671
1035
  async (request) => asText(tools.catalog(request))
672
1036
  );
673
1037
  server.registerTool(
674
1038
  "skill_find",
675
1039
  {
676
- title: "Find skills by describing the problem",
1040
+ ...readOnlyTool("Find skills"),
677
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.",
678
- inputSchema: z.object({
679
- intent: z.string().min(1).max(256).describe("What you are trying to do, as an English sentence."),
680
- limit: z.number().int().min(1).max(25).optional(),
681
- filters: z.object({
682
- tier: z.enum(["open", "free", "paid", "internal"]).optional(),
683
- package: z.string().optional(),
684
- level: z.string().optional(),
685
- domain: z.string().optional(),
686
- medium: z.string().optional()
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()
687
1052
  }).optional().describe("Exact-match filters over the catalog before ranking.")
688
- }),
689
- annotations: readOnly
1053
+ })
690
1054
  },
691
1055
  async ({ intent, limit, filters }) => asText(await tools.find(intent, limit, filters))
692
1056
  );
693
1057
  server.registerTool(
694
1058
  "skill_describe",
695
1059
  {
696
- title: "Check one skill's properties before loading it",
1060
+ ...readOnlyTool("Describe a skill"),
697
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.",
698
- inputSchema: z.object({
699
- name: z.string().min(1).describe("The skill name skill_find or skill_catalog returned.")
700
- }),
701
- annotations: readOnly
1062
+ inputSchema: z2.object({
1063
+ name: z2.string().min(1).describe("The skill name skill_find or skill_catalog returned.")
1064
+ })
702
1065
  },
703
1066
  async ({ name }) => {
704
1067
  const described = tools.describe(name);
@@ -718,15 +1081,14 @@ function registerSkillTools(server, tools) {
718
1081
  server.registerTool(
719
1082
  "skill_read",
720
1083
  {
721
- title: "Read one file a skill carries",
722
- description: "A reference or script file inside one skill's directory, as text, with its sha256. 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.",
723
- inputSchema: z.object({
724
- name: z.string().min(1),
725
- path: z.string().min(1).optional().describe(
1084
+ ...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(
726
1089
  "Path relative to the skill directory, as skill_describe lists it. Defaults to SKILL.md."
727
1090
  )
728
- }),
729
- annotations: readOnly
1091
+ })
730
1092
  },
731
1093
  async ({ name, path }) => {
732
1094
  const result = tools.read(name, path);
@@ -743,12 +1105,11 @@ function registerSkillTools(server, tools) {
743
1105
  server.registerTool(
744
1106
  "skill_call",
745
1107
  {
746
- title: "Read one skill",
1108
+ ...readOnlyTool("Read a skill"),
747
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.",
748
- inputSchema: z.object({
749
- name: z.string().min(1).describe("The skill name, as <package>/<slug>.")
750
- }),
751
- annotations: readOnly
1110
+ inputSchema: z2.object({
1111
+ name: z2.string().min(1).describe("The skill name, as <package>/<slug>.")
1112
+ })
752
1113
  },
753
1114
  async ({ name }) => {
754
1115
  const found = tools.call(name);
@@ -768,8 +1129,10 @@ function registerSkillTools(server, tools) {
768
1129
  return server;
769
1130
  }
770
1131
  function createSkillServer(options) {
771
- const server = new McpServer({ name: SERVER_NAME, version: "0.4.0" });
772
- return registerSkillTools(server, createSkillTools(options));
1132
+ const server = new McpServer({ name: SERVER_NAME, version: "0.5.1" });
1133
+ const tools = createSkillTools(options);
1134
+ registerSkillsExtension(server, tools.skills);
1135
+ return registerSkillTools(server, tools);
773
1136
  }
774
1137
 
775
1138
  export {
@@ -777,6 +1140,9 @@ export {
777
1140
  readMetadata,
778
1141
  readFrontmatterText,
779
1142
  readMarketplace,
1143
+ assetRoot,
1144
+ ASSET_KINDS,
1145
+ kindOf,
780
1146
  SKIPPED_DIRS,
781
1147
  MAX_SKILL_DEPTH,
782
1148
  readFrontmatter,
@@ -793,8 +1159,12 @@ export {
793
1159
  CATALOG_PAGE_SIZE,
794
1160
  CATALOG_PAGE_MAX,
795
1161
  createSkillTools,
1162
+ SKILLS_EXTENSION,
1163
+ skillUri,
1164
+ extensionEntry,
1165
+ registerSkillsExtension,
796
1166
  SERVER_NAME,
797
1167
  registerSkillTools,
798
1168
  createSkillServer
799
1169
  };
800
- //# sourceMappingURL=chunk-BFET6MFN.js.map
1170
+ //# sourceMappingURL=chunk-76UKXVUE.js.map