PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.1
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.1
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 2.8.17 2.8.18 All 259 releases
wpfunnels / includes / core / MCP / MCPInit.php

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

283 lines 9.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCPInit — bootstraps the WPFunnels MCP surface.
4 *
5 * Responsibilities:
6 * - Register the "wpfunnels" ability category.
7 * - Register all Free abilities via AbilitiesRegistrar, then fire
8 * `wpfunnels/mcp_loaded` so Pro can add its own.
9 * - Create the MCP server at POST /wp-json/wpfunnels/mcp.
10 * - Invalidate the cached install context when funnels or steps change.
11 *
12 * The adapter is shared with any other plugin that ships it (Mail Mint does), so
13 * boot defensively: if the classes are missing or too old, skip the server
14 * rather than fatal, and leave the abilities registered for the copilot.
15 *
16 * @package WPFunnels\MCP
17 * @since 3.13.0
18 */
19
20 namespace WPFunnels\MCP;
21
22 defined( 'ABSPATH' ) || exit;
23
24 use WPFunnels\MCP\Tools\ContextTools;
25 use WPFunnels\MCP\Observability\FunnelMcpObservabilityHandler;
26
27 /**
28 * Class MCPInit
29 */
30 class MCPInit {
31
32 /**
33 * Server identifier.
34 */
35 private const SERVER_ID = 'wpfunnels';
36
37 /**
38 * Wire up all hooks.
39 *
40 * Ability registration always runs (whenever the Abilities API is present):
41 * the in-plugin AI copilot calls tools through the SAME registry
42 * (ToolGateway -> wp_get_abilities()) as the external MCP server, so it
43 * needs abilities registered even on a site that has turned the external
44 * MCP endpoint off. `$expose_server` gates ONLY the external-agent surface
45 * (the MCP REST server) — the `_wpfnl_mcp_enabled` option is about
46 * whether third-party agents can reach this site, not whether the
47 * copilot has tools to call.
48 *
49 * @param bool $expose_server Whether to also boot the external MCP server.
50 * @return void
51 */
52 public function init( $expose_server = true ) {
53 add_action( 'wp_abilities_api_categories_init', [ $this, 'registerCategory' ] );
54 add_action( 'wp_abilities_api_init', [ $this, 'registerAbilities' ] );
55
56 if ( $expose_server ) {
57 add_action( 'mcp_adapter_init', [ $this, 'registerServer' ] );
58
59 // The adapter package is PSR-4 only: vendoring it does not boot it, and
60 // nothing fires `mcp_adapter_init` until someone asks for the singleton.
61 // It self-hooks on init/rest_api_init and guards against double
62 // initialization, so calling this is safe even when another plugin
63 // (Mail Mint ships the same package) has already done it.
64 $this->bootAdapter();
65 }
66
67 // Context cache invalidation — funnel/step mutations change what the
68 // model is told this site contains. Needed regardless of server exposure.
69 $invalidate = [ ContextTools::class, 'invalidateCache' ];
70 foreach ( [ 'wpfunnels_after_funnel_creation', 'wpfunnels_after_step_creation', 'wpfunnels/ai/funnel_created' ] as $hook ) {
71 add_action( $hook, $invalidate );
72 }
73 }
74
75 /**
76 * Ask the adapter for its singleton so it schedules its own bootstrap.
77 *
78 * @return void
79 */
80 private function bootAdapter() {
81 if ( ! self::adapterAvailable() ) {
82 return;
83 }
84
85 try {
86 \WP\MCP\Core\McpAdapter::instance();
87 } catch ( \Throwable $e ) {
88 do_action( 'wpfunnels/mcp_server_error', $e );
89 }
90 }
91
92 /**
93 * Whether the Abilities API is available on this WordPress version.
94 *
95 * @return bool
96 */
97 public static function abilitiesApiAvailable() {
98 return function_exists( 'wp_register_ability' ) && function_exists( 'wp_register_ability_category' );
99 }
100
101 /**
102 * Whether a usable MCP adapter is loaded.
103 *
104 * Another plugin may have loaded an older copy of the package first, so check
105 * for the specific entry point we call rather than assuming the version.
106 *
107 * @return bool
108 */
109 public static function adapterAvailable() {
110 return class_exists( '\WP\MCP\Core\McpAdapter' )
111 && class_exists( '\WP\MCP\Transport\HttpTransport' )
112 && method_exists( '\WP\MCP\Core\McpAdapter', 'create_server' );
113 }
114
115 /**
116 * Register the ability category.
117 *
118 * @return void
119 */
120 public function registerCategory() {
121 if ( ! function_exists( 'wp_register_ability_category' ) ) {
122 return;
123 }
124
125 wp_register_ability_category(
126 AbilitiesRegistrar::CATEGORY,
127 [
128 'label' => __( 'WPFunnels', 'wpfnl' ),
129 'description' => __( 'Funnel, step, offer and analytics abilities for WPFunnels.', 'wpfnl' ),
130 ]
131 );
132 }
133
134 /**
135 * Register Free abilities, then let Pro register its own.
136 *
137 * @return void
138 */
139 public function registerAbilities() {
140 AbilitiesRegistrar::register();
141
142 /**
143 * Fires once the Free abilities are registered.
144 *
145 * Pro (and third parties) register their own tools here and append the
146 * names via the `wpfunnels/mcp_ability_names` filter.
147 *
148 * @since 3.13.0
149 */
150 do_action( 'wpfunnels/mcp_loaded' );
151 }
152
153 /**
154 * Create the MCP server.
155 *
156 * @param object $adapter McpAdapter instance passed by the adapter bootstrap.
157 * @return void
158 */
159 public function registerServer( $adapter ) {
160 if ( ! is_object( $adapter ) || ! method_exists( $adapter, 'create_server' ) ) {
161 return;
162 }
163
164 /**
165 * Filter which abilities the MCP server exposes.
166 *
167 * @since 3.13.0
168 * @param array $ability_names Ability names.
169 */
170 $ability_names = apply_filters(
171 'wpfunnels/mcp_ability_names',
172 array_keys( AbilitiesRegistrar::getDefinitions() )
173 );
174
175 /** Namespace of the MCP endpoint. */
176 $namespace = apply_filters( 'wpfunnels/mcp_server_namespace', 'wpfunnels' );
177 /** Route of the MCP endpoint. */
178 $route = apply_filters( 'wpfunnels/mcp_server_route', 'mcp' );
179
180 /**
181 * Filter the observability handler.
182 *
183 * Deliberately not the Null handler: this surface can delete funnels and
184 * flip live traffic, so calls are logged by default.
185 *
186 * FunnelMcpObservabilityHandler is the adapter's error-log sink minus the
187 * events it emits while building the server — those fire on every REST
188 * request and would otherwise flood the site's PHP error log.
189 *
190 * @since 3.13.0
191 * @param string $handler Fully-qualified handler class.
192 */
193 $observability = apply_filters(
194 'wpfunnels/mcp_observability_handler',
195 FunnelMcpObservabilityHandler::class
196 );
197
198 try {
199 $adapter->create_server(
200 self::SERVER_ID,
201 $namespace,
202 $route,
203 __( 'WPFunnels MCP Server', 'wpfnl' ),
204 self::serverDescription(),
205 defined( 'WPFNL_VERSION' ) ? WPFNL_VERSION : '1.0.0',
206 [ '\WP\MCP\Transport\HttpTransport' ],
207 '\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler',
208 $observability,
209 array_values( array_unique( array_filter( (array) $ability_names ) ) ),
210 [], // Resources.
211 [], // Prompts.
212 [ $this, 'checkTransportPermission' ]
213 );
214 } catch ( \Throwable $e ) {
215 // A shared adapter of an unexpected shape must not take the site down.
216 do_action( 'wpfunnels/mcp_server_error', $e );
217 }
218 }
219
220 /**
221 * Server description — also returned verbatim as the MCP `initialize`
222 * response's `instructions` field (see InitializeHandler), so this is
223 * the one piece of guidance every MCP client sees regardless of which
224 * agent or system prompt is driving it. Keep the funnel-build policy
225 * here in sync with SystemPrompt::basePrompt()'s hard rule — that one
226 * only reaches the in-plugin copilot, this one reaches everyone else.
227 *
228 * @return string
229 */
230 private static function serverDescription() {
231 return __(
232 'AI agent tools for WPFunnels funnels, steps, offers and products. ' .
233 'When asked to build or create a funnel: do not call wpfunnels/create-funnel first. ' .
234 'Call wpfunnels/list-funnels to check for a similar existing funnel to mirror, then ' .
235 'wpfunnels/list-funnel-templates to look for a real matching template — each template ' .
236 'includes an actual page-builder design for every step, not a blank shell. If one fits, ' .
237 'call wpfunnels/import-funnel-template instead of create-funnel/create-step; it creates the ' .
238 'funnel and clones the real design in one call. Only hand-build with create-funnel + ' .
239 'create-step when no template matches or the user explicitly asks for a from-scratch build. ' .
240 'Either way, finish the job: assign-products-to-step, upsert-order-bump, and set-offer-routing ' .
241 'for any accept/reject branching the user described, before proposing to publish.',
242 'wpfnl'
243 );
244 }
245
246 /**
247 * Transport-level capability floor.
248 *
249 * The adapter's own default is only `read`, which any subscriber has. This
250 * floor is defence in depth — the real gate is each ability's own
251 * permission_callback.
252 *
253 * @param \WP_REST_Request $request Incoming request.
254 * @return bool
255 */
256 public function checkTransportPermission( $request ) {
257 /** Capability required to reach the MCP endpoint at all. */
258 $capability = apply_filters( 'wpfunnels/mcp_transport_capability', 'wpf_manage_funnels', $request );
259 $allowed = current_user_can( $capability ) || current_user_can( 'manage_options' );
260
261 /**
262 * Filter the final transport-level allow decision.
263 *
264 * @since 3.13.0
265 * @param bool $allowed Whether the request may reach the server.
266 * @param \WP_REST_Request $request The incoming request.
267 */
268 return (bool) apply_filters( 'wpfunnels/mcp_transport_allowed', $allowed, $request );
269 }
270
271 /**
272 * Public endpoint URL, for the settings screen.
273 *
274 * @return string
275 */
276 public static function endpointUrl() {
277 $namespace = apply_filters( 'wpfunnels/mcp_server_namespace', 'wpfunnels' );
278 $route = apply_filters( 'wpfunnels/mcp_server_route', 'mcp' );
279
280 return rest_url( trailingslashit( $namespace ) . $route );
281 }
282 }
283