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.
nullor 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’scapabilityfield 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.
Per hook and method
Section titled “Per hook and method”| 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. |
The portable subset
Section titled “The portable subset”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.
The NimbleBrain extensions
Section titled “The NimbleBrain extensions”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.
Deciding what to offer
Section titled “Deciding what to offer”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.
Related
Section titled “Related”- Cross-host architecture: one component, many hosts.
- The MCP ext-apps protocol: the spec surface this contract is built on.