mlola-ui 1.1.9 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,8 +62,9 @@ that its UI is Mlola:
62
62
  `npx mlola-ui mcp` is that server. It answers from the registry bundled with
63
63
  this CLI, offline: `get_design_rules`, `search_components`, `get_component`,
64
64
  `get_tokens`, `check_markup` (invented classes, wrong `data-*` values, utility
65
- classes, hand-written colors), `add_components` and `init_project`. For
66
- Claude Code without init:
65
+ classes, and hand-written colors or faded text in `style` attributes and
66
+ `<style>` blocks), `add_components` and `init_project`. For Claude Code
67
+ without init:
67
68
 
68
69
  ```sh
69
70
  claude mcp add mlola --scope project -- npx -y mlola-ui mcp
@@ -71,7 +72,9 @@ claude mcp add mlola --scope project -- npx -y mlola-ui mcp
71
72
 
72
73
  It is started by your agent and talks over stdio, so run by hand it prints one
73
74
  line and waits. The same tools, minus the two that write, answer at
74
- `https://ui.mlola.com/mcp` for agents that cannot run a command, and the server
75
+ `https://ui.mlola.com/mcp` for agents that cannot run a command (with
76
+ `get_install_command`, which also gives the CDN link and script for a page with
77
+ no build step), and the server
75
78
  is listed in the MCP Registry as `io.github.mlolahq/mlola-ui`. See
76
79
  https://ui.mlola.com/docs/agents for Cursor, VS Code, Codex and chat apps.
77
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlola-ui",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "mcpName": "io.github.mlolahq/mlola-ui",
5
5
  "description": "Source-copy CLI that writes framework-free Mlola UI items into your project",
6
6
  "type": "module",
@@ -2,7 +2,7 @@
2
2
  "$schema": "./schema/registry-index.schema.json",
3
3
  "schemaVersion": 2,
4
4
  "name": "mlola-ui",
5
- "version": "1.1.9",
5
+ "version": "1.1.10",
6
6
  "homepage": "https://ui.mlola.com",
7
7
  "themeContract": {
8
8
  "attribute": "data-theme",
@@ -177,8 +177,8 @@
177
177
  },
178
178
  "registryDependencies": [],
179
179
  "engineDependencies": {
180
- "@mlola-ui/behavior": "^1.1.9",
181
- "@mlola-ui/icons": "^1.1.9"
180
+ "@mlola-ui/behavior": "^1.1.10",
181
+ "@mlola-ui/icons": "^1.1.10"
182
182
  },
183
183
  "options": [
184
184
  {
@@ -239,7 +239,7 @@
239
239
  },
240
240
  "registryDependencies": [],
241
241
  "engineDependencies": {
242
- "@mlola-ui/icons": "^1.1.9"
242
+ "@mlola-ui/icons": "^1.1.10"
243
243
  },
244
244
  "options": [
245
245
  {
@@ -390,7 +390,9 @@
390
390
  "tags": [
391
391
  "status",
392
392
  "label",
393
- "indicator"
393
+ "indicator",
394
+ "chip",
395
+ "pill"
394
396
  ],
395
397
  "usage": {
396
398
  "importPath": "{{aliases.components}}/badge",
@@ -399,7 +401,7 @@
399
401
  },
400
402
  "registryDependencies": [],
401
403
  "engineDependencies": {
402
- "@mlola-ui/icons": "^1.1.9"
404
+ "@mlola-ui/icons": "^1.1.10"
403
405
  },
404
406
  "options": [
405
407
  {
@@ -491,7 +493,7 @@
491
493
  },
492
494
  "registryDependencies": [],
493
495
  "engineDependencies": {
494
- "@mlola-ui/icons": "^1.1.9"
496
+ "@mlola-ui/icons": "^1.1.10"
495
497
  },
496
498
  "options": [],
497
499
  "variants": [],
@@ -544,7 +546,7 @@
544
546
  "spinner"
545
547
  ],
546
548
  "engineDependencies": {
547
- "@mlola-ui/motion": "^1.1.9"
549
+ "@mlola-ui/motion": "^1.1.10"
548
550
  },
549
551
  "options": [
550
552
  {
@@ -632,7 +634,10 @@
632
634
  "tags": [
633
635
  "layout",
634
636
  "container",
635
- "surface"
637
+ "surface",
638
+ "stat",
639
+ "kpi",
640
+ "metric"
636
641
  ],
637
642
  "usage": {
638
643
  "importPath": "{{aliases.components}}/card",
@@ -708,8 +713,8 @@
708
713
  },
709
714
  "registryDependencies": [],
710
715
  "engineDependencies": {
711
- "@mlola-ui/behavior": "^1.1.9",
712
- "@mlola-ui/icons": "^1.1.9"
716
+ "@mlola-ui/behavior": "^1.1.10",
717
+ "@mlola-ui/icons": "^1.1.10"
713
718
  },
714
719
  "options": [
715
720
  {
@@ -777,7 +782,7 @@
777
782
  },
778
783
  "registryDependencies": [],
779
784
  "engineDependencies": {
780
- "@mlola-ui/icons": "^1.1.9"
785
+ "@mlola-ui/icons": "^1.1.10"
781
786
  },
782
787
  "options": [],
783
788
  "variants": [],
@@ -832,7 +837,7 @@
832
837
  "modal"
833
838
  ],
834
839
  "engineDependencies": {
835
- "@mlola-ui/icons": "^1.1.9"
840
+ "@mlola-ui/icons": "^1.1.10"
836
841
  },
837
842
  "options": [],
838
843
  "variants": [],
@@ -895,8 +900,8 @@
895
900
  },
896
901
  "registryDependencies": [],
897
902
  "engineDependencies": {
898
- "@mlola-ui/behavior": "^1.1.9",
899
- "@mlola-ui/icons": "^1.1.9"
903
+ "@mlola-ui/behavior": "^1.1.10",
904
+ "@mlola-ui/icons": "^1.1.10"
900
905
  },
901
906
  "options": [
902
907
  {
@@ -960,7 +965,7 @@
960
965
  "input"
961
966
  ],
962
967
  "engineDependencies": {
963
- "@mlola-ui/icons": "^1.1.9"
968
+ "@mlola-ui/icons": "^1.1.10"
964
969
  },
965
970
  "options": [],
966
971
  "variants": [],
@@ -1184,8 +1189,8 @@
1184
1189
  },
1185
1190
  "registryDependencies": [],
1186
1191
  "engineDependencies": {
1187
- "@mlola-ui/behavior": "^1.1.9",
1188
- "@mlola-ui/icons": "^1.1.9"
1192
+ "@mlola-ui/behavior": "^1.1.10",
1193
+ "@mlola-ui/icons": "^1.1.10"
1189
1194
  },
1190
1195
  "options": [
1191
1196
  {
@@ -1335,7 +1340,7 @@
1335
1340
  },
1336
1341
  "registryDependencies": [],
1337
1342
  "engineDependencies": {
1338
- "@mlola-ui/icons": "^1.1.9"
1343
+ "@mlola-ui/icons": "^1.1.10"
1339
1344
  },
1340
1345
  "options": [],
1341
1346
  "variants": [],
@@ -1386,7 +1391,7 @@
1386
1391
  },
1387
1392
  "registryDependencies": [],
1388
1393
  "engineDependencies": {
1389
- "@mlola-ui/behavior": "^1.1.9"
1394
+ "@mlola-ui/behavior": "^1.1.10"
1390
1395
  },
1391
1396
  "options": [
1392
1397
  {
@@ -1761,7 +1766,7 @@
1761
1766
  },
1762
1767
  "registryDependencies": [],
1763
1768
  "engineDependencies": {
1764
- "@mlola-ui/behavior": "^1.1.9"
1769
+ "@mlola-ui/behavior": "^1.1.10"
1765
1770
  },
1766
1771
  "options": [],
1767
1772
  "variants": [],
@@ -1814,8 +1819,8 @@
1814
1819
  "input"
1815
1820
  ],
1816
1821
  "engineDependencies": {
1817
- "@mlola-ui/behavior": "^1.1.9",
1818
- "@mlola-ui/icons": "^1.1.9"
1822
+ "@mlola-ui/behavior": "^1.1.10",
1823
+ "@mlola-ui/icons": "^1.1.10"
1819
1824
  },
1820
1825
  "options": [
1821
1826
  {
@@ -1892,8 +1897,8 @@
1892
1897
  },
1893
1898
  "registryDependencies": [],
1894
1899
  "engineDependencies": {
1895
- "@mlola-ui/behavior": "^1.1.9",
1896
- "@mlola-ui/icons": "^1.1.9"
1900
+ "@mlola-ui/behavior": "^1.1.10",
1901
+ "@mlola-ui/icons": "^1.1.10"
1897
1902
  },
1898
1903
  "options": [
1899
1904
  {
@@ -2039,7 +2044,7 @@
2039
2044
  "input"
2040
2045
  ],
2041
2046
  "engineDependencies": {
2042
- "@mlola-ui/behavior": "^1.1.9"
2047
+ "@mlola-ui/behavior": "^1.1.10"
2043
2048
  },
2044
2049
  "options": [
2045
2050
  {
@@ -2184,7 +2189,7 @@
2184
2189
  },
2185
2190
  "registryDependencies": [],
2186
2191
  "engineDependencies": {
2187
- "@mlola-ui/behavior": "^1.1.9"
2192
+ "@mlola-ui/behavior": "^1.1.10"
2188
2193
  },
2189
2194
  "options": [
2190
2195
  {
@@ -2316,7 +2321,7 @@
2316
2321
  },
2317
2322
  "registryDependencies": [],
2318
2323
  "engineDependencies": {
2319
- "@mlola-ui/icons": "^1.1.9"
2324
+ "@mlola-ui/icons": "^1.1.10"
2320
2325
  },
2321
2326
  "options": [
2322
2327
  {
@@ -2402,7 +2407,7 @@
2402
2407
  "spinner"
2403
2408
  ],
2404
2409
  "engineDependencies": {
2405
- "@mlola-ui/icons": "^1.1.9"
2410
+ "@mlola-ui/icons": "^1.1.10"
2406
2411
  },
2407
2412
  "options": [
2408
2413
  {
@@ -2545,7 +2550,7 @@
2545
2550
  },
2546
2551
  "registryDependencies": [],
2547
2552
  "engineDependencies": {
2548
- "@mlola-ui/behavior": "^1.1.9"
2553
+ "@mlola-ui/behavior": "^1.1.10"
2549
2554
  },
2550
2555
  "options": [
2551
2556
  {
@@ -2684,7 +2689,7 @@
2684
2689
  },
2685
2690
  "registryDependencies": [],
2686
2691
  "engineDependencies": {
2687
- "@mlola-ui/icons": "^1.1.9"
2692
+ "@mlola-ui/icons": "^1.1.10"
2688
2693
  },
2689
2694
  "options": [
2690
2695
  {
@@ -2756,7 +2761,7 @@
2756
2761
  "popover"
2757
2762
  ],
2758
2763
  "engineDependencies": {
2759
- "@mlola-ui/icons": "^1.1.9"
2764
+ "@mlola-ui/icons": "^1.1.10"
2760
2765
  },
2761
2766
  "options": [
2762
2767
  {
@@ -2826,7 +2831,7 @@
2826
2831
  "popover"
2827
2832
  ],
2828
2833
  "engineDependencies": {
2829
- "@mlola-ui/icons": "^1.1.9"
2834
+ "@mlola-ui/icons": "^1.1.10"
2830
2835
  },
2831
2836
  "options": [],
2832
2837
  "variants": [],
@@ -3024,7 +3029,7 @@
3024
3029
  "input"
3025
3030
  ],
3026
3031
  "engineDependencies": {
3027
- "@mlola-ui/icons": "^1.1.9"
3032
+ "@mlola-ui/icons": "^1.1.10"
3028
3033
  },
3029
3034
  "options": [
3030
3035
  {
@@ -3101,7 +3106,7 @@
3101
3106
  "input"
3102
3107
  ],
3103
3108
  "engineDependencies": {
3104
- "@mlola-ui/icons": "^1.1.9"
3109
+ "@mlola-ui/icons": "^1.1.10"
3105
3110
  },
3106
3111
  "options": [],
3107
3112
  "variants": [],
@@ -3156,7 +3161,7 @@
3156
3161
  "input"
3157
3162
  ],
3158
3163
  "engineDependencies": {
3159
- "@mlola-ui/icons": "^1.1.9"
3164
+ "@mlola-ui/icons": "^1.1.10"
3160
3165
  },
3161
3166
  "options": [],
3162
3167
  "variants": [],
@@ -3219,7 +3224,7 @@
3219
3224
  "popover"
3220
3225
  ],
3221
3226
  "engineDependencies": {
3222
- "@mlola-ui/icons": "^1.1.9"
3227
+ "@mlola-ui/icons": "^1.1.10"
3223
3228
  },
3224
3229
  "options": [
3225
3230
  {
@@ -3289,7 +3294,7 @@
3289
3294
  },
3290
3295
  "registryDependencies": [],
3291
3296
  "engineDependencies": {
3292
- "@mlola-ui/icons": "^1.1.9"
3297
+ "@mlola-ui/icons": "^1.1.10"
3293
3298
  },
3294
3299
  "options": [
3295
3300
  {
@@ -3408,8 +3413,8 @@
3408
3413
  },
3409
3414
  "registryDependencies": [],
3410
3415
  "engineDependencies": {
3411
- "@mlola-ui/behavior": "^1.1.9",
3412
- "@mlola-ui/icons": "^1.1.9"
3416
+ "@mlola-ui/behavior": "^1.1.10",
3417
+ "@mlola-ui/icons": "^1.1.10"
3413
3418
  },
3414
3419
  "options": [],
3415
3420
  "variants": [],
@@ -3473,7 +3478,7 @@
3473
3478
  },
3474
3479
  "registryDependencies": [],
3475
3480
  "engineDependencies": {
3476
- "@mlola-ui/behavior": "^1.1.9"
3481
+ "@mlola-ui/behavior": "^1.1.10"
3477
3482
  },
3478
3483
  "options": [
3479
3484
  {
@@ -3560,8 +3565,8 @@
3560
3565
  },
3561
3566
  "registryDependencies": [],
3562
3567
  "engineDependencies": {
3563
- "@mlola-ui/behavior": "^1.1.9",
3564
- "@mlola-ui/icons": "^1.1.9"
3568
+ "@mlola-ui/behavior": "^1.1.10",
3569
+ "@mlola-ui/icons": "^1.1.10"
3565
3570
  },
3566
3571
  "options": [],
3567
3572
  "variants": [],
@@ -3635,7 +3640,7 @@
3635
3640
  "sheet"
3636
3641
  ],
3637
3642
  "engineDependencies": {
3638
- "@mlola-ui/icons": "^1.1.9"
3643
+ "@mlola-ui/icons": "^1.1.10"
3639
3644
  },
3640
3645
  "options": [],
3641
3646
  "variants": [],
@@ -3758,7 +3763,7 @@
3758
3763
  "button"
3759
3764
  ],
3760
3765
  "engineDependencies": {
3761
- "@mlola-ui/icons": "^1.1.9"
3766
+ "@mlola-ui/icons": "^1.1.10"
3762
3767
  },
3763
3768
  "options": [
3764
3769
  {
@@ -3861,8 +3866,8 @@
3861
3866
  "input"
3862
3867
  ],
3863
3868
  "engineDependencies": {
3864
- "@mlola-ui/behavior": "^1.1.9",
3865
- "@mlola-ui/icons": "^1.1.9"
3869
+ "@mlola-ui/behavior": "^1.1.10",
3870
+ "@mlola-ui/icons": "^1.1.10"
3866
3871
  },
3867
3872
  "options": [
3868
3873
  {
package/src/knowledge.js CHANGED
@@ -50,21 +50,41 @@ export function allItems() {
50
50
  return [...free, ...pro];
51
51
  }
52
52
 
53
- /** Items matching a query: every word must appear in the name, title, description, category or tags. */
53
+ /**
54
+ * Items matching a query, searched in the name, title, description, category
55
+ * and tags. Items that match every word come first; when none does, the items
56
+ * that match the most words, so "radio card" still finds the radio group. A
57
+ * plural matches its singular, and "stat-card" reads as "stat card".
58
+ */
54
59
  export function searchItems({ query = "", kind, category, tier } = {}) {
55
- const words = query.toLowerCase().split(/\s+/).filter(Boolean);
56
- return allItems()
60
+ const words = query.toLowerCase().split(/[\s-]+/).filter(Boolean);
61
+ const forms = (word) => [word, ...(word.length > 3 && word.endsWith("s") ? [word.slice(0, -1)] : [])];
62
+ const scored = allItems()
57
63
  .filter((item) => (!kind || item.kind === kind) && (!category || item.category === category) && (!tier || item.tier === tier))
58
64
  .map((item) => {
59
65
  const haystack = [item.name, item.title, item.description, item.category, ...item.tags].join(" ").toLowerCase();
60
- const score = words.reduce((sum, word) => sum + (item.name === word ? 5 : item.name.includes(word) ? 3 : haystack.includes(word) ? 1 : -100), 0);
61
- return { item, score };
62
- })
63
- .filter(({ score }) => score >= 0)
66
+ let matched = 0;
67
+ let score = 0;
68
+ for (const word of words) {
69
+ const hit = Math.max(...forms(word).map((form) => (item.name === form ? 5 : item.name.includes(form) ? 3 : haystack.includes(form) ? 1 : 0)));
70
+ if (hit) matched += 1;
71
+ score += hit;
72
+ }
73
+ return { item, score, matched };
74
+ });
75
+ const best = Math.max(0, ...scored.map(({ matched }) => matched));
76
+ return scored
77
+ .filter(({ matched }) => !words.length || (best > 0 && matched === best))
64
78
  .sort((a, b) => b.score - a.score || a.item.name.localeCompare(b.item.name))
65
79
  .map(({ item }) => item);
66
80
  }
67
81
 
82
+ /** " Did you mean: …?" for a name that is not an item, or nothing. */
83
+ export function suggest(name) {
84
+ const close = [...new Set([...allItems().filter((entry) => entry.name.includes(name) || name.includes(entry.name)).map((entry) => entry.name), ...searchItems({ query: name }).map((entry) => entry.name)])].slice(0, 5);
85
+ return close.length ? ` Did you mean: ${close.join(", ")}?` : "";
86
+ }
87
+
68
88
  /** The classes a component styles (its own prefix) and the attributes each reacts to. */
69
89
  function elementsOf(name) {
70
90
  const contract = bundled("contract.json");
@@ -84,7 +104,9 @@ function projectConfig(cwd) {
84
104
  }
85
105
 
86
106
  /** Everything an agent needs to use one item correctly. */
87
- export function describeItem(name, { cwd = process.cwd(), includeSource = false } = {}) {
107
+ export function describeItem(requested, { cwd = process.cwd(), includeSource = false } = {}) {
108
+ // Agents often ask by class ("ml-button") or title ("Date picker"): both mean the item.
109
+ const name = requested.trim().toLowerCase().replace(/^ml-/, "").replace(/\s+/g, "-");
88
110
  const registry = loadRegistry();
89
111
  const item = registry.items.find((entry) => entry.name === name);
90
112
  if (!item) {
@@ -101,8 +123,7 @@ export function describeItem(name, { cwd = process.cwd(), includeSource = false
101
123
  note: "Part of Mlola Pro. Its source arrives with a license token; its classes and attributes are listed in mlola-pro.agents.md after the first Pro install.",
102
124
  };
103
125
  }
104
- const close = allItems().filter((entry) => entry.name.includes(name) || name.includes(entry.name)).map((entry) => entry.name).slice(0, 5);
105
- throw new Error(`No item named "${name}".${close.length ? ` Did you mean: ${close.join(", ")}?` : ""} Search with search_components.`);
126
+ throw new Error(`No item named "${requested}".${suggest(name)} Search with search_components.`);
106
127
  }
107
128
  const config = projectConfig(cwd);
108
129
  const usage = item.usage
@@ -267,8 +288,8 @@ export function checkMarkup(markup) {
267
288
  if (name === "data-theme" && value && !themeIds.has(value) && !/^th-[0-9a-z]{12}$/.test(value)) {
268
289
  note("error", tag, `data-theme="${value}" is not a theme.`, `Use one of: ${[...themeIds].join(", ")}, or a Studio theme id.`);
269
290
  }
270
- if (name === "data-mode" && !["light", "dark"].includes(value)) {
271
- note("error", tag, `data-mode is "light" or "dark"; "${value}" belongs in an attribute of your own.`, "data-theme and data-mode belong to the engine. Use data-kind or data-state for a component's own meaning.");
291
+ if (name === "data-mode" && !["light", "dark", "system"].includes(value)) {
292
+ note("error", tag, `data-mode is "light", "dark" or "system"; "${value}" belongs in an attribute of your own.`, "data-theme and data-mode belong to the engine. Use data-kind or data-state for a component's own meaning.");
272
293
  }
273
294
  if (!SHARED.has(name) || !mlola.length) continue;
274
295
  const allowed = new Set();
@@ -295,5 +316,28 @@ export function checkMarkup(markup) {
295
316
  }
296
317
  }
297
318
  }
319
+
320
+ // The page's own stylesheet: the same rules hold in a <style> block.
321
+ for (const [, css] of markup.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi)) {
322
+ for (const rule of css.replace(/\/\*[\s\S]*?\*\//g, "").split("}")) {
323
+ const open = rule.lastIndexOf("{");
324
+ if (open < 0) continue;
325
+ const selector = rule.slice(0, open).split("{").pop().trim();
326
+ const body = rule.slice(open + 1).replace(/url\([^)]*\)/g, "");
327
+ const where = `<style> ${selector}`;
328
+ const overridden = [...body.matchAll(/(--ml-[\w-]+)\s*:/g)].map((match) => match[1]).filter((token) => tokenNames.has(token));
329
+ if (COLOR.test(body.replace(/--[\w-]+\s*:[^;]*/g, ""))) {
330
+ note("error", where, "A color is written by hand in the stylesheet.", "Read a token: var(--ml-text), var(--ml-primary-text), var(--ml-surface)… (get_tokens). The theme then keeps its contrast in every mode.");
331
+ }
332
+ if (overridden.length) {
333
+ note("error", where, `${overridden.join(", ")} is a theme token; setting it here overrides the theme.`, "Pick a theme, or change the theme's spec (mlola.theme.json), instead of redefining its tokens.");
334
+ }
335
+ // Faded text: its contrast now depends on the theme behind it.
336
+ const opacity = /(?:^|;)\s*opacity\s*:\s*(0?\.\d+|0)\s*(?:;|$)/.exec(body);
337
+ if (opacity && !/disabled|:empty|::?placeholder|\[hidden\]|inert/.test(selector)) {
338
+ note("warning", where, `opacity: ${opacity[1]} fades whatever text is inside, and how far it falls below the contrast floor depends on the theme and the fill behind it.`, "For quieter text use color: var(--ml-text-muted); on a filled control, its -foreground role. Keep opacity for disabled or decorative parts.");
339
+ }
340
+ }
341
+ }
298
342
  return issues;
299
343
  }
package/src/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import readline from "node:readline";
4
- import { checkMarkup, describeItem, designData, designGuide, searchItems, themes, tokens } from "./knowledge.js";
4
+ import { checkMarkup, describeItem, designData, designGuide, searchItems, suggest, themes, tokens } from "./knowledge.js";
5
5
 
6
6
  /**
7
7
  * `mlola-ui mcp` — a Model Context Protocol server over stdio.
@@ -24,12 +24,28 @@ const INSTRUCTIONS = `This project's UI is Mlola UI: native CSS classes (ml-*),
24
24
  Before writing UI: call get_design_rules once, then search_components for what you need and get_component for how to use it. Read tokens with get_tokens instead of writing colors, sizes, shadows or durations. After writing markup, run check_markup on it and fix what it reports.`;
25
25
 
26
26
  const REMOTE_INSTRUCTIONS = `Mlola UI is a component system on native CSS classes (ml-*), data-* attributes for state and variant, and --ml-* tokens. There is no Tailwind.
27
- Before writing UI: call get_design_rules once, then search_components for what you need and get_component for how to use it. Read tokens with get_tokens. After writing markup, run check_markup and fix what it reports. This server cannot write files: get_install_command gives the commands to run in the project, and Mlola Pro items need a license token (npx mlola-ui login).`;
27
+ Before writing UI: call get_design_rules once, then search_components for what you need and get_component for how to use it. Read tokens with get_tokens. After writing markup, run check_markup and fix what it reports. This server cannot write files: get_install_command gives the commands to run in the project, and Mlola Pro items need a license token (npx mlola-ui login). For a page with no build step (one HTML file), get_design_rules gives the stylesheet link and the script to paste.`;
28
28
 
29
29
  const text = (value) => ({ content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }] });
30
30
 
31
+ /**
32
+ * How to load Mlola in a page with no build step: the engine's stylesheet and
33
+ * the behavior runtime from a CDN, at this release's version.
34
+ */
35
+ export function noBuildSetup(version) {
36
+ const cdn = (pkg, file) => `https://cdn.jsdelivr.net/npm/@mlola-ui/${pkg}@${version}/${file}`;
37
+ return [
38
+ "Without a build step (one HTML file), load Mlola from a CDN:",
39
+ `<link rel="stylesheet" href="${cdn("engine", "generated/mlola.css")}">`,
40
+ `<html data-theme="graphite" data-mode="system"> (data-mode system follows the reader's light or dark setting)`,
41
+ "For menus, dialogs, tabs, selects, sliders, switches, tooltips and toasts, mark each root with data-ml=\"<behavior>\" as get_component's html example shows, and load the runtime once:",
42
+ `<script type="module">import { observe } from "${cdn("behavior", "src/index.js")}"; observe();</script>`,
43
+ "The free components work this way. Mlola Pro items need a project, the CLI and a license.",
44
+ ].join("\n");
45
+ }
46
+
31
47
  /** Tools that only read the registry: the local and the remote server share them. */
32
- function readTools({ cwd }) {
48
+ function readTools({ cwd, version }) {
33
49
  return [
34
50
  {
35
51
  name: "get_design_rules",
@@ -39,7 +55,7 @@ function readTools({ cwd }) {
39
55
  annotations: { readOnlyHint: true },
40
56
  handler: () => {
41
57
  const data = designData();
42
- return text(`${data?.rules ?? ""}\nThemes: ${themes().map((theme) => theme.id).join(", ")} (data-theme on any ancestor, data-mode light|dark).\nComposition primitives: ${(data?.primitives ?? []).map((entry) => entry.name).join(" ")}`);
58
+ return text(`${data?.rules ?? ""}\nThemes: ${themes().map((theme) => theme.id).join(", ")} (data-theme on any ancestor, data-mode light|dark|system).\nComposition primitives: ${(data?.primitives ?? []).map((entry) => entry.name).join(" ")}\n\n${noBuildSetup(version)}`);
43
59
  },
44
60
  },
45
61
  {
@@ -88,7 +104,7 @@ function readTools({ cwd }) {
88
104
  {
89
105
  name: "check_markup",
90
106
  title: "Check markup against the Mlola contract",
91
- description: "Checks HTML or JSX: classes that do not exist, variant classes, data-* values a class does not react to, utility classes, hand-written colors, and misuse of data-theme or data-mode. Run it on markup you wrote before finishing.",
107
+ description: "Checks HTML or JSX: classes that do not exist, variant classes, data-* values a class does not react to, utility classes, misuse of data-theme or data-mode, and in style attributes and <style> blocks hand-written colors, overridden theme tokens and text faded with opacity. Run it on markup you wrote before finishing.",
92
108
  inputSchema: { type: "object", properties: { markup: { type: "string" } }, required: ["markup"], additionalProperties: false },
93
109
  annotations: { readOnlyHint: true },
94
110
  handler: ({ markup }) => {
@@ -99,7 +115,7 @@ function readTools({ cwd }) {
99
115
  ];
100
116
  }
101
117
 
102
- function tools({ cwd, run }) {
118
+ function tools({ cwd, run, version }) {
103
119
  const capture = async (argv) => {
104
120
  const lines = [];
105
121
  const output = { log: (...parts) => lines.push(parts.join(" ")), error: (...parts) => lines.push(parts.join(" ")) };
@@ -109,7 +125,7 @@ function tools({ cwd, run }) {
109
125
  return { code, output: lines.join("\n") };
110
126
  };
111
127
  return [
112
- ...readTools({ cwd }),
128
+ ...readTools({ cwd, version }),
113
129
  {
114
130
  name: "add_components",
115
131
  title: "Install Mlola components",
@@ -179,6 +195,32 @@ function prompt(name, args) {
179
195
  };
180
196
  }
181
197
 
198
+ /**
199
+ * Arguments that do not fit a tool's input schema, described so an agent can
200
+ * correct the call: a missing required argument, one of the wrong type, one
201
+ * the tool does not take. Null when they fit.
202
+ */
203
+ function invalidArguments(tool, args) {
204
+ const schema = tool.inputSchema ?? {};
205
+ const properties = schema.properties ?? {};
206
+ const example = (name) => (properties[name]?.type === "array" ? `["…"]` : properties[name]?.type === "boolean" ? "true" : `"…"`);
207
+ const typeOf = (value) => (Array.isArray(value) ? "array" : typeof value);
208
+ if (typeOf(args) !== "object" || args === null) return `${tool.name} takes an object of arguments.`;
209
+ for (const name of schema.required ?? []) {
210
+ if (args[name] === undefined) return `${tool.name} needs "${name}" (${properties[name]?.type ?? "a value"}), for example {"${name}": ${example(name)}}.`;
211
+ }
212
+ for (const [name, value] of Object.entries(args)) {
213
+ const expected = properties[name];
214
+ if (!expected) {
215
+ if (schema.additionalProperties === false) return `${tool.name} does not take "${name}". It takes: ${Object.keys(properties).join(", ") || "no arguments"}.`;
216
+ continue;
217
+ }
218
+ if (expected.type && typeOf(value) !== expected.type) return `"${name}" should be ${expected.type === "array" ? "an array" : `a ${expected.type}`}, not ${typeOf(value) === "array" ? "an array" : `a ${typeOf(value)}`}.`;
219
+ if (expected.enum && !expected.enum.includes(value)) return `"${name}" is one of: ${expected.enum.join(", ")}.`;
220
+ }
221
+ return null;
222
+ }
223
+
182
224
  /**
183
225
  * The protocol, apart from any transport: takes one JSON-RPC message and
184
226
  * resolves to the reply, or to null for a notification. The stdio server
@@ -208,6 +250,8 @@ export function createMcpHandler({ tools: available, listResources, readResource
208
250
  case "tools/call": {
209
251
  const tool = available.find((entry) => entry.name === params?.name);
210
252
  if (!tool) return fail(id, -32602, `Unknown tool ${params?.name}`);
253
+ const problem = invalidArguments(tool, params?.arguments ?? {});
254
+ if (problem) return reply(id, { ...text(problem), isError: true });
211
255
  try {
212
256
  return reply(id, await tool.handler(params?.arguments ?? {}));
213
257
  } catch (error) {
@@ -246,13 +290,14 @@ export function remoteMcpHandler({ version }) {
246
290
  const install = {
247
291
  name: "get_install_command",
248
292
  title: "How to install Mlola components",
249
- description: "The commands to run in the project to set Mlola up and add components, blocks, pages or templates, including the license step for Mlola Pro items.",
250
- inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" }, minItems: 1 } }, required: ["names"], additionalProperties: false },
293
+ description: "How to set Mlola up: the commands to run in a project to add components, blocks, pages or templates (with the license step for Mlola Pro items), and the stylesheet link and script for a page with no build step. Leave names out for the setup alone.",
294
+ inputSchema: { type: "object", properties: { names: { type: "array", items: { type: "string" }, description: "Item names from search_components, for example [\"button\", \"date-picker\"]." } }, additionalProperties: false },
251
295
  annotations: { readOnlyHint: true },
252
- handler: ({ names }) => {
296
+ handler: ({ names = [] }) => {
297
+ if (!names.length) return text(["In a project:", "", "npx mlola-ui init # once: config, stylesheet, the engine, and agent instructions", "npx mlola-ui add <names>", "", noBuildSetup(version)].join("\n"));
253
298
  const found = names.map((name) => searchItems({ query: name }).find((item) => item.name === name));
254
299
  const unknown = names.filter((_, index) => !found[index]);
255
- if (unknown.length) return { ...text(`No Mlola item is called ${unknown.join(", ")}. Find names with search_components.`), isError: true };
300
+ if (unknown.length) return { ...text(unknown.map((name) => `No Mlola item is called ${name}.${suggest(name)}`).join("\n") + " Find names with search_components."), isError: true };
256
301
  const pro = found.filter((item) => item.tier === "pro").map((item) => item.name);
257
302
  return text([
258
303
  "Run in the project:",
@@ -262,11 +307,13 @@ export function remoteMcpHandler({ version }) {
262
307
  `npx mlola-ui add ${names.join(" ")}`,
263
308
  "",
264
309
  'Then import styles/mlola/index.css once and set data-theme="graphite" (or atelier, machined, aerogel, nordic) on the root element.',
310
+ "",
311
+ noBuildSetup(version),
265
312
  ].join("\n"));
266
313
  },
267
314
  };
268
315
  return createMcpHandler({
269
- tools: [...readTools({ cwd }), install],
316
+ tools: [...readTools({ cwd, version }), install],
270
317
  listResources: () => [{ uri: "mlola://guide", name: "Mlola UI design guide", description: "Classes, attributes, tokens and rules, generated from the stylesheet.", mimeType: "text/markdown" }],
271
318
  readResource: (uri) => {
272
319
  if (uri === "mlola://guide") return designGuide() ?? "";
@@ -279,7 +326,7 @@ export function remoteMcpHandler({ version }) {
279
326
 
280
327
  export async function serveMcp({ cwd, run, version, input = process.stdin, output = process.stdout }) {
281
328
  const handle = createMcpHandler({
282
- tools: tools({ cwd, run }),
329
+ tools: tools({ cwd, run, version }),
283
330
  listResources: () => resources(cwd),
284
331
  readResource: (uri) => readResource(uri, cwd),
285
332
  version,