@plitzi/sdk-server 0.32.16 → 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 +20 -0
- package/dist/helpers/serverLog.js +3 -3
- package/dist/modules/mcp/apps/render/view.tsx +107 -3
- package/dist/modules/mcp/apps/shared/shell.ejs +19 -2
- package/dist/modules/mcp/helpers/guide.js +9 -2
- package/dist/modules/mcp/helpers/space.js +1 -1
- package/dist/modules/mcp/resources/renderGuide.js +205 -18
- 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/modules/oauth/metadata.js +11 -4
- package/dist/src/helpers/serverLog.d.ts +1 -1
- 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/helpers/space.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/dist/src/modules/oauth/metadata.d.ts +11 -4
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @plitzi/sdk-server
|
|
2
2
|
|
|
3
|
+
## 0.32.18
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- v0.32.18
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @plitzi/plitzi-sdk@0.32.18
|
|
10
|
+
- @plitzi/sdk-schema@0.32.18
|
|
11
|
+
- @plitzi/sdk-shared@0.32.18
|
|
12
|
+
|
|
13
|
+
## 0.32.17
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- v0.32.17
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
- @plitzi/plitzi-sdk@0.32.17
|
|
20
|
+
- @plitzi/sdk-schema@0.32.17
|
|
21
|
+
- @plitzi/sdk-shared@0.32.17
|
|
22
|
+
|
|
3
23
|
## 0.32.16
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
|
@@ -8,12 +8,12 @@ var renderRequest = (event) => {
|
|
|
8
8
|
};
|
|
9
9
|
var renderTool = (event) => {
|
|
10
10
|
const args = event.argsSummary ? ` ${event.argsSummary}` : "";
|
|
11
|
-
return `[
|
|
11
|
+
return `[MCP] tools/call ${event.name}${args} ${Math.round(event.durationMs)}ms ${outcomeOf(event)}`;
|
|
12
12
|
};
|
|
13
|
-
var renderResource = (event) => `[
|
|
13
|
+
var renderResource = (event) => `[MCP] resources/read ${event.name} ${Math.round(event.durationMs)}ms ${outcomeOf(event)}`;
|
|
14
14
|
/** One line for any {@link ServerLogEvent}: an HTTP request reads as an access-log line
|
|
15
15
|
* (`[SSR] 203.0.113.7 GET /pricing 200 12ms ok`), the MCP events as what happened inside one
|
|
16
|
-
* (`[
|
|
16
|
+
* (`[MCP] tools/call plitzi_apply {operations:[3]} 41ms ok`). Rendering is a pure format — the dispatcher
|
|
17
17
|
* already stripped query values, collected no headers, cookies or tokens and summarised tool args by shape;
|
|
18
18
|
* the client IP it does carry is personal data, so a sink that persists these lines must say so. */
|
|
19
19
|
var renderLogEvent = (event) => {
|
|
@@ -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 = () => ({});
|
|
@@ -10,17 +10,34 @@
|
|
|
10
10
|
<title><%= title %></title>
|
|
11
11
|
|
|
12
12
|
<style>
|
|
13
|
+
/*
|
|
14
|
+
The page inherits the HOST's theme, which may be dark: nothing here paints a surface, so an app that sets no
|
|
15
|
+
colours would otherwise draw the browser's black default text over the host's own background — unreadable in
|
|
16
|
+
a dark chat. `light-dark()` picks the right side of every pair; `color-scheme` is what tells it which side,
|
|
17
|
+
and it follows the OS until useHostStyles replaces it with the theme the host actually reported (that hook
|
|
18
|
+
also fills the --color-* variables in, so the var() fallbacks below only apply on a host that sends none).
|
|
19
|
+
*/
|
|
20
|
+
html {
|
|
21
|
+
color-scheme: light dark;
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
html,
|
|
14
25
|
body {
|
|
15
26
|
margin: 0;
|
|
16
27
|
}
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
background-color: transparent;
|
|
31
|
+
color: var(--color-text-primary, light-dark(#0f172a, #e8eaed));
|
|
32
|
+
font-family: var(--font-sans, system-ui, sans-serif);
|
|
33
|
+
}
|
|
17
34
|
/* The app renders nothing until the host pushes the tool result in. */
|
|
18
35
|
#app:empty::after {
|
|
19
36
|
content: 'Rendering…';
|
|
20
37
|
display: block;
|
|
21
38
|
padding: 16px;
|
|
22
|
-
font: 14px
|
|
23
|
-
color: #64748b;
|
|
39
|
+
font-size: 14px;
|
|
40
|
+
color: var(--color-text-tertiary, light-dark(#64748b, #9aa4b2));
|
|
24
41
|
}
|
|
25
42
|
</style>
|
|
26
43
|
<style>
|
|
@@ -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":
|
|
@@ -280,7 +280,7 @@ 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
|
-
var unauthorizedSpaceMessage = "This
|
|
283
|
+
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.";
|
|
284
284
|
var generateObjectId = () => {
|
|
285
285
|
return `${Math.floor(Date.now() / 1e3).toString(16).padStart(8, "0")}${Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join("")}`;
|
|
286
286
|
};
|
|
@@ -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,28 +76,172 @@ 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
|
|
62
80
|
|
|
63
|
-
|
|
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\`:
|
|
64
84
|
|
|
65
85
|
\`\`\`json
|
|
66
|
-
{ "type": "
|
|
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
|
+
}
|
|
67
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
|
|
144
|
+
|
|
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.
|
|
148
|
+
|
|
149
|
+
\`\`\`json
|
|
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
|
+
} }
|
|
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
|
-
- Lay containers out with flexbox
|
|
161
|
+
- Lay containers out with flexbox or grid — pick the direction on purpose, see **Fit the panel** below.
|
|
162
|
+
- **You are not styling from zero.** Each type lands on the page with CSS you did not write, and it is the usual
|
|
163
|
+
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.
|
|
168
|
+
- The BROWSER's own defaults for the rest: \`heading\` keeps its UA font-size and ~0.67em top/bottom margins,
|
|
169
|
+
\`paragraph\` ~1em margins, \`list\` a 40px \`padding-left\`, \`button\` its native chrome, \`image\` its intrinsic
|
|
170
|
+
size, \`link\` its own colour and underline. In a compact widget set these explicitly — usually
|
|
171
|
+
\`"margin-top": "0"\`, \`"margin-bottom": "0"\` — and space things with the parent's \`gap\` instead.
|
|
172
|
+
- Borders start at \`0 solid\`, so \`border-color\` alone paints nothing: give \`border-width\` (and the colour).
|
|
173
|
+
- The \`defaultStyle\` a type reports in the EDITING catalog (\`plitzi://types\`) is authoring metadata for the
|
|
174
|
+
builder — it does not paint here, so do not count on it in a widget.
|
|
72
175
|
- **Mind the intrinsic display.** Some types start non-block: \`text\` is \`display: inline\`, so to stack or size it,
|
|
73
176
|
wrap it in a \`container\` (or set \`display: block\`). \`heading\` and \`paragraph\` are already block.
|
|
74
|
-
- **
|
|
75
|
-
\`
|
|
76
|
-
|
|
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.
|
|
77
181
|
- **Responsive:** add \`tablet\` and/or \`mobile\` blocks next to \`desktop\` (same shape); they override desktop on
|
|
78
182
|
smaller screens — \`{ "desktop": { "font-size": "36px" }, "mobile": { "font-size": "24px" } }\`.
|
|
79
183
|
- **Interactive states:** nest under \`states\` keyed by pseudo-class, each with its own breakpoint block —
|
|
80
184
|
\`{ "desktop": { "background-color": "#3b82f6" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } }\`
|
|
81
185
|
(\`hover\`, \`active\`, \`focus\`).
|
|
82
186
|
|
|
187
|
+
## Fit the panel — go wide, stay short
|
|
188
|
+
|
|
189
|
+
The widget renders in a **side panel** (Claude Desktop, ChatGPT, the Plitzi builder), so it gets a usable width but
|
|
190
|
+
very little height: everything past the first screenful costs the user a scroll. Height is the scarce resource —
|
|
191
|
+
spend width instead.
|
|
192
|
+
|
|
193
|
+
Plain containers are blocks, so doing nothing stacks children **vertically** and produces exactly the tall,
|
|
194
|
+
half-empty widget to avoid. Choose the axis every time:
|
|
195
|
+
|
|
196
|
+
- **Peers side by side** — metrics, plans, options, a comparison, an image next to its text: a row, wrapping when
|
|
197
|
+
it runs out of width, children sharing it (no fixed widths).
|
|
198
|
+
\`\`\`json
|
|
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
|
+
} }
|
|
203
|
+
\`\`\`
|
|
204
|
+
\`flex-grow: 1\` + \`flex-basis: 0%\` splits the row evenly; \`min-width\` is the wrap threshold — under it the item
|
|
205
|
+
drops to the next line by itself, so a narrow panel degrades gracefully with no breakpoints.
|
|
206
|
+
- **Many uniform items** — cards, tiles, a gallery: one grid line does it all.
|
|
207
|
+
\`\`\`json
|
|
208
|
+
{ "type": "upsertDefinitions", "definitions": { "grid": { "desktop": { "display": "grid", "grid-template-columns": "repeat(auto-fit, minmax(160px, 1fr))", "gap": "12px" } } } }
|
|
209
|
+
\`\`\`
|
|
210
|
+
- **Label + value pairs** stay on one line (\`display: flex\`, \`justify-content: space-between\`) instead of two.
|
|
211
|
+
- **Vertical is right** for reading order: a heading over its paragraph, a form, a step list, long prose.
|
|
212
|
+
- Force a stack on tiny screens with the \`mobile\` block: \`"mobile": { "flex-direction": "column" }\`.
|
|
213
|
+
|
|
214
|
+
Keep it compact, and the numbers low: \`padding\` 12–16px (24+ only on a single hero card), \`gap\` 8–12px,
|
|
215
|
+
\`font-size\` 13–15px for body and 16–20px for headings. Let the outer container **fill** the panel — no \`width\` on
|
|
216
|
+
it — and reach for \`max-width\` only to stop one lone card from stretching across the whole panel.
|
|
217
|
+
|
|
218
|
+
## Match the host theme — never hardcode a light palette
|
|
219
|
+
|
|
220
|
+
The widget is embedded in the host's own UI (Claude Desktop, ChatGPT, the builder), and that UI **may be in dark
|
|
221
|
+
mode**. A widget painted with fixed light colours is the most common way to ship something unusable: a white card
|
|
222
|
+
in a dark chat glares, and — worse — text left at a dark default disappears against the host's dark background.
|
|
223
|
+
|
|
224
|
+
The host publishes its palette as CSS variables on the page, so use them for every colour, with a
|
|
225
|
+
\`light-dark(<light>, <dark>)\` fallback for hosts that send none:
|
|
226
|
+
|
|
227
|
+
\`\`\`json
|
|
228
|
+
{ "type": "upsertDefinitions", "definitions": { "card": { "desktop": {
|
|
229
|
+
"background-color": "var(--color-background-secondary, light-dark(#ffffff, #1f2430))",
|
|
230
|
+
"color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))",
|
|
231
|
+
"border-width": "1px", "border-style": "solid",
|
|
232
|
+
"border-color": "var(--color-border-primary, light-dark(#e2e8f0, #333a48))"
|
|
233
|
+
} } } }
|
|
234
|
+
\`\`\`
|
|
235
|
+
|
|
236
|
+
- Surfaces: \`--color-background-primary\` (the page), \`--color-background-secondary\` / \`--color-background-tertiary\`
|
|
237
|
+
(cards, raised areas). Text: \`--color-text-primary\`, \`--color-text-secondary\`, \`--color-text-tertiary\` (muted).
|
|
238
|
+
Borders: \`--color-border-primary\` / \`--color-border-secondary\`. Status pairs: \`--color-background-danger\` |
|
|
239
|
+
\`success\` | \`warning\` | \`info\` with the matching \`--color-text-…\`. Also \`--font-sans\`, \`--border-radius-md\` /
|
|
240
|
+
\`lg\` / \`full\`, \`--shadow-sm\` / \`md\`.
|
|
241
|
+
- **Set \`color\` wherever you set \`background-color\`** — the pair is what stays legible, either one alone is a
|
|
242
|
+
gamble. Same rule for a brand accent: a CTA on your own blue must state its own text colour (\`#ffffff\`).
|
|
243
|
+
- Prefer a border to a drop shadow for separation: shadows all but vanish on a dark surface.
|
|
244
|
+
|
|
83
245
|
## Element types (type → what to set)
|
|
84
246
|
|
|
85
247
|
| type | renders | set |
|
|
@@ -103,21 +265,46 @@ that is not here (lists, tabs, dialogs, forms, icons…).
|
|
|
103
265
|
\`image\`/\`video\` \`src\` accepts any \`https\` URL, or a \`data:\`/\`blob:\` URI for a fully self-contained graphic
|
|
104
266
|
(e.g. an inline SVG icon or a base64 image) — both render with no extra setup.
|
|
105
267
|
|
|
106
|
-
##
|
|
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
|
+
|
|
281
|
+
## Full worked example — two plans side by side
|
|
282
|
+
|
|
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
|
|
284
|
+
narrow panel \`min-width\` drops the second card under the first on its own.
|
|
107
285
|
|
|
108
286
|
\`\`\`json
|
|
109
287
|
{
|
|
110
288
|
"operations": [
|
|
111
|
-
{ "type": "
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
} },
|
|
114
295
|
{ "type": "upsertElement", "pageRef": "render", "element": {
|
|
115
|
-
"ref": "
|
|
296
|
+
"ref": "plans", "type": "container", "style": { "base": ["plans"] },
|
|
116
297
|
"children": [
|
|
117
|
-
{ "ref": "
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
298
|
+
{ "ref": "free", "type": "container", "style": { "base": ["card"] }, "children": [
|
|
299
|
+
{ "ref": "free-plan", "type": "heading", "subType": "h3", "props": { "content": "Starter" } },
|
|
300
|
+
{ "ref": "free-amount", "type": "text", "props": { "content": "$0" }, "style": { "base": ["price"] } },
|
|
301
|
+
{ "ref": "free-buy", "type": "button", "props": { "content": "Start free" }, "style": { "base": ["cta"] } }
|
|
302
|
+
] },
|
|
303
|
+
{ "ref": "pro", "type": "container", "style": { "base": ["card"] }, "children": [
|
|
304
|
+
{ "ref": "pro-plan", "type": "heading", "subType": "h3", "props": { "content": "Pro" } },
|
|
305
|
+
{ "ref": "pro-amount", "type": "text", "props": { "content": "$29/mo" }, "style": { "base": ["price"] } },
|
|
306
|
+
{ "ref": "pro-buy", "type": "button", "props": { "content": "Start free trial" }, "style": { "base": ["cta"] } }
|
|
307
|
+
] }
|
|
121
308
|
]
|
|
122
309
|
} }
|
|
123
310
|
]
|
|
@@ -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,
|