@respira/wordpress-mcp-server 7.6.1 → 8.0.0
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 +59 -31
- package/TOOL_CATALOG.md +288 -0
- package/dist/__tests__/pagespeed-error-classification.test.d.ts +2 -0
- package/dist/__tests__/pagespeed-error-classification.test.d.ts.map +1 -0
- package/dist/__tests__/pagespeed-error-classification.test.js +52 -0
- package/dist/__tests__/pagespeed-error-classification.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.map +1 -1
- package/dist/config.js +20 -7
- 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 +758 -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 +41 -1
- package/dist/usage-emitter.js.map +1 -1
- package/dist/wordpress-client.d.ts +59 -42
- package/dist/wordpress-client.d.ts.map +1 -1
- package/dist/wordpress-client.js +149 -135
- package/dist/wordpress-client.js.map +1 -1
- package/package.json +5 -8
- 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;
|
|
@@ -872,10 +868,10 @@ export class WordPressClient {
|
|
|
872
868
|
: 'The site returned an HTML error page instead of a WordPress REST API response. ' +
|
|
873
869
|
'A firewall, CDN, or maintenance mode may be blocking API access. ' +
|
|
874
870
|
'Verify that ' + this.siteConfig.url + '/wp-json/respira/v1/context/site-info is accessible in your browser.';
|
|
875
|
-
return
|
|
871
|
+
return this.codedError(`Site not reachable: ${this.siteConfig.url} returned a ${status} response from the web server (not WordPress).\n\n${hint}`, 'http_401_non_wordpress');
|
|
876
872
|
}
|
|
877
873
|
const reason = data?.message || data?.code || 'Invalid API key';
|
|
878
|
-
const authError =
|
|
874
|
+
const authError = this.codedError(`Authentication failed: ${reason}`, typeof data?.code === 'string' && data.code ? data.code : 'http_401', data);
|
|
879
875
|
// Same rationale as in formatErrorWithInstructions / the generic
|
|
880
876
|
// catch-all below: stamp the WP_Error code (respira_invalid_site_token,
|
|
881
877
|
// respira_token_stale, license_inactive, ...) onto error.name so
|
|
@@ -887,56 +883,47 @@ export class WordPressClient {
|
|
|
887
883
|
// its machine-readable code the moment it crossed into the npm
|
|
888
884
|
// server — telemetry only recorded the tool name, not why it failed.
|
|
889
885
|
// Ticket fc4fa49e.
|
|
890
|
-
if (typeof data?.code === 'string' && data.code) {
|
|
891
|
-
authError.name = data.code.slice(0, 80);
|
|
892
|
-
}
|
|
893
886
|
return authError;
|
|
894
887
|
}
|
|
895
888
|
else if (status === 429) {
|
|
896
|
-
return
|
|
889
|
+
return this.codedError('Rate limit exceeded. Please wait before making more requests.', typeof data?.code === 'string' && data.code ? data.code : 'http_429', data);
|
|
897
890
|
}
|
|
898
891
|
else if (status === 403) {
|
|
899
892
|
if (isNonWordPressResponse) {
|
|
900
|
-
return
|
|
893
|
+
return this.codedError(`Site blocked: ${this.siteConfig.url} returned a 403 Forbidden from the web server (not WordPress).\n\n` +
|
|
901
894
|
'A firewall, security plugin (e.g. Wordfence, Sucuri), or CDN rule is blocking REST API requests. ' +
|
|
902
|
-
'Whitelist the /wp-json/respira/* path or the MCP server\'s User-Agent in your security settings.');
|
|
895
|
+
'Whitelist the /wp-json/respira/* path or the MCP server\'s User-Agent in your security settings.', 'http_403_non_wordpress');
|
|
903
896
|
}
|
|
904
897
|
// Format error with instructions if available
|
|
905
898
|
return this.formatErrorWithInstructions(data);
|
|
906
899
|
}
|
|
907
900
|
else if (status >= 500) {
|
|
908
901
|
if (isNonWordPressResponse) {
|
|
909
|
-
return
|
|
902
|
+
return this.codedError(`Server error: ${this.siteConfig.url} returned a ${status} error from the web server (not WordPress).\n\n` +
|
|
910
903
|
'The site may be down, misconfigured, or behind a proxy that is failing. ' +
|
|
911
|
-
'Check that the site loads normally in a browser first.');
|
|
904
|
+
'Check that the site loads normally in a browser first.', `http_${status}_non_wordpress`);
|
|
912
905
|
}
|
|
913
906
|
const serverDetails = this.extractServerErrorDetails(data);
|
|
914
907
|
const message = data?.message || 'Internal server error';
|
|
915
908
|
const detailsSuffix = serverDetails ? `\n\nDebug details: ${serverDetails}` : '';
|
|
916
|
-
return
|
|
909
|
+
return this.codedError(`WordPress server error (${status}): ${message}${detailsSuffix}`, typeof data?.code === 'string' && data.code ? data.code : `http_${status}`, data);
|
|
917
910
|
}
|
|
918
911
|
// Check if error has instructions data
|
|
919
912
|
if (data?.data && (data.data.instructions || data.data.links)) {
|
|
920
913
|
return this.formatErrorWithInstructions(data);
|
|
921
914
|
}
|
|
922
915
|
if (isNonWordPressResponse) {
|
|
923
|
-
return
|
|
916
|
+
return this.codedError(`Unexpected response: ${this.siteConfig.url} returned HTTP ${status} with an HTML page instead of a WordPress REST API response.\n\n` +
|
|
924
917
|
'The Respira plugin may not be active, the REST API may be disabled, or a proxy/firewall is intercepting requests. ' +
|
|
925
|
-
'Verify that ' + this.siteConfig.url + '/wp-json/respira/v1/context/site-info is accessible in your browser.');
|
|
918
|
+
'Verify that ' + this.siteConfig.url + '/wp-json/respira/v1/context/site-info is accessible in your browser.', `http_${status}_non_wordpress`);
|
|
926
919
|
}
|
|
927
|
-
const apiError =
|
|
920
|
+
const apiError = this.codedError(`API error (${status}): ${data?.message || error.message}`, typeof data?.code === 'string' && data.code ? data.code : `http_${status}`, data);
|
|
928
921
|
// Promote the WP_Error code (e.g. 'respira_no_builder',
|
|
929
922
|
// 'respira_element_not_found') to error.name so the MCP usage emitter
|
|
930
923
|
// records something diagnostic instead of the literal 'Error'. Without
|
|
931
924
|
// this, every WP-side failure collapses to error_code='Error' in
|
|
932
925
|
// mcp_tool_events and the admin/mcp-quality dashboard cannot tell
|
|
933
926
|
// 'wrong target' from 'no builder detected' from 'permission denied'.
|
|
934
|
-
if (typeof data?.code === 'string' && data.code) {
|
|
935
|
-
apiError.name = data.code.slice(0, 80);
|
|
936
|
-
}
|
|
937
|
-
else {
|
|
938
|
-
apiError.name = `http_${status}`;
|
|
939
|
-
}
|
|
940
927
|
// v6.21.2: preserve the structured WP_Error envelope so the agent sees
|
|
941
928
|
// the rich fields (missing_args, required_path_keys, hint, instructions,
|
|
942
929
|
// registered_abilities_sample, destructive_tools, available_types, ...).
|
|
@@ -948,26 +935,6 @@ export class WordPressClient {
|
|
|
948
935
|
// Attach the WP wire envelope as a non-enumerable property on the
|
|
949
936
|
// Error so server.ts can surface it in the tool result without
|
|
950
937
|
// accidentally duplicating fields the message already carries.
|
|
951
|
-
const errorEnvelope = {};
|
|
952
|
-
if (typeof data?.code === 'string')
|
|
953
|
-
errorEnvelope.code = data.code;
|
|
954
|
-
if (data?.data && typeof data.data === 'object') {
|
|
955
|
-
// Spread the structured fields. `status` would duplicate the HTTP code,
|
|
956
|
-
// skip it.
|
|
957
|
-
for (const [k, v] of Object.entries(data.data)) {
|
|
958
|
-
if (k === 'status')
|
|
959
|
-
continue;
|
|
960
|
-
errorEnvelope[k] = v;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
if (Object.keys(errorEnvelope).length > 0) {
|
|
964
|
-
Object.defineProperty(apiError, 'respiraErrorEnvelope', {
|
|
965
|
-
value: errorEnvelope,
|
|
966
|
-
enumerable: false,
|
|
967
|
-
configurable: false,
|
|
968
|
-
writable: false,
|
|
969
|
-
});
|
|
970
|
-
}
|
|
971
938
|
// v6.19.2: when WordPress reports rest_no_route, enrich the message
|
|
972
939
|
// with the attempted HTTP method and request path. Without this,
|
|
973
940
|
// the /mcp-quality dashboard sees the generic WP string "No route
|
|
@@ -1061,7 +1028,39 @@ export class WordPressClient {
|
|
|
1061
1028
|
netError.name = 'respira_write_outcome_unknown';
|
|
1062
1029
|
return netError;
|
|
1063
1030
|
}
|
|
1064
|
-
return
|
|
1031
|
+
return this.codedError(`Unknown error: ${error.message}`, 'unknown_error');
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Create a telemetry-safe Error while preserving the structured WP_Error
|
|
1035
|
+
* envelope. Every HTTP/proxy/upstream branch must use this helper so useful
|
|
1036
|
+
* PageSpeed and hosting failures never collapse into `<tool>_unclassified`.
|
|
1037
|
+
*/
|
|
1038
|
+
codedError(message, code, data) {
|
|
1039
|
+
const coded = new Error(message);
|
|
1040
|
+
const safeCode = String(code || 'unknown_error')
|
|
1041
|
+
.toLowerCase()
|
|
1042
|
+
.replace(/[^a-z0-9_:-]/g, '_')
|
|
1043
|
+
.slice(0, 80);
|
|
1044
|
+
coded.name = safeCode || 'unknown_error';
|
|
1045
|
+
const errorEnvelope = {};
|
|
1046
|
+
if (typeof data?.code === 'string' && data.code) {
|
|
1047
|
+
errorEnvelope.code = data.code;
|
|
1048
|
+
}
|
|
1049
|
+
if (data?.data && typeof data.data === 'object') {
|
|
1050
|
+
for (const [key, value] of Object.entries(data.data)) {
|
|
1051
|
+
if (key !== 'status')
|
|
1052
|
+
errorEnvelope[key] = value;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (Object.keys(errorEnvelope).length > 0) {
|
|
1056
|
+
Object.defineProperty(coded, 'respiraErrorEnvelope', {
|
|
1057
|
+
value: errorEnvelope,
|
|
1058
|
+
enumerable: false,
|
|
1059
|
+
configurable: false,
|
|
1060
|
+
writable: false,
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
return coded;
|
|
1065
1064
|
}
|
|
1066
1065
|
/**
|
|
1067
1066
|
* Extract concise server-side diagnostics from a failed WP response.
|
|
@@ -1279,6 +1278,10 @@ export class WordPressClient {
|
|
|
1279
1278
|
const response = await this.client.get('/abilities/gap-report');
|
|
1280
1279
|
return response.data;
|
|
1281
1280
|
}
|
|
1281
|
+
async searchAbilities(params) {
|
|
1282
|
+
const response = await this.client.get('/abilities/search', { params: params || {} });
|
|
1283
|
+
return response.data;
|
|
1284
|
+
}
|
|
1282
1285
|
/**
|
|
1283
1286
|
* Invoke an inhaled ability via the Respira safety proxy.
|
|
1284
1287
|
*
|
|
@@ -1550,8 +1553,22 @@ export class WordPressClient {
|
|
|
1550
1553
|
/**
|
|
1551
1554
|
* Replace the content of an existing Divi Theme Builder layout.
|
|
1552
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
|
+
*/
|
|
1553
1565
|
async updateThemeBuilderTemplate(args) {
|
|
1554
|
-
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
|
+
});
|
|
1555
1572
|
return response.data;
|
|
1556
1573
|
}
|
|
1557
1574
|
/**
|
|
@@ -2690,32 +2707,11 @@ export class WordPressClient {
|
|
|
2690
2707
|
}
|
|
2691
2708
|
if (restRouteFallbackWorked) {
|
|
2692
2709
|
this.lastRestRouteFallbackWorked = true;
|
|
2693
|
-
// Ticket 73b64e82: these probes run over a bare `axios.request()` (see
|
|
2694
|
-
// the `probe()` helper above), not through `this.client`/`this.rootClient`,
|
|
2695
|
-
// so they never pass through the response interceptor that normally
|
|
2696
|
-
// trips the sticky `useRestRouteFallback` flag on a real tool call
|
|
2697
|
-
// (see the success interceptor installed in the constructor, a few
|
|
2698
|
-
// hundred lines up). Without this, diagnoseConnection would *report*
|
|
2699
|
-
// `rest_route_fallback_worked: true` while every subsequent normal tool
|
|
2700
|
-
// call (get_builder_info, list_pages, ...) kept retrying the doomed
|
|
2701
|
-
// pretty path from scratch on this client instance — forcing the user
|
|
2702
|
-
// to hand-set `forceRestRoute: true` even though the diagnostic had
|
|
2703
|
-
// already proven the fallback works. Flip the same flag a real
|
|
2704
|
-
// tool-call fallback would, so the diagnostic's finding actually
|
|
2705
|
-
// sticks for the rest of the session instead of just being reported.
|
|
2706
|
-
if (!this.useRestRouteFallback) {
|
|
2707
|
-
this.useRestRouteFallback = true;
|
|
2708
|
-
if (!this.restRouteFallbackWarned) {
|
|
2709
|
-
this.restRouteFallbackWarned = true;
|
|
2710
|
-
process.stderr.write(`[respira-mcp] Site ${this.siteConfig.name} has REST rewrite shadowing; ` +
|
|
2711
|
-
`falling back to ?rest_route= for this session (confirmed by wordpress_diagnose_connection).\n`);
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
2710
|
recommendations.push('Pretty `/wp-json/respira/...` path returned HTML but `?rest_route=/respira/...` returned JSON. ' +
|
|
2715
2711
|
'A plugin or theme rewrite rule is shadowing `/wp-json/[anything]` and triggering ' +
|
|
2716
2712
|
'WordPress `redirect_canonical()` (look for `x-redirect-by: WordPress` on the 301). ' +
|
|
2717
|
-
'
|
|
2718
|
-
'`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.');
|
|
2719
2715
|
}
|
|
2720
2716
|
return {
|
|
2721
2717
|
success: true,
|
|
@@ -2847,8 +2843,12 @@ export class WordPressClient {
|
|
|
2847
2843
|
* Activate a plugin (EXPERIMENTAL). Approval-gated; echo approval_token
|
|
2848
2844
|
* back on the second call with the same slug to complete.
|
|
2849
2845
|
*/
|
|
2850
|
-
async activatePlugin(slug, approvalToken) {
|
|
2851
|
-
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;
|
|
2852
2852
|
const response = await this.client.post(`/plugins/${slug}/activate`, body);
|
|
2853
2853
|
return response.data;
|
|
2854
2854
|
}
|
|
@@ -3106,13 +3106,14 @@ export class WordPressClient {
|
|
|
3106
3106
|
return response.data;
|
|
3107
3107
|
}
|
|
3108
3108
|
/**
|
|
3109
|
-
*
|
|
3110
|
-
*
|
|
3111
|
-
*
|
|
3112
|
-
*
|
|
3113
|
-
*
|
|
3114
|
-
*
|
|
3115
|
-
*
|
|
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.
|
|
3116
3117
|
*/
|
|
3117
3118
|
async deleteMedia(id, force, approvalToken) {
|
|
3118
3119
|
const params = {};
|
|
@@ -3348,6 +3349,15 @@ export class WordPressClient {
|
|
|
3348
3349
|
const response = await this.client.post('/woocommerce/storefront/update-checkout-layout', data);
|
|
3349
3350
|
return response.data;
|
|
3350
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
|
+
}
|
|
3351
3361
|
// WooCommerce Add-on v3.0 — variations and attributes
|
|
3352
3362
|
async woocommerceSetProductAttributes(id, data) {
|
|
3353
3363
|
const response = await this.client.post(`/woocommerce/products/${id}/attributes`, data);
|
|
@@ -3524,6 +3534,10 @@ export class WordPressClient {
|
|
|
3524
3534
|
const response = await this.client.get('/woocommerce/feeds/status');
|
|
3525
3535
|
return response.data;
|
|
3526
3536
|
}
|
|
3537
|
+
async woocommerceRepairFeedScheduler(data) {
|
|
3538
|
+
const response = await this.client.post('/woocommerce/feeds/repair', data || {});
|
|
3539
|
+
return response.data;
|
|
3540
|
+
}
|
|
3527
3541
|
async woocommerceValidateFeed(params) {
|
|
3528
3542
|
const response = await this.client.get('/woocommerce/feeds/validate', { params: params || {} });
|
|
3529
3543
|
return response.data;
|