get-skill 1.0.21

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/CHANGELOG.md ADDED
@@ -0,0 +1,117 @@
1
+ # Changelog
2
+
3
+ All notable changes to `skillmds` are documented here. Versions follow [semver](https://semver.org).
4
+
5
+ ## 1.0.21
6
+
7
+ - **No more `tar@6` deprecation warning on install.** Upgraded `giget` 1.x → 3.x, whose GitHub downloads no longer depend on the deprecated (and CVE-flagged) `tar@6.2.1`. The `skillmds` dependency tree is now tar-free; download behavior (`skillmd add <github url>`, pack fallbacks) is unchanged.
8
+
9
+ ## 1.0.20
10
+
11
+ - **`skillmd add` now installs to every agent on your machine.** Detection is machine-level (an agent's config dir under your home, e.g. `~/.cursor`), so agents no longer need a pre-existing `skills/` folder to be targeted. The agent registry grows from 5 to **60+**: Gemini CLI, Antigravity, Kiro, Warp, GitHub Copilot, Cline, Roo Code, Goose, Trae, Qwen Code, Amp, OpenClaw, Kilo Code, Hermes (Nous Research), Continue, Devin, OpenHands, Replit, Mistral Vibe, Zed, Kimi Code, Droid (Factory), Crush, Augment, Junie, and more — with per-agent dirs matching each agent's own convention (`~/.codeium/windsurf/skills` for Windsurf, `~/.gemini/antigravity/skills` for Antigravity, etc.). Agents sharing the cross-agent `.agents/skills` convention are written once and labelled with every agent they serve. `-a/--agent` still overrides.
12
+ - Running from your home directory now behaves like `--global` (project scope rooted at `$HOME` *is* the global scope), so agents whose global skills live outside `~/<agent>/skills` get the right directory.
13
+
14
+ ## 1.0.19
15
+
16
+ - **Clear diagnostics when the registry is blocked.** In sandboxed or proxied environments that deny network egress to `api.skillmd.com`, `skillmd add owner/name` used to fall back to GitHub and surface a misleading "repo not found" error. The CLI now tells 404 ("not in the registry" — GitHub fallback proceeds as before) apart from unreachable (DNS failure, proxy 403, 5xx): if the GitHub fallback also fails, the error names the blocked registry and says what fixes it (allowlist `api.skillmd.com`, or install via the SkillMD MCP server); if the fallback succeeds, a warning notes the copy came from GitHub, not the registry.
17
+
18
+ ## 1.0.18
19
+
20
+ - **Cleaner tool output for agents.** `skillmd_search`, `skillmd_trending`, and `skillmd_recommend` no longer echo site-oriented fields (`avg_rating`, `rating_count`, `install_count`, `save_count`, `verified`, `type`, `owner_avatar`, `category_slug`, and the internal `confident` ranker flag). Results now carry just what an agent needs to pick a skill: slug, title, description, category, agents, repo stars, and the install snippet.
21
+ - **Quality score no longer punishes ordinary skills.** The security flags (`reads_secrets`, `network_calls`, `executes_scripts`) are pattern matches against prose and code samples, not proof of anything unsafe — they now cost 2/1/5 points respectively (was 25/8/15), so a skill that merely documents an env var or a URL keeps its high score. Verified skills are floored at 85 (unless the content fails to parse). `skillmd_lint` passes a skill's verified status through to the score.
22
+
23
+ ## 1.0.16
24
+
25
+ - **Installs are no longer gated on verification.** Any skill installs directly — verified or not — with no flag, no prompt, and no second step, from both the CLI (`skillmd add`) and the MCP `skillmd_install` tool. Security flags (network_calls, executes_scripts, reads_secrets, docs_only) are still shown on the result as information, but never block. `--deny <flag>` remains available as an opt-in for anyone who wants to exclude a flag; `--allow-unverified` is now a no-op kept for backward compatibility.
26
+
27
+ ## 1.0.15
28
+
29
+ - Search is now **browsable in the interactive menu**: arrow through results, press Enter on one to see a card, and install it to your project or user directory — all without leaving the CLI (`← Back` at each level; Esc goes up one step, never quits).
30
+ - Search results are clean two-line entries showing the source repo's **star count** (e.g. `★ 223.7k`) instead of the placeholder rating, with the description truncated to fit your terminal.
31
+ - Fixes for narrow terminals: result rows are width-budgeted so they don't wrap and garble the menu; the result card prints once instead of repainting on every action.
32
+
33
+ ## 1.0.14
34
+
35
+ - Interactive menu (`skillmds` in a terminal) no longer exits after one action — it returns to the menu until you pick **Quit**; cancelling a sub-prompt goes back to the menu instead of quitting.
36
+ - "List installed skills" now shows project **and** global skills together, tagged `[agent/scope]`, replacing the misleading "Include global skills?" prompt (which actually toggled global-only vs project-only). Invalid skills show the parse reason.
37
+ - The in-menu command reference now includes `info`.
38
+
39
+ ## 1.0.13
40
+
41
+ Safe & easy installs release.
42
+
43
+ - **Packs install their full file tree.** `skillmd add owner/pack` now downloads the pack's source directory (pinned to the registry's commit; falls back to the default branch with a warning if the pin is gone upstream), so `assets/` and `references/` files referenced by SKILL.md actually exist. Hardened: symlinks are skipped, 200-file/20MB caps, temp dirs cleaned up.
44
+ - **Unverified skills prompt instead of hard-blocking.** Interactive installs show the lint score and security flags and ask before installing; non-interactive installs (and `-y`) still require the explicit `--allow-unverified`. Blocked output links the skill's page for review.
45
+ - **New `skillmd info <slug>`** shows verification status, security flags, rating, provenance (source repo @ commit), and the install snippet. Unknown subcommands now print a proper error instead of silently starting the MCP server.
46
+ - **MCP `skillmd_install` gained the CLI's safety gates**: refuses unverified skills unless `allow_unverified: true` (only after explicit human approval), validates before writing, reports security flags, and guards against malformed skill names.
47
+ - **Installs are exact.** The registry now stores the published SKILL.md verbatim (`raw_md`) and installs deliver it byte-for-byte; reconstructed frontmatter (legacy rows) is YAML-quoted so descriptions with `:`/`#`/quotes can't corrupt the file.
48
+ - Description limit aligned with Claude Code's real spec (1024 chars, was 500) — valid skills are no longer flagged "(invalid SKILL.md)" by `skillmd list`, and `list` now says *why* a file is invalid.
49
+
50
+ ## 1.0.12
51
+
52
+ - Registry-native `skillmd` CLI shipped inside `skillmds`: `search`, `add`, `list`, `remove`, `update`, `publish`, `login`/`logout`, plus lint/scan/rules/init. (Release notes added retroactively.)
53
+
54
+ ## 1.0.11
55
+
56
+ - Security flags render with full readable labels, each with its own marker: `⚑ Executes Scripts ⚑ Calls Network ⚑ Reads Secrets`.
57
+
58
+ ## 1.0.10
59
+
60
+ - Quality score is now a composite of spec correctness, completeness, and **security risk** — skills that read secrets, execute scripts, or make network calls score lower (a risky/stub skill is no longer a green 100).
61
+ - Lint output ends with a **Failed** section listing each failure's rule, message, and full path, so problems don't scroll off in a large run.
62
+ - New `--errors-only` flag to show only failing skills.
63
+ - Fixed emoji spacing/overlap; rule ids are grouped by severity per skill.
64
+ - The closer states how many skills failed instead of a vague "Problems found".
65
+
66
+ ## 1.0.9
67
+
68
+ - Compact lint output: one line per skill (status dot, score, name, rule-id chips) with a single summary line, instead of a tall multi-row block per skill. Linting a single skill still shows full diagnostic messages.
69
+
70
+ ## 1.0.8
71
+
72
+ - `skillmds` with no command in a terminal now opens the interactive CLI menu; it only starts the MCP server when launched over piped stdio (how MCP clients run it). Previously a bare `npx skillmds` in a terminal appeared to hang on the silent MCP server.
73
+
74
+ ## 1.0.7
75
+
76
+ - Nicer CLI output: colored PASS/FAIL, quality-score bars, `ERROR`/`WARN`/`INFO` badges, and a summary with average score.
77
+ - Running `skillmd` (or `skillmds`) with no command on a terminal now opens a guided interactive menu (lint, scan, search, init, list). Piped/CI still prints help.
78
+ - Progress spinner while scanning.
79
+
80
+ ## 1.0.6
81
+
82
+ - Bundle the banner image in the package and serve it via jsDelivr so it renders on the npm page (the previous absolute URL was unreachable).
83
+
84
+ ## 1.0.5
85
+
86
+ - The `skillmds` binary now dispatches: `skillmds <command>` (lint, scan, add, …) runs the CLI, while `skillmds` with no command starts the MCP server. This makes `npx skillmds lint .` work without the `-p` flag.
87
+
88
+ ## 1.0.4
89
+
90
+ - `skillmd lint`/`scan` print a progress line to stderr and warn when scanning a home directory, so a directory scan never looks like a frozen blank screen.
91
+ - Directory scans skip large build/OS directories (`AppData`, `node_modules`, `target`, …) and cap recursion depth, so `lint .` in a big tree terminates quickly.
92
+ - Clearer message when no `SKILL.md` is found.
93
+ - Bare `skillmd` (no command) prints help.
94
+
95
+ ## 1.0.3
96
+
97
+ - CLI (`skillmd`) and MCP server (`skillmds`) now report their version from `package.json` instead of a hardcoded string.
98
+ - READMEs rewritten; added this changelog.
99
+
100
+ ## 1.0.2
101
+
102
+ - `skillmd lint`: skip directories that cannot be read (e.g. `EPERM` on protected system folders) instead of aborting the scan.
103
+
104
+ ## 1.0.1
105
+
106
+ - Fix `skillmd_lint` in the MCP server: keep `yaml` external in the bundled engine so it loads at runtime (bundling a CommonJS module into ESM raised `Dynamic require of "process"`).
107
+
108
+ ## 1.0.0
109
+
110
+ - Add the `skillmd` CLI, shipped alongside the MCP server in this package. Commands: `lint`/`check`, `scan`, `rules`, `init`, `search`, `add`, `list`, `remove`, `update`, `publish`/`submit`, `login`, `logout`.
111
+ - Validation engine shared with the SkillMD registry: `SKILL.md` parser, lint rules `SK001`–`SK040`, security scan, quality score, and `text`/`json`/`sarif`/`github` output.
112
+ - Add the `skillmd_lint` MCP tool.
113
+ - MCP server and CLI authenticate with `Authorization: Bearer` tokens.
114
+
115
+ ## 0.1.1
116
+
117
+ - Initial MCP server: `skillmd_search`, `skillmd_get`, `skillmd_install`, `skillmd_trending`, `skillmd_recommend`, `skillmd_list_saved`.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ <p align="center">
2
+ <img src="https://skillmd.com/brand/banner.png" alt="SkillMD" width="560">
3
+ </p>
4
+
5
+ # get-skill
6
+
7
+ [![npm version](https://img.shields.io/npm/v/get-skill.svg)](https://www.npmjs.com/package/get-skill)
8
+ [![license](https://img.shields.io/npm/l/get-skill.svg)](#license)
9
+
10
+ Get any [Agent Skill](https://docs.claude.com/en/docs/agents-and-tools/agent-skills) (`SKILL.md`) into your agent, from the [SkillMD](https://skillmd.com) registry — 860,000+ skills, each linted, security-scanned and content-pinned.
11
+
12
+ ```bash
13
+ npx get-skill add anthropic/pdf
14
+ ```
15
+
16
+ > `get-skill` is the same package as [`skillmds`](https://www.npmjs.com/package/skillmds), published under a second, easier-to-remember name. Same build, same version. Either one works.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm i -g get-skill
22
+ ```
23
+
24
+ That gives you three commands off one build:
25
+
26
+ - **`get-skill`** — everything, in one word. `get-skill <command>` runs the CLI; bare `get-skill` starts the MCP server when an MCP client launches it, or the interactive menu in a terminal.
27
+ - **`skillmd`** — the CLI on its own.
28
+ - **`skillmds`** — the MCP server on its own.
29
+
30
+ No install needed to try it:
31
+
32
+ ```bash
33
+ npx get-skill search pdf
34
+ npx get-skill lint .
35
+ ```
36
+
37
+ ## CLI
38
+
39
+ | Command | Description |
40
+ |---|---|
41
+ | `get-skill add <source>` | Install a skill (registry slug, GitHub repo, or local path). Lints before writing. |
42
+ | `get-skill search <query>` | Search the registry. |
43
+ | `get-skill lint [path]` (`check`) | Validate `SKILL.md` files; print diagnostics and a quality score. |
44
+ | `get-skill scan [path]` | Report scripts, network calls, and secret access. |
45
+ | `get-skill list` (`ls`) | List installed skills. |
46
+ | `get-skill remove <names...>` (`rm`) | Remove installed skills. |
47
+ | `get-skill update [names...]` | Update installed skills from the registry. |
48
+ | `get-skill init [name]` | Create a `SKILL.md` from a template. |
49
+ | `get-skill rules [id]` | List lint rules, or show one. |
50
+ | `get-skill publish [path]` (`submit`) | Publish to the registry. Exits non-zero on lint errors. |
51
+ | `get-skill login` / `logout` | Store or remove a token. |
52
+
53
+ All commands accept `--json`.
54
+
55
+ ### add
56
+
57
+ ```bash
58
+ get-skill add anthropic/pdf # registry slug (verified-only by default)
59
+ get-skill add owner/repo # GitHub repo
60
+ get-skill add ./path # local
61
+ get-skill add anthropic/pdf -a claude-code -g
62
+ get-skill add some/skill --deny executes_scripts
63
+ get-skill add some/skill --skip-lint
64
+ ```
65
+
66
+ ### lint
67
+
68
+ ```bash
69
+ get-skill lint ./my-skill
70
+ get-skill lint . --format sarif > skillmd.sarif
71
+ get-skill lint . --strict
72
+ get-skill lint . --fix
73
+ ```
74
+
75
+ Formats: `text` (default), `json`, `sarif` (GitHub Code Scanning), `github` (Actions annotations).
76
+ Exit codes: `0` clean, `1` errors (or warnings with `--strict` / `--fail-on-warning`), `2` usage error.
77
+
78
+ ### publish
79
+
80
+ ```bash
81
+ get-skill login # store a token from your skillmd.com account
82
+ get-skill publish ./my-skill # blocked on lint errors
83
+ get-skill publish ./my-skill --dry-run
84
+ get-skill publish ./my-skill --force # publish despite warnings; never bypasses errors
85
+ ```
86
+
87
+ ## MCP server
88
+
89
+ Claude Code:
90
+
91
+ ```bash
92
+ claude mcp add skillmd -- npx -y get-skill
93
+ ```
94
+
95
+ Other MCP clients:
96
+
97
+ ```jsonc
98
+ {
99
+ "mcpServers": {
100
+ "skillmd": { "command": "npx", "args": ["-y", "get-skill"] }
101
+ }
102
+ }
103
+ ```
104
+
105
+ Tools: `skillmd_search`, `skillmd_get`, `skillmd_install`, `skillmd_trending`, `skillmd_recommend`, `skillmd_list_saved`, `skillmd_lint`. `skillmd_install` writes a skill's `SKILL.md` and never executes scripts.
106
+
107
+ The CLI and the registry validate `SKILL.md` with the same engine, so they apply identical rules.
108
+
109
+ ## Find skills on skillmd.com
110
+
111
+ - [Search all skills](https://skillmd.com/search) · [browse by category](https://skillmd.com/categories) · [by publisher](https://skillmd.com/publishers)
112
+ - Install guides per agent: [Claude Code](https://skillmd.com/agents/claude-code) · [Cursor](https://skillmd.com/agents/cursor) · [Codex](https://skillmd.com/agents/codex) · [all 60+ agents](https://skillmd.com/agents)
113
+ - [The SKILL.md format](https://skillmd.com/docs/format) · [CLI docs](https://skillmd.com/docs/cli) · [MCP docs](https://skillmd.com/docs/mcp) · [registry stats](https://skillmd.com/stats)
114
+
115
+ ## Configuration
116
+
117
+ Resolution order, highest first:
118
+
119
+ | Setting | Sources |
120
+ |---|---|
121
+ | Token | `--token` → `SKILLMD_TOKEN` → `~/.skillmd/config.json` |
122
+ | API base | `--api` → `SKILLMD_API` → default registry |
123
+
124
+ `SKILLMD_TOKEN` enables `skillmd_list_saved`, personalized recommendations, and publishing.
125
+
126
+ ## License
127
+
128
+ MIT
Binary file
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env node
2
+ // SkillMD MCP server (stdio). Wraps the public SkillMD API so any MCP-capable agent
3
+ // can search, fetch, install, and discover Agent Skills by name or task.
4
+ //
5
+ // claude mcp add skillmd -- npx -y skillmds
6
+ //
7
+ // Env:
8
+ // SKILLMD_API override API base (default https://api.skillmd.com)
9
+ // SKILLMD_TOKEN personal access token (enables skillmd_list_saved / personalized recommend)
10
+
11
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
14
+ import { mkdir, writeFile } from "node:fs/promises";
15
+ import { readFileSync } from "node:fs";
16
+ import { createHash } from "node:crypto";
17
+ import { homedir } from "node:os";
18
+ import { join, resolve, dirname, sep } from "node:path";
19
+ import { stringify as stringifyYaml } from "yaml";
20
+
21
+ const VERSION = (() => {
22
+ try {
23
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
24
+ } catch {
25
+ return "0.0.0";
26
+ }
27
+ })();
28
+ // Shared validation engine, bundled into ./vendor at publish time (npm run build).
29
+ // Falls back gracefully if the vendor build is absent (e.g. running from source).
30
+ let lint = null;
31
+ try {
32
+ ({ lint } = await import("../vendor/index.js"));
33
+ } catch {
34
+ /* skillmd_lint will report that validation is unavailable */
35
+ }
36
+
37
+ const API = process.env.SKILLMD_API || "https://api.skillmd.com";
38
+ const TOKEN = process.env.SKILLMD_TOKEN || "";
39
+
40
+ // Companion files not in R2 carry a registry-supplied source_url. Ingest only
41
+ // pins these to GitHub raw hosts, so anything else is a hostile/compromised
42
+ // registry response — refuse to fetch it (client-side SSRF guard).
43
+ const ALLOWED_SOURCE_HOSTS = new Set(["raw.githubusercontent.com", "gist.githubusercontent.com"]);
44
+ function isAllowedSourceUrl(u) {
45
+ try {
46
+ const parsed = new URL(u);
47
+ return parsed.protocol === "https:" && ALLOWED_SOURCE_HOSTS.has(parsed.hostname.toLowerCase());
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ async function api(path, init = {}) {
54
+ const headers = { accept: "application/json", ...(init.headers || {}) };
55
+ if (TOKEN) headers.authorization = `Bearer ${TOKEN}`;
56
+ const res = await fetch(API + path, { ...init, headers });
57
+ if (!res.ok) throw new Error(`SkillMD API ${res.status} on ${path}: ${await res.text().catch(() => "")}`);
58
+ return res.json();
59
+ }
60
+ const text = (obj) => ({ content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }] });
61
+
62
+ // The public API returns site-oriented fields (ratings, counters, avatars,
63
+ // ranker internals) that are noise for an agent picking a skill. Strip them
64
+ // from every list-returning tool so results stay compact and rankable only
65
+ // on what matters to the caller.
66
+ const OMIT_FIELDS = new Set([
67
+ "type",
68
+ "verified",
69
+ "avg_rating",
70
+ "rating_count",
71
+ "install_count",
72
+ "save_count",
73
+ "owner_avatar",
74
+ "confident",
75
+ "category_slug",
76
+ ]);
77
+ const trimItems = (items) => (items ?? []).map((s) => Object.fromEntries(Object.entries(s).filter(([k]) => !OMIT_FIELDS.has(k))));
78
+
79
+ const TOOLS = [
80
+ {
81
+ name: "skillmd_search",
82
+ description: "Search SkillMD for Agent Skills by name, description, or task. Returns matching skills with their owner/name slug and install snippets.",
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: {
86
+ query: { type: "string", description: "Natural-language query or keywords" },
87
+ category: { type: "string", description: "Filter by category slug (e.g. devops, data, security)" },
88
+ verified_only: { type: "boolean", description: "Only verified skills" },
89
+ type: { type: "string", enum: ["single", "pack"], description: "single file or pack" },
90
+ min_rating: { type: "number", description: "Minimum average rating 0-5" },
91
+ limit: { type: "number", description: "Max results (default 20)" },
92
+ },
93
+ required: ["query"],
94
+ },
95
+ },
96
+ {
97
+ name: "skillmd_get",
98
+ description: "Fetch a single skill by slug (owner/name), including its SKILL.md body, provenance, license and security flags.",
99
+ inputSchema: { type: "object", properties: { slug: { type: "string", description: "owner/name, e.g. anthropic/pdf" } }, required: ["slug"] },
100
+ },
101
+ {
102
+ name: "skillmd_install",
103
+ description:
104
+ "Install a skill by slug: writes its SKILL.md into the agent's skills directory. Never executes any scripts. Any skill installs; the response reports its security flags (docs_only / network_calls / executes_scripts / reads_secrets) as information.",
105
+ inputSchema: {
106
+ type: "object",
107
+ properties: {
108
+ slug: { type: "string", description: "owner/name, e.g. anthropic/pdf" },
109
+ dest: { type: "string", description: "Target skills dir. Default ~/.claude/skills" },
110
+ },
111
+ required: ["slug"],
112
+ },
113
+ },
114
+ {
115
+ name: "skillmd_trending",
116
+ description: "Top skills on the SkillMD leaderboard. range: '30d' or 'all'.",
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ range: { type: "string", enum: ["30d", "all"], description: "Time window (default all)" },
121
+ category: { type: "string", description: "Optional category slug" },
122
+ limit: { type: "number", description: "Max results (default 20)" },
123
+ },
124
+ },
125
+ },
126
+ {
127
+ name: "skillmd_recommend",
128
+ description: "Recommend skills. If based_on is given, suggests skills similar to it (same category); otherwise returns trending.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: { based_on: { type: "string", description: "A slug to base recommendations on" }, limit: { type: "number" } },
132
+ },
133
+ },
134
+ {
135
+ name: "skillmd_list_saved",
136
+ description: "List the signed-in user's saved skills. Requires SKILLMD_TOKEN to be set.",
137
+ inputSchema: { type: "object", properties: {} },
138
+ },
139
+ {
140
+ name: "skillmd_lint",
141
+ description:
142
+ "Validate a SKILL.md against the SkillMD spec and return diagnostics, security flags, and a quality score (0-100). Pass raw SKILL.md content, or a registry slug (owner/name) to fetch and lint.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ content: { type: "string", description: "Raw SKILL.md content to validate" },
147
+ slug: { type: "string", description: "Registry slug owner/name to fetch and validate" },
148
+ },
149
+ },
150
+ },
151
+ ];
152
+
153
+ // KEEP IN SYNC with apps/cli/src/api.ts (reconstructSkillMd / skillMdFor) —
154
+ // duplicated because the MCP bin ships as a standalone script; no automated
155
+ // check enforces this.
156
+ function reconstructSkillMd(skill) {
157
+ // Verbatim when the author shipped frontmatter; a frontmatter-less upstream
158
+ // SKILL.md gets the registry's name/description prepended so it parses.
159
+ if (skill.raw_md && /^\uFEFF?\s*---\r?\n/.test(skill.raw_md)) return skill.raw_md;
160
+ const fm = { name: skill.slug.split("/")[1] || skill.title, description: String(skill.description).replace(/\n/g, " ") };
161
+ if (skill.license) fm.license = skill.license;
162
+ return `---\n${stringifyYaml(fm)}---\n${skill.raw_md || skill.body_md || ""}`;
163
+ }
164
+
165
+ async function handleCall(name, args) {
166
+ switch (name) {
167
+ case "skillmd_search": {
168
+ const p = new URLSearchParams({ q: args.query, limit: String(args.limit ?? 20) });
169
+ if (args.category) p.set("category", args.category);
170
+ if (args.verified_only) p.set("verified", "true");
171
+ if (args.type) p.set("type", args.type);
172
+ if (args.min_rating != null) p.set("min_rating", String(args.min_rating));
173
+ const r = await api(`/api/search?${p}`);
174
+ return text(trimItems(r.items));
175
+ }
176
+ case "skillmd_get": {
177
+ const [owner, slugName] = String(args.slug).split("/");
178
+ const r = await api(`/api/skills/${owner}/${slugName}`);
179
+ return text(r);
180
+ }
181
+ case "skillmd_trending": {
182
+ const p = new URLSearchParams({ range: args.range ?? "all", limit: String(args.limit ?? 20) });
183
+ if (args.category) p.set("category", args.category);
184
+ const r = await api(`/api/leaderboard?${p}`);
185
+ return text(trimItems(r.items));
186
+ }
187
+ case "skillmd_recommend": {
188
+ if (args.based_on) {
189
+ const [owner, slugName] = String(args.based_on).split("/");
190
+ const base = await api(`/api/skills/${owner}/${slugName}`).catch(() => null);
191
+ if (base?.category_slug) {
192
+ const r = await api(`/api/leaderboard?range=all&limit=${args.limit ?? 10}&category=${base.category_slug}`);
193
+ return text(trimItems(r.items).filter((s) => s.slug !== args.based_on));
194
+ }
195
+ }
196
+ const r = await api(`/api/leaderboard?range=30d&limit=${args.limit ?? 10}`);
197
+ return text(trimItems(r.items));
198
+ }
199
+ case "skillmd_list_saved": {
200
+ if (!TOKEN) return text("Set SKILLMD_TOKEN (a SkillMD personal access token) to list your saved skills.");
201
+ const r = await api(`/api/favorites`);
202
+ return text(r.items ?? []);
203
+ }
204
+ case "skillmd_lint": {
205
+ if (!lint) return text("Validation engine unavailable — run `npm run build` in the skillmds package.");
206
+ let raw = args.content;
207
+ let slug;
208
+ let verified = false;
209
+ if (!raw && args.slug) {
210
+ const [owner, slugName] = String(args.slug).split("/");
211
+ const skill = await api(`/api/skills/${owner}/${slugName}`);
212
+ raw = reconstructSkillMd(skill);
213
+ slug = slugName;
214
+ verified = !!skill.verified;
215
+ }
216
+ if (!raw) return text("Provide `content` (raw SKILL.md) or `slug` (owner/name).");
217
+ const result = lint(raw, { ...(slug ? { slug } : {}), verified });
218
+ return text({ ok: result.ok, score: result.score, diagnostics: result.diagnostics, security: result.security });
219
+ }
220
+ case "skillmd_install": {
221
+ const [owner, slugName] = String(args.slug).split("/");
222
+ // Defense in depth: slugName becomes a directory name below — refuse
223
+ // anything outside the registry's slugify alphabet (path separators,
224
+ // "..", etc.) before touching the network or filesystem.
225
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(slugName ?? "")) {
226
+ return text(`Refused: invalid skill name "${slugName}" — expected a lowercase slug.`);
227
+ }
228
+ const skill = await api(`/api/skills/${owner}/${slugName}`);
229
+ const flags = skill.security_flags ?? [];
230
+ // Verification never gates installs — any skill installs; flags are
231
+ // reported in the response as information, not a wall.
232
+ const raw = reconstructSkillMd(skill); // returns raw_md verbatim when the registry has it
233
+ if (lint) {
234
+ const result = lint(raw, { slug: slugName });
235
+ if (!result.ok) {
236
+ const errors = result.diagnostics.filter((d) => d.severity === "error");
237
+ return text(`Refused: ${args.slug} failed validation (${errors.map((d) => `${d.id}: ${d.message}`).join("; ")}). Not installed.`);
238
+ }
239
+ }
240
+ const baseDir = args.dest ? resolve(args.dest) : join(homedir(), ".claude", "skills");
241
+ const dir = join(baseDir, slugName);
242
+ await mkdir(dir, { recursive: true });
243
+ const root = resolve(dir);
244
+
245
+ // Zip-slip guard: refuse any path that resolves outside the skill's dir.
246
+ const writeSafe = async (relPath, contents) => {
247
+ const fp = resolve(dir, relPath);
248
+ if (fp !== root && !fp.startsWith(root + sep)) throw new Error(`unsafe path: ${relPath}`);
249
+ await mkdir(dirname(fp), { recursive: true });
250
+ await writeFile(fp, contents);
251
+ };
252
+
253
+ let written = 1;
254
+ let note = "";
255
+ if (skill.type === "pack") {
256
+ // Pull the full pack (SKILL.md + companion files) in one call. The JSON
257
+ // bundle avoids adding an unzip dependency to this standalone binary.
258
+ try {
259
+ const bundle = await api(`/api/skills/${owner}/${slugName}/bundle?format=json`);
260
+ // Resolve + integrity-check every file BEFORE writing any, so a
261
+ // tampered pack refuses cleanly instead of leaving a partial install.
262
+ const resolved = [];
263
+ for (const f of bundle.files ?? []) {
264
+ if (f.path.includes("..") || f.path.startsWith("/")) continue;
265
+ let buf;
266
+ if (f.content_base64 != null) {
267
+ buf = Buffer.from(f.content_base64, "base64");
268
+ // R2 files are content-addressed; verify the bytes match the
269
+ // registry's sha256. Files without one (the reconstructed inline
270
+ // SKILL.md) carry no hash and are skipped.
271
+ if (f.sha256) {
272
+ const got = createHash("sha256").update(buf).digest("hex");
273
+ if (got !== f.sha256) {
274
+ const err = new Error(`integrity check failed for "${f.path}"`);
275
+ err.integrity = true;
276
+ throw err;
277
+ }
278
+ }
279
+ } else if (f.source_url && isAllowedSourceUrl(f.source_url)) {
280
+ const r = await fetch(f.source_url).catch(() => null);
281
+ if (!r || !r.ok) continue;
282
+ buf = Buffer.from(await r.arrayBuffer());
283
+ } else continue;
284
+ resolved.push({ path: f.path, contents: f.path === "SKILL.md" ? raw : buf });
285
+ }
286
+ for (const f of resolved) await writeSafe(f.path, f.contents);
287
+ written = resolved.length || 1;
288
+ if (!resolved.some((f) => f.path === "SKILL.md")) await writeSafe("SKILL.md", raw);
289
+ } catch (e) {
290
+ if (e && e.integrity) return text(`Refused: ${args.slug} failed integrity verification — ${e.message}. Not installed.`);
291
+ await writeSafe("SKILL.md", raw);
292
+ note = ` NOTE: pack assets could not be fetched — only SKILL.md was written. For the full pack run: npx skillmds add ${args.slug}`;
293
+ }
294
+ } else {
295
+ await writeSafe("SKILL.md", raw);
296
+ }
297
+ api(`/api/skills/${owner}/${slugName}/install`, { method: "POST", body: JSON.stringify({ via: "mcp" }) }).catch(() => {});
298
+ return text(`Installed ${args.slug} → ${dir} (${written} file${written === 1 ? "" : "s"}; security: ${flags.join(", ") || "unscanned"})${note}`);
299
+ }
300
+ default:
301
+ throw new Error(`Unknown tool: ${name}`);
302
+ }
303
+ }
304
+
305
+ async function startMcpServer() {
306
+ const server = new Server({ name: "skillmd", version: VERSION }, { capabilities: { tools: {} } });
307
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
308
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
309
+ try {
310
+ return await handleCall(req.params.name, req.params.arguments ?? {});
311
+ } catch (e) {
312
+ return { isError: true, content: [{ type: "text", text: String(e?.message || e) }] };
313
+ }
314
+ });
315
+ const transport = new StdioServerTransport();
316
+ await server.connect(transport);
317
+ console.error("skillmd MCP server running (stdio)");
318
+ }
319
+
320
+ // This binary is invoked three ways:
321
+ // - `skillmds <anything>` → run the CLI. commander owns argv, so known
322
+ // commands run and unknown commands get a proper "unknown command" error
323
+ // instead of silently starting an MCP server.
324
+ // - `skillmds` in a terminal → run the CLI (opens the interactive menu)
325
+ // - `skillmds` over piped stdio → start the MCP server (how MCP clients launch it;
326
+ // they always connect stdin as a pipe, never a TTY, and pass no args)
327
+ if (process.argv.length > 2 || process.stdin.isTTY) {
328
+ await import("../dist/cli.js"); // the CLI parses process.argv itself
329
+ } else {
330
+ await startMcpServer();
331
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // The `skillmd` CLI, shipped inside the `skillmds` package.
3
+ import("../dist/cli.js");