Nimblebrain
Self-hosted platform for MCP Apps and agent automations β tools, interactive UIs, scheduled runs, multi-agent delegation.
Ask AI about Nimblebrain
Powered by Claude Β· Grounded in docs
I know everything about Nimblebrain. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
NimbleBrain
A self-hosted platform for MCP Apps and agent automations. Install an MCP bundle and you get more than tools β you get an interactive UI in the sidebar with live agent-UI data sync, and the ability to run the agent on demand or on a cron schedule. Full ext-apps host support on top of an agentic loop with skill-driven prompt composition and multi-agent delegation.
Ships as container images on GHCR (ghcr.io/nimblebraininc/nimblebrain, ghcr.io/nimblebraininc/nimblebrain-web). Also exposes itself as an MCP server via Streamable HTTP so external MCP clients can consume the aggregated toolset.
Quick Start
Option 1: Docker (recommended)
# Prerequisites: Docker
export ANTHROPIC_API_KEY=sk-ant-...
docker compose up
# Pulls ghcr.io/nimblebraininc/nimblebrain + nimblebrain-web
# Web UI: http://localhost:27246
# API: http://localhost:27246/v1/health
Open http://localhost:27246 in your browser. Auth is configured via instance.json (see Configuration).
To build from source instead of pulling (e.g. when developing against local changes), run docker compose up --build.
Option 2: Local development
# Prerequisites: Bun (https://bun.sh), mpak CLI (https://mpak.dev), Node.js 22+
export ANTHROPIC_API_KEY=sk-ant-...
bun install
cd web && bun install && cd ..
bun run dev
# API on http://localhost:27247 (auto-restarts on file changes)
# Web on http://localhost:27246 (Vite HMR, proxies /v1/* to :27247)
One command, one terminal. Output is prefixed [api] / [web]. Ctrl+C stops both.
For API-only development (no web client):
bun run dev:api
Option 3: CLI only (no web)
bun install
bun run dev:tui # Interactive TUI (Ink)
How It Works
User message
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββ
β Runtime.chat() β
β β
β 1. Skill matching (triggers β keywords)β
β 2. System prompt composition β
β 3. Tool filtering (per-skill scoping) β
β 4. AgentEngine loop: β
β LLM call β tool execution β repeat β
β 5. Conversation persistence β
βββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
ChatResult { response, toolCalls, tokens, ... }
The engine loops until the model stops calling tools, hits the iteration limit (default 10, max 25), or exceeds the token budget.
How to Test and Verify
bun install
bun run verify # lint β typecheck β test β test:web β smoke
# Or individually:
bun run test # Unit + integration tests
bun run lint # Biome linter
bun run check # TypeScript strict mode
# Web client β build verification
cd web && bun install
bun run build # TypeScript + Vite build β dist/
# Docker β validate configs
docker compose config # Validate compose file
HTTP API
All endpoints require authentication (Bearer token or session cookie) unless noted. Auth is configured via instance.json β see the identity system docs.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /v1/health | No | Health check |
| GET | /v1/bootstrap | Yes | Bootstrap workspace context (user, workspaces, shell config) |
| POST | /v1/chat | Yes | Synchronous chat |
| POST | /v1/chat/stream | Yes | SSE streaming chat |
| GET | /v1/apps/:name/resources/:path | Yes | Fetch app UI resource |
| POST | /v1/tools/call | Yes | Direct tool invocation |
| GET | /v1/shell | Yes | Shell configuration (placements, endpoints) |
| GET | /v1/files/:fileId | Yes | Serve uploaded file |
| GET | /v1/events | Yes | SSE workspace event stream |
| GET | /v1/auth/authorize | No | OAuth authorization redirect |
| GET | /v1/auth/callback | No | OAuth callback handler |
| POST | /v1/auth/logout | Yes | Clear session cookie |
| POST | /v1/auth/refresh | No | Refresh access token |
| GET | /.well-known/oauth-protected-resource | No | MCP OAuth discovery (RFC 9728) |
| GET | /.well-known/oauth-authorization-server | No | AuthKit metadata proxy (RFC 8414) |
| POST/DELETE | /mcp | Yes | Streamable HTTP MCP server endpoint (GET returns 405; no standalone serverβclient SSE channel) |
Architecture
NimbleBrain is both an MCP client (connecting to installed bundles via stdio/HTTP) and an MCP server (exposing composed tools to external hosts via the /mcp Streamable HTTP endpoint). The ToolRegistry aggregates tools from all connected MCP servers into a single namespace, while skills scope tool access per task.
Three port interfaces isolate concerns:
| Port | Purpose | Implementations |
|---|---|---|
ModelPort | LLM provider | AnthropicModelAdapter (prompt caching), EchoModelAdapter (tests) |
ToolRouter | Tool discovery + execution | ToolRegistry (MCP sources + inline sources), StaticToolRouter (tests) |
EventSink | Observability | StructuredLogSink, WorkspaceLogSink, SseEventManager, ConsoleEventSink, CallbackEventSink, DebugEventSink, NoopEventSink |
System Tools
All system tools are prefixed with nb__ (the nb source name + __ separator).
| Tool | Purpose |
|---|---|
nb__status | Platform status: overview, bundles, skills, or config (scope param) |
nb__search | Unified search: installed tools or mpak registry (scope param) |
nb__read_resource | Read a skill:// / ui:// resource from an installed app's MCP server |
nb__set_model_config | Update model provider and limits (admin only) |
nb__set_preferences | Set user preferences (name, timezone, theme) |
nb__manage_app | Install, uninstall, or configure an app |
nb__manage_skill | Create, edit, delete user skills |
nb__delegate | Spawn child agent for sub-tasks (multi-agent) |
nb__briefing | Generate personalized activity briefing |
nb__manage_users | Create or delete users (admin only) |
nb__manage_workspaces | Workspace CRUD + member management + conversation sharing (admin only) |
Additional internal tools (UI-only, hidden from LLM) are listed in Architecture Reference.
Skills
Skills are markdown files with YAML frontmatter. They inject system prompts and scope tool access:
---
name: my-skill
description: What this skill does
metadata:
triggers: ["exact phrase match"]
keywords: [fuzzy, keyword, matching]
category: domain
allowed-tools: ["server__*"]
---
# System prompt content injected when this skill matches
Skill matching is two-phase:
- Triggers β exact substring match on the user message (first hit wins)
- Keywords β count keyword hits, require minimum 2 to qualify
Two categories of skills:
- Core (
src/skills/core/) β always injected into the system prompt (e.g.,bootstrap.mdteaches meta-tool usage) - User-matchable β loaded from
src/skills/builtin/(currently empty),~/.nimblebrain/skills/, and config-specified directories
Bundles
Bundles are MCPB-format MCP servers. They can be:
- Named β downloaded and cached via
mpak run @scope/name - Local β resolved from a path on disk
- Remote β connected via Streamable HTTP or SSE transport (distributed MCP servers)
Local and named bundles spawn as subprocesses communicating via stdio (MCP JSON-RPC 2.0). Remote bundles connect over HTTP. All three types are aggregated into the same unified tool namespace by the ToolRegistry.
No MCP bundles are installed by default. Platform capabilities (home, conversations, files, settings, usage, automations) are built in as inline tool sources (see src/tools/platform/). Install bundles explicitly via the mpak registry or a local path. Tool visibility follows the tiered surfacing rules described under Tiered Tool Surfacing.
Configuration
NimbleBrain splits configuration across two files:
nimblebrain.jsonβ instance-level settings (models, HTTP, logging, limits, feature flags). One file per deployment.workspace.jsonβ per-workspace settings (bundles, skill directories, named agent profiles, optional model + identity overrides). One file per workspace under<workDir>/workspaces/<ws-id>/.
This split is the workspace isolation boundary: two workspaces in the same deployment can install different bundles and agents without touching the instance config. See Workspace Isolation below.
nimblebrain.json (instance config)
Create a nimblebrain.json in your working directory. A minimal file:
{
"$schema": "https://schemas.nimblebrain.ai/v1/nimblebrain-config.schema.json",
"version": "1"
}
A fully specified example:
{
"$schema": "https://schemas.nimblebrain.ai/v1/nimblebrain-config.schema.json",
"version": "1",
"models": {
"default": "anthropic:claude-sonnet-4-6",
"fast": "anthropic:claude-haiku-4-5-20251001",
"reasoning": "anthropic:claude-opus-4-6"
},
"providers": {
"anthropic": { "apiKey": "sk-ant-..." },
"openai": { "apiKey": "sk-..." }
},
"http": { "port": 27247, "host": "127.0.0.1" },
"logging": { "dir": "~/.nimblebrain/logs", "level": "normal", "retentionDays": 30 },
"store": { "type": "jsonl", "dir": "~/.nimblebrain/conversations" },
"telemetry": { "enabled": true },
"files": { "maxFileSize": 26214400, "maxFilesPerMessage": 10 },
"features": { "bundleManagement": true },
"maxIterations": 25,
"maxInputTokens": 500000,
"maxOutputTokens": 16384,
"workDir": "~/.nimblebrain"
}
Model slots. models takes three named slots β default (chat / general), fast (title generation, briefings, skill matching), and reasoning (complex analysis). Each is a provider:model-id string. providers supplies per-provider API keys when you want to mix providers across slots. The older single-model / defaultModel shape is still accepted for backward compatibility but is deprecated.
Feature flags. All default to true. Disable a flag to remove the capability entirely β the tool is unregistered, not visible to the LLM, and POST /v1/tools/call returns 403. See Feature Flags for the full set.
Deprecated fields. identity and contextFile are ignored with a warning β use a skill with type: "context" instead.
workspace.json (per-workspace config)
Each workspace has its own config at <workDir>/workspaces/<ws-id>/workspace.json. In dev mode (no instance.json), the runtime uses a single _dev workspace.
{
"id": "ws_product",
"name": "Product",
"members": [{ "userId": "usr_default", "role": "admin" }],
"bundles": [
{ "name": "@nimblebraininc/ipinfo" },
{ "path": "../mcp-servers/hello" }
],
"skillDirs": ["./skills"],
"agents": {
"researcher": {
"description": "Research agent",
"systemPrompt": "You are a research agent...",
"tools": ["search__*"],
"maxIterations": 8
}
},
"models": { "default": "anthropic:claude-opus-4-6" },
"identity": { "name": "Acme Copilot" }
}
bundles, skillDirs, agents, and optional models / identity overrides live here, not in nimblebrain.json. Entries placed at the top level of nimblebrain.json are silently stripped on load β the runtime treats them as configuration errors rather than falling back to a global scope.
Workspace Isolation
Bundles, tool registries, and conversation data are scoped to a workspace. Every tool handler resolves its workspace via runtime.requireWorkspaceId() before touching data. In dev mode this returns "_dev"; behind auth it resolves from the request's session or API key.
Two workspaces that install the same bundle spawn independent subprocesses with data directories under <workDir>/workspaces/<wsId>/data/<bundle>/, so their entity data never crosses. Sidebar placements, briefing facets, and the app list are filtered per workspace.
CLI Commands
nb Interactive TUI (default) / headless pipe mode
nb serve HTTP API server (production)
nb dev Dev mode: API with file watching + web HMR
nb bundle list|add|remove|search Manage bundles
nb skill list|info Inspect skills
nb config set|get|clear Configure per-bundle credentials (requires `-w <wsId>`)
nb status Workspace status
nb reload Hot-reload bundles and config
nb telemetry on|off|status|reset Manage anonymous telemetry
nb automation Manage automation rules
Run nb --help or nb <command> --help for full usage. If you haven't run bun link, use bun run src/cli/index.ts instead of nb.
CLI Flags
| Flag | Scope | Purpose |
|---|---|---|
--config <path> | Global | Config file (default: ./nimblebrain.json) |
--model <id> | Global | Override default model |
--workdir <path> | Global | Override working directory |
--debug | Global | Enable debug event logging |
--help | Global | Print help and exit |
--json | Headless + subcommands | Structured JSON output |
--resume <id> | TUI/headless | Resume a previous conversation |
--port <number> | serve, dev | HTTP server port (default: 27247) |
--no-web | dev | Skip web dev server (API only) |
Environment Variables
Model providers
| Variable | Purpose |
|---|---|
ANTHROPIC_API_KEY | Anthropic API key (required unless set via providers.anthropic.apiKey) |
OPENAI_API_KEY | OpenAI API key (when using openai:* model slots) |
GOOGLE_GENERATIVE_AI_API_KEY | Google Gemini API key (when using google:* model slots) |
Runtime
| Variable | Purpose |
|---|---|
NB_WORK_DIR | Override working directory (takes precedence over config and --workdir) |
ALLOWED_ORIGINS | Comma-separated allowed CORS origins (for cookie-based auth) |
MCP_MAX_SESSIONS | Max concurrent MCP sessions (default: 100) |
MCP_SESSION_TTL_MS | MCP session inactivity TTL in ms (default: 1800000) |
NB_CHAT_RATE_LIMIT | Chat requests per minute per user (default: 20) |
NB_TOOL_RATE_LIMIT | Tool calls per minute per user (default: 60) |
NB_BUNDLE_START_CONCURRENCY | Max bundle subprocesses spawned in parallel at boot (default: 4, set to 1 for sequential) |
NB_TIMEZONE | Default IANA timezone for time-aware features |
NB_HOST_URL | Public host URL for OAuth redirects |
NB_HSTS | Strict-Transport-Security value (default: max-age=31536000; includeSubDomains). Set to "" to disable β e.g., when a reverse proxy already emits this header |
NB_CSP | Content-Security-Policy value (default: default-src 'none'; frame-ancestors 'none'; base-uri 'none'). Set to "" to disable |
Identity & telemetry
| Variable | Purpose |
|---|---|
WORKOS_API_KEY | WorkOS API key (when auth.adapter: "workos" in instance.json) |
NB_INTERNAL_TOKEN | Shared secret for service-to-service calls (never forwarded to bundles) |
TURNSTILE_SECRET_KEY | Cloudflare Turnstile secret (CAPTCHA on auth endpoints) |
POSTHOG_API_KEY | PostHog key for anonymous product telemetry |
NB_TELEMETRY_DISABLED | Set to 1 to disable telemetry (also DO_NOT_TRACK=1) |
Headless / Pipe Mode
When stdin is not a terminal (piped), the CLI runs in headless mode:
echo "What is 2 + 2?" | bun run dev:tui
# Multi-turn (conversation carried across lines)
printf "Hello\nWhat did I just say?\n" | bun run dev:tui
# Structured JSON output
echo "List files" | bun run dev:tui -- --json
Each line of stdin is one message. The conversation ID is carried across lines automatically. Responses go to stdout (plain text by default, JSON objects with --json). Logs go to stderr. EOF exits cleanly.
Programmatic API
import { Runtime } from "nimblebrain";
const runtime = await Runtime.start({
model: { provider: "anthropic" },
store: { type: "memory" },
});
const result = await runtime.chat({ message: "What can you help me with?" });
console.log(result.response);
await runtime.shutdown();
Key Types
interface ChatRequest {
message: string;
conversationId?: string; // Resume existing conversation
model?: string; // Override model for this request
maxIterations?: number; // Override iteration limit
workspaceId?: string; // Target workspace
fileRefs?: FileReference[]; // Attached files for context
contentParts?: ContentPart[];
metadata?: Record<string, unknown>;
}
interface ChatResult {
response: string;
conversationId: string;
workspaceId?: string;
skillName: string | null;
toolCalls: Array<{
id: string;
name: string;
input: Record<string, unknown>;
output: string;
ok: boolean;
ms: number;
}>;
inputTokens: number;
outputTokens: number;
stopReason: string;
usage: TurnUsage;
}
Project Structure
src/
βββ index.ts Public API exports
βββ engine/ Agentic loop (model β tool β repeat)
β βββ engine.ts AgentEngine class
β βββ types.ts ModelPort, ToolRouter, EventSink interfaces
β βββ tasks.ts MCP Tasks client (polling, progress, cancellation)
β βββ cost.ts Token cost estimation by model
βββ runtime/ High-level orchestration
β βββ runtime.ts Runtime.start() β runtime.chat()
β βββ types.ts RuntimeConfig, ChatRequest, ChatResult
β βββ tools.ts filterTools (skill-scoped tool filtering)
β βββ features.ts Feature flags resolution and tool gating
β βββ env-filter.ts Bundle env var allowlist/filter
β βββ workspace-runtime.ts Per-workspace bundle spawning
βββ identity/ Authentication adapters
β βββ provider.ts IdentityProvider interface, UserIdentity type
β βββ providers/dev.ts Dev mode (no auth)
β βββ providers/oidc.ts OIDC provider (JWT verification)
β βββ providers/workos.ts WorkOS provider (OAuth + AuthKit MCP)
β βββ instance.ts Instance configuration loading
βββ workspace/ Multi-tenant workspace system
β βββ workspace-store.ts Workspace CRUD operations
β βββ types.ts Workspace, WorkspaceMember, WorkspaceRole
β βββ scaffold.ts Workspace initialization helpers
βββ bundles/ MCPB bundle lifecycle
β βββ lifecycle.ts Bundle install/uninstall/start/stop state machine
β βββ manifest.ts MCPB manifest validation (ajv, v0.3/v0.4)
β βββ resolve.ts Local bundle resolution
β βββ types.ts BundleRef, BundleManifest, BundleInstance
β βββ schemas/ Vendored MCPB JSON Schemas (v0.3, v0.4)
βββ api/ HTTP API (Hono framework)
β βββ app.ts Hono app factory, route registration
β βββ server.ts HTTP server startup
β βββ auth-middleware.ts Auth middleware with workspace resolution
β βββ handlers.ts Route handler implementations
β βββ events.ts SSE event manager (broadcast, heartbeat)
β βββ routes/ Modular route files (auth, chat, bootstrap, etc.)
β βββ middleware/ Hono middleware (CORS, etc.)
βββ tools/ Tool definitions
β βββ system-tools.ts System tools factory (search, manage, delegate)
β βββ delegate.ts nb__delegate multi-agent tool
β βββ registry.ts ToolRegistry (aggregates MCP sources)
β βββ workspace-mgmt-tools.ts Workspace management tools
β βββ user-tools.ts User management tools
β βββ conversation-tools.ts Conversation sharing tools
βββ adapters/ Pluggable implementations
β βββ structured-log-sink.ts Per-conversation JSONL logs with cost
β βββ workspace-log-sink.ts Workspace-level daily JSONL logs
β βββ console-events.ts Stderr event logging
β βββ callback-events.ts Callback-based events (Ink UI)
β βββ debug-events.ts Verbose debug logging
β βββ noop-events.ts Silent event sink
βββ files/ File context extraction
β βββ types.ts File config, supported formats (PDF, DOCX, etc.)
βββ skills/ Skill discovery and matching
β βββ loader.ts File parsing (YAML frontmatter + markdown)
β βββ matcher.ts Two-phase matching (triggers β keywords)
β βββ types.ts Skill, SkillManifest, SkillMetadata
β βββ core/ Core skills (always injected, e.g. bootstrap.md)
βββ conversation/ Message persistence
β βββ event-sourced-store.ts Event-sourced store (persists engine events)
β βββ jsonl-store.ts Append-only JSONL (one file per conversation)
β βββ memory-store.ts In-memory (ephemeral)
β βββ window.ts History windowing (sliceHistory)
β βββ types.ts ConversationStore interface
βββ prompt/ System prompt composition
β βββ compose.ts Multi-layer: identity β core skills β apps β skill
βββ model/ LLM provider management
β βββ registry.ts Provider registry (AI SDK createProviderRegistry)
β βββ stream.ts doStream helper β calls model, emits text deltas
βββ telemetry/ Anonymous product telemetry
β βββ posthog-sink.ts PostHog event mapping
β βββ manager.ts TelemetryManager (opt-in/out, anonymous ID)
βββ cli/ Interactive + headless terminal interface
βββ index.ts Entry point (Commander program assembly)
βββ config.ts nimblebrain.json loading
βββ commands/ One file per command group
βββ dev.ts nb dev dual-process supervisor
βββ app.tsx Ink (React) UI component
βββ markdown.tsx Lightweight markdown renderer for Ink
Deployment
Docker Compose
export ANTHROPIC_API_KEY=sk-ant-...
export ALLOWED_ORIGINS=http://localhost:27246 # for cookie-based auth
docker compose up
# Platform: internal only (API), Web: localhost:27246 (UI)
Images are published to GHCR on every release:
ghcr.io/nimblebraininc/nimblebrainβ runtime (Bun + Python 3.13 + Node 22 + mpak)ghcr.io/nimblebraininc/nimblebrain-webβ Caddy serving the SPA, proxying/v1/*to the platform
Each release is tagged with the version (e.g. v1.2.3) and the short git SHA. Stable releases also move :latest forward; pre-releases (e.g. v0.4.0-beta.1) do not. Pin to a version tag in production. Pass --build to docker compose to build from source instead.
See Dockerfile, web/Dockerfile, and docker-compose.yml for full config.
Architecture Reference
This section contains detailed internal architecture documentation for contributors.
Token Budget Behavior
When cumulative input tokens exceed maxInputTokens, the engine returns immediately with stopReason: "token_budget". Tool calls from the current LLM response are dropped (not executed) to avoid running tools whose results can't be processed.
Tiered Tool Surfacing
When total tools β€30, all are surfaced directly. Above 30 with no skill matched, only nb__* tools are direct (rest via proxy). When a skill matches with allowed-tools, matching tools + system tools are direct. Configurable via maxDirectTools (default 30). Implementation in src/runtime/tools.ts.
Internal System Tools (UI-only, hidden from LLM)
| Tool | What it does |
|---|---|
nb__list_apps | List installed apps with status, tools, trust scores |
nb__get_config | Get runtime configuration (providers, model, limits) |
nb__manage_identity | Write or reset workspace agent identity override (admin only) |
nb__version | Platform version info |
nb__workspace_info | Workspace metadata, telemetry status |
Bundle Lifecycle
BundleLifecycleManager (src/bundles/lifecycle.ts) tracks bundle states:
- Install: mpak download β read manifest β extract UI metadata from
_meta["ai.nimblebrain/host"]β record trust score β spawn MCP server β register β atomic config write β emit event - Uninstall: check protected β stop server β remove source β atomic config removal β emit event (data NOT deleted)
- States: starting β running β crashed β dead (+ stopped for manual stop)
- Atomic writes: config changes use write-temp-then-rename
Multi-Agent Delegation
nb__delegate (src/tools/delegate.ts) spawns child AgentEngine.run() with scoped prompt and filtered tools. Named agent profiles configured in nimblebrain.json under agents. Child iteration budget capped at min(child.max, parent.remaining - 1). Multiple delegations in the same turn run concurrently via Promise.all().
MCP Tasks Client
src/engine/tasks.ts detects CreateTaskResult from MCP tool calls. Polls tasks/get until terminal state (completed/failed/cancelled). Emits tool.progress events during polling. Cancels active tasks on engine abort.
Conversation Storage
InMemoryConversationStoreβ default for programmatic useJsonlConversationStoreβ default for CLI, files in~/.nimblebrain/conversations/. Line 1:{ id, createdAt }metadata. Lines 2+:StoredMessageobjects.EventSourcedConversationStoreβ persists engine events as JSONL. Append-only after creation. Token totals, cost, and last model derived at read time fromllm.responseevents viaderiveUsageMetrics(). Supports multi-user conversations with ownership, visibility (private/shared), and participant management.
User-uploaded files are persisted in the workspace FileStore and referenced from user.message events as MCP resource_link blocks ({type:"resource_link", uri:"files://<id>", mimeType, name}) β the conversation log never carries inline bytes. At the model.doStream boundary the runtime rehydrates image links to AI SDK V3 file parts with bytes loaded from the store, so vision content survives across multi-turn agent loops without inflating the JSONL. Files are also addressable as MCP resources at files://<id> (any client can fetch via resources/read).
Identity System
Pluggable authentication via IdentityProvider interface (src/identity/provider.ts). Configured via instance.json in the work directory:
devβ No auth, default when noinstance.jsonexists. All requests get a default identity.oidcβ JWT verification via any OIDC provider. Auto-provisions users on first valid login.workosβ Full OAuth code flow with PKCE, token refresh, managed users via WorkOS. Supports MCP OAuth for external client access via AuthKit.
Each request carries a UserIdentity (id, name, email, role) threaded through AppContext in Hono middleware.
Workspace System
Multi-tenant workspace isolation (src/workspace/). Key types: Workspace, WorkspaceMember, WorkspaceRole (owner, admin, member).
Bundles can be installed per-workspace (tracked via BundleInstance.wsId). Each workspace gets its own ToolRegistry with unqualified tool names. WorkspaceRuntime handles per-workspace bundle spawning.
createSystemTools() takes getRegistry: () => ToolRegistry (callback) instead of a direct registry reference, enabling dynamic workspace-scoped registries. The runtime maintains a _workspaceRegistries map keyed by workspace ID.
Workspace isolation in tool handlers: All tool handlers that access data must use runtime.requireWorkspaceId() (throws if missing). Do not use getCurrentWorkspaceId() (nullable) or getBundleInstances() (unfiltered) in tool handlers. In dev mode, requireWorkspaceId() returns "_dev".
System Prompt Composition
src/prompt/compose.ts joins layers with ---:
- Layer 0: Identity β context skills or default fallback
- Layer 1: Core skills β always present (bootstrap.md teaches meta-tool usage)
- Layer 2: Installed Apps β dynamically injected list with UI status and MTF trust scores
- Layer 3: Matched skill system prompt
HTTP API Internals
Authentication: Bearer token via Authorization header or HttpOnly session cookie (nb_session). Cookie attributes: HttpOnly, SameSite=Lax, Secure in production. Bearer header takes precedence over cookie.
CORS: Dynamic. Dev mode: Access-Control-Allow-Origin: *. With auth: only ALLOWED_ORIGINS env var origins, with credentials support.
MCP endpoint (/mcp): Streamable HTTP. The bundled web UI uses this endpoint to drive the platform, and external MCP clients (Claude Code, Claude Desktop, Cursor) can connect to the same endpoint. 100 concurrent sessions (env: MCP_MAX_SESSIONS), 30-minute TTL (env: MCP_SESSION_TTL_MS). When authkitDomain is configured, returns WWW-Authenticate header on 401 for automatic OAuth discovery by MCP clients. Full setup guide: MCP Endpoint and Connecting External Clients on docs.nimblebrain.ai.
Deploying behind a TLS-terminating proxy: OAuth discovery advertises its resource URL from X-Forwarded-Proto, falling back to the request scheme. Upstream proxies (AWS ALB, nginx, Cloudflare, etc.) must set that header to the client-facing scheme (https) for MCP OAuth to work. The bundled nimblebrain-web Caddy container already honors it via trusted_proxies static private_ranges; if you front it with an additional proxy, ensure that proxy also propagates X-Forwarded-Proto. Without this, /.well-known/oauth-protected-resource returns resource: http://... and modern MCP clients reject the response. See MCP OAuth behind a reverse proxy for ALB / nginx / Caddy snippets.
MCP OAuth discovery endpoints:
GET /.well-known/oauth-protected-resourceβ RFC 9728 Protected Resource MetadataGET /.well-known/oauth-authorization-serverβ RFC 8414 Authorization Server Metadata (proxied from AuthKit)
SSE Event Streams
Workspace-level (GET /v1/events): Events: bundle.installed, bundle.uninstalled, bundle.crashed, bundle.recovered, bundle.dead, data.changed, config.changed, skill.created, skill.updated, skill.deleted, file.created, file.deleted, bridge.tool.call, bridge.tool.done, heartbeat (30s).
Per-conversation (GET /v1/conversations/:id/events): For multi-participant chat. Security: requireAuth β requireWorkspace β canAccess(). Events: user.message, text.delta, tool.start, tool.done, llm.done, done, heartbeat. Sender excluded from own broadcast.
Web Client Internals
- Chat: editorial conversation style (serif assistant, italic user), streaming via SSE, inline tool call display
- MCP App Bridge: sandboxed iframes, postMessage proxy for tool calls
- Agent-UI sync:
data.changedevents forwarded to iframes with 100ms debounce - Login:
"__cookie__"sentinel token indicates cookie-based auth (suppresses Authorization header)
Sidebar Slot Convention
The sidebar is data-driven from the placement registry:
| Slot | Purpose | Example |
|---|---|---|
sidebar (priority < 10) | Ungrouped core nav at top | Home (0), Conversations (1) |
sidebar (priority >= 10) | Grouped under "general" label | β |
sidebar.<group> | Named group | sidebar.apps β "Apps" |
sidebar.bottom | Pinned to bottom zone | Settings |
main | App routes (pages, not nav) | Third-party apps |
Placements with a route field get React Router routes in App.tsx. Routes from sidebar use /app/<route> (except Home β /).
Configuration Reference
Files:
nimblebrain.jsonβ instance config. Validated at startup againstsrc/config/nimblebrain-config.schema.json(JSON Schema draft-07, AJV). Unknown keys warn; structural errors throw. Workspace-owned fields (bundles,skillDirs,agents,preferences,home,noDefaultBundles) are silently stripped on load.identityandcontextFileare deprecated with a warning.<workDir>/workspaces/<wsId>/workspace.jsonβ per-workspace config. Ownsbundles,skillDirs,agents, and optionalmodels/identityoverrides.<workDir>/instance.jsonβ auth configuration (OIDC or WorkOS adapter). Absence signals dev mode.
Config resolution for nimblebrain.json (when no --config flag):
--workdir <dir>β<dir>/nimblebrain.json- Otherwise β
./nimblebrain.json(CWD)
NB_WORK_DIR overrides workDir from either the config file or --workdir.
Bundle Entry Fields (in workspace.json)
Each entry in workspace.json β bundles[] accepts:
| Field | Type | Description |
|---|---|---|
name | string | Bundle name from the mpak registry |
path | string | Local filesystem path (resolved relative to the config file) |
url | string | Remote MCP server URL (HTTPS; HTTP blocked unless allowInsecureRemotes) |
env | object | Environment variables passed to the bundle process |
allowedEnv | string[] | Host env vars this bundle may read |
protected | boolean | Prevents uninstall via nb__manage_app |
trustScore | number|null | MTF trust score (0-100) |
ui | object|null | UI metadata: { name, icon, primaryView? } |
Feature Flags
All default to true. Setting to false removes the capability entirely β tool not registered, not visible to LLM, returns 403 via HTTP.
| Flag | Controls | Tool(s) Affected |
|---|---|---|
bundleManagement | Install/uninstall/configure apps | nb__manage_app |
skillManagement | Create/edit/delete skills | nb__manage_skill |
delegation | Multi-agent delegation | nb__delegate |
toolDiscovery | Tool search (scope=tools) | nb__search |
bundleDiscovery | Registry search (scope=registry) | nb__search |
fileContext | File upload and context extraction | File processing |
userManagement | Create/delete users | nb__manage_users |
workspaceManagement | Workspaces, members, sharing | nb__manage_workspaces |
Enforcement: Three layers β (1) tools excluded from registry at startup, (2) POST /v1/tools/call returns 403, (3) MCP ListTools filters and CallTool rejects. Read-only tools (nb__status) are never gated.
Bundle Env Isolation
Bundle processes receive a filtered host environment. Default allowlist: PATH, HOME, USER, SHELL, LANG, LC_ALL, LC_CTYPE, TERM, TMPDIR, TZ, XDG_DATA_HOME, XDG_CONFIG_HOME, NODE_ENV, BUN_ENV, NB_WORK_DIR, UPJACK_ROOT, PYTHONPATH, VIRTUAL_ENV, NODE_PATH. Hard deny (never passed): NB_API_KEY, NB_INTERNAL_TOKEN. Opt in via allowedEnv in bundle config.
Remote Bundle Security
- Protocol must be
https:(SSRF protection) - Private IP ranges rejected:
10.x,172.16-31.x,192.168.x,169.254.x,::1 - Cloud metadata hostnames rejected
- Embedded credentials rejected
- Dev exception:
"allowInsecureRemotes": trueallowshttp://localhost
Source Name Protection
- Reserved prefix β
nbcannot be used as a bundle source name - No duplicate sources β registry rejects duplicates; built-in bundles register first
MCP App Bridge Invariants
These are non-negotiable patterns. Violating them causes production bugs:
tools/callmust returnCallToolResultas-is β never unwrap or cherry-pick fields- No
data.changedfrom tool proxy β causes infinite loops (tool β SSE β iframe refresh β tool) - Tool errors β JSON-RPC errors β
isError: truemust send error response, not result - Bridge
destroyedflag β React StrictMode double-mounts; guard listeners withdestroyedboolean - Iframe DOM isolation β never put React-managed children in same container as raw DOM iframes
- SlotRenderer effect depends only on
placementKeyβ callbacks via refs, not dep array (prevents flickering) - Shell components must not consume
ChatContextβ useChatConfigContext(stable) to avoid re-renders during streaming "primary"virtual path βGET /v1/apps/:name/resources/primaryresolves toprimaryView.resourceUrifrom manifest- Spec methods only β use ext-apps spec method names in bridge; NimbleBrain extensions use
synapse/prefix ui/initializefield names βhostInfo(notserverInfo),hostCapabilities(notcapabilities),hostContext.themeis string
Conventions
- Runtime: Bun (not Node). Use
bun run,bun test,bunx. - Module system: ESM only. All imports use
.tsextensions. - Linting: Biome (not ESLint/Prettier).
- Type checking:
bunx tsc --noEmit. Strict mode. - Testing: Bun's built-in test runner. Use
createEchoModel()andStaticToolRouterto avoid LLM calls. - Model types: Vercel AI SDK V3 types from
@ai-sdk/provider. - HTTP: Hono. Typed context via
AppEnv/AuthEnv. - No classes for data β plain interfaces + factory functions.
- Tool results:
structuredContentfor typed data,contentfor human-readable summary. - Prompt security:
sanitizeLineField()and XML containment tags incompose.tsβ do not remove without reviewingtest/unit/prompt-injection.test.ts.
Defaults
| Setting | Value |
|---|---|
models.default | anthropic:claude-sonnet-4-6 |
models.fast | anthropic:claude-haiku-4-5-20251001 |
models.reasoning | anthropic:claude-opus-4-6 |
| Max iterations | 25 (hard cap: 50) |
| Max input tokens | 500,000 |
| Max output tokens | 16,384 |
| Max history messages | 40 |
| Max tool result size | 1,000,000 chars (0 disables) |
| Default bundles | none (platform capabilities are built in) |
| Work directory | ~/.nimblebrain |
| HTTP port | 27247 |
| HTTP host | 127.0.0.1 |
| Conversation store (CLI) | JSONL in ~/.nimblebrain/conversations/ |
| Conversation store (programmatic) | In-memory |
Dependencies
| Package | Purpose |
|---|---|
ai | Vercel AI SDK core (provider registry, types) |
@ai-sdk/anthropic | Anthropic provider (prompt caching, streaming) |
@ai-sdk/openai | OpenAI provider |
@ai-sdk/google | Google Gemini provider |
@modelcontextprotocol/sdk | MCP client (stdio transport) |
ajv + ajv-formats | JSON Schema validation for MCPB manifests |
gray-matter | YAML frontmatter parsing for skill files |
ink | React-based terminal UI |
posthog-node | Anonymous product telemetry (server-side) |
posthog-js | Anonymous product telemetry (web client) |
hono | HTTP framework (routing, middleware, typed context) |
Observability
StructuredLogSinkβ Per-conversation JSONL logs with LLM/tool latency, cache tokens, cost. Disable withlogging.disabled: true.WorkspaceLogSinkβ Workspace-level daily rolling JSONL logs. Only persists workspace events (bundle lifecycle, data/config changes, skill/file operations).ConsoleEventSinkβ Human-readable stderr for development.DebugEventSinkβ Verbose JSON dumps (--debug).CallbackEventSinkβ Bridges events into React state (Ink UI).PostHogEventSinkβ Anonymous telemetry. No PII. Opt-out:telemetry.enabled: false,NB_TELEMETRY_DISABLED=1, orDO_NOT_TRACK=1.
