@respira/wordpress-mcp-server 7.6.2 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -23
- package/TOOL_CATALOG.md +288 -0
- package/dist/__tests__/config-path-expansion.test.d.ts +2 -0
- package/dist/__tests__/config-path-expansion.test.d.ts.map +1 -0
- package/dist/__tests__/config-path-expansion.test.js +63 -0
- package/dist/__tests__/config-path-expansion.test.js.map +1 -0
- package/dist/__tests__/rest-route-fallback.test.js +0 -64
- package/dist/__tests__/rest-route-fallback.test.js.map +1 -1
- package/dist/__tests__/site-token-401-error-classification.test.d.ts +0 -19
- package/dist/__tests__/site-token-401-error-classification.test.d.ts.map +1 -1
- package/dist/__tests__/site-token-401-error-classification.test.js +20 -60
- package/dist/__tests__/site-token-401-error-classification.test.js.map +1 -1
- package/dist/__tests__/tools-list-protocol-smoke.test.d.ts +2 -0
- package/dist/__tests__/tools-list-protocol-smoke.test.d.ts.map +1 -0
- package/dist/__tests__/tools-list-protocol-smoke.test.js +78 -0
- package/dist/__tests__/tools-list-protocol-smoke.test.js.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +80 -9
- package/dist/config.js.map +1 -1
- package/dist/elementor-tools.js +2 -2
- package/dist/elementor-tools.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +750 -19
- package/dist/server.js.map +1 -1
- package/dist/usage-emitter.d.ts +7 -0
- package/dist/usage-emitter.d.ts.map +1 -1
- package/dist/usage-emitter.js +34 -0
- package/dist/usage-emitter.js.map +1 -1
- package/dist/wordpress-client.d.ts +53 -42
- package/dist/wordpress-client.d.ts.map +1 -1
- package/dist/wordpress-client.js +105 -94
- package/dist/wordpress-client.js.map +1 -1
- package/package.json +5 -3
- package/tool-capabilities.json +4122 -0
package/dist/wordpress-client.js
CHANGED
|
@@ -13,6 +13,63 @@ import { randomUUID } from 'node:crypto';
|
|
|
13
13
|
import { getRespiraAgentHeaders } from './agent-signature.js';
|
|
14
14
|
import { CONFIG_FILE, isTlsInsecureEnv } from './config.js';
|
|
15
15
|
import { makeHttpsAgent } from './system-ca.js';
|
|
16
|
+
/** `C:\...` or `C:/...`. Windows drive-letter absolute path. */
|
|
17
|
+
const WINDOWS_DRIVE_PATH_RE = /^[a-zA-Z]:[\\/]/;
|
|
18
|
+
/**
|
|
19
|
+
* Does this string name a file on the caller's own disk, as opposed to a URL,
|
|
20
|
+
* a data: URI, or a bare base64 blob?
|
|
21
|
+
*
|
|
22
|
+
* Matches POSIX absolute (`/home/...`), relative (`./`, `../`), home-relative
|
|
23
|
+
* (`~/...`), Windows drive-letter (`C:\...`, `C:/...`), and `file://` URIs
|
|
24
|
+
* (`file:///C:/Users/...` on Windows, `file:///home/...` on Unix). The caller
|
|
25
|
+
* converts `file://` to a real path with `resolveLocalFilePath()` below.
|
|
26
|
+
*
|
|
27
|
+
* The Windows and `file://` arms are why this is a named helper rather than an
|
|
28
|
+
* inline condition. Without them those inputs fall through to the base64
|
|
29
|
+
* branch, where they do not error: they decode to garbage and upload as a
|
|
30
|
+
* corrupt file. Silent, not loud.
|
|
31
|
+
*
|
|
32
|
+
* Not handled: Windows UNC paths (`\\server\share\file.jpg`). They do not
|
|
33
|
+
* reproduce the same way, since backslashes are not valid base64 characters,
|
|
34
|
+
* but they are not read as a local file either. Out of scope until a UNC
|
|
35
|
+
* report comes in.
|
|
36
|
+
*/
|
|
37
|
+
export function isLocalFilePath(file) {
|
|
38
|
+
return (file.startsWith('/') ||
|
|
39
|
+
file.startsWith('./') ||
|
|
40
|
+
file.startsWith('../') ||
|
|
41
|
+
file.startsWith('~') ||
|
|
42
|
+
WINDOWS_DRIVE_PATH_RE.test(file) ||
|
|
43
|
+
file.startsWith('file://'));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Turns an `isLocalFilePath()`-matched string into a real filesystem path.
|
|
47
|
+
* Handles `file://` URIs (unwrapping via `fileURLToPath`, forcing Windows
|
|
48
|
+
* semantics when the URI's own path looks like a drive letter, so the
|
|
49
|
+
* conversion is correct even when the MCP server process itself is running on
|
|
50
|
+
* a non-Windows host), `~` home-dir expansion, and relative-path resolution
|
|
51
|
+
* against cwd.
|
|
52
|
+
*
|
|
53
|
+
* Windows drive-letter absolute paths are returned as-is rather than run
|
|
54
|
+
* through `resolve()`: they are already absolute, and POSIX `resolve()` does
|
|
55
|
+
* not recognise a drive letter as an absolute-path marker, so calling it on a
|
|
56
|
+
* non-Windows host would wrongly prefix the string with cwd. On the Windows
|
|
57
|
+
* machine where such a path is real, `fs` accepts it verbatim.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveLocalFilePath(file) {
|
|
60
|
+
let input = file;
|
|
61
|
+
if (file.startsWith('file://')) {
|
|
62
|
+
const looksWindows = WINDOWS_DRIVE_PATH_RE.test(new URL(file).pathname.replace(/^\//, ''));
|
|
63
|
+
input = looksWindows ? fileURLToPath(file, { windows: true }) : fileURLToPath(file);
|
|
64
|
+
}
|
|
65
|
+
if (input.startsWith('~')) {
|
|
66
|
+
return input.replace(/^~/, process.env.HOME || '');
|
|
67
|
+
}
|
|
68
|
+
if (WINDOWS_DRIVE_PATH_RE.test(input)) {
|
|
69
|
+
return input;
|
|
70
|
+
}
|
|
71
|
+
return resolve(input);
|
|
72
|
+
}
|
|
16
73
|
const MCP_CLIENT_VERSION = (() => {
|
|
17
74
|
try {
|
|
18
75
|
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -60,67 +117,6 @@ export function isReplaySafeRetry(args) {
|
|
|
60
117
|
}
|
|
61
118
|
return safe && !!args.connectionCode && RETRYABLE_CONNECTION_CODES.has(args.connectionCode);
|
|
62
119
|
}
|
|
63
|
-
// Windows drive-letter absolute path, e.g. `C:\Users\bob\photo.jpg` or `C:/Users/bob/photo.jpg`.
|
|
64
|
-
const WINDOWS_DRIVE_PATH_RE = /^[A-Za-z]:[\\/]/;
|
|
65
|
-
/**
|
|
66
|
-
* True when `file` (the `file` argument to `uploadMedia`) looks like a local
|
|
67
|
-
* filesystem path rather than a base64 blob or a `data:`/`http(s):` URL.
|
|
68
|
-
*
|
|
69
|
-
* Historically this only matched POSIX-style paths (`/`, `./`, `../`, `~`),
|
|
70
|
-
* so a Windows absolute path like `C:/Users/bob/photo.jpg` — which starts
|
|
71
|
-
* with none of those — fell through to the base64 branch. `Buffer.from(file,
|
|
72
|
-
* 'base64')` doesn't throw on non-base64 input; it silently decodes whatever
|
|
73
|
-
* valid base64 characters happen to appear (mostly none, for a path like
|
|
74
|
-
* that) and produces a few garbage bytes instead of an error, so the upload
|
|
75
|
-
* "succeeds" with a corrupt file. See ticket 5a19d816.
|
|
76
|
-
*
|
|
77
|
-
* Also matches `file://` URIs (`file:///C:/Users/...` on Windows,
|
|
78
|
-
* `file:///home/...` on Unix); the caller converts these to a real path with
|
|
79
|
-
* `fileURLToPath` before reuse of the existing local-file-read logic below.
|
|
80
|
-
*
|
|
81
|
-
* Not handled: Windows UNC paths (`\\server\share\file.jpg`). They don't
|
|
82
|
-
* reproduce this bug — backslashes aren't valid base64 characters, so they'd
|
|
83
|
-
* still fall into the base64 branch but wouldn't "succeed" quietly the same
|
|
84
|
-
* way — but they also aren't read as a local file today. Out of scope for
|
|
85
|
-
* this fix; revisit if a UNC-specific report comes in.
|
|
86
|
-
*/
|
|
87
|
-
export function isLocalFilePath(file) {
|
|
88
|
-
return (file.startsWith('/') ||
|
|
89
|
-
file.startsWith('./') ||
|
|
90
|
-
file.startsWith('../') ||
|
|
91
|
-
file.startsWith('~') ||
|
|
92
|
-
WINDOWS_DRIVE_PATH_RE.test(file) ||
|
|
93
|
-
file.startsWith('file://'));
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Turns an `isLocalFilePath()`-matched string into a real filesystem path.
|
|
97
|
-
* Handles `file://` URIs (unwrapping via `fileURLToPath`, forcing Windows
|
|
98
|
-
* semantics when the URI's own path looks like a drive letter so this
|
|
99
|
-
* converts correctly even when the MCP server process itself is running on
|
|
100
|
-
* a non-Windows host), `~` home-dir expansion, and relative-path resolution
|
|
101
|
-
* against cwd.
|
|
102
|
-
*
|
|
103
|
-
* Windows drive-letter absolute paths (`C:\...`, `C:/...`) are returned
|
|
104
|
-
* as-is rather than run through `resolve()`: they're already absolute, and
|
|
105
|
-
* POSIX `resolve()` does not recognize a drive letter as an absolute-path
|
|
106
|
-
* marker, so calling it on a non-Windows host (e.g. this test suite) would
|
|
107
|
-
* wrongly prefix the string with `cwd`. On the Windows machine where such a
|
|
108
|
-
* path is actually real, `fs` calls accept it verbatim.
|
|
109
|
-
*/
|
|
110
|
-
export function resolveLocalFilePath(file) {
|
|
111
|
-
let input = file;
|
|
112
|
-
if (file.startsWith('file://')) {
|
|
113
|
-
const looksWindows = WINDOWS_DRIVE_PATH_RE.test(new URL(file).pathname.replace(/^\//, ''));
|
|
114
|
-
input = looksWindows ? fileURLToPath(file, { windows: true }) : fileURLToPath(file);
|
|
115
|
-
}
|
|
116
|
-
if (input.startsWith('~')) {
|
|
117
|
-
return input.replace(/^~/, process.env.HOME || '');
|
|
118
|
-
}
|
|
119
|
-
if (WINDOWS_DRIVE_PATH_RE.test(input)) {
|
|
120
|
-
return input;
|
|
121
|
-
}
|
|
122
|
-
return resolve(input);
|
|
123
|
-
}
|
|
124
120
|
export class WordPressClient {
|
|
125
121
|
client;
|
|
126
122
|
rootClient;
|
|
@@ -1282,6 +1278,10 @@ export class WordPressClient {
|
|
|
1282
1278
|
const response = await this.client.get('/abilities/gap-report');
|
|
1283
1279
|
return response.data;
|
|
1284
1280
|
}
|
|
1281
|
+
async searchAbilities(params) {
|
|
1282
|
+
const response = await this.client.get('/abilities/search', { params: params || {} });
|
|
1283
|
+
return response.data;
|
|
1284
|
+
}
|
|
1285
1285
|
/**
|
|
1286
1286
|
* Invoke an inhaled ability via the Respira safety proxy.
|
|
1287
1287
|
*
|
|
@@ -1553,8 +1553,22 @@ export class WordPressClient {
|
|
|
1553
1553
|
/**
|
|
1554
1554
|
* Replace the content of an existing Divi Theme Builder layout.
|
|
1555
1555
|
*/
|
|
1556
|
+
/**
|
|
1557
|
+
* `confirm_live_edit` is required by the plugin on the first call: a global
|
|
1558
|
+
* layout rewrites every page that uses it and goes live with no staging step,
|
|
1559
|
+
* so the first attempt is refused with `respira_tb_live_edit_confirmation_required`
|
|
1560
|
+
* and the caller has to come back having acknowledged the blast radius.
|
|
1561
|
+
*
|
|
1562
|
+
* Threaded through only when the caller actually set it. Sending `false`
|
|
1563
|
+
* explicitly is not the same as not answering, and the plugin reads both.
|
|
1564
|
+
*/
|
|
1556
1565
|
async updateThemeBuilderTemplate(args) {
|
|
1557
|
-
const response = await this.client.post(`/divi/theme-builder/template/${args.layout_id}`, {
|
|
1566
|
+
const response = await this.client.post(`/divi/theme-builder/template/${args.layout_id}`, {
|
|
1567
|
+
structure: args.structure,
|
|
1568
|
+
...(args.confirm_live_edit === undefined
|
|
1569
|
+
? {}
|
|
1570
|
+
: { confirm_live_edit: args.confirm_live_edit }),
|
|
1571
|
+
});
|
|
1558
1572
|
return response.data;
|
|
1559
1573
|
}
|
|
1560
1574
|
/**
|
|
@@ -2693,32 +2707,11 @@ export class WordPressClient {
|
|
|
2693
2707
|
}
|
|
2694
2708
|
if (restRouteFallbackWorked) {
|
|
2695
2709
|
this.lastRestRouteFallbackWorked = true;
|
|
2696
|
-
// Ticket 73b64e82: these probes run over a bare `axios.request()` (see
|
|
2697
|
-
// the `probe()` helper above), not through `this.client`/`this.rootClient`,
|
|
2698
|
-
// so they never pass through the response interceptor that normally
|
|
2699
|
-
// trips the sticky `useRestRouteFallback` flag on a real tool call
|
|
2700
|
-
// (see the success interceptor installed in the constructor, a few
|
|
2701
|
-
// hundred lines up). Without this, diagnoseConnection would *report*
|
|
2702
|
-
// `rest_route_fallback_worked: true` while every subsequent normal tool
|
|
2703
|
-
// call (get_builder_info, list_pages, ...) kept retrying the doomed
|
|
2704
|
-
// pretty path from scratch on this client instance — forcing the user
|
|
2705
|
-
// to hand-set `forceRestRoute: true` even though the diagnostic had
|
|
2706
|
-
// already proven the fallback works. Flip the same flag a real
|
|
2707
|
-
// tool-call fallback would, so the diagnostic's finding actually
|
|
2708
|
-
// sticks for the rest of the session instead of just being reported.
|
|
2709
|
-
if (!this.useRestRouteFallback) {
|
|
2710
|
-
this.useRestRouteFallback = true;
|
|
2711
|
-
if (!this.restRouteFallbackWarned) {
|
|
2712
|
-
this.restRouteFallbackWarned = true;
|
|
2713
|
-
process.stderr.write(`[respira-mcp] Site ${this.siteConfig.name} has REST rewrite shadowing; ` +
|
|
2714
|
-
`falling back to ?rest_route= for this session (confirmed by wordpress_diagnose_connection).\n`);
|
|
2715
|
-
}
|
|
2716
|
-
}
|
|
2717
2710
|
recommendations.push('Pretty `/wp-json/respira/...` path returned HTML but `?rest_route=/respira/...` returned JSON. ' +
|
|
2718
2711
|
'A plugin or theme rewrite rule is shadowing `/wp-json/[anything]` and triggering ' +
|
|
2719
2712
|
'WordPress `redirect_canonical()` (look for `x-redirect-by: WordPress` on the 301). ' +
|
|
2720
|
-
'
|
|
2721
|
-
'`forceRestRoute: true` in the site config to
|
|
2713
|
+
'The MCP server will auto-fall-back to `?rest_route=` for this session — set ' +
|
|
2714
|
+
'`forceRestRoute: true` in the site config to skip the pretty-permalink probe entirely.');
|
|
2722
2715
|
}
|
|
2723
2716
|
return {
|
|
2724
2717
|
success: true,
|
|
@@ -2850,8 +2843,12 @@ export class WordPressClient {
|
|
|
2850
2843
|
* Activate a plugin (EXPERIMENTAL). Approval-gated; echo approval_token
|
|
2851
2844
|
* back on the second call with the same slug to complete.
|
|
2852
2845
|
*/
|
|
2853
|
-
async activatePlugin(slug, approvalToken) {
|
|
2854
|
-
const body =
|
|
2846
|
+
async activatePlugin(slug, approvalToken, forceWithoutProbe) {
|
|
2847
|
+
const body = {};
|
|
2848
|
+
if (approvalToken)
|
|
2849
|
+
body.approval_token = approvalToken;
|
|
2850
|
+
if (forceWithoutProbe)
|
|
2851
|
+
body.force_without_probe = true;
|
|
2855
2852
|
const response = await this.client.post(`/plugins/${slug}/activate`, body);
|
|
2856
2853
|
return response.data;
|
|
2857
2854
|
}
|
|
@@ -3109,13 +3106,14 @@ export class WordPressClient {
|
|
|
3109
3106
|
return response.data;
|
|
3110
3107
|
}
|
|
3111
3108
|
/**
|
|
3112
|
-
*
|
|
3113
|
-
*
|
|
3114
|
-
*
|
|
3115
|
-
*
|
|
3116
|
-
*
|
|
3117
|
-
*
|
|
3118
|
-
*
|
|
3109
|
+
* `force` skips the trash and deletes permanently. `approvalToken` carries a
|
|
3110
|
+
* governance approval back to the plugin on the retry after a first call was
|
|
3111
|
+
* refused.
|
|
3112
|
+
*
|
|
3113
|
+
* Both are optional and, as of 8.0, no caller in server.ts passes either:
|
|
3114
|
+
* the delete_media tool schema does not expose them. The parameters are kept
|
|
3115
|
+
* because they are the shipped signature and the plugin route accepts them.
|
|
3116
|
+
* Wiring the tool schema is a separate change, not a release-eve one.
|
|
3119
3117
|
*/
|
|
3120
3118
|
async deleteMedia(id, force, approvalToken) {
|
|
3121
3119
|
const params = {};
|
|
@@ -3351,6 +3349,15 @@ export class WordPressClient {
|
|
|
3351
3349
|
const response = await this.client.post('/woocommerce/storefront/update-checkout-layout', data);
|
|
3352
3350
|
return response.data;
|
|
3353
3351
|
}
|
|
3352
|
+
// WooCommerce Add-on 8.0: product-card field addressing
|
|
3353
|
+
async woocommerceAnalyzeStorefrontCard(params) {
|
|
3354
|
+
const response = await this.client.get('/woocommerce/storefront/analyze-card', { params: params || {} });
|
|
3355
|
+
return response.data;
|
|
3356
|
+
}
|
|
3357
|
+
async woocommerceUpdateStorefrontCardField(data) {
|
|
3358
|
+
const response = await this.client.post('/woocommerce/storefront/update-card-field', data);
|
|
3359
|
+
return response.data;
|
|
3360
|
+
}
|
|
3354
3361
|
// WooCommerce Add-on v3.0 — variations and attributes
|
|
3355
3362
|
async woocommerceSetProductAttributes(id, data) {
|
|
3356
3363
|
const response = await this.client.post(`/woocommerce/products/${id}/attributes`, data);
|
|
@@ -3527,6 +3534,10 @@ export class WordPressClient {
|
|
|
3527
3534
|
const response = await this.client.get('/woocommerce/feeds/status');
|
|
3528
3535
|
return response.data;
|
|
3529
3536
|
}
|
|
3537
|
+
async woocommerceRepairFeedScheduler(data) {
|
|
3538
|
+
const response = await this.client.post('/woocommerce/feeds/repair', data || {});
|
|
3539
|
+
return response.data;
|
|
3540
|
+
}
|
|
3530
3541
|
async woocommerceValidateFeed(params) {
|
|
3531
3542
|
const response = await this.client.get('/woocommerce/feeds/validate', { params: params || {} });
|
|
3532
3543
|
return response.data;
|