@modootoday/pmcp 0.3.0 → 0.5.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.
- package/README.md +57 -2
- package/dist/{catalog-BiPLcbLu.d.ts → catalog-5v7jrGwP.d.ts} +12 -1
- package/dist/{chunk-SPWHSHOC.js → chunk-JOQTOVTJ.js} +451 -80
- package/dist/{chunk-MBAXK4IZ.js → chunk-PUGLIJYM.js} +62 -3
- package/dist/{chunk-QJS3NYLT.js → chunk-X4LYYULW.js} +2 -2
- package/dist/cli.js +67 -17
- package/dist/http.d.ts +34 -0
- package/dist/http.js +65 -0
- package/dist/index.d.ts +74 -195
- package/dist/index.js +25 -3
- package/dist/server-Dm71qZgM.d.ts +207 -0
- package/dist/stdio.d.ts +1 -1
- package/dist/stdio.js +2 -2
- package/package.json +8 -2
|
@@ -5,11 +5,13 @@ function normaliseIntent(intent) {
|
|
|
5
5
|
|
|
6
6
|
// src/marketplace.ts
|
|
7
7
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
8
|
-
import { join as join2, resolve } from "path";
|
|
8
|
+
import { basename as basename2, dirname as dirname2, join as join2, resolve } from "path";
|
|
9
9
|
|
|
10
10
|
// src/catalog.ts
|
|
11
11
|
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
12
12
|
import { basename, dirname, isAbsolute, join, relative } from "path";
|
|
13
|
+
var ASSET_KINDS = ["skill", "agent", "hook", "mcp"];
|
|
14
|
+
var kindOf = (entry) => entry.kind ?? "skill";
|
|
13
15
|
var SKIPPED_DIRS = /* @__PURE__ */ new Set([
|
|
14
16
|
"node_modules",
|
|
15
17
|
".git",
|
|
@@ -295,6 +297,151 @@ var asList = (value) => {
|
|
|
295
297
|
if (value === void 0) return [];
|
|
296
298
|
return typeof value === "string" ? [value] : value;
|
|
297
299
|
};
|
|
300
|
+
function readJson(path) {
|
|
301
|
+
try {
|
|
302
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
303
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function markdownEntry(context, path, fallbackSlug, shape) {
|
|
309
|
+
const source = readFileSync2(path, "utf8");
|
|
310
|
+
const front = readFrontmatter(source);
|
|
311
|
+
const description = front["description"] ?? "";
|
|
312
|
+
if (description === "") {
|
|
313
|
+
context.onReject?.({ path, reason: "no description" });
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const metadata = readMetadata(source);
|
|
317
|
+
const ownTier = asString(metadata["tier"]);
|
|
318
|
+
if (context.tier !== void 0 && ownTier !== void 0 && ownTier !== context.tier) {
|
|
319
|
+
context.onReject?.({
|
|
320
|
+
path,
|
|
321
|
+
reason: `metadata.tier ${ownTier} in a ${context.tier} marketplace`
|
|
322
|
+
});
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const slug = front["name"] ?? fallbackSlug;
|
|
326
|
+
const prefix = `${context.marketplace}/${context.plugin}`;
|
|
327
|
+
return {
|
|
328
|
+
name: shape.namespace ? `${prefix}/${shape.namespace}/${slug}` : `${prefix}/${slug}`,
|
|
329
|
+
package: prefix,
|
|
330
|
+
slug,
|
|
331
|
+
description,
|
|
332
|
+
path,
|
|
333
|
+
tier: ownTier ?? context.tier,
|
|
334
|
+
metadata,
|
|
335
|
+
keywords: asList(metadata["keywords"]),
|
|
336
|
+
...shape.kind ? { kind: shape.kind } : {},
|
|
337
|
+
...shape.root ? { root: shape.root } : {}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function skillEntries(context) {
|
|
341
|
+
const dir = join2(context.pluginDir, "skills");
|
|
342
|
+
if (!existsSync2(dir)) return [];
|
|
343
|
+
const found = [];
|
|
344
|
+
for (const child of readdirSync2(dir, { withFileTypes: true })) {
|
|
345
|
+
if (!child.isDirectory()) continue;
|
|
346
|
+
const path = join2(dir, child.name, "SKILL.md");
|
|
347
|
+
if (!existsSync2(path)) continue;
|
|
348
|
+
const entry = markdownEntry(context, path, child.name, {});
|
|
349
|
+
if (entry) found.push(entry);
|
|
350
|
+
}
|
|
351
|
+
return found;
|
|
352
|
+
}
|
|
353
|
+
function commandEntries(context) {
|
|
354
|
+
const dir = join2(context.pluginDir, "commands");
|
|
355
|
+
if (!existsSync2(dir)) return [];
|
|
356
|
+
const found = [];
|
|
357
|
+
for (const child of readdirSync2(dir, { withFileTypes: true })) {
|
|
358
|
+
if (!child.isFile() || !child.name.endsWith(".md")) continue;
|
|
359
|
+
const path = join2(dir, child.name);
|
|
360
|
+
const entry = markdownEntry(context, path, basename2(child.name, ".md"), {
|
|
361
|
+
root: path
|
|
362
|
+
});
|
|
363
|
+
if (entry) found.push(entry);
|
|
364
|
+
}
|
|
365
|
+
return found;
|
|
366
|
+
}
|
|
367
|
+
function agentEntries(context) {
|
|
368
|
+
const dir = join2(context.pluginDir, "agents");
|
|
369
|
+
if (!existsSync2(dir)) return [];
|
|
370
|
+
const found = [];
|
|
371
|
+
for (const child of readdirSync2(dir, { withFileTypes: true })) {
|
|
372
|
+
let entry = null;
|
|
373
|
+
if (child.isFile() && child.name.endsWith(".md")) {
|
|
374
|
+
const path = join2(dir, child.name);
|
|
375
|
+
entry = markdownEntry(context, path, basename2(child.name, ".md"), {
|
|
376
|
+
kind: "agent",
|
|
377
|
+
root: path,
|
|
378
|
+
namespace: "agents"
|
|
379
|
+
});
|
|
380
|
+
} else if (child.isDirectory() && existsSync2(join2(dir, child.name, "agent.md"))) {
|
|
381
|
+
entry = markdownEntry(
|
|
382
|
+
context,
|
|
383
|
+
join2(dir, child.name, "agent.md"),
|
|
384
|
+
child.name,
|
|
385
|
+
{
|
|
386
|
+
kind: "agent",
|
|
387
|
+
namespace: "agents"
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (entry) found.push(entry);
|
|
392
|
+
}
|
|
393
|
+
return found;
|
|
394
|
+
}
|
|
395
|
+
function hookEntries(context, manifest) {
|
|
396
|
+
const declared = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
|
|
397
|
+
const path = resolve(context.pluginDir, declared ?? "hooks/hooks.json");
|
|
398
|
+
if (!existsSync2(path)) return [];
|
|
399
|
+
const hooks = readJson(path);
|
|
400
|
+
if (hooks === null) {
|
|
401
|
+
context.onReject?.({ path, reason: "hooks.json is not a JSON object" });
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
const own = typeof hooks["description"] === "string" ? hooks["description"] : void 0;
|
|
405
|
+
const description = own ?? context.pluginDescription ?? "";
|
|
406
|
+
if (description === "") {
|
|
407
|
+
context.onReject?.({ path, reason: "no description" });
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
const prefix = `${context.marketplace}/${context.plugin}`;
|
|
411
|
+
return [
|
|
412
|
+
{
|
|
413
|
+
name: `${prefix}/hooks`,
|
|
414
|
+
package: prefix,
|
|
415
|
+
slug: "hooks",
|
|
416
|
+
description,
|
|
417
|
+
path,
|
|
418
|
+
tier: context.tier,
|
|
419
|
+
kind: "hook",
|
|
420
|
+
root: context.pluginDir
|
|
421
|
+
}
|
|
422
|
+
];
|
|
423
|
+
}
|
|
424
|
+
function mcpEntries(context) {
|
|
425
|
+
const path = join2(context.pluginDir, ".mcp.json");
|
|
426
|
+
if (!existsSync2(path)) return [];
|
|
427
|
+
const config = readJson(path);
|
|
428
|
+
const servers = config?.["mcpServers"];
|
|
429
|
+
if (servers === null || typeof servers !== "object" || Array.isArray(servers)) {
|
|
430
|
+
context.onReject?.({ path, reason: ".mcp.json has no mcpServers object" });
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
const prefix = `${context.marketplace}/${context.plugin}`;
|
|
434
|
+
return Object.keys(servers).map((server) => ({
|
|
435
|
+
name: `${prefix}/mcp/${server}`,
|
|
436
|
+
package: prefix,
|
|
437
|
+
slug: server,
|
|
438
|
+
description: context.pluginDescription ? `MCP server ${server}. ${context.pluginDescription}` : `MCP server ${server} from plugin ${context.plugin}.`,
|
|
439
|
+
path,
|
|
440
|
+
tier: context.tier,
|
|
441
|
+
kind: "mcp",
|
|
442
|
+
root: path
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
298
445
|
function readMarketplace(dir, onReject) {
|
|
299
446
|
const manifestPath = join2(dir, ".claude-plugin", "marketplace.json");
|
|
300
447
|
if (!existsSync2(manifestPath)) {
|
|
@@ -317,47 +464,33 @@ function readMarketplace(dir, onReject) {
|
|
|
317
464
|
return [];
|
|
318
465
|
}
|
|
319
466
|
const marketplace = manifest.name ?? "marketplace";
|
|
320
|
-
const tier = manifest.metadata?.tier;
|
|
321
467
|
const found = [];
|
|
322
468
|
for (const plugin of manifest.plugins ?? []) {
|
|
323
469
|
if (!plugin.name || !plugin.source) continue;
|
|
324
|
-
const
|
|
325
|
-
if (!existsSync2(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
const slug = front["name"] ?? child.name;
|
|
347
|
-
found.push({
|
|
348
|
-
name: `${marketplace}/${plugin.name}/${slug}`,
|
|
349
|
-
package: `${marketplace}/${plugin.name}`,
|
|
350
|
-
slug,
|
|
351
|
-
description,
|
|
352
|
-
path,
|
|
353
|
-
tier: ownTier ?? tier,
|
|
354
|
-
metadata,
|
|
355
|
-
keywords: asList(metadata["keywords"])
|
|
356
|
-
});
|
|
357
|
-
}
|
|
470
|
+
const pluginDir = resolve(dir, plugin.source);
|
|
471
|
+
if (!existsSync2(pluginDir)) continue;
|
|
472
|
+
const pluginManifest = readJson(
|
|
473
|
+
join2(pluginDir, ".claude-plugin", "plugin.json")
|
|
474
|
+
) ?? {};
|
|
475
|
+
const context = {
|
|
476
|
+
marketplace,
|
|
477
|
+
plugin: plugin.name,
|
|
478
|
+
pluginDir,
|
|
479
|
+
pluginDescription: typeof pluginManifest.description === "string" ? pluginManifest.description : void 0,
|
|
480
|
+
tier: manifest.metadata?.tier,
|
|
481
|
+
onReject
|
|
482
|
+
};
|
|
483
|
+
found.push(
|
|
484
|
+
...skillEntries(context),
|
|
485
|
+
...commandEntries(context),
|
|
486
|
+
...agentEntries(context),
|
|
487
|
+
...hookEntries(context, pluginManifest),
|
|
488
|
+
...mcpEntries(context)
|
|
489
|
+
);
|
|
358
490
|
}
|
|
359
491
|
return found;
|
|
360
492
|
}
|
|
493
|
+
var assetRoot = (entry) => entry.root ?? dirname2(entry.path);
|
|
361
494
|
|
|
362
495
|
// src/find.ts
|
|
363
496
|
var DEFAULT_FIND_LIMIT = 5;
|
|
@@ -445,12 +578,22 @@ function rank(matches2, limit) {
|
|
|
445
578
|
// src/server.ts
|
|
446
579
|
import { createHash } from "crypto";
|
|
447
580
|
import { readFileSync as readFileSync3, readdirSync as readdirSync3, realpathSync, statSync } from "fs";
|
|
448
|
-
import {
|
|
581
|
+
import {
|
|
582
|
+
basename as basename3,
|
|
583
|
+
dirname as dirname3,
|
|
584
|
+
isAbsolute as isAbsolute2,
|
|
585
|
+
join as join3,
|
|
586
|
+
relative as relative2,
|
|
587
|
+
resolve as resolve2,
|
|
588
|
+
sep
|
|
589
|
+
} from "path";
|
|
449
590
|
var READ_BUDGET_BYTES = 256 * 1024;
|
|
450
591
|
var DESCRIBE_FILE_LIMIT = 200;
|
|
451
592
|
var CATALOG_PAGE_SIZE = 8;
|
|
452
593
|
var CATALOG_PAGE_MAX = 20;
|
|
453
594
|
function matches(entry, filters) {
|
|
595
|
+
const kind = filters?.kind ?? "skill";
|
|
596
|
+
if (kind !== "any" && kindOf(entry) !== kind) return false;
|
|
454
597
|
if (!filters) return true;
|
|
455
598
|
const meta = entry.metadata ?? {};
|
|
456
599
|
const field = (key) => {
|
|
@@ -471,6 +614,15 @@ function matches(entry, filters) {
|
|
|
471
614
|
var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
472
615
|
function listFiles(root) {
|
|
473
616
|
const files = [];
|
|
617
|
+
if (statSync(root).isFile()) {
|
|
618
|
+
const bytes = readFileSync3(root);
|
|
619
|
+
const only = {
|
|
620
|
+
path: basename3(root),
|
|
621
|
+
bytes: bytes.length,
|
|
622
|
+
sha256: sha256(bytes)
|
|
623
|
+
};
|
|
624
|
+
return { files: [only], truncated: false };
|
|
625
|
+
}
|
|
474
626
|
const stack = [root];
|
|
475
627
|
while (stack.length > 0) {
|
|
476
628
|
const dir = stack.pop();
|
|
@@ -495,6 +647,14 @@ function listFiles(root) {
|
|
|
495
647
|
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
496
648
|
return { files, truncated: false };
|
|
497
649
|
}
|
|
650
|
+
function catalogItem(entry) {
|
|
651
|
+
return {
|
|
652
|
+
name: entry.name,
|
|
653
|
+
description: entry.description,
|
|
654
|
+
...entry.tier === void 0 ? {} : { tier: entry.tier },
|
|
655
|
+
...kindOf(entry) === "skill" ? {} : { kind: kindOf(entry) }
|
|
656
|
+
};
|
|
657
|
+
}
|
|
498
658
|
function createSkillTools(options) {
|
|
499
659
|
const load = options.loadCatalog ?? readCatalog;
|
|
500
660
|
let entries = null;
|
|
@@ -518,16 +678,11 @@ function createSkillTools(options) {
|
|
|
518
678
|
const byPackage = /* @__PURE__ */ new Map();
|
|
519
679
|
let count = 0;
|
|
520
680
|
for (const entry of catalog()) {
|
|
521
|
-
if (!matches(entry, { tier: request.tier }))
|
|
681
|
+
if (!matches(entry, { tier: request.tier, kind: request.kind }))
|
|
682
|
+
continue;
|
|
522
683
|
count += 1;
|
|
523
684
|
const list = byPackage.get(entry.package) ?? [];
|
|
524
|
-
list.push(
|
|
525
|
-
entry.tier === void 0 ? { name: entry.name, description: entry.description } : {
|
|
526
|
-
name: entry.name,
|
|
527
|
-
description: entry.description,
|
|
528
|
-
tier: entry.tier
|
|
529
|
-
}
|
|
530
|
-
);
|
|
685
|
+
list.push(catalogItem(entry));
|
|
531
686
|
byPackage.set(entry.package, list);
|
|
532
687
|
}
|
|
533
688
|
const groups = [...byPackage.entries()].sort(
|
|
@@ -575,9 +730,10 @@ function createSkillTools(options) {
|
|
|
575
730
|
const entry = catalog().find((candidate) => candidate.name === name);
|
|
576
731
|
if (!entry) return null;
|
|
577
732
|
const source = readFileSync3(entry.path, "utf8");
|
|
578
|
-
const listing = listFiles(
|
|
733
|
+
const listing = listFiles(assetRoot(entry));
|
|
579
734
|
return {
|
|
580
735
|
name: entry.name,
|
|
736
|
+
kind: kindOf(entry),
|
|
581
737
|
package: entry.package,
|
|
582
738
|
description: entry.description,
|
|
583
739
|
tier: entry.tier,
|
|
@@ -591,21 +747,27 @@ function createSkillTools(options) {
|
|
|
591
747
|
* One file inside a skill's directory. Paths that leave the directory,
|
|
592
748
|
* directly or through a link, are refused; nothing is ever executed.
|
|
593
749
|
*/
|
|
594
|
-
read(name, path
|
|
750
|
+
read(name, path) {
|
|
595
751
|
const entry = catalog().find((candidate) => candidate.name === name);
|
|
596
752
|
if (!entry) return { ok: false, error: "unknown_skill", detail: name };
|
|
597
|
-
|
|
598
|
-
|
|
753
|
+
const wanted = path ?? relative2(assetRoot(entry), entry.path);
|
|
754
|
+
if (isAbsolute2(wanted) || wanted.split(/[\\/]/u).includes("..")) {
|
|
755
|
+
return { ok: false, error: "path_outside_skill", detail: wanted };
|
|
599
756
|
}
|
|
600
|
-
const
|
|
757
|
+
const scope = realpathSync(assetRoot(entry));
|
|
758
|
+
const single = statSync(scope).isFile();
|
|
759
|
+
const root = single ? dirname3(scope) : scope;
|
|
601
760
|
let target;
|
|
602
761
|
try {
|
|
603
|
-
target = realpathSync(
|
|
762
|
+
target = realpathSync(
|
|
763
|
+
single && wanted === "" ? scope : resolve2(root, wanted)
|
|
764
|
+
);
|
|
604
765
|
} catch {
|
|
605
|
-
return { ok: false, error: "no_such_file", detail:
|
|
766
|
+
return { ok: false, error: "no_such_file", detail: wanted };
|
|
606
767
|
}
|
|
607
|
-
|
|
608
|
-
|
|
768
|
+
const inside = single ? target === scope : target === root || target.startsWith(root + sep);
|
|
769
|
+
if (!inside) {
|
|
770
|
+
return { ok: false, error: "path_outside_skill", detail: wanted };
|
|
609
771
|
}
|
|
610
772
|
const size = statSync(target).size;
|
|
611
773
|
if (size > READ_BUDGET_BYTES) {
|
|
@@ -638,13 +800,208 @@ function createSkillTools(options) {
|
|
|
638
800
|
refresh() {
|
|
639
801
|
entries = null;
|
|
640
802
|
},
|
|
641
|
-
entries: () => catalog().map(withoutBody)
|
|
803
|
+
entries: () => catalog().map(withoutBody),
|
|
804
|
+
/** The catalog with paths, for the Skills extension. The same array until refresh. */
|
|
805
|
+
skills: () => catalog()
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/skills-extension.ts
|
|
810
|
+
import { createHash as createHash2 } from "crypto";
|
|
811
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
812
|
+
import { basename as basename4, dirname as dirname4, join as join4, relative as relative3, sep as sep2 } from "path";
|
|
813
|
+
import {
|
|
814
|
+
ProtocolError,
|
|
815
|
+
ProtocolErrorCode,
|
|
816
|
+
ResourceNotFoundError,
|
|
817
|
+
ResourceTemplate
|
|
818
|
+
} from "@modelcontextprotocol/server";
|
|
819
|
+
import { parse as parseYaml } from "yaml";
|
|
820
|
+
import { z } from "zod";
|
|
821
|
+
var SKILLS_EXTENSION = "io.modelcontextprotocol/skills";
|
|
822
|
+
var AUTHORITY = "pmcp";
|
|
823
|
+
var SKILL_NAME = /^[a-z0-9](?:[a-z0-9]|-(?!-))*[a-z0-9]$|^[a-z0-9]$/u;
|
|
824
|
+
var MAX_RESOURCES = 512;
|
|
825
|
+
var MAX_SKILL_BYTES = 16 * 1024 * 1024;
|
|
826
|
+
var PAGE_SIZE = 50;
|
|
827
|
+
var LIST_TTL_MS = 6e4;
|
|
828
|
+
var digests = /* @__PURE__ */ new Map();
|
|
829
|
+
function facts(path) {
|
|
830
|
+
const stat = statSync2(path);
|
|
831
|
+
const key = `${stat.mtimeMs}:${stat.size}`;
|
|
832
|
+
const cached = digests.get(path);
|
|
833
|
+
if (cached?.key === key) return cached;
|
|
834
|
+
const digest = `sha256:${createHash2("sha256").update(readFileSync4(path)).digest("hex")}`;
|
|
835
|
+
const fresh = { key, digest, size: stat.size };
|
|
836
|
+
digests.set(path, fresh);
|
|
837
|
+
return fresh;
|
|
838
|
+
}
|
|
839
|
+
function filesUnder(root) {
|
|
840
|
+
const found = [];
|
|
841
|
+
const stack = [root];
|
|
842
|
+
while (stack.length > 0) {
|
|
843
|
+
const dir = stack.pop();
|
|
844
|
+
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
845
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
846
|
+
const full = join4(dir, entry.name);
|
|
847
|
+
if (entry.isDirectory()) stack.push(full);
|
|
848
|
+
else if (entry.isFile()) found.push(full);
|
|
849
|
+
if (found.length > MAX_RESOURCES) return null;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return found.sort();
|
|
853
|
+
}
|
|
854
|
+
function frontmatterObject(source) {
|
|
855
|
+
if (!source.startsWith("---")) return null;
|
|
856
|
+
const end = source.indexOf("\n---", 3);
|
|
857
|
+
if (end === -1) return null;
|
|
858
|
+
try {
|
|
859
|
+
const parsed = parseYaml(source.slice(3, end));
|
|
860
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
861
|
+
} catch {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
var skillUri = (entry, file = "SKILL.md") => `skill://${AUTHORITY}/${entry.name}/${file}`;
|
|
866
|
+
function extensionEntry(entry) {
|
|
867
|
+
if (kindOf(entry) !== "skill" || basename4(entry.path) !== "SKILL.md")
|
|
868
|
+
return null;
|
|
869
|
+
const frontmatter = frontmatterObject(readFileSync4(entry.path, "utf8"));
|
|
870
|
+
const name = frontmatter?.["name"];
|
|
871
|
+
if (typeof name !== "string" || name !== entry.slug || !SKILL_NAME.test(name))
|
|
872
|
+
return null;
|
|
873
|
+
if (basename4(dirname4(entry.path)) !== name) return null;
|
|
874
|
+
if (typeof frontmatter?.["description"] !== "string") return null;
|
|
875
|
+
const root = dirname4(entry.path);
|
|
876
|
+
const files = filesUnder(root);
|
|
877
|
+
if (files === null) return null;
|
|
878
|
+
let total = 0;
|
|
879
|
+
const resources = files.map((path) => {
|
|
880
|
+
const fact = facts(path);
|
|
881
|
+
total += fact.size;
|
|
882
|
+
const file = relative3(root, path).split(sep2).join("/");
|
|
883
|
+
return { uri: skillUri(entry, file), digest: fact.digest, size: fact.size };
|
|
884
|
+
});
|
|
885
|
+
if (total > MAX_SKILL_BYTES) return null;
|
|
886
|
+
return { uri: skillUri(entry), frontmatter, resources };
|
|
887
|
+
}
|
|
888
|
+
var MIME = {
|
|
889
|
+
".md": "text/markdown",
|
|
890
|
+
".json": "application/json",
|
|
891
|
+
".txt": "text/plain",
|
|
892
|
+
".py": "text/x-python",
|
|
893
|
+
".js": "text/javascript",
|
|
894
|
+
".mjs": "text/javascript",
|
|
895
|
+
".ts": "text/typescript",
|
|
896
|
+
".sh": "text/x-shellscript",
|
|
897
|
+
".yaml": "text/yaml",
|
|
898
|
+
".yml": "text/yaml",
|
|
899
|
+
".toml": "text/plain"
|
|
900
|
+
};
|
|
901
|
+
var mimeOf = (path) => {
|
|
902
|
+
const dot = path.lastIndexOf(".");
|
|
903
|
+
return dot === -1 ? void 0 : MIME[path.slice(dot).toLowerCase()];
|
|
904
|
+
};
|
|
905
|
+
function registerSkillsExtension(server, skills) {
|
|
906
|
+
let memo = null;
|
|
907
|
+
const entries = () => {
|
|
908
|
+
const source = skills();
|
|
909
|
+
if (memo?.source === source) return memo.entries;
|
|
910
|
+
const map = /* @__PURE__ */ new Map();
|
|
911
|
+
for (const entry of source) {
|
|
912
|
+
let served = null;
|
|
913
|
+
try {
|
|
914
|
+
served = extensionEntry(entry);
|
|
915
|
+
} catch {
|
|
916
|
+
}
|
|
917
|
+
if (served) map.set(served.uri, served);
|
|
918
|
+
}
|
|
919
|
+
memo = { source, entries: map };
|
|
920
|
+
return map;
|
|
921
|
+
};
|
|
922
|
+
const pathOf = /* @__PURE__ */ new Map();
|
|
923
|
+
const locate = (uri) => {
|
|
924
|
+
if (pathOf.size === 0) {
|
|
925
|
+
for (const entry of skills()) {
|
|
926
|
+
if (kindOf(entry) === "skill" && basename4(entry.path) === "SKILL.md") {
|
|
927
|
+
pathOf.set(skillUri(entry), dirname4(entry.path));
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
for (const [skill, root] of pathOf) {
|
|
932
|
+
const prefix = skill.slice(0, -"SKILL.md".length);
|
|
933
|
+
if (uri.startsWith(prefix)) return join4(root, uri.slice(prefix.length));
|
|
934
|
+
}
|
|
935
|
+
return void 0;
|
|
642
936
|
};
|
|
937
|
+
server.server.registerCapabilities({
|
|
938
|
+
extensions: { [SKILLS_EXTENSION]: {} }
|
|
939
|
+
});
|
|
940
|
+
server.server.setRequestHandler(
|
|
941
|
+
"skills/list",
|
|
942
|
+
{ params: z.object({ cursor: z.string().optional() }).passthrough() },
|
|
943
|
+
async (params) => {
|
|
944
|
+
const all = [...entries().values()];
|
|
945
|
+
const start = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
|
|
946
|
+
if (!Number.isInteger(start) || start < 0 || start > all.length) {
|
|
947
|
+
throw new ProtocolError(
|
|
948
|
+
ProtocolErrorCode.InvalidParams,
|
|
949
|
+
"invalid cursor"
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
const end = start + PAGE_SIZE;
|
|
953
|
+
return {
|
|
954
|
+
resultType: "complete",
|
|
955
|
+
skills: all.slice(start, end),
|
|
956
|
+
...end < all.length ? { nextCursor: String(end) } : {},
|
|
957
|
+
ttlMs: LIST_TTL_MS,
|
|
958
|
+
cacheScope: "public"
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
);
|
|
962
|
+
server.server.setRequestHandler(
|
|
963
|
+
"skills/get",
|
|
964
|
+
{ params: z.object({ uri: z.string() }).passthrough() },
|
|
965
|
+
async (params) => {
|
|
966
|
+
const skill = entries().get(params.uri);
|
|
967
|
+
if (!skill) {
|
|
968
|
+
throw new ProtocolError(
|
|
969
|
+
ProtocolErrorCode.InvalidParams,
|
|
970
|
+
`not a skill this server serves: ${params.uri}`
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
return { resultType: "complete", skill };
|
|
974
|
+
}
|
|
975
|
+
);
|
|
976
|
+
server.registerResource(
|
|
977
|
+
"skill-files",
|
|
978
|
+
new ResourceTemplate(`skill://${AUTHORITY}/{+path}`, { list: void 0 }),
|
|
979
|
+
{
|
|
980
|
+
title: "Skill files",
|
|
981
|
+
description: "Files of the skills listed by skills/list."
|
|
982
|
+
},
|
|
983
|
+
async (uri) => {
|
|
984
|
+
const href = uri.toString();
|
|
985
|
+
const listed = [...entries().values()].some(
|
|
986
|
+
(skill) => skill.resources.some((resource) => resource.uri === href)
|
|
987
|
+
);
|
|
988
|
+
const path = listed ? locate(href) : void 0;
|
|
989
|
+
if (path === void 0) throw new ResourceNotFoundError(href);
|
|
990
|
+
const bytes = readFileSync4(path);
|
|
991
|
+
const mimeType = mimeOf(path);
|
|
992
|
+
const text = mimeType !== void 0;
|
|
993
|
+
return {
|
|
994
|
+
contents: [
|
|
995
|
+
text ? { uri: href, mimeType, text: bytes.toString("utf8") } : { uri: href, blob: bytes.toString("base64") }
|
|
996
|
+
]
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
);
|
|
643
1000
|
}
|
|
644
1001
|
|
|
645
1002
|
// src/mcp.ts
|
|
646
1003
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
647
|
-
import { z } from "zod";
|
|
1004
|
+
import { z as z2 } from "zod";
|
|
648
1005
|
var SERVER_NAME = "pmcp";
|
|
649
1006
|
var readOnly = {
|
|
650
1007
|
readOnlyHint: true,
|
|
@@ -652,6 +1009,9 @@ var readOnly = {
|
|
|
652
1009
|
idempotentHint: true,
|
|
653
1010
|
openWorldHint: false
|
|
654
1011
|
};
|
|
1012
|
+
var kindInput = z2.enum(["skill", "agent", "hook", "mcp", "any"]).optional().describe(
|
|
1013
|
+
"skill (default), agent, hook, mcp, or any. Commands are skills; agents, hooks and MCP servers are installed with pmcp add rather than read."
|
|
1014
|
+
);
|
|
655
1015
|
var asText = (value) => ({
|
|
656
1016
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
657
1017
|
});
|
|
@@ -660,11 +1020,12 @@ function registerSkillTools(server, tools) {
|
|
|
660
1020
|
"skill_catalog",
|
|
661
1021
|
{
|
|
662
1022
|
title: "List installed skills without their bodies",
|
|
663
|
-
description: "Every skill the installed packages and marketplaces ship, grouped by package or plugin, with names, one-line descriptions and tier, and no bodies. Paged by group. Use it to see what exists, or when skill_find returns nothing.",
|
|
664
|
-
inputSchema:
|
|
665
|
-
page:
|
|
666
|
-
pageSize:
|
|
667
|
-
tier:
|
|
1023
|
+
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.",
|
|
1024
|
+
inputSchema: z2.object({
|
|
1025
|
+
page: z2.number().int().min(1).optional(),
|
|
1026
|
+
pageSize: z2.number().int().min(1).max(20).optional(),
|
|
1027
|
+
tier: z2.enum(["open", "free", "paid", "internal"]).optional(),
|
|
1028
|
+
kind: kindInput
|
|
668
1029
|
}),
|
|
669
1030
|
annotations: readOnly
|
|
670
1031
|
},
|
|
@@ -675,15 +1036,16 @@ function registerSkillTools(server, tools) {
|
|
|
675
1036
|
{
|
|
676
1037
|
title: "Find skills by describing the problem",
|
|
677
1038
|
description: "Ranked skill names for an intent. Describe the problem in a full sentence in English rather than a keyword: a single word matches far worse, and the ranking is over descriptions. Returns names only -- read one with skill_call.",
|
|
678
|
-
inputSchema:
|
|
679
|
-
intent:
|
|
680
|
-
limit:
|
|
681
|
-
filters:
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1039
|
+
inputSchema: z2.object({
|
|
1040
|
+
intent: z2.string().min(1).max(256).describe("What you are trying to do, as an English sentence."),
|
|
1041
|
+
limit: z2.number().int().min(1).max(25).optional(),
|
|
1042
|
+
filters: z2.object({
|
|
1043
|
+
kind: kindInput,
|
|
1044
|
+
tier: z2.enum(["open", "free", "paid", "internal"]).optional(),
|
|
1045
|
+
package: z2.string().optional(),
|
|
1046
|
+
level: z2.string().optional(),
|
|
1047
|
+
domain: z2.string().optional(),
|
|
1048
|
+
medium: z2.string().optional()
|
|
687
1049
|
}).optional().describe("Exact-match filters over the catalog before ranking.")
|
|
688
1050
|
}),
|
|
689
1051
|
annotations: readOnly
|
|
@@ -695,8 +1057,8 @@ function registerSkillTools(server, tools) {
|
|
|
695
1057
|
{
|
|
696
1058
|
title: "Check one skill's properties before loading it",
|
|
697
1059
|
description: "Frontmatter, metadata (tier, level, requirements, licence, approval), and the files the skill carries with sizes and sha256, without any body. Use it to decide between candidates before skill_call.",
|
|
698
|
-
inputSchema:
|
|
699
|
-
name:
|
|
1060
|
+
inputSchema: z2.object({
|
|
1061
|
+
name: z2.string().min(1).describe("The skill name skill_find or skill_catalog returned.")
|
|
700
1062
|
}),
|
|
701
1063
|
annotations: readOnly
|
|
702
1064
|
},
|
|
@@ -720,9 +1082,9 @@ function registerSkillTools(server, tools) {
|
|
|
720
1082
|
{
|
|
721
1083
|
title: "Read one file a skill carries",
|
|
722
1084
|
description: "A reference or script file inside one skill's directory, as text, with its sha256. Read only when the skill body points at the file. Scripts are returned, never run. Files over 256 KiB and paths outside the skill are refused.",
|
|
723
|
-
inputSchema:
|
|
724
|
-
name:
|
|
725
|
-
path:
|
|
1085
|
+
inputSchema: z2.object({
|
|
1086
|
+
name: z2.string().min(1),
|
|
1087
|
+
path: z2.string().min(1).optional().describe(
|
|
726
1088
|
"Path relative to the skill directory, as skill_describe lists it. Defaults to SKILL.md."
|
|
727
1089
|
)
|
|
728
1090
|
}),
|
|
@@ -745,8 +1107,8 @@ function registerSkillTools(server, tools) {
|
|
|
745
1107
|
{
|
|
746
1108
|
title: "Read one skill",
|
|
747
1109
|
description: "The body of a single skill, by the name skill_find or skill_catalog returned. This is the only call that costs a document.",
|
|
748
|
-
inputSchema:
|
|
749
|
-
name:
|
|
1110
|
+
inputSchema: z2.object({
|
|
1111
|
+
name: z2.string().min(1).describe("The skill name, as <package>/<slug>.")
|
|
750
1112
|
}),
|
|
751
1113
|
annotations: readOnly
|
|
752
1114
|
},
|
|
@@ -768,8 +1130,10 @@ function registerSkillTools(server, tools) {
|
|
|
768
1130
|
return server;
|
|
769
1131
|
}
|
|
770
1132
|
function createSkillServer(options) {
|
|
771
|
-
const server = new McpServer({ name: SERVER_NAME, version: "0.
|
|
772
|
-
|
|
1133
|
+
const server = new McpServer({ name: SERVER_NAME, version: "0.5.0" });
|
|
1134
|
+
const tools = createSkillTools(options);
|
|
1135
|
+
registerSkillsExtension(server, tools.skills);
|
|
1136
|
+
return registerSkillTools(server, tools);
|
|
773
1137
|
}
|
|
774
1138
|
|
|
775
1139
|
export {
|
|
@@ -777,6 +1141,9 @@ export {
|
|
|
777
1141
|
readMetadata,
|
|
778
1142
|
readFrontmatterText,
|
|
779
1143
|
readMarketplace,
|
|
1144
|
+
assetRoot,
|
|
1145
|
+
ASSET_KINDS,
|
|
1146
|
+
kindOf,
|
|
780
1147
|
SKIPPED_DIRS,
|
|
781
1148
|
MAX_SKILL_DEPTH,
|
|
782
1149
|
readFrontmatter,
|
|
@@ -793,8 +1160,12 @@ export {
|
|
|
793
1160
|
CATALOG_PAGE_SIZE,
|
|
794
1161
|
CATALOG_PAGE_MAX,
|
|
795
1162
|
createSkillTools,
|
|
1163
|
+
SKILLS_EXTENSION,
|
|
1164
|
+
skillUri,
|
|
1165
|
+
extensionEntry,
|
|
1166
|
+
registerSkillsExtension,
|
|
796
1167
|
SERVER_NAME,
|
|
797
1168
|
registerSkillTools,
|
|
798
1169
|
createSkillServer
|
|
799
1170
|
};
|
|
800
|
-
//# sourceMappingURL=chunk-
|
|
1171
|
+
//# sourceMappingURL=chunk-JOQTOVTJ.js.map
|