@respira/wordpress-mcp-server 8.3.10 → 8.3.11

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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=empty-read-is-not-an-error.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"empty-read-is-not-an-error.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/empty-read-is-not-an-error.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,65 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { isEmptyReadResult, readErrorCode, EMPTY_READ_TOOLS, EMPTY_READ_CODES } from '../server.js';
4
+ // A search that matched nothing is an answer, not a failure.
5
+ //
6
+ // Telemetry, 30 days to 2026-08-24: respira_element_not_found was the largest
7
+ // caller_error code at 4,626 calls, and 4,278 of them (92.5%) came from
8
+ // find_element itself. Across the read surface that was 5,695 calls over 318 of
9
+ // 716 sites reported to agents as errors. The agent-facing cost is that a failed
10
+ // search and a broken site look identical, so agents give up on pages they could
11
+ // have edited.
12
+ //
13
+ // These tests exist for the OTHER half: the narrowness. Turning an error into a
14
+ // success is the direction this codebase's worst bugs went, so the write path
15
+ // and the bad-site-id path must keep failing loudly.
16
+ test('a read tool reporting nothing found is not an error', () => {
17
+ for (const tool of ['find_element', 'get_option', 'read_page', 'get_media']) {
18
+ for (const prefix of ['', 'respira_', 'wordpress_']) {
19
+ assert.equal(isEmptyReadResult(prefix + tool, { name: 'respira_element_not_found' }), true, `${prefix}${tool} should be treated as an empty read`);
20
+ }
21
+ }
22
+ });
23
+ test('a WRITE reporting not-found stays an error', () => {
24
+ // The whole point of the narrowness. On a write, "not found" means the caller
25
+ // targeted something that does not exist, which it needs to hear about.
26
+ for (const tool of [
27
+ 'update_element', 'remove_element', 'move_element', 'duplicate_element',
28
+ 'batch_update', 'update_page', 'update_post', 'delete_page', 'apply_builder_patch',
29
+ ]) {
30
+ for (const prefix of ['', 'respira_', 'wordpress_']) {
31
+ assert.equal(isEmptyReadResult(prefix + tool, { name: 'respira_element_not_found' }), false, `${prefix}${tool} is a write and must stay an error`);
32
+ }
33
+ }
34
+ });
35
+ test('a bad site id stays an error even on a read tool', () => {
36
+ // 410 calls over 25 sites in the same window. That is a misconfigured client,
37
+ // not an empty page, and it must not hide behind "nothing matched".
38
+ assert.equal(isEmptyReadResult('respira_find_element', { name: 'respira_site_id_not_found' }), false);
39
+ assert.equal(EMPTY_READ_CODES.has('respira_site_id_not_found'), false);
40
+ });
41
+ test('a read tool failing for any other reason stays an error', () => {
42
+ for (const code of [
43
+ 'respira_no_builder', 'rest_no_route', 'http_500', 'respira_invalid_site_token',
44
+ 'respira_rate_limit_exceeded', 'Error', '',
45
+ ]) {
46
+ assert.equal(isEmptyReadResult('respira_find_element', { name: code }), false, `find_element + ${code || '(empty)'} must stay an error`);
47
+ }
48
+ });
49
+ test('readErrorCode prefers the explicit code and falls back to the promoted name', () => {
50
+ assert.equal(readErrorCode({ code: 'respira_option_not_found' }), 'respira_option_not_found');
51
+ // wordpress-client promotes the WP_Error code onto error.name
52
+ assert.equal(readErrorCode({ name: 'respira_post_not_found' }), 'respira_post_not_found');
53
+ assert.equal(readErrorCode({ code: 'a', name: 'b' }), 'a');
54
+ assert.equal(readErrorCode({}), '');
55
+ assert.equal(readErrorCode(null), '');
56
+ });
57
+ test('every tool on the empty-read list reads rather than writes', () => {
58
+ // A name-shaped rule would quietly start swallowing errors the first time
59
+ // someone adds find_and_replace. This asserts the list stays a read list.
60
+ const writeVerbs = /(^|_)(update|create|delete|remove|move|duplicate|apply|inject|build|batch|upload|install|activate|save|import)(_|$)/;
61
+ for (const tool of EMPTY_READ_TOOLS) {
62
+ assert.ok(!writeVerbs.test(tool), `${tool} looks like a write and must not be on the empty-read list`);
63
+ }
64
+ });
65
+ //# sourceMappingURL=empty-read-is-not-an-error.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"empty-read-is-not-an-error.test.js","sourceRoot":"","sources":["../../src/__tests__/empty-read-is-not-an-error.test.ts"],"names":[],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -68,6 +68,38 @@ export declare function applyActiveSiteIdForConfig(state: RespiraMcpState, names
68
68
  * agent would learn the wrong contract from a call that happened to work.
69
69
  */
70
70
  export declare function requireAbilityName(args: Record<string, any> | undefined): string;
71
+ /**
72
+ * The read/search tools where "nothing matched" is a complete answer.
73
+ *
74
+ * An explicit list, not a regex on the tool name. `find_builder_targets` and
75
+ * `find_element` both start with "find" and so does nothing else that writes,
76
+ * but a name-shaped rule would quietly start swallowing errors the first time
77
+ * someone adds `find_and_replace`. The cost of this list is remembering to add
78
+ * to it; the cost of the regex is a silent write failure, and this codebase has
79
+ * paid that one before.
80
+ *
81
+ * Names are matched after stripping the `respira_` / `wordpress_` prefix, since
82
+ * both surfaces exist and the telemetry shows both in use.
83
+ */
84
+ export declare const EMPTY_READ_TOOLS: Set<string>;
85
+ /**
86
+ * "Not found" codes that mean the search came back empty.
87
+ *
88
+ * respira_site_id_not_found is deliberately absent: that is the caller naming a
89
+ * site that does not exist, which is a caller mistake on any tool and must stay
90
+ * an error. It was 410 calls over 25 sites in the 30 days to 2026-08-24, and
91
+ * folding it in here would hide a misconfigured client behind a friendly
92
+ * "nothing matched".
93
+ */
94
+ export declare const EMPTY_READ_CODES: Set<string>;
95
+ /** The WP_Error code, which wordpress-client promotes onto error.name. */
96
+ export declare function readErrorCode(error: any): string;
97
+ /**
98
+ * True when this is a read/search tool reporting that nothing matched.
99
+ * Both halves must hold: the wrong tool with the right code, or the right tool
100
+ * with any other code, stays an error.
101
+ */
102
+ export declare function isEmptyReadResult(toolName: string, error: any): boolean;
71
103
  export declare class RespiraWordPressServer {
72
104
  private server;
73
105
  private currentSite;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -302,6 +302,32 @@ function hoistDroppedStylingWarnings(result) {
302
302
  * flipping the global current site (different intent from per-call override)
303
303
  * - redeem_token: pre-site setup; no site exists yet
304
304
  */
305
+ /**
306
+ * Pull the builder a tool actually acted through out of its response.
307
+ *
308
+ * Responses are not uniform about this. Element operations return
309
+ * `builder: { name, version }`, extract/inject return a bare `builder` string,
310
+ * and builder-info style responses use `active_builder` or `page_builder`.
311
+ * Accepting all three is cheaper than making every tool agree, and a null here
312
+ * is exactly as informative as the column was before: empty.
313
+ *
314
+ * The ingest endpoint lowercases and strips this to [a-z0-9_-], so no
315
+ * normalisation is needed on the way out.
316
+ */
317
+ function telemetryBuilderName(result) {
318
+ if (!result || typeof result !== 'object')
319
+ return null;
320
+ const candidates = [result.builder, result.active_builder, result.page_builder];
321
+ for (const candidate of candidates) {
322
+ if (typeof candidate === 'string' && candidate.trim()) {
323
+ return candidate.trim().slice(0, 40);
324
+ }
325
+ if (candidate && typeof candidate === 'object' && typeof candidate.name === 'string' && candidate.name.trim()) {
326
+ return candidate.name.trim().slice(0, 40);
327
+ }
328
+ }
329
+ return null;
330
+ }
305
331
  const SITE_AGNOSTIC_TOOLS = new Set([
306
332
  'wordpress_list_sites',
307
333
  'wordpress_get_active_site',
@@ -372,6 +398,76 @@ const TOOLS_WITH_PAGINATION_HINT = {
372
398
  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.',
373
399
  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.',
374
400
  };
401
+ /**
402
+ * The read/search tools where "nothing matched" is a complete answer.
403
+ *
404
+ * An explicit list, not a regex on the tool name. `find_builder_targets` and
405
+ * `find_element` both start with "find" and so does nothing else that writes,
406
+ * but a name-shaped rule would quietly start swallowing errors the first time
407
+ * someone adds `find_and_replace`. The cost of this list is remembering to add
408
+ * to it; the cost of the regex is a silent write failure, and this codebase has
409
+ * paid that one before.
410
+ *
411
+ * Names are matched after stripping the `respira_` / `wordpress_` prefix, since
412
+ * both surfaces exist and the telemetry shows both in use.
413
+ */
414
+ export const EMPTY_READ_TOOLS = new Set([
415
+ 'find_element',
416
+ 'find_builder_targets',
417
+ 'get_option',
418
+ 'get_media',
419
+ 'get_post_type',
420
+ 'get_custom_post',
421
+ 'get_term',
422
+ 'get_menu',
423
+ 'get_menu_item',
424
+ 'get_page_outline',
425
+ 'read_page',
426
+ 'read_page_v2',
427
+ 'read_post',
428
+ 'read_post_v2',
429
+ 'read_custom_post_v2',
430
+ 'read_theme_file',
431
+ 'extract_builder_content',
432
+ ]);
433
+ /**
434
+ * "Not found" codes that mean the search came back empty.
435
+ *
436
+ * respira_site_id_not_found is deliberately absent: that is the caller naming a
437
+ * site that does not exist, which is a caller mistake on any tool and must stay
438
+ * an error. It was 410 calls over 25 sites in the 30 days to 2026-08-24, and
439
+ * folding it in here would hide a misconfigured client behind a friendly
440
+ * "nothing matched".
441
+ */
442
+ export const EMPTY_READ_CODES = new Set([
443
+ 'respira_element_not_found',
444
+ 'respira_option_not_found',
445
+ 'respira_post_not_found',
446
+ 'respira_page_not_found',
447
+ 'respira_post_type_not_found',
448
+ 'respira_media_not_found',
449
+ 'respira_theme_file_not_found',
450
+ 'respira_module_not_found',
451
+ 'respira_acf_group_not_found',
452
+ 'respira_acf_post_not_found',
453
+ ]);
454
+ /** The WP_Error code, which wordpress-client promotes onto error.name. */
455
+ export function readErrorCode(error) {
456
+ const fromName = typeof error?.name === 'string' ? error.name : '';
457
+ const fromCode = typeof error?.code === 'string' ? error.code : '';
458
+ return fromCode || fromName || '';
459
+ }
460
+ /**
461
+ * True when this is a read/search tool reporting that nothing matched.
462
+ * Both halves must hold: the wrong tool with the right code, or the right tool
463
+ * with any other code, stays an error.
464
+ */
465
+ export function isEmptyReadResult(toolName, error) {
466
+ const bare = String(toolName || '').replace(/^(respira_|wordpress_)+/, '');
467
+ if (!EMPTY_READ_TOOLS.has(bare))
468
+ return false;
469
+ return EMPTY_READ_CODES.has(readErrorCode(error));
470
+ }
375
471
  export class RespiraWordPressServer {
376
472
  server;
377
473
  currentSite = null;
@@ -1789,6 +1885,53 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
1789
1885
  clearTimeout(timeoutHandle);
1790
1886
  }
1791
1887
  this.currentSite?.setCurrentToolName(null);
1888
+ // A search that matched nothing is an answer, not a failure.
1889
+ //
1890
+ // Telemetry, 30 days to 2026-08-24: respira_element_not_found is the
1891
+ // single largest caller_error code at 4,626 calls, and 4,278 of those
1892
+ // (92.5%) come from find_element itself. The same shape repeats on
1893
+ // get_option (632 calls / 173 sites), read_post, get_post_type,
1894
+ // read_page, get_media and read_theme_file. Across the read surface
1895
+ // that is 5,695 calls over 318 of 716 sites, reported to the agent as
1896
+ // errors when the honest answer is "nothing matched".
1897
+ //
1898
+ // It costs more than a wrong statistic. An agent that gets an error
1899
+ // back from find_element cannot tell "this page has no such element,
1900
+ // try another identifier" from "the tool or the site is broken". The
1901
+ // first should prompt a different query and the second should prompt a
1902
+ // stop, and today they are indistinguishable, so agents give up on
1903
+ // pages they could have edited.
1904
+ //
1905
+ // Deliberately narrow, because turning an error into a success is the
1906
+ // exact direction this codebase's worst bugs went:
1907
+ // - only read/search tools, never a write. On a write, "not found"
1908
+ // means the caller targeted something that does not exist and it
1909
+ // must stay an error.
1910
+ // - only *_not_found codes, and never respira_site_id_not_found: a
1911
+ // bad site id IS a caller mistake and stays loud.
1912
+ // - the payload says found: false and matches: [] in as many words.
1913
+ // It never fabricates a result, and isError stays false only
1914
+ // because the question was answered.
1915
+ if (isEmptyReadResult(name, error)) {
1916
+ return {
1917
+ content: [
1918
+ {
1919
+ type: 'text',
1920
+ text: JSON.stringify({
1921
+ found: false,
1922
+ matches: [],
1923
+ tool: name,
1924
+ reason: readErrorCode(error),
1925
+ message: 'Nothing matched. This is a complete answer, not a failure: the site responded and ' +
1926
+ 'contains no item matching what was asked for. Try a different identifier rather than ' +
1927
+ 'retrying the same one. For elements, identifier_type "content" searches visible text ' +
1928
+ 'and "path" names exactly one element; respira_get_page_outline lists what is actually ' +
1929
+ 'on the page.',
1930
+ }),
1931
+ },
1932
+ ],
1933
+ };
1934
+ }
1792
1935
  if (error instanceof ToolTimeoutError) {
1793
1936
  const activeSite = this.getActiveSiteSummary(args);
1794
1937
  return {
@@ -7412,6 +7555,13 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
7412
7555
  stageTimings: result?.write_diag?.stage_timings || result?.stage_timings || null,
7413
7556
  pluginVersion,
7414
7557
  mcpVersion: MCP_SERVER_VERSION,
7558
+ // usage-emitter has accepted and sanitised a `builder` field since it
7559
+ // was written, and mcp_tool_events.builder was NULL for every one of
7560
+ // 236,803 events over 30 days, because no call site ever passed it.
7561
+ // Per-builder quality was therefore invisible in the event stream for
7562
+ // exactly the builders that carry the most traffic. The plugin already
7563
+ // returns the builder it acted through; this forwards it.
7564
+ builder: telemetryBuilderName(result),
7415
7565
  // The plugin compares a storage signature before and after a write and
7416
7566
  // says so on the response when nothing moved. It reports that as a
7417
7567
  // warning on a SUCCESSFUL call, because the call was well formed, so
@@ -8507,6 +8657,22 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
8507
8657
  // unambiguous.
8508
8658
  r.agent_must_escalate = true;
8509
8659
  }
8660
+ // The plugin used to fail this write outright whenever it could not
8661
+ // read the changed value back off the public page, including when the
8662
+ // page simply did not answer in time. That was 8.9% of live Elementor
8663
+ // element updates, every one of them a write that had already
8664
+ // persisted, and the agent's response to a failure is to retry. The
8665
+ // write now succeeds and says what could not be confirmed, but this
8666
+ // tool has an 81% in-session retry rate precisely because agents do not
8667
+ // read nested fields, so the caveat has to be in the message or it may
8668
+ // as well not exist. Softer than the no-op headline on purpose: nothing
8669
+ // is wrong, the write landed, only the confirmation is missing.
8670
+ else if (r && typeof r === 'object' &&
8671
+ Array.isArray(r.warnings) && r.warnings.includes('elementor_frontend_unverifiable')) {
8672
+ const hint = r.frontend_verification_hint || 'The write persisted but Respira could not read the public page to confirm it is visible. Do not retry the same mutation.';
8673
+ const original = typeof r.message === 'string' ? r.message : '';
8674
+ r.message = `Note — write saved, publication unconfirmed. ${hint}\n\nOriginal message: ${original}`.trim();
8675
+ }
8510
8676
  return r;
8511
8677
  }
8512
8678
  case 'wordpress_move_element': {