A minimalist single-user note-taking app focused on server-side rendering (SSR), fast in-memory tree operations, and efficient sync/diff updates.
- Rich text editing (ContentEditable) with image support
- Drag-and-drop note reordering
- Real-time content saving
- Keyboard shortcuts (press
?in the app) - Linked-list ordering model for efficient reorders
- Optional password protection + encryption at rest (AES-GCM)
- Multi-tab search contexts with server-persisted scroll/search state (survives browser restarts)
- Manual namespace backups/restores to a user-selected backup folder with retention controls
- FastAPI
- SQLite (via stdlib
sqlite3) with a guard-aware wrapper (SafeSession) - Mako templates for SSR
- Vanilla JavaScript (no framework)
- HTML5 Drag and Drop API
- ContentEditable for rich text editing
- CSS custom properties for theming
- Python/unit tests plus manual regression passes
- Server renders the base page via Mako templates.
- The browser client drives interaction via
/api2JSON endpoints. - Notes are loaded/decrypted into an in-memory store at startup; a post-startup DB read guard prevents accidental runtime SELECTs.
For a published install, users should run pip install metalist. For a non-editable local install from this checkout, use pip install . instead of the editable command below.
python3 -m venv .venv
source .venv/bin/activate
pip install -e .[dev]
npm installThe installed entrypoint starts Uvicorn with the FastAPI app:
metalistFor source-checkout compatibility, python main.py still works. Plain python main.py is now an orchestration command: it restarts already-running namespaces from the current checkout, launches stopped namespaces, prints their URLs, and exits. Use python main.py --namespace work or python main.py work when you want one foreground namespace process.
metalist and explicit single-namespace source runs bind HTTP on 0.0.0.0:8000 by default, matching the old MetaList LAN-friendly behavior.
On first startup, MetaList also auto-generates a self-signed TLS pair at ~/MetaList/certs/metalist-cert.pem and ~/MetaList/certs/metalist-key.pem, then enables HTTPS on 0.0.0.0:8443. If you already have real PEM files, point METALIST_TLS_CERT and METALIST_TLS_KEY at them instead. Set METALIST_AUTO_GENERATE_TLS=0 only if you explicitly want HTTP-only startup.
Database selection:
- No explicit namespace on a single-namespace launch:
~/MetaList/namespaces/default/default.metalist.db --namespace workorMETALIST_NAMESPACE=work:~/MetaList/namespaces/work/work.metalist.db- The related files DB is derived automatically, so
namespaces/work/work.metalist.dbusesnamespaces/work/work.metalist.files.db - Remembered launch ports are stored per namespace in
~/MetaList/namespaces.db - Launch precedence is: explicit CLI flags > env vars > saved namespace profile > built-in defaults
- Backups stay beside the namespace data under
~/MetaList/namespaces/work/backups/and use one archive per snapshot with filenames likework-<timestamp>.metalist-backup.tar.gz - The Backup Settings modal targets one user-selected backup folder and can include multiple namespaces in a single run
Useful env flags:
CRASH_SERVER_ON_FAIL=1(default): fail-fast on validation errorsAPI_PREFIX=/api2: override API prefix (client assumes/api2by default)METALIST_NAMESPACE=work: select~/MetaList/namespaces/work/work.metalist.dbMETALIST_HOST=0.0.0.0(default): bind the main app to a different interface such as127.0.0.1METALIST_PORT=8000(default): bind the main app to a different portMETALIST_HTTPS_PORT=8443: override the HTTPS port when TLS is enabledMCP_AGENT_WEB_PORT=8765(default): bind the MCP sidecar web UI to a different portMETALIST_TLS_CERT=/path/to/fullchain.pem+METALIST_TLS_KEY=/path/to/privkey.pem: override TLS pathsMETALIST_AUTO_GENERATE_TLS=0: disable automatic creation of the default self-signed TLS pair- default TLS paths:
~/MetaList/certs/metalist-cert.pemand~/MetaList/certs/metalist-key.pem METALIST_FORWARDED_ALLOW_IPS=127.0.0.1,::1(default): trust proxy headers only from those reverse-proxy IPsMCP_AGENT_PUBLIC_ORIGIN=https://notes.example.com:8765: public origin for the MCP sidecar redirect when it is exposed behind HTTPS or a separate hostname/port
Plain LAN or VPN HTTP works with a normal PyCharm run:
metalistOn a fresh machine, that first launch also creates the default TLS cert pair automatically. Then open either http://<laptop-ip>:8000 or https://<laptop-ip>:8443 from the other machine.
Namespaced launch example:
metalist --namespace work --port 8001 --mcp-port 8766This starts a separate process backed by ~/MetaList/namespaces/work/work.metalist.db on http://127.0.0.1:8001.
Its backup snapshots live under ~/MetaList/namespaces/work/backups/ with filenames like work-<timestamp>.metalist-backup.tar.gz. New backups are versioned .tar.gz workspace archives; legacy .bak backups remain restorable.
After you launch a namespace once with explicit ports, MetaList remembers them in ~/MetaList/namespaces.db, so later you can use the shorthand:
metalist workand MetaList will reuse the saved HTTP / HTTPS / MCP sidecar ports for work. The same applies to the default namespace: metalist will reuse the saved default-namespace profile.
Equivalent explicit launch, if you want it:
METALIST_HOST=0.0.0.0 \
METALIST_PORT=8000 \
METALIST_HTTPS_PORT=8443 \
metalistFrom the other machine, open https://<laptop-ip>:8443.
If you already have a real certificate and key, use the same dual-listener flow:
METALIST_HOST=0.0.0.0 \
METALIST_PORT=8000 \
METALIST_HTTPS_PORT=8443 \
METALIST_TLS_CERT=/path/to/fullchain.pem \
METALIST_TLS_KEY=/path/to/privkey.pem \
metalistIf you want to rotate or regenerate the default self-signed pair manually, the helper script is still available:
generate-lan-cert.shWhen HTTPS is enabled:
- remote HTTP requests to
http://<laptop-ip>:8000are redirected to HTTPS - localhost HTTP requests still stay on plain
http://127.0.0.1:8000so the laptop can keep using the non-TLS port
If TLS is terminated by a reverse proxy on the same machine instead, keep MetaList on loopback and let the proxy forward to it:
METALIST_HOST=127.0.0.1 \
METALIST_PORT=8000 \
METALIST_FORWARDED_ALLOW_IPS=127.0.0.1,::1 \
metalistIf you do not need the MCP sidecar remotely, disable it:
MCP_AGENT_WEB_ENABLED=0 metalistMCP is available automatically when you run:
metalistmetalist also auto-starts the agent web app sidecar and prints:
Agent web app: http://127.0.0.1:8765- The sidecar default MCP URL follows the resolved MetaList HTTP port for the current process.
- Use
--mcp-portwhen you want multiple MetaList instances to auto-start sidecars without colliding on8765. - On startup, local Ollama (
127.0.0.1) is reset by default so a fresh runner is used. - Sidecar Ollama auto-start uses
OLLAMA_CONTEXT_LENGTH=16384by default.
Manual web mode (optional):
metalist-mcp web --port 8765Then open http://127.0.0.1:8765.
Run direct MCP CLI calls:
metalist-mcp cli tools/list
metalist-mcp cli tools/call health_check '{}'Compatibility shortcut (still works):
python mcp_client.py tools/listDisable auto sidecar if needed:
MCP_AGENT_WEB_ENABLED=0 metalistControl Ollama startup behavior:
# disable Ollama reset-on-start (default is enabled)
MCP_AGENT_RESET_OLLAMA_ON_START=0 metalist
# override auto-start context length (default 16384)
MCP_AGENT_OLLAMA_CONTEXT_LENGTH=32768 metalistOptional: direct stdio transport (advanced/manual):
python -m app.mcpTool catalog and schemas:
docs/mcp_tools.md
convert-from-legacy.py replaces the SQLite database referenced by app.config.DATABASE_URL and imports notes from a legacy JSON export.
This is destructive. It deletes the existing DB file before rebuilding it.
Example usage:
convert-from-legacy.py --input /path/to/legacy-export.jsonTarget a namespaced database during import:
convert-from-legacy.py --namespace work --input /path/to/legacy-export.jsonIf --namespace, --port, --https-port, or --mcp-port are omitted, the import script prompts for them and saves the resulting launch profile to ~/MetaList/namespaces.db. That means a one-time import into work can immediately seed later shorthand launches like metalist work.
For the real user-facing install flow:
pip install metalist
metalistThis repo now packages itself under the PyPI distribution name metalist. The remaining release step is publishing version 0.3.0 to the existing PyPI project.
Recommended release path:
- In the existing PyPI project
metalist, configure GitHub Trusted Publishing forevolvingstuff/metalist3and the workflow file.github/workflows/publish-pypi.yml. - Push a tag such as
v0.3.0. - After the GitHub Actions workflow completes, users can install with
pip install metalist.
If --input is omitted, a file picker opens (when tkinter is available).
Notes tagged with @implies are converted into ontology rules and are not imported as notes.
Python/unit test examples:
source .venv/bin/activate
.venv/bin/pytest
node --test tests/unit/*.mjs
.venv/bin/python -c "from pathlib import Path; import main; main._run_startup_sanity_gates(repo_root=Path.cwd())"TEST_MODE=1 and POST /api2/test/reset still exist for deterministic browser automation if we decide to add a new harness later, but Cypress is not part of the current workflow.
Render Mermaid diagrams to PNGs:
npm run render-diagrams