AgentStack
MCP verified MIT Self-run

Synapse

mcp-nimblebraininc-synapse · by NimbleBrainInc

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.

No reviews yet
0 installs
2 views
0.0% view→install

Install

$ agentstack add mcp-nimblebraininc-synapse

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Synapse? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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 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).

What is Synapse?

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

  • Zero-config handshakeawait 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

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

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

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:


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

// 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:

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

Handling Events

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

// 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:

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  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.

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

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

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.

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.

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.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.