Skip to main content
Glama

upnote-mcp

A local Model Context Protocol server for UpNote. It lets Claude Code, Cursor, and other AI agents read and write your UpNote library.

UpNote has no public cloud API, so this server works with the two integration surfaces UpNote exposes locally:

  • Reads come from UpNote's local SQLite database (read-only).

  • Note/notebook creation uses UpNote's official upnote:// URL scheme (app-native).

  • Edits, moves, deletes, restores, and tagging are written directly into the SQLite database (guarded by backups and a "quit the app first" preflight), because UpNote provides no edit/delete endpoint.

Features

Read tools:

  • search_notes - full-text-ish search over title/body, scoped optionally to a notebook or tag

  • get_note - a note's content (markdown / html / text) plus metadata, notebooks, and tags

  • list_notebooks - the notebook tree with per-notebook note counts

  • list_notes - by filter: recent, notebook, tag, bookmarked, pinned, trashed, templates

  • list_tags - all tags with note counts

  • get_note_attachments / read_attachment - attachment metadata and bytes

  • get_stats - library counts and the resolved DB path

Write tools:

  • create_note, create_notebook - via the upnote:// URL scheme (works while the app is running)

  • update_note, move_note, delete_note, restore_note, tag_note, untag_note - direct SQLite writes (app should be quit)

  • append_note - append content to the end of a note without rewriting its existing body (safe for rich/image-heavy notes)

  • merge_notes - fold one note into another (append its body under an ## <source title> divider, migrate its images so nothing is orphaned, then trash the source)

  • open_in_upnote - open a note or run a search in the app

Related MCP server: bear-mcp

How it works

flowchart LR
  agent["Claude Code / Cursor"] -->|stdio MCP| server["upnote-mcp"]
  server -->|read-only| readdb[("upnote.sqlite3")]
  server -->|"create (default)"| scheme["upnote:// URL scheme"]
  scheme --> app["UpNote app"]
  server -->|"edit / move / delete (app quit)"| writer["backup + preflight + tx"]
  writer --> writedb[("upnote.sqlite3")]
  app --> writedb

When a row is changed directly, the server sets synced = 0, bumps revision, and updates updatedAt. UpNote re-pushes those rows to its sync backend the next time it launches.

Safety model

Editing a live, syncing database is inherently risky. This server defends your data:

  • Reads use a read-only connection and never lock or modify anything.

  • Before any direct write it takes a timestamped backup of upnote.sqlite3 (plus -wal/-shm) into a MCP Backups/ folder.

  • Direct writes are refused while UpNote is running (it caches rows in memory and could overwrite your change). Quit UpNote first, or set UPNOTE_ALLOW_WHILE_RUNNING=1 to override (not recommended).

  • Each mutation runs in a single transaction and checkpoints the WAL afterward.

  • Destructive tools accept dry_run=true to preview without writing.

Recommended workflow for edits/deletes: quit UpNote, run the changes, then reopen UpNote to let it sync. Creating notes via the default URL scheme is safe while the app is open.

Inbox / whiteboard workflow

A capture-then-triage loop keeps the library tidy:

  1. Capture (app open). Dump quick thoughts as notes into a single 📥 Inbox notebook — type them in UpNote, or have the agent create_note them. No structure required.

  2. Triage (ask the agent). Tell the agent "triage my inbox". It reads every note in 📥 Inbox, then proposes, per note, one of: move to the right notebook, merge into an existing note, or create a new note — and shows the plan for you to review.

  3. Accept. On your OK (and with UpNote quit), it applies the plan with move_note / merge_notes / append_note. A timestamped backup is taken first; merged sources go to Trash (recoverable) and their images are migrated so nothing is orphaned.

merge_notes and append_note make this safe: the destination note's existing content (including images) is preserved verbatim and the new material is appended under a labelled ## divider.

Requirements

  • UpNote desktop app installed (macOS verified; Windows/Linux best-effort).

  • uv installed.

  • Python 3.10+.

Install

git clone https://github.com/bug-breeder/upnote-mcp.git
cd upnote-mcp
uv sync

Run it directly (stdio):

uv run upnote-mcp

Configuration

All paths are auto-detected per platform and can be overridden with environment variables.

Variable

Purpose

Default (macOS)

UPNOTE_DB

Path to upnote.sqlite3

<data dir>/upnote.sqlite3

UPNOTE_DATA_DIR

UpNote data directory

~/Library/Containers/com.getupnote.desktop/Data/Library/Application Support/UpNote

UPNOTE_IMAGES_DIR

Attachment blob directory

<data dir>/images

UPNOTE_BACKUPS_DIR

Where pre-write backups go

<data dir>/MCP Backups

UPNOTE_WRITE_MODE

Create backend: url (default) or sqlite

url

UPNOTE_ALLOW_WHILE_RUNNING

Permit direct writes while UpNote runs

unset (off)

Default data directories: macOS uses the container path above; Windows uses %APPDATA%/UpNote; Linux uses ~/.config/UpNote.

Client setup

Replace the path below with your local clone if different: /Users/alanguyen/Code/Personal/upnote-mcp.

Cursor

Add to ~/.cursor/mcp.json (global) or .cursor/mcp.json (per project):

{
  "mcpServers": {
    "upnote": {
      "command": "uv",
      "args": ["run", "--directory", "/Users/alanguyen/Code/Personal/upnote-mcp", "upnote-mcp"]
    }
  }
}

Claude Code

claude mcp add upnote -- uv run --directory /Users/alanguyen/Code/Personal/upnote-mcp upnote-mcp

Or add it to .mcp.json with the same command/args shown above.

uvx alternative

If you prefer uvx, use "command": "uvx" with "args": ["--from", "/Users/alanguyen/Code/Personal/upnote-mcp", "upnote-mcp"].

Limitations and caveats

  • The URL scheme is fire-and-forget: create_note/create_notebook do not return the id of what they create. Set UPNOTE_WRITE_MODE=sqlite to create via SQLite and receive the new note id (requires the app to be quit).

  • UpNote must be quit for edits/moves/deletes to be reliable.

  • Direct writes set synced = 0 so UpNote re-syncs on next launch; the first time you use the write tools, verify a test edit appears correctly in the app before trusting it with important notes.

  • Search is substring-based (LIKE) over title and plain text, not a ranked full-text index.

Development

uv sync
uv run pytest

Tests run against a throwaway SQLite database in a temp directory and never touch your real UpNote data.

License

MIT

Available Tools

19 tools
append_noteA

Append content to the end of a note, preserving its existing body verbatim.

Unlike update_note (which replaces content), this concatenates the new content after the current html, optionally under an '## heading' divider. Safe for rich/image-heavy notes. Direct SQLite write; the app must be quit.

Inputs: content (required), note_id (required), dry_run, heading, +1 more. Returns: object

create_noteA

Create a new note.

By default this uses the upnote:// URL scheme (app-native and safe while UpNote is running); the app generates the note, so no id is returned. Set the UPNOTE_WRITE_MODE=sqlite environment variable to create via direct SQLite instead (requires the app to be quit; returns the new note id). For 'notebook', pass a notebook title (URL mode) or notebook id (sqlite mode).

Inputs: title (required), content, notebook, content_format. Returns: object

create_notebookC

Create a new notebook via the upnote:// URL scheme.

Inputs: title (required). Returns: object

delete_noteA

Delete a note: move to trash (default) or hard-delete with hard=true. Direct SQLite write; the app must be quit. Use dry_run=true to preview.

Inputs: note_id (required), hard, dry_run. Returns: object

get_noteB

Get a single note's full content and metadata in the requested format.

Inputs: note_id (required), format. Returns: object

get_note_attachmentsA

List the attachments/files referenced by a note, with on-disk paths.

Inputs: note_id (required). Returns: result

get_statsA

Return library statistics (counts of notes, notebooks, tags, files) and the DB path.

Inputs: no named parameters. Returns: object

list_notebooksA

List all notebooks as a parent/child tree, with per-notebook note counts.

Inputs: no named parameters. Returns: object

list_notesC

List notes by a filter: recent, notebook (needs notebook_id), tag (needs tag_id), bookmarked, pinned, trashed, or templates.

Inputs: limit, filter, tag_id, notebook_id. Returns: result

list_tagsA

List all tags with their note counts.

Inputs: no named parameters. Returns: result

merge_notesA

Fold one note into another: append the source's body under an '## ' divider, migrate its images so nothing is orphaned, then trash the source.

Ideal for triaging an inbox/whiteboard note into an existing canonical note. Direct SQLite write; the app must be quit. Use dry_run=true to preview.

Inputs: source_note_id (required), target_note_id (required), dry_run, trash_source. Returns: object

move_noteA

Move a note into the given notebook (direct SQLite write; app must be quit).

Inputs: note_id (required), notebook_id (required), dry_run. Returns: object

open_in_upnoteA

Open a note in UpNote (by note_id) or run a search in the app (by query) using the upnote:// URL scheme.

Inputs: query, note_id. Returns: object

read_attachmentA

Read an attachment by file id. Small files are returned inline as base64; large files return their on-disk path instead.

Inputs: file_id (required). Returns: object

restore_noteB

Restore a trashed/deleted note (direct SQLite write; app must be quit).

Inputs: note_id (required), dry_run. Returns: object

search_notesA

Search notes by text in the title/body, optionally scoped to a notebook or tag.

Returns brief records (id, title, snippet, notebooks, flags, timestamps), most-recently-updated first. Omit query to list recent notes.

Inputs: limit, query, tag_id, notebook_id, +1 more. Returns: result

tag_noteA

Add a tag (by title or id) to a note, creating the tag if needed. Direct SQLite write; the app must be quit.

Inputs: tag (required), note_id (required), dry_run, create_if_missing. Returns: object

untag_noteA

Remove a tag (by title or id) from a note (direct SQLite write; app must be quit).

Inputs: tag (required), note_id (required), dry_run. Returns: object

update_noteA

Edit a note's title/content/flags via direct SQLite write.

Requires the UpNote app to be quit (override with UPNOTE_ALLOW_WHILE_RUNNING=1). A database backup is taken automatically. Use dry_run=true to preview.

Inputs: note_id (required), title, pinned, content, +3 more. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.7/5.0

    Scored across 19 tools

    Disambiguation5/5

    Each tool targets a distinct operation (create, read, update, delete, search, etc.) on specific resources (notes, notebooks, tags, attachments). No two tools have overlapping purposes; even similar operations like append_note and update_note are clearly differentiated.

    Naming Consistency5/5

    All tool names follow a verb_noun pattern in snake_case (e.g., create_note, list_notebooks, tag_note). The naming is consistent across the entire set with no mixing of conventions like camelCase or inconsistent verbs.

    Tool Count5/5

    With 19 tools, the set is well-scoped for a note-taking application. It covers the full lifecycle of notes, notebooks, tags, and attachments without being bloated. The count aligns with the typical range for a focused domain.

    Completeness4/5

    The tool surface covers CRUD for notes, plus advanced operations (merge, append, restore, move) and tag/attachment management. Minor gaps exist—notebook update/delete and direct tag creation are missing—but these are less critical and do not hinder core workflows.

    Maintenance

    ActivityInactive
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI assistants to interact with Upnote via its x-callback-url API, allowing creation of notes, notebooks, tag management, and search. It also supports navigation to various Upnote sections and custom filters.
      26
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI assistants to read and write Bear notes using SQLite for queries and x-callback-url for safe writes.
      1
      MIT
    • F
      license
      Not graded
      quality
      D
      maintenance
      Provides AI assistants with tools to create, list, search, tag, and delete notes stored in a local SQLite database.
      -
    • A
      license
      A
      quality
      C
      maintenance
      Enables Claude to read and create UpNote notes locally, using a WAL-correct SQLite snapshot for reading and the upnote:// URL scheme for writing, with no account, cloud, or API key required.
      10
      16
      MIT