@diviops/mcp-server 0.1.0 → 0.2.1
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 +20 -6
- package/dist/index.js +96 -2
- package/dist/wp-cli.js +30 -5
- package/dist/wp-client.js +4 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -91,9 +91,9 @@ The server connects via standard WordPress REST API and works with any environme
|
|
|
91
91
|
|
|
92
92
|
> **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.
|
|
93
93
|
|
|
94
|
-
## Available Tools (
|
|
94
|
+
## Available Tools (46)
|
|
95
95
|
|
|
96
|
-
### Read (
|
|
96
|
+
### Read (25)
|
|
97
97
|
| Tool | Description |
|
|
98
98
|
|------|-------------|
|
|
99
99
|
| `diviops_test_connection` | Test WordPress connection and Divi version |
|
|
@@ -111,6 +111,7 @@ The server connects via standard WordPress REST API and works with any environme
|
|
|
111
111
|
| `diviops_list_templates` | List available MCP prompt templates |
|
|
112
112
|
| `diviops_get_template` | Get a specific template's block markup |
|
|
113
113
|
| `diviops_preset_audit` | Audit presets with referenced/unreferenced analysis |
|
|
114
|
+
| `diviops_preset_scan_orphans` | List page-referenced preset UUIDs missing from the D5 registry (separates dangling orphans from D4-legacy refs) |
|
|
114
115
|
| `diviops_list_library` | List saved Divi Library items |
|
|
115
116
|
| `diviops_get_library_item` | Get a library item's block markup |
|
|
116
117
|
| `diviops_render_preview` | Render block markup to HTML for preview |
|
|
@@ -121,7 +122,7 @@ The server connects via standard WordPress REST API and works with any environme
|
|
|
121
122
|
| `diviops_list_canvases` | List all canvas pages |
|
|
122
123
|
| `diviops_get_canvas` | Get canvas content |
|
|
123
124
|
|
|
124
|
-
### Write (
|
|
125
|
+
### Write (20)
|
|
125
126
|
| Tool | Description |
|
|
126
127
|
|------|-------------|
|
|
127
128
|
| `diviops_create_page` | Create a new page with optional Divi content |
|
|
@@ -132,6 +133,8 @@ The server connects via standard WordPress REST API and works with any environme
|
|
|
132
133
|
| `diviops_update_module` | Update specific module attributes by label or text match |
|
|
133
134
|
| `diviops_move_module` | Move a block before/after another block (reorder modules, sections) |
|
|
134
135
|
| `diviops_preset_cleanup` | Remove spam/duplicate presets, bulk rename |
|
|
136
|
+
| `diviops_preset_create` | Write a new preset to the D5 registry (module or group type, supports `divi/column` etc.) |
|
|
137
|
+
| `diviops_preset_reassign` | Rewrite `modulePreset` references across pages (dry-run by default; optional `strip_inline` removes redundant inline attrs) |
|
|
135
138
|
| `diviops_preset_update` | Update a specific preset (name, attrs) |
|
|
136
139
|
| `diviops_preset_delete` | Delete a preset by ID |
|
|
137
140
|
| `diviops_save_to_library` | Save block markup to Divi Library |
|
|
@@ -154,16 +157,20 @@ The `diviops_wp_cli` tool validates every command against a safety allowlist bef
|
|
|
154
157
|
|
|
155
158
|
### Default allowlist (always available)
|
|
156
159
|
|
|
157
|
-
Read-only commands plus non-destructive writes needed for core MCP functionality:
|
|
160
|
+
Read-only commands plus non-destructive writes needed for core MCP functionality and local development workflows:
|
|
158
161
|
|
|
159
162
|
| Category | Commands |
|
|
160
163
|
|----------|----------|
|
|
161
164
|
| Options | `option get`, `option list` |
|
|
162
165
|
| Posts | `post list`, `post get`, `post create`, `post update` |
|
|
163
166
|
| Post meta | `post meta get`, `post meta list`, `post meta set`, `post meta update` |
|
|
167
|
+
| Post types | `post-type list`, `post-type get` |
|
|
168
|
+
| Taxonomies | `taxonomy list`, `term list`, `term create`, `term update` |
|
|
169
|
+
| ACF / SCF | `acf export`, `acf import`, `acf field-group list`, `acf field-group get` |
|
|
164
170
|
| Users | `user list` |
|
|
165
171
|
| Cache | `cache flush`, `transient delete`, `rewrite flush` |
|
|
166
|
-
|
|
|
172
|
+
| Export | `export` (WXR data export to file) |
|
|
173
|
+
| Info | `cron event list`, `plugin list`, `theme list`, `menu list`, `site url` |
|
|
167
174
|
|
|
168
175
|
### Extended commands (opt-in)
|
|
169
176
|
|
|
@@ -174,6 +181,9 @@ These commands carry higher risk and require explicit opt-in via the `DIVIOPS_WP
|
|
|
174
181
|
| `option update` | High | Can change site URL, admin email, or security settings |
|
|
175
182
|
| `post delete` | Medium | Permanently removes content |
|
|
176
183
|
| `post meta delete` | Medium | Removes metadata |
|
|
184
|
+
| `term delete` | Medium | Permanently removes taxonomy terms |
|
|
185
|
+
| `search-replace` | High | Bulk database modification — can corrupt content if misused |
|
|
186
|
+
| `import` | Medium | Bulk content ingestion from WXR files |
|
|
177
187
|
| `plugin activate` | Medium | Can enable untrusted plugins |
|
|
178
188
|
| `plugin deactivate` | Medium | Can disable security plugins |
|
|
179
189
|
| `eval-file` | Critical | Executes arbitrary PHP from a file path |
|
|
@@ -186,12 +196,16 @@ claude mcp add diviops-mcp -- env \
|
|
|
186
196
|
WP_USER=admin \
|
|
187
197
|
WP_APP_PASSWORD=xxxx \
|
|
188
198
|
WP_PATH="/path/to/wordpress" \
|
|
189
|
-
DIVIOPS_WP_CLI_ALLOW="option update,post delete" \
|
|
199
|
+
DIVIOPS_WP_CLI_ALLOW="option update,post delete,search-replace" \
|
|
190
200
|
npx @diviops/mcp-server
|
|
191
201
|
```
|
|
192
202
|
|
|
193
203
|
Only list the specific commands you need. Unknown entries are ignored with a warning.
|
|
194
204
|
|
|
205
|
+
> **Note on `acf import`**: included in the default allowlist because it's an idempotent dev-time schema operation (re-creates field groups from JSON). Bulk content imports use `wp import` instead, which is opt-in.
|
|
206
|
+
|
|
207
|
+
> **Known limitation — filesystem access**: Validation is prefix-based, not flag-aware. Commands that read from or write to the filesystem (`acf export`/`acf import`, `export`, opt-in `import`/`eval-file`) can target any path reachable by the WP-CLI user. For shared or multi-tenant environments, consider wrapping the MCP server with a stricter proxy or running it under an account with limited filesystem permissions. Flag-level validation is a candidate future enhancement.
|
|
208
|
+
|
|
195
209
|
## Example Usage
|
|
196
210
|
|
|
197
211
|
After setup, Claude can:
|
package/dist/index.js
CHANGED
|
@@ -661,6 +661,100 @@ server.registerTool("diviops_preset_delete", {
|
|
|
661
661
|
],
|
|
662
662
|
};
|
|
663
663
|
});
|
|
664
|
+
server.registerTool("diviops_preset_create", {
|
|
665
|
+
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.',
|
|
666
|
+
inputSchema: {
|
|
667
|
+
module_name: z
|
|
668
|
+
.string()
|
|
669
|
+
.describe('Divi module slug (e.g. "divi/column", "divi/button", "divi/section"). For group presets, this is still required and describes the module the preset originated from.'),
|
|
670
|
+
name: z.string().describe("Display name for the new preset"),
|
|
671
|
+
attrs: z
|
|
672
|
+
.record(z.string(), z.any())
|
|
673
|
+
.describe("Full module attribute bag (same shape as a module's top-level attrs in block markup). Saved to both attrs and styleAttrs."),
|
|
674
|
+
type: z
|
|
675
|
+
.enum(["module", "group"])
|
|
676
|
+
.optional()
|
|
677
|
+
.default("module")
|
|
678
|
+
.describe('"module" (default) or "group" for attribute-level presets.'),
|
|
679
|
+
group_name: z
|
|
680
|
+
.string()
|
|
681
|
+
.optional()
|
|
682
|
+
.describe('Group name (e.g. "divi/font", "divi/button"). Required when type="group".'),
|
|
683
|
+
group_id: z
|
|
684
|
+
.string()
|
|
685
|
+
.optional()
|
|
686
|
+
.describe('Group id (e.g. "designTitleText", "designText", "button"). Required when type="group".'),
|
|
687
|
+
primary_attr_name: z
|
|
688
|
+
.string()
|
|
689
|
+
.optional()
|
|
690
|
+
.describe('Primary attr name for the group (e.g. "title" for designTitleText). Optional.'),
|
|
691
|
+
},
|
|
692
|
+
}, async ({ module_name, name, attrs, type, group_name, group_id, primary_attr_name }) => {
|
|
693
|
+
if (type === "group" && (!group_name || !group_id)) {
|
|
694
|
+
throw new Error('type="group" requires both group_name and group_id. Example: group_name="divi/font", group_id="designTitleText".');
|
|
695
|
+
}
|
|
696
|
+
const body = { module_name, name, attrs, type };
|
|
697
|
+
if (group_name)
|
|
698
|
+
body.group_name = group_name;
|
|
699
|
+
if (group_id)
|
|
700
|
+
body.group_id = group_id;
|
|
701
|
+
if (primary_attr_name)
|
|
702
|
+
body.primary_attr_name = primary_attr_name;
|
|
703
|
+
const result = await wp.request("/preset-create", { method: "POST", body });
|
|
704
|
+
return {
|
|
705
|
+
content: [
|
|
706
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
707
|
+
],
|
|
708
|
+
};
|
|
709
|
+
});
|
|
710
|
+
server.registerTool("diviops_preset_reassign", {
|
|
711
|
+
description: 'Reassign a preset UUID across page content. Walks pages and rewrites modules referencing old_uuid in their modulePreset array to reference new_uuid. Optionally strips inline attrs that duplicate the new preset\'s attrs (otherwise inline wins over preset). Defaults to dry-run — set mode="apply" to actually rewrite. Use this to consolidate repeated inline styling into a reusable preset after creating one with diviops_preset_create.',
|
|
712
|
+
inputSchema: {
|
|
713
|
+
old_uuid: z
|
|
714
|
+
.string()
|
|
715
|
+
.describe("Preset UUID to replace (can be a dangling/orphan UUID)"),
|
|
716
|
+
new_uuid: z
|
|
717
|
+
.string()
|
|
718
|
+
.describe("New preset UUID to insert. Must already exist in the registry."),
|
|
719
|
+
page_ids: z
|
|
720
|
+
.array(z.number().int().positive())
|
|
721
|
+
.optional()
|
|
722
|
+
.describe("Restrict to specific post IDs. Omit to scan all pages and posts."),
|
|
723
|
+
mode: z
|
|
724
|
+
.enum(["dry-run", "apply"])
|
|
725
|
+
.optional()
|
|
726
|
+
.default("dry-run")
|
|
727
|
+
.describe('"dry-run" (default) returns the diff without writing. "apply" rewrites page content.'),
|
|
728
|
+
strip_inline: z
|
|
729
|
+
.boolean()
|
|
730
|
+
.optional()
|
|
731
|
+
.default(true)
|
|
732
|
+
.describe("If true (default), strip inline attrs that deep-equal the new preset's attrs so the preset actually takes effect. Set false to swap UUIDs only."),
|
|
733
|
+
},
|
|
734
|
+
}, async ({ old_uuid, new_uuid, page_ids, mode, strip_inline }) => {
|
|
735
|
+
const body = { old_uuid, new_uuid, mode, strip_inline };
|
|
736
|
+
if (page_ids)
|
|
737
|
+
body.page_ids = page_ids;
|
|
738
|
+
const result = await wp.request("/preset-reassign", {
|
|
739
|
+
method: "POST",
|
|
740
|
+
body,
|
|
741
|
+
});
|
|
742
|
+
return {
|
|
743
|
+
content: [
|
|
744
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
745
|
+
],
|
|
746
|
+
};
|
|
747
|
+
});
|
|
748
|
+
server.registerTool("diviops_preset_scan_orphans", {
|
|
749
|
+
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.",
|
|
750
|
+
}, async () => {
|
|
751
|
+
const result = await wp.request("/preset-scan-orphans");
|
|
752
|
+
return {
|
|
753
|
+
content: [
|
|
754
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
755
|
+
],
|
|
756
|
+
};
|
|
757
|
+
});
|
|
664
758
|
// ── Library Tools ───────────────────────────────────────────────────
|
|
665
759
|
server.registerTool("diviops_list_library", {
|
|
666
760
|
description: "List saved Divi Library items. Filter by layout_type (section, row, module) and scope (global, non_global).",
|
|
@@ -968,7 +1062,7 @@ server.registerTool("diviops_delete_canvas", {
|
|
|
968
1062
|
});
|
|
969
1063
|
// ── WP-CLI ──────────────────────────────────────────────────────────
|
|
970
1064
|
server.registerTool("diviops_wp_cli", {
|
|
971
|
-
description: "Run a WP-CLI command on the WordPress site. Requires WP_PATH env var (LOCAL_SITE_ID auto-detected from Local by Flywheel). Commands validated against a safety allowlist. Default tier
|
|
1065
|
+
description: "Run a WP-CLI command on the WordPress site. Requires WP_PATH env var (LOCAL_SITE_ID auto-detected from Local by Flywheel). Commands validated against a safety allowlist. Default tier covers read ops across options/posts/post-types/taxonomies/users/info, non-destructive writes (post/term create+update, post meta read/write, cache/rewrite/transient flush), ACF schema ops (export/import/list/get field-group), and WXR export. Extended tier (requires DIVIOPS_WP_CLI_ALLOW env var) adds destructive or bulk-modifying ops: option update, post/post meta/term delete, search-replace, import, plugin activate/deactivate, eval-file. Use --format=json for structured output. Full allowlist + tier rationale in the MCP server README.",
|
|
972
1066
|
inputSchema: {
|
|
973
1067
|
command: z
|
|
974
1068
|
.string()
|
|
@@ -1032,7 +1126,7 @@ server.registerTool("diviops_server_info", {
|
|
|
1032
1126
|
};
|
|
1033
1127
|
});
|
|
1034
1128
|
// ── Resources ────────────────────────────────────────────────────────
|
|
1035
|
-
server.
|
|
1129
|
+
server.registerResource("divi-block-format-guide", "divi://block-format-guide", {}, async () => ({
|
|
1036
1130
|
contents: [
|
|
1037
1131
|
{
|
|
1038
1132
|
uri: "divi://block-format-guide",
|
package/dist/wp-cli.js
CHANGED
|
@@ -26,32 +26,57 @@ const DEFAULT_COMMANDS = [
|
|
|
26
26
|
'post meta list',
|
|
27
27
|
'post meta set',
|
|
28
28
|
'post meta update',
|
|
29
|
+
// Post types (read-only)
|
|
30
|
+
'post-type list',
|
|
31
|
+
'post-type get',
|
|
32
|
+
// Taxonomies (read + non-destructive write)
|
|
33
|
+
'taxonomy list',
|
|
34
|
+
'term list',
|
|
35
|
+
'term create',
|
|
36
|
+
'term update',
|
|
37
|
+
// ACF / SCF (schema ops — idempotent dev-time workflow)
|
|
38
|
+
'acf export',
|
|
39
|
+
'acf import',
|
|
40
|
+
'acf field-group list',
|
|
41
|
+
'acf field-group get',
|
|
29
42
|
// Users (read-only)
|
|
30
43
|
'user list',
|
|
31
44
|
// Cache (non-destructive maintenance)
|
|
32
45
|
'cache flush',
|
|
33
46
|
'transient delete',
|
|
34
47
|
'rewrite flush',
|
|
48
|
+
// Export (reads data, writes to file only)
|
|
49
|
+
'export',
|
|
35
50
|
// Info (read-only)
|
|
36
51
|
'cron event list',
|
|
37
52
|
'plugin list',
|
|
38
53
|
'theme list',
|
|
39
54
|
'menu list',
|
|
40
|
-
'term list',
|
|
41
|
-
'term create',
|
|
42
55
|
'site url',
|
|
43
56
|
];
|
|
44
57
|
/**
|
|
45
58
|
* Extended commands that require explicit opt-in via DIVIOPS_WP_CLI_ALLOW env var.
|
|
46
|
-
* These carry higher risk: destructive operations,
|
|
47
|
-
* or the ability to disable security features.
|
|
59
|
+
* These carry higher risk: destructive operations, bulk database modification,
|
|
60
|
+
* arbitrary code execution, or the ability to disable security features.
|
|
61
|
+
*
|
|
62
|
+
* To enable, set a comma-separated subset, e.g.:
|
|
63
|
+
* DIVIOPS_WP_CLI_ALLOW="option update,post delete,search-replace"
|
|
64
|
+
*
|
|
65
|
+
* Only list the specific commands you need — unknown entries are ignored.
|
|
48
66
|
*
|
|
49
|
-
*
|
|
67
|
+
* Known limitation: validation is prefix-based, not flag-aware. Commands that
|
|
68
|
+
* touch the filesystem (acf export/import, export, eval-file) can write to
|
|
69
|
+
* or read from any path reachable by the WP-CLI user. For shared environments
|
|
70
|
+
* consider wrapping the MCP server with a stricter proxy or running it under
|
|
71
|
+
* an account with limited filesystem permissions.
|
|
50
72
|
*/
|
|
51
73
|
const EXTENDED_COMMANDS = [
|
|
52
74
|
'option update', // Can change site URL, admin email, active plugins
|
|
53
75
|
'post delete', // Destructive — permanently removes content
|
|
54
76
|
'post meta delete', // Destructive — removes metadata
|
|
77
|
+
'term delete', // Destructive — removes taxonomy terms
|
|
78
|
+
'search-replace', // Bulk DB modification — highest-risk content op
|
|
79
|
+
'import', // Bulk content ingestion from WXR files
|
|
55
80
|
'plugin activate', // Can enable untrusted plugins
|
|
56
81
|
'plugin deactivate', // Can disable security plugins
|
|
57
82
|
'eval-file', // Executes arbitrary PHP from a file path
|
package/dist/wp-client.js
CHANGED
|
@@ -47,6 +47,10 @@ export class WPClient {
|
|
|
47
47
|
catch {
|
|
48
48
|
errorMessage = errorBody;
|
|
49
49
|
}
|
|
50
|
+
if (response.status === 429) {
|
|
51
|
+
const retryAfter = response.headers.get('Retry-After') || '60';
|
|
52
|
+
throw new Error(`Rate limited: ${errorMessage} (retry after ${retryAfter}s)`);
|
|
53
|
+
}
|
|
50
54
|
throw new Error(`WordPress API error (${response.status}): ${errorMessage}`);
|
|
51
55
|
}
|
|
52
56
|
return response.json();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diviops/mcp-server",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "MCP server exposing Divi 5 Visual Builder as tools for Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"node": ">=18.0.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
44
44
|
"zod": "^4.3.6"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|