AI and MCP

This content was generated with the assistance of AI and should be verified against the official documentation of each platform before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written against WordPress 7.1.3 and WooCommerce 11.2.1 (PHP 8.3.36, MariaDB 11.8), with the MCP Adapter plugin 0.7.1 (released 2026-10-16, requires WordPress 6.9 and PHP 7.4) and the Model Context Protocol revision 2025-06-18, and it was checked on 2026-10-27. The WooCommerce 11.3 changes (the removal of the old MCP endpoint, the extensible abilities and the Universal Commerce Protocol) were checked on a separate, throw-away site with WooCommerce 11.3.0-beta.1, the first beta of 11.3 (2026-10-26); 11.3 has no stable release yet, so everything attributed to it can still change. The whole feature set is a developer preview in the official documentation. It applies to every WooCommerce store on WordPress 6.9 or later, with no plan or edition restriction; hosting plans that do not allow installing plugins cannot use the adapter. The page follows the pages on the REST API and webhooks and on the Store API, which are the non-agent ways to reach the same data, and on performance and security, whose hardening advice applies to every endpoint opened here. For the protocol itself see the section on the Model Context Protocol, in particular the pages on architecture, primitives, authorization, security and testing and debugging.

The running example is Pocket Coffee. The plugin pocket-coffee-mcp registers the shop’s shipping quote (flat 4.90 EUR in Spain, free from 40 EUR, flat 9.90 EUR in the rest of the European Union, at most 5 kg per parcel) as an ability that is offered to agents as a tool, the shipping policy as an MCP resource, a restock email request as an MCP prompt and a stock audit that is deliberately kept away from agents. An agent that works for the shop owner can then ask what Madrid and Berlin customers pay, read WooCommerce’s product catalogue and, with the right permissions, change an order. Everything marked as run was run on the local store; what could not be run is listed in What was run and what was not.

How the pieces fit

Four layers are involved, and each is a separate package with its own version.

Layer What it is Where it lives

Abilities API

A registry of named, typed capabilities: a name such as pocket-coffee/shipping-quote, a label and description for humans and models, a JSON Schema for the input and the output, a permission callback and an execute callback. It knows nothing about AI; REST and the adapters are consumers.

WordPress core. The functions in the 7.1.3 source are tagged @since 6.9.0; meta.public and the $args of wp_get_abilities() were added in 7.1.0.

WooCommerce abilities

Purpose-built abilities for products and orders, registered in the woocommerce category.

The WooCommerce plugin (src/Internal/Abilities). MCP Adapter support was first added in 10.3.0 (2025-10-22).

MCP Adapter

Turns abilities into MCP tools, resources and prompts and serves them over HTTP (a REST route) or over STDIO (a WP-CLI command). It owns sessions, protocol handling and the three generic tools of the default server.

The plugin mcp-adapter from WordPress.org. WooCommerce 11.3 no longer bundles it.

Authentication

The caller is a WordPress user. Remote clients send an Application Password with HTTP Basic authentication; the local STDIO transport runs as the user given with --user.

WordPress core (Application Passwords).

The figure shows the registrants on the left, the core registry in the middle and the doors on the right, with the numbers and routes seen on the local store.

Abilities from WordPress core, WooCommerce and the Pocket Coffee plugin go into the Abilities API registry, which is reached through the REST routes, the MCP Adapter HTTP server and the WP-CLI STDIO command; the old WooCommerce MCP endpoint is shown as removed

Registering an ability

An ability is registered in two steps, both on hooks of the Abilities API, never at plugin load time: the category on wp_abilities_api_categories_init and the ability on wp_abilities_api_init. Registering outside those hooks is rejected with a _doing_it_wrong notice, and registering an ability whose category does not exist fails. The arguments of wp_register_ability( $name, $args ) are these.

Argument Meaning

$name

namespace/ability, lowercase letters, digits, dashes and slashes. The prefix should be the plugin slug.

label, description

Required. The description is what a model reads to decide when to call the ability, so say what it does and when to use it.

category

Required. A category registered with wp_register_ability_category( $slug, array( 'label', 'description' ) ).

input_schema, output_schema

Optional JSON Schema objects. Input is validated before the callbacks run and output is validated after; an invalid input is an error result, not an exception. Use additionalProperties => false and required.

execute_callback

Required. Receives the validated input and returns any value or a WP_Error.

permission_callback

Required. Receives the same input and returns true, false or a WP_Error. Check a real capability here, never __return_true for anything that is not public data.

meta.annotations

readonly, destructive and idempotent hints (true, false or null) for tools and documentation. They are hints; nothing enforces them. A read-only ability must be called with GET over REST.

meta.public

Since 7.1.0. Whether the ability is meant for clients such as REST, MCP or agents; it seeds the per-channel flags below. Default false.

meta.show_in_rest

Whether the ability can be listed and run under /wp-json/wp-abilities/v1. Defaults to the value of meta.public, else false.

meta.mcp.public, meta.mcp.type

Read by the MCP Adapter: public exposes the ability to MCP clients, type is tool (default), resource or prompt. Resources need meta.mcp.uri and may set mimeType.

ability_class

Optional class that extends WP_Ability, for custom behaviour.

The functions around it are wp_unregister_ability(), wp_has_ability(), wp_get_ability(), wp_get_abilities() (with an $args filter since 7.1.0) and the category counterparts wp_register_ability_category(), wp_unregister_ability_category(), wp_has_ability_category(), wp_get_ability_category() and wp_get_ability_categories(). A WP_Ability object offers check_permissions(), execute(), validate_input() and getters. The hooks that a plugin can use to observe or change the flow are the filters wp_register_ability_args, wp_ability_normalize_input, wp_ability_validate_input, wp_ability_permission_result, wp_pre_execute_ability, wp_ability_execute_result and wp_ability_validate_output, and the actions wp_ability_invoked, wp_before_execute_ability and wp_after_execute_ability.

The Pocket Coffee plugin

The first part of the plugin registers the category and the shipping quote. The ability exposes the quote as an MCP tool (meta.mcp.public true), asks for the WooCommerce capability manage_woocommerce and uses WC()->countries->get_european_union_countries() for the zone, so the example has real WooCommerce behaviour behind it.

<?php
/**
 * Plugin Name:       Pocket Coffee MCP
 * Description:       Exposes the Pocket Coffee shipping quote as a WordPress ability and an MCP tool.
 * Version:           0.1.0
 * Requires at least: 7.0
 * Requires PHP:      7.4
 * Requires Plugins:  woocommerce
 * Author:            Pocket Coffee
 * License:           GPL-2.0-or-later
 * Text Domain:       pocket-coffee-mcp
 *
 * @package PocketCoffeeMcp
 */

defined( 'ABSPATH' ) || exit;

/**
 * Register the ability category.
 */
function pcmcp_register_category() {
	if ( ! function_exists( 'wp_register_ability_category' ) ) {
		return;
	}

	wp_register_ability_category(
		'pocket-coffee',
		array(
			'label'       => __( 'Pocket Coffee', 'pocket-coffee-mcp' ),
			'description' => __( 'Abilities provided by the Pocket Coffee shop.', 'pocket-coffee-mcp' ),
		)
	);
}
add_action( 'wp_abilities_api_categories_init', 'pcmcp_register_category' );

/**
 * Shipping quote: Spain 4.90 EUR (free from 40 EUR), rest of the EU 9.90 EUR, 5 kg per parcel.
 *
 * @param array $input Validated input: country, subtotal, weight_kg.
 * @return array|WP_Error
 */
function pcmcp_shipping_quote( $input ) {
	$country  = strtoupper( $input['country'] );
	$subtotal = (float) $input['subtotal'];
	$weight   = (float) $input['weight_kg'];

	if ( $weight > 5.0 ) {
		return new WP_Error( 'pcmcp_too_heavy', __( 'A parcel cannot exceed 5 kg.', 'pocket-coffee-mcp' ) );
	}

	if ( 'ES' === $country ) {
		$rate = $subtotal >= 40.0 ? 0.0 : 4.90;
		$zone = 'Spain';
	} elseif ( in_array( $country, WC()->countries->get_european_union_countries(), true ) ) {
		$rate = 9.90;
		$zone = 'European Union';
	} else {
		return new WP_Error( 'pcmcp_no_zone', __( 'Pocket Coffee does not ship there.', 'pocket-coffee-mcp' ) );
	}

	return array(
		'zone'     => $zone,
		'rate'     => $rate,
		'currency' => get_woocommerce_currency(),
	);
}

/**
 * Register the ability.
 */
function pcmcp_register_ability() {
	if ( ! function_exists( 'wp_register_ability' ) ) {
		return;
	}

	wp_register_ability(
		'pocket-coffee/shipping-quote',
		array(
			'label'               => __( 'Pocket Coffee shipping quote', 'pocket-coffee-mcp' ),
			'description'         => __( 'Returns the shipping zone and rate for a destination country, subtotal and parcel weight.', 'pocket-coffee-mcp' ),
			'category'            => 'pocket-coffee',
			'input_schema'        => array(
				'type'                 => 'object',
				'properties'           => array(
					'country'   => array(
						'type'        => 'string',
						'description' => 'ISO 3166-1 alpha-2 country code.',
						'pattern'     => '^[A-Za-z]{2}$',
					),
					'subtotal'  => array(
						'type'    => 'number',
						'minimum' => 0,
					),
					'weight_kg' => array(
						'type'    => 'number',
						'minimum' => 0,
					),
				),
				'required'             => array( 'country', 'subtotal', 'weight_kg' ),
				'additionalProperties' => false,
			),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'zone'     => array( 'type' => 'string' ),
					'rate'     => array( 'type' => 'number' ),
					'currency' => array( 'type' => 'string' ),
				),
			),
			'execute_callback'    => 'pcmcp_shipping_quote',
			'permission_callback' => static function () {
				return current_user_can( 'manage_woocommerce' );
			},
			'meta'                => array(
				'show_in_rest' => true,
				'annotations'  => array(
					'readonly'    => true,
					'destructive' => false,
					'idempotent'  => true,
				),
				'mcp'          => array(
					'public' => true,
					'type'   => 'tool',
				),
			),
		)
	);
}
add_action( 'wp_abilities_api_init', 'pcmcp_register_ability' );

The same ability is reachable without MCP through the Abilities REST routes, authenticated here with an Application Password (${WP_APP_PASSWORD} stands for a password created for a test user; the local Docker image needs the header X-Forwarded-Proto: https to treat the plain-HTTP request as secure, as for the REST API keys). A read-only ability runs with GET and the input as input[name] query parameters; running it with POST is refused with HTTP 405.

curl -s -H 'X-Forwarded-Proto: https' -u "admin:${WP_APP_PASSWORD}" -G \
  "http://localhost:8081/wp-json/wp-abilities/v1/abilities/pocket-coffee/shipping-quote/run" \
  --data-urlencode 'input[country]=DE' --data-urlencode 'input[subtotal]=20' --data-urlencode 'input[weight_kg]=1'
# {"zone":"European Union","rate":9.9,"currency":"EUR"}

curl -s ... --data-urlencode 'input[country]=ES' --data-urlencode 'input[subtotal]=20'
# {"code":"ability_invalid_input","message":"Ability \"pocket-coffee/shipping-quote\" has invalid input. Reason: weight_kg is a required property of input.","data":{"status":400}}

From PHP the same ability is wp_get_ability( 'pocket-coffee/shipping-quote' ), then ->check_permissions() and ->execute( $input ), which returns the output array or a WP_Error. With a user that may manage WooCommerce, check_permissions() returned true, execute( array( 'country' => 'FR', 'subtotal' => 10, 'weight_kg' => 2 ) ) returned the European Union quote, and the same call without weight_kg returned a WP_Error with the code ability_invalid_input and the message "weight_kg is a required property of input". One pitfall showed up while testing: an ability callback that calls WC() fails with the error ability_callback_exception ("Call to undefined function WC()") whenever WooCommerce is not loaded, which happened for a moment on the shared store when another test deactivated it; declare Requires Plugins: woocommerce and keep the callbacks defensive.

The second part registers the other two kinds of MCP component and one ability that is not offered to agents. A resource is a read-only document that a client can attach to a conversation: meta.mcp.type resource with a URI (any RFC 3986 scheme; pocket-coffee://shipping-policy here). A prompt is a reusable message template: the callback returns messages in the MCP shape, and the input schema becomes the prompt’s arguments. The third ability, pocket-coffee/stock-audit, sets only show_in_rest, so it is available to REST clients but not to MCP.

/**
 * Register a resource, a prompt and an ability that is kept away from MCP clients.
 */
function pcmcp_register_more_abilities() {
	if ( ! function_exists( 'wp_register_ability' ) ) {
		return;
	}

	// A read-only document that an agent can attach to a conversation: type "resource" needs a URI.
	wp_register_ability(
		'pocket-coffee/shipping-policy',
		array(
			'label'               => __( 'Pocket Coffee shipping policy', 'pocket-coffee-mcp' ),
			'description'         => __( 'The shipping rules of Pocket Coffee as plain text.', 'pocket-coffee-mcp' ),
			'category'            => 'pocket-coffee',
			'execute_callback'    => static function () {
				return "Spain: 4.90 EUR, free from 40 EUR.\nRest of the EU: 9.90 EUR.\nMaximum 5 kg per parcel.";
			},
			'permission_callback' => static function () {
				return current_user_can( 'manage_woocommerce' );
			},
			'meta'                => array(
				'show_in_rest' => true,
				'annotations'  => array(
					'readonly'    => true,
					'destructive' => false,
					'idempotent'  => true,
				),
				'mcp'          => array(
					'public'   => true,
					'type'     => 'resource',
					'uri'      => 'pocket-coffee://shipping-policy',
					'mimeType' => 'text/plain',
				),
			),
		)
	);

	// A prompt template: the callback returns messages for the client to send to its model.
	wp_register_ability(
		'pocket-coffee/restock-prompt',
		array(
			'label'               => __( 'Restock reminder prompt', 'pocket-coffee-mcp' ),
			'description'         => __( 'Builds a prompt that asks the model to draft a restock email for one product.', 'pocket-coffee-mcp' ),
			'category'            => 'pocket-coffee',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'sku' => array(
						'type'        => 'string',
						'description' => 'Product SKU, for example PC-ESP-1KG.',
					),
				),
				'required'   => array( 'sku' ),
			),
			'execute_callback'    => static function ( $input ) {
				return array(
					'messages' => array(
						array(
							'role'    => 'user',
							'content' => array(
								'type' => 'text',
								'text' => sprintf( 'Draft a short email to our roaster asking for a restock of %s.', $input['sku'] ),
							),
						),
					),
				);
			},
			'permission_callback' => static function () {
				return current_user_can( 'manage_woocommerce' );
			},
			'meta'                => array(
				'show_in_rest' => true,
				'mcp'          => array(
					'public' => true,
					'type'   => 'prompt',
				),
			),
		)
	);

	// Visible through the REST API, but never offered to MCP clients: no "mcp.public".
	wp_register_ability(
		'pocket-coffee/stock-audit',
		array(
			'label'               => __( 'Stock audit', 'pocket-coffee-mcp' ),
			'description'         => __( 'Counts the products that track stock. For back-office use only.', 'pocket-coffee-mcp' ),
			'category'            => 'pocket-coffee',
			'execute_callback'    => static function () {
				$ids = wc_get_products(
					array(
						'limit'        => -1,
						'manage_stock' => true,
						'return'       => 'ids',
					)
				);

				return array( 'tracked' => count( $ids ) );
			},
			'permission_callback' => static function () {
				return current_user_can( 'manage_woocommerce' );
			},
			'meta'                => array(
				'show_in_rest' => true,
				'annotations'  => array( 'readonly' => true ),
			),
		)
	);
}
add_action( 'wp_abilities_api_init', 'pcmcp_register_more_abilities' );

The abilities that WooCommerce 11.2.1 registers

WooCommerce registers its abilities on the same hooks, through Automattic\WooCommerce\Internal\Abilities\AbilitiesRegistry (with AbilitiesLoader and AbilitiesCategories next to it and the classes of Abilities/Domain that hold each ability). Listing the REST route as the shop owner on the local store showed ten abilities, three of them from core:

Ability Annotations In the MCP default server Purpose

woocommerce/products-query

readonly, idempotent

yes (mcp.public)

Find products by id or filters: id, search, sku (partial match, * for any), status, product_type_alias (physical, virtual, digital, affiliate, grouped), stock_status, page and per_page (1 to 100, default 10).

woocommerce/product-create

not readonly, not destructive

yes

Create a product.

woocommerce/product-update

not readonly, destructive

yes

Update a product.

woocommerce/product-delete

not readonly, destructive, idempotent

yes

Delete a product.

woocommerce/orders-query

readonly, idempotent

yes

Find orders by id or common filters.

woocommerce/order-add-note

not readonly, not destructive

yes

Add a note to an order.

woocommerce/order-update-status

not readonly, destructive

yes

Set an order status to pending, processing, on-hold, completed, cancelled, refunded or failed, with an optional note.

Two categories exist for WooCommerce: woocommerce for the purpose-built abilities above and woocommerce-rest for abilities derived from REST controllers, which belonged to the old endpoint. The purpose-built abilities have meta.public false and meta.mcp.public true, so they are offered through MCP, which is the reason why the explicit MCP flag wins over meta.public in the rules below. On the 11.3.0-beta.1 site the list had one more entry, woocommerce/order-notes-query, and no other change. These abilities enforce WooCommerce’s own permissions in their callbacks; use a dedicated user that holds only what the agent needs.

The MCP Adapter

Installing and what it creates

The adapter is a normal plugin: wp plugin install mcp-adapter --activate. Plugins that depend on it should add mcp-adapter to the Requires Plugins header. When WordPress starts it creates the default server (the filter mcp_adapter_create_default_server can turn that off) and fires the action mcp_adapter_init, where a plugin can create its own servers. The default server on the local store, as wp mcp-adapter list printed it (the second line appears after the next section’s plugin code registers its own server):

$ wp mcp-adapter list
ID	Name	Version	Tools	Resources	Prompts
pocket-coffee-server	Pocket Coffee MCP server	1.0.0	1	1	1
mcp-adapter-default-server	MCP Adapter Default Server	v1.0.0	3	1	1
Property Default server

Route (HTTP)

/wp-json/mcp/mcp-adapter-default-server (namespace mcp). The filter mcp_adapter_default_server_config changes id, route, name, description and version.

Tools

Three generic tools: mcp-adapter-discover-abilities, mcp-adapter-get-ability-info and mcp-adapter-execute-ability. Every exposed ability is reached through them; tools/list does not show one tool per ability.

Resources and prompts

Abilities with mcp.type resource or prompt are listed directly in resources/list and prompts/list.

Transport permission

The user must be logged in and have the read capability (filter mcp_adapter_default_transport_permission_user_capability); the three tools check read too (filters mcp_adapter_discover_abilities_capability, mcp_adapter_get_ability_info_capability, mcp_adapter_execute_ability_capability).

Sessions

initialize returns an Mcp-Session-Id header; later requests send it with MCP-Protocol-Version. Limits per user and inactivity timeout are filterable (mcp_adapter_session_max_per_user, mcp_adapter_session_inactivity_timeout).

Which abilities are exposed

The adapter resolves exposure after registration is complete: an explicit meta.mcp.public wins; when it is absent the ability follows meta.public, which defaults to false; a malformed meta.mcp fails closed. The figure also lists what is checked on every call. On the local store the discover tool listed core’s three abilities (they set public), the seven WooCommerce abilities and pocket-coffee/shipping-quote, and not pocket-coffee/stock-audit. Calling the hidden ability through the execute tool gave a "not found" error, while the REST route answered {"tracked":3}.

Decision chart: an explicit meta.mcp.public value wins, otherwise meta.public decides, and exposed abilities are then checked for the read capability, the input schema, the permission callback and the mcp.type

A full session

The sequence below is the call order of every MCP client, whatever the transport: initialise, learn the tools, discover what the site offers, read the schema of one ability, execute it. Each step was run with curl against the local store, so the replies quoted afterwards are real.

sequenceDiagram participant A as Agent (MCP client) participant P as MCP Adapter (HTTP transport) participant R as Abilities API participant W as Ability callback A->>P: POST initialize with an Application Password P-->>A: capabilities and the Mcp-Session-Id header A->>P: notifications/initialized P-->>A: 202 Accepted A->>P: tools/call mcp-adapter-discover-abilities P->>R: wp_get_abilities(), only abilities with mcp.public R-->>P: names, labels and descriptions P-->>A: the list of abilities A->>P: tools/call mcp-adapter-get-ability-info P-->>A: input_schema, output_schema, meta A->>P: tools/call mcp-adapter-execute-ability P->>R: validate input, check permission_callback R->>W: execute_callback with the input W-->>R: result or WP_Error R-->>P: output checked against output_schema P-->>A: content and structuredContent, or isError true

The script that produced the replies is below. The Application Password comes from the environment and never appears in the file; -D saves the response headers so that the session id can be read from them.

#!/bin/sh
# WP_APP_PASSWORD: an Application Password of a user with the read capability (create it with
#   wp user application-password create <user> <name> --porcelain). Local Docker only: the header X-Forwarded-Proto.
URL=http://localhost:8081/wp-json/mcp/mcp-adapter-default-server
C="-s -H X-Forwarded-Proto:https -u admin:${WP_APP_PASSWORD} -H Content-Type:application/json"

curl $C -D headers.txt -o /dev/null -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' $URL
SID=$(grep -i '^mcp-session-id' headers.txt | awk '{print $2}' | tr -d '\r')
H="-H Mcp-Session-Id:$SID -H MCP-Protocol-Version:2025-06-18"

curl $C $H -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' $URL
curl $C $H -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' $URL
curl $C $H -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"mcp-adapter-discover-abilities","arguments":{}}}' $URL
curl $C $H -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"mcp-adapter-get-ability-info","arguments":{"ability_name":"pocket-coffee/shipping-quote"}}}' $URL
curl $C $H -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"mcp-adapter-execute-ability","arguments":{"ability_name":"pocket-coffee/shipping-quote","parameters":{"country":"ES","subtotal":35,"weight_kg":1}}}}' $URL

What the store answered (shortened where marked):

initialize          -> {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"MCP Adapter Default Server","version":"v1.0.0"},"instructions":"Default MCP server for WordPress abilities discovery and execution"}}
notifications/initialized -> HTTP 202
tools/list          -> ['mcp-adapter-discover-abilities', 'mcp-adapter-get-ability-info', 'mcp-adapter-execute-ability']
discover-abilities  -> core/get-site-info, core/get-user-info, core/get-environment-info, pocket-coffee/shipping-quote, woocommerce/orders-query, woocommerce/order-add-note, woocommerce/order-update-status, woocommerce/products-query, woocommerce/product-create, woocommerce/product-delete, woocommerce/product-update
execute-ability     -> {"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"{\"success\":true,\"data\":{\"zone\":\"Spain\",\"rate\":4.9,\"currency\":\"EUR\"}}"}],"isError":false,"structuredContent":{"success":true,"data":{"zone":"Spain","rate":4.9,"currency":"EUR"}}}}

Failures are reported inside a normal JSON-RPC result with "isError": true, so a client (and the model) can read the reason. These came back from the store:

Call Result

Overweight parcel (6 kg) for Germany

A parcel cannot exceed 5 kg. (the WP_Error message of the callback)

weight_kg sent as the string "heavy"

Ability "pocket-coffee/shipping-quote" has invalid input. Reason: input[weight_kg] is not of type number.

Country US

Pocket Coffee does not ship there.

An ability that is not exposed

Ability 'pocket-coffee/stock-audit' not found on this WordPress site, and no ability you can call has a similar name. Call the mcp-adapter-discover-abilities tool to see what this site offers, ...

A customer account (role customer, with an Application Password of its own) calling pocket-coffee/shipping-quote or woocommerce/orders-query

Permission denied (discover worked, because the account has read)

No credentials

HTTP 401 before any JSON-RPC is processed

A WooCommerce ability through the same tool, woocommerce/products-query with the parameters {"sku":"PC-ESP-1KG","per_page":1}, returned total_pages 1, page 1, per_page 1 and the product with id 10, type simple, status publish, sku PC-ESP-1KG, currency EUR and price 19.90.

Resources and prompts

The default server also lists the abilities of type resource and prompt. On the local store:

resources/list -> {"resources":[{"name":"pocket-coffee/shipping-policy","uri":"pocket-coffee://shipping-policy","title":"Pocket Coffee shipping policy","description":"The shipping rules of Pocket Coffee as plain text.","mimeType":"text/plain"}]}
resources/read -> {"contents":[{"uri":"pocket-coffee://shipping-policy","text":"Spain: 4.90 EUR, free from 40 EUR.\nRest of the EU: 9.90 EUR.\nMaximum 5 kg per parcel."}]}
prompts/list   -> {"prompts":[{"name":"pocket-coffee-restock-prompt","title":"Restock reminder prompt", ... "arguments":[{"name":"sku","description":"Product SKU, for example PC-ESP-1KG.","required":true}]}]}
prompts/get    -> {"messages":[{"role":"user","content":{"type":"text","text":"Draft a short email to our roaster asking for a restock of PC-ESP-1KG."}}], ...}

Names are sanitised for MCP: the slash of pocket-coffee/restock-prompt became a dash. Filters exist to change tool, prompt, resource names and URIs (mcp_adapter_tool_name, mcp_adapter_prompt_name, mcp_adapter_resource_name, mcp_adapter_resource_uri) and to rewrite resource lists and reads.

A dedicated server

The generic tools are a good default for an assistant that explores a site, but a focused integration is better served by a server whose tool list shows the abilities themselves, with their schemas. The third part of the plugin creates one on mcp_adapter_init with create_server(): the arguments are the server id, the REST namespace and route, name, description, version, the transports, the error and observability handler classes, and then the lists of tools, resources and prompts as ability names (an optional last argument is a transport permission callback that replaces the default login check). WooCommerce’s own provider uses the same call.

/**
 * A dedicated MCP server: its tools list shows the abilities themselves, not the three generic adapter tools.
 *
 * @param object $adapter The MCP Adapter instance.
 */
function pcmcp_create_server( $adapter ) {
	if ( ! class_exists( '\WP\MCP\Transport\HttpTransport' ) ) {
		return;
	}

	$adapter->create_server(
		'pocket-coffee-server',
		'pocket-coffee',
		'mcp',
		'Pocket Coffee MCP server',
		'Shipping quotes, the shipping policy and a restock prompt.',
		'1.0.0',
		array( \WP\MCP\Transport\HttpTransport::class ),
		\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
		\WP\MCP\Infrastructure\Observability\NullMcpObservabilityHandler::class,
		array( 'pocket-coffee/shipping-quote' ),
		array( 'pocket-coffee/shipping-policy' ),
		array( 'pocket-coffee/restock-prompt' )
	);
}
add_action( 'mcp_adapter_init', 'pcmcp_create_server' );

The server answers at /wp-json/pocket-coffee/mcp. Its tools/list returned one tool, pocket-coffee-shipping-quote, with the input schema of the ability, the output schema and the annotation hint readOnlyHint true (the mapping of meta.annotations.readonly), and tools/call on that tool returned {"zone":"European Union","rate":9.9,"currency":"EUR"} as structuredContent. wp mcp-adapter list shows both servers (listing above).

Connecting an AI client

The official guide describes two ways for a client to connect, both of which were exercised only in part here.

  • Local site, STDIO. The client starts wp mcp-adapter serve --server=mcp-adapter-default-server --user=<user> and talks JSON-RPC over standard input and output. No password is involved: the process runs as the given user and needs the WordPress files and WP-CLI. The command was run on the local store through Docker with two JSON-RPC lines on its input and returned the initialize result and the three tools.

  • Remote site, HTTP. The client reaches the REST route with an Application Password. Clients that speak only STDIO use the proxy @automattic/mcp-wordpress-remote (0.5.4 on npm today), which turns STDIO into HTTP requests and adds the credentials from its environment.

{
  "mcpServers": {
    "woocommerce_store": {
      "command": "wp",
      "args": [
        "--path=/path/to/your/wordpress/site",
        "mcp-adapter",
        "serve",
        "--server=mcp-adapter-default-server",
        "--user=admin"
      ]
    }
  }
}
{
  "mcpServers": {
    "woocommerce_store": {
      "command": "npx",
      "args": [ "-y", "@automattic/mcp-wordpress-remote@latest" ],
      "env": {
        "WP_API_URL": "https://yourstore.example.com/wp-json/mcp/mcp-adapter-default-server",
        "WP_API_USERNAME": "agent-reader",
        "WP_API_PASSWORD": "<application-password>"
      }
    }
  }
}
# Claude Code, local STDIO
claude mcp add woocommerce_store -- wp --path=/path/to/your/wordpress/site mcp-adapter serve --server=mcp-adapter-default-server --user=admin

# Claude Code, remote HTTP through the proxy; the password comes from the environment
claude mcp add --env WP_API_URL=https://yourstore.example.com/wp-json/mcp/mcp-adapter-default-server \
  --env WP_API_USERNAME=agent-reader --env WP_API_PASSWORD="${WP_APP_PASSWORD}" \
  woocommerce_store -- npx -y @automattic/mcp-wordpress-remote@latest

The remote proxy was not started for this page, and no real AI client was connected: the local store runs over plain HTTP, and the proxy needs a reachable HTTPS URL. Claude Desktop, Claude Code and Cursor use the mcpServers property; VS Code uses servers. Do not commit a configuration that contains an Application Password.

Security of an agent-facing store

  • Use a dedicated user with least privilege. The agent acts as that user. An administrator account hands the model every capability, including order-update-status (which can mark an order refunded) and product-delete. The permission check of each ability is the real boundary: on the local store a customer account could list abilities but every call to a WooCommerce ability returned "Permission denied".

  • Use an Application Password, not the account password or a REST API key. Create it under Users > Profile > Application Passwords or with wp user application-password create, revoke it independently (wp user application-password delete), and give each client its own. WordPress shows the value only once. Remote use needs HTTPS.

  • Expose only what the agent needs. Do not add mcp.public to an ability because it is convenient; write a small ability with a narrow schema instead of exposing a general one. The default is hidden.

  • Treat annotations as hints. destructive and readonly describe the tool to the client; clients may ask a human to confirm destructive tools, but the server does not rely on it.

  • Mind personal data. Order and customer abilities return names, emails and addresses. The documentation warns that the store owner stays responsible for data protection compliance, retention and handling when an AI service processes that data.

  • Validate inputs and outputs with the schema, and do not put secrets in descriptions or schemas: they are shown to the model.

  • Assume tool results can contain instructions. Product titles, reviews and order notes are written by third parties and reach the model through the tool results. The pages on MCP security and excessive agency and tool security cover prompt injection and confirmation of destructive actions.

  • Log. WooCommerce writes ability errors to WooCommerce > Status > Logs with the source woocommerce-abilities.

Version history: the old endpoint and its removal

Version Change

10.3.0 (2025-10-22)

MCP Adapter added to WooCommerce and connected to the Abilities API; an experimental WooCommerce MCP server at /wp-json/woocommerce/mcp that exposed REST-derived abilities and authenticated with a WooCommerce REST API key in the X-MCP-API-Key header.

10.4.0 (2025-12-10)

Adapter updated to 0.3.0.

10.9.0

Purpose-built (canonical) product and order abilities added; the REST-derived ones stayed in the old endpoint through the metadata expose_in_deprecated_woocommerce_mcp.

11.2.1 (stable today)

The legacy server is created only when the experimental feature mcp_integration is on. On the local store the feature was off and /wp-json/woocommerce/mcp answered 404.

11.3.0-beta.1 (2026-10-26)

The endpoint is removed, with its REST-derived abilities, the working legacy server (the beta’s feature list still shows an experimental mcp_integration entry, off), the filters woocommerce_mcp_include_ability, woocommerce_mcp_allow_insecure_transport and woocommerce_check_rest_ability_permissions_for_method, and the expose_in_deprecated_woocommerce_mcp metadata. The adapter is no longer bundled: install it as a plugin. woocommerce/order-notes-query is added.

On the 11.3.0-beta.1 site a request to the old route is answered with HTTP 410 Gone and a JSON-RPC error that tells the client what to do (the message names the adapter plugin, the new URL and the Application Password requirement):

HTTP/1.1 410 Gone
{"jsonrpc":"2.0","id":null,"error":{"code":-32000,"message":"The WooCommerce MCP endpoint (woocommerce/mcp) was removed in WooCommerce 11.3.0. To keep using WooCommerce with MCP, install and activate the MCP Adapter plugin (...) on this site, then connect your MCP client to http://localhost:8099/wp-json/mcp/mcp-adapter-default-server using a WordPress application password. Documentation: https://developer.woocommerce.com/docs/features/mcp/"}}

Migration checklist for a store or extension that used the old endpoint: install the adapter, change the client URL and credentials (Application Password instead of the REST key), and give every custom ability that used to appear in the old endpoint the metadata meta.mcp.public true (and meta.mcp.type), because the adapter ignores expose_in_deprecated_woocommerce_mcp, the namespace rule and the removed filter. Abilities that other tools registered with a woocommerce/ name are not carried over either.

Extensible abilities (experimental, 11.3)

The call for testing of 2026-10-22 introduces an experimental API with which extensions add their own data to WooCommerce’s resource abilities, so that an agent can read and change it through the abilities it already knows. It is off by default (the experimental feature extensible_abilities, which exists on 11.3.0-beta.1 and not on 11.2.1; the setting is under WooCommerce > Settings > Advanced > Features) and no hook fires while it is off. The announced pieces are ExtensibleAbilities::register_field() (read, under extensions.<namespace>.<field>, for product, order and order_item), an extensions input in woocommerce/product-create and woocommerce/product-update, register_validator(), register_query_filter() (under filters.extensions.<namespace>.<filter> of the two query abilities), register_resource() for custom abilities, and the actions woocommerce_extensible_abilities_init and woocommerce_extensible_abilities_resources_init. The authors advise against production use and list gaps such as order writes, custom product types and variable products that need multiple saves. Its code was not run for this page; only the presence of the feature flag on the beta was checked.

The experimental Universal Commerce Protocol flag

The Universal Commerce Protocol (UCP) is a protocol with a public specification at ucp.dev (the version the code implements is dated 2026-04-08) that lets shopping agents discover a merchant’s capabilities and query its catalogue. WooCommerce is adding server support behind a hidden experimental feature. The state on 2026-10-27, from the repository and from the beta site:

  • It is not in 11.2.1: the stable plugin has no ucp feature and no Ucp classes (checked in the plugin files and with the list of features on the local store).

  • It is in 11.3.0-beta.1 (and trunk): the feature id ucp, name "Universal Commerce Protocol", described as exposing the catalogue to AI shopping agents through UCP endpoints; experimental, off by default and hidden from the settings screen (disable_ui), because "the flag is set by the integration that drives UCP, not by merchants". The option is woocommerce_feature_ucp_enabled.

  • What the merged code provides is the discovery profile and three catalogue routes, implementing UCP spec version 2026-04-08. The cart endpoints and idempotency support were still open pull requests on 2026-10-27.

On a throw-away WordPress 7.1.3 site with WooCommerce 11.3.0-beta.1, pretty permalinks and one product (all removed afterwards), with the option unset the profile and the route answered 404. After wp option update woocommerce_feature_ucp_enabled yes and a rewrite flush:

wp option update woocommerce_feature_ucp_enabled yes
wp rewrite flush

# Discovery profile (public, served with Cache-Control: public, max-age=300)
curl -s http://localhost:8099/.well-known/ucp

# Catalogue search: a UCP-Agent header with the agent profile URL is required
curl -s -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -H 'UCP-Agent: profile="https://example.com/agent-profile"' \
  -d '{"query":"espresso","context":{"currency":"EUR"}}' \
  http://localhost:8099/wp-json/wc/ucp/v1/catalog/search

curl -s -X POST ... -d '{"ids":["11"]}' http://localhost:8099/wp-json/wc/ucp/v1/catalog/lookup
curl -s -X POST ... -d '{"id":"11"}'    http://localhost:8099/wp-json/wc/ucp/v1/catalog/product

The profile (shortened) named the services and capabilities, and the search answered with the product in UCP’s shape, prices in minor units and a warning when the requested currency is not the store’s:

{"ucp":{"version":"2026-04-08","services":{"dev.ucp.shopping":[{"version":"2026-04-08","transport":"rest","endpoint":"http://localhost:8099/wp-json/wc/ucp/v1", ...}]},
 "capabilities":{"dev.ucp.shopping.catalog.search":[ ... ],"dev.ucp.shopping.catalog.lookup":[ ... ]}}, "links":[{"rel":"self","href":"http://localhost:8099/.well-known/ucp"}]}

catalog/search -> "products":[{"id":"11","title":"Espresso Beans 1kg","price_range":{"min":{"amount":1990,"currency":"USD"}, ...},"variants":[{ ... "sku":"PC-ESP-1KG"}], ...}]
                  "messages":[{"type":"warning","code":"currency_not_supported","content":"This store only transacts in USD and does not convert currencies. ..."}]
catalog/lookup  -> status success, product id 11        catalog/product -> keys ucp, product
without UCP-Agent header -> HTTP 400 with "status":"error"

The discovery file needs pretty permalinks and a site in the domain root (no subdirectory); with plain permalinks the request was redirected with 301 instead of answered. UCP does not use the Abilities API or MCP: it is its own set of REST routes. Treat all of it as experimental: do not enable it on a production store, do not build on the route shapes, and expect the flag, the routes and the filter that gates them to change before a stable release.

Tooling for people who build with agents

Separate from exposing a store to agents, WooCommerce and WordPress publish material to help coding agents write extensions.

Resource What it is

llms.txt and llms-full.txt

https://developer.woocommerce.com/docs/llms.txt is a table of contents of the developer docs (title, URL, description) and llms-full.txt is the whole documentation as one Markdown file, for feeding an LLM or an AI-assisted IDE. They do not contain the PHP code reference.

Markdown versions of pages

Append .md to any documentation URL (for example .../docs/getting-started/ai.md), or use the clipboard icon of the page to copy it as Markdown.

Agent skills

The WooCommerce repository ships agent skills in .ai/skills/ (backend PHP conventions, code review, git workflow, build and lint, UI copy). For extension authors, woocommerce/agent-skills builds on WordPress/agent-skills with guidance for WooCommerce extensions. Skills are tool-agnostic SKILL.md files.

Cursor rules

The repository’s .cursor/rules/ holds guidance for working in the core codebase.

QIT for agents

The QIT documentation has an AI getting-started guide and a Claude Code plugin (/plugin marketplace add woocommerce/qit-cli, then /plugin install qit@woocommerce-qit); other assistants can read https://qit.woo.com/docs/llms.txt.

WordPress Studio

WooCommerce’s recommended local environment; it includes a WordPress-specific AI assistant and Blueprint support.

Demo plugin

woocommerce/wc-mcp-ability is a small plugin that registers an ability woocommerce-demo/store-info with schemas and permissions, for third-party developers.

One related thing is experimental and was not run: the Dual API (an experimental GraphQL engine that was moved out of WooCommerce core into the separate WooCommerce Dual API plugin in 11.2.0),. It is not a dependable base for a production integration yet.

What was run and what was not

Ran on the local WordPress 7.1.3 and WooCommerce 11.2.1 store: the plugin pocket-coffee-mcp (PHPCS with the WooCommerce standard clean, activated, used); the Abilities REST routes (list, categories, run with GET, input validation error, 405 for POST on a read-only ability); wp_get_ability() from PHP; the complete MCP session over HTTP with curl on the default server and on the custom server (initialize, initialized notification, tools, discover, get-info, execute, resources, prompts, error results, the hidden ability, permission denied for a customer account created and deleted for the test, 401 without credentials); the STDIO transport with two JSON-RPC lines through WP-CLI; wp mcp-adapter list; the 404 of the legacy endpoint. On a separate throw-away site (new database, containers removed afterwards) with WooCommerce 11.3.0-beta.1: the 410 reply of the removed endpoint, the list of abilities and features, the UCP discovery profile and the three UCP catalogue routes, the 400 without the UCP-Agent header. The Application Passwords used were created for the tests and are not shown.

Not run: a real AI client (Claude Desktop, Claude Code, Cursor) against the store; the @automattic/mcp-wordpress-remote proxy; the extensible abilities API; UCP cart endpoints (not merged) and checkout; the Dual API; the agent skills repositories; WordPress Studio; and anything that needs a hosted HTTPS site.