# React hooks

> Every hook in @frontmcp/react: calling tools, reading resources and prompts, listing what the server has, live resources, registering tools and resources from components, and the hooks in /state, /api and /ai.

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

The hooks of `@frontmcp/react` talk to the server a [`FrontMcpProvider`](https://frontmcp.dev/reference/react) connected. Three kinds: hooks that call it (`useCallTool`, `useReadResource`, `useGetPrompt`), hooks that read what it has (`useFrontMcp`, `useListTools`, `useListResources`, `useListPrompts`, `useStoreResource`), and hooks that add to it from a component, for as long as the component is mounted (`useDynamicTool`, `useDynamicResource`, `useComponentTree`). Each takes `{ server: "name" }` to use one of the provider's other `servers`.

```tsx
const [call, { data, loading, error, called }, reset] = useCallTool(name, { server?, onSuccess?, onError?, resetOnToolChange? });
const { data, loading, error, refetch } = useReadResource(uri, { server? });
const [read, { data, loading, error }] = useReadResource({ server? });
const [getPrompt, { data, loading, error }] = useGetPrompt(name, { server? });

useDynamicTool({ name, description, schema | inputSchema, execute, annotations?, availableWhen?, app?, enabled?, server? });
useDynamicResource({ uri, name, description?, mimeType?, read, enabled?, server? });
```

---

## Reference

Everything here ran in Node with FrontMCP 1.9.2, with React 19 rendering into jsdom, and the hooks of the provider ran in Chromium too, in a Vite 8 build ([Running in a browser](https://frontmcp.dev/reference/react#running-in-a-browser)). `@frontmcp/react` 1.9.3 is the same code, and the provider with `useFrontMcp()` and `useCallTool()` ran again with it, in jsdom and in a Vite 8 build.

### `useFrontMcp()`

`useFrontMcp(name?)` returns the state of the provider's server, or of the one named:

| Field | Description |
| --- | --- |
| `name` | The server's name. |
| `status` | `"idle"`, `"connecting"`, `"connected"` or `"error"`. |
| `error` | The connection error, or `null`. |
| `server`, `client` | The wrapped `DirectMcpServer`, and its `DirectClient` once connected. |
| `tools`, `resources`, `resourceTemplates`, `prompts` | What the server lists, with the tools and resources components added. |
| `registry` | The provider's [`ComponentRegistry`](https://frontmcp.dev/reference/react/components#dynamicrenderer-and-componentregistry). |
| `connect()` | Connects, for a provider with `autoConnect={false}`. Does nothing once connected. |

`useServer(name?)` returns the raw `ServerEntry` from [`serverRegistry`](https://frontmcp.dev/reference/react#serverregistry), or `undefined` when none is registered under the name. Without one it reads the name of the provider it sits under, like `useFrontMcp()`. `useResolvedServer(name?)` returns `{ entry, name, registry, connect }`, what the other hooks are built on.

Changed in 1.8.7: `useServer()` always looked for `"default"`, so under a provider with a `name` it returned `undefined`.

### `useCallTool()`

```tsx
const [call, state, reset] = useCallTool<Input, Output>(toolName, options?);
```

| Option | Default | Description |
| --- | --- | --- |
| `server` | the provider's | Call a tool on another of the provider's servers. |
| `onSuccess(data)` | | Called with the result after each call that returned one. |
| `onError(error)` | | Called when a call couldn't be made or threw. |
| `resetOnToolChange` | `true` | Clear the state when `toolName` changes. |

`call(args)` returns a promise of the result, or `null` when the call failed. `state` is `{ data, loading, error, called }`. `reset()` sets it back to `{ data: null, loading: false, error: null, called: false }`.

`data` is the tool's MCP result, `{ content, structuredContent? }`. `Output` types `data` as a whole, not the tool's output, so type it as the result: `useCallTool<{ name: string }, { structuredContent?: { message: string } }>("greet")`.

> **Pitfall: A tool that fails isn't an error**
When the tool throws, or doesn't exist, or its input is invalid, the server still answers, so `call()` resolves: `data` is `{ content: [{ type: "text", text: "Tool \"missing\" not found" }], isError: true, _meta: { code: "TOOL_NOT_FOUND", … } }`, `error` is `null`, and `onSuccess` runs. Check `data?.isError`. `error` is set only when the call couldn't be made, like `FrontMCP not connected` before the provider connects.

### `useReadResource()`

With a URI, it reads the resource when the provider connects, and again when the URI changes:

```tsx
const { data, loading, error, refetch } = useReadResource("app://config");
```

Without one, it returns a function to read on demand:

```tsx
const [read, { data, loading, error }] = useReadResource();
await read("app://config");
```

`data` is the MCP result, `{ contents: [{ uri, mimeType, text }] }`, text not parsed. A read that fails sets `error` to the client's error, like `MCP error -32002: … Resource not found: state://counter`. Before the provider connects, `read()` returns `null` with `error` `FrontMCP not connected`.

### `useGetPrompt()`

```tsx
const [getPrompt, { data, loading, error }] = useGetPrompt("summarize");
await getPrompt({ topic: "AI" }); // data: { description, messages }
```

Arguments are strings, as MCP prompts take them.

### `useListTools()`, `useListResources()`, `useListPrompts()`

Read the lists the provider loaded when it connected. They start empty, and follow the server: the tools when the server says they changed, a component's tool or one from [`server.registerTool()`](https://frontmcp.dev/reference/sdk/register-tool) included, and the resources when a component adds or removes one.

| Hook | Returns |
| --- | --- |
| `useListTools(options?)` | `ToolInfo[]`: `{ name, description, inputSchema, annotations }`. |
| `useListResources(options?)` | `{ resources, resourceTemplates }`. |
| `useListPrompts(options?)` | `PromptInfo[]`: `{ name, description, arguments }`. |

### `useStoreResource()`

```tsx
const { data, loading, error, refetch } = useStoreResource("app://count");
```

Reads a resource when the provider connects, parses its text as JSON when it can, and subscribes to it: when the server sends `notifications/resources/updated` for the URI, it reads it again. A tool that calls `this.notifyResourceUpdated("app://count")` updates every component showing it.

The `state://` resources of the provider's [`stores`](https://frontmcp.dev/reference/react#exposing-app-state-with-stores) send it whenever the store's state changes, so `data` follows them too. Changed in 1.8.7: they never sent it, and `data` kept its first value until you called `refetch()`.

`@frontmcp/react/state` exports a different hook with the same name, which [registers a store](#hooks-in-frontmcpreactstate).

### `useDynamicTool()`

Registers a tool with the server while the component is mounted, through [`server.registerTool()`](https://frontmcp.dev/reference/sdk/register-tool). It's a tool of the server like any other: listed by `useListTools()` and `server.listTools()`, callable with `useCallTool()`, with `server.callTool()` and from any client of the server, and its calls run through the server's flow, with the app's plugin hooks, authorities and `availableWhen`. [`@frontmcp/plugin-webmcp`](https://frontmcp.dev/reference/plugins/webmcp) offers it to agents in the browser. It's gone when the component unmounts. Two components registering the same name make one tool, which the last to register handles. When that component unmounts, the other one's `execute` and description take over, and the tool stays until both unmount.

| Option | Type | Description |
| --- | --- | --- |
| `name`, `description` | `string` | **Required.** |
| `schema` | a Zod object | The input, as a Zod object from `@frontmcp/sdk`'s `z`. The input is checked before `execute` runs, and `execute`'s arguments are typed. |
| `inputSchema` | JSON Schema | Instead of `schema`. Nothing is checked. |
| `execute(args)` | `(args) => Promise<CallToolResult>` | **Required.** Returns an MCP result, like `{ content: [{ type: "text", text: "Added" }] }`. The tool calls the `execute` of the component's last committed render, so it sees the state on screen. |
| `annotations` | `{ readOnlyHint?, destructiveHint?, idempotentHint?, openWorldHint?, … }` | Listed in `tools/list`, as for any tool. |
| `availableWhen` | | Where the tool is offered, like `{ surface: ["webmcp"] }` for agents in the browser only ([The `"webmcp"` call surface](https://frontmcp.dev/reference/sdk/register-tool#the-webmcp-call-surface)). The provider's own client is on the `"mcp"` surface, so it neither lists nor calls such a tool. |
| `app` | `string` | The app the tool joins, on a server with several. Default: the provider's [`dynamicToolApps`](https://frontmcp.dev/reference/react#props) entry. |
| `enabled` | `boolean` | `false` unregisters the tool, or never registers it. Default `true`. |
| `server` | `string` | Register it on another of the provider's servers. |

With `schema`, input that doesn't match returns `isError: true` and the problems as JSON: `{"error":"validation_error","issues":[{"path":["itemId"],"message":"Invalid input: expected string, received undefined"}]}`. The check runs inside the tool, so a plugin's `Will("execute")` hook sees the call before it's rejected. The tool's `inputSchema` in `tools/list` is the JSON Schema of `schema`.

A schema written inside the component, like `schema: z.object({ … })` in the hook's options, is fine: the tool is registered again only when the schema's content changes, so the component doesn't render without end under `useListTools()` or `useFrontMcp()`, and a schema that does change, like one built from state, updates the tool's `inputSchema`. A new `read` function for `useDynamicResource()` doesn't register it again either. Changed in 1.8.7: each render's new schema object registered the tool again, and the page froze.

The server refuses a name it already has, or one longer than 64 characters. The component renders without its tool, and the provider's `onError` gets `Dynamic tool "greet" was not registered: A tool named "greet" is already registered`, or `… Tool entry validation failed: runtime tool name must be 1-64 characters`. Without `onError`, the console gets a warning, `[frontmcp] dynamic tool "greet" was not registered: …`.

A render that React throws away, like one of a transition that hasn't finished, never reaches the tool: during the transition, a call still sees the state on screen.

Changed in 1.9: the tool lived in the provider's wrapper, not in the server. Its calls skipped the server's flow, so no plugin hook or authority saw them, `server.callTool("add_to_cart")` on the server you created answered `Tool "add_to_cart" not found`, and a component's tool named like a server tool replaced it for the provider's client. And the tool kept the `execute` of the last render, committed or not.

### `useDynamicResource()`

Registers a resource while the component is mounted, like `useDynamicTool()`. The resource stays in the provider's wrapper, not in the server: the provider's client and hooks read it, and `server.readResource()` on the server you created answers `Resource not found`.

| Option | Type | Description |
| --- | --- | --- |
| `uri`, `name` | `string` | **Required.** |
| `read()` | `() => Promise<ReadResourceResult>` | **Required.** Returns `{ contents: [{ uri, mimeType?, text }] }`. The `read` of the component's last committed render is called. |
| `description`, `mimeType` | `string` | |
| `enabled`, `server` | | As for `useDynamicTool()`. |

### `useComponentTree()`

Registers a resource that describes the DOM under an element, for a model that needs to see the page's structure:

| Option | Default | Description |
| --- | --- | --- |
| `rootRef` | | **Required.** A ref to the root element. |
| `uri` | `"react://component-tree"` | Must start with a scheme, like `app://`: `"component-tree"` throws `URI must have a valid scheme (e.g., file://, https://, custom://)` while rendering. |
| `maxDepth` | `10` | How many levels below the root. |
| `includeProps` | `false` | Add each element's `data-*` attributes, except `data-component`, as `props`. |
| `server` | | |

Reading it returns `{"component":"Dashboard","tag":"div","children":[{"component":"Sidebar","tag":"div","children":[…],"props":{"data-open":"true"}}, …]}`: `component` is the element's `data-component`, or its tag name.

### Hooks in `@frontmcp/react/state`

`useStoreResource(options)`, `useReduxResource(options)` and `useValtioResource(options)` do in a component what the provider's [`stores`](https://frontmcp.dev/reference/react#exposing-app-state-with-stores) do: they register the state as resources and the actions as tools, while the component is mounted. They take the options of the matching adapter, `createStore()`, `reduxStore()` or `valtioStore()`, and the tool and resource names follow the same pattern: `useStoreResource({ name: "counter", getState, subscribe, selectors, actions })` makes `state://counter`, `state://counter/<selector>` and `counter_<action>`. Without a `name`, Redux and Valtio use `"redux"` and `"valtio"`.

The action tools are server tools, as with [`useDynamicTool()`](#usedynamictool). The options can be written in the component: the hooks register again only when a name changes. Changed in 1.9: they registered again whenever the options object was a new one, and under a component that read the server's state the page froze, so options had to be made outside the component, and `useReduxResource()` needed a component of its own ([Caveats](https://frontmcp.dev/reference/react#caveats)).

```tsx RegisterCounter.tsx
import { useStoreResource } from "@frontmcp/react/state";

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

const options = {
  name: "counter",
  getState: () => counter,
  subscribe: (cb: () => void) => {
    listeners.add(cb);
    return () => { listeners.delete(cb); };
  },
  selectors: { value: (state: unknown) => (state as typeof counter).value },
  actions: {
    increment: (by: unknown) => {
      counter = { value: counter.value + (typeof by === "number" ? by : 1) };
      listeners.forEach((listener) => listener());
      return counter.value;
    },
  },
};

export function RegisterCounter() {
  useStoreResource(options);
  return null;
}
```

Mounted under the provider, it lists the tool `counter_increment` and the resources `state://counter` and `state://counter/value`. Calling the tool with `{ args: [5] }` returns `{"success":true,"result":6}`, and a component reading `useStoreResource("state://counter")` from the root entry point shows `{ value: 6 }` without a `refetch()`. The tool and the resources go away when the component unmounts.

Changed in 1.8.7: these hooks registered nothing, because `/state` had its own copy of the provider's context. Use `stores` if you want the registration to live as long as the provider.

### `useApiClient()`

`useApiClient({ baseUrl, operations, headers?, prefix = "api", client?, fetch?, server? })`, from `@frontmcp/react/api`, registers a tool per operation while the component is mounted, named `<prefix>_<operationId>`, with the operation's `description` and `inputSchema`:

- A call fills the operation's path from the arguments, with each value URL-encoded: `{ id: "u 1" }` for `GET /users/{id}` requests `https://api.example.com/users/u%201`.
- Query parameters go in the URL, `{ fields: "name" }` as `?fields=name`, and header parameters are sent as headers. A `body` argument is sent as JSON, except for `GET` and `HEAD`.
- `headers` is an object, or a function called on every request, and `Content-Type: application/json` is added. `client` replaces the `fetch` client with an `HttpClient` of your own, and `fetch` gives the default one a `fetch` function.
- The result is text, `{"status":200,"statusText":"","data":{…}}`, with `isError: true` when the status is 400 or more. If the client throws, like on a network failure, the call has no result, and [`useCallTool()`](#usecalltool)'s `error` is what it threw.

Parse the spec once, at module level, as [below](#registering-api-operations-as-tools). An `operations` array made in the component works too: the tools are registered again only when an operation's name changes.

`parseOpenApiSpec(spec)` turns an OpenAPI 3 document into operations: `{ operationId, description, method, path, inputSchema, parameters }`. Path, query and header parameters become properties, `parameters` records where each one goes, a JSON request body becomes `body`, and an operation without an `operationId` gets one from its method and path, like `delete__users__id_`. `description` is the `summary`, else the `description`, else `"POST /users"`.

`createFetchClient(fetch?)` returns an `HttpClient`: `request({ method, url, headers, body? })` sends `body` as JSON with `Content-Type: application/json`, and resolves to `{ status, statusText, data }`, with the response parsed as JSON when it can be.

Changed in 1.8.7: `useApiClient()` registered nothing, for the same reason as the `/state` hooks, and the workaround was a `useDynamicTool()` for each operation.

Changed in 1.9: query and header parameters were listed in the tool's schema but not sent, and a new `operations` array registered every tool again on every render, which froze the page under a component that read the server's state.

### `useTools()` and `useAITools()`

`useAITools(platform, { server? })` and `useTools(platform, { server? })`, from `@frontmcp/react/ai`, give an AI SDK the server's tools in its format, components' tools included:

| Hook | Returns |
| --- | --- |
| `useAITools(platform)` | `{ tools, callTool(name, args?), loading, error }`. `tools` is the list in the platform's format, `null` until loaded. `callTool` runs the tool and converts the result. |
| `useTools(platform)` | `{ tools, processToolCalls(calls), loading, error }`. `processToolCalls` takes the tool calls the model made, in the platform's shape, and returns the results to send back. |

`platform` is `"openai"`, `"claude"`, `"langchain"`, `"vercel-ai"` or, for `useAITools()`, `"raw"`. For one tool `a` that takes `{ x: string }`: `"openai"` gives `[{ type: "function", function: { name: "a", description, parameters, strict: true } }]` and `callTool` the tool's result as an object; `"claude"` gives `[{ name, description, input_schema }]` and `callTool` the result's content blocks, `[{ type: "text", text: "{…}" }]`; `"langchain"` gives `[{ name, description, schema }]`; `"vercel-ai"` gives `{ a: { description, parameters } }`; `"raw"` gives `[{ name, description, inputSchema }]` and the whole MCP result. `useTools("claude").processToolCalls([{ type: "tool_use", id, name, input }])` returns `[{ type: "tool_result", tool_use_id, content }]`, and the OpenAI one `[{ role: "tool", tool_call_id, content }]`.

`formatToolsForPlatform(tools, platform)` from `@frontmcp/sdk` converts a list by hand, and `createToolCallHandler(server, platform)` from `@frontmcp/react/ai` gives `{ callTool }` for a server you hold, like the one from `useFrontMcp()`. The conversions are those of [`connect()`'s variants](https://frontmcp.dev/reference/sdk/connect#the-adapters).

Changed in 1.8.7: `useAITools()` and `useTools()` read their own copy of `serverRegistry`, which no provider filled, and stayed at `{ tools: null, loading: true }`.

---

## Usage

### Showing a tool's result

```tsx Search.tsx
import { useCallTool } from "@frontmcp/react";

type SearchResult = { structuredContent?: { tickets: { id: string; title: string }[] }; isError?: boolean; content?: { text?: string }[] };

export function Search() {
  const [search, { data, loading, error }, reset] = useCallTool<{ query: string }, SearchResult>("search_tickets");

  return (
    <div>
      <button disabled={loading} onClick={() => search({ query: "login" })}>Search</button>
      <button onClick={reset}>Clear</button>
      {error && <p role="alert">Couldn't call the tool: {error.message}</p>}
      {data?.isError && <p role="alert">{data.content?.[0]?.text}</p>}
      <ul>{data?.structuredContent?.tickets.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
    </div>
  );
}
```

### Giving the model a tool from a component

`add_to_cart` exists while the cart is on screen, and only for signed-in users. Its schema is defined once, outside the component.

```tsx CartControls.tsx
import { useState } from "react";
import { useDynamicTool } from "@frontmcp/react";
import { z } from "@frontmcp/sdk";

const addToCartInput = z.object({ itemId: z.string().describe("Product id"), quantity: z.number().optional() });

export function CartControls({ signedIn }: { signedIn: boolean }) {
  const [items, setItems] = useState<string[]>([]);

  useDynamicTool({
    name: "add_to_cart",
    description: "Add a product to the user's cart",
    schema: addToCartInput,
    enabled: signedIn,
    execute: async ({ itemId, quantity }) => {
      setItems((list) => [...list, `${itemId} × ${quantity ?? 1}`]);
      return { content: [{ type: "text", text: `Added ${itemId}` }] };
    },
  });

  return <ul>{items.map((item) => <li key={item}>{item}</li>)}</ul>;
}
```

Calling `add_to_cart` with `{ itemId: "sku-1", quantity: 2 }` returns `Added sku-1` and adds `sku-1 × 2` to the list. With `signedIn` false, the tool isn't listed.

### Keeping a resource live

`bump` changes a count and tells subscribers; every `Count` on the page reads it again.

```tsx Count.tsx
import { Resource, ResourceContext, Tool, ToolContext, useCallTool, useStoreResource } from "@frontmcp/react";

let count = 0;

@Resource({ uri: "app://count", name: "count", mimeType: "application/json" })
export class CountResource extends ResourceContext {
  async execute() {
    return { count };
  }
}

@Tool({ name: "bump", description: "Add one to the count", inputSchema: {} })
export class Bump extends ToolContext {
  async execute() {
    count++;
    this.notifyResourceUpdated("app://count");
    return { count };
  }
}

export function Count() {
  const { data } = useStoreResource("app://count");
  const [bump] = useCallTool("bump");
  return <button onClick={() => bump({})}>Count: {(data as { count: number } | null)?.count}</button>;
}
```

The button reads `Count: 0`, then `Count: 1` after a click, with no `refetch()`.

### Registering API operations as tools

`useApiClient()` turns an OpenAPI document into tools. The operations are parsed once, outside the component, and the component renders nothing:

```tsx ApiTools.tsx
import { useApiClient, parseOpenApiSpec } from "@frontmcp/react/api";
import spec from "./openapi.json";

const operations = parseOpenApiSpec(spec);

export function ApiTools({ token }: { token: string }) {
  useApiClient({
    baseUrl: "https://api.example.com",
    operations,
    headers: () => ({ Authorization: `Bearer ${token}` }),
  });
  return null;
}
```

For a `GET /users/{id}` operation with `operationId: "getUser"`, `api_getUser` is listed, and calling it with `{ id: "u 1" }` requests `https://api.example.com/users/u%201` with the `Authorization` header, and answers `{"status":200,"statusText":"","data":{…}}` as text. `headers` is a function, so each request reads the token the component has now.

### Handing the tools to an AI SDK

```tsx Chat.tsx
import { useAITools } from "@frontmcp/react/ai";

export function useOpenAITools() {
  const { tools, callTool, loading } = useAITools("openai");
  return { tools, callTool, ready: !loading && tools !== null };
}
```

`tools` goes to the chat completion request, with the components' tools among the server's, and `callTool(name, args)` runs each tool call the model makes and returns its result converted for OpenAI. To handle a whole batch of tool calls in the platform's own shape, `useTools("openai").processToolCalls(calls)` returns the messages to send back. Keep API keys on a server of your own.

---

## Troubleshooting

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

The provider's `onError` got it, or the console, when the server refused a component's tool ([useDynamicTool](#usedynamictool)):

- `A tool named "…" is already registered`: the server has a tool by that name, its own or another one registered at run time. Rename the component's tool.
- `runtime tool name must be 1-64 characters`: shorten the name.
- `runtime tool "…" must name its app (one of: …)`: the server has several apps. Give the tool an `app`, or the provider `dynamicToolApps`.

### `data` holds an error but `error` is `null`

The tool failed on the server, which is a result, not an error ([useCallTool](#usecalltool)). Check `data.isError`; the message is in `data.content[0].text`.

### `FrontMCP not connected`

The call ran before the provider connected. Wait for `status === "connected"` from `useFrontMcp()`.

### `MCP error -32002: … Resource not found: state://…`

`useStoreResource()` or `useReadResource()` read a `state://` URI no store registers: the store's `name` differs, or nothing registered it yet. Pass the store in the provider's `stores`, or mount the `/state` hook that registers it ([Hooks in `@frontmcp/react/state`](#hooks-in-frontmcpreactstate)).

### A component's tool answers `{"error":"validation_error",…}`

Its `useDynamicTool()` `schema` rejected the input, before `execute` ran. The `issues` list what's wrong, with a `path` to each field.

### `URI must have a valid scheme (e.g., file://, https://, custom://)`

`useComponentTree()` got a `uri` without a scheme. Use something like `app://component-tree`.

### `useAITools()` stays `{ tools: null, loading: true }`

The provider hasn't connected: `autoConnect={false}` before `connect()`, or the hook sits outside a `FrontMcpProvider`. It fills in once the provider's status is `"connected"` ([useTools and useAITools](#usetools-and-useaitools)).
