get-skill 1.0.21 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,15 +5,15 @@
5
5
  # get-skill
6
6
 
7
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)
8
+ [![license](https://img.shields.io/npm/l/get-skill.svg)](https://github.com/skillmds/skillmd/blob/main/LICENSE)
9
+ [![source](https://img.shields.io/badge/source-skillmds%2Fskillmd-blue?logo=github)](https://github.com/skillmds/skillmd)
9
10
 
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
+ Tools for working with [Agent Skills](https://docs.claude.com/en/docs/agents-and-tools/agent-skills) (`SKILL.md`) from the [SkillMD](https://skillmd.com) registry. One package, two binaries:
11
12
 
12
- ```bash
13
- npx get-skill add anthropic/pdf
14
- ```
13
+ - **`skillmd`** — a CLI to lint, scan, search, install, and publish skills.
14
+ - **`get-skill`** — the same CLI, and an MCP server exposing the registry to MCP clients (Claude Code, Claude Desktop, Cursor, …).
15
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.
16
+ Both validate `SKILL.md` with the same engine ([`@skillmds/core`](https://www.npmjs.com/package/@skillmds/core)), so the CLI and the registry apply identical rules. Source lives at [github.com/skillmds/skillmd](https://github.com/skillmds/skillmd).
17
17
 
18
18
  ## Install
19
19
 
@@ -21,70 +21,138 @@ npx get-skill add anthropic/pdf
21
21
  npm i -g get-skill
22
22
  ```
23
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:
24
+ This installs both the `get-skill` and `skillmd` commands. To run the CLI without installing:
31
25
 
32
26
  ```bash
33
- npx get-skill search pdf
34
27
  npx get-skill lint .
35
28
  ```
36
29
 
37
- ## CLI
30
+ **Which binary runs what.** `get-skill` with any argument runs the CLI. `get-skill` with no arguments starts the **MCP server** when stdin is piped (how an MCP client launches it), and opens the interactive CLI menu when there's a terminal attached. `skillmd` is always the CLI.
31
+
32
+ ## CLI: `skillmd`
38
33
 
39
34
  | Command | Description |
40
35
  |---|---|
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`.
36
+ | `skillmd lint [path]` | Validate `SKILL.md` files; print diagnostics and a quality score. |
37
+ | `skillmd scan [path]` | Report scripts, network calls, and secret access. |
38
+ | `skillmd rules [id]` | List lint rules, or show one. |
39
+ | `skillmd init [name]` | Create a `SKILL.md` from a template. |
40
+ | `skillmd search <query>` | Search the registry. |
41
+ | `skillmd info <slug>` | Show a skill's registry record: verification, security flags, provenance, rating. |
42
+ | `skillmd add <source>` | Install a skill (registry slug, GitHub source, gist, or local path). Lints before writing. |
43
+ | `skillmd list` (`ls`) | List installed skills with their scope, agents, link mode and source. |
44
+ | `skillmd remove [names...]` (`rm`) | Remove installed skills, or unlink them from specific agents. |
45
+ | `skillmd update [names...]` | Update installed skills from the source recorded in the lock file. |
46
+ | `skillmd check [names...]` | Report which installed skills have updates (`update --check`). |
47
+ | `skillmd publish [path]` (`submit`) | Publish to the registry. Exits non-zero on lint errors. |
48
+ | `skillmd login` / `logout` | Store or remove a token. |
49
+
50
+ > `check` is a command of its own since 1.2.0. It used to be an alias of `lint` — scripts that ran `skillmd check .` to validate a skill should now run `skillmd lint .`.
51
+
52
+ ### lint
53
+
54
+ ```bash
55
+ skillmd lint ./my-skill
56
+ skillmd lint . --format sarif > skillmd.sarif
57
+ skillmd lint . --strict
58
+ skillmd lint . --fix
59
+ ```
60
+
61
+ Formats: `text` (default), `json`, `sarif` (GitHub Code Scanning), `github` (Actions annotations).
62
+ Exit codes: `0` clean, `1` errors (or warnings with `--strict` / `--fail-on-warning`).
54
63
 
55
64
  ### add
56
65
 
57
66
  ```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
67
+ skillmd add anthropic/pdf # registry slug
68
+ skillmd add owner/repo # GitHub repo
69
+ skillmd add owner/repo/skills/pdf#main # a subfolder at a ref
70
+ skillmd add owner/repo@pdf # one skill inside a repo or pack
71
+ skillmd add https://github.com/owner/repo/tree/v2/skills/pdf
72
+ skillmd add https://gist.github.com/user/0123456789abcdef
73
+ skillmd add ./path # local directory
74
+ skillmd add owner/repo --list # what's in there? install nothing
75
+ skillmd add anthropic/pdf -a claude-code cursor -g
76
+ skillmd add anthropic/pdf --mode copy # no links; real files per agent
77
+ skillmd add some/skill --deny executes_scripts
78
+ skillmd add some/skill --skip-lint
64
79
  ```
65
80
 
66
- ### lint
81
+ Sources: a registry slug `owner/name`; a GitHub repo, subdirectory, ref (`#ref`) or named skill (`@skill`), including `tree/` and `blob/` URLs; `github:`/`gh:` to force the GitHub reading of a bare `owner/name`; a gist URL; or a local path. `--ref <ref>` overrides the ref on any GitHub source. GitLab and `git@` sources aren't supported — clone them and install from the path.
82
+
83
+ **Where skills land.** One canonical copy, linked into each agent you chose:
84
+
85
+ | Scope | Flag | Canonical copy | Agent links | Lock file |
86
+ | --- | --- | --- | --- | --- |
87
+ | **Project** | `-p`, `--project` | `.agents/skills/<name>` | `./<agent-dir>/skills/<name>` | `skills-lock.json` (commit it) |
88
+ | **Global** | `-g`, `--global` | `~/.agents/skills/<name>` | `~/<agent-dir>/skills/<name>` | `~/.skillmd/lock.json` |
89
+
90
+ Each agent link is a junction on Windows and a symlink elsewhere, so updating the canonical copy updates every agent at once; `--mode copy` writes real files instead. A project install only touches agents the project already uses (or the ones you name with `-a`), so `add -p` never invents a folder for an agent you don't run.
91
+
92
+ In a terminal, `add` asks for the scope (**Project** / **Global**) and then which detected agents to link into, remembering your agent choice for next time. Off a terminal the scope is auto-detected — see [Non-interactive use](#non-interactive-use).
93
+
94
+ Re-installing the same skill from the same source is fine. Replacing a skill that came from a *different* source, or a directory the lock file doesn't know about, needs `--force`.
95
+
96
+ ### list / remove / update / check
67
97
 
68
98
  ```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
99
+ skillmd list # everything installed, project and global
100
+ skillmd list -p -a claude-code # just this project, just one agent
101
+ skillmd remove pdf # unlink from every agent, delete the copy
102
+ skillmd remove pdf -a cursor # unlink from Cursor only; copy stays
103
+ skillmd remove --all -g -y # clear your user-level skills
104
+ skillmd check # which skills have updates?
105
+ skillmd update # refresh them from their recorded sources
106
+ skillmd update pdf -g
73
107
  ```
74
108
 
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.
109
+ `list` shows name · scope · agents · mode · source. Skills installed by hand or by an older CLI have no lock entry and are tagged `untracked`: `update` skips them and tells you to re-run `skillmd add` once to start tracking them.
110
+
111
+ `update` re-fetches the full bundle from the source recorded in the lock file, re-lints it, and skips skills whose recorded commit hasn't moved. `check` is `update --check`: it reports and writes nothing.
112
+
113
+ `remove` with no names opens a picker, and confirms before deleting unless you pass `-y`.
114
+
115
+ ### Non-interactive use
116
+
117
+ Every command works without a terminal, and none of them will ever block on a prompt when there isn't one.
118
+
119
+ ```bash
120
+ skillmd add anthropic/pdf -y --json
121
+ skillmd list --json
122
+ skillmd check --json
123
+ ```
124
+
125
+ - **`--json` requires `-y`** on `add`, and prints exactly one document on stdout; human text and warnings go to stderr, so `skillmd add … --json | jq` is always valid JSON. The `add` document is `{ ok, scope, installed: [{ name, canonical, targets, skipped, digest }], blocked, warnings }`.
126
+ - **Scope auto-detection.** With `-y`, `--json`, piped stdin, or inside a host agent (Claude Code, Cursor, Codex, Gemini CLI, or any CI), `add` picks **project** when the working directory carries `.git`, `package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `AGENTS.md`, `CLAUDE.md`, `skills-lock.json` or an agent dot-dir, and **global** otherwise. It says which it picked. Pass `-p` or `-g` to decide yourself.
127
+ - **Piped with neither `-y` nor a scope flag exits 1** with a hint rather than guessing where to write.
128
+ - **Exit codes.** `0` success; `1` anything that failed — lint errors, a blocked or denied skill, nothing removed, an update that couldn't be applied, or a prompt that was required but impossible. `login`, `logout` and `init` also exit 1 off a TTY, naming the flags to pass instead.
129
+ - **Tokens.** Prefer `skillmd login` or `SKILLMD_TOKEN` over `--token`: a token on the command line is visible in your shell history and in every process listing on the machine, and the CLI warns about it. A stored token is bound to the API base it was saved for and is never sent anywhere else. An `http://` API base is refused unless you add `--insecure-http`.
130
+
131
+ ### What the CLI sends
132
+
133
+ Installing a skill **from the registry** sends one fire-and-forget request so the skill's install count on skillmd.com stays accurate:
134
+
135
+ ```
136
+ POST https://api.skillmd.com/api/skills/<owner>/<name>/install
137
+ {"via":"cli"}
138
+ ```
139
+
140
+ That is the whole payload — no identifiers, no paths, no machine information — and the install succeeds whether or not it goes through. Installs from GitHub, gists and local paths send nothing. To turn it off:
141
+
142
+ ```bash
143
+ export SKILLMD_NO_TELEMETRY=1 # or the cross-tool DO_NOT_TRACK=1
144
+ ```
77
145
 
78
146
  ### publish
79
147
 
80
148
  ```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
149
+ skillmd login # store a token from your skillmd.com account
150
+ skillmd publish ./my-skill # blocked on lint errors
151
+ skillmd publish ./my-skill --dry-run
152
+ skillmd publish ./my-skill --force # publish despite warnings; never bypasses errors
85
153
  ```
86
154
 
87
- ## MCP server
155
+ ## MCP server: `get-skill`
88
156
 
89
157
  Claude Code:
90
158
 
@@ -102,12 +170,24 @@ Other MCP clients:
102
170
  }
103
171
  ```
104
172
 
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.
173
+ Tools: `skillmd_search`, `skillmd_get`, `skillmd_install`, `skillmd_trending`, `skillmd_recommend`, `skillmd_list_saved`, `skillmd_lint`.
174
+
175
+ `skillmd_install` runs the same installer as the CLI — it validates the skill, writes one canonical copy and links it into the chosen agents — and takes `scope` (`project` | `global`), `agents`, `mode` (`link` | `copy`), `force` and `deny`. It never executes scripts. The old `dest` parameter is deprecated and is accepted only when it points at a known agent skills directory; use `scope` + `agents` instead.
176
+
177
+ ### Remote server (no install)
106
178
 
107
- The CLI and the registry validate `SKILL.md` with the same engine, so they apply identical rules.
179
+ SkillMD also hosts a remote MCP server over streamable HTTP — point any client that supports remote servers at:
180
+
181
+ ```
182
+ https://api.skillmd.com/mcp
183
+ ```
184
+
185
+ No auth required. It exposes `skillmd_search`, `skillmd_get`, `skillmd_download`, `skillmd_trending`, and `skillmd_recommend` (`skillmd_download` returns file contents for the client to write, instead of writing to disk).
108
186
 
109
187
  ## Find skills on skillmd.com
110
188
 
189
+ The registry behind these tools is [skillmd.com](https://skillmd.com): 1,000,000+ Agent Skills, each linted, security-scanned and content-pinned.
190
+
111
191
  - [Search all skills](https://skillmd.com/search) · [browse by category](https://skillmd.com/categories) · [by publisher](https://skillmd.com/publishers)
112
192
  - 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
193
  - [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)
@@ -121,8 +201,8 @@ Resolution order, highest first:
121
201
  | Token | `--token` → `SKILLMD_TOKEN` → `~/.skillmd/config.json` |
122
202
  | API base | `--api` → `SKILLMD_API` → default registry |
123
203
 
124
- `SKILLMD_TOKEN` enables `skillmd_list_saved`, personalized recommendations, and publishing.
204
+ `SKILLMD_TOKEN` enables `skillmd_list_saved`, personalized recommendations, and publishing. A stored token records the API base it belongs to and is only ever sent there. `SKILLMD_NO_TELEMETRY=1` (or `DO_NOT_TRACK=1`) disables the install-count ping described above.
125
205
 
126
206
  ## License
127
207
 
128
- MIT
208
+ [MIT](https://github.com/skillmds/skillmd/blob/main/LICENSE)
Binary file
@@ -1,331 +1,3 @@
1
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
- }
2
+ // SkillMD MCP server (stdio) + CLI dispatch. Source: src/mcp/index.ts.
3
+ import("../dist/mcp.js");
package/bin/skillmd.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- // The `skillmd` CLI, shipped inside the `skillmds` package.
2
+ // The `skillmd` CLI, shipped inside the `get-skill` package.
3
3
  import("../dist/cli.js");