@diviops/mcp-server 0.2.19 → 0.2.21

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
@@ -32,31 +32,33 @@ Go to **WP Admin -> Users -> Your Profile -> Application Passwords**:
32
32
  ### 3. Configure Claude Code
33
33
 
34
34
  ```bash
35
- claude mcp add diviops-mcp -- env \
36
- WP_URL=http://your-site.local \
37
- WP_USER=your-wp-username \
38
- WP_APP_PASSWORD=xxxxXXXXxxxxXXXXxxxxXXXX \
39
- npx @diviops/mcp-server
35
+ claude mcp add diviops-mcp \
36
+ --env WP_URL=http://your-site.local \
37
+ --env WP_USER=your-wp-username \
38
+ --env WP_APP_PASSWORD=xxxxXXXXxxxxXXXXxxxxXXXX \
39
+ -- npx @diviops/mcp-server
40
40
  ```
41
41
 
42
+ > **Use `--env` flags, not the `env` command.** Claude Code's native `--env KEY=VALUE` flags survive copy-paste; the older `-- env KEY=VALUE` form (piping through unix `env`) breaks silently when any value contains a space. Quote any value with spaces (e.g. `--env "WP_PATH=/Users/you/Local Sites/site/app/public"`) — no backslash escaping needed inside quotes.
43
+
42
44
  **With WP-CLI** (optional — enables `diviops_wp_cli` tool):
43
45
  ```bash
44
- claude mcp add diviops-mcp -- env \
45
- WP_URL=http://your-site.local \
46
- WP_USER=your-wp-username \
47
- WP_APP_PASSWORD=xxxxXXXXxxxxXXXXxxxxXXXX \
48
- WP_PATH="/path/to/wordpress" \
49
- npx @diviops/mcp-server
46
+ claude mcp add diviops-mcp \
47
+ --env WP_URL=http://your-site.local \
48
+ --env WP_USER=your-wp-username \
49
+ --env WP_APP_PASSWORD=xxxxXXXXxxxxXXXXxxxxXXXX \
50
+ --env "WP_PATH=/path/to/wordpress" \
51
+ -- npx @diviops/mcp-server
50
52
  ```
51
53
 
52
54
  **With Docker-based WP-CLI** (optional — uses a custom command prefix):
53
55
  ```bash
54
- claude mcp add diviops-mcp -- env \
55
- WP_URL=https://site-name.ddev.site \
56
- WP_USER=your-wp-username \
57
- WP_APP_PASSWORD=xxxxXXXXxxxxXXXXxxxxXXXX \
58
- WP_CLI_CMD="ddev wp" \
59
- npx @diviops/mcp-server
56
+ claude mcp add diviops-mcp \
57
+ --env WP_URL=https://site-name.ddev.site \
58
+ --env WP_USER=your-wp-username \
59
+ --env WP_APP_PASSWORD=xxxxXXXXxxxxXXXXxxxxXXXX \
60
+ --env "WP_CLI_CMD=ddev wp" \
61
+ -- npx @diviops/mcp-server
60
62
  ```
61
63
 
62
64
  ### Environment Variables
@@ -186,6 +188,7 @@ These commands carry higher risk and require explicit opt-in via the `DIVIOPS_WP
186
188
  | Command | Risk | Why opt-in |
187
189
  |---------|------|------------|
188
190
  | `option update` | High | Can change site URL, admin email, or security settings |
191
+ | `option delete` | High | Permanently removes a WP option (no undo) |
189
192
  | `post delete` | Medium | Permanently removes content |
190
193
  | `post meta delete` | Medium | Removes metadata |
191
194
  | `term delete` | Medium | Permanently removes taxonomy terms |
@@ -198,17 +201,29 @@ These commands carry higher risk and require explicit opt-in via the `DIVIOPS_WP
198
201
  To enable extended commands, add `DIVIOPS_WP_CLI_ALLOW` to your MCP registration:
199
202
 
200
203
  ```bash
201
- claude mcp add diviops-mcp -- env \
202
- WP_URL=http://your-site.local \
203
- WP_USER=admin \
204
- WP_APP_PASSWORD=xxxx \
205
- WP_PATH="/path/to/wordpress" \
206
- DIVIOPS_WP_CLI_ALLOW="option update,post delete,search-replace" \
207
- npx @diviops/mcp-server
204
+ claude mcp add diviops-mcp \
205
+ --env WP_URL=http://your-site.local \
206
+ --env WP_USER=admin \
207
+ --env WP_APP_PASSWORD=xxxx \
208
+ --env "WP_PATH=/path/to/wordpress" \
209
+ --env "DIVIOPS_WP_CLI_ALLOW=option update,post delete,search-replace" \
210
+ -- npx @diviops/mcp-server
208
211
  ```
209
212
 
210
213
  Only list the specific commands you need. Unknown entries are ignored with a warning.
211
214
 
215
+ #### Wildcard / "god-mode" (local dev only)
216
+
217
+ For trusted local-dev environments where you don't want to re-list every extended command per site, the values `*` and `all` grant the full extended set:
218
+
219
+ ```bash
220
+ --env "DIVIOPS_WP_CLI_ALLOW=*"
221
+ ```
222
+
223
+ The sentinel grants exactly the extended set above — it does NOT unlock anything beyond it (notably: `db query` stays out by design). The server emits a startup warning to stderr whenever the wildcard is active, so the broad grant is never silent. Auto-adopts new extended commands on future versions.
224
+
225
+ > **Don't use this in shared or production environments.** Pin the specific commands you need with the comma-separated form instead.
226
+
212
227
  > **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.
213
228
 
214
229
  ### Filesystem flag validation
package/dist/wp-cli.js CHANGED
@@ -90,6 +90,7 @@ const DEFAULT_COMMANDS = [
90
90
  */
91
91
  const EXTENDED_COMMANDS = [
92
92
  'option update', // Can change site URL, admin email, active plugins
93
+ 'option delete', // Destructive — permanently removes a WP option
93
94
  'post delete', // Destructive — permanently removes content
94
95
  'post meta delete', // Destructive — removes metadata
95
96
  'term delete', // Destructive — removes taxonomy terms
@@ -104,6 +105,14 @@ function buildAllowlist() {
104
105
  const extra = process.env.DIVIOPS_WP_CLI_ALLOW?.trim();
105
106
  if (!extra)
106
107
  return DEFAULT_COMMANDS;
108
+ // Wildcard sentinel — convenience for trusted local-dev environments. Grants
109
+ // every entry in EXTENDED_COMMANDS but does NOT unlock anything beyond it
110
+ // (notably: `db query` stays out — see #361 Chunk B). Always emits a startup
111
+ // warning so the broad grant is never silent.
112
+ if (extra === '*' || extra === 'all') {
113
+ console.warn(`[diviops] DIVIOPS_WP_CLI_ALLOW="${extra}" — granting ALL ${EXTENDED_COMMANDS.length} extended commands. Intended for trusted local-dev only.`);
114
+ return [...DEFAULT_COMMANDS, ...EXTENDED_COMMANDS];
115
+ }
107
116
  const requested = extra.split(',').map((s) => s.trim()).filter(Boolean);
108
117
  const granted = new Set(DEFAULT_COMMANDS);
109
118
  for (const cmd of requested) {
package/dist/wp-client.js CHANGED
@@ -6,43 +6,88 @@
6
6
  */
7
7
  import { MIN_PLUGIN_VERSION, compareVersions, } from './compatibility.js';
8
8
  /**
9
- * Normalize over-escaped variable-token quote sequences inside `$variable(...)$`
10
- * token regions only.
9
+ * Normalize quote-escape pathologies inside `$variable(...)$` token regions only.
11
10
  *
12
- * Divi block-attrs JSON uses `\"` (2-byte) for inner quotes inside Divi variable
13
- * tokens. Two over-escaped forms can leak in via callers that round-trip
14
- * pre-existing broken bytes:
15
- * - `\u0022` (11 bytes literal) — the mass-corruption form (backslash
16
- * itself unicode-escaped, observed in the wild on Divi 5.3.x sites)
17
- * - `\\u0022` (7 bytes literal) — produced when a caller passes the
18
- * 6-byte `"` form through an extra JSON-encoding layer
11
+ * Divi block-attrs JSON uses `\"` (2-byte: backslash + quote) for inner quotes
12
+ * inside variable token payloads. Three pathological forms can leak in through
13
+ * callers and silently break the WP block parser at write time.
19
14
  *
20
- * Both decode to the same value after Divi's resolver, but cause render asymmetry
21
- * across attr paths (`background.color`, `spacing.margin`, `border.color`,
22
- * `layout.columnGap` silently fail to resolve). We normalize defensively so any
23
- * write clean or pre-broken settles on canonical bytes.
15
+ * Over-escape (existing #395/#396 fix). Two forms produced when callers
16
+ * round-trip pre-existing broken bytes:
17
+ * - `\u005cu0022` (11 bytes literal) the
18
+ * mass-corruption form (backslash itself unicode-escaped, observed in the
19
+ * wild on Divi 5.3.x sites)
20
+ * - `\\u0022` (7 bytes literal: 2 backslashes + `u0022`) — produced when
21
+ * a caller passes the 6-byte unicode-escape form through an extra
22
+ * JSON-encoding layer
23
+ * These cause render-only failure: the resolver can't decode the token, and
24
+ * attr paths like `background.color`, `spacing.margin`, `border.color`,
25
+ * `layout.columnGap` silently fall through to defaults (or leak literal
26
+ * `0022` into emitted CSS).
27
+ *
28
+ * Under-escape (#409 fix). One form produced when an agent transcribes
29
+ * `get_section` markup (which emits inner quotes as `"` HTML entities) and
30
+ * a layer in the agent → MCP → WP pipeline strips one level of escaping:
31
+ * - bare `"` (1 byte) — the inner quote loses its `\` prefix and prematurely
32
+ * terminates the OUTER block-attrs string at parse time. The WP block
33
+ * parser then silently drops ALL attrs from the affected module. Section
34
+ * appears to save (`success: true`) but renders empty / broken.
35
+ *
36
+ * We normalize defensively so any write — clean, pre-broken, or
37
+ * agent-transcribed — settles on canonical 2-byte `\"`.
38
+ *
39
+ * Order matters: collapse over-escapes first, then escape under-escapes. The
40
+ * negative lookbehind on the under-escape rule skips `\"` produced by the
41
+ * over-escape pass (and any already-canonical input). Idempotent.
24
42
  *
25
43
  * Scope is intentionally narrow: rewrites only happen inside `$variable(...)$`
26
44
  * regions (Divi's actual resolver boundary). Bytes outside those regions —
27
45
  * arbitrary user text, code samples, string-variable values that happen to
28
- * contain `\u0022` — are left untouched.
46
+ * contain `\u005cu0022`, `\\u0022`, or bare `"` — are left untouched.
29
47
  */
30
48
  function normalizeQuoteEscapes(s) {
31
- return s.replace(/\$variable\([^$]*?\)\$/g, (token) => token.replace(/(?:\\u005cu0022|\\\\u0022)/g, '\\"'));
49
+ return s.replace(/\$variable\([^$]*?\)\$/g, (token) => {
50
+ // Pass 1: collapse over-escaped forms (#395/#396) to canonical \"
51
+ let normalized = token.replace(/(?:\\u005cu0022|\\\\u0022)/g, '\\"');
52
+ // Pass 2: escape any bare " (#409) to canonical \" — negative lookbehind
53
+ // skips properly-escaped quotes produced by Pass 1 or already canonical.
54
+ normalized = normalized.replace(/(?<!\\)"/g, '\\"');
55
+ return normalized;
56
+ });
32
57
  }
33
- function normalizeBody(value) {
34
- if (typeof value === 'string')
35
- return normalizeQuoteEscapes(value);
58
+ /**
59
+ * Body keys whose values (and descendants) carry Divi block markup or block
60
+ * attribute trees, where `$variable(...)$` token-region normalization is the
61
+ * intended behavior. Strings reachable only through other top-level keys
62
+ * — variable values, labels, match-text predicates, descriptions, etc.
63
+ * — are passed through verbatim so a literal `$variable({"x":"y"})$`
64
+ * docs example in a `string_variable_value` is preserved (#409 review:
65
+ * Codex-flagged regression — without this scoping, the bare-quote pass would
66
+ * silently rewrite literal token-shaped substrings in user-prose fields).
67
+ */
68
+ const BLOCK_CONTENT_KEYS = new Set([
69
+ 'content', // update_page_content, render_preview, validate_blocks,
70
+ // append_section, replace_section, update_tb_layout,
71
+ // save_to_library, create_page
72
+ 'attrs', // update_module — attr values embedded in block JSON
73
+ 'header_content', // create_tb_template
74
+ 'footer_content', // create_tb_template
75
+ ]);
76
+ function normalizeBody(value, withinBlockTree = false) {
77
+ if (typeof value === 'string') {
78
+ return withinBlockTree ? normalizeQuoteEscapes(value) : value;
79
+ }
36
80
  if (Array.isArray(value))
37
- return value.map(normalizeBody);
81
+ return value.map((v) => normalizeBody(v, withinBlockTree));
38
82
  // Restrict recursion to plain objects so Date / RegExp / class instances
39
83
  // with custom `toJSON` keep their canonical serialization.
40
84
  if (value &&
41
85
  typeof value === 'object' &&
42
86
  Object.getPrototypeOf(value) === Object.prototype) {
43
87
  const out = {};
44
- for (const [k, v] of Object.entries(value))
45
- out[k] = normalizeBody(v);
88
+ for (const [k, v] of Object.entries(value)) {
89
+ out[k] = normalizeBody(v, withinBlockTree || BLOCK_CONTENT_KEYS.has(k));
90
+ }
46
91
  return out;
47
92
  }
48
93
  return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diviops/mcp-server",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "description": "MCP server exposing Divi 5 Visual Builder as tools for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",