PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.95
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.95
2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 1.45 All 41 releases
fluent-boards / app / Http / Controllers / MCPSettingsController.php

MCPSettingsController.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 1.95, at app/Http/Controllers/MCPSettingsController.php

357 lines 14.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Http\Controllers;
4
5 use FluentBoards\App\Modules\MCP\MCPInit;
6 use FluentBoards\Framework\Http\Request\Request;
7
8 /**
9 * Settings endpoints for connecting Fluent Boards MCP tools to AI agents.
10 */
11 class MCPSettingsController extends Controller
12 {
13 const ADAPTER_PLUGIN_FILE = 'mcp-adapter/mcp-adapter.php';
14 const TOOLKIT_PLUGIN_FILE = 'fluent-toolkit/fluent-toolkit.php';
15
16 public function status()
17 {
18 if (!function_exists('is_plugin_active')) {
19 require_once ABSPATH . 'wp-admin/includes/plugin.php';
20 }
21
22 $adapterInstalled = $this->isAdapterPresent();
23 $toolkitInstalled = $this->isToolkitPresent();
24 $adapterRuntimeAvailable = $this->isAdapterRuntimeAvailable();
25 $standaloneActive = is_plugin_active(self::ADAPTER_PLUGIN_FILE) && $adapterRuntimeAvailable;
26 $toolkitActive = $this->isToolkitLoaded();
27 $toolkitAdapterActive = $toolkitActive && $this->isToolkitAdapterAvailable();
28 $adapterActive = $standaloneActive || $toolkitAdapterActive;
29 $adapterProvider = $standaloneActive ? 'plugin' : ($toolkitAdapterActive ? 'toolkit' : '');
30 $abilitiesAvailable = function_exists('wp_register_ability');
31 $canAutoInstall = $this->canAutoInstallFluentKit();
32 $currentUser = wp_get_current_user();
33
34 return $this->sendSuccess([
35 'adapter_installed' => $adapterInstalled || $toolkitInstalled,
36 'adapter_active' => $adapterActive,
37 'adapter_provider' => $adapterProvider,
38 'standalone_adapter_installed' => $adapterInstalled,
39 'toolkit_installed' => $toolkitInstalled,
40 'toolkit_active' => $toolkitActive,
41 'toolkit_adapter_available' => $toolkitAdapterActive,
42 'adapter_runtime_available' => $adapterRuntimeAvailable,
43 'adapter_version' => $this->detectAdapterVersion(),
44 'toolkit_version' => $this->detectToolkitVersion(),
45 'abilities_api_loaded' => $abilitiesAvailable,
46 'endpoint_url' => MCPInit::getEndpointUrl(),
47 'tools_count' => $abilitiesAvailable ? MCPInit::getToolsCount() : 0,
48 'mcp_enabled' => fluent_boards_get_option('mcp_enabled', 'yes') === 'yes',
49 'app_passwords_url' => admin_url('profile.php#application-passwords-section'),
50 'plugins_url' => admin_url('plugins.php'),
51 'can_auto_install_adapter' => $canAutoInstall,
52 'toolkit_download_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
53 'current_user_login' => $currentUser ? $currentUser->user_login : '',
54 'is_local_dev' => self::detectLocalDevEnvironment(),
55 ]);
56 }
57
58 public function toggle(Request $request)
59 {
60 $value = $request->get('mcp_enabled');
61 $enabled = is_string($value) ? ($value === 'yes' || $value === 'true' || $value === '1') : (bool) $value;
62
63 fluent_boards_update_option('mcp_enabled', $enabled ? 'yes' : 'no');
64
65 return $this->sendSuccess([
66 'ok' => true,
67 'mcp_enabled' => $enabled,
68 'message' => $enabled
69 ? __('MCP tools enabled. New requests will see the Fluent Boards abilities.', 'fluent-boards')
70 : __('MCP tools disabled. The adapter will no longer report Fluent Boards abilities.', 'fluent-boards'),
71 ]);
72 }
73
74 public function installAdapter()
75 {
76 if (!current_user_can('install_plugins')) {
77 return $this->sendError([
78 'message' => __('Sorry! you do not have permission to install plugins', 'fluent-boards'),
79 ]);
80 }
81
82 $canAutoInstall = $this->canAutoInstallFluentKit();
83 if (!$canAutoInstall) {
84 return $this->sendError([
85 'message' => __('Please install FluentKit from GitHub, then reload this page to connect Fluent Boards with AI agents.', 'fluent-boards'),
86 'toolkit_download_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
87 ]);
88 }
89
90 $this->autoInstallFluentKit();
91
92 wp_clean_plugins_cache();
93
94 if (!function_exists('is_plugin_active')) {
95 require_once ABSPATH . 'wp-admin/includes/plugin.php';
96 }
97
98 $toolkitInstalled = $this->isToolkitPresent();
99 $toolkitActive = $this->isToolkitLoaded();
100 $adapterRuntimeAvailable = $this->isAdapterRuntimeAvailable();
101 $toolkitAdapterAvailable = $toolkitActive && $this->isToolkitAdapterAvailable();
102 $isInstalled = $this->isAdapterPresent() || $toolkitInstalled;
103 $isActive = (is_plugin_active(self::ADAPTER_PLUGIN_FILE) && $adapterRuntimeAvailable) || $toolkitAdapterAvailable;
104
105 if ($isInstalled && $isActive) {
106 $message = __('FluentKit installed and activated. Reload the page to register Fluent Boards MCP tools.', 'fluent-boards');
107 } elseif ($toolkitInstalled && $toolkitActive) {
108 $message = __('FluentKit is installed and active, but this version does not include the bundled MCP adapter yet. Please update FluentKit when the MCP-ready build is available, then reload this page.', 'fluent-boards');
109 } elseif ($toolkitInstalled) {
110 $message = __('FluentKit is installed but could not be activated automatically. Please activate FluentKit from the Plugins page, then reload this page.', 'fluent-boards');
111 } else {
112 $message = __('Could not install FluentKit automatically. Please install FluentKit manually, then reload this page.', 'fluent-boards');
113 }
114
115 return $this->sendSuccess([
116 'is_installed' => $isInstalled,
117 'adapter_active' => $isActive,
118 'toolkit_active' => $toolkitActive,
119 'toolkit_adapter_available' => $toolkitAdapterAvailable,
120 'message' => $message,
121 ]);
122 }
123
124 public function getConfigSnippet(Request $request)
125 {
126 $client = sanitize_key((string) $request->get('client', 'claude-code'));
127 $endpoint = MCPInit::getEndpointUrl();
128 $isLocalDev = $this->resolveLocalDevOverride($request);
129
130 $basicPlaceholder = '<base64(your-username:application-password)>';
131 $usernamePlaceholder = '<your-username>';
132 $passwordPlaceholder = '<your-application-password>';
133 $appPasswordsUrl = admin_url('profile.php#application-passwords-section');
134
135 switch ($client) {
136 case 'codex':
137 $snippet = sprintf(
138 "Settings > Connect to a custom MCP\n\nName: fluent-boards\nTransport: Streamable HTTP\n\nURL: %s\n\nHeader:\n Key: Authorization\n Value: Basic %s\n\nClick Save.",
139 $endpoint,
140 $basicPlaceholder
141 );
142 $instructions = sprintf(
143 __('Open OpenAI Codex settings, connect to a custom MCP, choose Streamable HTTP, then use a WordPress Application Password from %s.', 'fluent-boards'),
144 $appPasswordsUrl
145 );
146 break;
147
148 case 'cursor':
149 $snippet = wp_json_encode([
150 'mcpServers' => [
151 'fluent-boards' => [
152 'url' => $endpoint,
153 'type' => 'http',
154 'headers' => [
155 'Authorization' => 'Basic ' . $basicPlaceholder,
156 ],
157 ],
158 ],
159 ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
160 $instructions = __('Paste this into Cursor Settings > MCP, then restart Cursor.', 'fluent-boards');
161 break;
162
163 case 'generic':
164 $snippet = sprintf(
165 "URL: %s\nAuth: Authorization: Basic %s\n\n# Quick test\ncurl -s -u '%s:%s' \\\n -X POST %s \\\n -H 'Content-Type: application/json' \\\n -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'",
166 $endpoint,
167 $basicPlaceholder,
168 $usernamePlaceholder,
169 $passwordPlaceholder,
170 $endpoint
171 );
172 $instructions = __('Use the URL and Basic Auth header with any HTTP MCP client. The endpoint speaks MCP over JSON-RPC.', 'fluent-boards');
173 break;
174
175 case 'claude-desktop':
176 $env = [
177 'WP_API_URL' => $endpoint,
178 'WP_API_USERNAME' => $usernamePlaceholder,
179 'WP_API_PASSWORD' => $passwordPlaceholder,
180 ];
181
182 if ($isLocalDev) {
183 $env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
184 }
185
186 $snippet = wp_json_encode([
187 'mcpServers' => [
188 'fluent-boards' => [
189 'command' => 'npx',
190 'args' => ['-y', '@automattic/mcp-wordpress-remote@latest'],
191 'env' => $env,
192 ],
193 ],
194 ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
195
196 $instructions = __('Paste into the Claude Desktop config file, then restart Claude Desktop.', 'fluent-boards');
197 if ($isLocalDev) {
198 $instructions .= ' ' . __('Local dev mode is on, so NODE_TLS_REJECT_UNAUTHORIZED is included for self-signed SSL.', 'fluent-boards');
199 }
200 break;
201
202 case 'claude-code':
203 default:
204 $snippet = sprintf(
205 "claude mcp add \\\n --transport http \\\n fluent-boards %s \\\n --header \"Authorization: Basic %s\"",
206 $endpoint,
207 $basicPlaceholder
208 );
209 $instructions = __('Fill in your username and application password above, then paste the command into your terminal.', 'fluent-boards');
210 $client = 'claude-code';
211 break;
212 }
213
214 return $this->sendSuccess([
215 'client' => $client,
216 'snippet' => $snippet,
217 'instructions' => $instructions,
218 'endpoint' => $endpoint,
219 'app_passwords_url' => $appPasswordsUrl,
220 'is_local_dev' => $isLocalDev,
221 ]);
222 }
223
224 private function resolveLocalDevOverride(Request $request)
225 {
226 $forceLocalDev = $request->get('local_dev');
227 if ($forceLocalDev === 'yes' || $forceLocalDev === '1' || $forceLocalDev === 'true') {
228 return true;
229 }
230
231 if ($forceLocalDev === 'no' || $forceLocalDev === '0' || $forceLocalDev === 'false') {
232 return false;
233 }
234
235 return self::detectLocalDevEnvironment();
236 }
237
238 private static function detectLocalDevEnvironment()
239 {
240 $host = wp_parse_url(home_url(), PHP_URL_HOST);
241 $host = strtolower((string) $host);
242
243 $isDev = false;
244 foreach (['.test', '.lab', '.local', '.localhost', '.docker', '.dev'] as $tld) {
245 if (substr($host, -strlen($tld)) === $tld) {
246 $isDev = true;
247 break;
248 }
249 }
250
251 if (!$isDev && ($host === 'localhost' || $host === '127.0.0.1' || $host === '::1')) {
252 $isDev = true;
253 }
254
255 if (!$isDev && filter_var($host, FILTER_VALIDATE_IP)) {
256 $isDev = !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
257 }
258
259 return (bool) apply_filters('fluent_boards/mcp_is_local_dev', $isDev, $host);
260 }
261
262 private function isAdapterPresent()
263 {
264 return $this->isPluginPresent(self::ADAPTER_PLUGIN_FILE);
265 }
266
267 private function isToolkitPresent()
268 {
269 return $this->isToolkitLoaded() || $this->isPluginPresent(self::TOOLKIT_PLUGIN_FILE);
270 }
271
272 private function detectAdapterVersion()
273 {
274 return $this->detectPluginVersion(self::ADAPTER_PLUGIN_FILE);
275 }
276
277 private function detectToolkitVersion()
278 {
279 if ($this->isToolkitLoaded()) {
280 return (string) FLUENT_TOOLKIT_VERSION;
281 }
282
283 return $this->detectPluginVersion(self::TOOLKIT_PLUGIN_FILE);
284 }
285
286 private function isToolkitLoaded()
287 {
288 return defined('FLUENT_TOOLKIT_VERSION');
289 }
290
291 private function canAutoInstallFluentKit()
292 {
293 $canAutoInstall = (bool) apply_filters('fluent_kit/can_auto_install', false);
294
295 if (!$canAutoInstall) {
296 $canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
297 }
298
299 return $canAutoInstall;
300 }
301
302 private function autoInstallFluentKit()
303 {
304 if ((bool) apply_filters('fluent_kit/can_auto_install', false)) {
305 do_action('fluent_kit/do_auto_install');
306 return;
307 }
308
309 do_action('fluent_toolkit/do_auto_install');
310 }
311
312 private function isPluginPresent($pluginFile)
313 {
314 if (!function_exists('get_plugins')) {
315 require_once ABSPATH . 'wp-admin/includes/plugin.php';
316 }
317
318 $plugins = get_plugins();
319 return isset($plugins[$pluginFile]);
320 }
321
322 private function detectPluginVersion($pluginFile)
323 {
324 if (!function_exists('get_plugins')) {
325 require_once ABSPATH . 'wp-admin/includes/plugin.php';
326 }
327
328 $plugins = get_plugins();
329 if (!isset($plugins[$pluginFile])) {
330 return null;
331 }
332
333 return $plugins[$pluginFile]['Version'] ?? null;
334 }
335
336 private function isToolkitAdapterAvailable()
337 {
338 if (!$this->isToolkitLoaded()) {
339 return false;
340 }
341
342 if (class_exists('\FluentToolkit\Mcp\AdapterBootstrap') && method_exists('\FluentToolkit\Mcp\AdapterBootstrap', 'available')) {
343 return (bool) \FluentToolkit\Mcp\AdapterBootstrap::available();
344 }
345
346 return $this->isAdapterRuntimeAvailable();
347 }
348
349 private function isAdapterRuntimeAvailable()
350 {
351 return defined('WP_MCP_VERSION')
352 && class_exists('\WP\MCP\Core\McpAdapter')
353 && function_exists('wp_register_ability');
354 }
355
356 }
357