%s %s
', esc_html__( 'ThinkRank MCP: AI assistants will connect but see no tools.', 'thinkrank' ), esc_html__( 'MCP access is enabled, but the bundled Abilities runtime (dependencies/vendor) is missing from this installation — usually a plugin package built without it. Reinstall ThinkRank from wordpress.org or an official build; until then, connected AI clients get an empty tool list.', 'thinkrank' ) ); } /** * Whether the MCP integration is enabled via the admin setting. * * @return bool */ public static function is_enabled(): bool { return (bool) Settings::instance()->get( 'enable_mcp', false ); } // -- Pretty endpoint: /thinkrank/mcp -- /** * Register rewrite rules for the MCP endpoint, OAuth discovery documents, * and the browser-facing authorize page. * * @return void */ public function add_rewrite(): void { $rules = self::rewrite_rules(); foreach ( $rules as $regex => $query ) { add_rewrite_rule( $regex, $query, 'top' ); } // Self-heal: flush once if ANY of our rules is missing from the stored // rewrite table, so the endpoints work without a manual permalink // re-save (and newly added rules trigger a re-flush on upgrade). // // Checked against the same array that was just registered, never // against a second hand-maintained list. The two drifted apart once // already: #775's first pass changed the discovery regex and left the // superseded one in the list, so a rule that is never registered was // permanently "missing" and every front-end request rebuilt the whole // rewrite table. Measured at 4 regenerations across 3 page loads // against 0 before the change — silent, because a rebuilt table is // still a correct one. // // It also has to notice the opposite: a rule of ours that is stored // but no longer wanted. The bare discovery rule is registered only // while thinkrank_mcp_serve_root_discovery is on, and "missing" alone // never fires when it is switched off again, because every rule still // registered is present. The stale rule then kept claiming the bare // URL from other MCP plugins (#775) until someone re-saved // permalinks. The unwanted set is derived from the same table built // with every optional rule on, not listed, so it cannot drift either. // A flush rebuilds the table from what this request registered, so // the stale rule is gone afterwards and the check settles instead of // flushing on every request. $stored = get_option( 'rewrite_rules' ); if ( is_array( $stored ) ) { $unwanted = array_diff_key( self::rewrite_rules( true ), $rules ); $stale = array_intersect_key( $unwanted, $stored ); $missing = array_diff_key( $rules, $stored ); if ( ! empty( $missing ) || ! empty( $stale ) ) { flush_rewrite_rules( false ); } } } /** * Every rewrite rule this plugin owns, as regex => query string. * * Single source of truth for registration AND for the self-heal check, so * the two cannot describe different sets of rules. All of them register at * `'top'`; a `'bottom'` rule would never be reached, because core's own * pagename rule matches almost any path ahead of it. * * @since 2.10.0 * * @param bool|null $root_discovery Include the opt-in bare discovery rule. * Null (the default) follows * {@see self::serves_root_discovery()}; * true is how add_rewrite() learns which * optional rules exist, to retire one * that was switched off. * @return array' . $sub . '
'; // phpcs:ignore WordPress.Security.EscapeOutput -- static translation; client name esc_html'd. echo '' . $lock . '' . esc_html__( 'Secured with OAuth. Revoke anytime in ThinkRank → MCP.', 'thinkrank' ) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput -- static icon; text esc_html'd. echo '' . esc_html( $message ) . '