PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 All 128 releases
extendify / app / Mcp / Tools.php

Tools.php in Extendify 3.2.2, at app/Mcp/Tools.php

1,274 lines 57.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The tools this plugin writes by hand, and what each one accepts.
5 */
6
7 namespace Extendify\Mcp;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 /**
12 * Descriptions and schemas stay English: the model reads them, the user never
13 * does, and MCP carries no locale to translate them into.
14 */
15 class Tools
16 {
17 /**
18 * Read on every tools/list, so it is a PHP array rather than parsed JSON.
19 *
20 * @return array
21 */
22 public static function all()
23 {
24 return [
25 'list_posts' => self::listPosts(),
26 'get_post' => self::getPost(),
27 'update_posts_metadata' => self::updatePostsMetadata(),
28 'search_site' => self::searchSite(),
29 'list_comments' => self::listComments(),
30 'set_comment_status' => self::setCommentStatus(),
31 'reply_to_comment' => self::replyToComment(),
32 'list_terms' => self::listTerms(),
33 'create_term' => self::createTerm(),
34 'set_post_terms' => self::setPostTerms(),
35 'list_media' => self::listMedia(),
36 'add_media_from_url' => self::addMediaFromUrl(),
37 'update_alt_texts' => self::updateAltTexts(),
38 'regenerate_thumbnails' => self::regenerateThumbnails(),
39 'list_plugins' => self::listPlugins(),
40 'install_plugin' => self::installPlugin(),
41 'set_plugin_status' => self::setPluginStatus(),
42 'update_plugins' => self::updatePlugins(),
43 'set_auto_updates' => self::setAutoUpdates(),
44 'delete_inactive_plugins' => self::deleteInactivePlugins(),
45 'list_themes' => self::listThemes(),
46 'update_themes' => self::updateThemes(),
47 'delete_inactive_themes' => self::deleteInactiveThemes(),
48 'list_users' => self::listUsers(),
49 'create_user' => self::createUser(),
50 'delete_users' => self::deleteUsers(),
51 'get_site_info' => self::getSiteInfo(),
52 'update_site_settings' => self::updateSiteSettings(),
53 'get_site_health' => self::getSiteHealth(),
54 'update_core' => self::updateCore(),
55 'get_task_status' => self::getTaskStatus(),
56 'request_feature' => self::requestFeature(),
57 ];
58 }
59
60 /**
61 * The editor's own types are show_in_rest without being content anyone asks for.
62 *
63 * @return array
64 */
65 public static function types()
66 {
67 $types = \get_post_types(['public' => true, 'show_in_rest' => true], 'names');
68
69 return array_values(array_diff($types, ['attachment']));
70 }
71
72 /**
73 * A role that manages the site is left out, so a model cannot name one and be refused.
74 *
75 * @return array - The roles create_user may give someone.
76 */
77 public static function roles()
78 {
79 $offered = [];
80 foreach (array_keys(\wp_roles()->get_names()) as $role) {
81 $granted = \get_role($role);
82 if ($granted && !$granted->has_cap('manage_options')) {
83 $offered[] = $role;
84 }
85 }
86
87 return $offered;
88 }
89
90 /**
91 * @return array - The taxonomies a term tool may name.
92 */
93 public static function taxonomies()
94 {
95 return array_values(\get_taxonomies(['public' => true, 'show_in_rest' => true], 'names'));
96 }
97
98 /**
99 * @return array
100 */
101 private static function listPosts()
102 {
103 return [
104 'mode' => Grants::READ,
105 'handler' => 'listPosts',
106 'description' => 'Lists posts, pages, or any other content type on this site - titles, status, dates,'
107 . ' author and links, but not the full body. Use it to find content before reading or changing it.'
108 . " Set type to 'page' for pages, or to a custom type reported by get_site_info. To read a post's"
109 . ' actual content, call get_post with an id from these results, and for its comments call'
110 . ' list_comments with that id.',
111 'inputSchema' => [
112 'type' => 'object',
113 'properties' => [
114 'type' => [
115 'type' => 'string',
116 'enum' => self::types(),
117 'default' => 'post',
118 'description' => "Content type to list. 'post' for blog posts, 'page' for pages."
119 . ' Custom types vary by site and are reported by get_site_info.',
120 ],
121 'search' => [
122 'type' => 'string',
123 'description' => 'Only return items whose title or content matches this text.',
124 ],
125 'status' => [
126 'type' => 'string',
127 'enum' => ['publish', 'draft', 'pending', 'private', 'future', 'trash', 'any'],
128 'default' => 'publish',
129 'description' => 'Only return items with this status.',
130 ],
131 'author' => [
132 'type' => 'integer',
133 'description' => 'Only return items written by this user id.',
134 ],
135 'after' => [
136 'type' => 'string',
137 'format' => 'date-time',
138 'description' => 'Only items published after this ISO 8601 date.',
139 ],
140 'before' => [
141 'type' => 'string',
142 'format' => 'date-time',
143 'description' => 'Only items published before this ISO 8601 date.',
144 ],
145 'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 20],
146 'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1],
147 ],
148 ],
149 ];
150 }
151
152 /**
153 * @return array
154 */
155 private static function getPost()
156 {
157 return [
158 'mode' => Grants::READ,
159 'handler' => 'getPost',
160 'description' => 'Reads one post or page in full, including its block markup. Use it after list_posts'
161 . ' or search_site has given you an id. The block markup is what you need to understand how a page'
162 . ' is built before suggesting changes to it.',
163 'inputSchema' => [
164 'type' => 'object',
165 'properties' => [
166 'id' => [
167 'type' => 'integer',
168 'description' => 'The post id, from list_posts or search_site.',
169 ],
170 'type' => [
171 'type' => 'string',
172 'enum' => self::types(),
173 'default' => 'post',
174 'description' => 'Content type the id belongs to.',
175 ],
176 ],
177 'required' => ['id'],
178 ],
179 ];
180 }
181
182 /**
183 * @return array
184 */
185 private static function updatePostsMetadata()
186 {
187 return [
188 'mode' => Grants::WRITE,
189 'handler' => 'updatePostsMetadata',
190 'description' => 'Changes fields on posts and pages, one or many in a single call: title, status,'
191 . ' excerpt, slug, publication date, and any custom field the site has registered for its own API.'
192 . ' Use it for the bulk work a person would otherwise click through - publishing a batch of drafts,'
193 . ' trashing an old series, rewriting excerpts. Earlier wording stays in each post revision'
194 . ' history, so text is recoverable, but a changed slug breaks every link to the old address and a'
195 . ' date moved into the future unpublishes the post until then. List the exact changes for the user'
196 . ' before calling. Most SEO plugins do not register their fields for the API, so their titles and'
197 . ' descriptions cannot be set here.',
198 'inputSchema' => [
199 'type' => 'object',
200 'properties' => [
201 'items' => [
202 'type' => 'array',
203 'minItems' => 1,
204 'maxItems' => 50,
205 'description' => 'One entry per post, each naming the fields to change on it.',
206 'items' => [
207 'type' => 'object',
208 'properties' => [
209 'id' => ['type' => 'integer', 'description' => 'The post id.'],
210 'type' => [
211 'type' => 'string',
212 'enum' => self::types(),
213 'default' => 'post',
214 'description' => 'The content type of that post.',
215 ],
216 'title' => ['type' => 'string', 'description' => 'New title.'],
217 'status' => [
218 'type' => 'string',
219 'enum' => ['publish', 'draft', 'pending', 'private', 'future', 'trash'],
220 'description' => "New status. 'future' needs a date to go live on.",
221 ],
222 'excerpt' => ['type' => 'string', 'description' => 'New excerpt.'],
223 'slug' => [
224 'type' => 'string',
225 'description' => 'New slug. Changes the address visitors and search engines'
226 . ' already have.',
227 ],
228 'date' => [
229 'type' => 'string',
230 'format' => 'date-time',
231 'description' => 'New publication date, ISO 8601.',
232 ],
233 'meta' => [
234 'type' => 'object',
235 'description' => 'Custom fields to set, by name. Only fields the site has'
236 . ' registered for its own API are accepted, and never footnotes, which'
237 . ' are part of the content; anything else is refused.',
238 ],
239 ],
240 'required' => ['id'],
241 ],
242 ],
243 ],
244 'required' => ['items'],
245 ],
246 ];
247 }
248
249 /**
250 * @return array
251 */
252 private static function searchSite()
253 {
254 return [
255 'mode' => Grants::READ,
256 'handler' => 'searchSite',
257 'description' => 'Searches every content type at once - posts, pages, media and custom types -'
258 . ' returning id, title, type and URL for each match. Use it when you know roughly what the user'
259 . ' means but not which content type it lives in. When you already know the type, list_posts gives'
260 . ' you more precise filtering.',
261 'inputSchema' => [
262 'type' => 'object',
263 'properties' => [
264 'query' => ['type' => 'string', 'description' => 'What to search for.'],
265 'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 20],
266 'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1],
267 ],
268 'required' => ['query'],
269 ],
270 ];
271 }
272
273 /**
274 * @return array
275 */
276 private static function listComments()
277 {
278 return [
279 'mode' => Grants::READ,
280 'handler' => 'listComments',
281 'description' => 'Lists comments with the commenter\'s name, the date, the post they are on, their'
282 . ' moderation state and their text. Email and IP addresses are deliberately excluded. Call it'
283 . ' before set_comment_status so you can show the user exactly what would change. Pending comments'
284 . ' are the ones waiting on a decision, and spam arrives in bursts, often on one post. The default'
285 . ' status covers approved and pending only - ask for spam or trash by name.',
286 'inputSchema' => [
287 'type' => 'object',
288 'properties' => [
289 'status' => [
290 'type' => 'string',
291 'enum' => ['any', 'approved', 'pending', 'spam', 'trash'],
292 'default' => 'any',
293 'description' => "Moderation state. 'any' covers approved and pending, not spam or trash.",
294 ],
295 'post' => ['type' => 'integer', 'description' => 'Only comments on this post id.'],
296 'search' => ['type' => 'string', 'description' => 'Match against the comment text.'],
297 'after' => [
298 'type' => 'string',
299 'format' => 'date-time',
300 'description' => 'Only comments written after this ISO 8601 date.',
301 ],
302 'before' => [
303 'type' => 'string',
304 'format' => 'date-time',
305 'description' => 'Only comments written before this ISO 8601 date.',
306 ],
307 'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 20],
308 'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1],
309 ],
310 ],
311 ];
312 }
313
314 /**
315 * @return array
316 */
317 private static function setCommentStatus()
318 {
319 return [
320 'mode' => Grants::WRITE,
321 'handler' => 'setCommentStatus',
322 'description' => 'Approves comments, sends them back to pending, marks them as spam, or moves them to'
323 . ' the trash. Each of those is reversible by calling again with another value, and a trashed'
324 . ' comment stays recoverable until the site owner empties the trash. Ids are the ones list_comments'
325 . ' reports. Show the user the comments first: marking a real person as spam is worse than leaving'
326 . ' one pending, since it teaches the filter to catch them again.',
327 'inputSchema' => [
328 'type' => 'object',
329 'properties' => [
330 'ids' => [
331 'type' => 'array',
332 'items' => ['type' => 'integer'],
333 'maxItems' => 100,
334 'description' => 'Comment ids to change, as list_comments reports them.',
335 ],
336 'status' => [
337 'type' => 'string',
338 'enum' => ['approved', 'pending', 'spam', 'trash'],
339 'description' => 'The state to put them in.',
340 ],
341 ],
342 'required' => ['ids', 'status'],
343 ],
344 ];
345 }
346
347 /**
348 * @return array
349 */
350 private static function replyToComment()
351 {
352 return [
353 'mode' => Grants::WRITE,
354 'handler' => 'replyToComment',
355 'description' => 'Replies to a comment, on the same post, as the account this connection belongs to.'
356 . ' The reply is published straight away and is visible to everyone reading that post, so show the'
357 . ' user the exact wording and let them agree before sending it. Write the reply in the language'
358 . ' of the comment it answers.',
359 'inputSchema' => [
360 'type' => 'object',
361 'properties' => [
362 'comment' => [
363 'type' => 'integer',
364 'description' => 'The comment id being replied to, as list_comments reports it.',
365 ],
366 'content' => ['type' => 'string', 'description' => 'The reply text.'],
367 ],
368 'required' => ['comment', 'content'],
369 ],
370 ];
371 }
372
373 /**
374 * @return array
375 */
376 private static function listTerms()
377 {
378 return [
379 'mode' => Grants::READ,
380 'handler' => 'listTerms',
381 'description' => 'Lists the categories, tags or other taxonomy terms on this site, with how many posts'
382 . ' each one holds and which term it sits under. Call it before set_post_terms so you use ids that'
383 . ' exist, and to spot the near-duplicates a site collects over time - a category with one post in'
384 . ' it is usually a typo of another one.',
385 'inputSchema' => [
386 'type' => 'object',
387 'properties' => [
388 'taxonomy' => [
389 'type' => 'string',
390 'enum' => self::taxonomies(),
391 'default' => 'category',
392 'description' => 'Which set of terms to list.',
393 ],
394 'search' => ['type' => 'string', 'description' => 'Match against the term name.'],
395 'post' => ['type' => 'integer', 'description' => 'Only terms assigned to this post id.'],
396 'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 50],
397 'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1],
398 ],
399 ],
400 ];
401 }
402
403 /**
404 * @return array
405 */
406 private static function createTerm()
407 {
408 return [
409 'mode' => Grants::WRITE,
410 'handler' => 'createTerm',
411 'description' => 'Creates a category, tag or other taxonomy term. Check list_terms first: a site that'
412 . ' already has a close match wants that one, not a second one beside it. A term with no posts on'
413 . ' it changes nothing a visitor sees until set_post_terms assigns it.',
414 'inputSchema' => [
415 'type' => 'object',
416 'properties' => [
417 'taxonomy' => [
418 'type' => 'string',
419 'enum' => self::taxonomies(),
420 'default' => 'category',
421 'description' => 'Which set of terms to add to.',
422 ],
423 'name' => ['type' => 'string', 'description' => 'The term name, as a visitor would read it.'],
424 'parent' => [
425 'type' => 'integer',
426 'description' => 'Sit it under this term id. Only taxonomies that nest accept one.',
427 ],
428 'description' => [
429 'type' => 'string',
430 'description' => 'Optional text some themes show on the term archive.',
431 ],
432 ],
433 'required' => ['name'],
434 ],
435 ];
436 }
437
438 /**
439 * @return array
440 */
441 private static function setPostTerms()
442 {
443 return [
444 'mode' => Grants::WRITE,
445 'handler' => 'setPostTerms',
446 'description' => "Sets which terms of one taxonomy a post carries. 'replace' is the default and drops"
447 . " every term of that taxonomy the post currently has, so read the post's terms with list_terms"
448 . " before using it; 'add' keeps what is there. Terms may be named by id or by name, and a name"
449 . ' that no term matches is reported back rather than created - create_term does that. Changing'
450 . ' categories changes which archive pages a post appears on, and a post left with no category'
451 . ' falls into the default one.',
452 'inputSchema' => [
453 'type' => 'object',
454 'properties' => [
455 'id' => ['type' => 'integer', 'description' => 'The post id.'],
456 'type' => [
457 'type' => 'string',
458 'enum' => self::types(),
459 'default' => 'post',
460 'description' => 'The content type of that post.',
461 ],
462 'taxonomy' => [
463 'type' => 'string',
464 'enum' => self::taxonomies(),
465 'default' => 'category',
466 'description' => 'Which set of terms is being set.',
467 ],
468 'terms' => [
469 'type' => 'array',
470 'items' => ['type' => ['integer', 'string']],
471 'description' => 'Term ids, or term names exactly as list_terms reports them.',
472 ],
473 'mode' => [
474 'type' => 'string',
475 'enum' => ['replace', 'add'],
476 'default' => 'replace',
477 'description' => 'Whether the terms given replace what the post has or join it.',
478 ],
479 ],
480 'required' => ['id', 'terms'],
481 ],
482 ];
483 }
484
485 /**
486 * @return array
487 */
488 private static function listMedia()
489 {
490 return [
491 'mode' => Grants::READ,
492 'handler' => 'listMedia',
493 'description' => 'Lists items in the media library - images, video, documents - with id, filename,'
494 . ' MIME type, dimensions, URL and alt text. Use it to find an image, or to see which images'
495 . ' are missing alt text before calling update_alt_texts.',
496 'inputSchema' => [
497 'type' => 'object',
498 'properties' => [
499 'search' => [
500 'type' => 'string',
501 'description' => 'Match against filename, title and caption.',
502 ],
503 'mime_type' => [
504 'type' => 'string',
505 'description' => "Filter by MIME type or prefix, for example 'image' or 'image/png'.",
506 ],
507 'missing_alt_text' => [
508 'type' => 'boolean',
509 'default' => false,
510 'description' => 'Only return images that have no alt text set.',
511 ],
512 'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 20],
513 'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1],
514 ],
515 ],
516 ];
517 }
518
519 /**
520 * @return array
521 */
522 private static function addMediaFromUrl()
523 {
524 return [
525 'mode' => Grants::WRITE,
526 'handler' => 'addMediaFromUrl',
527 'description' => 'Downloads a file from a public web address and adds it to the media library, then'
528 . ' reports the id to use with it. Only addresses the whole internet can reach work: an address on'
529 . ' the server itself or inside its private network is refused, and so is any file type this'
530 . ' WordPress does not accept. The file is copied, so a later change at the original address does'
531 . ' not reach the site. Ask the user where the file came from before adding it - a copyrighted'
532 . ' image is their liability, not the model\'s. Pass alt_text for an image whenever the page it is'
533 . ' meant for is known; list_media can find images missing it later.',
534 'inputSchema' => [
535 'type' => 'object',
536 'properties' => [
537 'url' => [
538 'type' => 'string',
539 'format' => 'uri',
540 'description' => 'The public address of the file to copy.',
541 ],
542 'filename' => [
543 'type' => 'string',
544 'description' => 'Name to store it under. Taken from the address when left out.',
545 ],
546 'alt_text' => [
547 'type' => 'string',
548 'description' => 'What the image shows, for someone who cannot see it.',
549 ],
550 'title' => ['type' => 'string', 'description' => 'Library title. Defaults to the file name.'],
551 'post' => [
552 'type' => 'integer',
553 'description' => 'Attach it to this post id, the way an upload from that editor would.',
554 ],
555 ],
556 'required' => ['url'],
557 ],
558 ];
559 }
560
561 /**
562 * @return array
563 */
564 private static function updateAltTexts()
565 {
566 return [
567 'mode' => Grants::WRITE,
568 'handler' => 'updateAltTexts',
569 'description' => 'Sets the alt text of images in the media library, for screen readers and search'
570 . ' engines. Find images with list_media, look at each one, and write one plain, concrete sentence'
571 . " describing it; do not start with 'Image of' or 'Photo of'. Images that already have alt text"
572 . ' are left alone unless overwrite is true. Low risk and easy to correct afterwards in the media'
573 . ' library.',
574 'inputSchema' => [
575 'type' => 'object',
576 'properties' => [
577 'items' => [
578 'type' => 'array',
579 'minItems' => 1,
580 'maxItems' => 100,
581 'description' => 'The images to describe, by attachment id from list_media.',
582 'items' => [
583 'type' => 'object',
584 'properties' => [
585 'id' => ['type' => 'integer', 'description' => 'The attachment id.'],
586 'alt_text' => [
587 'type' => 'string',
588 'minLength' => 1,
589 'description' => 'The sentence describing the image.',
590 ],
591 ],
592 'required' => ['id', 'alt_text'],
593 ],
594 ],
595 'overwrite' => [
596 'type' => 'boolean',
597 'default' => false,
598 'description' => 'Replace alt text an image already has.',
599 ],
600 ],
601 'required' => ['items'],
602 ],
603 ];
604 }
605
606 /**
607 * @return array
608 */
609 private static function listPlugins()
610 {
611 return [
612 'mode' => Grants::READ,
613 'handler' => 'listPlugins',
614 'description' => 'Lists every plugin installed on the site, active or not: name, slug, version, active'
615 . ' status, whether an update is available, and whether auto-updates are on. Call it before'
616 . ' update_plugins, set_plugin_status, set_auto_updates or delete_inactive_plugins so you can tell'
617 . ' the user exactly what would change, and to say which plugins are out of date. Check it before'
618 . ' install_plugin too - what the user is asking for is often already installed.',
619 'inputSchema' => [
620 'type' => 'object',
621 'properties' => [
622 'status' => ['type' => 'string', 'enum' => ['active', 'inactive', 'any'], 'default' => 'any'],
623 'has_update' => [
624 'type' => 'boolean',
625 'default' => false,
626 'description' => 'Only return plugins with an available update.',
627 ],
628 ],
629 ],
630 ];
631 }
632
633 /**
634 * @return array
635 */
636 private static function installPlugin()
637 {
638 return [
639 'mode' => Grants::WRITE,
640 'handler' => 'installPlugin',
641 'description' => 'Installs a plugin from the WordPress.org plugin directory by its slug - the last part'
642 . ' of its directory listing address, like contact-form-7. Nothing else can be installed this way:'
643 . ' a plugin sold or hosted elsewhere has to be uploaded by hand. A plugin is third-party code that'
644 . ' runs with full access to the site, so name the exact plugin to the user and get their agreement'
645 . ' before calling this. It arrives deactivated unless activate is true; either way the site keeps'
646 . ' working as it did until it is activated. Check list_plugins first, since what the user wants is'
647 . ' often already installed and only needs set_plugin_status.',
648 'inputSchema' => [
649 'type' => 'object',
650 'properties' => [
651 'slug' => [
652 'type' => 'string',
653 'description' => 'The directory slug of the plugin, as WordPress.org lists it.',
654 ],
655 'activate' => [
656 'type' => 'boolean',
657 'default' => false,
658 'description' => 'Activate it immediately after installing.',
659 ],
660 ],
661 'required' => ['slug'],
662 ],
663 ];
664 }
665
666 /**
667 * @return array
668 */
669 private static function setPluginStatus()
670 {
671 return [
672 'mode' => Grants::WRITE,
673 'handler' => 'setPluginStatus',
674 'description' => 'Activates or deactivates installed plugins. Activating runs that plugin on every'
675 . ' page of the site, and deactivating takes away whatever it provided - a shop, a form, a'
676 . ' cache - so say which plugins and which direction, and let the user agree first. Reversible by'
677 . ' calling again with the opposite value, though a plugin that stores nothing outside its own'
678 . ' settings can still lose in-progress state. Slugs are the ones list_plugins reports. Deleting'
679 . ' the files instead is delete_inactive_plugins.',
680 'inputSchema' => [
681 'type' => 'object',
682 'properties' => [
683 'plugins' => [
684 'type' => 'array',
685 'items' => ['type' => 'string'],
686 'description' => 'Plugin slugs to change, as list_plugins reports them.',
687 ],
688 'active' => [
689 'type' => 'boolean',
690 'description' => 'True to activate them, false to deactivate them.',
691 ],
692 ],
693 'required' => ['plugins', 'active'],
694 ],
695 ];
696 }
697
698 /**
699 * @return array
700 */
701 private static function deleteInactivePlugins()
702 {
703 return [
704 'mode' => Grants::WRITE,
705 'handler' => 'deleteInactivePlugins',
706 'description' => 'Permanently removes plugins that are installed but not active. This deletes their'
707 . ' files; settings held in the database often survive but not always, so reinstalling does not'
708 . " reliably restore a plugin's configuration. Active plugins are never touched. Run preview first"
709 . ' and show the user the list - people frequently keep a deactivated plugin on purpose. Slugs are'
710 . ' the ones list_plugins reports.',
711 'inputSchema' => [
712 'type' => 'object',
713 'properties' => [
714 'exclude' => [
715 'type' => 'array',
716 'items' => ['type' => 'string'],
717 'description' => 'Plugin slugs to keep regardless.',
718 ],
719 'preview' => [
720 'type' => 'boolean',
721 'default' => true,
722 'description' => 'When true, lists what would be deleted and returns a confirm_token'
723 . ' without deleting anything.',
724 ],
725 'confirm_token' => [
726 'type' => 'string',
727 'description' => 'The token returned by a previous preview. Required to actually delete.',
728 ],
729 ],
730 ],
731 ];
732 }
733
734 /**
735 * @return array
736 */
737 private static function listThemes()
738 {
739 return [
740 'mode' => Grants::READ,
741 'handler' => 'listThemes',
742 'description' => 'Lists installed themes with name, stylesheet directory, version, whether each is the'
743 . ' active theme or the parent of it, whether an update is available, and whether auto-updates are'
744 . ' on. Only one theme is active at a time, and its parent is in use even though it is not the'
745 . ' active one. Call it before update_themes or delete_inactive_themes so you can tell the user'
746 . ' exactly what would change.',
747 'inputSchema' => [
748 'type' => 'object',
749 'properties' => [
750 'status' => ['type' => 'string', 'enum' => ['active', 'inactive', 'any'], 'default' => 'any'],
751 ],
752 ],
753 ];
754 }
755
756 /**
757 * @return array
758 */
759 private static function updateThemes()
760 {
761 return [
762 'mode' => Grants::WRITE,
763 'handler' => 'updateThemes',
764 'description' => 'Updates themes to their latest published versions. A theme update can change how the'
765 . ' site looks, and it cannot be undone from here, so run with preview first, show the user which'
766 . ' themes and which version numbers would change, and only proceed once they agree. A theme with'
767 . " customizations made outside the site editor is the risky case. Omit 'themes' to update"
768 . ' everything that has an update available. Starts a background job and returns a job id to follow'
769 . ' with get_task_status.',
770 'inputSchema' => [
771 'type' => 'object',
772 'properties' => [
773 'themes' => [
774 'type' => 'array',
775 'items' => ['type' => 'string'],
776 'description' => 'Theme stylesheet directories to update, as list_themes reports them.'
777 . ' Omit to update every theme with an available update.',
778 ],
779 'preview' => [
780 'type' => 'boolean',
781 'default' => true,
782 'description' => 'When true, reports what would be updated and returns a confirm_token'
783 . ' without changing anything.',
784 ],
785 'confirm_token' => [
786 'type' => 'string',
787 'description' => 'The token returned by a previous preview. Required to actually perform'
788 . ' the update, and only valid for the exact set the preview reported.',
789 ],
790 ],
791 ],
792 ];
793 }
794
795 /**
796 * @return array
797 */
798 private static function listUsers()
799 {
800 return [
801 'mode' => Grants::READ,
802 'handler' => 'listUsers',
803 'description' => 'Lists user accounts with id, display name, username, roles, registration date and'
804 . ' post count. Email addresses are deliberately excluded. Use it before delete_users so you can'
805 . ' show exactly which accounts match, and to spot spam registrations - they usually arrive in'
806 . ' bursts and have no posts.',
807 'inputSchema' => [
808 'type' => 'object',
809 'properties' => [
810 'role' => ['type' => 'string', 'description' => 'Only return users with this role.'],
811 'registered_after' => [
812 'type' => 'string',
813 'format' => 'date-time',
814 'description' => 'Only users who registered after this ISO 8601 date.',
815 ],
816 'registered_before' => [
817 'type' => 'string',
818 'format' => 'date-time',
819 'description' => 'Only users who registered before this ISO 8601 date.',
820 ],
821 'max_posts' => [
822 'type' => 'integer',
823 'minimum' => 0,
824 'description' => 'Only users with at most this many published or private posts, pages'
825 . ' or other content.',
826 ],
827 'search' => [
828 'type' => 'string',
829 'description' => 'Match against username and display name.',
830 ],
831 'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 20],
832 'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1],
833 ],
834 ],
835 ];
836 }
837
838 /**
839 * @return array
840 */
841 private static function createUser()
842 {
843 return [
844 'mode' => Grants::WRITE,
845 'handler' => 'createUser',
846 'description' => 'Creates a user account. No password is set here and none is ever returned: with'
847 . ' send_email true WordPress emails the person a link to choose their own, and without it someone'
848 . ' has to send that link from the users screen before the account can be used. Accounts that can'
849 . " manage the site cannot be created through a connection, and an existing account's role cannot"
850 . ' be changed at all. Read the role back to the user before calling - the difference between one'
851 . ' that may only write drafts and one that may publish is not obvious from its name.',
852 'inputSchema' => [
853 'type' => 'object',
854 'properties' => [
855 'username' => [
856 'type' => 'string',
857 'description' => 'The login name. It cannot be changed afterwards.',
858 ],
859 'email' => [
860 'type' => 'string',
861 'format' => 'email',
862 'description' => 'Their email address. No other account may already use it.',
863 ],
864 'role' => [
865 'type' => 'string',
866 'enum' => self::roles(),
867 'default' => 'subscriber',
868 'description' => 'What the account may do.',
869 ],
870 'name' => [
871 'type' => 'string',
872 'description' => 'The name shown beside their posts and comments.',
873 ],
874 'send_email' => [
875 'type' => 'boolean',
876 'default' => false,
877 'description' => 'Email the person that the account exists, with a link to set a password.',
878 ],
879 ],
880 'required' => ['username', 'email'],
881 ],
882 ];
883 }
884
885 /**
886 * @return array
887 */
888 private static function deleteUsers()
889 {
890 return [
891 'mode' => Grants::WRITE,
892 'handler' => 'deleteUsers',
893 'description' => 'Permanently deletes user accounts matching a filter and reassigns any content they'
894 . ' own to another user. Built for clearing spam registrations - accounts that arrive in a burst'
895 . ' and have written nothing. Deletion cannot be undone. The account this connection belongs to,'
896 . ' the reassign_to account and every administrator are never deleted, whatever the filter says,'
897 . ' and the tool refuses to run on multisite. reassign_to is required: without it WordPress'
898 . " deletes the users' posts along with their accounts. Handles up to " . Handlers::DELETE_BATCH
899 . ' accounts per run; when more match, the result says how many remain for the next preview.'
900 . ' Always preview first and show the user the full list of accounts.',
901 'inputSchema' => [
902 'type' => 'object',
903 'properties' => [
904 'reassign_to' => [
905 'type' => 'integer',
906 'description' => 'User id that inherits any content owned by the deleted accounts.',
907 ],
908 'registered_after' => [
909 'type' => 'string',
910 'format' => 'date-time',
911 'description' => 'Only delete users who registered after this ISO 8601 date.',
912 ],
913 'registered_before' => [
914 'type' => 'string',
915 'format' => 'date-time',
916 'description' => 'Only delete users who registered before this ISO 8601 date.',
917 ],
918 'role' => [
919 'type' => 'string',
920 'description' => 'Only delete users with this role. Administrators are refused regardless.',
921 ],
922 'max_posts' => [
923 'type' => 'integer',
924 'minimum' => 0,
925 'default' => 0,
926 'description' => 'Only delete users with at most this many published or private posts,'
927 . ' pages or other content. Their drafts do not count and pass to reassign_to.',
928 ],
929 'preview' => [
930 'type' => 'boolean',
931 'default' => true,
932 'description' => 'When true, lists the accounts that match and returns a confirm_token'
933 . ' without deleting anything.',
934 ],
935 'confirm_token' => [
936 'type' => 'string',
937 'description' => 'The token returned by a previous preview. Only valid for the exact set'
938 . ' of accounts that preview reported.',
939 ],
940 ],
941 'required' => ['reassign_to'],
942 ],
943 ];
944 }
945
946 /**
947 * @return array
948 */
949 private static function regenerateThumbnails()
950 {
951 return [
952 'mode' => Grants::WRITE,
953 'handler' => 'regenerateThumbnails',
954 'description' => 'Recreates the resized copies WordPress makes of each uploaded image. Use it after'
955 . ' switching themes or changing image sizes, when thumbnails are cropped wrong or look blurry.'
956 . ' Original uploads are never altered, so it is safe to run again. Starts a background job and'
957 . ' returns a job id to follow with get_task_status; a large media library takes a long time.',
958 'inputSchema' => [
959 'type' => 'object',
960 'properties' => [
961 'ids' => [
962 'type' => 'array',
963 'items' => ['type' => 'integer'],
964 'description' => 'Specific attachment ids. Omit to process the whole media library.',
965 ],
966 'only_missing' => [
967 'type' => 'boolean',
968 'default' => false,
969 'description' => 'Only create sizes that are absent, rather than rebuilding every size.',
970 ],
971 ],
972 ],
973 ];
974 }
975
976 /**
977 * @return array
978 */
979 private static function updatePlugins()
980 {
981 return [
982 'mode' => Grants::WRITE,
983 'handler' => 'updatePlugins',
984 'description' => 'Updates plugins to their latest published versions. This cannot be undone from here,'
985 . ' and an update can change or break how a site looks or behaves. Always run with preview first,'
986 . ' show the user which plugins and which version numbers would change, and only proceed once they'
987 . " agree. Omit 'plugins' to update everything that has an update available. Update plugins before"
988 . ' update_core, not after. Starts a background job and returns a job id to follow with'
989 . ' get_task_status.',
990 'inputSchema' => [
991 'type' => 'object',
992 'properties' => [
993 'plugins' => [
994 'type' => 'array',
995 'items' => ['type' => 'string'],
996 'description' => 'Plugin slugs to update. Omit to update every plugin with an available'
997 . ' update.',
998 ],
999 'preview' => [
1000 'type' => 'boolean',
1001 'default' => true,
1002 'description' => 'When true, reports what would be updated and returns a confirm_token'
1003 . ' without changing anything.',
1004 ],
1005 'confirm_token' => [
1006 'type' => 'string',
1007 'description' => 'The token returned by a previous preview. Required to actually perform'
1008 . ' the update, and only valid for the exact set the preview reported.',
1009 ],
1010 ],
1011 ],
1012 ];
1013 }
1014
1015 /**
1016 * @return array
1017 */
1018 private static function updateCore()
1019 {
1020 return [
1021 'mode' => Grants::WRITE,
1022 'handler' => 'updateCore',
1023 'description' => 'Updates WordPress itself to the latest version. This is the highest-risk action'
1024 . ' available: a core update can break plugins and themes that have not been tested against the'
1025 . ' new version, and it cannot be undone from here. Always preview first, tell the user the version'
1026 . ' they would move from and to, and recommend they have a backup. Run update_plugins before this,'
1027 . ' since plugin authors ship compatibility fixes ahead of core releases; the tool refuses to start'
1028 . ' while plugin updates are waiting. Starts a background job to follow with get_task_status.',
1029 'inputSchema' => [
1030 'type' => 'object',
1031 'properties' => [
1032 'preview' => [
1033 'type' => 'boolean',
1034 'default' => true,
1035 'description' => 'When true, reports the current and target versions and returns a'
1036 . ' confirm_token without changing anything.',
1037 ],
1038 'confirm_token' => [
1039 'type' => 'string',
1040 'description' => 'The token returned by a previous preview. Required to actually perform'
1041 . ' the update.',
1042 ],
1043 ],
1044 ],
1045 ];
1046 }
1047
1048 /**
1049 * @return array
1050 */
1051 private static function setAutoUpdates()
1052 {
1053 return [
1054 'mode' => Grants::WRITE,
1055 'handler' => 'setAutoUpdates',
1056 'description' => 'Turns automatic updates on or off for specific plugins and themes, or for all of them'
1057 . ' at once. Enabling them is the low-effort way to keep a site current, at the cost of future'
1058 . ' versions installing without anyone reviewing them first. Immediately reversible - call again'
1059 . ' with the opposite value.',
1060 'inputSchema' => [
1061 'type' => 'object',
1062 'properties' => [
1063 'enabled' => [
1064 'type' => 'boolean',
1065 'description' => 'True to turn auto-updates on, false to turn them off.',
1066 ],
1067 'plugins' => [
1068 'type' => 'array',
1069 'items' => ['type' => 'string'],
1070 'description' => 'Plugin slugs to change, as list_plugins reports them.',
1071 ],
1072 'themes' => [
1073 'type' => 'array',
1074 'items' => ['type' => 'string'],
1075 'description' => 'Theme stylesheet directories to change, as list_themes reports them.',
1076 ],
1077 'all' => [
1078 'type' => 'boolean',
1079 'default' => false,
1080 'description' => 'Apply to every installed plugin and theme, ignoring the lists above.',
1081 ],
1082 ],
1083 'required' => ['enabled'],
1084 ],
1085 ];
1086 }
1087
1088 /**
1089 * @return array
1090 */
1091 private static function deleteInactiveThemes()
1092 {
1093 return [
1094 'mode' => Grants::WRITE,
1095 'handler' => 'deleteInactiveThemes',
1096 'description' => 'Permanently removes themes that are not in use. The active theme and its parent are'
1097 . ' never deleted. Keep one bundled default theme as a fallback - WordPress switches to it if the'
1098 . ' active theme breaks - which keep_default does by default. Not reversible from here: a deleted'
1099 . ' theme has to be reinstalled, though its saved settings stay in the database. Run preview first'
1100 . ' and show the user the list.',
1101 'inputSchema' => [
1102 'type' => 'object',
1103 'properties' => [
1104 'exclude' => [
1105 'type' => 'array',
1106 'items' => ['type' => 'string'],
1107 'description' => 'Theme stylesheet directories to keep regardless.',
1108 ],
1109 'keep_default' => [
1110 'type' => 'boolean',
1111 'default' => true,
1112 'description' => 'Keep the most recent bundled WordPress theme as a fallback.',
1113 ],
1114 'preview' => [
1115 'type' => 'boolean',
1116 'default' => true,
1117 'description' => 'When true, lists what would be deleted and returns a confirm_token'
1118 . ' without deleting anything.',
1119 ],
1120 'confirm_token' => [
1121 'type' => 'string',
1122 'description' => 'The token returned by a previous preview. Required to actually delete.',
1123 ],
1124 ],
1125 ],
1126 ];
1127 }
1128
1129 /**
1130 * @return array
1131 */
1132 private static function updateSiteSettings()
1133 {
1134 return [
1135 'mode' => Grants::WRITE,
1136 'handler' => 'updateSiteSettings',
1137 'description' => "Changes the site's name, tagline, language, timezone and the way it writes dates and"
1138 . ' times. The name and tagline are read by every visitor and by search engines, so quote the exact'
1139 . ' wording to the user first. A timezone or format change only affects how existing dates are'
1140 . ' shown; nothing is renamed or moved. Changing the language changes the wording WordPress itself'
1141 . ' uses, never the words already written into posts, and the translation has to be installable on'
1142 . ' this site. get_site_info reports what each of these is now. This is the only tool that may'
1143 . ' change a site setting - every other setting a connection could reach is refused.',
1144 'inputSchema' => [
1145 'type' => 'object',
1146 'properties' => [
1147 'title' => ['type' => 'string', 'description' => "The site's name."],
1148 'tagline' => ['type' => 'string', 'description' => 'The short line under the name.'],
1149 'language' => [
1150 'type' => 'string',
1151 'description' => 'A WordPress locale such as de_DE, or en_US for English.',
1152 ],
1153 'timezone' => [
1154 'type' => 'string',
1155 'description' => 'A timezone name such as Europe/Amsterdam.',
1156 ],
1157 'date_format' => [
1158 'type' => 'string',
1159 'description' => 'PHP date format for dates, such as F j, Y.',
1160 ],
1161 'time_format' => [
1162 'type' => 'string',
1163 'description' => 'PHP date format for times, such as g:i a.',
1164 ],
1165 'start_of_week' => [
1166 'type' => 'integer',
1167 'minimum' => 0,
1168 'maximum' => 6,
1169 'description' => 'Which day calendars start on, 0 for Sunday.',
1170 ],
1171 ],
1172 ],
1173 ];
1174 }
1175
1176 /**
1177 * @return array
1178 */
1179 private static function getSiteHealth()
1180 {
1181 return [
1182 'mode' => Grants::READ,
1183 'handler' => 'getSiteHealth',
1184 'description' => "Reports the site's maintenance status: WordPress and PHP versions and whether PHP is"
1185 . ' still supported, database version, memory limit, HTTPS status, whether WP-Cron and background'
1186 . ' updates are working, how many plugins and themes are behind, and whether automatic updates are'
1187 . ' honored. Use it to explain why maintenance is needed before running any update - a site whose'
1188 . ' cron or background updates are broken will silently fail to keep itself current, and a'
1189 . ' background job started here depends on cron. Filesystem paths, database credentials and'
1190 . ' server constants are deliberately excluded.',
1191 'inputSchema' => ['type' => 'object', 'properties' => (object) []],
1192 ];
1193 }
1194
1195 /**
1196 * @return array
1197 */
1198 private static function requestFeature()
1199 {
1200 return [
1201 'mode' => Grants::READ,
1202 'handler' => 'requestFeature',
1203 'annotations' => ['destructiveHint' => false],
1204 'description' => 'Passes on a request for a tool this site does not have, to the people who build'
1205 . ' these tools. Call it only when the user has asked for something none of the other tools can'
1206 . ' do, tell the user you are passing the request on, and send one request per conversation. It'
1207 . ' changes nothing on the site. What you write is sent as it is, so leave out names, email'
1208 . ' addresses and anything quoted from the site.',
1209 'inputSchema' => [
1210 'type' => 'object',
1211 'properties' => [
1212 'tool' => [
1213 'type' => 'string',
1214 'pattern' => '^[a-z0-9_]{1,64}$',
1215 'description' => 'The name the missing tool would have, in the style of the tools that'
1216 . " exist, such as 'schedule_post'.",
1217 ],
1218 'justification' => [
1219 'type' => 'string',
1220 'minLength' => 20,
1221 'maxLength' => 2000,
1222 'description' => 'What the tool would do and why the existing tools cannot.',
1223 ],
1224 'context' => [
1225 'type' => 'string',
1226 'maxLength' => 2000,
1227 'description' => 'What the user was trying to get done.',
1228 ],
1229 ],
1230 'required' => ['tool', 'justification'],
1231 ],
1232 ];
1233 }
1234
1235 /**
1236 * @return array
1237 */
1238 private static function getTaskStatus()
1239 {
1240 return [
1241 'mode' => Grants::READ,
1242 'handler' => 'getTaskStatus',
1243 'description' => 'Reports on a background job started by update_plugins, update_themes, update_core or'
1244 . ' regenerate_thumbnails: whether it is queued, running, done or failed, how many steps are'
1245 . ' finished, and the result of each. Jobs run on WP-Cron, so one that stays queued means the'
1246 . " site's cron is not firing; get_site_health says whether it is.",
1247 'inputSchema' => [
1248 'type' => 'object',
1249 'properties' => [
1250 'job_id' => ['type' => 'string', 'description' => 'The job id the tool returned.'],
1251 ],
1252 'required' => ['job_id'],
1253 ],
1254 ];
1255 }
1256
1257 /**
1258 * @return array
1259 */
1260 private static function getSiteInfo()
1261 {
1262 return [
1263 'mode' => Grants::READ,
1264 'handler' => 'getSiteInfo',
1265 'description' => "Returns the site's identity and setup: name, tagline, URL, language, timezone,"
1266 . ' WordPress version, active theme, the content types available, and how many plugins are'
1267 . ' installed and active. Call this early in a conversation to ground yourself in what kind of'
1268 . ' site this is before doing anything else. update_site_settings changes the ones that can be'
1269 . ' changed.',
1270 'inputSchema' => ['type' => 'object', 'properties' => (object) []],
1271 ];
1272 }
1273 }
1274