@nimblebrain/synapse

Agent-aware app SDK for the NimbleBrain platform. Typed tool calls, reactive state, and React hooks over the MCP ext-apps protocol.
What is Synapse?
Synapse is an optional enhancement layer over @modelcontextprotocol/ext-apps. It wraps the ext-apps protocol handshake and adds:
- Typed tool calls — call MCP tools with full TypeScript input/output types
- Reactive data sync — subscribe to data change events from the agent
- Theme tracking — automatic light/dark mode and custom design tokens
- State store — Redux-like store with optional persistence and LLM visibility
- Keyboard forwarding — forward shortcuts from sandboxed iframes to the host
- Code generation — generate TypeScript types from manifests, running servers, or JSON schemas
In non-NimbleBrain hosts (Claude Desktop, VS Code, ChatGPT), NB-specific features degrade gracefully to no-ops while ext-apps baseline behavior is preserved.
Install
npm install @nimblebrain/synapse
Peer dependency: @modelcontextprotocol/ext-apps@^1.3.1
Package Exports
@nimblebrain/synapse | Vanilla JS core (no framework dependency) |
@nimblebrain/synapse/react | React hooks and provider |
@nimblebrain/synapse/vite | Vite plugin for dev mode |
@nimblebrain/synapse/codegen | CLI + programmatic code generation |
Quick Start
Vanilla JS
import { createSynapse } from "@nimblebrain/synapse";
const synapse = createSynapse({
name: "my-app",
version: "1.0.0",
});
await synapse.ready;
const result = await synapse.callTool("get_items", { limit: 10 });
console.log(result.data);
synapse.onDataChanged((event) => {
console.log(`${event.tool} was called on ${event.server}`);
});
synapse.setVisibleState(
{ selectedItem: "item-42" },
"User is viewing item 42",
);
React
import { SynapseProvider, useCallTool, useTheme } from "@nimblebrain/synapse/react";
function App() {
return (
<SynapseProvider name="my-app" version="1.0.0">
<ItemList />
</SynapseProvider>
);
}
function ItemList() {
const { call, data, isPending } = useCallTool<Item[]>("list_items");
const theme = useTheme();
return (
<div style={{ colorScheme: theme.mode }}>
<button onClick={() => call()} disabled={isPending}>
Load Items
</button>
{data?.map((item) => <div key={item.id}>{item.name}</div>)}
</div>
);
}
Vite Plugin
import { synapseVite } from "@nimblebrain/synapse/vite";
export default {
plugins: [
synapseVite({
appName: "my-app",
}),
],
};
Code Generation
Generate TypeScript types from an app manifest:
npx synapse --from-manifest ./manifest.json --out src/generated/types.ts
Or from a running MCP server:
npx synapse --from-server http://localhost:3000 --out src/generated/types.ts
Or from a directory of .schema.json files (generates CRUD tool types):
npx synapse --from-schema ./schemas --out src/generated/types.ts
State Store
Create a typed, reactive store with optional persistence and agent visibility:
import { createSynapse, createStore } from "@nimblebrain/synapse";
const synapse = createSynapse({ name: "my-app", version: "1.0.0" });
const store = createStore(synapse, {
initialState: { count: 0, items: [] },
actions: {
increment: (state) => ({ ...state, count: state.count + 1 }),
addItem: (state, item: string) => ({
...state,
items: [...state.items, item],
}),
},
persist: true,
visibleToAgent: true,
summarize: (state) => `${state.items.length} items, count=${state.count}`,
});
store.dispatch.increment();
store.dispatch.addItem("hello");
Use useStore in React:
import { useStore } from "@nimblebrain/synapse/react";
function Counter() {
const { state, dispatch } = useStore(store);
return <button onClick={() => dispatch.increment()}>{state.count}</button>;
}
API Reference
createSynapse(options)
Creates a Synapse instance. Returns a Synapse object.
name | string | App name (must match registered bundle name) |
version | string | Semver version |
internal | boolean? | Enable cross-server tool calls (NB internal only) |
forwardKeys | KeyForwardConfig[]? | Custom keyboard forwarding rules |
Synapse Methods
ready | Promise that resolves after the ext-apps handshake |
isNimbleBrainHost | Whether the host is a NimbleBrain platform |
callTool(name, args?) | Call an MCP tool and get typed result |
onDataChanged(cb) | Subscribe to data change events |
getTheme() | Get current theme |
onThemeChanged(cb) | Subscribe to theme changes |
action(name, params?) | Dispatch a NB platform action |
chat(message, context?) | Send a chat message to the agent |
setVisibleState(state, summary?) | Push LLM-visible state (debounced 250ms) |
downloadFile(name, content, mime?) | Trigger a file download |
openLink(url) | Open a URL (host-aware) |
destroy() | Clean up all listeners and timers |
React Hooks
useSynapse() | Access the Synapse instance |
useCallTool(name) | { call, data, isPending, error } for a tool |
useDataSync(cb) | Subscribe to data change events |
useTheme() | Reactive theme object |
useAction() | Dispatch platform actions |
useChat() | Send chat messages |
useVisibleState() | Push LLM-visible state |
useStore(store) | { state, dispatch } for a store |
Development
npm install
npm run build
npm test
npm run typecheck
npm run lint
npm run lint:fix
npm run ci
Publishing
Requires npm login with access to the @nimblebrain org.
npm login
npm version patch
npm publish --access public
git push origin main --tags
License
MIT