@respira/wordpress-mcp-server 8.3.25 → 8.3.26
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 +55 -0
- package/README.md +17 -0
- package/dist/__tests__/087103bc-the-host-answers-well-known-first.test.d.ts +2 -0
- package/dist/__tests__/087103bc-the-host-answers-well-known-first.test.d.ts.map +1 -0
- package/dist/__tests__/087103bc-the-host-answers-well-known-first.test.js +73 -0
- package/dist/__tests__/087103bc-the-host-answers-well-known-first.test.js.map +1 -0
- package/dist/__tests__/685d8023-a-403-from-the-web-server-shows-its-evidence.test.d.ts +2 -0
- package/dist/__tests__/685d8023-a-403-from-the-web-server-shows-its-evidence.test.d.ts.map +1 -0
- package/dist/__tests__/685d8023-a-403-from-the-web-server-shows-its-evidence.test.js +106 -0
- package/dist/__tests__/685d8023-a-403-from-the-web-server-shows-its-evidence.test.js.map +1 -0
- package/dist/__tests__/81c478df-rest-no-route-blames-the-plugin.test.d.ts +16 -0
- package/dist/__tests__/81c478df-rest-no-route-blames-the-plugin.test.d.ts.map +1 -0
- package/dist/__tests__/81c478df-rest-no-route-blames-the-plugin.test.js +105 -0
- package/dist/__tests__/81c478df-rest-no-route-blames-the-plugin.test.js.map +1 -0
- package/dist/__tests__/dfc0e156-plugin-file-with-a-slash.test.d.ts +2 -0
- package/dist/__tests__/dfc0e156-plugin-file-with-a-slash.test.d.ts.map +1 -0
- package/dist/__tests__/dfc0e156-plugin-file-with-a-slash.test.js +69 -0
- package/dist/__tests__/dfc0e156-plugin-file-with-a-slash.test.js.map +1 -0
- package/dist/__tests__/fb4ec63e-update-advice-matches-the-install.test.d.ts +15 -0
- package/dist/__tests__/fb4ec63e-update-advice-matches-the-install.test.d.ts.map +1 -0
- package/dist/__tests__/fb4ec63e-update-advice-matches-the-install.test.js +61 -0
- package/dist/__tests__/fb4ec63e-update-advice-matches-the-install.test.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +57 -0
- package/dist/config.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +76 -16
- package/dist/server.js.map +1 -1
- package/dist/wordpress-client.d.ts +92 -0
- package/dist/wordpress-client.d.ts.map +1 -1
- package/dist/wordpress-client.js +311 -22
- package/dist/wordpress-client.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,61 @@ 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.3.26] - 2026-09-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **diagnose_connection says when the host answers /.well-known/ itself.** Some hosts serve
|
|
13
|
+
everything under `/.well-known/` before WordPress runs, so the OAuth discovery document
|
|
14
|
+
ChatGPT looks for comes back as the host's own 404 while the plugin is fine. The diagnostic
|
|
15
|
+
fetches both that address and the plugin's own route, compares them, and when the host is
|
|
16
|
+
answering first it says so with both status codes, the title of the page that came back, and
|
|
17
|
+
the one line to ask the host for.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Duplicating a post takes the id names agents already use.** `create_post_duplicate` read
|
|
22
|
+
only `original_id`, so a call with `post_id` or `page_id` sent WordPress the literal word
|
|
23
|
+
"undefined" in the path and came back a 404. It now accepts all three and refuses a missing
|
|
24
|
+
id before anything leaves your machine, the same way the page tool already did in 8.3.25.
|
|
25
|
+
- **A missing route stops blaming your plugin version.** When a Respira route 404s on a site
|
|
26
|
+
whose namespace is live, the message said the installed plugin is older than the tool and
|
|
27
|
+
sent you to wp-admin. On a site running 8.9.1 with no update offered anywhere that advice
|
|
28
|
+
was wrong three times across two sessions, while the real cause, an unusable id in the
|
|
29
|
+
request path, was already printed in the same sentence. The message now names that segment
|
|
30
|
+
when it is there, and says the plugin is behind only when the plugin's own declared MCP
|
|
31
|
+
ceiling is below this build.
|
|
32
|
+
- **Update advice matches the install that is answering.** On a Claude Desktop `.mcpb` bundle
|
|
33
|
+
every response printed `npm i -g @respira/wordpress-mcp-server@latest`. Running it installs
|
|
34
|
+
a second copy Claude Desktop never loads: the version does not change, and nothing says so.
|
|
35
|
+
A bundle install is told to update the extension in Claude Desktop, an npx run to restart
|
|
36
|
+
and clear its cache, a project install to update that project, a source checkout to rebuild,
|
|
37
|
+
and only a genuine global install gets the global npm command.
|
|
38
|
+
- **A 403 from a web server now carries its evidence.** The answer used to be a ranked list of
|
|
39
|
+
likely causes, which read as a diagnosis and sent at least one person through Cloudflare
|
|
40
|
+
rules that were not the cause. It now reports the verb and address, how many bytes the body
|
|
41
|
+
carried, the headers that name the layer, and that the response has none of the plugin's own
|
|
42
|
+
headers, which proves WordPress never ran and nothing was written. Uploads already leave as
|
|
43
|
+
multipart form-data, so when the block is by size the message says to quote that size to the
|
|
44
|
+
host along with the exact request.
|
|
45
|
+
- **One element can be read on a page too big to extract.** `find_element` now declares the
|
|
46
|
+
`include` parameter its own answer tells you to pass, so `include: "content"` returns the
|
|
47
|
+
element body the builder stored instead of a one-line preview. A truncated answer also says
|
|
48
|
+
how many bytes were cut and which parts are missing.
|
|
49
|
+
- **A plugin whose file contains a slash can be deactivated.** WordPress names a plugin by its
|
|
50
|
+
file, `WP PROTECTION/SECURITY.php`, and a slash cannot travel inside one URL path segment.
|
|
51
|
+
The folder now goes in the path and the exact file in the body, so activate, deactivate,
|
|
52
|
+
update and delete reach the plugin the site actually lists. The body field needs plugin
|
|
53
|
+
9.0.2; on older plugins the folder alone still resolves.
|
|
54
|
+
|
|
55
|
+
### Documentation
|
|
56
|
+
|
|
57
|
+
- **The block libraries are documented as what they are.** Spectra, Kadence Blocks,
|
|
58
|
+
GenerateBlocks and GreenShift each have their own adapter, detection, block namespace and
|
|
59
|
+
module list, and read and write through the Gutenberg block path. Their schemas come from
|
|
60
|
+
the live block registry on your site rather than a Respira-authored pack, which is why
|
|
61
|
+
`intelligence_package` reads `gutenberg` for all four.
|
|
62
|
+
|
|
8
63
|
## [8.3.25] - 2026-09-14
|
|
9
64
|
|
|
10
65
|
### Fixed
|
package/README.md
CHANGED
|
@@ -377,6 +377,23 @@ respira_add_image({ post_id: 42, image_url: "https://..." })
|
|
|
377
377
|
| **Thrive Architect** | Basic | Tree utility | Best-effort | — |
|
|
378
378
|
| **Visual Composer** | Basic | Tree utility | Best-effort | — |
|
|
379
379
|
|
|
380
|
+
### The Gutenberg-native block libraries
|
|
381
|
+
|
|
382
|
+
Spectra, Kadence Blocks, GenerateBlocks and GreenShift store ordinary WordPress
|
|
383
|
+
blocks in `post_content`. Each has its own adapter in the plugin, with its own
|
|
384
|
+
detection, its own block namespace (`kadence/*`, `uagb/*`, `generateblocks/*`,
|
|
385
|
+
`greenshift-blocks/*`) and its own module list, and reads and writes go through
|
|
386
|
+
the Gutenberg parse and serialize path. Element operations, `build_page` and
|
|
387
|
+
snapshots all work on them.
|
|
388
|
+
|
|
389
|
+
What they do not have is a Respira-authored schema pack of their own, and they
|
|
390
|
+
do not need one: the Gutenberg intelligence package reads the live
|
|
391
|
+
`WP_Block_Type_Registry` on your site, so it already enumerates their blocks
|
|
392
|
+
and attributes, including any block the library added after this build shipped.
|
|
393
|
+
`respira_get_builder_info` reports this in `intelligence_package`, which reads
|
|
394
|
+
`gutenberg` for all four. `intelligence_available: false` for one of them means
|
|
395
|
+
the site is running a plugin older than 8.2.1.
|
|
396
|
+
|
|
380
397
|
---
|
|
381
398
|
|
|
382
399
|
## All Tools
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"087103bc-the-host-answers-well-known-first.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/087103bc-the-host-answers-well-known-first.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket 087103bc: the host answers /.well-known/ itself, before WordPress.
|
|
3
|
+
*
|
|
4
|
+
* Measured on an nginx SiteGround site: /.well-known/oauth-protected-resource
|
|
5
|
+
* returned the host's own 404 page, titled "404 - Not found", with nothing
|
|
6
|
+
* WordPress in it, while /wp-json/respira/v1/oauth-protected-resource returned
|
|
7
|
+
* 200 JSON. ChatGPT only looks at the /.well-known/ address, so the connection
|
|
8
|
+
* failed while the plugin was fine, and nothing inside WordPress could see it.
|
|
9
|
+
*/
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import test from 'node:test';
|
|
12
|
+
import { wellKnownOwnership } from '../wordpress-client.js';
|
|
13
|
+
const hostFourOhFour = {
|
|
14
|
+
label: 'well_known_oauth_protected_resource',
|
|
15
|
+
url: 'https://www.example.com/.well-known/oauth-protected-resource',
|
|
16
|
+
status: 404,
|
|
17
|
+
looks_like_html: true,
|
|
18
|
+
title_text: '404 - Not found',
|
|
19
|
+
body_snippet: '<html><head><title>404 - Not found</title></head><body>404 - Not found</body></html>',
|
|
20
|
+
headers: { server: 'nginx', link: null },
|
|
21
|
+
};
|
|
22
|
+
const pluginRoute = {
|
|
23
|
+
label: 'respira_oauth_protected_resource',
|
|
24
|
+
url: 'https://www.example.com/wp-json/respira/v1/oauth-protected-resource',
|
|
25
|
+
status: 200,
|
|
26
|
+
looks_like_html: false,
|
|
27
|
+
title_text: null,
|
|
28
|
+
body_snippet: '{"resource":"https://www.example.com/wp-json/respira/v1/mcp","authorization_servers":["https://www.respira.press"]}',
|
|
29
|
+
headers: {},
|
|
30
|
+
};
|
|
31
|
+
test('a host 404 on /.well-known/ next to a working plugin route is reported as the host answering first', () => {
|
|
32
|
+
const result = wellKnownOwnership(hostFourOhFour, pluginRoute);
|
|
33
|
+
assert.equal(result.verdict, 'host_answers_first');
|
|
34
|
+
assert.equal(result.evidence.well_known_status, 404);
|
|
35
|
+
assert.equal(result.evidence.rest_mirror_status, 200);
|
|
36
|
+
assert.equal(result.evidence.well_known_looks_like_wordpress, false);
|
|
37
|
+
const said = String(result.recommendation);
|
|
38
|
+
assert.match(said, /returned 404/);
|
|
39
|
+
assert.match(said, /404 - Not found/);
|
|
40
|
+
assert.match(said, /no WordPress marker/);
|
|
41
|
+
assert.match(said, /returned 200 as WordPress/);
|
|
42
|
+
assert.match(said, /except \/\.well-known\/acme-challenge\//);
|
|
43
|
+
});
|
|
44
|
+
test('WordPress serving the metadata itself needs no advice', () => {
|
|
45
|
+
const served = { ...hostFourOhFour, status: 200, title_text: null, body_snippet: pluginRoute.body_snippet };
|
|
46
|
+
const result = wellKnownOwnership(served, pluginRoute);
|
|
47
|
+
assert.equal(result.verdict, 'wordpress_serves_it');
|
|
48
|
+
assert.equal(result.recommendation, null);
|
|
49
|
+
assert.equal(result.evidence.well_known_looks_like_wordpress, true);
|
|
50
|
+
});
|
|
51
|
+
test("a theme's own 404 at that address is WordPress, not the host", () => {
|
|
52
|
+
const themeFourOhFour = {
|
|
53
|
+
...hostFourOhFour,
|
|
54
|
+
title_text: 'Page not found - Example',
|
|
55
|
+
body_snippet: '<html><head><link rel="https://api.w.org/" href="https://www.example.com/wp-json/"></head></html>',
|
|
56
|
+
};
|
|
57
|
+
const result = wellKnownOwnership(themeFourOhFour, pluginRoute);
|
|
58
|
+
assert.equal(result.verdict, 'wordpress_serves_it');
|
|
59
|
+
assert.equal(result.recommendation, null);
|
|
60
|
+
});
|
|
61
|
+
test('a site where neither address answers is not blamed on the host', () => {
|
|
62
|
+
const result = wellKnownOwnership(hostFourOhFour, { ...pluginRoute, status: 503 });
|
|
63
|
+
assert.equal(result.verdict, 'both_unreachable');
|
|
64
|
+
assert.equal(result.recommendation, null);
|
|
65
|
+
assert.match(String(result.evidence.note), /the plugin route did not answer either/);
|
|
66
|
+
});
|
|
67
|
+
test('a probe that never came back says so instead of guessing', () => {
|
|
68
|
+
const dropped = { label: 'well_known_oauth_protected_resource', url: 'x', error: 'ETIMEDOUT' };
|
|
69
|
+
const result = wellKnownOwnership(dropped, pluginRoute);
|
|
70
|
+
assert.equal(result.verdict, 'unknown');
|
|
71
|
+
assert.equal(result.recommendation, null);
|
|
72
|
+
});
|
|
73
|
+
//# sourceMappingURL=087103bc-the-host-answers-well-known-first.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"087103bc-the-host-answers-well-known-first.test.js","sourceRoot":"","sources":["../../src/__tests__/087103bc-the-host-answers-well-known-first.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"685d8023-a-403-from-the-web-server-shows-its-evidence.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/685d8023-a-403-from-the-web-server-shows-its-evidence.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket 685d8023: uploads over roughly 500 bytes were refused with a 403 on
|
|
3
|
+
* one site while a 250-byte upload succeeded. The connector answered with a
|
|
4
|
+
* ranked list of likely causes, which read as a diagnosis and was not one.
|
|
5
|
+
*
|
|
6
|
+
* The upload path already sends multipart/form-data, not a base64 JSON body,
|
|
7
|
+
* so there is no plainer request shape left to try from this side. What the
|
|
8
|
+
* answer can do is carry the proof: the verb, the path, the size of the body,
|
|
9
|
+
* the headers that name the layer, and the absence of the header the plugin
|
|
10
|
+
* stamps on every REST response it serves.
|
|
11
|
+
*/
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import test from 'node:test';
|
|
14
|
+
import FormData from 'form-data';
|
|
15
|
+
import axios from 'axios';
|
|
16
|
+
import { nonWordPressBlockEvidence, nonWordPressBlockAsk, WordPressClient, } from '../wordpress-client.js';
|
|
17
|
+
const blocked403 = {
|
|
18
|
+
config: {
|
|
19
|
+
method: 'post',
|
|
20
|
+
url: '/media/upload',
|
|
21
|
+
baseURL: 'https://www.example.com/wp-json/respira/v1',
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'multipart/form-data; boundary=--------x',
|
|
24
|
+
'Content-Length': '36412',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
response: {
|
|
28
|
+
status: 403,
|
|
29
|
+
headers: { server: 'nginx', 'cf-ray': '8d2f1a2b3c4d5e6f-OTP' },
|
|
30
|
+
data: '<html><head><title>403 Forbidden</title></head></html>',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
test('a web-server 403 reports the request and the headers, not a ranked guess', () => {
|
|
34
|
+
const evidence = nonWordPressBlockEvidence(blocked403);
|
|
35
|
+
assert.match(evidence, /POST \/media\/upload/);
|
|
36
|
+
assert.match(evidence, /36,412 byte body/);
|
|
37
|
+
assert.match(evidence, /multipart\/form-data/);
|
|
38
|
+
assert.match(evidence, /no X-Respira-Plugin-Version header/);
|
|
39
|
+
assert.match(evidence, /nothing was written/);
|
|
40
|
+
assert.match(evidence, /server: nginx/);
|
|
41
|
+
assert.match(evidence, /cf-ray: 8d2f1a2b3c4d5e6f-OTP/);
|
|
42
|
+
});
|
|
43
|
+
test('the answer says what to ask the host, with the request in it', () => {
|
|
44
|
+
const ask = nonWordPressBlockAsk(blocked403);
|
|
45
|
+
assert.match(ask, /POST to https:\/\/www\.example\.com\/wp-json\/respira\/v1\/media\/upload/);
|
|
46
|
+
assert.match(ask, /ModSecurity audit log/);
|
|
47
|
+
assert.match(ask, /rule id/);
|
|
48
|
+
});
|
|
49
|
+
test('a layer that names itself in no header is described as exactly that', () => {
|
|
50
|
+
const evidence = nonWordPressBlockEvidence({
|
|
51
|
+
config: { method: 'get', url: '/pages' },
|
|
52
|
+
response: { status: 403, headers: {}, data: '<html></html>' },
|
|
53
|
+
});
|
|
54
|
+
assert.match(evidence, /did not identify itself/);
|
|
55
|
+
assert.match(evidence, /no body/);
|
|
56
|
+
});
|
|
57
|
+
test('when WordPress did answer, the evidence says so instead', () => {
|
|
58
|
+
const evidence = nonWordPressBlockEvidence({
|
|
59
|
+
config: { method: 'post', url: '/media/upload' },
|
|
60
|
+
response: { status: 403, headers: { 'x-respira-plugin-version': '9.0.1' }, data: '' },
|
|
61
|
+
});
|
|
62
|
+
assert.match(evidence, /X-Respira-Plugin-Version: 9\.0\.1/);
|
|
63
|
+
assert.match(evidence, /WordPress did answer/);
|
|
64
|
+
});
|
|
65
|
+
test('the upload leaves as multipart form-data, not as a base64 JSON body', async () => {
|
|
66
|
+
const saved = axios.defaults.adapter;
|
|
67
|
+
const seen = [];
|
|
68
|
+
try {
|
|
69
|
+
axios.defaults.adapter = ((config) => {
|
|
70
|
+
const contentType = String(config.headers?.['Content-Type'] || config.headers?.['content-type'] || '');
|
|
71
|
+
const body = config.data;
|
|
72
|
+
seen.push({
|
|
73
|
+
url: config.url || '',
|
|
74
|
+
contentType,
|
|
75
|
+
isMultipart: Buffer.isBuffer(body) && body.toString('utf8', 0, 200).includes('Content-Disposition: form-data'),
|
|
76
|
+
});
|
|
77
|
+
return Promise.resolve({
|
|
78
|
+
status: 200,
|
|
79
|
+
statusText: 'OK',
|
|
80
|
+
headers: { 'content-type': 'application/json' },
|
|
81
|
+
config,
|
|
82
|
+
data: { media: { id: 1 } },
|
|
83
|
+
request: {},
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
const client = new WordPressClient({
|
|
87
|
+
id: 'upload-shape-test',
|
|
88
|
+
name: 'Upload Shape Test',
|
|
89
|
+
url: 'https://example.com',
|
|
90
|
+
apiKey: 'test-key',
|
|
91
|
+
});
|
|
92
|
+
// A 3 KB payload, above the size that failed on the reporting site.
|
|
93
|
+
const payload = Buffer.alloc(3072, 7).toString('base64');
|
|
94
|
+
await client.uploadMedia(payload, 'featured.webp', 'image/webp');
|
|
95
|
+
assert.equal(seen.length, 1);
|
|
96
|
+
assert.equal(seen[0].url, '/media/upload');
|
|
97
|
+
assert.match(seen[0].contentType, /^multipart\/form-data/);
|
|
98
|
+
assert.equal(seen[0].isMultipart, true);
|
|
99
|
+
// FormData is what builds it, so this pins the dependency the shape rests on.
|
|
100
|
+
assert.equal(typeof new FormData().getBuffer, 'function');
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
axios.defaults.adapter = saved;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
//# sourceMappingURL=685d8023-a-403-from-the-web-server-shows-its-evidence.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"685d8023-a-403-from-the-web-server-shows-its-evidence.test.js","sourceRoot":"","sources":["../../src/__tests__/685d8023-a-403-from-the-web-server-shows-its-evidence.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,QAAQ,MAAM,WAAW,CAAC;AACjC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EACL,yBAAyB,EACzB,oBAAoB,EACpB,eAAe,GAChB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,UAAU,GAAG;IACjB,MAAM,EAAE;QACN,MAAM,EAAE,MAAM;QACd,GAAG,EAAE,eAAe;QACpB,OAAO,EAAE,4CAA4C;QACrD,OAAO,EAAE;YACP,cAAc,EAAE,yCAAyC;YACzD,gBAAgB,EAAE,OAAO;SAC1B;KACF;IACD,QAAQ,EAAE;QACR,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,sBAAsB,EAAE;QAC9D,IAAI,EAAE,wDAAwD;KAC/D;CACF,CAAC;AAEF,IAAI,CAAC,0EAA0E,EAAE,GAAG,EAAE;IACpF,MAAM,QAAQ,GAAG,yBAAyB,CAAC,UAAU,CAAC,CAAC;IAEvD,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,sBAAsB,CAAC,CAAC;IAC/C,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;IAC3C,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,sBAAsB,CAAC,CAAC;IAC/C,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,oCAAoC,CAAC,CAAC;IAC7D,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,qBAAqB,CAAC,CAAC;IAC9C,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;IACxC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,8BAA8B,CAAC,CAAC;AACzD,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,8DAA8D,EAAE,GAAG,EAAE;IACxE,MAAM,GAAG,GAAG,oBAAoB,CAAC,UAAU,CAAC,CAAC;IAC7C,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,0EAA0E,CAAC,CAAC;IAC9F,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,uBAAuB,CAAC,CAAC;IAC3C,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;AAC/B,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,qEAAqE,EAAE,GAAG,EAAE;IAC/E,MAAM,QAAQ,GAAG,yBAAyB,CAAC;QACzC,MAAM,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE;QACxC,QAAQ,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE;KAC9D,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,yBAAyB,CAAC,CAAC;IAClD,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AACpC,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,yDAAyD,EAAE,GAAG,EAAE;IACnE,MAAM,QAAQ,GAAG,yBAAyB,CAAC;QACzC,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,EAAE;QAChD,QAAQ,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,0BAA0B,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;KACtF,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,mCAAmC,CAAC,CAAC;IAC5D,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,sBAAsB,CAAC,CAAC;AACjD,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,qEAAqE,EAAE,KAAK,IAAI,EAAE;IACrF,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC;IACrC,MAAM,IAAI,GAAsE,EAAE,CAAC;IAEnF,IAAI,CAAC;QACH,KAAK,CAAC,QAAQ,CAAC,OAAO,GAAG,CAAC,CAAC,MAAW,EAAE,EAAE;YACxC,MAAM,WAAW,GAAG,MAAM,CACxB,MAAM,CAAC,OAAO,EAAE,CAAC,cAAc,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,CAC3E,CAAC;YACF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;YACzB,IAAI,CAAC,IAAI,CAAC;gBACR,GAAG,EAAE,MAAM,CAAC,GAAG,IAAI,EAAE;gBACrB,WAAW;gBACX,WAAW,EACT,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,gCAAgC,CAAC;aACpG,CAAC,CAAC;YACH,OAAO,OAAO,CAAC,OAAO,CAAC;gBACrB,MAAM,EAAE,GAAG;gBACX,UAAU,EAAE,IAAI;gBAChB,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,MAAM;gBACN,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE;gBAC1B,OAAO,EAAE,EAAE;aACZ,CAAC,CAAC;QACL,CAAC,CAAQ,CAAC;QAEV,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;YACjC,EAAE,EAAE,mBAAmB;YACvB,IAAI,EAAE,mBAAmB;YACzB,GAAG,EAAE,qBAAqB;YAC1B,MAAM,EAAE,UAAU;SACZ,CAAC,CAAC;QAEV,oEAAoE;QACpE,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACzD,MAAM,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,CAAC,CAAC;QAEjE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC7B,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;QAC3C,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,uBAAuB,CAAC,CAAC;QAC3D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QACxC,8EAA8E;QAC9E,MAAM,CAAC,KAAK,CAAC,OAAO,IAAI,QAAQ,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAC5D,CAAC;YAAS,CAAC;QACT,KAAK,CAAC,QAAQ,CAAC,OAAO,GAAG,KAAK,CAAC;IACjC,CAAC;AACH,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Second half of ticket 81c478df.
|
|
3
|
+
*
|
|
4
|
+
* The id-alias fix stopped `create_page_duplicate` from sending
|
|
5
|
+
* /pages/undefined/duplicate. This covers what the operator was TOLD while it
|
|
6
|
+
* did: every rest_no_route under a registered namespace was reported as "the
|
|
7
|
+
* installed plugin is older than this tool. Update Respira in wp-admin under
|
|
8
|
+
* Plugins." The reporting site ran 8.9.1 with no update available anywhere,
|
|
9
|
+
* so the advice sent them to wp-admin three times across two sessions for an
|
|
10
|
+
* update that did not exist.
|
|
11
|
+
*
|
|
12
|
+
* Two facts were available and unused: the unusable path segment sitting in
|
|
13
|
+
* the message already, and the plugin's own declared max_mcp_version.
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=81c478df-rest-no-route-blames-the-plugin.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"81c478df-rest-no-route-blames-the-plugin.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/81c478df-rest-no-route-blames-the-plugin.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Second half of ticket 81c478df.
|
|
3
|
+
*
|
|
4
|
+
* The id-alias fix stopped `create_page_duplicate` from sending
|
|
5
|
+
* /pages/undefined/duplicate. This covers what the operator was TOLD while it
|
|
6
|
+
* did: every rest_no_route under a registered namespace was reported as "the
|
|
7
|
+
* installed plugin is older than this tool. Update Respira in wp-admin under
|
|
8
|
+
* Plugins." The reporting site ran 8.9.1 with no update available anywhere,
|
|
9
|
+
* so the advice sent them to wp-admin three times across two sessions for an
|
|
10
|
+
* update that did not exist.
|
|
11
|
+
*
|
|
12
|
+
* Two facts were available and unused: the unusable path segment sitting in
|
|
13
|
+
* the message already, and the plugin's own declared max_mcp_version.
|
|
14
|
+
*/
|
|
15
|
+
import { test, after } from 'node:test';
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import http from 'node:http';
|
|
18
|
+
import { WordPressClient } from '../wordpress-client.js';
|
|
19
|
+
const servers = [];
|
|
20
|
+
after(() => { for (const s of servers)
|
|
21
|
+
s.close(); });
|
|
22
|
+
/**
|
|
23
|
+
* A site with the Respira namespace live and the compatibility endpoint
|
|
24
|
+
* answering, where every other route is missing.
|
|
25
|
+
*/
|
|
26
|
+
async function startSite(compatibility) {
|
|
27
|
+
const server = http.createServer((req, res) => {
|
|
28
|
+
const url = req.url || '';
|
|
29
|
+
if (url.startsWith('/wp-json/respira/v2/server/compatibility') && compatibility) {
|
|
30
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
31
|
+
res.end(JSON.stringify(compatibility));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// The namespace index: proof the plugin is active and REST routing works.
|
|
35
|
+
if (url === '/wp-json/respira/v2' || url === '/wp-json/respira/v2/') {
|
|
36
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
37
|
+
res.end(JSON.stringify({ namespace: 'respira/v2', routes: {} }));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
41
|
+
res.end(JSON.stringify({
|
|
42
|
+
code: 'rest_no_route',
|
|
43
|
+
message: 'No route was found matching the URL and request method.',
|
|
44
|
+
data: { status: 404 },
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
servers.push(server);
|
|
48
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
49
|
+
const { port } = server.address();
|
|
50
|
+
const client = new WordPressClient({
|
|
51
|
+
id: 's',
|
|
52
|
+
name: 's',
|
|
53
|
+
url: `http://127.0.0.1:${port}`,
|
|
54
|
+
apiKey: `respira_site_${'x'.repeat(24)}`,
|
|
55
|
+
});
|
|
56
|
+
// Skip version negotiation: the route under test is a v2 route.
|
|
57
|
+
client.negotiatedApiVersion = 'v2';
|
|
58
|
+
return client;
|
|
59
|
+
}
|
|
60
|
+
async function messageFor(client, id) {
|
|
61
|
+
try {
|
|
62
|
+
await client.duplicatePage(id);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
return String(err?.message ?? err);
|
|
66
|
+
}
|
|
67
|
+
throw new Error('the duplicate call was expected to fail');
|
|
68
|
+
}
|
|
69
|
+
test('an "undefined" id in the path is named, not blamed on the plugin version', async () => {
|
|
70
|
+
const client = await startSite({ plugin_version: '8.9.1', min_mcp_version: '8.0.0' });
|
|
71
|
+
const message = await messageFor(client, undefined);
|
|
72
|
+
assert.match(message, /\[attempted: POST .*\/pages\/undefined\/duplicate\]/);
|
|
73
|
+
assert.match(message, /carried "undefined" where the route expects an id/);
|
|
74
|
+
assert.doesNotMatch(message, /older than this tool/, 'a malformed request path says nothing about the plugin version');
|
|
75
|
+
assert.doesNotMatch(message, /Update Respira in wp-admin/);
|
|
76
|
+
});
|
|
77
|
+
test('a plugin that does not declare itself behind is not called out of date', async () => {
|
|
78
|
+
// 8.9.1 with no max_mcp_version: the plugin publishes no ceiling, so there
|
|
79
|
+
// is no evidence it predates this tool.
|
|
80
|
+
const client = await startSite({ plugin_version: '8.9.1', min_mcp_version: '8.0.0' });
|
|
81
|
+
const message = await messageFor(client, 200);
|
|
82
|
+
assert.match(message, /\[attempted: POST .*\/pages\/200\/duplicate\]/);
|
|
83
|
+
assert.match(message, /The Respira namespace IS registered/);
|
|
84
|
+
assert.match(message, /v8\.9\.1/, 'name the version actually installed');
|
|
85
|
+
assert.match(message, /does not report itself as older than this tool/);
|
|
86
|
+
assert.doesNotMatch(message, /Update Respira in wp-admin/);
|
|
87
|
+
});
|
|
88
|
+
test('a plugin whose declared ceiling is below this build is told to update', async () => {
|
|
89
|
+
const client = await startSite({
|
|
90
|
+
plugin_version: '6.2.0',
|
|
91
|
+
min_mcp_version: '6.0.0',
|
|
92
|
+
max_mcp_version: '6.2.0',
|
|
93
|
+
});
|
|
94
|
+
const message = await messageFor(client, 200);
|
|
95
|
+
assert.match(message, /supports Respira MCP up to v6\.2\.0/);
|
|
96
|
+
assert.match(message, /Update Respira in wp-admin under Plugins/);
|
|
97
|
+
});
|
|
98
|
+
test('an unreadable compatibility endpoint still does not invent a verdict', async () => {
|
|
99
|
+
const client = await startSite(null);
|
|
100
|
+
const message = await messageFor(client, 200);
|
|
101
|
+
assert.match(message, /installed version unknown/);
|
|
102
|
+
assert.doesNotMatch(message, /Update Respira in wp-admin/);
|
|
103
|
+
assert.match(message, /respira_diagnose_connection/);
|
|
104
|
+
});
|
|
105
|
+
//# sourceMappingURL=81c478df-rest-no-route-blames-the-plugin.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"81c478df-rest-no-route-blames-the-plugin.test.js","sourceRoot":"","sources":["../../src/__tests__/81c478df-rest-no-route-blames-the-plugin.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dfc0e156-plugin-file-with-a-slash.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/dfc0e156-plugin-file-with-a-slash.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { WordPressClient, pluginRouteTarget } from '../wordpress-client.js';
|
|
5
|
+
test('a plugin file keeps its folder in the path and its file in the body', () => {
|
|
6
|
+
assert.deepEqual(pluginRouteTarget('WP PROTECTION/SECURITY.php'), {
|
|
7
|
+
segment: 'WP PROTECTION',
|
|
8
|
+
body: { plugin_file: 'WP PROTECTION/SECURITY.php' },
|
|
9
|
+
});
|
|
10
|
+
// A folder name, and a single-file plugin at the plugins root, carry no
|
|
11
|
+
// slash and are sent exactly as before.
|
|
12
|
+
assert.deepEqual(pluginRouteTarget('WP PROTECTION'), { segment: 'WP PROTECTION', body: {} });
|
|
13
|
+
assert.deepEqual(pluginRouteTarget('hello.php'), { segment: 'hello.php', body: {} });
|
|
14
|
+
assert.deepEqual(pluginRouteTarget(' /akismet/akismet.php '), {
|
|
15
|
+
segment: 'akismet',
|
|
16
|
+
body: { plugin_file: 'akismet/akismet.php' },
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
test('deactivating a plugin whose folder has a space sends one path segment and the exact file', async () => {
|
|
20
|
+
const saved = axios.defaults.adapter;
|
|
21
|
+
const seen = [];
|
|
22
|
+
try {
|
|
23
|
+
axios.defaults.adapter = ((config) => {
|
|
24
|
+
seen.push({
|
|
25
|
+
method: config.method || '',
|
|
26
|
+
url: config.url || '',
|
|
27
|
+
data: typeof config.data === 'string' ? JSON.parse(config.data) : config.data,
|
|
28
|
+
});
|
|
29
|
+
return Promise.resolve({
|
|
30
|
+
status: 200,
|
|
31
|
+
statusText: 'OK',
|
|
32
|
+
headers: { 'content-type': 'application/json' },
|
|
33
|
+
config,
|
|
34
|
+
data: { success: true, message: 'Plugin deactivated successfully' },
|
|
35
|
+
request: {},
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
const client = new WordPressClient({
|
|
39
|
+
id: 'plugin-slash-test',
|
|
40
|
+
name: 'Plugin Slash Test',
|
|
41
|
+
url: 'https://example.com',
|
|
42
|
+
apiKey: 'test-key',
|
|
43
|
+
});
|
|
44
|
+
await client.deactivatePlugin('WP PROTECTION/SECURITY.php');
|
|
45
|
+
await client.activatePlugin('WP PROTECTION/SECURITY.php');
|
|
46
|
+
await client.updatePlugin('WP PROTECTION/SECURITY.php', 'token-123');
|
|
47
|
+
await client.deletePlugin('WP PROTECTION/SECURITY.php');
|
|
48
|
+
// No %2F anywhere: that is what both attempts in the report sent, and it
|
|
49
|
+
// never reached the route.
|
|
50
|
+
assert.equal(seen.some((r) => r.url.toUpperCase().includes('%2F')), false);
|
|
51
|
+
assert.deepEqual(seen.map((r) => [r.method, r.url]), [
|
|
52
|
+
['post', '/plugins/WP%20PROTECTION/deactivate'],
|
|
53
|
+
['post', '/plugins/WP%20PROTECTION/activate'],
|
|
54
|
+
['post', '/plugins/WP%20PROTECTION/update'],
|
|
55
|
+
['delete', '/plugins/WP%20PROTECTION'],
|
|
56
|
+
]);
|
|
57
|
+
assert.deepEqual(seen[0].data, { plugin_file: 'WP PROTECTION/SECURITY.php' });
|
|
58
|
+
assert.deepEqual(seen[1].data, { plugin_file: 'WP PROTECTION/SECURITY.php' });
|
|
59
|
+
assert.deepEqual(seen[2].data, {
|
|
60
|
+
plugin_file: 'WP PROTECTION/SECURITY.php',
|
|
61
|
+
approval_token: 'token-123',
|
|
62
|
+
});
|
|
63
|
+
assert.deepEqual(seen[3].data, { plugin_file: 'WP PROTECTION/SECURITY.php' });
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
axios.defaults.adapter = saved;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=dfc0e156-plugin-file-with-a-slash.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dfc0e156-plugin-file-with-a-slash.test.js","sourceRoot":"","sources":["../../src/__tests__/dfc0e156-plugin-file-with-a-slash.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for ticket fb4ec63e: the update block printed
|
|
3
|
+
* `npm i -g @respira/wordpress-mcp-server@latest` on every install shape.
|
|
4
|
+
*
|
|
5
|
+
* On a Claude Desktop .mcpb bundle that command succeeds, installs a second
|
|
6
|
+
* copy the client never loads, and leaves the version unchanged with no error
|
|
7
|
+
* to say so. The reporter kept a private note reading "do not run the command
|
|
8
|
+
* Respira prints".
|
|
9
|
+
*
|
|
10
|
+
* describeTransportMode already classified the install correctly in the same
|
|
11
|
+
* session. describeUpdateRoute reads the same signal, so the advice and the
|
|
12
|
+
* diagnostic can no longer disagree.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=fb4ec63e-update-advice-matches-the-install.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fb4ec63e-update-advice-matches-the-install.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/fb4ec63e-update-advice-matches-the-install.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for ticket fb4ec63e: the update block printed
|
|
3
|
+
* `npm i -g @respira/wordpress-mcp-server@latest` on every install shape.
|
|
4
|
+
*
|
|
5
|
+
* On a Claude Desktop .mcpb bundle that command succeeds, installs a second
|
|
6
|
+
* copy the client never loads, and leaves the version unchanged with no error
|
|
7
|
+
* to say so. The reporter kept a private note reading "do not run the command
|
|
8
|
+
* Respira prints".
|
|
9
|
+
*
|
|
10
|
+
* describeTransportMode already classified the install correctly in the same
|
|
11
|
+
* session. describeUpdateRoute reads the same signal, so the advice and the
|
|
12
|
+
* diagnostic can no longer disagree.
|
|
13
|
+
*/
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { describeUpdateRoute, describeTransportMode } from '../config.js';
|
|
17
|
+
const BUNDLE_PATH = '/Users/someone/Library/Application Support/Claude/Claude Extensions/local.mcpb.mihai-dragomirescu.respira-wordpress/server/index.js';
|
|
18
|
+
test('a .mcpb bundle is never told to run npm', () => {
|
|
19
|
+
const route = describeUpdateRoute({
|
|
20
|
+
binaryPath: BUNDLE_PATH,
|
|
21
|
+
env: { RESPIRA_CONNECTION_SOURCE: 'claude_desktop_mcpb' },
|
|
22
|
+
});
|
|
23
|
+
assert.equal(route.transport_mode, 'mcpb_bundle');
|
|
24
|
+
assert.equal(route.upgrade_command, undefined, 'no shell command updates a bundle install');
|
|
25
|
+
assert.doesNotMatch(route.how_to_update, /npm i -g/);
|
|
26
|
+
assert.doesNotMatch(String(route.rerun_setup_command), /npx /);
|
|
27
|
+
assert.match(route.how_to_update, /Claude Desktop/);
|
|
28
|
+
assert.match(route.how_to_update, /Extensions/);
|
|
29
|
+
});
|
|
30
|
+
test('only a genuine global install gets the global npm command', () => {
|
|
31
|
+
const route = describeUpdateRoute({
|
|
32
|
+
binaryPath: '/opt/homebrew/lib/node_modules/@respira/wordpress-mcp-server/dist/index.js',
|
|
33
|
+
env: {},
|
|
34
|
+
});
|
|
35
|
+
assert.equal(route.transport_mode, 'global_npm_install');
|
|
36
|
+
assert.equal(route.upgrade_command, 'npm i -g @respira/wordpress-mcp-server@latest');
|
|
37
|
+
});
|
|
38
|
+
test('every other install shape names its own update route', () => {
|
|
39
|
+
const cases = [
|
|
40
|
+
['/Users/someone/.npm/_npx/9f2/node_modules/.bin/wordpress-mcp-server', 'npx_cache'],
|
|
41
|
+
['/srv/app/node_modules/@respira/wordpress-mcp-server/dist/index.js', 'project_local_install'],
|
|
42
|
+
['/Users/someone/code/respira/mcp-server/dist/index.js', 'source_checkout'],
|
|
43
|
+
['/opt/weird/launcher.js', 'unknown'],
|
|
44
|
+
];
|
|
45
|
+
for (const [binaryPath, expectedMode] of cases) {
|
|
46
|
+
const route = describeUpdateRoute({ binaryPath, env: {} });
|
|
47
|
+
assert.equal(route.transport_mode, expectedMode, binaryPath);
|
|
48
|
+
assert.notEqual(route.upgrade_command, 'npm i -g @respira/wordpress-mcp-server@latest', `${expectedMode} must not be sent to a global install it does not use`);
|
|
49
|
+
assert.ok(route.how_to_update.length > 0, `${expectedMode} must say how to update`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
test('the update advice and respira_diagnose_connection read the same signal', () => {
|
|
53
|
+
const env = { RESPIRA_CONNECTION_SOURCE: 'claude_desktop_mcpb' };
|
|
54
|
+
const transport = describeTransportMode({ binaryPath: BUNDLE_PATH, env, processScanWorked: false });
|
|
55
|
+
const route = describeUpdateRoute({ binaryPath: BUNDLE_PATH, env });
|
|
56
|
+
assert.equal(route.transport_mode, transport.transport_mode);
|
|
57
|
+
assert.equal(route.install_kind, transport.label);
|
|
58
|
+
assert.equal(route.binary_path, transport.binary_path);
|
|
59
|
+
assert.equal(transport.is_global_install, false);
|
|
60
|
+
});
|
|
61
|
+
//# sourceMappingURL=fb4ec63e-update-advice-matches-the-install.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fb4ec63e-update-advice-matches-the-install.test.js","sourceRoot":"","sources":["../../src/__tests__/fb4ec63e-update-advice-matches-the-install.test.ts"],"names":[],"mappings":"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"}
|