PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.2
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / labs / mcp-core.php
ai-engine / labs Last commit date
mcp-core.php 1 year ago mcp-rest.php 1 year ago mcp.conf 1 year ago mcp.js 1 year ago mcp.md 1 year ago mcp.php 1 year ago oauth.php 1 year ago realtime.php 1 year ago
mcp-core.php
1437 lines
1 <?php
2
3 class Meow_MWAI_Labs_MCP_Core {
4 private $core = null;
5
6 #region Initialize
7 public function __construct( $core ) {
8 $this->core = $core;
9 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
10 }
11 public function rest_api_init() {
12 add_filter( 'mwai_mcp_tools', [ $this, 'register_rest_tools' ] );
13 add_filter( 'mwai_mcp_callback', [ $this, 'handle_call' ], 10, 4 );
14 }
15 #endregion
16
17 #region Helpers
18 private function add_result_text( array &$r, string $text ): void {
19 if ( !isset( $r['result']['content'] ) ) {
20 $r['result']['content'] = [];
21 }
22 $r['result']['content'][] = [ 'type' => 'text', 'text' => $text ];
23 }
24 private function clean_html( string $v ): string {
25 return wp_kses_post( wp_unslash( $v ) );
26 }
27 private function post_excerpt( WP_Post $p ): string {
28 return wp_trim_words( wp_strip_all_tags( $p->post_excerpt ?: $p->post_content ), 55 );
29 }
30 private function empty_schema(): array {
31 return [ 'type' => 'object', 'properties' => (object) [] ];
32 }
33 #endregion
34
35 #region Tools Definitions
36 private function tools(): array {
37 return [
38
39 /* -------- Plugins -------- */
40 'wp_list_plugins' => [
41 'name' => 'wp_list_plugins',
42 'description' => 'List installed plugins (returns array of {Name, Version}).',
43 'inputSchema' => [
44 'type' => 'object',
45 'properties' => [ 'search' => [ 'type' => 'string' ] ],
46 ],
47 ],
48
49 /* -------- Users -------- */
50 'wp_get_users' => [
51 'name' => 'wp_get_users',
52 'description' => 'Retrieve users (fields: ID, user_login, display_name, roles). If no limit supplied, returns 10. `paged` ignored if `offset` is used.',
53 'inputSchema' => [
54 'type' => 'object',
55 'properties' => [
56 'search' => [ 'type' => 'string' ],
57 'role' => [ 'type' => 'string' ],
58 'limit' => [ 'type' => 'integer' ],
59 'offset' => [ 'type' => 'integer' ],
60 'paged' => [ 'type' => 'integer' ],
61 ],
62 ],
63 ],
64 'wp_create_user' => [
65 'name' => 'wp_create_user',
66 'description' => 'Create a user. Requires user_login and user_email. Optional: user_pass (random if omitted), display_name, role.',
67 'inputSchema' => [
68 'type' => 'object',
69 'properties' => [
70 'user_login' => [ 'type' => 'string' ],
71 'user_email' => [ 'type' => 'string' ],
72 'user_pass' => [ 'type' => 'string' ],
73 'display_name' => [ 'type' => 'string' ],
74 'role' => [ 'type' => 'string' ],
75 ],
76 'required' => [ 'user_login', 'user_email' ],
77 ],
78 ],
79 'wp_update_user' => [
80 'name' => 'wp_update_user',
81 'description' => 'Update a user – pass ID plus a “fields” object (user_email, display_name, user_pass, role).',
82 'inputSchema' => [
83 'type' => 'object',
84 'properties' => [
85 'ID' => [ 'type' => 'integer' ],
86 'fields' => [
87 'type' => 'object',
88 'properties' => [
89 'user_email' => [ 'type' => 'string' ],
90 'display_name' => [ 'type' => 'string' ],
91 'user_pass' => [ 'type' => 'string' ],
92 'role' => [ 'type' => 'string' ],
93 ],
94 'additionalProperties' => true
95 ],
96 ],
97 'required' => [ 'ID' ],
98 ],
99 ],
100
101 /* -------- Comments -------- */
102 'wp_get_comments' => [
103 'name' => 'wp_get_comments',
104 'description' => 'Retrieve comments (fields: comment_ID, comment_post_ID, comment_author, comment_content, comment_date, comment_approved). Returns 10 by default.',
105 'inputSchema' => [
106 'type' => 'object',
107 'properties' => [
108 'post_id' => [ 'type' => 'integer' ],
109 'status' => [ 'type' => 'string' ],
110 'search' => [ 'type' => 'string' ],
111 'limit' => [ 'type' => 'integer' ],
112 'offset' => [ 'type' => 'integer' ],
113 'paged' => [ 'type' => 'integer' ],
114 ],
115 ],
116 ],
117 'wp_create_comment' => [
118 'name' => 'wp_create_comment',
119 'description' => 'Insert a comment. Requires post_id and comment_content. Optional author, author_email, author_url.',
120 'inputSchema' => [
121 'type' => 'object',
122 'properties' => [
123 'post_id' => [ 'type' => 'integer' ],
124 'comment_content' => [ 'type' => 'string' ],
125 'comment_author' => [ 'type' => 'string' ],
126 'comment_author_email' => [ 'type' => 'string' ],
127 'comment_author_url' => [ 'type' => 'string' ],
128 'comment_approved' => [ 'type' => 'string' ],
129 ],
130 'required' => [ 'post_id', 'comment_content' ],
131 ],
132 ],
133 'wp_update_comment' => [
134 'name' => 'wp_update_comment',
135 'description' => 'Update a comment – pass comment_ID plus fields (comment_content, comment_approved).',
136 'inputSchema' => [
137 'type' => 'object',
138 'properties' => [
139 'comment_ID' => [ 'type' => 'integer' ],
140 'fields' => [
141 'type' => 'object',
142 'properties' => [
143 'comment_content' => [ 'type' => 'string' ],
144 'comment_approved' => [ 'type' => 'string' ],
145 ],
146 'additionalProperties' => true
147 ],
148 ],
149 'required' => [ 'comment_ID' ],
150 ],
151 ],
152 'wp_delete_comment' => [
153 'name' => 'wp_delete_comment',
154 'description' => 'Delete a comment. `force` true bypasses trash.',
155 'inputSchema' => [
156 'type' => 'object',
157 'properties' => [
158 'comment_ID' => [ 'type' => 'integer' ],
159 'force' => [ 'type' => 'boolean' ],
160 ],
161 'required' => [ 'comment_ID' ],
162 ],
163 ],
164
165 /* -------- Options -------- */
166 'wp_get_option' => [
167 'name' => 'wp_get_option',
168 'description' => 'Get a single WordPress option value (scalar or array) by key.',
169 'inputSchema' => [
170 'type' => 'object',
171 'properties' => [ 'key' => [ 'type' => 'string' ] ],
172 'required' => [ 'key' ],
173 ],
174 ],
175 'wp_update_option' => [
176 'name' => 'wp_update_option',
177 'description' => 'Create or update a WordPress option (JSON-serialised if necessary).',
178 'inputSchema' => [
179 'type' => 'object',
180 'properties' => [
181 'key' => [ 'type' => 'string' ],
182 'value' => [ 'type' => [ 'string', 'number', 'boolean', 'object', 'array' ] ],
183 ],
184 'required' => [ 'key', 'value' ],
185 ],
186 ],
187
188 /* -------- Counts -------- */
189 'wp_count_posts' => [
190 'name' => 'wp_count_posts',
191 'description' => 'Return counts of posts by status. Optional post_type (default post).',
192 'inputSchema' => [
193 'type' => 'object',
194 'properties' => [ 'post_type' => [ 'type' => 'string' ] ],
195 ],
196 ],
197 'wp_count_terms' => [
198 'name' => 'wp_count_terms',
199 'description' => 'Return total number of terms in a taxonomy.',
200 'inputSchema' => [
201 'type' => 'object',
202 'properties' => [ 'taxonomy' => [ 'type' => 'string' ] ],
203 'required' => [ 'taxonomy' ],
204 ],
205 ],
206 'wp_count_media' => [
207 'name' => 'wp_count_media',
208 'description' => 'Return number of attachments (optionally after/before date).',
209 'inputSchema' => [
210 'type' => 'object',
211 'properties' => [
212 'after' => [ 'type' => 'string' ],
213 'before' => [ 'type' => 'string' ],
214 ],
215 ],
216 ],
217
218 /* -------- Post-types -------- */
219 'wp_get_post_types' => [
220 'name' => 'wp_get_post_types',
221 'description' => 'List public post types (key, label).',
222 'inputSchema' => $this->empty_schema(),
223 ],
224
225 /* -------- Posts -------- */
226 'wp_get_posts' => [
227 'name' => 'wp_get_posts',
228 'description' => 'Retrieve posts (fields: ID, title, status, excerpt, link). No full content. **If no limit is supplied it returns 10 posts by default.** `paged` is ignored if `offset` is used.',
229 'inputSchema' => [
230 'type' => 'object',
231 'properties' => [
232 'post_type' => [ 'type' => 'string' ],
233 'post_status' => [ 'type' => 'string' ],
234 'search' => [ 'type' => 'string' ],
235 'after' => [ 'type' => 'string' ],
236 'before' => [ 'type' => 'string' ],
237 'limit' => [ 'type' => 'integer' ],
238 'offset' => [ 'type' => 'integer' ],
239 'paged' => [ 'type' => 'integer' ],
240 ],
241 ],
242 ],
243 'wp_get_post' => [
244 'name' => 'wp_get_post',
245 'description' => 'Get a single post by ID (all fields inc. full content).',
246 'inputSchema' => [
247 'type' => 'object',
248 'properties' => [ 'ID' => [ 'type' => 'integer' ] ],
249 'required' => [ 'ID' ],
250 ],
251 ],
252 'wp_create_post' => [
253 'name' => 'wp_create_post',
254 'description' => 'Create a post or page – post_title required; Markdown accepted in post_content; defaults to draft post_status and post post_type; set categories later with wp_add_post_terms; meta_input is an associative array of custom-field key/value pairs.',
255 'inputSchema' => [
256 'type' => 'object',
257 'properties' => [
258 'post_title' => [ 'type' => 'string' ],
259 'post_content' => [ 'type' => 'string' ],
260 'post_excerpt' => [ 'type' => 'string' ],
261 'post_status' => [ 'type' => 'string' ],
262 'post_type' => [ 'type' => 'string' ],
263 'post_name' => [ 'type' => 'string' ],
264 'meta_input' => [ 'type' => 'object', 'description' => 'Associative array of custom fields.' ],
265 ],
266 'required' => [ 'post_title' ],
267 ],
268 ],
269 'wp_update_post' => [
270 'name' => 'wp_update_post',
271 'description' => 'Update a post – pass ID plus a “fields” object containing any post fields to update; meta_input adds/updates custom fields. post_category (array of term IDs) REPLACES existing categories; use wp_add_post_terms to append.',
272 'inputSchema' => [
273 'type' => 'object',
274 'properties' => [
275 'ID' => [ 'type' => 'integer', 'description' => 'The ID of the post to update.' ],
276 'fields' => [
277 'type' => 'object',
278 'properties' => [
279 'post_title' => [ 'type' => 'string' ],
280 'post_content' => [ 'type' => 'string' ],
281 'post_status' => [ 'type' => 'string' ],
282 'post_name' => [ 'type' => 'string' ],
283 'post_excerpt' => [ 'type' => 'string' ],
284 'post_category' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
285 ],
286 'additionalProperties' => true
287 ],
288 'meta_input' => [
289 'type' => 'object',
290 'description' => 'Associative array of custom fields.'
291 ],
292 ],
293 'required' => [ 'ID' ],
294 ],
295 ],
296 'wp_delete_post' => [
297 'name' => 'wp_delete_post',
298 'description' => 'Delete/trash a post.',
299 'inputSchema' => [
300 'type' => 'object',
301 'properties' => [
302 'ID' => [ 'type' => 'integer' ],
303 'force' => [ 'type' => 'boolean' ],
304 ],
305 'required' => [ 'ID' ],
306 ],
307 ],
308
309 /* -------- Post-meta -------- */
310 'wp_get_post_meta' => [
311 'name' => 'wp_get_post_meta',
312 'description' => 'Retrieve post meta. Provide "key" to fetch a single value; omit to fetch all custom fields.',
313 'inputSchema' => [
314 'type' => 'object',
315 'properties' => [
316 'ID' => [ 'type' => 'integer' ],
317 'key' => [ 'type' => 'string' ],
318 ],
319 'required' => [ 'ID' ],
320 ],
321 ],
322 'wp_update_post_meta' => [
323 'name' => 'wp_update_post_meta',
324 'description' => 'Create or update one or more custom fields for a post.',
325 'inputSchema' => [
326 'type' => 'object',
327 'properties' => [
328 'ID' => [ 'type' => 'integer' ],
329 'meta' => [ 'type' => 'object', 'description' => 'Key/value pairs to set. Alternative: provide "key" + "value".' ],
330 'key' => [ 'type' => 'string' ],
331 'value' => [ 'type' => [ 'string', 'number', 'boolean' ] ],
332 ],
333 'required' => [ 'ID' ],
334 ],
335 ],
336 'wp_delete_post_meta' => [
337 'name' => 'wp_delete_post_meta',
338 'description' => 'Delete custom field(s) from a post. Provide value to remove a single row; omit value to delete all rows for the key.',
339 'inputSchema' => [
340 'type' => 'object',
341 'properties' => [
342 'ID' => [ 'type' => 'integer' ],
343 'key' => [ 'type' => 'string' ],
344 'value' => [ 'type' => [ 'string', 'number', 'boolean' ] ],
345 ],
346 'required' => [ 'ID', 'key' ],
347 ],
348 ],
349
350 /* -------- Featured image -------- */
351 'wp_set_featured_image' => [
352 'name' => 'wp_set_featured_image',
353 'description' => 'Attach or remove a featured image (thumbnail) for a post/page. Provide media_id to attach, omit or null to remove.',
354 'inputSchema' => [
355 'type' => 'object',
356 'properties' => [
357 'post_id' => [ 'type' => 'integer' ],
358 'media_id' => [ 'type' => 'integer' ],
359 ],
360 'required' => [ 'post_id' ],
361 ],
362 ],
363
364 /* -------- Taxonomies / Terms -------- */
365 'wp_get_taxonomies' => [
366 'name' => 'wp_get_taxonomies',
367 'description' => 'List taxonomies for a post type.',
368 'inputSchema' => [
369 'type' => 'object',
370 'properties' => [ 'post_type' => [ 'type' => 'string' ] ],
371 ],
372 ],
373 'wp_get_terms' => [
374 'name' => 'wp_get_terms',
375 'description' => 'List terms of a taxonomy.',
376 'inputSchema' => [
377 'type' => 'object',
378 'properties' => [
379 'taxonomy' => [ 'type' => 'string' ],
380 'search' => [ 'type' => 'string' ],
381 'parent' => [ 'type' => 'integer' ],
382 'limit' => [ 'type' => 'integer' ],
383 ],
384 'required' => [ 'taxonomy' ],
385 ],
386 ],
387 'wp_create_term' => [
388 'name' => 'wp_create_term',
389 'description' => 'Create a term.',
390 'inputSchema' => [
391 'type' => 'object',
392 'properties' => [
393 'taxonomy' => [ 'type' => 'string' ],
394 'term_name' => [ 'type' => 'string' ],
395 'slug' => [ 'type' => 'string' ],
396 'description' => [ 'type' => 'string' ],
397 'parent' => [ 'type' => 'integer' ],
398 ],
399 'required' => [ 'taxonomy', 'term_name' ],
400 ],
401 ],
402 'wp_update_term' => [
403 'name' => 'wp_update_term',
404 'description' => 'Update a term.',
405 'inputSchema' => [
406 'type' => 'object',
407 'properties' => [
408 'term_id' => [ 'type' => 'integer' ],
409 'taxonomy' => [ 'type' => 'string' ],
410 'name' => [ 'type' => 'string' ],
411 'slug' => [ 'type' => 'string' ],
412 'description' => [ 'type' => 'string' ],
413 'parent' => [ 'type' => 'integer' ],
414 ],
415 'required' => [ 'term_id', 'taxonomy' ],
416 ],
417 ],
418 'wp_delete_term' => [
419 'name' => 'wp_delete_term',
420 'description' => 'Delete a term.',
421 'inputSchema' => [
422 'type' => 'object',
423 'properties' => [
424 'term_id' => [ 'type' => 'integer' ],
425 'taxonomy' => [ 'type' => 'string' ],
426 ],
427 'required' => [ 'term_id', 'taxonomy' ],
428 ],
429 ],
430 'wp_get_post_terms' => [
431 'name' => 'wp_get_post_terms',
432 'description' => 'Get terms attached to a post.',
433 'inputSchema' => [
434 'type' => 'object',
435 'properties' => [
436 'ID' => [ 'type' => 'integer' ],
437 'taxonomy' => [ 'type' => 'string' ],
438 ],
439 'required' => [ 'ID' ],
440 ],
441 ],
442 'wp_add_post_terms' => [
443 'name' => 'wp_add_post_terms',
444 'description' => 'Attach terms to a post.',
445 'inputSchema' => [
446 'type' => 'object',
447 'properties' => [
448 'ID' => [ 'type' => 'integer' ],
449 'taxonomy' => [ 'type' => 'string' ],
450 'terms' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
451 'append' => [ 'type' => 'boolean' ],
452 ],
453 'required' => [ 'ID', 'terms' ],
454 ],
455 ],
456
457 /* -------- Media -------- */
458 'wp_get_media' => [
459 'name' => 'wp_get_media',
460 'description' => 'List media items.',
461 'inputSchema' => [
462 'type' => 'object',
463 'properties' => [
464 'search' => [ 'type' => 'string' ],
465 'after' => [ 'type' => 'string' ],
466 'before' => [ 'type' => 'string' ],
467 'limit' => [ 'type' => 'integer' ],
468 ],
469 ],
470 ],
471 'wp_upload_media' => [
472 'name' => 'wp_upload_media',
473 'description' => 'Download file from URL and add to Media Library.',
474 'inputSchema' => [
475 'type' => 'object',
476 'properties' => [
477 'url' => [ 'type' => 'string' ],
478 'title' => [ 'type' => 'string' ],
479 'description' => [ 'type' => 'string' ],
480 'alt' => [ 'type' => 'string' ],
481 ],
482 'required' => [ 'url' ],
483 ],
484 ],
485 'wp_update_media' => [
486 'name' => 'wp_update_media',
487 'description' => 'Update attachment meta.',
488 'inputSchema' => [
489 'type' => 'object',
490 'properties' => [
491 'ID' => [ 'type' => 'integer' ],
492 'title' => [ 'type' => 'string' ],
493 'caption' => [ 'type' => 'string' ],
494 'description' => [ 'type' => 'string' ],
495 'alt' => [ 'type' => 'string' ],
496 ],
497 'required' => [ 'ID' ],
498 ],
499 ],
500 'wp_delete_media' => [
501 'name' => 'wp_delete_media',
502 'description' => 'Delete/trash an attachment.',
503 'inputSchema' => [
504 'type' => 'object',
505 'properties' => [
506 'ID' => [ 'type' => 'integer' ],
507 'force' => [ 'type' => 'boolean' ],
508 ],
509 'required' => [ 'ID' ],
510 ],
511 ],
512
513 /* -------- MWAI Vision / Image -------- */
514 'mwai_vision' => [
515 'name' => 'mwai_vision',
516 'description' => 'Analyze an image via AI Engine Vision.',
517 'inputSchema' => [
518 'type' => 'object',
519 'properties' => [
520 'message' => [ 'type' => 'string' ],
521 'url' => [ 'type' => 'string' ],
522 'path' => [ 'type' => 'string' ],
523 ],
524 'required' => [ 'message' ],
525 ],
526 ],
527 'mwai_image' => [
528 'name' => 'mwai_image',
529 'description' => 'Generate an image with AI Engine and store it in the Media Library. Optional: title, caption, description, alt. Returns { id, url, title, caption, alt }.',
530 'inputSchema' => [
531 'type' => 'object',
532 'properties' => [
533 'message' => [ 'type' => 'string', 'description' => 'Prompt describing the desired image.' ],
534 'postId' => [ 'type' => 'integer', 'description' => 'Optional post ID to attach the image to.' ],
535 'title' => [ 'type' => 'string' ],
536 'caption' => [ 'type' => 'string' ],
537 'description' => [ 'type' => 'string' ],
538 'alt' => [ 'type' => 'string' ],
539 ],
540 'required' => [ 'message' ],
541 ],
542 ],
543
544 /* -------- Tools -------- */
545 // Note: mcp_ping is now handled by the base MCP class
546
547 /* -------- OpenAI Deep Research Tools -------- */
548 'search' => [
549 'name' => 'search',
550 '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.',
551 'inputSchema' => [
552 'type' => 'object',
553 'properties' => [
554 'query' => [ 'type' => 'string', 'description' => 'Search query to find relevant posts and pages. Can be keywords, phrases, or topics.' ],
555 ],
556 'required' => [ 'query' ],
557 ],
558 ],
559
560 'fetch' => [
561 'name' => 'fetch',
562 '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.',
563 'inputSchema' => [
564 'type' => 'object',
565 'properties' => [
566 'id' => [ 'type' => 'string', 'description' => 'The WordPress post ID obtained from search results.' ],
567 ],
568 'required' => [ 'id' ],
569 ],
570 ],
571 ];
572 }
573 #endregion
574
575 #region Tool Registration
576 public function register_rest_tools( array $prev ): array {
577 $tools = $this->tools();
578 // Add category to each tool
579 foreach ( $tools as &$tool ) {
580 if ( !isset( $tool['category'] ) ) {
581 // Set Core: OpenAI category for search and fetch tools
582 if ( in_array( $tool['name'], ['search', 'fetch'] ) ) {
583 $tool['category'] = 'Core: OpenAI';
584 }
585 else {
586 $tool['category'] = 'Core';
587 }
588 }
589 }
590 return array_merge( $prev, array_values( $tools ) );
591 }
592 #endregion
593
594 #region Callback
595 public function handle_call( $prev, string $tool, array $args, int $id ) {
596 // Security check is already done in the MCP auth layer
597 // If we reach here, the user is authorized to use MCP
598 if ( !empty( $prev ) || !isset( $this->tools()[ $tool ] ) ) {
599 return $prev;
600 }
601 return $this->dispatch( $tool, $args, $id );
602 }
603 #endregion
604
605 #region Dispatcher
606 private function dispatch( string $tool, array $a, int $id ): array {
607 $r = [ 'jsonrpc' => '2.0', 'id' => $id ];
608
609 switch ( $tool ) {
610
611 /* ===== Users ===== */
612 case 'wp_get_users':
613 $q = [
614 'search' => '*' . esc_attr( $a['search'] ?? '' ) . '*',
615 'role' => $a['role'] ?? '',
616 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
617 ];
618 if ( isset( $a['offset'] ) ) {
619 $q['offset'] = max( 0, intval( $a['offset'] ) );
620 }
621 if ( isset( $a['paged'] ) ) {
622 $q['paged'] = max( 1, intval( $a['paged'] ) );
623 }
624 $rows = [];
625 foreach ( get_users( $q ) as $u ) {
626 $rows[] = [
627 'ID' => $u->ID,
628 'user_login' => $u->user_login,
629 'display_name' => $u->display_name,
630 'roles' => $u->roles,
631 ];
632 }
633 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
634 break;
635
636 case 'wp_create_user':
637 $data = [
638 'user_login' => sanitize_user( $a['user_login'] ),
639 'user_email' => sanitize_email( $a['user_email'] ),
640 'user_pass' => $a['user_pass'] ?? wp_generate_password( 12, true ),
641 'display_name' => sanitize_text_field( $a['display_name'] ?? '' ),
642 'role' => sanitize_key( $a['role'] ?? get_option( 'default_role', 'subscriber' ) ),
643 ];
644 $uid = wp_insert_user( $data );
645 if ( is_wp_error( $uid ) ) {
646 $r['error'] = [ 'code' => $uid->get_error_code(), 'message' => $uid->get_error_message() ];
647 }
648 else {
649 $this->add_result_text( $r, 'User created ID ' . $uid );
650 }
651 break;
652
653 case 'wp_update_user':
654 if ( empty( $a['ID'] ) ) {
655 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
656 break;
657 }
658 $upd = [ 'ID' => intval( $a['ID'] ) ];
659 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) ) {
660 foreach ( $a['fields'] as $k => $v ) {
661 $upd[ $k ] = ( $k === 'role' ) ? sanitize_key( $v ) : sanitize_text_field( $v );
662 }
663 }
664 $u = wp_update_user( $upd );
665 if ( is_wp_error( $u ) ) {
666 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
667 }
668 else {
669 $this->add_result_text( $r, 'User #' . $u . ' updated' );
670 }
671 break;
672
673 /* ===== Comments ===== */
674 case 'wp_get_comments':
675 $args = [
676 'post_id' => isset( $a['post_id'] ) ? intval( $a['post_id'] ) : '',
677 'status' => $a['status'] ?? 'approve',
678 'search' => $a['search'] ?? '',
679 'number' => max( 1, intval( $a['limit'] ?? 10 ) ),
680 ];
681 if ( isset( $a['offset'] ) ) {
682 $args['offset'] = max( 0, intval( $a['offset'] ) );
683 }
684 if ( isset( $a['paged'] ) ) {
685 $args['paged'] = max( 1, intval( $a['paged'] ) );
686 }
687 $list = [];
688 foreach ( get_comments( $args ) as $c ) {
689 $list[] = [
690 'comment_ID' => $c->comment_ID,
691 'comment_post_ID' => $c->comment_post_ID,
692 'comment_author' => $c->comment_author,
693 'comment_content' => wp_trim_words( wp_strip_all_tags( $c->comment_content ), 40 ),
694 'comment_date' => $c->comment_date,
695 'comment_approved' => $c->comment_approved,
696 ];
697 }
698 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
699 break;
700
701 case 'wp_create_comment':
702 if ( empty( $a['post_id'] ) || empty( $a['comment_content'] ) ) {
703 $r['error'] = [ 'code' => -32602, 'message' => 'post_id & comment_content required' ];
704 break;
705 }
706 $ins = [
707 'comment_post_ID' => intval( $a['post_id'] ),
708 'comment_content' => $this->clean_html( $a['comment_content'] ),
709 'comment_author' => sanitize_text_field( $a['comment_author'] ?? '' ),
710 'comment_author_email' => sanitize_email( $a['comment_author_email'] ?? '' ),
711 'comment_author_url' => esc_url_raw( $a['comment_author_url'] ?? '' ),
712 'comment_approved' => $a['comment_approved'] ?? 1,
713 ];
714 $cid = wp_insert_comment( $ins );
715 if ( is_wp_error( $cid ) ) {
716 /** @var WP_Error $cid */
717 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
718 }
719 else {
720 $this->add_result_text( $r, 'Comment created ID ' . $cid );
721 }
722 break;
723
724 case 'wp_update_comment':
725 if ( empty( $a['comment_ID'] ) ) {
726 $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ];
727 break;
728 }
729 $c = [ 'comment_ID' => intval( $a['comment_ID'] ) ];
730 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) ) {
731 foreach ( $a['fields'] as $k => $v ) {
732 $c[ $k ] = ( $k === 'comment_content' ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
733 }
734 }
735 $cid = wp_update_comment( $c, true );
736 if ( is_wp_error( $cid ) ) {
737 $r['error'] = [ 'code' => $cid->get_error_code(), 'message' => $cid->get_error_message() ];
738 }
739 else {
740 $this->add_result_text( $r, 'Comment #' . $cid . ' updated' );
741 }
742 break;
743
744 case 'wp_delete_comment':
745 if ( empty( $a['comment_ID'] ) ) {
746 $r['error'] = [ 'code' => -32602, 'message' => 'comment_ID required' ];
747 break;
748 }
749 $done = wp_delete_comment( intval( $a['comment_ID'] ), !empty( $a['force'] ) );
750 if ( $done ) {
751 $this->add_result_text( $r, 'Comment #' . $a['comment_ID'] . ' deleted' );
752 }
753 else {
754 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
755 }
756 break;
757
758 /* ===== Options ===== */
759 case 'wp_get_option':
760 $val = get_option( sanitize_key( $a['key'] ) );
761 $this->add_result_text( $r, wp_json_encode( $val, JSON_PRETTY_PRINT ) );
762 break;
763
764 case 'wp_update_option':
765 $set = update_option( sanitize_key( $a['key'] ), $a['value'], 'yes' );
766 if ( $set ) {
767 $this->add_result_text( $r, 'Option "' . $a['key'] . '" updated' );
768 }
769 else {
770 $r['error'] = [ 'code' => -32603, 'message' => 'Update failed' ];
771 }
772 break;
773
774 /* ===== Counts ===== */
775 case 'wp_count_posts':
776 $pt = sanitize_key( $a['post_type'] ?? 'post' );
777 $obj = wp_count_posts( $pt );
778 $this->add_result_text( $r, wp_json_encode( $obj, JSON_PRETTY_PRINT ) );
779 break;
780
781 case 'wp_count_terms':
782 $tax = sanitize_key( $a['taxonomy'] );
783 $total = wp_count_terms( $tax, [ 'hide_empty' => false ] );
784 if ( is_wp_error( $total ) ) {
785 $r['error'] = [ 'code' => $total->get_error_code(), 'message' => $total->get_error_message() ];
786 }
787 else {
788 $this->add_result_text( $r, (string) $total );
789 }
790 break;
791
792 case 'wp_count_media':
793 $args = [ 'post_type' => 'attachment', 'post_status' => 'inherit', 'fields' => 'ids' ];
794 $d = [];
795 if ( $a['after'] ?? '' ) {
796 $d['after'] = $a['after'];
797 }
798 if ( $a['before'] ?? '' ) {
799 $d['before'] = $a['before'];
800 }
801 if ( $d ) {
802 $args['date_query'] = [ $d ];
803 }
804 $total = count( get_posts( $args ) );
805 $this->add_result_text( $r, (string) $total );
806 break;
807
808 /* ===== Post-types ===== */
809 case 'wp_get_post_types':
810 $out = [];
811 foreach ( get_post_types( [ 'public' => true ], 'objects' ) as $pt ) {
812 $out[] = [ 'key' => $pt->name, 'label' => $pt->label ];
813 }
814 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
815 break;
816
817 /* ===== Plugins ===== */
818 case 'wp_list_plugins':
819 if ( !function_exists( 'get_plugins' ) ) {
820 require_once ABSPATH . 'wp-admin/includes/plugin.php';
821 }
822 $search = sanitize_text_field( $a['search'] ?? '' );
823 $out = [];
824 foreach ( get_plugins() as $p ) {
825 if ( !$search || stripos( $p['Name'], $search ) !== false ) {
826 $out[] = [ 'Name' => $p['Name'], 'Version' => $p['Version'] ];
827 }
828 }
829 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
830 break;
831
832 /* ===== Posts: list ===== */
833 case 'wp_get_posts':
834 $q = [
835 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
836 'post_status' => sanitize_key( $a['post_status'] ?? 'publish' ),
837 's' => sanitize_text_field( $a['search'] ?? '' ),
838 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
839 ];
840 if ( isset( $a['offset'] ) ) {
841 $q['offset'] = max( 0, intval( $a['offset'] ) );
842 }
843 if ( isset( $a['paged'] ) ) {
844 $q['paged'] = max( 1, intval( $a['paged'] ) );
845 }
846 $date = [];
847 if ( $a['after'] ?? '' ) {
848 $date['after'] = $a['after'];
849 }
850 if ( $a['before'] ?? '' ) {
851 $date['before'] = $a['before'];
852 }
853 if ( $date ) {
854 $q['date_query'] = [ $date ];
855 }
856 $rows = [];
857 foreach ( get_posts( $q ) as $p ) {
858 $rows[] = [
859 'ID' => $p->ID,
860 'post_title' => $p->post_title,
861 'post_status' => $p->post_status,
862 'post_excerpt' => $this->post_excerpt( $p ),
863 'permalink' => get_permalink( $p ),
864 ];
865 }
866 $this->add_result_text( $r, wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
867 break;
868
869 /* ===== Posts: single ===== */
870 case 'wp_get_post':
871 if ( empty( $a['ID'] ) ) {
872 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
873 break;
874 }
875 $p = get_post( intval( $a['ID'] ) );
876 if ( !$p ) {
877 $r['error'] = [ 'code' => -32602, 'message' => 'Post not found' ];
878 break;
879 }
880 $out = [
881 'ID' => $p->ID,
882 'post_title' => $p->post_title,
883 'post_status' => $p->post_status,
884 'post_content' => $this->clean_html( $p->post_content ),
885 'post_excerpt' => $this->post_excerpt( $p ),
886 'permalink' => get_permalink( $p ),
887 'post_date' => $p->post_date,
888 'post_modified' => $p->post_modified,
889 ];
890 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
891 break;
892
893 /* ===== Posts: create ===== */
894 case 'wp_create_post':
895 if ( empty( $a['post_title'] ) ) {
896 $r['error'] = [ 'code' => -32602, 'message' => 'post_title required' ];
897 break;
898 }
899 $ins = [
900 'post_title' => sanitize_text_field( $a['post_title'] ),
901 'post_status' => sanitize_key( $a['post_status'] ?? 'draft' ),
902 'post_type' => sanitize_key( $a['post_type'] ?? 'post' ),
903 ];
904 if ( $a['post_content'] ?? '' ) {
905 $ins['post_content'] = $this->core->markdown_to_html( $a['post_content'] );
906 }
907 if ( $a['post_excerpt'] ?? '' ) {
908 $ins['post_excerpt'] = $this->clean_html( $a['post_excerpt'] );
909 }
910 if ( $a['post_name'] ?? '' ) {
911 $ins['post_name'] = sanitize_title( $a['post_name'] );
912 }
913 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) {
914 $ins['meta_input'] = $a['meta_input'];
915 }
916 $new = wp_insert_post( $ins, true );
917 if ( is_wp_error( $new ) ) {
918 $r['error'] = [ 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ];
919 }
920 else {
921 if ( empty( $ins['meta_input'] ) && !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) {
922 foreach ( $a['meta_input'] as $k => $v ) {
923 update_post_meta( $new, sanitize_key( $k ), maybe_serialize( $v ) );
924 }
925 }
926 $this->add_result_text( $r, 'Post created ID ' . $new );
927 }
928 break;
929
930 /* ===== Posts: update ===== */
931 case 'wp_update_post':
932 if ( empty( $a['ID'] ) ) {
933 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
934 break;
935 }
936 $c = [ 'ID' => intval( $a['ID'] ) ];
937 if ( !empty( $a['fields'] ) && is_array( $a['fields'] ) ) {
938 foreach ( $a['fields'] as $k => $v ) {
939 $c[ $k ] = in_array( $k, [ 'post_content', 'post_excerpt' ], true ) ? $this->clean_html( $v ) : sanitize_text_field( $v );
940 }
941 }
942 $u = ( count( $c ) > 1 ) ? wp_update_post( $c, true ) : $c['ID'];
943 if ( is_wp_error( $u ) ) {
944 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
945 break;
946 }
947 if ( !empty( $a['meta_input'] ) && is_array( $a['meta_input'] ) ) {
948 foreach ( $a['meta_input'] as $k => $v ) {
949 update_post_meta( $u, sanitize_key( $k ), maybe_serialize( $v ) );
950 }
951 }
952 $this->add_result_text( $r, 'Post #' . $u . ' updated' );
953 break;
954
955 /* ===== Posts: delete ===== */
956 case 'wp_delete_post':
957 if ( empty( $a['ID'] ) ) {
958 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
959 break;
960 }
961 $del = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
962 if ( $del ) {
963 $this->add_result_text( $r, 'Post #' . $a['ID'] . ' deleted' );
964 }
965 else {
966 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
967 }
968 break;
969
970 /* ===== Post-meta ===== */
971 case 'wp_get_post_meta':
972 if ( empty( $a['ID'] ) ) {
973 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
974 break;
975 }
976 $pid = intval( $a['ID'] );
977 $out = ( $a['key'] ?? '' ) ? get_post_meta( $pid, sanitize_key( $a['key'] ), true ) : get_post_meta( $pid );
978 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
979 break;
980
981 case 'wp_update_post_meta':
982 if ( empty( $a['ID'] ) ) {
983 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
984 break;
985 }
986 $pid = intval( $a['ID'] );
987 if ( !empty( $a['meta'] ) && is_array( $a['meta'] ) ) {
988 foreach ( $a['meta'] as $k => $v ) {
989 update_post_meta( $pid, sanitize_key( $k ), maybe_serialize( $v ) );
990 }
991 }
992 elseif ( isset( $a['key'], $a['value'] ) ) {
993 update_post_meta( $pid, sanitize_key( $a['key'] ), maybe_serialize( $a['value'] ) );
994 }
995 else {
996 $r['error'] = [ 'code' => -32602, 'message' => 'meta array or key/value required' ];
997 break;
998 }
999 $this->add_result_text( $r, 'Meta updated for post #' . $pid );
1000 break;
1001
1002 case 'wp_delete_post_meta':
1003 if ( empty( $a['ID'] ) || empty( $a['key'] ) ) {
1004 $r['error'] = [ 'code' => -32602, 'message' => 'ID & key required' ];
1005 break;
1006 }
1007 $pid = intval( $a['ID'] );
1008 $key = sanitize_key( $a['key'] );
1009 $done = isset( $a['value'] ) ? delete_post_meta( $pid, $key, maybe_serialize( $a['value'] ) ) : delete_post_meta( $pid, $key );
1010 if ( $done ) {
1011 $this->add_result_text( $r, 'Meta deleted on post #' . $pid );
1012 }
1013 else {
1014 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1015 }
1016 break;
1017
1018 /* ===== Featured image ===== */
1019 case 'wp_set_featured_image':
1020 if ( empty( $a['post_id'] ) ) {
1021 $r['error'] = [ 'code' => -32602, 'message' => 'post_id required' ];
1022 break;
1023 }
1024 $post_id = intval( $a['post_id'] );
1025 $media_id = isset( $a['media_id'] ) ? intval( $a['media_id'] ) : 0;
1026 if ( $media_id ) {
1027 $done = set_post_thumbnail( $post_id, $media_id );
1028 if ( $done ) {
1029 $this->add_result_text( $r, 'Featured image set on post #' . $post_id );
1030 }
1031 else {
1032 $r['error'] = [ 'code' => -32603, 'message' => 'Failed to set thumbnail' ];
1033 }
1034 }
1035 else {
1036 delete_post_thumbnail( $post_id );
1037 $this->add_result_text( $r, 'Featured image removed from post #' . $post_id );
1038 }
1039 break;
1040
1041 /* ===== Taxonomies ===== */
1042 case 'wp_get_taxonomies':
1043 $pt = sanitize_key( $a['post_type'] ?? 'post' );
1044 $out = [];
1045 foreach ( get_object_taxonomies( $pt, 'objects' ) as $t ) {
1046 $out[] = [ 'key' => $t->name, 'label' => $t->label ];
1047 }
1048 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1049 break;
1050
1051 case 'wp_get_terms':
1052 $tax = sanitize_key( $a['taxonomy'] );
1053 $args = [
1054 'taxonomy' => $tax,
1055 'hide_empty' => false,
1056 'number' => intval( $a['limit'] ?? 0 ),
1057 'search' => $a['search'] ?? '',
1058 ];
1059 if ( isset( $a['parent'] ) ) {
1060 $args['parent'] = intval( $a['parent'] );
1061 }
1062 $out = [];
1063 foreach ( get_terms( $args ) as $t ) {
1064 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name, 'slug' => $t->slug, 'count' => $t->count ];
1065 }
1066 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1067 break;
1068
1069 case 'wp_create_term':
1070 if ( empty( $a['term_name'] ) ) {
1071 $r['error'] = [ 'code' => -32602, 'message' => 'term_name required' ];
1072 break;
1073 }
1074 $tax = sanitize_key( $a['taxonomy'] );
1075 $args = [];
1076 if ( $a['slug'] ?? '' ) {
1077 $args['slug'] = sanitize_title( $a['slug'] );
1078 }
1079 if ( $a['description'] ?? '' ) {
1080 $args['description'] = sanitize_text_field( $a['description'] );
1081 }
1082 if ( isset( $a['parent'] ) ) {
1083 $args['parent'] = intval( $a['parent'] );
1084 }
1085 $term = wp_insert_term( sanitize_text_field( $a['term_name'] ), $tax, $args );
1086 if ( is_wp_error( $term ) ) {
1087 $r['error'] = [ 'code' => $term->get_error_code(), 'message' => $term->get_error_message() ];
1088 }
1089 else {
1090 $this->add_result_text( $r, 'Term ' . $term['term_id'] . ' created' );
1091 }
1092 break;
1093
1094 case 'wp_update_term':
1095 $tid = intval( $a['term_id'] ?? 0 );
1096 if ( !$tid ) {
1097 $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ];
1098 break;
1099 }
1100 $tax = sanitize_key( $a['taxonomy'] );
1101 $uargs = [];
1102 foreach ( [ 'name', 'slug', 'description', 'parent' ] as $f ) {
1103 if ( isset( $a[$f] ) ) {
1104 $uargs[$f] = $a[$f];
1105 }
1106 }
1107 $t = wp_update_term( $tid, $tax, $uargs );
1108 if ( is_wp_error( $t ) ) {
1109 $r['error'] = [ 'code' => $t->get_error_code(), 'message' => $t->get_error_message() ];
1110 }
1111 else {
1112 $this->add_result_text( $r, 'Term ' . $tid . ' updated' );
1113 }
1114 break;
1115
1116 case 'wp_delete_term':
1117 $tid = intval( $a['term_id'] ?? 0 );
1118 if ( !$tid ) {
1119 $r['error'] = [ 'code' => -32602, 'message' => 'term_id required' ];
1120 break;
1121 }
1122 $tax = sanitize_key( $a['taxonomy'] );
1123 $d = wp_delete_term( $tid, $tax );
1124 if ( $d ) {
1125 $this->add_result_text( $r, 'Term ' . $tid . ' deleted' );
1126 }
1127 else {
1128 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1129 }
1130 break;
1131
1132 case 'wp_get_post_terms':
1133 if ( empty( $a['ID'] ) ) {
1134 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1135 break;
1136 }
1137 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
1138 $out = [];
1139 foreach ( wp_get_post_terms( intval( $a['ID'] ), $tax, [ 'fields' => 'all' ] ) as $t ) {
1140 $out[] = [ 'term_id' => $t->term_id, 'name' => $t->name ];
1141 }
1142 $this->add_result_text( $r, wp_json_encode( $out, JSON_PRETTY_PRINT ) );
1143 break;
1144
1145 case 'wp_add_post_terms':
1146 if ( empty( $a['ID'] ) || empty( $a['terms'] ) ) {
1147 $r['error'] = [ 'code' => -32602, 'message' => 'ID & terms required' ];
1148 break;
1149 }
1150 $tax = sanitize_key( $a['taxonomy'] ?? 'category' );
1151 $append = !isset( $a['append'] ) || $a['append'];
1152 $set = wp_set_post_terms( intval( $a['ID'] ), $a['terms'], $tax, $append );
1153 if ( is_wp_error( $set ) ) {
1154 $r['error'] = [ 'code' => $set->get_error_code(), 'message' => $set->get_error_message() ];
1155 }
1156 else {
1157 $this->add_result_text( $r, 'Terms set for post #' . $a['ID'] );
1158 }
1159 break;
1160
1161 /* ===== Media: list ===== */
1162 case 'wp_get_media':
1163 $q = [
1164 'post_type' => 'attachment',
1165 's' => $a['search'] ?? '',
1166 'posts_per_page' => max( 1, intval( $a['limit'] ?? 10 ) ),
1167 'post_status' => 'inherit',
1168 ];
1169 $d = [];
1170 if ( $a['after'] ?? '' ) {
1171 $d['after'] = $a['after'];
1172 }
1173 if ( $a['before'] ?? '' ) {
1174 $d['before'] = $a['before'];
1175 }
1176 if ( $d ) {
1177 $q['date_query'] = [ $d ];
1178 }
1179 $list = [];
1180 foreach ( get_posts( $q ) as $m ) {
1181 $list[] = [ 'ID' => $m->ID, 'title' => $m->post_title, 'url' => wp_get_attachment_url( $m->ID ) ];
1182 }
1183 $this->add_result_text( $r, wp_json_encode( $list, JSON_PRETTY_PRINT ) );
1184 break;
1185
1186 /* ===== Media: upload ===== */
1187 case 'wp_upload_media':
1188 if ( empty( $a['url'] ) ) {
1189 $r['error'] = [ 'code' => -32602, 'message' => 'url required' ];
1190 break;
1191 }
1192 try {
1193 require_once ABSPATH . 'wp-admin/includes/file.php';
1194 require_once ABSPATH . 'wp-admin/includes/media.php';
1195 require_once ABSPATH . 'wp-admin/includes/image.php';
1196 $tmp = download_url( $a['url'] );
1197 if ( is_wp_error( $tmp ) ) {
1198 throw new Exception( $tmp->get_error_message(), $tmp->get_error_code() );
1199 }
1200 $file = [ 'name' => basename( parse_url( $a['url'], PHP_URL_PATH ) ), 'tmp_name' => $tmp ];
1201 $id = media_handle_sideload( $file, 0, $a['description'] ?? '' );
1202 @unlink( $tmp );
1203 if ( is_wp_error( $id ) ) {
1204 throw new Exception( $id->get_error_message(), $id->get_error_code() );
1205 }
1206 if ( $a['title'] ?? '' ) {
1207 wp_update_post( [ 'ID' => $id, 'post_title' => sanitize_text_field( $a['title'] ) ] );
1208 }
1209 if ( $a['alt'] ?? '' ) {
1210 update_post_meta( $id, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
1211 }
1212 $this->add_result_text( $r, wp_get_attachment_url( $id ) );
1213 }
1214 catch ( \Throwable $e ) {
1215 $r['error'] = [ 'code' => $e->getCode() ?: -32603, 'message' => $e->getMessage() ];
1216 }
1217 break;
1218
1219 /* ===== Media: update ===== */
1220 case 'wp_update_media':
1221 if ( empty( $a['ID'] ) ) {
1222 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1223 break;
1224 }
1225 $upd = [ 'ID' => intval( $a['ID'] ) ];
1226 if ( $a['title'] ?? '' ) {
1227 $upd['post_title'] = sanitize_text_field( $a['title'] );
1228 }
1229 if ( $a['caption'] ?? '' ) {
1230 $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1231 }
1232 if ( $a['description'] ?? '' ) {
1233 $upd['post_content'] = $this->clean_html( $a['description'] );
1234 }
1235 $u = wp_update_post( $upd, true );
1236 if ( is_wp_error( $u ) ) {
1237 $r['error'] = [ 'code' => $u->get_error_code(), 'message' => $u->get_error_message() ];
1238 }
1239 else {
1240 if ( $a['alt'] ?? '' ) {
1241 update_post_meta( $u, '_wp_attachment_image_alt', sanitize_text_field( $a['alt'] ) );
1242 }
1243 $this->add_result_text( $r, 'Media #' . $u . ' updated' );
1244 }
1245 break;
1246
1247 /* ===== Media: delete ===== */
1248 case 'wp_delete_media':
1249 if ( empty( $a['ID'] ) ) {
1250 $r['error'] = [ 'code' => -32602, 'message' => 'ID required' ];
1251 break;
1252 }
1253 $d = wp_delete_post( intval( $a['ID'] ), !empty( $a['force'] ) );
1254 if ( $d ) {
1255 $this->add_result_text( $r, 'Media #' . $a['ID'] . ' deleted' );
1256 }
1257 else {
1258 $r['error'] = [ 'code' => -32603, 'message' => 'Deletion failed' ];
1259 }
1260 break;
1261
1262 /* ===== MWAI Vision ===== */
1263 case 'mwai_vision':
1264 if ( empty( $a['message'] ) ) {
1265 $r['error'] = [ 'code' => -32602, 'message' => 'message required' ];
1266 break;
1267 }
1268 global $mwai;
1269 if ( !isset( $mwai ) ) {
1270 $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ];
1271 break;
1272 }
1273 $analysis = $mwai->simpleVisionQuery(
1274 $a['message'],
1275 $a['url'] ?? null,
1276 $a['path'] ?? null,
1277 [ 'scope' => 'mcp' ]
1278 );
1279 $this->add_result_text( $r, is_string( $analysis ) ? $analysis : wp_json_encode( $analysis, JSON_PRETTY_PRINT ) );
1280 break;
1281
1282 /* ===== MWAI Image ===== */
1283 case 'mwai_image':
1284 if ( empty( $a['message'] ) ) {
1285 $r['error'] = [ 'code' => -32602, 'message' => 'message required' ];
1286 break;
1287 }
1288 global $mwai;
1289 if ( !isset( $mwai ) ) {
1290 $r['error'] = [ 'code' => -32603, 'message' => 'MWAI not found' ];
1291 break;
1292 }
1293
1294 $media = $mwai->imageQueryForMediaLibrary( $a['message'], [ 'scope' => 'mcp' ], $a['postId'] ?? null );
1295 if ( is_wp_error( $media ) ) {
1296 $r['error'] = [ 'code' => $media->get_error_code(), 'message' => $media->get_error_message() ];
1297 break;
1298 }
1299
1300 $mid = intval( $media['id'] );
1301
1302 $upd = [ 'ID' => $mid ];
1303 if ( !empty( $a['title'] ) ) {
1304 $upd['post_title'] = sanitize_text_field( $a['title'] );
1305 }
1306 if ( !empty( $a['caption'] ) ) {
1307 $upd['post_excerpt'] = $this->clean_html( $a['caption'] );
1308 }
1309 if ( !empty( $a['description'] ) ) {
1310 $upd['post_content'] = $this->clean_html( $a['description'] );
1311 }
1312 if ( count( $upd ) > 1 ) {
1313 wp_update_post( $upd, true );
1314 }
1315 if ( array_key_exists( 'alt', $a ) ) {
1316 update_post_meta( $mid, '_wp_attachment_image_alt', sanitize_text_field( (string) $a['alt'] ) );
1317 }
1318
1319 $media = [
1320 'id' => $mid,
1321 'url' => wp_get_attachment_url( $mid ),
1322 'title' => get_the_title( $mid ),
1323 'caption' => wp_get_attachment_caption( $mid ),
1324 'alt' => get_post_meta( $mid, '_wp_attachment_image_alt', true ),
1325 ];
1326 $this->add_result_text( $r, wp_json_encode( $media, JSON_PRETTY_PRINT ) );
1327 break;
1328
1329 /* ===== Ping ===== */
1330 // Note: mcp_ping is now handled by the base MCP class
1331
1332 /* ===== OpenAI Deep Research Tools ===== */
1333 case 'search':
1334 if ( empty( $a['query'] ) ) {
1335 $r['error'] = [ 'code' => -32602, 'message' => 'query required' ];
1336 break;
1337 }
1338
1339 $query = sanitize_text_field( $a['query'] );
1340
1341 // Search in posts and pages
1342 $args = [
1343 's' => $query,
1344 'post_type' => [ 'post', 'page' ],
1345 'post_status' => 'publish',
1346 'posts_per_page' => 20,
1347 'orderby' => 'relevance',
1348 'order' => 'DESC',
1349 ];
1350
1351 $search_query = new WP_Query( $args );
1352 $results = [];
1353
1354 if ( $search_query->have_posts() ) {
1355 while ( $search_query->have_posts() ) {
1356 $search_query->the_post();
1357 $post = get_post();
1358
1359 // Create result matching OpenAI's expected format
1360 $results[] = [
1361 'id' => (string) $post->ID,
1362 'title' => get_the_title(),
1363 'text' => wp_trim_words( wp_strip_all_tags( $post->post_content ), 100 ),
1364 'url' => get_permalink(),
1365 ];
1366 }
1367 wp_reset_postdata();
1368 }
1369
1370 // Return results in OpenAI's expected format
1371 // We need to return the raw result structure for OpenAI
1372 return [
1373 'jsonrpc' => '2.0',
1374 'id' => $id,
1375 'result' => [ 'results' => $results ],
1376 ];
1377
1378 case 'fetch':
1379 if ( empty( $a['id'] ) ) {
1380 $r['error'] = [ 'code' => -32602, 'message' => 'id required' ];
1381 break;
1382 }
1383
1384 $post_id = intval( $a['id'] );
1385 $post = get_post( $post_id );
1386
1387 if ( !$post || $post->post_status !== 'publish' ) {
1388 $r['error'] = [ 'code' => -32603, 'message' => 'Resource not found or not published' ];
1389 break;
1390 }
1391
1392 // Get full content with proper formatting
1393 $content = apply_filters( 'the_content', $post->post_content );
1394 $content = wp_strip_all_tags( $content );
1395
1396 // Get metadata
1397 $metadata = [
1398 'author' => get_the_author_meta( 'display_name', $post->post_author ),
1399 'date' => get_the_date( 'Y-m-d', $post ),
1400 'modified' => get_the_modified_date( 'Y-m-d', $post ),
1401 'type' => $post->post_type,
1402 ];
1403
1404 // Add categories if it's a post
1405 if ( $post->post_type === 'post' ) {
1406 $categories = wp_get_post_categories( $post_id, [ 'fields' => 'names' ] );
1407 if ( !empty( $categories ) ) {
1408 $metadata['categories'] = implode( ', ', $categories );
1409 }
1410
1411 $tags = wp_get_post_tags( $post_id, [ 'fields' => 'names' ] );
1412 if ( !empty( $tags ) ) {
1413 $metadata['tags'] = implode( ', ', $tags );
1414 }
1415 }
1416
1417 // Return in OpenAI's expected format
1418 // We need to return the raw result structure for OpenAI
1419 return [
1420 'jsonrpc' => '2.0',
1421 'id' => $id,
1422 'result' => [
1423 'id' => (string) $post_id,
1424 'title' => get_the_title( $post ),
1425 'text' => $content,
1426 'url' => get_permalink( $post ),
1427 'metadata' => $metadata,
1428 ],
1429 ];
1430
1431 default: $r['error'] = [ 'code' => -32601, 'message' => 'Unknown tool' ];
1432 }
1433 return $r;
1434 }
1435 #endregion
1436 }
1437