# Synapse

> Agent-aware app SDK for the MCP ext-apps protocol. Typed tool calls, reactive data sync, and React hooks — works in any ext-apps host.

- **Type:** MCP server
- **Install:** `agentstack add mcp-nimblebraininc-synapse`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [NimbleBrainInc](https://agentstack.voostack.com/s/nimblebraininc)
- **Installs:** 0
- **Category:** [Integrations](https://agentstack.voostack.com/c/integrations)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [NimbleBrainInc](https://github.com/NimbleBrainInc)
- **Source:** https://github.com/NimbleBrainInc/synapse
- **Website:** https://www.npmjs.com/package/@nimblebrain/synapse

## Install

```sh
agentstack add mcp-nimblebraininc-synapse
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# @nimblebrain/synapse

[](https://github.com/NimbleBrainInc/synapse/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/@nimblebrain/synapse)
[](https://www.npmjs.com/package/@nimblebrain/synapse)
[](https://opensource.org/licenses/MIT)
[](https://www.typescriptlang.org/)
[](https://nodejs.org/)

Agent-aware app SDK for the [MCP ext-apps](https://modelcontextprotocol.io/specification/2025-06-18/user-interaction/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](https://nimblebrain.ai), or your own runtime).

## What is Synapse?

Synapse is an optional enhancement layer over `@modelcontextprotocol/ext-apps`. It wraps the ext-apps protocol handshake and adds:

- **Zero-config handshake** — `await connect()` resolves when the host is ready. You never see `ui/initialize`.
- **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.

## Why Synapse?

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?](docs/WHY.md)** for before/after comparisons of each problem it solves.

## Install

```bash
npm install @nimblebrain/synapse
```

**Peer dependency:** `@modelcontextprotocol/ext-apps@^1.3.1`

## Package Exports

| Entry Point | Description |
|-------------|-------------|
| `@nimblebrain/synapse` | Vanilla JS core — `connect()`, `createSynapse()`, `createStore()` |
| `@nimblebrain/synapse/react` | React hooks and providers (`AppProvider`, `SynapseProvider`) |
| `@nimblebrain/synapse/ui` | Component library — tokens, primitives, components, layouts |
| `@nimblebrain/synapse/ui/fonts` | Side-effect import that loads the brand fonts into the iframe |
| `@nimblebrain/synapse/ui/base` | Side-effect import that establishes the root-height chain (`html, body, #root`) the app shell fills. `AppFrame` does this automatically on render; import it in your entry to apply it before first paint |
| `@nimblebrain/synapse/vite` | Vite plugin for dev mode |
| `@nimblebrain/synapse/codegen` | CLI + programmatic code generation |
| `@nimblebrain/synapse/iife` | Pre-built IIFE bundle for `` tags (`window.Synapse`) |

## UI Components (`@nimblebrain/synapse/ui`)

A React component library for embedded Synapse apps: a token contract, layout
primitives (`Stack`, `Inline`, …), components (`Card`, `Badge`, `Drawer`,
`Table`, `ListRow`, …), and responsive layout scaffolds (`AppFrame`,
`SidebarLayout`, `ListDetailLayout`). The `gallery/` app is a living reference —
every token and component in light/dark across several themes.

### Design principles

These are the durable decisions behind the library; they rarely change.

- **The library holds no brand.** Tokens are `var(--token, neutral-fallback)`
  references, not hex values. The host injects the real palette/fonts at runtime
  (the MCP ext-apps `hostContext.styles.variables`), so the same app adopts
  whatever host it runs in. Standalone, it renders in neutral fallbacks.
- **Theme via CSS, not React.** Components style with token-driven inline-style
  objects whose values are those `var()` refs, so theming — including light/dark —
  resolves in CSS with no re-render. `ensureStyle` injects keyframes and
  pseudo-state rules once; brand values never get baked in.
- **Scaffold only genuinely-complex layouts.** `AppFrame`, `SidebarLayout`, and
  `ListDetailLayout` exist because they encapsulate real responsive/stateful
  complexity. Boards, grids, and simple lists are primitives + recipes, not
  components — the library codifies the shapes apps actually take, not a general
  layout engine.
- **Responsive to the pane, not the device.** Layouts observe their own width
  (`ResizeObserver` via `useBreakpoint`), because an app's iframe may be
  fullscreen, split, or a narrow rail regardless of screen size.
- **Lean on the platform.** `Drawer` is built on the native `` element
  (focus-trap, Escape, scroll behavior for free) rather than re-implementing them.

## Quick Start

### Vanilla JS

```typescript
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",
);
```

### React

```tsx
import { AppProvider, useToolResult, useCallTool, useResize } from "@nimblebrain/synapse/react";

function App() {
  return (
    
      
    
  );
}

function ItemList() {
  const result = useToolResult();
  const { call, data, isPending } = useCallTool("list_items");
  const resize = useResize();

  useEffect(() => { if (result) resize(); }, [result, resize]);

  if (!result) return Waiting for data...;
  return result.content.items.map((item) => {item.name});
}
```

### Script Tag (IIFE)

Drop a single `` tag — no bundler required:

```html

Synapse.connect({ name: "widget", version: "1.0.0", autoResize: true })
  .then(app => {
    app.on("tool-result", (data) => {
      document.getElementById("root").innerHTML = render(data.content);
    });
  });

```

### Vite Plugin

```typescript
// vite.config.ts
import { synapseVite } from "@nimblebrain/synapse/vite";

export default {
  plugins: [
    synapseVite({
      appName: "my-app",
    }),
  ],
};
```

### Code Generation

Generate TypeScript types from an app manifest:

```bash
npx synapse --from-manifest ./manifest.json --out src/generated/types.ts
```

Or from a running MCP server:

```bash
npx synapse --from-server http://localhost:3000 --out src/generated/types.ts
```

Or from a directory of `.schema.json` files (generates CRUD tool types):

```bash
npx synapse --from-schema ./schemas --out src/generated/types.ts
```

## Handling Events

The `App` object returned by `connect()` uses a unified `on()` method for all events. Each call returns an unsubscribe function.

```typescript
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
});

// 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` |
| `"tool-input-partial"` | `ui/notifications/tool-input-partial` | `Record` |
| `"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` |

## State Store

Create a typed, reactive store with optional persistence and agent visibility:

```typescript
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:

```tsx
import { useStore } from "@nimblebrain/synapse/react";

function Counter() {
  const { state, dispatch } = useStore(store);
  return  dispatch.increment()}>{state.count};
}
```

## API Reference

### `connect(options)` — Recommended

Creates a connected `App` instance. The returned promise resolves after the ext-apps handshake completes — theme, host info, and tool context are available immediately.

```typescript
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 / Legacy

The 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()`.

```typescript
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 |
| `callToolAsTask(name, args?, opts?)` | Call a long-running tool task-augmented; returns a `TaskHandle` immediately. See [Long-running tools](#long-running-tools-tasks) below. |
| `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) |
| `downloadFile(name, content, mime?)` | Trigger a file download (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 |

`Synapse` also exposes `_hostTasksCapability: TasksCapability | undefined | null` — the host's declared `tasks` capability from `ui/initialize`. `null` pre-handshake, `undefined` if the host did not advertise tasks, or the `TasksCapability` shape if it did. Read it to feature-detect before calling `callToolAsTask`.

## React Hooks

### `AppProvider`-based (Recommended)

Wrap your app with `` and use these hooks. Each is a thin wrapper over `connect()`.

```tsx
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 \| 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.

```tsx
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 |
| `useCallToolAsTask(name)` | `{ fire, task, result, error, isWorking, isTerminal, cancel }` | Lifecycle wrapper around `callToolAsTask` for long-running tools. See below. |

## Long-running tools (tasks)

For tools whose work exceeds the stock MCP request timeout (~60s) — research runs, batch imports, multi-stage analyses — use `callToolAsTask` (or `useCallToolAsTask` in React) instead of `callTool`. The host returns a `CreateTaskResult` immediately; the actual `CallToolResult` is fetched via `tasks/result` when the task reaches a terminal state.

```tsx
import { useCallToolAsTask } from "@nimblebrain/synapse/react";

function ResearchPanel() {
  const { fire, task, result, error, isWorking, isTerminal, cancel } =
    useCallToolAsTask("start_research");

  if (!task) return  fire({ query: "Q2 metrics" })}>Run;
  if (isWorking)

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [NimbleBrainInc](https://github.com/NimbleBrainInc)
- **Source:** [NimbleBrainInc/synapse](https://github.com/NimbleBrainInc/synapse)
- **License:** MIT
- **Homepage:** https://www.npmjs.com/package/@nimblebrain/synapse

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-nimblebraininc-synapse
- Seller: https://agentstack.voostack.com/s/nimblebraininc
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
