@diviops/mcp-server 0.1.0 → 0.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 CHANGED
@@ -154,16 +154,20 @@ The `diviops_wp_cli` tool validates every command against a safety allowlist bef
154
154
 
155
155
  ### Default allowlist (always available)
156
156
 
157
- Read-only commands plus non-destructive writes needed for core MCP functionality:
157
+ Read-only commands plus non-destructive writes needed for core MCP functionality and local development workflows:
158
158
 
159
159
  | Category | Commands |
160
160
  |----------|----------|
161
161
  | Options | `option get`, `option list` |
162
162
  | Posts | `post list`, `post get`, `post create`, `post update` |
163
163
  | Post meta | `post meta get`, `post meta list`, `post meta set`, `post meta update` |
164
+ | Post types | `post-type list`, `post-type get` |
165
+ | Taxonomies | `taxonomy list`, `term list`, `term create`, `term update` |
166
+ | ACF / SCF | `acf export`, `acf import`, `acf field-group list`, `acf field-group get` |
164
167
  | Users | `user list` |
165
168
  | Cache | `cache flush`, `transient delete`, `rewrite flush` |
166
- | Info | `cron event list`, `plugin list`, `theme list`, `menu list`, `term list`, `term create`, `site url` |
169
+ | Export | `export` (WXR data export to file) |
170
+ | Info | `cron event list`, `plugin list`, `theme list`, `menu list`, `site url` |
167
171
 
168
172
  ### Extended commands (opt-in)
169
173
 
@@ -174,6 +178,9 @@ These commands carry higher risk and require explicit opt-in via the `DIVIOPS_WP
174
178
  | `option update` | High | Can change site URL, admin email, or security settings |
175
179
  | `post delete` | Medium | Permanently removes content |
176
180
  | `post meta delete` | Medium | Removes metadata |
181
+ | `term delete` | Medium | Permanently removes taxonomy terms |
182
+ | `search-replace` | High | Bulk database modification — can corrupt content if misused |
183
+ | `import` | Medium | Bulk content ingestion from WXR files |
177
184
  | `plugin activate` | Medium | Can enable untrusted plugins |
178
185
  | `plugin deactivate` | Medium | Can disable security plugins |
179
186
  | `eval-file` | Critical | Executes arbitrary PHP from a file path |
@@ -186,12 +193,16 @@ claude mcp add diviops-mcp -- env \
186
193
  WP_USER=admin \
187
194
  WP_APP_PASSWORD=xxxx \
188
195
  WP_PATH="/path/to/wordpress" \
189
- DIVIOPS_WP_CLI_ALLOW="option update,post delete" \
196
+ DIVIOPS_WP_CLI_ALLOW="option update,post delete,search-replace" \
190
197
  npx @diviops/mcp-server
191
198
  ```
192
199
 
193
200
  Only list the specific commands you need. Unknown entries are ignored with a warning.
194
201
 
202
+ > **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.
203
+
204
+ > **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.
205
+
195
206
  ## Example Usage
196
207
 
197
208
  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: read commands, post create/update, post meta read/write, cache/rewrite flush, term create. Extended tier (requires DIVIOPS_WP_CLI_ALLOW env var): option update, post delete, post meta delete, plugin activate/deactivate, eval-file. Use --format=json for structured output.",
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.resource("divi-block-format-guide", "divi://block-format-guide", async () => ({
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, arbitrary code execution,
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
- * To enable, set: DIVIOPS_WP_CLI_ALLOW="option update,post delete,eval-file"
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.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server exposing Divi 5 Visual Builder as tools for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,7 +40,7 @@
40
40
  "node": ">=18.0.0"
41
41
  },
42
42
  "dependencies": {
43
- "@modelcontextprotocol/sdk": "^1.12.1",
43
+ "@modelcontextprotocol/sdk": "^1.29.0",
44
44
  "zod": "^4.3.6"
45
45
  },
46
46
  "devDependencies": {