Install
Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/wpscholar/agentic-editor/releases/download/0.1.0/agentic-editor.zipWordPress plugin that registers client-side block editor abilities via @wordpress/abilities, exposes them to browser AI agents through WebMCP, and ships a chat panel that drives those tools using the site's own AI connector.
Requires WordPress 7.0+ (client-side Abilities API and AI Client) and PHP 8.0+.
What it does
On block editor screens the plugin:
- Registers a
block-editorability category - Registers twenty-one editor abilities (inspect / query / mutate the live editor), plus
editor/generate-imagewhen the site's AI connector can generate images - Bridges each ability to
document.modelContext.registerTool(), installing the WebMCP polyfill when the browser has no native support - Adds an AI Chat sidebar that can call those tools
| Ability | WebMCP tool name | Purpose |
|---|---|---|
editor/get-editor-tree |
editor_get-editor-tree |
Full hierarchical block tree (optional maxDepth) |
editor/find-editor-blocks |
editor_find-editor-blocks |
Find blocks by text / name / attribute; returns flat summaries |
editor/get-block-location |
editor_get-block-location |
Parents, root, and index for a block |
editor/insert-block |
editor_insert-block |
Insert a block, with nested innerBlocks (optional parent / after) |
editor/move-block |
editor_move-block |
Move a block within or between parents |
editor/update-block |
editor_update-block |
Merge attribute changes into a block |
editor/transform-block |
editor_transform-block |
Convert a block to another block type in place |
editor/remove-block |
editor_remove-block |
Remove a block and everything nested inside it |
editor/get-editor-selection |
editor_get-editor-selection |
Current selection state |
editor/select-block |
editor_select-block |
Select a block, without touching the document |
editor/can-insert-block |
editor_can-insert-block |
Whether a block type can be inserted |
editor/get-block-types |
editor_get-block-types |
List registered block types, filtered by search / category / insertability |
editor/get-block-type |
editor_get-block-type |
One block type in full: attribute schema, nesting rules, styles, variations |
editor/undo |
editor_undo |
Undo the last change to the document |
editor/redo |
editor_redo |
Redo the last undone change |
editor/get-patterns |
editor_get-patterns |
List available patterns, filtered by search / category / block types / destination |
editor/get-pattern |
editor_get-pattern |
One pattern as a block tree, optionally with its markup |
editor/get-pattern-categories |
editor_get-pattern-categories |
Pattern categories, registered and user-created |
editor/insert-pattern |
editor_insert-pattern |
Insert a pattern at a location |
editor/create-pattern |
editor_create-pattern |
Save blocks as a new pattern on this site |
editor/search-media |
editor_search-media |
Find files already in the Media Library by title, alt text, caption or file name |
editor/generate-image |
editor_generate-image |
Generate an image with the site's AI connector and add it to the Media Library (only when the connector supports image generation) |
Ability names keep the namespace/name form. WebMCP tool names replace / with _ (some agents reject / in tool names).
Behavior worth knowing when calling these:
- Unknown client IDs are an error, never a silent no-op. Passing a stale
afterClientIdtoeditor/insert-blockfails instead of inserting at the top of the document. editor/find-editor-blocksreturns each match once as{ clientId, name, attributes, innerBlockCount }, so a match nested inside another match is not duplicated. A suppliedclientIdscopes the search and includes that block itself.searchis the way to find a block by the words shown in the editor: it is a case-insensitive substring match over the block's string attributes, and it also matches with markup and common HTML entities resolved, soChloefinds<strong>Chloe Nolan</strong>. Avaluepassed without anattributeis treated assearchrather than matching every block.attributematches on presence; addvalueto compare, which is done as a string (objects and arrays compare as JSON).editor/move-blocktakesafterClientId/beforeClientId(the sibling's parent becomes the destination) or an explicitrootClientId+index. Indexes are the block's position after the move. Moving a block into itself or a descendant is an error, as is a move the editor refuses because of a lock: the block's own move lock, a parent's template lock, or a remove lock when the move would take it out of its parent (it can still be reordered there).editor/insert-blocktakesinnerBlocks(recursive{ name, attributes, innerBlocks }), and container blocks have to be built that way. An emptycore/columnsrenders a layout placeholder rather than an inner block list, so the editor registers no block list settings for it and refuses every child until it has inner blocks — a two-column layout must be inserted as onecore/columnsholding twocore/columnblocks. Nesting the block types forbid fails before anything is inserted: aparentrule (core/columnoutsidecore/columns), a container'sallowedBlocks, or anancestorrule, judged against where the blocks will actually sit in the document (core/comment-author-nameanywhere outsidecore/comment-template).editor/update-blockmerges the attributes you pass; anything you omit is left alone. Object attributes such asstylemerge at every depth, so{ style: { color: { text: '#f00' } } }keeps the block's existing typography and spacing, and a nestednullremoves that key. Arrays and other values are replaced whole. Attribute keys the block type does not define are rejected with the list of keys it accepts, since unknown keys are stored but never saved. It holds the agent to the same locks a person is held to:metadata,lockandtemplateLockcannot be set (here or througheditor/insert-blockandeditor/create-pattern), a block indisabledediting mode (such as the inside of a synced pattern) cannot be updated at all, and one incontentOnlymode accepts only the attributes its block type marks as content.- Attribute values are checked against the shape the block type declares, and defaults nested inside
querysources are filled in. Those defaults are otherwise only applied while parsing saved markup, so acore/tablecell set programmatically without itstagwould render an undefined element and break the block. editor/get-block-typesis how to discover blocks the theme or a plugin registered, which no model knows in advance. It omits attribute schemas to stay small;editor/get-block-typereturns one block in full, including the style variations and theis-style-*class name that applies each, and block variations whoseinnerBlocksare in the{ name, attributes, innerBlocks }shapeeditor/insert-blocktakes. Blocks hidden from the inserter are excluded unlessincludeHiddenis set, and passingrootClientIdnarrows the list to what that block will actually accept.editor/remove-blockreportsremovedInnerBlockCount: every block removed along with it, at any depth, including what a synced pattern reference was showing.editor/can-insert-blocktreats an unknown block name as an error rather than a block that does not fit.editor/transform-blockuses the block type's own registered transforms, so it keeps content that a remove-then-insert would lose. A refused target comes back with the list of types the block can become, and one transform can produce several blocks (a list becomes one paragraph per item).editor/undoandeditor/redodrive the editor's history, so a person can also step through the agent's work with the toolbar buttons. Each editing ability lands as its own undo step; there is no batching yet, so reverting a five-call edit takes five undos.- Ability failures come back as MCP tool errors with a readable message rather than rejecting the tool call.
Patterns
editor/get-patternsis the pattern counterpart ofeditor/get-block-types: it answers "what layouts does this site already have" before an agent assembles one block at a time. It lists registered patterns (core, theme, plugin, pattern directory) alongside the patterns saved on this site, and omits markup so the list stays small. Each entry carriesrootBlockNamesandblockCount, so a pattern can be judged without fetching it.- Pattern names keep the form the editor uses. Registered patterns are named by their author (
twentytwentyfive/hero); a pattern saved on this site iscore/block/<id>, after thecore/blockblock that references it. - Passing
rootClientIdtoeditor/get-patternsnarrows the list to patterns whose top-level blocks the destination will actually accept, which is how to avoid offering a template-part pattern inside a post. editor/get-patternreturns blocks as{ name, attributes, innerBlocks }— the shapeeditor/insert-blockandeditor/create-patternaccept, and deliberately without client IDs, since none of those blocks are in the document.editor/insert-patterncopies an unsynced or registered pattern in as ordinary blocks, and inserts a synced pattern as a singlecore/blockreference, which is what the editor does. PassasReference: falseto copy a synced pattern's blocks in as an independent, editable set instead.asReference: trueis refused for unsynced and registered patterns, since a reference would make an unsynced pattern behave as a synced one. Every top-level block is checked against the destination first, so a pattern that does not fit fails before anything is inserted.editor/create-patternsaves either blocks already in the document (clientIds) or a structure supplied directly (blocks). Sync status follows core:unsyncedwrites thewp_pattern_sync_statusmeta and inserts independent copies,syncedomits it and keeps every instance in step. A category with no term behind it yet gets one created, the same as the editor does, and the response reports which were created. A category named twice, or by both its slug and its label, is filed once, and categories created for a pattern that then fails to save are deleted again. The pattern and any new categories are published immediately, independently of the post being edited, and editor undo does not remove them, which is why the ability is annotated as destructive.replaceSource: trueswaps the source blocks for a reference to the new synced pattern, which is the editor's own "Create pattern" behavior. It needssyncStatus: 'synced'and blocks that sit next to each other under one parent.- Creating anything requires an account that may create
wp_blockposts; that is checked up front so the failure reads as a permission problem rather than a REST error. - Pattern data is fetched over REST, so the first pattern call on a page load waits on that request. Later calls are served from the store.
Media
editor/search-mediafinds files already in the Media Library, newest first; it looks for images unlessmediaTypesays otherwise. Core's attachment search only looks at the title, caption and description, so the ability sends anagentic_editor_searchflag, andincludes/media-search.phpwidens that one query to alt text and file names too. A photo titledIMG_1234with the alt text "Lighthouse at dusk" is found by "lighthouse". Every search word must match somewhere. Other media queries on the site are unaffected.- Each image result carries the same
blockAttributesas a generated image, so placing a library image and placing a generated one are the same next step. Titles, alt text and captions are written by people, so results are marked as untrusted content. - Searching needs no approval and is always available. Generating is only for a new image: the tool descriptions and the system instruction steer "an image from the media library" to search, and tell the model to report an empty search rather than generate unasked.
editor/generate-imageis registered only when a connector on the site can generate images and the user may upload files. PHP asks the AI Client (is_supported_for_image_generation(), which makes no request to a provider) and passes the answer to the page. On a site without an image model the tool is simply not there, so the model cannot reach for it and fail.- The AI Client runs only in PHP, so the ability calls
POST /wp-json/agentic-editor/v1/image. The endpoint generates one image, sideloads it into the Media Library (attached to the post being edited, when the user may edit it), sets its alt text, and records the prompt in the attachment's description and in_agentic_editor_image_promptmeta. It returns a local attachment, never a provider's URL. - The ability does not place the image. It returns
blockAttributesfor acore/imageblock (id,url,alt,sizeSlug,linkDestination), which the model passes toeditor/insert-block, or theidandurlit passes toeditor/update-blockfor an existing image, cover or media & text block. Placement is therefore an ordinary editor change that undo reverts; the attachment is not. orientationislandscape(the default),portraitorsquare. Which model draws the image is up to the connectors;agentic_editor_image_model_preferencecan name preferred models.- Generation is billed and the attachment outlives undo, so the chat asks for approval before each call. It also waits up to three minutes for the result rather than the usual thirty seconds.
- The system instruction tells the model never to use an image URL it found or made up, so on a site without image generation it says so instead of hot-linking something.
Synced patterns in the tree
A synced pattern (core/block) owns its content as a separate entity, so editor/get-editor-tree, editor/find-editor-blocks, and editor/get-editor-selection reach into it through the editor's controlled-inner-block plumbing rather than the block itself, and mark the block with controlledInnerBlocks: true. Blocks below that marker are shared: editing one changes every post using the pattern, and the change is saved with that pattern rather than with the post, so editor/undo does not necessarily cover it. A pattern nested inside itself stops the walk and is reported as truncated.
Chat
The chat panel talks to whichever AI provider the site has configured under Settings → Connectors (Anthropic, Google, OpenAI, or anything else that registers with the AI Client). It never holds credentials of its own.
How a turn works
WordPress 7.0 keeps the AI Client server-side, so the chat is split across the two:
- The browser lists the WebMCP tools the current page registers and sends them, with the conversation, to
POST /wp-json/agentic-editor/v1/chat. - PHP declares those tools as function declarations on
wp_ai_client_prompt()and runs one model turn. - If the model asked for tools, the browser runs them against the live page and posts the results back. This repeats until the model answers with text (25 rounds by default).
Conversation state lives entirely in the browser, so the endpoint is stateless and the same chat works on any screen. Assistant turns are replayed verbatim from the parts the previous response returned, which keeps provider-specific details such as function call IDs intact across rounds.
Gemini is the exception: it requires the thought signature it issued with a function call to come back with that call. The chat replays signatures whenever the provider plugin returns them, which the Google connector does from version 1.2.0, but an older connector drops them. When a turn fails for that reason it is retried once with the tool calls and results replayed as a text transcript, and the browser reports the working mode back so the rest of the conversation skips the failed attempt.
Where the tools come from
The chat offers whatever the page registered with WebMCP — nothing is hard-coded. In the block editor that is the abilities above, so the assistant can read the block tree and edit the post. Tools registered by other plugins on the same page are picked up automatically.
Tools this plugin registered are called through their own executor. Anything else goes through document.modelContext.executeTool(), which the polyfill always provides and native Chrome provides as an optional extension.
Tool names are rewritten server-side to the character set every provider accepts (editor_get-editor-tree survives as-is; dots become underscores) and mapped back before the browser sees them.
The interface
The panel is a React app built with shadcn/ui components and the AI SDK's useChat. Because WordPress 7.0 keeps the AI Client server-side, there is no AI SDK provider to point useChat at; instead src/chat/transport.ts implements a custom ChatTransport that calls the REST route once per round, runs the tools the model asked for against the page, and emits the whole exchange as one streaming assistant message.
React itself is not bundled. WordPress already puts React 18.3 on the page, and core asks plugins not to ship a second copy, so the build rewrites every React import to read WordPress's globals. Sharing the runtime is also what lets the editor sidebar render the panel as ordinary PluginSidebar children.
Tailwind is loaded without Preflight, since that reset would strip WordPress's own admin styling off any screen the chat appears on. The styles the components need are re-applied scoped to .cdchat.
Using the chat elsewhere
Add an entry under src/entries/ that renders <ChatPanel />, register it in vite.config.ts, and enqueue it:
import '@/styles/chat.css';
import { createRoot } from 'react-dom/client';
import { ChatPanel } from '@/components/chat-panel';
createRoot( document.getElementById( 'my-chat' )! ).render(
<ChatPanel
getContext={ () => ( { screen: 'my screen', notes: 'What the page is showing.' } ) }
suggestions={ [ 'What can you do here?' ] }
/>
);
Tool calls run without asking, since editor undo reverts them, except for three kinds that wait for Approve or Deny in the chat: calls to tools another script put on the page, calls that cannot be undone (editor/create-pattern, which publishes straight away, and editor/generate-image, which is billed and adds to the Media Library), and calls whose arguments carry HTML that could run script, such as a core/html block, a `
Releases
1 release. Each count is every asset in that release; expand a row for the breakdown.