Zurück zum Journal
12 Min. Lesezeit

WebMCP in React: Client-State und Formular-Aktionen für Browser-KI-Agenten bereitstellen

Verwandeln Sie React 19-Webanwendungen mit dem W3C WebMCP-Standard, deklarativen Formularattributen und useWebMCPTool-Hooks in agentenfähige Schnittstellen.

React.jsTypeScriptModel Context Protocol (MCP)LLM AgentsIn-Browser LLMs
WebMCP in React: Client-State und Formular-Aktionen für Browser-KI-Agenten bereitstellen

Wenn Software-Ingenieure große Sprachmodelle mit externen Systemen verknüpfen, hat sich das Model Context Protocol (MCP) rasch als Standardformat für Tool-Aufrufe etabliert. Fast alle bisherigen MCP-Architekturen basieren jedoch auf einem klassischen Client-Server-Modell: Ein Client (wie Claude Desktop oder ein IDE-Assistent) kommuniziert über stdio oder Server-Sent Events (SSE) mit Hintergrundprozessen, um Datenbanken abzufragen, Cloud-APIs auszuführen oder Dateisysteme zu durchsuchen.

Innerhalb des Webbrowsers stößt dieses Paradigma an seine Grenzen.

Moderne Webanwendungen sind komplexe Single-Page-Applications (SPAs) mit ausgeprägtem internem Zustand. Wichtige Zustandsdaten – wie aktive Canvas-Auswahlen, ungespeicherte Formulardaten, clientseitige Paginierung, Filterparameter oder interaktive Modalfenster – existieren ausschließlich im Arbeitsspeicher des Clients (React-Komponentenstatus, Zustand-Stores oder DOM). Wenn ein autonomer Browser-Agent oder eine Erweiterung mit einer Webanwendung interagieren möchte, bleibt meist nur der Rückgriff auf instabiles DOM-Scraping, das Parsen von Accessibility-Bäumen oder künstlich simulierte Maus-Events.

Die neue Spezifikation WebMCP (Web Model Context Protocol) – entwickelt in der W3C Web Machine Learning Community Group und als experimentelle Vorschau in Chromium integriert – schließt diese Lücke, indem sie das Model Context Protocol direkt in den Browser-Tab einbettet.

Dieser Leitfaden veranschaulicht die Funktionsweise von WebMCP, grenzt es von serverseitigem MCP ab und demonstriert, wie React 19-Zustände und Formularaktionen über deklarative HTML-Attribute und React-Hooks für Browser-Agenten bereitgestellt werden.

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    |
+-------------------------------------------------------------------------+

Das Tool-Vakuum auf der Client-Seite

Um die Notwendigkeit von WebMCP zu verstehen, genügt ein Blick auf herkömmliche Versuche eines KI-Agenten, eine alltägliche Aktion auszuführen – etwa „Filtere die Tabelle nach Bestellungen über 500 $ und exportiere das Ergebnis“:

  1. Accessibility-Tree-Scraping: Der Agent liest den Accessibility-Baum aus oder analysiert Screenshots. Er muss die Filter-Schaltfläche aus Dutzenden verschachtelten <div>- und <button>-Tags erraten.
  2. Fragile Selektoren: Sobald das Entwicklungsteam CSS-Module, Tailwind-Klassen oder DOM-Strukturen ändert, schlagen automatisierte CSS-Selektoren fehl.
  3. Kontext-Overhead: Das Senden eines mehrere Megabyte großen DOM-Baums oder Bild-Snapshots bei jedem Einzelschritt verbraucht zehntausende Tokens und erzeugt erhebliche Netzwerklatenz.
  4. Probleme synthetischer Klicks: Das Auslösen von MouseEvent- oder KeyboardEvent-Events umgeht häufig Reacts synthetisches Event-System, was zu veralteten Closures oder übergangenen Validierungen führen kann.

WebMCP ersetzt dieses fehleranfällige Vorgehen durch strukturierte, programmatische RPC-Aufrufe direkt im Ausführungskontext des Browsers. Anstelle unsicherer Selektoren registriert die Seite eindeutige Werkzeuge mit typisierten JSON-Schemas.

WebMCP im Vergleich zu Backend-MCP

Die Unterschiede zwischen serverseitigem MCP und WebMCP sind grundlegend:

Dimension Backend-MCP (Node.js / Python) WebMCP (Nativ im Browser)
Ausführungskontext Hintergrund-Dienst, Container, Serverless-Funktion Ausführungs-Thread des aktiven Browser-Tabs
Transport stdio, Server-Sent Events (SSE), WebSockets Direkter JavaScript-Funktionsaufruf
Zieldaten Externe Datenbanken, Dateisysteme, Drittanbieter-APIs React-State, Client-Router, lokaler Speicher, DOM
Unterstützte Primitive Tools, Resources, Prompts Ausschließlich Tools (gebunden an das aktive Dokument)
Sicherheitsgrenze Prozess-Isolation, OS-Rechte, API-Keys Browser-Sandbox, Same-Origin-Policy, Nutzerbestätigung

WebMCP benötigt weder Netzwerk-Sockets noch Child-Prozesse. Das Webdokument selbst fungiert über die Schnittstelle document.modelContext als Registry.


Die Browser-Architektur: document.modelContext

In Chromium-Builds (ab Chromium 146 unter dem Flag #enable-webmcp-testing) stellt der Browser einen Tool-Broker direkt auf dem globalen document-Objekt bereit:

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;
  }
}

Zwei Kernprinzipien strukturieren diese Schnittstelle:

  1. Dokument-Gültigkeitsbereich: Werkzeuge sind an den Lebenszyklus des Dokuments gebunden. Verlässt der Nutzer die Seite oder schließt den Tab, wird die Registry verworfen.
  2. Abmeldung über AbortSignal: Statt einer separaten unregisterTool-Methode wird der Lebenszyklus über standardisierte AbortSignal-Primitive gesteuert. Bricht das Signal ab, entfernt der Browser das Tool automatisch.

Muster 1: Deklaratives WebMCP mit React 19-Formularen

Der geradlinigste Weg zur Bereitstellung clientseitiger Funktionen ist die deklarative WebMCP-API. WebMCP erweitert herkömmliche HTML-Formulare um Attribute für Agenten:

  • toolname: Eindeutiger Name des Werkzeugs.
  • tooldescription: Prägnante Beschreibung der Funktionalität und wann der Agent sie aufrufen sollte.

In React 19 arbeiten deklarative Formulare direkt mit useActionState und Client-/Server-Aktionen zusammen.

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>
  );
}

Öffnet ein Agent die Seite, analysiert der Browser das DOM nach Formularen mit toolname-Attributen und generiert anhand der Feldnamen, Datentypen und Validierungsregeln dynamisch ein Tool-Schema. Ruft der Agent filter-orders auf, übergibt der Browser die Eingaben direkt an Reacts formAction.


Muster 2: Der imperative useWebMCPTool-Hook in React

Während deklarative Formulare für einfache Eingaben ausreichen, verlangen anspruchsvollere Anwendungen eine imperative Tool-Registrierung. Ein Agent muss eventuell eine Zoom-Ebene steuern, einen In-Memory-Index durchsuchen oder Navigationsschritte ausführen.

Für eine saubere React-Architektur erstellen wir einen benutzerdefinierten Hook, der drei zentrale Herausforderungen löst:

  1. Dynamischer Lebenszyklus: Werkzeuge müssen beim Mounten registriert und beim Unmounten entfernt werden.
  2. Aktueller Komponentenstatus: Der Callback muss auf die neuesten Props und States zugreifen können, ohne wiederholte Neuregistrierungen zu erzwingen.
  3. Ressourcenfreigabe via AbortSignal: Saubere Bereinigung bei SPA-Routenwechseln.

Hier ist die praxiserprobte Implementierung von 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]);
}

Verwendung in einer realen Komponente

Das folgende Beispiel zeigt einen Dokumenten-Betrachter, in dem der KI-Agent Seiten wechseln und den aktuellen Textauszug abfragen kann:

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>
  );
}

Muster 3: Schutzmechanismen und Nutzerbestätigung (Human-in-the-Loop)

Das direkte Bereitstellen clientseitiger Methoden für KI-Agenten birgt betriebliche und sicherheitstechnische Risiken:

  • Ein Agent könnte unbeabsichtigt ungespeicherte Daten verwerfen oder kritische Transaktionen auslösen.
  • Prompt-Injections in analysierten Webtexten könnten versuchen, sensible Werkzeuge unbefugt anzusteuern.

WebMCP begegnet diesen Risiken mit zwei Instrumenten: Read-Only-Hinweisen (readOnlyHint) und Interaktiven Bestätigungsbarrieren.

1. Read-Only-Hinweise

Reine Leseabfragen sollten stets mit annotations: { readOnlyHint: true } gekennzeichnet werden. Dies signalisiert dem Agenten und der Browser-Umgebung, dass der Aufruf nebenwirkungsfrei ist, wodurch das Modell Recherche-Schritte ohne wiederholte Sicherheitsabfragen bündeln kann.

2. Das Bestätigungs-Muster

Bei irreversiblen oder statusverändernden Aktionen sollte das Tool-Promise nicht unmittelbar aufgelöst werden. Stattdessen versetzt der Hook die React-Oberfläche in einen Bestätigungsmodus. Erst wenn die Anwenderin oder der Anwender im Dialog zustimmt, schließt das Promise erfolgreich ab.

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>
      )}
    </>
  );
}

Muster 4: Autonomer Tab-Agent: WebMCP + Chrome Gemini Nano

Ein besonders leistungsfähiges Einsatzfeld von WebMCP entsteht im Verbund mit lokalen On-Device-Modellen. Über die Verknüpfung der W3C Prompt API (window.LanguageModel) mit document.modelContext lässt sich ein vollständig autarker In-Tab-Agent realisieren.

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();
  }
}

Diese Ausführung erfolgt komplett auf der lokalen Hardware:

  • Die Latenz bis zum ersten Token (TTFT) liegt unter 30 Millisekunden.
  • Es fließen keine Daten an externe Server, was den Anforderungen von DSGVO und Bankenstandards entspricht.
  • Es entstehen keine laufenden API-Kosten.

Produktionspraxis und Progressive Enhancement

Solange sich WebMCP im Standardisierungsprozess befindet, sollte die Technologie nach dem Prinzip der schrittweisen Verbesserung (Progressive Enhancement) eingesetzt werden:

  1. Feature-Erkennung: Prüfen Sie stets die Verfügbarkeit von document.modelContext. Die React-Anwendung muss für menschliche Benutzer uneingeschränkt benutzbar bleiben, selbst wenn ein Browser keine KI-Schnittstellen unterstützt.
  2. Kompakte Schemas: Lokale Gerätemodelle verfügen über kleinere Kontextfenster als Cloud-Modelle (meist 4k bis 8k Tokens). Parameterbeschreibungen sollten prägnant formuliert und tiefe Verschachtelungen vermieden werden.
  3. Atomare Zustandsübergänge: Achten Sie darauf, dass Tool-Aktionen erst dann zurückkehren, wenn React die DOM-Aktualisierungen abgeschlossen hat. Führt ein Agent zwei Werkzeuge nacheinander aus, muss der Folgeschritt stets auf dem frischen Zustand aufsetzen.

Durch die Kombination von React 19-Formularprimitiven mit imperativen WebMCP-Hooks wandeln Sie passive Benutzeroberflächen in strukturierte Aktionsräume für moderne KI-Agenten um.

Share this article