PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.1
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.1
3.7.5 3.7.4 3.7.3 3.7.2 3.7.1 3.7.0 3.6.9 3.6.8 3.6.7 3.6.6 3.6.4 3.6.5 3.6.3 3.6.2 3.6.1 3.6.0 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
1059 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 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
649 else
650 $this->add_result_text( $r, 'Comment created ID '.$cid );
651 break;
652
653 case 'wp_update_comment':
654 if ( empty( $a['comment_ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ]; break; }
655 $c = [ 'comment_ID' => intval( $a['comment_ID'] ) ];
656 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) )
657 foreach ( $a['fields'] as $k => $v )
658 $c[ $k ] = ( $k === 'comment_content' ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
659 $cid = wp_update_comment( $c, true );
660 if ( is_wp_error( $cid ) )
661 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
662 else
663 $this->add_result_text( $r, 'Comment #'.$cid.' updated' );
664 break;
665
666 case 'wp_delete_comment':
667 if ( empty( $a['comment_ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ]; break; }
668 $done = wp_delete_comment( intval( $a['comment_ID'] ), !empty( $a['force'] ) );
669 if ( $done )
670 $this->add_result_text( $r, 'Comment #'.$a['comment_ID'].' deleted' );
671 else
672 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
673 break;
674
675 /* ===== Options ===== */
676 case 'wp_get_option':
677 $val = get_option( sanitize_key( $a['key'] ) );
678 $this->add_result_text( $r, wp_json_encode( $val, JSON_PRETTY_PRINT ) );
679 break;
680
681 case 'wp_update_option':
682 $set = update_option( sanitize_key( $a['key'] ), $a['value'], 'yes' );
683 if ( $set ) $this->add_result_text( $r, 'Option "'.$a['key'].'" updated' );
684 else $r['error'] = [ 'code' => -32603, 'message' => 'Update failed' ];
685 break;
686
687 /* ===== Counts ===== */
688 case 'wp_count_posts':
689 $pt = sanitize_key( $a['post_type'] ?? 'post' );
690 $obj = wp_count_posts( $pt );
691 $this->add_result_text( $r, wp_json_encode( $obj, JSON_PRETTY_PRINT ) );
692 break;
693
694 case 'wp_count_terms':
695 $tax = sanitize_key( $a['taxonomy'] );
696 $total = wp_count_terms( $tax, [ 'hide_empty' => false ] );
697 if ( is_wp_error( $total ) )
698 $r['error'] = [ 'code' => $total->get_error_code(), 'message' => $total->get_error_message() ];
699 else
700 $this->add_result_text( $r, (string) $total );
701 break;
702
703 case 'wp_count_media':
704 $args = [ 'post_type' => 'attachment', 'post_status' => 'inherit', 'fields' => 'ids' ];
705 $d = [];
706 if ( $a['after'] ?? '' ) $d['after'] = $a['after'];
707 if ( $a['before'] ?? '' ) $d['before'] = $a['before'];
708 if ( $d ) $args['date_query'] = [ $d ];
709 $total = count( get_posts( $args ) );
710 $this->add_result_text( $r, (string) $total );
711 break;
712
713 /* ===== Post-types ===== */
714 case 'wp_get_post_types':
715 $out = [];
716 foreach ( get_post_types( [ 'public' => true ], 'objects' ) as $pt )
717 $out[] = [ 'key' => $pt->name, 'label' => $pt->label ];
718 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
719 break;
720
721 /* ===== Plugins ===== */
722 case 'wp_list_plugins':
723 if ( !function_exists( 'get_plugins' ) ) require_once ABSPATH . 'wp-admin/includes/plugin.php';
724 $search = sanitize_text_field( $a['search'] ?? '' );
725 $out = [];
726 foreach ( get_plugins() as $p ) {
727 if ( !$search || stripos( $p['Name'], $search ) !== false )
728 $out[] = [ 'Name' => $p['Name'], 'Version' => $p['Version'] ];
729 }
730 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
731 break;
732
733 /* ===== Posts: list ===== */
734 case 'wp_get_posts':
735 $q = [
736 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
737 'post_status' => sanitize_key( $a['post_status'] ?? 'publish' ),
738 's' => sanitize_text_field( $a['search'] ?? '' ),
739 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
740 ];
741 if ( isset( $a['offset'] ) ) $q['offset'] = max( 0, intval( $a['offset'] ) );
742 if ( isset( $a['paged'] ) ) $q['paged'] = max( 1, intval( $a['paged'] ) );
743 $date = [];
744 if ( $a['after'] ?? '' ) $date['after'] = $a['after'];
745 if ( $a['before'] ?? '' ) $date['before'] = $a['before'];
746 if ( $date ) $q['date_query'] = [ $date ];
747 $rows = [];
748 foreach ( get_posts( $q ) as $p ) {
749 $rows[] = [
750 'ID' => $p->ID,
751 'post_title' => $p->post_title,
752 'post_status' => $p->post_status,
753 'post_excerpt' => $this->post_excerpt( $p ),
754 'permalink' => get_permalink( $p ),
755 ];
756 }
757 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
758 break;
759
760 /* ===== Posts: single ===== */
761 case 'wp_get_post':
762 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
763 $p = get_post( intval( $a['ID'] ) );
764 if ( !$p ) { $r['error'] = [ 'code' => -32602, 'message' => 'Post not found' ]; break; }
765 $out = [
766 'ID' => $p->ID,
767 'post_title' => $p->post_title,
768 'post_status' => $p->post_status,
769 'post_content' => $this->clean_html( $p->post_content ),
770 'post_excerpt' => $this->post_excerpt( $p ),
771 'permalink' => get_permalink( $p ),
772 'post_date' => $p->post_date,
773 'post_modified' => $p->post_modified,
774 ];
775 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
776 break;
777
778 /* ===== Posts: create ===== */
779 case 'wp_create_post':
780 if ( empty( $a['post_title'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'post_title required' ]; break; }
781 $ins = [
782 'post_title' => sanitize_text_field( $a['post_title'] ),
783 'post_status' => sanitize_key( $a['post_status'] ?? 'draft' ),
784 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
785 ];
786 if ( $a['post_content'] ?? '' ) $ins['post_content'] = $this->core->markdown_to_html( $a['post_content'] );
787 if ( $a['post_excerpt'] ?? '' ) $ins['post_excerpt'] = $this->clean_html( $a['post_excerpt'] );
788 if ( $a['post_name'] ?? '' ) $ins['post_name'] = sanitize_title( $a['post_name'] );
789 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) $ins['meta_input'] = $a['meta_input'];
790 $new = wp_insert_post( $ins, true );
791 if ( is_wp_error( $new ) ) {
792 $r['error'] = [ 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ];
793 } else {
794 if ( empty( $ins['meta_input'] ) && !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) )
795 foreach ( $a['meta_input'] as $k => $v ) update_post_meta( $new, sanitize_key( $k ), maybe_serialize( $v ) );
796 $this->add_result_text( $r, 'Post created ID '.$new );
797 }
798 break;
799
800 /* ===== Posts: update ===== */
801 case 'wp_update_post':
802 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
803 $c = [ 'ID' => intval( $a['ID'] ) ];
804 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) )
805 foreach ( $a['fields'] as $k => $v )
806 $c[ $k ] = in_array( $k, [ 'post_content','post_excerpt' ], true ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
807 $u = ( count( $c ) > 1 ) ? wp_update_post( $c, true ) : $c['ID'];
808 if ( is_wp_error( $u ) ) { $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ]; break; }
809 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) )
810 foreach ( $a['meta_input'] as $k => $v ) update_post_meta( $u, sanitize_key( $k ), maybe_serialize( $v ) );
811 $this->add_result_text( $r, 'Post #'.$u.' updated' );
812 break;
813
814 /* ===== Posts: delete ===== */
815 case 'wp_delete_post':
816 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
817 $del = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
818 if ( $del ) $this->add_result_text( $r, 'Post #'.$a['ID'].' deleted' );
819 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
820 break;
821
822 /* ===== Post-meta ===== */
823 case 'wp_get_post_meta':
824 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
825 $pid = intval( $a['ID'] );
826 $out = ( $a['key'] ?? '' ) ? get_post_meta( $pid, sanitize_key( $a['key'] ), true ) : get_post_meta( $pid );
827 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
828 break;
829
830 case 'wp_update_post_meta':
831 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
832 $pid = intval( $a['ID'] );
833 if ( !empty( $a['meta'] ) && is_array( $a['meta'] ) ) {
834 foreach ( $a['meta'] as $k => $v ) update_post_meta( $pid, sanitize_key( $k ), maybe_serialize( $v ) );
835 } elseif ( isset( $a['key'], $a['value'] ) ) {
836 update_post_meta( $pid, sanitize_key( $a['key'] ), maybe_serialize( $a['value'] ) );
837 } else { $r['error'] = [ 'code' => -32602, 'message' => 'meta array or key/value required' ]; break; }
838 $this->add_result_text( $r, 'Meta updated for post #'.$pid );
839 break;
840
841 case 'wp_delete_post_meta':
842 if ( empty( $a['ID'] ) || empty( $a['key'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID & key required' ]; break; }
843 $pid = intval( $a['ID'] );
844 $key = sanitize_key( $a['key'] );
845 $done = isset( $a['value'] ) ? delete_post_meta( $pid, $key, maybe_serialize( $a['value'] ) ) : delete_post_meta( $pid, $key );
846 if ( $done ) $this->add_result_text( $r, 'Meta deleted on post #'.$pid );
847 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
848 break;
849
850 /* ===== Featured image ===== */
851 case 'wp_set_featured_image':
852 if ( empty( $a['post_id'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'post_id required' ]; break; }
853 $post_id = intval( $a['post_id'] );
854 $media_id = isset( $a['media_id'] ) ? intval( $a['media_id'] ) : 0;
855 if ( $media_id ) {
856 $done = set_post_thumbnail( $post_id, $media_id );
857 if ( $done ) $this->add_result_text( $r, 'Featured image set on post #'.$post_id );
858 else $r['error'] = [ 'code' => -32603, 'message' => 'Failed to set thumbnail' ];
859 } else {
860 delete_post_thumbnail( $post_id );
861 $this->add_result_text( $r, 'Featured image removed from post #'.$post_id );
862 }
863 break;
864
865 /* ===== Taxonomies ===== */
866 case 'wp_get_taxonomies':
867 $pt = sanitize_key( $a['post_type'] ?? 'post' );
868 $out = [];
869 foreach ( get_object_taxonomies( $pt, 'objects' ) as $t )
870 $out[] = [ 'key' => $t->name, 'label' => $t->label ];
871 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
872 break;
873
874 case 'wp_get_terms':
875 $tax = sanitize_key( $a['taxonomy'] );
876 $args = [
877 'taxonomy' => $tax,
878 'hide_empty' => false,
879 'number' => intval( $a['limit'] ?? 0 ),
880 'search' => $a['search'] ?? '',
881 ];
882 if ( isset( $a['parent'] ) ) $args['parent'] = intval( $a['parent'] );
883 $out = [];
884 foreach ( get_terms( $args ) as $t )
885 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name, 'slug' => $t->slug, 'count' => $t->count ];
886 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
887 break;
888
889 case 'wp_create_term':
890 if ( empty( $a['term_name'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'term_name required' ]; break; }
891 $tax = sanitize_key( $a['taxonomy'] );
892 $args = [];
893 if ( $a['slug'] ?? '' ) $args['slug'] = sanitize_title( $a['slug'] );
894 if ( $a['description'] ?? '' ) $args['description'] = sanitize_text_field( $a['description'] );
895 if ( isset( $a['parent'] ) ) $args['parent'] = intval( $a['parent'] );
896 $term = wp_insert_term( sanitize_text_field( $a['term_name'] ), $tax, $args );
897 if ( is_wp_error( $term ) ) $r['error'] = [ 'code' => $term->get_error_code(), 'message' => $term->get_error_message() ];
898 else $this->add_result_text( $r, 'Term '.$term['term_id'].' created' );
899 break;
900
901 case 'wp_update_term':
902 $tid = intval( $a['term_id'] ?? 0 );
903 if ( !$tid ) { $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ]; break; }
904 $tax = sanitize_key( $a['taxonomy'] );
905 $uargs = [];
906 foreach ( [ 'name','slug','description','parent' ] as $f ) if ( isset( $a[$f] ) ) $uargs[$f] = $a[$f];
907 $t = wp_update_term( $tid, $tax, $uargs );
908 if ( is_wp_error( $t ) ) $r['error'] = [ 'code' => $t->get_error_code(), 'message' => $t->get_error_message() ];
909 else $this->add_result_text( $r, 'Term '.$tid.' updated' );
910 break;
911
912 case 'wp_delete_term':
913 $tid = intval( $a['term_id'] ?? 0 );
914 if ( !$tid ) { $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ]; break; }
915 $tax = sanitize_key( $a['taxonomy'] );
916 $d = wp_delete_term( $tid, $tax );
917 if ( $d ) $this->add_result_text( $r, 'Term '.$tid.' deleted' );
918 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
919 break;
920
921 case 'wp_get_post_terms':
922 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
923 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
924 $out = [];
925 foreach ( wp_get_post_terms( intval( $a['ID'] ), $tax, [ 'fields' => 'all' ] ) as $t )
926 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name ];
927 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
928 break;
929
930 case 'wp_add_post_terms':
931 if ( empty( $a['ID'] ) || empty( $a['terms'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID & terms required' ]; break; }
932 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
933 $append = !isset( $a['append'] ) || $a['append'];
934 $set = wp_set_post_terms( intval( $a['ID'] ), $a['terms'], $tax, $append );
935 if ( is_wp_error( $set ) ) $r['error'] = [ 'code' => $set->get_error_code(), 'message' => $set->get_error_message() ];
936 else $this->add_result_text( $r, 'Terms set for post #'.$a['ID'] );
937 break;
938
939 /* ===== Media: list ===== */
940 case 'wp_get_media':
941 $q = [
942 'post_type' => 'attachment',
943 's' => $a['search'] ?? '',
944 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
945 'post_status' => 'inherit',
946 ];
947 $d = [];
948 if ( $a['after'] ?? '' ) $d['after'] = $a['after'];
949 if ( $a['before'] ?? '' ) $d['before'] = $a['before'];
950 if ( $d ) $q['date_query'] = [ $d ];
951 $list = [];
952 foreach ( get_posts( $q ) as $m )
953 $list[] = [ 'ID' => $m->ID, 'title' => $m->post_title, 'url' => wp_get_attachment_url( $m->ID ) ];
954 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
955 break;
956
957 /* ===== Media: upload ===== */
958 case 'wp_upload_media':
959 if ( empty( $a['url'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'url required' ]; break; }
960 try {
961 require_once ABSPATH.'wp-admin/includes/file.php';
962 require_once ABSPATH.'wp-admin/includes/media.php';
963 require_once ABSPATH.'wp-admin/includes/image.php';
964 $tmp = download_url( $a['url'] );
965 if ( is_wp_error( $tmp ) ) throw new Exception( $tmp->get_error_message(), $tmp->get_error_code() );
966 $file = [ 'name' => basename( parse_url( $a['url'], PHP_URL_PATH ) ), 'tmp_name' => $tmp ];
967 $id = media_handle_sideload( $file, 0, $a['description'] ?? '' );
968 @unlink( $tmp );
969 if ( is_wp_error( $id ) ) throw new Exception( $id->get_error_message(), $id->get_error_code() );
970 if ( $a['title'] ?? '' ) wp_update_post( [ 'ID' => $id, 'post_title' => sanitize_text_field( $a['title'] ) ] );
971 if ( $a['alt'] ?? '' ) update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
972 $this->add_result_text( $r, wp_get_attachment_url( $id ) );
973 } catch ( \Throwable $e ) {
974 $r['error'] = [ 'code' => $e->getCode() ?: -32603, 'message' => $e->getMessage() ];
975 }
976 break;
977
978 /* ===== Media: update ===== */
979 case 'wp_update_media':
980 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
981 $upd = [ 'ID' => intval( $a['ID'] ) ];
982 if ( $a['title'] ?? '' ) $upd['post_title'] = sanitize_text_field( $a['title'] );
983 if ( $a['caption'] ?? '' ) $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
984 if ( $a['description'] ?? '' ) $upd['post_content'] = $this->clean_html( $a['description'] );
985 $u = wp_update_post( $upd, true );
986 if ( is_wp_error( $u ) ) { $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ]; }
987 else {
988 if ( $a['alt'] ?? '' ) update_post_meta( $u, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
989 $this->add_result_text( $r, 'Media #'.$u.' updated' );
990 }
991 break;
992
993 /* ===== Media: delete ===== */
994 case 'wp_delete_media':
995 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
996 $d = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
997 if ( $d ) $this->add_result_text( $r, 'Media #'.$a['ID'].' deleted' );
998 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
999 break;
1000
1001 /* ===== MWAI Vision ===== */
1002 case 'mwai_vision':
1003 if ( empty( $a['message'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'message required' ]; break; }
1004 global $mwai;
1005 if ( !isset( $mwai ) ) { $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ]; break; }
1006 $analysis = $mwai->simpleVisionQuery(
1007 $a['message'],
1008 $a['url'] ?? null,
1009 $a['path'] ?? null,
1010 [ 'scope' => 'mcp' ]
1011 );
1012 $this->add_result_text( $r, is_string( $analysis ) ? $analysis : wp_json_encode( $analysis, JSON_PRETTY_PRINT ) );
1013 break;
1014
1015 /* ===== MWAI Image ===== */
1016 case 'mwai_image':
1017 if ( empty( $a['message'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'message required' ]; break; }
1018 global $mwai;
1019 if ( !isset( $mwai ) ) { $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ]; break; }
1020
1021 $media = $mwai->imageQueryForMediaLibrary( $a['message'], [ 'scope' => 'mcp' ], $a['postId'] ?? null );
1022 if ( is_wp_error( $media ) ) {
1023 $r['error'] = [ 'code' => $media->get_error_code(), 'message' => $media->get_error_message() ];
1024 break;
1025 }
1026
1027 $mid = intval( $media['id'] );
1028
1029 $upd = [ 'ID' => $mid ];
1030 if ( !empty( $a['title'] ) ) $upd['post_title'] = sanitize_text_field( $a['title'] );
1031 if ( !empty( $a['caption'] ) ) $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1032 if ( !empty( $a['description'] ) ) $upd['post_content'] = $this->clean_html( $a['description'] );
1033 if ( count( $upd ) > 1 ) wp_update_post( $upd, true );
1034 if ( array_key_exists( 'alt', $a ) ) update_post_meta( $mid, '_wp_attachment_image_alt', sanitize_text_field( (string) $a['alt'] ) );
1035
1036 $media = [
1037 'id' => $mid,
1038 'url' => wp_get_attachment_url( $mid ),
1039 'title' => get_the_title( $mid ),
1040 'caption' => wp_get_attachment_caption( $mid ),
1041 'alt' => get_post_meta( $mid, '_wp_attachment_image_alt', true ),
1042 ];
1043 $this->add_result_text( $r, wp_json_encode( $media, JSON_PRETTY_PRINT ) );
1044 break;
1045
1046 /* ===== Ping ===== */
1047 case 'mcp_ping':
1048 $r['result'] = [ 'time' => gmdate( 'Y-m-d H:i:s' ), 'name' => get_bloginfo( 'name' ) ];
1049 $this->add_result_text( $r, 'Ping successful: '.wp_json_encode( $r['result'], JSON_PRETTY_PRINT ) );
1050 break;
1051
1052 default: $r['error'] = [ 'code' => -32601, 'message' => 'Unknown tool' ];
1053 }
1054 return $r;
1055 }
1056 #endregion
1057 }
1058 ?>
1059