@plitzi/sdk-server 0.32.18 → 0.32.20

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 (57) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +18 -0
  3. package/dist/core/services/mcp.js +2 -1
  4. package/dist/modules/mcp/apps/index.js +7 -3
  5. package/dist/modules/mcp/apps/render/index.js +5 -4
  6. package/dist/modules/mcp/apps/render/styles.js +54 -0
  7. package/dist/modules/mcp/apps/render/view/heldBatch.ts +107 -0
  8. package/dist/modules/mcp/apps/render/{view.tsx → view/index.tsx} +106 -4
  9. package/dist/modules/mcp/apps/render/view/streamProgress.ts +137 -0
  10. package/dist/modules/mcp/apps/shared/assets.js +10 -0
  11. package/dist/modules/mcp/apps/shared/bundle.js +15 -14
  12. package/dist/modules/mcp/apps/shared/page.js +17 -6
  13. package/dist/modules/mcp/apps/shared/registerApp.js +2 -2
  14. package/dist/modules/mcp/apps/shared/shell.ejs +5 -0
  15. package/dist/modules/mcp/handler.js +1 -1
  16. package/dist/modules/mcp/helpers/guide.js +2 -1
  17. package/dist/modules/mcp/helpers/space.js +13 -1
  18. package/dist/modules/mcp/resources/register.js +8 -3
  19. package/dist/modules/mcp/resources/renderGuide.js +63 -11
  20. package/dist/modules/mcp/resources/router.js +13 -1
  21. package/dist/modules/mcp/server.js +32 -20
  22. package/dist/modules/mcp/tools/read.js +21 -3
  23. package/dist/modules/mcp/tools/render.js +7 -3
  24. package/dist/modules/mcp/tools/shared/tool.js +16 -11
  25. package/dist/modules/mcp/tools/shared/validator/elements.js +11 -1
  26. package/dist/modules/mcp/tools/shared/validator/index.js +9 -5
  27. package/dist/src/modules/mcp/apps/index.d.ts +6 -3
  28. package/dist/src/modules/mcp/apps/render/index.d.ts +1 -0
  29. package/dist/src/modules/mcp/apps/render/styles.d.ts +10 -0
  30. package/dist/src/modules/mcp/apps/shared/assets.d.ts +10 -0
  31. package/dist/src/modules/mcp/apps/shared/bundle.d.ts +3 -0
  32. package/dist/src/modules/mcp/apps/shared/index.d.ts +1 -0
  33. package/dist/src/modules/mcp/apps/shared/page.d.ts +2 -2
  34. package/dist/src/modules/mcp/apps/shared/registerApp.d.ts +2 -2
  35. package/dist/src/modules/mcp/e2e/mcpEndpoint.d.ts +8 -1
  36. package/dist/src/modules/mcp/e2e/renderingHost.d.ts +5 -0
  37. package/dist/src/modules/mcp/handler.d.ts +2 -0
  38. package/dist/src/modules/mcp/helpers/guide.d.ts +1 -0
  39. package/dist/src/modules/mcp/helpers/space.d.ts +9 -0
  40. package/dist/src/modules/mcp/resources/index.d.ts +2 -1
  41. package/dist/src/modules/mcp/resources/register.d.ts +6 -2
  42. package/dist/src/modules/mcp/resources/renderGuide.d.ts +5 -0
  43. package/dist/src/modules/mcp/resources/router.d.ts +4 -0
  44. package/dist/src/modules/mcp/server.d.ts +9 -5
  45. package/dist/src/modules/mcp/tools/read.d.ts +1 -0
  46. package/dist/src/modules/mcp/tools/shared/tool.d.ts +8 -0
  47. package/dist/src/modules/mcp/tools/shared/validator/context.d.ts +4 -0
  48. package/dist/src/modules/mcp/tools/shared/validator/elements.d.ts +1 -0
  49. package/dist/src/modules/mcp/tools/shared/validator/index.d.ts +3 -3
  50. package/dist/src/modules/mcp/types/appTypes.d.ts +9 -1
  51. package/package.json +6 -5
  52. package/skills/plitzi-render/SKILL.md +174 -0
  53. package/dist/src/modules/mcp/apps/render/heldBatch.d.ts +0 -10
  54. package/dist/src/modules/mcp/apps/render/view.d.ts +0 -1
  55. /package/dist/modules/mcp/apps/example/{view.tsx → view/index.tsx} +0 -0
  56. /package/dist/src/modules/mcp/apps/{example/view.d.ts → render/styles.test.d.ts} +0 -0
  57. /package/dist/src/modules/mcp/{apps/render/heldBatch.test.d.ts → tests/skill.test.d.ts} +0 -0
@@ -11,7 +11,7 @@ var DEFAULT_CSP = {
11
11
  };
12
12
  /** Serves the app as a self-contained page: no import map, no asset mounts, no cross-origin fetches, so the
13
13
  * strictest host sandbox runs it and no deployment has to serve anything extra. */
14
- var registerApp = (server, app) => {
14
+ var registerApp = (server, app, settings) => {
15
15
  const meta = { ui: { csp: app.csp ?? DEFAULT_CSP } };
16
16
  registerAppResource(server, app.name, app.uri, {
17
17
  description: app.description,
@@ -19,7 +19,7 @@ var registerApp = (server, app) => {
19
19
  }, async () => ({ contents: [{
20
20
  uri: app.uri,
21
21
  mimeType: RESOURCE_MIME_TYPE,
22
- text: await page(app),
22
+ text: await page(app, settings),
23
23
  _meta: meta
24
24
  }] }));
25
25
  };
@@ -47,6 +47,11 @@
47
47
  <body>
48
48
  <div id="app"></div>
49
49
 
50
+ <%# Deployment switches, before the app so the view reads them as it mounts. They ride in the page, not the
51
+ bundle, so a server serving both settings still builds the browser bundle once. -%>
52
+ <script id="plitzi-view-settings">
53
+ window.__PLITZI_VIEW__ = <%- settings %>;
54
+ </script>
50
55
  <script>
51
56
  <%- app %>
52
57
  </script>
@@ -74,7 +74,7 @@ var serveMcp = async (raw, res, server) => {
74
74
  await transport.close();
75
75
  }
76
76
  };
77
- var handleMcp = (raw, res, req, adapters, options = {}) => serveMcp(raw, res, createMcpServer({
77
+ var handleMcp = async (raw, res, req, adapters, options = {}) => serveMcp(raw, res, await createMcpServer({
78
78
  adapters,
79
79
  getSpaceId: () => adapters.getSpaceId?.(req) ?? Promise.resolve(void 0),
80
80
  ...options
@@ -1,5 +1,6 @@
1
1
  //#region src/modules/mcp/helpers/guide.ts
2
2
  var serverInstructions = "Plitzi AI server: read-then-write editing of a Plitzi space. Reads follow a filesystem model — list cheap, read one item in detail on demand; never fetch a whole tree you do not need. Workflow: (1) read plitzi://primer/{env} once — it bundles the guide, types, css-properties and page/definition/variable summaries in a single call; (2) plitzi_search with include:\"detail\" to jump to elements — each hit then carries its uri, stateVersion AND full style/resolvedStyle, so an edit needs no per-element read; open a page skeleton or element only when you need its tree/detail (the skeleton already lists the style classes of each node, and plitzi_read fetches many uris at once); (3) plitzi_apply with dryRun to preview a batch; (4) plitzi_apply to persist, passing expectedResourceVersions to guard against concurrent edits — apply and search both hand back the versions you need for the next edit. Use patchElement / patchDefinition to change only some props / CSS (the upsert variants replace them all). An element read (and search include:\"detail\") inlines the CSS of the definitions it attaches under resolvedStyle, so you rarely need a separate definition read. Refs accept a semantic idRef ([A-Za-z0-9_-] starting with a letter, unique, chosen by you) or the raw id — the idRef is ALSO the runtime wiring key, so a provider source is `<type>_<idRef>.<field>`, visible to the provider’s DESCENDANTS only (bind inside its subtree). CSS is plain kebab-case — write shorthands freely (`border: 1px solid red`, `padding: 8px 16px`, `font: bold 16px/1.5 Arial`), they are expanded to longhands for you and STORED that way, so read-back shows the longhands; style vars are var(--name), schema vars are {{name}}. READERS — do not confuse them: MCP *resources* are the browsable catalog (list them, or open one by URI); plitzi_search FINDS refs by label/type/attribute; plitzi_read BATCH-fetches URIs you already hold. Reach for search/read to work; browse resources to discover. Elements also carry applied style variants + visibility (initialState), data bindings and interaction flows: edit them with patchElement (initialState), upsertBinding/patchBinding/deleteBinding, and upsertInteractionFlow/patchInteractionNode/deleteInteraction. An element read shows all three plus availableVariants (which variant each of its classes offers). Separately, to SHOW the user a small self-contained widget (offline, no space or backend) instead of editing the space — a card, hero, pricing table, a visual answer — use plitzi_render; read plitzi://render/guide for it.";
3
+ var widgetsOnlyInstructions = "Plitzi widget server (widgets-only connection). This connection carries NO Plitzi space: nothing can be read or edited in one, and the space tools are not offered here — do not look for them. What it does do is build self-contained UI widgets fully offline, with no backend, account or setup: call plitzi_render to SHOW the user a real rendered layout (card, hero, pricing table, checklist, form, gallery) instead of describing one. Read plitzi://render/guide first — the element/prop table, the style model and a worked example; plitzi://render/types lists every element type you can author (plitzi_read fetches both). To change a widget you already rendered, call plitzi_render again with patch:true and its renderId. If the user wants to edit their real Plitzi space from here, that is a reconnection they make: the integration must be re-authorized and granted a space (the consent screen lists theirs) — you cannot do it from this connection, and no retry will change it.";
3
4
  var guideQuickstart = `# Plitzi AI MCP — quickstart
4
5
  This is the condensed guide; read \`plitzi://guide\` for the full reference (every resource, op and example).
5
6
 
@@ -463,4 +464,4 @@ Space-level configuration lives in \`plitzi://settings/{env}\` and is edited wit
463
464
  never omit it "to save a call".
464
465
  `;
465
466
  //#endregion
466
- export { guideQuickstart, guideText, serverInstructions };
467
+ export { guideQuickstart, guideText, serverInstructions, widgetsOnlyInstructions };
@@ -280,9 +280,21 @@ var pageRefOfElement = (schema, el) => {
280
280
  /** Total number of descendant elements under a subtree (excluding the root). */
281
281
  var descendantCount = (schema, rootId) => descendantIds(schema, rootId).length;
282
282
  var emptySpaceMessage = "Space data not available";
283
+ /** The code a space-dependent answer carries when the connection reaches no space, so both the agent and a host UI
284
+ * can tell "this connection cannot do that" from "the call failed". */
285
+ var noSpaceError = "NO_SPACE_ATTACHED";
283
286
  var unauthorizedSpaceMessage = "This connection has no space attached (a guest or widgets-only grant, or a token that carries no space), so NOTHING in a space can be read or edited — every other space tool will fail the same way, do not retry them. Use plitzi_render instead: it builds a self-contained widget offline, with no space, backend or account (read plitzi://render/guide). To edit a real space, the user must reconnect the integration and grant access to one.";
287
+ /** Raised when a space-dependent operation runs on a connection that resolves no spaceId. A type of its own so the
288
+ * host answers it as a STATE of the connection — a plain result the agent reads — instead of letting it surface as
289
+ * a failed call, which hosts render to the user as "cannot connect to this server". */
290
+ var NoSpaceError = class extends Error {
291
+ constructor() {
292
+ super(unauthorizedSpaceMessage);
293
+ this.name = "NoSpaceError";
294
+ }
295
+ };
284
296
  var generateObjectId = () => {
285
297
  return `${Math.floor(Date.now() / 1e3).toString(16).padStart(8, "0")}${Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join("")}`;
286
298
  };
287
299
  //#endregion
288
- export { cloneSpace, descendantCount, descendantIds, elementById, elementRefOf, emptySpace, emptySpaceMessage, findElementByRef, findFolderByRef, findPageByRef, folderAncestorIds, generateObjectId, getPageElements, indexAddElement, indexAddPage, indexInvalidateDetails, indexReRefElement, indexReRefPage, indexRemoveElements, indexRemovePage, isPageElement, nameOf, orderedChildren, pageFoldersOf, pageRefOf, pageRefOfElement, resolveRef, routeParamNames, slugRouteParams, slugify, sortFolders, spaceIndex, strOr, unauthorizedSpaceMessage };
300
+ export { NoSpaceError, cloneSpace, descendantCount, descendantIds, elementById, elementRefOf, emptySpace, emptySpaceMessage, findElementByRef, findFolderByRef, findPageByRef, folderAncestorIds, generateObjectId, getPageElements, indexAddElement, indexAddPage, indexInvalidateDetails, indexReRefElement, indexReRefPage, indexRemoveElements, indexRemovePage, isPageElement, nameOf, noSpaceError, orderedChildren, pageFoldersOf, pageRefOf, pageRefOfElement, resolveRef, routeParamNames, slugRouteParams, slugify, sortFolders, spaceIndex, strOr, unauthorizedSpaceMessage };
@@ -2,13 +2,17 @@ import { cssProperties, cssShorthands } from "../catalogs/cssCatalog/index.js";
2
2
  import { guideText } from "../helpers/guide.js";
3
3
  import { resourceErrorMessage } from "./canonical.js";
4
4
  import { envelope, jsonContents } from "./envelope.js";
5
- import { readResource } from "./router.js";
6
5
  import { registerRenderResources } from "./renderGuide.js";
6
+ import { readResource } from "./router.js";
7
7
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  //#region src/modules/mcp/resources/register.ts
9
9
  /** Register every resource on the MCP server: fixed listings plus templated per-item reads. The space is
10
- * loaded lazily via getSpace, so listing resources never touches the store — only reading one does. */
11
- var registerResources = (server, getSpace, env, log) => {
10
+ * loaded lazily via getSpace, so listing resources never touches the store — only reading one does.
11
+ *
12
+ * `hasSpace` is false when the connection reaches no space (a guest / widgets-only grant): the space-dependent
13
+ * families are then not registered at all, so the catalog the agent browses holds only what it can actually
14
+ * open — no listing that answers every read with the same refusal. */
15
+ var registerResources = (server, getSpace, env, log, hasSpace) => {
12
16
  const emit = async (uri) => {
13
17
  const start = performance.now();
14
18
  try {
@@ -43,6 +47,7 @@ var registerResources = (server, getSpace, env, log) => {
43
47
  shorthands: cssShorthands
44
48
  }))));
45
49
  registerRenderResources(server, log);
50
+ if (!hasSpace) return;
46
51
  const fixed = [
47
52
  [
48
53
  "Primer",
@@ -10,6 +10,8 @@ var RENDER_TYPE_CATEGORIES = /* @__PURE__ */ new Set([
10
10
  "form",
11
11
  "provider"
12
12
  ]);
13
+ var RENDER_RAW_HTML_TYPE = "blockHtml";
14
+ var rawHtmlDescription = "Renders a raw HTML string (props.content) — in a widget, the way to draw an inline <svg>: a logo, an icon, a sparkline, a decorative shape. Use fill/stroke \"currentColor\" so it takes the host theme. Markup only: <script>, javascript: URLs and inline on* handlers are rejected.";
13
15
  var renderTypesNote = "The built-in element types you can put in a plitzi_render widget, grouped by category. Pick by `description`. For the props of each type and the full authoring model, read plitzi://render/guide. Types that need a backend (providers) or a plugin are omitted because the widget renders offline.";
14
16
  var renderTypes = () => {
15
17
  const types = {};
@@ -18,6 +20,12 @@ var renderTypes = () => {
18
20
  category: info.category,
19
21
  description: info.description
20
22
  };
23
+ const rawHtml = BUILTIN_COMPONENTS[RENDER_RAW_HTML_TYPE];
24
+ if (rawHtml) types[RENDER_RAW_HTML_TYPE] = {
25
+ label: rawHtml.label,
26
+ category: rawHtml.category,
27
+ description: rawHtmlDescription
28
+ };
21
29
  return {
22
30
  note: renderTypesNote,
23
31
  types
@@ -161,10 +169,10 @@ and \`patchDefinition\` still changes only some CSS of one class.
161
169
  - Lay containers out with flexbox or grid — pick the direction on purpose, see **Fit the panel** below.
162
170
  - **You are not styling from zero.** Each type lands on the page with CSS you did not write, and it is the usual
163
171
  reason a widget does not look like the definitions say:
164
- - The per-type rule the SDK ships the one that changes layouts is \`container\`, which carries
165
- \`min-width: 50px; min-height: 50px\`. A rail, a divider, a dot, a spacer or a narrow cell will NOT go below
166
- 50px until you say \`"min-width": "0"\` (and/or \`"min-height": "0"\`) on it: a 2px timeline line renders 50px
167
- wide otherwise. Any flex child that must be allowed to shrink needs it too.
172
+ - The per-type CSS the SDK ships imposes **no minimum size**: a rail, a divider, a dot or a narrow cell is
173
+ exactly as small as you make it, and no \`"min-width": "0"\` escape hatch is needed. The flip side is that an
174
+ element with no content and no size takes no space at all — give a spacer its own \`height\`. (The 50px box
175
+ empty containers show on the builder canvas is a drop target, scoped to it; it never reaches a widget.)
168
176
  - The BROWSER's own defaults for the rest: \`heading\` keeps its UA font-size and ~0.67em top/bottom margins,
169
177
  \`paragraph\` ~1em margins, \`list\` a 40px \`padding-left\`, \`button\` its native chrome, \`image\` its intrinsic
170
178
  size, \`link\` its own colour and underline. In a compact widget set these explicitly — usually
@@ -256,24 +264,61 @@ The host publishes its palette as CSS variables on the page, so use them for eve
256
264
  | \`video\` | embedded video | \`props.src\` |
257
265
  | \`list\` / \`listItem\` | \`<ul>\` / \`<li>\` | nesting only |
258
266
  | \`markdown\` | rendered markdown | \`props.content\` |
267
+ | \`blockHtml\` | a raw HTML string, as written | \`props.content\` — how you draw an inline \`<svg>\`, see below |
259
268
 
260
269
  \`subType\` is an element-level field (not a prop). Guessing is safe: an unknown prop for a type comes back as a
261
270
  **warning naming the right one**, not an error. This table covers the everyday types; the resource
262
271
  **plitzi://render/types** lists every built-in type you can use (with descriptions) — read it when you need one
263
272
  that is not here (lists, tabs, dialogs, forms, icons…).
264
273
 
265
- \`image\`/\`video\` \`src\` accepts any \`https\` URL, or a \`data:\`/\`blob:\` URI for a fully self-contained graphic
266
- (e.g. an inline SVG icon or a base64 image) — both render with no extra setup.
274
+ \`image\`/\`video\` \`src\` accepts any \`https\` URL, or a \`data:\`/\`blob:\` URI for a fully self-contained graphic (a
275
+ base64 raster) — both render with no extra setup. For a **vector** graphic do not encode a \`data:\` URI: draw it
276
+ inline, as below.
277
+
278
+ ## Draw with inline SVG
279
+
280
+ A widget can draw its own graphics, and it is often the difference between a plain block of text and something
281
+ worth showing: a brand mark, a sparkline or donut, a badge, an empty-state glyph, a wave or blob behind a header,
282
+ an arrow between two steps. Put the SVG **inline** in a \`blockHtml\` element — its \`props.content\` is rendered as
283
+ written, so the markup needs no escaping, and the element is styled and laid out like any other box.
284
+
285
+ \`\`\`json
286
+ {
287
+ "operations": [
288
+ { "type": "upsertDefinitions", "definitions": {
289
+ "mark": { "desktop": { "display": "flex", "width": "32px", "height": "32px", "color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))" } }
290
+ } },
291
+ { "type": "upsertElement", "pageRef": "render", "element": {
292
+ "ref": "trend", "type": "blockHtml", "style": { "base": ["mark"] },
293
+ "props": { "content": "<svg viewBox='0 0 24 24' width='100%' height='100%' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round'><path d='M3 17l6-6 4 4 8-8'/><path d='M21 7v6h-6'/></svg>" }
294
+ } }
295
+ ]
296
+ }
297
+ \`\`\`
298
+ - **Size from the class, not the drawing**: keep a \`viewBox\` and set \`width\`/\`height\` to \`100%\`, then the
299
+ \`blockHtml\`'s own \`width\`/\`height\` decide how big it renders — one drawing, any size.
300
+ - **Colour with \`currentColor\`** (\`fill\` and/or \`stroke\`) and set \`color\` on the class from a host variable. A hex
301
+ buried inside the path is the same mistake as a hardcoded light palette: it cannot follow the theme.
302
+ - **Single quotes inside the markup** keep the JSON free of \`\\"\` escapes, as above.
303
+ - **Markup only.** \`<script>\`, \`javascript:\` URLs and inline \`on*\` handlers are rejected with an error — a widget
304
+ runs inside the host's UI. Animate with CSS (a \`states\` block, a \`transition\`) and wire behaviour with
305
+ interaction flows.
306
+
307
+ **The budget.** Every path is text you write, so an SVG earns its place only while it stays small: a handful of
308
+ paths, a few hundred characters, drawn ONCE and reused (same class, same content, or a \`repeatElement\` row) rather
309
+ than redrawn per card. Skip it entirely when something cheaper does the job — a \`fontAwesome\` icon, an emoji in a
310
+ \`text\`, an \`https\` image the user gave you, a CSS gradient or a \`border-radius\` shape. And a full illustration or
311
+ a photo-real scene (a castle, a skyline, a detailed mascot) is still the wrong trade: it costs more than the entire
312
+ rest of the widget and renders worse than the image it is imitating.
267
313
 
268
314
  ## Keep the call small
269
315
 
270
316
  Everything in \`operations\` is text you write, and a widget that takes two calls because the first ran long is a
271
317
  widget the user waits twice for. Two habits pay for themselves:
272
318
 
273
- - **Do not draw pictures in \`data:\` URIs.** A hand-written SVG scene (a castle, a skyline, a logo) costs more than
274
- the entire rest of the widget and renders worse than nothing at all. Use an \`https\` image the user gave you, a
275
- flat colour or a two-stop \`linear-gradient\` as a banner, an emoji or an \`fontAwesome\` icon for a glyph — or drop
276
- the decoration. A small self-contained \`data:\` SVG is fine for a **simple** shape (a check, an arrow, a dot).
319
+ - **Keep a drawing to a drawing.** Inline SVG is allowed and often worth it (see above), but a graphic that runs
320
+ longer than the widget around it has stopped paying for itself: a handful of paths, drawn once and reused rather
321
+ than redrawn per card, and an \`https\` image, a flat colour or a two-stop \`linear-gradient\` for anything bigger.
277
322
  - **One class per look, not per property.** Classes like \`tone-blue\` + \`tone-blue-text\` + \`tone-blue-bg\` for the
278
323
  same card triple the declarations and the attachments. Put everything the look needs in one class, add a second
279
324
  only for the part that genuinely varies between siblings (a colour), and reuse it — the whole point of a class.
@@ -355,6 +400,13 @@ connects that source to a descendant's field.
355
400
  The tool returns \`rendered: false\` with \`errors: [{ path, message, hint }]\`. Read the hint, fix that one op, and
356
401
  retry — you never lose the rest of the batch.
357
402
  `;
403
+ /** The same two documents through the READ path (the router, hence plitzi_read), not only as MCP resources: an
404
+ * agent that reaches for the batch-read tool — the reflex the guide itself teaches — must not be told its own
405
+ * authoring guide does not exist. Space-independent, like the core singletons. */
406
+ var readRenderResource = (uri) => {
407
+ if (uri === "plitzi://render/guide") return envelope(renderGuideText);
408
+ if (uri === "plitzi://render/types") return envelope(renderTypes());
409
+ };
358
410
  var registerRenderResources = (server, log) => {
359
411
  server.registerResource("Render guide", RENDER_GUIDE_URI, {
360
412
  description: "How to author a plitzi_render widget: operations, element types, styling, examples.",
@@ -380,4 +432,4 @@ var registerRenderResources = (server, log) => {
380
432
  });
381
433
  };
382
434
  //#endregion
383
- export { RENDER_GUIDE_URI, RENDER_TYPES_URI, registerRenderResources };
435
+ export { RENDER_GUIDE_URI, RENDER_TYPES_URI, readRenderResource, registerRenderResources };
@@ -1,11 +1,15 @@
1
+ import { emptySpace } from "../helpers/space.js";
2
+ import "../helpers/uris.js";
1
3
  import { canonicalUri } from "./canonical.js";
2
4
  import { readCoreResource } from "./core.js";
3
5
  import { readPrimerResource } from "./primer.js";
6
+ import { readRenderResource } from "./renderGuide.js";
4
7
  import { readSchemaResource } from "./schema.js";
5
8
  import { readStyleResource } from "./style.js";
6
9
  //#region src/modules/mcp/resources/router.ts
7
10
  var resolvers = [
8
11
  (space, _env, uri) => readCoreResource(space, uri),
12
+ (_space, _env, uri) => readRenderResource(uri),
9
13
  readPrimerResource,
10
14
  readSchemaResource,
11
15
  readStyleResource
@@ -19,7 +23,15 @@ var readResource = (space, env, rawUri) => {
19
23
  }
20
24
  return null;
21
25
  };
26
+ /** What a connection carrying NO space (a guest / widgets-only grant) can still read: the space-independent
27
+ * singletons and the render docs. Undefined for everything else, so the caller says the connection has no space
28
+ * instead of answering out of an empty one — an empty page list would read as "this space has no pages". */
29
+ var readPublicResource = (env, rawUri) => {
30
+ const uri = canonicalUri(env, rawUri);
31
+ if (uri === "plitzi://types") return;
32
+ return readRenderResource(uri) ?? readCoreResource(emptySpace(), uri);
33
+ };
22
34
  /** Current version of a resource, for optimistic-concurrency checks. Null when the URI is unknown. */
23
35
  var resourceVersion = (space, env, uri) => readResource(space, env, uri)?.stateVersion ?? null;
24
36
  //#endregion
25
- export { readResource, resourceVersion };
37
+ export { readPublicResource, readResource, resourceVersion };
@@ -1,6 +1,6 @@
1
1
  import { registerApps } from "./apps/index.js";
2
- import { emptySpace, emptySpaceMessage, unauthorizedSpaceMessage } from "./helpers/space.js";
3
- import { serverInstructions } from "./helpers/guide.js";
2
+ import { NoSpaceError, emptySpace, emptySpaceMessage, noSpaceError } from "./helpers/space.js";
3
+ import { serverInstructions, widgetsOnlyInstructions } from "./helpers/guide.js";
4
4
  import { createMcpLog } from "./helpers/log.js";
5
5
  import { registerResources } from "./resources/register.js";
6
6
  import { tools } from "./tools/index.js";
@@ -13,19 +13,20 @@ var asText = (data) => ({ content: [{
13
13
  type: "text",
14
14
  text: JSON.stringify(data)
15
15
  }] });
16
- var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) => {
16
+ var createMcpServer = async ({ adapters, getSpaceId, preview, screenshot, logger, renderStreaming = true }) => {
17
17
  const log = createMcpLog(logger);
18
- let spaceIdPromise;
19
- const requireSpaceId = () => spaceIdPromise ??= getSpaceId().then((id) => {
20
- if (!id) throw new Error(unauthorizedSpaceMessage);
21
- return id;
22
- });
18
+ const spaceId = await getSpaceId().catch(() => void 0);
19
+ const hasSpace = spaceId !== void 0;
20
+ const requireSpaceId = () => {
21
+ if (spaceId === void 0) throw new NoSpaceError();
22
+ return spaceId;
23
+ };
23
24
  const loadSpace = async () => {
24
- const spaceId = await requireSpaceId();
25
+ const id = requireSpaceId();
25
26
  const [schema, style, catalog] = await Promise.all([
26
- adapters.getSchema?.(spaceId, MCP_ENV),
27
- adapters.getStyle?.(spaceId, MCP_ENV),
28
- adapters.getComponentCatalog?.(spaceId, MCP_ENV).catch(() => void 0)
27
+ adapters.getSchema?.(id, MCP_ENV),
28
+ adapters.getStyle?.(id, MCP_ENV),
29
+ adapters.getComponentCatalog?.(id, MCP_ENV).catch(() => void 0)
29
30
  ]);
30
31
  if (!schema || !style) throw new Error(emptySpaceMessage);
31
32
  return {
@@ -36,22 +37,22 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
36
37
  };
37
38
  const { saveSchema, saveStyle } = adapters;
38
39
  const persisters = {
39
- schema: saveSchema ? async (schema) => saveSchema(await requireSpaceId(), MCP_ENV, schema) : void 0,
40
- style: saveStyle ? async (style) => saveStyle(await requireSpaceId(), MCP_ENV, style) : void 0
40
+ schema: saveSchema ? (schema) => saveSchema(requireSpaceId(), MCP_ENV, schema) : void 0,
41
+ style: saveStyle ? (style) => saveStyle(requireSpaceId(), MCP_ENV, style) : void 0
41
42
  };
42
43
  let spacePromise;
43
44
  const getSpace = () => spacePromise ??= loadSpace();
44
45
  const server = new McpServer({
45
46
  name: "plitzi-mcp",
46
- version: "0.32.18"
47
- }, { instructions: serverInstructions });
48
- registerResources(server, getSpace, MCP_ENV, log);
49
- registerApps(server);
47
+ version: "0.32.20"
48
+ }, { instructions: hasSpace ? serverInstructions : widgetsOnlyInstructions });
49
+ registerResources(server, getSpace, MCP_ENV, log, hasSpace);
50
+ registerApps(server, { streaming: renderStreaming });
50
51
  const toolContext = async () => ({
51
52
  space: await getSpace(),
52
53
  env: MCP_ENV,
53
54
  persisters,
54
- spaceId: await requireSpaceId(),
55
+ spaceId: requireSpaceId(),
55
56
  preview,
56
57
  screenshot
57
58
  });
@@ -62,16 +63,27 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
62
63
  preview,
63
64
  screenshot
64
65
  });
66
+ const behaviorOf = (tool) => {
67
+ if (tool.spaceless) return (args) => tool.execute(args, spacelessContext());
68
+ if (hasSpace) return async (args) => tool.execute(args, await toolContext());
69
+ return tool.executePublic ? (args) => tool.executePublic?.(args, MCP_ENV) : void 0;
70
+ };
65
71
  for (const tool of tools) {
66
72
  if (tool.requires === "screenshot" && !screenshot) continue;
73
+ const behavior = behaviorOf(tool);
74
+ if (!behavior) continue;
67
75
  const run = async (args) => {
68
76
  const start = performance.now();
69
77
  try {
70
- const result = await tool.execute(args, tool.spaceless ? spacelessContext() : await toolContext());
78
+ const result = await behavior(args);
71
79
  log.toolCall(tool.name, args, performance.now() - start);
72
80
  return isCallToolResult(result) ? result : asText(result);
73
81
  } catch (error) {
74
82
  log.toolCall(tool.name, args, performance.now() - start, error);
83
+ if (error instanceof NoSpaceError) return asText({
84
+ error: noSpaceError,
85
+ message: error.message
86
+ });
75
87
  throw error;
76
88
  }
77
89
  };
@@ -1,5 +1,7 @@
1
+ import { noSpaceError, unauthorizedSpaceMessage } from "../helpers/space.js";
1
2
  import { resourceErrorMessage } from "../resources/canonical.js";
2
- import { readResource } from "../resources/router.js";
3
+ import { RENDER_GUIDE_URI, RENDER_TYPES_URI } from "../resources/renderGuide.js";
4
+ import { readPublicResource, readResource } from "../resources/router.js";
3
5
  import { defineTool } from "./shared/tool.js";
4
6
  import { z } from "zod";
5
7
  //#region src/modules/mcp/tools/read.ts
@@ -21,13 +23,29 @@ var read = (input, space, env) => {
21
23
  };
22
24
  }) };
23
25
  };
26
+ var publicHint = `Readable on this connection: ${RENDER_GUIDE_URI}, ${RENDER_TYPES_URI}, plitzi://guide, plitzi://css-properties. Everything else lives in a space this connection has none of.`;
27
+ var readPublic = (input, env) => ({ results: input.uris.map((uri) => {
28
+ const found = readPublicResource(env, uri);
29
+ if (found) return {
30
+ uri,
31
+ stateVersion: found.stateVersion,
32
+ data: found.data
33
+ };
34
+ return {
35
+ uri,
36
+ error: noSpaceError,
37
+ message: unauthorizedSpaceMessage,
38
+ hint: publicHint
39
+ };
40
+ }) });
24
41
  var readTool = defineTool({
25
42
  name: "plitzi_read",
26
43
  title: "Batch read",
27
44
  description: "Batch-fetch resource content for URIs you ALREADY HOLD (from plitzi_search or a write response) — the tool form of opening MCP resources, for many at once. Pass an array of URIs (pages, elements, definitions, variables) and get them all in one call. NOT for finding things: to locate an element by label/type use plitzi_search; to browse what exists, list the plitzi:// resources. Each result is { uri, stateVersion, data } or a teachable error, so one bad URI never fails the batch. Never hand-build a URI to guess your way to an element — search for it.",
28
45
  inputShape: readShape,
29
46
  access: "read",
30
- run: (input, ctx) => read(input, ctx.space, ctx.env)
47
+ run: (input, ctx) => read(input, ctx.space, ctx.env),
48
+ runPublic: readPublic
31
49
  });
32
50
  //#endregion
33
- export { read, readShape, readTool };
51
+ export { read, readPublic, readShape, readTool };
@@ -1,3 +1,4 @@
1
+ import { iconFontCss } from "../apps/render/styles.js";
1
2
  import { RENDER_APP_URI } from "../apps/render/index.js";
2
3
  import { emptySpace } from "../helpers/space.js";
3
4
  import { operations } from "./operations/index.js";
@@ -48,7 +49,7 @@ var render = (input) => {
48
49
  errors: expansion.errors
49
50
  };
50
51
  const ops = expansion.operations;
51
- const validation = validateOperations(space, ops);
52
+ const validation = validateOperations(space, ops, "widget");
52
53
  if (!validation.valid) return {
53
54
  rendered: false,
54
55
  errors: validation.errors,
@@ -136,6 +137,8 @@ var toPatchResult = (ops, renderId) => {
136
137
  };
137
138
  };
138
139
  var newRenderId = () => `r${randomUUID().slice(0, 8)}`;
140
+ var ICON_TYPE = "fontAwesome";
141
+ var drawsIcons = (offlineData) => Object.values(offlineData.schema.flat).some((element) => element.definition.type === ICON_TYPE);
139
142
  var toRenderResult = (res, renderId) => {
140
143
  if (!res.rendered) return { content: [{
141
144
  type: "text",
@@ -160,14 +163,15 @@ var toRenderResult = (res, renderId) => {
160
163
  structuredContent: {
161
164
  ...summary,
162
165
  offlineData: res.offlineData,
163
- operations: res.operations
166
+ operations: res.operations,
167
+ ...drawsIcons(res.offlineData) ? { iconCss: iconFontCss() } : {}
164
168
  }
165
169
  };
166
170
  };
167
171
  var renderTool = defineTool({
168
172
  name: "plitzi_render",
169
173
  title: "Render widget",
170
- description: "Show the user a real, rendered UI widget instead of describing one — cards, hero sections, pricing tables, forms, menus, checklists, profiles, galleries. It runs the Plitzi SDK fully offline: no backend, account, or setup. Reach for it whenever a visual layout beats prose: the user asks you to design/build/show something, OR your answer is naturally visual (a recipe → a card, a comparison → a table, steps → a checklist). Prefer showing over telling.\n\nAuthor the widget as an ordered list of `operations` that build an element tree under the pre-seeded root page \"render\". Three rules:\n1. STRUCTURE — one upsertElement builds the whole tree: set pageRef:\"render\" and give element a nested `children` array. Each element is { ref (unique), type, subType?, props?, style?, children? }; children render in order. (To attach to something you already made, use a top-level parentRef:\"<existing ref>\" instead.)\n1b. REPEATS — the moment two siblings share a shape and differ only in data (list, steps, cards, table, timeline), do NOT copy-paste them: use repeatElement { pageRef, ref (wrapper), style, template, items }. The template is written once with {{item.field}} placeholders and rendered per row; refs come out numbered (\"step-1\", \"step-2\"…). A list INSIDE each row (days with their own steps) is the same op: give the wrapping node repeat:{ items:\"{{item.<list>}}\", template:… } and put the sub-rows in the row data.\n2. STYLE — declare ALL the classes in ONE upsertDefinitions { definitions: { \"<class>\": { desktop:{ …CSS in kebab-case… } }, … } }, then attach via the element style:{ base:[\"<class ref>\"] }. Lay containers out with flex/grid. Keep the call small: one class per look (not per property), and never hand-draw a scene in a data: URI — it costs more than the whole widget; use an https image, a flat colour or a gradient.\n2b. LAYOUT — it renders in a side panel, so width is free and HEIGHT is scarce. Plain containers stack children vertically, which is the tall half-empty default to avoid: put peers (metrics, plans, options, image + text) in a wrapping row — display:flex, flex-direction:row, flex-wrap:wrap, children flex-grow:\"1\" + flex-basis:\"0%\" + min-width — or a grid with grid-template-columns:\"repeat(auto-fit, minmax(160px, 1fr))\". Keep padding 12-16px and gap 8-12px, and let the outer container fill the panel. Stack only what reads in order (heading over paragraph, forms, steps, prose). Watch the SDK defaults: every container has min-width/min-height 50px (set them to \"0\" for rails, dividers, dots and any flex child that must shrink), and heading/paragraph keep the margins the browser gives them (zero them, space with the parent gap).\n2c. THEME — it is embedded in the host UI, which MAY BE DARK, so never hardcode a light palette. Take colours from the host variables with a light-dark() fallback — background-color:\"var(--color-background-secondary, light-dark(#ffffff, #1f2430))\", color:\"var(--color-text-primary, light-dark(#0f172a, #e8eaed))\", border-color:\"var(--color-border-primary, light-dark(#e2e8f0, #333a48))\" — and always set `color` wherever you set `background-color` (a brand accent states its own text colour too).\n3. CONTENT — visible copy goes in props.content (text, heading, paragraph, button); heading level is the element subType (\"h1\"..\"h6\"); image/video take props.src. An unknown prop comes back as a warning naming the right one.\n\nCommon types: container, heading, paragraph, text, button, link, image, video, list, listItem, markdown (plitzi://render/types lists every built-in type with descriptions). Widgets can also be data-driven and interactive — an apiContainer fetches at runtime, upsertBinding wires data into elements, and upsertInteractionFlow makes them react to clicks (see the guide).\nREAD the resource plitzi://render/guide first — it has the element/prop table, the style model and a full worked example, and following it is the difference between a widget that renders and repeated failed calls.\nITERATING — to change a widget you already rendered, do NOT rebuild it: call again with patch:true, the `renderId` that render answered with, and ONLY the operations that differ (patchDefinition, patchElement, deleteElement…). The widget merges them and reports back what it applied; address rows by the refs you already know. Patch ONLY to modify that widget: a different subject or a different kind of widget is a fresh render, without patch — a patch is merged into the previous batch, so patching a new idea leaves you with both.\nReturns a compact summary including the renderId (the widget itself is shown to the user); on failure it returns teachable errors (path + hint) — read them and retry.",
174
+ description: "Show the user a real, rendered UI widget instead of describing one — cards, hero sections, pricing tables, forms, menus, checklists, profiles, galleries. It runs the Plitzi SDK fully offline: no backend, account, or setup. Reach for it whenever a visual layout beats prose: the user asks you to design/build/show something, OR your answer is naturally visual (a recipe → a card, a comparison → a table, steps → a checklist). Prefer showing over telling.\n\nAuthor the widget as an ordered list of `operations` that build an element tree under the pre-seeded root page \"render\". Three rules:\n1. STRUCTURE — one upsertElement builds the whole tree: set pageRef:\"render\" and give element a nested `children` array. Each element is { ref (unique), type, subType?, props?, style?, children? }; children render in order. (To attach to something you already made, use a top-level parentRef:\"<existing ref>\" instead.)\n1b. REPEATS — the moment two siblings share a shape and differ only in data (list, steps, cards, table, timeline), do NOT copy-paste them: use repeatElement { pageRef, ref (wrapper), style, template, items }. The template is written once with {{item.field}} placeholders and rendered per row; refs come out numbered (\"step-1\", \"step-2\"…). A list INSIDE each row (days with their own steps) is the same op: give the wrapping node repeat:{ items:\"{{item.<list>}}\", template:… } and put the sub-rows in the row data.\n2. STYLE — declare ALL the classes in ONE upsertDefinitions { definitions: { \"<class>\": { desktop:{ …CSS in kebab-case… } }, … } }, then attach via the element style:{ base:[\"<class ref>\"] }. Lay containers out with flex/grid. Keep the call small: one class per look, not per property.\n2a. GRAPHICS — a logo, a sparkline, a badge, a decorative shape: draw it as an INLINE <svg> in a blockHtml element (props.content), keeping a viewBox with width/height \"100%\" so its class sizes it, and fill/stroke \"currentColor\" so it follows the host theme. Budget it: a handful of paths, drawn once and reused, never a data: URI and never a full illustration a photo-real scene costs more than the whole widget, so use an https image, a flat colour or a gradient for that. Markup only: scripts and on* handlers are rejected.\n2b. LAYOUT — it renders in a side panel, so width is free and HEIGHT is scarce. Plain containers stack children vertically, which is the tall half-empty default to avoid: put peers (metrics, plans, options, image + text) in a wrapping row — display:flex, flex-direction:row, flex-wrap:wrap, children flex-grow:\"1\" + flex-basis:\"0%\" + min-width — or a grid with grid-template-columns:\"repeat(auto-fit, minmax(160px, 1fr))\". Keep padding 12-16px and gap 8-12px, and let the outer container fill the panel. Stack only what reads in order (heading over paragraph, forms, steps, prose). Nothing carries a minimum size, so an element with no content and no size takes none give a spacer or a rail its own height/width — while heading/paragraph do keep the margins the browser gives them (zero them, space with the parent gap).\n2c. THEME — it is embedded in the host UI, which MAY BE DARK, so never hardcode a light palette. Take colours from the host variables with a light-dark() fallback — background-color:\"var(--color-background-secondary, light-dark(#ffffff, #1f2430))\", color:\"var(--color-text-primary, light-dark(#0f172a, #e8eaed))\", border-color:\"var(--color-border-primary, light-dark(#e2e8f0, #333a48))\" — and always set `color` wherever you set `background-color` (a brand accent states its own text colour too).\n3. CONTENT — visible copy goes in props.content (text, heading, paragraph, button); heading level is the element subType (\"h1\"..\"h6\"); image/video take props.src. An unknown prop comes back as a warning naming the right one.\n\nCommon types: container, heading, paragraph, text, button, link, image, video, list, listItem, markdown (plitzi://render/types lists every built-in type with descriptions). Widgets can also be data-driven and interactive — an apiContainer fetches at runtime, upsertBinding wires data into elements, and upsertInteractionFlow makes them react to clicks (see the guide).\nREAD the resource plitzi://render/guide first — it has the element/prop table, the style model and a full worked example, and following it is the difference between a widget that renders and repeated failed calls.\nITERATING — to change a widget you already rendered, do NOT rebuild it: call again with patch:true, the `renderId` that render answered with, and ONLY the operations that differ (patchDefinition, patchElement, deleteElement…). The widget merges them and reports back what it applied; address rows by the refs you already know. Patch ONLY to modify that widget: a different subject or a different kind of widget is a fresh render, without patch — a patch is merged into the previous batch, so patching a new idea leaves you with both.\nReturns a compact summary including the renderId (the widget itself is shown to the user); on failure it returns teachable errors (path + hint) — read them and retry.",
171
175
  inputShape: renderShape,
172
176
  access: "read",
173
177
  spaceless: true,
@@ -13,16 +13,21 @@ var imageResult = (images, meta) => ({ content: [{
13
13
  /** Author a tool: give it its metadata, its input shape and a typed `run`. The returned descriptor parses the
14
14
  * raw args against the shape before handing them to `run`, so `run` is fully typed and no cast is needed.
15
15
  * Adding a tool is: call defineTool in its own file and append it to the `tools` registry. */
16
- var defineTool = (spec) => ({
17
- name: spec.name,
18
- title: spec.title,
19
- description: spec.description,
20
- inputShape: spec.inputShape,
21
- access: spec.access,
22
- requires: spec.requires,
23
- spaceless: spec.spaceless,
24
- ui: spec.ui,
25
- execute: (args, ctx) => spec.run(z.object(spec.inputShape).parse(args), ctx)
26
- });
16
+ var defineTool = (spec) => {
17
+ const { runPublic } = spec;
18
+ const parse = (args) => z.object(spec.inputShape).parse(args);
19
+ return {
20
+ name: spec.name,
21
+ title: spec.title,
22
+ description: spec.description,
23
+ inputShape: spec.inputShape,
24
+ access: spec.access,
25
+ requires: spec.requires,
26
+ spaceless: spec.spaceless,
27
+ ui: spec.ui,
28
+ execute: (args, ctx) => spec.run(parse(args), ctx),
29
+ ...runPublic ? { executePublic: (args, env) => runPublic(parse(args), env) } : {}
30
+ };
31
+ };
27
32
  //#endregion
28
33
  export { defineTool, imageResult };
@@ -32,7 +32,17 @@ var checkVariantApplication = (initialState, path, ctx) => {
32
32
  }
33
33
  }
34
34
  };
35
+ var EXECUTABLE_MARKUP = /<script\b|\son[a-z]+\s*=|javascript:/i;
36
+ var checkRawMarkup = (type, props, path, ctx) => {
37
+ if (ctx.mode !== "widget" || !props || !RAW_CODE_TYPES.has(type)) return;
38
+ for (const [key, value] of Object.entries(props)) if (typeof value === "string" && EXECUTABLE_MARKUP.test(value)) ctx.errors.push({
39
+ path: `${path}.props.${key}`,
40
+ message: `Executable markup in a "${type}" prop is not allowed in a widget`,
41
+ hint: "A widget renders inside the host UI, so <script>, javascript: URLs and inline on* handlers are rejected. Keep the markup inert (an inline <svg> is what this type is for) and wire behaviour with upsertInteractionFlow."
42
+ });
43
+ };
35
44
  var checkElementProps = (element, path, ctx) => {
45
+ checkRawMarkup(element.type, element.props, path, ctx);
36
46
  if (!element.props || RAW_CODE_TYPES.has(element.type)) return;
37
47
  for (const [key, value] of Object.entries(element.props)) if (typeof value === "string") checkVarRefs(value, `${path}.props.${key}`, ctx);
38
48
  checkTypeProps(element.type, element.props, path, ctx);
@@ -55,4 +65,4 @@ var checkElementInput = (element, path, ctx, seen) => {
55
65
  element.children?.forEach((child, i) => checkElementInput(child, `${path}.children[${i}]`, ctx, seen));
56
66
  };
57
67
  //#endregion
58
- export { checkElementInput, checkTypeProps, checkVariantApplication };
68
+ export { checkElementInput, checkRawMarkup, checkTypeProps, checkVariantApplication };
@@ -8,7 +8,7 @@ import { checkObservedName, checkVarRefs, warnOnce } from "./context.js";
8
8
  import { checkBindingSourceScope, checkBindingTarget, checkBindingTransformers } from "./bindings.js";
9
9
  import { checkSlotCss } from "./css.js";
10
10
  import { checkRef } from "./refs.js";
11
- import { checkElementInput, checkTypeProps, checkVariantApplication } from "./elements.js";
11
+ import { checkElementInput, checkRawMarkup, checkTypeProps, checkVariantApplication } from "./elements.js";
12
12
  import { checkInteractionNode } from "./interactions.js";
13
13
  //#region src/modules/mcp/tools/shared/validator/index.ts
14
14
  var STYLE_CATEGORIES = [
@@ -35,9 +35,10 @@ var buildTypeMeta = (catalog) => {
35
35
  };
36
36
  /** The shared validation context, derived from a space (+ the batch's ops, for batch-declared names). Extracted so
37
37
  * the post-apply resource audit (auditResources) can run the same checks against the resulting draft. */
38
- var buildValidationCtx = (space, ops) => {
38
+ var buildValidationCtx = (space, ops, mode = "space") => {
39
39
  const registry = buildTypeRegistry(space.schema, space.catalog);
40
40
  return {
41
+ mode,
41
42
  errors: [],
42
43
  warnings: [],
43
44
  warned: /* @__PURE__ */ new Set(),
@@ -57,11 +58,11 @@ var buildValidationCtx = (space, ops) => {
57
58
  observedSources: observedDataSources(space.schema)
58
59
  };
59
60
  };
60
- var validateOperations = (space, ops) => {
61
+ var validateOperations = (space, ops, mode = "space") => {
61
62
  const batchPages = batchDeclaredPages(ops);
62
63
  const batchFolders = batchDeclaredFolders(ops);
63
64
  const folderRefs = () => pageFoldersOf(space.schema).map((f) => f.id);
64
- const ctx = buildValidationCtx(space, ops);
65
+ const ctx = buildValidationCtx(space, ops, mode);
65
66
  if (ops.length > 1e3) ctx.errors.push({
66
67
  path: "operations",
67
68
  message: `Batch has ${ops.length} operations (max ${MAX_OPS})`,
@@ -91,7 +92,10 @@ var validateOperations = (space, ops) => {
91
92
  const target = page ? resolveRef(space.schema, page, op.ref) : void 0;
92
93
  if (op.props) {
93
94
  for (const [key, value] of Object.entries(op.props)) if (typeof value === "string") checkVarRefs(value, `${base}.props.${key}`, ctx);
94
- if (target && target.id !== page?.id) checkTypeProps(target.definition.type, op.props, base, ctx);
95
+ if (target && target.id !== page?.id) {
96
+ checkRawMarkup(target.definition.type, op.props, base, ctx);
97
+ checkTypeProps(target.definition.type, op.props, base, ctx);
98
+ }
95
99
  }
96
100
  checkVariantApplication(op.initialState, `${base}.initialState`, ctx);
97
101
  break;
@@ -1,8 +1,11 @@
1
- import { McpApp } from '../types';
1
+ import { McpApp, McpViewSettings } from '../types';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ /** What a view gets when the deployment says nothing: streaming on, because a host that streams no arguments
4
+ * paints exactly what it painted before. */
5
+ export declare const DEFAULT_VIEW_SETTINGS: McpViewSettings;
3
6
  /** Every MCP App this server serves. A new one is a folder beside `render/` (its definition + its view) and one
4
7
  * line here — the tool that opens it points at its `uri` through `ui: { resourceUri }`. Start from `example/`,
5
8
  * which is that same shape stripped to the minimum. */
6
9
  export declare const apps: McpApp[];
7
- export declare const registerApps: (server: McpServer) => void;
8
- export { RENDER_APP_URI } from './render';
10
+ export declare const registerApps: (server: McpServer, settings?: McpViewSettings) => void;
11
+ export { iconFontCss, RENDER_APP_URI } from './render';
@@ -1,3 +1,4 @@
1
1
  import { McpApp } from '../../types';
2
2
  export declare const RENDER_APP_URI = "ui://plitzi/render.html";
3
3
  export declare const renderApp: McpApp;
4
+ export { iconFontCss } from './styles';
@@ -0,0 +1,10 @@
1
+ /** Lifts every @font-face block out of a stylesheet. Brace counting rather than a CSS parser: a @font-face body is
2
+ * plain declarations, and base64 payloads and url() values carry no braces of their own. */
3
+ export declare const splitFontFaces: (css: string) => {
4
+ base: string;
5
+ fonts: string;
6
+ };
7
+ /** What the page shell inlines: the SDK stylesheet without the icon fonts. */
8
+ export declare const widgetCss: () => string;
9
+ /** The icon fonts, for the renders that draw one. */
10
+ export declare const iconFontCss: () => string;