@diviops/mcp-server 0.2.11 → 0.2.13

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.
@@ -2,7 +2,7 @@
2
2
  * Version compatibility between MCP server and WP plugin.
3
3
  */
4
4
  /** Minimum WP plugin version this server requires. */
5
- export declare const MIN_PLUGIN_VERSION = "1.0.0-beta.30";
5
+ export declare const MIN_PLUGIN_VERSION = "1.0.0-beta.31";
6
6
  /**
7
7
  * Compare two semver-like version strings (supports pre-release tags).
8
8
  *
@@ -2,7 +2,7 @@
2
2
  * Version compatibility between MCP server and WP plugin.
3
3
  */
4
4
  /** Minimum WP plugin version this server requires. */
5
- export const MIN_PLUGIN_VERSION = '1.0.0-beta.30';
5
+ export const MIN_PLUGIN_VERSION = '1.0.0-beta.31';
6
6
  /**
7
7
  * Compare two semver-like version strings (supports pre-release tags).
8
8
  *
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { z } from "zod";
14
14
  import { WPClient } from "./wp-client.js";
15
15
  import { optimizeSchema } from "./schema-optimizer.js";
16
16
  import { createWpCli } from "./wp-cli.js";
17
+ import { findForeignVarRefs, scanAttrsForForeignVarRefs, isolationErrorResult, } from "./validate-attrs.js";
17
18
  import { readFileSync, readdirSync } from "fs";
18
19
  import { join, dirname } from "path";
19
20
  import { fileURLToPath } from "url";
@@ -240,6 +241,9 @@ server.registerTool("diviops_update_page_content", {
240
241
  .describe("Full page content in WordPress block markup format (<!-- wp:divi/section -->...<!-- /wp:divi/section -->)"),
241
242
  },
242
243
  }, async ({ page_id, content }) => {
244
+ const hits = findForeignVarRefs(content, "content");
245
+ if (hits.length > 0)
246
+ return isolationErrorResult("diviops_update_page_content", hits);
243
247
  const result = await wp.request(`/page/${page_id}/content`, {
244
248
  method: "POST",
245
249
  body: { content },
@@ -296,6 +300,9 @@ server.registerTool("diviops_append_section", {
296
300
  .describe('Where to insert: "start" or "end" (default)'),
297
301
  },
298
302
  }, async ({ page_id, content, position }) => {
303
+ const hits = findForeignVarRefs(content, "content");
304
+ if (hits.length > 0)
305
+ return isolationErrorResult("diviops_append_section", hits);
299
306
  const result = await wp.request(`/page/${page_id}/append`, {
300
307
  method: "POST",
301
308
  body: { content, position: position ?? "end" },
@@ -330,6 +337,9 @@ server.registerTool("diviops_replace_section", {
330
337
  .describe("Which match to target (1-based, default: 1)"),
331
338
  },
332
339
  }, async ({ page_id, label, match_text, content, occurrence }) => {
340
+ const hits = findForeignVarRefs(content, "content");
341
+ if (hits.length > 0)
342
+ return isolationErrorResult("diviops_replace_section", hits);
333
343
  const body = { content, occurrence };
334
344
  if (label)
335
345
  body.label = label;
@@ -443,6 +453,9 @@ server.registerTool("diviops_update_module", {
443
453
  .describe("Attribute paths (dot notation) and their new values"),
444
454
  },
445
455
  }, async ({ page_id, label, match_text, auto_index, occurrence, attrs }) => {
456
+ const hits = scanAttrsForForeignVarRefs(attrs);
457
+ if (hits.length > 0)
458
+ return isolationErrorResult("diviops_update_module", hits);
446
459
  const body = { attrs };
447
460
  if (auto_index)
448
461
  body.auto_index = auto_index;
@@ -552,6 +565,11 @@ server.registerTool("diviops_create_page", {
552
565
  .describe("Post status"),
553
566
  },
554
567
  }, async ({ title, content, status }) => {
568
+ if (content) {
569
+ const hits = findForeignVarRefs(content, "content");
570
+ if (hits.length > 0)
571
+ return isolationErrorResult("diviops_create_page", hits);
572
+ }
555
573
  const result = await wp.request("/page/create", {
556
574
  method: "POST",
557
575
  body: { title, content: content ?? "", status: status ?? "draft" },
@@ -708,7 +726,7 @@ server.registerTool("diviops_preset_create", {
708
726
  };
709
727
  });
710
728
  server.registerTool("diviops_preset_reassign", {
711
- description: 'Reassign a preset UUID across page content. Walks pages and rewrites modules referencing old_uuid in their modulePreset array to reference new_uuid. Optionally strips inline attrs that duplicate the new preset\'s attrs (otherwise inline wins over preset). Defaults to dry-run — set mode="apply" to actually rewrite. Use this to consolidate repeated inline styling into a reusable preset after creating one with diviops_preset_create.',
729
+ description: 'Reassign a preset UUID across page content. Covers both module-level refs (`attrs.modulePreset[...]`) and attribute-level group-preset refs (`attrs.groupPreset.<slot>.presetId`), plus — for group presets — registry chain refs in other presets\' `attrs.groupPresets.<slot>.presetId`. The `scope` param controls which ref types are walked (default "both", auto-selects based on new_uuid\'s bucket). Cross-bucket swaps (module ↔ group) are rejected. For module-scope swaps, optionally strips inline attrs that duplicate the new preset\'s attrs (otherwise inline wins over preset); slot-scoped inline strip for group scope is not yet implemented and is skipped with an advisory. Defaults to dry-run — set mode="apply" to actually rewrite. Use this to consolidate repeated inline styling into a reusable preset after creating one with diviops_preset_create.',
712
730
  inputSchema: {
713
731
  old_uuid: z
714
732
  .string()
@@ -724,15 +742,26 @@ server.registerTool("diviops_preset_reassign", {
724
742
  .enum(["dry-run", "apply"])
725
743
  .optional()
726
744
  .default("dry-run")
727
- .describe('"dry-run" (default) returns the diff without writing. "apply" rewrites page content.'),
745
+ .describe('"dry-run" (default) returns the diff without writing. "apply" rewrites page content (and registry chains for group-scope swaps).'),
728
746
  strip_inline: z
729
747
  .boolean()
730
748
  .optional()
731
749
  .default(true)
732
- .describe("If true (default), strip inline attrs that deep-equal the new preset's attrs so the preset actually takes effect. Set false to swap UUIDs only."),
750
+ .describe("If true (default), strip inline attrs that deep-equal the new preset's attrs so the preset actually takes effect. Applies to module-scope swaps only; group-scope swaps currently skip strip with an advisory in the summary. Set false to swap UUIDs only."),
751
+ scope: z
752
+ .enum(["module", "group", "both"])
753
+ .optional()
754
+ .default("both")
755
+ .describe('"module" walks `attrs.modulePreset[...]` only. "group" walks `attrs.groupPreset.<slot>.presetId` plus registry chain refs (`attrs.groupPresets.<slot>.presetId` in other presets). "both" (default) auto-selects based on new_uuid\'s bucket — module/group identity is disjoint, so there is one valid walk per swap. An explicit "module" or "group" rejects if new_uuid is in the wrong bucket.'),
733
756
  },
734
- }, async ({ old_uuid, new_uuid, page_ids, mode, strip_inline }) => {
735
- const body = { old_uuid, new_uuid, mode, strip_inline };
757
+ }, async ({ old_uuid, new_uuid, page_ids, mode, strip_inline, scope }) => {
758
+ const body = {
759
+ old_uuid,
760
+ new_uuid,
761
+ mode,
762
+ strip_inline,
763
+ scope,
764
+ };
736
765
  if (page_ids)
737
766
  body.page_ids = page_ids;
738
767
  const result = await wp.request("/preset-reassign", {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Isolation-rule validator: bans cross-system var(--alias) refs in Divi
3
+ * module attrs. See SKILL.md rule 8 and references/module-formats.md
4
+ * §"Design Token References in Attrs".
5
+ *
6
+ * CSS spec: var(--undeclared-name) with no fallback falls through to the
7
+ * property's initial value (0 for padding, browser default for color).
8
+ * Tool reports success, renderer emits ref as-is, page silently breaks.
9
+ *
10
+ * Only var() refs to gcid-* / gvid-* pass — Divi-owned namespaces that
11
+ * auto-resolve via :root. Any other alias is rejected.
12
+ */
13
+ export interface ForeignVarRef {
14
+ alias: string;
15
+ snippet: string;
16
+ location?: string;
17
+ }
18
+ export declare function findForeignVarRefs(value: unknown, location?: string): ForeignVarRef[];
19
+ export declare function scanAttrsForForeignVarRefs(attrs: Record<string, unknown>): ForeignVarRef[];
20
+ export declare function formatIsolationError(tool: string, hits: ForeignVarRef[]): string;
21
+ export declare function isolationErrorResult(tool: string, hits: ForeignVarRef[]): {
22
+ content: {
23
+ type: "text";
24
+ text: string;
25
+ }[];
26
+ isError: boolean;
27
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Isolation-rule validator: bans cross-system var(--alias) refs in Divi
3
+ * module attrs. See SKILL.md rule 8 and references/module-formats.md
4
+ * §"Design Token References in Attrs".
5
+ *
6
+ * CSS spec: var(--undeclared-name) with no fallback falls through to the
7
+ * property's initial value (0 for padding, browser default for color).
8
+ * Tool reports success, renderer emits ref as-is, page silently breaks.
9
+ *
10
+ * Only var() refs to gcid-* / gvid-* pass — Divi-owned namespaces that
11
+ * auto-resolve via :root. Any other alias is rejected.
12
+ */
13
+ const VAR_REF_RE = /var\(\s*--([A-Za-z_][A-Za-z0-9_-]*)/g;
14
+ const ALLOWED_PREFIXES = ["gcid-", "gvid-"];
15
+ export function findForeignVarRefs(value, location) {
16
+ if (typeof value !== "string" || value.length === 0)
17
+ return [];
18
+ const hits = [];
19
+ VAR_REF_RE.lastIndex = 0;
20
+ let m;
21
+ while ((m = VAR_REF_RE.exec(value)) !== null) {
22
+ const alias = m[1];
23
+ if (ALLOWED_PREFIXES.some((p) => alias.startsWith(p)))
24
+ continue;
25
+ hits.push({ alias, snippet: `var(--${alias})`, location });
26
+ }
27
+ return hits;
28
+ }
29
+ export function scanAttrsForForeignVarRefs(attrs) {
30
+ const hits = [];
31
+ for (const [path, value] of Object.entries(attrs)) {
32
+ hits.push(...findForeignVarRefs(value, path));
33
+ }
34
+ return hits;
35
+ }
36
+ export function formatIsolationError(tool, hits) {
37
+ const uniq = new Map();
38
+ for (const h of hits) {
39
+ const k = `${h.snippet}::${h.location ?? ""}`;
40
+ if (!uniq.has(k))
41
+ uniq.set(k, h);
42
+ }
43
+ const lines = [
44
+ `Isolation-rule violation in ${tool}: module attrs cannot reference non-Divi CSS aliases.`,
45
+ "",
46
+ "Offending refs:",
47
+ ...[...uniq.values()].map((h) => ` - ${h.snippet}${h.location ? ` (at ${h.location})` : ""}`),
48
+ "",
49
+ "Allowed: var(--gcid-*) and var(--gvid-*) (Divi-owned, auto-emitted to :root).",
50
+ 'Canonical form: $variable({"type":"content","value":{"name":"gvid-your-token","settings":{}}})$',
51
+ "",
52
+ "Fix: register the token inside Divi Variable Manager (readable ID is fine, e.g. gvid-oa-space-3) and reference via $variable({...})$. See SKILL.md rule 8 / references/module-formats.md#design-token-references-in-attrs-canonical-variable-only.",
53
+ ];
54
+ return lines.join("\n");
55
+ }
56
+ export function isolationErrorResult(tool, hits) {
57
+ return {
58
+ content: [
59
+ { type: "text", text: formatIsolationError(tool, hits) },
60
+ ],
61
+ isError: true,
62
+ };
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diviops/mcp-server",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "MCP server exposing Divi 5 Visual Builder as tools for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",