resolve(); wp_cache_set( $key, $list, self::CACHE_NS, 3600 ); return $list; } public function etag(): string { return md5( (string) wp_json_encode( $this->for_current_user() ) ); } public function flush(): void { wp_cache_flush_group( self::CACHE_NS ); } private function resolve(): array { if ( ! function_exists( 'wp_get_abilities' ) ) { return []; } $allowed = Core::TOOLS; $out = []; foreach ( wp_get_abilities() as $slug => $ability ) { if ( ! in_array( $slug, $allowed, true ) || true !== $ability->check_permissions() ) { continue; } $def = [ 'name' => $slug, 'description' => wp_strip_all_tags( $ability->get_description() ), 'inputSchema' => self::clean_schema( $ability->get_input_schema() ), ]; if ( $ability->get_meta_item( 'webmcp_readonly', false ) ) { $def['annotations'] = [ 'readOnlyHint' => true ]; } $def = apply_filters( 'webmcp_tool_def', $def, $slug, $ability ); if ( apply_filters( 'webmcp_show_tool', true, $slug, $ability ) ) { $out[] = $def; } } return $out; } /** * Normalize a JSON Schema for safe transport: reject deep/ref-using schemas * and ensure empty property bags serialize as {} not []. */ public static function clean_schema( array $s, int $lvl = 0 ): array { $blank = [ 'type' => 'object', 'properties' => new \stdClass() ]; if ( empty( $s ) ) { return $blank; } if ( $lvl > self::MAX_NESTING || isset( $s['$ref'] ) ) { return $blank; } foreach ( [ 'properties', 'items' ] as $branch ) { if ( ! isset( $s[ $branch ] ) || ! is_array( $s[ $branch ] ) ) { continue; } if ( $branch === 'properties' && $s[ $branch ] === [] ) { $s[ $branch ] = new \stdClass(); continue; } foreach ( $s[ $branch ] as $k => $child ) { if ( is_array( $child ) ) { $cleaned = self::clean_schema( $child, $lvl + 1 ); if ( $cleaned === $blank && $lvl === 0 ) { return $blank; } $s[ $branch ][ $k ] = $cleaned; } } } return $s; } }