PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.3
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.3
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / includes / core / MCP / AbilitiesRegistrar.php

AbilitiesRegistrar.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.3, at includes/core/MCP/AbilitiesRegistrar.php

292 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AbilitiesRegistrar — single source of truth for WPFunnels abilities.
4 *
5 * Every tool is declared once here (via the Tools\* definition arrays) and
6 * registered with the WordPress Abilities API. The same registry backs both the
7 * MCP server for external agents and the in-plugin copilot, so a new tool needs
8 * no second wiring.
9 *
10 * Every execute callback is wrapped with:
11 * 1. the destructive hard stop — a tool annotated `destructive` refuses to run
12 * without `confirm: true`, for every client, not just our own UI;
13 * 2. an exception guard that converts a thrown error into a WP_Error.
14 *
15 * @package WPFunnels\MCP
16 * @since 3.13.0
17 */
18
19 namespace WPFunnels\MCP;
20
21 defined( 'ABSPATH' ) || exit;
22
23 require_once __DIR__ . '/Helpers/MCPHelper.php';
24 require_once __DIR__ . '/Support/ElementorLayoutBuilder.php';
25 require_once __DIR__ . '/Tools/ContextTools.php';
26 require_once __DIR__ . '/Tools/FunnelTools.php';
27 require_once __DIR__ . '/Tools/StepTools.php';
28 require_once __DIR__ . '/Tools/ProductTools.php';
29 require_once __DIR__ . '/Tools/OptinTools.php';
30 require_once __DIR__ . '/Tools/TemplateTools.php';
31 require_once __DIR__ . '/Tools/PageContentTools.php';
32 require_once __DIR__ . '/Tools/ReportTools.php';
33 require_once __DIR__ . '/Tools/DiagnosticTools.php';
34 require_once __DIR__ . '/Tools/CheckoutTools.php';
35 require_once __DIR__ . '/Tools/OfferTools.php';
36 require_once __DIR__ . '/Tools/SetupWizardTools.php';
37 require_once __DIR__ . '/Tools/LegacyAutomationTools.php';
38
39 use WPFunnels\MCP\Tools\CheckoutTools;
40 use WPFunnels\MCP\Tools\ContextTools;
41 use WPFunnels\MCP\Tools\DiagnosticTools;
42 use WPFunnels\MCP\Tools\FunnelTools;
43 use WPFunnels\MCP\Tools\LegacyAutomationTools;
44 use WPFunnels\MCP\Tools\OfferTools;
45 use WPFunnels\MCP\Tools\OptinTools;
46 use WPFunnels\MCP\Tools\PageContentTools;
47 use WPFunnels\MCP\Tools\ProductTools;
48 use WPFunnels\MCP\Tools\ReportTools;
49 use WPFunnels\MCP\Tools\SetupWizardTools;
50 use WPFunnels\MCP\Tools\StepTools;
51 use WPFunnels\MCP\Tools\TemplateTools;
52
53 /**
54 * Class AbilitiesRegistrar
55 */
56 class AbilitiesRegistrar {
57
58 /**
59 * Ability category all WPFunnels tools belong to.
60 */
61 public const CATEGORY = 'wpfunnels';
62
63 /**
64 * Cached definitions.
65 *
66 * @var array|null
67 */
68 private static $definitions = null;
69
70 /**
71 * All Free ability definitions, keyed by ability name.
72 *
73 * @return array
74 */
75 public static function getDefinitions() {
76 if ( null !== self::$definitions ) {
77 return self::$definitions;
78 }
79
80 $definitions = array_merge(
81 ContextTools::definitions(),
82 FunnelTools::definitions(),
83 StepTools::definitions(),
84 ProductTools::definitions(),
85 OptinTools::definitions(),
86 TemplateTools::definitions(),
87 PageContentTools::definitions(),
88 ReportTools::definitions(),
89 DiagnosticTools::definitions(),
90 CheckoutTools::definitions(),
91 OfferTools::definitions(),
92 SetupWizardTools::definitions(),
93 LegacyAutomationTools::definitions()
94 );
95
96 self::$definitions = apply_filters( 'wpfunnels/mcp_register_tools', $definitions );
97
98 return self::$definitions;
99 }
100
101 /**
102 * Register every ability with the Abilities API.
103 *
104 * @return void
105 */
106 public static function register() {
107 if ( ! function_exists( 'wp_register_ability' ) ) {
108 return;
109 }
110
111 foreach ( self::getDefinitions() as $name => $definition ) {
112 wp_register_ability( $name, self::prepare( $name, $definition ) );
113 }
114 }
115
116 /**
117 * Turn a definition array into the shape WP_Ability expects.
118 *
119 * @param string $name Ability name.
120 * @param array $definition Definition array.
121 * @return array
122 */
123 public static function prepare( $name, $definition ) {
124 $annotations = isset( $definition['annotations'] ) ? (array) $definition['annotations'] : [];
125
126 $prepared = [
127 'label' => isset( $definition['label'] ) ? $definition['label'] : $name,
128 'description' => isset( $definition['description'] ) ? $definition['description'] : '',
129 'category' => self::CATEGORY,
130 'input_schema' => self::normalizeSchema( isset( $definition['input_schema'] ) ? $definition['input_schema'] : ( isset( $definition['parameters'] ) ? $definition['parameters'] : [] ) ),
131 'output_schema' => [ 'type' => 'object' ],
132 'execute_callback' => self::wrapExecuteCallback( $name, $definition ),
133 'permission_callback' => isset( $definition['permission_callback'] )
134 ? $definition['permission_callback']
135 : Helpers\MCPHelper::currentUserCan(),
136 'meta' => [
137 // WP_Ability expects annotations as key => true pairs.
138 'annotations' => array_fill_keys( $annotations, true ),
139 ],
140 ];
141
142 return $prepared;
143 }
144
145 /**
146 * Add the `confirm` flag to any destructive tool's schema so a client can
147 * satisfy the hard stop without guessing the parameter name.
148 *
149 * @param array $schema Input schema.
150 * @param bool $destructive Whether the tool is destructive.
151 * @return array
152 */
153 private static function withConfirmFlag( $schema, $destructive ) {
154 if ( ! $destructive ) {
155 return $schema;
156 }
157
158 if ( ! isset( $schema['properties'] ) || ! is_array( $schema['properties'] ) ) {
159 $schema['properties'] = [];
160 }
161
162 $schema['properties']['confirm'] = [
163 'type' => 'boolean',
164 'description' => 'Must be true to execute. Calling without it returns a preview of what would change.',
165 'default' => false,
166 ];
167
168 return $schema;
169 }
170
171 /**
172 * Wrap an execute callback with the confirmation hard stop and an exception
173 * guard.
174 *
175 * @param string $name Ability name.
176 * @param array $definition Definition array.
177 * @return callable
178 */
179 private static function wrapExecuteCallback( $name, $definition ) {
180 $callback = isset( $definition['execute_callback'] ) ? $definition['execute_callback'] : ( isset( $definition['callback'] ) ? $definition['callback'] : null );
181 $annotations = isset( $definition['annotations'] ) ? (array) $definition['annotations'] : [];
182 $destructive = in_array( 'destructive', $annotations, true );
183
184 return static function ( $input = [] ) use ( $name, $callback, $destructive ) {
185 $input = is_array( $input ) ? $input : [];
186
187 if ( $destructive && empty( $input['confirm'] ) ) {
188 return [
189 'confirmation_required' => true,
190 'tool' => $name,
191 'message' => sprintf(
192 /* translators: %s: tool name. */
193 __( 'This action changes or deletes data and needs confirmation. Re-call %s with "confirm": true to execute it.', 'wpfnl' ),
194 $name
195 ),
196 'pending_action' => $input,
197 ];
198 }
199
200 // Strip the flag so tool bodies never see it: a confirmed call must
201 // behave identically to one that never needed confirmation.
202 unset( $input['confirm'] );
203
204 try {
205 return call_user_func( $callback, $input );
206 } catch ( \Throwable $e ) {
207 /**
208 * Fires when an ability throws.
209 *
210 * @since 3.13.0
211 * @param \Throwable $e The exception.
212 * @param string $name Ability name.
213 * @param array $input Tool input.
214 */
215 do_action( 'wpfunnels/mcp_tool_exception', $e, $name, $input );
216
217 return Helpers\MCPHelper::error(
218 'tool_exception',
219 sprintf(
220 /* translators: 1: tool name, 2: error message. */
221 __( '%1$s failed: %2$s', 'wpfnl' ),
222 $name,
223 $e->getMessage()
224 )
225 );
226 }
227 };
228 }
229
230 /**
231 * Normalize a JSON schema for transport.
232 *
233 * Recursively converts empty `properties` arrays to objects so they
234 * serialize as `{}` rather than `[]` — OpenAI's strict function schema
235 * rejects the array form.
236 *
237 * @param array $schema Input schema.
238 * @return array
239 */
240 public static function normalizeSchema( $schema ) {
241 if ( ! is_array( $schema ) ) {
242 return [ 'type' => 'object' ];
243 }
244
245 if ( isset( $schema['properties'] ) ) {
246 if ( is_array( $schema['properties'] ) && empty( $schema['properties'] ) ) {
247 $schema['properties'] = new \stdClass();
248 } elseif ( is_array( $schema['properties'] ) ) {
249 foreach ( $schema['properties'] as $key => $property ) {
250 $schema['properties'][ $key ] = self::normalizeSchema( $property );
251 }
252 }
253 }
254
255 if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
256 $schema['items'] = self::normalizeSchema( $schema['items'] );
257 }
258
259 return $schema;
260 }
261
262 /**
263 * Definitions with the `confirm` flag folded into destructive schemas.
264 *
265 * Used by both the registrar and the copilot's tool gateway so both surfaces
266 * advertise the same parameters.
267 *
268 * @return array
269 */
270 public static function getPublicDefinitions() {
271 $public = [];
272
273 foreach ( self::getDefinitions() as $name => $definition ) {
274 $annotations = isset( $definition['annotations'] ) ? (array) $definition['annotations'] : [];
275 $destructive = in_array( 'destructive', $annotations, true );
276
277 $public[ $name ] = [
278 'name' => $name,
279 'label' => isset( $definition['label'] ) ? $definition['label'] : $name,
280 'description' => isset( $definition['description'] ) ? $definition['description'] : '',
281 'input_schema' => self::normalizeSchema(
282 self::withConfirmFlag( isset( $definition['input_schema'] ) ? $definition['input_schema'] : [], $destructive )
283 ),
284 'destructive' => $destructive,
285 'annotations' => $annotations,
286 ];
287 }
288
289 return $public;
290 }
291 }
292