@diviops/mcp-server 1.1.0 → 1.3.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 +9 -5
- package/dist/compatibility.d.ts +37 -4
- package/dist/compatibility.js +33 -3
- package/dist/index.js +147 -63
- package/dist/wp-client.d.ts +10 -5
- package/dist/wp-client.js +20 -10
- package/package.json +1 -1
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 (
|
|
99
|
+
## Available Tools (66)
|
|
100
100
|
|
|
101
101
|
### Read (30)
|
|
102
102
|
| Tool | Description |
|
|
@@ -132,7 +132,7 @@ The server connects via standard WordPress REST API and works with any environme
|
|
|
132
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
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 (
|
|
135
|
+
### Write (34)
|
|
136
136
|
| Tool | Description |
|
|
137
137
|
|------|-------------|
|
|
138
138
|
| `diviops_page_create` | Create a new page with optional Divi content |
|
|
@@ -163,7 +163,8 @@ The server connects via standard WordPress REST API and works with any environme
|
|
|
163
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
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
165
|
| `diviops_canvas_create` | Create a canvas page |
|
|
166
|
-
| `
|
|
166
|
+
| `diviops_canvas_duplicate` | Deep-copy a canvas (content + canvas-specific meta). Default copy title `<source> (Copy)` auto-suffixes on collision (Copy 2, …); explicit `title` collisions return 409. Supports `dry_run` |
|
|
167
|
+
| `diviops_canvas_update` | Update canvas content and/or metadata. Pass any subset of fields — `{canvas_post_id, title}` renames without touching content |
|
|
167
168
|
| `diviops_canvas_delete` | Delete a canvas page |
|
|
168
169
|
| `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` |
|
|
169
170
|
| `diviops_scf_import` | Import SCF schema from a JSON file (mutates DB; idempotent — existing items are updated). Wraps `wp scf json import <file>` |
|
|
@@ -320,8 +321,11 @@ Ensure `WP_URL`, `WP_USER`, and `WP_APP_PASSWORD` are all set. Check your `claud
|
|
|
320
321
|
- Verify the WP plugin is active: visit `{WP_URL}/wp-json/diviops/v1/schema/settings` in your browser
|
|
321
322
|
- Check Application Password is correct (try with curl first)
|
|
322
323
|
|
|
323
|
-
### "
|
|
324
|
-
|
|
324
|
+
### "This tool requires plugin capability" error
|
|
325
|
+
A specific tool failed at the per-tool capability gate (#486) because the active diviops-agent plugin doesn't advertise that capability. Update the plugin to ≥ 1.2.0 (the version that introduced the capability map). Other tools the older plugin does support keep working — the gate is per-tool, not a global floor.
|
|
326
|
+
|
|
327
|
+
### "Server too old for plugin" error
|
|
328
|
+
The plugin returned HTTP 426 because this MCP server is below the plugin's `MIN_SERVER_VERSION`. Update the MCP server (`npm install -g @diviops/mcp-server@latest` or rebuild from source).
|
|
325
329
|
|
|
326
330
|
### "Permission denied" errors
|
|
327
331
|
- The WP user must have `edit_posts` capability (Editor or Admin role)
|
package/dist/compatibility.d.ts
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Server↔plugin compatibility surface.
|
|
3
|
+
*
|
|
4
|
+
* As of #486, the global `MIN_PLUGIN_VERSION` floor is gone. Compatibility
|
|
5
|
+
* is now decided per-tool against a capability map returned by the plugin's
|
|
6
|
+
* `/handshake`. See `wp-client.ts` (handshake parse) and `index.ts`
|
|
7
|
+
* (`requireCapability` gate at each plugin-touching tool entry).
|
|
8
|
+
*
|
|
9
|
+
* Implicit floor: pre-1.2.0 plugins emit `capabilities` as a string[] of
|
|
10
|
+
* coarse namespace tags, not the per-tool map this server expects.
|
|
11
|
+
* `wp-client.ts` normalizes that legacy shape to an empty map, which makes
|
|
12
|
+
* every gated tool fail fast with the upgrade hint. So while there is no
|
|
13
|
+
* declared `MIN_PLUGIN_VERSION` constant, the capability-map shape change
|
|
14
|
+
* effectively sets a 1.2.0 floor for any tool that calls `requireCapability`.
|
|
15
|
+
*
|
|
16
|
+
* `compareVersions` is kept exported because handshake helper code and
|
|
17
|
+
* future per-tool soft-deprecation messages may want it.
|
|
3
18
|
*/
|
|
4
|
-
/** Minimum WP plugin version this server requires. */
|
|
5
|
-
export declare const MIN_PLUGIN_VERSION = "1.1.0";
|
|
6
19
|
/**
|
|
7
20
|
* Compare two semver-like version strings (supports pre-release tags).
|
|
8
21
|
*
|
|
@@ -14,6 +27,15 @@ export declare const MIN_PLUGIN_VERSION = "1.1.0";
|
|
|
14
27
|
* Pre-release versions (e.g. 1.0.0-beta.22) sort before their release (1.0.0).
|
|
15
28
|
*/
|
|
16
29
|
export declare function compareVersions(a: string, b: string): -1 | 0 | 1;
|
|
30
|
+
/**
|
|
31
|
+
* Shape returned by `POST /diviops/v1/handshake`.
|
|
32
|
+
*
|
|
33
|
+
* `capabilities` is a per-tool map keyed by post-rename tool slug
|
|
34
|
+
* (without the `diviops_` prefix). Older plugins (pre-1.2.0) emit
|
|
35
|
+
* a string[] of coarse namespace keys; the server normalizes that
|
|
36
|
+
* legacy shape to an empty map (every gated tool then fails fast
|
|
37
|
+
* with an upgrade hint, which is the intended behavior).
|
|
38
|
+
*/
|
|
17
39
|
export interface HandshakeResult {
|
|
18
40
|
compatible: boolean;
|
|
19
41
|
plugin_version: string;
|
|
@@ -22,5 +44,16 @@ export interface HandshakeResult {
|
|
|
22
44
|
active: boolean;
|
|
23
45
|
version: string | null;
|
|
24
46
|
};
|
|
25
|
-
capabilities: string
|
|
47
|
+
capabilities: Record<string, boolean>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Thrown when a tool handler calls `requireCapability(key)` and the
|
|
51
|
+
* plugin's handshake response did not include `key`. The server's
|
|
52
|
+
* tool dispatch wraps this into the MCP error response, surfacing
|
|
53
|
+
* the upgrade hint to the agent.
|
|
54
|
+
*/
|
|
55
|
+
export declare class MissingCapabilityError extends Error {
|
|
56
|
+
readonly capability: string;
|
|
57
|
+
readonly pluginVersion: string;
|
|
58
|
+
constructor(capability: string, pluginVersion: string);
|
|
26
59
|
}
|
package/dist/compatibility.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Server↔plugin compatibility surface.
|
|
3
|
+
*
|
|
4
|
+
* As of #486, the global `MIN_PLUGIN_VERSION` floor is gone. Compatibility
|
|
5
|
+
* is now decided per-tool against a capability map returned by the plugin's
|
|
6
|
+
* `/handshake`. See `wp-client.ts` (handshake parse) and `index.ts`
|
|
7
|
+
* (`requireCapability` gate at each plugin-touching tool entry).
|
|
8
|
+
*
|
|
9
|
+
* Implicit floor: pre-1.2.0 plugins emit `capabilities` as a string[] of
|
|
10
|
+
* coarse namespace tags, not the per-tool map this server expects.
|
|
11
|
+
* `wp-client.ts` normalizes that legacy shape to an empty map, which makes
|
|
12
|
+
* every gated tool fail fast with the upgrade hint. So while there is no
|
|
13
|
+
* declared `MIN_PLUGIN_VERSION` constant, the capability-map shape change
|
|
14
|
+
* effectively sets a 1.2.0 floor for any tool that calls `requireCapability`.
|
|
15
|
+
*
|
|
16
|
+
* `compareVersions` is kept exported because handshake helper code and
|
|
17
|
+
* future per-tool soft-deprecation messages may want it.
|
|
3
18
|
*/
|
|
4
|
-
/** Minimum WP plugin version this server requires. */
|
|
5
|
-
export const MIN_PLUGIN_VERSION = '1.1.0';
|
|
6
19
|
/**
|
|
7
20
|
* Compare two semver-like version strings (supports pre-release tags).
|
|
8
21
|
*
|
|
@@ -66,3 +79,20 @@ export function compareVersions(a, b) {
|
|
|
66
79
|
}
|
|
67
80
|
return 0;
|
|
68
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Thrown when a tool handler calls `requireCapability(key)` and the
|
|
84
|
+
* plugin's handshake response did not include `key`. The server's
|
|
85
|
+
* tool dispatch wraps this into the MCP error response, surfacing
|
|
86
|
+
* the upgrade hint to the agent.
|
|
87
|
+
*/
|
|
88
|
+
export class MissingCapabilityError extends Error {
|
|
89
|
+
capability;
|
|
90
|
+
pluginVersion;
|
|
91
|
+
constructor(capability, pluginVersion) {
|
|
92
|
+
super(`Tool requires plugin capability "${capability}", which is not present in the active diviops-agent plugin (version ${pluginVersion}). ` +
|
|
93
|
+
'Upgrade the diviops-agent plugin to the version shipped alongside this MCP server release.');
|
|
94
|
+
this.capability = capability;
|
|
95
|
+
this.pluginVersion = pluginVersion;
|
|
96
|
+
this.name = 'MissingCapabilityError';
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
12
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import { WPClient } from "./wp-client.js";
|
|
15
|
+
import { MissingCapabilityError } from "./compatibility.js";
|
|
15
16
|
import { optimizeSchema } from "./schema-optimizer.js";
|
|
16
17
|
import { createWpCli } from "./wp-cli.js";
|
|
17
18
|
import { findForeignVarRefs, scanAttrsForForeignVarRefs, isolationErrorResult, } from "./validate-attrs.js";
|
|
@@ -82,8 +83,49 @@ const server = new McpServer({
|
|
|
82
83
|
name: "diviops-mcp",
|
|
83
84
|
version: SERVER_VERSION,
|
|
84
85
|
});
|
|
86
|
+
let handshakeState = { kind: "pending" };
|
|
87
|
+
function requireCapability(key) {
|
|
88
|
+
// Only gate when we have a real capability map. On handshake failure,
|
|
89
|
+
// bypass the gate so the underlying request surfaces the actual cause
|
|
90
|
+
// (auth, network, 5xx) rather than misattributing it to the plugin
|
|
91
|
+
// version.
|
|
92
|
+
if (handshakeState.kind !== "ok")
|
|
93
|
+
return;
|
|
94
|
+
if (!handshakeState.capabilities[key]) {
|
|
95
|
+
throw new MissingCapabilityError(key, handshakeState.pluginVersion);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// `any` here is deliberate, not laziness. McpServer.registerTool is a
|
|
99
|
+
// multi-overload generic whose `cb`/`InputArgs` machinery doesn't compose
|
|
100
|
+
// with `Parameters<typeof server.registerTool>` (overload collapse to
|
|
101
|
+
// `never`). Restating its Zod-driven generics in this thin wrapper buys
|
|
102
|
+
// no real safety — the per-callsite `inputSchema` Zod object at every
|
|
103
|
+
// usage site below is what enforces actual argument shape; this helper
|
|
104
|
+
// only adds a capability-check + an error envelope on top, both shape-
|
|
105
|
+
// independent. Scope: 4 narrow suppressions, all in this 25-line block.
|
|
106
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
107
|
+
function registerPluginTool(name, config, handler) {
|
|
108
|
+
const key = name.replace(/^diviops_/, "");
|
|
109
|
+
const wrapped = (async (args) => {
|
|
110
|
+
try {
|
|
111
|
+
requireCapability(key);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
if (e instanceof MissingCapabilityError) {
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: e.message }],
|
|
117
|
+
isError: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
throw e;
|
|
121
|
+
}
|
|
122
|
+
return handler(args);
|
|
123
|
+
});
|
|
124
|
+
server.registerTool(name, config, wrapped);
|
|
125
|
+
}
|
|
126
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
85
127
|
// ── Read Tools ───────────────────────────────────────────────────────
|
|
86
|
-
|
|
128
|
+
registerPluginTool("diviops_page_list", {
|
|
87
129
|
description: "List pages/posts in the WordPress site. Returns title, ID, URL, status, and whether each page uses Divi builder.",
|
|
88
130
|
inputSchema: {
|
|
89
131
|
post_type: z
|
|
@@ -112,7 +154,7 @@ server.registerTool("diviops_page_list", {
|
|
|
112
154
|
],
|
|
113
155
|
};
|
|
114
156
|
});
|
|
115
|
-
|
|
157
|
+
registerPluginTool("diviops_page_get", {
|
|
116
158
|
description: "Get detailed info about a specific page including its raw Divi block content.",
|
|
117
159
|
inputSchema: {
|
|
118
160
|
page_id: z.number().describe("WordPress post/page ID"),
|
|
@@ -125,7 +167,7 @@ server.registerTool("diviops_page_get", {
|
|
|
125
167
|
],
|
|
126
168
|
};
|
|
127
169
|
});
|
|
128
|
-
|
|
170
|
+
registerPluginTool("diviops_page_get_layout", {
|
|
129
171
|
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
172
|
inputSchema: {
|
|
131
173
|
page_id: z.number().describe("WordPress post/page ID"),
|
|
@@ -145,7 +187,7 @@ server.registerTool("diviops_page_get_layout", {
|
|
|
145
187
|
],
|
|
146
188
|
};
|
|
147
189
|
});
|
|
148
|
-
|
|
190
|
+
registerPluginTool("diviops_schema_list_modules", {
|
|
149
191
|
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
192
|
}, async () => {
|
|
151
193
|
const result = await wp.request("/schema/modules");
|
|
@@ -155,7 +197,7 @@ server.registerTool("diviops_schema_list_modules", {
|
|
|
155
197
|
],
|
|
156
198
|
};
|
|
157
199
|
});
|
|
158
|
-
|
|
200
|
+
registerPluginTool("diviops_schema_get_module", {
|
|
159
201
|
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
202
|
inputSchema: {
|
|
161
203
|
module_name: z
|
|
@@ -176,7 +218,7 @@ server.registerTool("diviops_schema_get_module", {
|
|
|
176
218
|
],
|
|
177
219
|
};
|
|
178
220
|
});
|
|
179
|
-
|
|
221
|
+
registerPluginTool("diviops_schema_get_settings", {
|
|
180
222
|
description: "Get Divi site settings including theme options, site info, and builder version. Useful for understanding the site context before generating content.",
|
|
181
223
|
}, async () => {
|
|
182
224
|
const result = await wp.request("/schema/settings");
|
|
@@ -186,7 +228,7 @@ server.registerTool("diviops_schema_get_settings", {
|
|
|
186
228
|
],
|
|
187
229
|
};
|
|
188
230
|
});
|
|
189
|
-
|
|
231
|
+
registerPluginTool("diviops_global_color_list", {
|
|
190
232
|
description: "Get the global color palette defined in Divi. Returns all global colors that can be referenced by modules.",
|
|
191
233
|
}, async () => {
|
|
192
234
|
const result = await wp.request("/global-color/list");
|
|
@@ -196,7 +238,7 @@ server.registerTool("diviops_global_color_list", {
|
|
|
196
238
|
],
|
|
197
239
|
};
|
|
198
240
|
});
|
|
199
|
-
|
|
241
|
+
registerPluginTool("diviops_global_color_create", {
|
|
200
242
|
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
243
|
inputSchema: {
|
|
202
244
|
color: z
|
|
@@ -230,7 +272,7 @@ server.registerTool("diviops_global_color_create", {
|
|
|
230
272
|
});
|
|
231
273
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
232
274
|
});
|
|
233
|
-
|
|
275
|
+
registerPluginTool("diviops_global_color_update", {
|
|
234
276
|
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
277
|
inputSchema: {
|
|
236
278
|
gcid: z
|
|
@@ -269,7 +311,7 @@ server.registerTool("diviops_global_color_update", {
|
|
|
269
311
|
});
|
|
270
312
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
271
313
|
});
|
|
272
|
-
|
|
314
|
+
registerPluginTool("diviops_global_color_delete", {
|
|
273
315
|
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
316
|
inputSchema: {
|
|
275
317
|
gcid: z
|
|
@@ -291,7 +333,7 @@ server.registerTool("diviops_global_color_delete", {
|
|
|
291
333
|
});
|
|
292
334
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
293
335
|
});
|
|
294
|
-
|
|
336
|
+
registerPluginTool("diviops_global_font_list", {
|
|
295
337
|
description: "Get the global font definitions from Divi settings.",
|
|
296
338
|
}, async () => {
|
|
297
339
|
const result = await wp.request("/global-font/list");
|
|
@@ -301,7 +343,7 @@ server.registerTool("diviops_global_font_list", {
|
|
|
301
343
|
],
|
|
302
344
|
};
|
|
303
345
|
});
|
|
304
|
-
|
|
346
|
+
registerPluginTool("diviops_meta_find_icon", {
|
|
305
347
|
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
348
|
inputSchema: {
|
|
307
349
|
query: z
|
|
@@ -327,7 +369,7 @@ server.registerTool("diviops_meta_find_icon", {
|
|
|
327
369
|
};
|
|
328
370
|
});
|
|
329
371
|
// ── Write Tools ──────────────────────────────────────────────────────
|
|
330
|
-
|
|
372
|
+
registerPluginTool("diviops_page_update_content", {
|
|
331
373
|
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
374
|
inputSchema: {
|
|
333
375
|
page_id: z.number().describe("WordPress post/page ID to update"),
|
|
@@ -349,7 +391,7 @@ server.registerTool("diviops_page_update_content", {
|
|
|
349
391
|
],
|
|
350
392
|
};
|
|
351
393
|
});
|
|
352
|
-
|
|
394
|
+
registerPluginTool("diviops_render_preview", {
|
|
353
395
|
description: "Render Divi block markup to HTML. Use this to preview what the output will look like before saving. Useful for validation.",
|
|
354
396
|
inputSchema: {
|
|
355
397
|
content: z.string().describe("Divi block markup to render to HTML"),
|
|
@@ -365,7 +407,7 @@ server.registerTool("diviops_render_preview", {
|
|
|
365
407
|
],
|
|
366
408
|
};
|
|
367
409
|
});
|
|
368
|
-
|
|
410
|
+
registerPluginTool("diviops_validate_blocks", {
|
|
369
411
|
description: "Validate Divi block markup before saving. Checks structure (malformed comments, unknown blocks, missing builderVersion), required attributes (layout display on containers), and known pitfalls (button padding path, icon.enable, gradient enabled/positions). Returns errors and warnings.",
|
|
370
412
|
inputSchema: {
|
|
371
413
|
content: z.string().describe("Divi block markup to validate"),
|
|
@@ -381,7 +423,7 @@ server.registerTool("diviops_validate_blocks", {
|
|
|
381
423
|
],
|
|
382
424
|
};
|
|
383
425
|
});
|
|
384
|
-
|
|
426
|
+
registerPluginTool("diviops_section_append", {
|
|
385
427
|
description: "Append a Divi section to an existing page without overwriting other content. Use this to incrementally build pages.",
|
|
386
428
|
inputSchema: {
|
|
387
429
|
page_id: z.number().describe("WordPress post/page ID"),
|
|
@@ -408,7 +450,7 @@ server.registerTool("diviops_section_append", {
|
|
|
408
450
|
],
|
|
409
451
|
};
|
|
410
452
|
});
|
|
411
|
-
|
|
453
|
+
registerPluginTool("diviops_section_replace", {
|
|
412
454
|
description: "Replace a section on a page. Target by admin label OR text content. Use occurrence when multiple sections match.",
|
|
413
455
|
inputSchema: {
|
|
414
456
|
page_id: z.number().describe("WordPress post/page ID"),
|
|
@@ -450,7 +492,7 @@ server.registerTool("diviops_section_replace", {
|
|
|
450
492
|
],
|
|
451
493
|
};
|
|
452
494
|
});
|
|
453
|
-
|
|
495
|
+
registerPluginTool("diviops_section_remove", {
|
|
454
496
|
description: "Remove a section from a page. Target by admin label OR text content. Use occurrence when multiple sections match.",
|
|
455
497
|
inputSchema: {
|
|
456
498
|
page_id: z.number().describe("WordPress post/page ID"),
|
|
@@ -486,7 +528,7 @@ server.registerTool("diviops_section_remove", {
|
|
|
486
528
|
],
|
|
487
529
|
};
|
|
488
530
|
});
|
|
489
|
-
|
|
531
|
+
registerPluginTool("diviops_section_get", {
|
|
490
532
|
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
533
|
inputSchema: {
|
|
492
534
|
page_id: z.number().describe("WordPress post/page ID"),
|
|
@@ -520,7 +562,7 @@ server.registerTool("diviops_section_get", {
|
|
|
520
562
|
],
|
|
521
563
|
};
|
|
522
564
|
});
|
|
523
|
-
|
|
565
|
+
registerPluginTool("diviops_module_update", {
|
|
524
566
|
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
567
|
inputSchema: {
|
|
526
568
|
page_id: z.number().describe("WordPress post/page ID"),
|
|
@@ -570,7 +612,7 @@ server.registerTool("diviops_module_update", {
|
|
|
570
612
|
],
|
|
571
613
|
};
|
|
572
614
|
});
|
|
573
|
-
|
|
615
|
+
registerPluginTool("diviops_module_move", {
|
|
574
616
|
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
617
|
inputSchema: {
|
|
576
618
|
page_id: z.number().describe("WordPress post/page ID"),
|
|
@@ -644,7 +686,7 @@ server.registerTool("diviops_module_move", {
|
|
|
644
686
|
],
|
|
645
687
|
};
|
|
646
688
|
});
|
|
647
|
-
|
|
689
|
+
registerPluginTool("diviops_module_lock", {
|
|
648
690
|
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
691
|
inputSchema: {
|
|
650
692
|
page_id: z.number().describe("WordPress post/page ID"),
|
|
@@ -666,7 +708,7 @@ server.registerTool("diviops_module_lock", {
|
|
|
666
708
|
const result = await wp.request(`/module/lock/${page_id}`, { method: "POST", body });
|
|
667
709
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
668
710
|
});
|
|
669
|
-
|
|
711
|
+
registerPluginTool("diviops_module_unlock", {
|
|
670
712
|
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
713
|
inputSchema: {
|
|
672
714
|
page_id: z.number().describe("WordPress post/page ID"),
|
|
@@ -688,7 +730,7 @@ server.registerTool("diviops_module_unlock", {
|
|
|
688
730
|
const result = await wp.request(`/module/unlock/${page_id}`, { method: "POST", body });
|
|
689
731
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
690
732
|
});
|
|
691
|
-
|
|
733
|
+
registerPluginTool("diviops_module_clone", {
|
|
692
734
|
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
735
|
inputSchema: {
|
|
694
736
|
page_id: z.number().describe("WordPress post/page ID"),
|
|
@@ -713,7 +755,7 @@ server.registerTool("diviops_module_clone", {
|
|
|
713
755
|
const result = await wp.request(`/module/clone/${page_id}`, { method: "POST", body });
|
|
714
756
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
715
757
|
});
|
|
716
|
-
|
|
758
|
+
registerPluginTool("diviops_page_create", {
|
|
717
759
|
description: "Create a new WordPress page, optionally with Divi block content.",
|
|
718
760
|
inputSchema: {
|
|
719
761
|
title: z.string().describe("Page title"),
|
|
@@ -744,7 +786,7 @@ server.registerTool("diviops_page_create", {
|
|
|
744
786
|
],
|
|
745
787
|
};
|
|
746
788
|
});
|
|
747
|
-
|
|
789
|
+
registerPluginTool("diviops_page_trash", {
|
|
748
790
|
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
791
|
inputSchema: {
|
|
750
792
|
post_id: z.number().int().describe("WordPress post/page ID"),
|
|
@@ -773,7 +815,7 @@ server.registerTool("diviops_page_trash", {
|
|
|
773
815
|
],
|
|
774
816
|
};
|
|
775
817
|
});
|
|
776
|
-
|
|
818
|
+
registerPluginTool("diviops_page_update_status", {
|
|
777
819
|
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
820
|
inputSchema: {
|
|
779
821
|
post_id: z.number().int().describe("WordPress post/page ID"),
|
|
@@ -808,7 +850,7 @@ server.registerTool("diviops_page_update_status", {
|
|
|
808
850
|
};
|
|
809
851
|
});
|
|
810
852
|
// ── Preset Tools ────────────────────────────────────────────────────
|
|
811
|
-
|
|
853
|
+
registerPluginTool("diviops_preset_audit", {
|
|
812
854
|
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.",
|
|
813
855
|
}, async () => {
|
|
814
856
|
const result = await wp.request("/preset/audit");
|
|
@@ -818,7 +860,7 @@ server.registerTool("diviops_preset_audit", {
|
|
|
818
860
|
],
|
|
819
861
|
};
|
|
820
862
|
});
|
|
821
|
-
|
|
863
|
+
registerPluginTool("diviops_preset_cleanup", {
|
|
822
864
|
description: 'Clean up presets. Default: remove spam presets. Optional: dedup=true to also remove duplicates, action="rename_strip_prefix" with prefix to strip a name prefix, or action="remove_orphans" with scope="spam"|"all" to remove unreferenced presets. Use dry_run: true (default) to preview.',
|
|
823
865
|
inputSchema: {
|
|
824
866
|
dry_run: z
|
|
@@ -864,7 +906,7 @@ server.registerTool("diviops_preset_cleanup", {
|
|
|
864
906
|
],
|
|
865
907
|
};
|
|
866
908
|
});
|
|
867
|
-
|
|
909
|
+
registerPluginTool("diviops_preset_update", {
|
|
868
910
|
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.",
|
|
869
911
|
inputSchema: {
|
|
870
912
|
preset_id: z.string().describe("Preset ID (UUID or short ID)"),
|
|
@@ -897,7 +939,7 @@ server.registerTool("diviops_preset_update", {
|
|
|
897
939
|
],
|
|
898
940
|
};
|
|
899
941
|
});
|
|
900
|
-
|
|
942
|
+
registerPluginTool("diviops_preset_delete", {
|
|
901
943
|
description: "Delete a specific preset by ID. Use diviops_preset_audit first to verify the preset is unreferenced before deleting. Refuses with 409 preset_is_default if the target is the registered default for its module/group 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.",
|
|
902
944
|
inputSchema: {
|
|
903
945
|
preset_id: z.string().describe("Preset ID to delete"),
|
|
@@ -920,7 +962,7 @@ server.registerTool("diviops_preset_delete", {
|
|
|
920
962
|
],
|
|
921
963
|
};
|
|
922
964
|
});
|
|
923
|
-
|
|
965
|
+
registerPluginTool("diviops_preset_create", {
|
|
924
966
|
description: 'Create a new preset in the Divi 5 registry. For module presets, supply module_name (e.g. "divi/column", "divi/button", "divi/section"), name, and attrs. For group (attribute-level) presets, set type="group" and supply group_name ("divi/font", "divi/button", etc.), group_id ("designTitleText", "button", etc.), and optionally primary_attr_name.',
|
|
925
967
|
inputSchema: {
|
|
926
968
|
module_name: z
|
|
@@ -979,7 +1021,7 @@ server.registerTool("diviops_preset_create", {
|
|
|
979
1021
|
],
|
|
980
1022
|
};
|
|
981
1023
|
});
|
|
982
|
-
|
|
1024
|
+
registerPluginTool("diviops_preset_reassign", {
|
|
983
1025
|
description: 'Reassign a preset UUID across page content. Covers both module-level refs (`attrs.modulePreset[...]`) and attribute-level group-preset refs (`attrs.groupPreset.<slot>.presetId`), plus — for group presets — registry chain refs: module-bucket presets via top-level `groupPresets.<slot>.presetId`, group-bucket presets via `attrs.groupPreset.<slot>.presetId`. The `scope` param controls which ref types are walked (default "both", auto-selects based on new_uuid\'s bucket). Cross-bucket swaps (module ↔ group) are rejected. When `strip_inline=true` (default), strips inline attrs that duplicate the new preset\'s attrs (otherwise inline wins over preset): for module scope, strips from block root; for group scope, strips per-slot using Divi\'s own slot→target-path resolver (handles composite button groups, `-id-classes` suffix, FormField/checkbox/radio `attrName` mappings, cross-module translation). Both scopes enforce a singular-stack guard (skip strip when slot holds multiple presets). Unmappable group slots skip strip and emit a per-slot advisory at `summary.strip_advisory_per_slot[<module>::<slot>]`; neighbor slots are unaffected. Defaults to dry-run — set mode="apply" to actually rewrite. Use this to consolidate repeated inline styling into a reusable preset after creating one with diviops_preset_create.',
|
|
984
1026
|
inputSchema: {
|
|
985
1027
|
old_uuid: z
|
|
@@ -1028,7 +1070,7 @@ server.registerTool("diviops_preset_reassign", {
|
|
|
1028
1070
|
],
|
|
1029
1071
|
};
|
|
1030
1072
|
});
|
|
1031
|
-
|
|
1073
|
+
registerPluginTool("diviops_preset_scan_orphans", {
|
|
1032
1074
|
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.",
|
|
1033
1075
|
}, async () => {
|
|
1034
1076
|
const result = await wp.request("/preset/scan-orphans");
|
|
@@ -1038,7 +1080,7 @@ server.registerTool("diviops_preset_scan_orphans", {
|
|
|
1038
1080
|
],
|
|
1039
1081
|
};
|
|
1040
1082
|
});
|
|
1041
|
-
|
|
1083
|
+
registerPluginTool("diviops_preset_set_default", {
|
|
1042
1084
|
description: "Set or clear the per-module/group default preset. Two addressing modes: (1) preset_id mode — walks both buckets to locate the preset by UUID, then points the containing module/group's `default` slot at it (or clears it with unset=true). (2) Bucket-addressed clear — pass type + module + unset=true to clear an orphan default pointer when the preset_id no longer exists in items[] (the preset_id walk path can't locate orphans — that's the very state being repaired; surfaced via diviops_preset_audit's `orphan_default_pointers`). 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` and `orphan_default_pointers` fields to verify state before/after.",
|
|
1043
1085
|
inputSchema: {
|
|
1044
1086
|
preset_id: z
|
|
@@ -1079,7 +1121,7 @@ server.registerTool("diviops_preset_set_default", {
|
|
|
1079
1121
|
};
|
|
1080
1122
|
});
|
|
1081
1123
|
// ── Library Tools ───────────────────────────────────────────────────
|
|
1082
|
-
|
|
1124
|
+
registerPluginTool("diviops_library_list", {
|
|
1083
1125
|
description: "List saved Divi Library items. Filter by layout_type (section, row, module) and scope (global, non_global).",
|
|
1084
1126
|
inputSchema: {
|
|
1085
1127
|
layout_type: z
|
|
@@ -1111,7 +1153,7 @@ server.registerTool("diviops_library_list", {
|
|
|
1111
1153
|
],
|
|
1112
1154
|
};
|
|
1113
1155
|
});
|
|
1114
|
-
|
|
1156
|
+
registerPluginTool("diviops_library_get", {
|
|
1115
1157
|
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.",
|
|
1116
1158
|
inputSchema: {
|
|
1117
1159
|
item_id: z.number().describe("Library item ID"),
|
|
@@ -1124,7 +1166,7 @@ server.registerTool("diviops_library_get", {
|
|
|
1124
1166
|
],
|
|
1125
1167
|
};
|
|
1126
1168
|
});
|
|
1127
|
-
|
|
1169
|
+
registerPluginTool("diviops_library_save", {
|
|
1128
1170
|
description: 'Save Divi block markup to the Divi Library for reuse. Saved items appear in the VB\'s "Add From Library" panel.',
|
|
1129
1171
|
inputSchema: {
|
|
1130
1172
|
title: z.string().describe("Display name for the library item"),
|
|
@@ -1159,7 +1201,7 @@ server.registerTool("diviops_library_save", {
|
|
|
1159
1201
|
};
|
|
1160
1202
|
});
|
|
1161
1203
|
// ── Theme Builder Tools ─────────────────────────────────────────────
|
|
1162
|
-
|
|
1204
|
+
registerPluginTool("diviops_tb_template_list", {
|
|
1163
1205
|
description: "List all Theme Builder templates with their conditions, layout IDs, and enabled status. Shows which template applies to which pages/post types.",
|
|
1164
1206
|
inputSchema: {
|
|
1165
1207
|
per_page: z
|
|
@@ -1183,7 +1225,7 @@ server.registerTool("diviops_tb_template_list", {
|
|
|
1183
1225
|
],
|
|
1184
1226
|
};
|
|
1185
1227
|
});
|
|
1186
|
-
|
|
1228
|
+
registerPluginTool("diviops_tb_layout_get", {
|
|
1187
1229
|
description: "Get a Theme Builder layout's block markup content (header, body, or footer). Use the layout IDs from diviops_tb_template_list.",
|
|
1188
1230
|
inputSchema: {
|
|
1189
1231
|
layout_id: z
|
|
@@ -1198,7 +1240,7 @@ server.registerTool("diviops_tb_layout_get", {
|
|
|
1198
1240
|
],
|
|
1199
1241
|
};
|
|
1200
1242
|
});
|
|
1201
|
-
|
|
1243
|
+
registerPluginTool("diviops_tb_layout_update", {
|
|
1202
1244
|
description: "Update a Theme Builder layout's block markup (header, body, or footer). Replaces the full content.",
|
|
1203
1245
|
inputSchema: {
|
|
1204
1246
|
layout_id: z.number().describe("Layout post ID to update"),
|
|
@@ -1215,7 +1257,7 @@ server.registerTool("diviops_tb_layout_update", {
|
|
|
1215
1257
|
],
|
|
1216
1258
|
};
|
|
1217
1259
|
});
|
|
1218
|
-
|
|
1260
|
+
registerPluginTool("diviops_tb_template_create", {
|
|
1219
1261
|
description: "Create a Theme Builder template with custom header and/or footer. Automatically creates layout posts, sets conditions, and links to Theme Builder.",
|
|
1220
1262
|
inputSchema: {
|
|
1221
1263
|
title: z.string().describe('Template name (e.g. "Landing Pages")'),
|
|
@@ -1245,7 +1287,7 @@ server.registerTool("diviops_tb_template_create", {
|
|
|
1245
1287
|
};
|
|
1246
1288
|
});
|
|
1247
1289
|
// ── Canvas Tools ────────────────────────────────────────────────────
|
|
1248
|
-
|
|
1290
|
+
registerPluginTool("diviops_canvas_create", {
|
|
1249
1291
|
description: "Create a canvas (off-canvas workspace) linked to a page. Used for popups, off-canvas menus, modals. Content uses standard Divi block markup.",
|
|
1250
1292
|
inputSchema: {
|
|
1251
1293
|
title: z
|
|
@@ -1289,7 +1331,7 @@ server.registerTool("diviops_canvas_create", {
|
|
|
1289
1331
|
],
|
|
1290
1332
|
};
|
|
1291
1333
|
});
|
|
1292
|
-
|
|
1334
|
+
registerPluginTool("diviops_canvas_list", {
|
|
1293
1335
|
description: "List canvases (off-canvas workspaces). Filter by parent page or list all.",
|
|
1294
1336
|
inputSchema: {
|
|
1295
1337
|
parent_page_id: z
|
|
@@ -1318,7 +1360,7 @@ server.registerTool("diviops_canvas_list", {
|
|
|
1318
1360
|
],
|
|
1319
1361
|
};
|
|
1320
1362
|
});
|
|
1321
|
-
|
|
1363
|
+
registerPluginTool("diviops_canvas_get", {
|
|
1322
1364
|
description: "Get a canvas's block content and metadata.",
|
|
1323
1365
|
inputSchema: {
|
|
1324
1366
|
canvas_post_id: z
|
|
@@ -1333,8 +1375,8 @@ server.registerTool("diviops_canvas_get", {
|
|
|
1333
1375
|
],
|
|
1334
1376
|
};
|
|
1335
1377
|
});
|
|
1336
|
-
|
|
1337
|
-
description: "Update a canvas's content and/or metadata.
|
|
1378
|
+
registerPluginTool("diviops_canvas_update", {
|
|
1379
|
+
description: "Update a canvas's content and/or metadata. Pass any subset of fields — e.g. `{canvas_post_id, title}` to rename without touching content. `content` replaces the entire canvas when present. At least one of content/title/append_to_main/z_index is required.",
|
|
1338
1380
|
inputSchema: {
|
|
1339
1381
|
canvas_post_id: z.number().describe("Canvas post ID"),
|
|
1340
1382
|
content: z
|
|
@@ -1368,7 +1410,35 @@ server.registerTool("diviops_canvas_update", {
|
|
|
1368
1410
|
],
|
|
1369
1411
|
};
|
|
1370
1412
|
});
|
|
1371
|
-
|
|
1413
|
+
registerPluginTool("diviops_canvas_duplicate", {
|
|
1414
|
+
description: "Deep-copy a canvas (post_content + canvas-specific meta: parent page, append_to_main, z_index). Source canvas untouched. Default copy title is `<source title> (Copy)` with auto-suffix on collision (Copy 2, Copy 3, …) — use this for repeat-clone workflows. Pass an explicit `title` for a deliberate name; collisions return 409 instead of silently auto-suffixing. Pass `dry_run: true` to preview without mutating.",
|
|
1415
|
+
inputSchema: {
|
|
1416
|
+
canvas_post_id: z.number().describe("Source canvas post ID"),
|
|
1417
|
+
title: z
|
|
1418
|
+
.string()
|
|
1419
|
+
.optional()
|
|
1420
|
+
.describe("Optional explicit title for the duplicate. Omit to auto-derive `<source> (Copy [N])`. Explicit collisions return 409."),
|
|
1421
|
+
dry_run: z
|
|
1422
|
+
.boolean()
|
|
1423
|
+
.optional()
|
|
1424
|
+
.default(false)
|
|
1425
|
+
.describe("When true, return the change plan without creating the canvas."),
|
|
1426
|
+
},
|
|
1427
|
+
}, async ({ canvas_post_id, title, dry_run }) => {
|
|
1428
|
+
const body = { dry_run: dry_run ?? false };
|
|
1429
|
+
if (title !== undefined)
|
|
1430
|
+
body.title = title;
|
|
1431
|
+
const result = await wp.request(`/canvas/duplicate/${canvas_post_id}`, {
|
|
1432
|
+
method: "POST",
|
|
1433
|
+
body,
|
|
1434
|
+
});
|
|
1435
|
+
return {
|
|
1436
|
+
content: [
|
|
1437
|
+
{ type: "text", text: JSON.stringify(result) },
|
|
1438
|
+
],
|
|
1439
|
+
};
|
|
1440
|
+
});
|
|
1441
|
+
registerPluginTool("diviops_canvas_delete", {
|
|
1372
1442
|
description: "Delete a canvas. This permanently removes the canvas post.",
|
|
1373
1443
|
inputSchema: {
|
|
1374
1444
|
canvas_post_id: z.number().describe("Canvas post ID to delete"),
|
|
@@ -1851,7 +1921,7 @@ server.registerTool("diviops_template_get", {
|
|
|
1851
1921
|
};
|
|
1852
1922
|
});
|
|
1853
1923
|
// ── Variable Manager CRUD ─────────────────────────────────────────────
|
|
1854
|
-
|
|
1924
|
+
registerPluginTool("diviops_variable_list", {
|
|
1855
1925
|
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.",
|
|
1856
1926
|
inputSchema: {
|
|
1857
1927
|
type: z
|
|
@@ -1876,7 +1946,7 @@ server.registerTool("diviops_variable_list", {
|
|
|
1876
1946
|
],
|
|
1877
1947
|
};
|
|
1878
1948
|
});
|
|
1879
|
-
|
|
1949
|
+
registerPluginTool("diviops_variable_create", {
|
|
1880
1950
|
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.',
|
|
1881
1951
|
inputSchema: {
|
|
1882
1952
|
type: z
|
|
@@ -1944,7 +2014,7 @@ server.registerTool("diviops_variable_create", {
|
|
|
1944
2014
|
],
|
|
1945
2015
|
};
|
|
1946
2016
|
});
|
|
1947
|
-
|
|
2017
|
+
registerPluginTool("diviops_variable_create_fluid_system", {
|
|
1948
2018
|
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.",
|
|
1949
2019
|
inputSchema: {
|
|
1950
2020
|
profile: z
|
|
@@ -2112,7 +2182,7 @@ server.registerTool("diviops_variable_create_fluid_system", {
|
|
|
2112
2182
|
],
|
|
2113
2183
|
};
|
|
2114
2184
|
});
|
|
2115
|
-
|
|
2185
|
+
registerPluginTool("diviops_variable_delete", {
|
|
2116
2186
|
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.",
|
|
2117
2187
|
inputSchema: {
|
|
2118
2188
|
id: z
|
|
@@ -2135,7 +2205,7 @@ server.registerTool("diviops_variable_delete", {
|
|
|
2135
2205
|
],
|
|
2136
2206
|
};
|
|
2137
2207
|
});
|
|
2138
|
-
|
|
2208
|
+
registerPluginTool("diviops_variable_scan_orphans", {
|
|
2139
2209
|
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.",
|
|
2140
2210
|
}, async () => {
|
|
2141
2211
|
const result = await wp.request("/variable/scan-orphans");
|
|
@@ -2145,7 +2215,7 @@ server.registerTool("diviops_variable_scan_orphans", {
|
|
|
2145
2215
|
],
|
|
2146
2216
|
};
|
|
2147
2217
|
});
|
|
2148
|
-
|
|
2218
|
+
registerPluginTool("diviops_variable_used_on_page", {
|
|
2149
2219
|
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.",
|
|
2150
2220
|
inputSchema: {
|
|
2151
2221
|
post_id: z
|
|
@@ -2162,7 +2232,7 @@ server.registerTool("diviops_variable_used_on_page", {
|
|
|
2162
2232
|
],
|
|
2163
2233
|
};
|
|
2164
2234
|
});
|
|
2165
|
-
|
|
2235
|
+
registerPluginTool("diviops_meta_flush_cache", {
|
|
2166
2236
|
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.",
|
|
2167
2237
|
inputSchema: {
|
|
2168
2238
|
post_id: z
|
|
@@ -2203,24 +2273,38 @@ server.registerTool("diviops_meta_flush_cache", {
|
|
|
2203
2273
|
});
|
|
2204
2274
|
// ── Start ────────────────────────────────────────────────────────────
|
|
2205
2275
|
async function main() {
|
|
2206
|
-
//
|
|
2276
|
+
// Capability handshake — populate the per-tool gate map (#486).
|
|
2207
2277
|
try {
|
|
2208
2278
|
const hs = await wp.handshake(SERVER_VERSION);
|
|
2279
|
+
handshakeState = {
|
|
2280
|
+
kind: "ok",
|
|
2281
|
+
capabilities: hs.capabilities,
|
|
2282
|
+
pluginVersion: hs.plugin_version,
|
|
2283
|
+
};
|
|
2209
2284
|
const diviInfo = hs.divi.active
|
|
2210
2285
|
? `Divi ${hs.divi.version ?? "unknown"}`
|
|
2211
2286
|
: "Divi not active";
|
|
2212
|
-
|
|
2287
|
+
const capCount = Object.keys(hs.capabilities).filter((k) => hs.capabilities[k]).length;
|
|
2288
|
+
console.error(`Handshake OK: plugin ${hs.plugin_version}, ${diviInfo}, ${capCount} capabilities`);
|
|
2289
|
+
if (capCount === 0) {
|
|
2290
|
+
console.error("Warning: plugin returned an empty capability map. Plugin-touching tools will fail with an upgrade hint. Update diviops-agent to ≥1.2.0.");
|
|
2291
|
+
}
|
|
2213
2292
|
}
|
|
2214
2293
|
catch (error) {
|
|
2215
2294
|
const msg = error instanceof Error ? error.message : String(error);
|
|
2216
|
-
//
|
|
2217
|
-
if (msg.includes("WordPress API error (426)")
|
|
2218
|
-
|
|
2219
|
-
console.error(`Version mismatch: ${msg}`);
|
|
2295
|
+
// Plugin rejected this server as too old (HTTP 426) — fatal.
|
|
2296
|
+
if (msg.includes("WordPress API error (426)")) {
|
|
2297
|
+
console.error(`Server too old for plugin: ${msg}`);
|
|
2220
2298
|
process.exit(1);
|
|
2221
2299
|
}
|
|
2222
|
-
//
|
|
2223
|
-
|
|
2300
|
+
// Network / auth / other transient failure — mark the gate as
|
|
2301
|
+
// failed so plugin-touching tools fall through to their own
|
|
2302
|
+
// wp.request() calls and surface the real error (401, 5xx, etc.)
|
|
2303
|
+
// instead of being misreported as missing capabilities.
|
|
2304
|
+
// Codex review on PR #525: pre-#486 behavior surfaced the actual
|
|
2305
|
+
// cause; the gate must preserve that.
|
|
2306
|
+
handshakeState = { kind: "failed" };
|
|
2307
|
+
console.error(`Handshake warning (gate disabled): ${msg}`);
|
|
2224
2308
|
}
|
|
2225
2309
|
const transport = new StdioServerTransport();
|
|
2226
2310
|
await server.connect(transport);
|
package/dist/wp-client.d.ts
CHANGED
|
@@ -30,12 +30,17 @@ export declare class WPClient {
|
|
|
30
30
|
message: string;
|
|
31
31
|
}>;
|
|
32
32
|
/**
|
|
33
|
-
* Perform
|
|
33
|
+
* Perform a capability handshake with the WP plugin.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* As of #486 there is no global plugin-version floor — compatibility is
|
|
36
|
+
* decided per-tool against `result.capabilities`. This method only:
|
|
37
|
+
* - issues the request (network/auth errors propagate)
|
|
38
|
+
* - normalizes the legacy pre-1.2.0 shape (`capabilities: string[]`)
|
|
39
|
+
* into the post-1.2.0 shape (`capabilities: Record<string,boolean>`)
|
|
40
|
+
* so the rest of the server can assume a uniform map.
|
|
41
|
+
*
|
|
42
|
+
* The plugin still rejects servers below its own MIN_SERVER_VERSION
|
|
43
|
+
* with HTTP 426 — that error surfaces here as a regular request error.
|
|
39
44
|
*/
|
|
40
45
|
handshake(serverVersion: string): Promise<HandshakeResult>;
|
|
41
46
|
}
|
package/dist/wp-client.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Uses WP Application Passwords (built into WP 5.6+) for auth.
|
|
5
5
|
* Generate one at: WP Admin → Users → Your Profile → Application Passwords.
|
|
6
6
|
*/
|
|
7
|
-
import { MIN_PLUGIN_VERSION, compareVersions, } from './compatibility.js';
|
|
8
7
|
/**
|
|
9
8
|
* Normalize quote-escape pathologies inside `$variable(...)$` token regions only.
|
|
10
9
|
*
|
|
@@ -161,22 +160,33 @@ export class WPClient {
|
|
|
161
160
|
}
|
|
162
161
|
}
|
|
163
162
|
/**
|
|
164
|
-
* Perform
|
|
163
|
+
* Perform a capability handshake with the WP plugin.
|
|
165
164
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
165
|
+
* As of #486 there is no global plugin-version floor — compatibility is
|
|
166
|
+
* decided per-tool against `result.capabilities`. This method only:
|
|
167
|
+
* - issues the request (network/auth errors propagate)
|
|
168
|
+
* - normalizes the legacy pre-1.2.0 shape (`capabilities: string[]`)
|
|
169
|
+
* into the post-1.2.0 shape (`capabilities: Record<string,boolean>`)
|
|
170
|
+
* so the rest of the server can assume a uniform map.
|
|
171
|
+
*
|
|
172
|
+
* The plugin still rejects servers below its own MIN_SERVER_VERSION
|
|
173
|
+
* with HTTP 426 — that error surfaces here as a regular request error.
|
|
170
174
|
*/
|
|
171
175
|
async handshake(serverVersion) {
|
|
172
176
|
const result = await this.request('/handshake', {
|
|
173
177
|
method: 'POST',
|
|
174
178
|
body: { mcp_server_version: serverVersion },
|
|
175
179
|
});
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
+
// Pre-1.2.0 plugins emit `capabilities` as a string[] of coarse
|
|
181
|
+
// namespace tags. Coerce to an empty map so per-tool gates fail fast
|
|
182
|
+
// with the upgrade hint instead of silently passing because of a
|
|
183
|
+
// shape mismatch.
|
|
184
|
+
if (Array.isArray(result.capabilities)) {
|
|
185
|
+
result.capabilities = {};
|
|
186
|
+
}
|
|
187
|
+
else if (result.capabilities === null ||
|
|
188
|
+
typeof result.capabilities !== 'object') {
|
|
189
|
+
result.capabilities = {};
|
|
180
190
|
}
|
|
181
191
|
return result;
|
|
182
192
|
}
|