
Security News
GitHub Actions Adds cache-mode to Limit Cache Poisoning Risk
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.
@nimblebrain/synapse
Advanced tools
Agent-aware app SDK for the MCP ext-apps protocol. One await connect() and you're live — typed tool calls, reactive data sync, and React hooks that work in any host implementing ext-apps (Claude Desktop, VS Code, ChatGPT, NimbleBrain, or your own runtime).
Synapse is an optional enhancement layer over @modelcontextprotocol/ext-apps. It wraps the ext-apps protocol handshake and adds:
await connect() resolves when the host is ready. You never see ui/initialize.In non-NimbleBrain hosts (Claude Desktop, VS Code, ChatGPT), NB-specific features degrade gracefully to no-ops while ext-apps baseline behavior is preserved.
Raw ext-apps gives you an iframe and postMessage. That works — until the agent changes data and your UI goes stale, or the user filters a view and the agent can't see what they're looking at, or you spend an afternoon wiring up JSON-RPC request tracking for the third time.
Synapse handles the plumbing so you can focus on the UI. See Why Synapse? for before/after comparisons of each problem it solves.
npm install @nimblebrain/synapse
Peer dependency: @modelcontextprotocol/ext-apps@^1.3.1
| Entry Point | Description |
|---|---|
@nimblebrain/synapse | Vanilla JS core — connect(), createSynapse(), createStore() |
@nimblebrain/synapse/react | React hooks and providers (AppProvider, SynapseProvider) |
@nimblebrain/synapse/vite | Vite plugin for dev mode |
@nimblebrain/synapse/codegen | CLI + programmatic code generation |
@nimblebrain/synapse/iife | Pre-built IIFE bundle for <script> tags (window.Synapse) |
import { connect } from "@nimblebrain/synapse";
const app = await connect({ name: "my-app", version: "1.0.0" });
// Theme, host info, and tool context are available immediately
console.log(app.theme.mode); // "dark"
console.log(app.hostInfo); // { name: "nimblebrain", version: "2.0.0" }
// Subscribe to tool results from the agent
app.on("tool-result", (data) => {
console.log(data.content); // parsed JSON or raw string
});
// Call an MCP tool
const result = await app.callTool("get_items", { limit: 10 });
console.log(result.data);
// Tell the agent what the user sees
app.updateModelContext(
{ selectedItem: "item-42" },
"User is viewing item 42",
);
import { AppProvider, useToolResult, useCallTool, useResize } from "@nimblebrain/synapse/react";
function App() {
return (
<AppProvider name="my-app" version="1.0.0">
<ItemList />
</AppProvider>
);
}
function ItemList() {
const result = useToolResult();
const { call, data, isPending } = useCallTool("list_items");
const resize = useResize();
useEffect(() => { if (result) resize(); }, [result, resize]);
if (!result) return <p>Waiting for data...</p>;
return result.content.items.map((item) => <div key={item.id}>{item.name}</div>);
}
Drop a single <script> tag — no bundler required:
<script src="https://unpkg.com/@nimblebrain/synapse/dist/connect.iife.global.js"></script>
<script>
Synapse.connect({ name: "widget", version: "1.0.0", autoResize: true })
.then(app => {
app.on("tool-result", (data) => {
document.getElementById("root").innerHTML = render(data.content);
});
});
</script>
// vite.config.ts
import { synapseVite } from "@nimblebrain/synapse/vite";
export default {
plugins: [
synapseVite({
appName: "my-app",
}),
],
};
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
The App object returned by connect() uses a unified on() method for all events. Each call returns an unsubscribe function.
const app = await connect({ name: "my-app", version: "1.0.0" });
// Tool results from the agent (parsed content, not raw JSON-RPC)
const unsub = app.on("tool-result", (data) => {
console.log(data.content); // parsed JSON or raw string
console.log(data.structuredContent); // structuredContent if host sent it
console.log(data.raw); // original params for advanced use
});
// Tool input arguments (what the agent is calling with)
app.on("tool-input", (args) => {
console.log(args); // Record<string, unknown>
});
// Theme changes
app.on("theme-changed", (theme) => {
document.body.classList.toggle("dark", theme.mode === "dark");
});
// Lifecycle — clean up when the host tears down the view
app.on("teardown", () => {
saveState();
});
// NimbleBrain extensions work as passthrough event names
app.on("synapse/data-changed", (params) => {
refreshData();
});
// Unsubscribe when done
unsub();
on() Event | Spec Method | Data |
|---|---|---|
"tool-result" | ui/notifications/tool-result | ToolResultData (parsed) |
"tool-input" | ui/notifications/tool-input | Record<string, unknown> |
"tool-input-partial" | ui/notifications/tool-input-partial | Record<string, unknown> |
"tool-cancelled" | ui/notifications/tool-cancelled | — |
"theme-changed" | ui/notifications/host-context-changed | Theme |
"teardown" | ui/resource-teardown | — |
| Any custom string | Passed through as-is | unknown |
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>;
}
connect(options) — RecommendedCreates a connected App instance. The returned promise resolves after the ext-apps handshake completes — theme, host info, and tool context are available immediately.
import { connect } from "@nimblebrain/synapse";
const app = await connect({ name: "my-app", version: "1.0.0" });
| Option | Type | Description |
|---|---|---|
name | string | App name (must match registered bundle name) |
version | string | Semver version |
autoResize | boolean? | Observe document.body and auto-send size-changed. Default: false |
App Properties| Property | Type | Description |
|---|---|---|
theme | Theme | Current theme (mode, tokens) |
hostInfo | { name, version } | Host identity |
toolInfo | { tool } | null | Tool context if launched from a tool call |
containerDimensions | Dimensions | null | Container size constraints from host |
App Methods| Method | Description |
|---|---|
on(event, handler) | Subscribe to events. Returns unsubscribe function. |
resize(width?, height?) | Send size to host. Auto-measures document.body if no args. |
openLink(url) | Open a URL (host-aware) |
updateModelContext(state, summary?) | Push LLM-visible state |
callTool(name, args?) | Call an MCP tool and get typed result |
sendMessage(text, context?) | Send a chat message to the agent |
destroy() | Clean up all listeners, observers, and timers |
createSynapse(options) — Advanced / LegacyThe original Synapse API. Still fully supported — use it when you need the state store, agent actions, file operations, or NimbleBrain-specific features not yet surfaced in connect().
import { createSynapse } from "@nimblebrain/synapse";
const synapse = createSynapse({ name: "my-app", version: "1.0.0" });
await synapse.ready;
| Option | Type | Description |
|---|---|---|
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| Method | Description |
|---|---|
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 |
onAction(cb) | Subscribe to agent actions (typed, declarative) |
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) |
saveFile(name, content, mime?) | Trigger a file save (NB-only) |
pickFile(options?) | Open native file picker, single file (NB-only) |
pickFiles(options?) | Open native file picker, multiple files (NB-only) |
openLink(url) | Open a URL (host-aware) |
destroy() | Clean up all listeners and timers |
AppProvider-based (Recommended)Wrap your app with <AppProvider> and use these hooks. Each is a thin wrapper over connect().
import { AppProvider, useApp, useToolResult, useToolInput, useResize, useCallTool } from "@nimblebrain/synapse/react";
| Hook | Returns | Description |
|---|---|---|
useApp() | App | Access the connected App instance |
useToolResult() | ToolResultData | null | Re-renders on every tool-result event |
useToolInput() | Record<string, unknown> | null | Re-renders on every tool-input event |
useConnectTheme() | Theme | Reactive theme from connect() |
useResize() | (w?, h?) => void | Resize helper — auto-measures body if no args |
useCallTool(name) | { call, data, isPending, error } | Call a tool with loading/error state |
SynapseProvider-based (Legacy)For existing apps using createSynapse(). Still fully supported.
import { SynapseProvider, useSynapse, useCallTool, useTheme } from "@nimblebrain/synapse/react";
| Hook | Returns | Description |
|---|---|---|
useSynapse() | Synapse | Access the Synapse instance |
useCallTool(name) | { call, data, isPending, error } | Call a tool with loading/error state |
useDataSync(cb) | — | Subscribe to data change events |
useTheme() | SynapseTheme | Reactive theme object |
useAction() | (name, params?) => void | Dispatch platform actions |
useAgentAction(cb) | — | Subscribe to agent actions |
useChat() | (msg, ctx?) => void | Send chat messages |
useVisibleState() | (state, summary?) => void | Push LLM-visible state |
useFileUpload() | File picker helpers | File upload (NB-only) |
useStore(store) | { state, dispatch } | Bind a store to React |
npm install
npm run build # Build ESM + CJS + IIFE
npm test # Run tests
npm run typecheck # Type-check
npm run lint # Lint with Biome
npm run lint:fix # Auto-fix lint issues
npm run ci # Run full CI pipeline locally (lint → typecheck → build → test)
Publishing uses npm trusted publishing via GitHub Actions. No npm login needed.
# 1. Bump version in package.json
npm version patch # or minor / major
# 2. Push commit + tag — CI verifies (lint, typecheck, build, test) then publishes
git push origin main --tags
The publish.yml workflow triggers on v* tags. It runs the full CI suite, verifies the tag matches package.json, then publishes with --provenance.
FAQs
Agent-aware app SDK for the MCP ext-apps protocol
The npm package @nimblebrain/synapse receives a total of 421 weekly downloads. As such, @nimblebrain/synapse popularity was classified as not popular.
We found that @nimblebrain/synapse demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.

Company News
Allow myself to introduce... myself.

Research
/Security News
A Twitch browser extension on Chrome and Firefox forwards users’ live OAuth session tokens through proxies controlled by a Russian bot service.