PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.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.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
1064 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 // Note: mcp_ping is now handled by the base MCP class
540 ];
541 }
542 #endregion
543
544 #region Tool Registration
545 public function register_rest_tools( array $prev ) : array {
546 $tools = $this->tools();
547 // Add category to each tool
548 foreach ( $tools as &$tool ) {
549 if ( !isset( $tool['category'] ) ) {
550 $tool['category'] = 'Core';
551 }
552 }
553 return array_merge( $prev, array_values( $tools ) );
554 }
555 #endregion
556
557 #region Callback
558 public function handle_call( $prev, string $tool, array $args, int $id ) {
559 if ( !current_user_can( 'administrator' ) ) wp_set_current_user( 1 );
560 if ( !empty( $prev ) || !isset( $this->tools()[ $tool ] ) ) return $prev;
561 return $this->dispatch( $tool, $args, $id );
562 }
563 #endregion
564
565 #region Dispatcher
566 private function dispatch( string $tool, array $a, int $id ) : array {
567 $r = [ 'jsonrpc' => '2.0', 'id' => $id ];
568
569 switch ( $tool ) {
570
571 /* ===== Users ===== */
572 case 'wp_get_users':
573 $q = [
574 'search' => '*' . esc_attr( $a['search'] ?? '' ) . '*',
575 'role' => $a['role'] ?? '',
576 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
577 ];
578 if ( isset( $a['offset'] ) ) $q['offset'] = max( 0, intval( $a['offset'] ) );
579 if ( isset( $a['paged'] ) ) $q['paged'] = max( 1, intval( $a['paged'] ) );
580 $rows = [];
581 foreach ( get_users( $q ) as $u )
582 $rows[] = [
583 'ID' => $u->ID,
584 'user_login' => $u->user_login,
585 'display_name' => $u->display_name,
586 'roles' => $u->roles,
587 ];
588 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
589 break;
590
591 case 'wp_create_user':
592 $data = [
593 'user_login' => sanitize_user( $a['user_login'] ),
594 'user_email' => sanitize_email( $a['user_email'] ),
595 'user_pass' => $a['user_pass'] ?? wp_generate_password( 12, true ),
596 'display_name' => sanitize_text_field( $a['display_name'] ?? '' ),
597 'role' => sanitize_key( $a['role'] ?? get_option( 'default_role', 'subscriber' ) ),
598 ];
599 $uid = wp_insert_user( $data );
600 if ( is_wp_error( $uid ) )
601 $r['error'] = [ 'code' => $uid->get_error_code(), 'message' => $uid->get_error_message() ];
602 else
603 $this->add_result_text( $r, 'User created ID '.$uid );
604 break;
605
606 case 'wp_update_user':
607 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
608 $upd = [ 'ID' => intval( $a['ID'] ) ];
609 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) )
610 foreach ( $a['fields'] as $k => $v ) $upd[ $k ] = ( $k === 'role' ) ? sanitize_key( $v ) : sanitize_text_field( $v );
611 $u = wp_update_user( $upd );
612 if ( is_wp_error( $u ) )
613 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
614 else
615 $this->add_result_text( $r, 'User #'.$u.' updated' );
616 break;
617
618 /* ===== Comments ===== */
619 case 'wp_get_comments':
620 $args = [
621 'post_id' => isset( $a['post_id'] ) ? intval( $a['post_id'] ) : '',
622 'status' => $a['status'] ?? 'approve',
623 'search' => $a['search'] ?? '',
624 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
625 ];
626 if ( isset( $a['offset'] ) ) $args['offset'] = max( 0, intval( $a['offset'] ) );
627 if ( isset( $a['paged'] ) ) $args['paged'] = max( 1, intval( $a['paged'] ) );
628 $list = [];
629 foreach ( get_comments( $args ) as $c )
630 $list[] = [
631 'comment_ID' => $c->comment_ID,
632 'comment_post_ID' => $c->comment_post_ID,
633 'comment_author' => $c->comment_author,
634 'comment_content' => wp_trim_words( wp_strip_all_tags( $c->comment_content ), 40 ),
635 'comment_date' => $c->comment_date,
636 'comment_approved'=> $c->comment_approved,
637 ];
638 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
639 break;
640
641 case 'wp_create_comment':
642 if ( empty( $a['post_id'] ) || empty( $a['comment_content'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'post_id & comment_content required' ]; break; }
643 $ins = [
644 'comment_post_ID' => intval( $a['post_id'] ),
645 'comment_content' => $this->clean_html( $a['comment_content'] ),
646 'comment_author' => sanitize_text_field( $a['comment_author'] ?? '' ),
647 'comment_author_email' => sanitize_email( $a['comment_author_email'] ?? '' ),
648 'comment_author_url' => esc_url_raw( $a['comment_author_url'] ?? '' ),
649 'comment_approved' => $a['comment_approved'] ?? 1,
650 ];
651 $cid = wp_insert_comment( $ins );
652 if ( is_wp_error( $cid ) ) {
653 /** @var WP_Error $cid */
654 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
655 }
656 else {
657 $this->add_result_text( $r, 'Comment created ID '.$cid );
658 }
659 break;
660
661 case 'wp_update_comment':
662 if ( empty( $a['comment_ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ]; break; }
663 $c = [ 'comment_ID' => intval( $a['comment_ID'] ) ];
664 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) )
665 foreach ( $a['fields'] as $k => $v )
666 $c[ $k ] = ( $k === 'comment_content' ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
667 $cid = wp_update_comment( $c, true );
668 if ( is_wp_error( $cid ) )
669 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
670 else
671 $this->add_result_text( $r, 'Comment #'.$cid.' updated' );
672 break;
673
674 case 'wp_delete_comment':
675 if ( empty( $a['comment_ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ]; break; }
676 $done = wp_delete_comment( intval( $a['comment_ID'] ), !empty( $a['force'] ) );
677 if ( $done )
678 $this->add_result_text( $r, 'Comment #'.$a['comment_ID'].' deleted' );
679 else
680 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
681 break;
682
683 /* ===== Options ===== */
684 case 'wp_get_option':
685 $val = get_option( sanitize_key( $a['key'] ) );
686 $this->add_result_text( $r, wp_json_encode( $val, JSON_PRETTY_PRINT ) );
687 break;
688
689 case 'wp_update_option':
690 $set = update_option( sanitize_key( $a['key'] ), $a['value'], 'yes' );
691 if ( $set ) $this->add_result_text( $r, 'Option "'.$a['key'].'" updated' );
692 else $r['error'] = [ 'code' => -32603, 'message' => 'Update failed' ];
693 break;
694
695 /* ===== Counts ===== */
696 case 'wp_count_posts':
697 $pt = sanitize_key( $a['post_type'] ?? 'post' );
698 $obj = wp_count_posts( $pt );
699 $this->add_result_text( $r, wp_json_encode( $obj, JSON_PRETTY_PRINT ) );
700 break;
701
702 case 'wp_count_terms':
703 $tax = sanitize_key( $a['taxonomy'] );
704 $total = wp_count_terms( $tax, [ 'hide_empty' => false ] );
705 if ( is_wp_error( $total ) )
706 $r['error'] = [ 'code' => $total->get_error_code(), 'message' => $total->get_error_message() ];
707 else
708 $this->add_result_text( $r, (string) $total );
709 break;
710
711 case 'wp_count_media':
712 $args = [ 'post_type' => 'attachment', 'post_status' => 'inherit', 'fields' => 'ids' ];
713 $d = [];
714 if ( $a['after'] ?? '' ) $d['after'] = $a['after'];
715 if ( $a['before'] ?? '' ) $d['before'] = $a['before'];
716 if ( $d ) $args['date_query'] = [ $d ];
717 $total = count( get_posts( $args ) );
718 $this->add_result_text( $r, (string) $total );
719 break;
720
721 /* ===== Post-types ===== */
722 case 'wp_get_post_types':
723 $out = [];
724 foreach ( get_post_types( [ 'public' => true ], 'objects' ) as $pt )
725 $out[] = [ 'key' => $pt->name, 'label' => $pt->label ];
726 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
727 break;
728
729 /* ===== Plugins ===== */
730 case 'wp_list_plugins':
731 if ( !function_exists( 'get_plugins' ) ) require_once ABSPATH . 'wp-admin/includes/plugin.php';
732 $search = sanitize_text_field( $a['search'] ?? '' );
733 $out = [];
734 foreach ( get_plugins() as $p ) {
735 if ( !$search || stripos( $p['Name'], $search ) !== false )
736 $out[] = [ 'Name' => $p['Name'], 'Version' => $p['Version'] ];
737 }
738 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
739 break;
740
741 /* ===== Posts: list ===== */
742 case 'wp_get_posts':
743 $q = [
744 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
745 'post_status' => sanitize_key( $a['post_status'] ?? 'publish' ),
746 's' => sanitize_text_field( $a['search'] ?? '' ),
747 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
748 ];
749 if ( isset( $a['offset'] ) ) $q['offset'] = max( 0, intval( $a['offset'] ) );
750 if ( isset( $a['paged'] ) ) $q['paged'] = max( 1, intval( $a['paged'] ) );
751 $date = [];
752 if ( $a['after'] ?? '' ) $date['after'] = $a['after'];
753 if ( $a['before'] ?? '' ) $date['before'] = $a['before'];
754 if ( $date ) $q['date_query'] = [ $date ];
755 $rows = [];
756 foreach ( get_posts( $q ) as $p ) {
757 $rows[] = [
758 'ID' => $p->ID,
759 'post_title' => $p->post_title,
760 'post_status' => $p->post_status,
761 'post_excerpt' => $this->post_excerpt( $p ),
762 'permalink' => get_permalink( $p ),
763 ];
764 }
765 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
766 break;
767
768 /* ===== Posts: single ===== */
769 case 'wp_get_post':
770 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
771 $p = get_post( intval( $a['ID'] ) );
772 if ( !$p ) { $r['error'] = [ 'code' => -32602, 'message' => 'Post not found' ]; break; }
773 $out = [
774 'ID' => $p->ID,
775 'post_title' => $p->post_title,
776 'post_status' => $p->post_status,
777 'post_content' => $this->clean_html( $p->post_content ),
778 'post_excerpt' => $this->post_excerpt( $p ),
779 'permalink' => get_permalink( $p ),
780 'post_date' => $p->post_date,
781 'post_modified' => $p->post_modified,
782 ];
783 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
784 break;
785
786 /* ===== Posts: create ===== */
787 case 'wp_create_post':
788 if ( empty( $a['post_title'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'post_title required' ]; break; }
789 $ins = [
790 'post_title' => sanitize_text_field( $a['post_title'] ),
791 'post_status' => sanitize_key( $a['post_status'] ?? 'draft' ),
792 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
793 ];
794 if ( $a['post_content'] ?? '' ) $ins['post_content'] = $this->core->markdown_to_html( $a['post_content'] );
795 if ( $a['post_excerpt'] ?? '' ) $ins['post_excerpt'] = $this->clean_html( $a['post_excerpt'] );
796 if ( $a['post_name'] ?? '' ) $ins['post_name'] = sanitize_title( $a['post_name'] );
797 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) $ins['meta_input'] = $a['meta_input'];
798 $new = wp_insert_post( $ins, true );
799 if ( is_wp_error( $new ) ) {
800 $r['error'] = [ 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ];
801 } else {
802 if ( empty( $ins['meta_input'] ) && !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) )
803 foreach ( $a['meta_input'] as $k => $v ) update_post_meta( $new, sanitize_key( $k ), maybe_serialize( $v ) );
804 $this->add_result_text( $r, 'Post created ID '.$new );
805 }
806 break;
807
808 /* ===== Posts: update ===== */
809 case 'wp_update_post':
810 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
811 $c = [ 'ID' => intval( $a['ID'] ) ];
812 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) )
813 foreach ( $a['fields'] as $k => $v )
814 $c[ $k ] = in_array( $k, [ 'post_content','post_excerpt' ], true ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
815 $u = ( count( $c ) > 1 ) ? wp_update_post( $c, true ) : $c['ID'];
816 if ( is_wp_error( $u ) ) { $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ]; break; }
817 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) )
818 foreach ( $a['meta_input'] as $k => $v ) update_post_meta( $u, sanitize_key( $k ), maybe_serialize( $v ) );
819 $this->add_result_text( $r, 'Post #'.$u.' updated' );
820 break;
821
822 /* ===== Posts: delete ===== */
823 case 'wp_delete_post':
824 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
825 $del = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
826 if ( $del ) $this->add_result_text( $r, 'Post #'.$a['ID'].' deleted' );
827 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
828 break;
829
830 /* ===== Post-meta ===== */
831 case 'wp_get_post_meta':
832 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
833 $pid = intval( $a['ID'] );
834 $out = ( $a['key'] ?? '' ) ? get_post_meta( $pid, sanitize_key( $a['key'] ), true ) : get_post_meta( $pid );
835 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
836 break;
837
838 case 'wp_update_post_meta':
839 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
840 $pid = intval( $a['ID'] );
841 if ( !empty( $a['meta'] ) && is_array( $a['meta'] ) ) {
842 foreach ( $a['meta'] as $k => $v ) update_post_meta( $pid, sanitize_key( $k ), maybe_serialize( $v ) );
843 } elseif ( isset( $a['key'], $a['value'] ) ) {
844 update_post_meta( $pid, sanitize_key( $a['key'] ), maybe_serialize( $a['value'] ) );
845 } else { $r['error'] = [ 'code' => -32602, 'message' => 'meta array or key/value required' ]; break; }
846 $this->add_result_text( $r, 'Meta updated for post #'.$pid );
847 break;
848
849 case 'wp_delete_post_meta':
850 if ( empty( $a['ID'] ) || empty( $a['key'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID & key required' ]; break; }
851 $pid = intval( $a['ID'] );
852 $key = sanitize_key( $a['key'] );
853 $done = isset( $a['value'] ) ? delete_post_meta( $pid, $key, maybe_serialize( $a['value'] ) ) : delete_post_meta( $pid, $key );
854 if ( $done ) $this->add_result_text( $r, 'Meta deleted on post #'.$pid );
855 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
856 break;
857
858 /* ===== Featured image ===== */
859 case 'wp_set_featured_image':
860 if ( empty( $a['post_id'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'post_id required' ]; break; }
861 $post_id = intval( $a['post_id'] );
862 $media_id = isset( $a['media_id'] ) ? intval( $a['media_id'] ) : 0;
863 if ( $media_id ) {
864 $done = set_post_thumbnail( $post_id, $media_id );
865 if ( $done ) $this->add_result_text( $r, 'Featured image set on post #'.$post_id );
866 else $r['error'] = [ 'code' => -32603, 'message' => 'Failed to set thumbnail' ];
867 } else {
868 delete_post_thumbnail( $post_id );
869 $this->add_result_text( $r, 'Featured image removed from post #'.$post_id );
870 }
871 break;
872
873 /* ===== Taxonomies ===== */
874 case 'wp_get_taxonomies':
875 $pt = sanitize_key( $a['post_type'] ?? 'post' );
876 $out = [];
877 foreach ( get_object_taxonomies( $pt, 'objects' ) as $t )
878 $out[] = [ 'key' => $t->name, 'label' => $t->label ];
879 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
880 break;
881
882 case 'wp_get_terms':
883 $tax = sanitize_key( $a['taxonomy'] );
884 $args = [
885 'taxonomy' => $tax,
886 'hide_empty' => false,
887 'number' => intval( $a['limit'] ?? 0 ),
888 'search' => $a['search'] ?? '',
889 ];
890 if ( isset( $a['parent'] ) ) $args['parent'] = intval( $a['parent'] );
891 $out = [];
892 foreach ( get_terms( $args ) as $t )
893 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name, 'slug' => $t->slug, 'count' => $t->count ];
894 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
895 break;
896
897 case 'wp_create_term':
898 if ( empty( $a['term_name'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'term_name required' ]; break; }
899 $tax = sanitize_key( $a['taxonomy'] );
900 $args = [];
901 if ( $a['slug'] ?? '' ) $args['slug'] = sanitize_title( $a['slug'] );
902 if ( $a['description'] ?? '' ) $args['description'] = sanitize_text_field( $a['description'] );
903 if ( isset( $a['parent'] ) ) $args['parent'] = intval( $a['parent'] );
904 $term = wp_insert_term( sanitize_text_field( $a['term_name'] ), $tax, $args );
905 if ( is_wp_error( $term ) ) $r['error'] = [ 'code' => $term->get_error_code(), 'message' => $term->get_error_message() ];
906 else $this->add_result_text( $r, 'Term '.$term['term_id'].' created' );
907 break;
908
909 case 'wp_update_term':
910 $tid = intval( $a['term_id'] ?? 0 );
911 if ( !$tid ) { $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ]; break; }
912 $tax = sanitize_key( $a['taxonomy'] );
913 $uargs = [];
914 foreach ( [ 'name','slug','description','parent' ] as $f ) if ( isset( $a[$f] ) ) $uargs[$f] = $a[$f];
915 $t = wp_update_term( $tid, $tax, $uargs );
916 if ( is_wp_error( $t ) ) $r['error'] = [ 'code' => $t->get_error_code(), 'message' => $t->get_error_message() ];
917 else $this->add_result_text( $r, 'Term '.$tid.' updated' );
918 break;
919
920 case 'wp_delete_term':
921 $tid = intval( $a['term_id'] ?? 0 );
922 if ( !$tid ) { $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ]; break; }
923 $tax = sanitize_key( $a['taxonomy'] );
924 $d = wp_delete_term( $tid, $tax );
925 if ( $d ) $this->add_result_text( $r, 'Term '.$tid.' deleted' );
926 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
927 break;
928
929 case 'wp_get_post_terms':
930 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
931 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
932 $out = [];
933 foreach ( wp_get_post_terms( intval( $a['ID'] ), $tax, [ 'fields' => 'all' ] ) as $t )
934 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name ];
935 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
936 break;
937
938 case 'wp_add_post_terms':
939 if ( empty( $a['ID'] ) || empty( $a['terms'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID & terms required' ]; break; }
940 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
941 $append = !isset( $a['append'] ) || $a['append'];
942 $set = wp_set_post_terms( intval( $a['ID'] ), $a['terms'], $tax, $append );
943 if ( is_wp_error( $set ) ) $r['error'] = [ 'code' => $set->get_error_code(), 'message' => $set->get_error_message() ];
944 else $this->add_result_text( $r, 'Terms set for post #'.$a['ID'] );
945 break;
946
947 /* ===== Media: list ===== */
948 case 'wp_get_media':
949 $q = [
950 'post_type' => 'attachment',
951 's' => $a['search'] ?? '',
952 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
953 'post_status' => 'inherit',
954 ];
955 $d = [];
956 if ( $a['after'] ?? '' ) $d['after'] = $a['after'];
957 if ( $a['before'] ?? '' ) $d['before'] = $a['before'];
958 if ( $d ) $q['date_query'] = [ $d ];
959 $list = [];
960 foreach ( get_posts( $q ) as $m )
961 $list[] = [ 'ID' => $m->ID, 'title' => $m->post_title, 'url' => wp_get_attachment_url( $m->ID ) ];
962 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
963 break;
964
965 /* ===== Media: upload ===== */
966 case 'wp_upload_media':
967 if ( empty( $a['url'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'url required' ]; break; }
968 try {
969 require_once ABSPATH.'wp-admin/includes/file.php';
970 require_once ABSPATH.'wp-admin/includes/media.php';
971 require_once ABSPATH.'wp-admin/includes/image.php';
972 $tmp = download_url( $a['url'] );
973 if ( is_wp_error( $tmp ) ) throw new Exception( $tmp->get_error_message(), $tmp->get_error_code() );
974 $file = [ 'name' => basename( parse_url( $a['url'], PHP_URL_PATH ) ), 'tmp_name' => $tmp ];
975 $id = media_handle_sideload( $file, 0, $a['description'] ?? '' );
976 @unlink( $tmp );
977 if ( is_wp_error( $id ) ) throw new Exception( $id->get_error_message(), $id->get_error_code() );
978 if ( $a['title'] ?? '' ) wp_update_post( [ 'ID' => $id, 'post_title' => sanitize_text_field( $a['title'] ) ] );
979 if ( $a['alt'] ?? '' ) update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
980 $this->add_result_text( $r, wp_get_attachment_url( $id ) );
981 } catch ( \Throwable $e ) {
982 $r['error'] = [ 'code' => $e->getCode() ?: -32603, 'message' => $e->getMessage() ];
983 }
984 break;
985
986 /* ===== Media: update ===== */
987 case 'wp_update_media':
988 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
989 $upd = [ 'ID' => intval( $a['ID'] ) ];
990 if ( $a['title'] ?? '' ) $upd['post_title'] = sanitize_text_field( $a['title'] );
991 if ( $a['caption'] ?? '' ) $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
992 if ( $a['description'] ?? '' ) $upd['post_content'] = $this->clean_html( $a['description'] );
993 $u = wp_update_post( $upd, true );
994 if ( is_wp_error( $u ) ) { $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ]; }
995 else {
996 if ( $a['alt'] ?? '' ) update_post_meta( $u, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
997 $this->add_result_text( $r, 'Media #'.$u.' updated' );
998 }
999 break;
1000
1001 /* ===== Media: delete ===== */
1002 case 'wp_delete_media':
1003 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
1004 $d = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
1005 if ( $d ) $this->add_result_text( $r, 'Media #'.$a['ID'].' deleted' );
1006 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1007 break;
1008
1009 /* ===== MWAI Vision ===== */
1010 case 'mwai_vision':
1011 if ( empty( $a['message'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'message required' ]; break; }
1012 global $mwai;
1013 if ( !isset( $mwai ) ) { $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ]; break; }
1014 $analysis = $mwai->simpleVisionQuery(
1015 $a['message'],
1016 $a['url'] ?? null,
1017 $a['path'] ?? null,
1018 [ 'scope' => 'mcp' ]
1019 );
1020 $this->add_result_text( $r, is_string( $analysis ) ? $analysis : wp_json_encode( $analysis, JSON_PRETTY_PRINT ) );
1021 break;
1022
1023 /* ===== MWAI Image ===== */
1024 case 'mwai_image':
1025 if ( empty( $a['message'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'message required' ]; break; }
1026 global $mwai;
1027 if ( !isset( $mwai ) ) { $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ]; break; }
1028
1029 $media = $mwai->imageQueryForMediaLibrary( $a['message'], [ 'scope' => 'mcp' ], $a['postId'] ?? null );
1030 if ( is_wp_error( $media ) ) {
1031 $r['error'] = [ 'code' => $media->get_error_code(), 'message' => $media->get_error_message() ];
1032 break;
1033 }
1034
1035 $mid = intval( $media['id'] );
1036
1037 $upd = [ 'ID' => $mid ];
1038 if ( !empty( $a['title'] ) ) $upd['post_title'] = sanitize_text_field( $a['title'] );
1039 if ( !empty( $a['caption'] ) ) $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1040 if ( !empty( $a['description'] ) ) $upd['post_content'] = $this->clean_html( $a['description'] );
1041 if ( count( $upd ) > 1 ) wp_update_post( $upd, true );
1042 if ( array_key_exists( 'alt', $a ) ) update_post_meta( $mid, '_wp_attachment_image_alt', sanitize_text_field( (string) $a['alt'] ) );
1043
1044 $media = [
1045 'id' => $mid,
1046 'url' => wp_get_attachment_url( $mid ),
1047 'title' => get_the_title( $mid ),
1048 'caption' => wp_get_attachment_caption( $mid ),
1049 'alt' => get_post_meta( $mid, '_wp_attachment_image_alt', true ),
1050 ];
1051 $this->add_result_text( $r, wp_json_encode( $media, JSON_PRETTY_PRINT ) );
1052 break;
1053
1054 /* ===== Ping ===== */
1055 // Note: mcp_ping is now handled by the base MCP class
1056
1057 default: $r['error'] = [ 'code' => -32601, 'message' => 'Unknown tool' ];
1058 }
1059 return $r;
1060 }
1061 #endregion
1062 }
1063 ?>
1064