@golemui/gui-mcp 0.16.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 0.16.2 (2026-05-30)
2
+
3
+ ### 🩹 Fixes
4
+
5
+ - **mcp:** add missing rollup-generated chunk files ([#147](https://github.com/golemui/golemui/pull/147))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Mud Scientist @mudscientist
10
+
1
11
  ## 0.16.1 (2026-05-30)
2
12
 
3
13
  ### 🚀 Features
package/cli.js CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
8
- import { V as VALIDATE_FORM_DEFINITION_TOOL, G as GENERATE_FROM_JSON_SCHEMA_TOOL, b as GENERATE_FROM_OPENAPI_TOOL, d as GET_WIDGET_SPEC_TOOL, f as GET_CONCEPT_TOOL, e as getConcept, c as getWidgetSpec, a as generateFromOpenapi, g as generateFromJsonSchema, v as validateFormDefinition } from "./get-concept-CHgtnDXr.js";
8
+ import { V as VALIDATE_FORM_DEFINITION_TOOL, G as GENERATE_FROM_JSON_SCHEMA_TOOL, b as GENERATE_FROM_OPENAPI_TOOL, d as GET_WIDGET_SPEC_TOOL, f as GET_CONCEPT_TOOL, e as getConcept, c as getWidgetSpec, a as generateFromOpenapi, g as generateFromJsonSchema, v as validateFormDefinition } from "./get-concept-ChE67L3a.js";
9
9
  function readPackageMeta() {
10
10
  const here = dirname(fileURLToPath(import.meta.url));
11
11
  for (const candidate of [join(here, "package.json"), join(here, "..", "package.json")]) {
@@ -482,7 +482,7 @@ const $schema$6 = "https://json-schema.org/draft/2020-12/schema";
482
482
  const $id$6 = "https://golemui.com/schemas/components/repeater.schema.json";
483
483
  const title$6 = "Repeater Widget";
484
484
  const type$6 = "object";
485
- const $defs$6 = { "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "addLabelProp": { "description": "Label for the button that adds a new repeated item", "$ref": "../common.schema.json#/$defs/localizable" }, "removeLabelProp": { "description": "Label for the button that removes an existing repeated item", "$ref": "../common.schema.json#/$defs/localizable" }, "limitProp": { "type": "number", "description": "Maximum number of items the user can add. No limit when omitted." }, "templateProp": { "description": "Widget tree used as the template for each repeated item. Paths inside are resolved relative to the item's index in the array.", "$ref": "../layout-widget.schema.json" }, "titleProp": { "description": "Title displayed in the header of each item panel (supports reactive expressions referencing the item's index via `$index`)", "$ref": "../common.schema.json#/$defs/localizable" }, "addButtonIconProp": { "type": "string", "description": "Icon name or CSS class for the add-item button icon" }, "removeButtonIconProp": { "type": "string", "description": "Icon name or CSS class for the remove-item button icon" } };
485
+ const $defs$6 = { "labelDef": { "$ref": "../common.schema.json#/$defs/localizable" }, "addLabelProp": { "description": "Label for the button that adds a new repeated item", "$ref": "../common.schema.json#/$defs/localizable" }, "removeLabelProp": { "description": "Label for the button that removes an existing repeated item", "$ref": "../common.schema.json#/$defs/localizable" }, "limitProp": { "type": "number", "description": "Maximum number of items the user can add. No limit when omitted." }, "templateProp": { "description": "Widget tree used as the template for each repeated item. Paths inside are resolved relative to the item's index in the array.", "$ref": "../layout-widget.schema.json" }, "titleProp": { "description": "Title displayed in the header of each item panel.", "$ref": "../common.schema.json#/$defs/localizable" }, "addButtonIconProp": { "type": "string", "description": "Icon name or CSS class for the add-item button icon" }, "removeButtonIconProp": { "type": "string", "description": "Icon name or CSS class for the remove-item button icon" } };
486
486
  const allOf$6 = [{ "$ref": "../common.schema.json#/$defs/baseWidget" }];
487
487
  const properties$6 = { "kind": { "const": "input" }, "type": { "const": "repeater" }, "path": { "$ref": "../common.schema.json#/$defs/dotPath" }, "label": { "$ref": "#/$defs/labelDef" }, "on": { "$ref": "../common.schema.json#/$defs/on" }, "validator": { "$ref": "../validators.schema.json#/$defs/validator" }, "props": { "type": "object", "properties": { "addLabel": { "$ref": "#/$defs/addLabelProp" }, "removeLabel": { "$ref": "#/$defs/removeLabelProp" }, "limit": { "$ref": "#/$defs/limitProp" }, "template": { "$ref": "#/$defs/templateProp" }, "title": { "$ref": "#/$defs/titleProp" }, "addButtonIcon": { "$ref": "#/$defs/addButtonIconProp" }, "removeButtonIcon": { "$ref": "#/$defs/removeButtonIconProp" } }, "required": ["template"], "patternProperties": { "^addLabel\\.[^.]+$": { "$ref": "#/$defs/addLabelProp" }, "^removeLabel\\.[^.]+$": { "$ref": "#/$defs/removeLabelProp" }, "^limit\\.[^.]+$": { "$ref": "#/$defs/limitProp" }, "^template\\.[^.]+$": { "$ref": "#/$defs/templateProp" }, "^title\\.[^.]+$": { "$ref": "#/$defs/titleProp" }, "^addButtonIcon\\.[^.]+$": { "$ref": "#/$defs/addButtonIconProp" }, "^removeButtonIcon\\.[^.]+$": { "$ref": "#/$defs/removeButtonIconProp" } }, "additionalProperties": false } };
488
488
  const patternProperties$5 = { "^label\\.[^.]+$": { "$ref": "#/$defs/labelDef" }, "^validator\\.[^.]+$": { "$ref": "../validators.schema.json#/$defs/validator" } };
@@ -781,7 +781,18 @@ function rewriteRefs(node, baseId) {
781
781
  }
782
782
  for (const v of Object.values(obj)) rewriteRefs(v, baseId);
783
783
  }
784
- const STRING_FORMATS = ["email", "hostname", "ipv4", "ipv6", "url", "uuid", "date", "time", "date-time", "duration"];
784
+ const STRING_FORMATS = [
785
+ "email",
786
+ "hostname",
787
+ "ipv4",
788
+ "ipv6",
789
+ "url",
790
+ "uuid",
791
+ "date",
792
+ "time",
793
+ "date-time",
794
+ "duration"
795
+ ];
785
796
  const WIDGET_TYPES = Object.keys(COMPONENT_SCHEMAS);
786
797
  function levenshtein(a, b) {
787
798
  if (a === b) return 0;
@@ -817,7 +828,25 @@ function nearest(target, candidates) {
817
828
  }
818
829
  function suggestForAdditional(propertyName, instancePath) {
819
830
  if (/\/validator(\/|$)/.test(instancePath)) {
820
- const validatorKeys = ["type", "required", "minLength", "maxLength", "minimum", "maximum", "pattern", "format", "const", "enum", "messages", "minItems", "maxItems", "uniqueItems", "multipleOf", "exclusiveMinimum", "exclusiveMaximum"];
831
+ const validatorKeys = [
832
+ "type",
833
+ "required",
834
+ "minLength",
835
+ "maxLength",
836
+ "minimum",
837
+ "maximum",
838
+ "pattern",
839
+ "format",
840
+ "const",
841
+ "enum",
842
+ "messages",
843
+ "minItems",
844
+ "maxItems",
845
+ "uniqueItems",
846
+ "multipleOf",
847
+ "exclusiveMinimum",
848
+ "exclusiveMaximum"
849
+ ];
821
850
  const guess = nearest(propertyName, validatorKeys);
822
851
  if (guess && guess !== propertyName) return `Did you mean \`${guess}\`?`;
823
852
  return void 0;
@@ -829,7 +858,8 @@ function suggestForEnum(value, allowed) {
829
858
  const stringAllowed = allowed.filter((v) => typeof v === "string");
830
859
  if (!stringAllowed.length) return void 0;
831
860
  const guess = nearest(value, stringAllowed);
832
- if (guess) return `Did you mean \`${guess}\`? Allowed values: ${stringAllowed.map((v) => `\`${v}\``).join(", ")}.`;
861
+ if (guess)
862
+ return `Did you mean \`${guess}\`? Allowed values: ${stringAllowed.map((v) => `\`${v}\``).join(", ")}.`;
833
863
  return `Allowed values: ${stringAllowed.map((v) => `\`${v}\``).join(", ")}.`;
834
864
  }
835
865
  const VALIDATOR_TYPES = ["string", "number", "integer", "boolean", "array", "custom"];
@@ -959,15 +989,13 @@ function collectWidgetErrors(widget, widgetPath, out, warnings) {
959
989
  if (!intended) {
960
990
  const widgetType = widget?.type;
961
991
  if (typeof widgetType !== "string" || !widgetType) {
962
- out.push(
963
- {
964
- keyword: "required",
965
- instancePath: widgetPath,
966
- schemaPath: "",
967
- params: { missingProperty: "type" },
968
- message: `Widget at ${widgetPath} is missing or has an invalid \`type\``
969
- }
970
- );
992
+ out.push({
993
+ keyword: "required",
994
+ instancePath: widgetPath,
995
+ schemaPath: "",
996
+ params: { missingProperty: "type" },
997
+ message: `Widget at ${widgetPath} is missing or has an invalid \`type\``
998
+ });
971
999
  } else {
972
1000
  warnings.push({
973
1001
  path: `${widgetPath}/type`,
@@ -1805,7 +1833,9 @@ async function generateFromOpenapi(input) {
1805
1833
  async function fetchDoc(url) {
1806
1834
  const res = await fetch(url);
1807
1835
  if (!res.ok) {
1808
- throw new Error(`Failed to fetch OpenAPI document from ${url}: ${res.status} ${res.statusText}`);
1836
+ throw new Error(
1837
+ `Failed to fetch OpenAPI document from ${url}: ${res.status} ${res.statusText}`
1838
+ );
1809
1839
  }
1810
1840
  const text = await res.text();
1811
1841
  try {
@@ -2139,7 +2169,8 @@ const NOTES = {
2139
2169
  textinput: [
2140
2170
  "`path` is the dot-path into form data this field writes to.",
2141
2171
  "`validator.format` supports: `email`, `hostname`, `ipv4`, `ipv6`, `url`, `uuid`, `date`, `time`, `date-time`, `duration`.",
2142
- 'Root props `label`, `disabled`, `readonly`, `validator`, and `size` accept state suffixes — e.g. `"label.<stateName>": "New label"` overrides the label only when that named state is active. Props inside `props` (e.g. `hint`, `placeholder`) also accept suffixes as `"hint.<stateName>"`. Call `get_concept({ concept: "states" })` for the full pattern.'
2172
+ 'Root props `label`, `disabled`, `readonly`, `validator`, and `size` accept state suffixes — e.g. `"label.<stateName>": "New label"` overrides the label only when that named state is active. Props inside `props` (e.g. `hint`, `placeholder`) also accept suffixes as `"hint.<stateName>"`. Call `get_concept({ concept: "states" })` for the full pattern.',
2173
+ '`props.icon` accepts a Google Material Icons ligature name (e.g. `"search"`, `"email"`, `"lock"`) to display a decorative icon inside the input field. Supports state suffix: `"icon.<stateName>": "check"` to swap the icon when a state is active. Call `get_concept({ concept: "icons" })` for setup and the full list of icon-capable widgets.'
2143
2174
  ],
2144
2175
  markdownText: [
2145
2176
  "Display-only widget for rendering markdown. Can be used as a top-level form widget (inside any layout) or inside templates like `dropdown.props.items[].template`.",
@@ -2158,8 +2189,24 @@ const NOTES = {
2158
2189
  '`props.options[]` is the simple form: `[{ label: "United States", value: "us" }, ...]`. Use this for ANY plain text list — countries, plans, sizes, status enums. Only switch to `dropdown` if you need custom per-item rendering (icons, flags, rich layouts).',
2159
2190
  "For very large lists (>50 items) consider `dropdown` for its virtualization (`height`, `itemHeight`, `searchFields`)."
2160
2191
  ],
2161
- flex: ["`children` is an array of any widgets (inputs, displays, nested layouts)."],
2162
- grid: ["`children` is an array of any widgets. `props.columns` controls layout."],
2192
+ flex: [
2193
+ "Use `flex` for page scaffolding and directional grouping: a column stack of sections, a row of side-by-side panels, a row of action buttons. For field-level layout (inputs that should sit in columns with aligned labels), prefer `grid` instead.",
2194
+ '`props.direction`: `"row"` (default) | `"column"` | `"row-reverse"` | `"column-reverse"` — controls the main axis. `"column"` stacks children vertically; `"row"` places them side by side.',
2195
+ "`props.gap`: number (pixels) — uniform spacing between all children along the main axis.",
2196
+ '`props.justify`: `"center"` | `"start"` | `"end"` | `"stretch"` — aligns children along the cross-axis (perpendicular to `direction`).',
2197
+ '`props.align`: `"center"` | `"start"` | `"end"` | `"space-between"` | `"space-around"` | `"space-evenly"` — distributes children along the main axis.',
2198
+ 'All `flex` props support state suffixes: `"direction.<stateName>": "column"` swaps the direction when a state is active. Call `get_concept({ concept: "states" })` for the full pattern.'
2199
+ ],
2200
+ grid: [
2201
+ "Use `grid` when you want multiple form fields to sit side by side with their labels and inputs aligned across columns. GolemUI's `grid` uses CSS subgrid internally: each child widget gets two implicit sub-tracks (one for its label, one for its input) that align to the parent grid columns, giving consistent label/input alignment across all rows without manual sizing.",
2202
+ "For loose page scaffolding — stacking sections, wrapping a group in a header — use `flex` instead.",
2203
+ "`props.columnGap`: number (pixels) — horizontal gap between columns.",
2204
+ "`props.rowGap`: number (pixels) — vertical gap between rows.",
2205
+ "`props.autoFit`: boolean — when `true`, the grid auto-fits as many columns as will fit in the available container width, using `columnGap` as the gutter. Useful for responsive layouts where the number of columns should adapt to the viewport.",
2206
+ '`props.direction`: `"row"` | `"column"` — controls how children flow into the grid tracks.',
2207
+ "`props.align` and `props.justify` accept the same values as `flex`.",
2208
+ 'All `grid` props support state suffixes. Call `get_concept({ concept: "states" })` for the full pattern.'
2209
+ ],
2163
2210
  tabs: [
2164
2211
  "Children are associated with tabs by **`uid` matching**, not by array order: each direct child must have a `uid` field at the widget level (alongside `kind`/`type`) whose string value equals one of the `props.tabs[].uid` entries. There is no `tag` property — that is not a real GolemUI field.",
2165
2212
  "Children are typically written in display order for readability, but rearranging them does not change which tab they belong to — only the `uid` string match does."
@@ -2177,7 +2224,8 @@ const NOTES = {
2177
2224
  button: [
2178
2225
  "`actionType` controls the button's role. `actionType: \"submit\"` makes the button fire the form's `formSubmit` event natively — the host listens for it via `(formSubmit)` (Angular), `@formSubmit` (Vue), `onFormSubmit` (React), or the `form-submit` event (Lit). No custom handler needed. Use this for the primary submit button on a form.",
2179
2226
  '`actionType: "button"` (the default, can be omitted) is a regular action button. Wire it via `on.click: "<handlerName>"` where `<handlerName>` is registered in the form config\'s event handlers.',
2180
- 'Supports state-suffixed props: `"label.<stateName>"` and `"disabled.<stateName>"` swap the label or disabled state when a named state is active — e.g. disable the submit button until terms are accepted, then re-enable it. Call `get_concept({ concept: "states" })` for the full pattern.'
2227
+ 'Supports state-suffixed props: `"label.<stateName>"` and `"disabled.<stateName>"` swap the label or disabled state when a named state is active — e.g. disable the submit button until terms are accepted, then re-enable it. Call `get_concept({ concept: "states" })` for the full pattern.',
2228
+ '`props.icon` accepts a Google Material Icons ligature name (e.g. `"send"`, `"check"`, `"arrow_forward"`) to render an icon on the button. `props.iconPosition` controls placement: `"left"` (default) or `"right"`. Both support state suffixes: `"icon.<stateName>": "hourglass_empty"` swaps the icon while a state is active. Call `get_concept({ concept: "icons" })` for setup instructions and the full list of icon-capable widgets.'
2181
2229
  ],
2182
2230
  checkbox: ["Set `validator.const: true` to require the user to tick it (e.g. terms acceptance)."],
2183
2231
  alert: [
@@ -2229,7 +2277,7 @@ const STATES_CONCEPT = {
2229
2277
  patterns: [
2230
2278
  {
2231
2279
  name: "Declare states at the form root",
2232
- description: 'Add a `"states"` object to the top-level form definition. Each key is a state name; each value is a reactive expression string. Expressions are evaluated at runtime — they have access to `$form` (all current form values), `$meta` (host-supplied metadata), and `$formIsInvalid` (built-in boolean — `true` when any field currently fails validation). State names can contain letters, numbers, hyphens, and underscores. Colons enable hierarchical composition — see the "Composed sub-states (colon notation)" pattern below.',
2280
+ description: 'Add a `"states"` object to the top-level form definition. Each key is a state name; each value is a reactive expression string. Expressions are evaluated at runtime — they have access to `$form` (all current form values), `$meta` (host-supplied metadata), and `$formIsInvalid`. State names can contain letters, numbers, hyphens, and underscores. Colons enable hierarchical composition — see the "Composed sub-states (colon notation)" pattern below.',
2233
2281
  example: {
2234
2282
  $schema: "https://golemui.com/schemas/form.schema.json",
2235
2283
  states: {
@@ -2261,8 +2309,8 @@ const STATES_CONCEPT = {
2261
2309
  }
2262
2310
  },
2263
2311
  {
2264
- name: "Conditional rendering with include / exclude",
2265
- description: 'Use `"include": { "in": ["stateName"] }` on any widget to render it only when the named state is active. Use `"exclude": { "from": ["stateName"] }` to render it only when the state is NOT active. Both `in` and `from` are arrays — a widget can be gated on multiple states simultaneously. Prefer the named-state form (`in`/`from`) over the inline `when` expression when the same condition applies to several widgets; use `when` for one-off conditions with no reuse.',
2312
+ name: "Conditional rendering with include / exclude (named-state form)",
2313
+ description: 'Use `"include": { "in": ["stateName"] }` on any widget to render it only when the named state is active. Use `"exclude": { "from": ["stateName"] }` to render it only when the state is NOT active. Both `in` and `from` are arrays — a widget can be gated on multiple states simultaneously.',
2266
2314
  example: {
2267
2315
  $schema: "https://golemui.com/schemas/form.schema.json",
2268
2316
  states: {
@@ -2291,6 +2339,37 @@ const STATES_CONCEPT = {
2291
2339
  ]
2292
2340
  }
2293
2341
  },
2342
+ {
2343
+ name: "Inline conditional rendering with `when`",
2344
+ description: 'For a one-off condition that applies to a single widget only, use the inline `when` form: `"include": { "when": "expression" }` or `"exclude": { "when": "expression" }`. The expression is evaluated exactly like a state expression — it has access to `$form`, `$meta`, and `$formIsInvalid`, uses the same safe operator subset, and must use optional chaining for nested fields. No entry in the root `states` map is required. Use this form when the condition is not shared with any other widget; use the named-state form (`in`/`from`) when the same condition gates two or more widgets.',
2345
+ example: {
2346
+ $schema: "https://golemui.com/schemas/form.schema.json",
2347
+ form: [
2348
+ {
2349
+ kind: "input",
2350
+ type: "checkbox",
2351
+ path: "agreeTerms",
2352
+ label: "I agree to the terms"
2353
+ },
2354
+ {
2355
+ kind: "action",
2356
+ type: "button",
2357
+ actionType: "submit",
2358
+ label: "Continue",
2359
+ // Show only after terms are accepted — one-off condition, no state needed.
2360
+ include: { when: "$form.agreeTerms === true" }
2361
+ },
2362
+ {
2363
+ kind: "input",
2364
+ type: "textinput",
2365
+ path: "companyName",
2366
+ label: "Company name",
2367
+ // Hide when the user selects "individual" account type — one-off condition.
2368
+ exclude: { when: "$form.accountType === 'individual'" }
2369
+ }
2370
+ ]
2371
+ }
2372
+ },
2294
2373
  {
2295
2374
  name: "State-suffixed props",
2296
2375
  description: 'Override individual widget properties when a named state is active by appending `".<stateName>"` to the property key. The unsuffixed key holds the default value; each suffixed key supplies the override for that state. This works on root-level widget properties (`label`, `disabled`, `readonly`, `validator`, `size`) AND on any key inside the `props` object (`hint`, `placeholder`, `items`, `addLabel`, etc.). There is NO inline `when` equivalent for this — state-suffixed props REQUIRE a named state. Multiple suffixes can coexist on the same property; when more than one state is active, the last matching suffix in document order wins.',
@@ -2357,10 +2436,9 @@ const STATES_CONCEPT = {
2357
2436
  "State-suffixed root props — only these support suffixes at the widget root level: `label`, `disabled`, `readonly`, `validator`, `size`. All other overridable properties live inside `props`.",
2358
2437
  'State-suffixed props inside `props` — any key inside the `props` object can be suffixed: `"hint.<state>"`, `"placeholder.<state>"`, `"items.<state>"`, `"addLabel.<state>"`, etc.',
2359
2438
  'Suffix names must not contain dots (the dot is the separator between property and state name): `"label.myState"` ✅, `"label.register:adult"` ✅ — `"label.my.state"` ❌.',
2360
- 'Reactive expressions must reference `$form`, `$meta`, or `$formIsInvalid`. A bare identifier like `termsAccepted` without a root reference is invalid. `$formIsInvalid` is a built-in boolean (no property chain — use it as-is: `disabled: { when: "$formIsInvalid" }` or inside a state expression: `states: { formInvalid: "$formIsInvalid" }`).',
2439
+ "Reactive expressions must reference `$form`, `$meta`, or `$formIsInvalid`. A bare identifier like `termsAccepted` without a root reference is invalid.",
2361
2440
  "Use `===` / `!==` for equality, `&&` / `||` for logic. Avoid `=` (assignment), `==`/`!=` (loose equality), or bitwise `&`/`|`.",
2362
2441
  'When multiple states are active at the same time and a property has more than one matching suffix, the longest state name wins (most specific takes priority). Example: if both `register` and `register:adult` are active, `"label.register:adult"` overrides `"label.register"`.',
2363
- "The `include.when` / `exclude.when` inline form is an alternative to named states for one-off conditions, but it cannot replace state-suffixed props — those require a named state.",
2364
2442
  '`include.in` and `exclude.from` each accept an Array of state names. A widget included `in: ["a", "b"]` renders when state `a` OR state `b` is active.',
2365
2443
  "Use optional chaining (`?.`) when accessing nested fields that may not yet exist in the form data: `$form.user?.age >= 18` not `$form.user.age >= 18`."
2366
2444
  ]
@@ -2475,8 +2553,7 @@ const STRING_INTERPOLATION_CONCEPT = {
2475
2553
  }
2476
2554
  ],
2477
2555
  rules: [
2478
- "Slots must reference at least one of `$form`, `$meta`, `$errors`, or `$formIsInvalid`. A bare identifier without a scope prefix is invalid inside `{{}}`: use `{{$form.name}}` not `{{name}}`.",
2479
- "`$formIsInvalid` is a built-in boolean — use it as-is: `{{$formIsInvalid}}`. Do not chain properties onto it.",
2556
+ "Slots must reference at least one of `$form`, `$meta`, `$errors`, or `$formIsInvalid`. A bare identifier without a scope prefix is invalid inside `{{}}`: use `{{$form.name}}` not `{{name}}`. `$formIsInvalid` is a bare boolean — do not chain properties onto it.",
2480
2557
  "If an expression evaluates to `null` or `undefined`, the slot renders as an empty string in display text.",
2481
2558
  "Use optional chaining (`?.`) when accessing nested fields that may not yet exist: `{{$form.address?.city}}` not `{{$form.address.city}}`.",
2482
2559
  "Do not use assignment `=` inside a slot — slots are read-only. Use `===` for equality checks.",
@@ -2484,13 +2561,202 @@ const STRING_INTERPOLATION_CONCEPT = {
2484
2561
  "Every `{{` must have a matching `}}`. Unbalanced delimiters cause a lint warning.",
2485
2562
  'i18n `params` values are bare expressions — do NOT wrap them in `{{}}`. Write `"$form.name"` not `"{{$form.name}}"`.',
2486
2563
  'Static string params (not starting with `$`) are passed through as-is — use them for constant values like `"Hola"` or `"px"`.',
2487
- "Supported operators in expressions: arithmetic (`+`, `-`, `*`, `/`, `%`), comparison (`===`, `!==`, `<`, `>`, `<=`, `>=`), logical (`&&`, `||`, `!`), ternary (`? :`), optional chaining (`?.`), nullish coalescing (`??`).",
2488
- "Expressions are evaluated using a safe subset of JavaScript — no `eval`, no function calls, no side effects."
2564
+ 'The expression language is the same as reactive expressions — see `get_concept({ concept: "reactive-scope" })` for the operator reference and safe-expression constraints.'
2565
+ ]
2566
+ };
2567
+ const REACTIVE_SCOPE_CONCEPT = {
2568
+ concept: "reactive-scope",
2569
+ summary: "GolemUI reactive expressions (used in `states`, `include.when`, `exclude.when`, and `{{}}` template slots) share a common read-only scope object. The scope exposes four variables: `$form` (live form data), `$meta` (host-supplied metadata), `$errors` (validation error messages), and `$formIsInvalid` (built-in boolean). All variables are read-only — expressions can only read from them, never write to them.",
2570
+ patterns: [
2571
+ {
2572
+ name: "$form — live form data",
2573
+ description: '`$form` is a plain object whose keys are the field paths currently registered in the form. Each key equals the `path` property of an input widget. A simple field `path: "firstName"` is accessed as `$form.firstName`. A nested field `path: "address.city"` is accessed as `$form.address?.city` — the dot segments in the path become dot-property accesses, and optional chaining (`?.`) is required at each intermediate segment because the parent object may not yet exist while the user is filling in the form. `$form` itself is always a defined object; only leaf values may be `undefined` before the user fills them in.',
2574
+ example: {
2575
+ states: {
2576
+ // path: "firstName" -> $form.firstName
2577
+ hasName: '$form.firstName !== undefined && $form.firstName !== ""',
2578
+ // path: "address.city" -> $form.address?.city (optional chaining at "address" segment)
2579
+ hasCity: '$form.address?.city !== undefined && $form.address?.city !== ""',
2580
+ // path: "user.profile.age" -> $form.user?.profile?.age
2581
+ isAdult: "$form.user?.profile?.age >= 18"
2582
+ }
2583
+ }
2584
+ },
2585
+ {
2586
+ name: "$meta — host-supplied metadata",
2587
+ description: "`$meta` is a free-form key/value object supplied by the host application at mount time. It is useful for passing server-side flags, the current user's role, locale, or any context that is not part of the form data itself. The host sets the `meta` prop on the GolemUI form component; the MCP tool has no knowledge of which keys the host will supply. Access values with `$meta.keyName`. Use optional chaining if a key may not always be present.",
2588
+ example: {
2589
+ states: {
2590
+ isAdmin: "$meta.role === 'admin'",
2591
+ isOnline: '$meta.connectionStatus === "online"'
2592
+ },
2593
+ form: [
2594
+ {
2595
+ kind: "input",
2596
+ type: "textinput",
2597
+ path: "apiKey",
2598
+ label: "API key",
2599
+ // Show only for admin users — the role comes from $meta, not from $form.
2600
+ include: { when: "$meta.role === 'admin'" }
2601
+ }
2602
+ ]
2603
+ }
2604
+ },
2605
+ {
2606
+ name: "$errors — validation error messages",
2607
+ description: '`$errors` is a plain object keyed by widget `uid`. `$errors.myField` is the current validation error message string for the widget with `uid: "myField"`, or `undefined` if that field is currently valid. Use it to display a field\'s error message inside another widget (e.g. an `alert`). Note: a widget must have an explicit `uid` value set for its errors to appear in `$errors`; auto-generated uids are not predictable.',
2608
+ example: {
2609
+ $schema: "https://golemui.com/schemas/form.schema.json",
2610
+ form: [
2611
+ {
2612
+ uid: "emailField",
2613
+ kind: "input",
2614
+ type: "textinput",
2615
+ path: "email",
2616
+ label: "Email",
2617
+ validator: { type: "string", required: true, format: "email" }
2618
+ },
2619
+ {
2620
+ kind: "display",
2621
+ type: "alert",
2622
+ props: {
2623
+ level: "error",
2624
+ // Render the live validation error message for the emailField widget.
2625
+ text: "{{$errors.emailField}}"
2626
+ },
2627
+ // Only show this alert when the field actually has an error.
2628
+ include: { when: "$errors.emailField !== undefined" }
2629
+ }
2630
+ ]
2631
+ }
2632
+ },
2633
+ {
2634
+ name: "$formIsInvalid — whole-form validity flag",
2635
+ description: "`$formIsInvalid` is a built-in boolean that the runtime maintains automatically. It is `true` when ANY field in the form currently fails its validator; `false` otherwise. Use it to disable the submit button, show a banner, or guard a navigation step. It is NOT a property of `$form` — use it as a bare identifier. Do NOT chain properties onto it: `$formIsInvalid.something` is invalid.",
2636
+ example: {
2637
+ $schema: "https://golemui.com/schemas/form.schema.json",
2638
+ form: [
2639
+ {
2640
+ kind: "action",
2641
+ type: "button",
2642
+ actionType: "submit",
2643
+ label: "Submit",
2644
+ // Disable the submit button while any field is invalid.
2645
+ disabled: true,
2646
+ "disabled.formValid": false
2647
+ },
2648
+ {
2649
+ kind: "display",
2650
+ type: "alert",
2651
+ props: { level: "warning", text: "Please fix the errors above before submitting." },
2652
+ include: { when: "$formIsInvalid" }
2653
+ }
2654
+ ],
2655
+ states: {
2656
+ formValid: "!$formIsInvalid"
2657
+ }
2658
+ }
2659
+ }
2660
+ ],
2661
+ rules: [
2662
+ "All four scope variables are read-only. Expressions may only read values, never assign them.",
2663
+ "`$form` is always a defined object. Its leaf values (`$form.someField`) may be `undefined` until the user fills them in.",
2664
+ "Use optional chaining (`?.`) at every intermediate segment of a nested path. `$form.address?.city` is safe; `$form.address.city` throws if `address` is undefined.",
2665
+ 'A widget\'s `path` value maps directly to a `$form` key path using the same dot segments. `path: "shipping.address.zip"` -> `$form.shipping?.address?.zip`.',
2666
+ '`$formIsInvalid` is a built-in bare boolean — use it directly: `disabled: { when: "$formIsInvalid" }` or `states: { formValid: "!$formIsInvalid" }`. Never chain: `$formIsInvalid.value` is invalid.',
2667
+ "`$errors` keys are widget `uid` values, not field path values. A widget must have an explicit `uid` for its errors to be addressable.",
2668
+ "`$meta` keys are arbitrary — they depend entirely on what the host application passes to the form component. Use optional chaining if a key may be absent.",
2669
+ "Supported operators across all expressions: arithmetic (`+`, `-`, `*`, `/`, `%`), comparison (`===`, `!==`, `<`, `>`, `<=`, `>=`), logical (`&&`, `||`, `!`), ternary (`? :`), optional chaining (`?.`), nullish coalescing (`??`).",
2670
+ "Use `===` / `!==` for equality. Avoid `==` / `!=` (loose equality causes unexpected coercions with `undefined`).",
2671
+ "No function calls, no `eval`, no side effects. The expression must be a pure read."
2672
+ ]
2673
+ };
2674
+ const ICONS_CONCEPT = {
2675
+ concept: "icons",
2676
+ summary: 'GolemUI uses Google Material Icons for all icon props. An icon value is a Material Icons ligature name — a lowercase string with underscores, such as `"search"`, `"home"`, `"arrow_forward"`, or `"check_circle"`. The icon font must be loaded by the host page; without it, the icon value renders as raw text. Several widgets accept a `props.icon` property; `button` additionally has `props.iconPosition`. All icon props support state suffixes for reactive icon swapping.',
2677
+ patterns: [
2678
+ {
2679
+ name: "Widget icon props",
2680
+ description: "The following widgets accept `props.icon` (a Material Icons ligature name string): `button`, `textinput`, `password`, `select`, `currency`, `tags`, `dateInput`, `datePicker`, `rangeCalendar`, `rangedateinput`, `rangedatepicker`. The `button` widget additionally accepts `props.iconPosition` to control where the icon appears relative to the label. All other icon-capable widgets render the icon at a default position determined by the component.",
2681
+ example: {
2682
+ $schema: "https://golemui.com/schemas/form.schema.json",
2683
+ form: [
2684
+ {
2685
+ kind: "action",
2686
+ type: "button",
2687
+ actionType: "submit",
2688
+ label: "Send",
2689
+ props: {
2690
+ // "send" is the Google Material Icons ligature name for the send icon.
2691
+ icon: "send",
2692
+ iconPosition: "right"
2693
+ }
2694
+ },
2695
+ {
2696
+ kind: "input",
2697
+ type: "textinput",
2698
+ path: "email",
2699
+ label: "Email",
2700
+ props: {
2701
+ icon: "email",
2702
+ placeholder: "you@example.com"
2703
+ }
2704
+ },
2705
+ {
2706
+ kind: "input",
2707
+ type: "select",
2708
+ path: "country",
2709
+ label: "Country",
2710
+ props: {
2711
+ icon: "public",
2712
+ options: [
2713
+ { label: "United States", value: "us" },
2714
+ { label: "Canada", value: "ca" }
2715
+ ]
2716
+ }
2717
+ }
2718
+ ]
2719
+ }
2720
+ },
2721
+ {
2722
+ name: "Reactive icon swapping with state suffixes",
2723
+ description: 'Like other widget props, `icon` supports state suffixes: `"icon.<stateName>": "checkName"` swaps the icon when the named state is active. This is useful for toggling between a default and a confirmation icon, or for indicating a loading state on a button.',
2724
+ example: {
2725
+ $schema: "https://golemui.com/schemas/form.schema.json",
2726
+ states: {
2727
+ submitted: "$meta.submitting === true"
2728
+ },
2729
+ form: [
2730
+ {
2731
+ kind: "action",
2732
+ type: "button",
2733
+ actionType: "submit",
2734
+ label: "Submit",
2735
+ "label.submitted": "Submitting...",
2736
+ props: {
2737
+ icon: "send",
2738
+ // Swap to a spinner/hourglass icon while the form is being submitted.
2739
+ "icon.submitted": "hourglass_empty"
2740
+ }
2741
+ }
2742
+ ]
2743
+ }
2744
+ }
2745
+ ],
2746
+ rules: [
2747
+ 'Icon values are Google Material Icons ligature names — lowercase strings, words separated by underscores: `"search"`, `"arrow_forward"`, `"check_circle"`. Wrong casing or spaces will not render correctly.',
2748
+ 'The Material Icons font MUST be loaded in the host page\'s `<head>`. Without it, `props.icon` renders as the raw string (e.g. the word "search") rather than an icon glyph.',
2749
+ "Widgets that support `props.icon`: `button`, `textinput`, `password`, `select`, `currency`, `tags`, `dateInput`, `datePicker`, `rangeCalendar`, `rangedateinput`, `rangedatepicker`.",
2750
+ '`button` is the only widget with `props.iconPosition`. Allowed values: `"left"` (default) and `"right"`.',
2751
+ 'All icon props support state suffixes: `"icon.<stateName>": "check"` swaps the icon when that state is active. Call `get_concept({ concept: "states" })` for the full state-suffix pattern.',
2752
+ "Do not set `props.icon` on widgets that do not support it — the schema will reject it and `validate_form_definition` will report an error."
2489
2753
  ]
2490
2754
  };
2491
2755
  const CONCEPTS = {
2492
2756
  states: STATES_CONCEPT,
2493
- "string-interpolation": STRING_INTERPOLATION_CONCEPT
2757
+ "string-interpolation": STRING_INTERPOLATION_CONCEPT,
2758
+ "reactive-scope": REACTIVE_SCOPE_CONCEPT,
2759
+ icons: ICONS_CONCEPT
2494
2760
  };
2495
2761
  function getConcept(input) {
2496
2762
  const result = CONCEPTS[input.concept];
@@ -2502,7 +2768,7 @@ function getConcept(input) {
2502
2768
  }
2503
2769
  const GET_CONCEPT_TOOL = {
2504
2770
  name: "get_concept",
2505
- description: 'Return a detailed guide for a cross-cutting GolemUI form concept — things that span multiple widgets and affect the whole form, rather than the API of a single widget. Call this when you need to: (1) change a widget\'s props based on form state (state-suffixed props like `"label.stateName": "…"`), or (2) reuse the same condition across multiple widgets (`include: { in: […] }` / `exclude: { from: […] }`). For a one-off show/hide on a single widget, use `include: { when: "…" }` or `exclude: { when: "…" }` directly — no states needed, no need to call this tool. Currently supported concepts: `states`, `string-interpolation`.',
2771
+ description: 'Return a detailed guide for a cross-cutting GolemUI form concept — things that span multiple widgets and affect the whole form, rather than the API of a single widget. Call this when you need to: (1) conditionally show or hide widgets (`include`/`exclude`) — both the named-state form (`in`/`from`) and the inline `when` expression form are covered under the `states` concept; (2) change a widget\'s props based on form state (state-suffixed props like `"label.stateName": "…"`); (3) understand what `$form`, `$meta`, `$errors`, and `$formIsInvalid` are and how to reference form data in reactive expressions — use the `reactive-scope` concept; (4) add icons to widgets — use the `icons` concept. Currently supported concepts: `states`, `string-interpolation`, `reactive-scope`, `icons`.',
2506
2772
  inputSchema: {
2507
2773
  type: "object",
2508
2774
  properties: {
package/lib.js CHANGED
@@ -1,4 +1,4 @@
1
- import { G, b, f, d, V, g, a, e, c, v } from "./get-concept-CHgtnDXr.js";
1
+ import { G, b, f, d, V, g, a, e, c, v } from "./get-concept-ChE67L3a.js";
2
2
  export {
3
3
  G as GENERATE_FROM_JSON_SCHEMA_TOOL,
4
4
  b as GENERATE_FROM_OPENAPI_TOOL,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golemui/gui-mcp",
3
- "version": "0.16.2",
3
+ "version": "0.17.0",
4
4
  "description": "Model Context Protocol server for GolemUI — gives AI coding assistants deterministic schema validation and form generation for GolemUI form definitions.",
5
5
  "type": "module",
6
6
  "main": "./lib.js",
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "ajv": "^8.17.1",
38
38
  "ajv-formats": "^3.0.1",
39
- "@golemui/gui-schemas": "0.16.2"
39
+ "@golemui/gui-schemas": "0.17.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@modelcontextprotocol/sdk": "^1.0.0"