init(); } } public function init() { add_action('wp_abilities_api_categories_init', [$this, 'registerCategory']); add_action('wp_abilities_api_init', [$this, 'registerAbilities']); add_action('mcp_adapter_init', [$this, 'registerCustomServer']); PaymentDataProvider::register(); $invalidate = [ContextTools::class, 'invalidateCache']; foreach ([ 'fluentform/inserted_new_form', 'fluentform/form_duplicated', // after_ only: bumping the cache version twice per deletion costs an // extra option write and buys nothing, since the bump is not // order-sensitive. 'fluentform/after_form_deleted', // Permissions changed — drop the cached context so it can't briefly // advertise stale capabilities/visible-forms after a change. The // role-level hook covers the Role Manager; the per-user hook covers // an individual manager's assignment, which fires no role event. 'fluentform/after_permission_set_assignment', 'fluentform/after_user_permissions_attached', ] as $hook) { add_action($hook, $invalidate); } add_action('admin_notices', [$this, 'maybeShowAdapterNotice']); } public function registerCategory() { wp_register_ability_category('fluentform', [ 'label' => __('FluentForm', 'fluentform'), 'description' => __('Form abilities for FluentForm — forms, entries, analytics, and integrations.', 'fluentform'), ]); } public function registerAbilities() { AbilitiesRegistrar::register(); /** * Fires after FluentForm registers its core MCP abilities. Pro and * extensions hook this to register their own abilities (payments, * advanced reports) under the same `fluentform/` namespace. * * @since 6.2.5 */ do_action('fluentform/mcp_loaded'); } /** * Register the dedicated FluentForm MCP server. Endpoint defaults to * /wp-json/fluentform/mcp. * * @param object $adapter The \WP\MCP\Core\McpAdapter instance. */ public function registerCustomServer($adapter) { if (!$adapter || !is_object($adapter) || !method_exists($adapter, 'create_server')) { return; } $abilityNames = array_keys(AbilitiesRegistrar::getDefinitions()); /** * Filter the ability names exposed by the FluentForm MCP server. Pro and * extensions push their ability names here. * * @since 6.2.5 * * @param array $abilityNames Fully-qualified ability names. */ $abilityNames = apply_filters('fluentform/mcp_ability_names', $abilityNames); $abilityNames = array_values(array_unique(array_filter((array) $abilityNames))); $namespace = apply_filters('fluentform/mcp_server_namespace', 'fluentform'); $route = apply_filters('fluentform/mcp_server_route', 'mcp'); $adapter->create_server( self::SERVER_ID, $namespace, $route, __('FluentForm MCP Server', 'fluentform'), __('AI agent tools for FluentForm forms, entries, analytics, and integrations.', 'fluentform'), defined('FLUENTFORM_VERSION') ? FLUENTFORM_VERSION : '1.0.0', ['\WP\MCP\Transport\HttpTransport'], '\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler', '\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler', $abilityNames, [], [], [PermissionGate::class, 'transport'] ); } /** * Announce FluentForm to FluentHub's MCP page. FluentHub discovers products * through these filters; without them the server is fully functional yet * never appears in the Toolkit's list. Runs unconditionally (even when MCP is * off) so the operator can flip it on from the Toolkit. Both filters are * cheap no-ops unless the Toolkit applies them. */ public static function registerWithToolkit() { add_filter('fluent_kit/mcp_products', function ($products) { if (!is_array($products)) { $products = []; } $products[] = [ 'slug' => self::SERVER_ID, 'name' => __('FluentForm', 'fluentform'), 'mcp_enabled' => PermissionGate::isEnabled(), 'tools_count' => self::toolsCount(), 'endpoint_url' => self::getEndpointUrl(), 'status' => self::toolkitStatus(), ]; return $products; }); add_filter('fluent_kit/mcp_toggle_handlers', function ($handlers) { if (!is_array($handlers)) { $handlers = []; } $handlers[self::SERVER_ID] = [ 'get_enabled' => [PermissionGate::class, 'isEnabled'], 'set_enabled' => function ($enabled) { return PermissionGate::setEnabled($enabled); }, ]; return $handlers; }); } public static function toolsCount() { $names = array_keys(AbilitiesRegistrar::getDefinitions()); $names = apply_filters('fluentform/mcp_ability_names', $names); return is_array($names) ? count(array_unique($names)) : 0; } public static function toolkitStatus() { if (!self::adapterAvailable()) { return 'adapter_required'; } return PermissionGate::isEnabled() ? 'ready' : 'disabled'; } public static function getEndpointUrl() { $namespace = apply_filters('fluentform/mcp_server_namespace', 'fluentform'); $route = apply_filters('fluentform/mcp_server_route', 'mcp'); return get_rest_url(null, trailingslashit($namespace) . $route); } /** True when an MCP adapter + the Abilities API are both available. */ public static function adapterAvailable() { return defined('WP_MCP_VERSION') && class_exists('\WP\MCP\Core\McpAdapter') && function_exists('wp_register_ability'); } /** * Warn that MCP is on but no adapter is installed, so the endpoint is silently * dead. Scoped to FluentForm admin screens — the settings card carries the * richer prompt; this is only a cross-page reminder inside the plugin, never a * global dashboard nag. */ public function maybeShowAdapterNotice() { if (self::adapterAvailable() || !current_user_can('manage_options')) { return; } $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; if (0 !== strpos($page, 'fluent_forms')) { return; } // The MCP settings card renders its own richer adapter alert (with an // install/activate action), so skip this notice on the settings screen to // avoid double-reporting the same state. if ('fluent_forms_settings' === $page) { return; } echo '
'; echo esc_html__('FluentForm MCP is enabled but no MCP adapter was found. Install FluentHub (recommended) or the MCP Adapter plugin, on WordPress 6.9+.', 'fluentform'); echo '