/home/altere25/sportzsoftlivemeet.com/wp-content/plugins/stifli-flex-mcp
NameSizeModeActions
add-on/-0755rm
assets/-0755rm
client/-0755rm
copilot/-0755rm
languages/-0755rm
models/-0755rm
oauth/-0755rm
controller.php1420644editdlrm
mod.php2652730644editdlrm
readme.txt480000644editdlrm
stifli-flex-mcp.php1489280644editdlrm
uninstall.php19200644editdlrm
Edit: /home/altere25/sportzsoftlivemeet.com/wp-content/plugins/stifli-flex-mcp/mod.php (265273B)
prefix)) { $this->queueTable = StifliFlexMcpUtils::getPrefixedTable('sflmcp_queue', false); } } public function init() { add_action('rest_api_init', array($this, 'restApiInit')); add_filter('rest_post_dispatch', array($this, 'addNoCacheHeadersForNamespace'), 9, 3); add_action('sflmcp_process_task', array($this, 'processTaskAsync'), 10, 1); $this->ensureDispatcherCallbackRegistered(); // Register admin menu and settings when in WP admin if (is_admin()) { add_action('admin_menu', array($this, 'registerAdmin')); add_action('admin_menu', array($this, 'registerMcpServerSubmenu'), 15); add_action('admin_menu', array($this, 'registerMultimediaSubmenu'), 25); add_action('admin_init', array($this, 'registerSettings')); add_action('admin_init', array($this, 'handleOAuthWellKnownNoticeDismiss')); add_action('admin_notices', array($this, 'renderOAuthWellKnownNotice')); add_action('admin_enqueue_scripts', array($this, 'enqueueAdminScripts')); // AJAX handlers for profiles management add_action('wp_ajax_sflmcp_create_profile', array($this, 'ajax_create_profile')); add_action('wp_ajax_sflmcp_update_profile', array($this, 'ajax_update_profile')); add_action('wp_ajax_sflmcp_delete_profile', array($this, 'ajax_delete_profile')); add_action('wp_ajax_sflmcp_duplicate_profile', array($this, 'ajax_duplicate_profile')); add_action('wp_ajax_sflmcp_apply_profile', array($this, 'ajax_apply_profile')); add_action('wp_ajax_sflmcp_export_profile', array($this, 'ajax_export_profile')); add_action('wp_ajax_sflmcp_import_profile', array($this, 'ajax_import_profile')); add_action('wp_ajax_sflmcp_restore_system_profiles', array($this, 'ajax_restore_system_profiles')); // AJAX handlers for custom tools add_action('wp_ajax_sflmcp_get_custom_tools', array($this, 'ajax_get_custom_tools')); add_action('wp_ajax_sflmcp_save_custom_tool', array($this, 'ajax_save_custom_tool')); add_action('wp_ajax_sflmcp_delete_custom_tool', array($this, 'ajax_delete_custom_tool')); add_action('wp_ajax_sflmcp_test_custom_tool', array($this, 'ajax_test_custom_tool')); add_action('wp_ajax_sflmcp_toggle_custom_tool', array($this, 'ajax_toggle_custom_tool')); // AJAX handlers for WordPress/WooCommerce tools add_action('wp_ajax_sflmcp_toggle_tool', array($this, 'ajax_toggle_tool')); add_action('wp_ajax_sflmcp_bulk_toggle_tools', array($this, 'ajax_bulk_toggle_tools')); add_action('wp_ajax_sflmcp_toggle_tool_by_checkbox', array($this, 'ajax_toggle_tool_by_checkbox')); add_action('wp_ajax_sflmcp_bulk_toggle_tools_by_id', array($this, 'ajax_bulk_toggle_tools_by_id')); // AJAX handlers for WordPress Abilities API (WordPress 6.9+) add_action('wp_ajax_sflmcp_discover_abilities', array($this, 'ajax_discover_abilities')); add_action('wp_ajax_sflmcp_import_ability', array($this, 'ajax_import_ability')); add_action('wp_ajax_sflmcp_toggle_ability', array($this, 'ajax_toggle_ability')); add_action('wp_ajax_sflmcp_delete_ability', array($this, 'ajax_delete_ability')); add_action('wp_ajax_sflmcp_get_imported_abilities', array($this, 'ajax_get_imported_abilities')); add_action('wp_ajax_sflmcp_bulk_manage_abilities', array($this, 'ajax_bulk_manage_abilities')); // AJAX handlers for Multimedia settings add_action('wp_ajax_sflmcp_save_multimedia_settings', array($this, 'ajax_save_multimedia_settings')); add_action('wp_ajax_sflmcp_load_multimedia_settings', array($this, 'ajax_load_multimedia_settings')); add_action('wp_ajax_sflmcp_mm_toggle_tool', array($this, 'ajax_mm_toggle_tool')); add_action('wp_ajax_sflmcp_mm_reveal_key', array($this, 'ajax_mm_reveal_key')); // AJAX handlers for OAuth Clients add_action('wp_ajax_sflmcp_oauth_delete_client', array($this, 'ajax_oauth_delete_client')); add_action('wp_ajax_sflmcp_oauth_revoke_token', array($this, 'ajax_oauth_revoke_token')); add_action('wp_ajax_sflmcp_oauth_reset_state', array($this, 'ajax_oauth_reset_state')); add_action('wp_ajax_sflmcp_oauth_save_settings', array($this, 'ajax_oauth_save_settings')); add_action('wp_ajax_sflmcp_generate_app_password', array($this, 'ajax_generate_app_password')); } } private function ensureDispatcherCallbackRegistered(): void { if ($this->addedFilter) { return; } StifliFlexMcpDispatcher::addFilter('sflmcp_callback', array($this, 'handleCallback'), 10, 4); $this->addedFilter = true; } public function restApiInit() { register_rest_route($this->namespace, '/sse', array( 'methods' => 'GET', 'callback' => array($this, 'handleSSE'), 'permission_callback' => function( $request ) { return $this->canAccessMCP($request); }, )); register_rest_route($this->namespace, '/sse', array( 'methods' => 'POST', 'callback' => array($this, 'handleSSE'), 'permission_callback' => function( $request ) { return $this->canAccessMCP($request); }, )); register_rest_route($this->namespace, '/messages', array( 'methods' => 'POST', 'callback' => array($this, 'handleMessage'), 'permission_callback' => function( $request ) { return $this->canAccessMCP($request); }, )); register_rest_route($this->namespace, '/messages', array( 'methods' => 'GET', 'callback' => array($this, 'handleMessagesGet'), 'permission_callback' => function( $request ) { return $this->canAccessMCP($request); }, )); $this->ensureDispatcherCallbackRegistered(); } /** * Check if the request can access the MCP endpoint. * WordPress 5.6+ handles Application Password authentication natively. * This method only checks that the user has sufficient capabilities. * * @see https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/ * @see https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/ */ public function canAccessMCP( $request ) { // --- Verbose debug logging --- $req_method = $request->get_method(); $req_route = $request->get_route(); $auth_hdr = $request->get_header( 'Authorization' ); stifli_flex_mcp_log( sprintf( 'canAccessMCP: %s %s | Auth: %s | User-Agent: %s', $req_method, $req_route, $auth_hdr ? substr( $auth_hdr, 0, 20 ) . '...' : '(none)', $request->get_header( 'User-Agent' ) ?: '(none)' ) ); // Streamable HTTP security: reject invalid Origin headers. $origin = trim( (string) $request->get_header( 'Origin' ) ); if ( '' !== $origin ) { $origin_host = wp_parse_url( $origin, PHP_URL_HOST ); $site_host = wp_parse_url( home_url(), PHP_URL_HOST ); $allowed_hosts = array_filter( apply_filters( 'sflmcp_allowed_origin_hosts', array( $site_host ) ) ); $allowed_hosts = array_map( static function( $host ) { return strtolower( (string) $host ); }, $allowed_hosts ); $origin_host = strtolower( (string) $origin_host ); if ( '' === $origin_host || ! in_array( $origin_host, $allowed_hosts, true ) ) { stifli_flex_mcp_log( sprintf( 'canAccessMCP: Invalid Origin header rejected (%s)', $origin ) ); return new WP_Error( 'invalid_origin', 'Invalid Origin header.', array( 'status' => 403 ) ); } } // --- Rate limiting: 30 requests/minute per IP --- $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '0.0.0.0'; $rate_key = 'sflmcp_rate_' . md5( $ip ); $rate_data = get_transient( $rate_key ); if ( false === $rate_data ) { $rate_data = array( 'count' => 0, 'start' => time() ); } $rate_data['count']++; $window = 60; // seconds $limit = 30; // max requests per window if ( ( time() - $rate_data['start'] ) > $window ) { // Window expired, reset. $rate_data = array( 'count' => 1, 'start' => time() ); } elseif ( $rate_data['count'] > $limit ) { stifli_flex_mcp_log( sprintf( 'canAccessMCP: Rate limit exceeded for IP %s (%d requests in %ds)', $ip, $rate_data['count'], time() - $rate_data['start'] ) ); return new WP_Error( 'rate_limit_exceeded', 'Rate limit exceeded. Max ' . $limit . ' requests per minute.', array( 'status' => 429 ) ); } set_transient( $rate_key, $rate_data, $window ); // --- OAuth 2.1 Bearer token validation --- $auth_header = $request->get_header( 'Authorization' ); if ( $auth_header && stripos( $auth_header, 'Bearer ' ) === 0 ) { $bearer_token = substr( $auth_header, 7 ); if ( class_exists( 'StifliFlexMcp_OAuth_Server' ) ) { $oauth_user_id = StifliFlexMcp_OAuth_Server::get_instance()->validate_token( $bearer_token ); if ( $oauth_user_id ) { wp_set_current_user( $oauth_user_id ); stifli_flex_mcp_log( sprintf( 'canAccessMCP: OAuth token validated for user %d', $oauth_user_id ) ); // Resolve OAuth client name for source tracking if ( class_exists( 'StifliFlexMcp_ChangeTracker' ) ) { $client_label = $this->resolveOAuthClientLabel( $bearer_token ); StifliFlexMcp_ChangeTracker::setSourceContext( 'mcp', $client_label ); } return true; } } // Bearer token present but invalid → 401. stifli_flex_mcp_log( 'canAccessMCP: Invalid OAuth Bearer token' ); return new WP_Error( 'invalid_token', 'Invalid or expired Bearer token.', array( 'status' => 401 ) ); } $current_user_id = get_current_user_id(); if ($current_user_id > 0 && current_user_can('edit_posts')) { stifli_flex_mcp_log(sprintf('canAccessMCP: user %d has sufficient capabilities', $current_user_id)); return true; } stifli_flex_mcp_log('canAccessMCP: Access denied - no authenticated user with edit_posts capability'); if ( class_exists( 'StifliFlexMcp_OAuth_Server' ) ) { return StifliFlexMcp_OAuth_Server::get_instance()->get_unauthorized_error(); } return false; } /** * Resolve OAuth client_name from a bearer token for source tracking. * * @param string $bearer_token Raw bearer token. * @return string Client name or empty string. */ private function resolveOAuthClientLabel( $bearer_token ) { if ( ! class_exists( 'StifliFlexMcp_OAuth_Storage' ) ) { return ''; } $storage = StifliFlexMcp_OAuth_Storage::get_instance(); $record = $storage->validate_access_token( $bearer_token ); if ( $record && ! empty( $record->client_id ) ) { $client = $storage->get_client( $record->client_id ); if ( $client && ! empty( $client->client_name ) ) { return $client->client_name; } } return ''; } public function handleCallback( $result, string $tool, array $args, $id ) { if (!empty($result)) { return $result; } $tools = $this->getModel()->getTools(); if (!isset($tools[$tool])) { StifliFlexMcpFrame::_()->saveDebugLogging('Tool not found ' . $tool, false, 'SFLMCP'); return $result; } return $this->getModel()->dispatchTool($tool, $args, $id); } private function getSSEid($req) { $last = $req ? $req->get_header('last-event-id') : ''; return empty($last) ? str_replace('-', '', wp_generate_uuid4()) : $last; } private function getSupportedProtocolVersions(): array { return array('2025-11-25', '2025-06-18', '2025-03-26'); } private function isSupportedProtocolVersion( $version ): bool { if (!is_string($version) || '' === $version) { return false; } return in_array($version, $this->getSupportedProtocolVersions(), true); } private function negotiateProtocolVersion( $requestedVersion ): string { if ($this->isSupportedProtocolVersion($requestedVersion)) { return $requestedVersion; } return $this->protocolVersion; } private function getProtocolVersionHeader( $request ) { $version = trim((string) $request->get_header('MCP-Protocol-Version')); return '' === $version ? null : $version; } private function protocolVersionErrorResponse( $id, string $requestedVersion ) { $response = new WP_REST_Response(array( 'jsonrpc' => '2.0', 'id' => $id, 'error' => array( 'code' => -32602, 'message' => 'Unsupported protocol version', 'data' => array( 'supported' => $this->getSupportedProtocolVersions(), 'requested' => $requestedVersion, ), ), ), 400); $response->set_headers(array('Content-Type' => 'application/json')); return $this->withNoCacheHeaders($response); } private function getNoCacheHeaders(): array { return array( 'Cache-Control' => 'no-store, no-cache, must-revalidate, private', 'Pragma' => 'no-cache', 'Expires' => '0', ); } private function withNoCacheHeaders( WP_REST_Response $response ): WP_REST_Response { foreach ($this->getNoCacheHeaders() as $name => $value) { $response->header($name, $value); } return $response; } public function addNoCacheHeadersForNamespace( $response, $server, $request ) { if (!is_object($response) || !method_exists($response, 'header')) { return $response; } $route = (is_object($request) && method_exists($request, 'get_route')) ? (string) $request->get_route() : ''; if ('' === $route) { return $response; } $prefix = '/' . $this->namespace; if (0 !== strpos($route, $prefix)) { return $response; } foreach ($this->getNoCacheHeaders() as $name => $value) { $response->header($name, $value); } return $response; } private function withProtocolHeaders( WP_REST_Response $response, string $protocolVersion, bool $hasJsonBody = true ): WP_REST_Response { $headers = array( 'MCP-Protocol-Version' => $protocolVersion, ); $headers = array_merge($headers, $this->getNoCacheHeaders()); if ($hasJsonBody) { $headers['Content-Type'] = 'application/json'; } $response->set_headers($headers); return $response; } private function getTaskManagedTools(): array { return array('wp_generate_image', 'wp_generate_video'); } private function toolRequiresTask( $tool ): bool { return is_string($tool) && in_array($tool, $this->getTaskManagedTools(), true); } private function normalizeArgumentsForTaskKey( $value ) { if (!is_array($value)) { return $value; } $isList = array_keys($value) === range(0, count($value) - 1); if (!$isList) { ksort($value); } foreach ($value as $k => $v) { $value[$k] = $this->normalizeArgumentsForTaskKey($v); } return $value; } private function getManagedFallbackTransientKey( int $userId, string $tool, array $arguments ): string { $normalized = $this->normalizeArgumentsForTaskKey($arguments); $encoded = wp_json_encode($normalized); if (false === $encoded) { $encoded = ''; } return 'sflmcp_mft_' . md5($userId . '|' . $tool . '|' . $encoded); } private function buildManagedFallbackWorkingReply( $id, string $taskId, string $tool, string $status = 'working', string $message = '' ): array { if ('' === $message) { $message = 'Generating media in background. Please wait and repeat the same tool call to retrieve the result.'; } return array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => array( 'content' => array( array( 'type' => 'text', 'text' => $message, ), ), 'structuredContent' => array( 'taskId' => $taskId, 'status' => $status, 'tool' => $tool, ), 'isError' => false, ), ); } private function withRequestIdFromPayload( array $payload, $id ): array { if (isset($payload['error']) && is_array($payload['error'])) { return array( 'jsonrpc' => '2.0', 'id' => $id, 'error' => $payload['error'], ); } if (isset($payload['result']) && is_array($payload['result'])) { return array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => $payload['result'], ); } return $this->rpcError($id, -32603, 'Task payload is invalid'); } private function handleManagedToolWithoutTask( $id, string $tool, array $arguments, array $params, string $toolLog ) { $userId = (int) get_current_user_id(); $fallbackKey = $this->getManagedFallbackTransientKey($userId, $tool, $arguments); $existingTaskId = get_transient($fallbackKey); if (is_string($existingTaskId) && '' !== $existingTaskId) { $existingTask = $this->loadTaskRecord($existingTaskId, true); if (is_array($existingTask)) { $status = isset($existingTask['status']) ? (string) $existingTask['status'] : 'working'; if ($this->isTaskTerminalStatus($status)) { stifli_flex_mcp_log('tools/call: returning completed fallback task result for tool=' . $toolLog . ' taskId=' . $existingTaskId); delete_transient($fallbackKey); $payload = isset($existingTask['payload']) && is_array($existingTask['payload']) ? $existingTask['payload'] : array(); return $this->withRequestIdFromPayload($payload, $id); } stifli_flex_mcp_log('tools/call: fallback task still working for tool=' . $toolLog . ' taskId=' . $existingTaskId); return $this->buildManagedFallbackWorkingReply($id, $existingTaskId, $tool, $status); } delete_transient($fallbackKey); } $taskReply = $this->createToolTaskResult($id, $tool, $arguments, $params); $taskId = isset($taskReply['result']['task']['taskId']) ? (string) $taskReply['result']['task']['taskId'] : ''; if ('' === $taskId) { return $this->rpcError($id, -32603, 'Failed to create background task'); } $taskTtlMs = isset($taskReply['result']['task']['ttl']) ? (int) $taskReply['result']['task']['ttl'] : (int) $this->taskDefaultTtlMs; $taskTtlSeconds = max(60, (int) ceil($taskTtlMs / 1000) + 300); set_transient($fallbackKey, $taskId, $taskTtlSeconds); stifli_flex_mcp_log('tools/call: created fallback background task for tool=' . $toolLog . ' taskId=' . $taskId); return $this->buildManagedFallbackWorkingReply($id, $taskId, $tool); } private function nowIso8601(): string { return gmdate('c'); } private function normalizeTaskTtlMs( $requestedTtl ): int { if (!is_numeric($requestedTtl)) { return (int) $this->taskDefaultTtlMs; } $ttl = (int) $requestedTtl; if ($ttl <= 0) { return (int) $this->taskDefaultTtlMs; } return min($ttl, (int) $this->taskMaxTtlMs); } private function getTaskTransientKey( string $taskId ): string { return 'sflmcp_task_' . md5($taskId); } private function getTaskIndexTransientKey( int $userId ): string { return 'sflmcp_task_index_' . $userId; } private function getTaskIndexForUser( int $userId ): array { if ($userId <= 0) { return array(); } $index = get_transient($this->getTaskIndexTransientKey($userId)); if (!is_array($index)) { return array(); } return array_values(array_filter($index, 'is_string')); } private function saveTaskIndexForUser( int $userId, array $taskIds, int $ttlSeconds ): void { if ($userId <= 0) { return; } $taskIds = array_values(array_unique(array_filter($taskIds, 'is_string'))); if (count($taskIds) > 200) { $taskIds = array_slice($taskIds, -200); } set_transient($this->getTaskIndexTransientKey($userId), $taskIds, max(60, $ttlSeconds)); } private function addTaskToUserIndex( int $userId, string $taskId, int $ttlSeconds ): void { $index = $this->getTaskIndexForUser($userId); $index[] = $taskId; $this->saveTaskIndexForUser($userId, $index, $ttlSeconds); } private function removeTaskFromUserIndex( int $userId, string $taskId ): void { $index = $this->getTaskIndexForUser($userId); if (empty($index)) { return; } $index = array_values(array_filter($index, static function( $value ) use ( $taskId ) { return $value !== $taskId; })); $this->saveTaskIndexForUser($userId, $index, 3600); } private function isTaskTerminalStatus( $status ): bool { return in_array($status, array('completed', 'failed', 'cancelled'), true); } private function isTaskExpired( array $task ): bool { if (!isset($task['ttl']) || is_null($task['ttl'])) { return false; } $createdAt = isset($task['createdAt']) ? strtotime((string) $task['createdAt']) : false; if (false === $createdAt) { return false; } $expiresAtMs = ((int) $createdAt * 1000) + (int) $task['ttl']; $nowMs = (int) round(microtime(true) * 1000); return $nowMs > $expiresAtMs; } private function saveTaskRecord( array $task ): void { if (empty($task['taskId'])) { return; } $ttlMs = isset($task['ttl']) && !is_null($task['ttl']) ? (int) $task['ttl'] : (int) $this->taskDefaultTtlMs; $ttlSeconds = max(60, (int) ceil($ttlMs / 1000) + 300); set_transient($this->getTaskTransientKey($task['taskId']), $task, $ttlSeconds); $ownerUserId = isset($task['ownerUserId']) ? (int) $task['ownerUserId'] : 0; if ($ownerUserId > 0) { $this->addTaskToUserIndex($ownerUserId, $task['taskId'], $ttlSeconds); } } private function loadTaskRecord( string $taskId, bool $pruneExpired = true ) { $task = get_transient($this->getTaskTransientKey($taskId)); if (!is_array($task)) { return null; } if ($pruneExpired && $this->isTaskExpired($task)) { $this->deleteTaskRecord($taskId, $task); return null; } return $task; } private function deleteTaskRecord( string $taskId, $existingTask = null ): void { $task = is_array($existingTask) ? $existingTask : $this->loadTaskRecord($taskId, false); delete_transient($this->getTaskTransientKey($taskId)); if (is_array($task) && isset($task['ownerUserId'])) { $this->removeTaskFromUserIndex((int) $task['ownerUserId'], $taskId); } } private function formatTaskForResult( array $task ): array { $result = array( 'taskId' => isset($task['taskId']) ? $task['taskId'] : '', 'status' => isset($task['status']) ? $task['status'] : 'working', 'createdAt' => isset($task['createdAt']) ? $task['createdAt'] : $this->nowIso8601(), 'lastUpdatedAt' => isset($task['lastUpdatedAt']) ? $task['lastUpdatedAt'] : $this->nowIso8601(), 'ttl' => isset($task['ttl']) ? $task['ttl'] : (int) $this->taskDefaultTtlMs, 'pollInterval' => isset($task['pollInterval']) ? (int) $task['pollInterval'] : (int) $this->taskPollIntervalMs, ); if (!empty($task['statusMessage'])) { $result['statusMessage'] = $task['statusMessage']; } return $result; } private function getScopedTaskRecord( string $taskId, $id, &$errorReply = null ) { $task = $this->loadTaskRecord($taskId, true); if (!is_array($task)) { $errorReply = $this->rpcError($id, -32602, 'Failed to retrieve task: Task not found'); return null; } $currentUserId = get_current_user_id(); $ownerUserId = isset($task['ownerUserId']) ? (int) $task['ownerUserId'] : 0; if ($ownerUserId > 0 && $currentUserId > 0 && $ownerUserId !== $currentUserId) { $errorReply = $this->rpcError($id, -32602, 'Failed to retrieve task: Task not found'); return null; } return $task; } private function createToolTaskResult( $id, string $tool, array $arguments, array $params ): array { $taskParams = isset($params['task']) && is_array($params['task']) ? $params['task'] : array(); $ttlMs = $this->normalizeTaskTtlMs(isset($taskParams['ttl']) ? $taskParams['ttl'] : null); $taskId = wp_generate_uuid4(); $now = $this->nowIso8601(); $task = array( 'taskId' => $taskId, 'status' => 'working', 'statusMessage' => 'The operation is now in progress.', 'createdAt' => $now, 'lastUpdatedAt' => $now, 'ttl' => $ttlMs, 'pollInterval' => (int) $this->taskPollIntervalMs, 'ownerUserId' => (int) get_current_user_id(), 'toolName' => $tool, 'toolArguments' => $arguments, 'requestId' => $id, 'payload' => null, ); $this->saveTaskRecord($task); if (!wp_next_scheduled('sflmcp_process_task', array($taskId))) { wp_schedule_single_event(time() + 1, 'sflmcp_process_task', array($taskId)); if (function_exists('spawn_cron')) { spawn_cron(time()); } } return array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => array( 'task' => $this->formatTaskForResult($task), '_meta' => array( 'io.modelcontextprotocol/model-immediate-response' => 'Generating media. Please wait and poll the task status.', ), ), ); } public function processTaskAsync( $taskId ): void { $taskId = is_scalar($taskId) ? (string) $taskId : ''; if ('' === $taskId) { return; } $task = $this->loadTaskRecord($taskId, true); if (!is_array($task)) { return; } if ($this->isTaskTerminalStatus(isset($task['status']) ? $task['status'] : '')) { return; } stifli_flex_mcp_log('task/process: start taskId=' . $taskId . ' tool=' . (isset($task['toolName']) ? $task['toolName'] : 'n/a')); if ( function_exists( 'ignore_user_abort' ) ) { ignore_user_abort(true); } // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged,WordPress.PHP.NoSilencedErrors.Discouraged -- required to avoid execution cut during long-running task processing. @set_time_limit(0); $originalUserId = get_current_user_id(); $ownerUserId = isset($task['ownerUserId']) ? (int) $task['ownerUserId'] : 0; if ($ownerUserId > 0 && $ownerUserId !== $originalUserId) { wp_set_current_user($ownerUserId); } $this->ensureDispatcherCallbackRegistered(); try { $toolName = isset($task['toolName']) ? (string) $task['toolName'] : ''; $toolArgs = isset($task['toolArguments']) && is_array($task['toolArguments']) ? $task['toolArguments'] : array(); $requestId = isset($task['requestId']) ? $task['requestId'] : null; $payload = $this->executeTool($toolName, $toolArgs, $requestId); $status = 'completed'; $statusMessage = 'Task completed successfully.'; if (is_array($payload) && isset($payload['error'])) { $status = 'failed'; $errorMessage = isset($payload['error']['message']) ? (string) $payload['error']['message'] : 'Unknown error'; $statusMessage = 'Task failed: ' . $errorMessage; } elseif (is_array($payload) && isset($payload['result']['isError']) && true === $payload['result']['isError']) { $status = 'failed'; $statusMessage = 'Task failed during tool execution.'; } $task['payload'] = $payload; $task['status'] = $status; $task['statusMessage'] = $statusMessage; $task['lastUpdatedAt'] = $this->nowIso8601(); $this->saveTaskRecord($task); stifli_flex_mcp_log('task/process: done taskId=' . $taskId . ' status=' . $status); } catch ( Throwable $e ) { $task['payload'] = $this->rpcError(isset($task['requestId']) ? $task['requestId'] : null, -32603, 'Internal task error', $e->getMessage()); $task['status'] = 'failed'; $task['statusMessage'] = 'Task failed: ' . $e->getMessage(); $task['lastUpdatedAt'] = $this->nowIso8601(); $this->saveTaskRecord($task); stifli_flex_mcp_log('task/process: failed taskId=' . $taskId . ' message=' . $e->getMessage()); } if ($ownerUserId !== $originalUserId) { wp_set_current_user($originalUserId); } } private function handleTasksMethod( string $method, array $params, $id ) { $taskId = isset($params['taskId']) && is_scalar($params['taskId']) ? trim((string) $params['taskId']) : ''; switch ($method) { case 'tasks/get': if ('' === $taskId) { return $this->rpcError($id, -32602, 'Failed to retrieve task: taskId is required'); } $errorReply = null; $task = $this->getScopedTaskRecord($taskId, $id, $errorReply); if (!is_array($task)) { return $errorReply; } return array('jsonrpc' => '2.0', 'id' => $id, 'result' => $this->formatTaskForResult($task)); case 'tasks/result': if ('' === $taskId) { return $this->rpcError($id, -32602, 'Failed to retrieve task: taskId is required'); } $errorReply = null; $task = $this->getScopedTaskRecord($taskId, $id, $errorReply); if (!is_array($task)) { return $errorReply; } if (!$this->isTaskTerminalStatus(isset($task['status']) ? $task['status'] : '')) { $this->processTaskAsync($taskId); $task = $this->loadTaskRecord($taskId, true); } if (!is_array($task)) { return $this->rpcError($id, -32602, 'Failed to retrieve task: Task not found'); } if (!$this->isTaskTerminalStatus(isset($task['status']) ? $task['status'] : '')) { return $this->rpcError($id, -32603, 'Task is still in progress'); } $payload = isset($task['payload']) ? $task['payload'] : null; if (!is_array($payload)) { return $this->rpcError($id, -32603, 'Task has no result payload'); } if (isset($payload['error']) && is_array($payload['error'])) { return array('jsonrpc' => '2.0', 'id' => $id, 'error' => $payload['error']); } if (!isset($payload['result']) || !is_array($payload['result'])) { return $this->rpcError($id, -32603, 'Task payload is invalid'); } $result = $payload['result']; if (!isset($result['_meta']) || !is_array($result['_meta'])) { $result['_meta'] = array(); } $result['_meta']['io.modelcontextprotocol/related-task'] = array('taskId' => $taskId); return array('jsonrpc' => '2.0', 'id' => $id, 'result' => $result); case 'tasks/cancel': if ('' === $taskId) { return $this->rpcError($id, -32602, 'Cannot cancel task: taskId is required'); } $errorReply = null; $task = $this->getScopedTaskRecord($taskId, $id, $errorReply); if (!is_array($task)) { return $errorReply; } if ($this->isTaskTerminalStatus(isset($task['status']) ? $task['status'] : '')) { return $this->rpcError($id, -32602, "Cannot cancel task: already in terminal status '" . $task['status'] . "'"); } $task['status'] = 'cancelled'; $task['statusMessage'] = 'The task was cancelled by request.'; $task['lastUpdatedAt'] = $this->nowIso8601(); $this->saveTaskRecord($task); return array('jsonrpc' => '2.0', 'id' => $id, 'result' => $this->formatTaskForResult($task)); case 'tasks/list': $cursor = isset($params['cursor']) ? (string) $params['cursor'] : ''; if ('' !== $cursor && !ctype_digit($cursor)) { return $this->rpcError($id, -32602, 'Invalid cursor'); } $offset = '' === $cursor ? 0 : (int) $cursor; $limit = 20; $userId = (int) get_current_user_id(); $index = $this->getTaskIndexForUser($userId); $tasks = array(); $validTaskIds = array(); foreach ($index as $taskIdFromIndex) { $task = $this->loadTaskRecord($taskIdFromIndex, true); if (!is_array($task)) { continue; } $validTaskIds[] = $taskIdFromIndex; $tasks[] = $task; } $this->saveTaskIndexForUser($userId, $validTaskIds, 3600); usort($tasks, static function( $a, $b ) { $ta = isset($a['createdAt']) ? strtotime((string) $a['createdAt']) : 0; $tb = isset($b['createdAt']) ? strtotime((string) $b['createdAt']) : 0; return $tb <=> $ta; }); $total = count($tasks); $slice = array_slice($tasks, $offset, $limit); $result = array( 'tasks' => array_map(array($this, 'formatTaskForResult'), $slice), ); if (($offset + $limit) < $total) { $result['nextCursor'] = (string) ($offset + $limit); } return array('jsonrpc' => '2.0', 'id' => $id, 'result' => $result); default: return $this->rpcError($id, -32601, 'Method not found: ' . $method); } } private function handleToolsCallMethod( $id, array $params ) { $tool = null; $arguments = array(); if (isset($params['name'])) { $tool = $params['name']; $arguments = isset($params['arguments']) && is_array($params['arguments']) ? $params['arguments'] : array(); } elseif (isset($params['tool'])) { $tool = $params['tool']; $arguments = isset($params['args']) && is_array($params['args']) ? $params['args'] : array(); } $isTaskAugmented = isset($params['task']) && is_array($params['task']); $toolLog = wp_json_encode($tool); if (false === $toolLog) { $toolLog = is_scalar($tool) ? (string) $tool : '[unserializable]'; } $argsLog = wp_json_encode($arguments); if (false === $argsLog) { $argsLog = '[unserializable]'; } stifli_flex_mcp_log(sprintf('tools/call: tool=%s arguments=%s task_augmented=%s', $toolLog, $argsLog, $isTaskAugmented ? 'yes' : 'no')); if ($this->toolRequiresTask($tool)) { if ($isTaskAugmented) { stifli_flex_mcp_log('tools/call: creating async task for tool=' . $toolLog); return $this->createToolTaskResult($id, $tool, $arguments, $params); } // Compatibility fallback for clients that still don't support task augmentation. stifli_flex_mcp_log('tools/call: task missing for managed tool, using async compatibility fallback for tool=' . $toolLog); return $this->handleManagedToolWithoutTask($id, $tool, $arguments, $params, $toolLog); } if ($isTaskAugmented) { stifli_flex_mcp_log('tools/call: task augmentation rejected for unsupported tool=' . $toolLog); return $this->rpcError($id, -32601, 'Task augmentation is not supported for tool: ' . $tool); } return $this->executeTool($tool, $arguments, $id); } public function handleSSE( $request ) { $body = $request->get_body(); $remote = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'n/a'; $uaHeader = $request->get_header('User-Agent'); $ua = $uaHeader ? sanitize_text_field( $uaHeader ) : 'n/a'; $hdrAuth = $request->get_header('Authorization') ? 'present' : 'none'; $qp = $request->get_param('token') ? 'present' : 'none'; stifli_flex_mcp_log(sprintf('handleSSE start: remote=%s, method=%s, auth_header=%s, query_token=%s, body_len=%d, ua=%s', $remote, $request->get_method(), $hdrAuth, $qp, strlen($body), $ua)); if ($request->get_method() === 'POST' && !empty($body)) { $data = json_decode($body, true); if ($data && isset($data['method'])) { return $this->handleDirectJsonRPC($request, $data); } } if ( function_exists( 'ini_set' ) ) { ini_set('zlib.output_compression', '0'); // phpcs:ignore WordPress.PHP.DiscouragedFunctions.runtime_ini_set,Squiz.PHP.DiscouragedFunctions.Discouraged ini_set('output_buffering', '0'); // phpcs:ignore WordPress.PHP.DiscouragedFunctions.runtime_ini_set,Squiz.PHP.DiscouragedFunctions.Discouraged ini_set('implicit_flush', '1'); // phpcs:ignore WordPress.PHP.DiscouragedFunctions.runtime_ini_set,Squiz.PHP.DiscouragedFunctions.Discouraged } if (function_exists('ob_implicit_flush')) { ob_implicit_flush( true ); } header('Content-Type: text/event-stream'); header('Cache-Control: no-store, no-cache, must-revalidate, private'); header('Pragma: no-cache'); header('Expires: 0'); header('X-Accel-Buffering: no'); header('Connection: keep-alive'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Headers: Authorization, Content-Type'); while (ob_get_level()) { ob_end_flush(); } $this->sessionID = $this->getSSEid($request); $this->lastAction = time(); $msgUri = sprintf('%s/messages?session_id=%s', rest_url($this->namespace), $this->sessionID); // Note: Client must include HTTP Basic auth header when posting to msgUri stifli_flex_mcp_log('handleSSE: sessionID=' . $this->sessionID . ' msgUri=' . $msgUri); $this->reply('endpoint', $msgUri, 'text'); while (true) { $maxTime = $this->logging ? 60 : 60 * 5; $idle = ( time() - $this->lastAction ) >= $maxTime; if (connection_aborted() || $idle) { stifli_flex_mcp_log('handleSSE: connection aborted or idle, aborting session ' . $this->sessionID); $this->reply('bye'); break; } foreach ($this->fetchMessages($this->sessionID) as $p) { if (isset($p['method']) && 'SFLMCP/kill' === $p['method']) { $this->reply('bye'); exit; } stifli_flex_mcp_log('handleSSE: sending message to session ' . $this->sessionID . ' method=' . (isset($p['method']) ? $p['method'] : 'n/a')); $this->reply('message', $p); } usleep(200000); if (time() - $this->lastAction > 10) $this->reply('heartbeat', ['status' => 'alive']); } exit; } private function reply( string $event, $data = null, string $enc = 'json' ) { if ('bye' === $event) { echo "event: bye\ndata: \n\n"; if (ob_get_level()) { ob_end_flush(); } flush(); $this->lastAction = time(); return; } if ('json' === $enc && null === $data) { return; } echo 'event: ' . esc_attr( $event ) . "\n"; if ('json' === $enc) { // SSE data is consumed by MCP clients (not HTML context). // wp_json_encode handles safe encoding; esc_html would break JSON // by converting " to ". phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $data = null === $data ? '{}' : str_replace('[]', '{}', wp_json_encode($data, JSON_UNESCAPED_UNICODE)); } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- SSE text/event-stream data, not HTML context echo 'data: ' . $data . "\n\n"; if (ob_get_level()) { ob_end_flush(); } flush(); $this->lastAction = time(); } public function handleDirectJsonRPC( $request, $data ) { $id = isset($data['id']) ? $data['id'] : null; $method = isset($data['method']) ? $data['method'] : null; $replyProtocolVersion = $this->protocolVersion; $qp = $request->get_param('token') ? 'present' : 'none'; $hdr = $request->get_header('Authorization') ? 'present' : 'none'; stifli_flex_mcp_log(sprintf('handleDirectJsonRPC: id=%s method=%s header=%s query=%s', $id, $method, $hdr, $qp)); // Set session_id for ChangeTracker — use query param or generate per request. if ( class_exists( 'StifliFlexMcp_ChangeTracker' ) ) { $sess = sanitize_text_field( $request->get_param( 'session_id' ) ); if ( ! $sess ) { $sess = 'mcp-' . wp_generate_uuid4(); } StifliFlexMcp_ChangeTracker::getInstance()->setSessionId( $sess ); } if (json_last_error() !== JSON_ERROR_NONE) { return new WP_REST_Response(array( 'jsonrpc' => '2.0', 'id' => null, 'error' => array('code' => -32700, 'message' => 'Parse error: invalid JSON'), ), 200); } if (!is_array($data) || !$method) { return new WP_REST_Response(array( 'jsonrpc' => '2.0', 'id' => $id, 'error' => array('code' => -32600, 'message' => 'Invalid Request'), ), 200); } $headerProtocolVersion = $this->getProtocolVersionHeader($request); if ('initialize' !== $method && null !== $headerProtocolVersion) { if (!$this->isSupportedProtocolVersion($headerProtocolVersion)) { return $this->protocolVersionErrorResponse($id, $headerProtocolVersion); } $replyProtocolVersion = $headerProtocolVersion; } try { $reply = null; switch ($method) { case 'initialize': $params = StifliFlexMcpUtils::getArrayValue($data, 'params', array(), 2); $reqVersion = StifliFlexMcpUtils::getArrayValue($params, 'protocolVersion', null); $replyProtocolVersion = $this->negotiateProtocolVersion($reqVersion); $clientInfo = StifliFlexMcpUtils::getArrayValue($params, 'clientInfo', false); // Store MCP client name for source tracking if ( $clientInfo && class_exists( 'StifliFlexMcp_ChangeTracker' ) ) { $client_name = is_array( $clientInfo ) && ! empty( $clientInfo['name'] ) ? $clientInfo['name'] : ''; if ( $client_name ) { StifliFlexMcp_ChangeTracker::setSourceContext( 'mcp', $client_name ); } } $reply = array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => array( 'protocolVersion' => $replyProtocolVersion, 'serverInfo' => (object) array( 'name' => get_bloginfo('name') . ' StifliFlexMcp', 'version' => $this->serverVersion, ), 'capabilities' => array( 'tools' => array('listChanged' => true), 'prompts' => array('subscribe' => false, 'listChanged' => false), 'resources' => array('subscribe' => false, 'listChanged' => false), 'tasks' => array( 'list' => (object) array(), 'cancel' => (object) array(), 'requests' => array( 'tools' => array( 'call' => (object) array(), ), ), ), ), ), ); break; case 'ping': $reply = array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => (object) array(), ); break; case 'tools/list': $tools = $this->getToolsList(); $reply = array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => array('tools' => $tools), ); break; case 'tools/call': $params = StifliFlexMcpUtils::getArrayValue($data, 'params', array(), 2); $reply = $this->handleToolsCallMethod($id, $params); break; case 'tasks/get': case 'tasks/result': case 'tasks/list': case 'tasks/cancel': $params = StifliFlexMcpUtils::getArrayValue($data, 'params', array(), 2); $reply = $this->handleTasksMethod($method, $params, $id); break; case 'initialized': case 'notifications/initialized': return $this->withProtocolHeaders(new WP_REST_Response(null, 202), $replyProtocolVersion, false); case 'resources/list': $reply = array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => array('resources' => array()), ); break; case 'prompts/list': $reply = array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => array('prompts' => array()), ); break; default: if (is_null($id) && strpos($method, 'notifications/') === 0) { return $this->withProtocolHeaders(new WP_REST_Response(null, 202), $replyProtocolVersion, false); } $reply = array( 'jsonrpc' => '2.0', 'id' => $id, 'error' => array('code' => -44001, 'message' => "Method not found: {$method}"), ); } return $this->withProtocolHeaders(new WP_REST_Response($reply, 200), $replyProtocolVersion); } catch ( Exception $e ) { return $this->withProtocolHeaders(new WP_REST_Response(array( 'jsonrpc' => '2.0', 'id' => $id, 'error' => array('code' => -44000, 'message' => 'Internal error', 'data' => $e->getMessage()) ), 200), $replyProtocolVersion); } } public function handleMessagesGet( $request ) { $response = new WP_REST_Response(null, 405); $response->set_headers(array_merge($this->getNoCacheHeaders(), array( 'Allow' => 'POST', 'MCP-Protocol-Version' => $this->protocolVersion, ))); return $response; } public function handleMessage( $request ) { $sess = sanitize_text_field($request->get_param('session_id')); $body = $request->get_body(); $hdr = $request->get_header('Authorization') ? 'present' : 'none'; $qp = $request->get_param('token') ? 'present' : 'none'; $remote = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'n/a'; stifli_flex_mcp_log(sprintf('handleMessage: session=%s remote=%s header=%s query=%s body_len=%d', $sess, $remote, $hdr, $qp, strlen($body))); stifli_flex_mcp_log('handleMessage: RAW BODY: ' . $body); $data = json_decode($body, true); $decodedForLog = wp_json_encode($data); if (false === $decodedForLog) { $decodedForLog = '[unserializable]'; } stifli_flex_mcp_log('handleMessage: JSON decoded: ' . $decodedForLog); $id = isset($data['id']) ? $data['id'] : null; $method = StifliFlexMcpUtils::getArrayValue($data, 'method', null); $replyProtocolVersion = $this->protocolVersion; $headerProtocolVersion = $this->getProtocolVersionHeader($request); if ('initialize' !== $method && null !== $headerProtocolVersion) { if (!$this->isSupportedProtocolVersion($headerProtocolVersion)) { return $this->protocolVersionErrorResponse($id, $headerProtocolVersion); } $replyProtocolVersion = $headerProtocolVersion; } if ('initialized' === $method || 'notifications/initialized' === $method) { return $this->withProtocolHeaders(new WP_REST_Response(null, 202), $replyProtocolVersion, false); } if ('SFLMCP/kill' === $method) { $this->storeMessage($sess, array('jsonrpc' => '2.0', 'method' => 'SFLMCP/kill')); usleep( 100000 ); return $this->withProtocolHeaders(new WP_REST_Response(null, 202), $replyProtocolVersion, false); } if (is_null($id) && !is_null($method)) { return $this->withProtocolHeaders(new WP_REST_Response(null, 202), $replyProtocolVersion, false); } if (!$method) { $hasRpcResponseShape = is_array($data) && array_key_exists('id', $data) && (array_key_exists('result', $data) || array_key_exists('error', $data)); if ($hasRpcResponseShape) { return $this->withProtocolHeaders(new WP_REST_Response(null, 202), $replyProtocolVersion, false); } $this->queueError($sess, $id, -32900, 'Invalid Request: method missing'); return $this->withProtocolHeaders(new WP_REST_Response(null, 202), $replyProtocolVersion, false); } // Pass session_id to ChangeTracker for grouping if ( class_exists( 'StifliFlexMcp_ChangeTracker' ) ) { if ( ! $sess ) { $sess = 'sse-' . wp_generate_uuid4(); } StifliFlexMcp_ChangeTracker::getInstance()->setSessionId( $sess ); } try { $reply = null; switch ($method) { case 'initialize': $params = StifliFlexMcpUtils::getArrayValue($data, 'params', array(), 2); $requestedVersion = StifliFlexMcpUtils::getArrayValue($params, 'protocolVersion', null); $replyProtocolVersion = $this->negotiateProtocolVersion($requestedVersion); $clientInfo = StifliFlexMcpUtils::getArrayValue($params, 'clientInfo', null); // Store MCP client name for source tracking (e.g. "Claude", "ChatGPT") if ( $clientInfo && class_exists( 'StifliFlexMcp_ChangeTracker' ) ) { $client_name = is_array( $clientInfo ) && ! empty( $clientInfo['name'] ) ? $clientInfo['name'] : ''; if ( $client_name ) { StifliFlexMcp_ChangeTracker::setSourceContext( 'mcp', $client_name ); } } $reply = array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => array( 'protocolVersion' => $replyProtocolVersion, 'serverInfo' => (object) array( 'name' => get_bloginfo( 'name' ) . ' StifliFlexMcp', 'version' => $this->serverVersion, ), 'capabilities' => array( 'tools' => array('listChanged' => true), 'prompts' => array('subscribe' => false, 'listChanged' => false), 'resources' => array('subscribe' => false, 'listChanged' => false), 'tasks' => array( 'list' => (object) array(), 'cancel' => (object) array(), 'requests' => array( 'tools' => array( 'call' => (object) array(), ), ), ), ), ), ); break; case 'ping': $reply = array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => (object) array(), ); break; case 'tools/list': $tools = $this->getToolsList(); $reply = array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => array('tools' => $tools), ); break; case 'resources/list': $reply = array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => array('resources' => $this->getResourcesList()), ); break; case 'prompts/list': $reply = array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => array('prompts' => $this->getPromptsList()), ); break; case 'tools/call': $params = StifliFlexMcpUtils::getArrayValue($data, 'params', array(), 2); $reply = $this->handleToolsCallMethod($id, $params); break; case 'tasks/get': case 'tasks/result': case 'tasks/list': case 'tasks/cancel': $params = StifliFlexMcpUtils::getArrayValue($data, 'params', array(), 2); $reply = $this->handleTasksMethod($method, $params, $id); break; default: $reply = $this->rpcError($id, -45601, "Method not found: {$method}"); } if ($reply) { // Devolver la respuesta JSON-RPC directamente return $this->withProtocolHeaders(new WP_REST_Response($reply, 200), $replyProtocolVersion); } } catch ( Exception $e ) { $error = $this->rpcError($id, -45603, 'Internal error', $e->getMessage() ); return $this->withProtocolHeaders(new WP_REST_Response($error, 200), $replyProtocolVersion); } return $this->withProtocolHeaders(new WP_REST_Response(null, 202), $replyProtocolVersion, false); } public function getToolsList() { $model = new StifliFlexMcpModel(); return $model->getToolsList(); } private function getResourcesList() { return array(); } private function getPromptsList() { return array(); } private function executeTool( $tool, $args, $id ) { try { $toolLog = wp_json_encode($tool); if (false === $toolLog) { $toolLog = is_scalar($tool) ? (string) $tool : '[unserializable]'; } $argsLog = wp_json_encode($args); if (false === $argsLog) { $argsLog = '[unserializable]'; } $idLog = wp_json_encode($id); if (false === $idLog) { $idLog = is_scalar($id) ? (string) $id : '[unserializable]'; } stifli_flex_mcp_log(sprintf('executeTool: tool=%s args=%s id=%s', $toolLog, $argsLog, $idLog)); $filtered = StifliFlexMcpDispatcher::applyFilters('sflmcp_callback', null, $tool, $args, $id, $this); if (!is_null($filtered)) { if (is_array($filtered) && isset($filtered['jsonrpc']) && isset($filtered['id'])) { return $filtered; } return array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => $filtered, ); } throw new Exception("Unknown tool: {$tool}"); } catch ( Exception $e ) { stifli_flex_mcp_log('executeTool: Exception: ' . $e->getMessage()); return $this->rpcError( $id, -44003, $e->getMessage() ); } } private function rpcError( $id, int $code, string $msg, $extra = null ): array { $err = array('code' => $code, 'message' => $msg); if (!is_null($extra)) { $err['data'] = $extra; } return array('jsonrpc' => '2.0', 'id' => $id, 'error' => $err); } private function queueError( $sess, $id, int $code, string $msg, $extra = null ): void { $this->storeMessage($sess, $this->rpcError($id, $code, $msg, $extra)); } /* * Custom queue and profile storage relies on plugin-managed tables. * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,PluginCheck.Security.DirectDB.UnescapedDBParameter */ private function storeMessage( $sess, $payload ) { if (empty($sess) || empty($this->queueTable)) { return; } global $wpdb; $sessionKey = $this->normalizeSessionId($sess); if ('' === $sessionKey) { return; } $messageId = null; if (is_array($payload) && array_key_exists('id', $payload)) { if (is_null($payload['id'])) { $messageId = null; } elseif (is_scalar($payload['id'])) { $messageId = (string) $payload['id']; } else { $messageId = wp_json_encode($payload['id']); } } if (!is_null($messageId)) { $messageId = substr($messageId, 0, 191); } $nowTs = current_time('timestamp', true); $now = gmdate('Y-m-d H:i:s', $nowTs); $expires = gmdate('Y-m-d H:i:s', $nowTs + $this->queueTtl); $wpdb->insert( $this->queueTable, array( 'session_id' => $sessionKey, 'message_id' => $messageId, 'payload' => maybe_serialize($payload), 'created_at' => $now, 'expires_at' => $expires, ), array('%s', '%s', '%s', '%s', '%s') ); } private function fetchMessages( $sess ) { if (empty($sess) || empty($this->queueTable)) { return array(); } global $wpdb; $sessionKey = $this->normalizeSessionId($sess); if ('' === $sessionKey) { return array(); } $now = gmdate('Y-m-d H:i:s'); $queue_tbl = StifliFlexMcpUtils::getPrefixedTable('sflmcp_queue'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from sanitized helper. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, payload FROM {$queue_tbl} WHERE session_id = %s AND expires_at >= %s ORDER BY id ASC", $sessionKey, $now ), ARRAY_A ); if (empty($rows)) { return array(); } $ids = array(); $msgs = array(); foreach ($rows as $row) { $ids[] = (int) $row['id']; $decoded = maybe_unserialize($row['payload']); if ($decoded === false && 'b:0;' !== $row['payload']) { $decoded = $row['payload']; } $msgs[] = $decoded; } if (!empty($ids)) { foreach ($ids as $deleteId) { $wpdb->delete($this->queueTable, array('id' => $deleteId), array('%d')); } } return $msgs; } private function normalizeSessionId( $sess ) { if (empty($sess)) { return ''; } $sess = preg_replace('/[^A-Za-z0-9_\-]/', '', (string) $sess); return substr($sess, 0, 191); } private function getModel() { return new StifliFlexMcpModel(); } // ============ PROFILE MANAGEMENT AJAX HANDLERS ============ public function ajax_apply_profile() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'No permission'), 403); } check_ajax_referer('sflmcp_profiles'); global $wpdb; $profile_id = isset($_POST['profile_id']) ? absint( wp_unslash( $_POST['profile_id'] ) ) : 0; if ($profile_id <= 0) { wp_send_json_error(array('message' => 'Invalid profile ID')); } $profiles_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profiles', false); $profile_tools_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profile_tools', false); $tools_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_tools', false); $profiles_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profiles_table); $profile_tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profile_tools_table); $tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($tools_table); $profiles_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profiles_table); $profile_tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profile_tools_table); $tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($tools_table); $profiles_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profiles_table); $profile_tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profile_tools_table); $tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($tools_table); $profiles_table_sql = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profiles'); $profile_tools_table_sql = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profile_tools'); $tools_table_sql = StifliFlexMcpUtils::getPrefixedTable('sflmcp_tools'); // Get profile tools // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from sanitized helper. $profile_tools = $wpdb->get_col( $wpdb->prepare( "SELECT tool_name FROM {$profile_tools_table_sql} WHERE profile_id = %d", $profile_id ) ); if ($profile_tools === null) { wp_send_json_error(array('message' => 'Profile not found')); } // Disable all tools first // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from sanitized helper. $wpdb->query($wpdb->prepare( "UPDATE {$tools_table_sql} SET enabled = %d", 0 )); // Enable profile tools if (!empty($profile_tools)) { $placeholders = implode(',', array_fill(0, count($profile_tools), '%s')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from sanitized helper, placeholders are dynamic. $wpdb->query( $wpdb->prepare( "UPDATE {$tools_table_sql} SET enabled = 1 WHERE tool_name IN ({$placeholders})", ...$profile_tools ) ); } // Mark profile as active // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name from sanitized helper. $wpdb->query($wpdb->prepare( "UPDATE {$profiles_table_sql} SET is_active = %d", 0 )); $wpdb->update($profiles_table, array('is_active' => 1), array('id' => $profile_id), array('%d'), array('%d')); wp_send_json_success(array('message' => count($profile_tools) . ' herramientas habilitadas')); } public function ajax_delete_profile() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'No permission'), 403); } check_ajax_referer('sflmcp_profiles'); global $wpdb; $profile_id = isset($_POST['profile_id']) ? absint( wp_unslash( $_POST['profile_id'] ) ) : 0; $profiles_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profiles', false); $profiles_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profiles_table); // Check if system profile $system_query = sprintf('SELECT is_system FROM %s WHERE id = %%d', $profiles_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $is_system = $wpdb->get_var($wpdb->prepare($system_query, $profile_id)); if ($is_system === null) { wp_send_json_error(array('message' => 'Profile not found')); } if (intval($is_system) === 1) { wp_send_json_error(array('message' => 'Cannot delete system profiles')); } $wpdb->delete($profiles_table, array('id' => $profile_id), array('%d')); wp_send_json_success(); } public function ajax_duplicate_profile() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'No permission'), 403); } check_ajax_referer('sflmcp_profiles'); global $wpdb; $profile_id = isset($_POST['profile_id']) ? absint( wp_unslash( $_POST['profile_id'] ) ) : 0; $profiles_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profiles', false); $profile_tools_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profile_tools', false); $profiles_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profiles_table); $profile_tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profile_tools_table); // Get original profile $profile_query = sprintf('SELECT * FROM %s WHERE id = %%d', $profiles_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $original = $wpdb->get_row($wpdb->prepare($profile_query, $profile_id), ARRAY_A); if (!$original) { wp_send_json_error(array('message' => 'Profile not found')); } // Create new profile name $new_name = 'Copia de ' . $original['profile_name']; $counter = 1; $profile_name_check = sprintf('SELECT id FROM %s WHERE profile_name = %%s', $profiles_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. while ($wpdb->get_var($wpdb->prepare($profile_name_check, $new_name))) { $counter++; $new_name = 'Copia de ' . $original['profile_name'] . ' (' . $counter . ')'; } $now = current_time('mysql', true); // Insert new profile $wpdb->insert( $profiles_table, array( 'profile_name' => $new_name, 'profile_description' => $original['profile_description'], 'is_system' => 0, // Duplicates are always custom 'is_active' => 0, 'created_at' => $now, 'updated_at' => $now, ), array('%s', '%s', '%d', '%d', '%s', '%s') ); $new_profile_id = $wpdb->insert_id; // Copy tools $tools_query = sprintf('SELECT tool_name FROM %s WHERE profile_id = %%d', $profile_tools_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $tools = $wpdb->get_col($wpdb->prepare($tools_query, $profile_id)); foreach ($tools as $tool_name) { $wpdb->insert( $profile_tools_table, array( 'profile_id' => $new_profile_id, 'tool_name' => $tool_name, 'created_at' => $now, ), array('%d', '%s', '%s') ); } wp_send_json_success(); } public function ajax_export_profile() { if (!current_user_can('manage_options')) { wp_die('No permission', 403); } check_ajax_referer('sflmcp_profiles'); global $wpdb; $profile_id = intval($_GET['profile_id'] ?? 0); $profiles_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profiles', false); $profile_tools_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profile_tools', false); $tools_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_tools', false); $profiles_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profiles_table); $profile_tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profile_tools_table); $tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($tools_table); $profile_query = sprintf('SELECT * FROM %s WHERE id = %%d', $profiles_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $profile = $wpdb->get_row($wpdb->prepare($profile_query, $profile_id), ARRAY_A); if (!$profile) { wp_die('Profile not found', 404); } $tools_query = sprintf('SELECT tool_name FROM %s WHERE profile_id = %%d ORDER BY tool_name', $profile_tools_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $tools = $wpdb->get_col($wpdb->prepare($tools_query, $profile_id)); // Get categories $categories = array(); if (!empty($tools)) { $placeholders = implode(',', array_fill(0, count($tools), '%s')); $categories_query = 'SELECT DISTINCT category FROM ' . $tools_table_sql . ' WHERE tool_name IN (' . $placeholders . ') ORDER BY category'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses dynamic placeholders with prepare. $categories = $wpdb->get_col($wpdb->prepare($categories_query, ...$tools)); } $export = array( 'format_version' => '1.0', 'export_date' => gmdate('Y-m-d\TH:i:s\Z'), 'plugin_version' => '0.1.0', 'profile' => array( 'name' => $profile['profile_name'], 'description' => $profile['profile_description'], 'tools' => $tools, 'tools_count' => count($tools), 'categories_included' => $categories, ), ); $filename = sanitize_file_name($profile['profile_name']) . '-profile.json'; header('Content-Type: application/json'); header('Content-Disposition: attachment; filename="' . $filename . '"'); echo json_encode($export, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); exit; } public function ajax_import_profile() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'No permission'), 403); } check_ajax_referer('sflmcp_profiles'); global $wpdb; $profiles_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profiles', false); $profile_tools_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profile_tools', false); $tools_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_tools', false); $profiles_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profiles_table); $profile_tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profile_tools_table); $tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($tools_table); $json_data = isset($_POST['profile_json']) ? StifliFlexMcpUtils::sanitizeJsonString( sanitize_text_field( wp_unslash( $_POST['profile_json'] ) ) ) : ''; if (empty($json_data)) { wp_send_json_error(array('message' => 'No JSON data provided')); } $data = json_decode($json_data, true); if (!$data || !isset($data['profile'])) { wp_send_json_error(array('message' => 'Invalid JSON format')); } $profile = $data['profile']; $name = sanitize_text_field($profile['name'] ?? 'Importado'); $description = sanitize_textarea_field($profile['description'] ?? ''); $tools_list = isset($profile['tools']) && is_array($profile['tools']) ? $profile['tools'] : array(); $tools = array(); foreach ($tools_list as $tool_name) { if (!is_string($tool_name)) { continue; } $clean_tool = sanitize_key($tool_name); if ('' !== $clean_tool) { $tools[] = $clean_tool; } } // Check if name exists $counter = 1; $original_name = $name; $profile_name_check = sprintf('SELECT id FROM %s WHERE profile_name = %%s', $profiles_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. while ($wpdb->get_var($wpdb->prepare($profile_name_check, $name))) { $counter++; $name = $original_name . ' (' . $counter . ')'; } // Validate tools exist $existing_tools_query = sprintf('SELECT tool_name FROM %s WHERE 1 = %%d', $tools_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $existing_tools = $wpdb->get_col($wpdb->prepare($existing_tools_query, 1)); $valid_tools = array_intersect($tools, $existing_tools); if (empty($valid_tools)) { wp_send_json_error(array('message' => 'No valid tools found in profile')); } $now = current_time('mysql', true); // Insert profile $wpdb->insert( $profiles_table, array( 'profile_name' => $name, 'profile_description' => $description, 'is_system' => 0, 'is_active' => 0, 'created_at' => $now, 'updated_at' => $now, ), array('%s', '%s', '%d', '%d', '%s', '%s') ); $profile_id = $wpdb->insert_id; // Insert tools foreach ($valid_tools as $tool_name) { $wpdb->insert( $profile_tools_table, array( 'profile_id' => $profile_id, 'tool_name' => $tool_name, 'created_at' => $now, ), array('%d', '%s', '%s') ); } $ignored_count = count($tools) - count($valid_tools); $message = 'Perfil importado: ' . count($valid_tools) . ' herramientas'; if ($ignored_count > 0) { $message .= ' (' . $ignored_count . ' herramientas no encontradas fueron ignoradas)'; } wp_send_json_success(array('message' => $message)); } public function ajax_restore_system_profiles() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'No permission'), 403); } check_ajax_referer('sflmcp_profiles'); global $wpdb; $profiles_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profiles', false); $profile_tools_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profile_tools', false); $profiles_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profiles_table); $profile_tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profile_tools_table); // Delete existing system profiles $system_ids_query = sprintf('SELECT id FROM %s WHERE is_system = %%d', $profiles_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $system_ids = $wpdb->get_col($wpdb->prepare($system_ids_query, 1)); if (!empty($system_ids)) { $placeholders = implode(',', array_fill(0, count($system_ids), '%d')); $delete_relations_query = 'DELETE FROM ' . $profile_tools_table_sql . ' WHERE profile_id IN (' . $placeholders . ')'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses dynamic placeholders with prepare. $wpdb->query($wpdb->prepare($delete_relations_query, ...$system_ids)); $delete_profiles_query = sprintf('DELETE FROM %s WHERE is_system = %%d', $profiles_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $wpdb->query($wpdb->prepare($delete_profiles_query, 1)); } // Re-seed system profiles stifli_flex_mcp_seed_system_profiles(); wp_send_json_success(); } public function ajax_create_profile() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'No permission'), 403); } check_ajax_referer('sflmcp_profiles'); // TODO: Implement create/edit modal in next phase wp_send_json_error(array('message' => 'Not implemented yet')); } public function ajax_update_profile() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'No permission'), 403); } check_ajax_referer('sflmcp_profiles'); // TODO: Implement create/edit modal in next phase wp_send_json_error(array('message' => 'Not implemented yet')); } /** * Register top-level admin menu. * Only creates the parent — submenus are added by priority: * Priority 10: AI Chat Agent (client class, same slug as parent → first item) * Priority 20: MCP Server (this class → second item) */ public function registerAdmin() { add_menu_page( __('StifLi Flex MCP', 'stifli-flex-mcp'), __('StifLi Flex MCP', 'stifli-flex-mcp'), 'manage_options', 'stifli-flex-mcp', '__return_null', 'dashicons-rest-api', 30 ); } /** * Register MCP Server submenu at priority 20 (after AI Chat Agent at priority 10). */ public function registerMcpServerSubmenu() { add_submenu_page( 'stifli-flex-mcp', __('MCP Server', 'stifli-flex-mcp'), __('MCP Server', 'stifli-flex-mcp'), 'manage_options', 'sflmcp-server', array($this, 'adminPage') ); } /** * Register Multimedia submenu at priority 25 (after MCP Server at 15). */ public function registerMultimediaSubmenu() { add_submenu_page( 'stifli-flex-mcp', __('Multimedia', 'stifli-flex-mcp'), __('Multimedia', 'stifli-flex-mcp'), 'manage_options', 'sflmcp-multimedia', array($this, 'multimediaPage') ); } /** * Register settings used by the plugin */ public function registerSettings() { // No custom settings needed - uses WordPress Application Passwords } /** * Check if Application Passwords are available for a given user. * * @param WP_User $user WordPress user object. * @return bool */ private function isApplicationPasswordsAvailableForUser( $user ) { if ( ! class_exists( 'WP_Application_Passwords' ) ) { return false; } if ( function_exists( 'wp_is_application_passwords_available_for_user' ) ) { return (bool) wp_is_application_passwords_available_for_user( $user ); } if ( function_exists( 'wp_is_application_passwords_available' ) ) { return (bool) wp_is_application_passwords_available(); } return true; } /** * Check if an application password name already exists for a user. * * @param int $user_id User ID. * @param string $name Candidate application password name. * @return bool */ private function applicationPasswordNameExistsForUser( $user_id, $name ) { if ( class_exists( 'WP_Application_Passwords' ) && method_exists( 'WP_Application_Passwords', 'application_name_exists_for_user' ) ) { return (bool) WP_Application_Passwords::application_name_exists_for_user( $user_id, $name ); } if ( class_exists( 'WP_Application_Passwords' ) && method_exists( 'WP_Application_Passwords', 'get_user_application_passwords' ) ) { $passwords = WP_Application_Passwords::get_user_application_passwords( $user_id ); foreach ( $passwords as $password ) { $current_name = isset( $password['name'] ) ? (string) $password['name'] : ''; if ( 0 === strcasecmp( $current_name, $name ) ) { return true; } } } return false; } /** * Generate a unique default Application Password name for the current site. * * @param int $user_id User ID. * @return string */ private function getNextDefaultApplicationPasswordName( $user_id ) { $base_name = 'StifLi MCP App'; $candidate = $base_name; $next_index = 2; while ( $this->applicationPasswordNameExistsForUser( $user_id, $candidate ) ) { $candidate = $base_name . ' ' . $next_index; $next_index++; if ( $next_index > 999 ) { $candidate = $base_name . ' ' . wp_generate_password( 4, false, false ); break; } } return $candidate; } private function getOAuthWellKnownProbeTransientKey() { $host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST)); if ('' === $host) { $host = 'site'; } return 'sflmcp_oauth_wk_probe_' . md5($host); } private function getOAuthWellKnownDismissOptionKey() { $host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST)); if ('' === $host) { $host = 'site'; } return 'sflmcp_oauth_wk_notice_dismissed_' . md5($host); } private function invalidateOAuthWellKnownProbeCache() { delete_transient($this->getOAuthWellKnownProbeTransientKey()); delete_option($this->getOAuthWellKnownDismissOptionKey()); } private function shouldSkipOAuthWellKnownSelfCheck() { if (is_multisite() && !is_main_site()) { return true; } if (function_exists('wp_get_environment_type')) { $env = (string) wp_get_environment_type(); if (in_array($env, array('local', 'development'), true)) { return true; } } $host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST)); if ('' === $host) { return true; } if (in_array($host, array('localhost', '127.0.0.1', '::1'), true)) { return true; } $devSuffixes = array('.local', '.test', '.example', '.invalid', '.localhost'); foreach ($devSuffixes as $suffix) { if (strlen($host) > strlen($suffix) && substr($host, -strlen($suffix)) === $suffix) { return true; } } if (filter_var($host, FILTER_VALIDATE_IP)) { $publicIp = filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ); if (false === $publicIp) { return true; } } return false; } private function getOAuthWellKnownSelfCheckResult() { if ($this->shouldSkipOAuthWellKnownSelfCheck()) { return array( 'status' => 'skipped', 'blocked' => false, 'checked_at' => time(), ); } $transientKey = $this->getOAuthWellKnownProbeTransientKey(); $cached = get_transient($transientKey); if (is_array($cached) && isset($cached['status'])) { return $cached; } $url = home_url('/.well-known/oauth-authorization-server'); $result = array( 'status' => 'unknown', 'blocked' => false, 'checked_at' => time(), 'url' => $url, 'code' => 0, 'message' => '', ); $response = wp_remote_get($url, array( 'timeout' => 8, 'redirection' => 2, )); if (is_wp_error($response)) { $result['status'] = 'unknown'; $result['message'] = $response->get_error_message(); set_transient($transientKey, $result, (int) $this->oauthWellKnownProbeTtl); return $result; } $code = (int) wp_remote_retrieve_response_code($response); $body = (string) wp_remote_retrieve_body($response); $result['code'] = $code; if (200 === $code) { $decoded = json_decode($body, true); if (is_array($decoded) && !empty($decoded['authorization_endpoint']) && !empty($decoded['token_endpoint'])) { $result['status'] = 'ok'; } else { $result['status'] = 'unexpected'; $result['message'] = 'Unexpected payload at oauth-authorization-server endpoint.'; } } elseif (in_array($code, array(403, 404), true)) { $result['status'] = 'blocked'; $result['blocked'] = true; $result['message'] = 'Host likely blocks /.well-known/* before WordPress routing.'; } else { $result['status'] = 'error'; $result['message'] = 'Unexpected HTTP status from oauth-authorization-server endpoint.'; } set_transient($transientKey, $result, (int) $this->oauthWellKnownProbeTtl); return $result; } public function handleOAuthWellKnownNoticeDismiss() { if (!current_user_can('manage_options')) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified below. $dismiss = isset($_GET['sflmcp_dismiss_oauth_well_known_notice']) ? sanitize_text_field(wp_unslash($_GET['sflmcp_dismiss_oauth_well_known_notice'])) : ''; if ('1' !== $dismiss) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce is explicitly validated. $nonce = isset($_GET['_sflmcpnonce']) ? sanitize_text_field(wp_unslash($_GET['_sflmcpnonce'])) : ''; if (!wp_verify_nonce($nonce, 'sflmcp_dismiss_oauth_well_known_notice')) { return; } update_option($this->getOAuthWellKnownDismissOptionKey(), time(), false); $redirect = remove_query_arg(array('sflmcp_dismiss_oauth_well_known_notice', '_sflmcpnonce')); wp_safe_redirect($redirect); exit; } public function renderOAuthWellKnownNotice() { if (!current_user_can('manage_options')) { return; } if (!function_exists('get_current_screen')) { return; } $screen = get_current_screen(); if (!$screen || empty($screen->id)) { return; } $allowedScreens = array('stifli-flex-mcp_page_sflmcp-server', 'plugins'); if (!in_array((string) $screen->id, $allowedScreens, true)) { return; } $check = $this->getOAuthWellKnownSelfCheckResult(); if (empty($check['blocked'])) { return; } $dismissedAt = (int) get_option($this->getOAuthWellKnownDismissOptionKey(), 0); $checkedAt = isset($check['checked_at']) ? (int) $check['checked_at'] : 0; if ($dismissedAt > 0 && $dismissedAt >= $checkedAt) { return; } $dismissUrl = wp_nonce_url( add_query_arg(array('sflmcp_dismiss_oauth_well_known_notice' => '1')), 'sflmcp_dismiss_oauth_well_known_notice', '_sflmcpnonce' ); $helpUrl = admin_url('admin.php?page=sflmcp-server&tab=help#troubleshooting'); $endpoint = isset($check['url']) ? (string) $check['url'] : home_url('/.well-known/oauth-authorization-server'); $code = isset($check['code']) ? (int) $check['code'] : 0; ?>

0) : ?> (HTTP )

enqueueMultimediaAssets(); return; } // Only load on our MCP Server page if ($hook !== 'stifli-flex-mcp_page_sflmcp-server') { return; } // Get active tab early for conditional loading $active_tab = isset($_GET['tab']) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'settings'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended // Enqueue Settings tab JavaScript wp_enqueue_script( 'sflmcp-admin-settings', plugin_dir_url(__FILE__) . 'assets/admin-settings.js', array(), '1.0.4', true ); // Localize script with data wp_localize_script('sflmcp-admin-settings', 'sflmcpSettings', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('SFLMCP-admin'), 'i18n' => array( 'urlCopied' => __('URL copied', 'stifli-flex-mcp'), 'headerCopied' => __('Header copied', 'stifli-flex-mcp'), 'appPasswordGenerating' => __('Generating...', 'stifli-flex-mcp'), 'appPasswordGenerateError' => __('Could not generate Application Password. Please try again.', 'stifli-flex-mcp'), ), )); // Enqueue Profiles tab JavaScript wp_enqueue_script( 'sflmcp-admin-profiles', plugin_dir_url(__FILE__) . 'assets/admin-profiles.js', array(), '1.0.1', true ); // Localize script with data wp_localize_script('sflmcp-admin-profiles', 'sflmcpProfiles', array( 'nonce' => wp_create_nonce('sflmcp_profiles'), 'i18n' => array( 'includedTools' => __('Included tools:', 'stifli-flex-mcp'), ), )); // Enqueue main admin styles (tools, help page) wp_enqueue_style( 'sflmcp-admin-styles', plugin_dir_url(__FILE__) . 'assets/admin-styles.css', array(), '1.0.5' ); // Enqueue Custom Tools assets for legacy tab. if ($active_tab === 'custom') { $this->enqueueCustomToolsAssets(); } // Enqueue Abilities tab assets (WordPress 6.9+) if ($active_tab === 'abilities' && stifli_flex_mcp_abilities_available()) { wp_enqueue_style( 'sflmcp-admin-abilities', plugin_dir_url(__FILE__) . 'assets/admin-abilities.css', array(), '1.0.0' ); wp_enqueue_script( 'sflmcp-admin-abilities', plugin_dir_url(__FILE__) . 'assets/admin-abilities.js', array('jquery'), '1.0.0', true ); wp_localize_script('sflmcp-admin-abilities', 'sflmcpAbilities', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('sflmcp_abilities'), 'i18n' => array( 'discovering' => __('Discovering abilities...', 'stifli-flex-mcp'), 'discoverButton' => __('Discover Abilities', 'stifli-flex-mcp'), 'noAbilities' => __('No abilities found. Install plugins that register WordPress Abilities.', 'stifli-flex-mcp'), 'confirmDelete' => __('Are you sure you want to remove this ability?', 'stifli-flex-mcp'), 'confirmBulkRemove' => __('Remove the selected imported abilities?', 'stifli-flex-mcp'), 'confirmBulkImport' => __('Import the selected discovered abilities?', 'stifli-flex-mcp'), 'imported' => __('Ability imported successfully', 'stifli-flex-mcp'), 'deleted' => __('Ability removed', 'stifli-flex-mcp'), 'error' => __('An error occurred', 'stifli-flex-mcp'), 'chooseBulkAction' => __('Select a bulk action first.', 'stifli-flex-mcp'), 'noSelection' => __('Select at least one ability.', 'stifli-flex-mcp'), 'applying' => __('Applying...', 'stifli-flex-mcp'), 'alreadyImported' => __('Already imported', 'stifli-flex-mcp'), 'import' => __('Import', 'stifli-flex-mcp'), 'importing' => __('Importing...', 'stifli-flex-mcp'), 'enabledLabel' => __('Enabled', 'stifli-flex-mcp'), 'disabledLabel' => __('Disabled', 'stifli-flex-mcp'), 'selectedSuffix' => __('selected', 'stifli-flex-mcp'), 'importedSuffix' => __('imported', 'stifli-flex-mcp'), 'visibleSuffix' => __('visible', 'stifli-flex-mcp'), 'allCategories' => __('All categories', 'stifli-flex-mcp'), 'selectAllVisible' => __('Select all visible', 'stifli-flex-mcp'), 'clearSelection' => __('Clear selection', 'stifli-flex-mcp'), 'importSelected' => __('Import selected', 'stifli-flex-mcp'), 'importVisible' => __('Import visible', 'stifli-flex-mcp'), ), )); } // Enqueue OAuth assets on Settings tab if ($active_tab === 'settings') { wp_enqueue_style( 'sflmcp-admin-oauth', plugin_dir_url(__FILE__) . 'assets/admin-oauth.css', array(), '1.0.0' ); wp_enqueue_script( 'sflmcp-admin-oauth', plugin_dir_url(__FILE__) . 'assets/admin-oauth.js', array('jquery'), '1.0.0', true ); wp_localize_script('sflmcp-admin-oauth', 'sflmcpOAuth', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('sflmcp_oauth'), 'i18n' => array( 'confirmDeleteClient' => __('Are you sure you want to delete this OAuth client and revoke all its tokens?', 'stifli-flex-mcp'), 'confirmRevokeToken' => __('Revoke this token? The client will need to re-authorize.', 'stifli-flex-mcp'), 'confirmResetState' => __('Reset all OAuth clients, access tokens, refresh tokens, and pending authorization codes? All MCP clients will need to authorize again.', 'stifli-flex-mcp'), 'clientDeleted' => __('OAuth client deleted', 'stifli-flex-mcp'), 'tokenRevoked' => __('Token revoked', 'stifli-flex-mcp'), 'resetStateSuccess' => __('OAuth state reset. Reloading...', 'stifli-flex-mcp'), 'settingsSaved' => __('Settings saved', 'stifli-flex-mcp'), 'error' => __('An error occurred', 'stifli-flex-mcp'), ), )); } // Enqueue Tools tab JavaScript (WordPress and WooCommerce tools) if ($active_tab === 'tools' || $active_tab === 'wc_tools') { wp_enqueue_script( 'sflmcp-admin-tools', plugin_dir_url(__FILE__) . 'assets/admin-tools.js', array('jquery'), '1.0.5', true ); wp_localize_script('sflmcp-admin-tools', 'sflmcpTools', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('sflmcp_tools'), 'i18n' => array( 'enabled' => __('Enabled', 'stifli-flex-mcp'), 'disabled' => __('Disabled', 'stifli-flex-mcp'), 'error' => __('Error updating tool', 'stifli-flex-mcp'), ), )); } // Extension point for modular admin tabs. do_action('sflmcp_admin_enqueue_tab_assets', $active_tab, $hook, $this); } /** * Enqueue assets used by the Custom Tools UI. */ public function enqueueCustomToolsAssets() { wp_enqueue_style( 'sflmcp-admin-custom-tools', plugin_dir_url(__FILE__) . 'assets/admin-custom-tools.css', array(), '1.0.5' ); wp_enqueue_script( 'sflmcp-admin-custom-tools', plugin_dir_url(__FILE__) . 'assets/admin-custom-tools.js', array('jquery'), '1.0.5', true ); wp_localize_script('sflmcp-admin-custom-tools', 'sflmcpCustom', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('sflmcp_custom_tools'), 'i18n' => array( 'confirmDelete' => __('Are you sure you want to delete this tool?', 'stifli-flex-mcp'), 'errorSaving' => __('Error saving tool', 'stifli-flex-mcp'), 'saved' => __('Tool saved successfully', 'stifli-flex-mcp'), 'testing' => __('Testing...', 'stifli-flex-mcp'), 'success' => __('Success', 'stifli-flex-mcp'), 'failed' => __('Failed', 'stifli-flex-mcp'), ), )); } /** * Render the admin settings page */ public function adminPage() { if (!current_user_can('manage_options')) { wp_die( esc_html__('You do not have permission to view this page.','stifli-flex-mcp') ); } // Get active tab $active_tab = isset($_GET['tab']) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'settings'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- tab selection is a read-only navigation parameter $tabs = array( 'settings' => __('Settings', 'stifli-flex-mcp'), 'profiles' => __('Profiles', 'stifli-flex-mcp'), 'tools' => __('WordPress Tools', 'stifli-flex-mcp'), 'wc_tools' => __('WooCommerce Tools', 'stifli-flex-mcp'), ); if ( stifli_flex_mcp_abilities_available() ) { $tabs['abilities'] = __('Abilities', 'stifli-flex-mcp'); } $tabs['help'] = __('📚 Help', 'stifli-flex-mcp'); $tabs = apply_filters('sflmcp_admin_tabs', $tabs, $active_tab, $this); ?>

renderSettingsTab(); $handled = true; } elseif ($active_tab === 'profiles') { $this->renderProfilesTab(); $handled = true; } elseif ($active_tab === 'tools') { $this->renderToolsTab(); $handled = true; } elseif ($active_tab === 'wc_tools') { $this->renderWCToolsTab(); $handled = true; } elseif ($active_tab === 'abilities' && stifli_flex_mcp_abilities_available()) { $this->renderAbilitiesTab(); $handled = true; } elseif ($active_tab === 'custom' && (bool) apply_filters('sflmcp_enable_legacy_custom_tab', false, $this)) { $this->renderCustomToolsTab(); $handled = true; } elseif ($active_tab === 'help') { $this->renderHelpTab(); $handled = true; } if (!$handled) { $external_renderers = apply_filters('sflmcp_admin_tab_renderers', array(), $this); if (isset($external_renderers[$active_tab]) && is_callable($external_renderers[$active_tab])) { call_user_func($external_renderers[$active_tab], $active_tab, $this); $handled = true; } } if (!$handled) { $this->renderSettingsTab(); } ?>
namespace . '/sse' ); if ( ! class_exists( 'StifliFlexMcp_OAuth_Storage' ) ) { echo '

' . esc_html__( 'OAuth module is not loaded.', 'stifli-flex-mcp' ) . '

'; return; } $storage = StifliFlexMcp_OAuth_Storage::get_instance(); $clients = $storage->get_all_clients(); // Preload token data. global $wpdb; $tokens_table = $wpdb->prefix . 'sflmcp_oauth_tokens'; $now = gmdate( 'Y-m-d H:i:s' ); $token_counts = array(); $token_data = array(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $counts_raw = $wpdb->get_results( $wpdb->prepare( "SELECT client_id, COUNT(*) as cnt FROM {$tokens_table} WHERE revoked = 0 AND access_expires_at > %s GROUP BY client_id", $now ) ); foreach ( $counts_raw as $row ) { $token_counts[ $row->client_id ] = (int) $row->cnt; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $tokens_raw = $wpdb->get_results( $wpdb->prepare( "SELECT t.id, t.client_id, t.user_id, t.scope, t.access_expires_at, t.created_at, u.display_name as user_name FROM {$tokens_table} t LEFT JOIN {$wpdb->users} u ON t.user_id = u.ID WHERE t.revoked = 0 AND t.access_expires_at > %s ORDER BY t.created_at DESC", $now ) ); foreach ( $tokens_raw as $tok ) { $token_data[ $tok->client_id ][] = $tok; } ?>

1
2

Claude Desktop:

ChatGPT:

3

client_id ); $count = isset( $token_counts[ $client->client_id ] ) ? $token_counts[ $client->client_id ] : 0; $tokens_list = isset( $token_data[ $client->client_id ] ) ? $token_data[ $client->client_id ] : array(); ?>
client_name ); ?> client_id ); ?> 0 ) : ?> 0 created_at ); echo esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $registered + ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ) ) ); ?>
created_at ); $expires = strtotime( $tok->access_expires_at ); $offset = get_option( 'gmt_offset' ) * HOUR_IN_SECONDS; ?>
user_name ?: '#' . $tok->user_id ); ?> scope ); ?>

' . esc_html__( 'your profile', 'stifli-flex-mcp' ) . '' ), array( 'a' => array( 'href' => array(), 'target' => array() ) ) ); ?>

namespace . '/messages' ); $current_user = wp_get_current_user(); $example_user = ( $current_user && ! empty( $current_user->user_login ) ) ? $current_user->user_login : 'your_wp_username'; $curl_example = implode( "\n", array( sprintf( 'curl -u "%s:" \\', $example_user ), ' -H "Content-Type: application/json" \\', ' -d \'{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}\' \\', sprintf( ' "%s"', $messages_endpoint ), ) ); ?>

' . esc_html( $messages_endpoint ) . '' ), array( 'code' => array() ) ); ?>

getModel(); $integration_managed_tools = $this->getIntegrationManagedTools($model); global $wpdb; $table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_tools', false); $profiles_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profiles', false); $table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($table); $profiles_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profiles_table); // Check if there's an active profile $active_profile_query = sprintf('SELECT * FROM %s WHERE is_active = %%d', $profiles_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $active_profile = $wpdb->get_row($wpdb->prepare($active_profile_query, 1), ARRAY_A); // Handle re-seeding $reseed_nonce = isset($_POST['sflmcp_reseed_nonce']) ? sanitize_text_field( wp_unslash( $_POST['sflmcp_reseed_nonce'] ) ) : ''; if (!empty($reseed_nonce) && wp_verify_nonce($reseed_nonce, 'sflmcp_reseed_tools')) { $truncate_query = sprintf('TRUNCATE TABLE %s', $table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared -- admin action intentionally resets plugin-managed table. $wpdb->query($truncate_query); stifli_flex_mcp_seed_initial_tools(); echo '

' . esc_html__('Tools reset and reseeded successfully.', 'stifli-flex-mcp') . '

'; } // Get all tools grouped by category (ONLY WordPress, excluding WooCommerce). $tools_query = sprintf('SELECT * FROM %s WHERE category NOT LIKE %%s ORDER BY category, tool_name', $table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $tools = $wpdb->get_results($wpdb->prepare($tools_query, 'WooCommerce%'), ARRAY_A); if (!empty($integration_managed_tools)) { $tools = array_values(array_filter($tools, function($tool) use ($integration_managed_tools) { $name = isset($tool['tool_name']) ? (string) $tool['tool_name'] : ''; return '' === $name || !isset($integration_managed_tools[$name]); })); } $enabled_token_total = 0; foreach ($tools as $tool_row) { if (intval($tool_row['enabled']) === 1) { $enabled_token_total += intval($tool_row['token_estimate']); } } $grouped_tools = array(); foreach ($tools as $tool) { $category = $tool['category']; if (!isset($grouped_tools[$category])) { $grouped_tools[$category] = array(); } $grouped_tools[$category][] = $tool; } ?>

⚠️

$category_tools): ?> getToolModeLabel($model, $tool_meta['tool_name']); if ('WRITE' === $tool_mode) { $category_write++; } else { $category_read++; } if (intval($tool_meta['enabled']) === 1) { $category_enabled++; $category_enabled_tokens += intval($tool_meta['token_estimate']); if ('WRITE' === $tool_mode) { $category_enabled_write++; } else { $category_enabled_read++; } } } $count_class = ($category_enabled === count($category_tools) && count($category_tools) > 0) ? 'sflmcp-count-full' : ($category_enabled > 0 ? 'sflmcp-count-partial' : ''); $summary_html = esc_html($category_enabled . '/' . count($category_tools) . ' enabled'); if ($category_enabled_read > 0) { $summary_html .= ' · ' . esc_html($category_enabled_read . ' read') . ''; } if ($category_enabled_write > 0) { $summary_html .= ' · ' . esc_html($category_enabled_write . ' write') . ''; } if ($category_enabled > 0) { $summary_html .= ' · ' . esc_html(number_format_i18n($category_enabled_tokens)) . ' tokens'; } ?>
/>
getToolModeLabel($model, $tool['tool_name']); $mode_class = 'WRITE' === $tool_mode ? 'sflmcp-mode-write' : 'sflmcp-mode-read'; ?>
/> tokens
getModel(); global $wpdb; $table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_tools', false); $profiles_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profiles', false); $table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($table); $profiles_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profiles_table); // Check if there's an active profile $active_profile_query = sprintf('SELECT * FROM %s WHERE is_active = %%d', $profiles_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $active_profile = $wpdb->get_row($wpdb->prepare($active_profile_query, 1), ARRAY_A); // Get all WooCommerce tools grouped by category $wc_tools_query = sprintf("SELECT * FROM %s WHERE category LIKE %%s ORDER BY category, tool_name", $table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $tools = $wpdb->get_results($wpdb->prepare($wc_tools_query, 'WooCommerce%'), ARRAY_A); $wc_token_sum_query = sprintf("SELECT COALESCE(SUM(token_estimate),0) FROM %s WHERE category LIKE %%s AND enabled = %%d", $table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $enabled_token_total = (int) $wpdb->get_var($wpdb->prepare($wc_token_sum_query, 'WooCommerce%', 1)); $grouped_tools = array(); foreach ($tools as $tool) { $category = $tool['category']; if (!isset($grouped_tools[$category])) { $grouped_tools[$category] = array(); } $grouped_tools[$category][] = $tool; } ?>

⚠️

⚠️

$category_tools): ?> getToolModeLabel($model, $tool_meta['tool_name']); if ('WRITE' === $tool_mode) { $category_write++; } else { $category_read++; } if (intval($tool_meta['enabled']) === 1) { $category_enabled++; $category_enabled_tokens += intval($tool_meta['token_estimate']); if ('WRITE' === $tool_mode) { $category_enabled_write++; } else { $category_enabled_read++; } } } $count_class = ($category_enabled === count($category_tools) && count($category_tools) > 0) ? 'sflmcp-count-full' : ($category_enabled > 0 ? 'sflmcp-count-partial' : ''); $summary_html = esc_html($category_enabled . '/' . count($category_tools) . ' enabled'); if ($category_enabled_read > 0) { $summary_html .= ' · ' . esc_html($category_enabled_read . ' read') . ''; } if ($category_enabled_write > 0) { $summary_html .= ' · ' . esc_html($category_enabled_write . ' write') . ''; } if ($category_enabled > 0) { $summary_html .= ' · ' . esc_html(number_format_i18n($category_enabled_tokens)) . ' tokens'; } ?>
/>
getToolModeLabel($model, $tool['tool_name']); $mode_class = 'WRITE' === $tool_mode ? 'sflmcp-mode-write' : 'sflmcp-mode-read'; ?>
/> tokens
getTools(); if (!is_array($all_tools)) { return $managed; } foreach (array_keys($all_tools) as $tool_name) { if (!empty(StifliFlexMcp_Plugin_Integrations_Registry::get_integrations_for_tool($tool_name))) { $managed[$tool_name] = true; } } return $managed; } private function getToolModeLabel($model, $tool_name) { if (!is_object($model) || !method_exists($model, 'getIntentForTool')) { return 'READ'; } $meta = $model->getIntentForTool((string) $tool_name); if (is_array($meta) && isset($meta['intent']) && 'write' === $meta['intent']) { return 'WRITE'; } return 'READ'; } private function renderProfilesTab() { global $wpdb; $profiles_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profiles', false); $profile_tools_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_profile_tools', false); $tools_table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_tools', false); $profiles_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profiles_table); $profile_tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($profile_tools_table); $tools_table_sql = StifliFlexMcpUtils::wrapTableNameForQuery($tools_table); // Get all profiles with tool count and estimated tokens $profiles_query = sprintf( "SELECT p.*, COUNT(pt.id) AS tools_count, COALESCE(SUM(t.token_estimate),0) AS tokens_sum\n" ."FROM %s p\n" ."LEFT JOIN %s pt ON p.id = pt.profile_id\n" ."LEFT JOIN %s t ON pt.tool_name = t.tool_name\n" ."WHERE 1 = %%d\n" ."GROUP BY p.id\n" ."ORDER BY p.is_system DESC, p.profile_name ASC", $profiles_table_sql, $profile_tools_table_sql, $tools_table_sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $profiles = $wpdb->get_results($wpdb->prepare($profiles_query, 1), ARRAY_A); $total_tools_query = sprintf('SELECT COUNT(*) FROM %s WHERE 1 = %%d', $tools_table_sql); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- query uses sprintf with safe table wrapper. $total_tools = $wpdb->get_var($wpdb->prepare($total_tools_query, 1)); ?>

get_results($wpdb->prepare($system_tools_query, $profile['id']), ARRAY_A); $profile_tools_list = array(); if (!empty($profile_tools_rows)) { foreach ($profile_tools_rows as $tool_row) { $profile_tools_list[] = $tool_row['tool_name']; } } $tools_list_html = !empty($profile_tools_list) ? implode(', ', $profile_tools_list) : esc_html__('None', 'stifli-flex-mcp'); ?>

📋

get_results($wpdb->prepare($custom_tools_query, $profile['id']), ARRAY_A); $profile_tools_list = array(); if (!empty($profile_tools_rows)) { foreach ($profile_tools_rows as $tool_row) { $token_str = number_format_i18n(intval($tool_row['token_estimate'])); $profile_tools_list[] = sprintf('%s (≈%s)', $tool_row['tool_name'], $token_str); } } $tools_list_html = !empty($profile_tools_list) ? implode(', ', $profile_tools_list) : esc_html__('None', 'stifli-flex-mcp'); ?>

📋

📚

×

get_results("SELECT * FROM $table ORDER BY created_at DESC"); wp_send_json_success($tools); } public function ajax_save_custom_tool() { check_ajax_referer('sflmcp_custom_tools', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(); global $wpdb; $table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_custom_tools'); $id = isset($_POST['id']) ? intval($_POST['id']) : 0; $name = isset($_POST['tool_name']) ? sanitize_text_field( wp_unslash( $_POST['tool_name'] ) ) : ''; $desc = isset($_POST['tool_description']) ? sanitize_text_field( wp_unslash( $_POST['tool_description'] ) ) : ''; $method = isset($_POST['method']) ? sanitize_text_field( wp_unslash( $_POST['method'] ) ) : 'GET'; $endpoint = isset($_POST['endpoint']) ? esc_url_raw( wp_unslash( $_POST['endpoint'] ) ) : ''; $enabled = isset($_POST['enabled']) ? 1 : 0; // Handle headers JSON $headers = isset($_POST['headers']) ? sanitize_textarea_field( wp_unslash( $_POST['headers'] ) ) : ''; if (!empty($headers) && null === json_decode($headers)) { wp_send_json_error(array('message' => 'Invalid JSON in headers')); } // Handle Arguments Builder -> JSON Schema $args_json = isset($_POST['arguments']) ? sanitize_textarea_field( wp_unslash( $_POST['arguments'] ) ) : '{}'; // Validate that it's valid JSON if (null === json_decode($args_json)) { wp_send_json_error(array('message' => 'Invalid JSON in arguments')); } $data = array( 'tool_name' => $name, 'tool_description' => $desc, 'method' => $method, 'endpoint' => $endpoint, 'headers' => $headers, 'arguments' => $args_json, 'enabled' => $enabled ); if ($id > 0) { $wpdb->update($table, $data, array('id' => $id)); } else { // Ensure unique name if ($wpdb->get_var($wpdb->prepare("SELECT id FROM $table WHERE tool_name = %s", $name))) { wp_send_json_error(array('message' => 'Tool name already exists')); } $wpdb->insert($table, $data); } wp_send_json_success(); } public function ajax_delete_custom_tool() { check_ajax_referer('sflmcp_custom_tools', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(); global $wpdb; $table = StifliFlexMcpUtils::getPrefixedTable('sflmcp_custom_tools'); $id = isset($_POST['id']) ? intval($_POST['id']) : 0; if (!$id) { wp_send_json_error(array('message' => __('Invalid tool ID', 'stifli-flex-mcp'))); return; } $wpdb->delete($table, array('id' => $id)); wp_send_json_success(); } public function ajax_toggle_custom_tool() { check_ajax_referer('sflmcp_custom_tools', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied', 'stifli-flex-mcp'))); return; } global $wpdb; $table = $wpdb->prefix . 'sflmcp_custom_tools'; $id = isset($_POST['id']) ? intval($_POST['id']) : 0; if (!$id) { wp_send_json_error(array('message' => __('Invalid tool ID', 'stifli-flex-mcp'))); return; } // Get current status // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Plugin-managed table with parameterized query. $current = $wpdb->get_var($wpdb->prepare("SELECT enabled FROM $table WHERE id = %d", $id)); if ($current === null) { wp_send_json_error(array('message' => __('Tool not found', 'stifli-flex-mcp'))); return; } $new_status = ($current == 1) ? 0 : 1; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->update($table, array('enabled' => $new_status), array('id' => $id)); wp_send_json_success(array('enabled' => $new_status)); } public function ajax_test_custom_tool() { check_ajax_referer('sflmcp_custom_tools', 'nonce'); if (!current_user_can('manage_options')) wp_send_json_error(); $endpoint = isset($_POST['endpoint']) ? esc_url_raw( wp_unslash( $_POST['endpoint'] ) ) : ''; $method = isset($_POST['method']) ? sanitize_text_field( wp_unslash( $_POST['method'] ) ) : 'GET'; $headers_raw = isset($_POST['headers']) ? sanitize_textarea_field( wp_unslash( $_POST['headers'] ) ) : ''; $test_args = array( 'test' => true, 'timestamp' => time(), 'source' => 'StifLi Flex MCP Test' ); $args = array( 'method' => $method, 'timeout' => 15, 'user-agent' => 'StifLi-Flex-MCP/Tester' ); if (!empty($headers_raw)) { $h = json_decode($headers_raw, true); if (is_array($h)) { $args['headers'] = $h; } } if ($method === 'GET') { $endpoint = add_query_arg($test_args, $endpoint); } else { $args['body'] = wp_json_encode($test_args); if (!isset($args['headers']['Content-Type'])) { $args['headers']['Content-Type'] = 'application/json'; } } $response = wp_remote_request($endpoint, $args); if (is_wp_error($response)) { wp_send_json_error(array('message' => $response->get_error_message())); } else { $code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); wp_send_json_success(array('code' => $code, 'body' => substr($body, 0, 500))); // Truncate for preview } } /** * AJAX handler to toggle a single WordPress/WooCommerce tool */ public function ajax_toggle_tool() { check_ajax_referer('sflmcp_tools', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied', 'stifli-flex-mcp'))); return; } global $wpdb; $table = $wpdb->prefix . 'sflmcp_tools'; $tool_id = isset($_POST['tool_id']) ? intval($_POST['tool_id']) : 0; if (!$tool_id) { wp_send_json_error(array('message' => __('Invalid tool ID', 'stifli-flex-mcp'))); return; } // Get current status // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $tool = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $tool_id), ARRAY_A); if (!$tool) { wp_send_json_error(array('message' => __('Tool not found', 'stifli-flex-mcp'))); return; } $new_status = ($tool['enabled'] == 1) ? 0 : 1; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->update( $table, array('enabled' => $new_status, 'updated_at' => current_time('mysql', true)), array('id' => $tool_id), array('%d', '%s'), array('%d') ); // Sync to active profile if exists $this->syncToolToActiveProfile($tool['tool_name'], $new_status); // Calculate new token totals $is_wc = strpos($tool['category'], 'WooCommerce') === 0; $like_pattern = $is_wc ? 'WooCommerce%' : ''; if ($is_wc) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $total_tokens = $wpdb->get_var($wpdb->prepare( "SELECT COALESCE(SUM(token_estimate),0) FROM $table WHERE category LIKE %s AND enabled = %d", 'WooCommerce%', 1 )); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $total_tokens = $wpdb->get_var($wpdb->prepare( "SELECT COALESCE(SUM(token_estimate),0) FROM $table WHERE category NOT LIKE %s AND enabled = %d", 'WooCommerce%', 1 )); } wp_send_json_success(array( 'enabled' => $new_status, 'total_tokens' => number_format_i18n(intval($total_tokens)) )); } /** * AJAX handler to bulk toggle tools in a category */ public function ajax_bulk_toggle_tools() { check_ajax_referer('sflmcp_tools', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied', 'stifli-flex-mcp'))); return; } global $wpdb; $table = $wpdb->prefix . 'sflmcp_tools'; $bulk_action = isset($_POST['bulk_action']) ? sanitize_text_field( wp_unslash( $_POST['bulk_action'] ) ) : ''; $category = isset($_POST['category']) ? sanitize_text_field( wp_unslash( $_POST['category'] ) ) : ''; if (!in_array($bulk_action, array('enable', 'disable'))) { wp_send_json_error(array('message' => __('Invalid action', 'stifli-flex-mcp'))); return; } $new_status = ($bulk_action === 'enable') ? 1 : 0; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query($wpdb->prepare( "UPDATE $table SET enabled = %d, updated_at = %s WHERE category = %s", $new_status, current_time('mysql', true), $category )); // Get affected tools and sync to active profile // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $affected_tools = $wpdb->get_col($wpdb->prepare( "SELECT tool_name FROM $table WHERE category = %s", $category )); foreach ($affected_tools as $tool_name) { $this->syncToolToActiveProfile($tool_name, $new_status); } wp_send_json_success(); } /** * Sync a tool's enabled status to the active profile */ private function syncToolToActiveProfile($tool_name, $enabled) { global $wpdb; $profiles_table = $wpdb->prefix . 'sflmcp_profiles'; $profile_tools_table = $wpdb->prefix . 'sflmcp_profile_tools'; // Get active profile // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $active_profile = $wpdb->get_row($wpdb->prepare( "SELECT id FROM $profiles_table WHERE is_active = %d", 1 ), ARRAY_A); if (!$active_profile) { return; } $profile_id = $active_profile['id']; if ($enabled) { // Check if already exists // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $exists = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $profile_tools_table WHERE profile_id = %d AND tool_name = %s", $profile_id, $tool_name )); if (!$exists) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->insert( $profile_tools_table, array('profile_id' => $profile_id, 'tool_name' => $tool_name), array('%d', '%s') ); } } else { // Remove from profile // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->delete( $profile_tools_table, array('profile_id' => $profile_id, 'tool_name' => $tool_name), array('%d', '%s') ); } } /** * AJAX handler: toggle single tool via checkbox */ public function ajax_toggle_tool_by_checkbox() { check_ajax_referer('sflmcp_tools', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied', 'stifli-flex-mcp'))); return; } global $wpdb; $table = $wpdb->prefix . 'sflmcp_tools'; $tool_id = isset($_POST['tool_id']) ? intval($_POST['tool_id']) : 0; $enabled = isset($_POST['enabled']) ? intval($_POST['enabled']) : 0; if (!$tool_id) { wp_send_json_error(array('message' => __('Invalid tool ID', 'stifli-flex-mcp'))); return; } // Get tool info // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $tool = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $tool_id), ARRAY_A); if (!$tool) { wp_send_json_error(array('message' => __('Tool not found', 'stifli-flex-mcp'))); return; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->update( $table, array('enabled' => $enabled, 'updated_at' => current_time('mysql', true)), array('id' => $tool_id), array('%d', '%s'), array('%d') ); // Sync to active profile $this->syncToolToActiveProfile($tool['tool_name'], $enabled); // Calculate new token totals $is_wc = strpos($tool['category'], 'WooCommerce') === 0; if ($is_wc) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $total_tokens = $wpdb->get_var($wpdb->prepare( "SELECT COALESCE(SUM(token_estimate),0) FROM $table WHERE category LIKE %s AND enabled = %d", 'WooCommerce%', 1 )); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $total_tokens = $wpdb->get_var($wpdb->prepare( "SELECT COALESCE(SUM(token_estimate),0) FROM $table WHERE category NOT LIKE %s AND enabled = %d", 'WooCommerce%', 1 )); } wp_send_json_success(array( 'enabled' => $enabled, 'total_tokens' => number_format_i18n(intval($total_tokens)) )); } /** * AJAX handler: bulk toggle multiple tools by ID (from category checkbox) */ public function ajax_bulk_toggle_tools_by_id() { check_ajax_referer('sflmcp_tools', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied', 'stifli-flex-mcp'))); return; } global $wpdb; $table = $wpdb->prefix . 'sflmcp_tools'; $tool_ids = isset($_POST['tool_ids']) && is_array($_POST['tool_ids']) ? array_map('intval', wp_unslash($_POST['tool_ids'])) : array(); if (empty($tool_ids)) { wp_send_json_error(array('message' => __('No tools specified', 'stifli-flex-mcp'))); return; } // Get current enabled status of first tool to toggle // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $first_tool = $wpdb->get_row($wpdb->prepare( "SELECT enabled FROM $table WHERE id = %d", $tool_ids[0] ), ARRAY_A); if (!$first_tool) { wp_send_json_error(array('message' => __('Tool not found', 'stifli-flex-mcp'))); return; } // Toggle status $new_status = ($first_tool['enabled'] == 1) ? 0 : 1; // Update all tools foreach ($tool_ids as $tool_id) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->update( $table, array('enabled' => $new_status, 'updated_at' => current_time('mysql', true)), array('id' => $tool_id), array('%d', '%s'), array('%d') ); // Get tool name for syncing // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $tool = $wpdb->get_row($wpdb->prepare("SELECT tool_name FROM $table WHERE id = %d", $tool_id), ARRAY_A); if ($tool) { $this->syncToolToActiveProfile($tool['tool_name'], $new_status); } } // Get WC vs WordPress status from first tool // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $first_tool_full = $wpdb->get_row($wpdb->prepare("SELECT category FROM $table WHERE id = %d", $tool_ids[0]), ARRAY_A); $is_wc = strpos($first_tool_full['category'], 'WooCommerce') === 0; // Calculate new token totals if ($is_wc) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $total_tokens = $wpdb->get_var($wpdb->prepare( "SELECT COALESCE(SUM(token_estimate),0) FROM $table WHERE category LIKE %s AND enabled = %d", 'WooCommerce%', 1 )); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $total_tokens = $wpdb->get_var($wpdb->prepare( "SELECT COALESCE(SUM(token_estimate),0) FROM $table WHERE category NOT LIKE %s AND enabled = %d", 'WooCommerce%', 1 )); } wp_send_json_success(array( 'enabled' => $new_status, 'total_tokens' => number_format_i18n(intval($total_tokens)) )); } /** * Render Abilities Tab - WordPress 6.9+ Abilities API Integration */ private function renderAbilitiesTab() { ?>

renderImportedAbilitiesList(); ?>
prefix . 'sflmcp_abilities'; // Check if table exists $like = $wpdb->esc_like($table); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- schema check. if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $like)) !== $table) { echo '

' . esc_html__('Abilities table not initialized. Please deactivate and reactivate the plugin.', 'stifli-flex-mcp') . '

'; return; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- fresh data needed. $abilities = $wpdb->get_results("SELECT * FROM {$table} ORDER BY ability_category, ability_label"); if (empty($abilities)) { echo '

' . esc_html__('No abilities imported yet. Use the Discover button to find and import abilities.', 'stifli-flex-mcp') . '

'; return; } echo '
'; echo '
'; echo ''; echo ''; echo ''; echo '
'; echo '
'; echo '0 ' . esc_html__('selected', 'stifli-flex-mcp') . ''; echo '
'; echo '
'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ($abilities as $ability) { $enabled_class = $ability->enabled ? 'dashicons-yes-alt' : 'dashicons-marker'; $enabled_color = $ability->enabled ? '#46b450' : '#dc3232'; $tool_name = 'ability_' . str_replace(array('/', '-'), '_', $ability->ability_name); /* translators: %s: ability label */ $select_ability_aria = sprintf( __( 'Select %s', 'stifli-flex-mcp' ), $ability->ability_label ); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__('Actions', 'stifli-flex-mcp') . '
'; echo ''; echo ''; echo '' . esc_html($ability->ability_label) . ''; echo '
' . esc_html($tool_name) . ''; if (!empty($ability->ability_description)) { echo '
' . esc_html(wp_trim_words($ability->ability_description, 15)) . ''; } echo '
' . esc_html($ability->ability_category) . ''; echo ''; echo ''; echo ''; echo '
'; } /** * Normalize an ability category label. * * @param mixed $ability_or_category Ability instance or raw category value. * @return string */ private function getAbilityCategoryLabel( $ability_or_category ) { $category = $ability_or_category; if ( is_object( $ability_or_category ) && method_exists( $ability_or_category, 'get_category' ) ) { $category = $ability_or_category->get_category(); } if ( is_object( $category ) && method_exists( $category, 'get_label' ) ) { $category = $category->get_label(); } if ( is_scalar( $category ) ) { $category = sanitize_text_field( (string) $category ); } else { $category = ''; } return '' !== $category ? $category : __( 'Uncategorized', 'stifli-flex-mcp' ); } /** * Import a single registered WordPress ability into the plugin table. * * @param string $ability_name Ability slug. * @return int|WP_Error */ private function importAbilityRecord( $ability_name ) { if ( ! stifli_flex_mcp_abilities_available() ) { return new WP_Error( 'abilities_unavailable', __( 'WordPress Abilities API not available', 'stifli-flex-mcp' ) ); } $ability = wp_get_ability( $ability_name ); if ( ! $ability ) { return new WP_Error( 'ability_not_found', __( 'Ability not found', 'stifli-flex-mcp' ) ); } global $wpdb; $table = $wpdb->prefix . 'sflmcp_abilities'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE ability_name = %s", $ability_name ) ); if ( $exists ) { return new WP_Error( 'ability_exists', __( 'Ability already imported', 'stifli-flex-mcp' ) ); } $input_schema = StifliFlexMcpUtils::normalizeToolInputSchema( $ability->get_input_schema() ); $output_schema = $ability->get_output_schema(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $result = $wpdb->insert( $table, array( 'ability_name' => $ability_name, 'ability_label' => $ability->get_label(), 'ability_description' => $ability->get_description(), 'ability_category' => $this->getAbilityCategoryLabel( $ability ), 'input_schema' => is_array( $input_schema ) ? wp_json_encode( $input_schema ) : null, 'output_schema' => is_array( $output_schema ) ? wp_json_encode( $output_schema ) : null, 'enabled' => 1, 'created_at' => current_time( 'mysql', true ), 'updated_at' => current_time( 'mysql', true ), ), array( '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s' ) ); if ( false === $result ) { return new WP_Error( 'ability_insert_failed', __( 'Failed to import ability', 'stifli-flex-mcp' ) ); } return (int) $wpdb->insert_id; } /** * AJAX handler: Discover available WordPress Abilities */ public function ajax_discover_abilities() { check_ajax_referer('sflmcp_abilities', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied', 'stifli-flex-mcp'))); } if (!stifli_flex_mcp_abilities_available()) { wp_send_json_error(array('message' => __('WordPress Abilities API not available. Requires WordPress 6.9+', 'stifli-flex-mcp'))); } // Get all registered abilities using wp_get_abilities() $all_abilities = wp_get_abilities(); if (empty($all_abilities)) { wp_send_json_success(array( 'abilities' => array(), 'message' => __('No abilities found. Install plugins that register WordPress Abilities.', 'stifli-flex-mcp'), )); return; } // Get already imported abilities global $wpdb; $table = $wpdb->prefix . 'sflmcp_abilities'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $imported = $wpdb->get_col("SELECT ability_name FROM {$table}"); $imported_map = array_flip($imported); $abilities_list = array(); foreach ($all_abilities as $ability) { $name = $ability->get_name(); // Skip our own abilities if we ever register any if (strpos($name, 'sflmcp/') === 0) { continue; } // Get category - may be a string or null $abilities_list[] = array( 'name' => $name, 'label' => $ability->get_label(), 'description' => $ability->get_description(), 'category' => $this->getAbilityCategoryLabel( $ability ), 'input_schema' => $ability->get_input_schema(), 'output_schema' => method_exists($ability, 'get_output_schema') ? $ability->get_output_schema() : null, 'imported' => isset($imported_map[$name]), ); } wp_send_json_success(array( 'abilities' => $abilities_list, 'count' => count($abilities_list), )); } /** * AJAX handler: Import an ability */ public function ajax_import_ability() { check_ajax_referer('sflmcp_abilities', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied', 'stifli-flex-mcp'))); } $ability_name = isset($_POST['ability_name']) ? sanitize_text_field(wp_unslash($_POST['ability_name'])) : ''; if (empty($ability_name)) { wp_send_json_error(array('message' => __('Ability name is required', 'stifli-flex-mcp'))); } if (!stifli_flex_mcp_abilities_available()) { wp_send_json_error(array('message' => __('WordPress Abilities API not available', 'stifli-flex-mcp'))); } $result = $this->importAbilityRecord( $ability_name ); if ( is_wp_error( $result ) ) { wp_send_json_error(array('message' => $result->get_error_message())); } wp_send_json_success(array( 'message' => __('Ability imported successfully', 'stifli-flex-mcp'), 'id' => $result, )); } /** * AJAX handler: Bulk manage imported or discovered abilities. */ public function ajax_bulk_manage_abilities() { check_ajax_referer( 'sflmcp_abilities', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied', 'stifli-flex-mcp' ) ) ); } $bulk_action = isset( $_POST['bulk_action'] ) ? sanitize_key( wp_unslash( $_POST['bulk_action'] ) ) : ''; if ( empty( $bulk_action ) ) { wp_send_json_error( array( 'message' => __( 'Bulk action is required', 'stifli-flex-mcp' ) ) ); } $ability_ids = array(); if ( isset( $_POST['ability_ids'] ) ) { $ability_ids = array_values( array_unique( array_filter( array_map( 'intval', (array) wp_unslash( $_POST['ability_ids'] ) ) ) ) ); } $ability_names = array(); if ( isset( $_POST['ability_names'] ) ) { $ability_names = array_values( array_unique( array_filter( array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['ability_names'] ) ) ) ) ); } global $wpdb; $table = $wpdb->prefix . 'sflmcp_abilities'; switch ( $bulk_action ) { case 'enable': case 'disable': if ( empty( $ability_ids ) ) { wp_send_json_error( array( 'message' => __( 'Select at least one imported ability.', 'stifli-flex-mcp' ) ) ); } $new_state = 'enable' === $bulk_action ? 1 : 0; $updated = 0; $timestamp = current_time( 'mysql', true ); foreach ( $ability_ids as $ability_id ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $row_result = $wpdb->update( $table, array( 'enabled' => $new_state, 'updated_at' => $timestamp, ), array( 'id' => $ability_id ), array( '%d', '%s' ), array( '%d' ) ); if ( false === $row_result ) { wp_send_json_error( array( 'message' => __( 'Failed to update abilities', 'stifli-flex-mcp' ) ) ); } $updated += (int) $row_result; } wp_send_json_success( array( 'updated' => (int) $updated, 'message' => 'enable' === $bulk_action ? __( 'Selected abilities enabled', 'stifli-flex-mcp' ) : __( 'Selected abilities disabled', 'stifli-flex-mcp' ), ) ); break; case 'delete': if ( empty( $ability_ids ) ) { wp_send_json_error( array( 'message' => __( 'Select at least one imported ability.', 'stifli-flex-mcp' ) ) ); } $deleted = 0; foreach ( $ability_ids as $ability_id ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $row_result = $wpdb->delete( $table, array( 'id' => $ability_id ), array( '%d' ) ); if ( false === $row_result ) { wp_send_json_error( array( 'message' => __( 'Failed to delete abilities', 'stifli-flex-mcp' ) ) ); } $deleted += (int) $row_result; } wp_send_json_success( array( 'deleted' => (int) $deleted, 'message' => __( 'Selected abilities removed', 'stifli-flex-mcp' ), ) ); break; case 'import': if ( empty( $ability_names ) ) { wp_send_json_error( array( 'message' => __( 'Select at least one discovered ability.', 'stifli-flex-mcp' ) ) ); } $imported_count = 0; $skipped_count = 0; $processed_names = array(); $errors = array(); foreach ( $ability_names as $ability_name ) { $result = $this->importAbilityRecord( $ability_name ); if ( is_wp_error( $result ) ) { if ( 'ability_exists' === $result->get_error_code() ) { ++$skipped_count; $processed_names[] = $ability_name; continue; } $errors[] = $result->get_error_message(); continue; } ++$imported_count; $processed_names[] = $ability_name; } if ( 0 === $imported_count && ! empty( $errors ) ) { wp_send_json_error( array( 'message' => $errors[0] ) ); } /* translators: %d: number of imported abilities. */ $imported_message = sprintf( __( '%d abilities imported', 'stifli-flex-mcp' ), $imported_count ); $message = $imported_count > 0 ? $imported_message : __( 'No new abilities were imported', 'stifli-flex-mcp' ); wp_send_json_success( array( 'imported' => $imported_count, 'skipped' => $skipped_count, 'processed_names' => $processed_names, 'errors' => $errors, 'message' => $message, ) ); break; default: wp_send_json_error( array( 'message' => __( 'Invalid bulk action', 'stifli-flex-mcp' ) ) ); } } /** * AJAX handler: Toggle ability enabled state */ public function ajax_toggle_ability() { check_ajax_referer('sflmcp_abilities', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied', 'stifli-flex-mcp'))); } $ability_id = isset($_POST['ability_id']) ? intval($_POST['ability_id']) : 0; if ($ability_id <= 0) { wp_send_json_error(array('message' => __('Invalid ability ID', 'stifli-flex-mcp'))); } global $wpdb; $table = $wpdb->prefix . 'sflmcp_abilities'; // Get current state // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $current = $wpdb->get_var($wpdb->prepare("SELECT enabled FROM {$table} WHERE id = %d", $ability_id)); if ($current === null) { wp_send_json_error(array('message' => __('Ability not found', 'stifli-flex-mcp'))); } $new_state = $current ? 0 : 1; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->update( $table, array('enabled' => $new_state, 'updated_at' => current_time('mysql', true)), array('id' => $ability_id), array('%d', '%s'), array('%d') ); wp_send_json_success(array( 'enabled' => $new_state, 'message' => $new_state ? __('Ability enabled', 'stifli-flex-mcp') : __('Ability disabled', 'stifli-flex-mcp'), )); } /** * AJAX handler: Delete an imported ability */ public function ajax_delete_ability() { check_ajax_referer('sflmcp_abilities', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied', 'stifli-flex-mcp'))); } $ability_id = isset($_POST['ability_id']) ? intval($_POST['ability_id']) : 0; if ($ability_id <= 0) { wp_send_json_error(array('message' => __('Invalid ability ID', 'stifli-flex-mcp'))); } global $wpdb; $table = $wpdb->prefix . 'sflmcp_abilities'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $result = $wpdb->delete($table, array('id' => $ability_id), array('%d')); if ($result === false) { wp_send_json_error(array('message' => __('Failed to delete ability', 'stifli-flex-mcp'))); } wp_send_json_success(array('message' => __('Ability removed', 'stifli-flex-mcp'))); } /** * AJAX handler: Get imported abilities list (for refresh) */ public function ajax_get_imported_abilities() { check_ajax_referer('sflmcp_abilities', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied', 'stifli-flex-mcp'))); } ob_start(); $this->renderImportedAbilitiesList(); $html = ob_get_clean(); wp_send_json_success(array('html' => $html)); } /** * Enqueue assets for the Multimedia submenu page. */ private function enqueueMultimediaAssets() { wp_enqueue_style( 'sflmcp-admin-multimedia', plugin_dir_url(__FILE__) . 'assets/admin-multimedia.css', array(), '1.3.0' ); wp_enqueue_script( 'sflmcp-admin-multimedia', plugin_dir_url(__FILE__) . 'assets/admin-multimedia.js', array('jquery'), '1.6.0', true ); wp_localize_script('sflmcp-admin-multimedia', 'sflmcpMultimedia', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('sflmcp_multimedia'), 'i18n' => array( 'saving' => __('Saving...', 'stifli-flex-mcp'), 'saved' => __('Saved', 'stifli-flex-mcp'), 'error' => __('Error saving settings', 'stifli-flex-mcp'), 'loaded' => __('Settings loaded', 'stifli-flex-mcp'), 'enabled' => __('Enabled', 'stifli-flex-mcp'), 'disabled' => __('Disabled', 'stifli-flex-mcp'), ), )); } /** * Render the Multimedia admin page (standalone submenu). */ public function multimediaPage() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'stifli-flex-mcp' ) ); } $active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'images'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>

renderVideoSettingsTab(); } else { $this->renderMultimediaTab(); } ?>
'openai', // OpenAI image settings 'openai_api_key' => '', 'openai_model' => 'gpt-image-1', 'openai_quality' => 'medium', 'openai_size' => 'square', 'openai_style' => 'natural', 'openai_background' => 'auto', 'openai_output_format' => 'png', // Gemini image settings 'gemini_api_key' => '', 'gemini_model' => 'gemini-2.5-flash-image', 'gemini_aspect_ratio' => '1:1', // Post-processing 'pp_enabled' => '1', 'pp_max_width' => 1024, 'pp_max_height' => 1024, 'pp_quality' => 80, 'pp_format' => 'original', // Video settings 'video_provider' => 'gemini', 'video_gemini_model' => 'veo-3.0-generate-preview', 'video_openai_model' => 'sora-2', 'video_duration' => '5', 'video_aspect_ratio' => '16:9', 'video_resolution' => '720p', 'video_poll_interval' => 10, 'video_max_wait' => 300, ); $saved = get_option( 'sflmcp_multimedia_settings', array() ); return wp_parse_args( $saved, $defaults ); } /** * Create a partial display mask for an encrypted API key. * * Decrypts the stored key, then returns a masked version showing the first * few characters and last 4, with bullets in between matching the real length. * Example: "sk-proj-••••••••••••••••abcd" * * @param string $encrypted_value The encrypted (or empty) key from settings. * @return string Partial mask for display, or empty string if no key stored. */ private function maskApiKeyForDisplay( $encrypted_value ) { if ( empty( $encrypted_value ) ) { return ''; } // Decrypt to get real key $plain = ''; if ( class_exists( 'StifliFlexMcp_Client_Admin' ) ) { $plain = StifliFlexMcp_Client_Admin::decrypt_value( $encrypted_value ); } else { $plain = $encrypted_value; } if ( empty( $plain ) ) { return ''; } $len = strlen( $plain ); // Very short keys: just show bullets if ( $len <= 8 ) { return str_repeat( '•', $len ); } // Show first 4 chars + bullets + last 4 chars $prefix = substr( $plain, 0, 4 ); $suffix = substr( $plain, -4 ); $mid_count = max( 4, $len - 8 ); return $prefix . str_repeat( '•', $mid_count ) . $suffix; } /** * Render Multimedia Settings Tab */ private function renderMultimediaTab() { $s = $this->getMultimediaSettings(); $has_gd = extension_loaded( 'gd' ); $gd_info = $has_gd ? gd_info() : array(); // Build partial display strings for API keys (e.g. "sk-••••••••xxxx") $openai_display = $this->maskApiKeyForDisplay( $s['openai_api_key'] ); $gemini_display = $this->maskApiKeyForDisplay( $s['gemini_api_key'] ); ?>
wp_generate_image

🖼️

OpenAI
Gemini

⚙️

⚠️

%

getMultimediaSettings(); // Build partial display strings for API keys (same shared keys as image tab) $openai_display = $this->maskApiKeyForDisplay( $s['openai_api_key'] ); $gemini_display = $this->maskApiKeyForDisplay( $s['gemini_api_key'] ); ?>
wp_generate_video

🎬

Google Veo
OpenAI Sora

⚙️

__( 'Permission denied', 'stifli-flex-mcp' ) ) ); } // Allowed values for enum fields $allowed_enums = array( 'image_provider' => array( 'openai', 'gemini' ), 'openai_model' => array( 'gpt-image-2', 'gpt-image-1.5', 'gpt-image-1', 'gpt-image-1-mini', 'dall-e-3', 'dall-e-2' ), 'openai_quality' => array( 'low', 'medium', 'high' ), 'openai_size' => array( 'square', 'landscape', 'portrait' ), 'openai_style' => array( 'natural', 'vivid' ), 'openai_background' => array( 'auto', 'transparent', 'opaque' ), 'openai_output_format' => array( 'png', 'jpeg', 'webp' ), 'gemini_model' => array( 'gemini-3.1-flash-image-preview', 'gemini-3-pro-image-preview', 'gemini-2.5-flash-image', 'imagen-4.0-generate-001', 'imagen-4.0-fast-generate-001', 'imagen-4.0-ultra-generate-001' ), 'gemini_aspect_ratio' => array( '1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3' ), 'pp_format' => array( 'original', 'jpeg', 'webp', 'png' ), 'video_provider' => array( 'gemini', 'openai' ), 'video_gemini_model' => array( 'veo-3.0-generate-preview', 'veo-2.0-generate-001' ), 'video_openai_model' => array( 'sora-2', 'sora-2-pro' ), 'video_duration' => array( '4', '5', '6', '8', '12' ), 'video_aspect_ratio' => array( '16:9', '9:16', '1:1' ), 'video_resolution' => array( '480p', '720p', '1080p' ), ); // Numeric fields: key => array( min, max ) $numeric_fields = array( 'pp_max_width' => array( 0, 4096 ), 'pp_max_height' => array( 0, 4096 ), 'pp_quality' => array( 30, 100 ), 'video_poll_interval' => array( 5, 60 ), 'video_max_wait' => array( 60, 600 ), ); // Start from existing settings (partial merge — only update fields present in POST) $settings = get_option( 'sflmcp_multimedia_settings', array() ); // phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce verified above via check_ajax_referer // Update enum fields only if present in POST foreach ( $allowed_enums as $key => $values ) { if ( isset( $_POST[ $key ] ) ) { $val = sanitize_text_field( wp_unslash( $_POST[ $key ] ) ); if ( in_array( $val, $values, true ) ) { $settings[ $key ] = $val; } } } // Update checkbox field if ( isset( $_POST['pp_enabled'] ) ) { $settings['pp_enabled'] = sanitize_text_field( wp_unslash( $_POST['pp_enabled'] ) ) === '1' ? '1' : '0'; } // Update numeric fields only if present in POST foreach ( $numeric_fields as $key => $range ) { if ( isset( $_POST[ $key ] ) ) { $settings[ $key ] = max( $range[0], min( $range[1], intval( $_POST[ $key ] ) ) ); } } // Handle API keys — only update if user entered a real value (not the masked placeholder) $api_key_fields = array( 'openai_api_key', 'gemini_api_key' ); foreach ( $api_key_fields as $key ) { if ( isset( $_POST[ $key ] ) ) { $raw = sanitize_text_field( wp_unslash( $_POST[ $key ] ) ); // Skip if empty or contains bullet chars (masked value, not a real new key) if ( ! empty( $raw ) && strpos( $raw, '•' ) === false ) { if ( class_exists( 'StifliFlexMcp_Client_Admin' ) ) { $ref = new ReflectionMethod( 'StifliFlexMcp_Client_Admin', 'encrypt_value' ); $ref->setAccessible( true ); $settings[ $key ] = $ref->invoke( null, $raw ); } else { $settings[ $key ] = $raw; } } // If empty or masked, keep existing value — do not touch $settings[$key] } } // phpcs:enable WordPress.Security.NonceVerification.Missing update_option( 'sflmcp_multimedia_settings', $settings ); wp_send_json_success( array( 'message' => __( 'Settings saved', 'stifli-flex-mcp' ) ) ); } /** * AJAX handler: Load multimedia settings. */ public function ajax_load_multimedia_settings() { check_ajax_referer( 'sflmcp_multimedia', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied', 'stifli-flex-mcp' ) ) ); } $s = $this->getMultimediaSettings(); // Mask API keys — partial reveal (first 4 + bullets + last 4 chars) $api_keys = array( 'openai_api_key', 'gemini_api_key' ); foreach ( $api_keys as $key ) { $s[ $key ] = $this->maskApiKeyForDisplay( $s[ $key ] ); } // Include tool enabled/disabled status from wp_sflmcp_tools. global $wpdb; $tools_table = $wpdb->prefix . 'sflmcp_tools'; $tool_names = array( 'wp_generate_image', 'wp_generate_video' ); foreach ( $tool_names as $tname ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $enabled = $wpdb->get_var( $wpdb->prepare( "SELECT enabled FROM {$tools_table} WHERE tool_name = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $tname ) ); $s[ 'tool_enabled_' . $tname ] = ( '1' === $enabled || 1 === (int) $enabled ) ? '1' : '0'; } wp_send_json_success( $s ); } /** * AJAX handler: Reveal a full decrypted API key (admin only). */ public function ajax_mm_reveal_key() { check_ajax_referer( 'sflmcp_multimedia', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied', 'stifli-flex-mcp' ) ) ); } $key_name = sanitize_text_field( wp_unslash( $_POST['key_name'] ?? '' ) ); $allowed = array( 'openai_api_key', 'gemini_api_key' ); if ( ! in_array( $key_name, $allowed, true ) ) { wp_send_json_error( array( 'message' => __( 'Invalid key name', 'stifli-flex-mcp' ) ) ); } $settings = $this->getMultimediaSettings(); $encrypted = isset( $settings[ $key_name ] ) ? $settings[ $key_name ] : ''; if ( empty( $encrypted ) ) { wp_send_json_success( array( 'key' => '' ) ); } $plain = ''; if ( class_exists( 'StifliFlexMcp_Client_Admin' ) ) { $plain = StifliFlexMcp_Client_Admin::decrypt_value( $encrypted ); } else { $plain = $encrypted; } wp_send_json_success( array( 'key' => $plain ) ); } /** * AJAX handler: Toggle a multimedia tool on/off by tool_name. */ public function ajax_mm_toggle_tool() { check_ajax_referer( 'sflmcp_multimedia', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied', 'stifli-flex-mcp' ) ) ); } $tool_name = sanitize_text_field( wp_unslash( $_POST['tool_name'] ?? '' ) ); $enabled = isset( $_POST['enabled'] ) ? intval( $_POST['enabled'] ) : -1; $allowed = array( 'wp_generate_image', 'wp_generate_video' ); if ( ! in_array( $tool_name, $allowed, true ) || ! in_array( $enabled, array( 0, 1 ), true ) ) { wp_send_json_error( array( 'message' => __( 'Invalid parameters', 'stifli-flex-mcp' ) ) ); } global $wpdb; $tools_table = $wpdb->prefix . 'sflmcp_tools'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->update( $tools_table, array( 'enabled' => $enabled, 'updated_at' => current_time( 'mysql', true ) ), array( 'tool_name' => $tool_name ), array( '%d', '%s' ), array( '%s' ) ); // Sync to active profile. $this->syncToolToActiveProfile( $tool_name, $enabled ); wp_send_json_success( array( 'tool_name' => $tool_name, 'enabled' => $enabled ) ); } // ================================================================ // OAuth Clients Tab // ================================================================ /** * Render the OAuth Clients admin tab. */ private function renderOAuthClientsTab() { if ( ! class_exists( 'StifliFlexMcp_OAuth_Storage' ) ) { echo '

' . esc_html__( 'OAuth module is not loaded.', 'stifli-flex-mcp' ) . '

'; return; } $storage = StifliFlexMcp_OAuth_Storage::get_instance(); $clients = $storage->get_all_clients(); $auto_approve = get_option( 'sflmcp_oauth_auto_approve', '1' ); // Preload token counts per client. global $wpdb; $tokens_table = $wpdb->prefix . 'sflmcp_oauth_tokens'; $now = gmdate( 'Y-m-d H:i:s' ); $token_counts = array(); $token_data = array(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $counts_raw = $wpdb->get_results( $wpdb->prepare( "SELECT client_id, COUNT(*) as cnt FROM {$tokens_table} WHERE revoked = 0 AND access_expires_at > %s GROUP BY client_id", $now ) ); foreach ( $counts_raw as $row ) { $token_counts[ $row->client_id ] = (int) $row->cnt; } // Get active tokens grouped by client for expandable detail. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $tokens_raw = $wpdb->get_results( $wpdb->prepare( "SELECT t.id, t.client_id, t.user_id, t.scope, t.access_expires_at, t.created_at, u.display_name as user_name FROM {$tokens_table} t LEFT JOIN {$wpdb->users} u ON t.user_id = u.ID WHERE t.revoked = 0 AND t.access_expires_at > %s ORDER BY t.created_at DESC", $now ) ); foreach ( $tokens_raw as $tok ) { $token_data[ $tok->client_id ][] = $tok; } ?>

client_id ); $count = isset( $token_counts[ $client->client_id ] ) ? $token_counts[ $client->client_id ] : 0; $is_public = ( 'none' === $client->token_endpoint_auth_method ); $tokens_list = isset( $token_data[ $client->client_id ] ) ? $token_data[ $client->client_id ] : array(); ?>
client_name ); ?> client_uri ) : ?>
client_uri ); ?>
client_id ); ?> 0 ) : ?> 0 created_at ); echo esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $registered + ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ) ) ); ?>

created_at ); $expires = strtotime( $tok->access_expires_at ); $offset = get_option( 'gmt_offset' ) * HOUR_IN_SECONDS; ?>
user_name ?: '#' . $tok->user_id ); ?> scope ); ?>

🟣

  1. ' . ( PHP_OS_FAMILY === 'Darwin' ? '~/Library/Application Support/Claude/claude_desktop_config.json' : '%APPDATA%\\Claude\\claude_desktop_config.json' ) . '' ), array( 'code' => array() ) ); ?>
  2. {
      "mcpServers": {
        "": {
          "type": "sse",
          "url": "namespace . '/sse' ) ); ?>"
        }
      }
    }

' . esc_html( rest_url( $this->namespace . '/sse' ) ) . '' ), array( 'code' => array() ) ); ?>

🟢

    • token_endpoint_auth_method: "client_secret_post"
    • redirect_uris: ["https://chatgpt.com/aip/YOUR_PLUGIN_ID/oauth/callback"]
  1. namespace . '/oauth/token' ) ); ?>
    mcp

🔑

' . esc_html__( 'your profile', 'stifli-flex-mcp' ) . '' ), array( 'a' => array( 'href' => array() ) ) ); ?>

namespace . '/messages' ); $current_user = wp_get_current_user(); $example_user = ( $current_user && ! empty( $current_user->user_login ) ) ? $current_user->user_login : 'your_wp_username'; $curl_example = implode( "\n", array( sprintf( 'curl -u "%s:" \\', $example_user ), ' -H "Content-Type: application/json" \\', ' -d \'{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}\' \\', sprintf( ' "%s"', $messages_endpoint ), ) ); ?>

' . esc_html( $messages_endpoint ) . '' ), array( 'code' => array() ) ); ?>

__( 'Permission denied', 'stifli-flex-mcp' ) ) ); return; } $client_id = isset( $_POST['client_id'] ) ? sanitize_text_field( wp_unslash( $_POST['client_id'] ) ) : ''; if ( empty( $client_id ) ) { wp_send_json_error( array( 'message' => __( 'Missing client_id', 'stifli-flex-mcp' ) ) ); return; } $storage = StifliFlexMcp_OAuth_Storage::get_instance(); $deleted = $storage->delete_client( $client_id ); if ( $deleted ) { wp_send_json_success(); } else { wp_send_json_error( array( 'message' => __( 'Client not found', 'stifli-flex-mcp' ) ) ); } } /** * AJAX: Revoke a specific OAuth token by row ID. */ public function ajax_oauth_revoke_token() { check_ajax_referer( 'sflmcp_oauth', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied', 'stifli-flex-mcp' ) ) ); return; } $token_id = isset( $_POST['token_id'] ) ? intval( $_POST['token_id'] ) : 0; if ( ! $token_id ) { wp_send_json_error( array( 'message' => __( 'Missing token_id', 'stifli-flex-mcp' ) ) ); return; } global $wpdb; $table = $wpdb->prefix . 'sflmcp_oauth_tokens'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $updated = $wpdb->update( $table, array( 'revoked' => 1 ), array( 'id' => $token_id ), array( '%d' ), array( '%d' ) ); if ( false !== $updated ) { wp_send_json_success(); } else { wp_send_json_error( array( 'message' => __( 'Token not found', 'stifli-flex-mcp' ) ) ); } } /** * AJAX: Save OAuth settings (auto-approve toggle). */ public function ajax_oauth_save_settings() { check_ajax_referer( 'sflmcp_oauth', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied', 'stifli-flex-mcp' ) ) ); return; } $auto_approve = isset( $_POST['auto_approve'] ) ? sanitize_text_field( wp_unslash( $_POST['auto_approve'] ) ) : '0'; update_option( 'sflmcp_oauth_auto_approve', $auto_approve === '1' ? '1' : '0' ); $this->invalidateOAuthWellKnownProbeCache(); wp_send_json_success(); } /** * AJAX: Reset all OAuth clients, tokens, and auth codes. */ public function ajax_oauth_reset_state() { check_ajax_referer( 'sflmcp_oauth', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied', 'stifli-flex-mcp' ) ) ); return; } $storage = StifliFlexMcp_OAuth_Storage::get_instance(); $result = $storage->reset_all_state(); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) ); return; } wp_send_json_success( $result ); } /** * AJAX: Generate an Application Password for current user. */ public function ajax_generate_app_password() { check_ajax_referer( 'SFLMCP-admin', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied', 'stifli-flex-mcp' ) ), 403 ); return; } if ( ! class_exists( 'WP_Application_Passwords' ) ) { wp_send_json_error( array( 'message' => __( 'Application Passwords are not available on this WordPress installation.', 'stifli-flex-mcp' ) ) ); return; } $current_user = wp_get_current_user(); if ( ! is_a( $current_user, 'WP_User' ) || empty( $current_user->ID ) ) { wp_send_json_error( array( 'message' => __( 'Could not resolve the current user.', 'stifli-flex-mcp' ) ) ); return; } if ( ! $this->isApplicationPasswordsAvailableForUser( $current_user ) ) { wp_send_json_error( array( 'message' => __( 'Application Passwords are not available for this user/site. Ensure HTTPS is enabled and no plugin disables this feature.', 'stifli-flex-mcp' ) ) ); return; } $app_name = $this->getNextDefaultApplicationPasswordName( (int) $current_user->ID ); $created = WP_Application_Passwords::create_new_application_password( (int) $current_user->ID, array( 'name' => $app_name, 'app_id' => wp_generate_uuid4(), ) ); if ( is_wp_error( $created ) ) { wp_send_json_error( array( 'message' => wp_strip_all_tags( $created->get_error_message() ) ) ); return; } $raw_password = isset( $created[0] ) ? (string) $created[0] : ''; $record = ( isset( $created[1] ) && is_array( $created[1] ) ) ? $created[1] : array(); if ( '' === $raw_password ) { wp_send_json_error( array( 'message' => __( 'Application Password was created but the plain-text value was not returned.', 'stifli-flex-mcp' ) ) ); return; } wp_send_json_success( array( 'message' => __( 'Application Password created. Copy it now: it is only shown this one time.', 'stifli-flex-mcp' ), 'user_login' => isset( $current_user->user_login ) ? (string) $current_user->user_login : '', 'app_name' => isset( $record['name'] ) ? (string) $record['name'] : $app_name, 'password' => $raw_password, ) ); } /** * Render Help Tab - Complete documentation */ private function renderHelpTab() { $site_url = site_url(); $endpoint = rest_url($this->namespace . '/messages'); ?>

📚

🎯

🔧

Posts & Pageswp_get_posts, wp_create_post, wp_update_post, wp_delete_post
Mediawp_upload_image, wp_upload_image_from_url, wp_get_media
Taxonomieswp_get_categories, wp_create_tag, wp_get_terms
Userswp_get_users, wp_get_user_meta, wp_update_user_meta
WooCommercewc_get_products, wc_create_order, wc_update_stock
Systemwp_get_site_health, wp_get_settings, mcp_ping

🔌

HTTP (GET/POST/PUT/DELETE) HTTP
ACTION ACTION

💡 :

flush_rewrite_rules
wp_cron
wp_cache_flush

woocommerce_cancel_unpaid_orders
woocommerce_cleanup_sessions
woocommerce_scheduled_sales

Yoast SEOwpseo_reindex
WP Super Cachewp_cache_clear_cache
W3 Total Cachew3tc_flush_all
WP Rocketrocket_clean_domain
Elementorelementor/core/files/clear_cache

🔍

  • "[plugin name] action hooks"
  • "[plugin name] do_action"
  • "[plugin name] developer documentation"

# In your plugin folder, search for hooks:
grep -r "do_action(" wp-content/plugins/your-plugin/

# Common patterns:
do_action('plugin_prefix_event_name');
do_action('plugin_prefix_before_save', $data);
do_action('plugin_prefix_after_delete', $id);

📋

  1. : custom_clear_rocket_cache
  2. : "Clear WP Rocket cache. Use when site shows outdated content."
  3. : ACTION (WordPress do_action)
  4. : rocket_clean_domain
  5. : (none needed)

🌐

Zapierhttps://hooks.zapier.com/hooks/catch/...
Make (Integromat)https://hook.eu1.make.com/...
n8nhttps://your-n8n.com/webhook/...
IFTTThttps://maker.ifttt.com/trigger/...

📋

    • Name: custom_create_jira_ticket
    • Type: POST
    • Endpoint: (your Zapier URL)
    • Parameters: title (string, required), description (string), priority (string)

wttr.inhttps://wttr.in/{city}?format=j1
ipapi.cohttps://ipapi.co/{ip}/json/
OpenAIhttps://api.openai.com/v1/...

🏠

/wp-json/wp/v2/posts
/wp-json/wp/v2/pages/{id}
/wp-json/wc/v3/products
/wp-json/contact-form-7/v1/contact-forms
⚠️ /wp-json/

💡

🛒

  • woocommerce_cancel_unpaid_orders
  • → built-in wc_update_stock
  • → built-in wc_create_coupon

🔧

  • w3tc_flush_all / rocket_clean_domain
  • sflmcp_maintenance_mode
  • flush_rewrite_rules

📊

  • → Notion API webhook
  • → Zapier/Make webhook
  • → Twilio API

🔐

Authorization: Bearer your-api-token
Content-Type: application/json
X-Custom-Header: value

🔧

📝