PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.4
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / labs / mcp_core.php
ai-engine / labs Last commit date
mcp.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 oauth.php 1 year ago realtime.php 1 year ago
mcp_core.php
1194 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 /* -------- OpenAI Deep Research Tools -------- */
542 'search' => [
543 'name' => 'search',
544 'description' => 'Searches through all published posts and pages on the "' . get_bloginfo( 'name' ) . '" WordPress website' . ( get_bloginfo( 'description' ) ? ' - ' . get_bloginfo( 'description' ) : '' ) . '. This tool performs full-text search across titles and content to find relevant articles, blog posts, and static pages. The search results include article summaries and URLs for citation purposes. Use this to find information about topics covered on this WordPress site, including blog posts, tutorials, documentation, news, and any other content published on the website.',
545 'inputSchema' => [
546 'type' => 'object',
547 'properties' => [
548 'query' => [ 'type' => 'string', 'description' => 'Search query to find relevant posts and pages. Can be keywords, phrases, or topics.' ],
549 ],
550 'required' => [ 'query' ],
551 ],
552 ],
553
554 'fetch' => [
555 'name' => 'fetch',
556 'description' => 'Retrieves the complete content of a specific post or page from the "' . get_bloginfo( 'name' ) . '" WordPress website' . ( get_bloginfo( 'description' ) ? ' - ' . get_bloginfo( 'description' ) : '' ) . ' using its ID. This returns the full article text, metadata (author, publication date, categories, tags), and URL for proper citation. Use this after searching to get the complete content of relevant articles for deep analysis and comprehensive answers. The content is essential for providing accurate, detailed responses based on the actual information published on the website.',
557 'inputSchema' => [
558 'type' => 'object',
559 'properties' => [
560 'id' => [ 'type' => 'string', 'description' => 'The WordPress post ID obtained from search results.' ],
561 ],
562 'required' => [ 'id' ],
563 ],
564 ],
565 ];
566 }
567 #endregion
568
569 #region Tool Registration
570 public function register_rest_tools( array $prev ) : array {
571 $tools = $this->tools();
572 // Add category to each tool
573 foreach ( $tools as &$tool ) {
574 if ( !isset( $tool['category'] ) ) {
575 // Set Core: OpenAI category for search and fetch tools
576 if ( in_array( $tool['name'], ['search', 'fetch'] ) ) {
577 $tool['category'] = 'Core: OpenAI';
578 } else {
579 $tool['category'] = 'Core';
580 }
581 }
582 }
583 return array_merge( $prev, array_values( $tools ) );
584 }
585 #endregion
586
587 #region Callback
588 public function handle_call( $prev, string $tool, array $args, int $id ) {
589 // Security check is already done in the MCP auth layer
590 // If we reach here, the user is authorized to use MCP
591 if ( !empty( $prev ) || !isset( $this->tools()[ $tool ] ) ) return $prev;
592 return $this->dispatch( $tool, $args, $id );
593 }
594 #endregion
595
596 #region Dispatcher
597 private function dispatch( string $tool, array $a, int $id ) : array {
598 $r = [ 'jsonrpc' => '2.0', 'id' => $id ];
599
600 switch ( $tool ) {
601
602 /* ===== Users ===== */
603 case 'wp_get_users':
604 $q = [
605 'search' => '*' . esc_attr( $a['search'] ?? '' ) . '*',
606 'role' => $a['role'] ?? '',
607 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
608 ];
609 if ( isset( $a['offset'] ) ) $q['offset'] = max( 0, intval( $a['offset'] ) );
610 if ( isset( $a['paged'] ) ) $q['paged'] = max( 1, intval( $a['paged'] ) );
611 $rows = [];
612 foreach ( get_users( $q ) as $u )
613 $rows[] = [
614 'ID' => $u->ID,
615 'user_login' => $u->user_login,
616 'display_name' => $u->display_name,
617 'roles' => $u->roles,
618 ];
619 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
620 break;
621
622 case 'wp_create_user':
623 $data = [
624 'user_login' => sanitize_user( $a['user_login'] ),
625 'user_email' => sanitize_email( $a['user_email'] ),
626 'user_pass' => $a['user_pass'] ?? wp_generate_password( 12, true ),
627 'display_name' => sanitize_text_field( $a['display_name'] ?? '' ),
628 'role' => sanitize_key( $a['role'] ?? get_option( 'default_role', 'subscriber' ) ),
629 ];
630 $uid = wp_insert_user( $data );
631 if ( is_wp_error( $uid ) )
632 $r['error'] = [ 'code' => $uid->get_error_code(), 'message' => $uid->get_error_message() ];
633 else
634 $this->add_result_text( $r, 'User created ID '.$uid );
635 break;
636
637 case 'wp_update_user':
638 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
639 $upd = [ 'ID' => intval( $a['ID'] ) ];
640 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) )
641 foreach ( $a['fields'] as $k => $v ) $upd[ $k ] = ( $k === 'role' ) ? sanitize_key( $v ) : sanitize_text_field( $v );
642 $u = wp_update_user( $upd );
643 if ( is_wp_error( $u ) )
644 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
645 else
646 $this->add_result_text( $r, 'User #'.$u.' updated' );
647 break;
648
649 /* ===== Comments ===== */
650 case 'wp_get_comments':
651 $args = [
652 'post_id' => isset( $a['post_id'] ) ? intval( $a['post_id'] ) : '',
653 'status' => $a['status'] ?? 'approve',
654 'search' => $a['search'] ?? '',
655 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
656 ];
657 if ( isset( $a['offset'] ) ) $args['offset'] = max( 0, intval( $a['offset'] ) );
658 if ( isset( $a['paged'] ) ) $args['paged'] = max( 1, intval( $a['paged'] ) );
659 $list = [];
660 foreach ( get_comments( $args ) as $c )
661 $list[] = [
662 'comment_ID' => $c->comment_ID,
663 'comment_post_ID' => $c->comment_post_ID,
664 'comment_author' => $c->comment_author,
665 'comment_content' => wp_trim_words( wp_strip_all_tags( $c->comment_content ), 40 ),
666 'comment_date' => $c->comment_date,
667 'comment_approved'=> $c->comment_approved,
668 ];
669 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
670 break;
671
672 case 'wp_create_comment':
673 if ( empty( $a['post_id'] ) || empty( $a['comment_content'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'post_id & comment_content required' ]; break; }
674 $ins = [
675 'comment_post_ID' => intval( $a['post_id'] ),
676 'comment_content' => $this->clean_html( $a['comment_content'] ),
677 'comment_author' => sanitize_text_field( $a['comment_author'] ?? '' ),
678 'comment_author_email' => sanitize_email( $a['comment_author_email'] ?? '' ),
679 'comment_author_url' => esc_url_raw( $a['comment_author_url'] ?? '' ),
680 'comment_approved' => $a['comment_approved'] ?? 1,
681 ];
682 $cid = wp_insert_comment( $ins );
683 if ( is_wp_error( $cid ) ) {
684 /** @var WP_Error $cid */
685 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
686 }
687 else {
688 $this->add_result_text( $r, 'Comment created ID '.$cid );
689 }
690 break;
691
692 case 'wp_update_comment':
693 if ( empty( $a['comment_ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ]; break; }
694 $c = [ 'comment_ID' => intval( $a['comment_ID'] ) ];
695 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) )
696 foreach ( $a['fields'] as $k => $v )
697 $c[ $k ] = ( $k === 'comment_content' ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
698 $cid = wp_update_comment( $c, true );
699 if ( is_wp_error( $cid ) )
700 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
701 else
702 $this->add_result_text( $r, 'Comment #'.$cid.' updated' );
703 break;
704
705 case 'wp_delete_comment':
706 if ( empty( $a['comment_ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ]; break; }
707 $done = wp_delete_comment( intval( $a['comment_ID'] ), !empty( $a['force'] ) );
708 if ( $done )
709 $this->add_result_text( $r, 'Comment #'.$a['comment_ID'].' deleted' );
710 else
711 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
712 break;
713
714 /* ===== Options ===== */
715 case 'wp_get_option':
716 $val = get_option( sanitize_key( $a['key'] ) );
717 $this->add_result_text( $r, wp_json_encode( $val, JSON_PRETTY_PRINT ) );
718 break;
719
720 case 'wp_update_option':
721 $set = update_option( sanitize_key( $a['key'] ), $a['value'], 'yes' );
722 if ( $set ) $this->add_result_text( $r, 'Option "'.$a['key'].'" updated' );
723 else $r['error'] = [ 'code' => -32603, 'message' => 'Update failed' ];
724 break;
725
726 /* ===== Counts ===== */
727 case 'wp_count_posts':
728 $pt = sanitize_key( $a['post_type'] ?? 'post' );
729 $obj = wp_count_posts( $pt );
730 $this->add_result_text( $r, wp_json_encode( $obj, JSON_PRETTY_PRINT ) );
731 break;
732
733 case 'wp_count_terms':
734 $tax = sanitize_key( $a['taxonomy'] );
735 $total = wp_count_terms( $tax, [ 'hide_empty' => false ] );
736 if ( is_wp_error( $total ) )
737 $r['error'] = [ 'code' => $total->get_error_code(), 'message' => $total->get_error_message() ];
738 else
739 $this->add_result_text( $r, (string) $total );
740 break;
741
742 case 'wp_count_media':
743 $args = [ 'post_type' => 'attachment', 'post_status' => 'inherit', 'fields' => 'ids' ];
744 $d = [];
745 if ( $a['after'] ?? '' ) $d['after'] = $a['after'];
746 if ( $a['before'] ?? '' ) $d['before'] = $a['before'];
747 if ( $d ) $args['date_query'] = [ $d ];
748 $total = count( get_posts( $args ) );
749 $this->add_result_text( $r, (string) $total );
750 break;
751
752 /* ===== Post-types ===== */
753 case 'wp_get_post_types':
754 $out = [];
755 foreach ( get_post_types( [ 'public' => true ], 'objects' ) as $pt )
756 $out[] = [ 'key' => $pt->name, 'label' => $pt->label ];
757 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
758 break;
759
760 /* ===== Plugins ===== */
761 case 'wp_list_plugins':
762 if ( !function_exists( 'get_plugins' ) ) require_once ABSPATH . 'wp-admin/includes/plugin.php';
763 $search = sanitize_text_field( $a['search'] ?? '' );
764 $out = [];
765 foreach ( get_plugins() as $p ) {
766 if ( !$search || stripos( $p['Name'], $search ) !== false )
767 $out[] = [ 'Name' => $p['Name'], 'Version' => $p['Version'] ];
768 }
769 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
770 break;
771
772 /* ===== Posts: list ===== */
773 case 'wp_get_posts':
774 $q = [
775 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
776 'post_status' => sanitize_key( $a['post_status'] ?? 'publish' ),
777 's' => sanitize_text_field( $a['search'] ?? '' ),
778 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
779 ];
780 if ( isset( $a['offset'] ) ) $q['offset'] = max( 0, intval( $a['offset'] ) );
781 if ( isset( $a['paged'] ) ) $q['paged'] = max( 1, intval( $a['paged'] ) );
782 $date = [];
783 if ( $a['after'] ?? '' ) $date['after'] = $a['after'];
784 if ( $a['before'] ?? '' ) $date['before'] = $a['before'];
785 if ( $date ) $q['date_query'] = [ $date ];
786 $rows = [];
787 foreach ( get_posts( $q ) as $p ) {
788 $rows[] = [
789 'ID' => $p->ID,
790 'post_title' => $p->post_title,
791 'post_status' => $p->post_status,
792 'post_excerpt' => $this->post_excerpt( $p ),
793 'permalink' => get_permalink( $p ),
794 ];
795 }
796 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
797 break;
798
799 /* ===== Posts: single ===== */
800 case 'wp_get_post':
801 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
802 $p = get_post( intval( $a['ID'] ) );
803 if ( !$p ) { $r['error'] = [ 'code' => -32602, 'message' => 'Post not found' ]; break; }
804 $out = [
805 'ID' => $p->ID,
806 'post_title' => $p->post_title,
807 'post_status' => $p->post_status,
808 'post_content' => $this->clean_html( $p->post_content ),
809 'post_excerpt' => $this->post_excerpt( $p ),
810 'permalink' => get_permalink( $p ),
811 'post_date' => $p->post_date,
812 'post_modified' => $p->post_modified,
813 ];
814 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
815 break;
816
817 /* ===== Posts: create ===== */
818 case 'wp_create_post':
819 if ( empty( $a['post_title'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'post_title required' ]; break; }
820 $ins = [
821 'post_title' => sanitize_text_field( $a['post_title'] ),
822 'post_status' => sanitize_key( $a['post_status'] ?? 'draft' ),
823 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
824 ];
825 if ( $a['post_content'] ?? '' ) $ins['post_content'] = $this->core->markdown_to_html( $a['post_content'] );
826 if ( $a['post_excerpt'] ?? '' ) $ins['post_excerpt'] = $this->clean_html( $a['post_excerpt'] );
827 if ( $a['post_name'] ?? '' ) $ins['post_name'] = sanitize_title( $a['post_name'] );
828 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) $ins['meta_input'] = $a['meta_input'];
829 $new = wp_insert_post( $ins, true );
830 if ( is_wp_error( $new ) ) {
831 $r['error'] = [ 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ];
832 } else {
833 if ( empty( $ins['meta_input'] ) && !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) )
834 foreach ( $a['meta_input'] as $k => $v ) update_post_meta( $new, sanitize_key( $k ), maybe_serialize( $v ) );
835 $this->add_result_text( $r, 'Post created ID '.$new );
836 }
837 break;
838
839 /* ===== Posts: update ===== */
840 case 'wp_update_post':
841 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
842 $c = [ 'ID' => intval( $a['ID'] ) ];
843 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) )
844 foreach ( $a['fields'] as $k => $v )
845 $c[ $k ] = in_array( $k, [ 'post_content','post_excerpt' ], true ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
846 $u = ( count( $c ) > 1 ) ? wp_update_post( $c, true ) : $c['ID'];
847 if ( is_wp_error( $u ) ) { $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ]; break; }
848 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) )
849 foreach ( $a['meta_input'] as $k => $v ) update_post_meta( $u, sanitize_key( $k ), maybe_serialize( $v ) );
850 $this->add_result_text( $r, 'Post #'.$u.' updated' );
851 break;
852
853 /* ===== Posts: delete ===== */
854 case 'wp_delete_post':
855 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
856 $del = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
857 if ( $del ) $this->add_result_text( $r, 'Post #'.$a['ID'].' deleted' );
858 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
859 break;
860
861 /* ===== Post-meta ===== */
862 case 'wp_get_post_meta':
863 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
864 $pid = intval( $a['ID'] );
865 $out = ( $a['key'] ?? '' ) ? get_post_meta( $pid, sanitize_key( $a['key'] ), true ) : get_post_meta( $pid );
866 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
867 break;
868
869 case 'wp_update_post_meta':
870 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
871 $pid = intval( $a['ID'] );
872 if ( !empty( $a['meta'] ) && is_array( $a['meta'] ) ) {
873 foreach ( $a['meta'] as $k => $v ) update_post_meta( $pid, sanitize_key( $k ), maybe_serialize( $v ) );
874 } elseif ( isset( $a['key'], $a['value'] ) ) {
875 update_post_meta( $pid, sanitize_key( $a['key'] ), maybe_serialize( $a['value'] ) );
876 } else { $r['error'] = [ 'code' => -32602, 'message' => 'meta array or key/value required' ]; break; }
877 $this->add_result_text( $r, 'Meta updated for post #'.$pid );
878 break;
879
880 case 'wp_delete_post_meta':
881 if ( empty( $a['ID'] ) || empty( $a['key'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID & key required' ]; break; }
882 $pid = intval( $a['ID'] );
883 $key = sanitize_key( $a['key'] );
884 $done = isset( $a['value'] ) ? delete_post_meta( $pid, $key, maybe_serialize( $a['value'] ) ) : delete_post_meta( $pid, $key );
885 if ( $done ) $this->add_result_text( $r, 'Meta deleted on post #'.$pid );
886 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
887 break;
888
889 /* ===== Featured image ===== */
890 case 'wp_set_featured_image':
891 if ( empty( $a['post_id'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'post_id required' ]; break; }
892 $post_id = intval( $a['post_id'] );
893 $media_id = isset( $a['media_id'] ) ? intval( $a['media_id'] ) : 0;
894 if ( $media_id ) {
895 $done = set_post_thumbnail( $post_id, $media_id );
896 if ( $done ) $this->add_result_text( $r, 'Featured image set on post #'.$post_id );
897 else $r['error'] = [ 'code' => -32603, 'message' => 'Failed to set thumbnail' ];
898 } else {
899 delete_post_thumbnail( $post_id );
900 $this->add_result_text( $r, 'Featured image removed from post #'.$post_id );
901 }
902 break;
903
904 /* ===== Taxonomies ===== */
905 case 'wp_get_taxonomies':
906 $pt = sanitize_key( $a['post_type'] ?? 'post' );
907 $out = [];
908 foreach ( get_object_taxonomies( $pt, 'objects' ) as $t )
909 $out[] = [ 'key' => $t->name, 'label' => $t->label ];
910 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
911 break;
912
913 case 'wp_get_terms':
914 $tax = sanitize_key( $a['taxonomy'] );
915 $args = [
916 'taxonomy' => $tax,
917 'hide_empty' => false,
918 'number' => intval( $a['limit'] ?? 0 ),
919 'search' => $a['search'] ?? '',
920 ];
921 if ( isset( $a['parent'] ) ) $args['parent'] = intval( $a['parent'] );
922 $out = [];
923 foreach ( get_terms( $args ) as $t )
924 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name, 'slug' => $t->slug, 'count' => $t->count ];
925 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
926 break;
927
928 case 'wp_create_term':
929 if ( empty( $a['term_name'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'term_name required' ]; break; }
930 $tax = sanitize_key( $a['taxonomy'] );
931 $args = [];
932 if ( $a['slug'] ?? '' ) $args['slug'] = sanitize_title( $a['slug'] );
933 if ( $a['description'] ?? '' ) $args['description'] = sanitize_text_field( $a['description'] );
934 if ( isset( $a['parent'] ) ) $args['parent'] = intval( $a['parent'] );
935 $term = wp_insert_term( sanitize_text_field( $a['term_name'] ), $tax, $args );
936 if ( is_wp_error( $term ) ) $r['error'] = [ 'code' => $term->get_error_code(), 'message' => $term->get_error_message() ];
937 else $this->add_result_text( $r, 'Term '.$term['term_id'].' created' );
938 break;
939
940 case 'wp_update_term':
941 $tid = intval( $a['term_id'] ?? 0 );
942 if ( !$tid ) { $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ]; break; }
943 $tax = sanitize_key( $a['taxonomy'] );
944 $uargs = [];
945 foreach ( [ 'name','slug','description','parent' ] as $f ) if ( isset( $a[$f] ) ) $uargs[$f] = $a[$f];
946 $t = wp_update_term( $tid, $tax, $uargs );
947 if ( is_wp_error( $t ) ) $r['error'] = [ 'code' => $t->get_error_code(), 'message' => $t->get_error_message() ];
948 else $this->add_result_text( $r, 'Term '.$tid.' updated' );
949 break;
950
951 case 'wp_delete_term':
952 $tid = intval( $a['term_id'] ?? 0 );
953 if ( !$tid ) { $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ]; break; }
954 $tax = sanitize_key( $a['taxonomy'] );
955 $d = wp_delete_term( $tid, $tax );
956 if ( $d ) $this->add_result_text( $r, 'Term '.$tid.' deleted' );
957 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
958 break;
959
960 case 'wp_get_post_terms':
961 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
962 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
963 $out = [];
964 foreach ( wp_get_post_terms( intval( $a['ID'] ), $tax, [ 'fields' => 'all' ] ) as $t )
965 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name ];
966 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
967 break;
968
969 case 'wp_add_post_terms':
970 if ( empty( $a['ID'] ) || empty( $a['terms'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID & terms required' ]; break; }
971 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
972 $append = !isset( $a['append'] ) || $a['append'];
973 $set = wp_set_post_terms( intval( $a['ID'] ), $a['terms'], $tax, $append );
974 if ( is_wp_error( $set ) ) $r['error'] = [ 'code' => $set->get_error_code(), 'message' => $set->get_error_message() ];
975 else $this->add_result_text( $r, 'Terms set for post #'.$a['ID'] );
976 break;
977
978 /* ===== Media: list ===== */
979 case 'wp_get_media':
980 $q = [
981 'post_type' => 'attachment',
982 's' => $a['search'] ?? '',
983 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
984 'post_status' => 'inherit',
985 ];
986 $d = [];
987 if ( $a['after'] ?? '' ) $d['after'] = $a['after'];
988 if ( $a['before'] ?? '' ) $d['before'] = $a['before'];
989 if ( $d ) $q['date_query'] = [ $d ];
990 $list = [];
991 foreach ( get_posts( $q ) as $m )
992 $list[] = [ 'ID' => $m->ID, 'title' => $m->post_title, 'url' => wp_get_attachment_url( $m->ID ) ];
993 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
994 break;
995
996 /* ===== Media: upload ===== */
997 case 'wp_upload_media':
998 if ( empty( $a['url'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'url required' ]; break; }
999 try {
1000 require_once ABSPATH.'wp-admin/includes/file.php';
1001 require_once ABSPATH.'wp-admin/includes/media.php';
1002 require_once ABSPATH.'wp-admin/includes/image.php';
1003 $tmp = download_url( $a['url'] );
1004 if ( is_wp_error( $tmp ) ) throw new Exception( $tmp->get_error_message(), $tmp->get_error_code() );
1005 $file = [ 'name' => basename( parse_url( $a['url'], PHP_URL_PATH ) ), 'tmp_name' => $tmp ];
1006 $id = media_handle_sideload( $file, 0, $a['description'] ?? '' );
1007 @unlink( $tmp );
1008 if ( is_wp_error( $id ) ) throw new Exception( $id->get_error_message(), $id->get_error_code() );
1009 if ( $a['title'] ?? '' ) wp_update_post( [ 'ID' => $id, 'post_title' => sanitize_text_field( $a['title'] ) ] );
1010 if ( $a['alt'] ?? '' ) update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
1011 $this->add_result_text( $r, wp_get_attachment_url( $id ) );
1012 } catch ( \Throwable $e ) {
1013 $r['error'] = [ 'code' => $e->getCode() ?: -32603, 'message' => $e->getMessage() ];
1014 }
1015 break;
1016
1017 /* ===== Media: update ===== */
1018 case 'wp_update_media':
1019 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
1020 $upd = [ 'ID' => intval( $a['ID'] ) ];
1021 if ( $a['title'] ?? '' ) $upd['post_title'] = sanitize_text_field( $a['title'] );
1022 if ( $a['caption'] ?? '' ) $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1023 if ( $a['description'] ?? '' ) $upd['post_content'] = $this->clean_html( $a['description'] );
1024 $u = wp_update_post( $upd, true );
1025 if ( is_wp_error( $u ) ) { $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ]; }
1026 else {
1027 if ( $a['alt'] ?? '' ) update_post_meta( $u, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
1028 $this->add_result_text( $r, 'Media #'.$u.' updated' );
1029 }
1030 break;
1031
1032 /* ===== Media: delete ===== */
1033 case 'wp_delete_media':
1034 if ( empty( $a['ID'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ]; break; }
1035 $d = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
1036 if ( $d ) $this->add_result_text( $r, 'Media #'.$a['ID'].' deleted' );
1037 else $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1038 break;
1039
1040 /* ===== MWAI Vision ===== */
1041 case 'mwai_vision':
1042 if ( empty( $a['message'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'message required' ]; break; }
1043 global $mwai;
1044 if ( !isset( $mwai ) ) { $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ]; break; }
1045 $analysis = $mwai->simpleVisionQuery(
1046 $a['message'],
1047 $a['url'] ?? null,
1048 $a['path'] ?? null,
1049 [ 'scope' => 'mcp' ]
1050 );
1051 $this->add_result_text( $r, is_string( $analysis ) ? $analysis : wp_json_encode( $analysis, JSON_PRETTY_PRINT ) );
1052 break;
1053
1054 /* ===== MWAI Image ===== */
1055 case 'mwai_image':
1056 if ( empty( $a['message'] ) ) { $r['error'] = [ 'code' => -32602, 'message' => 'message required' ]; break; }
1057 global $mwai;
1058 if ( !isset( $mwai ) ) { $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ]; break; }
1059
1060 $media = $mwai->imageQueryForMediaLibrary( $a['message'], [ 'scope' => 'mcp' ], $a['postId'] ?? null );
1061 if ( is_wp_error( $media ) ) {
1062 $r['error'] = [ 'code' => $media->get_error_code(), 'message' => $media->get_error_message() ];
1063 break;
1064 }
1065
1066 $mid = intval( $media['id'] );
1067
1068 $upd = [ 'ID' => $mid ];
1069 if ( !empty( $a['title'] ) ) $upd['post_title'] = sanitize_text_field( $a['title'] );
1070 if ( !empty( $a['caption'] ) ) $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1071 if ( !empty( $a['description'] ) ) $upd['post_content'] = $this->clean_html( $a['description'] );
1072 if ( count( $upd ) > 1 ) wp_update_post( $upd, true );
1073 if ( array_key_exists( 'alt', $a ) ) update_post_meta( $mid, '_wp_attachment_image_alt', sanitize_text_field( (string) $a['alt'] ) );
1074
1075 $media = [
1076 'id' => $mid,
1077 'url' => wp_get_attachment_url( $mid ),
1078 'title' => get_the_title( $mid ),
1079 'caption' => wp_get_attachment_caption( $mid ),
1080 'alt' => get_post_meta( $mid, '_wp_attachment_image_alt', true ),
1081 ];
1082 $this->add_result_text( $r, wp_json_encode( $media, JSON_PRETTY_PRINT ) );
1083 break;
1084
1085 /* ===== Ping ===== */
1086 // Note: mcp_ping is now handled by the base MCP class
1087
1088 /* ===== OpenAI Deep Research Tools ===== */
1089 case 'search':
1090 if ( empty( $a['query'] ) ) {
1091 $r['error'] = [ 'code' => -32602, 'message' => 'query required' ];
1092 break;
1093 }
1094
1095 $query = sanitize_text_field( $a['query'] );
1096
1097 // Search in posts and pages
1098 $args = [
1099 's' => $query,
1100 'post_type' => [ 'post', 'page' ],
1101 'post_status' => 'publish',
1102 'posts_per_page' => 20,
1103 'orderby' => 'relevance',
1104 'order' => 'DESC',
1105 ];
1106
1107 $search_query = new WP_Query( $args );
1108 $results = [];
1109
1110 if ( $search_query->have_posts() ) {
1111 while ( $search_query->have_posts() ) {
1112 $search_query->the_post();
1113 $post = get_post();
1114
1115 // Create result matching OpenAI's expected format
1116 $results[] = [
1117 'id' => (string) $post->ID,
1118 'title' => get_the_title(),
1119 'text' => wp_trim_words( wp_strip_all_tags( $post->post_content ), 100 ),
1120 'url' => get_permalink(),
1121 ];
1122 }
1123 wp_reset_postdata();
1124 }
1125
1126 // Return results in OpenAI's expected format
1127 // We need to return the raw result structure for OpenAI
1128 return [
1129 'jsonrpc' => '2.0',
1130 'id' => $id,
1131 'result' => [ 'results' => $results ],
1132 ];
1133
1134 case 'fetch':
1135 if ( empty( $a['id'] ) ) {
1136 $r['error'] = [ 'code' => -32602, 'message' => 'id required' ];
1137 break;
1138 }
1139
1140 $post_id = intval( $a['id'] );
1141 $post = get_post( $post_id );
1142
1143 if ( ! $post || $post->post_status !== 'publish' ) {
1144 $r['error'] = [ 'code' => -32603, 'message' => 'Resource not found or not published' ];
1145 break;
1146 }
1147
1148 // Get full content with proper formatting
1149 $content = apply_filters( 'the_content', $post->post_content );
1150 $content = wp_strip_all_tags( $content );
1151
1152 // Get metadata
1153 $metadata = [
1154 'author' => get_the_author_meta( 'display_name', $post->post_author ),
1155 'date' => get_the_date( 'Y-m-d', $post ),
1156 'modified' => get_the_modified_date( 'Y-m-d', $post ),
1157 'type' => $post->post_type,
1158 ];
1159
1160 // Add categories if it's a post
1161 if ( $post->post_type === 'post' ) {
1162 $categories = wp_get_post_categories( $post_id, [ 'fields' => 'names' ] );
1163 if ( ! empty( $categories ) ) {
1164 $metadata['categories'] = implode( ', ', $categories );
1165 }
1166
1167 $tags = wp_get_post_tags( $post_id, [ 'fields' => 'names' ] );
1168 if ( ! empty( $tags ) ) {
1169 $metadata['tags'] = implode( ', ', $tags );
1170 }
1171 }
1172
1173 // Return in OpenAI's expected format
1174 // We need to return the raw result structure for OpenAI
1175 return [
1176 'jsonrpc' => '2.0',
1177 'id' => $id,
1178 'result' => [
1179 'id' => (string) $post_id,
1180 'title' => get_the_title( $post ),
1181 'text' => $content,
1182 'url' => get_permalink( $post ),
1183 'metadata' => $metadata,
1184 ],
1185 ];
1186
1187 default: $r['error'] = [ 'code' => -32601, 'message' => 'Unknown tool' ];
1188 }
1189 return $r;
1190 }
1191 #endregion
1192 }
1193 ?>
1194