@modootoday/pmcp 0.10.0 → 0.12.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.
@@ -1,1774 +0,0 @@
1
- import {
2
- RUNTIMES,
3
- detectRuntime,
4
- registerMailboxTools,
5
- verifiedRuntimesOf
6
- } from "./chunk-XOH2CW6P.js";
7
-
8
- // src/intent.ts
9
- function normaliseIntent(intent) {
10
- return intent.toLowerCase().replaceAll(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/u).filter(Boolean).join(" ");
11
- }
12
-
13
- // src/frontmatter.ts
14
- import { parse } from "yaml";
15
- function frontmatterText(source) {
16
- const text = source.replace(/^/u, "").replace(/\r\n/gu, "\n");
17
- if (!text.startsWith("---\n") && text !== "---") return null;
18
- const end = text.indexOf("\n---", 3);
19
- if (end === -1) return null;
20
- return text.slice(4, end + 1);
21
- }
22
- function bodyOf(source) {
23
- const text = source.replace(/^/u, "").replace(/\r\n/gu, "\n");
24
- if (!text.startsWith("---\n")) return text;
25
- const end = text.indexOf("\n---", 3);
26
- if (end === -1) return text;
27
- return text.slice(end + 4).replace(/^\n+/u, "");
28
- }
29
- var asObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
30
- function frontmatterObject(source) {
31
- const text = frontmatterText(source);
32
- if (text === null) return null;
33
- try {
34
- return asObject(parse(text));
35
- } catch {
36
- const quoted = text.split("\n").map((line) => {
37
- const match = /^([A-Za-z][\w-]*):\s+(.*:\s.*)$/u.exec(line);
38
- if (!match || /^["'|>[{]/u.test(match[2] ?? "")) return line;
39
- return `${match[1]}: ${JSON.stringify(match[2])}`;
40
- }).join("\n");
41
- try {
42
- return asObject(parse(quoted));
43
- } catch {
44
- return null;
45
- }
46
- }
47
- }
48
- function stringFields(front) {
49
- const out = {};
50
- for (const [key, value] of Object.entries(front ?? {})) {
51
- if (typeof value === "string") out[key] = value.trim();
52
- else if (typeof value === "number" || typeof value === "boolean")
53
- out[key] = String(value);
54
- }
55
- return out;
56
- }
57
- function leafOf(value) {
58
- if (typeof value === "string") return value;
59
- if (typeof value === "number" || typeof value === "boolean")
60
- return String(value);
61
- if (Array.isArray(value))
62
- return value.filter((v) => typeof v === "string");
63
- return void 0;
64
- }
65
- function metadataOf(front) {
66
- const out = {};
67
- const meta = asObject(front?.["metadata"]);
68
- for (const [key, value] of Object.entries(meta ?? {})) {
69
- const leaf = leafOf(value);
70
- if (leaf !== void 0) {
71
- out[key] = leaf;
72
- continue;
73
- }
74
- const nested = asObject(value);
75
- if (nested === null) continue;
76
- const map = {};
77
- for (const [inner, innerValue] of Object.entries(nested)) {
78
- const innerLeaf = leafOf(innerValue);
79
- if (innerLeaf !== void 0) map[inner] = innerLeaf;
80
- }
81
- if (Object.keys(map).length > 0) out[key] = map;
82
- }
83
- return out;
84
- }
85
- var SKILL_NAME = /^[a-z0-9](?:[a-z0-9]|-(?!-))*[a-z0-9]$|^[a-z0-9]$/u;
86
- var NAME_MAX = 64;
87
- var DESCRIPTION_MAX = 1024;
88
- var COMPATIBILITY_MAX = 500;
89
- function specProblems(front, directory) {
90
- if (front === null) return ["frontmatter is missing or is not YAML"];
91
- const problems = [];
92
- const name = front["name"];
93
- if (typeof name !== "string" || name === "") {
94
- problems.push("name is required");
95
- } else {
96
- if (name.length > NAME_MAX)
97
- problems.push(`name is longer than ${NAME_MAX} characters`);
98
- if (!SKILL_NAME.test(name))
99
- problems.push(
100
- "name must be lowercase letters, digits and single hyphens, not starting or ending with one"
101
- );
102
- if (name !== directory)
103
- problems.push(`name ${name} does not match its directory ${directory}`);
104
- }
105
- const description = front["description"];
106
- if (typeof description !== "string" || description.trim() === "") {
107
- problems.push("description is required");
108
- } else if (description.length > DESCRIPTION_MAX) {
109
- problems.push(`description is longer than ${DESCRIPTION_MAX} characters`);
110
- }
111
- const compatibility = front["compatibility"];
112
- if (compatibility !== void 0) {
113
- if (typeof compatibility !== "string")
114
- problems.push("compatibility must be a string");
115
- else if (compatibility.length > COMPATIBILITY_MAX)
116
- problems.push(
117
- `compatibility is longer than ${COMPATIBILITY_MAX} characters`
118
- );
119
- }
120
- const license = front["license"];
121
- if (license !== void 0 && typeof license !== "string")
122
- problems.push("license must be a string");
123
- const allowed = front["allowed-tools"];
124
- if (allowed !== void 0 && typeof allowed !== "string")
125
- problems.push("allowed-tools must be a space-separated string");
126
- const metadata = front["metadata"];
127
- if (metadata !== void 0 && asObject(metadata) === null)
128
- problems.push("metadata must be a map");
129
- return problems;
130
- }
131
-
132
- // src/marketplace.ts
133
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
134
- import { basename as basename2, dirname as dirname2, join as join2, resolve } from "path";
135
-
136
- // src/catalog.ts
137
- import { existsSync, readFileSync, readdirSync } from "fs";
138
- import { basename, dirname, isAbsolute, join, relative } from "path";
139
- var ASSET_KINDS = ["skill", "agent", "hook", "mcp"];
140
- var kindOf = (entry) => entry.kind ?? "skill";
141
- var SKIPPED_DIRS = /* @__PURE__ */ new Set([
142
- "node_modules",
143
- ".git",
144
- ".cache",
145
- ".turbo",
146
- "coverage",
147
- "__tests__",
148
- "__fixtures__"
149
- ]);
150
- var MAX_SKILL_DEPTH = 3;
151
- function readFrontmatter(source) {
152
- return stringFields(frontmatterObject(source));
153
- }
154
- function readBody(source) {
155
- return bodyOf(source);
156
- }
157
- function skillFiles(packageDir) {
158
- const found = [];
159
- const stack = [[packageDir, 0]];
160
- while (stack.length > 0) {
161
- const next = stack.pop();
162
- if (next === void 0) break;
163
- const [dir, depth] = next;
164
- let entries;
165
- try {
166
- entries = readdirSync(dir, { withFileTypes: true });
167
- } catch {
168
- continue;
169
- }
170
- for (const entry of entries) {
171
- if (entry.isFile()) {
172
- if (entry.name === "SKILL.md") found.push(join(dir, entry.name));
173
- continue;
174
- }
175
- if (!entry.isDirectory()) continue;
176
- if (SKIPPED_DIRS.has(entry.name)) continue;
177
- if (depth + 1 <= MAX_SKILL_DEPTH)
178
- stack.push([join(dir, entry.name), depth + 1]);
179
- }
180
- }
181
- return found.sort();
182
- }
183
- function packageDirs(root) {
184
- if (!existsSync(root)) return [];
185
- const found = [];
186
- for (const entry of readdirSync(root, { withFileTypes: true })) {
187
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
188
- if (entry.name === ".bin" || entry.name === ".cache") continue;
189
- const path = join(root, entry.name);
190
- if (entry.name.startsWith("@")) {
191
- if (!existsSync(path)) continue;
192
- for (const scoped of readdirSync(path, { withFileTypes: true })) {
193
- if (scoped.isDirectory() || scoped.isSymbolicLink()) {
194
- found.push(join(path, scoped.name));
195
- }
196
- }
197
- continue;
198
- }
199
- found.push(path);
200
- }
201
- return found;
202
- }
203
- function readManifest(dir) {
204
- try {
205
- const parsed = JSON.parse(
206
- readFileSync(join(dir, "package.json"), "utf8")
207
- );
208
- return parsed !== null && typeof parsed === "object" ? parsed : null;
209
- } catch {
210
- return null;
211
- }
212
- }
213
- function workspacePatterns(manifest2) {
214
- const field = manifest2["workspaces"];
215
- const list = Array.isArray(field) ? field : field?.packages;
216
- if (!Array.isArray(list)) return [];
217
- return list.filter((p) => typeof p === "string");
218
- }
219
- function expandBraces(pattern) {
220
- const match = /\{([^{}]*)\}/u.exec(pattern);
221
- if (!match) return [pattern];
222
- const head = pattern.slice(0, match.index);
223
- const tail = pattern.slice(match.index + match[0].length);
224
- return match[1].split(",").flatMap((option) => expandBraces(`${head}${option}${tail}`));
225
- }
226
- function segmentMatcher(segment) {
227
- const body = segment.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/gu, "\\$&")).join("[^/]*");
228
- return new RegExp(`^${body}$`, "u");
229
- }
230
- function globDirs(base, pattern) {
231
- const segments = pattern.split("/").filter((s) => s !== "" && s !== ".");
232
- const found = /* @__PURE__ */ new Set();
233
- const walk = (dir, index) => {
234
- if (index === segments.length) {
235
- found.add(dir);
236
- return;
237
- }
238
- const segment = segments[index];
239
- if (!segment.includes("*")) {
240
- const next = join(dir, segment);
241
- if (existsSync(next)) walk(next, index + 1);
242
- return;
243
- }
244
- let entries;
245
- try {
246
- entries = readdirSync(dir, { withFileTypes: true });
247
- } catch {
248
- return;
249
- }
250
- const children = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).filter((e) => !SKIPPED_DIRS.has(e.name));
251
- if (segment === "**") {
252
- walk(dir, index + 1);
253
- for (const child of children) walk(join(dir, child.name), index);
254
- return;
255
- }
256
- const matcher = segmentMatcher(segment);
257
- for (const child of children) {
258
- if (matcher.test(child.name)) walk(join(dir, child.name), index + 1);
259
- }
260
- };
261
- walk(base, 0);
262
- return [...found];
263
- }
264
- function workspacePackageDirs(root) {
265
- const ordered = [];
266
- const visited = /* @__PURE__ */ new Set();
267
- const expand = (dir) => {
268
- if (visited.has(dir)) return;
269
- visited.add(dir);
270
- const manifest2 = readManifest(dir);
271
- if (manifest2 === null) return;
272
- const included = /* @__PURE__ */ new Set();
273
- const excluded = /* @__PURE__ */ new Set();
274
- for (const raw of workspacePatterns(manifest2)) {
275
- const negated = raw.startsWith("!");
276
- const pattern = negated ? raw.slice(1) : raw;
277
- for (const alternative of expandBraces(pattern)) {
278
- for (const match of globDirs(dir, alternative)) {
279
- (negated ? excluded : included).add(match);
280
- }
281
- }
282
- }
283
- const members = [...included].filter((m) => !excluded.has(m) && existsSync(join(m, "package.json"))).sort();
284
- for (const member of members) expand(member);
285
- ordered.push(dir);
286
- };
287
- expand(root);
288
- return ordered;
289
- }
290
- function nameOf(dir, named) {
291
- const manifestName = readManifest(dir)?.["name"];
292
- if (typeof manifestName === "string" && manifestName !== "") {
293
- return manifestName;
294
- }
295
- if (named) return basename(dir);
296
- return "";
297
- }
298
- function expandRootGlob(dir) {
299
- if (!dir.includes("*")) return [dir];
300
- const absolute = isAbsolute(dir);
301
- const base = absolute ? "/" : ".";
302
- return globDirs(base, dir).filter((d) => existsSync(join(d, "package.json"))).sort();
303
- }
304
- function readCatalog(options) {
305
- const scopes = options.scopes ?? [];
306
- const found = [];
307
- const seen = /* @__PURE__ */ new Set();
308
- const seenPaths = /* @__PURE__ */ new Set();
309
- const direct = new Set(options.packages ?? []);
310
- const packageDirsToRead = [
311
- ...options.roots.flatMap(packageDirs),
312
- ...direct,
313
- ...(options.workspaces ?? []).flatMap(expandRootGlob).flatMap(workspacePackageDirs)
314
- ];
315
- for (const packageDir of packageDirsToRead) {
316
- const packageName = nameOf(packageDir, direct.has(packageDir));
317
- if (packageName === "") continue;
318
- if (scopes.length > 0 && !scopes.some((s) => packageName.startsWith(s))) {
319
- continue;
320
- }
321
- for (const path of skillFiles(packageDir)) {
322
- if (seenPaths.has(path)) continue;
323
- seenPaths.add(path);
324
- let source;
325
- try {
326
- source = readFileSync(path, "utf8");
327
- } catch {
328
- continue;
329
- }
330
- const parsed = frontmatterObject(source);
331
- const front = stringFields(parsed);
332
- const description = front["description"] ?? "";
333
- if (description === "") continue;
334
- const dirName = basename(dirname(path));
335
- const slug = front["name"] ?? (dirName === basename(packageDir) ? packageName : dirName);
336
- const name = `${packageName}/${slug}`;
337
- if (seen.has(name)) {
338
- if (!path.includes("/node_modules/"))
339
- options.onWarning?.({
340
- path,
341
- reason: `duplicate name ${name}; the first one found is served`
342
- });
343
- continue;
344
- }
345
- seen.add(name);
346
- for (const problem of specProblems(parsed, dirName))
347
- options.onWarning?.({ path, reason: problem });
348
- const metadata = metadataOf(parsed);
349
- found.push({
350
- name,
351
- package: packageName,
352
- slug,
353
- description,
354
- path,
355
- ...Object.keys(metadata).length > 0 ? { metadata } : {}
356
- });
357
- }
358
- }
359
- for (const dir of options.marketplaces ?? []) {
360
- for (const entry of readMarketplace(dir, options.onReject)) {
361
- if (seen.has(entry.name)) {
362
- options.onReject?.({
363
- path: entry.path,
364
- reason: `duplicate name ${entry.name}`
365
- });
366
- continue;
367
- }
368
- seen.add(entry.name);
369
- if (kindOf(entry) === "skill" && basename(entry.path) === "SKILL.md") {
370
- const parsed = frontmatterObject(readFileSync(entry.path, "utf8"));
371
- for (const problem of specProblems(
372
- parsed,
373
- basename(dirname(entry.path))
374
- ))
375
- options.onWarning?.({ path: entry.path, reason: problem });
376
- }
377
- found.push(entry);
378
- }
379
- }
380
- return found.sort((a, b) => a.name.localeCompare(b.name));
381
- }
382
- function withoutBody({
383
- path,
384
- ...rest
385
- }) {
386
- void path;
387
- return rest;
388
- }
389
- function displayPath(root, entry) {
390
- return relative(root, entry.path).replaceAll("\\", "/");
391
- }
392
-
393
- // src/marketplace.ts
394
- function readMetadata(source) {
395
- return metadataOf(frontmatterObject(source));
396
- }
397
- function readFrontmatterText(source) {
398
- return frontmatterText(source) ?? "";
399
- }
400
- var asString = (value) => typeof value === "string" ? value : void 0;
401
- var asList = (value) => {
402
- if (value === void 0) return [];
403
- if (typeof value === "string") return [value];
404
- return Array.isArray(value) ? value : [];
405
- };
406
- function readJson(path) {
407
- try {
408
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
409
- return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
410
- } catch {
411
- return null;
412
- }
413
- }
414
- function markdownEntry(context, path, fallbackSlug, shape) {
415
- const source = readFileSync2(path, "utf8");
416
- const front = readFrontmatter(source);
417
- const description = front["description"] ?? "";
418
- if (description === "") {
419
- context.onReject?.({ path, reason: "no description" });
420
- return null;
421
- }
422
- const metadata = readMetadata(source);
423
- const ownTier = asString(metadata["tier"]);
424
- if (context.tier !== void 0 && ownTier !== void 0 && ownTier !== context.tier) {
425
- context.onReject?.({
426
- path,
427
- reason: `metadata.tier ${ownTier} in a ${context.tier} marketplace`
428
- });
429
- return null;
430
- }
431
- const slug = front["name"] ?? fallbackSlug;
432
- const prefix = `${context.marketplace}/${context.plugin}`;
433
- return {
434
- name: shape.namespace ? `${prefix}/${shape.namespace}/${slug}` : `${prefix}/${slug}`,
435
- package: prefix,
436
- slug,
437
- description,
438
- path,
439
- tier: ownTier ?? context.tier,
440
- metadata,
441
- keywords: asList(metadata["keywords"]),
442
- ...shape.kind ? { kind: shape.kind } : {},
443
- ...shape.root ? { root: shape.root } : {}
444
- };
445
- }
446
- function skillEntries(context) {
447
- const dir = join2(context.pluginDir, "skills");
448
- if (!existsSync2(dir)) return [];
449
- const found = [];
450
- for (const child of readdirSync2(dir, { withFileTypes: true })) {
451
- if (!child.isDirectory()) continue;
452
- const path = join2(dir, child.name, "SKILL.md");
453
- if (!existsSync2(path)) continue;
454
- const entry = markdownEntry(context, path, child.name, {});
455
- if (entry) found.push(entry);
456
- }
457
- return found;
458
- }
459
- function commandEntries(context) {
460
- const dir = join2(context.pluginDir, "commands");
461
- if (!existsSync2(dir)) return [];
462
- const found = [];
463
- for (const child of readdirSync2(dir, { withFileTypes: true })) {
464
- if (!child.isFile() || !child.name.endsWith(".md")) continue;
465
- const path = join2(dir, child.name);
466
- const entry = markdownEntry(context, path, basename2(child.name, ".md"), {
467
- root: path
468
- });
469
- if (entry) found.push(entry);
470
- }
471
- return found;
472
- }
473
- function agentEntries(context) {
474
- const dir = join2(context.pluginDir, "agents");
475
- if (!existsSync2(dir)) return [];
476
- const found = [];
477
- for (const child of readdirSync2(dir, { withFileTypes: true })) {
478
- let entry = null;
479
- if (child.isFile() && child.name.endsWith(".md")) {
480
- const path = join2(dir, child.name);
481
- entry = markdownEntry(context, path, basename2(child.name, ".md"), {
482
- kind: "agent",
483
- root: path,
484
- namespace: "agents"
485
- });
486
- } else if (child.isDirectory() && existsSync2(join2(dir, child.name, "agent.md"))) {
487
- entry = markdownEntry(
488
- context,
489
- join2(dir, child.name, "agent.md"),
490
- child.name,
491
- {
492
- kind: "agent",
493
- namespace: "agents"
494
- }
495
- );
496
- }
497
- if (entry) found.push(entry);
498
- }
499
- return found;
500
- }
501
- function hookEntries(context, manifest2) {
502
- const declared = typeof manifest2.hooks === "string" ? manifest2.hooks : void 0;
503
- const path = resolve(context.pluginDir, declared ?? "hooks/hooks.json");
504
- if (!existsSync2(path)) return [];
505
- const hooks = readJson(path);
506
- if (hooks === null) {
507
- context.onReject?.({ path, reason: "hooks.json is not a JSON object" });
508
- return [];
509
- }
510
- const own = typeof hooks["description"] === "string" ? hooks["description"] : void 0;
511
- const description = own ?? context.pluginDescription ?? "";
512
- if (description === "") {
513
- context.onReject?.({ path, reason: "no description" });
514
- return [];
515
- }
516
- const prefix = `${context.marketplace}/${context.plugin}`;
517
- return [
518
- {
519
- name: `${prefix}/hooks`,
520
- package: prefix,
521
- slug: "hooks",
522
- description,
523
- path,
524
- tier: context.tier,
525
- kind: "hook",
526
- root: context.pluginDir
527
- }
528
- ];
529
- }
530
- function mcpEntries(context) {
531
- const path = join2(context.pluginDir, ".mcp.json");
532
- if (!existsSync2(path)) return [];
533
- const config = readJson(path);
534
- const servers = config?.["mcpServers"];
535
- if (servers === null || typeof servers !== "object" || Array.isArray(servers)) {
536
- context.onReject?.({ path, reason: ".mcp.json has no mcpServers object" });
537
- return [];
538
- }
539
- const prefix = `${context.marketplace}/${context.plugin}`;
540
- return Object.keys(servers).map((server) => ({
541
- name: `${prefix}/mcp/${server}`,
542
- package: prefix,
543
- slug: server,
544
- description: context.pluginDescription ? `MCP server ${server}. ${context.pluginDescription}` : `MCP server ${server} from plugin ${context.plugin}.`,
545
- path,
546
- tier: context.tier,
547
- kind: "mcp",
548
- root: path
549
- }));
550
- }
551
- function readMarketplace(dir, onReject) {
552
- const manifestPath = join2(dir, ".claude-plugin", "marketplace.json");
553
- if (!existsSync2(manifestPath)) {
554
- onReject?.({
555
- path: dir,
556
- reason: "no .claude-plugin/marketplace.json here"
557
- });
558
- return [];
559
- }
560
- let manifest2;
561
- try {
562
- manifest2 = JSON.parse(
563
- readFileSync2(manifestPath, "utf8")
564
- );
565
- } catch {
566
- onReject?.({
567
- path: manifestPath,
568
- reason: "marketplace.json is not valid JSON"
569
- });
570
- return [];
571
- }
572
- const marketplace = manifest2.name ?? "marketplace";
573
- const found = [];
574
- for (const plugin of manifest2.plugins ?? []) {
575
- if (!plugin.name || !plugin.source) continue;
576
- const pluginDir = resolve(dir, plugin.source);
577
- if (!existsSync2(pluginDir)) continue;
578
- const pluginManifest = readJson(
579
- join2(pluginDir, ".claude-plugin", "plugin.json")
580
- ) ?? {};
581
- const context = {
582
- marketplace,
583
- plugin: plugin.name,
584
- pluginDir,
585
- pluginDescription: typeof pluginManifest.description === "string" ? pluginManifest.description : void 0,
586
- tier: manifest2.metadata?.tier,
587
- onReject
588
- };
589
- found.push(
590
- ...skillEntries(context),
591
- ...commandEntries(context),
592
- ...agentEntries(context),
593
- ...hookEntries(context, pluginManifest),
594
- ...mcpEntries(context)
595
- );
596
- }
597
- return found;
598
- }
599
- var assetRoot = (entry) => entry.root ?? dirname2(entry.path);
600
-
601
- // src/prompts.ts
602
- import { z } from "zod";
603
- var promptName = (entry) => entry.name.replace(/^@/u, "").replace(/[^A-Za-z0-9_-]+/gu, "-");
604
- function messageText(tools, name, task) {
605
- const found = tools.call(name);
606
- if (!found) return `The skill ${name} is no longer in this catalog.`;
607
- const files = found.files.map((file) => `- ${file.path}`);
608
- const parts = [found.body];
609
- if (files.length > 0)
610
- parts.push(
611
- `Files in skill ${found.name} (skill_read returns each by path):
612
- ${files.join("\n")}`
613
- );
614
- if (task) parts.push(`Task: ${task}`);
615
- return parts.join("\n\n");
616
- }
617
- function registerSkillPrompts(server, tools) {
618
- const taken = /* @__PURE__ */ new Set();
619
- for (const entry of tools.skills()) {
620
- if (kindOf(entry) !== "skill") continue;
621
- const name = promptName(entry);
622
- if (taken.has(name)) continue;
623
- taken.add(name);
624
- server.registerPrompt(
625
- name,
626
- {
627
- title: entry.name,
628
- description: entry.description,
629
- argsSchema: z.object({
630
- task: z.string().optional().describe("What to apply the skill to.")
631
- })
632
- },
633
- ({ task }) => ({
634
- messages: [
635
- {
636
- role: "user",
637
- content: {
638
- type: "text",
639
- text: messageText(tools, entry.name, task)
640
- }
641
- }
642
- ]
643
- })
644
- );
645
- }
646
- }
647
-
648
- // src/files/list.ts
649
- import { createHash } from "crypto";
650
- import { readFileSync as readFileSync3, readdirSync as readdirSync3, statSync } from "fs";
651
- import { basename as basename4, join as join3, relative as relative2, sep } from "path";
652
-
653
- // src/files/redact.ts
654
- import { basename as basename3 } from "path";
655
- var REDACTED = "<redacted>";
656
- var SECRET_MAPS = /* @__PURE__ */ new Set(["env", "headers"]);
657
- function redactValue(value) {
658
- if (Array.isArray(value)) return value.map(redactValue);
659
- if (value === null || typeof value !== "object") return value;
660
- const out = {};
661
- for (const [key, inner] of Object.entries(value)) {
662
- const isSecretMap = SECRET_MAPS.has(key.toLowerCase()) && inner !== null && typeof inner === "object" && !Array.isArray(inner);
663
- out[key] = isSecretMap ? Object.fromEntries(Object.keys(inner).map((name) => [name, REDACTED])) : redactValue(inner);
664
- }
665
- return out;
666
- }
667
- function redactMcpConfig(text) {
668
- let parsed;
669
- try {
670
- parsed = JSON.parse(text);
671
- } catch {
672
- return `${JSON.stringify({ redacted: REDACTED, reason: "not valid JSON" }, null, 2)}
673
- `;
674
- }
675
- return `${JSON.stringify(redactValue(parsed), null, 2)}
676
- `;
677
- }
678
- var isMcpConfig = (path) => basename3(path) === ".mcp.json";
679
- function servedBytes(path, raw) {
680
- if (!isMcpConfig(path)) return raw;
681
- return Buffer.from(redactMcpConfig(raw.toString("utf8")), "utf8");
682
- }
683
-
684
- // src/files/list.ts
685
- var FILE_LIST_LIMIT = 200;
686
- var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
687
- function describe(path, full) {
688
- const bytes = servedBytes(full, readFileSync3(full));
689
- return { path, bytes: bytes.length, sha256: sha256(bytes) };
690
- }
691
- function listSkillFiles(root, limit = FILE_LIST_LIMIT) {
692
- if (statSync(root).isFile()) {
693
- return { files: [describe(basename4(root), root)], truncated: false };
694
- }
695
- const files = [];
696
- const stack = [root];
697
- while (stack.length > 0) {
698
- const dir = stack.pop();
699
- for (const entry of readdirSync3(dir, { withFileTypes: true })) {
700
- if (entry.name === "node_modules" || entry.name === ".git") continue;
701
- const full = join3(dir, entry.name);
702
- if (entry.isDirectory()) {
703
- stack.push(full);
704
- continue;
705
- }
706
- if (!entry.isFile()) continue;
707
- if (files.length >= limit) return { files, truncated: true };
708
- files.push(describe(relative2(root, full).split(sep).join("/"), full));
709
- }
710
- }
711
- files.sort((a, b) => a.path.localeCompare(b.path));
712
- return { files, truncated: false };
713
- }
714
-
715
- // src/mime.ts
716
- var TYPES = {
717
- ".md": "text/markdown",
718
- ".markdown": "text/markdown",
719
- ".txt": "text/plain",
720
- ".json": "application/json",
721
- ".jsonl": "application/jsonl",
722
- ".yaml": "text/yaml",
723
- ".yml": "text/yaml",
724
- ".toml": "application/toml",
725
- ".csv": "text/csv",
726
- ".tsv": "text/tab-separated-values",
727
- ".xml": "application/xml",
728
- ".html": "text/html",
729
- ".htm": "text/html",
730
- ".css": "text/css",
731
- ".js": "text/javascript",
732
- ".mjs": "text/javascript",
733
- ".cjs": "text/javascript",
734
- ".jsx": "text/javascript",
735
- ".ts": "text/typescript",
736
- ".tsx": "text/typescript",
737
- ".mts": "text/typescript",
738
- ".cts": "text/typescript",
739
- ".py": "text/x-python",
740
- ".rb": "text/x-ruby",
741
- ".go": "text/x-go",
742
- ".rs": "text/x-rust",
743
- ".java": "text/x-java",
744
- ".sh": "text/x-shellscript",
745
- ".bash": "text/x-shellscript",
746
- ".zsh": "text/x-shellscript",
747
- ".ps1": "text/x-powershell",
748
- ".sql": "application/sql",
749
- ".ejs": "text/x-ejs",
750
- ".hbs": "text/x-handlebars-template",
751
- ".mustache": "text/x-mustache",
752
- ".svg": "image/svg+xml",
753
- ".png": "image/png",
754
- ".jpg": "image/jpeg",
755
- ".jpeg": "image/jpeg",
756
- ".gif": "image/gif",
757
- ".webp": "image/webp",
758
- ".avif": "image/avif",
759
- ".ico": "image/x-icon",
760
- ".bmp": "image/bmp",
761
- ".mp3": "audio/mpeg",
762
- ".wav": "audio/wav",
763
- ".ogg": "audio/ogg",
764
- ".m4a": "audio/mp4",
765
- ".flac": "audio/flac",
766
- ".mp4": "video/mp4",
767
- ".webm": "video/webm",
768
- ".pdf": "application/pdf",
769
- ".zip": "application/zip",
770
- ".gz": "application/gzip",
771
- ".tar": "application/x-tar",
772
- ".wasm": "application/wasm",
773
- ".woff": "font/woff",
774
- ".woff2": "font/woff2",
775
- ".ttf": "font/ttf",
776
- ".otf": "font/otf",
777
- ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
778
- ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
779
- ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
780
- };
781
- var TEXT_APPLICATION = /* @__PURE__ */ new Set([
782
- "application/json",
783
- "application/jsonl",
784
- "application/toml",
785
- "application/xml",
786
- "application/sql",
787
- "image/svg+xml"
788
- ]);
789
- function mimeOf(path) {
790
- const slash = path.lastIndexOf("/");
791
- const dot = path.lastIndexOf(".");
792
- if (dot === -1 || dot < slash) return null;
793
- return TYPES[path.slice(dot).toLowerCase()] ?? null;
794
- }
795
- function isUtf8Text(bytes) {
796
- if (bytes.includes(0)) return false;
797
- try {
798
- new TextDecoder("utf-8", { fatal: true }).decode(bytes);
799
- return true;
800
- } catch {
801
- return false;
802
- }
803
- }
804
- function classify(path, bytes) {
805
- const declared = mimeOf(path);
806
- const textual = declared === null ? isUtf8Text(bytes) : declared.startsWith("text/") || TEXT_APPLICATION.has(declared);
807
- if (textual && isUtf8Text(bytes)) {
808
- return { kind: "text", mimeType: declared ?? "text/plain" };
809
- }
810
- if (declared?.startsWith("image/") && declared !== "image/svg+xml") {
811
- return { kind: "image", mimeType: declared };
812
- }
813
- if (declared?.startsWith("audio/"))
814
- return { kind: "audio", mimeType: declared };
815
- return { kind: "blob", mimeType: declared ?? "application/octet-stream" };
816
- }
817
-
818
- // src/files/read.ts
819
- import { readFileSync as readFileSync4, statSync as statSync3 } from "fs";
820
-
821
- // src/files/resolve.ts
822
- import { realpathSync, statSync as statSync2 } from "fs";
823
- import { dirname as dirname3, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
824
- var ownFile = (entry) => relative3(assetRoot(entry), entry.path).split(sep2).join("/");
825
- function resolveSkillFile(entry, wanted = ownFile(entry)) {
826
- if (isAbsolute2(wanted) || wanted.split(/[\\/]/u).includes("..")) {
827
- return { ok: false, error: "path_outside_skill", detail: wanted };
828
- }
829
- const scope = realpathSync(assetRoot(entry));
830
- const single = statSync2(scope).isFile();
831
- const root = single ? dirname3(scope) : scope;
832
- let target;
833
- try {
834
- target = realpathSync(
835
- single && wanted === "" ? scope : resolve2(root, wanted)
836
- );
837
- } catch {
838
- return { ok: false, error: "no_such_file", detail: wanted };
839
- }
840
- const inside = single ? target === scope : target === root || target.startsWith(root + sep2);
841
- if (!inside)
842
- return { ok: false, error: "path_outside_skill", detail: wanted };
843
- return {
844
- ok: true,
845
- target,
846
- path: relative3(root, target).split(sep2).join("/")
847
- };
848
- }
849
-
850
- // src/files/read.ts
851
- var READ_BUDGET_BYTES = 256 * 1024;
852
- function readSkillFile(entry, wanted, budget = READ_BUDGET_BYTES) {
853
- const resolved = resolveSkillFile(entry, wanted);
854
- if (!resolved.ok) return resolved;
855
- const size = statSync3(resolved.target).size;
856
- if (size > budget) {
857
- return {
858
- ok: false,
859
- error: "over_budget",
860
- detail: `${size} bytes > ${budget}`
861
- };
862
- }
863
- const bytes = servedBytes(resolved.target, readFileSync4(resolved.target));
864
- const { kind: kind2, mimeType } = classify(resolved.path, bytes);
865
- return {
866
- ok: true,
867
- name: entry.name,
868
- path: resolved.path,
869
- bytes: bytes.length,
870
- sha256: sha256(bytes),
871
- mimeType,
872
- kind: kind2,
873
- ...kind2 === "text" ? { text: bytes.toString("utf8") } : { blob: bytes.toString("base64") }
874
- };
875
- }
876
-
877
- // src/files/uri.ts
878
- var SKILL_URI_AUTHORITY = "pmcp";
879
- var PREFIX = `skill://${SKILL_URI_AUTHORITY}/`;
880
- var skillFileUri = (entry, file = "SKILL.md") => `${PREFIX}${entry.name}/${file}`;
881
- function resolveSkillFileUri(uri, entries) {
882
- if (!uri.startsWith(PREFIX)) return null;
883
- const rest = decodeURI(uri.slice(PREFIX.length));
884
- let best = null;
885
- for (const entry of entries) {
886
- if (!rest.startsWith(`${entry.name}/`)) continue;
887
- if (best === null || entry.name.length > best.name.length) best = entry;
888
- }
889
- if (best === null) return null;
890
- const file = rest.slice(best.name.length + 1);
891
- return file === "" ? null : { entry: best, file };
892
- }
893
-
894
- // src/resources.ts
895
- import { statSync as statSync4 } from "fs";
896
- import {
897
- ResourceNotFoundError,
898
- ResourceTemplate
899
- } from "@modelcontextprotocol/server";
900
- var RESOURCE_READ_BUDGET = 16 * 1024 * 1024;
901
- var COMPLETION_LIMIT = 100;
902
- function listing(entries) {
903
- return entries.filter((entry) => kindOf(entry) === "skill").map((entry) => ({
904
- uri: skillFileUri(entry, ownFile(entry)),
905
- name: entry.name,
906
- description: entry.description,
907
- mimeType: "text/markdown",
908
- size: statSync4(entry.path).size
909
- }));
910
- }
911
- function complete(value, entries) {
912
- const owner = entries.find((entry) => value.startsWith(`${entry.name}/`));
913
- if (!owner) {
914
- return entries.map((entry) => `${entry.name}/`).filter((name) => name.startsWith(value)).slice(0, COMPLETION_LIMIT);
915
- }
916
- return listSkillFiles(assetRoot(owner)).files.map((file) => `${owner.name}/${file.path}`).filter((path) => path.startsWith(value)).slice(0, COMPLETION_LIMIT);
917
- }
918
- function registerSkillResources(server, skills) {
919
- server.registerResource(
920
- "skill-files",
921
- new ResourceTemplate(`skill://${SKILL_URI_AUTHORITY}/{+path}`, {
922
- list: () => ({ resources: listing(skills()) }),
923
- complete: { path: (value) => complete(value, skills()) }
924
- }),
925
- {
926
- title: "Skill files",
927
- description: "Files of the skills this catalog serves: SKILL.md, references, scripts and assets."
928
- },
929
- async (uri) => {
930
- const href = uri.toString();
931
- const found = resolveSkillFileUri(href, skills());
932
- if (!found) throw new ResourceNotFoundError(href);
933
- const file = readSkillFile(found.entry, found.file, RESOURCE_READ_BUDGET);
934
- if (!file.ok) throw new ResourceNotFoundError(href);
935
- return {
936
- contents: [
937
- file.kind === "text" ? { uri: href, mimeType: file.mimeType, text: file.text ?? "" } : { uri: href, mimeType: file.mimeType, blob: file.blob ?? "" }
938
- ]
939
- };
940
- }
941
- );
942
- }
943
-
944
- // src/find.ts
945
- import { createHash as createHash2 } from "crypto";
946
- var DEFAULT_FIND_LIMIT = 5;
947
- function tokens(text) {
948
- return normaliseIntent(text).split(" ").filter(Boolean);
949
- }
950
- function bigrams(text) {
951
- const flat = normaliseIntent(text).replaceAll(" ", "");
952
- const out = /* @__PURE__ */ new Set();
953
- for (let i = 0; i + 1 < flat.length; i += 1) out.add(flat.slice(i, i + 2));
954
- return out;
955
- }
956
- function lexicalScore(intent, description) {
957
- const wanted = tokens(intent);
958
- if (wanted.length === 0) return 0;
959
- const have = new Set(tokens(description));
960
- const overlap = wanted.filter((token) => have.has(token)).length / wanted.length;
961
- const a = bigrams(intent);
962
- const b = bigrams(description);
963
- if (a.size === 0 || b.size === 0) return overlap / 2;
964
- let shared = 0;
965
- for (const gram of a) if (b.has(gram)) shared += 1;
966
- const dice = 2 * shared / (a.size + b.size);
967
- return (overlap + dice) / 2;
968
- }
969
- function cosine(a, b) {
970
- if (a.length !== b.length) return 0;
971
- let dot = 0;
972
- let na = 0;
973
- let nb = 0;
974
- for (let i = 0; i < a.length; i += 1) {
975
- dot += a[i] * b[i];
976
- na += a[i] * a[i];
977
- nb += b[i] * b[i];
978
- }
979
- if (na === 0 || nb === 0) return 0;
980
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
981
- }
982
- async function find(options) {
983
- const limit = options.limit ?? DEFAULT_FIND_LIMIT;
984
- const semantic = options.embedder !== void 0 && options.vectors !== void 0 && options.vectors.size > 0;
985
- if (semantic) {
986
- const embedder = options.embedder;
987
- const vectors = options.vectors;
988
- const namespace = createHash2("sha256").update(embedder.indexKey ?? embedder.modelId).digest("hex");
989
- const cacheKey = `${namespace} ${normaliseIntent(options.intent)}`;
990
- let query = options.cache?.get(cacheKey) ?? null;
991
- if (!query) {
992
- query = await embedder.embed(options.intent);
993
- options.cache?.put(cacheKey, query);
994
- }
995
- const scored = [];
996
- for (const entry of options.entries) {
997
- const vector = vectors.get(entry.name);
998
- if (!vector || vector.length !== query.length) continue;
999
- scored.push({
1000
- name: entry.name,
1001
- package: entry.package,
1002
- description: entry.description,
1003
- score: cosine(query, vector)
1004
- });
1005
- }
1006
- if (scored.length === options.entries.length) {
1007
- return {
1008
- ranking: "semantic",
1009
- matches: rank(scored, limit)
1010
- };
1011
- }
1012
- }
1013
- return {
1014
- ranking: "lexical",
1015
- matches: rank(
1016
- options.entries.map((entry) => ({
1017
- name: entry.name,
1018
- package: entry.package,
1019
- description: entry.description,
1020
- score: lexicalScore(options.intent, entry.description)
1021
- })),
1022
- limit
1023
- )
1024
- };
1025
- }
1026
- function rank(matches2, limit) {
1027
- return matches2.filter((match) => match.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
1028
- }
1029
-
1030
- // src/files/bundle.ts
1031
- import { readFileSync as readFileSync5, statSync as statSync5 } from "fs";
1032
- import { dirname as dirname4, join as join4 } from "path";
1033
- function bundleSkill(entry) {
1034
- const root = assetRoot(entry);
1035
- const base = statSync5(root).isFile() ? dirname4(root) : root;
1036
- const listing2 = listSkillFiles(root);
1037
- return {
1038
- name: entry.name,
1039
- truncated: listing2.truncated,
1040
- files: listing2.files.map((file) => ({
1041
- ...file,
1042
- uri: skillFileUri(entry, file.path),
1043
- mimeType: classify(file.path, readFileSync5(join4(base, file.path))).mimeType
1044
- }))
1045
- };
1046
- }
1047
-
1048
- // src/server.ts
1049
- import { readFileSync as readFileSync6 } from "fs";
1050
- var DESCRIBE_FILE_LIMIT = FILE_LIST_LIMIT;
1051
- var CATALOG_PAGE_SIZE = 8;
1052
- var CATALOG_PAGE_MAX = 20;
1053
- function matches(entry, filters) {
1054
- const kind2 = filters?.kind ?? "skill";
1055
- if (kind2 !== "any" && kindOf(entry) !== kind2) return false;
1056
- if (!filters) return true;
1057
- const meta = entry.metadata ?? {};
1058
- const field = (key) => {
1059
- const value = meta[key];
1060
- return typeof value === "string" ? value : void 0;
1061
- };
1062
- if (filters.tier !== void 0 && entry.tier !== filters.tier) return false;
1063
- if (filters.package !== void 0 && entry.package !== filters.package)
1064
- return false;
1065
- if (filters.level !== void 0 && field("level") !== filters.level)
1066
- return false;
1067
- if (filters.domain !== void 0 && field("domain") !== filters.domain)
1068
- return false;
1069
- if (filters.medium !== void 0 && field("medium") !== filters.medium)
1070
- return false;
1071
- if (filters.verifiedOnly) {
1072
- const verified = verifiedRuntimesOf(entry.metadata);
1073
- if (filters.runtime === void 0) return verified.length > 0;
1074
- return verified.includes(filters.runtime);
1075
- }
1076
- return true;
1077
- }
1078
- function verification(entry, runtime, asked) {
1079
- if (!asked) return {};
1080
- const verifiedRuntimes = verifiedRuntimesOf(entry?.metadata);
1081
- return {
1082
- ...verifiedRuntimes.length > 0 ? { verifiedRuntimes } : {},
1083
- ...runtime === null ? {} : { verified: verifiedRuntimes.includes(runtime) }
1084
- };
1085
- }
1086
- function verifiedFirst(items) {
1087
- return [
1088
- ...items.filter((item) => item.verified === true),
1089
- ...items.filter((item) => item.verified !== true)
1090
- ];
1091
- }
1092
- function catalogItem(entry, runtime, asked) {
1093
- return {
1094
- name: entry.name,
1095
- description: entry.description,
1096
- ...entry.tier === void 0 ? {} : { tier: entry.tier },
1097
- ...kindOf(entry) === "skill" ? {} : { kind: kindOf(entry) },
1098
- ...verification(entry, runtime, asked)
1099
- };
1100
- }
1101
- function flatMetadata(metadata) {
1102
- const flat = {};
1103
- for (const [key, value] of Object.entries(metadata ?? {})) {
1104
- if (typeof value === "string" || Array.isArray(value)) {
1105
- flat[key] = value;
1106
- continue;
1107
- }
1108
- for (const [inner, leaf] of Object.entries(value ?? {})) {
1109
- flat[`${key}.${inner}`] = leaf;
1110
- }
1111
- }
1112
- return flat;
1113
- }
1114
- function createSkillTools(options) {
1115
- const load = options.loadCatalog ?? readCatalog;
1116
- let entries = null;
1117
- const catalog = () => {
1118
- entries ??= load({
1119
- roots: options.roots,
1120
- scopes: options.scopes,
1121
- marketplaces: options.marketplaces,
1122
- packages: options.packages,
1123
- workspaces: options.workspaces,
1124
- onReject: options.onReject,
1125
- onWarning: options.onWarning
1126
- });
1127
- return entries;
1128
- };
1129
- const byName = (name) => catalog().find((candidate) => candidate.name === name) ?? null;
1130
- return {
1131
- /**
1132
- * Grouped, and without bodies. The cheap overview and the search's miss
1133
- * path. Paged by package so a large catalog never arrives in one answer.
1134
- */
1135
- catalog(request = {}) {
1136
- const client = options.rankByClientRuntime ? request.clientRuntime : null;
1137
- const runtime = request.runtime ?? client ?? null;
1138
- const asked = runtime !== null || request.verifiedOnly !== void 0;
1139
- const byPackage = /* @__PURE__ */ new Map();
1140
- let count = 0;
1141
- for (const entry of catalog()) {
1142
- if (!matches(entry, {
1143
- tier: request.tier,
1144
- kind: request.kind,
1145
- runtime: request.runtime,
1146
- verifiedOnly: request.verifiedOnly
1147
- }))
1148
- continue;
1149
- count += 1;
1150
- const list = byPackage.get(entry.package) ?? [];
1151
- list.push(catalogItem(entry, runtime, asked));
1152
- byPackage.set(entry.package, list);
1153
- }
1154
- const groups = [...byPackage.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([pkg, skills]) => [
1155
- pkg,
1156
- runtime === null ? skills : verifiedFirst(skills)
1157
- ]);
1158
- const size = Math.min(
1159
- Math.max(request.pageSize ?? CATALOG_PAGE_SIZE, 1),
1160
- CATALOG_PAGE_MAX
1161
- );
1162
- const totalPages = Math.max(1, Math.ceil(groups.length / size));
1163
- const page2 = Math.min(Math.max(request.page ?? 1, 1), totalPages);
1164
- return {
1165
- count,
1166
- page: page2,
1167
- totalPages,
1168
- hasNext: page2 < totalPages,
1169
- packages: groups.slice((page2 - 1) * size, page2 * size).map(([pkg, skills]) => ({ package: pkg, skills }))
1170
- };
1171
- },
1172
- /** Ranked names. Returns; never loads a body. */
1173
- find(intent, limit, filters, clientRuntime) {
1174
- const client = options.rankByClientRuntime ? clientRuntime : null;
1175
- const runtime = filters?.runtime ?? client ?? null;
1176
- const asked = runtime !== null || filters?.verifiedOnly !== void 0;
1177
- const scoped = catalog().filter((entry) => matches(entry, filters)).map(
1178
- (entry) => entry.keywords && entry.keywords.length > 0 ? {
1179
- ...entry,
1180
- description: `${entry.description} ${entry.keywords.join(" ")}`
1181
- } : entry
1182
- );
1183
- return find({
1184
- entries: scoped,
1185
- intent,
1186
- limit: runtime === null ? limit : scoped.length,
1187
- embedder: options.embedder,
1188
- cache: options.cache,
1189
- vectors: options.vectors
1190
- }).then((result) => {
1191
- const marked = result.matches.map((match) => {
1192
- const entry = byName(match.name) ?? void 0;
1193
- return {
1194
- ...match,
1195
- description: entry?.description ?? match.description,
1196
- ...verification(entry, runtime, asked)
1197
- };
1198
- });
1199
- const ordered = runtime === null ? marked : verifiedFirst(marked);
1200
- return {
1201
- ...result,
1202
- ...asked ? { runtime } : {},
1203
- matches: runtime === null ? ordered : ordered.slice(0, limit ?? DEFAULT_FIND_LIMIT)
1204
- };
1205
- });
1206
- },
1207
- /** Every property of one skill and the files it carries, without bodies. */
1208
- describe(name) {
1209
- const entry = byName(name);
1210
- if (!entry) return null;
1211
- const source = readFileSync6(entry.path, "utf8");
1212
- const listing2 = listSkillFiles(assetRoot(entry));
1213
- return {
1214
- name: entry.name,
1215
- kind: kindOf(entry),
1216
- package: entry.package,
1217
- description: entry.description,
1218
- ...entry.tier === void 0 ? {} : { tier: entry.tier },
1219
- metadata: flatMetadata(entry.metadata),
1220
- frontmatter: readFrontmatterText(source),
1221
- files: listing2.files.map((file) => ({ ...file })),
1222
- filesTruncated: listing2.truncated
1223
- };
1224
- },
1225
- /**
1226
- * One file inside a skill's directory. Paths that leave the directory,
1227
- * directly or through a link, are refused; nothing is ever executed.
1228
- */
1229
- read(name, path) {
1230
- const entry = byName(name);
1231
- if (!entry) return { ok: false, error: "unknown_skill", detail: name };
1232
- return readSkillFile(entry, path);
1233
- },
1234
- /** Every file of one skill as links; the bytes stay out of the answer. */
1235
- bundle(name) {
1236
- const entry = byName(name);
1237
- return entry ? bundleSkill(entry) : null;
1238
- },
1239
- /** One body, at the moment it is needed, and the other files it carries. */
1240
- call(name) {
1241
- const entry = byName(name);
1242
- if (!entry) return null;
1243
- const bundle = bundleSkill(entry);
1244
- const own = ownFile(entry);
1245
- return {
1246
- name: entry.name,
1247
- body: readBody(
1248
- servedBytes(entry.path, readFileSync6(entry.path)).toString("utf8")
1249
- ),
1250
- files: bundle.files.filter((file) => file.path !== own),
1251
- filesTruncated: bundle.truncated
1252
- };
1253
- },
1254
- /** The catalog entry for a name, among those this caller may see. */
1255
- entry: (name) => byName(name),
1256
- /** Drops the memoised catalog, so the next call re-reads the tree. */
1257
- refresh() {
1258
- entries = null;
1259
- },
1260
- entries: () => catalog().map(withoutBody),
1261
- /** The catalog with paths, for the Skills extension. The same array until refresh. */
1262
- skills: () => catalog()
1263
- };
1264
- }
1265
-
1266
- // src/extension/entry.ts
1267
- import { createHash as createHash3 } from "crypto";
1268
- import { readFileSync as readFileSync7, readdirSync as readdirSync4, statSync as statSync6 } from "fs";
1269
- import { basename as basename5, dirname as dirname5, join as join5, relative as relative4, sep as sep3 } from "path";
1270
- var MAX_RESOURCES = 512;
1271
- var MAX_SKILL_BYTES = 16 * 1024 * 1024;
1272
- var digests = /* @__PURE__ */ new Map();
1273
- function facts(path) {
1274
- const stat = statSync6(path);
1275
- const key = `${stat.mtimeMs}:${stat.size}`;
1276
- const cached = digests.get(path);
1277
- if (cached?.key === key) return cached;
1278
- const bytes = servedBytes(path, readFileSync7(path));
1279
- const digest = `sha256:${createHash3("sha256").update(bytes).digest("hex")}`;
1280
- const fresh = { key, digest, size: bytes.length };
1281
- digests.set(path, fresh);
1282
- return fresh;
1283
- }
1284
- function filesUnder(root) {
1285
- const found = [];
1286
- const stack = [root];
1287
- while (stack.length > 0) {
1288
- const dir = stack.pop();
1289
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
1290
- if (entry.name === "node_modules" || entry.name === ".git") continue;
1291
- const full = join5(dir, entry.name);
1292
- if (entry.isDirectory()) stack.push(full);
1293
- else if (entry.isFile()) found.push(full);
1294
- if (found.length > MAX_RESOURCES) return null;
1295
- }
1296
- }
1297
- return found.sort();
1298
- }
1299
- function extensionEntry(entry) {
1300
- if (kindOf(entry) !== "skill" || basename5(entry.path) !== "SKILL.md")
1301
- return null;
1302
- const frontmatter = frontmatterObject(readFileSync7(entry.path, "utf8"));
1303
- const root = dirname5(entry.path);
1304
- if (specProblems(frontmatter, basename5(root)).length > 0) return null;
1305
- if (frontmatter?.["name"] !== entry.slug) return null;
1306
- const files = filesUnder(root);
1307
- if (files === null) return null;
1308
- let total = 0;
1309
- const resources = files.map((path) => {
1310
- const fact = facts(path);
1311
- total += fact.size;
1312
- const file = relative4(root, path).split(sep3).join("/");
1313
- return {
1314
- uri: skillFileUri(entry, file),
1315
- digest: fact.digest,
1316
- size: fact.size
1317
- };
1318
- });
1319
- if (total > MAX_SKILL_BYTES) return null;
1320
- return { uri: skillFileUri(entry), frontmatter, resources };
1321
- }
1322
-
1323
- // src/extension/register.ts
1324
- import {
1325
- ProtocolError,
1326
- ProtocolErrorCode
1327
- } from "@modelcontextprotocol/server";
1328
- import { z as z2 } from "zod";
1329
- var SKILLS_EXTENSION = "io.modelcontextprotocol/skills";
1330
- var PAGE_SIZE = 50;
1331
- var TTL_MS = 6e4;
1332
- function entriesOf(skills) {
1333
- let memo = null;
1334
- return () => {
1335
- const source = skills();
1336
- if (memo?.source === source) return memo.entries;
1337
- const map = /* @__PURE__ */ new Map();
1338
- for (const entry of source) {
1339
- try {
1340
- const served = extensionEntry(entry);
1341
- if (served) map.set(served.uri, served);
1342
- } catch {
1343
- }
1344
- }
1345
- memo = { source, entries: map };
1346
- return map;
1347
- };
1348
- }
1349
- function page(all, cursor) {
1350
- const start = cursor ? Number.parseInt(cursor, 10) : 0;
1351
- if (!Number.isInteger(start) || start < 0 || start > all.length) {
1352
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, "invalid cursor");
1353
- }
1354
- const end = start + PAGE_SIZE;
1355
- return {
1356
- skills: all.slice(start, end),
1357
- ...end < all.length ? { nextCursor: String(end) } : {}
1358
- };
1359
- }
1360
- function registerSkillsExtension(server, skills, options = {}) {
1361
- const entries = entriesOf(skills);
1362
- const cache = { ttlMs: TTL_MS, cacheScope: options.cacheScope ?? "public" };
1363
- server.server.registerCapabilities({
1364
- extensions: { [SKILLS_EXTENSION]: {} }
1365
- });
1366
- server.server.setRequestHandler(
1367
- "skills/list",
1368
- { params: z2.object({ cursor: z2.string().optional() }).passthrough() },
1369
- async (params) => ({
1370
- resultType: "complete",
1371
- ...page([...entries().values()], params.cursor),
1372
- ...cache
1373
- })
1374
- );
1375
- server.server.setRequestHandler(
1376
- "skills/get",
1377
- { params: z2.object({ uri: z2.string() }).passthrough() },
1378
- async (params) => {
1379
- const skill = entries().get(params.uri);
1380
- if (!skill) {
1381
- throw new ProtocolError(
1382
- ProtocolErrorCode.InvalidParams,
1383
- `not a skill this server serves: ${params.uri}`
1384
- );
1385
- }
1386
- return { resultType: "complete", skill, ...cache };
1387
- }
1388
- );
1389
- }
1390
-
1391
- // src/tools/schemas.ts
1392
- import { z as z3 } from "zod";
1393
- var kind = z3.enum(ASSET_KINDS);
1394
- var runtimeSchema = z3.enum(RUNTIMES);
1395
- var catalogItemSchema = z3.looseObject({
1396
- name: z3.string(),
1397
- description: z3.string(),
1398
- tier: z3.string().optional(),
1399
- kind: kind.optional(),
1400
- verifiedRuntimes: z3.array(runtimeSchema).optional(),
1401
- verified: z3.boolean().optional()
1402
- });
1403
- var catalogResponseSchema = z3.looseObject({
1404
- count: z3.number().int(),
1405
- page: z3.number().int(),
1406
- totalPages: z3.number().int(),
1407
- hasNext: z3.boolean(),
1408
- packages: z3.array(
1409
- z3.looseObject({ package: z3.string(), skills: z3.array(catalogItemSchema) })
1410
- )
1411
- });
1412
- var findResultSchema = z3.looseObject({
1413
- ranking: z3.enum(["semantic", "lexical"]),
1414
- runtime: runtimeSchema.nullable().optional(),
1415
- matches: z3.array(
1416
- z3.looseObject({
1417
- name: z3.string(),
1418
- package: z3.string(),
1419
- description: z3.string(),
1420
- score: z3.number(),
1421
- verifiedRuntimes: z3.array(runtimeSchema).optional(),
1422
- verified: z3.boolean().optional()
1423
- })
1424
- )
1425
- });
1426
- var skillFileSchema = z3.looseObject({
1427
- path: z3.string(),
1428
- bytes: z3.number().int(),
1429
- sha256: z3.string()
1430
- });
1431
- var describeResponseSchema = z3.looseObject({
1432
- name: z3.string(),
1433
- kind,
1434
- package: z3.string(),
1435
- description: z3.string(),
1436
- tier: z3.string().optional(),
1437
- metadata: z3.record(
1438
- z3.string(),
1439
- z3.union([z3.string(), z3.array(z3.string()).readonly()])
1440
- ),
1441
- frontmatter: z3.string(),
1442
- files: z3.array(skillFileSchema),
1443
- filesTruncated: z3.boolean()
1444
- });
1445
- var bundleResponseSchema = z3.looseObject({
1446
- name: z3.string(),
1447
- truncated: z3.boolean(),
1448
- files: z3.array(
1449
- skillFileSchema.extend({ uri: z3.string(), mimeType: z3.string() })
1450
- )
1451
- });
1452
-
1453
- // src/mcp.ts
1454
- import { McpServer } from "@modelcontextprotocol/server";
1455
-
1456
- // src/tools/bundle.ts
1457
- import { z as z5 } from "zod";
1458
-
1459
- // src/tools/content.ts
1460
- function fileBlock(file) {
1461
- if (file.kind === "text") {
1462
- return { type: "text", text: file.text ?? "" };
1463
- }
1464
- if (file.kind === "image" || file.kind === "audio") {
1465
- return {
1466
- type: file.kind,
1467
- data: file.blob ?? "",
1468
- mimeType: file.mimeType
1469
- };
1470
- }
1471
- return {
1472
- type: "resource",
1473
- resource: {
1474
- uri: skillFileUri({ name: file.name }, file.path),
1475
- mimeType: file.mimeType,
1476
- blob: file.blob ?? ""
1477
- }
1478
- };
1479
- }
1480
- var resourceLink = (file) => ({
1481
- type: "resource_link",
1482
- uri: file.uri,
1483
- name: file.path,
1484
- mimeType: file.mimeType,
1485
- size: file.bytes
1486
- });
1487
-
1488
- // src/tools/shared.ts
1489
- import { z as z4 } from "zod";
1490
- var readOnly = {
1491
- readOnlyHint: true,
1492
- destructiveHint: false,
1493
- idempotentHint: true,
1494
- openWorldHint: false
1495
- };
1496
- var readOnlyTool = (title) => ({
1497
- title,
1498
- annotations: { ...readOnly, title }
1499
- });
1500
- var kindInput = z4.enum(["skill", "agent", "hook", "mcp", "any"]).optional().describe(
1501
- "skill (default), agent, hook, mcp, or any. Commands are skills; agents, hooks and MCP servers are installed with pmcp add rather than read."
1502
- );
1503
- var tierInput = z4.enum(["open", "free", "paid", "internal"]).optional();
1504
- var runtimeInput = runtimeSchema.optional().describe(
1505
- "A runtime (claude-code, codex-cli, gemini-cli, grok-cli, antigravity). Skills whose metadata.verified-runtimes lists it are marked verified and listed first; the rest are kept. Without it, the order is the same as without verification data."
1506
- );
1507
- var verifiedOnlyInput = z4.boolean().optional().describe(
1508
- "Drop skills that are not verified: on the given runtime, or on any runtime when none is given. The detected client runtime never filters."
1509
- );
1510
- var skillNameInput = z4.string().min(1).describe("A skill name as skill_find or skill_catalog returns it.");
1511
- var asText = (value) => ({
1512
- content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
1513
- });
1514
- var structured = (value) => ({
1515
- ...asText(value),
1516
- structuredContent: { ...value }
1517
- });
1518
- var unknownSkill = (name) => ({
1519
- ...asText({
1520
- error: "unknown_skill",
1521
- name,
1522
- hint: "skill_catalog lists the names that exist."
1523
- }),
1524
- isError: true
1525
- });
1526
-
1527
- // src/tools/bundle.ts
1528
- function registerBundleTool(server, tools) {
1529
- server.registerTool(
1530
- "skill_bundle",
1531
- {
1532
- ...readOnlyTool("List a skill's files"),
1533
- 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.",
1534
- inputSchema: z5.object({ name: skillNameInput }),
1535
- outputSchema: bundleResponseSchema
1536
- },
1537
- async ({ name }) => {
1538
- const bundle = tools.bundle(name);
1539
- if (!bundle) return unknownSkill(name);
1540
- const value = { ...bundle, files: [...bundle.files] };
1541
- return {
1542
- content: [
1543
- { type: "text", text: JSON.stringify(value, null, 2) },
1544
- ...bundle.files.map(resourceLink)
1545
- ],
1546
- structuredContent: value
1547
- };
1548
- }
1549
- );
1550
- }
1551
-
1552
- // src/tools/call.ts
1553
- import { z as z6 } from "zod";
1554
- function manifest(found) {
1555
- const lines = found.files.map(
1556
- (file) => `- ${file.path} (${file.bytes} bytes, ${file.mimeType}, sha256 ${file.sha256.slice(0, 12)})`
1557
- );
1558
- if (found.filesTruncated)
1559
- lines.push("- (list cut; skill_bundle has the rest)");
1560
- return [
1561
- `Files in skill ${found.name}. skill_read returns one by this name and its path; skill_bundle returns every file with its path:`,
1562
- ...lines
1563
- ].join("\n");
1564
- }
1565
- function registerCallTool(server, tools) {
1566
- server.registerTool(
1567
- "skill_call",
1568
- {
1569
- ...readOnlyTool("Read a skill"),
1570
- 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.",
1571
- inputSchema: z6.object({
1572
- name: z6.string().min(1).describe("The skill name, as <package>/<slug>.")
1573
- })
1574
- },
1575
- async ({ name }) => {
1576
- const found = tools.call(name);
1577
- if (!found) return unknownSkill(name);
1578
- const body = { type: "text", text: found.body };
1579
- if (found.files.length === 0) return { content: [body] };
1580
- return {
1581
- content: [
1582
- body,
1583
- { type: "text", text: manifest(found) },
1584
- ...found.files.map(resourceLink)
1585
- ]
1586
- };
1587
- }
1588
- );
1589
- }
1590
-
1591
- // src/tools/catalog.ts
1592
- import { z as z7 } from "zod";
1593
- function registerCatalogTool(server, tools) {
1594
- server.registerTool(
1595
- "skill_catalog",
1596
- {
1597
- ...readOnlyTool("List skills"),
1598
- 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. Items list the runtimes they were verified on (verifiedRuntimes). With runtime, verified items are marked and listed first within their group but the others are kept; verifiedOnly drops the unverified ones.",
1599
- inputSchema: z7.object({
1600
- page: z7.number().int().min(1).optional(),
1601
- pageSize: z7.number().int().min(1).max(20).optional(),
1602
- tier: tierInput,
1603
- kind: kindInput,
1604
- runtime: runtimeInput,
1605
- verifiedOnly: verifiedOnlyInput
1606
- }),
1607
- outputSchema: catalogResponseSchema
1608
- },
1609
- async (request, ctx) => structured(
1610
- tools.catalog({
1611
- ...request,
1612
- clientRuntime: detectRuntime(server, ctx)
1613
- })
1614
- )
1615
- );
1616
- }
1617
-
1618
- // src/tools/describe.ts
1619
- import { z as z8 } from "zod";
1620
- function registerDescribeTool(server, tools) {
1621
- server.registerTool(
1622
- "skill_describe",
1623
- {
1624
- ...readOnlyTool("Describe a skill"),
1625
- description: "One skill's frontmatter, metadata (tier, level, requirements as requires.<name> entries, verified-runtimes, licence, approval), and the files it carries with sizes and sha256, without any body.",
1626
- inputSchema: z8.object({ name: skillNameInput }),
1627
- outputSchema: describeResponseSchema
1628
- },
1629
- async ({ name }) => {
1630
- const described = tools.describe(name);
1631
- return described ? structured(described) : unknownSkill(name);
1632
- }
1633
- );
1634
- }
1635
-
1636
- // src/tools/find.ts
1637
- import { z as z9 } from "zod";
1638
- function registerFindTool(server, tools) {
1639
- server.registerTool(
1640
- "skill_find",
1641
- {
1642
- ...readOnlyTool("Find skills"),
1643
- 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. Each match lists the runtimes it was verified on (verifiedRuntimes). With runtime, verified matches are marked and listed first but the others are kept; verifiedOnly drops the unverified ones. The answer's runtime is the one the ordering used, or null.",
1644
- inputSchema: z9.object({
1645
- intent: z9.string().min(1).max(256).describe("What the task is, as a sentence."),
1646
- limit: z9.number().int().min(1).max(25).optional(),
1647
- runtime: runtimeInput,
1648
- verifiedOnly: verifiedOnlyInput,
1649
- filters: z9.object({
1650
- kind: kindInput,
1651
- tier: tierInput,
1652
- package: z9.string().optional(),
1653
- level: z9.string().optional(),
1654
- domain: z9.string().optional(),
1655
- medium: z9.string().optional()
1656
- }).optional().describe("Exact-match filters over the catalog before ranking.")
1657
- }),
1658
- outputSchema: findResultSchema
1659
- },
1660
- async ({ intent, limit, runtime, verifiedOnly, filters }, ctx) => structured(
1661
- await tools.find(
1662
- intent,
1663
- limit,
1664
- { ...filters, runtime, verifiedOnly },
1665
- detectRuntime(server, ctx)
1666
- )
1667
- )
1668
- );
1669
- }
1670
-
1671
- // src/tools/read.ts
1672
- import { z as z10 } from "zod";
1673
- function registerReadTool(server, tools) {
1674
- server.registerTool(
1675
- "skill_read",
1676
- {
1677
- ...readOnlyTool("Read a skill file"),
1678
- 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.",
1679
- inputSchema: z10.object({
1680
- name: skillNameInput,
1681
- path: z10.string().min(1).optional().describe(
1682
- "Path relative to the skill directory. Defaults to SKILL.md."
1683
- )
1684
- })
1685
- },
1686
- async ({ name, path }) => {
1687
- const result = tools.read(name, path);
1688
- if (!result.ok) return { ...asText(result), isError: true };
1689
- const { text: _text, blob: _blob, ...header } = result;
1690
- return {
1691
- content: [
1692
- { type: "text", text: JSON.stringify(header) },
1693
- fileBlock(result)
1694
- ]
1695
- };
1696
- }
1697
- );
1698
- }
1699
-
1700
- // src/mcp.ts
1701
- var SERVER_NAME = "pmcp";
1702
- var SERVER_VERSION = "0.10.0";
1703
- function registerSkillTools(server, tools) {
1704
- registerCatalogTool(server, tools);
1705
- registerFindTool(server, tools);
1706
- registerDescribeTool(server, tools);
1707
- registerReadTool(server, tools);
1708
- registerCallTool(server, tools);
1709
- registerBundleTool(server, tools);
1710
- return server;
1711
- }
1712
- function createSkillServer(options) {
1713
- const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
1714
- const tools = createSkillTools(options);
1715
- registerSkillsExtension(server, tools.skills, options.skillsExtension);
1716
- registerSkillResources(server, tools.skills);
1717
- if (options.prompts) registerSkillPrompts(server, tools);
1718
- if (options.mailbox) registerMailboxTools(server, options.mailbox);
1719
- return registerSkillTools(server, tools);
1720
- }
1721
-
1722
- export {
1723
- normaliseIntent,
1724
- frontmatterObject,
1725
- NAME_MAX,
1726
- DESCRIPTION_MAX,
1727
- COMPATIBILITY_MAX,
1728
- specProblems,
1729
- readMetadata,
1730
- readFrontmatterText,
1731
- readMarketplace,
1732
- assetRoot,
1733
- ASSET_KINDS,
1734
- kindOf,
1735
- SKIPPED_DIRS,
1736
- MAX_SKILL_DEPTH,
1737
- readFrontmatter,
1738
- readBody,
1739
- readCatalog,
1740
- withoutBody,
1741
- displayPath,
1742
- promptName,
1743
- registerSkillPrompts,
1744
- listSkillFiles,
1745
- mimeOf,
1746
- isUtf8Text,
1747
- classify,
1748
- READ_BUDGET_BYTES,
1749
- readSkillFile,
1750
- skillFileUri,
1751
- resolveSkillFileUri,
1752
- registerSkillResources,
1753
- DEFAULT_FIND_LIMIT,
1754
- lexicalScore,
1755
- cosine,
1756
- find,
1757
- bundleSkill,
1758
- DESCRIBE_FILE_LIMIT,
1759
- CATALOG_PAGE_SIZE,
1760
- CATALOG_PAGE_MAX,
1761
- createSkillTools,
1762
- extensionEntry,
1763
- SKILLS_EXTENSION,
1764
- registerSkillsExtension,
1765
- catalogResponseSchema,
1766
- findResultSchema,
1767
- describeResponseSchema,
1768
- bundleResponseSchema,
1769
- SERVER_NAME,
1770
- SERVER_VERSION,
1771
- registerSkillTools,
1772
- createSkillServer
1773
- };
1774
- //# sourceMappingURL=chunk-VRZFM2M5.js.map