Quick start
BrainLLM needs a running TriliumNext instance and Bun 1.0 or later. It uses Bun's APIs directly and does not run on Node.
1. Run Trilium and create a token
Install TriliumNext on your computer or anywhere reachable over HTTP, then create a token under Options → ETAPI → Create token. Or skip the token and set TRILIUM_PASSWORD: BrainLLM creates one on first start and caches it.
2. Install
# from 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
Client configuration
For Claude Desktop, Claude Code or any client that launches local servers:
{
"mcpServers": {
"BrainLLM": {
"command": "npx",
"args": ["-y", "brainllm"],
"env": {
"TRILIUM_BASE_URL": "http://localhost:8080",
"TRILIUM_ETAPI_TOKEN": "your-token"
}
}
}
}
For claude.ai, the Claude apps and Cowork, BrainLLM runs as a remote server instead: see Connect to Claude.
Bootstrap the brain
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 run at any time: it checks an existing root live and builds a new tree only on a confirmed miss.
Teach the model to operate it
The tools are terse single-word verbs; the working discipline lives in the bundled skill at skills/brainllm/. Copy it to ~/.claude/skills/brainllm for Claude Code and Cowork, or upload skills/brainllm.zip to claude.ai. Without it a model can call every tool correctly and still keep an untidy brain, because a correct call is not a correct practice.
Environment variables
| Variable | Required | Purpose |
|---|---|---|
TRILIUM_BASE_URL | Yes | URL of your TriliumNext instance |
TRILIUM_ETAPI_TOKEN | Yes* | ETAPI token (Trilium: Options → ETAPI → Create token) |
TRILIUM_PASSWORD | * | Alternative to the token: BrainLLM creates one on first start and caches it |
BRAINLLM_MODE | core (45 tools, default) or full (adds 33 raw Trilium tools, 78 in all) | |
BRAINLLM_TZ | IANA timezone for date stamps on hosted deploys, e.g. Africa/Johannesburg | |
BRAINLLM_DELETION_CATCHUP_DAYS | Days your Trilium keeps deleted notes, when it differs from Trilium's default of 7 | |
PORT | Switches to HTTP mode. Leave unset for local stdio | |
MCP_AUTH_TOKEN | Static bearer token for HTTP mode; HTTP refuses to start without it or OAuth | |
BRAINLLM_OWNER_PASSWORD | Turns on OAuth 2.1, which claude.ai and the Claude apps need. It is the password on the consent screen | |
BRAINLLM_OAUTH_SECRET | Optional 32+ character signing secret; rotate it to invalidate every token | |
BRAINLLM_CONFIG | Absolute file path for brainllm.json on a persistent volume | |
BRAINLLM_PUBLIC_URL | The public origin, when a proxy rewrites Host | |
BRAINLLM_TRUST_PROXY | Trust forwarded host and protocol headers, only behind a trusted proxy | |
BRAINLLM_ALLOW_UNAUTHENTICATED_HTTP | Trusted networks only. Never on a public interface |
Tool surface
45 tools by default: 35 verbs and 10 surface reads. BRAINLLM_MODE=full adds 33 raw Trilium tools, 78 in all. Every tool carries a human-readable title and is marked read-only, write or destructive, so clients can group their permission prompts; an unclassified tool is treated as a write, never a read.
| Group | Tools |
|---|---|
| Session | start · day · session · remarks · close · backup · health |
| Writing | remember · diary · split |
| Reading and search | read · recall · domain · brain · assembly · template · consistency |
| Updating | revise · resolve · withdraw · recover · label · forget |
| Graph and inspection | connect · explore · graph · outline · inspect · diff |
| Attachments | attach · detach |
| Maintenance | addendum · maintain · claim · bootstrap |
| Surface reads | master · llm · memory · knowledge · insights, each with a _recall skim twin |
Kinds and statuses
Sixteen kinds: biography, goals, preferences, responsibilities, protocols, selfcorrection, diary, session, thread, threadEntry, user, domain, information, sources, log and claim. Five statuses: active, dormant, resolved, superseded and eternal. Only threads age, and an eternal thread never does.
Session protocol
START start() the first BrainLLM call of a session
day() when start() reports a new day
DURING remember · revise · recall · domain · connect · ...
END session() → addendum() → maintain() → remarks() → diary() → close()
close() refuses until the closing steps have run, with session → remarks → diary in that order. diary() and close() take an identity line, "LLM · environment · agent/mode". The gate's state lives on the day's session note, so it survives a restart. session() lists the notes the session changed, so a figure corrected in one note can be checked everywhere it is recorded.
Editing large notes
revise(noteId, section="Heading")replaces one section;mode="before","after","prepend"and"remove"act around it.revise(noteId, find="a few words", within="tr")replaces, inserts beside or removes the table row containing them. Inline formatting in the stored text doesn't stop a match.revise(domain="Name", find=, body=)previews a find and replace across a whole domain; adddryRun=falseto write it.- Reads take
section=; records takeblock="14:05"orblock="-1"for one addendum;read(ids=[...], text=true)returns readable text for orientation. diff(since="today")shows every change made today, with table edits reported cell by cell.
Transports
| Mode | When | Use for |
|---|---|---|
| stdio | PORT unset | Claude Desktop and Claude Code, which start BrainLLM themselves |
| HTTP | PORT set | Docker, Railway or any container host. Streamable HTTP at /mcp, legacy SSE at /sse, and /health, all behind the same authentication |
On a container host, mount a volume and set BRAINLLM_CONFIG to a file inside it, or discovery reruns on every cold start.
OAuth
claude.ai and the Claude apps connect with OAuth only; they have no field for a bearer token. BrainLLM is its own OAuth 2.1 authorization server, with PKCE, Client ID Metadata Documents and dynamic registration at /register. Set BRAINLLM_OWNER_PASSWORD to turn it on; unset, the OAuth surface does not exist. The consent screen names the requesting client and the address it returns to, and warns when that address is your own computer. Refresh tokens rotate on use and are stored only as hashes.
Health and backups
close() backs up into one of seven weekday slots. backup(name) keeps a named milestone, which stays on Trilium's volume until deleted there. health() reports Trilium's version, an estimate of the database's size, the heaviest notes and their revision counts, and the named backups taken through BrainLLM, and flags growth before the disk fills.
Troubleshooting
| Symptom | Fix |
|---|---|
| Tools time out | Trilium is unreachable. Check TRILIUM_BASE_URL. Every call is bounded at 30 seconds, so a hang fails one call rather than the session |
start() returns uninitialized | Run bootstrap() once |
| claude.ai says it couldn't reach the server | OAuth discovery failed, which is almost never the network. Check BRAINLLM_OWNER_PASSWORD is set and that the URL you entered matches the server's, trailing slash included |
| Every call fails after an hour | Token refresh failed because the signing secret changed. Point BRAINLLM_CONFIG at a persistent volume |
ENOENT on a hosted start | BRAINLLM_CONFIG names a directory; it must be a file path |
revise(find=) replaced nothing | Read the hint: it names the cause and shows the stored text nearby. For a table row, anchor on a few words with within="tr" |
| A result ends with a note that it was cut | It passed about 140,000 characters. Narrow the read with section=, block=, limit= or fewer ids= |
Still stuck? See Support.