@modootoday/pmcp 0.2.0 → 0.4.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 +25 -2
- package/dist/catalog-BiPLcbLu.d.ts +86 -0
- package/dist/{chunk-3NNRQ5A4.js → chunk-BFET6MFN.js} +143 -41
- package/dist/{chunk-Y32X35YV.js → chunk-JL5BCIQM.js} +9 -10
- package/dist/{chunk-2NOO5RSP.js → chunk-RBT3ISF4.js} +2 -2
- package/dist/cli.js +23 -11
- package/dist/http.d.ts +34 -0
- package/dist/http.js +65 -0
- package/dist/index.d.ts +5 -273
- package/dist/index.js +2 -2
- package/dist/server-dNFrR_Zh.d.ts +194 -0
- package/dist/stdio.d.ts +4 -1
- package/dist/stdio.js +2 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# pmcp
|
|
2
2
|
|
|
3
3
|
Find and use package skills without loading every document into your agent's context.
|
|
4
|
-
`pmcp` reads `SKILL.md` files from installed packages
|
|
5
|
-
(`--
|
|
4
|
+
`pmcp` reads `SKILL.md` files from installed packages, from monorepo workspaces
|
|
5
|
+
(`--workspace <dir>`: the root and every member its `workspaces` globs name, nested
|
|
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`,
|
|
6
8
|
`skill_describe`, `skill_call`, and `skill_read`.
|
|
7
9
|
|
|
8
10
|
## Install
|
|
@@ -60,6 +62,27 @@ within the discovery depth. A skill needs a frontmatter `description` for search
|
|
|
60
62
|
`skill_find` accepts a problem description and returns matching skills.
|
|
61
63
|
`skill_call` opens the selected document. Local MCP reading requires no login.
|
|
62
64
|
|
|
65
|
+
## Hosting over HTTP
|
|
66
|
+
|
|
67
|
+
`@modootoday/pmcp/http` serves the same five tools over streamable HTTP for a hosted catalog.
|
|
68
|
+
The host owns identity: `authorize(request)` returns the caller or `undefined` (answered 401),
|
|
69
|
+
and `tiersFor(caller)` decides which tiers that caller sees (default `open` and `free`; skills
|
|
70
|
+
without a tier count as `open`). The catalog is read once; call `reload()` after deploying new
|
|
71
|
+
skill releases.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { createSkillHttpHandler } from "@modootoday/pmcp/http";
|
|
75
|
+
|
|
76
|
+
const skills = createSkillHttpHandler({
|
|
77
|
+
roots: [],
|
|
78
|
+
marketplaces: ["/srv/marketplace", "/srv/marketplace-free"],
|
|
79
|
+
authorize: async (request) => verifyBearer(request.headers.get("authorization")),
|
|
80
|
+
});
|
|
81
|
+
export default { fetch: (request: Request) => skills.fetch(request) };
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Put Origin and Host validation in front of it when it is mounted bare.
|
|
85
|
+
|
|
63
86
|
## Runtime and licence
|
|
64
87
|
|
|
65
88
|
Node.js 22 or later, or Bun 1.3 or later. Default ranking is lexical. The optional
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skills the installed packages brought with them.
|
|
3
|
+
*
|
|
4
|
+
* Derived from `node_modules` at read time rather than from a list somebody
|
|
5
|
+
* maintains: installing a package is how its skill arrives and uninstalling it
|
|
6
|
+
* is how the skill leaves, so the catalog is always true of the project.
|
|
7
|
+
*/
|
|
8
|
+
interface SkillEntry {
|
|
9
|
+
/** `<package>/<slug>`. Unique, and stable while the package is installed. */
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly package: string;
|
|
12
|
+
readonly slug: string;
|
|
13
|
+
/** Frontmatter description. The only text `find` ranks against. */
|
|
14
|
+
readonly description: string;
|
|
15
|
+
/** Absolute path to the SKILL.md. `call` reads the body from here. */
|
|
16
|
+
readonly path: string;
|
|
17
|
+
/** open, free or paid. Set for marketplace skills, absent for packages. */
|
|
18
|
+
readonly tier?: string;
|
|
19
|
+
/** The frontmatter `metadata:` block, one level deep. */
|
|
20
|
+
readonly metadata?: Readonly<Record<string, string | readonly string[]>>;
|
|
21
|
+
/** Extra search terms. Ranked with the description, never used as triggers. */
|
|
22
|
+
readonly keywords?: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
interface CatalogOptions {
|
|
25
|
+
/** Directories to walk. Each is a `node_modules` root, not a package root. */
|
|
26
|
+
readonly roots: readonly string[];
|
|
27
|
+
/** Package name prefixes to consider. Empty means every package. */
|
|
28
|
+
readonly scopes?: readonly string[];
|
|
29
|
+
/** Marketplace directories, each holding .claude-plugin/marketplace.json. */
|
|
30
|
+
readonly marketplaces?: readonly string[];
|
|
31
|
+
/** Package directories read directly, for a package no node_modules links. */
|
|
32
|
+
readonly packages?: readonly string[];
|
|
33
|
+
/** Workspace roots: the root and every member its workspaces globs name. */
|
|
34
|
+
readonly workspaces?: readonly string[];
|
|
35
|
+
/** Told about every marketplace skill left out, and why. */
|
|
36
|
+
readonly onReject?: (rejection: {
|
|
37
|
+
path: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
}) => void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Directories a walk does not enter. Kept as short as the evidence allows: a
|
|
43
|
+
* name here is a package whose skill will never be found, and the cost of one
|
|
44
|
+
* extra readdir is far below the cost of missing a skill. Measured 20260906 --
|
|
45
|
+
* `lib` was on this list and hid playwright's `lib/skill/SKILL.md`, which is
|
|
46
|
+
* the most widely installed skill in the real world.
|
|
47
|
+
*/
|
|
48
|
+
declare const SKIPPED_DIRS: ReadonlySet<string>;
|
|
49
|
+
/**
|
|
50
|
+
* How deep below a package root a SKILL.md is looked for. Three reaches every
|
|
51
|
+
* convention in use (skills, .agent/skills, .claude/skills, .gemini/skills,
|
|
52
|
+
* .codex/skills); four was measured to find nothing more.
|
|
53
|
+
*/
|
|
54
|
+
declare const MAX_SKILL_DEPTH = 3;
|
|
55
|
+
/**
|
|
56
|
+
* Reads `name` and `description` out of YAML frontmatter without a parser.
|
|
57
|
+
*
|
|
58
|
+
* A parser is a dependency this server does not otherwise need, and the two
|
|
59
|
+
* fields it wants are flat scalars on their own line. Anything more nested is
|
|
60
|
+
* not read, which is the honest limit rather than a partial YAML.
|
|
61
|
+
*/
|
|
62
|
+
declare function readFrontmatter(source: string): Record<string, string>;
|
|
63
|
+
/**
|
|
64
|
+
* The body after the frontmatter, or the whole file when there is none.
|
|
65
|
+
*
|
|
66
|
+
* The slice starts at the newline terminating the closing `---`, and a blank
|
|
67
|
+
* line after it is separator rather than content, so both are dropped.
|
|
68
|
+
*/
|
|
69
|
+
declare function readBody(source: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Every skill reachable from the given roots, sorted by name.
|
|
72
|
+
*
|
|
73
|
+
* A package with no skills directory contributes nothing and costs one
|
|
74
|
+
* `existsSync` per convention. A skill with no description is skipped rather
|
|
75
|
+
* than listed:
|
|
76
|
+
* `find` ranks descriptions, so one without a description can only ever be
|
|
77
|
+
* found by already knowing its name, and listing it would spend a session's
|
|
78
|
+
* tokens on an entry that cannot answer.
|
|
79
|
+
*/
|
|
80
|
+
declare function readCatalog(options: CatalogOptions): SkillEntry[];
|
|
81
|
+
/** A catalog entry without its body, which is what `catalog` returns. */
|
|
82
|
+
declare function withoutBody({ path, ...rest }: SkillEntry): Omit<SkillEntry, "path">;
|
|
83
|
+
/** Path relative to a root, for a message a reader can act on. */
|
|
84
|
+
declare function displayPath(root: string, entry: SkillEntry): string;
|
|
85
|
+
|
|
86
|
+
export { type CatalogOptions as C, MAX_SKILL_DEPTH as M, type SkillEntry as S, SKIPPED_DIRS as a, readCatalog as b, readFrontmatter as c, displayPath as d, readBody as r, withoutBody as w };
|
|
@@ -9,7 +9,7 @@ import { join as join2, resolve } from "path";
|
|
|
9
9
|
|
|
10
10
|
// src/catalog.ts
|
|
11
11
|
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
12
|
-
import { basename, dirname, join, relative } from "path";
|
|
12
|
+
import { basename, dirname, isAbsolute, join, relative } from "path";
|
|
13
13
|
var SKIPPED_DIRS = /* @__PURE__ */ new Set([
|
|
14
14
|
"node_modules",
|
|
15
15
|
".git",
|
|
@@ -86,48 +86,148 @@ function packageDirs(root) {
|
|
|
86
86
|
}
|
|
87
87
|
return found;
|
|
88
88
|
}
|
|
89
|
+
function readManifest(dir) {
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(
|
|
92
|
+
readFileSync(join(dir, "package.json"), "utf8")
|
|
93
|
+
);
|
|
94
|
+
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function workspacePatterns(manifest) {
|
|
100
|
+
const field = manifest["workspaces"];
|
|
101
|
+
const list = Array.isArray(field) ? field : field?.packages;
|
|
102
|
+
if (!Array.isArray(list)) return [];
|
|
103
|
+
return list.filter((p) => typeof p === "string");
|
|
104
|
+
}
|
|
105
|
+
function expandBraces(pattern) {
|
|
106
|
+
const match = /\{([^{}]*)\}/u.exec(pattern);
|
|
107
|
+
if (!match) return [pattern];
|
|
108
|
+
const head = pattern.slice(0, match.index);
|
|
109
|
+
const tail = pattern.slice(match.index + match[0].length);
|
|
110
|
+
return match[1].split(",").flatMap((option) => expandBraces(`${head}${option}${tail}`));
|
|
111
|
+
}
|
|
112
|
+
function segmentMatcher(segment) {
|
|
113
|
+
const body = segment.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/gu, "\\$&")).join("[^/]*");
|
|
114
|
+
return new RegExp(`^${body}$`, "u");
|
|
115
|
+
}
|
|
116
|
+
function globDirs(base, pattern) {
|
|
117
|
+
const segments = pattern.split("/").filter((s) => s !== "" && s !== ".");
|
|
118
|
+
const found = /* @__PURE__ */ new Set();
|
|
119
|
+
const walk = (dir, index) => {
|
|
120
|
+
if (index === segments.length) {
|
|
121
|
+
found.add(dir);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const segment = segments[index];
|
|
125
|
+
if (!segment.includes("*")) {
|
|
126
|
+
const next = join(dir, segment);
|
|
127
|
+
if (existsSync(next)) walk(next, index + 1);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
let entries;
|
|
131
|
+
try {
|
|
132
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
133
|
+
} catch {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const children = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).filter((e) => !SKIPPED_DIRS.has(e.name));
|
|
137
|
+
if (segment === "**") {
|
|
138
|
+
walk(dir, index + 1);
|
|
139
|
+
for (const child of children) walk(join(dir, child.name), index);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const matcher = segmentMatcher(segment);
|
|
143
|
+
for (const child of children) {
|
|
144
|
+
if (matcher.test(child.name)) walk(join(dir, child.name), index + 1);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
walk(base, 0);
|
|
148
|
+
return [...found];
|
|
149
|
+
}
|
|
150
|
+
function workspacePackageDirs(root) {
|
|
151
|
+
const ordered = [];
|
|
152
|
+
const visited = /* @__PURE__ */ new Set();
|
|
153
|
+
const expand = (dir) => {
|
|
154
|
+
if (visited.has(dir)) return;
|
|
155
|
+
visited.add(dir);
|
|
156
|
+
const manifest = readManifest(dir);
|
|
157
|
+
if (manifest === null) return;
|
|
158
|
+
const included = /* @__PURE__ */ new Set();
|
|
159
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
160
|
+
for (const raw of workspacePatterns(manifest)) {
|
|
161
|
+
const negated = raw.startsWith("!");
|
|
162
|
+
const pattern = negated ? raw.slice(1) : raw;
|
|
163
|
+
for (const alternative of expandBraces(pattern)) {
|
|
164
|
+
for (const match of globDirs(dir, alternative)) {
|
|
165
|
+
(negated ? excluded : included).add(match);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const members = [...included].filter((m) => !excluded.has(m) && existsSync(join(m, "package.json"))).sort();
|
|
170
|
+
for (const member of members) expand(member);
|
|
171
|
+
ordered.push(dir);
|
|
172
|
+
};
|
|
173
|
+
expand(root);
|
|
174
|
+
return ordered;
|
|
175
|
+
}
|
|
176
|
+
function nameOf(dir, named) {
|
|
177
|
+
const manifestName = readManifest(dir)?.["name"];
|
|
178
|
+
if (typeof manifestName === "string" && manifestName !== "") {
|
|
179
|
+
return manifestName;
|
|
180
|
+
}
|
|
181
|
+
if (named) return basename(dir);
|
|
182
|
+
return "";
|
|
183
|
+
}
|
|
184
|
+
function expandRootGlob(dir) {
|
|
185
|
+
if (!dir.includes("*")) return [dir];
|
|
186
|
+
const absolute = isAbsolute(dir);
|
|
187
|
+
const base = absolute ? "/" : ".";
|
|
188
|
+
return globDirs(base, dir).filter((d) => existsSync(join(d, "package.json"))).sort();
|
|
189
|
+
}
|
|
89
190
|
function readCatalog(options) {
|
|
90
191
|
const scopes = options.scopes ?? [];
|
|
91
192
|
const found = [];
|
|
92
193
|
const seen = /* @__PURE__ */ new Set();
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
194
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
195
|
+
const direct = new Set(options.packages ?? []);
|
|
196
|
+
const packageDirsToRead = [
|
|
197
|
+
...options.roots.flatMap(packageDirs),
|
|
198
|
+
...direct,
|
|
199
|
+
...(options.workspaces ?? []).flatMap(expandRootGlob).flatMap(workspacePackageDirs)
|
|
200
|
+
];
|
|
201
|
+
for (const packageDir of packageDirsToRead) {
|
|
202
|
+
const packageName = nameOf(packageDir, direct.has(packageDir));
|
|
203
|
+
if (packageName === "") continue;
|
|
204
|
+
if (scopes.length > 0 && !scopes.some((s) => packageName.startsWith(s))) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
for (const path of skillFiles(packageDir)) {
|
|
208
|
+
if (seenPaths.has(path)) continue;
|
|
209
|
+
seenPaths.add(path);
|
|
210
|
+
let source;
|
|
98
211
|
try {
|
|
99
|
-
|
|
100
|
-
if (!manifest.name) continue;
|
|
101
|
-
packageName = manifest.name;
|
|
212
|
+
source = readFileSync(path, "utf8");
|
|
102
213
|
} catch {
|
|
103
214
|
continue;
|
|
104
215
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const name = `${packageName}/${slug}`;
|
|
121
|
-
if (seen.has(name)) continue;
|
|
122
|
-
seen.add(name);
|
|
123
|
-
found.push({
|
|
124
|
-
name,
|
|
125
|
-
package: packageName,
|
|
126
|
-
slug,
|
|
127
|
-
description,
|
|
128
|
-
path
|
|
129
|
-
});
|
|
130
|
-
}
|
|
216
|
+
const front = readFrontmatter(source);
|
|
217
|
+
const description = front["description"] ?? "";
|
|
218
|
+
if (description === "") continue;
|
|
219
|
+
const dirName = basename(dirname(path));
|
|
220
|
+
const slug = front["name"] ?? (dirName === basename(packageDir) ? packageName : dirName);
|
|
221
|
+
const name = `${packageName}/${slug}`;
|
|
222
|
+
if (seen.has(name)) continue;
|
|
223
|
+
seen.add(name);
|
|
224
|
+
found.push({
|
|
225
|
+
name,
|
|
226
|
+
package: packageName,
|
|
227
|
+
slug,
|
|
228
|
+
description,
|
|
229
|
+
path
|
|
230
|
+
});
|
|
131
231
|
}
|
|
132
232
|
}
|
|
133
233
|
for (const dir of options.marketplaces ?? []) {
|
|
@@ -345,7 +445,7 @@ function rank(matches2, limit) {
|
|
|
345
445
|
// src/server.ts
|
|
346
446
|
import { createHash } from "crypto";
|
|
347
447
|
import { readFileSync as readFileSync3, readdirSync as readdirSync3, realpathSync, statSync } from "fs";
|
|
348
|
-
import { dirname as dirname2, isAbsolute, join as join3, relative as relative2, resolve as resolve2, sep } from "path";
|
|
448
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve2, sep } from "path";
|
|
349
449
|
var READ_BUDGET_BYTES = 256 * 1024;
|
|
350
450
|
var DESCRIBE_FILE_LIMIT = 200;
|
|
351
451
|
var CATALOG_PAGE_SIZE = 8;
|
|
@@ -403,6 +503,8 @@ function createSkillTools(options) {
|
|
|
403
503
|
roots: options.roots,
|
|
404
504
|
scopes: options.scopes,
|
|
405
505
|
marketplaces: options.marketplaces,
|
|
506
|
+
packages: options.packages,
|
|
507
|
+
workspaces: options.workspaces,
|
|
406
508
|
onReject: options.onReject
|
|
407
509
|
});
|
|
408
510
|
return entries;
|
|
@@ -492,7 +594,7 @@ function createSkillTools(options) {
|
|
|
492
594
|
read(name, path = "SKILL.md") {
|
|
493
595
|
const entry = catalog().find((candidate) => candidate.name === name);
|
|
494
596
|
if (!entry) return { ok: false, error: "unknown_skill", detail: name };
|
|
495
|
-
if (
|
|
597
|
+
if (isAbsolute2(path) || path.split(/[\\/]/u).includes("..")) {
|
|
496
598
|
return { ok: false, error: "path_outside_skill", detail: path };
|
|
497
599
|
}
|
|
498
600
|
const root = realpathSync(dirname2(entry.path));
|
|
@@ -562,7 +664,7 @@ function registerSkillTools(server, tools) {
|
|
|
562
664
|
inputSchema: z.object({
|
|
563
665
|
page: z.number().int().min(1).optional(),
|
|
564
666
|
pageSize: z.number().int().min(1).max(20).optional(),
|
|
565
|
-
tier: z.enum(["open", "free", "paid"]).optional()
|
|
667
|
+
tier: z.enum(["open", "free", "paid", "internal"]).optional()
|
|
566
668
|
}),
|
|
567
669
|
annotations: readOnly
|
|
568
670
|
},
|
|
@@ -577,7 +679,7 @@ function registerSkillTools(server, tools) {
|
|
|
577
679
|
intent: z.string().min(1).max(256).describe("What you are trying to do, as an English sentence."),
|
|
578
680
|
limit: z.number().int().min(1).max(25).optional(),
|
|
579
681
|
filters: z.object({
|
|
580
|
-
tier: z.enum(["open", "free", "paid"]).optional(),
|
|
682
|
+
tier: z.enum(["open", "free", "paid", "internal"]).optional(),
|
|
581
683
|
package: z.string().optional(),
|
|
582
684
|
level: z.string().optional(),
|
|
583
685
|
domain: z.string().optional(),
|
|
@@ -666,7 +768,7 @@ function registerSkillTools(server, tools) {
|
|
|
666
768
|
return server;
|
|
667
769
|
}
|
|
668
770
|
function createSkillServer(options) {
|
|
669
|
-
const server = new McpServer({ name: SERVER_NAME, version: "0.
|
|
771
|
+
const server = new McpServer({ name: SERVER_NAME, version: "0.4.0" });
|
|
670
772
|
return registerSkillTools(server, createSkillTools(options));
|
|
671
773
|
}
|
|
672
774
|
|
|
@@ -695,4 +797,4 @@ export {
|
|
|
695
797
|
registerSkillTools,
|
|
696
798
|
createSkillServer
|
|
697
799
|
};
|
|
698
|
-
//# sourceMappingURL=chunk-
|
|
800
|
+
//# sourceMappingURL=chunk-BFET6MFN.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSkillServer
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-BFET6MFN.js";
|
|
4
4
|
|
|
5
5
|
// src/stdio.ts
|
|
6
6
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
@@ -8,11 +8,9 @@ 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(
|
|
11
|
+
function serve(catalog) {
|
|
12
12
|
const options = {
|
|
13
|
-
|
|
14
|
-
...scopes.length > 0 ? { scopes } : {},
|
|
15
|
-
...marketplaces.length > 0 ? { marketplaces } : {},
|
|
13
|
+
...catalog,
|
|
16
14
|
onReject: ({ path, reason }) => process.stderr.write(`pmcp: skipped ${path}: ${reason}
|
|
17
15
|
`)
|
|
18
16
|
};
|
|
@@ -25,10 +23,11 @@ function serve(roots, scopes = [], marketplaces = []) {
|
|
|
25
23
|
}
|
|
26
24
|
function main(argv = process.argv.slice(2)) {
|
|
27
25
|
const scopeArg = argv.find((arg) => arg.startsWith("--scope="));
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
const scopes = scopeArg ? scopeArg.slice("--scope=".length).split(",") : [];
|
|
27
|
+
serve({
|
|
28
|
+
roots: resolveRoots(argv, process.cwd()),
|
|
29
|
+
...scopes.length > 0 ? { scopes } : {}
|
|
30
|
+
});
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
export {
|
|
@@ -36,4 +35,4 @@ export {
|
|
|
36
35
|
serve,
|
|
37
36
|
main
|
|
38
37
|
};
|
|
39
|
-
//# sourceMappingURL=chunk-
|
|
38
|
+
//# sourceMappingURL=chunk-JL5BCIQM.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normaliseIntent
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-BFET6MFN.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-
|
|
134
|
+
//# sourceMappingURL=chunk-RBT3ISF4.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
openDatabase
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-RBT3ISF4.js";
|
|
5
5
|
import {
|
|
6
6
|
serve
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-JL5BCIQM.js";
|
|
8
8
|
import {
|
|
9
9
|
readCatalog
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-BFET6MFN.js";
|
|
11
11
|
|
|
12
12
|
// src/cli/command.ts
|
|
13
13
|
var ArgumentError = class extends Error {
|
|
@@ -308,6 +308,18 @@ var CATALOG_OPTIONS = [
|
|
|
308
308
|
describe: "A plugin marketplace directory (holds .claude-plugin/marketplace.json). Repeatable.",
|
|
309
309
|
repeat: true,
|
|
310
310
|
placeholder: "<dir>"
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: "package",
|
|
314
|
+
describe: "A package directory read directly, for one no node_modules links; without a package.json its folder name is the package name. Repeatable.",
|
|
315
|
+
repeat: true,
|
|
316
|
+
placeholder: "<dir>"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: "workspace",
|
|
320
|
+
describe: "A workspace root, or a glob of them: each root and every member its workspaces globs name. Repeatable.",
|
|
321
|
+
repeat: true,
|
|
322
|
+
placeholder: "<dir>"
|
|
311
323
|
}
|
|
312
324
|
];
|
|
313
325
|
function rootsFrom(context) {
|
|
@@ -317,10 +329,14 @@ function rootsFrom(context) {
|
|
|
317
329
|
function catalogOptionsFrom(context, onReject) {
|
|
318
330
|
const scopes = many(context.args, "scope");
|
|
319
331
|
const marketplaces = many(context.args, "marketplace");
|
|
332
|
+
const packages = many(context.args, "package");
|
|
333
|
+
const workspaces = many(context.args, "workspace");
|
|
320
334
|
return {
|
|
321
335
|
roots: rootsFrom(context),
|
|
322
336
|
...scopes.length > 0 ? { scopes } : {},
|
|
323
337
|
...marketplaces.length > 0 ? { marketplaces } : {},
|
|
338
|
+
...packages.length > 0 ? { packages } : {},
|
|
339
|
+
...workspaces.length > 0 ? { workspaces } : {},
|
|
324
340
|
...onReject ? { onReject } : {}
|
|
325
341
|
};
|
|
326
342
|
}
|
|
@@ -433,7 +449,7 @@ var indexCommand = {
|
|
|
433
449
|
var listCommand = {
|
|
434
450
|
name: "list",
|
|
435
451
|
describe: "List the skills discovered in node_modules and marketplaces",
|
|
436
|
-
usage: "pmcp list [--root <dir>] [--scope <prefix>] [--marketplace <dir>] [--json]",
|
|
452
|
+
usage: "pmcp list [--root <dir>] [--scope <prefix>] [--marketplace <dir>] [--package <dir>] [--workspace <dir|glob>] [--json]",
|
|
437
453
|
options: [
|
|
438
454
|
...CATALOG_OPTIONS,
|
|
439
455
|
{ name: "json", describe: "Emit JSON rather than a table.", boolean: true }
|
|
@@ -472,14 +488,10 @@ var listCommand = {
|
|
|
472
488
|
var serveCommand = {
|
|
473
489
|
name: "serve",
|
|
474
490
|
describe: "Run the MCP server on stdio (the default with no command)",
|
|
475
|
-
usage: "pmcp serve [--root <dir>] [--scope <prefix>] [--marketplace <dir>]",
|
|
491
|
+
usage: "pmcp serve [--root <dir>] [--scope <prefix>] [--marketplace <dir>] [--package <dir>] [--workspace <dir>]",
|
|
476
492
|
options: [...CATALOG_OPTIONS],
|
|
477
493
|
run(context) {
|
|
478
|
-
serve(
|
|
479
|
-
rootsFrom(context),
|
|
480
|
-
many(context.args, "scope"),
|
|
481
|
-
many(context.args, "marketplace")
|
|
482
|
-
);
|
|
494
|
+
serve(catalogOptionsFrom(context));
|
|
483
495
|
return 0;
|
|
484
496
|
}
|
|
485
497
|
};
|
|
@@ -2073,7 +2085,7 @@ function dispatch(argv, options = {}) {
|
|
|
2073
2085
|
}
|
|
2074
2086
|
return run(command, rest, { ui, env, cwd });
|
|
2075
2087
|
}
|
|
2076
|
-
var VERSION = "0.
|
|
2088
|
+
var VERSION = "0.4.0";
|
|
2077
2089
|
async function run(command, argv, context) {
|
|
2078
2090
|
try {
|
|
2079
2091
|
return await command.run({
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { S as SkillEntry } from './catalog-BiPLcbLu.js';
|
|
2
|
+
import { S as SkillServerOptions } from './server-dNFrR_Zh.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The catalog over streamable HTTP, for a hosted skills server.
|
|
6
|
+
*
|
|
7
|
+
* The host owns identity: it passes authorize(), which turns a request into a
|
|
8
|
+
* caller or undefined. Anonymous requests are refused; each caller sees only the
|
|
9
|
+
* tiers tiersFor() grants. The catalog is read once and filtered per request.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface SkillCaller {
|
|
13
|
+
/** Stable id of the signed-in user, recorded by the host, never by pmcp. */
|
|
14
|
+
readonly id: string;
|
|
15
|
+
/** Whatever the host's token carried; passed to tiersFor unchanged. */
|
|
16
|
+
readonly claims?: Readonly<Record<string, unknown>>;
|
|
17
|
+
}
|
|
18
|
+
interface SkillHttpOptions extends SkillServerOptions {
|
|
19
|
+
readonly authorize: (request: Request) => SkillCaller | undefined | Promise<SkillCaller | undefined>;
|
|
20
|
+
/** Tiers a caller may list and load. Package skills without a tier count as open. */
|
|
21
|
+
readonly tiersFor?: (caller: SkillCaller) => readonly string[];
|
|
22
|
+
/** Sent in WWW-Authenticate on a refused request, e.g. a resource metadata URL. */
|
|
23
|
+
readonly challenge?: string;
|
|
24
|
+
}
|
|
25
|
+
declare const DEFAULT_TIERS: readonly string[];
|
|
26
|
+
declare function visibleTo(entries: readonly SkillEntry[], tiers: readonly string[]): SkillEntry[];
|
|
27
|
+
interface SkillHttpHandler {
|
|
28
|
+
fetch(request: Request): Promise<Response>;
|
|
29
|
+
/** Re-read the catalog, for a host that deploys new skill releases in place. */
|
|
30
|
+
reload(): void;
|
|
31
|
+
}
|
|
32
|
+
declare function createSkillHttpHandler(options: SkillHttpOptions): SkillHttpHandler;
|
|
33
|
+
|
|
34
|
+
export { DEFAULT_TIERS, type SkillCaller, type SkillHttpHandler, type SkillHttpOptions, createSkillHttpHandler, visibleTo };
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSkillServer,
|
|
3
|
+
readCatalog
|
|
4
|
+
} from "./chunk-BFET6MFN.js";
|
|
5
|
+
|
|
6
|
+
// src/http.ts
|
|
7
|
+
import {
|
|
8
|
+
createMcpHandler
|
|
9
|
+
} from "@modelcontextprotocol/server";
|
|
10
|
+
var DEFAULT_TIERS = ["open", "free"];
|
|
11
|
+
function visibleTo(entries, tiers) {
|
|
12
|
+
const allowed = new Set(tiers);
|
|
13
|
+
return entries.filter((entry) => allowed.has(entry.tier ?? "open"));
|
|
14
|
+
}
|
|
15
|
+
function createSkillHttpHandler(options) {
|
|
16
|
+
const load = options.loadCatalog ?? readCatalog;
|
|
17
|
+
let all = load(options);
|
|
18
|
+
const tiersFor = options.tiersFor ?? (() => DEFAULT_TIERS);
|
|
19
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
20
|
+
const handlerFor = (tiers) => {
|
|
21
|
+
const key = [...tiers].sort().join(",");
|
|
22
|
+
const cached = handlers.get(key);
|
|
23
|
+
if (cached) return cached;
|
|
24
|
+
const handler = createMcpHandler(
|
|
25
|
+
() => createSkillServer({
|
|
26
|
+
...options,
|
|
27
|
+
loadCatalog: () => visibleTo(all, tiers)
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
handlers.set(key, handler);
|
|
31
|
+
return handler;
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
async fetch(request) {
|
|
35
|
+
const caller = await options.authorize(request);
|
|
36
|
+
if (!caller) {
|
|
37
|
+
return new Response(JSON.stringify({ error: "unauthorized" }), {
|
|
38
|
+
status: 401,
|
|
39
|
+
headers: {
|
|
40
|
+
"content-type": "application/json",
|
|
41
|
+
"www-authenticate": options.challenge ?? "Bearer"
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return handlerFor(tiersFor(caller)).fetch(request, {
|
|
46
|
+
authInfo: {
|
|
47
|
+
token: "",
|
|
48
|
+
clientId: caller.id,
|
|
49
|
+
scopes: [],
|
|
50
|
+
extra: { ...caller.claims }
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
reload() {
|
|
55
|
+
all = load(options);
|
|
56
|
+
handlers.clear();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
DEFAULT_TIERS,
|
|
62
|
+
createSkillHttpHandler,
|
|
63
|
+
visibleTo
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=http.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,34 +1,9 @@
|
|
|
1
|
+
import { I as IntentCache, S as SkillServerOptions, a as SkillTools } from './server-dNFrR_Zh.js';
|
|
2
|
+
export { C as CATALOG_PAGE_MAX, b as CATALOG_PAGE_SIZE, c as CallResponse, d as CatalogResponse, D as DEFAULT_FIND_LIMIT, e as DESCRIBE_FILE_LIMIT, E as Embedder, F as FindOptions, f as FindResult, g as IntentCacheStats, M as Match, R as READ_BUDGET_BYTES, h as Ranking, i as ReadResult, j as SkillFilters, k as cosine, l as createSkillTools, m as find, n as lexicalScore, o as normaliseIntent } from './server-dNFrR_Zh.js';
|
|
3
|
+
import { S as SkillEntry } from './catalog-BiPLcbLu.js';
|
|
4
|
+
export { C as CatalogOptions, M as MAX_SKILL_DEPTH, a as SKIPPED_DIRS, d as displayPath, r as readBody, b as readCatalog, c as readFrontmatter, w as withoutBody } from './catalog-BiPLcbLu.js';
|
|
1
5
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
6
|
|
|
3
|
-
/**
|
|
4
|
-
* The intent key and the cache contract.
|
|
5
|
-
*
|
|
6
|
-
* Separate from the sqlite implementation so that ranking, which needs the key
|
|
7
|
-
* and the interface, does not pull a bun-only module into a consumer running
|
|
8
|
-
* under node. Only `cache.ts` requires bun.
|
|
9
|
-
*/
|
|
10
|
-
interface IntentCacheStats {
|
|
11
|
-
readonly hits: number;
|
|
12
|
-
readonly misses: number;
|
|
13
|
-
/** Rows found for the key but written by a different model. */
|
|
14
|
-
readonly mismatched: number;
|
|
15
|
-
readonly rows: number;
|
|
16
|
-
/** Which sqlite builtin answered, or absent when there is no cache. */
|
|
17
|
-
readonly provider?: string;
|
|
18
|
-
}
|
|
19
|
-
interface IntentCache {
|
|
20
|
-
/** The cached vector for an intent, or null when nothing usable is stored. */
|
|
21
|
-
get(intent: string): Float32Array | null;
|
|
22
|
-
put(intent: string, vector: Float32Array): void;
|
|
23
|
-
stats(): IntentCacheStats;
|
|
24
|
-
close(): void;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Same question, differently typed. Case, punctuation and spacing only — the
|
|
28
|
-
* key has to survive a caller writing "How do I wire X?" and "how do i wire x".
|
|
29
|
-
*/
|
|
30
|
-
declare function normaliseIntent(intent: string): string;
|
|
31
|
-
|
|
32
7
|
/**
|
|
33
8
|
* Query vectors, kept on disk between sessions.
|
|
34
9
|
*
|
|
@@ -61,143 +36,6 @@ declare const DEFAULT_INTENT_CACHE_LIMIT = 4096;
|
|
|
61
36
|
*/
|
|
62
37
|
declare function openIntentCache(options: IntentCacheOptions): Promise<IntentCache | null>;
|
|
63
38
|
|
|
64
|
-
/**
|
|
65
|
-
* The skills the installed packages brought with them.
|
|
66
|
-
*
|
|
67
|
-
* Derived from `node_modules` at read time rather than from a list somebody
|
|
68
|
-
* maintains: installing a package is how its skill arrives and uninstalling it
|
|
69
|
-
* is how the skill leaves, so the catalog is always true of the project.
|
|
70
|
-
*/
|
|
71
|
-
interface SkillEntry {
|
|
72
|
-
/** `<package>/<slug>`. Unique, and stable while the package is installed. */
|
|
73
|
-
readonly name: string;
|
|
74
|
-
readonly package: string;
|
|
75
|
-
readonly slug: string;
|
|
76
|
-
/** Frontmatter description. The only text `find` ranks against. */
|
|
77
|
-
readonly description: string;
|
|
78
|
-
/** Absolute path to the SKILL.md. `call` reads the body from here. */
|
|
79
|
-
readonly path: string;
|
|
80
|
-
/** open, free or paid. Set for marketplace skills, absent for packages. */
|
|
81
|
-
readonly tier?: string;
|
|
82
|
-
/** The frontmatter `metadata:` block, one level deep. */
|
|
83
|
-
readonly metadata?: Readonly<Record<string, string | readonly string[]>>;
|
|
84
|
-
/** Extra search terms. Ranked with the description, never used as triggers. */
|
|
85
|
-
readonly keywords?: readonly string[];
|
|
86
|
-
}
|
|
87
|
-
interface CatalogOptions {
|
|
88
|
-
/** Directories to walk. Each is a `node_modules` root, not a package root. */
|
|
89
|
-
readonly roots: readonly string[];
|
|
90
|
-
/** Package name prefixes to consider. Empty means every package. */
|
|
91
|
-
readonly scopes?: readonly string[];
|
|
92
|
-
/** Marketplace directories, each holding .claude-plugin/marketplace.json. */
|
|
93
|
-
readonly marketplaces?: readonly string[];
|
|
94
|
-
/** Told about every marketplace skill left out, and why. */
|
|
95
|
-
readonly onReject?: (rejection: {
|
|
96
|
-
path: string;
|
|
97
|
-
reason: string;
|
|
98
|
-
}) => void;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Directories a walk does not enter. Kept as short as the evidence allows: a
|
|
102
|
-
* name here is a package whose skill will never be found, and the cost of one
|
|
103
|
-
* extra readdir is far below the cost of missing a skill. Measured 20260906 --
|
|
104
|
-
* `lib` was on this list and hid playwright's `lib/skill/SKILL.md`, which is
|
|
105
|
-
* the most widely installed skill in the real world.
|
|
106
|
-
*/
|
|
107
|
-
declare const SKIPPED_DIRS: ReadonlySet<string>;
|
|
108
|
-
/**
|
|
109
|
-
* How deep below a package root a SKILL.md is looked for. Three reaches every
|
|
110
|
-
* convention in use (skills, .agent/skills, .claude/skills, .gemini/skills,
|
|
111
|
-
* .codex/skills); four was measured to find nothing more.
|
|
112
|
-
*/
|
|
113
|
-
declare const MAX_SKILL_DEPTH = 3;
|
|
114
|
-
/**
|
|
115
|
-
* Reads `name` and `description` out of YAML frontmatter without a parser.
|
|
116
|
-
*
|
|
117
|
-
* A parser is a dependency this server does not otherwise need, and the two
|
|
118
|
-
* fields it wants are flat scalars on their own line. Anything more nested is
|
|
119
|
-
* not read, which is the honest limit rather than a partial YAML.
|
|
120
|
-
*/
|
|
121
|
-
declare function readFrontmatter(source: string): Record<string, string>;
|
|
122
|
-
/**
|
|
123
|
-
* The body after the frontmatter, or the whole file when there is none.
|
|
124
|
-
*
|
|
125
|
-
* The slice starts at the newline terminating the closing `---`, and a blank
|
|
126
|
-
* line after it is separator rather than content, so both are dropped.
|
|
127
|
-
*/
|
|
128
|
-
declare function readBody(source: string): string;
|
|
129
|
-
/**
|
|
130
|
-
* Every skill reachable from the given roots, sorted by name.
|
|
131
|
-
*
|
|
132
|
-
* A package with no skills directory contributes nothing and costs one
|
|
133
|
-
* `existsSync` per convention. A skill with no description is skipped rather
|
|
134
|
-
* than listed:
|
|
135
|
-
* `find` ranks descriptions, so one without a description can only ever be
|
|
136
|
-
* found by already knowing its name, and listing it would spend a session's
|
|
137
|
-
* tokens on an entry that cannot answer.
|
|
138
|
-
*/
|
|
139
|
-
declare function readCatalog(options: CatalogOptions): SkillEntry[];
|
|
140
|
-
/** A catalog entry without its body, which is what `catalog` returns. */
|
|
141
|
-
declare function withoutBody({ path, ...rest }: SkillEntry): Omit<SkillEntry, "path">;
|
|
142
|
-
/** Path relative to a root, for a message a reader can act on. */
|
|
143
|
-
declare function displayPath(root: string, entry: SkillEntry): string;
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Ranking an intent against skill descriptions.
|
|
147
|
-
*
|
|
148
|
-
* Semantic when an embedder is supplied, lexical otherwise, and the answer says
|
|
149
|
-
* which. A caller that cannot tell the two apart cannot tell a degraded server
|
|
150
|
-
* from a working one.
|
|
151
|
-
*/
|
|
152
|
-
|
|
153
|
-
type Ranking = "semantic" | "lexical";
|
|
154
|
-
interface Match {
|
|
155
|
-
readonly name: string;
|
|
156
|
-
readonly package: string;
|
|
157
|
-
readonly description: string;
|
|
158
|
-
readonly score: number;
|
|
159
|
-
}
|
|
160
|
-
interface FindResult {
|
|
161
|
-
readonly ranking: Ranking;
|
|
162
|
-
readonly matches: readonly Match[];
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* The encoder, supplied by the caller. There is no default and no bundled
|
|
166
|
-
* model: which model runs decides what the scores mean, so it is the consuming
|
|
167
|
-
* repository's declaration rather than this package's.
|
|
168
|
-
*/
|
|
169
|
-
interface Embedder {
|
|
170
|
-
readonly modelId: string;
|
|
171
|
-
readonly dims: number;
|
|
172
|
-
embed(text: string): Promise<Float32Array>;
|
|
173
|
-
}
|
|
174
|
-
interface FindOptions {
|
|
175
|
-
readonly entries: readonly SkillEntry[];
|
|
176
|
-
readonly intent: string;
|
|
177
|
-
readonly limit?: number;
|
|
178
|
-
readonly embedder?: Embedder;
|
|
179
|
-
readonly cache?: IntentCache;
|
|
180
|
-
/** Precomputed description vectors, keyed by skill name. */
|
|
181
|
-
readonly vectors?: ReadonlyMap<string, Float32Array>;
|
|
182
|
-
}
|
|
183
|
-
declare const DEFAULT_FIND_LIMIT = 5;
|
|
184
|
-
/**
|
|
185
|
-
* Token overlap and bigram Dice, averaged. Deterministic and dependency-free:
|
|
186
|
-
* this is the answer when there is no model, so it must never be the thing
|
|
187
|
-
* that fails.
|
|
188
|
-
*/
|
|
189
|
-
declare function lexicalScore(intent: string, description: string): number;
|
|
190
|
-
/** Cosine over two equal-width vectors. Zero when either has no magnitude. */
|
|
191
|
-
declare function cosine(a: Float32Array, b: Float32Array): number;
|
|
192
|
-
/**
|
|
193
|
-
* Ranked matches, and which ranking produced them.
|
|
194
|
-
*
|
|
195
|
-
* Returns rather than loads: a search verb that also acts cannot be used to
|
|
196
|
-
* look, and the whole reason this server exists is that looking should be
|
|
197
|
-
* cheap.
|
|
198
|
-
*/
|
|
199
|
-
declare function find(options: FindOptions): Promise<FindResult>;
|
|
200
|
-
|
|
201
39
|
/**
|
|
202
40
|
* Skills laid out as an agent plugin marketplace: a directory holding
|
|
203
41
|
* `.claude-plugin/marketplace.json`, whose plugins keep skills at
|
|
@@ -224,112 +62,6 @@ declare function readFrontmatterText(source: string): string;
|
|
|
224
62
|
*/
|
|
225
63
|
declare function readMarketplace(dir: string, onReject?: (rejection: Rejection) => void): SkillEntry[];
|
|
226
64
|
|
|
227
|
-
/**
|
|
228
|
-
* The five tools, and nothing else.
|
|
229
|
-
*
|
|
230
|
-
* The point of the server is that a session's startup cost does not grow with
|
|
231
|
-
* the number of skills installed: the host lists five tools whatever the
|
|
232
|
-
* catalog holds, and a body is read only when one is asked for.
|
|
233
|
-
*/
|
|
234
|
-
|
|
235
|
-
/** Largest file skill_read returns. A skill needing more should split it. */
|
|
236
|
-
declare const READ_BUDGET_BYTES: number;
|
|
237
|
-
/** How many files skill_describe lists before saying the rest were cut. */
|
|
238
|
-
declare const DESCRIBE_FILE_LIMIT = 200;
|
|
239
|
-
declare const CATALOG_PAGE_SIZE = 8;
|
|
240
|
-
declare const CATALOG_PAGE_MAX = 20;
|
|
241
|
-
/** Equality filters over catalog fields. Unset keys match everything. */
|
|
242
|
-
interface SkillFilters {
|
|
243
|
-
readonly tier?: string;
|
|
244
|
-
readonly package?: string;
|
|
245
|
-
readonly level?: string;
|
|
246
|
-
readonly domain?: string;
|
|
247
|
-
readonly medium?: string;
|
|
248
|
-
}
|
|
249
|
-
type ReadResult = {
|
|
250
|
-
readonly ok: true;
|
|
251
|
-
readonly name: string;
|
|
252
|
-
readonly path: string;
|
|
253
|
-
readonly bytes: number;
|
|
254
|
-
readonly sha256: string;
|
|
255
|
-
readonly text: string;
|
|
256
|
-
} | {
|
|
257
|
-
readonly ok: false;
|
|
258
|
-
readonly error: string;
|
|
259
|
-
readonly detail: string;
|
|
260
|
-
};
|
|
261
|
-
interface SkillServerOptions extends CatalogOptions {
|
|
262
|
-
readonly embedder?: Embedder;
|
|
263
|
-
readonly cache?: IntentCache;
|
|
264
|
-
readonly vectors?: ReadonlyMap<string, Float32Array>;
|
|
265
|
-
/** Injected for tests. Reads the catalog off disk otherwise. */
|
|
266
|
-
readonly loadCatalog?: (options: CatalogOptions) => SkillEntry[];
|
|
267
|
-
}
|
|
268
|
-
interface CatalogResponse {
|
|
269
|
-
readonly count: number;
|
|
270
|
-
readonly page: number;
|
|
271
|
-
readonly totalPages: number;
|
|
272
|
-
readonly hasNext: boolean;
|
|
273
|
-
readonly packages: readonly {
|
|
274
|
-
readonly package: string;
|
|
275
|
-
readonly skills: readonly {
|
|
276
|
-
readonly name: string;
|
|
277
|
-
readonly description: string;
|
|
278
|
-
readonly tier?: string;
|
|
279
|
-
}[];
|
|
280
|
-
}[];
|
|
281
|
-
}
|
|
282
|
-
interface CallResponse {
|
|
283
|
-
readonly name: string;
|
|
284
|
-
readonly body: string;
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* The catalog, the search and the read, as plain functions.
|
|
288
|
-
*
|
|
289
|
-
* Separate from the MCP wiring so they can be exercised without a transport,
|
|
290
|
-
* and so a consumer that wants them from a hook or a script is not made to
|
|
291
|
-
* speak a protocol to get them.
|
|
292
|
-
*/
|
|
293
|
-
declare function createSkillTools(options: SkillServerOptions): {
|
|
294
|
-
/**
|
|
295
|
-
* Grouped, and without bodies. The cheap overview and the search's miss
|
|
296
|
-
* path. Paged by package so a large catalog never arrives in one answer.
|
|
297
|
-
*/
|
|
298
|
-
catalog(request?: {
|
|
299
|
-
page?: number;
|
|
300
|
-
pageSize?: number;
|
|
301
|
-
tier?: string;
|
|
302
|
-
}): CatalogResponse;
|
|
303
|
-
/** Ranked names. Returns; never loads a body. */
|
|
304
|
-
find(intent: string, limit?: number, filters?: SkillFilters): Promise<FindResult>;
|
|
305
|
-
/** Every property of one skill and the files it carries, without bodies. */
|
|
306
|
-
describe(name: string): {
|
|
307
|
-
name: string;
|
|
308
|
-
package: string;
|
|
309
|
-
description: string;
|
|
310
|
-
tier: string | undefined;
|
|
311
|
-
metadata: Readonly<Record<string, string | readonly string[]>>;
|
|
312
|
-
frontmatter: string;
|
|
313
|
-
files: {
|
|
314
|
-
path: string;
|
|
315
|
-
bytes: number;
|
|
316
|
-
sha256: string;
|
|
317
|
-
}[];
|
|
318
|
-
filesTruncated: boolean;
|
|
319
|
-
} | null;
|
|
320
|
-
/**
|
|
321
|
-
* One file inside a skill's directory. Paths that leave the directory,
|
|
322
|
-
* directly or through a link, are refused; nothing is ever executed.
|
|
323
|
-
*/
|
|
324
|
-
read(name: string, path?: string): ReadResult;
|
|
325
|
-
/** One body, at the moment it is needed. */
|
|
326
|
-
call(name: string): CallResponse | null;
|
|
327
|
-
/** Drops the memoised catalog, so the next call re-reads the tree. */
|
|
328
|
-
refresh(): void;
|
|
329
|
-
entries: () => Omit<SkillEntry, "path">[];
|
|
330
|
-
};
|
|
331
|
-
type SkillTools = ReturnType<typeof createSkillTools>;
|
|
332
|
-
|
|
333
65
|
/**
|
|
334
66
|
* The five tools, spoken over MCP.
|
|
335
67
|
*
|
|
@@ -356,4 +88,4 @@ declare function registerSkillTools(server: McpServer, tools: SkillTools): McpSe
|
|
|
356
88
|
*/
|
|
357
89
|
declare function createSkillServer(options: SkillServerOptions): McpServer;
|
|
358
90
|
|
|
359
|
-
export {
|
|
91
|
+
export { DEFAULT_INTENT_CACHE_LIMIT, IntentCache, type IntentCacheOptions, type MetadataValue, type Rejection, SERVER_NAME, SkillEntry, SkillServerOptions, SkillTools, createSkillServer, openIntentCache, readFrontmatterText, readMarketplace, readMetadata, registerSkillTools };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_INTENT_CACHE_LIMIT,
|
|
3
3
|
openIntentCache
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-RBT3ISF4.js";
|
|
5
5
|
import {
|
|
6
6
|
CATALOG_PAGE_MAX,
|
|
7
7
|
CATALOG_PAGE_SIZE,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
readMetadata,
|
|
27
27
|
registerSkillTools,
|
|
28
28
|
withoutBody
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-BFET6MFN.js";
|
|
30
30
|
export {
|
|
31
31
|
CATALOG_PAGE_MAX,
|
|
32
32
|
CATALOG_PAGE_SIZE,
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { S as SkillEntry, C as CatalogOptions } from './catalog-BiPLcbLu.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The intent key and the cache contract.
|
|
5
|
+
*
|
|
6
|
+
* Separate from the sqlite implementation so that ranking, which needs the key
|
|
7
|
+
* and the interface, does not pull a bun-only module into a consumer running
|
|
8
|
+
* under node. Only `cache.ts` requires bun.
|
|
9
|
+
*/
|
|
10
|
+
interface IntentCacheStats {
|
|
11
|
+
readonly hits: number;
|
|
12
|
+
readonly misses: number;
|
|
13
|
+
/** Rows found for the key but written by a different model. */
|
|
14
|
+
readonly mismatched: number;
|
|
15
|
+
readonly rows: number;
|
|
16
|
+
/** Which sqlite builtin answered, or absent when there is no cache. */
|
|
17
|
+
readonly provider?: string;
|
|
18
|
+
}
|
|
19
|
+
interface IntentCache {
|
|
20
|
+
/** The cached vector for an intent, or null when nothing usable is stored. */
|
|
21
|
+
get(intent: string): Float32Array | null;
|
|
22
|
+
put(intent: string, vector: Float32Array): void;
|
|
23
|
+
stats(): IntentCacheStats;
|
|
24
|
+
close(): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Same question, differently typed. Case, punctuation and spacing only — the
|
|
28
|
+
* key has to survive a caller writing "How do I wire X?" and "how do i wire x".
|
|
29
|
+
*/
|
|
30
|
+
declare function normaliseIntent(intent: string): string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ranking an intent against skill descriptions.
|
|
34
|
+
*
|
|
35
|
+
* Semantic when an embedder is supplied, lexical otherwise, and the answer says
|
|
36
|
+
* which. A caller that cannot tell the two apart cannot tell a degraded server
|
|
37
|
+
* from a working one.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
type Ranking = "semantic" | "lexical";
|
|
41
|
+
interface Match {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly package: string;
|
|
44
|
+
readonly description: string;
|
|
45
|
+
readonly score: number;
|
|
46
|
+
}
|
|
47
|
+
interface FindResult {
|
|
48
|
+
readonly ranking: Ranking;
|
|
49
|
+
readonly matches: readonly Match[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The encoder, supplied by the caller. There is no default and no bundled
|
|
53
|
+
* model: which model runs decides what the scores mean, so it is the consuming
|
|
54
|
+
* repository's declaration rather than this package's.
|
|
55
|
+
*/
|
|
56
|
+
interface Embedder {
|
|
57
|
+
readonly modelId: string;
|
|
58
|
+
readonly dims: number;
|
|
59
|
+
embed(text: string): Promise<Float32Array>;
|
|
60
|
+
}
|
|
61
|
+
interface FindOptions {
|
|
62
|
+
readonly entries: readonly SkillEntry[];
|
|
63
|
+
readonly intent: string;
|
|
64
|
+
readonly limit?: number;
|
|
65
|
+
readonly embedder?: Embedder;
|
|
66
|
+
readonly cache?: IntentCache;
|
|
67
|
+
/** Precomputed description vectors, keyed by skill name. */
|
|
68
|
+
readonly vectors?: ReadonlyMap<string, Float32Array>;
|
|
69
|
+
}
|
|
70
|
+
declare const DEFAULT_FIND_LIMIT = 5;
|
|
71
|
+
/**
|
|
72
|
+
* Token overlap and bigram Dice, averaged. Deterministic and dependency-free:
|
|
73
|
+
* this is the answer when there is no model, so it must never be the thing
|
|
74
|
+
* that fails.
|
|
75
|
+
*/
|
|
76
|
+
declare function lexicalScore(intent: string, description: string): number;
|
|
77
|
+
/** Cosine over two equal-width vectors. Zero when either has no magnitude. */
|
|
78
|
+
declare function cosine(a: Float32Array, b: Float32Array): number;
|
|
79
|
+
/**
|
|
80
|
+
* Ranked matches, and which ranking produced them.
|
|
81
|
+
*
|
|
82
|
+
* Returns rather than loads: a search verb that also acts cannot be used to
|
|
83
|
+
* look, and the whole reason this server exists is that looking should be
|
|
84
|
+
* cheap.
|
|
85
|
+
*/
|
|
86
|
+
declare function find(options: FindOptions): Promise<FindResult>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The five tools, and nothing else.
|
|
90
|
+
*
|
|
91
|
+
* The point of the server is that a session's startup cost does not grow with
|
|
92
|
+
* the number of skills installed: the host lists five tools whatever the
|
|
93
|
+
* catalog holds, and a body is read only when one is asked for.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/** Largest file skill_read returns. A skill needing more should split it. */
|
|
97
|
+
declare const READ_BUDGET_BYTES: number;
|
|
98
|
+
/** How many files skill_describe lists before saying the rest were cut. */
|
|
99
|
+
declare const DESCRIBE_FILE_LIMIT = 200;
|
|
100
|
+
declare const CATALOG_PAGE_SIZE = 8;
|
|
101
|
+
declare const CATALOG_PAGE_MAX = 20;
|
|
102
|
+
/** Equality filters over catalog fields. Unset keys match everything. */
|
|
103
|
+
interface SkillFilters {
|
|
104
|
+
readonly tier?: string;
|
|
105
|
+
readonly package?: string;
|
|
106
|
+
readonly level?: string;
|
|
107
|
+
readonly domain?: string;
|
|
108
|
+
readonly medium?: string;
|
|
109
|
+
}
|
|
110
|
+
type ReadResult = {
|
|
111
|
+
readonly ok: true;
|
|
112
|
+
readonly name: string;
|
|
113
|
+
readonly path: string;
|
|
114
|
+
readonly bytes: number;
|
|
115
|
+
readonly sha256: string;
|
|
116
|
+
readonly text: string;
|
|
117
|
+
} | {
|
|
118
|
+
readonly ok: false;
|
|
119
|
+
readonly error: string;
|
|
120
|
+
readonly detail: string;
|
|
121
|
+
};
|
|
122
|
+
interface SkillServerOptions extends CatalogOptions {
|
|
123
|
+
readonly embedder?: Embedder;
|
|
124
|
+
readonly cache?: IntentCache;
|
|
125
|
+
readonly vectors?: ReadonlyMap<string, Float32Array>;
|
|
126
|
+
/** Injected for tests. Reads the catalog off disk otherwise. */
|
|
127
|
+
readonly loadCatalog?: (options: CatalogOptions) => SkillEntry[];
|
|
128
|
+
}
|
|
129
|
+
interface CatalogResponse {
|
|
130
|
+
readonly count: number;
|
|
131
|
+
readonly page: number;
|
|
132
|
+
readonly totalPages: number;
|
|
133
|
+
readonly hasNext: boolean;
|
|
134
|
+
readonly packages: readonly {
|
|
135
|
+
readonly package: string;
|
|
136
|
+
readonly skills: readonly {
|
|
137
|
+
readonly name: string;
|
|
138
|
+
readonly description: string;
|
|
139
|
+
readonly tier?: string;
|
|
140
|
+
}[];
|
|
141
|
+
}[];
|
|
142
|
+
}
|
|
143
|
+
interface CallResponse {
|
|
144
|
+
readonly name: string;
|
|
145
|
+
readonly body: string;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The catalog, the search and the read, as plain functions.
|
|
149
|
+
*
|
|
150
|
+
* Separate from the MCP wiring so they can be exercised without a transport,
|
|
151
|
+
* and so a consumer that wants them from a hook or a script is not made to
|
|
152
|
+
* speak a protocol to get them.
|
|
153
|
+
*/
|
|
154
|
+
declare function createSkillTools(options: SkillServerOptions): {
|
|
155
|
+
/**
|
|
156
|
+
* Grouped, and without bodies. The cheap overview and the search's miss
|
|
157
|
+
* path. Paged by package so a large catalog never arrives in one answer.
|
|
158
|
+
*/
|
|
159
|
+
catalog(request?: {
|
|
160
|
+
page?: number;
|
|
161
|
+
pageSize?: number;
|
|
162
|
+
tier?: string;
|
|
163
|
+
}): CatalogResponse;
|
|
164
|
+
/** Ranked names. Returns; never loads a body. */
|
|
165
|
+
find(intent: string, limit?: number, filters?: SkillFilters): Promise<FindResult>;
|
|
166
|
+
/** Every property of one skill and the files it carries, without bodies. */
|
|
167
|
+
describe(name: string): {
|
|
168
|
+
name: string;
|
|
169
|
+
package: string;
|
|
170
|
+
description: string;
|
|
171
|
+
tier: string | undefined;
|
|
172
|
+
metadata: Readonly<Record<string, string | readonly string[]>>;
|
|
173
|
+
frontmatter: string;
|
|
174
|
+
files: {
|
|
175
|
+
path: string;
|
|
176
|
+
bytes: number;
|
|
177
|
+
sha256: string;
|
|
178
|
+
}[];
|
|
179
|
+
filesTruncated: boolean;
|
|
180
|
+
} | null;
|
|
181
|
+
/**
|
|
182
|
+
* One file inside a skill's directory. Paths that leave the directory,
|
|
183
|
+
* directly or through a link, are refused; nothing is ever executed.
|
|
184
|
+
*/
|
|
185
|
+
read(name: string, path?: string): ReadResult;
|
|
186
|
+
/** One body, at the moment it is needed. */
|
|
187
|
+
call(name: string): CallResponse | null;
|
|
188
|
+
/** Drops the memoised catalog, so the next call re-reads the tree. */
|
|
189
|
+
refresh(): void;
|
|
190
|
+
entries: () => Omit<SkillEntry, "path">[];
|
|
191
|
+
};
|
|
192
|
+
type SkillTools = ReturnType<typeof createSkillTools>;
|
|
193
|
+
|
|
194
|
+
export { CATALOG_PAGE_MAX as C, DEFAULT_FIND_LIMIT as D, type Embedder as E, type FindOptions as F, type IntentCache as I, type Match as M, READ_BUDGET_BYTES as R, type SkillServerOptions as S, type SkillTools as a, CATALOG_PAGE_SIZE as b, type CallResponse as c, type CatalogResponse as d, DESCRIBE_FILE_LIMIT as e, type FindResult as f, type IntentCacheStats as g, type Ranking as h, type ReadResult as i, type SkillFilters as j, cosine as k, createSkillTools as l, find as m, lexicalScore as n, normaliseIntent as o };
|
package/dist/stdio.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { C as CatalogOptions } from './catalog-BiPLcbLu.js';
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* The stdio entry a host launches.
|
|
4
6
|
*
|
|
@@ -6,9 +8,10 @@
|
|
|
6
8
|
* the dependency tree is its data source, and from an npx cache it is not a
|
|
7
9
|
* sibling of the consumer's packages and would find no skills at all.
|
|
8
10
|
*/
|
|
11
|
+
|
|
9
12
|
/** `node_modules` roots to walk, from argv or from the working directory. */
|
|
10
13
|
declare function resolveRoots(argv: readonly string[], cwd: string): string[];
|
|
11
|
-
declare function serve(
|
|
14
|
+
declare function serve(catalog: CatalogOptions): void;
|
|
12
15
|
declare function main(argv?: readonly string[]): void;
|
|
13
16
|
|
|
14
17
|
export { main, resolveRoots, serve };
|
package/dist/stdio.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modootoday/pmcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"author": "modootoday",
|
|
6
6
|
"description": "An MCP server that finds the skills your installed packages already ship. Reads SKILL.md out of node_modules by shape rather than by path, so it sees the skills, .agent, .claude, .gemini and .codex conventions alike, and keeps session startup flat as the number of installed skills grows.",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
"types": "./dist/stdio.d.ts",
|
|
38
38
|
"import": "./dist/stdio.js"
|
|
39
39
|
},
|
|
40
|
+
"./http": {
|
|
41
|
+
"types": "./dist/http.d.ts",
|
|
42
|
+
"import": "./dist/http.js"
|
|
43
|
+
},
|
|
40
44
|
"./cli": {
|
|
41
45
|
"types": "./dist/cli.d.ts",
|
|
42
46
|
"import": "./dist/cli.js"
|