Skip to content

Commit c8dde71

Browse files
andreilupuandreilupudkotterjeffpauldugyen
authored
Add MCP Access experiment: adapter auto-install and ability exposure controls (#992)
Added - MCP Access experiment: installs and activates the MCP Adapter plugin from WordPress.org when enabled, and adds a `Tools → MCP Access` screen to control which abilities are exposed to AI agents over MCP. Co-authored-by: andreilupu <euthelup@git.wordpress.org> Co-authored-by: dkotter <dkotter@git.wordpress.org> Co-authored-by: jeffpaul <jeffpaul@git.wordpress.org> Co-authored-by: dugyen <ugyensupport@git.wordpress.org> Co-authored-by: galatanovidiu <ovidiu-galatan@git.wordpress.org>
1 parent 54836fe commit c8dde71

22 files changed

Lines changed: 3102 additions & 12 deletions

‎docs/experiments/mcp-adapter.md‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# MCP Access
2+
3+
## Summary
4+
5+
The MCP Access experiment wires a site up to the [MCP Adapter](https://wordpress.org/plugins/mcp-adapter/) companion plugin and gives site owners control over which abilities are exposed to AI agents over the Model Context Protocol. Enabling the experiment installs and activates the MCP Adapter plugin from WordPress.org if it is not already active.
6+
7+
## Overview
8+
9+
### For End Users
10+
11+
When enabled, a new admin page appears under `Tools > MCP Access`. The page provides:
12+
13+
- The MCP server **endpoint URL**, read from the adapter's registered servers
14+
- The adapter's **install state**, with a manual install/activate button when it is missing or inactive
15+
- A table of registered abilities with per-ability **expose/hide** controls; a **Status** column shows which abilities are overridden, and toggling a checkbox back restores the default
16+
17+
This plugin's own abilities (`ai/*`) are exposed by default, since they only register while the feature or experiment providing them is enabled. Other abilities keep their developer-declared visibility unless the site owner changes them. Two groups are not listed: the adapter's own default-server abilities (`mcp-adapter/*`), which are the server's machinery and are active whenever the server runs, and deprecated aliases, which stay hidden because their replacement is listed instead.
18+
19+
While the experiment is enabled, the MCP Adapter plugin is installed and activated automatically on the first admin page load by a user who can install plugins. The attempt runs once per experiment activation: deactivating the adapter afterwards is respected, and toggling the experiment off and on runs the install again. Exposure overrides are only enforced while the experiment is enabled.
20+
21+
### For Developers
22+
23+
The experiment consists of:
24+
25+
1. **Experiment class** (`WordPress\AI\Experiments\MCP_Adapter\MCP_Adapter`): Boots the exposure filter, REST route, admin page, and installer
26+
2. **Auto-installer** (`WordPress\AI\Experiments\MCP_Adapter\Plugin_Installer`): Installs and activates the adapter from WordPress.org once per experiment enable, via `plugins_api()` and `Plugin_Upgrader`
27+
3. **Exposure overrides** (`WordPress\AI\Experiments\MCP_Adapter\Exposure_Overrides`): Injects saved overrides into ability registration meta so the adapter's default server honors them
28+
4. **REST controller** (`WordPress\AI\Experiments\MCP_Adapter\Settings_Controller`): Powers the admin page over `ai/v1/mcp/settings`
29+
5. **React admin app** (`src/experiments/mcp-adapter/`): Renders the install state and the ability exposure table
30+
31+
## Architecture & Implementation
32+
33+
### Key Hooks & Entry Points
34+
35+
`WordPress\AI\Experiments\MCP_Adapter\MCP_Adapter::register()` wires:
36+
37+
- `wp_register_ability_args` (via `Exposure_Overrides`) to apply exposure defaults and saved overrides as `meta.mcp.public`
38+
- `rest_api_init` to register the `ai/v1/mcp/settings` route
39+
- `admin_menu` and `admin_enqueue_scripts` for the Tools page
40+
- `admin_init` (via `Plugin_Installer`) for the once-per-enable install attempt
41+
42+
### Exposure Flow
43+
44+
1. The MCP Adapter resolves an ability's exposure from `meta.mcp.public`, falling back to `meta.public`.
45+
2. Overrides saved on the MCP Access screen are stored as an ability-name map and injected into that meta key at registration time, so the adapter picks them up with no hard dependency between the plugins. Class-based abilities extending `Abstract_Ability` merge the filtered meta over the meta their class declares.
46+
3. Without an override, `ai/*` abilities are injected as exposed, and abilities whose meta has a `deprecated` key are injected as hidden. Everything else is left untouched.
47+
4. The default is captured before injection, so the screen can always report and restore it.
48+
5. Overrides for the reserved `mcp-adapter/*` namespace and for deprecated aliases are never listed, accepted, or applied.
49+
50+
### Install Flow
51+
52+
1. On `admin_init`, if the current enable cycle has not been handled and the visitor can activate plugins, the installer resolves the adapter's install state.
53+
2. An active copy (including one installed under a non-standard directory, detected via the adapter's classes) marks the cycle handled with no action.
54+
3. Otherwise the installer claims the cycle atomically, downloads the plugin from WordPress.org if missing, and activates it. Success or failure is recorded; a failure is surfaced on the MCP Access screen with the manual button as the retry path.
55+
4. Disabling the experiment clears the marker, so re-enabling runs a fresh attempt.
56+
57+
### Data Storage
58+
59+
The experiment uses two options:
60+
61+
- `wpai_mcp_exposed_abilities`: exposure override map (`ability_name -> bool`); abilities not present keep their default
62+
- `wpai_mcp_adapter_autoinstall_handled`: once-per-enable marker (`'1'` on success, the error message on failure)
63+
64+
### Filters
65+
66+
- `wpai_mcp_adapter_plugin_slug`: Filters the WordPress.org slug of the companion plugin (useful for testing the install flow against a stand-in plugin)
67+
- `wpai_pre_mcp_adapter_autoinstall`: Short-circuits the automatic install; return `true` to report success or a `WP_Error` to report failure without touching the filesystem

‎includes/Abstracts/Abstract_Ability.php‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ abstract class Abstract_Ability extends WP_Ability {
2929
/**
3030
* Constructor.
3131
*
32+
* Meta from the registration args is merged over the class meta, so
33+
* `wp_register_ability_args` filters can adjust it.
34+
*
3235
* @since 0.1.0
36+
* @since x.x.x Merges meta from the registration args.
3337
*
3438
* @param string $name The name of the ability.
3539
* @param array<string,mixed> $properties The properties of the ability. Must include `label`.
@@ -45,7 +49,10 @@ public function __construct( string $name, array $properties = array() ) {
4549
'output_schema' => $this->output_schema(),
4650
'execute_callback' => array( $this, 'execute_callback' ),
4751
'permission_callback' => array( $this, 'permission_callback' ),
48-
'meta' => $this->meta(),
52+
'meta' => array_replace_recursive(
53+
$this->meta(),
54+
isset( $properties['meta'] ) && is_array( $properties['meta'] ) ? $properties['meta'] : array()
55+
),
4956
)
5057
);
5158
}

‎includes/Asset_Loader.php‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,32 @@ static function () use ( $message ) {
270270
);
271271
}
272272
}
273+
274+
/**
275+
* Enqueues the DataViews component styles.
276+
*
277+
* Uses the WP-registered handle when a WordPress release ships one;
278+
* otherwise falls back to the copy bundled into build/ by the build step.
279+
*
280+
* @since x.x.x
281+
*/
282+
public static function enqueue_dataviews_style(): void {
283+
if ( wp_styles()->query( 'wp-dataviews' ) ) {
284+
wp_enqueue_style( 'wp-dataviews' );
285+
return;
286+
}
287+
288+
$dataviews_css = WPAI_PLUGIN_DIR . 'build/admin/dataviews.css';
289+
290+
if ( ! file_exists( $dataviews_css ) ) {
291+
return;
292+
}
293+
294+
wp_enqueue_style(
295+
'ai-dataviews',
296+
WPAI_PLUGIN_URL . 'build/admin/dataviews.css',
297+
array(),
298+
(string) filemtime( $dataviews_css )
299+
);
300+
}
273301
}

‎includes/Experiments/Experiments.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ final class Experiments {
4343
\WordPress\AI\Experiments\Editorial_Notes\Editorial_Notes::class,
4444
\WordPress\AI\Experiments\Editorial_Updates\Editorial_Updates::class,
4545
\WordPress\AI\Experiments\Excerpt_Generation\Excerpt_Generation::class,
46+
\WordPress\AI\Experiments\MCP_Adapter\MCP_Adapter::class,
4647
\WordPress\AI\Experiments\Meta_Description\Meta_Description::class,
4748
\WordPress\AI\Experiments\Revision_Change_Summary\Revision_Change_Summary::class,
4849
\WordPress\AI\Experiments\Slug_Generation\Slug_Generation::class,
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
/**
3+
* Admin page for the MCP Access screen.
4+
*
5+
* @package WordPress\AI\Experiments\MCP_Adapter
6+
*/
7+
8+
declare( strict_types=1 );
9+
10+
namespace WordPress\AI\Experiments\MCP_Adapter;
11+
12+
if ( ! defined( 'ABSPATH' ) ) {
13+
exit;
14+
}
15+
16+
/**
17+
* Registers and renders the MCP Access admin page.
18+
*
19+
* @since x.x.x
20+
*/
21+
class Admin_Page {
22+
/**
23+
* The admin page slug.
24+
*
25+
* @since x.x.x
26+
* @var string
27+
*/
28+
public const PAGE_SLUG = 'ai-mcp-access';
29+
30+
/**
31+
* Initializes the admin page hooks.
32+
*
33+
* @since x.x.x
34+
*/
35+
public function init(): void {
36+
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
37+
}
38+
39+
/**
40+
* Adds the Tools submenu page.
41+
*
42+
* @since x.x.x
43+
*/
44+
public function add_admin_menu(): void {
45+
add_submenu_page(
46+
'tools.php',
47+
__( 'MCP Access', 'ai' ),
48+
__( 'MCP Access', 'ai' ),
49+
'manage_options',
50+
self::PAGE_SLUG,
51+
array( $this, 'render_page' )
52+
);
53+
}
54+
55+
/**
56+
* Renders the page mount point for the React app.
57+
*
58+
* @since x.x.x
59+
*/
60+
public function render_page(): void {
61+
if ( ! current_user_can( 'manage_options' ) ) {
62+
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'ai' ) );
63+
}
64+
65+
echo '<div class="wrap">';
66+
echo '<h1>' . esc_html__( 'MCP Access', 'ai' ) . '</h1>';
67+
echo '<div id="ai-mcp-access-root"></div>';
68+
echo '</div>';
69+
}
70+
}

0 commit comments

Comments
 (0)