@respira/wordpress-mcp-server 8.2.3 → 8.2.4
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/CHANGELOG.md +12 -0
- package/dist/__tests__/invoke-ability-required-args.test.d.ts +2 -0
- package/dist/__tests__/invoke-ability-required-args.test.d.ts.map +1 -0
- package/dist/__tests__/invoke-ability-required-args.test.js +53 -0
- package/dist/__tests__/invoke-ability-required-args.test.js.map +1 -0
- package/dist/__tests__/oq-put-fallback-callRestV2-integration.test.d.ts +24 -0
- package/dist/__tests__/oq-put-fallback-callRestV2-integration.test.d.ts.map +1 -0
- package/dist/__tests__/oq-put-fallback-callRestV2-integration.test.js +135 -0
- package/dist/__tests__/oq-put-fallback-callRestV2-integration.test.js.map +1 -0
- package/dist/__tests__/transport-mode-observability.test.d.ts +2 -0
- package/dist/__tests__/transport-mode-observability.test.d.ts.map +1 -0
- package/dist/__tests__/transport-mode-observability.test.js +201 -0
- package/dist/__tests__/transport-mode-observability.test.js.map +1 -0
- package/dist/__tests__/woo-card-tools-forward-fse-params.test.js +2 -2
- package/dist/__tests__/woo-card-tools-forward-fse-params.test.js.map +1 -1
- package/dist/config.d.ts +102 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +229 -0
- package/dist/config.js.map +1 -1
- package/dist/server.d.ts +42 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +218 -24
- package/dist/server.js.map +1 -1
- package/dist/wordpress-client.d.ts +17 -0
- package/dist/wordpress-client.d.ts.map +1 -1
- package/dist/wordpress-client.js +43 -0
- package/dist/wordpress-client.js.map +1 -1
- package/package.json +1 -1
- package/skills/design-system-synthesizer/SKILL.md +30 -9
- package/skills/design-system-synthesizer/metadata.json +6 -3
- package/skills/figma-to-elementor/SKILL.md +14 -2
- package/skills/html-to-bricks/SKILL.md +16 -2
- package/skills/html-to-bricks/metadata.json +4 -3
- package/skills/migrate-beaver-builder-to-bricks/SKILL.md +20 -3
- package/skills/migrate-beaver-builder-to-gutenberg/SKILL.md +20 -3
- package/skills/migrate-brizy-to-gutenberg/SKILL.md +20 -3
- package/skills/migrate-divi-to-breakdance/SKILL.md +20 -3
- package/skills/migrate-divi-to-bricks/SKILL.md +20 -3
- package/skills/migrate-divi-to-gutenberg/SKILL.md +20 -3
- package/skills/migrate-elementor-to-breakdance/SKILL.md +21 -4
- package/skills/migrate-elementor-to-bricks/SKILL.md +21 -4
- package/skills/migrate-elementor-to-gutenberg/SKILL.md +21 -4
- package/skills/migrate-elementor-to-oxygen/SKILL.md +21 -4
- package/skills/migrate-oxygen-to-breakdance/SKILL.md +20 -3
- package/skills/migrate-oxygen-to-bricks/SKILL.md +20 -3
- package/skills/migrate-thrive-architect-to-gutenberg/SKILL.md +20 -3
- package/skills/migrate-visual-composer-to-gutenberg/SKILL.md +20 -3
- package/skills/migrate-wpbakery-to-bricks/SKILL.md +20 -3
- package/skills/migrate-wpbakery-to-gutenberg/SKILL.md +20 -3
- package/skills/prime-the-agent/SKILL.md +35 -13
- package/skills/prime-the-agent/metadata.json +6 -5
package/dist/server.js
CHANGED
|
@@ -18,6 +18,7 @@ import { getBricksTools, dispatchBricksTool } from './bricks-tools.js';
|
|
|
18
18
|
import { getElementorTools, dispatchElementorTool } from './elementor-tools.js';
|
|
19
19
|
import { getAcfTools, resolveAcfToolName } from './acf-tools.js';
|
|
20
20
|
import { getUsageEmitter, deriveToolKind } from './usage-emitter.js';
|
|
21
|
+
import { collapseLauncherProcesses, validateApiKeyShape } from './config.js';
|
|
21
22
|
// Process-local secret keeps target hashes useful for same-session retry
|
|
22
23
|
// detection without making low-entropy WordPress ids reversible centrally.
|
|
23
24
|
const TELEMETRY_HASH_SECRET = randomUUID();
|
|
@@ -144,6 +145,35 @@ function getMaxToolTimeoutMs() {
|
|
|
144
145
|
* possible top-level `dropped_settings` array, and a pre-summarised
|
|
145
146
|
* `styling_dropped` count/string if the plugin sends one.
|
|
146
147
|
*/
|
|
148
|
+
/**
|
|
149
|
+
* The `ability` argument for wordpress_invoke_ability, or a refusal that says
|
|
150
|
+
* how to fix the call.
|
|
151
|
+
*
|
|
152
|
+
* Validated here rather than letting `undefined` ride to the site. The POST
|
|
153
|
+
* body drops the key, WordPress answers `rest_missing_callback_param`, and the
|
|
154
|
+
* agent is told a REST parameter is missing without being told which one or
|
|
155
|
+
* what this tool calls it. Seven of those across three sites, and the recovery
|
|
156
|
+
* path from that message is guesswork.
|
|
157
|
+
*
|
|
158
|
+
* Aliases are named in the error rather than silently accepted. Quietly
|
|
159
|
+
* honouring `ability_name` would make the tool's own schema a lie, and the next
|
|
160
|
+
* agent would learn the wrong contract from a call that happened to work.
|
|
161
|
+
*/
|
|
162
|
+
export function requireAbilityName(args) {
|
|
163
|
+
const ability = args?.ability;
|
|
164
|
+
if (typeof ability === 'string' && ability.trim() !== '') {
|
|
165
|
+
return ability.trim();
|
|
166
|
+
}
|
|
167
|
+
const aliases = ['name', 'ability_name', 'abilityName', 'tool', 'slug'].filter((key) => typeof args?.[key] === 'string' && String(args[key]).trim() !== '');
|
|
168
|
+
const sawInstead = aliases.length
|
|
169
|
+
? ` You sent ${aliases.map((k) => '`' + k + '`').join(' and ')} instead; rename it to \`ability\`.`
|
|
170
|
+
: '';
|
|
171
|
+
throw new Error('wordpress_invoke_ability requires `ability`, the full registered name in the form ' +
|
|
172
|
+
'"vendor/ability-name" (for example "rankmath/get-meta").' +
|
|
173
|
+
sawInstead +
|
|
174
|
+
' Arguments for the ability go in `args`, not at the top level.' +
|
|
175
|
+
' Call wordpress_search_abilities first if you do not know the exact name.');
|
|
176
|
+
}
|
|
147
177
|
function hoistDroppedStylingWarnings(result) {
|
|
148
178
|
if (!result || typeof result !== 'object') {
|
|
149
179
|
return result;
|
|
@@ -1019,6 +1049,18 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
1019
1049
|
message: 'The token redeemed but the account has no connected WordPress sites yet. Add a site at respira.press/dashboard/sites and try again.',
|
|
1020
1050
|
};
|
|
1021
1051
|
}
|
|
1052
|
+
// Make the redeemed sites live in THIS session before touching the disk.
|
|
1053
|
+
//
|
|
1054
|
+
// This is the fix for the shape M.S. reported twice (ce28ca7e, 135558a9):
|
|
1055
|
+
// redeem returns success, writes a file, and the running server carries on
|
|
1056
|
+
// with exactly the site list it booted with. Under RESPIRA_CONFIG_B64 the
|
|
1057
|
+
// next start ignores that file too, so the redeem is a no-op that reports
|
|
1058
|
+
// success — the worst possible combination. Merging into the live map is
|
|
1059
|
+
// the same in-memory path maybeSelfHealSiteList already uses, and it works
|
|
1060
|
+
// identically under B64 and inside a sandbox that cannot write at all.
|
|
1061
|
+
// Deliberately before the write, so a write failure still leaves a working
|
|
1062
|
+
// session instead of nothing.
|
|
1063
|
+
const activation = this.activateRedeemedSites(config.sites);
|
|
1022
1064
|
// Write ~/.respira/config.json with the canonical shape. Make the
|
|
1023
1065
|
// directory if absent. If a file already exists, back it up first
|
|
1024
1066
|
// so the user can recover the previous config if they want.
|
|
@@ -1088,12 +1130,18 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
1088
1130
|
}
|
|
1089
1131
|
catch (err) {
|
|
1090
1132
|
const attempted = process.env.RESPIRA_CONFIG_FILE?.trim() || '~/.respira/config.json';
|
|
1133
|
+
const liveNow = activation.activated.length + activation.already_active.length;
|
|
1091
1134
|
return {
|
|
1092
1135
|
success: false,
|
|
1093
1136
|
error: 'write_failed',
|
|
1094
1137
|
// Name the ABSOLUTE path that failed. "could not write
|
|
1095
1138
|
// ~/.respira/config.json" sends someone looking in the wrong home.
|
|
1096
|
-
message: `Token redeemed but could not write the config to ${attempted} (${err?.message || 'unknown error'}).
|
|
1139
|
+
message: `Token redeemed but could not write the config to ${attempted} (${err?.message || 'unknown error'}). ` +
|
|
1140
|
+
(liveNow > 0
|
|
1141
|
+
? `${liveNow} site${liveNow === 1 ? ' is' : 's are'} usable in THIS session anyway, but the redeem will not survive a restart until the file exists. `
|
|
1142
|
+
: '') +
|
|
1143
|
+
'Download it from respira.press/dashboard/mcp and place it at that exact path.',
|
|
1144
|
+
activated_in_session: activation.activated,
|
|
1097
1145
|
};
|
|
1098
1146
|
}
|
|
1099
1147
|
// This token's own sites (what was just redeemed) vs. the merged total
|
|
@@ -1135,9 +1183,15 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
1135
1183
|
};
|
|
1136
1184
|
}
|
|
1137
1185
|
const hasConfigB64 = Boolean(process.env.RESPIRA_CONFIG_B64);
|
|
1138
|
-
const
|
|
1186
|
+
const liveCount = this.sites.size;
|
|
1187
|
+
const activationSentence = activation.activated.length > 0
|
|
1188
|
+
? `${activation.activated.length} of them ${activation.activated.length === 1 ? 'is' : 'are'} live in this session already, no restart needed (${liveCount} site${liveCount === 1 ? '' : 's'} usable right now).`
|
|
1189
|
+
: activation.failed.length > 0
|
|
1190
|
+
? `None of them could be activated in this session (${activation.failed.map((f) => `${f.url}: ${f.reason}`).join('; ')}). Restart the client after fixing that.`
|
|
1191
|
+
: 'They were already connected in this session, so nothing changed here.';
|
|
1192
|
+
const baseMessage = `Connected ${tokenSites.length} site${tokenSites.length === 1 ? '' : 's'} via this Respira Cowork token. ${allSites.length} site${allSites.length === 1 ? '' : 's'} total now written to ${configPath}. ${activationSentence}`;
|
|
1139
1193
|
const message = hasConfigB64
|
|
1140
|
-
? `${baseMessage}
|
|
1194
|
+
? `${baseMessage} Note for next time: your MCP server is configured via the RESPIRA_CONFIG_B64 environment variable, which takes precedence over the file on disk. The sites above work now, but to keep them after a restart, update RESPIRA_CONFIG_B64 in your agent's MCP server config with the base64-encoded contents of ${configPath}.`
|
|
1141
1195
|
: baseMessage;
|
|
1142
1196
|
return {
|
|
1143
1197
|
success: true,
|
|
@@ -1149,9 +1203,81 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
1149
1203
|
// Proof rather than assumption: what landed on disk, and whether the
|
|
1150
1204
|
// next start will actually read it.
|
|
1151
1205
|
write_verification: writeVerification,
|
|
1206
|
+
// Proof for the OTHER half: what this running server can use right now.
|
|
1207
|
+
// A redeem that changes neither of these two is a no-op, and now says so.
|
|
1208
|
+
session_activation: {
|
|
1209
|
+
activated_now: activation.activated,
|
|
1210
|
+
already_active: activation.already_active,
|
|
1211
|
+
failed: activation.failed,
|
|
1212
|
+
sites_usable_this_session: liveCount,
|
|
1213
|
+
},
|
|
1152
1214
|
message,
|
|
1153
1215
|
};
|
|
1154
1216
|
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Merge freshly redeemed sites into the LIVE site map.
|
|
1219
|
+
*
|
|
1220
|
+
* Same mechanism as maybeSelfHealSiteList, applied at the moment of redeem
|
|
1221
|
+
* instead of on the next list_sites call. Deduped by id and by normalised
|
|
1222
|
+
* URL, because a stale frozen config can hold the same site under a
|
|
1223
|
+
* different id than the token returns. Never throws: one malformed entry
|
|
1224
|
+
* must not fail a redeem that otherwise worked.
|
|
1225
|
+
*/
|
|
1226
|
+
activateRedeemedSites(sites) {
|
|
1227
|
+
const activated = [];
|
|
1228
|
+
const alreadyActive = [];
|
|
1229
|
+
const failed = [];
|
|
1230
|
+
const knownUrls = new Set();
|
|
1231
|
+
for (const client of this.sites.values()) {
|
|
1232
|
+
knownUrls.add(this.normalizeSiteUrl(client.getSiteUrl()));
|
|
1233
|
+
}
|
|
1234
|
+
for (const s of Array.isArray(sites) ? sites : []) {
|
|
1235
|
+
if (!s || typeof s !== 'object' || !s.id || !s.url || !s.apiKey) {
|
|
1236
|
+
failed.push({ url: String(s?.url || '(no url)'), reason: 'incomplete site entry (id, url or key missing)' });
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
const normalized = this.normalizeSiteUrl(s.url);
|
|
1240
|
+
if (this.sites.has(s.id) || knownUrls.has(normalized)) {
|
|
1241
|
+
alreadyActive.push({ id: String(s.id), url: String(s.url) });
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
try {
|
|
1245
|
+
// Same gate loadConfig applies (8.1.4): a masked key must fail here,
|
|
1246
|
+
// loudly, not become a live site that 401s on its first tool call.
|
|
1247
|
+
validateApiKeyShape(String(s.apiKey), `redeemed site "${s.id}"`);
|
|
1248
|
+
const client = new WordPressClient({
|
|
1249
|
+
id: s.id,
|
|
1250
|
+
url: s.url,
|
|
1251
|
+
apiKey: s.apiKey,
|
|
1252
|
+
name: s.name || s.id,
|
|
1253
|
+
default: false,
|
|
1254
|
+
});
|
|
1255
|
+
this.sites.set(s.id, client);
|
|
1256
|
+
knownUrls.add(normalized);
|
|
1257
|
+
if (!this.currentSite) {
|
|
1258
|
+
this.currentSite = client;
|
|
1259
|
+
this.defaultSiteId = s.id;
|
|
1260
|
+
}
|
|
1261
|
+
try {
|
|
1262
|
+
getUsageEmitter().registerSiteToken(s.url, s.apiKey, () => client.getTelemetryToken());
|
|
1263
|
+
}
|
|
1264
|
+
catch {
|
|
1265
|
+
// usage telemetry never blocks a redeem
|
|
1266
|
+
}
|
|
1267
|
+
activated.push({ id: String(s.id), url: String(s.url) });
|
|
1268
|
+
}
|
|
1269
|
+
catch (err) {
|
|
1270
|
+
failed.push({ url: String(s.url), reason: String(err?.message || err) });
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (activated.length > 0) {
|
|
1274
|
+
// The tool catalog is filtered per site; a new site can change it.
|
|
1275
|
+
this.cachedFilterContext = null;
|
|
1276
|
+
console.error(`respira-mcp: redeem activated ${activated.length} site${activated.length === 1 ? '' : 's'} in the running session ` +
|
|
1277
|
+
`(now ${this.sites.size} connected).`);
|
|
1278
|
+
}
|
|
1279
|
+
return { activated, already_active: alreadyActive, failed };
|
|
1280
|
+
}
|
|
1155
1281
|
/**
|
|
1156
1282
|
* Self-heal the in-memory site list from respira.press.
|
|
1157
1283
|
*
|
|
@@ -3155,7 +3281,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
3155
3281
|
},
|
|
3156
3282
|
{
|
|
3157
3283
|
name: 'wordpress_diagnose_connection',
|
|
3158
|
-
description: 'Run a connection-fingerprint diagnostic for the active site. Combines the plugin\'s server-side report (route registration, php/wp/plugin versions, edge plugin presence) with outside-in probes from the MCP server (REST root reachability, content-type sanity check on Respira routes, edge-layer headers). Use when a tool returns "html instead of json", an opaque 5xx, or when a connection that worked yesterday silently breaks. Returns a structured object including detected edge layers (Cloudflare, Wordfence, Sucuri) and concrete remediation recommendations.',
|
|
3284
|
+
description: 'Run a connection-fingerprint diagnostic for the active site. Combines the plugin\'s server-side report (route registration, php/wp/plugin versions, edge plugin presence) with outside-in probes from the MCP server (REST root reachability, content-type sanity check on Respira routes, edge-layer headers). Use when a tool returns "html instead of json", an opaque 5xx, or when a connection that worked yesterday silently breaks. ALSO use it when the user suspects the wrong site, a stale connection, or a setup step that "did nothing": the response carries transport_mode (which install is answering: mcpb_bundle, npx_cache, global_npm_install, project_local_install, source_checkout), transport.duplicate_entries (whether a second Respira server is running, which usually means the client config lists Respira twice), and client_config (which config source won and how many sites came out of it). Returns a structured object including detected edge layers (Cloudflare, Wordfence, Sucuri) and concrete remediation recommendations.',
|
|
3159
3285
|
inputSchema: {
|
|
3160
3286
|
type: 'object',
|
|
3161
3287
|
properties: {
|
|
@@ -5934,7 +6060,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
5934
6060
|
dry_run: { type: 'boolean' },
|
|
5935
6061
|
edit_target: {
|
|
5936
6062
|
type: 'string',
|
|
5937
|
-
enum: ['
|
|
6063
|
+
enum: ['approval', 'live'],
|
|
5938
6064
|
description: 'Set "live" to write the template directly instead of creating an approval proposal. Requires respira_allow_direct_edit on the site; without that option the call is refused rather than silently downgraded. Omit to keep the default proposal flow.',
|
|
5939
6065
|
},
|
|
5940
6066
|
verification_marker: {
|
|
@@ -5958,7 +6084,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
5958
6084
|
dry_run: { type: 'boolean' },
|
|
5959
6085
|
edit_target: {
|
|
5960
6086
|
type: 'string',
|
|
5961
|
-
enum: ['
|
|
6087
|
+
enum: ['approval', 'live'],
|
|
5962
6088
|
description: 'Set "live" to write the template directly instead of creating an approval proposal. Requires respira_allow_direct_edit on the site; without that option the call is refused rather than silently downgraded. Omit to keep the default proposal flow.',
|
|
5963
6089
|
},
|
|
5964
6090
|
verification_marker: {
|
|
@@ -5978,7 +6104,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
5978
6104
|
dry_run: { type: 'boolean' },
|
|
5979
6105
|
edit_target: {
|
|
5980
6106
|
type: 'string',
|
|
5981
|
-
enum: ['
|
|
6107
|
+
enum: ['approval', 'live'],
|
|
5982
6108
|
description: 'Set "live" to write the template directly instead of creating an approval proposal. Requires respira_allow_direct_edit on the site; without that option the call is refused rather than silently downgraded. Omit to keep the default proposal flow.',
|
|
5983
6109
|
},
|
|
5984
6110
|
verification_marker: {
|
|
@@ -6008,7 +6134,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
6008
6134
|
dry_run: { type: 'boolean' },
|
|
6009
6135
|
edit_target: {
|
|
6010
6136
|
type: 'string',
|
|
6011
|
-
enum: ['
|
|
6137
|
+
enum: ['approval', 'live'],
|
|
6012
6138
|
description: 'Set "live" to write the template directly instead of creating an approval proposal. Requires respira_allow_direct_edit on the site; without that option the call is refused rather than silently downgraded. Omit to keep the default proposal flow.',
|
|
6013
6139
|
},
|
|
6014
6140
|
verification_marker: {
|
|
@@ -6998,7 +7124,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
6998
7124
|
case 'wordpress_search_abilities':
|
|
6999
7125
|
return await client.searchAbilities(args);
|
|
7000
7126
|
case 'wordpress_invoke_ability':
|
|
7001
|
-
return await client.invokeInhaledAbility(args
|
|
7127
|
+
return await client.invokeInhaledAbility(requireAbilityName(args), args?.args || {});
|
|
7002
7128
|
case 'wordpress_list_pages':
|
|
7003
7129
|
return await client.listPages(args);
|
|
7004
7130
|
case 'wordpress_read_page':
|
|
@@ -7212,7 +7338,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
7212
7338
|
}
|
|
7213
7339
|
case 'wordpress_diagnose_connection': {
|
|
7214
7340
|
const siteDiag = await client.diagnoseConnection({ post_id: args.post_id, probe_timeout_ms: args.probe_timeout_ms });
|
|
7215
|
-
const { describeEffectiveConfig } = await import('./config.js');
|
|
7341
|
+
const { describeEffectiveConfig, describeTransportMode } = await import('./config.js');
|
|
7216
7342
|
// v6.17.2: append process-level runtime info so version drift,
|
|
7217
7343
|
// orphan processes, and a stale RESPIRA_SITES filter become
|
|
7218
7344
|
// visible from a single tool call rather than requiring shell
|
|
@@ -7220,14 +7346,34 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
7220
7346
|
// processes flapping the tool catalog because npm exec ignored
|
|
7221
7347
|
// the @latest pin and bound to a homebrew v6.14.2 install).
|
|
7222
7348
|
const binaryPath = process.argv[1] || '<unknown>';
|
|
7349
|
+
const siblingScan = findSiblingServerProcesses();
|
|
7350
|
+
// Which INSTALL is answering, and whether a second one is running.
|
|
7351
|
+
//
|
|
7352
|
+
// client_config below says which configuration won. This says which
|
|
7353
|
+
// binary won. Desmond installed the .mcpb bundle while an older npx
|
|
7354
|
+
// entry stayed in his client config; both were valid Respira servers,
|
|
7355
|
+
// the client picked one, and nothing said which. See
|
|
7356
|
+
// describeTransportMode and describeDuplicateEntries in config.ts for
|
|
7357
|
+
// exactly what is and is not knowable from inside the server.
|
|
7358
|
+
const transport = describeTransportMode({
|
|
7359
|
+
binaryPath,
|
|
7360
|
+
siblings: siblingScan.siblings,
|
|
7361
|
+
processScanWorked: siblingScan.scanned,
|
|
7362
|
+
});
|
|
7223
7363
|
return {
|
|
7224
7364
|
...siteDiag,
|
|
7365
|
+
// Promoted to the top level because it is the field support asks for
|
|
7366
|
+
// first: 'mcpb_bundle' | 'npx_cache' | 'global_npm_install' |
|
|
7367
|
+
// 'project_local_install' | 'source_checkout' | 'unknown'.
|
|
7368
|
+
transport_mode: transport.transport_mode,
|
|
7369
|
+
transport,
|
|
7225
7370
|
runtime: {
|
|
7226
7371
|
version: MCP_SERVER_VERSION,
|
|
7227
7372
|
pid: process.pid,
|
|
7228
7373
|
binary_path: binaryPath,
|
|
7229
|
-
|
|
7230
|
-
|
|
7374
|
+
launch_mode: transport.transport_mode,
|
|
7375
|
+
is_global_install: transport.is_global_install,
|
|
7376
|
+
sibling_pids: collapseLauncherProcesses(process.pid, process.ppid, siblingScan.siblings).map((s) => s.pid),
|
|
7231
7377
|
uptime_seconds: Math.round(process.uptime()),
|
|
7232
7378
|
node_version: process.version,
|
|
7233
7379
|
},
|
|
@@ -8771,24 +8917,72 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
8771
8917
|
}
|
|
8772
8918
|
}
|
|
8773
8919
|
/**
|
|
8774
|
-
*
|
|
8775
|
-
*
|
|
8776
|
-
* the
|
|
8777
|
-
*
|
|
8920
|
+
* Command lines that belong to a Respira MCP server.
|
|
8921
|
+
*
|
|
8922
|
+
* The first two alternatives are the npm binaries. The third is the desktop
|
|
8923
|
+
* bundle, which the old filter missed: the .mcpb wrapper launches
|
|
8924
|
+
* `node .../Claude Extensions/<id>/server/index.js` and imports the package
|
|
8925
|
+
* in-process, so the string 'wordpress-mcp-server' never appears on its
|
|
8926
|
+
* command line. That blind spot is why a bundle running alongside an npx
|
|
8927
|
+
* entry — Desmond's case — showed up as zero siblings.
|
|
8778
8928
|
*/
|
|
8779
|
-
function
|
|
8929
|
+
function isRespiraMcpProcess(command) {
|
|
8930
|
+
if (/wordpress-mcp-server|respira-wordpress-mcp/i.test(command))
|
|
8931
|
+
return true;
|
|
8932
|
+
// The bundle: a Respira-named extension directory running server/index.js.
|
|
8933
|
+
// Both halves are required so another vendor's desktop extension, which also
|
|
8934
|
+
// ends in server/index.js, is never counted as a second Respira server.
|
|
8935
|
+
return /respira/i.test(command) && /[/\\]server[/\\]index\.js/.test(command);
|
|
8936
|
+
}
|
|
8937
|
+
/**
|
|
8938
|
+
* v6.17.2: detect other Respira MCP server processes — orphans from prior
|
|
8939
|
+
* supervisor reconnect cycles, or a second install the client also started.
|
|
8940
|
+
* Pure function — does not log. Used by the boot warning and by
|
|
8941
|
+
* respira_diagnose_connection so diagnose doesn't re-emit a stderr warning
|
|
8942
|
+
* on every call.
|
|
8943
|
+
*
|
|
8944
|
+
* Filtering happens in JS rather than in awk so the caller can also see each
|
|
8945
|
+
* sibling's command line and classify how it was launched.
|
|
8946
|
+
*/
|
|
8947
|
+
function findSiblingServerProcesses() {
|
|
8780
8948
|
try {
|
|
8781
|
-
//
|
|
8782
|
-
//
|
|
8783
|
-
const psOutput = execSync(
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
:
|
|
8949
|
+
// ppid is what lets collapseLauncherProcesses tell one npx entry's
|
|
8950
|
+
// three-process launcher chain apart from three separate servers.
|
|
8951
|
+
const psOutput = execSync('ps -eo pid=,ppid=,command= 2>/dev/null', {
|
|
8952
|
+
encoding: 'utf8',
|
|
8953
|
+
timeout: 2000,
|
|
8954
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
8955
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
8956
|
+
});
|
|
8957
|
+
const siblings = [];
|
|
8958
|
+
for (const line of psOutput.split('\n')) {
|
|
8959
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
8960
|
+
if (!match)
|
|
8961
|
+
continue;
|
|
8962
|
+
const pid = Number.parseInt(match[1], 10);
|
|
8963
|
+
const ppid = Number.parseInt(match[2], 10);
|
|
8964
|
+
const command = match[3];
|
|
8965
|
+
if (Number.isNaN(pid) || pid === process.pid)
|
|
8966
|
+
continue;
|
|
8967
|
+
if (!isRespiraMcpProcess(command))
|
|
8968
|
+
continue;
|
|
8969
|
+
siblings.push({ pid, ppid, command });
|
|
8970
|
+
}
|
|
8971
|
+
return { scanned: true, siblings };
|
|
8787
8972
|
}
|
|
8788
8973
|
catch {
|
|
8789
|
-
return [];
|
|
8974
|
+
return { scanned: false, siblings: [] };
|
|
8790
8975
|
}
|
|
8791
8976
|
}
|
|
8977
|
+
/**
|
|
8978
|
+
* Leaf server processes only. The launcher chain above an npx run belongs to
|
|
8979
|
+
* the same logical server, so counting it would make the boot warning fire on
|
|
8980
|
+
* every healthy npx install.
|
|
8981
|
+
*/
|
|
8982
|
+
function findSiblingProcesses() {
|
|
8983
|
+
const { siblings } = findSiblingServerProcesses();
|
|
8984
|
+
return collapseLauncherProcesses(process.pid, process.ppid, siblings).map((s) => s.pid);
|
|
8985
|
+
}
|
|
8792
8986
|
/**
|
|
8793
8987
|
* v6.17.2: emit a one-shot stderr warning at boot if any sibling
|
|
8794
8988
|
* wordpress-mcp-server processes are running. We can't kill them
|