PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.3.2
Fluent Support – Helpdesk & Customer Support Ticket System v2.3.2
2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 All 67 releases
fluent-support / app / Http / Controllers / McpSettingsController.php

McpSettingsController.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.3.2, at app/Http/Controllers/McpSettingsController.php

311 lines 13.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App\Http\Controllers;
4
5 use FluentSupport\App\Modules\MCP\MCPInit;
6 use FluentSupport\App\Modules\MCP\Support\PermissionGate;
7 use FluentSupport\Framework\Http\Request\Request;
8
9 /**
10 * Backend for the Settings → MCP for AI Agents page.
11 *
12 * Covers: enable/disable toggle, one-click FluentHub install, and per-client
13 * connection snippets. SLA and AI guidelines are intentionally not exposed here —
14 * they are tool-layer concerns read by PermissionGate and live in _mcp_settings.
15 */
16 class McpSettingsController extends Controller
17 {
18 const TOOLKIT_PLUGIN_FILE = 'fluent-toolkit/fluent-toolkit.php';
19 const TOOLKIT_DOWNLOAD_URL = 'https://github.com/WPManageNinja/fluent-toolkit';
20
21 /**
22 * GET fluent-support/v2/settings/mcp
23 * Returns all data the settings page needs in one request.
24 */
25 public function getStatus(Request $request)
26 {
27 $user = wp_get_current_user();
28
29 return [
30 'mcp_enabled' => PermissionGate::isEnabled(),
31 'adapter_available' => MCPInit::adapterAvailable(),
32 'toolkit_installed' => $this->isToolkitPresent(),
33 'toolkit_active' => $this->isToolkitLoaded(),
34 'toolkit_version' => $this->detectToolkitVersion(),
35 'can_auto_install' => (bool) apply_filters('fluent_toolkit/can_auto_install', false),
36 'toolkit_download_url' => self::TOOLKIT_DOWNLOAD_URL,
37 'endpoint_url' => MCPInit::getEndpointUrl(),
38 'tools_count' => MCPInit::toolsCount(),
39 'app_passwords_url' => admin_url('profile.php#application-passwords-section'),
40 'plugins_url' => admin_url('plugins.php'),
41 'current_user_login' => ($user && $user->exists()) ? $user->user_login : '',
42 'is_local_dev' => $this->isLocalDev(),
43 ];
44 }
45
46 /**
47 * POST fluent-support/v2/settings/mcp/toggle
48 * Body: { mcp_enabled: true|'yes'|1|'on' }
49 */
50 public function toggle(Request $request)
51 {
52 if (!current_user_can('manage_options')) {
53 return [
54 'success' => false,
55 'message' => __('Sorry, you do not have permission to change the MCP setting.', 'fluent-support'),
56 ];
57 }
58
59 $value = $request->get('mcp_enabled');
60 $enabled = is_string($value)
61 ? in_array(strtolower($value), ['yes', 'true', '1', 'on'], true)
62 : (bool) $value;
63
64 PermissionGate::setEnabled($enabled);
65
66 // Re-read the persisted state so the UI can never show "enabled" for a write that didn't land.
67 $stored = PermissionGate::isEnabled();
68
69 return [
70 'mcp_enabled' => $stored,
71 'message' => $stored
72 ? __('MCP enabled. AI agents with a valid application password can now reach the Fluent Support tools.', 'fluent-support')
73 : __('MCP disabled. The endpoint will reject requests until re-enabled.', 'fluent-support'),
74 ];
75 }
76
77 /**
78 * POST fluent-support/v2/settings/mcp/install-adapter
79 * One-click FluentHub install. Requires a Fluent Pro plugin to hook
80 * fluent_toolkit/can_auto_install + fluent_toolkit/do_auto_install.
81 */
82 public function installAdapter(Request $request)
83 {
84 if (!current_user_can('install_plugins')) {
85 return $this->sendError([
86 'message' => __('Sorry, you do not have permission to install plugins.', 'fluent-support'),
87 ]);
88 }
89
90 $canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
91 if (!$canAutoInstall) {
92 return $this->sendError([
93 'message' => __('Automatic install needs a Fluent Pro plugin active. Install FluentHub manually, then reload this page to connect Fluent Support with AI agents.', 'fluent-support'),
94 'toolkit_download_url' => self::TOOLKIT_DOWNLOAD_URL,
95 ]);
96 }
97
98 do_action('fluent_toolkit/do_auto_install');
99 wp_clean_plugins_cache();
100
101 $adapterAvailable = MCPInit::adapterAvailable();
102
103 return $this->sendSuccess([
104 'adapter_available' => $adapterAvailable,
105 'toolkit_installed' => $this->isToolkitPresent(),
106 'message' => $adapterAvailable
107 ? __('FluentHub installed and activated. The MCP endpoint is ready.', 'fluent-support')
108 : __('FluentHub was installed. Please reload this page to finish connecting the MCP endpoint.', 'fluent-support'),
109 ]);
110 }
111
112 /**
113 * GET fluent-support/v2/settings/mcp/config-snippets
114 * Query param: local_dev=yes|no (falls back to auto-detected isLocalDev when omitted)
115 */
116 public function getConfigSnippets(Request $request)
117 {
118 $endpoint = MCPInit::getEndpointUrl();
119 $localDevParam = $request->get('local_dev');
120 $isLocalDev = ($localDevParam === null || $localDevParam === '')
121 ? $this->isLocalDev()
122 : in_array(strtolower((string) $localDevParam), ['yes', 'true', '1', 'on'], true);
123
124 $snippets = [];
125 foreach (['claude-code', 'claude-desktop', 'cursor', 'codex', 'generic'] as $client) {
126 $snippets[$client] = $this->buildSnippet($client, $endpoint, $isLocalDev);
127 }
128
129 return [
130 'snippets' => $snippets,
131 'endpoint' => $endpoint,
132 'app_passwords_url' => admin_url('profile.php#application-passwords-section'),
133 'is_local_dev' => $isLocalDev,
134 ];
135 }
136
137 private function buildSnippet($client, $endpoint, $isLocalDev)
138 {
139 $basic = '<base64(your-username:application-password)>';
140 $user = '<your-username>';
141 $pass = '<your-application-password>';
142
143 switch ($client) {
144 case 'claude-desktop':
145 $env = [
146 'WP_API_URL' => $endpoint,
147 'WP_API_USERNAME' => $user,
148 'WP_API_PASSWORD' => $pass,
149 'OAUTH_ENABLED' => 'false',
150 ];
151 if ($isLocalDev) {
152 $env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
153 }
154 $localDevNote = $isLocalDev
155 ? ' ' . __('Local dev mode is on, so NODE_TLS_REJECT_UNAUTHORIZED is included — the npx proxy needs it to talk to self-signed SSL.', 'fluent-support')
156 : '';
157 return [
158 'client' => $client,
159 'snippet' => wp_json_encode([
160 'mcpServers' => [
161 'fluent-support' => [
162 'command' => 'npx',
163 'args' => ['-y', '@automattic/mcp-wordpress-remote@latest'],
164 'env' => $env,
165 ],
166 ],
167 ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
168 'instructions' => __('Add this to your Claude Desktop config (Settings → Developer → Edit Config), fill in your username + application password, then restart Claude Desktop.', 'fluent-support') . $localDevNote,
169 ];
170
171 case 'cursor':
172 return [
173 'client' => $client,
174 'snippet' => wp_json_encode([
175 'mcpServers' => [
176 'fluent-support' => [
177 'url' => $endpoint,
178 'type' => 'http',
179 'headers' => ['Authorization' => 'Basic ' . $basic],
180 ],
181 ],
182 ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
183 'instructions' => __("Add to Cursor's mcp.json, replacing the placeholder with base64 of \"username:application-password\".", 'fluent-support'),
184 ];
185
186 case 'codex':
187 return [
188 'client' => $client,
189 'snippet' => "Settings → Connect to a custom MCP\n\n"
190 . "Name: fluent-support\n"
191 . "Transport: Streamable HTTP\n"
192 . "URL: {$endpoint}\n\n"
193 . "Header:\n Key: Authorization\n Value: Basic {$basic}",
194 'instructions' => __('In Codex, add a custom MCP server with Streamable HTTP transport and the Authorization header above.', 'fluent-support'),
195 ];
196
197 case 'generic':
198 return [
199 'client' => $client,
200 'snippet' => "URL: {$endpoint}\n"
201 . "Auth: Authorization: Basic {$basic}\n\n"
202 . "# Quick test (curl base64-encodes for you):\n"
203 . "curl -s -u '{$user}:{$pass}' \\\n"
204 . " -X POST {$endpoint} \\\n"
205 . " -H 'Content-Type: application/json' \\\n"
206 . " -H 'Accept: application/json, text/event-stream' \\\n"
207 . ' -d \'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"c","version":"1.0"}}}\'',
208 'instructions' => __('Any MCP client that speaks Streamable HTTP can connect using this URL and a Basic auth header.', 'fluent-support'),
209 ];
210
211 case 'claude-code':
212 default:
213 return [
214 'client' => 'claude-code',
215 'snippet' => "claude mcp add \\\n"
216 . " --transport http \\\n"
217 . " fluent-support {$endpoint} \\\n"
218 . " --header \"Authorization: Basic {$basic}\"",
219 'instructions' => __('Run this in your terminal where Claude Code is installed, replacing the placeholder with base64 of "username:application-password".', 'fluent-support'),
220 ];
221 }
222 }
223
224 // -------------------------------------------------------------------------
225 // Toolkit detection helpers — mirrors FluentCRM MCPSettingsController
226 // -------------------------------------------------------------------------
227
228 /** True when FluentHub is loaded in the current request (constant defined). */
229 private function isToolkitLoaded()
230 {
231 return defined('FLUENT_TOOLKIT_VERSION');
232 }
233
234 /** True when FluentHub is loaded OR present in the plugin directory. */
235 private function isToolkitPresent()
236 {
237 return $this->isToolkitLoaded() || $this->isPluginPresent(self::TOOLKIT_PLUGIN_FILE);
238 }
239
240 /** Version string from the loaded constant, or from the plugin header, or null. */
241 private function detectToolkitVersion()
242 {
243 if ($this->isToolkitLoaded()) {
244 return (string) FLUENT_TOOLKIT_VERSION;
245 }
246
247 return $this->detectPluginVersion(self::TOOLKIT_PLUGIN_FILE);
248 }
249
250 private function isPluginPresent($pluginFile)
251 {
252 if (!function_exists('get_plugins')) {
253 require_once ABSPATH . 'wp-admin/includes/plugin.php';
254 }
255
256 return isset(get_plugins()[$pluginFile]);
257 }
258
259 private function detectPluginVersion($pluginFile)
260 {
261 if (!function_exists('get_plugins')) {
262 require_once ABSPATH . 'wp-admin/includes/plugin.php';
263 }
264
265 $plugins = get_plugins();
266
267 return $plugins[$pluginFile]['Version'] ?? null;
268 }
269
270 // -------------------------------------------------------------------------
271 // Local-dev detection
272 // -------------------------------------------------------------------------
273
274 /**
275 * Heuristic: are we on a local/dev install?
276 *
277 * Checks (in order):
278 * 1. Hostname ends in a dev TLD (.test, .local, .localhost, .lab, .docker)
279 * 2. Hostname is a loopback literal (localhost, 127.0.0.1, ::1)
280 * 3. Hostname is a private RFC 1918 IP address
281 *
282 * Note: .dev is intentionally excluded — it is a real HSTS-preloaded
283 * public gTLD (owned by Google) and we must not disable TLS on those sites.
284 *
285 * Filterable via fluent_support/mcp_is_local_dev for edge cases.
286 */
287 private function isLocalDev()
288 {
289 $host = strtolower((string) wp_parse_url(home_url(), PHP_URL_HOST));
290
291 $isLocal = false;
292
293 foreach (['.test', '.local', '.localhost', '.lab', '.docker'] as $tld) {
294 if ($tld !== '' && substr($host, -strlen($tld)) === $tld) {
295 $isLocal = true;
296 break;
297 }
298 }
299
300 if (!$isLocal && in_array($host, ['localhost', '127.0.0.1', '::1'], true)) {
301 $isLocal = true;
302 }
303
304 if (!$isLocal && filter_var($host, FILTER_VALIDATE_IP)) {
305 $isLocal = !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
306 }
307
308 return (bool) apply_filters('fluent_support/mcp_is_local_dev', $isLocal, $host);
309 }
310 }
311