PluginProbe ʕ •ᴥ•ʔ
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback / 5.1
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback v5.1
5.1.3 5.1.2 5.1.1 5.1 5.0 trunk 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.2.0 3.2.1 3.22 3.22.1 3.22.2 3.22.3 3.22.4 3.22.5 3.22.6 3.3.0 3.3.1 3.3.2 3.3.2.1 3.3.2.2 3.3.3 3.30 3.31 3.32 3.4 3.4.1 3.4.3 3.4.4 3.5 3.5.1 3.6 3.6.1 3.7 3.8 3.9 3.9.1 3.9.2 3.9.3 3.9.4 3.9.6 3.9.6.1 4.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.2 4.2.1 4.2.2 4.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.4
atarim-visual-collaboration / doit / class-avcf-mcp.php
atarim-visual-collaboration / doit Last commit date
abilities 3 weeks ago avcf-cluster-loader.php 3 weeks ago class-avcf-diagnostic.php 3 weeks ago class-avcf-mcp-auth.php 3 weeks ago class-avcf-mcp.php 3 weeks ago
class-avcf-mcp.php
468 lines
1 <?php
2 /**
3 * Atarim MCP server orchestrator.
4 *
5 * Bootstraps the Atarim MCP server, wires up authentication for incoming
6 * MCP requests, and dispatches ability registration to the category-specific
7 * classes under doit/abilities/ and third-party/{plugin}/.
8 *
9 * Each ability category is implemented in its own class that extends
10 * AVCF_Abilities_Base. To add a new category:
11 * 1. Create doit/abilities/class-avcf-abilities-{name}.php
12 * 2. require_once it in the main plugin file
13 * 3. Instantiate + register() it in avcf_mcp_register_abilities() below
14 * 4. Add the ability names to the $tools array in avcf_mcp_setup_server()
15 *
16 * Third-party plugin integrations follow the same pattern but live under
17 * third-party/{plugin}/ alongside their detector and data-layer classes.
18 *
19 * @package atarim-visual-collaboration
20 */
21
22 if ( ! defined('ABSPATH') ) {
23 exit;
24 }
25
26 use WP\MCP\Core\McpAdapter;
27 use WP\MCP\Transport\HttpTransport;
28 use WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler;
29 use WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler;
30
31 class AVCF_MCP {
32
33 private $function;
34 private $auth;
35
36 public function __construct() {
37 $this->function = new AVCF_Functions();
38 $this->auth = new AVCF_MCP_Auth();
39
40 $this->init_hooks();
41 }
42
43 private function init_hooks() {
44 // Authenticate MCP requests by mapping Atarim token to a WordPress user.
45 add_filter( 'determine_current_user', [ $this, 'avcf_mcp_authenticate_request' ], 20 );
46
47 // Gate the whole MCP endpoint behind the "Enable Do It" setting.
48 add_filter( 'rest_pre_dispatch', [ $this, 'avcf_mcp_gate_when_disabled' ], 5, 3 );
49
50 // Setup Atarim MCP server during adapter init.
51 add_action( 'mcp_adapter_init', [ $this, 'avcf_mcp_setup_server' ] );
52
53 // Fetch MCP token when plugin is updated.
54 add_action( 'upgrader_process_complete', [ $this, 'avcf_mcp_on_plugin_update' ], 10, 2 );
55 }
56
57 /**
58 * Dispatch ability registration to each category class.
59 *
60 * Called on wp_abilities_api_init (hook registered in the main plugin file).
61 * Each category class is responsible for registering its own abilities with
62 * the WordPress Abilities API. Third-party integrations are guarded by a
63 * detector / function-exists check so we never instantiate an integration
64 * whose host plugin isn't active.
65 */
66 public function avcf_mcp_register_abilities() {
67 // Core WordPress abilities — always available.
68 ( new AVCF_Abilities_Content() )->register();
69 ( new AVCF_Abilities_Gutenberg() )->register();
70 ( new AVCF_Abilities_Plugins() )->register();
71 ( new AVCF_Abilities_Themes() )->register();
72 ( new AVCF_Abilities_Theme_Files() )->register();
73 ( new AVCF_Abilities_Core() )->register();
74 ( new AVCF_Abilities_Taxonomies() )->register();
75 ( new AVCF_Abilities_Users() )->register();
76 ( new AVCF_Abilities_Settings() )->register();
77 ( new AVCF_Abilities_Media() )->register();
78 ( new AVCF_Abilities_Metadata() )->register();
79 ( new AVCF_Abilities_Navigation() )->register();
80 ( new AVCF_Abilities_Templates() )->register();
81 ( new AVCF_Abilities_Global_Styles() )->register();
82 ( new AVCF_Abilities_Patterns() )->register();
83 ( new AVCF_Abilities_Block_Navigation() )->register();
84 ( new AVCF_Abilities_Cache() )->register();
85
86 // Forms: Gravity Forms (standalone cluster).
87 $gravity_detector = new AVCF_Gravity_Detector();
88 if ( $gravity_detector->avcf_gravity_is_available() ) {
89 ( new AVCF_Abilities_Gravity() )->register();
90 ( new AVCF_Abilities_Gravity_Pro() )->register();
91 }
92
93 // Forms: WPForms (standalone cluster).
94 $wpforms_detector = new AVCF_WPForms_Detector();
95 if ( $wpforms_detector->avcf_wpforms_is_available() ) {
96 ( new AVCF_Abilities_WPForms() )->register();
97 ( new AVCF_Abilities_WPForms_Pro() )->register();
98 }
99
100 // Forms: Fluent Forms (standalone cluster).
101 $fluent_detector = new AVCF_Fluent_Detector();
102 if ( $fluent_detector->avcf_fluent_is_available() ) {
103 ( new AVCF_Abilities_Fluent() )->register();
104 ( new AVCF_Abilities_Fluent_Pro() )->register();
105 }
106
107 // Forms: Formidable Forms (standalone cluster).
108 $formidable_detector = new AVCF_Formidable_Detector();
109 if ( $formidable_detector->avcf_formidable_is_available() ) {
110 ( new AVCF_Abilities_Formidable() )->register();
111 ( new AVCF_Abilities_Formidable_Pro() )->register();
112 }
113
114 // Forms: Forminator (standalone cluster).
115 $forminator_detector = new AVCF_Forminator_Detector();
116 if ( $forminator_detector->avcf_forminator_is_available() ) {
117 ( new AVCF_Abilities_Forminator() )->register();
118 ( new AVCF_Abilities_Forminator_Pro() )->register();
119 }
120
121 // Forms: Ninja Forms (standalone cluster).
122 $ninja_detector = new AVCF_Ninja_Detector();
123 if ( $ninja_detector->avcf_ninja_is_available() ) {
124 ( new AVCF_Abilities_Ninja() )->register();
125 ( new AVCF_Abilities_Ninja_Pro() )->register();
126 }
127
128 // Forms: Contact Form 7 (standalone cluster, config-only).
129 $cf7_detector = new AVCF_CF7_Detector();
130 if ( $cf7_detector->avcf_cf7_is_available() ) {
131 ( new AVCF_Abilities_CF7() )->register();
132 ( new AVCF_Abilities_CF7_Pro() )->register();
133 }
134
135 // Flamingo (standalone top-level cluster — CF7's companion entry store).
136 $flamingo_detector = new AVCF_Flamingo_Detector();
137 if ( $flamingo_detector->avcf_flamingo_is_available() ) {
138 ( new AVCF_Abilities_Flamingo() )->register();
139 ( new AVCF_Abilities_Flamingo_Pro() )->register();
140 }
141
142 // Third-party: WooCommerce.
143 $wc_detector = new AVCF_WC_Detector();
144 if ( $wc_detector->avcf_wc_is_available() ) {
145 ( new AVCF_Abilities_WooCommerce() )->register();
146 }
147
148 // Third-party: WP Activity Log.
149 $wpal_detector = new AVCF_WPAL_Detector();
150 if ( $wpal_detector->avcf_wpal_is_available() ) {
151 ( new AVCF_Abilities_WPAL() )->register();
152 }
153
154 // Third-party: Advanced Custom Fields.
155 $acf_detector = new AVCF_ACF_Detector();
156 if ( $acf_detector->avcf_acf_is_available() ) {
157 ( new AVCF_Abilities_ACF() )->register();
158 }
159
160 // Third-party: Yoast SEO.
161 $yoast_detector = new AVCF_Yoast_Detector();
162 if ( $yoast_detector->avcf_yoast_is_available() ) {
163 ( new AVCF_Abilities_Yoast() )->register();
164 }
165
166 // Third-party: Rank Math.
167 $rankmath_detector = new AVCF_RankMath_Detector();
168 if ( $rankmath_detector->avcf_rankmath_is_available() ) {
169 ( new AVCF_Abilities_RankMath() )->register();
170 }
171
172 // Third-party: All in One SEO.
173 $aioseo_detector = new AVCF_AIOSEO_Detector();
174 if ( $aioseo_detector->avcf_aioseo_is_available() ) {
175 ( new AVCF_Abilities_AIOSEO() )->register();
176 }
177
178 // Third-party: Elementor.
179 $elementor_detector = new AVCF_Elementor_Detector();
180 if ( $elementor_detector->avcf_elementor_is_available() ) {
181 ( new AVCF_Abilities_Elementor() )->register();
182 ( new AVCF_Abilities_Elementor_Pro() )->register();
183 }
184
185 // Third-party: Meta Box.
186 $metabox_detector = new AVCF_MetaBox_Detector();
187 if ( $metabox_detector->avcf_mb_is_available() ) {
188 ( new AVCF_Abilities_MetaBox() )->register();
189 }
190
191 // Third-party: JetEngine.
192 $jetengine_detector = new AVCF_JetEngine_Detector();
193 if ( $jetengine_detector->avcf_je_is_available() ) {
194 ( new AVCF_Abilities_JetEngine() )->register();
195 }
196
197 // Third-party: Pods.
198 $pods_detector = new AVCF_Pods_Detector();
199 if ( $pods_detector->avcf_pods_is_available() ) {
200 ( new AVCF_Abilities_Pods() )->register();
201 }
202
203 // Third-party: ACPT.
204 $acpt_detector = new AVCF_ACPT_Detector();
205 if ( $acpt_detector->avcf_acpt_is_available() ) {
206 ( new AVCF_Abilities_ACPT() )->register();
207 }
208
209 // Third-party: ASE.
210 $ase_detector = new AVCF_ASE_Detector();
211 if ( $ase_detector->avcf_ase_is_available() ) {
212 ( new AVCF_Abilities_ASE() )->register();
213 }
214
215 // Third-party: Bricks (Wave 4 builder).
216 $bricks_detector = new AVCF_Bricks_Detector();
217 if ( $bricks_detector->avcf_bricks_is_available() ) {
218 ( new AVCF_Abilities_Bricks() )->register();
219 ( new AVCF_Abilities_Bricks_Pro() )->register();
220 }
221
222 // Third-party: Divi (Wave 4 builder).
223 $divi_detector = new AVCF_Divi_Detector();
224 if ( $divi_detector->avcf_divi_is_available() ) {
225 ( new AVCF_Abilities_Divi() )->register();
226 ( new AVCF_Abilities_Divi_Pro() )->register();
227 }
228
229 // Third-party: WPBakery (Wave 4 builder).
230 $wpbakery_detector = new AVCF_WPBakery_Detector();
231 if ( $wpbakery_detector->avcf_wpbakery_is_available() ) {
232 ( new AVCF_Abilities_WPBakery() )->register();
233 ( new AVCF_Abilities_WPBakery_Pro() )->register();
234 }
235
236 // Third-party: Breakdance (Wave 4 builder).
237 $breakdance_detector = new AVCF_Breakdance_Detector();
238 if ( $breakdance_detector->avcf_breakdance_is_available() ) {
239 ( new AVCF_Abilities_Breakdance() )->register();
240 ( new AVCF_Abilities_Breakdance_Pro() )->register();
241 }
242
243 // Third-party: Etch (Wave 4 builder).
244 $etch_detector = new AVCF_Etch_Detector();
245 if ( $etch_detector->avcf_etch_is_available() ) {
246 ( new AVCF_Abilities_Etch() )->register();
247 ( new AVCF_Abilities_Etch_Pro() )->register();
248 }
249
250 // Third-party: Mosaic (Wave 4 builder).
251 $mosaic_detector = new AVCF_Mosaic_Detector();
252 if ( $mosaic_detector->avcf_mosaic_is_available() ) {
253 ( new AVCF_Abilities_Mosaic() )->register();
254 ( new AVCF_Abilities_Mosaic_Pro() )->register();
255 }
256 }
257
258 /**
259 * Authenticate MCP requests by mapping Atarim token to a WordPress user.
260 * This satisfies the MCP Adapter's is_user_logged_in() requirement.
261 */
262 /**
263 * Short-circuit the Atarim MCP endpoint when "Do It" is disabled.
264 *
265 * Returns a clear notice for every request to /atarim/mcp (list and call)
266 * unless the avc_enable_doit setting is on. The option is unset on sites that
267 * updated into this feature (treated as disabled) and set to '1' on fresh
268 * installs via the activation hook.
269 *
270 * @param mixed $result Dispatch result (WP_Error short-circuits).
271 * @param WP_REST_Server $server REST server instance.
272 * @param WP_REST_Request $request Current request.
273 * @return mixed
274 */
275 public function avcf_mcp_gate_when_disabled( $result, $server, $request ) {
276 if ( is_wp_error( $result ) ) {
277 return $result;
278 }
279
280 $route = is_object( $request ) && method_exists( $request, 'get_route' ) ? (string) $request->get_route() : '';
281 if ( strpos( $route, '/atarim/mcp' ) === false ) {
282 return $result;
283 }
284
285 $enabled = $this->function->avcf_get_setting_data( 'avc_enable_doit', false );
286 if ( empty( $enabled ) ) {
287 return new WP_Error(
288 'avc_doit_disabled',
289 __( 'Do It via Atarim AI is disabled for this site. Enable it from the Atarim plugin settings to allow execution.', 'atarim-visual-collaboration' ),
290 [ 'status' => 403 ]
291 );
292 }
293
294 return $result;
295 }
296
297 public function avcf_mcp_authenticate_request( $user_id ) {
298 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
299 return $user_id;
300 }
301
302 $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
303 if ( strpos( $request_uri, '/atarim/mcp' ) === false ) {
304 return $user_id;
305 }
306
307 if ( ! $this->auth->avcf_mcp_validate_request() ) {
308 return $user_id;
309 }
310
311 $webmaster_email = $this->function->avcf_get_setting_data( 'avc_website_developer' );
312 if ( empty( $webmaster_email ) ) {
313 $admins = get_users( [ 'role' => 'administrator', 'number' => 1 ] );
314 if ( ! empty( $admins ) ) {
315 return $admins[0]->ID;
316 }
317 return $user_id;
318 }
319
320 $user = get_user_by( 'email', $webmaster_email );
321 if ( $user && ! is_wp_error( $user ) ) {
322 return $user->ID;
323 }
324
325 return $user_id;
326 }
327
328 /**
329 * Setup the Atarim MCP server.
330 *
331 * Exposes the adapter meta-tools plus every registered MCP-public tool
332 * ability. Exposure follows registration automatically — there is no
333 * per-ability whitelist to maintain. The site owner selectively hides
334 * abilities (yours or third-party) via the avcf_mcp_blocked_abilities
335 * setting, without touching ability code.
336 */
337 public function avcf_mcp_setup_server( $adapter ) {
338 // Expose adapter meta-tools plus EVERY registered MCP-public tool
339 // ability (mcp.public === true, type 'tool'), mirroring the adapter's
340 // own discover-abilities logic. Abilities are exposed by registration
341 // alone now — building a new cluster needs no edit here. Hide specific
342 // abilities (yours or third-party) via the avcf_mcp_blocked_abilities
343 // setting; that blocklist is applied just below.
344 $tools = array(
345 // Meta-tools intentionally NOT exposed. discover-abilities /
346 // get-ability-info / execute-ability are a generic gateway over the
347 // whole abilities registry that bypasses the named-tool surface (and
348 // therefore the avcf_mcp_blocked_abilities blocklist applied below).
349 // With them disabled, a named tools/call is the only execution door,
350 // so the blocklist is a real boundary. Re-enable only if the agent
351 // must call abilities generically by name via execute-ability.
352 // 'mcp-adapter/discover-abilities',
353 // 'mcp-adapter/get-ability-info',
354 // 'mcp-adapter/execute-ability',
355 );
356
357 if ( function_exists( 'wp_get_abilities' ) ) {
358 foreach ( wp_get_abilities() as $ability ) {
359 if ( ! is_object( $ability ) || ! method_exists( $ability, 'get_meta' ) || ! method_exists( $ability, 'get_name' ) ) {
360 continue;
361 }
362 $meta = (array) $ability->get_meta();
363 $is_public = isset( $meta['mcp']['public'] ) ? (bool) $meta['mcp']['public'] : false;
364 if ( ! $is_public ) {
365 continue;
366 }
367 $mcp_type = isset( $meta['mcp']['type'] ) ? (string) $meta['mcp']['type'] : 'tool';
368 if ( $mcp_type !== 'tool' ) {
369 continue;
370 }
371 $tools[] = $ability->get_name();
372 }
373 }
374
375 $tools = array_values( array_unique( $tools ) );
376
377 // Allow site owner to block specific abilities via Atarim dashboard
378 $blocked = (array) $this->function->avcf_get_setting_data( 'avcf_mcp_blocked_abilities', [] );
379 $allowed = array_values( array_filter(
380 $tools,
381 function( $name ) use ( $blocked ) {
382 return ! in_array( $name, $blocked, true );
383 }
384 ) );
385
386 $adapter->create_server(
387 'atarim-mcp-server',
388 'atarim',
389 'mcp',
390 'Atarim MCP Server',
391 'Atarim AI action layer',
392 'v1.0.0',
393 [ HttpTransport::class ],
394 ErrorLogMcpErrorHandler::class,
395 NullMcpObservabilityHandler::class,
396 $allowed,
397 [],
398 [],
399 function() {
400 $incoming_token = isset( $_SERVER['HTTP_X_ATARIM_TOKEN'] )
401 ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_ATARIM_TOKEN'] ) )
402 : '';
403
404 $stored_token = $this->auth->avcf_mcp_get_token();
405
406 if ( empty( $incoming_token ) || empty( $stored_token ) ) {
407 return false;
408 }
409
410 return hash_equals( $stored_token, $incoming_token );
411 }
412 );
413 }
414
415 /**
416 * Fires when any plugin is updated.
417 * Fetches MCP token from Atarim backend if site is connected but token not yet saved.
418 */
419 public function avcf_mcp_on_plugin_update( $upgrader, $options ) {
420 if (
421 $options['type'] !== 'plugin' ||
422 $options['action'] !== 'update'
423 ) {
424 return;
425 }
426
427 if (
428 ! isset( $options['plugins'] ) ||
429 ! in_array( AVCF_PLUGIN_BASE, $options['plugins'], true )
430 ) {
431 return;
432 }
433
434 $site_id = $this->function->avcf_get_setting_data( 'avc_site_id' );
435 $is_connected = $this->function->avcf_get_setting_data( 'avc_initial_setup_complete' );
436 $existing_token = $this->auth->avcf_mcp_get_token();
437
438 if ( empty( $site_id ) || $is_connected !== 'yes' || ! empty( $existing_token ) ) {
439 return;
440 }
441
442 $this->avcf_mcp_fetch_token( $site_id );
443 }
444
445 /**
446 * Fetch MCP token from Atarim backend and save it.
447 */
448 public function avcf_mcp_fetch_token( $site_id ) {
449 $response = $this->function->avcf_make_api_call(
450 AVCF_CRM_API . 'wp-api/mcp/token',
451 [ 'site_id' => $site_id ],
452 '',
453 '',
454 'POST'
455 );
456
457 if (
458 isset( $response['status_code'] ) &&
459 $response['status_code'] === 200 &&
460 ! empty( $response['data']['mcp_token'] )
461 ) {
462 $this->function->avcf_update_settings(
463 'avc_atarim_secret_token',
464 trim( $response['data']['mcp_token'] )
465 );
466 }
467 }
468 }