{"id":"e8b4cfbd-8314-4a3e-8226-23ca056799a7","slug":"a-launch-agent-mcp-react","kind":"mcp","name":"agent-mcp-react","summary":"Expose a running React application as an MCP server — a second control interface onto one application, not browser automation.","owner":"A-Launch","repoUrl":"https://github.com/A-Launch/agent-mcp-react","license":"Apache-2.0","licenseTier":"permissive","trust":"community","stars":1,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-09-09T06:24:03.000Z","addedAt":"2026-10-14T11:09:40.715Z","logoUrl":"https://avatars.githubusercontent.com/u/273414321?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["browser-web","cloud-infra","memory-knowledge"],"tags":["agent","ai-agent","browser","llm","react","tool-calling","typescript","webmcp"],"runtime":"node","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-17T20:42:51.282Z","sample":false,"url":"https://conduits.dev/tools/a-launch-agent-mcp-react","language":"TypeScript","transports":["stdio"],"install":{"npm":"agent-mcp-react"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# `agent-mcp-react`\n\n[![npm](https://img.shields.io/npm/v/agent-mcp-react.svg)](https://www.npmjs.com/package/agent-mcp-react)\n[![gate](https://github.com/A-Launch/agent-mcp-react/actions/workflows/gate.yml/badge.svg?branch=main)](https://github.com/A-Launch/agent-mcp-react/actions/workflows/gate.yml)\n[![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://github.com/A-Launch/agent-mcp-react/blob/main/LICENSE)\n[![node](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://github.com/A-Launch/agent-mcp-react/blob/main/.nvmrc)\n[![react](https://img.shields.io/badge/react-%3E%3D18-61dafb.svg)](https://react.dev)\n\n**Make your React page an MCP server.**\n\nExpose a running React application as an MCP server, so an agent can drive it through typed tools\ninstead of clicking its UI.\n\n```text\ncustomers.set_filters({ health: [\"at_risk\"], arrMin: 1000000 })\n```\n\ninstead of opening filters, finding Health, clicking At risk, then setting a revenue floor by hand.\n\n[![An agent composing a dashboard by calling the tools the page declared](https://raw.githubusercontent.com/A-Launch/agent-mcp-react/main/docs/media/composable-board.gif)](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/media/composable-board.mp4)\n\n*A person describes a dashboard; the agent composes it by calling the tools the page declared, and the\npanels it built are then driven by hand. Twenty seconds of the\n[full three-minute recording](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/media/composable-board.mp4). The left pane is the agent's real tool\ncalls — nothing there is a transcript written for the video.*\n\nThe application keeps owning its state. MCP is a **second control interface** onto the same\napplication actions the human UI already calls. It is not browser automation, and not a second store.\n\nThis library is the browser half. The MCP server runs in the tab. The page asks your app for a URL,\nconnects out to your gateway, and answers from there. You still supply the gateway, a ticket minter,\nand an agent runtime where the MCP client lives.\n\n## What the agent can do\n\n- **Application tools you declare in React.** They exist while that UI is on screen. Input is\n  schema-checked before the handler runs. You must configure a validator.\n- **Optional DOM tools (experimental):** inspect the page, then click, fill, type, or scroll. Reading\n  and acting are separate permissions. Off by default. These tools never enter the shared page\n  registry.\n- **Optional JavaScript evaluation** in the page. Off by default. Privileged. Each call needs\n  approval. This tool never enters the shared page registry.\n\nCapability and confirmation checks apply to this library's connection. Other callers using the\nshared page registry skip those gates. Keep real authorization in your handlers.\n\n## Browser requirements\n\n| Requirement | Why |\n|---|---|\n| **A secure context** | The tool registry is defined only in a secure context. `localhost` counts. |\n| **A tool registry** | One browser channel ships `document.modelContext` natively, behind a flag. Everywhere else the library installs a published portability shim. Application source is identical either way. |\n| `WebSocket`, `crypto.randomUUID`, `AbortController`, `AbortSignal.any` | Engine floor: Chrome 116, Safari 17.4, Firefox 124. |\n\nThe documented browser matrix is a support target, not fully verified. Three Playwright engines are\nnot nine browser-and-version combinations. What has been run, and what has not:\n[docs/browser-support.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/browser-support.md).\n\n## Quickstart — using it in your app\n\n```bash\nnpm install agent-mcp-react\n```\n\nESM only. React `>=18` as a peer, Node `>=22` for the toolchain, and a secure context in the browser\n(`localhost` counts).\n\nWrap your app in the provider and declare one tool in the component that owns the state — both shown\nin full under [Use it](#use-it) below. Then open the page and paste this into the browser console:\n\n```js\nconst mc = document.modelContext;\nconst tool = (await mc.getTools()).find((t) => t.name === 'counter.increment');\nawait mc.executeTool(tool, JSON.stringify({ by: 7 }));\n```\n\nThe number on the page changes. **You needed no agent, no API key and no backend to see that** — the\ntool is in the browser's own per-document registry, which is exactly where a browser-native agent\nlooks. Connecting an external agent over a socket is a later step, and your backend owns it.\n\nThe same path, at a slower pace and with the reasons:\n**[docs/tutorial-first-tool.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/tutorial-first-tool.md)** — about fifteen minutes.\n\n**Working from a checkout instead?** Pack it and depend on the tarball; a `file:` dependency on the\n*directory* does not work, because `publishConfig` applies when a package is packed and never when a\ndirectory is linked. Routes and failure modes:\n[docs/consuming-without-publishing.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/consuming-without-publishing.md).\n\n```bash\npnpm pack        # → agent-mcp-react-<version>.tgz\n```\n\n## Use it\n\nFour props are required: `connection`, `server`, `capabilities`, and `onUnexpectedState`. This\nsnippet is compiled against the packed tarball from a project outside this repository.\n\n```tsx\nimport { AgentMcpProvider, CONFIRMATION } from 'agent-mcp-react';\nimport { createAjvValidator } from 'agent-mcp-react/validation';\n\n<AgentMcpProvider\n  connection={{\n    getUrl: async () => {\n      // Called once per connection attempt. Opaque to the library: never parsed,\n      // never amended, never stored. Your backend mints a single-use credential.\n      const { url } = await fetch('/api/mcp-ticket', { method: 'POST' }).then((r) => r.json());\n      return url;\n    },\n  }}\n  server={{ name: 'my-app', version: '1.0.0' }}\n  // Every member is required. An omitted one is denied rather than defaulted.\n  capabilities={{\n    application: true,\n    dom: { inspect: false, interact: false },\n    evaluate: false,\n  }}\n  onUnexpectedState={(failure) => console.error('[mcp]', failure)}\n  validation={{ validator: createAjvValidator() }}\n  // Asked before a tool declared `confirmation: 'required'` runs. Without a resolver such a\n  // tool is refused at every call — absence is not consent.\n  confirmation={{\n    resolver: (request) =>\n      window.confirm(`Allow ${request.tool}?`) ? CONFIRMATION.approved : CONFIRMATION.refused,\n  }}\n>\n  <App />\n</AgentMcpProvider>\n```\n\nDeclare a tool in the component that owns the feature. It exists while that component is mounted,\nand the handler always reads current state:\n\n```tsx\nimport { useMcpTool } from 'agent-mcp-react';\n\nuseMcpTool({\n  name: 'account.set_health',\n  title: \"Change the open account's health\",\n  description: 'Set the relationship health of the account whose drawer is open.',\n  inputSchema: {\n    type: 'object',\n    properties: {\n      health: { type: 'string', enum: ['green', 'amber', 'red'] },\n      reason: { type: 'string', minLength: 1 },\n    },\n    required: ['health', 'reason'],\n    additionalProperties: false,\n  },\n  permissions: { confirmation: 'required' },\n  handler: (input) => {\n    dashboard.setHealth(account.id, input.health as Health, ACTOR.agent);\n    return { account: account.name, to: input.health };\n  },\n});\n```\n\nThe schema is enforced in the runtime before the handler runs.\n\n## What you have to supply\n\n| | |\n|---|---|\n| A **WebSocket gateway** | Terminates the socket and relays MCP to your agent runtime |\n| A **ticket minter** | A backend endpoint issuing a single-use credential per connection attempt |\n| An **agent runtime** | Where the MCP client lives, because that is where the socket is held |\n\n`tools/mock-agent/` implements all three for local development and is not a production component.\nThe provider takes `getUrl()` and never a credential.\n\n## Things that will bite you\n\n| | |\n|---|---|\n| `capabilities` is **required**, and so is **every member of it** | `{ application: true }` does not compile. Spell `dom` and `evaluate` too. |\n| `onUnexpectedState` is **required** | A registry-integrity alarm has no response to travel back on. |\n| A schema with **no validator** | The declaration is refused with `MCP_TOOL_VALIDATOR_MISSING` and the tool is never exposed — but your page keeps rendering. It reaches `onRegistration`, and the development console names the import to add. Other tools on the page are unaffected. [Declaring a tool](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/declaring-a-tool.md). |\n| `createAjvValidator` needs CSP `unsafe-eval` | 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. `localhost` has no policy, so a local demo will not show you this — check it before deploying. [The defect record](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/issues/validator-requires-unsafe-eval.md). |\n| A **secure context** is required | `localhost` counts. |\n| A mutating handler must **await `context.afterRender()`** | 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](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/explanation-commit-and-registration.md). |\n| `confirmation: 'required'` gates **this library's bridge, not the action** | 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](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/declaring-a-tool.md). |\n| `document.modelContext` is **not there when your component mounts** | 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](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/observing-tool-calls.md#reading-the-registry-from-your-own-code). |\n| 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 `onToolError` or the call log. Silence there does not mean nobody called. [Observing tool calls](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/observing-tool-calls.md). |\n\n## Docs\n\n| If you want | Read |\n|-------------|------|\n| **To see it work in fifteen minutes** | **[docs/tutorial-first-tool.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/tutorial-first-tool.md)** |\n| Every public export | [docs/reference-api.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/reference-api.md) |\n| To use it without publishing it | [docs/consuming-without-publishing.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/consuming-without-publishing.md) |\n| To declare a tool | [docs/declaring-a-tool.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/declaring-a-tool.md) |\n| To wire the socket | [docs/connecting-to-an-agent.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/connecting-to-an-agent.md) |\n| To let an agent read state | [docs/exposing-state.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/exposing-state.md) |\n| To bind Redux, Zustand, or a router | [docs/store-adapters.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/store-adapters.md) |\n| The design, condensed | [docs/design.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/design.md) |\n| What changed, and when | [CHANGELOG.md](https://github.com/A-Launch/agent-mcp-react/blob/main/CHANGELOG.md) |\n| Everything else | [docs/README.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/README.md) |\n\n## Quickstart — working on the library\n\n```bash\ngit clone https://github.com/A-Launch/agent-mcp-react.git\ncd agent-mcp-react\npnpm install                   # Node >=22; corepack enable gets the pinned pnpm\nscripts/setup-git-hooks.sh     # points core.hooksPath at .githooks/\npnpm gate                      # everything that must be green before a pull request\n```\n\n`pnpm gate` is the whole health check in one command and takes a few minutes. If it passes on a fresh\nclone, your environment is right and anything that breaks later is yours.\n\nTo watch an agent drive a real page, run three servers in three terminals — the pages are demonstrators\nin this repository, and the agent is a mock that stands in for a real runtime:\n\n```bash\npnpm dev:agent      # mock agent runtime  :45000\npnpm dev:example    # customer dashboard  :45010\npnpm dev:board      # composable board    :45030\n```\n\nThen open <http://localhost:45010>, and drive it from\n<http://localhost:45020> (`pnpm dev:chat`) or by calling the mock agent's HTTP endpoints directly.\nPorts, environment variables, capability profiles and the test layers:\n[docs/local-development.md](https://github.com/A-Launch/agent-mcp-react/blob/main/docs/local-development.md).\n\n**Before your first pull request**, read sections 2 and 3 of\n[CONTRIBUTING.md](https://github.com/A-Launch/agent-mcp-react/blob/main/CONTRIBUTING.md) — setup and the gate. The rest can wait until you need it.\n\n## Health gate\n\n```bash\npnpm gate\n```\n\nRequired after any change under `src/`, `examples/` or `tools/`. What it runs, and in which order, is\nin `package.json` — it builds the library and the examples, runs the unit, React, transport and\nintegration suites, typechecks, lints, and verifies the packed tarball and an external consumer.\n\nThree of those ask a question the suites cannot. `pnpm build` asserts what is **in** `dist/` rather\nthan what the compiler returned, because a build can exit zero having emitted nothing and exit non-zero\nhaving emitted something unloadable. `verify:package` resolves every export the packed tarball\ndeclares, from inside the tarball. `verify:consumer` installs that tarball into a project **outside\nthis repository**, typechecks it against the shipped types and bundles it with a real bundler — which\nis the only check that asks whether an embedder can actually consume this package, since everything\nin-repo resolves through workspace paths and a root tsconfig instead.\n\nThree more run separately, because they need dev servers or browser binaries:\n\n```bash\npnpm test:e2e               # three engines, real browser, real socket\npnpm test:e2e:native        # opt-in: Chromium with a real native tool registry\npnpm verify:consumer:runs   # loads an external project's production bundle in a browser\n```\n\n## Contributing\n\nIssues and pull requests are welcome. [CONTRIBUTING.md](https://github.com/A-Launch/agent-mcp-react/blob/main/CONTRIBUTING.md) owns every convention here —\nwhere code goes, the five invariants that fail silently, what the gate requires of a change, and the\npatterns that are refused outright. Read sections 2 and 3 before your first pull request; they are the\nsetup and the gate, and everything else can be read when you need it.\n\nBy participating you agree to the [code of conduct](https://github.com/A-Launch/agent-mcp-react/blob/main/CODE_OF_CONDUCT.md).\n\n**Found a security problem?** Do not open an issue. [SECURITY.md](https://github.com/A-Launch/agent-mcp-react/blob/main/SECURITY.md) says how to report it\nprivately — a reachability hole in this library is reachable on every page that embeds it, from the\nmoment the issue is readable.\n\n## License\n\nApache-2.0. See [LICENSE](https://github.com/A-Launch/agent-mcp-react/blob/main/LICENSE) and [NOTICE](https://github.com/A-Launch/agent-mcp-react/blob/main/NOTICE).\n"}