PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.3.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.3.4
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 5 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 5 months ago
mcp-core.php
1577 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 'exclude' => [
265 'type' => 'array',
266 'description' => 'Optional: fields to exclude from post data. Options: content (useful for posts with huge content like many galleries)',
267 'items' => [ 'type' => 'string' ],
268 ],
269 ],
270 'required' => [ 'ID' ],
271 ],
272 ],
273 'wp_create_post' => [
274 'name' => 'wp_create_post',
275 '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.',
276 'inputSchema' => [
277 'type' => 'object',
278 'properties' => [
279 'post_title' => [ 'type' => 'string' ],
280 'post_content' => [ 'type' => 'string' ],
281 'post_excerpt' => [ 'type' => 'string' ],
282 'post_status' => [ 'type' => 'string' ],
283 'post_type' => [ 'type' => 'string' ],
284 'post_name' => [ 'type' => 'string' ],
285 'meta_input' => [ 'type' => 'object', 'description' => 'Associative array of custom fields.' ],
286 ],
287 'required' => [ 'post_title' ],
288 ],
289 ],
290 'wp_update_post' => [
291 'name' => 'wp_update_post',
292 '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. Use schedule_for to easily schedule posts.',
293 'inputSchema' => [
294 'type' => 'object',
295 'properties' => [
296 'ID' => [ 'type' => 'integer', 'description' => 'The ID of the post to update.' ],
297 'fields' => [
298 'type' => 'object',
299 'properties' => [
300 'post_title' => [ 'type' => 'string' ],
301 'post_content' => [ 'type' => 'string' ],
302 'post_status' => [ 'type' => 'string' ],
303 'post_name' => [ 'type' => 'string' ],
304 'post_excerpt' => [ 'type' => 'string' ],
305 'post_category' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
306 ],
307 'additionalProperties' => true
308 ],
309 'meta_input' => [
310 'type' => 'object',
311 'description' => 'Associative array of custom fields.'
312 ],
313 'schedule_for' => [
314 'type' => 'string',
315 'description' => 'Schedule post for future publication. Provide local datetime (e.g., "2026-02-02 09:00:00"). Automatically sets status to "future" and calculates GMT from WordPress timezone.'
316 ],
317 ],
318 'required' => [ 'ID' ],
319 ],
320 ],
321 'wp_delete_post' => [
322 'name' => 'wp_delete_post',
323 'description' => 'Delete/trash a post.',
324 'inputSchema' => [
325 'type' => 'object',
326 'properties' => [
327 'ID' => [ 'type' => 'integer' ],
328 'force' => [ 'type' => 'boolean' ],
329 ],
330 'required' => [ 'ID' ],
331 ],
332 ],
333
334 /* -------- Post-meta -------- */
335 'wp_get_post_meta' => [
336 'name' => 'wp_get_post_meta',
337 '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.',
338 'inputSchema' => [
339 'type' => 'object',
340 'properties' => [
341 'ID' => [ 'type' => 'integer' ],
342 'key' => [ 'type' => 'string' ],
343 ],
344 'required' => [ 'ID' ],
345 ],
346 ],
347 'wp_update_post_meta' => [
348 'name' => 'wp_update_post_meta',
349 '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.',
350 'inputSchema' => [
351 'type' => 'object',
352 'properties' => [
353 'ID' => [ 'type' => 'integer' ],
354 'meta' => [ 'type' => 'object', 'description' => 'Key/value pairs to set. Alternative: provide "key" + "value".' ],
355 'key' => [ 'type' => 'string' ],
356 'value' => [ 'type' => [ 'string', 'number', 'boolean' ] ],
357 ],
358 'required' => [ 'ID' ],
359 ],
360 ],
361 'wp_delete_post_meta' => [
362 'name' => 'wp_delete_post_meta',
363 'description' => 'Delete custom field(s) from a post. Provide value to remove a single row; omit value to delete all rows for the key.',
364 'inputSchema' => [
365 'type' => 'object',
366 'properties' => [
367 'ID' => [ 'type' => 'integer' ],
368 'key' => [ 'type' => 'string' ],
369 'value' => [ 'type' => [ 'string', 'number', 'boolean' ] ],
370 ],
371 'required' => [ 'ID', 'key' ],
372 ],
373 ],
374
375 /* -------- Featured image -------- */
376 'wp_set_featured_image' => [
377 'name' => 'wp_set_featured_image',
378 'description' => 'Attach or remove a featured image (thumbnail) for a post/page. Provide media_id to attach, omit or null to remove.',
379 'inputSchema' => [
380 'type' => 'object',
381 'properties' => [
382 'post_id' => [ 'type' => 'integer' ],
383 'media_id' => [ 'type' => 'integer' ],
384 ],
385 'required' => [ 'post_id' ],
386 ],
387 ],
388
389 /* -------- Taxonomies / Terms -------- */
390 'wp_get_taxonomies' => [
391 'name' => 'wp_get_taxonomies',
392 'description' => 'List taxonomies for a post type.',
393 'inputSchema' => [
394 'type' => 'object',
395 'properties' => [ 'post_type' => [ 'type' => 'string' ] ],
396 ],
397 ],
398 'wp_get_terms' => [
399 'name' => 'wp_get_terms',
400 'description' => 'List terms of a taxonomy.',
401 'inputSchema' => [
402 'type' => 'object',
403 'properties' => [
404 'taxonomy' => [ 'type' => 'string' ],
405 'search' => [ 'type' => 'string' ],
406 'parent' => [ 'type' => 'integer' ],
407 'limit' => [ 'type' => 'integer' ],
408 ],
409 'required' => [ 'taxonomy' ],
410 ],
411 ],
412 'wp_create_term' => [
413 'name' => 'wp_create_term',
414 'description' => 'Create a term.',
415 'inputSchema' => [
416 'type' => 'object',
417 'properties' => [
418 'taxonomy' => [ 'type' => 'string' ],
419 'term_name' => [ 'type' => 'string' ],
420 'slug' => [ 'type' => 'string' ],
421 'description' => [ 'type' => 'string' ],
422 'parent' => [ 'type' => 'integer' ],
423 ],
424 'required' => [ 'taxonomy', 'term_name' ],
425 ],
426 ],
427 'wp_update_term' => [
428 'name' => 'wp_update_term',
429 'description' => 'Update a term.',
430 'inputSchema' => [
431 'type' => 'object',
432 'properties' => [
433 'term_id' => [ 'type' => 'integer' ],
434 'taxonomy' => [ 'type' => 'string' ],
435 'name' => [ 'type' => 'string' ],
436 'slug' => [ 'type' => 'string' ],
437 'description' => [ 'type' => 'string' ],
438 'parent' => [ 'type' => 'integer' ],
439 ],
440 'required' => [ 'term_id', 'taxonomy' ],
441 ],
442 ],
443 'wp_delete_term' => [
444 'name' => 'wp_delete_term',
445 'description' => 'Delete a term.',
446 'inputSchema' => [
447 'type' => 'object',
448 'properties' => [
449 'term_id' => [ 'type' => 'integer' ],
450 'taxonomy' => [ 'type' => 'string' ],
451 ],
452 'required' => [ 'term_id', 'taxonomy' ],
453 ],
454 ],
455 'wp_get_post_terms' => [
456 'name' => 'wp_get_post_terms',
457 'description' => 'Get terms attached to a post.',
458 'inputSchema' => [
459 'type' => 'object',
460 'properties' => [
461 'ID' => [ 'type' => 'integer' ],
462 'taxonomy' => [ 'type' => 'string' ],
463 ],
464 'required' => [ 'ID' ],
465 ],
466 ],
467 'wp_add_post_terms' => [
468 'name' => 'wp_add_post_terms',
469 '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.).',
470 'inputSchema' => [
471 'type' => 'object',
472 'properties' => [
473 'ID' => [ 'type' => 'integer' ],
474 'taxonomy' => [ 'type' => 'string' ],
475 'terms' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
476 'append' => [ 'type' => 'boolean' ],
477 ],
478 'required' => [ 'ID', 'terms' ],
479 ],
480 ],
481
482 /* -------- Media -------- */
483 'wp_get_media' => [
484 'name' => 'wp_get_media',
485 'description' => 'List media items.',
486 'inputSchema' => [
487 'type' => 'object',
488 'properties' => [
489 'search' => [ 'type' => 'string' ],
490 'after' => [ 'type' => 'string' ],
491 'before' => [ 'type' => 'string' ],
492 'limit' => [ 'type' => 'integer' ],
493 ],
494 ],
495 ],
496 'wp_upload_media' => [
497 'name' => 'wp_upload_media',
498 'description' => 'Upload a file to the WordPress Media Library. Provide either a url (WordPress will download it) or base64-encoded content with a filename. Base64 mode is useful for local files but doubles the payload size — keep files under a few MB to avoid memory or timeout issues.',
499 'inputSchema' => [
500 'type' => 'object',
501 'properties' => [
502 'url' => [
503 'type' => 'string',
504 'description' => 'URL to download the file from. Use this OR base64/filename.',
505 ],
506 'base64' => [
507 'type' => 'string',
508 'description' => 'Base64-encoded file content. Must be used together with filename.',
509 ],
510 'filename' => [
511 'type' => 'string',
512 'description' => 'Filename with extension (e.g. photo.jpg). Required when using base64.',
513 ],
514 'title' => [ 'type' => 'string' ],
515 'description' => [ 'type' => 'string' ],
516 'alt' => [ 'type' => 'string' ],
517 ],
518 ],
519 ],
520 'wp_upload_request' => [
521 'name' => 'wp_upload_request',
522 'description' => 'Upload a local file to the WordPress Media Library via a temporary upload endpoint. Use this instead of wp_upload_media when you have a local file (not a URL) — passing large base64 strings through MCP is impractical and will likely exceed context limits. Call this tool with the filename and optional metadata; it returns a one-time upload URL. Then use curl to POST the file: curl -X POST -F "file=@/local/path/file.jpg" "<upload_url>". The upload URL expires after 5 minutes and can only be used once.',
523 'inputSchema' => [
524 'type' => 'object',
525 'properties' => [
526 'filename' => [
527 'type' => 'string',
528 'description' => 'Filename with extension (e.g. photo.jpg).',
529 ],
530 'title' => [ 'type' => 'string' ],
531 'description' => [ 'type' => 'string' ],
532 'alt' => [ 'type' => 'string' ],
533 ],
534 'required' => [ 'filename' ],
535 ],
536 ],
537 'wp_update_media' => [
538 'name' => 'wp_update_media',
539 'description' => 'Update attachment meta.',
540 'inputSchema' => [
541 'type' => 'object',
542 'properties' => [
543 'ID' => [ 'type' => 'integer' ],
544 'title' => [ 'type' => 'string' ],
545 'caption' => [ 'type' => 'string' ],
546 'description' => [ 'type' => 'string' ],
547 'alt' => [ 'type' => 'string' ],
548 ],
549 'required' => [ 'ID' ],
550 ],
551 ],
552 'wp_delete_media' => [
553 'name' => 'wp_delete_media',
554 'description' => 'Delete/trash an attachment.',
555 'inputSchema' => [
556 'type' => 'object',
557 'properties' => [
558 'ID' => [ 'type' => 'integer' ],
559 'force' => [ 'type' => 'boolean' ],
560 ],
561 'required' => [ 'ID' ],
562 ],
563 ],
564
565 /* -------- MWAI Vision / Image -------- */
566 'mwai_vision' => [
567 'name' => 'mwai_vision',
568 'description' => 'Analyze an image via AI Engine Vision.',
569 'inputSchema' => [
570 'type' => 'object',
571 'properties' => [
572 'message' => [ 'type' => 'string' ],
573 'url' => [ 'type' => 'string' ],
574 'path' => [ 'type' => 'string' ],
575 ],
576 'required' => [ 'message' ],
577 ],
578 ],
579 'mwai_image' => [
580 'name' => 'mwai_image',
581 '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 }.',
582 'inputSchema' => [
583 'type' => 'object',
584 'properties' => [
585 'message' => [ 'type' => 'string', 'description' => 'Prompt describing the desired image.' ],
586 'postId' => [ 'type' => 'integer', 'description' => 'Optional post ID to attach the image to.' ],
587 'title' => [ 'type' => 'string' ],
588 'caption' => [ 'type' => 'string' ],
589 'description' => [ 'type' => 'string' ],
590 'alt' => [ 'type' => 'string' ],
591 ],
592 'required' => [ 'message' ],
593 ],
594 ],
595
596 ];
597 }
598 #endregion
599
600 #region Tool Registration
601 public function register_rest_tools( array $prev ): array {
602 $tools = $this->tools();
603
604 // All 36 core tools enabled and tested with ChatGPT.
605 // Automatic validation in mcp.php fixes problematic type definitions.
606
607 // Add category and annotations to each tool
608 foreach ( $tools as &$tool ) {
609 if ( !isset( $tool['category'] ) ) {
610 $tool['category'] = 'AI Engine (Core)';
611 }
612
613 // Add MCP tool annotations based on tool name/behavior
614 if ( !isset( $tool['annotations'] ) ) {
615 $name = $tool['name'];
616
617 // Read-only tools (safe, no modifications)
618 $is_readonly = (
619 strpos( $name, 'wp_get_' ) === 0 ||
620 strpos( $name, 'wp_list_' ) === 0 ||
621 strpos( $name, 'wp_count_' ) === 0 ||
622 $name === 'mwai_vision'
623 );
624
625 // Destructive tools (can delete/destroy data)
626 $is_destructive = (
627 strpos( $name, 'wp_delete_' ) === 0 ||
628 $name === 'wp_update_user' // Can change passwords/roles
629 );
630
631 $tool['annotations'] = [
632 'readOnlyHint' => $is_readonly,
633 'destructiveHint' => !$is_readonly && $is_destructive,
634 'openWorldHint' => false, // All operate on closed WordPress system
635 ];
636 }
637 }
638
639 $merged = array_merge( $prev, array_values( $tools ) );
640 return $merged;
641 }
642 #endregion
643
644 #region Callback
645 public function handle_call( $prev, string $tool, array $args, ?int $id ) {
646 // Security check is already done in the MCP auth layer
647 // If we reach here, the user is authorized to use MCP
648 if ( !empty( $prev ) || !isset( $this->tools()[ $tool ] ) ) {
649 return $prev;
650 }
651 return $this->dispatch( $tool, $args, $id );
652 }
653 #endregion
654
655 #region Dispatcher
656 private function dispatch( string $tool, array $a, ?int $id ): array {
657 $r = [ 'jsonrpc' => '2.0', 'id' => $id ];
658
659 switch ( $tool ) {
660
661 /* ===== Users ===== */
662 case 'wp_get_users':
663 $q = [
664 'search' => '*' . esc_attr( $a['search'] ?? '' ) . '*',
665 'role' => $a['role'] ?? '',
666 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
667 ];
668 if ( isset( $a['offset'] ) ) {
669 $q['offset'] = max( 0, intval( $a['offset'] ) );
670 }
671 if ( isset( $a['paged'] ) ) {
672 $q['paged'] = max( 1, intval( $a['paged'] ) );
673 }
674 $rows = [];
675 foreach ( get_users( $q ) as $u ) {
676 $rows[] = [
677 'ID' => $u->ID,
678 'user_login' => $u->user_login,
679 'display_name' => $u->display_name,
680 'roles' => $u->roles,
681 ];
682 }
683 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
684 break;
685
686 case 'wp_create_user':
687 $data = [
688 'user_login' => sanitize_user( $a['user_login'] ),
689 'user_email' => sanitize_email( $a['user_email'] ),
690 'user_pass' => $a['user_pass'] ?? wp_generate_password( 12, true ),
691 'display_name' => sanitize_text_field( $a['display_name'] ?? '' ),
692 'role' => sanitize_key( $a['role'] ?? get_option( 'default_role', 'subscriber' ) ),
693 ];
694 $uid = wp_insert_user( $data );
695 if ( is_wp_error( $uid ) ) {
696 $r['error'] = [ 'code' => $uid->get_error_code(), 'message' => $uid->get_error_message() ];
697 }
698 else {
699 $this->add_result_text( $r, 'User created ID ' . $uid );
700 }
701 break;
702
703 case 'wp_update_user':
704 if ( empty( $a['ID'] ) ) {
705 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
706 break;
707 }
708 $upd = [ 'ID' => intval( $a['ID'] ) ];
709 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) ) {
710 foreach ( $a['fields'] as $k => $v ) {
711 $upd[ $k ] = ( $k === 'role' ) ? sanitize_key( $v ) : sanitize_text_field( $v );
712 }
713 }
714 $u = wp_update_user( $upd );
715 if ( is_wp_error( $u ) ) {
716 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
717 }
718 else {
719 $this->add_result_text( $r, 'User #' . $u . ' updated' );
720 }
721 break;
722
723 /* ===== Comments ===== */
724 case 'wp_get_comments':
725 $args = [
726 'post_id' => isset( $a['post_id'] ) ? intval( $a['post_id'] ) : '',
727 'status' => $a['status'] ?? 'approve',
728 'search' => $a['search'] ?? '',
729 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
730 ];
731 if ( isset( $a['offset'] ) ) {
732 $args['offset'] = max( 0, intval( $a['offset'] ) );
733 }
734 if ( isset( $a['paged'] ) ) {
735 $args['paged'] = max( 1, intval( $a['paged'] ) );
736 }
737 $list = [];
738 foreach ( get_comments( $args ) as $c ) {
739 $list[] = [
740 'comment_ID' => $c->comment_ID,
741 'comment_post_ID' => $c->comment_post_ID,
742 'comment_author' => $c->comment_author,
743 'comment_content' => wp_trim_words( wp_strip_all_tags( $c->comment_content ), 40 ),
744 'comment_date' => $c->comment_date,
745 'comment_approved' => $c->comment_approved,
746 ];
747 }
748 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
749 break;
750
751 case 'wp_create_comment':
752 if ( empty( $a['post_id'] ) || empty( $a['comment_content'] ) ) {
753 $r['error'] = [ 'code' => -32602, 'message' => 'post_id & comment_content required' ];
754 break;
755 }
756 $ins = [
757 'comment_post_ID' => intval( $a['post_id'] ),
758 'comment_content' => $this->clean_html( $a['comment_content'] ),
759 'comment_author' => sanitize_text_field( $a['comment_author'] ?? '' ),
760 'comment_author_email' => sanitize_email( $a['comment_author_email'] ?? '' ),
761 'comment_author_url' => esc_url_raw( $a['comment_author_url'] ?? '' ),
762 'comment_approved' => $a['comment_approved'] ?? 1,
763 ];
764 $cid = wp_insert_comment( $ins );
765 if ( is_wp_error( $cid ) ) {
766 /** @var WP_Error $cid */
767 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
768 }
769 else {
770 $this->add_result_text( $r, 'Comment created ID ' . $cid );
771 }
772 break;
773
774 case 'wp_update_comment':
775 if ( empty( $a['comment_ID'] ) ) {
776 $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ];
777 break;
778 }
779 $c = [ 'comment_ID' => intval( $a['comment_ID'] ) ];
780 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) ) {
781 foreach ( $a['fields'] as $k => $v ) {
782 $c[ $k ] = ( $k === 'comment_content' ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
783 }
784 }
785 $cid = wp_update_comment( $c, true );
786 if ( is_wp_error( $cid ) ) {
787 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
788 }
789 else {
790 $this->add_result_text( $r, 'Comment #' . $cid . ' updated' );
791 }
792 break;
793
794 case 'wp_delete_comment':
795 if ( empty( $a['comment_ID'] ) ) {
796 $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ];
797 break;
798 }
799 $done = wp_delete_comment( intval( $a['comment_ID'] ), !empty( $a['force'] ) );
800 if ( $done ) {
801 $this->add_result_text( $r, 'Comment #' . $a['comment_ID'] . ' deleted' );
802 }
803 else {
804 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
805 }
806 break;
807
808 /* ===== Options ===== */
809 case 'wp_get_option':
810 $val = get_option( sanitize_key( $a['key'] ) );
811 $this->add_result_text( $r, wp_json_encode( $val, JSON_PRETTY_PRINT ) );
812 break;
813
814 case 'wp_update_option':
815 $set = update_option( sanitize_key( $a['key'] ), $a['value'], 'yes' );
816 if ( $set ) {
817 $this->add_result_text( $r, 'Option "' . $a['key'] . '" updated' );
818 }
819 else {
820 $r['error'] = [ 'code' => -32603, 'message' => 'Update failed' ];
821 }
822 break;
823
824 /* ===== Counts ===== */
825 case 'wp_count_posts':
826 $pt = sanitize_key( $a['post_type'] ?? 'post' );
827 $obj = wp_count_posts( $pt );
828 $this->add_result_text( $r, wp_json_encode( $obj, JSON_PRETTY_PRINT ) );
829 break;
830
831 case 'wp_count_terms':
832 $tax = sanitize_key( $a['taxonomy'] );
833 $total = wp_count_terms( $tax, [ 'hide_empty' => false ] );
834 if ( is_wp_error( $total ) ) {
835 $r['error'] = [ 'code' => $total->get_error_code(), 'message' => $total->get_error_message() ];
836 }
837 else {
838 $this->add_result_text( $r, (string) $total );
839 }
840 break;
841
842 case 'wp_count_media':
843 $args = [ 'post_type' => 'attachment', 'post_status' => 'inherit', 'fields' => 'ids' ];
844 $d = [];
845 if ( $a['after'] ?? '' ) {
846 $d['after'] = $a['after'];
847 }
848 if ( $a['before'] ?? '' ) {
849 $d['before'] = $a['before'];
850 }
851 if ( $d ) {
852 $args['date_query'] = [ $d ];
853 }
854 $total = count( get_posts( $args ) );
855 $this->add_result_text( $r, (string) $total );
856 break;
857
858 /* ===== Post-types ===== */
859 case 'wp_get_post_types':
860 $out = [];
861 foreach ( get_post_types( [ 'public' => true ], 'objects' ) as $pt ) {
862 $out[] = [ 'key' => $pt->name, 'label' => $pt->label ];
863 }
864 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
865 break;
866
867 /* ===== Plugins ===== */
868 case 'wp_list_plugins':
869 if ( !function_exists( 'get_plugins' ) ) {
870 require_once ABSPATH . 'wp-admin/includes/plugin.php';
871 }
872 $search = sanitize_text_field( $a['search'] ?? '' );
873 $out = [];
874 foreach ( get_plugins() as $p ) {
875 if ( !$search || stripos( $p['Name'], $search ) !== false ) {
876 $out[] = [ 'Name' => $p['Name'], 'Version' => $p['Version'] ];
877 }
878 }
879 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
880 break;
881
882 /* ===== Posts: list ===== */
883 case 'wp_get_posts':
884 $q = [
885 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
886 'post_status' => sanitize_key( $a['post_status'] ?? 'publish' ),
887 's' => sanitize_text_field( $a['search'] ?? '' ),
888 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
889 ];
890 if ( isset( $a['offset'] ) ) {
891 $q['offset'] = max( 0, intval( $a['offset'] ) );
892 }
893 if ( isset( $a['paged'] ) ) {
894 $q['paged'] = max( 1, intval( $a['paged'] ) );
895 }
896 $date = [];
897 if ( $a['after'] ?? '' ) {
898 $date['after'] = $a['after'];
899 }
900 if ( $a['before'] ?? '' ) {
901 $date['before'] = $a['before'];
902 }
903 if ( $date ) {
904 $q['date_query'] = [ $date ];
905 }
906 $rows = [];
907 foreach ( get_posts( $q ) as $p ) {
908 $rows[] = [
909 'ID' => $p->ID,
910 'post_title' => $p->post_title,
911 'post_status' => $p->post_status,
912 'post_excerpt' => $this->post_excerpt( $p ),
913 'permalink' => get_permalink( $p ),
914 ];
915 }
916 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
917 break;
918
919 /* ===== Posts: single ===== */
920 case 'wp_get_post':
921 if ( empty( $a['ID'] ) ) {
922 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
923 break;
924 }
925 $p = get_post( intval( $a['ID'] ) );
926 if ( !$p ) {
927 $r['error'] = [ 'code' => -32602, 'message' => 'Post not found' ];
928 break;
929 }
930 $out = [
931 'ID' => $p->ID,
932 'post_title' => $p->post_title,
933 'post_status' => $p->post_status,
934 'post_content' => $this->clean_html( $p->post_content ),
935 'post_excerpt' => $this->post_excerpt( $p ),
936 'permalink' => get_permalink( $p ),
937 'post_date' => $p->post_date,
938 'post_modified' => $p->post_modified,
939 ];
940 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
941 break;
942
943 /* ===== Posts: snapshot ===== */
944 case 'wp_get_post_snapshot':
945 if ( empty( $a['ID'] ) ) {
946 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
947 break;
948 }
949
950 $post_id = intval( $a['ID'] );
951 $p = get_post( $post_id );
952
953 if ( !$p ) {
954 $r['error'] = [ 'code' => -32602, 'message' => 'Post not found' ];
955 break;
956 }
957
958 $include = $a['include'] ?? [ 'meta', 'terms', 'thumbnail', 'author' ];
959 $exclude = $a['exclude'] ?? [];
960
961 // Handle JSON strings (some MCP clients send arrays as JSON strings)
962 if ( is_string( $include ) ) {
963 $include = json_decode( $include, true ) ?? [];
964 }
965 if ( is_string( $exclude ) ) {
966 $exclude = json_decode( $exclude, true ) ?? [];
967 }
968
969 $snapshot = [
970 'post' => [
971 'ID' => $p->ID,
972 'post_title' => $p->post_title,
973 'post_type' => $p->post_type,
974 'post_status' => $p->post_status,
975 'post_excerpt' => $this->post_excerpt( $p ),
976 'post_name' => $p->post_name,
977 'permalink' => get_permalink( $p ),
978 'post_date' => $p->post_date,
979 'post_modified' => $p->post_modified,
980 ],
981 ];
982
983 // Include content unless excluded (useful for posts with huge content)
984 if ( !in_array( 'content', $exclude ) ) {
985 $snapshot['post']['post_content'] = $this->clean_html( $p->post_content );
986 }
987
988 // Include all post meta
989 if ( in_array( 'meta', $include ) ) {
990 $snapshot['meta'] = [];
991 $all_meta = get_post_meta( $post_id );
992 foreach ( $all_meta as $key => $value ) {
993 if ( is_array( $value ) && count( $value ) === 1 ) {
994 $snapshot['meta'][ $key ] = maybe_unserialize( $value[0] );
995 }
996 else {
997 $snapshot['meta'][ $key ] = array_map( 'maybe_unserialize', $value );
998 }
999 }
1000 }
1001
1002 // Include all taxonomies and their terms
1003 if ( in_array( 'terms', $include ) ) {
1004 $snapshot['terms'] = [];
1005 $taxonomies = get_object_taxonomies( $p->post_type );
1006 foreach ( $taxonomies as $taxonomy ) {
1007 $terms = wp_get_post_terms( $post_id, $taxonomy, [ 'fields' => 'all' ] );
1008 if ( !is_wp_error( $terms ) && !empty( $terms ) ) {
1009 $snapshot['terms'][ $taxonomy ] = array_map( function ( $t ) {
1010 return [
1011 'term_id' => $t->term_id,
1012 'name' => $t->name,
1013 'slug' => $t->slug,
1014 ];
1015 }, $terms );
1016 }
1017 }
1018 }
1019
1020 // Include featured image
1021 if ( in_array( 'thumbnail', $include ) ) {
1022 $thumb_id = get_post_thumbnail_id( $post_id );
1023 if ( $thumb_id ) {
1024 $snapshot['thumbnail'] = [
1025 'ID' => $thumb_id,
1026 'url' => wp_get_attachment_url( $thumb_id ),
1027 'alt' => get_post_meta( $thumb_id, '_wp_attachment_image_alt', true ),
1028 ];
1029 }
1030 }
1031
1032 // Include author
1033 if ( in_array( 'author', $include ) ) {
1034 $author = get_userdata( $p->post_author );
1035 if ( $author ) {
1036 $snapshot['author'] = [
1037 'ID' => $author->ID,
1038 'display_name' => $author->display_name,
1039 'user_login' => $author->user_login,
1040 ];
1041 }
1042 }
1043
1044 $this->add_result_text( $r, wp_json_encode( $snapshot, JSON_PRETTY_PRINT ) );
1045 break;
1046
1047 /* ===== Posts: create ===== */
1048 case 'wp_create_post':
1049 if ( empty( $a['post_title'] ) ) {
1050 $r['error'] = [ 'code' => -32602, 'message' => 'post_title required' ];
1051 break;
1052 }
1053 $ins = [
1054 'post_title' => sanitize_text_field( $a['post_title'] ),
1055 'post_status' => sanitize_key( $a['post_status'] ?? 'draft' ),
1056 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
1057 ];
1058 if ( $a['post_content'] ?? '' ) {
1059 $ins['post_content'] = $this->core->markdown_to_html( $a['post_content'] );
1060 }
1061 if ( $a['post_excerpt'] ?? '' ) {
1062 $ins['post_excerpt'] = $this->clean_html( $a['post_excerpt'] );
1063 }
1064 if ( $a['post_name'] ?? '' ) {
1065 $ins['post_name'] = sanitize_title( $a['post_name'] );
1066 }
1067
1068 // Handle JSON strings for meta_input (some MCP clients send objects as JSON strings)
1069 $meta_input = $a['meta_input'] ?? [];
1070 if ( is_string( $meta_input ) ) {
1071 $meta_input = json_decode( $meta_input, true ) ?? [];
1072 }
1073 if ( !empty( $meta_input ) && is_array( $meta_input ) ) {
1074 $ins['meta_input'] = $meta_input;
1075 }
1076
1077 $new = wp_insert_post( $ins, true );
1078 if ( is_wp_error( $new ) ) {
1079 $r['error'] = [ 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ];
1080 }
1081 else {
1082 if ( empty( $ins['meta_input'] ) && !empty( $meta_input ) && is_array( $meta_input ) ) {
1083 foreach ( $meta_input as $k => $v ) {
1084 update_post_meta( $new, sanitize_key( $k ), maybe_serialize( $v ) );
1085 }
1086 }
1087 $this->add_result_text( $r, 'Post created ID ' . $new );
1088 }
1089 break;
1090
1091 /* ===== Posts: update ===== */
1092 case 'wp_update_post':
1093 if ( empty( $a['ID'] ) ) {
1094 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1095 break;
1096 }
1097 $c = [ 'ID' => intval( $a['ID'] ) ];
1098
1099 // Handle JSON strings (some MCP clients send objects as JSON strings)
1100 $fields = $a['fields'] ?? [];
1101 if ( is_string( $fields ) ) {
1102 $fields = json_decode( $fields, true ) ?? [];
1103 }
1104 if ( !empty( $fields ) && is_array( $fields ) ) {
1105 foreach ( $fields as $k => $v ) {
1106 $c[ $k ] = in_array( $k, [ 'post_content', 'post_excerpt' ], true ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
1107 }
1108 }
1109
1110 // Handle schedule_for convenience parameter
1111 if ( !empty( $a['schedule_for'] ) ) {
1112 $schedule_date = sanitize_text_field( $a['schedule_for'] );
1113 $c['post_status'] = 'future';
1114 $c['post_date'] = $schedule_date;
1115 $c['post_date_gmt'] = get_gmt_from_date( $schedule_date );
1116 $c['edit_date'] = true; // Required for WordPress to respect date changes
1117 }
1118
1119 $u = ( count( $c ) > 1 ) ? wp_update_post( $c, true ) : $c['ID'];
1120 if ( is_wp_error( $u ) ) {
1121 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
1122 break;
1123 }
1124
1125 // Handle JSON strings for meta_input
1126 $meta_input = $a['meta_input'] ?? [];
1127 if ( is_string( $meta_input ) ) {
1128 $meta_input = json_decode( $meta_input, true ) ?? [];
1129 }
1130 if ( !empty( $meta_input ) && is_array( $meta_input ) ) {
1131 foreach ( $meta_input as $k => $v ) {
1132 update_post_meta( $u, sanitize_key( $k ), maybe_serialize( $v ) );
1133 }
1134 }
1135 $msg = 'Post #' . $u . ' updated';
1136 if ( !empty( $a['schedule_for'] ) ) {
1137 $msg .= ' and scheduled for ' . $a['schedule_for'];
1138 }
1139 $this->add_result_text( $r, $msg );
1140 break;
1141
1142 /* ===== Posts: delete ===== */
1143 case 'wp_delete_post':
1144 if ( empty( $a['ID'] ) ) {
1145 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1146 break;
1147 }
1148 $del = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
1149 if ( $del ) {
1150 $this->add_result_text( $r, 'Post #' . $a['ID'] . ' deleted' );
1151 }
1152 else {
1153 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1154 }
1155 break;
1156
1157 /* ===== Post-meta ===== */
1158 case 'wp_get_post_meta':
1159 if ( empty( $a['ID'] ) ) {
1160 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1161 break;
1162 }
1163 $pid = intval( $a['ID'] );
1164 $out = ( $a['key'] ?? '' ) ? get_post_meta( $pid, sanitize_key( $a['key'] ), true ) : get_post_meta( $pid );
1165 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1166 break;
1167
1168 case 'wp_update_post_meta':
1169 if ( empty( $a['ID'] ) ) {
1170 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1171 break;
1172 }
1173 $pid = intval( $a['ID'] );
1174
1175 // Handle JSON strings for meta (some MCP clients send objects as JSON strings)
1176 $meta = $a['meta'] ?? null;
1177 if ( is_string( $meta ) ) {
1178 $meta = json_decode( $meta, true );
1179 }
1180
1181 if ( !empty( $meta ) && is_array( $meta ) ) {
1182 foreach ( $meta as $k => $v ) {
1183 update_post_meta( $pid, sanitize_key( $k ), maybe_serialize( $v ) );
1184 }
1185 }
1186 elseif ( isset( $a['key'], $a['value'] ) ) {
1187 update_post_meta( $pid, sanitize_key( $a['key'] ), maybe_serialize( $a['value'] ) );
1188 }
1189 else {
1190 $r['error'] = [ 'code' => -32602, 'message' => 'meta array or key/value required' ];
1191 break;
1192 }
1193 $this->add_result_text( $r, 'Meta updated for post #' . $pid );
1194 break;
1195
1196 case 'wp_delete_post_meta':
1197 if ( empty( $a['ID'] ) || empty( $a['key'] ) ) {
1198 $r['error'] = [ 'code' => -32602, 'message' => 'ID & key required' ];
1199 break;
1200 }
1201 $pid = intval( $a['ID'] );
1202 $key = sanitize_key( $a['key'] );
1203 $done = isset( $a['value'] ) ? delete_post_meta( $pid, $key, maybe_serialize( $a['value'] ) ) : delete_post_meta( $pid, $key );
1204 if ( $done ) {
1205 $this->add_result_text( $r, 'Meta deleted on post #' . $pid );
1206 }
1207 else {
1208 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1209 }
1210 break;
1211
1212 /* ===== Featured image ===== */
1213 case 'wp_set_featured_image':
1214 if ( empty( $a['post_id'] ) ) {
1215 $r['error'] = [ 'code' => -32602, 'message' => 'post_id required' ];
1216 break;
1217 }
1218 $post_id = intval( $a['post_id'] );
1219 $media_id = isset( $a['media_id'] ) ? intval( $a['media_id'] ) : 0;
1220 if ( $media_id ) {
1221 $done = set_post_thumbnail( $post_id, $media_id );
1222 if ( $done ) {
1223 $this->add_result_text( $r, 'Featured image set on post #' . $post_id );
1224 }
1225 else {
1226 $r['error'] = [ 'code' => -32603, 'message' => 'Failed to set thumbnail' ];
1227 }
1228 }
1229 else {
1230 delete_post_thumbnail( $post_id );
1231 $this->add_result_text( $r, 'Featured image removed from post #' . $post_id );
1232 }
1233 break;
1234
1235 /* ===== Taxonomies ===== */
1236 case 'wp_get_taxonomies':
1237 $pt = sanitize_key( $a['post_type'] ?? 'post' );
1238 $out = [];
1239 foreach ( get_object_taxonomies( $pt, 'objects' ) as $t ) {
1240 $out[] = [ 'key' => $t->name, 'label' => $t->label ];
1241 }
1242 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1243 break;
1244
1245 case 'wp_get_terms':
1246 $tax = sanitize_key( $a['taxonomy'] );
1247 $args = [
1248 'taxonomy' => $tax,
1249 'hide_empty' => false,
1250 'number' => intval( $a['limit'] ?? 0 ),
1251 'search' => $a['search'] ?? '',
1252 ];
1253 if ( isset( $a['parent'] ) ) {
1254 $args['parent'] = intval( $a['parent'] );
1255 }
1256 $out = [];
1257 foreach ( get_terms( $args ) as $t ) {
1258 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name, 'slug' => $t->slug, 'count' => $t->count ];
1259 }
1260 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1261 break;
1262
1263 case 'wp_create_term':
1264 if ( empty( $a['term_name'] ) ) {
1265 $r['error'] = [ 'code' => -32602, 'message' => 'term_name required' ];
1266 break;
1267 }
1268 $tax = sanitize_key( $a['taxonomy'] );
1269 $args = [];
1270 if ( $a['slug'] ?? '' ) {
1271 $args['slug'] = sanitize_title( $a['slug'] );
1272 }
1273 if ( $a['description'] ?? '' ) {
1274 $args['description'] = sanitize_text_field( $a['description'] );
1275 }
1276 if ( isset( $a['parent'] ) ) {
1277 $args['parent'] = intval( $a['parent'] );
1278 }
1279 $term = wp_insert_term( sanitize_text_field( $a['term_name'] ), $tax, $args );
1280 if ( is_wp_error( $term ) ) {
1281 $r['error'] = [ 'code' => $term->get_error_code(), 'message' => $term->get_error_message() ];
1282 }
1283 else {
1284 $this->add_result_text( $r, 'Term ' . $term['term_id'] . ' created' );
1285 }
1286 break;
1287
1288 case 'wp_update_term':
1289 $tid = intval( $a['term_id'] ?? 0 );
1290 if ( !$tid ) {
1291 $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ];
1292 break;
1293 }
1294 $tax = sanitize_key( $a['taxonomy'] );
1295 $uargs = [];
1296 foreach ( [ 'name', 'slug', 'description', 'parent' ] as $f ) {
1297 if ( isset( $a[$f] ) ) {
1298 $uargs[$f] = $a[$f];
1299 }
1300 }
1301 $t = wp_update_term( $tid, $tax, $uargs );
1302 if ( is_wp_error( $t ) ) {
1303 $r['error'] = [ 'code' => $t->get_error_code(), 'message' => $t->get_error_message() ];
1304 }
1305 else {
1306 $this->add_result_text( $r, 'Term ' . $tid . ' updated' );
1307 }
1308 break;
1309
1310 case 'wp_delete_term':
1311 $tid = intval( $a['term_id'] ?? 0 );
1312 if ( !$tid ) {
1313 $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ];
1314 break;
1315 }
1316 $tax = sanitize_key( $a['taxonomy'] );
1317 $d = wp_delete_term( $tid, $tax );
1318 if ( $d ) {
1319 $this->add_result_text( $r, 'Term ' . $tid . ' deleted' );
1320 }
1321 else {
1322 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1323 }
1324 break;
1325
1326 case 'wp_get_post_terms':
1327 if ( empty( $a['ID'] ) ) {
1328 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1329 break;
1330 }
1331 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
1332 $out = [];
1333 foreach ( wp_get_post_terms( intval( $a['ID'] ), $tax, [ 'fields' => 'all' ] ) as $t ) {
1334 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name ];
1335 }
1336 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1337 break;
1338
1339 case 'wp_add_post_terms':
1340 if ( empty( $a['ID'] ) || empty( $a['terms'] ) ) {
1341 $r['error'] = [ 'code' => -32602, 'message' => 'ID & terms required' ];
1342 break;
1343 }
1344 $terms = $a['terms'];
1345 // Handle JSON strings (some MCP clients send arrays as JSON strings)
1346 if ( is_string( $terms ) ) {
1347 $terms = json_decode( $terms, true ) ?? [];
1348 }
1349 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
1350 $append = !isset( $a['append'] ) || $a['append'];
1351 $set = wp_set_post_terms( intval( $a['ID'] ), $terms, $tax, $append );
1352 if ( is_wp_error( $set ) ) {
1353 $r['error'] = [ 'code' => $set->get_error_code(), 'message' => $set->get_error_message() ];
1354 }
1355 else {
1356 $this->add_result_text( $r, 'Terms set for post #' . $a['ID'] );
1357 }
1358 break;
1359
1360 /* ===== Media: list ===== */
1361 case 'wp_get_media':
1362 $q = [
1363 'post_type' => 'attachment',
1364 's' => $a['search'] ?? '',
1365 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
1366 'post_status' => 'inherit',
1367 ];
1368 $d = [];
1369 if ( $a['after'] ?? '' ) {
1370 $d['after'] = $a['after'];
1371 }
1372 if ( $a['before'] ?? '' ) {
1373 $d['before'] = $a['before'];
1374 }
1375 if ( $d ) {
1376 $q['date_query'] = [ $d ];
1377 }
1378 $list = [];
1379 foreach ( get_posts( $q ) as $m ) {
1380 $list[] = [ 'ID' => $m->ID, 'title' => $m->post_title, 'url' => wp_get_attachment_url( $m->ID ) ];
1381 }
1382 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
1383 break;
1384
1385 /* ===== Media: upload ===== */
1386 case 'wp_upload_media':
1387 $has_url = !empty( $a['url'] );
1388 $has_base64 = !empty( $a['base64'] ) && !empty( $a['filename'] );
1389 if ( !$has_url && !$has_base64 ) {
1390 $r['error'] = [ 'code' => -32602, 'message' => 'Provide either url, or base64 + filename.' ];
1391 break;
1392 }
1393 try {
1394 require_once ABSPATH . 'wp-admin/includes/file.php';
1395 require_once ABSPATH . 'wp-admin/includes/media.php';
1396 require_once ABSPATH . 'wp-admin/includes/image.php';
1397
1398 if ( $has_url ) {
1399 $tmp = download_url( $a['url'] );
1400 if ( is_wp_error( $tmp ) ) {
1401 throw new Exception( $tmp->get_error_message(), $tmp->get_error_code() );
1402 }
1403 $file = [ 'name' => basename( parse_url( $a['url'], PHP_URL_PATH ) ), 'tmp_name' => $tmp ];
1404 }
1405 else {
1406 $decoded = base64_decode( $a['base64'], true );
1407 if ( $decoded === false ) {
1408 throw new Exception( 'Invalid base64 data.' );
1409 }
1410 $tmp = wp_tempnam( $a['filename'] );
1411 file_put_contents( $tmp, $decoded );
1412 $file = [ 'name' => sanitize_file_name( $a['filename'] ), 'tmp_name' => $tmp ];
1413 }
1414
1415 $id = media_handle_sideload( $file, 0, $a['description'] ?? '' );
1416 @unlink( $tmp );
1417 if ( is_wp_error( $id ) ) {
1418 throw new Exception( $id->get_error_message(), $id->get_error_code() );
1419 }
1420 if ( $a['title'] ?? '' ) {
1421 wp_update_post( [ 'ID' => $id, 'post_title' => sanitize_text_field( $a['title'] ) ] );
1422 }
1423 if ( $a['alt'] ?? '' ) {
1424 update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
1425 }
1426 $this->add_result_text( $r, wp_get_attachment_url( $id ) );
1427 }
1428 catch ( \Throwable $e ) {
1429 $r['error'] = [ 'code' => $e->getCode() ?: -32603, 'message' => $e->getMessage() ];
1430 }
1431 break;
1432
1433 /* ===== Media: upload alternative (two-step) ===== */
1434 case 'wp_upload_request':
1435 if ( empty( $a['filename'] ) ) {
1436 $r['error'] = [ 'code' => -32602, 'message' => 'filename required' ];
1437 break;
1438 }
1439 try {
1440 $token = wp_generate_password( 32, false );
1441 $transient_key = 'mwai_mcp_upload_' . $token;
1442 $data = [
1443 'filename' => sanitize_file_name( $a['filename'] ),
1444 'title' => $a['title'] ?? '',
1445 'description' => $a['description'] ?? '',
1446 'alt' => $a['alt'] ?? '',
1447 ];
1448 set_transient( $transient_key, $data, 5 * MINUTE_IN_SECONDS );
1449 $upload_url = rest_url( 'mcp/v1/upload/' . $token );
1450 $this->add_result_text( $r, wp_json_encode( [
1451 'upload_url' => $upload_url,
1452 'expires_in' => '5 minutes',
1453 'usage' => 'curl -X POST -F "file=@/path/to/' . $a['filename'] . '" "' . $upload_url . '"',
1454 ], JSON_PRETTY_PRINT ) );
1455 }
1456 catch ( \Throwable $e ) {
1457 $r['error'] = [ 'code' => $e->getCode() ?: -32603, 'message' => $e->getMessage() ];
1458 }
1459 break;
1460
1461 /* ===== Media: update ===== */
1462 case 'wp_update_media':
1463 if ( empty( $a['ID'] ) ) {
1464 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1465 break;
1466 }
1467 $upd = [ 'ID' => intval( $a['ID'] ) ];
1468 if ( $a['title'] ?? '' ) {
1469 $upd['post_title'] = sanitize_text_field( $a['title'] );
1470 }
1471 if ( $a['caption'] ?? '' ) {
1472 $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1473 }
1474 if ( $a['description'] ?? '' ) {
1475 $upd['post_content'] = $this->clean_html( $a['description'] );
1476 }
1477 $u = wp_update_post( $upd, true );
1478 if ( is_wp_error( $u ) ) {
1479 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
1480 }
1481 else {
1482 if ( $a['alt'] ?? '' ) {
1483 update_post_meta( $u, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
1484 }
1485 $this->add_result_text( $r, 'Media #' . $u . ' updated' );
1486 }
1487 break;
1488
1489 /* ===== Media: delete ===== */
1490 case 'wp_delete_media':
1491 if ( empty( $a['ID'] ) ) {
1492 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1493 break;
1494 }
1495 $d = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
1496 if ( $d ) {
1497 $this->add_result_text( $r, 'Media #' . $a['ID'] . ' deleted' );
1498 }
1499 else {
1500 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1501 }
1502 break;
1503
1504 /* ===== MWAI Vision ===== */
1505 case 'mwai_vision':
1506 if ( empty( $a['message'] ) ) {
1507 $r['error'] = [ 'code' => -32602, 'message' => 'message required' ];
1508 break;
1509 }
1510 global $mwai;
1511 if ( !isset( $mwai ) ) {
1512 $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ];
1513 break;
1514 }
1515 $analysis = $mwai->simpleVisionQuery(
1516 $a['message'],
1517 $a['url'] ?? null,
1518 $a['path'] ?? null,
1519 [ 'scope' => 'mcp' ]
1520 );
1521 $this->add_result_text( $r, is_string( $analysis ) ? $analysis : wp_json_encode( $analysis, JSON_PRETTY_PRINT ) );
1522 break;
1523
1524 /* ===== MWAI Image ===== */
1525 case 'mwai_image':
1526 if ( empty( $a['message'] ) ) {
1527 $r['error'] = [ 'code' => -32602, 'message' => 'message required' ];
1528 break;
1529 }
1530 global $mwai;
1531 if ( !isset( $mwai ) ) {
1532 $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ];
1533 break;
1534 }
1535
1536 $media = $mwai->imageQueryForMediaLibrary( $a['message'], [ 'scope' => 'mcp' ], $a['postId'] ?? null );
1537 if ( is_wp_error( $media ) ) {
1538 $r['error'] = [ 'code' => $media->get_error_code(), 'message' => $media->get_error_message() ];
1539 break;
1540 }
1541
1542 $mid = intval( $media['id'] );
1543
1544 $upd = [ 'ID' => $mid ];
1545 if ( !empty( $a['title'] ) ) {
1546 $upd['post_title'] = sanitize_text_field( $a['title'] );
1547 }
1548 if ( !empty( $a['caption'] ) ) {
1549 $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1550 }
1551 if ( !empty( $a['description'] ) ) {
1552 $upd['post_content'] = $this->clean_html( $a['description'] );
1553 }
1554 if ( count( $upd ) > 1 ) {
1555 wp_update_post( $upd, true );
1556 }
1557 if ( array_key_exists( 'alt', $a ) ) {
1558 update_post_meta( $mid, '_wp_attachment_image_alt', sanitize_text_field( (string) $a['alt'] ) );
1559 }
1560
1561 $media = [
1562 'id' => $mid,
1563 'url' => wp_get_attachment_url( $mid ),
1564 'title' => get_the_title( $mid ),
1565 'caption' => wp_get_attachment_caption( $mid ),
1566 'alt' => get_post_meta( $mid, '_wp_attachment_image_alt', true ),
1567 ];
1568 $this->add_result_text( $r, wp_json_encode( $media, JSON_PRETTY_PRINT ) );
1569 break;
1570
1571 default: $r['error'] = [ 'code' => -32601, 'message' => 'Unknown tool' ];
1572 }
1573 return $r;
1574 }
1575 #endregion
1576 }
1577