Call MCP tools with types
Calling an MCP tool over raw ext-apps is a pile of boilerplate: build a JSON-RPC envelope, generate a request ID, post it to the parent, track the pending promise, time it out, then dig the payload out of the content array and parse it. You write it once, then again for the next tool, untyped every time.
The recipe
Section titled “The recipe”useCallTool<TOutput>(name) returns everything you need: a call function, the
last data, an isPending flag, and an error. Pass the tool’s output type as
the generic and data is typed for you. Wrap your tree in <AppProvider>.
import { useCallTool } from "@nimblebrain/synapse/react";
interface DocumentInfo { id: string; title: string;}
function DocumentList() { const { call, data, isPending, error } = useCallTool<DocumentInfo[]>("list_documents");
if (isPending) return <Spinner />; if (error) return <ErrorBox error={error} />; if (!data) return <button onClick={() => call({ limit: 10 })}>Load documents</button>;
return ( <ul> {data.map((doc) => ( <li key={doc.id}>{doc.title}</li> ))} </ul> );}Outside React, call the tool on the App and read result.data:
const result = await app.callTool("list_documents", { limit: 10 });console.log(result.data); // typed outputHow it works
Section titled “How it works”call(args?) sends the tools/call request, tracks it, waits for the host’s
response, and parses the content array into typed data for you. isPending
flips around the call so you can render a loading state, and a thrown or errored
call lands in error instead of an unhandled rejection. Concurrent calls are
guarded, so if the user triggers the same tool twice only the latest result is
kept, and a stale earlier response cannot overwrite it.
A call the page slept through
Section titled “A call the page slept through”A browser suspends a background tab, and a laptop lid closes. A call in flight
when that happens is cut off. The NimbleBrain host answers it with JSON-RPC error
-32001 whose data.suspendedMs says how long the page was suspended, so an app
can tell its own sleep from a slow server. isSuspensionError(err) checks for it.
The tool may or may not have run, so the SDK never sends it again: the call
rejects, and useCallTool puts the host’s answer in error. Re-read before
retrying a write, and tell the user to look first:
import { isSuspensionError } from "@nimblebrain/synapse";
const { call, error } = useCallTool("archive_document");if (isSuspensionError(error)) { // The page slept through the call. Re-read, then offer to try again.}A read is safe to call again. A view that reads through useDataSync re-reads
by itself when it is shown after a long hide (see
keep the UI in sync).
Related
Section titled “Related”- Keep the UI in sync with the agent: re-run a call when the agent mutates state.
- Handle long-running tools (tasks): for tool work that outlives the request timeout.
- Give the agent context about the UI: tell the agent what the user is acting on.