PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.3
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.3
1.6.5 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 All 48 releases
fluent-cart / app / Modules / MCP / MCPInit.php

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

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