Skip to main content

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 /api2 JSON 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 work or METALIST_NAMESPACE=work: ~/MetaList/namespaces/work/work.metalist.db
  • The related files DB is derived automatically, so namespaces/work/work.metalist.db uses namespaces/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 like work-<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 work into work is 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 errors
  • API_PREFIX=/api2: override API prefix (client assumes /api2 by default)
  • METALIST_NAMESPACE=work: select ~/MetaList/namespaces/work/work.metalist.db
  • METALIST_HOST=0.0.0.0 (default): bind the main app to a different interface such as 127.0.0.1
  • METALIST_PORT=8000 (default): bind the main app to a different port
  • METALIST_HTTPS_PORT=8443: override the HTTPS port when TLS is enabled
  • MCP_AGENT_WEB_PORT=8765 (default): bind the MCP sidecar web UI to a different port
  • METALIST_TLS_CERT=/path/to/fullchain.pem + METALIST_TLS_KEY=/path/to/privkey.pem: override TLS paths
  • METALIST_AUTO_GENERATE_TLS=0: disable automatic creation of the default self-signed TLS pair
  • default TLS paths: ~/MetaList/certs/metalist-cert.pem and ~/MetaList/certs/metalist-key.pem
  • METALIST_FORWARDED_ALLOW_IPS=127.0.0.1,::1 (default): trust proxy headers only from those reverse-proxy IPs
  • MCP_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>:8000 are redirected to HTTPS
  • localhost HTTP requests still stay on plain http://127.0.0.1:8000 so 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-port when you want multiple MetaList instances to auto-start sidecars without colliding on 8765.
  • 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=16384 by 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. The remaining release step is publishing version 0.3.0 to the existing PyPI project.

Recommended release path:

  1. In the existing PyPI project metalist, configure GitHub Trusted Publishing for evolvingstuff/metalist and the workflow file .github/workflows/publish-pypi.yml.
  2. Push a tag such as v0.3.0.
  3. After the GitHub Actions workflow completes, users can run it with uvx metalist, install it persistently with uv tool install metalist, or install it 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.

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

metalist-0.3.0.tar.gz (1.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

metalist-0.3.0-py3-none-any.whl (1.4 MB view details)

Uploaded Python 3

File details

Details for the file metalist-0.3.0.tar.gz.

File metadata

  • Download URL: metalist-0.3.0.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

Hashes for metalist-0.3.0.tar.gz
Algorithm Hash digest
SHA256 81f9323b70da69c2aa8bd03b4dcfb7435bedf17a68f8a2aef6648794e0d0e319
MD5 30318e86767c6bb49f9cd3ff062e7bc3
BLAKE2b-256 37f5168577491209f8755aa1906f4daab0a06f0e85999736bbfc7b8bcbecaee9

See more details on using hashes here.

Provenance

The following attestation bundles were made for metalist-0.3.0.tar.gz:

Publisher: publish-pypi.yml on evolvingstuff/metalist

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file metalist-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: metalist-0.3.0-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

Hashes for metalist-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e443e5fee4b156e03e54069f777c386823f435409fea53165f3c92abad667436
MD5 18ab9be3bbd53703763084422fa70e89
BLAKE2b-256 f841eef390c28bd4b716b62fa98bcb543792856d5eca64e79670fa9162994e6b

See more details on using hashes here.

Provenance

The following attestation bundles were made for metalist-0.3.0-py3-none-any.whl:

Publisher: publish-pypi.yml on evolvingstuff/metalist

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page