From 5111c915ada032c34f1ce570183b39a795a97bb4 Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Wed, 11 Feb 2026 14:03:38 -0600
Subject: [PATCH 01/11] WordPress.org: Add application password authorization
flow to wp-login.php.
Provides a login-based authorization flow for creating application
passwords, gated by a UUID-based app allowlist. After approval,
renders the MCP client configuration for easy copy-paste setup.
---
.../pub/authorize-application-login.php | 588 ++++++++++++++++++
1 file changed, 588 insertions(+)
create mode 100644 wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
new file mode 100644
index 0000000000..772086bbea
--- /dev/null
+++ b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
@@ -0,0 +1,588 @@
+ Map of app_id UUID => app config.
+ */
+function wporg_get_allowed_apps(): array {
+ return array(
+ 'c4c73a54-96d7-47b9-9bdc-1a66b9b04505' => array(
+ 'name' => 'WordPress.org MCP',
+ 'hosts' => array(),
+ ),
+ );
+}
+
+/**
+ * Handles the authorize_application action on wp-login.php.
+ */
+function wporg_handle_authorize_application_login(): void {
+ if ( ! is_user_logged_in() ) {
+ $authorize_url = add_query_arg(
+ array(
+ 'action' => 'authorize_application',
+ 'app_id' => sanitize_text_field( wp_unslash( $_REQUEST['app_id'] ?? '' ) ),
+ 'success_url' => sanitize_url( wp_unslash( $_REQUEST['success_url'] ?? '' ) ),
+ 'reject_url' => sanitize_url( wp_unslash( $_REQUEST['reject_url'] ?? '' ) ),
+ ),
+ site_url( 'wp-login.php', 'login' )
+ );
+
+ wp_safe_redirect( wp_login_url( $authorize_url ) );
+ exit;
+ }
+
+ $error = null;
+ $new_password = '';
+
+ // Handle form submission.
+ if ( 'POST' === $_SERVER['REQUEST_METHOD']
+ && isset( $_POST['wp_action'] ) && 'authorize_application_password' === $_POST['wp_action']
+ ) {
+ check_admin_referer( 'authorize_application_password' );
+
+ $app_id = sanitize_text_field( wp_unslash( $_POST['app_id'] ) );
+ $success_url = sanitize_url( wp_unslash( $_POST['success_url'] ) );
+ $reject_url = sanitize_url( wp_unslash( $_POST['reject_url'] ) );
+ $redirect = '';
+
+ // Re-validate POST values (don't trust hidden form fields).
+ $user = wp_get_current_user();
+ $allowed_apps = wporg_get_allowed_apps();
+ $app_name = isset( $allowed_apps[ $app_id ] ) ? $allowed_apps[ $app_id ]['name'] : '';
+ $request = compact( 'app_name', 'app_id', 'success_url', 'reject_url' );
+
+ $is_valid = wporg_validate_authorize_app_request( $request, $user );
+ if ( is_wp_error( $is_valid ) ) {
+ wp_die(
+ implode( ' ', $is_valid->get_error_messages() ),
+ __( 'Cannot Authorize Application' )
+ );
+ }
+
+ if ( isset( $_POST['reject'] ) ) {
+ $redirect = $reject_url ?: admin_url();
+ } elseif ( isset( $_POST['approve'] ) ) {
+ // Revoke any existing password for this app_id.
+ if ( $app_id ) {
+ $existing = WP_Application_Passwords::get_user_application_passwords( get_current_user_id() );
+ foreach ( $existing as $item ) {
+ if ( isset( $item['app_id'] ) && $item['app_id'] === $app_id ) {
+ $deleted = WP_Application_Passwords::delete_application_password( get_current_user_id(), $item['uuid'] );
+ if ( is_wp_error( $deleted ) ) {
+ $error = $deleted;
+ break;
+ }
+ }
+ }
+ }
+
+ if ( ! is_wp_error( $error ) ) {
+ $created = WP_Application_Passwords::create_new_application_password(
+ get_current_user_id(),
+ array(
+ 'name' => $app_name,
+ 'app_id' => $app_id,
+ )
+ );
+
+ if ( is_wp_error( $created ) ) {
+ $error = $created;
+ } else {
+ list( $new_password ) = $created;
+
+ if ( $success_url ) {
+ $redirect = add_query_arg(
+ array(
+ 'site_url' => urlencode( 'https://wordpress.org' ),
+ 'user_login' => urlencode( wp_get_current_user()->user_login ),
+ 'password' => urlencode( $new_password ),
+ ),
+ $success_url
+ );
+ }
+ }
+ }
+ }
+
+ if ( $redirect ) {
+ // Explicitly not using wp_safe_redirect b/c sends to arbitrary domain.
+ wp_redirect( $redirect );
+ exit;
+ }
+ }
+
+ // Extract and validate request parameters.
+ $app_id = sanitize_text_field( wp_unslash( $_REQUEST['app_id'] ?? '' ) );
+ $success_url = sanitize_url( wp_unslash( $_REQUEST['success_url'] ?? '' ) ) ?: null;
+
+ if ( ! empty( $_REQUEST['reject_url'] ) ) {
+ $reject_url = sanitize_url( wp_unslash( $_REQUEST['reject_url'] ) );
+ } elseif ( $success_url ) {
+ $reject_url = add_query_arg( 'success', 'false', $success_url );
+ } else {
+ $reject_url = null;
+ }
+
+ $user = wp_get_current_user();
+ $allowed_apps = wporg_get_allowed_apps();
+ $app_name = isset( $allowed_apps[ $app_id ] ) ? $allowed_apps[ $app_id ]['name'] : '';
+ $request = compact( 'app_name', 'app_id', 'success_url', 'reject_url' );
+
+ $is_valid = wporg_validate_authorize_app_request( $request, $user );
+ if ( is_wp_error( $is_valid ) ) {
+ wp_die(
+ implode( ' ', $is_valid->get_error_messages() ),
+ __( 'Cannot Authorize Application' )
+ );
+ }
+
+ if ( ! wp_is_application_passwords_available_for_user( $user ) ) {
+ if ( wp_is_application_passwords_available() ) {
+ $message = __( 'Application passwords are not available for your account. Please contact the site administrator for assistance.' );
+ } else {
+ $message = __( 'Application passwords are not available.' );
+ }
+
+ wp_die(
+ $message,
+ __( 'Cannot Authorize Application' ),
+ array(
+ 'response' => 501,
+ 'link_text' => __( 'Go Back' ),
+ 'link_url' => $reject_url ? add_query_arg( 'error', 'disabled', $reject_url ) : admin_url(),
+ )
+ );
+ }
+
+ // Render the authorization form.
+ $title = __( 'Authorize Application' );
+ $wp_error = new WP_Error();
+
+ if ( is_wp_error( $error ) ) {
+ $wp_error->add( 'authorize_error', $error->get_error_message() );
+ }
+
+ add_filter( 'login_site_html_link', '__return_empty_string' );
+
+ login_header( $title, '', $wp_error );
+
+ ?>
+
+ ' . esc_html( $app_name ) . ''
+ );
+ } else {
+ /* translators: %s: Website name. */
+ $message = sprintf(
+ __( 'Please log in to %s to proceed with authorization.' ),
+ get_bloginfo( 'name', 'display' )
+ );
+ }
+
+ $errors->add( 'authorize_application', $message, 'message' );
+
+ return $errors;
+}
+add_filter( 'wp_login_errors', 'wporg_authorize_application_login_message', 10, 2 );
+
+/**
+ * Outputs custom CSS for the authorize_application action on the login page.
+ */
+function wporg_authorize_application_login_styles(): void {
+ global $action;
+
+ if ( 'authorize_application' !== $action ) {
+ return;
+ }
+
+ ?>
+
+ add( 'invalid_redirect_url', __( 'The provided URL is not valid.' ) );
+ } elseif ( 'https' !== wp_parse_url( $request[ $key ], PHP_URL_SCHEME ) && ! $is_local ) {
+ $error->add( 'invalid_redirect_scheme', __( 'The URL must be served over a secure connection.' ) );
+ }
+ }
+
+ // Validate app_id format.
+ if ( empty( $request['app_id'] ) ) {
+ $error->add( 'missing_app_id', __( 'An application ID is required.' ) );
+ } elseif ( ! wp_is_uuid( $request['app_id'] ) ) {
+ $error->add( 'invalid_app_id', __( 'The application ID must be a UUID.' ) );
+ }
+
+ // Validate app_id against the allowlist and check callback domains.
+ $allowed_apps = wporg_get_allowed_apps();
+
+ if ( ! empty( $request['app_id'] ) && wp_is_uuid( $request['app_id'] ) ) {
+ if ( ! isset( $allowed_apps[ $request['app_id'] ] ) ) {
+ $error->add( 'unauthorized_app', __( 'This application is not authorized.' ) );
+ } else {
+ $allowed_hosts = $allowed_apps[ $request['app_id'] ]['hosts'];
+
+ if ( empty( $allowed_hosts ) ) {
+ if ( ! empty( $request['success_url'] ) || ! empty( $request['reject_url'] ) ) {
+ $error->add( 'redirect_not_allowed', __( 'This application does not support callback URLs.' ) );
+ }
+ } else {
+ foreach ( array( 'success_url', 'reject_url' ) as $key ) {
+ if ( empty( $request[ $key ] ) ) {
+ continue;
+ }
+
+ $host = wp_parse_url( $request[ $key ], PHP_URL_HOST );
+ if ( ! $host || ! in_array( $host, $allowed_hosts, true ) ) {
+ $error->add( 'unauthorized_redirect', __( 'The callback URL does not point to an allowed domain.' ) );
+ }
+ }
+ }
+ }
+ }
+
+ /** This action is documented in wp-admin/includes/user.php */
+ do_action( 'wp_authorize_application_password_request_errors', $error, $request, $user );
+
+ if ( $error->has_errors() ) {
+ return $error;
+ }
+
+ return true;
+}
+
+/**
+ * Renders the MCP client configuration after application password creation.
+ *
+ * @param string $new_password The newly created application password.
+ * @param array $request The request data.
+ * @param WP_User $user The user who authorized the application.
+ */
+function wporg_render_mcp_config( string $new_password, array $request, WP_User $user ): void {
+ $mcp_config = array(
+ 'mcpServers' => array(
+ 'wporg-mcp-server' => array(
+ 'command' => 'npx',
+ 'args' => array( '-y', '@automattic/mcp-wordpress-remote@latest' ),
+ 'env' => array(
+ 'WP_API_URL' => rest_url( 'mcp/wporg' ),
+ 'WP_API_USERNAME' => $user->user_login,
+ 'WP_API_PASSWORD' => WP_Application_Passwords::chunk_password( $new_password ),
+ ),
+ ),
+ ),
+ );
+ $json = wp_json_encode( $mcp_config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
+ $json = str_replace( ' ', ' ', $json ); // Use 2-space indentation instead of PHP's default 4-space.
+
+ ?>
+
+
+
+
+
+
+ - Claude Desktop —
+ - Claude Code — .mcp.json' );
+ ?>
+ - Cursor —
+ - VS Code — .vscode/mcp.json',
+ '
servers',
+ 'mcpServers'
+ );
+ ?>
+
+
+
+
Date: Wed, 11 Feb 2026 14:35:37 -0600
Subject: [PATCH 02/11] WordPress.org: Only render MCP config for the MCP app.
Guard wporg_render_mcp_config with an app_id check so it only
renders for the WordPress.org MCP application, not for any other
app that may be added to the allowlist in the future.
---
.../wp-content/mu-plugins/pub/authorize-application-login.php | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
index 772086bbea..4b47cb03c1 100644
--- a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
+++ b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
@@ -529,6 +529,10 @@ function wporg_validate_authorize_app_request( array $request, WP_User $user ):
* @param WP_User $user The user who authorized the application.
*/
function wporg_render_mcp_config( string $new_password, array $request, WP_User $user ): void {
+ if ( ( $request['app_id'] ?? '' ) !== 'c4c73a54-96d7-47b9-9bdc-1a66b9b04505' ) {
+ return;
+ }
+
$mcp_config = array(
'mcpServers' => array(
'wporg-mcp-server' => array(
From 9aa130f45e3aeff89020a91068176aef42fd777c Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Wed, 11 Feb 2026 14:38:43 -0600
Subject: [PATCH 03/11] WordPress.org: Fix double URL encoding and
case-sensitive host comparison.
Remove redundant urlencode() calls in add_query_arg() which already
handles encoding internally. Normalize hostnames to lowercase before
comparing against the allowlist since DNS is case-insensitive.
---
.../mu-plugins/pub/authorize-application-login.php | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
index 4b47cb03c1..7e29eb678d 100644
--- a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
+++ b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
@@ -105,9 +105,9 @@ function wporg_handle_authorize_application_login(): void {
if ( $success_url ) {
$redirect = add_query_arg(
array(
- 'site_url' => urlencode( 'https://wordpress.org' ),
- 'user_login' => urlencode( wp_get_current_user()->user_login ),
- 'password' => urlencode( $new_password ),
+ 'site_url' => 'https://wordpress.org',
+ 'user_login' => wp_get_current_user()->user_login,
+ 'password' => $new_password,
),
$success_url
);
@@ -502,8 +502,8 @@ function wporg_validate_authorize_app_request( array $request, WP_User $user ):
continue;
}
- $host = wp_parse_url( $request[ $key ], PHP_URL_HOST );
- if ( ! $host || ! in_array( $host, $allowed_hosts, true ) ) {
+ $host = strtolower( wp_parse_url( $request[ $key ], PHP_URL_HOST ) );
+ if ( ! $host || ! in_array( $host, array_map( 'strtolower', $allowed_hosts ), true ) ) {
$error->add( 'unauthorized_redirect', __( 'The callback URL does not point to an allowed domain.' ) );
}
}
From 80d0c385232615eddb6dddf829ccd47278805868 Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Wed, 11 Feb 2026 14:41:07 -0600
Subject: [PATCH 04/11] WordPress.org: Gate application password authorization
to login.wordpress.org.
Only load the mu-plugin on blog ID 350 (login.wordpress.org) so the
authorization flow is not available on other sites in the multisite.
---
.../mu-plugins/pub/authorize-application-login.php | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
index 7e29eb678d..1f90d105e8 100644
--- a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
+++ b/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
@@ -6,6 +6,11 @@
declare( strict_types = 1 );
+// Only load on login.wordpress.org.
+if ( get_current_blog_id() !== 350 ) {
+ return;
+}
+
/**
* Returns the registered applications, their names, and allowed callback domains.
*
From 77e98e377c93b58fcc176944c1066a05b37f3ba9 Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Wed, 11 Feb 2026 15:28:48 -0600
Subject: [PATCH 05/11] WordPress.org: Rename authorize-application-login.php
to login-application-passwords.php.
---
...rize-application-login.php => login-application-passwords.php} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename wordpress.org/public_html/wp-content/mu-plugins/pub/{authorize-application-login.php => login-application-passwords.php} (100%)
diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php
similarity index 100%
rename from wordpress.org/public_html/wp-content/mu-plugins/pub/authorize-application-login.php
rename to wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php
From 47bef8099588227664211d3e04eb0cf97aa96fb3 Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Thu, 12 Feb 2026 08:12:06 -0600
Subject: [PATCH 06/11] Apply suggestions from code review
Co-authored-by: Dion Hulse
---
.../mu-plugins/pub/login-application-passwords.php | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php
index 1f90d105e8..c0b8a54f8f 100644
--- a/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php
+++ b/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php
@@ -7,7 +7,8 @@
declare( strict_types = 1 );
// Only load on login.wordpress.org.
-if ( get_current_blog_id() !== 350 ) {
+// Limit to the login site on WordPress.org. Intentionally available to all sites in local environments.
+if ( defined( 'WPORG_LOGIN_REGISTER_BLOGID' ) && get_current_blog_id() !== WPORG_LOGIN_REGISTER_BLOGID ) {
return;
}
@@ -65,7 +66,7 @@ function wporg_handle_authorize_application_login(): void {
// Re-validate POST values (don't trust hidden form fields).
$user = wp_get_current_user();
$allowed_apps = wporg_get_allowed_apps();
- $app_name = isset( $allowed_apps[ $app_id ] ) ? $allowed_apps[ $app_id ]['name'] : '';
+ $app_name = $allowed_apps[ $app_id ]['name'] ?? '';
$request = compact( 'app_name', 'app_id', 'success_url', 'reject_url' );
$is_valid = wporg_validate_authorize_app_request( $request, $user );
@@ -142,7 +143,7 @@ function wporg_handle_authorize_application_login(): void {
$user = wp_get_current_user();
$allowed_apps = wporg_get_allowed_apps();
- $app_name = isset( $allowed_apps[ $app_id ] ) ? $allowed_apps[ $app_id ]['name'] : '';
+ $app_name = $allowed_apps[ $app_id ]['name'] ?? '';
$request = compact( 'app_name', 'app_id', 'success_url', 'reject_url' );
$is_valid = wporg_validate_authorize_app_request( $request, $user );
From 448d94560c647e347368433a88006d24f8dbc399 Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Thu, 12 Feb 2026 09:17:47 -0600
Subject: [PATCH 07/11] WordPress.org: Address code review feedback on
application password authorization.
Encode values passed to add_query_arg() to match core's authorize-application.php,
use null-coalesce consistently, and remove redundant comment.
---
.../mu-plugins/pub/login-application-passwords.php | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php b/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php
index c0b8a54f8f..4f41de91ee 100644
--- a/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php
+++ b/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php
@@ -6,7 +6,6 @@
declare( strict_types = 1 );
-// Only load on login.wordpress.org.
// Limit to the login site on WordPress.org. Intentionally available to all sites in local environments.
if ( defined( 'WPORG_LOGIN_REGISTER_BLOGID' ) && get_current_blog_id() !== WPORG_LOGIN_REGISTER_BLOGID ) {
return;
@@ -111,9 +110,9 @@ function wporg_handle_authorize_application_login(): void {
if ( $success_url ) {
$redirect = add_query_arg(
array(
- 'site_url' => 'https://wordpress.org',
- 'user_login' => wp_get_current_user()->user_login,
- 'password' => $new_password,
+ 'site_url' => urlencode( 'https://wordpress.org' ),
+ 'user_login' => urlencode( wp_get_current_user()->user_login ),
+ 'password' => urlencode( $new_password ),
),
$success_url
);
@@ -315,7 +314,7 @@ function wporg_authorize_application_login_message( WP_Error $errors, string $re
$allowed_apps = wporg_get_allowed_apps();
$app_id = $query['app_id'] ?? '';
- $app_name = isset( $allowed_apps[ $app_id ] ) ? $allowed_apps[ $app_id ]['name'] : '';
+ $app_name = $allowed_apps[ $app_id ]['name'] ?? '';
if ( $app_name ) {
/* translators: 1: Website name, 2: Application name. */
From 7de8da5c7133205c87d60b14a9d75751265ca10d Mon Sep 17 00:00:00 2001
From: Konstantin Obenland
Date: Tue, 17 Feb 2026 09:50:24 -0600
Subject: [PATCH 08/11] WordPress.org: Convert login-application-passwords from
mu-plugin to plugin.
---
.../login-application-passwords.php | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
rename wordpress.org/public_html/wp-content/{mu-plugins/pub => plugins/login-application-passwords}/login-application-passwords.php (99%)
diff --git a/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php
similarity index 99%
rename from wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php
rename to wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php
index 4f41de91ee..ef8cdedf32 100644
--- a/wordpress.org/public_html/wp-content/mu-plugins/pub/login-application-passwords.php
+++ b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php
@@ -1,7 +1,11 @@
Date: Tue, 17 Feb 2026 10:25:16 -0600
Subject: [PATCH 09/11] WordPress.org: Make allowed apps filterable and remove
MCP-specific code.
- Replace hardcoded app list with `wporg_login_application_passwords_allowed_apps` filter
- Validate filter output with `_doing_it_wrong` for malformed entries
- Default `hosts` to empty array via `wp_parse_args`
- Add app hosts to `allowed_redirect_hosts` for `wp_safe_redirect`
- Remove `wporg_render_mcp_config` and MCP-specific CSS (moving to wporg-abilities)
- Improve escaping throughout
---
.../login-application-passwords.php | 250 +++++++-----------
1 file changed, 99 insertions(+), 151 deletions(-)
diff --git a/wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php
index ef8cdedf32..6ca8c7af1e 100644
--- a/wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php
+++ b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/login-application-passwords.php
@@ -6,6 +6,8 @@
* Author: WordPress.org
* Author URI: https://wordpress.org/
* License: GPLv2 or later
+ *
+ * @package login-application-passwords
*/
declare( strict_types = 1 );
@@ -25,12 +27,64 @@
* @return array Map of app_id UUID => app config.
*/
function wporg_get_allowed_apps(): array {
- return array(
- 'c4c73a54-96d7-47b9-9bdc-1a66b9b04505' => array(
- 'name' => 'WordPress.org MCP',
- 'hosts' => array(),
- ),
- );
+ /**
+ * Filters the registered applications allowed to use the application password authorization flow.
+ *
+ * Each entry must be keyed by a valid UUID (the app_id) and contain an array
+ * with 'name' (non-empty string) and optionally 'hosts' (array of allowed
+ * callback domains). If 'hosts' is omitted, it defaults to an empty array,
+ * meaning the app does not support callback URLs.
+ *
+ * Example:
+ *
+ * add_filter( 'wporg_login_application_passwords_allowed_apps', function ( $apps ) {
+ * $apps['c4c73a54-96d7-47b9-9bdc-1a66b9b04505'] = array(
+ * 'name' => 'My Application',
+ * 'hosts' => array( 'example.com' ),
+ * );
+ * return $apps;
+ * } );
+ *
+ * @param array $apps Map of app_id UUID => app config.
+ */
+ $apps = apply_filters( 'wporg_login_application_passwords_allowed_apps', array() );
+
+ $validated = array();
+
+ foreach ( $apps as $app_id => $config ) {
+ $config = wp_parse_args( $config, array( 'hosts' => array() ) );
+
+ if ( ! wp_is_uuid( $app_id ) ) {
+ _doing_it_wrong(
+ __FUNCTION__,
+ sprintf( 'App ID must be a valid UUID. Got: %s', esc_html( $app_id ) ),
+ '1.0.0'
+ );
+ continue;
+ }
+
+ if ( ! is_array( $config ) || empty( $config['name'] ) || ! is_string( $config['name'] ) ) {
+ _doing_it_wrong(
+ __FUNCTION__,
+ sprintf( 'App "%s" must have a non-empty "name" string.', esc_html( $app_id ) ),
+ '1.0.0'
+ );
+ continue;
+ }
+
+ if ( ! is_array( $config['hosts'] ) ) {
+ _doing_it_wrong(
+ __FUNCTION__,
+ sprintf( 'App "%s" "hosts" must be an array.', esc_html( $app_id ) ),
+ '1.0.0'
+ );
+ continue;
+ }
+
+ $validated[ $app_id ] = $config;
+ }
+
+ return $validated;
}
/**
@@ -56,14 +110,15 @@ function wporg_handle_authorize_application_login(): void {
$new_password = '';
// Handle form submission.
- if ( 'POST' === $_SERVER['REQUEST_METHOD']
+ if ( isset( $_SERVER['REQUEST_METHOD'] )
+ && 'POST' === $_SERVER['REQUEST_METHOD']
&& isset( $_POST['wp_action'] ) && 'authorize_application_password' === $_POST['wp_action']
) {
check_admin_referer( 'authorize_application_password' );
- $app_id = sanitize_text_field( wp_unslash( $_POST['app_id'] ) );
- $success_url = sanitize_url( wp_unslash( $_POST['success_url'] ) );
- $reject_url = sanitize_url( wp_unslash( $_POST['reject_url'] ) );
+ $app_id = sanitize_text_field( wp_unslash( $_POST['app_id'] ?? '' ) );
+ $success_url = sanitize_url( wp_unslash( $_POST['success_url'] ?? '' ) );
+ $reject_url = sanitize_url( wp_unslash( $_POST['reject_url'] ?? '' ) );
$redirect = '';
// Re-validate POST values (don't trust hidden form fields).
@@ -75,13 +130,13 @@ function wporg_handle_authorize_application_login(): void {
$is_valid = wporg_validate_authorize_app_request( $request, $user );
if ( is_wp_error( $is_valid ) ) {
wp_die(
- implode( ' ', $is_valid->get_error_messages() ),
- __( 'Cannot Authorize Application' )
+ wp_kses_post( implode( ' ', $is_valid->get_error_messages() ) ),
+ esc_html__( 'Cannot Authorize Application' )
);
}
if ( isset( $_POST['reject'] ) ) {
- $redirect = $reject_url ?: admin_url();
+ $redirect = $reject_url ?: admin_url(); // phpcs:ignore Universal.Operators.DisallowShortTernary
} elseif ( isset( $_POST['approve'] ) ) {
// Revoke any existing password for this app_id.
if ( $app_id ) {
@@ -114,9 +169,9 @@ function wporg_handle_authorize_application_login(): void {
if ( $success_url ) {
$redirect = add_query_arg(
array(
- 'site_url' => urlencode( 'https://wordpress.org' ),
- 'user_login' => urlencode( wp_get_current_user()->user_login ),
- 'password' => urlencode( $new_password ),
+ 'site_url' => rawurlencode( 'https://wordpress.org' ),
+ 'user_login' => rawurlencode( wp_get_current_user()->user_login ),
+ 'password' => rawurlencode( $new_password ),
),
$success_url
);
@@ -126,15 +181,22 @@ function wporg_handle_authorize_application_login(): void {
}
if ( $redirect ) {
- // Explicitly not using wp_safe_redirect b/c sends to arbitrary domain.
- wp_redirect( $redirect );
+ $allowed_hosts = $allowed_apps[ $app_id ]['hosts'] ?? array();
+ add_filter(
+ 'allowed_redirect_hosts',
+ function ( $hosts ) use ( $allowed_hosts ) {
+ return array_merge( $hosts, $allowed_hosts );
+ }
+ );
+
+ wp_safe_redirect( $redirect );
exit;
}
}
// Extract and validate request parameters.
$app_id = sanitize_text_field( wp_unslash( $_REQUEST['app_id'] ?? '' ) );
- $success_url = sanitize_url( wp_unslash( $_REQUEST['success_url'] ?? '' ) ) ?: null;
+ $success_url = sanitize_url( wp_unslash( $_REQUEST['success_url'] ?? '' ) ) ?: null; // phpcs:ignore Universal.Operators.DisallowShortTernary
if ( ! empty( $_REQUEST['reject_url'] ) ) {
$reject_url = sanitize_url( wp_unslash( $_REQUEST['reject_url'] ) );
@@ -152,8 +214,8 @@ function wporg_handle_authorize_application_login(): void {
$is_valid = wporg_validate_authorize_app_request( $request, $user );
if ( is_wp_error( $is_valid ) ) {
wp_die(
- implode( ' ', $is_valid->get_error_messages() ),
- __( 'Cannot Authorize Application' )
+ wp_kses_post( implode( ' ', $is_valid->get_error_messages() ) ),
+ esc_html__( 'Cannot Authorize Application' )
);
}
@@ -165,12 +227,12 @@ function wporg_handle_authorize_application_login(): void {
}
wp_die(
- $message,
- __( 'Cannot Authorize Application' ),
+ esc_html( $message ),
+ esc_html__( 'Cannot Authorize Application' ),
array(
'response' => 501,
- 'link_text' => __( 'Go Back' ),
- 'link_url' => $reject_url ? add_query_arg( 'error', 'disabled', $reject_url ) : admin_url(),
+ 'link_text' => esc_html__( 'Go Back' ),
+ 'link_url' => esc_url( $reject_url ? add_query_arg( 'error', 'disabled', $reject_url ) : admin_url() ),
)
);
}
@@ -209,27 +271,27 @@ function wporg_handle_authorize_application_login(): void {
-
+
-
+
' . esc_html( $app_name ) . ''
);
?>
-
+
' . esc_html( $host ) . ''
);
} else {
- _e( 'You will be given a password to manually enter into the application in question.' );
+ esc_html_e( 'You will be given a password to manually enter into the application in question.' );
}
?>
@@ -321,16 +383,16 @@ function wporg_authorize_application_login_message( WP_Error $errors, string $re
$app_name = $allowed_apps[ $app_id ]['name'] ?? '';
if ( $app_name ) {
- /* translators: 1: Website name, 2: Application name. */
$message = sprintf(
- __( 'Please log in to %1$s to authorize %2$s to connect to your account.' ),
+ /* translators: 1: Website name, 2: Application name. */
+ esc_html__( 'Please log in to %1$s to authorize %2$s to connect to your account.' ),
get_bloginfo( 'name', 'display' ),
'' . esc_html( $app_name ) . ''
);
} else {
- /* translators: %s: Website name. */
$message = sprintf(
- __( 'Please log in to %s to proceed with authorization.' ),
+ /* translators: %s: Website name. */
+ esc_html__( 'Please log in to %s to proceed with authorization.' ),
get_bloginfo( 'name', 'display' )
);
}
@@ -358,10 +420,6 @@ function wporg_authorize_application_login_styles(): void {
max-width: 90vw;
}
- .login-action-authorize_application #login:has(.mcp-config-display) {
- width: 540px;
- }
-
.login-action-authorize_application h2 {
margin-bottom: 1em;
}
@@ -410,47 +468,6 @@ function wporg_authorize_application_login_styles(): void {
width: 100%;
}
- form:has(.mcp-config-display) .authorize-application-password-display {
- display: none;
- }
-
- .login .mcp-config-display textarea {
- font-family: Consolas, Monaco, monospace;
- font-size: 12px;
- line-height: 1.5;
- width: 100%;
- background: #f6f7f7;
- border: 1px solid #dcdcde;
- border-radius: 4px;
- resize: none;
- padding: 12px;
- margin-top: 1em;
- box-sizing: border-box;
- }
-
- .login .mcp-config-display #copy-mcp-config {
- float: none;
- width: 100%;
- text-align: center;
- margin-top: 12px;
- padding: 6px 0;
- }
-
- .login .mcp-config-display .client-notes {
- margin: 1.5em 0 0;
- padding: 0;
- list-style: none;
- font-size: 12px;
- color: #50575e;
- line-height: 1.8;
- }
-
- .login .mcp-config-display .client-notes code {
- font-size: 11px;
- background: #f6f7f7;
- padding: 2px 5px;
- border-radius: 2px;
- }
array(
- 'wporg-mcp-server' => array(
- 'command' => 'npx',
- 'args' => array( '-y', '@automattic/mcp-wordpress-remote@latest' ),
- 'env' => array(
- 'WP_API_URL' => rest_url( 'mcp/wporg' ),
- 'WP_API_USERNAME' => $user->user_login,
- 'WP_API_PASSWORD' => WP_Application_Passwords::chunk_password( $new_password ),
- ),
- ),
- ),
- );
- $json = wp_json_encode( $mcp_config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
- $json = str_replace( ' ', ' ', $json ); // Use 2-space indentation instead of PHP's default 4-space.
-
- ?>
-
-
-
-
-
-
- - Claude Desktop —
- - Claude Code — .mcp.json' );
- ?>
- - Cursor —
- - VS Code — .vscode/mcp.json',
- '
servers',
- 'mcpServers'
- );
- ?>
-
-
-
-
Date: Tue, 17 Feb 2026 13:41:52 -0600
Subject: [PATCH 10/11] Login Application Passwords: Add MCP client and extract
assets into files.
Introduces `MCP_Authorization` in `clients/mcp/` to register the
WordPress.org MCP server via the new allowed-apps filter and render
the client configuration after password creation.
Moves inline CSS and JS into enqueued files:
- `css/authorize-application.css` for the base authorization form.
- `clients/mcp/style.css` for MCP config display styles.
- `clients/mcp/copy-config.js` for the copy-to-clipboard button.
---
.../clients/mcp/class-mcp-authorization.php | 138 ++++++++++++++++++
.../clients/mcp/copy-config.js | 11 ++
.../clients/mcp/style.css | 45 ++++++
.../css/authorize-application.css | 52 +++++++
.../login-application-passwords.php | 71 ++-------
5 files changed, 255 insertions(+), 62 deletions(-)
create mode 100644 wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php
create mode 100644 wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/copy-config.js
create mode 100644 wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/style.css
create mode 100644 wordpress.org/public_html/wp-content/plugins/login-application-passwords/css/authorize-application.css
diff --git a/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php
new file mode 100644
index 0000000000..835ccc3b55
--- /dev/null
+++ b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php
@@ -0,0 +1,138 @@
+ 'WordPress.org MCP',
+ 'hosts' => array(),
+ );
+
+ return $apps;
+ }
+
+ /**
+ * Renders the MCP client configuration after application password creation.
+ *
+ * @param string $new_password The newly created application password.
+ * @param array $request The request data.
+ * @param WP_User $user The user who authorized the application.
+ */
+ public static function render_config( string $new_password, array $request, WP_User $user ): void {
+ if ( ( $request['app_id'] ?? '' ) !== self::APP_ID ) {
+ return;
+ }
+
+ $mcp_config = array(
+ 'mcpServers' => array(
+ 'wporg-mcp-server' => array(
+ 'command' => 'npx',
+ 'args' => array( '-y', '@automattic/mcp-wordpress-remote@latest' ),
+ 'env' => array(
+ 'WP_API_URL' => rest_url( 'mcp/wporg' ),
+ 'WP_API_USERNAME' => $user->user_login,
+ 'WP_API_PASSWORD' => WP_Application_Passwords::chunk_password( $new_password ),
+ ),
+ ),
+ ),
+ );
+
+ $json = wp_json_encode( $mcp_config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
+ $json = str_replace( ' ', ' ', $json );
+
+ ?>
+
+
+
+
+
+
+ - Claude Desktop —
+ - Claude Code —
+ .mcp.json' );
+ ?>
+
+ - Cursor —
+ - VS Code —
+ .vscode/mcp.json',
+ '
servers',
+ 'mcpServers'
+ );
+ ?>
+
+
+
+
-
-
Date: Tue, 17 Feb 2026 13:45:23 -0600
Subject: [PATCH 11/11] Login Application Passwords: Add authorization link to
MCP client file header.
---
.../clients/mcp/class-mcp-authorization.php | 3 +++
1 file changed, 3 insertions(+)
diff --git a/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php
index 835ccc3b55..f16eee550a 100644
--- a/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php
+++ b/wordpress.org/public_html/wp-content/plugins/login-application-passwords/clients/mcp/class-mcp-authorization.php
@@ -5,6 +5,9 @@
* Registers the WordPress.org MCP server as an allowed application and
* renders the client configuration after password creation.
*
+ * Authorization link:
+ * https://login.wordpress.org/wp-login.php?action=authorize_application&app_id=c4c73a54-96d7-47b9-9bdc-1a66b9b04505
+ *
* @package login-application-passwords
*/