Skip to content

Release 2.2.0 - #64

Merged
zackkatz merged 23 commits into
mainfrom
develop
Jul 24, 2026
Merged

Release 2.2.0#64
zackkatz merged 23 commits into
mainfrom
develop

Conversation

@zackkatz

@zackkatz zackkatz commented Jul 23, 2026

Copy link
Copy Markdown
Member

Release 2.2.0. Merging to main triggers CircleCI's build_package_release (tags v2.2.0, creates the GitHub release from the 2.2.0 readme.txt changelog, announces) and npm-publish (publishes @gravitykit/block-mcp@2.2.0).

What ships (net vs 2.1.0)

Added

  • Read-only FSE template tools (list_templates, get_template) — see a block theme's templates/parts and whether a DB override shadows a theme file.
  • Gated template editing (update_template, reset_template) — off by default (option gk_block_api_template_edits + filter gk/block-mcp/templates/allow-edits); edits create a revertable DB override.
  • create_pattern — synced or unsynced reusable patterns.
  • list_binding_sources — registered block binding sources.
  • All new tools are also registered as WordPress Abilities for the MCP Adapter, at REST parity on permissions.

Improved

  • list_block_types returns styles, parent, ancestor, allowed_blocks, and optional include_supports.
  • list_patterns accepts a category filter and returns the registered categories.

New REST routes (gk-block-api/v1)

GET /templates, GET /template, POST /template, POST /template/reset, GET /binding-sources, POST /patterns.

Provenance

15 commits (PRs #52#63 plus the release bump). Reviewed by Codex (5 P1s fixed) and CodeRabbit (2 rounds); validated live on staging.gravitykit.com. All new functionality is TDD'd; the version bump is verified green (869 vitest, 4 PHP suites, byte-identical bundle baked at 2.2.0).

https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

💾 Build file (4241d5d).

Summary by CodeRabbit

  • New Features
    • Added MCP tools to list/view/update/reset FSE block-theme templates and template parts.
    • Added create_pattern (synced/unsynced), including pattern-category filtering and returned category metadata.
    • Added block binding source discovery.
    • Enhanced block-type discovery with style/nesting enrichment and optional include_supports.
  • Improvements
    • Added gated template editing via a dedicated permission toggle, with database override semantics.
    • Improved write compatibility by replaying blocked REST edit verbs via HTTP method override on HTTP 405.
    • Updated pattern/category discovery output shape.
  • Documentation
    • Updated docs, guidance, and test instructions for the new tools and inputs.
  • Release
    • Version updated to 2.2.0.

zackkatz added 15 commits July 21, 2026 00:59
…onstraints, supports (#53)

* feat(block-mcp): expose style variations + nesting constraints in /block-types

format_block_type() now merges block.json `styles` with anything
registered via register_block_style(), dedupes by name, and reports
`parent`/`ancestor`/`allowed_blocks` nesting constraints. The full
`supports` object is opt-in via a new `include_supports` request arg
(default false) threaded through Block_Registry::get_block_types() and
the /block-types REST route.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(block-mcp): PHPUnit coverage for /block-types styles + nesting fields

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* feat(block-mcp): thread include_supports through list_block_types tool

client.getBlockTypes() and the list_block_types tool now accept
include_supports; BlockType grows styles/parent/ancestor/allowed_blocks/
supports fields; agent-guide notes to check styles before is-style-*
classNames and respect nesting constraints.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(block-mcp): TS coverage for include_supports forwarding + new field passthrough

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* chore(block-mcp): regenerate abilities manifest + bundle; README row

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
…gory vocabulary (#54)

* feat(block-mcp): /patterns response includes registered pattern categories

GET /patterns now returns a top-level `categories` array (from
WP_Block_Pattern_Categories_Registry), the vocabulary the existing
`category` filter arg matches against.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(block-mcp): PHPUnit coverage for /patterns category filter + categories vocabulary

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* feat(block-mcp): list_patterns exposes category filter + categories vocabulary

client.getPatterns() return type gains `categories`; the list_patterns
tool schema gains `category`, forwards it, and passes the response's
`categories` array through. Description explains the semantic split:
registered patterns match declared pattern categories, synced patterns
match the block categories used in their content.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(block-mcp): TS coverage for list_patterns category forwarding + categories passthrough

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* chore(block-mcp): regenerate abilities manifest + bundle; README row

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
…emplate (#52)

* feat(templates): add Template_Manager and read-only FSE template REST routes

New Template_Manager wraps get_block_templates()/get_block_template() to
list/read a block theme's templates and template parts (BLOCK-29). Adds
Block_Reader::format_content_blocks() (delegated through Block_CRUD) to
format raw template markup the same way get_page_blocks() formats a post,
without persisting gk_refs (there's no post to write them into). New
GET /templates and GET /template routes on REST_Controller, wired through
build_block_services(). REST_Controller now takes a Template_Manager
constructor argument; updated the three test call sites accordingly.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(templates): cover Template_Manager and the template REST routes

Unit coverage for get_templates()/get_template() (theme-file listing,
DB-override shadowing, area filtering, classic-theme fallback, title-as-array
defense, not-found/missing-id/invalid-type errors) plus REST dispatch tests
for permission checks and the id-as-query-arg contract. Both files work
around a wp-phpunit quirk where switch_theme() to any fixture theme other
than WP_DEFAULT_THEME resolves against the wrong theme root when only one
theme directory is registered.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* feat(templates): add list_templates/get_template MCP tools

Wires the new REST routes into the npm MCP server: WordPressBlockClient
gains getTemplates()/getTemplate(), src/types.ts gains the Template*
interfaces, and src/tools/templates.ts exposes list_templates + get_template
(read-only annotations, matching discovery.ts). Registered in src/index.ts's
tool aggregation + dispatch table. Documents the new tools in README.md and
adds a "Templates" section to the agent guide. Rebuilds dist/index.cjs and
its copy into the WordPress plugin (assets/mcp-server/index.cjs).

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
…ate (#56)

* feat(templates): add gated update_template/reset_template writes

New Template_Manager::update_template()/reset_template() (BLOCK-33), gated by
option gk_block_api_template_edits (default off, filter
gk/block-mcp/templates/allow-edits), modeled line-for-line on
Post_Manager::ALLOW_TRASH_OPTION/trashing_enabled(). The gate is enforced
both in the new REST permission callback (check_template_edit_permissions:
toggle ON and edit_posts or edit_theme_options) and again inside the service
methods, so a direct caller gets the same 403 a disabled route would.

update_template() resolves the template via get_block_template(); a
theme-file-only id gets a database override created the way the Site Editor
does (wp_insert_post + the mandatory wp_theme term, plus
wp_template_part_area for parts), then the new content is applied through
the standard Block_Writer pipeline (replace_all_blocks for `blocks` -- full
registry/tier/dual-storage validation -- or save_post_content for `content`,
wp_kses_post-sanitized). A content-apply failure on a freshly-created
override rolls it back so a rejected write never leaves an empty shell.
reset_template() deletes the override outright.

New POST /template + POST /template/reset routes. Settings page gets a
"Template editing" checkbox next to the trash/uploads/abilities toggles.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(templates): cover the gated template-write surface

New TemplateManagerWriteTest.php: the toggle gate (default off, both
directions via the filter), override creation (wp_theme +
wp_template_part_area terms), content vs. blocks input, idempotent reuse of
an existing override on a second write, rollback of a freshly-created
override when a legacy block is rejected, reset (deletes the override,
get_template reverts to the theme file), classic-theme and
content/blocks-mutual-exclusivity guards, and a probe test confirming
update_block works against a template override's wp_id like any other post.

TemplatesRestTest.php gains REST-dispatch coverage for POST /template and
POST /template/reset: 403 when the toggle is off, success for an editor
(edit_posts) once it's on, 403 for a subscriber even with the toggle on,
success via edit_theme_options alone (the "self" connection path), and
reset deleting the override through the full route.

The legacy-block-rejection REST test calls the controller handler directly
rather than through rest_get_server()->dispatch(): the plugin's own
rest_api_init wires a second, production REST_Controller onto the global
REST server, and that instance's Preferences lazily caches namespace scores
on first read, so a per-test option seed after that first read never
reaches it. Permission/sanitization aren't under test in that one case (both
are covered elsewhere in this file via real dispatch), so a direct call is
safe -- matches AGENTS.md's documented "direct handler call is not dispatch"
caveat.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* feat(templates): add update_template/reset_template MCP tools

Wires the new gated write routes into the npm MCP server: WordPressBlockClient
gains updateTemplate()/resetTemplate(), src/types.ts gains
UpdateTemplateRequest/Response + ResetTemplateResponse, and
src/tools/templates.ts exposes update_template + reset_template
(destructiveHint: true, content/blocks mutual-exclusivity validated
client-side before the request goes out). Documents the gate and both tools
in README.md ("Editing templates") and the agent guide. Rebuilds
dist/index.cjs and its copy into the WordPress plugin.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
* feat(block-mcp): add GET /binding-sources — list registered block bindings sources

Block_Registry::get_binding_sources() wraps
get_all_registered_block_bindings_sources() behind a function_exists
guard (Block Bindings requires WP 6.5+); below that it returns an
empty sources array with an explanatory note. Each source reports
{name, label, uses_context?}.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(block-mcp): PHPUnit coverage for /binding-sources; bump tool-count fixtures to 28

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* feat(block-mcp): add list_binding_sources MCP tool

client.getBindingSources() → GET /binding-sources; new no-arg
list_binding_sources discovery tool; agent-guide notes to check
registered sources before wiring metadata.bindings.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(block-mcp): TS coverage for list_binding_sources dispatch + passthrough

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* chore(block-mcp): regenerate abilities manifest + bundle; README row

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* chore(block-mcp): retrigger CI (prior run dropped during GitHub Actions runner incident)

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* fix(block-mcp): abilities-manifest generator was missing TEMPLATE_TOOLS

Discovered while rebasing BLOCK-32 onto develop: the read-only
list_templates/get_template tools (merged in #52) are wired into
src/index.ts but scripts/export-abilities-manifest.mjs never imported
TEMPLATE_TOOLS, so they were silently absent from the WordPress
Abilities API surface (register_abilities() only reads the manifest).
Both tools are readOnlyHint:true so they classify correctly as `read`
permission with no further generator changes.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(block-mcp): bump tool-count fixtures to 30 after rebase onto templates work

develop now includes #52 (list_templates/get_template). Combined with
this branch's list_binding_sources and the manifest-generator fix, the
full tool count is 30 (27 baseline + 2 template tools + 1 new).

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* chore(block-mcp): regenerate abilities manifest + bundle post-rebase

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(block-mcp): bump tool-count fixtures to 32 after rebase onto #56

develop now also includes update_template/reset_template (#56, same
TEMPLATE_TOOLS array my manifest-generator fix already covers).

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* chore(block-mcp): regenerate abilities manifest + bundle post-rebase (32 tools)

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
…ies API (#58)

* test(abilities): templates group registration + gate parity [red]

Writes the failing tests for BLOCK-34 first, before any production code
change. Confirmed red against current origin/develop, where TEMPLATE_TOOLS is
already wired into scripts/export-abilities-manifest.mjs's buildManifest()
(the manifest already carries all 4 templates tools at 32 total / 29
non-yoast -- registration is manifest-driven and doesn't depend on
Tool_Executor actually knowing how to run a tool), but two real gaps remain:

1. update_template/reset_template map to the manifest's default 'edit_post'
   permission instead of a gated one -- Abilities_Registry's permission
   check never consults Template_Manager::edits_enabled(), so the write gate
   the REST routes enforce (POST /template, POST /template/reset) doesn't
   apply on the Abilities/MCP-Adapter path at all.
2. Tool_Executor::execute() has no case for any of the 4 template tool
   names, so every one of them 400s with "Unknown Block MCP tool" the
   moment an agent actually tries to call it, gate aside.

New tests/Abilities/TemplateAbilitiesTest.php covers both: ability
registration presence, list_templates/get_template execution, and the write
pair's gate parity with their REST twins -- toggle off denies (editor,
subscriber), the gk/block-mcp/templates/allow-edits filter forcing it off
despite the stored option, toggle on + edit_posts succeeds and persists
(round-tripped via get-template), toggle on + no capability still denies,
edit_theme_options alone (the "self" connection path) succeeds, and
reset-template's full round trip. Extends AbilitiesRegistryTest's existing
readonly/destructive annotation tests to cover the templates group (these
already pass -- the annotations came through correctly with the manifest
wiring) and adds the four template tool/ability names to the existing
manifest-count/ability-ids assertions without changing the counts (both
already account for the templates group).

tests/abilities-manifest.test.ts gains three assertions: the templates
group is present (already true), list_templates/get_template map to 'read'
(already true), and update_template/reset_template map to a new
'template_edit' permission distinct from 'edit_post' (not yet true --
genuinely red).

Confirmed red:

  $ vendor/bin/phpunit -c tests/phpunit.xml tests/Abilities/
  Tests: 153, Assertions: 360, Failures: 8.
  (list_templates/get_template/update_template: "Unknown Block MCP tool";
   update_template/reset_template gate-off + filter-off tests: got
   'unknown_tool' instead of 'ability_invalid_permissions' -- the wrong
   'edit_post' permission branch let an editor through, then execution
   404'd instead of the permission callback denying it;
   edit_theme_options-alone test: "does not have necessary permission" --
   edit_post's per-post branch doesn't recognize edit_theme_options at all)

  $ npm test -- tests/abilities-manifest.test.ts
  Tests  1 failed | 4 passed (5)
  (update_template/reset_template permission: expected 'template_edit',
   received 'edit_post')

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* feat(abilities): wire templates group execution + gate parity [green]

Makes the BLOCK-34 red tests pass.

scripts/export-abilities-manifest.mjs: new TEMPLATE_EDIT permission bucket
(update_template, reset_template) mapped to a 'template_edit' permission
key, checked before the read/edit_post fallbacks in permissionFor() so
these two tools stop resolving to the ungated default 'edit_post' branch.
Regenerated tools.manifest.json (still 32 tools -- TEMPLATE_TOOLS was
already wired into buildManifest()'s tool list; only the two permission
values change).

Abilities_Registry::check_tool_permission() gets a 'template_edit' case
that delegates to REST_Controller::check_template_edit_permissions() --
the exact same method POST /template and POST /template/reset use as their
permission_callback -- so the toggle (Template_Manager::edits_enabled())
and its capability half (edit_posts or edit_theme_options) are enforced
identically on both surfaces. No gate logic is re-implemented here.

Tool_Executor::execute() gains dispatch cases + execute_*() methods for
list_templates, get_template, update_template, and reset_template, each
delegating to the matching REST_Controller handler via call_controller()
(the existing hand-built WP_REST_Request + set_url_params()/set_body_params()
pattern every other ability already uses).

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* docs(agents): add the Abilities-wiring step to the Add-an-MCP-tool recipe

A new tool group file needs an explicit import + spread in
scripts/export-abilities-manifest.mjs's buildManifest() (it only exports
groups it's told about), a permission mapping in permissionFor() (reusing
an existing bucket or adding a new gated one, mirroring the REST route's
own permission callback -- never re-implementing the gate), and a
Tool_Executor::execute() dispatch case -- three steps BLOCK-29/33 skipped
for the templates group and BLOCK-34 had to backfill.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
…atus control (#57)

* test(patterns): create_pattern sync-status + XOR + cap checks [red]

Failing-first PHPUnit coverage for POST /patterns (create_pattern),
written against the not-yet-implemented REST_Controller::create_pattern()
and check_create_pattern_permissions(). All 7 assertions currently
error with "Call to undefined method" — confirmed red before
implementation lands in the next commit(s).

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(patterns): create_pattern XOR validation + dispatch + response passthrough [red]

Failing-first Vitest coverage for the not-yet-implemented create_pattern
tool (handlePatternTool has no case for it yet). All 8 assertions
currently fail with "Unknown pattern tool: create_pattern" — confirmed
red before implementation lands in the next commit(s). Adds a
createPattern mock stub to the shared mock client as test scaffolding.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* feat(patterns): implement create_pattern [green]

Pattern_Manager::create_pattern() creates a wp_block post: structured
blocks go through Block_CRUD::build_block_from_def() (the same
registry/tier/dual-storage validation create_post uses), raw content
through wp_kses_post(). sync_status defaults to synced (meta absent);
'unsynced' sets core's own wp_pattern_sync_status meta key.
format_synced_pattern() now reports sync_status too.

POST /patterns registers CREATABLE alongside the existing GET, gated
by check_create_pattern_permissions() (edit_posts + the wp_block post
type's create_posts cap, which maps to publish_posts).

Pattern_Manager's constructor now takes a Block_CRUD; gk-block-mcp.php
reorders construction so block_crud is built before pattern_manager.

CreatePatternTest and the create_pattern Vitest suite (committed
red in prior commits) now pass.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(patterns): update Pattern_Manager call sites for the new Block_CRUD param

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* feat(patterns): create_pattern MCP tool [green]

client.createPattern() → POST /patterns; new create_pattern tool in
PATTERN_TOOLS validates title + the content/blocks XOR client-side
(mirroring create_post), defaults sync_status to synced and status to
publish, and forwards structured blocks via the shared BLOCK_INPUT_SCHEMA.
New CreatePatternRequest/CreatePatternResponse types; Pattern gains an
optional sync_status field.

The create_pattern Vitest suite (committed red in a prior commit) now
passes.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* test(patterns): bump tool-count fixtures to 33 for create_pattern

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* docs(patterns): README row + agent-guide sentence for create_pattern; regenerate manifest + bundle

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
…_Executor (#59)

* test(abilities): list_binding_sources + create_pattern execution/gate parity [red]

Writes the failing tests for BLOCK-35 first, before any production code
change. Same class of gap Agent A flagged after #57/#55 merged and I fixed
for the templates group in #58: both tools are already in the manifest and
register as abilities (registration is manifest-driven, independent of
Tool_Executor), but neither has a Tool_Executor::execute() case, so both
400 "Unknown Block MCP tool" over the Abilities/MCP-Adapter transport
regardless of the caller's permissions.

New tests/Abilities/BindingSourcesAbilityTest.php: registration presence,
execution returns the {sources:[...]} shape, subscriber denial (read
permission parity with the rest of the discovery group), readonly
annotation.

New tests/Abilities/CreatePatternAbilityTest.php: registration presence,
execution creates a real wp_block post for an editor, subscriber denial
(base edit_posts check), and the actual gate-parity bug this issue exists
to fix — a Contributor (has edit_posts, lacks publish_posts) must be denied
by the ability exactly as REST_Controller::check_create_pattern_permissions()
denies the identical request over REST, because that permission callback
checks wp_block's create_posts capability (-> publish_posts) on top of the
base edit_posts check. A manifest permission of plain 'edit_post' would let
this actor through the ability while REST denies them. Also pins
check_create_pattern_permissions() denying the same Contributor directly,
and the create-pattern annotation (not readonly, not destructive).

tests/abilities-manifest.test.ts gains two assertions: list_binding_sources
already maps to 'read' (confirms no TS-side change needed there — the gap
is PHP-execution-only for this tool) and create_pattern must map to a new
'create_pattern' permission distinct from 'edit_post' (genuinely red).

Confirmed red:

  $ vendor/bin/phpunit -c tests/phpunit.xml tests/Abilities/
  Tests: 163, Assertions: 403, Failures: 3.
  (list_binding_sources execution: "Unknown Block MCP tool";
   create_pattern execution: "Unknown Block MCP tool";
   create_pattern Contributor-denial: got 'unknown_tool' instead of
   'ability_invalid_permissions' -- the wrong 'edit_post' permission branch
   let the Contributor through, then execution 400'd instead of the
   permission callback denying it)

  $ npm test -- tests/abilities-manifest.test.ts
  Tests  1 failed | 6 passed (7)
  (create_pattern permission: expected 'create_pattern', received 'edit_post';
   list_binding_sources permission already correctly 'read')

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* feat(abilities): wire list_binding_sources + create_pattern execution/gate parity [green]

Makes the BLOCK-35 red tests pass.

scripts/export-abilities-manifest.mjs: new CREATE_PATTERN permission bucket
mapping create_pattern to a 'create_pattern' permission key (checked before
the edit_post fallback). list_binding_sources needed no change -- its
readOnlyHint annotation already resolved to 'read'. Regenerated
tools.manifest.json (still 33 tools; only create_pattern's permission
value changes).

Abilities_Registry::check_tool_permission() gets a 'create_pattern' case
that delegates to REST_Controller::check_create_pattern_permissions() --
the same dedicated callback POST /patterns uses, which checks edit_posts
AND wp_block's create_posts capability (publish_posts). No gate logic is
re-implemented.

Tool_Executor::execute() gains dispatch cases + execute_list_binding_sources()
(delegates to REST_Controller::get_binding_sources()) and
execute_create_pattern() (delegates to REST_Controller::create_pattern(),
passing input through as the JSON body -- Pattern_Manager::create_pattern()
already validates title/content-blocks-XOR/sync_status/status, so no
duplicate validation belongs here), both via the existing call_controller()
pattern.

Also fixes a bug in the red commit's own test: create_pattern's response
key is `pattern_id`, not `id` (Pattern_Manager::create_pattern()'s actual
return shape) -- caught by an "Undefined array key" error on the first
green run, not a silent false pass.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
* test(codex-review): pin all 5 confirmed P1 findings [red]

Verified each of Codex's adversarial-review findings against source before
writing anything (per the review request); all 5 confirmed real, no false
positives:

1. Template_Manager::update_template() checks wp_insert_post()'s error but
   silently discards both wp_set_object_terms() calls that follow it
   (class-template-manager.php:316, :320). A pre_insert_term failure (or any
   other wp_set_object_terms() failure) leaves a published wp_template/
   wp_template_part row with no wp_theme term -- orphaned: get_block_templates()
   can never find it again, yet it lingers in the database with whatever
   content the call went on to write, and the caller is told success:true.

2. Pattern_Manager::create_pattern() checks `'' === $title` on the RAW
   value before sanitize_text_field() runs (class-pattern-manager.php:747-748,
   applied only later at :817). A whitespace-only or pure-markup title
   (e.g. "<script></script>") passes the raw check and collapses to an
   empty post_title at insert time.

3. create_pattern() always returns a `core/block` synced-reference
   snippet (class-pattern-manager.php:858), even for sync_status:"unsynced" --
   following it would re-link content the caller explicitly asked to keep
   non-propagating.

4. Tool_Executor::execute_list_block_types() never reads `include_supports`
   from $input, so ability callers always get stripped block types
   regardless of what they pass (class-tool-executor.php:150-159).

5. Tool_Executor::execute_list_patterns() never reads `category` and never
   forwards the REST response's `categories` vocabulary, so ability
   results diverge from REST/npm-MCP (class-tool-executor.php:191-225).

New tests/Abilities/ToolExecutorParityAuditTest.php: a structural audit --
every property a manifest tool declares in its input_schema must appear,
verbatim, in that tool's Tool_Executor::execute_<name>() method body.
Running it against the unmodified codebase caught findings #4 and #5
exactly, plus a third divergence it correctly surfaced and I investigated
separately: update_block's `block_name` is declared but never referenced.
That one is NOT a wiring gap -- block_name selects which npm-MCP-server-side
enricher (src/enrichers.ts) to run entirely client-side before only the
already-enriched attributes/innerHTML are sent over the wire; the REST
endpoint never accepts a block_name parameter, so Tool_Executor has nothing
to forward it to. Documented as a real, separate, pre-existing gap (the
Abilities path gets no automatic computed-field derivation the way npm-MCP
callers do) via an explicit, commented exemption rather than silently
dropped or expanded into a PHP-side enrichment port.

New tests/Abilities/ListToolsAbilityParityTest.php: behavioral proof for
findings #4/#5 specifically (the structural audit proves the argument name
is *referenced*; these prove it changes the actual response).

tests/REST/CreatePatternTest.php gains title-sanitization tests (#2, plus a
control case: real text with incidental whitespace must still succeed) and
reference/insert_hint tests (#3, plus a control case pinning the synced
response shape is unchanged).

tests/Templates/TemplateManagerWriteTest.php gains two rollback tests (#1):
a pre_insert_term filter forces wp_theme term assignment to fail for a
wp_template write, and a second test isolates the wp_template_part_area
assignment specifically (by letting wp_theme succeed and only failing the
area term) so both checks this issue asks for are independently proven.

Confirmed red (full suite, not just the new files):

  $ composer test
  Tests: 1425, Assertions: 18456, Failures: 8.

  1) CreatePatternTest::test_whitespace_only_title_is_rejected
  2) CreatePatternTest::test_markup_only_title_that_sanitizes_to_empty_is_rejected
  3) CreatePatternTest::test_unsynced_pattern_does_not_return_synced_reference
  4) ListToolsAbilityParityTest::test_list_block_types_ability_forwards_include_supports
  5) ListToolsAbilityParityTest::test_list_patterns_ability_forwards_category_filter_and_includes_categories
  6) ToolExecutorParityAuditTest::test_every_manifest_input_property_is_referenced_by_its_tool_executor_method
  7) TemplateManagerWriteTest::test_update_template_rolls_back_new_override_when_wp_theme_term_assignment_fails
  8) TemplateManagerWriteTest::test_update_template_rolls_back_new_override_when_area_term_assignment_fails

  $ npm test
  Test Files  55 passed (55)  -- no TS-side change needed for red yet;
  types/tool description updates for finding #3 land in the green commit.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* fix(codex-review): resolve all 5 confirmed P1 findings [green]

Codex review of the merged range 602ae77...b089815 flagged five
findings; all five verified genuine against source and fixed here:

1. Template_Manager::update_template() now checks the WP_Error return
   of both wp_set_object_terms() calls when creating a new override,
   deleting the freshly-inserted post and returning the error on
   failure instead of leaving an untraceable orphan post behind.

2. Pattern_Manager::create_pattern() now sanitizes `title` before the
   emptiness check, so a whitespace- or markup-only title that
   collapses to '' under sanitize_text_field() is rejected instead of
   silently creating a nameless pattern.

3. create_pattern()'s response now omits `reference` for an unsynced
   pattern (core/block is a live, propagating reference — wrong for a
   one-off) and instead returns `insert_hint` pointing callers at
   insert_pattern's synced:false path. TS types (CreatePatternResponse)
   and the create_pattern tool description are updated to match; the
   synced case is unchanged (backward compatible).

4. Tool_Executor::execute_list_block_types() now forwards
   `include_supports` on the Abilities/MCP-Adapter path, matching REST
   and the npm MCP server.

5. Tool_Executor::execute_list_patterns() now forwards `category` and
   includes `categories` in its response, matching REST and the npm
   MCP server.

Findings 4 and 5 are the same REST-vs-Abilities parity bug class as
prior fixes; ToolExecutorParityAuditTest (added in the red commit)
statically compares every manifest-declared input property against
each tool's execute_*() method body via Reflection, so a newly added
argument that isn't forwarded on the Abilities path now fails CI
instead of shipping silently. It documents two narrow, deliberate
exemptions: tools that forward $input wholesale, and update_block's
`block_name` (a client-side-only enrichment selector with no REST
equivalent — a real, separate gap, not this bug class).

All five findings are pinned by tests (see the red commit); none
required a manual-only verification note.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
…edit real templates — BLOCK-37 (#61)

* test(templates): pin BLOCK-37 hybrid-theme template regression [red]

staging.gravitykit.com's "gravitykit" theme is a hybrid: wp_is_block_theme()
is false (no templates/index.html), but get_block_templates() finds real
templates and parts via the templates/ and parts/ folders directly (1
template, 25 parts, verified live). Every template tool method gates on
`! wp_is_block_theme()` instead of whether the id/type actually resolves,
so it hides those templates from list_templates and blocks writes to parts
that genuinely render on the site.

Adds a "hybrid-theme" fixture (real templates/ and parts/ files, no
templates/index.html or block-templates/index.html) plus:
- get_templates() must list a hybrid theme's real templates/parts, no
  misleading "not a block theme" note
- the note must stay absent for a real block theme's genuinely-empty
  result (regression pin — the note isn't "any empty result")
- update_template()/reset_template() must succeed against a hybrid
  theme's resolvable part
- a genuinely classic theme (nothing resolves) must keep the specific,
  actionable classic_theme 400 rather than regressing to a generic
  not_found — pins that the fix doesn't just delete the guard

All 3 hybrid-theme behavioral assertions fail against current code;
the classic-theme regression pin already passes (asserting current
behavior we must not break).

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* fix(templates): gate on template resolution, not wp_is_block_theme() [green]

get_templates() / update_template() / reset_template() all short-circuited
on `! wp_is_block_theme()`, which is only true when a theme ships
templates/index.html (or block-templates/index.html). A "hybrid" theme
can have real, renderable templates and template parts without that
specific file — staging.gravitykit.com's "gravitykit" theme is exactly
this shape (1 template, 25 parts, wp_is_block_theme() false) — and the
guard hid all of them from list_templates while blocking gated writes to
parts that genuinely render on the site.

- get_templates(): always queries get_block_templates(); the "not a
  block theme" note is now attached only when the result is empty AND
  wp_is_block_theme() is false, reworded so it no longer asserts
  nonexistence when the truth is "nothing matched this query."
- update_template() / reset_template(): the wp_is_block_theme() 400 now
  fires only as a fallback when get_block_template( $id, $type ) fails
  to resolve at all AND the theme isn't a block theme — the primary gate
  is resolution, matching get_template()'s existing behavior. A
  genuinely classic theme (nothing resolves) keeps the same specific,
  actionable classic_theme 400 it always returned; a hybrid theme's
  resolvable part now succeeds instead of being rejected up front.

Test infra: fixes a wp-phpunit gotcha the red commit's fixture tripped —
search_theme_directories() memoizes its scan in a function-local static,
so register_theme_directory() alone doesn't make a newly-registered root
visible once anything else has already forced a scan; wp_clean_themes_cache()
does.

TS: list_templates' description no longer says "block theme" as if that
were the qualifying condition.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
* test(coderabbit): pin CodeRabbit follow-up findings across PRs #52-61 [red]

Adds/updates test coverage for the confirmed-genuine CodeRabbit findings
that require a behavior change, plus test-quality fixes (docblocks,
cleanup, portability guards) for the ones that don't. Verified red
against pre-fix code via targeted stash of the implementation changes:

- discovery.ts: list_block_types rejects a non-boolean include_supports
  (e.g. the truthy string "false") instead of silently forwarding it.
- patterns.ts: create_pattern rejects an invalid sync_status/status
  instead of silently defaulting; declares oneOf(blocks, content) in its
  inputSchema so a schema-validating client rejects both-or-neither
  before dispatch, not just at runtime.
- class-template-manager.php: update_template's term-assignment rollback
  reports rollback_failed (distinct from the original cause) when its own
  wp_delete_post() also fails, instead of silently returning the original
  error as if cleanup succeeded.
- class-template-manager.php: a hybrid theme (real content elsewhere) with
  a bad id returns not_found, not classic_theme — classic_theme is now
  reserved for a genuinely classic theme (regression-pinned for both
  update_template and reset_template).

Test-quality only (no corresponding production-code change):
- BlockTypesTest.php: contract docblocks on every test; allowed_blocks
  assertion skipped on WordPress < 6.5 (the property was added in 6.5,
  the plugin's floor is 6.0 — a real portability gap in the test, not
  just a style nit).
- PatternsCategoryTest.php: contract docblocks; pattern-registry cleanup
  moved to try/finally so an assertion failure doesn't leak fixtures into
  later tests.
- BindingSourcesTest.php: contract docblocks; the two tests that assumed
  WordPress 6.5+ now branch on the real function_exists() capability and
  assert the 6.0-6.4 fallback shape too; a negative case added so
  uses_context presence is proven conditional, not unconditional.
- CreatePatternTest.php: the "real text plus whitespace" success case now
  wraps the text in markup too, so over-aggressive sanitization would be
  caught, not just under-aggressive; review-tool attribution stripped
  from section-header comments.
- TemplateManagerTest.php / TemplateManagerWriteTest.php: comments
  trimmed to state current contracts rather than implementation history
  ("the old X" / "Codex review" / "regression"/"proves" framing).

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* fix(coderabbit): resolve CodeRabbit follow-up findings across PRs #52-61 [green]

- discovery.ts: list_block_types validates include_supports is a boolean
  before forwarding, rejecting a non-boolean instead of letting a truthy
  non-boolean (e.g. the string "false") silently flip to true downstream.
- patterns.ts: create_pattern validates sync_status/status against their
  declared enums before casting; declares oneOf(blocks, content) in its
  inputSchema so the "exactly one" contract is enforced structurally, not
  only at runtime.
- coerce.ts: adds isNonEmptyArray()/isNonEmptyString(), the single shared
  definition of "present" for a list/string field. patterns.ts and
  client.ts's independent (if currently identical) copies of the same
  blocks/content emptiness check now both call it, so they can't
  silently drift apart.
- README.md: documents include_supports as opt-in (default false).
- class-rest-controller.php: adds the missing @SInCE 2.2.0 to
  get_binding_sources() and create_pattern() — both new in this range.
- class-template-manager.php:
  - update_template()'s rollback helper (used by both term-assignment
    failures and a content-write failure) now checks its own
    wp_delete_post() return value and reports a distinct rollback_failed
    error — including the orphaned post's ID — when cleanup itself also
    fails, instead of returning the original error as if cleanup
    succeeded.
  - the classic_theme/not_found choice on an unresolved id now also
    checks whether the theme has ANY real template/part, not just
    wp_is_block_theme(): a hybrid theme with content elsewhere gets
    not_found (this id is simply wrong); a genuinely classic theme keeps
    classic_theme (there's nothing here at all). Applies to both
    update_template and reset_template.
  - is_wp_error() checks in the touched code assign a named boolean
    before branching, per the repo's coding standard.
- src/tools/templates.ts: list_templates' description corrected —
  wp_id is present whenever a template is database-backed (an override
  OR a fully custom template with no theme file), not only when an
  override "shadows" a theme file.
- Manifest/bundle regenerated (tool count unchanged at 33) for the
  create_pattern schema/description and list_templates description
  changes.

## Skipped findings (verified against current code)

- PR52 class-template-manager.php:226 — stale. format_template_summary()'s
  wp_id comment already names update_template/reset_template as real,
  implemented tools, not speculative future work.
- PR57 tools.manifest.json:1192 — stale. create_pattern's manifest
  permission already routes through check_create_pattern_permissions
  (BLOCK-35), matching the REST route's cap check exactly.
- PR55 discovery.ts:214 (list_binding_sources enrichment) — the
  codebase's existing enrichment (enrichBlockTypes/enrichPatternList)
  groups/summarizes server-computed classification data (tier, score) it
  already has; binding sources carry no such classification, so
  "guidance" here would mean inventing and maintaining hardcoded prose
  about named third-party sources — a different, heavier kind of
  enrichment than the established pattern, not a small clean addition.
- PR57 src/client.ts:404 (@SInCE on createPattern) — this repo's @SInCE
  convention is PHP-only; zero TS docblocks anywhere in the codebase
  carry one.
- PR55 BindingSourcesTest.php:51 ("add a controllable capability-check
  seam") — would require adding a test-only filter/seam to production
  code (Block_Registry::get_binding_sources()) purely so a modern (6.9)
  test environment can force the pre-6.5 branch; CodeRabbit's own
  severity tag flags this as a "Heavy lift". The existing test already
  documents why it pins the fallback's literal shape instead of
  exercising it live. Its sibling asks (docblocks, branch-on-real-
  capability for the other two tests, negative uses_context case) are
  fixed.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
Three findings from CodeRabbit's re-review of PR #62:

1. TemplateManagerWriteTest.php: a real test-isolation bug —
   test_update_template_reports_rollback_failure_when_delete_also_fails()
   added pre_insert_term/pre_delete_post filters and only removed them
   after update_template() returned. An unexpected exception from that
   call would skip both remove_filter() calls, leaking the filters into
   every later test in the same PHP process (any subsequent
   wp_insert_term()/wp_delete_post() call would be affected). Wrapped the
   call in try/finally so cleanup always runs.

2. Same file: added `@var \WP_Error $result` narrowing docblocks after
   four assertInstanceOf(\WP_Error::class, $result) calls, immediately
   before each result's get_error_code()/get_error_data() calls.
   composer analyze is already clean on develop (tests/* is excluded
   from this repo's own PHPStan paths) — applied because it's a trivial,
   readability-neutral-to-positive one-liner per site, not because our
   gate required it.

3. src/tools/templates.ts: list_templates' description said a non-null
   wp_id "is what makes a template editable via update_template" —
   false. update_template()'s own description (and behavior) already
   documents that a wp_id:null template is still editable: calling it
   creates a new override. Reworded to describe wp_id as existing
   database backing, matching get_template's accurate framing, without
   implying editability requires it.

Manifest/bundle regenerated for the description change (tool count
unchanged at 33).

This is the last dispatch on the CodeRabbit-follow-up thread.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
…blocks

Supersedes the closed PR #51 with a smaller, correct change. Extract the
wp-cli runner into scripts/wp-exec.mjs and:

- Local wp-env branch now invokes argv directly with no shell, so nothing
  in the command reaches a shell for interpretation.
- SSH branch stays shell-based, since WP_CLI_SSH is a trusted operator
  prefix that may carry quoting, `&&`, or an sshpass wrapper.
- Restore fail-loud behavior: spawnSync does not throw on failure the way
  execSync did, so non-zero exits and spawn errors are surfaced explicitly
  instead of returning empty output (which read as "0 posts").

Add tests/wp-exec.test.ts (5 tests) covering both branch shapes, the
exit-status throw, the spawn-error rethrow, and the noise filter. Teeth
verified: reverting the exit-status check turns the two error-handling
tests red.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
Full Site Editing template tools (read + gated write), create_pattern with
sync-status control, list_binding_sources, and richer block-type/pattern
discovery, all also exposed as WordPress Abilities for the MCP Adapter.

Bumps package.json, package-lock.json (both root entries), the plugin header
and GK_BLOCK_MCP_VERSION, and readme.txt Stable tag, adds the 2.2.0 changelog
entry, and rebuilds the tracked server bundle (baked version 2.2.0).

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Version 2.2.0 adds FSE template listing and editing, pattern creation, binding-source discovery, richer block metadata, HTTP method fallback, Abilities API permission routing, centralized WP-CLI execution, and related REST, MCP, and integration tests.

Changes

MCP and REST feature surface

Layer / File(s) Summary
Contracts, client methods, and MCP dispatch
src/types.ts, src/client.ts, src/tools/*, src/index.ts
Adds template and pattern contracts, client methods, tool schemas, validation, dispatch wiring, block-support and binding-source discovery, category forwarding, and HTTP 405 method-override handling.
WordPress discovery, patterns, and templates
wordpress-plugin/gk-block-mcp/includes/*
Adds template management and override persistence, pattern creation, block metadata enrichment, REST routes, permission callbacks, settings, and service wiring.
Abilities permissions and executor parity
scripts/export-abilities-manifest.mjs, wordpress-plugin/gk-block-mcp/includes/abilities/*, wordpress-plugin/gk-block-mcp/tests/Abilities/*
Adds manifest entries, dedicated permission mappings, executor routing, agent capabilities, and permission/parity coverage.
Build tooling and release documentation
scripts/*, README.md, AGENTS.md, wordpress-plugin/*
Centralizes WP-CLI execution and updates guidance, documentation, version metadata, fixtures, and test discovery.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MCPTools
  participant REST
  participant TemplateManager
  participant Database
  Client->>MCPTools: update_template(id, content or blocks)
  MCPTools->>REST: POST /template
  REST->>TemplateManager: update_template()
  TemplateManager->>Database: create or update template override
  Database-->>TemplateManager: override result
  TemplateManager-->>REST: update response
  REST-->>Client: template result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too generic and does not describe the main change in this release. Use a concise, specific title such as "Add FSE templates, pattern creation, and binding source discovery".
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Comment @coderabbitai help to get the list of available commands.

…t edit_posts (#65)

* test(security): pin BLOCK-38 template-write cap gate [red]

check_template_edit_permissions() currently passes on toggle-on AND
(edit_posts OR edit_theme_options). Because the plugin performs the
wp_insert_post()/wp_update_post() on wp_template/wp_template_part itself
regardless of the caller's own caps, with the toggle on ANY
contributor-or-above account (or a leaked low-privilege Application
Password) can rewrite sitewide template chrome — header, footer, 404,
archive, search.

Primary red pin: test_update_template_route_403_for_contributor_even_with_toggle_on
in TemplatesRestTest.php. Confirmed genuinely red against current shipped
code via `git stash` of the (not-yet-written) implementation:

    1) TemplatesRestTest::test_update_template_route_403_for_contributor_even_with_toggle_on
    Failed asserting that 200 is identical to 403.

Companion pins added alongside it:
- TemplatesRestTest.php: an editor (same edit_posts-only shape as a
  contributor) is equally denied; the dedicated agent role, after
  register_role() runs with the toggle on, holds the new
  Agent_Provisioner::TEMPLATE_EDIT_CAP and POST /template succeeds;
  toggling back off and re-running register_role() revokes the cap
  (asserted at the capability level on the role, not just behaviorally)
  and the route 403s again. The existing edit_theme_options-alone success
  test and the reset-template round-trip test are updated to use an
  edit_theme_options actor instead of editor/contributor, since those
  roles no longer have template-write permission — this is intentional,
  not incidental breakage.
- TemplateAbilitiesTest.php: the same editor-denied case is pinned via
  the Abilities path (gk-block-mcp/update-template), proving the shared
  check_template_edit_permissions() callback can't be bypassed there.
  The persist/round-trip tests switch their actor to edit_theme_options
  for the same reason as above.
- AgentProvisionerTest.php: register_role() grants TEMPLATE_EDIT_CAP on
  a fresh role when the toggle is on, and revokes it from an existing
  role when the toggle is later switched off — the one exception to the
  existing "additive only" re-assert loop, proven not to weaken
  test_register_role_strips_forbidden_caps_from_existing_role() (still
  green, unmodified).

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

* fix(security): gate template writes on gk_block_mcp_edit_templates cap [green]

Fixes BLOCK-38, a security-review finding that blocks the develop→main
2.2.0 release (#64). Fable's design: a plugin-owned primitive cap managed
by the existing toggle, not a raw grant of edit_theme_options.

- Agent_Provisioner::TEMPLATE_EDIT_CAP ('gk_block_mcp_edit_templates') is
  a new constant. Deliberately NOT added to forbidden_capabilities() —
  that denylist strips caps this class never grants; this one it grants
  and revokes on purpose.
- register_role() computes the cap's value from
  Template_Manager::edits_enabled() before the existing
  gk/block-mcp/agent/caps filter runs, so operators can still override it.
  On an existing role, it's the one cap both ADDED (already covered by
  the additive re-assert loop) and — new — REMOVED when the toggle is
  off, scoped to only this single cap; the loop otherwise stays additive-
  only, and the forbidden-cap strip loop is untouched. Self-heals on
  every `init` (priority 99); a new `update_option_gk_block_api_template_edits`
  hook re-asserts immediately on settings save so grant/revoke doesn't
  wait for the next request.
- check_template_edit_permissions() now passes on
  current_user_can('gk_block_mcp_edit_templates') OR
  current_user_can('edit_theme_options'), replacing the edit_posts
  clause. The toggle check still runs first (403
  template_edits_disabled before any capability check). Docblock
  rewritten to document the actual vulnerability this closes, not the
  old (incorrect) edit_posts rationale.
- Consent copy (class-settings-page.php) reworded to disclose the
  mechanism: granting the toggle changes what the Block MCP agent
  account itself can do to the theme layer, not just "let the assistant
  edit templates."

Never grants raw edit_theme_options to the agent: that cap also opens
core's /wp/v2/templates, /wp/v2/template-parts, /wp/v2/navigation,
/wp/v2/global-styles, the Customizer, menus, and widgets, none of which
the toggle is meant to reach — and it would be stripped by the
forbidden-capabilities re-assert loop anyway.

Claude-Session: https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
wordpress-plugin/gk-block-mcp/includes/class-agent-provisioner.php (1)

178-184: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Compound has_cap() call inlined in the if condition.

As per coding guidelines, function-call results and compound expressions used in if/while/ternary conditions should be assigned to a named variable first (short-circuit null/isset dereference guards are exempted; this isn't one). $existing->has_cap( self::TEMPLATE_EDIT_CAP ) is called directly inside the &&. Note the identical pattern already exists in the unchanged loop just above this block, so this is consistent with existing style in the file rather than a new regression.

🧹 Proposed fix
-				if ( ! $caps[ self::TEMPLATE_EDIT_CAP ] && $existing->has_cap( self::TEMPLATE_EDIT_CAP ) ) {
+				$has_template_edit_cap = $existing->has_cap( self::TEMPLATE_EDIT_CAP );
+				if ( ! $caps[ self::TEMPLATE_EDIT_CAP ] && $has_template_edit_cap ) {
 					$existing->remove_cap( self::TEMPLATE_EDIT_CAP );
 				}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wordpress-plugin/gk-block-mcp/includes/class-agent-provisioner.php` around
lines 178 - 184, Assign the result of $existing->has_cap(
self::TEMPLATE_EDIT_CAP ) to a named boolean variable before the conditional,
then use that variable with the existing !$caps[...] check in the if statement
while preserving the current remove_cap behavior.

Source: Coding guidelines

src/tools/templates.ts (1)

80-108: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add schema-level oneOf to update_template, matching create_pattern.

create_pattern's inputSchema enforces "exactly one of blocks/content" via oneOf for schema-validating clients (with an explicit comment on why). update_template has the identical mutual-exclusivity contract but only enforces it at runtime in handleTemplateTool — schema-only validators won't catch bad input before dispatch.

♻️ Proposed fix
         blocks: {
           type: 'array',
           description: 'Structured blocks to replace the template with. Mutually exclusive with content.',
           items: BLOCK_INPUT_SCHEMA,
         },
       },
       required: ['id'],
+      oneOf: [{ required: ['id', 'content'] }, { required: ['id', 'blocks'] }],
     },
   },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools/templates.ts` around lines 80 - 108, Update the update_template
inputSchema to add the same schema-level oneOf blocks/content constraint used by
create_pattern, enforcing exactly one field while preserving the existing id and
type properties and runtime validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Line 147: Update the MCP tool inventory description in AGENTS.md so the
patterns group includes both insert_pattern and create_pattern, keeping the
existing grouping and wording unchanged otherwise.

In `@README.md`:
- Around line 340-349: Clarify the template editing documentation near the
per-block tool limitation: explicitly state that theme-file-only templates with
wp_id: null cannot be modified by per-block tools, while templates with numeric
database override IDs can be handled like normal posts by those tools. Preserve
the existing distinction between theme files and database overrides.

In `@wordpress-plugin/AGENTS.md`:
- Line 148: Update the template-edit permission documentation to reflect
authorization through gk_block_mcp_edit_templates while retaining the
edit_theme_options path: change wordpress-plugin/AGENTS.md lines 148-148,
AGENTS.md lines 143-143, and README.md lines 344-344, replacing references to
edit_posts or the “same edit_posts capability” wording with the shipped
capability behavior.

In `@wordpress-plugin/gk-block-mcp/includes/class-rest-controller.php`:
- Around line 356-359: Update the comment describing the template/template-part
POST route to state that access requires the toggle plus
Agent_Provisioner::TEMPLATE_EDIT_CAP or edit_theme_options, matching
check_template_edit_permissions(). Remove the outdated reference to edit_posts
while preserving the route and method context.

In `@wordpress-plugin/gk-block-mcp/includes/class-settings-page.php`:
- Around line 759-760: Update the settings checkbox rendering to pass the
persisted template-edits option value to checked(), rather than the filtered
result from Template_Manager::edits_enabled(). Retain $templates_enabled for the
warning display, and reuse that variable throughout the corresponding settings
section instead of invoking edits_enabled() again.

In `@wordpress-plugin/gk-block-mcp/includes/class-template-manager.php`:
- Around line 127-129: Assign the compound condition in the `wp_template_part`
query-building branch to a descriptive boolean variable, then use that variable
in the `if` statement. Preserve the existing `type` and non-empty `area` checks
and the `sanitize_key` assignment unchanged.

---

Nitpick comments:
In `@src/tools/templates.ts`:
- Around line 80-108: Update the update_template inputSchema to add the same
schema-level oneOf blocks/content constraint used by create_pattern, enforcing
exactly one field while preserving the existing id and type properties and
runtime validation.

In `@wordpress-plugin/gk-block-mcp/includes/class-agent-provisioner.php`:
- Around line 178-184: Assign the result of $existing->has_cap(
self::TEMPLATE_EDIT_CAP ) to a named boolean variable before the conditional,
then use that variable with the existing !$caps[...] check in the if statement
while preserving the current remove_cap behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 96204985-090e-4bf8-830b-ded81d3330d4

📥 Commits

Reviewing files that changed from the base of the PR and between 1b66ad5 and 6da6faf.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (63)
  • AGENTS.md
  • README.md
  • package.json
  • scripts/export-abilities-manifest.mjs
  • scripts/highlight-code-blocks.mjs
  • scripts/wp-exec.mjs
  • src/__tests__/helpers/mock-client.ts
  • src/__tests__/tools/discovery/list_binding_sources.test.ts
  • src/__tests__/tools/discovery/list_block_types.test.ts
  • src/__tests__/tools/discovery/list_patterns.test.ts
  • src/__tests__/tools/patterns/create_pattern.test.ts
  • src/__tests__/tools/templates/templates.test.ts
  • src/__tests__/unit/preferences/enrich-pattern-list.test.ts
  • src/agent-guide.ts
  • src/client.ts
  • src/coerce.ts
  • src/index.ts
  • src/tools/discovery.ts
  • src/tools/patterns.ts
  • src/tools/templates.ts
  • src/types.ts
  • tests/abilities-manifest.test.ts
  • tests/wp-exec.test.ts
  • wordpress-plugin/AGENTS.md
  • wordpress-plugin/gk-block-mcp/assets/mcp-server/index.cjs
  • wordpress-plugin/gk-block-mcp/gk-block-mcp.php
  • wordpress-plugin/gk-block-mcp/includes/abilities/tools.manifest.json
  • wordpress-plugin/gk-block-mcp/includes/class-abilities-registry.php
  • wordpress-plugin/gk-block-mcp/includes/class-agent-provisioner.php
  • wordpress-plugin/gk-block-mcp/includes/class-block-crud.php
  • wordpress-plugin/gk-block-mcp/includes/class-block-reader.php
  • wordpress-plugin/gk-block-mcp/includes/class-block-registry.php
  • wordpress-plugin/gk-block-mcp/includes/class-pattern-manager.php
  • wordpress-plugin/gk-block-mcp/includes/class-rest-controller.php
  • wordpress-plugin/gk-block-mcp/includes/class-settings-page.php
  • wordpress-plugin/gk-block-mcp/includes/class-template-manager.php
  • wordpress-plugin/gk-block-mcp/includes/class-tool-executor.php
  • wordpress-plugin/gk-block-mcp/phpstan-bootstrap.php
  • wordpress-plugin/gk-block-mcp/readme.txt
  • wordpress-plugin/gk-block-mcp/tests/AGENTS.md
  • wordpress-plugin/gk-block-mcp/tests/Abilities/AbilitiesRegistryTest.php
  • wordpress-plugin/gk-block-mcp/tests/Abilities/BindingSourcesAbilityTest.php
  • wordpress-plugin/gk-block-mcp/tests/Abilities/CreatePatternAbilityTest.php
  • wordpress-plugin/gk-block-mcp/tests/Abilities/ListToolsAbilityParityTest.php
  • wordpress-plugin/gk-block-mcp/tests/Abilities/TemplateAbilitiesTest.php
  • wordpress-plugin/gk-block-mcp/tests/Abilities/ToolExecutorParityAuditTest.php
  • wordpress-plugin/gk-block-mcp/tests/Block/PatternReferenceCountsTest.php
  • wordpress-plugin/gk-block-mcp/tests/Connect/AgentProvisionerTest.php
  • wordpress-plugin/gk-block-mcp/tests/REST/BindingSourcesTest.php
  • wordpress-plugin/gk-block-mcp/tests/REST/BlockTypesTest.php
  • wordpress-plugin/gk-block-mcp/tests/REST/CreatePatternTest.php
  • wordpress-plugin/gk-block-mcp/tests/REST/PatternsCategoryTest.php
  • wordpress-plugin/gk-block-mcp/tests/REST/RestSummaryTest.php
  • wordpress-plugin/gk-block-mcp/tests/REST/WriteHandlerErrorEnvelopeTest.php
  • wordpress-plugin/gk-block-mcp/tests/RestControllerTestCase.php
  • wordpress-plugin/gk-block-mcp/tests/Stress/PatternRecursionStressTest.php
  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerTest.php
  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplateManagerWriteTest.php
  • wordpress-plugin/gk-block-mcp/tests/Templates/TemplatesRestTest.php
  • wordpress-plugin/gk-block-mcp/tests/fixtures/themes/hybrid-theme/parts/footer.html
  • wordpress-plugin/gk-block-mcp/tests/fixtures/themes/hybrid-theme/style.css
  • wordpress-plugin/gk-block-mcp/tests/fixtures/themes/hybrid-theme/templates/single.html
  • wordpress-plugin/gk-block-mcp/tests/phpunit.xml

Comment thread AGENTS.md Outdated
Comment thread README.md Outdated
Comment thread wordpress-plugin/AGENTS.md Outdated
Comment thread wordpress-plugin/gk-block-mcp/includes/class-rest-controller.php Outdated
Comment thread wordpress-plugin/gk-block-mcp/includes/class-settings-page.php
Comment thread wordpress-plugin/gk-block-mcp/includes/class-template-manager.php Outdated
@linear-code

linear-code Bot commented Jul 23, 2026

Copy link
Copy Markdown

BLOCK-39

…/PATCH/DELETE (#67)

* fix(client): fall back to X-HTTP-Method-Override when a host 405s PUT/PATCH/DELETE

Some managed hosts (Convesio among them) front WordPress with a WAF that
answers PUT, PATCH, and DELETE with a bare nginx 405 before the request
reaches PHP. GET and POST pass, so reads and create_post kept working while
every editing tool failed: update_block, update_blocks, delete_block,
rewrite_post_blocks, update_post, and the two Yoast writers — eight call
sites in all.

The `?rest_route=` form is what the WAF singles out: the same PUT against
the pretty `/wp-json/` path reaches WordPress fine. Switching to pretty
permalinks was rejected because `?rest_route=` exists precisely so tool
calls don't 404 on plain-permalink sites (see src/rest-url.ts).

WordPress core honours `X-HTTP-Method-Override` on a POST, so a rejected
request is replayed in that shape and the host is remembered for the life of
the client, leaving later writes a single round-trip. The fallback is
adaptive rather than blanket: hosts that accept the real verbs never see an
override header, so the change is inert everywhere it isn't needed.

The editing routes are registered as literal PUT / PATCH / DELETE rather
than the EDITABLE alias, so a plain POST would not match them; the override
header is what carries the intended verb through.

Verified against production: update_post on a docs post returned 405 before
and succeeds after.

Claude-Session: https://claude.ai/code/session_01Njh4D63XnZhsJHMbEU7vYq

* test(client): cover method-override edge cases; document 405 in the README

Broadens the fallback coverage from 6 cases to 16. New cases pin the parts a
naive replay gets wrong: the request body, query parameters, and Basic auth all
have to survive the replay, or a write silently lands empty or unauthenticated.

Also pins the boundaries. The fallback must not engage for a 405 on POST (not an
override verb) or for a non-405 failure on PATCH, since masking a real 403 behind
a replay would hide a permission problem. When a host rejects the replay too, the
error surfaces after exactly two attempts rather than looping, and the override
survives a 429 backoff retry so the two interceptors compose.

Reverting src/client.ts fails 11 of the 16; the 5 that still pass are the ones
asserting the fallback stays dormant (three permissive-host cases, two scope
guards), which is the signature we want.

README: adds a 405 section to Error Codes explaining that the status comes from
the host's firewall rather than the plugin, that the client replays such
requests itself, and what to ask the host for when even the replay is rejected.
Test counts were stale (257/335); verified counts are 885 Vitest and 1,440
PHPUnit.

Claude-Session: https://claude.ai/code/session_01Njh4D63XnZhsJHMbEU7vYq

* fix(client): replay every rejected write, not only the first

Concurrent writes all go out as real verbs, because none of them has seen a 405
yet. Gating the replay on `!useMethodOverride` meant the first rejection set the
flag and replayed while the rest fell through, surfacing a 405 the caller could
do nothing about.

The flag was never what prevented recursion: a replay carries method `post`,
which is not an override verb, so it cannot re-enter the branch. Requests issued
after the flag is set are converted by the request interceptor and likewise
arrive as `post`. Dropping the guard therefore only affects the race window.

Caught by CodeRabbit on #67. The added test fails against the guarded client and
passes here.

Claude-Session: https://claude.ai/code/session_01Njh4D63XnZhsJHMbEU7vYq

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wordpress-plugin/gk-block-mcp/assets/mcp-server/index.cjs`:
- Around line 40106-40116: Update the docblock above useMethodOverride to
replace the claim that only the first write pays for the rejected round-trip;
clarify that concurrent writes may each replay once before the sticky flag is
observed, while only later writes avoid the probe.
- Around line 40163-40167: Refine the fallback in the request handling around
edgeRejectedVerb so it only activates for an identifiable edge-generated 405 or
explicit configuration opt-in, not arbitrary PUT/PATCH/DELETE responses. Ensure
each request is replayed at most once, and set useMethodOverride only after the
method-override replay succeeds; leave it disabled when the replay fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0a819c22-28b2-44e8-8ea5-429a8080f0e3

📥 Commits

Reviewing files that changed from the base of the PR and between 6da6faf and b13bfee.

📒 Files selected for processing (5)
  • README.md
  • src/client.ts
  • tests/client-method-override.test.ts
  • wordpress-plugin/gk-block-mcp/assets/mcp-server/index.cjs
  • wordpress-plugin/gk-block-mcp/readme.txt
🚧 Files skipped from review as they are similar to previous changes (2)
  • wordpress-plugin/gk-block-mcp/readme.txt
  • README.md

Comment thread wordpress-plugin/gk-block-mcp/assets/mcp-server/index.cjs
Comment thread wordpress-plugin/gk-block-mcp/assets/mcp-server/index.cjs Outdated
zackkatz added 2 commits July 23, 2026 21:23
…38) (#66)

* fix(security): BLOCK-38 CodeRabbit follow-ups — option lifecycle, cap-map safety, coverage

Three follow-ups from PR #65's review:

- Reassert the agent role on add/delete of gk_block_api_template_edits, not
  just update. delete_option() fires the generic `deleted_option` action
  (no `delete_option_{$option}` hook exists), and a fresh site's first
  enable routes through add_option() rather than update_option() — both
  previously left TEMPLATE_EDIT_CAP stale until the next `init`, so a
  settings reset didn't immediately revoke the write capability.
- Guard the TEMPLATE_EDIT_CAP read in Agent_Provisioner::register_role()
  against the public gk/block-mcp/agent/caps filter unsetting the key
  entirely (rather than setting it false), which previously raised an
  undefined-array-key warning. Unset now revokes the cap, the safe default.
- Add ability-layer coverage proving the dedicated block_mcp_agent role
  (holding TEMPLATE_EDIT_CAP, not edit_theme_options) can write templates
  through the Abilities API — the actual allow branch BLOCK-38 added,
  previously exercised only via edit_theme_options in this test file.

Each of the first two fixes ships with a test that failed pre-fix
(deleting the option left the cap granted; the filter unset triggered a
PHP warning) and passes post-fix. composer test: 1488 tests green across
all four suite configs (main, yoast, adapter, multisite).

Claude-Session: https://claude.ai/code/session_01EddiNE6Q2mqXo5koeiVTpT

* fix(settings): Release 2.2.0 CodeRabbit follow-ups — toggles persist stored value; template-cap docs

Address the CodeRabbit review on PR #64 (Release 2.2.0).

Behavioral fix (security): the Media uploads, Move-to-trash, template-editing,
and MCP Adapter toggles rendered their checkbox from the post-filter effective
value (*_enabled()), not the stored option. Because the checkbox is the form
control, a gk/block-mcp/* filter overriding the stored value meant saving the
settings page silently wrote the filtered value back into the option, flipping
the admin's persisted security setting. Each checkbox now renders from the
stored option; the divergence is surfaced by the existing "Heads up" override
notice, which now reuses the already-computed *_enabled value instead of
re-applying the filter. CodeRabbit flagged only template editing; the identical
bug existed in all four toggles (uploads/trash shipped since 2.0.0/2.1.0), so
all four are fixed. Regression test drives the real render_page() across every
toggle (fails pre-fix, has teeth), and confirmed e2e against a live site.

Docs/comments:
- Template-write authorization is the dedicated gk_block_mcp_edit_templates
  capability or edit_theme_options (not edit_posts): corrected the stale
  rest-controller.php comment and the AGENTS.md/README.md permission docs.
- README: reconcile per-block-tool applicability on templates (theme-file-only
  wp_id:null not writable; a numeric override is an ordinary post).
- AGENTS.md: list create_pattern in the patterns MCP-tool inventory.
- template-manager.php: assign the compound area condition to a named variable.

Claude-Session: https://claude.ai/code/session_01EddiNE6Q2mqXo5koeiVTpT
2.2.0 has not shipped: there is no v2.2.0 tag, the newest release is 2.1.0, and
the release PR is still open. So the method-override fix belongs in the 2.2.0
entry rather than a separate heading, and no version bump is needed.

Also corrects the release date, which had gone stale while the release PR sat
open. It needs another correction if the release slips past July 24.

Claude-Session: https://claude.ai/code/session_01Njh4D63XnZhsJHMbEU7vYq
@zackkatz

Copy link
Copy Markdown
Member Author

The two findings against the method-override fallback are addressed in #68 (into develop, so this PR picks them up).

Structured 405s no longer trigger the fallback. The route-collision risk is the sharp part of that finding: POST /posts/{id}/blocks is insert_blocks while PUT is replace_all_blocks, so a replay that lost its intent would append instead of replace and still report success. WordPress answers an unroutable method with 404 rest_no_route rather than 405 (verified live: PUT → 400, PATCH/DELETE → 404 on /wp-json/), so requiring the 405 to carry no REST error body cleanly separates an edge rejection from a real answer.

The flag no longer sticks on a failed replay. It now flips only after a replay succeeds, with a per-request marker carrying the override through.

One replay per request was already guaranteed structurally rather than by a counter: a replay carries method post, which is not an override verb, so it cannot re-enter the branch. The existing test asserting exactly two attempts against a host that rejects both still passes, so I left that alone.

The docblock claim about "only the first write" was stale after the concurrency fix and is reworded.

zackkatz added 3 commits July 23, 2026 21:39
…rm it works (#68)

Two tightenings to the PUT/PATCH/DELETE fallback.

Only an unstructured 405 now counts as an edge rejection. WordPress answers an
unroutable method with 404 `rest_no_route`, so a 405 carrying a REST error body
is a real answer from a route or a plugin, and replaying it as POST could reach
a different route than the caller asked for: POST on the blocks collection
inserts rather than replaces. Such a response is now surfaced untouched.

The sticky flag is also set only after a replay succeeds. It previously flipped
before the replay was attempted, so a host that rejects the override too was
remembered as override-capable and every later write skipped the real-verb
probe for a fallback already known to fail. A per-request marker carries the
override through the replay so the flag no longer has to.

Corrects the field docblock, which claimed only the first write pays the
rejected round-trip. Concurrent writes have each already gone out as a real
verb, so each pays its own; only later writes skip the probe.

Verified against the WAF that prompted the fallback: PATCH, PUT, and DELETE all
still complete through it.

Claude-Session: https://claude.ai/code/session_01Njh4D63XnZhsJHMbEU7vYq
…[ci skip]

Drops the method-override bullet from Developer Updates. `WordPressBlockClient`
is internal to the MCP server, so no integrator can reference it, and the bullet
described the override header and its caching rather than a contract anyone can
write code against. The user-visible outcome already lives in Fixed, which is
also where the readme style guide puts a firewall-blocked-connection change.

Narrows the settings-toggle fix to the toggles that shipped with the bug. Media
uploads, Move to trash, and the MCP Adapter all existed in 2.1.0 or earlier;
template editing arrives in this release, so its instance of the bug never
reached a customer and is not a change worth reporting.

Trims the firewall entry to the symptom and the resolution, and extends the
summary to cover the release's two fixes rather than only its additions.

Claude-Session: https://claude.ai/code/session_01Njh4D63XnZhsJHMbEU7vYq

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
wordpress-plugin/gk-block-mcp/tests/Connect/AgentProvisionerTest.php (1)

171-180: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Keep test docblocks focused on current contracts.

Remove “Before the fix,” regression-history, and implementation-history narration; retain only the behavior each test guarantees.

  • wordpress-plugin/gk-block-mcp/tests/Connect/AgentProvisionerTest.php#L171-L180: state that deleting the option immediately revokes the capability.
  • wordpress-plugin/gk-block-mcp/tests/Connect/AgentProvisionerTest.php#L199-L209: state that first-time enabling immediately grants the capability.
  • wordpress-plugin/gk-block-mcp/tests/Connect/AgentRoleTest.php#L172-L177: state that an unset capability-map entry revokes the capability safely.
  • wordpress-plugin/gk-block-mcp/tests/Connect/SettingsPagePreferencesTest.php#L534-L545: state that checkbox state follows storage and divergence displays an override notice.

As per coding guidelines, comments and docblocks must document present-day behavior and hard contracts, not development history.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wordpress-plugin/gk-block-mcp/tests/Connect/AgentProvisionerTest.php` around
lines 171 - 180, Rewrite the test docblocks to describe only current behavioral
contracts, removing regression and implementation-history narration. In
wordpress-plugin/gk-block-mcp/tests/Connect/AgentProvisionerTest.php lines
171-180, state that deleting the option immediately revokes the capability;
lines 199-209, state that first-time enabling immediately grants it. In
wordpress-plugin/gk-block-mcp/tests/Connect/AgentRoleTest.php lines 172-177,
state that an unset capability-map entry safely revokes the capability. In
wordpress-plugin/gk-block-mcp/tests/Connect/SettingsPagePreferencesTest.php
lines 534-545, state that checkbox state follows storage and divergence displays
an override notice.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wordpress-plugin/gk-block-mcp/tests/Abilities/TemplateAbilitiesTest.php`:
- Around line 331-351: Before executing the update-template ability in this
test, explicitly inspect the registered Agent_Provisioner role and assert it has
TEMPLATE_EDIT_CAP while lacking edit_theme_options, manage_options,
unfiltered_html, and all delete capabilities. Keep these least-privilege
assertions before the ability execution so the test cannot pass if the role is
overprivileged.
- Around line 329-351: Update
test_update_template_ability_succeeds_via_dedicated_capability to preserve and
restore the preexisting Agent_Provisioner::ROLE state instead of unconditionally
calling remove_role(). Also reset the wp_template override created by the
successful update, using a finally/teardown path that runs on both success and
failure so later tests start with the original role and template state.

In `@wordpress-plugin/gk-block-mcp/tests/Connect/SettingsPagePreferencesTest.php`:
- Around line 558-571: Update the test using the temporary filter registered by
add_filter in the render_policy_html assertion flow: wrap the assertions in
try/finally and remove the same filter with remove_filter in the finally block,
ensuring cleanup occurs even when an assertion fails.

---

Nitpick comments:
In `@wordpress-plugin/gk-block-mcp/tests/Connect/AgentProvisionerTest.php`:
- Around line 171-180: Rewrite the test docblocks to describe only current
behavioral contracts, removing regression and implementation-history narration.
In wordpress-plugin/gk-block-mcp/tests/Connect/AgentProvisionerTest.php lines
171-180, state that deleting the option immediately revokes the capability;
lines 199-209, state that first-time enabling immediately grants it. In
wordpress-plugin/gk-block-mcp/tests/Connect/AgentRoleTest.php lines 172-177,
state that an unset capability-map entry safely revokes the capability. In
wordpress-plugin/gk-block-mcp/tests/Connect/SettingsPagePreferencesTest.php
lines 534-545, state that checkbox state follows storage and divergence displays
an override notice.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31046250-e9cc-45b6-ab40-9729371bf745

📥 Commits

Reviewing files that changed from the base of the PR and between b13bfee and 6fa2557.

📒 Files selected for processing (16)
  • AGENTS.md
  • README.md
  • src/client.ts
  • tests/client-method-override.test.ts
  • wordpress-plugin/AGENTS.md
  • wordpress-plugin/gk-block-mcp/assets/mcp-server/index.cjs
  • wordpress-plugin/gk-block-mcp/gk-block-mcp.php
  • wordpress-plugin/gk-block-mcp/includes/class-agent-provisioner.php
  • wordpress-plugin/gk-block-mcp/includes/class-rest-controller.php
  • wordpress-plugin/gk-block-mcp/includes/class-settings-page.php
  • wordpress-plugin/gk-block-mcp/includes/class-template-manager.php
  • wordpress-plugin/gk-block-mcp/readme.txt
  • wordpress-plugin/gk-block-mcp/tests/Abilities/TemplateAbilitiesTest.php
  • wordpress-plugin/gk-block-mcp/tests/Connect/AgentProvisionerTest.php
  • wordpress-plugin/gk-block-mcp/tests/Connect/AgentRoleTest.php
  • wordpress-plugin/gk-block-mcp/tests/Connect/SettingsPagePreferencesTest.php
🚧 Files skipped from review as they are similar to previous changes (11)
  • wordpress-plugin/gk-block-mcp/includes/class-agent-provisioner.php
  • wordpress-plugin/gk-block-mcp/gk-block-mcp.php
  • wordpress-plugin/AGENTS.md
  • AGENTS.md
  • wordpress-plugin/gk-block-mcp/includes/class-settings-page.php
  • README.md
  • tests/client-method-override.test.ts
  • src/client.ts
  • wordpress-plugin/gk-block-mcp/includes/class-template-manager.php
  • wordpress-plugin/gk-block-mcp/includes/class-rest-controller.php
  • wordpress-plugin/gk-block-mcp/assets/mcp-server/index.cjs

Comment on lines +329 to +351
public function test_update_template_ability_succeeds_via_dedicated_capability() {
update_option( Template_Manager::ALLOW_TEMPLATE_EDITS_OPTION, '1' );
Agent_Provisioner::register_role();

wp_set_current_user( self::factory()->user->create( array( 'role' => Agent_Provisioner::ROLE ) ) );

$marker = 'AGENT-CAP-MARKER-' . wp_rand();
$result = wp_get_ability( 'gk-block-mcp/update-template' )->execute(
array(
'id' => $this->theme . '//index',
'content' => '<!-- wp:paragraph --><p>' . $marker . '</p><!-- /wp:paragraph -->',
)
);

$this->assertNotWPError( $result );
$this->assertTrue( $result['success'] );

$read = wp_get_ability( 'gk-block-mcp/get-template' )->execute( array( 'id' => $this->theme . '//index' ) );

remove_role( Agent_Provisioner::ROLE );

$this->assertNotWPError( $read );
$this->assertStringContainsString( $marker, $read['content'] );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Restore shared role and template state after this test.

register_role() can update the preexisting canonical role, but Line 348 unconditionally deletes it. The successful write also leaves a wp_template override behind. Restore the prior role state and reset the created override in finally/teardown so later tests do not depend on execution order.

Based on supplied role-registration context, register_role() manages an existing canonical role in place.

🧰 Tools
🪛 PHPStan (2.2.5)

[error] 333-333: Call to an undefined static method TemplateAbilitiesTest::factory().

(staticMethod.notFound)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wordpress-plugin/gk-block-mcp/tests/Abilities/TemplateAbilitiesTest.php`
around lines 329 - 351, Update
test_update_template_ability_succeeds_via_dedicated_capability to preserve and
restore the preexisting Agent_Provisioner::ROLE state instead of unconditionally
calling remove_role(). Also reset the wp_template override created by the
successful update, using a finally/teardown path that runs on both success and
failure so later tests start with the original role and template state.

Comment on lines +331 to +351
Agent_Provisioner::register_role();

wp_set_current_user( self::factory()->user->create( array( 'role' => Agent_Provisioner::ROLE ) ) );

$marker = 'AGENT-CAP-MARKER-' . wp_rand();
$result = wp_get_ability( 'gk-block-mcp/update-template' )->execute(
array(
'id' => $this->theme . '//index',
'content' => '<!-- wp:paragraph --><p>' . $marker . '</p><!-- /wp:paragraph -->',
)
);

$this->assertNotWPError( $result );
$this->assertTrue( $result['success'] );

$read = wp_get_ability( 'gk-block-mcp/get-template' )->execute( array( 'id' => $this->theme . '//index' ) );

remove_role( Agent_Provisioner::ROLE );

$this->assertNotWPError( $read );
$this->assertStringContainsString( $marker, $read['content'] );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Assert the least-privilege precondition explicitly.

This passes if the agent role gains edit_theme_options even when TEMPLATE_EDIT_CAP is missing. Assert that the role has TEMPLATE_EDIT_CAP and lacks edit_theme_options, manage_options, unfiltered_html, and delete capabilities before executing the ability.

As per coding guidelines, “The dedicated agent role must remain least-privilege: do not grant delete capabilities, unfiltered_html, or manage_options by default.”

🧰 Tools
🪛 PHPStan (2.2.5)

[error] 333-333: Call to an undefined static method TemplateAbilitiesTest::factory().

(staticMethod.notFound)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wordpress-plugin/gk-block-mcp/tests/Abilities/TemplateAbilitiesTest.php`
around lines 331 - 351, Before executing the update-template ability in this
test, explicitly inspect the registered Agent_Provisioner role and assert it has
TEMPLATE_EDIT_CAP while lacking edit_theme_options, manage_options,
unfiltered_html, and all delete capabilities. Keep these least-privilege
assertions before the ability execution so the test cannot pass if the role is
overprivileged.

Source: Coding guidelines

Comment on lines +558 to +571
add_filter( $filter, $filter_forces ? '__return_true' : '__return_false' );

$html = $this->render_policy_html();

$this->assertSame(
$expect_checked,
$this->checkbox_is_checked( $html, $option ),
sprintf( 'the %s checkbox must reflect the stored option (%s), not the filter override', $option, $stored )
);
$this->assertStringContainsString(
'<code>' . $filter . '</code>',
$html,
'a filter diverging from the stored value must surface the Heads-up override notice naming the filter'
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove each temporary override filter after the assertion.

The data-provider runs leave global filters registered, affecting later tests’ effective security settings. Use try/finally so cleanup also runs on assertion failure.

Proposed fix
-		add_filter( $filter, $filter_forces ? '__return_true' : '__return_false' );
+		$callback = $filter_forces ? '__return_true' : '__return_false';
+		add_filter( $filter, $callback );
 
-		$html = $this->render_policy_html();
+		try {
+			$html = $this->render_policy_html();
 
-		$this->assertSame(
+			$this->assertSame(
 			$expect_checked,
 			$this->checkbox_is_checked( $html, $option ),
 			sprintf( 'the %s checkbox must reflect the stored option (%s), not the filter override', $option, $stored )
-		);
-		$this->assertStringContainsString(
+			);
+			$this->assertStringContainsString(
 			'<code>' . $filter . '</code>',
 			$html,
 			'a filter diverging from the stored value must surface the Heads-up override notice naming the filter'
-		);
+			);
+		} finally {
+			remove_filter( $filter, $callback );
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
add_filter( $filter, $filter_forces ? '__return_true' : '__return_false' );
$html = $this->render_policy_html();
$this->assertSame(
$expect_checked,
$this->checkbox_is_checked( $html, $option ),
sprintf( 'the %s checkbox must reflect the stored option (%s), not the filter override', $option, $stored )
);
$this->assertStringContainsString(
'<code>' . $filter . '</code>',
$html,
'a filter diverging from the stored value must surface the Heads-up override notice naming the filter'
);
$callback = $filter_forces ? '__return_true' : '__return_false';
add_filter( $filter, $callback );
try {
$html = $this->render_policy_html();
$this->assertSame(
$expect_checked,
$this->checkbox_is_checked( $html, $option ),
sprintf( 'the %s checkbox must reflect the stored option (%s), not the filter override', $option, $stored )
);
$this->assertStringContainsString(
'<code>' . $filter . '</code>',
$html,
'a filter diverging from the stored value must surface the Heads-up override notice naming the filter'
);
} finally {
remove_filter( $filter, $callback );
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wordpress-plugin/gk-block-mcp/tests/Connect/SettingsPagePreferencesTest.php`
around lines 558 - 571, Update the test using the temporary filter registered by
add_filter in the render_policy_html assertion flow: wrap the assertions in
try/finally and remove the same filter with remove_filter in the finally block,
ensuring cleanup occurs even when an assertion fails.

@zackkatz
zackkatz merged commit c105a14 into main Jul 24, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant