PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.1.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.1.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 11 months ago mcp.md 1 year ago mcp.php 8 months ago oauth.php 9 months ago realtime.php 1 year ago
mcp-core.php
1333 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 a single post by ID (all fields inc. full content).',
246 'inputSchema' => [
247 'type' => 'object',
248 'properties' => [ 'ID' => [ 'type' => 'integer' ] ],
249 'required' => [ 'ID' ],
250 ],
251 ],
252 'wp_create_post' => [
253 'name' => 'wp_create_post',
254 '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.',
255 'inputSchema' => [
256 'type' => 'object',
257 'properties' => [
258 'post_title' => [ 'type' => 'string' ],
259 'post_content' => [ 'type' => 'string' ],
260 'post_excerpt' => [ 'type' => 'string' ],
261 'post_status' => [ 'type' => 'string' ],
262 'post_type' => [ 'type' => 'string' ],
263 'post_name' => [ 'type' => 'string' ],
264 'meta_input' => [ 'type' => 'object', 'description' => 'Associative array of custom fields.' ],
265 ],
266 'required' => [ 'post_title' ],
267 ],
268 ],
269 'wp_update_post' => [
270 'name' => 'wp_update_post',
271 'description' => 'Update a post – pass ID plus a “fields” object containing any post fields to update; meta_input adds/updates custom fields. post_category (array of term IDs) REPLACES existing categories; use wp_add_post_terms to append.',
272 'inputSchema' => [
273 'type' => 'object',
274 'properties' => [
275 'ID' => [ 'type' => 'integer', 'description' => 'The ID of the post to update.' ],
276 'fields' => [
277 'type' => 'object',
278 'properties' => [
279 'post_title' => [ 'type' => 'string' ],
280 'post_content' => [ 'type' => 'string' ],
281 'post_status' => [ 'type' => 'string' ],
282 'post_name' => [ 'type' => 'string' ],
283 'post_excerpt' => [ 'type' => 'string' ],
284 'post_category' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
285 ],
286 'additionalProperties' => true
287 ],
288 'meta_input' => [
289 'type' => 'object',
290 'description' => 'Associative array of custom fields.'
291 ],
292 ],
293 'required' => [ 'ID' ],
294 ],
295 ],
296 'wp_delete_post' => [
297 'name' => 'wp_delete_post',
298 'description' => 'Delete/trash a post.',
299 'inputSchema' => [
300 'type' => 'object',
301 'properties' => [
302 'ID' => [ 'type' => 'integer' ],
303 'force' => [ 'type' => 'boolean' ],
304 ],
305 'required' => [ 'ID' ],
306 ],
307 ],
308
309 /* -------- Post-meta -------- */
310 'wp_get_post_meta' => [
311 'name' => 'wp_get_post_meta',
312 'description' => 'Retrieve post meta. Provide "key" to fetch a single value; omit to fetch all custom fields.',
313 'inputSchema' => [
314 'type' => 'object',
315 'properties' => [
316 'ID' => [ 'type' => 'integer' ],
317 'key' => [ 'type' => 'string' ],
318 ],
319 'required' => [ 'ID' ],
320 ],
321 ],
322 'wp_update_post_meta' => [
323 'name' => 'wp_update_post_meta',
324 'description' => 'Create or update one or more custom fields for a post.',
325 'inputSchema' => [
326 'type' => 'object',
327 'properties' => [
328 'ID' => [ 'type' => 'integer' ],
329 'meta' => [ 'type' => 'object', 'description' => 'Key/value pairs to set. Alternative: provide "key" + "value".' ],
330 'key' => [ 'type' => 'string' ],
331 'value' => [ 'type' => [ 'string', 'number', 'boolean' ] ],
332 ],
333 'required' => [ 'ID' ],
334 ],
335 ],
336 'wp_delete_post_meta' => [
337 'name' => 'wp_delete_post_meta',
338 'description' => 'Delete custom field(s) from a post. Provide value to remove a single row; omit value to delete all rows for the key.',
339 'inputSchema' => [
340 'type' => 'object',
341 'properties' => [
342 'ID' => [ 'type' => 'integer' ],
343 'key' => [ 'type' => 'string' ],
344 'value' => [ 'type' => [ 'string', 'number', 'boolean' ] ],
345 ],
346 'required' => [ 'ID', 'key' ],
347 ],
348 ],
349
350 /* -------- Featured image -------- */
351 'wp_set_featured_image' => [
352 'name' => 'wp_set_featured_image',
353 'description' => 'Attach or remove a featured image (thumbnail) for a post/page. Provide media_id to attach, omit or null to remove.',
354 'inputSchema' => [
355 'type' => 'object',
356 'properties' => [
357 'post_id' => [ 'type' => 'integer' ],
358 'media_id' => [ 'type' => 'integer' ],
359 ],
360 'required' => [ 'post_id' ],
361 ],
362 ],
363
364 /* -------- Taxonomies / Terms -------- */
365 'wp_get_taxonomies' => [
366 'name' => 'wp_get_taxonomies',
367 'description' => 'List taxonomies for a post type.',
368 'inputSchema' => [
369 'type' => 'object',
370 'properties' => [ 'post_type' => [ 'type' => 'string' ] ],
371 ],
372 ],
373 'wp_get_terms' => [
374 'name' => 'wp_get_terms',
375 'description' => 'List terms of a taxonomy.',
376 'inputSchema' => [
377 'type' => 'object',
378 'properties' => [
379 'taxonomy' => [ 'type' => 'string' ],
380 'search' => [ 'type' => 'string' ],
381 'parent' => [ 'type' => 'integer' ],
382 'limit' => [ 'type' => 'integer' ],
383 ],
384 'required' => [ 'taxonomy' ],
385 ],
386 ],
387 'wp_create_term' => [
388 'name' => 'wp_create_term',
389 'description' => 'Create a term.',
390 'inputSchema' => [
391 'type' => 'object',
392 'properties' => [
393 'taxonomy' => [ 'type' => 'string' ],
394 'term_name' => [ 'type' => 'string' ],
395 'slug' => [ 'type' => 'string' ],
396 'description' => [ 'type' => 'string' ],
397 'parent' => [ 'type' => 'integer' ],
398 ],
399 'required' => [ 'taxonomy', 'term_name' ],
400 ],
401 ],
402 'wp_update_term' => [
403 'name' => 'wp_update_term',
404 'description' => 'Update a term.',
405 'inputSchema' => [
406 'type' => 'object',
407 'properties' => [
408 'term_id' => [ 'type' => 'integer' ],
409 'taxonomy' => [ 'type' => 'string' ],
410 'name' => [ 'type' => 'string' ],
411 'slug' => [ 'type' => 'string' ],
412 'description' => [ 'type' => 'string' ],
413 'parent' => [ 'type' => 'integer' ],
414 ],
415 'required' => [ 'term_id', 'taxonomy' ],
416 ],
417 ],
418 'wp_delete_term' => [
419 'name' => 'wp_delete_term',
420 'description' => 'Delete a term.',
421 'inputSchema' => [
422 'type' => 'object',
423 'properties' => [
424 'term_id' => [ 'type' => 'integer' ],
425 'taxonomy' => [ 'type' => 'string' ],
426 ],
427 'required' => [ 'term_id', 'taxonomy' ],
428 ],
429 ],
430 'wp_get_post_terms' => [
431 'name' => 'wp_get_post_terms',
432 'description' => 'Get terms attached to a post.',
433 'inputSchema' => [
434 'type' => 'object',
435 'properties' => [
436 'ID' => [ 'type' => 'integer' ],
437 'taxonomy' => [ 'type' => 'string' ],
438 ],
439 'required' => [ 'ID' ],
440 ],
441 ],
442 'wp_add_post_terms' => [
443 'name' => 'wp_add_post_terms',
444 'description' => 'Attach terms to a post.',
445 'inputSchema' => [
446 'type' => 'object',
447 'properties' => [
448 'ID' => [ 'type' => 'integer' ],
449 'taxonomy' => [ 'type' => 'string' ],
450 'terms' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
451 'append' => [ 'type' => 'boolean' ],
452 ],
453 'required' => [ 'ID', 'terms' ],
454 ],
455 ],
456
457 /* -------- Media -------- */
458 'wp_get_media' => [
459 'name' => 'wp_get_media',
460 'description' => 'List media items.',
461 'inputSchema' => [
462 'type' => 'object',
463 'properties' => [
464 'search' => [ 'type' => 'string' ],
465 'after' => [ 'type' => 'string' ],
466 'before' => [ 'type' => 'string' ],
467 'limit' => [ 'type' => 'integer' ],
468 ],
469 ],
470 ],
471 'wp_upload_media' => [
472 'name' => 'wp_upload_media',
473 'description' => 'Download file from URL and add to Media Library.',
474 'inputSchema' => [
475 'type' => 'object',
476 'properties' => [
477 'url' => [ 'type' => 'string' ],
478 'title' => [ 'type' => 'string' ],
479 'description' => [ 'type' => 'string' ],
480 'alt' => [ 'type' => 'string' ],
481 ],
482 'required' => [ 'url' ],
483 ],
484 ],
485 'wp_update_media' => [
486 'name' => 'wp_update_media',
487 'description' => 'Update attachment meta.',
488 'inputSchema' => [
489 'type' => 'object',
490 'properties' => [
491 'ID' => [ 'type' => 'integer' ],
492 'title' => [ 'type' => 'string' ],
493 'caption' => [ 'type' => 'string' ],
494 'description' => [ 'type' => 'string' ],
495 'alt' => [ 'type' => 'string' ],
496 ],
497 'required' => [ 'ID' ],
498 ],
499 ],
500 'wp_delete_media' => [
501 'name' => 'wp_delete_media',
502 'description' => 'Delete/trash an attachment.',
503 'inputSchema' => [
504 'type' => 'object',
505 'properties' => [
506 'ID' => [ 'type' => 'integer' ],
507 'force' => [ 'type' => 'boolean' ],
508 ],
509 'required' => [ 'ID' ],
510 ],
511 ],
512
513 /* -------- MWAI Vision / Image -------- */
514 'mwai_vision' => [
515 'name' => 'mwai_vision',
516 'description' => 'Analyze an image via AI Engine Vision.',
517 'inputSchema' => [
518 'type' => 'object',
519 'properties' => [
520 'message' => [ 'type' => 'string' ],
521 'url' => [ 'type' => 'string' ],
522 'path' => [ 'type' => 'string' ],
523 ],
524 'required' => [ 'message' ],
525 ],
526 ],
527 'mwai_image' => [
528 'name' => 'mwai_image',
529 '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 }.',
530 'inputSchema' => [
531 'type' => 'object',
532 'properties' => [
533 'message' => [ 'type' => 'string', 'description' => 'Prompt describing the desired image.' ],
534 'postId' => [ 'type' => 'integer', 'description' => 'Optional post ID to attach the image to.' ],
535 'title' => [ 'type' => 'string' ],
536 'caption' => [ 'type' => 'string' ],
537 'description' => [ 'type' => 'string' ],
538 'alt' => [ 'type' => 'string' ],
539 ],
540 'required' => [ 'message' ],
541 ],
542 ],
543
544 ];
545 }
546 #endregion
547
548 #region Tool Registration
549 public function register_rest_tools( array $prev ): array {
550 $tools = $this->tools();
551
552 // All 36 core tools enabled and tested with ChatGPT.
553 // Automatic validation in mcp.php fixes problematic type definitions.
554
555 // Add category and annotations to each tool
556 foreach ( $tools as &$tool ) {
557 if ( !isset( $tool['category'] ) ) {
558 $tool['category'] = 'Core';
559 }
560
561 // Add MCP tool annotations based on tool name/behavior
562 if ( !isset( $tool['annotations'] ) ) {
563 $name = $tool['name'];
564
565 // Read-only tools (safe, no modifications)
566 $is_readonly = (
567 strpos( $name, 'wp_get_' ) === 0 ||
568 strpos( $name, 'wp_list_' ) === 0 ||
569 strpos( $name, 'wp_count_' ) === 0 ||
570 $name === 'mwai_vision'
571 );
572
573 // Destructive tools (can delete/destroy data)
574 $is_destructive = (
575 strpos( $name, 'wp_delete_' ) === 0 ||
576 $name === 'wp_update_user' // Can change passwords/roles
577 );
578
579 $tool['annotations'] = [
580 'readOnlyHint' => $is_readonly,
581 'destructiveHint' => !$is_readonly && $is_destructive,
582 'openWorldHint' => false, // All operate on closed WordPress system
583 ];
584 }
585 }
586
587 $merged = array_merge( $prev, array_values( $tools ) );
588 return $merged;
589 }
590 #endregion
591
592 #region Callback
593 public function handle_call( $prev, string $tool, array $args, int $id ) {
594 // Security check is already done in the MCP auth layer
595 // If we reach here, the user is authorized to use MCP
596 if ( !empty( $prev ) || !isset( $this->tools()[ $tool ] ) ) {
597 return $prev;
598 }
599 return $this->dispatch( $tool, $args, $id );
600 }
601 #endregion
602
603 #region Dispatcher
604 private function dispatch( string $tool, array $a, int $id ): array {
605 $r = [ 'jsonrpc' => '2.0', 'id' => $id ];
606
607 switch ( $tool ) {
608
609 /* ===== Users ===== */
610 case 'wp_get_users':
611 $q = [
612 'search' => '*' . esc_attr( $a['search'] ?? '' ) . '*',
613 'role' => $a['role'] ?? '',
614 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
615 ];
616 if ( isset( $a['offset'] ) ) {
617 $q['offset'] = max( 0, intval( $a['offset'] ) );
618 }
619 if ( isset( $a['paged'] ) ) {
620 $q['paged'] = max( 1, intval( $a['paged'] ) );
621 }
622 $rows = [];
623 foreach ( get_users( $q ) as $u ) {
624 $rows[] = [
625 'ID' => $u->ID,
626 'user_login' => $u->user_login,
627 'display_name' => $u->display_name,
628 'roles' => $u->roles,
629 ];
630 }
631 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
632 break;
633
634 case 'wp_create_user':
635 $data = [
636 'user_login' => sanitize_user( $a['user_login'] ),
637 'user_email' => sanitize_email( $a['user_email'] ),
638 'user_pass' => $a['user_pass'] ?? wp_generate_password( 12, true ),
639 'display_name' => sanitize_text_field( $a['display_name'] ?? '' ),
640 'role' => sanitize_key( $a['role'] ?? get_option( 'default_role', 'subscriber' ) ),
641 ];
642 $uid = wp_insert_user( $data );
643 if ( is_wp_error( $uid ) ) {
644 $r['error'] = [ 'code' => $uid->get_error_code(), 'message' => $uid->get_error_message() ];
645 }
646 else {
647 $this->add_result_text( $r, 'User created ID ' . $uid );
648 }
649 break;
650
651 case 'wp_update_user':
652 if ( empty( $a['ID'] ) ) {
653 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
654 break;
655 }
656 $upd = [ 'ID' => intval( $a['ID'] ) ];
657 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) ) {
658 foreach ( $a['fields'] as $k => $v ) {
659 $upd[ $k ] = ( $k === 'role' ) ? sanitize_key( $v ) : sanitize_text_field( $v );
660 }
661 }
662 $u = wp_update_user( $upd );
663 if ( is_wp_error( $u ) ) {
664 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
665 }
666 else {
667 $this->add_result_text( $r, 'User #' . $u . ' updated' );
668 }
669 break;
670
671 /* ===== Comments ===== */
672 case 'wp_get_comments':
673 $args = [
674 'post_id' => isset( $a['post_id'] ) ? intval( $a['post_id'] ) : '',
675 'status' => $a['status'] ?? 'approve',
676 'search' => $a['search'] ?? '',
677 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
678 ];
679 if ( isset( $a['offset'] ) ) {
680 $args['offset'] = max( 0, intval( $a['offset'] ) );
681 }
682 if ( isset( $a['paged'] ) ) {
683 $args['paged'] = max( 1, intval( $a['paged'] ) );
684 }
685 $list = [];
686 foreach ( get_comments( $args ) as $c ) {
687 $list[] = [
688 'comment_ID' => $c->comment_ID,
689 'comment_post_ID' => $c->comment_post_ID,
690 'comment_author' => $c->comment_author,
691 'comment_content' => wp_trim_words( wp_strip_all_tags( $c->comment_content ), 40 ),
692 'comment_date' => $c->comment_date,
693 'comment_approved' => $c->comment_approved,
694 ];
695 }
696 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
697 break;
698
699 case 'wp_create_comment':
700 if ( empty( $a['post_id'] ) || empty( $a['comment_content'] ) ) {
701 $r['error'] = [ 'code' => -32602, 'message' => 'post_id & comment_content required' ];
702 break;
703 }
704 $ins = [
705 'comment_post_ID' => intval( $a['post_id'] ),
706 'comment_content' => $this->clean_html( $a['comment_content'] ),
707 'comment_author' => sanitize_text_field( $a['comment_author'] ?? '' ),
708 'comment_author_email' => sanitize_email( $a['comment_author_email'] ?? '' ),
709 'comment_author_url' => esc_url_raw( $a['comment_author_url'] ?? '' ),
710 'comment_approved' => $a['comment_approved'] ?? 1,
711 ];
712 $cid = wp_insert_comment( $ins );
713 if ( is_wp_error( $cid ) ) {
714 /** @var WP_Error $cid */
715 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
716 }
717 else {
718 $this->add_result_text( $r, 'Comment created ID ' . $cid );
719 }
720 break;
721
722 case 'wp_update_comment':
723 if ( empty( $a['comment_ID'] ) ) {
724 $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ];
725 break;
726 }
727 $c = [ 'comment_ID' => intval( $a['comment_ID'] ) ];
728 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) ) {
729 foreach ( $a['fields'] as $k => $v ) {
730 $c[ $k ] = ( $k === 'comment_content' ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
731 }
732 }
733 $cid = wp_update_comment( $c, true );
734 if ( is_wp_error( $cid ) ) {
735 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
736 }
737 else {
738 $this->add_result_text( $r, 'Comment #' . $cid . ' updated' );
739 }
740 break;
741
742 case 'wp_delete_comment':
743 if ( empty( $a['comment_ID'] ) ) {
744 $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ];
745 break;
746 }
747 $done = wp_delete_comment( intval( $a['comment_ID'] ), !empty( $a['force'] ) );
748 if ( $done ) {
749 $this->add_result_text( $r, 'Comment #' . $a['comment_ID'] . ' deleted' );
750 }
751 else {
752 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
753 }
754 break;
755
756 /* ===== Options ===== */
757 case 'wp_get_option':
758 $val = get_option( sanitize_key( $a['key'] ) );
759 $this->add_result_text( $r, wp_json_encode( $val, JSON_PRETTY_PRINT ) );
760 break;
761
762 case 'wp_update_option':
763 $set = update_option( sanitize_key( $a['key'] ), $a['value'], 'yes' );
764 if ( $set ) {
765 $this->add_result_text( $r, 'Option "' . $a['key'] . '" updated' );
766 }
767 else {
768 $r['error'] = [ 'code' => -32603, 'message' => 'Update failed' ];
769 }
770 break;
771
772 /* ===== Counts ===== */
773 case 'wp_count_posts':
774 $pt = sanitize_key( $a['post_type'] ?? 'post' );
775 $obj = wp_count_posts( $pt );
776 $this->add_result_text( $r, wp_json_encode( $obj, JSON_PRETTY_PRINT ) );
777 break;
778
779 case 'wp_count_terms':
780 $tax = sanitize_key( $a['taxonomy'] );
781 $total = wp_count_terms( $tax, [ 'hide_empty' => false ] );
782 if ( is_wp_error( $total ) ) {
783 $r['error'] = [ 'code' => $total->get_error_code(), 'message' => $total->get_error_message() ];
784 }
785 else {
786 $this->add_result_text( $r, (string) $total );
787 }
788 break;
789
790 case 'wp_count_media':
791 $args = [ 'post_type' => 'attachment', 'post_status' => 'inherit', 'fields' => 'ids' ];
792 $d = [];
793 if ( $a['after'] ?? '' ) {
794 $d['after'] = $a['after'];
795 }
796 if ( $a['before'] ?? '' ) {
797 $d['before'] = $a['before'];
798 }
799 if ( $d ) {
800 $args['date_query'] = [ $d ];
801 }
802 $total = count( get_posts( $args ) );
803 $this->add_result_text( $r, (string) $total );
804 break;
805
806 /* ===== Post-types ===== */
807 case 'wp_get_post_types':
808 $out = [];
809 foreach ( get_post_types( [ 'public' => true ], 'objects' ) as $pt ) {
810 $out[] = [ 'key' => $pt->name, 'label' => $pt->label ];
811 }
812 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
813 break;
814
815 /* ===== Plugins ===== */
816 case 'wp_list_plugins':
817 if ( !function_exists( 'get_plugins' ) ) {
818 require_once ABSPATH . 'wp-admin/includes/plugin.php';
819 }
820 $search = sanitize_text_field( $a['search'] ?? '' );
821 $out = [];
822 foreach ( get_plugins() as $p ) {
823 if ( !$search || stripos( $p['Name'], $search ) !== false ) {
824 $out[] = [ 'Name' => $p['Name'], 'Version' => $p['Version'] ];
825 }
826 }
827 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
828 break;
829
830 /* ===== Posts: list ===== */
831 case 'wp_get_posts':
832 $q = [
833 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
834 'post_status' => sanitize_key( $a['post_status'] ?? 'publish' ),
835 's' => sanitize_text_field( $a['search'] ?? '' ),
836 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
837 ];
838 if ( isset( $a['offset'] ) ) {
839 $q['offset'] = max( 0, intval( $a['offset'] ) );
840 }
841 if ( isset( $a['paged'] ) ) {
842 $q['paged'] = max( 1, intval( $a['paged'] ) );
843 }
844 $date = [];
845 if ( $a['after'] ?? '' ) {
846 $date['after'] = $a['after'];
847 }
848 if ( $a['before'] ?? '' ) {
849 $date['before'] = $a['before'];
850 }
851 if ( $date ) {
852 $q['date_query'] = [ $date ];
853 }
854 $rows = [];
855 foreach ( get_posts( $q ) as $p ) {
856 $rows[] = [
857 'ID' => $p->ID,
858 'post_title' => $p->post_title,
859 'post_status' => $p->post_status,
860 'post_excerpt' => $this->post_excerpt( $p ),
861 'permalink' => get_permalink( $p ),
862 ];
863 }
864 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
865 break;
866
867 /* ===== Posts: single ===== */
868 case 'wp_get_post':
869 if ( empty( $a['ID'] ) ) {
870 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
871 break;
872 }
873 $p = get_post( intval( $a['ID'] ) );
874 if ( !$p ) {
875 $r['error'] = [ 'code' => -32602, 'message' => 'Post not found' ];
876 break;
877 }
878 $out = [
879 'ID' => $p->ID,
880 'post_title' => $p->post_title,
881 'post_status' => $p->post_status,
882 'post_content' => $this->clean_html( $p->post_content ),
883 'post_excerpt' => $this->post_excerpt( $p ),
884 'permalink' => get_permalink( $p ),
885 'post_date' => $p->post_date,
886 'post_modified' => $p->post_modified,
887 ];
888 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
889 break;
890
891 /* ===== Posts: create ===== */
892 case 'wp_create_post':
893 if ( empty( $a['post_title'] ) ) {
894 $r['error'] = [ 'code' => -32602, 'message' => 'post_title required' ];
895 break;
896 }
897 $ins = [
898 'post_title' => sanitize_text_field( $a['post_title'] ),
899 'post_status' => sanitize_key( $a['post_status'] ?? 'draft' ),
900 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
901 ];
902 if ( $a['post_content'] ?? '' ) {
903 $ins['post_content'] = $this->core->markdown_to_html( $a['post_content'] );
904 }
905 if ( $a['post_excerpt'] ?? '' ) {
906 $ins['post_excerpt'] = $this->clean_html( $a['post_excerpt'] );
907 }
908 if ( $a['post_name'] ?? '' ) {
909 $ins['post_name'] = sanitize_title( $a['post_name'] );
910 }
911 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) {
912 $ins['meta_input'] = $a['meta_input'];
913 }
914 $new = wp_insert_post( $ins, true );
915 if ( is_wp_error( $new ) ) {
916 $r['error'] = [ 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ];
917 }
918 else {
919 if ( empty( $ins['meta_input'] ) && !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) {
920 foreach ( $a['meta_input'] as $k => $v ) {
921 update_post_meta( $new, sanitize_key( $k ), maybe_serialize( $v ) );
922 }
923 }
924 $this->add_result_text( $r, 'Post created ID ' . $new );
925 }
926 break;
927
928 /* ===== Posts: update ===== */
929 case 'wp_update_post':
930 if ( empty( $a['ID'] ) ) {
931 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
932 break;
933 }
934 $c = [ 'ID' => intval( $a['ID'] ) ];
935 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) ) {
936 foreach ( $a['fields'] as $k => $v ) {
937 $c[ $k ] = in_array( $k, [ 'post_content', 'post_excerpt' ], true ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
938 }
939 }
940 $u = ( count( $c ) > 1 ) ? wp_update_post( $c, true ) : $c['ID'];
941 if ( is_wp_error( $u ) ) {
942 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
943 break;
944 }
945 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) {
946 foreach ( $a['meta_input'] as $k => $v ) {
947 update_post_meta( $u, sanitize_key( $k ), maybe_serialize( $v ) );
948 }
949 }
950 $this->add_result_text( $r, 'Post #' . $u . ' updated' );
951 break;
952
953 /* ===== Posts: delete ===== */
954 case 'wp_delete_post':
955 if ( empty( $a['ID'] ) ) {
956 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
957 break;
958 }
959 $del = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
960 if ( $del ) {
961 $this->add_result_text( $r, 'Post #' . $a['ID'] . ' deleted' );
962 }
963 else {
964 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
965 }
966 break;
967
968 /* ===== Post-meta ===== */
969 case 'wp_get_post_meta':
970 if ( empty( $a['ID'] ) ) {
971 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
972 break;
973 }
974 $pid = intval( $a['ID'] );
975 $out = ( $a['key'] ?? '' ) ? get_post_meta( $pid, sanitize_key( $a['key'] ), true ) : get_post_meta( $pid );
976 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
977 break;
978
979 case 'wp_update_post_meta':
980 if ( empty( $a['ID'] ) ) {
981 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
982 break;
983 }
984 $pid = intval( $a['ID'] );
985 if ( !empty( $a['meta'] ) && is_array( $a['meta'] ) ) {
986 foreach ( $a['meta'] as $k => $v ) {
987 update_post_meta( $pid, sanitize_key( $k ), maybe_serialize( $v ) );
988 }
989 }
990 elseif ( isset( $a['key'], $a['value'] ) ) {
991 update_post_meta( $pid, sanitize_key( $a['key'] ), maybe_serialize( $a['value'] ) );
992 }
993 else {
994 $r['error'] = [ 'code' => -32602, 'message' => 'meta array or key/value required' ];
995 break;
996 }
997 $this->add_result_text( $r, 'Meta updated for post #' . $pid );
998 break;
999
1000 case 'wp_delete_post_meta':
1001 if ( empty( $a['ID'] ) || empty( $a['key'] ) ) {
1002 $r['error'] = [ 'code' => -32602, 'message' => 'ID & key required' ];
1003 break;
1004 }
1005 $pid = intval( $a['ID'] );
1006 $key = sanitize_key( $a['key'] );
1007 $done = isset( $a['value'] ) ? delete_post_meta( $pid, $key, maybe_serialize( $a['value'] ) ) : delete_post_meta( $pid, $key );
1008 if ( $done ) {
1009 $this->add_result_text( $r, 'Meta deleted on post #' . $pid );
1010 }
1011 else {
1012 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1013 }
1014 break;
1015
1016 /* ===== Featured image ===== */
1017 case 'wp_set_featured_image':
1018 if ( empty( $a['post_id'] ) ) {
1019 $r['error'] = [ 'code' => -32602, 'message' => 'post_id required' ];
1020 break;
1021 }
1022 $post_id = intval( $a['post_id'] );
1023 $media_id = isset( $a['media_id'] ) ? intval( $a['media_id'] ) : 0;
1024 if ( $media_id ) {
1025 $done = set_post_thumbnail( $post_id, $media_id );
1026 if ( $done ) {
1027 $this->add_result_text( $r, 'Featured image set on post #' . $post_id );
1028 }
1029 else {
1030 $r['error'] = [ 'code' => -32603, 'message' => 'Failed to set thumbnail' ];
1031 }
1032 }
1033 else {
1034 delete_post_thumbnail( $post_id );
1035 $this->add_result_text( $r, 'Featured image removed from post #' . $post_id );
1036 }
1037 break;
1038
1039 /* ===== Taxonomies ===== */
1040 case 'wp_get_taxonomies':
1041 $pt = sanitize_key( $a['post_type'] ?? 'post' );
1042 $out = [];
1043 foreach ( get_object_taxonomies( $pt, 'objects' ) as $t ) {
1044 $out[] = [ 'key' => $t->name, 'label' => $t->label ];
1045 }
1046 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1047 break;
1048
1049 case 'wp_get_terms':
1050 $tax = sanitize_key( $a['taxonomy'] );
1051 $args = [
1052 'taxonomy' => $tax,
1053 'hide_empty' => false,
1054 'number' => intval( $a['limit'] ?? 0 ),
1055 'search' => $a['search'] ?? '',
1056 ];
1057 if ( isset( $a['parent'] ) ) {
1058 $args['parent'] = intval( $a['parent'] );
1059 }
1060 $out = [];
1061 foreach ( get_terms( $args ) as $t ) {
1062 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name, 'slug' => $t->slug, 'count' => $t->count ];
1063 }
1064 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1065 break;
1066
1067 case 'wp_create_term':
1068 if ( empty( $a['term_name'] ) ) {
1069 $r['error'] = [ 'code' => -32602, 'message' => 'term_name required' ];
1070 break;
1071 }
1072 $tax = sanitize_key( $a['taxonomy'] );
1073 $args = [];
1074 if ( $a['slug'] ?? '' ) {
1075 $args['slug'] = sanitize_title( $a['slug'] );
1076 }
1077 if ( $a['description'] ?? '' ) {
1078 $args['description'] = sanitize_text_field( $a['description'] );
1079 }
1080 if ( isset( $a['parent'] ) ) {
1081 $args['parent'] = intval( $a['parent'] );
1082 }
1083 $term = wp_insert_term( sanitize_text_field( $a['term_name'] ), $tax, $args );
1084 if ( is_wp_error( $term ) ) {
1085 $r['error'] = [ 'code' => $term->get_error_code(), 'message' => $term->get_error_message() ];
1086 }
1087 else {
1088 $this->add_result_text( $r, 'Term ' . $term['term_id'] . ' created' );
1089 }
1090 break;
1091
1092 case 'wp_update_term':
1093 $tid = intval( $a['term_id'] ?? 0 );
1094 if ( !$tid ) {
1095 $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ];
1096 break;
1097 }
1098 $tax = sanitize_key( $a['taxonomy'] );
1099 $uargs = [];
1100 foreach ( [ 'name', 'slug', 'description', 'parent' ] as $f ) {
1101 if ( isset( $a[$f] ) ) {
1102 $uargs[$f] = $a[$f];
1103 }
1104 }
1105 $t = wp_update_term( $tid, $tax, $uargs );
1106 if ( is_wp_error( $t ) ) {
1107 $r['error'] = [ 'code' => $t->get_error_code(), 'message' => $t->get_error_message() ];
1108 }
1109 else {
1110 $this->add_result_text( $r, 'Term ' . $tid . ' updated' );
1111 }
1112 break;
1113
1114 case 'wp_delete_term':
1115 $tid = intval( $a['term_id'] ?? 0 );
1116 if ( !$tid ) {
1117 $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ];
1118 break;
1119 }
1120 $tax = sanitize_key( $a['taxonomy'] );
1121 $d = wp_delete_term( $tid, $tax );
1122 if ( $d ) {
1123 $this->add_result_text( $r, 'Term ' . $tid . ' deleted' );
1124 }
1125 else {
1126 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1127 }
1128 break;
1129
1130 case 'wp_get_post_terms':
1131 if ( empty( $a['ID'] ) ) {
1132 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1133 break;
1134 }
1135 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
1136 $out = [];
1137 foreach ( wp_get_post_terms( intval( $a['ID'] ), $tax, [ 'fields' => 'all' ] ) as $t ) {
1138 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name ];
1139 }
1140 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1141 break;
1142
1143 case 'wp_add_post_terms':
1144 if ( empty( $a['ID'] ) || empty( $a['terms'] ) ) {
1145 $r['error'] = [ 'code' => -32602, 'message' => 'ID & terms required' ];
1146 break;
1147 }
1148 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
1149 $append = !isset( $a['append'] ) || $a['append'];
1150 $set = wp_set_post_terms( intval( $a['ID'] ), $a['terms'], $tax, $append );
1151 if ( is_wp_error( $set ) ) {
1152 $r['error'] = [ 'code' => $set->get_error_code(), 'message' => $set->get_error_message() ];
1153 }
1154 else {
1155 $this->add_result_text( $r, 'Terms set for post #' . $a['ID'] );
1156 }
1157 break;
1158
1159 /* ===== Media: list ===== */
1160 case 'wp_get_media':
1161 $q = [
1162 'post_type' => 'attachment',
1163 's' => $a['search'] ?? '',
1164 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
1165 'post_status' => 'inherit',
1166 ];
1167 $d = [];
1168 if ( $a['after'] ?? '' ) {
1169 $d['after'] = $a['after'];
1170 }
1171 if ( $a['before'] ?? '' ) {
1172 $d['before'] = $a['before'];
1173 }
1174 if ( $d ) {
1175 $q['date_query'] = [ $d ];
1176 }
1177 $list = [];
1178 foreach ( get_posts( $q ) as $m ) {
1179 $list[] = [ 'ID' => $m->ID, 'title' => $m->post_title, 'url' => wp_get_attachment_url( $m->ID ) ];
1180 }
1181 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
1182 break;
1183
1184 /* ===== Media: upload ===== */
1185 case 'wp_upload_media':
1186 if ( empty( $a['url'] ) ) {
1187 $r['error'] = [ 'code' => -32602, 'message' => 'url required' ];
1188 break;
1189 }
1190 try {
1191 require_once ABSPATH . 'wp-admin/includes/file.php';
1192 require_once ABSPATH . 'wp-admin/includes/media.php';
1193 require_once ABSPATH . 'wp-admin/includes/image.php';
1194 $tmp = download_url( $a['url'] );
1195 if ( is_wp_error( $tmp ) ) {
1196 throw new Exception( $tmp->get_error_message(), $tmp->get_error_code() );
1197 }
1198 $file = [ 'name' => basename( parse_url( $a['url'], PHP_URL_PATH ) ), 'tmp_name' => $tmp ];
1199 $id = media_handle_sideload( $file, 0, $a['description'] ?? '' );
1200 @unlink( $tmp );
1201 if ( is_wp_error( $id ) ) {
1202 throw new Exception( $id->get_error_message(), $id->get_error_code() );
1203 }
1204 if ( $a['title'] ?? '' ) {
1205 wp_update_post( [ 'ID' => $id, 'post_title' => sanitize_text_field( $a['title'] ) ] );
1206 }
1207 if ( $a['alt'] ?? '' ) {
1208 update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
1209 }
1210 $this->add_result_text( $r, wp_get_attachment_url( $id ) );
1211 }
1212 catch ( \Throwable $e ) {
1213 $r['error'] = [ 'code' => $e->getCode() ?: -32603, 'message' => $e->getMessage() ];
1214 }
1215 break;
1216
1217 /* ===== Media: update ===== */
1218 case 'wp_update_media':
1219 if ( empty( $a['ID'] ) ) {
1220 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1221 break;
1222 }
1223 $upd = [ 'ID' => intval( $a['ID'] ) ];
1224 if ( $a['title'] ?? '' ) {
1225 $upd['post_title'] = sanitize_text_field( $a['title'] );
1226 }
1227 if ( $a['caption'] ?? '' ) {
1228 $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1229 }
1230 if ( $a['description'] ?? '' ) {
1231 $upd['post_content'] = $this->clean_html( $a['description'] );
1232 }
1233 $u = wp_update_post( $upd, true );
1234 if ( is_wp_error( $u ) ) {
1235 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
1236 }
1237 else {
1238 if ( $a['alt'] ?? '' ) {
1239 update_post_meta( $u, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
1240 }
1241 $this->add_result_text( $r, 'Media #' . $u . ' updated' );
1242 }
1243 break;
1244
1245 /* ===== Media: delete ===== */
1246 case 'wp_delete_media':
1247 if ( empty( $a['ID'] ) ) {
1248 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1249 break;
1250 }
1251 $d = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
1252 if ( $d ) {
1253 $this->add_result_text( $r, 'Media #' . $a['ID'] . ' deleted' );
1254 }
1255 else {
1256 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1257 }
1258 break;
1259
1260 /* ===== MWAI Vision ===== */
1261 case 'mwai_vision':
1262 if ( empty( $a['message'] ) ) {
1263 $r['error'] = [ 'code' => -32602, 'message' => 'message required' ];
1264 break;
1265 }
1266 global $mwai;
1267 if ( !isset( $mwai ) ) {
1268 $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ];
1269 break;
1270 }
1271 $analysis = $mwai->simpleVisionQuery(
1272 $a['message'],
1273 $a['url'] ?? null,
1274 $a['path'] ?? null,
1275 [ 'scope' => 'mcp' ]
1276 );
1277 $this->add_result_text( $r, is_string( $analysis ) ? $analysis : wp_json_encode( $analysis, JSON_PRETTY_PRINT ) );
1278 break;
1279
1280 /* ===== MWAI Image ===== */
1281 case 'mwai_image':
1282 if ( empty( $a['message'] ) ) {
1283 $r['error'] = [ 'code' => -32602, 'message' => 'message required' ];
1284 break;
1285 }
1286 global $mwai;
1287 if ( !isset( $mwai ) ) {
1288 $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ];
1289 break;
1290 }
1291
1292 $media = $mwai->imageQueryForMediaLibrary( $a['message'], [ 'scope' => 'mcp' ], $a['postId'] ?? null );
1293 if ( is_wp_error( $media ) ) {
1294 $r['error'] = [ 'code' => $media->get_error_code(), 'message' => $media->get_error_message() ];
1295 break;
1296 }
1297
1298 $mid = intval( $media['id'] );
1299
1300 $upd = [ 'ID' => $mid ];
1301 if ( !empty( $a['title'] ) ) {
1302 $upd['post_title'] = sanitize_text_field( $a['title'] );
1303 }
1304 if ( !empty( $a['caption'] ) ) {
1305 $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1306 }
1307 if ( !empty( $a['description'] ) ) {
1308 $upd['post_content'] = $this->clean_html( $a['description'] );
1309 }
1310 if ( count( $upd ) > 1 ) {
1311 wp_update_post( $upd, true );
1312 }
1313 if ( array_key_exists( 'alt', $a ) ) {
1314 update_post_meta( $mid, '_wp_attachment_image_alt', sanitize_text_field( (string) $a['alt'] ) );
1315 }
1316
1317 $media = [
1318 'id' => $mid,
1319 'url' => wp_get_attachment_url( $mid ),
1320 'title' => get_the_title( $mid ),
1321 'caption' => wp_get_attachment_caption( $mid ),
1322 'alt' => get_post_meta( $mid, '_wp_attachment_image_alt', true ),
1323 ];
1324 $this->add_result_text( $r, wp_json_encode( $media, JSON_PRETTY_PRINT ) );
1325 break;
1326
1327 default: $r['error'] = [ 'code' => -32601, 'message' => 'Unknown tool' ];
1328 }
1329 return $r;
1330 }
1331 #endregion
1332 }
1333