Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php
/**
* MCP authorization for the application password flow on wp-login.php.
*
* 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
*/

declare( strict_types = 1 );

defined( 'ABSPATH' ) || exit;

/**
* MCP_Authorization class.
*/
class MCP_Authorization {

/**
* The MCP application UUID.
*
* @var string
*/
const APP_ID = 'c4c73a54-96d7-47b9-9bdc-1a66b9b04505';

/**
* Initialize hooks.
*/
public static function init(): void {
add_filter( 'wporg_login_application_passwords_allowed_apps', array( __CLASS__, 'register' ) );
add_action( 'wp_authorize_application_password_form_approved_no_js', array( __CLASS__, 'render_config' ), 10, 3 );
add_action( 'login_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
}

/**
* Registers the WordPress.org MCP application.
*
* @param array $apps Allowed applications.
* @return array
*/
public static function register( array $apps ): array {
$apps[ self::APP_ID ] = array(
'name' => '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 );

?>
<div class="mcp-config-display">
<h2><?php esc_html_e( 'Set Up Your MCP Client' ); ?></h2>
<label for="mcp-config" class="authorize-application-details">
<?php esc_html_e( 'Copy the configuration below and add it to your MCP client.' ); ?>
</label>
<textarea id="mcp-config" readonly rows="14"><?php echo esc_textarea( $json ); ?></textarea>
<button type="button" class="button button-primary button-large" id="copy-mcp-config" data-copied="<?php esc_attr_e( 'Copied!' ); ?>"><?php esc_html_e( 'Copy' ); ?></button>
<ul class="client-notes">
<li><strong>Claude Desktop</strong> &mdash; <?php esc_html_e( 'Settings &rarr; Developer &rarr; Edit Config.' ); ?></li>
<li><strong>Claude Code</strong> &mdash;
<?php
/* translators: %s: file name */
printf( esc_html__( 'Add to %s in your project directory.' ), '<code>.mcp.json</code>' );
?>
</li>
<li><strong>Cursor</strong> &mdash; <?php esc_html_e( 'Settings &rarr; Tools and MCP &rarr; Add Custom MCP.' ); ?></li>
<li><strong>VS Code</strong> &mdash;
<?php
printf(
/* translators: 1: file path, 2: old key, 3: new key */
esc_html__( 'Save as %1$s &mdash; use %2$s instead of %3$s.' ),
'<code>.vscode/mcp.json</code>',
'<code>servers</code>',
'<code>mcpServers</code>'
);
?>
</li>
</ul>
</div>
<?php
}

/**
* Enqueues MCP-specific assets on the authorize_application login page.
*/
public static function enqueue_assets(): void {
global $action;

if ( 'authorize_application' !== $action ) {
return;
}

wp_enqueue_style(
'login-mcp-authorization',
plugins_url( 'style.css', __FILE__ ),
array( 'login-authorize-application' ),
filemtime( __DIR__ . '/style.css' )
);

wp_enqueue_script(
'login-mcp-copy-config',
plugins_url( 'copy-config.js', __FILE__ ),
array(),
filemtime( __DIR__ . '/copy-config.js' ),
true
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
document.getElementById( 'copy-mcp-config' ).addEventListener( 'click', function() {
const textarea = document.getElementById( 'mcp-config' );
navigator.clipboard.writeText( textarea.value ).then( function() {
const btn = document.getElementById( 'copy-mcp-config' );
const original = btn.textContent;
btn.textContent = btn.dataset.copied;
setTimeout( function() {
btn.textContent = original;
}, 2000 );
} );
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.login-action-authorize_application #login:has(.mcp-config-display) {
width: 540px;
}

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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.login-action-authorize_application #login {
width: 420px;
max-width: 90vw;
}

.login-action-authorize_application h2 {
margin-bottom: 1em;
}

.login form p.authorize-application-details {
color: #50575e;
font-size: 14px;
line-height: 1.6;
margin: 0;
}

.login .authorize-application-submit {
display: flex;
gap: 12px;
margin: 2em 0 0 !important;
padding: 0;
}

.login .authorize-application-submit.submit .button {
flex: 1;
text-align: center;
padding: 6px 0;
}

.login p.description {
margin-top: 1.5em;
padding-top: 1.5em;
border-top: 1px solid #f0f0f1;
font-size: 12px;
color: #787c82;
overflow-wrap: break-word;
line-height: 1.6;
}

.login p.description code {
font-size: 11px;
background: #f6f7f7;
padding: 2px 5px;
border-radius: 2px;
}

.login .authorize-application-password-display .input {
font-family: Consolas, Monaco, monospace;
text-align: center;
letter-spacing: 1px;
width: 100%;
}
Loading