@adsim/wordpress-mcp-server 4.6.0 → 5.1.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/.env.example +18 -0
- package/README.md +851 -499
- package/companion/mcp-diagnostics.php +1184 -0
- package/dxt/manifest.json +715 -98
- package/index.js +166 -4786
- package/package.json +14 -6
- package/src/data/plugin-performance-data.json +59 -0
- package/src/shared/api.js +79 -0
- package/src/shared/audit.js +39 -0
- package/src/shared/context.js +15 -0
- package/src/shared/governance.js +98 -0
- package/src/shared/utils.js +148 -0
- package/src/tools/comments.js +50 -0
- package/src/tools/content.js +353 -0
- package/src/tools/core.js +114 -0
- package/src/tools/editorial.js +634 -0
- package/src/tools/fse.js +370 -0
- package/src/tools/health.js +160 -0
- package/src/tools/index.js +96 -0
- package/src/tools/intelligence.js +2082 -0
- package/src/tools/links.js +118 -0
- package/src/tools/media.js +71 -0
- package/src/tools/performance.js +219 -0
- package/src/tools/plugins.js +368 -0
- package/src/tools/schema.js +417 -0
- package/src/tools/security.js +590 -0
- package/src/tools/seo.js +1633 -0
- package/src/tools/taxonomy.js +115 -0
- package/src/tools/users.js +188 -0
- package/src/tools/woocommerce.js +1008 -0
- package/src/tools/workflow.js +409 -0
- package/src/transport/http.js +39 -0
- package/tests/unit/helpers/pagination.test.js +43 -0
- package/tests/unit/tools/bulkUpdate.test.js +188 -0
- package/tests/unit/tools/diagnostics.test.js +397 -0
- package/tests/unit/tools/dynamicFiltering.test.js +100 -8
- package/tests/unit/tools/editorialIntelligence.test.js +817 -0
- package/tests/unit/tools/fse.test.js +548 -0
- package/tests/unit/tools/multilingual.test.js +653 -0
- package/tests/unit/tools/performance.test.js +351 -0
- package/tests/unit/tools/runWorkflow.test.js +150 -0
- package/tests/unit/tools/schema.test.js +477 -0
- package/tests/unit/tools/security.test.js +695 -0
- package/tests/unit/tools/site.test.js +1 -1
- package/tests/unit/tools/users.crud.test.js +399 -0
- package/tests/unit/tools/validateBlocks.test.js +186 -0
- package/tests/unit/tools/visualStaging.test.js +271 -0
- package/tests/unit/tools/woocommerce.advanced.test.js +679 -0
|
@@ -0,0 +1,271 @@
|
|
|
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, TOOLS_DEFINITIONS } from '../../../index.js';
|
|
7
|
+
import { makeRequest, mockSuccess, parseResult } from '../../helpers/mockWpRequest.js';
|
|
8
|
+
|
|
9
|
+
function call(name, args = {}) {
|
|
10
|
+
return handleToolCall(makeRequest(name, args));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let consoleSpy;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
fetch.mockReset();
|
|
17
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
18
|
+
_testSetTarget('site1', { url: 'https://example.com', auth: 'Basic dGVzdDp0ZXN0' });
|
|
19
|
+
// Reset env vars
|
|
20
|
+
delete process.env.WP_VISUAL_STAGING;
|
|
21
|
+
delete process.env.WP_READ_ONLY;
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
consoleSpy.mockRestore();
|
|
25
|
+
delete process.env.WP_VISUAL_STAGING;
|
|
26
|
+
delete process.env.WP_READ_ONLY;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function mockPage(overrides = {}) {
|
|
30
|
+
const { title, content, excerpt, ...rest } = overrides;
|
|
31
|
+
return {
|
|
32
|
+
id: 42,
|
|
33
|
+
title: { rendered: title || 'Live Page' },
|
|
34
|
+
content: { rendered: content || '<p>Live content</p>' },
|
|
35
|
+
excerpt: { rendered: excerpt || '' },
|
|
36
|
+
status: 'publish',
|
|
37
|
+
slug: 'live-page',
|
|
38
|
+
link: 'https://example.com/live-page/',
|
|
39
|
+
date: '2025-06-15T10:00:00',
|
|
40
|
+
modified: '2025-06-15T10:00:00',
|
|
41
|
+
parent: 0,
|
|
42
|
+
template: '',
|
|
43
|
+
meta: {},
|
|
44
|
+
...rest
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mockStagingDraft(overrides = {}) {
|
|
49
|
+
const { title, content, excerpt, ...rest } = overrides;
|
|
50
|
+
return {
|
|
51
|
+
id: 99,
|
|
52
|
+
title: { rendered: title || '[STAGING] Live Page' },
|
|
53
|
+
content: { rendered: content || '<p>Updated staging content</p>' },
|
|
54
|
+
excerpt: { rendered: excerpt || '' },
|
|
55
|
+
status: 'draft',
|
|
56
|
+
slug: 'staging-live-page',
|
|
57
|
+
link: 'https://example.com/?p=99',
|
|
58
|
+
date: '2025-06-16T10:00:00',
|
|
59
|
+
modified: '2025-06-16T12:00:00',
|
|
60
|
+
meta: { _staging_source_id: 42 },
|
|
61
|
+
...rest
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =========================================================================
|
|
66
|
+
// 1. wp_create_staging_draft — creates draft with correct _staging_source_id
|
|
67
|
+
// =========================================================================
|
|
68
|
+
describe('wp_create_staging_draft', () => {
|
|
69
|
+
it('creates a staging draft from a published page', async () => {
|
|
70
|
+
// 1st call: GET /pages/42 → published page
|
|
71
|
+
mockSuccess(mockPage());
|
|
72
|
+
// 2nd call: GET /pages?status=draft... → no existing drafts
|
|
73
|
+
mockSuccess([]);
|
|
74
|
+
// 3rd call: POST /pages → created draft
|
|
75
|
+
mockSuccess(mockStagingDraft());
|
|
76
|
+
|
|
77
|
+
const res = await call('wp_create_staging_draft', { source_id: 42, post_type: 'page' });
|
|
78
|
+
const data = parseResult(res);
|
|
79
|
+
expect(data.status).toBe('created');
|
|
80
|
+
expect(data.staging_draft_id).toBe(99);
|
|
81
|
+
expect(data.source_id).toBe(42);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 2. Returns already_exists if draft exists
|
|
85
|
+
it('returns already_exists if staging draft already exists', async () => {
|
|
86
|
+
mockSuccess(mockPage());
|
|
87
|
+
mockSuccess([mockStagingDraft()]);
|
|
88
|
+
|
|
89
|
+
const res = await call('wp_create_staging_draft', { source_id: 42 });
|
|
90
|
+
const data = parseResult(res);
|
|
91
|
+
expect(data.status).toBe('already_exists');
|
|
92
|
+
expect(data.staging_draft_id).toBe(99);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// 3. Error if source is not published
|
|
96
|
+
it('throws error if source is not published', async () => {
|
|
97
|
+
mockSuccess(mockPage({ status: 'draft' }));
|
|
98
|
+
|
|
99
|
+
const res = await call('wp_create_staging_draft', { source_id: 42 });
|
|
100
|
+
expect(res.isError).toBe(true);
|
|
101
|
+
expect(res.content[0].text).toContain('Only published content can be staged');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// =========================================================================
|
|
106
|
+
// 4. wp_list_staging_drafts — returns list filtered by source_id
|
|
107
|
+
// =========================================================================
|
|
108
|
+
describe('wp_list_staging_drafts', () => {
|
|
109
|
+
it('returns staging drafts filtered by source_id', async () => {
|
|
110
|
+
// Posts search
|
|
111
|
+
mockSuccess([]);
|
|
112
|
+
// Pages search
|
|
113
|
+
mockSuccess([mockStagingDraft(), mockStagingDraft({ id: 100, meta: { _staging_source_id: 55 } })]);
|
|
114
|
+
|
|
115
|
+
const res = await call('wp_list_staging_drafts', { source_id: 42 });
|
|
116
|
+
const data = parseResult(res);
|
|
117
|
+
expect(data.total).toBe(1);
|
|
118
|
+
expect(data.staging_drafts[0].source_id).toBe(42);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// =========================================================================
|
|
123
|
+
// 5. wp_get_staging_preview_url — returns correct preview_url
|
|
124
|
+
// =========================================================================
|
|
125
|
+
describe('wp_get_staging_preview_url', () => {
|
|
126
|
+
it('returns preview URL for a staging draft', async () => {
|
|
127
|
+
mockSuccess(mockStagingDraft());
|
|
128
|
+
|
|
129
|
+
const res = await call('wp_get_staging_preview_url', { staging_id: 99 });
|
|
130
|
+
const data = parseResult(res);
|
|
131
|
+
expect(data.preview_url).toBe('https://example.com/?p=99&preview=true');
|
|
132
|
+
expect(data.source_id).toBe(42);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// 6. Error if not a staging draft
|
|
136
|
+
it('throws error if post is not a staging draft', async () => {
|
|
137
|
+
mockSuccess(mockPage({ id: 42, meta: {} }));
|
|
138
|
+
|
|
139
|
+
const res = await call('wp_get_staging_preview_url', { staging_id: 42 });
|
|
140
|
+
expect(res.isError).toBe(true);
|
|
141
|
+
expect(res.content[0].text).toContain('not a staging draft');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// =========================================================================
|
|
146
|
+
// 7-8. wp_merge_staging_to_live
|
|
147
|
+
// =========================================================================
|
|
148
|
+
describe('wp_merge_staging_to_live', () => {
|
|
149
|
+
// 7. confirm:false → dry-run
|
|
150
|
+
it('returns dry-run preview when confirm=false', async () => {
|
|
151
|
+
// GET /pages/99 → staging draft
|
|
152
|
+
mockSuccess(mockStagingDraft());
|
|
153
|
+
// GET /pages/42 → live page
|
|
154
|
+
mockSuccess(mockPage());
|
|
155
|
+
|
|
156
|
+
const res = await call('wp_merge_staging_to_live', { staging_id: 99, confirm: false });
|
|
157
|
+
const data = parseResult(res);
|
|
158
|
+
expect(data.status).toBe('dry_run');
|
|
159
|
+
expect(data.changes_preview).toBeDefined();
|
|
160
|
+
expect(data.changes_preview.title.from).toBe('Live Page');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// 8. confirm:true → merge + audit
|
|
164
|
+
it('merges staging draft to live when confirm=true', async () => {
|
|
165
|
+
// GET /pages/99 → staging draft
|
|
166
|
+
mockSuccess(mockStagingDraft());
|
|
167
|
+
// GET /pages/42 → live page (for comparison, though merge skips this in confirm path)
|
|
168
|
+
mockSuccess(mockPage());
|
|
169
|
+
// POST /pages/42 → updated live page
|
|
170
|
+
mockSuccess(mockPage({ modified: '2025-06-16T14:00:00' }));
|
|
171
|
+
// DELETE /pages/99 → deleted
|
|
172
|
+
mockSuccess({ deleted: true });
|
|
173
|
+
|
|
174
|
+
const res = await call('wp_merge_staging_to_live', { staging_id: 99, confirm: true });
|
|
175
|
+
const data = parseResult(res);
|
|
176
|
+
expect(data.status).toBe('merged');
|
|
177
|
+
expect(data.source_id).toBe(42);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// 9. Blocked by WP_READ_ONLY
|
|
181
|
+
it('is blocked when WP_READ_ONLY=true', async () => {
|
|
182
|
+
process.env.WP_READ_ONLY = 'true';
|
|
183
|
+
|
|
184
|
+
const res = await call('wp_merge_staging_to_live', { staging_id: 99 });
|
|
185
|
+
expect(res.isError).toBe(true);
|
|
186
|
+
expect(res.content[0].text).toContain('READ-ONLY');
|
|
187
|
+
|
|
188
|
+
delete process.env.WP_READ_ONLY;
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// =========================================================================
|
|
193
|
+
// 10-11. wp_discard_staging_draft
|
|
194
|
+
// =========================================================================
|
|
195
|
+
describe('wp_discard_staging_draft', () => {
|
|
196
|
+
// 10. confirm:false → dry-run
|
|
197
|
+
it('returns dry-run when confirm=false', async () => {
|
|
198
|
+
mockSuccess(mockStagingDraft());
|
|
199
|
+
|
|
200
|
+
const res = await call('wp_discard_staging_draft', { staging_id: 99, confirm: false });
|
|
201
|
+
const data = parseResult(res);
|
|
202
|
+
expect(data.status).toBe('dry_run');
|
|
203
|
+
expect(data.source_id).toBe(42);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// 11. confirm:true → deletion + audit
|
|
207
|
+
it('discards staging draft when confirm=true', async () => {
|
|
208
|
+
mockSuccess(mockStagingDraft());
|
|
209
|
+
mockSuccess({ deleted: true });
|
|
210
|
+
|
|
211
|
+
const res = await call('wp_discard_staging_draft', { staging_id: 99, confirm: true });
|
|
212
|
+
const data = parseResult(res);
|
|
213
|
+
expect(data.status).toBe('discarded');
|
|
214
|
+
expect(data.staging_id).toBe(99);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// =========================================================================
|
|
219
|
+
// 12-14. WP_VISUAL_STAGING interception
|
|
220
|
+
// =========================================================================
|
|
221
|
+
describe('WP_VISUAL_STAGING interception', () => {
|
|
222
|
+
// 12. wp_update_post on published → intercepted
|
|
223
|
+
it('intercepts wp_update_post on published content when WP_VISUAL_STAGING=true', async () => {
|
|
224
|
+
process.env.WP_VISUAL_STAGING = 'true';
|
|
225
|
+
// GET /posts/42 → published post
|
|
226
|
+
mockSuccess({ id: 42, status: 'publish', title: { rendered: 'Test' }, content: { rendered: '' }, meta: {} });
|
|
227
|
+
|
|
228
|
+
const res = await call('wp_update_post', { id: 42, title: 'New Title' });
|
|
229
|
+
const data = parseResult(res);
|
|
230
|
+
expect(data.status).toBe('intercepted');
|
|
231
|
+
expect(data.reason).toContain('WP_VISUAL_STAGING');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// 13. wp_update_post on draft → NOT intercepted (normal flow)
|
|
235
|
+
it('does NOT intercept wp_update_post on draft content', async () => {
|
|
236
|
+
process.env.WP_VISUAL_STAGING = 'true';
|
|
237
|
+
// GET /posts/42 → draft post
|
|
238
|
+
mockSuccess({ id: 42, status: 'draft', title: { rendered: 'Test' }, content: { rendered: '' }, meta: {} });
|
|
239
|
+
// POST /posts/42 → updated
|
|
240
|
+
mockSuccess({ id: 42, status: 'draft', title: { rendered: 'New Title' }, link: 'https://example.com/?p=42', modified: '2025-06-16' });
|
|
241
|
+
|
|
242
|
+
const res = await call('wp_update_post', { id: 42, title: 'New Title' });
|
|
243
|
+
const data = parseResult(res);
|
|
244
|
+
expect(data.success).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 14. WP_VISUAL_STAGING=false → normal (no interception)
|
|
248
|
+
it('does NOT intercept when WP_VISUAL_STAGING is not set', async () => {
|
|
249
|
+
// No WP_VISUAL_STAGING env var
|
|
250
|
+
// POST /posts/42 → updated
|
|
251
|
+
mockSuccess({ id: 42, status: 'publish', title: { rendered: 'New Title' }, link: 'https://example.com/test/', modified: '2025-06-16' });
|
|
252
|
+
|
|
253
|
+
const res = await call('wp_update_post', { id: 42, title: 'New Title' });
|
|
254
|
+
const data = parseResult(res);
|
|
255
|
+
expect(data.success).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// =========================================================================
|
|
260
|
+
// 15. TOOLS_DEFINITIONS contains the 5 new tools
|
|
261
|
+
// =========================================================================
|
|
262
|
+
describe('TOOLS_DEFINITIONS', () => {
|
|
263
|
+
it('contains the 5 visual staging tools with workflow category', () => {
|
|
264
|
+
const stagingTools = ['wp_create_staging_draft', 'wp_list_staging_drafts', 'wp_get_staging_preview_url', 'wp_merge_staging_to_live', 'wp_discard_staging_draft'];
|
|
265
|
+
for (const toolName of stagingTools) {
|
|
266
|
+
const tool = TOOLS_DEFINITIONS.find(t => t.name === toolName);
|
|
267
|
+
expect(tool, `${toolName} should exist in TOOLS_DEFINITIONS`).toBeDefined();
|
|
268
|
+
expect(tool._category).toBe('workflow');
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|