PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.9.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.9.0
2.9.0 2.8.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 All 50 releases
← All changes | includes/mcp/class-mcp-self-test.php +76 -10 1.32.0 → 2.9.0 View file →
@@ -52,8 +52,15 @@
52 52 * none of them is a browser, which is exactly what "block bad bots" rules
53 53 * key on. A site that answers WordPress's own UA but 403s these is
54 54 * unreachable for every AI client while looking perfectly healthy from
55 55 * inside.
56 + *
57 + * DO NOT replace these with a descriptive agent such as
58 + * `ThinkRank-SelfTest/1.0`. An affected host allowlists a named agent and
59 + * keeps refusing `python-requests/…`, so the check would go green while
60 + * ChatGPT stays blocked — the exact false pass this test exists to catch.
61 + * SiteGround support has recommended that change; declining it is
62 + * deliberate. See #379.
56 63 */
57 64 private const CLIENT_USER_AGENTS = [
58 65 'python-requests/2.32.3',
59 66 'node-fetch/3.3.2',
@@ -59,8 +66,15 @@
59 66 'node-fetch/3.3.2',
60 67 ];
61 68
62 69 /**
70 + * Where an affected site owner is sent for the workaround list. The plugin
71 + * cannot fix an edge block, so the failing check hands over the diagnostic
72 + * and the host-side options instead.
73 + */
74 + private const HOSTING_DOC_URL = 'https://thinkrank.ai/docs/mcp/hosting-compatibility/';
75 +
76 + /**
63 77 * Run the round-trip self-test.
64 78 *
65 79 * @return array<string, mixed>
66 80 */
@@ -96,8 +110,18 @@
96 110 $result['message'] = __( 'No connection token exists yet. Click Connect to mint one, then run the test again.', 'thinkrank' );
97 111 return $result;
98 112 }
99 113
114 + if ( Mcp_Pairing::state()['token_sealed'] ) {
115 + // A token exists and still authenticates the clients holding it,
116 + // but this site can no longer decrypt it, so there is nothing to
117 + // present. Probing with '' would report an authentication failure
118 + // and point support at entirely the wrong thing.
119 + $result['stage'] = 'token_sealed';
120 + $result['message'] = __( 'A connection token exists but can no longer be read on this site — the security keys in wp-config.php changed after it was minted. Clients already set up with it keep working. Use Reset token to mint one this site can show, then run the test again.', 'thinkrank' );
121 + return $result;
122 + }
123 +
100 124 $token = Mcp_Pairing::site_token();
101 125
102 126 $pretty = self::probe_jsonrpc( $endpoint, $token );
103 127 $rest = self::probe_jsonrpc( $fallback, $token );
@@ -125,9 +149,9 @@
125 149 // Only reported when it could actually run — claiming a pass we did
126 150 // not measure is the failure mode this whole test exists to avoid.
127 151 $user_agent = self::probe_user_agent( $endpoint );
128 152 if ( null !== $user_agent ) {
129 - $result['checks'][] = self::check( 'user_agent', __( 'Client access', 'thinkrank' ), $user_agent['stage'], $user_agent['detail'] );
153 + $result['checks'][] = self::check( 'user_agent', __( 'Client access', 'thinkrank' ), $user_agent['stage'], $user_agent['detail'], $user_agent['doc_url'] ?? '' );
130 154 }
131 155
132 156 // Locked-out clients. The loopback below can pass while a REMOTE client
133 157 // is walled off by the failed-auth limiter — the exact state a connector
@@ -332,8 +356,41 @@
332 356 */
333 357 private static function probe_discovery(): array {
334 358 $documents = [];
335 359
360 + // --- Published files vs. the identity this site has NOW -----------
361 + // The static /.well-known/ documents embed absolute home_url()-derived
362 + // identifiers, and the whole reason they exist is that the host serves
363 + // them before WordPress. After a domain change, an http->https switch
364 + // or a staging clone, the stale copy therefore wins over the correct
365 + // dynamic route and the site advertises an issuer it no longer owns,
366 + // which a spec-compliant client must refuse (#486).
367 + //
368 + // Checked on disk, ahead of the HTTP probes below, because loopback
369 + // does not always take the path an external client does — a site can
370 + // serve a stale document to the internet while our own request never
371 + // sees it, and every probe below then passes.
372 + $stale = Mcp_Static_Discovery::stale_document();
373 + if ( null !== $stale ) {
374 + Mcp_Static_Discovery::refresh();
375 + $still_stale = Mcp_Static_Discovery::stale_document();
376 +
377 + if ( null !== $still_stale ) {
378 + return [
379 + 'stage' => 'stale_static_discovery',
380 + 'documents' => $documents,
381 + 'detail' => sprintf(
382 + /* translators: 1: file path relative to the site root, 2: identifier name, 3: value found in the file, 4: value it should carry. */
383 + __( 'The static discovery file %1$s advertises %2$s as %3$s, but this site is %4$s. It was written before the site URL changed, the host serves it ahead of WordPress, and it could not be rewritten or removed — so clients read the old identity and refuse to connect. Delete that file from the site root, or restore write access there and run this test again.', 'thinkrank' ),
384 + $still_stale['file'],
385 + $still_stale['key'],
386 + '' === $still_stale['found'] ? __( 'nothing', 'thinkrank' ) : $still_stale['found'],
387 + $still_stale['expected']
388 + ),
389 + ];
390 + }
391 + }
392 +
336 393 // --- The documents clients are POINTED at (must work) -------------
337 394 // The 401 challenge advertises the REST-served resource metadata, and
338 395 // spec-compliant clients derive the OIDC-suffix form of the AS
339 396 // metadata from our path-based issuer. Neither lives under the site
@@ -706,10 +763,11 @@
706 763 if ( in_array( $status, [ 200, 202, 401 ], true ) ) {
707 764 continue;
708 765 }
709 766 return [
710 - 'stage' => 'ua_filter',
711 - 'detail' => sprintf(
767 + 'stage' => 'ua_filter',
768 + 'doc_url' => self::HOSTING_DOC_URL,
769 + 'detail' => sprintf(
712 770 /* translators: 1: user agent string, 2: HTTP status returned for it, 3: HTTP status returned for WordPress's own user agent. */
713 771 __( 'The endpoint answered %3$d for WordPress but %2$d for an AI client\'s User-Agent (%1$s). ThinkRank deliberately tests with the generic agents real MCP backends send; this refusal means a security plugin, firewall or host-level "block bad bots" rule (SiteGround\'s edge protection does this) will also refuse the real AI client. Ask the host to exempt the MCP and /.well-known/ paths, or allowlist these User-Agents.', 'thinkrank' ),
714 772 $agent,
715 773 $status,
@@ -763,19 +821,27 @@
763 821
764 822 /**
765 823 * Shape one check for the UI list.
766 824 *
767 - * @param string $id Check id.
768 - * @param string $label Human label.
769 - * @param string $stage Resulting stage ('ok' when it passed).
770 - * @param string $detail Explanatory line.
771 - * @return array{id:string,label:string,ok:bool,detail:string}
825 + * @param string $id Check id.
826 + * @param string $label Human label.
827 + * @param string $stage Resulting stage ('ok' when it passed).
828 + * @param string $detail Explanatory line.
829 + * @param string $doc_url Optional docs page for a failure the user has to
830 + * fix outside WordPress. Omitted when empty.
831 + * @return array{id:string,label:string,ok:bool,detail:string,doc_url?:string}
772 832 */
773 - private static function check( string $id, string $label, string $stage, string $detail ): array {
774 - return [
833 + private static function check( string $id, string $label, string $stage, string $detail, string $doc_url = '' ): array {
834 + $check = [
775 835 'id' => $id,
776 836 'label' => $label,
777 837 'ok' => 'ok' === $stage,
778 838 'detail' => $detail,
779 839 ];
840 +
841 + if ( '' !== $doc_url ) {
842 + $check['doc_url'] = $doc_url;
843 + }
844 +
845 + return $check;
780 846 }
781 847 }