@adsim/wordpress-mcp-server 4.5.1 → 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.
Files changed (61) hide show
  1. package/.env.example +18 -0
  2. package/README.md +857 -447
  3. package/companion/mcp-diagnostics.php +1184 -0
  4. package/dxt/manifest.json +718 -90
  5. package/index.js +188 -4747
  6. package/package.json +14 -6
  7. package/src/data/plugin-performance-data.json +59 -0
  8. package/src/plugins/IPluginAdapter.js +95 -0
  9. package/src/plugins/adapters/acf/acfAdapter.js +181 -0
  10. package/src/plugins/adapters/elementor/elementorAdapter.js +176 -0
  11. package/src/plugins/contextGuard.js +57 -0
  12. package/src/plugins/registry.js +94 -0
  13. package/src/shared/api.js +79 -0
  14. package/src/shared/audit.js +39 -0
  15. package/src/shared/context.js +15 -0
  16. package/src/shared/governance.js +98 -0
  17. package/src/shared/utils.js +148 -0
  18. package/src/tools/comments.js +50 -0
  19. package/src/tools/content.js +353 -0
  20. package/src/tools/core.js +114 -0
  21. package/src/tools/editorial.js +634 -0
  22. package/src/tools/fse.js +370 -0
  23. package/src/tools/health.js +160 -0
  24. package/src/tools/index.js +96 -0
  25. package/src/tools/intelligence.js +2082 -0
  26. package/src/tools/links.js +118 -0
  27. package/src/tools/media.js +71 -0
  28. package/src/tools/performance.js +219 -0
  29. package/src/tools/plugins.js +368 -0
  30. package/src/tools/schema.js +417 -0
  31. package/src/tools/security.js +590 -0
  32. package/src/tools/seo.js +1633 -0
  33. package/src/tools/taxonomy.js +115 -0
  34. package/src/tools/users.js +188 -0
  35. package/src/tools/woocommerce.js +1008 -0
  36. package/src/tools/workflow.js +409 -0
  37. package/src/transport/http.js +39 -0
  38. package/tests/unit/helpers/pagination.test.js +43 -0
  39. package/tests/unit/pluginLayer.test.js +151 -0
  40. package/tests/unit/plugins/acf/acfAdapter.test.js +205 -0
  41. package/tests/unit/plugins/acf/acfAdapter.write.test.js +157 -0
  42. package/tests/unit/plugins/contextGuard.test.js +51 -0
  43. package/tests/unit/plugins/elementor/elementorAdapter.test.js +206 -0
  44. package/tests/unit/plugins/iPluginAdapter.test.js +34 -0
  45. package/tests/unit/plugins/registry.test.js +84 -0
  46. package/tests/unit/tools/bulkUpdate.test.js +188 -0
  47. package/tests/unit/tools/diagnostics.test.js +397 -0
  48. package/tests/unit/tools/dynamicFiltering.test.js +100 -8
  49. package/tests/unit/tools/editorialIntelligence.test.js +817 -0
  50. package/tests/unit/tools/fse.test.js +548 -0
  51. package/tests/unit/tools/multilingual.test.js +653 -0
  52. package/tests/unit/tools/performance.test.js +351 -0
  53. package/tests/unit/tools/runWorkflow.test.js +150 -0
  54. package/tests/unit/tools/schema.test.js +477 -0
  55. package/tests/unit/tools/security.test.js +695 -0
  56. package/tests/unit/tools/site.test.js +1 -1
  57. package/tests/unit/tools/siteOptions.test.js +101 -0
  58. package/tests/unit/tools/users.crud.test.js +399 -0
  59. package/tests/unit/tools/validateBlocks.test.js +186 -0
  60. package/tests/unit/tools/visualStaging.test.js +271 -0
  61. package/tests/unit/tools/woocommerce.advanced.test.js +679 -0
@@ -80,7 +80,7 @@ describe('wp_site_info', () => {
80
80
 
81
81
  // Server info
82
82
  expect(data.server.mcp_version).toBeDefined();
83
- expect(data.server.tools_total).toBe(85);
83
+ expect(data.server.tools_total).toBe(175);
84
84
  expect(typeof data.server.tools_exposed).toBe('number');
85
85
  expect(Array.isArray(data.server.filtered_out)).toBe(true);
86
86
  });
@@ -0,0 +1,101 @@
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 } from '../../../index.js';
7
+ import { mockSuccess, mockError, makeRequest, parseResult, getAuditLogs } from '../../helpers/mockWpRequest.js';
8
+
9
+ function call(name, args = {}) {
10
+ return handleToolCall(makeRequest(name, args));
11
+ }
12
+
13
+ let consoleSpy;
14
+ const envBackup = {};
15
+
16
+ function saveEnv(...keys) {
17
+ keys.forEach(k => { envBackup[k] = process.env[k]; });
18
+ }
19
+ function restoreEnv() {
20
+ Object.entries(envBackup).forEach(([k, v]) => {
21
+ if (v === undefined) delete process.env[k];
22
+ else process.env[k] = v;
23
+ });
24
+ }
25
+
26
+ beforeEach(() => {
27
+ fetch.mockReset();
28
+ consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
29
+ saveEnv('WP_READ_ONLY');
30
+ });
31
+ afterEach(() => {
32
+ consoleSpy.mockRestore();
33
+ restoreEnv();
34
+ });
35
+
36
+ // ── Mock data ──
37
+
38
+ const mockSettings = {
39
+ title: 'My Site',
40
+ description: 'Just another WordPress site',
41
+ url: 'https://test.example.com',
42
+ email: 'admin@test.example.com',
43
+ timezone_string: 'Europe/Brussels',
44
+ date_format: 'Y-m-d',
45
+ time_format: 'H:i',
46
+ language: 'en_US',
47
+ posts_per_page: 10,
48
+ };
49
+
50
+ // =========================================================================
51
+ // wp_get_site_options
52
+ // =========================================================================
53
+
54
+ describe('wp_get_site_options', () => {
55
+ it('returns all options when no keys parameter', async () => {
56
+ fetch.mockResolvedValue(mockSuccess(mockSettings));
57
+ const data = parseResult(await call('wp_get_site_options'));
58
+ expect(data.title).toBe('My Site');
59
+ expect(data.description).toBe('Just another WordPress site');
60
+ expect(data.timezone_string).toBe('Europe/Brussels');
61
+ expect(Object.keys(data).length).toBe(Object.keys(mockSettings).length);
62
+ });
63
+
64
+ it('filters to requested keys only', async () => {
65
+ fetch.mockResolvedValue(mockSuccess(mockSettings));
66
+ const data = parseResult(await call('wp_get_site_options', { keys: ['title', 'language'] }));
67
+ expect(data.title).toBe('My Site');
68
+ expect(data.language).toBe('en_US');
69
+ expect(Object.keys(data)).toEqual(['title', 'language']);
70
+ expect(data.email).toBeUndefined();
71
+ });
72
+
73
+ it('handles 403 (insufficient permissions)', async () => {
74
+ mockError(403, '{"code":"rest_forbidden","message":"Sorry, you are not allowed to manage options."}');
75
+ const res = await call('wp_get_site_options');
76
+ expect(res.isError).toBe(true);
77
+ expect(res.content[0].text).toContain('403');
78
+ });
79
+
80
+ it('logs correct audit format to stderr', async () => {
81
+ fetch.mockResolvedValue(mockSuccess(mockSettings));
82
+ await call('wp_get_site_options', { keys: ['title'] });
83
+ const logs = getAuditLogs();
84
+ const entry = logs.find(l => l.tool === 'wp_get_site_options');
85
+ expect(entry).toBeDefined();
86
+ expect(entry.action).toBe('read_options');
87
+ expect(entry.status).toBe('success');
88
+ expect(typeof entry.latency_ms).toBe('number');
89
+ expect(entry.params.keys_requested).toBe(1);
90
+ expect(entry.params.keys_returned).toBe(1);
91
+ });
92
+
93
+ it('is NOT blocked by WP_READ_ONLY=true', async () => {
94
+ process.env.WP_READ_ONLY = 'true';
95
+ fetch.mockResolvedValue(mockSuccess(mockSettings));
96
+ const res = await call('wp_get_site_options');
97
+ expect(res.isError).toBeUndefined();
98
+ const data = parseResult(res);
99
+ expect(data.title).toBe('My Site');
100
+ });
101
+ });
@@ -0,0 +1,399 @@
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 } 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
+ // Helper: mock two sequential fetch calls
14
+ function mockTwoSuccess(data1, data2) {
15
+ fetch
16
+ .mockImplementationOnce(() => Promise.resolve({
17
+ ok: true, status: 200,
18
+ headers: { get: (h) => h === 'content-type' ? 'application/json' : null },
19
+ json: () => Promise.resolve(data1),
20
+ text: () => Promise.resolve(JSON.stringify(data1))
21
+ }))
22
+ .mockImplementationOnce(() => Promise.resolve({
23
+ ok: true, status: 200,
24
+ headers: { get: (h) => h === 'content-type' ? 'application/json' : null },
25
+ json: () => Promise.resolve(data2),
26
+ text: () => Promise.resolve(JSON.stringify(data2))
27
+ }));
28
+ }
29
+
30
+ // ════════════════════════════════════════════════════════════
31
+ // wp_get_user
32
+ // ════════════════════════════════════════════════════════════
33
+
34
+ describe('wp_get_user', () => {
35
+ let consoleSpy;
36
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
37
+ afterEach(() => { consoleSpy.mockRestore(); });
38
+
39
+ it('returns full user profile', async () => {
40
+ fetch.mockResolvedValue(mockSuccess({
41
+ id: 1, username: 'admin', name: 'Admin', slug: 'admin', email: 'admin@example.com',
42
+ roles: ['administrator'], first_name: 'Site', last_name: 'Admin', url: 'https://example.com',
43
+ description: 'The admin', link: 'https://example.com/author/admin', registered_date: '2023-01-01T00:00:00',
44
+ locale: 'en_US', nickname: 'admin', avatar_urls: { '96': 'https://example.com/avatar.jpg' }, meta: {}
45
+ }));
46
+ const result = await call('wp_get_user', { id: 1 });
47
+ const data = parseResult(result);
48
+ expect(data.id).toBe(1);
49
+ expect(data.username).toBe('admin');
50
+ expect(data.email).toBe('admin@example.com');
51
+ expect(data.roles).toContain('administrator');
52
+ expect(data.first_name).toBe('Site');
53
+ expect(data.registered_date).toBe('2023-01-01T00:00:00');
54
+ });
55
+
56
+ it('returns error on 404', async () => {
57
+ fetch.mockResolvedValue(mockError(404));
58
+ const result = await call('wp_get_user', { id: 9999 });
59
+ expect(result.isError).toBe(true);
60
+ });
61
+
62
+ it('logs audit entry', async () => {
63
+ fetch.mockResolvedValue(mockSuccess({ id: 1, name: 'Admin', slug: 'admin', roles: [] }));
64
+ await call('wp_get_user', { id: 1 });
65
+ const logs = getAuditLogs();
66
+ const entry = logs.find(l => l.tool === 'wp_get_user');
67
+ expect(entry).toBeDefined();
68
+ expect(entry.target).toBe(1);
69
+ });
70
+ });
71
+
72
+ // ════════════════════════════════════════════════════════════
73
+ // wp_create_user
74
+ // ════════════════════════════════════════════════════════════
75
+
76
+ describe('wp_create_user', () => {
77
+ let consoleSpy;
78
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
79
+ afterEach(() => { consoleSpy.mockRestore(); });
80
+
81
+ it('creates a user when confirm=true', async () => {
82
+ fetch.mockResolvedValue(mockSuccess({
83
+ id: 5, username: 'newuser', slug: 'newuser', email: 'new@example.com',
84
+ name: 'New User', roles: ['subscriber']
85
+ }));
86
+ const result = await call('wp_create_user', {
87
+ username: 'newuser', email: 'new@example.com', password: 'Str0ngP@ss!', confirm: true
88
+ });
89
+ const data = parseResult(result);
90
+ expect(data.success).toBe(true);
91
+ expect(data.user.id).toBe(5);
92
+ expect(data.user.username).toBe('newuser');
93
+ });
94
+
95
+ it('rejects when confirm is not true', async () => {
96
+ const result = await call('wp_create_user', {
97
+ username: 'test', email: 'test@example.com', password: 'pass', confirm: false
98
+ });
99
+ expect(result.isError).toBe(true);
100
+ expect(result.content[0].text).toContain('confirm=true');
101
+ });
102
+
103
+ it('rejects when confirm is missing', async () => {
104
+ const result = await call('wp_create_user', {
105
+ username: 'test', email: 'test@example.com', password: 'pass'
106
+ });
107
+ expect(result.isError).toBe(true);
108
+ });
109
+
110
+ it('passes optional fields', async () => {
111
+ fetch.mockResolvedValue(mockSuccess({
112
+ id: 6, username: 'jane', slug: 'jane', email: 'jane@example.com',
113
+ name: 'Jane Doe', roles: ['editor']
114
+ }));
115
+ await call('wp_create_user', {
116
+ username: 'jane', email: 'jane@example.com', password: 'P@ss123!',
117
+ role: 'editor', first_name: 'Jane', last_name: 'Doe', confirm: true
118
+ });
119
+ const [, opts] = fetch.mock.calls[0];
120
+ const body = JSON.parse(opts.body);
121
+ expect(body.roles).toEqual(['editor']);
122
+ expect(body.first_name).toBe('Jane');
123
+ expect(body.last_name).toBe('Doe');
124
+ });
125
+
126
+ it('is blocked by WP_READ_ONLY', async () => {
127
+ process.env.WP_READ_ONLY = 'true';
128
+ try {
129
+ const result = await call('wp_create_user', {
130
+ username: 'test', email: 'test@example.com', password: 'pass', confirm: true
131
+ });
132
+ expect(result.isError).toBe(true);
133
+ expect(result.content[0].text).toContain('READ-ONLY');
134
+ } finally {
135
+ delete process.env.WP_READ_ONLY;
136
+ }
137
+ });
138
+ });
139
+
140
+ // ════════════════════════════════════════════════════════════
141
+ // wp_update_user
142
+ // ════════════════════════════════════════════════════════════
143
+
144
+ describe('wp_update_user', () => {
145
+ let consoleSpy;
146
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
147
+ afterEach(() => { consoleSpy.mockRestore(); });
148
+
149
+ it('updates user fields', async () => {
150
+ fetch.mockResolvedValue(mockSuccess({
151
+ id: 2, name: 'Updated Name', email: 'updated@example.com', roles: ['editor'], slug: 'updated'
152
+ }));
153
+ const result = await call('wp_update_user', { id: 2, display_name: 'Updated Name', role: 'editor' });
154
+ const data = parseResult(result);
155
+ expect(data.success).toBe(true);
156
+ expect(data.user.name).toBe('Updated Name');
157
+ });
158
+
159
+ it('sends correct payload', async () => {
160
+ fetch.mockResolvedValue(mockSuccess({ id: 2, name: 'X', email: 'x@x.com', roles: ['author'], slug: 'x' }));
161
+ await call('wp_update_user', { id: 2, email: 'new@example.com', description: 'A bio', meta: { custom: 'value' } });
162
+ const [, opts] = fetch.mock.calls[0];
163
+ const body = JSON.parse(opts.body);
164
+ expect(body.email).toBe('new@example.com');
165
+ expect(body.description).toBe('A bio');
166
+ expect(body.meta).toEqual({ custom: 'value' });
167
+ });
168
+
169
+ it('is blocked by WP_READ_ONLY', async () => {
170
+ process.env.WP_READ_ONLY = 'true';
171
+ try {
172
+ const result = await call('wp_update_user', { id: 2, display_name: 'X' });
173
+ expect(result.isError).toBe(true);
174
+ expect(result.content[0].text).toContain('READ-ONLY');
175
+ } finally {
176
+ delete process.env.WP_READ_ONLY;
177
+ }
178
+ });
179
+ });
180
+
181
+ // ════════════════════════════════════════════════════════════
182
+ // wp_delete_user
183
+ // ════════════════════════════════════════════════════════════
184
+
185
+ describe('wp_delete_user', () => {
186
+ let consoleSpy;
187
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
188
+ afterEach(() => { consoleSpy.mockRestore(); });
189
+
190
+ it('deletes user when confirm=true and reassign provided', async () => {
191
+ fetch.mockResolvedValue(mockSuccess({ deleted: true }));
192
+ const result = await call('wp_delete_user', { id: 5, reassign: 1, confirm: true });
193
+ const data = parseResult(result);
194
+ expect(data.success).toBe(true);
195
+ expect(data.message).toContain('reassigned to user 1');
196
+ });
197
+
198
+ it('rejects when confirm is not true', async () => {
199
+ const result = await call('wp_delete_user', { id: 5, reassign: 1, confirm: false });
200
+ expect(result.isError).toBe(true);
201
+ expect(result.content[0].text).toContain('confirm=true');
202
+ });
203
+
204
+ it('passes force and reassign in URL', async () => {
205
+ fetch.mockResolvedValue(mockSuccess({ deleted: true }));
206
+ await call('wp_delete_user', { id: 5, reassign: 1, confirm: true });
207
+ const [url] = fetch.mock.calls[0];
208
+ expect(url).toContain('/users/5?force=true&reassign=1');
209
+ });
210
+
211
+ it('is blocked by WP_READ_ONLY', async () => {
212
+ process.env.WP_READ_ONLY = 'true';
213
+ try {
214
+ const result = await call('wp_delete_user', { id: 5, reassign: 1, confirm: true });
215
+ expect(result.isError).toBe(true);
216
+ expect(result.content[0].text).toContain('READ-ONLY');
217
+ } finally {
218
+ delete process.env.WP_READ_ONLY;
219
+ }
220
+ });
221
+
222
+ it('is blocked by WP_DISABLE_DELETE', async () => {
223
+ process.env.WP_DISABLE_DELETE = 'true';
224
+ try {
225
+ const result = await call('wp_delete_user', { id: 5, reassign: 1, confirm: true });
226
+ expect(result.isError).toBe(true);
227
+ expect(result.content[0].text).toContain('DISABLE_DELETE');
228
+ } finally {
229
+ delete process.env.WP_DISABLE_DELETE;
230
+ }
231
+ });
232
+ });
233
+
234
+ // ════════════════════════════════════════════════════════════
235
+ // wp_list_user_roles
236
+ // ════════════════════════════════════════════════════════════
237
+
238
+ describe('wp_list_user_roles', () => {
239
+ let consoleSpy;
240
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
241
+ afterEach(() => { consoleSpy.mockRestore(); });
242
+
243
+ it('returns roles as array', async () => {
244
+ fetch.mockResolvedValue(mockSuccess([
245
+ { slug: 'administrator', name: 'Administrator', capabilities: { manage_options: true, edit_posts: true } },
246
+ { slug: 'editor', name: 'Editor', capabilities: { edit_posts: true, edit_others_posts: true } }
247
+ ]));
248
+ const result = await call('wp_list_user_roles');
249
+ const data = parseResult(result);
250
+ expect(data.total).toBe(2);
251
+ expect(data.roles[0].slug).toBe('administrator');
252
+ expect(data.roles[0].capabilities.manage_options).toBe(true);
253
+ });
254
+
255
+ it('handles object format', async () => {
256
+ fetch.mockResolvedValue(mockSuccess({
257
+ administrator: { name: 'Administrator', capabilities: { manage_options: true } },
258
+ subscriber: { name: 'Subscriber', capabilities: { read: true } }
259
+ }));
260
+ const result = await call('wp_list_user_roles');
261
+ const data = parseResult(result);
262
+ expect(data.total).toBe(2);
263
+ expect(data.roles.find(r => r.slug === 'administrator')).toBeDefined();
264
+ });
265
+ });
266
+
267
+ // ════════════════════════════════════════════════════════════
268
+ // wp_get_user_capabilities
269
+ // ════════════════════════════════════════════════════════════
270
+
271
+ describe('wp_get_user_capabilities', () => {
272
+ let consoleSpy;
273
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
274
+ afterEach(() => { consoleSpy.mockRestore(); });
275
+
276
+ it('returns active capabilities', async () => {
277
+ fetch.mockResolvedValue(mockSuccess({
278
+ id: 1, name: 'Admin', roles: ['administrator'],
279
+ capabilities: { manage_options: true, edit_posts: true, read: true, delete_posts: false }
280
+ }));
281
+ const result = await call('wp_get_user_capabilities', { id: 1 });
282
+ const data = parseResult(result);
283
+ expect(data.id).toBe(1);
284
+ expect(data.capabilities).toContain('manage_options');
285
+ expect(data.capabilities).toContain('edit_posts');
286
+ expect(data.capabilities).not.toContain('delete_posts');
287
+ expect(data.capabilities_count).toBe(3);
288
+ });
289
+ });
290
+
291
+ // ════════════════════════════════════════════════════════════
292
+ // wp_reset_user_password
293
+ // ════════════════════════════════════════════════════════════
294
+
295
+ describe('wp_reset_user_password', () => {
296
+ let consoleSpy;
297
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
298
+ afterEach(() => { consoleSpy.mockRestore(); });
299
+
300
+ it('sends password reset email', async () => {
301
+ mockTwoSuccess(
302
+ { id: 3, name: 'User', email: 'user@example.com' },
303
+ { success: true, user_id: 3, message: 'Password reset email sent.' }
304
+ );
305
+ const result = await call('wp_reset_user_password', { id: 3 });
306
+ const data = parseResult(result);
307
+ expect(data.success).toBe(true);
308
+ expect(data.user.email).toContain('***'); // Masked email
309
+ });
310
+
311
+ it('is blocked by WP_READ_ONLY', async () => {
312
+ process.env.WP_READ_ONLY = 'true';
313
+ try {
314
+ const result = await call('wp_reset_user_password', { id: 3 });
315
+ expect(result.isError).toBe(true);
316
+ expect(result.content[0].text).toContain('READ-ONLY');
317
+ } finally {
318
+ delete process.env.WP_READ_ONLY;
319
+ }
320
+ });
321
+
322
+ it('calls mcp-diagnostics endpoint', async () => {
323
+ mockTwoSuccess(
324
+ { id: 3, name: 'User', email: 'user@example.com' },
325
+ { success: true }
326
+ );
327
+ await call('wp_reset_user_password', { id: 3 });
328
+ const secondUrl = fetch.mock.calls[1][0];
329
+ expect(secondUrl).toContain('/wp-json/mcp-diagnostics/v1/password-reset');
330
+ });
331
+ });
332
+
333
+ // ════════════════════════════════════════════════════════════
334
+ // wp_list_user_application_passwords
335
+ // ════════════════════════════════════════════════════════════
336
+
337
+ describe('wp_list_user_application_passwords', () => {
338
+ let consoleSpy;
339
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
340
+ afterEach(() => { consoleSpy.mockRestore(); });
341
+
342
+ it('returns application passwords', async () => {
343
+ fetch.mockResolvedValue(mockSuccess([
344
+ { uuid: 'abc-123', name: 'MCP Server', created: '2024-01-01', last_used: '2024-06-01', last_ip: '1.2.3.4' },
345
+ { uuid: 'def-456', name: 'Mobile App', created: '2024-03-01', last_used: null, last_ip: null }
346
+ ]));
347
+ const result = await call('wp_list_user_application_passwords', { id: 1 });
348
+ const data = parseResult(result);
349
+ expect(data.user_id).toBe(1);
350
+ expect(data.total).toBe(2);
351
+ expect(data.application_passwords[0].uuid).toBe('abc-123');
352
+ expect(data.application_passwords[0].name).toBe('MCP Server');
353
+ expect(data.application_passwords[1].last_used).toBeNull();
354
+ });
355
+
356
+ it('calls correct endpoint', async () => {
357
+ fetch.mockResolvedValue(mockSuccess([]));
358
+ await call('wp_list_user_application_passwords', { id: 1 });
359
+ const [url] = fetch.mock.calls[0];
360
+ expect(url).toContain('/users/1/application-passwords');
361
+ });
362
+ });
363
+
364
+ // ════════════════════════════════════════════════════════════
365
+ // wp_revoke_application_password
366
+ // ════════════════════════════════════════════════════════════
367
+
368
+ describe('wp_revoke_application_password', () => {
369
+ let consoleSpy;
370
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
371
+ afterEach(() => { consoleSpy.mockRestore(); });
372
+
373
+ it('revokes an application password', async () => {
374
+ fetch.mockResolvedValue(mockSuccess({ deleted: true }));
375
+ const result = await call('wp_revoke_application_password', { id: 1, uuid: 'abc-123' });
376
+ const data = parseResult(result);
377
+ expect(data.success).toBe(true);
378
+ expect(data.message).toContain('abc-123');
379
+ });
380
+
381
+ it('calls correct DELETE endpoint', async () => {
382
+ fetch.mockResolvedValue(mockSuccess({ deleted: true }));
383
+ await call('wp_revoke_application_password', { id: 1, uuid: 'abc-123' });
384
+ const [url, opts] = fetch.mock.calls[0];
385
+ expect(url).toContain('/users/1/application-passwords/abc-123');
386
+ expect(opts.method).toBe('DELETE');
387
+ });
388
+
389
+ it('is blocked by WP_READ_ONLY', async () => {
390
+ process.env.WP_READ_ONLY = 'true';
391
+ try {
392
+ const result = await call('wp_revoke_application_password', { id: 1, uuid: 'abc-123' });
393
+ expect(result.isError).toBe(true);
394
+ expect(result.content[0].text).toContain('READ-ONLY');
395
+ } finally {
396
+ delete process.env.WP_READ_ONLY;
397
+ }
398
+ });
399
+ });