@modootoday/pmcp 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,8 +4,9 @@ Find and use package skills without loading every document into your agent's con
4
4
  `pmcp` reads `SKILL.md` files from installed packages, from monorepo workspaces
5
5
  (`--workspace <dir>`: the root and every member its `workspaces` globs name, nested
6
6
  workspace roots included), from single package directories (`--package <dir>`), and
7
- from plugin marketplaces (`--marketplace <dir>`), and exposes five MCP tools: `skill_catalog`, `skill_find`,
8
- `skill_describe`, `skill_call`, and `skill_read`.
7
+ from plugin marketplaces (`--marketplace <dir>`), and exposes six MCP tools: `skill_catalog`, `skill_find`,
8
+ `skill_describe`, `skill_call`, `skill_read` and `skill_bundle`, plus every skill file as a
9
+ `skill://` resource and, on request, one prompt per skill.
9
10
 
10
11
  ## Install
11
12
 
@@ -121,7 +122,28 @@ Skills are discovered by filename, including `skills/`, `.agents/skills/`,
121
122
  within the discovery depth. A skill needs a frontmatter `description` for search.
122
123
 
123
124
  `skill_find` accepts a problem description and returns matching skills.
124
- `skill_call` opens the selected document. Local MCP reading requires no login.
125
+ `skill_call` opens the selected document and lists the reference, script and asset
126
+ files it carries, as text and as resource links. Local MCP reading requires no login.
127
+
128
+ Files keep their type, as the Agent Skills layout (`references/`, `scripts/`, `assets/`)
129
+ expects: `skill_read` returns text as text, images and audio as image and audio content,
130
+ and other binary files as an embedded resource with its media type; a file whose extension
131
+ names text but whose bytes are not UTF-8 goes out as a blob, so its sha256 stays exact.
132
+ Scripts are returned, never run: running them is the host's job, from a local copy of the
133
+ skill. `skill_bundle` lists every file of a skill as resource links that `resources/read`
134
+ returns, so a host can write the skill into a local skills directory without the bytes
135
+ passing through the model.
136
+
137
+ `skill_catalog`, `skill_find`, `skill_describe` and `skill_bundle` declare an
138
+ `outputSchema` and return `structuredContent` alongside the same JSON as text.
139
+ Frontmatter is read as YAML (folded and block descriptions, a BOM and CRLF are fine), and
140
+ `metadata` is read for every skill. Loading is lenient, as the client guide asks; what
141
+ breaks the specification is reported, and `pmcp validate [<skill-dir>...]` checks given
142
+ directories or every skill the catalog reaches (name rules and length, description and
143
+ compatibility length, field types, name matching the directory, duplicate names).
144
+
145
+ `pmcp serve --prompts` also offers one prompt per skill, for hosts that turn prompts into
146
+ commands; the prompt's text is the skill body and its file list, with an optional `task`.
125
147
 
126
148
  Marketplace plugins also carry commands, agents, hooks and MCP servers.
127
149
  Commands are listed as skills. The others are listed under their own kind and
@@ -132,15 +154,24 @@ are left out of `skill_catalog` and `skill_find` unless `kind` asks for them
132
154
  ## MCP Skills extension
133
155
 
134
156
  The server also declares `io.modelcontextprotocol/skills` (SEP-2640): it
135
- answers `skills/list` and `skills/get`, and serves each file of a conforming
136
- skill as `skill://pmcp/<package>/<name>/<file>` through `resources/read`, with
137
- a sha256 digest and size per file. A skill qualifies when its frontmatter
138
- `name` matches its directory and the Agent Skills naming rules, and it stays
139
- within 512 files and 16 MiB; the rest remain reachable through the tools.
157
+ answers `skills/list` and `skills/get` (both with `ttlMs` and `cacheScope`), with
158
+ a sha256 digest and size per file. A skill qualifies when its frontmatter passes the
159
+ Agent Skills specification and its `name` matches its directory, and it stays within
160
+ 512 files and 16 MiB; the rest remain reachable through the tools.
161
+
162
+ Every file of every skill the caller may see is a `skill://pmcp/<package>/<name>/<file>`
163
+ resource: `resources/list` lists each skill's `SKILL.md`, `resources/read` returns any file
164
+ (text, or a blob with its media type), and the template completes skill names and paths.
165
+ List-change notifications and subscriptions are not offered: they need a session, and the
166
+ HTTP handler is stateless.
140
167
 
141
168
  ## Hosting over HTTP
142
169
 
143
- `@modootoday/pmcp/http` serves the same five tools over streamable HTTP for a hosted catalog.
170
+ `@modootoday/pmcp/http` serves the same tools and resources over streamable HTTP for a hosted
171
+ catalog. Tools, resources, the Skills extension and prompts all read the same per-caller,
172
+ tier-filtered catalog, so a file of a skill a caller cannot list cannot be read either. The
173
+ extension's `cacheScope` is `private` there by default (`cacheScope` overrides it), and
174
+ `prompts: true` turns prompts on.
144
175
  The host owns identity: `authorize(request)` returns the caller or `undefined` (answered 401),
145
176
  and `tiersFor(caller)` decides which tiers that caller sees (default `open` and `free`; skills
146
177
  without a tier count as `open`). The catalog is read once; call `reload()` after deploying new
@@ -48,6 +48,14 @@ interface CatalogOptions {
48
48
  path: string;
49
49
  reason: string;
50
50
  }) => void;
51
+ /**
52
+ * Told about a skill that is served but breaks the Agent Skills specification,
53
+ * or that lost a name collision. Loading stays lenient, as the client guide asks.
54
+ */
55
+ readonly onWarning?: (warning: {
56
+ path: string;
57
+ reason: string;
58
+ }) => void;
51
59
  }
52
60
  /**
53
61
  * Directories a walk does not enter. Kept as short as the evidence allows: a
@@ -64,19 +72,11 @@ declare const SKIPPED_DIRS: ReadonlySet<string>;
64
72
  */
65
73
  declare const MAX_SKILL_DEPTH = 3;
66
74
  /**
67
- * Reads `name` and `description` out of YAML frontmatter without a parser.
68
- *
69
- * A parser is a dependency this server does not otherwise need, and the two
70
- * fields it wants are flat scalars on their own line. Anything more nested is
71
- * not read, which is the honest limit rather than a partial YAML.
75
+ * Top-level scalar fields of a SKILL.md frontmatter, parsed as YAML, so a folded
76
+ * `description: >-` arrives as its text rather than as the indicator.
72
77
  */
73
78
  declare function readFrontmatter(source: string): Record<string, string>;
74
- /**
75
- * The body after the frontmatter, or the whole file when there is none.
76
- *
77
- * The slice starts at the newline terminating the closing `---`, and a blank
78
- * line after it is separator rather than content, so both are dropped.
79
- */
79
+ /** The body after the frontmatter, or the whole file when there is none. */
80
80
  declare function readBody(source: string): string;
81
81
  /**
82
82
  * Every skill reachable from the given roots, sorted by name.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createSkillServer
3
- } from "./chunk-QHFORYIN.js";
3
+ } from "./chunk-ZDV75HZN.js";
4
4
 
5
5
  // src/stdio.ts
6
6
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
@@ -8,10 +8,13 @@ function resolveRoots(argv, cwd) {
8
8
  const given = argv.filter((arg) => !arg.startsWith("-"));
9
9
  return given.length > 0 ? given : [`${cwd}/node_modules`];
10
10
  }
11
- function serve(catalog) {
11
+ function serve(catalog, serving = {}) {
12
12
  const options = {
13
13
  ...catalog,
14
+ ...serving,
14
15
  onReject: ({ path, reason }) => process.stderr.write(`pmcp: skipped ${path}: ${reason}
16
+ `),
17
+ onWarning: ({ path, reason }) => process.stderr.write(`pmcp: warning ${path}: ${reason}
15
18
  `)
16
19
  };
17
20
  serveStdio(() => createSkillServer(options), {
@@ -35,4 +38,4 @@ export {
35
38
  serve,
36
39
  main
37
40
  };
38
- //# sourceMappingURL=chunk-POJOFL4L.js.map
41
+ //# sourceMappingURL=chunk-LUBYOET7.js.map
@@ -1,3 +1,9 @@
1
+ import {
2
+ frontmatterObject,
3
+ readCatalog,
4
+ specProblems
5
+ } from "./chunk-ZDV75HZN.js";
6
+
1
7
  // src/config.ts
2
8
  import { existsSync, readFileSync } from "fs";
3
9
  import { dirname, join, resolve } from "path";
@@ -1086,6 +1092,33 @@ function readSpec(config) {
1086
1092
  return { root: config.dir, tools, skills, agents, packageRules, mcp };
1087
1093
  }
1088
1094
 
1095
+ // src/validate.ts
1096
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync } from "fs";
1097
+ import { basename, join as join6, resolve as resolve2 } from "path";
1098
+ function validateSkillDir(dir) {
1099
+ const root = resolve2(dir);
1100
+ const skillMd = basename(root) === "SKILL.md" ? root : join6(root, "SKILL.md");
1101
+ if (!existsSync6(skillMd) || !statSync(skillMd).isFile()) {
1102
+ return [{ path: root, reason: "no SKILL.md", level: "error" }];
1103
+ }
1104
+ const front = frontmatterObject(readFileSync5(skillMd, "utf8"));
1105
+ const directory = basename(skillMd === root ? join6(root, "..") : root);
1106
+ return specProblems(front, directory).map((reason) => ({
1107
+ path: skillMd,
1108
+ reason,
1109
+ level: "error"
1110
+ }));
1111
+ }
1112
+ function validateCatalog(options) {
1113
+ const findings = [];
1114
+ readCatalog({
1115
+ ...options,
1116
+ onReject: (r) => findings.push({ ...r, level: "error" }),
1117
+ onWarning: (w) => findings.push({ ...w, level: "warning" })
1118
+ });
1119
+ return findings;
1120
+ }
1121
+
1089
1122
  export {
1090
1123
  CONFIG_FILE,
1091
1124
  ConfigError,
@@ -1102,6 +1135,8 @@ export {
1102
1135
  spawnRunner,
1103
1136
  doctor,
1104
1137
  TOOLS,
1105
- readSpec
1138
+ readSpec,
1139
+ validateSkillDir,
1140
+ validateCatalog
1106
1141
  };
1107
- //# sourceMappingURL=chunk-GNJSZCRH.js.map
1142
+ //# sourceMappingURL=chunk-M6LBYJDS.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  normaliseIntent
3
- } from "./chunk-QHFORYIN.js";
3
+ } from "./chunk-ZDV75HZN.js";
4
4
 
5
5
  // src/cache.ts
6
6
  async function openDatabase(path) {
@@ -131,4 +131,4 @@ export {
131
131
  DEFAULT_INTENT_CACHE_LIMIT,
132
132
  openIntentCache
133
133
  };
134
- //# sourceMappingURL=chunk-ZLELYZHL.js.map
134
+ //# sourceMappingURL=chunk-OPDVUHTK.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  openDatabase
3
- } from "./chunk-ZLELYZHL.js";
3
+ } from "./chunk-OPDVUHTK.js";
4
4
 
5
5
  // src/embed.ts
6
6
  var DEFAULT_MODEL_ID = "Xenova/multilingual-e5-small";
@@ -157,4 +157,4 @@ export {
157
157
  encodeEntries,
158
158
  openEntryVectors
159
159
  };
160
- //# sourceMappingURL=chunk-NI2OLZPA.js.map
160
+ //# sourceMappingURL=chunk-WSTCBVXW.js.map