# @frontmcp/react

> @frontmcp/react runs a FrontMCP server in the same process as a React app and gives components hooks to call it. FrontMcpProvider, the client it connects, several servers, stores, and building it with Vite.

Source: https://frontmcp.dev/reference/react

`@frontmcp/react` puts a FrontMCP server inside a React app. You build the server with [`create()`](https://frontmcp.dev/reference/sdk/create), in the same process, and `FrontMcpProvider` connects a client to it. Components then call its tools, read its resources and get its prompts with [hooks](https://frontmcp.dev/reference/react/hooks), render forms and results with [components](https://frontmcp.dev/reference/react/components), and add tools and resources of their own that exist only while they're mounted. A component's tool is a tool of the server, so everything a model can call is in one place: an agent loop in the app that calls through the provider's server sees the server's tools and the components' tools together ([Handing the tools to an AI SDK](https://frontmcp.dev/reference/react/hooks#handing-the-tools-to-an-ai-sdk)).

```tsx
const server = await create({ info: { name, version }, tools?, resources?, prompts?, ... });

<FrontMcpProvider server={server} name? servers? components? stores? dynamicToolApps? autoConnect? onConnected? onError?>
  {children}
</FrontMcpProvider>
```

---

## Reference

### Installing

```bash
npm install @frontmcp/react react react-dom
```

`@frontmcp/sdk` and `@frontmcp/utils` come with it, at the same version. You don't need to import `reflect-metadata`.

| Entry point | What's in it |
| --- | --- |
| `@frontmcp/react` | `FrontMcpProvider`, the [hooks](https://frontmcp.dev/reference/react/hooks), the [components](https://frontmcp.dev/reference/react/components), `serverRegistry`, [`DynamicRegistry` and `bindDynamicTools()`](#binddynamictools), the store adapters, and from `@frontmcp/sdk`: `create`, `clearCreateCache`, `connect` and its variants, the decorators (`Tool`, `Resource`, `ResourceTemplate`, `Prompt`, `App`, `Plugin`, `Adapter`, `FrontMcp`), their function forms (`tool()`, `resource()`, …), the context classes and `z`. |
| `@frontmcp/react/router` | [The router bridge](https://frontmcp.dev/reference/react/components#router-tools). Needs `react-router-dom` 7. |
| `@frontmcp/react/state` | Store hooks, which do in a component what [`stores`](#exposing-app-state-with-stores) do ([Hooks](https://frontmcp.dev/reference/react/hooks#hooks-in-frontmcpreactstate)). |
| `@frontmcp/react/api` | `useApiClient`, `parseOpenApiSpec` and `createFetchClient` ([Hooks](https://frontmcp.dev/reference/react/hooks#useapiclient)). |
| `@frontmcp/react/ai` | `useAITools`, `useTools` and `createToolCallHandler` ([Hooks](https://frontmcp.dev/reference/react/hooks#usetools-and-useaitools)). |

### Running in a browser

`@frontmcp/sdk` has a build for browsers, which a bundler that follows the `browser` export condition picks, as Vite does. A Vite 8 app (8.3, with `@vitejs/plugin-react` 6) needs only React's plugin:

```ts vite.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({ plugins: [react()] });
```

`vite build` finishes with no warning about FrontMCP's packages, and the app runs. The smallest app's bundle is about 2.9 MB, 780 KB gzipped. It also ran from Vite 8's dev server, and built and ran with Vite 7.3. This is how the pages in this section were run in Chromium; the rest of their examples ran in Node ([Testing components in Node](#testing-components-in-node)).

Changed in 1.9.3: `@frontmcp/utils` 1.9.2 loaded `@upstash/redis`, for a storage adapter a page doesn't use, without declaring it, so `vite build` stopped with `Rolldown failed to resolve import "@upstash/redis" from ".../@frontmcp/utils/esm/index.mjs"` and the dev server's page stayed blank, until the package was aliased to an empty module in `vite.config.ts`. The build also warned that `node:crypto` had been externalized for browser compatibility. That alias can go; a config that keeps it still builds and runs.

Changed in 1.9: the bundler had to supply what the SDK took from Node: `vite-plugin-node-polyfills` for `process`, `global`, `Buffer` and the Node modules, and an empty module in place of `express`. Without them the page failed with `process is not defined` before rendering anything, and a Vite 7 build stopped at `"PassThrough" is not exported by "__vite-browser-external"`. A config that still has both builds and runs on 1.9.3, with a bundle of about 2.9 MB.

Changed in 1.8.7: a bundle used to fail as soon as it loaded, with `(0 , A.createRequire) is not a function`, because the SDK's ES module build imported Node's `module` at the top. It now looks for `process.getBuiltinModule("module")` and goes without it when there's none.

### `FrontMcpProvider`

Wrap the part of the app that uses the server. [See more examples below.](#usage)

```tsx App.tsx
import { create, FrontMcpProvider } from "@frontmcp/react";
import { GreetTool } from "./greet.tool";
import { Greeter } from "./Greeter";

const server = await create({ info: { name: "my-app", version: "1.0.0" }, tools: [GreetTool] });

export function App() {
  return (
    <FrontMcpProvider server={server}>
      <Greeter />
    </FrontMcpProvider>
  );
}
```

#### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `server` | `DirectMcpServer` | | **Required.** From `create()` or [`FrontMcpInstance.createDirect()`](https://frontmcp.dev/reference/sdk/create#frontmcpinstancecreatedirectconfig). |
| `children` | `ReactNode` | | **Required.** |
| `name` | `string` | `"default"` | The server's name in `serverRegistry`, which hooks without a `server` option use. |
| `servers` | `Record<string, DirectMcpServer>` | | More servers, by name. Hooks reach them with `{ server: "name" }`. Each connects after the main one. |
| `components` | `Record<string, ComponentType>` | | Components for [`DynamicRenderer`](https://frontmcp.dev/reference/react/components#dynamicrenderer-and-componentregistry), by URI, like `{ "component://Card": Card }`. |
| `stores` | `StoreAdapter[]` | | App state to expose as resources and tools. See [Exposing app state](#exposing-app-state-with-stores). |
| `dynamicToolApps` | `Record<string, string>` | | The app that components' tools join, by server name, like `{ default: "billing" }`. Only for a server with several apps; a `create()` server has one. See [`useDynamicTool()`](https://frontmcp.dev/reference/react/hooks#usedynamictool). |
| `autoConnect` | `boolean` | `true` | Connect on mount. With `false`, call `connect()` from [`useFrontMcp()`](https://frontmcp.dev/reference/react/hooks#usefrontmcp). |
| `onConnected` | `(client: DirectClient) => void` | | Called once the client is connected and the lists are loaded. |
| `onError` | `(error: Error) => void` | | Called when connecting fails, and when a server refuses a tool a component registers. Without it, a refused tool is a `console.warn`. |

#### What it does

1. On mount, it registers the server under `name` in `serverRegistry`, and each of `servers` under its key, wrapped so that components can [add resources](https://frontmcp.dev/reference/react/hooks#usedynamicresource). The status is `"idle"`.
2. With `autoConnect`, it sets `"connecting"`, calls the wrapped server's `connect()`, and lists its tools, resources, resource templates and prompts. The status is then `"connected"`, or `"error"` with `error` set if connecting failed.
3. It registers each tool a component adds with that component's server, through [`server.registerTool()`](https://frontmcp.dev/reference/sdk/register-tool), and removes it when the last component that added it unmounts. The server tells the client its tools changed, and the provider lists them again, as it does for a tool registered any other way. When a component adds or removes a resource, it lists the resources again.
4. On unmount, it removes its servers from `serverRegistry`, and its components' tools from the servers.

#### The client

The provider's client is a [`DirectClient`](https://frontmcp.dev/reference/sdk/connect#the-directclient) named `mcp-client`, so tools see `this.clientInfo.name` as `"mcp-client"` and `this.platform` as `"generic-mcp"`. Calls go through every check a client call has. What the hooks return is the MCP result as the client got it: [`useCallTool()`](https://frontmcp.dev/reference/react/hooks#usecalltool) gives you `{ content, structuredContent }`, or `{ content, isError: true, _meta }` for a tool that failed.

A tool a component adds is a tool of the server you passed in. `server.callTool("add_to_cart")` reaches it, its calls run through the server's flow, with the app's plugin hooks, authorities and `availableWhen`, and every client of the server sees it, an in-page agent through [`@frontmcp/plugin-webmcp`](https://frontmcp.dev/reference/plugins/webmcp) included. A resource a component adds lives in the provider's wrapper: the client and the hooks see it, and `server.readResource()` on the server you passed in answers `Resource not found: app://cart`.

Changed in 1.9: components' tools lived in the wrapper too. They skipped the server's flow, so no hook or authority saw their calls, `server.callTool()` answered `Tool "add_to_cart" not found`, and a component's tool named like a server tool replaced it for the provider's client. Such a tool is now refused ([`useDynamicTool()`](https://frontmcp.dev/reference/react/hooks#usedynamictool)).

### `serverRegistry`

The module's `ServerRegistry`, shared by every provider in the app:

| Method | Description |
| --- | --- |
| `list()` | The registered names. |
| `get(name)`, `has(name)` | A `ServerEntry`: `{ server, client, status, error, tools, resources, resourceTemplates, prompts }`. |
| `connect(name)`, `connectAll()` | Connect a registered server that isn't connected yet. |
| `subscribe(listener)` | Called on every change. Returns an unsubscribe function. |
| `register(name, server)`, `unregister(name)`, `update(name, partial)`, `clear()` | What providers use. |

### `bindDynamicTools()`

The provider keeps its components' tools in a `DynamicRegistry`, and `bindDynamicTools()` makes each of them a tool of the server. Both are exported, for tools registered outside a provider:

```ts
import { bindDynamicTools, create, DynamicRegistry } from "@frontmcp/react";

const server = await create({ info: { name: "my-app", version: "1.0.0" }, tools: [] });
const registry = new DynamicRegistry();
const stop = bindDynamicTools(registry, server, { onError: (error, toolName) => console.warn(toolName, error.message) });

const remove = registry.registerTool({
  name: "hello",
  description: "Say hello",
  inputSchema: { type: "object", properties: {} },
  execute: async () => ({ content: [{ type: "text", text: "hi" }] }),
});
```

A moment later `server.listTools()` lists `hello`, and `server.callTool("hello")` returns `hi`: the tool is registered with [`server.registerTool()`](https://frontmcp.dev/reference/sdk/register-tool), and its calls run through the server's flow. `remove()` takes it away again, and `stop()` stops following the registry and removes every tool it added. A tool the server refuses goes to `onError`, like `A tool named "greet" is already registered`; without `onError` it's a `console.warn`. The other option, `app`, names the app tools join on a server with several, as the provider's `dynamicToolApps` does.

#### Caveats

- A server refuses a component's tool when it already has a tool by that name, or the name is longer than 64 characters. The provider passes `onError` an error like `Dynamic tool "greet" was not registered: A tool named "greet" is already registered`, and the component keeps rendering without its tool.
- On a server with several apps, a component's tool has to say which app it joins, with its own `app` option or the provider's `dynamicToolApps`. Otherwise it's refused with `runtime tool "…" must name its app (one of: billing, support)`.
- A component's resources stay in the provider's wrapper, so only the provider's client sees them.

Changed in 1.9: hooks given a new object on every render registered their tools again on every render, and each registration made the provider list the server again, so under a component that read the server's state the page froze: [`useStoreResource()`](https://frontmcp.dev/reference/react/hooks#hooks-in-frontmcpreactstate) and `useValtioResource()` from `/state` with options written in the component, [`useApiClient()`](https://frontmcp.dev/reference/react/hooks#useapiclient) with `operations` written there, and `useReduxResource()` always. They register again only when a name changes, and the provider ignores a listing that hasn't changed.

Changed in 1.8.7: `@frontmcp/react/state`, `/api` and `/ai` each carried their own copy of the provider's context, so their hooks never saw a `FrontMcpProvider` from `@frontmcp/react`. `useStoreResource()` from `/state` and `useApiClient()` registered nothing, and `useAITools()` stayed `{ tools: null, loading: true }`. Inline `useDynamicTool()` schemas, `AgentSearch` and `AgentContent` froze the page.

---

## Usage

### Getting started

A tool, the server, and a component that calls it. `useCallTool()` returns the call function, the call's state, and a reset function; `data` is the tool's result, so the message is in `data.structuredContent`.

```ts greet.tool.ts
import { Tool, ToolContext } from "@frontmcp/react";
import { z } from "@frontmcp/sdk";

@Tool({ name: "greet", description: "Greet someone by name", inputSchema: { name: z.string() } })
export class GreetTool extends ToolContext {
  async execute({ name }: { name: string }) {
    return { message: `Hello, ${name}!` };
  }
}
```

```tsx Greeter.tsx
import { useCallTool, useFrontMcp } from "@frontmcp/react";

export function Greeter() {
  const { status } = useFrontMcp();
  const [greet, { data, loading, error }] = useCallTool<{ name: string }, { structuredContent?: { message: string } }>("greet");

  if (status !== "connected") return <p>{status}</p>;
  return (
    <div>
      <button disabled={loading} onClick={() => greet({ name: "Ada" })}>Greet</button>
      {data?.structuredContent && <p>{data.structuredContent.message}</p>}
      {error && <p role="alert">{error.message}</p>}
    </div>
  );
}
```

With the `App` above, clicking the button shows `Hello, Ada!`. `error` is set only when the call couldn't be made, like `FrontMCP not connected`: a tool that fails returns a result with `isError: true` in `data` ([useCallTool](https://frontmcp.dev/reference/react/hooks#usecalltool)).

### Connecting later

```tsx Connect.tsx
import { useFrontMcp } from "@frontmcp/react";

export function Connect() {
  const { status, error, connect } = useFrontMcp();
  if (status === "idle") return <button onClick={connect}>Connect</button>;
  if (status === "error") return <p role="alert">{error?.message}</p>;
  return <p>{status}</p>;
}

// <FrontMcpProvider server={server} autoConnect={false} onError={(e) => report(e)}>
//   <Connect />
// </FrontMcpProvider>
```

Before `connect()`, the status is `"idle"`, and a hook's call returns `null` with `error` `FrontMCP not connected`. A server whose `connect()` rejects ends in `"error"`, and `onError` gets the error.

### Using several servers

```tsx App.tsx
import { create, FrontMcpProvider, useCallTool, useListTools } from "@frontmcp/react";
import { SearchTool, TrackTool } from "./tools";

const main = await create({ info: { name: "main", version: "1.0.0" }, tools: [SearchTool] });
const analytics = await create({ info: { name: "analytics", version: "1.0.0" }, tools: [TrackTool] });

export function App() {
  return (
    <FrontMcpProvider server={main} servers={{ analytics }}>
      <Dashboard />
    </FrontMcpProvider>
  );
}

function Dashboard() {
  const [track] = useCallTool("track", { server: "analytics" }); // on analytics
  const tools = useListTools();                                   // main's tools only
  const analyticsTools = useListTools({ server: "analytics" });
  return <button onClick={() => track({ event: "click" })}>{tools.length} + {analyticsTools.length} tools</button>;
}
```

Without `server`, a hook uses the provider's own server: calling `track` there returns `isError: true` with `Tool "track" not found`.

### Exposing app state with `stores`

Each adapter in `stores` becomes a resource with the whole state, a resource per selector, and a tool per action. The tools are the server's, like a component's; the resources stay in the provider's wrapper:

| Adapter | Resource | Selector resources | Action tools |
| --- | --- | --- | --- |
| `createStore({ name, getState, subscribe, selectors?, actions? })` | `state://<name>` | `state://<name>/<selector>` | `<name>_<action>` |
| `reduxStore({ store, name = "redux", selectors?, actions? })` | `state://redux` | the same | the same, and each action creator's result is dispatched |
| `valtioStore({ proxy, subscribe, name = "valtio", paths?, mutations? })` | `state://valtio`, a JSON copy of the proxy | one per `paths` entry, a dot path like `"user.name"` | one per `mutations` entry |

Names and selector keys may use letters, digits, `_` and `-`; any other name throws ([Troubleshooting](#usestoreregistration-invalid-store-name)). Pass `valtio/utils`' `subscribe` to `valtioStore` yourself.

```tsx App.tsx
import { create, createStore, FrontMcpProvider, reduxStore } from "@frontmcp/react";

let counter = { value: 1 };
const listeners = new Set<() => void>();

const counterStore = createStore({
  name: "counter",
  getState: () => counter,
  subscribe: (cb) => {
    listeners.add(cb);
    return () => { listeners.delete(cb); };
  },
  selectors: { value: (s) => (s as typeof counter).value },
  actions: {
    increment: (by) => {
      counter = { value: counter.value + (typeof by === "number" ? by : 1) };
      listeners.forEach((l) => l());
      return counter.value;
    },
  },
});

const stores = [counterStore, reduxStore({ store: todoStore, actions: { addTodo: (text) => ({ type: "todos/add", text }) } })];

export const App = () => (
  <FrontMcpProvider server={server} stores={stores}>
    <Dashboard />
  </FrontMcpProvider>
);
```

This lists the tools `counter_increment` and `redux_addTodo`, and the resources `state://counter`, `state://counter/value` and `state://redux`. Reading `state://counter/value` returns `1` as JSON.

An action tool's input is `{ args?: unknown[] }`. `{ args: [5] }` calls `increment(5)`, and the result is `{"success":true,"result":6}`. Without an `args` array, the action gets the whole input object: `{ by: 2 }` calls `increment({ by: 2 })`. A Redux action's result is what `dispatch()` returned, the action itself.

Keep the `stores` array outside the component, as above: a new array on every render registers everything again.

The store's resources tell subscribers when the state changes, so [`useStoreResource("state://counter")`](https://frontmcp.dev/reference/react/hooks#usestoreresource) shows the new value as soon as an action has run, without `refetch()`. Changed in 1.8.7: a store's resource never sent that notification, and the hook kept its first value until `refetch()`.

### Testing components in Node

Components run in Node as well, which is where most examples on these pages were checked: jsdom for the DOM, React's `act()` to let effects and calls settle.

```bash
npm install --save-dev jsdom @types/jsdom tsx
```

```tsx greeter.test.tsx
import { test } from "node:test";
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";

const { window } = new JSDOM("<!doctype html><body></body>", { url: "http://localhost/" });
Object.assign(globalThis, { window, document: window.document, HTMLElement: window.HTMLElement, Event: window.Event, IS_REACT_ACT_ENVIRONMENT: true });

test("greets", async () => {
  const { act } = await import("react");
  const { createRoot } = await import("react-dom/client");
  const { create, FrontMcpProvider } = await import("@frontmcp/react");
  const { GreetTool } = await import("./greet.tool");
  const { Greeter } = await import("./Greeter");

  const server = await create({ info: { name: "test", version: "1.0.0" }, tools: [GreetTool] });
  const container = document.body.appendChild(document.createElement("div"));
  await act(async () => createRoot(container).render(<FrontMcpProvider server={server}><Greeter /></FrontMcpProvider>));
  await act(async () => container.querySelector("button")!.click());

  assert.match(container.textContent!, /Hello, Ada!/);
  await server.dispose();
});
```

Run it with `npx tsx --test greeter.test.tsx`, with `"jsx": "react-jsx"` and `"experimentalDecorators": true` in `tsconfig.json`.

---

## Troubleshooting

### `Module "node:crypto" has been externalized for browser compatibility`

Vite warned this once, for `@frontmcp/utils` 1.9.2, which imported `node:crypto` at the top of its ES module build. The build finished and the app ran. `@frontmcp/utils` 1.9.3 doesn't import it there ([Running in a browser](#running-in-a-browser)).

### `Rolldown failed to resolve import "@upstash/redis"`

Or `Rollup failed to resolve import` with Vite 7, or a blank page and `Failed to resolve import "@upstash/redis"` from the dev server. `@frontmcp/utils` 1.9.2 imports that package for its Upstash storage adapter without declaring it. Update `@frontmcp/react` to 1.9.3, where it's an optional peer that the build leaves out ([Running in a browser](#running-in-a-browser)).

### `process is not defined`, or `"PassThrough" is not exported by "__vite-browser-external"`

A browser bundle of FrontMCP 1.8 or earlier failed this way, without Node's modules supplied by a plugin. Since 1.9 the SDK has a browser build, and 1.9.3 needs only React's plugin ([Running in a browser](#running-in-a-browser)).

### `FrontMCP not connected`

A hook's call ran before the provider connected: with `autoConnect={false}` before `connect()`, or while the status is `"connecting"`. Check `status` from `useFrontMcp()`, or disable the control until it's `"connected"`.

### `Dynamic tool "…" was not registered: …`

The server refused a tool a component registers, and the provider passed the error to `onError`, or to the console: the name is taken or too long, or the server has several apps and the tool didn't say which it joins ([Caveats](#caveats), [`useDynamicTool()`](https://frontmcp.dev/reference/react/hooks#dynamic-tool--was-not-registered-)).

### `useStoreRegistration: invalid store name`

The full message is `useStoreRegistration: invalid store name "my store". Names must match /^[a-zA-Z0-9_-]+$/.`, thrown while the provider mounts. Store names and selector keys become URIs and tool names, so use letters, digits, `_` and `-`.

### `useServer()` returns `undefined`

No server is registered under that name. `useServer("main")` finds a server only when a provider with `name="main"`, or an entry of a provider's `servers` called `main`, has mounted. Without an argument it reads the name of the provider the hook sits under, which is `"default"` unless you set `name`.
