PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.26.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.26.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / mcp / class-mcp-self-test.php

class-mcp-self-test.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.26.0, at includes/mcp/class-mcp-self-test.php

140 lines 5.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP connection self-test.
4 *
5 * @package ThinkRank\Mcp
6 */
7
8 declare(strict_types=1);
9
10 namespace ThinkRank\Mcp;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15
16 /**
17 * Exercises the MCP round trip the way an external client would and reports
18 * *where* it broke, so an admin can tell a certificate problem from an
19 * authentication problem from an ability-discovery problem without leaving the
20 * MCP page (see #189).
21 *
22 * It performs one real loopback `tools/list` call against this site's own MCP
23 * endpoint using the active connection token. The staged result names the first
24 * failing step: `disabled` → `not_connected` → `unreachable` → `tls` →
25 * `redirect` → `auth` → `no_tools` → `ok`.
26 */
27 final class Mcp_Self_Test {
28
29 /**
30 * Run the round-trip self-test.
31 *
32 * @return array<string, mixed>
33 */
34 public static function run(): array {
35 $endpoint = Mcp_Pairing::site_endpoint_fallback();
36
37 $result = [
38 'ok' => false,
39 'stage' => '',
40 'message' => '',
41 'endpoint' => $endpoint,
42 'mcp_enabled' => Mcp_Manager::is_enabled(),
43 'connected' => Mcp_Pairing::is_connected(),
44 'http_status' => null,
45 'redirected' => false,
46 'authenticated' => false,
47 'tools_count' => null,
48 ];
49
50 if ( ! $result['mcp_enabled'] ) {
51 $result['stage'] = 'disabled';
52 $result['message'] = __( 'MCP access is turned off, so the endpoint refuses every request. Enable MCP access above and try again.', 'thinkrank' );
53 return $result;
54 }
55
56 if ( ! $result['connected'] ) {
57 $result['stage'] = 'not_connected';
58 $result['message'] = __( 'No connection token exists yet. Click Connect to mint one, then run the test again.', 'thinkrank' );
59 return $result;
60 }
61
62 $response = wp_remote_post(
63 $endpoint,
64 [
65 'timeout' => 10,
66 // Don't follow redirects: a 301/302 here IS the finding (the
67 // classic http<->https scheme bounce), so surface it verbatim.
68 'redirection' => 0,
69 'headers' => [
70 'Authorization' => 'Bearer ' . Mcp_Pairing::site_token(),
71 'Content-Type' => 'application/json',
72 'Accept' => 'application/json',
73 ],
74 'body' => wp_json_encode(
75 [
76 'jsonrpc' => '2.0',
77 'id' => 1,
78 'method' => 'tools/list',
79 ]
80 ),
81 ]
82 );
83
84 if ( is_wp_error( $response ) ) {
85 $err = $response->get_error_message();
86 $is_tls = false !== stripos( $err, 'ssl' ) || false !== stripos( $err, 'certificate' );
87 $result['stage'] = $is_tls ? 'tls' : 'unreachable';
88 $result['message'] = $is_tls
89 /* translators: %s: underlying transport error. */
90 ? sprintf( __( 'The endpoint could not be reached over HTTPS: %s. On local/dev sites this is usually a self-signed certificate the AI client must be told to trust.', 'thinkrank' ), $err )
91 /* translators: %s: underlying transport error. */
92 : sprintf( __( 'The endpoint could not be reached: %s.', 'thinkrank' ), $err );
93 return $result;
94 }
95
96 $status = (int) wp_remote_retrieve_response_code( $response );
97 $result['http_status'] = $status;
98
99 if ( in_array( $status, [ 301, 302, 307, 308 ], true ) ) {
100 $location = (string) wp_remote_retrieve_header( $response, 'location' );
101 $result['redirected'] = true;
102 $result['stage'] = 'redirect';
103 $result['message'] = $location
104 /* translators: %s: redirect target URL. */
105 ? sprintf( __( 'The endpoint redirected to %s instead of answering. A redirect between HTTP and HTTPS usually means the site address and WordPress address schemes disagree.', 'thinkrank' ), $location )
106 : __( 'The endpoint redirected instead of answering, which usually means the site address and WordPress address schemes disagree.', 'thinkrank' );
107 return $result;
108 }
109
110 if ( 401 === $status || 403 === $status ) {
111 $result['stage'] = 'auth';
112 $result['message'] = __( 'The endpoint rejected the connection token (authentication failed). Rotate the token and reconnect your AI client.', 'thinkrank' );
113 return $result;
114 }
115
116 $result['authenticated'] = true;
117 $body = json_decode( (string) wp_remote_retrieve_body( $response ), true );
118 $tools = ( is_array( $body ) && isset( $body['result']['tools'] ) && is_array( $body['result']['tools'] ) )
119 ? $body['result']['tools']
120 : null;
121
122 if ( 200 !== $status || null === $tools ) {
123 $result['stage'] = 'no_tools';
124 $result['message'] = __( 'The endpoint answered but returned no tool catalog. Confirm the MCP runtime is built and abilities are registered.', 'thinkrank' );
125 $result['tools_count'] = is_array( $tools ) ? count( $tools ) : 0;
126 return $result;
127 }
128
129 $result['ok'] = true;
130 $result['stage'] = 'ok';
131 $result['tools_count'] = count( $tools );
132 $result['message'] = sprintf(
133 /* translators: %d: number of MCP tools returned. */
134 _n( 'Connection healthy: the endpoint authenticated and returned %d tool.', 'Connection healthy: the endpoint authenticated and returned %d tools.', $result['tools_count'], 'thinkrank' ),
135 $result['tools_count']
136 );
137 return $result;
138 }
139 }
140