humanmade / hm-rest-ability
OAuth2 discovery endpoints and a REST API ability for exposing WordPress to MCP clients via the MCP Adapter
Package info
github.com/humanmade/hm-rest-ability
Type:wordpress-plugin
pkg:composer/humanmade/hm-rest-ability
Requires
- php: ^7.4 || ^8.0
- composer/installers: ^1 || ^2
Requires (Dev)
- brain/monkey: ^2.6
- humanmade/coding-standards: ^2.5
- phpunit/phpunit: ^9.6
- yoast/phpunit-polyfills: ^3.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-18 11:43:22 UTC
README
OAuth2 discovery endpoints and a REST API ability, for exposing WordPress to MCP clients (like Claude) via the official MCP Adapter plugin and the WordPress Abilities API.
What it does
OAuth2 discovery (inc/oauth2-discovery.php)
- Serves
/.well-known/oauth-authorization-server— RFC 8414 Authorization Server Metadata — so MCP clients can auto-discover the OAuth2 endpoints provided by the WP-API/OAuth2 plugin. - Serves
/.well-known/oauth-protected-resource— RFC 9728 Protected Resource Metadata — so clients can discover the authorization server from a401on the MCP endpoint. - Adds a
WWW-Authenticateheader to401responses on MCP REST routes, pointing clients at the protected resource metadata.
REST API abilities (inc/rest-api-abilities.php)
-
Registers three abilities that let an MCP client dispatch any internal WordPress REST API request, instead of needing a bespoke ability per endpoint. Permissions are enforced by running the matched route's own
permission_callback, so a user can only do through these abilities what their WordPress capabilities already allow.Ability Methods readOnlyHintdestructiveHintidempotentHintrest-api/readGET,OPTIONStruefalsetruerest-api/writePOST,PUT,PATCHfalsefalsefalserest-api/deleteDELETEfalsetruefalseSplitting by method means an MCP client can gate each kind of request separately, for example auto-approving reads while asking for confirmation before a write or a delete. All three carry
openWorldHint: true, since a route can be anything registered with WordPress, not a fixed set of operations. -
Caps the response data at 50KB by default, so a large payload can't fill a client's context window. Oversized lists keep their leading items, oversized objects keep their smallest fields, and the result says what was left out.
_fieldsis passed through to the request, so clients can ask for less up front. -
Gives clients a two-step way to find routes.
GET /(viarest-api/read) returns every route path and the methods it accepts, a few kilobytes instead of the ~1MB full index.OPTIONS /wp/v2/poststhen returns that one route's parameters. Core only answersOPTIONSwhen serving a real HTTP request, so the ability builds the same description from the route table itself. -
Adds confirmation guidance where it matters.
rest-api/writeandrest-api/deleteask a client to confirm with the user before calling. AnOPTIONSresponse also carries aguidancekey for a route that changes site settings or access (/wp/v2/settings,/wp/v2/users,/wp/v2/plugins, and similar), or that deletes one — routine routes get nothing extra. This is advice, not enforcement: WordPress capabilities still decide what a user may do. Reclassify a route with thehm_rest_ability_route_riskfilter, or add to the guidance text itself withhm_rest_ability_route_guidance. The plugin uses that second filter itself, as a working example: a route with a WordPress-stylestatusfield, such as/wp/v2/posts, gets a note that creating an item there already defaults todraftwhenstatusis omitted, so a client shouldn't set it topublishunless the user asked for that. Detected from the route's own schema (astatusargument whoseenumincludespublish), not a hardcoded list of routes, so it covers custom post types too — seeinc/status-field-guidance.php. A second module does the same for thecontentfield: a route whose content is stored as block markup gets a note saying so, and pointing atGET /wp/v2/block-typesfor the blocks that site has registered — a route the same abilities can already call. Detected from the schema again (acontentobject with ablock_versionproperty, which core adds only for post types that support the editor) — seeinc/content-field-guidance.php.
Media upload ability (inc/media-abilities.php)
- Registers a
media/uploadability that takes a base64-encoded file and puts it in the media library, returning the attachment ID and URL. The REST API ability can't do this: it sends JSON params, andPOST /wp/v2/medianeeds a request body plusContent-TypeandContent-Dispositionheaders. - Requires the
upload_filescapability, andedit_postwhen a parent post is given. Uploads are capped at the site's own limit,wp_max_upload_size().
Requirements
- WordPress 6.9+ (for the built-in Abilities API)
- PHP 7.4+
- The MCP Adapter plugin
(
wordpress/mcp-adapteron Packagist), declared as a dependency via theRequires Pluginsheader.
Installation
Composer (recommended):
composer require humanmade/hm-rest-ability
Or download a release ZIP
and upload it to /wp-content/plugins/.
Then activate both MCP Adapter and HM REST Ability.
Filters
-
hm_oauth2_discovery_metadata— filter the RFC 8414 authorization server metadata document. -
hm_oauth2_protected_resource_metadata— filter the RFC 9728 protected resource metadata document. -
hm_rest_ability_max_response_bytes— filter the maximum size, in bytes, of the response data returned for onerest-api/read,rest-api/write, orrest-api/deletecall. Defaults to50000; set it to0or less to disable trimming. -
hm_rest_ability_max_upload_bytes— filter the maximum size, in bytes, of a decodedmedia/uploadfile. Defaults towp_max_upload_size(), the site's own limit; set it to0or less to remove the limit. -
hm_rest_ability_login_wall_exemptions— filter the login-wall callbacks removed from.well-known/requests (defaults to Human Made's Require Login plugin; no-ops elsewhere). -
hm_rest_ability_route_guidance— filter theguidancetext anOPTIONSresponse carries for a route, after the built-in risk-tier guidance is assembled ($guidance, $route, $handlers). Add to it, replace it, or return''to suppress it. Two modules hook it themselves:inc/status-field-guidance.phpflags a publishablestatusfield, andinc/content-field-guidance.phpflags a block-markupcontentfield. Remove either on its own, for exampleremove_filter( 'hm_rest_ability_route_guidance', 'HM\StatusFieldGuidance\add_guidance' ), or add your own hooked callback alongside them for anything else worth flagging. -
hm_rest_ability_policy— filter todenyarest-api/read,rest-api/write, orrest-api/deletecall after the matched route's ownpermission_callbackhas already allowed it. Runs after capabilities, so it can only narrow access, never grant access a user's capabilities would not otherwise allow. Allows everything by default. Example, blocking writes to settings, plugins and themes:add_filter( 'hm_rest_ability_policy', function ( $decision, $method, $route, $params ) { if ( 'GET' === $method ) { return $decision; } $locked_down = [ '/wp/v2/settings', '/wp/v2/plugins', '/wp/v2/themes' ]; foreach ( $locked_down as $prefix ) { if ( str_starts_with( $route, $prefix ) ) { return 'deny'; } } return $decision; }, 10, 4 );
Using this with an agent
The plugin tells an agent what a route is for and where to be careful. It doesn't tell it how to write block markup, because that isn't the plugin's job — and the tools that do it well live outside WordPress.
skills/wordpress-block-content/SKILL.md covers that gap. It's a skill file
for agent harnesses that read them, such as Claude Code. It explains the
two-step route lookup, that a post's content is block markup, how to upload
an image before referencing it, and it points at two Human Made npm packages:
wesper— collects one JSON manifest of what a site actually registers: block types, post types, bindable fields, patterns, theme.json.block-runner— turns HTML or a block tree into block markup, validated with Gutenberg's own packages. It ships its own skill, which the one here defers to.
Neither is required. The skill says what to do when they aren't installed.
The skill belongs in your project, not on the server, so it isn't in the release ZIP or the Composer package. Copy it from a checkout of this repo into your project's skills directory:
cp -r skills/wordpress-block-content /path/to/your-project/.claude/skills/
Then a prompt like this has what it needs:
Add a case study page to the site for the Acme rebrand, with a heading, two paragraphs and the hero image from ./hero.jpg. Leave it as a draft.
Putting it in the tool output instead
The skill is the default because tool descriptions are sent on every request, and a site's agents may not have Node at all. If you'd rather the advice travel with the tools, add it to the route guidance yourself:
add_filter( 'hm_rest_ability_route_guidance', function ( $guidance, $route, $handlers ) { if ( ! \HM\ContentFieldGuidance\has_block_content_field( $handlers ) ) { return $guidance; } $note = 'Generate block markup with `npx block-runner convert`, not by hand.'; return '' === $guidance ? $note : $guidance . ' ' . $note; }, 10, 3 );
Development
composer install npm install
composer lint/composer format— PHPCS / PHPCBF against the HM coding standard.composer test— PHPUnit unit tests (Brain Monkey, no WordPress load).npm run test:e2e— Playwright end-to-end tests against WordPress Playground.
Release process
Releases are cut from the Actions tab: Release workflow → run with the
version to release (e.g. 0.2.0). It stamps the version into the plugin
header, tags the commit, and publishes a GitHub release with a distributable
ZIP.
License
GPL-2.0-or-later. See LICENSE.