@sakaniui/react 0.8.7 → 0.8.9

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.
@@ -2,37 +2,128 @@
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) and answers from the
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
- * Tools: search_components, get_component, list_components, get_tokens, check_usage, get_guide.
9
+ * Read-only by construction. It reads five fixed files (package.json, dist/manifest.json,
10
+ * dist/icon-index.json, AGENTS.md, CHANGELOG.md) from a @sakaniui/react install and writes nothing,
11
+ * runs nothing and opens no network connection. No tool takes a path; every argument is validated.
12
+ * Code passed to check_code is only read as text, never executed.
13
+ *
14
+ * Version-aware. `npx` may fetch a newer release than the project uses, so the server looks for
15
+ * the project's own installed @sakaniui/react (walking up from --project or the working
16
+ * directory). If it has a manifest, answers come from that copy. If it is older than the
17
+ * manifest era, answers come from this copy but every component is checked against its `since`
18
+ * release and flagged when the project does not have it yet.
12
19
  */
13
20
  import fs from 'node:fs';
14
21
  import path from 'node:path';
15
22
  import readline from 'node:readline';
16
23
  import { fileURLToPath } from 'node:url';
17
24
 
18
- const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
- const readPkgFile = (p) => fs.readFileSync(path.join(pkgRoot, p), 'utf8');
25
+ const ownRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+
27
+ /* ---------- reading an install (fixed file names only) ---------- */
28
+ const readText = (root, rel) => { try { return fs.readFileSync(path.join(root, rel), 'utf8'); } catch { return null; } };
29
+ const readJson = (root, rel) => { try { return JSON.parse(readText(root, rel)); } catch { return null; } };
30
+
31
+ function loadInstall(root) {
32
+ const pkg = readJson(root, 'package.json');
33
+ if (!pkg || pkg.name !== '@sakaniui/react') return null;
34
+ return { root, version: pkg.version, manifest: readJson(root, 'dist/manifest.json'), icons: readJson(root, 'dist/icon-index.json'), guide: readText(root, 'AGENTS.md'), changelog: readText(root, 'CHANGELOG.md') };
35
+ }
20
36
 
21
- let manifest;
22
- try {
23
- manifest = JSON.parse(readPkgFile('dist/manifest.json'));
24
- } catch {
37
+ function findProjectInstall() {
38
+ const i = process.argv.indexOf('--project');
39
+ let dir = path.resolve(i > -1 && process.argv[i + 1] ? process.argv[i + 1] : process.cwd());
40
+ for (let depth = 0; depth < 12; depth++) {
41
+ const candidate = path.join(dir, 'node_modules', '@sakaniui', 'react');
42
+ if (fs.existsSync(path.join(candidate, 'package.json'))) return loadInstall(candidate);
43
+ const up = path.dirname(dir);
44
+ if (up === dir) break;
45
+ dir = up;
46
+ }
47
+ return null;
48
+ }
49
+
50
+ const bundled = loadInstall(ownRoot);
51
+ if (!bundled || !bundled.manifest) {
25
52
  process.stderr.write('sakaniui-mcp: dist/manifest.json not found. Run `npm run build:lib` (or install a published version).\n');
26
53
  process.exit(1);
27
54
  }
28
- const guide = (() => { try { return readPkgFile('AGENTS.md'); } catch { return ''; } })();
29
- const byName = new Map(manifest.components.map((c) => [c.name.toLowerCase(), c]));
55
+ const project = findProjectInstall();
56
+
57
+ /**
58
+ * Answer from the project's own copy when it can (it is what the code will run against) -- but only
59
+ * if its manifest is at least as complete as this one. Older manifest schemas are known to omit
60
+ * things that exist (components, tokens), and "not in the manifest" must mean "does not exist".
61
+ * In that case the newer data is used and checked against the project's version instead.
62
+ */
63
+ const schemaOf = (i) => (i && i.manifest && Number.isInteger(i.manifest.schemaVersion) ? i.manifest.schemaVersion : 0);
64
+ const active = project && project.manifest && schemaOf(project) >= schemaOf(bundled) ? project : bundled;
65
+ const manifest = active.manifest;
66
+ const guide = active.guide || bundled.guide || '';
67
+ const changelog = active.changelog || bundled.changelog || '';
68
+ const iconIndex = (active.icons && active.icons.icons) || (bundled.icons && bundled.icons.icons) || [];
69
+ const projectVersion = project ? project.version : null;
70
+
71
+ /* ---------- versions ---------- */
72
+ const SEMVER = /^\d+\.\d+\.\d+/;
73
+ const parse = (v) => String(v).split('-')[0].split('.').map(Number);
74
+ 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; };
75
+
76
+ const refVersion = manifest.version;
77
+ const answeringFromProject = active === project;
78
+
79
+ function versionNote(installed = projectVersion) {
80
+ if (answeringFromProject) return null;
81
+ if (!installed) {
82
+ 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.`;
83
+ }
84
+ if (installed !== refVersion) {
85
+ 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.`;
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /** Is this component in the project's install? null = cannot tell. */
91
+ function inInstalled(c, installed = projectVersion) {
92
+ if (!installed || !c.since) return null;
93
+ return cmp(c.since, installed) <= 0;
94
+ }
30
95
 
31
96
  /* ---------- helpers ---------- */
32
- const brief = (c) => ({ name: c.name, kind: c.kind, category: c.category, import: c.import, summary: c.summary, requiredProps: c.contract.requiredProps });
97
+ const byName = new Map(manifest.components.map((c) => [c.name.toLowerCase(), c]));
98
+ const brief = (c) => {
99
+ const out = { name: c.name, kind: c.kind, category: c.category, import: c.import, since: c.since, summary: c.summary, requiredProps: c.contract.requiredProps };
100
+ if (inInstalled(c) === false) out.availableInInstalled = false;
101
+ return out;
102
+ };
33
103
  const text = (obj) => ({ content: [{ type: 'text', text: typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2) }] });
34
104
  const fail = (msg) => ({ isError: true, content: [{ type: 'text', text: msg }] });
35
105
 
106
+ const MAX_STR = 200;
107
+ function strArg(args, key, { required = false } = {}) {
108
+ const v = args[key];
109
+ if (v === undefined || v === null) return required ? { err: `"${key}" is required.` } : { value: undefined };
110
+ if (typeof v !== 'string') return { err: `"${key}" must be a string.` };
111
+ if (v.length > MAX_STR) return { err: `"${key}" is too long (max ${MAX_STR} characters).` };
112
+ return { value: v };
113
+ }
114
+ function enumArg(args, key, allowed) {
115
+ const v = args[key];
116
+ if (v === undefined || v === null) return { value: undefined };
117
+ if (!allowed.includes(v)) return { err: `"${key}" must be one of: ${allowed.join(', ')}.` };
118
+ return { value: v };
119
+ }
120
+ const verArg = (args, key) => {
121
+ const s = strArg(args, key);
122
+ if (s.err || s.value === undefined) return s;
123
+ return SEMVER.test(s.value) ? s : { err: `"${key}" must be a version like 0.8.7.` };
124
+ };
125
+ const firstError = (...rs) => rs.find((r) => r.err);
126
+
36
127
  function score(c, terms) {
37
128
  const hay = `${c.name} ${c.category ?? ''} ${c.summary} ${c.props.map((p) => p.name).join(' ')}`.toLowerCase();
38
129
  let s = 0;
@@ -63,90 +154,442 @@ function closest(name) {
63
154
  .map(([, c]) => c);
64
155
  }
65
156
 
157
+ /* ---------- icons ---------- */
158
+ const iconNames = new Set();
159
+ for (const i of iconIndex) { iconNames.add(i.name); if (i.filled) iconNames.add(i.filled); }
160
+ const iconWords = (name) => name.replace(/^Icon/, '').replace(/Filled$/, '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/([A-Za-z])(\d)/g, '$1 $2').toLowerCase();
161
+
162
+ // The conventional icon for common UI intents, ranked first (tags alone rank e.g. IconHttpDelete above IconTrash for "delete").
163
+ const COMMON = {
164
+ delete: ['IconTrash'], remove: ['IconTrash', 'IconX'], trash: ['IconTrash'], bin: ['IconTrash'], edit: ['IconPencil', 'IconEdit'], rename: ['IconPencil'],
165
+ close: ['IconX'], dismiss: ['IconX'], cancel: ['IconX'], add: ['IconPlus'], new: ['IconPlus'], create: ['IconPlus'], plus: ['IconPlus'],
166
+ search: ['IconSearch'], find: ['IconSearch'], settings: ['IconSettings', 'IconAdjustments'], preferences: ['IconSettings'], menu: ['IconMenu2'],
167
+ more: ['IconDots', 'IconDotsVertical'], options: ['IconDots', 'IconDotsVertical'], user: ['IconUser'], profile: ['IconUser', 'IconUserCircle'], account: ['IconUserCircle'],
168
+ home: ['IconHome'], back: ['IconArrowLeft', 'IconChevronLeft'], next: ['IconArrowRight', 'IconChevronRight'], forward: ['IconArrowRight'], previous: ['IconArrowLeft', 'IconChevronLeft'],
169
+ expand: ['IconChevronDown'], collapse: ['IconChevronUp'], dropdown: ['IconChevronDown'], upload: ['IconUpload'], download: ['IconDownload'], share: ['IconShare'],
170
+ copy: ['IconCopy'], duplicate: ['IconCopy'], save: ['IconDeviceFloppy'], filter: ['IconFilter'], sort: ['IconArrowsSort'], calendar: ['IconCalendar'], date: ['IconCalendar'],
171
+ notification: ['IconBell'], notifications: ['IconBell'], bell: ['IconBell'], mail: ['IconMail'], email: ['IconMail'], message: ['IconMessage'], chat: ['IconMessageCircle'],
172
+ phone: ['IconPhone'], call: ['IconPhone'], logout: ['IconLogout'], signout: ['IconLogout'], login: ['IconLogin'], help: ['IconHelpCircle', 'IconHelp'], info: ['IconInfoCircle'],
173
+ warning: ['IconAlertTriangle'], error: ['IconAlertCircle'], success: ['IconCircleCheck'], check: ['IconCheck'], done: ['IconCheck'], favorite: ['IconHeart', 'IconStar'],
174
+ like: ['IconHeart', 'IconThumbUp'], star: ['IconStar'], rating: ['IconStar'], cart: ['IconShoppingCart'], refresh: ['IconRefresh'], reload: ['IconRefresh'], link: ['IconLink'],
175
+ attach: ['IconPaperclip'], attachment: ['IconPaperclip'], lock: ['IconLock'], password: ['IconLock'], view: ['IconEye'], show: ['IconEye'], hide: ['IconEyeOff'],
176
+ play: ['IconPlayerPlay'], pause: ['IconPlayerPause'], stop: ['IconPlayerStop'], theme: ['IconSun', 'IconMoon'], dark: ['IconMoon'], light: ['IconSun'],
177
+ external: ['IconExternalLink'], drag: ['IconGripVertical'], sidebar: ['IconLayoutSidebar'], dashboard: ['IconLayoutDashboard'], analytics: ['IconChartBar'], chart: ['IconChartBar'],
178
+ money: ['IconCurrencyDollar'], payment: ['IconCreditCard'], card: ['IconCreditCard'], location: ['IconMapPin'], time: ['IconClock'], clock: ['IconClock'], file: ['IconFile'],
179
+ folder: ['IconFolder'], image: ['IconPhoto'], photo: ['IconPhoto'], video: ['IconVideo'],
180
+ };
181
+
182
+ function searchIcons(query, limit = 12) {
183
+ const terms = String(query).toLowerCase().replace(/icon/g, ' ').split(/[^a-z0-9]+/).filter(Boolean);
184
+ if (!terms.length) return [];
185
+ const boosted = new Map();
186
+ for (const t of terms) (COMMON[t] || []).forEach((n, k) => boosted.set(n, Math.max(boosted.get(n) || 0, 40 - k * 5)));
187
+ return iconIndex
188
+ .map((i) => {
189
+ const words = iconWords(i.name).split(' ');
190
+ const tags = (i.tags || []).map((t) => t.toLowerCase());
191
+ let s = boosted.get(i.name) || 0;
192
+ for (const t of terms) {
193
+ if (words.join(' ') === t) s += 30;
194
+ else if (words[0] === t) s += 12;
195
+ else if (words.includes(t)) s += 8;
196
+ else if (words.some((w) => w.startsWith(t))) s += 4;
197
+ if (tags.includes(t)) s += 6;
198
+ else if (tags.some((g) => g.includes(t))) s += 2;
199
+ if ((i.category || '').toLowerCase() === t) s += 2;
200
+ }
201
+ return [s - words.length * 0.1, i];
202
+ })
203
+ .filter(([s]) => s > 0.5)
204
+ .sort((a, b) => b[0] - a[0])
205
+ .slice(0, limit)
206
+ .map(([, i]) => i);
207
+ }
208
+
209
+ /** Checks a value given for an icon prop. Returns an error string, or null. */
210
+ function iconProblem(propName, raw, importedElsewhere = new Set()) {
211
+ const v = String(raw).trim().replace(/^\{|\}$/g, '').trim();
212
+ if (/^</.test(v)) {
213
+ const tag = (/^<\s*([A-Za-z][\w.]*)/.exec(v) || [])[1] || 'Icon';
214
+ const real = iconNames.has(tag) || importedElsewhere.has(tag) ? tag : (searchIcons(tag, 1)[0] || {}).name;
215
+ const unknown = real && real !== tag ? ` ${tag} is not a SakaniUI icon either.` : '';
216
+ return `"${propName}" takes the icon component itself, not an element: write ${propName}={${real || 'IconName'}}, not ${propName}={<${tag} />}.${unknown}${!real ? ' Use search_icons to find the real name.' : ''}`;
217
+ }
218
+ if (!/^[A-Za-z_$][\w$]*$/.test(v)) return null; // an expression we cannot judge
219
+ if (iconNames.has(v) || importedElsewhere.has(v)) return null;
220
+ const hint = searchIcons(v, 3).map((i) => i.name);
221
+ return `"${v}" is not a SakaniUI icon.${hint.length ? ` Did you mean: ${hint.join(', ')}?` : ''} Use search_icons to find the real name.`;
222
+ }
223
+
66
224
  const NATIVE = /^(className|style|id|ref|key|children|role|tabIndex|title|hidden|lang|dir|slot|on[A-Z].*|aria-.*|data-.*)$/;
67
225
 
226
+ /** Release notes: [{version, body}] newest first, from the CHANGELOG that matches the answering install. */
227
+ function releases() {
228
+ const parts = changelog.split(/^## (\d+\.\d+\.\d+[^\n]*)\n/m);
229
+ const out = [];
230
+ for (let i = 1; i < parts.length; i += 2) out.push({ version: parts[i].trim(), body: (parts[i + 1] || '').trim() });
231
+ return out.sort((a, b) => cmp(b.version, a.version));
232
+ }
233
+
234
+ /* ---------- check_code: a small, read-only JSX scanner (the code is never executed) ---------- */
235
+ /** Line number lookup in O(log n): a precomputed index of line starts per code string. */
236
+ function lineIndex(code) {
237
+ const starts = [0];
238
+ for (let i = 0; i < code.length; i++) if (code.charCodeAt(i) === 10) starts.push(i + 1);
239
+ return (pos) => { let lo = 0, hi = starts.length - 1; while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (starts[mid] <= pos) lo = mid; else hi = mid - 1; } return lo + 1; };
240
+ }
241
+ let lineAt = (code, i) => code.slice(0, i).split('\n').length;
242
+
243
+ function parseImports(code) {
244
+ const out = [];
245
+ const re = /import\s+(type\s+)?(?:([A-Za-z_$][\w$]*)\s*,?\s*)?(?:\{([^}]*)\})?\s*from\s*['"]([^'"]+)['"]/g;
246
+ let m;
247
+ while ((m = re.exec(code))) {
248
+ const names = (m[3] || '').split(',').map((x) => x.trim()).filter(Boolean).map((x) => {
249
+ const typeOnly = /^type\s+/.test(x);
250
+ const [imported, local] = x.replace(/^type\s+/, '').split(/\s+as\s+/).map((y) => y.trim());
251
+ return { imported, local: local || imported, typeOnly: typeOnly || !!m[1] };
252
+ });
253
+ out.push({ from: m[4], names, line: lineAt(code, m.index), def: m[2] || null });
254
+ }
255
+ return out;
256
+ }
257
+
258
+ /** Reads a balanced {...} starting at code[i] === '{'. Returns the index after the closing brace. */
259
+ function skipBraces(code, i) {
260
+ let depth = 0;
261
+ for (; i < code.length; i++) {
262
+ const ch = code[i];
263
+ if (ch === '"' || ch === "'" || ch === '`') {
264
+ const q = ch;
265
+ for (i++; i < code.length && code[i] !== q; i++) if (code[i] === '\\') i++;
266
+ continue;
267
+ }
268
+ if (ch === '{') depth++;
269
+ else if (ch === '}') { depth--; if (depth === 0) return i + 1; }
270
+ }
271
+ return code.length;
272
+ }
273
+
274
+ const ATTR_NAME = /[A-Za-z_][\w:.-]*/y;
275
+
276
+ function parseElements(code, wanted) {
277
+ const els = [];
278
+ const re = /<([A-Z][\w]*(?:\.[A-Z][\w]*)?)(?=[\s/>])/g;
279
+ let m;
280
+ while ((m = re.exec(code))) {
281
+ const tag = m[1];
282
+ if (!wanted(tag)) continue;
283
+ let i = m.index + m[0].length;
284
+ const attrs = [];
285
+ let spread = false;
286
+ let selfClosing = false;
287
+ // every pass must move forward; an unterminated attribute ends the element instead of looping
288
+ for (let guard = 0; i < code.length && guard < 5000; guard++) {
289
+ while (/\s/.test(code[i])) i++;
290
+ if (code[i] === '/' && code[i + 1] === '>') { selfClosing = true; i += 2; break; }
291
+ if (code[i] === '>') { i++; break; }
292
+ if (code[i] === '<') break; // a new tag before this one closed: malformed, stop here
293
+ if (code[i] === '{') { const end = skipBraces(code, i); if (/^\{\s*\.\.\./.test(code.slice(i, end))) spread = true; i = end; continue; }
294
+ ATTR_NAME.lastIndex = i;
295
+ const nm = ATTR_NAME.exec(code);
296
+ if (!nm) { i++; continue; }
297
+ const name = nm[0];
298
+ i += name.length;
299
+ while (/\s/.test(code[i])) i++;
300
+ if (code[i] !== '=') { attrs.push({ name, kind: 'boolean', value: true }); continue; }
301
+ i++;
302
+ while (/\s/.test(code[i])) i++;
303
+ if (code[i] === '"' || code[i] === "'") {
304
+ const q = code[i];
305
+ const end = code.indexOf(q, i + 1);
306
+ if (end === -1) { attrs.push({ name, kind: 'string', value: code.slice(i + 1) }); i = code.length; break; }
307
+ attrs.push({ name, kind: 'string', value: code.slice(i + 1, end) });
308
+ i = end + 1;
309
+ } else if (code[i] === '{') {
310
+ const end = skipBraces(code, i);
311
+ const expr = code.slice(i + 1, end - 1).trim();
312
+ attrs.push({ name, ...classify(expr) });
313
+ i = end;
314
+ }
315
+ }
316
+ els.push({ tag, attrs, spread, selfClosing, line: lineAt(code, m.index) });
317
+ if (els.length >= 5000) break;
318
+ re.lastIndex = Math.max(re.lastIndex, i);
319
+ }
320
+ return els;
321
+ }
322
+
323
+ function classify(expr) {
324
+ let m;
325
+ if ((m = /^(['"])([\s\S]*)\1$/.exec(expr))) return { kind: 'string', value: m[2], raw: expr };
326
+ if ((m = /^`([^`$]*)`$/.exec(expr))) return { kind: 'string', value: m[1], raw: expr };
327
+ if (/^-?\d+(\.\d+)?$/.test(expr)) return { kind: 'number', value: Number(expr), raw: expr };
328
+ if (expr === 'true' || expr === 'false') return { kind: 'boolean', value: expr === 'true', raw: expr };
329
+ if (/^</.test(expr)) return { kind: 'element', raw: expr };
330
+ if (/^[A-Za-z_$][\w$]*$/.test(expr)) return { kind: 'identifier', value: expr, raw: expr };
331
+ return { kind: 'expression', raw: expr };
332
+ }
333
+
334
+ const TYPE_LIKE = /(Props|Type|Variant|Size|Item|Option|Datum|Column|Link|Plan|Post|Action|Step|Image|Author|Change|Data|Tab|Color|Category|Job|Member|Testimonial|Transaction)$/;
335
+
336
+ function checkCode(code) {
337
+ lineAt = ((idx) => (_c, pos) => idx(pos))(lineIndex(code));
338
+ const errors = [];
339
+ const warnings = [];
340
+ const imports = parseImports(code);
341
+ const importedFrom = new Map(); // local name -> module
342
+ for (const imp of imports) for (const n of imp.names) importedFrom.set(n.local, imp.from);
343
+ const importedElsewhere = new Set([...importedFrom].filter(([, from]) => !from.startsWith('@sakaniui/react')).map(([n]) => n));
344
+
345
+ for (const imp of imports) {
346
+ if (!imp.from.startsWith('@sakaniui/react')) continue;
347
+ const entry = imp.from;
348
+ if (!['@sakaniui/react', '@sakaniui/react/blocks', '@sakaniui/react/icons'].includes(entry)) {
349
+ if (imp.names.length) errors.push({ line: imp.line, message: `"${entry}" has no named exports. Import components from @sakaniui/react, blocks from @sakaniui/react/blocks and icons from @sakaniui/react/icons.` });
350
+ continue;
351
+ }
352
+ if (imp.def) errors.push({ line: imp.line, message: `${entry} has no default export; use a named import: import { ${imp.def} } from '${entry}';` });
353
+ for (const n of imp.names) {
354
+ if (n.typeOnly) continue;
355
+ const c = byName.get(n.imported.toLowerCase());
356
+ const isUtility = manifest.utilities?.some((u) => u.name === n.imported);
357
+ if (entry === '@sakaniui/react/icons') {
358
+ if (!iconNames.has(n.imported)) {
359
+ const hint = searchIcons(n.imported, 3).map((i) => i.name);
360
+ errors.push({ line: imp.line, message: `${n.imported} is not exported by @sakaniui/react/icons.${hint.length ? ` Did you mean: ${hint.join(', ')}?` : ''}` });
361
+ }
362
+ continue;
363
+ }
364
+ if (c && c.name !== n.imported) { errors.push({ line: imp.line, message: `${n.imported} has the wrong casing; the export is ${c.name}.` }); continue; }
365
+ if (entry === '@sakaniui/react/blocks') {
366
+ if (!c || c.kind !== 'block') errors.push({ line: imp.line, message: c ? `${n.imported} is a component: import it from @sakaniui/react.` : `${n.imported} is not a SakaniUI block.` });
367
+ continue;
368
+ }
369
+ if (c && c.kind === 'block') { errors.push({ line: imp.line, message: `${n.imported} is a block: import it from @sakaniui/react/blocks.` }); continue; }
370
+ if (!c && !isUtility) {
371
+ if (TYPE_LIKE.test(n.imported)) continue; // most likely a type export
372
+ const near = closest(n.imported);
373
+ errors.push({ line: imp.line, message: `${n.imported} is not exported by @sakaniui/react.${near.length ? ` Did you mean: ${near.join(', ')}?` : ''} Use search_components.` });
374
+ }
375
+ if (c && inInstalled(c) === false) errors.push({ line: imp.line, message: `${c.name} was added in ${c.since}; the project has ${projectVersion}.` });
376
+ }
377
+ }
378
+
379
+ // elements: anything imported from @sakaniui, plus manifest names used without an import
380
+ const isSakani = (tag) => importedFrom.get(tag)?.startsWith('@sakaniui/react') || (!importedFrom.has(tag) && byName.has(tag.toLowerCase()) && byName.get(tag.toLowerCase()).name === tag);
381
+ const elements = parseElements(code, isSakani);
382
+ for (const el of elements) {
383
+ const c = byName.get(el.tag.toLowerCase());
384
+ if (!c) continue;
385
+ const at = (message) => ({ line: el.line, message: `<${el.tag}>: ${message}` });
386
+ if (!importedFrom.has(el.tag)) warnings.push(at(`not imported. Add: import { ${c.name} } from '${c.import}';`));
387
+ const given = new Map(el.attrs.map((a) => [a.name, a]));
388
+ if (!el.selfClosing) given.set('children', { kind: 'expression' });
389
+ for (const r of c.contract.requiredProps) {
390
+ if (given.has(r)) continue;
391
+ if (el.spread) warnings.push(at(`required prop "${r}" is not written out (it may come from the spread).`));
392
+ else errors.push(at(`missing required prop "${r}".`));
393
+ }
394
+ const known = new Map(c.props.map((p) => [p.name, p]));
395
+ for (const a of el.attrs) {
396
+ const p = known.get(a.name);
397
+ if (!p) { if (!NATIVE.test(a.name) && a.name !== 'key' && a.name !== 'ref') warnings.push(at(`"${a.name}" is not a ${c.name} prop (it only works if it is a native attribute).`)); continue; }
398
+ if (p.values && ['string', 'number', 'boolean'].includes(a.kind) && !p.values.includes(a.value)) {
399
+ errors.push(at(`${a.name}=${JSON.stringify(a.value)} is not allowed. Use one of: ${p.values.map((x) => JSON.stringify(x)).join(', ')}.`));
400
+ }
401
+ if (p.type === 'IconComponent') {
402
+ if (a.kind === 'string') errors.push(at(`"${a.name}" takes an icon component, not a string: ${a.name}={IconName}.`));
403
+ else if (a.kind === 'element' || a.kind === 'identifier') {
404
+ const e = iconProblem(a.name, a.raw, importedElsewhere);
405
+ if (e) errors.push(at(e));
406
+ else if (a.kind === 'identifier' && iconNames.has(a.value) && importedFrom.get(a.value) !== '@sakaniui/react/icons') warnings.push(at(`${a.value} is not imported. Add: import { ${a.value} } from '@sakaniui/react/icons';`));
407
+ }
408
+ }
409
+ }
410
+ }
411
+ if (!elements.length && !imports.some((i) => i.from.startsWith('@sakaniui/react'))) warnings.push({ line: 1, message: 'No SakaniUI imports or elements found in this code.' });
412
+ const out = { ok: errors.length === 0, checkedElements: elements.length, errors, warnings };
413
+ const note = versionNote(); if (note) out.versionNote = note;
414
+ return out;
415
+ }
416
+
417
+ const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
418
+
68
419
  /* ---------- tools ---------- */
69
420
  const tools = {
70
421
  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.',
422
+ 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
423
  inputSchema: { type: 'object', properties: { query: { type: 'string' }, kind: { type: 'string', enum: ['component', 'block'] }, limit: { type: 'number' } }, required: ['query'] },
73
- run({ query, kind, limit = 8 }) {
74
- const terms = String(query).toLowerCase().split(/\s+/).filter(Boolean);
424
+ run(args) {
425
+ const q = strArg(args, 'query', { required: true }); const k = enumArg(args, 'kind', ['component', 'block']);
426
+ const e = firstError(q, k); if (e) return fail(e.err);
427
+ const limit = Number.isInteger(args.limit) ? Math.min(Math.max(args.limit, 1), 25) : 8;
428
+ const terms = q.value.toLowerCase().split(/\s+/).filter(Boolean);
75
429
  const hits = manifest.components
76
- .filter((c) => !kind || c.kind === kind)
430
+ .filter((c) => !k.value || c.kind === k.value)
77
431
  .map((c) => [score(c, terms), c])
78
432
  .filter(([s]) => s > 0)
79
433
  .sort((a, b) => b[0] - a[0])
80
434
  .slice(0, limit)
81
435
  .map(([, c]) => brief(c));
82
- return hits.length ? text(hits) : text(`No match for "${query}". If nothing fits, follow AGENTS.md section 3 ("When nothing fits"): compose from tokens and say so. Try list_components to browse.`);
436
+ const note = versionNote();
437
+ 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.`;
438
+ return text(note && hits.length ? { versionNote: note, results: hits } : body);
83
439
  },
84
440
  },
85
441
  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.',
442
+ 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
443
  inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
88
- run({ name }) {
89
- const c = byName.get(String(name).toLowerCase());
90
- if (!c) return fail(`No component named "${name}". Did you mean: ${closest(String(name)).join(', ') || '(none close)'}? Use search_components.`);
91
- return text({ ...c, note: 'Native HTML attributes (className, style, aria-*, event handlers) are also accepted on the root element.' });
444
+ run(args) {
445
+ const n = strArg(args, 'name', { required: true }); if (n.err) return fail(n.err);
446
+ const c = byName.get(n.value.toLowerCase());
447
+ if (!c) return fail(`No component named "${n.value}". Did you mean: ${closest(n.value).join(', ') || '(none close)'}? Use search_components.`);
448
+ const out = { ...c, note: 'Native HTML attributes (className, style, aria-*, event handlers) are also accepted on the root element.' };
449
+ if (c.example) out.exampleNote = 'Start from `example`: it is type-checked against this release. Replace the placeholder text and values; keep the prop names and shapes.';
450
+ if (c.props.some((p) => p.type === 'IconComponent')) out.iconNote = 'Icon props take the component itself (icon={IconTrash}), not an element. The example uses IconStar as a placeholder: find the real icon with search_icons.';
451
+ if (inInstalled(c) === false) out.availableInInstalled = false;
452
+ const note = versionNote();
453
+ if (note) out.versionNote = note;
454
+ 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).`;
455
+ return text(out);
92
456
  },
93
457
  },
94
458
  list_components: {
95
459
  description: 'List all components and blocks (name + one-line summary), optionally filtered by kind or category (e.g. Forms, Core, Marketing).',
96
460
  inputSchema: { type: 'object', properties: { kind: { type: 'string', enum: ['component', 'block'] }, category: { type: 'string' } } },
97
- run({ kind, category }) {
461
+ run(args) {
462
+ const k = enumArg(args, 'kind', ['component', 'block']); const cat = strArg(args, 'category');
463
+ const e = firstError(k, cat); if (e) return fail(e.err);
98
464
  const list = manifest.components
99
- .filter((c) => (!kind || c.kind === kind) && (!category || (c.category ?? '').toLowerCase().includes(String(category).toLowerCase())))
100
- .map((c) => ({ name: c.name, kind: c.kind, category: c.category, summary: c.summary }));
465
+ .filter((c) => (!k.value || c.kind === k.value) && (!cat.value || (c.category ?? '').toLowerCase().includes(cat.value.toLowerCase())))
466
+ .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
467
  const categories = [...new Set(manifest.components.map((c) => c.category).filter(Boolean))].sort();
102
- return text({ count: list.length, categories, components: list });
468
+ const out = { version: refVersion, count: list.length, categories, components: list };
469
+ const note = versionNote(); if (note) out.versionNote = note;
470
+ return text(out);
103
471
  },
104
472
  },
105
473
  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.',
474
+ 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
475
  inputSchema: { type: 'object', properties: { group: { type: 'string' }, query: { type: 'string' } } },
108
- run({ group, query }) {
476
+ run(args) {
477
+ const g = strArg(args, 'group'); const q = strArg(args, 'query');
478
+ const e = firstError(g, q); if (e) return fail(e.err);
109
479
  const groups = manifest.tokens.groups;
110
- if (group && !groups[group]) return fail(`Unknown token group "${group}". Groups: ${Object.keys(groups).join(', ')}`);
480
+ if (g.value && !Object.hasOwn(groups, g.value)) return fail(`Unknown token group "${g.value}". Groups: ${Object.keys(groups).join(', ')}`);
481
+ const needle = q.value?.toLowerCase();
111
482
  const out = {};
112
- for (const [g, toks] of Object.entries(groups)) {
113
- if (group && g !== group) continue;
114
- const sel = Object.entries(toks).filter(([k, v]) => !query || k.includes(String(query).toLowerCase()) || String(v).toLowerCase().includes(String(query).toLowerCase()));
115
- if (sel.length) out[g] = Object.fromEntries(sel);
483
+ for (const [name, toks] of Object.entries(groups)) {
484
+ if (g.value && name !== g.value) continue;
485
+ const sel = Object.entries(toks).filter(([k, v]) => !needle || k.includes(needle) || String(v).toLowerCase().includes(needle));
486
+ if (sel.length) out[name] = Object.fromEntries(sel);
116
487
  }
117
488
  return text(Object.keys(out).length ? out : `No tokens match. Groups: ${Object.keys(groups).join(', ')}`);
118
489
  },
119
490
  },
120
491
  check_usage: {
121
- description: 'Validate props you are about to use against the contract: reports missing required props, values outside an enum, and props that do not exist. Run it on each component you write.',
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({ component, props = {} }) {
124
- const c = byName.get(String(component).toLowerCase());
125
- if (!c) return fail(`No component named "${component}". Did you mean: ${closest(String(component)).join(', ') || '(none close)'}?`);
492
+ 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.',
493
+ 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'] },
494
+ run(args) {
495
+ const n = strArg(args, 'component', { required: true }); if (n.err) return fail(n.err);
496
+ const props = args.props === undefined || args.props === null ? {} : args.props;
497
+ if (typeof props !== 'object' || Array.isArray(props)) return fail('"props" must be an object of prop name -> value.');
498
+ if (Object.keys(props).length > 200) return fail('"props" has too many entries (max 200).');
499
+ const c = byName.get(n.value.toLowerCase());
500
+ if (!c) return fail(`No component named "${n.value}". Did you mean: ${closest(n.value).join(', ') || '(none close)'}?`);
126
501
  const known = new Map(c.props.map((p) => [p.name, p]));
127
502
  const errors = [];
128
503
  const warnings = [];
129
- for (const r of c.contract.requiredProps) if (!(r in props)) errors.push(`Missing required prop "${r}".`);
504
+ if (inInstalled(c) === false) errors.push(`${c.name} was added in ${c.since}; the project has ${projectVersion}. It does not exist there.`);
505
+ for (const r of c.contract.requiredProps) if (!Object.hasOwn(props, r)) errors.push(`Missing required prop "${r}".`);
130
506
  for (const [k, v] of Object.entries(props)) {
131
507
  const p = known.get(k);
132
508
  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' && !p.values.includes(v)) errors.push(`"${k}" = "${v}" is not allowed. Use one of: ${p.values.join(', ')}.`);
509
+ if (p.values && (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') && !p.values.includes(v)) {
510
+ errors.push(`"${k}" = ${JSON.stringify(v)} is not allowed. Use one of: ${p.values.map((x) => JSON.stringify(x)).join(', ')}.`);
511
+ }
512
+ if (p.type === 'IconComponent' && typeof v === 'string') { const e = iconProblem(k, v); if (e) errors.push(e); }
134
513
  }
135
514
  if (c.contract.requiresAccessibleName && !errors.length) warnings.push('This control has no visible text of its own: keep its accessible-name prop set.');
136
- return text({ component: c.name, ok: errors.length === 0, errors, warnings });
515
+ const out = { component: c.name, ok: errors.length === 0, errors, warnings };
516
+ const note = versionNote(); if (note) out.versionNote = note;
517
+ return text(out);
518
+ },
519
+ },
520
+ check_compatibility: {
521
+ 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).',
522
+ inputSchema: { type: 'object', properties: { installedVersion: { type: 'string', description: 'e.g. 0.7.0' } } },
523
+ run(args) {
524
+ const v = verArg(args, 'installedVersion'); if (v.err) return fail(v.err.err);
525
+ const installed = v.value ? v.value.match(SEMVER)[0] : projectVersion;
526
+ const source = v.value ? 'provided' : projectVersion ? 'detected' : 'unknown';
527
+ if (!installed) {
528
+ 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.' });
529
+ }
530
+ const missing = manifest.components.filter((c) => c.since && cmp(c.since, installed) > 0).map((c) => ({ name: c.name, since: c.since }));
531
+ const newer = releases().filter((r) => cmp(r.version, installed) > 0).map((r) => r.version);
532
+ const relation = cmp(installed, refVersion) === 0 ? 'same' : cmp(installed, refVersion) < 0 ? 'project is older' : 'project is newer than this reference';
533
+ const advice = relation === 'same' ? 'The reference matches the installed version.'
534
+ : 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.`
535
+ : '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.';
536
+ return text({ reference: refVersion, installed, source, relation, missingInInstalled: missing, releasesAfterInstalled: newer, advice });
537
+ },
538
+ },
539
+ get_changelog: {
540
+ 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).',
541
+ inputSchema: { type: 'object', properties: { version: { type: 'string' }, since: { type: 'string' } } },
542
+ run(args) {
543
+ const ver = verArg(args, 'version'); const since = verArg(args, 'since');
544
+ const e = firstError(ver, since); if (e) return fail(e.err);
545
+ const all = releases();
546
+ if (!all.length) return fail('No release notes are bundled with this install.');
547
+ let pick;
548
+ if (ver.value) {
549
+ pick = all.filter((r) => cmp(r.version, ver.value) === 0 && r.version.startsWith(ver.value.match(SEMVER)[0]));
550
+ if (!pick.length) return fail(`No release notes for ${ver.value}. Releases: ${all.map((r) => r.version).join(', ')}`);
551
+ } else {
552
+ const from = since.value ?? (projectVersion && cmp(projectVersion, refVersion) < 0 ? projectVersion : null);
553
+ pick = from ? all.filter((r) => cmp(r.version, from) > 0 && cmp(r.version, refVersion) <= 0) : all.slice(0, 1);
554
+ if (!pick.length) return text(`No releases after ${from}. This reference describes ${refVersion}.`);
555
+ }
556
+ return text(pick.map((r) => `## ${r.version}\n\n${r.body}`).join('\n\n'));
557
+ },
558
+ },
559
+ search_icons: {
560
+ description: 'Find real icon names (Tabler set, exported from @sakaniui/react/icons) by meaning, e.g. "delete", "settings", "arrow right". Never guess an icon name: only names returned here exist.',
561
+ inputSchema: { type: 'object', properties: { query: { type: 'string' }, limit: { type: 'number' } }, required: ['query'] },
562
+ run(args) {
563
+ const q = strArg(args, 'query', { required: true }); if (q.err) return fail(q.err);
564
+ if (!iconIndex.length) return fail('The icon index is not available in this install.');
565
+ const limit = Number.isInteger(args.limit) ? Math.min(Math.max(args.limit, 1), 30) : 12;
566
+ const hits = searchIcons(q.value, limit).map((i) => { const o = { name: i.name }; if (i.filled) o.filled = i.filled; if (i.category) o.category = i.category; if (i.tags) o.tags = i.tags.slice(0, 5); return o; });
567
+ if (!hits.length) return text(`No icon matches "${q.value}". Try a simpler word (e.g. "delete" rather than "remove item").`);
568
+ return text({ import: "import { IconName } from '@sakaniui/react/icons';", usage: 'icon={IconName} (the component, not <IconName />)', icons: hits });
569
+ },
570
+ },
571
+ check_code: {
572
+ description: 'Check a JSX/TSX snippet you wrote: imports from @sakaniui/react (and /blocks, /icons) must exist, every SakaniUI element must have its required props, prop values must be allowed, and icon props must be real icon components passed as components. The code is only read, never run. Run it on every snippet before you answer.',
573
+ inputSchema: { type: 'object', properties: { code: { type: 'string', description: 'The snippet, including its import lines.' } }, required: ['code'] },
574
+ run(args) {
575
+ const code = args.code;
576
+ if (typeof code !== 'string' || !code.trim()) return fail('"code" must be a non-empty string.');
577
+ if (code.length > 50000) return fail('"code" is too long (max 50,000 characters).');
578
+ return text(checkCode(code));
137
579
  },
138
580
  },
139
581
  get_guide: {
140
582
  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
583
  inputSchema: { type: 'object', properties: { section: { type: 'string' } } },
142
- run({ section }) {
584
+ run(args) {
585
+ const s0 = strArg(args, 'section'); if (s0.err) return fail(s0.err);
143
586
  if (!guide) return fail('AGENTS.md is not available in this install.');
144
- const parts = guide.split(/^(?=## )/m);
145
- const heads = parts.filter((p) => p.startsWith('## ')).map((p) => p.split('\n')[0].replace(/^## /, ''));
146
- if (!section) return text({ sections: heads, hint: 'Call again with a number (e.g. "5") or heading text.' });
147
- const s = String(section).toLowerCase().trim();
148
- const hit = parts.filter((p) => p.startsWith('## ')).filter((p) => { const h = p.split('\n')[0].replace(/^## /, '').toLowerCase(); return h === s || h.startsWith(s + '.') || h.startsWith(s + ' ') || h.includes(s); });
149
- return hit.length ? text(hit.join('\n')) : fail(`No section "${section}". Sections: ${heads.join(' | ')}`);
587
+ const parts = guide.split(/^(?=## )/m).filter((p) => p.startsWith('## '));
588
+ const heads = parts.map((p) => p.split('\n')[0].replace(/^## /, ''));
589
+ if (!s0.value) return text({ version: refVersion, sections: heads, hint: 'Call again with a number (e.g. "5") or heading text.' });
590
+ const s = s0.value.toLowerCase().trim();
591
+ 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); });
592
+ return hit.length ? text(hit.join('\n')) : fail(`No section "${s0.value}". Sections: ${heads.join(' | ')}`);
150
593
  },
151
594
  },
152
595
  };
@@ -155,28 +598,33 @@ const tools = {
155
598
  const send = (msg) => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...msg }) + '\n');
156
599
 
157
600
  function handle(msg) {
601
+ if (!msg || typeof msg !== 'object' || Array.isArray(msg)) return send({ id: null, error: { code: -32600, message: 'Invalid request' } });
158
602
  const { id, method, params } = msg;
159
603
  const isRequest = id !== undefined && id !== null;
160
604
  try {
161
605
  switch (method) {
162
- case 'initialize':
606
+ case 'initialize': {
607
+ const note = versionNote();
163
608
  return send({ id, result: {
164
609
  protocolVersion: params?.protocolVersion || '2025-06-18',
165
610
  capabilities: { tools: {} },
166
- serverInfo: { name: 'sakaniui', version: manifest.version },
167
- instructions: `SakaniUI ${manifest.version}. 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.`,
611
+ serverInfo: { name: 'sakaniui', version: refVersion },
612
+ 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
613
  } });
614
+ }
169
615
  case 'ping':
170
616
  return send({ id, result: {} });
171
617
  case 'tools/list':
172
- return send({ id, result: { tools: Object.entries(tools).map(([name, t]) => ({ name, description: t.description, inputSchema: t.inputSchema })) } });
618
+ return send({ id, result: { tools: Object.entries(tools).map(([name, t]) => ({ name, description: t.description, inputSchema: t.inputSchema, annotations: READ_ONLY })) } });
173
619
  case 'tools/call': {
174
- const t = tools[params?.name];
175
- if (!t) return send({ id, error: { code: -32602, message: `Unknown tool: ${params?.name}` } });
176
- return send({ id, result: t.run(params.arguments || {}) });
620
+ const t = typeof params?.name === 'string' && Object.hasOwn(tools, params.name) ? tools[params.name] : null;
621
+ if (!t) return send({ id, error: { code: -32602, message: `Unknown tool: ${String(params?.name).slice(0, 60)}` } });
622
+ const args = params.arguments;
623
+ if (args !== undefined && args !== null && (typeof args !== 'object' || Array.isArray(args))) return send({ id, result: fail('Tool arguments must be an object.') });
624
+ return send({ id, result: t.run(args || {}) });
177
625
  }
178
626
  default:
179
- if (isRequest) send({ id, error: { code: -32601, message: `Method not found: ${method}` } });
627
+ if (isRequest) send({ id, error: { code: -32601, message: `Method not found: ${String(method).slice(0, 60)}` } });
180
628
  }
181
629
  } catch (e) {
182
630
  if (isRequest) send({ id, error: { code: -32603, message: String(e?.message || e) } });