Skip to content

React hooks

Everything React lives at @nimblebrain/synapse/react. Wrap your tree in <AppProvider>, then use the hooks below. There is one provider and one hook family; a hook used outside the provider throws rather than returning something half-connected.

import { AppProvider } from "@nimblebrain/synapse/react";
createRoot(document.getElementById("root")!).render(
<AppProvider name="my-app" version="1.0.0">
<App />
</AppProvider>,
);

<AppProvider> takes the same options as connect() and renders nothing until the handshake completes, so nothing below it can observe an app that is not connected yet.

Each hook checks what the host declared before it sends anything. The Without it column says what the hook does when the host did not declare the capability it needs. The portable-app contract has the reasoning.

Hook Returns Description Without it
useApp() App The connected app n/a
useTheme() Theme The current theme. Re-renders only when the theme actually moves — a host-context change that leaves it untouched does not re-render your components. Needs nothing. Neutral defaults when the host sends no tokens.
useHostContext<T>() T The full host context, including host extensions. Re-renders on every change. Needs nothing. Host-only fields are undefined.
useToolResult() ToolResultData | null Re-renders on every tool-result event Needs nothing. null until the host sends one.
useToolInput() Record<string, unknown> | null Re-renders on every tool-input event Needs nothing. null until the host sends one.
useResize() (w?, h?) => void Resize helper, auto-measures body if no args Needs nothing. Always sent.
Hook Returns Description Without it
useCallTool(name) { call, data, isPending, error } Call a tool on this app’s own MCP server with loading/error state. call(args?) fires it. A call the page’s suspension interrupted lands in error; see call tools. serverTools: call rejects with HostCapabilityError, and error holds it.
useCallToolAsTask(name) { fire, task, result, error, isWorking, isTerminal, cancel } A tool call the server may run as a task, followed to its end by polling. See long-running tools. The tasks capability: fire rejects with HostCapabilityError. Check app.supportsTasks.
useDataSync(cb) none Run cb when your server announces its data changed (notifications/resources/list_changed). cb receives the latest notification’s params. Notifications that arrive together run cb once, and while a promise cb returned is pending, later ones run it once after it settles. cb also runs, with {} and through the same path, when the view is shown after staying hidden for a minute or more. See keep the UI in sync. serverResources.listChanged: cb runs only on resume.
Hook Returns Description Without it
useModelContext() (state, summary?) => void Push what the user is looking at to the agent, debounced 250 ms updateModelContext: no-op
useModelContext(factory, deps) none The same, pushed whenever deps change updateModelContext: no-op
useSendMessage() (text) => void Send a message into the agent conversation message: no-op

The debounce lives in the hook rather than on app.updateModelContext, which sends immediately: a selection the user drags through should cost one frame, not thirty, and that problem is a React one.

Not part of the MCP Apps spec. Each works only on a host that declares it in hostCapabilities.experimental. hostSupports(app, extension) tells you whether it did.

Hook Returns Description Without it
useOpen() (uri) => Promise<boolean> Open what a resource URI names, through the host. See open ai.nimblebrain/open: resolves false without sending
useOpenConnectorSettings() () => Promise<boolean> Open this connector’s settings page through the host. See openConnectorSettings ai.nimblebrain/open-connector-settings: resolves false without sending
useNotify() (notice) => Promise<boolean> Show the user a notice through the host, labelled with your app. See notify ai.nimblebrain/notify: resolves false without sending
useFileUpload() { pickFile, pickFiles, uploadFiles, isPending } The host’s native file picker, and uploads of files the app holds ai.nimblebrain/request-file for the pickers, ai.nimblebrain/upload-files for uploadFiles: each rejects with HostCapabilityError without its own
useTrail(trail, navigate) boolean Report the view’s trail (root first, current last) so the host shows its title and breadcrumb; navigate(id) runs when the user picks a level. Returns whether the host shows them, so the view drops its own ai.nimblebrain/location: sends nothing, returns false
const trail = [
{ id: "people://contacts", label: "People" },
{ id: `people://contacts/${contact.id}`, label: contact.name },
];
const hostShowsTrail = useTrail(trail, (id) => router.go(id));
const crumbs = [{ label: "People", onClick: () => router.go("people://contacts") }];
<PageHeader
crumbs={hostShowsTrail ? undefined : crumbs}
title={hostShowsTrail ? undefined : contact.name}
actions={<Button>Edit</Button>}
/>
  • connect() for the App these hooks wrap.
  • Events for the full app.on() event table.