PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk 1.2.0 All 47 releases
fluent-cart / app / Http / Controllers / McpSettingsController.php

McpSettingsController.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Http/Controllers/McpSettingsController.php

276 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Http\Controllers;
4
5 use FluentCart\App\Modules\MCP\MCPInit;
6 use FluentCart\App\Modules\MCP\Support\PermissionGate;
7 use FluentCart\Framework\Http\Request\Request;
8
9 /**
10 * Backend for the Settings → Features & addon → MCP card.
11 *
12 * The MCP feature stores its on/off state under the `mcp` key of the shared
13 * `fluent_cart_modules_settings` blob (autoloaded — no extra query on init).
14 * It still owns its own status/toggle/snippet endpoints (instant toggle + the
15 * connection helpers) rather than riding the shared modules save; toggle()
16 * read-modify-writes only the `mcp` key so the rest of the blob is untouched.
17 */
18 class McpSettingsController extends Controller
19 {
20 const TOOLKIT_PLUGIN_FILE = 'fluent-toolkit/fluent-toolkit.php';
21
22 /** GitHub link shown when no Pro plugin can auto-install the toolkit. */
23 const TOOLKIT_DOWNLOAD_URL = 'https://github.com/WPManageNinja/fluent-toolkit';
24
25 /** Status payload for the MCP card: enabled state, endpoint, adapter, snippet helpers. */
26 public function status()
27 {
28 $user = wp_get_current_user();
29
30 return [
31 'mcp_enabled' => PermissionGate::isEnabled(),
32 'adapter_available' => MCPInit::adapterAvailable(),
33 'toolkit_installed' => $this->isToolkitInstalled(),
34 'can_auto_install' => (bool) apply_filters('fluent_toolkit/can_auto_install', false),
35 'toolkit_download_url' => self::TOOLKIT_DOWNLOAD_URL,
36 'endpoint_url' => MCPInit::getEndpointUrl(),
37 'tools_count' => MCPInit::toolsCount(),
38 'app_passwords_url' => admin_url('profile.php#application-passwords-section'),
39 'plugins_url' => admin_url('plugins.php'),
40 'current_user_login' => ($user && $user->exists()) ? $user->user_login : '',
41 'is_local_dev' => $this->isLocalDev(),
42 ];
43 }
44
45 /**
46 * One-click FluentHub install. The free plugin can only detect + trigger;
47 * the actual installer lives in a Fluent Pro plugin (FluentCart Pro, or any
48 * Fluent product) that hooks the site-wide `fluent_toolkit/*` contract. With
49 * no Pro present we hand back the manual download link.
50 */
51 public function installAdapter()
52 {
53 if (!current_user_can('install_plugins')) {
54 return $this->sendError([
55 'message' => __('Sorry, you do not have permission to install plugins.', 'fluent-cart'),
56 ]);
57 }
58
59 $canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
60 if (!$canAutoInstall) {
61 return $this->sendError([
62 'message' => __('Automatic install needs FluentCart Pro (or another Fluent Pro plugin). Install FluentHub manually, then reload this page to connect FluentCart with AI agents.', 'fluent-cart'),
63 'toolkit_download_url' => self::TOOLKIT_DOWNLOAD_URL,
64 ]);
65 }
66
67 do_action('fluent_toolkit/do_auto_install');
68
69 wp_clean_plugins_cache();
70
71 $available = MCPInit::adapterAvailable();
72
73 return $this->sendSuccess([
74 'adapter_available' => $available,
75 'toolkit_installed' => $this->isToolkitInstalled(),
76 'message' => $available
77 ? __('FluentHub installed and activated. The MCP endpoint is ready.', 'fluent-cart')
78 : __('FluentHub was installed. Please reload this page to finish connecting the MCP endpoint.', 'fluent-cart'),
79 ]);
80 }
81
82 /** Is FluentHub (the toolkit plugin) present on disk (installed, active or not)? */
83 private function isToolkitInstalled()
84 {
85 if (defined('FLUENT_TOOLKIT_VERSION')) {
86 return true;
87 }
88
89 if (!function_exists('get_plugins')) {
90 require_once ABSPATH . 'wp-admin/includes/plugin.php';
91 }
92
93 $plugins = get_plugins();
94
95 return isset($plugins[self::TOOLKIT_PLUGIN_FILE]);
96 }
97
98 /** Flip the master switch. Writes the same blob key the server boot guard reads. */
99 public function toggle(Request $request)
100 {
101 $value = $request->get('mcp_enabled');
102 $enabled = is_string($value) ? in_array(strtolower($value), ['yes', 'true', '1', 'on'], true) : (bool) $value;
103
104 // setEnabled() fails closed when the user can't manage_options. Re-check
105 // here so we return an error instead of a misleading success response.
106 if (!current_user_can('manage_options')) {
107 return $this->sendError([
108 'message' => __('Sorry, you do not have permission to change the MCP setting.', 'fluent-cart'),
109 ]);
110 }
111
112 PermissionGate::setEnabled($enabled);
113
114 // Report the actually-persisted state, not the requested value, so the UI
115 // can never show "enabled" for a write that didn't land.
116 $stored = PermissionGate::isEnabled();
117
118 return $this->sendSuccess([
119 'mcp_enabled' => $stored,
120 'message' => $stored
121 ? __('MCP enabled. AI agents with a valid app password can now reach the FluentCart tools.', 'fluent-cart')
122 : __('MCP disabled. The endpoint will reject requests until re-enabled.', 'fluent-cart'),
123 ]);
124 }
125
126 /**
127 * Connection snippets for EVERY supported client, in one response. They're
128 * cheap server-side string templates, so we build them all at once rather
129 * than round-tripping per tab — the UI just switches between cached entries.
130 *
131 * Credentials are NEVER sent here: each snippet carries placeholders the
132 * browser fills in client-side, so an application password never round-trips
133 * through the server. Only `local_dev` (TLS verification for Claude Desktop)
134 * varies the output.
135 */
136 public function getConfigSnippets(Request $request)
137 {
138 $endpoint = MCPInit::getEndpointUrl();
139
140 $localDevParam = $request->get('local_dev');
141 $isLocalDev = ($localDevParam === null || $localDevParam === '')
142 ? $this->isLocalDev()
143 : in_array(strtolower((string) $localDevParam), ['yes', 'true', '1', 'on'], true);
144
145 $clients = ['claude-code', 'claude-desktop', 'cursor', 'codex', 'generic'];
146 $snippets = [];
147 foreach ($clients as $client) {
148 $snippets[$client] = $this->buildSnippet($client, $endpoint, $isLocalDev);
149 }
150
151 return [
152 'snippets' => $snippets,
153 'endpoint' => $endpoint,
154 'app_passwords_url' => admin_url('profile.php#application-passwords-section'),
155 'is_local_dev' => $isLocalDev,
156 ];
157 }
158
159 /**
160 * Build a single client's connection snippet + instructions. Pure string
161 * assembly — no DB, no credentials.
162 */
163 private function buildSnippet($client, $endpoint, $isLocalDev)
164 {
165 $basic = '<base64(your-username:application-password)>';
166 $user = '<your-username>';
167 $pass = '<your-application-password>';
168
169 switch ($client) {
170 case 'claude-desktop':
171 $env = [
172 'WP_API_URL' => $endpoint,
173 'WP_API_USERNAME' => $user,
174 'WP_API_PASSWORD' => $pass,
175 'OAUTH_ENABLED' => 'false',
176 ];
177 if ($isLocalDev) {
178 $env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
179 }
180 $snippet = wp_json_encode([
181 'mcpServers' => [
182 'fluent-cart' => [
183 'command' => 'npx',
184 'args' => ['-y', '@automattic/mcp-wordpress-remote@latest'],
185 'env' => $env,
186 ],
187 ],
188 ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
189 $instructions = __('Add this to your Claude Desktop config (Settings → Developer → Edit Config), fill in your username + application password, then restart Claude Desktop.', 'fluent-cart');
190 break;
191
192 case 'cursor':
193 $snippet = wp_json_encode([
194 'mcpServers' => [
195 'fluent-cart' => [
196 'url' => $endpoint,
197 'type' => 'http',
198 'headers' => ['Authorization' => 'Basic ' . $basic],
199 ],
200 ],
201 ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
202 $instructions = __('Add to Cursor’s mcp.json, replacing the placeholder with base64 of "username:application-password".', 'fluent-cart');
203 break;
204
205 case 'codex':
206 $snippet = "Settings → Connect to a custom MCP\n\n"
207 . "Name: fluent-cart\n"
208 . "Transport: Streamable HTTP\n"
209 . "URL: {$endpoint}\n\n"
210 . "Header:\n Key: Authorization\n Value: Basic {$basic}";
211 $instructions = __('In Codex, add a custom MCP server with Streamable HTTP transport and the Authorization header above.', 'fluent-cart');
212 break;
213
214 case 'generic':
215 $snippet = "URL: {$endpoint}\n"
216 . "Auth: Authorization: Basic {$basic}\n\n"
217 . "# Quick test (curl base64-encodes for you):\n"
218 . "curl -s -u '{$user}:{$pass}' \\\n"
219 . " -X POST {$endpoint} \\\n"
220 . " -H 'Content-Type: application/json' \\\n"
221 . " -H 'Accept: application/json, text/event-stream' \\\n"
222 . ' -d \'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"c","version":"1.0"}}}\'';
223 $instructions = __('Any MCP client that speaks Streamable HTTP can connect using this URL and a Basic auth header.', 'fluent-cart');
224 break;
225
226 case 'claude-code':
227 default:
228 $client = 'claude-code';
229 $snippet = "claude mcp add \\\n"
230 . " --transport http \\\n"
231 . " fluent-cart {$endpoint} \\\n"
232 . " --header \"Authorization: Basic {$basic}\"";
233 $instructions = __('Run this in your terminal where Claude Code is installed, with base64 of "username:application-password".', 'fluent-cart');
234 break;
235 }
236
237 return [
238 'client' => $client,
239 'snippet' => $snippet,
240 'instructions' => $instructions,
241 ];
242 }
243
244 /**
245 * Best-effort "is this a local dev site?" check, used to default the TLS
246 * override in the Claude Desktop snippet. Filterable for edge cases.
247 */
248 private function isLocalDev()
249 {
250 $host = '';
251 $home = home_url();
252 if ($home) {
253 $parsed = wp_parse_url($home, PHP_URL_HOST);
254 $host = $parsed ? strtolower($parsed) : '';
255 }
256
257 $isLocal = false;
258 if ($host) {
259 // Note: '.dev' is a real public HSTS-preloaded gTLD, not a local
260 // suffix — excluded so a production .dev site isn't told to disable
261 // TLS verification.
262 foreach (['.test', '.local', '.localhost', '.lab'] as $tld) {
263 if (substr($host, -strlen($tld)) === $tld) {
264 $isLocal = true;
265 break;
266 }
267 }
268 if (!$isLocal && in_array($host, ['localhost', '127.0.0.1', '::1'], true)) {
269 $isLocal = true;
270 }
271 }
272
273 return (bool) apply_filters('fluent_cart/mcp_is_local_dev', $isLocal, $host);
274 }
275 }
276