@respira/wordpress-mcp-server 8.1.4 → 8.1.6

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/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to Respira WordPress MCP Server will be documented in this f
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [8.1.6] - 2026-08-02
9
+
10
+ ### Fixed
11
+
12
+ - **An authentication failure was reported as "respira/v2 is not available on this site".** The version probe swallowed its own error and fell back to v1, so every failure mode reached you as the same message followed by four suggested causes: a security plugin, Plain permalinks, a WAF or CDN, an outdated plugin. For M.O. all four were false. Permalinks were `/%postname%/`, there was no CDN, the plugin was already current, and `/wp-json/respira/v2/status` was answering `401` rather than `404`, which proves the opposite of what the message said: the route existed, WordPress routed to it, and it ran far enough to reach its auth check. That cost most of a day chasing firewall rules for a deactivated key. The probe now keeps the reason and reports it. A `401` or `403` says the key was rejected and points at reconnecting, a `5xx` reports the status and sends you to the debug log, a connection failure says so, and the original text is kept only for a real `404`, which is the one case it was ever true for.
13
+
14
+ ### Added
15
+
16
+ - **`excerpt` on `update_page`, `update_post` and `update_custom_post`.** There was no way to ask for it, so callers put it in `meta`, where it became a post meta row that reads back perfectly and renders nowhere. The description says as much. Requires plugin 8.2.1 for the write to land in the native field. Reported by P.F.
17
+
18
+ ## [8.1.5] - 2026-08-01
19
+
20
+ ### Fixed
21
+
22
+ - **Updates now succeed on hosts that block PUT at the edge.** Managed hosts and WAFs commonly allow `GET` and `POST` but reject `PUT`, `PATCH` and `DELETE` before the request ever reaches WordPress. The result was every update failing with "403 Forbidden from the web server (not WordPress)" while creating pages and reading them worked perfectly, which sends you looking at plugin whitelists, permalink settings, CDN exceptions and User-Agent rules when the only thing that actually mattered was the HTTP verb. Respira already shipped the fallback that fixes this, retrying the write as a `POST`, but it only fired on `404` (the old-plugin case), so it never ran for the hosts that needed it. It now also fires on `403` and `405`, at all five write paths. Reported, root-caused and verified live against a real write by O.Q., who also identified the exact shape of the fix.
23
+
8
24
  ## [8.1.4] - 2026-07-30
9
25
 
10
26
  ### Fixed
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  <h1 align="center">Respira WordPress MCP Server</h1>
8
8
 
9
9
  <p align="center">
10
- <strong>198 MCP tools, 303 with the WooCommerce add-on. 16 page builders and the native Site Editor. MCP 2026-07-28 with legacy-client support.</strong><br>
10
+ <strong>198 MCP tools, 303 with the WooCommerce add-on. 17 page builders and the native Site Editor. MCP 2026-07-28 with legacy-client support.</strong><br>
11
11
  Element-level editing, full page creation, HTML-to-builder conversion with rendering fidelity validation, per-site tool governance, storefront design intelligence, stock images, bulk operations — all through one MCP server.
12
12
  </p>
13
13
 
@@ -15,7 +15,7 @@
15
15
  <a href="https://www.npmjs.com/package/@respira/wordpress-mcp-server"><img src="https://img.shields.io/npm/v/@respira/wordpress-mcp-server.svg?style=flat-square&color=10b981" alt="npm version"></a>
16
16
  <a href="https://www.npmjs.com/package/@respira/wordpress-mcp-server"><img src="https://img.shields.io/npm/dm/@respira/wordpress-mcp-server.svg?style=flat-square" alt="npm downloads"></a>
17
17
  <img src="https://img.shields.io/badge/tools-303-10b981?style=flat-square" alt="303 Tools with WooCommerce">
18
- <img src="https://img.shields.io/badge/builders-16-10b981?style=flat-square" alt="16 Page Builders">
18
+ <img src="https://img.shields.io/badge/builders-17-10b981?style=flat-square" alt="16 Page Builders">
19
19
  <img src="https://img.shields.io/badge/TypeScript-100%25-blue?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript">
20
20
  </p>
21
21
 
@@ -33,7 +33,7 @@
33
33
 
34
34
  Other WordPress MCP servers wrap the REST API. They can create posts and pages, but **they can't touch your page builder content**.
35
35
 
36
- Respira includes a WordPress plugin that gives AI **native access** to 16 page builders, plus element-level precision, full page creation from structure, HTML-to-builder conversion, storefront design intelligence, stock image search, and bulk operations across hundreds of pages.
36
+ Respira includes a WordPress plugin that gives AI **native access** to 17 page builders, plus element-level precision, full page creation from structure, HTML-to-builder conversion, storefront design intelligence, stock image search, and bulk operations across hundreds of pages.
37
37
 
38
38
  ### New in 8.1: MCP 2026-07-28, without breaking existing clients
39
39
 
@@ -129,7 +129,7 @@ The MCP server exposes 198 core tools and 103 WooCommerce add-on tools. It autom
129
129
 
130
130
  | Capability | Respira | Typical WordPress MCP servers |
131
131
  |---|---|---|
132
- | Page builder support | **16 builders** (incl. Spectra, Kadence, GenerateBlocks) | None, or a single builder |
132
+ | Page builder support | **17 builders** (incl. Spectra, Kadence, GenerateBlocks) | None, or a single builder |
133
133
  | Element-level find/update/move/remove | **Yes** | No |
134
134
  | Build full pages from structure | **Yes** | No |
135
135
  | Convert HTML to native builder | **Yes** | No |
@@ -817,6 +817,6 @@ MIT © [Respira](https://respira.press)
817
817
  ---
818
818
 
819
819
  <p align="center">
820
- <strong>198 core tools, 303 with WooCommerce. 16 builders. The AI infrastructure layer for WordPress.</strong><br>
820
+ <strong>198 core tools, 303 with WooCommerce. 17 builders. The AI infrastructure layer for WordPress.</strong><br>
821
821
  <a href="https://respira.press">respira.press</a>
822
822
  </p>
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=oq-put-fallback-403.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oq-put-fallback-403.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/oq-put-fallback-403.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,59 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { isRetryableAsPost } from '../wordpress-client.js';
4
+ /**
5
+ * O.Q. field report, 2026-07-31.
6
+ *
7
+ * Managed hosts and WAFs allow GET and POST but block PUT, PATCH and DELETE at
8
+ * the edge, before WordPress sees the request. Every update failed with
9
+ * "403 Forbidden from the web server (not WordPress)" while creation and reads
10
+ * worked, which sends the customer chasing plugin whitelists and CDN rules when
11
+ * the only thing that mattered was the HTTP verb.
12
+ *
13
+ * The client already had a PUT-to-POST fallback. It only fired on 404.
14
+ *
15
+ * THE PART THIS TEST EXISTS FOR: widening the status check alone does NOT fix
16
+ * it, and a build plus the existing suite passed while the bug was still live.
17
+ * The response interceptor rewrites a non-WordPress 403 into a coded Error via
18
+ * codedError(), which sets `name` and does NOT carry `response` across. So a
19
+ * predicate that only looks at `error.response.status` never fires on the exact
20
+ * case the fallback exists for. Proven against a real site with a shim
21
+ * returning 403 on PUT: the status-only version still failed the write, the
22
+ * shape-aware version completed it.
23
+ */
24
+ describe('isRetryableAsPost', () => {
25
+ it('fires on the transformed non-WordPress 403, which has no response object', () => {
26
+ // Exactly what codedError() produces for a host-level block.
27
+ const coded = new Error('Site blocked: ... 403 Forbidden from the web server (not WordPress).');
28
+ coded.name = 'http_403_non_wordpress';
29
+ assert.equal(isRetryableAsPost(coded), true);
30
+ });
31
+ it('a status-only check would have missed that error', () => {
32
+ const coded = new Error('Site blocked');
33
+ coded.name = 'http_403_non_wordpress';
34
+ // Documents why the first attempt at this fix did nothing.
35
+ assert.equal(coded.response, undefined);
36
+ });
37
+ it('still fires on the original raw 404 case (old plugin, no v2 route)', () => {
38
+ assert.equal(isRetryableAsPost({ response: { status: 404 } }), true);
39
+ });
40
+ it('fires on a raw 403 and 405', () => {
41
+ assert.equal(isRetryableAsPost({ response: { status: 403 } }), true);
42
+ assert.equal(isRetryableAsPost({ response: { status: 405 } }), true);
43
+ });
44
+ it('does NOT retry a 403 raised by WordPress itself', () => {
45
+ // A real permission decision. Retrying as POST just fails again, slower,
46
+ // and would muddy the error the user needs to see.
47
+ const wpDenied = new Error('Sorry, you are not allowed to edit this post.');
48
+ wpDenied.name = 'rest_cannot_edit';
49
+ assert.equal(isRetryableAsPost(wpDenied), false);
50
+ });
51
+ it('does not retry unrelated failures', () => {
52
+ assert.equal(isRetryableAsPost({ response: { status: 500 } }), false);
53
+ assert.equal(isRetryableAsPost({ response: { status: 401 } }), false);
54
+ assert.equal(isRetryableAsPost(new Error('network down')), false);
55
+ assert.equal(isRetryableAsPost(undefined), false);
56
+ assert.equal(isRetryableAsPost(null), false);
57
+ });
58
+ });
59
+ //# sourceMappingURL=oq-put-fallback-403.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oq-put-fallback-403.test.js","sourceRoot":"","sources":["../../src/__tests__/oq-put-fallback-403.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,6DAA6D;QAC7D,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,sEAAsE,CAAC,CAAC;QAChG,KAAK,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACtC,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC;QACxC,KAAK,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACtC,2DAA2D;QAC3D,MAAM,CAAC,KAAK,CAAE,KAAa,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IACvE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;QACrE,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IACvE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,yEAAyE;QACzE,mDAAmD;QACnD,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;QAC5E,QAAQ,CAAC,IAAI,GAAG,kBAAkB,CAAC;QACnC,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;QACtE,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;QACtE,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAClE,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,CAAC;QAClD,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=v2-negotiation-failure.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-negotiation-failure.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/v2-negotiation-failure.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,104 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { classifyV2Failure, describeV2Failure } from '../wordpress-client.js';
4
+ /**
5
+ * The v2 probe used to swallow its own error:
6
+ *
7
+ * try { ...get('/wp-json/respira/v2/status')... } catch { }
8
+ * return 'v1'
9
+ *
10
+ * so every failure mode reached the user as one message: "respira/v2 is not
11
+ * available on this site", followed by four suggested causes (security plugin,
12
+ * Plain permalinks, WAF/CDN, outdated plugin).
13
+ *
14
+ * M.O. hit this with a 401. All four suggestions were false: permalinks were
15
+ * /%postname%/, there was no CDN, the plugin was on the newest version, and
16
+ * /wp-json/respira/v2/status answered 401 rather than 404 — meaning the route
17
+ * existed, WordPress routed to it, and it ran far enough to reach its auth
18
+ * check. The message described the one situation that provably was not
19
+ * happening, and cost most of a day.
20
+ */
21
+ const SITE = 'https://example.com';
22
+ const err = (status, data) => ({ response: { status, data } });
23
+ describe('classifyV2Failure', () => {
24
+ it('401 is auth, not a missing namespace', () => {
25
+ const f = classifyV2Failure(err(401, { code: 'respira_invalid_site_token', message: 'token out of date' }));
26
+ assert.equal(f.kind, 'auth');
27
+ assert.equal(f.status, 401);
28
+ assert.equal(f.code, 'respira_invalid_site_token');
29
+ });
30
+ it('403 is auth too', () => {
31
+ assert.equal(classifyV2Failure(err(403)).kind, 'auth');
32
+ });
33
+ it('404 is the one case that really means unavailable', () => {
34
+ assert.equal(classifyV2Failure(err(404)).kind, 'missing');
35
+ });
36
+ it('500 is a server fault, distinct from both', () => {
37
+ assert.equal(classifyV2Failure(err(500)).kind, 'server');
38
+ });
39
+ it('no response at all is a network failure', () => {
40
+ assert.equal(classifyV2Failure({ message: 'ECONNREFUSED' }).kind, 'network');
41
+ });
42
+ it('an undefined error does not throw', () => {
43
+ assert.equal(classifyV2Failure(undefined).kind, 'network');
44
+ });
45
+ });
46
+ describe('describeV2Failure', () => {
47
+ it("the auth message does NOT repeat the four causes that misled the reporter", () => {
48
+ const msg = describeV2Failure(classifyV2Failure(err(401)), SITE);
49
+ assert.ok(!/Wordfence/.test(msg), msg);
50
+ assert.ok(!/permalinks/i.test(msg), msg);
51
+ assert.ok(!/Cloudflare/.test(msg), msg);
52
+ assert.ok(!/outdated/i.test(msg), msg);
53
+ });
54
+ it('the auth message states what a 401 actually proves', () => {
55
+ const msg = describeV2Failure(classifyV2Failure(err(401)), SITE);
56
+ assert.ok(/routes exist and are reachable/.test(msg), msg);
57
+ assert.ok(/authentication result, not a missing namespace/.test(msg), msg);
58
+ });
59
+ it('it names the deactivated-key cause, which is the one that actually happened', () => {
60
+ const msg = describeV2Failure(classifyV2Failure(err(401)), SITE);
61
+ assert.ok(/lapsed licence disables API keys/.test(msg), msg);
62
+ assert.ok(/reconnect the site/i.test(msg), msg);
63
+ });
64
+ it("it quotes the site's own words when they were given", () => {
65
+ const msg = describeV2Failure(classifyV2Failure(err(401, { message: 'The saved connection token for this site is out of date.' })), SITE);
66
+ assert.ok(msg.includes('The saved connection token for this site is out of date.'), msg);
67
+ });
68
+ it('a real 404 keeps the original guidance, which is correct for a 404', () => {
69
+ const msg = describeV2Failure(classifyV2Failure(err(404)), SITE);
70
+ assert.ok(/is not available on this site/.test(msg), msg);
71
+ assert.ok(/Wordfence/.test(msg), msg);
72
+ });
73
+ it('a null failure falls back to the 404 text rather than inventing a cause', () => {
74
+ const msg = describeV2Failure(null, SITE);
75
+ assert.ok(/is not available on this site/.test(msg), msg);
76
+ });
77
+ it('the server branch reports the status instead of blaming the namespace', () => {
78
+ const msg = describeV2Failure(classifyV2Failure(err(500, { message: 'Fatal error' })), SITE);
79
+ assert.ok(/HTTP 500/.test(msg), msg);
80
+ assert.ok(/debug\.log/.test(msg), msg);
81
+ assert.ok(!/Wordfence/.test(msg), msg);
82
+ });
83
+ it('the network branch says connectivity, not configuration', () => {
84
+ const msg = describeV2Failure(classifyV2Failure({ message: 'ETIMEDOUT' }), SITE);
85
+ assert.ok(/connectivity failure, not a plugin problem/.test(msg), msg);
86
+ assert.ok(msg.includes('ETIMEDOUT'), msg);
87
+ });
88
+ it('branches where probing helps point at the probe URL for the specific site', () => {
89
+ // Not the auth branch: /v2/status answers 401 to an unauthenticated browser
90
+ // whether or not the key is the problem, so sending someone there proves
91
+ // nothing. Its next step is reconnecting, asserted above.
92
+ for (const e of [err(404), err(500), { message: 'x' }]) {
93
+ const msg = describeV2Failure(classifyV2Failure(e), SITE);
94
+ assert.ok(msg.includes(`${SITE}/wp-json/respira/v2/status`), msg);
95
+ }
96
+ });
97
+ it('no branch ever tells the reader the namespace is missing when it is not', () => {
98
+ for (const e of [err(401), err(403), err(500), { message: 'x' }]) {
99
+ const msg = describeV2Failure(classifyV2Failure(e), SITE);
100
+ assert.ok(!/is not available on this site/.test(msg), msg);
101
+ }
102
+ });
103
+ });
104
+ //# sourceMappingURL=v2-negotiation-failure.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-negotiation-failure.test.js","sourceRoot":"","sources":["../../src/__tests__/v2-negotiation-failure.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=v2-negotiation-through-real-client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-negotiation-through-real-client.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/v2-negotiation-through-real-client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,104 @@
1
+ import { describe, it, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import http from 'node:http';
4
+ import { WordPressClient } from '../wordpress-client.js';
5
+ /**
6
+ * The check the pure-unit tests structurally cannot make.
7
+ *
8
+ * v2-negotiation-failure.test.ts feeds classifyV2Failure a hand-built
9
+ * `{response:{status:401}}`. In production no such object ever reaches it: the
10
+ * shared axios error interceptor funnels every non-2xx through handleError ->
11
+ * codedError, which returns `new Error(message)` with the status recorded only
12
+ * in `.name`. `.response` is gone.
13
+ *
14
+ * So an implementation that reads `error.response.status` passes every unit
15
+ * test and misclassifies *every real failure* as a network problem — telling a
16
+ * customer with a deactivated key that the site could not be reached. That is
17
+ * the same trap that made the first PUT->POST fallback fix a no-op (it needed
18
+ * isRetryableAsPost() matching on `.name` for exactly this reason).
19
+ *
20
+ * These tests run a real HTTP server, a real WordPressClient with its real
21
+ * interceptors, and assert on the message a user would actually see.
22
+ */
23
+ const servers = [];
24
+ after(() => {
25
+ for (const s of servers)
26
+ s.close();
27
+ });
28
+ /** Start a server that answers the v2 status probe with a fixed status. */
29
+ async function clientAnswering(status, body) {
30
+ const server = http.createServer((_req, res) => {
31
+ res.writeHead(status, { 'Content-Type': 'application/json' });
32
+ res.end(JSON.stringify(body));
33
+ });
34
+ servers.push(server);
35
+ await new Promise((resolve) => server.listen(0, resolve));
36
+ const { port } = server.address();
37
+ return new WordPressClient({
38
+ id: 'probe',
39
+ name: 'probe',
40
+ url: `http://127.0.0.1:${port}`,
41
+ apiKey: 'respira_test_key',
42
+ });
43
+ }
44
+ /** Drive a v2-gated call and return the message the user would see. */
45
+ async function v2FailureMessage(client) {
46
+ try {
47
+ await client.listSnapshots({ post_id: 1 });
48
+ return '(no error thrown)';
49
+ }
50
+ catch (err) {
51
+ return String(err?.message ?? err);
52
+ }
53
+ }
54
+ describe('v2 negotiation through the real client', () => {
55
+ it('a real 401 is reported as an auth rejection, not a missing namespace', async () => {
56
+ const client = await clientAnswering(401, {
57
+ code: 'respira_invalid_site_token',
58
+ message: 'The saved connection token for this site is out of date.',
59
+ });
60
+ const msg = await v2FailureMessage(client);
61
+ assert.match(msg, /rejected this API key \(HTTP 401/);
62
+ assert.doesNotMatch(msg, /is not available on this site/);
63
+ // The four causes that sent the reporter chasing firewall rules for a day.
64
+ assert.doesNotMatch(msg, /Wordfence/);
65
+ assert.doesNotMatch(msg, /permalinks/i);
66
+ assert.doesNotMatch(msg, /Cloudflare/);
67
+ });
68
+ it("and quotes the site's own explanation", async () => {
69
+ const client = await clientAnswering(401, {
70
+ code: 'respira_invalid_site_token',
71
+ message: 'The saved connection token for this site is out of date.',
72
+ });
73
+ const msg = await v2FailureMessage(client);
74
+ assert.match(msg, /The saved connection token for this site is out of date\./);
75
+ assert.match(msg, /respira_invalid_site_token/);
76
+ });
77
+ it('a 403 is auth too', async () => {
78
+ const client = await clientAnswering(403, { code: 'respira_scope_denied', message: 'nope' });
79
+ const msg = await v2FailureMessage(client);
80
+ assert.match(msg, /rejected this API key \(HTTP 403/);
81
+ });
82
+ it('a real 404 keeps the original guidance, which is correct only for a 404', async () => {
83
+ const client = await clientAnswering(404, { code: 'rest_no_route', message: 'No route' });
84
+ const msg = await v2FailureMessage(client);
85
+ assert.match(msg, /is not available on this site/);
86
+ assert.match(msg, /Wordfence/);
87
+ });
88
+ it('a 500 names the status instead of blaming the namespace', async () => {
89
+ const client = await clientAnswering(500, { code: 'fatal', message: 'Fatal error' });
90
+ const msg = await v2FailureMessage(client);
91
+ assert.match(msg, /returned HTTP 500 on this site/);
92
+ assert.doesNotMatch(msg, /is not available on this site/);
93
+ });
94
+ it('a dead host is reported as connectivity, not configuration', async () => {
95
+ // Nothing listening: a genuine transport failure, where there IS no status.
96
+ const client = new WordPressClient({
97
+ id: 'probe', name: 'probe', url: 'http://127.0.0.1:1', apiKey: 'respira_test_key',
98
+ });
99
+ const msg = await v2FailureMessage(client);
100
+ assert.match(msg, /Could not reach respira\/v2|connectivity failure/);
101
+ assert.doesNotMatch(msg, /Wordfence/);
102
+ });
103
+ });
104
+ //# sourceMappingURL=v2-negotiation-through-real-client.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-negotiation-through-real-client.test.js","sourceRoot":"","sources":["../../src/__tests__/v2-negotiation-through-real-client.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAsQzE,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,KAAK,CAA2C;IACxD,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB,CAAS;IACpC,iFAAiF;IACjF,OAAO,CAAC,iBAAiB,CAAK;IAC9B,iFAAiF;IACjF,OAAO,CAAC,mBAAmB,CAAgC;IAE3D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAsB;IAEhE;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;gBA4Bb,WAAW,EAAE,mBAAmB,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE;IAiVvE,OAAO,CAAC,cAAc;IAItB;;;;;;;;;OASG;IACH,OAAO,CAAC,oBAAoB;IAkB5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IA4BrB,gEAAgE;IAChE,OAAO,CAAC,aAAa;IAUrB;;;;;;OAMG;YACW,UAAU;IA2CxB;;;;;;;;;;;;;;OAcG;YACW,WAAW;IAiJzB;;;;;;;;;OASG;YACW,kBAAkB;IA6KhC;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;YACW,qBAAqB;YAiBrB,kBAAkB;IA6FhC,yFAAyF;IACzF,OAAO,CAAC,gBAAgB;IASxB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,6BAA6B;IA0BrC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA+BjC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YAmQP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IA+xGtB;;;;;;OAMG;IACH;;;;;;;;;;;;;oDAagD;IAChD,OAAO,CAAC,mBAAmB,CAAoD;IAC/E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAAU;IAC1D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAS;IAC1D;;mDAE+C;IAC/C,OAAO,CAAC,0BAA0B,CAAoC;IAEtE;;;;;;;;OAQG;YACW,0BAA0B;YAyC1B,oBAAoB;YA6CpB,2BAA2B;IAQzC;;;;OAIG;YACW,cAAc;IAQ5B,OAAO,CAAC,mBAAmB;IAo7C3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAe3C;IAEF;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;YAmCpB,cAAc;IA4G5B,oEAAoE;IACpE,OAAO,CAAC,iBAAiB;IAoBzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,eAAe;YAuCT,gBAAgB;IA+0C9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAkZzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA6UxB,GAAG;CA+CV"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAsQzE,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,KAAK,CAA2C;IACxD,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB,CAAS;IACpC,iFAAiF;IACjF,OAAO,CAAC,iBAAiB,CAAK;IAC9B,iFAAiF;IACjF,OAAO,CAAC,mBAAmB,CAAgC;IAE3D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAsB;IAEhE;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;gBA4Bb,WAAW,EAAE,mBAAmB,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE;IAiVvE,OAAO,CAAC,cAAc;IAItB;;;;;;;;;OASG;IACH,OAAO,CAAC,oBAAoB;IAkB5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IA4BrB,gEAAgE;IAChE,OAAO,CAAC,aAAa;IAUrB;;;;;;OAMG;YACW,UAAU;IA2CxB;;;;;;;;;;;;;;OAcG;YACW,WAAW;IAiJzB;;;;;;;;;OASG;YACW,kBAAkB;IA6KhC;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;YACW,qBAAqB;YAiBrB,kBAAkB;IA6FhC,yFAAyF;IACzF,OAAO,CAAC,gBAAgB;IASxB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,6BAA6B;IA0BrC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA+BjC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YAmQP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IA2yGtB;;;;;;OAMG;IACH;;;;;;;;;;;;;oDAagD;IAChD,OAAO,CAAC,mBAAmB,CAAoD;IAC/E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAAU;IAC1D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAS;IAC1D;;mDAE+C;IAC/C,OAAO,CAAC,0BAA0B,CAAoC;IAEtE;;;;;;;;OAQG;YACW,0BAA0B;YAyC1B,oBAAoB;YA6CpB,2BAA2B;IAQzC;;;;OAIG;YACW,cAAc;IAQ5B,OAAO,CAAC,mBAAmB;IAo7C3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAe3C;IAEF;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;YAmCpB,cAAc;IA4G5B,oEAAoE;IACpE,OAAO,CAAC,iBAAiB;IAoBzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,eAAe;YAuCT,gBAAgB;IA+0C9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAkZzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA6UxB,GAAG;CA+CV"}
package/dist/server.js CHANGED
@@ -1836,7 +1836,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
1836
1836
  },
1837
1837
  {
1838
1838
  name: 'wordpress_update_page',
1839
- description: 'Update PAGE-LEVEL fields (title, status, slug, full HTML content replacement, custom CSS). For editing SPECIFIC elements WITHIN a page (text widgets, images, buttons, headings), use respira_update_element instead — it is element-aware, builder-aware, snapshot-safe, and works across all 12 page builders. Use respira_update_page only when you need to replace the entire content body, change the page title/slug/status, or set page-level metadata. When direct editing is enabled and you target an original page, Respira returns a confirmation_required preflight by default so you can choose live/original or duplicate.',
1839
+ description: 'Update PAGE-LEVEL fields (title, status, slug, full HTML content replacement, custom CSS). For editing SPECIFIC elements WITHIN a page (text widgets, images, buttons, headings), use respira_update_element instead — it is element-aware, builder-aware, snapshot-safe, and works across all 17 page builders. Use respira_update_page only when you need to replace the entire content body, change the page title/slug/status, or set page-level metadata. When direct editing is enabled and you target an original page, Respira returns a confirmation_required preflight by default so you can choose live/original or duplicate.',
1840
1840
  inputSchema: {
1841
1841
  type: 'object',
1842
1842
  properties: {
@@ -1860,6 +1860,10 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
1860
1860
  type: 'string',
1861
1861
  description: 'URL slug (post_name). Example: "about-us" produces /about-us/.',
1862
1862
  },
1863
+ excerpt: {
1864
+ type: 'string',
1865
+ description: 'Post excerpt (wp_posts.post_excerpt) — the "Excerpt"/"Zajawka" panel, RSS summaries and archive teasers. Do NOT pass this inside meta: an excerpt written as post meta reads back fine and renders nowhere.',
1866
+ },
1863
1867
  custom_css: {
1864
1868
  type: 'string',
1865
1869
  description: 'Custom CSS for the page. Auto-detects builder: Divi → _et_pb_custom_css, Elementor → _elementor_page_settings.custom_css.',
@@ -2137,6 +2141,10 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
2137
2141
  type: 'string',
2138
2142
  description: 'URL slug (post_name). Example: "my-post" produces /my-post/.',
2139
2143
  },
2144
+ excerpt: {
2145
+ type: 'string',
2146
+ description: 'Post excerpt (wp_posts.post_excerpt) — the "Excerpt"/"Zajawka" panel, RSS summaries and archive teasers. Do NOT pass this inside meta: an excerpt written as post meta reads back fine and renders nowhere.',
2147
+ },
2140
2148
  author: {
2141
2149
  description: 'Author (user id or user login). Requires edit_others_posts capability when different from the acting user.',
2142
2150
  oneOf: [
@@ -2293,7 +2301,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
2293
2301
  properties: {
2294
2302
  builder: {
2295
2303
  type: 'string',
2296
- description: 'Optional. Builder name. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery. Omit to auto-detect the active site builder.',
2304
+ description: 'Optional. Builder name. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery, flatsome, spectra, kadence, generateblocks, greenshift. Omit to auto-detect the active site builder.',
2297
2305
  },
2298
2306
  page_id: {
2299
2307
  type: 'number',
@@ -2312,7 +2320,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
2312
2320
  properties: {
2313
2321
  builder: {
2314
2322
  type: 'string',
2315
- description: 'Builder name. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery. Use get_builder_info to discover the active builder first.',
2323
+ description: 'Builder name. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery, flatsome, spectra, kadence, generateblocks, greenshift. Use get_builder_info to discover the active builder first.',
2316
2324
  },
2317
2325
  page_id: {
2318
2326
  type: 'number',
@@ -2376,13 +2384,13 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
2376
2384
  },
2377
2385
  {
2378
2386
  name: 'wordpress_inject_builder_content',
2379
- description: 'REPLACE (or append to) the entire page builder layout. WARNING: By default this REPLACES all existing content (use mode:"append" to add content without destroying existing elements). For editing a single module, use wordpress_update_module instead. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery. For Divi, divi_version is required ("4" or "5"). Replacing a page that already has content with mode="replace" requires confirm_replace=true, else returns 409 respira_replace_confirmation_required. Alternatives: mode="append" to add to existing content, or edit_target="live" to overwrite the live page directly. The gate prevents silent data loss.\n\nCanonical Divi 5 nested shape: `content: [{ type: "section", children: [{ type: "row", children: [{ type: "column", children: [{ type: "heading", settings: {...} }] }] }] }]` (rows/cols/columns/modules/elements/innerBlocks also accepted as `children` aliases). Each Divi module needs its render-critical attr in `settings` (e.g. heading→title, text→content, button→button_text); the plugin names any missing one via `respira_divi_required_attrs_missing`.',
2387
+ description: 'REPLACE (or append to) the entire page builder layout. WARNING: By default this REPLACES all existing content (use mode:"append" to add content without destroying existing elements). For editing a single module, use wordpress_update_module instead. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery, flatsome, spectra, kadence, generateblocks, greenshift. For Divi, divi_version is required ("4" or "5"). Replacing a page that already has content with mode="replace" requires confirm_replace=true, else returns 409 respira_replace_confirmation_required. Alternatives: mode="append" to add to existing content, or edit_target="live" to overwrite the live page directly. The gate prevents silent data loss.\n\nCanonical Divi 5 nested shape: `content: [{ type: "section", children: [{ type: "row", children: [{ type: "column", children: [{ type: "heading", settings: {...} }] }] }] }]` (rows/cols/columns/modules/elements/innerBlocks also accepted as `children` aliases). Each Divi module needs its render-critical attr in `settings` (e.g. heading→title, text→content, button→button_text); the plugin names any missing one via `respira_divi_required_attrs_missing`.',
2380
2388
  inputSchema: {
2381
2389
  type: 'object',
2382
2390
  properties: {
2383
2391
  builder: {
2384
2392
  type: 'string',
2385
- description: 'Builder identifier. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery.',
2393
+ description: 'Builder identifier. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery, flatsome, spectra, kadence, generateblocks, greenshift.',
2386
2394
  },
2387
2395
  page_id: {
2388
2396
  type: 'number',
@@ -2442,7 +2450,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
2442
2450
  properties: {
2443
2451
  builder: {
2444
2452
  type: 'string',
2445
- description: 'Builder name. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery.',
2453
+ description: 'Builder name. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery, flatsome, spectra, kadence, generateblocks, greenshift.',
2446
2454
  },
2447
2455
  page_id: {
2448
2456
  type: 'number',
@@ -4424,6 +4432,10 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
4424
4432
  type: 'string',
4425
4433
  description: 'URL slug (post_name).',
4426
4434
  },
4435
+ excerpt: {
4436
+ type: 'string',
4437
+ description: 'Post excerpt (wp_posts.post_excerpt) — the "Excerpt"/"Zajawka" panel, RSS summaries and archive teasers. Do NOT pass this inside meta: an excerpt written as post meta reads back fine and renders nowhere.',
4438
+ },
4427
4439
  author: {
4428
4440
  description: 'Author (user id or user login). Requires edit_others_posts capability when different from the acting user.',
4429
4441
  oneOf: [
@@ -7843,7 +7855,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
7843
7855
  // --- Element Operations ---
7844
7856
  {
7845
7857
  name: 'wordpress_find_element',
7846
- description: 'Find an element in a page by ID, type, CSS class, text content, or uncode_shortcode_id. This is the PRIMARY tool for locating content to edit — use it instead of searching the database or reading PHP files. Works with all 12 page builders. Use identifier_type "content" to search by visible text (e.g. find a heading containing "2025" to update a year). On WPBakery + Uncode pages, prefer identifier_type "uncode_shortcode_id" — the id is stable across saves and theme updates while class lists and admin labels drift. Returns matching element(s) with their position, settings, and element ID for use with update_element.',
7858
+ description: 'Find an element in a page by ID, type, CSS class, text content, or uncode_shortcode_id. This is the PRIMARY tool for locating content to edit — use it instead of searching the database or reading PHP files. Works with all 17 page builders. Use identifier_type "content" to search by visible text (e.g. find a heading containing "2025" to update a year). On WPBakery + Uncode pages, prefer identifier_type "uncode_shortcode_id" — the id is stable across saves and theme updates while class lists and admin labels drift. Returns matching element(s) with their position, settings, and element ID for use with update_element.',
7847
7859
  inputSchema: {
7848
7860
  type: 'object',
7849
7861
  properties: {
@@ -7865,7 +7877,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
7865
7877
  },
7866
7878
  {
7867
7879
  name: 'wordpress_update_element',
7868
- description: 'Update settings or content on a specific element in a page. This is the PRIMARY tool for making content changes (text edits, style changes, image swaps, link updates, etc.). Works with all 12 page builders. First use find_element to locate the element, then pass the same identifier here with the updates object containing the new values. On WPBakery + Uncode pages, prefer identifier_type "uncode_shortcode_id" for stable round-trip matching. Response includes target_id, original_id, edit_target ("live" or "duplicate"), is_duplicate, duplicate_created and post_status so the caller can never mistake a duplicate-routed write for a live-page change.\n\nLive-edit confirmation: when respira_allow_direct_edit=1 AND the post is a published original, the first call returns `status: "confirmation_required"` with a `next_call_examples` payload. Re-call with the suggested `edit_target` (and `confirm_live_edit: true` for the live path) to acknowledge. Both params are exposed at the top level here so autonomous flows can complete the handshake through MCP without polluting the `updates` object.',
7880
+ description: 'Update settings or content on a specific element in a page. This is the PRIMARY tool for making content changes (text edits, style changes, image swaps, link updates, etc.). Works with all 17 page builders. First use find_element to locate the element, then pass the same identifier here with the updates object containing the new values. On WPBakery + Uncode pages, prefer identifier_type "uncode_shortcode_id" for stable round-trip matching. Response includes target_id, original_id, edit_target ("live" or "duplicate"), is_duplicate, duplicate_created and post_status so the caller can never mistake a duplicate-routed write for a live-page change.\n\nLive-edit confirmation: when respira_allow_direct_edit=1 AND the post is a published original, the first call returns `status: "confirmation_required"` with a `next_call_examples` payload. Re-call with the suggested `edit_target` (and `confirm_live_edit: true` for the live path) to acknowledge. Both params are exposed at the top level here so autonomous flows can complete the handshake through MCP without polluting the `updates` object.',
7869
7881
  inputSchema: {
7870
7882
  type: 'object',
7871
7883
  properties: {