Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
32.70% covered (danger)
32.70%
52 / 159
13.04% covered (danger)
13.04%
3 / 23
CRAP
n/a
0 / 0
blu_register_ability
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
blu_unregister_ability
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
blu_get_ability
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
blu_get_abilities
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
blu_register_ability_category
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
blu_unregister_ability_category
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
blu_get_ability_category
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
blu_get_ability_categories
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
blu_filter_abilities_by_category
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
blu_get_abilities_by_category
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
blu_get_ability_by_type
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
blu_filter_abilities_by_namespace
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
blu_get_abilities_by_namespace
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
blu_prepare_ability_response
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
blu_standardize_rest_response
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
blu_get_status_type
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
blu_resolve_post_type
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
110
blu_post_type_not_found_response
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
blu_project_post_summary
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
blu_project_post_full
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
blu_is_valid_list
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
blu_is_valid_input_array
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
8.06
blu_filter_terms_by_patterns
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
12
1<?php
2
3/**
4 * Register a new ability in the system.
5 *
6 * @param string $name The unique name of the ability to register.
7 * @param array  $args The arguments to configure the ability (e.g., description, metadata).
8 *
9 * @return WP_Ability|null The registered ability object if registration is successful, or null if the function `wp_register_ability` is unavailable.
10 */
11function blu_register_ability( string $name, array $args ): ?WP_Ability {
12    if ( function_exists( 'wp_register_ability' ) ) {
13
14        return wp_register_ability( $name, $args );
15    }
16
17    return null;
18}
19
20/**
21 * Unregisters an ability by its name.
22 *
23 * @param string $name The name of the ability to unregister.
24 *
25 * @return WP_Ability|null The unregistered ability object if successful, or null if the function `wp_unregister_ability` does not exist.
26 */
27function blu_unregister_ability( string $name ): ?WP_Ability {
28    if ( function_exists( 'wp_unregister_ability' ) ) {
29        return wp_unregister_ability( $name );
30    }
31
32    return null;
33}
34
35/**
36 * Retrieves an ability by its name.
37 *
38 * @param string $name The name of the ability to retrieve.
39 *
40 * @return WP_Ability|null The ability object if found, or null if not found or if the function does not exist.
41 */
42function blu_get_ability( string $name ): ?WP_Ability {
43    if ( function_exists( 'wp_get_ability' ) ) {
44        return wp_get_ability( $name );
45    }
46
47    return null;
48}
49
50/**
51 * Retrieves a list of all abilities available in the system.
52 *
53 * @return WP_Ability[] An array of all abilities if the underlying function exists, or an empty array otherwise.
54 */
55function blu_get_abilities(): array {
56    if ( function_exists( 'wp_get_abilities' ) ) {
57        return wp_get_abilities();
58    }
59
60    return array();
61}
62
63/**
64 * Registers a new ability category with the specified slug and arguments.
65 *
66 * @param string $slug The unique identifier for the ability category to be registered.
67 * @param array  $args The arguments defining the properties of the ability category.
68 *
69 * @return WP_Ability_Category|null The registered ability category if successful, or null if the registration function is not available.
70 */
71function blu_register_ability_category( string $slug, array $args ): ?WP_Ability_Category {
72    if ( function_exists( 'wp_register_ability_category' ) ) {
73        return wp_register_ability_category( $slug, $args );
74    }
75
76    return null;
77}
78
79/**
80 * Unregisters an ability category by its slug.
81 *
82 * @param string $slug The slug of the ability category to unregister.
83 *
84 * @return WP_Ability_Category|null The unregistered ability category object if successful, or null if the function does not exist or the category could not be unregistered.
85 */
86function blu_unregister_ability_category( string $slug ): ?WP_Ability_Category {
87    if ( function_exists( 'wp_unregister_ability_category' ) ) {
88        return wp_unregister_ability_category( $slug );
89    }
90
91    return null;
92}
93
94/**
95 * Retrieves the ability category associated with the given slug.
96 *
97 * @param string $slug The slug identifying the ability category.
98 *
99 * @return WP_Ability_Category|null The ability category object if found, or null if no category exists or the function is unavailable.
100 */
101function blu_get_ability_category( string $slug ): ?WP_Ability_Category {
102    if ( function_exists( 'wp_get_ability_category' ) ) {
103        return wp_get_ability_category( $slug );
104    }
105
106    return null;
107}
108
109/**
110 * Retrieves a list of available ability categories.
111 *
112 * @return string[] An array of ability categories. If the function `wp_get_ability_categories` is not available, it returns an empty array.
113 */
114function blu_get_ability_categories(): array {
115    if ( function_exists( 'wp_get_ability_categories' ) ) {
116        return wp_get_ability_categories();
117    }
118
119    return array();
120}
121
122/**
123 * Filters a list of abilities by the specified category.
124 *
125 * @param WP_Ability[] $abilities An array of abilities to be filtered.
126 * @param string       $category  The category used to filter the abilities.
127 *
128 * @return WP_Ability[] An array of abilities that match the specified category.
129 */
130function blu_filter_abilities_by_category( array $abilities, string $category ): array {
131    return array_filter(
132        $abilities,
133        function ( $ability ) use ( $category ) {
134            return $ability->get_category() === $category;
135        }
136    );
137}
138
139/**
140 * Retrieves a list of abilities filtered by the specified category.
141 *
142 * @param string $category The category used to filter the abilities.
143 *
144 * @return WP_Ability[] An array of abilities that belong to the specified category.
145 */
146function blu_get_abilities_by_category( string $category ): array {
147    return blu_filter_abilities_by_category( blu_get_abilities(), $category );
148}
149
150
151/**
152 * Get the abilities name by type
153 *
154 * @param string $type The type
155 *
156 * @return array
157 */
158function blu_get_ability_by_type( $type = 'tool' ) {
159    $all_abilities     = blu_get_abilities_by_category( 'blu-mcp' );
160    $current_abilities = array();
161    $type              = in_array( $type, array( 'tool', 'prompt', 'resource' ), true ) ? $type : 'tool';
162    foreach ( $all_abilities as $ability ) {
163        $meta         = $ability->get_meta();
164        $ability_type = 'tool';
165        $public       = true;
166
167        if ( isset( $meta['mcp']['type'] ) ) {
168            $ability_type = $meta['mcp']['type'];
169        }
170        if ( isset( $meta['mcp']['public'] ) ) {
171            $public = $meta['mcp']['public'];
172        }
173
174        if ( $public && $ability_type === $type ) {
175            $current_abilities[] = $ability->get_name();
176        }
177    }
178
179    return $current_abilities;
180}
181
182/**
183 * Filters a list of abilities by a specified namespace.
184 *
185 * @param WP_Ability[] $abilities An array of abilities to filter.
186 * @param string       $namespace The namespace used to filter the abilities.
187 *
188 * @return WP_Ability[] An array of abilities that match the specified namespace.
189 */
190function blu_filter_abilities_by_namespace( array $abilities, string $namespace ): array {
191    $namespace_prefix = rtrim( $namespace, '/' ) . '/';
192
193    return array_filter(
194        $abilities,
195        function ( $ability ) use ( $namespace_prefix ) {
196            return 0 === strpos( $ability->get_name(), $namespace_prefix );
197        }
198    );
199}
200
201/**
202 * Get all abilities that belong to a specific namespace.
203 *
204 * @param string $namespace The namespace to filter by (e.g., 'my-plugin').
205 *
206 * @return WP_Ability[] Array of abilities matching the namespace.
207 */
208function blu_get_abilities_by_namespace( string $namespace ): array {
209    return blu_filter_abilities_by_namespace( blu_get_abilities(), $namespace );
210}
211
212/**
213 * Prepares a standardized ability response.
214 *
215 * @param int   $status  The HTTP status code of the response.
216 * @param mixed $message The response message or data.
217 *
218 * @return array An associative array containing 'status' and 'response' keys.
219 */
220function blu_prepare_ability_response( $status, $message ) {
221    return array(
222        'statusCode' => $status,
223        'status'     => blu_get_status_type( $status ),
224        'message'    => $message,
225    );
226}
227
228/**
229 * Standardizes a REST API response into a consistent format.
230 *
231 * @param mixed $response The original response which can be a WP_Error or WP_REST_Response.
232 *
233 * @return array An associative array containing 'status' and 'response' keys.
234 */
235function blu_standardize_rest_response( $response ) {
236
237    if ( is_wp_error( $response ) ) {
238
239        $status = $response->get_error_code() ? $response->get_error_code() : 500;
240
241        return blu_prepare_ability_response( $status, $response->get_error_message() );
242
243    } elseif ( $response instanceof \WP_REST_Response ) {
244
245        return blu_prepare_ability_response( $response->get_status(), $response->get_data() );
246
247    } else {
248        return blu_prepare_ability_response( 500, 'Unexpected response format.' );
249    }
250}
251
252/**
253 * Maps an HTTP status code to a simplified status type.
254 *
255 * @param int $status_code The HTTP status code to evaluate.
256 *
257 * @return string The corresponding status type: 'success', 'error', or 'unknown'.
258 */
259function blu_get_status_type( $status_code ) {
260
261    $status = 'unknown';
262
263    if ( $status_code >= 200 && $status_code < 400 ) {
264
265        $status = 'success';
266
267    } elseif ( $status_code >= 400 && $status_code <= 599 ) {
268
269        $status = 'error';
270
271    }
272    return $status;
273}
274
275/**
276 * Resolve a user-supplied post-type identifier to its canonical registered slug.
277 *
278 * Accepts any of: the slug itself ("bmcp_book"), the REST base ("books"),
279 * the plural label ("Books"), the singular label ("Book"), or the menu name â€”
280 * case-insensitively. This lets LLM tool callers pass whichever string they
281 * have on hand instead of being forced to learn the internal slug.
282 *
283 * @param string $input The identifier provided by the caller.
284 *
285 * @return string|null The canonical post-type slug, or null if no match.
286 */
287function blu_resolve_post_type( string $input ): ?string {
288    $input = trim( $input );
289    if ( '' === $input ) {
290        return null;
291    }
292
293    if ( post_type_exists( $input ) ) {
294        return $input;
295    }
296
297    $needle = strtolower( $input );
298
299    foreach ( get_post_types( array(), 'objects' ) as $slug => $object ) {
300        $candidates = array(
301            $slug,
302            $object->name ?? '',
303            $object->rest_base ?? '',
304            isset( $object->labels->name ) ? $object->labels->name : '',
305            isset( $object->labels->singular_name ) ? $object->labels->singular_name : '',
306            isset( $object->labels->menu_name ) ? $object->labels->menu_name : '',
307        );
308        foreach ( $candidates as $candidate ) {
309            if ( '' !== $candidate && strtolower( (string) $candidate ) === $needle ) {
310                return $slug;
311            }
312        }
313    }
314
315    return null;
316}
317
318/**
319 * Format a "post type not found" error that gives the LLM enough information
320 * to self-correct on the next turn.
321 *
322 * @param string $input The identifier the caller tried to use.
323 *
324 * @return array Standardized 400 ability response listing valid options.
325 */
326function blu_post_type_not_found_response( string $input ): array {
327    $available = array();
328    foreach ( get_post_types( array(), 'objects' ) as $slug => $object ) {
329        $label       = isset( $object->labels->name ) ? $object->labels->name : $slug;
330        $available[] = sprintf( '%s ("%s")', $slug, $label );
331    }
332    sort( $available );
333
334    return blu_prepare_ability_response(
335        400,
336        sprintf(
337            'Unknown post type "%s". Pass the slug, REST base, or label. Available: %s.',
338            $input,
339            implode( ', ', $available )
340        )
341    );
342}
343
344/**
345 * Slim projection of a WP_Post suitable for list / search responses.
346 *
347 * Returning full WP_Post objects buries the ID in dozens of internal fields
348 * and routinely causes LLMs to drop the ID when summarizing for the user.
349 * This projection puts the id first and limits noise.
350 *
351 * @param WP_Post $post The post to project.
352 *
353 * @return array
354 */
355function blu_project_post_summary( WP_Post $post ): array {
356    return array(
357        'id'       => (int) $post->ID,
358        'title'    => get_the_title( $post ),
359        'status'   => $post->post_status,
360        'type'     => $post->post_type,
361        'slug'     => $post->post_name,
362        'author'   => (int) $post->post_author,
363        'date'     => $post->post_date,
364        'modified' => $post->post_modified,
365        'excerpt'  => has_excerpt( $post ) ? get_the_excerpt( $post ) : wp_trim_words( wp_strip_all_tags( $post->post_content ), 30 ),
366        'link'     => get_permalink( $post ),
367    );
368}
369
370/**
371 * Full projection of a WP_Post â€” summary plus raw content. Used by single-item
372 * fetches (get) and after writes (add/update) where the caller wants to see
373 * what was stored.
374 *
375 * @param WP_Post $post The post to project.
376 *
377 * @return array
378 */
379function blu_project_post_full( WP_Post $post ): array {
380    return array_merge(
381        blu_project_post_summary( $post ),
382        array(
383            'content' => $post->post_content,
384        )
385    );
386}
387
388if ( ! function_exists( 'blu_is_valid_list' ) ) {
389    /**
390     * Check if the list is a simple array
391     *
392     * @param array $list The list
393     *
394     * @return bool
395     */
396    function blu_is_valid_list( $list ) {
397        $i = 0;
398        foreach ( $list as $k => $_ ) {
399            if ( $k !== $i++ ) {
400                return false;
401            }
402        }
403
404        return true;
405    }
406
407}
408
409/**
410 * Check if input array is a valid input
411 *
412 * @param array    $input_value The input.
413 * @param string   $input_name The input name.
414 * @param bool|int $min_items The min amount of items.
415 * @param bool|int $max_items The max amount of items.
416 *
417 * @return WP_Error|bool
418 */
419function blu_is_valid_input_array( $input_value, $input_name, $min_items = false, $max_items = false ) {
420
421    $error = '';
422
423    if ( ! is_array( $input_value ) ) {
424        $error = $input_name . ' must be an array: ' . gettype( $input_value ) . ' given.';
425    }
426
427    if ( $min_items && count( $input_value ) < $min_items ) {
428        $error = $input_name . ' must contain at least ' . $min_items . ' element';
429    }
430
431    if ( $max_items && count( $input_value ) > $max_items ) {
432        $error = $input_name . ' cannot be contain more than ' . $max_items . ' element.';
433
434    }
435
436    if ( ! blu_is_valid_list( $input_value ) ) {
437        $error = $input_name . ' can\'t be an object-shaped array';
438    }
439
440    return '' === $error ? true : new WP_Error( 400, $error );
441}
442
443
444if ( ! function_exists( 'blu_filter_terms_by_patterns' ) ) {
445
446    /**
447     * Filter terms by patterns
448     *
449     * @param array $patterns The patterns
450     * @param array $terms The terms to filter by reference.
451     *
452     * @return void
453     */
454    function blu_filter_terms_by_patterns( $patterns, &$terms ) {
455        if ( count( $patterns ) > 0 ) {
456            $filtered_ids = array();
457            foreach ( $terms as $term ) {
458                if ( ! isset( $term['name'] ) || ! isset( $term['id'] ) || ! is_string( $term['name'] ) ) {
459                    continue;
460                }
461                $term_name = trim( $term['name'] );
462
463                foreach ( $patterns as $pattern ) {
464
465                    if ( @preg_match( $pattern, '' ) !== false ) {
466                        $regex = $pattern;
467                        if ( substr( $regex, - 1 ) !== 'i' ) {
468                            // Ensure case-insensitive
469                            $regex = rtrim( $regex, '/' ) . '/i';
470                        }
471                        if ( preg_match( $regex, $term_name ) ) {
472                            $filtered_ids[] = $term['id'];
473                            break;
474                        }
475                    } elseif ( false !== stripos( $term_name, $pattern ) ) {
476                        $filtered_ids[] = $term['id'];
477                        break;
478                    }
479                }
480            }
481
482            if ( count( $filtered_ids ) > 0 ) {
483                $terms = array_filter(
484                    $terms,
485                    function ( $term ) use ( $filtered_ids ) {
486                        return in_array( $term['id'], $filtered_ids );
487                    }
488                );
489            }
490        }
491    }
492}