PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.2
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.conf 1 year ago mcp.js 1 year ago mcp.md 1 year ago mcp.php 1 year ago mcp_core.php 1 year ago mcp_rest.php 1 year ago realtime.php 1 year ago
mcp_core.php
1062 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'] ) ) $r['result']['content'] = [];
20 $r['result']['content'][] = [ 'type' => 'text', 'text' => $text ];
21 }
22 private function clean_html( string $v ) : string { return wp_kses_post( wp_unslash( $v ) ); }
23 private function post_excerpt( WP_Post $p ) : string {
24 return wp_trim_words( wp_strip_all_tags( $p->post_excerpt ?: $p->post_content ), 55 );
25 }
26 private function empty_schema() : array { return [ 'type' => 'object', 'properties' => (object) [] ]; }
27 #endregion
28
29 #region Tools Definitions
30 private function tools() : array {
31 return [
32
33 /* -------- Plugins -------- */
34 'wp_list_plugins' => [
35 'name' => 'wp_list_plugins',
36 'description' => 'List installed plugins (returns array of {Name, Version}).',
37 'inputSchema' => [
38 'type' => 'object',
39 'properties' => [ 'search' => [ 'type' => 'string' ] ],
40 ],
41 ],
42
43 /* -------- Users -------- */
44 'wp_get_users' => [
45 'name' => 'wp_get_users',
46 'description' => 'Retrieve users (fields: ID, user_login, display_name, roles). If no limit supplied, returns 10. `paged` ignored if `offset` is used.',
47 'inputSchema' => [
48 'type' => 'object',
49 'properties' => [
50 'search' => [ 'type' => 'string' ],
51 'role' => [ 'type' => 'string' ],
52 'limit' => [ 'type' => 'integer' ],
53 'offset' => [ 'type' => 'integer' ],
54 'paged' => [ 'type' => 'integer' ],
55 ],
56 ],
57 ],
58 'wp_create_user' => [
59 'name' => 'wp_create_user',
60 'description' => 'Create a user. Requires user_login and user_email. Optional: user_pass (random if omitted), display_name, role.',
61 'inputSchema' => [
62 'type' => 'object',
63 'properties' => [
64 'user_login' => [ 'type' => 'string' ],
65 'user_email' => [ 'type' => 'string' ],
66 'user_pass' => [ 'type' => 'string' ],
67 'display_name' => [ 'type' => 'string' ],
68 'role' => [ 'type' => 'string' ],
69 ],
70 'required' => [ 'user_login', 'user_email' ],
71 ],
72 ],
73 'wp_update_user' => [
74 'name' => 'wp_update_user',
75 'description' => 'Update a user – pass ID plus a “fields” object (user_email, display_name, user_pass, role).',
76 'inputSchema' => [
77 'type' => 'object',
78 'properties' => [
79 'ID' => [ 'type' => 'integer' ],
80 'fields' => [
81 'type' => 'object',
82 'properties' => [
83 'user_email' => [ 'type' => 'string' ],
84 'display_name' => [ 'type' => 'string' ],
85 'user_pass' => [ 'type' => 'string' ],
86 'role' => [ 'type' => 'string' ],
87 ],
88 'additionalProperties' => true
89 ],
90 ],
91 'required' => [ 'ID' ],
92 ],
93 ],
94
95 /* -------- Comments -------- */
96 'wp_get_comments' => [
97 'name' => 'wp_get_comments',
98 'description' => 'Retrieve comments (fields: comment_ID, comment_post_ID, comment_author, comment_content, comment_date, comment_approved). Returns 10 by default.',
99 'inputSchema' => [
100 'type' => 'object',
101 'properties' => [
102 'post_id' => [ 'type' => 'integer' ],
103 'status' => [ 'type' => 'string' ],
104 'search' => [ 'type' => 'string' ],
105 'limit' => [ 'type' => 'integer' ],
106 'offset' => [ 'type' => 'integer' ],
107 'paged' => [ 'type' => 'integer' ],
108 ],
109 ],
110 ],
111 'wp_create_comment' => [
112 'name' => 'wp_create_comment',
113 'description' => 'Insert a comment. Requires post_id and comment_content. Optional author, author_email, author_url.',
114 'inputSchema' => [
115 'type' => 'object',
116 'properties' => [
117 'post_id' => [ 'type' => 'integer' ],
118 'comment_content' => [ 'type' => 'string' ],
119 'comment_author' => [ 'type' => 'string' ],
120 'comment_author_email' => [ 'type' => 'string' ],
121 'comment_author_url' => [ 'type' => 'string' ],
122 'comment_approved' => [ 'type' => 'string' ],
123 ],
124 'required' => [ 'post_id', 'comment_content' ],
125 ],
126 ],
127 'wp_update_comment' => [
128 'name' => 'wp_update_comment',
129 'description' => 'Update a comment – pass comment_ID plus fields (comment_content, comment_approved).',
130 'inputSchema' => [
131 'type' => 'object',
132 'properties' => [
133 'comment_ID' => [ 'type' => 'integer' ],
134 'fields' => [
135 'type' => 'object',
136 'properties' => [
137 'comment_content' => [ 'type' => 'string' ],
138 'comment_approved' => [ 'type' => 'string' ],
139 ],
140 'additionalProperties' => true
141 ],
142 ],
143 'required' => [ 'comment_ID' ],
144 ],
145 ],
146 'wp_delete_comment' => [
147 'name' => 'wp_delete_comment',
148 'description' => 'Delete a comment. `force` true bypasses trash.',
149 'inputSchema' => [
150 'type' => 'object',
151 'properties' => [
152 'comment_ID' => [ 'type' => 'integer' ],
153 'force' => [ 'type' => 'boolean' ],
154 ],
155 'required' => [ 'comment_ID' ],
156 ],
157 ],
158
159 /* -------- Options -------- */
160 'wp_get_option' => [
161 'name' => 'wp_get_option',
162 'description' => 'Get a single WordPress option value (scalar or array) by key.',
163 'inputSchema' => [
164 'type' => 'object',
165 'properties' => [ 'key' => [ 'type' => 'string' ] ],
166 'required' => [ 'key' ],
167 ],
168 ],
169 'wp_update_option' => [
170 'name' => 'wp_update_option',
171 'description' => 'Create or update a WordPress option (JSON-serialised if necessary).',
172 'inputSchema' => [
173 'type' => 'object',
174 'properties' => [
175 'key' => [ 'type' => 'string' ],
176 'value' => [ 'type' => [ 'string','number','boolean','object','array' ] ],
177 ],
178 'required' => [ 'key','value' ],
179 ],
180 ],
181
182 /* -------- Counts -------- */
183 'wp_count_posts' => [
184 'name' => 'wp_count_posts',
185 'description' => 'Return counts of posts by status. Optional post_type (default post).',
186 'inputSchema' => [
187 'type' => 'object',
188 'properties' => [ 'post_type' => [ 'type' => 'string' ] ],
189 ],
190 ],
191 'wp_count_terms' => [
192 'name' => 'wp_count_terms',
193 'description' => 'Return total number of terms in a taxonomy.',
194 'inputSchema' => [
195 'type' => 'object',
196 'properties' => [ 'taxonomy' => [ 'type' => 'string' ] ],
197 'required' => [ 'taxonomy' ],
198 ],
199 ],
200 'wp_count_media' => [
201 'name' => 'wp_count_media',
202 'description' => 'Return number of attachments (optionally after/before date).',
203 'inputSchema' => [
204 'type' => 'object',
205 'properties' => [
206 'after' => [ 'type' => 'string' ],
207 'before' => [ 'type' => 'string' ],
208 ],
209 ],
210 ],
211
212 /* -------- Post-types -------- */
213 'wp_get_post_types' => [
214 'name' => 'wp_get_post_types',
215 'description' => 'List public post types (key, label).',
216 'inputSchema' => $this->empty_schema(),
217 ],
218
219 /* -------- Posts -------- */
220 'wp_get_posts' => [
221 'name' => 'wp_get_posts',
222 '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.',
223 'inputSchema' => [
224 'type' => 'object',
225 'properties' => [
226 'post_type' => [ 'type' => 'string' ],
227 'post_status' => [ 'type' => 'string' ],
228 'search' => [ 'type' => 'string' ],
229 'after' => [ 'type' => 'string' ],
230 'before' => [ 'type' => 'string' ],
231 'limit' => [ 'type' => 'integer' ],
232 'offset' => [ 'type' => 'integer' ],
233 'paged' => [ 'type' => 'integer' ],
234 ],
235 ],
236 ],
237 'wp_get_post' => [
238 'name' => 'wp_get_post',
239 'description' => 'Get a single post by ID (all fields inc. full content).',
240 'inputSchema' => [
241 'type' => 'object',
242 'properties' => [ 'ID' => [ 'type' => 'integer' ] ],
243 'required' => [ 'ID' ],
244 ],
245 ],
246 'wp_create_post' => [
247 'name' => 'wp_create_post',
248 '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.',
249 'inputSchema' => [
250 'type' => 'object',
251 'properties' => [
252 'post_title' => [ 'type' => 'string' ],
253 'post_content' => [ 'type' => 'string' ],
254 'post_excerpt' => [ 'type' => 'string' ],
255 'post_status' => [ 'type' => 'string' ],
256 'post_type' => [ 'type' => 'string' ],
257 'post_name' => [ 'type' => 'string' ],
258 'meta_input' => [ 'type' => 'object', 'description' => 'Associative array of custom fields.' ],
259 ],
260 'required' => [ 'post_title' ],
261 ],
262 ],
263 'wp_update_post' => [
264 'name' => 'wp_update_post',
265 '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.',
266 'inputSchema' => [
267 'type' => 'object',
268 'properties' => [
269 'ID' => [ 'type' => 'integer', 'description' => 'The ID of the post to update.' ],
270 'fields' => [
271 'type' => 'object',
272 'properties' => [
273 'post_title' => [ 'type' => 'string' ],
274 'post_content' => [ 'type' => 'string' ],
275 'post_status' => [ 'type' => 'string' ],
276 'post_name' => [ 'type' => 'string' ],
277 'post_excerpt' => [ 'type' => 'string' ],
278 'post_category'=> [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
279 ],
280 'additionalProperties' => true
281 ],
282 'meta_input' => [
283 'type' => 'object',
284 'description' => 'Associative array of custom fields.'
285 ],
286 ],
287 'required' => [ 'ID' ],
288 ],
289 ],
290 'wp_delete_post' => [
291 'name' => 'wp_delete_post',
292 'description' => 'Delete/trash a post.',
293 'inputSchema' => [
294 'type' => 'object',
295 'properties' => [
296 'ID' => [ 'type' => 'integer' ],
297 'force' => [ 'type' => 'boolean' ],
298 ],
299 'required' => [ 'ID' ],
300 ],
301 ],
302
303 /* -------- Post-meta -------- */
304 'wp_get_post_meta' => [
305 'name' => 'wp_get_post_meta',
306 'description' => 'Retrieve post meta. Provide "key" to fetch a single value; omit to fetch all custom fields.',
307 'inputSchema' => [
308 'type' => 'object',
309 'properties' => [
310 'ID' => [ 'type' => 'integer' ],
311 'key' => [ 'type' => 'string' ],
312 ],
313 'required' => [ 'ID' ],
314 ],
315 ],
316 'wp_update_post_meta' => [
317 'name' => 'wp_update_post_meta',
318 'description' => 'Create or update one or more custom fields for a post.',
319 'inputSchema' => [
320 'type' => 'object',
321 'properties' => [
322 'ID' => [ 'type' => 'integer' ],
323 'meta' => [ 'type' => 'object', 'description' => 'Key/value pairs to set. Alternative: provide "key" + "value".' ],
324 'key' => [ 'type' => 'string' ],
325 'value' => [ 'type' => [ 'string','number','boolean' ] ],
326 ],
327 'required' => [ 'ID' ],
328 ],
329 ],
330 'wp_delete_post_meta' => [
331 'name' => 'wp_delete_post_meta',
332 'description' => 'Delete custom field(s) from a post. Provide value to remove a single row; omit value to delete all rows for the key.',
333 'inputSchema' => [
334 'type' => 'object',
335 'properties' => [
336 'ID' => [ 'type' => 'integer' ],
337 'key' => [ 'type' => 'string' ],
338 'value' => [ 'type' => [ 'string','number','boolean' ] ],
339 ],
340 'required' => [ 'ID','key' ],
341 ],
342 ],
343
344 /* -------- Featured image -------- */
345 'wp_set_featured_image' => [
346 'name' => 'wp_set_featured_image',
347 'description' => 'Attach or remove a featured image (thumbnail) for a post/page. Provide media_id to attach, omit or null to remove.',
348 'inputSchema' => [
349 'type' => 'object',
350 'properties' => [
351 'post_id' => [ 'type' => 'integer' ],
352 'media_id' => [ 'type' => 'integer' ],
353 ],
354 'required' => [ 'post_id' ],
355 ],
356 ],
357
358 /* -------- Taxonomies / Terms -------- */
359 'wp_get_taxonomies' => [
360 'name' => 'wp_get_taxonomies',
361 'description' => 'List taxonomies for a post type.',
362 'inputSchema' => [
363 'type' => 'object',
364 'properties' => [ 'post_type' => [ 'type' => 'string' ] ],
365 ],
366 ],
367 'wp_get_terms' => [
368 'name' => 'wp_get_terms',
369 'description' => 'List terms of a taxonomy.',
370 'inputSchema' => [
371 'type' => 'object',
372 'properties' => [
373 'taxonomy' => [ 'type' => 'string' ],
374 'search' => [ 'type' => 'string' ],
375 'parent' => [ 'type' => 'integer' ],
376 'limit' => [ 'type' => 'integer' ],
377 ],
378 'required' => [ 'taxonomy' ],
379 ],
380 ],
381 'wp_create_term' => [
382 'name' => 'wp_create_term',
383 'description' => 'Create a term.',
384 'inputSchema' => [
385 'type' => 'object',
386 'properties' => [
387 'taxonomy' => [ 'type' => 'string' ],
388 'term_name' => [ 'type' => 'string' ],
389 'slug' => [ 'type' => 'string' ],
390 'description' => [ 'type' => 'string' ],
391 'parent' => [ 'type' => 'integer' ],
392 ],
393 'required' => [ 'taxonomy','term_name' ],
394 ],
395 ],
396 'wp_update_term' => [
397 'name' => 'wp_update_term',
398 'description' => 'Update a term.',
399 'inputSchema' => [
400 'type' => 'object',
401 'properties' => [
402 'term_id' => [ 'type' => 'integer' ],
403 'taxonomy' => [ 'type' => 'string' ],
404 'name' => [ 'type' => 'string' ],
405 'slug' => [ 'type' => 'string' ],
406 'description'=> [ 'type' => 'string' ],
407 'parent' => [ 'type' => 'integer' ],
408 ],
409 'required' => [ 'term_id','taxonomy' ],
410 ],
411 ],
412 'wp_delete_term' => [
413 'name' => 'wp_delete_term',
414 'description' => 'Delete a term.',
415 'inputSchema' => [
416 'type' => 'object',
417 'properties' => [
418 'term_id' => [ 'type' => 'integer' ],
419 'taxonomy' => [ 'type' => 'string' ],
420 ],
421 'required' => [ 'term_id','taxonomy' ],
422 ],
423 ],
424 'wp_get_post_terms' => [
425 'name' => 'wp_get_post_terms',
426 'description' => 'Get terms attached to a post.',
427 'inputSchema' => [
428 'type' => 'object',
429 'properties' => [
430 'ID' => [ 'type' => 'integer' ],
431 'taxonomy'=> [ 'type' => 'string' ],
432 ],
433 'required' => [ 'ID' ],
434 ],
435 ],
436 'wp_add_post_terms' => [
437 'name' => 'wp_add_post_terms',
438 'description' => 'Attach terms to a post.',
439 'inputSchema' => [
440 'type' => 'object',
441 'properties' => [
442 'ID' => [ 'type' => 'integer' ],
443 'taxonomy'=> [ 'type' => 'string' ],
444 'terms' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
445 'append' => [ 'type' => 'boolean' ],
446 ],
447 'required' => [ 'ID','terms' ],
448 ],
449 ],
450
451 /* -------- Media -------- */
452 'wp_get_media' => [
453 'name' => 'wp_get_media',
454 'description' => 'List media items.',
455 'inputSchema' => [
456 'type' => 'object',
457 'properties' => [
458 'search' => [ 'type' => 'string' ],
459 'after' => [ 'type' => 'string' ],
460 'before' => [ 'type' => 'string' ],
461 'limit' => [ 'type' => 'integer' ],
462 ],
463 ],
464 ],
465 'wp_upload_media' => [
466 'name' => 'wp_upload_media',
467 'description' => 'Download file from URL and add to Media Library.',
468 'inputSchema' => [
469 'type' => 'object',
470 'properties' => [
471 'url' => [ 'type' => 'string' ],
472 'title' => [ 'type' => 'string' ],
473 'description' => [ 'type' => 'string' ],
474 'alt' => [ 'type' => 'string' ],
475 ],
476 'required' => [ 'url' ],
477 ],
478 ],
479 'wp_update_media' => [
480 'name' => 'wp_update_media',
481 'description' => 'Update attachment meta.',
482 'inputSchema' => [
483 'type' => 'object',
484 'properties' => [
485 'ID' => [ 'type' => 'integer' ],
486 'title' => [ 'type' => 'string' ],
487 'caption' => [ 'type' => 'string' ],
488 'description' => [ 'type' => 'string' ],
489 'alt' => [ 'type' => 'string' ],
490 ],
491 'required' => [ 'ID' ],
492 ],
493 ],
494 'wp_delete_media' => [
495 'name' => 'wp_delete_media',
496 'description' => 'Delete/trash an attachment.',
497 'inputSchema' => [
498 'type' => 'object',
499 'properties' => [
500 'ID' => [ 'type' => 'integer' ],
501 'force' => [ 'type' => 'boolean' ],
502 ],
503 'required' => [ 'ID' ],
504 ],
505 ],
506
507 /* -------- MWAI Vision / Image -------- */
508 'mwai_vision' => [
509 'name' => 'mwai_vision',
510 'description' => 'Analyze an image via AI Engine Vision.',
511 'inputSchema' => [
512 'type' => 'object',
513 'properties' => [
514 'message' => [ 'type' => 'string' ],
515 'url' => [ 'type' => 'string' ],
516 'path' => [ 'type' => 'string' ],
517 ],
518 'required' => [ 'message' ],
519 ],
520 ],
521 'mwai_image' => [
522 'name' => 'mwai_image',
523 '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 }.',
524 'inputSchema' => [
525 'type' => 'object',
526 'properties' => [
527 'message' => [ 'type' => 'string', 'description' => 'Prompt describing the desired image.' ],
528 'postId' => [ 'type' => 'integer', 'description' => 'Optional post ID to attach the image to.' ],
529 'title' => [ 'type' => 'string' ],
530 'caption' => [ 'type' => 'string' ],
531 'description' => [ 'type' => 'string' ],
532 'alt' => [ 'type' => 'string' ],
533 ],
534 'required' => [ 'message' ],
535 ],
536 ],
537
538 /* -------- Tools -------- */
539 'mcp_ping' => [
540 'name' => 'mcp_ping',
541 'description' => 'Simple connectivity check. Returns the current GMT time and the WordPress site name. Whenever a tool call fails (error or timeout), immediately invoke mcp_ping to verify the server; if mcp_ping itself does not respond, assume the server is temporarily unreachable and pause additional tool calls.',
542 'inputSchema' => $this->empty_schema(),
543 ],
544 ];
545 }
546 #endregion
547
548 #region Tool Registration
549 public function register_rest_tools( array $prev ) : array { return array_merge( $prev, array_values( $this->tools() ) ); }
550 #endregion
551
552 #region Callback
553 public function handle_call( $prev, string $tool, array $args, int $id ) {
554 if ( !current_user_can( 'administrator' ) ) wp_set_current_user( 1 );
555 if ( !empty( $prev ) || !isset( $this->tools()[ $tool ] ) ) return $prev;
556 return $this->dispatch( $tool, $args, $id );
557 }
558 #endregion
559
560 #region Dispatcher
561 private function dispatch( string $tool, array $a, int $id ) : array {
562 $r = [ 'jsonrpc' => '2.0', 'id' => $id ];
563
564 switch ( $tool ) {
565
566 /* ===== Users ===== */
567 case 'wp_get_users':
568 $q = [
569 'search' => '*' . esc_attr( $a['search'] ?? '' ) . '*',
570 'role' => $a['role'] ?? '',
571 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
572 ];
573 if ( isset( $a['offset'] ) ) $q['offset'] = max( 0, intval( $a['offset'] ) );
574 if ( isset( $a['paged'] ) ) $q['paged'] = max( 1, intval( $a['paged'] ) );
575 $rows = [];
576 foreach ( get_users( $q ) as $u )
577 $rows[] = [
578 'ID' => $u->ID,
579 'user_login' => $u->user_login,
580 'display_name' => $u->display_name,
581 'roles' => $u->roles,
582 ];
583 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
584 break;
585
586 case 'wp_create_user':
587 $data = [
588 'user_login' => sanitize_user( $a['user_login'] ),
589 'user_email' => sanitize_email( $a['user_email'] ),
590 'user_pass' => $a['user_pass'] ?? wp_generate_password( 12, true ),
591 'display_name' => sanitize_text_field( $a['display_name'] ?? '' ),
592 'role' => sanitize_key( $a['role'] ?? get_option( 'default_role', 'subscriber' ) ),
593 ];
594 $uid = wp_insert_user( $data );
595 if ( is_wp_error( $uid ) )
596 $r['error'] = [ 'code' => $uid->get_error_code(), 'message' => $uid->get_error_message() ];
597 else
598 $this->add_result_text( $r, 'User created ID '.$uid );
599 break;
600
601 case 'wp_update_user':
602 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
603 $upd = [ 'ID' => intval( $a['ID'] ) ];
604 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) )
605 foreach ( $a['fields'] as $k => $v ) $upd[ $k ] = ( $k === 'role' ) ? sanitize_key( $v ) : sanitize_text_field( $v );
606 $u = wp_update_user( $upd );
607 if ( is_wp_error( $u ) )
608 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
609 else
610 $this->add_result_text( $r, 'User #'.$u.' updated' );
611 break;
612
613 /* ===== Comments ===== */
614 case 'wp_get_comments':
615 $args = [
616 'post_id' => isset( $a['post_id'] ) ? intval( $a['post_id'] ) : '',
617 'status' => $a['status'] ?? 'approve',
618 'search' => $a['search'] ?? '',
619 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
620 ];
621 if ( isset( $a['offset'] ) ) $args['offset'] = max( 0, intval( $a['offset'] ) );
622 if ( isset( $a['paged'] ) ) $args['paged'] = max( 1, intval( $a['paged'] ) );
623 $list = [];
624 foreach ( get_comments( $args ) as $c )
625 $list[] = [
626 'comment_ID' => $c->comment_ID,
627 'comment_post_ID' => $c->comment_post_ID,
628 'comment_author' => $c->comment_author,
629 'comment_content' => wp_trim_words( wp_strip_all_tags( $c->comment_content ), 40 ),
630 'comment_date' => $c->comment_date,
631 'comment_approved'=> $c->comment_approved,
632 ];
633 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
634 break;
635
636 case 'wp_create_comment':
637 if ( empty( $a['post_id'] ) || empty( $a['comment_content'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'post_id & comment_content required' ]; break; }
638 $ins = [
639 'comment_post_ID' => intval( $a['post_id'] ),
640 'comment_content' => $this->clean_html( $a['comment_content'] ),
641 'comment_author' => sanitize_text_field( $a['comment_author'] ?? '' ),
642 'comment_author_email' => sanitize_email( $a['comment_author_email'] ?? '' ),
643 'comment_author_url' => esc_url_raw( $a['comment_author_url'] ?? '' ),
644 'comment_approved' => $a['comment_approved'] ?? 1,
645 ];
646 $cid = wp_insert_comment( $ins );
647 if ( is_wp_error( $cid ) ) {
648 /** @var WP_Error $cid */
649 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
650 }
651 else {
652 $this->add_result_text( $r, 'Comment created ID '.$cid );
653 }
654 break;
655
656 case 'wp_update_comment':
657 if ( empty( $a['comment_ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ]; break; }
658 $c = [ 'comment_ID' => intval( $a['comment_ID'] ) ];
659 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) )
660 foreach ( $a['fields'] as $k => $v )
661 $c[ $k ] = ( $k === 'comment_content' ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
662 $cid = wp_update_comment( $c, true );
663 if ( is_wp_error( $cid ) )
664 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
665 else
666 $this->add_result_text( $r, 'Comment #'.$cid.' updated' );
667 break;
668
669 case 'wp_delete_comment':
670 if ( empty( $a['comment_ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ]; break; }
671 $done = wp_delete_comment( intval( $a['comment_ID'] ), !empty( $a['force'] ) );
672 if ( $done )
673 $this->add_result_text( $r, 'Comment #'.$a['comment_ID'].' deleted' );
674 else
675 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
676 break;
677
678 /* ===== Options ===== */
679 case 'wp_get_option':
680 $val = get_option( sanitize_key( $a['key'] ) );
681 $this->add_result_text( $r, wp_json_encode( $val, JSON_PRETTY_PRINT ) );
682 break;
683
684 case 'wp_update_option':
685 $set = update_option( sanitize_key( $a['key'] ), $a['value'], 'yes' );
686 if ( $set ) $this->add_result_text( $r, 'Option "'.$a['key'].'" updated' );
687 else $r['error'] = [ 'code' => -32603, 'message' => 'Update failed' ];
688 break;
689
690 /* ===== Counts ===== */
691 case 'wp_count_posts':
692 $pt = sanitize_key( $a['post_type'] ?? 'post' );
693 $obj = wp_count_posts( $pt );
694 $this->add_result_text( $r, wp_json_encode( $obj, JSON_PRETTY_PRINT ) );
695 break;
696
697 case 'wp_count_terms':
698 $tax = sanitize_key( $a['taxonomy'] );
699 $total = wp_count_terms( $tax, [ 'hide_empty' => false ] );
700 if ( is_wp_error( $total ) )
701 $r['error'] = [ 'code' => $total->get_error_code(), 'message' => $total->get_error_message() ];
702 else
703 $this->add_result_text( $r, (string) $total );
704 break;
705
706 case 'wp_count_media':
707 $args = [ 'post_type' => 'attachment', 'post_status' => 'inherit', 'fields' => 'ids' ];
708 $d = [];
709 if ( $a['after'] ?? '' ) $d['after'] = $a['after'];
710 if ( $a['before'] ?? '' ) $d['before'] = $a['before'];
711 if ( $d ) $args['date_query'] = [ $d ];
712 $total = count( get_posts( $args ) );
713 $this->add_result_text( $r, (string) $total );
714 break;
715
716 /* ===== Post-types ===== */
717 case 'wp_get_post_types':
718 $out = [];
719 foreach ( get_post_types( [ 'public' => true ], 'objects' ) as $pt )
720 $out[] = [ 'key' => $pt->name, 'label' => $pt->label ];
721 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
722 break;
723
724 /* ===== Plugins ===== */
725 case 'wp_list_plugins':
726 if ( !function_exists( 'get_plugins' ) ) require_once ABSPATH . 'wp-admin/includes/plugin.php';
727 $search = sanitize_text_field( $a['search'] ?? '' );
728 $out = [];
729 foreach ( get_plugins() as $p ) {
730 if ( !$search || stripos( $p['Name'], $search ) !== false )
731 $out[] = [ 'Name' => $p['Name'], 'Version' => $p['Version'] ];
732 }
733 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
734 break;
735
736 /* ===== Posts: list ===== */
737 case 'wp_get_posts':
738 $q = [
739 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
740 'post_status' => sanitize_key( $a['post_status'] ?? 'publish' ),
741 's' => sanitize_text_field( $a['search'] ?? '' ),
742 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
743 ];
744 if ( isset( $a['offset'] ) ) $q['offset'] = max( 0, intval( $a['offset'] ) );
745 if ( isset( $a['paged'] ) ) $q['paged'] = max( 1, intval( $a['paged'] ) );
746 $date = [];
747 if ( $a['after'] ?? '' ) $date['after'] = $a['after'];
748 if ( $a['before'] ?? '' ) $date['before'] = $a['before'];
749 if ( $date ) $q['date_query'] = [ $date ];
750 $rows = [];
751 foreach ( get_posts( $q ) as $p ) {
752 $rows[] = [
753 'ID' => $p->ID,
754 'post_title' => $p->post_title,
755 'post_status' => $p->post_status,
756 'post_excerpt' => $this->post_excerpt( $p ),
757 'permalink' => get_permalink( $p ),
758 ];
759 }
760 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
761 break;
762
763 /* ===== Posts: single ===== */
764 case 'wp_get_post':
765 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
766 $p = get_post( intval( $a['ID'] ) );
767 if ( !$p ) { $r['error'] = [ 'code' => -32602, 'message' => 'Post not found' ]; break; }
768 $out = [
769 'ID' => $p->ID,
770 'post_title' => $p->post_title,
771 'post_status' => $p->post_status,
772 'post_content' => $this->clean_html( $p->post_content ),
773 'post_excerpt' => $this->post_excerpt( $p ),
774 'permalink' => get_permalink( $p ),
775 'post_date' => $p->post_date,
776 'post_modified' => $p->post_modified,
777 ];
778 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
779 break;
780
781 /* ===== Posts: create ===== */
782 case 'wp_create_post':
783 if ( empty( $a['post_title'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'post_title required' ]; break; }
784 $ins = [
785 'post_title' => sanitize_text_field( $a['post_title'] ),
786 'post_status' => sanitize_key( $a['post_status'] ?? 'draft' ),
787 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
788 ];
789 if ( $a['post_content'] ?? '' ) $ins['post_content'] = $this->core->markdown_to_html( $a['post_content'] );
790 if ( $a['post_excerpt'] ?? '' ) $ins['post_excerpt'] = $this->clean_html( $a['post_excerpt'] );
791 if ( $a['post_name'] ?? '' ) $ins['post_name'] = sanitize_title( $a['post_name'] );
792 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) $ins['meta_input'] = $a['meta_input'];
793 $new = wp_insert_post( $ins, true );
794 if ( is_wp_error( $new ) ) {
795 $r['error'] = [ 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ];
796 } else {
797 if ( empty( $ins['meta_input'] ) && !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) )
798 foreach ( $a['meta_input'] as $k => $v ) update_post_meta( $new, sanitize_key( $k ), maybe_serialize( $v ) );
799 $this->add_result_text( $r, 'Post created ID '.$new );
800 }
801 break;
802
803 /* ===== Posts: update ===== */
804 case 'wp_update_post':
805 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
806 $c = [ 'ID' => intval( $a['ID'] ) ];
807 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) )
808 foreach ( $a['fields'] as $k => $v )
809 $c[ $k ] = in_array( $k, [ 'post_content','post_excerpt' ], true ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
810 $u = ( count( $c ) > 1 ) ? wp_update_post( $c, true ) : $c['ID'];
811 if ( is_wp_error( $u ) ) { $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ]; break; }
812 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) )
813 foreach ( $a['meta_input'] as $k => $v ) update_post_meta( $u, sanitize_key( $k ), maybe_serialize( $v ) );
814 $this->add_result_text( $r, 'Post #'.$u.' updated' );
815 break;
816
817 /* ===== Posts: delete ===== */
818 case 'wp_delete_post':
819 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
820 $del = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
821 if ( $del ) $this->add_result_text( $r, 'Post #'.$a['ID'].' deleted' );
822 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
823 break;
824
825 /* ===== Post-meta ===== */
826 case 'wp_get_post_meta':
827 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
828 $pid = intval( $a['ID'] );
829 $out = ( $a['key'] ?? '' ) ? get_post_meta( $pid, sanitize_key( $a['key'] ), true ) : get_post_meta( $pid );
830 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
831 break;
832
833 case 'wp_update_post_meta':
834 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
835 $pid = intval( $a['ID'] );
836 if ( !empty( $a['meta'] ) && is_array( $a['meta'] ) ) {
837 foreach ( $a['meta'] as $k => $v ) update_post_meta( $pid, sanitize_key( $k ), maybe_serialize( $v ) );
838 } elseif ( isset( $a['key'], $a['value'] ) ) {
839 update_post_meta( $pid, sanitize_key( $a['key'] ), maybe_serialize( $a['value'] ) );
840 } else { $r['error'] = [ 'code' => -32602, 'message' => 'meta array or key/value required' ]; break; }
841 $this->add_result_text( $r, 'Meta updated for post #'.$pid );
842 break;
843
844 case 'wp_delete_post_meta':
845 if ( empty( $a['ID'] ) || empty( $a['key'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID & key required' ]; break; }
846 $pid = intval( $a['ID'] );
847 $key = sanitize_key( $a['key'] );
848 $done = isset( $a['value'] ) ? delete_post_meta( $pid, $key, maybe_serialize( $a['value'] ) ) : delete_post_meta( $pid, $key );
849 if ( $done ) $this->add_result_text( $r, 'Meta deleted on post #'.$pid );
850 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
851 break;
852
853 /* ===== Featured image ===== */
854 case 'wp_set_featured_image':
855 if ( empty( $a['post_id'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'post_id required' ]; break; }
856 $post_id = intval( $a['post_id'] );
857 $media_id = isset( $a['media_id'] ) ? intval( $a['media_id'] ) : 0;
858 if ( $media_id ) {
859 $done = set_post_thumbnail( $post_id, $media_id );
860 if ( $done ) $this->add_result_text( $r, 'Featured image set on post #'.$post_id );
861 else $r['error'] = [ 'code' => -32603, 'message' => 'Failed to set thumbnail' ];
862 } else {
863 delete_post_thumbnail( $post_id );
864 $this->add_result_text( $r, 'Featured image removed from post #'.$post_id );
865 }
866 break;
867
868 /* ===== Taxonomies ===== */
869 case 'wp_get_taxonomies':
870 $pt = sanitize_key( $a['post_type'] ?? 'post' );
871 $out = [];
872 foreach ( get_object_taxonomies( $pt, 'objects' ) as $t )
873 $out[] = [ 'key' => $t->name, 'label' => $t->label ];
874 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
875 break;
876
877 case 'wp_get_terms':
878 $tax = sanitize_key( $a['taxonomy'] );
879 $args = [
880 'taxonomy' => $tax,
881 'hide_empty' => false,
882 'number' => intval( $a['limit'] ?? 0 ),
883 'search' => $a['search'] ?? '',
884 ];
885 if ( isset( $a['parent'] ) ) $args['parent'] = intval( $a['parent'] );
886 $out = [];
887 foreach ( get_terms( $args ) as $t )
888 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name, 'slug' => $t->slug, 'count' => $t->count ];
889 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
890 break;
891
892 case 'wp_create_term':
893 if ( empty( $a['term_name'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'term_name required' ]; break; }
894 $tax = sanitize_key( $a['taxonomy'] );
895 $args = [];
896 if ( $a['slug'] ?? '' ) $args['slug'] = sanitize_title( $a['slug'] );
897 if ( $a['description'] ?? '' ) $args['description'] = sanitize_text_field( $a['description'] );
898 if ( isset( $a['parent'] ) ) $args['parent'] = intval( $a['parent'] );
899 $term = wp_insert_term( sanitize_text_field( $a['term_name'] ), $tax, $args );
900 if ( is_wp_error( $term ) ) $r['error'] = [ 'code' => $term->get_error_code(), 'message' => $term->get_error_message() ];
901 else $this->add_result_text( $r, 'Term '.$term['term_id'].' created' );
902 break;
903
904 case 'wp_update_term':
905 $tid = intval( $a['term_id'] ?? 0 );
906 if ( !$tid ) { $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ]; break; }
907 $tax = sanitize_key( $a['taxonomy'] );
908 $uargs = [];
909 foreach ( [ 'name','slug','description','parent' ] as $f ) if ( isset( $a[$f] ) ) $uargs[$f] = $a[$f];
910 $t = wp_update_term( $tid, $tax, $uargs );
911 if ( is_wp_error( $t ) ) $r['error'] = [ 'code' => $t->get_error_code(), 'message' => $t->get_error_message() ];
912 else $this->add_result_text( $r, 'Term '.$tid.' updated' );
913 break;
914
915 case 'wp_delete_term':
916 $tid = intval( $a['term_id'] ?? 0 );
917 if ( !$tid ) { $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ]; break; }
918 $tax = sanitize_key( $a['taxonomy'] );
919 $d = wp_delete_term( $tid, $tax );
920 if ( $d ) $this->add_result_text( $r, 'Term '.$tid.' deleted' );
921 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
922 break;
923
924 case 'wp_get_post_terms':
925 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
926 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
927 $out = [];
928 foreach ( wp_get_post_terms( intval( $a['ID'] ), $tax, [ 'fields' => 'all' ] ) as $t )
929 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name ];
930 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
931 break;
932
933 case 'wp_add_post_terms':
934 if ( empty( $a['ID'] ) || empty( $a['terms'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID & terms required' ]; break; }
935 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
936 $append = !isset( $a['append'] ) || $a['append'];
937 $set = wp_set_post_terms( intval( $a['ID'] ), $a['terms'], $tax, $append );
938 if ( is_wp_error( $set ) ) $r['error'] = [ 'code' => $set->get_error_code(), 'message' => $set->get_error_message() ];
939 else $this->add_result_text( $r, 'Terms set for post #'.$a['ID'] );
940 break;
941
942 /* ===== Media: list ===== */
943 case 'wp_get_media':
944 $q = [
945 'post_type' => 'attachment',
946 's' => $a['search'] ?? '',
947 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
948 'post_status' => 'inherit',
949 ];
950 $d = [];
951 if ( $a['after'] ?? '' ) $d['after'] = $a['after'];
952 if ( $a['before'] ?? '' ) $d['before'] = $a['before'];
953 if ( $d ) $q['date_query'] = [ $d ];
954 $list = [];
955 foreach ( get_posts( $q ) as $m )
956 $list[] = [ 'ID' => $m->ID, 'title' => $m->post_title, 'url' => wp_get_attachment_url( $m->ID ) ];
957 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
958 break;
959
960 /* ===== Media: upload ===== */
961 case 'wp_upload_media':
962 if ( empty( $a['url'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'url required' ]; break; }
963 try {
964 require_once ABSPATH.'wp-admin/includes/file.php';
965 require_once ABSPATH.'wp-admin/includes/media.php';
966 require_once ABSPATH.'wp-admin/includes/image.php';
967 $tmp = download_url( $a['url'] );
968 if ( is_wp_error( $tmp ) ) throw new Exception( $tmp->get_error_message(), $tmp->get_error_code() );
969 $file = [ 'name' => basename( parse_url( $a['url'], PHP_URL_PATH ) ), 'tmp_name' => $tmp ];
970 $id = media_handle_sideload( $file, 0, $a['description'] ?? '' );
971 @unlink( $tmp );
972 if ( is_wp_error( $id ) ) throw new Exception( $id->get_error_message(), $id->get_error_code() );
973 if ( $a['title'] ?? '' ) wp_update_post( [ 'ID' => $id, 'post_title' => sanitize_text_field( $a['title'] ) ] );
974 if ( $a['alt'] ?? '' ) update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
975 $this->add_result_text( $r, wp_get_attachment_url( $id ) );
976 } catch ( \Throwable $e ) {
977 $r['error'] = [ 'code' => $e->getCode() ?: -32603, 'message' => $e->getMessage() ];
978 }
979 break;
980
981 /* ===== Media: update ===== */
982 case 'wp_update_media':
983 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
984 $upd = [ 'ID' => intval( $a['ID'] ) ];
985 if ( $a['title'] ?? '' ) $upd['post_title'] = sanitize_text_field( $a['title'] );
986 if ( $a['caption'] ?? '' ) $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
987 if ( $a['description'] ?? '' ) $upd['post_content'] = $this->clean_html( $a['description'] );
988 $u = wp_update_post( $upd, true );
989 if ( is_wp_error( $u ) ) { $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ]; }
990 else {
991 if ( $a['alt'] ?? '' ) update_post_meta( $u, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
992 $this->add_result_text( $r, 'Media #'.$u.' updated' );
993 }
994 break;
995
996 /* ===== Media: delete ===== */
997 case 'wp_delete_media':
998 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
999 $d = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
1000 if ( $d ) $this->add_result_text( $r, 'Media #'.$a['ID'].' deleted' );
1001 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1002 break;
1003
1004 /* ===== MWAI Vision ===== */
1005 case 'mwai_vision':
1006 if ( empty( $a['message'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'message required' ]; break; }
1007 global $mwai;
1008 if ( !isset( $mwai ) ) { $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ]; break; }
1009 $analysis = $mwai->simpleVisionQuery(
1010 $a['message'],
1011 $a['url'] ?? null,
1012 $a['path'] ?? null,
1013 [ 'scope' => 'mcp' ]
1014 );
1015 $this->add_result_text( $r, is_string( $analysis ) ? $analysis : wp_json_encode( $analysis, JSON_PRETTY_PRINT ) );
1016 break;
1017
1018 /* ===== MWAI Image ===== */
1019 case 'mwai_image':
1020 if ( empty( $a['message'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'message required' ]; break; }
1021 global $mwai;
1022 if ( !isset( $mwai ) ) { $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ]; break; }
1023
1024 $media = $mwai->imageQueryForMediaLibrary( $a['message'], [ 'scope' => 'mcp' ], $a['postId'] ?? null );
1025 if ( is_wp_error( $media ) ) {
1026 $r['error'] = [ 'code' => $media->get_error_code(), 'message' => $media->get_error_message() ];
1027 break;
1028 }
1029
1030 $mid = intval( $media['id'] );
1031
1032 $upd = [ 'ID' => $mid ];
1033 if ( !empty( $a['title'] ) ) $upd['post_title'] = sanitize_text_field( $a['title'] );
1034 if ( !empty( $a['caption'] ) ) $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1035 if ( !empty( $a['description'] ) ) $upd['post_content'] = $this->clean_html( $a['description'] );
1036 if ( count( $upd ) > 1 ) wp_update_post( $upd, true );
1037 if ( array_key_exists( 'alt', $a ) ) update_post_meta( $mid, '_wp_attachment_image_alt', sanitize_text_field( (string) $a['alt'] ) );
1038
1039 $media = [
1040 'id' => $mid,
1041 'url' => wp_get_attachment_url( $mid ),
1042 'title' => get_the_title( $mid ),
1043 'caption' => wp_get_attachment_caption( $mid ),
1044 'alt' => get_post_meta( $mid, '_wp_attachment_image_alt', true ),
1045 ];
1046 $this->add_result_text( $r, wp_json_encode( $media, JSON_PRETTY_PRINT ) );
1047 break;
1048
1049 /* ===== Ping ===== */
1050 case 'mcp_ping':
1051 $r['result'] = [ 'time' => gmdate( 'Y-m-d H:i:s' ), 'name' => get_bloginfo( 'name' ) ];
1052 $this->add_result_text( $r, 'Ping successful: '.wp_json_encode( $r['result'], JSON_PRETTY_PRINT ) );
1053 break;
1054
1055 default: $r['error'] = [ 'code' => -32601, 'message' => 'Unknown tool' ];
1056 }
1057 return $r;
1058 }
1059 #endregion
1060 }
1061 ?>
1062