@respira/wordpress-mcp-server 8.2.3 → 8.2.5
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 +19 -0
- package/dist/__tests__/config-bracketed-key.test.d.ts +2 -0
- package/dist/__tests__/config-bracketed-key.test.d.ts.map +1 -0
- package/dist/__tests__/config-bracketed-key.test.js +46 -0
- package/dist/__tests__/config-bracketed-key.test.js.map +1 -0
- package/dist/__tests__/config-placeholder.test.js +32 -5
- package/dist/__tests__/config-placeholder.test.js.map +1 -1
- package/dist/__tests__/invoke-ability-required-args.test.d.ts +2 -0
- package/dist/__tests__/invoke-ability-required-args.test.d.ts.map +1 -0
- package/dist/__tests__/invoke-ability-required-args.test.js +53 -0
- package/dist/__tests__/invoke-ability-required-args.test.js.map +1 -0
- package/dist/__tests__/oq-put-fallback-callRestV2-integration.test.d.ts +24 -0
- package/dist/__tests__/oq-put-fallback-callRestV2-integration.test.d.ts.map +1 -0
- package/dist/__tests__/oq-put-fallback-callRestV2-integration.test.js +135 -0
- package/dist/__tests__/oq-put-fallback-callRestV2-integration.test.js.map +1 -0
- package/dist/__tests__/transport-mode-observability.test.d.ts +2 -0
- package/dist/__tests__/transport-mode-observability.test.d.ts.map +1 -0
- package/dist/__tests__/transport-mode-observability.test.js +201 -0
- package/dist/__tests__/transport-mode-observability.test.js.map +1 -0
- package/dist/__tests__/woo-card-tools-forward-fse-params.test.js +2 -2
- package/dist/__tests__/woo-card-tools-forward-fse-params.test.js.map +1 -1
- package/dist/config.d.ts +118 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +267 -8
- package/dist/config.js.map +1 -1
- package/dist/server.d.ts +42 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +218 -24
- package/dist/server.js.map +1 -1
- package/dist/wordpress-client.d.ts +17 -0
- package/dist/wordpress-client.d.ts.map +1 -1
- package/dist/wordpress-client.js +43 -0
- package/dist/wordpress-client.js.map +1 -1
- package/package.json +1 -1
- package/skills/design-system-synthesizer/SKILL.md +30 -9
- package/skills/design-system-synthesizer/metadata.json +6 -3
- package/skills/figma-to-elementor/SKILL.md +14 -2
- package/skills/html-to-bricks/SKILL.md +16 -2
- package/skills/html-to-bricks/metadata.json +4 -3
- package/skills/migrate-beaver-builder-to-bricks/SKILL.md +20 -3
- package/skills/migrate-beaver-builder-to-gutenberg/SKILL.md +20 -3
- package/skills/migrate-brizy-to-gutenberg/SKILL.md +20 -3
- package/skills/migrate-divi-to-breakdance/SKILL.md +20 -3
- package/skills/migrate-divi-to-bricks/SKILL.md +20 -3
- package/skills/migrate-divi-to-gutenberg/SKILL.md +20 -3
- package/skills/migrate-elementor-to-breakdance/SKILL.md +21 -4
- package/skills/migrate-elementor-to-bricks/SKILL.md +21 -4
- package/skills/migrate-elementor-to-gutenberg/SKILL.md +21 -4
- package/skills/migrate-elementor-to-oxygen/SKILL.md +21 -4
- package/skills/migrate-oxygen-to-breakdance/SKILL.md +20 -3
- package/skills/migrate-oxygen-to-bricks/SKILL.md +20 -3
- package/skills/migrate-thrive-architect-to-gutenberg/SKILL.md +20 -3
- package/skills/migrate-visual-composer-to-gutenberg/SKILL.md +20 -3
- package/skills/migrate-wpbakery-to-bricks/SKILL.md +20 -3
- package/skills/migrate-wpbakery-to-gutenberg/SKILL.md +20 -3
- package/skills/prime-the-agent/SKILL.md +35 -13
- package/skills/prime-the-agent/metadata.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ 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.2.5] - 2026-08-09
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A partly-edited starter config was accepted as a real one, sending every request to `example.com`.** The first run writes a placeholder config carrying a dummy address AND a dummy key, and `isPlaceholderConfig()` tested only the key. Replace the key and leave the address, which is what people do, and the file stopped looking like a placeholder while still pointing at a site nobody owns. It was then served as a genuine setup: the server started cleanly, exposed every tool, reported no error, and sent all traffic to the wrong place. The reporter described it exactly: "connected, tools loaded, nothing flagged, just consistently the wrong site behind it." A site now counts as a placeholder when EITHER field is still the dummy, normalised for trailing slashes and case. A config mixing one real site with a leftover stub is still kept, and a hostname merely containing `example.com` is still a real site. Reported by R.M.
|
|
13
|
+
- **An API key pasted with the documentation brackets still around it is now rejected at startup.** `<respira_...>` is not a valid key, but it is also not the placeholder key, so it defeated the same check above and pinned the session to the dummy site with no error anywhere. It now throws on load and names the fix, matching how a masked key full of bullet characters is already handled. Both are values no real key can hold. This and the entry above were one mechanism behind two separate incidents a month apart for the same customer. Reported by R.M.
|
|
14
|
+
|
|
15
|
+
## [8.2.4] - 2026-08-09
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **A PUT request that a host blocks now retries as POST**, closing the gap for Elementor's global-colors and global-typography routes (403 on hosts like OVH that block PUT) and every other route built on the shared REST client in the same fix: Bricks global classes/theme styles/color palette/typography, FSE templates/patterns/navigation, design tokens, and the v1 custom post-type/taxonomy/ACF surface.
|
|
20
|
+
- **The four WooCommerce card tools' `edit_target` values now match the FSE tool family they actually route through** (`approval`/`live`, the same vocabulary `update_site_template` uses), instead of the page-edit tools' `live`/`duplicate`, which described a mode the FSE layer doesn't implement.
|
|
21
|
+
- **`redeem_token` now activates sites in the running session immediately.** A successful redeem wrote a config file and reported success, but the server kept using its boot-time site list until restarted, and under a sandboxed or read-only config source that restart never comes, so the write was silently a no-op. Sites are now merged into the live list on redeem, a first-ever redeem sets the active site so tools work without a follow-up call, and a malformed or masked key is reported with a reason instead of vanishing.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`diagnose_connection` reports the actual running transport**: bundle, npx cache, global npm install, project-local install, or source checkout, plus a flag when a differently-launched sibling Respira process is detected, closing the observability gap behind three separate "my agent is talking to the wrong site" reports that each turned out to have a different root cause.
|
|
26
|
+
|
|
8
27
|
## [8.2.3] - 2026-08-04
|
|
9
28
|
|
|
10
29
|
### Fixed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-bracketed-key.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/config-bracketed-key.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { validateApiKeyShape } from '../config.js';
|
|
4
|
+
/**
|
|
5
|
+
* A key pasted with the documentation brackets still around it.
|
|
6
|
+
*
|
|
7
|
+
* R.M. found this in his own Claude Desktop config after losing a day to it:
|
|
8
|
+
* "a misconfigured entry in the config file itself (wrong server name, stray
|
|
9
|
+
* angle brackets around the API key) that the UI never surfaced. I only found
|
|
10
|
+
* it by editing the JSON directly."
|
|
11
|
+
*
|
|
12
|
+
* It was worse than an invalid key. `<respira_abc…>` does not equal the
|
|
13
|
+
* placeholder key either, so it defeated isPlaceholderConfig and the session
|
|
14
|
+
* was pinned to the dummy site while reporting itself perfectly healthy. Two
|
|
15
|
+
* separate incidents of his turned out to be this one mechanism.
|
|
16
|
+
*
|
|
17
|
+
* Throwing matches the masked-bullets case in the same function: both are
|
|
18
|
+
* values that no real key can hold, so there is nothing to be tentative about.
|
|
19
|
+
*/
|
|
20
|
+
const REAL = 'respira_' + 'a'.repeat(36);
|
|
21
|
+
test('a key wrapped in angle brackets is rejected outright', () => {
|
|
22
|
+
assert.throws(() => validateApiKeyShape(`<${REAL}>`, 'my site'), /angle brackets/i, 'the brackets are documentation, not part of the key');
|
|
23
|
+
});
|
|
24
|
+
test('the error names the fix rather than just the fault', () => {
|
|
25
|
+
try {
|
|
26
|
+
validateApiKeyShape(`<${REAL}>`, 'my site');
|
|
27
|
+
assert.fail('should have thrown');
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const msg = String(err.message);
|
|
31
|
+
assert.match(msg, /Remove them/i);
|
|
32
|
+
assert.match(msg, /my site/, 'says which site, since a config can hold many');
|
|
33
|
+
// The reason this is worth throwing over: it used to end in the wrong site.
|
|
34
|
+
assert.match(msg, /placeholder site/i);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
test('a stray bracket on either side is caught, not just a matched pair', () => {
|
|
38
|
+
assert.throws(() => validateApiKeyShape(`<${REAL}`, 's'), /angle brackets/i);
|
|
39
|
+
assert.throws(() => validateApiKeyShape(`${REAL}>`, 's'), /angle brackets/i);
|
|
40
|
+
});
|
|
41
|
+
test('a clean key still passes untouched', () => {
|
|
42
|
+
assert.doesNotThrow(() => validateApiKeyShape(REAL, 's'));
|
|
43
|
+
assert.doesNotThrow(() => validateApiKeyShape('rsp_at_' + 'b'.repeat(40), 's'));
|
|
44
|
+
assert.doesNotThrow(() => validateApiKeyShape('respira_site_' + 'c'.repeat(32), 's'));
|
|
45
|
+
});
|
|
46
|
+
//# sourceMappingURL=config-bracketed-key.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-bracketed-key.test.js","sourceRoot":"","sources":["../../src/__tests__/config-bracketed-key.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,IAAI,GAAG,UAAU,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAEzC,IAAI,CAAC,sDAAsD,EAAE,GAAG,EAAE;IAChE,MAAM,CAAC,MAAM,CACX,GAAG,EAAE,CAAC,mBAAmB,CAAC,IAAI,IAAI,GAAG,EAAE,SAAS,CAAC,EACjD,iBAAiB,EACjB,qDAAqD,CACtD,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,oDAAoD,EAAE,GAAG,EAAE;IAC9D,IAAI,CAAC;QACH,mBAAmB,CAAC,IAAI,IAAI,GAAG,EAAE,SAAS,CAAC,CAAC;QAC5C,MAAM,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,MAAM,CAAE,GAAa,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QAClC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,SAAS,EAAE,+CAA+C,CAAC,CAAC;QAC9E,4EAA4E;QAC5E,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;IACzC,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,mEAAmE,EAAE,GAAG,EAAE;IAC7E,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,IAAI,IAAI,EAAE,EAAE,GAAG,CAAC,EAAE,iBAAiB,CAAC,CAAC;IAC7E,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,GAAG,IAAI,GAAG,EAAE,GAAG,CAAC,EAAE,iBAAiB,CAAC,CAAC;AAC/E,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAC9C,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1D,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAChF,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,eAAe,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;AACxF,CAAC,CAAC,CAAC"}
|
|
@@ -13,11 +13,38 @@ test('isPlaceholderConfig flags the first-run default file', () => {
|
|
|
13
13
|
test('isPlaceholderConfig does NOT flag a real config', () => {
|
|
14
14
|
assert.equal(isPlaceholderConfig({ sites: [realSite], preferences: prefs }), false);
|
|
15
15
|
});
|
|
16
|
-
test('isPlaceholderConfig
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
test('isPlaceholderConfig DOES flag example.com even when the key is real', () => {
|
|
17
|
+
// REVERSAL, on purpose. This test previously asserted the opposite, to avoid
|
|
18
|
+
// discarding "a legitimate example.com test config". That case cannot exist:
|
|
19
|
+
// example.com is IANA-reserved for documentation, so no customer WordPress
|
|
20
|
+
// install lives there and no real key authenticates against it.
|
|
21
|
+
//
|
|
22
|
+
// Meanwhile the case it permitted happens constantly. The starter file ships
|
|
23
|
+
// a dummy url AND a dummy key. A user pastes a real key and leaves the
|
|
24
|
+
// address, or pastes a key with the doc brackets still attached, and the key
|
|
25
|
+
// test stops matching. The config was then accepted as genuine: the server
|
|
26
|
+
// started cleanly, listed all 234 tools, reported no errors, and sent every
|
|
27
|
+
// request to example.com.
|
|
28
|
+
//
|
|
29
|
+
// R.M. lost hours to exactly this, twice, and nearly cancelled over it:
|
|
30
|
+
// "connected, tools loaded, nothing flagged, just consistently the wrong site
|
|
31
|
+
// behind it." The asymmetry decides it. A false positive here tells a
|
|
32
|
+
// hypothetical developer to fix a config. A false negative silently points a
|
|
33
|
+
// paying customer's agent at a site that is not theirs, while every signal
|
|
34
|
+
// says healthy.
|
|
35
|
+
const realKeyDummyUrl = { sites: [{ ...realSite, url: 'https://example.com' }], preferences: prefs };
|
|
36
|
+
assert.equal(isPlaceholderConfig(realKeyDummyUrl), true);
|
|
37
|
+
});
|
|
38
|
+
test('a trailing slash or different case on the dummy url does not defeat the check', () => {
|
|
39
|
+
for (const url of ['https://example.com/', 'HTTPS://Example.com', 'https://example.com//']) {
|
|
40
|
+
assert.equal(isPlaceholderConfig({ sites: [{ ...realSite, url }], preferences: prefs }), true, `expected ${url} to be recognised as the placeholder address`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
test('a real site is still never flagged, whatever its key looks like', () => {
|
|
44
|
+
// The guard must not start eating working configs. This is the regression
|
|
45
|
+
// that would actually break people.
|
|
46
|
+
assert.equal(isPlaceholderConfig({ sites: [realSite], preferences: prefs }), false);
|
|
47
|
+
assert.equal(isPlaceholderConfig({ sites: [{ ...realSite, url: 'https://example.com.mysite.dk' }], preferences: prefs }), false, 'a hostname merely CONTAINING example.com is a real site');
|
|
21
48
|
});
|
|
22
49
|
test('isPlaceholderConfig is false for empty / missing sites', () => {
|
|
23
50
|
assert.equal(isPlaceholderConfig({ sites: [], preferences: prefs }), false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-placeholder.test.js","sourceRoot":"","sources":["../../src/__tests__/config-placeholder.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,QAAQ,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,4BAA4B,EAAE,MAAM,EAAE,qBAAqB,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACzH,MAAM,KAAK,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC;AAE5D,IAAI,CAAC,sDAAsD,EAAE,GAAG,EAAE;IAChE,MAAM,WAAW,GAAG;QAClB,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,mBAAmB,EAAE,GAAG,EAAE,qBAAqB,EAAE,MAAM,EAAE,2BAA2B,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC1I,WAAW,EAAE,KAAK;KACnB,CAAC;IACF,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,CAAC;AACvD,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,iDAAiD,EAAE,GAAG,EAAE;IAC3D,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;AACtF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"config-placeholder.test.js","sourceRoot":"","sources":["../../src/__tests__/config-placeholder.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,QAAQ,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,4BAA4B,EAAE,MAAM,EAAE,qBAAqB,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACzH,MAAM,KAAK,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC;AAE5D,IAAI,CAAC,sDAAsD,EAAE,GAAG,EAAE;IAChE,MAAM,WAAW,GAAG;QAClB,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,mBAAmB,EAAE,GAAG,EAAE,qBAAqB,EAAE,MAAM,EAAE,2BAA2B,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC1I,WAAW,EAAE,KAAK;KACnB,CAAC;IACF,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,CAAC;AACvD,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,iDAAiD,EAAE,GAAG,EAAE;IAC3D,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;AACtF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,qEAAqE,EAAE,GAAG,EAAE;IAC/E,6EAA6E;IAC7E,6EAA6E;IAC7E,2EAA2E;IAC3E,gEAAgE;IAChE,EAAE;IACF,6EAA6E;IAC7E,uEAAuE;IACvE,6EAA6E;IAC7E,2EAA2E;IAC3E,4EAA4E;IAC5E,0BAA0B;IAC1B,EAAE;IACF,wEAAwE;IACxE,8EAA8E;IAC9E,sEAAsE;IACtE,6EAA6E;IAC7E,2EAA2E;IAC3E,gBAAgB;IAChB,MAAM,eAAe,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,EAAE,qBAAqB,EAAE,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;IACrG,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,eAAe,CAAC,EAAE,IAAI,CAAC,CAAC;AAC3D,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,+EAA+E,EAAE,GAAG,EAAE;IACzF,KAAK,MAAM,GAAG,IAAI,CAAC,sBAAsB,EAAE,qBAAqB,EAAE,uBAAuB,CAAC,EAAE,CAAC;QAC3F,MAAM,CAAC,KAAK,CACV,mBAAmB,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,EAAE,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EAC1E,IAAI,EACJ,YAAY,GAAG,8CAA8C,CAC9D,CAAC;IACJ,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,iEAAiE,EAAE,GAAG,EAAE;IAC3E,0EAA0E;IAC1E,oCAAoC;IACpC,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;IACpF,MAAM,CAAC,KAAK,CACV,mBAAmB,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,EAAE,+BAA+B,EAAE,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EAC3G,KAAK,EACL,yDAAyD,CAC1D,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,wDAAwD,EAAE,GAAG,EAAE;IAClE,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;IAC5E,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;IAC/C,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,CAAC;IACpD,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,mFAAmF,EAAE,GAAG,EAAE;IAC7F,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,qBAAqB,EAAE,MAAM,EAAE,2BAA2B,EAAE,CAAC;IACnG,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IACjF,yCAAyC;IACzC,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;AAC1F,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoke-ability-required-args.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/invoke-ability-required-args.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { requireAbilityName } from '../server.js';
|
|
4
|
+
// wordpress_invoke_ability has never succeeded once: 36 calls, 36 failures.
|
|
5
|
+
// Most of those are the agent naming an ability that does not exist, fixed
|
|
6
|
+
// plugin-side in 8.6.2. Seven are this: the tool was called without `ability`,
|
|
7
|
+
// the dispatcher passed undefined straight through, JSON dropped the key, and
|
|
8
|
+
// the site answered `rest_missing_callback_param` — a message that names
|
|
9
|
+
// neither the parameter nor the tool's word for it.
|
|
10
|
+
//
|
|
11
|
+
// The contract is worth guarding because it is the one an agent has to guess.
|
|
12
|
+
test('a well-formed ability name passes through, trimmed', () => {
|
|
13
|
+
assert.equal(requireAbilityName({ ability: 'rankmath/get-meta' }), 'rankmath/get-meta');
|
|
14
|
+
assert.equal(requireAbilityName({ ability: ' woocommerce/get-order ' }), 'woocommerce/get-order');
|
|
15
|
+
});
|
|
16
|
+
test('a missing ability names the parameter, the shape, and the way out', () => {
|
|
17
|
+
assert.throws(() => requireAbilityName({ args: { post_id: 1 } }), (err) => {
|
|
18
|
+
assert.match(err.message, /requires `ability`/, 'names the parameter');
|
|
19
|
+
assert.match(err.message, /vendor\/ability-name/, 'gives the shape');
|
|
20
|
+
assert.match(err.message, /wordpress_search_abilities/, 'points at discovery');
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
test('undefined, empty and whitespace-only are all refused', () => {
|
|
25
|
+
for (const bad of [undefined, {}, { ability: '' }, { ability: ' ' }, { ability: null }, { ability: 42 }]) {
|
|
26
|
+
assert.throws(() => requireAbilityName(bad), /requires `ability`/, `should refuse ${JSON.stringify(bad)}`);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
test('a plausible alias is named in the error, not silently accepted', () => {
|
|
30
|
+
// Silently honouring these would make the tool schema a lie and teach the
|
|
31
|
+
// next agent the wrong contract from a call that worked.
|
|
32
|
+
for (const alias of ['name', 'ability_name', 'abilityName', 'tool', 'slug']) {
|
|
33
|
+
assert.throws(() => requireAbilityName({ [alias]: 'rankmath/get-meta' }), (err) => {
|
|
34
|
+
assert.match(err.message, new RegExp('`' + alias + '`'), `should name ${alias}`);
|
|
35
|
+
assert.match(err.message, /rename it to `ability`/, 'should say what to do');
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
test('several aliases at once are all named', () => {
|
|
41
|
+
assert.throws(() => requireAbilityName({ name: 'a/b', tool: 'c/d' }), (err) => {
|
|
42
|
+
assert.match(err.message, /`name`/);
|
|
43
|
+
assert.match(err.message, /`tool`/);
|
|
44
|
+
return true;
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
test('an alias holding a non-string is not reported as if it were usable', () => {
|
|
48
|
+
assert.throws(() => requireAbilityName({ name: 12345 }), (err) => {
|
|
49
|
+
assert.doesNotMatch(err.message, /You sent/, 'a numeric alias is not a rename suggestion');
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
//# sourceMappingURL=invoke-ability-required-args.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoke-ability-required-args.test.js","sourceRoot":"","sources":["../../src/__tests__/invoke-ability-required-args.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket 2b3a706b — the PUT-blocked-host fallback existed (isRetryableAsPost /
|
|
3
|
+
* PUT_FALLBACK_STATUSES, oq-put-fallback-403.test.ts) but was only wired into
|
|
4
|
+
* the dedicated updatePost/updatePage methods. Every tool routed through the
|
|
5
|
+
* shared `callRestV2`/`callRestV1` REST callers — Elementor global colors and
|
|
6
|
+
* typography (elementor-tools.ts), Bricks global classes/theme styles/color
|
|
7
|
+
* palette/typography (bricks-tools.ts), FSE templates/patterns/navigation and
|
|
8
|
+
* design tokens (server.ts) — emitted a bare PUT with no fallback at all, so a
|
|
9
|
+
* host that blocks PUT (allows only OPTIONS/HEAD/GET/POST, e.g. shared Apache
|
|
10
|
+
* hosting) still got a misleading "site blocked by firewall" 403 even though
|
|
11
|
+
* there is no firewall, just a host that rejects the verb.
|
|
12
|
+
*
|
|
13
|
+
* Reported, root-caused and verified live by O.Q. against
|
|
14
|
+
* linvestisseurantifragile.com (OVH shared hosting): POSTing the identical
|
|
15
|
+
* payloads to the same v2 routes succeeded with 200, proving the server
|
|
16
|
+
* already accepts POST — only the client's method choice needed the fix.
|
|
17
|
+
*
|
|
18
|
+
* These tests run a real HTTP server (no adapter mocking) so the real
|
|
19
|
+
* interceptor chain runs, including the `.response`-stripping codedError()
|
|
20
|
+
* step that made a status-only predicate a no-op the first time around
|
|
21
|
+
* (see oq-put-fallback-403.test.ts).
|
|
22
|
+
*/
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=oq-put-fallback-callRestV2-integration.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oq-put-fallback-callRestV2-integration.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/oq-put-fallback-callRestV2-integration.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket 2b3a706b — the PUT-blocked-host fallback existed (isRetryableAsPost /
|
|
3
|
+
* PUT_FALLBACK_STATUSES, oq-put-fallback-403.test.ts) but was only wired into
|
|
4
|
+
* the dedicated updatePost/updatePage methods. Every tool routed through the
|
|
5
|
+
* shared `callRestV2`/`callRestV1` REST callers — Elementor global colors and
|
|
6
|
+
* typography (elementor-tools.ts), Bricks global classes/theme styles/color
|
|
7
|
+
* palette/typography (bricks-tools.ts), FSE templates/patterns/navigation and
|
|
8
|
+
* design tokens (server.ts) — emitted a bare PUT with no fallback at all, so a
|
|
9
|
+
* host that blocks PUT (allows only OPTIONS/HEAD/GET/POST, e.g. shared Apache
|
|
10
|
+
* hosting) still got a misleading "site blocked by firewall" 403 even though
|
|
11
|
+
* there is no firewall, just a host that rejects the verb.
|
|
12
|
+
*
|
|
13
|
+
* Reported, root-caused and verified live by O.Q. against
|
|
14
|
+
* linvestisseurantifragile.com (OVH shared hosting): POSTing the identical
|
|
15
|
+
* payloads to the same v2 routes succeeded with 200, proving the server
|
|
16
|
+
* already accepts POST — only the client's method choice needed the fix.
|
|
17
|
+
*
|
|
18
|
+
* These tests run a real HTTP server (no adapter mocking) so the real
|
|
19
|
+
* interceptor chain runs, including the `.response`-stripping codedError()
|
|
20
|
+
* step that made a status-only predicate a no-op the first time around
|
|
21
|
+
* (see oq-put-fallback-403.test.ts).
|
|
22
|
+
*/
|
|
23
|
+
import { test, after } from 'node:test';
|
|
24
|
+
import assert from 'node:assert/strict';
|
|
25
|
+
import http from 'node:http';
|
|
26
|
+
import { WordPressClient } from '../wordpress-client.js';
|
|
27
|
+
const servers = [];
|
|
28
|
+
after(() => {
|
|
29
|
+
for (const s of servers)
|
|
30
|
+
s.close();
|
|
31
|
+
});
|
|
32
|
+
// Apache-style "Forbidden" page for a method the host rejects at the edge —
|
|
33
|
+
// HTML, not JSON, which is what makes handleError() treat it as
|
|
34
|
+
// "not WordPress" and stamp error.name = 'http_403_non_wordpress'.
|
|
35
|
+
const APACHE_403_HTML = '<!DOCTYPE html><html><head><title>403 Forbidden</title></head><body><h1>Forbidden</h1></body></html>';
|
|
36
|
+
/**
|
|
37
|
+
* A fake site: GET /wp-json/respira/v2/status negotiates v2 (200 JSON).
|
|
38
|
+
* PUT to `blockedPath` is rejected exactly like a host that disallows the
|
|
39
|
+
* verb: HTML body, 403 status, no WordPress JSON envelope. POST to the same
|
|
40
|
+
* path succeeds with a JSON body, mirroring O.Q.'s manual proof.
|
|
41
|
+
*/
|
|
42
|
+
async function startBlockedPutSite(blockedPath) {
|
|
43
|
+
const hits = [];
|
|
44
|
+
const server = http.createServer((req, res) => {
|
|
45
|
+
hits.push({ method: req.method || '', url: req.url || '' });
|
|
46
|
+
const path = (req.url || '').split('?')[0];
|
|
47
|
+
if (req.method === 'GET' && path === '/wp-json/respira/v2/status') {
|
|
48
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
49
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (req.method === 'PUT' && path === blockedPath) {
|
|
53
|
+
res.writeHead(403, { 'Content-Type': 'text/html' });
|
|
54
|
+
res.end(APACHE_403_HTML);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (req.method === 'POST' && path === blockedPath) {
|
|
58
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
59
|
+
res.end(JSON.stringify({ success: true, via: 'POST' }));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
63
|
+
res.end(JSON.stringify({ code: 'rest_no_route', message: 'No route' }));
|
|
64
|
+
});
|
|
65
|
+
servers.push(server);
|
|
66
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
67
|
+
const { port } = server.address();
|
|
68
|
+
const client = new WordPressClient({
|
|
69
|
+
id: 'oq-blocked-put',
|
|
70
|
+
name: 'oq-blocked-put',
|
|
71
|
+
url: `http://127.0.0.1:${port}`,
|
|
72
|
+
apiKey: 'respira_test_key',
|
|
73
|
+
});
|
|
74
|
+
return { client, hits };
|
|
75
|
+
}
|
|
76
|
+
test('callRestV2 PUT on a host that blocks PUT retries as POST transparently (Elementor global colors)', async () => {
|
|
77
|
+
const { client, hits } = await startBlockedPutSite('/wp-json/respira/v2/elementor/global-colors');
|
|
78
|
+
const result = await client.callRestV2('PUT', '/elementor/global-colors', {
|
|
79
|
+
colors: [{ _id: 'brand_primary', title: 'Brand Primary', color: '#FF5733' }],
|
|
80
|
+
});
|
|
81
|
+
assert.deepEqual(result, { success: true, via: 'POST' });
|
|
82
|
+
const put = hits.filter((h) => h.method === 'PUT' && h.url.includes('/elementor/global-colors'));
|
|
83
|
+
const post = hits.filter((h) => h.method === 'POST' && h.url.includes('/elementor/global-colors'));
|
|
84
|
+
assert.equal(put.length, 1, 'PUT was attempted first');
|
|
85
|
+
assert.equal(post.length, 1, 'POST fallback fired exactly once');
|
|
86
|
+
});
|
|
87
|
+
test('callRestV2 PUT on a host that blocks PUT retries as POST transparently (Elementor global typography)', async () => {
|
|
88
|
+
const { client, hits } = await startBlockedPutSite('/wp-json/respira/v2/elementor/global-typography');
|
|
89
|
+
const result = await client.callRestV2('PUT', '/elementor/global-typography', {
|
|
90
|
+
typography: [{ _id: 'heading_primary', title: 'Heading Primary' }],
|
|
91
|
+
});
|
|
92
|
+
assert.deepEqual(result, { success: true, via: 'POST' });
|
|
93
|
+
// Scoped to the target path: a background, non-blocking checkCompatibility()
|
|
94
|
+
// POST to /server/compatibility also hits this same fake server and would
|
|
95
|
+
// otherwise inflate an unscoped count.
|
|
96
|
+
assert.equal(hits.filter((h) => h.method === 'POST' && h.url.includes('/elementor/global-typography')).length, 1);
|
|
97
|
+
});
|
|
98
|
+
test('callRestV2 PUT on a host that blocks PUT retries as POST transparently (Bricks theme styles, a sibling call site)', async () => {
|
|
99
|
+
const { client, hits } = await startBlockedPutSite('/wp-json/respira/v2/bricks/theme-styles');
|
|
100
|
+
const result = await client.callRestV2('PUT', '/bricks/theme-styles', { value: { spacing: 'md' } });
|
|
101
|
+
assert.deepEqual(result, { success: true, via: 'POST' });
|
|
102
|
+
assert.equal(hits.filter((h) => h.method === 'POST' && h.url.includes('/bricks/theme-styles')).length, 1);
|
|
103
|
+
});
|
|
104
|
+
test('callRestV2 does NOT retry a real WordPress permission 403 as POST', async () => {
|
|
105
|
+
const hits = [];
|
|
106
|
+
const server = http.createServer((req, res) => {
|
|
107
|
+
hits.push({ method: req.method || '', url: req.url || '' });
|
|
108
|
+
const path = (req.url || '').split('?')[0];
|
|
109
|
+
if (req.method === 'GET' && path === '/wp-json/respira/v2/status') {
|
|
110
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
111
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (req.method === 'PUT' && path === '/wp-json/respira/v2/elementor/global-colors') {
|
|
115
|
+
// A genuine WordPress permission decision: JSON body, not HTML.
|
|
116
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
117
|
+
res.end(JSON.stringify({ code: 'rest_cannot_edit', message: 'Sorry, you are not allowed to edit this.' }));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
121
|
+
res.end(JSON.stringify({ code: 'rest_no_route' }));
|
|
122
|
+
});
|
|
123
|
+
servers.push(server);
|
|
124
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
125
|
+
const { port } = server.address();
|
|
126
|
+
const client = new WordPressClient({
|
|
127
|
+
id: 'oq-real-403',
|
|
128
|
+
name: 'oq-real-403',
|
|
129
|
+
url: `http://127.0.0.1:${port}`,
|
|
130
|
+
apiKey: 'respira_test_key',
|
|
131
|
+
});
|
|
132
|
+
await assert.rejects(() => client.callRestV2('PUT', '/elementor/global-colors', { colors: [] }), /not allowed to edit/);
|
|
133
|
+
assert.equal(hits.filter((h) => h.method === 'POST' && h.url.includes('/elementor/global-colors')).length, 0, 'no POST retry for a real WP permission denial');
|
|
134
|
+
});
|
|
135
|
+
//# sourceMappingURL=oq-put-fallback-callRestV2-integration.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oq-put-fallback-callRestV2-integration.test.js","sourceRoot":"","sources":["../../src/__tests__/oq-put-fallback-callRestV2-integration.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport-mode-observability.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/transport-mode-observability.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* describeEffectiveConfig answers "which config won". This is the other half:
|
|
3
|
+
* which INSTALL won, and is a second one running.
|
|
4
|
+
*
|
|
5
|
+
* Desmond (c9a95152) installed the .mcpb desktop bundle while an older npx
|
|
6
|
+
* entry stayed in his client config. Both were real Respira servers. The
|
|
7
|
+
* client kept talking to the old one for three weeks and nothing anywhere
|
|
8
|
+
* said which install was answering.
|
|
9
|
+
*
|
|
10
|
+
* These tests pin four things:
|
|
11
|
+
* 1. each install shape is recognised from its launch path alone, because a
|
|
12
|
+
* sibling process only ever gives us a command line, never an env;
|
|
13
|
+
* 2. the bundle is recognised even though its command line does not contain
|
|
14
|
+
* the string 'wordpress-mcp-server' — the exact blind spot that made
|
|
15
|
+
* Desmond's second server invisible to the old sibling scan;
|
|
16
|
+
* 3. two different launch modes running at once reads as a duplicate client
|
|
17
|
+
* entry, while two identical ones stays honestly ambiguous (orphans from
|
|
18
|
+
* a reconnect look the same from here);
|
|
19
|
+
* 4. nothing token-shaped reaches the output, which gets pasted into
|
|
20
|
+
* support threads.
|
|
21
|
+
*/
|
|
22
|
+
import { describe, it } from 'node:test';
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import { classifyLaunchMode, collapseLauncherProcesses, describeDuplicateEntries, describeTransportMode, redactSecrets, } from '../config.js';
|
|
25
|
+
const MCPB_PATH = '/Users/d/Library/Application Support/Claude/Claude Extensions/local.mcpb.respira-wordpress/server/index.js';
|
|
26
|
+
const NPX_PATH = '/Users/d/.npm/_npx/8f3a21/node_modules/@respira/wordpress-mcp-server/dist/index.js';
|
|
27
|
+
describe('classifyLaunchMode', () => {
|
|
28
|
+
it('recognises the desktop bundle from its path, with no env to help', () => {
|
|
29
|
+
assert.equal(classifyLaunchMode(MCPB_PATH).mode, 'mcpb_bundle');
|
|
30
|
+
});
|
|
31
|
+
it('trusts the connection source the bundle manifest sets', () => {
|
|
32
|
+
const { mode, evidence } = classifyLaunchMode('/wherever/index.js', {
|
|
33
|
+
RESPIRA_CONNECTION_SOURCE: 'claude_desktop_mcpb',
|
|
34
|
+
});
|
|
35
|
+
assert.equal(mode, 'mcpb_bundle');
|
|
36
|
+
assert.match(evidence.join(' '), /claude_desktop_mcpb/);
|
|
37
|
+
});
|
|
38
|
+
it('recognises an npx one-off run', () => {
|
|
39
|
+
assert.equal(classifyLaunchMode(NPX_PATH).mode, 'npx_cache');
|
|
40
|
+
});
|
|
41
|
+
it('recognises a global install (homebrew and nvm)', () => {
|
|
42
|
+
assert.equal(classifyLaunchMode('/opt/homebrew/lib/node_modules/@respira/wordpress-mcp-server/dist/index.js').mode, 'global_npm_install');
|
|
43
|
+
assert.equal(classifyLaunchMode('/Users/d/.nvm/versions/node/v22.3.0/lib/node_modules/@respira/wordpress-mcp-server/dist/index.js').mode, 'global_npm_install');
|
|
44
|
+
});
|
|
45
|
+
it('recognises a project-local install and a source checkout', () => {
|
|
46
|
+
assert.equal(classifyLaunchMode('/Users/d/app/node_modules/@respira/wordpress-mcp-server/dist/index.js').mode, 'project_local_install');
|
|
47
|
+
assert.equal(classifyLaunchMode('/Users/d/code/respira-wordpress/mcp-server/dist/index.js').mode, 'source_checkout');
|
|
48
|
+
});
|
|
49
|
+
it('says unknown rather than guessing', () => {
|
|
50
|
+
assert.equal(classifyLaunchMode('/opt/custom-wrapper/run.js').mode, 'unknown');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
/**
|
|
54
|
+
* Captured from a real Claude Desktop machine. ONE npx entry, three processes.
|
|
55
|
+
* If these count as three servers, the duplicate warning fires on every
|
|
56
|
+
* healthy install and everyone learns to ignore it.
|
|
57
|
+
*/
|
|
58
|
+
const NPX_LAUNCHER_CHAIN = [
|
|
59
|
+
{
|
|
60
|
+
pid: 72496,
|
|
61
|
+
ppid: 1,
|
|
62
|
+
command: '/Applications/Claude.app/Contents/Helpers/disclaimer /Users/d/.nvm/versions/node/v20.19.5/bin/npx -y @respira/wordpress-mcp-server@latest',
|
|
63
|
+
},
|
|
64
|
+
{ pid: 72497, ppid: 72496, command: 'npm exec @respira/wordpress-mcp-server@latest' },
|
|
65
|
+
{
|
|
66
|
+
pid: 73159,
|
|
67
|
+
ppid: 72497,
|
|
68
|
+
command: 'node /Users/d/.npm/_npx/9fa6fea18a2d317a/node_modules/.bin/wordpress-mcp-server',
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
describe('collapseLauncherProcesses', () => {
|
|
72
|
+
it("keeps only the leaf of one npx entry's three-process chain", () => {
|
|
73
|
+
const kept = collapseLauncherProcesses(999, 1, NPX_LAUNCHER_CHAIN);
|
|
74
|
+
assert.deepEqual(kept.map((s) => s.pid), [73159]);
|
|
75
|
+
});
|
|
76
|
+
it('drops this process\'s own launchers when we are the leaf', () => {
|
|
77
|
+
const kept = collapseLauncherProcesses(73159, 72497, NPX_LAUNCHER_CHAIN.slice(0, 2));
|
|
78
|
+
assert.deepEqual(kept, []);
|
|
79
|
+
});
|
|
80
|
+
it('keeps two genuinely unrelated servers', () => {
|
|
81
|
+
const kept = collapseLauncherProcesses(1, 0, [
|
|
82
|
+
{ pid: 10, ppid: 1, command: `node ${NPX_PATH}` },
|
|
83
|
+
{ pid: 20, ppid: 1, command: `node ${MCPB_PATH}` },
|
|
84
|
+
]);
|
|
85
|
+
assert.equal(kept.length, 2);
|
|
86
|
+
});
|
|
87
|
+
it('does not hang on a ppid cycle', () => {
|
|
88
|
+
const kept = collapseLauncherProcesses(1, 0, [
|
|
89
|
+
{ pid: 10, ppid: 20, command: 'node a/wordpress-mcp-server/dist/index.js' },
|
|
90
|
+
{ pid: 20, ppid: 10, command: 'node b/wordpress-mcp-server/dist/index.js' },
|
|
91
|
+
]);
|
|
92
|
+
assert.ok(Array.isArray(kept));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('describeDuplicateEntries', () => {
|
|
96
|
+
it("flags Desmond's case: a bundle and an npx entry running at once", () => {
|
|
97
|
+
const d = describeDuplicateEntries('mcpb_bundle', [{ pid: 4242, ppid: 1, command: `node ${NPX_PATH}` }], true);
|
|
98
|
+
assert.equal(d.suspected, true);
|
|
99
|
+
assert.equal(d.confidence, 'high');
|
|
100
|
+
assert.match(String(d.message), /lists Respira twice/);
|
|
101
|
+
assert.equal(d.siblings[0].launch_mode, 'npx_cache');
|
|
102
|
+
});
|
|
103
|
+
it('stays ambiguous when the siblings were launched the same way', () => {
|
|
104
|
+
const d = describeDuplicateEntries('npx_cache', [{ pid: 1, ppid: 1, command: `node ${NPX_PATH}` }], true);
|
|
105
|
+
assert.equal(d.suspected, false);
|
|
106
|
+
assert.equal(d.confidence, 'low');
|
|
107
|
+
assert.match(String(d.message), /orphans/);
|
|
108
|
+
});
|
|
109
|
+
it('reports a clean single server when nothing else is running', () => {
|
|
110
|
+
const d = describeDuplicateEntries('mcpb_bundle', [], true);
|
|
111
|
+
assert.equal(d.suspected, false);
|
|
112
|
+
assert.equal(d.sibling_count, 0);
|
|
113
|
+
});
|
|
114
|
+
it('admits it knows nothing when the process scan could not run', () => {
|
|
115
|
+
const d = describeDuplicateEntries('unknown', [], false);
|
|
116
|
+
assert.equal(d.server_detectable, false);
|
|
117
|
+
assert.equal(d.suspected, null);
|
|
118
|
+
assert.match(String(d.message), /Check your client config by hand/);
|
|
119
|
+
});
|
|
120
|
+
it('always states the limit that a non-running duplicate is invisible', () => {
|
|
121
|
+
for (const scanned of [true, false]) {
|
|
122
|
+
const d = describeDuplicateEntries('npx_cache', [], scanned);
|
|
123
|
+
assert.match(d.limits.join(' '), /not currently running cannot be seen/);
|
|
124
|
+
assert.match(d.limits.join(' '), /deliberately does not read it/);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('describeTransportMode', () => {
|
|
129
|
+
it('names the protocol as stdio and rules out the remote connector', () => {
|
|
130
|
+
const t = describeTransportMode({ binaryPath: NPX_PATH, env: {} });
|
|
131
|
+
assert.equal(t.protocol, 'stdio');
|
|
132
|
+
assert.match(t.notes.join(' '), /remote HTTP connector runs inside the WordPress plugin/);
|
|
133
|
+
});
|
|
134
|
+
it('promotes a single readable mode string', () => {
|
|
135
|
+
const t = describeTransportMode({ binaryPath: MCPB_PATH, env: {} });
|
|
136
|
+
assert.equal(t.transport_mode, 'mcpb_bundle');
|
|
137
|
+
assert.equal(t.label, 'Claude Desktop .mcpb bundle');
|
|
138
|
+
});
|
|
139
|
+
it('reports the connection source verbatim, or says it is unset', () => {
|
|
140
|
+
assert.equal(describeTransportMode({ binaryPath: NPX_PATH, env: { RESPIRA_CONNECTION_SOURCE: 'dashboard_export' } }).connection_source, 'dashboard_export');
|
|
141
|
+
assert.equal(describeTransportMode({ binaryPath: NPX_PATH, env: {} }).connection_source, 'unset');
|
|
142
|
+
});
|
|
143
|
+
it('warns when a global install is quietly outranking a pinned @latest', () => {
|
|
144
|
+
const t = describeTransportMode({
|
|
145
|
+
binaryPath: '/opt/homebrew/lib/node_modules/@respira/wordpress-mcp-server/dist/index.js',
|
|
146
|
+
env: {},
|
|
147
|
+
});
|
|
148
|
+
assert.equal(t.is_global_install, true);
|
|
149
|
+
assert.match(t.notes.join(' '), /npm exec prefers the global binary/);
|
|
150
|
+
});
|
|
151
|
+
it('notices a bundle running without the setup code the bundle normally passes', () => {
|
|
152
|
+
const t = describeTransportMode({ binaryPath: MCPB_PATH, env: {} });
|
|
153
|
+
assert.match(t.notes.join(' '), /setup code may not have been saved/);
|
|
154
|
+
});
|
|
155
|
+
it('carries the duplicate signal through', () => {
|
|
156
|
+
const t = describeTransportMode({
|
|
157
|
+
binaryPath: MCPB_PATH,
|
|
158
|
+
env: {},
|
|
159
|
+
siblings: [{ pid: 9, ppid: 1, command: `node ${NPX_PATH}` }],
|
|
160
|
+
selfPid: 1000,
|
|
161
|
+
selfPpid: 1,
|
|
162
|
+
});
|
|
163
|
+
assert.equal(t.duplicate_entries.suspected, true);
|
|
164
|
+
});
|
|
165
|
+
it('does not cry wolf on a healthy npx install seen from its own leaf', () => {
|
|
166
|
+
const t = describeTransportMode({
|
|
167
|
+
binaryPath: NPX_PATH,
|
|
168
|
+
env: {},
|
|
169
|
+
siblings: NPX_LAUNCHER_CHAIN.slice(0, 2),
|
|
170
|
+
selfPid: 73159,
|
|
171
|
+
selfPpid: 72497,
|
|
172
|
+
});
|
|
173
|
+
assert.equal(t.duplicate_entries.suspected, false);
|
|
174
|
+
assert.equal(t.duplicate_entries.sibling_count, 0);
|
|
175
|
+
});
|
|
176
|
+
it('never emits a token — this output gets pasted into support threads', () => {
|
|
177
|
+
const serialised = JSON.stringify(describeTransportMode({
|
|
178
|
+
binaryPath: NPX_PATH,
|
|
179
|
+
env: {},
|
|
180
|
+
siblings: [
|
|
181
|
+
{ pid: 7, ppid: 1, command: 'node /x/wordpress-mcp-server/dist/index.js --token respira_site_ABCDEF123456' },
|
|
182
|
+
],
|
|
183
|
+
selfPid: 1000,
|
|
184
|
+
selfPpid: 1,
|
|
185
|
+
}));
|
|
186
|
+
assert.doesNotMatch(serialised, /ABCDEF123456/);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
describe('redactSecrets', () => {
|
|
190
|
+
it('masks site tokens, oauth tokens and base64 config blobs', () => {
|
|
191
|
+
assert.equal(redactSecrets('key respira_site_abc123def'), 'key respira_site_***');
|
|
192
|
+
assert.equal(redactSecrets('key rsp_at_abc123def'), 'key rsp_at_***');
|
|
193
|
+
assert.match(redactSecrets('RESPIRA_CONFIG_B64=eyJzaXRlcyI6W3sidXJsIjoiaHR0cHM6Ly9hIn1dfQ=='), /\*\*\*/);
|
|
194
|
+
assert.match(redactSecrets('node run eyJzaXRlcyI6W3sidXJsIjoiaHR0cHM6Ly9hIn1dfQ=='), /<base64-config-redacted>/);
|
|
195
|
+
});
|
|
196
|
+
it('leaves an ordinary command line alone', () => {
|
|
197
|
+
const plain = 'node /usr/local/lib/node_modules/@respira/wordpress-mcp-server/dist/index.js';
|
|
198
|
+
assert.equal(redactSecrets(plain), plain);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
//# sourceMappingURL=transport-mode-observability.test.js.map
|