PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Http / Controllers / McpSettingsController.php

McpSettingsController.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.14, at app/Http/Controllers/McpSettingsController.php

302 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 FluentForm\App\Http\Controllers;
4
5 use FluentForm\App\Modules\MCP\AbilitiesRegistrar;
6 use FluentForm\App\Modules\MCP\MCPInit;
7 use FluentForm\App\Modules\MCP\Support\PermissionGate;
8
9 /**
10 * Backend for the FluentForm → Settings → MCP card.
11 *
12 * The MCP feature stores its on/off state in the dedicated, autoloaded
13 * _fluentform_mcp_settings option (PermissionGate::isEnabled/setEnabled). This
14 * controller owns the status / toggle / connection-snippet endpoints; the toggle
15 * is instant rather than riding the generic global-settings save.
16 */
17 class McpSettingsController extends Controller
18 {
19 const TOOLKIT_PLUGIN_FILE = 'fluent-toolkit/fluent-toolkit.php';
20
21 const ADAPTER_PLUGIN_FILE = 'mcp-adapter/mcp-adapter.php';
22
23 const TOOLKIT_DOWNLOAD_URL = 'https://github.com/WPManageNinja/fluent-toolkit';
24
25 public function status()
26 {
27 // The route policy only asks for fluentform_settings_manager, but every
28 // control this payload drives is manage_options-only — and it discloses
29 // the endpoint URL and the full tool catalogue. Match the toggle's bar.
30 if (!current_user_can('manage_options')) {
31 return $this->sendError([
32 'message' => __('Sorry, you do not have permission to view the MCP settings.', 'fluentform'),
33 ]);
34 }
35
36 $user = wp_get_current_user();
37
38 // Count the same catalogue the card lists; MCPInit::toolsCount() applies
39 // the Pro ability-names filter and would drift from the visible list.
40 // Count only actually-available tools — the greyed Pro teasers are not.
41 $tools = AbilitiesRegistrar::catalogue();
42 $availableCount = count(array_filter($tools, function ($tool) {
43 return !isset($tool['available']) || $tool['available'];
44 }));
45
46 return $this->sendSuccess([
47 'mcp_enabled' => PermissionGate::isEnabled(),
48 'adapter_available' => MCPInit::adapterAvailable(),
49 'adapter_installed' => $this->isToolkitInstalled() || $this->isPluginInstalled(self::ADAPTER_PLUGIN_FILE),
50 'toolkit_installed' => $this->isToolkitInstalled(),
51 'can_auto_install' => (bool) apply_filters('fluent_toolkit/can_auto_install', false),
52 'toolkit_download_url' => self::TOOLKIT_DOWNLOAD_URL,
53 'endpoint_url' => MCPInit::getEndpointUrl(),
54 'tools_count' => $availableCount,
55 'tools' => $tools,
56 'app_passwords_url' => admin_url('profile.php#application-passwords-section'),
57 'plugins_url' => admin_url('plugins.php'),
58 'current_user_login' => ($user && $user->exists()) ? $user->user_login : '',
59 'is_local_dev' => $this->isLocalDev(),
60 ]);
61 }
62
63 public function toggle()
64 {
65 if (!current_user_can('manage_options')) {
66 return $this->sendError([
67 'message' => __('Sorry, you do not have permission to change the MCP setting.', 'fluentform'),
68 ]);
69 }
70
71 $value = $this->request->get('mcp_enabled');
72 $enabled = is_string($value) ? in_array(strtolower($value), ['yes', 'true', '1', 'on'], true) : (bool) $value;
73
74 PermissionGate::setEnabled($enabled);
75
76 $stored = PermissionGate::isEnabled();
77
78 return $this->sendSuccess([
79 'mcp_enabled' => $stored,
80 'message' => $stored
81 ? __('MCP enabled. AI agents with a valid application password can now reach the FluentForm tools.', 'fluentform')
82 : __('MCP disabled. The endpoint will reject requests until re-enabled.', 'fluentform'),
83 ]);
84 }
85
86 public function installAdapter()
87 {
88 // Match the toggle's bar: enabling MCP and installing its adapter are
89 // both admin-only, so require manage_options in addition to the
90 // plugin-install capability.
91 if (!current_user_can('manage_options') || !current_user_can('install_plugins')) {
92 return $this->sendError([
93 'message' => __('Sorry, you do not have permission to install plugins.', 'fluentform'),
94 ]);
95 }
96
97 $canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
98 if (!$canAutoInstall) {
99 return $this->sendError([
100 'message' => __('Automatic install needs a Fluent Pro plugin. Install FluentHub / Fluent Toolkit manually, then reload this page to connect FluentForm with AI agents.', 'fluentform'),
101 'toolkit_download_url' => self::TOOLKIT_DOWNLOAD_URL,
102 ]);
103 }
104
105 do_action('fluent_toolkit/do_auto_install');
106
107 wp_clean_plugins_cache();
108
109 $available = MCPInit::adapterAvailable();
110
111 return $this->sendSuccess([
112 'adapter_available' => $available,
113 'toolkit_installed' => $this->isToolkitInstalled(),
114 'message' => $available
115 ? __('Adapter installed and activated. The MCP endpoint is ready.', 'fluentform')
116 : __('Adapter installed. Please reload this page to finish connecting the MCP endpoint.', 'fluentform'),
117 ]);
118 }
119
120 /**
121 * Connection snippets for every supported client. Credentials are never sent:
122 * each snippet carries placeholders the browser fills in, so an application
123 * password never round-trips through the server.
124 */
125 public function getConfigSnippets()
126 {
127 if (!current_user_can('manage_options')) {
128 return $this->sendError([
129 'message' => __('Sorry, you do not have permission to view the MCP connection details.', 'fluentform'),
130 ]);
131 }
132
133 $endpoint = MCPInit::getEndpointUrl();
134
135 // Determined server-side only. This flag decides whether the Claude
136 // Desktop snippet disables TLS certificate validation, so a caller must
137 // not be able to ask for it — a request parameter here meant a snippet
138 // that skips certificate checks could be produced on a production host.
139 $isLocalDev = $this->isLocalDev();
140
141 $clients = ['claude-code', 'claude-desktop', 'cursor', 'codex', 'generic'];
142 $snippets = [];
143 foreach ($clients as $client) {
144 $snippets[$client] = $this->buildSnippet($client, $endpoint, $isLocalDev);
145 }
146
147 return $this->sendSuccess([
148 'snippets' => $snippets,
149 'endpoint' => $endpoint,
150 'app_passwords_url' => admin_url('profile.php#application-passwords-section'),
151 'is_local_dev' => $isLocalDev,
152 ]);
153 }
154
155 private function isToolkitInstalled()
156 {
157 if (defined('FLUENT_TOOLKIT_VERSION')) {
158 return true;
159 }
160
161 return $this->isPluginInstalled(self::TOOLKIT_PLUGIN_FILE);
162 }
163
164 private function isPluginInstalled($pluginFile)
165 {
166 if (!function_exists('get_plugins')) {
167 require_once ABSPATH . 'wp-admin/includes/plugin.php';
168 }
169
170 $plugins = get_plugins();
171
172 return isset($plugins[$pluginFile]);
173 }
174
175 private function buildSnippet($client, $endpoint, $isLocalDev)
176 {
177 $basic = '<base64(your-username:application-password)>';
178 $user = '<your-username>';
179 $pass = '<your-application-password>';
180
181 switch ($client) {
182 case 'claude-desktop':
183 $env = [
184 'WP_API_URL' => $endpoint,
185 'WP_API_USERNAME' => $user,
186 'WP_API_PASSWORD' => $pass,
187 'OAUTH_ENABLED' => 'false',
188 ];
189 // Local installs typically run behind a self-signed certificate
190 // that Node rejects outright. Only ever emitted for a host this
191 // server itself recognises as local, and always with the warning
192 // below attached so nobody copies it onto a live site.
193 if ($isLocalDev) {
194 $env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
195 }
196 $snippet = wp_json_encode([
197 'mcpServers' => [
198 'fluentform' => [
199 'command' => 'npx',
200 'args' => ['-y', '@automattic/mcp-wordpress-remote@latest'],
201 'env' => $env,
202 ],
203 ],
204 ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
205 $instructions = __('Add this to your Claude Desktop config (Settings → Developer → Edit Config), fill in your username + application password, then restart Claude Desktop.', 'fluentform');
206 if ($isLocalDev) {
207 $instructions .= ' ' . __('This site looks like a local development install, so the snippet sets NODE_TLS_REJECT_UNAUTHORIZED=0 to accept its self-signed certificate. That disables TLS verification for the client — remove that line before using this config against any site reachable over the internet.', 'fluentform');
208 }
209 break;
210
211 case 'cursor':
212 $snippet = wp_json_encode([
213 'mcpServers' => [
214 'fluentform' => [
215 'url' => $endpoint,
216 'type' => 'http',
217 'headers' => ['Authorization' => 'Basic ' . $basic],
218 ],
219 ],
220 ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
221 $instructions = __('Fill your username and application password above — the base64 Authorization header is generated for you — then add this to Cursor’s mcp.json.', 'fluentform');
222 break;
223
224 case 'codex':
225 $snippet = "Settings → Connect to a custom MCP\n\n"
226 . "Name: fluentform\n"
227 . "Transport: Streamable HTTP\n"
228 . "URL: {$endpoint}\n\n"
229 . "Header:\n Key: Authorization\n Value: Basic {$basic}";
230 $instructions = __('In Codex, add a custom MCP server with Streamable HTTP transport and the Authorization header above.', 'fluentform');
231 break;
232
233 case 'generic':
234 $snippet = "URL: {$endpoint}\n"
235 . "Auth: Authorization: Basic {$basic}\n\n"
236 . "# Quick test (curl base64-encodes for you):\n"
237 . "curl -s -u '{$user}:{$pass}' \\\n"
238 . " -X POST {$endpoint} \\\n"
239 . " -H 'Content-Type: application/json' \\\n"
240 . " -H 'Accept: application/json, text/event-stream' \\\n"
241 . ' -d \'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"c","version":"1.0"}}}\'';
242 $instructions = __('Any MCP client that speaks Streamable HTTP can connect using this URL and a Basic auth header.', 'fluentform');
243 break;
244
245 case 'claude-code':
246 default:
247 $client = 'claude-code';
248 $snippet = "claude mcp add \\\n"
249 . " --transport http \\\n"
250 . " fluentform {$endpoint} \\\n"
251 . " --header \"Authorization: Basic {$basic}\"";
252 $instructions = __('Fill your username and application password above — the base64 Authorization header is generated for you — then run this in the terminal where Claude Code is installed.', 'fluentform');
253 break;
254 }
255
256 // The browser fills credential placeholders into this template; it must
257 // escape them for the snippet's syntax (JSON string vs single-quoted shell
258 // arg). Declaring the format here keeps that contract on one side.
259 $jsonClients = ['claude-desktop', 'cursor'];
260 $shellClients = ['generic', 'claude-code'];
261 if (in_array($client, $jsonClients, true)) {
262 $format = 'json';
263 } elseif (in_array($client, $shellClients, true)) {
264 $format = 'shell';
265 } else {
266 $format = 'text';
267 }
268
269 return [
270 'client' => $client,
271 'snippet' => $snippet,
272 'instructions' => $instructions,
273 'format' => $format,
274 ];
275 }
276
277 private function isLocalDev()
278 {
279 $host = '';
280 $home = home_url();
281 if ($home) {
282 $parsed = wp_parse_url($home, PHP_URL_HOST);
283 $host = $parsed ? strtolower($parsed) : '';
284 }
285
286 $isLocal = false;
287 if ($host) {
288 foreach (['.test', '.local', '.localhost', '.lab'] as $tld) {
289 if (substr($host, -strlen($tld)) === $tld) {
290 $isLocal = true;
291 break;
292 }
293 }
294 if (!$isLocal && in_array($host, ['localhost', '127.0.0.1', '::1'], true)) {
295 $isLocal = true;
296 }
297 }
298
299 return (bool) apply_filters('fluentform/mcp_is_local_dev', $isLocal, $host);
300 }
301 }
302