PluginProbe ʕ •ᴥ•ʔ
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback / trunk
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback vtrunk
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 days ago avcf-cluster-loader.php 3 days ago class-avcf-diagnostic.php 4 weeks ago class-avcf-mcp-auth.php 4 weeks ago class-avcf-mcp.php 3 days ago
class-avcf-mcp.php
619 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 /** JSON-RPC code the MCP adapter returns for an unknown tool name. */
34 const TOOL_NOT_FOUND = -32003;
35
36 private $function;
37 private $auth;
38
39 public function __construct() {
40 $this->function = new AVCF_Functions();
41 $this->auth = new AVCF_MCP_Auth();
42
43 $this->init_hooks();
44 }
45
46 private function init_hooks() {
47 // Security: suppress the MCP Adapter's auto-registered "default server"
48 // (/wp-json/mcp/mcp-adapter-default-server). It exposes every public
49 // ability through execute-ability with NO transport permission callback,
50 // so HttpTransport::check_permission falls back to current_user_can('read')
51 // — i.e. any logged-in user, bypassing the Atarim token gate entirely.
52 // Atarim registers its own token-authenticated server, so the default
53 // server is pure attack surface. Registered here (constructed before
54 // McpAdapter::instance() in the cluster loader) so it applies in time.
55 add_filter( 'mcp_adapter_create_default_server', '__return_false' );
56
57 // Authenticate MCP requests by mapping Atarim token to a WordPress user.
58 add_filter( 'determine_current_user', [ $this, 'avcf_mcp_authenticate_request' ], 20 );
59
60 // Gate the whole MCP endpoint behind the "Enable Do It" setting.
61 add_filter( 'rest_pre_dispatch', [ $this, 'avcf_mcp_gate_when_disabled' ], 5, 3 );
62
63 // Point unknown-tool errors at tools/list instead of leaving a dead end.
64 add_filter( 'rest_post_dispatch', [ $this, 'avcf_mcp_redirect_unknown_tool' ], 10, 3 );
65
66 // Setup Atarim MCP server during adapter init.
67 add_action( 'mcp_adapter_init', [ $this, 'avcf_mcp_setup_server' ] );
68
69 // Fetch MCP token when plugin is updated.
70 add_action( 'upgrader_process_complete', [ $this, 'avcf_mcp_on_plugin_update' ], 10, 2 );
71 }
72
73 /**
74 * Dispatch ability registration to each category class.
75 *
76 * Called on wp_abilities_api_init (hook registered in the main plugin file).
77 * Each category class is responsible for registering its own abilities with
78 * the WordPress Abilities API. Third-party integrations are guarded by a
79 * detector / function-exists check so we never instantiate an integration
80 * whose host plugin isn't active.
81 */
82 public function avcf_mcp_register_abilities() {
83 // Core WordPress abilities — always available.
84 ( new AVCF_Abilities_Content() )->register();
85 ( new AVCF_Abilities_Gutenberg() )->register();
86 ( new AVCF_Abilities_Plugins() )->register();
87 ( new AVCF_Abilities_Themes() )->register();
88 ( new AVCF_Abilities_Theme_Files() )->register();
89 ( new AVCF_Abilities_Core() )->register();
90 ( new AVCF_Abilities_Taxonomies() )->register();
91 ( new AVCF_Abilities_Users() )->register();
92 ( new AVCF_Abilities_Settings() )->register();
93 ( new AVCF_Abilities_Media() )->register();
94 ( new AVCF_Abilities_Metadata() )->register();
95 ( new AVCF_Abilities_Navigation() )->register();
96 ( new AVCF_Abilities_Templates() )->register();
97 ( new AVCF_Abilities_Global_Styles() )->register();
98 ( new AVCF_Abilities_Patterns() )->register();
99 ( new AVCF_Abilities_Block_Navigation() )->register();
100 ( new AVCF_Abilities_Cache() )->register();
101 ( new AVCF_Abilities_ReadOnly() )->register();
102 ( new AVCF_Abilities_ExecutePHP() )->register();
103 ( new AVCF_Abilities_WP_CLI() )->register();
104
105 // Forms: Gravity Forms (standalone cluster).
106 $gravity_detector = new AVCF_Gravity_Detector();
107 if ( $gravity_detector->avcf_gravity_is_available() ) {
108 ( new AVCF_Abilities_Gravity() )->register();
109 ( new AVCF_Abilities_Gravity_Pro() )->register();
110 }
111
112 // Forms: WPForms (standalone cluster).
113 $wpforms_detector = new AVCF_WPForms_Detector();
114 if ( $wpforms_detector->avcf_wpforms_is_available() ) {
115 ( new AVCF_Abilities_WPForms() )->register();
116 ( new AVCF_Abilities_WPForms_Pro() )->register();
117 }
118
119 // Forms: Fluent Forms (standalone cluster).
120 $fluent_detector = new AVCF_Fluent_Detector();
121 if ( $fluent_detector->avcf_fluent_is_available() ) {
122 ( new AVCF_Abilities_Fluent() )->register();
123 ( new AVCF_Abilities_Fluent_Pro() )->register();
124 }
125
126 // Forms: Formidable Forms (standalone cluster).
127 $formidable_detector = new AVCF_Formidable_Detector();
128 if ( $formidable_detector->avcf_formidable_is_available() ) {
129 ( new AVCF_Abilities_Formidable() )->register();
130 ( new AVCF_Abilities_Formidable_Pro() )->register();
131 }
132
133 // Forms: Forminator (standalone cluster).
134 $forminator_detector = new AVCF_Forminator_Detector();
135 if ( $forminator_detector->avcf_forminator_is_available() ) {
136 ( new AVCF_Abilities_Forminator() )->register();
137 ( new AVCF_Abilities_Forminator_Pro() )->register();
138 }
139
140 // Forms: Ninja Forms (standalone cluster).
141 $ninja_detector = new AVCF_Ninja_Detector();
142 if ( $ninja_detector->avcf_ninja_is_available() ) {
143 ( new AVCF_Abilities_Ninja() )->register();
144 ( new AVCF_Abilities_Ninja_Pro() )->register();
145 }
146
147 // Forms: Contact Form 7 (standalone cluster, config-only).
148 $cf7_detector = new AVCF_CF7_Detector();
149 if ( $cf7_detector->avcf_cf7_is_available() ) {
150 ( new AVCF_Abilities_CF7() )->register();
151 ( new AVCF_Abilities_CF7_Pro() )->register();
152 }
153
154 // Flamingo (standalone top-level cluster — CF7's companion entry store).
155 $flamingo_detector = new AVCF_Flamingo_Detector();
156 if ( $flamingo_detector->avcf_flamingo_is_available() ) {
157 ( new AVCF_Abilities_Flamingo() )->register();
158 ( new AVCF_Abilities_Flamingo_Pro() )->register();
159 }
160
161 // Third-party: WooCommerce.
162 $wc_detector = new AVCF_WC_Detector();
163 if ( $wc_detector->avcf_wc_is_available() ) {
164 ( new AVCF_Abilities_WooCommerce() )->register();
165 }
166
167 // Third-party: WP Activity Log.
168 $wpal_detector = new AVCF_WPAL_Detector();
169 if ( $wpal_detector->avcf_wpal_is_available() ) {
170 ( new AVCF_Abilities_WPAL() )->register();
171 }
172
173 // Third-party: Advanced Custom Fields.
174 $acf_detector = new AVCF_ACF_Detector();
175 if ( $acf_detector->avcf_acf_is_available() ) {
176 ( new AVCF_Abilities_ACF() )->register();
177 }
178
179 // Third-party: Yoast SEO.
180 $yoast_detector = new AVCF_Yoast_Detector();
181 if ( $yoast_detector->avcf_yoast_is_available() ) {
182 ( new AVCF_Abilities_Yoast() )->register();
183 }
184
185 // Third-party: Rank Math.
186 $rankmath_detector = new AVCF_RankMath_Detector();
187 if ( $rankmath_detector->avcf_rankmath_is_available() ) {
188 ( new AVCF_Abilities_RankMath() )->register();
189 }
190
191 // Third-party: All in One SEO.
192 $aioseo_detector = new AVCF_AIOSEO_Detector();
193 if ( $aioseo_detector->avcf_aioseo_is_available() ) {
194 ( new AVCF_Abilities_AIOSEO() )->register();
195 }
196
197 // Third-party: Elementor.
198 $elementor_detector = new AVCF_Elementor_Detector();
199 if ( $elementor_detector->avcf_elementor_is_available() ) {
200 ( new AVCF_Abilities_Elementor() )->register();
201 ( new AVCF_Abilities_Elementor_Pro() )->register();
202 }
203
204 $shortpixel_detector = new AVCF_ShortPixel_Detector();
205 if ( $shortpixel_detector->avcf_shortpixel_is_available() ) {
206 ( new AVCF_Abilities_ShortPixel() )->register();
207 }
208
209 $ewww_detector = new AVCF_EWWW_Detector();
210 if ( $ewww_detector->avcf_ewww_is_available() ) {
211 ( new AVCF_Abilities_EWWW() )->register();
212 }
213
214 $resmushit_detector = new AVCF_ReSmushit_Detector();
215 if ( $resmushit_detector->avcf_resmushit_is_available() ) {
216 ( new AVCF_Abilities_ReSmushit() )->register();
217 }
218
219 $smush_detector = new AVCF_Smush_Detector();
220 if ( $smush_detector->avcf_smush_is_available() ) {
221 ( new AVCF_Abilities_Smush() )->register();
222 }
223
224 $optimole_detector = new AVCF_Optimole_Detector();
225 if ( $optimole_detector->avcf_optimole_is_available() ) {
226 ( new AVCF_Abilities_Optimole() )->register();
227 }
228
229 // Third-party: Meta Box.
230 $metabox_detector = new AVCF_MetaBox_Detector();
231 if ( $metabox_detector->avcf_mb_is_available() ) {
232 ( new AVCF_Abilities_MetaBox() )->register();
233 }
234
235 // Third-party: JetEngine.
236 $jetengine_detector = new AVCF_JetEngine_Detector();
237 if ( $jetengine_detector->avcf_je_is_available() ) {
238 ( new AVCF_Abilities_JetEngine() )->register();
239 }
240
241 // Third-party: Pods.
242 $pods_detector = new AVCF_Pods_Detector();
243 if ( $pods_detector->avcf_pods_is_available() ) {
244 ( new AVCF_Abilities_Pods() )->register();
245 }
246
247 // Third-party: ACPT.
248 $acpt_detector = new AVCF_ACPT_Detector();
249 if ( $acpt_detector->avcf_acpt_is_available() ) {
250 ( new AVCF_Abilities_ACPT() )->register();
251 }
252
253 // Third-party: ASE.
254 $ase_detector = new AVCF_ASE_Detector();
255 if ( $ase_detector->avcf_ase_is_available() ) {
256 ( new AVCF_Abilities_ASE() )->register();
257 }
258
259 // Third-party: Bricks (Wave 4 builder).
260 $bricks_detector = new AVCF_Bricks_Detector();
261 if ( $bricks_detector->avcf_bricks_is_available() ) {
262 ( new AVCF_Abilities_Bricks() )->register();
263 ( new AVCF_Abilities_Bricks_Pro() )->register();
264 }
265
266 // Third-party: Divi (Wave 4 builder).
267 $divi_detector = new AVCF_Divi_Detector();
268 if ( $divi_detector->avcf_divi_is_available() ) {
269 ( new AVCF_Abilities_Divi() )->register();
270 ( new AVCF_Abilities_Divi_Pro() )->register();
271 }
272
273 // Third-party: WPBakery (Wave 4 builder).
274 $wpbakery_detector = new AVCF_WPBakery_Detector();
275 if ( $wpbakery_detector->avcf_wpbakery_is_available() ) {
276 ( new AVCF_Abilities_WPBakery() )->register();
277 ( new AVCF_Abilities_WPBakery_Pro() )->register();
278 }
279
280 // Third-party: Breakdance (Wave 4 builder).
281 $breakdance_detector = new AVCF_Breakdance_Detector();
282 if ( $breakdance_detector->avcf_breakdance_is_available() ) {
283 ( new AVCF_Abilities_Breakdance() )->register();
284 ( new AVCF_Abilities_Breakdance_Pro() )->register();
285 }
286
287 // Third-party: Etch (Wave 4 builder).
288 $etch_detector = new AVCF_Etch_Detector();
289 if ( $etch_detector->avcf_etch_is_available() ) {
290 ( new AVCF_Abilities_Etch() )->register();
291 ( new AVCF_Abilities_Etch_Pro() )->register();
292 }
293
294 // Third-party: Mosaic (Wave 4 builder).
295 $mosaic_detector = new AVCF_Mosaic_Detector();
296 if ( $mosaic_detector->avcf_mosaic_is_available() ) {
297 ( new AVCF_Abilities_Mosaic() )->register();
298 ( new AVCF_Abilities_Mosaic_Pro() )->register();
299 }
300
301 // Third-party: JetBackup (backup cluster). Native jetbackup/* abilities
302 // are blocklisted (see AVCF_JetBackup_Detector::filter_blocklist) so only
303 // our unified atarim/jetbackup-* surface is exposed.
304 $jetbackup_detector = new AVCF_JetBackup_Detector();
305 if ( $jetbackup_detector->avcf_jetbackup_is_available() ) {
306 ( new AVCF_Abilities_JetBackup_Backups() )->register();
307 ( new AVCF_Abilities_JetBackup_Restore() )->register();
308 ( new AVCF_Abilities_JetBackup_Jobs() )->register();
309 ( new AVCF_Abilities_JetBackup_Schedules() )->register();
310 ( new AVCF_Abilities_JetBackup_Destinations() )->register();
311 ( new AVCF_Abilities_JetBackup_Queue() )->register();
312 ( new AVCF_Abilities_JetBackup_Settings() )->register();
313 ( new AVCF_Abilities_JetBackup_System() )->register();
314 ( new AVCF_Abilities_JetBackup_Restore_Point() )->register();
315 }
316 }
317
318 /**
319 * Authenticate MCP requests by mapping Atarim token to a WordPress user.
320 * This satisfies the MCP Adapter's is_user_logged_in() requirement.
321 */
322 /**
323 * Whether a REST route/URI targets one of the MCP endpoints we protect.
324 *
325 * Covers the Atarim server (/atarim/mcp) and the MCP Adapter's default server
326 * (/mcp/mcp-adapter-default-server). The default server is disabled in
327 * init_hooks(); matching it here as well means the DoIt gate and token mapping
328 * still apply if it is ever re-enabled (e.g. by another consumer of the bundled
329 * adapter), rather than silently reopening an ungated surface.
330 *
331 * @param string $route_or_uri REST route or request URI.
332 * @return bool
333 */
334 private function avcf_is_protected_mcp_route( $route_or_uri ) {
335 $route_or_uri = (string) $route_or_uri;
336 return ( false !== strpos( $route_or_uri, '/atarim/mcp' ) )
337 || ( false !== strpos( $route_or_uri, '/mcp/mcp-adapter-default-server' ) );
338 }
339
340 /**
341 * Short-circuit the Atarim MCP endpoint when "Do It" is disabled.
342 *
343 * Returns a clear notice for every request to /atarim/mcp (list and call)
344 * unless the avc_enable_doit setting is on. The option is unset on sites that
345 * updated into this feature (treated as disabled) and set to '1' on fresh
346 * installs via the activation hook.
347 *
348 * @param mixed $result Dispatch result (WP_Error short-circuits).
349 * @param WP_REST_Server $server REST server instance.
350 * @param WP_REST_Request $request Current request.
351 * @return mixed
352 */
353 public function avcf_mcp_gate_when_disabled( $result, $server, $request ) {
354 if ( is_wp_error( $result ) ) {
355 return $result;
356 }
357
358 $route = is_object( $request ) && method_exists( $request, 'get_route' ) ? (string) $request->get_route() : '';
359 if ( ! $this->avcf_is_protected_mcp_route( $route ) ) {
360 return $result;
361 }
362
363 $enabled = $this->function->avcf_get_setting_data( 'avc_enable_doit', false );
364 if ( empty( $enabled ) ) {
365 return new WP_Error(
366 'avc_doit_disabled',
367 __( 'Do It via Atarim AI is disabled for this site. Enable it from the Atarim plugin settings to allow execution.', 'atarim-visual-collaboration' ),
368 [ 'status' => 403 ]
369 );
370 }
371
372 return $result;
373 }
374
375 public function avcf_mcp_authenticate_request( $user_id ) {
376 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
377 return $user_id;
378 }
379
380 $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
381 if ( ! $this->avcf_is_protected_mcp_route( $request_uri ) ) {
382 return $user_id;
383 }
384
385 if ( ! $this->auth->avcf_mcp_validate_request() ) {
386 return $user_id;
387 }
388
389 $webmaster_email = $this->function->avcf_get_setting_data( 'avc_website_developer' );
390 if ( empty( $webmaster_email ) ) {
391 $admins = get_users( [ 'role' => 'administrator', 'number' => 1 ] );
392 if ( ! empty( $admins ) ) {
393 return $admins[0]->ID;
394 }
395 return $user_id;
396 }
397
398 $user = get_user_by( 'email', $webmaster_email );
399 if ( $user && ! is_wp_error( $user ) ) {
400 return $user->ID;
401 }
402
403 return $user_id;
404 }
405
406 /**
407 * Setup the Atarim MCP server.
408 *
409 * Exposes the adapter meta-tools plus every registered MCP-public tool
410 * ability. Exposure follows registration automatically — there is no
411 * per-ability whitelist to maintain. The site owner selectively hides
412 * abilities (yours or third-party) via the avcf_mcp_blocked_abilities
413 * setting, without touching ability code.
414 */
415 public function avcf_mcp_setup_server( $adapter ) {
416 // Expose adapter meta-tools plus EVERY registered MCP-public tool
417 // ability (mcp.public === true, type 'tool'), mirroring the adapter's
418 // own discover-abilities logic. Abilities are exposed by registration
419 // alone now — building a new cluster needs no edit here. Hide specific
420 // abilities (yours or third-party) via the avcf_mcp_blocked_abilities
421 // setting; that blocklist is applied just below.
422 $tools = array(
423 // Meta-tools intentionally NOT exposed. discover-abilities /
424 // get-ability-info / execute-ability are a generic gateway over the
425 // whole abilities registry that bypasses the named-tool surface (and
426 // therefore the avcf_mcp_blocked_abilities blocklist applied below).
427 // With them disabled, a named tools/call is the only execution door,
428 // so the blocklist is a real boundary. Re-enable only if the agent
429 // must call abilities generically by name via execute-ability.
430 // 'mcp-adapter/discover-abilities',
431 // 'mcp-adapter/get-ability-info',
432 // 'mcp-adapter/execute-ability',
433 );
434
435 if ( function_exists( 'wp_get_abilities' ) ) {
436 foreach ( wp_get_abilities() as $ability ) {
437 if ( ! is_object( $ability ) || ! method_exists( $ability, 'get_meta' ) || ! method_exists( $ability, 'get_name' ) ) {
438 continue;
439 }
440 $meta = (array) $ability->get_meta();
441 $is_public = isset( $meta['mcp']['public'] ) ? (bool) $meta['mcp']['public'] : false;
442 if ( ! $is_public ) {
443 continue;
444 }
445 $mcp_type = isset( $meta['mcp']['type'] ) ? (string) $meta['mcp']['type'] : 'tool';
446 if ( $mcp_type !== 'tool' ) {
447 continue;
448 }
449 $tools[] = $ability->get_name();
450 }
451 }
452
453 $tools = array_values( array_unique( $tools ) );
454
455 // Allow site owner to block specific abilities via Atarim dashboard
456 $blocked = (array) $this->function->avcf_get_setting_data( 'avcf_mcp_blocked_abilities', [] );
457 // Allow integrations (e.g. the JetBackup cluster) to contribute blocked
458 // names conditionally, without persisting them to the stored setting.
459 $blocked = (array) apply_filters( 'avcf_mcp_blocked_abilities', $blocked );
460
461 $allowed = array_values( array_filter(
462 $tools,
463 function( $name ) use ( $blocked ) {
464 return ! in_array( $name, $blocked, true );
465 }
466 ) );
467
468 $adapter->create_server(
469 'atarim-mcp-server',
470 'atarim',
471 'mcp',
472 'Atarim MCP Server',
473 'Atarim AI action layer',
474 'v1.0.0',
475 [ HttpTransport::class ],
476 ErrorLogMcpErrorHandler::class,
477 NullMcpObservabilityHandler::class,
478 $allowed,
479 [],
480 [],
481 function() {
482 $incoming_token = isset( $_SERVER['HTTP_X_ATARIM_TOKEN'] )
483 ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_ATARIM_TOKEN'] ) )
484 : '';
485
486 $stored_token = $this->auth->avcf_mcp_get_token();
487
488 if ( empty( $incoming_token ) || empty( $stored_token ) ) {
489 return false;
490 }
491
492 return hash_equals( $stored_token, $incoming_token );
493 }
494 );
495 }
496
497 /**
498 * Replace the adapter's bare "Tool not found: X" with an instruction to
499 * re-read the tool list.
500 *
501 * The adapter answers an unknown tool name with a JSON-RPC -32003 and
502 * nothing else, so a caller that guessed a name has no route back and
503 * commonly guesses again, or reports the invented name upstream as though
504 * it were real.
505 *
506 * We deliberately do NOT suggest alternatives. tools/list is the
507 * authoritative set and already reflects which plugins are active on this
508 * site; anything we computed here would be an approximation of it, and a
509 * wrong suggestion is worse than none because it invites a call to an
510 * unrelated tool.
511 *
512 * @param mixed $response Dispatch result.
513 * @param WP_REST_Server $server REST server instance.
514 * @param WP_REST_Request $request Current request.
515 * @return mixed
516 */
517 public function avcf_mcp_redirect_unknown_tool( $response, $server, $request ) {
518 if ( ! is_object( $request ) || ! method_exists( $request, 'get_route' ) ) {
519 return $response;
520 }
521 if ( strpos( (string) $request->get_route(), '/atarim/mcp' ) === false ) {
522 return $response;
523 }
524 if ( ! is_object( $response ) || ! method_exists( $response, 'get_data' ) || ! method_exists( $response, 'set_data' ) ) {
525 return $response;
526 }
527
528 $data = $response->get_data();
529 if ( ! is_array( $data ) || $data === [] ) {
530 return $response;
531 }
532
533 // A JSON-RPC batch comes back as a list of responses.
534 if ( isset( $data[0] ) && is_array( $data[0] ) ) {
535 $changed = false;
536 foreach ( $data as $i => $entry ) {
537 $new = $this->avcf_mcp_rewrite_not_found( $entry );
538 if ( null !== $new ) { $data[ $i ] = $new; $changed = true; }
539 }
540 if ( $changed ) { $response->set_data( $data ); }
541 return $response;
542 }
543
544 $new = $this->avcf_mcp_rewrite_not_found( $data );
545 if ( null !== $new ) { $response->set_data( $new ); }
546 return $response;
547 }
548
549 /** Returns the rewritten entry, or null if it isn't a tool-not-found error. */
550 private function avcf_mcp_rewrite_not_found( $entry ) {
551 if ( ! is_array( $entry ) || ! isset( $entry['error']['code'] ) ) {
552 return null;
553 }
554 if ( (int) $entry['error']['code'] !== self::TOOL_NOT_FOUND ) {
555 return null;
556 }
557
558 $entry['error']['message'] = __(
559 'No tool or ability with that name exists on this site. Call tools/list to get the current list, and use only a name that appears in it exactly. Which tools exist depends on which plugins are active on this site, so a tool that exists elsewhere may not exist here. If nothing in the list does what you need, that capability is unavailable here: report that and stop, rather than trying another name.',
560 'atarim-visual-collaboration'
561 );
562
563 return $entry;
564 }
565
566 /**
567 * Fires when any plugin is updated.
568 * Fetches MCP token from Atarim backend if site is connected but token not yet saved.
569 */
570 public function avcf_mcp_on_plugin_update( $upgrader, $options ) {
571 if (
572 $options['type'] !== 'plugin' ||
573 $options['action'] !== 'update'
574 ) {
575 return;
576 }
577
578 if (
579 ! isset( $options['plugins'] ) ||
580 ! in_array( AVCF_PLUGIN_BASE, $options['plugins'], true )
581 ) {
582 return;
583 }
584
585 $site_id = $this->function->avcf_get_setting_data( 'avc_site_id' );
586 $is_connected = $this->function->avcf_get_setting_data( 'avc_initial_setup_complete' );
587 $existing_token = $this->auth->avcf_mcp_get_token();
588
589 if ( empty( $site_id ) || $is_connected !== 'yes' || ! empty( $existing_token ) ) {
590 return;
591 }
592
593 $this->avcf_mcp_fetch_token( $site_id );
594 }
595
596 /**
597 * Fetch MCP token from Atarim backend and save it.
598 */
599 public function avcf_mcp_fetch_token( $site_id ) {
600 $response = $this->function->avcf_make_api_call(
601 AVCF_CRM_API . 'wp-api/mcp/token',
602 [ 'site_id' => $site_id ],
603 '',
604 '',
605 'POST'
606 );
607
608 if (
609 isset( $response['status_code'] ) &&
610 $response['status_code'] === 200 &&
611 ! empty( $response['data']['mcp_token'] )
612 ) {
613 $this->function->avcf_update_settings(
614 'avc_atarim_secret_token',
615 trim( $response['data']['mcp_token'] )
616 );
617 }
618 }
619 }