@plitzi/sdk-mcp 0.38.13 → 0.38.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,268 @@
1
1
  # @plitzi/sdk-mcp
2
2
 
3
+ ## 0.38.15
4
+
5
+ ### Patch Changes
6
+
7
+ - 6cdd5cf: ## Security
8
+
9
+ - **A write another site caused is refused, cookie or none** (`@plitzi/sdk-server`, `@plitzi/sdk-shared`): the page
10
+ server guarded nothing it answers past `/auth` — a server action (`/_action`), a realtime publish, a space's `/fn/`
11
+ route, its runtime's endpoints, the deployment's own `data` stages, the adapter-only `loginPath`, `logoutPath` and
12
+ `exchangePath`. A plain `text/plain` POST from any page open in the same browser ran them, with the visitor's session
13
+ cookie when there was one (`SameSite=None` off this machine) and, in a project nobody signs in to, as whoever holds
14
+ the server's own credentials. Every unsafe request a browser says another site caused (`Sec-Fetch-Site: cross-site`,
15
+ or a foreign `Origin`) is now refused `403 reason: 'foreign'`, saying how to allow it: `crossSiteWrites.origins`
16
+ (with `auth`, `identity.platformOrigins` too) and `crossSiteWrites.paths` for a form other sites post to by design.
17
+ `Authorization`, and callers that are not browsers — webhooks, scripts — pass as before. One rule for this and the
18
+ sign-in check (`crossSiteRequest`, `csrf.ts`). A self-hosted runtime's endpoints now sit where the platform forwards a
19
+ space's — after the auth chain and this guard (`data`, was `preAuth`).
20
+
21
+ ## Testing
22
+ - **`page check` and `page shot` anywhere the project is served** (`@plitzi/cli`): `--url <origin>` for a second server
23
+ of it, staging or a deploy, instead of `npm start`'s port; `page check --space <module>` holds the pages to that space
24
+ instead of the project's — an agent's page — authored with `authorIsolated`.
25
+ - **A route with a query is checked as its page** (`@plitzi/cli`): `page check "/?v=2"` matched the query as part of
26
+ the path, found no page, and checked the space's "not found" page in its place.
27
+
28
+ - **What an element that chooses its children draws is checked** (`@plitzi/sdk-authoring`, `@plitzi/cli`): with
29
+ `choosesChildren`, the page checks owed none of its children — and checked none of them either: a dashboard drawing
30
+ three panels reported `checked: 1`. Each child it may draw, and what is inside one under no condition of its own,
31
+ now carries `shownWith` (the child it comes with) on its handle: once that child is on screen it and everything
32
+ named in it are owed like the rest of the page, and `checked` counts them. `onScreenIfChosen(handles, page)` lists
33
+ them.
34
+ - **A page that met a server error is looked at once more** (`@plitzi/cli`): `page check` and `verify` failed a page
35
+ for a `500` from somebody else's service having a bad second (`/fn/<space>/weather`), and passed on the next run with
36
+ nothing changed. A page whose requests got a `5xx` is checked again once; its report is the second look, and says
37
+ what failed the first time (`retriedAfter`). A `4xx` fails the same way twice, and is never retried.
38
+
39
+ ## Plugins
40
+ - **`useNotify`** (`@plitzi/plitzi-sdk`): a plugin shows the page's notifications — `notify('Deleted 79 recordings', {
41
+ appearance: 'danger' })` — the same toasts an `addNotification` step shows, in the space's theme and `notifications`
42
+ look, with no trigger, flow or `window` event of the space's to wire. The step's vocabulary (`NOTIFICATION_PLACEMENTS`,
43
+ `NOTIFICATION_APPEARANCES`, `NOTIFICATION_DEFAULTS`) is one list in `@plitzi/sdk-shared`, read by both.
44
+ - **`appearance: 'danger'` looks like one** (`@plitzi/plitzi-sdk`): it reached the toast library as a type it does not
45
+ know, so a danger notification showed neither the error icon nor the colour `notifications.danger` sets.
46
+ A value a notification does not take — from a flow, or a plugin in plain JavaScript — is set aside by name and said
47
+ in the console, rather than shown as the default in silence (`readNotificationOptions`, `@plitzi/sdk-shared`).
48
+ - **`useServerAction`** (`@plitzi/plitzi-sdk`, `@plitzi/sdk-authoring`, `@plitzi/sdk-interactions`,
49
+ `@plitzi/sdk-server`): a plugin runs one of the space's server actions, awaited —
50
+ `useServerAction<typeof habitDone>('habits-done')({ id })` answers `{ ok, output }` or `{ ok: false, reason, error }`
51
+ — through the `runServerAction` step itself: the same access, refusals, session handling and dev-tools entry.
52
+ `defineAction` keeps the literal types of its `call` way in, so the input is typed by the fields it declares (the
53
+ required ones required); `taskAction` types both ends from the task's `run`, and `FunctionTask<T, R>` takes what
54
+ `run` answers. `CallInputOf` / `CallOutputOf` read them. One operation is one action, never a `case` in one route.
55
+ What a run answers is typed (`ServerActionStatus`): a status the server never uses reads as a run that did not
56
+ complete, never as one that did.
57
+ The key those types ride under (`CALL_TYPES`) is exported, by `@plitzi/sdk-shared` and `@plitzi/sdk-authoring` with
58
+ `TypedActionEntry`, `CallInputOf` and `CallOutputOf`: a package that emits declarations could not write the type of an
59
+ action it exports (`TS4023` on every `export const actions = [defineAction(…)]`).
60
+ - **A plugin is drawn on the server first** (docs): a preference per device belongs in kept state (`keepState`,
61
+ `paintedState`) as a bound prop, never read from `localStorage` while rendering — React discards the whole tree on the
62
+ mismatch, and only in the browser of whoever saved something. `@plitzi/plitzi-ui` is not the page's: a plugin
63
+ importing it bundles its own copy, with its own contexts and its own look. The authoring skill's `plugin-reach.md`
64
+ says what a plugin says and to whom: a notification, another element (an event and the space's flow), a server
65
+ action, shared state, other pages.
66
+
67
+ ## Realtime
68
+ - **A channel that retains** (`@plitzi/sdk-shared`, `@plitzi/sdk-server`, `@plitzi/sdk-elements`): declared
69
+ `retain: true` (with `publish: 'server'`, which authoring requires), the server keeps the latest message of each type
70
+ on a topic — said by `ctx.publish` or a flow's `realtime.publish` — and hands them to every page that joins, marked
71
+ `retained`. A page reads them as `<channel>.retained.<type>`, a plugin as `useChannel(topic).retained`, the newest of
72
+ a type holding whichever order they arrive in; `null` forgets a type. Kept in the store the actions' `kv` uses (beside
73
+ the grants, by compare-and-swap), so every replica hands out the same; a topic keeps at most 64 types and 512 KB, and
74
+ a publish past that is refused before anything is sent. State every screen shares is one publish and one binding —
75
+ no kv key, initial read or copy in `state` of the project's own. The fleet store `realtime.grants` is now
76
+ `realtime.kv`.
77
+ - **The MCP writes every field a channel takes** (`@plitzi/sdk-mcp`): `patchSettings`'s channel had no `grant` — which
78
+ the guide taught — nor `retain` or the limits, so an agent could not declare a private channel at all. All are there
79
+ now, and the guide says what `retain` is for and that `channel_<id>.retained` holds it.
80
+
81
+ ## Authoring
82
+ - **`plitzi explain` knows every `@plitzi` package installed** (`@plitzi/cli`): a name that was not
83
+ `@plitzi/sdk-authoring`'s (`createServer`, `createJsonAdapters`, `useChannel`) was answered as the project's own code,
84
+ to `grep` in `src/`. It is read from the published types of every one installed, through their re-exports and entry
85
+ points (`@plitzi/sdk-server/handlers`), and a name two packages export — `createServer` is sdk-server's and sdk-mcp's
86
+ — is answered with both.
87
+
88
+ - **A space module somebody else wrote, authored where it reaches nothing** (`@plitzi/sdk-authoring`):
89
+ `authorIsolated(file, { timeoutMs })` of `@plitzi/sdk-authoring/node` runs `authorSpace` on it in a process Node's
90
+ permission model holds to reading the module's folder and its packages — no file written, no process started, no
91
+ variable of the caller's environment — kills one that runs too long, and validates what it answers again, so a forged
92
+ answer gains nothing. A refusal comes back as `authorSpace` says it, with the problems as data.
93
+ - **Pages an agent writes, served** (`@plitzi/sdk-server`): `createAuthoringHost({ dir, frameAncestors })` serves each
94
+ space it is handed at an address of its own (`put(space, { id }) → /?__pt=<id>`) as a draft of its one space, kept in
95
+ `dir` across restarts; `drop(id)` forgets one, and anything else answers `404` saying where the pages are.
96
+ - **A space is chosen by host or credential** (`@plitzi/sdk-server`, docs): `getSpaceDeployment` is asked on every
97
+ request, and what a page fetches after it carries nothing of its address but the host — a space chosen by the page's
98
+ query or path lost everything it asked for, and `/_plitzi/space/<hash>.json` answered a bare `404`. The contract and
99
+ the README say so (the README offered "by path prefix"), and that `404` says why.
100
+
101
+ - **`plitzi explain on` says how to read what a trigger carries** (`@plitzi/sdk-authoring`): its doc sat above a
102
+ helper, so `explain` printed only the signature. It now leads with `[named('picked', on('onPick')), …('{{ picked.seat
103
+ }}')]`, and the CLI skill's plugin section says the same.
104
+
105
+ - Updated dependencies [6cdd5cf]
106
+ - @plitzi/plitzi-sdk@0.38.15
107
+ - @plitzi/sdk-server@0.38.15
108
+ - @plitzi/sdk-authoring@0.38.15
109
+ - @plitzi/sdk-schema@0.38.15
110
+ - @plitzi/sdk-shared@0.38.15
111
+ - @plitzi/sdk-style@0.38.15
112
+
113
+ ## 0.38.14
114
+
115
+ ### Patch Changes
116
+
117
+ - 493955a: ## SDK
118
+
119
+ - **An embedded space leaves the page's theme alone** (`@plitzi/plitzi-sdk`, `@plitzi/plitzi-builder`): plitzi-ui's
120
+ `Provider` stamps `dark` on `<html>` by default — and, being `light`, takes it off — so every space mounted with
121
+ `themeScope: 'container'` turned the page around it light, and nothing put it back. The SDK and the builder now leave
122
+ the class to their own theme provider (`applyColorModeClass={false}`).
123
+ - **A space inside a space** (`@plitzi/plitzi-sdk`, `@plitzi/sdk-shared`): one drawn inside another — the `plitziSdk`
124
+ element, a plugin rendering one, the builder's canvas — knows it (`render.enclosed`). It is `container` unless it
125
+ says otherwise, wears its theme on its own root and starts from the page's when its own is `system`, keeps its
126
+ stylesheet to its own root (`@scope` — its `:root` palette and `.dark` rules repainted the outer page, its
127
+ `.plitzi-sdk` rule restyled the outer root), writes no `<title>` (`ownsHead`) and no `theme` cookie.
128
+ - **The `plitziSdk` element draws** (`@plitzi/plitzi-sdk`): it rendered in `widget` mode with no page to show, so it
129
+ drew nothing. A widget with no `currentPageId` now shows the page its home address would. And it takes a space's
130
+ documents as well as a key: `offlineData`, bound whole (an `apiContainer`'s answer, a runtime's route, what
131
+ `plitzi_render` returns) or as `offlineData.schema` and `offlineData.style` from two elements — waiting, without an
132
+ error, for the half that has not answered; one that cannot be drawn is said why, in the console and on the canvas.
133
+ - **`<PlitziSdk>` inside a plugin draws** (`@plitzi/plitzi-sdk`): it mounted a router of its own inside the page's, and
134
+ React Router refused it — `routing: 'memory'` or not. A space inside another is a widget: `raw` becomes `widget`
135
+ there, one page and no router (a frame of its own, `iframe` or `shadow`, is left as asked).
136
+ - **A space inside another is as tall as what it holds** (`@plitzi/plitzi-sdk`): its root was `min-h-screen`, as a
137
+ space that IS the page is, so a card of three lines drawn in a panel took the window's height.
138
+ - **`<PlitziSdk.Plugin component>` takes a component with its own props** (`@plitzi/plitzi-sdk`), as `render()`'s
139
+ plugins do: typed `ComponentPluginFC`, it refused any component that read its attributes.
140
+ - **An edited page keeps its address in step** (`@plitzi/plitzi-sdk`): the routes are built from the pages' own
141
+ elements, and those were read again only when the list of pages changed — an edit to one page (its slug, its flag)
142
+ leaves that list the same, and the routes kept its old address until something else rebuilt them.
143
+ - **Packages declare only what they use** (`@plitzi/sdk-dev-tools`, `@plitzi/sdk-shared`): `@plitzi/sdk-plugins`, which
144
+ the dev tools no longer import, and `terser`, which no build here runs, are no longer dependencies.
145
+ - **An edited page keeps its route up to date** (`@plitzi/plitzi-sdk`): the routes are built from the pages' own
146
+ elements, which were read again only when the list of pages changed — an edit to one page (its slug, its flag) left
147
+ the list the same and the route at the old address. They follow every edit to a page now, and stay the same object
148
+ while no page changes.
149
+ - **`@plitzi/sdk-dev-tools` no longer depends on `@plitzi/sdk-plugins`**, which nothing in it imports.
150
+ - **A dropdown's menu opens inside a card that lifts or clips** (`@plitzi/sdk-elements`, `@plitzi/sdk-shared`): drawn
151
+ inside the dropdown, an ancestor with a `transform` — a card lifting on hover — made itself the containing block of
152
+ the menu's `position: fixed`, and one with `overflow: hidden` cut it off: the menu opened and nobody saw it. The open
153
+ menu and its backdrop are drawn at the space's root (`spaceRootOf`, `@plitzi/sdk-shared/helpers/spaceRoot`); a
154
+ style written for the popup by an ancestor's selector no longer reaches it — its own classes do.
155
+ - **A copy button says when it could not copy** (`@plitzi/sdk-elements`, `@plitzi/sdk-shared`,
156
+ `@plitzi/sdk-interactions`, `@plitzi/sdk-variables`, `@plitzi/sdk-dev-tools`): a page not served over https has no
157
+ clipboard — a phone opening a development server by its address — and a code block's "Copy" threw there. It reads
158
+ "Not copied", and says why. `clipboardWriter` and `copyText` (`@plitzi/sdk-shared/helpers/clipboard`) are the one
159
+ check, the `copyToClipboard` step's too.
160
+
161
+ ## Authoring
162
+ - **`plitziSdk` is authored like any element** (`@plitzi/sdk-authoring`, `@plitzi/sdk-elements`): its declaration moved
163
+ to `@plitzi/sdk-elements` beside the others, so it has a factory — `plitziSdk({ id, bind: { offlineData: 'view.data'
164
+ } })` —, its attributes typed, and `plitzi explain` knows it.
165
+ - **A binding may write part of an attribute** (`@plitzi/sdk-authoring`): `offlineData.schema` writes into
166
+ `offlineData`, as the runtime always did; authoring refused it as an attribute nothing reads.
167
+ - **`choosesChildren`** (`@plitzi/sdk-shared`, `@plitzi/sdk-authoring`): a plugin that shows only some of the elements
168
+ put inside it — a dashboard's panels, a wizard's step — says so in its declaration, and the page checks owe none of
169
+ them on screen; what it draws is still read by the checks of the whole page (images, sideways scroll, legibility).
170
+ `verify` was red on every such page.
171
+
172
+ - **A page says how the document scrolls** (`@plitzi/sdk-elements`, `@plitzi/sdk-authoring`, `@plitzi/sdk-mcp`):
173
+ `documentScroll: 'locked'` holds the document still while the page is open — a dashboard or a board used with a
174
+ finger, where the whole page was dragged and bounced back though nothing in it scrolls — and `'noBounce'` keeps the
175
+ scroll without the rubber band or pull-to-refresh. The page draws the rule itself, so it is in the server's HTML and
176
+ leaves with the page; never inside a widget, a space drawn in another, or the builder's canvas. A Settings select,
177
+ `PageSpec.documentScroll`, `upsertPage`'s `documentScroll` (`null` undoes it) and the page read; an unknown value is
178
+ refused (`attribute-value`). `upsertPage` is now held to the lint of the page it writes, as element writes are.
179
+
180
+ ## MCP
181
+ - **`@plitzi/sdk-mcp/render`** (`@plitzi/sdk-mcp`): `plitzi_render`'s widget render on its own — no MCP server, no SSR,
182
+ no OAuth — for a project whose own agent draws in its own pages: `render`, `renderWidgetShape` to declare it as a
183
+ tool, `renderGuideText` for the agent. `base`, the style of the project's space, brings its tokens, classes and fonts
184
+ to the widget.
185
+
186
+ ## Server
187
+ - **`publicUrl` says where people reach the server** (`@plitzi/sdk-server`): it was `http://127.0.0.1:<port>` whatever
188
+ the server was. It is `https` once `serverOptions.tls` gives it a certificate, and this machine's network address
189
+ with `HOST` open to the network (what a tablet opens, what an OAuth provider sends it back to); `PUBLIC_URL` still
190
+ wins. The network addresses it prints say `https` too.
191
+ - **A server serving TLS reaches itself** (`@plitzi/sdk-server`): with a local certificate (mkcert) Node trusts nothing
192
+ the browsers were told to, so a runtime calling its own MCP failed. Its requests to its own address now connect to its
193
+ listener and are taken when it presents exactly its certificate (`reachOwnServer`, exported from `/runtime`).
194
+ - **A runtime reads compressed answers again** (`@plitzi/sdk-server`): the dispatcher `reachOwnServer` (and the
195
+ platform's `reachSpaceInside`) installs reached every other host with an undici connector that offers HTTP/2, and an
196
+ answer that came back over h2 reached Node's `fetch` without its `content-encoding` — every API that compresses read
197
+ as Brotli bytes. Other hosts are reached over HTTP/1.1 now, as Node's own `fetch` reaches them.
198
+ - **A project's server serves HTTPS from `.env`** (`@plitzi/sdk-server`, `@plitzi/sdk-shared`): `TLS_CERT` and
199
+ `TLS_KEY`, the certificate's and the key's paths — what `plitzi cert` writes — with no code of the project's;
200
+ `serverOptions.tls` still wins. A network address the certificate does not name is said at boot, the machine's
201
+ `.local` name is printed among the addresses when it is named, and a server on loopback alone says how to open it to
202
+ the network.
203
+ - **A session survives a phone on the Wi-Fi** (`@plitzi/sdk-server`): a private network address (`192.168.…`, `10.…`,
204
+ Tailscale's `100.64/10`, IPv6 unique- and link-local) and a machine's mDNS name (`studio.local`) get the local
205
+ cookie profile — `SameSite=Lax`, no `Secure`, no `Domain`. They got `SameSite=None; Secure`, which a browser drops
206
+ over http: nobody could sign in from a tablet. A deployment's own `.local` sub-domains (`app.example.local`) keep the
207
+ strict profile.
208
+ - **Hiding the dev tools on a server over HTTPS hydrates** (`@plitzi/sdk-server`): a server with a certificate speaks
209
+ HTTP/2, which sends no `Host` — only `:authority` — and the cookies named for the origin's port (the dev tools'
210
+ `plitzi_debug_<port>`, forced flags, painted state) were looked for without it. Shift+F12 hid the dev tools in the
211
+ browser, the server rendered them back, and the page failed to hydrate. Every one is read off `requestAuthority`.
212
+ - **A rebuild of plugins says it finished** (`@plitzi/sdk-server`): bundles removed under a running server (`tmp/`
213
+ deleted) were each a warning, "rebuilding…", and their "ready" at a level a project's terminal does not show. It is
214
+ one episode now: said when noticed, and again when the last is built — how many, how long, which failed.
215
+ - **`tmp/dev-server.json` records the scheme** (`@plitzi/sdk-server`): `url` is `https` with a certificate, and the
216
+ file adds the network addresses and the `publicUrl`.
217
+ - **`public/` is asked about every time** (`@plitzi/sdk-server`): it was cached for an hour — a `.js` or `.css` in it
218
+ for a year, `immutable` — so a file a project rewrites while it runs arrived stale. It is `no-cache` now, answered
219
+ `304` by its `ETag` while unchanged; `immutable` stays for what is versioned (the SDK, the plugins).
220
+
221
+ ## CLI
222
+ - **`check`, `shot`, `verify` and `visual` find a server on HTTPS** (`@plitzi/cli`): they rebuilt
223
+ `http://127.0.0.1:<port>`, so a project serving a certificate had no page checked. They read the URL `npm start`
224
+ recorded, and take a local certificate on this machine only. A project's `playwright.config.ts` comes from
225
+ `plitzi upgrade`.
226
+ - **`verify` runs the project's tests** (`@plitzi/cli`): a step `test`, after the lint, when `package.json` has a
227
+ `test` script.
228
+ - **`tsconfig.json` is the project's** (`@plitzi/cli`): the compiler options moved to `plitzi/tsconfig.base.json`, which
229
+ the CLI keeps up and `tsconfig.json` extends — so a folder of the project's own (`scripts/`, `tools/`) goes in its
230
+ `include` without changing a file of the CLI's. `plitzi doctor --fix` makes an older `tsconfig.json` extend it,
231
+ keeping only the options the project changed.
232
+ - **`doctor --fix` and `upgrade` write as the project's Prettier does** (`@plitzi/cli`): a repaired `tsconfig.json` or
233
+ `package.json` left `verify`'s format step red.
234
+ - **`upgrade` renames `usePlitziServiceContext`** (`@plitzi/cli`): with `PlitziServiceProvider` and
235
+ `PlitziServiceContextValue`, renamed in 0.38.12 with no alias — import, use and the module path that named it.
236
+ - **`skills update` compares every file** (`@plitzi/cli`): only `SKILL.md` was, so a change in a reference alone was
237
+ "up to date".
238
+ - **`NOTES.md`, the project's notes for agents** (`@plitzi/cli`): `AGENTS.md` and `CLAUDE.md` are the CLI's and
239
+ `upgrade` replaces them, so what a project's own sessions had to know had nowhere to live. `CLAUDE.md` imports
240
+ `NOTES.md`, `AGENTS.md` sends every agent to it, and nothing the CLI does touches it; `doctor --fix` writes one for
241
+ a project that has none.
242
+
243
+ - **`start:dev-inspect`** (`@plitzi/cli`): a server project's `start:dev` with Node's inspector open on
244
+ 127.0.0.1:9229, so a debugger (VS Code, `chrome://inspect`) attaches to the server, and again after each restart.
245
+ `plitzi upgrade` adds it; `plitzi runtime add` has it restart on the runtime too.
246
+ - **`plitzi cert`** (`@plitzi/cli`): HTTPS while developing, for a phone or a tablet on the Wi-Fi — over http its
247
+ browser gives a page no microphone, camera or clipboard. A certificate from mkcert's local authority for this machine,
248
+ its network addresses and its `.local` name, in `tmp/tls/`, named in `.env`; what to install on the phone, said.
249
+ - **A browser project opens to the network** (`@plitzi/cli`): its `vite.config.ts` reads `HOST`, `TLS_CERT` and
250
+ `TLS_KEY` from `.env`, as a server project does, and writes `tmp/dev-server.json` — the port Vite took and the scheme
251
+ — so `page check`, `page shot` and `visual` find it on another port or over HTTPS. `plitzi upgrade` brings it.
252
+
253
+ ## Docs
254
+ - **Two plugins share through `state`** (`@plitzi/sdk-authoring` skill): each plugin is a bundle of its own, so a module
255
+ both import is two copies, and what one keeps there the other never sees.
256
+ - **`verify --help`** names the project's tests among its steps.
257
+
258
+ - Updated dependencies [493955a]
259
+ - @plitzi/plitzi-sdk@0.38.14
260
+ - @plitzi/sdk-server@0.38.14
261
+ - @plitzi/sdk-authoring@0.38.14
262
+ - @plitzi/sdk-schema@0.38.14
263
+ - @plitzi/sdk-shared@0.38.14
264
+ - @plitzi/sdk-style@0.38.14
265
+
3
266
  ## 0.38.13
4
267
 
5
268
  ### Patch Changes
package/README.md CHANGED
@@ -133,6 +133,24 @@ hand-build a URI — every write and search response hands back the URI to use n
133
133
  The tool functions are exported directly (`apply`, `search`, `read`, `validate`, `tools`), so a consumer can run
134
134
  them in-process instead of speaking MCP over HTTP.
135
135
 
136
+ ### A project's own agent drawing widgets
137
+
138
+ `plitzi_render` hands its widget to a host that draws `ui://` resources — claude.ai, Desktop. An agent working for a
139
+ project (a voice assistant, a terminal session) draws in the project's own page instead, so the render comes apart:
140
+
141
+ ```ts
142
+ import { render, renderGuideText, renderWidgetShape } from '@plitzi/sdk-mcp/render';
143
+
144
+ // A tool of the project's own MCP server, its input `renderWidgetShape`, its guide `renderGuideText`.
145
+ const answer = render({ operations }, { base: space.style }); // the project's authored space
146
+ if (answer.rendered) {
147
+ publish(answer.offlineData); // what a `plitziSdk` element is bound to (`offlineData`)
148
+ }
149
+ ```
150
+
151
+ `base` is the style of the space that shows the widget: its tokens in both themes, its classes and its fonts come
152
+ with it, so the widget looks like the page around it. Left out, the widget starts from nothing, as one in a chat does.
153
+
136
154
  ## Draft preview
137
155
 
138
156
  The two halves live in different packages, joined by a one-shot token:
@@ -204,6 +222,7 @@ MCP server, [self-hosting/07](../../examples/self-hosting/07-ssr-preview) is the
204
222
  |---|---|
205
223
  | `@plitzi/sdk-mcp/server` | The dedicated MCP server and its clients. What an MCP deployment wires. |
206
224
  | `@plitzi/sdk-mcp` | Everything above plus the pipeline stages, `createPreview` and the tool engine. |
225
+ | `@plitzi/sdk-mcp/render` | `plitzi_render`'s widget render alone — no MCP server, no SSR, no OAuth — for a project's own agent. |
207
226
 
208
227
  The split is about weight, not taste: ESM re-exports load eagerly, so the barrel pulls the draft-preview path and
209
228
  the renderer it reaches into. A `no-restricted-imports` rule and a test in `src/packageBoundary.test.ts` keep this
@@ -797,7 +797,7 @@ Pages that see each other — cursors, presence, a shared board, a game. Three p
797
797
  \`publish: "server"\` is for what must be validated and saved first; \`"clients"\` (the default) for what pages send
798
798
  directly — add \`presence: true\` for who-is-here.
799
799
  2. **Subscribe** with a \`channel\` element whose \`topic\` a pattern matches (\`board:{{ id }}\`, a route param).
800
- Its descendants bind \`channel_<id>\`: \`connected\`, \`members\`, \`messages\`, \`last\`; flows use its \`onMessage\`,
800
+ Its descendants bind \`channel_<id>\`: \`connected\`, \`members\`, \`messages\`, \`last\`, \`retained\`; flows use its \`onMessage\`,
801
801
  \`onJoin\`, \`onLeave\` triggers (\`from\`, \`user\` and the \`state\` the member announced — \`onJoin\` only for who
802
802
  came after this page) and its \`publish\` / \`setPresence\` callbacks.
803
803
  3. **Announce from the server**: an action whose last step is the \`realtime.publish\` task — validate, save, then say so.
@@ -809,6 +809,11 @@ decides the visitor may be there ends with \`realtime.grant { topic }\` and answ
809
809
  element binds \`grant\` to it. Without one the topic is refused however well its name is known (\`channel-grant\`).
810
810
  \`realtime.revoke { topic, grant }\` takes one back — or all, naming none — and lets go whoever is on it with it.
811
811
 
812
+ **State every screen shares** (a dashboard's lists, a board's settings): declare the pattern \`retain: true\` with
813
+ \`publish: "server"\`. The server keeps the latest message of each type it says (\`realtime.publish\`) and hands them to
814
+ every page that joins — and to its \`onMessage\` flows, as if just said; the page binds \`desk.retained.lists\`, with no
815
+ read of its own and no copy in \`state\`. \`data: null\` forgets a type; a topic keeps at most 64 types and 512 KB.
816
+
812
817
  **Two writers at once**: read-change-write with \`kv.setIf\` (written only if the value is still the one read — empty
813
818
  \`expected\` = only if nothing is there yet), ordered lists with \`list.put\` / \`list.range\`, and put
814
819
  \`flow.rateLimit\` first in any public action that writes.
@@ -977,6 +982,10 @@ Pages can be grouped into **folders** (the sidebar tree). A folder is \`{ ref, n
977
982
  a page read reports its current \`enabled\`). Disabling only affects the **published SDK runtime** — the page stops
978
983
  being routable/accessible to end users. It stays fully **editable here**: you can still read it and apply any op to
979
984
  a disabled page. This does not delete it — \`deletePage\` does.
985
+ - **A page that fills the window** (a dashboard, a board) takes \`upsertPage\`'s \`documentScroll: "locked"\`: the
986
+ document holds still and only parts with \`overflow: auto\` scroll, so a finger on a tablet no longer drags and
987
+ bounces the page. \`"noBounce"\` keeps the scroll without the rubber band; \`null\` undoes either. Never an
988
+ \`html, body\` rule in \`customCss\`: that holds every page, and the site the space is a widget on.
980
989
 
981
990
  ## Settings
982
991
  Space-level configuration lives in \`plitzi://settings/{env}\` and is edited with a single **\`patchSettings\`** op
@@ -3,6 +3,8 @@ import { ResourceEnvelope } from '../types';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  export declare const RENDER_GUIDE_URI = "plitzi://render/guide";
5
5
  export declare const RENDER_TYPES_URI = "plitzi://render/types";
6
+ /** How a widget is authored: what an agent reads before its first render, whichever server hands it the tool. */
7
+ export declare const renderGuideText: string;
6
8
  /** The same two documents through the READ path (the router, hence plitzi_read), not only as MCP resources: an
7
9
  * agent that reaches for the batch-read tool — the reflex the guide itself teaches — must not be told its own
8
10
  * authoring guide does not exist. Space-independent, like the core singletons. */
@@ -34,6 +34,7 @@ var renderTypes = () => {
34
34
  types
35
35
  };
36
36
  };
37
+ /** How a widget is authored: what an agent reads before its first render, whichever server hands it the tool. */
37
38
  var renderGuideText = `# plitzi_render — authoring guide
38
39
 
39
40
  \`plitzi_render\` shows the user a live UI widget. You build it as a list of \`operations\` (applied IN ORDER) that
@@ -504,4 +505,4 @@ var registerRenderResources = (server, log) => {
504
505
  });
505
506
  };
506
507
  //#endregion
507
- export { RENDER_GUIDE_URI, RENDER_TYPES_URI, readRenderResource, registerRenderResources };
508
+ export { RENDER_GUIDE_URI, RENDER_TYPES_URI, readRenderResource, registerRenderResources, renderGuideText };
@@ -72,7 +72,7 @@ var createMcpServer = async ({ adapters, getGrant, preview, screenshot, logger,
72
72
  const getSpace = () => spacePromise ??= loadSpace();
73
73
  const server = new McpServer({
74
74
  name: "plitzi-mcp",
75
- version: "0.38.13"
75
+ version: "0.38.15"
76
76
  }, { instructions: hasSpace ? serverInstructions : widgetsOnlyInstructions });
77
77
  const { getChanges } = adapters;
78
78
  registerResources(server, getSpace, MCP_ENV, log, hasSpace, getChanges ? (query) => getChanges(requireSpaceId(), MCP_ENV, query) : void 0);
@@ -82,6 +82,10 @@ export declare const applyShape: {
82
82
  layout: z.ZodOptional<z.ZodNullable<z.ZodString>>;
83
83
  layoutContainer: z.ZodOptional<z.ZodNullable<z.ZodString>>;
84
84
  enabled: z.ZodOptional<z.ZodBoolean>;
85
+ documentScroll: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
86
+ locked: "locked";
87
+ noBounce: "noBounce";
88
+ }>>>;
85
89
  flag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
86
90
  }, z.core.$strict>, z.ZodObject<{
87
91
  type: z.ZodLiteral<"deletePage">;
@@ -288,6 +292,10 @@ export declare const applyShape: {
288
292
  clients: "clients";
289
293
  }>>;
290
294
  presence: z.ZodOptional<z.ZodBoolean>;
295
+ grant: z.ZodOptional<z.ZodBoolean>;
296
+ retain: z.ZodOptional<z.ZodBoolean>;
297
+ maxMessageBytes: z.ZodOptional<z.ZodNumber>;
298
+ messagesPerSecond: z.ZodOptional<z.ZodNumber>;
291
299
  }, z.core.$strip>>>>;
292
300
  }, z.core.$strict>, z.ZodObject<{
293
301
  type: z.ZodLiteral<"upsertComponent">;
@@ -78,6 +78,10 @@ export declare const documentOperation: z.ZodDiscriminatedUnion<readonly [z.ZodO
78
78
  layout: z.ZodOptional<z.ZodNullable<z.ZodString>>;
79
79
  layoutContainer: z.ZodOptional<z.ZodNullable<z.ZodString>>;
80
80
  enabled: z.ZodOptional<z.ZodBoolean>;
81
+ documentScroll: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
82
+ locked: "locked";
83
+ noBounce: "noBounce";
84
+ }>>>;
81
85
  flag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
82
86
  }, z.core.$strict>, z.ZodObject<{
83
87
  type: z.ZodLiteral<"deletePage">;
@@ -284,6 +288,10 @@ export declare const documentOperation: z.ZodDiscriminatedUnion<readonly [z.ZodO
284
288
  clients: "clients";
285
289
  }>>;
286
290
  presence: z.ZodOptional<z.ZodBoolean>;
291
+ grant: z.ZodOptional<z.ZodBoolean>;
292
+ retain: z.ZodOptional<z.ZodBoolean>;
293
+ maxMessageBytes: z.ZodOptional<z.ZodNumber>;
294
+ messagesPerSecond: z.ZodOptional<z.ZodNumber>;
287
295
  }, z.core.$strip>>>>;
288
296
  }, z.core.$strict>, z.ZodObject<{
289
297
  type: z.ZodLiteral<"upsertComponent">;
@@ -1496,6 +1504,10 @@ export declare const operation: z.ZodDiscriminatedUnion<[z.ZodObject<{
1496
1504
  layout: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1497
1505
  layoutContainer: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1498
1506
  enabled: z.ZodOptional<z.ZodBoolean>;
1507
+ documentScroll: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
1508
+ locked: "locked";
1509
+ noBounce: "noBounce";
1510
+ }>>>;
1499
1511
  flag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1500
1512
  }, z.core.$strict>, z.ZodObject<{
1501
1513
  type: z.ZodLiteral<"deletePage">;
@@ -1702,6 +1714,10 @@ export declare const operation: z.ZodDiscriminatedUnion<[z.ZodObject<{
1702
1714
  clients: "clients";
1703
1715
  }>>;
1704
1716
  presence: z.ZodOptional<z.ZodBoolean>;
1717
+ grant: z.ZodOptional<z.ZodBoolean>;
1718
+ retain: z.ZodOptional<z.ZodBoolean>;
1719
+ maxMessageBytes: z.ZodOptional<z.ZodNumber>;
1720
+ messagesPerSecond: z.ZodOptional<z.ZodNumber>;
1705
1721
  }, z.core.$strip>>>>;
1706
1722
  }, z.core.$strict>, z.ZodObject<{
1707
1723
  type: z.ZodLiteral<"upsertComponent">;
@@ -3193,6 +3209,10 @@ export declare const operations: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject
3193
3209
  layout: z.ZodOptional<z.ZodNullable<z.ZodString>>;
3194
3210
  layoutContainer: z.ZodOptional<z.ZodNullable<z.ZodString>>;
3195
3211
  enabled: z.ZodOptional<z.ZodBoolean>;
3212
+ documentScroll: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
3213
+ locked: "locked";
3214
+ noBounce: "noBounce";
3215
+ }>>>;
3196
3216
  flag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
3197
3217
  }, z.core.$strict>, z.ZodObject<{
3198
3218
  type: z.ZodLiteral<"deletePage">;
@@ -3399,6 +3419,10 @@ export declare const operations: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject
3399
3419
  clients: "clients";
3400
3420
  }>>;
3401
3421
  presence: z.ZodOptional<z.ZodBoolean>;
3422
+ grant: z.ZodOptional<z.ZodBoolean>;
3423
+ retain: z.ZodOptional<z.ZodBoolean>;
3424
+ maxMessageBytes: z.ZodOptional<z.ZodNumber>;
3425
+ messagesPerSecond: z.ZodOptional<z.ZodNumber>;
3402
3426
  }, z.core.$strip>>>>;
3403
3427
  }, z.core.$strict>, z.ZodObject<{
3404
3428
  type: z.ZodLiteral<"upsertComponent">;
@@ -4885,6 +4909,10 @@ export declare const documentOperations: z.ZodArray<z.ZodDiscriminatedUnion<read
4885
4909
  layout: z.ZodOptional<z.ZodNullable<z.ZodString>>;
4886
4910
  layoutContainer: z.ZodOptional<z.ZodNullable<z.ZodString>>;
4887
4911
  enabled: z.ZodOptional<z.ZodBoolean>;
4912
+ documentScroll: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
4913
+ locked: "locked";
4914
+ noBounce: "noBounce";
4915
+ }>>>;
4888
4916
  flag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
4889
4917
  }, z.core.$strict>, z.ZodObject<{
4890
4918
  type: z.ZodLiteral<"deletePage">;
@@ -5091,6 +5119,10 @@ export declare const documentOperations: z.ZodArray<z.ZodDiscriminatedUnion<read
5091
5119
  clients: "clients";
5092
5120
  }>>;
5093
5121
  presence: z.ZodOptional<z.ZodBoolean>;
5122
+ grant: z.ZodOptional<z.ZodBoolean>;
5123
+ retain: z.ZodOptional<z.ZodBoolean>;
5124
+ maxMessageBytes: z.ZodOptional<z.ZodNumber>;
5125
+ messagesPerSecond: z.ZodOptional<z.ZodNumber>;
5094
5126
  }, z.core.$strip>>>>;
5095
5127
  }, z.core.$strict>, z.ZodObject<{
5096
5128
  type: z.ZodLiteral<"upsertComponent">;
@@ -81,6 +81,10 @@ export declare const elementOps: {
81
81
  layout: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
82
82
  layoutContainer: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
83
83
  enabled: import('zod').ZodOptional<import('zod').ZodBoolean>;
84
+ documentScroll: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodEnum<{
85
+ locked: "locked";
86
+ noBounce: "noBounce";
87
+ }>>>;
84
88
  flag: import('zod').ZodOptional<import('zod').ZodNullable<import('zod').ZodString>>;
85
89
  }, import('zod/v4/core').$strip>;
86
90
  deletePage: import('zod').ZodObject<{
@@ -303,6 +307,10 @@ export declare const elementOps: {
303
307
  clients: "clients";
304
308
  }>>;
305
309
  presence: import('zod').ZodOptional<import('zod').ZodBoolean>;
310
+ grant: import('zod').ZodOptional<import('zod').ZodBoolean>;
311
+ retain: import('zod').ZodOptional<import('zod').ZodBoolean>;
312
+ maxMessageBytes: import('zod').ZodOptional<import('zod').ZodNumber>;
313
+ messagesPerSecond: import('zod').ZodOptional<import('zod').ZodNumber>;
306
314
  }, import('zod/v4/core').$strip>>>>;
307
315
  }, import('zod/v4/core').$strip>;
308
316
  upsertComponent: import('zod').ZodObject<{
@@ -11,6 +11,10 @@ export declare const upsertPageOp: z.ZodObject<{
11
11
  layout: z.ZodOptional<z.ZodNullable<z.ZodString>>;
12
12
  layoutContainer: z.ZodOptional<z.ZodNullable<z.ZodString>>;
13
13
  enabled: z.ZodOptional<z.ZodBoolean>;
14
+ documentScroll: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
15
+ locked: "locked";
16
+ noBounce: "noBounce";
17
+ }>>>;
14
18
  flag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
15
19
  }, z.core.$strip>;
16
20
  export type UpsertPage = z.infer<typeof upsertPageOp>;
@@ -3,7 +3,7 @@ import { layoutsUri, pageUri, pagesUri } from "../../../../helpers/uris.js";
3
3
  import { empty, fail } from "../../../../helpers/opResult.js";
4
4
  import { elementFlag } from "../shared.js";
5
5
  import { guardNewRef } from "../write.js";
6
- import { flagGateOf } from "@plitzi/sdk-authoring";
6
+ import { DOCUMENT_SCROLLS, flagGateOf } from "@plitzi/sdk-authoring";
7
7
  import { z } from "zod";
8
8
  //#region src/modules/mcp/tools/operations/schema/pages/upsertPage.ts
9
9
  var upsertPageOp = z.object({
@@ -16,6 +16,7 @@ var upsertPageOp = z.object({
16
16
  layout: z.string().nullable().optional().describe("Ref of the layout shell this page renders inside; \"\" or null detaches it. See plitzi://schema/{env}/layouts."),
17
17
  layoutContainer: z.string().nullable().optional().describe("Ref of the container INSIDE that shell where this page body goes — the slot. Must belong to it."),
18
18
  enabled: z.boolean().optional().describe("false disables the page in the published SDK runtime (not routable/accessible to end users); it stays editable here. Defaults to true"),
19
+ documentScroll: z.enum(DOCUMENT_SCROLLS).nullable().optional().describe("\"locked\": the document never moves, only overflow:auto panels scroll; \"noBounce\": no rubber band"),
19
20
  flag: elementFlag.nullable().optional()
20
21
  }).describe("Create a page, or update it when ref already exists (only the fields you pass change).");
21
22
  var upsertPage = (space, env, op) => {
@@ -66,6 +67,8 @@ var upsertPage = (space, env, op) => {
66
67
  ...layoutValue !== void 0 ? { layout: layoutValue } : {},
67
68
  ...slotValue !== void 0 ? { layoutContainer: slotValue } : {}
68
69
  };
70
+ if (op.documentScroll === null) Reflect.deleteProperty(existing.attributes, "documentScroll");
71
+ else if (op.documentScroll !== void 0) existing.attributes.documentScroll = op.documentScroll;
69
72
  if (op.flag === null) Reflect.deleteProperty(existing.definition, "flag");
70
73
  else if (op.flag !== void 0) existing.definition.flag = flagGateOf(op.flag, `Page "${op.ref}"`);
71
74
  return {
@@ -90,7 +93,8 @@ var upsertPage = (space, env, op) => {
90
93
  enabled: op.enabled ?? true,
91
94
  folder: folderValue ?? "",
92
95
  layout: layoutValue ?? "",
93
- layoutContainer: slotValue ?? ""
96
+ layoutContainer: slotValue ?? "",
97
+ ...op.documentScroll ? { documentScroll: op.documentScroll } : {}
94
98
  },
95
99
  definition: {
96
100
  rootId: id,
@@ -48,6 +48,10 @@ export declare const patchSettingsOp: z.ZodObject<{
48
48
  clients: "clients";
49
49
  }>>;
50
50
  presence: z.ZodOptional<z.ZodBoolean>;
51
+ grant: z.ZodOptional<z.ZodBoolean>;
52
+ retain: z.ZodOptional<z.ZodBoolean>;
53
+ maxMessageBytes: z.ZodOptional<z.ZodNumber>;
54
+ messagesPerSecond: z.ZodOptional<z.ZodNumber>;
51
55
  }, z.core.$strip>>>>;
52
56
  }, z.core.$strip>;
53
57
  export type PatchSettings = z.infer<typeof patchSettingsOp>;
@@ -20,7 +20,11 @@ var channel = z.object({
20
20
  permissions: z.array(z.string()).optional()
21
21
  }),
22
22
  publish: z.enum(["clients", "server"]).optional(),
23
- presence: z.boolean().optional()
23
+ presence: z.boolean().optional(),
24
+ grant: z.boolean().optional().describe("A page needs a grant from realtime.grant to open a topic"),
25
+ retain: z.boolean().optional().describe("The server keeps the latest message of each type for pages that join"),
26
+ maxMessageBytes: z.number().int().positive().optional(),
27
+ messagesPerSecond: z.number().int().positive().optional()
24
28
  });
25
29
  var notifications = z.record(z.string(), z.string().nullable());
26
30
  var patchSettingsOp = z.object({
@@ -3,7 +3,7 @@ import { descendantCount, elementById, getLayoutElements, getPageElements, nameO
3
3
  import { bindingsToAI, flowsFromInteractions } from "../../../helpers/interactions.js";
4
4
  import { definitionToAI, globalStylesForType, idStyleToAI } from "../style/translator.js";
5
5
  import { descendants } from "@plitzi/sdk-schema/helpers/elementTree";
6
- import { flagSpecOf, splitNotificationsCss } from "@plitzi/sdk-authoring";
6
+ import { flagSpecOf, isDocumentScroll, splitNotificationsCss } from "@plitzi/sdk-authoring";
7
7
  //#region src/modules/mcp/tools/operations/schema/translator.ts
8
8
  var splitClasses = (value) => value ? value.split(/\s+/).filter(Boolean) : [];
9
9
  var propsOf = (el) => {
@@ -84,6 +84,7 @@ var pageSkeletonToAI = (schema, pageEl, style) => {
84
84
  slug,
85
85
  default: pageEl.attributes.default === true,
86
86
  enabled: pageEl.attributes.enabled !== false,
87
+ ...isDocumentScroll(pageEl.attributes.documentScroll) ? { documentScroll: pageEl.attributes.documentScroll } : {},
87
88
  routeParams: slugRouteParams(slug),
88
89
  tree: orderedChildren(schema, pageEl).map((child) => skeletonNode(schema, child, style))
89
90
  };