@plitzi/sdk-mcp 0.37.7 → 0.37.9

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +267 -0
  2. package/README.md +5 -0
  3. package/dist/modules/mcp/helpers/components.d.ts +14 -0
  4. package/dist/modules/mcp/helpers/components.js +32 -0
  5. package/dist/modules/mcp/helpers/guide.js +48 -1
  6. package/dist/modules/mcp/helpers/index.d.ts +1 -0
  7. package/dist/modules/mcp/helpers/space.d.ts +5 -0
  8. package/dist/modules/mcp/helpers/space.js +2 -1
  9. package/dist/modules/mcp/helpers/uris.d.ts +2 -0
  10. package/dist/modules/mcp/helpers/uris.js +3 -1
  11. package/dist/modules/mcp/resources/primer.js +7 -1
  12. package/dist/modules/mcp/resources/schema.js +16 -10
  13. package/dist/modules/mcp/server.js +1 -1
  14. package/dist/modules/mcp/tests/components.test.d.ts +1 -0
  15. package/dist/modules/mcp/tools/apply/dispatch.js +26 -1
  16. package/dist/modules/mcp/tools/apply/index.d.ts +11 -0
  17. package/dist/modules/mcp/tools/operations/index.d.ts +44 -0
  18. package/dist/modules/mcp/tools/operations/index.js +2 -0
  19. package/dist/modules/mcp/tools/operations/schema/components/deleteComponent.d.ts +9 -0
  20. package/dist/modules/mcp/tools/operations/schema/components/deleteComponent.js +21 -0
  21. package/dist/modules/mcp/tools/operations/schema/components/index.d.ts +2 -0
  22. package/dist/modules/mcp/tools/operations/schema/components/upsertComponent.d.ts +14 -0
  23. package/dist/modules/mcp/tools/operations/schema/components/upsertComponent.js +147 -0
  24. package/dist/modules/mcp/tools/operations/schema/index.d.ts +1 -0
  25. package/dist/modules/mcp/tools/operations/schema/operations.d.ts +13 -0
  26. package/dist/modules/mcp/tools/operations/schema/operations.js +5 -1
  27. package/dist/modules/mcp/tools/operations/schema/write.js +3 -1
  28. package/dist/modules/mcp/tools/preview.d.ts +11 -0
  29. package/dist/modules/mcp/tools/render.d.ts +11 -0
  30. package/dist/modules/mcp/tools/screenshot.d.ts +11 -0
  31. package/dist/modules/mcp/tools/shared/validator/batch.js +1 -1
  32. package/dist/modules/mcp/tools/shared/validator/index.js +17 -9
  33. package/dist/modules/mcp/tools/validate.d.ts +11 -0
  34. package/dist/modules/mcp/types/aiSchema.d.ts +20 -0
  35. package/package.json +9 -9
package/CHANGELOG.md CHANGED
@@ -1,5 +1,272 @@
1
1
  # @plitzi/sdk-mcp
2
2
 
3
+ ## 0.37.9
4
+
5
+ ### Patch Changes
6
+
7
+ - 3ae61a4: ## Components replace segments
8
+
9
+ - **What a component is:** a reusable block written once and placed anywhere as an instance. An edit to the component
10
+ is an edit to every instance.
11
+ - **Where it lives:** in the space document, as `schema.components`, one tree per component and never in
12
+ `schema.flat`. It is published, rolled back, copied by templates and exported with the space.
13
+ - **Props and slots:**
14
+ - A component declares props (`type`, `description`, `required`, `default`, `options`). An instance hands them in
15
+ as its own attributes, so templates and bindings reach them. Inside, they are read as `{{ props.<name> }}`.
16
+ - Slots are elements an instance fills; each child names its slot in `attributes.slot`.
17
+ - **A prop an instance leaves out prints nothing.** It is its `default`, or `null`. `props` is a settled source:
18
+ - `processTwig` takes `{ settled }` in place of `keepEmptyTokens: true`.
19
+ - Every other empty token is still kept for a later pass.
20
+ - `COMPONENT_PROPS_SOURCE` names the source.
21
+ - **Fixed: a flow in a list row or a component instance acts on its own copy.** A step's target resolves in the
22
+ replica the flow fired in first, then outwards. It used to reach whichever copy registered last.
23
+ - **Fixed: an instance's own `visible` hides it in preview.** The same goes for an element reference.
24
+ - **Fixed: a list row keeps its state with its record.** Rows are keyed by each record's unique `id`, so filtering no
25
+ longer moves one row's state onto another.
26
+ - **Closed scope:** inside, a component reads only its props and the globals. The validator, `lintSpace` and
27
+ `authorSpace` each refuse a read of the page around an instance. Components nest, and a cycle is refused.
28
+ - **Where to use them:**
29
+ - In code (`@plitzi/sdk-authoring`): `SpaceSpec.components` and `component(id, { props, children })`.
30
+ `specFromSpace` and `specToSource` read and write both.
31
+ - In the builder:
32
+ - a Components panel lists them, and an open component becomes the canvas;
33
+ - an element becomes a component with **Save as component**;
34
+ - in an instance's settings, an instance gets its props and slots, or is **detached** back into a copy.
35
+ - In the MCP: `upsertComponent` and `deleteComponent`. Element ops work inside a component through `pageRef`.
36
+ - **Schema helpers:** `@plitzi/sdk-schema` gains `addComponent`, `updateComponent`, `removeComponent`,
37
+ `detachInstance`, `renameElement`, `treeOf`, `flatMapOf` and `documentIds`.
38
+ - **GraphQL and live events:** `SpaceAddComponent`, `SpaceUpdateComponent`, `SpaceRemoveComponent` and
39
+ `SpaceDetachInstance`, each with a live event. History records a declaration change as a `component` entry.
40
+ - **Breaking: segments are removed.** This covers:
41
+ - `@plitzi/sdk-shared`'s segment types, queries, mutations, context and `SEGMENT_*` events;
42
+ - `referenceType: 'segment'`;
43
+ - the `Segments` builder module;
44
+ - `Space.segments` and the `Segment`/`Segments` queries;
45
+ - `CommonState.prevSchema`.
46
+ - **Breaking: `ElementLayout` and `LayoutBody` change shape.**
47
+ - `ElementLayout` is `{ slots, rootId, type }`; it was `{ containerId }`.
48
+ - `LayoutBody` takes `bodies` keyed by slot.
49
+ - `reference`'s `referenceContainer` attribute is removed.
50
+ - Guide: `docs/en/components.md`.
51
+
52
+ ## Functions ask for the time they need
53
+ - **What changes:** a task can ask for more CPU or wall time than the default with `limits`, in milliseconds. Example:
54
+ `limits: { cpuMs: 1000, wallMs: 20_000 }`. `defineFunctions({ limits })` asks it for every task and route at once,
55
+ and a task's own limits win over those.
56
+ - **What a run is given:** what it asked for, or the default (100 ms of CPU, 10 s) when it asked for nothing. Never
57
+ above the space's plan or the deployment's ceiling.
58
+ - **Deployment ceilings:** `functions.limits` sets them. `DEFAULT_FUNCTION_CEILINGS` covers each unset one, at 2 s of
59
+ CPU and 30 s.
60
+ - **Asking for more than the ceiling** is a problem when the functions are saved; it is never quietly cut down.
61
+ - **Manifest:** carries what each task asked for, and the builder shows it beside the task.
62
+
63
+ ## Functions answer under `/fn`, not `/api`
64
+
65
+ A space's routes are served at `/fn/<path>` (`FUNCTION_ROUTES_PREFIX`). `/api` is a slug a site wants for a page of its
66
+ own. A page under `/fn` is refused instead (`page-route-reserved`).
67
+
68
+ ## The builder's Functions panel
69
+
70
+ Rebuilt around the code. The panel reads `defineFunctions` as it is typed and writes into it, so the code stays the one
71
+ place a function is declared.
72
+
73
+ - **Layout:** the tasks, routes and files on the left, the code in the middle, the selected task on the right.
74
+ - **Live list:** tasks and routes are listed as the code declares them, including tasks imported from another file.
75
+ A task you have written but not saved says so. Tasks built by calling something are counted, and listed once saved.
76
+ - **Code and panel follow each other:** clicking a task or a route opens its file at its line. Putting the cursor inside
77
+ a task's code selects that task.
78
+ - **New task:** + in Tasks asks for its namespace, action and title. The task is written into `defineFunctions` with a
79
+ `run` to start from, in the file's own quotes, and the editor opens on it.
80
+ - **Time limit:** each task gets a slider from 100 ms to 1 s of CPU per run, with presets. The value is written into
81
+ the task as `limits: { cpuMs }`. "Use default" takes it out. `DEFAULT_FUNCTION_TIME_LIMITS` in
82
+ `@plitzi/sdk-shared/actions` is the default both the panel and the server use.
83
+ - **Test:** fills a task's params the way its step does, from their defaults: a select, a switch or text. JSON stays a
84
+ toggle away. With unsaved changes, the button reads "Save & run": it saves, then runs. If the save fails, it says why.
85
+ - **Header:** shows Saved, Unsaved (and in how many files) or the number of problems the last save found. ⌘S saves.
86
+ **Discard** asks first, then puts every file back to what was last saved, or back to nothing for functions never
87
+ saved. Removing the functions is a quiet button beside Save.
88
+ - **Problems:** clicking one opens its file at its line.
89
+ - **Editor:** each file has its own editor and undo history. Opening another file no longer marks the one you left as
90
+ changed. Before this, it could also write the newly opened file's text into the one you left. The cause was in
91
+ `@plitzi/plitzi-ui`'s CodeMirror, fixed in 1.6.25, which every package now depends on. That release also sets code
92
+ editors (several lines) in a monospaced face again. Long lines scroll inside the editor, and the line numbers stay
93
+ in place.
94
+
95
+ ## A space's own functions are their own category of steps
96
+
97
+ In the action editor's step picker, the space's own functions are listed under **Functions**, apart from the
98
+ platform's **Tasks**. The other headings now read Callbacks, Global callbacks and Utilities. The saved step is still a
99
+ `task` node.
100
+
101
+ - `@plitzi/sdk-server`: every registered task has an `origin`, `'deployment'` (shipped with the server or a native
102
+ function) or `'space'` (from the space's functions). `describeCatalog`, `/_action/catalog` and the builder's
103
+ `SpaceActionTasks` carry it (`ActionTaskDescriptor.origin`).
104
+ - `@plitzi/sdk-shared`: an `InteractionCallback` may name the `group` the picker lists it under.
105
+
106
+ ## Fixed: preview no longer breaks a builder that is embedded in a page
107
+
108
+ When the builder is mounted inside a Plitzi page (the platform's `/spaces/:id/update`), going to preview with a page
109
+ that has SEO turned on left the builder unstyled. The previewed page wrote its title and description through a head
110
+ manager of the builder's own, and that manager rewrote the host page's head, removing the builder's own stylesheet.
111
+
112
+ - `@plitzi/sdk-shared`: a new render setting, `ownsHead`, says whether the page may write the document head. It is on
113
+ by default (`DEFAULT_RENDER_SETTINGS`).
114
+ - `@plitzi/sdk-elements`: `Page` writes its SEO only where `ownsHead` is on.
115
+ - Builder: sets `ownsHead: false` for its canvas. A page drawn there is in a frame, and the document head is the
116
+ editor's. The canvas's own `HelmetProvider` is gone.
117
+
118
+ ## Fixed: the builder's plan usage panel shows the space being edited
119
+ - **The 404 is gone.** Opening Plan usage said "The account breakdown could not be read (The server answered 404.)".
120
+ The panel asked `/account/usage`, which went away when accounts became workspaces.
121
+ - **Only this space.** The panel listed every space of the workspace. It now reads `/spaces/:spaceId/usage`, which
122
+ now also answers the space's own `pages` (the heaviest ten), `pagesTotal` and `periodEndsAt`. The plan's ceilings
123
+ stay on top. Anyone who can edit the space can read it, including a guest of another workspace.
124
+ - **It scrolls.** A long breakdown scrolls inside the modal, where it used to overflow.
125
+ - **`getKeyDecoded(webKey, true)` reads the token's subject.** The function, in `@plitzi/sdk-shared`, looked for
126
+ `data.spaceId`, which space tokens no longer carry, so every space decoded as 0. The builder asked about space 0, and
127
+ the builder and the SDK kept every space's persisted state under the same key on a host. Pages served without a
128
+ `webKey` (SSR) still decode as 0, which is what their painted-state cookie is named after.
129
+
130
+ ## Fixed: a remote plugin in the builder reads the canvas it sits in
131
+
132
+ The builder carries its own copy of the Plitzi runtime. A remote plugin imports `@plitzi/plitzi-sdk`, which the page's
133
+ import map resolves to the SDK's copy. Each copy made its own React contexts, so on the builder's canvas a plugin read
134
+ none of the canvas's providers. In the builder embedded in Plitzi's site, it read the site around the builder instead:
135
+ the site's element as its own, the site's live mode (so it stayed interactive while being edited), and the site's store
136
+ and interactions.
137
+
138
+ - `@plitzi/sdk-shared` gains `sharedContext(name, default)`: a context made once per page and handed to every copy of
139
+ the runtime that asks for it.
140
+ - The runtime's contexts now go through it, so a provider from any copy reaches a consumer from any copy:
141
+ - `@plitzi/sdk-shared`: service, component, schema, network, theme scope, dev tools, builder.
142
+ - `@plitzi/sdk-elements`: element, element parent, layout body.
143
+ - interactions, plugins, event bridge, auth, variables and style.
144
+ - The store's contexts need `@plitzi/nexus` 1.4.0, which does the same. Every `@plitzi/*` package now asks for
145
+ `^1.4.0`, which is published.
146
+
147
+ ## The source of plugins and runtimes is kept on the space
148
+
149
+ What a plugin or a runtime is built from now goes up with it, so a space can be taken back out as a project
150
+ (`plitzi create --from`, below).
151
+
152
+ - `@plitzi/cli`:
153
+ - `plitzi pack plugin` writes the plugin's source beside its zip: every file of the project its elements import,
154
+ `import type` included, and the packages they need. `--source-root` names the project those paths are relative to.
155
+ - `plitzi upload plugin` and `plitzi runtime push` keep that source on the space, in its private bucket.
156
+ - `plitzi pack source` writes it to a file.
157
+ - A source that cannot be kept (a file outside the project, an undeclared package, a credential in the code) never
158
+ stops the upload or the push: they say why, and the artifact is kept built only.
159
+ - `@plitzi/sdk-shared/source`: the snapshot's format, the paths it may hold and the credential check, shared by the CLI
160
+ and the platform.
161
+
162
+ ## `plitzi create --from` and `plitzi pull`: a space on Plitzi as a project of your own
163
+
164
+ The way back from everything the CLI puts on Plitzi. `plitzi create my-board --from pizarra` writes a server project
165
+ holding what the space is made of, and runs it with nothing of Plitzi's: neither its servers nor its CDN. `plitzi pull`
166
+ keeps it in step with the space. See `docs/en/projects-from-spaces.md`.
167
+
168
+ - **What lands in the project:**
169
+ - its pages as authoring code;
170
+ - its server actions as `defineAction` code — JSON, with the reason said, for one that would not read back exactly;
171
+ - its plugins and runtime as the source they were uploaded from, and its functions;
172
+ - its files, downloaded into `public/`, with every CDN address rewritten to the project's.
173
+ - **`src/main.ts`** serves all of it, the runtime in the same process.
174
+ - **`.env`** gets a key made for the project's actions to sign with, and the names of the variables and credentials
175
+ the space had. Their values stay on Plitzi.
176
+ - **Who may run it:** the person must be signed in and able to change the space (owner, administrator or writer).
177
+ - **Plugins** are rebuilt against the project's SDK. One uploaded before sources were kept runs as it was built, from
178
+ `vendor/plugins/`, and the report says to upload it again.
179
+ - **The end of `create`** says what came across differently, including a space whose visitors sign in with Plitzi.
180
+ - `--source cloud` keeps the pages on Plitzi and runs the rest locally.
181
+ - **Any version:** `--environment` and `--revision` take out a published snapshot — its latest, or one revision pinned
182
+ — instead of the draft, with the source its plugins and runtime were built from then. A cloud project serves that
183
+ revision pinned.
184
+ - **`plitzi pull`** writes what changed on the space, keeps what changed in the project, and writes nothing when a
185
+ file changed on both — naming them, with `--force` to take the space's copy. It never touches `.env`, only adds to
186
+ `package.json`, and keeps `plitzi functions push` working from the project. It follows the version the project was
187
+ made from, and `--environment`/`--revision` move it.
188
+ - `@plitzi/sdk-authoring`:
189
+ - `actionSpecFromEntry` reads an action document back into its `defineAction` declaration, only when the round trip
190
+ is exact, and `actionToSource` writes it as a module.
191
+ - `defineAction` takes `limits`.
192
+ - `specToSource` takes `importExtension: '.ts'`, for split files that Node imports as they are.
193
+
194
+ ## The builder shows what a snapshot holds
195
+
196
+ **Make Snapshot** lists what it will freeze — pages, layouts and elements, server actions, connectors, functions, the
197
+ runtime and the plugins, each with whether its source is kept — and what no snapshot freezes: the space's files, its
198
+ variables and credentials. A space's components are part of its document, so they are frozen with its pages. **Publish Snapshot** lists what the chosen environment's snapshot holds.
199
+
200
+ ## Fixed: a space read from Plitzi kept its server elements
201
+ - **What happened:** a page server reading its space from Plitzi (`createCloudAdapters`) never ran an element's
202
+ `render` action, and a browser-rendered space ignored `loadStrategy`. The space's GraphQL answered neither
203
+ `runtime` nor `loadStrategy` of an element, nor the space's `rsc` settings.
204
+ - **Now:** the platform answers them, and the SDK, the builder and the cloud adapters ask for them.
205
+
206
+ ## A plugin the deployment registers is not looked for elsewhere
207
+
208
+ `@plitzi/sdk-server` used to fetch the manifest of every plugin the space lists on its CDN, even one the deployment
209
+ registers itself, and logged a warning when the CDN was out of reach. It now asks only for the ones it does not have.
210
+
211
+ ## Fixed: inline code in Markdown carries nothing of the syntax tree
212
+
213
+ A `markdown` element wrote every inline `` `code` `` as `<code node="[object Object]">`: `react-markdown` hands its
214
+ renderers the syntax-tree node as a prop, and the inline branch spread it onto the tag. Fixed in
215
+ `@plitzi/plitzi-ui` 1.6.26 (with a test), which every package now asks for.
216
+
217
+ ## Dev tools hear about the render run the page stopped waiting for
218
+
219
+ When a server element's action ran past the section's budget, the page was answered without it. The run ended a moment
220
+ later, after the page's runs had been sent, so the dev tools never showed the one run that needed debugging. It is now
221
+ told the moment the page stops waiting, as `aborted`, with the reason.
222
+
223
+ ## Fixed: a tab you come back to no longer loses its session
224
+ - **What happened:** a page left in another tab past its access token's life signed its visitor out on return. A
225
+ reload put them right back in.
226
+ - **Why:** the browser drops the cookie carrying the access token the moment the token expires, and the background
227
+ tab's renewal timer had not run. The first check on return was told `missing`, which the client took as no
228
+ session at all.
229
+ - **Now:** a `missing` refusal is renewed instead whenever the browser can still renew, meaning it holds a refresh
230
+ token or a session hint whose renewal window is open. The session ends only if that renewal fails.
231
+ - **Requests made in that moment:** `reportAuthFailure` now answers whether it renewed the session. A read refused
232
+ as the tab came back is asked again, once, once the session is renewed. This covers an api container's read and a
233
+ server section's refresh, so they no longer show a 401 that only a reload cleared.
234
+
235
+ - Updated dependencies [3ae61a4]
236
+ - @plitzi/sdk-authoring@0.37.9
237
+ - @plitzi/sdk-elements@0.37.9
238
+ - @plitzi/sdk-interactions@0.37.9
239
+ - @plitzi/sdk-schema@0.37.9
240
+ - @plitzi/sdk-shared@0.37.9
241
+ - @plitzi/sdk-style@0.37.9
242
+ - @plitzi/plitzi-sdk@0.37.9
243
+ - @plitzi/sdk-server@0.37.9
244
+
245
+ ## 0.37.8
246
+
247
+ ### Patch Changes
248
+
249
+ - 4d10e0a: ## Fixed: an api container could not read a file from a CDN
250
+
251
+ - **What happened:** the api container sent `Content-Type: application/json` on every request, including a GET with no
252
+ body to describe. That made every read a non-simple cross-origin request, so the browser asked the host's
253
+ permission first.
254
+ - **Who it hit:** hosts that allow plain cross-origin reads (GET and HEAD from any origin, the usual CORS of a public
255
+ bucket, the space's own CDN among them) refused that preflight. The file never loaded. Tremor's world outlines on
256
+ `cdn-dev.plitzi.com` are one example.
257
+ - **What changes:** a read now sends `Accept: application/json`, and `Content-Type` goes only with a body, unless the
258
+ author set their own. Headers the author adds still make the request non-simple, as they must.
259
+
260
+ - Updated dependencies [4d10e0a]
261
+ - @plitzi/sdk-authoring@0.37.8
262
+ - @plitzi/sdk-elements@0.37.8
263
+ - @plitzi/sdk-interactions@0.37.8
264
+ - @plitzi/sdk-schema@0.37.8
265
+ - @plitzi/sdk-shared@0.37.8
266
+ - @plitzi/sdk-style@0.37.8
267
+ - @plitzi/plitzi-sdk@0.37.8
268
+ - @plitzi/sdk-server@0.37.8
269
+
3
270
  ## 0.37.7
4
271
 
5
272
  ### Patch Changes
package/README.md CHANGED
@@ -73,6 +73,10 @@ The server is stateless: it resolves the space per request and reads and writes
73
73
  | `saveStyle(spaceId, env, style)` | for writes | Persist a mutated style document. |
74
74
  | `getChanges(spaceId, env, query)` | no | The space's change history, for the `plitzi://changes` resources. Writes arrive with an `SSRWriteContext` (`userId`, one `batch` per tool call) so a consumer can record them. |
75
75
  | `getOfflineData(spaceId, env, rev)` | for preview | Read side of draft-preview. Only the preview endpoint calls it. |
76
+ | `getComponentCatalog(spaceId, env)` | no | The element types the space's plugins add, for the catalog resources. |
77
+ | `getConnectors` / `saveConnector` / `deleteConnector` | for connectors | The space's connector manifests, read and written by the connector operations. |
78
+ | `getActions` / `getActionTasks` / `saveAction` / `deleteAction` | for actions | The space's server actions, and the catalog of tasks a step may run — the deployment's and the space's own functions, each with its `origin`. |
79
+ | `getFunctions` / `saveFunctions` / `tryFunction` | for functions | The space's functions: read for `plitzi://functions/{env}`, saved (built and checked, refused from an older copy) by `upsertFunctionFile` / `deleteFunctionFile`, and run by `plitzi_try_function`. |
76
80
 
77
81
  Schema and style are read as **separate documents** on purpose: `getOfflineData` is SSR-shaped and strips
78
82
  `style.platform`, which the style resources need.
@@ -92,6 +96,7 @@ resolves with `canWrite: false` reads everything and is refused at every write t
92
96
  | `plitzi_preview` | read | Render a draft to HTML through an SSR server |
93
97
  | `plitzi_screenshot` | read | Render a draft to a PNG (desktop, mobile or both) through the screenshot service |
94
98
  | `plitzi_render` | read | Render a self-contained UI widget, offline, with no space |
99
+ | `plitzi_try_function` | write | Run one task of the space's functions against the draft, in the sandbox: its value, logs and error |
95
100
 
96
101
  Reads follow a filesystem model: list cheap, read one item in detail on demand. Agents are told never to
97
102
  hand-build a URI — every write and search response hands back the URI to use next.
@@ -0,0 +1,14 @@
1
+ import { Space } from './space';
2
+ import { AIComponentSummary } from '../types';
3
+ import { Schema, SpaceComponent } from '@plitzi/sdk-shared';
4
+ /** A component by the name it is placed by, unless a page or a layout answers to it first. */
5
+ export declare const findComponentByRef: (schema: Schema, ref: string) => SpaceComponent | undefined;
6
+ /**
7
+ * The space as an op addressed to a component sees it: the component's tree as the only one, its root as the one root.
8
+ *
9
+ * `flat` IS the component's own object, so whatever the op writes lands in the component. Every other tree is out of
10
+ * sight — a component is closed, nothing in it may name them — but not out of mind: `document` is the whole space,
11
+ * which is what a new name has to be free of.
12
+ */
13
+ export declare const componentView: (space: Space, component: SpaceComponent) => Space;
14
+ export declare const componentSummariesToAI: (schema: Schema) => AIComponentSummary[];
@@ -0,0 +1,32 @@
1
+ import { componentNamed, instancesOf } from "@plitzi/sdk-schema/helpers/components";
2
+ //#region src/modules/mcp/helpers/components.ts
3
+ /** A component by the name it is placed by, unless a page or a layout answers to it first. */
4
+ var findComponentByRef = (schema, ref) => Object.hasOwn(schema.flat, ref) ? void 0 : componentNamed(schema, ref);
5
+ /**
6
+ * The space as an op addressed to a component sees it: the component's tree as the only one, its root as the one root.
7
+ *
8
+ * `flat` IS the component's own object, so whatever the op writes lands in the component. Every other tree is out of
9
+ * sight — a component is closed, nothing in it may name them — but not out of mind: `document` is the whole space,
10
+ * which is what a new name has to be free of.
11
+ */
12
+ var componentView = (space, component) => ({
13
+ ...space,
14
+ schema: {
15
+ ...space.schema,
16
+ flat: component.flat,
17
+ pages: [component.rootId],
18
+ components: {}
19
+ },
20
+ document: space.document ?? space.schema
21
+ });
22
+ var componentSummariesToAI = (schema) => Object.values(schema.components).map((component) => ({
23
+ ref: component.id,
24
+ label: component.label ?? component.id,
25
+ props: component.props ?? {},
26
+ slots: component.slots ?? [],
27
+ rootRef: component.rootId,
28
+ instances: instancesOf(schema, component.id).length,
29
+ elementCount: Object.keys(component.flat).length
30
+ }));
31
+ //#endregion
32
+ export { componentSummariesToAI, componentView, findComponentByRef };
@@ -113,6 +113,8 @@ Never download a whole tree you do not need.
113
113
  - \`plitzi://schema/{env}/pages\` — page **summaries** (ref, label, elementCount, folder). No element trees.
114
114
  - \`plitzi://schema/{env}/layouts\` — the shared **layout shells** (header/sidebar/footer) and the pages rendered
115
115
  inside each. Read one like a page. See *Shared layouts* below.
116
+ - \`plitzi://schema/{env}/components\` — the space's **components** (reusable subtrees): their props, slots, root and
117
+ how many places render each. Read one like a page. See *Components* below.
116
118
  - \`plitzi://folders/{env}\` — page **folders** (the sidebar tree): ref, name, slug, parentId. \`/{ref}\` for one.
117
119
  - \`plitzi://schema/{env}/pages/{ref}\` — one page as a **skeleton tree**: each node is \`ref/type/label\` **plus the
118
120
  style classes it attaches** (\`base\`, and \`slots\` for non-base slots) — names only, no CSS. So you can map every
@@ -645,7 +647,11 @@ any other (\`<namespace>.<action>\`), listed in \`plitzi://actions/{env}/tasks\`
645
647
  \`ctx.user\` (who asked, never their session). A secret is NAMED, never read: \`ctx.fetch(url, { credential: "stripe",
646
648
  headers: { authorization: "Bearer {{ credential.apiKey }}" } })\`.
647
649
  - **Stateless:** nothing a function keeps in memory outlives the call — a module-level cache or counter starts empty
648
- every time, on whichever instance runs it. What must last goes in \`ctx.kv\`. A call gets 100 ms of CPU and 10 s.
650
+ every time, on whichever instance runs it. What must last goes in \`ctx.kv\`. A call gets 100 ms of CPU and 10 s;
651
+ a task that needs more says so — \`limits: { cpuMs: 1000, wallMs: 20000 }\`, or \`defineFunctions({ limits })\` for
652
+ all — up to the plan's ceiling. Asking above it is refused at save, never quietly cut.
653
+ - **Routes** answer HTTP on the space's own host under \`/fn/\`: \`routes: { "GET /boards/:board": (request, ctx) =>
654
+ Response.json(…) }\` answers \`/fn/boards/kanban\` (\`ctx.params.board\`). No page may live under \`/fn\`.
649
655
  - **Saving builds and checks** the files: the batch is refused with each problem where it is (file and line). A
650
656
  namespace the platform uses (\`kv\`, \`http\`, \`flow\`…) is refused.
651
657
  - **Try it** with \`plitzi_try_function { task, params }\` before a page relies on it: the value, the logs, the error.
@@ -712,6 +718,47 @@ vocabulary:
712
718
  link, an upgrade prompt in the sidebar — it belongs in the LAYOUT, not in the page you happen to have open. Check
713
719
  the page's \`layout\` first; if the element you are looking for is not in the page's own tree, it is in the shell.
714
720
 
721
+ ## Components — a subtree written once, placed anywhere
722
+ A **component** is a reusable subtree: a product card, a pricing tier, a testimonial — written once and placed on
723
+ any page as an **instance**. It is part of the space, so it is published, versioned and copied with it. Its tree is
724
+ not inside any page: it is a root of its own, like a layout, and **editing it changes every instance** — say so
725
+ before you touch one.
726
+
727
+ It is **closed**. Inside, a binding or a template reads **\`props\`** — what each instance hands in — and the globals
728
+ (\`state\`, \`auth\`, \`navigation\`, \`theme\`, \`variables\`, \`computed\`), never the page an instance sits on. A
729
+ component that needs a list row's record gets it as a prop: the INSTANCE binds it (\`item: "{{ list_rows.item }}"\`),
730
+ the component reads \`{{ props.item.name }}\`.
731
+
732
+ **Reading them.** \`plitzi://schema/{env}/components\` lists them: \`ref\`, \`label\`, \`props\`, \`slots\`, \`rootRef\`,
733
+ \`instances\`. Read one like a page: \`plitzi://schema/{env}/pages/{componentRef}\` (and \`/styles\`).
734
+
735
+ **Writing them.**
736
+ - \`upsertComponent { ref, label?, props?, slots? }\` declares one — with nothing in it but its root, \`{ref}-root\`, a
737
+ container — or updates the declaration of one that exists (\`props\` is REPLACED whole). Fill it with every
738
+ element/binding/interaction op you already know, with **\`pageRef: "<componentRef>"\`**:
739
+ \`\`\`json
740
+ { "type": "upsertElement", "pageRef": "product-card", "parentRef": "product-card-root",
741
+ "element": { "ref": "product-card-title", "type": "heading" } }
742
+ \`\`\`
743
+ - \`upsertComponent { ref, fromRef, instanceRef }\` makes a component OF an element that already exists: the element
744
+ and everything inside it become the component, and an instance named \`instanceRef\` takes its place, so the page
745
+ renders what it did. The way to turn a repeated block into one.
746
+ - A **prop** is \`{ type, description, required?, default?, options? }\`, \`type\` one of \`text\`, \`textarea\`,
747
+ \`number\`, \`boolean\`, \`select\` (with \`options\`), \`scalar\`, \`json\`. Its name is read as \`{{ props.<name> }}\`, so
748
+ letters, digits and \`_\` — and never one an instance already has (\`referenceId\`, \`referenceType\`, \`slot\`…).
749
+ - A **slot** is an element of the component's tree (usually an empty container) an instance fills with children.
750
+ - \`deleteComponent { ref }\` removes one nothing places; while instances remain it is refused, naming them.
751
+
752
+ **Placing one** is an element: a \`reference\` whose props say which component and hand in its props, one attribute each:
753
+ \`\`\`json
754
+ { "type": "upsertElement", "pageRef": "shop", "parentRef": "grid",
755
+ "element": { "ref": "lamp-card", "type": "reference",
756
+ "props": { "referenceType": "component", "referenceId": "product-card", "title": "Lamp" },
757
+ "children": [ { "ref": "lamp-buy", "type": "button", "props": { "content": "Buy", "slot": "product-card-actions" } } ] } }
758
+ \`\`\`
759
+ A required prop left out, a prop the component does not declare, a value of the wrong kind or a child naming a slot
760
+ the component does not have is reported by \`plitzi_validate\` — fix it before applying.
761
+
715
762
  ## Pages & folders
716
763
  - **Always set a \`slug\` when creating a page** (\`upsertPage\`) — it is the page's URL path and good practice for a
717
764
  clean, stable route (e.g. \`"pricing"\` or \`"posts/:postId"\`). Omit it and the page ref is used as the slug,
@@ -1,4 +1,5 @@
1
1
  export * from './agentPrompt';
2
+ export * from './components';
2
3
  export * from './computeVersion';
3
4
  export * from './space';
4
5
  export * from './uris';
@@ -17,6 +17,11 @@ export interface Space {
17
17
  * the deployment offers no functions: then there is nothing to read and every function op is refused. */
18
18
  functions?: FunctionsDraft;
19
19
  catalog?: ComponentCatalog;
20
+ /**
21
+ * The whole document, when `schema` is a view of one tree of it — a component's, which an op addressed to a
22
+ * component edits as if it were the space's only tree. What a new name has to be free of is still all of it.
23
+ */
24
+ document?: Schema;
20
25
  }
21
26
  export declare const cloneSpace: (space: Space) => Space;
22
27
  export declare const emptySpace: () => Space;
@@ -19,7 +19,8 @@ var emptySpace = () => ({
19
19
  variables: [],
20
20
  settings: { customCss: "" },
21
21
  pages: [],
22
- pageFolders: []
22
+ pageFolders: [],
23
+ components: {}
23
24
  },
24
25
  style: {
25
26
  platform: {
@@ -9,6 +9,8 @@ export declare const pageUri: (env: Env, ref: string) => string;
9
9
  /** The shared layout shells. One of them is read as a page is — `pages/{ref}` resolves either root. */
10
10
  export declare const layoutsUri: (env: Env) => string;
11
11
  export declare const elementUri: (env: Env, ref: string) => string;
12
+ /** The components. One of them is read as a page is — `pages/{ref}` resolves it — and edited with `pageRef: <ref>`. */
13
+ export declare const componentsUri: (env: Env) => string;
12
14
  export declare const schemaVarsUri: (env: Env) => string;
13
15
  export declare const settingsUri: (env: Env) => string;
14
16
  export declare const interactionsUri: (env: Env) => string;
@@ -9,6 +9,8 @@ var pageUri = (env, ref) => `plitzi://schema/${env}/pages/${ref}`;
9
9
  /** The shared layout shells. One of them is read as a page is — `pages/{ref}` resolves either root. */
10
10
  var layoutsUri = (env) => `plitzi://schema/${env}/layouts`;
11
11
  var elementUri = (env, ref) => `plitzi://schema/${env}/elements/${ref}`;
12
+ /** The components. One of them is read as a page is — `pages/{ref}` resolves it — and edited with `pageRef: <ref>`. */
13
+ var componentsUri = (env) => `plitzi://schema/${env}/components`;
12
14
  var schemaVarsUri = (env) => `plitzi://schema-variables/${env}`;
13
15
  var settingsUri = (env) => `plitzi://settings/${env}`;
14
16
  var interactionsUri = (env) => `plitzi://interactions/${env}`;
@@ -59,4 +61,4 @@ var itemTemplates = (env) => [
59
61
  * family and extracts its ref in one step, without repeating the prefix string for both the startsWith and slice. */
60
62
  var afterPrefix = (uri, prefix) => uri.startsWith(prefix) ? uri.slice(prefix.length) : void 0;
61
63
  //#endregion
62
- export { actionTasksUri, actionUri, actionsUri, afterPrefix, aliasedRoots, changesUri, connectorPresetsUri, connectorUri, connectorsUri, cssPropertiesUri, dataSourcesUri, defUri, defsUri, elementUri, folderUri, foldersUri, fontUri, fontsUri, functionFileUri, functionsUri, globalUri, globalsUri, guideUri, idUri, idsUri, interactionsUri, itemTemplates, layoutsUri, pageUri, pagesUri, primerUri, schemaVarsUri, settingsUri, styleVarUri, styleVarsUri, typesUri };
64
+ export { actionTasksUri, actionUri, actionsUri, afterPrefix, aliasedRoots, changesUri, componentsUri, connectorPresetsUri, connectorUri, connectorsUri, cssPropertiesUri, dataSourcesUri, defUri, defsUri, elementUri, folderUri, foldersUri, fontUri, fontsUri, functionFileUri, functionsUri, globalUri, globalsUri, guideUri, idUri, idsUri, interactionsUri, itemTemplates, layoutsUri, pageUri, pagesUri, primerUri, schemaVarsUri, settingsUri, styleVarUri, styleVarsUri, typesUri };
@@ -1,7 +1,8 @@
1
1
  import { buildDataSourceCatalog, buildInteractionCatalog } from "../catalogs/observed.js";
2
2
  import { buildTypeRegistry } from "../catalogs/registry.js";
3
3
  import { cssProperties, cssShorthands } from "../catalogs/index.js";
4
- import { connectorsUri, dataSourcesUri, defsUri, foldersUri, fontsUri, interactionsUri, layoutsUri, pagesUri, primerUri, schemaVarsUri, settingsUri, styleVarsUri, typesUri } from "../helpers/uris.js";
4
+ import { componentSummariesToAI } from "../helpers/components.js";
5
+ import { componentsUri, connectorsUri, dataSourcesUri, defsUri, foldersUri, fontsUri, interactionsUri, layoutsUri, pagesUri, primerUri, schemaVarsUri, settingsUri, styleVarsUri, typesUri } from "../helpers/uris.js";
5
6
  import { guideQuickstart } from "../helpers/guide.js";
6
7
  import { envelope } from "./envelope.js";
7
8
  import { connectorSummaries } from "./connectors.js";
@@ -83,6 +84,11 @@ var readPrimerResource = (space, env, uri) => {
83
84
  value: layoutSummariesToAI(space.schema),
84
85
  read: layoutsUri(env)
85
86
  },
87
+ {
88
+ key: "components",
89
+ value: componentSummariesToAI(space.schema),
90
+ read: componentsUri(env)
91
+ },
86
92
  {
87
93
  key: "folders",
88
94
  value: foldersToAI(space.schema),
@@ -1,14 +1,17 @@
1
1
  import { buildDataSourceCatalog, buildInteractionCatalog } from "../catalogs/observed.js";
2
+ import { componentSummariesToAI, componentView, findComponentByRef } from "../helpers/components.js";
2
3
  import { findElementByRef, findRootByRef } from "../helpers/space.js";
3
- import { afterPrefix, dataSourcesUri, elementUri, folderUri, foldersUri, interactionsUri, layoutsUri, pageUri, pagesUri, schemaVarsUri, settingsUri } from "../helpers/uris.js";
4
+ import { afterPrefix, componentsUri, dataSourcesUri, elementUri, folderUri, foldersUri, interactionsUri, layoutsUri, pageUri, pagesUri, schemaVarsUri, settingsUri } from "../helpers/uris.js";
4
5
  import { envelope } from "./envelope.js";
5
6
  import { elementView, folderRefToAI, foldersToAI, layoutSummariesToAI, pageSkeletonToAI, pageStylesToAI, pageSummariesToAI, schemaVariablesToAI, settingsToAI } from "../tools/operations/schema/translator.js";
7
+ import { treeOf } from "@plitzi/sdk-schema/helpers/components";
6
8
  //#region src/modules/mcp/resources/schema.ts
7
9
  /** Element-schema reads: page listings/skeletons/styles, folders, single elements and schema variables. Returns
8
10
  * undefined when the URI belongs to another domain, null when the shape is ours but the ref does not resolve. */
9
11
  var readSchemaResource = (space, env, uri) => {
10
12
  if (uri === pagesUri(env)) return envelope(pageSummariesToAI(space.schema));
11
13
  if (uri === layoutsUri(env)) return envelope(layoutSummariesToAI(space.schema));
14
+ if (uri === componentsUri(env)) return envelope(componentSummariesToAI(space.schema));
12
15
  if (uri === foldersUri(env)) return envelope(foldersToAI(space.schema));
13
16
  const folderRef = afterPrefix(uri, folderUri(env, ""));
14
17
  if (folderRef !== void 0) {
@@ -17,19 +20,22 @@ var readSchemaResource = (space, env, uri) => {
17
20
  }
18
21
  const pageItem = afterPrefix(uri, pageUri(env, ""));
19
22
  if (pageItem !== void 0) {
20
- if (pageItem.endsWith("/styles")) {
21
- const ref = pageItem.slice(0, -7);
22
- const page = findRootByRef(space.schema, ref);
23
- return page ? envelope(pageStylesToAI(space.schema, space.style, page)) : null;
24
- }
25
- const page = findRootByRef(space.schema, pageItem);
26
- return page ? envelope(pageSkeletonToAI(space.schema, page, space.style)) : null;
23
+ const styles = pageItem.endsWith("/styles");
24
+ const ref = styles ? pageItem.slice(0, -7) : pageItem;
25
+ const component = findComponentByRef(space.schema, ref);
26
+ const target = component ? componentView(space, component) : space;
27
+ const page = findRootByRef(target.schema, component ? component.rootId : ref);
28
+ if (!page) return null;
29
+ return envelope(styles ? pageStylesToAI(target.schema, target.style, page) : pageSkeletonToAI(target.schema, page, target.style));
27
30
  }
28
31
  const elementRef = afterPrefix(uri, elementUri(env, ""));
29
32
  if (elementRef !== void 0) {
30
- const el = findElementByRef(space.schema, elementRef);
33
+ const componentId = treeOf(space.schema, elementRef)?.componentId;
34
+ const component = componentId ? findComponentByRef(space.schema, componentId) : void 0;
35
+ const target = component ? componentView(space, component) : space;
36
+ const el = findElementByRef(target.schema, elementRef);
31
37
  if (!el) return null;
32
- const view = elementView(space.schema, el, space.style);
38
+ const view = elementView(target.schema, el, target.style);
33
39
  return {
34
40
  stateVersion: view.version,
35
41
  data: view.detail
@@ -69,7 +69,7 @@ var createMcpServer = async ({ adapters, getGrant, preview, screenshot, logger,
69
69
  const getSpace = () => spacePromise ??= loadSpace();
70
70
  const server = new McpServer({
71
71
  name: "plitzi-mcp",
72
- version: "0.37.7"
72
+ version: "0.37.9"
73
73
  }, { instructions: hasSpace ? serverInstructions : widgetsOnlyInstructions });
74
74
  const { getChanges } = adapters;
75
75
  registerResources(server, getSpace, MCP_ENV, log, hasSpace, getChanges ? (query) => getChanges(requireSpaceId(), MCP_ENV, query) : void 0);
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,5 @@
1
+ import { componentView, findComponentByRef } from "../../helpers/components.js";
2
+ import { pageUri } from "../../helpers/uris.js";
1
3
  import { fail } from "../../helpers/opResult.js";
2
4
  import { deleteAction } from "../operations/actions/deleteAction.js";
3
5
  import { patchAction } from "../operations/actions/patchAction.js";
@@ -9,6 +11,8 @@ import { deleteFunctionFile, upsertFunctionFile } from "../operations/functions/
9
11
  import { deleteBinding } from "../operations/schema/bindings/deleteBinding.js";
10
12
  import { patchBinding } from "../operations/schema/bindings/patchBinding.js";
11
13
  import { upsertBinding } from "../operations/schema/bindings/upsertBinding.js";
14
+ import { deleteComponent } from "../operations/schema/components/deleteComponent.js";
15
+ import { upsertComponent } from "../operations/schema/components/upsertComponent.js";
12
16
  import { deleteElement } from "../operations/schema/elements/deleteElement.js";
13
17
  import { moveElement } from "../operations/schema/elements/moveElement.js";
14
18
  import { patchElement } from "../operations/schema/elements/patchElement.js";
@@ -41,6 +45,25 @@ import { deleteStyleVariable } from "../operations/style/variables/deleteStyleVa
41
45
  import { upsertStyleVariable } from "../operations/style/variables/upsertStyleVariable.js";
42
46
  import { isStyleOp } from "../operations/index.js";
43
47
  //#region src/modules/mcp/tools/apply/dispatch.ts
48
+ var isRooted = (op) => "pageRef" in op;
49
+ /**
50
+ * An element op names its root with `pageRef`, and a component is one: the op runs on the component's tree as if it
51
+ * were the space's only one — rooted at the component's root — so every element op works inside a component with no
52
+ * code of its own. What it made stale is the component, read as `pages/<component>`.
53
+ */
54
+ var executeRooted = (space, env, op) => {
55
+ const component = isRooted(op) ? findComponentByRef(space.schema, op.pageRef) : void 0;
56
+ if (!component || !isRooted(op)) return executeOp(space, env, op);
57
+ const retargeted = {
58
+ ...op,
59
+ pageRef: component.rootId
60
+ };
61
+ const result = executeOp(componentView(space, component), env, retargeted);
62
+ return {
63
+ ...result,
64
+ staleResources: result.staleResources.map((uri) => uri === pageUri(env, component.rootId) ? pageUri(env, component.id) : uri)
65
+ };
66
+ };
44
67
  var executeOp = (space, env, op) => {
45
68
  switch (op.type) {
46
69
  case "upsertElement": return upsertElement(space, env, op);
@@ -62,6 +85,8 @@ var executeOp = (space, env, op) => {
62
85
  case "patchInteractionNode": return patchInteractionNode(space, env, op);
63
86
  case "deleteInteraction": return deleteInteraction(space, env, op);
64
87
  case "patchSettings": return patchSettings(space, env, op);
88
+ case "upsertComponent": return upsertComponent(space, env, op);
89
+ case "deleteComponent": return deleteComponent(space, env, op);
65
90
  case "upsertDefinition": return upsertDefinition(space, env, op);
66
91
  case "upsertDefinitions": return upsertDefinitions(space, env, op);
67
92
  case "patchDefinition": return patchDefinition(space, env, op);
@@ -113,7 +138,7 @@ var applyOperations = (space, env, ops) => {
113
138
  const droppedActions = /* @__PURE__ */ new Set();
114
139
  for (let i = 0; i < ops.length; i++) {
115
140
  const op = ops[i];
116
- const result = executeOp(space, env, op);
141
+ const result = executeRooted(space, env, op);
117
142
  if (result.errors) {
118
143
  outcome.errors.push(...result.errors.map((e) => ({
119
144
  ...e,
@@ -269,6 +269,17 @@ export declare const applyShape: {
269
269
  }>>;
270
270
  presence: z.ZodOptional<z.ZodBoolean>;
271
271
  }, z.core.$strip>>>>;
272
+ }, z.core.$strip>, z.ZodObject<{
273
+ type: z.ZodLiteral<"upsertComponent">;
274
+ ref: z.ZodString;
275
+ label: z.ZodOptional<z.ZodString>;
276
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
277
+ slots: z.ZodOptional<z.ZodArray<z.ZodString>>;
278
+ fromRef: z.ZodOptional<z.ZodString>;
279
+ instanceRef: z.ZodOptional<z.ZodString>;
280
+ }, z.core.$strip>, z.ZodObject<{
281
+ type: z.ZodLiteral<"deleteComponent">;
282
+ ref: z.ZodString;
272
283
  }, z.core.$strip>, z.ZodObject<{
273
284
  desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
274
285
  tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
@@ -265,6 +265,17 @@ export declare const documentOperation: z.ZodDiscriminatedUnion<readonly [z.ZodO
265
265
  }>>;
266
266
  presence: z.ZodOptional<z.ZodBoolean>;
267
267
  }, z.core.$strip>>>>;
268
+ }, z.core.$strip>, z.ZodObject<{
269
+ type: z.ZodLiteral<"upsertComponent">;
270
+ ref: z.ZodString;
271
+ label: z.ZodOptional<z.ZodString>;
272
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
273
+ slots: z.ZodOptional<z.ZodArray<z.ZodString>>;
274
+ fromRef: z.ZodOptional<z.ZodString>;
275
+ instanceRef: z.ZodOptional<z.ZodString>;
276
+ }, z.core.$strip>, z.ZodObject<{
277
+ type: z.ZodLiteral<"deleteComponent">;
278
+ ref: z.ZodString;
268
279
  }, z.core.$strip>, z.ZodObject<{
269
280
  desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
270
281
  tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
@@ -1022,6 +1033,17 @@ export declare const operation: z.ZodDiscriminatedUnion<[z.ZodObject<{
1022
1033
  }>>;
1023
1034
  presence: z.ZodOptional<z.ZodBoolean>;
1024
1035
  }, z.core.$strip>>>>;
1036
+ }, z.core.$strip>, z.ZodObject<{
1037
+ type: z.ZodLiteral<"upsertComponent">;
1038
+ ref: z.ZodString;
1039
+ label: z.ZodOptional<z.ZodString>;
1040
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1041
+ slots: z.ZodOptional<z.ZodArray<z.ZodString>>;
1042
+ fromRef: z.ZodOptional<z.ZodString>;
1043
+ instanceRef: z.ZodOptional<z.ZodString>;
1044
+ }, z.core.$strip>, z.ZodObject<{
1045
+ type: z.ZodLiteral<"deleteComponent">;
1046
+ ref: z.ZodString;
1025
1047
  }, z.core.$strip>, z.ZodObject<{
1026
1048
  desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
1027
1049
  tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
@@ -2051,6 +2073,17 @@ export declare const operations: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject
2051
2073
  }>>;
2052
2074
  presence: z.ZodOptional<z.ZodBoolean>;
2053
2075
  }, z.core.$strip>>>>;
2076
+ }, z.core.$strip>, z.ZodObject<{
2077
+ type: z.ZodLiteral<"upsertComponent">;
2078
+ ref: z.ZodString;
2079
+ label: z.ZodOptional<z.ZodString>;
2080
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
2081
+ slots: z.ZodOptional<z.ZodArray<z.ZodString>>;
2082
+ fromRef: z.ZodOptional<z.ZodString>;
2083
+ instanceRef: z.ZodOptional<z.ZodString>;
2084
+ }, z.core.$strip>, z.ZodObject<{
2085
+ type: z.ZodLiteral<"deleteComponent">;
2086
+ ref: z.ZodString;
2054
2087
  }, z.core.$strip>, z.ZodObject<{
2055
2088
  desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
2056
2089
  tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
@@ -3075,6 +3108,17 @@ export declare const documentOperations: z.ZodArray<z.ZodDiscriminatedUnion<read
3075
3108
  }>>;
3076
3109
  presence: z.ZodOptional<z.ZodBoolean>;
3077
3110
  }, z.core.$strip>>>>;
3111
+ }, z.core.$strip>, z.ZodObject<{
3112
+ type: z.ZodLiteral<"upsertComponent">;
3113
+ ref: z.ZodString;
3114
+ label: z.ZodOptional<z.ZodString>;
3115
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
3116
+ slots: z.ZodOptional<z.ZodArray<z.ZodString>>;
3117
+ fromRef: z.ZodOptional<z.ZodString>;
3118
+ instanceRef: z.ZodOptional<z.ZodString>;
3119
+ }, z.core.$strip>, z.ZodObject<{
3120
+ type: z.ZodLiteral<"deleteComponent">;
3121
+ ref: z.ZodString;
3078
3122
  }, z.core.$strip>, z.ZodObject<{
3079
3123
  desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
3080
3124
  tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
@@ -28,6 +28,8 @@ var documentOps = [
28
28
  elementOps.patchInteractionNode,
29
29
  elementOps.deleteInteraction,
30
30
  elementOps.patchSettings,
31
+ elementOps.upsertComponent,
32
+ elementOps.deleteComponent,
31
33
  styleOps.upsertDefinition,
32
34
  styleOps.upsertDefinitions,
33
35
  styleOps.patchDefinition,
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+ import { OpResult, Space } from '../../../../helpers';
3
+ import { Env } from '../../../../types';
4
+ export declare const deleteComponentOp: z.ZodObject<{
5
+ type: z.ZodLiteral<"deleteComponent">;
6
+ ref: z.ZodString;
7
+ }, z.core.$strip>;
8
+ export type DeleteComponent = z.infer<typeof deleteComponentOp>;
9
+ export declare const deleteComponent: (space: Space, env: Env, op: DeleteComponent) => OpResult;
@@ -0,0 +1,21 @@
1
+ import { componentsUri } from "../../../../helpers/uris.js";
2
+ import { empty, fail } from "../../../../helpers/opResult.js";
3
+ import { componentNamed, instancesOf, removeComponent } from "@plitzi/sdk-schema/helpers/components";
4
+ import { z } from "zod";
5
+ //#region src/modules/mcp/tools/operations/schema/components/deleteComponent.ts
6
+ var deleteComponentOp = z.object({
7
+ type: z.literal("deleteComponent"),
8
+ ref: z.string()
9
+ }).describe("Remove a component nothing places any more.");
10
+ var deleteComponent = (space, env, op) => {
11
+ if (!componentNamed(space.schema, op.ref)) return fail("ref", `Component "${op.ref}" not found`, `Read plitzi://schema/${env}/components for valid refs`);
12
+ const placed = instancesOf(space.schema, op.ref);
13
+ if (placed.length > 0 || !removeComponent(space.schema, op.ref)) return fail("ref", `Component "${op.ref}" is still placed by ${placed.map((instance) => `"${instance.id}"`).join(", ")}`, "detachInstance or deleteElement each of them first");
14
+ return {
15
+ ...empty(),
16
+ deleted: 1,
17
+ staleResources: [componentsUri(env)]
18
+ };
19
+ };
20
+ //#endregion
21
+ export { deleteComponent, deleteComponentOp };
@@ -0,0 +1,2 @@
1
+ export * from './deleteComponent';
2
+ export * from './upsertComponent';
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod';
2
+ import { OpResult, Space } from '../../../../helpers';
3
+ import { Env } from '../../../../types';
4
+ export declare const upsertComponentOp: z.ZodObject<{
5
+ type: z.ZodLiteral<"upsertComponent">;
6
+ ref: z.ZodString;
7
+ label: z.ZodOptional<z.ZodString>;
8
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
9
+ slots: z.ZodOptional<z.ZodArray<z.ZodString>>;
10
+ fromRef: z.ZodOptional<z.ZodString>;
11
+ instanceRef: z.ZodOptional<z.ZodString>;
12
+ }, z.core.$strip>;
13
+ export type UpsertComponent = z.infer<typeof upsertComponentOp>;
14
+ export declare const upsertComponent: (space: Space, env: Env, op: UpsertComponent) => OpResult;
@@ -0,0 +1,147 @@
1
+ import { invalidateIndex } from "../../../../helpers/space.js";
2
+ import { componentsUri, pageUri } from "../../../../helpers/uris.js";
3
+ import { empty, fail } from "../../../../helpers/opResult.js";
4
+ import { ID_REF_HINT, guardNewRef } from "../write.js";
5
+ import { addComponent, componentNamed, propNameProblem, treeOf, updateComponent } from "@plitzi/sdk-schema/helpers/components";
6
+ import { descendants } from "@plitzi/sdk-schema/helpers/elementTree";
7
+ import { z } from "zod";
8
+ import { isValidElementId } from "@plitzi/sdk-schema/helpers/elementId";
9
+ //#region src/modules/mcp/tools/operations/schema/components/upsertComponent.ts
10
+ /**
11
+ * A prop as a component declares it. Checked when the op runs rather than spelled out in the op's schema: the op union
12
+ * rides on every request of every conversation, and the guide — read once — says what a prop is.
13
+ */
14
+ var componentProp = z.object({
15
+ type: z.enum([
16
+ "text",
17
+ "textarea",
18
+ "number",
19
+ "boolean",
20
+ "select",
21
+ "scalar",
22
+ "json"
23
+ ]),
24
+ description: z.string(),
25
+ required: z.boolean().optional(),
26
+ default: z.union([
27
+ z.string(),
28
+ z.number(),
29
+ z.boolean()
30
+ ]).optional(),
31
+ options: z.array(z.string()).optional(),
32
+ label: z.string().optional()
33
+ });
34
+ var upsertComponentOp = z.object({
35
+ type: z.literal("upsertComponent"),
36
+ ref: z.string(),
37
+ label: z.string().optional(),
38
+ props: z.record(z.string(), z.unknown()).optional().describe("{ name: { type, description, required?, default?, options? } }"),
39
+ slots: z.array(z.string()).optional(),
40
+ fromRef: z.string().optional().describe("New only: the element it is made of"),
41
+ instanceRef: z.string().optional().describe("With fromRef: the instance left in its place")
42
+ }).describe("Declare or update a component; edit inside it with pageRef: \"<ref>\". See \"Components\" in plitzi://guide.");
43
+ /** The declaration an op writes, with nothing it leaves out — an update keeps what it does not name. */
44
+ var declarationOf = (op, props) => ({
45
+ ...op.label === void 0 ? {} : { label: op.label },
46
+ ...props === void 0 ? {} : { props },
47
+ ...op.slots === void 0 ? {} : { slots: op.slots }
48
+ });
49
+ /** The props an op declares, each held to what a prop is — or why one is not, by name. */
50
+ var parseProps = (props) => {
51
+ if (props === void 0) return {};
52
+ const parsed = {};
53
+ for (const [name, value] of Object.entries(props)) {
54
+ const result = componentProp.safeParse(value);
55
+ if (!result.success) return { error: `prop "${name}" is not a prop: ${result.error.issues.map((issue) => `${issue.path.join(".") || "it"} ${issue.message}`).join("; ")}` };
56
+ parsed[name] = result.data;
57
+ }
58
+ return { props: parsed };
59
+ };
60
+ /** Why a declaration cannot stand, in words, or undefined: the same two rules `sdk-schema` holds every writer to. */
61
+ var declarationProblem = (declaration, flat) => {
62
+ for (const name of Object.keys(declaration.props ?? {})) {
63
+ const problem = propNameProblem(name);
64
+ if (problem) return problem;
65
+ }
66
+ const missing = (declaration.slots ?? []).find((slot) => !Object.hasOwn(flat, slot));
67
+ return missing ? `slot "${missing}" is not an element of the component` : void 0;
68
+ };
69
+ var upsertComponent = (space, env, op) => {
70
+ const { schema } = space;
71
+ const stale = [componentsUri(env), pageUri(env, op.ref)];
72
+ const props = parseProps(op.props);
73
+ if ("error" in props) return fail("props", props.error, "A prop is { type: \"text\" | \"textarea\" | \"number\" | \"boolean\" | \"select\" | \"scalar\" | \"json\", description, required?, default?, options? }");
74
+ const declaration = declarationOf(op, props.props);
75
+ const existing = componentNamed(schema, op.ref);
76
+ if (existing) {
77
+ if (op.fromRef) return fail("fromRef", `Component "${op.ref}" already exists`, "fromRef makes a NEW component; pick another ref");
78
+ const problem = declarationProblem({
79
+ ...existing,
80
+ ...declaration
81
+ }, existing.flat);
82
+ if (problem || !updateComponent(schema, op.ref, {
83
+ ...existing,
84
+ ...declaration
85
+ })) return fail("props", problem ?? `Component "${op.ref}" refused the declaration`, "Name props as {{ props.<name> }} reads them, and slots as elements of the tree");
86
+ return {
87
+ ...empty(),
88
+ updated: 1,
89
+ staleResources: stale
90
+ };
91
+ }
92
+ if (!isValidElementId(op.ref)) return fail("ref", `"${op.ref}" is not a name a component can have`, ID_REF_HINT);
93
+ if (op.fromRef) {
94
+ const tree = treeOf(schema, op.fromRef);
95
+ const element = tree?.flat[op.fromRef];
96
+ if (!tree || !element?.definition.parentId) return fail("fromRef", `"${op.fromRef}" is not an element inside a page, a layout or a component`, "A page or a layout is a root, not a subtree — name an element inside one");
97
+ if (!op.instanceRef) return fail("instanceRef", "fromRef needs the name of the instance that takes the element’s place", "e.g. instanceRef: \"hero-card\"");
98
+ const guard = guardNewRef(space, op.instanceRef, "instanceRef");
99
+ if (guard) return guard;
100
+ const component = {
101
+ ...declaration,
102
+ id: op.ref,
103
+ rootId: "",
104
+ flat: {}
105
+ };
106
+ const problem = declarationProblem(declaration, Object.fromEntries([op.fromRef, ...descendants(tree.flat, op.fromRef)].map((id) => [id, tree.flat[id]])));
107
+ if (problem || !addComponent(schema, component, {
108
+ elementId: op.fromRef,
109
+ instanceId: op.instanceRef
110
+ })) return fail("props", problem ?? `"${op.fromRef}" could not be made a component`, "Slots name elements inside fromRef");
111
+ invalidateIndex(schema);
112
+ return {
113
+ ...empty(),
114
+ created: 1,
115
+ staleResources: stale,
116
+ elementRefs: [op.instanceRef]
117
+ };
118
+ }
119
+ const rootRef = `${op.ref}-root`;
120
+ const guard = guardNewRef(space, rootRef, "ref");
121
+ if (guard) return guard;
122
+ const component = {
123
+ ...declaration,
124
+ id: op.ref,
125
+ rootId: rootRef,
126
+ flat: { [rootRef]: {
127
+ id: rootRef,
128
+ attributes: { subType: "div" },
129
+ definition: {
130
+ label: op.label ?? op.ref,
131
+ type: "container",
132
+ rootId: rootRef,
133
+ items: [],
134
+ styleSelectors: { base: "" }
135
+ }
136
+ } }
137
+ };
138
+ const problem = declarationProblem(declaration, component.flat);
139
+ if (problem || !addComponent(schema, component)) return fail("props", problem ?? `Component "${op.ref}" could not be declared`, "A new component has one element yet — its root; add the rest with upsertElement { pageRef: \"<ref>\" } first, then name slots");
140
+ return {
141
+ ...empty(),
142
+ created: 1,
143
+ staleResources: stale
144
+ };
145
+ };
146
+ //#endregion
147
+ export { upsertComponent, upsertComponentOp };
@@ -21,3 +21,4 @@ export * from './interactions/upsertInteractionFlow';
21
21
  export * from './interactions/patchInteractionNode';
22
22
  export * from './interactions/deleteInteraction';
23
23
  export * from './settings/patchSettings';
24
+ export * from './components';
@@ -283,4 +283,17 @@ export declare const elementOps: {
283
283
  presence: import('zod').ZodOptional<import('zod').ZodBoolean>;
284
284
  }, import('zod/v4/core').$strip>>>>;
285
285
  }, import('zod/v4/core').$strip>;
286
+ upsertComponent: import('zod').ZodObject<{
287
+ type: import('zod').ZodLiteral<"upsertComponent">;
288
+ ref: import('zod').ZodString;
289
+ label: import('zod').ZodOptional<import('zod').ZodString>;
290
+ props: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnknown>>;
291
+ slots: import('zod').ZodOptional<import('zod').ZodArray<import('zod').ZodString>>;
292
+ fromRef: import('zod').ZodOptional<import('zod').ZodString>;
293
+ instanceRef: import('zod').ZodOptional<import('zod').ZodString>;
294
+ }, import('zod/v4/core').$strip>;
295
+ deleteComponent: import('zod').ZodObject<{
296
+ type: import('zod').ZodLiteral<"deleteComponent">;
297
+ ref: import('zod').ZodString;
298
+ }, import('zod/v4/core').$strip>;
286
299
  };
@@ -1,6 +1,8 @@
1
1
  import { deleteBindingOp } from "./bindings/deleteBinding.js";
2
2
  import { patchBindingOp } from "./bindings/patchBinding.js";
3
3
  import { upsertBindingOp } from "./bindings/upsertBinding.js";
4
+ import { deleteComponentOp } from "./components/deleteComponent.js";
5
+ import { upsertComponentOp } from "./components/upsertComponent.js";
4
6
  import { deleteElementOp } from "./elements/deleteElement.js";
5
7
  import { moveElementOp } from "./elements/moveElement.js";
6
8
  import { patchElementOp } from "./elements/patchElement.js";
@@ -39,7 +41,9 @@ var elementOps = {
39
41
  upsertInteractionFlow: upsertInteractionFlowOp,
40
42
  patchInteractionNode: patchInteractionNodeOp,
41
43
  deleteInteraction: deleteInteractionOp,
42
- patchSettings: patchSettingsOp
44
+ patchSettings: patchSettingsOp,
45
+ upsertComponent: upsertComponentOp,
46
+ deleteComponent: deleteComponentOp
43
47
  };
44
48
  //#endregion
45
49
  export { elementOps };
@@ -1,6 +1,7 @@
1
1
  import { findRootByRef, indexAddElement, resolveRef } from "../../../helpers/space.js";
2
2
  import { folderUri, foldersUri, layoutsUri, pageUri, pagesUri, schemaVarsUri, settingsUri } from "../../../helpers/uris.js";
3
3
  import { fail } from "../../../helpers/opResult.js";
4
+ import { documentIds } from "@plitzi/sdk-schema/helpers/components";
4
5
  import { elementIdConflict, isValidElementId } from "@plitzi/sdk-schema/helpers/elementId";
5
6
  import FlatMap from "@plitzi/sdk-schema/helpers/FlatMap";
6
7
  //#region src/modules/mcp/tools/operations/schema/write.ts
@@ -12,7 +13,8 @@ var collectInputRefs = (input) => [input.ref, ...(input.children ?? []).flatMap(
12
13
  * it. Both rules are sdk-schema's `elementIdConflict` — never restated here, so the MCP cannot drift from what
13
14
  * the builder and the schema validator enforce. Null when the name is usable. */
14
15
  var guardNewRef = (space, ref, field) => {
15
- const conflict = elementIdConflict(space.schema.flat, ref);
16
+ const elsewhere = documentIds(space.document ?? space.schema).has(ref) ? `"${ref}" is already used by another element here` : null;
17
+ const conflict = elementIdConflict(space.schema.flat, ref) ?? elsewhere;
16
18
  if (!conflict) return null;
17
19
  return isValidElementId(ref) ? fail(field, conflict, TAKEN_HINT) : fail(field, conflict, ID_REF_HINT);
18
20
  };
@@ -265,6 +265,17 @@ export declare const previewShape: {
265
265
  }>>;
266
266
  presence: z.ZodOptional<z.ZodBoolean>;
267
267
  }, z.core.$strip>>>>;
268
+ }, z.core.$strip>, z.ZodObject<{
269
+ type: z.ZodLiteral<"upsertComponent">;
270
+ ref: z.ZodString;
271
+ label: z.ZodOptional<z.ZodString>;
272
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
273
+ slots: z.ZodOptional<z.ZodArray<z.ZodString>>;
274
+ fromRef: z.ZodOptional<z.ZodString>;
275
+ instanceRef: z.ZodOptional<z.ZodString>;
276
+ }, z.core.$strip>, z.ZodObject<{
277
+ type: z.ZodLiteral<"deleteComponent">;
278
+ ref: z.ZodString;
268
279
  }, z.core.$strip>, z.ZodObject<{
269
280
  desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
270
281
  tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
@@ -267,6 +267,17 @@ export declare const renderShape: {
267
267
  }>>;
268
268
  presence: z.ZodOptional<z.ZodBoolean>;
269
269
  }, z.core.$strip>>>>;
270
+ }, z.core.$strip>, z.ZodObject<{
271
+ type: z.ZodLiteral<"upsertComponent">;
272
+ ref: z.ZodString;
273
+ label: z.ZodOptional<z.ZodString>;
274
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
275
+ slots: z.ZodOptional<z.ZodArray<z.ZodString>>;
276
+ fromRef: z.ZodOptional<z.ZodString>;
277
+ instanceRef: z.ZodOptional<z.ZodString>;
278
+ }, z.core.$strip>, z.ZodObject<{
279
+ type: z.ZodLiteral<"deleteComponent">;
280
+ ref: z.ZodString;
270
281
  }, z.core.$strip>, z.ZodObject<{
271
282
  desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
272
283
  tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
@@ -265,6 +265,17 @@ export declare const screenshotShape: {
265
265
  }>>;
266
266
  presence: z.ZodOptional<z.ZodBoolean>;
267
267
  }, z.core.$strip>>>>;
268
+ }, z.core.$strip>, z.ZodObject<{
269
+ type: z.ZodLiteral<"upsertComponent">;
270
+ ref: z.ZodString;
271
+ label: z.ZodOptional<z.ZodString>;
272
+ props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
273
+ slots: z.ZodOptional<z.ZodArray<z.ZodString>>;
274
+ fromRef: z.ZodOptional<z.ZodString>;
275
+ instanceRef: z.ZodOptional<z.ZodString>;
276
+ }, z.core.$strip>, z.ZodObject<{
277
+ type: z.ZodLiteral<"deleteComponent">;
278
+ ref: z.ZodString;
268
279
  }, z.core.$strip>, z.ZodObject<{
269
280
  desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
270
281
  tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
@@ -8,7 +8,7 @@ var batchDeclaredVars = (ops) => {
8
8
  };
9
9
  var batchDeclaredPages = (ops) => {
10
10
  const refs = /* @__PURE__ */ new Set();
11
- for (const op of ops) if (op.type === "upsertPage" || op.type === "upsertLayout") refs.add(op.ref);
11
+ for (const op of ops) if (op.type === "upsertPage" || op.type === "upsertLayout" || op.type === "upsertComponent") refs.add(op.ref);
12
12
  return refs;
13
13
  };
14
14
  var batchDeclaredFolders = (ops) => {
@@ -1,6 +1,7 @@
1
1
  import { observedDataSources, observedInteractionActions } from "../../../catalogs/observed.js";
2
2
  import { buildTypeRegistry } from "../../../catalogs/registry.js";
3
3
  import { elementTypeNames } from "../../../catalogs/index.js";
4
+ import { componentView, findComponentByRef } from "../../../helpers/components.js";
4
5
  import { findElementByRef, findFolderByRef, findPageByRef, findRootByRef, folderAncestorIds, getLayoutElements, getPageElements, pageFoldersOf, resolveRef, routeParamNames } from "../../../helpers/space.js";
5
6
  import { MAX_OPS } from "../../operations/index.js";
6
7
  import { batchDeclaredElements, batchDeclaredFolders, batchDeclaredPages, batchDeclaredVariants, batchDeclaredVars } from "./batch.js";
@@ -11,6 +12,7 @@ import { checkSlotCss } from "./css.js";
11
12
  import { checkRef } from "./refs.js";
12
13
  import { checkElementInput, checkRawMarkup, checkTypeProps, checkVariantApplication } from "./elements.js";
13
14
  import { checkInteractionNode } from "./interactions.js";
15
+ import { treeOf } from "@plitzi/sdk-schema/helpers/components";
14
16
  import { isValidElementId } from "@plitzi/sdk-schema/helpers/elementId";
15
17
  //#region src/modules/mcp/tools/shared/validator/index.ts
16
18
  var STYLE_CATEGORIES = [
@@ -50,8 +52,8 @@ var buildValidationCtx = (space, ops, mode = "space") => {
50
52
  knownTypes: /* @__PURE__ */ new Set([...Object.keys(registry.types), ...elementTypeNames]),
51
53
  typeProps: new Map(Object.entries(registry.types).map(([type, info]) => [type, new Set(Object.keys(info.props))])),
52
54
  typeMeta: buildTypeMeta(space.catalog),
53
- elementType: (ref) => (findElementByRef(space.schema, ref) ?? findPageByRef(space.schema, ref))?.definition.type,
54
- elementExists: (ref) => batchElements.has(ref) || Boolean(findElementByRef(space.schema, ref) ?? findPageByRef(space.schema, ref)),
55
+ elementType: (ref) => treeOf(space.schema, ref)?.flat[ref]?.definition.type,
56
+ elementExists: (ref) => batchElements.has(ref) || treeOf(space.schema, ref) !== void 0,
55
57
  schemaVars: /* @__PURE__ */ new Set([
56
58
  ...space.schema.variables.map((v) => v.name),
57
59
  ...routeParamNames(space.schema),
@@ -81,13 +83,19 @@ var validateOperations = (space, ops, mode = "space") => {
81
83
  });
82
84
  ops.forEach((op, i) => {
83
85
  const base = `operations[${i}]`;
86
+ const component = "pageRef" in op ? findComponentByRef(space.schema, op.pageRef) : void 0;
87
+ const scoped = component ? componentView(space, component).schema : space.schema;
84
88
  if ((op.type === "upsertElement" || op.type === "patchElement" || op.type === "deleteElement" || op.type === "moveElement" || op.type === "upsertBinding" || op.type === "patchBinding" || op.type === "deleteBinding" || op.type === "upsertInteractionFlow" || op.type === "patchInteractionNode" || op.type === "deleteInteraction") && op.pageRef) {
85
- if (!findRootByRef(space.schema, op.pageRef) && !batchPages.has(op.pageRef)) {
86
- const validRefs = [...getPageElements(space.schema).map((page) => page.id), ...getLayoutElements(space.schema).map((layout) => layout.id)];
89
+ if (!component && !findRootByRef(space.schema, op.pageRef) && !batchPages.has(op.pageRef)) {
90
+ const validRefs = [
91
+ ...getPageElements(space.schema).map((page) => page.id),
92
+ ...getLayoutElements(space.schema).map((layout) => layout.id),
93
+ ...Object.keys(space.schema.components)
94
+ ];
87
95
  ctx.errors.push({
88
96
  path: `${base}.pageRef`,
89
- message: `Page or layout "${op.pageRef}" does not exist`,
90
- hint: "Use an existing page or layout ref, or create the page with upsertPage earlier in the same batch",
97
+ message: `Page, layout or component "${op.pageRef}" does not exist`,
98
+ hint: "Use an existing page, layout or component ref, or create it earlier in the same batch",
91
99
  validValues: validRefs
92
100
  });
93
101
  }
@@ -100,8 +108,8 @@ var validateOperations = (space, ops, mode = "space") => {
100
108
  break;
101
109
  case "patchElement": {
102
110
  checkRef(op.ref, `${base}.ref`, ctx);
103
- const page = findRootByRef(space.schema, op.pageRef);
104
- const target = page ? resolveRef(space.schema, page, op.ref) : void 0;
111
+ const page = findRootByRef(scoped, component ? component.rootId : op.pageRef);
112
+ const target = page ? resolveRef(scoped, page, op.ref) : void 0;
105
113
  if (op.props && target && target.id !== page?.id) {
106
114
  checkRawMarkup(target.definition.type, op.props, base, ctx);
107
115
  checkTypeProps(target.definition.type, op.props, base, ctx);
@@ -230,7 +238,7 @@ var validateOperations = (space, ops, mode = "space") => {
230
238
  break;
231
239
  case "patchInteractionNode": {
232
240
  checkRef(op.ref, `${base}.ref`, ctx);
233
- const existing = findElementByRef(space.schema, op.ref)?.definition.interactions?.[op.nodeId];
241
+ const existing = findElementByRef(scoped, op.ref)?.definition.interactions?.[op.nodeId];
234
242
  if (existing) {
235
243
  const merged = {
236
244
  id: existing.id,
@@ -266,6 +266,17 @@ export declare const validateShape: {
266
266
  }>>;
267
267
  presence: import('zod').ZodOptional<import('zod').ZodBoolean>;
268
268
  }, import('zod/v4/core').$strip>>>>;
269
+ }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
270
+ type: import('zod').ZodLiteral<"upsertComponent">;
271
+ ref: import('zod').ZodString;
272
+ label: import('zod').ZodOptional<import('zod').ZodString>;
273
+ props: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnknown>>;
274
+ slots: import('zod').ZodOptional<import('zod').ZodArray<import('zod').ZodString>>;
275
+ fromRef: import('zod').ZodOptional<import('zod').ZodString>;
276
+ instanceRef: import('zod').ZodOptional<import('zod').ZodString>;
277
+ }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
278
+ type: import('zod').ZodLiteral<"deleteComponent">;
279
+ ref: import('zod').ZodString;
269
280
  }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
270
281
  desktop: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
271
282
  tablet: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
@@ -49,6 +49,26 @@ export interface AILayoutSummary {
49
49
  slots: string[];
50
50
  elementCount: number;
51
51
  }
52
+ /** A component of the space: a reusable subtree, placed on a page as an instance and edited as a root of its own. */
53
+ export interface AIComponentSummary {
54
+ ref: string;
55
+ label: string;
56
+ /** What an instance hands in, each an attribute of the instance and read inside as `{{ props.<name> }}`. */
57
+ props: Record<string, {
58
+ type: string;
59
+ description: string;
60
+ required?: boolean;
61
+ default?: unknown;
62
+ options?: string[];
63
+ }>;
64
+ /** Elements of its tree an instance fills with its children, each child naming the one it fills (`slot`). */
65
+ slots: string[];
66
+ /** The root of its tree: `pageRef` for every element op inside it is the component's ref. */
67
+ rootRef: string;
68
+ /** How many places render it. */
69
+ instances: number;
70
+ elementCount: number;
71
+ }
52
72
  /** A page folder in the sidebar tree. `ref` is the folder's id; pages reference it
53
73
  * by that id via their `folder`, and nested folders via `parentId`. */
54
74
  export interface AIFolder {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plitzi/sdk-mcp",
3
- "version": "0.37.7",
3
+ "version": "0.37.9",
4
4
  "license": "AGPL-3.0",
5
5
  "files": [
6
6
  "dist",
@@ -30,14 +30,14 @@
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/ext-apps": "^1.7.5",
32
32
  "@modelcontextprotocol/sdk": "^1.30.1",
33
- "@plitzi/plitzi-sdk": "0.37.7",
34
- "@plitzi/sdk-authoring": "0.37.7",
35
- "@plitzi/sdk-elements": "0.37.7",
36
- "@plitzi/sdk-interactions": "0.37.7",
37
- "@plitzi/sdk-schema": "0.37.7",
38
- "@plitzi/sdk-server": "0.37.7",
39
- "@plitzi/sdk-shared": "0.37.7",
40
- "@plitzi/sdk-style": "0.37.7",
33
+ "@plitzi/plitzi-sdk": "0.37.9",
34
+ "@plitzi/sdk-authoring": "0.37.9",
35
+ "@plitzi/sdk-elements": "0.37.9",
36
+ "@plitzi/sdk-interactions": "0.37.9",
37
+ "@plitzi/sdk-schema": "0.37.9",
38
+ "@plitzi/sdk-server": "0.37.9",
39
+ "@plitzi/sdk-shared": "0.37.9",
40
+ "@plitzi/sdk-style": "0.37.9",
41
41
  "ejs": "^6.0.1",
42
42
  "esbuild": "^0.28.2",
43
43
  "zod": "^4.6.5"