@adsim/wordpress-mcp-server 3.1.0 → 4.5.0
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/README.md +564 -176
- package/dxt/manifest.json +93 -9
- package/index.js +3624 -36
- package/package.json +1 -1
- package/src/confirmationToken.js +64 -0
- package/src/contentAnalyzer.js +476 -0
- package/src/htmlParser.js +80 -0
- package/src/linkUtils.js +158 -0
- package/src/pluginDetector.js +158 -0
- package/src/utils/contentCompressor.js +116 -0
- package/src/woocommerceClient.js +88 -0
- package/tests/unit/contentAnalyzer.test.js +397 -0
- package/tests/unit/pluginDetector.test.js +167 -0
- package/tests/unit/tools/analyzeEeatSignals.test.js +192 -0
- package/tests/unit/tools/approval.test.js +251 -0
- package/tests/unit/tools/auditCanonicals.test.js +149 -0
- package/tests/unit/tools/auditHeadingStructure.test.js +150 -0
- package/tests/unit/tools/auditMediaSeo.test.js +123 -0
- package/tests/unit/tools/auditOutboundLinks.test.js +175 -0
- package/tests/unit/tools/auditTaxonomies.test.js +173 -0
- package/tests/unit/tools/contentCompressor.test.js +320 -0
- package/tests/unit/tools/contentIntelligence.test.js +2168 -0
- package/tests/unit/tools/destructive.test.js +246 -0
- package/tests/unit/tools/findBrokenInternalLinks.test.js +222 -0
- package/tests/unit/tools/findKeywordCannibalization.test.js +183 -0
- package/tests/unit/tools/findOrphanPages.test.js +145 -0
- package/tests/unit/tools/findThinContent.test.js +145 -0
- package/tests/unit/tools/internalLinks.test.js +283 -0
- package/tests/unit/tools/perTargetControls.test.js +228 -0
- package/tests/unit/tools/pluginIntelligence.test.js +864 -0
- package/tests/unit/tools/site.test.js +6 -1
- package/tests/unit/tools/woocommerce.test.js +344 -0
- package/tests/unit/tools/woocommerceIntelligence.test.js +341 -0
- package/tests/unit/tools/woocommerceWrite.test.js +323 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('node-fetch', () => ({ default: vi.fn() }));
|
|
4
|
+
|
|
5
|
+
import fetch from 'node-fetch';
|
|
6
|
+
import { handleToolCall, getActiveControls, getControlSources, _testSetTarget } from '../../../index.js';
|
|
7
|
+
import { mockSuccess, getAuditLogs, makeRequest, parseResult } from '../../helpers/mockWpRequest.js';
|
|
8
|
+
|
|
9
|
+
function call(name, args = {}) {
|
|
10
|
+
return handleToolCall(makeRequest(name, args));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const ENV_KEYS = ['WP_READ_ONLY', 'WP_DRAFT_ONLY', 'WP_DISABLE_DELETE', 'WP_DISABLE_PLUGIN_MANAGEMENT', 'WP_REQUIRE_APPROVAL', 'WP_CONFIRM_DESTRUCTIVE', 'WP_MAX_CALLS_PER_MINUTE'];
|
|
14
|
+
const savedEnv = {};
|
|
15
|
+
|
|
16
|
+
function clearGovEnv() {
|
|
17
|
+
for (const k of ENV_KEYS) {
|
|
18
|
+
savedEnv[k] = process.env[k];
|
|
19
|
+
delete process.env[k];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function restoreGovEnv() {
|
|
24
|
+
for (const k of ENV_KEYS) {
|
|
25
|
+
if (savedEnv[k] === undefined) delete process.env[k];
|
|
26
|
+
else process.env[k] = savedEnv[k];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ────────────────────────────────────────────────────────────
|
|
31
|
+
// getActiveControls — unit tests
|
|
32
|
+
// ────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
describe('getActiveControls', () => {
|
|
35
|
+
let consoleSpy;
|
|
36
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); clearGovEnv(); });
|
|
37
|
+
afterEach(() => { consoleSpy.mockRestore(); restoreGovEnv(); _testSetTarget(null); });
|
|
38
|
+
|
|
39
|
+
it('returns global env vars when target has no controls', () => {
|
|
40
|
+
process.env.WP_READ_ONLY = 'true';
|
|
41
|
+
process.env.WP_DISABLE_DELETE = 'true';
|
|
42
|
+
_testSetTarget('prod', { url: 'https://prod.example.com', username: 'u', password: 'p' });
|
|
43
|
+
|
|
44
|
+
const c = getActiveControls();
|
|
45
|
+
|
|
46
|
+
expect(c.read_only).toBe(true);
|
|
47
|
+
expect(c.disable_delete).toBe(true);
|
|
48
|
+
expect(c.draft_only).toBe(false);
|
|
49
|
+
expect(c.require_approval).toBe(false);
|
|
50
|
+
expect(c.confirm_destructive).toBe(false);
|
|
51
|
+
expect(c.disable_plugin_management).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('target read_only:true + global false → blocked (OR strict)', () => {
|
|
55
|
+
_testSetTarget('prod', { url: 'https://prod.example.com', username: 'u', password: 'p', controls: { read_only: true } });
|
|
56
|
+
|
|
57
|
+
const c = getActiveControls();
|
|
58
|
+
|
|
59
|
+
expect(c.read_only).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('global read_only:true + target false → blocked (OR strict)', () => {
|
|
63
|
+
process.env.WP_READ_ONLY = 'true';
|
|
64
|
+
_testSetTarget('prod', { url: 'https://prod.example.com', username: 'u', password: 'p', controls: { read_only: false } });
|
|
65
|
+
|
|
66
|
+
const c = getActiveControls();
|
|
67
|
+
|
|
68
|
+
expect(c.read_only).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('both global and target true → blocked (OR strict)', () => {
|
|
72
|
+
process.env.WP_READ_ONLY = 'true';
|
|
73
|
+
_testSetTarget('prod', { url: 'https://prod.example.com', username: 'u', password: 'p', controls: { read_only: true } });
|
|
74
|
+
|
|
75
|
+
const c = getActiveControls();
|
|
76
|
+
const s = getControlSources();
|
|
77
|
+
|
|
78
|
+
expect(c.read_only).toBe(true);
|
|
79
|
+
expect(s.read_only_source).toBe('both');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('max_calls_per_minute: MIN of global and target', () => {
|
|
83
|
+
process.env.WP_MAX_CALLS_PER_MINUTE = '30';
|
|
84
|
+
_testSetTarget('prod', { url: 'https://prod.example.com', username: 'u', password: 'p', controls: { max_calls_per_minute: 10 } });
|
|
85
|
+
|
|
86
|
+
const c = getActiveControls();
|
|
87
|
+
|
|
88
|
+
expect(c.max_calls_per_minute).toBe(10);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('max_calls_per_minute: global unlimited + target=20 → 20', () => {
|
|
92
|
+
// global is 0 (unlimited) by default
|
|
93
|
+
_testSetTarget('prod', { url: 'https://prod.example.com', username: 'u', password: 'p', controls: { max_calls_per_minute: 20 } });
|
|
94
|
+
|
|
95
|
+
const c = getActiveControls();
|
|
96
|
+
const s = getControlSources();
|
|
97
|
+
|
|
98
|
+
expect(c.max_calls_per_minute).toBe(20);
|
|
99
|
+
expect(s.max_calls_per_minute_source).toBe('target');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ────────────────────────────────────────────────────────────
|
|
104
|
+
// Governance functions use getActiveControls — integration
|
|
105
|
+
// ────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
describe('Per-target governance integration', () => {
|
|
108
|
+
let consoleSpy;
|
|
109
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); clearGovEnv(); });
|
|
110
|
+
afterEach(() => { consoleSpy.mockRestore(); restoreGovEnv(); _testSetTarget(null); });
|
|
111
|
+
|
|
112
|
+
it('target read_only blocks write tools via enforceReadOnly', async () => {
|
|
113
|
+
_testSetTarget('prod', { url: 'https://prod.example.com', username: 'u', password: 'p', controls: { read_only: true } });
|
|
114
|
+
|
|
115
|
+
const result = await call('wp_create_post', { title: 'T', content: 'C' });
|
|
116
|
+
|
|
117
|
+
expect(result.isError).toBe(true);
|
|
118
|
+
expect(result.content[0].text).toContain('READ-ONLY');
|
|
119
|
+
|
|
120
|
+
const logs = getAuditLogs(consoleSpy);
|
|
121
|
+
const entry = logs.find(l => l.tool === 'wp_create_post');
|
|
122
|
+
expect(entry).toBeDefined();
|
|
123
|
+
expect(entry.status).toBe('blocked');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('target draft_only blocks publish via enforceDraftOnly', async () => {
|
|
127
|
+
_testSetTarget('staging', { url: 'https://staging.example.com', username: 'u', password: 'p', controls: { draft_only: true } });
|
|
128
|
+
|
|
129
|
+
const result = await call('wp_create_post', { title: 'T', content: 'C', status: 'publish' });
|
|
130
|
+
|
|
131
|
+
expect(result.isError).toBe(true);
|
|
132
|
+
expect(result.content[0].text).toContain('DRAFT-ONLY');
|
|
133
|
+
|
|
134
|
+
const logs = getAuditLogs(consoleSpy);
|
|
135
|
+
const entry = logs.find(l => l.tool === 'wp_create_post');
|
|
136
|
+
expect(entry).toBeDefined();
|
|
137
|
+
expect(entry.status).toBe('error');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ────────────────────────────────────────────────────────────
|
|
142
|
+
// wp_site_info — per-target controls
|
|
143
|
+
// ────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
describe('wp_site_info with per-target controls', () => {
|
|
146
|
+
let consoleSpy;
|
|
147
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); clearGovEnv(); });
|
|
148
|
+
afterEach(() => { consoleSpy.mockRestore(); restoreGovEnv(); _testSetTarget(null); });
|
|
149
|
+
|
|
150
|
+
const siteInfoResponse = { name: 'Test', description: 'T', url: 'https://test.example.com', gmt_offset: 1, timezone_string: 'UTC' };
|
|
151
|
+
const userMeResponse = { id: 1, name: 'Admin', slug: 'admin', roles: ['administrator'] };
|
|
152
|
+
const postTypesResponse = { post: { slug: 'post', name: 'Posts', rest_base: 'posts' } };
|
|
153
|
+
|
|
154
|
+
it('enterprise_controls reflects target controls', async () => {
|
|
155
|
+
_testSetTarget('prod', { url: 'https://test.example.com', username: 'testuser', password: 'testpass', controls: { disable_delete: true, confirm_destructive: true } });
|
|
156
|
+
mockSuccess(siteInfoResponse);
|
|
157
|
+
mockSuccess(userMeResponse);
|
|
158
|
+
mockSuccess(postTypesResponse);
|
|
159
|
+
|
|
160
|
+
const res = await call('wp_site_info');
|
|
161
|
+
const data = parseResult(res);
|
|
162
|
+
|
|
163
|
+
expect(data.enterprise_controls.delete_disabled).toBe(true);
|
|
164
|
+
expect(data.enterprise_controls.confirm_destructive).toBe(true);
|
|
165
|
+
expect(data.enterprise_controls.read_only).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('controls_source indicates "target" when restriction comes from target', async () => {
|
|
169
|
+
_testSetTarget('prod', { url: 'https://test.example.com', username: 'testuser', password: 'testpass', controls: { disable_delete: true } });
|
|
170
|
+
mockSuccess(siteInfoResponse);
|
|
171
|
+
mockSuccess(userMeResponse);
|
|
172
|
+
mockSuccess(postTypesResponse);
|
|
173
|
+
|
|
174
|
+
const res = await call('wp_site_info');
|
|
175
|
+
const data = parseResult(res);
|
|
176
|
+
|
|
177
|
+
expect(data.enterprise_controls.disable_delete_source).toBe('target');
|
|
178
|
+
expect(data.enterprise_controls.read_only_source).toBe('none');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ────────────────────────────────────────────────────────────
|
|
183
|
+
// wp_set_target — audit log and controls persistence
|
|
184
|
+
// ────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
describe('wp_set_target with per-target controls', () => {
|
|
187
|
+
let consoleSpy;
|
|
188
|
+
beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); clearGovEnv(); });
|
|
189
|
+
afterEach(() => { consoleSpy.mockRestore(); restoreGovEnv(); _testSetTarget(null); });
|
|
190
|
+
|
|
191
|
+
it('audit log contains effective_controls after switch', async () => {
|
|
192
|
+
// Pre-populate two targets via _testSetTarget for the first, then manually set second
|
|
193
|
+
_testSetTarget('staging', { url: 'https://staging.example.com', username: 'u', password: 'p', controls: { draft_only: true, disable_delete: true } });
|
|
194
|
+
|
|
195
|
+
// Now call wp_set_target to switch to staging (already current, but validates the flow)
|
|
196
|
+
const result = await call('wp_set_target', { site: 'staging' });
|
|
197
|
+
const data = parseResult(result);
|
|
198
|
+
|
|
199
|
+
expect(data.success).toBe(true);
|
|
200
|
+
expect(data.effective_controls).toBeDefined();
|
|
201
|
+
expect(data.effective_controls.draft_only).toBe(true);
|
|
202
|
+
expect(data.effective_controls.disable_delete).toBe(true);
|
|
203
|
+
expect(data.effective_controls.read_only).toBe(false);
|
|
204
|
+
|
|
205
|
+
const logs = getAuditLogs(consoleSpy);
|
|
206
|
+
const entry = logs.find(l => l.tool === 'wp_set_target' && l.status === 'success');
|
|
207
|
+
expect(entry).toBeDefined();
|
|
208
|
+
expect(entry.effective_controls).toBeDefined();
|
|
209
|
+
expect(entry.effective_controls.draft_only).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('per-target controls survive a wp_set_target switch', async () => {
|
|
213
|
+
// Set up two targets
|
|
214
|
+
_testSetTarget('dev', { url: 'https://dev.example.com', username: 'u', password: 'p' });
|
|
215
|
+
_testSetTarget('prod', { url: 'https://prod.example.com', username: 'u', password: 'p', controls: { read_only: true } });
|
|
216
|
+
// Current is now 'prod'. Switch to prod explicitly to test persistence.
|
|
217
|
+
|
|
218
|
+
const result = await call('wp_set_target', { site: 'prod' });
|
|
219
|
+
const data = parseResult(result);
|
|
220
|
+
|
|
221
|
+
expect(data.effective_controls.read_only).toBe(true);
|
|
222
|
+
|
|
223
|
+
// Verify the control actually blocks writes
|
|
224
|
+
const writeResult = await call('wp_create_post', { title: 'T', content: 'C' });
|
|
225
|
+
expect(writeResult.isError).toBe(true);
|
|
226
|
+
expect(writeResult.content[0].text).toContain('READ-ONLY');
|
|
227
|
+
});
|
|
228
|
+
});
|