@plitzi/sdk-mcp 0.37.2 → 0.37.3

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 (94) hide show
  1. package/CHANGELOG.md +1156 -0
  2. package/README.md +6 -2
  3. package/dist/createServer.js +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +2 -1
  6. package/dist/modules/mcp/accessibilityOutline.d.ts +38 -0
  7. package/dist/modules/mcp/accessibilityOutline.js +75 -0
  8. package/dist/modules/mcp/accessibilityOutline.test.d.ts +1 -0
  9. package/dist/modules/mcp/catalogs/index.d.ts +2 -2
  10. package/dist/modules/mcp/catalogs/index.js +2 -2
  11. package/dist/modules/mcp/catalogs/observed.js +2 -2
  12. package/dist/modules/mcp/catalogs/pluginCatalog.d.ts +6 -3
  13. package/dist/modules/mcp/catalogs/pluginCatalog.js +12 -7
  14. package/dist/modules/mcp/helpers/agentPrompt.js +22 -5
  15. package/dist/modules/mcp/helpers/guide.d.ts +2 -2
  16. package/dist/modules/mcp/helpers/guide.js +212 -48
  17. package/dist/modules/mcp/helpers/interactions.d.ts +2 -1
  18. package/dist/modules/mcp/helpers/interactions.js +2 -0
  19. package/dist/modules/mcp/helpers/space.d.ts +6 -4
  20. package/dist/modules/mcp/helpers/space.js +7 -20
  21. package/dist/modules/mcp/helpers/uris.d.ts +3 -1
  22. package/dist/modules/mcp/helpers/uris.js +4 -1
  23. package/dist/modules/mcp/index.d.ts +2 -0
  24. package/dist/modules/mcp/localScreenshotClient.d.ts +11 -0
  25. package/dist/modules/mcp/localScreenshotClient.js +24 -9
  26. package/dist/modules/mcp/proxy/guard.d.ts +6 -0
  27. package/dist/modules/mcp/proxy/guard.js +8 -12
  28. package/dist/modules/mcp/resources/functions.d.ts +8 -0
  29. package/dist/modules/mcp/resources/functions.js +35 -0
  30. package/dist/modules/mcp/resources/register.d.ts +2 -1
  31. package/dist/modules/mcp/resources/register.js +49 -2
  32. package/dist/modules/mcp/resources/renderGuide.js +3 -2
  33. package/dist/modules/mcp/resources/router.js +3 -1
  34. package/dist/modules/mcp/screenshotClient.js +12 -4
  35. package/dist/modules/mcp/server.js +20 -10
  36. package/dist/modules/mcp/tests/guide.test.d.ts +1 -0
  37. package/dist/modules/mcp/tools/apply/dispatch.js +9 -1
  38. package/dist/modules/mcp/tools/apply/index.d.ts +34 -0
  39. package/dist/modules/mcp/tools/apply/index.js +35 -70
  40. package/dist/modules/mcp/tools/index.js +4 -2
  41. package/dist/modules/mcp/tools/operations/connectors/manifest.d.ts +0 -1
  42. package/dist/modules/mcp/tools/operations/functions/functions.test.d.ts +1 -0
  43. package/dist/modules/mcp/tools/operations/functions/index.d.ts +31 -0
  44. package/dist/modules/mcp/tools/operations/functions/index.js +48 -0
  45. package/dist/modules/mcp/tools/operations/index.d.ts +122 -2
  46. package/dist/modules/mcp/tools/operations/index.js +4 -3
  47. package/dist/modules/mcp/tools/operations/schema/elements/deleteElement.js +5 -6
  48. package/dist/modules/mcp/tools/operations/schema/elements/moveElement.js +3 -11
  49. package/dist/modules/mcp/tools/operations/schema/elements/upsertElement.js +6 -9
  50. package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.d.ts +5 -0
  51. package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.js +3 -1
  52. package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.d.ts +5 -0
  53. package/dist/modules/mcp/tools/operations/schema/operations.d.ts +27 -0
  54. package/dist/modules/mcp/tools/operations/schema/pages/deleteLayout.js +6 -5
  55. package/dist/modules/mcp/tools/operations/schema/pages/deletePage.js +6 -5
  56. package/dist/modules/mcp/tools/operations/schema/settings/patchSettings.d.ts +17 -0
  57. package/dist/modules/mcp/tools/operations/schema/settings/patchSettings.js +34 -5
  58. package/dist/modules/mcp/tools/operations/schema/shared.d.ts +6 -0
  59. package/dist/modules/mcp/tools/operations/schema/shared.js +5 -0
  60. package/dist/modules/mcp/tools/operations/schema/translator.js +3 -2
  61. package/dist/modules/mcp/tools/operations/schema/write.d.ts +10 -5
  62. package/dist/modules/mcp/tools/operations/schema/write.js +16 -16
  63. package/dist/modules/mcp/tools/preview.d.ts +27 -0
  64. package/dist/modules/mcp/tools/render.d.ts +27 -0
  65. package/dist/modules/mcp/tools/render.js +7 -43
  66. package/dist/modules/mcp/tools/screenshot.d.ts +39 -0
  67. package/dist/modules/mcp/tools/screenshot.js +40 -5
  68. package/dist/modules/mcp/tools/shared/draftBatch.d.ts +25 -0
  69. package/dist/modules/mcp/tools/shared/draftBatch.js +58 -0
  70. package/dist/modules/mcp/tools/shared/draftBatch.test.d.ts +1 -0
  71. package/dist/modules/mcp/tools/shared/lintDraft.d.ts +30 -0
  72. package/dist/modules/mcp/tools/shared/lintDraft.js +142 -0
  73. package/dist/modules/mcp/tools/shared/lintDraft.test.d.ts +1 -0
  74. package/dist/modules/mcp/tools/shared/tool.d.ts +4 -0
  75. package/dist/modules/mcp/tools/shared/validator/bindings.d.ts +0 -4
  76. package/dist/modules/mcp/tools/shared/validator/bindings.js +1 -60
  77. package/dist/modules/mcp/tools/shared/validator/elements.js +3 -15
  78. package/dist/modules/mcp/tools/shared/validator/index.d.ts +1 -4
  79. package/dist/modules/mcp/tools/shared/validator/index.js +25 -20
  80. package/dist/modules/mcp/tools/shared/validator/interactions.js +3 -58
  81. package/dist/modules/mcp/tools/tryFunction.d.ts +18 -0
  82. package/dist/modules/mcp/tools/tryFunction.js +43 -0
  83. package/dist/modules/mcp/tools/validate.d.ts +34 -0
  84. package/dist/modules/mcp/tools/validate.js +9 -26
  85. package/dist/modules/mcp/types/aiSchema.d.ts +8 -1
  86. package/dist/modules/mcp/types/previewTypes.d.ts +6 -0
  87. package/dist/modules/mcp/types/screenshotTypes.d.ts +16 -0
  88. package/dist/modules/mcp/types/toolTypes.d.ts +5 -1
  89. package/dist/preview/createPreview.js +5 -4
  90. package/dist/stages/preview.js +15 -1
  91. package/package.json +11 -10
  92. package/skills/plitzi-render/SKILL.md +1 -2
  93. package/dist/modules/mcp/tools/shared/validator/audit.d.ts +0 -4
  94. package/dist/modules/mcp/tools/shared/validator/audit.js +0 -127
@@ -1,5 +1,6 @@
1
1
  import { operation } from "./operations/index.js";
2
2
  import { defineTool, imageResult } from "./shared/tool.js";
3
+ import { unnamedControls } from "../accessibilityOutline.js";
3
4
  import { z } from "zod";
4
5
  //#region src/modules/mcp/tools/screenshot.ts
5
6
  var VIEWPORTS = {
@@ -27,12 +28,29 @@ var screenshotShape = {
27
28
  "mobile",
28
29
  "both"
29
30
  ]).optional().describe("Which viewport(s) to capture. \"both\" catches responsive issues (e.g. overflow at one width). Default desktop."),
30
- fullPage: z.boolean().optional().describe("Capture the full scrollable page instead of only the visible viewport. Default false.")
31
+ fullPage: z.boolean().optional().describe("Capture the full scrollable page instead of only the visible viewport. Default false."),
32
+ view: z.enum([
33
+ "image",
34
+ "accessibility",
35
+ "both"
36
+ ]).optional().describe("What to bring back. \"image\" (default) is the picture. \"accessibility\" is the page as a screen reader and a browser agent (Claude in Chrome) read it — its accessibility tree as an outline of roles and names — with every control and picture that has no name listed; text only, far cheaper than an image. \"both\" is both.")
37
+ };
38
+ var VIEWS = {
39
+ image: ["image"],
40
+ accessibility: ["accessibility"],
41
+ both: ["image", "accessibility"]
31
42
  };
43
+ /** Each viewport's outline, with what in it has no name — the part an agent acts on. */
44
+ var readingOf = (outlines) => outlines.map(({ label, outline }) => ({
45
+ viewport: label,
46
+ outline,
47
+ unnamed: unnamedControls(outline)
48
+ }));
49
+ var ACCESSIBILITY_HINT = "Every entry in `unnamed` is a control or picture announced by its role alone (\"button\") — nobody using a screen reader, and no browser agent, can tell what it does. Name it — a button: `title` or words inside; a link: `label`; a field: `label` (with `hideLabel` to keep it out of sight); an image: `alt`, or `decorative: true`; an icon that means something alone: `label`. `line` is where it sits in the outline.";
32
50
  var screenshotTool = defineTool({
33
51
  name: "plitzi_screenshot",
34
52
  title: "Screenshot",
35
- description: "Render a page to a real PNG image so you can SEE the visual result of your edits — overflow, misalignment and broken layout that data alone never reveals. Pass unsaved `operations` to screenshot a proposed change before committing, and viewport \"both\" to compare desktop and mobile. Returns the image(s).",
53
+ description: "Render a page to a real PNG image so you can SEE the visual result of your edits — overflow, misalignment and broken layout that data alone never reveals. Pass unsaved `operations` to screenshot a proposed change before committing, and viewport \"both\" to compare desktop and mobile. `view: \"accessibility\"` reads the page as a screen reader and a browser agent do instead, and lists every control with no name. Returns the image(s).",
36
54
  inputShape: screenshotShape,
37
55
  access: "read",
38
56
  requires: "screenshot",
@@ -54,9 +72,10 @@ var screenshotTool = defineTool({
54
72
  });
55
73
  if (!pv.ok) return pv;
56
74
  const viewports = resolveViewports(input.viewport);
75
+ const view = input.view ?? "image";
57
76
  if (!ctx.screenshot) return {
58
77
  warning: "SCREENSHOT_DISABLED",
59
- message: "The screenshot service is not configured; returning the HTML preview instead.",
78
+ message: view === "image" ? "The screenshot service is not configured; returning the HTML preview instead." : "No browser is configured, so the accessibility tree cannot be read; returning the HTML preview instead. Its roles and names are in the markup: every button, link and field needs words, every image an `alt`.",
60
79
  pageRef: input.pageRef ?? "default",
61
80
  pagePath: pv.pagePath,
62
81
  stateVersion: pv.stateVersion,
@@ -66,7 +85,8 @@ var screenshotTool = defineTool({
66
85
  pagePath: pv.pagePath,
67
86
  token: pv.token,
68
87
  viewports,
69
- fullPage: input.fullPage
88
+ fullPage: input.fullPage,
89
+ views: VIEWS[view]
70
90
  });
71
91
  if (!shot.ok) {
72
92
  const fallback = await ctx.preview.render(draft);
@@ -80,11 +100,26 @@ var screenshotTool = defineTool({
80
100
  html: fallback.ok ? fallback.html : ""
81
101
  };
82
102
  }
83
- return imageResult(shot.images, {
103
+ const meta = {
84
104
  pageRef: input.pageRef ?? "default",
85
105
  pagePath: pv.pagePath,
86
106
  stateVersion: pv.stateVersion,
87
107
  viewports: viewports.map((v) => v.label)
108
+ };
109
+ const accessibility = view === "image" ? {} : shot.accessibility ? {
110
+ accessibility: readingOf(shot.accessibility),
111
+ hint: ACCESSIBILITY_HINT
112
+ } : {
113
+ warning: "ACCESSIBILITY_UNSUPPORTED",
114
+ message: "The browser service answered without the accessibility tree — it predates the accessibility view. The image, if asked for, is still here."
115
+ };
116
+ if (view === "accessibility") return {
117
+ ...meta,
118
+ ...accessibility
119
+ };
120
+ return imageResult(shot.images, {
121
+ ...meta,
122
+ ...accessibility
88
123
  });
89
124
  }
90
125
  });
@@ -0,0 +1,25 @@
1
+ import { ValidationMode } from './validator/context';
2
+ import { Space } from '../../helpers';
3
+ import { Env, MutationOutcome, ValidationError } from '../../types';
4
+ import { Operation } from '../operations';
5
+ export type DraftResult = {
6
+ ok: false;
7
+ errors: ValidationError[];
8
+ warnings: string[];
9
+ } | {
10
+ ok: true;
11
+ ops: Operation[];
12
+ draft: Space;
13
+ outcome: MutationOutcome;
14
+ warnings: string[];
15
+ };
16
+ /**
17
+ * A batch run on a copy of the space and read back — the one way every tool that takes operations handles them, so
18
+ * `plitzi_validate` answers exactly what `plitzi_apply` would, and `plitzi_render` holds a widget to the same rules.
19
+ *
20
+ * In order: the sugar ops expanded; what was already wrong with the elements the batch touches fixed where it has one
21
+ * reading (`fixTouched`, each fix said in `warnings`); each op checked against the input it takes; the batch applied to
22
+ * the copy; and the copy read by `lintDraft` — structure and meaning, the same reading every other writer gets. The
23
+ * first stage to refuse ends it; the space handed in is never touched.
24
+ */
25
+ export declare const draftBatch: (space: Space, env: Env, operations: Operation[], mode?: ValidationMode) => DraftResult;
@@ -0,0 +1,58 @@
1
+ import { cloneSpace } from "../../helpers/space.js";
2
+ import { expandOperations } from "./expandOperations.js";
3
+ import { fixTouched, lintDraft } from "./lintDraft.js";
4
+ import { validateOperations } from "./validator/index.js";
5
+ import { applyOperations } from "../apply/dispatch.js";
6
+ //#region src/modules/mcp/tools/shared/draftBatch.ts
7
+ /**
8
+ * A batch run on a copy of the space and read back — the one way every tool that takes operations handles them, so
9
+ * `plitzi_validate` answers exactly what `plitzi_apply` would, and `plitzi_render` holds a widget to the same rules.
10
+ *
11
+ * In order: the sugar ops expanded; what was already wrong with the elements the batch touches fixed where it has one
12
+ * reading (`fixTouched`, each fix said in `warnings`); each op checked against the input it takes; the batch applied to
13
+ * the copy; and the copy read by `lintDraft` — structure and meaning, the same reading every other writer gets. The
14
+ * first stage to refuse ends it; the space handed in is never touched.
15
+ */
16
+ var draftBatch = (space, env, operations, mode = "space") => {
17
+ const expansion = expandOperations(operations);
18
+ if (expansion.errors.length > 0) return {
19
+ ok: false,
20
+ errors: expansion.errors,
21
+ warnings: []
22
+ };
23
+ const ops = expansion.operations;
24
+ const prepared = fixTouched(space, ops);
25
+ const validation = validateOperations(prepared.space, ops, mode);
26
+ if (!validation.valid) return {
27
+ ok: false,
28
+ errors: validation.errors,
29
+ warnings: [...prepared.fixed, ...validation.warnings]
30
+ };
31
+ const draft = cloneSpace(prepared.space);
32
+ const outcome = applyOperations(draft, env, ops);
33
+ if (outcome.errors.length > 0) return {
34
+ ok: false,
35
+ errors: outcome.errors,
36
+ warnings: [...prepared.fixed, ...validation.warnings]
37
+ };
38
+ const reading = lintDraft(draft, ops, prepared.space);
39
+ const warnings = [
40
+ ...prepared.fixed,
41
+ ...validation.warnings,
42
+ ...reading.warnings
43
+ ];
44
+ if (reading.errors.length > 0) return {
45
+ ok: false,
46
+ errors: reading.errors,
47
+ warnings
48
+ };
49
+ return {
50
+ ok: true,
51
+ ops,
52
+ draft,
53
+ outcome,
54
+ warnings
55
+ };
56
+ };
57
+ //#endregion
58
+ export { draftBatch };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { Space } from '../../helpers';
2
+ import { ValidationResult } from '../../types';
3
+ import { Operation } from '../operations';
4
+ /**
5
+ * The space as the batch should meet it: what was already wrong with the elements it is about to change, fixed where
6
+ * the problem has one reading — a typo of an attribute, a URL left in page mode, a flag written as text.
7
+ *
8
+ * On the space BEFORE the batch, and only on the elements it touches. Fixing the result instead would "fix" the
9
+ * batch's own mistakes — drop the attribute an agent just misspelt, and with it what it meant to say — where those
10
+ * must be refused with the reason. Every fix is said in the answer, so nothing changes that the agent is not told of.
11
+ * What has no single reading is left, and still blocks the batch as pre-existing.
12
+ */
13
+ export declare const fixTouched: (space: Space, ops: Operation[]) => {
14
+ space: Space;
15
+ fixed: string[];
16
+ };
17
+ /**
18
+ * The resulting space, read by the same checks every other door uses — `authorSpace`, the server's publish gate, the
19
+ * builder's problems panel — and held to what this batch did.
20
+ *
21
+ * - A structural error the batch introduced blocks it wherever it landed: a delete can orphan an element the batch
22
+ * never named, and a broken tree is never left for later.
23
+ * - Everything found in an element the batch touches blocks it, structural or not. One already there is labelled
24
+ * pre-existing, so the agent never takes it for its own change — and fixes it in the same batch.
25
+ * - Elements the batch never touches are not held against it.
26
+ *
27
+ * The space as it was is read only when the draft has something to say about: telling an old issue from a new one is
28
+ * the one thing that reading is for, and most batches leave nothing to tell.
29
+ */
30
+ export declare const lintDraft: (draft: Space, ops: Operation[], before?: Space) => ValidationResult;
@@ -0,0 +1,142 @@
1
+ import { findElementByRef, findPageByRef } from "../../helpers/space.js";
2
+ import { elementSourceTypes, fixSpace, lintSpace } from "@plitzi/sdk-authoring";
3
+ import { validateSchema } from "@plitzi/sdk-schema/helpers/schemaValidator";
4
+ import { styleWithoutTag } from "@plitzi/sdk-schema/helpers/styleWithoutTag";
5
+ //#region src/modules/mcp/tools/shared/lintDraft.ts
6
+ /** The elements a batch touches, by the ref each op names — what the lint of the result is held to. */
7
+ var touchedRefs = (ops) => {
8
+ const refs = /* @__PURE__ */ new Set();
9
+ for (const op of ops) switch (op.type) {
10
+ case "upsertElement":
11
+ refs.add(op.element.ref);
12
+ break;
13
+ case "patchElement":
14
+ case "moveElement":
15
+ case "upsertBinding":
16
+ case "patchBinding":
17
+ case "deleteBinding":
18
+ case "upsertInteractionFlow":
19
+ case "patchInteractionNode":
20
+ case "deleteInteraction": refs.add(op.ref);
21
+ }
22
+ return refs;
23
+ };
24
+ /** Those refs as the ids they name in `space` — a ref for an element the batch creates names nothing yet. */
25
+ var touchedIds = (space, ops) => new Set([...touchedRefs(ops)].flatMap((ref) => {
26
+ const element = findElementByRef(space.schema, ref) ?? findPageByRef(space.schema, ref);
27
+ return element ? [element.id] : [];
28
+ }));
29
+ /** The deployment's own plugins are types too; without them every one of them reads as a typo. */
30
+ var catalogsOf = (space) => ({ pluginTypes: Object.entries(space.catalog ?? {}).filter(([, entry]) => entry.custom).map(([type]) => type) });
31
+ /**
32
+ * The space as the batch should meet it: what was already wrong with the elements it is about to change, fixed where
33
+ * the problem has one reading — a typo of an attribute, a URL left in page mode, a flag written as text.
34
+ *
35
+ * On the space BEFORE the batch, and only on the elements it touches. Fixing the result instead would "fix" the
36
+ * batch's own mistakes — drop the attribute an agent just misspelt, and with it what it meant to say — where those
37
+ * must be refused with the reason. Every fix is said in the answer, so nothing changes that the agent is not told of.
38
+ * What has no single reading is left, and still blocks the batch as pre-existing.
39
+ */
40
+ var fixTouched = (space, ops) => {
41
+ const ids = touchedIds(space, ops);
42
+ if (ids.size === 0) return {
43
+ space,
44
+ fixed: []
45
+ };
46
+ const { schema, applied } = fixSpace({
47
+ schema: space.schema,
48
+ style: space.style
49
+ }, catalogsOf(space), void 0, ids);
50
+ if (applied.length === 0) return {
51
+ space,
52
+ fixed: []
53
+ };
54
+ return {
55
+ space: {
56
+ ...space,
57
+ schema
58
+ },
59
+ fixed: applied.map((fix) => `Fixed a pre-existing problem in element "${fix.elementId ?? ""}" while changing it: ${fix.message}`)
60
+ };
61
+ };
62
+ /** One reading of a space: its structure — with the source catalogue, so a binding onto nothing is caught — and its meaning. */
63
+ var readingOf = (space) => {
64
+ const structure = validateSchema(space.schema, { sourceTypes: elementSourceTypes });
65
+ const meaning = lintSpace({
66
+ schema: space.schema,
67
+ style: space.style
68
+ }, catalogsOf(space));
69
+ return {
70
+ structure,
71
+ meaning,
72
+ all: [
73
+ ...structure.errors,
74
+ ...meaning.errors,
75
+ ...meaning.warnings
76
+ ]
77
+ };
78
+ };
79
+ var keyOf = (issue) => JSON.stringify([
80
+ issue.code,
81
+ issue.elementId ?? "",
82
+ issue.message
83
+ ]);
84
+ var kindOf = (issue) => JSON.stringify([issue.code, issue.elementId ?? ""]);
85
+ var countByKind = (issues) => {
86
+ const counts = /* @__PURE__ */ new Map();
87
+ issues.forEach((issue) => counts.set(kindOf(issue), (counts.get(kindOf(issue)) ?? 0) + 1));
88
+ return counts;
89
+ };
90
+ /**
91
+ * Which of the draft's issues the space already had.
92
+ *
93
+ * The same message is the same issue. But a message also says where the element is and what it might have meant — a
94
+ * page renamed, a closer name added to the space — so it can change while the issue does not. A kind of issue on an
95
+ * element (its code there) the draft has no more of than the space had is the same issue in other words; one more of
96
+ * it than before is the batch's.
97
+ */
98
+ var preExistingIn = (before, draft) => {
99
+ const messages = new Set(before.map(keyOf));
100
+ const had = countByKind(before);
101
+ const has = countByKind(draft);
102
+ return (issue) => messages.has(keyOf(issue)) || (has.get(kindOf(issue)) ?? 0) <= (had.get(kindOf(issue)) ?? 0);
103
+ };
104
+ /**
105
+ * The resulting space, read by the same checks every other door uses — `authorSpace`, the server's publish gate, the
106
+ * builder's problems panel — and held to what this batch did.
107
+ *
108
+ * - A structural error the batch introduced blocks it wherever it landed: a delete can orphan an element the batch
109
+ * never named, and a broken tree is never left for later.
110
+ * - Everything found in an element the batch touches blocks it, structural or not. One already there is labelled
111
+ * pre-existing, so the agent never takes it for its own change — and fixes it in the same batch.
112
+ * - Elements the batch never touches are not held against it.
113
+ *
114
+ * The space as it was is read only when the draft has something to say about: telling an old issue from a new one is
115
+ * the one thing that reading is for, and most batches leave nothing to tell.
116
+ */
117
+ var lintDraft = (draft, ops, before) => {
118
+ const touched = touchedIds(draft, ops);
119
+ const { structure, meaning, all } = readingOf(draft);
120
+ const onTouched = (issue) => issue.elementId !== void 0 && touched.has(issue.elementId);
121
+ const candidates = [...structure.errors, ...meaning.errors.filter(onTouched)];
122
+ const touchedWarnings = [...structure.warnings, ...meaning.warnings].filter(onTouched);
123
+ const preExisting = before && (candidates.length > 0 || touchedWarnings.length > 0) ? preExistingIn(readingOf(before).all, all) : () => false;
124
+ const toError = (issue) => ({
125
+ path: issue.elementId ? `element "${issue.elementId}"` : "schema",
126
+ message: preExisting(issue) ? `Pre-existing malformation in element "${issue.elementId ?? ""}": ${issue.message}` : issue.message,
127
+ hint: preExisting(issue) ? "This issue already exists in the space (NOT caused by your change), but the save is blocked until you fix it too, in this same batch." : ""
128
+ });
129
+ const errors = candidates.filter((issue) => onTouched(issue) || !preExisting(issue)).map(toError);
130
+ const warnings = touchedWarnings.map((issue) => preExisting(issue) ? `Pre-existing issue in element "${issue.elementId ?? ""}": ${issue.message}` : issue.message);
131
+ for (const id of touched) {
132
+ const tagless = styleWithoutTag(draft.schema.flat[id], draft.style);
133
+ if (tagless) warnings.push(`element "${id}" ${tagless}`);
134
+ }
135
+ return {
136
+ valid: errors.length === 0,
137
+ errors,
138
+ warnings
139
+ };
140
+ };
141
+ //#endregion
142
+ export { fixTouched, lintDraft };
@@ -0,0 +1 @@
1
+ export {};
@@ -4,6 +4,7 @@ import { ResourceProxy } from '../../proxy';
4
4
  import { PreviewClient, ScreenshotClient, ScreenshotImage, Env, Persisters } from '../../types';
5
5
  import { McpUiToolMeta } from '@modelcontextprotocol/ext-apps';
6
6
  import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
7
+ import { ActionRunReport } from '@plitzi/sdk-shared';
7
8
  /** Everything a tool needs at call time: the loaded space, the target environment, and the persisters (only the
8
9
  * write tools use them). Built by whoever hosts the tools — the standalone MCP server or the in-process AI
9
10
  * engine — so a tool's behavior never touches spaceId resolution or adapters directly. `spaceId`, `preview` and
@@ -19,6 +20,9 @@ export interface ToolContext {
19
20
  * plitzi_render uses it: a widget renders inside the host's sandbox, where an undeclared origin cannot be
20
21
  * reached at all. */
21
22
  proxy?: ResourceProxy;
23
+ /** Runs one task of the space's functions in the sandbox, for the person behind the connection — when the host
24
+ * wired it. Only plitzi_try_function uses it. */
25
+ tryFunction?: (task: string, params: Record<string, unknown>) => Promise<ActionRunReport>;
22
26
  }
23
27
  /** A capability a tool depends on; the host skips registering a tool whose capability it did not wire (so
24
28
  * plitzi_screenshot simply does not appear when no browser service is configured). */
@@ -1,6 +1,2 @@
1
1
  import { ValidationCtx } from './context';
2
- import { Space } from '../../../helpers';
3
- import { BindingTransformer } from '@plitzi/sdk-shared';
4
2
  export declare const checkBindingTarget: (ref: string, category: string | undefined, to: string, path: string, ctx: ValidationCtx) => void;
5
- export declare const checkBindingSourceScope: (space: Space, ctx: ValidationCtx, elementRef: string, source: string, path: string) => void;
6
- export declare const checkBindingTransformers: (transformers: BindingTransformer[] | undefined, path: string, ctx: ValidationCtx) => void;
@@ -1,20 +1,5 @@
1
- import { getTransformer, suggestTransformer } from "../../../catalogs/index.js";
2
- import { descendantIds, findElementByRef } from "../../../helpers/space.js";
3
1
  import { warnOnce } from "./context.js";
4
- import getSourceName from "@plitzi/sdk-shared/dataSource/helpers/getSourceName";
5
2
  //#region src/modules/mcp/tools/shared/validator/bindings.ts
6
- var providerOfSource = (space, source) => {
7
- const head = source.split(".")[0];
8
- const sep = head.indexOf("_");
9
- if (sep < 0) return;
10
- const elementId = head.slice(sep + 1);
11
- const el = findElementByRef(space.schema, elementId);
12
- if (!el || getSourceName(el.definition.type, el.id) !== head) return;
13
- return {
14
- ref: el.id,
15
- id: el.id
16
- };
17
- };
18
3
  var checkBindingTarget = (ref, category, to, path, ctx) => {
19
4
  if (category !== "attributes" && category !== "initialState") return;
20
5
  const type = ctx.elementType(ref);
@@ -22,49 +7,5 @@ var checkBindingTarget = (ref, category, to, path, ctx) => {
22
7
  if (!targets || targets.size === 0 || targets.has(to)) return;
23
8
  warnOnce(ctx, `Binding target "${to}" at ${path} is not among the "${category}" targets the type "${type}" declares (${[...targets].sort().join(", ")}). Verify against plitzi://data-sources; it may still be valid.`);
24
9
  };
25
- var checkBindingSourceScope = (space, ctx, elementRef, source, path) => {
26
- const provider = providerOfSource(space, source);
27
- if (!provider) return;
28
- const bound = findElementByRef(space.schema, elementRef);
29
- if (!bound) return;
30
- if (bound.id !== provider.id && descendantIds(space.schema, provider.id).includes(bound.id)) return;
31
- ctx.errors.push({
32
- path,
33
- message: `Binding source "${source}" is provided by element "${provider.ref}", but "${elementRef}" is not inside its subtree`,
34
- hint: `An element source is scoped to its DESCENDANTS only, so this binding resolves to nothing at runtime (the source is not available outside "${provider.ref}"). Move "${elementRef}" under "${provider.ref}", or bind to a source that is in scope (e.g. a global module source like state/space/navigation).`
35
- });
36
- };
37
- var checkBindingTransformers = (transformers, path, ctx) => {
38
- transformers?.forEach((transformer, i) => {
39
- if (transformer.enabled === false) return;
40
- const base = `${path}[${i}]`;
41
- const spec = getTransformer(transformer.action);
42
- if (!spec) {
43
- const suggestion = suggestTransformer(transformer.action);
44
- ctx.errors.push({
45
- path: `${base}.action`,
46
- message: `Unknown transformer action "${transformer.action}"`,
47
- hint: `The runtime skips an unknown transformer and passes the value through unchanged.${suggestion ? ` Did you mean "${suggestion}"?` : ""} See the transformers list in plitzi://data-sources.`
48
- });
49
- return;
50
- }
51
- const params = transformer.params;
52
- const unknown = Object.keys(params).filter((key) => !(key in spec.params));
53
- if (spec.strictParams && unknown.length > 0) warnOnce(ctx, `Transformer "${transformer.action}" at ${base} got unknown param(s) ${unknown.map((k) => `"${k}"`).join(", ")} — the runtime ignores them. Valid params: ${Object.keys(spec.params).join(", ") || "(none)"}.`);
54
- for (const [key, param] of Object.entries(spec.params)) {
55
- if (param.required && !(key in params)) ctx.errors.push({
56
- path: `${base}.params.${key}`,
57
- message: `Transformer "${transformer.action}" is missing required param "${key}"`,
58
- hint: param.description
59
- });
60
- if (param.type === "select" && param.options && key in params && !param.options.includes(String(params[key]))) ctx.errors.push({
61
- path: `${base}.params.${key}`,
62
- message: `Transformer "${transformer.action}" param "${key}" is "${params[key]}", not one of its options`,
63
- hint: `Use one of: ${param.options.join(", ")}.`,
64
- validValues: param.options
65
- });
66
- }
67
- });
68
- };
69
10
  //#endregion
70
- export { checkBindingSourceScope, checkBindingTarget, checkBindingTransformers };
11
+ export { checkBindingTarget };
@@ -1,23 +1,13 @@
1
1
  import { definitionVariantNames } from "../../operations/style/translator.js";
2
- import { RAW_CODE_TYPES, checkVarRefs, warnOnce } from "./context.js";
2
+ import { RAW_CODE_TYPES, warnOnce } from "./context.js";
3
3
  import { checkIdRef } from "./refs.js";
4
4
  //#region src/modules/mcp/tools/shared/validator/elements.ts
5
5
  var checkTypeProps = (type, props, path, ctx) => {
6
- if (!props || RAW_CODE_TYPES.has(type)) return;
7
6
  const meta = ctx.typeMeta.get(type);
7
+ if (!props || RAW_CODE_TYPES.has(type) || meta?.custom === false) return;
8
8
  const known = meta && meta.attributes.size > 0 ? meta.attributes : ctx.typeProps.get(type);
9
9
  if (!known || known.size === 0) return;
10
- const strict = meta?.custom === false;
11
- for (const key of Object.keys(props)) {
12
- if (key === "subType" || known.has(key)) continue;
13
- if (strict) ctx.errors.push({
14
- path,
15
- message: `Type "${type}" has no attribute "${key}"`,
16
- hint: `Valid attributes: ${[...known].sort().join(", ")}`,
17
- validValues: [...known].sort()
18
- });
19
- else warnOnce(ctx, `Type "${type}" has no observed prop "${key}" at ${path} (known: ${[...known].sort().join(", ")}). It may still be valid — verify against plitzi://types.`);
20
- }
10
+ for (const key of Object.keys(props)) if (key !== "subType" && !known.has(key)) warnOnce(ctx, `Type "${type}" has no observed prop "${key}" at ${path} (known: ${[...known].sort().join(", ")}). It may still be valid — verify against plitzi://types.`);
21
11
  };
22
12
  var checkVariantApplication = (initialState, path, ctx) => {
23
13
  for (const [cls, selectors] of Object.entries(initialState?.styleVariant ?? {})) {
@@ -43,8 +33,6 @@ var checkRawMarkup = (type, props, path, ctx) => {
43
33
  };
44
34
  var checkElementProps = (element, path, ctx) => {
45
35
  checkRawMarkup(element.type, element.props, path, ctx);
46
- if (!element.props || RAW_CODE_TYPES.has(element.type)) return;
47
- for (const [key, value] of Object.entries(element.props)) if (typeof value === "string") checkVarRefs(value, `${path}.props.${key}`, ctx);
48
36
  checkTypeProps(element.type, element.props, path, ctx);
49
37
  };
50
38
  var checkElementInput = (element, path, ctx, seen) => {
@@ -1,8 +1,5 @@
1
- import { ValidationCtx, ValidationMode } from './context';
1
+ import { ValidationMode } from './context';
2
2
  import { Space } from '../../../helpers';
3
3
  import { ValidationResult } from '../../../types';
4
4
  import { Operation } from '../../operations';
5
- /** The shared validation context, derived from a space (+ the batch's ops, for batch-declared names). Extracted so
6
- * the post-apply resource audit (auditResources) can run the same checks against the resulting draft. */
7
- export declare const buildValidationCtx: (space: Space, ops: Operation[], mode?: ValidationMode) => ValidationCtx;
8
5
  export declare const validateOperations: (space: Space, ops: Operation[], mode?: ValidationMode) => ValidationResult;
@@ -4,8 +4,8 @@ import { elementTypeNames } from "../../../catalogs/index.js";
4
4
  import { findElementByRef, findFolderByRef, findPageByRef, findRootByRef, folderAncestorIds, getLayoutElements, getPageElements, pageFoldersOf, resolveRef, routeParamNames } from "../../../helpers/space.js";
5
5
  import { MAX_OPS } from "../../operations/index.js";
6
6
  import { batchDeclaredElements, batchDeclaredFolders, batchDeclaredPages, batchDeclaredVariants, batchDeclaredVars } from "./batch.js";
7
- import { checkObservedName, checkVarRefs, warnOnce } from "./context.js";
8
- import { checkBindingSourceScope, checkBindingTarget, checkBindingTransformers } from "./bindings.js";
7
+ import { checkObservedName, warnOnce } from "./context.js";
8
+ import { checkBindingTarget } from "./bindings.js";
9
9
  import { batchDeclaredConnectors, checkConnectorOp, checkProviderElement } from "./connectors.js";
10
10
  import { checkSlotCss } from "./css.js";
11
11
  import { checkRef } from "./refs.js";
@@ -38,8 +38,7 @@ var buildTypeMeta = (catalog) => {
38
38
  }
39
39
  return meta;
40
40
  };
41
- /** The shared validation context, derived from a space (+ the batch's ops, for batch-declared names). Extracted so
42
- * the post-apply resource audit (auditResources) can run the same checks against the resulting draft. */
41
+ /** The shared validation context, derived from a space (+ the batch's ops, for batch-declared names). */
43
42
  var buildValidationCtx = (space, ops, mode = "space") => {
44
43
  const registry = buildTypeRegistry(space.schema, space.catalog);
45
44
  const batchElements = batchDeclaredElements(ops);
@@ -103,12 +102,9 @@ var validateOperations = (space, ops, mode = "space") => {
103
102
  checkRef(op.ref, `${base}.ref`, ctx);
104
103
  const page = findRootByRef(space.schema, op.pageRef);
105
104
  const target = page ? resolveRef(space.schema, page, op.ref) : void 0;
106
- if (op.props) {
107
- for (const [key, value] of Object.entries(op.props)) if (typeof value === "string") checkVarRefs(value, `${base}.props.${key}`, ctx);
108
- if (target && target.id !== page?.id) {
109
- checkRawMarkup(target.definition.type, op.props, base, ctx);
110
- checkTypeProps(target.definition.type, op.props, base, ctx);
111
- }
105
+ if (op.props && target && target.id !== page?.id) {
106
+ checkRawMarkup(target.definition.type, op.props, base, ctx);
107
+ checkTypeProps(target.definition.type, op.props, base, ctx);
112
108
  }
113
109
  checkVariantApplication(op.initialState, `${base}.initialState`, ctx);
114
110
  if (target && target.definition.type === PROVIDER_TYPE) checkProviderElement(space, ctx, op.ref, op.runtime ?? target.definition.runtime, {
@@ -202,26 +198,17 @@ var validateOperations = (space, ops, mode = "space") => {
202
198
  checkRef(op.ref, `${base}.ref`, ctx);
203
199
  checkObservedName(op.binding.source, ctx.observedSources, "Data source", "plitzi://data-sources", `${base}.binding.source`, ctx);
204
200
  checkBindingTarget(op.ref, op.category, op.binding.to, `${base}.binding.to`, ctx);
205
- checkBindingSourceScope(space, ctx, op.ref, op.binding.source, `${base}.binding.source`);
206
- checkBindingTransformers(op.binding.transformers, `${base}.binding.transformers`, ctx);
207
201
  break;
208
202
  case "patchBinding":
209
203
  checkRef(op.ref, `${base}.ref`, ctx);
210
204
  checkObservedName(op.source, ctx.observedSources, "Data source", "plitzi://data-sources", `${base}.source`, ctx);
211
205
  checkBindingTarget(op.ref, op.category, op.to, `${base}.to`, ctx);
212
- if (op.source !== void 0) checkBindingSourceScope(space, ctx, op.ref, op.source, `${base}.source`);
213
- checkBindingTransformers(op.transformers, `${base}.transformers`, ctx);
214
206
  break;
215
207
  case "deleteBinding":
216
208
  checkRef(op.ref, `${base}.ref`, ctx);
217
209
  break;
218
210
  case "upsertInteractionFlow":
219
211
  checkRef(op.ref, `${base}.ref`, ctx);
220
- if (op.nodes[0] && op.nodes[0].nodeType !== "trigger") ctx.errors.push({
221
- path: `${base}.nodes[0].nodeType`,
222
- message: "The first node of a flow must be a trigger",
223
- hint: "Put the trigger first; the callbacks/utilities that run after it follow in order"
224
- });
225
212
  {
226
213
  const named = /* @__PURE__ */ new Set();
227
214
  op.nodes.forEach((node, n) => {
@@ -269,6 +256,24 @@ var validateOperations = (space, ops, mode = "space") => {
269
256
  case "patchConnector":
270
257
  checkRef(op.ref, `${base}.ref`, ctx);
271
258
  break;
259
+ case "patchSettings":
260
+ ["transientState", "paintedState"].forEach((setting) => {
261
+ (op[setting] ?? []).forEach((key, index) => {
262
+ if (key.trim() === "" || key.includes(".")) ctx.errors.push({
263
+ path: `${base}.${setting}[${index}]`,
264
+ message: `"${key}" is not a top-level state key: no dots, not empty`,
265
+ hint: `Name the key setState writes — "${key.split(".")[0] || "tourStep"}" covers everything under it`
266
+ });
267
+ });
268
+ });
269
+ (op.paintedState ?? []).forEach((key, index) => {
270
+ if (op.transientState?.includes(key)) ctx.errors.push({
271
+ path: `${base}.paintedState[${index}]`,
272
+ message: `"${key}" is in both paintedState and transientState`,
273
+ hint: "A painted key is kept so the server can draw with it; a transient one never is — remove it from one"
274
+ });
275
+ });
276
+ break;
272
277
  case "deleteInteraction":
273
278
  checkRef(op.ref, `${base}.ref`, ctx);
274
279
  if (Boolean(op.flowId) === Boolean(op.nodeId)) ctx.errors.push({
@@ -285,4 +290,4 @@ var validateOperations = (space, ops, mode = "space") => {
285
290
  };
286
291
  };
287
292
  //#endregion
288
- export { buildValidationCtx, validateOperations };
293
+ export { validateOperations };