@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.
Files changed (48) hide show
  1. package/.env.example +18 -0
  2. package/README.md +851 -499
  3. package/companion/mcp-diagnostics.php +1184 -0
  4. package/dxt/manifest.json +715 -98
  5. package/index.js +166 -4786
  6. package/package.json +14 -6
  7. package/src/data/plugin-performance-data.json +59 -0
  8. package/src/shared/api.js +79 -0
  9. package/src/shared/audit.js +39 -0
  10. package/src/shared/context.js +15 -0
  11. package/src/shared/governance.js +98 -0
  12. package/src/shared/utils.js +148 -0
  13. package/src/tools/comments.js +50 -0
  14. package/src/tools/content.js +353 -0
  15. package/src/tools/core.js +114 -0
  16. package/src/tools/editorial.js +634 -0
  17. package/src/tools/fse.js +370 -0
  18. package/src/tools/health.js +160 -0
  19. package/src/tools/index.js +96 -0
  20. package/src/tools/intelligence.js +2082 -0
  21. package/src/tools/links.js +118 -0
  22. package/src/tools/media.js +71 -0
  23. package/src/tools/performance.js +219 -0
  24. package/src/tools/plugins.js +368 -0
  25. package/src/tools/schema.js +417 -0
  26. package/src/tools/security.js +590 -0
  27. package/src/tools/seo.js +1633 -0
  28. package/src/tools/taxonomy.js +115 -0
  29. package/src/tools/users.js +188 -0
  30. package/src/tools/woocommerce.js +1008 -0
  31. package/src/tools/workflow.js +409 -0
  32. package/src/transport/http.js +39 -0
  33. package/tests/unit/helpers/pagination.test.js +43 -0
  34. package/tests/unit/tools/bulkUpdate.test.js +188 -0
  35. package/tests/unit/tools/diagnostics.test.js +397 -0
  36. package/tests/unit/tools/dynamicFiltering.test.js +100 -8
  37. package/tests/unit/tools/editorialIntelligence.test.js +817 -0
  38. package/tests/unit/tools/fse.test.js +548 -0
  39. package/tests/unit/tools/multilingual.test.js +653 -0
  40. package/tests/unit/tools/performance.test.js +351 -0
  41. package/tests/unit/tools/runWorkflow.test.js +150 -0
  42. package/tests/unit/tools/schema.test.js +477 -0
  43. package/tests/unit/tools/security.test.js +695 -0
  44. package/tests/unit/tools/site.test.js +1 -1
  45. package/tests/unit/tools/users.crud.test.js +399 -0
  46. package/tests/unit/tools/validateBlocks.test.js +186 -0
  47. package/tests/unit/tools/visualStaging.test.js +271 -0
  48. package/tests/unit/tools/woocommerce.advanced.test.js +679 -0
@@ -0,0 +1,370 @@
1
+ // src/tools/fse.js — fse tools (26)
2
+ // Definitions + handlers (v5.0.0 refactor Step B+C)
3
+
4
+ import { json } 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_templates', _category: 'fse', description: 'Use to list all block templates. Supports filtering by post_type. Read-only.',
10
+ inputSchema: { type: 'object', properties: { per_page: { type: 'number', description: 'Results per page (1-100)' }, page: { type: 'number', description: 'Page number' }, post_type: { type: 'string', description: 'Filter by post type' } }}},
11
+ { name: 'wp_get_template', _category: 'fse', description: 'Use to get a single block template by ID. Template IDs are strings like "theme-slug//template-name". Read-only.',
12
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Template ID (e.g. "theme-slug//template-name")' } }, required: ['id'] }},
13
+ { name: 'wp_create_template', _category: 'fse', description: 'Use to create a new block template. Write — blocked by WP_READ_ONLY.',
14
+ inputSchema: { type: 'object', properties: { slug: { type: 'string', description: 'Template slug' }, title: { type: 'string', description: 'Template title' }, content: { type: 'string', description: 'Block markup content' }, description: { type: 'string' }, status: { type: 'string', enum: ['publish', 'draft'] } }, required: ['slug'] }},
15
+ { name: 'wp_update_template', _category: 'fse', description: 'Use to update an existing block template. Write — blocked by WP_READ_ONLY.',
16
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Template ID' }, title: { type: 'string' }, content: { type: 'string' }, description: { type: 'string' }, status: { type: 'string', enum: ['publish', 'draft'] } }, required: ['id'] }},
17
+ { name: 'wp_delete_template', _category: 'fse', description: 'Use to delete a block template. Destructive — blocked by WP_DISABLE_DELETE.',
18
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Template ID' }, force: { type: 'boolean', description: 'Force permanent deletion' } }, required: ['id'] }},
19
+ { name: 'wp_list_template_parts', _category: 'fse', description: 'Use to list all template parts. Supports filtering by area (header/footer/general). Read-only.',
20
+ inputSchema: { type: 'object', properties: { per_page: { type: 'number' }, page: { type: 'number' }, area: { type: 'string', enum: ['header', 'footer', 'general'], description: 'Filter by area' } }}},
21
+ { name: 'wp_get_template_part', _category: 'fse', description: 'Use to get a single template part by ID. Read-only.',
22
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Template part ID' } }, required: ['id'] }},
23
+ { name: 'wp_create_template_part', _category: 'fse', description: 'Use to create a new template part. Write — blocked by WP_READ_ONLY.',
24
+ inputSchema: { type: 'object', properties: { slug: { type: 'string' }, title: { type: 'string' }, content: { type: 'string' }, area: { type: 'string', enum: ['header', 'footer', 'general'] }, description: { type: 'string' }, status: { type: 'string', enum: ['publish', 'draft'] } }, required: ['slug'] }},
25
+ { name: 'wp_update_template_part', _category: 'fse', description: 'Use to update an existing template part. Write — blocked by WP_READ_ONLY.',
26
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Template part ID' }, title: { type: 'string' }, content: { type: 'string' }, area: { type: 'string', enum: ['header', 'footer', 'general'] }, description: { type: 'string' }, status: { type: 'string', enum: ['publish', 'draft'] } }, required: ['id'] }},
27
+ { name: 'wp_delete_template_part', _category: 'fse', description: 'Use to delete a template part. Destructive — blocked by WP_DISABLE_DELETE.',
28
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Template part ID' }, force: { type: 'boolean' } }, required: ['id'] }},
29
+ { name: 'wp_get_global_styles', _category: 'fse', description: 'Use to get global styles by ID. Read-only.',
30
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Global styles post ID' } }, required: ['id'] }},
31
+ { name: 'wp_update_global_styles', _category: 'fse', description: 'Use to update global styles (colors, typography, spacing etc). Write — blocked by WP_READ_ONLY.',
32
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Global styles post ID' }, styles: { type: 'object', description: 'Styles object (colors, typography, spacing, etc.)' }, settings: { type: 'object', description: 'Settings object (color palette, font families, etc.)' }, title: { type: 'string' } }, required: ['id'] }},
33
+ { name: 'wp_get_global_styles_variations', _category: 'fse', description: 'Use to list available global style variations for a theme. Read-only.',
34
+ inputSchema: { type: 'object', properties: { stylesheet: { type: 'string', description: 'Theme stylesheet slug' } }, required: ['stylesheet'] }},
35
+ { name: 'wp_list_block_patterns', _category: 'fse', description: 'Use to list all registered block patterns. Read-only.',
36
+ inputSchema: { type: 'object', properties: {} }},
37
+ { name: 'wp_get_block_pattern', _category: 'fse', description: 'Use to get a single block pattern by name. Filters from the full list. Read-only.',
38
+ inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Pattern name (e.g. "core/query-standard-posts")' } }, required: ['name'] }},
39
+ { name: 'wp_create_block_pattern', _category: 'fse', description: 'Use to create a custom block pattern. Write — blocked by WP_READ_ONLY.',
40
+ inputSchema: { type: 'object', properties: { title: { type: 'string' }, content: { type: 'string', description: 'Block markup' }, name: { type: 'string', description: 'Unique pattern name' }, description: { type: 'string' }, categories: { type: 'array', items: { type: 'string' }, description: 'Pattern categories' }, keywords: { type: 'array', items: { type: 'string' }, description: 'Search keywords' } }, required: ['title', 'content', 'name'] }},
41
+ { name: 'wp_delete_block_pattern', _category: 'fse', description: 'Use to delete a custom block pattern. Destructive — blocked by WP_DISABLE_DELETE.',
42
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Block pattern post ID' }, force: { type: 'boolean' } }, required: ['id'] }},
43
+ { name: 'wp_list_navigation_menus', _category: 'fse', description: 'Use to list navigation menus. Supports search and status filtering. Read-only.',
44
+ inputSchema: { type: 'object', properties: { per_page: { type: 'number' }, page: { type: 'number' }, status: { type: 'string', enum: ['publish', 'draft', 'any'] }, search: { type: 'string' } }}},
45
+ { name: 'wp_get_navigation_menu', _category: 'fse', description: 'Use to get a single navigation menu by ID. Read-only.',
46
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Navigation menu post ID' } }, required: ['id'] }},
47
+ { name: 'wp_create_navigation_menu', _category: 'fse', description: 'Use to create a navigation menu. Write — blocked by WP_READ_ONLY.',
48
+ inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Menu title' }, content: { type: 'string', description: 'Block markup for menu items' }, status: { type: 'string', enum: ['publish', 'draft'] } }, required: ['title'] }},
49
+ { name: 'wp_update_navigation_menu', _category: 'fse', description: 'Use to update a navigation menu. Write — blocked by WP_READ_ONLY.',
50
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Navigation menu post ID' }, title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string', enum: ['publish', 'draft'] } }, required: ['id'] }},
51
+ { name: 'wp_delete_navigation_menu', _category: 'fse', description: 'Use to delete a navigation menu. Destructive — blocked by WP_DISABLE_DELETE.',
52
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, force: { type: 'boolean' } }, required: ['id'] }},
53
+ { name: 'wp_list_widgets', _category: 'fse', description: 'Use to list all widgets. Supports filtering by sidebar ID. Read-only.',
54
+ inputSchema: { type: 'object', properties: { sidebar: { type: 'string', description: 'Sidebar ID to filter by' } }}},
55
+ { name: 'wp_get_widget', _category: 'fse', description: 'Use to get a single widget by ID. Read-only.',
56
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Widget ID' } }, required: ['id'] }},
57
+ { name: 'wp_update_widget', _category: 'fse', description: 'Use to update a widget. Write — blocked by WP_READ_ONLY.',
58
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Widget ID' }, id_base: { type: 'string', description: 'Widget type base ID' }, sidebar: { type: 'string', description: 'Target sidebar ID' }, instance: { type: 'object', description: 'Widget settings object' }, rendered: { type: 'string', description: 'Block content for block-based widgets' } }, required: ['id'] }},
59
+ { name: 'wp_delete_widget', _category: 'fse', description: 'Use to delete a widget. Destructive — blocked by WP_DISABLE_DELETE.',
60
+ inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Widget ID' }, force: { type: 'boolean' } }, required: ['id'] }}
61
+ ];
62
+
63
+ export const handlers = {};
64
+
65
+ handlers['wp_list_templates'] = async (args) => {
66
+ const t0 = Date.now();
67
+ let result;
68
+ const { wpApiCall, auditLog, sanitizeParams, name } = rt;
69
+ validateInput(args, { per_page: { type: 'number', min: 1, max: 100 }, page: { type: 'number', min: 1 }, post_type: { type: 'string' } });
70
+ const { per_page = 10, page = 1, post_type } = args;
71
+ let ep = `/templates?per_page=${per_page}&page=${page}`;
72
+ if (post_type) ep += `&post_type=${encodeURIComponent(post_type)}`;
73
+ const templates = await wpApiCall(ep);
74
+ result = json({ total: templates.length, page, templates: templates.map(t => ({ id: t.id, slug: t.slug, title: t.title?.rendered || t.title?.raw || t.title, description: t.description, status: t.status, type: t.type, theme: t.theme, has_theme_file: t.has_theme_file })) });
75
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: sanitizeParams(args) });
76
+ return result;
77
+ };
78
+ handlers['wp_get_template'] = async (args) => {
79
+ const t0 = Date.now();
80
+ let result;
81
+ const { wpApiCall, auditLog, name } = rt;
82
+ validateInput(args, { id: { type: 'string', required: true } });
83
+ const t = await wpApiCall(`/templates/${encodeURIComponent(args.id)}`);
84
+ result = json({ id: t.id, slug: t.slug, title: t.title?.rendered || t.title?.raw || t.title, content: t.content?.raw || t.content?.rendered || '', description: t.description, status: t.status, type: t.type, theme: t.theme, has_theme_file: t.has_theme_file, modified: t.modified });
85
+ auditLog({ tool: name, action: 'get', status: 'success', latency_ms: Date.now() - t0, params: { id: args.id } });
86
+ return result;
87
+ };
88
+ handlers['wp_create_template'] = async (args) => {
89
+ const t0 = Date.now();
90
+ let result;
91
+ const { wpApiCall, auditLog, name } = rt;
92
+ validateInput(args, { slug: { type: 'string', required: true } });
93
+ const { slug, title, content, description, status = 'publish' } = args;
94
+ const data = { slug };
95
+ if (title) data.title = title; if (content) data.content = content; if (description) data.description = description; if (status) data.status = status;
96
+ const nt = await wpApiCall('/templates', { method: 'POST', body: JSON.stringify(data) });
97
+ result = json({ success: true, message: 'Template created', template: { id: nt.id, slug: nt.slug, title: nt.title?.rendered || nt.title?.raw || nt.title, status: nt.status } });
98
+ auditLog({ tool: name, action: 'create', status: 'success', latency_ms: Date.now() - t0, params: { slug, status } });
99
+ return result;
100
+ };
101
+ handlers['wp_update_template'] = async (args) => {
102
+ const t0 = Date.now();
103
+ let result;
104
+ const { wpApiCall, auditLog, name } = rt;
105
+ validateInput(args, { id: { type: 'string', required: true } });
106
+ const { id, title, content, description, status } = args;
107
+ const data = {};
108
+ if (title) data.title = title; if (content) data.content = content; if (description) data.description = description; if (status) data.status = status;
109
+ const ut = await wpApiCall(`/templates/${encodeURIComponent(id)}`, { method: 'POST', body: JSON.stringify(data) });
110
+ result = json({ success: true, message: 'Template updated', template: { id: ut.id, slug: ut.slug, title: ut.title?.rendered || ut.title?.raw || ut.title, status: ut.status } });
111
+ auditLog({ tool: name, action: 'update', status: 'success', latency_ms: Date.now() - t0, params: { id, ...data } });
112
+ return result;
113
+ };
114
+ handlers['wp_delete_template'] = async (args) => {
115
+ const t0 = Date.now();
116
+ let result;
117
+ const { wpApiCall, auditLog, name } = rt;
118
+ validateInput(args, { id: { type: 'string', required: true } });
119
+ const { id, force = true } = args;
120
+ await wpApiCall(`/templates/${encodeURIComponent(id)}?force=${force}`, { method: 'DELETE' });
121
+ result = json({ success: true, message: `Template "${id}" deleted` });
122
+ auditLog({ tool: name, action: 'delete', status: 'success', latency_ms: Date.now() - t0, params: { id, force } });
123
+ return result;
124
+ };
125
+ handlers['wp_list_template_parts'] = async (args) => {
126
+ const t0 = Date.now();
127
+ let result;
128
+ const { wpApiCall, auditLog, sanitizeParams, name } = rt;
129
+ validateInput(args, { per_page: { type: 'number', min: 1, max: 100 }, page: { type: 'number', min: 1 }, area: { type: 'string', enum: ['header', 'footer', 'general'] } });
130
+ const { per_page = 10, page = 1, area } = args;
131
+ let ep = `/template-parts?per_page=${per_page}&page=${page}`;
132
+ if (area) ep += `&area=${encodeURIComponent(area)}`;
133
+ const parts = await wpApiCall(ep);
134
+ result = json({ total: parts.length, page, template_parts: parts.map(t => ({ id: t.id, slug: t.slug, title: t.title?.rendered || t.title?.raw || t.title, area: t.area, description: t.description, status: t.status, theme: t.theme, has_theme_file: t.has_theme_file })) });
135
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: sanitizeParams(args) });
136
+ return result;
137
+ };
138
+ handlers['wp_get_template_part'] = async (args) => {
139
+ const t0 = Date.now();
140
+ let result;
141
+ const { wpApiCall, auditLog, name } = rt;
142
+ validateInput(args, { id: { type: 'string', required: true } });
143
+ const tp = await wpApiCall(`/template-parts/${encodeURIComponent(args.id)}`);
144
+ result = json({ id: tp.id, slug: tp.slug, title: tp.title?.rendered || tp.title?.raw || tp.title, content: tp.content?.raw || tp.content?.rendered || '', area: tp.area, description: tp.description, status: tp.status, theme: tp.theme, has_theme_file: tp.has_theme_file, modified: tp.modified });
145
+ auditLog({ tool: name, action: 'get', status: 'success', latency_ms: Date.now() - t0, params: { id: args.id } });
146
+ return result;
147
+ };
148
+ handlers['wp_create_template_part'] = async (args) => {
149
+ const t0 = Date.now();
150
+ let result;
151
+ const { wpApiCall, auditLog, name } = rt;
152
+ validateInput(args, { slug: { type: 'string', required: true } });
153
+ const { slug, title, content, area, description, status = 'publish' } = args;
154
+ const data = { slug };
155
+ if (title) data.title = title; if (content) data.content = content; if (area) data.area = area; if (description) data.description = description; if (status) data.status = status;
156
+ const ntp = await wpApiCall('/template-parts', { method: 'POST', body: JSON.stringify(data) });
157
+ result = json({ success: true, message: 'Template part created', template_part: { id: ntp.id, slug: ntp.slug, title: ntp.title?.rendered || ntp.title?.raw || ntp.title, area: ntp.area, status: ntp.status } });
158
+ auditLog({ tool: name, action: 'create', status: 'success', latency_ms: Date.now() - t0, params: { slug, area, status } });
159
+ return result;
160
+ };
161
+ handlers['wp_update_template_part'] = async (args) => {
162
+ const t0 = Date.now();
163
+ let result;
164
+ const { wpApiCall, auditLog, name } = rt;
165
+ validateInput(args, { id: { type: 'string', required: true } });
166
+ const { id, title, content, area, description, status } = args;
167
+ const data = {};
168
+ if (title) data.title = title; if (content) data.content = content; if (area) data.area = area; if (description) data.description = description; if (status) data.status = status;
169
+ const utp = await wpApiCall(`/template-parts/${encodeURIComponent(id)}`, { method: 'POST', body: JSON.stringify(data) });
170
+ result = json({ success: true, message: 'Template part updated', template_part: { id: utp.id, slug: utp.slug, title: utp.title?.rendered || utp.title?.raw || utp.title, area: utp.area, status: utp.status } });
171
+ auditLog({ tool: name, action: 'update', status: 'success', latency_ms: Date.now() - t0, params: { id, ...data } });
172
+ return result;
173
+ };
174
+ handlers['wp_delete_template_part'] = async (args) => {
175
+ const t0 = Date.now();
176
+ let result;
177
+ const { wpApiCall, auditLog, name } = rt;
178
+ validateInput(args, { id: { type: 'string', required: true } });
179
+ const { id, force = true } = args;
180
+ await wpApiCall(`/template-parts/${encodeURIComponent(id)}?force=${force}`, { method: 'DELETE' });
181
+ result = json({ success: true, message: `Template part "${id}" deleted` });
182
+ auditLog({ tool: name, action: 'delete', status: 'success', latency_ms: Date.now() - t0, params: { id, force } });
183
+ return result;
184
+ };
185
+ handlers['wp_get_global_styles'] = async (args) => {
186
+ const t0 = Date.now();
187
+ let result;
188
+ const { wpApiCall, auditLog, name } = rt;
189
+ validateInput(args, { id: { type: 'number', required: true, min: 1 } });
190
+ const gs = await wpApiCall(`/global-styles/${args.id}`);
191
+ result = json({ id: gs.id, title: gs.title?.rendered || gs.title?.raw || gs.title, styles: gs.styles, settings: gs.settings, modified: gs.modified });
192
+ auditLog({ tool: name, action: 'get', status: 'success', latency_ms: Date.now() - t0, params: { id: args.id } });
193
+ return result;
194
+ };
195
+ handlers['wp_update_global_styles'] = async (args) => {
196
+ const t0 = Date.now();
197
+ let result;
198
+ const { wpApiCall, auditLog, name } = rt;
199
+ validateInput(args, { id: { type: 'number', required: true, min: 1 } });
200
+ const { id, styles, settings, title } = args;
201
+ const data = {};
202
+ if (styles) data.styles = styles; if (settings) data.settings = settings; if (title) data.title = title;
203
+ const ugs = await wpApiCall(`/global-styles/${id}`, { method: 'POST', body: JSON.stringify(data) });
204
+ result = json({ success: true, message: 'Global styles updated', global_styles: { id: ugs.id, title: ugs.title?.rendered || ugs.title?.raw || ugs.title, styles: ugs.styles, settings: ugs.settings } });
205
+ auditLog({ tool: name, action: 'update', status: 'success', latency_ms: Date.now() - t0, params: { id } });
206
+ return result;
207
+ };
208
+ handlers['wp_get_global_styles_variations'] = async (args) => {
209
+ const t0 = Date.now();
210
+ let result;
211
+ const { wpApiCall, auditLog, name } = rt;
212
+ validateInput(args, { stylesheet: { type: 'string', required: true } });
213
+ const variations = await wpApiCall(`/global-styles/themes/${encodeURIComponent(args.stylesheet)}/variations`);
214
+ result = json({ stylesheet: args.stylesheet, total: variations.length, variations });
215
+ auditLog({ tool: name, action: 'list_variations', status: 'success', latency_ms: Date.now() - t0, params: { stylesheet: args.stylesheet } });
216
+ return result;
217
+ };
218
+ handlers['wp_list_block_patterns'] = async (args) => {
219
+ const t0 = Date.now();
220
+ let result;
221
+ const { wpApiCall, auditLog, name } = rt;
222
+ const patterns = await wpApiCall('/block-patterns/patterns');
223
+ result = json({ total: patterns.length, patterns: patterns.map(p => ({ name: p.name, title: p.title, description: p.description, categories: p.categories, keywords: p.keywords, blockTypes: p.blockTypes, content: p.content?.substring(0, 200) })) });
224
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
225
+ return result;
226
+ };
227
+ handlers['wp_get_block_pattern'] = async (args) => {
228
+ const t0 = Date.now();
229
+ let result;
230
+ const { wpApiCall, auditLog, name } = rt;
231
+ validateInput(args, { name: { type: 'string', required: true } });
232
+ const allPatterns = await wpApiCall('/block-patterns/patterns');
233
+ const found = allPatterns.find(p => p.name === args.name);
234
+ if (!found) throw new Error(`Block pattern "${args.name}" not found`);
235
+ result = json({ name: found.name, title: found.title, description: found.description, content: found.content, categories: found.categories, keywords: found.keywords, blockTypes: found.blockTypes });
236
+ auditLog({ tool: name, action: 'get', status: 'success', latency_ms: Date.now() - t0, params: { name: args.name } });
237
+ return result;
238
+ };
239
+ handlers['wp_create_block_pattern'] = async (args) => {
240
+ const t0 = Date.now();
241
+ let result;
242
+ const { wpApiCall, auditLog, name } = rt;
243
+ validateInput(args, { title: { type: 'string', required: true }, content: { type: 'string', required: true }, name: { type: 'string', required: true } });
244
+ const { title, content, name: patternName, description, categories, keywords } = args;
245
+ const data = { title, content, name: patternName };
246
+ if (description) data.description = description; if (categories) data.categories = categories; if (keywords) data.keywords = keywords; data.status = 'publish';
247
+ const np = await wpApiCall('/block-patterns', { method: 'POST', body: JSON.stringify(data) });
248
+ result = json({ success: true, message: 'Block pattern created', pattern: { id: np.id, name: np.name || patternName, title: np.title?.rendered || np.title?.raw || np.title || title } });
249
+ auditLog({ tool: name, action: 'create', status: 'success', latency_ms: Date.now() - t0, params: { name: patternName, title } });
250
+ return result;
251
+ };
252
+ handlers['wp_delete_block_pattern'] = async (args) => {
253
+ const t0 = Date.now();
254
+ let result;
255
+ const { wpApiCall, auditLog, name } = rt;
256
+ validateInput(args, { id: { type: 'number', required: true, min: 1 } });
257
+ const { id, force = true } = args;
258
+ await wpApiCall(`/block-patterns/${id}?force=${force}`, { method: 'DELETE' });
259
+ result = json({ success: true, message: `Block pattern ${id} deleted` });
260
+ auditLog({ tool: name, action: 'delete', status: 'success', latency_ms: Date.now() - t0, params: { id, force } });
261
+ return result;
262
+ };
263
+ handlers['wp_list_navigation_menus'] = async (args) => {
264
+ const t0 = Date.now();
265
+ let result;
266
+ const { wpApiCall, auditLog, sanitizeParams, name } = rt;
267
+ validateInput(args, { per_page: { type: 'number', min: 1, max: 100 }, page: { type: 'number', min: 1 }, status: { type: 'string', enum: ['publish', 'draft', 'any'] }, search: { type: 'string' } });
268
+ const { per_page = 10, page = 1, status, search } = args;
269
+ let ep = `/navigation?per_page=${per_page}&page=${page}`;
270
+ if (status) ep += `&status=${status}`;
271
+ if (search) ep += `&search=${encodeURIComponent(search)}`;
272
+ const navs = await wpApiCall(ep);
273
+ result = json({ total: navs.length, page, navigation_menus: navs.map(n => ({ id: n.id, title: n.title?.rendered || n.title?.raw || n.title, status: n.status, date: n.date, modified: n.modified, slug: n.slug })) });
274
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: sanitizeParams(args) });
275
+ return result;
276
+ };
277
+ handlers['wp_get_navigation_menu'] = async (args) => {
278
+ const t0 = Date.now();
279
+ let result;
280
+ const { wpApiCall, auditLog, name } = rt;
281
+ validateInput(args, { id: { type: 'number', required: true, min: 1 } });
282
+ const nav = await wpApiCall(`/navigation/${args.id}`);
283
+ result = json({ id: nav.id, title: nav.title?.rendered || nav.title?.raw || nav.title, content: nav.content?.rendered || nav.content?.raw || '', status: nav.status, date: nav.date, modified: nav.modified, slug: nav.slug });
284
+ auditLog({ tool: name, action: 'get', status: 'success', latency_ms: Date.now() - t0, params: { id: args.id } });
285
+ return result;
286
+ };
287
+ handlers['wp_create_navigation_menu'] = async (args) => {
288
+ const t0 = Date.now();
289
+ let result;
290
+ const { wpApiCall, auditLog, name } = rt;
291
+ validateInput(args, { title: { type: 'string', required: true } });
292
+ const { title, content, status = 'publish' } = args;
293
+ const data = { title };
294
+ if (content) data.content = content; if (status) data.status = status;
295
+ const nn = await wpApiCall('/navigation', { method: 'POST', body: JSON.stringify(data) });
296
+ result = json({ success: true, message: 'Navigation menu created', navigation: { id: nn.id, title: nn.title?.rendered || nn.title?.raw || nn.title, status: nn.status } });
297
+ auditLog({ tool: name, action: 'create', status: 'success', latency_ms: Date.now() - t0, params: { title, status } });
298
+ return result;
299
+ };
300
+ handlers['wp_update_navigation_menu'] = async (args) => {
301
+ const t0 = Date.now();
302
+ let result;
303
+ const { wpApiCall, auditLog, name } = rt;
304
+ validateInput(args, { id: { type: 'number', required: true, min: 1 } });
305
+ const { id, title, content, status } = args;
306
+ const data = {};
307
+ if (title) data.title = title; if (content) data.content = content; if (status) data.status = status;
308
+ const un = await wpApiCall(`/navigation/${id}`, { method: 'POST', body: JSON.stringify(data) });
309
+ result = json({ success: true, message: 'Navigation menu updated', navigation: { id: un.id, title: un.title?.rendered || un.title?.raw || un.title, status: un.status } });
310
+ auditLog({ tool: name, action: 'update', status: 'success', latency_ms: Date.now() - t0, params: { id, ...data } });
311
+ return result;
312
+ };
313
+ handlers['wp_delete_navigation_menu'] = async (args) => {
314
+ const t0 = Date.now();
315
+ let result;
316
+ const { wpApiCall, auditLog, name } = rt;
317
+ validateInput(args, { id: { type: 'number', required: true, min: 1 } });
318
+ const { id, force = true } = args;
319
+ await wpApiCall(`/navigation/${id}?force=${force}`, { method: 'DELETE' });
320
+ result = json({ success: true, message: `Navigation menu ${id} deleted` });
321
+ auditLog({ tool: name, action: 'delete', status: 'success', latency_ms: Date.now() - t0, params: { id, force } });
322
+ return result;
323
+ };
324
+ handlers['wp_list_widgets'] = async (args) => {
325
+ const t0 = Date.now();
326
+ let result;
327
+ const { wpApiCall, auditLog, sanitizeParams, name } = rt;
328
+ validateInput(args, { sidebar: { type: 'string' } });
329
+ const { sidebar } = args;
330
+ let ep = '/widgets';
331
+ if (sidebar) ep += `?sidebar=${encodeURIComponent(sidebar)}`;
332
+ const widgets = await wpApiCall(ep);
333
+ result = json({ total: widgets.length, widgets: widgets.map(w => ({ id: w.id, id_base: w.id_base, sidebar: w.sidebar, rendered: w.rendered?.substring(0, 200) })) });
334
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: sanitizeParams(args) });
335
+ return result;
336
+ };
337
+ handlers['wp_get_widget'] = async (args) => {
338
+ const t0 = Date.now();
339
+ let result;
340
+ const { wpApiCall, auditLog, name } = rt;
341
+ validateInput(args, { id: { type: 'string', required: true } });
342
+ const w = await wpApiCall(`/widgets/${encodeURIComponent(args.id)}`);
343
+ result = json({ id: w.id, id_base: w.id_base, sidebar: w.sidebar, instance: w.instance, rendered: w.rendered });
344
+ auditLog({ tool: name, action: 'get', status: 'success', latency_ms: Date.now() - t0, params: { id: args.id } });
345
+ return result;
346
+ };
347
+ handlers['wp_update_widget'] = async (args) => {
348
+ const t0 = Date.now();
349
+ let result;
350
+ const { wpApiCall, auditLog, name } = rt;
351
+ validateInput(args, { id: { type: 'string', required: true } });
352
+ const { id, id_base, sidebar, instance, rendered } = args;
353
+ const data = {};
354
+ if (id_base) data.id_base = id_base; if (sidebar) data.sidebar = sidebar; if (instance) data.instance = instance; if (rendered) data.rendered = rendered;
355
+ const uw = await wpApiCall(`/widgets/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify(data) });
356
+ result = json({ success: true, message: 'Widget updated', widget: { id: uw.id, id_base: uw.id_base, sidebar: uw.sidebar } });
357
+ auditLog({ tool: name, action: 'update', status: 'success', latency_ms: Date.now() - t0, params: { id, sidebar } });
358
+ return result;
359
+ };
360
+ handlers['wp_delete_widget'] = async (args) => {
361
+ const t0 = Date.now();
362
+ let result;
363
+ const { wpApiCall, auditLog, name } = rt;
364
+ validateInput(args, { id: { type: 'string', required: true } });
365
+ const { id, force = true } = args;
366
+ await wpApiCall(`/widgets/${encodeURIComponent(id)}?force=${force}`, { method: 'DELETE' });
367
+ result = json({ success: true, message: `Widget "${id}" deleted` });
368
+ auditLog({ tool: name, action: 'delete', status: 'success', latency_ms: Date.now() - t0, params: { id, force } });
369
+ return result;
370
+ };
@@ -0,0 +1,160 @@
1
+ // src/tools/health.js — health tools (8)
2
+ // Definitions + handlers (v5.0.0 refactor Step B+C)
3
+
4
+ import { json, strip } 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_get_site_health_status', _category: 'health', description: 'Use to get overall site health score (good/recommended/critical) and issue counts by severity. Read-only. Requires wp-site-health REST endpoint (WP 5.6+).',
10
+ inputSchema: { type: 'object', properties: {} }},
11
+ { name: 'wp_list_site_health_issues', _category: 'health', description: 'Use to list all site health issues with label, description, severity, and type. Read-only.',
12
+ inputSchema: { type: 'object', properties: { severity: { type: 'string', enum: ['critical', 'recommended', 'good'], description: 'Filter by severity' } }}},
13
+ { name: 'wp_get_site_health_info', _category: 'health', description: 'Use to get system info: PHP version, MySQL version, memory limit, active extensions, WP constants. Read-only.',
14
+ inputSchema: { type: 'object', properties: { section: { type: 'string', description: 'Filter by section (e.g. "wp-server", "wp-database", "wp-constants", "wp-paths-sizes")' } }}},
15
+ { name: 'wp_get_debug_log', _category: 'health', description: 'Use to read the last N lines of the WordPress debug.log. Requires the mcp-diagnostics companion mu-plugin. Read-only.',
16
+ inputSchema: { type: 'object', properties: { lines: { type: 'number', description: 'default 100, max 500' }, level: { type: 'string', enum: ['error', 'warning', 'notice', 'all'], description: 'Filter by log level (default all)' } }}},
17
+ { name: 'wp_get_cron_events', _category: 'health', description: 'Use to list all scheduled WP-Cron events with hook, args, schedule, next run, and overdue status. Read-only.',
18
+ inputSchema: { type: 'object', properties: { hook: { type: 'string', description: 'Filter by hook name' } }}},
19
+ { name: 'wp_get_transients', _category: 'health', description: 'Use to list database transients with key, expiration, and approximate size. Supports filtering expired. Read-only.',
20
+ inputSchema: { type: 'object', properties: { filter: { type: 'string', enum: ['all', 'expired', 'active'], description: 'Filter by expiration status (default all)' }, search: { type: 'string', description: 'Search transient keys' }, per_page: { type: 'number', description: 'default 50, max 200' } }}},
21
+ { name: 'wp_check_php_compatibility', _category: 'health', description: 'Use to check each active plugin PHP version requirement vs current PHP. Reports compatible/incompatible/unknown. Read-only.',
22
+ inputSchema: { type: 'object', properties: {} }},
23
+ { name: 'wp_get_active_hooks', _category: 'health', description: 'Use to list registered WordPress actions and filters with callbacks and priorities. Requires mcp-diagnostics companion mu-plugin. Read-only.',
24
+ inputSchema: { type: 'object', properties: { type: { type: 'string', enum: ['actions', 'filters', 'all'], description: 'Hook type (default all)' }, search: { type: 'string', description: 'Search hook names' }, per_page: { type: 'number', description: 'default 50, max 200' } }}}
25
+ ];
26
+
27
+ export const handlers = {};
28
+
29
+ handlers['wp_get_site_health_status'] = async (args) => {
30
+ const t0 = Date.now();
31
+ let result;
32
+ const { wpApiCall, auditLog, name } = rt;
33
+ const tests = await wpApiCall('/tests/background', { basePath: '/wp-json/wp-site-health/v1' });
34
+ const issues = Array.isArray(tests) ? tests : [];
35
+ const counts = { critical: 0, recommended: 0, good: 0 };
36
+ issues.forEach(i => { if (counts[i.status] !== undefined) counts[i.status]++; });
37
+ const total = issues.length;
38
+ const score = total === 0 ? 'good' : counts.critical > 0 ? 'critical' : counts.recommended > 0 ? 'recommended' : 'good';
39
+ result = json({ score, total_issues: total, counts, summary: `${counts.critical} critical, ${counts.recommended} recommended, ${counts.good} good` });
40
+ auditLog({ tool: name, action: 'read', status: 'success', latency_ms: Date.now() - t0 });
41
+ return result;
42
+ };
43
+ handlers['wp_list_site_health_issues'] = async (args) => {
44
+ const t0 = Date.now();
45
+ let result;
46
+ const { wpApiCall, auditLog, name } = rt;
47
+ const { severity } = args;
48
+ const allTests = await wpApiCall('/tests/background', { basePath: '/wp-json/wp-site-health/v1' });
49
+ let issues = Array.isArray(allTests) ? allTests : [];
50
+ if (severity) issues = issues.filter(i => i.status === severity);
51
+ result = json({ total: issues.length, filter: severity || 'all', issues: issues.map(i => ({ label: i.label, status: i.status, badge: i.badge?.label, description: strip(i.description || '').substring(0, 300), actions: strip(i.actions || '').substring(0, 200), test: i.test })) });
52
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: { severity } });
53
+ return result;
54
+ };
55
+ handlers['wp_get_site_health_info'] = async (args) => {
56
+ const t0 = Date.now();
57
+ let result;
58
+ const { wpApiCall, auditLog, name } = rt;
59
+ const { section } = args;
60
+ const info = await wpApiCall('/info', { basePath: '/wp-json/wp-site-health/v1' });
61
+ if (section && info[section]) {
62
+ result = json({ section, label: info[section].label, fields: info[section].fields });
63
+ } else if (section) {
64
+ const available = Object.keys(info).filter(k => typeof info[k] === 'object' && info[k].label);
65
+ throw new Error(`Section "${section}" not found. Available: ${available.join(', ')}`);
66
+ } else {
67
+ const summary = {};
68
+ for (const [key, val] of Object.entries(info)) {
69
+ if (val && typeof val === 'object' && val.label) {
70
+ const fields = val.fields || {};
71
+ const fieldSummary = {};
72
+ for (const [fk, fv] of Object.entries(fields)) {
73
+ fieldSummary[fk] = fv.value || fv.debug || '';
74
+ }
75
+ summary[key] = { label: val.label, fields: fieldSummary };
76
+ }
77
+ }
78
+ result = json(summary);
79
+ }
80
+ auditLog({ tool: name, action: 'read', status: 'success', latency_ms: Date.now() - t0, params: { section } });
81
+ return result;
82
+ };
83
+ handlers['wp_get_debug_log'] = async (args) => {
84
+ const t0 = Date.now();
85
+ let result;
86
+ const { wpApiCall, log, auditLog, name } = rt;
87
+ const { lines = 100, level = 'all' } = args;
88
+ validateInput(args, { lines: { type: 'number', min: 1, max: 500 }, level: { type: 'string', enum: ['error', 'warning', 'notice', 'all'] } });
89
+ const logData = await wpApiCall(`/debug-log?lines=${Math.min(lines, 500)}&level=${encodeURIComponent(level)}`, { basePath: '/wp-json/mcp-diagnostics/v1' });
90
+ result = json({ lines_returned: logData.lines?.length || 0, total_lines: logData.total_lines || 0, level, lines: logData.lines || [], file_size: logData.file_size || null, last_modified: logData.last_modified || null });
91
+ auditLog({ tool: name, action: 'read', status: 'success', latency_ms: Date.now() - t0, params: { lines, level } });
92
+ return result;
93
+ };
94
+ handlers['wp_get_cron_events'] = async (args) => {
95
+ const t0 = Date.now();
96
+ let result;
97
+ const { wpApiCall, auditLog, name } = rt;
98
+ const { hook } = args;
99
+ const cronData = await wpApiCall('/cron-events', { basePath: '/wp-json/mcp-diagnostics/v1' });
100
+ let events = Array.isArray(cronData) ? cronData : cronData.events || [];
101
+ if (hook) events = events.filter(e => e.hook === hook || (e.hook && e.hook.includes(hook)));
102
+ const now = Math.floor(Date.now() / 1000);
103
+ result = json({ total: events.length, filter: hook || null, events: events.map(e => ({ hook: e.hook, args: e.args, schedule: e.schedule || 'single', interval: e.interval || null, next_run: e.next_run, next_run_date: e.next_run ? new Date(e.next_run * 1000).toISOString() : null, overdue: e.next_run ? e.next_run < now : false })) });
104
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: { hook } });
105
+ return result;
106
+ };
107
+ handlers['wp_get_transients'] = async (args) => {
108
+ const t0 = Date.now();
109
+ let result;
110
+ const { wpApiCall, auditLog, name } = rt;
111
+ const { filter = 'all', search, per_page = 50 } = args;
112
+ validateInput(args, { filter: { type: 'string', enum: ['all', 'expired', 'active'] }, per_page: { type: 'number', min: 1, max: 200 } });
113
+ let ep = `/transients?filter=${filter}&per_page=${Math.min(per_page, 200)}`;
114
+ if (search) ep += `&search=${encodeURIComponent(search)}`;
115
+ const transientData = await wpApiCall(ep, { basePath: '/wp-json/mcp-diagnostics/v1' });
116
+ const transients = Array.isArray(transientData) ? transientData : transientData.transients || [];
117
+ result = json({ total: transients.length, filter, transients: transients.map(t => ({ key: t.key, expiration: t.expiration, expiration_date: t.expiration ? new Date(t.expiration * 1000).toISOString() : 'no expiry', expired: t.expiration ? t.expiration < Math.floor(Date.now() / 1000) : false, size_bytes: t.size_bytes || null })) });
118
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: { filter, search } });
119
+ return result;
120
+ };
121
+ handlers['wp_check_php_compatibility'] = async (args) => {
122
+ const t0 = Date.now();
123
+ let result;
124
+ const { wpApiCall, auditLog, name } = rt;
125
+ const plugins = await wpApiCall('/plugins');
126
+ const siteHealth = await wpApiCall('/info', { basePath: '/wp-json/wp-site-health/v1' });
127
+ let phpVersion = 'unknown';
128
+ if (siteHealth?.['wp-server']?.fields?.php_version) {
129
+ phpVersion = siteHealth['wp-server'].fields.php_version.value || siteHealth['wp-server'].fields.php_version.debug || 'unknown';
130
+ }
131
+ const activePlugins = Array.isArray(plugins) ? plugins.filter(p => p.status === 'active') : [];
132
+ const report = activePlugins.map(p => {
133
+ const required = p.requires_php || null;
134
+ let compatible = 'unknown';
135
+ if (required && phpVersion !== 'unknown') {
136
+ const reqParts = required.split('.').map(Number);
137
+ const curParts = phpVersion.split('.').map(Number);
138
+ compatible = curParts[0] > reqParts[0] || (curParts[0] === reqParts[0] && (curParts[1] || 0) >= (reqParts[1] || 0)) ? 'compatible' : 'incompatible';
139
+ }
140
+ return { plugin: p.plugin || p.textdomain, name: p.name, version: p.version, requires_php: required, status: compatible };
141
+ });
142
+ const incompatible = report.filter(r => r.status === 'incompatible').length;
143
+ result = json({ php_version: phpVersion, total_active: report.length, incompatible_count: incompatible, plugins: report });
144
+ auditLog({ tool: name, action: 'check', status: 'success', latency_ms: Date.now() - t0 });
145
+ return result;
146
+ };
147
+ handlers['wp_get_active_hooks'] = async (args) => {
148
+ const t0 = Date.now();
149
+ let result;
150
+ const { wpApiCall, auditLog, name } = rt;
151
+ const { type = 'all', search, per_page = 50 } = args;
152
+ validateInput(args, { type: { type: 'string', enum: ['actions', 'filters', 'all'] }, per_page: { type: 'number', min: 1, max: 200 } });
153
+ let ep = `/hooks?type=${type}&per_page=${Math.min(per_page, 200)}`;
154
+ if (search) ep += `&search=${encodeURIComponent(search)}`;
155
+ const hookData = await wpApiCall(ep, { basePath: '/wp-json/mcp-diagnostics/v1' });
156
+ const hooks = Array.isArray(hookData) ? hookData : hookData.hooks || [];
157
+ result = json({ total: hooks.length, type, hooks: hooks.map(h => ({ name: h.name, type: h.type, callbacks: (h.callbacks || []).map(cb => ({ function: cb.function, priority: cb.priority, accepted_args: cb.accepted_args })) })) });
158
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: { type, search } });
159
+ return result;
160
+ };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Tool Loader — aggregates definitions and handlers from all category modules.
3
+ * v5.0.0 refactor Steps B+C.
4
+ */
5
+
6
+ import * as content from './content.js';
7
+ import * as core from './core.js';
8
+ import * as workflow from './workflow.js';
9
+ import { createWorkflowHandler } from './workflow.js';
10
+ import * as media from './media.js';
11
+ import * as taxonomy from './taxonomy.js';
12
+ import * as comments from './comments.js';
13
+ import * as users from './users.js';
14
+ import * as seo from './seo.js';
15
+ import * as plugins from './plugins.js';
16
+ import * as links from './links.js';
17
+ import * as woocommerce from './woocommerce.js';
18
+ import * as intelligence from './intelligence.js';
19
+ import * as fse from './fse.js';
20
+ import * as health from './health.js';
21
+ import * as performance from './performance.js';
22
+ import * as schema from './schema.js';
23
+ import * as security from './security.js';
24
+ import * as editorial from './editorial.js';
25
+
26
+ /** All tool modules */
27
+ export const toolModules = [
28
+ content, core, workflow, media, taxonomy, comments,
29
+ users, seo, plugins, links, woocommerce, intelligence,
30
+ fse, health, performance, schema, security, editorial,
31
+ ];
32
+
33
+ /** All static definitions (175 tools), unfiltered */
34
+ export const ALL_DEFINITIONS = toolModules.flatMap(m => m.definitions);
35
+
36
+ /** All handlers merged into a single lookup */
37
+ export const allHandlers = Object.assign(
38
+ {},
39
+ content.handlers, core.handlers, workflow.handlers,
40
+ media.handlers, taxonomy.handlers, comments.handlers,
41
+ users.handlers, seo.handlers, plugins.handlers,
42
+ links.handlers, woocommerce.handlers, intelligence.handlers,
43
+ fse.handlers, health.handlers, performance.handlers,
44
+ schema.handlers, security.handlers, editorial.handlers,
45
+ );
46
+
47
+ // Plugin-intelligence tools: only shown when WP_ENABLE_PLUGIN_INTELLIGENCE=true
48
+ const PLUGIN_INTEL_TOOLS = [
49
+ 'wp_get_rendered_head', 'wp_audit_rendered_seo', 'wp_get_pillar_content',
50
+ 'wp_audit_schema_plugins', 'wp_get_seo_score', 'wp_get_twitter_meta'
51
+ ];
52
+
53
+ // Editorial workflow tools: only shown when WP_REQUIRE_APPROVAL=true
54
+ const EDITORIAL_TOOLS = ['wp_submit_for_review', 'wp_approve_post', 'wp_reject_post'];
55
+
56
+ function getEnabledCategories() {
57
+ return process.env.WP_TOOL_CATEGORIES
58
+ ?.split(',')
59
+ .map(s => s.trim().toLowerCase())
60
+ .filter(Boolean) ?? null;
61
+ }
62
+
63
+ /**
64
+ * Return filtered tool definitions — mirrors getFilteredTools() from index.js.
65
+ */
66
+ export function getTools(opts = {}) {
67
+ let tools = ALL_DEFINITIONS.filter(tool => {
68
+ const n = tool.name;
69
+ if (n.startsWith('wc_') && !process.env.WC_CONSUMER_KEY) return false;
70
+ if (EDITORIAL_TOOLS.includes(n) && process.env.WP_REQUIRE_APPROVAL !== 'true') return false;
71
+ if (PLUGIN_INTEL_TOOLS.includes(n) && process.env.WP_ENABLE_PLUGIN_INTELLIGENCE !== 'true') return false;
72
+ return true;
73
+ });
74
+
75
+ if (opts.pluginTools && opts.pluginTools.length > 0) {
76
+ const pluginTools = opts.pluginTools.map(t => t._category ? t : { ...t, _category: 'plugins' });
77
+ tools = [...tools, ...pluginTools];
78
+ }
79
+
80
+ const enabledCats = getEnabledCategories();
81
+ if (enabledCats !== null && enabledCats.length > 0) {
82
+ tools = tools.filter(t => t._category === 'core' || enabledCats.includes(t._category ?? 'core'));
83
+ }
84
+
85
+ return tools;
86
+ }
87
+
88
+ /**
89
+ * Get a handler function by tool name.
90
+ */
91
+ export function getHandler(name) {
92
+ return allHandlers[name] || null;
93
+ }
94
+
95
+ // Late-bind wp_run_workflow to avoid circular import (workflow.js → index.js)
96
+ allHandlers['wp_run_workflow'] = createWorkflowHandler(getHandler);