PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.2.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.2.3
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / labs / mcp-core.php
ai-engine / labs Last commit date
mcp-core.php 8 months ago mcp-rest.php 1 year ago mcp.conf 1 year ago mcp.js 8 months ago mcp.md 8 months ago mcp.php 8 months ago oauth.php 9 months ago realtime.php 1 year ago
mcp-core.php
1440 lines
1 <?php
2
3 class Meow_MWAI_Labs_MCP_Core {
4 private $core = null;
5
6 #region Initialize
7 public function __construct( $core ) {
8 $this->core = $core;
9 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
10 }
11 public function rest_api_init() {
12 add_filter( 'mwai_mcp_tools', [ $this, 'register_rest_tools' ] );
13 add_filter( 'mwai_mcp_callback', [ $this, 'handle_call' ], 10, 4 );
14 }
15 #endregion
16
17 #region Helpers
18 private function add_result_text( array &$r, string $text ): void {
19 if ( !isset( $r['result']['content'] ) ) {
20 $r['result']['content'] = [];
21 }
22 $r['result']['content'][] = [ 'type' => 'text', 'text' => $text ];
23 }
24 private function clean_html( string $v ): string {
25 return wp_kses_post( wp_unslash( $v ) );
26 }
27 private function post_excerpt( WP_Post $p ): string {
28 return wp_trim_words( wp_strip_all_tags( $p->post_excerpt ?: $p->post_content ), 55 );
29 }
30 private function empty_schema(): array {
31 return [ 'type' => 'object', 'properties' => (object) [] ];
32 }
33 #endregion
34
35 #region Tools Definitions
36 private function tools(): array {
37 return [
38
39 /* -------- Plugins -------- */
40 'wp_list_plugins' => [
41 'name' => 'wp_list_plugins',
42 'description' => 'List installed plugins (returns array of {Name, Version}).',
43 'inputSchema' => [
44 'type' => 'object',
45 'properties' => [ 'search' => [ 'type' => 'string' ] ],
46 ],
47 ],
48
49 /* -------- Users -------- */
50 'wp_get_users' => [
51 'name' => 'wp_get_users',
52 'description' => 'Retrieve users (fields: ID, user_login, display_name, roles). If no limit supplied, returns 10. `paged` ignored if `offset` is used.',
53 'inputSchema' => [
54 'type' => 'object',
55 'properties' => [
56 'search' => [ 'type' => 'string' ],
57 'role' => [ 'type' => 'string' ],
58 'limit' => [ 'type' => 'integer' ],
59 'offset' => [ 'type' => 'integer' ],
60 'paged' => [ 'type' => 'integer' ],
61 ],
62 ],
63 ],
64 'wp_create_user' => [
65 'name' => 'wp_create_user',
66 'description' => 'Create a user. Requires user_login and user_email. Optional: user_pass (random if omitted), display_name, role.',
67 'inputSchema' => [
68 'type' => 'object',
69 'properties' => [
70 'user_login' => [ 'type' => 'string' ],
71 'user_email' => [ 'type' => 'string' ],
72 'user_pass' => [ 'type' => 'string' ],
73 'display_name' => [ 'type' => 'string' ],
74 'role' => [ 'type' => 'string' ],
75 ],
76 'required' => [ 'user_login', 'user_email' ],
77 ],
78 ],
79 'wp_update_user' => [
80 'name' => 'wp_update_user',
81 'description' => 'Update a user – pass ID plus a “fields” object (user_email, display_name, user_pass, role).',
82 'inputSchema' => [
83 'type' => 'object',
84 'properties' => [
85 'ID' => [ 'type' => 'integer' ],
86 'fields' => [
87 'type' => 'object',
88 'properties' => [
89 'user_email' => [ 'type' => 'string' ],
90 'display_name' => [ 'type' => 'string' ],
91 'user_pass' => [ 'type' => 'string' ],
92 'role' => [ 'type' => 'string' ],
93 ],
94 'additionalProperties' => true
95 ],
96 ],
97 'required' => [ 'ID' ],
98 ],
99 ],
100
101 /* -------- Comments -------- */
102 'wp_get_comments' => [
103 'name' => 'wp_get_comments',
104 'description' => 'Retrieve comments (fields: comment_ID, comment_post_ID, comment_author, comment_content, comment_date, comment_approved). Returns 10 by default.',
105 'inputSchema' => [
106 'type' => 'object',
107 'properties' => [
108 'post_id' => [ 'type' => 'integer' ],
109 'status' => [ 'type' => 'string' ],
110 'search' => [ 'type' => 'string' ],
111 'limit' => [ 'type' => 'integer' ],
112 'offset' => [ 'type' => 'integer' ],
113 'paged' => [ 'type' => 'integer' ],
114 ],
115 ],
116 ],
117 'wp_create_comment' => [
118 'name' => 'wp_create_comment',
119 'description' => 'Insert a comment. Requires post_id and comment_content. Optional author, author_email, author_url.',
120 'inputSchema' => [
121 'type' => 'object',
122 'properties' => [
123 'post_id' => [ 'type' => 'integer' ],
124 'comment_content' => [ 'type' => 'string' ],
125 'comment_author' => [ 'type' => 'string' ],
126 'comment_author_email' => [ 'type' => 'string' ],
127 'comment_author_url' => [ 'type' => 'string' ],
128 'comment_approved' => [ 'type' => 'string' ],
129 ],
130 'required' => [ 'post_id', 'comment_content' ],
131 ],
132 ],
133 'wp_update_comment' => [
134 'name' => 'wp_update_comment',
135 'description' => 'Update a comment – pass comment_ID plus fields (comment_content, comment_approved).',
136 'inputSchema' => [
137 'type' => 'object',
138 'properties' => [
139 'comment_ID' => [ 'type' => 'integer' ],
140 'fields' => [
141 'type' => 'object',
142 'properties' => [
143 'comment_content' => [ 'type' => 'string' ],
144 'comment_approved' => [ 'type' => 'string' ],
145 ],
146 'additionalProperties' => true
147 ],
148 ],
149 'required' => [ 'comment_ID' ],
150 ],
151 ],
152 'wp_delete_comment' => [
153 'name' => 'wp_delete_comment',
154 'description' => 'Delete a comment. `force` true bypasses trash.',
155 'inputSchema' => [
156 'type' => 'object',
157 'properties' => [
158 'comment_ID' => [ 'type' => 'integer' ],
159 'force' => [ 'type' => 'boolean' ],
160 ],
161 'required' => [ 'comment_ID' ],
162 ],
163 ],
164
165 /* -------- Options -------- */
166 'wp_get_option' => [
167 'name' => 'wp_get_option',
168 'description' => 'Get a single WordPress option value (scalar or array) by key.',
169 'inputSchema' => [
170 'type' => 'object',
171 'properties' => [ 'key' => [ 'type' => 'string' ] ],
172 'required' => [ 'key' ],
173 ],
174 ],
175 'wp_update_option' => [
176 'name' => 'wp_update_option',
177 'description' => 'Create or update a WordPress option (JSON-serialised if necessary).',
178 'inputSchema' => [
179 'type' => 'object',
180 'properties' => [
181 'key' => [ 'type' => 'string' ],
182 'value' => [ 'type' => [ 'string', 'number', 'boolean', 'object', 'array' ] ],
183 ],
184 'required' => [ 'key', 'value' ],
185 ],
186 ],
187
188 /* -------- Counts -------- */
189 'wp_count_posts' => [
190 'name' => 'wp_count_posts',
191 'description' => 'Return counts of posts by status. Optional post_type (default post).',
192 'inputSchema' => [
193 'type' => 'object',
194 'properties' => [ 'post_type' => [ 'type' => 'string' ] ],
195 ],
196 ],
197 'wp_count_terms' => [
198 'name' => 'wp_count_terms',
199 'description' => 'Return total number of terms in a taxonomy.',
200 'inputSchema' => [
201 'type' => 'object',
202 'properties' => [ 'taxonomy' => [ 'type' => 'string' ] ],
203 'required' => [ 'taxonomy' ],
204 ],
205 ],
206 'wp_count_media' => [
207 'name' => 'wp_count_media',
208 'description' => 'Return number of attachments (optionally after/before date).',
209 'inputSchema' => [
210 'type' => 'object',
211 'properties' => [
212 'after' => [ 'type' => 'string' ],
213 'before' => [ 'type' => 'string' ],
214 ],
215 ],
216 ],
217
218 /* -------- Post-types -------- */
219 'wp_get_post_types' => [
220 'name' => 'wp_get_post_types',
221 'description' => 'List public post types (key, label).',
222 'inputSchema' => $this->empty_schema(),
223 ],
224
225 /* -------- Posts -------- */
226 'wp_get_posts' => [
227 'name' => 'wp_get_posts',
228 'description' => 'Retrieve posts (fields: ID, title, status, excerpt, link). No full content. **If no limit is supplied it returns 10 posts by default.** `paged` is ignored if `offset` is used.',
229 'inputSchema' => [
230 'type' => 'object',
231 'properties' => [
232 'post_type' => [ 'type' => 'string' ],
233 'post_status' => [ 'type' => 'string' ],
234 'search' => [ 'type' => 'string' ],
235 'after' => [ 'type' => 'string' ],
236 'before' => [ 'type' => 'string' ],
237 'limit' => [ 'type' => 'integer' ],
238 'offset' => [ 'type' => 'integer' ],
239 'paged' => [ 'type' => 'integer' ],
240 ],
241 ],
242 ],
243 'wp_get_post' => [
244 'name' => 'wp_get_post',
245 'description' => 'Get basic post data by ID (title, content, status, dates). For complete data including all meta and terms, use wp_get_post_snapshot instead.',
246 'inputSchema' => [
247 'type' => 'object',
248 'properties' => [ 'ID' => [ 'type' => 'integer' ] ],
249 'required' => [ 'ID' ],
250 ],
251 ],
252 'wp_get_post_snapshot' => [
253 'name' => 'wp_get_post_snapshot',
254 'description' => 'Get complete post data in ONE call: all post fields, all meta, all terms/taxonomies, featured image, and author. Use this for WooCommerce products, events, or any post type where you need full context. Reduces 10-20 API calls to just 1. Returns structured JSON with post, meta, terms, thumbnail, and author keys.',
255 'inputSchema' => [
256 'type' => 'object',
257 'properties' => [
258 'ID' => [ 'type' => 'integer', 'description' => 'Post ID' ],
259 'include' => [
260 'type' => 'array',
261 'description' => 'Optional: fields to include (default: all). Options: meta, terms, thumbnail, author',
262 'items' => [ 'type' => 'string' ],
263 ],
264 ],
265 'required' => [ 'ID' ],
266 ],
267 ],
268 'wp_create_post' => [
269 'name' => 'wp_create_post',
270 'description' => 'Create a post or page – post_title required; Markdown accepted in post_content; defaults to draft post_status and post post_type; set categories later with wp_add_post_terms; meta_input is an associative array of custom-field key/value pairs.',
271 'inputSchema' => [
272 'type' => 'object',
273 'properties' => [
274 'post_title' => [ 'type' => 'string' ],
275 'post_content' => [ 'type' => 'string' ],
276 'post_excerpt' => [ 'type' => 'string' ],
277 'post_status' => [ 'type' => 'string' ],
278 'post_type' => [ 'type' => 'string' ],
279 'post_name' => [ 'type' => 'string' ],
280 'meta_input' => [ 'type' => 'object', 'description' => 'Associative array of custom fields.' ],
281 ],
282 'required' => [ 'post_title' ],
283 ],
284 ],
285 'wp_update_post' => [
286 'name' => 'wp_update_post',
287 'description' => 'Update post fields and/or meta in ONE call. Pass ID + "fields" object (post_title, post_content, post_status, etc.) and/or "meta_input" object for custom fields. Efficient for WooCommerce products: update title + price + stock together. Note: post_category REPLACES categories; use wp_add_post_terms to append instead.',
288 'inputSchema' => [
289 'type' => 'object',
290 'properties' => [
291 'ID' => [ 'type' => 'integer', 'description' => 'The ID of the post to update.' ],
292 'fields' => [
293 'type' => 'object',
294 'properties' => [
295 'post_title' => [ 'type' => 'string' ],
296 'post_content' => [ 'type' => 'string' ],
297 'post_status' => [ 'type' => 'string' ],
298 'post_name' => [ 'type' => 'string' ],
299 'post_excerpt' => [ 'type' => 'string' ],
300 'post_category' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
301 ],
302 'additionalProperties' => true
303 ],
304 'meta_input' => [
305 'type' => 'object',
306 'description' => 'Associative array of custom fields.'
307 ],
308 ],
309 'required' => [ 'ID' ],
310 ],
311 ],
312 'wp_delete_post' => [
313 'name' => 'wp_delete_post',
314 'description' => 'Delete/trash a post.',
315 'inputSchema' => [
316 'type' => 'object',
317 'properties' => [
318 'ID' => [ 'type' => 'integer' ],
319 'force' => [ 'type' => 'boolean' ],
320 ],
321 'required' => [ 'ID' ],
322 ],
323 ],
324
325 /* -------- Post-meta -------- */
326 'wp_get_post_meta' => [
327 'name' => 'wp_get_post_meta',
328 'description' => 'Get specific post meta field(s). Provide "key" to fetch a single value; omit to fetch all custom fields. If you need ALL meta along with post data and terms, use wp_get_post_snapshot instead for efficiency.',
329 'inputSchema' => [
330 'type' => 'object',
331 'properties' => [
332 'ID' => [ 'type' => 'integer' ],
333 'key' => [ 'type' => 'string' ],
334 ],
335 'required' => [ 'ID' ],
336 ],
337 ],
338 'wp_update_post_meta' => [
339 'name' => 'wp_update_post_meta',
340 'description' => 'Update post meta efficiently. Use "meta" object to update MULTIPLE fields at once (e.g., {_price: "19.99", _stock: "50", _sku: "WIDGET"}), or use "key"+"value" for a single field. Essential for WooCommerce products and custom post types.',
341 'inputSchema' => [
342 'type' => 'object',
343 'properties' => [
344 'ID' => [ 'type' => 'integer' ],
345 'meta' => [ 'type' => 'object', 'description' => 'Key/value pairs to set. Alternative: provide "key" + "value".' ],
346 'key' => [ 'type' => 'string' ],
347 'value' => [ 'type' => [ 'string', 'number', 'boolean' ] ],
348 ],
349 'required' => [ 'ID' ],
350 ],
351 ],
352 'wp_delete_post_meta' => [
353 'name' => 'wp_delete_post_meta',
354 'description' => 'Delete custom field(s) from a post. Provide value to remove a single row; omit value to delete all rows for the key.',
355 'inputSchema' => [
356 'type' => 'object',
357 'properties' => [
358 'ID' => [ 'type' => 'integer' ],
359 'key' => [ 'type' => 'string' ],
360 'value' => [ 'type' => [ 'string', 'number', 'boolean' ] ],
361 ],
362 'required' => [ 'ID', 'key' ],
363 ],
364 ],
365
366 /* -------- Featured image -------- */
367 'wp_set_featured_image' => [
368 'name' => 'wp_set_featured_image',
369 'description' => 'Attach or remove a featured image (thumbnail) for a post/page. Provide media_id to attach, omit or null to remove.',
370 'inputSchema' => [
371 'type' => 'object',
372 'properties' => [
373 'post_id' => [ 'type' => 'integer' ],
374 'media_id' => [ 'type' => 'integer' ],
375 ],
376 'required' => [ 'post_id' ],
377 ],
378 ],
379
380 /* -------- Taxonomies / Terms -------- */
381 'wp_get_taxonomies' => [
382 'name' => 'wp_get_taxonomies',
383 'description' => 'List taxonomies for a post type.',
384 'inputSchema' => [
385 'type' => 'object',
386 'properties' => [ 'post_type' => [ 'type' => 'string' ] ],
387 ],
388 ],
389 'wp_get_terms' => [
390 'name' => 'wp_get_terms',
391 'description' => 'List terms of a taxonomy.',
392 'inputSchema' => [
393 'type' => 'object',
394 'properties' => [
395 'taxonomy' => [ 'type' => 'string' ],
396 'search' => [ 'type' => 'string' ],
397 'parent' => [ 'type' => 'integer' ],
398 'limit' => [ 'type' => 'integer' ],
399 ],
400 'required' => [ 'taxonomy' ],
401 ],
402 ],
403 'wp_create_term' => [
404 'name' => 'wp_create_term',
405 'description' => 'Create a term.',
406 'inputSchema' => [
407 'type' => 'object',
408 'properties' => [
409 'taxonomy' => [ 'type' => 'string' ],
410 'term_name' => [ 'type' => 'string' ],
411 'slug' => [ 'type' => 'string' ],
412 'description' => [ 'type' => 'string' ],
413 'parent' => [ 'type' => 'integer' ],
414 ],
415 'required' => [ 'taxonomy', 'term_name' ],
416 ],
417 ],
418 'wp_update_term' => [
419 'name' => 'wp_update_term',
420 'description' => 'Update a term.',
421 'inputSchema' => [
422 'type' => 'object',
423 'properties' => [
424 'term_id' => [ 'type' => 'integer' ],
425 'taxonomy' => [ 'type' => 'string' ],
426 'name' => [ 'type' => 'string' ],
427 'slug' => [ 'type' => 'string' ],
428 'description' => [ 'type' => 'string' ],
429 'parent' => [ 'type' => 'integer' ],
430 ],
431 'required' => [ 'term_id', 'taxonomy' ],
432 ],
433 ],
434 'wp_delete_term' => [
435 'name' => 'wp_delete_term',
436 'description' => 'Delete a term.',
437 'inputSchema' => [
438 'type' => 'object',
439 'properties' => [
440 'term_id' => [ 'type' => 'integer' ],
441 'taxonomy' => [ 'type' => 'string' ],
442 ],
443 'required' => [ 'term_id', 'taxonomy' ],
444 ],
445 ],
446 'wp_get_post_terms' => [
447 'name' => 'wp_get_post_terms',
448 'description' => 'Get terms attached to a post.',
449 'inputSchema' => [
450 'type' => 'object',
451 'properties' => [
452 'ID' => [ 'type' => 'integer' ],
453 'taxonomy' => [ 'type' => 'string' ],
454 ],
455 'required' => [ 'ID' ],
456 ],
457 ],
458 'wp_add_post_terms' => [
459 'name' => 'wp_add_post_terms',
460 'description' => 'Attach or replace terms for a post. Set "append=true" to ADD terms to existing ones, or "append=false" (default) to REPLACE all terms. Use for categories, tags, or WooCommerce attributes (pa_color, pa_size, etc.).',
461 'inputSchema' => [
462 'type' => 'object',
463 'properties' => [
464 'ID' => [ 'type' => 'integer' ],
465 'taxonomy' => [ 'type' => 'string' ],
466 'terms' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
467 'append' => [ 'type' => 'boolean' ],
468 ],
469 'required' => [ 'ID', 'terms' ],
470 ],
471 ],
472
473 /* -------- Media -------- */
474 'wp_get_media' => [
475 'name' => 'wp_get_media',
476 'description' => 'List media items.',
477 'inputSchema' => [
478 'type' => 'object',
479 'properties' => [
480 'search' => [ 'type' => 'string' ],
481 'after' => [ 'type' => 'string' ],
482 'before' => [ 'type' => 'string' ],
483 'limit' => [ 'type' => 'integer' ],
484 ],
485 ],
486 ],
487 'wp_upload_media' => [
488 'name' => 'wp_upload_media',
489 'description' => 'Download file from URL and add to Media Library.',
490 'inputSchema' => [
491 'type' => 'object',
492 'properties' => [
493 'url' => [ 'type' => 'string' ],
494 'title' => [ 'type' => 'string' ],
495 'description' => [ 'type' => 'string' ],
496 'alt' => [ 'type' => 'string' ],
497 ],
498 'required' => [ 'url' ],
499 ],
500 ],
501 'wp_update_media' => [
502 'name' => 'wp_update_media',
503 'description' => 'Update attachment meta.',
504 'inputSchema' => [
505 'type' => 'object',
506 'properties' => [
507 'ID' => [ 'type' => 'integer' ],
508 'title' => [ 'type' => 'string' ],
509 'caption' => [ 'type' => 'string' ],
510 'description' => [ 'type' => 'string' ],
511 'alt' => [ 'type' => 'string' ],
512 ],
513 'required' => [ 'ID' ],
514 ],
515 ],
516 'wp_delete_media' => [
517 'name' => 'wp_delete_media',
518 'description' => 'Delete/trash an attachment.',
519 'inputSchema' => [
520 'type' => 'object',
521 'properties' => [
522 'ID' => [ 'type' => 'integer' ],
523 'force' => [ 'type' => 'boolean' ],
524 ],
525 'required' => [ 'ID' ],
526 ],
527 ],
528
529 /* -------- MWAI Vision / Image -------- */
530 'mwai_vision' => [
531 'name' => 'mwai_vision',
532 'description' => 'Analyze an image via AI Engine Vision.',
533 'inputSchema' => [
534 'type' => 'object',
535 'properties' => [
536 'message' => [ 'type' => 'string' ],
537 'url' => [ 'type' => 'string' ],
538 'path' => [ 'type' => 'string' ],
539 ],
540 'required' => [ 'message' ],
541 ],
542 ],
543 'mwai_image' => [
544 'name' => 'mwai_image',
545 'description' => 'Generate an image with AI Engine and store it in the Media Library. Optional: title, caption, description, alt. Returns { id, url, title, caption, alt }.',
546 'inputSchema' => [
547 'type' => 'object',
548 'properties' => [
549 'message' => [ 'type' => 'string', 'description' => 'Prompt describing the desired image.' ],
550 'postId' => [ 'type' => 'integer', 'description' => 'Optional post ID to attach the image to.' ],
551 'title' => [ 'type' => 'string' ],
552 'caption' => [ 'type' => 'string' ],
553 'description' => [ 'type' => 'string' ],
554 'alt' => [ 'type' => 'string' ],
555 ],
556 'required' => [ 'message' ],
557 ],
558 ],
559
560 ];
561 }
562 #endregion
563
564 #region Tool Registration
565 public function register_rest_tools( array $prev ): array {
566 $tools = $this->tools();
567
568 // All 36 core tools enabled and tested with ChatGPT.
569 // Automatic validation in mcp.php fixes problematic type definitions.
570
571 // Add category and annotations to each tool
572 foreach ( $tools as &$tool ) {
573 if ( !isset( $tool['category'] ) ) {
574 $tool['category'] = 'Core';
575 }
576
577 // Add MCP tool annotations based on tool name/behavior
578 if ( !isset( $tool['annotations'] ) ) {
579 $name = $tool['name'];
580
581 // Read-only tools (safe, no modifications)
582 $is_readonly = (
583 strpos( $name, 'wp_get_' ) === 0 ||
584 strpos( $name, 'wp_list_' ) === 0 ||
585 strpos( $name, 'wp_count_' ) === 0 ||
586 $name === 'mwai_vision'
587 );
588
589 // Destructive tools (can delete/destroy data)
590 $is_destructive = (
591 strpos( $name, 'wp_delete_' ) === 0 ||
592 $name === 'wp_update_user' // Can change passwords/roles
593 );
594
595 $tool['annotations'] = [
596 'readOnlyHint' => $is_readonly,
597 'destructiveHint' => !$is_readonly && $is_destructive,
598 'openWorldHint' => false, // All operate on closed WordPress system
599 ];
600 }
601 }
602
603 $merged = array_merge( $prev, array_values( $tools ) );
604 return $merged;
605 }
606 #endregion
607
608 #region Callback
609 public function handle_call( $prev, string $tool, array $args, int $id ) {
610 // Security check is already done in the MCP auth layer
611 // If we reach here, the user is authorized to use MCP
612 if ( !empty( $prev ) || !isset( $this->tools()[ $tool ] ) ) {
613 return $prev;
614 }
615 return $this->dispatch( $tool, $args, $id );
616 }
617 #endregion
618
619 #region Dispatcher
620 private function dispatch( string $tool, array $a, int $id ): array {
621 $r = [ 'jsonrpc' => '2.0', 'id' => $id ];
622
623 switch ( $tool ) {
624
625 /* ===== Users ===== */
626 case 'wp_get_users':
627 $q = [
628 'search' => '*' . esc_attr( $a['search'] ?? '' ) . '*',
629 'role' => $a['role'] ?? '',
630 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
631 ];
632 if ( isset( $a['offset'] ) ) {
633 $q['offset'] = max( 0, intval( $a['offset'] ) );
634 }
635 if ( isset( $a['paged'] ) ) {
636 $q['paged'] = max( 1, intval( $a['paged'] ) );
637 }
638 $rows = [];
639 foreach ( get_users( $q ) as $u ) {
640 $rows[] = [
641 'ID' => $u->ID,
642 'user_login' => $u->user_login,
643 'display_name' => $u->display_name,
644 'roles' => $u->roles,
645 ];
646 }
647 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
648 break;
649
650 case 'wp_create_user':
651 $data = [
652 'user_login' => sanitize_user( $a['user_login'] ),
653 'user_email' => sanitize_email( $a['user_email'] ),
654 'user_pass' => $a['user_pass'] ?? wp_generate_password( 12, true ),
655 'display_name' => sanitize_text_field( $a['display_name'] ?? '' ),
656 'role' => sanitize_key( $a['role'] ?? get_option( 'default_role', 'subscriber' ) ),
657 ];
658 $uid = wp_insert_user( $data );
659 if ( is_wp_error( $uid ) ) {
660 $r['error'] = [ 'code' => $uid->get_error_code(), 'message' => $uid->get_error_message() ];
661 }
662 else {
663 $this->add_result_text( $r, 'User created ID ' . $uid );
664 }
665 break;
666
667 case 'wp_update_user':
668 if ( empty( $a['ID'] ) ) {
669 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
670 break;
671 }
672 $upd = [ 'ID' => intval( $a['ID'] ) ];
673 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) ) {
674 foreach ( $a['fields'] as $k => $v ) {
675 $upd[ $k ] = ( $k === 'role' ) ? sanitize_key( $v ) : sanitize_text_field( $v );
676 }
677 }
678 $u = wp_update_user( $upd );
679 if ( is_wp_error( $u ) ) {
680 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
681 }
682 else {
683 $this->add_result_text( $r, 'User #' . $u . ' updated' );
684 }
685 break;
686
687 /* ===== Comments ===== */
688 case 'wp_get_comments':
689 $args = [
690 'post_id' => isset( $a['post_id'] ) ? intval( $a['post_id'] ) : '',
691 'status' => $a['status'] ?? 'approve',
692 'search' => $a['search'] ?? '',
693 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
694 ];
695 if ( isset( $a['offset'] ) ) {
696 $args['offset'] = max( 0, intval( $a['offset'] ) );
697 }
698 if ( isset( $a['paged'] ) ) {
699 $args['paged'] = max( 1, intval( $a['paged'] ) );
700 }
701 $list = [];
702 foreach ( get_comments( $args ) as $c ) {
703 $list[] = [
704 'comment_ID' => $c->comment_ID,
705 'comment_post_ID' => $c->comment_post_ID,
706 'comment_author' => $c->comment_author,
707 'comment_content' => wp_trim_words( wp_strip_all_tags( $c->comment_content ), 40 ),
708 'comment_date' => $c->comment_date,
709 'comment_approved' => $c->comment_approved,
710 ];
711 }
712 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
713 break;
714
715 case 'wp_create_comment':
716 if ( empty( $a['post_id'] ) || empty( $a['comment_content'] ) ) {
717 $r['error'] = [ 'code' => -32602, 'message' => 'post_id & comment_content required' ];
718 break;
719 }
720 $ins = [
721 'comment_post_ID' => intval( $a['post_id'] ),
722 'comment_content' => $this->clean_html( $a['comment_content'] ),
723 'comment_author' => sanitize_text_field( $a['comment_author'] ?? '' ),
724 'comment_author_email' => sanitize_email( $a['comment_author_email'] ?? '' ),
725 'comment_author_url' => esc_url_raw( $a['comment_author_url'] ?? '' ),
726 'comment_approved' => $a['comment_approved'] ?? 1,
727 ];
728 $cid = wp_insert_comment( $ins );
729 if ( is_wp_error( $cid ) ) {
730 /** @var WP_Error $cid */
731 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
732 }
733 else {
734 $this->add_result_text( $r, 'Comment created ID ' . $cid );
735 }
736 break;
737
738 case 'wp_update_comment':
739 if ( empty( $a['comment_ID'] ) ) {
740 $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ];
741 break;
742 }
743 $c = [ 'comment_ID' => intval( $a['comment_ID'] ) ];
744 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) ) {
745 foreach ( $a['fields'] as $k => $v ) {
746 $c[ $k ] = ( $k === 'comment_content' ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
747 }
748 }
749 $cid = wp_update_comment( $c, true );
750 if ( is_wp_error( $cid ) ) {
751 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
752 }
753 else {
754 $this->add_result_text( $r, 'Comment #' . $cid . ' updated' );
755 }
756 break;
757
758 case 'wp_delete_comment':
759 if ( empty( $a['comment_ID'] ) ) {
760 $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ];
761 break;
762 }
763 $done = wp_delete_comment( intval( $a['comment_ID'] ), !empty( $a['force'] ) );
764 if ( $done ) {
765 $this->add_result_text( $r, 'Comment #' . $a['comment_ID'] . ' deleted' );
766 }
767 else {
768 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
769 }
770 break;
771
772 /* ===== Options ===== */
773 case 'wp_get_option':
774 $val = get_option( sanitize_key( $a['key'] ) );
775 $this->add_result_text( $r, wp_json_encode( $val, JSON_PRETTY_PRINT ) );
776 break;
777
778 case 'wp_update_option':
779 $set = update_option( sanitize_key( $a['key'] ), $a['value'], 'yes' );
780 if ( $set ) {
781 $this->add_result_text( $r, 'Option "' . $a['key'] . '" updated' );
782 }
783 else {
784 $r['error'] = [ 'code' => -32603, 'message' => 'Update failed' ];
785 }
786 break;
787
788 /* ===== Counts ===== */
789 case 'wp_count_posts':
790 $pt = sanitize_key( $a['post_type'] ?? 'post' );
791 $obj = wp_count_posts( $pt );
792 $this->add_result_text( $r, wp_json_encode( $obj, JSON_PRETTY_PRINT ) );
793 break;
794
795 case 'wp_count_terms':
796 $tax = sanitize_key( $a['taxonomy'] );
797 $total = wp_count_terms( $tax, [ 'hide_empty' => false ] );
798 if ( is_wp_error( $total ) ) {
799 $r['error'] = [ 'code' => $total->get_error_code(), 'message' => $total->get_error_message() ];
800 }
801 else {
802 $this->add_result_text( $r, (string) $total );
803 }
804 break;
805
806 case 'wp_count_media':
807 $args = [ 'post_type' => 'attachment', 'post_status' => 'inherit', 'fields' => 'ids' ];
808 $d = [];
809 if ( $a['after'] ?? '' ) {
810 $d['after'] = $a['after'];
811 }
812 if ( $a['before'] ?? '' ) {
813 $d['before'] = $a['before'];
814 }
815 if ( $d ) {
816 $args['date_query'] = [ $d ];
817 }
818 $total = count( get_posts( $args ) );
819 $this->add_result_text( $r, (string) $total );
820 break;
821
822 /* ===== Post-types ===== */
823 case 'wp_get_post_types':
824 $out = [];
825 foreach ( get_post_types( [ 'public' => true ], 'objects' ) as $pt ) {
826 $out[] = [ 'key' => $pt->name, 'label' => $pt->label ];
827 }
828 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
829 break;
830
831 /* ===== Plugins ===== */
832 case 'wp_list_plugins':
833 if ( !function_exists( 'get_plugins' ) ) {
834 require_once ABSPATH . 'wp-admin/includes/plugin.php';
835 }
836 $search = sanitize_text_field( $a['search'] ?? '' );
837 $out = [];
838 foreach ( get_plugins() as $p ) {
839 if ( !$search || stripos( $p['Name'], $search ) !== false ) {
840 $out[] = [ 'Name' => $p['Name'], 'Version' => $p['Version'] ];
841 }
842 }
843 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
844 break;
845
846 /* ===== Posts: list ===== */
847 case 'wp_get_posts':
848 $q = [
849 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
850 'post_status' => sanitize_key( $a['post_status'] ?? 'publish' ),
851 's' => sanitize_text_field( $a['search'] ?? '' ),
852 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
853 ];
854 if ( isset( $a['offset'] ) ) {
855 $q['offset'] = max( 0, intval( $a['offset'] ) );
856 }
857 if ( isset( $a['paged'] ) ) {
858 $q['paged'] = max( 1, intval( $a['paged'] ) );
859 }
860 $date = [];
861 if ( $a['after'] ?? '' ) {
862 $date['after'] = $a['after'];
863 }
864 if ( $a['before'] ?? '' ) {
865 $date['before'] = $a['before'];
866 }
867 if ( $date ) {
868 $q['date_query'] = [ $date ];
869 }
870 $rows = [];
871 foreach ( get_posts( $q ) as $p ) {
872 $rows[] = [
873 'ID' => $p->ID,
874 'post_title' => $p->post_title,
875 'post_status' => $p->post_status,
876 'post_excerpt' => $this->post_excerpt( $p ),
877 'permalink' => get_permalink( $p ),
878 ];
879 }
880 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
881 break;
882
883 /* ===== Posts: single ===== */
884 case 'wp_get_post':
885 if ( empty( $a['ID'] ) ) {
886 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
887 break;
888 }
889 $p = get_post( intval( $a['ID'] ) );
890 if ( !$p ) {
891 $r['error'] = [ 'code' => -32602, 'message' => 'Post not found' ];
892 break;
893 }
894 $out = [
895 'ID' => $p->ID,
896 'post_title' => $p->post_title,
897 'post_status' => $p->post_status,
898 'post_content' => $this->clean_html( $p->post_content ),
899 'post_excerpt' => $this->post_excerpt( $p ),
900 'permalink' => get_permalink( $p ),
901 'post_date' => $p->post_date,
902 'post_modified' => $p->post_modified,
903 ];
904 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
905 break;
906
907 /* ===== Posts: snapshot ===== */
908 case 'wp_get_post_snapshot':
909 if ( empty( $a['ID'] ) ) {
910 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
911 break;
912 }
913
914 $post_id = intval( $a['ID'] );
915 $p = get_post( $post_id );
916
917 if ( !$p ) {
918 $r['error'] = [ 'code' => -32602, 'message' => 'Post not found' ];
919 break;
920 }
921
922 $include = $a['include'] ?? [ 'meta', 'terms', 'thumbnail', 'author' ];
923
924 $snapshot = [
925 'post' => [
926 'ID' => $p->ID,
927 'post_title' => $p->post_title,
928 'post_type' => $p->post_type,
929 'post_status' => $p->post_status,
930 'post_content' => $this->clean_html( $p->post_content ),
931 'post_excerpt' => $this->post_excerpt( $p ),
932 'post_name' => $p->post_name,
933 'permalink' => get_permalink( $p ),
934 'post_date' => $p->post_date,
935 'post_modified' => $p->post_modified,
936 ],
937 ];
938
939 // Include all post meta
940 if ( in_array( 'meta', $include ) ) {
941 $snapshot['meta'] = [];
942 $all_meta = get_post_meta( $post_id );
943 foreach ( $all_meta as $key => $value ) {
944 if ( is_array( $value ) && count( $value ) === 1 ) {
945 $snapshot['meta'][ $key ] = maybe_unserialize( $value[0] );
946 }
947 else {
948 $snapshot['meta'][ $key ] = array_map( 'maybe_unserialize', $value );
949 }
950 }
951 }
952
953 // Include all taxonomies and their terms
954 if ( in_array( 'terms', $include ) ) {
955 $snapshot['terms'] = [];
956 $taxonomies = get_object_taxonomies( $p->post_type );
957 foreach ( $taxonomies as $taxonomy ) {
958 $terms = wp_get_post_terms( $post_id, $taxonomy, [ 'fields' => 'all' ] );
959 if ( !is_wp_error( $terms ) && !empty( $terms ) ) {
960 $snapshot['terms'][ $taxonomy ] = array_map( function ( $t ) {
961 return [
962 'term_id' => $t->term_id,
963 'name' => $t->name,
964 'slug' => $t->slug,
965 ];
966 }, $terms );
967 }
968 }
969 }
970
971 // Include featured image
972 if ( in_array( 'thumbnail', $include ) ) {
973 $thumb_id = get_post_thumbnail_id( $post_id );
974 if ( $thumb_id ) {
975 $snapshot['thumbnail'] = [
976 'ID' => $thumb_id,
977 'url' => wp_get_attachment_url( $thumb_id ),
978 'alt' => get_post_meta( $thumb_id, '_wp_attachment_image_alt', true ),
979 ];
980 }
981 }
982
983 // Include author
984 if ( in_array( 'author', $include ) ) {
985 $author = get_userdata( $p->post_author );
986 if ( $author ) {
987 $snapshot['author'] = [
988 'ID' => $author->ID,
989 'display_name' => $author->display_name,
990 'user_login' => $author->user_login,
991 ];
992 }
993 }
994
995 $this->add_result_text( $r, wp_json_encode( $snapshot, JSON_PRETTY_PRINT ) );
996 break;
997
998 /* ===== Posts: create ===== */
999 case 'wp_create_post':
1000 if ( empty( $a['post_title'] ) ) {
1001 $r['error'] = [ 'code' => -32602, 'message' => 'post_title required' ];
1002 break;
1003 }
1004 $ins = [
1005 'post_title' => sanitize_text_field( $a['post_title'] ),
1006 'post_status' => sanitize_key( $a['post_status'] ?? 'draft' ),
1007 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
1008 ];
1009 if ( $a['post_content'] ?? '' ) {
1010 $ins['post_content'] = $this->core->markdown_to_html( $a['post_content'] );
1011 }
1012 if ( $a['post_excerpt'] ?? '' ) {
1013 $ins['post_excerpt'] = $this->clean_html( $a['post_excerpt'] );
1014 }
1015 if ( $a['post_name'] ?? '' ) {
1016 $ins['post_name'] = sanitize_title( $a['post_name'] );
1017 }
1018 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) {
1019 $ins['meta_input'] = $a['meta_input'];
1020 }
1021 $new = wp_insert_post( $ins, true );
1022 if ( is_wp_error( $new ) ) {
1023 $r['error'] = [ 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ];
1024 }
1025 else {
1026 if ( empty( $ins['meta_input'] ) && !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) {
1027 foreach ( $a['meta_input'] as $k => $v ) {
1028 update_post_meta( $new, sanitize_key( $k ), maybe_serialize( $v ) );
1029 }
1030 }
1031 $this->add_result_text( $r, 'Post created ID ' . $new );
1032 }
1033 break;
1034
1035 /* ===== Posts: update ===== */
1036 case 'wp_update_post':
1037 if ( empty( $a['ID'] ) ) {
1038 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1039 break;
1040 }
1041 $c = [ 'ID' => intval( $a['ID'] ) ];
1042 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) ) {
1043 foreach ( $a['fields'] as $k => $v ) {
1044 $c[ $k ] = in_array( $k, [ 'post_content', 'post_excerpt' ], true ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
1045 }
1046 }
1047 $u = ( count( $c ) > 1 ) ? wp_update_post( $c, true ) : $c['ID'];
1048 if ( is_wp_error( $u ) ) {
1049 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
1050 break;
1051 }
1052 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) {
1053 foreach ( $a['meta_input'] as $k => $v ) {
1054 update_post_meta( $u, sanitize_key( $k ), maybe_serialize( $v ) );
1055 }
1056 }
1057 $this->add_result_text( $r, 'Post #' . $u . ' updated' );
1058 break;
1059
1060 /* ===== Posts: delete ===== */
1061 case 'wp_delete_post':
1062 if ( empty( $a['ID'] ) ) {
1063 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1064 break;
1065 }
1066 $del = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
1067 if ( $del ) {
1068 $this->add_result_text( $r, 'Post #' . $a['ID'] . ' deleted' );
1069 }
1070 else {
1071 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1072 }
1073 break;
1074
1075 /* ===== Post-meta ===== */
1076 case 'wp_get_post_meta':
1077 if ( empty( $a['ID'] ) ) {
1078 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1079 break;
1080 }
1081 $pid = intval( $a['ID'] );
1082 $out = ( $a['key'] ?? '' ) ? get_post_meta( $pid, sanitize_key( $a['key'] ), true ) : get_post_meta( $pid );
1083 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1084 break;
1085
1086 case 'wp_update_post_meta':
1087 if ( empty( $a['ID'] ) ) {
1088 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1089 break;
1090 }
1091 $pid = intval( $a['ID'] );
1092 if ( !empty( $a['meta'] ) && is_array( $a['meta'] ) ) {
1093 foreach ( $a['meta'] as $k => $v ) {
1094 update_post_meta( $pid, sanitize_key( $k ), maybe_serialize( $v ) );
1095 }
1096 }
1097 elseif ( isset( $a['key'], $a['value'] ) ) {
1098 update_post_meta( $pid, sanitize_key( $a['key'] ), maybe_serialize( $a['value'] ) );
1099 }
1100 else {
1101 $r['error'] = [ 'code' => -32602, 'message' => 'meta array or key/value required' ];
1102 break;
1103 }
1104 $this->add_result_text( $r, 'Meta updated for post #' . $pid );
1105 break;
1106
1107 case 'wp_delete_post_meta':
1108 if ( empty( $a['ID'] ) || empty( $a['key'] ) ) {
1109 $r['error'] = [ 'code' => -32602, 'message' => 'ID & key required' ];
1110 break;
1111 }
1112 $pid = intval( $a['ID'] );
1113 $key = sanitize_key( $a['key'] );
1114 $done = isset( $a['value'] ) ? delete_post_meta( $pid, $key, maybe_serialize( $a['value'] ) ) : delete_post_meta( $pid, $key );
1115 if ( $done ) {
1116 $this->add_result_text( $r, 'Meta deleted on post #' . $pid );
1117 }
1118 else {
1119 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1120 }
1121 break;
1122
1123 /* ===== Featured image ===== */
1124 case 'wp_set_featured_image':
1125 if ( empty( $a['post_id'] ) ) {
1126 $r['error'] = [ 'code' => -32602, 'message' => 'post_id required' ];
1127 break;
1128 }
1129 $post_id = intval( $a['post_id'] );
1130 $media_id = isset( $a['media_id'] ) ? intval( $a['media_id'] ) : 0;
1131 if ( $media_id ) {
1132 $done = set_post_thumbnail( $post_id, $media_id );
1133 if ( $done ) {
1134 $this->add_result_text( $r, 'Featured image set on post #' . $post_id );
1135 }
1136 else {
1137 $r['error'] = [ 'code' => -32603, 'message' => 'Failed to set thumbnail' ];
1138 }
1139 }
1140 else {
1141 delete_post_thumbnail( $post_id );
1142 $this->add_result_text( $r, 'Featured image removed from post #' . $post_id );
1143 }
1144 break;
1145
1146 /* ===== Taxonomies ===== */
1147 case 'wp_get_taxonomies':
1148 $pt = sanitize_key( $a['post_type'] ?? 'post' );
1149 $out = [];
1150 foreach ( get_object_taxonomies( $pt, 'objects' ) as $t ) {
1151 $out[] = [ 'key' => $t->name, 'label' => $t->label ];
1152 }
1153 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1154 break;
1155
1156 case 'wp_get_terms':
1157 $tax = sanitize_key( $a['taxonomy'] );
1158 $args = [
1159 'taxonomy' => $tax,
1160 'hide_empty' => false,
1161 'number' => intval( $a['limit'] ?? 0 ),
1162 'search' => $a['search'] ?? '',
1163 ];
1164 if ( isset( $a['parent'] ) ) {
1165 $args['parent'] = intval( $a['parent'] );
1166 }
1167 $out = [];
1168 foreach ( get_terms( $args ) as $t ) {
1169 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name, 'slug' => $t->slug, 'count' => $t->count ];
1170 }
1171 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1172 break;
1173
1174 case 'wp_create_term':
1175 if ( empty( $a['term_name'] ) ) {
1176 $r['error'] = [ 'code' => -32602, 'message' => 'term_name required' ];
1177 break;
1178 }
1179 $tax = sanitize_key( $a['taxonomy'] );
1180 $args = [];
1181 if ( $a['slug'] ?? '' ) {
1182 $args['slug'] = sanitize_title( $a['slug'] );
1183 }
1184 if ( $a['description'] ?? '' ) {
1185 $args['description'] = sanitize_text_field( $a['description'] );
1186 }
1187 if ( isset( $a['parent'] ) ) {
1188 $args['parent'] = intval( $a['parent'] );
1189 }
1190 $term = wp_insert_term( sanitize_text_field( $a['term_name'] ), $tax, $args );
1191 if ( is_wp_error( $term ) ) {
1192 $r['error'] = [ 'code' => $term->get_error_code(), 'message' => $term->get_error_message() ];
1193 }
1194 else {
1195 $this->add_result_text( $r, 'Term ' . $term['term_id'] . ' created' );
1196 }
1197 break;
1198
1199 case 'wp_update_term':
1200 $tid = intval( $a['term_id'] ?? 0 );
1201 if ( !$tid ) {
1202 $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ];
1203 break;
1204 }
1205 $tax = sanitize_key( $a['taxonomy'] );
1206 $uargs = [];
1207 foreach ( [ 'name', 'slug', 'description', 'parent' ] as $f ) {
1208 if ( isset( $a[$f] ) ) {
1209 $uargs[$f] = $a[$f];
1210 }
1211 }
1212 $t = wp_update_term( $tid, $tax, $uargs );
1213 if ( is_wp_error( $t ) ) {
1214 $r['error'] = [ 'code' => $t->get_error_code(), 'message' => $t->get_error_message() ];
1215 }
1216 else {
1217 $this->add_result_text( $r, 'Term ' . $tid . ' updated' );
1218 }
1219 break;
1220
1221 case 'wp_delete_term':
1222 $tid = intval( $a['term_id'] ?? 0 );
1223 if ( !$tid ) {
1224 $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ];
1225 break;
1226 }
1227 $tax = sanitize_key( $a['taxonomy'] );
1228 $d = wp_delete_term( $tid, $tax );
1229 if ( $d ) {
1230 $this->add_result_text( $r, 'Term ' . $tid . ' deleted' );
1231 }
1232 else {
1233 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1234 }
1235 break;
1236
1237 case 'wp_get_post_terms':
1238 if ( empty( $a['ID'] ) ) {
1239 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1240 break;
1241 }
1242 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
1243 $out = [];
1244 foreach ( wp_get_post_terms( intval( $a['ID'] ), $tax, [ 'fields' => 'all' ] ) as $t ) {
1245 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name ];
1246 }
1247 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1248 break;
1249
1250 case 'wp_add_post_terms':
1251 if ( empty( $a['ID'] ) || empty( $a['terms'] ) ) {
1252 $r['error'] = [ 'code' => -32602, 'message' => 'ID & terms required' ];
1253 break;
1254 }
1255 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
1256 $append = !isset( $a['append'] ) || $a['append'];
1257 $set = wp_set_post_terms( intval( $a['ID'] ), $a['terms'], $tax, $append );
1258 if ( is_wp_error( $set ) ) {
1259 $r['error'] = [ 'code' => $set->get_error_code(), 'message' => $set->get_error_message() ];
1260 }
1261 else {
1262 $this->add_result_text( $r, 'Terms set for post #' . $a['ID'] );
1263 }
1264 break;
1265
1266 /* ===== Media: list ===== */
1267 case 'wp_get_media':
1268 $q = [
1269 'post_type' => 'attachment',
1270 's' => $a['search'] ?? '',
1271 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
1272 'post_status' => 'inherit',
1273 ];
1274 $d = [];
1275 if ( $a['after'] ?? '' ) {
1276 $d['after'] = $a['after'];
1277 }
1278 if ( $a['before'] ?? '' ) {
1279 $d['before'] = $a['before'];
1280 }
1281 if ( $d ) {
1282 $q['date_query'] = [ $d ];
1283 }
1284 $list = [];
1285 foreach ( get_posts( $q ) as $m ) {
1286 $list[] = [ 'ID' => $m->ID, 'title' => $m->post_title, 'url' => wp_get_attachment_url( $m->ID ) ];
1287 }
1288 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
1289 break;
1290
1291 /* ===== Media: upload ===== */
1292 case 'wp_upload_media':
1293 if ( empty( $a['url'] ) ) {
1294 $r['error'] = [ 'code' => -32602, 'message' => 'url required' ];
1295 break;
1296 }
1297 try {
1298 require_once ABSPATH . 'wp-admin/includes/file.php';
1299 require_once ABSPATH . 'wp-admin/includes/media.php';
1300 require_once ABSPATH . 'wp-admin/includes/image.php';
1301 $tmp = download_url( $a['url'] );
1302 if ( is_wp_error( $tmp ) ) {
1303 throw new Exception( $tmp->get_error_message(), $tmp->get_error_code() );
1304 }
1305 $file = [ 'name' => basename( parse_url( $a['url'], PHP_URL_PATH ) ), 'tmp_name' => $tmp ];
1306 $id = media_handle_sideload( $file, 0, $a['description'] ?? '' );
1307 @unlink( $tmp );
1308 if ( is_wp_error( $id ) ) {
1309 throw new Exception( $id->get_error_message(), $id->get_error_code() );
1310 }
1311 if ( $a['title'] ?? '' ) {
1312 wp_update_post( [ 'ID' => $id, 'post_title' => sanitize_text_field( $a['title'] ) ] );
1313 }
1314 if ( $a['alt'] ?? '' ) {
1315 update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
1316 }
1317 $this->add_result_text( $r, wp_get_attachment_url( $id ) );
1318 }
1319 catch ( \Throwable $e ) {
1320 $r['error'] = [ 'code' => $e->getCode() ?: -32603, 'message' => $e->getMessage() ];
1321 }
1322 break;
1323
1324 /* ===== Media: update ===== */
1325 case 'wp_update_media':
1326 if ( empty( $a['ID'] ) ) {
1327 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1328 break;
1329 }
1330 $upd = [ 'ID' => intval( $a['ID'] ) ];
1331 if ( $a['title'] ?? '' ) {
1332 $upd['post_title'] = sanitize_text_field( $a['title'] );
1333 }
1334 if ( $a['caption'] ?? '' ) {
1335 $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1336 }
1337 if ( $a['description'] ?? '' ) {
1338 $upd['post_content'] = $this->clean_html( $a['description'] );
1339 }
1340 $u = wp_update_post( $upd, true );
1341 if ( is_wp_error( $u ) ) {
1342 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
1343 }
1344 else {
1345 if ( $a['alt'] ?? '' ) {
1346 update_post_meta( $u, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
1347 }
1348 $this->add_result_text( $r, 'Media #' . $u . ' updated' );
1349 }
1350 break;
1351
1352 /* ===== Media: delete ===== */
1353 case 'wp_delete_media':
1354 if ( empty( $a['ID'] ) ) {
1355 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1356 break;
1357 }
1358 $d = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
1359 if ( $d ) {
1360 $this->add_result_text( $r, 'Media #' . $a['ID'] . ' deleted' );
1361 }
1362 else {
1363 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1364 }
1365 break;
1366
1367 /* ===== MWAI Vision ===== */
1368 case 'mwai_vision':
1369 if ( empty( $a['message'] ) ) {
1370 $r['error'] = [ 'code' => -32602, 'message' => 'message required' ];
1371 break;
1372 }
1373 global $mwai;
1374 if ( !isset( $mwai ) ) {
1375 $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ];
1376 break;
1377 }
1378 $analysis = $mwai->simpleVisionQuery(
1379 $a['message'],
1380 $a['url'] ?? null,
1381 $a['path'] ?? null,
1382 [ 'scope' => 'mcp' ]
1383 );
1384 $this->add_result_text( $r, is_string( $analysis ) ? $analysis : wp_json_encode( $analysis, JSON_PRETTY_PRINT ) );
1385 break;
1386
1387 /* ===== MWAI Image ===== */
1388 case 'mwai_image':
1389 if ( empty( $a['message'] ) ) {
1390 $r['error'] = [ 'code' => -32602, 'message' => 'message required' ];
1391 break;
1392 }
1393 global $mwai;
1394 if ( !isset( $mwai ) ) {
1395 $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ];
1396 break;
1397 }
1398
1399 $media = $mwai->imageQueryForMediaLibrary( $a['message'], [ 'scope' => 'mcp' ], $a['postId'] ?? null );
1400 if ( is_wp_error( $media ) ) {
1401 $r['error'] = [ 'code' => $media->get_error_code(), 'message' => $media->get_error_message() ];
1402 break;
1403 }
1404
1405 $mid = intval( $media['id'] );
1406
1407 $upd = [ 'ID' => $mid ];
1408 if ( !empty( $a['title'] ) ) {
1409 $upd['post_title'] = sanitize_text_field( $a['title'] );
1410 }
1411 if ( !empty( $a['caption'] ) ) {
1412 $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1413 }
1414 if ( !empty( $a['description'] ) ) {
1415 $upd['post_content'] = $this->clean_html( $a['description'] );
1416 }
1417 if ( count( $upd ) > 1 ) {
1418 wp_update_post( $upd, true );
1419 }
1420 if ( array_key_exists( 'alt', $a ) ) {
1421 update_post_meta( $mid, '_wp_attachment_image_alt', sanitize_text_field( (string) $a['alt'] ) );
1422 }
1423
1424 $media = [
1425 'id' => $mid,
1426 'url' => wp_get_attachment_url( $mid ),
1427 'title' => get_the_title( $mid ),
1428 'caption' => wp_get_attachment_caption( $mid ),
1429 'alt' => get_post_meta( $mid, '_wp_attachment_image_alt', true ),
1430 ];
1431 $this->add_result_text( $r, wp_json_encode( $media, JSON_PRETTY_PRINT ) );
1432 break;
1433
1434 default: $r['error'] = [ 'code' => -32601, 'message' => 'Unknown tool' ];
1435 }
1436 return $r;
1437 }
1438 #endregion
1439 }
1440