PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.7.0
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.7.0
2.7.1 2.7.0 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.4.0 2.3.3 2.3.2 2.3.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 All 130 releases
wdesignkit / includes / abilities / utilities / wdesignkit-list-abilities.php

wdesignkit-list-abilities.php in WDesignKit – AI Templates, Widget Builder & MCP Workflow 2.7.0, at includes/abilities/utilities/wdesignkit-list-abilities.php

383 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability: List all registered WDesignKit abilities.
4 *
5 * WordPress 7.1 gave wp_get_abilities() an $args array so the registry can be filtered by
6 * category, namespace and meta before it is ever materialised, plus item_include_callback /
7 * result_callback for anything declarative filters cannot express. This ability now pushes
8 * its filtering and sorting down into that call when core supports it, and falls back to the
9 * previous walk-everything-and-filter-in-PHP path on WordPress 7.0.
10 *
11 * One 7.1 behaviour change worth knowing: a bare wp_get_abilities() no longer returns the raw
12 * registry — it runs the global wp_get_abilities_item_include / wp_get_abilities_result
13 * filter pipeline. That is the right thing for a discovery tool (if a site has chosen to hide
14 * an ability, this list should agree), so it is deliberately not bypassed here.
15 * WP_Abilities_Registry::get_all_registered() is the unfiltered accessor if that is ever needed.
16 *
17 * @link https://make.wordpress.org/core/2026/08/05/filtering-registered-abilities-with-wp_get_abilities-in-wordpress-7-1/
18 */
19
20 declare(strict_types=1);
21
22 if (!defined('ABSPATH')) {
23 exit();
24 }
25
26 wp_register_ability('wdesignkit/list-abilities', [
27 'label' => __('List WDesignKit Abilities', 'wdesignkit'),
28 'description' => __(
29 'Returns all registered WDesignKit abilities with their slugs, labels, descriptions, and input parameter summaries. Filterable by category, namespace and public-exposure flag. Useful for discovering what operations are available.',
30 'wdesignkit',
31 ),
32 'category' => 'wdesignkit',
33 'input_schema' => [
34 'type' => 'object',
35 'properties' => [
36 'category' => [
37 'type' => 'string',
38 'description' => 'Filter by ability category slug. Defaults to "wdesignkit". Pass an empty string to list every category.',
39 ],
40 'namespace' => [
41 'type' => 'string',
42 'description' => 'Filter by ability namespace — the part before the "/" in the ability name (e.g. "wdesignkit"). Leave empty for all namespaces.',
43 ],
44 'only_public' => [
45 'type' => 'boolean',
46 'description' => 'When true, returns only abilities whose meta.public flag is true — i.e. those intended for external clients such as REST, MCP adapters and AI agents. Defaults to false (return all).',
47 ],
48 'include_schemas' => [
49 'type' => 'boolean',
50 'description' => 'Whether to include full input/output JSON schemas. Defaults to false (returns parameter names only).',
51 ],
52 'fields' => [
53 'type' => 'array',
54 'description' => 'Return only these properties for each ability instead of the full entry. "name" is always included. Names outside the listed set are rejected by input validation. input_schema/output_schema are only returned when include_schemas is also true.',
55 'items' => [
56 'type' => 'string',
57 'enum' => [
58 'name',
59 'label',
60 'description',
61 'category',
62 'namespace',
63 'mcp_tool',
64 'params',
65 'public',
66 'show_in_rest',
67 'required_capability',
68 'readonly',
69 'destructive',
70 'idempotent',
71 'input_schema',
72 'output_schema',
73 ],
74 ],
75 ],
76 ],
77 'additionalProperties' => false,
78 ],
79 'output_schema' => [
80 'type' => 'object',
81 'properties' => [
82 'success' => ['type' => 'boolean'],
83 'total' => ['type' => 'integer'],
84 'abilities' => ['type' => 'array'],
85 'message' => ['type' => 'string'],
86 ],
87 ],
88 'execute_callback' => 'wdesignkit_mcp_list_abilities',
89 'permission_callback' => 'wdesignkit_mcp_permission_callback',
90 'meta' => [
91 'public' => true,
92 'show_in_rest' => true,
93 'mcp' => ['public' => true],
94 'annotations' => [
95 'instructions' => implode("\n", [
96 'Lists all registered WDesignKit abilities.',
97 'By default filters to the "wdesignkit" category only; pass category: "" to list every category.',
98 'Use namespace to filter by ability namespace (the part before "/"), e.g. "wdesignkit".',
99 'Use only_public: true to see just the abilities exposed to external clients (meta.public).',
100 'Use include_schemas: true to get full JSON schemas for each ability.',
101 'Use fields to trim each entry to the properties you need, e.g. fields: ["name","description"].',
102 'Ability slug format: "wdesignkit/{action}" — e.g. "wdesignkit/list-widgets".',
103 'MCP tool name is derived by replacing "/" with "-" — e.g. "wdesignkit-list-widgets".',
104 'required_capability, when present, is an extra WordPress capability that ability needs beyond manage_options.',
105 ]),
106 'readonly' => true,
107 'destructive' => false,
108 'idempotent' => true,
109 ],
110 ],
111 ]);
112
113 /**
114 * Whether this WordPress supports the wp_get_abilities( $args ) query API (7.1+).
115 *
116 * Detected by reflecting the function's arity rather than comparing $wp_version, so the
117 * capability is read from the function that will actually be called — correct on nightlies,
118 * on a site where the Abilities API arrives via a feature plugin, and if the signature is
119 * ever backported.
120 *
121 * @return bool True when wp_get_abilities() accepts filtering arguments.
122 */
123 function wdesignkit_mcp_abilities_query_supported(): bool {
124 static $supported = null;
125
126 if (null !== $supported) {
127 return $supported;
128 }
129
130 $supported = false;
131
132 if (function_exists('wp_get_abilities')) {
133 try {
134 $supported = (new ReflectionFunction('wp_get_abilities'))->getNumberOfParameters() > 0;
135 } catch (ReflectionException $e) {
136 $supported = false;
137 }
138 }
139
140 return $supported;
141 }
142
143 /**
144 * Every property a list entry can carry, in output order.
145 *
146 * Used to validate and order the `fields` input. Kept next to the enum in the input schema
147 * above — both must be updated together when an entry gains a property.
148 *
149 * @return string[]
150 */
151 function wdesignkit_mcp_list_abilities_fields(): array {
152 return [
153 'name',
154 'label',
155 'description',
156 'category',
157 'namespace',
158 'mcp_tool',
159 'params',
160 'public',
161 'show_in_rest',
162 'required_capability',
163 'readonly',
164 'destructive',
165 'idempotent',
166 'input_schema',
167 'output_schema',
168 ];
169 }
170
171 function wdesignkit_mcp_list_abilities(array $input): array {
172 if (!function_exists('wp_get_abilities')) {
173 return [
174 'success' => false,
175 'message' => 'WordPress Abilities API is not available.',
176 'total' => 0,
177 'abilities' => [],
178 ];
179 }
180
181 $filter_category = sanitize_text_field($input['category'] ?? 'wdesignkit');
182 $filter_namespace = sanitize_text_field($input['namespace'] ?? '');
183 $only_public = !empty($input['only_public']);
184 $include_schemas = !empty($input['include_schemas']);
185
186 // Resolve the requested field subset. The input schema's enum has already rejected any
187 // name outside the known set (core validates before this callback runs), so the
188 // intersect here is belt-and-braces for a direct call that bypasses validation. `name`
189 // is always kept so every entry stays identifiable.
190 $requested_fields = [];
191 if (!empty($input['fields']) && is_array($input['fields'])) {
192 $allowed = wdesignkit_mcp_list_abilities_fields();
193 $requested = array_map('strval', $input['fields']);
194 $requested_fields = array_values(array_intersect($allowed, $requested));
195
196 if (!in_array('name', $requested_fields, true)) {
197 array_unshift($requested_fields, 'name');
198 }
199 }
200
201 if (wdesignkit_mcp_abilities_query_supported()) {
202 // WordPress 7.1+: let core do the filtering and sorting.
203 $args = [];
204
205 if ('' !== $filter_category) {
206 $args['category'] = $filter_category;
207 }
208
209 if ('' !== $filter_namespace) {
210 $args['namespace'] = $filter_namespace;
211 }
212
213 if ($only_public) {
214 // meta matching is strict, and `public` resolves to a boolean for every ability
215 // in 7.1 (defaulting to false when not supplied at registration).
216 $args['meta'] = ['public' => true];
217 }
218
219 // Sorting belongs in result_callback — it sees the whole matched set, so core does
220 // not have to materialise an intermediate copy for us to sort afterwards.
221 $args['result_callback'] = static function (array $abilities): array {
222 uasort($abilities, static function ($a, $b): int {
223 return strcmp($a->get_name(), $b->get_name());
224 });
225
226 return $abilities;
227 };
228
229 $abilities = wp_get_abilities($args);
230 $prefiltered = true;
231 } else {
232 // WordPress 7.0: no query API — fetch everything and filter below.
233 $abilities = wp_get_abilities();
234 $prefiltered = false;
235 }
236
237 if (empty($abilities)) {
238 return [
239 'success' => true,
240 'total' => 0,
241 'abilities' => [],
242 ];
243 }
244
245 $result = [];
246
247 foreach ($abilities as $ability) {
248 $name = $ability->get_name();
249 $category = $ability->get_category();
250 $meta = $ability->get_meta();
251
252 if (!$prefiltered) {
253 if ('' !== $filter_category && $category !== $filter_category) {
254 continue;
255 }
256
257 if ('' !== $filter_namespace && 0 !== strpos($name, $filter_namespace . '/')) {
258 continue;
259 }
260
261 // On 7.0 core resolves no meta.public, so read the flag registered explicitly
262 // (see wdesignkit_mcp_resolve_public_flag()). An ability that declares nothing
263 // resolves to null and is excluded, matching 7.1's default of false.
264 if ($only_public && true !== wdesignkit_mcp_resolve_public_flag($meta)) {
265 continue;
266 }
267 }
268
269 $input_schema = $ability->get_input_schema();
270 $annotations = $meta['annotations'] ?? [];
271
272 // Build a lightweight parameter summary (names + types only)
273 $params = [];
274 $required_params = $input_schema['required'] ?? [];
275 $properties = $input_schema['properties'] ?? [];
276
277 // Handle the case where properties is cast to stdClass (empty object)
278 if (is_object($properties)) {
279 $properties = (array) $properties;
280 }
281
282 foreach ($properties as $param_name => $param_schema) {
283 $type = $param_schema['type'] ?? 'any';
284 if (is_array($type)) {
285 $type = implode('|', $type);
286 }
287 $params[] = [
288 'name' => $param_name,
289 'type' => $type,
290 'description' => $param_schema['description'] ?? '',
291 'required' => in_array($param_name, $required_params, true),
292 'enum' => $param_schema['enum'] ?? null,
293 ];
294 }
295
296 $slash = strpos($name, '/');
297 $namespace = false === $slash ? '' : substr($name, 0, $slash);
298
299 $entry = [
300 'name' => $name,
301 'label' => $ability->get_label(),
302 'description' => $ability->get_description(),
303 'category' => $category,
304 'namespace' => $namespace,
305 'mcp_tool' => str_replace('/', '-', $name),
306 'params' => $params,
307 'public' => wdesignkit_mcp_resolve_public_flag($meta),
308 'show_in_rest' => $meta['show_in_rest'] ?? null,
309 'required_capability' => $meta['required_capability'] ?? null,
310 'readonly' => $annotations['readonly'] ?? null,
311 'destructive' => $annotations['destructive'] ?? null,
312 'idempotent' => $annotations['idempotent'] ?? null,
313 ];
314
315 if ($include_schemas) {
316 $entry['input_schema'] = $input_schema;
317 $entry['output_schema'] = $ability->get_output_schema();
318 }
319
320 if (!empty($requested_fields)) {
321 $trimmed = [];
322 foreach ($requested_fields as $field) {
323 // A schema field is only available when include_schemas asked for it.
324 if (array_key_exists($field, $entry)) {
325 $trimmed[$field] = $entry[$field];
326 }
327 }
328 $entry = $trimmed;
329 }
330
331 $result[] = $entry;
332 }
333
334 // Sort alphabetically by ability name. Already done by result_callback on 7.1, but the
335 // 7.0 fallback path still needs it and re-sorting a sorted list costs nothing.
336 usort($result, static function (array $a, array $b): int {
337 return strcmp($a['name'] ?? '', $b['name'] ?? '');
338 });
339
340 return [
341 'success' => true,
342 'total' => count($result),
343 'abilities' => $result,
344 ];
345 }
346
347 /**
348 * Resolves an ability's effective public-exposure intent from its meta.
349 *
350 * WordPress 7.1 added a unified boolean `meta.public` meaning "available to external clients"
351 * (REST, MCP adapters, AI agents), resolved for every ability and defaulting to false. On 7.0
352 * that resolved value does not exist, so this reads the flag WDesignKit now registers
353 * explicitly on every ability, then falls back to a channel flag for an ability (a third
354 * party's, say) that declares no general intent.
355 *
356 * Note this is the reverse of how core resolves a *channel*: REST asks
357 * $meta['show_in_rest'] ?? $meta['public'] ?? false, because a specific channel setting
358 * overrides the general one. Here the general intent is the answer being sought, so
359 * `public` is authoritative and the channel flags are only a last-resort inference. The
360 * four destructive abilities are exactly this case — public false, show_in_rest true — and
361 * are correctly reported as public: false with show_in_rest: true alongside.
362 *
363 * @link https://make.wordpress.org/core/2026/08/04/a-unified-public-exposure-flag-for-abilities-in-wordpress-7-1/
364 *
365 * @param array $meta Ability meta.
366 * @return bool|null True/false when determinable, null when the ability declares nothing.
367 */
368 function wdesignkit_mcp_resolve_public_flag(array $meta) {
369 if (isset($meta['public'])) {
370 return (bool) $meta['public'];
371 }
372
373 if (isset($meta['mcp']['public'])) {
374 return (bool) $meta['mcp']['public'];
375 }
376
377 if (isset($meta['show_in_rest'])) {
378 return (bool) $meta['show_in_rest'];
379 }
380
381 return null;
382 }
383