@gravitykit/block-mcp 2.0.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -0
- package/LICENSE +26 -0
- package/README.md +592 -0
- package/dist/index.cjs +52721 -0
- package/package.json +70 -0
- package/src/__tests__/fixtures/block-trees.ts +199 -0
- package/src/__tests__/fixtures/error-envelopes.ts +115 -0
- package/src/__tests__/fixtures/rest-responses.ts +280 -0
- package/src/__tests__/helpers/mock-client.ts +185 -0
- package/src/__tests__/helpers/request-matchers.ts +88 -0
- package/src/__tests__/helpers/schema-asserts.ts +132 -0
- package/src/__tests__/integration/concurrency.test.ts +129 -0
- package/src/__tests__/integration/dual-storage.test.ts +156 -0
- package/src/__tests__/integration/error-envelopes.test.ts +238 -0
- package/src/__tests__/integration/global-setup.ts +17 -0
- package/src/__tests__/integration/rate-limit.test.ts +88 -0
- package/src/__tests__/integration/read-edit-read.test.ts +141 -0
- package/src/__tests__/integration/ref-stability.test.ts +175 -0
- package/src/__tests__/integration/setup.ts +201 -0
- package/src/__tests__/tools/discovery/get_pattern.test.ts +58 -0
- package/src/__tests__/tools/discovery/get_post_info.test.ts +100 -0
- package/src/__tests__/tools/discovery/get_site_usage.test.ts +41 -0
- package/src/__tests__/tools/discovery/list_block_types.test.ts +103 -0
- package/src/__tests__/tools/discovery/list_patterns.test.ts +106 -0
- package/src/__tests__/tools/discovery/list_posts.test.ts +47 -0
- package/src/__tests__/tools/discovery/resolve_url.test.ts +69 -0
- package/src/__tests__/tools/discovery/scan_storage_modes.test.ts +34 -0
- package/src/__tests__/tools/media/upload_media.test.ts +123 -0
- package/src/__tests__/tools/mutate/edit_block_tree.test.ts +439 -0
- package/src/__tests__/tools/mutate/ref_routing.test.ts +105 -0
- package/src/__tests__/tools/patterns/insert_pattern.test.ts +117 -0
- package/src/__tests__/tools/posts/create_post.test.ts +84 -0
- package/src/__tests__/tools/posts/update_post.test.ts +93 -0
- package/src/__tests__/tools/read/get_block.test.ts +96 -0
- package/src/__tests__/tools/read/get_page_blocks.test.ts +184 -0
- package/src/__tests__/tools/read/persist_refs.test.ts +35 -0
- package/src/__tests__/tools/terms/list_terms.test.ts +91 -0
- package/src/__tests__/tools/write/delete_block.test.ts +91 -0
- package/src/__tests__/tools/write/insert_blocks.test.ts +149 -0
- package/src/__tests__/tools/write/ref_routing.test.ts +177 -0
- package/src/__tests__/tools/write/replace_block_range.test.ts +90 -0
- package/src/__tests__/tools/write/rewrite_post_blocks.test.ts +126 -0
- package/src/__tests__/tools/write/update_block.test.ts +206 -0
- package/src/__tests__/tools/write/update_blocks.test.ts +173 -0
- package/src/__tests__/tools/yoast/yoast_bulk_update_seo.test.ts +112 -0
- package/src/__tests__/tools/yoast/yoast_get_seo.test.ts +78 -0
- package/src/__tests__/tools/yoast/yoast_update_seo.test.ts +105 -0
- package/src/__tests__/unit/client/ref-endpoints.test.ts +232 -0
- package/src/__tests__/unit/enrichers/cbp-enricher.test.ts +457 -0
- package/src/__tests__/unit/error-translator/translate-wp-error.test.ts +318 -0
- package/src/__tests__/unit/instructions.test.ts +374 -0
- package/src/__tests__/unit/preferences/enrich-block-list.test.ts +175 -0
- package/src/__tests__/unit/preferences/enrich-pattern-list.test.ts +227 -0
- package/src/client.ts +964 -0
- package/src/connect.ts +877 -0
- package/src/enrichers.ts +348 -0
- package/src/error-translator.ts +156 -0
- package/src/index.ts +450 -0
- package/src/instructions.ts +270 -0
- package/src/preferences.ts +273 -0
- package/src/tools/discovery.ts +251 -0
- package/src/tools/media.ts +75 -0
- package/src/tools/mutate.ts +243 -0
- package/src/tools/patterns.ts +94 -0
- package/src/tools/posts.ts +200 -0
- package/src/tools/read.ts +201 -0
- package/src/tools/terms.ts +44 -0
- package/src/tools/write.ts +542 -0
- package/src/tools/yoast.ts +224 -0
- package/src/types.ts +862 -0
package/.env.example
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# WordPress site URL (required)
|
|
2
|
+
WORDPRESS_URL=https://example.com
|
|
3
|
+
|
|
4
|
+
# WordPress Application Password auth (required)
|
|
5
|
+
# Generate at: WordPress Admin → Users → Profile → Application Passwords
|
|
6
|
+
WORDPRESS_USER=
|
|
7
|
+
WORDPRESS_APP_PASSWORD=
|
|
8
|
+
|
|
9
|
+
# ── Integration tests (npm run test:integration) ──────────────────────────
|
|
10
|
+
# These vars gate the live WordPress integration suite. When any of the three
|
|
11
|
+
# vars above is absent every integration test skips automatically (no failures).
|
|
12
|
+
#
|
|
13
|
+
# Optional: prefix for throwaway posts created during integration tests.
|
|
14
|
+
# Posts with this prefix are swept by globalTeardown even if a test crashes.
|
|
15
|
+
# INTEGRATION_POST_TITLE_PREFIX=[integration-test]
|
package/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GravityKit (Katz Web Services, Inc.)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
The MIT license applies to the MCP server (TypeScript portion) only. The
|
|
24
|
+
WordPress plugin under wordpress-plugin/gk-block-api/ is licensed
|
|
25
|
+
GPL-2.0-or-later per WordPress plugin distribution requirements; see the
|
|
26
|
+
plugin's gk-block-api.php header.
|
package/README.md
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
# Block MCP
|
|
2
|
+
|
|
3
|
+
**Block MCP** is the WordPress MCP built for the way agents actually edit — one block at a time, across multiple turns, without corrupting the page. It's an MCP server plus WordPress plugin that exposes Gutenberg content as a structured, addressable block tree instead of raw HTML, so an agent can change a single heading without rewriting the page. Every block carries a stable `gk_ref` UUID that survives sibling shifts (no other WordPress MCP has this), so multi-turn edit chains don't re-fetch the page between calls. Every write creates a WordPress revision for rollback, ETag/If-Match guards against concurrent overwrites, and a server-side tier policy stops legacy blocks from ever hitting disk. Backed by **326 PHP tests, 249 TypeScript tests**, CI on PHP 8.2/8.3 + Node 20, and translations to 20 languages.
|
|
4
|
+
|
|
5
|
+
### Why agents pick Block MCP
|
|
6
|
+
|
|
7
|
+
- **Edits one block, not the whole page.** Change a heading's level without touching the surrounding HTML. Standard MCPs force a full page rewrite on every edit; Block MCP touches just the one heading.
|
|
8
|
+
- **Editor-safe round-trips.** `<!-- wp:* -->` block markers are preserved exactly. No "this block contains unexpected or invalid content" warnings on reopen.
|
|
9
|
+
- **Stable block refs no other WordPress MCP has.** Quickly chain inserts, deletes, and updates across turns from a single read.
|
|
10
|
+
- **Atomic batch edits.** Fix N independent blocks in **one** revision with `update_blocks` — all-or-nothing validation, so a stale ref or out-of-range index aborts the whole batch before anything hits disk. Keeps revision history clean instead of 6 entries for one logical change.
|
|
11
|
+
- **Tier policy enforced server-side.** Decide what blocks you want to allow or reject before they are saved, with suggested replacements.
|
|
12
|
+
- **Optimistic concurrency built in.** Two agents working on the same post can't silently overwrite each other.
|
|
13
|
+
- **Yoast SEO support built in.** Read and write Yoast meta (titles, descriptions, focus keywords, canonical URLs, schema types, primary terms, Open Graph / Twitter cards) the moment Yoast SEO is active on the site.
|
|
14
|
+
|
|
15
|
+
## Table of Contents
|
|
16
|
+
|
|
17
|
+
* [At a glance](#at-a-glance)
|
|
18
|
+
* [The numbers](#the-numbers)
|
|
19
|
+
* [When you actually ask an AI to edit a page](#when-you-actually-ask-an-ai-to-edit-a-page)
|
|
20
|
+
* [Why this MCP](#why-this-mcp)
|
|
21
|
+
* [Compared to other WordPress MCPs](#compared-to-other-wordpress-mcps)
|
|
22
|
+
* [Features](#features)
|
|
23
|
+
* [How It Works](#how-it-works)
|
|
24
|
+
* [Quick Start](#quick-start)
|
|
25
|
+
* [1. Install the WordPress plugin](#1-install-the-wordpress-plugin)
|
|
26
|
+
* [2. Connect your AI assistant](#2-connect-your-ai-assistant)
|
|
27
|
+
* [3. Manual setup (advanced)](#3-manual-setup-advanced)
|
|
28
|
+
* [4. (Optional) Tune the settings](#4-optional-tune-the-settings)
|
|
29
|
+
* [MCP Tools](#mcp-tools)
|
|
30
|
+
* [Stable Refs](#stable-refs)
|
|
31
|
+
* [Configuration](#configuration)
|
|
32
|
+
* [Namespace tier scores](#namespace-tier-scores)
|
|
33
|
+
* [Replacement map](#replacement-map)
|
|
34
|
+
* [Blocks that store data in two places](#blocks-that-store-data-in-two-places)
|
|
35
|
+
* [Post types AI agents can create](#post-types-ai-agents-can-create)
|
|
36
|
+
* [Storage-mode scan + reset](#storage-mode-scan--reset)
|
|
37
|
+
* [Examples](#examples)
|
|
38
|
+
* [Testing](#testing)
|
|
39
|
+
* [Requirements](#requirements)
|
|
40
|
+
* [Limitations](#limitations)
|
|
41
|
+
* [Error Codes](#error-codes)
|
|
42
|
+
* [Translations](#translations)
|
|
43
|
+
* [License](#license)
|
|
44
|
+
* [Contributing](#contributing)
|
|
45
|
+
|
|
46
|
+
## At a glance
|
|
47
|
+
|
|
48
|
+
Here's where Block MCP wins. Most other WordPress MCPs are wrappers around the standard WordPress REST API — fine for writing, but wrong for editing. "Change a heading, then add a button, then fix the next paragraph" could result in your post needing major rehab to get back to correct syntax. Block MCP is the answer to an WordPress block editor MCP that **just works**.
|
|
49
|
+
|
|
50
|
+
| What the agent can do | Standard WP REST API | Block MCP |
|
|
51
|
+
|---|:---:|:---:|
|
|
52
|
+
| **Edit one heading without touching the rest of the page** | ❌ Rewrites the entire page on every edit | ✅ Updates just the one heading |
|
|
53
|
+
| **Make 5 edits in a row without re-sending the whole page each time** | ❌ Sends the full page body 5× | ✅ Sends only what changed |
|
|
54
|
+
| **Find which block contains "Pricing" without scanning rendered HTML** | ❌ No structured search — agent has to regex through HTML | ✅ Built-in search by text or block type |
|
|
55
|
+
| **Stop legacy/deprecated blocks from being saved in the first place** | ❌ Writes any HTML, valid or not | ✅ Server rejects legacy blocks, suggests modern replacements |
|
|
56
|
+
| **Edit a page and have it still open cleanly in the block editor afterward** | ❌ Edits as raw HTML — expect many blocks with **"This block contains unexpected or invalid content"** because the original block markers got stripped | ✅ Block markup is preserved exactly. |
|
|
57
|
+
| **Keep editing the right block after adding or removing other blocks above it** | ❌ Re-reads the whole page after each edit | ✅ AI can keep working without re-reading |
|
|
58
|
+
| **Fix N typos across one page in a single revision** | ❌ N round-trips, N revisions cluttering history | ✅ One `update_blocks` call, one revision, atomic — partial failure rolls back the whole batch |
|
|
59
|
+
|
|
60
|
+
### When you actually ask an AI to edit a page
|
|
61
|
+
|
|
62
|
+
What matters is whether the page is correct after the agent finishes. So we put Claude in front of each MCP, typed a real instruction — *"change the H2 heading 'Code samples' to H3"* — then re-opened the page and inspected it.
|
|
63
|
+
|
|
64
|
+
27 runs total: three MCP servers × Haiku, Sonnet, Opus × 3 trials each.
|
|
65
|
+
|
|
66
|
+
| Model | Block MCP | [AI Engine Pro](https://meowapps.com/ai-engine/) | [InstaWP/mcp-wp](https://github.com/InstaWP/mcp-wp) |
|
|
67
|
+
|---|:---:|:---:|:---:|
|
|
68
|
+
| Haiku | ✅ 3 / 3 · 10 s avg | ⚠️ 2 / 3 · 44 s avg | ❌ 0 / 3 · 20 s avg |
|
|
69
|
+
| Sonnet | ✅ 3 / 3 · 9 s avg | ✅ 3 / 3 · 14 s avg | ❌ 0 / 3 · 36 s avg |
|
|
70
|
+
| Opus | ✅ 3 / 3 · 9 s avg | ✅ 3 / 3 · 13 s avg | ⚠️ 2 / 3 · 38 s avg |
|
|
71
|
+
| **Total** | **✅ 9 / 9** | **8 / 9** | **2 / 9** |
|
|
72
|
+
|
|
73
|
+
Three takeaways:
|
|
74
|
+
|
|
75
|
+
**Block MCP works on the cheapest model — and finishes fastest.** Haiku passes every trial in 10 seconds. The agent doesn't need to think hard about the page because the API is shaped exactly like the task. AI Engine Pro on Haiku takes 44 seconds when it works at all; InstaWP never does.
|
|
76
|
+
|
|
77
|
+
**InstaWP's wp/v2 wrapper fails 7 out of 9 times — even Opus only gets it right 2/3.** When the agent reports success, it's technically right that the heading text changed. But the whole-page round-trip through `update_page` strips every `<!-- wp:* -->` block marker. Reopen the page in the block editor and you'll see "This block contains unexpected or invalid content" warnings on most blocks. The standard REST API isn't broken — it does exactly what it's documented to do — but its data shape lets the AI corrupt content without realizing it.
|
|
78
|
+
|
|
79
|
+
**AI Engine Pro is competitive with Sonnet and Opus but stumbles on Haiku.** Its `wp_alter_post` tool is block-aware (the post markup stays valid), but on the failed Haiku trials the rendered HTML and the block's declared attributes drift out of sync — e.g., the comment marker still says `level: 2` while the inner tag is `<h3>`. The block editor flags that as broken too. Sonnet and Opus retry until consistent (2–3 tool calls); Haiku sometimes gives up after declaring success.
|
|
80
|
+
|
|
81
|
+
Reproduce with [`scripts/mcp-agent-bench.mjs`](scripts/mcp-agent-bench.mjs).
|
|
82
|
+
|
|
83
|
+
### Now try the structural ops agents actually need
|
|
84
|
+
|
|
85
|
+
A single heading-level change is the easy case. The interesting work is when an agent has to move a block, drop a paragraph inside an existing container, modify a table, or delete a block — the kind of multi-step structural editing real content workflows demand.
|
|
86
|
+
|
|
87
|
+
Five harder scenarios. Same matrix: three MCPs × Claude Haiku.
|
|
88
|
+
|
|
89
|
+
| Scenario | Block MCP | [AI Engine Pro](https://meowapps.com/ai-engine/) | [InstaWP/mcp-wp](https://github.com/InstaWP/mcp-wp) |
|
|
90
|
+
|---|:---:|:---:|:---:|
|
|
91
|
+
| **Move a block** to a new sibling position | ✅ 15 s · 2 calls | ✅ 25 s · 3 calls | ❌ structural fail · 29 s |
|
|
92
|
+
| **Insert a paragraph inside a `core/group`** | ✅ 15 s · 2 calls | ✅ 20 s · 4 calls | ❌ structural fail · 32 s |
|
|
93
|
+
| **Add a row** to a comparison table | ✅ 13 s · 2 calls | ✅ 25 s · 4 calls | ❌ structural fail · 32 s |
|
|
94
|
+
| **Delete a column** from a table | ✅ 12 s · 2 calls | ✅ 24 s · 4 calls | ❌ structural fail · 26 s |
|
|
95
|
+
| **Delete a heading block** | ✅ 12 s · 3 calls | ✅ 16 s · 3 calls | ❌ structural fail · 24 s |
|
|
96
|
+
| **Total** | **✅ 5 / 5** | **✅ 5 / 5** | **❌ 0 / 5** |
|
|
97
|
+
|
|
98
|
+
**Block MCP averages 13 seconds and two tool calls per scenario.** The agent reads the page once, finds the target block by ref or path, calls one mutation, done.
|
|
99
|
+
|
|
100
|
+
**AI Engine Pro keeps the page intact and finishes correctly,** about 2× slower. Its `wp_alter_post` tool asks the agent to supply both the block-comment markup and the rendered HTML, so most scenarios spend an extra round-trip generating the right shape.
|
|
101
|
+
|
|
102
|
+
**InstaWP/mcp-wp fails every scenario with a "structural fail":** the agent (Haiku, given `update_page`) writes the page back as plain HTML — `<h1>...</h1><p>...</p><ol>...` — with no `<!-- wp:* -->` block markers. WordPress accepts the save, `parse_blocks()` collapses the entire page into one freeform chunk, and every distinctive block on the page disappears as a structured entity. The agent thinks it succeeded; the page is broken in the block editor on reopen. That's the penalty of wrapping the standard wp/v2 REST surface and trusting the agent to reconstruct block markup by hand.
|
|
103
|
+
|
|
104
|
+
Reproduce with [`scripts/mcp-agent-bench.mjs`](scripts/mcp-agent-bench.mjs).
|
|
105
|
+
|
|
106
|
+
## Why Block MCP
|
|
107
|
+
|
|
108
|
+
Block MCP is the only WordPress MCP designed from the ground up for the way agents actually edit pages: one block at a time, across multiple turns, without corrupting anything along the way. The agent-loop bench reflects that — 9 of 9 across every Claude tier, including the cheapest.
|
|
109
|
+
|
|
110
|
+
Most WordPress MCPs wrap the default REST API. That gives an agent post-level CRUD, but it stops there — to change one heading on a page, the agent has to read the entire `post_content` HTML, parse it, find the right tag, mutate it, and write the whole thing back. Block boundaries dissolve, structure breaks subtly, and there's no undo path.
|
|
111
|
+
|
|
112
|
+
Block MCP is built around the block tree itself. The agent sees a structured, addressable, well-typed view of the page — and writes through purpose-built endpoints that know what blocks are.
|
|
113
|
+
|
|
114
|
+
What that gets you in practice:
|
|
115
|
+
|
|
116
|
+
- **Block-aware editing.** Change a heading's level, swap a button's URL, reorder columns — without touching surrounding HTML. The agent works in JSON; the plugin handles parse/serialize.
|
|
117
|
+
- **Stable block refs.** Every block carries a persistent ID. An agent can fetch a page once, capture the refs of every block it intends to edit, then chain inserts/deletes/updates against those refs without re-reading. Sibling shifts don't invalidate the addresses.
|
|
118
|
+
- **Path-based structural ops.** Nine operations (`update-attrs`, `replace-block`, `wrap-in-group`, `unwrap-group`, `move`, `duplicate`, `insert-child`, `remove-block`, `update-html`) work on any nesting depth via integer paths or refs.
|
|
119
|
+
- **Auto-transforms.** Change a heading's `level` attribute and the `<h2>`/`<h3>` tag updates with it. Toggle a list to ordered and `<ul>` becomes `<ol>`. The plugin keeps attributes and innerHTML in sync for the common patterns so agents don't have to.
|
|
120
|
+
- **Site policy enforcement.** Per-site preference tiers reject inserts of blocks you've marked as legacy and surface suggested replacements. An agent can't write blocks your site doesn't want.
|
|
121
|
+
- **Revision-backed undo.** Every write returns `before_revision_id` and `revision_id`. `revert_to_revision` rolls back to either side of any edit.
|
|
122
|
+
- **Discovery tools.** Browse registered block types with preference scoring, search patterns, query site-wide block/pattern usage, resolve URLs to post IDs. The agent can plan with knowledge of what your site actually contains.
|
|
123
|
+
- **Static-block safety guards.** Warns when an attribute change would leave rendered markup stale, so the agent knows when to also pass innerHTML.
|
|
124
|
+
|
|
125
|
+
The combination — block-aware, ref-stable, revision-tracked, policy-enforcing — is what existing REST-API-wrapping MCPs don't give you.
|
|
126
|
+
|
|
127
|
+
## Compared to other WordPress MCPs
|
|
128
|
+
|
|
129
|
+
The WordPress MCP space is small, and Block MCP is the only one operating at the block-tree layer. The other projects work at different layers and target different workflows — they're often complementary rather than head-to-head, but the agent-loop bench above shows they don't all produce correct results when asked to edit a block.
|
|
130
|
+
|
|
131
|
+
**[InstaWP/mcp-wp](https://github.com/InstaWP/mcp-wp)** — A REST-API-wrapping MCP that operates on whole posts, plus broad coverage of users, comments, media, plugins, and plugin-repo search. Standout feature: multi-site management from one MCP instance. Reach for it when you need post-level CRUD across many sites or general-purpose WordPress administration. *Not block-aware:* editing a single heading inside a long page means reading and rewriting the entire post, and the round-trip through `wp/v2`'s `update_page` strips every `<!-- wp:* -->` block marker. In our bench it failed validation on 7 of 9 trials across Haiku/Sonnet/Opus.
|
|
132
|
+
|
|
133
|
+
**[AI Engine Pro](https://meowapps.com/ai-engine/)** — Self-hosted MCP server inside WordPress (Streamable HTTP at `/wp-json/mcp/v1/http`), built by Meow Apps and the most-installed WordPress AI plugin (100K+). Free tier exposes posts/comments/users/media as MCP tools; Pro adds an Editor Assistant sidebar and additional MCP plumbing. Its `wp_alter_post` tool *is* block-aware — block-comment markers survive — but it can desync the block's declared attributes from its innerHTML (e.g., comment marker still says `level: 2` while the inner tag is `<h3>`), and the block editor flags that as broken too. Sonnet and Opus retry until consistent and pass; Haiku sometimes gives up at 7–12 tool calls. 8 of 9 in the bench.
|
|
134
|
+
|
|
135
|
+
**Block MCP** (this project) — Operates one layer below: inside a single post's block tree. Path- and ref-based addressing, auto-transforms that keep attributes and innerHTML in sync server-side, preference-tier enforcement, per-block revisions. None of those exist in the other three. Reach for it when an agent needs to edit *blocks* — change a heading level, swap a column layout, insert a CTA after the third paragraph — without rewriting the surrounding content. 9 of 9 in the bench, perfect across all three Claude models, including the cheapest.
|
|
136
|
+
|
|
137
|
+
These can coexist. Block MCP could (and likely will) be exposed through the official adapter as registered abilities once that path matures — same logic, blessed plumbing. See [issues](https://github.com/GravityKit/block-mcp/issues) for the roadmap.
|
|
138
|
+
|
|
139
|
+
## Features
|
|
140
|
+
|
|
141
|
+
**Read**
|
|
142
|
+
- Full block tree as structured JSON: paths, names, attributes, refs, `text_preview` of each block's content
|
|
143
|
+
- Page summary in one call: block type counts, headings with paths, section markers, max nesting depth
|
|
144
|
+
- Outline mode for fast page structure inspection
|
|
145
|
+
- Search blocks by text or block name
|
|
146
|
+
- Render mode expands shortcodes, resolves synced patterns, marks dynamic blocks
|
|
147
|
+
|
|
148
|
+
**Write — by index, by ref, or by path**
|
|
149
|
+
- `update_block` — flat-index OR ref
|
|
150
|
+
- `update_blocks` — atomic N-update batch in ONE revision; all-or-nothing validation, max 50 items, counts as one write against the rate limit
|
|
151
|
+
- `delete_block` — top-level counter OR ref
|
|
152
|
+
- `insert_blocks` — anchor on `after_top_level`/`before_top_level` OR `after_ref`/`before_ref`
|
|
153
|
+
- `edit_block_tree` — 9 path-based or ref-based structural ops:
|
|
154
|
+
- `update-attrs`, `update-html`, `replace-block`, `remove-block`
|
|
155
|
+
- `wrap-in-group`, `unwrap-group`, `insert-child`, `duplicate`, `move`
|
|
156
|
+
- `rewrite_post_blocks` — full page rewrite
|
|
157
|
+
- `dry_run` parameter to validate any mutation without writing
|
|
158
|
+
|
|
159
|
+
**Safety**
|
|
160
|
+
- Auto-transform keeps innerHTML in sync when attributes change (heading level, list ordered, group tagName, button URL, image src, spacer height, etc.)
|
|
161
|
+
- Static block guards warn when an attribute change may leave rendered markup stale
|
|
162
|
+
- Configurable preference tiers: legacy blocks rejected on insert, avoid-tier blocks return warnings with suggested replacements
|
|
163
|
+
- Per-post rate limiting (10 writes/min, 2 full rewrites/min)
|
|
164
|
+
- Every write creates a WordPress revision; `revert_to_revision` undoes any edit
|
|
165
|
+
|
|
166
|
+
**Discover**
|
|
167
|
+
- List block types filtered by namespace, category, or preference tier
|
|
168
|
+
- Browse patterns (synced + registered) scored by recency, reference count, and legacy content
|
|
169
|
+
- Site-wide block/pattern usage analytics (cached)
|
|
170
|
+
- Resolve any URL or slug to its post ID, type, and edit link
|
|
171
|
+
|
|
172
|
+
## How It Works
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
AI Agent ←stdio→ MCP server (your machine) ←HTTPS→ WordPress plugin (your site)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**WordPress plugin** (`wordpress-plugin/gk-block-api/`) — REST API at `gk-block-api/v1`. Handles block parsing, serialization, safety checks, preference scoring, rate limiting, revisions. Works with any post type that stores Gutenberg blocks in `post_content`.
|
|
179
|
+
|
|
180
|
+
**MCP server** (`src/`) — TypeScript stdio server that exposes the REST API as MCP tools. Authenticates as a normal WordPress user via Application Password. No special privileges, no direct DB access from the MCP side.
|
|
181
|
+
|
|
182
|
+
## Quick Start
|
|
183
|
+
|
|
184
|
+
### 1. Install the WordPress plugin
|
|
185
|
+
|
|
186
|
+
**Easiest — download the latest ZIP:** [gk-block-api.zip](https://github.com/GravityKit/block-mcp/releases/download/latest/gk-block-api.zip) (auto-built from `main` on every push).
|
|
187
|
+
|
|
188
|
+
Then in WordPress: **Plugins → Add New → Upload Plugin** and pick the ZIP.
|
|
189
|
+
|
|
190
|
+
Or copy `wordpress-plugin/gk-block-api/` to your site's `wp-content/plugins/` and activate manually. Or via WP-CLI:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
wp plugin install https://github.com/GravityKit/block-mcp/releases/download/latest/gk-block-api.zip --activate
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 2. Connect your AI assistant
|
|
197
|
+
|
|
198
|
+
The fastest path provisions everything for you — a dedicated `block-mcp` service
|
|
199
|
+
account, a minimal-capability role, and an Application Password — from inside
|
|
200
|
+
WordPress. Go to **Settings → Block MCP → Connect** and pick your client.
|
|
201
|
+
|
|
202
|
+
**Claude Desktop (one-click).** Download the generated `.mcpb` file and open it;
|
|
203
|
+
Claude Desktop installs the server and stores the credential in your OS keychain.
|
|
204
|
+
The `.mcpb` is self-contained — it bundles the server, so there's nothing else to
|
|
205
|
+
install.
|
|
206
|
+
|
|
207
|
+
**Cursor, Claude Code, ChatGPT Desktop (browser approve).** Run the connector and
|
|
208
|
+
click **Approve** in the browser that opens:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npx -y @gravitykit/block-mcp connect --site https://example.com
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
It writes your client's MCP config for you (owner-only, mode `0600`), so the
|
|
215
|
+
site-wide password never lands in your shell history or a hand-edited file. Add
|
|
216
|
+
`--client cursor|claude-code|claude-desktop|print` to target a specific client.
|
|
217
|
+
Each site you connect gets its own server entry, so one assistant can point at
|
|
218
|
+
several sites.
|
|
219
|
+
|
|
220
|
+
> **Runtime:** the common path runs the server with `npx -y @gravitykit/block-mcp`
|
|
221
|
+
> — nothing to clone or build. The Claude Desktop `.mcpb` embeds the same bundle.
|
|
222
|
+
|
|
223
|
+
### 3. Manual setup (advanced)
|
|
224
|
+
|
|
225
|
+
Prefer to wire it up by hand? Create an Application Password and register the
|
|
226
|
+
server yourself.
|
|
227
|
+
|
|
228
|
+
In WordPress admin: **Users → Profile → Application Passwords**. Or via CLI:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
wp user application-password create <username> "Block MCP" --porcelain
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The user needs at minimum the `edit_posts` capability for any post you want to
|
|
235
|
+
read or write. Then build and register the server:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
git clone https://github.com/GravityKit/block-mcp
|
|
239
|
+
cd block-mcp
|
|
240
|
+
npm install # auto-builds dist/index.cjs via the prepare script
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Register the server in your MCP client. Example for Claude Code's `~/.claude.json`:
|
|
244
|
+
|
|
245
|
+
```json
|
|
246
|
+
{
|
|
247
|
+
"mcpServers": {
|
|
248
|
+
"block-mcp": {
|
|
249
|
+
"command": "node",
|
|
250
|
+
"args": ["/absolute/path/to/block-mcp/dist/index.cjs"],
|
|
251
|
+
"env": {
|
|
252
|
+
"WORDPRESS_URL": "https://example.com",
|
|
253
|
+
"WORDPRESS_USER": "your-wp-username",
|
|
254
|
+
"WORDPRESS_APP_PASSWORD": "xxxx xxxx xxxx xxxx xxxx xxxx"
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Restart your MCP client. Run `npm run inspect` to test the tools interactively.
|
|
262
|
+
|
|
263
|
+
### 4. (Optional) Tune the settings
|
|
264
|
+
|
|
265
|
+
When the plugin is active, an admin page appears at **Settings → Block MCP**. The defaults work out of the box, but it's worth a look — this is where you decide which blocks AI agents are allowed to write, what to suggest as replacements, and which post types `create_post` can target.
|
|
266
|
+
|
|
267
|
+

|
|
268
|
+
|
|
269
|
+
See the [Configuration](#configuration) section below for the full breakdown.
|
|
270
|
+
|
|
271
|
+
## MCP Tools
|
|
272
|
+
|
|
273
|
+
**Content I/O**
|
|
274
|
+
|
|
275
|
+
| Tool | Purpose |
|
|
276
|
+
|---|---|
|
|
277
|
+
| `get_page_blocks` | Read a post's blocks. Supports `outline`, `summary_only`, `search`, `block_name`, `render`, `fields`, `persist_refs` |
|
|
278
|
+
| `update_block` | Update one block's attributes/innerHTML (by `flat_index` or `ref`) |
|
|
279
|
+
| `update_blocks` | Apply N independent updates atomically in ONE revision (max 50). All-or-nothing validation — any stale ref / out-of-range index / dual-storage rejection / duplicate target aborts the batch with itemized errors before anything hits disk |
|
|
280
|
+
| `insert_blocks` | Insert blocks at a position (by counter or ref) |
|
|
281
|
+
| `delete_block` | Remove block(s) (by counter or ref) |
|
|
282
|
+
| `replace_block_range` | Atomic single-revision swap of N blocks for M blocks |
|
|
283
|
+
| `rewrite_post_blocks` | Full page rewrite |
|
|
284
|
+
| `edit_block_tree` | 9 path-or-ref-based structural ops |
|
|
285
|
+
| `insert_pattern` | Insert a pattern, synced or inline |
|
|
286
|
+
| `revert_to_revision` | Roll back to a prior revision ID |
|
|
287
|
+
|
|
288
|
+
**Posts & taxonomies**
|
|
289
|
+
|
|
290
|
+
| Tool | Purpose |
|
|
291
|
+
|---|---|
|
|
292
|
+
| `create_post` | Create a post or page (draft, publish, future) — accepts blocks or HTML |
|
|
293
|
+
| `update_post` | Update post metadata, status, terms — covers publish/trash/untrash transitions |
|
|
294
|
+
| `list_terms` | List taxonomy terms (categories, tags, custom) for ID lookup |
|
|
295
|
+
| `find_posts` / `post_info` / `resolve_url` | Locate posts by search, ID, slug, or URL |
|
|
296
|
+
|
|
297
|
+
**Media**
|
|
298
|
+
|
|
299
|
+
| Tool | Purpose |
|
|
300
|
+
|---|---|
|
|
301
|
+
| `upload_media` | Upload via local path, URL sideload (with SSRF guard), or base64. Returns attachment ID + URL |
|
|
302
|
+
|
|
303
|
+
**Discovery**
|
|
304
|
+
|
|
305
|
+
| Tool | Purpose |
|
|
306
|
+
|---|---|
|
|
307
|
+
| `list_block_types` | Browse registered block types with preference tiers |
|
|
308
|
+
| `list_patterns` / `get_pattern` | Search and inspect patterns with scoring |
|
|
309
|
+
| `get_site_usage` | Block/pattern usage analytics |
|
|
310
|
+
|
|
311
|
+
**SEO** (when [Yoast SEO](https://wordpress.org/plugins/wordpress-seo/) is active)
|
|
312
|
+
|
|
313
|
+
| Tool | Purpose |
|
|
314
|
+
|---|---|
|
|
315
|
+
| `yoast_get_seo` | Read SEO metadata: title, description, robots, OG, Twitter, schema, scores |
|
|
316
|
+
| `yoast_update_seo` / `yoast_bulk_update_seo` | Update SEO fields on one or many posts |
|
|
317
|
+
|
|
318
|
+
## Stable Refs
|
|
319
|
+
|
|
320
|
+
Every block in a `get_page_blocks` response includes a `ref` field:
|
|
321
|
+
|
|
322
|
+
```json
|
|
323
|
+
{
|
|
324
|
+
"index": 5,
|
|
325
|
+
"path": [0, 2, 1],
|
|
326
|
+
"ref": "blk_a3f2c1q9",
|
|
327
|
+
"name": "core/heading",
|
|
328
|
+
"attributes": { "level": 2, "content": "Hello" }
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Refs are stored in `attrs.metadata.gk_ref` inside `post_content`, so they survive across sessions and across mutations that shift sibling positions. Pass `ref` to `update_block`, `delete_block`, or `edit_block_tree` to address the same block reliably even after inserts or deletes elsewhere on the page.
|
|
333
|
+
|
|
334
|
+
The first read of a post lazily assigns + persists refs via a direct DB write that skips revision creation (refs are editor-only metadata, not content). Pass `persist_refs: false` to read without that side effect.
|
|
335
|
+
|
|
336
|
+
## Configuration
|
|
337
|
+
|
|
338
|
+
Everything in this section is editable at **Settings → Block MCP** in WordPress admin. Defaults are sensible — none of this is required to get started.
|
|
339
|
+
|
|
340
|
+
### Namespace tier scores
|
|
341
|
+
|
|
342
|
+
Block preferences are stored as a WordPress option (`gk_block_api_preferences`) and configurable per-site. Each block namespace gets a score 0–100, which maps to a tier:
|
|
343
|
+
|
|
344
|
+
| Tier | Score | Policy |
|
|
345
|
+
|---|---|---|
|
|
346
|
+
| **preferred** | ≥ 80 | Use freely |
|
|
347
|
+
| **acceptable** | 50–79 | Use if preferred unavailable |
|
|
348
|
+
| **avoid** | 10–49 | Warn, return suggested replacement |
|
|
349
|
+
| **legacy** | < 10 | Reject on insert |
|
|
350
|
+
|
|
351
|
+
Defaults ship with `core/*` preferred and a starter set of known-deprecated namespaces marked legacy. Add new namespaces by typing into the bottom row — a fresh blank row appears as soon as you start typing.
|
|
352
|
+
|
|
353
|
+
### Replacement map
|
|
354
|
+
|
|
355
|
+
When an agent attempts to insert a legacy block, the rejection error includes a suggested replacement from this map. Both columns are searchable dropdowns of every block currently registered on your site (you can also type a block name that isn't currently registered).
|
|
356
|
+
|
|
357
|
+

|
|
358
|
+
|
|
359
|
+
### Blocks that store data in two places
|
|
360
|
+
|
|
361
|
+
A few blocks (notably `yoast/faq-block`) keep the same data in *both* their attributes and their innerHTML. Updating one without the other corrupts the block silently. Block MCP detects most automatically by scanning your site; list any extras here so the API forces agents to send both fields together.
|
|
362
|
+
|
|
363
|
+

|
|
364
|
+
|
|
365
|
+
### Post types AI agents can create
|
|
366
|
+
|
|
367
|
+
Restrict `create_post` to specific post types. Leave everything unchecked to allow any public post type with REST support (the default).
|
|
368
|
+
|
|
369
|
+

|
|
370
|
+
|
|
371
|
+
### Storage-mode scan + reset
|
|
372
|
+
|
|
373
|
+
The scan walks every published post and classifies each distinct block as static / dynamic / dual, replacing the filter defaults with live data from your site. Slow on large sites; the result is cached. The Reset button below it clears every option this plugin owns and restores hard-coded defaults.
|
|
374
|
+
|
|
375
|
+

|
|
376
|
+
|
|
377
|
+
## Examples
|
|
378
|
+
|
|
379
|
+
**Update a heading by URL**
|
|
380
|
+
|
|
381
|
+
> "Change the H2 'Welcome' on `/about/` to 'About Us'."
|
|
382
|
+
|
|
383
|
+
1. `resolve_url({ url: "/about/" })` → post ID
|
|
384
|
+
2. `get_page_blocks({ post_id, outline: true })` → finds heading at `path: [4]`, ref `blk_a3f2c1q9`
|
|
385
|
+
3. `edit_block_tree({ post_id, op: "update-attrs", ref: "blk_a3f2c1q9", attributes: { content: "About Us" } })`
|
|
386
|
+
|
|
387
|
+
Auto-transform updates both the `content` attribute and the inner `<h2>` text. Revision created.
|
|
388
|
+
|
|
389
|
+
**Chained edit workflow (where refs shine)**
|
|
390
|
+
|
|
391
|
+
> "On the homepage: delete the third paragraph, change the next H2 to H3, and add a CTA button after it."
|
|
392
|
+
|
|
393
|
+
1. `get_page_blocks({ post_id })` once — capture refs for all three target blocks
|
|
394
|
+
2. `delete_block({ post_id, ref: <para-ref> })`
|
|
395
|
+
3. `edit_block_tree({ post_id, op: "update-attrs", ref: <heading-ref>, attributes: { level: 3 } })`
|
|
396
|
+
4. `insert_blocks({ post_id, after_ref: <heading-ref>, blocks: [{ name: "core/buttons", … }] })`
|
|
397
|
+
|
|
398
|
+
With path-based addressing, the agent would need to re-fetch between every step. With refs, one read covers the whole chain.
|
|
399
|
+
|
|
400
|
+
**Author and publish a doc**
|
|
401
|
+
|
|
402
|
+
1. `list_terms({ taxonomy: "category", search: "Documentation" })` → category ID
|
|
403
|
+
2. `create_post({ title: "Getting Started", status: "draft", categories: [<id>], blocks: [...] })` → post ID
|
|
404
|
+
3. `upload_media({ path: "/tmp/screenshot.png", alt_text: "...", post_id })` → attachment ID + URL
|
|
405
|
+
4. `insert_blocks({ post_id, after_top_level: 0, blocks: [{ name: "core/image", attributes: { id: <atch>, url, alt: "..." } }] })`
|
|
406
|
+
5. `yoast_update_seo({ post_id, title: "...", description: "...", focus_keyword: "..." })`
|
|
407
|
+
6. `update_post({ post_id, status: "publish" })`
|
|
408
|
+
|
|
409
|
+
## Testing
|
|
410
|
+
|
|
411
|
+
Run all suites locally:
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
# TypeScript (Vitest) — 257 tests
|
|
415
|
+
npm test
|
|
416
|
+
|
|
417
|
+
# PHP (PHPUnit, stub WP bootstrap) — 335 tests
|
|
418
|
+
cd wordpress-plugin/gk-block-api && phpunit -c tests/phpunit.xml
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
The PHP suite uses a minimal WordPress stub layer (no full WP install required) to exercise validation, error paths, mutation engine, ref resolution, HTML auto-transforms, post lifecycle, term listing, media validation, and REST summary/outline.
|
|
422
|
+
|
|
423
|
+
An end-to-end smoke script is included under `scripts/` for live-WordPress validation; point it at any WordPress site by setting `WORDPRESS_URL`, `WORDPRESS_USER`, and `WORDPRESS_APP_PASSWORD`.
|
|
424
|
+
|
|
425
|
+
## Requirements
|
|
426
|
+
|
|
427
|
+
- Node.js ≥ 20
|
|
428
|
+
- WordPress ≥ 6.0 with Application Passwords enabled
|
|
429
|
+
- PHP ≥ 7.4
|
|
430
|
+
- HTTPS (required by WordPress for Application Password authentication)
|
|
431
|
+
|
|
432
|
+
## Limitations
|
|
433
|
+
|
|
434
|
+
**Scope**
|
|
435
|
+
|
|
436
|
+
- Edits work on posts stored as blocks. Block-theme templates (`wp_template`, `wp_template_part`) and widget areas are not yet supported.
|
|
437
|
+
- Custom post types must declare `show_in_rest: true` (or be in the configured allow-list) to be writable.
|
|
438
|
+
- innerHTML passes through `wp_kses_post` on every write — `<script>`, inline event handlers, and other disallowed markup are stripped. Whitelist additional tags with the `wp_kses_allowed_html` filter if needed.
|
|
439
|
+
|
|
440
|
+
**Tier policy**
|
|
441
|
+
|
|
442
|
+
- Legacy-tier blocks (score < 10) are **hard-rejected** on insert, `replace-block`, `insert-child`, `wrap-in-group`, and `replace_all_blocks`. The error includes a suggested replacement when one is mapped.
|
|
443
|
+
- Avoid-tier blocks (score 10–49) write through with warnings, not errors.
|
|
444
|
+
- Tier policy is **insert-only** — `update-attrs` and `update-html` can mutate a legacy block that's already on the page (so existing pages aren't bricked).
|
|
445
|
+
|
|
446
|
+
**Structural caps**
|
|
447
|
+
|
|
448
|
+
- Block nesting depth is capped at **32** levels (`MAX_BLOCK_DEPTH`). Trees deeper than that reject with `block_depth_exceeded`. Not filterable.
|
|
449
|
+
- Batch writes (`update_blocks`) cap at **50 items** per call (`MAX_BATCH_SIZE`). One batch counts as one write against the rate limit regardless of N.
|
|
450
|
+
|
|
451
|
+
**Rate limits**
|
|
452
|
+
|
|
453
|
+
- Per-post, per-minute, transient-backed. **10 writes/min** for `update_*`/`delete_*`/`insert_*`/`mutate_*`/`update_post`; **2/min** for full-rewrite `PUT /blocks`.
|
|
454
|
+
- Buckets are per-post, not per-user — multiple agents editing the same post share the budget.
|
|
455
|
+
- Returns HTTP 429 `rate_limit_exceeded`; resets naturally after 60 s.
|
|
456
|
+
|
|
457
|
+
**Static block innerHTML**
|
|
458
|
+
|
|
459
|
+
- WordPress has no PHP equivalent of the React `save` function, so the server cannot regenerate the rendered markup of a static block from its attributes alone. Auto-transforms cover heading level, list ordered, group `tagName`, button URL, image `src`/`alt`, video/audio booleans, spacer height/width, details `open`, and quote citation. For anything else, send `innerHTML` along with `attributes` (`update_block` will refuse dual-storage writes that omit either side).
|
|
460
|
+
|
|
461
|
+
**Dual-storage blocks**
|
|
462
|
+
|
|
463
|
+
- A small set of blocks (notably `yoast/faq-block`) duplicate state across `attributes` *and* `innerHTML`. The API requires both fields together on update (`dual_storage_requires_both` error otherwise) and the dual-storage list is configurable at **Settings → Block MCP**.
|
|
464
|
+
|
|
465
|
+
**Block Bindings API**
|
|
466
|
+
|
|
467
|
+
- Requires WordPress 6.5+ on the target site.
|
|
468
|
+
- Attributes listed in `attrs.metadata.bindings` are **write-locked** by default — a write that targets a bound attribute returns 400 `bound_attribute`. Pass `allow_bound_writes: true` on the update to override.
|
|
469
|
+
- Reads surface the binding map as a top-level `bindings` field and a `bound_attributes` array; binding *resolution* (rendering the dynamic value) happens only in `render` mode.
|
|
470
|
+
|
|
471
|
+
**Schema-aware attribute extraction**
|
|
472
|
+
|
|
473
|
+
- Reads merge attributes sourced via `block.json` (`source: attribute | html | rich-text | text`) into the response.
|
|
474
|
+
- `source: 'query'` is not yet supported — it returns the delimiter attrs only with a TODO. `source: 'meta'` is deprecated and ignored.
|
|
475
|
+
|
|
476
|
+
**Patterns**
|
|
477
|
+
|
|
478
|
+
- Registered patterns are always inlined on insert. Only synced patterns (`wp_block` CPT entries) can be inserted as a `core/block` reference.
|
|
479
|
+
|
|
480
|
+
**Media uploads**
|
|
481
|
+
|
|
482
|
+
- URL sideload is capped at **25 MB** and uses a 10 s timeout.
|
|
483
|
+
- SSRF guard rejects RFC1918 / loopback / link-local / cloud-metadata (`169.254.0.0/16`) hosts before download. The block list is extensible via the `gk_block_api_url_sideload_blocked_ranges` filter.
|
|
484
|
+
- Uploads can be disabled site-wide with the kill-switch at **Settings → Block MCP**.
|
|
485
|
+
|
|
486
|
+
**Render mode**
|
|
487
|
+
|
|
488
|
+
- `?render=true` resolves dynamic blocks, expands shortcodes, and follows synced pattern references. Disabled by default — read paths return raw block markup so an agent sees what the editor sees.
|
|
489
|
+
|
|
490
|
+
## Error Codes
|
|
491
|
+
|
|
492
|
+
Every REST endpoint returns errors as JSON in the standard WordPress shape `{ code, message, data: { status, … } }`. The MCP server forwards the HTTP status and code through to the tool result so the agent can dispatch on `code` directly.
|
|
493
|
+
|
|
494
|
+
### Auth & permissions (HTTP 403)
|
|
495
|
+
|
|
496
|
+
| Code | When it fires | How to recover |
|
|
497
|
+
|---|---|---|
|
|
498
|
+
| `rest_forbidden` | Caller lacks `edit_posts` capability on the request | Use an Application Password for a user with `edit_posts` |
|
|
499
|
+
| `rest_cannot_edit` | Caller lacks `edit_post` for the specific post | Reassign the post or elevate the user's capability |
|
|
500
|
+
| `rest_cannot_create` | Caller lacks `edit_posts` (or post-type-specific create cap) for `create_post` | Same |
|
|
501
|
+
| `rest_cannot_publish` | `create_post` / `update_post` requested `publish` but caller lacks `publish_posts` | Lower status to `draft`/`pending`, or elevate the user |
|
|
502
|
+
| `rest_cannot_upload` | `upload_media` called without `upload_files` cap | Elevate the user |
|
|
503
|
+
| `rest_cannot_assign_author` | `create_post` / `update_post` set `author` to another user without `edit_others_posts` | Drop the `author` field or elevate |
|
|
504
|
+
| `uploads_disabled` | Site admin flipped the uploads kill-switch off at Settings → Block MCP | Re-enable in admin or stop calling `upload_media` |
|
|
505
|
+
|
|
506
|
+
### Not found (HTTP 404)
|
|
507
|
+
|
|
508
|
+
| Code | When it fires | How to recover |
|
|
509
|
+
|---|---|---|
|
|
510
|
+
| `post_not_found` | `post_id` doesn't resolve to a post | Re-run `resolve_url` or `find_posts` |
|
|
511
|
+
| `block_not_found` | `flat_index` / `path` / `ref` doesn't address an existing block | Re-fetch `get_page_blocks` |
|
|
512
|
+
| `ref_stale` | `gk_ref` no longer exists in the post (deleted or replaced) | Re-fetch and re-bind |
|
|
513
|
+
| `pattern_not_found` | `pattern_id` doesn't match a synced or registered pattern | Use `list_patterns` |
|
|
514
|
+
| `revision_not_found` | `revert_to_revision` got an ID that isn't a revision of the target post | Use `update_post` history or query the post's revisions |
|
|
515
|
+
| `not_found` | Generic resource-not-found for endpoints that don't have a specific code | Inspect `message` for which resource |
|
|
516
|
+
|
|
517
|
+
### Precondition / concurrency (HTTP 412)
|
|
518
|
+
|
|
519
|
+
| Code | When it fires | How to recover |
|
|
520
|
+
|---|---|---|
|
|
521
|
+
| `stale_revision` | `If-Match` header / `if_match` body field didn't match the current revision ID (someone else edited the post) | Re-fetch, re-apply changes against fresh state, retry |
|
|
522
|
+
|
|
523
|
+
### Validation (HTTP 400)
|
|
524
|
+
|
|
525
|
+
| Code | When it fires | How to recover |
|
|
526
|
+
|---|---|---|
|
|
527
|
+
| `legacy_block` | Inserting a block in the legacy tier | Use the suggested replacement returned in `data.suggested_replacement` |
|
|
528
|
+
| `dual_storage_requires_both` | Updating a dual-storage block with only `attributes` or only `innerHTML` | Send both fields together |
|
|
529
|
+
| `bound_attribute` | Update targets an attribute listed in `attrs.metadata.bindings` | Resolve the binding upstream, or pass `allow_bound_writes: true` |
|
|
530
|
+
| `batch_too_large` | `update_blocks` payload exceeds `MAX_BATCH_SIZE` (50) | Split into multiple batches |
|
|
531
|
+
| `batch_validation_failed` | One or more items in a batch failed validation; the whole call was rejected before any disk write | Inspect `data.errors[]` for the per-item codes and retry valid items |
|
|
532
|
+
| `empty_batch` | `update_blocks` called with `updates: []` | Skip the call |
|
|
533
|
+
| `block_depth_exceeded` | Tree depth would exceed 32 levels after the write | Flatten the block structure |
|
|
534
|
+
| `invalid_path` / `invalid_destination` / `invalid_target` | Path array is not non-negative integers, or doesn't address a block | Re-fetch and use a fresh path |
|
|
535
|
+
| `invalid_ref` | Ref isn't a valid `blk_XXXXXXXX` shape | Re-fetch and use a returned ref |
|
|
536
|
+
| `ref_not_top_level` | Operation requires a top-level block (e.g. `replace_block_range`) but ref points into a nested block | Pass the top-level ancestor's ref |
|
|
537
|
+
| `invalid_op` | `edit_block_tree` op not in the 9-op enum | Use one of `update-attrs`, `update-html`, `replace-block`, `remove-block`, `wrap-in-group`, `unwrap-group`, `insert-child`, `duplicate`, `move` |
|
|
538
|
+
| `invalid_block` | Block definition is malformed (missing `name`, name not registered, etc.) | Check the block name with `list_block_types` |
|
|
539
|
+
| `missing_attributes` / `missing_html` / `missing_block` / `missing_blocks` / `missing_destination` / `missing_target` / `missing_data` / `missing_lookup` / `missing_file` / `missing_title` | Required field omitted | Include the field |
|
|
540
|
+
| `invalid_count` / `invalid_range` / `invalid_index` / `invalid_limit` / `invalid_cursor` | Numeric arg out of range or wrong shape | See `message` for the expected bounds |
|
|
541
|
+
| `invalid_updates` | `update_blocks` updates array malformed | Re-shape per the `update_blocks` schema |
|
|
542
|
+
| `invalid_post_type` / `invalid_status` / `invalid_taxonomy` / `invalid_term` / `invalid_author` / `invalid_parent` / `invalid_featured_media` | `create_post` / `update_post` field validation | Check the value against the relevant WordPress registry |
|
|
543
|
+
| `cycle_parent` | Parent assignment would create a hierarchy loop | Pick a different parent |
|
|
544
|
+
| `mixed_trash_payload` | `update_post` mixed `status: trash` with other fields | Trash first, then update separately |
|
|
545
|
+
| `invalid_if_match` | Header is present but not a positive integer | Send `If-Match: <revision_id>` |
|
|
546
|
+
| `revision_mismatch` | Internal — captured revision ID didn't match before save | Retry; if persistent, file an issue |
|
|
547
|
+
| `no_inner_blocks` | `unwrap-group` on a block that has none | Either remove the wrapper differently or insert children first |
|
|
548
|
+
| `no_file` / `missing_file` | `upload_media` got no multipart payload | Send a `file` field, `url`, or `data_base64` |
|
|
549
|
+
| `multiple_inputs` / `mutually_exclusive` | `upload_media` got more than one of `file` / `url` / `data_base64` | Send exactly one |
|
|
550
|
+
| `invalid_filename` / `disallowed_mime` / `file_too_large` / `invalid_base64` / `invalid_url` | `upload_media` payload rejected | See `message` for which gate failed |
|
|
551
|
+
| `upload_error` | WordPress' upload handler returned an error | Inspect `message` |
|
|
552
|
+
| `empty_pattern` | `insert_pattern` got a pattern with no parsed blocks | Pick a different pattern |
|
|
553
|
+
| `invalid_body` | Request JSON body could not be parsed | Validate JSON shape |
|
|
554
|
+
|
|
555
|
+
### Rate limit (HTTP 429)
|
|
556
|
+
|
|
557
|
+
| Code | When it fires | How to recover |
|
|
558
|
+
|---|---|---|
|
|
559
|
+
| `rate_limit_exceeded` | Per-post write budget exhausted (10 writes/min, or 2 full-rewrites/min) | Wait up to 60 s and retry; consider batching with `update_blocks` |
|
|
560
|
+
| `scan_rate_limited` | Settings-page scan triggered too frequently | Wait; this affects admin-side scans only |
|
|
561
|
+
|
|
562
|
+
### Upstream (HTTP 502)
|
|
563
|
+
|
|
564
|
+
| Code | When it fires | How to recover |
|
|
565
|
+
|---|---|---|
|
|
566
|
+
| `url_fetch_failed` | `upload_media` URL sideload failed at HTTP layer (DNS, TLS, non-2xx, or SSRF block) | Verify the URL is publicly reachable and not in a blocked IP range |
|
|
567
|
+
|
|
568
|
+
### Server error (HTTP 500)
|
|
569
|
+
|
|
570
|
+
| Code | When it fires | How to recover |
|
|
571
|
+
|---|---|---|
|
|
572
|
+
| `internal_error` | Uncaught exception bubbled up to the REST envelope | File an issue with the message + reproduction |
|
|
573
|
+
| `wp_insert_post_failed` | `wp_insert_post` returned a `WP_Error` | Inspect `message`; often a missing required field at the DB layer |
|
|
574
|
+
| `duplicate_failed` | `edit_block_tree` op `duplicate` could not JSON-clone the block (only fires on truly malformed input — resources, invalid UTF-8) | File an issue with the block definition |
|
|
575
|
+
| `sideload_failed` | `upload_media` URL passed SSRF + HTTP layers but `media_handle_sideload` failed | Inspect `message`; often disk-quota or MIME registration |
|
|
576
|
+
| `attachment_missing` | `upload_media` created the attachment but couldn't find it for metadata | File an issue |
|
|
577
|
+
| `trash_failed` / `untrash_failed` | `wp_trash_post` / `wp_untrash_post` returned `false` | Retry; if persistent, check for filter conflicts |
|
|
578
|
+
|
|
579
|
+
## Translations
|
|
580
|
+
|
|
581
|
+
The WordPress plugin ships with translations for the 20 most-used WordPress locales: Arabic, Chinese (simplified), Czech, Danish, Dutch, Finnish, French, German, Hungarian, Indonesian, Italian, Japanese, Korean, Polish, Portuguese (BR), Romanian, Russian, Spanish, Swedish, Turkish.
|
|
582
|
+
|
|
583
|
+
The translations were generated with [Potomatic](https://www.gravitykit.com/potomatic/) — an open-source CLI for AI-translating `.pot` files at scale.
|
|
584
|
+
|
|
585
|
+
## License
|
|
586
|
+
|
|
587
|
+
- WordPress plugin: GPL-2.0-or-later
|
|
588
|
+
- MCP server: MIT
|
|
589
|
+
|
|
590
|
+
## Contributing
|
|
591
|
+
|
|
592
|
+
Issues and PRs welcome at [github.com/GravityKit/block-mcp](https://github.com/GravityKit/block-mcp). Run the test suites before submitting; new mutations should ship with PHPUnit + Vitest coverage.
|