skills-atlas-cli 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/bin/skills.js +5 -1
- package/package.json +1 -1
- package/src/commands/mcp.js +5 -0
- package/src/mcp.js +182 -0
package/README.md
CHANGED
|
@@ -153,6 +153,20 @@ just describe what you need, or use `/skills-atlas:skill-search`, `:skill-info`,
|
|
|
153
153
|
/plugin install skills-atlas@skills-atlas
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
## In any MCP client
|
|
157
|
+
|
|
158
|
+
`skills-atlas mcp` runs a zero-dependency [MCP](https://modelcontextprotocol.io)
|
|
159
|
+
server over stdio, so any MCP-capable client (Claude Desktop, other agents) can use
|
|
160
|
+
the catalog. Add it to your client's config:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{ "mcpServers": { "skills-atlas": { "command": "npx", "args": ["-y", "skills-atlas-cli", "mcp"] } } }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
It exposes four tools: **search_skills**, **skill_info**, **install_skill**, and
|
|
167
|
+
**list_categories** — discover, inspect, install, and browse the catalog from
|
|
168
|
+
anywhere.
|
|
169
|
+
|
|
156
170
|
## Autopilot (opt-in) — the right skill finds you
|
|
157
171
|
|
|
158
172
|
```bash
|
package/bin/skills.js
CHANGED
|
@@ -16,12 +16,13 @@ const suggest = require('../src/commands/suggest');
|
|
|
16
16
|
const hook = require('../src/commands/hook');
|
|
17
17
|
const gaps = require('../src/commands/gaps');
|
|
18
18
|
const update = require('../src/commands/update');
|
|
19
|
+
const mcp = require('../src/commands/mcp');
|
|
19
20
|
const { categories, list } = require('../src/commands/categories');
|
|
20
21
|
|
|
21
22
|
const VERSION = require('../package.json').version;
|
|
22
23
|
// `use` = install + activate inline (emit the SKILL.md so an agent follows it now).
|
|
23
24
|
const use = argv => install([...argv, '--inline']);
|
|
24
|
-
const commands = { search, info, install, use, kit, sync, installed, upgrade, remove, outdated, doctor, suggest, hook, gaps, update, categories, list, registry };
|
|
25
|
+
const commands = { search, info, install, use, kit, sync, installed, upgrade, remove, outdated, doctor, suggest, hook, gaps, update, categories, list, registry, mcp };
|
|
25
26
|
|
|
26
27
|
const HELP = `skills-atlas — search, install & manage AI agent skills
|
|
27
28
|
|
|
@@ -52,6 +53,9 @@ catalog:
|
|
|
52
53
|
list [category] list skill groups (optionally within one category)
|
|
53
54
|
registry add/list/remove a private catalog source (org-internal skills)
|
|
54
55
|
|
|
56
|
+
integrations:
|
|
57
|
+
mcp run as an MCP server (search/info/install/categories for any MCP client)
|
|
58
|
+
|
|
55
59
|
global flags: --zh (中文 output; English by default), --json (machine output), -h/--help
|
|
56
60
|
docs: https://zita-go.github.io/Skills-Atlas/`;
|
|
57
61
|
|
package/package.json
CHANGED
package/src/mcp.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Hand-rolled, zero-dep MCP (Model Context Protocol) server exposing the catalog
|
|
2
|
+
// over stdio (newline-delimited JSON-RPC 2.0). `handle()` is a pure dispatcher
|
|
3
|
+
// (catalog injected) for tests; `start()` wires stdin/stdout. stdout carries ONLY
|
|
4
|
+
// protocol JSON — never console.log here.
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { loadData } = require('./data');
|
|
9
|
+
const { buildIndices, vendorsFor, skillDocPath, suggestSkills } = require('./index-build');
|
|
10
|
+
const { runSearch } = require('./search-core');
|
|
11
|
+
const { buildInfo } = require('./format');
|
|
12
|
+
const fsu = require('./fsutil');
|
|
13
|
+
const { installFolder } = require('./installer');
|
|
14
|
+
|
|
15
|
+
const VERSION = require('../package.json').version;
|
|
16
|
+
const PROTOCOL = '2025-06-18';
|
|
17
|
+
|
|
18
|
+
// ---- tool result formatters (plain text, no ANSI) ----
|
|
19
|
+
function fmtSearch(data, { query, limit }) {
|
|
20
|
+
const { flatRows } = buildIndices(data);
|
|
21
|
+
const { rows } = runSearch(flatRows, { query });
|
|
22
|
+
if (!rows.length) return `No skills match "${query}".`;
|
|
23
|
+
const n = Math.min(rows.length, (Number.isInteger(limit) && limit > 0) ? limit : 10);
|
|
24
|
+
const out = rows.slice(0, n).map(r => {
|
|
25
|
+
const src = [...(r.sources || [])].sort((a, b) => (b.stars || 0) - (a.stars || 0))[0] || {};
|
|
26
|
+
const uc = r.use_case_en || r.use_case || '';
|
|
27
|
+
const cmd = src.install && src.install.command ? src.install.command : '';
|
|
28
|
+
return `• ${(r.skills || []).join(', ')} [${r.group_en || r.group}]` +
|
|
29
|
+
(uc ? `\n ${uc}` : '') +
|
|
30
|
+
(src.name ? `\n via ${src.name} ★${src.stars || 0}${cmd ? ` — ${cmd}` : ''}` : '');
|
|
31
|
+
});
|
|
32
|
+
return `${rows.length} result(s) for "${query}" (showing ${n}):\n\n${out.join('\n\n')}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fmtInfo(data, { skill }) {
|
|
36
|
+
const idx = buildIndices(data);
|
|
37
|
+
const info = buildInfo(skill, { skillIndex: idx.skillIndex, vendors: data.vendors });
|
|
38
|
+
if (!info.found) {
|
|
39
|
+
const sugg = suggestSkills(idx.skillIndex, skill);
|
|
40
|
+
return `Skill "${skill}" not found.${sugg.length ? ` Did you mean: ${sugg.join(', ')}.` : ''}`;
|
|
41
|
+
}
|
|
42
|
+
const g = info.groups[0];
|
|
43
|
+
const lines = [`${skill} [${g.category_en || g.category}] — group: ${g.group_en || g.group}`];
|
|
44
|
+
const desc = g.description_en || g.description; if (desc) lines.push(desc);
|
|
45
|
+
const uc = g.use_case_en || g.use_case; if (uc) lines.push(`use case: ${uc}`);
|
|
46
|
+
const wt = g.when_to_use_en || g.when_to_use; if (wt) lines.push(`when: ${wt}`);
|
|
47
|
+
if (g.personas && g.personas.length) lines.push(`personas: ${g.personas.join(', ')}`);
|
|
48
|
+
lines.push('sources:');
|
|
49
|
+
for (const s of g.sources) {
|
|
50
|
+
lines.push(` - ${s.id} ★${s.stars || 0}${s.license ? ` (${s.license})` : ''} ${s.path || '(whole-repo install)'}`);
|
|
51
|
+
if (s.install && s.install.command) lines.push(` install: ${s.install.command}`);
|
|
52
|
+
}
|
|
53
|
+
if (info.groups.length > 1) lines.push(`(+${info.groups.length - 1} other group(s) with a same-named skill)`);
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fmtCategories(data, { category }) {
|
|
58
|
+
if (!category) {
|
|
59
|
+
const out = data.sections.map(s => {
|
|
60
|
+
const groups = s.subsections.reduce((m, ss) => m + ss.rows.length, 0);
|
|
61
|
+
return `• ${s.title_en || s.title} (${groups} groups)`;
|
|
62
|
+
});
|
|
63
|
+
return `Categories:\n${out.join('\n')}`;
|
|
64
|
+
}
|
|
65
|
+
const lc = String(category).toLowerCase();
|
|
66
|
+
const sec = data.sections.find(s =>
|
|
67
|
+
(s.title_en || '').toLowerCase().includes(lc) || (s.title || '').toLowerCase().includes(lc));
|
|
68
|
+
if (!sec) return `Category "${category}" not found. Run list_categories with no argument to see them.`;
|
|
69
|
+
const groups = sec.subsections.flatMap(ss => ss.rows.map(r => ` • ${r.group_en || r.group} — ${(r.skills || []).join(', ')}`));
|
|
70
|
+
return `${sec.title_en || sec.title}:\n${groups.join('\n')}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function doInstall(data, { skill, scope, source, dry_run }) {
|
|
74
|
+
const idx = buildIndices(data);
|
|
75
|
+
const candidates = vendorsFor(idx.skillIndex, skill);
|
|
76
|
+
if (!candidates.length) {
|
|
77
|
+
const sugg = suggestSkills(idx.skillIndex, skill);
|
|
78
|
+
return { text: `Skill "${skill}" not found.${sugg.length ? ` Did you mean: ${sugg.join(', ')}.` : ''}`, isError: true };
|
|
79
|
+
}
|
|
80
|
+
const chosen = source
|
|
81
|
+
? candidates.find(c => c.source.name.toLowerCase() === String(source).toLowerCase())
|
|
82
|
+
: candidates[0];
|
|
83
|
+
if (!chosen) return { text: `Source "${source}" does not provide "${skill}". Available: ${candidates.map(c => c.source.name).join(', ')}.`, isError: true };
|
|
84
|
+
const v = chosen.vendor, src = chosen.source;
|
|
85
|
+
const name = chosen.skill || skill;
|
|
86
|
+
const docPath = skillDocPath(v, name);
|
|
87
|
+
if (!docPath) {
|
|
88
|
+
const cmd = (src.install || v.install || {}).command;
|
|
89
|
+
return { text: `"${skill}" from ${src.name} installs the whole repo, not a single folder.${cmd ? ` Run: ${cmd}` : ''}` };
|
|
90
|
+
}
|
|
91
|
+
const global = scope !== 'project';
|
|
92
|
+
const targetRoot = fsu.installTargetDir({ global });
|
|
93
|
+
const dest = path.join(targetRoot, name);
|
|
94
|
+
if (dry_run) return { text: `[dry run] would install "${skill}" from ${src.name} → ${fsu.tildify(dest)} (no files written).` };
|
|
95
|
+
try {
|
|
96
|
+
const r = await installFolder({
|
|
97
|
+
author: v.author || src.author, repo: v.repo || src.repo,
|
|
98
|
+
branch: v.default_branch || src.default_branch || 'main',
|
|
99
|
+
docPath, dest, targetRoot, skillName: name,
|
|
100
|
+
source: src.name, group: chosen.row && chosen.row.group, category: chosen.row && chosen.row._cat,
|
|
101
|
+
});
|
|
102
|
+
return { text: `Installed "${skill}" — ${r.fileCount} file(s) from ${src.name}@${r.branchUsed} → ${fsu.tildify(r.dest)}.${r.scripts.length ? ` Includes ${r.scripts.length} script file(s); review before use.` : ''}` };
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return { text: `Install failed: ${e.message}`, isError: true };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function toolDefs() {
|
|
109
|
+
return [
|
|
110
|
+
{ name: 'search_skills', description: 'Search the Skills Atlas catalog of AI agent skills by keyword or short task description.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'keywords or a short task description' }, limit: { type: 'integer', minimum: 1, description: 'max results (default 10)' } }, required: ['query'] } },
|
|
111
|
+
{ name: 'skill_info', description: 'Show what a catalog skill does, when to use it, its sources, and how to install it.', inputSchema: { type: 'object', properties: { skill: { type: 'string' } }, required: ['skill'] } },
|
|
112
|
+
{ name: 'install_skill', description: 'Install a skill from the catalog into .claude/skills/.', inputSchema: { type: 'object', properties: { skill: { type: 'string' }, scope: { type: 'string', enum: ['global', 'project'], description: 'default global (~/.claude/skills)' }, source: { type: 'string', description: 'pick a source when several provide the skill' }, dry_run: { type: 'boolean' } }, required: ['skill'] } },
|
|
113
|
+
{ name: 'list_categories', description: "List the catalog's top-level categories, or the skill groups within one.", inputSchema: { type: 'object', properties: { category: { type: 'string', description: 'optional — drill into one category' } } } },
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function callTool(name, args, data) {
|
|
118
|
+
args = args || {};
|
|
119
|
+
switch (name) {
|
|
120
|
+
case 'search_skills':
|
|
121
|
+
if (!args.query) return { text: 'missing required "query".', isError: true };
|
|
122
|
+
return { text: fmtSearch(data, args) };
|
|
123
|
+
case 'skill_info':
|
|
124
|
+
if (!args.skill) return { text: 'missing required "skill".', isError: true };
|
|
125
|
+
return { text: fmtInfo(data, args) };
|
|
126
|
+
case 'list_categories':
|
|
127
|
+
return { text: fmtCategories(data, args) };
|
|
128
|
+
case 'install_skill':
|
|
129
|
+
if (!args.skill) return { text: 'missing required "skill".', isError: true };
|
|
130
|
+
return await doInstall(data, args);
|
|
131
|
+
default:
|
|
132
|
+
return { text: `unknown tool: ${name}`, isError: true };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function handle(req, { data }) {
|
|
137
|
+
const id = req && req.id;
|
|
138
|
+
const ok = result => ({ jsonrpc: '2.0', id, result });
|
|
139
|
+
const err = (code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
|
|
140
|
+
if (!req || req.jsonrpc !== '2.0') return id === undefined ? null : err(-32600, 'Invalid Request');
|
|
141
|
+
switch (req.method) {
|
|
142
|
+
case 'initialize':
|
|
143
|
+
return ok({ protocolVersion: (req.params && req.params.protocolVersion) || PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: 'skills-atlas', version: VERSION } });
|
|
144
|
+
case 'notifications/initialized':
|
|
145
|
+
case 'initialized':
|
|
146
|
+
return null;
|
|
147
|
+
case 'tools/list':
|
|
148
|
+
return ok({ tools: toolDefs() });
|
|
149
|
+
case 'tools/call': {
|
|
150
|
+
const p = req.params || {};
|
|
151
|
+
const r = await callTool(p.name, p.arguments, data);
|
|
152
|
+
return ok({ content: [{ type: 'text', text: r.text }], isError: Boolean(r.isError) });
|
|
153
|
+
}
|
|
154
|
+
default:
|
|
155
|
+
return id === undefined ? null : err(-32601, 'Method not found');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function start() {
|
|
160
|
+
let data;
|
|
161
|
+
try { data = loadData({ quiet: true }).data; }
|
|
162
|
+
catch (e) { process.stderr.write(`skills-atlas mcp: ${e.message}\n`); process.exit(1); return; }
|
|
163
|
+
let buf = '';
|
|
164
|
+
let queue = Promise.resolve();
|
|
165
|
+
process.stdin.setEncoding('utf8');
|
|
166
|
+
process.stdin.on('data', chunk => {
|
|
167
|
+
buf += chunk;
|
|
168
|
+
let nl;
|
|
169
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
170
|
+
const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1);
|
|
171
|
+
if (!line) continue;
|
|
172
|
+
let req; try { req = JSON.parse(line); } catch { continue; }
|
|
173
|
+
queue = queue.then(async () => {
|
|
174
|
+
try { const res = await handle(req, { data }); if (res) process.stdout.write(JSON.stringify(res) + '\n'); }
|
|
175
|
+
catch { /* never crash the server */ }
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
process.stdin.on('end', () => { queue.then(() => process.exit(0)); });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { handle, toolDefs, callTool, start };
|