WebMCP sistemoje React: kliento būsenos ir formų veiksmų pateikimas naršyklės AI agentams
Paverskite React 19 saityno programas agentams pritaikytomis erdvėmis naudodami W3C WebMCP standartą, deklaratyvias formas ir useWebMCPTool kabliukus.

Kai programinės įrangos inžinieriai jungia kalbos modelius su išorinėmis sistemomis, Model Context Protocol (MCP) tapo standartiniu formatu įrankių iškvietimui. Vis dėlto beveik visos dabartinės MCP sistemos remiasi kliento-serverio modeliu: klientas (pvz., Claude Desktop ar IDE asistentas) bendrauja su fono procesu per stdio arba Server-Sent Events (SSE), kad atliktų užklausas duomenų bazėse, kviestų debesų API ar naršytų failų struktūrose.
Tačiau naršyklės viduje šis modelis susiduria su esminiais apribojimais.
Šiuolaikinės saityno programos yra sudėtingos vieno puslapio programos (SPA), valdančios didelį vietinės būsenos kiekį. Svarbi informacija (aktyvūs drobės canvas pažymėjimai, neįrašyti formų laukai, vietinis puslapiavimas, rikiavimo filtrai ar atviri modaliniai langai) egzistuoja tik kliento operatyviojoje atmintyje (React būsenoje, Zustand saugyklose ar DOM). Kai autonominis naršyklės agentas ar plėtinys bando atlikti veiksmus tokioje programoje, jam tenka remtis nepatikimu vizualiu DOM nuskaitymu (scraping), prieinamumo medžio analize ar sintetiniais pelės paspaudimais.
Besiformuojanti WebMCP (Web Model Context Protocol) specifikacija, kuriama W3C Web Machine Learning Community Group ir aprašyta W3C WebMCP Draft Community Group Report (bandoma Chromium), išsprendžia šią problemą, perkeldama Model Context Protocol tiesiai į naršyklės skirtuką.
Šiame gide nagrinėjama, kaip veikia WebMCP, kuo jis skiriasi nuo serverio MCP ir kaip pateikti React 19 komponentų būseną bei formų veiksmus naršyklės agentams pasitelkiant deklaratyvius HTML atributus ir React kabliukus (hooks).
+-------------------------------------------------------------------------+
| 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 |
+-------------------------------------------------------------------------+Kliento pusės įrankių vakuumas
Kad suprastume WebMCP prasmę, pažvelkime, kaip autonominis agentas šiandien atlieka paprastą užduotį, pavyzdžiui, „filtruoti lentelę pagal užsakymus, viršijančius 500 $, ir juos eksportuoti“:
- Prieinamumo medžio nuskaitymas: Agentas skaito prieinamumo medį arba fiksuoja ekrano kopiją. Jis bando atspėti filtro mygtuką tarp dešimčių įterptų
<div>ir<button>elementų. - Selektorių trapumas: Jei komanda atnaujina CSS modulius, Tailwind klases ar DOM medžio struktūrą, automatizuoti CSS selektoriai iškart nustoja veikti.
- Konteksto perteklius: Kiekviename žingsnyje siunčiant visą DOM medį ar vaizdus į LLM, išeikvojama dešimtys tūkstančių prieigos žetonų (tokens) ir sukeliama pastebima delsos problema.
- Sintetinių paspaudimų bėdos:
MouseEventarKeyboardEventįvykių siuntimas dažnai apeina React sintetinių įvykių tvarkykles, todėl atsiranda pasenusių uždarymų (stale closures) ir neįvykdomos validacijos.
WebMCP pakeičia šiuos apytikslius metodus struktūrizuotais, programiniais RPC iškvietimais pačiame kliento vykdymo sraute. Puslapis registruoja konkrečius įrankius su griežtomis JSON schemomis.
WebMCP ir serverio MCP palyginimas
Skirtumas tarp serverio MCP ir WebMCP yra esminis:
| Aspektas | Serverio MCP (Node.js / Python) | WebMCP (Naršyklės lygmeniu) |
|---|---|---|
| Vykdymo aplinka | Foninis procesas, konteineris, serverless funkcija | Aktyvaus naršyklės skirtuko vykdymo gija |
| Transportas | stdio, Server-Sent Events (SSE), WebSockets |
Tiesioginė JavaScript funkcijos nuoroda |
| Tiksliniai duomenys | Nutolusios duomenų bazės, failų sistemos, debesų API | React būsena, maršrutizatorius, vietinė saugykla, DOM |
| Palaikomi elementai | Tools, Resources, Prompts | Tik Tools (apribota aktyviu dokumentu) |
| Saugumo riba | Procesų izoliacija, OS teisės, API raktai | Naršyklės smėliadėžė, Same-Origin Policy, vartotojo patvirtinimas |
WebMCP nenaudoja tinklo lizdų ar šalutinių procesų. Pats saityno dokumentas tampa įrankių registru per document.modelContext sąsają.
Naršyklės architektūra: document.modelContext
Chromium naršyklėse (nuo Chromium 146 versijos su vėliavėle #enable-webmcp-testing) suteikiamas įrankių tarpininkas, pasiekiamas per visuotinį document objektą:
// 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;
}
}Šią sąsają apibrėžia dvi taisyklės:
- Dokumento galiojimo sritis: Įrankiai yra tiesiogiai susieti su dokumento gyvavimo ciklu. Kai naudotojas išeina iš puslapio ar užveria skirtuką, registras sunaikinamas.
- Išregistravimas per AbortSignal: Užuot pateikus atskirą
unregisterToolmetodą, įrankių gyvavimo ciklas valdomas per standartinįAbortSignal. Nutraukus signalą, naršyklė tvarkingai pašalina įrankį.
1 modelis: Deklaratyvus WebMCP su React 19 formomis
Paprasčiausias būdas suteikti galimybes naršyklės agentams: naudoti deklaratyvią WebMCP API. Ji papildo įprastas HTML formas specialiais atributais:
toolname: Unikalus įrankio pavadinimas.tooldescription: Glaustas įrankio paskirties paaiškinimas, nurodantis agentui, kada jį kviesti.
Sistemoje React 19 deklaratyvios formos sklandžiai jungiasi su useActionState ir serverio bei kliento veiksmais.
"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 > ${state.minAmount} in category "{state.category}"
</p>
)}
</form>
);
}Agentui atidarius puslapį, naršyklė peržiūri DOM medį, aptinka formas su toolname atributu ir sukuria parametrų schemą pagal laukų pavadinimus bei apribojimus. Agentui iškvietus filter-orders, naršyklė perduoda vykdymą tiesiai į React formAction.
2 modelis: Imperatyvus useWebMCPTool kabliukas sistemoje React
Nors deklaratyvios formos tinka įprastoms įvestims, sudėtingose programose būtina imperatyvi įrankių registracija. Pavyzdžiui, agentui gali tekti pakeisti drobės mastelį, atlikti paiešką atmintyje esančioje lentelėje ar pereiti kelis vedlio žingsnius.
Kad tai integruotume pagal React principus, sukuriame specialų kabliuką, atitinkantį tris reikalavimus:
- Dinaminis gyvavimo ciklas: Įrankiai turi būti užregistruoti prijungus komponentą ir pašalinti jį atjungus.
- Prieiga prie naujausios būsenos: Funkcija turi pasiekti naujausius rekvizitus (props) ir būseną, neišprovokuodama pakartotinių registracijų.
- Išvalymas su AbortSignal: Tvarkingas išteklių atlaisvinimas keičiantis maršrutams.
Štai išbaigtas useWebMCPTool kabliuko kodas:
"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]);
}Praktinis panaudojimas komponente
Žemiau pateiktas interaktyvus dokumentų peržiūros komponentas, kuriame agentas gali versti puslapius ir perskaityti pažymėtą tekstą:
"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 modelis: Saugumo užkardos ir patvirtinimas su žmogaus įsikišimu
Atveriant kliento funkcijas AI agentams, būtina valdyti rizikas:
- Agentas gali netyčia ištrinti neįrašytus duomenis arba inicijuoti nepageidaujamą mokėjimą.
- Svetainėje esantys raginimų injekcijos (prompt injection) bandymai gali siekti atlikti jautrius veiksmus.
WebMCP siūlo du būdus šioms rizikoms mažinti: tik skaitymo žymą (readOnlyHint) ir patvirtinimo užkardas su žmogaus dalyvavimu.
1. Tik skaitymo žymos
Užklausų įrankiams visada nurodykite annotations: { readOnlyHint: true }. Tai praneša agentui ir naršyklei, kad veiksmas neturi šalutinio poveikio būsenai, todėl modelis gali rinkti informaciją be pakartotinių patvirtinimo prašymų.
2. Patvirtinimo užkardos modelis
Destruktyviems ar svarbiems veiksmams nereikėtų iškart patvirtinti įrankio pažado (promise). Vietoje to komponentas parodo patvirtinimo langą naudotojui. Įrankio pažadas išsprendžiamas tik tada, kai naudotojas spusteli „Patvirtinti“.
"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 modelis: Autonominis vietinis ciklas: WebMCP + Chrome Gemini Nano
Vienas įspūdingiausių WebMCP pritaikymo būdų apima darbą su vietiniais įrenginio kalbos modeliais. Sujungus W3C Prompt API (window.LanguageModel) su document.modelContext, gaunamas visiškai vietinis, minimalios delsos autonominis agentas naršyklės skirtuke.
+-------------------------------------------------------------+
| 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) |
+-------------------------------------------------------------+// 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();
}
}Šis procesas vyksta vien tik įrenginio techninėje įrangoje:
- Laikas iki pirmojo žetono (TTFT) siekia mažiau nei 30 milisekundžių.
- Jokie duomenys nepalieka naudotojo kompiuterio, užtikrinant BDAR reikalavimų laikymąsi.
- Nėra jokių debesų infrastruktūros išlaidų.
Gamybos realijos ir laipsniškas tobulinimas
Kol WebMCP specifikacija tobulinama standartų komitetuose, komercinėse programose ją verta taikyti pagal laipsniško tobulinimo (progressive enhancement) principą:
- Galimybių tikrinimas: Visada patikrinkite
document.modelContextbuvimą. React programa turi veikti be priekaištų įprastiems naudotojams net ir naršyklėse be agentų palaikymo. - Kompaktiškos schemos: Įrenginiuose veikiantys modeliai turi ribotą konteksto langą (dažniausiai nuo 4k iki 8k žetonų). Parametrų aprašymai turi būti trumpi ir aiškūs.
- Atominiai būsenos atnaujinimai: Užtikrinkite, kad įrankių pažadai būtų išsprendžiami tik po to, kai React užbaigia DOM atnaujinimą, taip išvengiant konkurencinių situacijų (race conditions).
Sujungus React 19 formų priemones su imperatyviais WebMCP kabliukais, pasyvios saityno sąsajos tampa paruoštos autonominių AI agentų veiksmams.