onsystem 0.4.0 → 0.6.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.
@@ -0,0 +1,1396 @@
1
+ import { Q as formatPx, R as CONFIG_FILES, W as LEGACY_CONFIG_FILES, Y as loadConfig, _ as VERSION, _t as relativePath, d as searchComponents, dt as closest, ft as plural, g as NAME, ht as isInside, j as TOKEN_CATEGORIES, m as uncheckedNotice, mt as unique, n as DesignSystemHost, pt as truncate, u as buildSearchIndex } from "./design-system-A3pRX3Ka.js";
2
+ import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-CHvUwgz9.js";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import * as z from "zod";
7
+ import fsp from "node:fs/promises";
8
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
9
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
10
+ //#region src/server/render.ts
11
+ /**
12
+ * Tool results are read by models, so they are rendered as compact Markdown:
13
+ * fewer tokens than JSON, and the same information. The JSON is still sent
14
+ * as structured content.
15
+ */
16
+ function firstSentence(text, max = 140) {
17
+ if (!text) return "";
18
+ const sentence = /^[\s\S]*?[.!?](?=\s+[A-Z`"(]|\s*$)/.exec(text.trim())?.[0] ?? text.trim();
19
+ return truncate(sentence.replace(/\s+/g, " "), max);
20
+ }
21
+ function importStatement(component) {
22
+ return `import { ${component.exportName.split(".")[0] ?? component.name} } from "${component.importPath}"`;
23
+ }
24
+ function variantSummary(component) {
25
+ return component.variants.map((v) => `${v.name}: ${v.values.join(" | ")}${v.default ? ` (default ${v.default})` : ""}`);
26
+ }
27
+ /** `project`: the folder of a workspace root's project the list is for. */
28
+ function renderComponentList(ds, project) {
29
+ const roots = ds.roots();
30
+ if (!roots.length) return `No components found${project ? ` in ${project}` : ""}. Check the "components" globs in the config (root: ${ds.root}).`;
31
+ const parts = ds.components.length - roots.length;
32
+ const lines = [`${plural(roots.length, "component")}${parts ? ` (+${plural(parts, "part")})` : ""}${project ? ` in ${project}` : ""}. Call get_component before using one.`, ""];
33
+ const own = roots.filter((c) => !c.library);
34
+ for (const component of own) lines.push(...listItem(component));
35
+ for (const library of new Set(roots.flatMap((c) => c.library ? [c.library] : []))) {
36
+ if (own.length) lines.push("");
37
+ lines.push(`From ${library} (import { Name } from "${library}"):`, "");
38
+ for (const component of roots.filter((c) => c.library === library)) lines.push(...listItem(component, "", false));
39
+ }
40
+ return lines.join("\n");
41
+ }
42
+ function listItem(component, note = "", withImport = true) {
43
+ const element = component.element ? ` <${component.element}>` : "";
44
+ const lines = [`${component.name}${element}${note} — ${firstSentence(component.description) || "No description."}`];
45
+ for (const variant of variantSummary(component)) lines.push(` ${variant}`);
46
+ if (component.subcomponents.length) lines.push(` parts: ${component.subcomponents.join(", ")}`);
47
+ if (withImport) lines.push(` ${importStatement(component)}`);
48
+ return lines;
49
+ }
50
+ /** `apps/web`, `apps/web and packages/ui`, `a, b, c and 4 more`. */
51
+ function listOf$1(items, max = 3) {
52
+ if (items.length <= 1) return items[0] ?? "";
53
+ if (items.length <= max) return `${items.slice(0, -1).join(", ")} and ${items.at(-1) ?? ""}`;
54
+ return `${items.slice(0, max).join(", ")} and ${items.length - max} more`;
55
+ }
56
+ const PICK_PROJECT = "This is a monorepo root: pass `path`, the file you are editing, to get_component, get_tokens and check_ui so they answer for its project.";
57
+ /** Every project's components at a workspace root, grouped by the package they come from. */
58
+ function renderCatalogList(catalog) {
59
+ const roots = catalog.roots();
60
+ if (!roots.length) return `No components found in any project of this monorepo (root: ${catalog.root}). Run \`onsystem inspect\` there to see what was looked at.`;
61
+ const byPackage = /* @__PURE__ */ new Map();
62
+ for (const entry of roots) byPackage.set(entry.package, [...byPackage.get(entry.package) ?? [], entry]);
63
+ const parts = catalog.entries.length - roots.length;
64
+ const lines = [`${plural(roots.length, "component")}${parts ? ` (+${plural(parts, "part")})` : ""} from ${plural(byPackage.size, "package")}, used by ${plural(catalog.projects.length, "project")}. ${PICK_PROJECT}`];
65
+ for (const [pkg, entries] of byPackage) {
66
+ const projects = [...new Set(entries.flatMap((e) => e.projects))];
67
+ lines.push("", `## ${pkg} (used by ${listOf$1(projects)})`);
68
+ for (const entry of entries) {
69
+ const others = entry.ambiguous ? catalog.roots().filter((e) => e !== entry && e.component.name === entry.component.name) : [];
70
+ const note = others.length ? ` (also in ${[...new Set(others.map((e) => e.package))].join(", ")})` : "";
71
+ lines.push(...listItem(entry.component, note));
72
+ }
73
+ }
74
+ return lines.join("\n");
75
+ }
76
+ /** Several components under one name at a workspace root, and how to pick one. */
77
+ function renderAmbiguous(name, entries) {
78
+ const lines = [`"${name}" is the name of ${entries.length} components in this monorepo:`];
79
+ for (const entry of entries) lines.push(`- ${entry.package}: ${entry.component.source.file}, used by ${listOf$1(entry.projects)} (${importStatement(entry.component)})`);
80
+ const dirs = [...new Set(entries.flatMap((e) => e.projects))];
81
+ lines.push("", `Pass \`path\`: the file you are editing, or its project folder (${dirs.join(", ")}), to get the one that project uses.`);
82
+ return lines.join("\n");
83
+ }
84
+ /** Inherited attributes worth naming, per element; the rest are summarised. */
85
+ const NOTABLE_INHERITED = {
86
+ button: [
87
+ "onClick",
88
+ "type",
89
+ "disabled",
90
+ "form"
91
+ ],
92
+ input: [
93
+ "type",
94
+ "value",
95
+ "defaultValue",
96
+ "onChange",
97
+ "placeholder",
98
+ "disabled",
99
+ "name",
100
+ "required"
101
+ ],
102
+ textarea: [
103
+ "value",
104
+ "defaultValue",
105
+ "onChange",
106
+ "placeholder",
107
+ "disabled",
108
+ "name",
109
+ "rows"
110
+ ],
111
+ select: [
112
+ "value",
113
+ "defaultValue",
114
+ "onChange",
115
+ "disabled",
116
+ "name"
117
+ ],
118
+ a: [
119
+ "href",
120
+ "target",
121
+ "rel"
122
+ ],
123
+ img: [
124
+ "src",
125
+ "alt",
126
+ "width",
127
+ "height"
128
+ ],
129
+ label: ["htmlFor"],
130
+ default: [
131
+ "onClick",
132
+ "id",
133
+ "role",
134
+ "children"
135
+ ]
136
+ };
137
+ /** What callers of a React Aria component most often need, from its inherited props. */
138
+ const REACT_ARIA_NOTABLE = [
139
+ "onPress",
140
+ "onChange",
141
+ "onAction",
142
+ "onSelectionChange",
143
+ "onOpenChange",
144
+ "value",
145
+ "defaultValue",
146
+ "isDisabled",
147
+ "isRequired",
148
+ "isInvalid",
149
+ "isReadOnly",
150
+ "autoFocus"
151
+ ];
152
+ function renderProp(prop) {
153
+ const optional = prop.required ? "" : "?";
154
+ const fallback = prop.default !== void 0 ? ` = ${prop.default}` : "";
155
+ const deprecated = prop.deprecated ? ` DEPRECATED${prop.deprecated === true ? "" : `: ${prop.deprecated}`}.` : "";
156
+ const description = prop.description ? ` — ${prop.description.replace(/\s*\n\s*/g, " ")}` : "";
157
+ return `- ${prop.name}${optional}: ${prop.type}${fallback}${description}${deprecated}`;
158
+ }
159
+ /** `origin`: at a workspace root, the package the component comes from and who uses it. */
160
+ function renderComponent(ds, component, origin) {
161
+ const lines = [`# ${component.name}`];
162
+ if (component.deprecated) lines.push(`DEPRECATED${component.deprecated === true ? "" : `: ${component.deprecated}`}`);
163
+ if (component.description) lines.push(component.description);
164
+ lines.push("", importStatement(component));
165
+ const facts = [
166
+ component.element ? `Renders <${component.element}>` : void 0,
167
+ component.parent ? `Part of ${component.parent}` : void 0,
168
+ component.aliases.length ? `Also available as ${component.aliases.join(", ")}` : void 0,
169
+ `${component.source.file}:${component.source.line}`,
170
+ component.docs ? `docs: ${component.docs.file}` : void 0
171
+ ].filter(Boolean);
172
+ lines.push(facts.join(" · "));
173
+ if (origin) lines.push(`From ${origin.package}, used by ${listOf$1(origin.projects)}.`);
174
+ lines.push("", "## Props");
175
+ if (!component.props.length && !component.inherits.length) lines.push("No props.");
176
+ for (const prop of component.props) lines.push(renderProp(prop));
177
+ for (const inherited of component.inherits) {
178
+ const names = ds.model.propSets[inherited.set] ?? [];
179
+ const notable = (names.some((n) => n === "onPress" || n === "onFocusChange") ? REACT_ARIA_NOTABLE : NOTABLE_INHERITED[component.element ?? ""] ?? NOTABLE_INHERITED.default ?? []).filter((n) => names.includes(n) && !inherited.deprecated?.includes(n));
180
+ lines.push(`- …plus ${inherited.count} props from ${inherited.from}` + (notable.length ? ` (${notable.join(", ")}, aria-*, data-*, …)` : ""));
181
+ }
182
+ if (component.openProps) lines.push("- Part of the props type could not be resolved; extra props may be accepted.");
183
+ if (component.variants.length) {
184
+ lines.push("", "## Variants");
185
+ for (const variant of component.variants) {
186
+ if (!Object.values(variant.classes).some(Boolean)) {
187
+ lines.push(`${variant.name}: ${variant.values.join(" | ")}${variant.default ? ` (default "${variant.default}")` : ""}`);
188
+ continue;
189
+ }
190
+ lines.push(`${variant.name}${variant.default ? ` (default "${variant.default}")` : ""}`);
191
+ const width = Math.max(...variant.values.map((v) => v.length));
192
+ for (const value of variant.values) lines.push(` ${value.padEnd(width)} ${variant.classes[value] ?? ""}`.trimEnd());
193
+ }
194
+ for (const compound of component.compoundVariants) {
195
+ const when = Object.entries(compound.when).map(([k, v]) => `${k}=${Array.isArray(v) ? v.join("|") : v}`).join(" & ");
196
+ lines.push(` when ${when}: ${compound.classes}`);
197
+ }
198
+ }
199
+ if (component.subcomponents.length) {
200
+ lines.push("", "## Parts");
201
+ for (const name of component.subcomponents) {
202
+ const part = ds.getComponent(name);
203
+ lines.push(`- ${name}${part?.description ? ` — ${firstSentence(part.description)}` : ""}`);
204
+ }
205
+ }
206
+ const tokens = ds.relatedTokens(component);
207
+ if (tokens.length) {
208
+ lines.push("", "## Design tokens used");
209
+ lines.push(tokens.map((t) => `${t.name} (${t.usage[0] ?? t.value})`).join(", "));
210
+ }
211
+ if (component.docs?.sections.length) {
212
+ lines.push("", `## Docs (${component.docs.file})`);
213
+ for (const section of component.docs.sections) {
214
+ if (/^examples?$/i.test(section.heading) && !section.body) continue;
215
+ lines.push(`### ${section.heading}`);
216
+ if (section.body) lines.push(section.body);
217
+ }
218
+ }
219
+ if (component.examples.length) {
220
+ lines.push("", "## Examples");
221
+ for (const example of component.examples) {
222
+ if (example.title) lines.push(`### ${example.title}`);
223
+ lines.push(`\`\`\`${example.lang}`, example.code, "```");
224
+ }
225
+ }
226
+ return lines.join("\n");
227
+ }
228
+ function renderSearch(query, hits) {
229
+ if (!hits.length) return `No components match "${query}". Try other words, or list_components to browse.`;
230
+ const lines = [`Components for "${query}", best first:`, ""];
231
+ hits.forEach((hit, i) => {
232
+ const c = hit.component;
233
+ const parent = c.parent ? ` (part of ${c.parent})` : "";
234
+ lines.push(`${i + 1}. ${c.name}${parent} — ${firstSentence(c.description) || "No description."}`);
235
+ lines.push(` score ${hit.score} · matched: ${hit.matched.join(", ")} · ${importStatement(c)}`);
236
+ });
237
+ lines.push("", "Call get_component for props, variants and examples.");
238
+ return lines.join("\n");
239
+ }
240
+ /** Search hits at a workspace root, each with the package it comes from. */
241
+ function renderCatalogSearch(query, hits) {
242
+ if (!hits.length) return `No components match "${query}" in any project. Try other words, or list_components to browse.`;
243
+ const lines = [`Components for "${query}" across the monorepo's projects, best first:`, ""];
244
+ hits.forEach((hit, i) => {
245
+ const c = hit.component;
246
+ const parent = c.parent ? ` (part of ${c.parent})` : "";
247
+ lines.push(`${i + 1}. ${c.name}${parent} from ${hit.entry.package} — ${firstSentence(c.description) || "No description."}`);
248
+ lines.push(` score ${hit.score} · matched: ${hit.matched.join(", ")} · used by ${listOf$1(hit.entry.projects)} · ${importStatement(c)}`);
249
+ });
250
+ lines.push("", `Call get_component for props, variants and examples. ${PICK_PROJECT}`);
251
+ return lines.join("\n");
252
+ }
253
+ /** Tokens at a workspace root, grouped by category, each with the package that defines it. */
254
+ function renderCatalogTokens(catalog, tokens, filter = {}) {
255
+ if (!catalog.tokens.length) return `No design tokens found in any project of this monorepo (root: ${catalog.root}). Set "tokens" in a project's config: https://onsystem.vercel.app/docs/configuration`;
256
+ if (!tokens.length) {
257
+ const what = [filter.category, filter.query && `"${filter.query}"`].filter(Boolean).join(" ");
258
+ return `No tokens match${what ? ` ${what}` : ""} in any project; call get_tokens without filters to see them.`;
259
+ }
260
+ const groups = /* @__PURE__ */ new Map();
261
+ for (const t of tokens) groups.set(t.token.category, [...groups.get(t.token.category) ?? [], t]);
262
+ const lines = [PICK_PROJECT, ""];
263
+ for (const [category, list] of groups) {
264
+ lines.push(`## ${category} (${list.length})`);
265
+ for (const { token, package: pkg } of list) {
266
+ const usage = token.usage.length ? ` → ${token.usage.join(", ")}` : "";
267
+ lines.push(`- ${token.name}: ${token.value}${usage} (${pkg})`);
268
+ }
269
+ lines.push("");
270
+ }
271
+ return lines.join("\n").trimEnd();
272
+ }
273
+ function renderTokens(ds, tokens, filter = {}) {
274
+ if (!ds.tokens.length) return `No design tokens found in this project (root: ${ds.root}): no stylesheet or *.tokens.json defines any. Set "tokens" in the config: https://onsystem.vercel.app/docs/configuration`;
275
+ if (!tokens.length) {
276
+ const counts = /* @__PURE__ */ new Map();
277
+ for (const token of ds.tokens) counts.set(token.category, (counts.get(token.category) ?? 0) + 1);
278
+ const categories = [...counts].map(([category, n]) => `${category} (${n})`).join(", ");
279
+ const what = [filter.category, filter.query && `"${filter.query}"`].filter(Boolean).join(" ");
280
+ return `No tokens match${what ? ` ${what}` : ""}. The design system has ${categories}; call get_tokens without filters to see them.`;
281
+ }
282
+ const groups = /* @__PURE__ */ new Map();
283
+ for (const token of tokens) groups.set(token.category, [...groups.get(token.category) ?? [], token]);
284
+ const lines = [];
285
+ for (const [category, list] of groups) {
286
+ lines.push(`## ${category} (${list.length})`);
287
+ for (const token of list) {
288
+ const px = token.category === "spacing" || token.category === "radius" ? ds.tokenIndex.toPx(token.value) : void 0;
289
+ const value = px !== void 0 && !/^-?[\d.]+px$/.test(token.value) ? `${token.value} = ${formatPx(px)}` : token.value;
290
+ const modes = token.modes ? ` (${Object.entries(token.modes).map(([mode, v]) => `${mode}: ${v}`).join(", ")})` : "";
291
+ const usage = token.usage.length ? ` → ${token.usage.join(", ")}` : "";
292
+ const description = token.description ? ` — ${token.description}` : "";
293
+ const deprecated = token.deprecated ? " DEPRECATED" : "";
294
+ lines.push(`- ${token.name}: ${value}${modes}${usage}${description}${deprecated}`);
295
+ }
296
+ lines.push("");
297
+ }
298
+ return lines.join("\n").trimEnd();
299
+ }
300
+ function capDiagnostics(result, limit) {
301
+ const ordered = [...result.diagnostics.filter((d) => d.severity === "error"), ...result.diagnostics.filter((d) => d.severity !== "error")];
302
+ const byRule = {};
303
+ for (const d of result.diagnostics) {
304
+ const counts = byRule[d.ruleId] ??= {
305
+ errors: 0,
306
+ warnings: 0
307
+ };
308
+ if (d.severity === "error") counts.errors++;
309
+ else counts.warnings++;
310
+ }
311
+ const shown = ordered.slice(0, limit);
312
+ return {
313
+ shown,
314
+ omitted: ordered.length - shown.length,
315
+ byRule
316
+ };
317
+ }
318
+ function renderCheck(result, capped = capDiagnostics(result, Infinity)) {
319
+ if (result.skipped) return `${result.file}: not checked. ${result.skipped}`;
320
+ if (!result.diagnostics.length) return `${result.file}: no design-system problems found.`;
321
+ const lines = [`${result.file}: ${plural(result.errorCount, "error")}, ${plural(result.warningCount, "warning")}`, ""];
322
+ for (const d of capped.shown) lines.push(`${d.line}:${d.column} ${d.severity} [${d.ruleId}] ${d.message}`);
323
+ if (capped.omitted) {
324
+ const rules = Object.entries(capped.byRule).sort(([, a], [, b]) => b.errors + b.warnings - (a.errors + a.warnings)).map(([rule, c]) => `${rule} ${(c.errors + c.warnings).toLocaleString("en-US")}`).join(", ");
325
+ lines.push("", `Showing ${capped.shown.length} of ${result.diagnostics.length.toLocaleString("en-US")} (errors first). By rule: ${rules}.`, `${capped.omitted.toLocaleString("en-US")} more not shown: fix these, then run check_ui again, or pass a higher \`limit\`.`);
326
+ }
327
+ lines.push("", result.errorCount ? "Fix every error, then run check_ui again." : "Only warnings: fix them if the design system has an equivalent.");
328
+ return lines.join("\n");
329
+ }
330
+ //#endregion
331
+ //#region src/server/index.ts
332
+ const INSTRUCTIONS = `This server describes the project's design system: its React components (props, variants, parts), design tokens and usage docs, and lints UI code against it.
333
+ When writing or changing UI in this project:
334
+ 1. Find components with search_components (by intent) or list_components.
335
+ 2. Read get_component before using a component: use only the props and variant values it lists.
336
+ 3. Style with tokens from get_tokens, never with hex/rgb colors, arbitrary px values or Tailwind's default palette.
337
+ 4. Run check_ui on every snippet or file you write and fix all errors before you finish.
338
+ In a monorepo served from its root, pass \`path\` (the file you are editing) to list_components, get_component, search_components and get_tokens, and \`path\` or \`filename\` to check_ui, so each answer comes from that file's own project.`;
339
+ const READ_ONLY = {
340
+ readOnlyHint: true,
341
+ destructiveHint: false,
342
+ idempotentHint: true,
343
+ openWorldHint: false
344
+ };
345
+ /** What a client may cache: what never changes while the server runs, and what does. */
346
+ const STATIC = {
347
+ ttlMs: 36e5,
348
+ cacheScope: "private"
349
+ };
350
+ const FRESH = {
351
+ ttlMs: 0,
352
+ cacheScope: "private"
353
+ };
354
+ /** Input limits, so one oversized request cannot stall the server. */
355
+ const MAX_NAME = 256;
356
+ const MAX_QUERY = 1e3;
357
+ const MAX_PATH = 4096;
358
+ /** Larger than any hand-written source file; generated bundles are not UI to check. */
359
+ const MAX_CODE = 1e6;
360
+ /** Diagnostics check_ui returns by default: a 3,000-line file can have thousands, more than a context holds. */
361
+ const DEFAULT_LIMIT = 50;
362
+ const MAX_LIMIT = 1e3;
363
+ /**
364
+ * An integer, without the ±9007199254740991 bounds zod adds to the JSON Schema,
365
+ * which every client would otherwise read in each tool definition.
366
+ */
367
+ const int = () => z.number().int().meta({
368
+ minimum: void 0,
369
+ maximum: void 0
370
+ });
371
+ /** At a workspace root, where a component comes from. */
372
+ const originShape = {
373
+ package: z.string().optional().describe("At a workspace root: the package its source belongs to, e.g. \"@acme/ui\"."),
374
+ projects: z.array(z.string()).optional().describe("At a workspace root: the folders of the projects that use it.")
375
+ };
376
+ const diagnosticSchema = z.object({
377
+ ruleId: z.string(),
378
+ severity: z.enum(["error", "warning"]),
379
+ message: z.string(),
380
+ line: int(),
381
+ column: int(),
382
+ endLine: int(),
383
+ endColumn: int(),
384
+ source: z.string(),
385
+ suggestion: z.string().optional(),
386
+ fix: z.array(z.object({
387
+ range: z.array(int()).length(2),
388
+ text: z.string()
389
+ })).optional().describe("Edits (0-based offsets into the checked code) that apply the suggestion.")
390
+ });
391
+ const deprecatedSchema = z.union([z.string(), z.literal(true)]).optional();
392
+ const componentSchema = {
393
+ name: z.string().describe("How the component is written in JSX: Button, CardHeader, Tabs.List."),
394
+ aliases: z.array(z.string()),
395
+ description: z.string().optional(),
396
+ deprecated: deprecatedSchema,
397
+ importPath: z.string(),
398
+ exportName: z.string(),
399
+ source: z.object({
400
+ file: z.string(),
401
+ line: int()
402
+ }),
403
+ parent: z.string().optional(),
404
+ subcomponents: z.array(z.string()),
405
+ element: z.string().optional().describe("The native element it renders or wraps."),
406
+ props: z.array(z.object({
407
+ name: z.string(),
408
+ type: z.string(),
409
+ required: z.boolean(),
410
+ default: z.string().optional(),
411
+ description: z.string().optional(),
412
+ values: z.array(z.string()).optional(),
413
+ deprecated: deprecatedSchema,
414
+ kind: z.enum(["prop", "variant"])
415
+ })),
416
+ inherits: z.array(z.object({
417
+ from: z.string(),
418
+ count: int(),
419
+ set: z.string(),
420
+ deprecated: z.array(z.string()).optional()
421
+ })),
422
+ openProps: z.boolean().describe("Part of the props type did not resolve; extra props may be accepted."),
423
+ variants: z.array(z.object({
424
+ name: z.string(),
425
+ values: z.array(z.string()),
426
+ default: z.string().optional(),
427
+ classes: z.record(z.string(), z.string())
428
+ })),
429
+ compoundVariants: z.array(z.object({
430
+ when: z.record(z.string(), z.union([z.string(), z.array(z.string())])),
431
+ classes: z.string()
432
+ })),
433
+ examples: z.array(z.object({
434
+ title: z.string().optional(),
435
+ code: z.string(),
436
+ lang: z.string(),
437
+ source: z.enum(["docs", "jsdoc"])
438
+ })),
439
+ docs: z.object({
440
+ file: z.string(),
441
+ description: z.string().optional(),
442
+ sections: z.array(z.object({
443
+ heading: z.string(),
444
+ body: z.string()
445
+ })),
446
+ frontmatter: z.record(z.string(), z.union([z.string(), z.array(z.string())]))
447
+ }).optional(),
448
+ classNames: z.array(z.string()),
449
+ cssVars: z.array(z.string()),
450
+ import: z.string().describe("The import statement to use."),
451
+ tokens: z.array(z.string()).describe("Names of the design tokens its classes use."),
452
+ ...originShape
453
+ };
454
+ const componentSummarySchema = z.object({
455
+ name: z.string(),
456
+ description: z.string().optional(),
457
+ importPath: z.string(),
458
+ element: z.string().optional(),
459
+ variants: z.record(z.string(), z.array(z.string())),
460
+ subcomponents: z.array(z.string()),
461
+ ...originShape
462
+ });
463
+ const tokenSchema = z.object({
464
+ name: z.string(),
465
+ category: z.enum(TOKEN_CATEGORIES),
466
+ value: z.string(),
467
+ modes: z.record(z.string(), z.string()).optional(),
468
+ cssVar: z.string().optional(),
469
+ tailwind: z.string().optional(),
470
+ usage: z.array(z.string()),
471
+ description: z.string().optional(),
472
+ ...originShape
473
+ });
474
+ /** The optional `path` that picks a project at a workspace root. */
475
+ const projectPath = z.string().max(MAX_PATH).optional().describe("In a monorepo served from its root: the file you are editing, or a project folder, relative to the root, to answer from that project. Without it, every project is listed together. Ignored in a single project. When the server was started outside your project, pass the file as an absolute path: the project is found from it.");
476
+ /**
477
+ * Thrown by `getDesignSystem` or `getProjects` when nothing says which project a request is
478
+ * about: the server was started outside any project, and the request names no absolute path.
479
+ * Tools return its message as an error result; listing resources returns none.
480
+ */
481
+ var ProjectNotFoundError = class extends Error {
482
+ name = "ProjectNotFoundError";
483
+ };
484
+ /** A component's source path relative to `root`, the workspace root the client knows. */
485
+ function fromRoot(ds, component, root) {
486
+ if (!root || root === ds.root) return component;
487
+ const file = relativePath(root, path.resolve(ds.root, component.source.file));
488
+ return {
489
+ ...component,
490
+ source: {
491
+ ...component.source,
492
+ file
493
+ }
494
+ };
495
+ }
496
+ /** `a, b and c`, `a, b, c and 4 more`. */
497
+ function listOf(items, max) {
498
+ if (items.length <= 1) return items[0] ?? "";
499
+ if (items.length <= max) return `${items.slice(0, -1).join(", ")} and ${items.at(-1) ?? ""}`;
500
+ return `${items.slice(0, max).join(", ")} and ${items.length - max} more`;
501
+ }
502
+ function errorResult(text) {
503
+ return {
504
+ isError: true,
505
+ content: [{
506
+ type: "text",
507
+ text
508
+ }]
509
+ };
510
+ }
511
+ function origin(entry) {
512
+ return {
513
+ package: entry.package,
514
+ projects: entry.projects
515
+ };
516
+ }
517
+ function summary(c) {
518
+ return {
519
+ name: c.name,
520
+ ...c.description ? { description: c.description } : {},
521
+ importPath: c.importPath,
522
+ ...c.element ? { element: c.element } : {},
523
+ variants: Object.fromEntries(c.variants.map((v) => [v.name, v.values])),
524
+ subcomponents: c.subcomponents
525
+ };
526
+ }
527
+ /** Builds the MCP server: five tools, two resources and one prompt. Transport-agnostic. */
528
+ function createServer({ getDesignSystem, getProjects }) {
529
+ const server = new McpServer({
530
+ name: NAME,
531
+ title: "onsystem",
532
+ version: VERSION
533
+ }, {
534
+ instructions: INSTRUCTIONS,
535
+ capabilities: { resources: { listChanged: true } },
536
+ cacheHints: {
537
+ "server/discover": STATIC,
538
+ "tools/list": STATIC,
539
+ "prompts/list": STATIC,
540
+ "resources/templates/list": STATIC,
541
+ "resources/list": FRESH,
542
+ "resources/read": FRESH
543
+ }
544
+ });
545
+ /** The design system for `target` at a workspace root, or the merged catalog without one. */
546
+ const scope = async (target) => {
547
+ const set = await getProjects?.(target);
548
+ if (!set) return { ds: await getDesignSystem(target) };
549
+ if (target !== void 0 && target !== "") {
550
+ const project = await set.forPath(target);
551
+ return {
552
+ ds: project.ds,
553
+ project,
554
+ root: set.root
555
+ };
556
+ }
557
+ return {
558
+ set,
559
+ catalog: await set.catalog()
560
+ };
561
+ };
562
+ /** `scope`, with a path outside the workspace as an error result instead of a throw. */
563
+ const scoped = async (target) => {
564
+ try {
565
+ return await scope(target);
566
+ } catch (error) {
567
+ return errorResult(error.message);
568
+ }
569
+ };
570
+ const isResult = (value) => "content" in value;
571
+ /** `scope` without a target, for listings: none yet, when no project is known. */
572
+ const listed = async () => {
573
+ try {
574
+ return await scope();
575
+ } catch (error) {
576
+ if (error instanceof ProjectNotFoundError) return void 0;
577
+ throw error;
578
+ }
579
+ };
580
+ server.registerTool("list_components", {
581
+ title: "List design-system components",
582
+ description: "List every component in the project design system with a one-line description, the native element it renders, its variant values, its parts and its import. Call this before writing UI in this project. At a monorepo root, pass `path` for the project of the file you are editing; without it, every project is listed, each component with its package.",
583
+ inputSchema: z.object({ path: projectPath }),
584
+ outputSchema: z.object({ components: z.array(componentSummarySchema) }),
585
+ annotations: READ_ONLY
586
+ }, async ({ path: target }) => {
587
+ const found = await scoped(target);
588
+ if (isResult(found)) return found;
589
+ if ("catalog" in found) {
590
+ const { catalog } = found;
591
+ return {
592
+ content: [{
593
+ type: "text",
594
+ text: renderCatalogList(catalog)
595
+ }],
596
+ structuredContent: { components: catalog.roots().map((entry) => ({
597
+ ...summary(entry.component),
598
+ ...origin(entry)
599
+ })) }
600
+ };
601
+ }
602
+ const { ds } = found;
603
+ return {
604
+ content: [{
605
+ type: "text",
606
+ text: renderComponentList(ds, found.project?.dir)
607
+ }],
608
+ structuredContent: { components: ds.roots().map(summary) }
609
+ };
610
+ });
611
+ server.registerTool("get_component", {
612
+ title: "Get a component contract",
613
+ description: "Get the full contract of one component: import, props with types and defaults, cva variants and the classes each applies, parts (sub-components), design tokens it uses, usage guidelines and examples from its docs. Call it before using a component. Accepts \"Button\", \"CardHeader\" or \"Card.Header\". At a monorepo root, pass `path` (the file you are editing) to get the component its project uses.",
614
+ inputSchema: z.object({
615
+ name: z.string().min(1).max(MAX_NAME).describe("Component name, e.g. \"Button\" or \"CardHeader\"."),
616
+ path: projectPath
617
+ }),
618
+ outputSchema: z.object(componentSchema),
619
+ annotations: READ_ONLY
620
+ }, async ({ name, path: target }) => {
621
+ const found = await scoped(target);
622
+ if (isResult(found)) return found;
623
+ if ("catalog" in found) {
624
+ const { catalog } = found;
625
+ const matches = catalog.find(name);
626
+ const [entry] = matches;
627
+ if (!entry) {
628
+ const guess = catalog.suggest(name);
629
+ const names = [...new Set(catalog.roots().map((e) => e.component.name))];
630
+ return errorResult(`No component named "${name}" in any project.` + (guess ? ` Did you mean "${guess}"?` : "") + ` Available: ${names.join(", ")}.`);
631
+ }
632
+ if (matches.length > 1) return errorResult(renderAmbiguous(name, matches));
633
+ return {
634
+ content: [{
635
+ type: "text",
636
+ text: renderComponent(entry.ds, entry.component, entry)
637
+ }],
638
+ structuredContent: {
639
+ ...entry.component,
640
+ import: importStatement(entry.component),
641
+ tokens: entry.ds.relatedTokens(entry.component).map((t) => t.name),
642
+ ...origin(entry)
643
+ }
644
+ };
645
+ }
646
+ const { ds } = found;
647
+ const component = ds.getComponent(name);
648
+ if (!component) {
649
+ const guess = ds.suggestComponent(name);
650
+ return {
651
+ isError: true,
652
+ content: [{
653
+ type: "text",
654
+ text: `No component named "${name}".` + (guess ? ` Did you mean "${guess}"?` : "") + ` Available: ${ds.roots().map((c) => c.name).join(", ")}.`
655
+ }]
656
+ };
657
+ }
658
+ const shown = fromRoot(ds, component, found.root);
659
+ return {
660
+ content: [{
661
+ type: "text",
662
+ text: renderComponent(ds, shown)
663
+ }],
664
+ structuredContent: {
665
+ ...shown,
666
+ import: importStatement(component),
667
+ tokens: ds.relatedTokens(component).map((t) => t.name)
668
+ }
669
+ };
670
+ });
671
+ server.registerTool("search_components", {
672
+ title: "Search components by intent",
673
+ description: "Find components by what you want to build, e.g. \"confirm a destructive action\", \"status label\", \"text field with validation\". Ranks names, descriptions, docs, props and variant values with BM25 (local, no API key). Use it when you do not know which component fits.",
674
+ inputSchema: z.object({
675
+ query: z.string().min(1).max(MAX_QUERY).describe("What the UI should do, in plain words."),
676
+ limit: z.number().int().min(1).max(20).default(5).describe("Maximum results."),
677
+ path: projectPath
678
+ }),
679
+ outputSchema: z.object({ results: z.array(z.object({
680
+ name: z.string(),
681
+ parent: z.string().optional(),
682
+ description: z.string().optional(),
683
+ importPath: z.string(),
684
+ score: z.number(),
685
+ matched: z.array(z.string()),
686
+ ...originShape
687
+ })) }),
688
+ annotations: READ_ONLY
689
+ }, async ({ query, limit, path: target }) => {
690
+ const found = await scoped(target);
691
+ if (isResult(found)) return found;
692
+ if ("catalog" in found) {
693
+ const hits = found.catalog.search(query, limit);
694
+ return {
695
+ content: [{
696
+ type: "text",
697
+ text: renderCatalogSearch(query, hits)
698
+ }],
699
+ structuredContent: { results: hits.map((h) => ({
700
+ name: h.component.name,
701
+ ...h.component.parent ? { parent: h.component.parent } : {},
702
+ ...h.component.description ? { description: h.component.description } : {},
703
+ importPath: h.component.importPath,
704
+ score: h.score,
705
+ matched: h.matched,
706
+ ...origin(h.entry)
707
+ })) }
708
+ };
709
+ }
710
+ const { ds } = found;
711
+ const hits = ds.search(query, limit);
712
+ return {
713
+ content: [{
714
+ type: "text",
715
+ text: renderSearch(query, hits)
716
+ }],
717
+ structuredContent: { results: hits.map((h) => ({
718
+ name: h.component.name,
719
+ ...h.component.parent ? { parent: h.component.parent } : {},
720
+ ...h.component.description ? { description: h.component.description } : {},
721
+ importPath: h.component.importPath,
722
+ score: h.score,
723
+ matched: h.matched
724
+ })) }
725
+ };
726
+ });
727
+ server.registerTool("get_tokens", {
728
+ title: "Get design tokens",
729
+ description: "List design tokens with resolved values, dark-mode values and how to use each (Tailwind class or CSS variable). Filter by category (color, spacing, radius, typography, shadow, other) or a substring. Use these instead of hex codes, px values or Tailwind default palette classes.",
730
+ inputSchema: z.object({
731
+ category: z.enum(TOKEN_CATEGORIES).optional().describe("Only tokens of this category."),
732
+ query: z.string().max(MAX_QUERY).optional().describe("Substring to match in names, usages or descriptions."),
733
+ path: projectPath
734
+ }),
735
+ outputSchema: z.object({ tokens: z.array(tokenSchema) }),
736
+ annotations: READ_ONLY
737
+ }, async ({ category, query, path: target }) => {
738
+ const found = await scoped(target);
739
+ if (isResult(found)) return found;
740
+ if ("catalog" in found) {
741
+ const tokens = found.catalog.getTokens({
742
+ category,
743
+ query
744
+ });
745
+ return {
746
+ content: [{
747
+ type: "text",
748
+ text: renderCatalogTokens(found.catalog, tokens, {
749
+ category,
750
+ query
751
+ })
752
+ }],
753
+ structuredContent: { tokens: tokens.map(({ token: t, package: pkg, projects }) => ({
754
+ name: t.name,
755
+ category: t.category,
756
+ value: t.value,
757
+ ...t.modes ? { modes: t.modes } : {},
758
+ ...t.cssVar ? { cssVar: t.cssVar } : {},
759
+ ...t.tailwind !== void 0 ? { tailwind: t.tailwind } : {},
760
+ usage: t.usage,
761
+ ...t.description ? { description: t.description } : {},
762
+ package: pkg,
763
+ projects
764
+ })) }
765
+ };
766
+ }
767
+ const { ds } = found;
768
+ const tokens = ds.getTokens({
769
+ category,
770
+ query
771
+ });
772
+ return {
773
+ content: [{
774
+ type: "text",
775
+ text: renderTokens(ds, tokens, {
776
+ category,
777
+ query
778
+ })
779
+ }],
780
+ structuredContent: { tokens: tokens.map((t) => ({
781
+ name: t.name,
782
+ category: t.category,
783
+ value: t.value,
784
+ ...t.modes ? { modes: t.modes } : {},
785
+ ...t.cssVar ? { cssVar: t.cssVar } : {},
786
+ ...t.tailwind !== void 0 ? { tailwind: t.tailwind } : {},
787
+ usage: t.usage,
788
+ ...t.description ? { description: t.description } : {}
789
+ })) }
790
+ };
791
+ });
792
+ server.registerTool("check_ui", {
793
+ title: "Check UI code against the design system",
794
+ description: "Lint TSX/JSX against the design system. Reports hardcoded colors, spacing and radius (with the nearest token), native elements that have a design-system component, unknown components, props and variant values, and icon-only buttons without an accessible name. Each diagnostic has line/column, a rule id, a message and a suggested fix. Run it on every snippet or file you write and fix all errors. Pass `code` for unsaved code, or `path` for a file in the project.",
795
+ inputSchema: z.object({
796
+ code: z.string().max(MAX_CODE).optional().describe(`TSX/JSX source to check (up to ${MAX_CODE.toLocaleString("en")} characters).`),
797
+ path: z.string().max(MAX_PATH).optional().describe("Path of a file to check: relative to the project root (the monorepo root, when served from there), or absolute."),
798
+ filename: z.string().max(MAX_PATH).optional().describe("Name to report `code` under; a .jsx extension parses it as JSX. At a monorepo root, the path the code will be saved under, which picks the project whose design system checks it. When the server was started outside your project, pass it as an absolute path: the project is found from it."),
799
+ limit: z.number().int().min(1).max(MAX_LIMIT).default(DEFAULT_LIMIT).describe(`Most diagnostics to return, errors first (default ${DEFAULT_LIMIT}). The counts and byRule cover all of them.`)
800
+ }),
801
+ outputSchema: z.object({
802
+ file: z.string(),
803
+ ok: z.boolean().describe("True when there are no errors."),
804
+ errorCount: int(),
805
+ warningCount: int(),
806
+ diagnostics: z.array(diagnosticSchema).describe("Up to `limit` diagnostics, errors first; `omitted` says how many more there are."),
807
+ omitted: int().describe("Diagnostics left out by `limit`."),
808
+ byRule: z.record(z.string(), z.object({
809
+ errors: int(),
810
+ warnings: int()
811
+ })).describe("Every finding per rule, shown or not."),
812
+ skipped: z.string().optional().describe("Why the file was not checked: it renders an image (next/og) or an email."),
813
+ unchecked: z.object({
814
+ names: z.array(z.string()),
815
+ modules: z.array(z.string())
816
+ }).optional().describe("Components imported from modules the model does not include, not checked."),
817
+ notice: z.string().optional().describe("Set when no components or no color tokens were found, so rules did not run, or when components came from modules the model does not include.")
818
+ }),
819
+ annotations: READ_ONLY
820
+ }, async ({ code, path: filePath, filename, limit }) => {
821
+ if (code === void 0 && !filePath) return errorResult("Pass either `code` or `path`.");
822
+ const routeBy = code === void 0 ? filePath : filename ?? void 0;
823
+ const set = await getProjects?.(routeBy);
824
+ let ds;
825
+ if (!set) ds = await getDesignSystem(routeBy);
826
+ else if (routeBy) {
827
+ const found = await scoped(routeBy);
828
+ if (isResult(found)) return found;
829
+ if (!("ds" in found)) return errorResult(`Cannot find the project of ${routeBy}.`);
830
+ ds = found.ds;
831
+ } else if (set.dirs.length <= 1) ds = (await set.forPath(set.dirs[0] ?? ".")).ds;
832
+ else return errorResult(`This is a monorepo root with ${set.dirs.length} projects (${listOf(set.dirs, 6)}). Pass \`filename\`, the path the code will be saved under relative to the root, so the right project's design system checks it.`);
833
+ const base = set?.root ?? ds.root;
834
+ let source = code;
835
+ let file = filename ?? "snippet.tsx";
836
+ let shown = file;
837
+ if (filename) {
838
+ const absolute = path.resolve(base, filename);
839
+ if (set || isInside(ds.root, absolute)) file = relativePath(ds.root, absolute);
840
+ }
841
+ if (source === void 0) {
842
+ if (!filePath) return errorResult("Pass either `code` or `path`.");
843
+ const absolute = path.resolve(base, filePath);
844
+ const outside = errorResult(`${filePath} is outside the project root (${base}).`);
845
+ if (!isInside(base, absolute)) return outside;
846
+ try {
847
+ const [root, real] = await Promise.all([fsp.realpath(base), fsp.realpath(absolute)]);
848
+ if (!isInside(root, real)) return outside;
849
+ if ((await fsp.stat(real)).size > MAX_CODE) return {
850
+ isError: true,
851
+ content: [{
852
+ type: "text",
853
+ text: `${filePath} is too large to check.`
854
+ }]
855
+ };
856
+ source = await fsp.readFile(real, "utf8");
857
+ } catch {
858
+ return {
859
+ isError: true,
860
+ content: [{
861
+ type: "text",
862
+ text: `Cannot read ${filePath}.`
863
+ }]
864
+ };
865
+ }
866
+ file = relativePath(ds.root, absolute);
867
+ shown = relativePath(base, absolute);
868
+ }
869
+ const checked = ds.check(source, file);
870
+ const result = file === shown ? checked : {
871
+ ...checked,
872
+ file: shown
873
+ };
874
+ const capped = capDiagnostics(result, limit);
875
+ const notice = result.skipped ? void 0 : [ds.notice(), uncheckedNotice([result])].filter(Boolean).join("\n\n") || void 0;
876
+ return {
877
+ content: [{
878
+ type: "text",
879
+ text: renderCheck(result, capped) + (notice ? `\n\n${notice}` : "")
880
+ }],
881
+ structuredContent: {
882
+ ...result,
883
+ diagnostics: capped.shown,
884
+ omitted: capped.omitted,
885
+ byRule: capped.byRule,
886
+ ok: result.errorCount === 0,
887
+ ...notice ? { notice } : {}
888
+ }
889
+ };
890
+ });
891
+ server.registerResource("component", new ResourceTemplate("ds://components/{name}", {
892
+ list: async () => {
893
+ const found = await listed();
894
+ if (!found) return { resources: [] };
895
+ if ("catalog" in found) return { resources: found.catalog.entries.map(({ component: c, ambiguous, projects }) => ({
896
+ uri: `ds://components/${c.name}${ambiguous ? `?path=${encodeURIComponent(projects[0] ?? ".")}` : ""}`,
897
+ name: c.name,
898
+ title: c.name,
899
+ ...c.description ? { description: c.description } : {},
900
+ mimeType: "text/markdown"
901
+ })) };
902
+ return { resources: found.ds.components.map((c) => ({
903
+ uri: `ds://components/${c.name}`,
904
+ name: c.name,
905
+ title: c.name,
906
+ ...c.description ? { description: c.description } : {},
907
+ mimeType: "text/markdown"
908
+ })) };
909
+ },
910
+ complete: { name: async (value) => {
911
+ const found = await listed();
912
+ if (!found) return [];
913
+ const lower = value.toLowerCase();
914
+ const names = "catalog" in found ? found.catalog.entries.map((e) => e.component.name) : found.ds.components.map((c) => c.name);
915
+ return [...new Set(names)].filter((n) => n.toLowerCase().startsWith(lower));
916
+ } }
917
+ }), {
918
+ title: "Design-system component",
919
+ description: "Props, variants, parts, tokens, guidelines and examples for one component.",
920
+ mimeType: "text/markdown"
921
+ }, async (uri, { name }) => {
922
+ const wanted = decodeURIComponent(String(name).split("?")[0] ?? "");
923
+ const found = await scope(uri.searchParams.get("path") ?? void 0);
924
+ if ("catalog" in found) {
925
+ const matches = found.catalog.find(wanted);
926
+ const [entry] = matches;
927
+ if (!entry) throw new Error(`No component named "${wanted}".`);
928
+ if (matches.length > 1) throw new Error(renderAmbiguous(wanted, matches));
929
+ return { contents: [{
930
+ uri: uri.href,
931
+ mimeType: "text/markdown",
932
+ text: renderComponent(entry.ds, entry.component, entry)
933
+ }] };
934
+ }
935
+ const { ds } = found;
936
+ const component = ds.getComponent(wanted);
937
+ if (!component) throw new Error(`No component named "${wanted}".`);
938
+ const text = renderComponent(ds, fromRoot(ds, component, found.root));
939
+ return { contents: [{
940
+ uri: uri.href,
941
+ mimeType: "text/markdown",
942
+ text
943
+ }] };
944
+ });
945
+ server.registerResource("tokens", "ds://tokens", {
946
+ title: "Design tokens",
947
+ description: "Every design token with resolved values, modes and usages, as JSON.",
948
+ mimeType: "application/json"
949
+ }, async (uri) => {
950
+ const found = await scope();
951
+ const tokens = "catalog" in found ? found.catalog.getTokens().map(({ token, package: pkg, projects }) => ({
952
+ ...token,
953
+ package: pkg,
954
+ projects
955
+ })) : found.ds.getTokens();
956
+ return { contents: [{
957
+ uri: uri.href,
958
+ mimeType: "application/json",
959
+ text: JSON.stringify(tokens, null, 2)
960
+ }] };
961
+ });
962
+ server.registerPrompt("build-with-design-system", {
963
+ title: "Build UI with the design system",
964
+ description: "Build a piece of UI using only design-system components and tokens, then verify it with check_ui.",
965
+ argsSchema: z.object({ task: z.string().max(MAX_QUERY * 4).describe("What to build, e.g. \"a settings card that lets owners delete the workspace\".") })
966
+ }, async ({ task }) => {
967
+ const found = await scope();
968
+ const roots = "catalog" in found ? [...new Map(found.catalog.roots().map((e) => [e.component.name, e.component])).values()] : found.ds.roots();
969
+ const allTokens = "catalog" in found ? found.catalog.getTokens().map((t) => t.token) : found.ds.tokens;
970
+ const components = roots.map((c) => {
971
+ const extras = [...c.variants.map((v) => v.name), ...c.subcomponents.length ? [`+${c.subcomponents.length} parts`] : []];
972
+ return `${c.name}${extras.length ? ` (${extras.join(", ")})` : ""}`;
973
+ }).join(", ");
974
+ const counts = /* @__PURE__ */ new Map();
975
+ for (const token of allTokens) counts.set(token.category, (counts.get(token.category) ?? 0) + 1);
976
+ const tokens = [...counts].map(([category, n]) => `${n} ${category}`).join(", ");
977
+ return {
978
+ description: "Build UI with the design system",
979
+ messages: [{
980
+ role: "user",
981
+ content: {
982
+ type: "text",
983
+ text: `Build this UI with the project's design system: ${task}
984
+
985
+ Work like this:
986
+ 1. Find components: search_components for each piece of the UI (for example "confirm a destructive action"), or list_components for an overview.
987
+ 2. Read the contract: get_component for every component you use. Use only the props and variant values it lists, and compose parts the way its examples do.
988
+ 3. Style with tokens: get_tokens when you need a color, spacing or radius. Never write hex/rgb colors, arbitrary px values or Tailwind default palette classes such as bg-blue-500.
989
+ 4. Use design-system components instead of native elements (<Button>, not <button>).
990
+ 5. Verify: run check_ui on the code you wrote, fix every error, and run it again until it reports none.
991
+
992
+ Design system: ${components || "no components found"}.
993
+ Tokens: ${tokens || "none"}.${"catalog" in found ? `\n\nThis is a monorepo root with ${found.set.dirs.length} projects (${found.set.dirs.join(", ")}): pass \`path\` with the file you are writing to get_component, get_tokens and check_ui, so they answer for its project.` : ""}`
994
+ }
995
+ }]
996
+ };
997
+ });
998
+ return server;
999
+ }
1000
+ //#endregion
1001
+ //#region src/catalog.ts
1002
+ /**
1003
+ * Every project's components and tokens in one list, for a workspace root when no path
1004
+ * picks a project. A component used by several projects (a design-system package) is
1005
+ * listed once, tagged with its package; two different components under one name are both
1006
+ * listed, each with its package, and a lookup by that name returns both.
1007
+ */
1008
+ var Catalog = class {
1009
+ root;
1010
+ projects;
1011
+ entries;
1012
+ tokens;
1013
+ index;
1014
+ constructor(root, projects) {
1015
+ this.root = root;
1016
+ this.projects = projects;
1017
+ const byKey = /* @__PURE__ */ new Map();
1018
+ const owner = packageOwner(root);
1019
+ for (const { ds, dir } of projects) for (const component of ds.components) {
1020
+ const file = path.resolve(ds.root, component.source.file);
1021
+ const key = `${file}\0${component.name}`;
1022
+ const known = byKey.get(key);
1023
+ if (known) {
1024
+ if (!known.projects.includes(dir)) known.projects.push(dir);
1025
+ if (isBare(component.importPath) && !isBare(known.component.importPath)) {
1026
+ known.component = rebase(component, root, file);
1027
+ known.ds = ds;
1028
+ }
1029
+ continue;
1030
+ }
1031
+ byKey.set(key, {
1032
+ component: rebase(component, root, file),
1033
+ ds,
1034
+ package: component.library ?? owner(file),
1035
+ projects: [dir],
1036
+ ambiguous: false
1037
+ });
1038
+ }
1039
+ this.entries = [...byKey.values()];
1040
+ const names = /* @__PURE__ */ new Map();
1041
+ for (const entry of this.entries) names.set(entry.component.name, (names.get(entry.component.name) ?? 0) + 1);
1042
+ for (const entry of this.entries) entry.ambiguous = (names.get(entry.component.name) ?? 0) > 1;
1043
+ this.index = buildSearchIndex(this.entries.map((e) => e.component));
1044
+ const tokens = /* @__PURE__ */ new Map();
1045
+ for (const { ds, dir } of projects) for (const token of ds.getTokens()) {
1046
+ const file = path.resolve(ds.root, token.source.file);
1047
+ const key = `${file}\0${token.name}\0${token.value}`;
1048
+ const known = tokens.get(key);
1049
+ if (known) {
1050
+ if (!known.projects.includes(dir)) known.projects.push(dir);
1051
+ continue;
1052
+ }
1053
+ tokens.set(key, {
1054
+ token: {
1055
+ ...token,
1056
+ source: {
1057
+ ...token.source,
1058
+ file: relativePath(root, file)
1059
+ }
1060
+ },
1061
+ ds,
1062
+ package: owner(file),
1063
+ projects: [dir]
1064
+ });
1065
+ }
1066
+ this.tokens = [...tokens.values()];
1067
+ }
1068
+ /** Top-level components, not parts of another. */
1069
+ roots() {
1070
+ return this.entries.filter((e) => !e.component.parent);
1071
+ }
1072
+ /** Every distinct component a name resolves to, through each project's lookup. */
1073
+ find(name) {
1074
+ const found = [];
1075
+ for (const { ds } of this.projects) {
1076
+ const component = ds.getComponent(name);
1077
+ if (!component) continue;
1078
+ const file = path.resolve(ds.root, component.source.file);
1079
+ const entry = this.entries.find((e) => e.component.name === component.name && path.resolve(this.root, e.component.source.file) === file);
1080
+ if (entry && !found.includes(entry)) found.push(entry);
1081
+ }
1082
+ return found;
1083
+ }
1084
+ suggest(name) {
1085
+ return closest(name, unique(this.entries.map((e) => e.component.name)), .5);
1086
+ }
1087
+ search(query, limit = 5) {
1088
+ return searchComponents(this.index, query, limit).flatMap((hit) => {
1089
+ const entry = this.entries.find((e) => e.component === hit.component);
1090
+ return entry ? [{
1091
+ ...hit,
1092
+ entry
1093
+ }] : [];
1094
+ });
1095
+ }
1096
+ /** Tokens as `get_tokens` filters them, core ones before scoped families. */
1097
+ getTokens(filter = {}) {
1098
+ const query = filter.query?.toLowerCase();
1099
+ const scoped = (t) => Number(t.ds.tokenIndex.scopedFamilyOf(t.token) !== void 0);
1100
+ return this.tokens.filter(({ token: t }) => (!filter.category || t.category === filter.category) && (!query || t.name.toLowerCase().includes(query) || (t.description ?? "").toLowerCase().includes(query) || t.usage.some((u) => u.toLowerCase().includes(query)))).sort((a, b) => scoped(a) - scoped(b));
1101
+ }
1102
+ };
1103
+ /** A specifier apps import by package name, not a relative path or a tsconfig alias. */
1104
+ function isBare(specifier) {
1105
+ return !/^(?:\.|@\/|~\/|#)/.test(specifier);
1106
+ }
1107
+ function rebase(component, root, file) {
1108
+ return {
1109
+ ...component,
1110
+ source: {
1111
+ ...component.source,
1112
+ file: relativePath(root, file)
1113
+ }
1114
+ };
1115
+ }
1116
+ /** The name of the package a file belongs to (its nearest package.json), cached per folder. */
1117
+ function packageOwner(root) {
1118
+ const names = /* @__PURE__ */ new Map();
1119
+ return (file) => {
1120
+ const visited = [];
1121
+ let name;
1122
+ for (let dir = path.dirname(file);; dir = path.dirname(dir)) {
1123
+ const known = names.get(dir);
1124
+ if (known !== void 0) {
1125
+ name = known;
1126
+ break;
1127
+ }
1128
+ visited.push(dir);
1129
+ try {
1130
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
1131
+ name = typeof pkg.name === "string" ? pkg.name : relativePath(root, dir) || ".";
1132
+ break;
1133
+ } catch {}
1134
+ if (dir === root || dir === path.dirname(dir)) {
1135
+ name = relativePath(root, dir) || ".";
1136
+ break;
1137
+ }
1138
+ }
1139
+ for (const dir of visited) names.set(dir, name);
1140
+ return name;
1141
+ };
1142
+ }
1143
+ //#endregion
1144
+ //#region src/server/projects.ts
1145
+ /**
1146
+ * A workspace root's projects for the MCP server: one `DesignSystemHost` per project,
1147
+ * created on first use and watched from then on, and the merged catalog, rebuilt when a
1148
+ * project reloads.
1149
+ */
1150
+ var WorkspaceProjects = class {
1151
+ workspace;
1152
+ options;
1153
+ hosts = /* @__PURE__ */ new Map();
1154
+ merged;
1155
+ constructor(workspace, options) {
1156
+ this.workspace = workspace;
1157
+ this.options = options;
1158
+ }
1159
+ get root() {
1160
+ return this.workspace.root;
1161
+ }
1162
+ get dirs() {
1163
+ return this.workspace.projects.map((p) => p.dir);
1164
+ }
1165
+ async forPath(target) {
1166
+ const absolute = path.resolve(this.root, target);
1167
+ if (!isInside(this.root, absolute)) throw new Error(`${target} is outside the workspace root (${this.root}).`);
1168
+ const project = await this.workspace.projectFor(absolute);
1169
+ return {
1170
+ ds: await this.host(project).get(),
1171
+ dir: project.dir,
1172
+ name: project.name
1173
+ };
1174
+ }
1175
+ async all() {
1176
+ const served = [];
1177
+ for (const project of this.workspace.projects) served.push({
1178
+ ds: await this.host(project).get(),
1179
+ dir: project.dir,
1180
+ name: project.name
1181
+ });
1182
+ return served;
1183
+ }
1184
+ async catalog() {
1185
+ const all = await this.all();
1186
+ const from = all.map((p) => p.ds);
1187
+ if (this.merged?.from.length !== from.length || this.merged.from.some((ds, i) => ds !== from[i])) this.merged = {
1188
+ from,
1189
+ catalog: new Catalog(this.root, all)
1190
+ };
1191
+ return this.merged.catalog;
1192
+ }
1193
+ close() {
1194
+ for (const host of this.hosts.values()) host.close();
1195
+ this.hosts.clear();
1196
+ }
1197
+ host(project) {
1198
+ let host = this.hosts.get(project.root);
1199
+ if (host) return host;
1200
+ host = new DesignSystemHost(project.config, {
1201
+ cache: this.options.cache,
1202
+ logger: this.options.logger,
1203
+ loadConfig: () => this.workspace.configFor(project)
1204
+ });
1205
+ host.onChange(() => this.options.onChange?.());
1206
+ if (this.options.watch) host.watch();
1207
+ this.hosts.set(project.root, host);
1208
+ return host;
1209
+ }
1210
+ };
1211
+ //#endregion
1212
+ //#region src/server/stdio.ts
1213
+ /**
1214
+ * Starts the server (on stdio unless another transport is given), for clients on
1215
+ * either protocol era: a 2026-07-28 client (stateless, `server/discover`) and a
1216
+ * 2025-era one (the `initialize` handshake) are both served, the era chosen by the
1217
+ * client's first message.
1218
+ *
1219
+ * The project root comes from `--root` or `--config`, a config file in the working
1220
+ * directory, or the working directory itself when it holds a project (a
1221
+ * package.json, a components.json). MCP roots, which the 2026-07-28 revision
1222
+ * deprecates, are the fallback for a 2025-era client that starts servers somewhere
1223
+ * else (Claude Desktop starts them in `/`): its first `file://` root. At a monorepo
1224
+ * root with no design system of its own, every project in it is served.
1225
+ *
1226
+ * When none of those gives a project (a client that starts servers in the home or the
1227
+ * plugin's folder and reports no roots), each request's project is found from the
1228
+ * absolute path it names (`findProjectRoot`): the nearest folder at or above it with a
1229
+ * project marker, or the monorepo root above that, served as if the server had been
1230
+ * started there, except that a config there is read only when it is JSON: a prompt can
1231
+ * choose the path, so a JavaScript or TypeScript config is not run. Each root found is
1232
+ * loaded once and kept; a request without an absolute path is answered from the one root
1233
+ * found so far.
1234
+ */
1235
+ function serveStdio$1(options) {
1236
+ const { logger } = options;
1237
+ let decide = () => void 0;
1238
+ const decided = new Promise((resolve) => {
1239
+ decide = resolve;
1240
+ });
1241
+ const instances = /* @__PURE__ */ new Set();
1242
+ const notify = () => {
1243
+ for (const server of instances) {
1244
+ if (!server.isConnected()) continue;
1245
+ server.server.sendResourceListChanged().catch((error) => {
1246
+ logger.warn(`could not notify the client: ${error.message}`);
1247
+ });
1248
+ }
1249
+ };
1250
+ const closers = [];
1251
+ /**
1252
+ * The project at `root` (the working directory when undefined), or a monorepo root's
1253
+ * projects. `jsonConfigOnly` for a root found from a tool call's path: its configs, and those
1254
+ * of the projects in it, are read only when they are JSON, never run.
1255
+ */
1256
+ const load = async (root, jsonConfigOnly = false) => {
1257
+ const configOptions = {
1258
+ ...options,
1259
+ root,
1260
+ jsonConfigOnly
1261
+ };
1262
+ const { config, workspace } = await loadTarget(configOptions);
1263
+ for (const text of config.deprecations ?? []) logger.warn(text);
1264
+ for (const text of (workspace ? workspace.warnings : config.warnings) ?? []) logger.warn(text);
1265
+ if (workspace) {
1266
+ const projects = new WorkspaceProjects(workspace, {
1267
+ cache: options.cache,
1268
+ watch: options.watch,
1269
+ logger,
1270
+ onChange: notify
1271
+ });
1272
+ logger.info(`monorepo root: ${workspace.projects.length} of ${workspace.projects.length + workspace.others.length} workspace packages have a design system${workspace.projects.length ? ` (${workspace.projects.map((p) => p.dir).join(", ")})` : ""}`);
1273
+ closers.push(() => {
1274
+ projects.close();
1275
+ });
1276
+ return { projects };
1277
+ }
1278
+ if (config.detected) logger.info(`found the design system through ${config.detected}`);
1279
+ const host = new DesignSystemHost(config, {
1280
+ cache: options.cache,
1281
+ logger,
1282
+ loadConfig: () => loadConfig(configOptions)
1283
+ });
1284
+ host.onChange(notify);
1285
+ if (options.watch) host.watch();
1286
+ host.get().catch((error) => {
1287
+ logger.error(error.message);
1288
+ });
1289
+ closers.push(() => {
1290
+ host.close();
1291
+ });
1292
+ return { host };
1293
+ };
1294
+ let started = false;
1295
+ /** One project for every request: the one at `root`, or the working directory. */
1296
+ const serveOne = (root) => {
1297
+ if (started) return;
1298
+ started = true;
1299
+ const served = load(root);
1300
+ served.catch((error) => {
1301
+ logger.error(error.message);
1302
+ });
1303
+ decide(() => served);
1304
+ };
1305
+ /** Each request's project, from the absolute path it names. */
1306
+ const serveByPath = () => {
1307
+ if (started) return;
1308
+ started = true;
1309
+ logger.info(`${options.cwd} holds no project and the client reported no roots: finding each request's project from the absolute path it names`);
1310
+ const found = /* @__PURE__ */ new Map();
1311
+ decide(async (target) => {
1312
+ if (target !== void 0 && path.isAbsolute(target)) {
1313
+ const dir = findProjectRoot(target);
1314
+ if (!dir) throw new ProjectNotFoundError(`No project at or above ${target}: no folder from there up holds a package.json, components.json, project.json or onsystem config (the home folder and the filesystem root do not count). Pass the absolute path of a file in your project.`);
1315
+ let served = found.get(dir);
1316
+ if (!served) {
1317
+ logger.info(`using ${dir}, the project of ${target}`);
1318
+ served = load(dir, true);
1319
+ served.catch((error) => {
1320
+ logger.error(error.message);
1321
+ });
1322
+ found.set(dir, served);
1323
+ }
1324
+ return served;
1325
+ }
1326
+ const [only, ...others] = found.values();
1327
+ if (only && !others.length) return only;
1328
+ throw new ProjectNotFoundError(only ? `Requests have named files in ${found.size} projects (${[...found.keys()].join(", ")}). Pass \`path\` (for check_ui with \`code\`, \`filename\`) as the absolute path of the file you are editing, so the answer comes from its project.` : `onsystem cannot tell which project to answer for: it was started in ${options.cwd}, which holds no project, and the client reported no workspace roots. Pass \`path\` (for check_ui with \`code\`, \`filename\`) as the absolute path of the file you are editing, and it answers from that file's project. Or start the server with --root <project folder>.`);
1329
+ });
1330
+ };
1331
+ if (options.root !== void 0 || options.config !== void 0 || [...CONFIG_FILES, ...LEGACY_CONFIG_FILES].some((file) => fs.existsSync(path.join(options.cwd, file))) || hasProjectMarker(options.cwd)) serveOne(options.root);
1332
+ /**
1333
+ * When nothing else says where the project is: the working directory when flags
1334
+ * (`--components`, `--tokens`, `--docs`) describe it, else each request's path.
1335
+ */
1336
+ const fallback = () => {
1337
+ if (options.components?.length || options.tokens?.length || options.docs?.length) serveOne(void 0);
1338
+ else serveByPath();
1339
+ };
1340
+ let askingRoots = false;
1341
+ /** What answers requests, once decided; nothing has said where the project is by the first request. */
1342
+ const resolve = async (target) => {
1343
+ if (!started && !askingRoots) fallback();
1344
+ return (await decided)(target);
1345
+ };
1346
+ const factory = ({ era }) => {
1347
+ const server = createServer({
1348
+ getDesignSystem: async (target) => {
1349
+ const served = await resolve(target);
1350
+ if ("host" in served) return served.host.get();
1351
+ throw new Error("This server serves a workspace root: pass a path.");
1352
+ },
1353
+ getProjects: async (target) => {
1354
+ const served = await resolve(target);
1355
+ return "projects" in served ? served.projects : void 0;
1356
+ }
1357
+ });
1358
+ instances.add(server);
1359
+ if (!started && era === "legacy") server.server.oninitialized = () => {
1360
+ askingRoots = true;
1361
+ clientRoot(server).then((root) => {
1362
+ if (!root) {
1363
+ fallback();
1364
+ return;
1365
+ }
1366
+ if (root !== options.cwd) logger.info(`using client root ${root}`);
1367
+ serveOne(root);
1368
+ });
1369
+ };
1370
+ return server;
1371
+ };
1372
+ const handle = serveStdio(factory, {
1373
+ ...options.transport ? { transport: options.transport } : {},
1374
+ onerror: (error) => {
1375
+ logger.warn(error.message);
1376
+ }
1377
+ });
1378
+ logger.info(options.transport ? "ready" : "ready on stdio");
1379
+ return Promise.resolve({ close: async () => {
1380
+ for (const close of closers.splice(0)) close();
1381
+ await handle.close();
1382
+ } });
1383
+ }
1384
+ /** First `file://` root the client reports, if it supports roots (deprecated in 2026-07-28). */
1385
+ async function clientRoot(server) {
1386
+ if (!server.server.getClientCapabilities()?.roots) return void 0;
1387
+ try {
1388
+ const { roots } = await server.server.listRoots(void 0, { timeout: 3e3 });
1389
+ const uri = roots.find((r) => r.uri.startsWith("file://"))?.uri;
1390
+ return uri ? fileURLToPath(uri) : void 0;
1391
+ } catch {
1392
+ return;
1393
+ }
1394
+ }
1395
+ //#endregion
1396
+ export { INSTRUCTIONS as n, createServer as r, serveStdio$1 as t };