is_mcp_request() ? true : $is_api_request; } /** * Exempt the MCP route from GoDaddy Launch's coming-soon REST restriction. * * @param mixed $endpoints Unrestricted REST endpoint path prefixes. * @return mixed Endpoints with the MCP route appended, or the input unchanged. */ public function allow_mcp_route( $endpoints ) { if ( ! is_array( $endpoints ) ) { return $endpoints; } $endpoints[] = self::MCP_ROUTE_PREFIX; return $endpoints; } /** * Determine whether the current request targets the MCP route. * * Has to read the raw request URI, because this runs before REST dispatch: neither * the rest_route query var nor WP_REST_Server exists yet. Covers the pretty * /wp-json/ form and the ?rest_route= fallback. * * Matching is anchored on a path-segment boundary rather than a substring search. * An unanchored search would also match '/page/airo-wp/v1/mcp-decoy' and * '/anything?x=airo-wp/v1/mcp', letting an arbitrary URL trigger Application * Password validation on a route that is not the MCP endpoint. The effect would be * limited -- invalid credentials still fail -- but there is no reason to accept it. * * @return bool */ public function is_mcp_request(): bool { if ( empty( $_SERVER['REQUEST_URI'] ) ) { return false; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized on the next line. $uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); if ( ! is_string( $uri ) || '' === $uri ) { return false; } // ?rest_route=/airo-wp/v1/mcp... — read just that one value rather than // parsing the whole query string, which keeps this free of by-reference // helpers and of any assumption about the rest of the query. $query = (string) wp_parse_url( $uri, PHP_URL_QUERY ); if ( '' !== $query ) { foreach ( explode( '&', $query ) as $pair ) { if ( 0 !== strpos( $pair, 'rest_route=' ) ) { continue; } $value = rawurldecode( substr( $pair, strlen( 'rest_route=' ) ) ); return '' !== $value && $this->is_mcp_route( $value ); } } // /wp-json/airo-wp/v1/mcp... — the prefix is filterable, so ask for it. $path = (string) wp_parse_url( $uri, PHP_URL_PATH ); if ( '' === $path ) { return false; } $prefix = '/' . trim( rest_get_url_prefix(), '/' ) . '/'; $position = strpos( $path, $prefix ); if ( false === $position ) { return false; } return $this->is_mcp_route( substr( $path, $position + strlen( $prefix ) ) ); } /** * Whether a REST route path is the MCP route or below it. * * Exact match, or followed by '/' so only a real path segment counts. The server * registers 'mcp/streamable', so the sub-route case is the normal one. * * @param string $route REST route path, with or without a leading slash. * @return bool */ private function is_mcp_route( string $route ): bool { $route = '/' . ltrim( $route, '/' ); return self::MCP_ROUTE_PREFIX === $route || 0 === strpos( $route, self::MCP_ROUTE_PREFIX . '/' ); } }