@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 +39 -24
- package/dist/wp-cli.js +9 -0
- package/dist/wp-client.js +66 -21
- package/package.json +1 -1
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
|
|
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
|
|
45
|
-
WP_URL=http://your-site.local \
|
|
46
|
-
WP_USER=your-wp-username \
|
|
47
|
-
WP_APP_PASSWORD=xxxxXXXXxxxxXXXXxxxxXXXX \
|
|
48
|
-
WP_PATH
|
|
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
|
|
55
|
-
WP_URL=https://site-name.ddev.site \
|
|
56
|
-
WP_USER=your-wp-username \
|
|
57
|
-
WP_APP_PASSWORD=xxxxXXXXxxxxXXXXxxxxXXXX \
|
|
58
|
-
WP_CLI_CMD=
|
|
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
|
|
202
|
-
WP_URL=http://your-site.local \
|
|
203
|
-
WP_USER=admin \
|
|
204
|
-
WP_APP_PASSWORD=xxxx \
|
|
205
|
-
WP_PATH
|
|
206
|
-
DIVIOPS_WP_CLI_ALLOW=
|
|
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
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
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) =>
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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;
|