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 / Http / Controllers / McpController.php

McpController.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.5.0, at app/Http/Controllers/McpController.php

329 lines 11.3 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\Http\Controllers;
4
5 use FluentBooking\App\Modules\MCP\AbilitiesRegistrar;
6 use FluentBooking\App\Modules\MCP\MCPInit;
7 use FluentBooking\App\Modules\MCP\Support\PermissionGate;
8 use FluentBooking\App\Modules\MCP\Tools\ContextTools;
9 use FluentBooking\Framework\Http\Request\Request;
10 use FluentBooking\Framework\Support\Arr;
11
12 /**
13 * Settings → MCP for AI Agents.
14 *
15 * SettingsPolicy requires `manage_all_data`. Writes also need `manage_options`,
16 * since enabling MCP exposes booking data to any application-password client.
17 * The read endpoint reports `can_manage` so the UI can disable the controls.
18 */
19 class McpController extends Controller
20 {
21 /**
22 * "FluentHub" in the UI. It can bundle the MCP adapter; the standalone
23 * MCP Adapter plugin is the other accepted provider.
24 */
25 const TOOLKIT_PLUGIN_FILE = 'fluent-toolkit/fluent-toolkit.php';
26
27 const ADAPTER_PLUGIN_FILE = 'mcp-adapter/mcp-adapter.php';
28
29 const TOOLKIT_DOWNLOAD_URL = 'https://github.com/WPManageNinja/fluent-toolkit';
30
31 public function getSettings(Request $request)
32 {
33 return array_merge([
34 'settings' => [
35 'enabled' => PermissionGate::isEnabled(),
36 'toolsets' => PermissionGate::enabledToolsets(),
37 ],
38 'available_toolsets' => self::withCosts(PermissionGate::availableToolsets()),
39 'tools_count' => MCPInit::toolsCount(),
40 'can_manage' => current_user_can('manage_options'),
41 ], self::statusFields());
42 }
43
44 public function updateSettings(Request $request)
45 {
46 if (!current_user_can('manage_options')) {
47 return $this->sendError([
48 'message' => __('Only site administrators can change MCP settings.', 'fluent-booking'),
49 ], 403);
50 }
51
52 $data = $request->get('settings', []);
53
54 if (!is_array($data)) {
55 $data = [];
56 }
57
58 // Absent keys keep their stored value, so a partial POST can't switch
59 // the server off. Arr::isTrue(), not a (bool) cast: the body is
60 // form-encoded and `(bool) "false"` is true.
61 $enabled = array_key_exists('enabled', $data)
62 ? (bool) Arr::isTrue($data, 'enabled')
63 : PermissionGate::isEnabled();
64
65 $toolsets = isset($data['toolsets']) && is_array($data['toolsets'])
66 ? $data['toolsets']
67 : PermissionGate::enabledToolsets();
68
69 // Toolsets first, so the server is never live with a stale toolset list.
70 PermissionGate::setToolsets($toolsets);
71 PermissionGate::setEnabled($enabled);
72
73 // get-booking-context's cached output depends on the enabled toolsets.
74 ContextTools::invalidateCache();
75
76 return [
77 'message' => __('MCP settings have been updated', 'fluent-booking'),
78 'settings' => [
79 'enabled' => PermissionGate::isEnabled(),
80 'toolsets' => PermissionGate::enabledToolsets(),
81 ],
82 'tools_count' => MCPInit::toolsCount(),
83 ];
84 }
85
86 /**
87 * Install FluentHub, which carries the MCP adapter.
88 *
89 * FluentBooking has no installer of its own: the download runs on
90 * `fluent_toolkit/do_auto_install`, which a paid tier registers. Without a
91 * handler the endpoint returns the GitHub link for a manual install.
92 */
93 public function installAdapter(Request $request)
94 {
95 if (!current_user_can('install_plugins')) {
96 return $this->sendError([
97 'message' => __('Sorry! You do not have permission to install plugins.', 'fluent-booking'),
98 ], 403);
99 }
100
101 $canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
102
103 if (!$canAutoInstall) {
104 return $this->sendError([
105 'message' => __('Please install FluentHub from GitHub, then reload this page to connect FluentBooking with AI agents.', 'fluent-booking'),
106 'toolkit_download_url' => self::TOOLKIT_DOWNLOAD_URL,
107 ], 422);
108 }
109
110 do_action('fluent_toolkit/do_auto_install');
111
112 wp_clean_plugins_cache();
113
114 $status = self::statusFields();
115
116 if (Arr::get($status, 'adapter_active')) {
117 $message = __('FluentHub is installed and connected. FluentBooking is now reachable by AI agents.', 'fluent-booking');
118 } elseif (Arr::get($status, 'toolkit_active')) {
119 $message = __('FluentHub is installed and active, but its MCP adapter is not available yet. Update FluentHub to the MCP-ready build.', 'fluent-booking');
120 } elseif (Arr::get($status, 'toolkit_installed')) {
121 $message = __('FluentHub was installed but could not be activated automatically. Activate it from the Plugins page.', 'fluent-booking');
122 } else {
123 $message = __('FluentHub could not be installed automatically. Please install it from GitHub.', 'fluent-booking');
124 }
125
126 return array_merge(['message' => $message], $status);
127 }
128
129 /**
130 * Adapter and FluentHub state, shared by the read and install endpoints.
131 *
132 * @return array
133 */
134 private static function statusFields()
135 {
136 $standaloneInstalled = self::isPluginPresent(self::ADAPTER_PLUGIN_FILE);
137 $standaloneActive = self::isPluginActive(self::ADAPTER_PLUGIN_FILE);
138 $toolkitInstalled = self::isToolkitPresent();
139 $toolkitLoaded = self::isToolkitLoaded();
140 $adapterAvailable = MCPInit::adapterAvailable();
141
142 // Which supplier is actually serving the adapter runtime.
143 if ($standaloneActive && $adapterAvailable) {
144 $provider = 'plugin';
145 } elseif ($toolkitLoaded && $adapterAvailable) {
146 $provider = 'toolkit';
147 } else {
148 $provider = '';
149 }
150
151 $currentUser = wp_get_current_user();
152
153 return [
154 'endpoint_url' => MCPInit::getEndpointUrl(),
155 'adapter_available' => $adapterAvailable,
156 'adapter_active' => $adapterAvailable,
157 // Any provider on disk (standalone adapter counts even when inactive).
158 'adapter_installed' => $adapterAvailable || $standaloneInstalled || $toolkitInstalled,
159 'adapter_provider' => $provider,
160 'adapter_version' => self::detectPluginVersion(self::ADAPTER_PLUGIN_FILE),
161 'standalone_adapter_installed' => $standaloneInstalled,
162 'toolkit_installed' => $toolkitInstalled,
163 'toolkit_active' => $toolkitLoaded,
164 'toolkit_version' => self::detectToolkitVersion(),
165 'can_install_adapter' => current_user_can('install_plugins'),
166 'can_auto_install_adapter' => (bool) apply_filters('fluent_toolkit/can_auto_install', false),
167 'toolkit_download_url' => self::TOOLKIT_DOWNLOAD_URL,
168 'app_password_url' => admin_url('profile.php#application-passwords-section'),
169 'plugins_url' => admin_url('plugins.php'),
170 'current_user_login' => $currentUser ? $currentUser->user_login : '',
171 'is_local_dev' => self::detectLocalDevEnvironment(),
172 'pro_active' => defined('FLUENT_BOOKING_PRO_DIR_FILE'),
173 ];
174 }
175
176 /**
177 * FluentHub loaded in this request or installed on disk.
178 *
179 * @return bool
180 */
181 private static function isToolkitPresent()
182 {
183 return self::isToolkitLoaded() || self::isPluginPresent(self::TOOLKIT_PLUGIN_FILE);
184 }
185
186 /**
187 * @return bool
188 */
189 private static function isToolkitLoaded()
190 {
191 return defined('FLUENT_TOOLKIT_VERSION');
192 }
193
194 /**
195 * @return string|null
196 */
197 private static function detectToolkitVersion()
198 {
199 if (self::isToolkitLoaded()) {
200 return (string) FLUENT_TOOLKIT_VERSION;
201 }
202
203 return self::detectPluginVersion(self::TOOLKIT_PLUGIN_FILE);
204 }
205
206 /**
207 * @param string $pluginFile
208 *
209 * @return bool
210 */
211 private static function isPluginPresent($pluginFile)
212 {
213 return isset(self::installedPlugins()[$pluginFile]);
214 }
215
216 /**
217 * @param string $pluginFile
218 *
219 * @return bool
220 */
221 private static function isPluginActive($pluginFile)
222 {
223 if (!function_exists('is_plugin_active')) {
224 require_once ABSPATH . 'wp-admin/includes/plugin.php';
225 }
226
227 return is_plugin_active($pluginFile);
228 }
229
230 /**
231 * @param string $pluginFile
232 *
233 * @return string|null
234 */
235 private static function detectPluginVersion($pluginFile)
236 {
237 $plugins = self::installedPlugins();
238
239 if (!isset($plugins[$pluginFile])) {
240 return null;
241 }
242
243 return isset($plugins[$pluginFile]['Version']) ? $plugins[$pluginFile]['Version'] : null;
244 }
245
246 /**
247 * @return array
248 */
249 private static function installedPlugins()
250 {
251 if (!function_exists('get_plugins')) {
252 require_once ABSPATH . 'wp-admin/includes/plugin.php';
253 }
254
255 return get_plugins();
256 }
257
258 /**
259 * Guess whether this is a local dev host, to offer the self-signed TLS
260 * override in the Claude Desktop snippet. Filterable for unusual TLDs.
261 *
262 * @return bool
263 */
264 private static function detectLocalDevEnvironment()
265 {
266 $host = strtolower((string) wp_parse_url(home_url(), PHP_URL_HOST));
267 $isDev = false;
268
269 // Not .dev: it is a real public TLD and must never get the TLS-bypass hint.
270 $devTlds = ['.test', '.lab', '.local', '.localhost', '.docker'];
271
272 foreach ($devTlds as $tld) {
273 $len = strlen($tld);
274
275 if ($len > 0 && substr($host, -$len) === $tld) {
276 $isDev = true;
277 break;
278 }
279 }
280
281 if (!$isDev && ($host === 'localhost' || $host === '127.0.0.1' || $host === '::1')) {
282 $isDev = true;
283 }
284
285 if (!$isDev && filter_var($host, FILTER_VALIDATE_IP)) {
286 $isPrivate = !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
287
288 if ($isPrivate) {
289 $isDev = true;
290 }
291 }
292
293 return (bool) apply_filters('fluent_booking/mcp_is_local_dev', $isDev, $host);
294 }
295
296 /**
297 * Attach each toolset's tool count and rough context cost, since every
298 * enabled tool definition sits in the agent's context all session.
299 * AbilitiesRegistrar does the measuring so this and check-mcp-budget.php agree.
300 *
301 * @param array $toolsets
302 *
303 * @return array
304 */
305 private static function withCosts($toolsets)
306 {
307 foreach ($toolsets as $key => $meta) {
308 $definitions = AbilitiesRegistrar::getDefinitions([$key]);
309
310 // Prompts aren't in tools/list and load only when run.
311 $definitions = array_filter($definitions, function ($definition) {
312 return empty($definition['is_prompt']);
313 });
314
315 $bytes = 0;
316
317 foreach ($definitions as $name => $definition) {
318 $bytes += AbilitiesRegistrar::wireBytes($name, $definition);
319 }
320
321 $toolsets[$key]['tools_count'] = count($definitions);
322 $toolsets[$key]['approx_tokens'] = AbilitiesRegistrar::wireTokens($bytes);
323 }
324
325 return $toolsets;
326 }
327
328 }
329