@respira/wordpress-mcp-server 7.5.3 → 7.5.5

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/dist/server.js CHANGED
@@ -428,6 +428,8 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
428
428
  - Content format: Array of sections containing columns containing widgets
429
429
  - Each widget has a widgetType (e.g. "heading", "text-editor", "image", "button") and settings object
430
430
  - Dynamic schemas: Respira reads Elementor's control registry at runtime, so it knows the exact settings, types, and valid values for every widget
431
+ - This includes registered third-party/theme widgets. For an existing custom widget: list_elementor_widgets -> get_elementor_widget_schema -> find_element by content/type -> update_element by the returned stable id with only changed settings -> find_element again to verify
432
+ - Never use extract_builder_content + inject_builder_content or rewrite _elementor_data for a single-widget edit. Widget source code is only a fallback when its rendering semantics are not represented in the registered controls
431
433
  - Settings validator catches typos: "Unknown setting 'titel'. Did you mean 'title'?"
432
434
  - Responsive: settings support _mobile and _tablet suffixes (e.g. "align" for desktop, "align_mobile" for mobile)
433
435
  - respira_find_element works with Elementor widget IDs, types, CSS classes, and text content
@@ -4380,26 +4382,80 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
4380
4382
  * Widget shortcuts are filtered to only the detected builder's element types.
4381
4383
  * Core tools (site context, pages, posts, media, menus, etc.) always remain.
4382
4384
  */
4383
- /** Cached site context for tool filtering (refreshed on site switch). */
4385
+ /** Cached site context, shared by tool filtering and the WooCommerce/ACF
4386
+ * availability checks below — all three used to independently call
4387
+ * getSiteContext() (getCompactSiteContext() itself just wraps it), meaning
4388
+ * every getTools() invocation made up to three uncached, sequential live
4389
+ * HTTP calls to the customer's own WordPress site before it could return
4390
+ * the tool catalog. Each call carries the wordpress-client retry backoff
4391
+ * (200/600/1800ms across 3 attempts), so a slow or failing site could push
4392
+ * a single tools/list response past 7s — long enough for an MCP client's
4393
+ * own handshake patience to give up and tear down the process, which
4394
+ * looks identical to a crash with zero stderr output (nathankohut.co.uk,
4395
+ * Windows, 2026-07-19). getContextForToolFiltering() below is the only
4396
+ * caller of currentSite.getSiteContext() in this class; everything that
4397
+ * used to call it directly or via getCompactSiteContext() now shares this
4398
+ * one cached, timeout-bounded fetch instead. */
4384
4399
  cachedFilterContext = null;
4385
- static FILTER_CACHE_TTL = 60_000; // 1 minute
4386
- async filterToolsByContext(tools) {
4400
+ static FILTER_CACHE_TTL = 60_000; // successful fetch: 1 minute
4401
+ static FILTER_CACHE_FAILURE_TTL = 15_000; // failed fetch: retry sooner
4402
+ static FILTER_CONTEXT_TIMEOUT_MS = 2_000;
4403
+ /** Dedupes concurrent callers (e.g. isWooCommerceAddonAvailable() and
4404
+ * isAcfAvailable() both missing a cold cache at once) onto one in-flight
4405
+ * fetch instead of each starting their own. */
4406
+ filterContextFetchInFlight = null;
4407
+ /**
4408
+ * Fetch (or reuse a cached / in-flight) site context for tool-catalog
4409
+ * decisions. Bounded to FILTER_CONTEXT_TIMEOUT_MS regardless of how long
4410
+ * the underlying client's own retry backoff would otherwise take, so a
4411
+ * slow or unreachable site can never block tools/list indefinitely.
4412
+ * Resolves to null (and caches the miss briefly) on timeout or any fetch
4413
+ * error — callers treat null as "show everything / skip context-dependent
4414
+ * gating" rather than failing the whole tool list.
4415
+ */
4416
+ async getContextForToolFiltering() {
4387
4417
  if (!this.currentSite) {
4388
- return tools; // No site connected — show everything.
4418
+ return null;
4389
4419
  }
4390
- let context;
4391
4420
  const now = Date.now();
4392
- if (this.cachedFilterContext && (now - this.cachedFilterContext.timestamp) < RespiraWordPressServer.FILTER_CACHE_TTL) {
4393
- context = this.cachedFilterContext.context;
4421
+ if (this.cachedFilterContext) {
4422
+ const ttl = this.cachedFilterContext.context
4423
+ ? RespiraWordPressServer.FILTER_CACHE_TTL
4424
+ : RespiraWordPressServer.FILTER_CACHE_FAILURE_TTL;
4425
+ if (now - this.cachedFilterContext.timestamp < ttl) {
4426
+ return this.cachedFilterContext.context;
4427
+ }
4394
4428
  }
4395
- else {
4429
+ if (this.filterContextFetchInFlight) {
4430
+ return this.filterContextFetchInFlight;
4431
+ }
4432
+ const fetchPromise = (async () => {
4433
+ let context = null;
4396
4434
  try {
4397
- context = await this.currentSite.getCompactSiteContext();
4398
- this.cachedFilterContext = { context, timestamp: now };
4435
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('site context fetch timed out')), RespiraWordPressServer.FILTER_CONTEXT_TIMEOUT_MS));
4436
+ context = await Promise.race([this.currentSite.getSiteContext(), timeout]);
4399
4437
  }
4400
4438
  catch {
4401
- return tools; // Can't fetch context show everything.
4439
+ context = null;
4402
4440
  }
4441
+ this.cachedFilterContext = { context, timestamp: now };
4442
+ return context;
4443
+ })();
4444
+ this.filterContextFetchInFlight = fetchPromise;
4445
+ try {
4446
+ return await fetchPromise;
4447
+ }
4448
+ finally {
4449
+ this.filterContextFetchInFlight = null;
4450
+ }
4451
+ }
4452
+ async filterToolsByContext(tools) {
4453
+ if (!this.currentSite) {
4454
+ return tools; // No site connected — show everything.
4455
+ }
4456
+ const context = await this.getContextForToolFiltering();
4457
+ if (!context) {
4458
+ return tools; // Can't fetch context — show everything.
4403
4459
  }
4404
4460
  const detectedBuilder = (context?.page_builder?.name || '').toLowerCase();
4405
4461
  const hasWooCommerce = Boolean(context?.woocommerce?.active || context?.addons?.woocommerce?.installed);
@@ -4430,16 +4486,11 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
4430
4486
  });
4431
4487
  }
4432
4488
  async isWooCommerceAddonAvailable() {
4433
- if (!this.currentSite) {
4434
- return false;
4435
- }
4436
- try {
4437
- const context = await this.currentSite.getSiteContext();
4438
- return Boolean(context.addons?.woocommerce?.installed && context.addons?.woocommerce?.licensed);
4439
- }
4440
- catch {
4489
+ const context = await this.getContextForToolFiltering();
4490
+ if (!context) {
4441
4491
  return false;
4442
4492
  }
4493
+ return Boolean(context.addons?.woocommerce?.installed && context.addons?.woocommerce?.licensed);
4443
4494
  }
4444
4495
  /**
4445
4496
  * Detect whether ACF is active on the currently selected site.
@@ -4447,16 +4498,11 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
4447
4498
  * (a Pro tool on a non-Pro site returns PRO_FEATURE_REQUIRED).
4448
4499
  */
4449
4500
  async isAcfAvailable() {
4450
- if (!this.currentSite) {
4451
- return false;
4452
- }
4453
- try {
4454
- const context = await this.currentSite.getSiteContext();
4455
- return Boolean(context.addons?.acf?.installed);
4456
- }
4457
- catch {
4501
+ const context = await this.getContextForToolFiltering();
4502
+ if (!context) {
4458
4503
  return false;
4459
4504
  }
4505
+ return Boolean(context.addons?.acf?.installed);
4460
4506
  }
4461
4507
  getWooCommerceTools() {
4462
4508
  return [
@@ -6838,7 +6884,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
6838
6884
  },
6839
6885
  {
6840
6886
  name: 'wordpress_update_element',
6841
- description: 'Update settings or content on a specific element in a page. This is the PRIMARY tool for making content changes (text edits, style changes, image swaps, link updates, etc.). Works with all 12 page builders. First use find_element to locate the element, then pass the same identifier here with the updates object containing the new values. On WPBakery + Uncode pages, prefer identifier_type "uncode_shortcode_id" for stable round-trip matching. Response includes target_id, original_id, edit_target ("live" or "duplicate"), is_duplicate, duplicate_created and post_status so the caller can never mistake a duplicate-routed write for a live-page change.\n\nLive-edit confirmation: when respira_allow_direct_edit=1 AND the post is a published original, the first call returns `status: "confirmation_required"` with a `next_call_examples` payload. Re-call with the suggested `edit_target` (and `confirm_live_edit: true` for the live path) to acknowledge. Both params are exposed at the top level here so autonomous flows can complete the handshake through MCP without polluting the `updates` object.',
6887
+ description: 'Update settings or content on one specific element. This is the PRIMARY tool for targeted changes (text, style, image, link, or a custom Elementor widget) and avoids rewriting the full builder document. First use find_element, then target its stable id and send only changed settings. For an Elementor custom/third-party widget, call list_elementor_widgets and get_elementor_widget_schema first; never use extract+inject or rewrite _elementor_data for a one-widget edit. On WPBakery + Uncode, prefer identifier_type "uncode_shortcode_id". Re-read with find_element after writing. Response identifies the resolved live/duplicate target so it cannot be mistaken for a live-page change.\n\nLive-edit confirmation: when respira_allow_direct_edit=1 AND the post is a published original, the first call returns `status: "confirmation_required"` with a `next_call_examples` payload. Re-call with the suggested `edit_target` (and `confirm_live_edit: true` for the live path) to acknowledge. Both params are exposed at the top level here so autonomous flows can complete the handshake through MCP without polluting the `updates` object.',
6842
6888
  inputSchema: {
6843
6889
  type: 'object',
6844
6890
  properties: {