PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.5
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.5
2.4.5 2.4.0 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 All 69 releases
fluent-support / app / Http / Controllers / McpSettingsController.php

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

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