@plitzi/sdk-server 0.32.7 → 0.32.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @plitzi/sdk-server
2
2
 
3
+ ## 0.32.9
4
+
5
+ ### Patch Changes
6
+
7
+ - v0.32.9
8
+ - Updated dependencies
9
+ - @plitzi/plitzi-sdk@0.32.9
10
+ - @plitzi/sdk-schema@0.32.9
11
+ - @plitzi/sdk-shared@0.32.9
12
+
13
+ ## 0.32.8
14
+
15
+ ### Patch Changes
16
+
17
+ - v0.32.8
18
+ - Updated dependencies
19
+ - @plitzi/plitzi-sdk@0.32.8
20
+ - @plitzi/sdk-schema@0.32.8
21
+ - @plitzi/sdk-shared@0.32.8
22
+
3
23
  ## 0.32.7
4
24
 
5
25
  ### Patch Changes
@@ -18,6 +18,8 @@ var configStaticStage = (ctx) => {
18
18
  for (const [prefix, rootDir] of Object.entries(mounts)) {
19
19
  const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
20
20
  if (ctx.req.path === prefix || ctx.req.path.startsWith(normalizedPrefix)) {
21
+ ctx.res.setHeader("Access-Control-Allow-Origin", "*");
22
+ ctx.res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
21
23
  if (serveStatic({
22
24
  ...ctx.req,
23
25
  path: ctx.req.path.slice(prefix.length) || "/"
@@ -7,7 +7,7 @@ var serveMcp = (ctx) => {
7
7
  const { previewClient, screenshot } = ctx.config;
8
8
  const preview = previewClient ? createHttpPreviewClient(previewClient) : void 0;
9
9
  const screenshotClient = screenshot ? createHttpScreenshotClient(screenshot) : void 0;
10
- return handleMcp(ctx.raw, ctx.rawRes, ctx.req, ctx.config.adapters, preview, screenshotClient, ctx.config.mcpLogger);
10
+ return handleMcp(ctx.raw, ctx.rawRes, ctx.req, ctx.config.adapters, preview, screenshotClient, ctx.config.mcpLogger, ctx.config.renderApp);
11
11
  };
12
12
  var mcpStage = async (ctx) => {
13
13
  if (!ctx.req.path.startsWith(mcpPathOf(ctx.config.mcp?.path))) return false;
@@ -25,6 +25,10 @@ var buildSSRPipeline = (services) => {
25
25
  stages.push(services.ssr ? ssrStage : notFoundStage);
26
26
  return stages;
27
27
  };
28
- var buildMCPPipeline = () => [healthStage, mcpOnlyStage];
28
+ var buildMCPPipeline = () => [
29
+ healthStage,
30
+ configStaticStage,
31
+ mcpOnlyStage
32
+ ];
29
33
  //#endregion
30
34
  export { buildMCPPipeline, buildSSRPipeline };
@@ -64,12 +64,13 @@ var serveMcp = async (raw, res, server) => {
64
64
  await transport.close();
65
65
  }
66
66
  };
67
- var handleMcp = (raw, res, req, adapters, preview, screenshot, logger) => serveMcp(raw, res, createMcpServer({
67
+ var handleMcp = (raw, res, req, adapters, preview, screenshot, logger, renderApp) => serveMcp(raw, res, createMcpServer({
68
68
  adapters,
69
69
  getSpaceId: () => adapters.getSpaceId?.(req) ?? Promise.resolve(void 0),
70
70
  preview,
71
71
  screenshot,
72
- logger
72
+ logger,
73
+ renderApp
73
74
  }));
74
75
  //#endregion
75
76
  export { createMcpServer, handleMcp, readMcpBody, serveMcp };
@@ -1,5 +1,5 @@
1
1
  //#region src/modules/mcp/helpers/guide.ts
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 kebab-case and ATOMIC (shorthands like border/padding expanded; compound ones like flex/background/font rejected — use 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).";
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 kebab-case and ATOMIC (shorthands like border/padding expanded; compound ones like flex/background/font rejected — use 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
3
  var guideQuickstart = `# Plitzi AI MCP — quickstart
4
4
  This is the condensed guide; read \`plitzi://guide\` for the full reference (every resource, op and example).
5
5
 
@@ -3,6 +3,7 @@ import { guideText } from "../helpers/guide.js";
3
3
  import { resourceErrorMessage } from "./canonical.js";
4
4
  import { envelope, jsonContents } from "./envelope.js";
5
5
  import { readResource } from "./router.js";
6
+ import { registerRenderResources } from "./renderGuide.js";
6
7
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp";
7
8
  //#region src/modules/mcp/resources/register.ts
8
9
  /** Register every resource on the MCP server: fixed listings plus templated per-item reads. The space is
@@ -32,6 +33,7 @@ var registerResources = (server, getSpace, env, log) => {
32
33
  description: "Valid kebab-case CSS property keys",
33
34
  mimeType: "application/json"
34
35
  }, () => jsonContents("plitzi://css-properties", envelope(cssProperties)));
36
+ registerRenderResources(server);
35
37
  const fixed = [
36
38
  [
37
39
  "Primer",
@@ -0,0 +1,182 @@
1
+ //#region src/modules/mcp/resources/renderApp.ts
2
+ var RENDER_APP_URI = "ui://plitzi/render.html";
3
+ var RENDER_APP_MIME = "text/html;profile=mcp-app";
4
+ var RENDER_APP_CSP = {
5
+ resourceDomains: [
6
+ "*",
7
+ "data:",
8
+ "blob:"
9
+ ],
10
+ connectDomains: ["*"]
11
+ };
12
+ var RENDER_APP_META = {
13
+ ui: { csp: RENDER_APP_CSP },
14
+ "openai/widgetCSP": {
15
+ resource_domains: RENDER_APP_CSP.resourceDomains,
16
+ connect_domains: RENDER_APP_CSP.connectDomains
17
+ }
18
+ };
19
+ var appHtml = (sdkBase, devMode) => {
20
+ const js = `${sdkBase}/sdk-assets/plitzi-sdk.js`;
21
+ const vendor = `${sdkBase}/sdk-assets/${devMode ? "plitzi-sdk-dev-vendor.js" : "plitzi-sdk-vendor.js"}`;
22
+ return `<!doctype html>
23
+ <html lang="en">
24
+ <head>
25
+ <meta charset="utf-8" />
26
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
27
+ <title>Plitzi widget</title>
28
+ <link rel="modulepreload" href="${vendor}" crossorigin />
29
+ <script type="importmap">
30
+ {
31
+ "imports": {
32
+ "react": "${vendor}",
33
+ "react-dom": "${vendor}",
34
+ "react-dom/client": "${vendor}",
35
+ "react/jsx-runtime": "${vendor}",
36
+ "react/compiler-runtime": "${vendor}",
37
+ "@plitzi/plitzi-sdk": "${js}"
38
+ }
39
+ }
40
+ <\/script>
41
+ <link href="${`${sdkBase}/sdk-assets/plitzi-sdk.css`}" rel="stylesheet" />
42
+ <style>
43
+ html,
44
+ body {
45
+ margin: 0;
46
+ }
47
+ #plitzi:empty::after {
48
+ content: 'Rendering…';
49
+ display: block;
50
+ padding: 16px;
51
+ font: 14px system-ui, sans-serif;
52
+ color: #64748b;
53
+ }
54
+ </style>
55
+ </head>
56
+ <body>
57
+ <div id="plitzi" class="plitzi-root-container"></div>
58
+ <script type="module">
59
+ const send = msg => window.parent.postMessage(msg, '*');
60
+ const root = document.getElementById('plitzi');
61
+ let mounted = false;
62
+
63
+ // Replace the 'Rendering…' placeholder with an explicit failure panel — otherwise a tool that returns
64
+ // { rendered:false } (no offlineData) or an SDK that fails to load would leave the iframe stuck forever.
65
+ const showError = (title, details) => {
66
+ mounted = true;
67
+ root.textContent = '';
68
+ const box = document.createElement('div');
69
+ box.setAttribute('style', 'padding:16px;font:13px/1.5 system-ui,sans-serif;color:#b91c1c');
70
+ const h = document.createElement('strong');
71
+ h.textContent = title;
72
+ box.appendChild(h);
73
+ if (details) {
74
+ const pre = document.createElement('pre');
75
+ pre.setAttribute('style', 'margin:8px 0 0;white-space:pre-wrap;color:#7f1d1d;font-size:12px');
76
+ pre.textContent = details;
77
+ box.appendChild(pre);
78
+ }
79
+
80
+ root.appendChild(box);
81
+ };
82
+
83
+ // The failed-render text is the tool's JSON summary ({ rendered:false, errors:[{path,message,hint}] }).
84
+ const errorText = params => {
85
+ const item = Array.isArray(params && params.content) ? params.content.find(c => c.type === 'text') : undefined;
86
+ if (!item) {
87
+ return '';
88
+ }
89
+
90
+ try {
91
+ const parsed = JSON.parse(item.text);
92
+ if (Array.isArray(parsed.errors)) {
93
+ return parsed.errors
94
+ .map(e => '• ' + (e.path ? e.path + ': ' : '') + e.message + (e.hint ? ' — ' + e.hint : ''))
95
+ .join('\\n');
96
+ }
97
+
98
+ return item.text;
99
+ } catch {
100
+ return item.text;
101
+ }
102
+ };
103
+
104
+ const mount = async params => {
105
+ if (mounted) {
106
+ return;
107
+ }
108
+
109
+ const offlineData = params && params.structuredContent && params.structuredContent.offlineData;
110
+ if (!offlineData) {
111
+ showError('Render failed', errorText(params) || 'The tool returned no widget data.');
112
+
113
+ return;
114
+ }
115
+
116
+ mounted = true;
117
+ try {
118
+ const { render } = await import('@plitzi/plitzi-sdk');
119
+ render('plitzi', { offlineData, offlineMode: true, environment: 'main', renderMode: 'raw' }, {}, false, false);
120
+ } catch (err) {
121
+ showError('SDK failed to load', String((err && err.message) || err));
122
+ }
123
+ };
124
+
125
+ window.addEventListener('message', event => {
126
+ const msg = event.data;
127
+ if (!msg || msg.jsonrpc !== '2.0') {
128
+ return;
129
+ }
130
+
131
+ // Reply to the initialize result: the host withholds tool data until it gets our 'initialized' notification.
132
+ if (msg.id === 1 && 'result' in msg) {
133
+ send({ jsonrpc: '2.0', method: 'ui/notifications/initialized', params: {} });
134
+
135
+ return;
136
+ }
137
+
138
+ if (msg.method === 'ui/notifications/tool-result') {
139
+ void mount(msg.params);
140
+ }
141
+ });
142
+
143
+ // Start the handshake immediately — nothing external blocks it, so the iframe is 'ready' at once.
144
+ send({
145
+ jsonrpc: '2.0',
146
+ id: 1,
147
+ method: 'ui/initialize',
148
+ params: {
149
+ protocolVersion: '2025-06-18',
150
+ appInfo: { name: 'Plitzi Widget', version: '1.0.0' },
151
+ appCapabilities: { availableDisplayModes: ['inline'] }
152
+ }
153
+ });
154
+ <\/script>
155
+ </body>
156
+ </html>`;
157
+ };
158
+ var appCache = /* @__PURE__ */ new Map();
159
+ var getAppHtml = (sdkBase, devMode) => {
160
+ const key = `${devMode ? "dev" : "prod"}|${sdkBase}`;
161
+ let html = appCache.get(key);
162
+ if (!html) {
163
+ html = appHtml(sdkBase, devMode);
164
+ appCache.set(key, html);
165
+ }
166
+ return html;
167
+ };
168
+ var registerRenderApp = (server, sdkBase, devMode) => {
169
+ const html = getAppHtml(sdkBase, devMode);
170
+ server.registerResource("plitzi-render-app", RENDER_APP_URI, {
171
+ description: "Interactive view that renders a plitzi_render widget with the Plitzi SDK (client-side).",
172
+ mimeType: RENDER_APP_MIME,
173
+ _meta: RENDER_APP_META
174
+ }, () => ({ contents: [{
175
+ uri: RENDER_APP_URI,
176
+ mimeType: RENDER_APP_MIME,
177
+ text: html,
178
+ _meta: RENDER_APP_META
179
+ }] }));
180
+ };
181
+ //#endregion
182
+ export { RENDER_APP_URI, registerRenderApp };
@@ -0,0 +1,186 @@
1
+ import { BUILTIN_COMPONENTS } from "../catalogs/builtinComponents.js";
2
+ import { envelope, jsonContents } from "./envelope.js";
3
+ //#region src/modules/mcp/resources/renderGuide.ts
4
+ var RENDER_GUIDE_URI = "plitzi://render/guide";
5
+ var RENDER_TYPES_URI = "plitzi://render/types";
6
+ var RENDER_TYPE_CATEGORIES = /* @__PURE__ */ new Set([
7
+ "structure",
8
+ "basic",
9
+ "media",
10
+ "form",
11
+ "provider"
12
+ ]);
13
+ 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
+ var renderTypes = () => {
15
+ const types = {};
16
+ for (const [name, info] of Object.entries(BUILTIN_COMPONENTS)) if (info && info.category && RENDER_TYPE_CATEGORIES.has(info.category)) types[name] = {
17
+ label: info.label,
18
+ category: info.category,
19
+ description: info.description
20
+ };
21
+ return {
22
+ note: renderTypesNote,
23
+ types
24
+ };
25
+ };
26
+ var renderGuideText = `# plitzi_render — authoring guide
27
+
28
+ \`plitzi_render\` shows the user a live UI widget. You build it as a list of \`operations\` (applied IN ORDER) that
29
+ assemble an element tree under one pre-seeded root page whose ref is **"render"**. It renders fully offline — no
30
+ space, no backend, no account. Same operation vocabulary as the editing tool plitzi_apply, aimed at one page.
31
+
32
+ Most widgets are **presentation only** — the structure + styling below is all you need, so start there. The widget
33
+ runs the live Plitzi SDK though, so it can also **fetch data** (an \`apiContainer\`) and **react to events**
34
+ (interaction flows); see "Data & interactivity" at the end when a widget needs them.
35
+
36
+ Each call renders a **fresh** widget with no memory of previous calls — always send **every** operation the widget
37
+ needs in the one call. To change a widget, re-send the whole thing.
38
+
39
+ ## Build the whole widget in ONE upsertElement (nest with \`children\`)
40
+
41
+ The simplest, least error-prone path: a single \`upsertElement\` whose \`element\` carries a nested \`children\` tree —
42
+ so you describe the layout the way it renders, no ref-juggling. \`pageRef: "render"\` is required; each element in the
43
+ tree needs a unique \`ref\` — a name you choose from letters, digits, \`-\` and \`_\`, **starting with a letter and with
44
+ no dots** (e.g. \`price-row\`, \`cta_button\`).
45
+
46
+ \`\`\`json
47
+ { "type": "upsertElement", "pageRef": "render", "element": {
48
+ "ref": "card", "type": "container", "style": { "base": ["card"] },
49
+ "children": [
50
+ { "ref": "plan", "type": "heading", "subType": "h3", "props": { "content": "Pro" } },
51
+ { "ref": "amount", "type": "text", "props": { "content": "$29/mo" }, "style": { "base": ["price"] } },
52
+ { "ref": "buy", "type": "button", "props": { "content": "Start free trial" }, "style": { "base": ["cta"] } }
53
+ ]
54
+ } }
55
+ \`\`\`
56
+
57
+ An element is \`{ ref, type, subType?, props?, style?, children? }\`. Children render in array order. (You *can* also
58
+ add elements one-by-one with a top-level \`parentRef: "<existing ref>"\` and optional \`position\` — useful to append to
59
+ or restructure something you already created — but for a fresh widget the inline \`children\` tree is easier.)
60
+
61
+ ## Style with reusable classes — upsertDefinition
62
+
63
+ Styling is separate from structure: declare a class, then attach it by ref.
64
+
65
+ \`\`\`json
66
+ { "type": "upsertDefinition", "ref": "card", "desktop": { "display": "flex", "flex-direction": "column", "gap": "8px", "padding": "24px", "border-radius": "12px" } }
67
+ \`\`\`
68
+ - CSS properties in **kebab-case** (\`background-color\`, \`font-size\`, \`border-radius\`), values as plain strings.
69
+ - Attach to an element via \`style: { "base": ["card"] }\`. Stack classes: \`"base": ["card", "shadow"]\`.
70
+ - One \`ref\` can name both an element and its class (as above) — they live in different namespaces.
71
+ - Lay containers out with flexbox: \`{ "display": "flex", "flex-direction": "column", "gap": "12px" }\`.
72
+ - **Mind the intrinsic display.** Some types start non-block: \`text\` is \`display: inline\`, so to stack or size it,
73
+ wrap it in a \`container\` (or set \`display: block\`). \`heading\` and \`paragraph\` are already block.
74
+ - **Use atomic longhands.** \`padding\`, \`margin\`, \`border\`, \`border-radius\` are fine (they expand cleanly), but
75
+ \`flex\`, \`background\` and \`font\` are rejected — write \`display\`+\`flex-direction\`, \`background-color\`,
76
+ \`font-size\`+\`font-weight\` instead. An unknown property errors with the correct kebab-case key suggested.
77
+ - **Responsive:** add \`tablet\` and/or \`mobile\` blocks next to \`desktop\` (same shape); they override desktop on
78
+ smaller screens — \`{ "desktop": { "font-size": "36px" }, "mobile": { "font-size": "24px" } }\`.
79
+ - **Interactive states:** nest under \`states\` keyed by pseudo-class, each with its own breakpoint block —
80
+ \`{ "desktop": { "background-color": "#3b82f6" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } }\`
81
+ (\`hover\`, \`active\`, \`focus\`).
82
+
83
+ ## Element types (type → what to set)
84
+
85
+ | type | renders | set |
86
+ |---|---|---|
87
+ | \`container\` | layout box (a div) | nothing — style it with flex/grid |
88
+ | \`heading\` | h1–h6 title | \`props.content\`, element \`subType\`: "h1"…"h6" |
89
+ | \`paragraph\` | body text block (\`<p>\`) | \`props.content\` |
90
+ | \`text\` | inline text | \`props.content\` |
91
+ | \`button\` | clickable button | \`props.content\` |
92
+ | \`link\` | anchor | \`props.content\` (label), \`props.href\`, \`props.target\`: "self" \\| "blank" |
93
+ | \`image\` | \`<img>\` | \`props.src\`, \`props.alt\` |
94
+ | \`video\` | embedded video | \`props.src\` |
95
+ | \`list\` / \`listItem\` | \`<ul>\` / \`<li>\` | nesting only |
96
+ | \`markdown\` | rendered markdown | \`props.content\` |
97
+
98
+ \`subType\` is an element-level field (not a prop). Guessing is safe: an unknown prop for a type comes back as a
99
+ **warning naming the right one**, not an error. This table covers the everyday types; the resource
100
+ **plitzi://render/types** lists every built-in type you can use (with descriptions) — read it when you need one
101
+ that is not here (lists, tabs, dialogs, forms, icons…).
102
+
103
+ \`image\`/\`video\` \`src\` accepts any \`https\` URL, or a \`data:\`/\`blob:\` URI for a fully self-contained graphic
104
+ (e.g. an inline SVG icon or a base64 image) — both render with no extra setup.
105
+
106
+ ## Full worked example — a pricing card
107
+
108
+ \`\`\`json
109
+ {
110
+ "operations": [
111
+ { "type": "upsertDefinition", "ref": "card", "desktop": { "display": "flex", "flex-direction": "column", "gap": "8px", "padding": "24px", "background-color": "#ffffff", "border-radius": "12px", "width": "260px", "text-align": "center", "box-shadow": "0 4px 20px rgba(0,0,0,0.08)" } },
112
+ { "type": "upsertDefinition", "ref": "price", "desktop": { "font-size": "36px", "font-weight": "800", "color": "#3b82f6" } },
113
+ { "type": "upsertDefinition", "ref": "cta", "desktop": { "background-color": "#3b82f6", "color": "#ffffff", "padding": "12px 20px", "border-radius": "8px", "font-weight": "600" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } },
114
+ { "type": "upsertElement", "pageRef": "render", "element": {
115
+ "ref": "card", "type": "container", "style": { "base": ["card"] },
116
+ "children": [
117
+ { "ref": "plan", "type": "heading", "subType": "h3", "props": { "content": "Pro" } },
118
+ { "ref": "amount", "type": "text", "props": { "content": "$29/mo" }, "style": { "base": ["price"] } },
119
+ { "ref": "feat", "type": "paragraph", "props": { "content": "Unlimited projects" } },
120
+ { "ref": "buy", "type": "button", "props": { "content": "Start free trial" }, "style": { "base": ["cta"] } }
121
+ ]
122
+ } }
123
+ ]
124
+ }
125
+ \`\`\`
126
+
127
+ ## Data & interactivity (optional)
128
+
129
+ The widget runs the live SDK, so beyond static layout it can fetch data and respond to events. Both are wired by
130
+ \`ref\`, just like styling. (Full reference: **plitzi://guide** — sections Data bindings and Interactions.)
131
+
132
+ ### Fetch data — a provider + a binding
133
+ An \`apiContainer\` fetches at runtime and exposes the result as the source **\`apiContainer_<idRef>.data\`**, visible
134
+ to its **DESCENDANTS only** — the bound element must live inside the container's subtree. \`upsertBinding\` then
135
+ connects that source to a descendant's field.
136
+
137
+ \`\`\`json
138
+ {
139
+ "operations": [
140
+ { "type": "upsertElement", "pageRef": "render", "element": {
141
+ "ref": "products", "type": "apiContainer", "props": { "query": "<your data query — see plitzi://guide>" },
142
+ "children": [ { "ref": "title", "type": "heading", "subType": "h3", "props": { "content": "…" } } ]
143
+ } },
144
+ { "type": "upsertBinding", "pageRef": "render", "ref": "title", "category": "attributes", "binding": { "to": "content", "source": "apiContainer_products.data.0.name" } }
145
+ ]
146
+ }
147
+ \`\`\`
148
+ - \`category\`: \`"attributes"\` (a prop such as \`content\`/\`src\`), \`"style"\` (a CSS value) or \`"initialState"\`.
149
+ - \`binding.to\` is the target field; \`binding.source\` is the \`<type>_<idRef>.path\` into the provider's data.
150
+ - \`mockData\` on the provider is builder-only — set a real \`query\` for the widget to actually fetch.
151
+
152
+ ### React to events — an interaction flow
153
+ \`upsertInteractionFlow\` attaches an ordered \`nodes\` list to an element; the FIRST node is a \`trigger\` (e.g.
154
+ \`onClick\`), the rest run after it. Each following step is one of:
155
+ - **\`globalCallback\`** (OMIT \`elementId\`) — a built-in app action: \`addNotification\` (\`params.content\`),
156
+ \`navigate\`, \`setState\` (\`key\`/\`type\`/\`value\`), \`authLogin\`/\`authLogout\`…
157
+ - **\`callback\`** (\`elementId\` = an element, defaults to the trigger's) — that ELEMENT's OWN callback: e.g. an
158
+ \`apiContainer\` re-fetches, a \`form\` submits. Each type's own callback action names are in plitzi://guide.
159
+
160
+ \`\`\`json
161
+ { "type": "upsertInteractionFlow", "pageRef": "render", "ref": "buy", "nodes": [
162
+ { "title": "On click", "nodeType": "trigger", "action": "onClick" },
163
+ { "title": "Toast", "nodeType": "globalCallback", "action": "addNotification", "params": { "content": "Added to cart" } }
164
+ ] }
165
+ \`\`\`
166
+
167
+ ## When it fails
168
+ The tool returns \`rendered: false\` with \`errors: [{ path, message, hint }]\`. Read the hint, fix that one op, and
169
+ retry — you never lose the rest of the batch.
170
+ `;
171
+ var registerRenderResources = (server) => {
172
+ server.registerResource("Render guide", RENDER_GUIDE_URI, {
173
+ description: "How to author a plitzi_render widget: operations, element types, styling, examples.",
174
+ mimeType: "text/markdown"
175
+ }, () => ({ contents: [{
176
+ uri: RENDER_GUIDE_URI,
177
+ mimeType: "text/markdown",
178
+ text: renderGuideText
179
+ }] }));
180
+ server.registerResource("Render element types", RENDER_TYPES_URI, {
181
+ description: "Built-in element types a plitzi_render widget can use, with descriptions.",
182
+ mimeType: "application/json"
183
+ }, () => jsonContents(RENDER_TYPES_URI, envelope(renderTypes())));
184
+ };
185
+ //#endregion
186
+ export { RENDER_GUIDE_URI, RENDER_TYPES_URI, registerRenderResources };
@@ -2,6 +2,7 @@ import { emptySpace, emptySpaceMessage, unauthorizedSpaceMessage } from "./helpe
2
2
  import { serverInstructions } from "./helpers/guide.js";
3
3
  import { createMcpLog } from "./helpers/log.js";
4
4
  import { registerResources } from "./resources/register.js";
5
+ import { registerRenderApp } from "./resources/renderApp.js";
5
6
  import { tools } from "./tools/index.js";
6
7
  import { isCallToolResult } from "../ai/toolkit.js";
7
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
@@ -11,7 +12,7 @@ var asText = (data) => ({ content: [{
11
12
  type: "text",
12
13
  text: JSON.stringify(data)
13
14
  }] });
14
- var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) => {
15
+ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger, renderApp }) => {
15
16
  const log = createMcpLog(logger);
16
17
  let spaceIdPromise;
17
18
  const requireSpaceId = () => spaceIdPromise ??= getSpaceId().then((id) => {
@@ -41,9 +42,10 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
41
42
  const getSpace = () => spacePromise ??= loadSpace();
42
43
  const server = new McpServer({
43
44
  name: "plitzi-mcp",
44
- version: "0.32.7"
45
+ version: "0.32.9"
45
46
  }, { instructions: serverInstructions });
46
47
  registerResources(server, getSpace, MCP_ENV, log);
48
+ if (renderApp) registerRenderApp(server, renderApp.sdkBase, renderApp.devMode ?? false);
47
49
  const toolContext = async () => ({
48
50
  space: await getSpace(),
49
51
  env: MCP_ENV,
@@ -64,7 +66,8 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
64
66
  server.registerTool(tool.name, {
65
67
  title: tool.title,
66
68
  description: tool.description,
67
- inputSchema: tool.inputShape
69
+ inputSchema: tool.inputShape,
70
+ ...tool.ui && renderApp ? { _meta: { ui: tool.ui } } : {}
68
71
  }, async (args) => {
69
72
  const start = performance.now();
70
73
  try {
@@ -1,4 +1,5 @@
1
1
  import { emptySpace } from "../helpers/space.js";
2
+ import { RENDER_APP_URI } from "../resources/renderApp.js";
2
3
  import { operations } from "./operations/index.js";
3
4
  import { applyOperations } from "./apply/dispatch.js";
4
5
  import { defineTool } from "./shared/tool.js";
@@ -104,10 +105,11 @@ var toRenderResult = (res) => {
104
105
  var renderTool = defineTool({
105
106
  name: "plitzi_render",
106
107
  title: "Render widget",
107
- description: "Render a small self-contained UI widget the user can SEE, with the Plitzi SDK offline (no backend). Reach for it in two cases: (1) the user asks you to create/design/build/show a piece of UI (a card, hero, form, button, banner, pricing table…); (2) you are answering a content question and a visual layout communicates better than prose render the ANSWER as a widget (a recipe as a card, a comparison as a table, steps as a checklist, a profile, a menu). Prefer showing it over a long text answer whenever the content is naturally visual. You author the widget as operations targeting the pre-seeded host page `pageRef: \"render\"` (same vocabulary as plitzi_apply: upsertElement, upsertDefinition, ). Returns a compact summary; the rendered widget is shown to the user. To edit an existing space page instead, use plitzi_preview.",
108
+ 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.)\n2. STYLE declare reusable classes with upsertDefinition { ref, desktop:{ …CSS props in kebab-case… } }, then attach via the element style:{ base:[\"<class ref>\"] }. Lay containers out with flex/grid.\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.\nReturns a compact summary (the widget is shown to the user); on failure it returns teachable errors (path + hint) — read them and retry.",
108
109
  inputShape: renderShape,
109
110
  access: "read",
110
111
  spaceless: true,
112
+ ui: { resourceUri: RENDER_APP_URI },
111
113
  run: (input) => toRenderResult(render(input))
112
114
  });
113
115
  //#endregion
@@ -21,6 +21,7 @@ var defineTool = (spec) => ({
21
21
  access: spec.access,
22
22
  requires: spec.requires,
23
23
  spaceless: spec.spaceless,
24
+ ui: spec.ui,
24
25
  execute: (args, ctx) => spec.run(z.object(spec.inputShape).parse(args), ctx)
25
26
  });
26
27
  //#endregion
@@ -1,3 +1,4 @@
1
+ import { BUILTIN_COMPONENTS } from "../../../catalogs/builtinComponents.js";
1
2
  import { observedDataSources, observedInteractionActions } from "../../../catalogs/observed.js";
2
3
  import { buildTypeRegistry } from "../../../catalogs/registry.js";
3
4
  import { findElementByRef, findFolderByRef, findPageByRef, folderAncestorIds, getPageElements, pageFoldersOf, pageRefOf, resolveRef, routeParamNames } from "../../../helpers/space.js";
@@ -40,7 +41,7 @@ var buildValidationCtx = (space, ops) => {
40
41
  errors: [],
41
42
  warnings: [],
42
43
  warned: /* @__PURE__ */ new Set(),
43
- knownTypes: new Set(Object.keys(registry.types)),
44
+ knownTypes: /* @__PURE__ */ new Set([...Object.keys(registry.types), ...Object.keys(BUILTIN_COMPONENTS)]),
44
45
  typeProps: new Map(Object.entries(registry.types).map(([type, info]) => [type, new Set(Object.keys(info.props))])),
45
46
  typeMeta: buildTypeMeta(space.catalog),
46
47
  elementType: (ref) => (findElementByRef(space.schema, ref) ?? findPageByRef(space.schema, ref))?.definition.type,
@@ -5,5 +5,8 @@ import { SSRAdapters, SSRRequest, McpLogger } from '@plitzi/sdk-shared';
5
5
  import { IncomingMessage, ServerResponse } from 'node:http';
6
6
  export declare const readMcpBody: (req: IncomingMessage) => Promise<unknown>;
7
7
  export declare const serveMcp: (raw: IncomingMessage, res: ServerResponse, server: McpServer) => Promise<void>;
8
- export declare const handleMcp: (raw: IncomingMessage, res: ServerResponse, req: SSRRequest, adapters: SSRAdapters, preview?: PreviewClient, screenshot?: ScreenshotClient, logger?: McpLogger) => Promise<void>;
8
+ export declare const handleMcp: (raw: IncomingMessage, res: ServerResponse, req: SSRRequest, adapters: SSRAdapters, preview?: PreviewClient, screenshot?: ScreenshotClient, logger?: McpLogger, renderApp?: {
9
+ sdkBase: string;
10
+ devMode?: boolean;
11
+ }) => Promise<void>;
9
12
  export { createMcpServer };
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
2
+ export declare const RENDER_APP_URI = "ui://plitzi/render.html";
3
+ export declare const registerRenderApp: (server: McpServer, sdkBase: string, devMode: boolean) => void;
@@ -0,0 +1,4 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
2
+ export declare const RENDER_GUIDE_URI = "plitzi://render/guide";
3
+ export declare const RENDER_TYPES_URI = "plitzi://render/types";
4
+ export declare const registerRenderResources: (server: McpServer) => void;
@@ -18,5 +18,13 @@ export interface McpServerContext {
18
18
  /** Structured request-log sink. When set, every tool call and resource read emits an McpLogEvent to it (the
19
19
  * consumer renders them); otherwise logging falls back to the console when MCP_DEBUG=1. */
20
20
  logger?: McpLogger;
21
+ /** MCP Apps: the interactive render view for plitzi_render. `sdkBase` is this server's absolute origin, which
22
+ * serves the Plitzi SDK bundle under /sdk-assets; the iframe imports it and renders the widget client-side.
23
+ * `devMode` picks the SDK vendor bundle name (dev vs prod split) to match the served bundle. Absent → the
24
+ * ui:// view is not registered (the tool still returns its text summary + offlineData). */
25
+ renderApp?: {
26
+ sdkBase: string;
27
+ devMode?: boolean;
28
+ };
21
29
  }
22
- export declare const createMcpServer: ({ adapters, getSpaceId, preview, screenshot, logger }: McpServerContext) => McpServer;
30
+ export declare const createMcpServer: ({ adapters, getSpaceId, preview, screenshot, logger, renderApp }: McpServerContext) => McpServer;
@@ -34,6 +34,11 @@ export interface ToolSpec<Shape extends ZodRawShape> {
34
34
  /** The tool operates on no space (plitzi_render authors a throwaway one), so the host must NOT resolve a spaceId
35
35
  * or load a space for it — that keeps it callable with no auth, on the public surface. */
36
36
  spaceless?: boolean;
37
+ /** MCP Apps: link this tool to an interactive UI resource (a `ui://` HTML page the host renders in a sandboxed
38
+ * iframe, receiving the tool result). The `_meta.ui` the host reads is built from this. */
39
+ ui?: {
40
+ resourceUri: string;
41
+ };
37
42
  run: (input: z.infer<ZodObject<Shape>>, ctx: ToolContext) => unknown;
38
43
  }
39
44
  /** What the registry holds and the hosts register from: the same metadata plus a type-erased `execute` that
@@ -46,6 +51,9 @@ export interface ToolDef {
46
51
  access: 'read' | 'write';
47
52
  requires?: ToolRequires;
48
53
  spaceless?: boolean;
54
+ ui?: {
55
+ resourceUri: string;
56
+ };
49
57
  execute: (args: unknown, ctx: ToolContext) => unknown;
50
58
  }
51
59
  /** Author a tool: give it its metadata, its input shape and a typed `run`. The returned descriptor parses the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plitzi/sdk-server",
3
- "version": "0.32.7",
3
+ "version": "0.32.9",
4
4
  "license": "AGPL-3.0",
5
5
  "files": [
6
6
  "dist"
@@ -23,9 +23,9 @@
23
23
  "sideEffects": false,
24
24
  "dependencies": {
25
25
  "@modelcontextprotocol/sdk": "^1.29.0",
26
- "@plitzi/plitzi-sdk": "0.32.7",
27
- "@plitzi/sdk-schema": "0.32.7",
28
- "@plitzi/sdk-shared": "0.32.7",
26
+ "@plitzi/plitzi-sdk": "0.32.9",
27
+ "@plitzi/sdk-schema": "0.32.9",
28
+ "@plitzi/sdk-shared": "0.32.9",
29
29
  "ejs": "^6.0.1",
30
30
  "esbuild": "^0.28.1",
31
31
  "zod": "^4.4.3"