React Web Mcp
БесплатноНе проверенReact hooks and components for the WebMCP standard — expose your web app's functionality as tools for in-browser AI agents.
Описание
React hooks and components for the WebMCP standard — expose your web app's functionality as tools for in-browser AI agents.
README
React hooks and components for WebMCP — the emerging web standard that lets your web app expose its functionality as tools that in-browser AI agents (like Gemini in Chrome) can discover and call.
import { useWebMCPTool } from "@cr4yfish/react-web-mcp";
function TodoList() {
const [todos, setTodos] = useState<string[]>([]);
useWebMCPTool({
name: "add-todo",
description: "Add a new item to the user's todo list",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "The text of the todo item" },
},
required: ["text"],
},
execute: ({ text }) => {
setTodos((prev) => [...prev, text]);
return `Added todo: "${text}"`;
},
});
return <ul>{todos.map((t) => <li key={t}>{t}</li>)}</ul>;
}
That's it. While <TodoList /> is mounted, any WebMCP-capable agent in the browser can add todos through your own application logic — state updates, UI, and user all stay in sync.
Using an AI coding agent? This package ships its full docs inside the tarball, at
node_modules/@cr4yfish/react-web-mcp/doc/(plus anAGENTS.mdpointer at the package root). Point your agent there to use the API and the WebMCP standard reference offline — start withdoc/index.mdordoc/llms.txt.
What is WebMCP?
WebMCP (Web Model Context Protocol) is a proposed web standard, incubated by Google and Microsoft in the W3C Web Machine Learning group, that turns a web page into an in-browser MCP-style tool server.
Today, browser agents interact with pages the hard way: screenshots, accessibility-tree scraping, and simulated clicks. That's slow and brittle. WebMCP instead lets the page describe its capabilities directly:
- Imperative API — JavaScript registers tools on
document.modelContext(early Chrome versions:navigator.modelContext, deprecated since Chrome 150). Each tool has a name, a natural-language description, a JSON Schema for its inputs, and anexecutecallback that runs your existing client-side code. - Declarative API — plain HTML
<form>elements become tools via thetoolname,tooldescription, andtoolautosubmitattributes; the browser synthesizes the input schema from the form's controls (name,required,toolparamdescription, …).
Unlike backend MCP servers, everything runs client-side in the page: tools share the user's existing session, auth, and UI state — no separate server, no replicated credentials, and the user watches (and can intervene in) everything the agent does. WebMCP complements backend MCP; it doesn't replace it.
Browser support (as of mid-2026): Chrome 146+ behind the chrome://flags/#enable-webmcp-testing flag, Chrome 149+ via origin trial, Edge 147+ natively. In every other browser the API simply doesn't exist — which is why this package makes every primitive a safe no-op when WebMCP is unavailable.
Further reading:
- Chrome WebMCP docs — imperative API, declarative API, best practices, security, evals, WebMCP vs. MCP
- Spec & explainer
- Official demos & tooling (includes a React demo, an evals CLI, and a tool inspector)
Why this package?
Using WebMCP from raw React means hand-rolling the same plumbing in every app:
- registering on mount / unregistering on unmount (with
AbortController); - not re-registering on every render while still letting
executesee fresh state (the stale-closure trap); - supporting both
document.modelContextand the deprecatednavigator.modelContext; - staying SSR-safe in Next.js (no
navigator/documenton the server); - normalizing return values and errors into the MCP
CallToolResultshape; - TypeScript types for an API that isn't in
lib.dom.d.tsyet (including the declarative JSX attributes).
react-web-mcp packages all of that, with zero runtime dependencies.
Installation
npm install @cr4yfish/react-web-mcp
# or
pnpm add @cr4yfish/react-web-mcp
Requires React 18+. Works in Vite, CRA, Remix, and Next.js (App Router and Pages Router — the main entry ships a "use client" directive).
Usage
Check for support: useWebMCP
import { useWebMCP } from "@cr4yfish/react-web-mcp";
function AgentBadge() {
const { isSupported } = useWebMCP();
if (!isSupported) return null;
return <span>🤖 This page is agent-ready</span>;
}
SSR-safe: returns false on the server and during hydration, the real value after mount.
Register a tool: useWebMCPTool
import { useWebMCPTool } from "@cr4yfish/react-web-mcp";
function ProductSearch({ category }: { category: string }) {
const [results, setResults] = useState<Product[]>([]);
const { isRegistered } = useWebMCPTool({
name: "search-products",
description:
"Searches the product catalog and updates the visible result list. Returns the matching products as JSON.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Free-text search query" },
maxPrice: { type: "number", description: "Optional price ceiling in EUR" },
},
required: ["query"],
},
annotations: { readOnlyHint: true },
// `execute` may close over props/state freely — it always sees the
// latest values and changing it never re-registers the tool.
execute: async ({ query, maxPrice }) => {
const products = await searchProducts(query, { category, maxPrice });
setResults(products);
return products; // objects are JSON-stringified (and length-capped) for the agent
},
});
return <ResultList items={results} highlight={isRegistered} />;
}
Behavior:
- Registered while mounted, unregistered on unmount.
- Re-registers only when the definition changes (name, description, schemas, annotations,
exposedTo,enabled) — inlineinputSchemaobject literals are fine. enabled: falseunregisters without unmounting (e.g. tie tools to auth state).- Return values are normalized: strings → text content, objects → JSON text (truncated at 50 000 chars by default),
ToolResponseobjects pass through, thrown errors becomeisErrorresponses the agent can read and recover from. - Incoming arguments are validated against
inputSchemabeforeexecuteruns — the agent is an untrusted client and browsers don't enforce the schema. Schema-violating calls are answered with a readableisErrorresponse (so the agent can self-correct) and never reach your code: no more hand-rolledtypeofchecks for types,required,enum,min/maxLength, ranges. Conservative by design (composition keywords likeanyOf/$refare skipped, unknown keywords ignored); opt out per tool withvalidateInput: false. - Declare an
outputSchemaand your raw return value is passed through unnormalized for the browser to validate.
Declarative forms: ToolForm
import { ToolForm, toolParamAttrs } from "@cr4yfish/react-web-mcp";
function ReservationForm() {
return (
<ToolForm
name="book-table"
description="Books a table at the restaurant. Asks for party size and a date."
indicators
autoSubmit={false} // consequential action: opt into review-before-submit
onAgentSubmit={async (data) => {
const confirmation = await bookTable({
partySize: Number(data.get("partySize")),
date: String(data.get("date")),
});
return `Booked! Confirmation code: ${confirmation.code}`;
}}
>
<input
type="number"
name="partySize"
min={1}
max={12}
required
{...toolParamAttrs("Number of guests (1-12)")}
/>
<input type="date" name="date" required {...toolParamAttrs("Reservation date")} />
<button type="submit">Book</button>
</ToolForm>
);
}
- Renders a regular
<form>with the declarative WebMCP attributes; the browser synthesizes the input schema from the form controls. autoSubmitdefaults totrue(the agent submits the form itself), deliberately flipping the platform's human-in-the-loop default: in current Chromium, a pending review invocation that gets re-invoked is dropped and the page's WebMCP channel closes — every tool dies silently until reload. Auto-submission answers immediately, so that state never exists.- Review mode is channel-safe by default: with
autoSubmit={false},reviewResponsedefaults to"immediate"— each invocation is answered right away with a staged "form filled, awaiting user review" response (useFormToolsemantics), nothing stays pending browser-side, and the user's review submit completes as a normal form submission (handle it inonSubmit;onAgentSubmitis not called). Opt into the platform-native pending-until-submit flow withreviewResponse="on-submit"when the agent needs the final submitted data — that mode is protected by the re-invoke guard and the watchdog, but a re-invoke whose fill changes no values is invisible to the page and can still kill the channel. onAgentSubmithandles agent-invoked submissions without navigation: the default action is prevented and your return value is piped back to the agent viaSubmitEvent.respondWith(). User submissions behave exactly as before.indicators(opt-in) highlights the agent-filled/awaiting-review state: a small stylesheet is injected once per page, keyed on the native:tool-form-active/:tool-submit-activepseudo-classes plus adata-webmcp-active="true"attribute fallback the component maintains. Override the color with--webmcp-indicator-color, or skip the prop and style those selectors yourself (WEBMCP_INDICATOR_CSSis exported as a starting point).onPendingChange(pending)observes the same state programmatically (e.g. to render a "review & submit" banner).- Lifecycle safety (important): Chromium keeps exactly one pending invocation per form. If the tool is invoked again while a previous invocation still awaits the user's submit, the browser silently drops the older invocation's reply callback — which can close the page's WebMCP channel and disable every tool until reload.
ToolFormdefends against this:pendingTimeoutMs(default 2 minutes,0disables) auto-cancels a stale invocation viaform.reset()— the sanctioned page-side cancel that returns a proper "cancelled" error to the agent and keeps the channel healthy — and an overlapping invocation is reported as a loudinvocation-overlaperror diagnostic. - Re-invoke guard (
reinvokeGuard, defaulttrue; applies toreviewResponse="on-submit"forms): if the agent re-invokes while an invocation is pending, the guard catches the new fill'sinputevents — the one window before Chromium drops the old reply — cancels the old invocation cleanly viaform.reset()(snapshotting and restoring every control value so the new fill survives), and emits aninvocation-reinvokedwarning. Limits: a fill that changes no values dispatches no events and cannot be caught; don't combine with areset-event listener that callspreventDefault(). resetAfterAgentSubmitresets the form one tick after an agent submission was answered, so the next invocation starts from a clean slate.- In browsers without WebMCP it's just a normal form.
Prefer to keep your own <form>? Use the attribute helpers (TypeScript JSX types for toolname etc. are included):
import { toolFormAttrs, toolParamAttrs } from "@cr4yfish/react-web-mcp";
<form {...toolFormAttrs({ name: "search", description: "Search the site", autoSubmit: true })}>
<input name="q" required {...toolParamAttrs("The search query")} />
</form>
Any UI library, zero adapters: useFormTool
Component libraries like Material UI or Ant Design often don't forward the declarative tool* attributes to the DOM. useFormTool sidesteps the problem entirely: it derives the input schema from the rendered form element — so it works with any library, portals included, with no per-library adapters.
import { useRef } from "react";
import { useFormTool } from "@cr4yfish/react-web-mcp";
function CheckoutForm() {
const formRef = useRef<HTMLFormElement>(null);
useFormTool({
formRef,
name: "fill-checkout",
description: "Fills out the checkout form with shipping details.",
// autoSubmit: false (default) → agent fills, user reviews & submits
});
return (
<form ref={formRef}>
<TextField name="street" label="Street" required />
<TextField name="zip" label="ZIP code" required inputProps={{ pattern: "\\d{5}" }} />
<Select name="country" label="Country">…</Select>
<Button type="submit">Order</Button>
</form>
);
}
- The schema comes from
form.elements: controlnames, types (number→number,checkbox→boolean, radio groups &<select>→enums),required,min/max/maxLength/pattern, and descriptions fromtoolparamdescription/aria-label/<label for>/placeholder. Password, hidden, and file inputs are never exposed. - When the agent calls the tool, fields are filled via native value setters +
input/changeevents, so controlled React inputs update correctly. Then: user review (default),autoSubmit: true(requestSubmit()), or your ownonToolCall(args, form). - Dynamic forms: call the returned
refresh()after fields change.
Batches of tools: useWebMCPTools
useWebMCPTools([
{ name: "get-cart", description: "…", annotations: { readOnlyHint: true }, execute: () => cart },
{ name: "add-to-cart", description: "…", inputSchema, execute: addToCart },
]);
Registers each tool individually (not via provideContext), so multiple components can own batches without clobbering each other.
React to agent activity: useWebMCPEvent
import { useWebMCPEvent } from "@cr4yfish/react-web-mcp";
useWebMCPEvent("toolactivated", () => {
// e.g. scroll a filled-out form into view for user review
});
useWebMCPEvent("toolcanceled", () => {
// the agent abandoned an in-flight invocation
});
useWebMCPEvent("toolchange", () => {
// the page's toolset changed
});
These handlers fire in real Chrome: Chromium dispatches toolactivated and the cancel event at the window (not the ModelContext object) and names the cancel event toolcancel — the hook listens on both targets and both spellings, deduped, so you don't have to care. The event's toolName property (when present) names the tool concerned. Outside React, the same plumbing is available as addWebMCPEventListener(name, handler).
Never fail silently: verbose mode & diagnostics
Everything the package observes — registrations, agent invocations, responses, validation rejections, cancelled or overlapping invocations, truncated results — flows through a single diagnostics stream. Errors and warnings always reach the console; info-level lifecycle logs appear when verbose mode is on:
import { setWebMCPVerbose, onWebMCPDiagnostic } from "@cr4yfish/react-web-mcp";
setWebMCPVerbose(true); // e.g. on your /test page or behind a debug flag
// Mirror everything into your own UI (all levels, regardless of verbose):
const unsubscribe = onWebMCPDiagnostic(({ level, code, toolName, message }) => {
appendToDebugLog(`[${level}] ${code} ${toolName ?? ""}: ${message}`);
});
Diagnostic codes worth alerting on: invocation-overlap (a declarative form tool was re-invoked while a previous invocation was pending — see the lifecycle-safety note above), invocation-timeout (the watchdog cancelled a stale invocation), respondwith-missing, agent-response-error, register-failed.
Outside React: @cr4yfish/react-web-mcp/vanilla
A React-free entry point, safe to import from server components and plain scripts:
import { registerTool, provideContext, isWebMCPSupported } from "@cr4yfish/react-web-mcp/vanilla";
const unregister = registerTool({
name: "get-cart",
description: "Returns the current shopping cart contents as JSON.",
annotations: { readOnlyHint: true },
execute: () => cartStore.getState(),
});
// Replace the whole toolset at once (e.g. after login):
const clear = provideContext([toolA, toolB, toolC]);
API reference
| Export | Kind | Purpose |
|---|---|---|
useWebMCPTool(options) |
hook | Register an imperative tool for the component's lifetime |
useWebMCPTools(tools, options?) |
hook | Register a batch of tools (individually, composable) |
useFormTool(options) |
hook | Tool with a schema derived from a rendered <form> (works with MUI/AntD/any library) |
useWebMCP() |
hook | { isSupported, modelContext } |
useWebMCPEvent(name, handler) |
hook | Subscribe to toolchange / toolactivated / toolcanceled (window + ModelContext targets, toolcancel alias) |
ToolForm |
component | <form> registered as a declarative tool: onAgentSubmit, indicators, pendingTimeoutMs watchdog, onPendingChange, resetAfterAgentSubmit |
setWebMCPVerbose(on) / isWebMCPVerbose() |
function | Toggle verbose lifecycle logging ([webmcp] console prefix) |
onWebMCPDiagnostic(listener) |
function | Subscribe to every diagnostic the package emits; returns unsubscribe |
addWebMCPEventListener(name, handler) |
function | Framework-agnostic event subscription (same plumbing as useWebMCPEvent) |
injectWebMCPIndicatorStyles() / WEBMCP_INDICATOR_CSS |
function/const | Default visual-indicator stylesheet for agent-filled forms |
registerTool(tool, options?) |
function | Framework-agnostic registration; returns unregister() |
provideContext(tools) |
function | Replace the page's whole toolset; returns unregister() |
getModelContext() / isWebMCPSupported() |
function | Feature detection (SSR-safe) |
isWebMCPTestingSupported() |
function | Detects the #enable-webmcp-testing flag / Tool Inspector API |
extractFormSchema(form) / applyArgsToForm(form, args) |
function | DOM-based schema synthesis & agent-driven form filling |
textResult(text, isError?) / jsonResult(value, maxLength?) |
function | Build MCP-shaped tool responses |
toolFormAttrs(...) / toolParamAttrs(...) |
function | Declarative attribute bags for your own elements |
WebMCPTool, ToolResponse, ToolAnnotations, JSONSchema, ModelContext, … |
types | Full typings, incl. document.modelContext global augmentation |
Best practices (short version)
From Chrome's guidance and the security docs:
- Names & descriptions are your API docs for the model. Use clear, action-oriented names (
search-flights, nottool1) and succinct descriptions that say what the tool does, when to use it, and what it returns. - Validate inputs. Treat tool arguments like any other untrusted user input — agents make mistakes and schemas are hints, not enforcement.
- Keep outputs succinct and structured. Return compact JSON, not HTML dumps. (
jsonResulttruncates oversized payloads for you.) - Return errors as information, not exceptions — an agent can read
"No flights found for that date"and try again. This package converts thrown errors intoisErrorresponses automatically. - Honor human-in-the-loop. Mark read-only tools with
readOnlyHint, destructive ones withdestructiveHint; use review-before-submit forms (autoSubmit={false}) for consequential actions — knowing that in current Chromium a pending review invocation that gets re-invoked kills the page's tool channel, which is whyToolFormauto-submits by default. - Mind exposure. Tools are visible to the page, same-origin frames, and the built-in browser agent by default. Only widen this with
exposedTo: [origins]for origins you'd trust with the same data. - Treat agents as another client, not a trusted principal. Anything a tool allows, assume it will be called with arbitrary arguments at arbitrary times.
- Evaluate. Use the WebMCP evals CLI and the Chrome DevTools WebMCP panel to test that real models pick the right tools with the right arguments.
Security & supply chain
WebMCP tools execute inside your page with your users' sessions — a compromised dependency in the tool-registration path could silently register, alter, or hijack tools. This package is built to keep that surface minimal and verifiable:
- Zero runtime dependencies. The only thing
react-web-mcpadds to your bundle is its own code;reactis a peer dependency you already ship. No transitive packages can be compromised because there are none. - Small, auditable codebase. A few hundred lines of TypeScript in
src/— review it in one sitting. What you audit is what runs: the committeddist/is checked in CI against a fresh build and fails the pipeline if it drifts from the source. - Locked, audited installs. The lockfile is committed; CI installs with
--frozen-lockfileand runspnpm audit --audit-level=highon every push and pull request (this covers dev dependencies — the runtime has none). - CI-gated releases. npm publishes happen only through the GitHub Actions release workflow, gated on the test suite and on the release tag matching
package.json's version, using a granular npm token scoped to this package. Once the repository is public, releases will also carry npm provenance so you can verify the published artifact was built from this repo (npm audit signatures).
What you can do as a consumer:
- Pin an exact version (or commit your lockfile) and review the small diff between releases —
dist/index.jsis readable output. - Run
npm audit signaturesin your own CI to verify registry signatures (and provenance attestations once available). - Remember the broader rule from the WebMCP security guidance: every script you ship can touch
document.modelContext, so the same supply-chain scrutiny applies to all third-party code on pages that register tools — not just this package.
Testing your tools
- Chrome DevTools → WebMCP panel: inspect registered tools, schemas, and a live invocation log; invoke tools manually or via built-in Gemini integration.
- Model Context Tool Inspector: Chrome extension for verifying exposed tools.
- webmcp-tools evals CLI: scripted LLM evals against your live page's toolset.
- Unit tests: this package's
tests/mock-model-context.tsshows how to mockdocument.modelContextin jsdom.
Live example
This package is used in production-style fashion by genie-demo — see its /test/webmcp page for an end-to-end playground (support detection, imperative tools, declarative form, invocation log).
Bundled docs for coding agents
This package ships a doc/ folder inside the published tarball (it lands at
node_modules/@cr4yfish/react-web-mcp/doc/). A coding agent working in a repo
that depends on react-web-mcp can read these directly — no web access needed:
doc/index.md— overview, quick start, and the invariants worth knowing.doc/api-reference.md— every export, with behavior notes.doc/webmcp-standard.md— the WebMCP standard itself (imperative + declarative APIs, security, best practices, browser support).doc/llms.txt— a one-read, machine-readable digest of the whole API surface.
Agent skill
Working on WebMCP with a coding agent (Claude Code, Cursor, Codex, …)? Install the web-mcp-skill — a framework-agnostic deep-dive WebMCP reference skill — via the skills.sh CLI:
npx skills add cr4yfish/web-mcp-skill
License
MIT
Установить React Web Mcp в Claude Desktop, Claude Code, Cursor
unyly install react-web-mcpСтавит в Claude Desktop, Claude Code, Cursor и VS Code — сам разбирается с npx, uvx и сборкой из исходников.
Впервые? Поставь CLI: curl -fsSL https://unyly.org/install | sh
Или настроить вручную
Выполни в терминале:
claude mcp add react-web-mcp -- npx -y @cr4yfish/react-web-mcpFAQ
React Web Mcp MCP бесплатный?
Да, React Web Mcp MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для React Web Mcp?
Нет, React Web Mcp работает без API-ключей и переменных окружения.
React Web Mcp — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить React Web Mcp в Claude Desktop, Claude Code или Cursor?
Открой React Web Mcp на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
Playwright
Browser automation, scraping, screenshots
автор: MicrosoftPuppeteer
Browser automation and web scraping.
автор: modelcontextprotocolopentabs-dev/opentabs
Plugin-based MCP server + Chrome extension that gives AI agents access to web applications through the user's authenticated browser session. 100+ plugins with a
автор: opentabs-devrobhunter/agentdeals
1,500+ developer infrastructure deals, free tiers, and startup programs across 54 categories. Search deals, compare vendors, plan stacks, and track pricing chan
автор: robhunterCompare React Web Mcp with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории browse
