Skip to main content
Glama
krisxsee

Apple Notes on Steroids

by krisxsee
README.md
<img src="icon.png" alt="" width="96">

# Apple Notes on Steroids

An MCP server for Apple Notes. Read, search, write and sync notes at full fidelity, including the styles Apple's own import path throws away.

## Requirements

macOS with Apple Notes, Node 18 or newer, and up to three permission grants:

| Grant | Buys | Needed |
| --- | --- | --- |
| Automation | talking to Notes at all | always, prompted on first run |
| Accessibility | true styles and attachments on write | optional |
| Full Disk Access | reading Notes' own store: checked state, quotes, link targets, highlights. Also how a note holding attachments is safely updated, since the store says where they sit | optional |

The server works with Automation alone, at reduced fidelity, and says so in its answers. The optional two are granted in System Settings, Privacy & Security, to the exact app running the server, so they need re-granting after that app updates.

## Install

No terminal needed.

**Claude Desktop.** Download the `.mcpb` from the [latest release](https://github.com/krisxsee/apple-notes-on-steroids/releases/latest), double-click it, install.


**ChatGPT.** add marketplace `krisxsee/apple-notes-on-steroids` in Plugins.

**Claude Code / Codex.** It is a plugin:

```bash
claude plugin marketplace add krisxsee/apple-notes-on-steroids
claude plugin install apple-notes-on-steroids
```

## Connect

Any MCP client that runs local stdio servers works: Cursor, Zed, Codex CLI and the rest. The one constraint is that the server must run on the Mac where Notes lives.

```json
{ "mcpServers": { "apple-notes-on-steroids": { "command": "npx", "args": ["-y", "github:krisxsee/apple-notes-on-steroids"] } } }
```

From source: `npm install && npm run build`, then point the client at `dist/index.js`. To sync, set `APPLE_NOTES_MD_DIR` to the folder of `.md` files, or pass `dir` per call.

## Verify

```bash
npm run verify
```

Creates one note in "MCP Tests" holding every style the server claims to write, reads it back character for character, and never touches a note that existed before.

## Tools

| Tool | What it does |
| --- | --- |
| `list_folders` | Every folder with path and note count. |
| `list_notes` | Notes, newest first. Optional `folder`, `limit`. |
| `search_notes` | Titles by default; `search_body: true` also scans bodies, capped at 300 notes. |
| `get_note` | One note by `id` or `name`. `format: "markdown"` reads Notes' own store, the only place checked state, quotes, link targets and highlights exist; when the store is out of reach it falls back to the HTML and says so with `rich: false`. `format: "html"` gives the raw HTML. |
| `create_note` | New note from a Markdown body. `attach` puts files in it; `true_styles` applies what the importer cannot. |
| `append_note` | Adds to the end. `true_styles` pastes at the caret instead of rewriting the body. |
| `update_note` | Replaces a note's body, keeping its attachments. `attach` adds more. Destructive for that note. |
| `list_attachments` | What a note carries besides text, one entry per mark and in the same order. |
| `get_attachment` | One attachment as an image, by index. Drawings come back as Notes' own render. |
| `sync_now` | One pass over a folder of Markdown files. `dry_run` reports without writing. See Sync. |
| `export_note` | Writes a note to a new `.md` file with its media. Creates only. |
| `import_note` | Creates a note from a `.md` file. Creates only. |

No delete tool, on purpose.

## Formatting

Bodies are Markdown, both ways. Two write paths produce a note: the HTML importer takes everything it can, instantly and silently, and a pasteboard hand-off supplies what it cannot.

| Feature | State |
| --- | --- |
| Bold, italic, strikethrough, underline | both ways |
| Inline code | both ways, at the size of the line around it |
| Links, bare and labelled | both ways |
| Nested bullet and numbered lists | both ways, four spaces per level |
| Tables | both ways |
| Headings | both ways. `#` is Title, `##` Heading, `###` Subheading; deeper collapses to Subheading |
| Code fences | both ways. Notes cannot hold the language tag, but a pull keeps the one the file already has |
| Block quote | both ways, single level, which is all Notes has |
| `==highlight==` | both ways, orange, with inline code in mint so the two never read alike |
| `- [ ]`, `- [x]`, nested | both ways, checked state included |
| Horizontal rule | both ways, written as em dashes because Notes has no rule |
| Images, drawings, scans, any file | read fully; written by pasting the file, since the importer cannot make one |
| `[[Wikilinks]]` | a real note-link pill when the name matches exactly one note, literal text otherwise, so it becomes a link the day that note exists. Pulled back as `[[title]]` |
| Callouts, footnotes | plain text, no Notes equivalent |

## Sync

One direction per file, and Notes is the centre. A file's own name says which note it belongs to, and one property says which way it flows.

| `apple-note-sync` | Direction | What happens |
| --- | --- | --- |
| `push` | file → note | the note's body is replaced from the file |
| `pull` | file ← note | the file's body is replaced from the note, media included |
| `off` | paused | nothing |

A pass only touches notes that already exist. A name matching no note, or more than one, is reported and neither side is written. Rename a note and the pair is lost until you rename the file to match.

One direction cannot have a conflict, so there is no conflict resolution. One guard: a pull that finds the file edited since the last pull refuses and reports it `conflicted`, both sides left standing; merge, push once, flip back. `.apple-notes-sync.json` is a cache used only to skip work; delete it and the next pass reaches the same place.

Media pulls in beside the `.md` file that links it, with ordinary Markdown. A push reconciles those links against the note: a link added attaches the file, one removed detaches it. A drawing is never removed, because nothing outside Notes reads its strokes.

`export_note` and `import_note` create the other side. Both refuse to overwrite.

## Safety

- No delete tool.
- Sync never propagates deletions.
- Notes in "Recently Deleted" are excluded from listings, search and folder resolution, and are never written to.
- Locked notes: metadata reads, body reads return a placeholder, writes fail with `LOCKED`.
- Ambiguous names return candidates rather than a guess.
- The live check works only on notes it created.
- The store is opened read-only. Writing goes through Notes itself, always.

## Design

Four ideas hold it together.

**Nothing is addressed by position.** A position is a second model of the note kept beside the note, and it drifts. Text is what both sides can see.

**The store is the only witness.** A paste changes what a paragraph is and not one character of what it says, so the editor proves nothing.

**Losing state must never read as having no state.** Empty is a fact about the world; unreadable is a fact about this pass. Conflating them lets one side overwrite the other.

**Never destroy what cannot be put back.** An image removed still exists as a file. A drawing does not.

## Development

```bash
npm test
```

`npm run verify` covers what only macOS can answer.

TDQS

A3.9/5.0

Scored across 13 tools

Disambiguation4/5

Most tools are cleanly separated by resource and action, and list/search/read/write distinctions are clear. The only real ambiguity is among export_note, import_note, and sync_now, which all move markdown/attachments between Notes and disk, so agents must carefully compare one-off conversion vs batch sync.

Naming Consistency4/5

The dominant verb_noun pattern (list_notes, get_attachment, create_note, update_note, export_note) is consistent and readable. sync_now and sync_status are the only slight deviations, but they still read as a paired sync family rather than arbitrary names.

Tool Count5/5

At 13 tools, the server is well within the ideal scope and has no redundant entries. Each tool addresses a distinct operation: reading, writing, searching, attachments, import/export, or sync.

Completeness3/5

The content lifecycle is mostly covered with list/search/get/create/append/update and strong attachment/sync support. However, there is no delete/trash tool and no folder creation/move or ability to create a note in a chosen folder, which are notable gaps. These omissions create dead ends for cleanup and organizational workflows.

Maintenance

ActivityMaintained
ResponsivenessSyncing