PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 All 34 releases
fluent-booking / app / Modules / MCP / MCPInit.php

MCPInit.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.5.0, at app/Modules/MCP/MCPInit.php

326 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBooking\App\Modules\MCP;
4
5 use FluentBooking\App\Modules\MCP\Support\PermissionGate;
6 use FluentBooking\App\Modules\MCP\Tools\ContextTools;
7
8 defined('ABSPATH') || exit;
9
10 /**
11 * Bootstrap for the MCP integration.
12 *
13 * Wires the Abilities API (WP 6.9+) to the MCP Adapter, supplied by
14 * FluentToolkit or the standalone mcp-adapter plugin. Shows an admin notice
15 * when neither is loaded.
16 *
17 * Off by default (PermissionGate::isEnabled()). When on, the endpoint needs WP
18 * auth, the transport permission gate and per-ability permission checks.
19 */
20 class MCPInit
21 {
22 const SERVER_ID = 'fluent-booking';
23
24 /**
25 * Toolkit discovery and the settings entry always register, since they are
26 * where the operator finds the switch. The server starts only when enabled.
27 */
28 public static function boot()
29 {
30 self::registerWithToolkit();
31 self::registerSettingsMenu();
32
33 if (PermissionGate::isEnabled()) {
34 (new self())->init();
35 }
36 }
37
38 public function init()
39 {
40 // Fire only on WP 6.9+ or with the Abilities API plugin.
41 add_action('wp_abilities_api_categories_init', [$this, 'registerCategory']);
42 add_action('wp_abilities_api_init', [$this, 'registerAbilities']);
43
44 add_action('admin_init', [$this, 'registerPrivacyPolicyContent']);
45
46 // Fires only when an adapter is loaded.
47 add_action('mcp_adapter_init', [$this, 'registerCustomServer']);
48
49 // Drop get-booking-context's cache when anything it reports changes.
50 // Static callback so a site can remove_action it.
51 $invalidate = [ContextTools::class, 'invalidateCache'];
52
53 foreach ([
54 'fluent_booking/after_create_calendar',
55 'fluent_booking/after_update_calendar',
56 'fluent_booking/after_delete_calendar',
57 'fluent_booking/after_create_calendar_slot',
58 'fluent_booking/after_create_event',
59 'fluent_booking/after_update_event_details',
60 'fluent_booking/after_delete_calendar_event',
61 ] as $hook) {
62 add_action($hook, $invalidate);
63 }
64
65 add_action('admin_notices', [$this, 'maybeShowAdapterNotice']);
66 }
67
68 public function registerCategory()
69 {
70 wp_register_ability_category(AbilitiesRegistrar::CATEGORY, [
71 'label' => __('FluentBooking', 'fluent-booking'),
72 'description' => __('Scheduling abilities for FluentBooking — bookings, availability, event types and reports.', 'fluent-booking'),
73 ]);
74 }
75
76 public function registerAbilities()
77 {
78 AbilitiesRegistrar::register();
79
80 /**
81 * Fires after FluentBooking registers its core MCP abilities. Pro
82 * registers its own here, under the same namespace and server.
83 *
84 * @since 2.3.0
85 */
86 do_action('fluent_booking/mcp_loaded');
87 }
88
89 /**
90 * Register the FluentBooking MCP server at /wp-json/fluent-booking/mcp,
91 * outside fluent-booking/v2 so the admin policy stack doesn't apply.
92 *
93 * @param object $adapter the \WP\MCP\Core\McpAdapter instance
94 */
95 public function registerCustomServer($adapter)
96 {
97 if (!$adapter || !is_object($adapter) || !method_exists($adapter, 'create_server')) {
98 return;
99 }
100
101 $abilityNames = AbilitiesRegistrar::getToolNames();
102
103 /**
104 * Filters the ability names exposed by the FluentBooking MCP server.
105 * Pro and extensions add theirs here to share the server.
106 *
107 * @since 2.3.0
108 *
109 * @param array $abilityNames fully-qualified ability names
110 */
111 $abilityNames = apply_filters('fluent_booking/mcp_ability_names', $abilityNames);
112 $abilityNames = array_values(array_unique(array_filter((array) $abilityNames)));
113
114 // Prompts go in their own argument, not in tools/list.
115 $promptNames = AbilitiesRegistrar::getPromptNames();
116
117 /**
118 * Filters the prompt ability names exposed by the server.
119 *
120 * @since 2.3.0
121 *
122 * @param array $promptNames fully-qualified ability names
123 */
124 $promptNames = apply_filters('fluent_booking/mcp_prompt_names', $promptNames);
125 $promptNames = array_values(array_unique(array_filter((array) $promptNames)));
126
127 $namespace = self::serverNamespace();
128 $route = self::serverRoute();
129
130 $adapter->create_server(
131 self::SERVER_ID,
132 $namespace,
133 $route,
134 __('FluentBooking MCP Server', 'fluent-booking'),
135 __('AI agent tools for FluentBooking bookings, availability, event types and reports.', 'fluent-booking'),
136 defined('FLUENT_BOOKING_VERSION') ? FLUENT_BOOKING_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 $promptNames,
143 [PermissionGate::class, 'transport']
144 );
145 }
146
147 /**
148 * List FluentBooking on FluentToolkit's MCP page, even while MCP is off,
149 * so the operator can switch it on there.
150 */
151 public static function registerWithToolkit()
152 {
153 // Static callbacks, not closures, so a site can remove_filter them.
154 add_filter('fluent_kit/mcp_products', [self::class, 'addToolkitProduct']);
155 add_filter('fluent_kit/mcp_toggle_handlers', [self::class, 'addToolkitToggleHandler']);
156 }
157
158 /**
159 * @param array $products
160 * @return array
161 */
162 public static function addToolkitProduct($products)
163 {
164 if (!is_array($products)) {
165 $products = [];
166 }
167
168 $products[] = [
169 'slug' => self::SERVER_ID,
170 'name' => __('FluentBooking', 'fluent-booking'),
171 'mcp_enabled' => PermissionGate::isEnabled(),
172 'tools_count' => self::toolsCount(),
173 'endpoint_url' => self::getEndpointUrl(),
174 'status' => self::toolkitStatus(),
175 ];
176
177 return $products;
178 }
179
180 /**
181 * @param array $handlers
182 * @return array
183 */
184 public static function addToolkitToggleHandler($handlers)
185 {
186 if (!is_array($handlers)) {
187 $handlers = [];
188 }
189
190 $handlers[self::SERVER_ID] = [
191 'get_enabled' => [PermissionGate::class, 'isEnabled'],
192 'set_enabled' => [PermissionGate::class, 'setEnabled'],
193 ];
194
195 return $handlers;
196 }
197
198 /**
199 * Add the MCP entry to Settings. Registered while off too, since it holds
200 * the switch. Priority 30 puts it after the existing entries.
201 */
202 public static function registerSettingsMenu()
203 {
204 add_filter('fluent_booking/settings_menu_items', [self::class, 'addSettingsMenuItem'], 30);
205 }
206
207 /**
208 * @param array $items
209 * @return array
210 */
211 public static function addSettingsMenuItem($items)
212 {
213 if (!is_array($items)) {
214 $items = [];
215 }
216
217 $items['mcp'] = [
218 'title' => __('MCP for AI Agents', 'fluent-booking'),
219 'disable' => false,
220 'el_icon' => 'MagicStick',
221 'component_type' => 'StandAloneComponent',
222 'class' => 'mcp_settings',
223 'route' => [
224 'name' => 'mcpSettings',
225 ],
226 ];
227
228 return $items;
229 }
230
231 /**
232 * How many tools the server exposes for the enabled toolsets, Pro's
233 * included. Prompts don't count; they aren't in the tool list.
234 *
235 * @return int
236 */
237 public static function toolsCount()
238 {
239 $names = AbilitiesRegistrar::getToolNames();
240
241 $names = apply_filters('fluent_booking/mcp_ability_names', $names);
242
243 return is_array($names) ? count(array_unique($names)) : 0;
244 }
245
246 /**
247 * Status key the Toolkit renders on the FluentBooking card.
248 *
249 * @return string
250 */
251 public static function toolkitStatus()
252 {
253 if (!self::adapterAvailable()) {
254 return 'adapter_required';
255 }
256
257 return PermissionGate::isEnabled() ? 'ready' : 'disabled';
258 }
259
260 /**
261 * Stable endpoint URL for the settings UI and connection-snippet generator.
262 *
263 * @return string
264 */
265 public static function getEndpointUrl()
266 {
267 return get_rest_url(null, trailingslashit(self::serverNamespace()) . self::serverRoute());
268 }
269
270 /**
271 * True when both an MCP adapter and the Abilities API are available.
272 *
273 * @return bool
274 */
275 public static function adapterAvailable()
276 {
277 return defined('WP_MCP_VERSION')
278 && class_exists('\WP\MCP\Core\McpAdapter')
279 && function_exists('wp_register_ability');
280 }
281
282 /**
283 * Suggest privacy-policy wording while MCP is on. The connected client's
284 * model provider receives attendee data, which the site owner must disclose.
285 */
286 public function registerPrivacyPolicyContent()
287 {
288 if (!function_exists('wp_add_privacy_policy_content')) {
289 return;
290 }
291
292 $content = '<p>' . __('This site can expose booking data to AI assistants over the Model Context Protocol. While it is enabled, a connected client authenticates as one WordPress user and can read attendee names, email addresses, phone numbers and booking form answers, and can create, reschedule and cancel bookings, within that account\'s permissions.', 'fluent-booking') . '</p>';
293
294 $content .= '<p>' . __('Data a client reads leaves this site. The provider of the AI assistant is therefore a recipient of that data, and you should name them here. Access is granted per WordPress application password and is revoked by deleting it.', 'fluent-booking') . '</p>';
295
296 wp_add_privacy_policy_content(__('FluentBooking — MCP for AI Agents', 'fluent-booking'), wp_kses_post($content));
297 }
298
299 public function maybeShowAdapterNotice()
300 {
301 if (self::adapterAvailable() || !current_user_can('manage_options')) {
302 return;
303 }
304
305 echo '<div class="notice notice-warning"><p>';
306 echo esc_html__('FluentBooking MCP is enabled but no MCP adapter was found. Install Fluent Toolkit (recommended) or the MCP Adapter plugin, on WordPress 6.9 or newer.', 'fluent-booking');
307 echo '</p></div>';
308 }
309
310 /**
311 * @return string
312 */
313 private static function serverNamespace()
314 {
315 return 'fluent-booking';
316 }
317
318 /**
319 * @return string
320 */
321 private static function serverRoute()
322 {
323 return 'mcp';
324 }
325 }
326