@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 +23 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +133 -0
- package/dist/server.js.map +1 -1
- package/dist/wordpress-client.d.ts +5 -0
- package/dist/wordpress-client.d.ts.map +1 -1
- package/dist/wordpress-client.js +7 -0
- package/dist/wordpress-client.js.map +1 -1
- package/package.json +1 -1
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,
|
package/dist/server.d.ts.map
CHANGED
|
@@ -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;
|
|
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,
|