@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.
- package/CHANGELOG.md +1156 -0
- package/README.md +6 -2
- package/dist/createServer.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/modules/mcp/accessibilityOutline.d.ts +38 -0
- package/dist/modules/mcp/accessibilityOutline.js +75 -0
- package/dist/modules/mcp/accessibilityOutline.test.d.ts +1 -0
- package/dist/modules/mcp/catalogs/index.d.ts +2 -2
- package/dist/modules/mcp/catalogs/index.js +2 -2
- package/dist/modules/mcp/catalogs/observed.js +2 -2
- package/dist/modules/mcp/catalogs/pluginCatalog.d.ts +6 -3
- package/dist/modules/mcp/catalogs/pluginCatalog.js +12 -7
- package/dist/modules/mcp/helpers/agentPrompt.js +22 -5
- package/dist/modules/mcp/helpers/guide.d.ts +2 -2
- package/dist/modules/mcp/helpers/guide.js +212 -48
- package/dist/modules/mcp/helpers/interactions.d.ts +2 -1
- package/dist/modules/mcp/helpers/interactions.js +2 -0
- package/dist/modules/mcp/helpers/space.d.ts +6 -4
- package/dist/modules/mcp/helpers/space.js +7 -20
- package/dist/modules/mcp/helpers/uris.d.ts +3 -1
- package/dist/modules/mcp/helpers/uris.js +4 -1
- package/dist/modules/mcp/index.d.ts +2 -0
- package/dist/modules/mcp/localScreenshotClient.d.ts +11 -0
- package/dist/modules/mcp/localScreenshotClient.js +24 -9
- package/dist/modules/mcp/proxy/guard.d.ts +6 -0
- package/dist/modules/mcp/proxy/guard.js +8 -12
- package/dist/modules/mcp/resources/functions.d.ts +8 -0
- package/dist/modules/mcp/resources/functions.js +35 -0
- package/dist/modules/mcp/resources/register.d.ts +2 -1
- package/dist/modules/mcp/resources/register.js +49 -2
- package/dist/modules/mcp/resources/renderGuide.js +3 -2
- package/dist/modules/mcp/resources/router.js +3 -1
- package/dist/modules/mcp/screenshotClient.js +12 -4
- package/dist/modules/mcp/server.js +20 -10
- package/dist/modules/mcp/tests/guide.test.d.ts +1 -0
- package/dist/modules/mcp/tools/apply/dispatch.js +9 -1
- package/dist/modules/mcp/tools/apply/index.d.ts +34 -0
- package/dist/modules/mcp/tools/apply/index.js +35 -70
- package/dist/modules/mcp/tools/index.js +4 -2
- package/dist/modules/mcp/tools/operations/connectors/manifest.d.ts +0 -1
- package/dist/modules/mcp/tools/operations/functions/functions.test.d.ts +1 -0
- package/dist/modules/mcp/tools/operations/functions/index.d.ts +31 -0
- package/dist/modules/mcp/tools/operations/functions/index.js +48 -0
- package/dist/modules/mcp/tools/operations/index.d.ts +122 -2
- package/dist/modules/mcp/tools/operations/index.js +4 -3
- package/dist/modules/mcp/tools/operations/schema/elements/deleteElement.js +5 -6
- package/dist/modules/mcp/tools/operations/schema/elements/moveElement.js +3 -11
- package/dist/modules/mcp/tools/operations/schema/elements/upsertElement.js +6 -9
- package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.d.ts +5 -0
- package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.js +3 -1
- package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.d.ts +5 -0
- package/dist/modules/mcp/tools/operations/schema/operations.d.ts +27 -0
- package/dist/modules/mcp/tools/operations/schema/pages/deleteLayout.js +6 -5
- package/dist/modules/mcp/tools/operations/schema/pages/deletePage.js +6 -5
- package/dist/modules/mcp/tools/operations/schema/settings/patchSettings.d.ts +17 -0
- package/dist/modules/mcp/tools/operations/schema/settings/patchSettings.js +34 -5
- package/dist/modules/mcp/tools/operations/schema/shared.d.ts +6 -0
- package/dist/modules/mcp/tools/operations/schema/shared.js +5 -0
- package/dist/modules/mcp/tools/operations/schema/translator.js +3 -2
- package/dist/modules/mcp/tools/operations/schema/write.d.ts +10 -5
- package/dist/modules/mcp/tools/operations/schema/write.js +16 -16
- package/dist/modules/mcp/tools/preview.d.ts +27 -0
- package/dist/modules/mcp/tools/render.d.ts +27 -0
- package/dist/modules/mcp/tools/render.js +7 -43
- package/dist/modules/mcp/tools/screenshot.d.ts +39 -0
- package/dist/modules/mcp/tools/screenshot.js +40 -5
- package/dist/modules/mcp/tools/shared/draftBatch.d.ts +25 -0
- package/dist/modules/mcp/tools/shared/draftBatch.js +58 -0
- package/dist/modules/mcp/tools/shared/draftBatch.test.d.ts +1 -0
- package/dist/modules/mcp/tools/shared/lintDraft.d.ts +30 -0
- package/dist/modules/mcp/tools/shared/lintDraft.js +142 -0
- package/dist/modules/mcp/tools/shared/lintDraft.test.d.ts +1 -0
- package/dist/modules/mcp/tools/shared/tool.d.ts +4 -0
- package/dist/modules/mcp/tools/shared/validator/bindings.d.ts +0 -4
- package/dist/modules/mcp/tools/shared/validator/bindings.js +1 -60
- package/dist/modules/mcp/tools/shared/validator/elements.js +3 -15
- package/dist/modules/mcp/tools/shared/validator/index.d.ts +1 -4
- package/dist/modules/mcp/tools/shared/validator/index.js +25 -20
- package/dist/modules/mcp/tools/shared/validator/interactions.js +3 -58
- package/dist/modules/mcp/tools/tryFunction.d.ts +18 -0
- package/dist/modules/mcp/tools/tryFunction.js +43 -0
- package/dist/modules/mcp/tools/validate.d.ts +34 -0
- package/dist/modules/mcp/tools/validate.js +9 -26
- package/dist/modules/mcp/types/aiSchema.d.ts +8 -1
- package/dist/modules/mcp/types/previewTypes.d.ts +6 -0
- package/dist/modules/mcp/types/screenshotTypes.d.ts +16 -0
- package/dist/modules/mcp/types/toolTypes.d.ts +5 -1
- package/dist/preview/createPreview.js +5 -4
- package/dist/stages/preview.js +15 -1
- package/package.json +11 -10
- package/skills/plitzi-render/SKILL.md +1 -2
- package/dist/modules/mcp/tools/shared/validator/audit.d.ts +0 -4
- 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
|
-
|
|
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 {
|
|
11
|
+
export { checkBindingTarget };
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
import { definitionVariantNames } from "../../operations/style/translator.js";
|
|
2
|
-
import { RAW_CODE_TYPES,
|
|
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
|
|
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 {
|
|
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,
|
|
8
|
-
import {
|
|
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).
|
|
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
|
-
|
|
108
|
-
|
|
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 {
|
|
293
|
+
export { validateOperations };
|