MCP server that turns TriliumNext Notes into a persistent, graph-structured second brain for Claude and other LLM clients.
BrainLLM demonstrates solid tool definition quality with consistent naming patterns (verb_noun), comprehensive parameter schemas using Zod, and detailed descriptions. All 20 tools follow action-verb naming conventions (search_, get_, create_, update_, delete_, patch_). Descriptions are substantive (100-300+ chars), explaining WHAT the tool does and WHEN to use it. Zod schemas provide type safety and validation. However, several tools lack output schema documentation, error handling guidance is minimal, and some parameter descriptions could be more prescriptive about constraints and formats. The server shows production-grade structure but falls short of A-tier polish in error recovery and output documentation.
Create a note at an explicit parent. Types: text/code/book/canvas/mermaid/relationMap/render/search/file/image.
Hard-delete a note (and its subtree if this is its last branch). Irreversible.
Fetch a single attribute by id.
Full note metadata: title, type, mime, attributes (labels + relations), parent/child ids, dates.
Raw note content (HTML / text / code).
Read the BrainLLM's change log for a day (default: today). Format: YYYY-MM-DD. section="<heading>" reads one section instead of the whole log.
Output schemas not documented. Tools return text responses via txt() helper but LLMs cannot see the structure of returned objects (note metadata, attributes, history entries, etc.). This forces LLMs to parse unstructured JSON and risks extraction errors.
Error handling lacks recovery guidance. No tool descriptions explain what to do if a call fails (e.g., 'noteId not found, try search_notes() first'). Agents receive raw errors with no actionable next steps.
Destructive operations (delete_note) lack confirmation/dry-run support. No tool description warns that deletion is irreversible or offers a preview. Agents may delete notes without understanding consequences.
Inferred effective spec: <=2025-11-25.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 65 | <=2025-11-25 | v2 |
Skim recent change logs (days + previews).
Read a Knowledge note by id — a user-knowledge note, a domain information note, or a Sources note. Pass section="<heading>" to read ONE section instead of the whole body — the read-side counterpart to revise(section=), matching on the same heading contract, so a name that reads also writes. Use it on anything large: a note past the read ceiling cannot be returned whole at all, and outline(id) lists the headings to choose from. occurrence= disambiguates repeated heading texts.
Skim Knowledge. With a domain: that domain's Sources + information notes. Without: user-knowledge notes plus the list of domains.
Read an LLM note: responsibilities, protocols or selfcorrection (singletons), or a diary entry by id. selfcorrection holds the assistant's own corrections — what it got wrong, what generalises, and the rule that prevents a repeat. It is the one LLM singleton start() does NOT serve in full: it is the largest and the least load-bearing at a first message, so it arrives as section headings and is read here on demand. Reach for it when about to repeat a class of work that has gone wrong before — an audit, a large edit, a claim of completeness. section="<heading>" reads ONE section instead of the whole note — the efficient path on protocols and selfcorrection, the two largest singletons and the ones most often needed in part rather than whole. outline(id) lists the headings.
Skim the LLM surface: responsibilities, protocols & self-correction opening lines (with relation snippets), plus recent diary entries.
Read a Master singleton: biography, goals, or preferences. Returns its id, content, and relation snippet. section="<heading>" reads ONE section instead of the whole note — the efficient path on preferences, whose schedule tables make it the largest of the three, when the session needs one day's blocks rather than the whole week. outline(id) lists the headings.
Skim all three Master singletons — the opening lines of biography, goals, and preferences (with ids and relation snippets).
Read a Memory note in full by id — a thread or a session. A thread book returns its Context/Resolution plus a children index ({id, date, blocks[]}, newest first) instead of a flat body — the day-to-day content lives in those [yyyy-mm-dd] children, not the book. Each child is indexed by its ADDENDUM BLOCKS — marker, identification line, and lead — rather than a slice from the top of the body, which on a record note is the header and therefore identical on every child. Pass date="yyyy-mm-dd" to resolve straight to that day's child in the same call, or call memory() again with a child's id from the index. section="<heading>" reads one section instead of the whole body — use it on long sessions and consolidated thread Contexts.
Skim Memory: active threads and recent sessions (ids + previews). An optional query filters threads by title.
Recent changes feed (creations / modifications / deletions), newest first.
Mutate note metadata: title, type, or mime.
Raw Trilium query search. Native query language: #label=value, note.title =* "x", note.dateModified >= 'YYYY-MM-DD', AND/OR. Unscoped unless ancestorNoteId is given.
Recover a recently Trilium-deleted note from Trilium's trash. canBeUndeleted must be true (check note_history). Distinct from recover() which restores BrainLLM-archived notes. Re-applies the BrainLLM label set for the container it lands in — the restore itself does not bring attributes back, and a note without #noteType is invisible to every read path.
Replace a note's full content.
Parameter constraints under-specified. 'limit' and 'occurrence' parameters lack min/max bounds. 'orderBy' accepts free-form strings instead of enum. LLMs may pass invalid values (negative limits, non-existent sort fields).
Pagination not implemented. Tools like search_notes and memory_recall accept 'limit' but no cursor/offset or total_count. Large result sets risk context window exhaustion.