@webiny/mcp 6.3.0-beta.1 → 6.3.0-beta.3

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/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@webiny/mcp",
3
- "version": "6.3.0-beta.1",
3
+ "version": "6.3.0-beta.3",
4
4
  "type": "module",
5
- "main": "./index.js",
5
+ "exports": {
6
+ ".": "./index.js",
7
+ "./*": "./*"
8
+ },
6
9
  "bin": {
7
10
  "webiny-mcp": "./bin.js"
8
11
  },
@@ -25,7 +28,7 @@
25
28
  "devDependencies": {
26
29
  "@types/lodash": "4.17.24",
27
30
  "@types/ncp": "2.0.8",
28
- "@webiny/build-tools": "6.3.0-beta.1",
31
+ "@webiny/build-tools": "6.3.0-beta.3",
29
32
  "execa": "5.1.1",
30
33
  "tsx": "4.21.0",
31
34
  "typescript": "6.0.3"
@@ -33,5 +36,5 @@
33
36
  "scripts": {
34
37
  "prepublishOnly": "bash ./prepublishOnly.sh"
35
38
  },
36
- "gitHead": "664b273a9f0a971f9ca7e6ffe920db77fefdced1"
39
+ "gitHead": "e154ec3326903876c357d35422dc60d29e061419"
37
40
  }
@@ -0,0 +1,527 @@
1
+ ---
2
+ name: webiny-form-model
3
+ context: webiny-admin
4
+ description: >
5
+ Building forms with the FormModel system — field types, renderers, layout, validation,
6
+ conditional rules, computed fields, and dynamic zones. Use this skill when the developer
7
+ needs to define form fields with the builder API, choose renderers, build layouts with
8
+ tabs/rows/separators, add validation (Zod or imperative), use conditional visibility/disable
9
+ rules, create computed fields, or work with object fields and templates (dynamic zones).
10
+ ---
11
+
12
+ # Form Model
13
+
14
+ ## TL;DR
15
+
16
+ The Form Model is Webiny's declarative form system. Define fields with a fluent builder API (`fields.text()`, `fields.datetime()`, etc.), arrange them with a layout builder (`layout.row()`, `layout.tabs()`, etc.), and validate with Zod schemas or imperative rules. Fields support conditional visibility, computed values, and deeply nested object/list structures with templates (dynamic zones).
17
+
18
+ ## Field Types
19
+
20
+ All fields are created via the `fields` registry callback. Each builder method returns a chainable builder.
21
+
22
+ ### Text
23
+
24
+ ```typescript
25
+ fields.text();
26
+ ```
27
+
28
+ Default renderer: `textInput`. Value: `string | null`.
29
+
30
+ ```typescript
31
+ fields.text().label("Title").placeholder("Enter title").required("Title is required")
32
+ fields.text().renderer("textarea", { rows: 4 })
33
+ fields.text().list().renderer("tags").defaultValue([])
34
+ fields.text().list().renderer("textInputs", { addItemLabel: "Add text" })
35
+ fields.text().list().renderer("textareas", { addItemLabel: "Add description" })
36
+ fields.text().renderer("codeEditor", { language: "html", height: 300 })
37
+ fields.text().options([
38
+ { label: "Option A", value: "a" },
39
+ { label: "Option B", value: "b" }
40
+ ]) // auto-switches to "dropdown" renderer
41
+ fields.text().options([...]).renderer("radioButtons")
42
+ fields.text().list().options([...]).renderer("checkboxes")
43
+ ```
44
+
45
+ ### Number
46
+
47
+ ```typescript
48
+ fields.number();
49
+ ```
50
+
51
+ Default renderer: `numberInput`. Value: `number | null`. Auto-normalizes to number.
52
+
53
+ ```typescript
54
+ fields.number().label("Count").placeholder("0").required();
55
+ fields.number().list().renderer("numberInputs", { addItemLabel: "Add number" });
56
+ fields.number().options([
57
+ { label: "Tier 1", value: 100 },
58
+ { label: "Tier 2", value: 200 }
59
+ ]);
60
+ ```
61
+
62
+ ### Boolean
63
+
64
+ ```typescript
65
+ fields.boolean();
66
+ ```
67
+
68
+ Default renderer: `switch`. Value: `boolean | null`.
69
+
70
+ ```typescript
71
+ fields.boolean().label("Featured").defaultValue(false);
72
+ ```
73
+
74
+ ### DateTime
75
+
76
+ ```typescript
77
+ fields.datetime();
78
+ ```
79
+
80
+ Default renderer: `dateTimeInput`. Pick a variant method to set the subtype:
81
+
82
+ | Variant | Value Format | Example |
83
+ | ----------------------------------------- | ------------------------------ | ----------------------- |
84
+ | `.dateOnly()` | `"2026-05-01"` | Birthdays, due dates |
85
+ | `.timeOnly()` | `"14:30:00"` | Opening hours |
86
+ | `.withTimezone()` | `"2026-05-01T14:30:00+02:00"` | Events tied to a locale |
87
+ | `.withoutTimezone()` | `"2026-05-01T14:30:00.000Z"` | Timestamps, logs |
88
+ | `.monthOnly()` | `"2026-05"` | Billing cycles |
89
+ | `.weekOnly({ startsOn: 1 })` | `"2026-W18"` | Sprint planning |
90
+ | `.yearOnly({ range: [2020, 2035] })` | `2026` (number) | Fiscal years |
91
+ | `.dateRange()` | `{ from: "...", to: "..." }` | Vacation requests |
92
+ | `.multipleDates()` | `["2026-05-01", "2026-05-03"]` | Blackout dates |
93
+ | `.multipleMonths()` | `["2026-01", "2026-03"]` | Seasonal availability |
94
+ | `.multipleYears({ range: [2020, 2035] })` | `[2024, 2025, 2026]` | Multi-year budgets |
95
+
96
+ Additional chainable methods:
97
+
98
+ ```typescript
99
+ .presets([
100
+ { label: "Today", value: () => new Date() },
101
+ { label: "In a week", value: () => addDays(new Date(), 7) }
102
+ ])
103
+ .displayFormat("dd/MM/yyyy") // date-fns format tokens
104
+ .list() // switches renderer to "dateTimeInputs"
105
+ ```
106
+
107
+ ### File
108
+
109
+ ```typescript
110
+ fields.file();
111
+ ```
112
+
113
+ Default renderer: `filePicker`. Value: `FileValue | null` (object with `id`, `name`, `size`, `mimeType`, `src`, `width`, `height`).
114
+
115
+ ```typescript
116
+ fields.file().label("Image");
117
+ ```
118
+
119
+ ### File URL
120
+
121
+ ```typescript
122
+ fields.fileUrl();
123
+ ```
124
+
125
+ Default renderer: `fileUrlPicker`. Value: `string | null` (URL only).
126
+
127
+ ```typescript
128
+ fields.fileUrl().label("Image URL");
129
+ ```
130
+
131
+ ### Object
132
+
133
+ ```typescript
134
+ fields.object();
135
+ ```
136
+
137
+ Default renderer: `objectAccordionSingle`. For nested structures, lists, and dynamic zones.
138
+
139
+ ```typescript
140
+ // Simple nested object
141
+ fields.object().label("Address").fields(f => ({
142
+ street: f.text().label("Street"),
143
+ city: f.text().label("City"),
144
+ zip: f.text().label("ZIP")
145
+ }))
146
+
147
+ // List of objects
148
+ fields.object().list().label("Authors").fields(f => ({
149
+ name: f.text().label("Name").required(),
150
+ email: f.text().label("Email")
151
+ }))
152
+
153
+ // Dynamic zone (single template selection)
154
+ fields.object().label("Content Block")
155
+ .template("hero", t => {
156
+ t.label("Hero Banner")
157
+ .icon({ type: "icon", name: "fas/image" })
158
+ .fields(f => ({
159
+ heading: f.text().label("Heading").required(),
160
+ image: f.file().label("Image")
161
+ }));
162
+ })
163
+ .template("text", t => {
164
+ t.label("Rich Text").fields(f => ({
165
+ body: f.text().label("Body").renderer("textarea")
166
+ }));
167
+ })
168
+
169
+ // Dynamic zone list (multiple items, each picks a template)
170
+ fields.object().list().label("Page Sections")
171
+ .renderer("dynamicZone", { container: false })
172
+ .template("hero", t => { ... })
173
+ .template("cta", t => { ... })
174
+
175
+ // Key-value list
176
+ fields.object().list().label("Meta Tags")
177
+ .renderer("keyValueTags", { addItemLabel: "Add tag" })
178
+ .fields(f => ({
179
+ name: f.text().placeholder("Name"),
180
+ content: f.text().placeholder("Content")
181
+ }))
182
+ ```
183
+
184
+ Template visibility can be conditional:
185
+
186
+ ```typescript
187
+ .template("premium", t => {
188
+ t.label("Premium Widget")
189
+ .visible(form => form.field("plan").getValue() === "enterprise")
190
+ .fields(f => ({ ... }));
191
+ })
192
+ ```
193
+
194
+ ## Common Builder Methods
195
+
196
+ These are available on **all** field types:
197
+
198
+ | Method | Description |
199
+ | ----------------------------- | --------------------------------------------------------- |
200
+ | `.label(text)` | Field label |
201
+ | `.description(text)` | Description text below the field |
202
+ | `.help(text)` | Help text |
203
+ | `.note(text)` | Supplementary note |
204
+ | `.placeholder(text)` | Input placeholder |
205
+ | `.defaultValue(value)` | Default value (can be a function for dynamic defaults) |
206
+ | `.required(message?)` | Mark as required |
207
+ | `.requiredWhen(fn, message?)` | Conditionally required based on other field values |
208
+ | `.schema(zodSchema)` | Zod validation schema |
209
+ | `.renderer(name, settings?)` | Override the default renderer |
210
+ | `.options([...])` | Add value options (auto-switches text/number to dropdown) |
211
+ | `.list()` | Convert to array field |
212
+ | `.hidden()` | Hide the field (value still in form data) |
213
+ | `.disabled(value?)` | Disable the field |
214
+ | `.rules([...])` | Conditional visibility/disable rules |
215
+ | `.computed(fn)` | Always-computed value from other fields |
216
+ | `.computedUntilDirty(fn)` | Computed until user edits the field |
217
+ | `.beforeChange(fn)` | Transform value before change |
218
+ | `.afterChange(fn)` | Side effects after value changes |
219
+ | `.afterSetValue(fn)` | Side effects after programmatic value set |
220
+ | `.onBlur(fn)` | Blur event callback |
221
+ | `.cloneValue(fn)` | Custom clone logic for list item duplication |
222
+ | `.tags([...])` | Tag the field for programmatic lookup |
223
+
224
+ ## Renderers
225
+
226
+ ### Complete Renderer Reference
227
+
228
+ | Renderer | Field Type | Settings | Description |
229
+ | ------------------------- | -------------------------- | --------------------------------------------------------------- | --------------------------------------------------------- |
230
+ | `textInput` | text | — | Single-line text input (default for text) |
231
+ | `textarea` | text | `{ rows?: number }` | Multi-line text area |
232
+ | `textInputs` | text (list) | `{ addItemLabel?: string }` | List of text inputs |
233
+ | `textareas` | text (list) | `{ addItemLabel?: string }` | List of textareas |
234
+ | `tags` | text (list) | — | Comma-separated tag input |
235
+ | `codeEditor` | text | `{ language?: string; height?: number }` | Code editor with syntax highlighting |
236
+ | `dropdown` | text, number | — | Select dropdown (auto-selected when `.options()` is used) |
237
+ | `radioButtons` | text, number | — | Radio button group (requires `.options()`) |
238
+ | `checkboxes` | text (list), number (list) | — | Checkbox group (requires `.options()` + `.list()`) |
239
+ | `numberInput` | number | — | Number input (default for number) |
240
+ | `numberInputs` | number (list) | `{ addItemLabel?: string }` | List of number inputs |
241
+ | `switch` | boolean | — | Toggle switch (default for boolean) |
242
+ | `dateTimeInput` | datetime | `{ type, displayFormat?, yearRange?, weekStartsOn?, presets? }` | Date/time picker (default for datetime) |
243
+ | `dateTimeInputs` | datetime (list) | `{ type, displayFormat?, weekStartsOn?, addItemLabel? }` | List of date/time pickers |
244
+ | `filePicker` | file | — | File picker with full metadata (default for file) |
245
+ | `fileUrlPicker` | fileUrl | — | File picker returning URL only (default for fileUrl) |
246
+ | `objectAccordionSingle` | object | `{ open?: boolean }` | Single object in accordion (default for object) |
247
+ | `objectAccordionMultiple` | object (list) | `{ open?, container?, itemTitle?, addItemLabel? }` | List of objects in accordions (auto for `.list()`) |
248
+ | `dynamicZone` | object (templates) | `{ container?: boolean }` | Template picker zone (auto for `.template()`) |
249
+ | `passthrough` | object | — | Renders child fields inline without wrapper |
250
+ | `keyValueTags` | object (list) | `{ addItemLabel?: string }` | Key-value tag pairs |
251
+ | `hidden` | any | — | Hidden field (no UI rendered) |
252
+
253
+ ### Automatic Renderer Switching
254
+
255
+ - Calling `.options()` on text/number fields switches to `dropdown`
256
+ - Calling `.list()` on datetime switches to `dateTimeInputs`
257
+ - Calling `.list()` on object switches to `objectAccordionMultiple`
258
+ - Calling `.template()` on object switches to `dynamicZone`
259
+
260
+ ## Layout
261
+
262
+ Layout controls how fields are arranged in the UI. Defined via the `layout` callback.
263
+
264
+ ### Basic Layout
265
+
266
+ ```typescript
267
+ layout: layout => [
268
+ layout.row("title"), // single field row
269
+ layout.row("firstName", "lastName"), // two fields side by side
270
+ layout.separator() // visual divider
271
+ ];
272
+ ```
273
+
274
+ ### Tabs
275
+
276
+ ```typescript
277
+ layout: layout => [
278
+ layout
279
+ .tabs("myTabs")
280
+ .tab("general", tab => {
281
+ tab
282
+ .label("General")
283
+ .icon({ type: "icon", name: "fas/cog" })
284
+ .description("Basic settings")
285
+ .layout(l => [l.row("title"), l.row("description")]);
286
+ })
287
+ .tab("advanced", tab => {
288
+ tab.label("Advanced").layout(l => [l.row("config")]);
289
+ })
290
+ ];
291
+ ```
292
+
293
+ Vertical tabs (used by page settings):
294
+
295
+ ```typescript
296
+ layout.tabs("settings-tabs").renderer("tabsVertical");
297
+ ```
298
+
299
+ Tab-level conditional visibility:
300
+
301
+ ```typescript
302
+ .tab("premium", tab => {
303
+ tab.label("Premium")
304
+ .rules([{
305
+ type: "condition",
306
+ target: "plan",
307
+ operator: "neq",
308
+ value: "enterprise",
309
+ action: "hide"
310
+ }])
311
+ .layout(l => [...]);
312
+ })
313
+ ```
314
+
315
+ ### Object Layout
316
+
317
+ For object fields, define inner layout per template or for a flat object:
318
+
319
+ ```typescript
320
+ // Flat object
321
+ layout.object("address", l => [l.row("street"), l.row("city", "zip")]);
322
+
323
+ // Per-template layout (for dynamic zones)
324
+ layout.object("sections", {
325
+ hero: inner => [inner.row("heading", "subheading"), inner.row("image")],
326
+ cta: inner => [inner.row("label", "url")]
327
+ });
328
+ ```
329
+
330
+ ### Positioning
331
+
332
+ When modifying an existing layout (e.g., in a modifier), use `.after()` or `.before()` to position relative to existing fields:
333
+
334
+ ```typescript
335
+ layout.row("newField").after("existingField");
336
+ layout.row("anotherField").before("existingField");
337
+ ```
338
+
339
+ ## Validation
340
+
341
+ ### Field-Level (Zod)
342
+
343
+ ```typescript
344
+ import { z } from "zod";
345
+
346
+ fields.text().label("Email").schema(z.string().email("Must be a valid email"));
347
+
348
+ fields
349
+ .text()
350
+ .label("URL")
351
+ .schema(z.string().refine(val => !val || URL_REGEX.test(val), "Invalid URL format"));
352
+ ```
353
+
354
+ ### Conditional Required
355
+
356
+ ```typescript
357
+ fields
358
+ .text()
359
+ .label("Seats")
360
+ .requiredWhen(form => form.field("plan").getValue() === "pro", "Pro plan requires a seat count");
361
+ ```
362
+
363
+ ### Form-Level Rules
364
+
365
+ ```typescript
366
+ // Zod cross-field validation
367
+ form.addRule(
368
+ z
369
+ .object({
370
+ password: z.string().nullable(),
371
+ confirm: z.string().nullable()
372
+ })
373
+ .refine(d => d.password === d.confirm || (!d.password && !d.confirm), {
374
+ message: "Passwords must match",
375
+ path: ["confirm"]
376
+ })
377
+ );
378
+
379
+ // Imperative validation
380
+ form.addRule(form => {
381
+ const slug = String(form.field("slug").getValue() ?? "");
382
+ if (slug.length > 0 && slug.length < 3) {
383
+ return [{ path: "slug", message: "Slug must be at least 3 characters" }];
384
+ }
385
+ return [];
386
+ });
387
+ ```
388
+
389
+ ## Conditional Rules (Visibility / Disable)
390
+
391
+ Rules control field visibility and disabled state based on other field values:
392
+
393
+ ```typescript
394
+ fields
395
+ .text()
396
+ .label("Feature Name")
397
+ .rules([
398
+ {
399
+ type: "condition",
400
+ target: "enableFeature", // field to watch
401
+ operator: "isFalsy", // condition
402
+ value: null, // comparison value (null for unary operators)
403
+ action: "hide" // "hide" or "disable"
404
+ }
405
+ ]);
406
+ ```
407
+
408
+ Multiple rules can be chained (all are evaluated):
409
+
410
+ ```typescript
411
+ fields
412
+ .text()
413
+ .label("Advanced Config")
414
+ .rules([
415
+ {
416
+ type: "condition",
417
+ target: "enableFeature",
418
+ operator: "isFalsy",
419
+ value: null,
420
+ action: "hide"
421
+ },
422
+ {
423
+ type: "condition",
424
+ target: "featureMode",
425
+ operator: "neq",
426
+ value: "advanced",
427
+ action: "disable"
428
+ }
429
+ ]);
430
+ ```
431
+
432
+ ### Available Operators
433
+
434
+ | Operator | Description |
435
+ | -------------- | --------------------------------------------- |
436
+ | `"eq"` | Equal to value |
437
+ | `"neq"` | Not equal to value |
438
+ | `"isEmpty"` | Null, undefined, empty string, or empty array |
439
+ | `"isNotEmpty"` | Has a non-empty value |
440
+ | `"isTruthy"` | Boolean coercion is true |
441
+ | `"isFalsy"` | Boolean coercion is false |
442
+ | `"matches"` | Exact string match |
443
+
444
+ ## Computed Fields
445
+
446
+ ```typescript
447
+ // Always computed — recalculated when dependencies change
448
+ fields
449
+ .text()
450
+ .label("Full Name")
451
+ .computed(form => `${form.field("first").getValue()} ${form.field("last").getValue()}`);
452
+
453
+ // Computed until the user edits the field manually
454
+ fields
455
+ .text()
456
+ .label("Slug")
457
+ .computedUntilDirty(form => {
458
+ const name = String(form.field("title").getValue() ?? "");
459
+ return name.trim().toLowerCase().replace(/\s+/g, "-");
460
+ });
461
+ ```
462
+
463
+ ## Cross-Field Interaction
464
+
465
+ Use `.afterChange()` to react to value changes and modify other fields:
466
+
467
+ ```typescript
468
+ fields
469
+ .text()
470
+ .label("Visibility")
471
+ .options([
472
+ { label: "Public", value: "public" },
473
+ { label: "Password Protected", value: "password" }
474
+ ])
475
+ .afterChange((value, form) => {
476
+ const path = form.field("general.path").as("text").getValue() ?? "";
477
+ if (value === "password") {
478
+ form.field("general.path").setValue(path + "/protected");
479
+ } else {
480
+ form.field("general.path").setValue(path.replace("/protected", ""));
481
+ }
482
+ });
483
+ ```
484
+
485
+ ## Extending Object Fields After Creation
486
+
487
+ Object fields can be extended with additional children (modifier pattern):
488
+
489
+ ```typescript
490
+ // Original definition
491
+ profile: fields
492
+ .object()
493
+ .label("Profile")
494
+ .fields(f => ({
495
+ title: f.text().label("Title")
496
+ }));
497
+
498
+ // Later: add more fields
499
+ form
500
+ .field("profile")
501
+ .as("object")
502
+ .fields(f => ({
503
+ company: f.text().label("Company"),
504
+ bio: f.text().label("Short bio")
505
+ }));
506
+ ```
507
+
508
+ ## Runtime Template Management
509
+
510
+ Templates on object fields can be added/removed at runtime:
511
+
512
+ ```typescript
513
+ const sections = form.field("sections").as("object");
514
+
515
+ sections.templates.remove("text");
516
+
517
+ sections.templates.add("runtimeBanner", t => {
518
+ t.label("Runtime Banner").fields(f => ({
519
+ headline: f.text().label("Headline").required(),
520
+ note: f.text().label("Note")
521
+ }));
522
+ });
523
+ ```
524
+
525
+ ## Related Skills
526
+
527
+ - **webiny-page-settings-extensions** — Adding new settings groups or modifying existing ones in the Website Builder page settings drawer