@sakaniui/react 0.8.7 → 0.8.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/AGENTS.md +8 -2
- package/CHANGELOG.md +472 -0
- package/README.md +11 -2
- package/bin/sakaniui-mcp.mjs +229 -58
- package/dist/manifest.json +829 -674
- package/package.json +3 -2
package/bin/sakaniui-mcp.mjs
CHANGED
|
@@ -2,37 +2,126 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* SakaniUI MCP server -- lets an AI editor/agent look up the design system instead of guessing.
|
|
4
4
|
*
|
|
5
|
-
* npx -y -p @sakaniui/react sakaniui-mcp
|
|
5
|
+
* npx -y -p @sakaniui/react sakaniui-mcp [--project <dir>]
|
|
6
6
|
*
|
|
7
|
-
* Zero dependencies: speaks newline-delimited JSON-RPC (MCP stdio transport)
|
|
8
|
-
* files shipped in this package -- dist/manifest.json (generated from the source on every build)
|
|
9
|
-
* and AGENTS.md -- so it always matches the installed version.
|
|
7
|
+
* Zero dependencies: speaks newline-delimited JSON-RPC (MCP stdio transport).
|
|
10
8
|
*
|
|
11
|
-
*
|
|
9
|
+
* Read-only by construction. It reads four fixed files (package.json, dist/manifest.json,
|
|
10
|
+
* AGENTS.md, CHANGELOG.md) from a @sakaniui/react install and writes nothing, runs nothing and
|
|
11
|
+
* opens no network connection. No tool takes a path; every tool argument is validated.
|
|
12
|
+
*
|
|
13
|
+
* Version-aware. `npx` may fetch a newer release than the project uses, so the server looks for
|
|
14
|
+
* the project's own installed @sakaniui/react (walking up from --project or the working
|
|
15
|
+
* directory). If it has a manifest, answers come from that copy. If it is older than the
|
|
16
|
+
* manifest era, answers come from this copy but every component is checked against its `since`
|
|
17
|
+
* release and flagged when the project does not have it yet.
|
|
12
18
|
*/
|
|
13
19
|
import fs from 'node:fs';
|
|
14
20
|
import path from 'node:path';
|
|
15
21
|
import readline from 'node:readline';
|
|
16
22
|
import { fileURLToPath } from 'node:url';
|
|
17
23
|
|
|
18
|
-
const
|
|
19
|
-
|
|
24
|
+
const ownRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
|
|
26
|
+
/* ---------- reading an install (fixed file names only) ---------- */
|
|
27
|
+
const readText = (root, rel) => { try { return fs.readFileSync(path.join(root, rel), 'utf8'); } catch { return null; } };
|
|
28
|
+
const readJson = (root, rel) => { try { return JSON.parse(readText(root, rel)); } catch { return null; } };
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
function loadInstall(root) {
|
|
31
|
+
const pkg = readJson(root, 'package.json');
|
|
32
|
+
if (!pkg || pkg.name !== '@sakaniui/react') return null;
|
|
33
|
+
return { root, version: pkg.version, manifest: readJson(root, 'dist/manifest.json'), guide: readText(root, 'AGENTS.md'), changelog: readText(root, 'CHANGELOG.md') };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findProjectInstall() {
|
|
37
|
+
const i = process.argv.indexOf('--project');
|
|
38
|
+
let dir = path.resolve(i > -1 && process.argv[i + 1] ? process.argv[i + 1] : process.cwd());
|
|
39
|
+
for (let depth = 0; depth < 12; depth++) {
|
|
40
|
+
const candidate = path.join(dir, 'node_modules', '@sakaniui', 'react');
|
|
41
|
+
if (fs.existsSync(path.join(candidate, 'package.json'))) return loadInstall(candidate);
|
|
42
|
+
const up = path.dirname(dir);
|
|
43
|
+
if (up === dir) break;
|
|
44
|
+
dir = up;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const bundled = loadInstall(ownRoot);
|
|
50
|
+
if (!bundled || !bundled.manifest) {
|
|
25
51
|
process.stderr.write('sakaniui-mcp: dist/manifest.json not found. Run `npm run build:lib` (or install a published version).\n');
|
|
26
52
|
process.exit(1);
|
|
27
53
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
54
|
+
const project = findProjectInstall();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Answer from the project's own copy when it can (it is what the code will run against) -- but only
|
|
58
|
+
* if its manifest is at least as complete as this one. Older manifest schemas are known to omit
|
|
59
|
+
* things that exist (components, tokens), and "not in the manifest" must mean "does not exist".
|
|
60
|
+
* In that case the newer data is used and checked against the project's version instead.
|
|
61
|
+
*/
|
|
62
|
+
const schemaOf = (i) => (i && i.manifest && Number.isInteger(i.manifest.schemaVersion) ? i.manifest.schemaVersion : 0);
|
|
63
|
+
const active = project && project.manifest && schemaOf(project) >= schemaOf(bundled) ? project : bundled;
|
|
64
|
+
const manifest = active.manifest;
|
|
65
|
+
const guide = active.guide || bundled.guide || '';
|
|
66
|
+
const changelog = active.changelog || bundled.changelog || '';
|
|
67
|
+
const projectVersion = project ? project.version : null;
|
|
68
|
+
|
|
69
|
+
/* ---------- versions ---------- */
|
|
70
|
+
const SEMVER = /^\d+\.\d+\.\d+/;
|
|
71
|
+
const parse = (v) => String(v).split('-')[0].split('.').map(Number);
|
|
72
|
+
const cmp = (a, b) => { const x = parse(a), y = parse(b); for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i]; return 0; };
|
|
73
|
+
|
|
74
|
+
const refVersion = manifest.version;
|
|
75
|
+
const answeringFromProject = active === project;
|
|
76
|
+
|
|
77
|
+
function versionNote(installed = projectVersion) {
|
|
78
|
+
if (answeringFromProject) return null;
|
|
79
|
+
if (!installed) {
|
|
80
|
+
return `The project's installed @sakaniui/react version could not be detected, so this describes ${refVersion}. Confirm it (npm ls @sakaniui/react) and call check_compatibility with installedVersion before suggesting a component.`;
|
|
81
|
+
}
|
|
82
|
+
if (installed !== refVersion) {
|
|
83
|
+
return `The project has @sakaniui/react ${installed} installed; this reference describes ${refVersion}. Entries marked availableInInstalled: false were added after ${installed}. Props can also differ between releases: check get_changelog.`;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Is this component in the project's install? null = cannot tell. */
|
|
89
|
+
function inInstalled(c, installed = projectVersion) {
|
|
90
|
+
if (!installed || !c.since) return null;
|
|
91
|
+
return cmp(c.since, installed) <= 0;
|
|
92
|
+
}
|
|
30
93
|
|
|
31
94
|
/* ---------- helpers ---------- */
|
|
32
|
-
const
|
|
95
|
+
const byName = new Map(manifest.components.map((c) => [c.name.toLowerCase(), c]));
|
|
96
|
+
const brief = (c) => {
|
|
97
|
+
const out = { name: c.name, kind: c.kind, category: c.category, import: c.import, since: c.since, summary: c.summary, requiredProps: c.contract.requiredProps };
|
|
98
|
+
if (inInstalled(c) === false) out.availableInInstalled = false;
|
|
99
|
+
return out;
|
|
100
|
+
};
|
|
33
101
|
const text = (obj) => ({ content: [{ type: 'text', text: typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2) }] });
|
|
34
102
|
const fail = (msg) => ({ isError: true, content: [{ type: 'text', text: msg }] });
|
|
35
103
|
|
|
104
|
+
const MAX_STR = 200;
|
|
105
|
+
function strArg(args, key, { required = false } = {}) {
|
|
106
|
+
const v = args[key];
|
|
107
|
+
if (v === undefined || v === null) return required ? { err: `"${key}" is required.` } : { value: undefined };
|
|
108
|
+
if (typeof v !== 'string') return { err: `"${key}" must be a string.` };
|
|
109
|
+
if (v.length > MAX_STR) return { err: `"${key}" is too long (max ${MAX_STR} characters).` };
|
|
110
|
+
return { value: v };
|
|
111
|
+
}
|
|
112
|
+
function enumArg(args, key, allowed) {
|
|
113
|
+
const v = args[key];
|
|
114
|
+
if (v === undefined || v === null) return { value: undefined };
|
|
115
|
+
if (!allowed.includes(v)) return { err: `"${key}" must be one of: ${allowed.join(', ')}.` };
|
|
116
|
+
return { value: v };
|
|
117
|
+
}
|
|
118
|
+
const verArg = (args, key) => {
|
|
119
|
+
const s = strArg(args, key);
|
|
120
|
+
if (s.err || s.value === undefined) return s;
|
|
121
|
+
return SEMVER.test(s.value) ? s : { err: `"${key}" must be a version like 0.8.7.` };
|
|
122
|
+
};
|
|
123
|
+
const firstError = (...rs) => rs.find((r) => r.err);
|
|
124
|
+
|
|
36
125
|
function score(c, terms) {
|
|
37
126
|
const hay = `${c.name} ${c.category ?? ''} ${c.summary} ${c.props.map((p) => p.name).join(' ')}`.toLowerCase();
|
|
38
127
|
let s = 0;
|
|
@@ -65,88 +154,165 @@ function closest(name) {
|
|
|
65
154
|
|
|
66
155
|
const NATIVE = /^(className|style|id|ref|key|children|role|tabIndex|title|hidden|lang|dir|slot|on[A-Z].*|aria-.*|data-.*)$/;
|
|
67
156
|
|
|
157
|
+
/** Release notes: [{version, body}] newest first, from the CHANGELOG that matches the answering install. */
|
|
158
|
+
function releases() {
|
|
159
|
+
const parts = changelog.split(/^## (\d+\.\d+\.\d+[^\n]*)\n/m);
|
|
160
|
+
const out = [];
|
|
161
|
+
for (let i = 1; i < parts.length; i += 2) out.push({ version: parts[i].trim(), body: (parts[i + 1] || '').trim() });
|
|
162
|
+
return out.sort((a, b) => cmp(b.version, a.version));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
166
|
+
|
|
68
167
|
/* ---------- tools ---------- */
|
|
69
168
|
const tools = {
|
|
70
169
|
search_components: {
|
|
71
|
-
description: 'Find SakaniUI components/blocks by what you need (e.g. "date picker", "sidebar navigation", "pricing"). Returns the best matches with import path and required props.',
|
|
170
|
+
description: 'Find SakaniUI components/blocks by what you need (e.g. "date picker", "sidebar navigation", "pricing"). Returns the best matches with import path, the release that added them and required props.',
|
|
72
171
|
inputSchema: { type: 'object', properties: { query: { type: 'string' }, kind: { type: 'string', enum: ['component', 'block'] }, limit: { type: 'number' } }, required: ['query'] },
|
|
73
|
-
run(
|
|
74
|
-
const
|
|
172
|
+
run(args) {
|
|
173
|
+
const q = strArg(args, 'query', { required: true }); const k = enumArg(args, 'kind', ['component', 'block']);
|
|
174
|
+
const e = firstError(q, k); if (e) return fail(e.err);
|
|
175
|
+
const limit = Number.isInteger(args.limit) ? Math.min(Math.max(args.limit, 1), 25) : 8;
|
|
176
|
+
const terms = q.value.toLowerCase().split(/\s+/).filter(Boolean);
|
|
75
177
|
const hits = manifest.components
|
|
76
|
-
.filter((c) => !
|
|
178
|
+
.filter((c) => !k.value || c.kind === k.value)
|
|
77
179
|
.map((c) => [score(c, terms), c])
|
|
78
180
|
.filter(([s]) => s > 0)
|
|
79
181
|
.sort((a, b) => b[0] - a[0])
|
|
80
182
|
.slice(0, limit)
|
|
81
183
|
.map(([, c]) => brief(c));
|
|
82
|
-
|
|
184
|
+
const note = versionNote();
|
|
185
|
+
const body = hits.length ? hits : `No match for "${q.value}". If nothing fits, follow AGENTS.md section 3 ("When nothing fits"): compose from tokens and say so. Try list_components to browse.`;
|
|
186
|
+
return text(note && hits.length ? { versionNote: note, results: hits } : body);
|
|
83
187
|
},
|
|
84
188
|
},
|
|
85
189
|
get_component: {
|
|
86
|
-
description: 'Full contract for one component or block: import path, every prop with type / allowed values / default / required, and the accessible-name rule. Use this before writing JSX.',
|
|
190
|
+
description: 'Full contract for one component or block: import path, the release that added it, every prop with type / allowed values / default / required, and the accessible-name rule. Use this before writing JSX.',
|
|
87
191
|
inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
|
|
88
|
-
run(
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
192
|
+
run(args) {
|
|
193
|
+
const n = strArg(args, 'name', { required: true }); if (n.err) return fail(n.err);
|
|
194
|
+
const c = byName.get(n.value.toLowerCase());
|
|
195
|
+
if (!c) return fail(`No component named "${n.value}". Did you mean: ${closest(n.value).join(', ') || '(none close)'}? Use search_components.`);
|
|
196
|
+
const out = { ...c, note: 'Native HTML attributes (className, style, aria-*, event handlers) are also accepted on the root element.' };
|
|
197
|
+
if (inInstalled(c) === false) out.availableInInstalled = false;
|
|
198
|
+
const note = versionNote();
|
|
199
|
+
if (note) out.versionNote = note;
|
|
200
|
+
if (inInstalled(c) === false) out.warning = `${c.name} was added in ${c.since} but the project has ${projectVersion}. Do not use it without upgrading (npm install @sakaniui/react@latest).`;
|
|
201
|
+
return text(out);
|
|
92
202
|
},
|
|
93
203
|
},
|
|
94
204
|
list_components: {
|
|
95
205
|
description: 'List all components and blocks (name + one-line summary), optionally filtered by kind or category (e.g. Forms, Core, Marketing).',
|
|
96
206
|
inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['component', 'block'] }, category: { type: 'string' } } },
|
|
97
|
-
run(
|
|
207
|
+
run(args) {
|
|
208
|
+
const k = enumArg(args, 'kind', ['component', 'block']); const cat = strArg(args, 'category');
|
|
209
|
+
const e = firstError(k, cat); if (e) return fail(e.err);
|
|
98
210
|
const list = manifest.components
|
|
99
|
-
.filter((c) => (!
|
|
100
|
-
.map((c) =>
|
|
211
|
+
.filter((c) => (!k.value || c.kind === k.value) && (!cat.value || (c.category ?? '').toLowerCase().includes(cat.value.toLowerCase())))
|
|
212
|
+
.map((c) => { const o = { name: c.name, kind: c.kind, category: c.category, since: c.since, summary: c.summary }; if (inInstalled(c) === false) o.availableInInstalled = false; return o; });
|
|
101
213
|
const categories = [...new Set(manifest.components.map((c) => c.category).filter(Boolean))].sort();
|
|
102
|
-
|
|
214
|
+
const out = { version: refVersion, count: list.length, categories, components: list };
|
|
215
|
+
const note = versionNote(); if (note) out.versionNote = note;
|
|
216
|
+
return text(out);
|
|
103
217
|
},
|
|
104
218
|
},
|
|
105
219
|
get_tokens: {
|
|
106
|
-
description: 'Design tokens (CSS custom properties). Pass a group (color, space, radius, shadow, font, motion, ...) and/or a substring to filter. Only these tokens exist.',
|
|
220
|
+
description: 'Design tokens (CSS custom properties). Pass a group (color, space, radius, shadow, font, motion, glass, surface, liquid, ...) and/or a substring to filter. Only these tokens exist.',
|
|
107
221
|
inputSchema: { type: 'object', properties: { group: { type: 'string' }, query: { type: 'string' } } },
|
|
108
|
-
run(
|
|
222
|
+
run(args) {
|
|
223
|
+
const g = strArg(args, 'group'); const q = strArg(args, 'query');
|
|
224
|
+
const e = firstError(g, q); if (e) return fail(e.err);
|
|
109
225
|
const groups = manifest.tokens.groups;
|
|
110
|
-
if (
|
|
226
|
+
if (g.value && !Object.hasOwn(groups, g.value)) return fail(`Unknown token group "${g.value}". Groups: ${Object.keys(groups).join(', ')}`);
|
|
227
|
+
const needle = q.value?.toLowerCase();
|
|
111
228
|
const out = {};
|
|
112
|
-
for (const [
|
|
113
|
-
if (
|
|
114
|
-
const sel = Object.entries(toks).filter(([k, v]) => !
|
|
115
|
-
if (sel.length) out[
|
|
229
|
+
for (const [name, toks] of Object.entries(groups)) {
|
|
230
|
+
if (g.value && name !== g.value) continue;
|
|
231
|
+
const sel = Object.entries(toks).filter(([k, v]) => !needle || k.includes(needle) || String(v).toLowerCase().includes(needle));
|
|
232
|
+
if (sel.length) out[name] = Object.fromEntries(sel);
|
|
116
233
|
}
|
|
117
234
|
return text(Object.keys(out).length ? out : `No tokens match. Groups: ${Object.keys(groups).join(', ')}`);
|
|
118
235
|
},
|
|
119
236
|
},
|
|
120
237
|
check_usage: {
|
|
121
|
-
description: 'Validate props you are about to use against the contract: reports missing required props, values outside an enum,
|
|
122
|
-
inputSchema: { type: 'object', properties: { component: { type: 'string' }, props: { type: 'object', description: 'prop name -> value (use true/false/strings; for non-literal values pass any placeholder)' } }, required: ['component'] },
|
|
123
|
-
run(
|
|
124
|
-
const
|
|
125
|
-
|
|
238
|
+
description: 'Validate props you are about to use against the contract: reports missing required props, values outside an enum, props that do not exist, and components the project\'s installed version does not have. Run it on each component you write.',
|
|
239
|
+
inputSchema: { type: 'object', properties: { component: { type: 'string' }, props: { type: 'object', description: 'prop name -> value (use true/false/numbers/strings; for non-literal values pass any placeholder)' } }, required: ['component'] },
|
|
240
|
+
run(args) {
|
|
241
|
+
const n = strArg(args, 'component', { required: true }); if (n.err) return fail(n.err);
|
|
242
|
+
const props = args.props === undefined || args.props === null ? {} : args.props;
|
|
243
|
+
if (typeof props !== 'object' || Array.isArray(props)) return fail('"props" must be an object of prop name -> value.');
|
|
244
|
+
if (Object.keys(props).length > 200) return fail('"props" has too many entries (max 200).');
|
|
245
|
+
const c = byName.get(n.value.toLowerCase());
|
|
246
|
+
if (!c) return fail(`No component named "${n.value}". Did you mean: ${closest(n.value).join(', ') || '(none close)'}?`);
|
|
126
247
|
const known = new Map(c.props.map((p) => [p.name, p]));
|
|
127
248
|
const errors = [];
|
|
128
249
|
const warnings = [];
|
|
129
|
-
|
|
250
|
+
if (inInstalled(c) === false) errors.push(`${c.name} was added in ${c.since}; the project has ${projectVersion}. It does not exist there.`);
|
|
251
|
+
for (const r of c.contract.requiredProps) if (!Object.hasOwn(props, r)) errors.push(`Missing required prop "${r}".`);
|
|
130
252
|
for (const [k, v] of Object.entries(props)) {
|
|
131
253
|
const p = known.get(k);
|
|
132
254
|
if (!p) { if (!NATIVE.test(k)) warnings.push(`"${k}" is not a ${c.name} prop in the manifest (it would only work if it is a native attribute).`); continue; }
|
|
133
|
-
if (p.values && typeof v === 'string'
|
|
255
|
+
if (p.values && (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') && !p.values.includes(v)) {
|
|
256
|
+
errors.push(`"${k}" = ${JSON.stringify(v)} is not allowed. Use one of: ${p.values.map((x) => JSON.stringify(x)).join(', ')}.`);
|
|
257
|
+
}
|
|
134
258
|
}
|
|
135
259
|
if (c.contract.requiresAccessibleName && !errors.length) warnings.push('This control has no visible text of its own: keep its accessible-name prop set.');
|
|
136
|
-
|
|
260
|
+
const out = { component: c.name, ok: errors.length === 0, errors, warnings };
|
|
261
|
+
const note = versionNote(); if (note) out.versionNote = note;
|
|
262
|
+
return text(out);
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
check_compatibility: {
|
|
266
|
+
description: 'Compare the version this reference describes with the version installed in the project, and list what the project does not have yet. Call this first when you cannot tell which @sakaniui/react version the project uses (pass installedVersion, from `npm ls @sakaniui/react` or package.json).',
|
|
267
|
+
inputSchema: { type: 'object', properties: { installedVersion: { type: 'string', description: 'e.g. 0.7.0' } } },
|
|
268
|
+
run(args) {
|
|
269
|
+
const v = verArg(args, 'installedVersion'); if (v.err) return fail(v.err.err);
|
|
270
|
+
const installed = v.value ? v.value.match(SEMVER)[0] : projectVersion;
|
|
271
|
+
const source = v.value ? 'provided' : projectVersion ? 'detected' : 'unknown';
|
|
272
|
+
if (!installed) {
|
|
273
|
+
return text({ reference: refVersion, installed: null, source, advice: 'The project version is unknown. Run `npm ls @sakaniui/react`, then call this tool again with installedVersion.' });
|
|
274
|
+
}
|
|
275
|
+
const missing = manifest.components.filter((c) => c.since && cmp(c.since, installed) > 0).map((c) => ({ name: c.name, since: c.since }));
|
|
276
|
+
const newer = releases().filter((r) => cmp(r.version, installed) > 0).map((r) => r.version);
|
|
277
|
+
const relation = cmp(installed, refVersion) === 0 ? 'same' : cmp(installed, refVersion) < 0 ? 'project is older' : 'project is newer than this reference';
|
|
278
|
+
const advice = relation === 'same' ? 'The reference matches the installed version.'
|
|
279
|
+
: relation === 'project is older' ? `Do not suggest the components listed under missingInInstalled, and check get_changelog (since ${installed}) before relying on newer props. Or upgrade: npm install @sakaniui/react@latest.`
|
|
280
|
+
: 'The project has a newer release than this reference. Run the server from the project root (or pass --project) so it answers from the installed copy.';
|
|
281
|
+
return text({ reference: refVersion, installed, source, relation, missingInInstalled: missing, releasesAfterInstalled: newer, advice });
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
get_changelog: {
|
|
285
|
+
description: 'Release notes that match a version. With `version`: that release. With `since`: every release after it, up to the version this reference describes. With neither: the releases between the project\'s installed version and this reference (or the latest release when they match or the project version is unknown).',
|
|
286
|
+
inputSchema: { type: 'object', properties: { version: { type: 'string' }, since: { type: 'string' } } },
|
|
287
|
+
run(args) {
|
|
288
|
+
const ver = verArg(args, 'version'); const since = verArg(args, 'since');
|
|
289
|
+
const e = firstError(ver, since); if (e) return fail(e.err);
|
|
290
|
+
const all = releases();
|
|
291
|
+
if (!all.length) return fail('No release notes are bundled with this install.');
|
|
292
|
+
let pick;
|
|
293
|
+
if (ver.value) {
|
|
294
|
+
pick = all.filter((r) => cmp(r.version, ver.value) === 0 && r.version.startsWith(ver.value.match(SEMVER)[0]));
|
|
295
|
+
if (!pick.length) return fail(`No release notes for ${ver.value}. Releases: ${all.map((r) => r.version).join(', ')}`);
|
|
296
|
+
} else {
|
|
297
|
+
const from = since.value ?? (projectVersion && cmp(projectVersion, refVersion) < 0 ? projectVersion : null);
|
|
298
|
+
pick = from ? all.filter((r) => cmp(r.version, from) > 0 && cmp(r.version, refVersion) <= 0) : all.slice(0, 1);
|
|
299
|
+
if (!pick.length) return text(`No releases after ${from}. This reference describes ${refVersion}.`);
|
|
300
|
+
}
|
|
301
|
+
return text(pick.map((r) => `## ${r.version}\n\n${r.body}`).join('\n\n'));
|
|
137
302
|
},
|
|
138
303
|
},
|
|
139
304
|
get_guide: {
|
|
140
305
|
description: 'The AGENTS.md guide (principles, token rules, when nothing fits, glass, icons, per-component "why / when not to use", checklist). Pass a section number or heading text to get just that section; omit for the section list.',
|
|
141
306
|
inputSchema: { type: 'object', properties: { section: { type: 'string' } } },
|
|
142
|
-
run(
|
|
307
|
+
run(args) {
|
|
308
|
+
const s0 = strArg(args, 'section'); if (s0.err) return fail(s0.err);
|
|
143
309
|
if (!guide) return fail('AGENTS.md is not available in this install.');
|
|
144
|
-
const parts = guide.split(/^(?=## )/m);
|
|
145
|
-
const heads = parts.
|
|
146
|
-
if (!
|
|
147
|
-
const s =
|
|
148
|
-
const hit = parts.filter((p) =>
|
|
149
|
-
return hit.length ? text(hit.join('\n')) : fail(`No section "${
|
|
310
|
+
const parts = guide.split(/^(?=## )/m).filter((p) => p.startsWith('## '));
|
|
311
|
+
const heads = parts.map((p) => p.split('\n')[0].replace(/^## /, ''));
|
|
312
|
+
if (!s0.value) return text({ version: refVersion, sections: heads, hint: 'Call again with a number (e.g. "5") or heading text.' });
|
|
313
|
+
const s = s0.value.toLowerCase().trim();
|
|
314
|
+
const hit = parts.filter((p) => { const h = p.split('\n')[0].replace(/^## /, '').toLowerCase(); return h === s || h.startsWith(s + '.') || h.startsWith(s + ' ') || h.includes(s); });
|
|
315
|
+
return hit.length ? text(hit.join('\n')) : fail(`No section "${s0.value}". Sections: ${heads.join(' | ')}`);
|
|
150
316
|
},
|
|
151
317
|
},
|
|
152
318
|
};
|
|
@@ -155,28 +321,33 @@ const tools = {
|
|
|
155
321
|
const send = (msg) => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...msg }) + '\n');
|
|
156
322
|
|
|
157
323
|
function handle(msg) {
|
|
324
|
+
if (!msg || typeof msg !== 'object' || Array.isArray(msg)) return send({ id: null, error: { code: -32600, message: 'Invalid request' } });
|
|
158
325
|
const { id, method, params } = msg;
|
|
159
326
|
const isRequest = id !== undefined && id !== null;
|
|
160
327
|
try {
|
|
161
328
|
switch (method) {
|
|
162
|
-
case 'initialize':
|
|
329
|
+
case 'initialize': {
|
|
330
|
+
const note = versionNote();
|
|
163
331
|
return send({ id, result: {
|
|
164
332
|
protocolVersion: params?.protocolVersion || '2025-06-18',
|
|
165
333
|
capabilities: { tools: {} },
|
|
166
|
-
serverInfo: { name: 'sakaniui', version:
|
|
167
|
-
instructions: `SakaniUI ${
|
|
334
|
+
serverInfo: { name: 'sakaniui', version: refVersion },
|
|
335
|
+
instructions: `SakaniUI ${refVersion}${projectVersion ? ` (project has ${projectVersion})` : ''}. Read-only reference. Look components up with search_components / get_component before writing JSX, validate with check_usage, use only tokens from get_tokens, and read get_guide when nothing fits.${note ? ` ${note}` : ''}`,
|
|
168
336
|
} });
|
|
337
|
+
}
|
|
169
338
|
case 'ping':
|
|
170
339
|
return send({ id, result: {} });
|
|
171
340
|
case 'tools/list':
|
|
172
|
-
return send({ id, result: { tools: Object.entries(tools).map(([name, t]) => ({ name, description: t.description, inputSchema: t.inputSchema })) } });
|
|
341
|
+
return send({ id, result: { tools: Object.entries(tools).map(([name, t]) => ({ name, description: t.description, inputSchema: t.inputSchema, annotations: READ_ONLY })) } });
|
|
173
342
|
case 'tools/call': {
|
|
174
|
-
const t = tools[params
|
|
175
|
-
if (!t) return send({ id, error: { code: -32602, message: `Unknown tool: ${params?.name}` } });
|
|
176
|
-
|
|
343
|
+
const t = typeof params?.name === 'string' && Object.hasOwn(tools, params.name) ? tools[params.name] : null;
|
|
344
|
+
if (!t) return send({ id, error: { code: -32602, message: `Unknown tool: ${String(params?.name).slice(0, 60)}` } });
|
|
345
|
+
const args = params.arguments;
|
|
346
|
+
if (args !== undefined && args !== null && (typeof args !== 'object' || Array.isArray(args))) return send({ id, result: fail('Tool arguments must be an object.') });
|
|
347
|
+
return send({ id, result: t.run(args || {}) });
|
|
177
348
|
}
|
|
178
349
|
default:
|
|
179
|
-
if (isRequest) send({ id, error: { code: -32601, message: `Method not found: ${method}` } });
|
|
350
|
+
if (isRequest) send({ id, error: { code: -32601, message: `Method not found: ${String(method).slice(0, 60)}` } });
|
|
180
351
|
}
|
|
181
352
|
} catch (e) {
|
|
182
353
|
if (isRequest) send({ id, error: { code: -32603, message: String(e?.message || e) } });
|