@adsim/wordpress-mcp-server 4.6.0 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +18 -0
- package/README.md +867 -499
- package/companion/mcp-diagnostics.php +1184 -0
- package/dxt/manifest.json +715 -98
- package/index.js +166 -4786
- package/package.json +14 -6
- package/src/data/plugin-performance-data.json +59 -0
- package/src/plugins/adapters/acf/acfAdapter.js +55 -3
- package/src/shared/api.js +79 -0
- package/src/shared/audit.js +39 -0
- package/src/shared/context.js +15 -0
- package/src/shared/governance.js +98 -0
- package/src/shared/utils.js +148 -0
- package/src/tools/comments.js +50 -0
- package/src/tools/content.js +395 -0
- package/src/tools/core.js +114 -0
- package/src/tools/editorial.js +634 -0
- package/src/tools/fse.js +370 -0
- package/src/tools/health.js +160 -0
- package/src/tools/index.js +96 -0
- package/src/tools/intelligence.js +2082 -0
- package/src/tools/links.js +118 -0
- package/src/tools/media.js +71 -0
- package/src/tools/performance.js +219 -0
- package/src/tools/plugins.js +368 -0
- package/src/tools/schema.js +417 -0
- package/src/tools/security.js +590 -0
- package/src/tools/seo.js +1633 -0
- package/src/tools/taxonomy.js +115 -0
- package/src/tools/users.js +188 -0
- package/src/tools/woocommerce.js +1008 -0
- package/src/tools/workflow.js +409 -0
- package/src/transport/http.js +39 -0
- package/tests/unit/helpers/pagination.test.js +43 -0
- package/tests/unit/plugins/acf/acfAdapter.test.js +43 -5
- package/tests/unit/tools/bulkUpdate.test.js +188 -0
- package/tests/unit/tools/diagnostics.test.js +397 -0
- package/tests/unit/tools/dynamicFiltering.test.js +100 -8
- package/tests/unit/tools/editorialIntelligence.test.js +817 -0
- package/tests/unit/tools/fse.test.js +548 -0
- package/tests/unit/tools/multilingual.test.js +653 -0
- package/tests/unit/tools/performance.test.js +351 -0
- package/tests/unit/tools/postMeta.test.js +105 -0
- package/tests/unit/tools/runWorkflow.test.js +150 -0
- package/tests/unit/tools/schema.test.js +477 -0
- package/tests/unit/tools/security.test.js +695 -0
- package/tests/unit/tools/site.test.js +1 -1
- package/tests/unit/tools/users.crud.test.js +399 -0
- package/tests/unit/tools/validateBlocks.test.js +186 -0
- package/tests/unit/tools/visualStaging.test.js +271 -0
- package/tests/unit/tools/woocommerce.advanced.test.js +679 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// src/tools/content.js — content tools (13)
|
|
2
|
+
// Definitions + handlers (v5.0.0 refactor Step B+C)
|
|
3
|
+
|
|
4
|
+
import { json, strip, buildPaginationMeta, validateBlocks } 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_posts', _category: 'content', description: 'Use to browse/filter posts by status, category, tag, author, or search. 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 }, status: { type: 'string', default: 'publish' }, orderby: { type: 'string', default: 'date' }, order: { type: 'string', default: 'desc' }, categories: { type: 'string' }, tags: { type: 'string' }, search: { type: 'string' }, author: { type: 'number' }, mode: { type: 'string', default: 'full', description: 'full=all fields, summary=id/title/slug/date/status/link only, ids_only=flat ID array' } }}},
|
|
10
|
+
{ name: 'wp_get_post', _category: 'content', description: 'Use to read a single post with full content and meta. Read-only. Hint: use content_format=\'links_only\' for link audits (~800 chars vs 187k), \'text\' for rewrites, fields=[\'id\',\'title\',\'meta\'] for SEO-only.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, fields: { type: 'array', items: { type: 'string' }, description: 'Return only specified fields (id,title,content,excerpt,slug,status,date,modified,categories,tags,author,featured_media,meta,link). Omit for all.' }, content_format: { type: 'string', default: 'html', description: 'html=raw HTML (truncated at WP_MAX_CONTENT_CHARS), text=plain text, links_only=extract internal links only' } }, required: ['id'] }},
|
|
11
|
+
{ name: 'wp_create_post', _category: 'content', description: 'Use to create a post (defaults to draft). Accepts title, HTML content, categories, tags, featured_media, meta. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string', default: 'draft' }, excerpt: { type: 'string' }, categories: { type: 'array', items: { type: 'number' } }, tags: { type: 'array', items: { type: 'number' } }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' }, author: { type: 'number' } }, required: ['title', 'content'] }},
|
|
12
|
+
{ name: 'wp_update_post', _category: 'content', description: 'Use to modify any post field — only provided fields change. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string' }, excerpt: { type: 'string' }, categories: { type: 'array', items: { type: 'number' } }, tags: { type: 'array', items: { type: 'number' } }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' }, author: { type: 'number' } }, required: ['id'] }},
|
|
13
|
+
{ name: 'wp_delete_post', _category: 'content', description: 'Use to trash or permanently delete a post. Write — blocked by WP_READ_ONLY, WP_DISABLE_DELETE.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, force: { type: 'boolean', default: false }, confirmation_token: { type: 'string', description: 'Confirmation token returned by the first call when WP_CONFIRM_DESTRUCTIVE=true' } }, required: ['id'] }},
|
|
14
|
+
{ name: 'wp_search', _category: 'content', description: 'Use for full-text search across all content types. Returns id, title, url, type. Read-only.', inputSchema: { type: 'object', properties: { search: { type: 'string' }, per_page: { type: 'number', default: 10 }, type: { type: 'string', default: '' } }, required: ['search'] }},
|
|
15
|
+
{ name: 'wp_validate_block_structure', _category: 'content', description: 'Use to validate Gutenberg block HTML structure before saving. Detects unclosed blocks, malformed JSON attributes, invalid nesting, unclosed HTML tags, and deprecated blocks. Read-only.',
|
|
16
|
+
inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Gutenberg block HTML to validate' }, post_id: { type: 'number', description: 'Optional post ID for context' }, strict: { type: 'boolean', default: false, description: 'true = error on missing attributes' }, fix_suggestions: { type: 'boolean', default: true, description: 'Include fix suggestions' } }, required: ['content'] }},
|
|
17
|
+
{ name: 'wp_bulk_update', _category: 'content', description: 'Use to bulk update content across multiple posts/pages. Supports text replacement, meta updates, status changes, and content append. Dry-run by default for safety. Write — blocked by WP_READ_ONLY.',
|
|
18
|
+
inputSchema: { type: 'object', properties: { post_ids: { type: 'array', items: { type: 'number' }, description: 'Specific post IDs to update. If absent, uses filters.' }, post_type: { type: 'string', default: 'post', description: 'post or page' }, filters: { type: 'object', properties: { category_id: { type: 'number' }, tag_id: { type: 'number' }, status: { type: 'string' }, date_before: { type: 'string' }, date_after: { type: 'string' } }, description: 'Filters to select posts when post_ids is absent' }, operations: { type: 'array', items: { type: 'object', properties: { type: { type: 'string', enum: ['replace_text', 'update_meta', 'update_status', 'append_content'] }, params: { type: 'object' } }, required: ['type', 'params'] }, description: 'Operations to apply to each post' }, dry_run: { type: 'boolean', default: true }, confirm: { type: 'boolean', default: false }, limit: { type: 'number', default: 50, description: 'Max posts to process (max 500)' }, batch_size: { type: 'number', default: 10, description: 'Posts per batch' } }, required: ['operations'] }},
|
|
19
|
+
{ name: 'wp_list_pages', _category: 'content', description: 'Use to browse pages with hierarchy and templates. Supports parent filter, menu_order sort. 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 }, status: { type: 'string', default: 'publish' }, parent: { type: 'number' }, orderby: { type: 'string', default: 'menu_order' }, order: { type: 'string', default: 'asc' }, 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' } }}},
|
|
20
|
+
{ name: 'wp_get_page', _category: 'content', description: 'Use to read a single page with content and template. Read-only. Warning: Elementor pages can exceed 100k chars.', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] }},
|
|
21
|
+
{ name: 'wp_create_page', _category: 'content', description: 'Use to create a page (defaults to draft). Supports parent, template, menu_order. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string', default: 'draft' }, parent: { type: 'number', default: 0 }, template: { type: 'string' }, menu_order: { type: 'number', default: 0 }, excerpt: { type: 'string' }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' } }, required: ['title', 'content'] }},
|
|
22
|
+
{ name: 'wp_update_page', _category: 'content', description: 'Use to modify any page field. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string' }, parent: { type: 'number' }, template: { type: 'string' }, menu_order: { type: 'number' }, excerpt: { type: 'string' }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' } }, required: ['id'] }},
|
|
23
|
+
{ name: 'wp_get_post_meta', _category: 'content', description: 'Read post meta fields. Use for ACF data if acf/v3 unavailable, or for any custom meta (_elementor_data, _yoast_wpseo_*, etc.). Returns all meta if meta_key omitted.',
|
|
24
|
+
inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, post_type: { type: 'string', default: 'posts', description: 'posts or pages' }, meta_key: { type: 'string', description: 'Specific key. Omit for all meta.' }, parse_json: { type: 'boolean', default: true, description: 'Auto-parse JSON strings (e.g. _elementor_data)' } }, required: ['post_id'] }}
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const handlers = {};
|
|
28
|
+
|
|
29
|
+
handlers['wp_list_posts'] = async (args) => {
|
|
30
|
+
const t0 = Date.now();
|
|
31
|
+
let result;
|
|
32
|
+
const { wpApiCall, auditLog, sanitizeParams, name, STATUSES, ORDERBY, ORDERS } = rt;
|
|
33
|
+
validateInput(args, { per_page: { type: 'number', min: 1, max: 100 }, page: { type: 'number', min: 1 }, status: { type: 'string', enum: STATUSES }, orderby: { type: 'string', enum: ORDERBY }, order: { type: 'string', enum: ORDERS }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'] } });
|
|
34
|
+
const { per_page = 10, page = 1, status = 'publish', orderby = 'date', order = 'desc', categories, tags, search, author, mode = 'full' } = args;
|
|
35
|
+
let ep = `/posts?per_page=${per_page}&page=${page}&status=${status}&orderby=${orderby}&order=${order}`;
|
|
36
|
+
if (categories) ep += `&categories=${categories}`; if (tags) ep += `&tags=${tags}`;
|
|
37
|
+
if (search) ep += `&search=${encodeURIComponent(search)}`; if (author) ep += `&author=${author}`;
|
|
38
|
+
const posts = await wpApiCall(ep);
|
|
39
|
+
let listResult;
|
|
40
|
+
if (mode === 'ids_only') {
|
|
41
|
+
listResult = { total: posts.length, page, mode: 'ids_only', ids: posts.map(p => p.id) };
|
|
42
|
+
} else if (mode === 'summary') {
|
|
43
|
+
listResult = { total: posts.length, page, mode: 'summary', posts: posts.map(p => ({ id: p.id, title: p.title.rendered, slug: p.slug, date: p.date, status: p.status, link: p.link })) };
|
|
44
|
+
} else {
|
|
45
|
+
listResult = { total: posts.length, page, posts: posts.map(p => ({ id: p.id, title: p.title.rendered, status: p.status, date: p.date, modified: p.modified, link: p.link, author: p.author, categories: p.categories, tags: p.tags, excerpt: strip(p.excerpt.rendered).substring(0, 200) })) };
|
|
46
|
+
}
|
|
47
|
+
if (posts._wpTotal !== undefined) listResult.pagination = buildPaginationMeta(posts._wpTotal, page, per_page);
|
|
48
|
+
result = json(listResult);
|
|
49
|
+
auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: sanitizeParams(args) });
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
handlers['wp_get_post'] = async (args) => {
|
|
53
|
+
const t0 = Date.now();
|
|
54
|
+
let result;
|
|
55
|
+
const { wpApiCall, getActiveAuth, auditLog, name, summarizePost, applyContentFormat } = rt;
|
|
56
|
+
validateInput(args, { id: { type: 'number', required: true, min: 1 }, fields: { type: 'array' }, content_format: { type: 'string', enum: ['html', 'text', 'links_only'] } });
|
|
57
|
+
const { content_format = 'html', fields: requestedFields } = args;
|
|
58
|
+
const p = await wpApiCall(`/posts/${args.id}`);
|
|
59
|
+
let postData = { id: p.id, title: p.title.rendered, content: p.content.rendered, excerpt: p.excerpt.rendered, status: p.status, date: p.date, modified: p.modified, link: p.link, slug: p.slug, categories: p.categories, tags: p.tags, author: p.author, featured_media: p.featured_media, comment_status: p.comment_status, meta: p.meta || {} };
|
|
60
|
+
if (p.acf && Object.keys(p.acf).length > 0) { postData.acf_fields = p.acf; }
|
|
61
|
+
else { postData.acf_fields = {}; postData.acf_hint = 'ACF returned empty. Verify Show in REST API is enabled in each Field Group settings in WordPress Admin.'; }
|
|
62
|
+
const { url: siteUrl } = getActiveAuth();
|
|
63
|
+
postData = applyContentFormat(postData, content_format, siteUrl);
|
|
64
|
+
if (requestedFields && requestedFields.length > 0) postData = summarizePost(postData, requestedFields);
|
|
65
|
+
result = json(postData);
|
|
66
|
+
auditLog({ tool: name, target: args.id, target_type: 'post', action: 'read', status: 'success', latency_ms: Date.now() - t0 });
|
|
67
|
+
return result;
|
|
68
|
+
};
|
|
69
|
+
handlers['wp_create_post'] = async (args) => {
|
|
70
|
+
const t0 = Date.now();
|
|
71
|
+
let result;
|
|
72
|
+
const { wpApiCall, getActiveControls, auditLog, sanitizeParams, name, enforceDraftOnly, enforceAllowedStatuses, enforceAllowedTypes, STATUSES } = rt;
|
|
73
|
+
validateInput(args, { title: { type: 'string', required: true }, content: { type: 'string', required: true }, status: { type: 'string', enum: STATUSES.filter(s => s !== 'trash') } });
|
|
74
|
+
enforceDraftOnly(args.status); enforceAllowedStatuses(args.status); enforceAllowedTypes('post');
|
|
75
|
+
if (getActiveControls().require_approval && args.status === 'publish') {
|
|
76
|
+
auditLog({ tool: name, target: null, target_type: 'post', action: 'create', status: 'blocked', latency_ms: Date.now() - t0, params: sanitizeParams(args), error: 'APPROVAL REQUIRED: Use wp_submit_for_review then wp_approve_post' });
|
|
77
|
+
return { content: [{ type: 'text', text: 'Error: APPROVAL REQUIRED: Use wp_submit_for_review then wp_approve_post' }], isError: true };
|
|
78
|
+
}
|
|
79
|
+
const { title, content, status = 'draft', excerpt, categories, tags, slug, featured_media, meta, author } = args;
|
|
80
|
+
const data = { title, content, status };
|
|
81
|
+
if (excerpt) data.excerpt = excerpt; if (categories) data.categories = categories; if (tags) data.tags = tags;
|
|
82
|
+
if (slug) data.slug = slug; if (featured_media) data.featured_media = featured_media; if (meta) data.meta = meta; if (author) data.author = author;
|
|
83
|
+
const np = await wpApiCall('/posts', { method: 'POST', body: JSON.stringify(data) });
|
|
84
|
+
result = json({ success: true, message: 'Post created', post: { id: np.id, title: np.title.rendered, status: np.status, link: np.link, slug: np.slug } });
|
|
85
|
+
auditLog({ tool: name, target: np.id, target_type: 'post', action: 'create', status: 'success', latency_ms: Date.now() - t0, params: { title, status } });
|
|
86
|
+
return result;
|
|
87
|
+
};
|
|
88
|
+
handlers['wp_update_post'] = async (args) => {
|
|
89
|
+
const t0 = Date.now();
|
|
90
|
+
let result;
|
|
91
|
+
const { wpApiCall, getActiveControls, auditLog, sanitizeParams, name, enforceDraftOnly, enforceAllowedStatuses, STATUSES } = rt;
|
|
92
|
+
validateInput(args, { id: { type: 'number', required: true, min: 1 }, status: { type: 'string', enum: STATUSES } });
|
|
93
|
+
if (args.status) { enforceDraftOnly(args.status); enforceAllowedStatuses(args.status); }
|
|
94
|
+
if (getActiveControls().require_approval && args.status === 'publish') {
|
|
95
|
+
auditLog({ tool: name, target: args.id, target_type: 'post', action: 'update', status: 'blocked', latency_ms: Date.now() - t0, params: sanitizeParams(args), error: 'APPROVAL REQUIRED: Use wp_submit_for_review then wp_approve_post' });
|
|
96
|
+
return { content: [{ type: 'text', text: 'Error: APPROVAL REQUIRED: Use wp_submit_for_review then wp_approve_post' }], isError: true };
|
|
97
|
+
}
|
|
98
|
+
const { id, ...upd } = args;
|
|
99
|
+
const up = await wpApiCall(`/posts/${id}`, { method: 'POST', body: JSON.stringify(upd) });
|
|
100
|
+
result = json({ success: true, message: `Post ${id} updated`, post: { id: up.id, title: up.title.rendered, status: up.status, link: up.link, modified: up.modified } });
|
|
101
|
+
auditLog({ tool: name, target: id, target_type: 'post', action: 'update', status: 'success', latency_ms: Date.now() - t0, params: sanitizeParams(upd) });
|
|
102
|
+
return result;
|
|
103
|
+
};
|
|
104
|
+
handlers['wp_delete_post'] = async (args) => {
|
|
105
|
+
const t0 = Date.now();
|
|
106
|
+
let result;
|
|
107
|
+
const { wpApiCall, getActiveControls, generateToken, validateToken, auditLog, name } = rt;
|
|
108
|
+
validateInput(args, { id: { type: 'number', required: true, min: 1 } });
|
|
109
|
+
const { id, force = false, confirmation_token } = args;
|
|
110
|
+
const deleteAction = force ? 'permanent_delete' : 'trash';
|
|
111
|
+
|
|
112
|
+
// Two-step confirmation when WP_CONFIRM_DESTRUCTIVE=true
|
|
113
|
+
if (getActiveControls().confirm_destructive) {
|
|
114
|
+
if (!confirmation_token) {
|
|
115
|
+
// Step 1: return confirmation_required with token
|
|
116
|
+
const p = await wpApiCall(`/posts/${id}`);
|
|
117
|
+
const token = generateToken(id, deleteAction);
|
|
118
|
+
const verb = force ? 'permanently deleted' : 'trashed';
|
|
119
|
+
result = json({ status: 'confirmation_required', post_id: id, post_title: p.title?.rendered || `Post #${id}`, action: deleteAction, confirmation_token: token, expires_in: 60, message: `Post #${id} '${p.title?.rendered || ''}' will be ${verb}. Call again with confirmation_token to confirm.` });
|
|
120
|
+
auditLog({ tool: name, target: id, target_type: 'post', action: 'delete_requested', status: 'pending', latency_ms: Date.now() - t0, params: { id, force } });
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
// Step 2: validate token then execute
|
|
124
|
+
const validation = validateToken(confirmation_token, id, deleteAction);
|
|
125
|
+
if (!validation.valid) {
|
|
126
|
+
auditLog({ tool: name, target: id, target_type: 'post', action: deleteAction, status: 'error', latency_ms: Date.now() - t0, params: { id, force }, error: 'Invalid or expired confirmation token' });
|
|
127
|
+
return { content: [{ type: 'text', text: 'Error: Invalid or expired confirmation token' }], isError: true };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const dp = await wpApiCall(`/posts/${id}${force ? '?force=true' : ''}`, { method: 'DELETE' });
|
|
132
|
+
result = json({ success: true, message: force ? `Post ${id} permanently deleted` : `Post ${id} trashed`, post: { id: dp.id, title: dp.title?.rendered || dp.previous?.title?.rendered, status: force ? 'deleted' : 'trash' } });
|
|
133
|
+
auditLog({ tool: name, target: id, target_type: 'post', action: deleteAction, status: 'success', latency_ms: Date.now() - t0 });
|
|
134
|
+
return result;
|
|
135
|
+
};
|
|
136
|
+
handlers['wp_search'] = async (args) => {
|
|
137
|
+
const t0 = Date.now();
|
|
138
|
+
let result;
|
|
139
|
+
const { wpApiCall, auditLog, name } = rt;
|
|
140
|
+
validateInput(args, { search: { type: 'string', required: true }, per_page: { type: 'number', min: 1, max: 100 } });
|
|
141
|
+
const { search, per_page = 10, type } = args;
|
|
142
|
+
let ep = `/search?search=${encodeURIComponent(search)}&per_page=${per_page}`;
|
|
143
|
+
if (type) ep += `&type=${type}`;
|
|
144
|
+
const r = await wpApiCall(ep);
|
|
145
|
+
result = json({ query: search, total: r.length, results: r.map(x => ({ id: x.id, title: x.title, url: x.url, type: x.type, subtype: x.subtype })) });
|
|
146
|
+
auditLog({ tool: name, action: 'search', status: 'success', latency_ms: Date.now() - t0, params: { search, type } });
|
|
147
|
+
return result;
|
|
148
|
+
};
|
|
149
|
+
handlers['wp_validate_block_structure'] = async (args) => {
|
|
150
|
+
const t0 = Date.now();
|
|
151
|
+
let result;
|
|
152
|
+
const { auditLog, name } = rt;
|
|
153
|
+
validateInput(args, { content: { type: 'string', required: true } });
|
|
154
|
+
const { content, strict = false, fix_suggestions = true } = args;
|
|
155
|
+
const validation = validateBlocks(content, strict, fix_suggestions);
|
|
156
|
+
result = json(validation);
|
|
157
|
+
auditLog({ tool: name, action: 'validate_blocks', status: validation.valid ? 'valid' : 'invalid', latency_ms: Date.now() - t0, params: { errors: validation.errors.length, warnings: validation.warnings.length } });
|
|
158
|
+
return result;
|
|
159
|
+
};
|
|
160
|
+
handlers['wp_bulk_update'] = async (args) => {
|
|
161
|
+
const t0 = Date.now();
|
|
162
|
+
let result;
|
|
163
|
+
const { wpApiCall, auditLog, sanitizeParams, name } = rt;
|
|
164
|
+
const { post_ids, post_type = 'post', filters = {}, operations, dry_run = true, confirm = false, limit = 50, batch_size = 10 } = args;
|
|
165
|
+
if (!operations || !operations.length) throw new Error('operations array is required and must not be empty.');
|
|
166
|
+
const effectiveLimit = Math.min(Math.max(1, limit), 500);
|
|
167
|
+
const effectiveBatch = Math.min(Math.max(1, batch_size), 100);
|
|
168
|
+
|
|
169
|
+
// Resolve target post IDs
|
|
170
|
+
let targetIds = [];
|
|
171
|
+
if (post_ids && post_ids.length > 0) {
|
|
172
|
+
targetIds = post_ids.slice(0, effectiveLimit);
|
|
173
|
+
} else {
|
|
174
|
+
// Build query from filters
|
|
175
|
+
const hasFilter = filters.category_id || filters.tag_id || filters.status || filters.date_before || filters.date_after;
|
|
176
|
+
if (!hasFilter) throw new Error('Either post_ids or at least one filter (category_id, tag_id, status, date_before, date_after) is required.');
|
|
177
|
+
const endpoint = post_type === 'page' ? '/pages' : '/posts';
|
|
178
|
+
let ep = `${endpoint}?per_page=${effectiveLimit}&_fields=id`;
|
|
179
|
+
if (filters.status) ep += `&status=${filters.status}`;
|
|
180
|
+
else ep += '&status=publish';
|
|
181
|
+
if (filters.category_id) ep += `&categories=${filters.category_id}`;
|
|
182
|
+
if (filters.tag_id) ep += `&tags=${filters.tag_id}`;
|
|
183
|
+
if (filters.date_before) ep += `&before=${filters.date_before}`;
|
|
184
|
+
if (filters.date_after) ep += `&after=${filters.date_after}`;
|
|
185
|
+
const fetched = await wpApiCall(ep);
|
|
186
|
+
targetIds = (Array.isArray(fetched) ? fetched : []).map(p => p.id).slice(0, effectiveLimit);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (targetIds.length === 0) {
|
|
190
|
+
result = json({ mode: dry_run ? 'dry_run' : 'preview', posts_affected: 0, operations_preview: [], message: 'No posts matched the given criteria.' });
|
|
191
|
+
auditLog({ tool: name, action: 'bulk_update', status: 'empty', latency_ms: Date.now() - t0, params: sanitizeParams(args) });
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Helper: compute changes preview for a single post
|
|
196
|
+
function previewChanges(post, ops) {
|
|
197
|
+
const changes = [];
|
|
198
|
+
for (const op of ops) {
|
|
199
|
+
switch (op.type) {
|
|
200
|
+
case 'replace_text': {
|
|
201
|
+
const { search: s, replace: r, case_sensitive = true } = op.params || {};
|
|
202
|
+
const content = post.content?.rendered || '';
|
|
203
|
+
const flags = case_sensitive ? 'g' : 'gi';
|
|
204
|
+
const count = (content.match(new RegExp(s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags)) || []).length;
|
|
205
|
+
if (count > 0) changes.push({ type: 'replace_text', search: s, replace: r, occurrences: count });
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
case 'update_meta': {
|
|
209
|
+
const { key, value } = op.params || {};
|
|
210
|
+
changes.push({ type: 'update_meta', key, new_value: value, old_value: post.meta?.[key] ?? null });
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case 'update_status': {
|
|
214
|
+
const { status: newStatus } = op.params || {};
|
|
215
|
+
if (post.status !== newStatus) changes.push({ type: 'update_status', from: post.status, to: newStatus });
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
case 'append_content': {
|
|
219
|
+
const { content: c, position = 'after' } = op.params || {};
|
|
220
|
+
changes.push({ type: 'append_content', position, content_length: (c || '').length });
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return changes;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Dry-run or preview mode
|
|
229
|
+
if (dry_run || !confirm) {
|
|
230
|
+
const previewIds = targetIds.slice(0, 10);
|
|
231
|
+
const endpoint = post_type === 'page' ? '/pages' : '/posts';
|
|
232
|
+
const previews = [];
|
|
233
|
+
for (const pid of previewIds) {
|
|
234
|
+
const p = await wpApiCall(`${endpoint}/${pid}`);
|
|
235
|
+
previews.push({ post_id: pid, title: p.title?.rendered || '', changes: previewChanges(p, operations) });
|
|
236
|
+
}
|
|
237
|
+
result = json({
|
|
238
|
+
mode: dry_run ? 'dry_run' : 'preview',
|
|
239
|
+
posts_affected: targetIds.length,
|
|
240
|
+
operations_preview: previews,
|
|
241
|
+
warning: dry_run ? 'Set dry_run=false and confirm=true to apply changes.' : 'Set confirm=true to apply changes.'
|
|
242
|
+
});
|
|
243
|
+
auditLog({ tool: name, action: 'bulk_update_preview', status: 'dry_run', latency_ms: Date.now() - t0, params: { posts_count: targetIds.length, operations: operations.map(o => o.type) } });
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Execute mode: dry_run=false, confirm=true
|
|
248
|
+
const endpoint = post_type === 'page' ? '/pages' : '/posts';
|
|
249
|
+
let updated = 0;
|
|
250
|
+
const failed = [];
|
|
251
|
+
for (let i = 0; i < targetIds.length; i += effectiveBatch) {
|
|
252
|
+
const batch = targetIds.slice(i, i + effectiveBatch);
|
|
253
|
+
for (const pid of batch) {
|
|
254
|
+
try {
|
|
255
|
+
const p = await wpApiCall(`${endpoint}/${pid}`);
|
|
256
|
+
const updateData = {};
|
|
257
|
+
for (const op of operations) {
|
|
258
|
+
switch (op.type) {
|
|
259
|
+
case 'replace_text': {
|
|
260
|
+
const { search: s, replace: r, case_sensitive = true } = op.params || {};
|
|
261
|
+
const content = p.content?.rendered || '';
|
|
262
|
+
const flags = case_sensitive ? 'g' : 'gi';
|
|
263
|
+
updateData.content = (updateData.content || content).replace(new RegExp(s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags), r);
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case 'update_meta': {
|
|
267
|
+
const { key, value } = op.params || {};
|
|
268
|
+
if (!updateData.meta) updateData.meta = {};
|
|
269
|
+
updateData.meta[key] = value;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case 'update_status': {
|
|
273
|
+
updateData.status = op.params.status;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case 'append_content': {
|
|
277
|
+
const { content: c, position = 'after' } = op.params || {};
|
|
278
|
+
const existing = updateData.content || p.content?.rendered || '';
|
|
279
|
+
updateData.content = position === 'before' ? c + existing : existing + c;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (Object.keys(updateData).length > 0) {
|
|
285
|
+
await wpApiCall(`${endpoint}/${pid}`, { method: 'POST', body: JSON.stringify(updateData) });
|
|
286
|
+
updated++;
|
|
287
|
+
}
|
|
288
|
+
} catch (e) {
|
|
289
|
+
failed.push({ post_id: pid, error: e.message });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
result = json({ success: true, posts_updated: updated, posts_failed: failed, duration_ms: Date.now() - t0 });
|
|
294
|
+
auditLog({ tool: name, action: 'bulk_update', status: 'success', latency_ms: Date.now() - t0, params: { posts_count: updated, operations: operations.map(o => o.type) } });
|
|
295
|
+
return result;
|
|
296
|
+
};
|
|
297
|
+
handlers['wp_list_pages'] = async (args) => {
|
|
298
|
+
const t0 = Date.now();
|
|
299
|
+
let result;
|
|
300
|
+
const { wpApiCall, auditLog, name, STATUSES, ORDERS } = rt;
|
|
301
|
+
validateInput(args, { per_page: { type: 'number', min: 1, max: 100 }, page: { type: 'number', min: 1 }, status: { type: 'string', enum: STATUSES }, order: { type: 'string', enum: ORDERS }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'] } });
|
|
302
|
+
const { per_page = 10, page = 1, status = 'publish', parent, orderby = 'menu_order', order = 'asc', search, mode = 'full' } = args;
|
|
303
|
+
let ep = `/pages?per_page=${per_page}&page=${page}&status=${status}&orderby=${orderby}&order=${order}`;
|
|
304
|
+
if (parent !== undefined) ep += `&parent=${parent}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
|
|
305
|
+
const pgs = await wpApiCall(ep);
|
|
306
|
+
const pgPagination = pgs._wpTotal !== undefined ? buildPaginationMeta(pgs._wpTotal, page, per_page) : undefined;
|
|
307
|
+
if (mode === 'ids_only') {
|
|
308
|
+
result = json({ total: pgs.length, page, mode: 'ids_only', ids: pgs.map(p => p.id), ...(pgPagination && { pagination: pgPagination }) });
|
|
309
|
+
auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
if (mode === 'summary') {
|
|
313
|
+
result = json({ total: pgs.length, page, mode: 'summary', pages: pgs.map(p => ({ id: p.id, title: p.title.rendered, slug: p.slug, status: p.status, link: p.link, parent: p.parent })), ...(pgPagination && { pagination: pgPagination }) });
|
|
314
|
+
auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
result = json({ total: pgs.length, page, pages: pgs.map(p => ({ id: p.id, title: p.title.rendered, status: p.status, date: p.date, link: p.link, parent: p.parent, menu_order: p.menu_order, template: p.template, excerpt: strip(p.excerpt.rendered).substring(0, 200) })), ...(pgPagination && { pagination: pgPagination }) });
|
|
318
|
+
auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
|
|
319
|
+
return result;
|
|
320
|
+
};
|
|
321
|
+
handlers['wp_get_page'] = async (args) => {
|
|
322
|
+
const t0 = Date.now();
|
|
323
|
+
let result;
|
|
324
|
+
const { wpApiCall, auditLog, name } = rt;
|
|
325
|
+
validateInput(args, { id: { type: 'number', required: true, min: 1 } });
|
|
326
|
+
const pg = await wpApiCall(`/pages/${args.id}`);
|
|
327
|
+
const pageData = { id: pg.id, title: pg.title.rendered, content: pg.content.rendered, excerpt: pg.excerpt.rendered, status: pg.status, date: pg.date, modified: pg.modified, link: pg.link, slug: pg.slug, parent: pg.parent, menu_order: pg.menu_order, template: pg.template, author: pg.author, featured_media: pg.featured_media, meta: pg.meta || {} };
|
|
328
|
+
if (pg.acf && Object.keys(pg.acf).length > 0) { pageData.acf_fields = pg.acf; }
|
|
329
|
+
else { pageData.acf_fields = {}; pageData.acf_hint = 'ACF returned empty. Verify Show in REST API is enabled in each Field Group settings in WordPress Admin.'; }
|
|
330
|
+
result = json(pageData);
|
|
331
|
+
auditLog({ tool: name, target: args.id, target_type: 'page', action: 'read', status: 'success', latency_ms: Date.now() - t0 });
|
|
332
|
+
return result;
|
|
333
|
+
};
|
|
334
|
+
handlers['wp_create_page'] = async (args) => {
|
|
335
|
+
const t0 = Date.now();
|
|
336
|
+
let result;
|
|
337
|
+
const { wpApiCall, auditLog, name, enforceDraftOnly, enforceAllowedStatuses, enforceAllowedTypes, STATUSES } = rt;
|
|
338
|
+
validateInput(args, { title: { type: 'string', required: true }, content: { type: 'string', required: true }, status: { type: 'string', enum: STATUSES.filter(s => s !== 'trash') } });
|
|
339
|
+
enforceDraftOnly(args.status); enforceAllowedStatuses(args.status); enforceAllowedTypes('page');
|
|
340
|
+
const { title, content, status = 'draft', parent = 0, template, menu_order = 0, excerpt, slug, featured_media, meta } = args;
|
|
341
|
+
const data = { title, content, status, parent, menu_order };
|
|
342
|
+
if (template) data.template = template; if (excerpt) data.excerpt = excerpt; if (slug) data.slug = slug;
|
|
343
|
+
if (featured_media) data.featured_media = featured_media; if (meta) data.meta = meta;
|
|
344
|
+
const np = await wpApiCall('/pages', { method: 'POST', body: JSON.stringify(data) });
|
|
345
|
+
result = json({ success: true, message: 'Page created', page: { id: np.id, title: np.title.rendered, status: np.status, link: np.link, parent: np.parent } });
|
|
346
|
+
auditLog({ tool: name, target: np.id, target_type: 'page', action: 'create', status: 'success', latency_ms: Date.now() - t0, params: { title, status } });
|
|
347
|
+
return result;
|
|
348
|
+
};
|
|
349
|
+
handlers['wp_update_page'] = async (args) => {
|
|
350
|
+
const t0 = Date.now();
|
|
351
|
+
let result;
|
|
352
|
+
const { wpApiCall, auditLog, sanitizeParams, name, enforceDraftOnly, enforceAllowedStatuses, STATUSES } = rt;
|
|
353
|
+
validateInput(args, { id: { type: 'number', required: true, min: 1 }, status: { type: 'string', enum: STATUSES } });
|
|
354
|
+
if (args.status) { enforceDraftOnly(args.status); enforceAllowedStatuses(args.status); }
|
|
355
|
+
const { id, ...upd } = args;
|
|
356
|
+
const up = await wpApiCall(`/pages/${id}`, { method: 'POST', body: JSON.stringify(upd) });
|
|
357
|
+
result = json({ success: true, message: `Page ${id} updated`, page: { id: up.id, title: up.title.rendered, status: up.status, link: up.link, modified: up.modified } });
|
|
358
|
+
auditLog({ tool: name, target: id, target_type: 'page', action: 'update', status: 'success', latency_ms: Date.now() - t0, params: sanitizeParams(upd) });
|
|
359
|
+
return result;
|
|
360
|
+
};
|
|
361
|
+
handlers['wp_get_post_meta'] = async (args) => {
|
|
362
|
+
const t0 = Date.now();
|
|
363
|
+
const { wpApiCall, auditLog, name } = rt;
|
|
364
|
+
validateInput(args, { post_id: { type: 'number', required: true, min: 1 }, post_type: { type: 'string', enum: ['posts', 'pages'] } });
|
|
365
|
+
const { post_id, post_type = 'posts', meta_key, parse_json = true } = args;
|
|
366
|
+
let meta, acfFields = {};
|
|
367
|
+
try {
|
|
368
|
+
const post = await wpApiCall(`/${post_type}/${post_id}?_fields=meta,acf`);
|
|
369
|
+
meta = post?.meta ?? {};
|
|
370
|
+
acfFields = post?.acf ?? {};
|
|
371
|
+
} catch (e) {
|
|
372
|
+
if (post_type === 'posts' && e.message.includes('404')) {
|
|
373
|
+
const post = await wpApiCall(`/pages/${post_id}?_fields=meta,acf`);
|
|
374
|
+
meta = post?.meta ?? {};
|
|
375
|
+
acfFields = post?.acf ?? {};
|
|
376
|
+
} else { throw e; }
|
|
377
|
+
}
|
|
378
|
+
// Auto-parse JSON strings
|
|
379
|
+
if (parse_json) {
|
|
380
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
381
|
+
if (typeof v === 'string' && v.length > 1 && (v[0] === '[' || v[0] === '{')) {
|
|
382
|
+
try { meta[k] = JSON.parse(v); } catch { /* keep as string */ }
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
let result;
|
|
387
|
+
if (meta_key) {
|
|
388
|
+
const value = meta[meta_key] !== undefined ? meta[meta_key] : (acfFields[meta_key] !== undefined ? acfFields[meta_key] : null);
|
|
389
|
+
result = json({ post_id, meta_key, value });
|
|
390
|
+
} else {
|
|
391
|
+
result = json({ post_id, meta, acf_fields: acfFields, source: Object.keys(acfFields).length > 0 ? 'acf_rest' : 'wp_meta_only' });
|
|
392
|
+
}
|
|
393
|
+
auditLog({ tool: name, target: post_id, action: 'read_meta', status: 'success', latency_ms: Date.now() - t0, params: { meta_key: meta_key || 'all' } });
|
|
394
|
+
return result;
|
|
395
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/tools/core.js — core tools (3)
|
|
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_set_target', _category: 'core', description: 'Use to switch active WordPress site in multi-target mode. Write.',
|
|
10
|
+
inputSchema: { type: 'object', properties: { site: { type: 'string', description: 'Site key from targets config' } }, required: ['site'] }},
|
|
11
|
+
{ name: 'wp_site_info', _category: 'core', description: 'Use first to discover site config, user, post types, governance flags, and available targets. Read-only.',
|
|
12
|
+
inputSchema: { type: 'object', properties: {} }},
|
|
13
|
+
{ name: 'wp_get_site_options', _category: 'core', description: 'Use to read WordPress site settings (title, tagline, language, timezone, etc.) via /wp/v2/settings. Requires manage_options. Read-only.',
|
|
14
|
+
inputSchema: { type: 'object', properties: { keys: { type: 'array', items: { type: 'string' }, description: 'Return only these option keys (returns all if omitted)' } }}}
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const handlers = {};
|
|
18
|
+
|
|
19
|
+
handlers['wp_set_target'] = async (args) => {
|
|
20
|
+
const t0 = Date.now();
|
|
21
|
+
let result;
|
|
22
|
+
const { getActiveControls, currentTarget, targets, log, auditLog, name } = rt;
|
|
23
|
+
validateInput(args, { site: { type: 'string', required: true } });
|
|
24
|
+
const { site } = args;
|
|
25
|
+
if (!targets[site]) {
|
|
26
|
+
const available = Object.keys(targets);
|
|
27
|
+
throw new Error(`Site "${site}" not found. Available: ${available.length > 0 ? available.join(', ') : 'none (configure WP_TARGETS_JSON or WP_TARGETS_FILE)'}`);
|
|
28
|
+
}
|
|
29
|
+
const prev = currentTarget?.name || 'default';
|
|
30
|
+
rt.currentTarget = { name: site, ...targets[site] };
|
|
31
|
+
log.info(`Target switched: ${prev} → ${site} (${rt.currentTarget.url})`);
|
|
32
|
+
const effectiveControls = getActiveControls();
|
|
33
|
+
result = json({ success: true, message: `Active site: ${site}`, previous: prev, current: { name: site, url: rt.currentTarget.url }, effective_controls: effectiveControls });
|
|
34
|
+
auditLog({ tool: name, action: 'switch_target', status: 'success', latency_ms: Date.now() - t0, params: { from: prev, to: site }, effective_controls: effectiveControls });
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
handlers['wp_site_info'] = async (args) => {
|
|
38
|
+
const t0 = Date.now();
|
|
39
|
+
let result;
|
|
40
|
+
const { wpApiCall, getActiveAuth, getActiveControls, getControlSources, currentTarget, targets, isMultiTarget, VERSION, TIMEOUT_MS, TOOLS_COUNT, AUDIT_LOG, ALLOWED_TYPES, ALLOWED_STATUSES, fetch, auditLog, name, getFilteredTools, getEnabledCategories, pluginRegistry, TOOLS_DEFINITIONS } = rt;
|
|
41
|
+
const { url: baseUrl, auth: activeAuth } = getActiveAuth();
|
|
42
|
+
const ctrl = new AbortController();
|
|
43
|
+
const tid = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
44
|
+
const resp = await fetch(`${baseUrl}/wp-json`, { headers: { 'Authorization': `Basic ${activeAuth}`, 'User-Agent': `WordPress-MCP-Server/${VERSION}` }, signal: ctrl.signal });
|
|
45
|
+
clearTimeout(tid);
|
|
46
|
+
const si = await resp.json();
|
|
47
|
+
const uResp = await fetch(`${baseUrl}/wp-json/wp/v2/users/me`, { headers: { 'Authorization': `Basic ${activeAuth}`, 'User-Agent': `WordPress-MCP-Server/${VERSION}` } });
|
|
48
|
+
const u = uResp.ok ? await uResp.json() : null;
|
|
49
|
+
let postTypes = [];
|
|
50
|
+
try { const t = await wpApiCall('/types'); postTypes = Object.values(t).map(x => ({ slug: x.slug, name: x.name, rest_base: x.rest_base })); } catch {}
|
|
51
|
+
|
|
52
|
+
result = json({
|
|
53
|
+
site: { name: si.name, description: si.description, url: si.url || baseUrl, gmt_offset: si.gmt_offset, timezone_string: si.timezone_string },
|
|
54
|
+
current_user: u ? { id: u.id, name: u.name, slug: u.slug, roles: u.roles } : null,
|
|
55
|
+
post_types: postTypes,
|
|
56
|
+
enterprise_controls: (() => {
|
|
57
|
+
const c = getActiveControls();
|
|
58
|
+
const s = getControlSources();
|
|
59
|
+
return {
|
|
60
|
+
read_only: c.read_only, draft_only: c.draft_only, delete_disabled: c.disable_delete, plugin_management_disabled: c.disable_plugin_management, require_approval: c.require_approval, confirm_destructive: c.confirm_destructive,
|
|
61
|
+
rate_limit: c.max_calls_per_minute > 0 ? `${c.max_calls_per_minute}/min` : 'unlimited',
|
|
62
|
+
allowed_types: ALLOWED_TYPES || 'all', allowed_statuses: ALLOWED_STATUSES || 'all',
|
|
63
|
+
audit_log: AUDIT_LOG,
|
|
64
|
+
...s
|
|
65
|
+
};
|
|
66
|
+
})(),
|
|
67
|
+
multi_target: {
|
|
68
|
+
enabled: isMultiTarget, active_site: currentTarget?.name || 'default',
|
|
69
|
+
available_sites: Object.keys(targets)
|
|
70
|
+
},
|
|
71
|
+
server: (() => {
|
|
72
|
+
const exposed = getFilteredTools().length;
|
|
73
|
+
const groups = [];
|
|
74
|
+
if (!process.env.WC_CONSUMER_KEY) groups.push('woocommerce');
|
|
75
|
+
if (process.env.WP_REQUIRE_APPROVAL !== 'true') groups.push('editorial');
|
|
76
|
+
if (process.env.WP_ENABLE_PLUGIN_INTELLIGENCE !== 'true') groups.push('plugin_intelligence');
|
|
77
|
+
return { mcp_version: VERSION, tools_total: TOOLS_COUNT, tools_exposed: exposed, filtered_out: groups };
|
|
78
|
+
})(),
|
|
79
|
+
tool_categories: (() => {
|
|
80
|
+
const ec = getEnabledCategories();
|
|
81
|
+
const expCount = getFilteredTools().length;
|
|
82
|
+
return {
|
|
83
|
+
available: ['core','content','media','taxonomy','engagement','users','seo','schema','intelligence','editorial','fse','plugins','workflow','links','woocommerce','security','performance','health'],
|
|
84
|
+
active: ec ?? ['all'],
|
|
85
|
+
tools_exposed: expCount,
|
|
86
|
+
tools_total: TOOLS_DEFINITIONS.length,
|
|
87
|
+
hint: ec
|
|
88
|
+
? `Showing ${expCount} tools. Set WP_TOOL_CATEGORIES= (empty) for all ${TOOLS_DEFINITIONS.length} tools.`
|
|
89
|
+
: `Showing all ${TOOLS_DEFINITIONS.length} tools. Set WP_TOOL_CATEGORIES=seo,content to reduce context.`
|
|
90
|
+
};
|
|
91
|
+
})(),
|
|
92
|
+
plugin_layer: pluginRegistry.getSummary()
|
|
93
|
+
});
|
|
94
|
+
auditLog({ tool: name, action: 'info', status: 'success', latency_ms: Date.now() - t0 });
|
|
95
|
+
return result;
|
|
96
|
+
};
|
|
97
|
+
handlers['wp_get_site_options'] = async (args) => {
|
|
98
|
+
const t0 = Date.now();
|
|
99
|
+
let result;
|
|
100
|
+
const { wpApiCall, auditLog, name } = rt;
|
|
101
|
+
validateInput(args, { keys: { type: 'array' } });
|
|
102
|
+
const { keys } = args;
|
|
103
|
+
const settings = await wpApiCall('/settings');
|
|
104
|
+
let data = settings;
|
|
105
|
+
if (keys && keys.length > 0) {
|
|
106
|
+
data = {};
|
|
107
|
+
for (const k of keys) {
|
|
108
|
+
if (k in settings) data[k] = settings[k];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
result = json(data);
|
|
112
|
+
auditLog({ tool: name, action: 'read_options', status: 'success', latency_ms: Date.now() - t0, params: { keys_requested: keys?.length || 'all', keys_returned: Object.keys(data).length } });
|
|
113
|
+
return result;
|
|
114
|
+
};
|