PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk 1.2.0 All 47 releases
fluent-cart / app / Modules / MCP / MCPInit.php

MCPInit.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Modules/MCP/MCPInit.php

290 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\MCP;
4
5 use FluentCart\App\Modules\MCP\Support\CrmContact;
6 use FluentCart\App\Modules\MCP\Support\PermissionGate;
7 use FluentCart\App\Modules\MCP\Tools\ContextTools;
8
9 /**
10 * Bootstrap for FluentCart's Model Context Protocol (MCP) integration.
11 *
12 * Wires the WordPress Abilities API (core 6.9+) + the WP MCP Adapter, which is
13 * provided by FluentHub (bundled) or the standalone mcp-adapter plugin —
14 * whichever is present. FluentCart bundles nothing; it consumes whatever's
15 * loaded. If neither is available we surface an admin notice rather than fail
16 * silently.
17 *
18 * The whole surface is gated behind the `mcp_enabled` option (default off) — a
19 * store owner turns it on in Settings → MCP and creates an application
20 * password. Even when on, the endpoint stays behind WP auth + a FluentCart
21 * role (transport gate) + per-ability permission checks.
22 *
23 * Instantiated from app/Hooks/actions.php under a `function_exists` +
24 * `PermissionGate::isEnabled()` guard.
25 */
26 class MCPInit
27 {
28 const SERVER_ID = 'fluent-cart';
29
30 /**
31 * Bootstrap entry point, called once from app/Hooks/actions.php.
32 *
33 * MCP ships OFF; it's enabled in Settings → Features & addon → MCP. The
34 * server itself is instantiated only when enabled, so there is zero overhead
35 * by default. The Abilities-API / MCP-Adapter hooks inside init() fire only
36 * when those systems are present (WP 6.9+ and FluentHub / mcp-adapter).
37 *
38 * Toolkit discovery and the settings card are registered UNCONDITIONALLY:
39 * FluentHub needs to list FluentCart on its MCP page even while disabled
40 * so the operator can toggle it on, and the card must be reachable to flip
41 * the switch. Both are lightweight add_filter calls — no-ops unless applied.
42 */
43 public static function boot()
44 {
45 self::registerWithToolkit();
46 self::registerModuleSettings();
47
48 if (PermissionGate::isEnabled()) {
49 (new self())->init();
50 }
51 }
52
53 public function init()
54 {
55 // Abilities API hooks (fire only on WP 6.9+).
56 add_action('wp_abilities_api_categories_init', [$this, 'registerCategory']);
57 add_action('wp_abilities_api_init', [$this, 'registerAbilities']);
58
59 // Server registration (fires only when an adapter is loaded).
60 add_action('mcp_adapter_init', [$this, 'registerCustomServer']);
61
62 // Keep get-store-context fresh: invalidate its cache when anything it
63 // reports changes. One array payload per hook (coding-rule friendly).
64 $invalidate = [ContextTools::class, 'invalidateCache'];
65 foreach ([
66 'fluent_cart/coupon_created',
67 'fluent_cart/coupon_updated',
68 'fluent_cart/label_created',
69 'fluent_cart/label_updated',
70 'fluent_cart/store_settings_saved',
71 'fluent_cart/payment_settings_saved',
72 ] as $hook) {
73 add_action($hook, $invalidate);
74 }
75
76 // FluentCRM contact context on get-customer / get-order, mirroring the
77 // contact widget FluentCRM already renders on those admin screens.
78 // No-op unless FluentCRM is active.
79 CrmContact::register();
80
81 // Warn the operator if they enabled MCP but no adapter is installed.
82 add_action('admin_notices', [$this, 'maybeShowAdapterNotice']);
83 }
84
85 public function registerCategory()
86 {
87 wp_register_ability_category('fluent-cart', [
88 'label' => __('FluentCart', 'fluent-cart'),
89 'description' => __('Commerce abilities for FluentCart — orders, customers, products, subscriptions, coupons, and reports.', 'fluent-cart'),
90 ]);
91 }
92
93 public function registerAbilities()
94 {
95 AbilitiesRegistrar::register();
96
97 /**
98 * Fires after FluentCart registers its core MCP abilities. FluentCart
99 * Pro hooks this to register its own abilities (licenses, advanced
100 * inventory) under the same `fluent-cart/` namespace.
101 *
102 * @since 1.0.0
103 */
104 do_action('fluent_cart/mcp_loaded');
105 }
106
107 /**
108 * Register the dedicated FluentCart MCP server. Endpoint defaults to
109 * /wp-json/fluent-cart/mcp (sibling to, but distinct from, the admin REST
110 * namespace so it doesn't get caught by that policy stack).
111 *
112 * @param object $adapter The \WP\MCP\Core\McpAdapter instance.
113 */
114 public function registerCustomServer($adapter)
115 {
116 if (!$adapter || !is_object($adapter) || !method_exists($adapter, 'create_server')) {
117 return;
118 }
119
120 $abilityNames = array_keys(AbilitiesRegistrar::getDefinitions());
121
122 /**
123 * Filter the ability names exposed by the FluentCart MCP server. Pro
124 * and extensions push their ability names here.
125 *
126 * @since 1.0.0
127 *
128 * @param array $abilityNames Fully-qualified ability names.
129 */
130 $abilityNames = apply_filters('fluent_cart/mcp_ability_names', $abilityNames);
131 $abilityNames = array_values(array_unique(array_filter((array) $abilityNames)));
132
133 $namespace = apply_filters('fluent_cart/mcp_server_namespace', 'fluent-cart');
134 $route = apply_filters('fluent_cart/mcp_server_route', 'mcp');
135
136 $adapter->create_server(
137 self::SERVER_ID,
138 $namespace,
139 $route,
140 __('FluentCart MCP Server', 'fluent-cart'),
141 __('AI agent tools for FluentCart orders, customers, products, subscriptions, and reports.', 'fluent-cart'),
142 defined('FLUENTCART_VERSION') ? FLUENTCART_VERSION : '1.0.0',
143 ['\WP\MCP\Transport\HttpTransport'],
144 '\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler',
145 '\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler',
146 $abilityNames,
147 [],
148 [],
149 [PermissionGate::class, 'transport']
150 );
151 }
152
153 /**
154 * Announce FluentCart to FluentHub's MCP page (Settings → MCP).
155 *
156 * FluentHub hardcodes FluentCRM but discovers every other product
157 * through the `fluent_kit/mcp_products` + `fluent_kit/mcp_toggle_handlers`
158 * filters. Without these, FluentCart's server is fully functional yet never
159 * appears in the Toolkit's list — which is exactly the symptom here.
160 *
161 * Runs UNCONDITIONALLY (even when MCP is OFF) so the operator can see the
162 * card and flip it on from the Toolkit; the toggle handler maps that switch
163 * onto our `mcp_enabled` option. Both filters are cheap no-ops unless the
164 * Toolkit actually applies them, so there's no cost when it's absent.
165 */
166 public static function registerWithToolkit()
167 {
168 add_filter('fluent_kit/mcp_products', function ($products) {
169 if (!is_array($products)) {
170 $products = [];
171 }
172
173 $products[] = [
174 'slug' => self::SERVER_ID,
175 'name' => __('FluentCart', 'fluent-cart'),
176 'mcp_enabled' => PermissionGate::isEnabled(),
177 'tools_count' => self::toolsCount(),
178 'endpoint_url' => self::getEndpointUrl(),
179 'status' => self::toolkitStatus(),
180 ];
181
182 return $products;
183 });
184
185 add_filter('fluent_kit/mcp_toggle_handlers', function ($handlers) {
186 if (!is_array($handlers)) {
187 $handlers = [];
188 }
189
190 $handlers[self::SERVER_ID] = [
191 'get_enabled' => [PermissionGate::class, 'isEnabled'],
192 'set_enabled' => function ($enabled) {
193 return PermissionGate::setEnabled($enabled);
194 },
195 ];
196
197 return $handlers;
198 });
199 }
200
201 /**
202 * Register the MCP card on Settings → Features & addon. Runs UNCONDITIONALLY
203 * (even when MCP is OFF) so the operator can find it and turn it on. The card
204 * renders the McpSettings.vue component, which drives the settings/mcp* REST
205 * endpoints (instant toggle + connection helpers) rather than the generic
206 * module-settings save — though the on/off flag itself lives under the `mcp`
207 * key of the shared modules blob (see PermissionGate::isEnabled/setEnabled).
208 */
209 public static function registerModuleSettings()
210 {
211 // Priority 100 so MCP is appended AFTER the other modules (which all
212 // register at the default priority 10), keeping it at the end of the
213 // Features & addon list rather than in the middle.
214 add_filter('fluent_cart/module_setting/fields', function ($fields) {
215 if (!is_array($fields)) {
216 $fields = [];
217 }
218 $fields['mcp'] = [
219 'title' => __('MCP for AI Agents', 'fluent-cart'),
220 'description' => __('Let AI assistants (Claude, Cursor, and other MCP clients) securely read your store and run operator tasks via the Model Context Protocol. Ships off; enable it and connect with an application password.', 'fluent-cart'),
221 'type' => 'component',
222 'component' => 'McpSettings',
223 ];
224 return $fields;
225 }, 100);
226
227 // Default the `mcp.active` flag to off so the shared modules blob always
228 // carries a structured value — the McpSettings.vue model stays in sync
229 // with it, so the generic "Save Settings" can't clobber the toggle.
230 add_filter('fluent_cart/module_setting/default_values', function ($defaults) {
231 if (!is_array($defaults)) {
232 $defaults = [];
233 }
234 $defaults['mcp'] = ['active' => 'no'];
235 return $defaults;
236 });
237 }
238
239 /**
240 * Count of abilities the server exposes, including any pushed by Pro via the
241 * `fluent_cart/mcp_ability_names` filter. Mirrors how the Toolkit counts
242 * FluentCRM's tools.
243 */
244 public static function toolsCount()
245 {
246 $names = array_keys(AbilitiesRegistrar::getDefinitions());
247 $names = apply_filters('fluent_cart/mcp_ability_names', $names);
248
249 return is_array($names) ? count(array_unique($names)) : 0;
250 }
251
252 /** Status key the Toolkit renders on the FluentCart card. */
253 public static function toolkitStatus()
254 {
255 if (!self::adapterAvailable()) {
256 return 'adapter_required';
257 }
258
259 return PermissionGate::isEnabled() ? 'ready' : 'disabled';
260 }
261
262 /** Stable endpoint URL for the Settings UI + connection-snippet generator. */
263 public static function getEndpointUrl()
264 {
265 $namespace = apply_filters('fluent_cart/mcp_server_namespace', 'fluent-cart');
266 $route = apply_filters('fluent_cart/mcp_server_route', 'mcp');
267
268 return get_rest_url(null, trailingslashit($namespace) . $route);
269 }
270
271 /** True when an MCP adapter + the Abilities API are both available. */
272 public static function adapterAvailable()
273 {
274 return defined('WP_MCP_VERSION')
275 && class_exists('\WP\MCP\Core\McpAdapter')
276 && function_exists('wp_register_ability');
277 }
278
279 public function maybeShowAdapterNotice()
280 {
281 if (self::adapterAvailable() || !current_user_can('manage_options')) {
282 return;
283 }
284
285 echo '<div class="notice notice-warning"><p>';
286 echo esc_html__('FluentCart MCP is enabled but no MCP adapter was found. Install FluentHub (recommended) or the MCP Adapter plugin, on WordPress 6.9+.', 'fluent-cart');
287 echo '</p></div>';
288 }
289 }
290