@respira/wordpress-mcp-server 8.0.1 → 8.0.3

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 CHANGED
@@ -768,6 +768,8 @@ npm cache clean --force
768
768
 
769
769
  API key validation happens server-side in the WordPress plugin. The MCP server passes credentials but does not store or validate them.
770
770
 
771
+ `wordpress_run_security_audit` performs bounded read-only evidence collection for reviewed advisories. `wordpress_update_core_security` installs only an exact, catalog-approved security patch after backup confirmation and explicit two-step approval, and returns post-update verification evidence.
772
+
771
773
  Report vulnerabilities to security@respira.press.
772
774
 
773
775
  ---
package/TOOL_CATALOG.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <!-- Generated by scripts/check-tool-registry-parity.mjs. Do not edit by hand. -->
4
4
 
5
- Generated catalog: **169 core tools**, **105 WooCommerce tools**, **274 total**.
5
+ Generated catalog: **171 core tools**, **105 WooCommerce tools**, **276 total**.
6
6
 
7
7
  The runtime applies site context and per-site governance, so an individual connection may expose a smaller relevant subset.
8
8
 
@@ -144,6 +144,7 @@ The runtime applies site context and per-site governance, so an individual conne
144
144
  - `wordpress_restore_session` — write; hosted/stdio
145
145
  - `wordpress_restore_snapshot` — write; native + hosted
146
146
  - `wordpress_run_pagespeed_audit` — read; hosted/stdio
147
+ - `wordpress_run_security_audit` — read; native + hosted
147
148
  - `wordpress_scan_page_accessibility` — read; hosted/stdio
148
149
  - `wordpress_search_abilities` — read; native + hosted
149
150
  - `wordpress_search_docs` — read; hosted/stdio
@@ -152,6 +153,7 @@ The runtime applies site context and per-site governance, so an individual conne
152
153
  - `wordpress_switch_site` — write; native + hosted
153
154
  - `wordpress_update_acf_field_group` — write; hosted/stdio
154
155
  - `wordpress_update_comment` — write; native + hosted
156
+ - `wordpress_update_core_security` — write; native + hosted
155
157
  - `wordpress_update_custom_post` — write; native + hosted
156
158
  - `wordpress_update_design_token` — write; native + hosted
157
159
  - `wordpress_update_element` — write; native + hosted
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=config-write-target.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-write-target.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/config-write-target.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * redeem must write the config where the server will READ it.
3
+ *
4
+ * The write target used to be hardcoded to homedir()/.respira/config.json,
5
+ * ignoring RESPIRA_CONFIG_FILE. On a plain machine those agree, which is why it
6
+ * looked correct. They diverge exactly where it hurts: a host that sets a
7
+ * different path, or a sandbox whose home is not the user's home. Redeem then
8
+ * reported success, wrote a real file, and the next boot read elsewhere and
9
+ * found nothing, which surfaced to the customer as "the installer could not
10
+ * write it itself".
11
+ */
12
+ import { test } from 'node:test';
13
+ import assert from 'node:assert/strict';
14
+ import { homedir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { expandHomePath } from '../config.js';
17
+ // The resolution rule redeem and loadConfig must agree on, asserted directly.
18
+ function writeTarget(env) {
19
+ const explicit = env?.trim();
20
+ return explicit ? expandHomePath(explicit) : join(homedir(), '.respira', 'config.json');
21
+ }
22
+ test('with no env set, writes the default location', () => {
23
+ assert.equal(writeTarget(undefined), join(homedir(), '.respira', 'config.json'));
24
+ });
25
+ test('RESPIRA_CONFIG_FILE wins over the default', () => {
26
+ assert.equal(writeTarget('/opt/respira/config.json'), '/opt/respira/config.json');
27
+ });
28
+ test('write target matches read target for the token Cowork ships', () => {
29
+ // The exact value in the Cowork MCP config. If these two ever disagree the
30
+ // config is written somewhere the server never looks.
31
+ const shipped = '${HOME}/.respira/config.json';
32
+ assert.equal(writeTarget(shipped), expandHomePath(shipped));
33
+ assert.ok(!writeTarget(shipped).includes('${'), 'target must be fully resolved');
34
+ });
35
+ test('a Windows path with a space survives', () => {
36
+ const win = 'C:\\Users\\Lebowski Firma\\.respira\\config.json';
37
+ assert.equal(writeTarget(win), win);
38
+ });
39
+ //# sourceMappingURL=config-write-target.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-write-target.test.js","sourceRoot":"","sources":["../../src/__tests__/config-write-target.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,8EAA8E;AAC9E,SAAS,WAAW,CAAC,GAAuB;IAC1C,MAAM,QAAQ,GAAG,GAAG,EAAE,IAAI,EAAE,CAAC;IAC7B,OAAO,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,aAAa,CAAC,CAAC;AAC1F,CAAC;AAED,IAAI,CAAC,8CAA8C,EAAE,GAAG,EAAE;IACxD,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,aAAa,CAAC,CAAC,CAAC;AACnF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,2CAA2C,EAAE,GAAG,EAAE;IACrD,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,0BAA0B,CAAC,EAAE,0BAA0B,CAAC,CAAC;AACpF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,6DAA6D,EAAE,GAAG,EAAE;IACvE,2EAA2E;IAC3E,sDAAsD;IACtD,MAAM,OAAO,GAAG,8BAA8B,CAAC;IAC/C,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5D,MAAM,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,+BAA+B,CAAC,CAAC;AACnF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,sCAAsC,EAAE,GAAG,EAAE;IAChD,MAAM,GAAG,GAAG,kDAAkD,CAAC;IAC/D,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;AACtC,CAAC,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAqBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAsQzE,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,KAAK,CAA2C;IACxD,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB,CAAS;IACpC,iFAAiF;IACjF,OAAO,CAAC,iBAAiB,CAAK;IAC9B,iFAAiF;IACjF,OAAO,CAAC,mBAAmB,CAAgC;IAE3D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAsB;IAEhE;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;gBA4Bb,WAAW,EAAE,mBAAmB,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE;IAiVvE,OAAO,CAAC,cAAc;IAItB;;;;;;;;;OASG;IACH,OAAO,CAAC,oBAAoB;IAkB5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IA4BrB,gEAAgE;IAChE,OAAO,CAAC,aAAa;IAUrB;;;;;;OAMG;YACW,UAAU;IA2CxB;;;;;;;;;;;;;;OAcG;YACW,WAAW;IAiJzB;;;;;;;;;OASG;YACW,kBAAkB;IAiHhC;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;YACW,qBAAqB;YAiBrB,kBAAkB;IA6FhC,yFAAyF;IACzF,OAAO,CAAC,gBAAgB;IASxB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,6BAA6B;IA0BrC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA+BjC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YAmQP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IAusGtB;;;;;;OAMG;IACH;;;;;;;;;;;;;oDAagD;IAChD,OAAO,CAAC,mBAAmB,CAAoD;IAC/E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAAU;IAC1D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAS;IAC1D;;mDAE+C;IAC/C,OAAO,CAAC,0BAA0B,CAAoC;IAEtE;;;;;;;;OAQG;YACW,0BAA0B;YAyC1B,oBAAoB;YA6CpB,2BAA2B;IAQzC;;;;OAIG;YACW,cAAc;IAQ5B,OAAO,CAAC,mBAAmB;IAo7C3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAe3C;IAEF;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;YAmCpB,cAAc;IA4G5B,oEAAoE;IACpE,OAAO,CAAC,iBAAiB;IAoBzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,eAAe;YAuCT,gBAAgB;IA2yC9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA8YzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA6UxB,GAAG;CAyCV"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAqBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAsQzE,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,KAAK,CAA2C;IACxD,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB,CAAS;IACpC,iFAAiF;IACjF,OAAO,CAAC,iBAAiB,CAAK;IAC9B,iFAAiF;IACjF,OAAO,CAAC,mBAAmB,CAAgC;IAE3D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAsB;IAEhE;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;gBA4Bb,WAAW,EAAE,mBAAmB,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE;IAiVvE,OAAO,CAAC,cAAc;IAItB;;;;;;;;;OASG;IACH,OAAO,CAAC,oBAAoB;IAkB5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IA4BrB,gEAAgE;IAChE,OAAO,CAAC,aAAa;IAUrB;;;;;;OAMG;YACW,UAAU;IA2CxB;;;;;;;;;;;;;;OAcG;YACW,WAAW;IAiJzB;;;;;;;;;OASG;YACW,kBAAkB;IAyIhC;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;YACW,qBAAqB;YAiBrB,kBAAkB;IA6FhC,yFAAyF;IACzF,OAAO,CAAC,gBAAgB;IASxB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,6BAA6B;IA0BrC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA+BjC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YAmQP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IAuvGtB;;;;;;OAMG;IACH;;;;;;;;;;;;;oDAagD;IAChD,OAAO,CAAC,mBAAmB,CAAoD;IAC/E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAAU;IAC1D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAS;IAC1D;;mDAE+C;IAC/C,OAAO,CAAC,0BAA0B,CAAoC;IAEtE;;;;;;;;OAQG;YACW,0BAA0B;YAyC1B,oBAAoB;YA6CpB,2BAA2B;IAQzC;;;;OAIG;YACW,cAAc;IAQ5B,OAAO,CAAC,mBAAmB;IAo7C3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAe3C;IAEF;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;YAmCpB,cAAc;IA4G5B,oEAAoE;IACpE,OAAO,CAAC,iBAAiB;IAoBzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,eAAe;YAuCT,gBAAgB;IAo0C9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA8YzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA6UxB,GAAG;CAyCV"}
package/dist/server.js CHANGED
@@ -1009,17 +1009,36 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
1009
1009
  // Write ~/.respira/config.json with the canonical shape. Make the
1010
1010
  // directory if absent. If a file already exists, back it up first
1011
1011
  // so the user can recover the previous config if they want.
1012
+ // Write to the path this server will actually READ on next boot.
1013
+ //
1014
+ // This used to hardcode homedir()/.respira/config.json and ignore
1015
+ // RESPIRA_CONFIG_FILE entirely. On a plain machine the two agree, so it
1016
+ // looked fine. They diverge exactly where it hurts: when the host sets
1017
+ // RESPIRA_CONFIG_FILE to somewhere else, or when the server runs in a
1018
+ // sandbox whose home is not the home the user is looking at. In that case
1019
+ // redeem reported success, wrote a real file, and the next boot read a
1020
+ // different path and found nothing. The user is then told to place the
1021
+ // file by hand, which is the "the installer could not write it itself"
1022
+ // complaint, and the honest description of it is that we wrote to the
1023
+ // wrong place and blamed the environment.
1024
+ //
1025
+ // RESPIRA_CONFIG_FILE wins, expanded the same way loadConfig expands it so
1026
+ // the two can never disagree about what ${HOME} means.
1012
1027
  let configPath;
1013
1028
  try {
1014
1029
  const { writeFileSync, mkdirSync, existsSync, copyFileSync } = await import('node:fs');
1015
- const { join } = await import('node:path');
1030
+ const { join, dirname } = await import('node:path');
1016
1031
  const { homedir } = await import('node:os');
1017
- const dir = join(homedir(), '.respira');
1032
+ const { expandHomePath } = await import('./config.js');
1033
+ const explicit = process.env.RESPIRA_CONFIG_FILE?.trim();
1034
+ configPath = explicit
1035
+ ? expandHomePath(explicit)
1036
+ : join(homedir(), '.respira', 'config.json');
1037
+ const dir = dirname(configPath);
1018
1038
  if (!existsSync(dir))
1019
1039
  mkdirSync(dir, { recursive: true });
1020
- configPath = join(dir, 'config.json');
1021
1040
  if (existsSync(configPath)) {
1022
- const backupPath = join(dir, `config.json.bak.${Date.now()}`);
1041
+ const backupPath = `${configPath}.bak.${Date.now()}`;
1023
1042
  try {
1024
1043
  copyFileSync(configPath, backupPath);
1025
1044
  }
@@ -1028,10 +1047,13 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
1028
1047
  writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
1029
1048
  }
1030
1049
  catch (err) {
1050
+ const attempted = process.env.RESPIRA_CONFIG_FILE?.trim() || '~/.respira/config.json';
1031
1051
  return {
1032
1052
  success: false,
1033
1053
  error: 'write_failed',
1034
- message: `Token redeemed but could not write ~/.respira/config.json (${err?.message || 'unknown error'}). Save manually from respira.press/dashboard/mcp.`,
1054
+ // Name the ABSOLUTE path that failed. "could not write
1055
+ // ~/.respira/config.json" sends someone looking in the wrong home.
1056
+ message: `Token redeemed but could not write the config to ${attempted} (${err?.message || 'unknown error'}). Download it from respira.press/dashboard/mcp and place it at that exact path.`,
1035
1057
  };
1036
1058
  }
1037
1059
  const sites = config.sites.map((s) => ({
@@ -1039,7 +1061,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
1039
1061
  url: s.url,
1040
1062
  }));
1041
1063
  const hasConfigB64 = Boolean(process.env.RESPIRA_CONFIG_B64);
1042
- const baseMessage = `Connected ${sites.length} site${sites.length === 1 ? '' : 's'} via the Respira Cowork token. The config was written to ~/.respira/config.json. Restart this Cowork chat (or open a new one) so the MCP server picks up the new sites.`;
1064
+ const baseMessage = `Connected ${sites.length} site${sites.length === 1 ? '' : 's'} via the Respira Cowork token. The config was written to ${configPath}. Restart this Cowork chat (or open a new one) so the MCP server picks up the new sites.`;
1043
1065
  const message = hasConfigB64
1044
1066
  ? `${baseMessage} IMPORTANT: your MCP server is currently configured via the RESPIRA_CONFIG_B64 environment variable, which takes precedence over the file on disk. To activate the new site, update RESPIRA_CONFIG_B64 in your agent's MCP server config with the base64-encoded contents of the new ~/.respira/config.json file, then restart the agent.`
1045
1067
  : baseMessage;
@@ -3280,6 +3302,54 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
3280
3302
  },
3281
3303
  readOnlyHint: true,
3282
3304
  },
3305
+ // Safety & Security center
3306
+ {
3307
+ name: 'wordpress_run_security_audit',
3308
+ description: 'Run a bounded, read-only WordPress security evidence pass. Returns authoritative WordPress/WooCommerce versions, official core checksum mismatches, administrators, standard and must-use plugins, WordPress cron hooks, known advisory indicators, scan coverage and explicit host-level limitations. Set deep_scan=true to inspect uploads and must-use plugin paths for executable files. This tool never deletes or rewrites anything.',
3309
+ inputSchema: {
3310
+ type: 'object',
3311
+ properties: {
3312
+ advisory_ids: {
3313
+ type: 'array',
3314
+ items: { type: 'string' },
3315
+ description: 'Optional reviewed advisory IDs to record as the audit scope, for example wordpress-wp2shell-2026.',
3316
+ },
3317
+ deep_scan: {
3318
+ type: 'boolean',
3319
+ default: false,
3320
+ description: 'Inspect uploads and must-use plugin paths with strict file, time, symlink and result limits.',
3321
+ },
3322
+ },
3323
+ },
3324
+ readOnlyHint: true,
3325
+ },
3326
+ {
3327
+ name: 'wordpress_update_core_security',
3328
+ description: 'Update WordPress core to an exact, catalog-approved security release on the current release branch. Requires a recent recoverable backup, then explicit two-step approval. The first call returns respira_approval_required and an approval_token; repeat the same arguments with that token. The result verifies the running version, REST/frontend boot and official checksums. WordPress core has no automatic rollback, and the receipt says so.',
3329
+ inputSchema: {
3330
+ type: 'object',
3331
+ properties: {
3332
+ advisory_id: {
3333
+ type: 'string',
3334
+ description: 'Reviewed security advisory ID, currently wordpress-wp2shell-2026.',
3335
+ },
3336
+ target_version: {
3337
+ type: 'string',
3338
+ description: 'Exact approved patch version on the current branch: 6.8.6, 6.9.5 or 7.0.2 for wp2shell.',
3339
+ },
3340
+ backup_confirmed: {
3341
+ type: 'boolean',
3342
+ description: 'True only after the operator confirms a recent recoverable backup exists.',
3343
+ },
3344
+ approval_token: {
3345
+ type: 'string',
3346
+ description: 'Approval token returned by the first blocked call. Echo it unchanged on the second call.',
3347
+ },
3348
+ },
3349
+ required: ['advisory_id', 'target_version', 'backup_confirmed'],
3350
+ },
3351
+ idempotentHint: true,
3352
+ },
3283
3353
  // Accessibility tools
3284
3354
  {
3285
3355
  name: 'wordpress_list_accessibility_scans',
@@ -6992,6 +7062,15 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
6992
7062
  return await client.analyzeAEO(args.page_id);
6993
7063
  case 'wordpress_check_structured_data':
6994
7064
  return await client.checkStructuredData(args.page_id);
7065
+ case 'wordpress_run_security_audit':
7066
+ return await client.runSecurityAudit(args.advisory_ids || [], args.deep_scan === true);
7067
+ case 'wordpress_update_core_security':
7068
+ return await client.updateCoreSecurity({
7069
+ advisoryId: args.advisory_id,
7070
+ targetVersion: args.target_version,
7071
+ backupConfirmed: args.backup_confirmed === true,
7072
+ approvalToken: args.approval_token,
7073
+ });
6995
7074
  // Accessibility
6996
7075
  case 'wordpress_list_accessibility_scans':
6997
7076
  return await client.listAccessibilityScans({
@@ -7608,12 +7687,26 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
7608
7687
  // Widget shortcuts and Bricks tools — dynamic dispatch.
7609
7688
  default: {
7610
7689
  // Check if this is a Bricks deep tool.
7611
- const bricksResult = await dispatchBricksTool(name, args, (method, path, body) => this.currentSite.callRestV2(method, path, body));
7690
+ //
7691
+ // `client` (resolved above via resolveClient(args), the whole switch
7692
+ // statement's site_id-aware client) — NOT this.currentSite, which is
7693
+ // only the globally active site. Every case above this default: uses
7694
+ // `client.callRestV2`; this fallback used `this.currentSite!` instead,
7695
+ // so any respira_*_bricks_typography (and every other tool that only
7696
+ // resolves here, e.g. the rest of bricks-tools.ts / elementor-tools.ts)
7697
+ // silently ignored a supplied site_id and read/wrote the active site
7698
+ // instead — while still reporting the requested site_id back in the
7699
+ // response, since that came from `args`, not from which site actually
7700
+ // executed the call. Reported by E.M. (wellnessist) 2026-07-29: a
7701
+ // `respira_update_bricks_typography` call with site_id set to site B
7702
+ // (site A active) overwrote site A's global variables/theme styles,
7703
+ // a cross-site write with no rollback for either store.
7704
+ const bricksResult = await dispatchBricksTool(name, args, (method, path, body) => client.callRestV2(method, path, body));
7612
7705
  if (bricksResult !== null) {
7613
7706
  return bricksResult;
7614
7707
  }
7615
- // Check if this is an Elementor deep tool.
7616
- const elementorResult = await dispatchElementorTool(name, args, (method, path, body) => this.currentSite.callRestV2(method, path, body));
7708
+ // Check if this is an Elementor deep tool. Same site_id fix as above.
7709
+ const elementorResult = await dispatchElementorTool(name, args, (method, path, body) => client.callRestV2(method, path, body));
7617
7710
  if (elementorResult !== null) {
7618
7711
  return elementorResult;
7619
7712
  }