Skip to content
Draft
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
40 changes: 40 additions & 0 deletions docs/experiments/mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# MCP Experiment

## Purpose

The MCP experiment surfaces a control panel where site owners can provision one or more Model Context Protocol (MCP) servers, expose hand-picked abilities, generate ready-to-use client configuration, and validate connectivity without leaving wp-admin.

## Prerequisites

- WordPress 6.8+
- PHP 7.4+
- The `wordpress/mcp-adapter`, `wordpress/abilities-api`, and `wordpress/wp-ai-client` Composer dependencies installed (`composer install`)
- Built admin assets (`npm run build` or `npm run start` during development)
- An Application Password for the administrator who will authenticate remote clients (`Users → Profile → Application Passwords`)

## UI Overview

1. **Status & Transports** – Shows whether MCP is globally enabled, then for the selected server displays its REST endpoint (`/wp-json/{namespace}/{route}`) plus the `wp mcp-adapter serve --server=<id>` STDIO command. Copy buttons keep the values aligned with the current site URL.
2. **Client Configuration Generator** – Provides JSON templates for Claude Desktop, Cursor, and a generic MCP client. The templates embed the site’s REST endpoint and highlight the `MCP_HEADERS` variable that should contain a Base64-encoded Application Password credential.
3. **Servers Toolbar** – Administrators can switch between existing servers or create new ones. Each server tracks its own route namespace/slug, transport list, and ability allow-list.
4. **Exposed Abilities Table** – Lists every registered ability (with category + provider badges) and lets administrators toggle whether it’s available for the selected server. When a server’s allow-list is empty it automatically falls back to “all MCP-public abilities”.
4. **Connection Test** – Issues a lightweight HTTP request against the endpoint and reports the HTTP status code so admins can confirm routing/authentication before wiring an external client.

## Implementation Notes

- `WordPress\AI\Experiments\MCP\Manager` owns all configuration: it stores server definitions in `ai_mcp_servers`, migrates the legacy `ai_mcp_enabled_tools` option, initializes the adapter, and registers each server (optionally passing custom allow-lists) during the `mcp_adapter_init` hook.
- REST routes live under `ai/v1/mcp` via `WordPress\AI\Experiments\MCP\REST\MCP_Controller`. Endpoints include overview (`GET /ai/v1/mcp?server_id=...`), global enable toggle, server CRUD, per-server tool updates, and connection tests.
- The React application is built from `src/admin/mcp-server` and enqueued from `WordPress\AI\Experiments\MCP\Admin_Page` (top-level **MCP** menu). Assets compile to `build/admin/mcp-server.js` and `build/admin/style-mcp-server.css`.
- Client-side data hydrates via `window.aiMcpServerSettings`, which now only contains REST routing + nonce metadata; the UI fetches its state from the overview endpoint on load and whenever the selected server changes.

## Manual Testing Checklist

1. Run `composer install` and `npm install` if dependencies are missing.
2. Build the assets with `npm run build` (or `npm run start` while iterating).
3. Navigate to the top-level **MCP** menu as an administrator. Confirm the status card shows “Running” once `/wp-json/{namespace}/{route}` is reachable.
4. Use the server selector to add an additional server. Verify a new REST route slug is generated and that it appears in the status card + CLI copy helper.
5. Toggle abilities for each server and ensure the REST response updates immediately (the allow-list lives inside `ai_mcp_servers`).
6. Use the **Copy URL** and **Copy Command** buttons and verify clipboard contents.
7. Use the **Client configuration** selector to copy the Claude Desktop template, then spot-check that it includes the site’s REST URL.
8. Click **Test connection** for each server. When authenticated, the notice should report the HTTP status code (401 is acceptable if Application Password headers weren’t supplied). If HTTPS fails locally, the fallback should retry with HTTP.
9. Create an Application Password for your admin user, launch `wp mcp-adapter serve --server=<server-id>`, and paste one of the generated templates into Claude Desktop or Cursor to confirm MCP clients can connect end-to-end.
1 change: 1 addition & 0 deletions includes/Experiment_Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ private function get_default_experiments(): array {
\WordPress\AI\Experiments\Image_Generation\Image_Generation::class,
\WordPress\AI\Experiments\Title_Generation\Title_Generation::class,
\WordPress\AI\Experiments\Excerpt_Generation\Excerpt_Generation::class,
\WordPress\AI\Experiments\MCP\MCP::class,
);

/**
Expand Down
146 changes: 146 additions & 0 deletions includes/Experiments/MCP/Admin_Page.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php
/**
* Admin page for MCP experiment.
*
* @package WordPress\AI\Experiments\MCP
*/

declare( strict_types=1 );

namespace WordPress\AI\Experiments\MCP;

use WordPress\AI\Asset_Loader;

use function __;
use function add_action;
use function admin_url;
use function current_user_can;
use function esc_html_e;
use function esc_url_raw;
use function rest_url;
use function wp_create_nonce;

/**
* Renders the MCP admin interface.
*
* @since 0.1.0
*/
class Admin_Page {

private const PAGE_SLUG = 'ai-mcp';
private const MENU_ICON = 'data:image/svg+xml;base64,PHN2ZyBmaWxsPSJjdXJyZW50Q29sb3IiIGZpbGwtcnVsZT0iZXZlbm9kZCIgaGVpZ2h0PSIxZW0iIHN0eWxlPSJmbGV4Om5vbmU7bGluZS1oZWlnaHQ6MSIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMWVtIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjx0aXRsZT5Nb2RlbENvbnRleHRQcm90b2NvbDwvdGl0bGU+PHBhdGggZD0iTTE1LjY4OCAyLjM0M2EyLjU4OCAyLjU4OCAwIDAwLTMuNjEgMGwtOS42MjYgOS40NGEuODYzLjg2MyAwIDAxLTEuMjAzIDAgLjgyMy44MjMgMCAwMTAtMS4xOGw5LjYyNi05LjQ0YTQuMzEzIDQuMzEzIDAgMDE2LjAxNiAwIDQuMTE2IDQuMTE2IDAgMDExLjIwNCAzLjU0IDQuMyA0LjMgMCAwMTMuNjA5IDEuMThsLjA1LjA1YTQuMTE1IDQuMTE1IDAgMDEwIDUuOWwtOC43MDYgOC41MzdhLjI3NC4yNzQgMCAwMDAgLjM5M2wxLjc4OCAxLjc1NGEuODIzLjgyMyAwIDAxMCAxLjE4Ljg2My44NjMgMCAwMS0xLjIwMyAwbC0xLjc4OC0xLjc1M2ExLjkyIDEuOTIgMCAwMTAtMi43NTRsOC43MDYtOC41MzhhMi40NyAyLjQ3IDAgMDAwLTMuNTRsLS4wNS0uMDQ5YTIuNTg4IDIuNTg4IDAgMDAtMy42MDctLjAwM2wtNy4xNzIgNy4wMzQtLjAwMi4wMDItLjA5OC4wOTdhLjg2My44NjMgMCAwMS0xLjIwNCAwIC44MjMuODIzIDAgMDEwLTEuMThsNy4yNzMtNy4xMzNhMi40NyAyLjQ3IDAgMDAtLjAwMy0zLjUzN3oiPjwvcGF0aD48cGF0aCBkPSJNMTQuNDg1IDQuNzAzYS44MjMuODIzIDAgMDAwLTEuMTguODYzLjg2MyAwIDAwLTEuMjA0IDBsLTcuMTE5IDYuOTgyYTQuMTE1IDQuMTE1IDAgMDAwIDUuOSA0LjMxNCA0LjMxNCAwIDAwNi4wMTYgMGw3LjEyLTYuOTgyYS44MjMuODIzIDAgMDAwLTEuMTguODYzLjg2MyAwIDAwLTEuMjA0IDBsLTcuMTE5IDYuOTgyYTIuNTg4IDIuNTg4IDAgMDEtMy42MSAwIDIuNDcgMi40NyAwIDAxMC0zLjU0bDcuMTItNi45ODJ6Ij48L3BhdGg+PC9zdmc+';

/**
* Manager instance.
*
* @var \WordPress\AI\Experiments\MCP\Manager
*/
private Manager $manager;

Check failure on line 38 in includes/Experiments/MCP/Admin_Page.php

View workflow job for this annotation

GitHub Actions / Run PHP static analysis

Property WordPress\AI\Experiments\MCP\Admin_Page::$manager is never read, only written.

/**
* Constructor.
*
* @param \WordPress\AI\Experiments\MCP\Manager $manager Manager instance.
*/
public function __construct( Manager $manager ) {
$this->manager = $manager;
}

/**
* Hook admin actions.
*/
public function init(): void {
add_action( 'admin_menu', array( $this, 'register_menu' ) );
}

/**
* Register the options page.
*/
public function register_menu(): void {
$page_hook = add_menu_page(
__( 'MCP', 'ai' ),
__( 'MCP', 'ai' ),
'manage_options',
self::PAGE_SLUG,
array( $this, 'render_page' ),
self::MENU_ICON,
58
);

if ( ! $page_hook ) {
return;
}

add_action( "load-{$page_hook}", array( $this, 'on_load' ) );
}

/**
* Enqueue assets.
*/
public function on_load(): void {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}

/**
* Render.
*/
public function render_page(): void {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap ai-mcp-server">
<div class="ai-admin-header ai-mcp-server__header">
<div class="ai-admin-header__inner">
<div class="ai-admin-header__left">
<span class="ai-admin-header__icon">
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg">
<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
</svg>
</span>
<div class="ai-admin-header__title">
<h1><?php esc_html_e( 'MCP', 'ai' ); ?></h1>
</div>
<!-- React mounts server selector here -->
<div id="ai-mcp-header-server-selector"></div>
<!-- React mounts status badge here -->
<div id="ai-mcp-header-status"></div>
</div>
<!-- React mounts header controls (global toggle, server toggle, add button) -->
<div id="ai-mcp-header-controls" class="ai-admin-header__right"></div>
</div>
</div>
<div id="ai-mcp-server-root"></div>
</div>
<?php
}

/**
* Enqueue JS/CSS bundle.
*/
public function enqueue_assets(): void {
Asset_Loader::enqueue_script( 'mcp_server', 'admin/mcp-server' );
Asset_Loader::enqueue_style( 'mcp_server', 'admin/style-mcp-server' );

Asset_Loader::localize_script(
'mcp_server',
'McpServerSettings',
array(
'rest' => array(
'nonce' => wp_create_nonce( 'wp_rest' ),
'root' => esc_url_raw( rest_url() ),
'routes' => array(
'overview' => 'ai/v1/mcp',
'enabled' => 'ai/v1/mcp/enabled',
'server' => 'ai/v1/mcp/server',
'addServer' => 'ai/v1/mcp/server/add',
'tools' => 'ai/v1/mcp/tools',
'test' => 'ai/v1/mcp/test',
),
),
'profileUrl' => esc_url_raw( admin_url( 'profile.php#application-passwords-section' ) ),
)
);
}
}
65 changes: 65 additions & 0 deletions includes/Experiments/MCP/MCP.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php
/**
* MCP experiment entry point.
*
* @package WordPress\AI\Experiments\MCP
*/

declare( strict_types=1 );

namespace WordPress\AI\Experiments\MCP;

use WordPress\AI\Abstracts\Abstract_Experiment;

use function __;
use function admin_url;
use function is_admin;

/**
* Registers the MCP experiment.
*
* @since 0.1.0
*/
class MCP extends Abstract_Experiment {

private Manager $manager;

/**
* {@inheritDoc}
*/
protected function load_experiment_metadata(): array {
return array(
'id' => 'mcp',
'label' => __( 'MCP', 'ai' ),
'description' => __( 'Manage Model Context Protocol servers and client access.', 'ai' ),
);
}

/**
* {@inheritDoc}
*/
public function register(): void {
$this->manager = new Manager();
$this->manager->init();

if ( ! is_admin() ) {
return;
}

$page = new Admin_Page( $this->manager );
$page->init();
}

/**
* {@inheritDoc}
*/
public function get_entry_points(): array {

Check failure on line 56 in includes/Experiments/MCP/MCP.php

View workflow job for this annotation

GitHub Actions / Run PHP static analysis

Method WordPress\AI\Experiments\MCP\MCP::get_entry_points() return type has no value type specified in iterable type array.
return array(
array(
'label' => __( 'Dashboard', 'ai' ),
'url' => admin_url( 'admin.php?page=ai-mcp' ),
'type' => 'dashboard',
),
);
}
}
Loading
Loading