Skip to content

Render one component across hosts

The same UI component can end up in very different hosts. ChatGPT, Claude and NimbleBrain all speak the MCP Apps standard, a JSON-RPC bridge over postMessage with a handshake a host enforces, and a standalone page has no host at all. Speak that bridge by hand and your component carries the handshake, sizing and theme plumbing itself.

Call connectUI from @nimblebrain/synapse/host. It detects whether it is framed, picks the adapter, applies the host theme to the DOM, and returns one SynapseUIClient façade you code against. It is synchronous, so data() is populated on return.

import { connectUI } from "@nimblebrain/synapse/host";
const synapse = connectUI({ name: "my-widget", version: "1.0.0" });
// Render the data that spawned the widget, then subscribe to future pushes.
render(synapse.data());
synapse.onData(render);
// Feature-detect before reaching for a capability that varies by host.
if (synapse.capabilities().pull) {
const rows = await synapse.callTool("list_items", { limit: 10 });
render(rows);
}
if (synapse.capabilities().sendPrompt) {
synapse.sendPrompt("Summarize what I'm looking at");
}

The façade exposes data() and onData() for pushed tool output, theme() and onTheme() for the resolved theme, callTool() for a widget-to-server call, sendPrompt() to send a follow-up to the conversation, openLink() to open an external URL, and resize() to report content height. destroy() tears down the listeners.

data(), onData(), theme(), and resize() work in every host. The other three vary, which is why you feature-detect with capabilities(). It returns whether the host supports pull (so callTool can reach the server), sendPrompt, and openLink. Calling callTool() on a host with no pull rejects with a HostUnsupportedError, so gate it on capabilities().pull first. When the host makes the call but the tool fails or refuses it, callTool() rejects with a ToolCallError: MCP reports that failure inside the result, as isError: true, and the error carries the whole result on result. Use host() only as an escape hatch (it returns "mcp-apps" in a frame and "generic" standalone, whichever product the frame belongs to); prefer capabilities() so your component stays host-agnostic.