@plitzi/sdk-server 0.32.17 → 0.32.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +18 -0
  3. package/dist/modules/mcp/apps/render/index.js +2 -1
  4. package/dist/modules/mcp/apps/render/view/heldBatch.ts +107 -0
  5. package/dist/modules/mcp/apps/render/view/index.tsx +212 -0
  6. package/dist/modules/mcp/apps/shared/assets.js +10 -0
  7. package/dist/modules/mcp/apps/shared/bundle.js +15 -14
  8. package/dist/modules/mcp/handler.js +1 -1
  9. package/dist/modules/mcp/helpers/guide.js +11 -3
  10. package/dist/modules/mcp/helpers/space.js +13 -1
  11. package/dist/modules/mcp/resources/register.js +8 -3
  12. package/dist/modules/mcp/resources/renderGuide.js +138 -24
  13. package/dist/modules/mcp/resources/router.js +13 -1
  14. package/dist/modules/mcp/server.js +31 -19
  15. package/dist/modules/mcp/tools/apply/dispatch.js +2 -0
  16. package/dist/modules/mcp/tools/apply/index.js +17 -3
  17. package/dist/modules/mcp/tools/operations/index.js +4 -0
  18. package/dist/modules/mcp/tools/operations/schema/elements/patchElement.js +1 -1
  19. package/dist/modules/mcp/tools/operations/schema/elements/repeatElement.js +148 -0
  20. package/dist/modules/mcp/tools/operations/schema/interactions/deleteInteraction.js +1 -1
  21. package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.js +1 -1
  22. package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.js +1 -1
  23. package/dist/modules/mcp/tools/operations/schema/operations.js +2 -0
  24. package/dist/modules/mcp/tools/operations/schema/pages/upsertPage.js +1 -1
  25. package/dist/modules/mcp/tools/operations/schema/shared.js +8 -5
  26. package/dist/modules/mcp/tools/operations/schemaIds.js +54 -0
  27. package/dist/modules/mcp/tools/operations/style/definitions/patchDefinition.js +1 -1
  28. package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinition.js +1 -1
  29. package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinitions.js +40 -0
  30. package/dist/modules/mcp/tools/operations/style/globalStyles/patchGlobalStyle.js +1 -1
  31. package/dist/modules/mcp/tools/operations/style/globalStyles/upsertGlobalStyle.js +1 -1
  32. package/dist/modules/mcp/tools/operations/style/idStyles/patchIdStyle.js +1 -1
  33. package/dist/modules/mcp/tools/operations/style/idStyles/upsertIdStyle.js +1 -1
  34. package/dist/modules/mcp/tools/operations/style/operations.js +2 -0
  35. package/dist/modules/mcp/tools/operations/style/shared.js +1 -1
  36. package/dist/modules/mcp/tools/read.js +21 -3
  37. package/dist/modules/mcp/tools/render.js +70 -8
  38. package/dist/modules/mcp/tools/shared/expandOperations.js +44 -0
  39. package/dist/modules/mcp/tools/shared/tool.js +16 -11
  40. package/dist/modules/mcp/tools/shared/validator/audit.js +3 -0
  41. package/dist/modules/mcp/tools/shared/validator/batch.js +13 -4
  42. package/dist/modules/mcp/tools/shared/validator/index.js +15 -0
  43. package/dist/modules/mcp/tools/validate.js +11 -3
  44. package/dist/src/modules/mcp/apps/shared/assets.d.ts +10 -0
  45. package/dist/src/modules/mcp/apps/shared/bundle.d.ts +3 -0
  46. package/dist/src/modules/mcp/apps/shared/index.d.ts +1 -0
  47. package/dist/src/modules/mcp/e2e/index.d.ts +2 -2
  48. package/dist/src/modules/mcp/e2e/mcpEndpoint.d.ts +6 -1
  49. package/dist/src/modules/mcp/e2e/renderingHost.d.ts +16 -1
  50. package/dist/src/modules/mcp/helpers/guide.d.ts +2 -1
  51. package/dist/src/modules/mcp/helpers/space.d.ts +9 -0
  52. package/dist/src/modules/mcp/resources/index.d.ts +2 -1
  53. package/dist/src/modules/mcp/resources/register.d.ts +6 -2
  54. package/dist/src/modules/mcp/resources/renderGuide.d.ts +5 -0
  55. package/dist/src/modules/mcp/resources/router.d.ts +4 -0
  56. package/dist/src/modules/mcp/server.d.ts +6 -5
  57. package/dist/src/modules/mcp/tools/apply/index.d.ts +50 -0
  58. package/dist/src/modules/mcp/tools/operations/index.d.ts +100 -0
  59. package/dist/src/modules/mcp/tools/operations/schema/elements/repeatElement.d.ts +55 -0
  60. package/dist/src/modules/mcp/tools/operations/schema/index.d.ts +1 -0
  61. package/dist/src/modules/mcp/tools/operations/schema/operations.d.ts +19 -0
  62. package/dist/src/modules/mcp/tools/operations/schema/shared.d.ts +15 -0
  63. package/dist/src/modules/mcp/tools/operations/schemaIds.d.ts +6 -0
  64. package/dist/src/modules/mcp/tools/operations/style/definitions/upsertDefinitions.d.ts +41 -0
  65. package/dist/src/modules/mcp/tools/operations/style/index.d.ts +1 -0
  66. package/dist/src/modules/mcp/tools/operations/style/operations.d.ts +33 -0
  67. package/dist/src/modules/mcp/tools/operations/style/shared.d.ts +4 -3
  68. package/dist/src/modules/mcp/tools/preview.d.ts +50 -0
  69. package/dist/src/modules/mcp/tools/read.d.ts +1 -0
  70. package/dist/src/modules/mcp/tools/render.d.ts +421 -363
  71. package/dist/src/modules/mcp/tools/screenshot.d.ts +50 -0
  72. package/dist/src/modules/mcp/tools/shared/expandOperations.d.ts +13 -0
  73. package/dist/src/modules/mcp/tools/shared/tool.d.ts +8 -0
  74. package/dist/src/modules/mcp/tools/validate.d.ts +50 -0
  75. package/package.json +6 -5
  76. package/skills/plitzi-render/SKILL.md +170 -0
  77. package/dist/modules/mcp/apps/render/view.tsx +0 -108
  78. /package/dist/modules/mcp/apps/example/{view.tsx → view/index.tsx} +0 -0
  79. /package/dist/src/modules/mcp/{apps/example/view.d.ts → tests/schemaIds.test.d.ts} +0 -0
  80. /package/dist/src/modules/mcp/{apps/render/view.d.ts → tests/skill.test.d.ts} +0 -0
@@ -18,7 +18,7 @@ var patchInteractionNodeOp = z.object({
18
18
  when,
19
19
  elementId,
20
20
  preview
21
- }).describe("Partially update ONE step of an interaction flow (found by nodeId): only the fields you pass change; the flow order/links are untouched. This is how you DISABLE/deactivate a step without removing it: pass { enabled: false } (re-enable with { enabled: true }) — the step stays in the flow. Use deleteInteraction only to actually remove a step or a flow. Fails if the node does not exist.");
21
+ }).describe("Partially update ONE step of a flow (found by nodeId): only the fields you pass change, the order/links are untouched. This is how you DISABLE a step without removing it { enabled: false } keeps it in the flow. deleteInteraction is for actually removing one. Fails if the node does not exist.");
22
22
  var patchInteractionNode = (space, env, op) => {
23
23
  const found = resolveElement(space, env, op.pageRef, op.ref);
24
24
  if ("error" in found) return found.error;
@@ -11,7 +11,7 @@ var upsertInteractionFlowOp = z.object({
11
11
  ref: z.string().describe("Element ref or id"),
12
12
  flowId: z.string().optional().describe("Existing flow to replace (the trigger node id). Omit to create a new flow"),
13
13
  nodes: z.array(interactionNode).min(1).describe("Ordered steps of the flow: the FIRST must be a trigger, the rest run after it in order")
14
- }).describe("Create or replace one interaction flow on an element from an ordered list of steps. The stored beforeNode/afterNode/flowId links are computed for you — pass the steps in execution order. For a globalCallback (addNotification, setState, navigate, auth*, *CollectionRecord) OMIT elementId: it is registered on a source module, not the host element, and the MCP sets the right source and fills the default param values for you. To edit a single step in place use patchInteractionNode.");
14
+ }).describe("Create or replace one interaction flow on an element from an ordered list of steps pass them in execution order, the beforeNode/afterNode/flowId links are computed for you. For a globalCallback OMIT elementId: it is registered on a source module, not the host element, and the source and default params are filled in for you. To edit a single step in place use patchInteractionNode.");
15
15
  var upsertInteractionFlow = (space, env, op) => {
16
16
  const found = resolveElement(space, env, op.pageRef, op.ref);
17
17
  if ("error" in found) return found.error;
@@ -4,6 +4,7 @@ import { upsertBindingOp } from "./bindings/upsertBinding.js";
4
4
  import { deleteElementOp } from "./elements/deleteElement.js";
5
5
  import { moveElementOp } from "./elements/moveElement.js";
6
6
  import { patchElementOp } from "./elements/patchElement.js";
7
+ import { repeatElementOp } from "./elements/repeatElement.js";
7
8
  import { upsertElementOp } from "./elements/upsertElement.js";
8
9
  import { deleteFolderOp } from "./folders/deleteFolder.js";
9
10
  import { upsertFolderOp } from "./folders/upsertFolder.js";
@@ -18,6 +19,7 @@ import { upsertVariableOp } from "./variables/upsertVariable.js";
18
19
  //#region src/modules/mcp/tools/operations/schema/operations.ts
19
20
  var elementOps = {
20
21
  upsertElement: upsertElementOp,
22
+ repeatElement: repeatElementOp,
21
23
  patchElement: patchElementOp,
22
24
  deleteElement: deleteElementOp,
23
25
  moveElement: moveElementOp,
@@ -8,7 +8,7 @@ var upsertPageOp = z.object({
8
8
  type: z.literal("upsertPage"),
9
9
  ref: z.string().describe("Page id/slug to update, or a new id you choose to create one. On a new page it is stored as its idRef: starts with a letter, then letters, numbers, hyphens and underscores (\"pricing\"), unique across the space."),
10
10
  label: z.string().optional(),
11
- slug: z.string().optional().describe("The page URL path, RELATIVE — do NOT start it with \"/\" (the runtime and any parent folder slugs prepend the path; a leading \"/\" is stripped). Good practice: always set one on create for a clean, stable route, e.g. \"pricing\" or \"posts/:postId\" (a \":name\" segment is a route param, readable as {{name}} and as the source navigation.routeParams.name). Omitted on create → the page ref is used as the slug."),
11
+ slug: z.string().optional().describe("The page URL path, RELATIVE — never start it with \"/\" (parent folder slugs prepend it; a leading \"/\" is stripped). Set one on create for a stable route: \"pricing\", \"posts/:postId\". A \":name\" segment is a route param, readable as {{name}} and as navigation.routeParams.name. Omitted → the page ref is used."),
12
12
  folder: z.string().nullable().optional().describe("Ref of an existing folder to place this page in; \"\" or null moves it to the root. Unknown → error"),
13
13
  default: z.boolean().optional(),
14
14
  enabled: z.boolean().optional().describe("false disables the page in the published SDK runtime (not routable/accessible to end users); it stays editable here. Defaults to true")
@@ -60,14 +60,17 @@ var initialStateInput = z.object({
60
60
  styleVariant: styleVariantInput.optional().describe("Variant each attached class uses: { className: { base|slot: variantName | [names] } }"),
61
61
  visibility: z.boolean().optional().describe("Initial visibility of the element")
62
62
  });
63
- var elementInput = z.lazy(() => z.object({
64
- ref: z.string().describe("Semantic id you choose, or an existing element ref/id. On a new element this is stored as its idRef and becomes the key everything else addresses it by its data source is named `<type>_<ref>`, and an interaction targets it by this ref. Start with a letter, then letters, numbers, hyphens and underscores (\"hero-cta\" or \"food_item\"): a dot would break the `<type>_<idRef>.<field>` grammar (an underscore is fine — the first `_` separates the type from the idRef). Must be unique across the space."),
63
+ var elementShape = {
64
+ ref: z.string().describe("Semantic id you choose, or an existing element ref/id. On a new element it becomes the idRef everything addresses it by: its data source is `<type>_<ref>`, and interactions target it by this ref. Letters, digits, `-` and `_`, starting with a letter a dot would break the `<type>_<idRef>.<field>` grammar. Unique across the space."),
65
65
  type: z.string().describe("Type from plitzi://types"),
66
66
  label: z.string().optional(),
67
67
  subType: z.string().optional(),
68
68
  props: z.record(z.string(), z.unknown()).optional().describe("Full replacement on update"),
69
69
  style: styleRefs.optional().describe("Definition refs per slot; style the element by attaching a definition"),
70
- initialState: initialStateInput.optional().describe("Applied style variant(s) and initial visibility (see plitzi://guide styling)"),
70
+ initialState: initialStateInput.optional().describe("Applied style variant(s) and initial visibility (see plitzi://guide styling)")
71
+ };
72
+ var elementInput = z.lazy(() => z.object({
73
+ ...elementShape,
71
74
  children: z.array(elementInput).optional()
72
75
  }));
73
76
  var position = z.enum([
@@ -112,8 +115,8 @@ var interactionNode = z.object({
112
115
  params: z.record(z.string(), z.unknown()).optional(),
113
116
  enabled: z.boolean().optional(),
114
117
  when: ruleGroup.optional().describe("QueryBuilder RuleGroup gating this step (validated structurally)"),
115
- elementId: z.string().optional().describe("Element whose callback this step invokes; defaults to this element. Give its ref or its raw id — a raw id is normalised to the element idRef the runtime looks callbacks up by, and a target without an idRef is given one automatically."),
118
+ elementId: z.string().optional().describe("Element whose callback this step invokes; defaults to this element. Its ref or raw id — a raw id is normalised to the idRef the runtime looks callbacks up by, and a target without one is given it."),
116
119
  preview: z.record(z.string(), z.unknown()).optional()
117
120
  });
118
121
  //#endregion
119
- export { bindingCategory, bindingInput, elementInput, initialStateInput, interactionNode, interactionNodeType, position, ruleGroup, scalar, styleRefs, styleVariantInput };
122
+ export { bindingCategory, bindingInput, elementInput, elementShape, initialStateInput, interactionNode, interactionNodeType, position, ruleGroup, scalar, styleRefs, styleVariantInput };
@@ -0,0 +1,54 @@
1
+ import { bindingInput, elementInput, initialStateInput, interactionNode, position, ruleGroup, styleRefs, styleVariantInput } from "./schema/shared.js";
2
+ import { cssMap, cssPatchMap, definitionSlot, definitionSlotPatch, displayModeCss, displayModeCssPatch, themeValue } from "./style/shared.js";
3
+ import { z } from "zod";
4
+ //#region src/modules/mcp/tools/operations/schemaIds.ts
5
+ /** The op union is the tool input of plitzi_apply, plitzi_validate, plitzi_render AND plitzi_preview, and every
6
+ * host reads it as JSON Schema on tools/list — four copies of the same 30 operations in the model's context, on
7
+ * EVERY request of every conversation the server is connected to. It measured ~25k tokens PER TOOL (~100k in
8
+ * total), which dwarfs anything a widget payload ever costs.
9
+ *
10
+ * Most of that weight is one subschema pasted over and over: an element tree appears in upsertElement,
11
+ * patchElement and both repeat templates; a breakpoint CSS block in every style op; a rule group in every binding
12
+ * and interaction step. Zod emits a named `definitions` entry (and `$ref`s to it) for any schema carrying an `id`
13
+ * in the global registry, so registering the shared ones here collapses those copies — without touching the ops,
14
+ * the tools, or the MCP SDK, whose own converter honours the registry (it is given no options otherwise).
15
+ *
16
+ * An id only pays when the schema is REUSED: giving one to a subschema that appears once makes the listing bigger,
17
+ * because the `definitions` entry and the ref cost more than the single copy they replace (measured, and reverted).
18
+ *
19
+ * The ids are the names the MODEL reads in the refs, so they are written for it: `Element`, not `ElementInput`.
20
+ * This runs once, at module load of the op vocabulary, so every conversion path benefits — the MCP tools list and
21
+ * the co-worker's own tool-schema converter alike. */
22
+ var SHARED_SCHEMAS = [
23
+ [elementInput, "Element"],
24
+ [ruleGroup, "RuleGroup"],
25
+ [styleRefs, "StyleRefs"],
26
+ [initialStateInput, "InitialState"],
27
+ [styleVariantInput, "StyleVariant"],
28
+ [interactionNode, "InteractionNode"],
29
+ [bindingInput, "Binding"],
30
+ [position, "Position"],
31
+ [cssMap, "Css"],
32
+ [cssPatchMap, "CssPatch"],
33
+ [definitionSlot, "StyleSlot"],
34
+ [definitionSlotPatch, "StyleSlotPatch"],
35
+ [displayModeCss, "BreakpointCss"],
36
+ [displayModeCssPatch, "BreakpointCssPatch"],
37
+ [themeValue, "ThemeValue"]
38
+ ];
39
+ var claimSchemaId = (schema, id) => {
40
+ const taken = z.globalRegistry._idmap.get(id);
41
+ if (taken !== void 0 && taken !== schema) {
42
+ z.globalRegistry.add(schema, { id: `Plitzi${id}` });
43
+ return;
44
+ }
45
+ z.globalRegistry.add(schema, { id });
46
+ };
47
+ /** Idempotent: the module can be imported more than once in a test run, and a schema that already carries its id
48
+ * is left alone (re-adding the same one is harmless but pointless). Runs at module load, never per request — the
49
+ * MCP builds a server per request and this must not grow with them (pinned in the statelessness test). */
50
+ var registerSharedSchemaIds = () => {
51
+ for (const [schema, id] of SHARED_SCHEMAS) if (z.globalRegistry.get(schema)?.id === void 0) claimSchemaId(schema, id);
52
+ };
53
+ //#endregion
54
+ export { claimSchemaId, registerSharedSchemaIds };
@@ -1,8 +1,8 @@
1
1
  import { defUri, defsUri } from "../../../../helpers/uris.js";
2
2
  import { empty, fail } from "../../../../helpers/opResult.js";
3
3
  import { definitionToAI } from "../translator.js";
4
- import { guardKind, mergePatch, writeStyleItem } from "../write.js";
5
4
  import { patchCssShape } from "../shared.js";
5
+ import { guardKind, mergePatch, writeStyleItem } from "../write.js";
6
6
  import { z } from "zod";
7
7
  //#region src/modules/mcp/tools/operations/style/definitions/patchDefinition.ts
8
8
  var patchDefinitionOp = z.object({
@@ -1,7 +1,7 @@
1
1
  import { defUri, defsUri } from "../../../../helpers/uris.js";
2
2
  import { empty } from "../../../../helpers/opResult.js";
3
- import { guardKind, writeStyleItem } from "../write.js";
4
3
  import { upsertCssShape } from "../shared.js";
4
+ import { guardKind, writeStyleItem } from "../write.js";
5
5
  import { z } from "zod";
6
6
  //#region src/modules/mcp/tools/operations/style/definitions/upsertDefinition.ts
7
7
  var upsertDefinitionOp = z.object({
@@ -0,0 +1,40 @@
1
+ import { defUri, defsUri } from "../../../../helpers/uris.js";
2
+ import { empty } from "../../../../helpers/opResult.js";
3
+ import { upsertCssShape } from "../shared.js";
4
+ import { guardKind, writeStyleItem } from "../write.js";
5
+ import { z } from "zod";
6
+ //#region src/modules/mcp/tools/operations/style/definitions/upsertDefinitions.ts
7
+ var definitionBody = z.object(upsertCssShape);
8
+ var upsertDefinitionsOp = z.object({
9
+ type: z.literal("upsertDefinitions"),
10
+ definitions: z.record(z.string(), definitionBody).describe("Every class keyed by the class name you choose (kebab-case); the value is that class CSS")
11
+ }).describe("Create or fully replace SEVERAL style classes in ONE operation — identical to a run of upsertDefinition, minus the repeated envelope, so prefer it whenever a batch declares more than one class. Apply each by attaching its name to an element via style.base. Use patchDefinition to change only some CSS of one class.");
12
+ /** Writes each entry exactly as {@link upsertDefinition} would. Entries are independent: one bad class reports its
13
+ * own error under `definitions.<ref>` and the rest still write — nothing is persisted while the batch carries
14
+ * errors, so a partial write cannot escape. */
15
+ var upsertDefinitions = (space, env, op) => {
16
+ const result = {
17
+ ...empty(),
18
+ staleResources: [defsUri(env)]
19
+ };
20
+ const errors = [];
21
+ for (const [ref, { slots, ...base }] of Object.entries(op.definitions)) {
22
+ const guard = guardKind(space.style, ref, "class");
23
+ if (guard?.errors) {
24
+ errors.push(...guard.errors.map((error) => ({
25
+ ...error,
26
+ path: `definitions.${ref}`
27
+ })));
28
+ continue;
29
+ }
30
+ writeStyleItem(space.style, ref, base, slots, "class", void 0);
31
+ result.updated += 1;
32
+ result.staleResources.push(defUri(env, ref));
33
+ }
34
+ return errors.length > 0 ? {
35
+ ...result,
36
+ errors
37
+ } : result;
38
+ };
39
+ //#endregion
40
+ export { upsertDefinitions, upsertDefinitionsOp };
@@ -1,8 +1,8 @@
1
1
  import { globalUri, globalsUri } from "../../../../helpers/uris.js";
2
2
  import { empty, fail } from "../../../../helpers/opResult.js";
3
3
  import { globalStyleToAI } from "../translator.js";
4
- import { guardKind, mergePatch, writeStyleItem } from "../write.js";
5
4
  import { patchCssShape } from "../shared.js";
5
+ import { guardKind, mergePatch, writeStyleItem } from "../write.js";
6
6
  import { z } from "zod";
7
7
  //#region src/modules/mcp/tools/operations/style/globalStyles/patchGlobalStyle.ts
8
8
  var patchGlobalStyleOp = z.object({
@@ -1,7 +1,7 @@
1
1
  import { globalUri, globalsUri } from "../../../../helpers/uris.js";
2
2
  import { empty } from "../../../../helpers/opResult.js";
3
- import { guardKind, writeStyleItem } from "../write.js";
4
3
  import { upsertCssShape } from "../shared.js";
4
+ import { guardKind, writeStyleItem } from "../write.js";
5
5
  import { z } from "zod";
6
6
  //#region src/modules/mcp/tools/operations/style/globalStyles/upsertGlobalStyle.ts
7
7
  var upsertGlobalStyleOp = z.object({
@@ -1,8 +1,8 @@
1
1
  import { idUri, idsUri } from "../../../../helpers/uris.js";
2
2
  import { empty, fail } from "../../../../helpers/opResult.js";
3
3
  import { idStyleToAI } from "../translator.js";
4
- import { guardKind, mergePatch, writeStyleItem } from "../write.js";
5
4
  import { patchCssShape } from "../shared.js";
5
+ import { guardKind, mergePatch, writeStyleItem } from "../write.js";
6
6
  import { z } from "zod";
7
7
  //#region src/modules/mcp/tools/operations/style/idStyles/patchIdStyle.ts
8
8
  var patchIdStyleOp = z.object({
@@ -1,7 +1,7 @@
1
1
  import { idUri, idsUri } from "../../../../helpers/uris.js";
2
2
  import { empty } from "../../../../helpers/opResult.js";
3
- import { guardKind, writeStyleItem } from "../write.js";
4
3
  import { upsertCssShape } from "../shared.js";
4
+ import { guardKind, writeStyleItem } from "../write.js";
5
5
  import { z } from "zod";
6
6
  //#region src/modules/mcp/tools/operations/style/idStyles/upsertIdStyle.ts
7
7
  var upsertIdStyleOp = z.object({
@@ -1,6 +1,7 @@
1
1
  import { deleteDefinitionOp } from "./definitions/deleteDefinition.js";
2
2
  import { patchDefinitionOp } from "./definitions/patchDefinition.js";
3
3
  import { upsertDefinitionOp } from "./definitions/upsertDefinition.js";
4
+ import { upsertDefinitionsOp } from "./definitions/upsertDefinitions.js";
4
5
  import { deleteGlobalStyleOp } from "./globalStyles/deleteGlobalStyle.js";
5
6
  import { patchGlobalStyleOp } from "./globalStyles/patchGlobalStyle.js";
6
7
  import { upsertGlobalStyleOp } from "./globalStyles/upsertGlobalStyle.js";
@@ -12,6 +13,7 @@ import { upsertStyleVariableOp } from "./variables/upsertStyleVariable.js";
12
13
  //#region src/modules/mcp/tools/operations/style/operations.ts
13
14
  var styleOps = {
14
15
  upsertDefinition: upsertDefinitionOp,
16
+ upsertDefinitions: upsertDefinitionsOp,
15
17
  patchDefinition: patchDefinitionOp,
16
18
  deleteDefinition: deleteDefinitionOp,
17
19
  upsertGlobalStyle: upsertGlobalStyleOp,
@@ -56,4 +56,4 @@ var patchCssShape = {
56
56
  slots: z.record(z.string(), definitionSlotPatch).optional()
57
57
  };
58
58
  //#endregion
59
- export { displayModeCss, displayModeCssPatch, patchCssShape, styleCategory, themeValue, upsertCssShape };
59
+ export { cssMap, cssPatchMap, definitionSlot, definitionSlotPatch, displayModeCss, displayModeCssPatch, patchCssShape, styleCategory, themeValue, upsertCssShape };
@@ -1,5 +1,7 @@
1
+ import { noSpaceError, unauthorizedSpaceMessage } from "../helpers/space.js";
1
2
  import { resourceErrorMessage } from "../resources/canonical.js";
2
- import { readResource } from "../resources/router.js";
3
+ import { RENDER_GUIDE_URI, RENDER_TYPES_URI } from "../resources/renderGuide.js";
4
+ import { readPublicResource, readResource } from "../resources/router.js";
3
5
  import { defineTool } from "./shared/tool.js";
4
6
  import { z } from "zod";
5
7
  //#region src/modules/mcp/tools/read.ts
@@ -21,13 +23,29 @@ var read = (input, space, env) => {
21
23
  };
22
24
  }) };
23
25
  };
26
+ var publicHint = `Readable on this connection: ${RENDER_GUIDE_URI}, ${RENDER_TYPES_URI}, plitzi://guide, plitzi://css-properties. Everything else lives in a space this connection has none of.`;
27
+ var readPublic = (input, env) => ({ results: input.uris.map((uri) => {
28
+ const found = readPublicResource(env, uri);
29
+ if (found) return {
30
+ uri,
31
+ stateVersion: found.stateVersion,
32
+ data: found.data
33
+ };
34
+ return {
35
+ uri,
36
+ error: noSpaceError,
37
+ message: unauthorizedSpaceMessage,
38
+ hint: publicHint
39
+ };
40
+ }) });
24
41
  var readTool = defineTool({
25
42
  name: "plitzi_read",
26
43
  title: "Batch read",
27
44
  description: "Batch-fetch resource content for URIs you ALREADY HOLD (from plitzi_search or a write response) — the tool form of opening MCP resources, for many at once. Pass an array of URIs (pages, elements, definitions, variables) and get them all in one call. NOT for finding things: to locate an element by label/type use plitzi_search; to browse what exists, list the plitzi:// resources. Each result is { uri, stateVersion, data } or a teachable error, so one bad URI never fails the batch. Never hand-build a URI to guess your way to an element — search for it.",
28
45
  inputShape: readShape,
29
46
  access: "read",
30
- run: (input, ctx) => read(input, ctx.space, ctx.env)
47
+ run: (input, ctx) => read(input, ctx.space, ctx.env),
48
+ runPublic: readPublic
31
49
  });
32
50
  //#endregion
33
- export { read, readShape, readTool };
51
+ export { read, readPublic, readShape, readTool };
@@ -2,9 +2,12 @@ import { RENDER_APP_URI } from "../apps/render/index.js";
2
2
  import { emptySpace } from "../helpers/space.js";
3
3
  import { operations } from "./operations/index.js";
4
4
  import { applyOperations } from "./apply/dispatch.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";
9
+ import { randomUUID } from "node:crypto";
10
+ import { z } from "zod";
8
11
  import { validateSchema } from "@plitzi/sdk-schema/helpers/schemaValidator";
9
12
  import { generateCache } from "@plitzi/sdk-style/StyleHelper";
10
13
  //#region src/modules/mcp/tools/render.ts
@@ -32,16 +35,26 @@ var seedSpace = () => {
32
35
  return space;
33
36
  };
34
37
  var noWarnings = (warnings) => warnings.length > 0 ? warnings : void 0;
35
- var renderShape = { operations };
38
+ var renderShape = {
39
+ operations,
40
+ patch: z.boolean().optional().describe("Set true to CHANGE a widget you already rendered instead of rebuilding it: send its `renderId` and ONLY the operations that differ (patchDefinition, patchElement, deleteElement, a new repeatElement…). The widget merges them into the batch it was built from and reports back what it applied. The refs it already has (card-1, blk-2-3) are the ones to address. If that widget cannot be recovered — a surface that renders none, a host that keeps no storage — the answer says so and you re-send the whole batch."),
41
+ renderId: z.string().optional().describe("Handle returned by a previous render. Required with patch:true; it names the widget being changed.")
42
+ };
36
43
  var render = (input) => {
37
44
  const space = seedSpace();
38
- const validation = validateOperations(space, input.operations);
45
+ const expansion = expandOperations(input.operations);
46
+ if (expansion.errors.length > 0) return {
47
+ rendered: false,
48
+ errors: expansion.errors
49
+ };
50
+ const ops = expansion.operations;
51
+ const validation = validateOperations(space, ops);
39
52
  if (!validation.valid) return {
40
53
  rendered: false,
41
54
  errors: validation.errors,
42
55
  warnings: noWarnings(validation.warnings)
43
56
  };
44
- const outcome = applyOperations(space, "main", input.operations);
57
+ const outcome = applyOperations(space, "main", ops);
45
58
  if (outcome.errors.length > 0) return {
46
59
  rendered: false,
47
60
  errors: outcome.errors,
@@ -57,7 +70,7 @@ var render = (input) => {
57
70
  })),
58
71
  warnings: noWarnings(validation.warnings)
59
72
  };
60
- const audit = auditResources(space, input.operations);
73
+ const audit = auditResources(space, ops);
61
74
  const warnings = [...validation.warnings, ...audit.warnings];
62
75
  if (audit.errors.length > 0) return {
63
76
  rendered: false,
@@ -67,6 +80,7 @@ var render = (input) => {
67
80
  space.style.cache = generateCache(space.style);
68
81
  return {
69
82
  rendered: true,
83
+ operations: ops,
70
84
  rootRef: HOST_PAGE_REF,
71
85
  elementCount: Object.keys(space.schema.flat).length - 1,
72
86
  offlineData: {
@@ -76,7 +90,53 @@ var render = (input) => {
76
90
  warnings: noWarnings(warnings)
77
91
  };
78
92
  };
79
- var toRenderResult = (res) => {
93
+ /** A patch is not rendered here — it CANNOT be: this server keeps nothing between calls (no session, no store,
94
+ * any replica answers any request), so the previous widget only exists on the host side. The delta therefore
95
+ * travels as a courier result: the model pays for the delta alone, the view recovers the batch that widget was
96
+ * built from (by renderId — see apps/render/heldBatch.ts), merges, and re-calls this same tool with the whole
97
+ * thing over the host bridge, never through the model's context, then reports back with ui/update-model-context.
98
+ *
99
+ * The renderId is the explicit handle the MCP RC asks for in place of implicit session state: the model carries
100
+ * it, so a patch names the widget it means and any replica can serve it.
101
+ *
102
+ * Validation is not skipped, only deferred: the re-call carries the full batch, so refs, integrity and the audit
103
+ * all run exactly as they do on a first render, and their errors reach the model through that report. */
104
+ var toPatchResult = (ops, renderId) => {
105
+ if (ops.length === 0) return { content: [{
106
+ type: "text",
107
+ text: JSON.stringify({
108
+ patch: false,
109
+ error: "A patch carries no operations",
110
+ hint: "Send the operations that differ from the widget on screen, or drop `patch` to render a new one."
111
+ })
112
+ }] };
113
+ if (renderId === void 0) return { content: [{
114
+ type: "text",
115
+ text: JSON.stringify({
116
+ patch: false,
117
+ error: "A patch needs the renderId of the widget it changes",
118
+ hint: "Pass the renderId the render answered with, or drop `patch` to render a new widget."
119
+ })
120
+ }] };
121
+ return {
122
+ content: [{
123
+ type: "text",
124
+ text: JSON.stringify({
125
+ patch: true,
126
+ renderId,
127
+ operations: ops.length,
128
+ note: "Handed to the widget; it will report what it applied. If nothing reports back, it could not be recovered — re-send the full batch without patch."
129
+ })
130
+ }],
131
+ structuredContent: {
132
+ patch: true,
133
+ renderId,
134
+ operations: ops
135
+ }
136
+ };
137
+ };
138
+ var newRenderId = () => `r${randomUUID().slice(0, 8)}`;
139
+ var toRenderResult = (res, renderId) => {
80
140
  if (!res.rendered) return { content: [{
81
141
  type: "text",
82
142
  text: JSON.stringify({
@@ -87,6 +147,7 @@ var toRenderResult = (res) => {
87
147
  }] };
88
148
  const summary = {
89
149
  rendered: true,
150
+ renderId,
90
151
  rootRef: res.rootRef,
91
152
  elementCount: res.elementCount,
92
153
  warnings: res.warnings
@@ -98,19 +159,20 @@ var toRenderResult = (res) => {
98
159
  }],
99
160
  structuredContent: {
100
161
  ...summary,
101
- offlineData: res.offlineData
162
+ offlineData: res.offlineData,
163
+ operations: res.operations
102
164
  }
103
165
  };
104
166
  };
105
167
  var renderTool = defineTool({
106
168
  name: "plitzi_render",
107
169
  title: "Render widget",
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.\n2b. LAYOUT — it renders in a side panel, so width is free and HEIGHT is scarce. Plain containers stack children vertically, which is the tall half-empty default to avoid: put peers (metrics, plans, options, image + text) in a wrapping row — display:flex, flex-direction:row, flex-wrap:wrap, children flex-grow:\"1\" + flex-basis:\"0%\" + min-width — or a grid with grid-template-columns:\"repeat(auto-fit, minmax(160px, 1fr))\". Keep padding 12-16px and gap 8-12px, and let the outer container fill the panel. Stack only what reads in order (heading over paragraph, forms, steps, prose). Watch the SDK defaults: every container has min-width/min-height 50px (set them to \"0\" for rails, dividers, dots and any flex child that must shrink), and heading/paragraph keep the browser's own margins (zero them and space with the parent's gap).\n2c. THEME — it is embedded in the host UI, which MAY BE DARK, so never hardcode a light palette. Take colours from the host variables with a light-dark() fallback — background-color:\"var(--color-background-secondary, light-dark(#ffffff, #1f2430))\", color:\"var(--color-text-primary, light-dark(#0f172a, #e8eaed))\", border-color:\"var(--color-border-primary, light-dark(#e2e8f0, #333a48))\" — and always set `color` wherever you set `background-color` (a brand accent states its own text colour too).\n3. CONTENT — visible copy goes in props.content (text, heading, paragraph, button); heading level is the element subType (\"h1\"..\"h6\"); image/video take props.src. An unknown prop comes back as a warning naming the right one.\n\nCommon types: container, heading, paragraph, text, button, link, image, video, list, listItem, markdown (plitzi://render/types lists every built-in type with descriptions). Widgets can also be data-driven and interactive — an apiContainer fetches at runtime, upsertBinding wires data into elements, and upsertInteractionFlow makes them react to clicks (see the guide).\nREAD the resource plitzi://render/guide first — it has the element/prop table, the style model and a full worked example, and following it is the difference between a widget that renders and repeated failed calls.\nReturns a compact summary (the widget is shown to the user); on failure it returns teachable errors (path + hint) — read them and retry.",
170
+ description: "Show the user a real, rendered UI widget instead of describing one — cards, hero sections, pricing tables, forms, menus, checklists, profiles, galleries. It runs the Plitzi SDK fully offline: no backend, account, or setup. Reach for it whenever a visual layout beats prose: the user asks you to design/build/show something, OR your answer is naturally visual (a recipe → a card, a comparison → a table, steps → a checklist). Prefer showing over telling.\n\nAuthor the widget as an ordered list of `operations` that build an element tree under the pre-seeded root page \"render\". Three rules:\n1. STRUCTURE — one upsertElement builds the whole tree: set pageRef:\"render\" and give element a nested `children` array. Each element is { ref (unique), type, subType?, props?, style?, children? }; children render in order. (To attach to something you already made, use a top-level parentRef:\"<existing ref>\" instead.)\n1b. REPEATS — the moment two siblings share a shape and differ only in data (list, steps, cards, table, timeline), do NOT copy-paste them: use repeatElement { pageRef, ref (wrapper), style, template, items }. The template is written once with {{item.field}} placeholders and rendered per row; refs come out numbered (\"step-1\", \"step-2\"…). A list INSIDE each row (days with their own steps) is the same op: give the wrapping node repeat:{ items:\"{{item.<list>}}\", template:… } and put the sub-rows in the row data.\n2. STYLE — declare ALL the classes in ONE upsertDefinitions { definitions: { \"<class>\": { desktop:{ …CSS in kebab-case… } }, … } }, then attach via the element style:{ base:[\"<class ref>\"] }. Lay containers out with flex/grid. Keep the call small: one class per look (not per property), and never hand-draw a scene in a data: URI — it costs more than the whole widget; use an https image, a flat colour or a gradient.\n2b. LAYOUT — it renders in a side panel, so width is free and HEIGHT is scarce. Plain containers stack children vertically, which is the tall half-empty default to avoid: put peers (metrics, plans, options, image + text) in a wrapping row — display:flex, flex-direction:row, flex-wrap:wrap, children flex-grow:\"1\" + flex-basis:\"0%\" + min-width — or a grid with grid-template-columns:\"repeat(auto-fit, minmax(160px, 1fr))\". Keep padding 12-16px and gap 8-12px, and let the outer container fill the panel. Stack only what reads in order (heading over paragraph, forms, steps, prose). Nothing carries a minimum size, so an element with no content and no size takes none give a spacer or a rail its own height/width — while heading/paragraph do keep the margins the browser gives them (zero them, space with the parent gap).\n2c. THEME — it is embedded in the host UI, which MAY BE DARK, so never hardcode a light palette. Take colours from the host variables with a light-dark() fallback — background-color:\"var(--color-background-secondary, light-dark(#ffffff, #1f2430))\", color:\"var(--color-text-primary, light-dark(#0f172a, #e8eaed))\", border-color:\"var(--color-border-primary, light-dark(#e2e8f0, #333a48))\" — and always set `color` wherever you set `background-color` (a brand accent states its own text colour too).\n3. CONTENT — visible copy goes in props.content (text, heading, paragraph, button); heading level is the element subType (\"h1\"..\"h6\"); image/video take props.src. An unknown prop comes back as a warning naming the right one.\n\nCommon types: container, heading, paragraph, text, button, link, image, video, list, listItem, markdown (plitzi://render/types lists every built-in type with descriptions). Widgets can also be data-driven and interactive — an apiContainer fetches at runtime, upsertBinding wires data into elements, and upsertInteractionFlow makes them react to clicks (see the guide).\nREAD the resource plitzi://render/guide first — it has the element/prop table, the style model and a full worked example, and following it is the difference between a widget that renders and repeated failed calls.\nITERATING — to change a widget you already rendered, do NOT rebuild it: call again with patch:true, the `renderId` that render answered with, and ONLY the operations that differ (patchDefinition, patchElement, deleteElement…). The widget merges them and reports back what it applied; address rows by the refs you already know. Patch ONLY to modify that widget: a different subject or a different kind of widget is a fresh render, without patch — a patch is merged into the previous batch, so patching a new idea leaves you with both.\nReturns a compact summary including the renderId (the widget itself is shown to the user); on failure it returns teachable errors (path + hint) — read them and retry.",
109
171
  inputShape: renderShape,
110
172
  access: "read",
111
173
  spaceless: true,
112
174
  ui: { resourceUri: RENDER_APP_URI },
113
- run: (input) => toRenderResult(render(input))
175
+ run: (input) => input.patch === true ? toPatchResult(input.operations, input.renderId) : toRenderResult(render(input), input.renderId ?? newRenderId())
114
176
  });
115
177
  //#endregion
116
178
  export { render, renderShape, renderTool };
@@ -0,0 +1,44 @@
1
+ import { expandRepeat } from "../operations/schema/elements/repeatElement.js";
2
+ //#region src/modules/mcp/tools/shared/expandOperations.ts
3
+ /** Rewrites the sugar ops into the vocabulary the rest of the pipeline knows, BEFORE validation — today that is
4
+ * `repeatElement`, which stands for the `upsertElement` its template + rows expand to. Everything downstream
5
+ * (validator, dispatch, audit, versions) therefore stays unaware of it, and a row that renders a bad element is
6
+ * reported by the ordinary element checks.
7
+ *
8
+ * One op in, one op out: the indices the agent sent are the indices every error path names, so `operations[3]`
9
+ * still means the fourth operation it wrote. */
10
+ var expandOperations = (ops) => {
11
+ if (!ops.some((op) => op.type === "repeatElement")) return {
12
+ operations: ops,
13
+ errors: []
14
+ };
15
+ const operations = [];
16
+ const errors = [];
17
+ for (const [index, op] of ops.entries()) {
18
+ if (op.type !== "repeatElement") {
19
+ operations.push(op);
20
+ continue;
21
+ }
22
+ const expanded = expandRepeat(op);
23
+ if (!expanded.element) {
24
+ errors.push(...expanded.errors.map((error) => ({
25
+ ...error,
26
+ path: `operations[${index}].${error.path}`
27
+ })));
28
+ continue;
29
+ }
30
+ operations.push({
31
+ type: "upsertElement",
32
+ pageRef: op.pageRef,
33
+ element: expanded.element,
34
+ ...op.parentRef === void 0 ? {} : { parentRef: op.parentRef },
35
+ ...op.position === void 0 ? {} : { position: op.position }
36
+ });
37
+ }
38
+ return {
39
+ operations,
40
+ errors
41
+ };
42
+ };
43
+ //#endregion
44
+ export { expandOperations };
@@ -13,16 +13,21 @@ var imageResult = (images, meta) => ({ content: [{
13
13
  /** Author a tool: give it its metadata, its input shape and a typed `run`. The returned descriptor parses the
14
14
  * raw args against the shape before handing them to `run`, so `run` is fully typed and no cast is needed.
15
15
  * Adding a tool is: call defineTool in its own file and append it to the `tools` registry. */
16
- var defineTool = (spec) => ({
17
- name: spec.name,
18
- title: spec.title,
19
- description: spec.description,
20
- inputShape: spec.inputShape,
21
- access: spec.access,
22
- requires: spec.requires,
23
- spaceless: spec.spaceless,
24
- ui: spec.ui,
25
- execute: (args, ctx) => spec.run(z.object(spec.inputShape).parse(args), ctx)
26
- });
16
+ var defineTool = (spec) => {
17
+ const { runPublic } = spec;
18
+ const parse = (args) => z.object(spec.inputShape).parse(args);
19
+ return {
20
+ name: spec.name,
21
+ title: spec.title,
22
+ description: spec.description,
23
+ inputShape: spec.inputShape,
24
+ access: spec.access,
25
+ requires: spec.requires,
26
+ spaceless: spec.spaceless,
27
+ ui: spec.ui,
28
+ execute: (args, ctx) => spec.run(parse(args), ctx),
29
+ ...runPublic ? { executePublic: (args, env) => runPublic(parse(args), env) } : {}
30
+ };
31
+ };
27
32
  //#endregion
28
33
  export { defineTool, imageResult };
@@ -78,6 +78,9 @@ var collectTouched = (ops) => {
78
78
  case "patchDefinition":
79
79
  touched.definitions.add(op.ref);
80
80
  break;
81
+ case "upsertDefinitions":
82
+ for (const ref of Object.keys(op.definitions)) touched.definitions.add(ref);
83
+ break;
81
84
  case "upsertGlobalStyle":
82
85
  case "patchGlobalStyle":
83
86
  touched.globalStyles.add(op.componentType);
@@ -16,13 +16,22 @@ var batchDeclaredFolders = (ops) => {
16
16
  for (const op of ops) if (op.type === "upsertFolder") refs.add(op.ref);
17
17
  return refs;
18
18
  };
19
+ var variantNamesOf = (source) => {
20
+ const names = new Set(Object.keys(source.variants ?? {}));
21
+ for (const slot of Object.values(source.slots ?? {})) for (const name of Object.keys(slot.variants ?? {})) names.add(name);
22
+ return names;
23
+ };
19
24
  var batchDeclaredVariants = (ops) => {
20
25
  const map = /* @__PURE__ */ new Map();
26
+ const record = (ref, names) => {
27
+ if (names.size > 0) map.set(ref, /* @__PURE__ */ new Set([...map.get(ref) ?? [], ...names]));
28
+ };
21
29
  for (const op of ops) {
22
- if (op.type !== "upsertDefinition" && op.type !== "patchDefinition") continue;
23
- const names = new Set(Object.keys(op.variants ?? {}));
24
- for (const slot of Object.values(op.slots ?? {})) for (const name of Object.keys(slot.variants ?? {})) names.add(name);
25
- if (names.size > 0) map.set(op.ref, /* @__PURE__ */ new Set([...map.get(op.ref) ?? [], ...names]));
30
+ if (op.type === "upsertDefinitions") {
31
+ for (const [ref, definition] of Object.entries(op.definitions)) record(ref, variantNamesOf(definition));
32
+ continue;
33
+ }
34
+ if (op.type === "upsertDefinition" || op.type === "patchDefinition") record(op.ref, variantNamesOf(op));
26
35
  }
27
36
  return map;
28
37
  };
@@ -96,6 +96,21 @@ var validateOperations = (space, ops) => {
96
96
  checkVariantApplication(op.initialState, `${base}.initialState`, ctx);
97
97
  break;
98
98
  }
99
+ case "upsertDefinitions": {
100
+ const entries = Object.entries(op.definitions);
101
+ if (entries.length === 0) ctx.errors.push({
102
+ path: `${base}.definitions`,
103
+ message: "This operation declares no classes",
104
+ hint: "Key each class by its name: { \"definitions\": { \"card\": { \"desktop\": { … } } } }"
105
+ });
106
+ for (const [ref, { slots, ...slot }] of entries) {
107
+ const entry = `${base}.definitions.${ref}`;
108
+ checkRef(ref, entry, ctx);
109
+ checkSlotCss(slot, entry, ctx);
110
+ for (const [slotName, slotDef] of Object.entries(slots ?? {})) checkSlotCss(slotDef, `${entry}.slots.${slotName}`, ctx);
111
+ }
112
+ break;
113
+ }
99
114
  case "upsertDefinition":
100
115
  case "patchDefinition": {
101
116
  const { type, ref, slots, ...slot } = op;