@diviops/mcp-server 0.2.29 → 1.1.0

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
@@ -41,7 +41,7 @@ claude mcp add diviops-mcp \
41
41
 
42
42
  > **Use `--env` flags, not the `env` command.** Claude Code's native `--env KEY=VALUE` flags survive copy-paste; the older `-- env KEY=VALUE` form (piping through unix `env`) breaks silently when any value contains a space. Quote any value with spaces (e.g. `--env "WP_PATH=/Users/you/Local Sites/site/app/public"`) — no backslash escaping needed inside quotes.
43
43
 
44
- **With WP-CLI** (optional — enables `diviops_wp_cli` tool):
44
+ **With WP-CLI** (optional — enables `diviops_meta_wp_cli` tool):
45
45
  ```bash
46
46
  claude mcp add diviops-mcp \
47
47
  --env WP_URL=http://your-site.local \
@@ -96,73 +96,75 @@ 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 (63)
99
+ ## Available Tools (65)
100
100
 
101
101
  ### Read (30)
102
102
  | Tool | Description |
103
103
  |------|-------------|
104
- | `diviops_test_connection` | Test WordPress connection and Divi version |
105
- | `diviops_server_info` | DiviOps server identity, version, license type, capabilities |
106
- | `diviops_list_pages` | List pages/posts with Divi status |
107
- | `diviops_get_page` | Get page details and raw content |
108
- | `diviops_get_page_layout` | Get parsed block tree (layout structure) |
109
- | `diviops_get_section` | Get a single section's markup by admin label |
110
- | `diviops_list_modules` | List all available Divi modules |
111
- | `diviops_get_module_schema` | Get attribute schema for a module (optimized by default, `raw: true` for full) |
112
- | `diviops_get_settings` | Get Divi site settings and theme options |
113
- | `diviops_get_global_colors` | Get global color palette |
114
- | `diviops_get_global_fonts` | Get global font definitions |
115
- | `diviops_find_icon` | Search 1,989 icons by keyword (FA + Divi) |
116
- | `diviops_list_templates` | List available MCP prompt templates |
117
- | `diviops_get_template` | Get a specific template's block markup |
104
+ | `diviops_meta_ping` | Test WordPress connection and Divi version |
105
+ | `diviops_meta_info` | DiviOps server identity, version, license type, capabilities |
106
+ | `diviops_page_list` | List pages/posts with Divi status |
107
+ | `diviops_page_get` | Get page details and raw content |
108
+ | `diviops_page_get_layout` | Get parsed block tree (layout structure) |
109
+ | `diviops_section_get` | Get a single section's markup by admin label |
110
+ | `diviops_schema_list_modules` | List all available Divi modules |
111
+ | `diviops_schema_get_module` | Get attribute schema for a module (optimized by default, `raw: true` for full) |
112
+ | `diviops_schema_get_settings` | Get Divi site settings and theme options |
113
+ | `diviops_global_color_list` | Get global color palette |
114
+ | `diviops_global_font_list` | Get global font definitions |
115
+ | `diviops_meta_find_icon` | Search 1,989 icons by keyword (FA + Divi) |
116
+ | `diviops_template_list` | List available MCP prompt templates |
117
+ | `diviops_template_get` | Get a specific template's block markup |
118
118
  | `diviops_preset_audit` | Audit presets with referenced/unreferenced analysis. Walks both page content and in-registry `groupPresets` chains; exposes `block_ref_count`, `group_ref_count`, `referenced_by_presets`. Also reports `orphan_default_pointers` — per-bucket `default` pointers referencing UUIDs missing from `items[]` (legacy damage from past unsafe deletes; clear via `diviops_preset_set_default` in bucket-addressed mode: `type` + `module` + `unset=true`) |
119
119
  | `diviops_preset_scan_orphans` | List page-referenced preset UUIDs missing from the D5 registry (separates dangling orphans from D4-legacy refs) |
120
- | `diviops_list_library` | List saved Divi Library items |
121
- | `diviops_get_library_item` | Get a library item's block markup |
120
+ | `diviops_library_list` | List saved Divi Library items |
121
+ | `diviops_library_get` | Get a library item's block markup |
122
122
  | `diviops_render_preview` | Render block markup to HTML for preview |
123
123
  | `diviops_validate_blocks` | Validate block markup (structure, required attrs, known pitfalls) |
124
- | `diviops_list_tb_templates` | List Theme Builder templates with conditions and layout IDs |
125
- | `diviops_get_tb_layout` | Get a Theme Builder layout's block markup (header/body/footer) |
126
- | `diviops_list_variables` | List design token variables (filter by type or prefix) |
127
- | `diviops_variables_scan_orphans` | Find `gvid-`/`gcid-` refs with no backing Variable Manager entry (orphans render as invalid CSS) + unused variables (defined, never referenced). Scans pages, Theme Builder layouts (header/body/footer), Divi Library items, canvas pages, and the preset registry |
128
- | `diviops_variables_used_on_page` | Detect which `gvid-` (numeric/font) IDs a single page emits — the exact set Divi 5.4.0+ uses to scope selective `:root{--gvid-*}` CSS variable emission. Walks the same content stack the frontend assembles (post_content + active TB header/body/footer + appended canvases + presets). `gcid-` colors are out of scope (separate emission path). Use for per-page orphan validation, preflight before bulk variable rename, or to debug why a numeric/font variable doesn't render on a specific page. Read-only |
129
- | `diviops_list_canvases` | List all canvas pages |
130
- | `diviops_get_canvas` | Get canvas content |
124
+ | `diviops_tb_template_list` | List Theme Builder templates with conditions and layout IDs |
125
+ | `diviops_tb_layout_get` | Get a Theme Builder layout's block markup (header/body/footer) |
126
+ | `diviops_variable_list` | List design token variables (filter by type or prefix) |
127
+ | `diviops_variable_scan_orphans` | Find `gvid-`/`gcid-` refs with no backing Variable Manager entry (orphans render as invalid CSS) + unused variables (defined, never referenced). Scans pages, Theme Builder layouts (header/body/footer), Divi Library items, canvas pages, and the preset registry |
128
+ | `diviops_variable_used_on_page` | Detect which `gvid-` (numeric/font) IDs a single page emits — the exact set Divi 5.4.0+ uses to scope selective `:root{--gvid-*}` CSS variable emission. Walks the same content stack the frontend assembles (post_content + active TB header/body/footer + appended canvases + presets). `gcid-` colors are out of scope (separate emission path). Use for per-page orphan validation, preflight before bulk variable rename, or to debug why a numeric/font variable doesn't render on a specific page. Read-only |
129
+ | `diviops_canvas_list` | List all canvas pages |
130
+ | `diviops_canvas_get` | Get canvas content |
131
131
  | `diviops_scf_status` | Show SCF (Secure Custom Fields) sync status — pending JSON-vs-DB drift across field groups, post types, taxonomies, options pages. Wraps `wp scf json status` |
132
- | `diviops_scf_list_field_groups` | List all SCF/ACF field groups (post_name = ACF key, post_title, post_status, post_modified). Queries the `acf-field-group` post type via `wp post list` (works on SCF 6.8.4+ and older ACF) |
133
- | `diviops_scf_get_field_group` | Fetch a single SCF/ACF field-group post by ACF key (`group_abc123` → post_name) or numeric WP post ID. For the parsed/structured field tree, use `diviops_scf_export --field-groups=<key> --stdout` |
132
+ | `diviops_scf_field_group_list` | List all SCF/ACF field groups (post_name = ACF key, post_title, post_status, post_modified). Queries the `acf-field-group` post type via `wp post list` (works on SCF 6.8.4+ and older ACF) |
133
+ | `diviops_scf_field_group_get` | Fetch a single SCF/ACF field-group post by ACF key (`group_abc123` → post_name) or numeric WP post ID. For the parsed/structured field tree, use `diviops_scf_export --field-groups=<key> --stdout` |
134
134
 
135
- ### Write (31)
135
+ ### Write (33)
136
136
  | Tool | Description |
137
137
  |------|-------------|
138
- | `diviops_create_page` | Create a new page with optional Divi content |
139
- | `diviops_update_page_content` | Full page content rewrite |
140
- | `diviops_append_section` | Append a section to existing page (start or end) |
141
- | `diviops_replace_section` | Replace a section by admin label |
142
- | `diviops_remove_section` | Remove a section by admin label |
143
- | `diviops_update_module` | Update specific module attributes by label or text match |
144
- | `diviops_move_module` | Move a block before/after another block (reorder modules, sections) |
145
- | `diviops_lock_module` | Lock a module so VB users cannot edit it (frontend renders normally) |
146
- | `diviops_unlock_module` | Unlock a module by removing `attrs.locked` (matches VB's absence convention) |
147
- | `diviops_clone_module` | Deep-copy a module + insert next to source within the same parent |
148
- | `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`) |
149
- | `diviops_update_global_color` | Update an existing global color by gcid (only provided fields change) |
150
- | `diviops_delete_global_color` | Delete a global color (refuses if `usedInPosts` non-empty unless `force=true`; customizer-bound defaults always protected) |
138
+ | `diviops_page_create` | Create a new page with optional Divi content |
139
+ | `diviops_page_update_content` | Full page content rewrite |
140
+ | `diviops_page_trash` | Trash (default) or permanently delete (`force=true`) a page. Idempotent on already-trashed posts. Supports `dry_run` |
141
+ | `diviops_page_update_status` | Update post_status (publish/draft/private/pending/future). `future` requires `date_gmt` (ISO 8601 UTC); `publish` clears stale future dates so re-publishing takes effect immediately. Supports `dry_run` |
142
+ | `diviops_section_append` | Append a section to existing page (start or end) |
143
+ | `diviops_section_replace` | Replace a section by admin label |
144
+ | `diviops_section_remove` | Remove a section by admin label |
145
+ | `diviops_module_update` | Update specific module attributes by label or text match |
146
+ | `diviops_module_move` | Move a block before/after another block (reorder modules, sections) |
147
+ | `diviops_module_lock` | Lock a module so VB users cannot edit it (frontend renders normally) |
148
+ | `diviops_module_unlock` | Unlock a module by removing `attrs.locked` (matches VB's absence convention) |
149
+ | `diviops_module_clone` | Deep-copy a module + insert next to source within the same parent |
150
+ | `diviops_global_color_create` | Add a new global color to Divi's palette (writes canonical shape; closes ET's bundle Zod gap that drops `label`) |
151
+ | `diviops_global_color_update` | Update an existing global color by gcid (only provided fields change) |
152
+ | `diviops_global_color_delete` | Delete a global color (refuses if `usedInPosts` non-empty unless `force=true`; customizer-bound defaults always protected) |
151
153
  | `diviops_preset_cleanup` | Remove spam/duplicate presets, bulk rename |
152
154
  | `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 |
153
155
  | `diviops_preset_reassign` | Rewrite `modulePreset` references across pages (dry-run by default; optional `strip_inline` removes redundant inline attrs) |
154
156
  | `diviops_preset_update` | Update a specific preset (name, attrs, priority) |
155
157
  | `diviops_preset_delete` | Delete a preset by ID. Refuses with HTTP 409 `preset_is_default` when the target is the registered default for its bucket — clear the pointer first via `diviops_preset_set_default` with `unset=true`, or pass `force=true` to delete and clear the pointer in one write |
156
158
  | `diviops_preset_set_default` | Set or clear the per-module/group default preset. Two modes: by `preset_id` (UUID-addressed; auto-resolves bucket) or by `type` + `module` + `unset=true` (bucket-addressed clear, used to repair orphan default pointers when the UUID is gone from `items[]`). Defaults apply to NEW instances only — use `diviops_preset_reassign` for retroactive swaps |
157
- | `diviops_save_to_library` | Save block markup to Divi Library |
158
- | `diviops_update_tb_layout` | Update a Theme Builder layout's block markup |
159
- | `diviops_create_tb_template` | Create Theme Builder template with header/footer and conditions |
160
- | `diviops_create_variable` | Create a design token variable. For `type=numbers` fluid tokens, pass `min`+`max` shorthand (anchors default to 320px/1920px) or explicit `targets` like `{"320px":"20px","1920px":"60px"}` — server generates arithmetically-correct `clamp()` instead of hand-written math that silently under-reaches the stated max. All-px inputs emit px (root-agnostic). Rem inputs OR rem output require explicit opt-in: pass `output_unit="rem"` (accepts the 1rem=16px default) or `root_font_size_px:N` (declares your site's actual root font-size, e.g. `10` for `html { font-size: 62.5% }`, `20` for `html { font-size: 20px }`) |
161
- | `diviops_create_fluid_system` | Batch-emit a fluid typography + spacing + radius variable set in one call. Mirrors Divi 5.4.0's Variable Generator Modal at the algorithm level (clamp() math is identical to `diviops_create_variable`'s fluid mode) but layers profile-selectable anchors over it: `divi-default` (360→1350) matches ET's defaults, `wide` (320→1920) matches the diviops convention, `custom` takes explicit anchors. Each category is independent and optional. Typography uses modular-scale chains (named ratios `major-third`/`perfect-fifth`/`golden`/etc., or raw numbers) — h1 = largest, hN = base. Spacing/radius support `linear` or `geometric` step distributions. `dry_run: true` returns the full plan without persisting; `overwrite: false` (default) skips existing IDs. Single atomic write to the registry — mid-batch failures roll back cleanly |
162
- | `diviops_delete_variable` | Delete a variable by ID. Returns HTTP 409 when live references exist unless `force=true` (use `diviops_variables_scan_orphans` to find reference locations). Returns HTTP 403 for Divi's customizer-bound defaults (`gcid-primary-color`, `gcid-secondary-color`, `gcid-heading-color`, `gcid-body-color`, `gcid-link-color` — managed via WP Customizer) |
163
- | `diviops_create_canvas` | Create a canvas page |
164
- | `diviops_update_canvas` | Update canvas content |
165
- | `diviops_delete_canvas` | Delete a canvas page |
159
+ | `diviops_library_save` | Save block markup to Divi Library |
160
+ | `diviops_tb_layout_update` | Update a Theme Builder layout's block markup |
161
+ | `diviops_tb_template_create` | Create Theme Builder template with header/footer and conditions |
162
+ | `diviops_variable_create` | Create a design token variable. For `type=numbers` fluid tokens, pass `min`+`max` shorthand (anchors default to 320px/1920px) or explicit `targets` like `{"320px":"20px","1920px":"60px"}` — server generates arithmetically-correct `clamp()` instead of hand-written math that silently under-reaches the stated max. All-px inputs emit px (root-agnostic). Rem inputs OR rem output require explicit opt-in: pass `output_unit="rem"` (accepts the 1rem=16px default) or `root_font_size_px:N` (declares your site's actual root font-size, e.g. `10` for `html { font-size: 62.5% }`, `20` for `html { font-size: 20px }`) |
163
+ | `diviops_variable_create_fluid_system` | Batch-emit a fluid typography + spacing + radius variable set in one call. Mirrors Divi 5.4.0's Variable Generator Modal at the algorithm level (clamp() math is identical to `diviops_variable_create`'s fluid mode) but layers profile-selectable anchors over it: `divi-default` (360→1350) matches ET's defaults, `wide` (320→1920) matches the diviops convention, `custom` takes explicit anchors. Each category is independent and optional. Typography uses modular-scale chains (named ratios `major-third`/`perfect-fifth`/`golden`/etc., or raw numbers) — h1 = largest, hN = base. Spacing/radius support `linear` or `geometric` step distributions. `dry_run: true` returns the full plan without persisting; `overwrite: false` (default) skips existing IDs. Single atomic write to the registry — mid-batch failures roll back cleanly |
164
+ | `diviops_variable_delete` | Delete a variable by ID. Returns HTTP 409 when live references exist unless `force=true` (use `diviops_variable_scan_orphans` to find reference locations). Returns HTTP 403 for Divi's customizer-bound defaults (`gcid-primary-color`, `gcid-secondary-color`, `gcid-heading-color`, `gcid-body-color`, `gcid-link-color` — managed via WP Customizer) |
165
+ | `diviops_canvas_create` | Create a canvas page |
166
+ | `diviops_canvas_update` | Update canvas content |
167
+ | `diviops_canvas_delete` | Delete a canvas page |
166
168
  | `diviops_scf_export` | Export SCF schema (field groups, post types, taxonomies, options pages) as JSON to a directory under the safe-root, or to stdout. Wraps `wp scf json export` |
167
169
  | `diviops_scf_import` | Import SCF schema from a JSON file (mutates DB; idempotent — existing items are updated). Wraps `wp scf json import <file>` |
168
170
  | `diviops_scf_sync` | Apply pending JSON-on-disk SCF changes to the DB. Defaults to `dry_run: true` for safety. Wraps `wp scf json sync` |
@@ -170,12 +172,12 @@ The server connects via standard WordPress REST API and works with any environme
170
172
  ### Utility (2)
171
173
  | Tool | Description |
172
174
  |------|-------------|
173
- | `diviops_wp_cli` | Run WP-CLI commands (allowlisted, requires `WP_PATH` or `WP_CLI_CMD`) |
174
- | `diviops_flush_static_cache` | Flush Divi's compiled CSS cache under `wp-content/et-cache/`. `wp cache flush` does NOT touch these files — the frontend can keep serving stale CSS after mutations. Delegates to Divi's native clearer (`ET_Core_PageResource::remove_static_resources`) when available — also clears Theme Builder / archive / taxonomy / home / notfound CSS, object cache, module features cache, post features cache, dynamic assets cache, Google Fonts cache, post meta caches. Falls back to a filesystem walk of numeric-named subdirs when Divi is inactive. Response includes `backend: "divi_native"` or `"fs_fallback"`. Exactly one selector required: `post_id`, `all`, or `after` (unix ts) — no default to `all` |
175
+ | `diviops_meta_wp_cli` | Run WP-CLI commands (allowlisted, requires `WP_PATH` or `WP_CLI_CMD`) |
176
+ | `diviops_meta_flush_cache` | Flush Divi's compiled CSS cache under `wp-content/et-cache/`. `wp cache flush` does NOT touch these files — the frontend can keep serving stale CSS after mutations. Delegates to Divi's native clearer (`ET_Core_PageResource::remove_static_resources`) when available — also clears Theme Builder / archive / taxonomy / home / notfound CSS, object cache, module features cache, post features cache, dynamic assets cache, Google Fonts cache, post meta caches. Falls back to a filesystem walk of numeric-named subdirs when Divi is inactive. Response includes `backend: "divi_native"` or `"fs_fallback"`. Exactly one selector required: `post_id`, `all`, or `after` (unix ts) — no default to `all` |
175
177
 
176
178
  ## WP-CLI Security
177
179
 
178
- The `diviops_wp_cli` tool validates every command against a safety allowlist before execution. Commands not on the list are rejected.
180
+ The `diviops_meta_wp_cli` tool validates every command against a safety allowlist before execution. Commands not on the list are rejected.
179
181
 
180
182
  ### Default allowlist (always available)
181
183
 
@@ -257,7 +259,7 @@ The DEFAULT-tier filesystem commands (`wp export`, `acf export <path>`, `acf imp
257
259
 
258
260
  ## Safety Patterns
259
261
 
260
- High-risk or bulk destructive tools follow one of two conventions to guard against unintended mutation. Both are stateless (no session tokens between calls), but they guard differently: Pattern A is a **stateless gate** — the first call mutates when the safety check passes, refuses with an explanatory error when it fires. Pattern B is **preview-before-commit** — the first call never mutates; an explicit apply step is required. Tools without a gate (e.g., `diviops_update_page_content`) execute their mutation directly — whether to adopt a pattern is a per-tool design decision, not a retrofit requirement.
262
+ High-risk or bulk destructive tools follow one of two conventions to guard against unintended mutation. Both are stateless (no session tokens between calls), but they guard differently: Pattern A is a **stateless gate** — the first call mutates when the safety check passes, refuses with an explanatory error when it fires. Pattern B is **preview-before-commit** — the first call never mutates; an explicit apply step is required. Tools without a gate (e.g., `diviops_page_update_content`) execute their mutation directly — whether to adopt a pattern is a per-tool design decision, not a retrofit requirement.
261
263
 
262
264
  ### Pattern A — `force: false/true` (refuse-with-override)
263
265
 
@@ -272,7 +274,7 @@ Tool refuses the operation with an explanatory error when a safety check fails;
272
274
  **Current tools:**
273
275
  | Tool | Guard | Override |
274
276
  |------|-------|----------|
275
- | `diviops_delete_variable` | HTTP 409 when live references exist | `force=true` |
277
+ | `diviops_variable_delete` | HTTP 409 when live references exist | `force=true` |
276
278
  | `diviops_preset_delete` | HTTP 409 `preset_is_default` when target is the registered default for its bucket | `force=true` (deletes + clears the `default` pointer in the same write) |
277
279
 
278
280
  ### Pattern B — `mode: "dry-run"/"apply"` (preview-then-commit)
@@ -315,7 +317,7 @@ After setup, Claude can:
315
317
  Ensure `WP_URL`, `WP_USER`, and `WP_APP_PASSWORD` are all set. Check your `claude mcp add` command.
316
318
 
317
319
  ### "Connection failed" error
318
- - Verify the WP plugin is active: visit `{WP_URL}/wp-json/diviops/v1/settings` in your browser
320
+ - Verify the WP plugin is active: visit `{WP_URL}/wp-json/diviops/v1/schema/settings` in your browser
319
321
  - Check Application Password is correct (try with curl first)
320
322
 
321
323
  ### "Version mismatch" error
@@ -327,7 +329,7 @@ The MCP server and WP plugin versions are incompatible. Update whichever side is
327
329
 
328
330
  ### Testing manually
329
331
  ```bash
330
- curl -u "username:apppassword" http://site.local/wp-json/diviops/v1/settings
332
+ curl -u "username:apppassword" http://site.local/wp-json/diviops/v1/schema/settings
331
333
  ```
332
334
 
333
335
  ### Preset edits not visible on the frontend
@@ -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.44";
5
+ export declare const MIN_PLUGIN_VERSION = "1.1.0";
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.44';
5
+ export const MIN_PLUGIN_VERSION = '1.1.0';
6
6
  /**
7
7
  * Compare two semver-like version strings (supports pre-release tags).
8
8
  *
package/dist/index.js CHANGED
@@ -83,7 +83,7 @@ const server = new McpServer({
83
83
  version: SERVER_VERSION,
84
84
  });
85
85
  // ── Read Tools ───────────────────────────────────────────────────────
86
- server.registerTool("diviops_list_pages", {
86
+ server.registerTool("diviops_page_list", {
87
87
  description: "List pages/posts in the WordPress site. Returns title, ID, URL, status, and whether each page uses Divi builder.",
88
88
  inputSchema: {
89
89
  post_type: z
@@ -99,7 +99,7 @@ server.registerTool("diviops_list_pages", {
99
99
  page: z.number().optional().default(1).describe("Page number"),
100
100
  },
101
101
  }, async ({ post_type, per_page, page }) => {
102
- const result = await wp.request("/pages", {
102
+ const result = await wp.request("/page/list", {
103
103
  params: {
104
104
  post_type: post_type ?? "page",
105
105
  per_page: String(per_page ?? 20),
@@ -112,20 +112,20 @@ server.registerTool("diviops_list_pages", {
112
112
  ],
113
113
  };
114
114
  });
115
- server.registerTool("diviops_get_page", {
115
+ server.registerTool("diviops_page_get", {
116
116
  description: "Get detailed info about a specific page including its raw Divi block content.",
117
117
  inputSchema: {
118
118
  page_id: z.number().describe("WordPress post/page ID"),
119
119
  },
120
120
  }, async ({ page_id }) => {
121
- const result = await wp.request(`/page/${page_id}`);
121
+ const result = await wp.request(`/page/get/${page_id}`);
122
122
  return {
123
123
  content: [
124
124
  { type: "text", text: JSON.stringify(result) },
125
125
  ],
126
126
  };
127
127
  });
128
- server.registerTool("diviops_get_page_layout", {
128
+ server.registerTool("diviops_page_get_layout", {
129
129
  description: "Get the parsed block tree for a page. Returns slim targeting metadata by default (block names, admin labels, text previews, auto_index). Use full: true for complete attrs (warning: can be very large on complex pages).",
130
130
  inputSchema: {
131
131
  page_id: z.number().describe("WordPress post/page ID"),
@@ -136,7 +136,7 @@ server.registerTool("diviops_get_page_layout", {
136
136
  .describe("Include full block attrs and raw content (default: false for slim mode)"),
137
137
  },
138
138
  }, async ({ page_id, full }) => {
139
- const result = await wp.request(`/page/${page_id}/layout`, {
139
+ const result = await wp.request(`/page/get-layout/${page_id}`, {
140
140
  params: full ? { full: "true" } : {},
141
141
  });
142
142
  return {
@@ -145,17 +145,17 @@ server.registerTool("diviops_get_page_layout", {
145
145
  ],
146
146
  };
147
147
  });
148
- server.registerTool("diviops_list_modules", {
148
+ server.registerTool("diviops_schema_list_modules", {
149
149
  description: "List all available Divi modules (block types) with their names, titles, and categories. Use this to discover what modules can be used in layouts.",
150
150
  }, async () => {
151
- const result = await wp.request("/modules");
151
+ const result = await wp.request("/schema/modules");
152
152
  return {
153
153
  content: [
154
154
  { type: "text", text: JSON.stringify(result) },
155
155
  ],
156
156
  };
157
157
  });
158
- server.registerTool("diviops_get_module_schema", {
158
+ server.registerTool("diviops_schema_get_module", {
159
159
  description: "Get the attribute schema for a Divi module. Returns optimized schema by default (~70% smaller) with content-relevant fields only. Use raw: true for the full schema including CSS selectors and VB metadata.",
160
160
  inputSchema: {
161
161
  module_name: z
@@ -168,7 +168,7 @@ server.registerTool("diviops_get_module_schema", {
168
168
  .describe("Return full schema including CSS selectors and VB metadata"),
169
169
  },
170
170
  }, async ({ module_name, raw }) => {
171
- const result = await wp.request(`/module/${encodeURIComponent(module_name)}`);
171
+ const result = await wp.request(`/schema/module/${encodeURIComponent(module_name)}`);
172
172
  const output = raw ? result : optimizeSchema(result);
173
173
  return {
174
174
  content: [
@@ -176,28 +176,28 @@ server.registerTool("diviops_get_module_schema", {
176
176
  ],
177
177
  };
178
178
  });
179
- server.registerTool("diviops_get_settings", {
179
+ server.registerTool("diviops_schema_get_settings", {
180
180
  description: "Get Divi site settings including theme options, site info, and builder version. Useful for understanding the site context before generating content.",
181
181
  }, async () => {
182
- const result = await wp.request("/settings");
182
+ const result = await wp.request("/schema/settings");
183
183
  return {
184
184
  content: [
185
185
  { type: "text", text: JSON.stringify(result) },
186
186
  ],
187
187
  };
188
188
  });
189
- server.registerTool("diviops_get_global_colors", {
189
+ server.registerTool("diviops_global_color_list", {
190
190
  description: "Get the global color palette defined in Divi. Returns all global colors that can be referenced by modules.",
191
191
  }, async () => {
192
- const result = await wp.request("/global-colors");
192
+ const result = await wp.request("/global-color/list");
193
193
  return {
194
194
  content: [
195
195
  { type: "text", text: JSON.stringify(result) },
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.",
199
+ server.registerTool("diviops_global_color_create", {
200
+ description: "Add a new global color to Divi's palette. The plugin mints a fresh `gcid-<uuid>` ID (the server forwards the color entry without an id and the WP-side handler generates one) 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
201
  inputSchema: {
202
202
  color: z
203
203
  .string()
@@ -224,18 +224,18 @@ server.registerTool("diviops_add_global_color", {
224
224
  colorEntry.folder = folder;
225
225
  if (status)
226
226
  colorEntry.status = status;
227
- const result = await wp.request("/global-colors", {
227
+ const result = await wp.request("/global-color/upsert", {
228
228
  method: "POST",
229
229
  body: { colors: [colorEntry], mode: "merge" },
230
230
  });
231
231
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
232
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.",
233
+ server.registerTool("diviops_global_color_update", {
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_global_color_list first to find the gcid for a color. CONCURRENCY: same VB-session race caveat as diviops_global_color_create — the write is read-modify-write on a single WP option, so an active VB session's next save can clobber this update.",
235
235
  inputSchema: {
236
236
  gcid: z
237
237
  .string()
238
- .describe('Global color ID, e.g. "gcid-abc123..." (must start with "gcid-"). Get from diviops_get_global_colors.'),
238
+ .describe('Global color ID, e.g. "gcid-abc123..." (must start with "gcid-"). Get from diviops_global_color_list.'),
239
239
  color: z
240
240
  .string()
241
241
  .optional()
@@ -263,14 +263,14 @@ server.registerTool("diviops_update_global_color", {
263
263
  colorEntry.folder = folder;
264
264
  if (status)
265
265
  colorEntry.status = status;
266
- const result = await wp.request("/global-colors", {
266
+ const result = await wp.request("/global-color/upsert", {
267
267
  method: "POST",
268
268
  body: { colors: [colorEntry], mode: "merge" },
269
269
  });
270
270
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
271
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.",
272
+ server.registerTool("diviops_global_color_delete", {
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_global_color_create — an active VB session's next save can re-introduce a color we just deleted if the session held stale data.",
274
274
  inputSchema: {
275
275
  gcid: z
276
276
  .string()
@@ -285,23 +285,23 @@ server.registerTool("diviops_delete_global_color", {
285
285
  const body = { gcid };
286
286
  if (force)
287
287
  body.force = true;
288
- const result = await wp.request("/global-color-delete", {
288
+ const result = await wp.request("/global-color/delete", {
289
289
  method: "POST",
290
290
  body,
291
291
  });
292
292
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
293
293
  });
294
- server.registerTool("diviops_get_global_fonts", {
294
+ server.registerTool("diviops_global_font_list", {
295
295
  description: "Get the global font definitions from Divi settings.",
296
296
  }, async () => {
297
- const result = await wp.request("/global-fonts");
297
+ const result = await wp.request("/global-font/list");
298
298
  return {
299
299
  content: [
300
300
  { type: "text", text: JSON.stringify(result) },
301
301
  ],
302
302
  };
303
303
  });
304
- server.registerTool("diviops_find_icon", {
304
+ server.registerTool("diviops_meta_find_icon", {
305
305
  description: "Search for icons by keyword. Returns matching icons with unicode, type (fa/divi), and weight. Use the returned unicode/type/weight in Blurb icon or Icon module attributes.",
306
306
  inputSchema: {
307
307
  query: z
@@ -319,7 +319,7 @@ server.registerTool("diviops_find_icon", {
319
319
  .describe("Max results (default 10, max 50)"),
320
320
  },
321
321
  }, async ({ query, type, limit }) => {
322
- const result = await wp.request(`/icons/search?q=${encodeURIComponent(query)}&type=${type ?? "all"}&limit=${limit ?? 10}`);
322
+ const result = await wp.request(`/meta/find-icon?q=${encodeURIComponent(query)}&type=${type ?? "all"}&limit=${limit ?? 10}`);
323
323
  return {
324
324
  content: [
325
325
  { type: "text", text: JSON.stringify(result) },
@@ -327,7 +327,7 @@ server.registerTool("diviops_find_icon", {
327
327
  };
328
328
  });
329
329
  // ── Write Tools ──────────────────────────────────────────────────────
330
- server.registerTool("diviops_update_page_content", {
330
+ server.registerTool("diviops_page_update_content", {
331
331
  description: "Update the content of a page with Divi block markup. The content should be valid WordPress block markup using divi/* blocks. IMPORTANT: This overwrites the entire page content.",
332
332
  inputSchema: {
333
333
  page_id: z.number().describe("WordPress post/page ID to update"),
@@ -338,8 +338,8 @@ server.registerTool("diviops_update_page_content", {
338
338
  }, async ({ page_id, content }) => {
339
339
  const hits = findForeignVarRefs(content, "content");
340
340
  if (hits.length > 0)
341
- return isolationErrorResult("diviops_update_page_content", hits);
342
- const result = await wp.request(`/page/${page_id}/content`, {
341
+ return isolationErrorResult("diviops_page_update_content", hits);
342
+ const result = await wp.request(`/page/update-content/${page_id}`, {
343
343
  method: "POST",
344
344
  body: { content },
345
345
  });
@@ -371,7 +371,7 @@ server.registerTool("diviops_validate_blocks", {
371
371
  content: z.string().describe("Divi block markup to validate"),
372
372
  },
373
373
  }, async ({ content }) => {
374
- const result = await wp.request("/validate", {
374
+ const result = await wp.request("/validate/blocks", {
375
375
  method: "POST",
376
376
  body: { content },
377
377
  });
@@ -381,7 +381,7 @@ server.registerTool("diviops_validate_blocks", {
381
381
  ],
382
382
  };
383
383
  });
384
- server.registerTool("diviops_append_section", {
384
+ server.registerTool("diviops_section_append", {
385
385
  description: "Append a Divi section to an existing page without overwriting other content. Use this to incrementally build pages.",
386
386
  inputSchema: {
387
387
  page_id: z.number().describe("WordPress post/page ID"),
@@ -397,8 +397,8 @@ server.registerTool("diviops_append_section", {
397
397
  }, async ({ page_id, content, position }) => {
398
398
  const hits = findForeignVarRefs(content, "content");
399
399
  if (hits.length > 0)
400
- return isolationErrorResult("diviops_append_section", hits);
401
- const result = await wp.request(`/page/${page_id}/append`, {
400
+ return isolationErrorResult("diviops_section_append", hits);
401
+ const result = await wp.request(`/section/append/${page_id}`, {
402
402
  method: "POST",
403
403
  body: { content, position: position ?? "end" },
404
404
  });
@@ -408,7 +408,7 @@ server.registerTool("diviops_append_section", {
408
408
  ],
409
409
  };
410
410
  });
411
- server.registerTool("diviops_replace_section", {
411
+ server.registerTool("diviops_section_replace", {
412
412
  description: "Replace a section on a page. Target by admin label OR text content. Use occurrence when multiple sections match.",
413
413
  inputSchema: {
414
414
  page_id: z.number().describe("WordPress post/page ID"),
@@ -434,13 +434,13 @@ server.registerTool("diviops_replace_section", {
434
434
  }, async ({ page_id, label, match_text, content, occurrence }) => {
435
435
  const hits = findForeignVarRefs(content, "content");
436
436
  if (hits.length > 0)
437
- return isolationErrorResult("diviops_replace_section", hits);
437
+ return isolationErrorResult("diviops_section_replace", hits);
438
438
  const body = { content, occurrence };
439
439
  if (label)
440
440
  body.label = label;
441
441
  if (match_text)
442
442
  body.match_text = match_text;
443
- const result = await wp.request(`/page/${page_id}/replace-section`, {
443
+ const result = await wp.request(`/section/replace/${page_id}`, {
444
444
  method: "POST",
445
445
  body,
446
446
  });
@@ -450,7 +450,7 @@ server.registerTool("diviops_replace_section", {
450
450
  ],
451
451
  };
452
452
  });
453
- server.registerTool("diviops_remove_section", {
453
+ server.registerTool("diviops_section_remove", {
454
454
  description: "Remove a section from a page. Target by admin label OR text content. Use occurrence when multiple sections match.",
455
455
  inputSchema: {
456
456
  page_id: z.number().describe("WordPress post/page ID"),
@@ -476,7 +476,7 @@ server.registerTool("diviops_remove_section", {
476
476
  body.label = label;
477
477
  if (match_text)
478
478
  body.match_text = match_text;
479
- const result = await wp.request(`/page/${page_id}/remove-section`, {
479
+ const result = await wp.request(`/section/remove/${page_id}`, {
480
480
  method: "POST",
481
481
  body,
482
482
  });
@@ -486,7 +486,7 @@ server.registerTool("diviops_remove_section", {
486
486
  ],
487
487
  };
488
488
  });
489
- server.registerTool("diviops_get_section", {
489
+ server.registerTool("diviops_section_get", {
490
490
  description: "Get the raw block markup of a section. Target by admin label OR text content. Use occurrence when multiple sections match. Returns total_matches warning when duplicates exist.",
491
491
  inputSchema: {
492
492
  page_id: z.number().describe("WordPress post/page ID"),
@@ -513,14 +513,14 @@ server.registerTool("diviops_get_section", {
513
513
  if (match_text)
514
514
  params.match_text = match_text;
515
515
  const qs = new URLSearchParams(params).toString();
516
- const result = await wp.request(`/page/${page_id}/get-section?${qs}`);
516
+ const result = await wp.request(`/section/get/${page_id}?${qs}`);
517
517
  return {
518
518
  content: [
519
519
  { type: "text", text: JSON.stringify(result) },
520
520
  ],
521
521
  };
522
522
  });
523
- server.registerTool("diviops_update_module", {
523
+ server.registerTool("diviops_module_update", {
524
524
  description: 'Update specific attributes of a module. Target by auto_index (e.g. "text:5"), admin label, or text content. Uses dot notation for attribute paths. Example: {"content.decoration.headingFont.h2.font.desktop.value.color": "#ff0000"}. For paths whose key segments contain literal dots — notably Composable Settings preset slots like groupPreset["title.decoration.spacing"] — escape the inner dots with `\\.` to keep the segment intact: {"groupPreset.title\\\\.decoration\\\\.spacing.presetId": ["uuid"]}. Priority: auto_index > label > match_text. Use occurrence with label when duplicates exist.',
525
525
  inputSchema: {
526
526
  page_id: z.number().describe("WordPress post/page ID"),
@@ -535,7 +535,7 @@ server.registerTool("diviops_update_module", {
535
535
  auto_index: z
536
536
  .string()
537
537
  .optional()
538
- .describe('Auto-index target in "type:N" format (e.g. "text:5", "icon:3"). Get from diviops_get_page_layout. Takes priority over label/match_text.'),
538
+ .describe('Auto-index target in "type:N" format (e.g. "text:5", "icon:3"). Get from diviops_page_get_layout. Takes priority over label/match_text.'),
539
539
  occurrence: z
540
540
  .number()
541
541
  .int()
@@ -550,7 +550,7 @@ server.registerTool("diviops_update_module", {
550
550
  }, async ({ page_id, label, match_text, auto_index, occurrence, attrs }) => {
551
551
  const hits = scanAttrsForForeignVarRefs(attrs);
552
552
  if (hits.length > 0)
553
- return isolationErrorResult("diviops_update_module", hits);
553
+ return isolationErrorResult("diviops_module_update", hits);
554
554
  const body = { attrs };
555
555
  if (auto_index)
556
556
  body.auto_index = auto_index;
@@ -560,7 +560,7 @@ server.registerTool("diviops_update_module", {
560
560
  body.match_text = match_text;
561
561
  if (occurrence > 1)
562
562
  body.occurrence = occurrence;
563
- const result = await wp.request(`/page/${page_id}/update-module`, {
563
+ const result = await wp.request(`/module/update/${page_id}`, {
564
564
  method: "POST",
565
565
  body,
566
566
  });
@@ -570,7 +570,7 @@ server.registerTool("diviops_update_module", {
570
570
  ],
571
571
  };
572
572
  });
573
- server.registerTool("diviops_move_module", {
573
+ server.registerTool("diviops_module_move", {
574
574
  description: 'Move a module to a new position on the page. Specify source and target blocks using auto_index (e.g. "text:3"), admin label, or text content. Position "before" or "after" the target. Works with any block type including sections, rows, and modules. Both blocks are found in the original content, so auto_index values refer to positions before the move.',
575
575
  inputSchema: {
576
576
  page_id: z.number().describe("WordPress post/page ID"),
@@ -634,7 +634,7 @@ server.registerTool("diviops_move_module", {
634
634
  body.target_auto_index = target_auto_index;
635
635
  if (target_occurrence > 1)
636
636
  body.target_occurrence = target_occurrence;
637
- const result = await wp.request(`/page/${page_id}/move-module`, {
637
+ const result = await wp.request(`/module/move/${page_id}`, {
638
638
  method: "POST",
639
639
  body,
640
640
  });
@@ -644,8 +644,8 @@ server.registerTool("diviops_move_module", {
644
644
  ],
645
645
  };
646
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.',
647
+ server.registerTool("diviops_module_lock", {
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_module_update — pick one of label / match_text / auto_index. Use diviops_module_unlock to reverse.',
649
649
  inputSchema: {
650
650
  page_id: z.number().describe("WordPress post/page ID"),
651
651
  label: z.string().optional().describe("Admin label of the module to lock (exact match)"),
@@ -663,11 +663,11 @@ server.registerTool("diviops_lock_module", {
663
663
  body.auto_index = auto_index;
664
664
  if (occurrence && occurrence > 1)
665
665
  body.occurrence = occurrence;
666
- const result = await wp.request(`/page/${page_id}/lock-module`, { method: "POST", body });
666
+ const result = await wp.request(`/module/lock/${page_id}`, { method: "POST", body });
667
667
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
668
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.",
669
+ server.registerTool("diviops_module_unlock", {
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_module_lock.",
671
671
  inputSchema: {
672
672
  page_id: z.number().describe("WordPress post/page ID"),
673
673
  label: z.string().optional().describe("Admin label of the module to unlock (exact match)"),
@@ -685,11 +685,11 @@ server.registerTool("diviops_unlock_module", {
685
685
  body.auto_index = auto_index;
686
686
  if (occurrence && occurrence > 1)
687
687
  body.occurrence = occurrence;
688
- const result = await wp.request(`/page/${page_id}/unlock-module`, { method: "POST", body });
688
+ const result = await wp.request(`/module/unlock/${page_id}`, { method: "POST", body });
689
689
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
690
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.',
691
+ server.registerTool("diviops_module_clone", {
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_module_lock.',
693
693
  inputSchema: {
694
694
  page_id: z.number().describe("WordPress post/page ID"),
695
695
  label: z.string().optional().describe("Admin label of the module to clone (exact match)"),
@@ -710,10 +710,10 @@ server.registerTool("diviops_clone_module", {
710
710
  body.occurrence = occurrence;
711
711
  if (position)
712
712
  body.position = position;
713
- const result = await wp.request(`/page/${page_id}/clone-module`, { method: "POST", body });
713
+ const result = await wp.request(`/module/clone/${page_id}`, { method: "POST", body });
714
714
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
715
715
  });
716
- server.registerTool("diviops_create_page", {
716
+ server.registerTool("diviops_page_create", {
717
717
  description: "Create a new WordPress page, optionally with Divi block content.",
718
718
  inputSchema: {
719
719
  title: z.string().describe("Page title"),
@@ -732,7 +732,7 @@ server.registerTool("diviops_create_page", {
732
732
  if (content) {
733
733
  const hits = findForeignVarRefs(content, "content");
734
734
  if (hits.length > 0)
735
- return isolationErrorResult("diviops_create_page", hits);
735
+ return isolationErrorResult("diviops_page_create", hits);
736
736
  }
737
737
  const result = await wp.request("/page/create", {
738
738
  method: "POST",
@@ -744,11 +744,74 @@ server.registerTool("diviops_create_page", {
744
744
  ],
745
745
  };
746
746
  });
747
+ server.registerTool("diviops_page_trash", {
748
+ description: "Trash or permanently delete a page/post. Defaults to trash (reversible via WP Admin → Trash). Pass force=true to permanently delete (wp_delete_post — irreversible). Idempotent: trashing an already-trashed post is a no-op. Pass dry_run=true to preview without mutating. Replaces wp-cli `post delete --force=0|1` routing for AI-agent callers (typed input, deterministic envelope).",
749
+ inputSchema: {
750
+ post_id: z.number().int().describe("WordPress post/page ID"),
751
+ force: z
752
+ .boolean()
753
+ .optional()
754
+ .default(false)
755
+ .describe("When true, permanently delete (skips trash). Default false moves to trash."),
756
+ dry_run: z
757
+ .boolean()
758
+ .optional()
759
+ .default(false)
760
+ .describe("When true, return the change plan without mutating state."),
761
+ },
762
+ }, async ({ post_id, force, dry_run }) => {
763
+ const result = await wp.request(`/page/trash/${post_id}`, {
764
+ method: "POST",
765
+ body: {
766
+ force: force ?? false,
767
+ dry_run: dry_run ?? false,
768
+ },
769
+ });
770
+ return {
771
+ content: [
772
+ { type: "text", text: JSON.stringify(result) },
773
+ ],
774
+ };
775
+ });
776
+ server.registerTool("diviops_page_update_status", {
777
+ description: "Update a page's post_status. Valid statuses: publish, draft, private, pending, future. status='future' requires date_gmt (ISO 8601 UTC, must be in the future) — server writes both post_date_gmt and the site-tz post_date so WP's scheduler picks it up. status='publish' on a previously-scheduled post clears the future date so it publishes immediately. Idempotent: same-status update is a no-op. Pass dry_run=true to preview. Replaces wp-cli `post update --post_status=...` routing.",
778
+ inputSchema: {
779
+ post_id: z.number().int().describe("WordPress post/page ID"),
780
+ status: z
781
+ .enum(["publish", "draft", "private", "pending", "future"])
782
+ .describe("Target post status"),
783
+ date_gmt: z
784
+ .string()
785
+ .optional()
786
+ .describe("Required when status='future'. ISO 8601 UTC datetime (e.g. '2026-06-01T09:00:00Z'). Must be in the future."),
787
+ dry_run: z
788
+ .boolean()
789
+ .optional()
790
+ .default(false)
791
+ .describe("When true, return the change plan without mutating state."),
792
+ },
793
+ }, async ({ post_id, status, date_gmt, dry_run }) => {
794
+ const body = {
795
+ status,
796
+ dry_run: dry_run ?? false,
797
+ };
798
+ if (date_gmt)
799
+ body.date_gmt = date_gmt;
800
+ const result = await wp.request(`/page/update-status/${post_id}`, {
801
+ method: "POST",
802
+ body,
803
+ });
804
+ return {
805
+ content: [
806
+ { type: "text", text: JSON.stringify(result) },
807
+ ],
808
+ };
809
+ });
747
810
  // ── Preset Tools ────────────────────────────────────────────────────
748
811
  server.registerTool("diviops_preset_audit", {
749
812
  description: "Audit all Divi presets (module + group). Each entry reports `block_ref_count` (page-content refs via modulePreset / groupPreset block markup), `group_ref_count` (in-registry chain refs from other presets — module presets via top-level `groupPresets.<slot>.presetId`, group presets via `attrs.groupPreset.<slot>.presetId`), and `referenced` (true if either > 0). Group presets that are chain-referenced also expose `referenced_by_presets` (UUIDs of the presets that wire them in — typically module presets, but type-agnostic). Use this before deleting — orphan-cleanup based only on page refs would silently wipe load-bearing chain-wired group presets (font, border, box-shadow, spacing, button). Also reports `orphan_default_pointers`: per-bucket `default` pointers that reference a UUID no longer present in `items[]` (caused by past unsafe deletes). Render-safe but blocks Divi's lazy recreate-on-VB-use path; clear via diviops_preset_set_default with unset=true on the affected module/group.",
750
813
  }, async () => {
751
- const result = await wp.request("/preset-audit");
814
+ const result = await wp.request("/preset/audit");
752
815
  return {
753
816
  content: [
754
817
  { type: "text", text: JSON.stringify(result) },
@@ -791,7 +854,7 @@ server.registerTool("diviops_preset_cleanup", {
791
854
  body.prefix = prefix;
792
855
  if (action === "remove_orphans" && scope)
793
856
  body.scope = scope;
794
- const result = await wp.request("/preset-cleanup", {
857
+ const result = await wp.request("/preset/cleanup", {
795
858
  method: "POST",
796
859
  body,
797
860
  });
@@ -824,7 +887,7 @@ server.registerTool("diviops_preset_update", {
824
887
  body.attrs = attrs;
825
888
  if (typeof priority === "number")
826
889
  body.priority = priority;
827
- const result = await wp.request("/preset-update", {
890
+ const result = await wp.request("/preset/update", {
828
891
  method: "POST",
829
892
  body,
830
893
  });
@@ -847,7 +910,7 @@ server.registerTool("diviops_preset_delete", {
847
910
  const body = { preset_id };
848
911
  if (force !== undefined)
849
912
  body.force = force;
850
- const result = await wp.request("/preset-delete", {
913
+ const result = await wp.request("/preset/delete", {
851
914
  method: "POST",
852
915
  body,
853
916
  });
@@ -909,7 +972,7 @@ server.registerTool("diviops_preset_create", {
909
972
  body.make_default = true;
910
973
  if (typeof priority === "number")
911
974
  body.priority = priority;
912
- const result = await wp.request("/preset-create", { method: "POST", body });
975
+ const result = await wp.request("/preset/create", { method: "POST", body });
913
976
  return {
914
977
  content: [
915
978
  { type: "text", text: JSON.stringify(result) },
@@ -955,7 +1018,7 @@ server.registerTool("diviops_preset_reassign", {
955
1018
  };
956
1019
  if (page_ids)
957
1020
  body.page_ids = page_ids;
958
- const result = await wp.request("/preset-reassign", {
1021
+ const result = await wp.request("/preset/reassign", {
959
1022
  method: "POST",
960
1023
  body,
961
1024
  });
@@ -968,7 +1031,7 @@ server.registerTool("diviops_preset_reassign", {
968
1031
  server.registerTool("diviops_preset_scan_orphans", {
969
1032
  description: "Scan page content for modulePreset UUIDs that are not in the D5 registry. Categorizes as dangling orphans (preset was deleted, reference remains) or D4-legacy candidates (preset exists in the legacy builder_global_presets_ng option but not in D5). Use before diviops_preset_reassign to identify stale UUIDs for consolidation.",
970
1033
  }, async () => {
971
- const result = await wp.request("/preset-scan-orphans");
1034
+ const result = await wp.request("/preset/scan-orphans");
972
1035
  return {
973
1036
  content: [
974
1037
  { type: "text", text: JSON.stringify(result) },
@@ -1005,7 +1068,7 @@ server.registerTool("diviops_preset_set_default", {
1005
1068
  body.module = module;
1006
1069
  if (unset)
1007
1070
  body.unset = true;
1008
- const result = await wp.request("/preset-set-default", {
1071
+ const result = await wp.request("/preset/set-default", {
1009
1072
  method: "POST",
1010
1073
  body,
1011
1074
  });
@@ -1016,7 +1079,7 @@ server.registerTool("diviops_preset_set_default", {
1016
1079
  };
1017
1080
  });
1018
1081
  // ── Library Tools ───────────────────────────────────────────────────
1019
- server.registerTool("diviops_list_library", {
1082
+ server.registerTool("diviops_library_list", {
1020
1083
  description: "List saved Divi Library items. Filter by layout_type (section, row, module) and scope (global, non_global).",
1021
1084
  inputSchema: {
1022
1085
  layout_type: z
@@ -1041,27 +1104,27 @@ server.registerTool("diviops_list_library", {
1041
1104
  params.scope = scope;
1042
1105
  if (per_page)
1043
1106
  params.per_page = String(per_page);
1044
- const result = await wp.request("/library", { params });
1107
+ const result = await wp.request("/library/items", { params });
1045
1108
  return {
1046
1109
  content: [
1047
1110
  { type: "text", text: JSON.stringify(result) },
1048
1111
  ],
1049
1112
  };
1050
1113
  });
1051
- server.registerTool("diviops_get_library_item", {
1052
- description: "Get a Divi Library item's content by ID. Returns the raw block markup that can be used with diviops_append_section or diviops_update_page_content.",
1114
+ server.registerTool("diviops_library_get", {
1115
+ description: "Get a Divi Library item's content by ID. Returns the raw block markup that can be used with diviops_section_append or diviops_page_update_content.",
1053
1116
  inputSchema: {
1054
1117
  item_id: z.number().describe("Library item ID"),
1055
1118
  },
1056
1119
  }, async ({ item_id }) => {
1057
- const result = await wp.request(`/library/${item_id}`);
1120
+ const result = await wp.request(`/library/item/${item_id}`);
1058
1121
  return {
1059
1122
  content: [
1060
1123
  { type: "text", text: JSON.stringify(result) },
1061
1124
  ],
1062
1125
  };
1063
1126
  });
1064
- server.registerTool("diviops_save_to_library", {
1127
+ server.registerTool("diviops_library_save", {
1065
1128
  description: 'Save Divi block markup to the Divi Library for reuse. Saved items appear in the VB\'s "Add From Library" panel.',
1066
1129
  inputSchema: {
1067
1130
  title: z.string().describe("Display name for the library item"),
@@ -1096,7 +1159,7 @@ server.registerTool("diviops_save_to_library", {
1096
1159
  };
1097
1160
  });
1098
1161
  // ── Theme Builder Tools ─────────────────────────────────────────────
1099
- server.registerTool("diviops_list_tb_templates", {
1162
+ server.registerTool("diviops_tb_template_list", {
1100
1163
  description: "List all Theme Builder templates with their conditions, layout IDs, and enabled status. Shows which template applies to which pages/post types.",
1101
1164
  inputSchema: {
1102
1165
  per_page: z
@@ -1113,36 +1176,36 @@ server.registerTool("diviops_list_tb_templates", {
1113
1176
  params.per_page = String(per_page);
1114
1177
  if (page)
1115
1178
  params.page = String(page);
1116
- const result = await wp.request("/theme-builder/templates", { params });
1179
+ const result = await wp.request("/theme-builder/template/list", { params });
1117
1180
  return {
1118
1181
  content: [
1119
1182
  { type: "text", text: JSON.stringify(result) },
1120
1183
  ],
1121
1184
  };
1122
1185
  });
1123
- server.registerTool("diviops_get_tb_layout", {
1124
- description: "Get a Theme Builder layout's block markup content (header, body, or footer). Use the layout IDs from diviops_list_tb_templates.",
1186
+ server.registerTool("diviops_tb_layout_get", {
1187
+ description: "Get a Theme Builder layout's block markup content (header, body, or footer). Use the layout IDs from diviops_tb_template_list.",
1125
1188
  inputSchema: {
1126
1189
  layout_id: z
1127
1190
  .number()
1128
1191
  .describe("Layout post ID (from template header_layout_id, body_layout_id, or footer_layout_id)"),
1129
1192
  },
1130
1193
  }, async ({ layout_id }) => {
1131
- const result = await wp.request(`/theme-builder/layout/${layout_id}`);
1194
+ const result = await wp.request(`/theme-builder/layout/get/${layout_id}`);
1132
1195
  return {
1133
1196
  content: [
1134
1197
  { type: "text", text: JSON.stringify(result) },
1135
1198
  ],
1136
1199
  };
1137
1200
  });
1138
- server.registerTool("diviops_update_tb_layout", {
1201
+ server.registerTool("diviops_tb_layout_update", {
1139
1202
  description: "Update a Theme Builder layout's block markup (header, body, or footer). Replaces the full content.",
1140
1203
  inputSchema: {
1141
1204
  layout_id: z.number().describe("Layout post ID to update"),
1142
1205
  content: z.string().describe("New block markup content"),
1143
1206
  },
1144
1207
  }, async ({ layout_id, content }) => {
1145
- const result = await wp.request(`/theme-builder/layout/${layout_id}`, {
1208
+ const result = await wp.request(`/theme-builder/layout/update/${layout_id}`, {
1146
1209
  method: "PUT",
1147
1210
  body: { content },
1148
1211
  });
@@ -1152,7 +1215,7 @@ server.registerTool("diviops_update_tb_layout", {
1152
1215
  ],
1153
1216
  };
1154
1217
  });
1155
- server.registerTool("diviops_create_tb_template", {
1218
+ server.registerTool("diviops_tb_template_create", {
1156
1219
  description: "Create a Theme Builder template with custom header and/or footer. Automatically creates layout posts, sets conditions, and links to Theme Builder.",
1157
1220
  inputSchema: {
1158
1221
  title: z.string().describe('Template name (e.g. "Landing Pages")'),
@@ -1171,7 +1234,7 @@ server.registerTool("diviops_create_tb_template", {
1171
1234
  .describe("Footer block markup (empty = inherit from default template)"),
1172
1235
  },
1173
1236
  }, async ({ title, condition, header_content, footer_content }) => {
1174
- const result = await wp.request("/theme-builder/template", {
1237
+ const result = await wp.request("/theme-builder/template/create", {
1175
1238
  method: "POST",
1176
1239
  body: { title, condition, header_content, footer_content },
1177
1240
  });
@@ -1182,7 +1245,7 @@ server.registerTool("diviops_create_tb_template", {
1182
1245
  };
1183
1246
  });
1184
1247
  // ── Canvas Tools ────────────────────────────────────────────────────
1185
- server.registerTool("diviops_create_canvas", {
1248
+ server.registerTool("diviops_canvas_create", {
1186
1249
  description: "Create a canvas (off-canvas workspace) linked to a page. Used for popups, off-canvas menus, modals. Content uses standard Divi block markup.",
1187
1250
  inputSchema: {
1188
1251
  title: z
@@ -1226,7 +1289,7 @@ server.registerTool("diviops_create_canvas", {
1226
1289
  ],
1227
1290
  };
1228
1291
  });
1229
- server.registerTool("diviops_list_canvases", {
1292
+ server.registerTool("diviops_canvas_list", {
1230
1293
  description: "List canvases (off-canvas workspaces). Filter by parent page or list all.",
1231
1294
  inputSchema: {
1232
1295
  parent_page_id: z
@@ -1248,29 +1311,29 @@ server.registerTool("diviops_list_canvases", {
1248
1311
  params.parent_page_id = String(parent_page_id);
1249
1312
  if (per_page)
1250
1313
  params.per_page = String(per_page);
1251
- const result = await wp.request("/canvases", { params });
1314
+ const result = await wp.request("/canvas/list", { params });
1252
1315
  return {
1253
1316
  content: [
1254
1317
  { type: "text", text: JSON.stringify(result) },
1255
1318
  ],
1256
1319
  };
1257
1320
  });
1258
- server.registerTool("diviops_get_canvas", {
1321
+ server.registerTool("diviops_canvas_get", {
1259
1322
  description: "Get a canvas's block content and metadata.",
1260
1323
  inputSchema: {
1261
1324
  canvas_post_id: z
1262
1325
  .number()
1263
- .describe("Canvas post ID (from diviops_list_canvases)"),
1326
+ .describe("Canvas post ID (from diviops_canvas_list)"),
1264
1327
  },
1265
1328
  }, async ({ canvas_post_id }) => {
1266
- const result = await wp.request(`/canvas/${canvas_post_id}`);
1329
+ const result = await wp.request(`/canvas/get/${canvas_post_id}`);
1267
1330
  return {
1268
1331
  content: [
1269
1332
  { type: "text", text: JSON.stringify(result) },
1270
1333
  ],
1271
1334
  };
1272
1335
  });
1273
- server.registerTool("diviops_update_canvas", {
1336
+ server.registerTool("diviops_canvas_update", {
1274
1337
  description: "Update a canvas's content and/or metadata. Content replaces the entire canvas.",
1275
1338
  inputSchema: {
1276
1339
  canvas_post_id: z.number().describe("Canvas post ID"),
@@ -1295,7 +1358,7 @@ server.registerTool("diviops_update_canvas", {
1295
1358
  body.append_to_main = append_to_main;
1296
1359
  if (z_index !== undefined)
1297
1360
  body.z_index = z_index;
1298
- const result = await wp.request(`/canvas/${canvas_post_id}`, {
1361
+ const result = await wp.request(`/canvas/update/${canvas_post_id}`, {
1299
1362
  method: "POST",
1300
1363
  body,
1301
1364
  });
@@ -1305,14 +1368,14 @@ server.registerTool("diviops_update_canvas", {
1305
1368
  ],
1306
1369
  };
1307
1370
  });
1308
- server.registerTool("diviops_delete_canvas", {
1371
+ server.registerTool("diviops_canvas_delete", {
1309
1372
  description: "Delete a canvas. This permanently removes the canvas post.",
1310
1373
  inputSchema: {
1311
1374
  canvas_post_id: z.number().describe("Canvas post ID to delete"),
1312
1375
  },
1313
1376
  }, async ({ canvas_post_id }) => {
1314
- const result = await wp.request(`/canvas/${canvas_post_id}`, {
1315
- method: "DELETE",
1377
+ const result = await wp.request(`/canvas/delete/${canvas_post_id}`, {
1378
+ method: "POST",
1316
1379
  });
1317
1380
  return {
1318
1381
  content: [
@@ -1321,7 +1384,7 @@ server.registerTool("diviops_delete_canvas", {
1321
1384
  };
1322
1385
  });
1323
1386
  // ── WP-CLI ──────────────────────────────────────────────────────────
1324
- server.registerTool("diviops_wp_cli", {
1387
+ server.registerTool("diviops_meta_wp_cli", {
1325
1388
  description: "Run a WP-CLI command on the WordPress site. Requires WP_PATH env var (LOCAL_SITE_ID auto-detected from Local by Flywheel), or WP_CLI_CMD for containerized wrappers. Commands validated against a safety allowlist. Default tier covers read ops across options/posts/post-types/taxonomies/users/info/core/db, non-destructive writes (post/term create+update, post meta read/write, cache/rewrite/transient flush), ACF/SCF schema ops (`acf export/import/field-group list/get` plus SCF 6.8.4+ `scf json {status,sync,import,export}` and the `acf json …` aliases), and WXR export. Extended tier (requires DIVIOPS_WP_CLI_ALLOW env var) adds destructive or bulk-modifying ops: option update, post/post meta/term delete, search-replace, import, plugin activate/deactivate, eval-file. Filesystem-touching commands (`wp export`, `acf export/import`, `scf|acf json export/import`) are additionally constrained: path arguments must resolve under a safe root (defaults to `<WP_PATH>/.diviops-tmp/`, overridable via DIVIOPS_WP_CLI_SAFE_FS_ROOT, disable via DIVIOPS_WP_CLI_UNSAFE_FS=1); `wp export` and `scf json export` require an explicit `--dir=<path>` (or `--stdout`). In WP_CLI_CMD wrapper mode, DIVIOPS_WP_CLI_SAFE_FS_ROOT is required for FS-sensitive commands. Prefer the typed `diviops_scf_*` wrappers for SCF round-trips — they're easier to invoke and accept the same safe-root scoping. Use --format=json for structured output. Full allowlist + tier rationale + filesystem semantics in the MCP server README.",
1326
1389
  inputSchema: {
1327
1390
  command: z
@@ -1411,7 +1474,7 @@ server.registerTool("diviops_scf_export", {
1411
1474
  field_groups: z
1412
1475
  .string()
1413
1476
  .optional()
1414
- .describe("Comma-separated field-group ACF keys (`group_abc123`) or admin titles (`My Field Group`). NOT WP post slugs — SCF matches against the def's `key` field or its `title` (case-insensitive). Use `diviops_scf_list_field_groups` to discover keys (post_name column)."),
1477
+ .describe("Comma-separated field-group ACF keys (`group_abc123`) or admin titles (`My Field Group`). NOT WP post slugs — SCF matches against the def's `key` field or its `title` (case-insensitive). Use `diviops_scf_field_group_list` to discover keys (post_name column)."),
1415
1478
  post_types: z
1416
1479
  .string()
1417
1480
  .optional()
@@ -1515,7 +1578,7 @@ server.registerTool("diviops_scf_sync", {
1515
1578
  : `Error: ${result.error}\n${result.output}`;
1516
1579
  return { content: [{ type: "text", text: output }] };
1517
1580
  });
1518
- server.registerTool("diviops_scf_list_field_groups", {
1581
+ server.registerTool("diviops_scf_field_group_list", {
1519
1582
  description: "List all SCF/ACF field groups in the database (post_name = ACF key, post_title, post_status, post_modified). Read-only. Queries the underlying `acf-field-group` post type via `wp post list` — works on both SCF 6.8.4+ (which dropped the legacy `wp acf field-group …` family in favor of the `wp scf json` namespace) and older ACF installs.",
1520
1583
  }, async () => {
1521
1584
  const gate = ensureWpCli();
@@ -1535,7 +1598,7 @@ server.registerTool("diviops_scf_list_field_groups", {
1535
1598
  : `Error: ${result.error}\n${result.output}`;
1536
1599
  return { content: [{ type: "text", text: output }] };
1537
1600
  });
1538
- server.registerTool("diviops_scf_get_field_group", {
1601
+ server.registerTool("diviops_scf_field_group_get", {
1539
1602
  description: "Fetch a single SCF/ACF field group from the `acf-field-group` post type — by ACF key (`group_abc123`, looked up via `post_name`) or by numeric WP post ID. Returns the WP post fields (post_name, post_title, post_content with serialized fields blob, post_status, post_modified). For the parsed/structured field tree including nested fields, use `diviops_scf_export --field-groups=<key> --stdout` instead. Read-only. SCF 6.8.4 dropped the legacy `wp acf field-group get` command, so this wrapper queries the post type directly via `wp post`.",
1540
1603
  inputSchema: {
1541
1604
  key: z
@@ -1598,7 +1661,7 @@ server.registerTool("diviops_scf_get_field_group", {
1598
1661
  content: [
1599
1662
  {
1600
1663
  type: "text",
1601
- text: `No field-group found for key "${key}". Use diviops_scf_list_field_groups to see available keys (post_name field).`,
1664
+ text: `No field-group found for key "${key}". Expected an ACF key (e.g. "group_5f8a1b2c3d4e5") or a numeric WP post ID (e.g. "287"). Use diviops_scf_field_group_list to see available keys (post_name field).`,
1602
1665
  },
1603
1666
  ],
1604
1667
  };
@@ -1615,7 +1678,7 @@ server.registerTool("diviops_scf_get_field_group", {
1615
1678
  return { content: [{ type: "text", text: output }] };
1616
1679
  });
1617
1680
  // ── Connection ──────────────────────────────────────────────────────
1618
- server.registerTool("diviops_test_connection", {
1681
+ server.registerTool("diviops_meta_ping", {
1619
1682
  description: "Test the connection to the WordPress site and verify the Divi MCP plugin is active.",
1620
1683
  }, async () => {
1621
1684
  const result = await wp.testConnection();
@@ -1625,7 +1688,7 @@ server.registerTool("diviops_test_connection", {
1625
1688
  ],
1626
1689
  };
1627
1690
  });
1628
- server.registerTool("diviops_server_info", {
1691
+ server.registerTool("diviops_meta_info", {
1629
1692
  description: "Returns DiviOps MCP server identity, version, license type, and available capabilities.",
1630
1693
  }, async () => {
1631
1694
  const info = {
@@ -1724,8 +1787,8 @@ Attributes are JSON in the block comment. Structure:
1724
1787
  - \`divi/cta\` — Call to action blocks
1725
1788
 
1726
1789
  ## Tips
1727
- 1. Always use \`diviops_get_module_schema\` to check exact attribute names before building markup.
1728
- 2. Use \`diviops_get_page_layout\` on existing pages to learn the format from real examples.
1790
+ 1. Always use \`diviops_schema_get_module\` to check exact attribute names before building markup.
1791
+ 2. Use \`diviops_page_get_layout\` on existing pages to learn the format from real examples.
1729
1792
  3. Use \`diviops_render_preview\` to validate markup before saving.
1730
1793
  `;
1731
1794
  // ── Template Resources ──────────────────────────────────────────────
@@ -1748,7 +1811,7 @@ function loadTemplates() {
1748
1811
  }
1749
1812
  const templates = loadTemplates();
1750
1813
  // Register a list tool so Claude can discover available templates
1751
- server.registerTool("diviops_list_templates", {
1814
+ server.registerTool("diviops_template_list", {
1752
1815
  description: "List available Divi page section templates. Each template contains verified block markup patterns that can be used as a base for page generation.",
1753
1816
  }, async () => {
1754
1817
  const list = Array.from(templates.entries()).map(([name, t]) => ({
@@ -1761,7 +1824,7 @@ server.registerTool("diviops_list_templates", {
1761
1824
  content: [{ type: "text", text: JSON.stringify(list) }],
1762
1825
  };
1763
1826
  });
1764
- server.registerTool("diviops_get_template", {
1827
+ server.registerTool("diviops_template_get", {
1765
1828
  description: "Get a specific Divi template with verified block markup, customizable variables, and usage notes. Use this to generate pages based on proven patterns.",
1766
1829
  inputSchema: {
1767
1830
  template_name: z
@@ -1788,7 +1851,7 @@ server.registerTool("diviops_get_template", {
1788
1851
  };
1789
1852
  });
1790
1853
  // ── Variable Manager CRUD ─────────────────────────────────────────────
1791
- server.registerTool("diviops_list_variables", {
1854
+ server.registerTool("diviops_variable_list", {
1792
1855
  description: "List all design token variables from the Divi Variable Manager. Colors (gcid-*) come from et_global_data, numbers/strings/etc (gvid-*) from et_divi_global_variables. Filter by type or ID prefix.",
1793
1856
  inputSchema: {
1794
1857
  type: z
@@ -1806,14 +1869,14 @@ server.registerTool("diviops_list_variables", {
1806
1869
  params.type = type;
1807
1870
  if (prefix)
1808
1871
  params.prefix = prefix;
1809
- const result = await wp.request("/variables", { params });
1872
+ const result = await wp.request("/variable/list", { params });
1810
1873
  return {
1811
1874
  content: [
1812
1875
  { type: "text", text: JSON.stringify(result) },
1813
1876
  ],
1814
1877
  };
1815
1878
  });
1816
- server.registerTool("diviops_create_variable", {
1879
+ server.registerTool("diviops_variable_create", {
1817
1880
  description: 'Create a design token variable in the Divi Variable Manager. Colors (type "colors") use gcid-* IDs and hex values. Numbers/strings/etc use gvid-* IDs. For type="numbers" fluid tokens, pass min+max shorthand (anchors default to 320px/1920px) or explicit targets — server generates arithmetically-correct clamp() formulas. All-px inputs emit px (safe default, root-agnostic). Rem inputs OR rem output require explicit opt-in: pass output_unit="rem" (accepts the 1rem=16px default) or root_font_size_px:N (declares your site\'s actual root font-size for correct rem emission on non-16px-root sites). Mutually exclusive with value.',
1818
1881
  inputSchema: {
1819
1882
  type: z
@@ -1881,8 +1944,8 @@ server.registerTool("diviops_create_variable", {
1881
1944
  ],
1882
1945
  };
1883
1946
  });
1884
- server.registerTool("diviops_create_fluid_system", {
1885
- description: "Batch-emit a fluid typography + spacing + radius variable set in one call — mirrors Divi 5.4.0's Variable Generator Modal at the algorithm level (clamp() math is identical to diviops_create_variable's fluid mode) but layers profile-selectable anchors over it. Each category is independent and optional. Use for: (1) bootstrapping a design system in one call instead of 20+ individual diviops_create_variable invocations; (2) mirroring ET's variable layout so your tokens coexist with VB-generated ones in the Variable Manager; (3) deterministic preflight via dry_run before committing the registry change. By default, refuses to overwrite existing IDs (returns them in `skipped`) — pass overwrite=true to update in place. Persists in a single atomic write to the variable registry; mid-batch failures roll back cleanly.",
1947
+ server.registerTool("diviops_variable_create_fluid_system", {
1948
+ description: "Batch-emit a fluid typography + spacing + radius variable set in one call — mirrors Divi 5.4.0's Variable Generator Modal at the algorithm level (clamp() math is identical to diviops_variable_create's fluid mode) but layers profile-selectable anchors over it. Each category is independent and optional. Use for: (1) bootstrapping a design system in one call instead of 20+ individual diviops_variable_create invocations; (2) mirroring ET's variable layout so your tokens coexist with VB-generated ones in the Variable Manager; (3) deterministic preflight via dry_run before committing the registry change. By default, refuses to overwrite existing IDs (returns them in `skipped`) — pass overwrite=true to update in place. Persists in a single atomic write to the variable registry; mid-batch failures roll back cleanly.",
1886
1949
  inputSchema: {
1887
1950
  profile: z
1888
1951
  .enum(["divi-default", "wide", "custom"])
@@ -2039,7 +2102,7 @@ server.registerTool("diviops_create_fluid_system", {
2039
2102
  body.dry_run = dry_run;
2040
2103
  if (overwrite !== undefined)
2041
2104
  body.overwrite = overwrite;
2042
- const result = await wp.request("/variables-create-fluid-system", {
2105
+ const result = await wp.request("/variable/create-fluid-system", {
2043
2106
  method: "POST",
2044
2107
  body,
2045
2108
  });
@@ -2049,8 +2112,8 @@ server.registerTool("diviops_create_fluid_system", {
2049
2112
  ],
2050
2113
  };
2051
2114
  });
2052
- server.registerTool("diviops_delete_variable", {
2053
- description: "Delete a design token variable by ID. Auto-detects storage from ID prefix (gcid-* = colors, gvid-* = numbers/strings/etc). Returns HTTP 409 when live references exist unless force=true — run diviops_variables_scan_orphans to see where the references live. Returns HTTP 403 for Divi's customizer-bound defaults (gcid-primary-color, gcid-secondary-color, gcid-heading-color, gcid-body-color, gcid-link-color); those are managed via WP Customizer theme options and can't be deleted via this tool.",
2115
+ server.registerTool("diviops_variable_delete", {
2116
+ description: "Delete a design token variable by ID. Auto-detects storage from ID prefix (gcid-* = colors, gvid-* = numbers/strings/etc). Returns HTTP 409 when live references exist unless force=true — run diviops_variable_scan_orphans to see where the references live. Returns HTTP 403 for Divi's customizer-bound defaults (gcid-primary-color, gcid-secondary-color, gcid-heading-color, gcid-body-color, gcid-link-color); those are managed via WP Customizer theme options and can't be deleted via this tool.",
2054
2117
  inputSchema: {
2055
2118
  id: z
2056
2119
  .string()
@@ -2059,7 +2122,7 @@ server.registerTool("diviops_delete_variable", {
2059
2122
  .boolean()
2060
2123
  .optional()
2061
2124
  .default(false)
2062
- .describe("Delete even if live references exist. Orphans will remain in page/preset content and render as invalid CSS on the frontend — run diviops_variables_scan_orphans afterwards to audit."),
2125
+ .describe("Delete even if live references exist. Orphans will remain in page/preset content and render as invalid CSS on the frontend — run diviops_variable_scan_orphans afterwards to audit."),
2063
2126
  },
2064
2127
  }, async ({ id, force }) => {
2065
2128
  const result = await wp.request("/variable/delete", {
@@ -2072,18 +2135,18 @@ server.registerTool("diviops_delete_variable", {
2072
2135
  ],
2073
2136
  };
2074
2137
  });
2075
- server.registerTool("diviops_variables_scan_orphans", {
2138
+ server.registerTool("diviops_variable_scan_orphans", {
2076
2139
  description: "Scan pages, Theme Builder layouts (header/body/footer), Divi Library items, canvas pages, and the preset registry for gvid-/gcid- references that have no backing entry in the Variable Manager (orphans), plus variables defined but referenced nowhere (unused). Orphans render as invalid CSS on the frontend — the $variable()$ resolver falls through with no fallback. Use after a deletion with force=true, or periodically as a hygiene check. Symmetric to diviops_preset_scan_orphans.",
2077
2140
  }, async () => {
2078
- const result = await wp.request("/variables-scan-orphans");
2141
+ const result = await wp.request("/variable/scan-orphans");
2079
2142
  return {
2080
2143
  content: [
2081
2144
  { type: "text", text: JSON.stringify(result) },
2082
2145
  ],
2083
2146
  };
2084
2147
  });
2085
- server.registerTool("diviops_variables_used_on_page", {
2086
- description: "Detect which numeric/font variable IDs a single page actually emits — the exact set Divi 5.4.0+ uses to scope selective `:root{--gvid-*}` CSS variable emission. Walks the same content stack the frontend assembles: post_content + active Theme Builder header/body/footer template content + appended canvas content (interaction targets etc.), plus presets referenced by that content. NOTE: this is `gvid-*` only — color variables (`gcid-*`) are emitted via a separate path (`GlobalData` color block) that is NOT scoped per-page in 5.4.0; this tool returns gvid IDs only. Use for per-page orphan validation (complements global diviops_variables_scan_orphans), preflight before bulk variable rename (know which pages are affected), or to debug why a numeric/font variable doesn't render on a specific page. Read-only. Returns variable_ids (sorted, deduped), count, and the tb_template_ids resolved for that post.",
2148
+ server.registerTool("diviops_variable_used_on_page", {
2149
+ description: "Detect which numeric/font variable IDs a single page actually emits — the exact set Divi 5.4.0+ uses to scope selective `:root{--gvid-*}` CSS variable emission. Walks the same content stack the frontend assembles: post_content + active Theme Builder header/body/footer template content + appended canvas content (interaction targets etc.), plus presets referenced by that content. NOTE: this is `gvid-*` only — color variables (`gcid-*`) are emitted via a separate path (`GlobalData` color block) that is NOT scoped per-page in 5.4.0; this tool returns gvid IDs only. Use for per-page orphan validation (complements global diviops_variable_scan_orphans), preflight before bulk variable rename (know which pages are affected), or to debug why a numeric/font variable doesn't render on a specific page. Read-only. Returns variable_ids (sorted, deduped), count, and the tb_template_ids resolved for that post.",
2087
2150
  inputSchema: {
2088
2151
  post_id: z
2089
2152
  .number()
@@ -2092,14 +2155,14 @@ server.registerTool("diviops_variables_used_on_page", {
2092
2155
  .describe("WordPress post/page ID. The page does not need to be Divi-built — TB templates and canvases attached to non-Divi posts are still scanned."),
2093
2156
  },
2094
2157
  }, async ({ post_id }) => {
2095
- const result = await wp.request(`/variables-used-on-page/${post_id}`);
2158
+ const result = await wp.request(`/variable/used-on-page/${post_id}`);
2096
2159
  return {
2097
2160
  content: [
2098
2161
  { type: "text", text: JSON.stringify(result) },
2099
2162
  ],
2100
2163
  };
2101
2164
  });
2102
- server.registerTool("diviops_flush_static_cache", {
2165
+ server.registerTool("diviops_meta_flush_cache", {
2103
2166
  description: "Flush Divi's compiled static CSS cache under wp-content/et-cache/. wp cache flush does NOT touch these files — the frontend can keep serving stale CSS after a preset/variable/module mutation until the cache is cleared. Delegates to Divi's native ET_Core_PageResource::remove_static_resources when available (response backend: \"divi_native\"), which additionally clears Theme Builder CSS scattered across other post dirs, archive/taxonomy/home/notfound CSS, the object cache, module features cache, post features cache, Google Fonts cache, dynamic assets cache, and post meta caches. Falls back to a targeted filesystem walk of numeric-named et-cache subdirs when the Divi class is absent (backend: \"fs_fallback\"). Provide exactly one selector — no site-wide default to prevent accidental full flush. Idempotent: missing cache root returns 200 with empty list.",
2104
2167
  inputSchema: {
2105
2168
  post_id: z
@@ -2128,7 +2191,7 @@ server.registerTool("diviops_flush_static_cache", {
2128
2191
  body.all = true;
2129
2192
  if (after !== undefined)
2130
2193
  body.after = after;
2131
- const result = await wp.request("/flush-static-cache", {
2194
+ const result = await wp.request("/meta/flush-cache", {
2132
2195
  method: "POST",
2133
2196
  body,
2134
2197
  });
package/dist/wp-client.js CHANGED
@@ -26,7 +26,7 @@ import { MIN_PLUGIN_VERSION, compareVersions, } from './compatibility.js';
26
26
  * `0022` into emitted CSS).
27
27
  *
28
28
  * Under-escape (#409 fix). One form produced when an agent transcribes
29
- * `get_section` markup (which emits inner quotes as `&quot;` HTML entities) and
29
+ * `section_get` markup (which emits inner quotes as `&quot;` HTML entities) and
30
30
  * a layer in the agent → MCP → WP pipeline strips one level of escaping:
31
31
  * - bare `"` (1 byte) — the inner quote loses its `\` prefix and prematurely
32
32
  * terminates the OUTER block-attrs string at parse time. The WP block
@@ -67,8 +67,8 @@ function normalizeQuoteEscapes(s) {
67
67
  */
68
68
  const BLOCK_CONTENT_KEYS = new Set([
69
69
  'content', // update_page_content, render_preview, validate_blocks,
70
- // append_section, replace_section, update_tb_layout,
71
- // save_to_library, create_page
70
+ // section_append, section_replace, update_tb_layout,
71
+ // library_save, create_page
72
72
  'attrs', // update_module — attr values embedded in block JSON
73
73
  'header_content', // create_tb_template
74
74
  'footer_content', // create_tb_template
@@ -147,7 +147,7 @@ export class WPClient {
147
147
  */
148
148
  async testConnection() {
149
149
  try {
150
- const result = await this.request('/settings');
150
+ const result = await this.request('/schema/settings');
151
151
  return {
152
152
  ok: true,
153
153
  message: `Connected to Divi ${result.builder?.version ?? 'unknown'}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diviops/mcp-server",
3
- "version": "0.2.29",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server exposing Divi 5 Visual Builder as tools for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",