@modootoday/pmcp 0.7.1 → 0.8.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.
@@ -1,1194 +0,0 @@
1
- // src/intent.ts
2
- function normaliseIntent(intent) {
3
- return intent.toLowerCase().replaceAll(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/u).filter(Boolean).join(" ");
4
- }
5
-
6
- // src/marketplace.ts
7
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
8
- import { basename as basename2, dirname as dirname2, join as join2, resolve } from "path";
9
-
10
- // src/catalog.ts
11
- import { existsSync, readFileSync, readdirSync } from "fs";
12
- import { basename, dirname, isAbsolute, join, relative } from "path";
13
- var ASSET_KINDS = ["skill", "agent", "hook", "mcp"];
14
- var kindOf = (entry) => entry.kind ?? "skill";
15
- var SKIPPED_DIRS = /* @__PURE__ */ new Set([
16
- "node_modules",
17
- ".git",
18
- ".cache",
19
- ".turbo",
20
- "coverage",
21
- "__tests__",
22
- "__fixtures__"
23
- ]);
24
- var MAX_SKILL_DEPTH = 3;
25
- function readFrontmatter(source) {
26
- if (!source.startsWith("---")) return {};
27
- const end = source.indexOf("\n---", 3);
28
- if (end === -1) return {};
29
- const fields = {};
30
- for (const line of source.slice(3, end).split("\n")) {
31
- const match = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/u.exec(line);
32
- if (!match) continue;
33
- const value = (match[2] ?? "").trim();
34
- if (value === "" || value.startsWith("#")) continue;
35
- fields[match[1]] = value.replace(/^["'](.*)["']$/u, "$1");
36
- }
37
- return fields;
38
- }
39
- function readBody(source) {
40
- if (!source.startsWith("---")) return source;
41
- const end = source.indexOf("\n---", 3);
42
- if (end === -1) return source;
43
- return source.slice(end + 4).replace(/^(?:\r?\n)+/u, "");
44
- }
45
- function skillFiles(packageDir) {
46
- const found = [];
47
- const stack = [[packageDir, 0]];
48
- while (stack.length > 0) {
49
- const next = stack.pop();
50
- if (next === void 0) break;
51
- const [dir, depth] = next;
52
- let entries;
53
- try {
54
- entries = readdirSync(dir, { withFileTypes: true });
55
- } catch {
56
- continue;
57
- }
58
- for (const entry of entries) {
59
- if (entry.isFile()) {
60
- if (entry.name === "SKILL.md") found.push(join(dir, entry.name));
61
- continue;
62
- }
63
- if (!entry.isDirectory()) continue;
64
- if (SKIPPED_DIRS.has(entry.name)) continue;
65
- if (depth + 1 <= MAX_SKILL_DEPTH)
66
- stack.push([join(dir, entry.name), depth + 1]);
67
- }
68
- }
69
- return found.sort();
70
- }
71
- function packageDirs(root) {
72
- if (!existsSync(root)) return [];
73
- const found = [];
74
- for (const entry of readdirSync(root, { withFileTypes: true })) {
75
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
76
- if (entry.name === ".bin" || entry.name === ".cache") continue;
77
- const path = join(root, entry.name);
78
- if (entry.name.startsWith("@")) {
79
- if (!existsSync(path)) continue;
80
- for (const scoped of readdirSync(path, { withFileTypes: true })) {
81
- if (scoped.isDirectory() || scoped.isSymbolicLink()) {
82
- found.push(join(path, scoped.name));
83
- }
84
- }
85
- continue;
86
- }
87
- found.push(path);
88
- }
89
- return found;
90
- }
91
- function readManifest(dir) {
92
- try {
93
- const parsed = JSON.parse(
94
- readFileSync(join(dir, "package.json"), "utf8")
95
- );
96
- return parsed !== null && typeof parsed === "object" ? parsed : null;
97
- } catch {
98
- return null;
99
- }
100
- }
101
- function workspacePatterns(manifest) {
102
- const field = manifest["workspaces"];
103
- const list = Array.isArray(field) ? field : field?.packages;
104
- if (!Array.isArray(list)) return [];
105
- return list.filter((p) => typeof p === "string");
106
- }
107
- function expandBraces(pattern) {
108
- const match = /\{([^{}]*)\}/u.exec(pattern);
109
- if (!match) return [pattern];
110
- const head = pattern.slice(0, match.index);
111
- const tail = pattern.slice(match.index + match[0].length);
112
- return match[1].split(",").flatMap((option) => expandBraces(`${head}${option}${tail}`));
113
- }
114
- function segmentMatcher(segment) {
115
- const body = segment.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/gu, "\\$&")).join("[^/]*");
116
- return new RegExp(`^${body}$`, "u");
117
- }
118
- function globDirs(base, pattern) {
119
- const segments = pattern.split("/").filter((s) => s !== "" && s !== ".");
120
- const found = /* @__PURE__ */ new Set();
121
- const walk = (dir, index) => {
122
- if (index === segments.length) {
123
- found.add(dir);
124
- return;
125
- }
126
- const segment = segments[index];
127
- if (!segment.includes("*")) {
128
- const next = join(dir, segment);
129
- if (existsSync(next)) walk(next, index + 1);
130
- return;
131
- }
132
- let entries;
133
- try {
134
- entries = readdirSync(dir, { withFileTypes: true });
135
- } catch {
136
- return;
137
- }
138
- const children = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).filter((e) => !SKIPPED_DIRS.has(e.name));
139
- if (segment === "**") {
140
- walk(dir, index + 1);
141
- for (const child of children) walk(join(dir, child.name), index);
142
- return;
143
- }
144
- const matcher = segmentMatcher(segment);
145
- for (const child of children) {
146
- if (matcher.test(child.name)) walk(join(dir, child.name), index + 1);
147
- }
148
- };
149
- walk(base, 0);
150
- return [...found];
151
- }
152
- function workspacePackageDirs(root) {
153
- const ordered = [];
154
- const visited = /* @__PURE__ */ new Set();
155
- const expand = (dir) => {
156
- if (visited.has(dir)) return;
157
- visited.add(dir);
158
- const manifest = readManifest(dir);
159
- if (manifest === null) return;
160
- const included = /* @__PURE__ */ new Set();
161
- const excluded = /* @__PURE__ */ new Set();
162
- for (const raw of workspacePatterns(manifest)) {
163
- const negated = raw.startsWith("!");
164
- const pattern = negated ? raw.slice(1) : raw;
165
- for (const alternative of expandBraces(pattern)) {
166
- for (const match of globDirs(dir, alternative)) {
167
- (negated ? excluded : included).add(match);
168
- }
169
- }
170
- }
171
- const members = [...included].filter((m) => !excluded.has(m) && existsSync(join(m, "package.json"))).sort();
172
- for (const member of members) expand(member);
173
- ordered.push(dir);
174
- };
175
- expand(root);
176
- return ordered;
177
- }
178
- function nameOf(dir, named) {
179
- const manifestName = readManifest(dir)?.["name"];
180
- if (typeof manifestName === "string" && manifestName !== "") {
181
- return manifestName;
182
- }
183
- if (named) return basename(dir);
184
- return "";
185
- }
186
- function expandRootGlob(dir) {
187
- if (!dir.includes("*")) return [dir];
188
- const absolute = isAbsolute(dir);
189
- const base = absolute ? "/" : ".";
190
- return globDirs(base, dir).filter((d) => existsSync(join(d, "package.json"))).sort();
191
- }
192
- function readCatalog(options) {
193
- const scopes = options.scopes ?? [];
194
- const found = [];
195
- const seen = /* @__PURE__ */ new Set();
196
- const seenPaths = /* @__PURE__ */ new Set();
197
- const direct = new Set(options.packages ?? []);
198
- const packageDirsToRead = [
199
- ...options.roots.flatMap(packageDirs),
200
- ...direct,
201
- ...(options.workspaces ?? []).flatMap(expandRootGlob).flatMap(workspacePackageDirs)
202
- ];
203
- for (const packageDir of packageDirsToRead) {
204
- const packageName = nameOf(packageDir, direct.has(packageDir));
205
- if (packageName === "") continue;
206
- if (scopes.length > 0 && !scopes.some((s) => packageName.startsWith(s))) {
207
- continue;
208
- }
209
- for (const path of skillFiles(packageDir)) {
210
- if (seenPaths.has(path)) continue;
211
- seenPaths.add(path);
212
- let source;
213
- try {
214
- source = readFileSync(path, "utf8");
215
- } catch {
216
- continue;
217
- }
218
- const front = readFrontmatter(source);
219
- const description = front["description"] ?? "";
220
- if (description === "") continue;
221
- const dirName = basename(dirname(path));
222
- const slug = front["name"] ?? (dirName === basename(packageDir) ? packageName : dirName);
223
- const name = `${packageName}/${slug}`;
224
- if (seen.has(name)) continue;
225
- seen.add(name);
226
- found.push({
227
- name,
228
- package: packageName,
229
- slug,
230
- description,
231
- path
232
- });
233
- }
234
- }
235
- for (const dir of options.marketplaces ?? []) {
236
- for (const entry of readMarketplace(dir, options.onReject)) {
237
- if (seen.has(entry.name)) {
238
- options.onReject?.({
239
- path: entry.path,
240
- reason: `duplicate name ${entry.name}`
241
- });
242
- continue;
243
- }
244
- seen.add(entry.name);
245
- found.push(entry);
246
- }
247
- }
248
- return found.sort((a, b) => a.name.localeCompare(b.name));
249
- }
250
- function withoutBody({
251
- path,
252
- ...rest
253
- }) {
254
- void path;
255
- return rest;
256
- }
257
- function displayPath(root, entry) {
258
- return relative(root, entry.path).replaceAll("\\", "/");
259
- }
260
-
261
- // src/marketplace.ts
262
- function parseScalar(raw) {
263
- const value = raw.trim();
264
- if (value.startsWith("[") && value.endsWith("]")) {
265
- return value.slice(1, -1).split(",").map((item) => item.trim().replace(/^["'](.*)["']$/u, "$1")).filter(Boolean);
266
- }
267
- return value.replace(/^["'](.*)["']$/u, "$1");
268
- }
269
- function readMetadata(source) {
270
- if (!source.startsWith("---")) return {};
271
- const end = source.indexOf("\n---", 3);
272
- if (end === -1) return {};
273
- const out = {};
274
- let inside = false;
275
- for (const line of source.slice(3, end).split("\n")) {
276
- if (/^metadata:\s*$/u.test(line)) {
277
- inside = true;
278
- continue;
279
- }
280
- if (!inside) continue;
281
- if (!/^\s/u.test(line)) break;
282
- const match = /^ {2}([A-Za-z][A-Za-z0-9_.-]*):\s*(.*)$/u.exec(line);
283
- if (!match) continue;
284
- const raw = match[2] ?? "";
285
- if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
286
- out[match[1]] = parseScalar(raw);
287
- }
288
- return out;
289
- }
290
- function readFrontmatterText(source) {
291
- if (!source.startsWith("---")) return "";
292
- const end = source.indexOf("\n---", 3);
293
- return end === -1 ? "" : source.slice(3, end).replace(/^\r?\n/u, "");
294
- }
295
- var asString = (value) => typeof value === "string" ? value : void 0;
296
- var asList = (value) => {
297
- if (value === void 0) return [];
298
- return typeof value === "string" ? [value] : value;
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
- }
445
- function readMarketplace(dir, onReject) {
446
- const manifestPath = join2(dir, ".claude-plugin", "marketplace.json");
447
- if (!existsSync2(manifestPath)) {
448
- onReject?.({
449
- path: dir,
450
- reason: "no .claude-plugin/marketplace.json here"
451
- });
452
- return [];
453
- }
454
- let manifest;
455
- try {
456
- manifest = JSON.parse(
457
- readFileSync2(manifestPath, "utf8")
458
- );
459
- } catch {
460
- onReject?.({
461
- path: manifestPath,
462
- reason: "marketplace.json is not valid JSON"
463
- });
464
- return [];
465
- }
466
- const marketplace = manifest.name ?? "marketplace";
467
- const found = [];
468
- for (const plugin of manifest.plugins ?? []) {
469
- if (!plugin.name || !plugin.source) continue;
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
- );
490
- }
491
- return found;
492
- }
493
- var assetRoot = (entry) => entry.root ?? dirname2(entry.path);
494
-
495
- // src/find.ts
496
- var DEFAULT_FIND_LIMIT = 5;
497
- function tokens(text) {
498
- return normaliseIntent(text).split(" ").filter(Boolean);
499
- }
500
- function bigrams(text) {
501
- const flat = normaliseIntent(text).replaceAll(" ", "");
502
- const out = /* @__PURE__ */ new Set();
503
- for (let i = 0; i + 1 < flat.length; i += 1) out.add(flat.slice(i, i + 2));
504
- return out;
505
- }
506
- function lexicalScore(intent, description) {
507
- const wanted = tokens(intent);
508
- if (wanted.length === 0) return 0;
509
- const have = new Set(tokens(description));
510
- const overlap = wanted.filter((token) => have.has(token)).length / wanted.length;
511
- const a = bigrams(intent);
512
- const b = bigrams(description);
513
- if (a.size === 0 || b.size === 0) return overlap / 2;
514
- let shared = 0;
515
- for (const gram of a) if (b.has(gram)) shared += 1;
516
- const dice = 2 * shared / (a.size + b.size);
517
- return (overlap + dice) / 2;
518
- }
519
- function cosine(a, b) {
520
- if (a.length !== b.length) return 0;
521
- let dot = 0;
522
- let na = 0;
523
- let nb = 0;
524
- for (let i = 0; i < a.length; i += 1) {
525
- dot += a[i] * b[i];
526
- na += a[i] * a[i];
527
- nb += b[i] * b[i];
528
- }
529
- if (na === 0 || nb === 0) return 0;
530
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
531
- }
532
- async function find(options) {
533
- const limit = options.limit ?? DEFAULT_FIND_LIMIT;
534
- const semantic = options.embedder !== void 0 && options.vectors !== void 0 && options.vectors.size > 0;
535
- if (semantic) {
536
- const embedder = options.embedder;
537
- const vectors = options.vectors;
538
- let query = options.cache?.get(options.intent) ?? null;
539
- if (!query) {
540
- query = await embedder.embed(options.intent);
541
- options.cache?.put(options.intent, query);
542
- }
543
- const scored = [];
544
- for (const entry of options.entries) {
545
- const vector = vectors.get(entry.name);
546
- if (!vector) continue;
547
- scored.push({
548
- name: entry.name,
549
- package: entry.package,
550
- description: entry.description,
551
- score: cosine(query, vector)
552
- });
553
- }
554
- if (scored.length === options.entries.length) {
555
- return {
556
- ranking: "semantic",
557
- matches: rank(scored, limit)
558
- };
559
- }
560
- }
561
- return {
562
- ranking: "lexical",
563
- matches: rank(
564
- options.entries.map((entry) => ({
565
- name: entry.name,
566
- package: entry.package,
567
- description: entry.description,
568
- score: lexicalScore(options.intent, entry.description)
569
- })),
570
- limit
571
- )
572
- };
573
- }
574
- function rank(matches2, limit) {
575
- return matches2.filter((match) => match.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
576
- }
577
-
578
- // src/server.ts
579
- import { createHash } from "crypto";
580
- import { readFileSync as readFileSync3, readdirSync as readdirSync3, realpathSync, statSync } from "fs";
581
- import {
582
- basename as basename3,
583
- dirname as dirname3,
584
- isAbsolute as isAbsolute2,
585
- join as join3,
586
- relative as relative2,
587
- resolve as resolve2,
588
- sep
589
- } from "path";
590
- var READ_BUDGET_BYTES = 256 * 1024;
591
- var DESCRIBE_FILE_LIMIT = 200;
592
- var CATALOG_PAGE_SIZE = 8;
593
- var CATALOG_PAGE_MAX = 20;
594
- function matches(entry, filters) {
595
- const kind = filters?.kind ?? "skill";
596
- if (kind !== "any" && kindOf(entry) !== kind) return false;
597
- if (!filters) return true;
598
- const meta = entry.metadata ?? {};
599
- const field = (key) => {
600
- const value = meta[key];
601
- return typeof value === "string" ? value : void 0;
602
- };
603
- if (filters.tier !== void 0 && entry.tier !== filters.tier) return false;
604
- if (filters.package !== void 0 && entry.package !== filters.package)
605
- return false;
606
- if (filters.level !== void 0 && field("level") !== filters.level)
607
- return false;
608
- if (filters.domain !== void 0 && field("domain") !== filters.domain)
609
- return false;
610
- if (filters.medium !== void 0 && field("medium") !== filters.medium)
611
- return false;
612
- return true;
613
- }
614
- var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
615
- function listFiles(root) {
616
- const files = [];
617
- if (statSync(root).isFile()) {
618
- const bytes = readFileSync3(root);
619
- const only = {
620
- path: basename3(root),
621
- bytes: bytes.length,
622
- sha256: sha256(bytes)
623
- };
624
- return { files: [only], truncated: false };
625
- }
626
- const stack = [root];
627
- while (stack.length > 0) {
628
- const dir = stack.pop();
629
- for (const entry of readdirSync3(dir, { withFileTypes: true })) {
630
- if (entry.name === "node_modules" || entry.name === ".git") continue;
631
- const full = join3(dir, entry.name);
632
- if (entry.isDirectory()) {
633
- stack.push(full);
634
- continue;
635
- }
636
- if (!entry.isFile()) continue;
637
- if (files.length >= DESCRIBE_FILE_LIMIT)
638
- return { files, truncated: true };
639
- const bytes = readFileSync3(full);
640
- files.push({
641
- path: relative2(root, full).split(sep).join("/"),
642
- bytes: bytes.length,
643
- sha256: sha256(bytes)
644
- });
645
- }
646
- }
647
- files.sort((a, b) => a.path.localeCompare(b.path));
648
- return { files, truncated: false };
649
- }
650
- 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
- }
658
- function createSkillTools(options) {
659
- const load = options.loadCatalog ?? readCatalog;
660
- let entries = null;
661
- const catalog = () => {
662
- entries ??= load({
663
- roots: options.roots,
664
- scopes: options.scopes,
665
- marketplaces: options.marketplaces,
666
- packages: options.packages,
667
- workspaces: options.workspaces,
668
- onReject: options.onReject
669
- });
670
- return entries;
671
- };
672
- return {
673
- /**
674
- * Grouped, and without bodies. The cheap overview and the search's miss
675
- * path. Paged by package so a large catalog never arrives in one answer.
676
- */
677
- catalog(request = {}) {
678
- const byPackage = /* @__PURE__ */ new Map();
679
- let count = 0;
680
- for (const entry of catalog()) {
681
- if (!matches(entry, { tier: request.tier, kind: request.kind }))
682
- continue;
683
- count += 1;
684
- const list = byPackage.get(entry.package) ?? [];
685
- list.push(catalogItem(entry));
686
- byPackage.set(entry.package, list);
687
- }
688
- const groups = [...byPackage.entries()].sort(
689
- ([a], [b]) => a.localeCompare(b)
690
- );
691
- const size = Math.min(
692
- Math.max(request.pageSize ?? CATALOG_PAGE_SIZE, 1),
693
- CATALOG_PAGE_MAX
694
- );
695
- const totalPages = Math.max(1, Math.ceil(groups.length / size));
696
- const page = Math.min(Math.max(request.page ?? 1, 1), totalPages);
697
- return {
698
- count,
699
- page,
700
- totalPages,
701
- hasNext: page < totalPages,
702
- packages: groups.slice((page - 1) * size, page * size).map(([pkg, skills]) => ({ package: pkg, skills }))
703
- };
704
- },
705
- /** Ranked names. Returns; never loads a body. */
706
- find(intent, limit, filters) {
707
- const scoped = catalog().filter((entry) => matches(entry, filters)).map(
708
- (entry) => entry.keywords && entry.keywords.length > 0 ? {
709
- ...entry,
710
- description: `${entry.description} ${entry.keywords.join(" ")}`
711
- } : entry
712
- );
713
- return find({
714
- entries: scoped,
715
- intent,
716
- limit,
717
- embedder: options.embedder,
718
- cache: options.cache,
719
- vectors: options.vectors
720
- }).then((result) => ({
721
- ...result,
722
- matches: result.matches.map((match) => ({
723
- ...match,
724
- description: catalog().find((entry) => entry.name === match.name)?.description ?? match.description
725
- }))
726
- }));
727
- },
728
- /** Every property of one skill and the files it carries, without bodies. */
729
- describe(name) {
730
- const entry = catalog().find((candidate) => candidate.name === name);
731
- if (!entry) return null;
732
- const source = readFileSync3(entry.path, "utf8");
733
- const listing = listFiles(assetRoot(entry));
734
- return {
735
- name: entry.name,
736
- kind: kindOf(entry),
737
- package: entry.package,
738
- description: entry.description,
739
- tier: entry.tier,
740
- metadata: entry.metadata ?? {},
741
- frontmatter: readFrontmatterText(source),
742
- files: listing.files,
743
- filesTruncated: listing.truncated
744
- };
745
- },
746
- /**
747
- * One file inside a skill's directory. Paths that leave the directory,
748
- * directly or through a link, are refused; nothing is ever executed.
749
- */
750
- read(name, path) {
751
- const entry = catalog().find((candidate) => candidate.name === name);
752
- if (!entry) return { ok: false, error: "unknown_skill", detail: name };
753
- const wanted = path ?? relative2(assetRoot(entry), entry.path);
754
- if (isAbsolute2(wanted) || wanted.split(/[\\/]/u).includes("..")) {
755
- return { ok: false, error: "path_outside_skill", detail: wanted };
756
- }
757
- const scope = realpathSync(assetRoot(entry));
758
- const single = statSync(scope).isFile();
759
- const root = single ? dirname3(scope) : scope;
760
- let target;
761
- try {
762
- target = realpathSync(
763
- single && wanted === "" ? scope : resolve2(root, wanted)
764
- );
765
- } catch {
766
- return { ok: false, error: "no_such_file", detail: wanted };
767
- }
768
- const inside = single ? target === scope : target === root || target.startsWith(root + sep);
769
- if (!inside) {
770
- return { ok: false, error: "path_outside_skill", detail: wanted };
771
- }
772
- const size = statSync(target).size;
773
- if (size > READ_BUDGET_BYTES) {
774
- return {
775
- ok: false,
776
- error: "over_budget",
777
- detail: `${size} bytes > ${READ_BUDGET_BYTES}`
778
- };
779
- }
780
- const bytes = readFileSync3(target);
781
- return {
782
- ok: true,
783
- name: entry.name,
784
- path: relative2(root, target).split(sep).join("/"),
785
- bytes: bytes.length,
786
- sha256: sha256(bytes),
787
- text: bytes.toString("utf8")
788
- };
789
- },
790
- /** One body, at the moment it is needed. */
791
- call(name) {
792
- const entry = catalog().find((candidate) => candidate.name === name);
793
- if (!entry) return null;
794
- const root = assetRoot(entry);
795
- const listing = listFiles(root);
796
- const own = relative2(root, entry.path).split(sep).join("/");
797
- return {
798
- name: entry.name,
799
- body: readBody(readFileSync3(entry.path, "utf8")),
800
- files: listing.files.filter(
801
- (file) => file.path !== own && file.path !== basename3(entry.path)
802
- ),
803
- filesTruncated: listing.truncated
804
- };
805
- },
806
- /** Drops the memoised catalog, so the next call re-reads the tree. */
807
- refresh() {
808
- entries = null;
809
- },
810
- entries: () => catalog().map(withoutBody),
811
- /** The catalog with paths, for the Skills extension. The same array until refresh. */
812
- skills: () => catalog()
813
- };
814
- }
815
-
816
- // src/skills-extension.ts
817
- import { createHash as createHash2 } from "crypto";
818
- import { readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
819
- import { basename as basename4, dirname as dirname4, join as join4, relative as relative3, sep as sep2 } from "path";
820
- import {
821
- ProtocolError,
822
- ProtocolErrorCode,
823
- ResourceNotFoundError,
824
- ResourceTemplate
825
- } from "@modelcontextprotocol/server";
826
- import { parse as parseYaml } from "yaml";
827
- import { z } from "zod";
828
- var SKILLS_EXTENSION = "io.modelcontextprotocol/skills";
829
- var AUTHORITY = "pmcp";
830
- var SKILL_NAME = /^[a-z0-9](?:[a-z0-9]|-(?!-))*[a-z0-9]$|^[a-z0-9]$/u;
831
- var MAX_RESOURCES = 512;
832
- var MAX_SKILL_BYTES = 16 * 1024 * 1024;
833
- var PAGE_SIZE = 50;
834
- var LIST_TTL_MS = 6e4;
835
- var digests = /* @__PURE__ */ new Map();
836
- function facts(path) {
837
- const stat = statSync2(path);
838
- const key = `${stat.mtimeMs}:${stat.size}`;
839
- const cached = digests.get(path);
840
- if (cached?.key === key) return cached;
841
- const digest = `sha256:${createHash2("sha256").update(readFileSync4(path)).digest("hex")}`;
842
- const fresh = { key, digest, size: stat.size };
843
- digests.set(path, fresh);
844
- return fresh;
845
- }
846
- function filesUnder(root) {
847
- const found = [];
848
- const stack = [root];
849
- while (stack.length > 0) {
850
- const dir = stack.pop();
851
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
852
- if (entry.name === "node_modules" || entry.name === ".git") continue;
853
- const full = join4(dir, entry.name);
854
- if (entry.isDirectory()) stack.push(full);
855
- else if (entry.isFile()) found.push(full);
856
- if (found.length > MAX_RESOURCES) return null;
857
- }
858
- }
859
- return found.sort();
860
- }
861
- function frontmatterObject(source) {
862
- if (!source.startsWith("---")) return null;
863
- const end = source.indexOf("\n---", 3);
864
- if (end === -1) return null;
865
- try {
866
- const parsed = parseYaml(source.slice(3, end));
867
- return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
868
- } catch {
869
- return null;
870
- }
871
- }
872
- var skillUri = (entry, file = "SKILL.md") => `skill://${AUTHORITY}/${entry.name}/${file}`;
873
- function extensionEntry(entry) {
874
- if (kindOf(entry) !== "skill" || basename4(entry.path) !== "SKILL.md")
875
- return null;
876
- const frontmatter = frontmatterObject(readFileSync4(entry.path, "utf8"));
877
- const name = frontmatter?.["name"];
878
- if (typeof name !== "string" || name !== entry.slug || !SKILL_NAME.test(name))
879
- return null;
880
- if (basename4(dirname4(entry.path)) !== name) return null;
881
- if (typeof frontmatter?.["description"] !== "string") return null;
882
- const root = dirname4(entry.path);
883
- const files = filesUnder(root);
884
- if (files === null) return null;
885
- let total = 0;
886
- const resources = files.map((path) => {
887
- const fact = facts(path);
888
- total += fact.size;
889
- const file = relative3(root, path).split(sep2).join("/");
890
- return { uri: skillUri(entry, file), digest: fact.digest, size: fact.size };
891
- });
892
- if (total > MAX_SKILL_BYTES) return null;
893
- return { uri: skillUri(entry), frontmatter, resources };
894
- }
895
- var MIME = {
896
- ".md": "text/markdown",
897
- ".json": "application/json",
898
- ".txt": "text/plain",
899
- ".py": "text/x-python",
900
- ".js": "text/javascript",
901
- ".mjs": "text/javascript",
902
- ".ts": "text/typescript",
903
- ".sh": "text/x-shellscript",
904
- ".yaml": "text/yaml",
905
- ".yml": "text/yaml",
906
- ".toml": "text/plain"
907
- };
908
- var mimeOf = (path) => {
909
- const dot = path.lastIndexOf(".");
910
- return dot === -1 ? void 0 : MIME[path.slice(dot).toLowerCase()];
911
- };
912
- function registerSkillsExtension(server, skills) {
913
- let memo = null;
914
- const entries = () => {
915
- const source = skills();
916
- if (memo?.source === source) return memo.entries;
917
- const map = /* @__PURE__ */ new Map();
918
- for (const entry of source) {
919
- let served = null;
920
- try {
921
- served = extensionEntry(entry);
922
- } catch {
923
- }
924
- if (served) map.set(served.uri, served);
925
- }
926
- memo = { source, entries: map };
927
- return map;
928
- };
929
- const pathOf = /* @__PURE__ */ new Map();
930
- const locate = (uri) => {
931
- if (pathOf.size === 0) {
932
- for (const entry of skills()) {
933
- if (kindOf(entry) === "skill" && basename4(entry.path) === "SKILL.md") {
934
- pathOf.set(skillUri(entry), dirname4(entry.path));
935
- }
936
- }
937
- }
938
- for (const [skill, root] of pathOf) {
939
- const prefix = skill.slice(0, -"SKILL.md".length);
940
- if (uri.startsWith(prefix)) return join4(root, uri.slice(prefix.length));
941
- }
942
- return void 0;
943
- };
944
- server.server.registerCapabilities({
945
- extensions: { [SKILLS_EXTENSION]: {} }
946
- });
947
- server.server.setRequestHandler(
948
- "skills/list",
949
- { params: z.object({ cursor: z.string().optional() }).passthrough() },
950
- async (params) => {
951
- const all = [...entries().values()];
952
- const start = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
953
- if (!Number.isInteger(start) || start < 0 || start > all.length) {
954
- throw new ProtocolError(
955
- ProtocolErrorCode.InvalidParams,
956
- "invalid cursor"
957
- );
958
- }
959
- const end = start + PAGE_SIZE;
960
- return {
961
- resultType: "complete",
962
- skills: all.slice(start, end),
963
- ...end < all.length ? { nextCursor: String(end) } : {},
964
- ttlMs: LIST_TTL_MS,
965
- cacheScope: "public"
966
- };
967
- }
968
- );
969
- server.server.setRequestHandler(
970
- "skills/get",
971
- { params: z.object({ uri: z.string() }).passthrough() },
972
- async (params) => {
973
- const skill = entries().get(params.uri);
974
- if (!skill) {
975
- throw new ProtocolError(
976
- ProtocolErrorCode.InvalidParams,
977
- `not a skill this server serves: ${params.uri}`
978
- );
979
- }
980
- return { resultType: "complete", skill };
981
- }
982
- );
983
- server.registerResource(
984
- "skill-files",
985
- new ResourceTemplate(`skill://${AUTHORITY}/{+path}`, { list: void 0 }),
986
- {
987
- title: "Skill files",
988
- description: "Files of the skills listed by skills/list."
989
- },
990
- async (uri) => {
991
- const href = uri.toString();
992
- const listed = [...entries().values()].some(
993
- (skill) => skill.resources.some((resource) => resource.uri === href)
994
- );
995
- const path = listed ? locate(href) : void 0;
996
- if (path === void 0) throw new ResourceNotFoundError(href);
997
- const bytes = readFileSync4(path);
998
- const mimeType = mimeOf(path);
999
- const text = mimeType !== void 0;
1000
- return {
1001
- contents: [
1002
- text ? { uri: href, mimeType, text: bytes.toString("utf8") } : { uri: href, blob: bytes.toString("base64") }
1003
- ]
1004
- };
1005
- }
1006
- );
1007
- }
1008
-
1009
- // src/mcp.ts
1010
- import { McpServer } from "@modelcontextprotocol/server";
1011
- import { z as z2 } from "zod";
1012
- var SERVER_NAME = "pmcp";
1013
- var readOnly = {
1014
- readOnlyHint: true,
1015
- destructiveHint: false,
1016
- idempotentHint: true,
1017
- openWorldHint: false
1018
- };
1019
- var readOnlyTool = (title) => ({
1020
- title,
1021
- annotations: { ...readOnly, title }
1022
- });
1023
- var kindInput = z2.enum(["skill", "agent", "hook", "mcp", "any"]).optional().describe(
1024
- "skill (default), agent, hook, mcp, or any. Commands are skills; agents, hooks and MCP servers are installed with pmcp add rather than read."
1025
- );
1026
- var asText = (value) => ({
1027
- content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
1028
- });
1029
- function registerSkillTools(server, tools) {
1030
- server.registerTool(
1031
- "skill_catalog",
1032
- {
1033
- ...readOnlyTool("List skills"),
1034
- description: "Every skill the installed packages and marketplaces ship, grouped by package or plugin, with names, one-line descriptions and tier, and no bodies. Paged by group. Use it to see what exists, or when skill_find returns nothing. Pass kind to list agents, hooks or MCP servers instead.",
1035
- inputSchema: z2.object({
1036
- page: z2.number().int().min(1).optional(),
1037
- pageSize: z2.number().int().min(1).max(20).optional(),
1038
- tier: z2.enum(["open", "free", "paid", "internal"]).optional(),
1039
- kind: kindInput
1040
- })
1041
- },
1042
- async (request) => asText(tools.catalog(request))
1043
- );
1044
- server.registerTool(
1045
- "skill_find",
1046
- {
1047
- ...readOnlyTool("Find skills"),
1048
- description: "Ranked skill names for an intent. Describe the problem in a full sentence in English rather than a keyword: a single word matches far worse, and the ranking is over descriptions. Returns names only -- read one with skill_call.",
1049
- inputSchema: z2.object({
1050
- intent: z2.string().min(1).max(256).describe("What you are trying to do, as an English sentence."),
1051
- limit: z2.number().int().min(1).max(25).optional(),
1052
- filters: z2.object({
1053
- kind: kindInput,
1054
- tier: z2.enum(["open", "free", "paid", "internal"]).optional(),
1055
- package: z2.string().optional(),
1056
- level: z2.string().optional(),
1057
- domain: z2.string().optional(),
1058
- medium: z2.string().optional()
1059
- }).optional().describe("Exact-match filters over the catalog before ranking.")
1060
- })
1061
- },
1062
- async ({ intent, limit, filters }) => asText(await tools.find(intent, limit, filters))
1063
- );
1064
- server.registerTool(
1065
- "skill_describe",
1066
- {
1067
- ...readOnlyTool("Describe a skill"),
1068
- description: "Frontmatter, metadata (tier, level, requirements, licence, approval), and the files the skill carries with sizes and sha256, without any body. Use it to decide between candidates before skill_call.",
1069
- inputSchema: z2.object({
1070
- name: z2.string().min(1).describe("The skill name skill_find or skill_catalog returned.")
1071
- })
1072
- },
1073
- async ({ name }) => {
1074
- const described = tools.describe(name);
1075
- if (!described) {
1076
- return {
1077
- ...asText({
1078
- error: "unknown_skill",
1079
- name,
1080
- hint: "Call skill_catalog for the names that exist."
1081
- }),
1082
- isError: true
1083
- };
1084
- }
1085
- return asText(described);
1086
- }
1087
- );
1088
- server.registerTool(
1089
- "skill_read",
1090
- {
1091
- ...readOnlyTool("Read a skill file"),
1092
- description: "A reference or script file inside one skill's directory in this catalog, as text, with its sha256. It reads only files the catalog ships and calls no external API; path is relative to the skill, as skill_call and skill_describe list it (layout: https://pmcp.build). Scripts are returned as text, never run. Files over 256 KiB and paths outside the skill are refused.",
1093
- inputSchema: z2.object({
1094
- name: z2.string().min(1),
1095
- path: z2.string().min(1).optional().describe(
1096
- "Path relative to the skill directory, as skill_describe lists it. Defaults to SKILL.md."
1097
- )
1098
- })
1099
- },
1100
- async ({ name, path }) => {
1101
- const result = tools.read(name, path);
1102
- if (!result.ok) return { ...asText(result), isError: true };
1103
- const { text, ...header } = result;
1104
- return {
1105
- content: [
1106
- { type: "text", text: JSON.stringify(header) },
1107
- { type: "text", text }
1108
- ]
1109
- };
1110
- }
1111
- );
1112
- server.registerTool(
1113
- "skill_call",
1114
- {
1115
- ...readOnlyTool("Read a skill"),
1116
- description: "The body of a single skill, by the name skill_find or skill_catalog returned, followed by a list of the reference and script files the skill carries, if any. This is the only call that costs a document.",
1117
- inputSchema: z2.object({
1118
- name: z2.string().min(1).describe("The skill name, as <package>/<slug>.")
1119
- })
1120
- },
1121
- async ({ name }) => {
1122
- const found = tools.call(name);
1123
- if (!found) {
1124
- return {
1125
- ...asText({
1126
- error: "unknown_skill",
1127
- name,
1128
- hint: "Call skill_catalog for the names that exist."
1129
- }),
1130
- isError: true
1131
- };
1132
- }
1133
- if (found.files.length === 0) {
1134
- return { content: [{ type: "text", text: found.body }] };
1135
- }
1136
- const lines = found.files.map(
1137
- (file) => `- ${file.path} (${file.bytes} bytes, sha256 ${file.sha256.slice(0, 12)})`
1138
- );
1139
- if (found.filesTruncated)
1140
- lines.push("- (list cut; skill_describe has the rest)");
1141
- const manifest = [
1142
- `Files in skill ${found.name}, each returned by skill_read with this name and the path:`,
1143
- ...lines
1144
- ].join("\n");
1145
- return {
1146
- content: [
1147
- { type: "text", text: found.body },
1148
- { type: "text", text: manifest }
1149
- ]
1150
- };
1151
- }
1152
- );
1153
- return server;
1154
- }
1155
- function createSkillServer(options) {
1156
- const server = new McpServer({ name: SERVER_NAME, version: "0.7.1" });
1157
- const tools = createSkillTools(options);
1158
- registerSkillsExtension(server, tools.skills);
1159
- return registerSkillTools(server, tools);
1160
- }
1161
-
1162
- export {
1163
- normaliseIntent,
1164
- readMetadata,
1165
- readFrontmatterText,
1166
- readMarketplace,
1167
- assetRoot,
1168
- ASSET_KINDS,
1169
- kindOf,
1170
- SKIPPED_DIRS,
1171
- MAX_SKILL_DEPTH,
1172
- readFrontmatter,
1173
- readBody,
1174
- readCatalog,
1175
- withoutBody,
1176
- displayPath,
1177
- DEFAULT_FIND_LIMIT,
1178
- lexicalScore,
1179
- cosine,
1180
- find,
1181
- READ_BUDGET_BYTES,
1182
- DESCRIBE_FILE_LIMIT,
1183
- CATALOG_PAGE_SIZE,
1184
- CATALOG_PAGE_MAX,
1185
- createSkillTools,
1186
- SKILLS_EXTENSION,
1187
- skillUri,
1188
- extensionEntry,
1189
- registerSkillsExtension,
1190
- SERVER_NAME,
1191
- registerSkillTools,
1192
- createSkillServer
1193
- };
1194
- //# sourceMappingURL=chunk-C46YCY2I.js.map