@adsim/wordpress-mcp-server 4.6.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +18 -0
- package/README.md +851 -499
- package/companion/mcp-diagnostics.php +1184 -0
- package/dxt/manifest.json +715 -98
- package/index.js +166 -4786
- package/package.json +14 -6
- package/src/data/plugin-performance-data.json +59 -0
- package/src/shared/api.js +79 -0
- package/src/shared/audit.js +39 -0
- package/src/shared/context.js +15 -0
- package/src/shared/governance.js +98 -0
- package/src/shared/utils.js +148 -0
- package/src/tools/comments.js +50 -0
- package/src/tools/content.js +353 -0
- package/src/tools/core.js +114 -0
- package/src/tools/editorial.js +634 -0
- package/src/tools/fse.js +370 -0
- package/src/tools/health.js +160 -0
- package/src/tools/index.js +96 -0
- package/src/tools/intelligence.js +2082 -0
- package/src/tools/links.js +118 -0
- package/src/tools/media.js +71 -0
- package/src/tools/performance.js +219 -0
- package/src/tools/plugins.js +368 -0
- package/src/tools/schema.js +417 -0
- package/src/tools/security.js +590 -0
- package/src/tools/seo.js +1633 -0
- package/src/tools/taxonomy.js +115 -0
- package/src/tools/users.js +188 -0
- package/src/tools/woocommerce.js +1008 -0
- package/src/tools/workflow.js +409 -0
- package/src/transport/http.js +39 -0
- package/tests/unit/helpers/pagination.test.js +43 -0
- package/tests/unit/tools/bulkUpdate.test.js +188 -0
- package/tests/unit/tools/diagnostics.test.js +397 -0
- package/tests/unit/tools/dynamicFiltering.test.js +100 -8
- package/tests/unit/tools/editorialIntelligence.test.js +817 -0
- package/tests/unit/tools/fse.test.js +548 -0
- package/tests/unit/tools/multilingual.test.js +653 -0
- package/tests/unit/tools/performance.test.js +351 -0
- package/tests/unit/tools/runWorkflow.test.js +150 -0
- package/tests/unit/tools/schema.test.js +477 -0
- package/tests/unit/tools/security.test.js +695 -0
- package/tests/unit/tools/site.test.js +1 -1
- package/tests/unit/tools/users.crud.test.js +399 -0
- package/tests/unit/tools/validateBlocks.test.js +186 -0
- package/tests/unit/tools/visualStaging.test.js +271 -0
- package/tests/unit/tools/woocommerce.advanced.test.js +679 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// src/tools/plugins.js — plugins tools (9)
|
|
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_list_plugins', _category: 'plugins', description: 'Use to list installed plugins with status and version. Requires Administrator role. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.',
|
|
10
|
+
inputSchema: { type: 'object', properties: { search: { type: 'string' }, status: { type: 'string', enum: ['active', 'inactive', 'all'], default: 'all' }, per_page: { type: 'number', default: 20 }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
|
|
11
|
+
{ name: 'wp_activate_plugin', _category: 'plugins', description: 'Use to activate a plugin. Write — blocked by WP_READ_ONLY, WP_DISABLE_PLUGIN_MANAGEMENT.',
|
|
12
|
+
inputSchema: { type: 'object', properties: { plugin: { type: 'string', description: 'Plugin slug/file (e.g. "akismet/akismet.php"). Use wp_list_plugins to find the correct value.' } }, required: ['plugin'] }},
|
|
13
|
+
{ name: 'wp_deactivate_plugin', _category: 'plugins', description: 'Use to deactivate a plugin. Write — blocked by WP_READ_ONLY, WP_DISABLE_PLUGIN_MANAGEMENT.',
|
|
14
|
+
inputSchema: { type: 'object', properties: { plugin: { type: 'string', description: 'Plugin slug/file (e.g. "akismet/akismet.php"). Use wp_list_plugins to find the correct value.' } }, required: ['plugin'] }},
|
|
15
|
+
{ name: 'wp_list_themes', _category: 'plugins', description: 'Use to list installed themes with active theme detection. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.',
|
|
16
|
+
inputSchema: { type: 'object', properties: { status: { type: 'string', enum: ['active', 'inactive', 'all'], default: 'all' }, per_page: { type: 'number', default: 20 }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
|
|
17
|
+
{ name: 'wp_get_theme', _category: 'plugins', description: 'Use to get theme details by stylesheet slug. Read-only.',
|
|
18
|
+
inputSchema: { type: 'object', properties: { stylesheet: { type: 'string', description: 'Theme stylesheet slug (e.g. "twentytwentyfour"). Use wp_list_themes to find the correct value.' } }, required: ['stylesheet'] }},
|
|
19
|
+
{ name: 'wp_list_revisions', _category: 'plugins', description: 'Use to list revisions of a post or page (metadata only). Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.',
|
|
20
|
+
inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'], default: 'post' }, per_page: { type: 'number', default: 10 }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }, required: ['post_id'] }},
|
|
21
|
+
{ name: 'wp_get_revision', _category: 'plugins', description: 'Use to read a specific revision with full content. Read-only.',
|
|
22
|
+
inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, revision_id: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'], default: 'post' } }, required: ['post_id', 'revision_id'] }},
|
|
23
|
+
{ name: 'wp_restore_revision', _category: 'plugins', description: 'Use to restore a post to a previous revision. Write — blocked by WP_READ_ONLY.',
|
|
24
|
+
inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, revision_id: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'], default: 'post' } }, required: ['post_id', 'revision_id'] }},
|
|
25
|
+
{ name: 'wp_delete_revision', _category: 'plugins', description: 'Use to permanently delete a revision. Write — blocked by WP_READ_ONLY, WP_DISABLE_DELETE, WP_CONFIRM_DESTRUCTIVE.',
|
|
26
|
+
inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, revision_id: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'], default: 'post' }, confirmation_token: { type: 'string', description: 'Confirmation token returned by the first call when WP_CONFIRM_DESTRUCTIVE=true' } }, required: ['post_id', 'revision_id'] }}
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export const handlers = {};
|
|
30
|
+
|
|
31
|
+
handlers['wp_list_plugins'] = async (args) => {
|
|
32
|
+
const t0 = Date.now();
|
|
33
|
+
let result;
|
|
34
|
+
const { wpApiCall, auditLog, name } = rt;
|
|
35
|
+
validateInput(args, {
|
|
36
|
+
search: { type: 'string' },
|
|
37
|
+
status: { type: 'string', enum: ['active', 'inactive', 'all'] },
|
|
38
|
+
per_page: { type: 'number', min: 1, max: 100 },
|
|
39
|
+
mode: { type: 'string', enum: ['full', 'summary', 'ids_only'] }
|
|
40
|
+
});
|
|
41
|
+
const { search, status = 'all', per_page = 20, mode = 'full' } = args;
|
|
42
|
+
let ep = `/plugins?per_page=${per_page}&context=edit`;
|
|
43
|
+
if (search) ep += `&search=${encodeURIComponent(search)}`;
|
|
44
|
+
if (status && status !== 'all') ep += `&status=${status}`;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const plugins = await wpApiCall(ep);
|
|
48
|
+
if (mode === 'ids_only') {
|
|
49
|
+
result = json({ total: plugins.length, mode: 'ids_only', ids: plugins.map(p => p.plugin) });
|
|
50
|
+
auditLog({ tool: name, action: 'list', target_type: 'plugin', status: 'success', latency_ms: Date.now() - t0, params: { search, status, per_page } });
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
const mapped = plugins.map(p => ({
|
|
54
|
+
plugin: p.plugin,
|
|
55
|
+
name: p.name,
|
|
56
|
+
version: p.version,
|
|
57
|
+
status: p.status,
|
|
58
|
+
author: p.author?.rendered ?? p.author ?? '',
|
|
59
|
+
description: p.description?.rendered ? strip(p.description.rendered) : '',
|
|
60
|
+
plugin_uri: p.plugin_uri ?? '',
|
|
61
|
+
requires_wp: p.requires_wp ?? '',
|
|
62
|
+
requires_php: p.requires_php ?? '',
|
|
63
|
+
network_only: p.network_only ?? false,
|
|
64
|
+
textdomain: p.textdomain ?? ''
|
|
65
|
+
}));
|
|
66
|
+
if (mode === 'summary') {
|
|
67
|
+
result = json({ total: mapped.length, mode: 'summary', plugins: mapped.map(p => ({ plugin: p.plugin, name: p.name, status: p.status, version: p.version })) });
|
|
68
|
+
auditLog({ tool: name, action: 'list', target_type: 'plugin', status: 'success', latency_ms: Date.now() - t0, params: { search, status, per_page } });
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
const activeCount = mapped.filter(p => p.status === 'active').length;
|
|
72
|
+
const inactiveCount = mapped.filter(p => p.status === 'inactive').length;
|
|
73
|
+
result = json({ total: mapped.length, active: activeCount, inactive: inactiveCount, plugins: mapped });
|
|
74
|
+
auditLog({ tool: name, action: 'list', target_type: 'plugin', status: 'success', latency_ms: Date.now() - t0, params: { search, status, per_page } });
|
|
75
|
+
} catch (pluginError) {
|
|
76
|
+
const is403 = pluginError.message && (pluginError.message.includes('403') || pluginError.message.includes('Forbidden'));
|
|
77
|
+
auditLog({ tool: name, action: 'list', target_type: 'plugin', status: 'error', latency_ms: Date.now() - t0, error: is403 ? 'Insufficient permissions' : pluginError.message, params: { search, status } });
|
|
78
|
+
if (is403) {
|
|
79
|
+
return { content: [{ type: 'text', text: 'Error: Access denied. wp_list_plugins requires Administrator role with activate_plugins capability. The current WordPress user does not have sufficient permissions to access the /wp/v2/plugins endpoint.' }], isError: true };
|
|
80
|
+
}
|
|
81
|
+
throw pluginError;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
};
|
|
85
|
+
handlers['wp_activate_plugin'] = async (args) => {
|
|
86
|
+
const t0 = Date.now();
|
|
87
|
+
let result;
|
|
88
|
+
const { wpApiCall, auditLog, name } = rt;
|
|
89
|
+
validateInput(args, { plugin: { type: 'string', required: true } });
|
|
90
|
+
const { plugin } = args;
|
|
91
|
+
const encodedPlugin = encodeURIComponent(plugin);
|
|
92
|
+
try {
|
|
93
|
+
const p = await wpApiCall(`/plugins/${encodedPlugin}`, { method: 'POST', body: JSON.stringify({ status: 'active' }) });
|
|
94
|
+
result = json({ success: true, message: `Plugin activated: ${p.name || plugin}`, plugin: { plugin: p.plugin, name: p.name, version: p.version, status: p.status } });
|
|
95
|
+
auditLog({ tool: name, action: 'activate', target: plugin, target_type: 'plugin', status: 'success', latency_ms: Date.now() - t0 });
|
|
96
|
+
} catch (pluginError) {
|
|
97
|
+
const is403 = pluginError.message && (pluginError.message.includes('403') || pluginError.message.includes('Forbidden'));
|
|
98
|
+
const is404 = pluginError.message && (pluginError.message.includes('404') || pluginError.message.includes('Not Found'));
|
|
99
|
+
auditLog({ tool: name, action: 'activate', target: plugin, target_type: 'plugin', status: 'error', latency_ms: Date.now() - t0, error: is403 ? 'Insufficient permissions' : pluginError.message });
|
|
100
|
+
if (is403) return { content: [{ type: 'text', text: 'Error: Access denied. wp_activate_plugin requires Administrator role with activate_plugins capability.' }], isError: true };
|
|
101
|
+
if (is404) return { content: [{ type: 'text', text: 'Error: Plugin not found. Use wp_list_plugins to get the correct plugin slug.' }], isError: true };
|
|
102
|
+
throw pluginError;
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
};
|
|
106
|
+
handlers['wp_deactivate_plugin'] = async (args) => {
|
|
107
|
+
const t0 = Date.now();
|
|
108
|
+
let result;
|
|
109
|
+
const { wpApiCall, auditLog, name } = rt;
|
|
110
|
+
validateInput(args, { plugin: { type: 'string', required: true } });
|
|
111
|
+
const { plugin } = args;
|
|
112
|
+
const encodedPlugin = encodeURIComponent(plugin);
|
|
113
|
+
try {
|
|
114
|
+
const p = await wpApiCall(`/plugins/${encodedPlugin}`, { method: 'POST', body: JSON.stringify({ status: 'inactive' }) });
|
|
115
|
+
result = json({ success: true, message: `Plugin deactivated: ${p.name || plugin}`, plugin: { plugin: p.plugin, name: p.name, version: p.version, status: p.status } });
|
|
116
|
+
auditLog({ tool: name, action: 'deactivate', target: plugin, target_type: 'plugin', status: 'success', latency_ms: Date.now() - t0 });
|
|
117
|
+
} catch (pluginError) {
|
|
118
|
+
const is403 = pluginError.message && (pluginError.message.includes('403') || pluginError.message.includes('Forbidden'));
|
|
119
|
+
const is404 = pluginError.message && (pluginError.message.includes('404') || pluginError.message.includes('Not Found'));
|
|
120
|
+
auditLog({ tool: name, action: 'deactivate', target: plugin, target_type: 'plugin', status: 'error', latency_ms: Date.now() - t0, error: is403 ? 'Insufficient permissions' : pluginError.message });
|
|
121
|
+
if (is403) return { content: [{ type: 'text', text: 'Error: Access denied. wp_deactivate_plugin requires Administrator role with activate_plugins capability.' }], isError: true };
|
|
122
|
+
if (is404) return { content: [{ type: 'text', text: 'Error: Plugin not found. Use wp_list_plugins to get the correct plugin slug.' }], isError: true };
|
|
123
|
+
throw pluginError;
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
};
|
|
127
|
+
handlers['wp_list_themes'] = async (args) => {
|
|
128
|
+
const t0 = Date.now();
|
|
129
|
+
let result;
|
|
130
|
+
const { wpApiCall, auditLog, name } = rt;
|
|
131
|
+
validateInput(args, {
|
|
132
|
+
status: { type: 'string', enum: ['active', 'inactive', 'all'] },
|
|
133
|
+
per_page: { type: 'number', min: 1, max: 100 },
|
|
134
|
+
mode: { type: 'string', enum: ['full', 'summary', 'ids_only'] }
|
|
135
|
+
});
|
|
136
|
+
const { status = 'all', per_page = 20, mode = 'full' } = args;
|
|
137
|
+
let ep = `/themes?per_page=${per_page}&context=edit`;
|
|
138
|
+
if (status && status !== 'all') ep += `&status=${status}`;
|
|
139
|
+
try {
|
|
140
|
+
const themes = await wpApiCall(ep);
|
|
141
|
+
if (mode === 'ids_only') {
|
|
142
|
+
result = json({ total: themes.length, mode: 'ids_only', ids: themes.map(t => t.stylesheet) });
|
|
143
|
+
auditLog({ tool: name, action: 'list', target_type: 'theme', status: 'success', latency_ms: Date.now() - t0, params: { status, per_page } });
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
const mapped = themes.map(t => ({
|
|
147
|
+
stylesheet: t.stylesheet,
|
|
148
|
+
template: t.template,
|
|
149
|
+
name: t.name?.rendered ?? t.name ?? '',
|
|
150
|
+
description: t.description?.rendered ? strip(t.description.rendered) : '',
|
|
151
|
+
status: t.status,
|
|
152
|
+
version: t.version ?? '',
|
|
153
|
+
author: t.author?.rendered ?? t.author ?? '',
|
|
154
|
+
author_uri: t.author_uri ?? '',
|
|
155
|
+
theme_uri: t.theme_uri ?? '',
|
|
156
|
+
requires_wp: t.requires_wp ?? '',
|
|
157
|
+
requires_php: t.requires_php ?? '',
|
|
158
|
+
tags: t.tags?.rendered ?? t.tags ?? []
|
|
159
|
+
}));
|
|
160
|
+
if (mode === 'summary') {
|
|
161
|
+
result = json({ total: mapped.length, mode: 'summary', themes: mapped.map(t => ({ stylesheet: t.stylesheet, name: t.name, status: t.status, version: t.version })) });
|
|
162
|
+
auditLog({ tool: name, action: 'list', target_type: 'theme', status: 'success', latency_ms: Date.now() - t0, params: { status, per_page } });
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
const activeTheme = mapped.find(t => t.status === 'active');
|
|
166
|
+
result = json({ total: mapped.length, active_theme: activeTheme ? activeTheme.name : null, themes: mapped });
|
|
167
|
+
auditLog({ tool: name, action: 'list', target_type: 'theme', status: 'success', latency_ms: Date.now() - t0, params: { status, per_page } });
|
|
168
|
+
} catch (themeError) {
|
|
169
|
+
const is403 = themeError.message && (themeError.message.includes('403') || themeError.message.includes('Forbidden'));
|
|
170
|
+
auditLog({ tool: name, action: 'list', target_type: 'theme', status: 'error', latency_ms: Date.now() - t0, error: is403 ? 'Insufficient permissions' : themeError.message, params: { status } });
|
|
171
|
+
if (is403) return { content: [{ type: 'text', text: 'Error: Access denied. wp_list_themes requires switch_themes capability (Administrator or Editor role).' }], isError: true };
|
|
172
|
+
throw themeError;
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
};
|
|
176
|
+
handlers['wp_get_theme'] = async (args) => {
|
|
177
|
+
const t0 = Date.now();
|
|
178
|
+
let result;
|
|
179
|
+
const { wpApiCall, auditLog, name } = rt;
|
|
180
|
+
validateInput(args, { stylesheet: { type: 'string', required: true } });
|
|
181
|
+
const { stylesheet } = args;
|
|
182
|
+
try {
|
|
183
|
+
const t = await wpApiCall(`/themes/${encodeURIComponent(stylesheet)}?context=edit`);
|
|
184
|
+
result = json({
|
|
185
|
+
stylesheet: t.stylesheet,
|
|
186
|
+
template: t.template,
|
|
187
|
+
name: t.name?.rendered ?? t.name ?? '',
|
|
188
|
+
description: t.description?.rendered ? strip(t.description.rendered) : '',
|
|
189
|
+
status: t.status,
|
|
190
|
+
version: t.version ?? '',
|
|
191
|
+
author: t.author?.rendered ?? t.author ?? '',
|
|
192
|
+
author_uri: t.author_uri ?? '',
|
|
193
|
+
theme_uri: t.theme_uri ?? '',
|
|
194
|
+
requires_wp: t.requires_wp ?? '',
|
|
195
|
+
requires_php: t.requires_php ?? '',
|
|
196
|
+
tags: t.tags?.rendered ?? t.tags ?? []
|
|
197
|
+
});
|
|
198
|
+
auditLog({ tool: name, target: stylesheet, target_type: 'theme', action: 'read', status: 'success', latency_ms: Date.now() - t0 });
|
|
199
|
+
} catch (themeError) {
|
|
200
|
+
const is403 = themeError.message && (themeError.message.includes('403') || themeError.message.includes('Forbidden'));
|
|
201
|
+
const is404 = themeError.message && (themeError.message.includes('404') || themeError.message.includes('Not Found'));
|
|
202
|
+
auditLog({ tool: name, target: stylesheet, target_type: 'theme', action: 'read', status: 'error', latency_ms: Date.now() - t0, error: themeError.message });
|
|
203
|
+
if (is404) return { content: [{ type: 'text', text: 'Error: Theme not found. Use wp_list_themes to get the correct stylesheet slug.' }], isError: true };
|
|
204
|
+
if (is403) return { content: [{ type: 'text', text: 'Error: Access denied. wp_get_theme requires switch_themes capability (Administrator or Editor role).' }], isError: true };
|
|
205
|
+
throw themeError;
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
};
|
|
209
|
+
handlers['wp_list_revisions'] = async (args) => {
|
|
210
|
+
const t0 = Date.now();
|
|
211
|
+
let result;
|
|
212
|
+
const { wpApiCall, auditLog, name } = rt;
|
|
213
|
+
validateInput(args, {
|
|
214
|
+
post_id: { type: 'number', required: true, min: 1 },
|
|
215
|
+
post_type: { type: 'string', enum: ['post', 'page'] },
|
|
216
|
+
per_page: { type: 'number', min: 1, max: 100 },
|
|
217
|
+
mode: { type: 'string', enum: ['full', 'summary', 'ids_only'] }
|
|
218
|
+
});
|
|
219
|
+
const { post_id, post_type = 'post', per_page = 10, mode = 'full' } = args;
|
|
220
|
+
const base = post_type === 'page' ? 'pages' : 'posts';
|
|
221
|
+
try {
|
|
222
|
+
const revisions = await wpApiCall(`/${base}/${post_id}/revisions?per_page=${per_page}&context=edit`);
|
|
223
|
+
if (mode === 'ids_only') {
|
|
224
|
+
result = json({ total: revisions.length, post_id, post_type, mode: 'ids_only', ids: revisions.map(r => r.id) });
|
|
225
|
+
auditLog({ tool: name, action: 'list', target: post_id, target_type: 'revision', status: 'success', latency_ms: Date.now() - t0, params: { post_type, per_page } });
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
if (mode === 'summary') {
|
|
229
|
+
result = json({ total: revisions.length, post_id, post_type, mode: 'summary', revisions: revisions.map(r => ({ id: r.id, date: r.date, author: r.author })) });
|
|
230
|
+
auditLog({ tool: name, action: 'list', target: post_id, target_type: 'revision', status: 'success', latency_ms: Date.now() - t0, params: { post_type, per_page } });
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
result = json({
|
|
234
|
+
total: revisions.length,
|
|
235
|
+
post_id,
|
|
236
|
+
post_type,
|
|
237
|
+
note: 'Use wp_get_revision to read content of a specific revision',
|
|
238
|
+
revisions: revisions.map(r => ({
|
|
239
|
+
id: r.id,
|
|
240
|
+
parent: r.parent,
|
|
241
|
+
date: r.date,
|
|
242
|
+
date_gmt: r.date_gmt,
|
|
243
|
+
modified: r.modified,
|
|
244
|
+
modified_gmt: r.modified_gmt,
|
|
245
|
+
author: r.author,
|
|
246
|
+
title: r.title?.rendered ?? '',
|
|
247
|
+
excerpt: r.excerpt?.rendered ? strip(r.excerpt.rendered) : '',
|
|
248
|
+
slug: r.slug
|
|
249
|
+
}))
|
|
250
|
+
});
|
|
251
|
+
auditLog({ tool: name, action: 'list', target: post_id, target_type: 'revision', status: 'success', latency_ms: Date.now() - t0, params: { post_type, per_page } });
|
|
252
|
+
} catch (revError) {
|
|
253
|
+
const is404 = revError.message && (revError.message.includes('404') || revError.message.includes('Not Found'));
|
|
254
|
+
auditLog({ tool: name, action: 'list', target: post_id, target_type: 'revision', status: 'error', latency_ms: Date.now() - t0, error: revError.message });
|
|
255
|
+
if (is404) return { content: [{ type: 'text', text: 'Error: Post not found or no revisions available. Revisions require autosave to be enabled.' }], isError: true };
|
|
256
|
+
throw revError;
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
};
|
|
260
|
+
handlers['wp_get_revision'] = async (args) => {
|
|
261
|
+
const t0 = Date.now();
|
|
262
|
+
let result;
|
|
263
|
+
const { wpApiCall, auditLog, name } = rt;
|
|
264
|
+
validateInput(args, {
|
|
265
|
+
post_id: { type: 'number', required: true, min: 1 },
|
|
266
|
+
revision_id: { type: 'number', required: true, min: 1 },
|
|
267
|
+
post_type: { type: 'string', enum: ['post', 'page'] }
|
|
268
|
+
});
|
|
269
|
+
const { post_id, revision_id, post_type = 'post' } = args;
|
|
270
|
+
const base = post_type === 'page' ? 'pages' : 'posts';
|
|
271
|
+
try {
|
|
272
|
+
const r = await wpApiCall(`/${base}/${post_id}/revisions/${revision_id}?context=edit`);
|
|
273
|
+
result = json({
|
|
274
|
+
id: r.id,
|
|
275
|
+
parent: r.parent,
|
|
276
|
+
date: r.date,
|
|
277
|
+
author: r.author,
|
|
278
|
+
title: r.title?.rendered ?? '',
|
|
279
|
+
content: r.content?.rendered ?? '',
|
|
280
|
+
excerpt: r.excerpt?.rendered ?? ''
|
|
281
|
+
});
|
|
282
|
+
auditLog({ tool: name, action: 'read', target: revision_id, target_type: 'revision', status: 'success', latency_ms: Date.now() - t0, params: { post_id, post_type } });
|
|
283
|
+
} catch (revError) {
|
|
284
|
+
const is404 = revError.message && (revError.message.includes('404') || revError.message.includes('Not Found'));
|
|
285
|
+
auditLog({ tool: name, action: 'read', target: revision_id, target_type: 'revision', status: 'error', latency_ms: Date.now() - t0, error: revError.message });
|
|
286
|
+
if (is404) return { content: [{ type: 'text', text: 'Error: Revision not found. Use wp_list_revisions to get valid revision IDs for this post.' }], isError: true };
|
|
287
|
+
throw revError;
|
|
288
|
+
}
|
|
289
|
+
return result;
|
|
290
|
+
};
|
|
291
|
+
handlers['wp_restore_revision'] = async (args) => {
|
|
292
|
+
const t0 = Date.now();
|
|
293
|
+
let result;
|
|
294
|
+
const { wpApiCall, auditLog, name } = rt;
|
|
295
|
+
validateInput(args, {
|
|
296
|
+
post_id: { type: 'number', required: true, min: 1 },
|
|
297
|
+
revision_id: { type: 'number', required: true, min: 1 },
|
|
298
|
+
post_type: { type: 'string', enum: ['post', 'page'] }
|
|
299
|
+
});
|
|
300
|
+
const { post_id, revision_id, post_type = 'post' } = args;
|
|
301
|
+
const base = post_type === 'page' ? 'pages' : 'posts';
|
|
302
|
+
|
|
303
|
+
// Step 1: Read the revision
|
|
304
|
+
let revData;
|
|
305
|
+
try {
|
|
306
|
+
revData = await wpApiCall(`/${base}/${post_id}/revisions/${revision_id}?context=edit`);
|
|
307
|
+
auditLog({ tool: name, action: 'read', target: revision_id, target_type: 'revision', status: 'success', latency_ms: Date.now() - t0, params: { post_id, post_type } });
|
|
308
|
+
} catch (readError) {
|
|
309
|
+
const is404 = readError.message && (readError.message.includes('404') || readError.message.includes('Not Found'));
|
|
310
|
+
auditLog({ tool: name, action: 'read', target: revision_id, target_type: 'revision', status: 'error', latency_ms: Date.now() - t0, error: readError.message });
|
|
311
|
+
if (is404) return { content: [{ type: 'text', text: 'Error: Revision not found. Use wp_list_revisions to get valid revision IDs.' }], isError: true };
|
|
312
|
+
throw readError;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Step 2: Update the post with revision content
|
|
316
|
+
const revTitle = revData.title?.raw ?? revData.title?.rendered ?? '';
|
|
317
|
+
const revContent = revData.content?.raw ?? revData.content?.rendered ?? '';
|
|
318
|
+
try {
|
|
319
|
+
await wpApiCall(`/${base}/${post_id}`, { method: 'POST', body: JSON.stringify({ title: revTitle, content: revContent }) });
|
|
320
|
+
result = json({ restored: true, post_id, revision_id, post_type, title: revTitle, note: `Post content restored from revision ${revision_id}` });
|
|
321
|
+
auditLog({ tool: name, action: 'restore', target: post_id, target_type: post_type, status: 'success', latency_ms: Date.now() - t0, params: { revision_id } });
|
|
322
|
+
} catch (writeError) {
|
|
323
|
+
auditLog({ tool: name, action: 'restore', target: post_id, target_type: post_type, status: 'error', latency_ms: Date.now() - t0, error: writeError.message, params: { revision_id } });
|
|
324
|
+
throw writeError;
|
|
325
|
+
}
|
|
326
|
+
return result;
|
|
327
|
+
};
|
|
328
|
+
handlers['wp_delete_revision'] = async (args) => {
|
|
329
|
+
const t0 = Date.now();
|
|
330
|
+
let result;
|
|
331
|
+
const { wpApiCall, getActiveControls, generateToken, validateToken, auditLog, name } = rt;
|
|
332
|
+
validateInput(args, {
|
|
333
|
+
post_id: { type: 'number', required: true, min: 1 },
|
|
334
|
+
revision_id: { type: 'number', required: true, min: 1 },
|
|
335
|
+
post_type: { type: 'string', enum: ['post', 'page'] }
|
|
336
|
+
});
|
|
337
|
+
const { post_id, revision_id, post_type = 'post', confirmation_token } = args;
|
|
338
|
+
const base = post_type === 'page' ? 'pages' : 'posts';
|
|
339
|
+
|
|
340
|
+
// Two-step confirmation when WP_CONFIRM_DESTRUCTIVE=true
|
|
341
|
+
if (getActiveControls().confirm_destructive) {
|
|
342
|
+
if (!confirmation_token) {
|
|
343
|
+
const token = generateToken(revision_id, 'delete_revision');
|
|
344
|
+
result = json({ status: 'confirmation_required', revision_id, post_id, action: 'delete_revision', confirmation_token: token, expires_in: 60, message: `Revision #${revision_id} of post #${post_id} will be permanently deleted. Call again with confirmation_token to confirm.` });
|
|
345
|
+
auditLog({ tool: name, target: revision_id, target_type: 'revision', action: 'delete_requested', status: 'pending', latency_ms: Date.now() - t0, params: { post_id, revision_id } });
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
const validation = validateToken(confirmation_token, revision_id, 'delete_revision');
|
|
349
|
+
if (!validation.valid) {
|
|
350
|
+
auditLog({ tool: name, target: revision_id, target_type: 'revision', action: 'delete_revision', status: 'error', latency_ms: Date.now() - t0, params: { post_id, revision_id }, error: 'Invalid or expired confirmation token' });
|
|
351
|
+
return { content: [{ type: 'text', text: 'Error: Invalid or expired confirmation token' }], isError: true };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
await wpApiCall(`/${base}/${post_id}/revisions/${revision_id}?force=true`, { method: 'DELETE' });
|
|
357
|
+
result = json({ deleted: true, revision_id, post_id, post_type });
|
|
358
|
+
auditLog({ tool: name, action: 'permanent_delete', target: revision_id, target_type: 'revision', status: 'success', latency_ms: Date.now() - t0, params: { post_id, post_type } });
|
|
359
|
+
} catch (delError) {
|
|
360
|
+
const is403 = delError.message && (delError.message.includes('403') || delError.message.includes('Forbidden'));
|
|
361
|
+
const is404 = delError.message && (delError.message.includes('404') || delError.message.includes('Not Found'));
|
|
362
|
+
auditLog({ tool: name, action: 'permanent_delete', target: revision_id, target_type: 'revision', status: 'error', latency_ms: Date.now() - t0, error: delError.message, params: { post_id } });
|
|
363
|
+
if (is404) return { content: [{ type: 'text', text: 'Error: Revision not found. Use wp_list_revisions to get valid revision IDs.' }], isError: true };
|
|
364
|
+
if (is403) return { content: [{ type: 'text', text: 'Error: Insufficient permissions to delete revisions (delete_posts capability required).' }], isError: true };
|
|
365
|
+
throw delError;
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
};
|