Skip to main content
Glama
README.md
# upnote-mcp

A local [Model Context Protocol](https://modelcontextprotocol.io) server for [UpNote](https://getupnote.com). 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

## How it works

```mermaid
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](https://docs.astral.sh/uv/) installed.
- Python 3.10+.

## Install

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

Run it directly (stdio):

```bash
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):

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

### Claude Code

```bash
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

```bash
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

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
ResponsivenessNo issues