MCP — это ключ к ИИ-автоматизации сайта: он даёт агентам стандартный способ видеть WordPress, понимать его возможности и вызывать действия без ручного копипаста через админку.
Для агента сайт перестаёт быть «чёрным ящиком». Через MCP он может узнать, какие инструменты доступны: получить записи, проверить товары, посмотреть термины, создать черновик, обновить данные или вызвать кастомную функцию. WordPress со своей стороны отдаёт эти возможности как abilities, а MCP-сервер переводит запросы агента в REST-вызовы к сайту.

В этой статье разберём, как связать WordPress и OpenCode через MCP: из чего состоит связка, как её настроить и как добавить свою ability. Все шаги проверены на реальном сайте wpcraft.ru.
Составляющие и особенности
Что нужно на стороне WordPress
- WordPress 7.0+ — в ядре появился Abilities API. Это набор функций (
wp_register_ability,wp_register_ability_category) для регистрации инструментов, которые сайт умеет выполнять. Abilities API не отдаёт данные сам по себе — он реестр. - Плагин MCP Adapter — превращает зарегистрированные abilities в MCP-инструменты, доступные по JSON-RPC. Плагин лежит в репозитории
WordPress/mcp-adapterи поставляется как отдельный плагин или как vendor-зависимость плагина AI Experiments. - Плагин-источник abilities — сам по себе адаптер бесполезен без abilities. Источниками могут быть:
- плагин AI Experiments (поставляет
ai/get-post-details,ai/get-post-terms); - WooCommerce (поставляет
woocommerce/products-query,woocommerce/orders-query, etc.); - Secure Custom Fields (SCF) — 33 ability для управления post types, taxonomies, field groups;
- любой ваш кастомный плагин, зарегистрировавший свои abilities.
- плагин AI Experiments (поставляет
- Application Password — WordPress умеет генерировать пароли приложений в Users → Profile → Application Passwords. MCP-клиент использует basic-auth с этим паролем. Отдельный пароль удобно отзывать, не меняя основной.
- HTTPS — MCP-адаптер работает через REST API, REST требует pretty permalinks и HTTPS на проде.
Что нужно на стороне агента
OpenCode — CLI-инструмент для работы с кодом. Он умеет подключать MCP-серверы через конфиг opencode.json в корне проекта. Один MCP-сервер в конфиге — один источник инструментов для агента.
Другие ИИ-агенты настраиваются похожим образом:
- Claude Desktop —
claude_desktop_config.jsonс блокомmcpServers. - Cursor — Settings → MCP.
- ChatGPT Desktop — выбор MCP-приложения при создании чата.
Принцип один: агент знает адрес MCP-сервера и креды, дальше открывает сессию и вызывает инструменты.
Какие abilities мы видим?
На wpcraft.ru после активации плагинов MCP Adapter, AI Experiments, WooCommerce и Secure Custom Fields discovery возвращает 50 инструментов в трёх неймспейсах:
woocommerce/*— заказы и товары: query, create, update, delete, статус, заметки. Всего 7 abilities, работают.ai/*— детали записи и термины по ID. Всего 2 abilities, работают.scf/*— CRUD для post types, taxonomies, field groups, fields, options pages. Всего 33 abilities, требуют SCF-плагин.
У каждого ability есть метаданные. Это не права доступа, а подсказки для MCP-клиента: какие особенности присущи инструменту, нужно ли просить подтверждение и насколько безопасно повторять вызов.
readonly: true— ability только читает данные и не меняет состояние сайта. Примеры: получить список товаров, прочитать запись, вывести настройки. В WordPress client-side Abilities API такие вызовы могут выполняться черезGET.destructive: true— ability может удалить, перезаписать или необратимо изменить данные. Примеры: удалить товар, снять публикацию, очистить кэш, массово обновить цены. Такой инструмент лучше показывать агенту как рискованный и запускать только после подтверждения.idempotent: true— повторный вызов с теми же входными данными приводит к тому же итоговому состоянию. Пример: «обновить статус заказа наcompleted» — если статус ужеcompleted, повторный вызов ничего нового не создаст. Противоположный пример: «создать новый пост» — каждый повтор создаст ещё один пост, значит это не idempotent.destructive: true+idempotent: true— важная комбинация для удалений. В WordPress Abilities API она мапится наDELETE: удалить объект по ID можно повторить безопасно с точки зрения итогового состояния — объект всё равно удалён.readonly: false,destructive: false,idempotent: false— обычное write-действие без удаления, но с побочными эффектами. Например, создать черновик, добавить заметку к заказу, отправить письмо.
В MCP эти поля называются readOnlyHint, destructiveHint, idempotentHint. WordPress MCP Adapter умеет принимать WordPress-формат (readonly, destructive, idempotent) и автоматически конвертировать его в MCP-формат. Важно: это именно hints — декларации автора ability, а не техническая гарантия безопасности. Реальную защиту всё равно должны обеспечивать permission_callback, проверка прав пользователя, валидация входных данных и ограничения на стороне MCP-клиента.
Что делает mcp.public?
Флаг `»mcp»: { «public»: true }` говорит MCP Adapter, что ability нужно отдать наружу через default MCP server. Без этого ability может быть зарегистрирована в WordPress, но не попадёт в MCP-discovery для внешних клиентов.
Проще всего использовать это на уровне фильтра `wp_register_ability_args`: он позволяет добавить `meta.mcp.public = true` к уже зарегистрированным abilities. В теме `app` мы так и делаем для core abilities:
add_filter('wp_register_ability_args', function (array $args, string $abilityName): array {
$publicCoreAbilities = [
'core/get-site-info',
'core/get-user-info',
'core/get-environment-info',
];
if (!in_array($abilityName, $publicCoreAbilities, true)) {
return $args;
}
$args['meta']['mcp']['public'] = true;
return $args;
}, 10, 2);
Если хотите сделать это через отдельный класс, логика такая же: берёте список нужных abilities, проверяете имя в фильтре и проставляете `meta.mcp.public = true`.
Транспорт
MCP Adapter поддерживает два транспорта:
- HTTP+SSE — для удалённых клиентов. OpenCode использует именно этот вариант: запускает локальный процесс
npx -y @automattic/mcp-wordpress-remote@latest, который общается с WordPress по HTTPS.{ "$schema": "<https://opencode.ai/config.json>", "mcp": { "wordpress-mcp": { "type": "local", "command": ["npx", "-y", "@automattic/mcp-wordpress-remote@latest"], "enabled": true, "environment": { "WP_API_URL": "<https://wpcraft.ru/wp-json/mcp/mcp-adapter-default-server>", "WP_API_USERNAME": "your-wp-username", "WP_API_PASSWORD": "your-application-password", "OAUTH_ENABLED": "false" } } } } - stdio — для локальных инсталляций, когда WordPress и агент на одной машине. Обычно через WP-CLI команду.
"wordpress-mcp-server": { "command": "wp", "args": [ "--path=/path/to/your/wordpress/installation", "mcp-adapter", "serve", "--server=mcp-adapter-default-server", "--user={admin_user}" ] }
Дальше в статье используем HTTP-транспорт — он универсальнее.
Как настроить? По шагам
Шаг 1. Проверить, что WordPress готов
REST API должен отвечать:
curl -s <https://wpcraft.ru/wp-json/> | jq '.name'
Ожидаемый ответ — название сайта. Если приходит 406 или 404, проверьте pretty permalinks (Settings → Permalinks → Post name) и HTTPS.
Шаг 2. Сгенерировать Application Password
В админке: Users → Profile → Application Passwords.
- Введите имя — например,
opencode-mcp. - Нажмите Add New Application Password.
- Скопируйте пароль сразу — он показывается один раз. Дальше доступны только первые символы.
- Создайте отдельного пользователя с ролью Editor, если не хотите давать MCP-агенту админ-доступ. Пароль генерируется на его профиле.
Для тестов можно использовать админа, но в проде — только отдельный пользователь с минимально нужными правами.
Шаг 3. Установить и активировать плагины
Минимальный набор:
- MCP Adapter — сам адаптер. Лежит в
wordpress/mcp-adapterрепозитории. Наwpcraft.ruактивирован как отдельный плагинmcp-adapter/mcp-adapter. - AI Experiments (
ai/ai) — поставляет abilities неймспейсаai/*и регистрирует категориюai-experiments. - Любой плагин, добавляющий свои abilities: WooCommerce, Secure Custom Fields, ваш кастомный плагин.
После активации проверьте, что endpoint MCP виден:
curl -s -u "<user>:<app_password>" \
<https://wpcraft.ru/wp-json/mcp/mcp-adapter-default-server>
С авторизацией должен прийти JSON-RPC-ответ (возможно Method Not Allowed для GET — это нормально, endpoint ждёт POST).
Без авторизации — 401. Если 404 — адаптер не активирован или не зарегистрировал маршрут.
Шаг 4. Проверить список abilities
Отдельно от MCP можно дёрнуть список abilities через REST:
curl -s -u "<user>:<app_password>" \
<https://wpcraft.ru/wp-json/wp-abilities/v1/abilities> | jq '.[].name'
Должны увидеть массив имён: ai/get-post-details, woocommerce/products-query, и так далее. Если массив пустой — abilities не зарегистрированы. Проверьте, активен ли плагин-источник.
Шаг 5. Настроить OpenCode
Ставим пакет https://github.com/Automattic/mcp-wordpress-remote
В корне проекта создайте файл opencode.json:
{
"$schema": "<https://opencode.ai/config.json>",
"mcp": {
"wordpress-mcp": {
"type": "local",
"command": ["npx", "-y", "@automattic/mcp-wordpress-remote@latest"],
"enabled": true,
"environment": {
"WP_API_URL": "<https://example.com/wp-json/mcp/mcp-adapter-default-server>",
"WP_API_USERNAME": "your-wp-username",
"WP_API_PASSWORD": "your-application-password",
"OAUTH_ENABLED": "false"
}
}
}
}
Разбор полей:
type: "local"— MCP-сервер запускается как локальный процесс, не как удалённый URL.command— команда, которая стартует MCP-сервер.@automattic/mcp-wordpress-remote— официальный пакет, который проксирует JSON-RPC через HTTP на ваш WordPress.WP_API_URL— endpoint MCP-адаптера на вашем сайте. Путьmcp-adapter-default-server— стандартный для плагина MCP Adapter.WP_API_USERNAMEиWP_API_PASSWORD— пользователь и Application Password из шага 2.OAUTH_ENABLED: "false"— для связки с Application Password этого достаточно. OAuth пригодится, если у вас несколько агентов с разными правами.
Шаг 6. Проверить связку из OpenCode
Запустите OpenCode в директории с opencode.json. Первый запрос на проверку:
давай проверим mcp wordpress - что он отдает и что может?
OpenCode вызовет discover-abilities — и вы увидите список всех 50 (или сколько у вас) abilities. Дальше можно проверить один read-only запрос:
покажи первый товар из каталога
OpenCode вызовет woocommerce/products-query с per_page: 1. Если в ответ пришёл JSON с товаром — связка работает.
Как прокачать абилки сайта? Пример ability из темы
Сделаем свою абилку для сайта и сохраним в файл wp-content/themes/app/includes/Abilities/getPages.php и сразу регистрируем категорию app через Abilities API.
Пример кода
Создайте файл includes/Abilities/getPages.php:
<?php
namespace App\Abilities;
if (!defined('ABSPATH')) {
exit;
}
GetPagesAbility::init();
final class GetPagesAbility
{
public static function init(): void
{
add_action('wp_abilities_api_categories_init', [self::class, 'registerAbilityCategory']);
add_action('wp_abilities_api_init', [self::class, 'registerAbility']);
}
public static function registerAbilityCategory(): void
{
if (!function_exists('wp_register_ability_category')) {
return;
}
wp_register_ability_category(
'app',
[
'label' => __('App', 'app'),
'description' => __('Abilities for website - Application layer.', 'app'),
]
);
}
public static function registerAbility(): void
{
if (!function_exists('wp_register_ability')) {
return;
}
wp_register_ability(
'app/get-pages',
[
'label' => __('Get pages', 'app'),
'description' => __('Returns site pages with ID, title, slug, status, parent, and link.', 'app'),
'category' => 'app',
'execute_callback' => [self::class, 'execute'],
'permission_callback' => '__return_true',
'input_schema' => [
'type' => 'object',
'properties' => [
'limit' => [
'type' => 'integer',
'description' => __('Maximum number of pages to return.', 'app'),
'minimum' => 1,
'maximum' => 100,
'default' => 50,
],
'search' => [
'type' => 'string',
'description' => __('Search term for page title/content.', 'app'),
],
'status' => [
'type' => 'string',
'description' => __('Page status filter: publish, draft, private, or any.', 'app'),
'enum' => ['publish', 'draft', 'private', 'any'],
'default' => 'publish',
],
'parent_id' => [
'type' => 'integer',
'description' => __('Optional parent page ID filter.', 'app'),
],
],
],
'output_schema' => [
'type' => 'object',
'properties' => [
'pages' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
'title' => ['type' => 'string'],
'slug' => ['type' => 'string'],
'status' => ['type' => 'string'],
'parent_id' => ['type' => 'integer'],
'menu_order' => ['type' => 'integer'],
'modified_gmt' => ['type' => 'string'],
'link' => ['type' => 'string'],
],
'required' => ['id', 'title', 'slug', 'status', 'parent_id', 'menu_order', 'modified_gmt', 'link'],
],
],
'total' => [
'type' => 'integer',
],
],
'required' => ['pages', 'total'],
],
'meta' => [
'show_in_rest' => true,
'annotations' => [
'readonly' => true,
'destructive' => false,
'idempotent' => true,
],
'mcp' => [
'public' => true,
'type' => 'tool',
],
],
]
);
}
public static function execute(array $input): array
{
$limit = isset($input['limit']) ? (int) $input['limit'] : 50;
$search = isset($input['search']) ? sanitize_text_field((string) $input['search']) : '';
$status = isset($input['status']) ? sanitize_key((string) $input['status']) : 'publish';
$parentId = isset($input['parent_id']) ? (int) $input['parent_id'] : null;
$limit = max(1, min(100, $limit));
$allowedStatuses = ['publish', 'draft', 'private', 'any'];
if (!in_array($status, $allowedStatuses, true)) {
$status = 'publish';
}
$args = [
'post_type' => 'page',
'post_status' => $status === 'any' ? 'any' : $status,
'posts_per_page' => $limit,
's' => $search,
'orderby' => 'menu_order title',
'order' => 'ASC',
'suppress_filters' => false,
];
if ($parentId !== null && $parentId >= 0) {
$args['post_parent'] = $parentId;
}
$items = get_posts($args);
$pages = [];
foreach ($items as $page) {
$link = get_permalink($page);
$pages[] = [
'id' => (int) $page->ID,
'title' => (string) get_the_title($page),
'slug' => (string) $page->post_name,
'status' => (string) $page->post_status,
'parent_id' => (int) $page->post_parent,
'menu_order' => (int) $page->menu_order,
'modified_gmt' => (string) $page->post_modified_gmt,
'link' => $link ? (string) $link : '',
];
}
return [
'pages' => $pages,
'total' => count($pages),
];
}
}
Что важно в этом коде:
GetPagesAbility::init()— инициализирует регистрацию категории и ability через хукиwp_abilities_api_categories_initиwp_abilities_api_init.wp_register_ability_category— создаёт категориюapp, к которой привязана ability.wp_register_ability— функция из Abilities API. Регистрирует инструмент с его схемами входа и выхода.execute_callback— callback, который собирает список страниц черезget_posts()иget_permalink().input_schemaиoutput_schema— JSON Schema. MCP-клиенты используют их, чтобы понять, какие параметры передавать и что ждать в ответ. Без схем ability не появится в discovery.meta.annotations— флагиreadonly,destructive,idempotent. OpenCode увидит, что ability безопасна, и не будет спрашивать подтверждение перед вызовом.meta.mcp.public = true— делает ability доступной через MCP-адаптер. Без этого флага ability видна только внутри WordPress, но не внешним агентам.permission_callback— здесь__return_true, потому что ability только читает публичные данные.
Проверить, что ability появилась
curl -s -u "<user>:<app_password>" \
<https://wpcraft.ru/wp-json/wp-abilities/v1/abilities> \
| jq '.[] | select(.name == "app/get-pages")'
Должны увидеть JSON с описанием ability.
После этого можно вызывать её из OpenCode:
покажи страницы сайта
OpenCode вызовет app/get-pages с параметрами по умолчанию и вернёт список страниц с id, title, slug, status, parent_id, menu_order, modified_gmt и link.
Несколько практических замечаний
- Валидируйте вход внутри callback-функции. JSON Schema проверяет структуру, но не содержимое. Здесь
limitдополнительно ограничен диапазоном1-100,searchочищается черезsanitize_text_field(), аstatus— черезsanitize_key()и whitelist. - Возвращайте массив, соответствующий
output_schema. Если схема обещаетpagesиtotal, а callback вернёт другую структуру, MCP-клиент получит несоответствие и может отбросить ответ. - Не регистрируйте ability на каждом запросе. Используйте хук
initс приоритетом по умолчанию — Abilities API сам кэширует реестр. - Документируйте в
descriptionдля кого и зачем. AI-клиенты читают описания, чтобы решить, какой tool вызвать. «Возвращает страницы сайта с ключевыми полями и фильтрами» лучше, чем слишком общий заголовок.
Выводы
Связка WordPress + MCP + OpenCode работает и экономит время. После настройки агент видит каталог, контент и структуру сайта так же, как вы в админке — и может действовать без копирования данных в чат.
Что мы получили на wpcraft.ru:
- На данном сайте были abilities в трёх неймспейсах: WooCommerce, AI, SCF.
- Read-only запросы работают из коробки: товары, записи, термины.
- Write-операции (создание товаров, обновление статусов заказов) тоже доступны, но требуют осторожности и отдельного пользователя с ограниченными правами.
- Кастомные abilities регистрируются за 50+ строк PHP и сразу видны агенту.
Что осталось за рамками статьи:
- OAuth вместо Application Password — если у вас несколько агентов с разными правами, OAuth даёт granular-доступ. Для одного-двух агентов Application Password проще.
- Локальный stdio-транспорт — если WordPress и агент на одной машине, stdio быстрее HTTP. Запускается через
wp mcp stdio. - MCP Tracker — отдельный плагин для мониторинга входящих MCP-запросов в реальном времени. Полезно в проде.
- Rate-limiting — WordPress REST API не имеет встроенных лимитов на MCP-запросы. На нагруженном сайте стоит добавить свой rate-limiter через хук
rest_pre_dispatch.
Следующим шагом логично написать про OAuth-настройку и кастомные abilities для WooCommerce — но это уже отдельные материалы.