The MCP endpoint was unauthenticated, relying on a loopback source-IP check plus the SDK's DNS-rebinding Host allow-list. That had three problems: - Loopback is not a credential. Any local process (a package postinstall script, another user on a shared box) could read and rewrite every note. - The Host allow-list was the only thing between a malicious page and the database, so a single parsing bug or a proxy that rewrites Host lost everything. - Because req.ip honours trust proxy, a misconfigured trustedReverseProxy could forge X-Forwarded-For: 127.0.0.1 and walk straight in. It also made the endpoint unusable in the deployment people actually have: under Docker, host traffic arrives from the bridge gateway rather than 127.0.0.1, so port mapping alone never satisfied the check (#10646). Require an ETAPI token on every request instead, mirroring what the web clipper already does on the server build. This makes the rebinding protection redundant rather than merely inconvenient — rebinding borrows the victim browser's network position, and a bearer token is not ambient authority, since cross-origin JS cannot set Authorization without a CORS preflight we never approve. The Host allow-list is therefore dropped; it would only have blocked legitimate remote clients, whose Host is their own domain. Only the Authorization header authenticates, never a session cookie, which would make /mcp CSRF-able with full tool access. The 401 deliberately omits WWW-Authenticate: in MCP that header triggers OAuth discovery (RFC 9728) and must point at protected-resource metadata, so advertising one would send spec-compliant clients into a flow Trilium cannot serve. Adds a rate limiter mirroring the login one (15 min / 10 / skip successful) since token guessing is otherwise unbounded once reachable off-loopback. BREAKING CHANGE: existing MCP clients must send an ETAPI token. MCP is off by default, so this affects only users who explicitly enabled it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
27 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Overview
Trilium Notes is a hierarchical note-taking application with synchronization, scripting, and rich text editing. TypeScript monorepo using pnpm with multiple apps and shared packages.
Development Commands
# Setup
corepack enable && pnpm install
# Run
pnpm server:start # Dev server at http://localhost:8080
pnpm desktop:start # Electron dev app
pnpm standalone:start # Standalone client dev
# Build
pnpm client:build # Frontend
pnpm server:build # Backend
pnpm desktop:build # Electron
# Test
pnpm test:all # All tests (parallel + sequential)
pnpm test:parallel # Client + most package tests
pnpm test:sequential # Server (shared DB) + browser-mode tests (ckeditor5, ckeditor5-mermaid, ckeditor5-math)
pnpm --filter server test # Single package tests
pnpm coverage # Coverage reports
# Lint & Format
pnpm dev:linter-check # ESLint check
pnpm dev:linter-fix # ESLint fix
pnpm dev:format-check # Format check (stricter stylistic rules)
pnpm dev:format-fix # Format fix
pnpm typecheck # TypeScript type check across all projects
Running a single test file: pnpm --filter server test spec/etapi/search.spec.ts
Main Applications
The four main apps share packages/trilium-core/ for business logic but differ in runtime:
- client (
apps/client/): Preact frontend with jQuery widget system. Shared UI layer used by both server and desktop. - server (
apps/server/): Node.js backend (Express, better-sqlite3). Serves the client and provides REST/WebSocket APIs. - desktop (
apps/desktop/): Electron wrapper around server + client, running both in a single process. - standalone (
apps/standalone/+apps/standalone-desktop/): Runs the entire stack in the browser — server logic compiled to WASM via sql.js, executed in a service worker. No Node.js dependency at runtime.
Monorepo Structure
apps/
client/ # Preact frontend (shared by server, desktop, standalone)
server/ # Node.js backend (Express, better-sqlite3)
desktop/ # Electron (bundles server + client)
standalone/ # Standalone client (WASM + service workers, no Node.js)
standalone-desktop/ # Standalone desktop variant
web-clipper/ # Browser extension
website/ # Project website
db-compare/, dump-db/, edit-docs/, build-docs/, icon-pack-builder/
packages/
trilium-core/ # Core business logic: entities, services, SQL, sync
commons/ # Shared interfaces and utilities
trilium-e2e/ # Shared Playwright E2E tests
ckeditor5/ # Custom rich text editor bundle
codemirror/ # Code editor integration
highlightjs/ # Syntax highlighting
share-theme/ # Theme for shared/published notes
ckeditor5-admonition/, ckeditor5-footnotes/, ckeditor5-math/, ckeditor5-mermaid/
ckeditor5-keyboard-marker/, express-partial-content/, pdfjs-viewer/, splitjs/
turndown-plugin-gfm/
Use pnpm --filter <package-name> <command> to run commands in specific packages.
Core Architecture
Three-Layer Cache System
All data access goes through cache layers — never bypass with direct DB queries:
- Becca (
packages/trilium-core/src/becca/): Server-side entity cache. Access viabecca.notes[noteId]. - Froca (
apps/client/src/services/froca.ts): Client-side mirror synced via WebSocket. Access viafroca.getNote(). - Shaca (
apps/server/src/share/): Optimized cache for shared/published notes.
Critical: Always use cache methods, not direct DB writes. Cache methods create EntityChange records needed for synchronization.
Entity System
Core entities live in packages/trilium-core/src/becca/entities/ (not apps/server/):
BNote— Notes with content and metadataBBranch— Multi-parent tree relationships (cloning supported)BAttribute— Key-value metadata (labels and relations)BRevision— Version historyBOption— Application configurationBBlob— Binary content storage
Entities extend AbstractBeccaEntity<T> with built-in change tracking, hash generation, and date management.
Entity Change & Sync Protocol
Every entity modification creates an EntityChange record driving sync:
- Login with HMAC authentication (document secret + timestamp)
- Push changes → Pull changes → Push again (conflict resolution)
- Content hash verification with retry loop
Sync services: packages/trilium-core/src/services/sync.ts, syncMutexService, syncUpdateService.
Widget-Based UI
Frontend widgets in apps/client/src/widgets/:
BasicWidget/TypedBasicWidget— Base classes (jQuerythis.$widgetfor DOM)NoteContextAwareWidget— Responds to note changesRightPanelWidget— Sidebar widgets with position ordering- Type-specific widgets in
type_widgets/directory
Widget lifecycle: doRenderBody() for initial render, refreshWithNote() for note changes, entitiesReloadedEvent({loadResults}) for entity updates. Uses jQuery — don't mix React patterns.
Reusable Preact Components
Common UI components are available in apps/client/src/widgets/react/ — always reuse these instead of writing raw HTML elements or custom implementations:
NoItems- Empty state placeholder with icon and message (use for "no results", "too many items", error states)ActionButton- Consistent button styling with icon supportFormTextBox- Text input with validation and controlled input handling;FormTextBoxWithUnitfor inputs with a unit suffix (e.g. "mm", "px")FormSelect- Dropdown/combobox taking an object array as dataSlider- Range slider with labelCheckbox,RadioButton- Form controlsCollapsibleSection- Expandable content sectionsBadge- Colored pill/label with optional icon, tooltip, andonClick(for counts, status flags). Set its color via the--colorCSS variable on a wrapper class (not inline styles); passoutlinefor a colored-border/transparent-fill variant instead of a solid background.BadgeWithDropdownpairs a badge with a dropdown menu. Don't hand-roll pill/badge markup — reuse it
Fluent builder pattern: .child(), .class(), .css() chaining with position-based ordering.
Do not use Bootstrap utility classes (e.g. form-control-sm, form-select-sm, input-group) on these components — they manage their own styling internally. If you need to adjust sizing or layout, use props provided by the component or CSS custom properties, not Bootstrap overrides.
Component Styling
- Avoid inline styles — do not use the
styleattribute/prop on JSX elements unless absolutely necessary (e.g. a truly dynamic, computed value that cannot be expressed in CSS). Static layout, sizing, spacing, and visual properties must go in CSS. - Per-component CSS files: each component should have a matching
.cssfile (e.g.my_dialog.tsx→my_dialog.css), imported at the top of the component file. - CSS nesting for scoping: since CSS modules are not available, scope styles using a root class and native CSS nesting. For example, a dialog with
className="my-dialog"should have its styles nested under.modal.my-dialog { … }. - Reuse existing components instead of building custom markup — prefer
FormTextBox,FormTextBoxWithUnit,FormSelect,Slider,Button, etc. over hand-rolled<input>,<select>, or<button>elements.
API Architecture
- Internal API: REST endpoints in
apps/server/src/routes/api/ - ETAPI: External API for third-party integrations (
apps/server/src/etapi/) - WebSocket: Real-time synchronization (
apps/server/src/services/ws.ts)
API Architecture
- Internal API (
apps/server/src/routes/api/): REST endpoints, trusts frontend - ETAPI (
apps/server/src/etapi/): External API with basic auth tokens — maintain backwards compatibility - WebSocket (
apps/server/src/services/ws.ts): Real-time sync
Platform Abstraction
packages/trilium-core/src/services/platform.ts defines PlatformProvider interface with implementations in apps/desktop/, apps/server/, and apps/standalone/. Singleton via initPlatform()/getPlatform().
PlatformProvider provides:
crash(message)— Platform-specific fatal error handlinggetEnv(key)— Environment variable access (server/desktop useprocess.env, standalone maps URL query params like?safeMode→TRILIUM_SAFE_MODE)isElectron,isMac,isWindows— Platform detection flags
Critical rules for trilium-core:
- No
process.envin core — usegetPlatform().getEnv()instead (not available in standalone/browser) - No
import path from "path"in core — Node'spathmodule is externalized in browser builds. Usepackages/trilium-core/src/services/utils/path.tsforextname()/basename()equivalents - No Node.js built-in modules in core — core runs in both Node.js and the browser (standalone). Use platform-agnostic alternatives or platform providers
- Platform detection via functions —
isElectron(),isMac(),isWindows()fromutils/index.tsare functions (not constants) that callgetPlatform(). They can only be called afterinitializeCore(), not at module top-level. If used in static definitions, wrap in a closure:value: () => isWindows() ? "0.9" : "1.0" - Barrel import caution —
import { x } from "@triliumnext/core"loads ALL core exports. Early-loading modules likeconfig.tsshould import specific subpaths (e.g.@triliumnext/core/src/services/utils/index) to avoid circular dependencies or initialization ordering issues - Electron custom protocol — In desktop mode, the renderer loads the UI and makes API calls via the
trilium-app://custom protocol (not HTTP).apps/desktop/src/protocol.tsdispatches these requests into the Express app running in the main process; the dispatcher tags them viaapps/server/src/services/electron_request.tsso auth/CSRF middleware can distinguish them from external TCP traffic
Mobile (Capacitor) request routing — Android vs iOS
The mobile app (apps/mobile/) wraps the standalone WASM stack in a Capacitor WebView. There is no network backend; the client's API/sync calls (/api, /sync, /bootstrap, /search) reach the in-process worker via two platform-specific request paths:
- Android:
androidScheme: "https"works → the app runs athttps://localhost→ the service worker (apps/standalone/src/sw.ts) routes those requests to the worker. - iOS: the app runs at
capacitor://localhost, where service workers cannot register, soapps/standalone/src/main.tsinstalls fetch/XHR/image interceptors (gated onlocation.protocol === "capacitor:") instead.
iosScheme: "https" is a no-op on iOS and must not be re-added. Capacitor rejects it — CAPInstanceDescriptor.normalize() checks WKWebView.handlesURLScheme(scheme) == false, and WKWebView reserves http/https, so the scheme is reset to the default capacitor. The config line only misleads (it implies an https origin that never exists on iOS). Do not delete the iOS interceptor path as "dead code" — it is the only working request path on iOS; a code reviewer assuming iosScheme:https ⇒ https origin will wrongly flag it.
Binary Utilities
Use utilities from packages/trilium-core/src/services/utils/binary.ts for string/buffer conversions instead of manual TextEncoder/TextDecoder or Buffer.from() calls:
wrapStringOrBuffer(input)— ConvertsstringtoUint8Array, returnsUint8Arrayunchanged. Use when a function expectsUint8Arraybut receivesstring | Uint8Array.unwrapStringOrBuffer(input)— ConvertsUint8Arraytostring, returnsstringunchanged. Use when a function expectsstringbut receivesstring | Uint8Array.encodeBase64(input)/decodeBase64(input)— Base64 encoding/decoding that works in both Node.js and browser.encodeUtf8(string)/decodeUtf8(buffer)— UTF-8 encoding/decoding.
Import via import { binary_utils } from "@triliumnext/core" or directly from the module.
Database
SQLite via better-sqlite3. SQL abstraction in packages/trilium-core/src/services/sql/ with DatabaseProvider interface, prepared statement caching, and transaction support.
- Schema:
apps/server/src/assets/db/schema.sql - Migrations:
apps/server/src/migrations/YYMMDD_HHMM__description.sql
Testing Strategy
- Server tests run sequentially due to shared database
- Client tests can run in parallel
- E2E tests use Playwright for both server and desktop apps
- Build validation tests check artifact integrity
- Write concise tests: Group related assertions together in a single test case rather than creating many one-shot tests
- Extract and test business logic: When adding pure business logic (e.g., data transformations, migrations, validations), extract it as a separate function and always write unit tests for it
Internationalization
- Translation files in
apps/client/src/translations/ - Supported languages: English, German, Spanish, French, Romanian, Chinese
- Only add new translation keys to
en/translation.json— translations for other languages are managed via Weblate and will be contributed by the community - Third-party components (e.g., mind-map context menu) should use i18next
t()for their labels, with the English strings added toen/translation.jsonunder a dedicated namespace (e.g.,"mind-map") - When a translated string contains interpolated components (e.g. links, note references) whose order may vary across languages, use
<Trans>fromreact-i18nextinstead oft(). This lets translators reorder components freely (e.g."<Note/> in <Parent/>"vs"in <Parent/>, <Note/>") - When adding a new locale, follow the step-by-step guide in
docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md - Server-side translations (e.g. hidden subtree titles) go in
apps/server/src/assets/translations/en/server.json, not in the clienttranslation.json
Client vs Server Translation Usage
- Client-side:
import { t } from "../services/i18n"with keys inapps/client/src/translations/en/translation.json - Server-side:
import { t } from "i18next"with keys inapps/server/src/assets/translations/en/server.json - Electron main process (e.g.
apps/desktop/src/):import { t } from "i18next"— uses server-side keys fromapps/server/src/assets/translations/en/server.json(same as server-side). Never hardcode user-facing strings in Electron dialogs, tray menus, or IPC handlers — always uset(). - Interpolation: Use
{{variable}}for normal interpolation; use{{- variable}}(with hyphen) for unescaped interpolation when the value contains special characters like quotes that shouldn't be HTML-escaped
Electron Desktop App
- Desktop entry point:
apps/desktop/src/main.ts, window management:apps/desktop/src/services/window.ts - Security:
nodeIntegrationis disabled andcontextIsolationis enabled. The renderer has no access to Node.js APIs or Electron internals. - Preload script (
apps/desktop/src/preload.ts): UsescontextBridge.exposeInMainWorld("electronApi", ...)to expose a whitelisted API to the renderer. Compiled to CJS via esbuild (dev:scripts/electron-start.mts, prod:apps/desktop/scripts/build.ts). - ElectronApi interface (
packages/commons/src/lib/electron_api_interface.ts): Shared type definition used by both the preload script (satisfies ElectronApi) and the client (window.electronApi). Grouped into sub-objects:window,clipboard,shell,contextMenu,spellcheck,tray,printing,navigation. - Client-side access: Use
window.electronApi?.group.method()— never userequire("electron")ordynamicRequire()in client code. - Adding new Electron APIs: Add the method to the interface in commons, implement it in
preload.ts, add the IPC handler inapps/desktop/src/services/window.ts, and add a test inapps/desktop/spec/preload.spec.ts. - IPC handlers: Use
electron.ipcMain.on(channel, handler)for fire-and-forget,electron.ipcMain.handle(channel, handler)for async request/response,ipcMain.on+event.returnValuefor synchronous queries. - Electron-only features should check
isElectron()fromapps/client/src/services/utils.ts(client) orutils.isElectron(server) @electron/remoteis removed — do not use it. All renderer↔main communication goes through the preload bridge.- Spurious
electron.app is undefinederror — when running Electron-based apps (pnpm desktop:start,pnpm edit-docs:edit-docs, etc.), the console may printTypeError: Cannot read properties of undefined (reading 'commandLine')fromapps/desktop/src/main.ts(theapp.commandLine.appendSwitch("disable-http-cache")line). This is not a real failure — the app runs correctly. Do not try to fix it, guard it, or investigate electron initialization order unless the user explicitly raises it as a bug. ELECTRON_RUN_AS_NODEleak crashes Electron launches — shells spawned by Electron-based tools (the VS Code extension host, AI coding agents running inside it) often inheritELECTRON_RUN_AS_NODE=1. With it set, launching the desktop app (pnpm desktop:start,pnpm --filter desktop start-prod,electron dist) crashes at startup withTypeError: Not running in an Electron environment!(thrown byelectron-is-dev, becauserequire("electron")resolves to the npm stub's path string instead of the built-in module). This is an environment problem, not an app bug — unset the variable before launching:Remove-Item Env:ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue(PowerShell) orunset ELECTRON_RUN_AS_NODE(bash).
Three inheritance mechanisms:
- Standard:
note.getInheritableAttributes()walks parent tree - Child prefix:
child:labelon parent copies to children - Template relation:
#template=noteNoteIdincludes template's inheritable attributes
Attribute Inheritance
Use note.getOwnedAttribute() for direct, note.getAttribute() for inherited.
Client-Side API Restrictions
- Do not use
crypto.randomUUID()or other Web Crypto APIs that require secure contexts - Trilium can run over HTTP, not just HTTPS - Use
randomString()fromapps/client/src/services/utils.tsfor generating IDs instead
Storing User Preferences
- Do not use
localStoragefor user preferences — Trilium has a synced options system that persists across devices - To add a new user preference:
- Add the option type to
OptionDefinitionsinpackages/commons/src/lib/options_interface.ts - Add a default value in
apps/server/src/services/options_init.tsin thedefaultOptionsarray - Whitelist the option in
apps/server/src/routes/api/options.tsby adding it to theALLOWED_OPTIONSarray — without this, the API will reject changes with "Option 'X' is not allowed to be changed" - If the option should be user-editable in the UI, add a control in the appropriate settings component (e.g.,
apps/client/src/widgets/type_widgets/options/other.tsx) and a translation key inapps/client/src/translations/en/translation.json - Use
useTriliumOption("optionName")hook in React components to read/write the option
- Add the option type to
- Available hooks:
useTriliumOption(string),useTriliumOptionBool,useTriliumOptionInt,useTriliumOptionJson - See
docs/Developer Guide/Developer Guide/Concepts/Options/Creating a new option.mdfor detailed documentation
Shared Types Policy
- Types shared between client and server belong in
@triliumnext/commons(packages/commons/src/lib/) - Import shared types directly from
@triliumnext/commons- do not re-export them from app-specific modules - Keep app-specific types (e.g.,
LlmProviderfor server,StreamCallbacksfor client) in their respective apps
Important Patterns
- Protected notes: Check
note.isContentAvailable()before accessing content; usenote.getTitleOrProtected()for safe title access - Long operations: Use
TaskContextfor progress reporting via WebSocket - Event system (
packages/trilium-core/src/services/events.ts): Events emitted in order (notes → branches → attributes) during load for referential integrity - Search: Expression-based, scoring happens in-memory — cannot add SQL-level LIMIT/OFFSET without losing scoring
- Widget cleanup: Unsubscribe from events in
cleanup()/doDestroy()to prevent memory leaks
Code Style
- 4-space indentation, semicolons always required
- Double quotes (enforced by format config)
- Max line length: 100 characters
- Unix line endings
- Import sorting via
eslint-plugin-simple-import-sort - Never use the TypeScript non-null assertion operator (postfix
!) — including in tests. Narrow instead: optional chaining (?.), a?? fallback, an explicit null check before use, or an*OrThrowaccessor (e.g.becca.getNoteOrThrow(id)rather thanbecca.getNote(id)!). - Helper placement — when extracting a standalone helper function from a component, widget, hook, or route, place it below the primary export it supports (or in a separate module), not wedged between the imports and the main definition. Keep the file's primary export near the top so the entry point reads first; supporting helpers follow it.
Testing
- Server tests (
apps/server/spec/): Vitest, must run sequentially (shared DB), forks pool, max 6 workers - Client tests (
apps/client/src/): Vitest with happy-dom environment, can run in parallel - E2E tests (
packages/trilium-e2e/): Shared Playwright tests, run viapnpm --filter server e2eorpnpm --filter standalone e2e - ETAPI tests (
apps/server/spec/etapi/): External API contract tests
Documentation
- Script API reference — Generated by
apps/build-docs(TypeDoc) into the gitignoredsite/script-api/{backend,frontend,electron}and published to docs.triliumnotes.org. Not committed; never hand-edit — it's regenerated from the script API type definitions docs/User Guide/— Edit viapnpm edit-docs:edit-docs, not manuallydocs/Developer Guide/anddocs/Release Notes/— Safe for direct Markdown editing
Key Entry Points
apps/server/src/main.ts— Server startupapps/client/src/desktop.ts— Client initializationpackages/trilium-core/src/becca/becca.ts— Backend data managementapps/client/src/services/froca.ts— Frontend cacheapps/server/src/routes/routes.ts— API route registrationpackages/trilium-core/src/services/sql/sql.ts— Database abstraction
Adding Hidden System Notes
The hidden subtree (_hidden) contains system notes with predictable IDs (prefixed with _). Defined in apps/server/src/services/hidden_subtree.ts via the HiddenSubtreeItem interface from @triliumnext/commons.
- Add the note definition to
buildHiddenSubtreeDefinition()inapps/server/src/services/hidden_subtree.ts - Add a translation key for the title in
apps/server/src/assets/translations/en/server.jsonunder"hidden-subtree" - The note is auto-created on startup by
checkHiddenSubtree()— uses deterministic IDs so all sync cluster instances generate the same structure - Key properties:
id(must start with_),title,type,icon(format:bx-icon-namewithoutbxprefix),attributes,children,content - Use
enforceAttributes: trueto keep attributes in sync,enforceBranches: truefor correct placement,enforceDeleted: trueto remove deprecated notes - For launcher bar entries, see
hidden_subtree_launcherbar.ts; for templates, seehidden_subtree_templates.ts
Writing to Notes from Server Services
note.setContent()requires a CLS (Continuation Local Storage) context — wrap calls incls.init(() => { ... })(fromapps/server/src/services/cls.ts)- Operations called from Express routes already have CLS context; standalone services (schedulers, Electron IPC handlers) do not
Adding New LLM Tools
Tools are defined using defineTools() in apps/server/src/services/llm/tools/ and automatically registered for both the LLM chat and MCP server.
- Add the tool definition in the appropriate module (
note_tools.ts,attribute_tools.ts,attachment_tools.ts,hierarchy_tools.ts) or create a new module - Each tool needs:
description,inputSchema(Zod),executefunction, and optionallymutates: truefor write operations - If creating a new module, wrap tools in
defineTools({...})and add the registry toallToolRegistriesintools/index.ts - Add a client-side friendly name in
apps/client/src/translations/en/translation.jsonunderllm.tools.<tool_name>— use imperative tense (e.g. "Search notes", "Create note", "Get attributes"), not present continuous - Use ETAPI (
apps/server/src/etapi/) as inspiration for what fields to expose, but do not import ETAPI mappers — inline the field mappings directly in the tool so the LLM layer stays decoupled from the API layer
Updating PDF.js
- Update
pdfjs-distversion inpackages/pdfjs-viewer/package.json - Run
npx tsx scripts/update-viewer.tsfrom that directory - Run
pnpm buildto verify success - Commit all changes including updated viewer files
Database Migrations
- Add migration scripts in
apps/server/src/migrations/ - Update schema in
apps/server/src/assets/db/schema.sql
Server-Side Static Assets
- Static assets (templates, SQL, translations, etc.) go in
apps/server/src/assets/ - Access them at runtime via
RESOURCE_DIRfromapps/server/src/services/resource_dir.ts(e.g.path.join(RESOURCE_DIR, "llm", "skills", "file.md")) - Do not use
import.meta.url/fileURLToPathto resolve file paths — the server is bundled into CJS for production, soimport.meta.urlwill not point to the source directory - Do not use
__dirnamewith relative paths from source files — after bundling,__dirnamepoints to the bundle output, not the original source tree
MCP Server
- Trilium exposes an MCP (Model Context Protocol) server at
http://localhost:8080/mcp, configured in.mcp.json - The MCP server is only available when the Trilium server is running (
pnpm run server:start) - It requires an ETAPI token on every request — create one in Options → ETAPI and export it as
TRILIUM_ETAPI_TOKENbefore starting Claude Code (.mcp.jsonreads that variable). Without it the endpoint answers401 - It provides tools for reading, searching, and modifying notes directly from the AI assistant
- Use it to interact with actual note data when developing or debugging note-related features
Build System Notes
- Uses pnpm for monorepo management
- Vite for fast development builds
- ESBuild for production optimization
- pnpm workspaces for dependency management
- Docker support with multi-stage builds