skillmds 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/skillmd-mcp.mjs +168 -0
  2. package/package.json +16 -0
@@ -0,0 +1,168 @@
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://skillmd-api.vinoor.workers.dev)
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 { homedir } from "node:os";
16
+ import { join, resolve } from "node:path";
17
+
18
+ const API = process.env.SKILLMD_API || "https://skillmd-api.vinoor.workers.dev";
19
+ const TOKEN = process.env.SKILLMD_TOKEN || "";
20
+
21
+ async function api(path, init = {}) {
22
+ const headers = { accept: "application/json", ...(init.headers || {}) };
23
+ if (TOKEN) headers.authorization = `Bearer ${TOKEN}`;
24
+ const res = await fetch(API + path, { ...init, headers });
25
+ if (!res.ok) throw new Error(`SkillMD API ${res.status} on ${path}: ${await res.text().catch(() => "")}`);
26
+ return res.json();
27
+ }
28
+ const text = (obj) => ({ content: [{ type: "text", text: typeof obj === "string" ? obj : JSON.stringify(obj, null, 2) }] });
29
+
30
+ const TOOLS = [
31
+ {
32
+ name: "skillmd_search",
33
+ description: "Search SkillMD for Agent Skills by name, description, or task. Returns matching skills with ratings and install snippets.",
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ query: { type: "string", description: "Natural-language query or keywords" },
38
+ category: { type: "string", description: "Filter by category slug (e.g. devops, data, security)" },
39
+ verified_only: { type: "boolean", description: "Only verified skills" },
40
+ type: { type: "string", enum: ["single", "pack"], description: "single file or pack" },
41
+ min_rating: { type: "number", description: "Minimum average rating 0-5" },
42
+ limit: { type: "number", description: "Max results (default 20)" },
43
+ },
44
+ required: ["query"],
45
+ },
46
+ },
47
+ {
48
+ name: "skillmd_get",
49
+ description: "Fetch a single skill by slug (owner/name), including its SKILL.md body, provenance, license and security flags.",
50
+ inputSchema: { type: "object", properties: { slug: { type: "string", description: "owner/name, e.g. anthropic/pdf" } }, required: ["slug"] },
51
+ },
52
+ {
53
+ name: "skillmd_install",
54
+ description: "Install a skill by slug: writes its SKILL.md into the agent's skills directory. Never executes any scripts.",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ slug: { type: "string", description: "owner/name, e.g. anthropic/pdf" },
59
+ dest: { type: "string", description: "Target skills dir. Default ~/.claude/skills" },
60
+ },
61
+ required: ["slug"],
62
+ },
63
+ },
64
+ {
65
+ name: "skillmd_trending",
66
+ description: "Top skills on the SkillMD leaderboard. range: '30d' or 'all'.",
67
+ inputSchema: {
68
+ type: "object",
69
+ properties: {
70
+ range: { type: "string", enum: ["30d", "all"], description: "Time window (default all)" },
71
+ category: { type: "string", description: "Optional category slug" },
72
+ limit: { type: "number", description: "Max results (default 20)" },
73
+ },
74
+ },
75
+ },
76
+ {
77
+ name: "skillmd_recommend",
78
+ description: "Recommend skills. If based_on is given, suggests skills similar to it (same category); otherwise returns trending.",
79
+ inputSchema: {
80
+ type: "object",
81
+ properties: { based_on: { type: "string", description: "A slug to base recommendations on" }, limit: { type: "number" } },
82
+ },
83
+ },
84
+ {
85
+ name: "skillmd_list_saved",
86
+ description: "List the signed-in user's saved skills. Requires SKILLMD_TOKEN to be set.",
87
+ inputSchema: { type: "object", properties: {} },
88
+ },
89
+ ];
90
+
91
+ function reconstructSkillMd(skill) {
92
+ const fm = ["---", `name: ${skill.slug.split("/")[1] || skill.title}`, `description: ${String(skill.description).replace(/\n/g, " ")}`];
93
+ if (skill.license) fm.push(`license: ${skill.license}`);
94
+ fm.push("---", "");
95
+ return fm.join("\n") + (skill.body_md || "");
96
+ }
97
+
98
+ async function handleCall(name, args) {
99
+ switch (name) {
100
+ case "skillmd_search": {
101
+ const p = new URLSearchParams({ q: args.query, limit: String(args.limit ?? 20) });
102
+ if (args.category) p.set("category", args.category);
103
+ if (args.verified_only) p.set("verified", "true");
104
+ if (args.type) p.set("type", args.type);
105
+ if (args.min_rating != null) p.set("min_rating", String(args.min_rating));
106
+ const r = await api(`/api/search?${p}`);
107
+ return text(r.items ?? []);
108
+ }
109
+ case "skillmd_get": {
110
+ const [owner, slugName] = String(args.slug).split("/");
111
+ const r = await api(`/api/skills/${owner}/${slugName}`);
112
+ return text(r);
113
+ }
114
+ case "skillmd_trending": {
115
+ const p = new URLSearchParams({ range: args.range ?? "all", limit: String(args.limit ?? 20) });
116
+ if (args.category) p.set("category", args.category);
117
+ const r = await api(`/api/leaderboard?${p}`);
118
+ return text(r.items ?? []);
119
+ }
120
+ case "skillmd_recommend": {
121
+ if (args.based_on) {
122
+ const [owner, slugName] = String(args.based_on).split("/");
123
+ const base = await api(`/api/skills/${owner}/${slugName}`).catch(() => null);
124
+ if (base?.category_slug) {
125
+ const r = await api(`/api/leaderboard?range=all&limit=${args.limit ?? 10}&category=${base.category_slug}`);
126
+ return text((r.items ?? []).filter((s) => s.slug !== args.based_on));
127
+ }
128
+ }
129
+ const r = await api(`/api/leaderboard?range=30d&limit=${args.limit ?? 10}`);
130
+ return text(r.items ?? []);
131
+ }
132
+ case "skillmd_list_saved": {
133
+ if (!TOKEN) return text("Set SKILLMD_TOKEN (a SkillMD personal access token) to list your saved skills.");
134
+ const r = await api(`/api/favorites`);
135
+ return text(r.items ?? []);
136
+ }
137
+ case "skillmd_install": {
138
+ const [owner, slugName] = String(args.slug).split("/");
139
+ const skill = await api(`/api/skills/${owner}/${slugName}`);
140
+ const baseDir = args.dest ? resolve(args.dest) : join(homedir(), ".claude", "skills");
141
+ const dir = join(baseDir, slugName);
142
+ await mkdir(dir, { recursive: true });
143
+ const file = join(dir, "SKILL.md");
144
+ await writeFile(file, reconstructSkillMd(skill), "utf8");
145
+ api(`/api/skills/${owner}/${slugName}/install`, { method: "POST", body: JSON.stringify({ via: "mcp" }) }).catch(() => {});
146
+ const note = skill.type === "pack"
147
+ ? " (pack assets are not bundled yet — only SKILL.md was written)"
148
+ : "";
149
+ return text(`Installed ${args.slug} → ${file}${note}`);
150
+ }
151
+ default:
152
+ throw new Error(`Unknown tool: ${name}`);
153
+ }
154
+ }
155
+
156
+ const server = new Server({ name: "skillmd", version: "0.1.0" }, { capabilities: { tools: {} } });
157
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
158
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
159
+ try {
160
+ return await handleCall(req.params.name, req.params.arguments ?? {});
161
+ } catch (e) {
162
+ return { isError: true, content: [{ type: "text", text: String(e?.message || e) }] };
163
+ }
164
+ });
165
+
166
+ const transport = new StdioServerTransport();
167
+ await server.connect(transport);
168
+ console.error("skillmd MCP server running (stdio)");
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "skillmds",
3
+ "version": "0.1.0",
4
+ "description": "SkillMD MCP server — search, fetch and install Agent Skills from SkillMD.com",
5
+ "type": "module",
6
+ "bin": {
7
+ "skillmds": "bin/skillmd-mcp.mjs"
8
+ },
9
+ "keywords": ["mcp", "agent-skills", "skillmd", "claude", "ai"],
10
+ "homepage": "https://skillmd.com",
11
+ "files": ["bin"],
12
+ "engines": { "node": ">=18" },
13
+ "dependencies": {
14
+ "@modelcontextprotocol/sdk": "^1.13.0"
15
+ }
16
+ }