@respira/wordpress-mcp-server 7.2.9 → 7.2.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.
package/dist/server.d.ts CHANGED
@@ -15,6 +15,9 @@ export declare class RespiraWordPressServer {
15
15
  private allowedSites;
16
16
  /** Whether the plugin version warning has already been shown this session. */
17
17
  private versionWarningShown;
18
+ /** Epoch ms of the last account site-list refresh (throttle network calls). */
19
+ private lastSiteRefreshAt;
20
+ private static readonly SITE_REFRESH_MIN_INTERVAL_MS;
18
21
  private static readonly MCP_SERVER_VERSION;
19
22
  /**
20
23
  * Normalize a tool name: respira_* → wordpress_* for switch dispatch.
@@ -28,6 +31,26 @@ export declare class RespiraWordPressServer {
28
31
  private generateDualTools;
29
32
  constructor(siteConfigs: WordPressSiteConfig[], enabledTools?: string[]);
30
33
  private getSiteSummary;
34
+ /**
35
+ * Re-pull the account's CURRENT site list from respira.press and merge in any
36
+ * sites that aren't in the running server yet.
37
+ *
38
+ * Why this exists: the server boots from whatever site list was in its config
39
+ * at startup. When the user adds a site on the dashboard afterwards, a
40
+ * long-lived server (Cowork especially) never learns about it and silently
41
+ * serves the stale list — Emil had 4 sites but Cowork only ever saw 3
42
+ * (2026-06-30). Rather than make the user re-paste config and restart, this
43
+ * lets list_sites self-heal: authenticate with a `respira_site_` token the
44
+ * server already holds, fetch the account's canonical config, and register
45
+ * any new sites in-memory (and persist to ~/.respira/config.json when that is
46
+ * the active source).
47
+ *
48
+ * Best-effort and throttled: a network/auth failure is swallowed and the
49
+ * existing list is returned unchanged. Servers with no site token (OAuth-only)
50
+ * or pinned to RESPIRA_CONFIG_B64 still get the in-session merge; only the
51
+ * on-disk persistence is skipped when the file is not the active source.
52
+ */
53
+ private refreshSitesFromAccount;
31
54
  /**
32
55
  * N6 fix (v6.19.0): the response envelope `site` field must reflect the
33
56
  * client that ACTUALLY serviced the call, not the global default. Pre-fix,
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAqTzE,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;IAsVvE,OAAO,CAAC,cAAc;IAItB;;;;;;;;;OASG;IACH,OAAO,CAAC,oBAAoB;IAkB5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IA4BrB,gEAAgE;IAChE,OAAO,CAAC,aAAa;IAUrB;;;;;;OAMG;YACW,UAAU;IAgDxB;;;;;;;;;;;;;;OAcG;YACW,WAAW;IAiJzB;;;;;;;;;OASG;YACW,kBAAkB;IAiHhC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,6BAA6B;IA0BrC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA+BjC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YA2QP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IAuwFtB;;;;;;OAMG;IACH,yEAAyE;IACzE,OAAO,CAAC,mBAAmB,CAAoD;IAC/E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAElD;;;;;;;;;OASG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAQ7C;IAEF;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAQ3C;IAEF,4EAA4E;IAC5E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,+BAA+B,CAAY;IAEnE,uEAAuE;IACvE,OAAO,CAAC,MAAM,CAAC,SAAS;YAWV,oBAAoB;YAqFpB,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;IAqG5B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,eAAe;YAuCT,gBAAgB;IA22B9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAwUzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA6UxB,GAAG;CAyCV"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAsTzE,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;IACpC,+EAA+E;IAC/E,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,4BAA4B,CAAU;IAE9D,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;IAsVvE,OAAO,CAAC,cAAc;IAItB;;;;;;;;;;;;;;;;;;OAkBG;YACW,uBAAuB;IA0GrC;;;;;;;;;OASG;IACH,OAAO,CAAC,oBAAoB;IAkB5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IA4BrB,gEAAgE;IAChE,OAAO,CAAC,aAAa;IAUrB;;;;;;OAMG;YACW,UAAU;IAgDxB;;;;;;;;;;;;;;OAcG;YACW,WAAW;IAiJzB;;;;;;;;;OASG;YACW,kBAAkB;IAyIhC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,6BAA6B;IA0BrC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA+BjC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YA2QP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IAuwFtB;;;;;;OAMG;IACH,yEAAyE;IACzE,OAAO,CAAC,mBAAmB,CAAoD;IAC/E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAElD;;;;;;;;;OASG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAQ7C;IAEF;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAQ3C;IAEF,4EAA4E;IAC5E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,+BAA+B,CAAY;IAEnE,uEAAuE;IACvE,OAAO,CAAC,MAAM,CAAC,SAAS;YAWV,oBAAoB;YAqFpB,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;IAqG5B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,eAAe;YAuCT,gBAAgB;IAw3B9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAwUzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA6UxB,GAAG;CAyCV"}
package/dist/server.js CHANGED
@@ -17,6 +17,7 @@ import { getBricksTools, dispatchBricksTool } from './bricks-tools.js';
17
17
  import { getElementorTools, dispatchElementorTool } from './elementor-tools.js';
18
18
  import { getAcfTools, ACF_TOOL_NAMES } from './acf-tools.js';
19
19
  import { getUsageEmitter, deriveToolKind } from './usage-emitter.js';
20
+ import { loadConfig, saveConfig } from './config.js';
20
21
  /**
21
22
  * Per-tool argument aliases. AI clients (especially Cowork / Claude desktop)
22
23
  * frequently pass a natural-but-wrong key for a value: `pageId` / `page_id`
@@ -315,6 +316,9 @@ export class RespiraWordPressServer {
315
316
  allowedSites = null;
316
317
  /** Whether the plugin version warning has already been shown this session. */
317
318
  versionWarningShown = false;
319
+ /** Epoch ms of the last account site-list refresh (throttle network calls). */
320
+ lastSiteRefreshAt = 0;
321
+ static SITE_REFRESH_MIN_INTERVAL_MS = 60_000;
318
322
  static MCP_SERVER_VERSION = MCP_SERVER_VERSION;
319
323
  /**
320
324
  * Normalize a tool name: respira_* → wordpress_* for switch dispatch.
@@ -693,6 +697,124 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
693
697
  getSiteSummary(site) {
694
698
  return site.getSiteSummary(site.getSiteId() === this.defaultSiteId);
695
699
  }
700
+ /**
701
+ * Re-pull the account's CURRENT site list from respira.press and merge in any
702
+ * sites that aren't in the running server yet.
703
+ *
704
+ * Why this exists: the server boots from whatever site list was in its config
705
+ * at startup. When the user adds a site on the dashboard afterwards, a
706
+ * long-lived server (Cowork especially) never learns about it and silently
707
+ * serves the stale list — Emil had 4 sites but Cowork only ever saw 3
708
+ * (2026-06-30). Rather than make the user re-paste config and restart, this
709
+ * lets list_sites self-heal: authenticate with a `respira_site_` token the
710
+ * server already holds, fetch the account's canonical config, and register
711
+ * any new sites in-memory (and persist to ~/.respira/config.json when that is
712
+ * the active source).
713
+ *
714
+ * Best-effort and throttled: a network/auth failure is swallowed and the
715
+ * existing list is returned unchanged. Servers with no site token (OAuth-only)
716
+ * or pinned to RESPIRA_CONFIG_B64 still get the in-session merge; only the
717
+ * on-disk persistence is skipped when the file is not the active source.
718
+ */
719
+ async refreshSitesFromAccount(force = false) {
720
+ const result = { added: [] };
721
+ const now = Date.now();
722
+ if (!force && now - this.lastSiteRefreshAt < RespiraWordPressServer.SITE_REFRESH_MIN_INTERVAL_MS) {
723
+ return result;
724
+ }
725
+ // Pick a live dashboard site token to authenticate the refresh.
726
+ let authToken = '';
727
+ for (const client of this.sites.values()) {
728
+ const key = client.getApiKey();
729
+ if (typeof key === 'string' && key.startsWith('respira_site_')) {
730
+ authToken = key;
731
+ break;
732
+ }
733
+ }
734
+ if (!authToken) {
735
+ // OAuth-only or token-less server: nothing to authenticate with.
736
+ return result;
737
+ }
738
+ // Stamp before the network call so a hang/failure still respects the
739
+ // throttle and we don't retry on every single list_sites.
740
+ this.lastSiteRefreshAt = now;
741
+ const apiBase = process.env.RESPIRA_API_BASE || 'https://www.respira.press';
742
+ const url = `${apiBase.replace(/\/+$/, '')}/api/mcp/config/refresh`;
743
+ let payload = null;
744
+ try {
745
+ const response = await fetch(url, {
746
+ method: 'POST',
747
+ headers: { 'Content-Type': 'application/json' },
748
+ body: JSON.stringify({ token: authToken }),
749
+ signal: AbortSignal.timeout(8000),
750
+ });
751
+ if (!response.ok) {
752
+ return result;
753
+ }
754
+ payload = await response.json().catch(() => null);
755
+ }
756
+ catch {
757
+ return result;
758
+ }
759
+ const accountSites = payload?.config?.sites;
760
+ if (!Array.isArray(accountSites)) {
761
+ return result;
762
+ }
763
+ for (const site of accountSites) {
764
+ if (!site?.id || !site?.url || !site?.apiKey)
765
+ continue;
766
+ if (this.sites.has(site.id))
767
+ continue;
768
+ const cfg = {
769
+ id: site.id,
770
+ name: site.name || site.id,
771
+ url: site.url,
772
+ apiKey: site.apiKey,
773
+ default: false,
774
+ };
775
+ const client = new WordPressClient(cfg);
776
+ this.sites.set(cfg.id, client);
777
+ if (this.currentSite === null) {
778
+ this.currentSite = client;
779
+ this.defaultSiteId = cfg.id;
780
+ }
781
+ try {
782
+ getUsageEmitter().registerSiteToken(cfg.url, cfg.apiKey);
783
+ }
784
+ catch {
785
+ // Best-effort; usage telemetry never blocks.
786
+ }
787
+ result.added.push(cfg.name);
788
+ }
789
+ // Persist new sites to disk so a restart keeps them — only when the file is
790
+ // the active config source. A RESPIRA_CONFIG_B64- or RESPIRA_CONFIG_FILE-
791
+ // pinned server still has the correct in-memory list this session; writing
792
+ // the default file would not be read on its next boot and could orphan a
793
+ // stray file, so skip it there.
794
+ const fileIsActiveSource = !process.env.RESPIRA_CONFIG_B64?.trim() && !process.env.RESPIRA_CONFIG_FILE?.trim();
795
+ if (result.added.length > 0 && fileIsActiveSource) {
796
+ try {
797
+ const onDisk = loadConfig();
798
+ const known = new Set(onDisk.sites.map((s) => s.id));
799
+ for (const site of accountSites) {
800
+ if (site?.id && site?.url && site?.apiKey && !known.has(site.id)) {
801
+ onDisk.sites.push({
802
+ id: site.id,
803
+ name: site.name || site.id,
804
+ url: site.url,
805
+ apiKey: site.apiKey,
806
+ default: false,
807
+ });
808
+ }
809
+ }
810
+ saveConfig(onDisk);
811
+ }
812
+ catch {
813
+ // Best-effort; the in-memory merge above is what unblocks this session.
814
+ }
815
+ }
816
+ return result;
817
+ }
696
818
  /**
697
819
  * N6 fix (v6.19.0): the response envelope `site` field must reflect the
698
820
  * client that ACTUALLY serviced the call, not the global default. Pre-fix,
@@ -1093,17 +1215,39 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
1093
1215
  url: s.url,
1094
1216
  }));
1095
1217
  const hasConfigB64 = Boolean(process.env.RESPIRA_CONFIG_B64);
1096
- const baseMessage = `Connected ${sites.length} site${sites.length === 1 ? '' : 's'} via the Respira Cowork token. The config was written to ~/.respira/config.json. Restart this Cowork chat (or open a new one) so the MCP server picks up the new sites.`;
1097
- const message = hasConfigB64
1098
- ? `${baseMessage} IMPORTANT: your MCP server is currently configured via the RESPIRA_CONFIG_B64 environment variable, which takes precedence over the file on disk. To activate the new site, update RESPIRA_CONFIG_B64 in your agent's MCP server config with the base64-encoded contents of the new ~/.respira/config.json file, then restart the agent.`
1099
- : baseMessage;
1218
+ // FAIL LOUD when the server is pinned to RESPIRA_CONFIG_B64. The config load
1219
+ // order is RESPIRA_CONFIG_B64 -> RESPIRA_CONFIG_FILE -> ~/.respira/config.json
1220
+ // (see config.ts), so the file we just wrote is OVERRIDDEN by the env var and
1221
+ // the freshly-redeemed token never takes effect. The old code reported
1222
+ // success with the caveat appended to the end of the message; agents glossed
1223
+ // over it and reported "no output", so the user reconnected forever with a
1224
+ // stale token (Sara / crediso.io: repeated redeems, token never updated,
1225
+ // every authenticated call rejected). Returning success:false with the fix
1226
+ // and the exact replacement value up front makes it impossible to miss.
1227
+ if (hasConfigB64) {
1228
+ const newConfigB64 = Buffer.from(JSON.stringify(config)).toString('base64');
1229
+ return {
1230
+ success: false,
1231
+ error: 'config_pinned_to_env',
1232
+ config_b64_active: true,
1233
+ action_required: true,
1234
+ config_path: configPath,
1235
+ sites_count: sites.length,
1236
+ sites,
1237
+ new_config_b64: newConfigB64,
1238
+ message: 'The token was redeemed and written to ~/.respira/config.json, but it will NOT take effect yet: this MCP server is pinned to the RESPIRA_CONFIG_B64 environment variable, which overrides the config file. That is exactly why reconnecting never updates the token and authenticated calls keep failing. ' +
1239
+ 'Fix it one of two ways, then FULLY restart the agent: ' +
1240
+ '(1) remove RESPIRA_CONFIG_B64 from this server\'s MCP config so the file just written is used, or ' +
1241
+ '(2) replace the value of RESPIRA_CONFIG_B64 with this updated base64:\n\n' + newConfigB64,
1242
+ };
1243
+ }
1100
1244
  return {
1101
1245
  success: true,
1102
1246
  sites_count: sites.length,
1103
1247
  sites,
1104
1248
  config_path: configPath,
1105
- config_b64_active: hasConfigB64,
1106
- message,
1249
+ config_b64_active: false,
1250
+ message: `Connected ${sites.length} site${sites.length === 1 ? '' : 's'} via the Respira Cowork token. The config was written to ~/.respira/config.json. Restart this Cowork chat (or open a new one) so the MCP server picks up the new sites.`,
1107
1251
  };
1108
1252
  }
1109
1253
  /**
@@ -5019,6 +5163,10 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5019
5163
  ? await client.getSiteContext()
5020
5164
  : await client.getCompactSiteContext();
5021
5165
  case 'wordpress_list_sites': {
5166
+ // Self-heal a stale list: re-pull the account's current sites so a site
5167
+ // added on the dashboard after this server booted shows up without a
5168
+ // re-paste or restart. Best-effort and throttled; never blocks the list.
5169
+ const refresh = await this.refreshSitesFromAccount().catch(() => ({ added: [] }));
5022
5170
  const allSites = Array.from(this.sites.values());
5023
5171
  const visibleSites = allSites.filter((site) => this.isSiteAllowed(site));
5024
5172
  // v6.17.2: surface hidden sites so the AI can explain
@@ -5031,6 +5179,13 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5031
5179
  return {
5032
5180
  sites: visibleSites.map((site) => this.getSiteSummary(site)),
5033
5181
  active_site: this.getActiveSiteSummary(),
5182
+ ...(refresh.added.length > 0
5183
+ ? {
5184
+ refreshed_note: `Picked up ${refresh.added.length} newly added site${refresh.added.length === 1 ? '' : 's'} from your respira.press account: ${refresh.added.join(', ')}.${process.env.RESPIRA_CONFIG_B64?.trim()
5185
+ ? ' This server is pinned to the RESPIRA_CONFIG_B64 env var, so update that env to keep these sites after a restart.'
5186
+ : ''}`,
5187
+ }
5188
+ : {}),
5034
5189
  ...(hiddenSites.length > 0
5035
5190
  ? {
5036
5191
  hidden_by_filter: hiddenSites,