Let’s start by ruling out a myth. There is no “headless mode” setting for the Abilities
API, no separate decoupled edition of the MCP Adapter, and no special registration
function you swap in when WordPress stops rendering its own front end. The ability you
registered in Course 2, with its input_schema, permission_callback, and
execute_callback, runs exactly the same PHP whether the request that triggers it came
from a wp-admin browser tab or from a Next.js server three regions away. What actually
changes when WordPress goes headless isn’t the code inside WordPress. It’s who’s on the
other end of the connection, and that has real consequences for authentication,
sessions, and how you think about the request.
A working ability registered with wp_register_ability() and exposed via the MCP
Adapter’s HttpTransport (Course 1 and Course 2), and the real authentication model
from Course 4, Application Passwords and permission_callback. This lesson is
conceptual, you won’t write new PHP yet.
Step 1: The same registration code, no exceptions
Open the ability you built in Course 2. Nothing about it references where the calling client lives:
// File: my-plugin.php
add_action( 'wp_abilities_api_init', function () {
wp_register_ability(
'my-plugin/get-latest-post-title',
array(
'label' => __( 'Get latest post title', 'my-plugin' ),
'description' => __( 'Returns the title of the most recently published post.', 'my-plugin' ),
'category' => 'content',
'input_schema' => array( 'type' => 'object', 'properties' => array() ),
'output_schema' => array(
'type' => 'object',
'properties' => array( 'title' => array( 'type' => 'string' ) ),
),
'permission_callback' => function () {
return current_user_can( 'edit_posts' );
},
'execute_callback' => function () {
$latest = get_posts( array( 'numberposts' => 1 ) );
return array( 'title' => $latest ? $latest[0]->post_title : '' );
},
'meta' => array( 'mcp' => array( 'public' => true ) ),
)
);
} );
There is nothing to add, remove, or configure differently to make this ability
“headless-ready.” It already is. The MCP Adapter exposes it over the same
HttpTransport regardless of whether the next request arrives from wp-admin’s block
editor or from a fetch call inside a Next.js route handler running on Vercel.
Step 2: What a same-origin request actually has, that a decoupled one doesn’t
A same-origin request, the kind an admin’s browser makes while looking at a wp-admin screen, carries three things by default that a request from an external app has to either replace or do without:
A Next.js server, a mobile app, or an edge function has none of these. There’s no browser, no cookie jar, and no WordPress-rendered page to read a nonce out of. That’s not a gap to work around with a workaround plugin, it’s exactly the situation Application Passwords already solve, covered end to end in Course 4 and revisited for the headless-specific angle in the next lesson.
Some integration guides suggest scraping a nonce out of a rendered wp-admin page and reusing it from an external script. Don’t. Nonces are short-lived, tied to a specific logged-in session, and were never meant to be a portable credential. If your caller lives outside WordPress, use Application Passwords, that’s the actual mechanism built for this.
Step 3: Who “the caller” becomes in a decoupled setup
The identity making the request is still always a real WordPress user, Course 4 established that clearly, but the thing holding that user’s credential changes shape depending on your architecture:
Each of these is a different transport and storage story, and each gets its own lesson
later in this course. But in every case, the ability’s permission_callback still runs
against a real, resolved WP_User, exactly like it does today.
Step 4: HttpTransport was already built for this
The MCP Adapter ships more than one transport. Local development tools often connect
over stdio, a process-to-process pipe that assumes both sides are on the same machine.
HttpTransport is different: it was built from the start to accept requests over
regular HTTPS, from any client capable of making an HTTP request, with no assumption
that the caller is a browser. A decoupled Next.js app, a mobile app, and Claude Desktop’s
remote connector all reach an MCP Adapter server through the exact same HttpTransport
code path. Headless isn’t a special case for this transport, it’s the case it was
designed around.
Test it: confirm the same ability answers a non-browser client
Using the Application Password you generated in Course 4, call your ability directly
with curl, no browser involved at all:
curl -u "your-username:xxxx xxxx xxxx xxxx xxxx xxxx" \
-X POST "https://your-site.example/wp-json/mcp/v1/my-plugin/get-latest-post-title" \
-H "Content-Type: application/json" \
-d '{}'
You should get back the same JSON shape your ability’s output_schema describes,
proving the exact same registration handles a headless caller with no code changes on
the WordPress side.
Recap
Abilities and the MCP Adapter’s HttpTransport need no modification to serve a
decoupled front end, the registration code, permission_callback, and execute_callback
are identical either way. What actually changes is that a decoupled caller has no
browser cookies, no existing login session, and no same-origin nonce to rely on, which
is exactly why Application Passwords, not a scraped nonce, are the real mechanism for
external callers. The rest of this course is about the client side: what a Next.js app,
a mobile app, and an edge function each need to do to call WordPress correctly and
securely.
Resources & further reading
- WordPress REST API handbook, developer.wordpress.org
- Abilities API reference, developer.wordpress.org
- MCP Adapter repository, GitHub