@respira/wordpress-mcp-server 7.2.10 → 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;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;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,
@@ -5041,6 +5163,10 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5041
5163
  ? await client.getSiteContext()
5042
5164
  : await client.getCompactSiteContext();
5043
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: [] }));
5044
5170
  const allSites = Array.from(this.sites.values());
5045
5171
  const visibleSites = allSites.filter((site) => this.isSiteAllowed(site));
5046
5172
  // v6.17.2: surface hidden sites so the AI can explain
@@ -5053,6 +5179,13 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
5053
5179
  return {
5054
5180
  sites: visibleSites.map((site) => this.getSiteSummary(site)),
5055
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
+ : {}),
5056
5189
  ...(hiddenSites.length > 0
5057
5190
  ? {
5058
5191
  hidden_by_filter: hiddenSites,