Goal
Expose Block MCP's REST endpoints as registered WordPress abilities (wp_register_ability()), so the official WordPress/mcp-adapter can publish them as MCP tools without our standalone TypeScript stdio server.
Why
Two things are converging:
- Abilities API shipped in WordPress 6.9 core. It's the canonical way to register a capability that an AI agent (or any other consumer) can discover and call.
- WordPress/mcp-adapter (1k+ stars, official WP org) automatically converts registered abilities into MCP tools/resources/prompts, with HTTP + STDIO transports, permission gates, error handling, and observability hooks built in.
Today our TypeScript MCP server reimplements pieces the adapter already gives us for free (transport, error envelope, capability gates). Once the adapter is stable, registering our endpoints as abilities means:
- Less code to maintain. Drop the stdio bridge, axios client, tool-input schemas duplicated from the PHP route definitions.
- Blessed transport. Inherit MCP-spec compliance and future protocol upgrades from the official adapter.
- Native discoverability. Other WordPress AI tooling that consumes the Abilities API (WP-CLI, future core surfaces, third-party plugins) gets our endpoints for free.
- Per-ability permissions. The Abilities API has structured permission callbacks; we currently roll capability checks per route.
Scope
Each REST endpoint becomes a registered ability. Approximate mapping:
| Current REST endpoint |
Ability slug |
GET /posts/{id}/blocks |
gk-block-api/get-page-blocks |
PATCH /posts/{id}/blocks/{index} |
gk-block-api/update-block |
PATCH /posts/{id}/blocks/by-ref/{ref} |
gk-block-api/update-block-by-ref |
POST /posts/{id}/blocks |
gk-block-api/insert-blocks |
DELETE /posts/{id}/blocks/{index} |
gk-block-api/delete-block |
DELETE /posts/{id}/blocks/by-ref/{ref} |
gk-block-api/delete-block-by-ref |
POST /posts/{id}/mutate |
gk-block-api/edit-block-tree |
POST /posts/{id}/blocks/replace |
gk-block-api/replace-block-range |
PUT /posts/{id}/blocks |
gk-block-api/rewrite-post-blocks |
POST /posts/{id}/insert-pattern |
gk-block-api/insert-pattern |
POST /posts/{id}/revert |
gk-block-api/revert-to-revision |
GET /block-types |
gk-block-api/list-block-types |
GET /patterns |
gk-block-api/list-patterns |
GET /site-usage |
gk-block-api/get-site-usage |
GET /resolve |
gk-block-api/resolve-url |
POST /posts |
gk-block-api/create-post |
PATCH /posts/{id} |
gk-block-api/update-post |
GET /terms |
gk-block-api/list-terms |
POST /media |
gk-block-api/upload-media |
Yoast gravitykit/v1/yoast-seo/* |
gk-block-api/yoast-* |
Each registration provides:
label, description (the existing tool descriptions are a starting point)
input_schema / output_schema (already defined per route — port from register_rest_route args)
permission_callback (existing check_post_edit_permission pattern)
execute_callback (delegates to the same Block_CRUD / Block_Mutator / Post_Manager / etc. service objects)
Phased plan
- Phase 1 — Wrap existing REST handlers as abilities. Keep the REST routes for backwards compatibility. Add
Abilities_Registrar class that registers each endpoint as an ability whose execute_callback calls the same service-class method. Behind a feature flag (gk_block_api_register_abilities filter, default off until stable).
- Phase 2 — Adapter compatibility check. Install WordPress/mcp-adapter in a test site, confirm our abilities are discovered, run the existing PHP smoke against the adapter's STDIO transport.
- Phase 3 — TypeScript thin-client deprecation path. Decide whether to keep the standalone TS MCP for backwards compatibility or document the adapter as the primary surface. Likely: keep TS as a supported entry point (some users won't want the adapter), but the adapter becomes the recommended path.
- Phase 4 — Drop the abilities feature flag. Default-on once we trust the surface.
Open questions
- Schema parity. Our
register_rest_route args arrays describe input shape but not output shape. Abilities want both. We'll need to formalize output schemas — useful regardless of this issue.
- Ref-based vs index-based abilities. Should
update_block and update_block_by_ref be one ability with an XOR'd input, or two abilities? Current REST has them as two routes. Two abilities is more discoverable; one is cleaner. Probably two.
- Preference scoring on the discovery side. Our
list_block_types returns enriched data (tier, replacement, usage). The Abilities API has its own discovery channel — is there value in exposing block types as abilities, or do they stay as ability inputs/outputs?
- Yoast endpoints live on a different REST namespace (
gravitykit/v1/yoast-seo/* in the Block-Theme mu-plugin). Either co-locate the ability registration or keep Yoast as a separate sub-package.
- MCP adapter dependency. Adapter requires WP 6.9+ for built-in Abilities API; WP 6.8 needs the (now-archived) plugin. Our minimum is currently WP 6.0. Either bump the floor for ability registration only, or keep dual paths.
Out of scope
- Replacing the standalone TS server. Phase 3 will revisit; for v1 of this work the TS client stays.
- Resources and prompts. The adapter supports both, but our endpoints are tools-shaped. Could revisit later (e.g., expose the ref system as a resource).
Acceptance
- All current REST endpoints registered as abilities behind the feature flag
- WordPress/mcp-adapter test site successfully discovers and invokes each ability
- New PHPUnit test file
AbilitiesRegistrarTest.php covering registration + permission + execute paths
- README updated with adapter setup instructions
Goal
Expose Block MCP's REST endpoints as registered WordPress abilities (
wp_register_ability()), so the official WordPress/mcp-adapter can publish them as MCP tools without our standalone TypeScript stdio server.Why
Two things are converging:
Today our TypeScript MCP server reimplements pieces the adapter already gives us for free (transport, error envelope, capability gates). Once the adapter is stable, registering our endpoints as abilities means:
Scope
Each REST endpoint becomes a registered ability. Approximate mapping:
GET /posts/{id}/blocksgk-block-api/get-page-blocksPATCH /posts/{id}/blocks/{index}gk-block-api/update-blockPATCH /posts/{id}/blocks/by-ref/{ref}gk-block-api/update-block-by-refPOST /posts/{id}/blocksgk-block-api/insert-blocksDELETE /posts/{id}/blocks/{index}gk-block-api/delete-blockDELETE /posts/{id}/blocks/by-ref/{ref}gk-block-api/delete-block-by-refPOST /posts/{id}/mutategk-block-api/edit-block-treePOST /posts/{id}/blocks/replacegk-block-api/replace-block-rangePUT /posts/{id}/blocksgk-block-api/rewrite-post-blocksPOST /posts/{id}/insert-patterngk-block-api/insert-patternPOST /posts/{id}/revertgk-block-api/revert-to-revisionGET /block-typesgk-block-api/list-block-typesGET /patternsgk-block-api/list-patternsGET /site-usagegk-block-api/get-site-usageGET /resolvegk-block-api/resolve-urlPOST /postsgk-block-api/create-postPATCH /posts/{id}gk-block-api/update-postGET /termsgk-block-api/list-termsPOST /mediagk-block-api/upload-mediagravitykit/v1/yoast-seo/*gk-block-api/yoast-*Each registration provides:
label,description(the existing tool descriptions are a starting point)input_schema/output_schema(already defined per route — port fromregister_rest_routeargs)permission_callback(existingcheck_post_edit_permissionpattern)execute_callback(delegates to the sameBlock_CRUD/Block_Mutator/Post_Manager/ etc. service objects)Phased plan
Abilities_Registrarclass that registers each endpoint as an ability whoseexecute_callbackcalls the same service-class method. Behind a feature flag (gk_block_api_register_abilitiesfilter, default off until stable).Open questions
register_rest_routeargsarrays describe input shape but not output shape. Abilities want both. We'll need to formalize output schemas — useful regardless of this issue.update_blockandupdate_block_by_refbe one ability with an XOR'd input, or two abilities? Current REST has them as two routes. Two abilities is more discoverable; one is cleaner. Probably two.list_block_typesreturns enriched data (tier, replacement, usage). The Abilities API has its own discovery channel — is there value in exposing block types as abilities, or do they stay as ability inputs/outputs?gravitykit/v1/yoast-seo/*in the Block-Theme mu-plugin). Either co-locate the ability registration or keep Yoast as a separate sub-package.Out of scope
Acceptance
AbilitiesRegistrarTest.phpcovering registration + permission + execute paths