@respira/wordpress-mcp-server 7.5.2 → 7.5.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/dist/__tests__/delete-media-approval-token.test.d.ts +21 -0
- package/dist/__tests__/delete-media-approval-token.test.d.ts.map +1 -0
- package/dist/__tests__/delete-media-approval-token.test.js +126 -0
- package/dist/__tests__/delete-media-approval-token.test.js.map +1 -0
- package/dist/__tests__/tool-filter-context-cache.test.d.ts +2 -0
- package/dist/__tests__/tool-filter-context-cache.test.d.ts.map +1 -0
- package/dist/__tests__/tool-filter-context-cache.test.js +69 -0
- package/dist/__tests__/tool-filter-context-cache.test.js.map +1 -0
- package/dist/__tests__/upload-media-path-detection.test.d.ts +2 -0
- package/dist/__tests__/upload-media-path-detection.test.d.ts.map +1 -0
- package/dist/__tests__/upload-media-path-detection.test.js +70 -0
- package/dist/__tests__/upload-media-path-detection.test.js.map +1 -0
- package/dist/server.d.ts +30 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +72 -28
- package/dist/server.js.map +1 -1
- package/dist/wordpress-client.d.ts +49 -1
- package/dist/wordpress-client.d.ts.map +1 -1
- package/dist/wordpress-client.js +81 -6
- package/dist/wordpress-client.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for ticket 927aa7ed: "delete_media approval flow never
|
|
3
|
+
* completes: token rejected in a loop".
|
|
4
|
+
*
|
|
5
|
+
* Root cause: `WordPressClient.deleteMedia()` only ever forwarded `id` to the
|
|
6
|
+
* WP REST endpoint (`DELETE /media/{id}`). The MCP tool schema for
|
|
7
|
+
* `wordpress_delete_media` advertises `approval_token` and `force` params and
|
|
8
|
+
* the server.ts dispatcher received them from the caller, but the client
|
|
9
|
+
* method silently dropped both on the floor before the request ever went
|
|
10
|
+
* out. The WP-side approval gate (Respira_Tool_Governance) never saw a token
|
|
11
|
+
* on the "confirm" call, so it always treated it as a fresh request and
|
|
12
|
+
* minted a brand-new token — an infinite loop, indistinguishable from the
|
|
13
|
+
* outside from a broken approval mechanism, even though the mechanism itself
|
|
14
|
+
* (proven by delete_page working) was fine.
|
|
15
|
+
*
|
|
16
|
+
* Uses the same axios mock-adapter pattern as rest-route-fallback.test.ts.
|
|
17
|
+
*
|
|
18
|
+
* @since 7.5.x (ticket 927aa7ed)
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=delete-media-approval-token.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delete-media-approval-token.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/delete-media-approval-token.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for ticket 927aa7ed: "delete_media approval flow never
|
|
3
|
+
* completes: token rejected in a loop".
|
|
4
|
+
*
|
|
5
|
+
* Root cause: `WordPressClient.deleteMedia()` only ever forwarded `id` to the
|
|
6
|
+
* WP REST endpoint (`DELETE /media/{id}`). The MCP tool schema for
|
|
7
|
+
* `wordpress_delete_media` advertises `approval_token` and `force` params and
|
|
8
|
+
* the server.ts dispatcher received them from the caller, but the client
|
|
9
|
+
* method silently dropped both on the floor before the request ever went
|
|
10
|
+
* out. The WP-side approval gate (Respira_Tool_Governance) never saw a token
|
|
11
|
+
* on the "confirm" call, so it always treated it as a fresh request and
|
|
12
|
+
* minted a brand-new token — an infinite loop, indistinguishable from the
|
|
13
|
+
* outside from a broken approval mechanism, even though the mechanism itself
|
|
14
|
+
* (proven by delete_page working) was fine.
|
|
15
|
+
*
|
|
16
|
+
* Uses the same axios mock-adapter pattern as rest-route-fallback.test.ts.
|
|
17
|
+
*
|
|
18
|
+
* @since 7.5.x (ticket 927aa7ed)
|
|
19
|
+
*/
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import axios from 'axios';
|
|
23
|
+
import { WordPressClient } from '../wordpress-client.js';
|
|
24
|
+
const callLog = [];
|
|
25
|
+
let currentHandler = null;
|
|
26
|
+
const originalAdapter = axios.defaults.adapter;
|
|
27
|
+
function installMockAdapter() {
|
|
28
|
+
callLog.length = 0;
|
|
29
|
+
axios.defaults.adapter = ((config) => {
|
|
30
|
+
callLog.push({
|
|
31
|
+
method: (config.method || 'get').toLowerCase(),
|
|
32
|
+
url: config.url,
|
|
33
|
+
params: config.params,
|
|
34
|
+
});
|
|
35
|
+
if (!currentHandler) {
|
|
36
|
+
return Promise.reject(new Error('no mock handler installed'));
|
|
37
|
+
}
|
|
38
|
+
const result = currentHandler(config);
|
|
39
|
+
return Promise.resolve(result).then((r) => ({
|
|
40
|
+
status: r.status,
|
|
41
|
+
statusText: 'OK',
|
|
42
|
+
headers: {},
|
|
43
|
+
config,
|
|
44
|
+
data: r.data,
|
|
45
|
+
request: {},
|
|
46
|
+
}));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function restoreAdapter() {
|
|
50
|
+
axios.defaults.adapter = originalAdapter;
|
|
51
|
+
currentHandler = null;
|
|
52
|
+
}
|
|
53
|
+
function setHandler(h) {
|
|
54
|
+
currentHandler = h;
|
|
55
|
+
}
|
|
56
|
+
function buildConfig() {
|
|
57
|
+
return {
|
|
58
|
+
id: 'test-site',
|
|
59
|
+
name: 'Test Site',
|
|
60
|
+
url: 'https://example.com',
|
|
61
|
+
apiKey: 'respira_test_key',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// -----------------------------------------------------------------------------
|
|
65
|
+
// Tests
|
|
66
|
+
// -----------------------------------------------------------------------------
|
|
67
|
+
test('deleteMedia forwards approval_token on the confirm call (927aa7ed regression)', async () => {
|
|
68
|
+
installMockAdapter();
|
|
69
|
+
try {
|
|
70
|
+
setHandler(() => ({ status: 200, data: { success: true } }));
|
|
71
|
+
const client = new WordPressClient(buildConfig());
|
|
72
|
+
// First call: no token yet, simulates the initial delete attempt.
|
|
73
|
+
await client.deleteMedia(42);
|
|
74
|
+
assert.equal(callLog.length, 1);
|
|
75
|
+
assert.equal(callLog[0].method, 'delete');
|
|
76
|
+
assert.equal(callLog[0].url, '/media/42');
|
|
77
|
+
assert.ok(!callLog[0].params || callLog[0].params.approval_token === undefined, 'first call must not send an approval_token');
|
|
78
|
+
// Second call: the agent echoes back the exact token it was handed.
|
|
79
|
+
// Pre-fix this token was silently dropped and the WP side looped forever.
|
|
80
|
+
await client.deleteMedia(42, undefined, 'approve-tok-abc123');
|
|
81
|
+
assert.equal(callLog.length, 2);
|
|
82
|
+
assert.equal(callLog[1].method, 'delete');
|
|
83
|
+
assert.equal(callLog[1].url, '/media/42');
|
|
84
|
+
assert.equal(callLog[1].params?.approval_token, 'approve-tok-abc123', 'confirm call must forward the approval_token verbatim');
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
restoreAdapter();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
test('deleteMedia forwards force alongside approval_token', async () => {
|
|
91
|
+
installMockAdapter();
|
|
92
|
+
try {
|
|
93
|
+
setHandler(() => ({ status: 200, data: { success: true } }));
|
|
94
|
+
const client = new WordPressClient(buildConfig());
|
|
95
|
+
await client.deleteMedia(7, true, 'tok-xyz');
|
|
96
|
+
assert.equal(callLog.length, 1);
|
|
97
|
+
assert.equal(callLog[0].params?.force, true);
|
|
98
|
+
assert.equal(callLog[0].params?.approval_token, 'tok-xyz');
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
restoreAdapter();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
test('deletePage approval flow is unaffected (no regression)', async () => {
|
|
105
|
+
installMockAdapter();
|
|
106
|
+
try {
|
|
107
|
+
setHandler(() => ({ status: 200, data: { success: true } }));
|
|
108
|
+
const client = new WordPressClient(buildConfig());
|
|
109
|
+
// No force -> no params sent at all (matches pre-existing behavior).
|
|
110
|
+
await client.deletePage(99);
|
|
111
|
+
assert.equal(callLog[0].url, '/pages/99');
|
|
112
|
+
assert.ok(!callLog[0].params || Object.keys(callLog[0].params).length === 0);
|
|
113
|
+
// force + approval_token -> both forwarded, plus confirm_live_edit which
|
|
114
|
+
// deletePage has always sent alongside force.
|
|
115
|
+
await client.deletePage(99, true, 'page-tok-1');
|
|
116
|
+
const call = callLog[1];
|
|
117
|
+
assert.equal(call.url, '/pages/99');
|
|
118
|
+
assert.equal(call.params.force, true);
|
|
119
|
+
assert.equal(call.params.confirm_live_edit, true);
|
|
120
|
+
assert.equal(call.params.approval_token, 'page-tok-1');
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
restoreAdapter();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=delete-media-approval-token.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delete-media-approval-token.test.js","sourceRoot":"","sources":["../../src/__tests__/delete-media-approval-token.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-filter-context-cache.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/tool-filter-context-cache.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { RespiraWordPressServer } from '../server.js';
|
|
4
|
+
/**
|
|
5
|
+
* Regression test for the getTools() blocking-network-calls bug
|
|
6
|
+
* (nathankohut.co.uk, Windows, 2026-07-19): isWooCommerceAddonAvailable(),
|
|
7
|
+
* isAcfAvailable(), and filterToolsByContext() each independently called
|
|
8
|
+
* getSiteContext()/getCompactSiteContext() — three uncached, sequential live
|
|
9
|
+
* HTTP calls to the customer's own site before tools/list could return,
|
|
10
|
+
* each carrying the wordpress-client retry backoff. A slow/failing site
|
|
11
|
+
* could push a single tools/list response well past an MCP client's own
|
|
12
|
+
* handshake patience, killing the process with zero stderr — indistinguishable
|
|
13
|
+
* from a crash.
|
|
14
|
+
*
|
|
15
|
+
* Fix: a single cached, timeout-bounded getContextForToolFiltering() shared
|
|
16
|
+
* by all three call sites.
|
|
17
|
+
*/
|
|
18
|
+
function makeServer() {
|
|
19
|
+
const server = new RespiraWordPressServer([
|
|
20
|
+
{ id: 'test-site', name: 'Test Site', url: 'https://example.test', apiKey: 'test-key', default: true },
|
|
21
|
+
]);
|
|
22
|
+
return server;
|
|
23
|
+
}
|
|
24
|
+
test('WooCommerce and ACF availability checks share ONE getSiteContext() call, not two', async () => {
|
|
25
|
+
const server = makeServer();
|
|
26
|
+
let callCount = 0;
|
|
27
|
+
server.currentSite.getSiteContext = async () => {
|
|
28
|
+
callCount++;
|
|
29
|
+
return { addons: { woocommerce: { installed: true, licensed: true }, acf: { installed: true } } };
|
|
30
|
+
};
|
|
31
|
+
const [woo, acf] = await Promise.all([
|
|
32
|
+
server.isWooCommerceAddonAvailable(),
|
|
33
|
+
server.isAcfAvailable(),
|
|
34
|
+
]);
|
|
35
|
+
assert.equal(woo, true);
|
|
36
|
+
assert.equal(acf, true);
|
|
37
|
+
assert.equal(callCount, 1, 'expected exactly one underlying getSiteContext() call, got ' + callCount);
|
|
38
|
+
});
|
|
39
|
+
test('a slow getSiteContext() never blocks the caller past the timeout bound', async () => {
|
|
40
|
+
const server = makeServer();
|
|
41
|
+
server.currentSite.getSiteContext = () => new Promise((resolve) => setTimeout(() => resolve({ addons: {} }), 10_000)); // deliberately far past any reasonable bound
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
const result = await server.getContextForToolFiltering();
|
|
44
|
+
const elapsed = Date.now() - start;
|
|
45
|
+
assert.equal(result, null, 'a timed-out fetch should resolve to null (fail-open), not hang or throw');
|
|
46
|
+
assert.ok(elapsed < 3000, `expected the timeout bound to cap the wait well under 3s, took ${elapsed}ms`);
|
|
47
|
+
});
|
|
48
|
+
test('filterToolsByContext shows all tools when the site context fetch fails', async () => {
|
|
49
|
+
const server = makeServer();
|
|
50
|
+
server.currentSite.getSiteContext = async () => {
|
|
51
|
+
throw new Error('ECONNRESET');
|
|
52
|
+
};
|
|
53
|
+
const tools = [{ name: 'wordpress_list_pages' }, { name: 'woocommerce_list_products' }];
|
|
54
|
+
const filtered = await server.filterToolsByContext(tools);
|
|
55
|
+
assert.equal(filtered.length, 2, 'a failed context fetch should fail open and show everything, not drop tools');
|
|
56
|
+
});
|
|
57
|
+
test('a successful fetch is cached and reused across repeated tools/list-style calls', async () => {
|
|
58
|
+
const server = makeServer();
|
|
59
|
+
let callCount = 0;
|
|
60
|
+
server.currentSite.getSiteContext = async () => {
|
|
61
|
+
callCount++;
|
|
62
|
+
return { addons: { woocommerce: { installed: true, licensed: true } } };
|
|
63
|
+
};
|
|
64
|
+
await server.isWooCommerceAddonAvailable();
|
|
65
|
+
await server.isWooCommerceAddonAvailable();
|
|
66
|
+
await server.filterToolsByContext([]);
|
|
67
|
+
assert.equal(callCount, 1, 'a fresh cache hit should never re-fetch within the TTL window');
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=tool-filter-context-cache.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-filter-context-cache.test.js","sourceRoot":"","sources":["../../src/__tests__/tool-filter-context-cache.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-media-path-detection.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/upload-media-path-detection.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import { isLocalFilePath, resolveLocalFilePath } from '../wordpress-client.js';
|
|
8
|
+
// Ticket 5a19d816: on Windows, respira_upload_media(file="C:/Users/.../photo.jpg")
|
|
9
|
+
// fell through the local-file detection (which only matched '/', './', '../',
|
|
10
|
+
// '~') into the base64 branch. Buffer.from(nonBase64, 'base64') doesn't throw,
|
|
11
|
+
// it just silently decodes garbage bytes, so the upload "succeeded" with a
|
|
12
|
+
// corrupt ~80-byte file instead of reading the real one from disk.
|
|
13
|
+
test('isLocalFilePath: Windows drive-letter paths are detected as local files, not base64', () => {
|
|
14
|
+
assert.equal(isLocalFilePath('C:/Users/bob/photo.jpg'), true);
|
|
15
|
+
assert.equal(isLocalFilePath('C:\\Users\\bob\\photo.jpg'), true);
|
|
16
|
+
assert.equal(isLocalFilePath('D:\\Media\\file.png'), true);
|
|
17
|
+
assert.equal(isLocalFilePath('z:/lower/drive/letter.gif'), true);
|
|
18
|
+
});
|
|
19
|
+
test('isLocalFilePath: file:// URIs are detected as local files (Windows and Unix forms)', () => {
|
|
20
|
+
assert.equal(isLocalFilePath('file:///C:/Users/bob/photo.jpg'), true);
|
|
21
|
+
assert.equal(isLocalFilePath('file:///home/bob/photo.jpg'), true);
|
|
22
|
+
});
|
|
23
|
+
test('isLocalFilePath: existing POSIX-style detection is unchanged (no regression)', () => {
|
|
24
|
+
assert.equal(isLocalFilePath('/absolute/unix/path.jpg'), true);
|
|
25
|
+
assert.equal(isLocalFilePath('./relative/path.jpg'), true);
|
|
26
|
+
assert.equal(isLocalFilePath('../parent/path.jpg'), true);
|
|
27
|
+
assert.equal(isLocalFilePath('~/home/path.jpg'), true);
|
|
28
|
+
});
|
|
29
|
+
test('isLocalFilePath: base64 payloads and data:/http(s): URLs are still NOT treated as local files', () => {
|
|
30
|
+
// A plausible base64 blob with no path-like prefix.
|
|
31
|
+
assert.equal(isLocalFilePath('aGVsbG8gd29ybGQgdGhpcyBpcyBhIHRlc3Q='), false);
|
|
32
|
+
assert.equal(isLocalFilePath('data:image/png;base64,iVBORw0KGgo='), false);
|
|
33
|
+
assert.equal(isLocalFilePath('http://example.com/photo.jpg'), false);
|
|
34
|
+
assert.equal(isLocalFilePath('https://example.com/photo.jpg'), false);
|
|
35
|
+
});
|
|
36
|
+
test('resolveLocalFilePath: Windows drive-letter paths pass through unchanged (no cwd corruption)', () => {
|
|
37
|
+
// Regression guard: node:path's resolve() is POSIX on non-Windows hosts and
|
|
38
|
+
// does not recognize "C:\..." as absolute, so running it through resolve()
|
|
39
|
+
// would wrongly prefix the string with cwd. The fix must return the drive
|
|
40
|
+
// path verbatim so fs calls on an actual Windows host see the real path
|
|
41
|
+
// (and, off-Windows, this test suite sees a clean "File not found: C:\..."
|
|
42
|
+
// instead of a mangled "<cwd>/C:\...").
|
|
43
|
+
assert.equal(resolveLocalFilePath('C:\\Users\\bob\\photo.jpg'), 'C:\\Users\\bob\\photo.jpg');
|
|
44
|
+
assert.equal(resolveLocalFilePath('C:/Users/bob/photo.jpg'), 'C:/Users/bob/photo.jpg');
|
|
45
|
+
});
|
|
46
|
+
test('resolveLocalFilePath: file:///C:/... extracts a Windows-style path', () => {
|
|
47
|
+
const resolved = resolveLocalFilePath('file:///C:/Users/bob/photo.jpg');
|
|
48
|
+
assert.equal(resolved, 'C:\\Users\\bob\\photo.jpg');
|
|
49
|
+
});
|
|
50
|
+
test('resolveLocalFilePath: file:///... on Unix round-trips to the real file, byte for byte', () => {
|
|
51
|
+
const dir = mkdtempSync(join(tmpdir(), 'respira-upload-media-test-'));
|
|
52
|
+
const filePath = join(dir, 'photo.jpg');
|
|
53
|
+
writeFileSync(filePath, 'not-actually-a-jpeg-but-that-is-fine');
|
|
54
|
+
try {
|
|
55
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
56
|
+
assert.equal(isLocalFilePath(fileUrl), true);
|
|
57
|
+
const resolved = resolveLocalFilePath(fileUrl);
|
|
58
|
+
assert.equal(resolved, filePath);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
rmSync(dir, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
test('resolveLocalFilePath: existing "~" and relative-path resolution behavior is unchanged', () => {
|
|
65
|
+
const home = process.env.HOME || '';
|
|
66
|
+
assert.equal(resolveLocalFilePath('~/photo.jpg'), `${home}/photo.jpg`);
|
|
67
|
+
// A relative path resolves against cwd, same as plain node:path resolve().
|
|
68
|
+
assert.equal(resolveLocalFilePath('./photo.jpg'), join(process.cwd(), 'photo.jpg'));
|
|
69
|
+
});
|
|
70
|
+
//# sourceMappingURL=upload-media-path-detection.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-media-path-detection.test.js","sourceRoot":"","sources":["../../src/__tests__/upload-media-path-detection.test.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -170,9 +170,38 @@ export declare class RespiraWordPressServer {
|
|
|
170
170
|
* Widget shortcuts are filtered to only the detected builder's element types.
|
|
171
171
|
* Core tools (site context, pages, posts, media, menus, etc.) always remain.
|
|
172
172
|
*/
|
|
173
|
-
/** Cached site context
|
|
173
|
+
/** Cached site context, shared by tool filtering and the WooCommerce/ACF
|
|
174
|
+
* availability checks below — all three used to independently call
|
|
175
|
+
* getSiteContext() (getCompactSiteContext() itself just wraps it), meaning
|
|
176
|
+
* every getTools() invocation made up to three uncached, sequential live
|
|
177
|
+
* HTTP calls to the customer's own WordPress site before it could return
|
|
178
|
+
* the tool catalog. Each call carries the wordpress-client retry backoff
|
|
179
|
+
* (200/600/1800ms across 3 attempts), so a slow or failing site could push
|
|
180
|
+
* a single tools/list response past 7s — long enough for an MCP client's
|
|
181
|
+
* own handshake patience to give up and tear down the process, which
|
|
182
|
+
* looks identical to a crash with zero stderr output (nathankohut.co.uk,
|
|
183
|
+
* Windows, 2026-07-19). getContextForToolFiltering() below is the only
|
|
184
|
+
* caller of currentSite.getSiteContext() in this class; everything that
|
|
185
|
+
* used to call it directly or via getCompactSiteContext() now shares this
|
|
186
|
+
* one cached, timeout-bounded fetch instead. */
|
|
174
187
|
private cachedFilterContext;
|
|
175
188
|
private static readonly FILTER_CACHE_TTL;
|
|
189
|
+
private static readonly FILTER_CACHE_FAILURE_TTL;
|
|
190
|
+
private static readonly FILTER_CONTEXT_TIMEOUT_MS;
|
|
191
|
+
/** Dedupes concurrent callers (e.g. isWooCommerceAddonAvailable() and
|
|
192
|
+
* isAcfAvailable() both missing a cold cache at once) onto one in-flight
|
|
193
|
+
* fetch instead of each starting their own. */
|
|
194
|
+
private filterContextFetchInFlight;
|
|
195
|
+
/**
|
|
196
|
+
* Fetch (or reuse a cached / in-flight) site context for tool-catalog
|
|
197
|
+
* decisions. Bounded to FILTER_CONTEXT_TIMEOUT_MS regardless of how long
|
|
198
|
+
* the underlying client's own retry backoff would otherwise take, so a
|
|
199
|
+
* slow or unreachable site can never block tools/list indefinitely.
|
|
200
|
+
* Resolves to null (and caches the miss briefly) on timeout or any fetch
|
|
201
|
+
* error — callers treat null as "show everything / skip context-dependent
|
|
202
|
+
* gating" rather than failing the whole tool list.
|
|
203
|
+
*/
|
|
204
|
+
private getContextForToolFiltering;
|
|
176
205
|
private filterToolsByContext;
|
|
177
206
|
private isWooCommerceAddonAvailable;
|
|
178
207
|
/**
|
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;AAqBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAsQzE,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,iFAAiF;IACjF,OAAO,CAAC,iBAAiB,CAAK;IAC9B,iFAAiF;IACjF,OAAO,CAAC,mBAAmB,CAAgC;IAE3D,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;IAiVvE,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;IA2CxB;;;;;;;;;;;;;;OAcG;YACW,WAAW;IAiJzB;;;;;;;;;OASG;YACW,kBAAkB;IAiHhC;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;YACW,qBAAqB;YAiBrB,kBAAkB;IA6FhC,yFAAyF;IACzF,OAAO,CAAC,gBAAgB;IASxB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,6BAA6B;IA0BrC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA+BjC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YAmQP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IA+vFtB;;;;;;OAMG;IACH
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAqBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAsQzE,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,iFAAiF;IACjF,OAAO,CAAC,iBAAiB,CAAK;IAC9B,iFAAiF;IACjF,OAAO,CAAC,mBAAmB,CAAgC;IAE3D,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;IAiVvE,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;IA2CxB;;;;;;;;;;;;;;OAcG;YACW,WAAW;IAiJzB;;;;;;;;;OASG;YACW,kBAAkB;IAiHhC;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;YACW,qBAAqB;YAiBrB,kBAAkB;IA6FhC,yFAAyF;IACzF,OAAO,CAAC,gBAAgB;IASxB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,6BAA6B;IA0BrC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA+BjC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YAmQP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IA+vFtB;;;;;;OAMG;IACH;;;;;;;;;;;;;oDAagD;IAChD,OAAO,CAAC,mBAAmB,CAAoD;IAC/E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAClD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAAU;IAC1D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAS;IAC1D;;mDAE+C;IAC/C,OAAO,CAAC,0BAA0B,CAAoC;IAEtE;;;;;;;;OAQG;YACW,0BAA0B;YAyC1B,oBAAoB;YA6CpB,2BAA2B;IAQzC;;;;OAIG;YACW,cAAc;IAQ5B,OAAO,CAAC,mBAAmB;IA2uC3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAe3C;IAEF;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;YAmCpB,cAAc;IA4G5B,oEAAoE;IACpE,OAAO,CAAC,iBAAiB;IAoBzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,eAAe;YAuCT,gBAAgB;IAimC9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAwUzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA6UxB,GAAG;CAyCV"}
|
package/dist/server.js
CHANGED
|
@@ -4380,26 +4380,80 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
4380
4380
|
* Widget shortcuts are filtered to only the detected builder's element types.
|
|
4381
4381
|
* Core tools (site context, pages, posts, media, menus, etc.) always remain.
|
|
4382
4382
|
*/
|
|
4383
|
-
/** Cached site context
|
|
4383
|
+
/** Cached site context, shared by tool filtering and the WooCommerce/ACF
|
|
4384
|
+
* availability checks below — all three used to independently call
|
|
4385
|
+
* getSiteContext() (getCompactSiteContext() itself just wraps it), meaning
|
|
4386
|
+
* every getTools() invocation made up to three uncached, sequential live
|
|
4387
|
+
* HTTP calls to the customer's own WordPress site before it could return
|
|
4388
|
+
* the tool catalog. Each call carries the wordpress-client retry backoff
|
|
4389
|
+
* (200/600/1800ms across 3 attempts), so a slow or failing site could push
|
|
4390
|
+
* a single tools/list response past 7s — long enough for an MCP client's
|
|
4391
|
+
* own handshake patience to give up and tear down the process, which
|
|
4392
|
+
* looks identical to a crash with zero stderr output (nathankohut.co.uk,
|
|
4393
|
+
* Windows, 2026-07-19). getContextForToolFiltering() below is the only
|
|
4394
|
+
* caller of currentSite.getSiteContext() in this class; everything that
|
|
4395
|
+
* used to call it directly or via getCompactSiteContext() now shares this
|
|
4396
|
+
* one cached, timeout-bounded fetch instead. */
|
|
4384
4397
|
cachedFilterContext = null;
|
|
4385
|
-
static FILTER_CACHE_TTL = 60_000; // 1 minute
|
|
4386
|
-
|
|
4398
|
+
static FILTER_CACHE_TTL = 60_000; // successful fetch: 1 minute
|
|
4399
|
+
static FILTER_CACHE_FAILURE_TTL = 15_000; // failed fetch: retry sooner
|
|
4400
|
+
static FILTER_CONTEXT_TIMEOUT_MS = 2_000;
|
|
4401
|
+
/** Dedupes concurrent callers (e.g. isWooCommerceAddonAvailable() and
|
|
4402
|
+
* isAcfAvailable() both missing a cold cache at once) onto one in-flight
|
|
4403
|
+
* fetch instead of each starting their own. */
|
|
4404
|
+
filterContextFetchInFlight = null;
|
|
4405
|
+
/**
|
|
4406
|
+
* Fetch (or reuse a cached / in-flight) site context for tool-catalog
|
|
4407
|
+
* decisions. Bounded to FILTER_CONTEXT_TIMEOUT_MS regardless of how long
|
|
4408
|
+
* the underlying client's own retry backoff would otherwise take, so a
|
|
4409
|
+
* slow or unreachable site can never block tools/list indefinitely.
|
|
4410
|
+
* Resolves to null (and caches the miss briefly) on timeout or any fetch
|
|
4411
|
+
* error — callers treat null as "show everything / skip context-dependent
|
|
4412
|
+
* gating" rather than failing the whole tool list.
|
|
4413
|
+
*/
|
|
4414
|
+
async getContextForToolFiltering() {
|
|
4387
4415
|
if (!this.currentSite) {
|
|
4388
|
-
return
|
|
4416
|
+
return null;
|
|
4389
4417
|
}
|
|
4390
|
-
let context;
|
|
4391
4418
|
const now = Date.now();
|
|
4392
|
-
if (this.cachedFilterContext
|
|
4393
|
-
|
|
4419
|
+
if (this.cachedFilterContext) {
|
|
4420
|
+
const ttl = this.cachedFilterContext.context
|
|
4421
|
+
? RespiraWordPressServer.FILTER_CACHE_TTL
|
|
4422
|
+
: RespiraWordPressServer.FILTER_CACHE_FAILURE_TTL;
|
|
4423
|
+
if (now - this.cachedFilterContext.timestamp < ttl) {
|
|
4424
|
+
return this.cachedFilterContext.context;
|
|
4425
|
+
}
|
|
4394
4426
|
}
|
|
4395
|
-
|
|
4427
|
+
if (this.filterContextFetchInFlight) {
|
|
4428
|
+
return this.filterContextFetchInFlight;
|
|
4429
|
+
}
|
|
4430
|
+
const fetchPromise = (async () => {
|
|
4431
|
+
let context = null;
|
|
4396
4432
|
try {
|
|
4397
|
-
|
|
4398
|
-
|
|
4433
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('site context fetch timed out')), RespiraWordPressServer.FILTER_CONTEXT_TIMEOUT_MS));
|
|
4434
|
+
context = await Promise.race([this.currentSite.getSiteContext(), timeout]);
|
|
4399
4435
|
}
|
|
4400
4436
|
catch {
|
|
4401
|
-
|
|
4437
|
+
context = null;
|
|
4402
4438
|
}
|
|
4439
|
+
this.cachedFilterContext = { context, timestamp: now };
|
|
4440
|
+
return context;
|
|
4441
|
+
})();
|
|
4442
|
+
this.filterContextFetchInFlight = fetchPromise;
|
|
4443
|
+
try {
|
|
4444
|
+
return await fetchPromise;
|
|
4445
|
+
}
|
|
4446
|
+
finally {
|
|
4447
|
+
this.filterContextFetchInFlight = null;
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
async filterToolsByContext(tools) {
|
|
4451
|
+
if (!this.currentSite) {
|
|
4452
|
+
return tools; // No site connected — show everything.
|
|
4453
|
+
}
|
|
4454
|
+
const context = await this.getContextForToolFiltering();
|
|
4455
|
+
if (!context) {
|
|
4456
|
+
return tools; // Can't fetch context — show everything.
|
|
4403
4457
|
}
|
|
4404
4458
|
const detectedBuilder = (context?.page_builder?.name || '').toLowerCase();
|
|
4405
4459
|
const hasWooCommerce = Boolean(context?.woocommerce?.active || context?.addons?.woocommerce?.installed);
|
|
@@ -4430,16 +4484,11 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
4430
4484
|
});
|
|
4431
4485
|
}
|
|
4432
4486
|
async isWooCommerceAddonAvailable() {
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
}
|
|
4436
|
-
try {
|
|
4437
|
-
const context = await this.currentSite.getSiteContext();
|
|
4438
|
-
return Boolean(context.addons?.woocommerce?.installed && context.addons?.woocommerce?.licensed);
|
|
4439
|
-
}
|
|
4440
|
-
catch {
|
|
4487
|
+
const context = await this.getContextForToolFiltering();
|
|
4488
|
+
if (!context) {
|
|
4441
4489
|
return false;
|
|
4442
4490
|
}
|
|
4491
|
+
return Boolean(context.addons?.woocommerce?.installed && context.addons?.woocommerce?.licensed);
|
|
4443
4492
|
}
|
|
4444
4493
|
/**
|
|
4445
4494
|
* Detect whether ACF is active on the currently selected site.
|
|
@@ -4447,16 +4496,11 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
4447
4496
|
* (a Pro tool on a non-Pro site returns PRO_FEATURE_REQUIRED).
|
|
4448
4497
|
*/
|
|
4449
4498
|
async isAcfAvailable() {
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
}
|
|
4453
|
-
try {
|
|
4454
|
-
const context = await this.currentSite.getSiteContext();
|
|
4455
|
-
return Boolean(context.addons?.acf?.installed);
|
|
4456
|
-
}
|
|
4457
|
-
catch {
|
|
4499
|
+
const context = await this.getContextForToolFiltering();
|
|
4500
|
+
if (!context) {
|
|
4458
4501
|
return false;
|
|
4459
4502
|
}
|
|
4503
|
+
return Boolean(context.addons?.acf?.installed);
|
|
4460
4504
|
}
|
|
4461
4505
|
getWooCommerceTools() {
|
|
4462
4506
|
return [
|
|
@@ -6401,7 +6445,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
6401
6445
|
case 'wordpress_update_media_batch':
|
|
6402
6446
|
return await client.updateMediaBatch(args.items);
|
|
6403
6447
|
case 'wordpress_delete_media':
|
|
6404
|
-
return await client.deleteMedia(args.id);
|
|
6448
|
+
return await client.deleteMedia(args.id, args.force, args.approval_token);
|
|
6405
6449
|
// Menu Management
|
|
6406
6450
|
case 'wordpress_list_menus':
|
|
6407
6451
|
return await client.listMenus();
|