@golemui/gui-mcp 0.16.2 β 1.0.0-rc.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 +21 -0
- package/cli.js +1 -1
- package/{get-concept-CHgtnDXr.js β get-concept-ChE67L3a.js} +295 -29
- package/lib.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
## 0.17.0 (2026-06-08)
|
|
2
|
+
|
|
3
|
+
### π©Ή Fixes
|
|
4
|
+
|
|
5
|
+
- **mcp:** fill documentation gaps and deduplicate get_concept/get_widget_spec tools ([#153](https://github.com/golemui/golemui/pull/153))
|
|
6
|
+
|
|
7
|
+
### β€οΈ Thank You
|
|
8
|
+
|
|
9
|
+
- Mud Scientist @mudscientist
|
|
10
|
+
- RaΓΊl JimΓ©nez @Elecash
|
|
11
|
+
|
|
12
|
+
## 0.16.2 (2026-05-30)
|
|
13
|
+
|
|
14
|
+
### π©Ή Fixes
|
|
15
|
+
|
|
16
|
+
- **mcp:** add missing rollup-generated chunk files ([#147](https://github.com/golemui/golemui/pull/147))
|
|
17
|
+
|
|
18
|
+
### β€οΈ Thank You
|
|
19
|
+
|
|
20
|
+
- Mud Scientist @mudscientist
|
|
21
|
+
|
|
1
22
|
## 0.16.1 (2026-05-30)
|
|
2
23
|
|
|
3
24
|
### π 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-
|
|
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
|
|
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 = [
|
|
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 = [
|
|
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)
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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(
|
|
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: [
|
|
2162
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2488
|
-
|
|
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": "β¦"`)
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@golemui/gui-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-rc.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.
|
|
39
|
+
"@golemui/gui-schemas": "1.0.0-rc.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.0.0"
|