PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.4
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.4
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 1.1.5 1.1.6 1.1.7 All 30 releases
← All changes | includes/modules/Mcp/Mcp_Tools.php +90 -12 1.2.41.3.4 View file →
@@ -29,8 +29,9 @@
29 29 use XSpeed\Server;
30 30 use XSpeed\Admin;
31 31 use XSpeed\Settings;
32 32 use XSpeed\Settings_Manager;
33 +use XSpeed\Module_Registry;
33 34 use XSpeed\Pro_Audit;
34 35 use XSpeed\Cache_Benchmark;
35 36 use XSpeed\Tier_Registry;
36 37 use XSpeed\Database_Cleaner;
@@ -39,9 +40,9 @@
39 40
40 41 final class Mcp_Tools {
41 42
42 43 /** Valid cache purge types. */
43 - public const PURGE_TYPES = array( 'all', 'page', 'assets', 'object', 'rest' );
44 + public const PURGE_TYPES = array( 'all', 'page', 'assets', 'object', 'rest', 'cloudflare', 'cdn' );
44 45
45 46 /**
46 47 * Per-call read-only override. Null means "defer to the pairing token's
47 48 * scope" (the JSON-RPC path that predates OAuth). true/false is set by
@@ -144,9 +145,9 @@
144 145 'write' => false,
145 146 'handler' => array( self::class, 'get_site_info' ),
146 147 ),
147 148 'optimize_site' => array(
148 - 'description' => 'Make this site faster, end to end: measure, apply the recommended settings ONE AT A TIME, check the page still renders after each, and undo any change that breaks it. Returns what was applied, the site\'s last recorded score, `next_steps` (riskier settings that could help but are NOT applied automatically), and `unfixable` (problems no caching plugin can reach). ALWAYS relay all three to the user: report the score and what is still wrong, then — if `next_steps` is non-empty — describe each one WITH its stated `risk` and ASK whether to run again with aggressiveness "aggressive". Never enable aggressive settings without the user agreeing first, and never present `unfixable` items as things you can solve; they need the site owner or the host. A site where nothing was left to do is a real, good answer — say so plainly rather than apologising or retrying. Use `dry_run` to preview the plan.',
149 + 'description' => 'Make this site faster, end to end: measure, apply the recommended settings ONE AT A TIME, check the page still renders after each, and undo any change that breaks it. Returns what was applied, the site\'s performance score, `next_steps` (riskier settings that could help but are NOT applied automatically), and `unfixable` (problems no caching plugin can reach). ALWAYS relay all three to the user: report the score and what is still wrong, then — if `next_steps` is non-empty — describe each one WITH its stated `risk` and ASK whether to run again with aggressiveness "aggressive". Never enable aggressive settings without the user agreeing first, and never present `unfixable` items as things you can solve; they need the site owner or the host. A site where nothing was left to do is a real, good answer — say so plainly rather than apologising or retrying. Use `dry_run` to preview the plan. The `score` object carries `age_seconds` and `stale`: quote the score WITH how recently it was measured, and never describe a stale score as the result this run produced — when a measurement could not be taken, say the number is old rather than implying it is current. WHEN CHANGES WERE APPLIED the response carries `verify_urls` and `verify_note`: the safety checks read HTML in PHP and cannot execute JavaScript, so a page can pass every one of them and still be broken in a browser. Before reporting success, OPEN each URL in `verify_urls` if you have any way to load a page and confirm it renders with no console errors; if you cannot, tell the user those URLs need checking and what a problem would look like. `verified: true` means the HTML checks passed — it is not a statement that the site works.',
149 150 'inputSchema' => self::object_schema(
150 151 array(
151 152 'aggressiveness' => array(
152 153 'type' => 'string',
@@ -156,8 +157,24 @@
156 157 'dry_run' => array(
157 158 'type' => 'boolean',
158 159 'description' => 'Return the plan without changing anything.',
159 160 ),
161 + 'measure_score' => array(
162 + 'type' => 'string',
163 + 'enum' => array( 'auto', 'never', 'always' ),
164 + 'description' => 'Whether to take a fresh PageSpeed measurement. auto (default) measures when the stored score is stale and after changes land; never reuses the stored score; always measures even for a dry run. Measurements are rate-limited, so a run inside the cooldown returns the stored score with its age rather than a new one.',
165 + ),
166 + 'target_score' => array(
167 + 'type' => 'integer',
168 + 'minimum' => 1,
169 + 'maximum' => 100,
170 + 'description' => 'Repeat the optimize cycle toward this score instead of running a single pass. Each round costs a real PageSpeed measurement and up to two minutes, so pass it only when the user asked for a specific number. Requires the iterative tuner; without it the run is a single pass and the report says so in `stopped_because`. The run also stops early when further rounds stop helping — either way, read `stopped_because` and relay it rather than retrying.',
171 + ),
172 + 'max_rounds' => array(
173 + 'type' => 'integer',
174 + 'minimum' => 1,
175 + 'description' => 'Ceiling on rounds when target_score is set. Clamped to what the tuner allows.',
176 + ),
160 177 ),
161 178 array()
162 179 ),
163 180 'write' => true,
@@ -175,9 +192,9 @@
175 192 'write' => false,
176 193 'handler' => array( self::class, 'get_pro_audit' ),
177 194 ),
178 195 'purge_cache' => array(
179 - 'description' => 'Purge the site cache. "type" selects what to purge: all, page, assets, object, or rest. Defaults to all.',
196 + 'description' => 'Purge the site cache and report what was actually cleared, what was skipped and why. "type" selects what to purge: all, page, assets, object, rest, cloudflare, or cdn. Defaults to all.',
180 197 'inputSchema' => self::object_schema(
181 198 array(
182 199 'type' => array(
183 200 'type' => 'string',
@@ -279,9 +296,9 @@
279 296 'write' => true,
280 297 'handler' => array( self::class, 'start_preloader' ),
281 298 ),
282 299 'run_score' => array(
283 - 'description' => 'Run an external performance audit (PageSpeed Insights, or GTmetrix when configured) against this site and return the score plus Core Web Vitals. Available on every install. Spends the site\'s own configured API quota. Use get_score_history to read past runs without starting a new one.',
300 + 'description' => 'Run an external performance audit (PageSpeed Insights, or GTmetrix when configured) against this site. Running it counts as the opt-in for external scores. With the site\'s own API key the score plus Core Web Vitals come back in the response; a keyless PageSpeed audit on a Hub-connected site is queued instead — the response says so, and the result lands in the history a minute or two later (poll with run_command "score status", or read get_score_history). Spends the site\'s own configured API quota when a key is set.',
284 301 'inputSchema' => self::object_schema(
285 302 array(
286 303 'target' => array(
287 304 'type' => 'string',
@@ -317,9 +334,9 @@
317 334 'write' => true,
318 335 'handler' => array( self::class, 'run_score' ),
319 336 ),
320 337 'run_pagespeed' => array(
321 - 'description' => 'Run an external performance audit (PageSpeed Insights, or GTmetrix when configured) and return the score + Core Web Vitals. Defaults to the site home page, mobile strategy. Requires external scores to be enabled in settings — the plugin makes no outbound calls otherwise.',
338 + 'description' => 'Run an external performance audit (PageSpeed Insights, or GTmetrix when configured) and return the score + Core Web Vitals. Defaults to the site home page, mobile strategy. Running it counts as the opt-in for external scores; a keyless PageSpeed audit routes through xSpeed Hub when the site is connected.',
322 339 'inputSchema' => self::object_schema(
323 340 array(
324 341 'url' => array(
325 342 'type' => 'string',
@@ -619,8 +636,9 @@
619 636 'update_settings' => 'xspeed settings',
620 637 'run_pagespeed' => 'xspeed psi',
621 638 'get_health' => 'xspeed health',
622 639 'get_score_history' => 'xspeed score',
640 + 'purge_cache' => 'xspeed purge',
623 641 );
624 642
625 643 $commands = Cli_Bridge::commands();
626 644 foreach ( $conditional as $tool => $command ) {
@@ -1157,14 +1175,29 @@
1157 1175 * @param array<string,mixed> $args Tool arguments.
1158 1176 * @return array<string,mixed>|\WP_Error
1159 1177 */
1160 1178 public static function optimize_site( array $args = array() ) {
1161 - return \XSpeed\Optimize_Runner::run(
1162 - array(
1163 - 'aggressiveness' => (string) ( $args['aggressiveness'] ?? 'standard' ),
1164 - 'dry_run' => (bool) ( $args['dry_run'] ?? false ),
1165 - )
1179 + $run = array(
1180 + 'aggressiveness' => (string) ( $args['aggressiveness'] ?? 'standard' ),
1181 + 'dry_run' => (bool) ( $args['dry_run'] ?? false ),
1182 + 'measure_score' => (string) ( $args['measure_score'] ?? 'auto' ),
1166 1183 );
1184 +
1185 + // Tuning arguments are forwarded ONLY when present, and are not
1186 + // understood by Free — a listener on `xspeed_optimize_report` reads
1187 + // them from that filter's `$context`. Passing them through rather
1188 + // than naming them in the array above is deliberate: this handler
1189 + // builds an explicit whitelist, so an argument it does not list is
1190 + // silently dropped. A caller asking to reach a score would have got a
1191 + // single pass and a success response — wrong behaviour with no error,
1192 + // which is the expensive kind to diagnose.
1193 + foreach ( array( 'target_score', 'max_rounds' ) as $key ) {
1194 + if ( isset( $args[ $key ] ) && is_numeric( $args[ $key ] ) ) {
1195 + $run[ $key ] = (int) $args[ $key ];
1196 + }
1197 + }
1198 +
1199 + return \XSpeed\Optimize_Runner::run( $run );
1167 1200 }
1168 1201
1169 1202 /**
1170 1203 * Before/after cache benchmark timings.
@@ -1212,12 +1245,43 @@
1212 1245 }
1213 1246 // Named source, not the default "manual": the purge log's whole job
1214 1247 // is to let an admin see that the cache cleared because an assistant
1215 1248 // asked, not because someone clicked.
1216 - $count = Cache::purge_type( $type, __( 'AI assistant', 'xspeed' ) );
1249 + $cause = __( 'AI assistant', 'xspeed' );
1250 +
1251 + /*
1252 + * `page`, `assets` and `rest` are fine-grained slices of the local
1253 + * sweep with no target of their own, and they predate this tool's
1254 + * per-store report — an assistant asking for `page` means the HTML,
1255 + * not the HTML plus the minified bundles plus every purge listener.
1256 + * They stay on purge_type() so their meaning does not change under
1257 + * callers already relying on it.
1258 + *
1259 + * Everything else routes through the runner — the same core function
1260 + * the CLI and the REST callback use — so an assistant told "cache
1261 + * cleared" is reading the same per-store verdict a human would get,
1262 + * including a Cloudflare zone that refused the purge.
1263 + */
1264 + if ( in_array( $type, array( 'page', 'assets', 'rest' ), true ) ) {
1265 + return array(
1266 + 'purged' => $type,
1267 + 'count' => Cache::purge_type( $type, $cause ),
1268 + 'ok' => true,
1269 + 'stats' => Cache::get_stats(),
1270 + );
1271 + }
1272 +
1273 + $report = \XSpeed\Purge_Runner::run( array( $type ), $cause );
1274 + $count = 0;
1275 + foreach ( $report['types'] as $row ) {
1276 + $count += (int) $row['entries'];
1277 + }
1278 +
1217 1279 return array(
1218 1280 'purged' => $type,
1219 1281 'count' => $count,
1282 + 'ok' => $report['ok'],
1283 + 'report' => $report['types'],
1220 1284 'stats' => Cache::get_stats(),
1221 1285 );
1222 1286 }
1223 1287
@@ -1497,13 +1561,26 @@
1497 1561 * @return true|\WP_Error True when every key would be applied.
1498 1562 */
1499 1563 private static function inspect_or_error( string $module, array $values ) {
1500 1564 $report = Settings_Manager::inspect_input( $module, $values );
1501 - if ( empty( $report['unknown'] ) && empty( $report['invalid'] ) ) {
1565 + if ( empty( $report['unknown'] ) && empty( $report['invalid'] ) && empty( $report['locked'] ) ) {
1502 1566 return true;
1503 1567 }
1504 1568
1505 1569 $parts = array();
1570 + // A field pinned by a wp-config.php constant cannot be written. Say so
1571 + // rather than returning a success the agent relays as "changed" over a
1572 + // write that update() would silently drop. (#398)
1573 + foreach ( $report['locked'] as $key ) {
1574 + $spec = ( Module_Registry::get( $module ) ? Module_Registry::get( $module )->settings_schema()[ $key ] ?? array() : array() );
1575 + $constant = Settings_Manager::effective_constant( $module, $key, is_array( $spec ) ? $spec : array() );
1576 + $parts[] = sprintf(
1577 + /* translators: 1: setting key, 2: wp-config.php constant name. */
1578 + __( '"%1$s" is defined in wp-config.php as %2$s and cannot be changed here', 'xspeed' ),
1579 + $key,
1580 + (string) $constant
1581 + );
1582 + }
1506 1583 foreach ( $report['unknown'] as $key ) {
1507 1584 $detail = sprintf(
1508 1585 /* translators: 1: setting key, 2: module slug. */
1509 1586 __( '"%1$s" is not a setting of module "%2$s"', 'xspeed' ),
@@ -1544,8 +1621,9 @@
1544 1621 array(
1545 1622 'status' => 400,
1546 1623 'refused_unknown' => $report['unknown'],
1547 1624 'refused_invalid' => $report['invalid'],
1625 + 'refused_locked' => $report['locked'],
1548 1626 'would_apply' => $report['applied'],
1549 1627 )
1550 1628 );
1551 1629 }