@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
@@ -0,0 +1,115 @@
1
+ // src/tools/taxonomy.js — taxonomy tools (5)
2
+ // Definitions + handlers (v5.0.0 refactor Step B+C)
3
+
4
+ import { json, buildPaginationMeta } from '../shared/utils.js';
5
+ import { validateInput } from '../shared/governance.js';
6
+ import { rt } from '../shared/context.js';
7
+
8
+ export const definitions = [
9
+ { name: 'wp_list_categories', _category: 'taxonomy', description: 'Use to list categories with hierarchy, post count, and descriptions. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 100 }, page: { type: 'number', default: 1 }, parent: { type: 'number' }, search: { type: 'string' }, orderby: { type: 'string', default: 'name' }, hide_empty: { type: 'boolean', default: false }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
10
+ { name: 'wp_list_tags', _category: 'taxonomy', description: 'Use to list tags with post count. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 100 }, page: { type: 'number', default: 1 }, search: { type: 'string' }, orderby: { type: 'string', default: 'name' }, hide_empty: { type: 'boolean', default: false }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
11
+ { name: 'wp_create_taxonomy_term', _category: 'taxonomy', description: 'Use to create a new category or tag. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { taxonomy: { type: 'string' }, name: { type: 'string' }, slug: { type: 'string' }, description: { type: 'string' }, parent: { type: 'number' } }, required: ['taxonomy', 'name'] }},
12
+ { name: 'wp_list_post_types', _category: 'taxonomy', description: 'Use to discover all registered post types including custom ones (products, portfolio, events). Read-only.', inputSchema: { type: 'object', properties: {} }},
13
+ { name: 'wp_list_custom_posts', _category: 'taxonomy', description: 'Use to list any custom post type (products, portfolio, events). Requires post_type slug. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { post_type: { type: 'string' }, per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, status: { type: 'string', default: 'publish' }, orderby: { type: 'string', default: 'date' }, order: { type: 'string', default: 'desc' }, search: { type: 'string' }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }, required: ['post_type'] }}
14
+ ];
15
+
16
+ export const handlers = {};
17
+
18
+ handlers['wp_list_categories'] = async (args) => {
19
+ const t0 = Date.now();
20
+ let result;
21
+ const { wpApiCall, auditLog, name } = rt;
22
+ const { per_page = 100, page = 1, parent, search, orderby = 'name', hide_empty = false, mode = 'full' } = args;
23
+ let ep = `/categories?per_page=${per_page}&page=${page}&orderby=${orderby}&hide_empty=${hide_empty}`;
24
+ if (parent !== undefined) ep += `&parent=${parent}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
25
+ const cats = await wpApiCall(ep);
26
+ const catPg = cats._wpTotal !== undefined ? buildPaginationMeta(cats._wpTotal, page, per_page) : undefined;
27
+ if (mode === 'ids_only') {
28
+ result = json({ total: cats.length, mode: 'ids_only', ids: cats.map(c => c.id), ...(catPg && { pagination: catPg }) });
29
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
30
+ return result;
31
+ }
32
+ if (mode === 'summary') {
33
+ result = json({ total: cats.length, mode: 'summary', categories: cats.map(c => ({ id: c.id, name: c.name, slug: c.slug, count: c.count, parent: c.parent })), ...(catPg && { pagination: catPg }) });
34
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
35
+ return result;
36
+ }
37
+ result = json({ total: cats.length, categories: cats.map(c => ({ id: c.id, name: c.name, slug: c.slug, description: c.description, parent: c.parent, count: c.count })), ...(catPg && { pagination: catPg }) });
38
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
39
+ return result;
40
+ };
41
+ handlers['wp_list_tags'] = async (args) => {
42
+ const t0 = Date.now();
43
+ let result;
44
+ const { wpApiCall, auditLog, name } = rt;
45
+ const { per_page = 100, page = 1, search, orderby = 'name', hide_empty = false, mode = 'full' } = args;
46
+ let ep = `/tags?per_page=${per_page}&page=${page}&orderby=${orderby}&hide_empty=${hide_empty}`;
47
+ if (search) ep += `&search=${encodeURIComponent(search)}`;
48
+ const tags = await wpApiCall(ep);
49
+ const tagPg = tags._wpTotal !== undefined ? buildPaginationMeta(tags._wpTotal, page, per_page) : undefined;
50
+ if (mode === 'ids_only') {
51
+ result = json({ total: tags.length, mode: 'ids_only', ids: tags.map(t => t.id), ...(tagPg && { pagination: tagPg }) });
52
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
53
+ return result;
54
+ }
55
+ if (mode === 'summary') {
56
+ result = json({ total: tags.length, mode: 'summary', tags: tags.map(t => ({ id: t.id, name: t.name, slug: t.slug, count: t.count })), ...(tagPg && { pagination: tagPg }) });
57
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
58
+ return result;
59
+ }
60
+ result = json({ total: tags.length, tags: tags.map(t => ({ id: t.id, name: t.name, slug: t.slug, count: t.count })), ...(tagPg && { pagination: tagPg }) });
61
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
62
+ return result;
63
+ };
64
+ handlers['wp_create_taxonomy_term'] = async (args) => {
65
+ const t0 = Date.now();
66
+ let result;
67
+ const { wpApiCall, auditLog, name } = rt;
68
+ validateInput(args, { taxonomy: { type: 'string', required: true, enum: ['category', 'tag'] }, name: { type: 'string', required: true } });
69
+ const { taxonomy, name: tName, slug, description, parent } = args;
70
+ const ep = taxonomy === 'category' ? '/categories' : '/tags';
71
+ const data = { name: tName }; if (slug) data.slug = slug; if (description) data.description = description;
72
+ if (parent && taxonomy === 'category') data.parent = parent;
73
+ const t = await wpApiCall(ep, { method: 'POST', body: JSON.stringify(data) });
74
+ result = json({ success: true, message: `${taxonomy} "${tName}" created`, term: { id: t.id, name: t.name, slug: t.slug } });
75
+ auditLog({ tool: name, target: t.id, target_type: taxonomy, action: 'create', status: 'success', latency_ms: Date.now() - t0, params: { name: tName } });
76
+ return result;
77
+ };
78
+ handlers['wp_list_post_types'] = async (args) => {
79
+ const t0 = Date.now();
80
+ let result;
81
+ const { wpApiCall, auditLog, name } = rt;
82
+ const types = await wpApiCall('/types');
83
+ result = json({ total: Object.keys(types).length, post_types: Object.values(types).map(t => ({ slug: t.slug, name: t.name, description: t.description, hierarchical: t.hierarchical, rest_base: t.rest_base })) });
84
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
85
+ return result;
86
+ };
87
+ handlers['wp_list_custom_posts'] = async (args) => {
88
+ const t0 = Date.now();
89
+ let result;
90
+ const { wpApiCall, auditLog, name, enforceAllowedTypes, ORDERS } = rt;
91
+ validateInput(args, { post_type: { type: 'string', required: true }, per_page: { type: 'number', min: 1, max: 100 }, page: { type: 'number', min: 1 }, order: { type: 'string', enum: ORDERS }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'] } });
92
+ enforceAllowedTypes(args.post_type);
93
+ const { post_type, per_page = 10, page = 1, status = 'publish', orderby = 'date', order = 'desc', search, mode = 'full' } = args;
94
+ const types = await wpApiCall('/types');
95
+ const typeInfo = Object.values(types).find(t => t.slug === post_type || t.rest_base === post_type);
96
+ if (!typeInfo) throw new Error(`Post type "${post_type}" not found.`);
97
+ const restBase = typeInfo.rest_base || post_type;
98
+ let ep = `/${restBase}?per_page=${per_page}&page=${page}&status=${status}&orderby=${orderby}&order=${order}`;
99
+ if (search) ep += `&search=${encodeURIComponent(search)}`;
100
+ const posts = await wpApiCall(ep);
101
+ const cpPg = posts._wpTotal !== undefined ? buildPaginationMeta(posts._wpTotal, page, per_page) : undefined;
102
+ if (mode === 'ids_only') {
103
+ result = json({ post_type, total: posts.length, page, mode: 'ids_only', ids: posts.map(p => p.id), ...(cpPg && { pagination: cpPg }) });
104
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: { post_type } });
105
+ return result;
106
+ }
107
+ if (mode === 'summary') {
108
+ result = json({ post_type, total: posts.length, page, mode: 'summary', posts: posts.map(p => ({ id: p.id, title: p.title?.rendered || p.title, slug: p.slug, date: p.date, status: p.status, link: p.link })), ...(cpPg && { pagination: cpPg }) });
109
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: { post_type } });
110
+ return result;
111
+ }
112
+ result = json({ post_type, total: posts.length, page, posts: posts.map(p => ({ id: p.id, title: p.title?.rendered || p.title, status: p.status, date: p.date, link: p.link, slug: p.slug, type: p.type, meta: p.meta || {} })), ...(cpPg && { pagination: cpPg }) });
113
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: { post_type } });
114
+ return result;
115
+ };
@@ -0,0 +1,188 @@
1
+ // src/tools/users.js — users tools (10)
2
+ // Definitions + handlers (v5.0.0 refactor Step B+C)
3
+
4
+ import { json, buildPaginationMeta } from '../shared/utils.js';
5
+ import { validateInput } from '../shared/governance.js';
6
+ import { rt } from '../shared/context.js';
7
+
8
+ export const definitions = [
9
+ { name: 'wp_list_users', _category: 'users', description: 'Use to list site users with roles. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, roles: { type: 'string' }, search: { type: 'string' }, orderby: { type: 'string', default: 'name' }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
10
+ { name: 'wp_get_user', _category: 'users', description: 'Use to get a user profile by ID. Returns login, email, role, meta, registration date. Read-only. Requires list_users capability.',
11
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'User ID' }, context: { type: 'string', enum: ['view', 'edit'], description: 'edit returns email and meta (requires edit_users)' } }, required: ['id'] }},
12
+ { name: 'wp_create_user', _category: 'users', description: 'Use to create a new WordPress user. Requires create_users capability. Write — blocked by WP_READ_ONLY. Requires confirm=true to prevent accidental creation.',
13
+ inputSchema: { type: 'object', properties: { username: { type: 'string' }, email: { type: 'string' }, password: { type: 'string' }, role: { type: 'string', description: 'Role slug (subscriber, contributor, author, editor, administrator)' }, first_name: { type: 'string' }, last_name: { type: 'string' }, url: { type: 'string' }, description: { type: 'string', description: 'User bio' }, meta: { type: 'object', description: 'Custom user meta' }, confirm: { type: 'boolean', description: 'Must be true to execute. Safety guard against accidental creation.' } }, required: ['username', 'email', 'password', 'confirm'] }},
14
+ { name: 'wp_update_user', _category: 'users', description: 'Use to update a user profile. Only provided fields are modified. Write — blocked by WP_READ_ONLY.',
15
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, email: { type: 'string' }, display_name: { type: 'string' }, first_name: { type: 'string' }, last_name: { type: 'string' }, role: { type: 'string' }, url: { type: 'string' }, description: { type: 'string', description: 'User bio' }, meta: { type: 'object' }, nickname: { type: 'string' }, locale: { type: 'string' } }, required: ['id'] }},
16
+ { name: 'wp_delete_user', _category: 'users', description: 'Use to delete a user permanently. Requires delete_users capability. Destructive — blocked by WP_READ_ONLY, WP_DISABLE_DELETE. Requires confirm=true and reassign to prevent data loss.',
17
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, reassign: { type: 'number', description: 'User ID to reassign posts to (required)' }, confirm: { type: 'boolean', description: 'Must be true to execute. Safety guard against accidental deletion.' } }, required: ['id', 'reassign', 'confirm'] }},
18
+ { name: 'wp_list_user_roles', _category: 'users', description: 'Use to list all available WordPress user roles with their capabilities. Read-only.',
19
+ inputSchema: { type: 'object', properties: {} }},
20
+ { name: 'wp_get_user_capabilities', _category: 'users', description: 'Use to get the capabilities of a specific user (can_publish, manage_options, etc.). Read-only.',
21
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'User ID' } }, required: ['id'] }},
22
+ { name: 'wp_reset_user_password', _category: 'users', description: 'Use to trigger a password reset email for a user. Write — blocked by WP_READ_ONLY. Requires edit_users capability.',
23
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'User ID' } }, required: ['id'] }},
24
+ { name: 'wp_list_user_application_passwords', _category: 'users', description: 'Use to list application passwords for a user. Returns name, UUID, created date, last used. Read-only.',
25
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'User ID' } }, required: ['id'] }},
26
+ { name: 'wp_revoke_application_password', _category: 'users', description: 'Use to revoke (delete) an application password by UUID. Destructive — blocked by WP_READ_ONLY.',
27
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'User ID' }, uuid: { type: 'string', description: 'Application password UUID' } }, required: ['id', 'uuid'] }}
28
+ ];
29
+
30
+ export const handlers = {};
31
+
32
+ handlers['wp_list_users'] = async (args) => {
33
+ const t0 = Date.now();
34
+ let result;
35
+ const { wpApiCall, auditLog, name } = rt;
36
+ const { per_page = 10, page = 1, roles, search, orderby = 'name', mode = 'full' } = args;
37
+ let ep = `/users?per_page=${per_page}&page=${page}&orderby=${orderby}`;
38
+ if (roles) ep += `&roles=${roles}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
39
+ const users = await wpApiCall(ep);
40
+ const usrPg = users._wpTotal !== undefined ? buildPaginationMeta(users._wpTotal, page, per_page) : undefined;
41
+ if (mode === 'ids_only') {
42
+ result = json({ total: users.length, mode: 'ids_only', ids: users.map(u => u.id), ...(usrPg && { pagination: usrPg }) });
43
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
44
+ return result;
45
+ }
46
+ if (mode === 'summary') {
47
+ result = json({ total: users.length, mode: 'summary', users: users.map(u => ({ id: u.id, name: u.name, slug: u.slug, roles: u.roles || [] })), ...(usrPg && { pagination: usrPg }) });
48
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
49
+ return result;
50
+ }
51
+ result = json({ total: users.length, users: users.map(u => ({ id: u.id, name: u.name, slug: u.slug, link: u.link, roles: u.roles, avatar: u.avatar_urls?.['96'] })), ...(usrPg && { pagination: usrPg }) });
52
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
53
+ return result;
54
+ };
55
+ handlers['wp_get_user'] = async (args) => {
56
+ const t0 = Date.now();
57
+ let result;
58
+ const { wpApiCall, auditLog, name } = rt;
59
+ validateInput(args, { id: { type: 'number', required: true, min: 1 }, context: { type: 'string', enum: ['view', 'edit'] } });
60
+ const { id, context = 'edit' } = args;
61
+ const u = await wpApiCall(`/users/${id}?context=${context}`);
62
+ result = json({ id: u.id, username: u.username || u.slug, name: u.name, slug: u.slug, email: u.email || null, roles: u.roles || [], first_name: u.first_name || '', last_name: u.last_name || '', url: u.url || '', description: u.description || '', link: u.link, registered_date: u.registered_date || null, locale: u.locale || '', nickname: u.nickname || '', avatar: u.avatar_urls?.['96'] || null, meta: u.meta || {} });
63
+ auditLog({ tool: name, target: id, target_type: 'user', action: 'read', status: 'success', latency_ms: Date.now() - t0 });
64
+ return result;
65
+ };
66
+ handlers['wp_create_user'] = async (args) => {
67
+ const t0 = Date.now();
68
+ let result;
69
+ const { wpApiCall, auditLog, name } = rt;
70
+ validateInput(args, { username: { type: 'string', required: true }, email: { type: 'string', required: true }, password: { type: 'string', required: true }, confirm: { type: 'boolean', required: true } });
71
+ if (args.confirm !== true) {
72
+ result = json({ error: 'Safety guard: set confirm=true to create a user. This prevents accidental user creation.' });
73
+ auditLog({ tool: name, action: 'create', status: 'blocked', latency_ms: Date.now() - t0, params: { username: args.username }, error: 'confirm not set' });
74
+ return { content: [{ type: 'text', text: 'Error: Safety guard: set confirm=true to create a user.' }], isError: true };
75
+ }
76
+ const { username, email, password, role = 'subscriber', first_name, last_name, url: userUrl, description: bio, meta: userMeta } = args;
77
+ const userData = { username, email, password, roles: [role] };
78
+ if (first_name) userData.first_name = first_name;
79
+ if (last_name) userData.last_name = last_name;
80
+ if (userUrl) userData.url = userUrl;
81
+ if (bio) userData.description = bio;
82
+ if (userMeta) userData.meta = userMeta;
83
+ const nu = await wpApiCall('/users', { method: 'POST', body: JSON.stringify(userData) });
84
+ result = json({ success: true, message: `User "${username}" created`, user: { id: nu.id, username: nu.username || nu.slug, email: nu.email, roles: nu.roles, name: nu.name } });
85
+ auditLog({ tool: name, target: nu.id, target_type: 'user', action: 'create', status: 'success', latency_ms: Date.now() - t0, params: { username, email, role } });
86
+ return result;
87
+ };
88
+ handlers['wp_update_user'] = async (args) => {
89
+ const t0 = Date.now();
90
+ let result;
91
+ const { wpApiCall, auditLog, sanitizeParams, name } = rt;
92
+ validateInput(args, { id: { type: 'number', required: true, min: 1 } });
93
+ const { id, ...userUpdates } = args;
94
+ const updateData = {};
95
+ if (userUpdates.email) updateData.email = userUpdates.email;
96
+ if (userUpdates.display_name) updateData.name = userUpdates.display_name;
97
+ if (userUpdates.first_name) updateData.first_name = userUpdates.first_name;
98
+ if (userUpdates.last_name) updateData.last_name = userUpdates.last_name;
99
+ if (userUpdates.role) updateData.roles = [userUpdates.role];
100
+ if (userUpdates.url) updateData.url = userUpdates.url;
101
+ if (userUpdates.description) updateData.description = userUpdates.description;
102
+ if (userUpdates.nickname) updateData.nickname = userUpdates.nickname;
103
+ if (userUpdates.locale) updateData.locale = userUpdates.locale;
104
+ if (userUpdates.meta) updateData.meta = userUpdates.meta;
105
+ const uu = await wpApiCall(`/users/${id}`, { method: 'POST', body: JSON.stringify(updateData) });
106
+ result = json({ success: true, message: `User ${id} updated`, user: { id: uu.id, name: uu.name, email: uu.email, roles: uu.roles, slug: uu.slug } });
107
+ auditLog({ tool: name, target: id, target_type: 'user', action: 'update', status: 'success', latency_ms: Date.now() - t0, params: sanitizeParams(updateData) });
108
+ return result;
109
+ };
110
+ handlers['wp_delete_user'] = async (args) => {
111
+ const t0 = Date.now();
112
+ let result;
113
+ const { wpApiCall, auditLog, name } = rt;
114
+ validateInput(args, { id: { type: 'number', required: true, min: 1 }, reassign: { type: 'number', required: true, min: 1 }, confirm: { type: 'boolean', required: true } });
115
+ if (args.confirm !== true) {
116
+ return { content: [{ type: 'text', text: 'Error: Safety guard: set confirm=true and reassign=<user_id> to delete a user. This permanently removes the account.' }], isError: true };
117
+ }
118
+ const { id, reassign } = args;
119
+ await wpApiCall(`/users/${id}?force=true&reassign=${reassign}`, { method: 'DELETE' });
120
+ result = json({ success: true, message: `User ${id} permanently deleted, content reassigned to user ${reassign}` });
121
+ auditLog({ tool: name, target: id, target_type: 'user', action: 'delete', status: 'success', latency_ms: Date.now() - t0, params: { id, reassign } });
122
+ return result;
123
+ };
124
+ handlers['wp_list_user_roles'] = async (args) => {
125
+ const t0 = Date.now();
126
+ let result;
127
+ const { wpApiCall, auditLog, name } = rt;
128
+ const rolesData = await wpApiCall('/roles', { basePath: '/wp-json/wp/v2/users' });
129
+ // WP REST doesn't have a /roles endpoint natively; fall back to reading from site info
130
+ if (Array.isArray(rolesData)) {
131
+ result = json({ total: rolesData.length, roles: rolesData.map(r => ({ slug: r.slug || r.name, name: r.name, capabilities: r.capabilities || {} })) });
132
+ } else if (rolesData && typeof rolesData === 'object') {
133
+ const rolesList = Object.entries(rolesData).map(([slug, data]) => ({ slug, name: data.name || slug, capabilities: data.capabilities || {} }));
134
+ result = json({ total: rolesList.length, roles: rolesList });
135
+ } else {
136
+ result = json({ total: 0, roles: [], note: 'Roles endpoint not available. Use wp_site_info or check WP version.' });
137
+ }
138
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
139
+ return result;
140
+ };
141
+ handlers['wp_get_user_capabilities'] = async (args) => {
142
+ const t0 = Date.now();
143
+ let result;
144
+ const { wpApiCall, auditLog, name } = rt;
145
+ validateInput(args, { id: { type: 'number', required: true, min: 1 } });
146
+ const capUser = await wpApiCall(`/users/${args.id}?context=edit`);
147
+ const caps = capUser.capabilities || {};
148
+ const activeCaps = Object.entries(caps).filter(([, v]) => v === true).map(([k]) => k);
149
+ result = json({ id: capUser.id, name: capUser.name, roles: capUser.roles || [], capabilities_count: activeCaps.length, capabilities: activeCaps });
150
+ auditLog({ tool: name, target: args.id, target_type: 'user', action: 'read', status: 'success', latency_ms: Date.now() - t0 });
151
+ return result;
152
+ };
153
+ handlers['wp_reset_user_password'] = async (args) => {
154
+ const t0 = Date.now();
155
+ let result;
156
+ const { wpApiCall, auditLog, name } = rt;
157
+ validateInput(args, { id: { type: 'number', required: true, min: 1 } });
158
+ // WP REST API: POST /users/{id} with an empty password field doesn't reset.
159
+ // Use the mcp-diagnostics companion endpoint, or generate a reset URL.
160
+ // Standard approach: read user email, then trigger via custom endpoint.
161
+ const resetUser = await wpApiCall(`/users/${args.id}?context=edit`);
162
+ await wpApiCall(`/password-reset`, { basePath: '/wp-json/mcp-diagnostics/v1', method: 'POST', body: JSON.stringify({ user_id: args.id }) });
163
+ result = json({ success: true, message: `Password reset email sent to user ${args.id}`, user: { id: resetUser.id, name: resetUser.name, email: resetUser.email ? resetUser.email.replace(/(.{2}).*(@.*)/, '$1***$2') : 'hidden' } });
164
+ auditLog({ tool: name, target: args.id, target_type: 'user', action: 'password_reset', status: 'success', latency_ms: Date.now() - t0 });
165
+ return result;
166
+ };
167
+ handlers['wp_list_user_application_passwords'] = async (args) => {
168
+ const t0 = Date.now();
169
+ let result;
170
+ const { wpApiCall, auditLog, name } = rt;
171
+ validateInput(args, { id: { type: 'number', required: true, min: 1 } });
172
+ const appPasswords = await wpApiCall(`/users/${args.id}/application-passwords`);
173
+ const passwords = Array.isArray(appPasswords) ? appPasswords : [];
174
+ result = json({ user_id: args.id, total: passwords.length, application_passwords: passwords.map(ap => ({ uuid: ap.uuid, name: ap.name, created: ap.created, last_used: ap.last_used || null, last_ip: ap.last_ip || null })) });
175
+ auditLog({ tool: name, target: args.id, target_type: 'user', action: 'list_app_passwords', status: 'success', latency_ms: Date.now() - t0 });
176
+ return result;
177
+ };
178
+ handlers['wp_revoke_application_password'] = async (args) => {
179
+ const t0 = Date.now();
180
+ let result;
181
+ const { wpApiCall, auditLog, name } = rt;
182
+ validateInput(args, { id: { type: 'number', required: true, min: 1 }, uuid: { type: 'string', required: true } });
183
+ const { id, uuid } = args;
184
+ await wpApiCall(`/users/${id}/application-passwords/${encodeURIComponent(uuid)}`, { method: 'DELETE' });
185
+ result = json({ success: true, message: `Application password ${uuid} revoked for user ${id}` });
186
+ auditLog({ tool: name, target: id, target_type: 'user', action: 'revoke_app_password', status: 'success', latency_ms: Date.now() - t0, params: { uuid } });
187
+ return result;
188
+ };