@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.
Files changed (35) hide show
  1. package/README.md +43 -23
  2. package/TOOL_CATALOG.md +288 -0
  3. package/dist/__tests__/config-path-expansion.test.d.ts +2 -0
  4. package/dist/__tests__/config-path-expansion.test.d.ts.map +1 -0
  5. package/dist/__tests__/config-path-expansion.test.js +63 -0
  6. package/dist/__tests__/config-path-expansion.test.js.map +1 -0
  7. package/dist/__tests__/rest-route-fallback.test.js +0 -64
  8. package/dist/__tests__/rest-route-fallback.test.js.map +1 -1
  9. package/dist/__tests__/site-token-401-error-classification.test.d.ts +0 -19
  10. package/dist/__tests__/site-token-401-error-classification.test.d.ts.map +1 -1
  11. package/dist/__tests__/site-token-401-error-classification.test.js +20 -60
  12. package/dist/__tests__/site-token-401-error-classification.test.js.map +1 -1
  13. package/dist/__tests__/tools-list-protocol-smoke.test.d.ts +2 -0
  14. package/dist/__tests__/tools-list-protocol-smoke.test.d.ts.map +1 -0
  15. package/dist/__tests__/tools-list-protocol-smoke.test.js +78 -0
  16. package/dist/__tests__/tools-list-protocol-smoke.test.js.map +1 -0
  17. package/dist/config.d.ts +26 -0
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +80 -9
  20. package/dist/config.js.map +1 -1
  21. package/dist/elementor-tools.js +2 -2
  22. package/dist/elementor-tools.js.map +1 -1
  23. package/dist/server.d.ts.map +1 -1
  24. package/dist/server.js +750 -19
  25. package/dist/server.js.map +1 -1
  26. package/dist/usage-emitter.d.ts +7 -0
  27. package/dist/usage-emitter.d.ts.map +1 -1
  28. package/dist/usage-emitter.js +34 -0
  29. package/dist/usage-emitter.js.map +1 -1
  30. package/dist/wordpress-client.d.ts +53 -42
  31. package/dist/wordpress-client.d.ts.map +1 -1
  32. package/dist/wordpress-client.js +105 -94
  33. package/dist/wordpress-client.js.map +1 -1
  34. package/package.json +5 -3
  35. package/tool-capabilities.json +4122 -0
@@ -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}`, { structure: args.structure });
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
- 'This session has switched to `?rest_route=` automatically — set ' +
2721
- '`forceRestRoute: true` in the site config to make that permanent and skip this probe on future connects.');
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 = approvalToken ? { approval_token: approvalToken } : undefined;
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
- * Delete a media attachment. Approval-gated since v7.1.0-beta.1: the
3113
- * first call comes back with `code: respira_approval_required` and a
3114
- * fresh `approval_token`; the caller re-sends the same id with that
3115
- * token to confirm. Ticket 927aa7ed — this method used to only forward
3116
- * `id`, silently dropping `force` and `approvalToken`, so the WP side
3117
- * never saw a token on the "confirm" call and looped forever minting
3118
- * new ones. Mirrors `deletePage`'s params shape (see comment above it).
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;