Skip to content

feat(abilities): expose templates tool group via the WordPress Abilities API - #58

Merged
zackkatz merged 3 commits into
developfrom
feature/block-34-expose-the-templates-tool-group-via-the-wordpress-abilities
Jul 22, 2026
Merged

feat(abilities): expose templates tool group via the WordPress Abilities API#58
zackkatz merged 3 commits into
developfrom
feature/block-34-expose-the-templates-tool-group-via-the-wordpress-abilities

Conversation

@zackkatz

Copy link
Copy Markdown
Member

What changed

Exposes the templates tool group (list_templates, get_template, update_template, reset_template) via the WordPress Abilities API / MCP Adapter (BLOCK-34) — the follow-up flagged in the BLOCK-29 PR.

Starting state on fresh origin/develop (worth stating plainly, since it changes the shape of this PR): TEMPLATE_TOOLS was already wired into scripts/export-abilities-manifest.mjs's buildManifest() — the committed manifest already carried all 4 templates tools at 32 tools total / 29 non-yoast ability ids, since ability registration is manifest-driven and doesn't depend on Tool_Executor actually knowing how to run a tool. Two real gaps remained:

  1. No gate parity. update_template/reset_template resolved to the manifest's default edit_post permission — the same as every ordinary per-block write — so the Abilities/MCP-Adapter surface could write to a template with none of the gk_block_api_template_edits toggle checking the REST routes (POST /template, POST /template/reset) enforce. An agent connected over the Adapter could bypass the operator's off-by-default consent entirely.
  2. No execution. Tool_Executor::execute() had no case for any of the 4 tool names, so every call 400'd with "Unknown Block MCP tool" regardless of permission.

Fix

  • scripts/export-abilities-manifest.mjs: new TEMPLATE_EDIT permission bucket (update_template, reset_template) → manifest permission 'template_edit', checked before the edit_post fallback. Regenerated tools.manifest.json — still 32 tools, only the two permission values change.
  • Abilities_Registry::check_tool_permission(): new 'template_edit' case that calls REST_Controller::check_template_edit_permissions() directly — the exact method the REST routes use as their permission_callback. No gate logic is duplicated or re-implemented; both surfaces share one source of truth for the toggle + capability check.
  • Tool_Executor::execute(): dispatch cases + execute_list_templates() / execute_get_template() / execute_update_template() / execute_reset_template(), each delegating to the matching REST_Controller handler via the existing call_controller() pattern (hand-built WP_REST_Request + set_url_params()/set_body_params()) every other ability already uses.
  • AGENTS.md: the "Add an MCP tool" recipe gets a 4th step covering the Abilities-wiring gap this issue backfilled — a new tool-group file needs an explicit import in the export script, a permission mapping, and a Tool_Executor case, none of which step 3 (npm run build) alone provides.

TDD: red before green (commit history is the proof, not squashed)

  1. 393d9dc test(abilities): templates group registration + gate parity [red] — new tests/Abilities/TemplateAbilitiesTest.php (registration presence, read execution, 8 gate-parity scenarios) + 3 new assertions in tests/abilities-manifest.test.ts (group presence, read permission, template_edit permission) + minor additions to AbilitiesRegistryTest's existing readonly/destructive-annotation tests. Confirmed genuinely red against unmodified origin/develop:
    $ vendor/bin/phpunit -c tests/phpunit.xml tests/Abilities/
    Tests: 153, Assertions: 360, Failures: 8.
    
    $ npm test -- tests/abilities-manifest.test.ts
    Tests  1 failed | 4 passed (5)
    
  2. 6a58fab feat(abilities): wire templates group execution + gate parity [green] — the fix described above. All 8 PHP failures + the 1 TS failure go green; nothing else regresses.
  3. bc484b3 docs(agents): add the Abilities-wiring step to the Add-an-MCP-tool recipe.

Acceptance criteria

  • TEMPLATE_TOOLS reachable through the manifest export and Abilities_Registry/Tool_Executor (already true for the manifest; Tool_Executor execution is new here).
  • Manifest/ability-id counts verified against the actual current baseline (32 / 29), not a guessed number — confirmed by inspecting the committed JSON directly before writing any assertion.
  • update_template/reset_template ability permission callbacks honor gk_block_api_template_edits + gk/block-mcp/templates/allow-edits exactly as the REST routes do — proven by delegating to the identical method, and by 8 PHPUnit scenarios plus live MCP-transport smoke evidence below (toggle off denies an editor and an admin's own filter-forced-off case; toggle on + edit_posts succeeds and persists; toggle on + no capability still denies a subscriber; edit_theme_options alone succeeds — the "self" connection path).
  • AGENTS.md's "Add an MCP tool" recipe updated.

Gate output (real numbers, run in the worktree)

$ npm test
 Test Files  54 passed (54)
      Tests  849 passed (849)

$ npm run build
  dist/index.cjs  3.6mb
  Copied dist/index.cjs -> wordpress-plugin/gk-block-mcp/assets/mcp-server/index.cjs

$ npx tsx scripts/export-abilities-manifest.mjs && git diff --exit-code wordpress-plugin/gk-block-mcp/includes/abilities/tools.manifest.json
Wrote 32 tools to .../tools.manifest.json
(no diff — exit 0)

$ composer test          (from wordpress-plugin/gk-block-mcp)
OK (1398 tests, 18856 assertions)
OK (28 tests, 82 assertions)     [yoast group]
OK (11 tests, 25 assertions)     [mcp-adapter group]
OK (5 tests, 37 assertions)      [multisite]

$ composer lint
FOUND 0 ERRORS AND 0 WARNINGS

$ composer analyze
[OK] No errors

Siteminter smoke — real MCP Adapter HTTP transport, not wp-cli fallback

Fresh site blockmcp-c (WordPress 7.0.2 — Abilities API ships in core; the separate WordPress MCP Adapter plugin isn't bundled, so I installed it from its GitHub Release (v0.5.0) to exercise the real path rather than falling back to direct wp_get_ability()->execute() calls). Plugin symlinked to the worktree; both toggles (gk_block_api_abilities_enabled, gk_block_api_template_edits) exercised live.

MCP initialize handshake (real session, real Mcp-Session-Id):

$ curl -u admin:*** -X POST http://localhost:8936/wp-json/gk-block-mcp/mcp \
    -d '{"jsonrpc":"2.0","id":1,"method":"initialize",...}'
HTTP/1.1 200 OK
Mcp-Session-Id: f007e3d4-b8dc-4b66-ae8f-c08a20d5e064
{"jsonrpc":"2.0","id":1,"result":{...,"serverInfo":{"name":"Block MCP","version":"2.1.0"},...}}

tools/list — all 4 template tools present among 29 total:

template tools: ['gk-block-mcp-list-templates', 'gk-block-mcp-get-template', 'gk-block-mcp-update-template', 'gk-block-mcp-reset-template']

Gate parity, toggle OFF → tools/call update-template denied (admin, who has every capability — proves the toggle, not a capability gap, is what's blocking):

{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"Editing theme templates is turned off for this site. A site administrator can enable it under Block MCP → Settings."}],"isError":true}}

Toggle ON → tools/call update-template succeeds, creates the override:

{"jsonrpc":"2.0","id":4,"result":{"structuredContent":{"success":true,"wp_id":4,"override_created":true,"revert_hint":"Call reset_template...","warnings":[],"before_revision_id":0,"revision_id":5},"isError":false}}

Verified independently via wp-cli and a real page render:

$ wp post list --post_type=wp_template_part --fields=ID,post_title,post_name,post_status
ID  post_title  post_name  post_status
4   Header      header     publish

$ wp post term list 4 wp_theme --format=table
term_id  name              slug              taxonomy
2        twentytwentyfive  twentytwentyfive  wp_theme

$ curl -s http://localhost:8936/ | grep -o "MCP-ADAPTER-SMOKE"
MCP-ADAPTER-SMOKE

tools/call get-template confirms the read side sees the override too:

source: custom wp_id: 4
content contains marker: True

Capability half of the gate — a subscriber denied even with the toggle ON (proves the toggle widens what a capable actor may do, it doesn't replace the capability check):

{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"You do not have permission to edit templates."}],"isError":true}}

tools/call reset-template — deletes the override, full round trip:

{"jsonrpc":"2.0","id":6,"result":{"structuredContent":{"success":true,"id":"twentytwentyfive//header","wp_id":4},"isError":false}}
$ wp post list --post_type=wp_template_part --name=header --fields=ID,post_status
(empty — post 4 is gone)

$ curl -s http://localhost:8936/ | grep -o "MCP-ADAPTER-SMOKE"
(empty — reverted)

Site destroyed after this run; confirmed no leftover containers.

Fixes BLOCK-34
https://linear.app/gravitykit/issue/BLOCK-34/expose-the-templates-tool-group-via-the-wordpress-abilities-adapter

https://claude.ai/code/session_013YcSbKroBJjPanX3okQrT3

zackkatz added 3 commits July 22, 2026 19:24
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
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
…cipe

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
@linear-code

linear-code Bot commented Jul 22, 2026

Copy link
Copy Markdown

BLOCK-34

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d4946811-eade-4ff9-a1ae-9545982f9095

📥 Commits

Reviewing files that changed from the base of the PR and between 0b6db23 and bc484b3.

📒 Files selected for processing (8)
  • AGENTS.md
  • scripts/export-abilities-manifest.mjs
  • tests/abilities-manifest.test.ts
  • 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-tool-executor.php
  • wordpress-plugin/gk-block-mcp/tests/Abilities/AbilitiesRegistryTest.php
  • wordpress-plugin/gk-block-mcp/tests/Abilities/TemplateAbilitiesTest.php
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/block-34-expose-the-templates-tool-group-via-the-wordpress-abilities

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

@zackkatz
zackkatz merged commit 930b985 into develop Jul 22, 2026
9 checks passed
@zackkatz
zackkatz deleted the feature/block-34-expose-the-templates-tool-group-via-the-wordpress-abilities branch July 22, 2026 23:38
zackkatz added a commit that referenced this pull request Jul 23, 2026
…_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
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