@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,6 +1,6 @@
1
1
  import {
2
2
  createSkillServer
3
- } from "./chunk-N3SHKKWG.js";
3
+ } from "./chunk-3NNRQ5A4.js";
4
4
 
5
5
  // src/stdio.ts
6
6
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
@@ -8,8 +8,14 @@ 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(roots, scopes = []) {
12
- const options = scopes.length > 0 ? { roots, scopes } : { roots };
11
+ function serve(roots, scopes = [], marketplaces = []) {
12
+ const options = {
13
+ roots,
14
+ ...scopes.length > 0 ? { scopes } : {},
15
+ ...marketplaces.length > 0 ? { marketplaces } : {},
16
+ onReject: ({ path, reason }) => process.stderr.write(`pmcp: skipped ${path}: ${reason}
17
+ `)
18
+ };
13
19
  serveStdio(() => createSkillServer(options), {
14
20
  // stdout is the protocol. A diagnostic written there corrupts the stream,
15
21
  // so out-of-band errors go to stderr and nowhere else.
@@ -30,4 +36,4 @@ export {
30
36
  serve,
31
37
  main
32
38
  };
33
- //# sourceMappingURL=chunk-AJNIMNCV.js.map
39
+ //# sourceMappingURL=chunk-Y32X35YV.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-SLTUWTII.js";
4
+ } from "./chunk-2NOO5RSP.js";
5
5
  import {
6
6
  serve
7
- } from "./chunk-AJNIMNCV.js";
7
+ } from "./chunk-Y32X35YV.js";
8
8
  import {
9
9
  readCatalog
10
- } from "./chunk-N3SHKKWG.js";
10
+ } from "./chunk-3NNRQ5A4.js";
11
11
 
12
12
  // src/cli/command.ts
13
13
  var ArgumentError = class extends Error {
@@ -302,16 +302,30 @@ var CATALOG_OPTIONS = [
302
302
  describe: "Only packages whose names start with this. Repeatable.",
303
303
  repeat: true,
304
304
  placeholder: "<prefix>"
305
+ },
306
+ {
307
+ name: "marketplace",
308
+ describe: "A plugin marketplace directory (holds .claude-plugin/marketplace.json). Repeatable.",
309
+ repeat: true,
310
+ placeholder: "<dir>"
305
311
  }
306
312
  ];
307
313
  function rootsFrom(context) {
308
314
  const given = many(context.args, "root");
309
315
  return given.length > 0 ? given : [`${context.cwd}/node_modules`];
310
316
  }
311
- function catalogFrom(context) {
317
+ function catalogOptionsFrom(context, onReject) {
312
318
  const scopes = many(context.args, "scope");
313
- const roots = rootsFrom(context);
314
- return readCatalog(scopes.length > 0 ? { roots, scopes } : { roots });
319
+ const marketplaces = many(context.args, "marketplace");
320
+ return {
321
+ roots: rootsFrom(context),
322
+ ...scopes.length > 0 ? { scopes } : {},
323
+ ...marketplaces.length > 0 ? { marketplaces } : {},
324
+ ...onReject ? { onReject } : {}
325
+ };
326
+ }
327
+ function catalogFrom(context, onReject) {
328
+ return readCatalog(catalogOptionsFrom(context, onReject));
315
329
  }
316
330
 
317
331
  // src/commands/reindex.ts
@@ -418,26 +432,39 @@ var indexCommand = {
418
432
  // src/commands/list.ts
419
433
  var listCommand = {
420
434
  name: "list",
421
- describe: "List the skills discovered in node_modules",
422
- usage: "pmcp list [--root <dir>] [--scope <prefix>] [--json]",
435
+ describe: "List the skills discovered in node_modules and marketplaces",
436
+ usage: "pmcp list [--root <dir>] [--scope <prefix>] [--marketplace <dir>] [--json]",
423
437
  options: [
424
438
  ...CATALOG_OPTIONS,
425
439
  { name: "json", describe: "Emit JSON rather than a table.", boolean: true }
426
440
  ],
427
441
  run(context) {
428
- const entries = catalogFrom(context);
442
+ const rejected = [];
443
+ const entries = catalogFrom(
444
+ context,
445
+ (rejection) => rejected.push(rejection)
446
+ );
429
447
  if (context.args.flags.has("json")) {
430
- context.ui.data(`${JSON.stringify(entries, null, 2)}
431
- `);
432
- return entries.length > 0 ? 0 : 1;
448
+ context.ui.data(
449
+ `${JSON.stringify({ skills: entries, rejected }, null, 2)}
450
+ `
451
+ );
452
+ return entries.length > 0 && rejected.length === 0 ? 0 : 1;
433
453
  }
454
+ for (const { path, reason } of rejected)
455
+ context.ui.error("skipped", `${path}: ${reason}`);
434
456
  if (entries.length === 0) {
435
457
  context.ui.error("no skills found", rootsFrom(context).join(" "));
436
458
  return 1;
437
459
  }
438
- context.ui.table(entries.map((entry2) => [entry2.name, entry2.description]));
460
+ context.ui.table(
461
+ entries.map((entry2) => [
462
+ entry2.tier === void 0 ? entry2.name : `${entry2.name} [${entry2.tier}]`,
463
+ entry2.description
464
+ ])
465
+ );
439
466
  context.ui.success(`${entries.length} skills`);
440
- return 0;
467
+ return rejected.length === 0 ? 0 : 1;
441
468
  }
442
469
  };
443
470
 
@@ -445,10 +472,14 @@ var listCommand = {
445
472
  var serveCommand = {
446
473
  name: "serve",
447
474
  describe: "Run the MCP server on stdio (the default with no command)",
448
- usage: "pmcp serve [--root <dir>] [--scope <prefix>]",
475
+ usage: "pmcp serve [--root <dir>] [--scope <prefix>] [--marketplace <dir>]",
449
476
  options: [...CATALOG_OPTIONS],
450
477
  run(context) {
451
- serve(rootsFrom(context), many(context.args, "scope"));
478
+ serve(
479
+ rootsFrom(context),
480
+ many(context.args, "scope"),
481
+ many(context.args, "marketplace")
482
+ );
452
483
  return 0;
453
484
  }
454
485
  };
@@ -2042,7 +2073,7 @@ function dispatch(argv, options = {}) {
2042
2073
  }
2043
2074
  return run(command, rest, { ui, env, cwd });
2044
2075
  }
2045
- var VERSION = "0.1.4";
2076
+ var VERSION = "0.2.0";
2046
2077
  async function run(command, argv, context) {
2047
2078
  try {
2048
2079
  return await command.run({
package/dist/index.d.ts CHANGED
@@ -77,12 +77,25 @@ interface SkillEntry {
77
77
  readonly description: string;
78
78
  /** Absolute path to the SKILL.md. `call` reads the body from here. */
79
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[];
80
86
  }
81
87
  interface CatalogOptions {
82
88
  /** Directories to walk. Each is a `node_modules` root, not a package root. */
83
89
  readonly roots: readonly string[];
84
90
  /** Package name prefixes to consider. Empty means every package. */
85
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;
86
99
  }
87
100
  /**
88
101
  * Directories a walk does not enter. Kept as short as the evidence allows: a
@@ -186,13 +199,65 @@ declare function cosine(a: Float32Array, b: Float32Array): number;
186
199
  declare function find(options: FindOptions): Promise<FindResult>;
187
200
 
188
201
  /**
189
- * The three tools, and nothing else.
202
+ * Skills laid out as an agent plugin marketplace: a directory holding
203
+ * `.claude-plugin/marketplace.json`, whose plugins keep skills at
204
+ * `skills/<skill>/SKILL.md`. No package.json is involved, which is why the
205
+ * node_modules walk never saw them.
206
+ */
207
+
208
+ type MetadataValue = string | readonly string[];
209
+ /** Why a skill was left out rather than served. */
210
+ interface Rejection {
211
+ readonly path: string;
212
+ readonly reason: string;
213
+ }
214
+ /**
215
+ * The `metadata:` block of a frontmatter, one level deep: scalars and inline
216
+ * lists. Deeper nesting is skipped rather than half-read.
217
+ */
218
+ declare function readMetadata(source: string): Record<string, MetadataValue>;
219
+ /** The raw frontmatter text between the fences, for skill_describe. */
220
+ declare function readFrontmatterText(source: string): string;
221
+ /**
222
+ * Every skill one marketplace lists. A skill whose own tier disagrees with the
223
+ * marketplace's is rejected: a mislabelled tier is how a private skill leaks.
224
+ */
225
+ declare function readMarketplace(dir: string, onReject?: (rejection: Rejection) => void): SkillEntry[];
226
+
227
+ /**
228
+ * The five tools, and nothing else.
190
229
  *
191
230
  * The point of the server is that a session's startup cost does not grow with
192
- * the number of skills installed: the host lists three tools whatever the
231
+ * the number of skills installed: the host lists five tools whatever the
193
232
  * catalog holds, and a body is read only when one is asked for.
194
233
  */
195
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
+ };
196
261
  interface SkillServerOptions extends CatalogOptions {
197
262
  readonly embedder?: Embedder;
198
263
  readonly cache?: IntentCache;
@@ -202,11 +267,15 @@ interface SkillServerOptions extends CatalogOptions {
202
267
  }
203
268
  interface CatalogResponse {
204
269
  readonly count: number;
270
+ readonly page: number;
271
+ readonly totalPages: number;
272
+ readonly hasNext: boolean;
205
273
  readonly packages: readonly {
206
274
  readonly package: string;
207
275
  readonly skills: readonly {
208
276
  readonly name: string;
209
277
  readonly description: string;
278
+ readonly tier?: string;
210
279
  }[];
211
280
  }[];
212
281
  }
@@ -222,10 +291,37 @@ interface CallResponse {
222
291
  * speak a protocol to get them.
223
292
  */
224
293
  declare function createSkillTools(options: SkillServerOptions): {
225
- /** Grouped, and without bodies. The cheap overview and the search's miss path. */
226
- catalog(): CatalogResponse;
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;
227
303
  /** Ranked names. Returns; never loads a body. */
228
- find(intent: string, limit?: number): Promise<FindResult>;
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;
229
325
  /** One body, at the moment it is needed. */
230
326
  call(name: string): CallResponse | null;
231
327
  /** Drops the memoised catalog, so the next call re-reads the tree. */
@@ -235,29 +331,29 @@ declare function createSkillTools(options: SkillServerOptions): {
235
331
  type SkillTools = ReturnType<typeof createSkillTools>;
236
332
 
237
333
  /**
238
- * The three tools, spoken over MCP.
334
+ * The five tools, spoken over MCP.
239
335
  *
240
336
  * Thin on purpose: every decision is in `createSkillTools`, and this file only
241
337
  * names the tools and shapes their arguments. A consumer that wants the same
242
- * three operations from a hook or a script uses the tools directly and never
243
- * speaks a protocol.
338
+ * operations from a hook or a script uses the tools directly and never speaks
339
+ * a protocol.
244
340
  */
245
341
 
246
342
  /** Reported to the host on connect. */
247
343
  declare const SERVER_NAME = "pmcp";
248
344
  /**
249
- * Registers the three tools on a server.
345
+ * Registers the five tools on a server.
250
346
  *
251
347
  * Exported separately from the stdio entry so a consumer already running an MCP
252
348
  * server can add these to it rather than starting a second process.
253
349
  */
254
350
  declare function registerSkillTools(server: McpServer, tools: SkillTools): McpServer;
255
351
  /**
256
- * A server with the three tools registered.
352
+ * A server with the five tools registered.
257
353
  *
258
354
  * The catalog is read on first use rather than here, so a host that connects
259
355
  * and never asks pays nothing for the walk.
260
356
  */
261
357
  declare function createSkillServer(options: SkillServerOptions): McpServer;
262
358
 
263
- export { type CallResponse, type CatalogOptions, type CatalogResponse, DEFAULT_FIND_LIMIT, DEFAULT_INTENT_CACHE_LIMIT, type Embedder, type FindOptions, type FindResult, type IntentCache, type IntentCacheOptions, type IntentCacheStats, MAX_SKILL_DEPTH, type Match, type Ranking, SERVER_NAME, SKIPPED_DIRS, type SkillEntry, type SkillServerOptions, type SkillTools, cosine, createSkillServer, createSkillTools, displayPath, find, lexicalScore, normaliseIntent, openIntentCache, readBody, readCatalog, readFrontmatter, registerSkillTools, withoutBody };
359
+ export { CATALOG_PAGE_MAX, CATALOG_PAGE_SIZE, type CallResponse, type CatalogOptions, type CatalogResponse, DEFAULT_FIND_LIMIT, DEFAULT_INTENT_CACHE_LIMIT, DESCRIBE_FILE_LIMIT, type Embedder, type FindOptions, type FindResult, type IntentCache, type IntentCacheOptions, type IntentCacheStats, MAX_SKILL_DEPTH, type Match, type MetadataValue, READ_BUDGET_BYTES, type Ranking, type ReadResult, type Rejection, SERVER_NAME, SKIPPED_DIRS, type SkillEntry, type SkillFilters, type SkillServerOptions, type SkillTools, cosine, createSkillServer, createSkillTools, displayPath, find, lexicalScore, normaliseIntent, openIntentCache, readBody, readCatalog, readFrontmatter, readFrontmatterText, readMarketplace, readMetadata, registerSkillTools, withoutBody };
package/dist/index.js CHANGED
@@ -1,10 +1,14 @@
1
1
  import {
2
2
  DEFAULT_INTENT_CACHE_LIMIT,
3
3
  openIntentCache
4
- } from "./chunk-SLTUWTII.js";
4
+ } from "./chunk-2NOO5RSP.js";
5
5
  import {
6
+ CATALOG_PAGE_MAX,
7
+ CATALOG_PAGE_SIZE,
6
8
  DEFAULT_FIND_LIMIT,
9
+ DESCRIBE_FILE_LIMIT,
7
10
  MAX_SKILL_DEPTH,
11
+ READ_BUDGET_BYTES,
8
12
  SERVER_NAME,
9
13
  SKIPPED_DIRS,
10
14
  cosine,
@@ -17,13 +21,20 @@ import {
17
21
  readBody,
18
22
  readCatalog,
19
23
  readFrontmatter,
24
+ readFrontmatterText,
25
+ readMarketplace,
26
+ readMetadata,
20
27
  registerSkillTools,
21
28
  withoutBody
22
- } from "./chunk-N3SHKKWG.js";
29
+ } from "./chunk-3NNRQ5A4.js";
23
30
  export {
31
+ CATALOG_PAGE_MAX,
32
+ CATALOG_PAGE_SIZE,
24
33
  DEFAULT_FIND_LIMIT,
25
34
  DEFAULT_INTENT_CACHE_LIMIT,
35
+ DESCRIBE_FILE_LIMIT,
26
36
  MAX_SKILL_DEPTH,
37
+ READ_BUDGET_BYTES,
27
38
  SERVER_NAME,
28
39
  SKIPPED_DIRS,
29
40
  cosine,
@@ -37,6 +48,9 @@ export {
37
48
  readBody,
38
49
  readCatalog,
39
50
  readFrontmatter,
51
+ readFrontmatterText,
52
+ readMarketplace,
53
+ readMetadata,
40
54
  registerSkillTools,
41
55
  withoutBody
42
56
  };
package/dist/stdio.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  /** `node_modules` roots to walk, from argv or from the working directory. */
10
10
  declare function resolveRoots(argv: readonly string[], cwd: string): string[];
11
- declare function serve(roots: readonly string[], scopes?: readonly string[]): void;
11
+ declare function serve(roots: readonly string[], scopes?: readonly string[], marketplaces?: readonly string[]): void;
12
12
  declare function main(argv?: readonly string[]): void;
13
13
 
14
14
  export { main, resolveRoots, serve };
package/dist/stdio.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  main,
4
4
  resolveRoots,
5
5
  serve
6
- } from "./chunk-AJNIMNCV.js";
7
- import "./chunk-N3SHKKWG.js";
6
+ } from "./chunk-Y32X35YV.js";
7
+ import "./chunk-3NNRQ5A4.js";
8
8
  export {
9
9
  main,
10
10
  resolveRoots,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modootoday/pmcp",
3
- "version": "0.1.4",
3
+ "version": "0.2.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.",
@@ -57,7 +57,7 @@
57
57
  ],
58
58
  "scripts": {
59
59
  "build": "tsup",
60
- "typecheck": "tsc --noEmit --incremental --tsBuildInfoFile ./dist/.tsbuildinfo && tsc -p tsconfig.tests.json --noEmit",
60
+ "typecheck": "tsgo --noEmit --incremental --tsBuildInfoFile ./dist/.tsbuildinfo --singleThreaded && tsgo -p tsconfig.tests.json --noEmit --singleThreaded",
61
61
  "test": "vitest run",
62
62
  "clean": "rimraf dist",
63
63
  "catalog:refresh": "node scripts/refresh-catalog.mjs",
@@ -84,6 +84,7 @@
84
84
  "devDependencies": {
85
85
  "@types/node": "^22.15.21",
86
86
  "@types/semver": "7.8.0",
87
+ "@typescript/native-preview": "7.0.0-dev.20260707.2",
87
88
  "prettier": "^3.6.2",
88
89
  "rimraf": "^6.0.1",
89
90
  "tsup": "^8.5.1",
@@ -5,9 +5,9 @@ description: A project has grown past a hundred dependencies and nobody knows wh
5
5
 
6
6
  # pmcp
7
7
 
8
- An MCP server that reads `SKILL.md` files out of `node_modules` and serves them
9
- as three tools, so a session pays for the one skill it opens rather than for the
10
- whole catalog.
8
+ An MCP server that reads `SKILL.md` files out of `node_modules` and plugin
9
+ marketplaces and serves them as five tools, so a session pays for the one skill
10
+ it opens rather than for the whole catalog.
11
11
 
12
12
  ## When to reach for it
13
13
 
@@ -108,11 +108,13 @@ discovery will and will not walk.
108
108
 
109
109
  ### Tools on the wire
110
110
 
111
- | Tool | Argument | Returns |
112
- | --------------- | -------------------------------------- | ---------------------------------- |
113
- | `skill_catalog` | optional scope | every skill's name and description |
114
- | `skill_find` | `intent`, a sentence; optional `limit` | ranked matches |
115
- | `skill_call` | `name` | that skill's body |
111
+ | Tool | Argument | Returns |
112
+ | ---------------- | ------------------------------------------------- | ------------------------------------ |
113
+ | `skill_catalog` | optional `page`, `pageSize`, `tier` | each skill's name, description, tier |
114
+ | `skill_find` | `intent`, a sentence; optional `limit`, `filters` | ranked matches |
115
+ | `skill_describe` | `name` | metadata and files, no body |
116
+ | `skill_call` | `name` | that skill's body |
117
+ | `skill_read` | `name`, optional `path` | one file inside the skill, as text |
116
118
 
117
119
  ## Worked examples
118
120