Repository navigation
Add WebMCP experiment #1081
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
webmyc
wants to merge
9
commits into
WordPress:develop
Choose a base branch
from
webmyc:add/webmcp-experiment
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add WebMCP experiment #1081
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
45595c7
Add WebMCP experiment
webmyc a07242e
WebMCP: satisfy static analysis, and test the enqueue paths
webmyc b1c05fd
WebMCP: drive the block editor on the page, not the Abilities API
webmyc 95d1acd
WebMCP: structural editor tools, and the editor's own rules on insert
webmyc 7faab23
WebMCP: respect editor locks and report save failures
webmyc 0f33ba4
WebMCP: stricter publish, transform, duplicate and lock handling
webmyc da009a7
WebMCP: respect post and save locks, reject templateLock, catch parti…
webmyc 07892d8
Add more robust checks that a block can be edited. When transforming …
dkotter 58ec101
Merge branch 'develop' into add/webmcp-experiment
dkotter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # WebMCP | ||
|
|
||
| ## Summary | ||
|
|
||
| The WebMCP experiment lets an agent browser (ChatGPT's in-app browser, Chrome builds with WebMCP behind a flag) work in the block editor. On the post editor screens it registers fourteen tools on `document.modelContext`, and every tool acts on the page the person is looking at, through the editor's own data stores: the title changes, the block appears, the post saves, in front of them, and they can stop at any point. | ||
|
|
||
| This is deliberately not a second door to the Abilities API. A site that wants server-side abilities in an agent connects it over MCP. WebMCP is for the page. | ||
|
|
||
| ## Overview | ||
|
|
||
| When enabled, on `post.php` and `post-new.php` the experiment enqueues a bridge script. The bridge: | ||
|
|
||
| 1. collects tools from its registry (the built-in editor tools, plus anything a plugin adds), | ||
| 2. runs them through the `wpai.webmcp.tools` filter, | ||
| 3. registers each one with `document.modelContext.registerTool()`, one call per tool, up to the per-page cap. | ||
|
|
||
| Each tool's `execute` runs in the page and dispatches into `core/editor` and `core/block-editor`, the same stores the editor's own UI uses, so the result is visible immediately and lands in the post's undo history. | ||
|
|
||
| ## Editor tools | ||
|
|
||
| | Tool | What the person sees | | ||
| | --- | --- | | ||
| | `editor-get-document` | Nothing changes. Returns the post ID, type, status, title, and an outline of the blocks with their `clientId`, block name and a short text preview, so the agent can refer to a block precisely. | | ||
| | `editor-set-title` | The title field updates. | | ||
| | `editor-insert-block` | A new block appears, selected. Defaults to a paragraph; takes a block name, attributes, and either `afterClientId` (place it after a block) or `parentClientId` (place it inside a container). Refuses a block the editor does not allow there. | | ||
| | `editor-update-block-text` | The text of a paragraph, heading, list item, verse, preformatted or code block is replaced; the block is selected. For a quote, edit its inner paragraphs. For a pullquote, set its `value` through `editor-update-block-attributes`. | | ||
| | `editor-update-block-attributes` | Any attributes of a block change; the block is selected. | | ||
| | `editor-remove-block` | The block disappears. | | ||
| | `editor-move-block` | The block moves: after another block, into a container, or to the top of its parent. | | ||
| | `editor-duplicate-block` | A copy appears directly after the original. | | ||
| | `editor-transform-block` | The block becomes another type, through the same transforms the editor's own menu offers. | | ||
| | `editor-select-block` | The block is highlighted, for the agent to point at something before asking. | | ||
| | `editor-get-block-types` | Nothing changes. Lists what the editor allows at a position, or returns one block type's attributes, so the agent sets attributes that exist. | | ||
| | `editor-undo` | The last change is undone, exactly like the editor's Undo button. Each tool call that changed something is one undo step. | | ||
| | `editor-save` | The post saves (draft stays draft). | | ||
| | `editor-publish` | The post's status changes to published and it saves. Annotated as not read-only so an agent asks first. | | ||
|
|
||
| Tool descriptions are written for the model, in English, and are not translated. | ||
|
|
||
| Editor tools register only on an initialized block editor page, not in the classic editor. Updates, moves and removals respect the editor's lock selectors. Moving a block after itself leaves it unchanged; moving it into itself or a descendant is refused. Save and publish check the editor's save failure state and report an error when saving fails. | ||
|
|
||
| ## Adding tools from a plugin or another screen | ||
|
|
||
| The bridge exposes a registry on `window.wpai.webmcp`: | ||
|
|
||
| ```js | ||
| wpai.webmcp.registerTool( { | ||
| name: 'woo-add-to-cart', | ||
| description: 'Adds the product on the current page to the cart. The cart count updates on the page.', | ||
| inputSchema: { type: 'object', properties: { quantity: { type: 'integer' } } }, | ||
| annotations: { readOnlyHint: false }, | ||
| execute: async ( { quantity = 1 } ) => { | ||
| // act on the page, then return text content | ||
| return { content: [ { type: 'text', text: `Added ${ quantity }.` } ] }; | ||
| }, | ||
| } ); | ||
| ``` | ||
|
|
||
| Register before `DOMContentLoaded` finishes, or call `wpai.webmcp.refresh()` afterwards. The `wpai.webmcp.tools` filter (`@wordpress/hooks`) receives the full list and the screen name and can remove or reorder tools. | ||
|
|
||
| To load the bridge on another admin screen, add its hook suffix through the PHP filter: | ||
|
|
||
| ```php | ||
| add_filter( 'wpai_webmcp_screens', fn( array $screens ) => array_merge( $screens, array( 'edit.php' ) ) ); | ||
| ``` | ||
|
|
||
| The bridge only ships editor tools; a screen added this way needs its own. | ||
|
|
||
| ## The per-page cap | ||
|
|
||
| Agent browsers cap the tools a page may register. Registering a few hundred disabled WebMCP for the document with no error in testing, while about thirty worked. The bridge registers at most 30 tools, filterable through `wpai_webmcp_max_tools`, and logs the ones it dropped to the console. | ||
|
|
||
| ## Testing | ||
|
|
||
| - `npm run test:php -- --filter WebMCP` covers the PHP side. | ||
| - `tests/e2e/specs/experiments/webmcp.spec.js` installs a `document.modelContext` shim before the editor loads, calls the tools the way a browser would, and asserts that the title and the canvas change. | ||
| - In an agent browser, enable the experiment, open a post, and ask the agent to give the post a title and add a paragraph. Both should appear in the editor as it works. | ||
|
|
||
| ## Prior art | ||
|
|
||
| This follows the direction set in [#448](https://github.com/WordPress/ai/issues/448), where the maintainers pointed out that WebMCP is for driving the UI on the current page rather than for exposing server-side abilities a second time. [#224](https://github.com/WordPress/ai/pull/224) remains as prior art. | ||
|
|
||
| The structural tools (insert into a container, move, duplicate, transform, discovery of block types, undo) follow what [Block MCP](https://github.com/GravityKit/block-mcp) established for atomic block editing outside the browser: an agent needs stable references, structural operations and a way to discover what a position allows. Inside the editor those come from the editor's own stores: `clientId` is the stable reference for the session, `canInsertBlockType` enforces the site's and the template's rules, attribute changes re-render through the block's own save function, and every tool call is one undo step. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| <?php | ||
| /** | ||
| * WebMCP experiment. | ||
| * | ||
| * @package WordPress\AI | ||
| */ | ||
|
|
||
| declare( strict_types=1 ); | ||
|
|
||
| namespace WordPress\AI\Experiments\WebMCP; | ||
|
|
||
| use WordPress\AI\Abstracts\Abstract_Feature; | ||
| use WordPress\AI\Asset_Loader; | ||
| use WordPress\AI\Experiments\Experiment_Category; | ||
|
|
||
| if ( ! defined( 'ABSPATH' ) ) { | ||
| exit; | ||
| } | ||
|
|
||
| /** | ||
| * Lets an agent browser drive the block editor through WebMCP. | ||
| * | ||
| * On the post editor screens the experiment loads a bridge that registers | ||
| * a small set of tools on `document.modelContext`, one `registerTool` call | ||
| * per tool. Every tool acts on the page the person is looking at, through | ||
| * the editor's own data stores, so the title changes, the block appears and | ||
| * the post saves in front of them. Nothing is exposed that has no visible | ||
| * effect on the current page; a site that wants server-side abilities in an | ||
| * agent uses MCP. | ||
| * | ||
| * Other screens and plugins can add their own page tools through the | ||
| * bridge's JavaScript registry (`wpai.webmcp.registerTool()`) and the | ||
| * `wpai.webmcp.tools` filter. The bridge caps how many tools one page | ||
| * registers, because agent browsers cap it too. | ||
| * | ||
| * @since x.x.x | ||
| */ | ||
| class WebMCP extends Abstract_Feature { | ||
|
|
||
| /** | ||
| * Script handle suffix used with Asset_Loader (the loader prefixes it with `ai_`). | ||
| * | ||
| * @since x.x.x | ||
| */ | ||
| public const SCRIPT_HANDLE = 'webmcp'; | ||
|
|
||
| /** | ||
| * Default cap on tools registered on one page. | ||
| * | ||
| * Agent browsers impose a per-page budget; registering a few hundred tools | ||
| * disabled WebMCP for the document with no error in testing, while about | ||
| * thirty worked. | ||
| * | ||
| * @since x.x.x | ||
| */ | ||
| public const DEFAULT_MAX_TOOLS = 30; | ||
|
|
||
| /** | ||
| * {@inheritDoc} | ||
| */ | ||
| public static function get_id(): string { | ||
| return 'webmcp'; | ||
| } | ||
|
|
||
| /** | ||
| * {@inheritDoc} | ||
| */ | ||
| protected function load_metadata(): array { | ||
| return array( | ||
| 'label' => __( 'WebMCP', 'ai' ), | ||
| 'description' => __( 'Lets an agent browser work in the block editor through WebMCP: set the title, insert and edit blocks, save and publish, with every change visible on the page as it happens.', 'ai' ), | ||
| 'category' => Experiment_Category::EDITOR, | ||
| 'capability' => 'none', | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * {@inheritDoc} | ||
| */ | ||
| public function register(): void { | ||
| add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); | ||
| } | ||
|
|
||
| /** | ||
| * Admin screens the bridge loads on. | ||
| * | ||
| * @since x.x.x | ||
| * | ||
| * @return list<string> Hook suffixes. | ||
| */ | ||
| public function get_screens(): array { | ||
| /** | ||
| * Filters the admin screens (hook suffixes) the WebMCP bridge loads on. | ||
| * | ||
| * The editor tools only work where the editor stores exist. A screen | ||
| * added here should register its own tools through the JavaScript | ||
| * registry, or the bridge will have nothing to register. | ||
| * | ||
| * @since x.x.x | ||
| * | ||
| * @param list<string> $screens Hook suffixes. Default the post editor screens. | ||
| */ | ||
| $defaults = array( 'post.php', 'post-new.php' ); | ||
| $screens = apply_filters( 'wpai_webmcp_screens', $defaults ); | ||
|
|
||
| // A filter that returns something other than an array is a mistake; | ||
| // keep the default screens rather than fail on every admin page. | ||
| if ( ! is_array( $screens ) ) { | ||
| return $defaults; | ||
| } | ||
|
|
||
| return array_values( array_filter( $screens, 'is_string' ) ); | ||
| } | ||
|
|
||
| /** | ||
| * Cap on tools registered per page. | ||
| * | ||
| * @since x.x.x | ||
| * | ||
| * @return int Cap, at least 1. | ||
| */ | ||
| public function get_max_tools(): int { | ||
| /** | ||
| * Filters how many tools one page may register with the browser. | ||
| * | ||
| * @since x.x.x | ||
| * | ||
| * @param int $max_tools Cap. Default 30. | ||
| */ | ||
| $max_tools = (int) apply_filters( 'wpai_webmcp_max_tools', self::DEFAULT_MAX_TOOLS ); | ||
|
|
||
| return max( 1, $max_tools ); | ||
| } | ||
|
|
||
| /** | ||
| * Loads the bridge on the editor screens. | ||
| * | ||
| * @since x.x.x | ||
| * | ||
| * @param string $hook_suffix Current admin page hook suffix. | ||
| */ | ||
| public function enqueue_assets( string $hook_suffix ): void { | ||
| if ( ! in_array( $hook_suffix, $this->get_screens(), true ) ) { | ||
| return; | ||
| } | ||
|
|
||
| Asset_Loader::enqueue_script( self::SCRIPT_HANDLE, 'experiments/webmcp' ); | ||
| Asset_Loader::localize_script( | ||
| self::SCRIPT_HANDLE, | ||
| 'WebMCP', | ||
| array( | ||
| 'screen' => $hook_suffix, | ||
| 'maxTools' => $this->get_max_tools(), | ||
| ) | ||
| ); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.