Foundations

What Changes When WordPress Is Headless

⏱ 10 min

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.

What you'll learn in this lesson
Why abilities and the MCP Adapter need zero changes for headless use
The same wp_register_ability() and HttpTransport code path serves any caller.
The three things a same-origin request has that a decoupled one does not
Browser cookies, an existing login session, and a same-origin nonce.
Who "the caller" actually becomes in three decoupled scenarios
A Next.js server, a mobile app, and an edge function, each with a different identity story.
Why the MCP Adapter was already built for this
HttpTransport never assumed a browser in the first place.
Prerequisites

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:

Browser cookies
The wp_logged_in_ cookie and friends, set only because the browser is on the same domain as WordPress.
An existing login session
A human already authenticated through wp-login.php at some point in this browser.
A same-origin X-WP-Nonce
Generated server-side into the page WordPress itself rendered, and only valid because it was read from that same page.

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.

Don't try to fake a nonce from outside WordPress

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:

A Next.js server
Holds an Application Password in a server-only environment variable, calls WordPress from a route handler or server component, never from the browser.
A mobile app
Stores a credential in the device's secure keychain or keystore, and calls WordPress directly over HTTPS from the device.
An edge or serverless function
Reads the same kind of secret from its platform's secret store, with its own cold-start and secret-scoping considerations.

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

Authenticating External Apps: Application Passwords vs Third-Party JWT →