@diviops/mcp-server 0.2.21 → 0.2.23

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/README.md CHANGED
@@ -96,7 +96,7 @@ The server connects via standard WordPress REST API and works with any environme
96
96
 
97
97
  > **WP-CLI note:** `WP_PATH` keeps the existing Local by Flywheel behavior by running `wp` directly on the host filesystem. For Docker-based environments (DDEV, wp-env, DevKinsta, WordPress Studio), set `WP_CLI_CMD` to the wrapper command instead. When `WP_CLI_CMD` is set, the server executes the wrapper from `WP_PATH` if provided, otherwise from its current working directory. The MCP server still validates the requested WP-CLI subcommand against its allowlist before executing either path.
98
98
 
99
- ## Available Tools (48)
99
+ ## Available Tools (55)
100
100
 
101
101
  ### Read (26)
102
102
  | Tool | Description |
@@ -128,7 +128,7 @@ The server connects via standard WordPress REST API and works with any environme
128
128
  | `diviops_list_canvases` | List all canvas pages |
129
129
  | `diviops_get_canvas` | Get canvas content |
130
130
 
131
- ### Write (20)
131
+ ### Write (27)
132
132
  | Tool | Description |
133
133
  |------|-------------|
134
134
  | `diviops_create_page` | Create a new page with optional Divi content |
@@ -138,11 +138,18 @@ The server connects via standard WordPress REST API and works with any environme
138
138
  | `diviops_remove_section` | Remove a section by admin label |
139
139
  | `diviops_update_module` | Update specific module attributes by label or text match |
140
140
  | `diviops_move_module` | Move a block before/after another block (reorder modules, sections) |
141
+ | `diviops_lock_module` | Lock a module so VB users cannot edit it (frontend renders normally) |
142
+ | `diviops_unlock_module` | Unlock a module by removing `attrs.locked` (matches VB's absence convention) |
143
+ | `diviops_clone_module` | Deep-copy a module + insert next to source within the same parent |
144
+ | `diviops_add_global_color` | Add a new global color to Divi's palette (writes canonical shape; closes ET's bundle Zod gap that drops `label`) |
145
+ | `diviops_update_global_color` | Update an existing global color by gcid (only provided fields change) |
146
+ | `diviops_delete_global_color` | Delete a global color (refuses if `usedInPosts` non-empty unless `force=true`; customizer-bound defaults always protected) |
141
147
  | `diviops_preset_cleanup` | Remove spam/duplicate presets, bulk rename |
142
- | `diviops_preset_create` | Write a new preset to the D5 registry (module or group type, supports `divi/column` etc.) |
148
+ | `diviops_preset_create` | Write a new preset to the D5 registry (module or group type, supports `divi/column` etc.). Optional `make_default: true` sets it as the bucket's default; optional `priority` controls stack-merge order |
143
149
  | `diviops_preset_reassign` | Rewrite `modulePreset` references across pages (dry-run by default; optional `strip_inline` removes redundant inline attrs) |
144
- | `diviops_preset_update` | Update a specific preset (name, attrs) |
150
+ | `diviops_preset_update` | Update a specific preset (name, attrs, priority) |
145
151
  | `diviops_preset_delete` | Delete a preset by ID |
152
+ | `diviops_preset_set_default` | Set or clear the per-module/group default preset (defaults apply to NEW instances only — use `diviops_preset_reassign` for retroactive swaps) |
146
153
  | `diviops_save_to_library` | Save block markup to Divi Library |
147
154
  | `diviops_update_tb_layout` | Update a Theme Builder layout's block markup |
148
155
  | `diviops_create_tb_template` | Create Theme Builder template with header/footer and conditions |
@@ -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.36";
5
+ export declare const MIN_PLUGIN_VERSION = "1.0.0-beta.38";
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.36';
5
+ export const MIN_PLUGIN_VERSION = '1.0.0-beta.38';
6
6
  /**
7
7
  * Compare two semver-like version strings (supports pre-release tags).
8
8
  *
package/dist/index.js CHANGED
@@ -196,6 +196,101 @@ server.registerTool("diviops_get_global_colors", {
196
196
  ],
197
197
  };
198
198
  });
199
+ server.registerTool("diviops_add_global_color", {
200
+ description: "Add a new global color to Divi's palette. Server mints a fresh `gcid-<uuid>` ID and writes to the et_global_data option in the canonical Divi shape `{color, folder, label, lastUpdated, status, usedInPosts}`. The color appears in the VB color picker after save and can be referenced via `$variable({type:color,value:{name:gcid-...}})$` tokens. Note: Divi's AI Agent bundle has a Zod schema gap that drops `label` on its own writes — our PHP path goes around that bug by writing directly to the option. CONCURRENCY: this is a read-modify-write on a single WP option with no conflict detection. If a Visual Builder session holds stale global data, its next save can clobber colors written here in the interim. Coordinate writes when VB sessions are active, or have the user reload VB after MCP color writes.",
201
+ inputSchema: {
202
+ color: z
203
+ .string()
204
+ .describe('CSS color value — hex (e.g. "#ff0000", "#ff0000aa") or functional rgba/hsla notation. Bare keywords like "red" are not accepted.'),
205
+ label: z
206
+ .string()
207
+ .optional()
208
+ .describe('Human-readable label shown in the VB color picker (e.g. "Brand Red"). Optional — defaults to empty (matches Divi\'s stock palette which leaves labels blank).'),
209
+ folder: z
210
+ .string()
211
+ .optional()
212
+ .describe("Folder name for grouping colors in the picker UI. Optional — defaults to empty (no folder)."),
213
+ status: z
214
+ .enum(["active", "archived"])
215
+ .optional()
216
+ .default("active")
217
+ .describe('Color status — "active" (default, visible in picker) or "archived" (hidden but preserved).'),
218
+ },
219
+ }, async ({ color, label, folder, status }) => {
220
+ const colorEntry = { color };
221
+ if (label !== undefined)
222
+ colorEntry.label = label;
223
+ if (folder !== undefined)
224
+ colorEntry.folder = folder;
225
+ if (status)
226
+ colorEntry.status = status;
227
+ const result = await wp.request("/global-colors", {
228
+ method: "POST",
229
+ body: { colors: [colorEntry], mode: "merge" },
230
+ });
231
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
232
+ });
233
+ server.registerTool("diviops_update_global_color", {
234
+ description: "Update an existing global color by gcid. Only provided fields are updated; omitted fields are preserved. The lastUpdated timestamp is bumped on every write. Use diviops_get_global_colors first to find the gcid for a color. CONCURRENCY: same VB-session race caveat as diviops_add_global_color — the write is read-modify-write on a single WP option, so an active VB session's next save can clobber this update.",
235
+ inputSchema: {
236
+ gcid: z
237
+ .string()
238
+ .describe('Global color ID, e.g. "gcid-abc123..." (must start with "gcid-"). Get from diviops_get_global_colors.'),
239
+ color: z
240
+ .string()
241
+ .optional()
242
+ .describe('New CSS color value — hex or rgba/hsla notation. Omit to keep existing.'),
243
+ label: z
244
+ .string()
245
+ .optional()
246
+ .describe('New human-readable label. Pass empty string to clear.'),
247
+ folder: z
248
+ .string()
249
+ .optional()
250
+ .describe('New folder. Pass empty string to clear.'),
251
+ status: z
252
+ .enum(["active", "archived"])
253
+ .optional()
254
+ .describe('Change status — "active" or "archived". Omit to keep existing.'),
255
+ },
256
+ }, async ({ gcid, color, label, folder, status }) => {
257
+ const colorEntry = { id: gcid };
258
+ if (color !== undefined)
259
+ colorEntry.color = color;
260
+ if (label !== undefined)
261
+ colorEntry.label = label;
262
+ if (folder !== undefined)
263
+ colorEntry.folder = folder;
264
+ if (status)
265
+ colorEntry.status = status;
266
+ const result = await wp.request("/global-colors", {
267
+ method: "POST",
268
+ body: { colors: [colorEntry], mode: "merge" },
269
+ });
270
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
271
+ });
272
+ server.registerTool("diviops_delete_global_color", {
273
+ description: "Delete a global color from the registry by gcid. Refuses by default if the color is tracked as referenced by any post (per Divi's `usedInPosts` index — pass `force: true` to delete anyway; orphan refs will render as invalid CSS until pages are re-saved through VB). Always refuses to delete the 5 customizer-bound defaults (gcid-primary-color, gcid-secondary-color, gcid-heading-color, gcid-body-color, gcid-link-color) regardless of force — those must be edited via WP Customizer. CONCURRENCY: same VB-session race caveat as diviops_add_global_color — an active VB session's next save can re-introduce a color we just deleted if the session held stale data.",
274
+ inputSchema: {
275
+ gcid: z
276
+ .string()
277
+ .describe('Global color ID to delete (must start with "gcid-").'),
278
+ force: z
279
+ .boolean()
280
+ .optional()
281
+ .default(false)
282
+ .describe("If true, delete even when usedInPosts shows live references. Customizer-bound defaults remain protected regardless."),
283
+ },
284
+ }, async ({ gcid, force }) => {
285
+ const body = { gcid };
286
+ if (force)
287
+ body.force = true;
288
+ const result = await wp.request("/global-color-delete", {
289
+ method: "POST",
290
+ body,
291
+ });
292
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
293
+ });
199
294
  server.registerTool("diviops_get_global_fonts", {
200
295
  description: "Get the global font definitions from Divi settings.",
201
296
  }, async () => {
@@ -549,6 +644,75 @@ server.registerTool("diviops_move_module", {
549
644
  ],
550
645
  };
551
646
  });
647
+ server.registerTool("diviops_lock_module", {
648
+ description: 'Lock a module so VB users cannot edit it. Sets attrs.locked = {desktop: {value: "on"}} per Divi\'s per-breakpoint convention (verified via VB-save probe). Locked modules render normally on frontend; only VB-side editing is gated. Same targeting pattern as diviops_update_module — pick one of label / match_text / auto_index. Use diviops_unlock_module to reverse.',
649
+ inputSchema: {
650
+ page_id: z.number().describe("WordPress post/page ID"),
651
+ label: z.string().optional().describe("Admin label of the module to lock (exact match)"),
652
+ match_text: z.string().optional().describe("Text to search for in module markup (case-insensitive)"),
653
+ auto_index: z.string().optional().describe('Auto-index in "type:N" format (e.g. "text:3")'),
654
+ occurrence: z.number().int().min(1).optional().default(1).describe("Which occurrence when multiple modules share the same label (1-based)"),
655
+ },
656
+ }, async ({ page_id, label, match_text, auto_index, occurrence }) => {
657
+ const body = {};
658
+ if (label)
659
+ body.label = label;
660
+ if (match_text)
661
+ body.match_text = match_text;
662
+ if (auto_index)
663
+ body.auto_index = auto_index;
664
+ if (occurrence && occurrence > 1)
665
+ body.occurrence = occurrence;
666
+ const result = await wp.request(`/page/${page_id}/lock-module`, { method: "POST", body });
667
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
668
+ });
669
+ server.registerTool("diviops_unlock_module", {
670
+ description: "Unlock a module by removing attrs.locked entirely. Matches Divi VB's convention: unlocked = attribute absent (NOT {value: 'off'}) — VB doesn't write a falsy value on unlock, it removes the field. Same targeting pattern as diviops_lock_module.",
671
+ inputSchema: {
672
+ page_id: z.number().describe("WordPress post/page ID"),
673
+ label: z.string().optional().describe("Admin label of the module to unlock (exact match)"),
674
+ match_text: z.string().optional().describe("Text to search for in module markup (case-insensitive)"),
675
+ auto_index: z.string().optional().describe('Auto-index in "type:N" format'),
676
+ occurrence: z.number().int().min(1).optional().default(1).describe("Which occurrence when multiple modules share the same label (1-based)"),
677
+ },
678
+ }, async ({ page_id, label, match_text, auto_index, occurrence }) => {
679
+ const body = {};
680
+ if (label)
681
+ body.label = label;
682
+ if (match_text)
683
+ body.match_text = match_text;
684
+ if (auto_index)
685
+ body.auto_index = auto_index;
686
+ if (occurrence && occurrence > 1)
687
+ body.occurrence = occurrence;
688
+ const result = await wp.request(`/page/${page_id}/unlock-module`, { method: "POST", body });
689
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
690
+ });
691
+ server.registerTool("diviops_clone_module", {
692
+ description: 'Clone a module by deep-copying its block JSON and inserting it next to the source within the same parent container. Position controls before/after placement (default "after"). Module IDs are reassigned by Divi at render time from the block tree position, so the clone gets fresh IDs automatically. Same targeting pattern as diviops_lock_module.',
693
+ inputSchema: {
694
+ page_id: z.number().describe("WordPress post/page ID"),
695
+ label: z.string().optional().describe("Admin label of the module to clone (exact match)"),
696
+ match_text: z.string().optional().describe("Text to search for in module markup (case-insensitive)"),
697
+ auto_index: z.string().optional().describe('Auto-index in "type:N" format'),
698
+ occurrence: z.number().int().min(1).optional().default(1).describe("Which occurrence when multiple modules share the same label (1-based)"),
699
+ position: z.enum(["before", "after"]).optional().default("after").describe('Place the clone "before" or "after" the source module within its parent.'),
700
+ },
701
+ }, async ({ page_id, label, match_text, auto_index, occurrence, position }) => {
702
+ const body = {};
703
+ if (label)
704
+ body.label = label;
705
+ if (match_text)
706
+ body.match_text = match_text;
707
+ if (auto_index)
708
+ body.auto_index = auto_index;
709
+ if (occurrence && occurrence > 1)
710
+ body.occurrence = occurrence;
711
+ if (position)
712
+ body.position = position;
713
+ const result = await wp.request(`/page/${page_id}/clone-module`, { method: "POST", body });
714
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
715
+ });
552
716
  server.registerTool("diviops_create_page", {
553
717
  description: "Create a new WordPress page, optionally with Divi block content.",
554
718
  inputSchema: {
@@ -638,7 +802,7 @@ server.registerTool("diviops_preset_cleanup", {
638
802
  };
639
803
  });
640
804
  server.registerTool("diviops_preset_update", {
641
- description: "Update a specific preset by ID. Can rename and/or replace its style attributes. Note: Divi serves frontend CSS from a per-post static cache at wp-content/et-cache/{post_id}/ that wp cache flush does NOT invalidate — if you're verifying a preset change on the rendered frontend, delete that dir for affected pages to force regeneration. Server-side preset state updates immediately; only the pre-rendered CSS file is stale.",
805
+ description: "Update a specific preset by ID. Can rename, replace its style attributes, and/or change its stack priority. Note: Divi serves frontend CSS from a per-post static cache at wp-content/et-cache/{post_id}/ that wp cache flush does NOT invalidate — if you're verifying a preset change on the rendered frontend, delete that dir for affected pages to force regeneration. Server-side preset state updates immediately; only the pre-rendered CSS file is stale.",
642
806
  inputSchema: {
643
807
  preset_id: z.string().describe("Preset ID (UUID or short ID)"),
644
808
  name: z.string().optional().describe("New display name for the preset"),
@@ -646,13 +810,20 @@ server.registerTool("diviops_preset_update", {
646
810
  .record(z.string(), z.any())
647
811
  .optional()
648
812
  .describe("New style attributes (replaces attrs, styleAttrs, and renderAttrs — matches VB save semantics so render cache stays in sync with edit state)"),
813
+ priority: z
814
+ .number()
815
+ .int()
816
+ .optional()
817
+ .describe("Stack-merge priority. When this preset is part of a stacked-preset arrangement (e.g. base typography + brand override on the same module/group slot), Divi sorts presets ascending and merges in priority order, so a higher number wins the cascade. Default in Divi is 10 when omitted. Only meaningful for presets that participate in a stack — solo presets render the same regardless of priority."),
649
818
  },
650
- }, async ({ preset_id, name, attrs }) => {
819
+ }, async ({ preset_id, name, attrs, priority }) => {
651
820
  const body = { preset_id };
652
821
  if (name)
653
822
  body.name = name;
654
823
  if (attrs)
655
824
  body.attrs = attrs;
825
+ if (typeof priority === "number")
826
+ body.priority = priority;
656
827
  const result = await wp.request("/preset-update", {
657
828
  method: "POST",
658
829
  body,
@@ -706,8 +877,17 @@ server.registerTool("diviops_preset_create", {
706
877
  .string()
707
878
  .optional()
708
879
  .describe('Primary attr name for the group (e.g. "title" for designTitleText). Optional.'),
880
+ make_default: z
881
+ .boolean()
882
+ .optional()
883
+ .describe("If true, set this newly-created preset as the default for its module/group after creation. Defaults apply to NEW instances only — existing modules keep their current preset bindings (use diviops_preset_reassign for retroactive swaps). Saves a round-trip vs. calling diviops_preset_set_default after creation."),
884
+ priority: z
885
+ .number()
886
+ .int()
887
+ .optional()
888
+ .describe("Stack-merge priority. When this preset participates in a stacked-preset arrangement, Divi sorts ascending and merges in priority order — higher number wins the cascade. Default in Divi is 10 when omitted."),
709
889
  },
710
- }, async ({ module_name, name, attrs, type, group_name, group_id, primary_attr_name }) => {
890
+ }, async ({ module_name, name, attrs, type, group_name, group_id, primary_attr_name, make_default, priority }) => {
711
891
  if (type === "group" && (!group_name || !group_id)) {
712
892
  throw new Error('type="group" requires both group_name and group_id. Example: group_name="divi/font", group_id="designTitleText".');
713
893
  }
@@ -718,6 +898,10 @@ server.registerTool("diviops_preset_create", {
718
898
  body.group_id = group_id;
719
899
  if (primary_attr_name)
720
900
  body.primary_attr_name = primary_attr_name;
901
+ if (make_default)
902
+ body.make_default = true;
903
+ if (typeof priority === "number")
904
+ body.priority = priority;
721
905
  const result = await wp.request("/preset-create", { method: "POST", body });
722
906
  return {
723
907
  content: [
@@ -784,6 +968,31 @@ server.registerTool("diviops_preset_scan_orphans", {
784
968
  ],
785
969
  };
786
970
  });
971
+ server.registerTool("diviops_preset_set_default", {
972
+ description: "Set or clear the per-module/group default preset. Walks both buckets to locate the preset by UUID, then points the containing module/group's `default` slot at it. Pass unset=true to clear the slot back to none. Defaults apply to NEW module instances only — existing modules keep their current preset bindings (use diviops_preset_reassign for retroactive swaps). Use diviops_preset_audit's `is_default` field to verify state before/after.",
973
+ inputSchema: {
974
+ preset_id: z
975
+ .string()
976
+ .describe("Preset UUID. Bucket (module vs. group) and target module/group are auto-resolved from the registry — no need to specify them."),
977
+ unset: z
978
+ .boolean()
979
+ .optional()
980
+ .describe("If true, clear the default pointer for the module/group this preset lives in (regardless of whether this preset is currently the default). Defaults to false (set the preset as the default)."),
981
+ },
982
+ }, async ({ preset_id, unset }) => {
983
+ const body = { preset_id };
984
+ if (unset)
985
+ body.unset = true;
986
+ const result = await wp.request("/preset-set-default", {
987
+ method: "POST",
988
+ body,
989
+ });
990
+ return {
991
+ content: [
992
+ { type: "text", text: JSON.stringify(result) },
993
+ ],
994
+ };
995
+ });
787
996
  // ── Library Tools ───────────────────────────────────────────────────
788
997
  server.registerTool("diviops_list_library", {
789
998
  description: "List saved Divi Library items. Filter by layout_type (section, row, module) and scope (global, non_global).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diviops/mcp-server",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "MCP server exposing Divi 5 Visual Builder as tools for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",