skillmds 1.1.2 → 1.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.
package/dist/mcp.js ADDED
@@ -0,0 +1,579 @@
1
+ import {
2
+ AGENTS,
3
+ IntegrityError,
4
+ agentDir,
5
+ canonicalDir,
6
+ createClient,
7
+ detectAgents,
8
+ fetchBundle,
9
+ installSkill,
10
+ looksLikeProject,
11
+ resolvePackFiles,
12
+ safeText,
13
+ stripTerminalEscapes,
14
+ telemetryDisabled
15
+ } from "./chunk-A7BKAUUO.js";
16
+
17
+ // src/mcp/server.ts
18
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
21
+ import { readFileSync } from "fs";
22
+
23
+ // src/mcp/tools.ts
24
+ import { lint as lint2, skillMdFor as skillMdFor2 } from "@skillmds/core";
25
+
26
+ // src/mcp/schemas.ts
27
+ var SKILL_ITEM = {
28
+ type: "object",
29
+ description: "A skill as listed in search and leaderboard results.",
30
+ properties: {
31
+ slug: { type: "string", description: "owner/name identifier, e.g. anthropic/pdf" },
32
+ title: { type: "string", description: "Human-readable skill title" },
33
+ description: { type: "string", description: "What the skill does and when an agent should use it" },
34
+ category: { type: ["string", "null"], description: "Category name" },
35
+ agents: { type: "array", items: { type: "string" }, description: "Agent ids the skill is published for" },
36
+ owner_handle: { type: "string", description: "Publisher handle" },
37
+ owner_name: { type: "string", description: "Publisher display name" },
38
+ license: { type: ["string", "null"], description: "SPDX license id when declared" },
39
+ repo_stars: { type: ["number", "null"], description: "GitHub stars of the source repository" },
40
+ ai_audit_verdict: {
41
+ type: ["string", "null"],
42
+ description: "Safety review verdict: pass, caution, warning, fail or inconclusive"
43
+ },
44
+ verdict: { type: ["string", "null"], description: "Safety verdict from the registry's audits" },
45
+ security_flags: {
46
+ type: "array",
47
+ items: { type: "string" },
48
+ description: "Capability flags: docs_only, network_calls, executes_scripts, reads_secrets, untrusted_install"
49
+ },
50
+ install_snippet: { type: "string", description: "One-line install command" },
51
+ install: { type: "object", additionalProperties: true, description: "Install snippets (cli, mcp)" }
52
+ },
53
+ // Only `slug` is guaranteed. Requiring title/description would make a client
54
+ // that validates structuredContent reject a sparse row the registry is
55
+ // entitled to return — and an agent can still act on a slug alone.
56
+ required: ["slug"],
57
+ additionalProperties: true
58
+ };
59
+ var ITEMS = {
60
+ type: "object",
61
+ properties: { items: { type: "array", items: SKILL_ITEM, description: "Matching skills, best first" } },
62
+ required: ["items"]
63
+ };
64
+ var LINT_RESULT = {
65
+ type: "object",
66
+ properties: {
67
+ ok: { type: "boolean", description: "True when there are no error-level diagnostics" },
68
+ score: { type: "integer", description: "Quality score 0-100" },
69
+ diagnostics: {
70
+ type: "array",
71
+ items: {
72
+ type: "object",
73
+ properties: {
74
+ id: { type: "string", description: "Rule id, e.g. SK011" },
75
+ severity: { type: "string", enum: ["error", "warn", "info"] },
76
+ message: { type: "string" },
77
+ line: { type: ["integer", "null"] }
78
+ },
79
+ required: ["id", "severity", "message"],
80
+ additionalProperties: true
81
+ }
82
+ },
83
+ security: { type: "object", additionalProperties: true, description: "Scanner findings and capability flags" }
84
+ },
85
+ required: ["ok", "score", "diagnostics"]
86
+ };
87
+ var INSTALL_RESULT = {
88
+ type: "object",
89
+ properties: {
90
+ installed: { type: "boolean", description: "Whether files were written" },
91
+ slug: { type: "string", description: "owner/name of the skill" },
92
+ scope: { type: "string", enum: ["project", "global"], description: "Which set of agent folders was written" },
93
+ canonical: { type: "string", description: "Absolute path of the canonical copy every target links to" },
94
+ targets: {
95
+ type: "array",
96
+ description: "Agent skill dirs the canonical copy was linked or copied into; empty when only the canonical copy was written",
97
+ items: {
98
+ type: "object",
99
+ properties: { agent: { type: "string" }, path: { type: "string" }, mode: { type: "string" } },
100
+ required: ["agent", "path", "mode"]
101
+ }
102
+ },
103
+ skipped: {
104
+ type: "array",
105
+ description: "Agents that were not written, with the reason",
106
+ items: {
107
+ type: "object",
108
+ properties: { agent: { type: "string" }, reason: { type: "string" } },
109
+ required: ["agent", "reason"]
110
+ }
111
+ },
112
+ files_written: { type: "integer", description: "Number of files written" },
113
+ security_flags: { type: "array", items: { type: "string" }, description: "Capability flags reported for the skill" },
114
+ note: { type: "string", description: "Caveats, e.g. when only SKILL.md could be written" }
115
+ },
116
+ required: ["installed", "slug", "scope", "canonical", "targets", "files_written"]
117
+ };
118
+ var OMIT_FIELDS = /* @__PURE__ */ new Set([
119
+ "type",
120
+ "verified",
121
+ "avg_rating",
122
+ "rating_count",
123
+ "install_count",
124
+ "save_count",
125
+ "owner_avatar",
126
+ "confident",
127
+ "category_slug"
128
+ ]);
129
+
130
+ // src/mcp/install.ts
131
+ import { resolve } from "path";
132
+ import { lint, skillMdFor } from "@skillmds/core";
133
+
134
+ // src/mcp/types.ts
135
+ var notice = (msg) => ({ isError: true, content: [{ type: "text", text: msg }] });
136
+ function parseSlug(slug) {
137
+ const m = String(slug ?? "").match(/^([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/i);
138
+ return m ? [m[1], m[2]] : null;
139
+ }
140
+
141
+ // src/mcp/install.ts
142
+ function agentForDest(dest, scopeOpts) {
143
+ const target = resolve(dest);
144
+ for (const global of [false, true]) {
145
+ if (resolve(canonicalDir({ ...scopeOpts, global })) === target) return { agent: null, global };
146
+ for (const a of AGENTS) {
147
+ if (global && a.global === null) continue;
148
+ if (resolve(agentDir(a.id, { ...scopeOpts, global })) === target) return { agent: a.id, global };
149
+ }
150
+ }
151
+ return null;
152
+ }
153
+ function skillMdOf(files) {
154
+ const f = files.find((x) => x.path === "SKILL.md");
155
+ if (!f) return null;
156
+ return typeof f.contents === "string" ? f.contents : f.contents.toString("utf8");
157
+ }
158
+ async function installFromRegistry(args, ctx, deps = {}) {
159
+ const getBundle = deps.fetchBundle ?? fetchBundle;
160
+ const getPackFiles = deps.resolvePackFiles ?? resolvePackFiles;
161
+ const parsed = parseSlug(args.slug);
162
+ if (!parsed) return notice(`Refused: invalid slug "${String(args.slug)}" \u2014 expected owner/name.`);
163
+ const [owner, name] = parsed;
164
+ const base = { cwd: ctx.cwd, home: ctx.home, env: ctx.env };
165
+ let agents = Array.isArray(args.agents) ? args.agents.map(String) : void 0;
166
+ const dest = typeof args.dest === "string" && args.dest ? args.dest : void 0;
167
+ if (dest && agents) return notice("Refused: pass either `dest` or `agents`, not both. `dest` names one directory; prefer scope + agents.");
168
+ let global;
169
+ let canonicalOnly = false;
170
+ if (dest) {
171
+ const hit = agentForDest(dest, base);
172
+ if (!hit) {
173
+ return notice(
174
+ `Refused: dest "${dest}" is not a known agent skills directory. Use scope ("project"|"global") and agents instead; known roots: ${[...new Set(AGENTS.map((a) => a.project.join("/")))].join(", ")}.`
175
+ );
176
+ }
177
+ global = hit.global;
178
+ if (hit.agent === null) canonicalOnly = true;
179
+ else agents = [hit.agent];
180
+ } else if (args.scope === "project" || args.scope === "global") {
181
+ global = args.scope === "global";
182
+ } else {
183
+ global = !looksLikeProject(resolve(ctx.cwd ?? process.cwd()));
184
+ }
185
+ const scope = { ...base, global };
186
+ const detected = detectAgents(scope);
187
+ const targets = canonicalOnly ? [] : agents?.length ? agents : detected.length ? detected : ["claude-code"];
188
+ for (const a of targets) {
189
+ if (!AGENTS.some((x) => x.id === a)) return notice(`Unknown agent "${a}". Known: ${AGENTS.map((x) => x.id).join(", ")}`);
190
+ }
191
+ const skill = await ctx.api(`/api/skills/${owner}/${name}`);
192
+ const flags = skill.security_flags ?? [];
193
+ const deny = Array.isArray(args.deny) ? args.deny.map(String) : [];
194
+ const denied = flags.filter((f) => deny.includes(f));
195
+ if (denied.length) return notice(`Refused: ${owner}/${name} carries denied security flags: ${denied.join(", ")}. Not installed.`);
196
+ const raw = skillMdFor(skill);
197
+ const refuseLint = (content) => {
198
+ const result = lint(content, { slug: name });
199
+ if (result.ok) return null;
200
+ const errors = result.diagnostics.filter((d) => d.severity === "error");
201
+ return notice(`Refused: ${owner}/${name} failed validation (${errors.map((d) => `${d.id}: ${d.message}`).join("; ")}). Not installed.`);
202
+ };
203
+ const bad = refuseLint(raw);
204
+ if (bad) return bad;
205
+ let files = [{ path: "SKILL.md", contents: raw }];
206
+ let note = "";
207
+ if (skill.type === "pack") {
208
+ try {
209
+ const bundle = await getBundle(ctx.base, `${owner}/${name}`, ctx.token);
210
+ if (bundle?.length) {
211
+ files = bundle.some((f) => f.path === "SKILL.md") ? bundle : [...bundle, { path: "SKILL.md", contents: raw }];
212
+ } else {
213
+ const pack = await getPackFiles(skill);
214
+ if (pack) {
215
+ files = pack.files;
216
+ if (pack.pinMiss) note = "pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead";
217
+ } else note = "pack assets could not be fetched \u2014 only SKILL.md was written";
218
+ }
219
+ } catch (e) {
220
+ if (e instanceof IntegrityError) return notice(`Refused: ${owner}/${name} failed integrity verification \u2014 ${e.message}. Not installed.`);
221
+ note = `pack assets could not be fetched (${e instanceof Error ? e.message : String(e)}) \u2014 only SKILL.md was written`;
222
+ }
223
+ }
224
+ const written = skillMdOf(files);
225
+ if (written === null) files = [...files, { path: "SKILL.md", contents: raw }];
226
+ else if (written !== raw) {
227
+ const badWritten = refuseLint(written);
228
+ if (badWritten) return badWritten;
229
+ }
230
+ let r;
231
+ try {
232
+ r = await installSkill({
233
+ name,
234
+ files,
235
+ source: `registry:${owner}/${name}`,
236
+ commit_sha: skill.commit_sha ?? void 0,
237
+ scope,
238
+ agents: targets,
239
+ explicitAgents: canonicalOnly || Boolean(agents?.length),
240
+ mode: args.mode === "copy" ? "copy" : "link",
241
+ force: Boolean(args.force)
242
+ });
243
+ } catch (e) {
244
+ return notice(`Refused: ${e instanceof Error ? e.message : String(e)}`);
245
+ }
246
+ if (!telemetryDisabled(ctx.env ?? process.env)) {
247
+ ctx.api(`/api/skills/${owner}/${name}/install`, {
248
+ method: "POST",
249
+ headers: { "content-type": "application/json" },
250
+ body: JSON.stringify({ via: "mcp" })
251
+ }).catch(() => {
252
+ });
253
+ }
254
+ const summary = {
255
+ installed: true,
256
+ slug: `${owner}/${name}`,
257
+ scope: global ? "global" : "project",
258
+ canonical: r.canonical,
259
+ targets: r.targets,
260
+ skipped: r.skipped,
261
+ // installSkill writes the whole tree or throws, so every file in `files`
262
+ // reached disk — the count is the list's length, not a running tally.
263
+ files_written: files.length,
264
+ security_flags: flags,
265
+ ...note ? { note } : {}
266
+ };
267
+ const where = r.targets.length ? r.targets.map((t) => `${t.agent} \u2192 ${t.path} (${t.mode})`).join("; ") : `${r.canonical} (canonical only)`;
268
+ return {
269
+ content: [
270
+ {
271
+ type: "text",
272
+ text: `Installed ${owner}/${name} [${summary.scope}]: ${where}. ${files.length} file(s); security: ${flags.join(", ") || "unscanned"}.${note ? ` NOTE: ${note}` : ""}`
273
+ }
274
+ ],
275
+ structuredContent: summary
276
+ };
277
+ }
278
+
279
+ // src/mcp/tools.ts
280
+ var TOOLS = [
281
+ {
282
+ name: "skillmd_search",
283
+ title: "Search skills",
284
+ description: "Search the SkillMD registry of Agent Skills (SKILL.md files) by task, name or keywords. Returns matching skills with their owner/name slug, description, category, safety verdict and install snippet, best match first.",
285
+ inputSchema: {
286
+ type: "object",
287
+ properties: {
288
+ query: { type: "string", description: "Natural-language query or keywords describing the task or skill" },
289
+ category: { type: "string", description: "Restrict to one category slug, e.g. coding, devops, data, security" },
290
+ verified_only: { type: "boolean", description: "Return only skills from verified publishers (default false)" },
291
+ type: { type: "string", enum: ["single", "pack"], description: "Restrict to single-file skills or multi-file packs" },
292
+ min_rating: { type: "number", minimum: 0, maximum: 5, description: "Minimum average rating, 0 to 5" },
293
+ limit: { type: "integer", minimum: 1, maximum: 100, description: "Maximum number of results (default 20)" }
294
+ },
295
+ required: ["query"],
296
+ additionalProperties: false
297
+ },
298
+ outputSchema: ITEMS,
299
+ annotations: { title: "Search skills", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
300
+ },
301
+ {
302
+ name: "skillmd_get",
303
+ title: "Get skill details",
304
+ description: "Fetch one skill by owner/name slug, including its SKILL.md body, provenance (source repo and pinned commit), license, safety verdict and capability flags.",
305
+ inputSchema: {
306
+ type: "object",
307
+ properties: {
308
+ slug: { type: "string", pattern: "^[^/]+/[^/]+$", description: "Skill identifier as owner/name, e.g. anthropic/pdf" }
309
+ },
310
+ required: ["slug"],
311
+ additionalProperties: false
312
+ },
313
+ outputSchema: {
314
+ type: "object",
315
+ description: "Full skill record.",
316
+ properties: {
317
+ slug: { type: "string", description: "owner/name identifier, e.g. anthropic/pdf" },
318
+ title: { type: "string", description: "Human-readable skill title" },
319
+ description: { type: "string", description: "What the skill does and when an agent should use it" },
320
+ category: { type: "string", description: "Category name" },
321
+ category_slug: { type: "string", description: "Category slug, as used by search and trending filters" },
322
+ owner_handle: { type: "string", description: "Publisher handle" },
323
+ owner_name: { type: "string", description: "Publisher display name" },
324
+ license: { type: ["string", "null"], description: "SPDX license id when declared" },
325
+ security_flags: {
326
+ type: "array",
327
+ items: { type: "string" },
328
+ description: "Capability flags: docs_only, network_calls, executes_scripts, reads_secrets, untrusted_install"
329
+ },
330
+ ai_audit_verdict: { type: ["string", "null"], description: "Safety review verdict: pass, caution, warning, fail or inconclusive" },
331
+ repo_stars: { type: ["number", "null"], description: "GitHub stars of the source repository" },
332
+ install_snippet: { type: "string", description: "One-line install command" },
333
+ type: { type: "string", enum: ["single", "pack"], description: "single SKILL.md or a multi-file pack" },
334
+ body_md: { type: "string", description: "Markdown body of SKILL.md" },
335
+ raw_md: { type: ["string", "null"], description: "Verbatim SKILL.md including frontmatter when stored" },
336
+ verified: { type: "boolean", description: "Published by a verified publisher" },
337
+ source_repo: { type: ["string", "null"], description: "Upstream repository" },
338
+ commit_sha: { type: ["string", "null"], description: "Pinned upstream commit" },
339
+ files: { type: "array", description: "Companion files for packs", items: { type: "object", additionalProperties: true } },
340
+ tags: { type: "array", items: { type: "object", additionalProperties: true }, description: "Registry tags" },
341
+ categories: { type: "array", items: { type: "object", additionalProperties: true }, description: "Every category the skill belongs to" }
342
+ },
343
+ required: ["slug", "title", "description"],
344
+ additionalProperties: true
345
+ },
346
+ annotations: { title: "Get skill details", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
347
+ },
348
+ {
349
+ name: "skillmd_install",
350
+ title: "Install skill",
351
+ description: "Install a skill by owner/name slug: validates it, then writes one canonical copy (.agents/skills/<name>) and links it into the chosen agents' skill directories. Never executes scripts. The response reports the skill's capability flags (docs_only, network_calls, executes_scripts, reads_secrets) as information.",
352
+ inputSchema: {
353
+ type: "object",
354
+ properties: {
355
+ slug: { type: "string", pattern: "^[^/]+/[^/]+$", description: "Skill identifier as owner/name, e.g. anthropic/pdf" },
356
+ scope: {
357
+ type: "string",
358
+ enum: ["project", "global"],
359
+ description: "project = the current directory's agent folders (default when the cwd looks like a project); global = the user's home-level agent folders"
360
+ },
361
+ agents: {
362
+ type: "array",
363
+ items: { type: "string" },
364
+ description: "Agent ids to link the skill into (default: every agent detected on this machine)"
365
+ },
366
+ mode: { type: "string", enum: ["link", "copy"], description: "link (junction/symlink to the canonical copy, default) or copy" },
367
+ force: { type: "boolean", description: "Replace a skill installed from a different source or an untracked directory" },
368
+ deny: { type: "array", items: { type: "string" }, description: "Refuse the skill if it carries any of these security flags" },
369
+ dest: {
370
+ type: "string",
371
+ description: "Deprecated: an explicit agent skills directory. Must be a known agent skills dir; prefer scope + agents."
372
+ }
373
+ },
374
+ required: ["slug"],
375
+ additionalProperties: false
376
+ },
377
+ outputSchema: INSTALL_RESULT,
378
+ annotations: { title: "Install skill", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
379
+ },
380
+ {
381
+ name: "skillmd_trending",
382
+ title: "Trending skills",
383
+ description: "Top skills on the SkillMD leaderboard, ranked by installs, saves and ratings, optionally within one category and time window.",
384
+ inputSchema: {
385
+ type: "object",
386
+ properties: {
387
+ range: { type: "string", enum: ["30d", "all"], description: "Ranking window: last 30 days or all time (default all)" },
388
+ category: { type: "string", description: "Restrict to one category slug, e.g. coding, devops, data, security" },
389
+ limit: { type: "integer", minimum: 1, maximum: 100, description: "Maximum number of results (default 20)" }
390
+ },
391
+ additionalProperties: false
392
+ },
393
+ outputSchema: ITEMS,
394
+ annotations: { title: "Trending skills", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
395
+ },
396
+ {
397
+ name: "skillmd_recommend",
398
+ title: "Recommend skills",
399
+ description: "Recommend skills. With based_on set to a skill's owner/name slug, returns the top skills in that skill's category (excluding itself); without it, returns what is trending over the last 30 days.",
400
+ inputSchema: {
401
+ type: "object",
402
+ properties: {
403
+ based_on: { type: "string", pattern: "^[^/]+/[^/]+$", description: "Skill slug (owner/name) to base recommendations on" },
404
+ limit: { type: "integer", minimum: 1, maximum: 100, description: "Maximum number of results (default 10)" }
405
+ },
406
+ additionalProperties: false
407
+ },
408
+ outputSchema: ITEMS,
409
+ annotations: { title: "Recommend skills", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
410
+ },
411
+ {
412
+ name: "skillmd_list_saved",
413
+ title: "List saved skills",
414
+ description: "List the signed-in user's saved skills. Requires the SKILLMD_TOKEN environment variable (a SkillMD personal access token).",
415
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
416
+ outputSchema: ITEMS,
417
+ annotations: { title: "List saved skills", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
418
+ },
419
+ {
420
+ name: "skillmd_lint",
421
+ title: "Lint a SKILL.md",
422
+ description: "Validate a SKILL.md against the SkillMD format and return diagnostics, capability flags and a 0-100 quality score. Pass raw SKILL.md content, or a registry slug (owner/name) to fetch and lint that skill.",
423
+ inputSchema: {
424
+ type: "object",
425
+ properties: {
426
+ content: { type: "string", description: "Raw SKILL.md text to validate (frontmatter plus body)" },
427
+ slug: {
428
+ type: "string",
429
+ pattern: "^[^/]+/[^/]+$",
430
+ description: "Registry skill (owner/name) to fetch and validate instead of content"
431
+ }
432
+ },
433
+ additionalProperties: false
434
+ },
435
+ outputSchema: LINT_RESULT,
436
+ annotations: { title: "Lint a SKILL.md", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
437
+ }
438
+ ];
439
+ var text = (obj) => {
440
+ if (typeof obj === "string") return { content: [{ type: "text", text: obj }] };
441
+ const structured = Array.isArray(obj) ? { items: obj } : obj;
442
+ return { content: [{ type: "text", text: JSON.stringify(structured, null, 2) }], structuredContent: structured };
443
+ };
444
+ var clean = (items) => (items ?? []).map(
445
+ (s) => Object.fromEntries(
446
+ Object.entries(s).filter(([k]) => !OMIT_FIELDS.has(k)).map(([k, v]) => [k, typeof v === "string" ? safeText(v, 2e3) : v])
447
+ )
448
+ );
449
+ var VERBATIM_FIELDS = /* @__PURE__ */ new Set(["body_md", "raw_md"]);
450
+ var sanitizeRecord = (skill) => Object.fromEntries(
451
+ Object.entries(skill).map(([k, v]) => {
452
+ if (typeof v !== "string") return [k, v];
453
+ return [k, VERBATIM_FIELDS.has(k) ? stripTerminalEscapes(v) : safeText(v, 2e3)];
454
+ })
455
+ );
456
+ async function handleCall(name, args, ctx) {
457
+ try {
458
+ switch (name) {
459
+ case "skillmd_search": {
460
+ const p = new URLSearchParams({ q: String(args.query ?? ""), limit: String(args.limit ?? 20) });
461
+ if (args.category) p.set("category", String(args.category));
462
+ if (args.verified_only) p.set("verified", "true");
463
+ if (args.type) p.set("type", String(args.type));
464
+ if (args.min_rating != null) p.set("min_rating", String(args.min_rating));
465
+ const r = await ctx.api(`/api/search?${p}`);
466
+ return text(clean(r.items));
467
+ }
468
+ case "skillmd_get": {
469
+ const s = parseSlug(args.slug);
470
+ if (!s) return notice("Provide `slug` as owner/name.");
471
+ const skill = await ctx.api(`/api/skills/${s[0]}/${s[1]}`);
472
+ return text(sanitizeRecord(skill));
473
+ }
474
+ case "skillmd_trending": {
475
+ const p = new URLSearchParams({ range: String(args.range ?? "all"), limit: String(args.limit ?? 20) });
476
+ if (args.category) p.set("category", String(args.category));
477
+ const r = await ctx.api(`/api/leaderboard?${p}`);
478
+ return text(clean(r.items));
479
+ }
480
+ case "skillmd_recommend": {
481
+ const limit = Number(args.limit ?? 10);
482
+ const s = parseSlug(args.based_on);
483
+ if (s) {
484
+ const base = await ctx.api(`/api/skills/${s[0]}/${s[1]}`).catch(() => null);
485
+ if (base?.category_slug) {
486
+ const r2 = await ctx.api(
487
+ `/api/leaderboard?range=all&limit=${limit}&category=${encodeURIComponent(base.category_slug)}`
488
+ );
489
+ return text(clean(r2.items).filter((x) => x.slug !== args.based_on));
490
+ }
491
+ }
492
+ const r = await ctx.api(`/api/leaderboard?range=30d&limit=${limit}`);
493
+ return text(clean(r.items));
494
+ }
495
+ case "skillmd_list_saved": {
496
+ if (!ctx.hasToken) return notice("Set SKILLMD_TOKEN (a SkillMD personal access token) to list your saved skills.");
497
+ const r = await ctx.api(`/api/favorites`);
498
+ return text(clean(r.items));
499
+ }
500
+ case "skillmd_lint": {
501
+ let raw = typeof args.content === "string" ? args.content : void 0;
502
+ let slug;
503
+ let verified = false;
504
+ if (!raw && args.slug) {
505
+ const s = parseSlug(args.slug);
506
+ if (!s) return notice("Provide `slug` as owner/name.");
507
+ const skill = await ctx.api(`/api/skills/${s[0]}/${s[1]}`);
508
+ raw = skillMdFor2(skill);
509
+ slug = s[1];
510
+ verified = Boolean(skill.verified);
511
+ }
512
+ if (!raw) return notice("Provide `content` (raw SKILL.md) or `slug` (owner/name).");
513
+ const result = lint2(raw, { ...slug ? { slug } : {}, verified });
514
+ return text({ ok: result.ok, score: result.score, diagnostics: result.diagnostics, security: result.security });
515
+ }
516
+ case "skillmd_install":
517
+ return await installFromRegistry(args, ctx);
518
+ default:
519
+ return notice(`Unknown tool: ${name}`);
520
+ }
521
+ } catch (e) {
522
+ return notice(e instanceof Error ? e.message : String(e));
523
+ }
524
+ }
525
+
526
+ // src/mcp/server.ts
527
+ var VERSION = (() => {
528
+ try {
529
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
530
+ } catch {
531
+ return "0.0.0";
532
+ }
533
+ })();
534
+ function buildContext() {
535
+ try {
536
+ const c = createClient({});
537
+ return { api: c.api, hasToken: c.hasToken, base: c.base, token: c.token, cwd: process.cwd(), home: void 0, env: process.env };
538
+ } catch (e) {
539
+ const why = e instanceof Error ? e.message : String(e);
540
+ const unavailable = async () => {
541
+ throw new Error(`SkillMD client unavailable: ${why}`);
542
+ };
543
+ return { api: unavailable, hasToken: false, base: "", cwd: process.cwd(), home: void 0, env: process.env };
544
+ }
545
+ }
546
+ async function startMcpServer() {
547
+ const server = new Server(
548
+ {
549
+ name: "skillmd",
550
+ version: VERSION,
551
+ title: "SkillMD",
552
+ description: "The open registry of Agent Skills: search, inspect, lint and install 25,000+ linted, security-reviewed SKILL.md files for Claude Code, Cursor, Codex and 60+ other agents.",
553
+ websiteUrl: "https://skillmd.com",
554
+ icons: [
555
+ { src: "https://skillmd.com/favicon.png", mimeType: "image/png", sizes: ["512x512"] },
556
+ { src: "https://skillmd.com/favicon.svg", mimeType: "image/svg+xml", sizes: ["any"] }
557
+ ]
558
+ },
559
+ {
560
+ capabilities: { tools: {} },
561
+ instructions: "SkillMD is a registry of Agent Skills (SKILL.md files). Use skillmd_search to find skills for a task, skillmd_get to inspect one (safety verdict, capability flags, provenance), skillmd_install to write it into the agent's skills directory, and skillmd_lint to validate your own SKILL.md. skillmd_trending and skillmd_recommend surface popular and related skills. Only skillmd_install writes files; it never executes scripts. skillmd_install writes one canonical copy (.agents/skills/<name>) and links it into the chosen agents' skill dirs; pass scope (project|global) and agents to choose where."
562
+ }
563
+ );
564
+ const ctx = buildContext();
565
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
566
+ server.setRequestHandler(
567
+ CallToolRequestSchema,
568
+ async (req) => await handleCall(req.params.name, req.params.arguments ?? {}, ctx)
569
+ );
570
+ await server.connect(new StdioServerTransport());
571
+ console.error("skillmd MCP server running (stdio)");
572
+ }
573
+
574
+ // src/mcp/index.ts
575
+ if (process.argv.length > 2 || process.stdin.isTTY) {
576
+ await import("./cli.js");
577
+ } else {
578
+ await startMcpServer();
579
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillmds",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "SkillMD for agents: the `skillmds` MCP server + the `skillmd` CLI (lint, scan, search, install, publish Agent Skills) from skillmd.com.",
5
5
  "license": "MIT",
6
6
  "author": "SkillMD",
@@ -60,8 +60,7 @@
60
60
  "@skillmds/core": "^1.0.0",
61
61
  "commander": "^12.1.0",
62
62
  "giget": "^3.3.0",
63
- "picocolors": "^1.1.0",
64
- "yaml": "^2.6.1"
63
+ "picocolors": "^1.1.0"
65
64
  },
66
65
  "devDependencies": {
67
66
  "@types/node": "^22.10.0",