| @@ -2,8 +2,9 @@ | ||
| 2 | 2 | |
| 3 | 3 | namespace FluentBoards\App\Http\Controllers; |
| 4 | 4 | |
| 5 | 5 | use FluentBoards\App\Modules\MCP\MCPInit; |
| 6 | +use FluentBoards\App\Services\MCPAccessService; | |
| 6 | 7 | use FluentBoards\Framework\Http\Request\Request; |
| 7 | 8 | |
| 8 | 9 | /** |
| 9 | 10 | * Settings endpoints for connecting Fluent Boards MCP tools to AI agents. |
| @@ -45,14 +46,14 @@ | ||
| 45 | 46 | 'abilities_api_loaded' => $abilitiesAvailable, |
| 46 | 47 | 'endpoint_url' => MCPInit::getEndpointUrl(), |
| 47 | 48 | 'tools_count' => $abilitiesAvailable ? MCPInit::getToolsCount() : 0, |
| 48 | 49 | 'mcp_enabled' => fluent_boards_get_option('mcp_enabled', 'yes') === 'yes', |
| 49 | - 'app_passwords_url' => admin_url('profile.php#application-passwords-section'), | |
| 50 | + 'mcp_member_access' => MCPAccessService::isMemberAccessEnabled(), | |
| 50 | 51 | 'plugins_url' => admin_url('plugins.php'), |
| 51 | 52 | 'can_auto_install_adapter' => $canAutoInstall, |
| 52 | 53 | 'toolkit_download_url' => 'https://github.com/WPManageNinja/fluent-toolkit', |
| 53 | 54 | 'current_user_login' => $currentUser ? $currentUser->user_login : '', |
| 54 | - 'is_local_dev' => self::detectLocalDevEnvironment(), | |
| 55 | + 'is_local_dev' => MCPAccessService::isLocalDev(), | |
| 55 | 56 | ]); |
| 56 | 57 | } |
| 57 | 58 | |
| 58 | 59 | public function toggle(Request $request) |
| @@ -70,8 +71,27 @@ | ||
| 70 | 71 | : __('MCP tools disabled. The adapter will no longer report Fluent Boards abilities.', 'fluent-boards'), |
| 71 | 72 | ]); |
| 72 | 73 | } |
| 73 | 74 | |
| 75 | + /** | |
| 76 | + * All-or-none switch: when on, every board member can use MCP and create keys from Account. | |
| 77 | + * Turning it off blocks members immediately; their keys are kept but stop working. | |
| 78 | + */ | |
| 79 | + public function toggleMemberAccess(Request $request) | |
| 80 | + { | |
| 81 | + $value = $request->get('mcp_member_access'); | |
| 82 | + $enabled = is_string($value) ? in_array($value, ['yes', 'true', '1'], true) : (bool) $value; | |
| 83 | + | |
| 84 | + fluent_boards_update_option('mcp_member_access', $enabled ? 'yes' : 'no'); | |
| 85 | + | |
| 86 | + return $this->sendSuccess([ | |
| 87 | + 'mcp_member_access' => $enabled, | |
| 88 | + 'message' => $enabled | |
| 89 | + ? __('Board members can now create their own MCP connections.', 'fluent-boards') | |
| 90 | + : __('MCP is now limited to administrators. Member connections are blocked.', 'fluent-boards'), | |
| 91 | + ]); | |
| 92 | + } | |
| 93 | + | |
| 74 | 94 | public function installAdapter() |
| 75 | 95 | { |
| 76 | 96 | if (!current_user_can('install_plugins')) { |
| 77 | 97 | return $this->sendError([ |
| @@ -118,141 +138,8 @@ | ||
| 118 | 138 | 'toolkit_active' => $toolkitActive, |
| 119 | 139 | 'toolkit_adapter_available' => $toolkitAdapterAvailable, |
| 120 | 140 | 'message' => $message, |
| 121 | 141 | ]); |
| 122 | - } | |
| 123 | - | |
| 124 | - public function getConfigSnippet(Request $request) | |
| 125 | - { | |
| 126 | - $client = sanitize_key((string) $request->get('client', 'claude-code')); | |
| 127 | - $endpoint = MCPInit::getEndpointUrl(); | |
| 128 | - $isLocalDev = $this->resolveLocalDevOverride($request); | |
| 129 | - | |
| 130 | - $basicPlaceholder = '<base64(your-username:application-password)>'; | |
| 131 | - $usernamePlaceholder = '<your-username>'; | |
| 132 | - $passwordPlaceholder = '<your-application-password>'; | |
| 133 | - $appPasswordsUrl = admin_url('profile.php#application-passwords-section'); | |
| 134 | - | |
| 135 | - switch ($client) { | |
| 136 | - case 'codex': | |
| 137 | - $snippet = sprintf( | |
| 138 | - "Settings > Connect to a custom MCP\n\nName: fluent-boards\nTransport: Streamable HTTP\n\nURL: %s\n\nHeader:\n Key: Authorization\n Value: Basic %s\n\nClick Save.", | |
| 139 | - $endpoint, | |
| 140 | - $basicPlaceholder | |
| 141 | - ); | |
| 142 | - $instructions = sprintf( | |
| 143 | - __('Open OpenAI Codex settings, connect to a custom MCP, choose Streamable HTTP, then use a WordPress Application Password from %s.', 'fluent-boards'), | |
| 144 | - $appPasswordsUrl | |
| 145 | - ); | |
| 146 | - break; | |
| 147 | - | |
| 148 | - case 'cursor': | |
| 149 | - $snippet = wp_json_encode([ | |
| 150 | - 'mcpServers' => [ | |
| 151 | - 'fluent-boards' => [ | |
| 152 | - 'url' => $endpoint, | |
| 153 | - 'type' => 'http', | |
| 154 | - 'headers' => [ | |
| 155 | - 'Authorization' => 'Basic ' . $basicPlaceholder, | |
| 156 | - ], | |
| 157 | - ], | |
| 158 | - ], | |
| 159 | - ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 160 | - $instructions = __('Paste this into Cursor Settings > MCP, then restart Cursor.', 'fluent-boards'); | |
| 161 | - break; | |
| 162 | - | |
| 163 | - case 'generic': | |
| 164 | - $snippet = sprintf( | |
| 165 | - "URL: %s\nAuth: Authorization: Basic %s\n\n# Quick test\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\"}'", | |
| 166 | - $endpoint, | |
| 167 | - $basicPlaceholder, | |
| 168 | - $usernamePlaceholder, | |
| 169 | - $passwordPlaceholder, | |
| 170 | - $endpoint | |
| 171 | - ); | |
| 172 | - $instructions = __('Use the URL and Basic Auth header with any HTTP MCP client. The endpoint speaks MCP over JSON-RPC.', 'fluent-boards'); | |
| 173 | - break; | |
| 174 | - | |
| 175 | - case 'claude-desktop': | |
| 176 | - $env = [ | |
| 177 | - 'WP_API_URL' => $endpoint, | |
| 178 | - 'WP_API_USERNAME' => $usernamePlaceholder, | |
| 179 | - 'WP_API_PASSWORD' => $passwordPlaceholder, | |
| 180 | - 'OAUTH_ENABLED' => 'false', | |
| 181 | - 'NODE_TLS_REJECT_UNAUTHORIZED' => '0', | |
| 182 | - ]; | |
| 183 | - | |
| 184 | - $snippet = wp_json_encode([ | |
| 185 | - 'mcpServers' => [ | |
| 186 | - 'fluent-boards' => [ | |
| 187 | - 'command' => 'npx', | |
| 188 | - 'args' => ['-y', '@automattic/mcp-wordpress-remote@latest'], | |
| 189 | - 'env' => $env, | |
| 190 | - ], | |
| 191 | - ], | |
| 192 | - ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); | |
| 193 | - | |
| 194 | - $instructions = __('Paste into the Claude Desktop config file, then restart Claude Desktop.', 'fluent-boards'); | |
| 195 | - break; | |
| 196 | - | |
| 197 | - case 'claude-code': | |
| 198 | - default: | |
| 199 | - $snippet = sprintf( | |
| 200 | - "claude mcp add \\\n --transport http \\\n fluent-boards %s \\\n --header \"Authorization: Basic %s\"", | |
| 201 | - $endpoint, | |
| 202 | - $basicPlaceholder | |
| 203 | - ); | |
| 204 | - $instructions = __('Fill in your username and application password above, then paste the command into your terminal.', 'fluent-boards'); | |
| 205 | - $client = 'claude-code'; | |
| 206 | - break; | |
| 207 | - } | |
| 208 | - | |
| 209 | - return $this->sendSuccess([ | |
| 210 | - 'client' => $client, | |
| 211 | - 'snippet' => $snippet, | |
| 212 | - 'instructions' => $instructions, | |
| 213 | - 'endpoint' => $endpoint, | |
| 214 | - 'app_passwords_url' => $appPasswordsUrl, | |
| 215 | - 'is_local_dev' => $isLocalDev, | |
| 216 | - ]); | |
| 217 | - } | |
| 218 | - | |
| 219 | - private function resolveLocalDevOverride(Request $request) | |
| 220 | - { | |
| 221 | - $forceLocalDev = $request->get('local_dev'); | |
| 222 | - if ($forceLocalDev === 'yes' || $forceLocalDev === '1' || $forceLocalDev === 'true') { | |
| 223 | - return true; | |
| 224 | - } | |
| 225 | - | |
| 226 | - if ($forceLocalDev === 'no' || $forceLocalDev === '0' || $forceLocalDev === 'false') { | |
| 227 | - return false; | |
| 228 | - } | |
| 229 | - | |
| 230 | - return self::detectLocalDevEnvironment(); | |
| 231 | - } | |
| 232 | - | |
| 233 | - private static function detectLocalDevEnvironment() | |
| 234 | - { | |
| 235 | - $host = wp_parse_url(home_url(), PHP_URL_HOST); | |
| 236 | - $host = strtolower((string) $host); | |
| 237 | - | |
| 238 | - $isDev = false; | |
| 239 | - foreach (['.test', '.lab', '.local', '.localhost', '.docker', '.dev'] as $tld) { | |
| 240 | - if (substr($host, -strlen($tld)) === $tld) { | |
| 241 | - $isDev = true; | |
| 242 | - break; | |
| 243 | - } | |
| 244 | - } | |
| 245 | - | |
| 246 | - if (!$isDev && ($host === 'localhost' || $host === '127.0.0.1' || $host === '::1')) { | |
| 247 | - $isDev = true; | |
| 248 | - } | |
| 249 | - | |
| 250 | - if (!$isDev && filter_var($host, FILTER_VALIDATE_IP)) { | |
| 251 | - $isDev = !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); | |
| 252 | - } | |
| 253 | - | |
| 254 | - return (bool) apply_filters('fluent_boards/mcp_is_local_dev', $isDev, $host); | |
| 255 | 142 | } |
| 256 | 143 | |
| 257 | 144 | private function isAdapterPresent() |
| 258 | 145 | { |