Skip to content

The portable-app contract

A Synapse app runs in any host that implements MCP Apps: Claude, ChatGPT, the NimbleBrain runtime, or one you build. Hosts do not all offer the same things, and each one says what it offers in its answer to ui/initialize, as hostCapabilities.

Synapse reads that declaration once, when the app connects. Every method and hook checks it before sending anything, and when a capability is missing the call does one of three documented things:

  • No-op. Fire-and-forget calls send nothing. Nothing on the other end would act on them.
  • null or silence. Hooks that wait for the host keep their initial value, and callbacks never run.
  • HostCapabilityError. Calls that return an answer reject without sending. A host that does not implement a request may never answer it, and Synapse’s requests have no deadline, so sending anyway would leave your promise pending forever. The error’s capability field names what was missing.

Nothing is gated on the host’s name. A host that declares a capability gets the call, whatever it is called.

Hook / method Needs Without it
useTheme, useHostContext, app.theme nothing Neutral defaults for the mode. Host-only context fields are undefined.
useToolResult, useToolInput nothing null until the host sends one. A host that mounts the app without a tool call never sends one.
useResize, app.resize nothing Always sent. The spec has no capability for it.
useCallTool, app.callTool serverTools HostCapabilityError. The hook’s error holds it.
app.readServerResource serverResources HostCapabilityError
useDataSync serverResources.listChanged The callback runs only when the view is shown after a long hide. Otherwise the app shows what it last loaded.
useModelContext, app.updateModelContext updateModelContext No-op
useSendMessage, app.sendMessage message No-op
app.openLink openLinks Opens the URL with window.open instead. The same happens when the host refuses.
downloadFile downloadFile HostCapabilityError
useCallToolAsTask, callToolAsTask io.modelcontextprotocol/tasks, under experimental HostCapabilityError. Check app.supportsTasks first and fall back to callTool.
useOpen, open ai.nimblebrain/open, under experimental Resolves false without sending
useOpenConnectorSettings, openConnectorSettings ai.nimblebrain/open-connector-settings, under experimental Resolves false without sending
useFileUpload, pickFile, pickFiles ai.nimblebrain/request-file, under experimental HostCapabilityError
uploadFiles ai.nimblebrain/upload-files, under experimental HostCapabilityError
useNotify, notify ai.nimblebrain/notify, under experimental Resolves false without sending. A notice is never essential.
connect({ forwardKeys }) ai.nimblebrain/keydown, under experimental Keys are not captured, so the browser handles them as usual.
useTrail, setLocation ai.nimblebrain/location, under experimental No-op. useTrail returns false, so the view keeps its own title and breadcrumb.

These parts of the surface are defined by the MCP Apps spec, and every host that renders apps is expected to support them:

  • the handshake, the host context and theme changes;
  • tool input and tool results;
  • calling your own server’s tools (callTool);
  • sending a message into the conversation (sendMessage);
  • pushing what the user sees to the model (updateModelContext);
  • reporting size (resize).

An app built only on these works the same in every host. Everything else in the table above is still spec surface, but hosts may leave it out, so check the declaration before you offer it.

These features have no equivalent in the spec. They are the NimbleBrain host’s own extensions, which Synapse implements a client for. Each has one name, used as its method and as the identifier a host declares in hostCapabilities.experimental to offer it. MCP Apps has no field for extensions, and experimental is the only part of that object whose contents a spec client keeps.

Extension Method and declared identifier Direction
Open what a URI names (open, useOpen) ai.nimblebrain/open app → host request, answered {}
Open the connector’s settings (openConnectorSettings, useOpenConnectorSettings) ai.nimblebrain/open-connector-settings app → host request, answered {}
File picker (pickFile, pickFiles, useFileUpload) ai.nimblebrain/request-file app → host request, answered { files }
Upload of held files (uploadFiles) ai.nimblebrain/upload-files app → host request carrying Files, answered { files }
Notices (notify, useNotify) ai.nimblebrain/notify app → host request, answered {}
Keyboard forwarding (forwardKeys) ai.nimblebrain/keydown app → host notification
Location (setLocation, onNavigate, useTrail) ai.nimblebrain/location app → host notification; the host answers a pick with ai.nimblebrain/navigate

That is the complete list. NIMBLEBRAIN_EXTENSIONS, exported from the package root, holds the same table in code.

One NimbleBrain field also rides inside a spec message, and other hosts ignore it: the workspace in the host context. What the user is acting on reaches the agent through the spec’s updateModelContext, on every host that declares it.

Read the declaration to decide what to show, rather than finding out from a no-op or an exception:

import { hostSupports } from "@nimblebrain/synapse";
import { useApp, useFileUpload } from "@nimblebrain/synapse/react";
function Attach() {
const app = useApp();
const { pickFile } = useFileUpload();
if (!hostSupports(app, "requestFile")) return null;
return <button onClick={() => pickFile()}>Attach</button>;
}

The spec capabilities are available as app.hostCapabilities, for example app.hostCapabilities.message, and tasks as app.supportsTasks.