A minimalist single-user note-taking app with SSR, fast in-memory tree operations, and efficient sync.
Project description
MetaList
A minimalist single-user note-taking app focused on server-side rendering (SSR), fast in-memory tree operations, and efficient sync/diff updates.
Features
- 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
Technology Stack
Backend
- FastAPI
- SQLite (via stdlib
sqlite3) with a guard-aware wrapper (SafeSession) - Mako templates for SSR
Frontend
- Vanilla JavaScript (no framework)
- HTML5 Drag and Drop API
- ContentEditable for rich text editing
- CSS custom properties for theming
Testing
- Python/unit tests plus manual regression passes
Architecture (High Level)
- 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.
Development
Setup
For a published one-off run with uv:
uvx metalist
For a persistent uv tool install:
uv tool install metalist
metalist
For pip, users can run pip install metalist. For a non-editable local install from this checkout, use uv pip install . or pip install . instead of the editable command below.
python3 -m venv .venv
source .venv/bin/activate
uv pip install -e .[dev]
npm install
Run
The installed entrypoint starts Uvicorn with the FastAPI app:
metalist
For 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 as plaintext metadata inside each namespace's main
*.metalist.db - Launch precedence is: explicit CLI flags > env vars > saved namespace profile; if a namespace has no saved profile, launch it once with explicit ports or configure ports from the UI
- 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
- Restoring
workintoworkis the normal overwrite path; importing a backup under a different namespace name requires a new target namespace and rejects saved launch-port conflicts.
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
Remote Access / HTTPS
Plain LAN or VPN HTTP works with a normal PyCharm run:
metalist
On 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 8766
This 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 that namespace's main DB, so later you can use the shorthand:
metalist work
and 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 \
metalist
From 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 \
metalist
If you want to rotate or regenerate the default self-signed pair manually, the helper script is still available:
generate-lan-cert.sh
When 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 \
metalist
If you do not need the MCP sidecar remotely, disable it:
MCP_AGENT_WEB_ENABLED=0 metalist
MCP (Phase 1 Read-Only)
MCP is available automatically when you run:
metalist
metalist 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 8765
Then 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/list
Disable auto sidecar if needed:
MCP_AGENT_WEB_ENABLED=0 metalist
Control 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 metalist
Optional: direct stdio transport (advanced/manual):
python -m app.mcp
Tool catalog and schemas:
docs/mcp_tools.md
Legacy Import
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.json
Target a namespaced database during import:
convert-from-legacy.py --namespace work --input /path/to/legacy-export.json
If --namespace, --port, --https-port, or --mcp-port are omitted, the import script prompts for them and saves the resulting launch profile inside the target namespace DB. That means a one-time import into work can immediately seed later shorthand launches like metalist work.
Publishing
For the real user-facing install flow:
uvx metalist
# or:
uv tool install metalist
metalist
This repo now packages itself under the PyPI distribution name metalist. Current releases support Python 3.10 through 3.13.
Recommended release path:
- In the existing PyPI project
metalist, configure GitHub Trusted Publishing forevolvingstuff/metalistand the workflow file.github/workflows/publish-pypi.yml. - Push a tag such as
v0.3.1. - After the GitHub Actions workflow completes, users can run it with
uvx metalist, install it persistently withuv tool install metalist, or install it withpip 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.
Run Tests
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.
Diagrams
Render Mermaid diagrams to PNGs:
npm run render-diagrams
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file metalist-0.3.1.tar.gz.
File metadata
- Download URL: metalist-0.3.1.tar.gz
- Upload date:
- Size: 1.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
949b8c7da0993659db3376b87d5d9913a8126ee0d7cab12fd444029ef3ad3431
|
|
| MD5 |
b3595d764b40ee6fb72d716d67a08608
|
|
| BLAKE2b-256 |
dbe94908e5fc601e63c7aa77e374fe5e10164e71edc892d9c6b08ceab0af2084
|
Provenance
The following attestation bundles were made for metalist-0.3.1.tar.gz:
Publisher:
publish-pypi.yml on evolvingstuff/metalist
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
metalist-0.3.1.tar.gz -
Subject digest:
949b8c7da0993659db3376b87d5d9913a8126ee0d7cab12fd444029ef3ad3431 - Sigstore transparency entry: 1888241542
- Sigstore integration time:
-
Permalink:
evolvingstuff/metalist@4beb7c6c03e47d777e60bf50e62fcadbf23072a0 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/evolvingstuff
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@4beb7c6c03e47d777e60bf50e62fcadbf23072a0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file metalist-0.3.1-py3-none-any.whl.
File metadata
- Download URL: metalist-0.3.1-py3-none-any.whl
- Upload date:
- Size: 1.4 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aaa8f64f7801626f7be5fb57a5a5b4e7ee2b1dce79e0b3437b76cfd746dfe9d1
|
|
| MD5 |
e6153d4a9801ccfb9be47fe360ffe1ab
|
|
| BLAKE2b-256 |
35c82110f0da51592e0c0ab7ae8b0e9a1d9654d61904fd3dde51f123b84a1e99
|
Provenance
The following attestation bundles were made for metalist-0.3.1-py3-none-any.whl:
Publisher:
publish-pypi.yml on evolvingstuff/metalist
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
metalist-0.3.1-py3-none-any.whl -
Subject digest:
aaa8f64f7801626f7be5fb57a5a5b4e7ee2b1dce79e0b3437b76cfd746dfe9d1 - Sigstore transparency entry: 1888241667
- Sigstore integration time:
-
Permalink:
evolvingstuff/metalist@4beb7c6c03e47d777e60bf50e62fcadbf23072a0 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/evolvingstuff
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@4beb7c6c03e47d777e60bf50e62fcadbf23072a0 -
Trigger Event:
push
-
Statement type: