Serving Your Abilities

Serving Abilities Over HTTP Transport

⏱ 19 min

Everything up to this point has lived entirely on your own machine, called from PHP or WP-CLI directly. This lesson is where your abilities become reachable over the network, as a real MCP server an AI client connects to over HTTP. You’ll register a custom server with HttpTransport, understand the session protocol MCP clients follow, and call it with curl the same way a real client would.

What you'll learn in this lesson
Registering a custom MCP server
Using McpAdapter::create_server() on the mcp_adapter_init hook, with HttpTransport.
The transport-level permission callback
A second, server-wide gate that sits in front of every ability on that server.
The HTTP session protocol
Why every request after the first needs an Mcp-Session-Id header.
Authenticating over HTTP
WordPress Application Passwords, since the adapter has no OAuth flow of its own.
Prerequisites

The my-plugin/create-draft-post ability from Lessons 2 to 5, with meta.mcp.public set to true. The MCP Adapter active. A WordPress user with an Application Password generated (Users → Profile → Application Passwords in wp-admin).

Step 1: Register a custom server

Custom servers are created on the mcp_adapter_init hook, which fires with the McpAdapter singleton instance passed in:

// File: my-mcp-abilities.php
add_action( 'mcp_adapter_init', function ( $adapter ) {
	$adapter->create_server(
		'my-plugin-content-server',            // Unique server ID
		'my-plugin',                           // REST API namespace
		'content-mcp',                         // REST API route
		'My Plugin Content Server',             // Server name
		'Exposes content-editing abilities over MCP.', // Description
		'v1.0.0',                              // Server version
		array( \WP\MCP\Transport\HttpTransport::class ),
		\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
		\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
		array( 'my-plugin/create-draft-post' ), // Tools (abilities)
		array(),                                // Resources
		array(),                                // Prompts
		function () {                           // Transport-level permission callback
			return current_user_can( 'edit_posts' );
		}
	);
} );

Unlike the default server, a custom server lists its abilities explicitly as its 10th argument, they’re exposed directly as named MCP tools, in the actual tools/list response an AI client sees, no discover-abilities indirection required, and no meta.mcp.public flag needed either. The endpoint this registers is /wp-json/my-plugin/content-mcp.

Step 2: The transport-level permission callback, a second gate

The last argument is a server-wide permission callback, distinct from any single ability’s permission_callback. If you omit it, the adapter defaults to is_user_logged_in(), any authenticated user can reach the server, and each ability’s own check still applies on top. Passing your own callback here, as above, means someone who is merely logged in but lacks edit_posts never even reaches the ability layer for this particular server.

The two permission layers, and where each applies
LayerScopeDefault if omitted
Transport permission callbackEvery request to this one server, before any specific ability is chosen.is_user_logged_in()
Ability’s own permission_callbackThat one ability, every time it’s called, on any server that exposes it.None, required per ability.

Step 3: Understand the session protocol before calling it

The HTTP transport implements the MCP 2025-11-25 Streamable HTTP session protocol, meaning a client can’t just fire a single request. It has to complete a short handshake first:

MCP HTTP session flow
1
Initialize
POST an "initialize" JSON-RPC request. No session header needed yet.
2
Capture the session ID
The response includes an Mcp-Session-Id header, a UUID your client must store.
3
Send notifications/initialized
Tells the server the client is ready, using the captured session header.
4
Include the header on every request after that
Every following POST and DELETE must carry Mcp-Session-Id.
5
Terminate with DELETE
Cleans up the session when the client is done.

Step 4: Call it with curl, using an Application Password

The MCP Adapter has no authentication system of its own, it relies entirely on WordPress’s existing REST API authentication: cookie/nonce auth for logged-in browser sessions, or Application Passwords for anything programmatic, like this. Generate one under your user’s profile in wp-admin, then:

# File: terminal
SITE="https://yoursite.test"
AUTH="your-username:xxxx xxxx xxxx xxxx xxxx xxxx"

# 1. Initialize and capture the session ID
SESSION_ID=$(curl -s -D - -X POST \
  "$SITE/wp-json/my-plugin/content-mcp" \
  --user "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0.0"}}}' \
  | grep -i 'mcp-session-id' | awk '{print $2}' | tr -d '\r')

# 2. Tell the server the client is ready
curl -s -X POST "$SITE/wp-json/my-plugin/content-mcp" \
  --user "$AUTH" \
  -H "Content-Type: application/json" \
  -H "Mcp-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

# 3. List tools
curl -s -X POST "$SITE/wp-json/my-plugin/content-mcp" \
  --user "$AUTH" \
  -H "Content-Type: application/json" \
  -H "Mcp-Session-Id: $SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

Test it

The tools/list response should include an entry named my-plugin-create-draft-post, note the slash from the ability’s registration name becomes a hyphen, MCP tool names can’t contain /. Then close the session cleanly:

# File: terminal
curl -s -X DELETE "$SITE/wp-json/my-plugin/content-mcp" \
  --user "$AUTH" \
  -H "Mcp-Session-Id: $SESSION_ID"
Never skip the transport permission callback on a production server

Leaving the transport permission callback unset defaults to is_user_logged_in(), which is a reasonable floor for a local dev site but is rarely the right default for a production server exposing anything more sensitive than read-only content. Pass an explicit callback checking the specific capability your server’s abilities actually need, the same way you would for any REST API endpoint you’d be comfortable putting on the public internet.

Recap

A custom MCP server is created with McpAdapter::create_server() on the mcp_adapter_init hook, listing its transports, error and observability handlers, abilities, and an optional transport-level permission callback. HTTP transport follows a stateful session protocol: initialize, capture the Mcp-Session-Id header, then include it on every subsequent request. Authentication is WordPress’s own Application Passwords, there’s no separate OAuth layer to configure. The next lesson covers the other transport this same server can use: STDIO, driven by WP-CLI.

Resources & further reading

← Controlling MCP Visibility with meta.mcp.public Serving Abilities Over STDIO via WP-CLI →