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.
Host state
Section titled “Host state”| 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. |
Agent-facing state
Section titled “Agent-facing state”| 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.
NimbleBrain extensions
Section titled “NimbleBrain extensions”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>}/>