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 |
| bun run dev —app | Full NimbleBrain platform with HMR | Testing data sync, agent interactions, multi-app |
| bun run 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 bun run dev |
| Widget state persistence | No — use bun run dev |
| Multi-app navigation | No — use bun run 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 (bun run dev --app)
Section titled “With UI hot-reload (bun run 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 - Restart the dev server so it reloads the bundle (
bun run devpicks up source changes automatically) - 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_appThen restart the dev server so the change takes effect (bun run dev restarts the API on source changes).
Debugging
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"] }}