WP Manifestindependent plugin directory
manifest / ai / contributor-day-editor-abilities

Contributor Day Editor Abilities

Registers client-side block editor abilities, bridges them to WebMCP, and adds an AI chat panel powered by the WordPress AI Client.

by Contributor Day · github.com/wpscholar/contributor-day-editor-abilities

1stars
1forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/wpscholar/contributor-day-editor-abilities/archive/refs/heads/main.zip

Readme

Contributor Day Editor Abilities

WordPress 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).

What it does

On block editor screens the plugin:

  1. Registers a block-editor ability category
  2. Registers twenty editor abilities (inspect / query / mutate the live editor)
  3. Bridges each ability to document.modelContext.registerTool(), installing the WebMCP polyfill when the browser has no native support
  4. Adds an AI Chat sidebar that can call those tools

There is also a standalone Tools → AI Chat screen running the same panel, to show the chat is not tied to the editor.

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

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 afterClientId to editor/insert-block fails instead of inserting at the top of the document.
  • editor/find-editor-blocks returns each match once as { clientId, name, attributes, innerBlockCount }, so a match nested inside another match is not duplicated. A supplied clientId scopes the search and includes that block itself.
  • search is 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, so Chloe finds <strong>Chloe Nolan</strong>. A value passed without an attribute is treated as search rather than matching every block.
  • attribute matches on presence; add value to compare, which is done as a string (objects and arrays compare as JSON).
  • editor/move-block takes afterClientId / beforeClientId (the sibling's parent becomes the destination) or an explicit rootClientId + 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.
  • editor/insert-block takes innerBlocks (recursive { name, attributes, innerBlocks }), and container blocks have to be built that way. An empty core/columns renders 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 one core/columns holding two core/column blocks. Nesting the block types forbid (core/column outside core/columns) fails before anything is inserted.
  • editor/update-block merges the attributes you pass; anything you omit is left alone. 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.
  • Attribute values are checked against the shape the block type declares, and defaults nested inside query sources are filled in. Those defaults are otherwise only applied while parsing saved markup, so a core/table cell set programmatically without its tag would render an undefined element and break the block.
  • editor/get-block-types is 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-type returns one block in full, including the style variations and the is-style-* class name that applies each. Blocks hidden from the inserter are excluded unless includeHidden is set, and passing rootClientId narrows the list to what that block will actually accept.
  • editor/transform-block uses 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/undo and editor/redo drive 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-patterns is the pattern counterpart of editor/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 carries rootBlockNames and blockCount, 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 is core/block/<id>, after the core/block block that references it.
  • Passing rootClientId to editor/get-patterns narrows 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-pattern returns blocks as { name, attributes, innerBlocks } — the shape editor/insert-block and editor/create-pattern accept, and deliberately without client IDs, since none of those blocks are in the document.
  • editor/insert-pattern copies an unsynced or registered pattern in as ordinary blocks, and inserts a synced pattern as a single core/block reference, which is what the editor does. Pass asReference: false to copy a synced pattern's blocks in as an independent, editable set instead. Every top-level block is checked against the destination first, so a pattern that does not fit fails before anything is inserted.
  • editor/create-pattern saves either blocks already in the document (clientIds) or a structure supplied directly (blocks). Sync status follows core: unsynced writes the wp_pattern_sync_status meta and inserts independent copies, synced omits 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.
  • replaceSource: true swaps the source blocks for a reference to the new synced pattern, which is the editor's own "Create pattern" behavior. It needs syncStatus: 'synced' and blocks that sit next to each other under one parent.
  • Creating anything requires an account that may create wp_block posts; 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.

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:

  1. The browser lists the WebMCP tools the current page registers and sends them, with the conversation, to POST /wp-json/contributor-day/v1/chat.
  2. PHP declares those tools as function declarations on wp_ai_client_prompt() and runs one model turn.
  3. 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 (8 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, and WordPress 7.0's AI Client does not yet carry signatures out of a provider response, so there is nothing to replay. 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 twenty abilities above, so the assistant can read the block tree and edit the post. On the standalone screen there are usually none, and the chat answers questions instead. 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: 'Extra system prompt context.' } ) }
        suggestions={ [ 'What can you do here?' ] }
    />
);

Read the full README on GitHub →