@respira/wordpress-mcp-server 8.3.24 → 8.3.26

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 (111) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +114 -20
  3. package/TOOL_CATALOG.md +445 -307
  4. package/dist/__tests__/087103bc-the-host-answers-well-known-first.test.d.ts +2 -0
  5. package/dist/__tests__/087103bc-the-host-answers-well-known-first.test.d.ts.map +1 -0
  6. package/dist/__tests__/087103bc-the-host-answers-well-known-first.test.js +73 -0
  7. package/dist/__tests__/087103bc-the-host-answers-well-known-first.test.js.map +1 -0
  8. package/dist/__tests__/0e6bd33c-redeem-preserves-per-site-settings.test.js +3 -2
  9. package/dist/__tests__/0e6bd33c-redeem-preserves-per-site-settings.test.js.map +1 -1
  10. package/dist/__tests__/685d8023-a-403-from-the-web-server-shows-its-evidence.test.d.ts +2 -0
  11. package/dist/__tests__/685d8023-a-403-from-the-web-server-shows-its-evidence.test.d.ts.map +1 -0
  12. package/dist/__tests__/685d8023-a-403-from-the-web-server-shows-its-evidence.test.js +106 -0
  13. package/dist/__tests__/685d8023-a-403-from-the-web-server-shows-its-evidence.test.js.map +1 -0
  14. package/dist/__tests__/81c478df-create-page-duplicate-id-aliases.test.d.ts +11 -0
  15. package/dist/__tests__/81c478df-create-page-duplicate-id-aliases.test.d.ts.map +1 -0
  16. package/dist/__tests__/81c478df-create-page-duplicate-id-aliases.test.js +80 -0
  17. package/dist/__tests__/81c478df-create-page-duplicate-id-aliases.test.js.map +1 -0
  18. package/dist/__tests__/81c478df-rest-no-route-blames-the-plugin.test.d.ts +16 -0
  19. package/dist/__tests__/81c478df-rest-no-route-blames-the-plugin.test.d.ts.map +1 -0
  20. package/dist/__tests__/81c478df-rest-no-route-blames-the-plugin.test.js +105 -0
  21. package/dist/__tests__/81c478df-rest-no-route-blames-the-plugin.test.js.map +1 -0
  22. package/dist/__tests__/dfc0e156-plugin-file-with-a-slash.test.d.ts +2 -0
  23. package/dist/__tests__/dfc0e156-plugin-file-with-a-slash.test.d.ts.map +1 -0
  24. package/dist/__tests__/dfc0e156-plugin-file-with-a-slash.test.js +69 -0
  25. package/dist/__tests__/dfc0e156-plugin-file-with-a-slash.test.js.map +1 -0
  26. package/dist/__tests__/fb4ec63e-update-advice-matches-the-install.test.d.ts +15 -0
  27. package/dist/__tests__/fb4ec63e-update-advice-matches-the-install.test.d.ts.map +1 -0
  28. package/dist/__tests__/fb4ec63e-update-advice-matches-the-install.test.js +61 -0
  29. package/dist/__tests__/fb4ec63e-update-advice-matches-the-install.test.js.map +1 -0
  30. package/dist/__tests__/plugin-slug-path-encoding.test.d.ts +2 -0
  31. package/dist/__tests__/plugin-slug-path-encoding.test.d.ts.map +1 -0
  32. package/dist/__tests__/plugin-slug-path-encoding.test.js +41 -0
  33. package/dist/__tests__/plugin-slug-path-encoding.test.js.map +1 -0
  34. package/dist/__tests__/release-9-surface.test.d.ts +2 -0
  35. package/dist/__tests__/release-9-surface.test.d.ts.map +1 -0
  36. package/dist/__tests__/release-9-surface.test.js +197 -0
  37. package/dist/__tests__/release-9-surface.test.js.map +1 -0
  38. package/dist/config.d.ts +31 -0
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +57 -0
  41. package/dist/config.js.map +1 -1
  42. package/dist/install-skills.d.ts +4 -5
  43. package/dist/install-skills.d.ts.map +1 -1
  44. package/dist/install-skills.js +4 -5
  45. package/dist/install-skills.js.map +1 -1
  46. package/dist/server.d.ts.map +1 -1
  47. package/dist/server.js +328 -89
  48. package/dist/server.js.map +1 -1
  49. package/dist/skill-prompts.d.ts +43 -0
  50. package/dist/skill-prompts.d.ts.map +1 -0
  51. package/dist/skill-prompts.js +135 -0
  52. package/dist/skill-prompts.js.map +1 -0
  53. package/dist/wordpress-client.d.ts +132 -11
  54. package/dist/wordpress-client.d.ts.map +1 -1
  55. package/dist/wordpress-client.js +380 -39
  56. package/dist/wordpress-client.js.map +1 -1
  57. package/package.json +2 -2
  58. package/skills/activity-report-composer/SKILL.md +17 -5
  59. package/skills/activity-report-composer/metadata.json +2 -2
  60. package/skills/art-direction/SKILL.md +9 -3
  61. package/skills/art-direction/metadata.json +7 -3
  62. package/skills/brand-voice-synthesizer/SKILL.md +36 -16
  63. package/skills/brand-voice-synthesizer/metadata.json +8 -4
  64. package/skills/build-oxygen6-page/SKILL.md +17 -2
  65. package/skills/conversion-audit/SKILL.md +12 -0
  66. package/skills/custom-post-type-architect/SKILL.md +12 -0
  67. package/skills/figma-to-bricks/SKILL.md +4 -2
  68. package/skills/figma-to-divi/SKILL.md +4 -2
  69. package/skills/figma-to-elementor/SKILL.md +8 -1
  70. package/skills/figma-to-gutenberg/SKILL.md +4 -2
  71. package/skills/html-to-breakdance/SKILL.md +7 -3
  72. package/skills/html-to-breakdance/metadata.json +7 -3
  73. package/skills/html-to-bricks/SKILL.md +7 -3
  74. package/skills/html-to-bricks/metadata.json +7 -3
  75. package/skills/internal-link-builder/SKILL.md +12 -0
  76. package/skills/migrate-beaver-builder-to-bricks/SKILL.md +19 -3
  77. package/skills/migrate-beaver-builder-to-gutenberg/SKILL.md +19 -3
  78. package/skills/migrate-brizy-to-gutenberg/SKILL.md +19 -3
  79. package/skills/migrate-divi-to-breakdance/SKILL.md +19 -3
  80. package/skills/migrate-divi-to-bricks/SKILL.md +19 -3
  81. package/skills/migrate-divi-to-gutenberg/SKILL.md +19 -3
  82. package/skills/migrate-elementor-to-breakdance/SKILL.md +20 -4
  83. package/skills/migrate-elementor-to-bricks/SKILL.md +20 -4
  84. package/skills/migrate-elementor-to-gutenberg/SKILL.md +20 -4
  85. package/skills/migrate-elementor-to-oxygen/SKILL.md +20 -4
  86. package/skills/migrate-oxygen-to-breakdance/SKILL.md +19 -3
  87. package/skills/migrate-oxygen-to-bricks/SKILL.md +19 -3
  88. package/skills/migrate-thrive-architect-to-gutenberg/SKILL.md +19 -3
  89. package/skills/migrate-visual-composer-to-gutenberg/SKILL.md +19 -3
  90. package/skills/migrate-wpbakery-to-bricks/SKILL.md +19 -3
  91. package/skills/migrate-wpbakery-to-gutenberg/SKILL.md +19 -3
  92. package/skills/mobile-experience-report/SKILL.md +12 -0
  93. package/skills/murmur-review-loop/SKILL.md +142 -0
  94. package/skills/page-template-library/SKILL.md +12 -0
  95. package/skills/respira-builder-edits/SKILL.md +1 -0
  96. package/skills/respira-setup-assistant/SKILL.md +12 -0
  97. package/skills/respira-site-audit/SKILL.md +18 -6
  98. package/skills/respira-woocommerce/SKILL.md +1 -0
  99. package/skills/seo-aeo-amplifier/SKILL.md +14 -2
  100. package/skills/seo-aeo-amplifier/metadata.json +2 -2
  101. package/skills/stale-content-detector/SKILL.md +12 -0
  102. package/skills/technical-debt-audit/SKILL.md +14 -2
  103. package/skills/woo-marketing-campaigns/SKILL.md +12 -9
  104. package/skills/woo-pricing-promotions/SKILL.md +9 -9
  105. package/skills/woocommerce-health-check/SKILL.md +22 -8
  106. package/skills/wordpress-ai-image-optimizer/SKILL.md +14 -2
  107. package/skills/wordpress-ai-image-optimizer/metadata.json +2 -2
  108. package/skills/wordpress-security-review/SKILL.md +154 -0
  109. package/skills/wordpress-site-dna/README.md +1 -1
  110. package/skills/wordpress-site-dna/SKILL.md +52 -19
  111. package/tool-capabilities.json +3171 -370
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Provides tools for AI coding assistants to interact with WordPress sites
5
5
  */
6
- import { Server } from '@modelcontextprotocol/server';
6
+ import { Server, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server';
7
7
  import { setMcpClientInfo } from './agent-signature.js';
8
8
  import { serveStdio } from '@modelcontextprotocol/server/stdio';
9
9
  import { execSync } from 'child_process';
@@ -17,9 +17,10 @@ import { RespiraVersionChecker } from './version-checker.js';
17
17
  import { getBricksTools, dispatchBricksTool } from './bricks-tools.js';
18
18
  import { getElementorTools, dispatchElementorTool } from './elementor-tools.js';
19
19
  import { getAcfTools, resolveAcfToolName } from './acf-tools.js';
20
+ import { listSkillPrompts, getSkillPrompt } from './skill-prompts.js';
20
21
  import { getUsageEmitter, deriveToolKind } from './usage-emitter.js';
21
22
  import { FSE_OPERATION_ITEM_SCHEMA } from './fse-operation-schema.js';
22
- import { collapseLauncherProcesses, validateApiKeyShape, describeConfigOrigin, configOriginKey } from './config.js';
23
+ import { collapseLauncherProcesses, validateApiKeyShape, describeConfigOrigin, configOriginKey, describeUpdateRoute } from './config.js';
23
24
  // Process-local secret keeps target hashes useful for same-session retry
24
25
  // detection without making low-entropy WordPress ids reversible centrally.
25
26
  const TELEMETRY_HASH_SECRET = randomUUID();
@@ -133,6 +134,19 @@ function saveRespiraMcpState(patch) {
133
134
  console.error('[respira-mcp] could not write', STATE_FILE_PATH, '-', err?.message);
134
135
  }
135
136
  }
137
+ /**
138
+ * Tools removed in a major release, keyed by the name a caller would still
139
+ * send. dispatchToolCall answers them with the replacement instead of a bare
140
+ * "Unknown tool". Not advertised and not dispatched, so the parity gate never
141
+ * sees them.
142
+ */
143
+ const RETIRED_TOOLS = {
144
+ woocommerce_sales_report: {
145
+ replacement: 'woocommerce_revenue_summary',
146
+ removedIn: '9.0',
147
+ note: 'It takes date_start and date_end (ISO, in the store timezone) instead of period, date_min and date_max, and states exactly which order statuses and amounts it counts.',
148
+ },
149
+ };
136
150
  const MCP_SERVER_VERSION = (() => {
137
151
  try {
138
152
  const currentDir = dirname(fileURLToPath(import.meta.url));
@@ -411,8 +425,8 @@ const TOOLS_WITH_PAGINATION_HINT = {
411
425
  // node_id and no pagination, so the old hint sent the agent looking for an
412
426
  // argument that does not exist and it silently did nothing. A customer hit
413
427
  // the cap on a 158KB Breakdance page and was handed exactly that advice.
414
- respira_extract_builder_content: 'This tool has no pagination and always returns the whole page. Use respira_find_builder_targets instead, which takes query, limit and offset and reports total_matches / has_more / next_offset, or respira_find_element to jump straight to one element. respira_get_page_outline gives a row-level summary. If you genuinely need the entire structure in one call, raise the cap with RESPIRA_MAX_TOOL_RESULT_BYTES.',
415
- wordpress_extract_builder_content: 'This tool has no pagination and always returns the whole page. Use wordpress_find_builder_targets instead, which takes query, limit and offset and reports total_matches / has_more / next_offset, or wordpress_find_element to jump straight to one element. wordpress_get_page_outline gives a row-level summary. If you genuinely need the entire structure in one call, raise the cap with RESPIRA_MAX_TOOL_RESULT_BYTES.',
428
+ respira_extract_builder_content: 'This tool has no pagination and always returns the whole page. To read ONE element\'s stored content on a page of any size, call respira_find_element with include: "content". It returns the element body the builder saved, which is what update_element wants back. respira_find_builder_targets takes query, limit and offset and reports total_matches / has_more / next_offset, and respira_get_page_outline gives a row-level summary. Do not rebuild an element from the rendered front end: WordPress adds attributes at output time (target, rel) that the stored value never had. Raising RESPIRA_MAX_TOOL_RESULT_BYTES is a last resort, and it is a client-side setting an agent cannot change mid-task.',
429
+ wordpress_extract_builder_content: 'This tool has no pagination and always returns the whole page. To read ONE element\'s stored content on a page of any size, call wordpress_find_element with include: "content". It returns the element body the builder saved, which is what update_element wants back. wordpress_find_builder_targets takes query, limit and offset and reports total_matches / has_more / next_offset, and wordpress_get_page_outline gives a row-level summary. Do not rebuild an element from the rendered front end: WordPress adds attributes at output time (target, rel) that the stored value never had. Raising RESPIRA_MAX_TOOL_RESULT_BYTES is a last resort, and it is a client-side setting an agent cannot change mid-task.',
416
430
  respira_list_plugins: 'No pagination arg today. The full plugin list is usually small; consider filing a feature request if your site hits the cap here.',
417
431
  };
418
432
  /**
@@ -566,6 +580,8 @@ export class RespiraWordPressServer {
566
580
  }, {
567
581
  capabilities: {
568
582
  tools: {},
583
+ // The bundled skills catalog, one prompt per skill (skill-prompts.ts).
584
+ prompts: {},
569
585
  },
570
586
  instructions: `${SOUL_MD_CONTENT}
571
587
 
@@ -592,7 +608,7 @@ Follow these on every site. They are what separates a great result from a frustr
592
608
 
593
609
  ## Tool naming
594
610
 
595
- Always use respira_* tool names (e.g. respira_update_page, respira_find_element). The wordpress_* names are deprecated aliases and will be removed in v6.0.
611
+ Always use respira_* tool names (e.g. respira_update_page, respira_find_element). The older wordpress_* names still answer, so a saved call keeps working, but new calls should use respira_*.
596
612
 
597
613
  ## Step 1: Understand the site
598
614
 
@@ -1826,6 +1842,20 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
1826
1842
  tools: await this.getTools(),
1827
1843
  };
1828
1844
  });
1845
+ // Skills as prompts. Served from the skills folder bundled into the
1846
+ // package, so listing them needs no site and no network: a client can
1847
+ // offer every Respira skill as a slash command before a site is set up.
1848
+ this.server.setRequestHandler('prompts/list', async () => {
1849
+ return { prompts: listSkillPrompts() };
1850
+ });
1851
+ this.server.setRequestHandler('prompts/get', async (request) => {
1852
+ const { name, arguments: promptArgs } = request.params;
1853
+ const prompt = getSkillPrompt(String(name || ''), promptArgs);
1854
+ if (!prompt) {
1855
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown prompt: ${name}. Call prompts/list for the Respira skills this server offers.`);
1856
+ }
1857
+ return prompt;
1858
+ });
1829
1859
  // Handle tool calls
1830
1860
  this.server.setRequestHandler('tools/call', async (request) => {
1831
1861
  const { name, arguments: args } = request.params;
@@ -2128,13 +2158,19 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
2128
2158
  if (!update) {
2129
2159
  return result;
2130
2160
  }
2161
+ // Ticket fb4ec63e: the advice has to describe the install that is
2162
+ // answering. On a .mcpb bundle the npm command completes, installs a copy
2163
+ // Claude Desktop never loads, and leaves the reporter certain they had
2164
+ // updated. describeUpdateRoute reads the same launch-path signal
2165
+ // respira_diagnose_connection already reports.
2166
+ const route = describeUpdateRoute();
2131
2167
  // Lead with what this build is missing, not with the fact that a number
2132
2168
  // changed. A customer on 7.1.2 received the old generic sentence on every
2133
2169
  // call for weeks while fighting a bug that a newer build had already fixed,
2134
2170
  // and neither she nor her AI had any reason to connect the two.
2135
2171
  const message = update.missingFixes.length > 0
2136
- ? `This MCP build (${update.currentVersion}) is behind ${update.latestVersion} and is missing behaviour you may be working around right now: ${update.missingFixes.join(' ')} Update with: npm i -g @respira/wordpress-mcp-server@latest`
2137
- : `A newer MCP version is available (${update.currentVersion} -> ${update.latestVersion}). Please update your local MCP package.`;
2172
+ ? `This MCP build (${update.currentVersion}) is behind ${update.latestVersion} and is missing behaviour you may be working around right now: ${update.missingFixes.join(' ')} ${route.how_to_update}`
2173
+ : `A newer MCP version is available (${update.currentVersion} -> ${update.latestVersion}). ${route.how_to_update}`;
2138
2174
  const noticePayload = {
2139
2175
  package: update.packageName,
2140
2176
  current_version: update.currentVersion,
@@ -2142,8 +2178,12 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
2142
2178
  checked_at: update.checkedAt,
2143
2179
  ...(update.missingFixes.length > 0 ? { missing_fixes: update.missingFixes } : {}),
2144
2180
  message,
2145
- upgrade_command: 'npm i -g @respira/wordpress-mcp-server@latest',
2146
- rerun_setup_command: 'npx @respira/wordpress-mcp-server --setup',
2181
+ install_kind: route.install_kind,
2182
+ transport_mode: route.transport_mode,
2183
+ binary_path: route.binary_path,
2184
+ how_to_update: route.how_to_update,
2185
+ ...(route.upgrade_command ? { upgrade_command: route.upgrade_command } : {}),
2186
+ ...(route.rerun_setup_command ? { rerun_setup_command: route.rerun_setup_command } : {}),
2147
2187
  };
2148
2188
  if (result && typeof result === 'object' && !Array.isArray(result)) {
2149
2189
  return {
@@ -2161,7 +2201,8 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
2161
2201
  if (!update) {
2162
2202
  return errorMessage;
2163
2203
  }
2164
- return `${errorMessage}\n\nUpdate available: ${update.currentVersion} -> ${update.latestVersion}. Run: npm i -g @respira/wordpress-mcp-server@latest`;
2204
+ const route = describeUpdateRoute();
2205
+ return `${errorMessage}\n\nUpdate available: ${update.currentVersion} -> ${update.latestVersion}. ${route.how_to_update}`;
2165
2206
  }
2166
2207
  /**
2167
2208
  * Append the plugin version compatibility warning to the first successful tool
@@ -2566,12 +2607,20 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
2566
2607
  type: 'number',
2567
2608
  description: 'ID of the original page to duplicate',
2568
2609
  },
2610
+ page_id: {
2611
+ type: 'number',
2612
+ description: 'Alias for original_id, matching the builder page tools',
2613
+ },
2614
+ post_id: {
2615
+ type: 'number',
2616
+ description: 'Alias for original_id, matching tools that operate on any post type',
2617
+ },
2569
2618
  suffix: {
2570
2619
  type: 'string',
2571
2620
  description: 'Optional suffix for the duplicated page title',
2572
2621
  },
2573
2622
  },
2574
- required: ['original_id'],
2623
+ required: [],
2575
2624
  },
2576
2625
  annotations: {
2577
2626
  title: "Duplicate Page",
@@ -2649,7 +2698,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
2649
2698
  },
2650
2699
  {
2651
2700
  name: 'wordpress_delete_page',
2652
- description: 'Delete a page. IMPORTANT: By default, this only works on Respira-created duplicates. To delete an original: pass `force=true` AND `confirm_live_edit=true`. The "Allow Direct Editing" setting in Respira must also be enabled (disabled by default for safety).\n\nApproval flow (added v6.14.2): the first call returns `code: respira_approval_required` with `data.approval_request.approval_token`. Pass that token back via the `approval_token` param on the next call to complete the delete. Pre-v6.14.2 the schema did not expose `approval_token` so agents could see the token in the response but had no way to pass it back — leaving every agent-driven cleanup of its own duplicates stuck.',
2701
+ description: 'Delete a page. IMPORTANT: By default, this only works on Respira-created duplicates. To delete an original: pass `force=true` AND `confirm_live_edit=true`. The "Allow Direct Editing" setting in Respira must also be enabled (disabled by default for safety).\n\nApproval flow: the first call returns `code: respira_approval_required` with `data.approval_request.approval_token`. Pass that token back via the `approval_token` param on the next call to complete the delete.',
2653
2702
  inputSchema: {
2654
2703
  type: 'object',
2655
2704
  properties: {
@@ -2663,7 +2712,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
2663
2712
  },
2664
2713
  confirm_live_edit: {
2665
2714
  type: 'boolean',
2666
- description: 'N37 fix (v6.19.0): operator confirmation that a force-delete on an original is intended. The server has always required this alongside force=true; pre-v6.19.0 it was undocumented and the surfaced approval response leaked it as a "surreptitious" field. Now in the schema.',
2715
+ description: 'Operator confirmation that a force-delete on an original is intended. Required alongside force=true, and honoured only when "Allow Direct Editing" is enabled in Respira settings.',
2667
2716
  },
2668
2717
  approval_token: {
2669
2718
  type: 'string',
@@ -2751,12 +2800,20 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
2751
2800
  type: 'number',
2752
2801
  description: 'Original post ID',
2753
2802
  },
2803
+ post_id: {
2804
+ type: 'number',
2805
+ description: 'Alias for original_id, matching tools that operate on any post type',
2806
+ },
2807
+ page_id: {
2808
+ type: 'number',
2809
+ description: 'Alias for original_id, matching the builder page tools',
2810
+ },
2754
2811
  suffix: {
2755
2812
  type: 'string',
2756
2813
  description: 'Optional suffix',
2757
2814
  },
2758
2815
  },
2759
- required: ['original_id'],
2816
+ required: [],
2760
2817
  },
2761
2818
  annotations: {
2762
2819
  title: "Duplicate Post",
@@ -3081,7 +3138,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
3081
3138
  },
3082
3139
  {
3083
3140
  name: 'wordpress_delete_post',
3084
- description: 'Delete a post. IMPORTANT: By default, this only works on Respira-created duplicates. To delete an original: pass `force=true` AND `confirm_live_edit=true`. The "Allow Direct Editing" setting in Respira must also be enabled (disabled by default for safety).\n\nApproval flow: destructive — the FIRST call returns `code: respira_approval_required` with `data.approval_request.approval_token`, and deletes nothing. Pass that exact token back via the `approval_token` param on the next call to complete the delete. Deleting a Respira-created duplicate still takes both calls; the approval gate runs before the duplicate check.\n\nFixed in 8.3.7 (ticket 429299b5): this tool used to discard the response body, so the approval token, the refusal reason and the success flag were all invisible and every call looked like an empty response. It now always returns the site\'s answer.',
3141
+ description: 'Delete a post. IMPORTANT: By default, this only works on Respira-created duplicates. To delete an original: pass `force=true` AND `confirm_live_edit=true`. The "Allow Direct Editing" setting in Respira must also be enabled (disabled by default for safety).\n\nApproval flow: destructive — the FIRST call returns `code: respira_approval_required` with `data.approval_request.approval_token`, and deletes nothing. Pass that exact token back via the `approval_token` param on the next call to complete the delete. Deleting a Respira-created duplicate still takes both calls; the approval gate runs before the duplicate check.\n\nThe response is always the site\'s answer: the approval token, the refusal reason, or the success flag.',
3085
3142
  inputSchema: {
3086
3143
  type: 'object',
3087
3144
  properties: {
@@ -3324,7 +3381,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
3324
3381
  },
3325
3382
  mode: {
3326
3383
  type: 'string',
3327
- description: 'replace (default): OVERWRITES all existing page content. append: adds new content after existing elements, preserving the current layout. As of plugin v7.0.16, replace against a page with existing content requires confirm_replace=true.',
3384
+ description: 'replace (default): OVERWRITES all existing page content. append: adds new content after existing elements, preserving the current layout. Replacing a page that already has content requires confirm_replace=true.',
3328
3385
  enum: ['replace', 'append'],
3329
3386
  },
3330
3387
  confirm_replace: {
@@ -3660,6 +3717,57 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
3660
3717
  openWorldHint: false,
3661
3718
  },
3662
3719
  },
3720
+ {
3721
+ // The client report. Before 9.0 it existed only as the ability
3722
+ // respira/generate-activity-report, which invoke_ability cannot reach
3723
+ // (the inhale gate refuses it), so every skill that asked for it
3724
+ // failed at step 1.
3725
+ name: 'wordpress_generate_activity_report',
3726
+ description: 'Generate the client report for a period from this site\'s Respira activity log: what changed, through which tools and builders, concrete before and after examples, notable moments, and time saved against per-tool manual baselines. Returns structured data for you to write up in the chosen framing, such as an agency client report or a personal recap. Reads the log and changes nothing. Defaults to the last 30 days. Needs an administrator key.',
3727
+ inputSchema: {
3728
+ type: 'object',
3729
+ properties: {
3730
+ window_days: { type: 'integer', minimum: 1, maximum: 365, description: 'Days back from now. Default 30. Ignored when start_date and end_date are given.' },
3731
+ start_date: { type: 'string', description: 'ISO 8601 start date. Use with end_date.' },
3732
+ end_date: { type: 'string', description: 'ISO 8601 end date. Use with start_date.' },
3733
+ content_types: {
3734
+ type: 'array',
3735
+ items: { type: 'string', enum: ['pages', 'posts', 'products', 'variables', 'snapshots', 'migrations', 'audits', 'skills'] },
3736
+ description: 'Limit the report to these content types. Default all.',
3737
+ },
3738
+ scope: {
3739
+ type: 'object',
3740
+ properties: {
3741
+ page_ids: { type: 'array', items: { type: 'integer' } },
3742
+ post_ids: { type: 'array', items: { type: 'integer' } },
3743
+ product_ids: { type: 'array', items: { type: 'integer' } },
3744
+ builder: { type: 'string' },
3745
+ },
3746
+ description: 'Focus on specific page, post or product IDs, or on one page builder by slug.',
3747
+ },
3748
+ framing: {
3749
+ type: 'string',
3750
+ enum: ['personal_recap', 'agency_client_report', 'agency_case_study', 'team_report', 'testimonial_draft', 'build_in_public'],
3751
+ description: 'Who the write-up is for. Default personal_recap; use agency_client_report for a client.',
3752
+ },
3753
+ output_mode: { type: 'string', enum: ['structured', 'client_report', 'ops_report'], description: 'structured returns the data; client_report and ops_report add ready-made sections.' },
3754
+ report_tone: { type: 'string', enum: ['client_update', 'retainer_proof', 'internal_ops'], description: 'Tone for the ready-made sections.' },
3755
+ client_name: { type: 'string', description: 'Who an agency report is addressed to.' },
3756
+ include_examples: { type: 'boolean', description: 'Up to 10 edits with 200-character before and after excerpts. Default true.' },
3757
+ include_time_estimates: { type: 'boolean', description: 'Estimate time saved from per-tool baselines. Default true.' },
3758
+ anonymize: { type: 'boolean', description: 'Redact page titles, URLs and excerpts, for a report that will be published. Default false.' },
3759
+ currency: { type: 'string', description: 'ISO 4217 code for the cost estimate. Default EUR.' },
3760
+ hourly_rate: { type: 'number', description: 'When set, time savings include a cost estimate at this hourly rate.' },
3761
+ },
3762
+ },
3763
+ annotations: {
3764
+ title: "Generate Activity Report",
3765
+ readOnlyHint: true,
3766
+ destructiveHint: false,
3767
+ idempotentHint: true,
3768
+ openWorldHint: false,
3769
+ },
3770
+ },
3663
3771
  {
3664
3772
  name: 'wordpress_get_site_template',
3665
3773
  description: 'Read one FSE template or template part as normalized blocks. Returns its source, render route, supported targeted operations, and expected_fingerprint for safe updates.',
@@ -4640,7 +4748,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
4640
4748
  },
4641
4749
  {
4642
4750
  name: 'wordpress_diagnose_connection',
4643
- description: 'Run a connection-fingerprint diagnostic for the active site. Combines the plugin\'s server-side report (route registration, php/wp/plugin versions, edge plugin presence) with outside-in probes from the MCP server (REST root reachability, content-type sanity check on Respira routes, edge-layer headers). Use when a tool returns "html instead of json", an opaque 5xx, or when a connection that worked yesterday silently breaks. ALSO use it when the user suspects the wrong site, a stale connection, or a setup step that "did nothing": the response carries transport_mode (which install is answering: mcpb_bundle, npx_cache, global_npm_install, project_local_install, source_checkout), transport.duplicate_entries (whether a second Respira server is running, which usually means the client config lists Respira twice), and client_config (which config source won and how many sites came out of it). Returns a structured object including detected edge layers (Cloudflare, Wordfence, Sucuri) and concrete remediation recommendations.',
4751
+ description: 'Run a connection-fingerprint diagnostic for the active site. Combines the plugin\'s server-side report (route registration, php/wp/plugin versions, edge plugin presence) with outside-in probes from the MCP server (REST root reachability, content-type sanity check on Respira routes, edge-layer headers). Use when a tool returns "html instead of json", an opaque 5xx, or when a connection that worked yesterday silently breaks. ALSO use it when the user suspects the wrong site, a stale connection, or a setup step that "did nothing": the response carries transport_mode (which install is answering: mcpb_bundle, npx_cache, global_npm_install, project_local_install, source_checkout), transport.duplicate_entries (whether a second Respira server is running, which usually means the client config lists Respira twice), and client_config (which config source won and how many sites came out of it). Returns a structured object including detected edge layers (Cloudflare, Wordfence, Sucuri) and concrete remediation recommendations. It also reports well_known_discovery: whether the host answers /.well-known/oauth-protected-resource itself instead of letting WordPress answer it, which is what breaks a ChatGPT connection on a site whose plugin is working, since ChatGPT checks only that address.',
4644
4752
  inputSchema: {
4645
4753
  type: 'object',
4646
4754
  properties: {
@@ -4650,7 +4758,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
4650
4758
  },
4651
4759
  probe_timeout_ms: {
4652
4760
  type: 'number',
4653
- description: 'Optional per-probe timeout in milliseconds. Default 30000 (was 15000 pre-v6.17). Clamped to [5000, 60000]. Bump this when probing sites behind slow edge layers (some Cloudflare custom rules add 8+ seconds before forwarding to origin).',
4761
+ description: 'Optional per-probe timeout in milliseconds. Default 30000. Clamped to [5000, 60000]. Bump this when probing sites behind slow edge layers (some Cloudflare custom rules add 8+ seconds before forwarding to origin).',
4654
4762
  minimum: 5000,
4655
4763
  maximum: 60000,
4656
4764
  },
@@ -4786,31 +4894,35 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
4786
4894
  },
4787
4895
  },
4788
4896
  {
4897
+ // Until 9.0 this returned a heuristic estimate from static page
4898
+ // analysis. AER's "what a visitor notices first" shortcut and three
4899
+ // skills call it by name expecting field data, so it now runs the real
4900
+ // PageSpeed Insights audit instead of being removed.
4789
4901
  name: 'wordpress_get_core_web_vitals',
4790
- description: 'Get Core Web Vitals metrics (LCP, FID, CLS) for a page.\n\n**Deprecated path** (v6.19.0+, Phase E Tier 1): the current implementation returns a HEURISTIC estimate from static page analysis, NOT real Lighthouse / CrUX data. The response includes `data_source: "respira_heuristic_v1"` and a `_deprecation` field. For real measurements use `respira_run_pagespeed_audit` (Phase E Tier 2).',
4902
+ description: 'Core Web Vitals for a page, measured by Google PageSpeed Insights: LCP, CLS and INP from real Chrome users over the last 28 days when Google publishes field data for the URL, plus Lighthouse lab metrics. Runs the same audit as respira_run_pagespeed_audit with one strategy, so the page must be publicly reachable; drafts and local sites return an error that says so. Cached for 1 hour per URL and strategy.',
4791
4903
  inputSchema: {
4792
4904
  type: 'object',
4793
4905
  properties: {
4794
- page_id: {
4795
- type: 'number',
4796
- description: 'Page ID to analyze',
4797
- },
4906
+ page_id: { type: 'number', description: 'Page ID to measure. Resolved to its public permalink.' },
4907
+ url: { type: 'string', description: 'Explicit public URL to measure instead of a page ID.' },
4908
+ strategy: { type: 'string', enum: ['mobile', 'desktop'], default: 'mobile', description: 'Device profile. Default mobile.' },
4798
4909
  },
4799
- required: ['page_id'],
4910
+ anyOf: [
4911
+ { required: ['page_id'] },
4912
+ { required: ['url'] },
4913
+ ],
4800
4914
  },
4801
4915
  annotations: {
4802
4916
  title: "Core Web Vitals",
4803
4917
  readOnlyHint: true,
4804
4918
  destructiveHint: false,
4805
4919
  idempotentHint: true,
4806
- openWorldHint: false,
4920
+ openWorldHint: true,
4807
4921
  },
4808
4922
  },
4809
4923
  {
4810
- // Phase E Tier 2 (v6.19.0): real PageSpeed Insights v5 audit. The
4811
- // honest replacement for `get_core_web_vitals` heuristic. Returns
4812
- // Lighthouse lab data + CrUX field data (when available) + the
4813
- // opportunity list ranked by metric savings.
4924
+ // Real PageSpeed Insights v5 audit: Lighthouse lab data, CrUX field
4925
+ // data when available, and opportunities ranked by metric savings.
4814
4926
  name: 'wordpress_run_pagespeed_audit',
4815
4927
  description: 'Run a real PageSpeed Insights v5 audit against a public URL. Returns Lighthouse lab data (scores for performance/accessibility/best-practices/seo, lab metrics FCP/LCP/TBT/CLS/SI/TTI, opportunities ranked by savings_ms, diagnostics) plus CrUX field data (real-user p75 metrics over the trailing ~28 days, when Google has enough traffic to publish them for the URL). Cached for 1 hour per (url, strategy). Pass `fresh=true` to bypass the cache. `strategy="both"` runs mobile + desktop in series. Set the `respira_pagespeed_api_key` WordPress option to lift the 1 req/sec free-tier rate limit to 200 req/min.',
4816
4928
  inputSchema: {
@@ -4836,9 +4948,8 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
4836
4948
  },
4837
4949
  },
4838
4950
  {
4839
- // Phase E Tier 2 (v6.19.0): standard-analyzer-envelope wrapper around
4840
- // PSI. Feeds the Health tab composite via Respira_Site_Health's new
4841
- // `pagespeed` slot. Use for periodic site-wide health passes; use
4951
+ // Standard-analyzer-envelope wrapper around PSI. Feeds the Health tab
4952
+ // composite via Respira_Site_Health's `pagespeed` slot. Use for periodic site-wide health passes; use
4842
4953
  // `respira_run_pagespeed_audit` for the raw lab+field data.
4843
4954
  name: 'wordpress_analyze_pagespeed',
4844
4955
  description: 'Analyzer-envelope wrapper around the PSI audit. Returns the standard `{success, score, grade, issues, recommendations, metrics, data_source, measured_at}` shape that the Reports → Health tab consumes. Score = Lighthouse performance score (0-100). Grade A≥90, B≥75, C≥60, D≥40, F<40. Issues derived from Lighthouse opportunities, ranked by savings_ms. CrUX p75 field-data surfaces as an info-priority recommendation when available.',
@@ -5016,7 +5127,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5016
5127
  // Safety & Security center
5017
5128
  {
5018
5129
  name: 'wordpress_run_security_audit',
5019
- 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.',
5130
+ description: 'Run a bounded WordPress security audit. Returns authoritative WordPress/WooCommerce versions, official core checksum mismatches, administrators and account hygiene, standard and must-use plugins, themes, WordPress cron hooks, advisory indicators, scan coverage and explicit host-level limitations. The audit reads and never changes the site, deep_scan included. One flag writes: probe_uploads_execution, off by default, puts one inert PHP file in uploads, requests it to learn whether PHP runs there, then deletes it. The result includes known_vulnerabilities: installed core, plugins and themes matched against the Respira vulnerability database for a licensed site, with status ok or unavailable. An unavailable status never means the site is clean. known_vulnerabilities=false skips that match. On plugins older than 9.0, deep_scan also runs the uploads probe.',
5020
5131
  inputSchema: {
5021
5132
  type: 'object',
5022
5133
  properties: {
@@ -5028,7 +5139,17 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5028
5139
  deep_scan: {
5029
5140
  type: 'boolean',
5030
5141
  default: false,
5031
- description: 'Inspect uploads and must-use plugin paths with strict file, time, symlink and result limits.',
5142
+ description: 'Inspect uploads and must-use plugin paths for executable files, with strict file, time, symlink and result limits. Reads only on plugin 9.0 and newer.',
5143
+ },
5144
+ probe_uploads_execution: {
5145
+ type: 'boolean',
5146
+ default: false,
5147
+ description: 'Writes one inert PHP file into uploads, requests it to test whether PHP executes there, then deletes it. Off by default. File-change monitors such as Wordfence may report the file. Needs plugin 9.0 or newer; older plugins ignore it.',
5148
+ },
5149
+ known_vulnerabilities: {
5150
+ type: 'boolean',
5151
+ default: true,
5152
+ description: 'Match installed core, plugins and themes against the Respira vulnerability database. On by default; false skips that round trip. Needs plugin 9.0 or newer.',
5032
5153
  },
5033
5154
  },
5034
5155
  },
@@ -5042,17 +5163,17 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5042
5163
  },
5043
5164
  {
5044
5165
  name: 'wordpress_update_core_security',
5045
- 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.',
5166
+ description: 'Update WordPress core to the security release WordPress.org offers on the site\'s 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. Plugins older than 9.0 accept only the releases in their own catalog.',
5046
5167
  inputSchema: {
5047
5168
  type: 'object',
5048
5169
  properties: {
5049
5170
  advisory_id: {
5050
5171
  type: 'string',
5051
- description: 'Reviewed security advisory ID. Do not guess it: run respira_run_security_audit first and use the advisory it reports. The approved list lives in the plugin catalog and grows with each WordPress security release, so any advisory named here would go stale.',
5172
+ description: 'Security advisory ID from respira_run_security_audit, recorded on the receipt. Do not guess it: run the audit first and use the advisory it reports.',
5052
5173
  },
5053
5174
  target_version: {
5054
5175
  type: 'string',
5055
- description: 'Exact approved patch version for that advisory on this site\'s own release branch, as reported by respira_run_security_audit. The plugin rejects anything outside its catalog, and it will not move a site across branches.',
5176
+ description: 'The exact version WordPress.org offers as the update on this site\'s own release branch, as reported by respira_run_security_audit. The plugin refuses any other version and never moves a site across branches.',
5056
5177
  },
5057
5178
  backup_confirmed: {
5058
5179
  type: 'boolean',
@@ -5073,6 +5194,60 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5073
5194
  openWorldHint: true,
5074
5195
  },
5075
5196
  },
5197
+ {
5198
+ name: 'wordpress_update_theme',
5199
+ description: 'Update a theme to the latest version offered to this site, then check that the site still answers (REST API, front page, fatal-error marker). Requires plugin management to be enabled in Respira settings. Approval-gated: the first call returns `respira_approval_required` with an `approval_token`; call again with the same `stylesheet` plus that token. If the result carries `stop_batch: true`, stop updating and check the site. There is no automatic rollback.',
5200
+ inputSchema: {
5201
+ type: 'object',
5202
+ properties: {
5203
+ stylesheet: {
5204
+ type: 'string',
5205
+ description: 'Theme directory name (its stylesheet), as respira_run_security_audit lists it under checks.themes.',
5206
+ },
5207
+ approval_token: {
5208
+ type: 'string',
5209
+ description: 'Second-step approval token returned by the first call. Echo it back unchanged with the same stylesheet.',
5210
+ },
5211
+ },
5212
+ required: ['stylesheet'],
5213
+ },
5214
+ annotations: {
5215
+ title: "Update Theme",
5216
+ readOnlyHint: false,
5217
+ destructiveHint: true,
5218
+ idempotentHint: true,
5219
+ openWorldHint: true,
5220
+ },
5221
+ },
5222
+ {
5223
+ name: 'wordpress_revoke_application_password',
5224
+ description: 'Revoke one WordPress application password, by user id and uuid as respira_run_security_audit reports them under checks.account_hygiene. Anything that signs in with it stops working and it cannot be restored. Approval-gated: the first call returns `respira_approval_required` with an `approval_token`; call again with the same arguments plus that token. The password this request itself signed in with is refused.',
5225
+ inputSchema: {
5226
+ type: 'object',
5227
+ properties: {
5228
+ user_id: {
5229
+ type: 'integer',
5230
+ description: 'The user the application password belongs to.',
5231
+ },
5232
+ uuid: {
5233
+ type: 'string',
5234
+ description: 'The application password uuid from the security audit. Never the password itself.',
5235
+ },
5236
+ approval_token: {
5237
+ type: 'string',
5238
+ description: 'Second-step approval token returned by the first call. Echo it back unchanged with the same user_id and uuid.',
5239
+ },
5240
+ },
5241
+ required: ['user_id', 'uuid'],
5242
+ },
5243
+ annotations: {
5244
+ title: "Revoke Application Password",
5245
+ readOnlyHint: false,
5246
+ destructiveHint: true,
5247
+ idempotentHint: true,
5248
+ openWorldHint: false,
5249
+ },
5250
+ },
5076
5251
  // Accessibility tools
5077
5252
  {
5078
5253
  name: 'wordpress_list_accessibility_scans',
@@ -5155,10 +5330,12 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5155
5330
  openWorldHint: false,
5156
5331
  },
5157
5332
  },
5158
- // Plugin Management tools (EXPERIMENTAL)
5333
+ // Plugin Management tools. The EXPERIMENTAL label came off in 9.0: every
5334
+ // mutation is approval-gated, and activation has probed the site and
5335
+ // rolled itself back on failure since plugin 8.5.5.
5159
5336
  {
5160
5337
  name: 'wordpress_list_plugins',
5161
- description: 'EXPERIMENTAL: List all installed plugins with their status, version, and update availability. Requires plugin management to be enabled in Respira settings.',
5338
+ description: 'List all installed plugins with their status, version, and update availability. Requires plugin management to be enabled in Respira settings.',
5162
5339
  inputSchema: {
5163
5340
  type: 'object',
5164
5341
  properties: {},
@@ -5173,7 +5350,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5173
5350
  },
5174
5351
  {
5175
5352
  name: 'wordpress_install_plugin',
5176
- description: 'EXPERIMENTAL: Install a plugin from WordPress.org or a ZIP URL. Requires plugin management to be enabled in Respira settings. Approval-gated: first call returns `respira_approval_required` with an `approval_token`; call again with that token to complete the install. Use with caution.',
5353
+ description: 'Install a plugin from WordPress.org or a ZIP URL. Requires plugin management to be enabled in Respira settings. Approval-gated: first call returns `respira_approval_required` with an `approval_token`; call again with that token to complete the install. Install only what the site owner asked for.',
5177
5354
  inputSchema: {
5178
5355
  type: 'object',
5179
5356
  properties: {
@@ -5203,13 +5380,13 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5203
5380
  },
5204
5381
  {
5205
5382
  name: 'wordpress_activate_plugin',
5206
- description: 'EXPERIMENTAL: Safely activate a plugin. Respira checks declared PHP, WordPress, dependency and companion-plugin compatibility, activates in a fresh loopback request, probes REST and frontend boot, and automatically deactivates the plugin if the site fails. Approval-gated. Use force_without_probe only after explicit operator approval on a host that blocks loopback requests.',
5383
+ description: 'Safely activate a plugin. Respira checks declared PHP, WordPress, dependency and companion-plugin compatibility, activates in a fresh loopback request, probes REST and frontend boot, and automatically deactivates the plugin if the site fails. Approval-gated. Use force_without_probe only after explicit operator approval on a host that blocks loopback requests.',
5207
5384
  inputSchema: {
5208
5385
  type: 'object',
5209
5386
  properties: {
5210
5387
  slug: {
5211
5388
  type: 'string',
5212
- description: 'Plugin slug',
5389
+ description: 'Plugin folder name, or the exact plugin file wordpress_list_plugins reports. Both work, including names with spaces and capitals ("WP PROTECTION" or "WP PROTECTION/SECURITY.php"). Do not sanitize the name yourself: pass it exactly as WordPress spells it.',
5213
5390
  },
5214
5391
  approval_token: {
5215
5392
  type: 'string',
@@ -5232,13 +5409,13 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5232
5409
  },
5233
5410
  {
5234
5411
  name: 'wordpress_deactivate_plugin',
5235
- description: 'EXPERIMENTAL: Deactivate a plugin. Requires plugin management to be enabled in Respira settings. Approval-gated: first call returns `respira_approval_required` with an `approval_token`; call again with the same `slug` plus that token to complete deactivation.',
5412
+ description: 'Deactivate a plugin. Requires plugin management to be enabled in Respira settings. Approval-gated: first call returns `respira_approval_required` with an `approval_token`; call again with the same `slug` plus that token to complete deactivation.',
5236
5413
  inputSchema: {
5237
5414
  type: 'object',
5238
5415
  properties: {
5239
5416
  slug: {
5240
5417
  type: 'string',
5241
- description: 'Plugin slug',
5418
+ description: 'Plugin folder name, or the exact plugin file wordpress_list_plugins reports. Both work, including names with spaces and capitals ("WP PROTECTION" or "WP PROTECTION/SECURITY.php"). Do not sanitize the name yourself: pass it exactly as WordPress spells it.',
5242
5419
  },
5243
5420
  approval_token: {
5244
5421
  type: 'string',
@@ -5257,13 +5434,13 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5257
5434
  },
5258
5435
  {
5259
5436
  name: 'wordpress_update_plugin',
5260
- description: 'EXPERIMENTAL: Update a plugin to the latest version. Requires plugin management to be enabled in Respira settings. Approval-gated: first call returns `respira_approval_required` with an `approval_token`; call again with the same `slug` plus that token to complete the update. Make sure you have backups before updating.',
5437
+ description: 'Update a plugin to the latest version. Requires plugin management to be enabled in Respira settings. Approval-gated: first call returns `respira_approval_required` with an `approval_token`; call again with the same `slug` plus that token to complete the update. Make sure a recent backup exists before updating, and load the front page afterwards to confirm the site still renders.',
5261
5438
  inputSchema: {
5262
5439
  type: 'object',
5263
5440
  properties: {
5264
5441
  slug: {
5265
5442
  type: 'string',
5266
- description: 'Plugin slug',
5443
+ description: 'Plugin folder name, or the exact plugin file wordpress_list_plugins reports. Both work, including names with spaces and capitals ("WP PROTECTION" or "WP PROTECTION/SECURITY.php"). Do not sanitize the name yourself: pass it exactly as WordPress spells it.',
5267
5444
  },
5268
5445
  approval_token: {
5269
5446
  type: 'string',
@@ -5282,13 +5459,13 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5282
5459
  },
5283
5460
  {
5284
5461
  name: 'wordpress_delete_plugin',
5285
- description: 'EXPERIMENTAL: Permanently delete a plugin. Requires plugin management to be enabled in Respira settings. The plugin must be deactivated first. Approval-gated: first call returns `respira_approval_required` with an `approval_token`; call again with the same `slug` plus that token to confirm deletion.',
5462
+ description: 'Permanently delete a plugin. Requires plugin management to be enabled in Respira settings. The plugin must be deactivated first. Approval-gated: first call returns `respira_approval_required` with an `approval_token`; call again with the same `slug` plus that token to confirm deletion.',
5286
5463
  inputSchema: {
5287
5464
  type: 'object',
5288
5465
  properties: {
5289
5466
  slug: {
5290
5467
  type: 'string',
5291
- description: 'Plugin slug',
5468
+ description: 'Plugin folder name, or the exact plugin file wordpress_list_plugins reports. Both work, including names with spaces and capitals ("WP PROTECTION" or "WP PROTECTION/SECURITY.php"). Do not sanitize the name yourself: pass it exactly as WordPress spells it.',
5292
5469
  },
5293
5470
  approval_token: {
5294
5471
  type: 'string',
@@ -5357,7 +5534,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5357
5534
  },
5358
5535
  {
5359
5536
  name: 'wordpress_create_user',
5360
- description: 'Create a new user.\n\nApproval flow (N9 fix, v6.19.0): destructive — the first call returns `code: respira_approval_required` with `data.approval_request.approval_token`. Pass that token back via the `approval_token` param on the next call to complete the create. Pre-v6.19.0 the schema did not expose `approval_token` so agents could see the token in the response but had no way to complete the flow.',
5537
+ description: 'Create a new user.\n\nApproval flow: the first call returns `code: respira_approval_required` with `data.approval_request.approval_token`. Pass that token back via the `approval_token` param on the next call to complete the create.',
5361
5538
  inputSchema: {
5362
5539
  type: 'object',
5363
5540
  properties: {
@@ -5394,7 +5571,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5394
5571
  },
5395
5572
  {
5396
5573
  name: 'wordpress_update_user',
5397
- description: 'Update user information.\n\nApproval flow (N9 fix, v6.19.0): destructive first call returns `respira_approval_required` with `approval_token`; pass it back to complete the update.',
5574
+ description: 'Update user information.\n\nApproval flow: the first call returns `respira_approval_required` with `approval_token`; pass it back to complete the update.',
5398
5575
  inputSchema: {
5399
5576
  type: 'object',
5400
5577
  properties: {
@@ -5435,7 +5612,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5435
5612
  },
5436
5613
  {
5437
5614
  name: 'wordpress_delete_user',
5438
- description: 'Delete a user.\n\nApproval flow (N9 fix, v6.19.0): destructive first call returns `respira_approval_required` with `approval_token`; pass it back to complete the delete.',
5615
+ description: 'Delete a user.\n\nApproval flow: the first call returns `respira_approval_required` with `approval_token`; pass it back to complete the delete.',
5439
5616
  inputSchema: {
5440
5617
  type: 'object',
5441
5618
  properties: {
@@ -6531,7 +6708,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
6531
6708
  },
6532
6709
  {
6533
6710
  name: 'wordpress_delete_custom_post',
6534
- description: 'Delete a custom post.\n\nSafety flow (N9 fix, v6.19.0): destructive. By default this only works on Respira-created duplicates. To delete an original: pass `force=true` AND `confirm_live_edit=true` (the latter requires "Allow Direct Editing" enabled in Respira settings). Approval gate may also fire if response is `respira_approval_required`, pass the returned `approval_token` back to complete the delete. Pre-v6.19.0 the schema lacked `force`, `confirm_live_edit`, and `approval_token` so agents could not complete the flow at all.',
6711
+ description: 'Delete a custom post.\n\nSafety flow: by default this only works on Respira-created duplicates. To delete an original: pass `force=true` AND `confirm_live_edit=true` (the latter requires "Allow Direct Editing" enabled in Respira settings). The approval gate may also fire: if the response is `respira_approval_required`, pass the returned `approval_token` back to complete the delete.',
6535
6712
  inputSchema: {
6536
6713
  type: 'object',
6537
6714
  properties: {
@@ -6610,7 +6787,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
6610
6787
  },
6611
6788
  {
6612
6789
  name: 'wordpress_update_option',
6613
- description: 'Update an option value.',
6790
+ description: 'Update an option value. Send the value in the same type get_option returns. A string stays a string, even when it contains JSON, because many plugins store JSON as text and decode it themselves. Send an object or array only for options that already hold one, such as theme_mods. Writing an object or array over an option that holds text is refused unless allow_type_change is true.',
6614
6791
  inputSchema: {
6615
6792
  type: 'object',
6616
6793
  properties: {
@@ -6619,7 +6796,15 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
6619
6796
  description: 'Option name',
6620
6797
  },
6621
6798
  value: {
6622
- description: 'Option value (string, number, boolean, object, or array). For complex options like theme_mods, pass as object not JSON string.',
6799
+ description: 'Option value, in the type get_option returns for this option. Text stays text (JSON-encode it if needed). Objects or arrays only for options that already hold them, such as theme_mods.',
6800
+ },
6801
+ allow_type_change: {
6802
+ type: 'boolean',
6803
+ description: 'Set true only to really replace an option that holds text with an object or array. Without it that write is refused, because a plugin that decodes its own text option can crash on every page load.',
6804
+ },
6805
+ allow_object_overwrite: {
6806
+ type: 'boolean',
6807
+ description: 'Set true only to replace an option that stores a PHP object, and only when the new value keeps the same shape. Without it that write is refused.',
6623
6808
  },
6624
6809
  },
6625
6810
  required: ['option', 'value'],
@@ -6771,7 +6956,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
6771
6956
  },
6772
6957
  {
6773
6958
  name: 'wordpress_delete_media',
6774
- description: 'Delete a media file. Approval-gated since v7.1.0-beta.1 — the first call returns `code: respira_approval_required` with an `approval_token`; pass that token in the second call to confirm. Same flow as `delete_page` / `delete_user` / `delete_plugin`.',
6959
+ description: 'Delete a media file. Approval-gated: the first call returns `code: respira_approval_required` with an `approval_token`; pass that token in the second call to confirm. Same flow as `delete_page` / `delete_user` / `delete_plugin`.',
6775
6960
  inputSchema: {
6776
6961
  type: 'object',
6777
6962
  properties: {
@@ -6891,7 +7076,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
6891
7076
  },
6892
7077
  {
6893
7078
  name: 'wordpress_delete_menu',
6894
- description: 'Delete a navigation menu.\n\nApproval flow (N9 fix, v6.19.0): destructive first call returns `respira_approval_required` with `approval_token`; pass it back to complete the delete.',
7079
+ description: 'Delete a navigation menu.\n\nApproval flow: the first call returns `respira_approval_required` with `approval_token`; pass it back to complete the delete.',
6895
7080
  inputSchema: {
6896
7081
  type: 'object',
6897
7082
  properties: {
@@ -7156,9 +7341,10 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
7156
7341
  }
7157
7342
  // Generate respira_* aliases for all wordpress_* tools.
7158
7343
  const allTools = this.generateDualTools(tools);
7159
- // Context-aware tool filtering: expose only relevant tools based on
7160
- // detected builder and active plugins. Reduces tool list from ~170 to
7161
- // ~30-60, saving significant tokens in AI agent context windows.
7344
+ // Context-aware tool filtering: builder-specific tools appear only on the
7345
+ // builder they serve, and WooCommerce tools only where WooCommerce runs.
7346
+ // When the site context cannot be read, everything is shown (fail-open).
7347
+ // The README's "Tools on one site" table has the measured counts.
7162
7348
  return this.filterToolsByContext(allTools);
7163
7349
  }
7164
7350
  /**
@@ -7719,25 +7905,6 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
7719
7905
  openWorldHint: false,
7720
7906
  },
7721
7907
  },
7722
- {
7723
- name: 'woocommerce_sales_report',
7724
- description: 'Deprecated compatibility alias for woocommerce_revenue_summary. Retained for two minor releases.',
7725
- inputSchema: {
7726
- type: 'object',
7727
- properties: {
7728
- period: { type: 'string', description: 'week, month, year, custom' },
7729
- date_min: { type: 'string', description: 'Start date for custom period' },
7730
- date_max: { type: 'string', description: 'End date for custom period' },
7731
- },
7732
- },
7733
- annotations: {
7734
- title: "Sales Report",
7735
- readOnlyHint: true,
7736
- destructiveHint: false,
7737
- idempotentHint: true,
7738
- openWorldHint: false,
7739
- },
7740
- },
7741
7908
  {
7742
7909
  name: 'woocommerce_revenue_summary',
7743
7910
  description: 'Bounded HPOS-compatible revenue summary with currency, store timezone, exact date boundaries, included statuses, pagination coverage, and explicit gross/discount/refund/tax/shipping/net definitions.',
@@ -9675,12 +9842,33 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
9675
9842
  const preview = serialized.slice(0, Math.min(2000, cap / 2));
9676
9843
  const paginationHint = TOOLS_WITH_PAGINATION_HINT[toolName] ||
9677
9844
  'Try re-calling this tool with pagination args (per_page, page, search filter) to limit the result size.';
9845
+ // Ticket 467cb71d: "_truncated: true" and two byte counts did not say
9846
+ // what was lost. On a 235 KB Divi page the preview stopped inside the
9847
+ // first section, far above the element being worked on, and nothing in
9848
+ // the envelope said so. Name the share that is missing and which
9849
+ // top-level keys did not survive the preview at all.
9850
+ const cutBytes = serialized.length - preview.length;
9851
+ const cutPercent = Math.max(1, Math.round((cutBytes / serialized.length) * 100));
9852
+ const topKeys = result && typeof result === 'object' && !Array.isArray(result) ? Object.keys(result) : [];
9853
+ const keysInPreview = topKeys.filter((key) => preview.includes(`${JSON.stringify(key)}:`));
9854
+ const keysCut = topKeys.filter((key) => !keysInPreview.includes(key));
9678
9855
  const envelope = {
9679
9856
  _truncated: true,
9680
9857
  _original_size_bytes: serialized.length,
9681
9858
  _cap_bytes: cap,
9682
9859
  _tool_name: toolName,
9683
9860
  _preview_json: preview,
9861
+ _returned_bytes: preview.length,
9862
+ _cut_bytes: cutBytes,
9863
+ _cut: `This response carries the first ${preview.length} bytes of ${serialized.length}. ` +
9864
+ `${cutBytes} bytes, about ${cutPercent}% of the answer, are not here, ` +
9865
+ 'and the preview stops mid-structure wherever that byte count fell.' +
9866
+ (keysCut.length
9867
+ ? ` Nothing was returned for: ${keysCut.join(', ')}.`
9868
+ : '') +
9869
+ (keysInPreview.length
9870
+ ? ` Partly present: ${keysInPreview.join(', ')}.`
9871
+ : ''),
9684
9872
  _hint: paginationHint,
9685
9873
  _why: 'Response exceeded the MCP server response-size cap. ' +
9686
9874
  'Most MCP hosts (Claude Desktop, Cursor) silently kill the subprocess ' +
@@ -9784,11 +9972,16 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
9784
9972
  return await client.listPages(args);
9785
9973
  case 'wordpress_read_page':
9786
9974
  return await client.getPage(args.id, args.include);
9787
- case 'wordpress_create_page_duplicate':
9975
+ case 'wordpress_create_page_duplicate': {
9976
+ const originalId = args.original_id ?? args.page_id ?? args.post_id;
9977
+ if (originalId === undefined || originalId === null) {
9978
+ throw new Error('create_page_duplicate needs original_id, page_id, or post_id.');
9979
+ }
9788
9980
  return {
9789
- ...(await client.duplicatePage(args.original_id, args.suffix, args.include)),
9981
+ ...(await client.duplicatePage(originalId, args.suffix, args.include)),
9790
9982
  respira_approvals_url: client.getApprovalsUrl(),
9791
9983
  };
9984
+ }
9792
9985
  case 'wordpress_update_page': {
9793
9986
  const approvalsUrl = client.getApprovalsUrl();
9794
9987
  const page = await client.updatePage(args.id, args);
@@ -9817,11 +10010,19 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
9817
10010
  return await client.listPosts(args);
9818
10011
  case 'wordpress_read_post':
9819
10012
  return await client.getPost(args.id, args.include);
9820
- case 'wordpress_create_post_duplicate':
10013
+ case 'wordpress_create_post_duplicate': {
10014
+ // Same shape as the page tool above (ticket 81c478df): a missing id
10015
+ // used to reach WordPress as the literal string "undefined" in the
10016
+ // path, and came back as a 404 that read like a plugin problem.
10017
+ const originalId = args.original_id ?? args.post_id ?? args.page_id;
10018
+ if (originalId === undefined || originalId === null) {
10019
+ throw new Error('create_post_duplicate needs original_id, post_id, or page_id.');
10020
+ }
9821
10021
  return {
9822
- ...(await client.duplicatePost(args.original_id, args.suffix, args.include)),
10022
+ ...(await client.duplicatePost(originalId, args.suffix, args.include)),
9823
10023
  respira_approvals_url: client.getApprovalsUrl(),
9824
10024
  };
10025
+ }
9825
10026
  case 'respira_duplicate_with_translations':
9826
10027
  return await client.duplicateWithTranslations({
9827
10028
  post_id: args.post_id,
@@ -10092,7 +10293,11 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
10092
10293
  case 'wordpress_analyze_performance':
10093
10294
  return await client.analyzePerformance(args.page_id);
10094
10295
  case 'wordpress_get_core_web_vitals':
10095
- return await client.getCoreWebVitals(args.page_id);
10296
+ return await client.getCoreWebVitals({
10297
+ page_id: args.page_id,
10298
+ url: args.url,
10299
+ strategy: args.strategy,
10300
+ });
10096
10301
  case 'wordpress_run_pagespeed_audit':
10097
10302
  return await client.runPageSpeedAudit({
10098
10303
  page_id: args.page_id,
@@ -10124,7 +10329,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
10124
10329
  case 'wordpress_check_structured_data':
10125
10330
  return await client.checkStructuredData(args.page_id);
10126
10331
  case 'wordpress_run_security_audit':
10127
- return await client.runSecurityAudit(args.advisory_ids || [], args.deep_scan === true);
10332
+ return await client.runSecurityAudit(args.advisory_ids || [], args.deep_scan === true, args.probe_uploads_execution === true, args.known_vulnerabilities !== false);
10128
10333
  case 'wordpress_update_core_security':
10129
10334
  return await client.updateCoreSecurity({
10130
10335
  advisoryId: args.advisory_id,
@@ -10132,6 +10337,10 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
10132
10337
  backupConfirmed: args.backup_confirmed === true,
10133
10338
  approvalToken: args.approval_token,
10134
10339
  });
10340
+ case 'wordpress_update_theme':
10341
+ return await client.updateTheme(args.stylesheet, args.approval_token);
10342
+ case 'wordpress_revoke_application_password':
10343
+ return await client.revokeApplicationPassword(Number(args.user_id), String(args.uuid), args.approval_token);
10135
10344
  // Accessibility
10136
10345
  case 'wordpress_list_accessibility_scans':
10137
10346
  return await client.listAccessibilityScans({
@@ -10145,7 +10354,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
10145
10354
  return await client.scanPageAccessibility(args.page_id, args.standard, args.url);
10146
10355
  case 'wordpress_apply_accessibility_fixes':
10147
10356
  return await client.applyAccessibilityFixes(args.scan_id, args.rule_ids);
10148
- // Plugin Management (EXPERIMENTAL)
10357
+ // Plugin Management
10149
10358
  case 'wordpress_list_plugins':
10150
10359
  return await client.listPlugins();
10151
10360
  case 'wordpress_install_plugin':
@@ -10274,7 +10483,10 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
10274
10483
  case 'wordpress_get_option':
10275
10484
  return await client.getOption(args.option);
10276
10485
  case 'wordpress_update_option':
10277
- return await client.updateOption(args.option, args.value);
10486
+ return await client.updateOption(args.option, args.value, {
10487
+ allow_type_change: args.allow_type_change,
10488
+ allow_object_overwrite: args.allow_object_overwrite,
10489
+ });
10278
10490
  case 'wordpress_delete_option':
10279
10491
  return await client.deleteOption(args.option, args.approval_token);
10280
10492
  case 'wordpress_purge_cache':
@@ -10341,6 +10553,10 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
10341
10553
  return await client.callRestV1('GET', '/activity', args);
10342
10554
  case 'wordpress_get_activity':
10343
10555
  return await client.callRestV1('GET', `/activity/${args.id}`);
10556
+ case 'wordpress_generate_activity_report': {
10557
+ const { site_id: _siteId, ...reportInput } = args || {};
10558
+ return await client.generateActivityReport(reportInput);
10559
+ }
10344
10560
  case 'wordpress_get_site_template':
10345
10561
  return await client.callRestV2('GET', '/fse/template', args);
10346
10562
  case 'wordpress_create_site_template':
@@ -10478,8 +10694,6 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
10478
10694
  const { id, ...payload } = args;
10479
10695
  return await client.woocommerceUpdateStock(id, payload);
10480
10696
  }
10481
- case 'woocommerce_sales_report':
10482
- return await client.woocommerceSalesReport(args);
10483
10697
  case 'woocommerce_revenue_summary':
10484
10698
  return await client.callRestV1('GET', '/woocommerce/reports/revenue-summary', args);
10485
10699
  case 'woocommerce_sales_timeseries':
@@ -10891,6 +11105,19 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
10891
11105
  }
10892
11106
  // Return isError result instead of throwing — unknown tool is an execution
10893
11107
  // error, not a protocol error. This lets LLMs self-correct gracefully.
11108
+ // A tool removed in a major release answers with its replacement, so a
11109
+ // saved workflow or an older skill learns the new name in one turn
11110
+ // instead of guessing from "Unknown tool".
11111
+ const retired = RETIRED_TOOLS[name];
11112
+ if (retired) {
11113
+ return {
11114
+ __respira_is_error: true,
11115
+ error: `Retired tool: ${name}`,
11116
+ code: 'respira_tool_retired',
11117
+ replacement: retired.replacement,
11118
+ hint: `${name} was removed in ${retired.removedIn}. Call ${retired.replacement} instead. ${retired.note}`,
11119
+ };
11120
+ }
10894
11121
  return {
10895
11122
  __respira_is_error: true,
10896
11123
  error: `Unknown tool: ${name}`,
@@ -10922,6 +11149,18 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
10922
11149
  type: 'string',
10923
11150
  description: 'Optional text to narrow the result set to elements whose own text or attribute values contain it. Works for every identifier_type. It separates siblings; it cannot separate an element from an ancestor that contains it, so use identifier_type "path" when you need certainty.',
10924
11151
  },
11152
+ // Ticket 467cb71d: the plugin has honoured `include` since 7.1.0
11153
+ // and the compact answer told agents to pass it, but it was missing
11154
+ // from this schema, so hosts dropped it before the call left. On a
11155
+ // 235 KB Divi page that left no way at all to read one element:
11156
+ // extract_builder_content was over the response cap and this was
11157
+ // the documented alternative. The element could be written and
11158
+ // never read, so the value had to be reconstructed from the
11159
+ // rendered front end, which added attributes nobody asked for.
11160
+ include: {
11161
+ type: 'string',
11162
+ description: 'CSV of optional response sections. Pass "content" (or "full") for the element\'s stored body: settings, attributes and children, exactly as the builder saved them. Without it the answer is a compact summary with a one-line preview, which cannot be edited from. This is the way to read one element on a page too large for extract_builder_content.',
11163
+ },
10925
11164
  },
10926
11165
  required: ['post_id', 'identifier_type', 'identifier_value'],
10927
11166
  },
@@ -10969,7 +11208,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
10969
11208
  editTarget: {
10970
11209
  type: 'string',
10971
11210
  enum: ['live', 'duplicate'],
10972
- description: 'Deprecated camelCase alias of edit_target. Prefer edit_target. Kept for back-compat with pre-v6.19.5 callers.',
11211
+ description: 'Deprecated camelCase alias of edit_target. Prefer edit_target. Kept so older callers keep working.',
10973
11212
  },
10974
11213
  confirm_live_edit: {
10975
11214
  type: 'boolean',