@respira/wordpress-mcp-server 6.11.12 → 6.12.0

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
@@ -6,11 +6,37 @@
6
6
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
7
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
9
+ import { readFileSync } from 'fs';
10
+ import { dirname, resolve } from 'path';
11
+ import { fileURLToPath } from 'url';
9
12
  import { WordPressClient } from './wordpress-client.js';
10
13
  import { RespiraVersionChecker } from './version-checker.js';
11
14
  import { getBricksTools, dispatchBricksTool } from './bricks-tools.js';
12
15
  import { getElementorTools, dispatchElementorTool } from './elementor-tools.js';
13
16
  import { getAcfTools, ACF_TOOL_NAMES } from './acf-tools.js';
17
+ /**
18
+ * Read the server version from package.json at module load time so the
19
+ * MCP handshake, the `instructions` block, and the version-checker all
20
+ * report the actual installed version instead of a stale hard-coded
21
+ * string. K.B. on the customer site reported v6.11.13 self-identifying
22
+ * as 6.11.4 because the constant below was last hand-bumped at the
23
+ * 6.11.4 release and never tracked subsequent npm publishes. Mirrors
24
+ * the existing MCP_CLIENT_VERSION helper in wordpress-client.ts.
25
+ */
26
+ const MCP_SERVER_VERSION = (() => {
27
+ try {
28
+ const currentDir = dirname(fileURLToPath(import.meta.url));
29
+ const packageJsonPath = resolve(currentDir, '../package.json');
30
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
31
+ if (typeof packageJson.version === 'string' && packageJson.version.trim()) {
32
+ return packageJson.version.trim();
33
+ }
34
+ }
35
+ catch {
36
+ // Fall through to the literal fallback below.
37
+ }
38
+ return 'unknown';
39
+ })();
14
40
  /**
15
41
  * Thrown by the per-call watchdog when a tool handler exceeds its deadline.
16
42
  * Surfaces as a structured `tool_timeout` response so the stdio loop stays
@@ -32,6 +58,43 @@ class ToolTimeoutError extends Error {
32
58
  function getMaxToolTimeoutMs() {
33
59
  return Math.max(5000, parseInt(process.env.RESPIRA_MAX_TOOL_TIMEOUT_MS || '120000', 10));
34
60
  }
61
+ /**
62
+ * v6.12.0: Tool names that do NOT receive the optional site_id input parameter.
63
+ * These tools operate on global Respira config or pre-site setup state, so a
64
+ * per-call site override doesn't apply:
65
+ * - list_sites: returns all configured sites
66
+ * - get_active_site: returns the global current site (a per-call override
67
+ * would be meaningless here; if you want a specific site's summary, look
68
+ * at list_sites)
69
+ * - switch_site: already takes site_id with the explicit semantics of
70
+ * flipping the global current site (different intent from per-call override)
71
+ * - redeem_token: pre-site setup; no site exists yet
72
+ */
73
+ const SITE_AGNOSTIC_TOOLS = new Set([
74
+ 'wordpress_list_sites',
75
+ 'wordpress_get_active_site',
76
+ 'wordpress_switch_site',
77
+ 'wordpress_redeem_token',
78
+ 'respira_list_sites',
79
+ 'respira_get_active_site',
80
+ 'respira_switch_site',
81
+ 'respira_redeem_token',
82
+ ]);
83
+ /**
84
+ * v6.12.0: Schema fragment auto-injected into every non-agnostic tool's
85
+ * inputSchema.properties. Lets callers override the active site on a single
86
+ * tool call without flipping the global current site.
87
+ *
88
+ * Solves the Cowork multi-chat contamination: when Cowork runs many chat
89
+ * sessions through one shared MCP server process, switch_site mutates the
90
+ * global current site for all sessions. Passing site_id per call sidesteps
91
+ * the shared state entirely. T.S. (studioscaler) reported the symptom
92
+ * 2026-05-16 across his multi-site workflow.
93
+ */
94
+ const SITE_ID_PROPERTY = {
95
+ type: 'string',
96
+ description: 'Optional. Override the active site for this single call without changing the global current site. Use this when running multiple Cowork chats against different WordPress sites in parallel so each chat can pin its own target site per tool call.',
97
+ };
35
98
  export class RespiraWordPressServer {
36
99
  server;
37
100
  currentSite = null;
@@ -43,7 +106,7 @@ export class RespiraWordPressServer {
43
106
  allowedSites = null;
44
107
  /** Whether the plugin version warning has already been shown this session. */
45
108
  versionWarningShown = false;
46
- static MCP_SERVER_VERSION = '6.11.4';
109
+ static MCP_SERVER_VERSION = MCP_SERVER_VERSION;
47
110
  /**
48
111
  * Normalize a tool name: respira_* → wordpress_* for switch dispatch.
49
112
  * Tracks whether the deprecated wordpress_* name was used.
@@ -333,6 +396,35 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
333
396
  }
334
397
  return this.getSiteSummary(this.currentSite);
335
398
  }
399
+ /**
400
+ * Resolve which WordPressClient should service this tool call.
401
+ *
402
+ * Order:
403
+ * 1) explicit args.site_id (per-call override; introduced in v6.12.0)
404
+ * 2) the global current site (`this.currentSite`)
405
+ *
406
+ * Throws if neither is set, or if the supplied site_id is unknown / not
407
+ * allowed for this MCP configuration group.
408
+ *
409
+ * @since 6.12.0
410
+ */
411
+ resolveClient(args) {
412
+ if (args && typeof args.site_id === 'string' && args.site_id.length > 0) {
413
+ const explicit = this.sites.get(args.site_id);
414
+ if (!explicit) {
415
+ const available = Array.from(this.sites.keys()).join(', ') || '(none configured)';
416
+ throw new Error(`Site with ID "${args.site_id}" not found in this MCP configuration. Available: ${available}`);
417
+ }
418
+ if (!this.isSiteAllowed(explicit)) {
419
+ throw new Error(`Site "${args.site_id}" is not in this MCP configuration group.`);
420
+ }
421
+ return explicit;
422
+ }
423
+ if (!this.currentSite) {
424
+ throw new Error('No active site. Either pass site_id on this tool call or run respira_switch_site / respira_redeem_token first.');
425
+ }
426
+ return this.currentSite;
427
+ }
336
428
  /** Check if a site is visible given RESPIRA_SITES filtering. */
337
429
  isSiteAllowed(site) {
338
430
  if (!this.allowedSites)
@@ -1137,20 +1229,20 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1137
1229
  },
1138
1230
  {
1139
1231
  name: 'wordpress_extract_builder_content',
1140
- description: 'Extract the full structured content from a page as builder-native JSON. Use this to see the complete page layout with all sections, columns, and widgets. For targeted searches, prefer find_element (faster). For a lightweight overview, prefer find_builder_targets. Returns the raw builder data structure that can be modified and passed back to inject_builder_content.',
1232
+ description: 'Extract the full structured content from a page as builder-native JSON. Use this to see the complete page layout with all sections, columns, and widgets. For targeted searches, prefer find_element (faster). For a lightweight overview, prefer find_builder_targets. Returns the raw builder data structure that can be modified and passed back to inject_builder_content. Works on any post type (pages, posts, custom post types). The `builder` arg is optional — if omitted, the active site builder is auto-detected via get_builder_info.',
1141
1233
  inputSchema: {
1142
1234
  type: 'object',
1143
1235
  properties: {
1144
1236
  builder: {
1145
1237
  type: 'string',
1146
- description: 'Builder name. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery. Use get_builder_info to discover the active builder first.',
1238
+ description: 'Optional. Builder name. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery. Omit to auto-detect the active site builder.',
1147
1239
  },
1148
1240
  page_id: {
1149
1241
  type: 'number',
1150
- description: 'Page ID',
1242
+ description: 'Page or post ID (works with any post type, including custom post types)',
1151
1243
  },
1152
1244
  },
1153
- required: ['builder', 'page_id'],
1245
+ required: ['page_id'],
1154
1246
  },
1155
1247
  readOnlyHint: true,
1156
1248
  },
@@ -1183,7 +1275,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1183
1275
  },
1184
1276
  {
1185
1277
  name: 'wordpress_inject_builder_content',
1186
- description: 'REPLACE (or append to) the entire page builder layout. WARNING: By default this REPLACES all existing content — use mode:"append" to add content without destroying existing elements. For editing a single module, use wordpress_update_module instead. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery. For Divi, divi_version is required ("4" or "5").',
1278
+ description: 'REPLACE (or append to) the entire page builder layout. WARNING: By default this REPLACES all existing content — use mode:"append" to add content without destroying existing elements. For editing a single module, use wordpress_update_module instead. Use exactly: gutenberg, divi, elementor, bricks, beaver, oxygen, breakdance, brizy, thrive, visual-composer, wpbakery. For Divi, divi_version is required ("4" or "5"). Starting in plugin v7.0.16, calling this against a page that already has content with mode="replace" (the default) without also passing confirm_replace=true returns a 409 respira_replace_confirmation_required — pass mode="append" to add to existing content, pass mode="replace" AND confirm_replace=true to overwrite, or pass edit_target="live" to overwrite the live page directly (plugin v7.0.22+ accepts an authorized live edit as the confirmation). The gate prevents silent data loss.',
1187
1279
  inputSchema: {
1188
1280
  type: 'object',
1189
1281
  properties: {
@@ -1201,9 +1293,13 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1201
1293
  },
1202
1294
  mode: {
1203
1295
  type: 'string',
1204
- description: 'replace (default): OVERWRITES all existing page content. append: adds new content after existing elements, preserving the current layout.',
1296
+ 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.',
1205
1297
  enum: ['replace', 'append'],
1206
1298
  },
1299
+ confirm_replace: {
1300
+ type: 'boolean',
1301
+ description: 'Required when mode="replace" (or unset, since replace is the default) AND the page already has content. Pass true only when you genuinely want to overwrite the existing structure. Without it, the plugin returns 409 respira_replace_confirmation_required so the agent can decide between mode="append" (preserve) and mode="replace"+confirm_replace=true (overwrite).',
1302
+ },
1207
1303
  edit_target: {
1208
1304
  type: 'string',
1209
1305
  description: 'When editing an original, choose ask, live, or duplicate. Defaults to ask when direct editing is enabled.',
@@ -2821,6 +2917,18 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
2821
2917
  if (await this.isAcfAvailable()) {
2822
2918
  tools.push(...getAcfTools());
2823
2919
  }
2920
+ // v6.12.0: inject optional site_id into every non-agnostic tool schema.
2921
+ // Agnostic tools (list_sites, get_active_site, switch_site, redeem_token)
2922
+ // skip the injection because they operate on global state.
2923
+ for (const t of tools) {
2924
+ if (SITE_AGNOSTIC_TOOLS.has(t.name))
2925
+ continue;
2926
+ if (!t.inputSchema || t.inputSchema.type !== 'object')
2927
+ continue;
2928
+ const props = (t.inputSchema.properties ||= {});
2929
+ if (!props.site_id)
2930
+ props.site_id = SITE_ID_PROPERTY;
2931
+ }
2824
2932
  // Generate respira_* aliases for all wordpress_* tools.
2825
2933
  const allTools = this.generateDualTools(tools);
2826
2934
  // Context-aware tool filtering: expose only relevant tools based on
@@ -3288,9 +3396,11 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3288
3396
  return args;
3289
3397
  }
3290
3398
  async handleToolCall(name, args) {
3291
- if (!this.currentSite) {
3292
- throw new Error('No WordPress site configured');
3293
- }
3399
+ // v6.12.0: relaxed top-of-handler guard. dispatchToolCall / resolveClient
3400
+ // now decides whether a site is needed for THIS call (some tools are
3401
+ // site-agnostic, others accept a per-call site_id override). The old
3402
+ // eager `if (!this.currentSite) throw` blocked the new "call with just
3403
+ // site_id on a server with no default site" path.
3294
3404
  // Normalize respira_* ↔ wordpress_* names.
3295
3405
  const { canonical, deprecated } = this.normalizeToolName(name);
3296
3406
  // mcp-v6.11.1: snake_case sweep across tool catalog. Schemas advertise
@@ -3308,14 +3418,15 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3308
3418
  return result;
3309
3419
  }
3310
3420
  async dispatchToolCall(name, args) {
3311
- if (!this.currentSite) {
3312
- throw new Error('No WordPress site configured');
3313
- }
3421
+ // v6.12.0: per-call site resolution. Agnostic tools work without a
3422
+ // resolved client; everything else requires either args.site_id or a
3423
+ // global currentSite. resolveClient throws cleanly if neither is set.
3424
+ const client = SITE_AGNOSTIC_TOOLS.has(name) ? this.currentSite : this.resolveClient(args);
3314
3425
  switch (name) {
3315
3426
  case 'wordpress_get_site_context':
3316
3427
  return args.detail === 'full'
3317
- ? await this.currentSite.getSiteContext()
3318
- : await this.currentSite.getCompactSiteContext();
3428
+ ? await client.getSiteContext()
3429
+ : await client.getCompactSiteContext();
3319
3430
  case 'wordpress_list_sites': {
3320
3431
  const allSites = Array.from(this.sites.values());
3321
3432
  const visibleSites = allSites.filter((site) => this.isSiteAllowed(site));
@@ -3329,21 +3440,21 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3329
3440
  site: this.getActiveSiteSummary(),
3330
3441
  };
3331
3442
  case 'wordpress_get_theme_docs':
3332
- return await this.currentSite.getThemeDocs();
3443
+ return await client.getThemeDocs();
3333
3444
  case 'wordpress_get_builder_info':
3334
- return await this.currentSite.getBuilderInfo({ debug: Boolean(args?.debug) });
3445
+ return await client.getBuilderInfo({ debug: Boolean(args?.debug) });
3335
3446
  case 'wordpress_list_pages':
3336
- return await this.currentSite.listPages(args);
3447
+ return await client.listPages(args);
3337
3448
  case 'wordpress_read_page':
3338
- return await this.currentSite.getPage(args.id, args.include);
3449
+ return await client.getPage(args.id, args.include);
3339
3450
  case 'wordpress_create_page_duplicate':
3340
3451
  return {
3341
- ...(await this.currentSite.duplicatePage(args.original_id, args.suffix, args.include)),
3342
- respira_approvals_url: this.currentSite.getApprovalsUrl(),
3452
+ ...(await client.duplicatePage(args.original_id, args.suffix, args.include)),
3453
+ respira_approvals_url: client.getApprovalsUrl(),
3343
3454
  };
3344
3455
  case 'wordpress_update_page': {
3345
- const approvalsUrl = this.currentSite.getApprovalsUrl();
3346
- const page = await this.currentSite.updatePage(args.id, args);
3456
+ const approvalsUrl = client.getApprovalsUrl();
3457
+ const page = await client.updatePage(args.id, args);
3347
3458
  // Check if Respira created a duplicate
3348
3459
  if (page.__respira_duplicate_info) {
3349
3460
  const info = page.__respira_duplicate_info;
@@ -3364,19 +3475,19 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3364
3475
  };
3365
3476
  }
3366
3477
  case 'wordpress_delete_page':
3367
- return await this.currentSite.deletePage(args.id, args.force);
3478
+ return await client.deletePage(args.id, args.force);
3368
3479
  case 'wordpress_list_posts':
3369
- return await this.currentSite.listPosts(args);
3480
+ return await client.listPosts(args);
3370
3481
  case 'wordpress_read_post':
3371
- return await this.currentSite.getPost(args.id, args.include);
3482
+ return await client.getPost(args.id, args.include);
3372
3483
  case 'wordpress_create_post_duplicate':
3373
3484
  return {
3374
- ...(await this.currentSite.duplicatePost(args.original_id, args.suffix, args.include)),
3375
- respira_approvals_url: this.currentSite.getApprovalsUrl(),
3485
+ ...(await client.duplicatePost(args.original_id, args.suffix, args.include)),
3486
+ respira_approvals_url: client.getApprovalsUrl(),
3376
3487
  };
3377
3488
  case 'wordpress_update_post': {
3378
- const approvalsUrl = this.currentSite.getApprovalsUrl();
3379
- const post = await this.currentSite.updatePost(args.id, args);
3489
+ const approvalsUrl = client.getApprovalsUrl();
3490
+ const post = await client.updatePost(args.id, args);
3380
3491
  // Check if Respira created a duplicate
3381
3492
  if (post.__respira_duplicate_info) {
3382
3493
  const info = post.__respira_duplicate_info;
@@ -3397,27 +3508,27 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3397
3508
  };
3398
3509
  }
3399
3510
  case 'wordpress_delete_post':
3400
- return await this.currentSite.deletePost(args.id, args.force);
3511
+ return await client.deletePost(args.id, args.force);
3401
3512
  case 'wordpress_list_media':
3402
- return await this.currentSite.listMedia(args);
3513
+ return await client.listMedia(args);
3403
3514
  case 'wordpress_upload_media':
3404
- return await this.currentSite.uploadMedia(args.file, args.filename, args.mime_type, args.title, args.alt, args.caption);
3515
+ return await client.uploadMedia(args.file, args.filename, args.mime_type, args.title, args.alt, args.caption);
3405
3516
  case 'wordpress_extract_builder_content':
3406
- return await this.currentSite.extractBuilderContent(args.builder, args.page_id);
3517
+ return await client.extractBuilderContent(args.builder, args.page_id);
3407
3518
  case 'wordpress_find_builder_targets':
3408
- return await this.currentSite.findBuilderTargets(args.builder, args.page_id, args.query, args.limit);
3519
+ return await client.findBuilderTargets(args.builder, args.page_id, args.query, args.limit);
3409
3520
  case 'wordpress_inject_builder_content':
3410
3521
  return {
3411
- ...(await this.currentSite.injectBuilderContent(args.builder, args.page_id, args.content, args.divi_version, args.edit_target, args.mode)),
3412
- respira_approvals_url: this.currentSite.getApprovalsUrl(),
3522
+ ...(await client.injectBuilderContent(args.builder, args.page_id, args.content, args.divi_version, args.edit_target, args.mode, args.confirm_replace)),
3523
+ respira_approvals_url: client.getApprovalsUrl(),
3413
3524
  };
3414
3525
  case 'wordpress_update_module':
3415
3526
  return {
3416
- ...(await this.currentSite.updateModule(args.builder, args.page_id, args.module_identifier, args.updates, args.edit_target)),
3417
- respira_approvals_url: this.currentSite.getApprovalsUrl(),
3527
+ ...(await client.updateModule(args.builder, args.page_id, args.module_identifier, args.updates, args.edit_target)),
3528
+ respira_approvals_url: client.getApprovalsUrl(),
3418
3529
  };
3419
3530
  case 'wordpress_validate_security':
3420
- return await this.currentSite.validateSecurity(args.content);
3531
+ return await client.validateSecurity(args.content);
3421
3532
  case 'wordpress_switch_site': {
3422
3533
  const newSite = this.sites.get(args.site_id);
3423
3534
  if (!newSite) {
@@ -3435,7 +3546,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3435
3546
  };
3436
3547
  }
3437
3548
  case 'wordpress_diagnose_connection':
3438
- return await this.currentSite.diagnoseConnection({ post_id: args.post_id });
3549
+ return await client.diagnoseConnection({ post_id: args.post_id });
3439
3550
  // Canonical name (normalizeToolName rewrites respira_* → wordpress_*
3440
3551
  // before this switch runs). The early-route guard in setRequestHandler
3441
3552
  // catches the redeem call before we ever land here when no site is
@@ -3445,245 +3556,245 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3445
3556
  return await this.redeemInstallToken(String(args.token || ''));
3446
3557
  // Page Speed Analysis
3447
3558
  case 'wordpress_analyze_performance':
3448
- return await this.currentSite.analyzePerformance(args.page_id);
3559
+ return await client.analyzePerformance(args.page_id);
3449
3560
  case 'wordpress_get_core_web_vitals':
3450
- return await this.currentSite.getCoreWebVitals(args.page_id);
3561
+ return await client.getCoreWebVitals(args.page_id);
3451
3562
  case 'wordpress_analyze_images':
3452
- return await this.currentSite.analyzeImages(args.page_id);
3563
+ return await client.analyzeImages(args.page_id);
3453
3564
  // SEO Analysis
3454
3565
  case 'wordpress_analyze_seo':
3455
- return await this.currentSite.analyzeSEO(args.page_id);
3566
+ return await client.analyzeSEO(args.page_id);
3456
3567
  case 'wordpress_check_seo_issues':
3457
- return await this.currentSite.checkSEOIssues(args.page_id);
3568
+ return await client.checkSEOIssues(args.page_id);
3458
3569
  case 'wordpress_analyze_readability':
3459
- return await this.currentSite.analyzeReadability(args.page_id);
3570
+ return await client.analyzeReadability(args.page_id);
3460
3571
  case 'wordpress_analyze_rankmath':
3461
- return await this.currentSite.analyzeRankMath(args.post_id);
3572
+ return await client.analyzeRankMath(args.post_id);
3462
3573
  // AEO Analysis
3463
3574
  case 'wordpress_analyze_aeo':
3464
- return await this.currentSite.analyzeAEO(args.page_id);
3575
+ return await client.analyzeAEO(args.page_id);
3465
3576
  case 'wordpress_check_structured_data':
3466
- return await this.currentSite.checkStructuredData(args.page_id);
3577
+ return await client.checkStructuredData(args.page_id);
3467
3578
  // Accessibility
3468
3579
  case 'wordpress_list_accessibility_scans':
3469
- return await this.currentSite.listAccessibilityScans();
3580
+ return await client.listAccessibilityScans();
3470
3581
  case 'wordpress_get_accessibility_scan':
3471
- return await this.currentSite.getAccessibilityScan(args.scan_id);
3582
+ return await client.getAccessibilityScan(args.scan_id);
3472
3583
  case 'wordpress_scan_page_accessibility':
3473
- return await this.currentSite.scanPageAccessibility(args.page_id, args.standard);
3584
+ return await client.scanPageAccessibility(args.page_id, args.standard);
3474
3585
  case 'wordpress_apply_accessibility_fixes':
3475
- return await this.currentSite.applyAccessibilityFixes(args.scan_id, args.rule_ids);
3586
+ return await client.applyAccessibilityFixes(args.scan_id, args.rule_ids);
3476
3587
  // Plugin Management (EXPERIMENTAL)
3477
3588
  case 'wordpress_list_plugins':
3478
- return await this.currentSite.listPlugins();
3589
+ return await client.listPlugins();
3479
3590
  case 'wordpress_install_plugin':
3480
- return await this.currentSite.installPlugin(args.slug_or_url, args.source || 'wordpress.org', args.approval_token);
3591
+ return await client.installPlugin(args.slug_or_url, args.source || 'wordpress.org', args.approval_token);
3481
3592
  case 'wordpress_activate_plugin':
3482
- return await this.currentSite.activatePlugin(args.slug, args.approval_token);
3593
+ return await client.activatePlugin(args.slug, args.approval_token);
3483
3594
  case 'wordpress_deactivate_plugin':
3484
- return await this.currentSite.deactivatePlugin(args.slug, args.approval_token);
3595
+ return await client.deactivatePlugin(args.slug, args.approval_token);
3485
3596
  case 'wordpress_update_plugin':
3486
- return await this.currentSite.updatePlugin(args.slug, args.approval_token);
3597
+ return await client.updatePlugin(args.slug, args.approval_token);
3487
3598
  case 'wordpress_delete_plugin':
3488
- return await this.currentSite.deletePlugin(args.slug, args.approval_token);
3599
+ return await client.deletePlugin(args.slug, args.approval_token);
3489
3600
  // Users Management
3490
3601
  case 'wordpress_list_users':
3491
- return await this.currentSite.listUsers(args);
3602
+ return await client.listUsers(args);
3492
3603
  case 'wordpress_get_user':
3493
- return await this.currentSite.getUser(args.id);
3604
+ return await client.getUser(args.id);
3494
3605
  case 'wordpress_create_user':
3495
- return await this.currentSite.createUser(args);
3606
+ return await client.createUser(args);
3496
3607
  case 'wordpress_update_user':
3497
- return await this.currentSite.updateUser(args.id, args);
3608
+ return await client.updateUser(args.id, args);
3498
3609
  case 'wordpress_delete_user':
3499
- return await this.currentSite.deleteUser(args.id, args.reassign);
3610
+ return await client.deleteUser(args.id, args.reassign);
3500
3611
  // Comments
3501
3612
  case 'wordpress_list_comments':
3502
- return await this.currentSite.listComments(args);
3613
+ return await client.listComments(args);
3503
3614
  case 'wordpress_get_comment':
3504
- return await this.currentSite.getComment(args.id);
3615
+ return await client.getComment(args.id);
3505
3616
  case 'wordpress_create_comment':
3506
- return await this.currentSite.createComment(args);
3617
+ return await client.createComment(args);
3507
3618
  case 'wordpress_update_comment':
3508
- return await this.currentSite.updateComment(args.id, args);
3619
+ return await client.updateComment(args.id, args);
3509
3620
  case 'wordpress_delete_comment':
3510
- return await this.currentSite.deleteComment(args.id);
3621
+ return await client.deleteComment(args.id);
3511
3622
  // Taxonomies
3512
3623
  case 'wordpress_list_taxonomies':
3513
- return await this.currentSite.listTaxonomies();
3624
+ return await client.listTaxonomies();
3514
3625
  case 'wordpress_get_taxonomy':
3515
- return await this.currentSite.getTaxonomy(args.taxonomy);
3626
+ return await client.getTaxonomy(args.taxonomy);
3516
3627
  case 'wordpress_list_terms':
3517
- return await this.currentSite.listTerms(args.taxonomy, args);
3628
+ return await client.listTerms(args.taxonomy, args);
3518
3629
  case 'wordpress_get_term':
3519
- return await this.currentSite.getTerm(args.taxonomy, args.id);
3630
+ return await client.getTerm(args.taxonomy, args.id);
3520
3631
  case 'wordpress_create_term':
3521
- return await this.currentSite.createTerm(args.taxonomy, args);
3632
+ return await client.createTerm(args.taxonomy, args);
3522
3633
  case 'wordpress_update_term':
3523
- return await this.currentSite.updateTerm(args.taxonomy, args.id, args);
3634
+ return await client.updateTerm(args.taxonomy, args.id, args);
3524
3635
  case 'wordpress_delete_term':
3525
- return await this.currentSite.deleteTerm(args.taxonomy, args.id);
3636
+ return await client.deleteTerm(args.taxonomy, args.id);
3526
3637
  // Custom Post Types
3527
3638
  case 'wordpress_list_post_types':
3528
- return await this.currentSite.listPostTypes();
3639
+ return await client.listPostTypes();
3529
3640
  case 'wordpress_get_post_type':
3530
- return await this.currentSite.getPostType(args.type);
3641
+ return await client.getPostType(args.type);
3531
3642
  case 'wordpress_list_custom_posts':
3532
- return await this.currentSite.listCustomPosts(args.type, args);
3643
+ return await client.listCustomPosts(args.type, args);
3533
3644
  case 'wordpress_get_custom_post':
3534
- return await this.currentSite.getCustomPost(args.type, args.id, args.include);
3645
+ return await client.getCustomPost(args.type, args.id, args.include);
3535
3646
  case 'wordpress_create_custom_post':
3536
- return await this.currentSite.createCustomPost(args.type, args);
3647
+ return await client.createCustomPost(args.type, args);
3537
3648
  case 'wordpress_update_custom_post':
3538
- return await this.currentSite.updateCustomPost(args.type, args.id, args);
3649
+ return await client.updateCustomPost(args.type, args.id, args);
3539
3650
  case 'wordpress_delete_custom_post':
3540
- return await this.currentSite.deleteCustomPost(args.type, args.id);
3651
+ return await client.deleteCustomPost(args.type, args.id);
3541
3652
  // Options
3542
3653
  case 'wordpress_list_options':
3543
- return await this.currentSite.listOptions(args.search);
3654
+ return await client.listOptions(args.search);
3544
3655
  case 'wordpress_get_option':
3545
- return await this.currentSite.getOption(args.option);
3656
+ return await client.getOption(args.option);
3546
3657
  case 'wordpress_update_option':
3547
- return await this.currentSite.updateOption(args.option, args.value);
3658
+ return await client.updateOption(args.option, args.value);
3548
3659
  case 'wordpress_delete_option':
3549
- return await this.currentSite.deleteOption(args.option);
3660
+ return await client.deleteOption(args.option);
3550
3661
  // Media enhancements
3551
3662
  case 'wordpress_get_media':
3552
- return await this.currentSite.getMedia(args.id);
3663
+ return await client.getMedia(args.id);
3553
3664
  case 'wordpress_update_media':
3554
- return await this.currentSite.updateMedia(args.id, args);
3665
+ return await client.updateMedia(args.id, args);
3555
3666
  case 'wordpress_update_media_batch':
3556
- return await this.currentSite.updateMediaBatch(args.items);
3667
+ return await client.updateMediaBatch(args.items);
3557
3668
  case 'wordpress_delete_media':
3558
- return await this.currentSite.deleteMedia(args.id);
3669
+ return await client.deleteMedia(args.id);
3559
3670
  // Menu Management
3560
3671
  case 'wordpress_list_menus':
3561
- return await this.currentSite.listMenus();
3672
+ return await client.listMenus();
3562
3673
  case 'wordpress_get_menu':
3563
- return await this.currentSite.getMenu(args.id);
3674
+ return await client.getMenu(args.id);
3564
3675
  case 'wordpress_create_menu':
3565
- return await this.currentSite.createMenu(args);
3676
+ return await client.createMenu(args);
3566
3677
  case 'wordpress_update_menu':
3567
- return await this.currentSite.updateMenu(args.id, args);
3678
+ return await client.updateMenu(args.id, args);
3568
3679
  case 'wordpress_delete_menu':
3569
- return await this.currentSite.deleteMenu(args.id);
3680
+ return await client.deleteMenu(args.id);
3570
3681
  // Menu Locations
3571
3682
  case 'wordpress_list_menu_locations':
3572
- return await this.currentSite.listMenuLocations();
3683
+ return await client.listMenuLocations();
3573
3684
  case 'wordpress_assign_menu_location':
3574
- return await this.currentSite.assignMenuToLocation(args.location, args.menu_id);
3685
+ return await client.assignMenuToLocation(args.location, args.menu_id);
3575
3686
  // Menu Items
3576
3687
  case 'wordpress_list_menu_items':
3577
- return await this.currentSite.listMenuItems(args.menu_id);
3688
+ return await client.listMenuItems(args.menu_id);
3578
3689
  case 'wordpress_create_menu_item':
3579
- return await this.currentSite.createMenuItem(args.menu_id, args);
3690
+ return await client.createMenuItem(args.menu_id, args);
3580
3691
  case 'wordpress_get_menu_item':
3581
- return await this.currentSite.getMenuItem(args.item_id);
3692
+ return await client.getMenuItem(args.item_id);
3582
3693
  case 'wordpress_update_menu_item':
3583
- return await this.currentSite.updateMenuItem(args.item_id, args);
3694
+ return await client.updateMenuItem(args.item_id, args);
3584
3695
  case 'wordpress_delete_menu_item':
3585
- return await this.currentSite.deleteMenuItem(args.item_id);
3696
+ return await client.deleteMenuItem(args.item_id);
3586
3697
  case 'wordpress_list_snapshots':
3587
- return await this.currentSite.listSnapshots(args);
3698
+ return await client.listSnapshots(args);
3588
3699
  case 'wordpress_get_snapshot':
3589
- return await this.currentSite.getSnapshot(args.snapshot_uuid);
3700
+ return await client.getSnapshot(args.snapshot_uuid);
3590
3701
  case 'wordpress_diff_snapshots':
3591
- return await this.currentSite.diffSnapshots(args.snapshot_uuid_a, args.snapshot_uuid_b);
3702
+ return await client.diffSnapshots(args.snapshot_uuid_a, args.snapshot_uuid_b);
3592
3703
  case 'wordpress_restore_snapshot':
3593
- return await this.currentSite.restoreSnapshot(args.snapshot_uuid);
3704
+ return await client.restoreSnapshot(args.snapshot_uuid);
3594
3705
  case 'wordpress_apply_builder_patch':
3595
- return await this.currentSite.applyBuilderPatch(args.builder, args.post_id, args.operations, args.include, args.edit_target);
3706
+ return await client.applyBuilderPatch(args.builder, args.post_id, args.operations, args.include, args.edit_target);
3596
3707
  case 'woocommerce_list_products':
3597
- return await this.currentSite.woocommerceListProducts(args);
3708
+ return await client.woocommerceListProducts(args);
3598
3709
  case 'woocommerce_get_product':
3599
- return await this.currentSite.woocommerceGetProduct(args.id);
3710
+ return await client.woocommerceGetProduct(args.id);
3600
3711
  case 'woocommerce_create_product':
3601
- return await this.currentSite.woocommerceCreateProduct(args);
3712
+ return await client.woocommerceCreateProduct(args);
3602
3713
  case 'woocommerce_update_product': {
3603
3714
  const { id, ...payload } = args;
3604
- return await this.currentSite.woocommerceUpdateProduct(id, payload);
3715
+ return await client.woocommerceUpdateProduct(id, payload);
3605
3716
  }
3606
3717
  case 'woocommerce_duplicate_product':
3607
- return await this.currentSite.woocommerceDuplicateProduct(args.id);
3718
+ return await client.woocommerceDuplicateProduct(args.id);
3608
3719
  case 'woocommerce_list_categories':
3609
- return await this.currentSite.woocommerceListCategories(args);
3720
+ return await client.woocommerceListCategories(args);
3610
3721
  case 'woocommerce_get_category':
3611
- return await this.currentSite.woocommerceGetCategory(args.id);
3722
+ return await client.woocommerceGetCategory(args.id);
3612
3723
  case 'woocommerce_create_category':
3613
- return await this.currentSite.woocommerceCreateCategory(args);
3724
+ return await client.woocommerceCreateCategory(args);
3614
3725
  case 'woocommerce_update_category': {
3615
3726
  const { id, ...payload } = args;
3616
- return await this.currentSite.woocommerceUpdateCategory(id, payload);
3727
+ return await client.woocommerceUpdateCategory(id, payload);
3617
3728
  }
3618
3729
  case 'woocommerce_delete_category':
3619
- return await this.currentSite.woocommerceDeleteCategory(args.id);
3730
+ return await client.woocommerceDeleteCategory(args.id);
3620
3731
  case 'woocommerce_list_tags':
3621
- return await this.currentSite.woocommerceListTags(args);
3732
+ return await client.woocommerceListTags(args);
3622
3733
  case 'woocommerce_get_tag':
3623
- return await this.currentSite.woocommerceGetTag(args.id);
3734
+ return await client.woocommerceGetTag(args.id);
3624
3735
  case 'woocommerce_create_tag':
3625
- return await this.currentSite.woocommerceCreateTag(args);
3736
+ return await client.woocommerceCreateTag(args);
3626
3737
  case 'woocommerce_update_tag': {
3627
3738
  const { id, ...payload } = args;
3628
- return await this.currentSite.woocommerceUpdateTag(id, payload);
3739
+ return await client.woocommerceUpdateTag(id, payload);
3629
3740
  }
3630
3741
  case 'woocommerce_delete_tag':
3631
- return await this.currentSite.woocommerceDeleteTag(args.id);
3742
+ return await client.woocommerceDeleteTag(args.id);
3632
3743
  case 'woocommerce_list_orders':
3633
- return await this.currentSite.woocommerceListOrders(args);
3744
+ return await client.woocommerceListOrders(args);
3634
3745
  case 'woocommerce_get_order':
3635
- return await this.currentSite.woocommerceGetOrder(args.id);
3746
+ return await client.woocommerceGetOrder(args.id);
3636
3747
  case 'woocommerce_update_order_status':
3637
- return await this.currentSite.woocommerceUpdateOrderStatus(args.id, args.status);
3748
+ return await client.woocommerceUpdateOrderStatus(args.id, args.status);
3638
3749
  case 'woocommerce_get_stock_status':
3639
- return await this.currentSite.woocommerceGetStockStatus();
3750
+ return await client.woocommerceGetStockStatus();
3640
3751
  case 'woocommerce_update_stock': {
3641
3752
  const { id, ...payload } = args;
3642
- return await this.currentSite.woocommerceUpdateStock(id, payload);
3753
+ return await client.woocommerceUpdateStock(id, payload);
3643
3754
  }
3644
3755
  case 'woocommerce_sales_report':
3645
- return await this.currentSite.woocommerceSalesReport(args);
3756
+ return await client.woocommerceSalesReport(args);
3646
3757
  // --- v5.2.0 Elemental tools ---
3647
3758
  case 'wordpress_find_element': {
3648
3759
  const { post_id, ...rest } = args;
3649
- return await this.currentSite.callRestV2('POST', `/builder/elements/find/${post_id}`, rest);
3760
+ return await client.callRestV2('POST', `/builder/elements/find/${post_id}`, rest);
3650
3761
  }
3651
3762
  case 'wordpress_update_element': {
3652
3763
  const { post_id, ...rest } = args;
3653
- return await this.currentSite.callRestV2('POST', `/builder/elements/update/${post_id}`, rest);
3764
+ return await client.callRestV2('POST', `/builder/elements/update/${post_id}`, rest);
3654
3765
  }
3655
3766
  case 'wordpress_move_element': {
3656
3767
  const { post_id, ...rest } = args;
3657
- return await this.currentSite.callRestV2('POST', `/builder/elements/move/${post_id}`, rest);
3768
+ return await client.callRestV2('POST', `/builder/elements/move/${post_id}`, rest);
3658
3769
  }
3659
3770
  case 'wordpress_duplicate_element': {
3660
3771
  const { post_id, ...rest } = args;
3661
- return await this.currentSite.callRestV2('POST', `/builder/elements/duplicate/${post_id}`, rest);
3772
+ return await client.callRestV2('POST', `/builder/elements/duplicate/${post_id}`, rest);
3662
3773
  }
3663
3774
  case 'wordpress_remove_element': {
3664
3775
  const { post_id, ...rest } = args;
3665
- return await this.currentSite.callRestV2('POST', `/builder/elements/remove/${post_id}`, rest);
3776
+ return await client.callRestV2('POST', `/builder/elements/remove/${post_id}`, rest);
3666
3777
  }
3667
3778
  case 'wordpress_batch_update': {
3668
3779
  const { post_id, ...rest } = args;
3669
- return await this.currentSite.callRestV2('POST', `/builder/elements/batch/${post_id}`, rest);
3780
+ return await client.callRestV2('POST', `/builder/elements/batch/${post_id}`, rest);
3670
3781
  }
3671
3782
  case 'wordpress_reorder_elements': {
3672
3783
  const { post_id, ...rest } = args;
3673
- return await this.currentSite.callRestV2('POST', `/builder/elements/reorder/${post_id}`, rest);
3784
+ return await client.callRestV2('POST', `/builder/elements/reorder/${post_id}`, rest);
3674
3785
  }
3675
3786
  case 'wordpress_build_page':
3676
- return await this.currentSite.callRestV2('POST', '/builder/build', args);
3787
+ return await client.callRestV2('POST', '/builder/build', args);
3677
3788
  case 'wordpress_convert_html_to_builder':
3678
- return await this.currentSite.callRestV2('POST', '/builder/convert', args);
3789
+ return await client.callRestV2('POST', '/builder/convert', args);
3679
3790
  case 'wordpress_bulk_pages_operation':
3680
- return await this.currentSite.callRestV2('POST', '/builder/bulk', args);
3791
+ return await client.callRestV2('POST', '/builder/bulk', args);
3681
3792
  case 'wordpress_search_stock_images':
3682
- return await this.currentSite.callRestV2('GET', '/stock-images/search', args);
3793
+ return await client.callRestV2('GET', '/stock-images/search', args);
3683
3794
  case 'wordpress_sideload_image':
3684
- return await this.currentSite.callRestV2('POST', '/stock-images/sideload', args);
3795
+ return await client.callRestV2('POST', '/stock-images/sideload', args);
3685
3796
  case 'wordpress_get_server_compatibility':
3686
- return await this.currentSite.callRestV2('GET', '/server/compatibility');
3797
+ return await client.callRestV2('GET', '/server/compatibility');
3687
3798
  // Widget shortcuts and Bricks tools — dynamic dispatch.
3688
3799
  default: {
3689
3800
  // Check if this is a Bricks deep tool.
@@ -3715,7 +3826,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3715
3826
  throw new Error(`Widget shortcut "${shortcutName}" requires a post_id parameter.`);
3716
3827
  }
3717
3828
  const { post_id: _pid, ...settings } = args;
3718
- return await this.currentSite.callRestV2('POST', `/builder/widget/${shortcutName}/${postId}`, settings);
3829
+ return await client.callRestV2('POST', `/builder/widget/${shortcutName}/${postId}`, settings);
3719
3830
  }
3720
3831
  // Return isError result instead of throwing — unknown tool is an execution
3721
3832
  // error, not a protocol error. This lets LLMs self-correct gracefully.
@@ -3757,7 +3868,7 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3757
3868
  },
3758
3869
  {
3759
3870
  name: 'wordpress_update_element',
3760
- description: 'Update settings or content on a specific element in a page. This is the PRIMARY tool for making content changes — use it for 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.',
3871
+ description: 'Update settings or content on a specific element in a page. This is the PRIMARY tool for making content changes — use it for 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. 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.',
3761
3872
  inputSchema: {
3762
3873
  type: 'object',
3763
3874
  properties: {
@@ -3771,6 +3882,11 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3771
3882
  type: 'object',
3772
3883
  description: 'Key-value pairs of settings to update on the matched element',
3773
3884
  },
3885
+ editTarget: {
3886
+ type: 'string',
3887
+ enum: ['live', 'duplicate'],
3888
+ description: 'Optional. "duplicate" (default) routes the write to the Respira duplicate of the post (or auto-creates one) so changes go through the Respira → Changes approval flow before they reach the public page. "live" writes straight to the published original — requires Respira → Settings → Allow direct edit, or the post to be a draft / existing Respira duplicate. The response always reports the resolved target so you know exactly which post received the write.',
3889
+ },
3774
3890
  },
3775
3891
  required: ['post_id', 'identifier_type', 'identifier_value', 'updates'],
3776
3892
  },