| Server IP : 52.25.153.185 / Your IP : 216.73.217.131 Web Server : Apache System : Linux ip-172-26-6-158 5.10.0-35-cloud-amd64 #1 SMP Debian 5.10.237-1 (2025-05-19) x86_64 User : daemon ( 1) PHP Version : 8.1.10 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /bitnami/wordpress/wp-content/plugins/fluent-crm/app/Http/Controllers/ |
Upload File : |
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Modules\MCP\AbilitiesRegistrar;
use FluentCrm\App\Modules\MCP\MCPInit;
use FluentCrm\Framework\Http\Request\Request;
/**
* Settings → MCP admin endpoints (MCP_PLAN.md § 13).
*
* Surfaces:
* - status: adapter detected? CRM count? enabled toggle?
* - install-adapter: one-click install of the WP MCP Adapter plugin
* - toggle: enable/disable the entire MCP module without uninstalling
* - config-snippet: pre-filled JSON for Claude Desktop / Claude Code /
* Cursor / generic clients
*/
class MCPSettingsController extends Controller
{
const ADAPTER_PLUGIN_FILE = 'mcp-adapter/mcp-adapter.php';
const TOOLKIT_PLUGIN_FILE = 'fluent-toolkit/fluent-toolkit.php';
/**
* Status block — what the Settings page lights up with on load.
*/
public function status()
{
if (!function_exists('is_plugin_active')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$adapterInstalled = $this->isAdapterPresent();
$toolkitInstalled = $this->isToolkitPresent();
$adapterRuntimeAvailable = $this->isAdapterRuntimeAvailable();
$standaloneActive = is_plugin_active(self::ADAPTER_PLUGIN_FILE) && $adapterRuntimeAvailable;
$toolkitActive = $this->isToolkitLoaded();
$toolkitAdapterActive = $toolkitActive && $this->isToolkitAdapterAvailable();
$adapterActive = $standaloneActive || $toolkitAdapterActive;
$adapterProvider = $standaloneActive ? 'plugin' : ($toolkitAdapterActive ? 'toolkit' : '');
$abilitiesAvailable = function_exists('wp_register_ability');
$canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
$toolsCount = $abilitiesAvailable ? $this->countAbilities() : 0;
$currentUser = wp_get_current_user();
// Detect a local dev environment heuristically. Self-signed/local
// certs trip Node's TLS validation in the npx proxy that Claude
// Desktop uses; if we know the user is on dev, we pre-bake the
// workaround into the generated snippet. The Vue page also exposes
// a manual toggle for edge cases.
$isLocalDev = self::detectLocalDevEnvironment();
return [
'adapter_installed' => $adapterInstalled || $toolkitInstalled,
'adapter_active' => $adapterActive,
'adapter_provider' => $adapterProvider,
'standalone_adapter_installed' => $adapterInstalled,
'toolkit_installed' => $toolkitInstalled,
'toolkit_active' => $toolkitActive,
'toolkit_adapter_available' => $toolkitAdapterActive,
'adapter_runtime_available' => $adapterRuntimeAvailable,
'adapter_version' => $this->detectAdapterVersion(),
'toolkit_version' => $this->detectToolkitVersion(),
'abilities_api_loaded' => $abilitiesAvailable,
'endpoint_url' => MCPInit::getEndpointUrl(),
'tools_count' => $toolsCount,
'mcp_enabled' => fluentcrm_get_option('mcp_enabled', 'yes') === 'yes',
'pro_active' => defined('FLUENTCAMPAIGN'),
'app_passwords_url' => admin_url('profile.php#application-passwords-section'),
'plugins_url' => admin_url('plugins.php'),
'can_auto_install_adapter' => $canAutoInstall,
'toolkit_download_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
'current_user_login' => $currentUser ? $currentUser->user_login : '',
'is_local_dev' => $isLocalDev,
];
}
/**
* Toggle the kill-switch. Stored as a FluentCRM option so the lazy-register
* guard in app/Hooks/actions.php picks it up on the next request.
*/
public function toggle(Request $request)
{
$value = $request->get('mcp_enabled');
$enabled = is_string($value) ? ($value === 'yes' || $value === 'true' || $value === '1') : (bool) $value;
fluentcrm_update_option('mcp_enabled', $enabled ? 'yes' : 'no');
return [
'ok' => true,
'mcp_enabled' => $enabled,
'message' => $enabled
? __('MCP tools enabled. New requests will see the FluentCRM abilities.', 'fluent-crm')
: __('MCP tools disabled. The adapter will no longer report FluentCRM abilities.', 'fluent-crm'),
];
}
/**
* One-click adapter install. Free can only explain the missing dependency;
* Pro may opt in to the Fluent Toolkit background installer via hooks.
*/
public function installAdapter()
{
if (!current_user_can('install_plugins')) {
return $this->sendError([
'message' => __('Sorry! you do not have permission to install plugins', 'fluent-crm'),
]);
}
$canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
if (!$canAutoInstall) {
return $this->sendError([
'message' => __('Please install Fluent Toolkit from GitHub, then reload this page to connect FluentCRM with AI agents.', 'fluent-crm'),
'toolkit_download_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
]);
}
do_action('fluent_toolkit/do_auto_install');
wp_clean_plugins_cache();
if (!function_exists('is_plugin_active')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$toolkitInstalled = $this->isToolkitPresent();
$toolkitActive = $this->isToolkitLoaded();
$adapterRuntimeAvailable = $this->isAdapterRuntimeAvailable();
$toolkitAdapterAvailable = $toolkitActive && $this->isToolkitAdapterAvailable();
$isInstalled = $this->isAdapterPresent() || $toolkitInstalled;
$isActive = (is_plugin_active(self::ADAPTER_PLUGIN_FILE) && $adapterRuntimeAvailable) || $toolkitAdapterAvailable;
if ($isInstalled && $isActive) {
$message = __('Fluent Toolkit installed and activated. Reload the page to register FluentCRM MCP tools.', 'fluent-crm');
} elseif ($toolkitInstalled && $toolkitActive) {
$message = __('Fluent Toolkit is installed and active, but this version does not include the bundled MCP adapter yet. Please update Fluent Toolkit when the MCP-ready build is available, then reload this page.', 'fluent-crm');
} elseif ($toolkitInstalled) {
$message = __('Fluent Toolkit is installed but could not be activated automatically. Please activate Fluent Toolkit from the Plugins page, then reload this page.', 'fluent-crm');
} else {
$message = __('Could not install Fluent Toolkit automatically. Please install Fluent Toolkit manually, then reload this page.', 'fluent-crm');
}
return [
'is_installed' => $isInstalled,
'adapter_active' => $isActive,
'toolkit_active' => $toolkitActive,
'toolkit_adapter_available' => $toolkitAdapterAvailable,
'message' => $message,
];
}
/**
* Generate a copy-paste config snippet for the requested client.
*
* Every client uses WordPress Application Passwords — built into WP 5.6+,
* no extra plugin needed. Direct HTTP clients (Claude Code, Cursor,
* generic) carry credentials via Basic Auth header; the
* @automattic/mcp-wordpress-remote stdio bridge that Claude Desktop uses
* accepts the username/password directly via WP_API_USERNAME and
* WP_API_PASSWORD env vars and handles encoding itself.
*
* Placeholders used here are stable strings the Vue page substitutes via
* regex when the user fills the credentials inputs.
*/
public function getConfigSnippet(Request $request)
{
$client = sanitize_key((string) $request->get('client', 'claude-code'));
$endpoint = MCPInit::getEndpointUrl();
// Optional override from the Settings UI checkbox. When the user
// explicitly says "I'm on local dev" we add the TLS-bypass env var to
// Claude Desktop's snippet; when they say "no" we omit it even if
// auto-detection thinks otherwise.
$forceLocalDev = $request->get('local_dev');
if ($forceLocalDev === 'yes' || $forceLocalDev === '1' || $forceLocalDev === 'true') {
$isLocalDev = true;
} elseif ($forceLocalDev === 'no' || $forceLocalDev === '0' || $forceLocalDev === 'false') {
$isLocalDev = false;
} else {
$isLocalDev = self::detectLocalDevEnvironment();
}
// The Vue page replaces these tokens with the user's real values.
// Keep them stable + distinct so the regex stays simple.
$basicPlaceholder = '<base64(your-username:application-password)>';
$usernamePlaceholder = '<your-username>';
$passwordPlaceholder = '<your-application-password>';
$appPasswordsUrl = admin_url('profile.php#application-passwords-section');
switch ($client) {
case 'codex':
$snippet = sprintf(
"Settings → Connect to a custom MCP\n\nName: fluent-crm\nTransport: Streamable HTTP ← click this tab first\n\nURL: %s\n\nHeader:\n Key: Authorization\n Value: Basic %s\n\nClick Save.",
$endpoint,
$basicPlaceholder
);
$instructions = sprintf(
/* translators: %s: link to WP user profile application passwords section */
__('Open OpenAI Codex → Settings → Connect to a custom MCP. Click the "Streamable HTTP" tab. Generate a WordPress Application Password from %s, then paste username + app password into the inputs above — the Value field will auto-fill with the encoded Basic auth string.', 'fluent-crm'),
$appPasswordsUrl
);
break;
case 'cursor':
$snippet = wp_json_encode([
'mcpServers' => [
'fluent-crm' => [
'url' => $endpoint,
'type' => 'http',
'headers' => [
'Authorization' => 'Basic ' . $basicPlaceholder,
],
],
],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$instructions = __('Cursor speaks HTTP MCP natively. Fill in your username and application password above and the snippet will be ready to paste into Cursor → Settings → MCP. Restart Cursor afterwards.', 'fluent-crm');
break;
case 'generic':
$snippet = sprintf(
"URL: %s\nAuth: Authorization: Basic %s\n\n# Quick test (curl handles the base64 for you)\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\"}'",
$endpoint,
$basicPlaceholder,
$usernamePlaceholder,
$passwordPlaceholder,
$endpoint
);
$instructions = __('Use the URL + Basic Auth header with any HTTP MCP client. The endpoint speaks the standard MCP protocol — initialize, tools/list, tools/call — over JSON-RPC.', 'fluent-crm');
break;
case 'claude-desktop':
// Claude Desktop cannot speak HTTP MCP directly yet — it
// routes through @automattic/mcp-wordpress-remote, which
// accepts WP_API_USERNAME / WP_API_PASSWORD plain (proxy
// does the encoding). No JWT plugin needed.
$env = [
'WP_API_URL' => $endpoint,
'WP_API_USERNAME' => $usernamePlaceholder,
'WP_API_PASSWORD' => $passwordPlaceholder,
'OAUTH_ENABLED' => 'false',
];
if ($isLocalDev) {
// Self-signed certs trip Node's bundled CA store. Trust
// the connection wholesale for local dev — the proxy
// only ever talks to one URL the user explicitly chose,
// so the practical risk is bounded.
$env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
}
$snippet = wp_json_encode([
'mcpServers' => [
'fluent-crm' => [
'command' => 'npx',
'args' => ['-y', '@automattic/mcp-wordpress-remote@latest'],
'env' => $env,
],
],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$localDevNote = $isLocalDev
? ' ' . __('Local dev mode is on, so NODE_TLS_REJECT_UNAUTHORIZED is included — the npx proxy needs it to talk to self-signed Valet/MAMP/Local SSL.', 'fluent-crm')
: '';
$instructions = __('Paste into ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows), then restart Claude Desktop.', 'fluent-crm') . $localDevNote;
break;
case 'claude-code':
default:
$snippet = sprintf(
"claude mcp add \\\n --transport http \\\n fluent-crm %s \\\n --header \"Authorization: Basic %s\"",
$endpoint,
$basicPlaceholder
);
$instructions = __('Fill in your username and application password above, then paste the command into your terminal. Run `claude` and the FluentCRM tools will appear under MCP servers.', 'fluent-crm');
$client = 'claude-code';
break;
}
return [
'client' => $client,
'snippet' => $snippet,
'instructions' => $instructions,
'endpoint' => $endpoint,
'app_passwords_url' => $appPasswordsUrl,
'is_local_dev' => $isLocalDev,
];
}
/**
* Heuristic check for "we're running on a local development install."
*
* Tested in order:
* 1. Hostname ends in a dev TLD (.test, .lab, .local, .localhost)
* 2. Hostname is literally `localhost`
* 3. Host resolves to a private/loopback IP range
*
* Filterable via `fluent_crm/mcp_is_local_dev` so operators can override
* detection on edge cases (a public-facing site on `.local`, an internal
* tool that needs the dev-mode behavior anyway, etc.).
*
* @return bool
*/
private static function detectLocalDevEnvironment()
{
$host = wp_parse_url(home_url(), PHP_URL_HOST);
$host = strtolower((string) $host);
$isDev = false;
$devTlds = ['.test', '.lab', '.local', '.localhost', '.docker', '.dev'];
foreach ($devTlds as $tld) {
$len = strlen($tld);
if ($len > 0 && substr($host, -$len) === $tld) {
$isDev = true;
break;
}
}
if (!$isDev && ($host === 'localhost' || $host === '127.0.0.1' || $host === '::1')) {
$isDev = true;
}
if (!$isDev && filter_var($host, FILTER_VALIDATE_IP)) {
// Private IP ranges per RFC 1918.
$isPrivate = !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
if ($isPrivate) {
$isDev = true;
}
}
/**
* Override the local-dev detection. Useful when the heuristic gets
* it wrong (e.g. a public site on a `.local` mDNS hostname).
*
* @since 2.10.0
*
* @param bool $isDev Whether the install looks like local dev.
* @param string $host The detected hostname.
*/
return (bool) apply_filters('fluent_crm/mcp_is_local_dev', $isDev, $host);
}
// ---------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------
private function isAdapterPresent()
{
return $this->isPluginPresent(self::ADAPTER_PLUGIN_FILE);
}
private function isToolkitPresent()
{
return $this->isToolkitLoaded() || $this->isPluginPresent(self::TOOLKIT_PLUGIN_FILE);
}
private function detectAdapterVersion()
{
return $this->detectPluginVersion(self::ADAPTER_PLUGIN_FILE);
}
private function detectToolkitVersion()
{
if ($this->isToolkitLoaded()) {
return (string) FLUENT_TOOLKIT_VERSION;
}
return $this->detectPluginVersion(self::TOOLKIT_PLUGIN_FILE);
}
private function isToolkitLoaded()
{
return defined('FLUENT_TOOLKIT_VERSION');
}
private function isPluginPresent($pluginFile)
{
if (!function_exists('get_plugins')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugins = get_plugins();
return isset($plugins[$pluginFile]);
}
private function detectPluginVersion($pluginFile)
{
if (!function_exists('get_plugins')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugins = get_plugins();
if (!isset($plugins[$pluginFile])) {
return null;
}
return $plugins[$pluginFile]['Version'] ?? null;
}
private function isToolkitAdapterAvailable()
{
if (!$this->isToolkitLoaded()) {
return false;
}
if (class_exists('\FluentToolkit\Mcp\AdapterBootstrap') && method_exists('\FluentToolkit\Mcp\AdapterBootstrap', 'available')) {
return (bool) \FluentToolkit\Mcp\AdapterBootstrap::available();
}
return $this->isAdapterRuntimeAvailable();
}
private function isAdapterRuntimeAvailable()
{
return defined('WP_MCP_VERSION')
&& class_exists('\WP\MCP\Core\McpAdapter')
&& function_exists('wp_register_ability');
}
private function countAbilities()
{
$count = count(AbilitiesRegistrar::getDefinitions());
// Pro tools are pushed onto the names list via the
// `fluent_crm/mcp_ability_names` filter in MCPInit.
$names = apply_filters('fluent_crm/mcp_ability_names', array_keys(AbilitiesRegistrar::getDefinitions()));
if (is_array($names)) {
$count = count(array_unique($names));
}
return $count;
}
}