@agent-mcp/react
agent-mcp-react
Make your React page an MCP server.
Expose a running React application as an MCP server, so an agent can drive it through typed tools instead of clicking its UI.
customers.set_filters({ health: ["at_risk"], arrMin: 1000000 })instead of opening filters, finding Health, clicking At risk, then setting a revenue floor by hand.

A person describes a dashboard; the agent composes it by calling the tools the page declared, and the panels it built are then driven by hand. Twenty seconds of the full three-minute recording. The left pane is the agent's real tool calls — nothing there is a transcript written for the video.
The application keeps owning its state. MCP is a second control interface onto the same application actions the human UI already calls. It is not browser automation, and not a second store.
This library is the browser half. The MCP server runs in the tab. The page asks your app for a URL, connects out to your gateway, and answers from there. You still supply the gateway, a ticket minter, and an agent runtime where the MCP client lives.
What the agent can do
Application tools you declare in React. They exist while that UI is on screen. Input is schema-checked before the handler runs. You must configure a validator.
Optional DOM tools (experimental): inspect the page, then click, fill, type, or scroll. Reading and acting are separate permissions. Off by default. These tools never enter the shared page registry.
Optional JavaScript evaluation in the page. Off by default. Privileged. Each call needs approval. This tool never enters the shared page registry.
Capability and confirmation checks apply to this library's connection. Other callers using the shared page registry skip those gates. Keep real authorization in your handlers.
Browser requirements
Requirement | Why |
A secure context | The tool registry is defined only in a secure context. |
A tool registry | One browser channel ships |
| Engine floor: Chrome 116, Safari 17.4, Firefox 124. |
The documented browser matrix is a support target, not fully verified. Three Playwright engines are not nine browser-and-version combinations. What has been run, and what has not: docs/browser-support.md.
Quickstart — using it in your app
npm install agent-mcp-reactESM only. React >=18 as a peer, Node >=22 for the toolchain, and a secure context in the browser
(localhost counts).
Wrap your app in the provider and declare one tool in the component that owns the state — both shown in full under Use it below. Then open the page and paste this into the browser console:
const mc = document.modelContext;
const tool = (await mc.getTools()).find((t) => t.name === 'counter.increment');
await mc.executeTool(tool, JSON.stringify({ by: 7 }));The number on the page changes. You needed no agent, no API key and no backend to see that — the tool is in the browser's own per-document registry, which is exactly where a browser-native agent looks. Connecting an external agent over a socket is a later step, and your backend owns it.
The same path, at a slower pace and with the reasons: docs/tutorial-first-tool.md — about fifteen minutes.
Working from a checkout instead? Pack it and depend on the tarball; a file: dependency on the
directory does not work, because publishConfig applies when a package is packed and never when a
directory is linked. Routes and failure modes:
docs/consuming-without-publishing.md.
pnpm pack # → agent-mcp-react-<version>.tgzUse it
Four props are required: connection, server, capabilities, and onUnexpectedState. This
snippet is compiled against the packed tarball from a project outside this repository.
import { AgentMcpProvider, CONFIRMATION } from 'agent-mcp-react';
import { createAjvValidator } from 'agent-mcp-react/validation';
<AgentMcpProvider
connection={{
getUrl: async () => {
// Called once per connection attempt. Opaque to the library: never parsed,
// never amended, never stored. Your backend mints a single-use credential.
const { url } = await fetch('/api/mcp-ticket', { method: 'POST' }).then((r) => r.json());
return url;
},
}}
server={{ name: 'my-app', version: '1.0.0' }}
// Every member is required. An omitted one is denied rather than defaulted.
capabilities={{
application: true,
dom: { inspect: false, interact: false },
evaluate: false,
}}
onUnexpectedState={(failure) => console.error('[mcp]', failure)}
validation={{ validator: createAjvValidator() }}
// Asked before a tool declared `confirmation: 'required'` runs. Without a resolver such a
// tool is refused at every call — absence is not consent.
confirmation={{
resolver: (request) =>
window.confirm(`Allow ${request.tool}?`) ? CONFIRMATION.approved : CONFIRMATION.refused,
}}
>
<App />
</AgentMcpProvider>Declare a tool in the component that owns the feature. It exists while that component is mounted, and the handler always reads current state:
import { useMcpTool } from 'agent-mcp-react';
useMcpTool({
name: 'account.set_health',
title: "Change the open account's health",
description: 'Set the relationship health of the account whose drawer is open.',
inputSchema: {
type: 'object',
properties: {
health: { type: 'string', enum: ['green', 'amber', 'red'] },
reason: { type: 'string', minLength: 1 },
},
required: ['health', 'reason'],
additionalProperties: false,
},
permissions: { confirmation: 'required' },
handler: (input) => {
dashboard.setHealth(account.id, input.health as Health, ACTOR.agent);
return { account: account.name, to: input.health };
},
});The schema is enforced in the runtime before the handler runs.
What you have to supply
A WebSocket gateway | Terminates the socket and relays MCP to your agent runtime |
A ticket minter | A backend endpoint issuing a single-use credential per connection attempt |
An agent runtime | Where the MCP client lives, because that is where the socket is held |
tools/mock-agent/ implements all three for local development and is not a production component.
The provider takes getUrl() and never a credential.
Things that will bite you
|
|
| A registry-integrity alarm has no response to travel back on. |
A schema with no validator | The declaration is refused with |
| Under a strict policy it throws at declaration, so the page exposes no tools at all and an agent sees an empty page rather than an error. |
A secure context is required |
|
A mutating handler must await | Otherwise it resolves before React commits, and a state read in the same turn returns the previous render. The agent is told the mutation succeeded and then reads a value that does not include it — which looks exactly like a tool that silently did nothing, so the agent retries. Commit and registration. |
| The agent waits for a person; any script, widget or extension on the page calls the same tool with no dialog. It is not authorization. Anything that must never happen without consent belongs inside the handler. Declaring a tool. |
| The provider adopts the registry asynchronously, in an effect. A sibling or child that reads the registry in its own mount effect finds nothing and, if it only looks once, stays empty forever. Reading the registry from the page. |
A page-script call refused for bad arguments is invisible to your observers | The registry validates against the declared schema and throws before this library sees the call, so nothing reaches |
Docs
If you want | Read |
To see it work in fifteen minutes | |
Every public export | |
To use it without publishing it | |
To declare a tool | |
To wire the socket | |
To let an agent read state | |
To bind Redux, Zustand, or a router | |
The design, condensed | |
What changed, and when | |
Everything else |
Quickstart — working on the library
git clone https://github.com/A-Launch/agent-mcp-react.git
cd agent-mcp-react
pnpm install # Node >=22; corepack enable gets the pinned pnpm
scripts/setup-git-hooks.sh # points core.hooksPath at .githooks/
pnpm gate # everything that must be green before a pull requestpnpm gate is the whole health check in one command and takes a few minutes. If it passes on a fresh
clone, your environment is right and anything that breaks later is yours.
To watch an agent drive a real page, run three servers in three terminals — the pages are demonstrators in this repository, and the agent is a mock that stands in for a real runtime:
pnpm dev:agent # mock agent runtime :45000
pnpm dev:example # customer dashboard :45010
pnpm dev:board # composable board :45030Then open http://localhost:45010, and drive it from
http://localhost:45020 (pnpm dev:chat) or by calling the mock agent's HTTP endpoints directly.
Ports, environment variables, capability profiles and the test layers:
docs/local-development.md.
Before your first pull request, read sections 2 and 3 of CONTRIBUTING.md — setup and the gate. The rest can wait until you need it.
Health gate
pnpm gateRequired after any change under src/, examples/ or tools/. What it runs, and in which order, is
in package.json — it builds the library and the examples, runs the unit, React, transport and
integration suites, typechecks, lints, and verifies the packed tarball and an external consumer.
Three of those ask a question the suites cannot. pnpm build asserts what is in dist/ rather
than what the compiler returned, because a build can exit zero having emitted nothing and exit non-zero
having emitted something unloadable. verify:package resolves every export the packed tarball
declares, from inside the tarball. verify:consumer installs that tarball into a project outside
this repository, typechecks it against the shipped types and bundles it with a real bundler — which
is the only check that asks whether an embedder can actually consume this package, since everything
in-repo resolves through workspace paths and a root tsconfig instead.
Three more run separately, because they need dev servers or browser binaries:
pnpm test:e2e # three engines, real browser, real socket
pnpm test:e2e:native # opt-in: Chromium with a real native tool registry
pnpm verify:consumer:runs # loads an external project's production bundle in a browserContributing
Issues and pull requests are welcome. CONTRIBUTING.md owns every convention here — where code goes, the five invariants that fail silently, what the gate requires of a change, and the patterns that are refused outright. Read sections 2 and 3 before your first pull request; they are the setup and the gate, and everything else can be read when you need it.
By participating you agree to the code of conduct.
Found a security problem? Do not open an issue. SECURITY.md says how to report it privately — a reachability hole in this library is reachable on every page that embeds it, from the moment the issue is readable.