@respira/wordpress-mcp-server 6.17.1 → 6.18.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.
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAkBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAyGzE,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,KAAK,CAA2C;IACxD,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB,CAAS;IAEpC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAsB;IAEhE;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;gBA4Bb,WAAW,EAAE,mBAAmB,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE;IAwRvE,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,oBAAoB;IAQ5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IAmBrB,gEAAgE;IAChE,OAAO,CAAC,aAAa;IAUrB;;;;;;OAMG;YACW,UAAU;IA2CxB;;;;;;;;;;;;;;OAcG;YACW,WAAW;IAyIzB;;;;;;;;;OASG;YACW,kBAAkB;IA6FhC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YA4MP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IA6tEtB;;;;;;OAMG;IACH,yEAAyE;IACzE,OAAO,CAAC,mBAAmB,CAAoD;IAC/E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAU;YAEpC,oBAAoB;YAqDpB,2BAA2B;IAazC;;;;OAIG;YACW,cAAc;IAY5B,OAAO,CAAC,mBAAmB;IAwT3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAe3C;IAEF;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;YAmCpB,cAAc;YA+Dd,gBAAgB;IA8lB9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAoRzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA6UxB,GAAG;CAgBV"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAmBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAyGzE,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,KAAK,CAA2C;IACxD,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB,CAAS;IAEpC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAsB;IAEhE;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;gBA4Bb,WAAW,EAAE,mBAAmB,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE;IAuTvE,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,oBAAoB;IAQ5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IAmBrB,gEAAgE;IAChE,OAAO,CAAC,aAAa;IAUrB;;;;;;OAMG;YACW,UAAU;IA2CxB;;;;;;;;;;;;;;OAcG;YACW,WAAW;IAyIzB;;;;;;;;;OASG;YACW,kBAAkB;IA6FhC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YA4MP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IAmxEtB;;;;;;OAMG;IACH,yEAAyE;IACzE,OAAO,CAAC,mBAAmB,CAAoD;IAC/E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAU;YAEpC,oBAAoB;YAqDpB,2BAA2B;IAazC;;;;OAIG;YACW,cAAc;IAY5B,OAAO,CAAC,mBAAmB;IAwT3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAe3C;IAEF;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;YAmCpB,cAAc;YA+Dd,gBAAgB;IAqpB9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAoRzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA6UxB,GAAG;CAyCV"}
package/dist/server.js CHANGED
@@ -6,6 +6,7 @@
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 { execSync } from 'child_process';
9
10
  import { readFileSync } from 'fs';
10
11
  import { dirname, resolve } from 'path';
11
12
  import { fileURLToPath } from 'url';
@@ -402,6 +403,16 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
402
403
  - Direct edit mode: when enabled in settings, writes go straight to the original (skip duplicate workflow)
403
404
  - respira_validate_security — check content for XSS and security issues before saving
404
405
 
406
+ ## Theme files (CSS-family only, plugin 7.0.42+)
407
+
408
+ For agencies that source-control a shared child theme stylesheet (e.g. vds/css/custom.css) across many sites: write the file on disk, not the Customizer Additional CSS post.
409
+
410
+ - respira_read_theme_file — read a CSS / SCSS / LESS / JSON theme file
411
+ - respira_write_theme_file — replace (or create) a theme file with new contents
412
+ - respira_append_theme_file — append a block to an existing theme file
413
+
414
+ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of scope. 1 MiB cap per file. Requires the WP edit_themes capability on the user behind the API key.
415
+
405
416
  ## WooCommerce (when addon installed)
406
417
 
407
418
  21 tools for product management, categories, tags, orders, stock, and sales reports. All prefixed woocommerce_*.`,
@@ -412,6 +423,29 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
412
423
  const allowedSitesEnv = process.env.RESPIRA_SITES;
413
424
  if (allowedSitesEnv) {
414
425
  this.allowedSites = new Set(allowedSitesEnv.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean));
426
+ // v6.17.2: log the filter at startup so a "missing site" never silently
427
+ // confuses the customer again. Previously a stale RESPIRA_SITES env var
428
+ // (typically baked into Claude Desktop's claude_desktop_config.json by
429
+ // the connector deeplink at install time) would silently hide every
430
+ // configured site whose hostname wasn't on the allow-list, and the only
431
+ // way to discover this was to read the source. Now the dropped sites
432
+ // are listed in the bootstrap stderr so the customer sees them on
433
+ // every reconnect.
434
+ const hidden = siteConfigs
435
+ .map((s) => {
436
+ try {
437
+ return new URL(s.url).hostname.toLowerCase();
438
+ }
439
+ catch {
440
+ return null;
441
+ }
442
+ })
443
+ .filter((h) => h !== null && !this.allowedSites.has(h));
444
+ if (hidden.length > 0) {
445
+ console.error(`respira-mcp: RESPIRA_SITES filter active (allowed: ${[...this.allowedSites].join(', ')}). ` +
446
+ `Hidden site${hidden.length === 1 ? '' : 's'}: ${hidden.join(', ')}. ` +
447
+ `To show all, remove or update RESPIRA_SITES in claude_desktop_config.json.`);
448
+ }
415
449
  }
416
450
  // Initialize WordPress clients for each site
417
451
  siteConfigs.forEach((config) => {
@@ -1816,6 +1850,57 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
1816
1850
  required: ['content'],
1817
1851
  },
1818
1852
  },
1853
+ {
1854
+ name: 'wordpress_read_theme_file',
1855
+ description: 'Read the contents of a theme stylesheet on disk (CSS / SCSS / LESS / JSON only). Path is relative to wp-content/themes, e.g. `vds/css/custom.css`. Useful for diffing before a write and for handing the existing stylesheet to an LLM as context. Requires plugin v7.0.42+ and the user behind the API key must have the WP `edit_themes` capability. Returns content, content_md5, byte_size, mtime, theme_role (active_stylesheet | active_template | other), is_writable.',
1856
+ inputSchema: {
1857
+ type: 'object',
1858
+ properties: {
1859
+ relative_path: {
1860
+ type: 'string',
1861
+ description: 'Path relative to wp-content/themes/. Example: `vds/css/custom.css`. No leading slash, no `..` segments. Extension must be one of: css, scss, less, json.',
1862
+ },
1863
+ },
1864
+ required: ['relative_path'],
1865
+ },
1866
+ readOnlyHint: true,
1867
+ },
1868
+ {
1869
+ name: 'wordpress_write_theme_file',
1870
+ description: 'Replace (or create) a theme stylesheet on disk with the given content. CSS / SCSS / LESS / JSON only. Creates any missing parent directories under the theme. Path is relative to wp-content/themes, e.g. `vds/css/custom.css`. Use this when the agency source-controls a shared child theme stylesheet and the agent needs to push CSS edits to the same file their human devs commit, not to the Customizer Additional CSS post. Requires plugin v7.0.42+ and the user behind the API key must have the WP `edit_themes` capability. Hard cap: 1 MiB per file. Returns bytes_written, content_md5, theme_role, mtime.',
1871
+ inputSchema: {
1872
+ type: 'object',
1873
+ properties: {
1874
+ relative_path: {
1875
+ type: 'string',
1876
+ description: 'Path relative to wp-content/themes/. Example: `vds/css/custom.css`. No leading slash, no `..` segments. Extension must be one of: css, scss, less, json.',
1877
+ },
1878
+ content: {
1879
+ type: 'string',
1880
+ description: 'Full file contents. Up to 1 MiB. Existing file is fully replaced.',
1881
+ },
1882
+ },
1883
+ required: ['relative_path', 'content'],
1884
+ },
1885
+ },
1886
+ {
1887
+ name: 'wordpress_append_theme_file',
1888
+ description: 'Append content to a theme stylesheet on disk. CSS / SCSS / LESS / JSON only. Adds a leading newline if the existing file did not end in one so two appended blocks do not fuse on the seam. If the file does not exist yet, behaves identically to write (creates it + any missing parent directories). Path is relative to wp-content/themes, e.g. `vds/css/custom.css`. Useful for adding per-page CSS to a shared theme stylesheet without rewriting the whole file. Requires plugin v7.0.42+ and the user behind the API key must have the WP `edit_themes` capability. Hard cap: post-append file size cannot exceed 1 MiB. Returns appended_bytes, total_bytes, content_md5, theme_role, mtime.',
1889
+ inputSchema: {
1890
+ type: 'object',
1891
+ properties: {
1892
+ relative_path: {
1893
+ type: 'string',
1894
+ description: 'Path relative to wp-content/themes/. Example: `vds/css/custom.css`. No leading slash, no `..` segments. Extension must be one of: css, scss, less, json.',
1895
+ },
1896
+ content: {
1897
+ type: 'string',
1898
+ description: 'Block to append. Up to 1 MiB. Final file size after append cannot exceed 1 MiB.',
1899
+ },
1900
+ },
1901
+ required: ['relative_path', 'content'],
1902
+ },
1903
+ },
1819
1904
  {
1820
1905
  name: 'wordpress_switch_site',
1821
1906
  description: 'Switch to a different WordPress site in the active Respira multi-site configuration.',
@@ -3844,9 +3929,22 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3844
3929
  case 'wordpress_list_sites': {
3845
3930
  const allSites = Array.from(this.sites.values());
3846
3931
  const visibleSites = allSites.filter((site) => this.isSiteAllowed(site));
3932
+ // v6.17.2: surface hidden sites so the AI can explain
3933
+ // ("baymcp.com is configured but hidden by RESPIRA_SITES") instead
3934
+ // of pretending they don't exist. The filter itself stays in place
3935
+ // for security/scoping; this just makes it visible.
3936
+ const hiddenSites = allSites
3937
+ .filter((site) => !this.isSiteAllowed(site))
3938
+ .map((site) => this.getSiteSummary(site));
3847
3939
  return {
3848
3940
  sites: visibleSites.map((site) => this.getSiteSummary(site)),
3849
3941
  active_site: this.getActiveSiteSummary(),
3942
+ ...(hiddenSites.length > 0
3943
+ ? {
3944
+ hidden_by_filter: hiddenSites,
3945
+ filter_note: `${hiddenSites.length} site${hiddenSites.length === 1 ? '' : 's'} hidden by RESPIRA_SITES env var (allow-list: ${this.allowedSites ? [...this.allowedSites].join(', ') : 'none'}). To show, remove or update RESPIRA_SITES in claude_desktop_config.json.`,
3946
+ }
3947
+ : {}),
3850
3948
  };
3851
3949
  }
3852
3950
  case 'wordpress_get_active_site':
@@ -3947,6 +4045,18 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3947
4045
  };
3948
4046
  case 'wordpress_validate_security':
3949
4047
  return await client.validateSecurity(args.content);
4048
+ case 'wordpress_read_theme_file':
4049
+ return await client.readThemeFile(args.relative_path);
4050
+ case 'wordpress_write_theme_file':
4051
+ return {
4052
+ ...(await client.writeThemeFile(args.relative_path, args.content)),
4053
+ respira_approvals_url: client.getApprovalsUrl(),
4054
+ };
4055
+ case 'wordpress_append_theme_file':
4056
+ return {
4057
+ ...(await client.appendThemeFile(args.relative_path, args.content)),
4058
+ respira_approvals_url: client.getApprovalsUrl(),
4059
+ };
3950
4060
  case 'wordpress_switch_site': {
3951
4061
  const newSite = this.sites.get(args.site_id);
3952
4062
  if (!newSite) {
@@ -3963,8 +4073,35 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
3963
4073
  site: this.getSiteSummary(newSite),
3964
4074
  };
3965
4075
  }
3966
- case 'wordpress_diagnose_connection':
3967
- return await client.diagnoseConnection({ post_id: args.post_id, probe_timeout_ms: args.probe_timeout_ms });
4076
+ case 'wordpress_diagnose_connection': {
4077
+ const siteDiag = await client.diagnoseConnection({ post_id: args.post_id, probe_timeout_ms: args.probe_timeout_ms });
4078
+ // v6.17.2: append process-level runtime info so version drift,
4079
+ // orphan processes, and a stale RESPIRA_SITES filter become
4080
+ // visible from a single tool call rather than requiring shell
4081
+ // access. Origin: cekt-ro 2026-05-20 bug report (5 orphan
4082
+ // processes flapping the tool catalog because npm exec ignored
4083
+ // the @latest pin and bound to a homebrew v6.14.2 install).
4084
+ const binaryPath = process.argv[1] || '<unknown>';
4085
+ return {
4086
+ ...siteDiag,
4087
+ runtime: {
4088
+ version: MCP_SERVER_VERSION,
4089
+ pid: process.pid,
4090
+ binary_path: binaryPath,
4091
+ is_global_install: /\/opt\/homebrew\/|\/usr\/local\//.test(binaryPath),
4092
+ sibling_pids: findSiblingProcesses(),
4093
+ uptime_seconds: Math.round(process.uptime()),
4094
+ node_version: process.version,
4095
+ },
4096
+ filter: {
4097
+ RESPIRA_SITES_active: this.allowedSites !== null,
4098
+ allowed_hostnames: this.allowedSites ? [...this.allowedSites] : null,
4099
+ hidden_site_count: this.allowedSites
4100
+ ? Array.from(this.sites.values()).filter((s) => !this.isSiteAllowed(s)).length
4101
+ : 0,
4102
+ },
4103
+ };
4104
+ }
3968
4105
  // v6.17: docs full-text search. See searchDocs() below.
3969
4106
  case 'wordpress_search_docs':
3970
4107
  return await this.searchDocs(String(args?.query || ''), Number(args?.limit ?? 5));
@@ -4869,7 +5006,61 @@ Use respira_get_builder_info first to detect which builder is active. Then use t
4869
5006
  })
4870
5007
  .filter((s) => !!s);
4871
5008
  const sitesLabel = siteNames.length ? siteNames.join(', ') : 'no sites configured';
4872
- console.error(`respira-mcp v${MCP_SERVER_VERSION} ready · ${siteNames.length} site${siteNames.length === 1 ? '' : 's'}: ${sitesLabel}`);
5009
+ console.error(`respira-mcp v${MCP_SERVER_VERSION} ready · pid ${process.pid} · ${siteNames.length} site${siteNames.length === 1 ? '' : 's'}: ${sitesLabel}`);
5010
+ // v6.17.2: detect orphan sibling processes from prior reconnect cycles
5011
+ // and warn loudly. The Claude Code / Conductor MCP supervisor sometimes
5012
+ // spawns a new server without reaping the previous one, leaving 5+
5013
+ // parallel pairs alive that flap the tool catalog because each one
5014
+ // ships a different feature set (see cekt-ro bug report 2026-05-20).
5015
+ // We can't kill the siblings ourselves (they belong to the supervisor),
5016
+ // but listing them makes the failure mode obvious instead of invisible.
5017
+ warnAboutSiblingProcesses(findSiblingProcesses());
5018
+ // v6.17.2: print the running binary path so a global homebrew /
5019
+ // /usr/local install accidentally shadowing the `@latest` pin in
5020
+ // claude_desktop_config.json becomes visible. npm exec strips the
5021
+ // @version part of the spec when it resolves the binary, so a
5022
+ // globally-installed older version will silently win over the
5023
+ // registry's latest. Same cekt-ro bug report, bug 2.
5024
+ const binaryPath = process.argv[1] || '<unknown>';
5025
+ const isGlobal = /\/opt\/homebrew\/|\/usr\/local\//.test(binaryPath);
5026
+ if (isGlobal) {
5027
+ console.error(`respira-mcp: running from global install at ${binaryPath}. ` +
5028
+ `If your claude_desktop_config.json pins @latest, npm exec is ignoring the pin and serving this older binary. ` +
5029
+ `To unblock: 'npm uninstall -g @respira/wordpress-mcp-server' (or 'brew uninstall wordpress-mcp-server'), then reconnect.`);
5030
+ }
5031
+ }
5032
+ }
5033
+ /**
5034
+ * v6.17.2: detect orphan wordpress-mcp-server processes from prior
5035
+ * supervisor reconnect cycles. Pure function — does not log. Used by
5036
+ * the boot warning and by respira_diagnose_connection so diagnose
5037
+ * doesn't re-emit a stderr warning on every call.
5038
+ */
5039
+ function findSiblingProcesses() {
5040
+ try {
5041
+ // ps -eo lists every process; awk filters to ones that contain
5042
+ // 'wordpress-mcp-server' in their command and aren't our own PID.
5043
+ const psOutput = execSync(`ps -eo pid,command 2>/dev/null | awk -v me=${process.pid} '$0 ~ /wordpress-mcp-server/ && $0 !~ /awk/ && $1 != me { print $1 }'`, { encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
5044
+ return psOutput
5045
+ ? psOutput.split('\n').map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n))
5046
+ : [];
5047
+ }
5048
+ catch {
5049
+ return [];
5050
+ }
5051
+ }
5052
+ /**
5053
+ * v6.17.2: emit a one-shot stderr warning at boot if any sibling
5054
+ * wordpress-mcp-server processes are running. We can't kill them
5055
+ * (they belong to the Claude Code / Conductor supervisor), but
5056
+ * listing them makes the flapping-tool-catalog failure mode obvious.
5057
+ * See cekt-ro bug report 2026-05-20.
5058
+ */
5059
+ function warnAboutSiblingProcesses(pids) {
5060
+ if (pids.length > 0) {
5061
+ console.error(`respira-mcp: ${pids.length} other wordpress-mcp-server process${pids.length === 1 ? '' : 'es'} running (pid${pids.length === 1 ? '' : 's'}: ${pids.join(', ')}). ` +
5062
+ `These are likely orphans from prior reconnects and may flap the tool catalog (different versions answering different calls). ` +
5063
+ `To clean: pkill -f wordpress-mcp-server, then reconnect.`);
4873
5064
  }
4874
5065
  }
4875
5066
  //# sourceMappingURL=server.js.map