@plitzi/sdk-server 0.32.17 → 0.32.18
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 +10 -0
- package/dist/modules/mcp/apps/render/view.tsx +107 -3
- package/dist/modules/mcp/helpers/guide.js +9 -2
- package/dist/modules/mcp/resources/renderGuide.js +126 -19
- package/dist/modules/mcp/server.js +1 -1
- package/dist/modules/mcp/tools/apply/dispatch.js +2 -0
- package/dist/modules/mcp/tools/apply/index.js +17 -3
- package/dist/modules/mcp/tools/operations/index.js +4 -0
- package/dist/modules/mcp/tools/operations/schema/elements/patchElement.js +1 -1
- package/dist/modules/mcp/tools/operations/schema/elements/repeatElement.js +148 -0
- package/dist/modules/mcp/tools/operations/schema/interactions/deleteInteraction.js +1 -1
- package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.js +1 -1
- package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.js +1 -1
- package/dist/modules/mcp/tools/operations/schema/operations.js +2 -0
- package/dist/modules/mcp/tools/operations/schema/pages/upsertPage.js +1 -1
- package/dist/modules/mcp/tools/operations/schema/shared.js +8 -5
- package/dist/modules/mcp/tools/operations/schemaIds.js +54 -0
- package/dist/modules/mcp/tools/operations/style/definitions/patchDefinition.js +1 -1
- package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinition.js +1 -1
- package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinitions.js +40 -0
- package/dist/modules/mcp/tools/operations/style/globalStyles/patchGlobalStyle.js +1 -1
- package/dist/modules/mcp/tools/operations/style/globalStyles/upsertGlobalStyle.js +1 -1
- package/dist/modules/mcp/tools/operations/style/idStyles/patchIdStyle.js +1 -1
- package/dist/modules/mcp/tools/operations/style/idStyles/upsertIdStyle.js +1 -1
- package/dist/modules/mcp/tools/operations/style/operations.js +2 -0
- package/dist/modules/mcp/tools/operations/style/shared.js +1 -1
- package/dist/modules/mcp/tools/render.js +70 -8
- package/dist/modules/mcp/tools/shared/expandOperations.js +44 -0
- package/dist/modules/mcp/tools/shared/validator/audit.js +3 -0
- package/dist/modules/mcp/tools/shared/validator/batch.js +13 -4
- package/dist/modules/mcp/tools/shared/validator/index.js +15 -0
- package/dist/modules/mcp/tools/validate.js +11 -3
- package/dist/src/modules/mcp/apps/render/heldBatch.d.ts +10 -0
- package/dist/src/modules/mcp/apps/render/heldBatch.test.d.ts +1 -0
- package/dist/src/modules/mcp/e2e/index.d.ts +2 -2
- package/dist/src/modules/mcp/e2e/renderingHost.d.ts +16 -1
- package/dist/src/modules/mcp/helpers/guide.d.ts +1 -1
- package/dist/src/modules/mcp/tests/schemaIds.test.d.ts +1 -0
- package/dist/src/modules/mcp/tools/apply/index.d.ts +50 -0
- package/dist/src/modules/mcp/tools/operations/index.d.ts +100 -0
- package/dist/src/modules/mcp/tools/operations/schema/elements/repeatElement.d.ts +55 -0
- package/dist/src/modules/mcp/tools/operations/schema/index.d.ts +1 -0
- package/dist/src/modules/mcp/tools/operations/schema/operations.d.ts +19 -0
- package/dist/src/modules/mcp/tools/operations/schema/shared.d.ts +15 -0
- package/dist/src/modules/mcp/tools/operations/schemaIds.d.ts +6 -0
- package/dist/src/modules/mcp/tools/operations/style/definitions/upsertDefinitions.d.ts +41 -0
- package/dist/src/modules/mcp/tools/operations/style/index.d.ts +1 -0
- package/dist/src/modules/mcp/tools/operations/style/operations.d.ts +33 -0
- package/dist/src/modules/mcp/tools/operations/style/shared.d.ts +4 -3
- package/dist/src/modules/mcp/tools/preview.d.ts +50 -0
- package/dist/src/modules/mcp/tools/render.d.ts +421 -363
- package/dist/src/modules/mcp/tools/screenshot.d.ts +50 -0
- package/dist/src/modules/mcp/tools/shared/expandOperations.d.ts +13 -0
- package/dist/src/modules/mcp/tools/validate.d.ts +50 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/* eslint-disable react-refresh/only-export-components -- one bundled entry: components cannot move out. */
|
|
2
2
|
import { useApp, useHostStyles } from '@modelcontextprotocol/ext-apps/react';
|
|
3
3
|
import PlitziSdk from '@plitzi/plitzi-sdk';
|
|
4
|
-
import { Component, useEffect, useState } from 'react';
|
|
4
|
+
import { Component, useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { createRoot } from 'react-dom/client';
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { readHeldBatch, writeHeldBatch } from './heldBatch';
|
|
8
|
+
|
|
9
|
+
import type { App, McpUiHostContext } from '@modelcontextprotocol/ext-apps';
|
|
8
10
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
9
11
|
import type { OfflineDataRaw } from '@plitzi/sdk-shared';
|
|
10
12
|
import type { ReactNode } from 'react';
|
|
@@ -39,17 +41,119 @@ class RenderBoundary extends Component<{ children: ReactNode }, { error?: Error
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/** The batch the widget on screen was built from. It lives on the HOST side — this ref plus localStorage — because
|
|
45
|
+
* the server keeps nothing between calls, which is what lets any replica (or an edge deployment) answer any
|
|
46
|
+
* request. A patch is merged into it and sent back through `callServerTool`, so the full batch never enters the
|
|
47
|
+
* model's context. The ref alone would not do: the host gives each tool call its own view, so a patch usually
|
|
48
|
+
* starts from an empty instance and reads the batch back from storage by renderId (see heldBatch.ts). */
|
|
49
|
+
type Held = { renderId?: string; operations: unknown[] };
|
|
50
|
+
|
|
51
|
+
const summarise = (result: CallToolResult): string => {
|
|
52
|
+
const text = result.content.find(entry => entry.type === 'text');
|
|
53
|
+
|
|
54
|
+
return text?.text ?? 'no summary';
|
|
55
|
+
};
|
|
56
|
+
|
|
42
57
|
const RenderApp = () => {
|
|
43
58
|
const [result, setResult] = useState<CallToolResult | null>(null);
|
|
44
59
|
const [cancelled, setCancelled] = useState<string | undefined>(undefined);
|
|
45
60
|
const [context, setContext] = useState<McpUiHostContext | undefined>(undefined);
|
|
61
|
+
const held = useRef<Held>({ operations: [] });
|
|
62
|
+
const appRef = useRef<App | null>(null);
|
|
63
|
+
|
|
64
|
+
// A patch carries only what changed. Merging it onto the held batch and re-calling the tool is what keeps the
|
|
65
|
+
// server stateless: it re-renders the WHOLE widget (so refs, integrity and the audit are all checked as usual)
|
|
66
|
+
// from a payload that travelled host↔server, and the model hears the outcome through updateModelContext.
|
|
67
|
+
const applyPatch = async (renderId: string, delta: unknown[]): Promise<void> => {
|
|
68
|
+
const app = appRef.current;
|
|
69
|
+
if (!app) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const base = held.current.renderId === renderId ? held.current.operations : (readHeldBatch(renderId) ?? []);
|
|
74
|
+
if (base.length === 0) {
|
|
75
|
+
await app.updateModelContext({
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: `The widget ${renderId} could not be recovered, so nothing was patched. Call plitzi_render again with the complete batch and without \`patch\`.`
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const merged = [...base, ...delta];
|
|
88
|
+
// The round trip can fail on its own: a host that forwards no tool calls, a connection dropped mid-patch, a
|
|
89
|
+
// view torn down while the answer is in flight. Unreported it would surface as an unhandled rejection inside
|
|
90
|
+
// the sandbox and the model would wait forever for a widget that is never coming.
|
|
91
|
+
try {
|
|
92
|
+
const rendered = await app.callServerTool({
|
|
93
|
+
name: 'plitzi_render',
|
|
94
|
+
arguments: { operations: merged, renderId }
|
|
95
|
+
});
|
|
96
|
+
const offlineData = rendered.structuredContent?.offlineData;
|
|
97
|
+
if (offlineData) {
|
|
98
|
+
const applied = (rendered.structuredContent?.operations as unknown[] | undefined) ?? merged;
|
|
99
|
+
held.current = { renderId, operations: applied };
|
|
100
|
+
writeHeldBatch(renderId, applied);
|
|
101
|
+
setResult(rendered);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await app.updateModelContext({
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: 'text',
|
|
108
|
+
text: offlineData
|
|
109
|
+
? `Widget updated: ${summarise(rendered)}`
|
|
110
|
+
: `The patch did not apply, the widget is unchanged: ${summarise(rendered)}`
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
});
|
|
114
|
+
} catch (reason) {
|
|
115
|
+
await app
|
|
116
|
+
.updateModelContext({
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: 'text',
|
|
120
|
+
text: `The patch could not be delivered (${reason instanceof Error ? reason.message : String(reason)}), so the widget is unchanged. Re-send the full batch without \`patch\` if it needs to change.`
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
})
|
|
124
|
+
.catch(() => undefined);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
46
127
|
|
|
47
128
|
// useApp creates the App, runs onAppCreated so every handler is in place BEFORE the handshake, and connects.
|
|
48
129
|
const { app, error } = useApp({
|
|
49
130
|
appInfo: { name: 'Plitzi Widget', version: '1.0.0' },
|
|
50
131
|
capabilities: {},
|
|
51
132
|
onAppCreated: instance => {
|
|
52
|
-
|
|
133
|
+
appRef.current = instance;
|
|
134
|
+
instance.ontoolresult = toolResult => {
|
|
135
|
+
const renderId = toolResult.structuredContent?.renderId as string | undefined;
|
|
136
|
+
if (toolResult.structuredContent?.patch === true && renderId) {
|
|
137
|
+
void applyPatch(renderId, (toolResult.structuredContent.operations as unknown[] | undefined) ?? []);
|
|
138
|
+
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A result carrying no widget is a failed render or a refused patch. The model already reads its reasons
|
|
143
|
+
// as text, so it must not blank a widget that is on screen — nor drop the batch that widget was built
|
|
144
|
+
// from, which is the only copy of it anywhere. With nothing on screen yet, the error IS the view.
|
|
145
|
+
if (!toolResult.structuredContent?.offlineData && held.current.operations.length > 0) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const operations = (toolResult.structuredContent?.operations as unknown[] | undefined) ?? [];
|
|
150
|
+
held.current = { renderId, operations };
|
|
151
|
+
if (renderId && operations.length > 0) {
|
|
152
|
+
writeHeldBatch(renderId, operations);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setResult(toolResult);
|
|
156
|
+
};
|
|
53
157
|
instance.ontoolcancelled = params => setCancelled(params.reason ?? 'The host cancelled the render.');
|
|
54
158
|
instance.onhostcontextchanged = params => setContext(previous => ({ ...previous, ...params }));
|
|
55
159
|
instance.onteardown = () => ({});
|
|
@@ -202,7 +202,12 @@ pointed at the old name is repointed with it, so the element stays wired. You do
|
|
|
202
202
|
(prefer a unitless ratio like \`1.5\`, which tracks the font size). Changing one without the other leaves cramped or
|
|
203
203
|
loosely-spaced text — they are a joint change, not two separate ones.
|
|
204
204
|
- A definition lives in the **style schema**; an element's \`style.base\` (element schema) is the link that applies
|
|
205
|
-
it. Styling an element =
|
|
205
|
+
it. Styling an element = upsertDefinitions + upsertElement with that ref in \`style.base\`, in one batch.
|
|
206
|
+
- **Repeating siblings**: when a set of siblings shares a shape and differs only in data (a list, cards, rows,
|
|
207
|
+
steps), use \`repeatElement\` — the template once with \`{{item.field}}\` placeholders plus \`items\`, which
|
|
208
|
+
creates the wrapper and numbers each row's refs (\`step-1\`, \`step-2\`…). A list inside each row is the same op:
|
|
209
|
+
the wrapping node carries \`repeat: { items: "{{item.<list>}}", template: … }\` and its refs number both levels
|
|
210
|
+
(\`blk-2-3\`). Copy-pasting the subtree N times costs N times the tokens and drifts.
|
|
206
211
|
- CSS keys are **kebab-case** (\`background-color\`). camelCase is rejected — read \`plitzi://css-properties\`.
|
|
207
212
|
- **Write normal CSS — shorthands are accepted and expanded for you.** \`border\`, \`border-{side}\`,
|
|
208
213
|
\`border-width\`/\`-color\`/\`-style\`, \`border-radius\`, \`padding\`, \`margin\`, \`inset\`, \`gap\`, \`overflow\`,
|
|
@@ -232,7 +237,9 @@ pointed at the old name is repointed with it, so the element stays wired. You do
|
|
|
232
237
|
- **Three kinds of style live in the style schema — do not confuse them:**
|
|
233
238
|
- **Definitions** = reusable CSS **classes** (\`upsertDefinition\`/\`patchDefinition\`/\`deleteDefinition\`, keyed by a
|
|
234
239
|
class \`ref\`). Attach one to an element via \`style.base\` to style **that** element (and anything else that opts in).
|
|
235
|
-
This is the **default** way to style one element.
|
|
240
|
+
This is the **default** way to style one element. Declaring MORE than one class in a batch? Use
|
|
241
|
+
\`upsertDefinitions\` — one op carrying \`{ "<class>": { desktop: … }, … }\`, same result as the run of
|
|
242
|
+
\`upsertDefinition\` it replaces, without repeating the envelope once per class.
|
|
236
243
|
- **Global styles** = the CSS equivalent of a bare element selector like \`button { … }\`
|
|
237
244
|
(\`upsertGlobalStyle\`/\`patchGlobalStyle\`/\`deleteGlobalStyle\`, keyed by \`componentType\`). They style **every**
|
|
238
245
|
element of that type at once. Use these for site-wide intent — e.g. "all buttons rounded":
|
|
@@ -33,8 +33,26 @@ Most widgets are **presentation only** — the structure + styling below is all
|
|
|
33
33
|
runs the live Plitzi SDK though, so it can also **fetch data** (an \`apiContainer\`) and **react to events**
|
|
34
34
|
(interaction flows); see "Data & interactivity" at the end when a widget needs them.
|
|
35
35
|
|
|
36
|
-
Each call renders a **fresh** widget
|
|
37
|
-
|
|
36
|
+
Each call renders a **fresh** widget: send **every** operation it needs in the one call.
|
|
37
|
+
|
|
38
|
+
**To CHANGE a widget you already rendered, do not rebuild it** — every render answers with a \`renderId\`; call
|
|
39
|
+
again with that id, \`patch: true\` and only the operations that differ:
|
|
40
|
+
|
|
41
|
+
\`\`\`json
|
|
42
|
+
{ "patch": true, "renderId": "r7f3a2c", "operations": [
|
|
43
|
+
{ "type": "patchDefinition", "ref": "tl-card", "desktop": { "border-radius": "20px" } },
|
|
44
|
+
{ "type": "patchElement", "pageRef": "render", "ref": "title-2", "props": { "content": "EPCOT (día largo)" } }
|
|
45
|
+
] }
|
|
46
|
+
\`\`\`
|
|
47
|
+
The widget merges the delta into the batch it was built from and reports back what it applied — including the
|
|
48
|
+
errors, if a ref does not exist. Address rows by the refs you already know (\`card-1\`, \`blk-2-3\`). If it reports
|
|
49
|
+
that the widget could not be recovered (a surface that renders none, a host that keeps no storage, a conversation
|
|
50
|
+
resumed elsewhere), send the whole batch again without \`patch\`.
|
|
51
|
+
|
|
52
|
+
**Patch only to modify what is on screen.** A different subject, or a different kind of widget, is a FRESH render
|
|
53
|
+
with no \`patch\` — the delta is merged into the previous widget, so patching a new idea leaves the user looking at
|
|
54
|
+
both at once. Rebuilding when you could have patched only costs tokens; patching when you should have rebuilt costs
|
|
55
|
+
the user a wrong widget.
|
|
38
56
|
|
|
39
57
|
## Build the whole widget in ONE upsertElement (nest with \`children\`)
|
|
40
58
|
|
|
@@ -58,20 +76,91 @@ An element is \`{ ref, type, subType?, props?, style?, children? }\`. Children r
|
|
|
58
76
|
add elements one-by-one with a top-level \`parentRef: "<existing ref>"\` and optional \`position\` — useful to append to
|
|
59
77
|
or restructure something you already created — but for a fresh widget the inline \`children\` tree is easier.)
|
|
60
78
|
|
|
61
|
-
##
|
|
79
|
+
## Repeating rows — repeatElement, never copy-paste
|
|
80
|
+
|
|
81
|
+
The moment two siblings have the same shape and different data — a list, steps, cards, a table, a timeline —
|
|
82
|
+
**write the shape once** and hand over the rows. \`repeatElement\` creates the wrapper (style it with the row/grid
|
|
83
|
+
class) and renders the template once per entry of \`items\`:
|
|
84
|
+
|
|
85
|
+
\`\`\`json
|
|
86
|
+
{ "type": "repeatElement", "pageRef": "render", "ref": "steps", "style": { "base": ["list"] },
|
|
87
|
+
"template": {
|
|
88
|
+
"ref": "step", "type": "container", "style": { "base": ["row"] },
|
|
89
|
+
"children": [
|
|
90
|
+
{ "ref": "at", "type": "paragraph", "style": { "base": ["time"] }, "props": { "content": "{{item.time}}" } },
|
|
91
|
+
{ "ref": "what", "type": "paragraph", "style": { "base": ["txt"] }, "props": { "content": "{{item.text}}" } }
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
"items": [
|
|
95
|
+
{ "time": "08:00", "text": "Doors open, head straight to the main hall." },
|
|
96
|
+
{ "time": "10:30", "text": "Workshops in the east wing." },
|
|
97
|
+
{ "time": "13:00", "text": "Lunch, then the keynote." }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
\`\`\`
|
|
101
|
+
- \`{{item.<field>}}\` is replaced by that row's field, anywhere in the template (props, a style ref, a param).
|
|
102
|
+
A placeholder that is the WHOLE value keeps the field's type (\`"{{item.count}}"\` with \`count: 3\` stays the
|
|
103
|
+
number 3); mixed with text it interpolates. Dotted paths work: \`{{item.author.name}}\`.
|
|
104
|
+
- Every ref in the template gets the row number appended — \`step\` becomes \`step-1\`, \`step-2\`… — so rows never
|
|
105
|
+
collide and you can address one later without reading anything back.
|
|
106
|
+
- Other \`{{…}}\` names are left untouched, so schema variables keep working inside a template.
|
|
107
|
+
- A row missing a field the template reads fails the batch and names the row and the fields it does carry.
|
|
108
|
+
- Up to 100 rows per op. Rows that differ in SHAPE (not just data) are not rows — write those as plain elements.
|
|
109
|
+
|
|
110
|
+
**A list inside each row** — a timeline of days each with its own steps, a menu of sections each with its dishes —
|
|
111
|
+
is one op too: give the node that should wrap the sub-list a \`repeat\`, and put the sub-rows in the row's data.
|
|
112
|
+
|
|
113
|
+
\`\`\`json
|
|
114
|
+
{ "type": "repeatElement", "pageRef": "render", "ref": "timeline", "style": { "base": ["tl"] },
|
|
115
|
+
"template": {
|
|
116
|
+
"ref": "day", "type": "container", "style": { "base": ["day"] },
|
|
117
|
+
"children": [
|
|
118
|
+
{ "ref": "title", "type": "heading", "subType": "h3", "props": { "content": "{{item.park}}" } },
|
|
119
|
+
{ "ref": "body", "type": "container", "style": { "base": ["body"] },
|
|
120
|
+
"repeat": { "items": "{{item.blocks}}", "template": {
|
|
121
|
+
"ref": "blk", "type": "container", "style": { "base": ["blk"] },
|
|
122
|
+
"children": [
|
|
123
|
+
{ "ref": "at", "type": "paragraph", "props": { "content": "{{item.time}}" } },
|
|
124
|
+
{ "ref": "what", "type": "paragraph", "props": { "content": "{{item.text}}" } }
|
|
125
|
+
]
|
|
126
|
+
} } }
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
"items": [
|
|
130
|
+
{ "park": "Magic Kingdom", "blocks": [ { "time": "08:00", "text": "Rope drop." }, { "time": "10:30", "text": "Space Mountain." } ] },
|
|
131
|
+
{ "park": "EPCOT", "blocks": [ { "time": "08:30", "text": "Cosmic Rewind." } ] }
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
\`\`\`
|
|
135
|
+
- The node carrying \`repeat\` becomes the WRAPPER of its sub-list (its own \`children\` are ignored), so put the
|
|
136
|
+
sub-list's layout class on it.
|
|
137
|
+
- Inside the sub-template \`{{item.…}}\` reads the SUB-row; a field of the outer row is not reachable from there,
|
|
138
|
+
so repeat it in each sub-row if you need it.
|
|
139
|
+
- Refs number both levels, outer first: \`blk-2-3\` is the third block of the second day.
|
|
140
|
+
- **One level of nesting**: the sub-template is a plain element tree, so it cannot carry another \`repeat\`.
|
|
141
|
+
- The whole op is capped at 500 rows across both levels.
|
|
142
|
+
|
|
143
|
+
## Style with reusable classes — upsertDefinitions
|
|
62
144
|
|
|
63
|
-
Styling is separate from structure: declare
|
|
145
|
+
Styling is separate from structure: declare the classes, then attach them by ref. **Declare them all in ONE
|
|
146
|
+
\`upsertDefinitions\`**, keyed by class name — a widget usually needs a dozen classes, and one op per class spends a
|
|
147
|
+
noticeable slice of the call on repeated \`{"type":"upsertDefinition","ref":…}\` envelopes.
|
|
64
148
|
|
|
65
149
|
\`\`\`json
|
|
66
|
-
{ "type": "
|
|
150
|
+
{ "type": "upsertDefinitions", "definitions": {
|
|
151
|
+
"card": { "desktop": { "display": "flex", "flex-direction": "column", "gap": "8px", "padding": "16px", "border-radius": "12px" } },
|
|
152
|
+
"title": { "desktop": { "font-size": "16px", "font-weight": "600", "margin-top": "0", "margin-bottom": "0" } }
|
|
153
|
+
} }
|
|
67
154
|
\`\`\`
|
|
155
|
+
Each value is exactly what \`upsertDefinition\` takes minus \`type\`/\`ref\` (\`desktop\`/\`tablet\`/\`mobile\`, \`states\`,
|
|
156
|
+
\`variants\`, \`slots\`), and the result is identical. The single \`upsertDefinition\` still exists for a one-off class,
|
|
157
|
+
and \`patchDefinition\` still changes only some CSS of one class.
|
|
68
158
|
- CSS properties in **kebab-case** (\`background-color\`, \`font-size\`, \`border-radius\`), values as plain strings.
|
|
69
159
|
- Attach to an element via \`style: { "base": ["card"] }\`. Stack classes: \`"base": ["card", "shadow"]\`.
|
|
70
160
|
- One \`ref\` can name both an element and its class (as above) — they live in different namespaces.
|
|
71
161
|
- Lay containers out with flexbox or grid — pick the direction on purpose, see **Fit the panel** below.
|
|
72
162
|
- **You are not styling from zero.** Each type lands on the page with CSS you did not write, and it is the usual
|
|
73
|
-
reason a widget does not look like the definitions say
|
|
74
|
-
\`border: 0 solid\`, \`body\` margin), so anything you leave unset comes from one of two places:
|
|
163
|
+
reason a widget does not look like the definitions say:
|
|
75
164
|
- The per-type rule the SDK ships — the one that changes layouts is \`container\`, which carries
|
|
76
165
|
\`min-width: 50px; min-height: 50px\`. A rail, a divider, a dot, a spacer or a narrow cell will NOT go below
|
|
77
166
|
50px until you say \`"min-width": "0"\` (and/or \`"min-height": "0"\`) on it: a 2px timeline line renders 50px
|
|
@@ -85,9 +174,10 @@ Styling is separate from structure: declare a class, then attach it by ref.
|
|
|
85
174
|
builder — it does not paint here, so do not count on it in a widget.
|
|
86
175
|
- **Mind the intrinsic display.** Some types start non-block: \`text\` is \`display: inline\`, so to stack or size it,
|
|
87
176
|
wrap it in a \`container\` (or set \`display: block\`). \`heading\` and \`paragraph\` are already block.
|
|
88
|
-
- **
|
|
89
|
-
\`
|
|
90
|
-
|
|
177
|
+
- **Shorthands are welcome** and cheaper to write: \`padding: 8px 16px\`, \`border: 1px solid #ddd\`,
|
|
178
|
+
\`font: bold 16px/1.5 Arial\`, \`flex: 1 1 auto\`, \`transition: opacity 200ms ease\`. They are stored expanded to
|
|
179
|
+
their longhands, so a breakpoint, state or variant can override each property on its own — and a read-back shows
|
|
180
|
+
the longhands, not what you wrote. An unknown property errors with the correct kebab-case key suggested.
|
|
91
181
|
- **Responsive:** add \`tablet\` and/or \`mobile\` blocks next to \`desktop\` (same shape); they override desktop on
|
|
92
182
|
smaller screens — \`{ "desktop": { "font-size": "36px" }, "mobile": { "font-size": "24px" } }\`.
|
|
93
183
|
- **Interactive states:** nest under \`states\` keyed by pseudo-class, each with its own breakpoint block —
|
|
@@ -106,14 +196,16 @@ half-empty widget to avoid. Choose the axis every time:
|
|
|
106
196
|
- **Peers side by side** — metrics, plans, options, a comparison, an image next to its text: a row, wrapping when
|
|
107
197
|
it runs out of width, children sharing it (no fixed widths).
|
|
108
198
|
\`\`\`json
|
|
109
|
-
{ "type": "
|
|
110
|
-
|
|
199
|
+
{ "type": "upsertDefinitions", "definitions": {
|
|
200
|
+
"row": { "desktop": { "display": "flex", "flex-direction": "row", "flex-wrap": "wrap", "gap": "12px", "align-items": "stretch" } },
|
|
201
|
+
"col": { "desktop": { "flex-grow": "1", "flex-basis": "0%", "min-width": "150px" } }
|
|
202
|
+
} }
|
|
111
203
|
\`\`\`
|
|
112
204
|
\`flex-grow: 1\` + \`flex-basis: 0%\` splits the row evenly; \`min-width\` is the wrap threshold — under it the item
|
|
113
205
|
drops to the next line by itself, so a narrow panel degrades gracefully with no breakpoints.
|
|
114
206
|
- **Many uniform items** — cards, tiles, a gallery: one grid line does it all.
|
|
115
207
|
\`\`\`json
|
|
116
|
-
{ "type": "
|
|
208
|
+
{ "type": "upsertDefinitions", "definitions": { "grid": { "desktop": { "display": "grid", "grid-template-columns": "repeat(auto-fit, minmax(160px, 1fr))", "gap": "12px" } } } }
|
|
117
209
|
\`\`\`
|
|
118
210
|
- **Label + value pairs** stay on one line (\`display: flex\`, \`justify-content: space-between\`) instead of two.
|
|
119
211
|
- **Vertical is right** for reading order: a heading over its paragraph, a form, a step list, long prose.
|
|
@@ -133,12 +225,12 @@ The host publishes its palette as CSS variables on the page, so use them for eve
|
|
|
133
225
|
\`light-dark(<light>, <dark>)\` fallback for hosts that send none:
|
|
134
226
|
|
|
135
227
|
\`\`\`json
|
|
136
|
-
{ "type": "
|
|
228
|
+
{ "type": "upsertDefinitions", "definitions": { "card": { "desktop": {
|
|
137
229
|
"background-color": "var(--color-background-secondary, light-dark(#ffffff, #1f2430))",
|
|
138
230
|
"color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))",
|
|
139
231
|
"border-width": "1px", "border-style": "solid",
|
|
140
232
|
"border-color": "var(--color-border-primary, light-dark(#e2e8f0, #333a48))"
|
|
141
|
-
} }
|
|
233
|
+
} } } }
|
|
142
234
|
\`\`\`
|
|
143
235
|
|
|
144
236
|
- Surfaces: \`--color-background-primary\` (the page), \`--color-background-secondary\` / \`--color-background-tertiary\`
|
|
@@ -173,6 +265,19 @@ that is not here (lists, tabs, dialogs, forms, icons…).
|
|
|
173
265
|
\`image\`/\`video\` \`src\` accepts any \`https\` URL, or a \`data:\`/\`blob:\` URI for a fully self-contained graphic
|
|
174
266
|
(e.g. an inline SVG icon or a base64 image) — both render with no extra setup.
|
|
175
267
|
|
|
268
|
+
## Keep the call small
|
|
269
|
+
|
|
270
|
+
Everything in \`operations\` is text you write, and a widget that takes two calls because the first ran long is a
|
|
271
|
+
widget the user waits twice for. Two habits pay for themselves:
|
|
272
|
+
|
|
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).
|
|
277
|
+
- **One class per look, not per property.** Classes like \`tone-blue\` + \`tone-blue-text\` + \`tone-blue-bg\` for the
|
|
278
|
+
same card triple the declarations and the attachments. Put everything the look needs in one class, add a second
|
|
279
|
+
only for the part that genuinely varies between siblings (a colour), and reuse it — the whole point of a class.
|
|
280
|
+
|
|
176
281
|
## Full worked example — two plans side by side
|
|
177
282
|
|
|
178
283
|
The cards sit in a wrapping row and split it evenly, so the widget uses the panel's width and stays short; on a
|
|
@@ -181,10 +286,12 @@ narrow panel \`min-width\` drops the second card under the first on its own.
|
|
|
181
286
|
\`\`\`json
|
|
182
287
|
{
|
|
183
288
|
"operations": [
|
|
184
|
-
{ "type": "
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
289
|
+
{ "type": "upsertDefinitions", "definitions": {
|
|
290
|
+
"plans": { "desktop": { "display": "flex", "flex-direction": "row", "flex-wrap": "wrap", "gap": "12px", "align-items": "stretch" } },
|
|
291
|
+
"card": { "desktop": { "display": "flex", "flex-direction": "column", "gap": "6px", "flex-grow": "1", "flex-basis": "0%", "min-width": "150px", "padding": "16px", "background-color": "var(--color-background-secondary, light-dark(#ffffff, #1f2430))", "color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))", "border-width": "1px", "border-style": "solid", "border-color": "var(--color-border-primary, light-dark(#e2e8f0, #333a48))", "border-radius": "var(--border-radius-lg, 12px)", "text-align": "center" } },
|
|
292
|
+
"price": { "desktop": { "font-size": "28px", "font-weight": "800", "color": "#3b82f6" } },
|
|
293
|
+
"cta": { "desktop": { "background-color": "#3b82f6", "color": "#ffffff", "padding": "10px 16px", "border-radius": "8px", "font-weight": "600" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } }
|
|
294
|
+
} },
|
|
188
295
|
{ "type": "upsertElement", "pageRef": "render", "element": {
|
|
189
296
|
"ref": "plans", "type": "container", "style": { "base": ["plans"] },
|
|
190
297
|
"children": [
|
|
@@ -43,7 +43,7 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
|
|
|
43
43
|
const getSpace = () => spacePromise ??= loadSpace();
|
|
44
44
|
const server = new McpServer({
|
|
45
45
|
name: "plitzi-mcp",
|
|
46
|
-
version: "0.32.
|
|
46
|
+
version: "0.32.18"
|
|
47
47
|
}, { instructions: serverInstructions });
|
|
48
48
|
registerResources(server, getSpace, MCP_ENV, log);
|
|
49
49
|
registerApps(server);
|
|
@@ -19,6 +19,7 @@ import { upsertVariable } from "../operations/schema/variables/upsertVariable.js
|
|
|
19
19
|
import { deleteDefinition } from "../operations/style/definitions/deleteDefinition.js";
|
|
20
20
|
import { patchDefinition } from "../operations/style/definitions/patchDefinition.js";
|
|
21
21
|
import { upsertDefinition } from "../operations/style/definitions/upsertDefinition.js";
|
|
22
|
+
import { upsertDefinitions } from "../operations/style/definitions/upsertDefinitions.js";
|
|
22
23
|
import { deleteGlobalStyle } from "../operations/style/globalStyles/deleteGlobalStyle.js";
|
|
23
24
|
import { patchGlobalStyle } from "../operations/style/globalStyles/patchGlobalStyle.js";
|
|
24
25
|
import { upsertGlobalStyle } from "../operations/style/globalStyles/upsertGlobalStyle.js";
|
|
@@ -49,6 +50,7 @@ var executeOp = (space, env, op) => {
|
|
|
49
50
|
case "deleteInteraction": return deleteInteraction(space, env, op);
|
|
50
51
|
case "patchSettings": return patchSettings(space, env, op);
|
|
51
52
|
case "upsertDefinition": return upsertDefinition(space, env, op);
|
|
53
|
+
case "upsertDefinitions": return upsertDefinitions(space, env, op);
|
|
52
54
|
case "patchDefinition": return patchDefinition(space, env, op);
|
|
53
55
|
case "deleteDefinition": return deleteDefinition(space, env, op);
|
|
54
56
|
case "upsertGlobalStyle": return upsertGlobalStyle(space, env, op);
|
|
@@ -2,6 +2,7 @@ import { cloneSpace } from "../../helpers/space.js";
|
|
|
2
2
|
import { environment, operations } from "../operations/index.js";
|
|
3
3
|
import { applyOperations } from "./dispatch.js";
|
|
4
4
|
import { changedResources, conflictMessage, detectConflicts, resolvedElements } from "./writeResult.js";
|
|
5
|
+
import { expandOperations } from "../shared/expandOperations.js";
|
|
5
6
|
import { defineTool } from "../shared/tool.js";
|
|
6
7
|
import { validateOperations } from "../shared/validator/index.js";
|
|
7
8
|
import { auditResources } from "../shared/validator/audit.js";
|
|
@@ -22,7 +23,20 @@ var schemaErrorToValidation = (error) => ({
|
|
|
22
23
|
});
|
|
23
24
|
var apply = async (input, space, persisters) => {
|
|
24
25
|
const env = input.environment ?? "main";
|
|
25
|
-
const
|
|
26
|
+
const expansion = expandOperations(input.operations);
|
|
27
|
+
if (expansion.errors.length > 0) return {
|
|
28
|
+
applied: false,
|
|
29
|
+
persisted: false,
|
|
30
|
+
summary: {
|
|
31
|
+
created: 0,
|
|
32
|
+
updated: 0,
|
|
33
|
+
deleted: 0
|
|
34
|
+
},
|
|
35
|
+
changed: [],
|
|
36
|
+
errors: expansion.errors
|
|
37
|
+
};
|
|
38
|
+
const ops = expansion.operations;
|
|
39
|
+
const validation = validateOperations(space, ops);
|
|
26
40
|
if (!validation.valid) return {
|
|
27
41
|
applied: false,
|
|
28
42
|
persisted: false,
|
|
@@ -51,7 +65,7 @@ var apply = async (input, space, persisters) => {
|
|
|
51
65
|
}
|
|
52
66
|
};
|
|
53
67
|
const draft = cloneSpace(space);
|
|
54
|
-
const outcome = applyOperations(draft, env,
|
|
68
|
+
const outcome = applyOperations(draft, env, ops);
|
|
55
69
|
if (outcome.errors.length > 0) return {
|
|
56
70
|
applied: false,
|
|
57
71
|
persisted: false,
|
|
@@ -77,7 +91,7 @@ var apply = async (input, space, persisters) => {
|
|
|
77
91
|
errors: integrity.errors.map(schemaErrorToValidation),
|
|
78
92
|
warnings: noWarnings(validation.warnings)
|
|
79
93
|
};
|
|
80
|
-
const audit = auditResources(draft,
|
|
94
|
+
const audit = auditResources(draft, ops);
|
|
81
95
|
const warnings = [...validation.warnings, ...audit.warnings];
|
|
82
96
|
if (audit.errors.length > 0) return {
|
|
83
97
|
applied: false,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { elementOps } from "./schema/operations.js";
|
|
2
|
+
import { registerSharedSchemaIds } from "./schemaIds.js";
|
|
2
3
|
import { styleOps } from "./style/operations.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
//#region src/modules/mcp/tools/operations/index.ts
|
|
6
|
+
registerSharedSchemaIds();
|
|
5
7
|
var operation = z.discriminatedUnion("type", [
|
|
6
8
|
elementOps.upsertElement,
|
|
9
|
+
elementOps.repeatElement,
|
|
7
10
|
elementOps.patchElement,
|
|
8
11
|
elementOps.deleteElement,
|
|
9
12
|
elementOps.moveElement,
|
|
@@ -21,6 +24,7 @@ var operation = z.discriminatedUnion("type", [
|
|
|
21
24
|
elementOps.deleteInteraction,
|
|
22
25
|
elementOps.patchSettings,
|
|
23
26
|
styleOps.upsertDefinition,
|
|
27
|
+
styleOps.upsertDefinitions,
|
|
24
28
|
styleOps.patchDefinition,
|
|
25
29
|
styleOps.deleteDefinition,
|
|
26
30
|
styleOps.upsertGlobalStyle,
|
|
@@ -10,7 +10,7 @@ var patchElementOp = z.object({
|
|
|
10
10
|
type: z.literal("patchElement"),
|
|
11
11
|
pageRef: z.string().describe("Page ref or id"),
|
|
12
12
|
ref: z.string().describe("Existing element ref or id"),
|
|
13
|
-
idRef: z.string().optional().describe("Assign or rename
|
|
13
|
+
idRef: z.string().optional().describe("Assign or rename this element idRef ([A-Za-z0-9_-], starting with a letter, unique in the space). Without one an element publishes no data source, so it is not bindable. A rename moves the source name with it: every binding and interaction that targeted the old one is repointed for you."),
|
|
14
14
|
label: z.string().optional(),
|
|
15
15
|
subType: z.string().optional(),
|
|
16
16
|
props: z.record(z.string(), z.unknown()).optional().describe("Merged onto existing props: listed keys change, null unsets a key, others are preserved"),
|