PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.6
1.3.7 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 All 33 releases
← All changes | includes/modules/Mcp/Mcp_Pairing.php +45 -2 1.2.4 → 1.3.6 View file →
@@ -58,11 +58,54 @@
58 58
59 59 /**
60 60 * The PRIMARY endpoint the user pastes into their AI client — this
61 61 * site's own MCP URL. No hosted infra involved.
62 + *
63 + * Normalised, because get_home_url() is not: it concatenates the `home`
64 + * option with the path verbatim, so a site whose `home` carries a
65 + * trailing slash yields `https://site//xspeed/mcp`. That string is the
66 + * OAuth resource AND (since #266) the issuer, so every discovery URL a
67 + * client derives from it would carry the doubled slash and 404 — and the
68 + * connect URL the user pastes would too. Mcp_Hub::site_url_canonical()
69 + * defends the same way for the attach nonce.
62 70 */
71 + /**
72 + * Collapse the doubled slash a trailing-slash `home` option leaves behind.
73 + *
74 + * `get_home_url()` appends `'/' . ltrim( $path, '/' )` to the raw option,
75 + * so a site stored as `https://example.test/` yields
76 + * `https://example.test//xspeed/authorize`, and `rest_url()` inherits the
77 + * same doubling through its pretty-permalink branch. The URLs still
78 + * resolve, but they are published in discovery documents that clients
79 + * compare as strings.
80 + *
81 + * Only the run immediately after the authority is collapsed. A doubled
82 + * slash deeper in a path can be meaningful, and rebuilding REST URLs by
83 + * hand instead would lose `index.php/wp-json`, the plain-permalink
84 + * `?rest_route=` form, and anything the `rest_url` filter did. (#266 QA)
85 + *
86 + * A subdirectory install doubles the slash after the subdirectory rather
87 + * than after the host (`https://x/blog//wp-json/...`), so the whole path
88 + * is collapsed, not just the run behind the authority.
89 + *
90 + * @param string $url Absolute URL.
91 + */
92 + public static function absolute( string $url ): string {
93 + if ( ! preg_match( '#^([a-z][a-z0-9+.-]*://[^/?\#]+)(.*)$#is', $url, $m ) ) {
94 + return $url;
95 + }
96 + // Only the path is collapsed -- never the query or the fragment,
97 + // where a doubled slash can carry meaning (a nested URL in a
98 + // redirect_to, say).
99 + $rest = $m[2];
100 + $split = strcspn( $rest, '?#' );
101 + $path = (string) preg_replace( '#/{2,}#', '/', substr( $rest, 0, $split ) );
102 +
103 + return $m[1] . $path . substr( $rest, $split );
104 + }
105 +
63 106 public static function site_endpoint(): string {
64 - return home_url( '/' . self::SITE_ENDPOINT_PATH );
107 + return untrailingslashit( home_url( '/' ) ) . '/' . self::SITE_ENDPOINT_PATH;
65 108 }
66 109
67 110 /**
68 111 * Always-on fallback endpoint via the REST namespace, for hosts where
@@ -68,9 +111,9 @@
68 111 * Always-on fallback endpoint via the REST namespace, for hosts where
69 112 * the pretty rewrite can't be served (e.g. plain permalinks).
70 113 */
71 114 public static function site_endpoint_fallback(): string {
72 - return rest_url( 'xspeed/v1/mcp' );
115 + return self::absolute( rest_url( 'xspeed/v1/mcp' ) );
73 116 }
74 117
75 118 /**
76 119 * The SINGLE URL the user pastes into their AI client — the pretty