@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 +23 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +161 -6
- 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,
|
|
@@ -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
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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:
|
|
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,
|