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/CHANGELOG.md +242 -117
- package/LICENSE +21 -0
- package/README.md +130 -50
- package/assets/skillmd-banner.jpg +0 -0
- package/bin/skillmd-mcp.mjs +2 -330
- package/bin/skillmd.mjs +1 -1
- package/dist/chunk-MPBRLZWJ.js +919 -0
- package/dist/cli.js +1497 -1073
- package/dist/mcp.js +581 -0
- package/package.json +43 -24
- package/vendor/index.js +0 -333
package/README.md
CHANGED
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
# get-skill
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/get-skill)
|
|
8
|
-
[](
|
|
8
|
+
[](https://github.com/skillmds/skillmd/blob/main/LICENSE)
|
|
9
|
+
[](https://github.com/skillmds/skillmd)
|
|
9
10
|
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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`.
|
|
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
|
-
|
|
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
|
package/bin/skillmd-mcp.mjs
CHANGED
|
@@ -1,331 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// SkillMD MCP server (stdio)
|
|
3
|
-
|
|
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