BrainLLM needs a running TriliumNext instance and Bun 1.0+. It uses Bun APIs directly and does not run on Node.
Install TriliumNext locally, or run it anywhere reachable over HTTP. Then create a token: Options → ETAPI → Create token.
You can skip the token step by setting TRILIUM_PASSWORD instead — BrainLLM mints a token on first start and caches it.
# via npm (needs bun on PATH) npx -y brainllm # or from source git clone https://github.com/miisodev/BrainLLM cd BrainLLM && bun install && bun run build
Add BrainLLM to your MCP client's config. The npm form is simplest:
{
"mcpServers": {
"BrainLLM": {
"command": "npx",
"args": ["-y", "brainllm"],
"env": {
"TRILIUM_BASE_URL": "http://localhost:8080",
"TRILIUM_ETAPI_TOKEN": "your-token-here"
}
}
}
}
On a fresh Trilium, call bootstrap() once from your client — or run bun run init from a checkout. It builds the five-area tree, engraves each container's purpose, and writes brainllm.json.
It is safe to call anytime. It verifies an existing root live and only builds a fresh tree on a confirmed 404, so a network blip can never silently produce a duplicate brain.
The tools are deliberately terse — single-word verbs. The operational discipline lives in the bundled skill at skills/brainllm/. Copy it to ~/.claude/skills/brainllm so Claude Code and Cowork load it.
Without it a model can call every tool correctly and still produce an incoherent brain, because correctness of call is not correctness of practice.
| Variable | Required | Purpose |
|---|---|---|
TRILIUM_BASE_URL | Yes | URL of your TriliumNext instance |
TRILIUM_ETAPI_TOKEN | Yes* | ETAPI bearer token |
TRILIUM_PASSWORD | — | * Alternative to the token — mints and caches one on first start |
BRAINLLM_MODE | — | core (42 tools, default) or full (+33 raw ETAPI tools) |
BRAINLLM_TZ | — | IANA timezone for date stamping on hosted deploys |
PORT | — | Presence switches to HTTP-connector mode. Never set it for local stdio |
MCP_AUTH_TOKEN | — | Bearer token required on /mcp in HTTP mode |
BRAINLLM_CONFIG | — | Absolute file path for brainllm.json on persistent-volume deploys |
BRAINLLM_OWNER_PASSWORD | — | Turns on the OAuth 2.1 surface. Unset means OAuth is disabled entirely |
BRAINLLM_PUBLIC_URL | — | Override the public origin when a proxy rewrites Host |
42 tools by default — 32 universal verbs plus 10 dual-mode surface reads. BRAINLLM_MODE=full adds 33 raw ETAPI tools, for a total of 75.
| Group | Tools |
|---|---|
| Session lifecycle | start · day · session · remarks · close · backup |
| Writing | remember · diary |
| Reading & search | recall · domain · brain · assembly · template · graph · consistency |
| Updating | revise · resolve · withdraw · recover · label · forget |
| Graph & inspection | connect · explore · outline · inspect · diff |
| Attachments | attach · detach |
| Maintenance | addendum · maintain · claim · bootstrap |
| Surface reads | master · llm · memory · knowledge · insights — each with a _recall skim twin |
Every tool is classified read-only, write or destructive so your client can group its permission prompts. The default is unsafe-side: an unclassified tool is treated as a write, never a read.
Fourteen kinds: biography goals preferences responsibilities protocols diary session thread threadEntry user domain information sources log.
Five statuses: active · dormant · resolved · superseded · eternal. Only threads age; eternal is exempt from the sweep entirely.
START start() once, before responding [day()] when start() flags a new day DURING remember · diary · recall · domain · revise · connect … END session() → addendum() → maintain() → remarks() → diary() → close()
close() refuses unless all five pre-close steps have run and session → remarks → diary holds. Gate state lives on today's session note, so it survives a restart. Pass force=true only when a step genuinely has nothing to log — bypassed steps are reported back, never silently skipped.
| Mode | Trigger | Use for |
|---|---|---|
| stdio | PORT unset | Claude Desktop, Claude Code — the client spawns BrainLLM as a child process |
| HTTP connector | PORT set | Docker, Railway, any container host. Serves streamable HTTP at /mcp, legacy SSE at /sse (with POST /messages), and /health — both transports behind the same auth gate |
For container deploys, mount a volume on the BrainLLM service and set BRAINLLM_CONFIG to a file path inside it — otherwise auto-discovery re-runs on every cold start.
Hosted Claude surfaces have no field for a bearer token — their connector UI offers OAuth or nothing. So BrainLLM is its own OAuth 2.1 authorization server, serving both registration mechanisms: Client ID Metadata Documents (what Claude selects) and RFC 7591 dynamic registration at /register (what opencode and other MCP TS SDK clients require).
Set BRAINLLM_OWNER_PASSWORD to turn it on. Leave it unset and the OAuth surface is disabled entirely — an authorization server that authorises whoever finds the URL is worse than none.
| Symptom | Fix |
|---|---|
| Tools time out | The backend is unreachable. Check the instance TRILIUM_BASE_URL names. Every call is bounded at 30s, so a hang fails the tool call rather than the session |
start() → uninitialized | Run bootstrap() |
| claude.ai says "couldn't reach the MCP server" | OAuth discovery failed — almost never a network problem. Check BRAINLLM_OWNER_PASSWORD is set and that the resource URL matches what you typed, trailing slash included |
| Every call 401s after an hour | Token refresh failed because the signing secret changed. BRAINLLM_CONFIG must point at a persistent volume |
ENOENT on a hosted start | BRAINLLM_CONFIG is a directory, not a file path |
revise(find=) returns replaced: 0 | Read the hint — it names the cause. Usually an entity-escaped search string: bodies accept escaped markup, find= does not |
Full reference lives in the README, and the operational guide in skills/brainllm/SKILL.md.