@plitzi/sdk-mcp 0.38.7 → 0.38.8

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 (66) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/README.md +9 -9
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +2 -3
  5. package/dist/modules/mcp/helpers/guide.js +37 -21
  6. package/dist/modules/mcp/index.d.ts +2 -2
  7. package/dist/modules/mcp/server.d.ts +4 -4
  8. package/dist/modules/mcp/server.js +10 -5
  9. package/dist/modules/mcp/tests/cheapMistakes.test.d.ts +1 -0
  10. package/dist/modules/mcp/tests/helpers.d.ts +9 -2
  11. package/dist/modules/mcp/tests/intents.test.d.ts +1 -0
  12. package/dist/modules/mcp/tests/look.test.d.ts +1 -0
  13. package/dist/modules/mcp/tools/apply/index.d.ts +64 -50
  14. package/dist/modules/mcp/tools/apply/index.js +70 -20
  15. package/dist/modules/mcp/tools/describeOperation.d.ts +11 -0
  16. package/dist/modules/mcp/tools/describeOperation.js +40 -0
  17. package/dist/modules/mcp/tools/describeOperation.test.d.ts +1 -0
  18. package/dist/modules/mcp/tools/index.d.ts +1 -6
  19. package/dist/modules/mcp/tools/index.js +12 -9
  20. package/dist/modules/mcp/tools/intents.d.ts +5 -0
  21. package/dist/modules/mcp/tools/intents.js +201 -0
  22. package/dist/modules/mcp/tools/look.d.ts +17 -0
  23. package/dist/modules/mcp/tools/look.js +32 -0
  24. package/dist/modules/mcp/tools/operations/closed.d.ts +7 -0
  25. package/dist/modules/mcp/tools/operations/closed.js +29 -0
  26. package/dist/modules/mcp/tools/operations/index.d.ts +229 -180
  27. package/dist/modules/mcp/tools/operations/index.js +72 -51
  28. package/dist/modules/mcp/tools/operations/schema/bindings/upsertBinding.d.ts +1 -1
  29. package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.d.ts +1 -1
  30. package/dist/modules/mcp/tools/operations/schema/operations.d.ts +2 -2
  31. package/dist/modules/mcp/tools/operations/schema/shared.d.ts +2 -2
  32. package/dist/modules/mcp/tools/operations/schema/shared.js +7 -6
  33. package/dist/modules/mcp/tools/operations/schemaIds.js +4 -4
  34. package/dist/modules/mcp/tools/render.d.ts +40 -40
  35. package/dist/modules/mcp/tools/render.js +5 -1
  36. package/dist/modules/mcp/tools/shared/draftBatch.d.ts +3 -2
  37. package/dist/modules/mcp/tools/shared/draftBatch.js +16 -5
  38. package/dist/modules/mcp/tools/shared/effects.d.ts +2 -0
  39. package/dist/modules/mcp/tools/shared/effects.js +155 -0
  40. package/dist/modules/mcp/tools/shared/effects.test.d.ts +1 -0
  41. package/dist/modules/mcp/tools/shared/look.d.ts +32 -0
  42. package/dist/modules/mcp/tools/shared/look.js +128 -0
  43. package/dist/modules/mcp/tools/shared/normalizeOperations.d.ts +6 -0
  44. package/dist/modules/mcp/tools/shared/normalizeOperations.js +37 -0
  45. package/dist/modules/mcp/tools/shared/repeats.d.ts +13 -0
  46. package/dist/modules/mcp/tools/shared/repeats.js +38 -0
  47. package/dist/modules/mcp/tools/shared/tool.d.ts +9 -6
  48. package/dist/modules/mcp/tools/shared/tool.js +2 -1
  49. package/dist/modules/mcp/tools/shared/validator/context.d.ts +0 -1
  50. package/dist/modules/mcp/tools/shared/validator/elements.js +0 -1
  51. package/dist/modules/mcp/tools/shared/validator/index.js +25 -2
  52. package/dist/modules/mcp/types/screenshotTypes.d.ts +3 -3
  53. package/dist/modules/mcp/types/toolTypes.d.ts +2 -4
  54. package/dist/options.d.ts +4 -5
  55. package/dist/preview/createPreview.js +9 -18
  56. package/package.json +7 -7
  57. package/skills/plitzi-render/SKILL.md +8 -184
  58. package/skills/plitzi-render/reference/good-call.md +88 -0
  59. package/skills/plitzi-render/reference/interactive.md +64 -0
  60. package/skills/plitzi-render/reference/looks.md +29 -0
  61. package/dist/modules/mcp/tools/preview.d.ts +0 -1423
  62. package/dist/modules/mcp/tools/preview.js +0 -37
  63. package/dist/modules/mcp/tools/screenshot.d.ts +0 -1708
  64. package/dist/modules/mcp/tools/screenshot.js +0 -127
  65. package/dist/modules/mcp/tools/validate.d.ts +0 -1699
  66. package/dist/modules/mcp/tools/validate.js +0 -32
@@ -2,14 +2,16 @@ import { cloneSpace } from "../../helpers/space.js";
2
2
  import { expandOperations } from "./expandOperations.js";
3
3
  import { fixTouched, lintDraft } from "./lintDraft.js";
4
4
  import { newSuggestions } from "./newSuggestions.js";
5
+ import { normalizeOperations } from "./normalizeOperations.js";
5
6
  import { validateOperations } from "./validator/index.js";
6
7
  import { applyOperations } from "../apply/dispatch.js";
7
8
  //#region src/modules/mcp/tools/shared/draftBatch.ts
8
9
  /**
9
10
  * A batch run on a copy of the space and read back — the one way every tool that takes operations handles them, so
10
- * `plitzi_validate` answers exactly what `plitzi_apply` would, and `plitzi_render` holds a widget to the same rules.
11
+ * a `dryRun` answers exactly what `plitzi_apply` would save, and `plitzi_render` holds a widget to the same rules.
11
12
  *
12
- * In order: the sugar ops expanded; what was already wrong with the elements the batch touches fixed where it has one
13
+ * In order: the sugar ops expanded; what has one reading read that way (`normalizeOperations`, said in `warnings`);
14
+ * what was already wrong with the elements the batch touches fixed where it has one
13
15
  * reading (`fixTouched`, each fix said in `warnings`); each op checked against the input it takes; the batch applied to
14
16
  * the copy; and the copy read by `lintDraft` — structure and meaning, the same reading every other writer gets. The
15
17
  * first stage to refuse ends it; the space handed in is never touched.
@@ -21,23 +23,32 @@ var draftBatch = (space, env, operations, mode = "space") => {
21
23
  errors: expansion.errors,
22
24
  warnings: []
23
25
  };
24
- const ops = expansion.operations;
26
+ const { operations: ops, notes } = normalizeOperations(space, expansion.operations);
25
27
  const prepared = fixTouched(space, ops);
26
28
  const validation = validateOperations(prepared.space, ops, mode);
27
29
  if (!validation.valid) return {
28
30
  ok: false,
29
31
  errors: validation.errors,
30
- warnings: [...prepared.fixed, ...validation.warnings]
32
+ warnings: [
33
+ ...notes,
34
+ ...prepared.fixed,
35
+ ...validation.warnings
36
+ ]
31
37
  };
32
38
  const draft = cloneSpace(prepared.space);
33
39
  const outcome = applyOperations(draft, env, ops);
34
40
  if (outcome.errors.length > 0) return {
35
41
  ok: false,
36
42
  errors: outcome.errors,
37
- warnings: [...prepared.fixed, ...validation.warnings]
43
+ warnings: [
44
+ ...notes,
45
+ ...prepared.fixed,
46
+ ...validation.warnings
47
+ ]
38
48
  };
39
49
  const reading = lintDraft(draft, ops, prepared.space);
40
50
  const warnings = [
51
+ ...notes,
41
52
  ...prepared.fixed,
42
53
  ...validation.warnings,
43
54
  ...reading.warnings
@@ -0,0 +1,2 @@
1
+ import { Space } from '../../helpers';
2
+ export declare const effectsOf: (before: Space, after: Space) => string[];
@@ -0,0 +1,155 @@
1
+ import { canonicalJson } from "@plitzi/sdk-shared/helpers/canonicalJson";
2
+ import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
3
+ //#region src/modules/mcp/tools/shared/effects.ts
4
+ /**
5
+ * What a write did to the space, read off the documents before and after it — never off what was asked. An agent
6
+ * that is told only what it asked for cannot see the element a shared class also restyled, the child a move took with
7
+ * it, or the operation that changed nothing; every one of those is a line here. Past `LIMIT` lines the rest is
8
+ * counted, never dropped unsaid.
9
+ */
10
+ var LIMIT = 40;
11
+ var SHOWN = 60;
12
+ var same = (before, after) => canonicalJson(before) === canonicalJson(after);
13
+ var shown = (value) => {
14
+ const text = canonicalJson(value);
15
+ return text.length > SHOWN ? `${text.slice(0, 59)}…` : text;
16
+ };
17
+ /** Each entry of a list by what names it — its `id`, or its `name` — when every entry has one. */
18
+ var keyedList = (list) => {
19
+ const keyed = /* @__PURE__ */ new Map();
20
+ for (const entry of list) {
21
+ const key = isRecord(entry) ? entry.id ?? entry.name : void 0;
22
+ if (typeof key !== "string" || keyed.has(key)) return;
23
+ keyed.set(key, entry);
24
+ }
25
+ return keyed;
26
+ };
27
+ var isScalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean";
28
+ /** Every difference between two values, at its path: a key added, removed or changed, an entry of a named list. */
29
+ var differences = (path, before, after, out) => {
30
+ if (same(before, after)) return;
31
+ if (before === void 0) {
32
+ out.push(`${path} = ${shown(after)}`);
33
+ return;
34
+ }
35
+ if (after === void 0) {
36
+ out.push(`${path} removed (was ${shown(before)})`);
37
+ return;
38
+ }
39
+ if (Array.isArray(before) && Array.isArray(after)) {
40
+ const was = keyedList(before);
41
+ const is = keyedList(after);
42
+ if (was && is) {
43
+ for (const key of /* @__PURE__ */ new Set([...was.keys(), ...is.keys()])) differences(`${path}[${key}]`, was.get(key), is.get(key), out);
44
+ if ([...was.keys()].join("\n") !== [...is.keys()].join("\n") && same([...was.keys()].toSorted(), [...is.keys()].toSorted())) out.push(`${path} reordered`);
45
+ return;
46
+ }
47
+ if (before.every(isScalar) && after.every(isScalar)) {
48
+ const added = after.filter((item) => !before.includes(item));
49
+ const removed = before.filter((item) => !after.includes(item));
50
+ if (added.length > 0 || removed.length > 0) out.push(`${path}: ${[...added.map((item) => `+${shown(item)}`), ...removed.map((item) => `−${shown(item)}`)].join(" ")}`);
51
+ else out.push(`${path}: ${shown(before)} → ${shown(after)}`);
52
+ return;
53
+ }
54
+ }
55
+ if (isRecord(before) && isRecord(after)) {
56
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) differences(`${path}.${key}`, before[key], after[key], out);
57
+ return;
58
+ }
59
+ out.push(`${path}: ${shown(before)} → ${shown(after)}`);
60
+ };
61
+ /** A key of a record, or nothing when it has none — said as such, where indexing would claim it is there. */
62
+ var lookup = (record, key) => Object.hasOwn(record, key) ? record[key] : void 0;
63
+ /** An object without the keys a caller reads apart. */
64
+ var without = (value, keys) => Object.fromEntries(Object.entries(value).filter(([key]) => !keys.includes(key)));
65
+ var classesOf = (selector) => (selector ?? "").split(/\s+/).filter(Boolean);
66
+ /** An element's own changes: its attributes, its classes, where it is, and the rest of its definition. */
67
+ var elementChanges = (id, before, after, out) => {
68
+ const bound = new Map((after.definition.bindings?.attributes ?? []).map((binding) => [binding.to, binding.source]));
69
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(before.attributes), ...Object.keys(after.attributes)])) {
70
+ const lines = [];
71
+ differences(`${id}.${key}`, before.attributes[key], after.attributes[key], lines);
72
+ const source = bound.get(key);
73
+ out.push(...source ? lines.map((line) => `${line} — bound to ${source}: the page shows the binding's value`) : lines);
74
+ }
75
+ const { parentId: wasIn, items: hadItems, styleSelectors: wore } = before.definition;
76
+ const { parentId: isIn, items: hasItems, styleSelectors: wears } = after.definition;
77
+ if (wasIn !== isIn) out.push(`${id} moved from ${wasIn ?? "the top"} to ${isIn ?? "the top"}`);
78
+ if (!same(hadItems, hasItems) && same([...hadItems ?? []].toSorted(), [...hasItems ?? []].toSorted())) out.push(`${id}'s children reordered`);
79
+ for (const selector of /* @__PURE__ */ new Set([...Object.keys(wore), ...Object.keys(wears)])) {
80
+ const was = classesOf(wore[selector]);
81
+ const is = classesOf(wears[selector]);
82
+ const added = is.filter((name) => !was.includes(name));
83
+ const removed = was.filter((name) => !is.includes(name));
84
+ if (added.length > 0 || removed.length > 0) {
85
+ const changes = [...added.map((name) => `+${name}`), ...removed.map((name) => `−${name}`)].join(" ");
86
+ out.push(`${id} classes${selector === "base" ? "" : ` (${selector})`}: ${changes}`);
87
+ } else if (was.join(" ") !== is.join(" ")) out.push(`${id} classes${selector === "base" ? "" : ` (${selector})`} reordered`);
88
+ }
89
+ const apart = [
90
+ "parentId",
91
+ "items",
92
+ "styleSelectors"
93
+ ];
94
+ differences(id, without(before.definition, apart), without(after.definition, apart), out);
95
+ };
96
+ /** The elements added to a tree, or taken from it, said by the outermost of each subtree with how many it held. */
97
+ var subtrees = (ids, flat, verb, where, out) => {
98
+ const top = (id) => {
99
+ const parentId = flat[id].definition.parentId;
100
+ return parentId !== void 0 && ids.has(parentId) ? top(parentId) : id;
101
+ };
102
+ const held = /* @__PURE__ */ new Map();
103
+ for (const id of ids) {
104
+ const root = top(id);
105
+ held.set(root, (held.get(root) ?? 0) + (root === id ? 0 : 1));
106
+ }
107
+ for (const [id, inside] of held) {
108
+ const { type, parentId } = flat[id].definition;
109
+ out.push(`${id} (${type}) ${verb}${parentId ? ` ${verb === "added" ? "in" : "from"} ${parentId}` : ""}${where}${inside > 0 ? `, with ${String(inside)} inside` : ""}`);
110
+ }
111
+ };
112
+ var treeChanges = (before, after, where, out) => {
113
+ const added = new Set(Object.keys(after).filter((id) => !(id in before)));
114
+ const removed = new Set(Object.keys(before).filter((id) => !(id in after)));
115
+ subtrees(added, after, "added", where, out);
116
+ subtrees(removed, before, "removed", where, out);
117
+ for (const id of Object.keys(after)) {
118
+ const was = lookup(before, id);
119
+ if (was) elementChanges(id, was, after[id], out);
120
+ }
121
+ };
122
+ var effectsOf = (before, after) => {
123
+ const out = [];
124
+ treeChanges(before.schema.flat, after.schema.flat, "", out);
125
+ const components = /* @__PURE__ */ new Set([...Object.keys(before.schema.components), ...Object.keys(after.schema.components)]);
126
+ for (const id of components) {
127
+ const was = lookup(before.schema.components, id);
128
+ const is = lookup(after.schema.components, id);
129
+ if (!was || !is) {
130
+ const held = Object.keys((was ?? is)?.flat ?? {}).length;
131
+ out.push(`component ${id} ${was ? "removed" : "added"}, ${String(held)} elements`);
132
+ continue;
133
+ }
134
+ treeChanges(was.flat, is.flat, ` (component ${id})`, out);
135
+ differences(`component ${id}`, without(was, ["flat"]), without(is, ["flat"]), out);
136
+ }
137
+ differences("space", without(before.schema, [
138
+ "flat",
139
+ "components",
140
+ "pages"
141
+ ]), without(after.schema, [
142
+ "flat",
143
+ "components",
144
+ "pages"
145
+ ]), out);
146
+ differences("space.pages", before.schema.pages.filter((id) => after.schema.pages.includes(id)), after.schema.pages.filter((id) => before.schema.pages.includes(id)), out);
147
+ differences("style", without(before.style, ["cache"]), without(after.style, ["cache"]), out);
148
+ differences("connectors", before.connectors, after.connectors, out);
149
+ differences("actions", before.actions, after.actions, out);
150
+ differences("functions", before.functions, after.functions, out);
151
+ differences("data", before.data, after.data, out);
152
+ return out.length > LIMIT ? [...out.slice(0, LIMIT), `… and ${String(out.length - LIMIT)} more changes: read the resources listed in \`changed\``] : out;
153
+ };
154
+ //#endregion
155
+ export { effectsOf };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ import { ScreenshotImage } from '../../types';
3
+ import { Operation } from '../operations';
4
+ import { ToolContext } from './tool';
5
+ type LookViewport = 'desktop' | 'mobile' | 'both';
6
+ type LookView = 'html' | 'image' | 'accessibility' | 'both';
7
+ export declare const pageRef: z.ZodOptional<z.ZodString>;
8
+ export declare const viewport: z.ZodOptional<z.ZodEnum<{
9
+ desktop: "desktop";
10
+ mobile: "mobile";
11
+ both: "both";
12
+ }>>;
13
+ export declare const fullPage: z.ZodOptional<z.ZodBoolean>;
14
+ type LookRequest = {
15
+ pageRef?: string;
16
+ /** A batch to render without saving it; left out, what is saved. */
17
+ operations?: Operation[];
18
+ view: LookView;
19
+ viewport?: LookViewport;
20
+ fullPage?: boolean;
21
+ };
22
+ /** What a look found: the facts about the page, and the pictures when one was asked for and could be taken. */
23
+ type Look = {
24
+ meta: Record<string, unknown>;
25
+ images: ScreenshotImage[];
26
+ };
27
+ /** A look that could not happen at all — no renderer, or a batch the renderer refused — said as the tools say it. */
28
+ type LookRefusal = {
29
+ refused: Record<string, unknown>;
30
+ };
31
+ export declare const lookAt: (ctx: ToolContext, request: LookRequest) => Promise<Look | LookRefusal>;
32
+ export {};
@@ -0,0 +1,128 @@
1
+ import { unnamedControls } from "../../accessibilityOutline.js";
2
+ import { z } from "zod";
3
+ //#region src/modules/mcp/tools/shared/look.ts
4
+ /**
5
+ * How a page looks — as HTML, as a picture, or as a screen reader and a browser agent read it — rendered from the saved
6
+ * space or from a batch that was never saved.
7
+ *
8
+ * One path for every tool that shows a page: `plitzi_apply`'s `dryRun` (the batch it just checked, so the agent writes
9
+ * its operations once instead of once to check, once to look and once to save), and `plitzi_look` (what is saved).
10
+ */
11
+ var VIEWPORTS = {
12
+ desktop: {
13
+ label: "desktop",
14
+ width: 1440,
15
+ height: 900
16
+ },
17
+ mobile: {
18
+ label: "mobile",
19
+ width: 390,
20
+ height: 844
21
+ }
22
+ };
23
+ var resolveViewports = (choice) => {
24
+ if (choice === "both") return [VIEWPORTS.desktop, VIEWPORTS.mobile];
25
+ if (choice === "mobile") return [VIEWPORTS.mobile];
26
+ return [VIEWPORTS.desktop];
27
+ };
28
+ var CAPTURES = {
29
+ image: ["image"],
30
+ accessibility: ["accessibility"],
31
+ both: ["image", "accessibility"]
32
+ };
33
+ var pageRef = z.string().optional().describe("The page, by name; defaults to the space default page");
34
+ var viewport = z.enum([
35
+ "desktop",
36
+ "mobile",
37
+ "both"
38
+ ]).optional().describe("Which viewport(s) to capture. \"both\" catches responsive issues (e.g. overflow at one width). Default desktop.");
39
+ var fullPage = z.boolean().optional().describe("Capture the full scrollable page instead of only the visible viewport. Default false.");
40
+ /** Each viewport's outline, with what in it has no name — the part an agent acts on. */
41
+ var readingOf = (outlines) => outlines.map(({ label, outline }) => ({
42
+ viewport: label,
43
+ outline,
44
+ unnamed: unnamedControls(outline)
45
+ }));
46
+ 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.";
47
+ var lookAt = async (ctx, request) => {
48
+ if (!ctx.preview || ctx.spaceId === void 0) return { refused: {
49
+ error: "PREVIEW_UNAVAILABLE",
50
+ message: "Visual preview is not enabled on this server.",
51
+ hint: "Preview needs the SSR render service; it is unavailable in MCP-only mode."
52
+ } };
53
+ const draft = {
54
+ spaceId: ctx.spaceId,
55
+ env: ctx.env,
56
+ pageRef: request.pageRef,
57
+ operations: request.operations
58
+ };
59
+ const page = request.pageRef ?? "default";
60
+ const wantsHtml = request.view === "html" || !ctx.screenshot;
61
+ const rendered = await ctx.preview.render({
62
+ ...draft,
63
+ includeHtml: wantsHtml
64
+ });
65
+ if (!rendered.ok) return { refused: rendered };
66
+ const facts = {
67
+ pageRef: page,
68
+ pagePath: rendered.pagePath,
69
+ stateVersion: rendered.stateVersion
70
+ };
71
+ if (request.view === "html") return {
72
+ meta: {
73
+ ...facts,
74
+ html: rendered.html
75
+ },
76
+ images: []
77
+ };
78
+ if (!ctx.screenshot) return {
79
+ meta: {
80
+ ...facts,
81
+ warning: "SCREENSHOT_DISABLED",
82
+ message: request.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`.",
83
+ html: rendered.html
84
+ },
85
+ images: []
86
+ };
87
+ const viewports = resolveViewports(request.viewport);
88
+ const shot = await ctx.screenshot.capture({
89
+ pagePath: rendered.pagePath,
90
+ token: rendered.token,
91
+ viewports,
92
+ fullPage: request.fullPage,
93
+ views: CAPTURES[request.view]
94
+ });
95
+ if (!shot.ok) {
96
+ const fallback = await ctx.preview.render(draft);
97
+ return {
98
+ meta: {
99
+ ...facts,
100
+ warning: "SCREENSHOT_UNAVAILABLE",
101
+ message: `The screenshot service failed (${shot.message}); returning the HTML preview instead.`,
102
+ hint: "The dedicated browser service is down or unreachable. Inspect the HTML, or retry later.",
103
+ html: fallback.ok ? fallback.html : ""
104
+ },
105
+ images: []
106
+ };
107
+ }
108
+ const meta = {
109
+ ...facts,
110
+ viewports: viewports.map((v) => v.label)
111
+ };
112
+ const accessibility = request.view === "image" ? {} : shot.accessibility ? {
113
+ accessibility: readingOf(shot.accessibility),
114
+ hint: ACCESSIBILITY_HINT
115
+ } : {
116
+ warning: "ACCESSIBILITY_UNSUPPORTED",
117
+ message: "The browser service answered without the accessibility tree — it predates the accessibility view. The image, if asked for, is still here."
118
+ };
119
+ return {
120
+ meta: {
121
+ ...meta,
122
+ ...accessibility
123
+ },
124
+ images: request.view === "accessibility" ? [] : shot.images
125
+ };
126
+ };
127
+ //#endregion
128
+ export { fullPage, lookAt, pageRef, viewport };
@@ -0,0 +1,6 @@
1
+ import { Space } from '../../helpers';
2
+ import { Operation } from '../operations';
3
+ export declare const normalizeOperations: (space: Space, operations: Operation[]) => {
4
+ operations: Operation[];
5
+ notes: string[];
6
+ };
@@ -0,0 +1,37 @@
1
+ import { buildTypeRegistry } from "../../catalogs/registry.js";
2
+ import { elementTypeNames } from "../../catalogs/index.js";
3
+ //#region src/modules/mcp/tools/shared/normalizeOperations.ts
4
+ /**
5
+ * What a batch wrote that has one reading, read that way and said: an element type spelt with other capitals
6
+ * (`Heading`) is the type the catalog spells (`heading`). A guess between two names is never made here — a name with a
7
+ * typo is refused, or warned of, with the nearest one, by the checks that come after.
8
+ */
9
+ /** The element types this space renders: the SDK's own and every one its catalog or its elements bring. */
10
+ var knownTypes = (space) => /* @__PURE__ */ new Set([...Object.keys(buildTypeRegistry(space.schema, space.catalog).types), ...elementTypeNames]);
11
+ var normalizedTree = (element, path, byLowerCase, known, notes) => {
12
+ const spelt = known.has(element.type) ? void 0 : byLowerCase.get(element.type.toLowerCase());
13
+ if (spelt) notes.push(`Read element type "${element.type}" as "${spelt}" (${path}.type): write it as the catalog spells it.`);
14
+ const children = element.children?.map((child, index) => normalizedTree(child, `${path}.children[${String(index)}]`, byLowerCase, known, notes));
15
+ return {
16
+ ...element,
17
+ ...spelt ? { type: spelt } : {},
18
+ ...children ? { children } : {}
19
+ };
20
+ };
21
+ var normalizeOperations = (space, operations) => {
22
+ const known = knownTypes(space);
23
+ const spellings = /* @__PURE__ */ new Map();
24
+ for (const type of known) spellings.set(type.toLowerCase(), [...spellings.get(type.toLowerCase()) ?? [], type]);
25
+ const byLowerCase = new Map([...spellings].flatMap(([lower, types]) => types.length === 1 ? [[lower, types[0]]] : []));
26
+ const notes = [];
27
+ const normalized = operations.map((operation, index) => operation.type === "upsertElement" ? {
28
+ ...operation,
29
+ element: normalizedTree(operation.element, `operations[${String(index)}].element`, byLowerCase, known, notes)
30
+ } : operation);
31
+ return {
32
+ operations: notes.length > 0 ? normalized : operations,
33
+ notes
34
+ };
35
+ };
36
+ //#endregion
37
+ export { normalizeOperations };
@@ -0,0 +1,13 @@
1
+ /** One batch, by where it was sent and what it holds. */
2
+ export declare const batchKey: (scope: string, operations: unknown) => string;
3
+ /** How many times this batch was refused lately — before this one. */
4
+ export declare const timesRefused: (key: string, now?: number) => number;
5
+ /** The batch was refused once more. */
6
+ export declare const noteRefused: (key: string, now?: number) => void;
7
+ /** The batch went through: what was refused before no longer counts. */
8
+ export declare const forgetRefused: (key: string) => void;
9
+ export declare const AGAIN: string;
10
+ export declare const REPEATED: {
11
+ error: string;
12
+ message: string;
13
+ };
@@ -0,0 +1,38 @@
1
+ import { createHash } from "node:crypto";
2
+ //#region src/modules/mcp/tools/shared/repeats.ts
3
+ /**
4
+ * The same batch refused again — an agent going round in circles, which a model is not trusted to notice. Counted per
5
+ * process, as a hint and never a lock: a call that lands on another worker only loses the hint.
6
+ */
7
+ var WINDOW_MS = 6e5;
8
+ var KEPT = 500;
9
+ var refused = /* @__PURE__ */ new Map();
10
+ /** One batch, by where it was sent and what it holds. */
11
+ var batchKey = (scope, operations) => `${scope}:${createHash("sha256").update(JSON.stringify(operations)).digest("hex")}`;
12
+ /** How many times this batch was refused lately — before this one. */
13
+ var timesRefused = (key, now = Date.now()) => {
14
+ const entry = refused.get(key);
15
+ return entry && now - entry.at < WINDOW_MS ? entry.count : 0;
16
+ };
17
+ /** The batch was refused once more. */
18
+ var noteRefused = (key, now = Date.now()) => {
19
+ refused.set(key, {
20
+ count: timesRefused(key, now) + 1,
21
+ at: now
22
+ });
23
+ if (refused.size > KEPT) {
24
+ const oldest = refused.keys().next().value;
25
+ if (oldest !== void 0) refused.delete(oldest);
26
+ }
27
+ };
28
+ /** The batch went through: what was refused before no longer counts. */
29
+ var forgetRefused = (key) => {
30
+ refused.delete(key);
31
+ };
32
+ var AGAIN = "This batch was refused before, for these reasons. Sending it again will not change the answer: change what the errors name — plitzi_describe_operation { type } gives an operation’s fields, a hint its way out — or stop and ask the person what they meant.";
33
+ var REPEATED = {
34
+ error: "REPEATED_BATCH",
35
+ message: "This exact batch was refused twice, and was not run again. Change what the errors named, or ask the person."
36
+ };
37
+ //#endregion
38
+ export { AGAIN, REPEATED, batchKey, forgetRefused, noteRefused, timesRefused };
@@ -24,9 +24,6 @@ export interface ToolContext {
24
24
  * wired it. Only plitzi_try_function uses it. */
25
25
  tryFunction?: (task: string, params: Record<string, unknown>) => Promise<ActionRunReport>;
26
26
  }
27
- /** A capability a tool depends on; the host skips registering a tool whose capability it did not wire (so
28
- * plitzi_screenshot simply does not appear when no browser service is configured). */
29
- export type ToolRequires = 'screenshot';
30
27
  /** Build the MCP CallToolResult for an image-returning tool: a text meta block followed by one image block per
31
28
  * PNG, so a vision-capable agent (or MCP client) sees the render directly. */
32
29
  export declare const imageResult: (images: ScreenshotImage[], meta: unknown) => CallToolResult;
@@ -40,8 +37,13 @@ export interface ToolSpec<Shape extends ZodRawShape> {
40
37
  description: string;
41
38
  inputShape: Shape;
42
39
  access: 'read' | 'write';
43
- /** A host capability this tool needs; hosts that did not wire it skip registering the tool. */
44
- requires?: ToolRequires;
40
+ /** This tool's input carries the whole operations vocabulary (`plitzi_apply`): the one copy of it a connection lists. */
41
+ carriesOperations?: boolean;
42
+ /**
43
+ * What is LISTED for this tool when another tool on the connection carries the vocabulary: the same input with the
44
+ * operations' fields left out. Its calls are still parsed against `inputShape`, so nothing is checked less.
45
+ */
46
+ compactInputShape?: ZodRawShape;
45
47
  /** The tool operates on no space (plitzi_render authors a throwaway one), so the host must NOT resolve a spaceId
46
48
  * or load a space for it — that keeps it callable with no auth, on the public surface. */
47
49
  spaceless?: boolean;
@@ -63,7 +65,8 @@ export interface ToolDef {
63
65
  description: string;
64
66
  inputShape: ZodRawShape;
65
67
  access: 'read' | 'write';
66
- requires?: ToolRequires;
68
+ carriesOperations?: boolean;
69
+ compactInputShape?: ZodRawShape;
67
70
  spaceless?: boolean;
68
71
  ui?: McpUiToolMeta;
69
72
  execute: (args: unknown, ctx: ToolContext) => unknown;
@@ -22,7 +22,8 @@ var defineTool = (spec) => {
22
22
  description: spec.description,
23
23
  inputShape: spec.inputShape,
24
24
  access: spec.access,
25
- requires: spec.requires,
25
+ carriesOperations: spec.carriesOperations,
26
+ compactInputShape: spec.compactInputShape,
26
27
  spaceless: spec.spaceless,
27
28
  ui: spec.ui,
28
29
  execute: (args, ctx) => spec.run(parse(args), ctx),
@@ -21,7 +21,6 @@ export interface ValidationCtx {
21
21
  errors: ValidationError[];
22
22
  warnings: string[];
23
23
  warned: Set<string>;
24
- knownTypes: Set<string>;
25
24
  typeProps: Map<string, Set<string>>;
26
25
  typeMeta: Map<string, TypeMeta>;
27
26
  elementType: (ref: string) => string | undefined;
@@ -48,7 +48,6 @@ var checkElementInput = (element, path, ctx, seen) => {
48
48
  message: "Element type is required",
49
49
  hint: "Read plitzi://types for known types"
50
50
  });
51
- else if (!ctx.knownTypes.has(element.type)) ctx.warnings.push(`Type "${element.type}" was not seen in this space; ensure a plugin provides it (${path}.type).`);
52
51
  checkElementProps(element, path, ctx);
53
52
  element.children?.forEach((child, i) => checkElementInput(child, `${path}.children[${i}]`, ctx, seen));
54
53
  };
@@ -2,7 +2,6 @@ import { componentView, findComponentByRef } from "../../../helpers/components.j
2
2
  import { findElementByRef, findFolderByRef, findPageByRef, findRootByRef, folderAncestorIds, getLayoutElements, getPageElements, pageFoldersOf, resolveRef, routeParamNames } from "../../../helpers/space.js";
3
3
  import { observedDataSources, observedInteractionActions } from "../../../catalogs/observed.js";
4
4
  import { buildTypeRegistry } from "../../../catalogs/registry.js";
5
- import { elementTypeNames } from "../../../catalogs/index.js";
6
5
  import { MAX_OPS } from "../../operations/index.js";
7
6
  import { batchDeclaredElements, batchDeclaredFolders, batchDeclaredPages, batchDeclaredVariants, batchDeclaredVars } from "./batch.js";
8
7
  import { checkObservedName, warnOnce } from "./context.js";
@@ -13,6 +12,7 @@ import { checkRef } from "./refs.js";
13
12
  import { checkElementInput, checkRawMarkup, checkTypeProps, checkVariantApplication } from "./elements.js";
14
13
  import { checkInteractionNode } from "./interactions.js";
15
14
  import { treeOf } from "@plitzi/sdk-schema/helpers/components";
15
+ import { closest } from "@plitzi/sdk-authoring";
16
16
  import { isValidElementId } from "@plitzi/sdk-schema/helpers/elementId";
17
17
  //#region src/modules/mcp/tools/shared/validator/index.ts
18
18
  var STYLE_CATEGORIES = [
@@ -49,7 +49,6 @@ var buildValidationCtx = (space, ops, mode = "space") => {
49
49
  errors: [],
50
50
  warnings: [],
51
51
  warned: /* @__PURE__ */ new Set(),
52
- knownTypes: /* @__PURE__ */ new Set([...Object.keys(registry.types), ...elementTypeNames]),
53
52
  typeProps: new Map(Object.entries(registry.types).map(([type, info]) => [type, new Set(Object.keys(info.props))])),
54
53
  typeMeta: buildTypeMeta(space.catalog),
55
54
  elementType: (ref) => treeOf(space.schema, ref)?.flat[ref]?.definition.type,
@@ -76,6 +75,28 @@ var validateOperations = (space, ops, mode = "space") => {
76
75
  if (input.type === PROVIDER_TYPE) checkProviderElement(space, ctx, input.ref, input.runtime, input.props, batchConnectors, base);
77
76
  (input.children ?? []).forEach((child, i) => checkProviders(child, `${base}.children[${i}]`));
78
77
  };
78
+ const classes = /* @__PURE__ */ new Set([...Object.values(space.style.platform).flatMap((breakpoint) => Object.entries(breakpoint).filter(([, item]) => item.type === "class").map(([name]) => name)), ...ops.flatMap((op) => op.type === "upsertDefinition" ? [op.ref] : op.type === "upsertDefinitions" ? Object.keys(op.definitions) : [])]);
79
+ const checkClasses = (style, path) => {
80
+ const worn = [...(style?.base ?? []).map((name) => ({
81
+ name,
82
+ at: `${path}.base`
83
+ })), ...Object.entries(style?.slots ?? {}).flatMap(([slot, names]) => names.map((name) => ({
84
+ name,
85
+ at: `${path}.slots.${slot}`
86
+ })))];
87
+ for (const { name, at } of worn) if (!classes.has(name)) {
88
+ const nearest = closest(name, classes);
89
+ ctx.errors.push({
90
+ path: at,
91
+ message: `"${name}" is not a class of the space${nearest ? ` — did you mean "${nearest}"?` : ""}`,
92
+ hint: "Name one of plitzi://definitions/{env}, or declare it earlier in the batch with upsertDefinition"
93
+ });
94
+ }
95
+ };
96
+ const checkTreeClasses = (input, path) => {
97
+ checkClasses(input.style, `${path}.style`);
98
+ (input.children ?? []).forEach((child, i) => checkTreeClasses(child, `${path}.children[${i}]`));
99
+ };
79
100
  if (ops.length > 1e3) ctx.errors.push({
80
101
  path: "operations",
81
102
  message: `Batch has ${ops.length} operations (max ${MAX_OPS})`,
@@ -103,11 +124,13 @@ var validateOperations = (space, ops, mode = "space") => {
103
124
  switch (op.type) {
104
125
  case "upsertElement":
105
126
  checkElementInput(op.element, `${base}.element`, ctx, /* @__PURE__ */ new Set());
127
+ checkTreeClasses(op.element, `${base}.element`);
106
128
  checkVariantApplication(op.element.initialState, `${base}.element.initialState`, ctx);
107
129
  checkProviders(op.element, `${base}.element`);
108
130
  break;
109
131
  case "patchElement": {
110
132
  checkRef(op.ref, `${base}.ref`, ctx);
133
+ checkClasses(op.style, `${base}.style`);
111
134
  const page = findRootByRef(scoped, component ? component.rootId : op.pageRef);
112
135
  const target = page ? resolveRef(scoped, page, op.ref) : void 0;
113
136
  if (op.props && target && target.id !== page?.id) {
@@ -44,9 +44,9 @@ export type ScreenshotInput = {
44
44
  */
45
45
  views?: CaptureView[];
46
46
  };
47
- /** How plitzi_screenshot reaches the headless-browser service. The consumer injects an implementation (an HTTP
48
- * client to the dedicated browser pod); absent → the tool is not registered. A capture that fails (pod down)
49
- * returns `ok:false` rather than throwing, so the tool can degrade to the HTML preview. */
47
+ /** How a look reaches the headless-browser service. The consumer injects an implementation (an HTTP
48
+ * client to the dedicated browser pod); absent, or a capture that fails (pod down) and answers `ok:false` rather than
49
+ * throwing, and the look answers the HTML instead. */
50
50
  export type ScreenshotClient = {
51
51
  capture: (input: ScreenshotInput) => Promise<ScreenshotResult>;
52
52
  };
@@ -44,6 +44,8 @@ export interface WriteResponse {
44
44
  updated: number;
45
45
  deleted: number;
46
46
  };
47
+ /** What the batch did, read off the space before and after it: a line per change, nothing left unsaid. */
48
+ effects?: string[];
47
49
  changed: ChangedResource[];
48
50
  elements?: WriteElement[];
49
51
  warnings?: string[];
@@ -83,10 +85,6 @@ export interface ApplyInput {
83
85
  expectedResourceVersions?: Record<string, string>;
84
86
  operations: Operation[];
85
87
  }
86
- export interface ValidateInput {
87
- environment?: string;
88
- operations: Operation[];
89
- }
90
88
  export interface ValidationResult {
91
89
  valid: boolean;
92
90
  errors: ValidationError[];