@diviops/mcp-server 1.0.0 → 1.2.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 -4
- package/dist/compatibility.d.ts +37 -4
- package/dist/compatibility.js +33 -3
- package/dist/index.js +179 -60
- 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 (65)
|
|
100
100
|
|
|
101
101
|
### Read (30)
|
|
102
102
|
| Tool | Description |
|
|
@@ -132,11 +132,13 @@ 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 (33)
|
|
136
136
|
| Tool | Description |
|
|
137
137
|
|------|-------------|
|
|
138
138
|
| `diviops_page_create` | Create a new page with optional Divi content |
|
|
139
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` |
|
|
140
142
|
| `diviops_section_append` | Append a section to existing page (start or end) |
|
|
141
143
|
| `diviops_section_replace` | Replace a section by admin label |
|
|
142
144
|
| `diviops_section_remove` | Remove a section by admin label |
|
|
@@ -318,8 +320,11 @@ Ensure `WP_URL`, `WP_USER`, and `WP_APP_PASSWORD` are all set. Check your `claud
|
|
|
318
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
|
-
### "
|
|
322
|
-
|
|
323
|
+
### "This tool requires plugin capability" error
|
|
324
|
+
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.
|
|
325
|
+
|
|
326
|
+
### "Server too old for plugin" error
|
|
327
|
+
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).
|
|
323
328
|
|
|
324
329
|
### "Permission denied" errors
|
|
325
330
|
- 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.0.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.0.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.0.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,8 +786,71 @@ server.registerTool("diviops_page_create", {
|
|
|
744
786
|
],
|
|
745
787
|
};
|
|
746
788
|
});
|
|
789
|
+
registerPluginTool("diviops_page_trash", {
|
|
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).",
|
|
791
|
+
inputSchema: {
|
|
792
|
+
post_id: z.number().int().describe("WordPress post/page ID"),
|
|
793
|
+
force: z
|
|
794
|
+
.boolean()
|
|
795
|
+
.optional()
|
|
796
|
+
.default(false)
|
|
797
|
+
.describe("When true, permanently delete (skips trash). Default false moves to trash."),
|
|
798
|
+
dry_run: z
|
|
799
|
+
.boolean()
|
|
800
|
+
.optional()
|
|
801
|
+
.default(false)
|
|
802
|
+
.describe("When true, return the change plan without mutating state."),
|
|
803
|
+
},
|
|
804
|
+
}, async ({ post_id, force, dry_run }) => {
|
|
805
|
+
const result = await wp.request(`/page/trash/${post_id}`, {
|
|
806
|
+
method: "POST",
|
|
807
|
+
body: {
|
|
808
|
+
force: force ?? false,
|
|
809
|
+
dry_run: dry_run ?? false,
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
return {
|
|
813
|
+
content: [
|
|
814
|
+
{ type: "text", text: JSON.stringify(result) },
|
|
815
|
+
],
|
|
816
|
+
};
|
|
817
|
+
});
|
|
818
|
+
registerPluginTool("diviops_page_update_status", {
|
|
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.",
|
|
820
|
+
inputSchema: {
|
|
821
|
+
post_id: z.number().int().describe("WordPress post/page ID"),
|
|
822
|
+
status: z
|
|
823
|
+
.enum(["publish", "draft", "private", "pending", "future"])
|
|
824
|
+
.describe("Target post status"),
|
|
825
|
+
date_gmt: z
|
|
826
|
+
.string()
|
|
827
|
+
.optional()
|
|
828
|
+
.describe("Required when status='future'. ISO 8601 UTC datetime (e.g. '2026-06-01T09:00:00Z'). Must be in the future."),
|
|
829
|
+
dry_run: z
|
|
830
|
+
.boolean()
|
|
831
|
+
.optional()
|
|
832
|
+
.default(false)
|
|
833
|
+
.describe("When true, return the change plan without mutating state."),
|
|
834
|
+
},
|
|
835
|
+
}, async ({ post_id, status, date_gmt, dry_run }) => {
|
|
836
|
+
const body = {
|
|
837
|
+
status,
|
|
838
|
+
dry_run: dry_run ?? false,
|
|
839
|
+
};
|
|
840
|
+
if (date_gmt)
|
|
841
|
+
body.date_gmt = date_gmt;
|
|
842
|
+
const result = await wp.request(`/page/update-status/${post_id}`, {
|
|
843
|
+
method: "POST",
|
|
844
|
+
body,
|
|
845
|
+
});
|
|
846
|
+
return {
|
|
847
|
+
content: [
|
|
848
|
+
{ type: "text", text: JSON.stringify(result) },
|
|
849
|
+
],
|
|
850
|
+
};
|
|
851
|
+
});
|
|
747
852
|
// ── Preset Tools ────────────────────────────────────────────────────
|
|
748
|
-
|
|
853
|
+
registerPluginTool("diviops_preset_audit", {
|
|
749
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.",
|
|
750
855
|
}, async () => {
|
|
751
856
|
const result = await wp.request("/preset/audit");
|
|
@@ -755,7 +860,7 @@ server.registerTool("diviops_preset_audit", {
|
|
|
755
860
|
],
|
|
756
861
|
};
|
|
757
862
|
});
|
|
758
|
-
|
|
863
|
+
registerPluginTool("diviops_preset_cleanup", {
|
|
759
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.',
|
|
760
865
|
inputSchema: {
|
|
761
866
|
dry_run: z
|
|
@@ -801,7 +906,7 @@ server.registerTool("diviops_preset_cleanup", {
|
|
|
801
906
|
],
|
|
802
907
|
};
|
|
803
908
|
});
|
|
804
|
-
|
|
909
|
+
registerPluginTool("diviops_preset_update", {
|
|
805
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.",
|
|
806
911
|
inputSchema: {
|
|
807
912
|
preset_id: z.string().describe("Preset ID (UUID or short ID)"),
|
|
@@ -834,7 +939,7 @@ server.registerTool("diviops_preset_update", {
|
|
|
834
939
|
],
|
|
835
940
|
};
|
|
836
941
|
});
|
|
837
|
-
|
|
942
|
+
registerPluginTool("diviops_preset_delete", {
|
|
838
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.",
|
|
839
944
|
inputSchema: {
|
|
840
945
|
preset_id: z.string().describe("Preset ID to delete"),
|
|
@@ -857,7 +962,7 @@ server.registerTool("diviops_preset_delete", {
|
|
|
857
962
|
],
|
|
858
963
|
};
|
|
859
964
|
});
|
|
860
|
-
|
|
965
|
+
registerPluginTool("diviops_preset_create", {
|
|
861
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.',
|
|
862
967
|
inputSchema: {
|
|
863
968
|
module_name: z
|
|
@@ -916,7 +1021,7 @@ server.registerTool("diviops_preset_create", {
|
|
|
916
1021
|
],
|
|
917
1022
|
};
|
|
918
1023
|
});
|
|
919
|
-
|
|
1024
|
+
registerPluginTool("diviops_preset_reassign", {
|
|
920
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.',
|
|
921
1026
|
inputSchema: {
|
|
922
1027
|
old_uuid: z
|
|
@@ -965,7 +1070,7 @@ server.registerTool("diviops_preset_reassign", {
|
|
|
965
1070
|
],
|
|
966
1071
|
};
|
|
967
1072
|
});
|
|
968
|
-
|
|
1073
|
+
registerPluginTool("diviops_preset_scan_orphans", {
|
|
969
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.",
|
|
970
1075
|
}, async () => {
|
|
971
1076
|
const result = await wp.request("/preset/scan-orphans");
|
|
@@ -975,7 +1080,7 @@ server.registerTool("diviops_preset_scan_orphans", {
|
|
|
975
1080
|
],
|
|
976
1081
|
};
|
|
977
1082
|
});
|
|
978
|
-
|
|
1083
|
+
registerPluginTool("diviops_preset_set_default", {
|
|
979
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.",
|
|
980
1085
|
inputSchema: {
|
|
981
1086
|
preset_id: z
|
|
@@ -1016,7 +1121,7 @@ server.registerTool("diviops_preset_set_default", {
|
|
|
1016
1121
|
};
|
|
1017
1122
|
});
|
|
1018
1123
|
// ── Library Tools ───────────────────────────────────────────────────
|
|
1019
|
-
|
|
1124
|
+
registerPluginTool("diviops_library_list", {
|
|
1020
1125
|
description: "List saved Divi Library items. Filter by layout_type (section, row, module) and scope (global, non_global).",
|
|
1021
1126
|
inputSchema: {
|
|
1022
1127
|
layout_type: z
|
|
@@ -1048,7 +1153,7 @@ server.registerTool("diviops_library_list", {
|
|
|
1048
1153
|
],
|
|
1049
1154
|
};
|
|
1050
1155
|
});
|
|
1051
|
-
|
|
1156
|
+
registerPluginTool("diviops_library_get", {
|
|
1052
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.",
|
|
1053
1158
|
inputSchema: {
|
|
1054
1159
|
item_id: z.number().describe("Library item ID"),
|
|
@@ -1061,7 +1166,7 @@ server.registerTool("diviops_library_get", {
|
|
|
1061
1166
|
],
|
|
1062
1167
|
};
|
|
1063
1168
|
});
|
|
1064
|
-
|
|
1169
|
+
registerPluginTool("diviops_library_save", {
|
|
1065
1170
|
description: 'Save Divi block markup to the Divi Library for reuse. Saved items appear in the VB\'s "Add From Library" panel.',
|
|
1066
1171
|
inputSchema: {
|
|
1067
1172
|
title: z.string().describe("Display name for the library item"),
|
|
@@ -1096,7 +1201,7 @@ server.registerTool("diviops_library_save", {
|
|
|
1096
1201
|
};
|
|
1097
1202
|
});
|
|
1098
1203
|
// ── Theme Builder Tools ─────────────────────────────────────────────
|
|
1099
|
-
|
|
1204
|
+
registerPluginTool("diviops_tb_template_list", {
|
|
1100
1205
|
description: "List all Theme Builder templates with their conditions, layout IDs, and enabled status. Shows which template applies to which pages/post types.",
|
|
1101
1206
|
inputSchema: {
|
|
1102
1207
|
per_page: z
|
|
@@ -1120,7 +1225,7 @@ server.registerTool("diviops_tb_template_list", {
|
|
|
1120
1225
|
],
|
|
1121
1226
|
};
|
|
1122
1227
|
});
|
|
1123
|
-
|
|
1228
|
+
registerPluginTool("diviops_tb_layout_get", {
|
|
1124
1229
|
description: "Get a Theme Builder layout's block markup content (header, body, or footer). Use the layout IDs from diviops_tb_template_list.",
|
|
1125
1230
|
inputSchema: {
|
|
1126
1231
|
layout_id: z
|
|
@@ -1135,7 +1240,7 @@ server.registerTool("diviops_tb_layout_get", {
|
|
|
1135
1240
|
],
|
|
1136
1241
|
};
|
|
1137
1242
|
});
|
|
1138
|
-
|
|
1243
|
+
registerPluginTool("diviops_tb_layout_update", {
|
|
1139
1244
|
description: "Update a Theme Builder layout's block markup (header, body, or footer). Replaces the full content.",
|
|
1140
1245
|
inputSchema: {
|
|
1141
1246
|
layout_id: z.number().describe("Layout post ID to update"),
|
|
@@ -1152,7 +1257,7 @@ server.registerTool("diviops_tb_layout_update", {
|
|
|
1152
1257
|
],
|
|
1153
1258
|
};
|
|
1154
1259
|
});
|
|
1155
|
-
|
|
1260
|
+
registerPluginTool("diviops_tb_template_create", {
|
|
1156
1261
|
description: "Create a Theme Builder template with custom header and/or footer. Automatically creates layout posts, sets conditions, and links to Theme Builder.",
|
|
1157
1262
|
inputSchema: {
|
|
1158
1263
|
title: z.string().describe('Template name (e.g. "Landing Pages")'),
|
|
@@ -1182,7 +1287,7 @@ server.registerTool("diviops_tb_template_create", {
|
|
|
1182
1287
|
};
|
|
1183
1288
|
});
|
|
1184
1289
|
// ── Canvas Tools ────────────────────────────────────────────────────
|
|
1185
|
-
|
|
1290
|
+
registerPluginTool("diviops_canvas_create", {
|
|
1186
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.",
|
|
1187
1292
|
inputSchema: {
|
|
1188
1293
|
title: z
|
|
@@ -1226,7 +1331,7 @@ server.registerTool("diviops_canvas_create", {
|
|
|
1226
1331
|
],
|
|
1227
1332
|
};
|
|
1228
1333
|
});
|
|
1229
|
-
|
|
1334
|
+
registerPluginTool("diviops_canvas_list", {
|
|
1230
1335
|
description: "List canvases (off-canvas workspaces). Filter by parent page or list all.",
|
|
1231
1336
|
inputSchema: {
|
|
1232
1337
|
parent_page_id: z
|
|
@@ -1255,7 +1360,7 @@ server.registerTool("diviops_canvas_list", {
|
|
|
1255
1360
|
],
|
|
1256
1361
|
};
|
|
1257
1362
|
});
|
|
1258
|
-
|
|
1363
|
+
registerPluginTool("diviops_canvas_get", {
|
|
1259
1364
|
description: "Get a canvas's block content and metadata.",
|
|
1260
1365
|
inputSchema: {
|
|
1261
1366
|
canvas_post_id: z
|
|
@@ -1270,7 +1375,7 @@ server.registerTool("diviops_canvas_get", {
|
|
|
1270
1375
|
],
|
|
1271
1376
|
};
|
|
1272
1377
|
});
|
|
1273
|
-
|
|
1378
|
+
registerPluginTool("diviops_canvas_update", {
|
|
1274
1379
|
description: "Update a canvas's content and/or metadata. Content replaces the entire canvas.",
|
|
1275
1380
|
inputSchema: {
|
|
1276
1381
|
canvas_post_id: z.number().describe("Canvas post ID"),
|
|
@@ -1305,7 +1410,7 @@ server.registerTool("diviops_canvas_update", {
|
|
|
1305
1410
|
],
|
|
1306
1411
|
};
|
|
1307
1412
|
});
|
|
1308
|
-
|
|
1413
|
+
registerPluginTool("diviops_canvas_delete", {
|
|
1309
1414
|
description: "Delete a canvas. This permanently removes the canvas post.",
|
|
1310
1415
|
inputSchema: {
|
|
1311
1416
|
canvas_post_id: z.number().describe("Canvas post ID to delete"),
|
|
@@ -1788,7 +1893,7 @@ server.registerTool("diviops_template_get", {
|
|
|
1788
1893
|
};
|
|
1789
1894
|
});
|
|
1790
1895
|
// ── Variable Manager CRUD ─────────────────────────────────────────────
|
|
1791
|
-
|
|
1896
|
+
registerPluginTool("diviops_variable_list", {
|
|
1792
1897
|
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
1898
|
inputSchema: {
|
|
1794
1899
|
type: z
|
|
@@ -1813,7 +1918,7 @@ server.registerTool("diviops_variable_list", {
|
|
|
1813
1918
|
],
|
|
1814
1919
|
};
|
|
1815
1920
|
});
|
|
1816
|
-
|
|
1921
|
+
registerPluginTool("diviops_variable_create", {
|
|
1817
1922
|
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
1923
|
inputSchema: {
|
|
1819
1924
|
type: z
|
|
@@ -1881,7 +1986,7 @@ server.registerTool("diviops_variable_create", {
|
|
|
1881
1986
|
],
|
|
1882
1987
|
};
|
|
1883
1988
|
});
|
|
1884
|
-
|
|
1989
|
+
registerPluginTool("diviops_variable_create_fluid_system", {
|
|
1885
1990
|
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
1991
|
inputSchema: {
|
|
1887
1992
|
profile: z
|
|
@@ -2049,7 +2154,7 @@ server.registerTool("diviops_variable_create_fluid_system", {
|
|
|
2049
2154
|
],
|
|
2050
2155
|
};
|
|
2051
2156
|
});
|
|
2052
|
-
|
|
2157
|
+
registerPluginTool("diviops_variable_delete", {
|
|
2053
2158
|
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
2159
|
inputSchema: {
|
|
2055
2160
|
id: z
|
|
@@ -2072,7 +2177,7 @@ server.registerTool("diviops_variable_delete", {
|
|
|
2072
2177
|
],
|
|
2073
2178
|
};
|
|
2074
2179
|
});
|
|
2075
|
-
|
|
2180
|
+
registerPluginTool("diviops_variable_scan_orphans", {
|
|
2076
2181
|
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
2182
|
}, async () => {
|
|
2078
2183
|
const result = await wp.request("/variable/scan-orphans");
|
|
@@ -2082,7 +2187,7 @@ server.registerTool("diviops_variable_scan_orphans", {
|
|
|
2082
2187
|
],
|
|
2083
2188
|
};
|
|
2084
2189
|
});
|
|
2085
|
-
|
|
2190
|
+
registerPluginTool("diviops_variable_used_on_page", {
|
|
2086
2191
|
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
2192
|
inputSchema: {
|
|
2088
2193
|
post_id: z
|
|
@@ -2099,7 +2204,7 @@ server.registerTool("diviops_variable_used_on_page", {
|
|
|
2099
2204
|
],
|
|
2100
2205
|
};
|
|
2101
2206
|
});
|
|
2102
|
-
|
|
2207
|
+
registerPluginTool("diviops_meta_flush_cache", {
|
|
2103
2208
|
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
2209
|
inputSchema: {
|
|
2105
2210
|
post_id: z
|
|
@@ -2140,24 +2245,38 @@ server.registerTool("diviops_meta_flush_cache", {
|
|
|
2140
2245
|
});
|
|
2141
2246
|
// ── Start ────────────────────────────────────────────────────────────
|
|
2142
2247
|
async function main() {
|
|
2143
|
-
//
|
|
2248
|
+
// Capability handshake — populate the per-tool gate map (#486).
|
|
2144
2249
|
try {
|
|
2145
2250
|
const hs = await wp.handshake(SERVER_VERSION);
|
|
2251
|
+
handshakeState = {
|
|
2252
|
+
kind: "ok",
|
|
2253
|
+
capabilities: hs.capabilities,
|
|
2254
|
+
pluginVersion: hs.plugin_version,
|
|
2255
|
+
};
|
|
2146
2256
|
const diviInfo = hs.divi.active
|
|
2147
2257
|
? `Divi ${hs.divi.version ?? "unknown"}`
|
|
2148
2258
|
: "Divi not active";
|
|
2149
|
-
|
|
2259
|
+
const capCount = Object.keys(hs.capabilities).filter((k) => hs.capabilities[k]).length;
|
|
2260
|
+
console.error(`Handshake OK: plugin ${hs.plugin_version}, ${diviInfo}, ${capCount} capabilities`);
|
|
2261
|
+
if (capCount === 0) {
|
|
2262
|
+
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.");
|
|
2263
|
+
}
|
|
2150
2264
|
}
|
|
2151
2265
|
catch (error) {
|
|
2152
2266
|
const msg = error instanceof Error ? error.message : String(error);
|
|
2153
|
-
//
|
|
2154
|
-
if (msg.includes("WordPress API error (426)")
|
|
2155
|
-
|
|
2156
|
-
console.error(`Version mismatch: ${msg}`);
|
|
2267
|
+
// Plugin rejected this server as too old (HTTP 426) — fatal.
|
|
2268
|
+
if (msg.includes("WordPress API error (426)")) {
|
|
2269
|
+
console.error(`Server too old for plugin: ${msg}`);
|
|
2157
2270
|
process.exit(1);
|
|
2158
2271
|
}
|
|
2159
|
-
//
|
|
2160
|
-
|
|
2272
|
+
// Network / auth / other transient failure — mark the gate as
|
|
2273
|
+
// failed so plugin-touching tools fall through to their own
|
|
2274
|
+
// wp.request() calls and surface the real error (401, 5xx, etc.)
|
|
2275
|
+
// instead of being misreported as missing capabilities.
|
|
2276
|
+
// Codex review on PR #525: pre-#486 behavior surfaced the actual
|
|
2277
|
+
// cause; the gate must preserve that.
|
|
2278
|
+
handshakeState = { kind: "failed" };
|
|
2279
|
+
console.error(`Handshake warning (gate disabled): ${msg}`);
|
|
2161
2280
|
}
|
|
2162
2281
|
const transport = new StdioServerTransport();
|
|
2163
2282
|
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
|
}
|