Главная / Статьи

WebMCP на практике: преобразование доски задач в инструменты для ИИ-агентов

WebMCP на практике: панель задач, преобразованная в инструменты для ИИ-агентов с декларативными и скриптовыми инструментами, меняющимся списком инструментов, мостом MCP и отказом.

Издатель: REACTAPP.TOP14 мин чтения3318 слов

Источники: WebMCP: When Websites Become AI Tools

Агенты, которые угадывают структуру вашего веб-сайта

Сегодня ИИ-агенты, работающие с веб-сайтами, в основном полагаются на угадывание. Они делают скриншот и ищут что-то, похожее на кнопку, или анализируют DOM, надеясь, что имена классов соответствуют их замыслу. После нажатия они проверяют, изменилась ли страница таким образом, который указывал бы на успех. Это впечатляющий трюк, основанный на слабой основе, поскольку сайт никогда ничего не сообщает агенту, и все выводы делаются исключительно на основе интуиции.

WebMCP — это предложение Chrome для прекращения таких угадываний. Страница объявляет о своих возможностях, агент читает это объявление и вызывает функцию с структурированными аргументами, вместо того чтобы целиться в какой-то прямоугольник на экране. Изменения кажутся незначительными, но они перемещают центр интеллектуальных усилий: вместо воссоздания цели приложения на основе пикселей и маркировки агент получает эту цель непосредственно от самого приложения.

Чтобы проверить, сработает ли это на практике, была создана небольшая панель задач для работы, к которой был привязан реальный агент. Самым показательным результатом стало случай, когда агент категорически отклонил запрос.

Три роли

В каждый момент эксперимента участвует один из трех участников.

  • Страница выполняет функцию поставщика. Она регистрирует инструменты. Каждый инструмент включает в себя название, описание, предназначенное для модели, JSON Schema с описанием аргументов и функцию, выполняющую работу.
  • Браузер выступает в роли реестра. Для каждой вкладки он хранит список инструментов, где возможно извлекает схемы из HTML-кода и определяет, когда инструмент появляется или исчезает.
  • Агент является потребителем. Он запрашивает у браузера информацию о доступных инструментах с помощью getTools() и запускает выбранный инструмент с помощью executeTool(). DOM остаётся вне его досягаемости, и у него нет возможности имитировать функции, которые страница никогда не предоставляла.
  • Именно этот последний момент составляет суть идеи. Документация Chrome достаточно чётко описывает API, но прочтение пояснений — это не то же самое, что понимание концепции, поэтому самым быстрым способом стало создание самой минимальной возможной версии.

    Демонстрация: панель задач в трёх файлах

    Платформа состоит из index.html, app.js и style.css. Ничего не нужно создавать или устанавливать. Пользователи могут добавлять задачи, отмечать их как выполненные и фильтровать список — это проект, над которым можно поработать за один день. Нововведением является то, что страница также представляется ИИ-агенту в виде набора доступных инструментов через объект, предоставляемый браузером.

    const mc = document.modelContext;   // undefined if WebMCP is off
    

    Эта единственная строка и является полным точкой входа. Когда объект существует, страница может регистрировать инструменты. Когда его нет, страница просто выступает в роли доски задач, что и является предусмотренным способом её работы в нестандартных условиях.

    Чтобы обе части были видны одновременно, на демо-странице они размещены рядом. Слева находится доска задач, то есть поставщик услуг. Справа — панель «Виртуальный агент», которая выполняет роль потребителя и отправляет вызовы getTools() и executeTool() так же, как это делал бы настоящий агент, тем самым избегая дополнительной настройки. Эту панель вы увидите на каждом из приведённых ниже скриншотов.

    WebMCP по-прежнему имеет статус «Предложено» в списке статусов платформы Chrome и по умолчанию не включён нигде, поэтому сначала требуется включить соответствующий флаг.

    Откройте chrome://flags/#enable-webmcp-testing, установите значение на «Включено» и перезапустите браузер. Без этого параметра document.modelContext остается неопределённым. Chrome также предлагает пробную версию WebMCP origin, доступную начиная с версии 149. Данный параметр подходит для локальных экспериментов, тогда как пробная версия origin предназначена для более широких тестов.

    Сначала готовый цикл

    Легче понять API, увидев результат его работы. Claude Code был подключён к платформе, и ему было дано задание добавить четыре задачи: поиск информации и проведение исследований, составление планов для блога, написание полного блога с примерами и его публикация. Появились четыре задачи. Второе задание предусматривало отметку задач 3, 4 и 5 как выполненных, и это тоже сработало.

    Клод не сделал скриншот доски и не читал её HTML-код. Он запросил у браузера доступные инструменты, нашёл add-task и вызвал его четыре раза с структурированными аргументами. Вот то же мгновение, видимое изнутри браузера.

    Четыре новых задачи теперь находятся на позициях с #3 по #6 на доске, а в панели DevTools (Application, затем WebMCP) перечислены все шесть инструментов, которые в настоящее время использует страница. Один из них, clear-completed, раньше там не был, и причина этого скоро станет ясна. Три завершения задач записываются по отдельности для каждого вызова.

    Каждый вызов отображается вместе со своими аргументами ({"id":3}, {"id":4} и {"id":5}), а в строке сводки указано, что всего было совершено три вызова без ошибок, отмен или вызовов, которые все ещё выполняются. Отладка WebMCP здесь происходит быстро, поскольку панель отображает точно то, что отправил агент.

    Таким образом, предложение, введённое в терминал, превратилось в четыре структурированных функциональных вызова внутри вкладки браузера. Следующий вопрос заключается в том, что должна была сделать страница, чтобы этого добиться.

    Два способа публикации инструмента

    Первый способ совсем не требует JavaScript. Существующая форма получает четыре дополнительных атрибута.

    <form id="add-form"
          toolname="add-task"
          tooldescription="Add a new task to the user's task board."
          toolautosubmit>
      <input name="title" required maxlength="80"
             toolparamdescription="Short description of the task to add.">
      <select name="priority"
              toolparamdescription="How urgent the task is.">
        <option value="low">low</option>
        <option value="normal" selected>normal</option>
        <option value="high">high</option>
      </select>
      <button type="submit">Add</button>
    </form>
    

    Этот формуляр — это инструмент add-task, который Claude использовал четыре раза. Имена полей, атрибуты required и опции <select> считываются браузером, который самостоятельно формирует JSON Schema. Агент видит одну обязательную строковую переменную и энумерацию с тремя значениями, причем никто не вводил схему вручную. В существующем приложении это, вероятно, самый простой эксперимент с WebMCP, поскольку обычный HTML-формуляр дает стартовое преимущество. Всё, что добавляется, — это описание того, что формуляр означает для агента.

    Есть одна деталь, которую стоит обдумать: один и тот же обработчик submit обслуживает как пользователя, нажавшего кнопку «Добавить», так и агента, вызывающего инструмент, причем событие указывает, кто именно это сделал.

    $('#add-form').addEventListener('submit', (e) => {
      e.preventDefault();
      const task = addTask(new FormData(e.target).get('title'), /* ... */);
    
      if (!e.agentInvoked) {
        e.target.reset();          // human — clear the box
      } else {
        e.respondWith?.(Promise.resolve(
          text(`Added task #${task.id}: "${task.title}".`)
        ));
      }
    });
    

    Флаг agentInvoked указывает, кто нажал кнопку, а функция respondWith() возвращает результат агенту без перехода на другую страницу. Существует один путь выполнения кода с двумя вызывающими сторонами, что является хорошим дизайнерским решением, поскольку никто не хочет, чтобы у каждого действия в приложении была «версия для ИИ». Та же логика должна использоваться как для людей, так и для агентов, когда это целесообразно.

    Декларативный подход, пожалуй, менее интересен, хотя именно он, скорее всего, привлекает внимание, поскольку он хорошо демонстрирует работу и не требует особых усилий. Интересно становится там, где форма не может выразить всё — это второй подход, registerTool, который очень похож на код, который можно написать для сервера MCP на стороне бэкенда.

    await mc.registerTool({
      name: 'list-tasks',
      description: 'List the tasks on the board. Use this before acting so you know the task IDs.',
      inputSchema: {
        type: 'object',
        properties: {
          status: { type: 'string', enum: ['all', 'open', 'done'] },
        },
      },
      annotations: { readOnlyHint: true },
      async execute({ status = 'all' }) {
        const rows = visible(tasks, status);
        return text(rows.map((t) => `#${t.id} [${t.done ? 'x' : ' '}] ${t.title}`).join('\n'));
      },
    });
    

    Внимательно посмотрите на этот description. Он написан для модели, а не для разработчика, и такая фраза, как «используйте это перед действием, чтобы знать ID задач», указывает на момент, когда его следует вызвать. Описания теперь являются частью интерфейса API, и нечеткие описания приводят к тому, что агенты делают предположения.

    Флаг readOnlyHint сигнализирует о том, что инструмент можно безопасно вызвать, не спрашивая сначала пользователя. Этот флаг присутствует у list-tasks и estimate-task. У complete-task и clear-completed его нет, поскольку они изменяют состояние.

    Инструменты также могут работать медленно, и агент может изменить свое решение посреди процесса. Инструмент estimate-task намеренно ждет три секунды, причем в качестве второго аргумента он получает сигнал AbortSignal.

    async execute({ id }, { signal }) {
      const t = findTask(tasks, id);
    
      if (!t) {
        return text(`No task #${id}.`);
      }
    
      await sleep(3000, signal); // throws if the agent aborts
    
      return text(
        `Task #${t.id} is about ${
          t.priority === 'high' ? '2 hours' : '30 minutes'
        }.`
      );
    }
    

    Если учитывать этот сигнал, аннулирование будет работать. Если его игнорировать, инструмент будет продолжать работать после ухода агента.

    Список инструментов, который меняется при открытом странице

    Самая весомая причина регистрировать инструменты в скрипте вместо отправки статического списка заключается в том, что этот список может меняться во время выполнения программы. В панели существует инструмент clear-completed, который удаляет все завершённые задачи. Это имеет смысл только тогда, когда что-то действительно завершено, что объясняет, почему он появился на предыдущем скриншоте сразу после того, как три задачи были отмечены как завершённые. Следовательно, он существует только в такие моменты.

    let clearCtl = null;
    
    async function syncClearTool() {
      const has = tasks.some((t) => t.done);
    
      if (has && !clearCtl) {
        const ctl = new AbortController();
        clearCtl = ctl;
    
        await mc.registerTool(
          {
            name: 'clear-completed',
            /* ... */
          },
          {
            signal: ctl.signal
          }
        );
      } else if (!has && clearCtl) {
        const ctl = clearCtl;
        clearCtl = null;
    
        setTimeout(() => ctl.abort(), 0); // abort() IS unregister
      }
    }
    

    Функции unregisterTool() не существует. При регистрации передаётся объект AbortSignal, и его аннулирование приводит к удалению инструмента. Именно здесь проявляется эффективность такого подхода: сигнал, отменяющий выполняемый запрос, одновременно отзывает соответствующие полномочия, тем самым один механизм выполняет две функции.

    Если отметить задачу как выполненную, в списке агента появляется элемент clear-completed, а при снятии отметки инструмент исчезает. Агент узнаёт об этом через событие toolchange и не нуждается в постоянном опросе состояния.

    mc.addEventListener('toolchange', refreshTools);
    

    Следовательно, список инструментов — это не манифест, опубликованный один раз. Он описывает то, что приложение может делать в настоящее время, в своем текущем состоянии.

    В чем это отличается от сервера MCP на стороне бэкенда

    Затем от Claude Code попросили ограничить список только завершенными задачами.

    Агент вызвал функцию filter-tasks с параметром {"status":"done"}; кнопка «Завершено» стала активной, и на списке отобразились только завершенные задачи. Агент не загружал отфильтрованную копию данных — он просто изменил вид, который видел пользователь.

    Сервер MCP на стороне сервера не может справиться с этой задачей. Находясь по другую сторону сетевого разделения, он работает с сохранёнными данными и не имеет никакого представления о том, что в данный момент отображается на экране. Инструменты WebMCP находятся внутри вкладки, делая общим её DOM, сессию и куки, поэтому агент и пользователь видят один экран, и любой из них может его изменить. Это является как существенным преимуществом, так и реальным риском, причём риск рассматривается в отдельном разделе ниже. Именно поэтому называние WebMCP «MCP внутри браузера» недооценивает его возможности. Местоположение инструмента имеет значение, поскольку он связан с самим состоянием страницы, которое просматривает пользователь.

    Подключение внешнего агента

    Панель Pretend Agent помогает во время разработки, но она мало чем полезна, поскольку это страница, которая обращается к самой себе. Для надлежащего тестирования требуется что-то вне браузера, что может видеть только то, что возвращает getTools(). Поэтому существует agent.mjs — небольшой инструмент командной строки с простыми механизмами протокола Chrome DevTools и без каких-либо зависимостей, поскольку в Node 22 уже имеется глобальный объект WebSocket. По своей конструкции он может видеть только то, что возвращает getTools(), и действовать только через executeTool(), при этом отсутствует возможность парсинга DOM.

    node agent.mjs list
    node agent.mjs call add-task '{"title":"Ship the demo","priority":"high"}'
    

    Эти два команды перечисляют инструменты, а затем вызывают add-task с указанием заголовка и приоритета. Настройка, использованная для создания скриншотов, идет еще на шаг дальше. Claude Desktop, Claude Code и ChatGPT используют протокол MCP, а не WebMCP, поэтому mcp-bridge.mjs выполняет роль адаптера; при этом он довольно прост, поскольку протоколы практически полностью совпадают: tools/list соответствует getTools(), а tools/call — executeTool(). Claude Code подключается к этому через файл .mcp.json.

    {
      "mcpServers": {
        "webmcp-board": {
          "command": "node",
          "args": ["C:\\projects\\web-mcp-demo\\mcp-bridge.mjs"],
          "env": { "PAGE_URL": "https://tusharkanjariya.github.io/web-mcp-demo/" }
        }
      }
    }
    

    Вся цепочка выглядит следующим образом: Claude Code использует протокол MCP для взаимодействия с бриджем, бридж использует протокол CDP для связи с Chrome, а Chrome вызывает инструменты, зарегистрированные на странице. Это четыре этапа передачи данных, и страница в конце цепочки не может определить, какой клиент находится по другую сторону.

    Claude Code
        ↓
    MCP
        ↓
    mcp-bridge.mjs
        ↓
    Chrome DevTools Protocol
        ↓
    WebMCP
        ↓
    my task board
    

    Ещё одно удобство: если по порту отладки нет ответа, бридж запускает собственную версию Chrome и открывает страницу, когда вкладка отсутствует. Клиент MCP может запустить всё с нуля.

    Отказ

    Затем от агента попросили удалить одну задачу.

    В верхней части скриншота показан запрос на фильтрацию, сделанный ранее. В нижней части — самое интересное: Claude заявил, что на этой платформе нет инструмента delete-task, что единственный способ удаления — clear-completed, который также удалит задачи №2, №3, №4 и №5, и что он не собирается этого делать.

    Отказ возник не из-за какого-то защитного механизма, системного указания или слоя безопасности. Возможности удаления отдельной задачи просто не существовало, и агент это заметил. Было бы ошибкой придавать слишком большое значение одному обмену сообщениями в простом приложении. Тем не менее это меняет восприятие проблемы. Агент, управляющий страницей с помощью пикселей и координат, нашёл бы что-то, похожее на инструмент для удаления, и попробовал бы его использовать, поскольку у агентов для сбора данных с экрана есть только импровизация в качестве альтернативы. Этот агент перечислил свои шесть инструментов, увидел, что ни один из них не предназначен для удаления задачи, осознал косвенный ущерб от ближайшего варианта и остановился, чтобы спросить.

    Границей служил набор инструментов. Никто не написал правило, запрещающее удалять элементы. Сама страница просто никогда не предлагала возможности удаления как возможного действия, и этого оказалось достаточно, хотя WebMCP и не делает агентов автоматически безопасными.

    Где Chrome и спецификация расходятся

    При запуске программы обнаружились расхождения между документацией и поведением браузера, и устранение их заняло полдня. Три из них касаются формата данных.

    • Аргументы для executeTool(): в спецификации описывается объект, но Chrome 151 ожидает строку в формате JSON, при этом объект вызывает ошибку Failed to parse input arguments.
    • RegisteredTool.inputSchema: в спецификации указан как объект, но Chrome возвращает строку в формате JSON, поэтому её необходимо парсить перед чтением properties.
    • Результат, который возвращает executeTool(): в спецификации указан DOMString, что соответствует действительности, но на самом деле это сериализованная оболочка {content:[…]}, которую нужно дважды разобрать, чтобы получить текст.

    Всё это не представляет сложности, как только вы это понимаете. Два наиболее серьёзных проблемы были связаны с ошибками жизненного цикла, и они проявляются только тогда, когда реальный агент обрабатывает страницу.

    Вызов form.reset() во время выполнения отправки, инициированной агентом, прерывает работу инструмента с сообщением Tool execution cancelled by a form reset. Обычный обработчик, который, как это делают большинство решений, очищает поля ввода после отправки, приводил к сбою каждой операции add-task, выполняемой агентом. Задача создавалась, но ответ так и не поступал, поэтому ранее показанный обработчик сбрасывает значения только при отправке от человека.

    В версиях Chrome до 153 удаление инструмента также прерывало все его ещё выполняющиеся вызовы. Это не представляло опасности, пока инструмент не удалялся сам по себе в результате успешного выполнения. Функция clear-completed удаляла все завершённые задачи, в результате чего ничего не оставалось выполненным, и инструмент самоудалялся. В версии 151 такое прерывание прерывало отправку собственного ответа до того, как он мог быть возвращён. Решением является использование setTimeout(() => ctl.abort(), 0): отсрочка прерывания на один такт позволяет сначала завершить вызов. Это имеет логическое обоснование и работает, но отсрочка завершения работы инструмента на один такт — это тот тип исправления, который стоит пересмотреть после нескольких обновлений Chrome.

    Отправка демо-версии

    Демо-версия полностью статична, поэтому GitHub Pages может отдавать её без изменений, а HTTPS обеспечивает безопасный контекст, необходимый WebMCP. Таким образом у всех появляется рабочая доска задач, но не работающий WebMCP, поскольку посетители, которые не установили соответствующий флаг, по-прежнему видят, что document.modelContext не определён. Чтобы это исправить, необходим токен для проверки происхождения в разделе <head>, зарегистрированный как токен первой стороны, поскольку Chrome отклоняет токены третьей стороны, размещённые в теге meta. На чистом профиле без флагов modelContext возвращался как «object» вместе со всеми шестью инструментами. Пробный период действует до версии Chrome 156.

    Вопрос безопасности без подходящего ответа

    Инструменты WebMCP выполняются во входном табе. В этом и заключается суть, но в этом же и кроется проблема.

    В спецификации предусмотрен параметр untrustedContentHint, который является лишь подсказкой. Вопрос о том, как следует обрабатывать внедрение команд, до сих пор не решен, и ни один элемент этого API не выполняет функции границы авторизации. Если текст, контролируемый злоумышленником, может быть отображен на странице и прочитан агентом, readOnlyHint не сможет помочь.

    Мост также можно предоставить по HTTP через туннель, чтобы соединители ChatGPT могли к нему обратиться. Это работает, но перед этим стоит тщательно подумать, поскольку туннель делает доступным конечную точку без аутентификации, которая позволяет любому, кто угадает адрес, запускать инструменты в открытом вами вкладке браузера. Двадцать минут экспериментов — это нормально, но оставлять его включенным на всю ночь — нет.

    Попробовать в собственном приложении

    Администраторы веб-приложения могут оценить API за один день, поскольку он небольшой. Выберите любую существующую форму, добавьте к ней атрибуты toolname и tooldescription, затем откройте панель WebMCP в DevTools — инструмент появится там. Примерно десять минут работы на этом принесут больше пользы, чем ещё час чтения.

    Суть заключается не только в синтаксисе. Честное описание того, что может делать ваше приложение, стало неотъемлемой частью самого продукта, а не документацией, написанной позже. В управляющем интерфейсе отсутствовала функция удаления одного элемента, поэтому агент никогда не выполнял такую операцию. То, что возвращает метод getTools(), определяет, как агент воспринимает возможности вашего приложения, и это представление постепенно превращается в сам интерфейс. В демонстрации использовалось шесть инструментов из трех файлов без каких-либо зависимостей, поэтому подумайте, что могло бы говорить о себе ваше собственное приложение. Чтобы воспроизвести это, включите флаг chrome://flags/#enable-webmcp-testing, затем выполните команду node agent.mjs list. Если в ответ будет шесть инструментов, значит, вы взаимодействуете с веб-страницей так, как это делал бы агент.

    Краткие ответы

    Что такое WebMCP? Это предлагаемый API браузера, с помощью которого страница может предоставлять свои собственные функции ИИ-агенту в виде инструментов, доступных для вызова, с использованием document.modelContext. Агенты могут вызывать эти инструменты напрямую, избегая парсинга DOM и координации кликов.

    В чём он отличается от обычного сервера MCP? Бэкенд-серверы работают за пределами сетевой границы и обрабатывают хранимые данные. Инструменты WebMCP выполняются внутри вкладки, поэтому они могут изменять то, что видит пользователь, и используют сессию и разрешения страницы. Вызов функции filter-tasks заметно изменил интерфейс панели.

    Как страница регистрирует инструмент? Либо добавьте атрибуты toolname и tooldescription к существующей форме, позволяя браузеру определить схему на основе полей, либо вызовите document.modelContext.registerTool() с именем, описанием, JSON Schema и функцией execute. Формы подходят для простых действий, а registerTool позволяет реализовать всё, что невозможно сделать с помощью форм.

    Требуется ли флаг? Пока да. Включите флаг и перезапустите Chrome или запустите его с параметром --enable-features=WebMCP. Для посетителей, которые не включили этот флаг, будет работать токен тестового режима первичного источника в разделе <head> страницы.

    Могут ли Claude Desktop или ChatGPT вызывать эти инструменты? Сами по себе — нет, поскольку они используют MCP вместо WebMCP. Небольшой адаптер преобразует tools/list в getTools(), а tools/call — в executeTool(). Приведённый здесь код насчитывает около 130 строк, не требует дополнительных зависимостей и работает через стандартный ввод-вывод для локальных клиентов.

    Является ли это безопасным? Вопрос остаётся открытым. untrustedContentHint и readOnlyHint являются лишь подсказками; ни один из них не определяет границы авторизации, к тому же спецификация ещё не решила вопрос с вставкой команд. Разрабатывайте интерфейс инструментов, предполагая, что агент может быть склонён вызывать что угодно из того, что вы предоставляете.

    Связанные материалы

  • Serena MCP Server: редактирование кода с учётом символов для помощников ИИ — настройка Serena с использованием Docker и SSE, что позволяет Claude Code, VS Code и OpenCode использовать поиск по символам с поддержкой LSP, безопасное переименование элементов и постоянную память при меньшем количестве токенов.