PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.7
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_Server.php +40 -5 1.3.1 → 1.3.7 View file →
@@ -67,8 +67,14 @@
67 67 }
68 68 Mcp_Rate_Limiter::clear();
69 69
70 70 $raw = $request->get_body();
71 + // Second line behind McpModule::cap_request_body(), which is the one
72 + // that runs before WordPress decodes. Kept because the pretty
73 + // front-door path reaches here without WP_REST_Server::dispatch().
74 + if ( strlen( $raw ) > Mcp_Tools::MAX_TOOL_BODY_BYTES ) {
75 + return self::error_response( null, self::INVALID_REQUEST, 'Request body is too large.', 413 );
76 + }
71 77 $msg = json_decode( $raw, true );
72 78
73 79 if ( null === $msg && JSON_ERROR_NONE !== json_last_error() ) {
74 80 return self::error_response( null, self::PARSE_ERROR, 'Parse error: body is not valid JSON.', 400 );
@@ -127,10 +133,11 @@
127 133 'capabilities' => array(
128 134 'tools' => array( 'listChanged' => false ),
129 135 ),
130 136 'serverInfo' => array(
131 - 'name' => 'xspeed',
132 - 'version' => defined( 'XSPEED_VERSION' ) ? XSPEED_VERSION : '1.0.0',
137 + 'name' => 'xspeed',
138 + 'version' => defined( 'XSPEED_VERSION' ) ? XSPEED_VERSION : '1.0.0',
139 + 'xspeedAuth' => self::$auth_kind,
133 140 ),
134 141 )
135 142 );
136 143
@@ -216,9 +223,20 @@
216 223 *
217 224 * @param \WP_REST_Request $request Incoming request.
218 225 * @return bool
219 226 */
227 + /**
228 + * Which credential authorized this request: 'site' (the pairing token)
229 + * or 'oauth'. Reported in `initialize` so xSpeed Hub stores only the
230 + * pairing token when a site syncs it. An OAuth token can be read-only
231 + * and expires within the hour; storing one would break the Hub.
232 + *
233 + * @var string
234 + */
235 + private static $auth_kind = '';
236 +
220 237 private static function authorize( \WP_REST_Request $request ): bool {
238 + self::$auth_kind = '';
221 239 $presented = self::extract_token( $request );
222 240 if ( '' === $presented ) {
223 241 return false;
224 242 }
@@ -231,8 +249,9 @@
231 249 $stored = Mcp_Pairing::site_token();
232 250 if ( '' !== $stored && hash_equals( $stored, $presented ) ) {
233 251 Mcp_Tools::set_read_only_override( null );
234 252 Mcp_Tools::set_configure_override( null );
253 + self::$auth_kind = 'site';
235 254 return true;
236 255 }
237 256
238 257 // Path 2: an OAuth 2.1 access token minted by Mcp_OAuth. Its own
@@ -242,8 +261,9 @@
242 261 $grant = Mcp_OAuth::validate_token( $presented );
243 262 if ( null !== $grant ) {
244 263 Mcp_Tools::set_read_only_override( Mcp_OAuth::scope_is_read_only( $grant['scope'] ) );
245 264 Mcp_Tools::set_configure_override( Mcp_OAuth::scope_allows_configure( $grant['scope'] ) );
265 + self::$auth_kind = 'oauth';
246 266 return true;
247 267 }
248 268
249 269 return false;
@@ -260,9 +280,9 @@
260 280
261 281 /**
262 282 * Where this site actually serves its protected-resource metadata.
263 283 *
264 - * Prefers the canonical /.well-known/ URL, but many hosts own that prefix
284 + * Prefers the canonical /.well-known/…/xspeed/mcp URL, but many hosts own that prefix
265 285 * for ACME/Let's Encrypt and answer it before WordPress runs — the client
266 286 * then follows a pointer to a 404 (or a redirect to the homepage) and the
267 287 * OAuth flow dead-ends. RFC 9728 allows a single resource_metadata value,
268 288 * so when the pretty path is not ours to serve we advertise the /wp-json
@@ -268,9 +288,24 @@
268 288 * so when the pretty path is not ours to serve we advertise the /wp-json
269 289 * fallback, which no ACME tooling claims.
270 290 */
271 291 private static function metadata_url(): string {
272 - $pretty = home_url( '/.well-known/oauth-protected-resource' );
292 + // RFC 9728 §3.1: a resource whose identifier carries a path is
293 + // discovered at the path-suffixed form. Always this one, never the
294 + // root form — even on a site where root is still ours to serve. The
295 + // challenge is what steers every re-discovery, so pointing it at the
296 + // canonical identity is what eventually moves clients onto it; and
297 + // its value must not depend on whether some other plugin happens to
298 + // be installed, or a client that cached the header would find the
299 + // URL under it change meaning. Root exists for clients that never
300 + // read this header at all. (#266)
301 + //
302 + // Built off untrailingslashit() because get_home_url() concatenates
303 + // the `home` option verbatim: with a trailing slash stored there,
304 + // home_url( '/.well-known/…' ) returns a doubled slash and the URL
305 + // 404s.
306 + $pretty = untrailingslashit( home_url( '/' ) )
307 + . '/.well-known/oauth-protected-resource/' . Mcp_Pairing::SITE_ENDPOINT_PATH;
273 308
274 309 /**
275 310 * Filter the advertised protected-resource metadata URL.
276 311 *
@@ -283,9 +318,9 @@
283 318
284 319 // Rewrites absent (plain permalinks, or a flush that never landed)
285 320 // means the pretty URL cannot resolve at all — use the fallback.
286 321 if ( ! McpModule::wellknown_rewrites_active() ) {
287 - return rest_url( McpModule::NS . '/mcp/.well-known/oauth-protected-resource' );
322 + return Mcp_Pairing::absolute( rest_url( McpModule::NS . '/mcp/.well-known/oauth-protected-resource' ) );
288 323 }
289 324
290 325 return $pretty;
291 326 }