Skip to content

Local Development

WorkflowWhat it gives youWhen to use
Synapse PreviewStandalone preview with tool calls and HMRBuilding a new app, iterating on UI
nb dev —appFull NimbleBrain platform with HMRTesting data sync, agent interactions, multi-app
nb dev + reloadFull platform, manual restartQuick tool changes, no UI build step
Tools onlyMCP server in Claude Desktop or any clientTesting tools without UI
Section titled “Synapse Preview (recommended for UI development)”

The fastest way to develop an MCP app with a UI. No NimbleBrain platform needed.

27246/__preview
cd your-app/ui
npm run dev

The Synapse Vite plugin (synapseVite() in your vite.config.ts) handles everything:

  • Reads ../manifest.json to get your app name and server command
  • Spawns the MCP server as a child process (stdio mode)
  • Serves a preview host page at /__preview that iframes your app
  • Proxies tool calls from the iframe to the MCP server
  • HMR works — edit .tsx files, see changes instantly
FeatureSupported
Tool calls (proxied to MCP server)Yes
ext-apps handshake (Synapse initializes)Yes
Dark/light theme toggleYes
NimbleBrain theme tokensYes
Console logging (chat, actions, state)Yes
Data sync (agent tool calls)No — use nb dev
Widget state persistenceNo — use nb dev
Multi-app navigationNo — use nb dev

The plugin reads ../manifest.json automatically. Zero config for most apps:

ui/vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";
import { synapseVite } from "@nimblebrain/synapse/vite";
export default defineConfig({
plugins: [react(), viteSingleFile(), synapseVite()],
build: { outDir: "dist", assetsInlineLimit: Infinity },
});

Override if needed:

synapseVite({
appName: "my-app", // override manifest name
serverCmd: "uv run python -m server", // override derived command
manifest: "../custom-manifest.json", // custom manifest path
preview: false, // disable preview (build-only)
})
Terminal window
cd nimblebrain # the platform repo
bun run dev --app /path/to/your-app/ui

Your app must already be configured as a local bundle in .nimblebrain/nimblebrain.json:

{
"bundles": [
{ "path": "/absolute/path/to/your-app" }
]
}

This gives you everything — data sync, agent interactions, theme injection, keyboard forwarding, multi-app navigation — plus Vite HMR on your UI.

Terminal window
bun run dev

The platform loads your bundle from the path in the config. UI changes require:

  1. Rebuild the UI: cd ui && npm run build
  2. Reload the bundle: nb reload
  3. Hard-refresh the browser (Cmd+Shift+R)

Test your MCP server in any client:

Terminal window
# Python
uv run python -m mcp_myapp.server
# TypeScript
node dist/index.js --stdio

Add to Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):

{
"mcpServers": {
"myapp": {
"command": "uv",
"args": ["run", "python", "-m", "mcp_myapp.server"],
"cwd": "/path/to/your-app"
}
}
}
  1. Create the MCP server (Python or TypeScript) with tools and a ui:// resource:

    @mcp.resource("ui://myapp/main")
    def app_ui() -> str:
    return load_ui() # reads ui/dist/index.html
  2. Create the UI project:

    Terminal window
    mkdir ui && cd ui
    npm init -y
    npm install react react-dom
    npm install -D @nimblebrain/synapse @vitejs/plugin-react vite vite-plugin-singlefile typescript @types/react @types/react-dom
  3. Configure Vite for single-file output:

    ui/vite.config.ts
    import { defineConfig } from "vite";
    import react from "@vitejs/plugin-react";
    import { viteSingleFile } from "vite-plugin-singlefile";
    export default defineConfig({
    plugins: [react(), viteSingleFile()],
    build: { outDir: "dist", assetsInlineLimit: Infinity },
    });
  4. Write your React app with Synapse hooks:

    ui/src/App.tsx
    import { SynapseProvider, useCallTool, useDataSync } from "@nimblebrain/synapse/react";
    function MyApp() {
    const { call, isPending, data } = useCallTool("my_tool");
    useDataSync(() => { /* refetch on agent changes */ });
    return <button onClick={() => call({ arg: "value" })} disabled={isPending}>Go</button>;
    }
    export function App() {
    return (
    <SynapseProvider name="myapp" version="1.0.0">
    <MyApp />
    </SynapseProvider>
    );
    }
  5. Build and preview:

    Terminal window
    cd ui && npm run build && cd ..
    npx @nimblebrain/synapse preview --server "uv run uvicorn mcp_myapp.server:app --port 8001" --ui ./ui

See the Hello World example for a complete walkthrough.

When you build your app for distribution (via mcpb-pack), dependencies are bundled into a deps/ directory. If your project has a deps/ directory, the platform uses it as the primary import path, placing it before src/ on PYTHONPATH.

This means edits to files in src/ are invisible to the running server — Python imports the stale copy from deps/ instead.

Fix: Delete the bundled copy of your package from deps/ during local development:

Terminal window
rm -rf deps/mcp_my_app
nb reload

Add this temporarily to see all bridge traffic:

window.addEventListener('message', (event) => {
console.log('[bridge]', event.data?.method, event.data);
});

In the Synapse Preview, ui/message, synapse/action, and ui/update-model-context messages are logged to the browser console automatically.

Your app runs inside a sandboxed iframe:

AllowedNot allowed
JavaScript executionHTML form submission
postMessage to hostTop-level navigation
Opening popups (new tabs)alert() / confirm() / prompt()
Inline styles and scriptsNested iframes

Network requests from the iframe are blocked by default (connect-src 'none'). If your app needs external API access, declare domains in your manifest:

{
"csp": {
"connectDomains": ["https://api.example.com"]
}
}