Назад в журнал
12 мин чтения

WebMCP в React: Предоставление Состояния Клиента и Действий Форм для ИИ-Агентов Браузера

Превратите веб-приложения на React 19 в управляемые агентами интерфейсы с помощью стандарта W3C WebMCP, декларативных форм и хуков useWebMCPTool.

React.jsTypeScriptModel Context Protocol (MCP)LLM AgentsIn-Browser LLMs
WebMCP в React: Предоставление Состояния Клиента и Действий Форм для ИИ-Агентов Браузера

При интеграции больших языковых моделей с внешними сервисами протокол Model Context Protocol (MCP) стал общепринятым стандартом для вызова инструментов. Тем не менее подавляющее большинство существующих архитектур MCP опирается на классическую схему «клиент-сервер»: внешний клиент (например, Claude Desktop или ассистент в IDE) взаимодействует с фоновым процессом через stdio или Server-Sent Events (SSE) для выполнения запросов к базам данных, вызова облачных API или чтения файлов.

Однако внутри веб-браузера эта модель сталкивается с непреодолимыми ограничениями.

Современные веб-приложения представляют собой многофункциональные Single Page Applications (SPA) со сложным внутренним состоянием. Критически важные данные — активные выделения на холсте (canvas), незавершенный ввод в формах, клиентская пагинация, параметры сортировки и открытые модальные окна — находятся исключительно в оперативной памяти клиента (состояние компонентов React, хранилища Zustand или DOM). Когда автономный браузерный агент или расширение пытается взаимодействовать с таким приложением, ему приходится прибегать к нестабильному парсингу визуального DOM, чтению дерева доступности или генерации искусственных кликов мыши.

Новая спецификация WebMCP (Web Model Context Protocol), разрабатываемая в рамках сообщества W3C Web Machine Learning и доступная в качестве эксперимента в Chromium, переносит Model Context Protocol непосредственно во вкладку браузера.

В этом руководстве рассматривается устройство WebMCP, его отличия от серверного MCP и способы предоставления состояния компонентов React 19 и действий форм браузерным агентам с использованием декларативных HTML-атрибутов и специализированных React-хуков.

CODE
+-------------------------------------------------------------------------+
| Browser Tab (Main Thread / React 19 Context)                            |
|                                                                         |
|  [ React Component State ] <---> [ useWebMCPTool Hook ]                 |
|            |                                |                           |
|            v                                v                           |
|  [ <form toolname="..." /> ]      [ document.modelContext ]             |
|            |                      (In-Memory Tool Registry)             |
|            +--------------------------------+                           |
+---------------------------------------------|---------------------------+
                                              | Local In-Tab Dispatch
+---------------------------------------------v---------------------------+
| In-Browser Agent Context (Chrome Assistant / Extension / Gemini Nano)   |
|                                                                         |
|  1. Inspects active tab tools via document.modelContext.listTools()     |
|  2. Invokes tool with structured JSON arguments                         |
|  3. React action dispatches state transition -> UI updates instantly    |
+-------------------------------------------------------------------------+

Вакуум Инструментов на Стороне Клиента

Чтобы понять необходимость WebMCP, обратим внимание на то, как автономный агент сейчас решает типовую задачу — например, «отфильтровать таблицу заказов дороже 500 $ и выгрузить результат»:

  1. Парсинг дерева доступности: Агент запрашивает дерево доступности или анализирует скриншот. Ему приходится угадывать кнопку фильтра среди десятков вложенных тегов <div> и <button>.
  2. Хрупкость селекторов: Любое обновление CSS-модулей, утилитарных классов Tailwind или иерархии DOM немедленно ломает автоматизированные селекторы.
  3. Избыточность контекста: Передача всего DOM-дерева или снимка страницы на каждом шаге расходует десятки тысяч токенов и создает ощутимую задержку сети.
  4. Сбои синтетических событий: Генерация событий MouseEvent или KeyboardEvent нередко обходит синтетическую систему событий React, вызывая устаревшие замыкания (stale closures) и пропуск валидаций.

WebMCP заменяет эвристический перебор структурированными вызовами RPC прямо в контексте выполнения клиента. Вместо угадывания селекторов веб-страница регистрирует явные инструменты с типизированными схемами JSON.

Сравнение WebMCP и серверного MCP

Различия между серверным MCP и WebMCP носят фундаментальный характер:

Параметр Серверный MCP (Node.js / Python) WebMCP (Встроенный в Браузер)
Среда выполнения Фоновый процесс, контейнер, serverless-функция Поток выполнения активной вкладки браузера
Транспорт stdio, Server-Sent Events (SSE), WebSockets Прямая ссылка на функцию JavaScript
Целевые данные Удаленные базы данных, файловые системы, внешние API Состояние React, роутер клиента, local storage, DOM
Примитивы Tools, Resources, Prompts Только Tools (в рамках активного документа)
Граница безопасности Изоляция процессов, права ОС, API-ключи Песочница браузера, Same-Origin Policy, подтверждение пользователя

WebMCP не использует сетевые сокеты или дочерние процессы. Сам документ выступает реестром инструментов через интерфейс document.modelContext.


Архитектура Браузера: document.modelContext

В Chromium (начиная с версии 146 под флагом #enable-webmcp-testing) браузер предоставляет брокер инструментов, доступный через глобальный объект document:

TYPESCRIPT
// Core interface for browser-native WebMCP
interface ModelContextTool {
  name: string;
  description: string;
  inputSchema: Record<string, unknown>;
  execute: (input: Record<string, unknown>) => Promise<Record<string, unknown>>;
  annotations?: {
    readOnlyHint?: boolean;
  };
}

interface ModelContext {
  registerTool(tool: ModelContextTool, options?: { signal?: AbortSignal }): void;
  listTools(): Promise<ModelContextTool[]>;
}

declare global {
  interface Document {
    modelContext?: ModelContext;
  }
}

Работа этого интерфейса опирается на два ключевых принципа:

  1. Область видимости документа: Инструменты привязаны к жизненному циклу документа. При переходе по ссылке или закрытии вкладки реестр уничтожается.
  2. Отмена регистрации через AbortSignal: Вместо явного метода unregisterTool управление жизненным циклом делегировано стандартному примитиву AbortSignal. При прерывании сигнала браузер корректно удаляет инструмент.

Шаблон 1: Декларативный WebMCP с формами React 19

Наиболее прямолинейный способ предоставить возможности агентам — декларативный API WebMCP. WebMCP расширяет стандартные HTML-формы специальными атрибутами:

  • toolname: Уникальное имя инструмента.
  • tooldescription: Краткое описание назначения инструмента и условий его вызова.

В React 19 декларативные формы напрямую связываются с хуком useActionState и серверными/клиентскими действиями.

TSX
"use client";

import React, { useActionState } from "react";

interface FilterState {
  minAmount: number;
  category: string;
  status: "idle" | "applied";
}

async function applyFilterAction(
  prevState: FilterState,
  formData: FormData
): Promise<FilterState> {
  const minAmount = Number(formData.get("minAmount") || 0);
  const category = String(formData.get("category") || "all");

  // Perform client-side filter computation or query
  return {
    minAmount,
    category,
    status: "applied",
  };
}

export function AgenticOrderFilter() {
  const [state, formAction, isPending] = useActionState(applyFilterAction, {
    minAmount: 0,
    category: "all",
    status: "idle",
  });

  return (
    <form
      action={formAction}
      // WebMCP Declarative Tool Annotations
      toolname="filter-orders"
      tooldescription="Filters the current order ledger by minimum dollar amount and product category."
      className="filter-form"
    >
      <label htmlFor="minAmount">Minimum Amount ($)</label>
      <input
        id="minAmount"
        name="minAmount"
        type="number"
        defaultValue={state.minAmount}
        required
      />

      <label htmlFor="category">Category</label>
      <select id="category" name="category" defaultValue={state.category}>
        <option value="all">All Categories</option>
        <option value="hardware">Hardware</option>
        <option value="software">Software</option>
      </select>

      <button type="submit" disabled={isPending}>
        {isPending ? "Filtering..." : "Apply Filter"}
      </button>

      {state.status === "applied" && (
        <p className="status-text">
          Showing orders &gt; ${state.minAmount} in category &quot;{state.category}&quot;
        </p>
      )}
    </form>
  );
}

Когда агент открывает страницу, браузер сканирует DOM на наличие форм с атрибутом toolname и формирует JSON-схему на основе названий, типов и ограничений полей ввода. При вызове filter-orders браузер передает управление в обработчик formAction в React.


Шаблон 2: Императивный хук useWebMCPTool в React

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

Для интеграции с React создается пользовательский хук, решающий три задачи:

  1. Динамический жизненный цикл: Инструменты должны регистрироваться при монтировании компонента и удаляться при его размонтировании.
  2. Доступ к актуальному состоянию: Обработчик инструмента должен использовать актуальные значения пропсов и стейта без необходимости постоянной перерегистрации.
  3. Очистка ресурсов через AbortSignal: Корректное освобождение ресурсов при переходах между маршрутами SPA.

Ниже приведена реализация хука useWebMCPTool:

TYPESCRIPT
"use client";

import { useEffect, useRef } from "react";

export interface ToolDefinition<TInput = Record<string, unknown>, TOutput = Record<string, unknown>> {
  name: string;
  description: string;
  inputSchema: Record<string, unknown>;
  execute: (input: TInput) => Promise<TOutput>;
  readOnlyHint?: boolean;
}

/**
 * Registers an imperative tool with document.modelContext, ensuring
 * safe teardown with AbortSignal and fresh closure references.
 */
export function useWebMCPTool<TInput = Record<string, unknown>, TOutput = Record<string, unknown>>(
  tool: ToolDefinition<TInput, TOutput>,
  enabled: boolean = true
) {
  // Store the latest executor in a ref to avoid re-registering on every state change
  const executeRef = useRef(tool.execute);
  useEffect(() => {
    executeRef.current = tool.execute;
  });

  useEffect(() => {
    // Feature detection: Check for browser WebMCP support
    if (typeof document === "undefined" || !document.modelContext || !enabled) {
      return;
    }

    const abortController = new AbortController();

    try {
      document.modelContext.registerTool(
        {
          name: tool.name,
          description: tool.description,
          inputSchema: tool.inputSchema,
          execute: async (args: Record<string, unknown>) => {
            return await executeRef.current(args as TInput);
          },
          annotations: tool.readOnlyHint ? { readOnlyHint: true } : undefined,
        },
        { signal: abortController.signal }
      );
    } catch (err) {
      console.warn(`[WebMCP] Failed to register tool "${tool.name}":`, err);
    }

    // Teardown: AbortSignal unregisters the tool automatically
    return () => {
      abortController.abort();
    };
  }, [tool.name, tool.description, tool.readOnlyHint, enabled]);
}

Пример использования в реальном компоненте

В приведенном ниже компоненте просмотра документов агент может переключать страницы и считывать активный фрагмент текста:

TSX
"use client";

import React, { useState, useCallback } from "react";
import { useWebMCPTool } from "./useWebMCPTool";

interface DocumentViewerProps {
  totalPages: number;
  documentTitle: string;
}

export function DocumentViewer({ totalPages, documentTitle }: DocumentViewerProps) {
  const [currentPage, setCurrentPage] = useState<number>(1);
  const [selection, setSelection] = useState<string>("");

  // Tool 1: Jump to a specific page (Mutating action)
  useWebMCPTool({
    name: "navigate-document-page",
    description: "Navigates the interactive PDF viewer to a specific page number.",
    inputSchema: {
      type: "object",
      properties: {
        pageNumber: {
          type: "integer",
          minimum: 1,
          maximum: totalPages,
          description: "Target page index to display",
        },
      },
      required: ["pageNumber"],
    },
    execute: async (args: { pageNumber: number }) => {
      if (args.pageNumber < 1 || args.pageNumber > totalPages) {
        return {
          success: false,
          error: `Page ${args.pageNumber} out of bounds (1-${totalPages}).`,
        };
      }
      setCurrentPage(args.pageNumber);
      return {
        success: true,
        activePage: args.pageNumber,
        documentTitle,
      };
    },
  });

  // Tool 2: Read current selection (Read-only query)
  useWebMCPTool({
    name: "get-active-selection",
    description: "Retrieves the currently highlighted text snippet in the viewer.",
    inputSchema: {
      type: "object",
      properties: {},
    },
    readOnlyHint: true,
    execute: async () => {
      return {
        hasSelection: selection.length > 0,
        text: selection,
        pageNumber: currentPage,
      };
    },
  });

  return (
    <div className="viewer-container">
      <header>
        <h3>{documentTitle}</h3>
        <span>Page {currentPage} of {totalPages}</span>
      </header>
      <main
        onMouseUp={() => {
          const selectedText = window.getSelection()?.toString() || "";
          setSelection(selectedText);
        }}
        className="document-canvas"
      >
        <p>Displaying page content for page {currentPage}...</p>
      </main>
    </div>
  );
}

Шаблон 3: Ограничения Безопасности и Подтверждение Пользователем (Human-in-the-Loop)

Предоставление функций клиента внешним ИИ-агентам влечет за собой риски:

  • Агент может по ошибке запустить деструктивное действие (например, удалить черновик или инициировать транзакцию).
  • Атаки с внедрением подсказок (prompt injection) в стороннем контенте могут попытаться вызвать защищенные инструменты.

WebMCP предоставляет два механизма контроля: подсказку «только чтение» (readOnlyHint) и барьеры подтверждения пользователем.

1. Подсказки только для чтения

Для информационных инструментов всегда указывайте annotations: { readOnlyHint: true }. Это сообщает агенту и браузеру, что действие не изменяет состояние приложения, позволяя модели последовательно собирать сведения без лишних запросов на подтверждение.

2. Паттерн барьера подтверждения

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

TSX
"use client";

import React, { useState, useRef } from "react";
import { useWebMCPTool } from "./useWebMCPTool";

interface PendingAction {
  id: string;
  description: string;
  resolve: (value: { approved: boolean }) => void;
}

export function DestructiveActionShield() {
  const [pendingAction, setPendingAction] = useState<PendingAction | null>(null);

  useWebMCPTool({
    name: "delete-active-workspace",
    description: "Permanently deletes the current active workspace. Requires user confirmation.",
    inputSchema: {
      type: "object",
      properties: {
        reason: { type: "string", description: "Reason for deletion" },
      },
      required: ["reason"],
    },
    execute: async (input: { reason: string }) => {
      // Pause tool execution and wait for manual user approval
      return new Promise((resolve) => {
        setPendingAction({
          id: crypto.randomUUID(),
          description: `Delete workspace: "${input.reason}"`,
          resolve,
        });
      });
    },
  });

  return (
    <>
      {pendingAction && (
        <aside className="confirmation-modal" role="alertdialog">
          <h4>Agent Action Confirmation</h4>
          <p>An AI assistant requested permission to:</p>
          <blockquote>{pendingAction.description}</blockquote>
          <div className="button-row">
            <button
              type="button"
              onClick={() => {
                pendingAction.resolve({ approved: false });
                setPendingAction(null);
              }}
            >
              Reject
            </button>
            <button
              type="button"
              className="danger-btn"
              onClick={() => {
                pendingAction.resolve({ approved: true });
                setPendingAction(null);
              }}
            >
              Confirm Deletion
            </button>
          </div>
        </aside>
      )}
    </>
  );
}

Шаблон 4: Локальный Автономный Цикл: WebMCP + Chrome Gemini Nano

Одно из самых перспективных применений WebMCP — взаимодействие с встроенными локальными моделями. Связывая W3C Prompt API (window.LanguageModel) и document.modelContext, можно получить полностью автономного агента прямо во вкладке браузера.

CODE
+-------------------------------------------------------------+
| Browser Tab Memory Space                                    |
|                                                             |
|  [ User Goal ] ---> [ window.LanguageModel Session ]        |
|                               |                             |
|                               v                             |
|                   [ Tool Call JSON Output ]                 |
|                               |                             |
|                               v                             |
|               [ document.modelContext.execute ]             |
|                               |                             |
|                               v                             |
|                 [ React State Mutates DOM ]                 |
|                               |                             |
|                               v                             |
|             (Observation Result fed back to model)          |
+-------------------------------------------------------------+
TYPESCRIPT
// Client-side agent loop executing in the browser tab
export async function runLocalTabAgent(userPrompt: string): Promise<string> {
  // 1. Verify availability of on-device LLM and WebMCP
  if (!("ai" in window) || !document.modelContext) {
    throw new Error("On-device AI or WebMCP is not supported in this browser.");
  }

  // 2. Discover available tools exposed by mounted React components
  const availableTools = await document.modelContext.listTools();
  
  // Format tool descriptions for system prompt
  const toolDeclarations = availableTools.map((t) => ({
    name: t.name,
    description: t.description,
    parameters: t.inputSchema,
  }));

  // 3. Instantiate local session with Gemini Nano
  const session = await (window as unknown as {
    ai: {
      languageModel: {
        create: (options: { systemPrompt: string }) => Promise<{
          prompt: (msg: string) => Promise<string>;
          destroy: () => void;
        }>;
      };
    };
  }).ai.languageModel.create({
    systemPrompt: `You are an in-tab browser assistant. You can control the active page using tools: ${JSON.stringify(
      toolDeclarations
    )}. Output JSON tool invocations as {"tool": string, "args": object}.`,
  });

  try {
    const response = await session.prompt(userPrompt);
    
    // Parse structured tool call
    const action = JSON.parse(response);
    const targetTool = availableTools.find((t) => t.name === action.tool);

    if (targetTool) {
      const result = await targetTool.execute(action.args);
      return `Tool executed successfully: ${JSON.stringify(result)}`;
    }

    return response;
  } finally {
    session.destroy();
  }
}

Все вычисления происходят на клиентском оборудовании:

  • Задержка до первого токена (TTFT) составляет менее 30 миллисекунд.
  • Данные не покидают компьютер пользователя, что гарантирует соблюдение требований конфиденциальности.
  • Затраты на облачную инфраструктуру равны нулю.

Промышленные Реалии и Постепенное Улучшение

Пока стандарт WebMCP проходит согласование в рабочих группах, в реальных веб-приложениях его следует применять согласно концепции постепенного улучшения (progressive enhancement):

  1. Проверка доступности: Обязательно проверяйте наличие document.modelContext. Интерфейс React должен стабильно работать для людей независимо от поддержки функций ИИ в браузере.
  2. Компактные схемы: Локальные модели обладают меньшим окном контекста (обычно от 4k до 8k токенов). Формулируйте описания параметров лаконично и избегайте глубокой вложенности структур JSON.
  3. Атомарные обновления состояния: Завершайте промисы инструментов только после того, как React завершил рендеринг обновлений в DOM. Это предотвращает состояния гонки при последовательном вызове нескольких действий агентом.

Объединяя декларативные формы React 19 с императивными хуками WebMCP, разработчики превращают привычные пользовательские интерфейсы в структурированную среду для работы автономных ИИ-агентов.

Share this article