Local Development
Choose your workflow
Section titled “Choose your workflow”| Workflow | What it gives you | When to use |
|---|---|---|
| Synapse Preview | Standalone preview with tool calls and HMR | Building a new app, iterating on UI |
| nb dev —app | Full NimbleBrain platform with HMR | Testing data sync, agent interactions, multi-app |
| nb dev + reload | Full platform, manual restart | Quick tool changes, no UI build step |
| Tools only | MCP server in Claude Desktop or any client | Testing tools without UI |
Synapse Preview (recommended for UI development)
Section titled “Synapse Preview (recommended for UI development)”The fastest way to develop an MCP app with a UI. No NimbleBrain platform needed.
cd your-app/uinpm run devThe Synapse Vite plugin (synapseVite() in your vite.config.ts) handles everything:
- Reads
../manifest.jsonto get your app name and server command - Spawns the MCP server as a child process (stdio mode)
- Serves a preview host page at
/__previewthat iframes your app - Proxies tool calls from the iframe to the MCP server
- HMR works — edit
.tsxfiles, see changes instantly
What the preview provides
Section titled “What the preview provides”| Feature | Supported |
|---|---|
| Tool calls (proxied to MCP server) | Yes |
| ext-apps handshake (Synapse initializes) | Yes |
| Dark/light theme toggle | Yes |
| NimbleBrain theme tokens | Yes |
| Console logging (chat, actions, state) | Yes |
| Data sync (agent tool calls) | No — use nb dev |
| Widget state persistence | No — use nb dev |
| Multi-app navigation | No — use nb dev |
Vite config
Section titled “Vite config”The plugin reads ../manifest.json automatically. Zero config for most apps:
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)})NimbleBrain dev mode
Section titled “NimbleBrain dev mode”With UI hot-reload (nb dev --app)
Section titled “With UI hot-reload (nb dev --app)”cd nimblebrain # the platform repobun run dev --app /path/to/your-app/uiYour 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.
Without UI hot-reload
Section titled “Without UI hot-reload”bun run devThe platform loads your bundle from the path in the config. UI changes require:
- Rebuild the UI:
cd ui && npm run build - Reload the bundle:
nb reload - Hard-refresh the browser (
Cmd+Shift+R)
Tools only (no UI)
Section titled “Tools only (no UI)”Test your MCP server in any client:
# Pythonuv run python -m mcp_myapp.server
# TypeScriptnode dist/index.js --stdioAdd 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" } }}# Pythonuv run uvicorn mcp_myapp.server:app --port 8001
# TypeScriptnode dist/index.js --port 8001Test with curl:
curl -X POST http://localhost:8001/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":"1","method":"tools/list","params":{}}'Setting up a new app with UI
Section titled “Setting up a new app with UI”-
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 -
Create the UI project:
Terminal window mkdir ui && cd uinpm init -ynpm install react react-domnpm install -D @nimblebrain/synapse @vitejs/plugin-react vite vite-plugin-singlefile typescript @types/react @types/react-dom -
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 },}); -
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>);} -
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.
The deps/ gotcha
Section titled “The deps/ gotcha”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:
rm -rf deps/mcp_my_appnb reloadDebugging
Section titled “Debugging”Bridge messages
Section titled “Bridge messages”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.
Iframe sandbox restrictions
Section titled “Iframe sandbox restrictions”Your app runs inside a sandboxed iframe:
| Allowed | Not allowed |
|---|---|
| JavaScript execution | HTML form submission |
postMessage to host | Top-level navigation |
| Opening popups (new tabs) | alert() / confirm() / prompt() |
| Inline styles and scripts | Nested 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"] }}