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
112 changes: 112 additions & 0 deletions docs/experiments/webmcp-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# WebMCP Adapter

## Summary

The WebMCP Adapter experiment exposes WordPress abilities to browser agents through `navigator.modelContext` using a layered tool surface:

- `wp-discover-abilities`
- `wp-get-ability-info`
- `wp-execute-ability`

It runs in wp-admin and executes abilities through the Abilities client API (`@wordpress/abilities` or `wp.abilities`), so server-side permission callbacks and REST auth still remain authoritative.

## Exposure Model

An ability is exposed to agents only when:

1. `meta.mcp.public === true`
2. Optional `meta.mcp.context` rules match the current WordPress page context.

Supported context rules:

- `screens`: matches `pagenow` (for example `post.php`, `site-editor.php`)
- `adminPages`: matches `adminpage`
- `postTypes`: matches `typenow` (or `post_type` query var fallback)
- `query`: exact query-var matching

## Confirmation Policy

Execution confirmation uses ability annotations:

- `meta.annotations.readonly === true` -> no extra confirmation
- `meta.annotations.destructive === true` -> double confirmation
- otherwise -> standard confirmation prompt

Prompts are requested through `agent.requestUserInteraction()` when available.

## Debug Panel

The experiment includes an optional in-page debug panel to verify WebMCP registration and run quick tool calls.

Enablement options:

- **Experiment setting:** `Enable WebMCP debug panel` in `Settings -> AI Experiments`
- **Runtime JS flag:** `window.aiWebMCPDebug = true` (or `false` to disable)

Advanced runtime config:

```js
window.aiWebMCPDebug = {
enabled: true,
open: true,
shimModelContext: true, // Optional: auto-install local modelContext shim for browser testing
};
```

The adapter also exposes a small runtime API:

```js
window.aiWebMCPAdapterDebug.enable();
window.aiWebMCPAdapterDebug.disable();
window.aiWebMCPAdapterDebug.refresh();
window.aiWebMCPAdapterDebug.getState();
window.aiWebMCPAdapterDebug.register( { forceReloadAbilities: true } );
window.aiWebMCPAdapterDebug.installModelContextShim();
window.aiWebMCPAdapterDebug.callTool( 'discover' );
```

## Hooks

### `ai_webmcp_adapter_allowed_hooks`

Filters admin hooks where the adapter is enqueued.

Default hooks:

- `post.php`
- `post-new.php`
- `site-editor.php`
- `appearance_page_gutenberg-edit-site`
- `admin_page_gutenberg-edit-site`
- `appearance_page_site-editor-v2`

### `ai_webmcp_adapter_tool_names`

Filters layered tool names.

Default:

- `discover`: `wp-discover-abilities`
- `info`: `wp-get-ability-info`
- `execute`: `wp-execute-ability`

### `ai.webmcp.isAbilityExposed` (JS filter)

Client-side filter (via `@wordpress/hooks`) to override default exposure decisions.

Arguments:

1. `isExposed` (`boolean`)
2. `ability` (`Ability`)
3. `wpContext` (`{ screen, adminPage, postType, query }`)

## Requirements

- Browser must support `navigator.modelContext` to register tools.
- The Gutenberg plugin must be active (WebMCP availability is gated on Gutenberg because it provides the Abilities API integration this experiment relies on).
- WordPress abilities API must be available, either via:
- `window.wp.abilities` globals, or
- script modules (`@wordpress/core-abilities` + `@wordpress/abilities`).
- If dependencies are unavailable, the WebMCP toggle is disabled in settings and forced off server-side on save.

For browsers without native WebMCP support, you can enable the local debug shim with `window.aiWebMCPDebug = { enabled: true, shimModelContext: true }` or `window.aiWebMCPAdapterDebug.installModelContextShim()`.
33 changes: 33 additions & 0 deletions includes/Abstracts/Abstract_Experiment.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,33 @@ public function get_description(): string {
return $this->description;
}

/**
* Checks if experiment can currently be enabled.
*
* Child classes can override this to enforce runtime dependencies
* (for example required plugins or APIs).
*
* @since 0.4.0
*
* @return bool True when available, false otherwise.
*/
public function is_available(): bool {
return true;
}

/**
* Gets a human-readable reason for why the experiment is unavailable.
*
* Child classes can override this to provide contextual guidance in UI.
*
* @since 0.4.0
*
* @return string Availability message or an empty string when not provided.
*/
public function get_unavailable_reason(): string {
return '';
}

/**
* Checks if experiment is enabled.
*
Expand All @@ -148,6 +175,12 @@ final public function is_enabled(): bool {
return $this->enabled_cache;
}

// Experiments cannot be enabled when runtime dependencies are unavailable.
if ( ! $this->is_available() ) {
$this->enabled_cache = false;
return false;
}

// Check global experiments toggle first.
$global_enabled = (bool) get_option( Settings_Registration::GLOBAL_OPTION, false );
if ( ! $global_enabled ) {
Expand Down
1 change: 1 addition & 0 deletions includes/Experiment_Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
*
* @param \WordPress\AI\Experiment_Registry $registry The experiment registry instance.
*/
do_action( 'ai_experiments_register_experiments', $this->registry );

Check warning on line 94 in includes/Experiment_Loader.php

View workflow job for this annotation

GitHub Actions / Run Plugin Check

WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

Hook names invoked by a theme/plugin should start with the theme/plugin prefix. Found: "ai_experiments_register_experiments".
}

/**
Expand All @@ -110,6 +110,7 @@
\WordPress\AI\Experiments\Image_Generation\Image_Generation::class,
\WordPress\AI\Experiments\Summarization\Summarization::class,
\WordPress\AI\Experiments\Title_Generation\Title_Generation::class,
\WordPress\AI\Experiments\WebMCP\WebMCP::class,
);

/**
Expand All @@ -122,7 +123,7 @@
*
* @param array $experiment_classes Array of experiment class names or instances.
*/
$items = apply_filters( 'ai_experiments_default_experiment_classes', $experiment_classes );

Check warning on line 126 in includes/Experiment_Loader.php

View workflow job for this annotation

GitHub Actions / Run Plugin Check

WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

Hook names invoked by a theme/plugin should start with the theme/plugin prefix. Found: "ai_experiments_default_experiment_classes".

$experiments = array();
foreach ( $items as $item ) {
Expand Down Expand Up @@ -193,7 +194,7 @@
*
* @param bool $enabled Whether to enable AI experiments.
*/
$experiments_enabled = apply_filters( 'ai_experiments_enabled', true );

Check warning on line 197 in includes/Experiment_Loader.php

View workflow job for this annotation

GitHub Actions / Run Plugin Check

WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

Hook names invoked by a theme/plugin should start with the theme/plugin prefix. Found: "ai_experiments_enabled".

if ( ! $experiments_enabled ) {
$this->initialized = true;
Expand All @@ -215,7 +216,7 @@
*
* @since 0.1.0
*/
do_action( 'ai_experiments_initialized' );

Check warning on line 219 in includes/Experiment_Loader.php

View workflow job for this annotation

GitHub Actions / Run Plugin Check

WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

Hook names invoked by a theme/plugin should start with the theme/plugin prefix. Found: "ai_experiments_initialized".

$this->initialized = true;
}
Expand Down
Loading
Loading