PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Modules / MCP / MCPInit.php

MCPInit.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.14, at app/Modules/MCP/MCPInit.php

254 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Modules\MCP;
4
5 defined('ABSPATH') || exit;
6
7 use FluentForm\App\Modules\MCP\Support\PaymentDataProvider;
8 use FluentForm\App\Modules\MCP\Support\PermissionGate;
9 use FluentForm\App\Modules\MCP\Tools\ContextTools;
10
11 /**
12 * Bootstrap for FluentForm's Model Context Protocol (MCP) integration.
13 *
14 * Wires the WordPress Abilities API (core 6.9+) + the WP MCP Adapter, which is
15 * provided by FluentHub / Fluent Toolkit (bundled) or the standalone
16 * mcp-adapter plugin — whichever is present. FluentForm bundles nothing; it
17 * consumes whatever's loaded and surfaces an admin notice instead of failing
18 * silently when nothing is.
19 *
20 * The whole surface is gated behind the enable option (default off): a site
21 * owner turns it on in FluentForm → Settings → MCP and connects with an
22 * application password. Even when on, the endpoint stays behind WP auth + a
23 * FluentForm role (transport gate) + per-ability permission checks.
24 *
25 * Booted from boot/app.php.
26 */
27 class MCPInit
28 {
29 const SERVER_ID = 'fluentform';
30
31 /**
32 * Bootstrap entry point, called on every request from boot/app.php.
33 *
34 * Toolkit discovery runs unconditionally so FluentHub can list FluentForm
35 * even while disabled. When MCP is off the front-end cost is two add_filter
36 * calls plus one read of an autoloaded option — not literally zero, but
37 * nothing that touches the database beyond that. The legacy-key prune adds a
38 * second option read, and is confined to admin requests for that reason.
39 */
40 public static function boot()
41 {
42 self::registerWithToolkit();
43
44 if (is_admin()) {
45 PermissionGate::pruneLegacyKeys();
46 }
47
48 if (PermissionGate::isEnabled()) {
49 (new self())->init();
50 }
51 }
52
53 public function init()
54 {
55 add_action('wp_abilities_api_categories_init', [$this, 'registerCategory']);
56 add_action('wp_abilities_api_init', [$this, 'registerAbilities']);
57
58 add_action('mcp_adapter_init', [$this, 'registerCustomServer']);
59
60 PaymentDataProvider::register();
61
62 $invalidate = [ContextTools::class, 'invalidateCache'];
63 foreach ([
64 'fluentform/inserted_new_form',
65 'fluentform/form_duplicated',
66 // after_ only: bumping the cache version twice per deletion costs an
67 // extra option write and buys nothing, since the bump is not
68 // order-sensitive.
69 'fluentform/after_form_deleted',
70 // Permissions changed — drop the cached context so it can't briefly
71 // advertise stale capabilities/visible-forms after a change. The
72 // role-level hook covers the Role Manager; the per-user hook covers
73 // an individual manager's assignment, which fires no role event.
74 'fluentform/after_permission_set_assignment',
75 'fluentform/after_user_permissions_attached',
76 ] as $hook) {
77 add_action($hook, $invalidate);
78 }
79
80 add_action('admin_notices', [$this, 'maybeShowAdapterNotice']);
81 }
82
83 public function registerCategory()
84 {
85 wp_register_ability_category('fluentform', [
86 'label' => __('FluentForm', 'fluentform'),
87 'description' => __('Form abilities for FluentForm — forms, entries, analytics, and integrations.', 'fluentform'),
88 ]);
89 }
90
91 public function registerAbilities()
92 {
93 AbilitiesRegistrar::register();
94
95 /**
96 * Fires after FluentForm registers its core MCP abilities. Pro and
97 * extensions hook this to register their own abilities (payments,
98 * advanced reports) under the same `fluentform/` namespace.
99 *
100 * @since 6.2.5
101 */
102 do_action('fluentform/mcp_loaded');
103 }
104
105 /**
106 * Register the dedicated FluentForm MCP server. Endpoint defaults to
107 * /wp-json/fluentform/mcp.
108 *
109 * @param object $adapter The \WP\MCP\Core\McpAdapter instance.
110 */
111 public function registerCustomServer($adapter)
112 {
113 if (!$adapter || !is_object($adapter) || !method_exists($adapter, 'create_server')) {
114 return;
115 }
116
117 $abilityNames = array_keys(AbilitiesRegistrar::getDefinitions());
118
119 /**
120 * Filter the ability names exposed by the FluentForm MCP server. Pro and
121 * extensions push their ability names here.
122 *
123 * @since 6.2.5
124 *
125 * @param array $abilityNames Fully-qualified ability names.
126 */
127 $abilityNames = apply_filters('fluentform/mcp_ability_names', $abilityNames);
128 $abilityNames = array_values(array_unique(array_filter((array) $abilityNames)));
129
130 $namespace = apply_filters('fluentform/mcp_server_namespace', 'fluentform');
131 $route = apply_filters('fluentform/mcp_server_route', 'mcp');
132
133 $adapter->create_server(
134 self::SERVER_ID,
135 $namespace,
136 $route,
137 __('FluentForm MCP Server', 'fluentform'),
138 __('AI agent tools for FluentForm forms, entries, analytics, and integrations.', 'fluentform'),
139 defined('FLUENTFORM_VERSION') ? FLUENTFORM_VERSION : '1.0.0',
140 ['\WP\MCP\Transport\HttpTransport'],
141 '\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler',
142 '\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler',
143 $abilityNames,
144 [],
145 [],
146 [PermissionGate::class, 'transport']
147 );
148 }
149
150 /**
151 * Announce FluentForm to FluentHub's MCP page. FluentHub discovers products
152 * through these filters; without them the server is fully functional yet
153 * never appears in the Toolkit's list. Runs unconditionally (even when MCP is
154 * off) so the operator can flip it on from the Toolkit. Both filters are
155 * cheap no-ops unless the Toolkit applies them.
156 */
157 public static function registerWithToolkit()
158 {
159 add_filter('fluent_kit/mcp_products', function ($products) {
160 if (!is_array($products)) {
161 $products = [];
162 }
163
164 $products[] = [
165 'slug' => self::SERVER_ID,
166 'name' => __('FluentForm', 'fluentform'),
167 'mcp_enabled' => PermissionGate::isEnabled(),
168 'tools_count' => self::toolsCount(),
169 'endpoint_url' => self::getEndpointUrl(),
170 'status' => self::toolkitStatus(),
171 ];
172
173 return $products;
174 });
175
176 add_filter('fluent_kit/mcp_toggle_handlers', function ($handlers) {
177 if (!is_array($handlers)) {
178 $handlers = [];
179 }
180
181 $handlers[self::SERVER_ID] = [
182 'get_enabled' => [PermissionGate::class, 'isEnabled'],
183 'set_enabled' => function ($enabled) {
184 return PermissionGate::setEnabled($enabled);
185 },
186 ];
187
188 return $handlers;
189 });
190 }
191
192 public static function toolsCount()
193 {
194 $names = array_keys(AbilitiesRegistrar::getDefinitions());
195 $names = apply_filters('fluentform/mcp_ability_names', $names);
196
197 return is_array($names) ? count(array_unique($names)) : 0;
198 }
199
200 public static function toolkitStatus()
201 {
202 if (!self::adapterAvailable()) {
203 return 'adapter_required';
204 }
205
206 return PermissionGate::isEnabled() ? 'ready' : 'disabled';
207 }
208
209 public static function getEndpointUrl()
210 {
211 $namespace = apply_filters('fluentform/mcp_server_namespace', 'fluentform');
212 $route = apply_filters('fluentform/mcp_server_route', 'mcp');
213
214 return get_rest_url(null, trailingslashit($namespace) . $route);
215 }
216
217 /** True when an MCP adapter + the Abilities API are both available. */
218 public static function adapterAvailable()
219 {
220 return defined('WP_MCP_VERSION')
221 && class_exists('\WP\MCP\Core\McpAdapter')
222 && function_exists('wp_register_ability');
223 }
224
225 /**
226 * Warn that MCP is on but no adapter is installed, so the endpoint is silently
227 * dead. Scoped to FluentForm admin screens — the settings card carries the
228 * richer prompt; this is only a cross-page reminder inside the plugin, never a
229 * global dashboard nag.
230 */
231 public function maybeShowAdapterNotice()
232 {
233 if (self::adapterAvailable() || !current_user_can('manage_options')) {
234 return;
235 }
236
237 $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';
238 if (0 !== strpos($page, 'fluent_forms')) {
239 return;
240 }
241
242 // The MCP settings card renders its own richer adapter alert (with an
243 // install/activate action), so skip this notice on the settings screen to
244 // avoid double-reporting the same state.
245 if ('fluent_forms_settings' === $page) {
246 return;
247 }
248
249 echo '<div class="notice notice-warning"><p>';
250 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');
251 echo '</p></div>';
252 }
253 }
254