Подключаем ИИ-агентов к WordPress через MCP на примере OpenCode

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

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

В этой статье разберём, как связать WordPress и OpenCode через MCP: из чего состоит связка, как её настроить и как добавить свою ability. Все шаги проверены на реальном сайте wpcraft.ru.

Составляющие и особенности

Что нужно на стороне WordPress

  1. WordPress 7.0+ — в ядре появился Abilities API. Это набор функций (wp_register_ability, wp_register_ability_category) для регистрации инструментов, которые сайт умеет выполнять. Abilities API не отдаёт данные сам по себе — он реестр.
  2. Плагин MCP Adapter — превращает зарегистрированные abilities в MCP-инструменты, доступные по JSON-RPC. Плагин лежит в репозитории WordPress/mcp-adapter и поставляется как отдельный плагин или как vendor-зависимость плагина AI Experiments.
  3. Плагин-источник 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.
  4. Application Password — WordPress умеет генерировать пароли приложений в Users → Profile → Application Passwords. MCP-клиент использует basic-auth с этим паролем. Отдельный пароль удобно отзывать, не меняя основной.

Что нужно на стороне агента

OpenCode — CLI-инструмент для работы с кодом. Он умеет подключать MCP-серверы через конфиг opencode.json в корне проекта. Один MCP-сервер в конфиге — один источник инструментов для агента.

Другие ИИ-агенты настраиваются похожим образом:

  • Claude Desktopclaude_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.

  1. Введите имя — например, opencode-mcp.
  2. Нажмите Add New Application Password.
  3. Скопируйте пароль сразу — он показывается один раз. Дальше доступны только первые символы.
  4. Создайте отдельного пользователя с ролью 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. Регистрирует инструмент с его схемами входа и выхода.
  • 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 — но это уже отдельные материалы.


Связанные Материалы и Ссылки

Фото аватара

Antony I

Веб разработчик, специализация на лучших мировых практиках: WordPress, WooCommerce, NextJS, Strapi, JAMStack ...

Основные типы проектов: CMS, eCommerce, SEO, LMS, ECM, BPM

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *