@modootoday/pmcp 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,363 +0,0 @@
1
- // src/intent.ts
2
- function normaliseIntent(intent) {
3
- return intent.toLowerCase().replaceAll(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/u).filter(Boolean).join(" ");
4
- }
5
-
6
- // src/catalog.ts
7
- import { existsSync, readFileSync, readdirSync } from "fs";
8
- import { basename, dirname, join, relative } from "path";
9
- var SKIPPED_DIRS = /* @__PURE__ */ new Set([
10
- "node_modules",
11
- ".git",
12
- ".cache",
13
- ".turbo",
14
- "coverage",
15
- "__tests__",
16
- "__fixtures__"
17
- ]);
18
- var MAX_SKILL_DEPTH = 3;
19
- function readFrontmatter(source) {
20
- if (!source.startsWith("---")) return {};
21
- const end = source.indexOf("\n---", 3);
22
- if (end === -1) return {};
23
- const fields = {};
24
- for (const line of source.slice(3, end).split("\n")) {
25
- const match = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/u.exec(line);
26
- if (!match) continue;
27
- const value = (match[2] ?? "").trim();
28
- if (value === "" || value.startsWith("#")) continue;
29
- fields[match[1]] = value.replace(/^["'](.*)["']$/u, "$1");
30
- }
31
- return fields;
32
- }
33
- function readBody(source) {
34
- if (!source.startsWith("---")) return source;
35
- const end = source.indexOf("\n---", 3);
36
- if (end === -1) return source;
37
- return source.slice(end + 4).replace(/^(?:\r?\n)+/u, "");
38
- }
39
- function skillFiles(packageDir) {
40
- const found = [];
41
- const stack = [[packageDir, 0]];
42
- while (stack.length > 0) {
43
- const next = stack.pop();
44
- if (next === void 0) break;
45
- const [dir, depth] = next;
46
- let entries;
47
- try {
48
- entries = readdirSync(dir, { withFileTypes: true });
49
- } catch {
50
- continue;
51
- }
52
- for (const entry of entries) {
53
- if (entry.isFile()) {
54
- if (entry.name === "SKILL.md") found.push(join(dir, entry.name));
55
- continue;
56
- }
57
- if (!entry.isDirectory()) continue;
58
- if (SKIPPED_DIRS.has(entry.name)) continue;
59
- if (depth + 1 <= MAX_SKILL_DEPTH)
60
- stack.push([join(dir, entry.name), depth + 1]);
61
- }
62
- }
63
- return found.sort();
64
- }
65
- function packageDirs(root) {
66
- if (!existsSync(root)) return [];
67
- const found = [];
68
- for (const entry of readdirSync(root, { withFileTypes: true })) {
69
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
70
- if (entry.name === ".bin" || entry.name === ".cache") continue;
71
- const path = join(root, entry.name);
72
- if (entry.name.startsWith("@")) {
73
- if (!existsSync(path)) continue;
74
- for (const scoped of readdirSync(path, { withFileTypes: true })) {
75
- if (scoped.isDirectory() || scoped.isSymbolicLink()) {
76
- found.push(join(path, scoped.name));
77
- }
78
- }
79
- continue;
80
- }
81
- found.push(path);
82
- }
83
- return found;
84
- }
85
- function readCatalog(options) {
86
- const scopes = options.scopes ?? [];
87
- const found = [];
88
- const seen = /* @__PURE__ */ new Set();
89
- for (const root of options.roots) {
90
- for (const packageDir of packageDirs(root)) {
91
- const manifestPath = join(packageDir, "package.json");
92
- if (!existsSync(manifestPath)) continue;
93
- let packageName;
94
- try {
95
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
96
- if (!manifest.name) continue;
97
- packageName = manifest.name;
98
- } catch {
99
- continue;
100
- }
101
- if (scopes.length > 0 && !scopes.some((s) => packageName.startsWith(s))) {
102
- continue;
103
- }
104
- for (const path of skillFiles(packageDir)) {
105
- let source;
106
- try {
107
- source = readFileSync(path, "utf8");
108
- } catch {
109
- continue;
110
- }
111
- const front = readFrontmatter(source);
112
- const description = front["description"] ?? "";
113
- if (description === "") continue;
114
- const dirName = basename(dirname(path));
115
- const slug = front["name"] ?? (dirName === basename(packageDir) ? packageName : dirName);
116
- const name = `${packageName}/${slug}`;
117
- if (seen.has(name)) continue;
118
- seen.add(name);
119
- found.push({
120
- name,
121
- package: packageName,
122
- slug,
123
- description,
124
- path
125
- });
126
- }
127
- }
128
- }
129
- return found.sort((a, b) => a.name.localeCompare(b.name));
130
- }
131
- function withoutBody({
132
- path,
133
- ...rest
134
- }) {
135
- void path;
136
- return rest;
137
- }
138
- function displayPath(root, entry) {
139
- return relative(root, entry.path).replaceAll("\\", "/");
140
- }
141
-
142
- // src/find.ts
143
- var DEFAULT_FIND_LIMIT = 5;
144
- function tokens(text) {
145
- return normaliseIntent(text).split(" ").filter(Boolean);
146
- }
147
- function bigrams(text) {
148
- const flat = normaliseIntent(text).replaceAll(" ", "");
149
- const out = /* @__PURE__ */ new Set();
150
- for (let i = 0; i + 1 < flat.length; i += 1) out.add(flat.slice(i, i + 2));
151
- return out;
152
- }
153
- function lexicalScore(intent, description) {
154
- const wanted = tokens(intent);
155
- if (wanted.length === 0) return 0;
156
- const have = new Set(tokens(description));
157
- const overlap = wanted.filter((token) => have.has(token)).length / wanted.length;
158
- const a = bigrams(intent);
159
- const b = bigrams(description);
160
- if (a.size === 0 || b.size === 0) return overlap / 2;
161
- let shared = 0;
162
- for (const gram of a) if (b.has(gram)) shared += 1;
163
- const dice = 2 * shared / (a.size + b.size);
164
- return (overlap + dice) / 2;
165
- }
166
- function cosine(a, b) {
167
- if (a.length !== b.length) return 0;
168
- let dot = 0;
169
- let na = 0;
170
- let nb = 0;
171
- for (let i = 0; i < a.length; i += 1) {
172
- dot += a[i] * b[i];
173
- na += a[i] * a[i];
174
- nb += b[i] * b[i];
175
- }
176
- if (na === 0 || nb === 0) return 0;
177
- return dot / (Math.sqrt(na) * Math.sqrt(nb));
178
- }
179
- async function find(options) {
180
- const limit = options.limit ?? DEFAULT_FIND_LIMIT;
181
- const semantic = options.embedder !== void 0 && options.vectors !== void 0 && options.vectors.size > 0;
182
- if (semantic) {
183
- const embedder = options.embedder;
184
- const vectors = options.vectors;
185
- let query = options.cache?.get(options.intent) ?? null;
186
- if (!query) {
187
- query = await embedder.embed(options.intent);
188
- options.cache?.put(options.intent, query);
189
- }
190
- const scored = [];
191
- for (const entry of options.entries) {
192
- const vector = vectors.get(entry.name);
193
- if (!vector) continue;
194
- scored.push({
195
- name: entry.name,
196
- package: entry.package,
197
- description: entry.description,
198
- score: cosine(query, vector)
199
- });
200
- }
201
- if (scored.length === options.entries.length) {
202
- return {
203
- ranking: "semantic",
204
- matches: rank(scored, limit)
205
- };
206
- }
207
- }
208
- return {
209
- ranking: "lexical",
210
- matches: rank(
211
- options.entries.map((entry) => ({
212
- name: entry.name,
213
- package: entry.package,
214
- description: entry.description,
215
- score: lexicalScore(options.intent, entry.description)
216
- })),
217
- limit
218
- )
219
- };
220
- }
221
- function rank(matches, limit) {
222
- return matches.filter((match) => match.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
223
- }
224
-
225
- // src/server.ts
226
- import { readFileSync as readFileSync2 } from "fs";
227
- function createSkillTools(options) {
228
- const load = options.loadCatalog ?? readCatalog;
229
- let entries = null;
230
- const catalog = () => {
231
- entries ??= load({ roots: options.roots, scopes: options.scopes });
232
- return entries;
233
- };
234
- return {
235
- /** Grouped, and without bodies. The cheap overview and the search's miss path. */
236
- catalog() {
237
- const byPackage = /* @__PURE__ */ new Map();
238
- for (const entry of catalog()) {
239
- const list = byPackage.get(entry.package) ?? [];
240
- list.push({ name: entry.name, description: entry.description });
241
- byPackage.set(entry.package, list);
242
- }
243
- return {
244
- count: catalog().length,
245
- packages: [...byPackage.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([pkg, skills]) => ({ package: pkg, skills }))
246
- };
247
- },
248
- /** Ranked names. Returns; never loads a body. */
249
- find(intent, limit) {
250
- return find({
251
- entries: catalog(),
252
- intent,
253
- limit,
254
- embedder: options.embedder,
255
- cache: options.cache,
256
- vectors: options.vectors
257
- });
258
- },
259
- /** One body, at the moment it is needed. */
260
- call(name) {
261
- const entry = catalog().find((candidate) => candidate.name === name);
262
- if (!entry) return null;
263
- return {
264
- name: entry.name,
265
- body: readBody(readFileSync2(entry.path, "utf8"))
266
- };
267
- },
268
- /** Drops the memoised catalog, so the next call re-reads the tree. */
269
- refresh() {
270
- entries = null;
271
- },
272
- entries: () => catalog().map(withoutBody)
273
- };
274
- }
275
-
276
- // src/mcp.ts
277
- import { McpServer } from "@modelcontextprotocol/server";
278
- import { z } from "zod";
279
- var SERVER_NAME = "pmcp";
280
- var readOnly = {
281
- readOnlyHint: true,
282
- destructiveHint: false,
283
- idempotentHint: true,
284
- openWorldHint: false
285
- };
286
- var asText = (value) => ({
287
- content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
288
- });
289
- function registerSkillTools(server, tools) {
290
- server.registerTool(
291
- "skill_catalog",
292
- {
293
- title: "List installed skills without their bodies",
294
- description: "Every skill the installed packages ship, grouped by package, with names and one-line descriptions and no bodies. Use it to see what exists, or when skill_find returns nothing.",
295
- inputSchema: z.object({}),
296
- annotations: readOnly
297
- },
298
- async () => asText(tools.catalog())
299
- );
300
- server.registerTool(
301
- "skill_find",
302
- {
303
- title: "Find skills by describing the problem",
304
- 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.",
305
- inputSchema: z.object({
306
- intent: z.string().min(1).max(256).describe("What you are trying to do, as an English sentence."),
307
- limit: z.number().int().min(1).max(25).optional()
308
- }),
309
- annotations: readOnly
310
- },
311
- async ({ intent, limit }) => asText(await tools.find(intent, limit))
312
- );
313
- server.registerTool(
314
- "skill_call",
315
- {
316
- title: "Read one skill",
317
- 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.",
318
- inputSchema: z.object({
319
- name: z.string().min(1).describe("The skill name, as <package>/<slug>.")
320
- }),
321
- annotations: readOnly
322
- },
323
- async ({ name }) => {
324
- const found = tools.call(name);
325
- if (!found) {
326
- return {
327
- ...asText({
328
- error: "unknown_skill",
329
- name,
330
- hint: "Call skill_catalog for the names that exist."
331
- }),
332
- isError: true
333
- };
334
- }
335
- return { content: [{ type: "text", text: found.body }] };
336
- }
337
- );
338
- return server;
339
- }
340
- function createSkillServer(options) {
341
- const server = new McpServer({ name: SERVER_NAME, version: "0.1.4" });
342
- return registerSkillTools(server, createSkillTools(options));
343
- }
344
-
345
- export {
346
- normaliseIntent,
347
- SKIPPED_DIRS,
348
- MAX_SKILL_DEPTH,
349
- readFrontmatter,
350
- readBody,
351
- readCatalog,
352
- withoutBody,
353
- displayPath,
354
- DEFAULT_FIND_LIMIT,
355
- lexicalScore,
356
- cosine,
357
- find,
358
- createSkillTools,
359
- SERVER_NAME,
360
- registerSkillTools,
361
- createSkillServer
362
- };
363
- //# sourceMappingURL=chunk-N3SHKKWG.js.map