@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
|
@@ -72,10 +72,15 @@ describe('wp_site_info', () => {
|
|
|
72
72
|
expect(typeof data.enterprise_controls.draft_only).toBe('boolean');
|
|
73
73
|
expect(typeof data.enterprise_controls.delete_disabled).toBe('boolean');
|
|
74
74
|
expect(typeof data.enterprise_controls.plugin_management_disabled).toBe('boolean');
|
|
75
|
+
expect(typeof data.enterprise_controls.confirm_destructive).toBe('boolean');
|
|
76
|
+
|
|
77
|
+
// Control sources
|
|
78
|
+
expect(data.enterprise_controls.read_only_source).toBeDefined();
|
|
79
|
+
expect(['global', 'target', 'both', 'none']).toContain(data.enterprise_controls.read_only_source);
|
|
75
80
|
|
|
76
81
|
// Server info
|
|
77
82
|
expect(data.server.mcp_version).toBeDefined();
|
|
78
|
-
expect(data.server.tools_count).toBe(
|
|
83
|
+
expect(data.server.tools_count).toBe(85);
|
|
79
84
|
});
|
|
80
85
|
|
|
81
86
|
it('SUCCESS — enterprise_controls reflect WP_READ_ONLY env var', async () => {
|
|
@@ -0,0 +1,344 @@
|
|
|
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, _testSetTarget } from '../../../index.js';
|
|
7
|
+
import { mockSuccess, mockError, getAuditLogs, makeRequest, parseResult } from '../../helpers/mockWpRequest.js';
|
|
8
|
+
|
|
9
|
+
function call(name, args = {}) {
|
|
10
|
+
return handleToolCall(makeRequest(name, args));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let consoleSpy;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
fetch.mockReset();
|
|
16
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
17
|
+
process.env.WC_CONSUMER_KEY = 'ck_test';
|
|
18
|
+
process.env.WC_CONSUMER_SECRET = 'cs_test';
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
consoleSpy.mockRestore();
|
|
22
|
+
_testSetTarget(null);
|
|
23
|
+
delete process.env.WC_CONSUMER_KEY;
|
|
24
|
+
delete process.env.WC_CONSUMER_SECRET;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// =========================================================================
|
|
28
|
+
// Mock data
|
|
29
|
+
// =========================================================================
|
|
30
|
+
|
|
31
|
+
const product1 = {
|
|
32
|
+
id: 101, name: 'T-Shirt', slug: 't-shirt', status: 'publish',
|
|
33
|
+
price: '19.99', regular_price: '24.99', sale_price: '19.99',
|
|
34
|
+
stock_status: 'instock', stock_quantity: 50,
|
|
35
|
+
categories: [{ id: 5, name: 'Clothing' }],
|
|
36
|
+
images: [{ src: 'https://shop.example.com/img/tshirt.jpg' }],
|
|
37
|
+
permalink: 'https://shop.example.com/product/t-shirt',
|
|
38
|
+
type: 'simple', variations: []
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const product2 = {
|
|
42
|
+
id: 102, name: 'Hoodie', slug: 'hoodie', status: 'publish',
|
|
43
|
+
price: '49.99', regular_price: '49.99', sale_price: '',
|
|
44
|
+
stock_status: 'instock', stock_quantity: 20,
|
|
45
|
+
categories: [{ id: 5, name: 'Clothing' }],
|
|
46
|
+
images: [{ src: 'https://shop.example.com/img/hoodie.jpg' }],
|
|
47
|
+
permalink: 'https://shop.example.com/product/hoodie',
|
|
48
|
+
type: 'variable', variations: [201, 202]
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const order1 = {
|
|
52
|
+
id: 500, number: '500', status: 'processing',
|
|
53
|
+
date_created: '2026-02-01T10:00:00',
|
|
54
|
+
customer_id: 10,
|
|
55
|
+
billing: { first_name: 'John', last_name: 'Doe', email: 'john@example.com' },
|
|
56
|
+
total: '69.98', currency: 'EUR',
|
|
57
|
+
line_items: [{ name: 'T-Shirt', quantity: 2, total: '39.98' }, { name: 'Hoodie', quantity: 1, total: '49.99' }],
|
|
58
|
+
shipping: {}, payment_method: 'stripe', transaction_id: 'txn_123', meta_data: []
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const customer1 = {
|
|
62
|
+
id: 10, first_name: 'John', last_name: 'Doe', email: 'john@example.com',
|
|
63
|
+
date_created: '2025-06-15T08:00:00', orders_count: 5, total_spent: '350.00', username: 'johndoe'
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// =========================================================================
|
|
67
|
+
// wc_list_products
|
|
68
|
+
// =========================================================================
|
|
69
|
+
|
|
70
|
+
describe('wc_list_products', () => {
|
|
71
|
+
it('SUCCESS — returns product list', async () => {
|
|
72
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
73
|
+
mockSuccess([product1, product2]);
|
|
74
|
+
|
|
75
|
+
const res = await call('wc_list_products');
|
|
76
|
+
const data = parseResult(res);
|
|
77
|
+
|
|
78
|
+
expect(data.total).toBe(2);
|
|
79
|
+
expect(data.products[0].id).toBe(101);
|
|
80
|
+
expect(data.products[0].name).toBe('T-Shirt');
|
|
81
|
+
expect(data.products[0].price).toBe('19.99');
|
|
82
|
+
expect(data.products[0].categories[0].name).toBe('Clothing');
|
|
83
|
+
expect(data.products[0].image).toBe('https://shop.example.com/img/tshirt.jpg');
|
|
84
|
+
|
|
85
|
+
const logs = getAuditLogs();
|
|
86
|
+
const entry = logs.find(l => l.tool === 'wc_list_products');
|
|
87
|
+
expect(entry).toBeDefined();
|
|
88
|
+
expect(entry.status).toBe('success');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('BLOCKED — WP_READ_ONLY prevents access', async () => {
|
|
92
|
+
const original = process.env.WP_READ_ONLY;
|
|
93
|
+
process.env.WP_READ_ONLY = 'true';
|
|
94
|
+
try {
|
|
95
|
+
const res = await call('wc_list_products');
|
|
96
|
+
expect(res.isError).toBe(true);
|
|
97
|
+
expect(res.content[0].text).toContain('READ-ONLY');
|
|
98
|
+
} finally {
|
|
99
|
+
if (original === undefined) delete process.env.WP_READ_ONLY;
|
|
100
|
+
else process.env.WP_READ_ONLY = original;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('ERROR — WC credentials missing', async () => {
|
|
105
|
+
delete process.env.WC_CONSUMER_KEY;
|
|
106
|
+
delete process.env.WC_CONSUMER_SECRET;
|
|
107
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
108
|
+
|
|
109
|
+
const res = await call('wc_list_products');
|
|
110
|
+
expect(res.isError).toBe(true);
|
|
111
|
+
expect(res.content[0].text).toContain('WC_CONSUMER_KEY');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// =========================================================================
|
|
116
|
+
// wc_get_product
|
|
117
|
+
// =========================================================================
|
|
118
|
+
|
|
119
|
+
describe('wc_get_product', () => {
|
|
120
|
+
it('SUCCESS — returns full product details', async () => {
|
|
121
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
122
|
+
mockSuccess(product1);
|
|
123
|
+
|
|
124
|
+
const res = await call('wc_get_product', { id: 101 });
|
|
125
|
+
const data = parseResult(res);
|
|
126
|
+
|
|
127
|
+
expect(data.id).toBe(101);
|
|
128
|
+
expect(data.name).toBe('T-Shirt');
|
|
129
|
+
expect(data.price).toBe('19.99');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('SUCCESS — variable product includes variations_detail', async () => {
|
|
133
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
134
|
+
mockSuccess(product2); // variable product with variations [201, 202]
|
|
135
|
+
mockSuccess([
|
|
136
|
+
{ id: 201, sku: 'hoodie-s', price: '49.99', regular_price: '49.99', sale_price: '', stock_status: 'instock', stock_quantity: 10, attributes: [{ name: 'Size', option: 'S' }] },
|
|
137
|
+
{ id: 202, sku: 'hoodie-l', price: '49.99', regular_price: '49.99', sale_price: '', stock_status: 'instock', stock_quantity: 10, attributes: [{ name: 'Size', option: 'L' }] }
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const res = await call('wc_get_product', { id: 102 });
|
|
141
|
+
const data = parseResult(res);
|
|
142
|
+
|
|
143
|
+
expect(data.id).toBe(102);
|
|
144
|
+
expect(data.type).toBe('variable');
|
|
145
|
+
expect(data.variations_detail).toHaveLength(2);
|
|
146
|
+
expect(data.variations_detail[0].sku).toBe('hoodie-s');
|
|
147
|
+
expect(data.variations_detail[1].attributes[0].option).toBe('L');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('ERROR — product not found (404)', async () => {
|
|
151
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
152
|
+
mockError(404, '{"code":"woocommerce_rest_product_invalid_id","message":"Invalid ID."}');
|
|
153
|
+
|
|
154
|
+
const res = await call('wc_get_product', { id: 999 });
|
|
155
|
+
expect(res.isError).toBe(true);
|
|
156
|
+
expect(res.content[0].text).toContain('404');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// =========================================================================
|
|
161
|
+
// wc_list_orders
|
|
162
|
+
// =========================================================================
|
|
163
|
+
|
|
164
|
+
describe('wc_list_orders', () => {
|
|
165
|
+
it('SUCCESS — returns order list', async () => {
|
|
166
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
167
|
+
mockSuccess([order1]);
|
|
168
|
+
|
|
169
|
+
const res = await call('wc_list_orders');
|
|
170
|
+
const data = parseResult(res);
|
|
171
|
+
|
|
172
|
+
expect(data.total).toBe(1);
|
|
173
|
+
expect(data.orders[0].id).toBe(500);
|
|
174
|
+
expect(data.orders[0].status).toBe('processing');
|
|
175
|
+
expect(data.orders[0].billing_name).toBe('John Doe');
|
|
176
|
+
expect(data.orders[0].billing_email).toBe('john@example.com');
|
|
177
|
+
expect(data.orders[0].total).toBe('69.98');
|
|
178
|
+
expect(data.orders[0].line_items).toHaveLength(2);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('BLOCKED — WP_READ_ONLY prevents access', async () => {
|
|
182
|
+
const original = process.env.WP_READ_ONLY;
|
|
183
|
+
process.env.WP_READ_ONLY = 'true';
|
|
184
|
+
try {
|
|
185
|
+
const res = await call('wc_list_orders');
|
|
186
|
+
expect(res.isError).toBe(true);
|
|
187
|
+
expect(res.content[0].text).toContain('READ-ONLY');
|
|
188
|
+
} finally {
|
|
189
|
+
if (original === undefined) delete process.env.WP_READ_ONLY;
|
|
190
|
+
else process.env.WP_READ_ONLY = original;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('SUCCESS — filter by status', async () => {
|
|
195
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
196
|
+
mockSuccess([order1]);
|
|
197
|
+
|
|
198
|
+
const res = await call('wc_list_orders', { status: 'processing' });
|
|
199
|
+
const data = parseResult(res);
|
|
200
|
+
expect(data.orders[0].status).toBe('processing');
|
|
201
|
+
|
|
202
|
+
// Verify the fetch URL contained the status param
|
|
203
|
+
const fetchUrl = fetch.mock.calls[0][0];
|
|
204
|
+
expect(fetchUrl).toContain('status=processing');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// =========================================================================
|
|
209
|
+
// wc_get_order
|
|
210
|
+
// =========================================================================
|
|
211
|
+
|
|
212
|
+
describe('wc_get_order', () => {
|
|
213
|
+
it('SUCCESS — returns full order details', async () => {
|
|
214
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
215
|
+
mockSuccess(order1);
|
|
216
|
+
|
|
217
|
+
const res = await call('wc_get_order', { id: 500 });
|
|
218
|
+
const data = parseResult(res);
|
|
219
|
+
|
|
220
|
+
expect(data.id).toBe(500);
|
|
221
|
+
expect(data.billing.first_name).toBe('John');
|
|
222
|
+
expect(data.payment_method).toBe('stripe');
|
|
223
|
+
expect(data.line_items).toHaveLength(2);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('ERROR — order not found (404)', async () => {
|
|
227
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
228
|
+
mockError(404, '{"code":"woocommerce_rest_shop_order_invalid_id","message":"Invalid ID."}');
|
|
229
|
+
|
|
230
|
+
const res = await call('wc_get_order', { id: 999 });
|
|
231
|
+
expect(res.isError).toBe(true);
|
|
232
|
+
expect(res.content[0].text).toContain('404');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// =========================================================================
|
|
237
|
+
// wc_list_customers
|
|
238
|
+
// =========================================================================
|
|
239
|
+
|
|
240
|
+
describe('wc_list_customers', () => {
|
|
241
|
+
it('SUCCESS — returns customer list', async () => {
|
|
242
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
243
|
+
mockSuccess([customer1]);
|
|
244
|
+
|
|
245
|
+
const res = await call('wc_list_customers');
|
|
246
|
+
const data = parseResult(res);
|
|
247
|
+
|
|
248
|
+
expect(data.total).toBe(1);
|
|
249
|
+
expect(data.customers[0].id).toBe(10);
|
|
250
|
+
expect(data.customers[0].first_name).toBe('John');
|
|
251
|
+
expect(data.customers[0].email).toBe('john@example.com');
|
|
252
|
+
expect(data.customers[0].orders_count).toBe(5);
|
|
253
|
+
expect(data.customers[0].total_spent).toBe('350.00');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('BLOCKED — WP_READ_ONLY prevents access', async () => {
|
|
257
|
+
const original = process.env.WP_READ_ONLY;
|
|
258
|
+
process.env.WP_READ_ONLY = 'true';
|
|
259
|
+
try {
|
|
260
|
+
const res = await call('wc_list_customers');
|
|
261
|
+
expect(res.isError).toBe(true);
|
|
262
|
+
expect(res.content[0].text).toContain('READ-ONLY');
|
|
263
|
+
} finally {
|
|
264
|
+
if (original === undefined) delete process.env.WP_READ_ONLY;
|
|
265
|
+
else process.env.WP_READ_ONLY = original;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// =========================================================================
|
|
271
|
+
// wc_price_guardrail
|
|
272
|
+
// =========================================================================
|
|
273
|
+
|
|
274
|
+
describe('wc_price_guardrail', () => {
|
|
275
|
+
it('SAFE — price change within default threshold (20%)', async () => {
|
|
276
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
277
|
+
mockSuccess({ id: 101, name: 'T-Shirt', price: '100.00' });
|
|
278
|
+
|
|
279
|
+
const res = await call('wc_price_guardrail', { product_id: 101, new_price: 110 });
|
|
280
|
+
const data = parseResult(res);
|
|
281
|
+
|
|
282
|
+
expect(data.safe).toBe(true);
|
|
283
|
+
expect(data.current_price).toBe(100);
|
|
284
|
+
expect(data.new_price).toBe(110);
|
|
285
|
+
expect(data.change_percent).toBe(10);
|
|
286
|
+
expect(data.message).toContain('within threshold');
|
|
287
|
+
|
|
288
|
+
const logs = getAuditLogs();
|
|
289
|
+
const entry = logs.find(l => l.tool === 'wc_price_guardrail');
|
|
290
|
+
expect(entry).toBeDefined();
|
|
291
|
+
expect(entry.action).toBe('price_check');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('UNSAFE — price change exceeds threshold', async () => {
|
|
295
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
296
|
+
mockSuccess({ id: 101, name: 'T-Shirt', price: '100.00' });
|
|
297
|
+
|
|
298
|
+
const res = await call('wc_price_guardrail', { product_id: 101, new_price: 150 });
|
|
299
|
+
const data = parseResult(res);
|
|
300
|
+
|
|
301
|
+
expect(data.safe).toBe(false);
|
|
302
|
+
expect(data.change_percent).toBe(50);
|
|
303
|
+
expect(data.requires_confirmation).toBe(true);
|
|
304
|
+
expect(data.message).toContain('exceeds threshold');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('CUSTOM THRESHOLD — respects threshold_percent param', async () => {
|
|
308
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
309
|
+
mockSuccess({ id: 101, name: 'T-Shirt', price: '100.00' });
|
|
310
|
+
|
|
311
|
+
const res = await call('wc_price_guardrail', { product_id: 101, new_price: 108, threshold_percent: 5 });
|
|
312
|
+
const data = parseResult(res);
|
|
313
|
+
|
|
314
|
+
expect(data.safe).toBe(false);
|
|
315
|
+
expect(data.change_percent).toBe(8);
|
|
316
|
+
expect(data.message).toContain('5%');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('ERROR — product not found', async () => {
|
|
320
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
321
|
+
mockError(404, '{"code":"woocommerce_rest_product_invalid_id","message":"Invalid ID."}');
|
|
322
|
+
|
|
323
|
+
const res = await call('wc_price_guardrail', { product_id: 999, new_price: 50 });
|
|
324
|
+
expect(res.isError).toBe(true);
|
|
325
|
+
expect(res.content[0].text).toContain('404');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('NOT blocked by WP_READ_ONLY (analysis only)', async () => {
|
|
329
|
+
const original = process.env.WP_READ_ONLY;
|
|
330
|
+
process.env.WP_READ_ONLY = 'true';
|
|
331
|
+
try {
|
|
332
|
+
_testSetTarget('shop', { url: 'https://shop.example.com', username: 'u', password: 'p' });
|
|
333
|
+
mockSuccess({ id: 101, name: 'T-Shirt', price: '100.00' });
|
|
334
|
+
|
|
335
|
+
const res = await call('wc_price_guardrail', { product_id: 101, new_price: 110 });
|
|
336
|
+
expect(res.isError).toBeUndefined();
|
|
337
|
+
const data = parseResult(res);
|
|
338
|
+
expect(data.safe).toBe(true);
|
|
339
|
+
} finally {
|
|
340
|
+
if (original === undefined) delete process.env.WP_READ_ONLY;
|
|
341
|
+
else process.env.WP_READ_ONLY = original;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
});
|