apple-notes-mcp 2.9.20 → 2.9.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Apple Notes MCP Server
2
2
 
3
- A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that enables AI assistants like Claude to read, create, search, and manage notes in Apple Notes on macOS.
3
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that lets Claude (Claude Code and Claude Desktop), Codex, and other MCP clients read, search, create, edit, organize, and export notes in Apple Notes on macOS. It runs locally on your Mac and talks to Notes through AppleScript, Apple Shortcuts, and read-only access to the Notes database.
4
+
5
+ Beyond creating and editing notes, it manages folders and accounts, native tags, checklists, tables, pinned notes, links, and attachments. It can also export notes as Markdown, HTML, or JSON, read audio transcripts and drawings, and run a query language across your whole library.
4
6
 
5
7
  [![npm version](https://img.shields.io/npm/v/apple-notes-mcp)](https://www.npmjs.com/package/apple-notes-mcp)
6
8
  [![npm downloads](https://img.shields.io/npm/dm/apple-notes-mcp)](https://www.npmjs.com/package/apple-notes-mcp)
@@ -15,6 +17,13 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that e
15
17
  <img src="https://raw.githubusercontent.com/sweetrb/apple-notes-mcp/main/codex/assets/screenshot.png" alt="Apple Notes MCP — create, search, and organize Apple Notes from Codex, Claude, and other AI assistants" width="680">
16
18
  </p>
17
19
 
20
+ ## Contents
21
+
22
+ - [What is This?](#what-is-this) · [Quick Start](#quick-start) · [Requirements](#requirements) · [Features](#features)
23
+ - [Tool Reference](#tool-reference): [notes](#note-operations), [folders](#folder-operations), [accounts](#account-operations), [batch](#batch-operations), [export, attachments, and media](#export-operations), [diagnostics](#diagnostics), [native background operations](#native-background-operations), [private helper](#private-helper-opt-in-unsupported-apple-api)
24
+ - [Usage Patterns](#usage-patterns) · [Installation Options](#installation-options) · [Configuration](#configuration) · [Full Disk Access](#full-disk-access) · [Public native helper](#public-native-helper)
25
+ - [Security and Privacy](#security-and-privacy) · [Known Limitations](#known-limitations) · [Troubleshooting](#troubleshooting) · [Recurring macOS permission prompts](#recurring-macos-permission-prompts) · [Development](#development)
26
+
18
27
  ## What is This?
19
28
 
20
29
  This server acts as a bridge between AI assistants and Apple Notes. Once configured, you can ask Claude (or any MCP-compatible AI) to:
@@ -25,7 +34,7 @@ This server acts as a bridge between AI assistants and Apple Notes. Once configu
25
34
  - "Move my draft notes to the Archive folder"
26
35
  - "What notes do I have in my Work folder?"
27
36
 
28
- The AI assistant communicates with this server, which then uses AppleScript to interact with the Notes app on your Mac. All data stays local on your machine.
37
+ The AI assistant communicates with this server, which then uses AppleScript to interact with the Notes app on your Mac. Some features also use packaged Apple Shortcuts (native tags, checklists, tables, pinning) or read the Notes database read-only (queries, checklist state, transcripts). The server itself makes no network requests; what your MCP client does with the results is up to that client.
29
38
 
30
39
  ## Quick Start
31
40
 
@@ -56,7 +65,7 @@ Install as a Claude Code plugin for automatic configuration and enhanced AI beha
56
65
 
57
66
  This method also installs a **skill** that teaches Claude when and how to use Apple Notes effectively.
58
67
 
59
- On the first tool call, macOS shows an Automation permission prompt ("Claude" wants access to control "Notes") — click **OK**. Optionally, grant **Full Disk Access** (under Claude Desktop, to the Node binary that runs the server; from a terminal, to the terminal app) to enable the database-backed tools (`get-checklist-state`, `get-note-metadata`, `get-audio-transcripts`, `list-special-notes`, `list-native-tags`, `list-recent-notes`, `list-folder-tree`, `get-note-link`, checklist annotations in `get-note-markdown`, and full `get-sync-status` detail); see the [Full Disk Access Setup Guide](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md). The rest of the server is pure AppleScript and works without it.
68
+ On the first tool call, macOS shows an Automation permission prompt ("Claude" wants access to control "Notes") — click **OK**. Optionally, grant **Full Disk Access** (under Claude Desktop, to the Node binary that runs the server; from a terminal, to the terminal app) to enable the database-backed tools, such as `query-notes`, `get-checklist-state`, `get-note-tables`, `get-audio-transcripts`, `list-native-tags`, and `list-recent-notes` (the full list is under [Full Disk Access](#full-disk-access)); see the [Full Disk Access Setup Guide](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md). The rest of the server is pure AppleScript and works without it.
60
69
 
61
70
  Native tag, checklist, table, pin, and rich append operations use two packaged
62
71
  Apple Shortcuts. A third, `Apple Notes MCP - Create Markdown Note`, is optional:
@@ -148,6 +157,10 @@ npm install -g apple-notes-mcp
148
157
 
149
158
  On first use, macOS will ask for permission to automate Notes.app. Click "OK" to allow.
150
159
 
160
+ ### Other MCP clients
161
+
162
+ The server is a standard MCP server over stdio, so any client that can launch a local stdio server can run it with the same command the hosts above use: `npx -y apple-notes-mcp`. Most clients take it in an `mcpServers` entry shaped like the Claude Desktop example. If your client cannot pass environment variables, use the [configuration file](#configuration-file-when-the-host-strips-env).
163
+
151
164
  ## Requirements
152
165
 
153
166
  - **macOS** - Apple Notes and AppleScript are macOS-only
@@ -158,24 +171,30 @@ On first use, macOS will ask for permission to automate Notes.app. Click "OK" to
158
171
 
159
172
  | Feature | Description |
160
173
  |---------|-------------|
161
- | **Create Notes** | Create notes with titles, content, and optional folder/account targeting |
174
+ | **Create Notes** | Create notes from plaintext, HTML, or Markdown, with optional folder/account targeting (Markdown creation uses an optional Shortcut on macOS 26 or later) |
162
175
  | **Search Notes** | Find notes by title or search within note content |
163
176
  | **Query Language** | `query-notes` combines text, folder, account, tag, attachment, checklist, flag, word-count, and date conditions with AND/OR/NOT, read from the Notes database (requires Full Disk Access) |
164
- | **Read Notes** | Retrieve note content and metadata |
165
- | **Update Notes** | Modify existing notes (title and/or content) |
177
+ | **Read Notes** | Retrieve note content as HTML, plain text, or Markdown, plus metadata that AppleScript does not expose (pinned, Quick Note, locked, Recently Deleted) |
178
+ | **Note Structure** | Decode a note into typed paragraph blocks, list its paragraphs and links, and get a direct link to one paragraph (requires Full Disk Access) |
179
+ | **Update Notes** | Replace, append to, or prepend to an existing note, guarded by a content hash so a note that changed since it was read is never overwritten |
166
180
  | **Delete Notes** | Remove notes (moves to Recently Deleted) |
167
181
  | **Move Notes** | Organize notes into folders (supports nested paths) |
168
- | **Folder Management** | Create, list, and delete folders with full hierarchical path support |
182
+ | **Folder Management** | Create, list, rename, and delete folders with full hierarchical path support; read the folder tree with note counts and Smart Folder rules |
169
183
  | **Multi-Account** | Work with iCloud, Gmail, Exchange, or any configured account, including account IDs and default folders |
170
184
  | **Batch Operations** | Delete or move multiple notes at once |
171
- | **Checklist State** | Read checklist done/undone state directly from the Notes database (requires Full Disk Access) |
172
- | **Audio Transcripts** | Read the transcripts and summaries Notes stored for audio recordings (requires Full Disk Access) |
173
- | **Export** | Export all notes as JSON or get individual notes as Markdown |
174
- | **Attachments** | List attachments with their on-disk asset and preview paths, find a note's lead image, save or batch-export them to disk, or fetch their bytes as base64 |
175
- | **Notes.app UI State** | Reveal a note in Notes.app or read the current Notes.app selection |
185
+ | **Native Tags** | List, add, remove, and replace real Notes tags (not just `#hashtag` text) |
186
+ | **Checklists** | Read checklist done/undone state from the Notes database, and append real checklist items through a Shortcut |
187
+ | **Tables** | Read native tables as Markdown and JSON, and append new native tables |
188
+ | **Pinning** | Read pinned state and pin or unpin a note |
189
+ | **Links** | Get a note's `notes://` deep link, insert web, mail, or note-to-note links, and list the links in a note or folder |
190
+ | **Export** | Export notes as paginated JSON, or render a note or folder as Markdown (with reusable templates) or standalone HTML |
191
+ | **Attachments** | Add files to notes (from a path or the pasteboard), list attachments with their on-disk paths, find a note's lead image, save or batch-export them, or fetch their bytes as base64 |
192
+ | **Audio and Drawings** | Read the transcripts Notes stored for recordings, transcribe audio on-device, and decode drawings to strokes and SVG |
193
+ | **Incremental Sync** | `list-recent-notes` pages through notes by modification time with an exact cursor |
194
+ | **Notes.app UI State** | Reveal a note, folder, account, or attachment in Notes.app, or read the current Notes.app selection |
176
195
  | **Sync Awareness** | Detect iCloud sync in progress, warn about incomplete results |
177
196
  | **Collaboration** | Detect shared notes, warn before modifying |
178
- | **Diagnostics** | `health-check` plus a richer `doctor` (reachability, automation permission, accounts, Full Disk Access), sync status, and statistics |
197
+ | **Diagnostics** | `health-check` plus a richer `doctor` (reachability, automation permission, accounts, Full Disk Access), sync status, statistics, and `get-capabilities` for the Shortcut bridges |
179
198
 
180
199
  Read/list/get tools also return **structured JSON** (`structuredContent`) alongside the text, so agents can consume results without parsing prose.
181
200
 
@@ -193,8 +212,8 @@ reading Notes' own database instead; the rest genuinely cannot be supported. See
193
212
  **[docs/APPLESCRIPT-LIMITATIONS.md](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/APPLESCRIPT-LIMITATIONS.md)**
194
213
  for the investigation and verification behind each:
195
214
 
196
- - **Pinned notes** — Notes has no scriptable `pinned` property via AppleScript. Pin state can now be **read** with the BETA `get-note-metadata` tool (from the NoteStore database), but it still cannot be **set** programmatically.
197
- - **Note-to-note links** — AppleScript exposes no link property or link element, so link *relationships* between notes cannot be read, and a link cannot be inserted into a note body. A shareable `notes://showNote?identifier=<uuid>` deep link **is** available via [`get-note-link`](#get-note-link).
215
+ - **Pinned notes** — Notes has no scriptable `pinned` property via AppleScript. Pin state is **read** from the NoteStore database by the BETA `get-note-metadata` tool and `list-special-notes`, and **set** through the Background Operations Shortcut by [`set-note-pinned`](#set-note-pinned).
216
+ - **Note-to-note links** — AppleScript exposes no link property or link element. Links are instead read from the NoteStore database by [`list-note-links`](#list-note-links), and inserted by [`insert-note-link`](#insert-note-link) (through a Shortcut) or [`insert-link`](#insert-link). A shareable `notes://showNote?identifier=<uuid>` deep link is available via [`get-note-link`](#get-note-link).
198
217
 
199
218
  ---
200
219
 
@@ -256,14 +275,14 @@ Creates a new note in Apple Notes.
256
275
  | `title` | string | Yes | The title of the note. Automatically prepended as `<h1>` — do NOT include the title in `content` |
257
276
  | `content` | string | One of `content`/`contentPath` | The body content of the note (do not repeat the title here) |
258
277
  | `contentPath` | string | One of `content`/`contentPath` | Absolute path of a local UTF-8 file to use as the body instead of `content`. Allowed in the same places [`save-attachment`](#save-attachment) may write (home, temp, `/Volumes`); symbolic links, non-regular files, invalid UTF-8 and files over 1 MiB are refused before anything is written. A leading byte-order mark is dropped |
259
- | `tags` | string[] | No | Returned-only metadata — **NOT written to Notes.app**. Apple Notes tags can't be set via AppleScript, so values passed here are echoed back in the response but do not appear on the created note. Use inline `#hashtags` in `content` instead (Notes.app turns those into real tags). Refused with `format: "markdown"` |
278
+ | `tags` | string[] | No | Returned-only metadata — **NOT written to Notes.app**. Apple Notes tags can't be set via AppleScript, so values passed here are echoed back in the response but do not appear on the created note. Inline `#hashtags` in `content` stay searchable text and are returned as `hashtags`, but they do not become native Notes tags; add real tags afterwards with [`add-native-tags`](#add-native-tags). Refused with `format: "markdown"` |
260
279
  | `folder` | string | No | Folder to create the note in. Supports nested paths like `"Work/Clients"`. **The folder must already exist** — create it first with [`create-folder`](#create-folder). A smart folder is refused (see [`move-note`](#move-note)). Defaults to account root |
261
280
  | `account` | string | No | Account name (defaults to Notes.app's default account; matched exactly or by a *unique* prefix — an ambiguous prefix is refused). Must be an account Notes.app already has configured — see [`list-accounts`](#list-accounts) |
262
281
  | `format` | string | No | Content format: `"plaintext"` (default), `"html"`, or `"markdown"`. In all formats, the title is automatically prepended as the note's title line. In plaintext mode, newlines become `<br>`, tabs become `<br>`, and backslashes are preserved as HTML entities. `"markdown"` produces real Title/Heading/Subheading styles through a Shortcut; see [Markdown notes](#markdown-notes) |
263
282
  | `markdownRoute` | string | No | With `format: "markdown"` only: `"shortcut"` (default) or `"html"`. See [Markdown through HTML](#markdown-through-html) |
264
283
  | `timeoutSeconds` | number | No | Whole seconds, 1–120, for each Notes.app automation step this call runs; overrides `APPLE_NOTES_MCP_TIMEOUT_MS` for this call only. A timed-out write is uncertain, not failed: read the note by id before any retry. Also accepted by `get-note-content`, `update-note`, `append-to-note`, `delete-note` and `move-note` |
265
284
 
266
- **Example (tagged with inline hashtags):**
285
+ **Example (with inline textual hashtags):**
267
286
  ```json
268
287
  {
269
288
  "title": "Meeting Notes",
@@ -587,8 +606,9 @@ base64 images larger than `APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES` (default
587
606
  image-heavy note cannot blow the MCP message limit. `structuredContent` reports
588
607
  this as `strippedImages` (count) and `truncated` (boolean). When either is set,
589
608
  passing this body to an unguarded full-body writer would replace the real images
590
- with placeholder text. This server refuses update and append operations on
591
- attachment-bearing notes; edit them in Notes.app.
609
+ with placeholder text. This server's `update-note` refuses attachment-bearing
610
+ notes, and `append-to-note` routes them to native end-append with `scopeText`,
611
+ which never rewrites the existing body; make any other edit in Notes.app.
592
612
 
593
613
  ---
594
614
 
@@ -600,6 +620,10 @@ stable row and column identifiers. `tableCellsComplete` is false when Notes
600
620
  metadata cannot be decoded completely. This tool is read-only and requires Full
601
621
  Disk Access.
602
622
 
623
+ | Parameter | Type | Required | Description |
624
+ |-----------|------|----------|-------------|
625
+ | `id` | string | Yes | Exact note ID, in any [identifier form](#identifier-forms) |
626
+
603
627
  ---
604
628
 
605
629
  #### `get-note-tables`
@@ -860,6 +884,13 @@ Shortcut runs. It refuses ambiguous title-and-scope matches and never retries an
860
884
  uncertain write. Install the signed workflow as described in
861
885
  [`shortcuts/README.md`](shortcuts/README.md).
862
886
 
887
+ | Parameter | Type | Required | Description |
888
+ |-----------|------|----------|-------------|
889
+ | `id` | string | Yes | Exact note ID, in any [identifier form](#identifier-forms) |
890
+ | `expectedContentHash` | string | Yes | `contentHash` from a fresh read of the note |
891
+ | `scopeText` | string | Yes | Distinctive phrase already in the note, 12–500 characters |
892
+ | `tags` | string[] | Yes | 1–100 tag names to add |
893
+
863
894
  ---
864
895
 
865
896
  #### `get-note-plaintext`
@@ -947,6 +978,7 @@ Updates an existing note's content and/or title.
947
978
  | `format` | string | No | Content format: `"plaintext"` (default) or `"html"`. When `"html"`, content replaces the entire note body as raw HTML and `newTitle` is ignored (the first HTML element serves as the title) |
948
979
  | `allowLinkChanges` | boolean | No | Set to `true` only when intentionally changing or removing existing links |
949
980
  | `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Folder preconditions; see [Folder scope guards](#folder-scope-guards) |
981
+ | `timeoutSeconds` | number | No | Per-call timeout, 1–120 seconds, as for [`create-note`](#create-note). A timed-out write is uncertain: read the note by id before any retry |
950
982
 
951
983
  Title-only updates are rejected because Apple Notes titles are not unique.
952
984
 
@@ -992,6 +1024,7 @@ Deletes a note (moves to Recently Deleted in Notes.app).
992
1024
  | `id` | string | Yes | Exact CoreData note ID returned by a read or search |
993
1025
  | `expectedContentHash` | string | Yes | `contentHash` from the exact note version being deleted |
994
1026
  | `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Folder preconditions; see [Folder scope guards](#folder-scope-guards) |
1027
+ | `timeoutSeconds` | number | No | Per-call timeout, 1–120 seconds, as for [`create-note`](#create-note). A timed-out write is uncertain: read the note by id before any retry |
995
1028
  | `guardNoteId` | string | No | A second note (usually a verified copy) that must still be intact; needs Full Disk Access |
996
1029
  | `expectedGuardContentHash` | string | With `guardNoteId` | `contentHash` of the guard note from `get-note-content` |
997
1030
  | `requireActiveNoteId` | string | No | A second note that must still exist, be unlocked, stay outside Recently Deleted, and not be a Quick Note; its content is not fingerprinted. Needs Full Disk Access |
@@ -1055,7 +1088,9 @@ A smart folder is never a destination: it only gathers notes by its rules, and N
1055
1088
  |-----------|------|----------|-------------|
1056
1089
  | `id` | string | Yes | Exact CoreData note ID returned by a read or search |
1057
1090
  | `folder` | string | Yes | Destination folder name or nested path (e.g., `"Work/Clients"`) |
1091
+ | `account` | string | No | Account whose `folder` is the destination (default: the default account) |
1058
1092
  | `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Folder preconditions; see [Folder scope guards](#folder-scope-guards) |
1093
+ | `timeoutSeconds` | number | No | Per-call timeout, 1–120 seconds, as for [`create-note`](#create-note). A timed-out write is uncertain: read the note by id before any retry |
1059
1094
 
1060
1095
  Title-only moves are rejected.
1061
1096
 
@@ -1113,7 +1148,9 @@ Appends or prepends content to an existing note without replacing it. Always rea
1113
1148
  | `position` | string | No | `"after"` (default) appends to the end; `"before"` inserts directly below the note's title line, so the title stays first |
1114
1149
  | `separator` | string | No | String placed between existing content and new content (default: two newlines → `<div><br></div>` in HTML) |
1115
1150
  | `format` | string | No | Format of the content being appended: `"plaintext"` (default) or `"html"` |
1151
+ | `scopeText` | string | Native-object notes only | Unique existing phrase, as for [`append-native`](#append-native) |
1116
1152
  | `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Folder preconditions; see [Folder scope guards](#folder-scope-guards) |
1153
+ | `timeoutSeconds` | number | No | Per-call timeout, 1–120 seconds, as for [`create-note`](#create-note). A timed-out write is uncertain: read the note by id before any retry |
1117
1154
 
1118
1155
  Title-only appends are rejected.
1119
1156
 
@@ -2354,6 +2391,15 @@ distinctive existing `scopeText` so the Shortcut and server can independently
2354
2391
  resolve the same note. Creating a note from Markdown is
2355
2392
  [`create-note`](#create-note) with `format: "markdown"`.
2356
2393
 
2394
+ Every tool below that edits one note takes these parameters, plus the ones
2395
+ listed under the tool itself:
2396
+
2397
+ | Parameter | Type | Required | Description |
2398
+ |-----------|------|----------|-------------|
2399
+ | `id` | string | Yes | Exact note ID: x-coredata id, Notes UUID, or numeric key (see [Identifier forms](#identifier-forms)) |
2400
+ | `expectedContentHash` | string | Yes | `contentHash` (`sha256:…`) from a fresh read of the note |
2401
+ | `scopeText` | string | Yes | Distinctive phrase already in the note, 12–500 characters, that Notes search can find. Prefer plain words without punctuation, hashtags, or paths |
2402
+
2357
2403
  When the Shortcut refuses a request (its Find Notes step did not return
2358
2404
  exactly one note with that exact title and `scopeText`) or stops with its own
2359
2405
  action error, and the note reads back unchanged, the tool reports the
@@ -2418,11 +2464,20 @@ unapproved bridge is identified rather than guessed at; a timeout also says it
2418
2464
  may be an unanswered first-run consent prompt and names the Shortcut to run once
2419
2465
  in the foreground.
2420
2466
 
2467
+ | Parameter | Type | Required | Description |
2468
+ |-----------|------|----------|-------------|
2469
+ | `content` | string | Yes | Content to append, up to 1,048,576 characters |
2470
+ | `format` | string | No | `"plaintext"` (default), `"html"`, or `"markdown"` |
2471
+
2421
2472
  #### `create-checklist-item`
2422
2473
 
2423
2474
  Appends one real unchecked Notes checklist item and verifies its native identity
2424
2475
  and text.
2425
2476
 
2477
+ | Parameter | Type | Required | Description |
2478
+ |-----------|------|----------|-------------|
2479
+ | `text` | string | Yes | The item's text |
2480
+
2426
2481
  #### `create-checklist-items`
2427
2482
 
2428
2483
  Appends several real unchecked checklist items, 1 to 20, in the order given.
@@ -2462,27 +2517,54 @@ cell. It never substitutes a text table. Omit `rows` for an empty 2 × 2 table,
2462
2517
  the size Notes itself inserts from Format > Table; its four empty cells are
2463
2518
  verified the same way.
2464
2519
 
2520
+ | Parameter | Type | Required | Description |
2521
+ |-----------|------|----------|-------------|
2522
+ | `rows` | string[][] | No | Cell text, row by row: 1–1,000 rows of 1–100 cells, every row the same length. Omit for an empty 2 × 2 table |
2523
+
2465
2524
  #### `set-note-pinned`
2466
2525
 
2467
2526
  Sets an explicit pinned state after checking both the expected current state and
2468
2527
  the note revision. It does not rewrite the body.
2469
2528
 
2529
+ | Parameter | Type | Required | Description |
2530
+ |-----------|------|----------|-------------|
2531
+ | `expectedPinned` | boolean | Yes | The pinned state you last read; the call is refused if it differs |
2532
+ | `pinned` | boolean | Yes | The pinned state to set |
2533
+
2470
2534
  #### `remove-native-tags`
2471
2535
 
2472
2536
  Removes specified tags from one exact note while preserving unrelated tags and
2473
2537
  native objects. It does not delete global tag definitions.
2474
2538
 
2539
+ | Parameter | Type | Required | Description |
2540
+ |-----------|------|----------|-------------|
2541
+ | `tags` | string[] | Yes | 1–100 tag names to remove |
2542
+
2475
2543
  #### `replace-native-tag`
2476
2544
 
2477
2545
  Adds and verifies the new tag before removing the old tag on an explicit list of
2478
2546
  freshly read notes. Stops on the first uncertain result. Smart Folder rules are
2479
2547
  not changed.
2480
2548
 
2549
+ Unlike the other tools in this section, it takes a list of notes instead of one
2550
+ `id`:
2551
+
2552
+ | Parameter | Type | Required | Description |
2553
+ |-----------|------|----------|-------------|
2554
+ | `notes` | object[] | Yes | 1–100 entries, each with the note's `id`, `expectedContentHash`, and `scopeText` |
2555
+ | `oldTag` | string | Yes | Tag to remove after the new one is verified |
2556
+ | `newTag` | string | Yes | Tag to add |
2557
+
2481
2558
  #### `insert-note-link`
2482
2559
 
2483
2560
  Retrieves another note's real deep link and appends it with a static label while
2484
2561
  preserving the target note's native objects.
2485
2562
 
2563
+ | Parameter | Type | Required | Description |
2564
+ |-----------|------|----------|-------------|
2565
+ | `linkedNoteId` | string | Yes | ID of the note to link to, in any [identifier form](#identifier-forms) |
2566
+ | `label` | string | No | Visible link text (default: the linked note's title) |
2567
+
2486
2568
  ### Private helper (opt-in, unsupported Apple API)
2487
2569
 
2488
2570
  An optional, **read-only** native helper reads note state through Notes' own
@@ -2675,8 +2757,14 @@ All configuration is optional — the server works out of the box. Override beha
2675
2757
  | `APPLE_NOTES_MCP_RETRY_DELAY_MS` | `1000` (1 s) | Base delay before the first retry; subsequent retries back off exponentially (1s, 2s, 4s, ...). |
2676
2758
  | `APPLE_NOTES_MCP_ENABLE_PRIVATE` | unset | Set to `1` to allow the opt-in [private helper](#private-helper-opt-in-unsupported-apple-api). Any other value keeps it off. |
2677
2759
  | `APPLE_NOTES_MCP_PRIVATE_HELPER_DIR` | `~/Library/Application Support/apple-notes-mcp/private-helper` | Where `setup --native-helper` installs the helper and its checksum manifest. |
2678
- | `APPLE_NOTES_MCP_PRIVATE_HELPER_TIMEOUT_MS` | `20000` (20 s) | Per-call helper timeout. A timed-out write is indeterminate. |
2760
+ | `APPLE_NOTES_MCP_PRIVATE_HELPER_TIMEOUT_MS` | `20000` (20 s) | Per-call timeout for the private helper. The helper is read-only, so a timeout changes nothing in Notes. |
2679
2761
  | `APPLE_NOTES_MCP_PRIVATE_STORE` | unset | Testing only: points the helper at a **copy** of `NoteStore.sqlite`. The helper refuses a path that resolves to the live store. |
2762
+ | `APPLE_NOTES_MCP_PUBLIC_HELPER_DIR` | `~/Library/Application Support/apple-notes-mcp/public-helper` | Where `setup --public-helper` installs the [public native helper](#public-native-helper) and its checksum manifest. |
2763
+ | `APPLE_NOTES_MCP_PUBLIC_HELPER_TIMEOUT_MS` | `30000` (30 s) | Per-call timeout for the public native helper (`get-note-drawings`, `transcribe-note-audio`). |
2764
+ | `APPLE_NOTES_MCP_TAGS_SHORTCUT` | `Apple Notes MCP - Native Tags` | Name or UUID of the installed Native Tags bridge to run. `setup` still installs and checks the default name. |
2765
+ | `APPLE_NOTES_MCP_BACKGROUND_SHORTCUT` | `Apple Notes MCP - Background Operations v5` | Name or UUID of the installed Background Operations bridge to run. `setup` still installs and checks the default name. |
2766
+ | `APPLE_NOTES_MCP_MARKDOWN_SHORTCUT` | `Apple Notes MCP - Create Markdown Note` | Name or UUID of the installed Create Markdown Note bridge to run. `setup` still installs and checks the default name. |
2767
+ | `APPLE_NOTES_MCP_PASTEBOARD_NAME` | unset | Testing only: makes [`add-attachment-from-pasteboard`](#add-attachment-from-pasteboard) read a private named pasteboard instead of the general clipboard. |
2680
2768
  | `DEBUG` / `VERBOSE` | unset | Set either to enable verbose diagnostic logging to stderr. |
2681
2769
 
2682
2770
  ### Configuration file (when the host strips `env`)
@@ -2703,7 +2791,7 @@ MCP stores no secrets, but as a general rule keep only non-secret config here.
2703
2791
 
2704
2792
  ## Full Disk Access
2705
2793
 
2706
- Several tools read directly from the Apple Notes SQLite database, which lives in a macOS-protected directory. Those tools require **Full Disk Access** for the process running the MCP server: `get-checklist-state`, `get-note-metadata`, `get-note-blocks`, `list-note-paragraphs`, `get-paragraph-link`, `get-note-structure`, `list-note-links`, `export-notes-markdown`, `export-notes-html`, `get-audio-transcripts`, `list-special-notes`, `list-native-tags`, `list-recent-notes`, `list-folder-tree`, `get-note-link`, the checklist annotations in `get-note-markdown`, `list-attachments` with `includePaths` or `firstImage`, `export-attachments`, `list-paper-attachments`, `export-paper-image`, and the database half of `get-sync-status`.
2794
+ Several tools read directly from the Apple Notes SQLite database, which lives in a macOS-protected directory. Those tools require **Full Disk Access** for the process running the MCP server: `query-notes`, `get-native-objects`, `get-note-tables`, `list-smart-folders`, `delete-folder-by-id`, `get-note-drawings`, `transcribe-note-audio`, `native-note-state`, `get-checklist-state`, `get-note-metadata`, `get-note-blocks`, `list-note-paragraphs`, `get-paragraph-link`, `get-note-structure`, `list-note-links`, `export-notes-markdown`, `export-notes-html`, `get-audio-transcripts`, `list-special-notes`, `list-native-tags`, `list-recent-notes`, `list-folder-tree`, `get-note-link`, the checklist annotations in `get-note-markdown`, `list-attachments` with `includePaths` or `firstImage`, `export-attachments`, `list-paper-attachments`, `export-paper-image`, and the database half of `get-sync-status`.
2707
2795
 
2708
2796
  > 📘 **For the full why-and-how walkthrough (which app to grant, verifying with `doctor`, graceful degradation), see the [Full Disk Access Setup Guide](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md).** The summary below is the quick version.
2709
2797
 
@@ -2723,6 +2811,8 @@ Several tools read directly from the Apple Notes SQLite database, which lives in
2723
2811
 
2724
2812
  Every tool that does not read the Notes database works normally without Full Disk Access — that is the whole AppleScript surface (create, read, search, update, move, delete, folders, accounts, attachments, stats, export). The database-backed tools degrade like this:
2725
2813
  - `get-checklist-state` returns an error explaining that database access is needed
2814
+ - `query-notes` returns the same kind of error; use `search-notes` instead
2815
+ - `delete-folder-by-id` refuses to delete anything (it fails closed)
2726
2816
  - `get-note-metadata` returns the same kind of error — it has no non-database path
2727
2817
  - `list-special-notes` and the `list-native-tags` inventory return the same kind of error
2728
2818
  - `list-recent-notes` and `list-folder-tree` return the same kind of error
@@ -2750,7 +2840,7 @@ The helper never opens the Notes database and never writes under the Notes group
2750
2840
 
2751
2841
  ## Security and Privacy
2752
2842
 
2753
- - **Local only** - All operations happen locally via AppleScript. No data is sent to external servers.
2843
+ - **Local only** - All operations happen locally, through AppleScript, the packaged Shortcuts, read-only reads of the Notes database, and helpers you build on your own Mac. The server makes no network requests, and `transcribe-note-audio` uses on-device speech recognition only. Whatever your MCP client does with the results is governed by that client.
2754
2844
  - **Permission required** - macOS will prompt for automation permission on first use.
2755
2845
  - **Password-protected notes** - Notes with passwords cannot be read or modified via this server.
2756
2846
  - **No credential storage** - The server doesn't store any passwords or authentication tokens.
@@ -2763,15 +2853,15 @@ The helper never opens the Notes database and never writes under the Notes group
2763
2853
  |------------|--------|
2764
2854
  | macOS only | Apple Notes and AppleScript are macOS-specific |
2765
2855
  | Batch ops run per-note | `batch-delete-notes` / `batch-move-notes` apply each note individually rather than as one bulk operation — AppleScript has no bulk equivalent to IMAP's `UID STORE`/`MOVE`. This is deliberate: it preserves per-note success/failure reporting. ([#26](https://github.com/sweetrb/apple-notes-mcp/issues/26)) |
2766
- | Pinned notes are read-only | AppleScript exposes no `pinned` property. Pin state is readable via the BETA `get-note-metadata` tool (NoteStore database, needs Full Disk Access) but cannot be set ([#28](https://github.com/sweetrb/apple-notes-mcp/issues/28)) |
2856
+ | Pinning needs a Shortcut | AppleScript exposes no `pinned` property. Pin state is readable via the BETA `get-note-metadata` tool (NoteStore database, needs Full Disk Access), and [`set-note-pinned`](#set-note-pinned) sets it through the Background Operations Shortcut rather than AppleScript ([#28](https://github.com/sweetrb/apple-notes-mcp/issues/28)) |
2767
2857
  | Limited rich formatting | Use `format: "html"` on create/update for headings, lists, bold, code blocks; some complex formatting may not render |
2768
- | Title matching | Most operations require exact title matches |
2858
+ | Exact IDs for writes | Writes take an exact note ID and a fresh `contentHash`, because Notes titles are not unique. Read tools that accept a `title` need an exact, case-sensitive match |
2769
2859
  | Checklist state | Requires [Full Disk Access](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md) to read done/undone state from the database |
2770
- | Checklist **creation** | Not supported. AppleScript's `body of note` setter strips `<input type="checkbox">` and ignores any checklist-styling CSS class. Apple Notes stores checklist items as a protobuf paragraph style (`style_type=103`) that AppleScript doesn't expose, and the SQLite database is read-only. See [Creating Checklists](#creating-checklists) below for the workaround. |
2860
+ | Checklist **creation** | Not possible through AppleScript: its `body of note` setter strips `<input type="checkbox">` and ignores any checklist-styling CSS class, because Notes stores checklist items as a protobuf paragraph style (`style_type=103`). The server creates real checklist items through Shortcuts instead; see [Creating Checklists](#creating-checklists). Checking or unchecking an existing item is not supported. |
2771
2861
 
2772
2862
  ### Creating Checklists
2773
2863
 
2774
- **There is no programmatic way to create a true Apple Notes checklist via AppleScript** — and therefore no way via this MCP server. This is an Apple limitation, not a bug.
2864
+ **There is no programmatic way to create a true Apple Notes checklist via AppleScript.** This is an Apple limitation, not a bug. Plaintext and HTML writes therefore cannot produce checklists, so this server creates them through Apple Shortcuts instead (workaround 2 below).
2775
2865
 
2776
2866
  When a note is created or updated via AppleScript:
2777
2867
 
@@ -2786,7 +2876,7 @@ Apple Notes stores checklists as a paragraph style (`style_type=103`) inside a g
2786
2876
  **Workarounds:**
2787
2877
 
2788
2878
  1. **Create the note with bulleted list items, then convert manually in Notes.app.** Select the items and press <kbd>⇧⌘L</kbd> (or **Format → Checklist**). This converts the list in place and the resulting checklist will be readable by `get-checklist-state` and annotated by `get-note-markdown`.
2789
- 2. **Use the Apple Shortcuts app** to script the checklist creation, since Shortcuts can manipulate Notes content at a higher level than AppleScript. This server does that for you in two ways: [`create-checklist-item`](#create-checklist-item) appends one unchecked item to an existing note, and [`create-note`](#create-note) with `format: "markdown"` turns `- [ ]` / `- [x]` lines into checklist items with that done state through Notes' own Markdown importer, (see [Markdown notes](#markdown-notes)).
2879
+ 2. **Use the Apple Shortcuts app** to script the checklist creation, since Shortcuts can manipulate Notes content at a higher level than AppleScript. This server does that for you in two ways: [`create-checklist-item`](#create-checklist-item) and [`create-checklist-items`](#create-checklist-items) append unchecked items to an existing note, and [`create-note`](#create-note) with `format: "markdown"` turns `- [ ]` / `- [x]` lines into checklist items with that done state through Notes' own Markdown importer (see [Markdown notes](#markdown-notes)).
2790
2880
  3. **Read-only checklist support is fully implemented** — once a checklist exists (created manually or by another app), `get-checklist-state` and `get-note-markdown` will read its done/undone state correctly (with Full Disk Access).
2791
2881
 
2792
2882
  If you need to *track* todos programmatically and don't strictly need them rendered as Apple Notes checklist UI, plain markdown-style `- [ ] item` / `- [x] item` lines in a `plaintext` note are a reasonable alternative — they are searchable, human-readable, and can be parsed by downstream tooling.
package/build/index.js CHANGED
@@ -59329,7 +59329,7 @@ ${lines}${footer}`,
59329
59329
  registerTool(
59330
59330
  "get-note-content",
59331
59331
  {
59332
- description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the exact note id, content, contentHash revision token, parsed hashtags, nativeTags, restored links, richContentComplete/writable, and strippedImages/truncated when the body was capped. Read the warning when writable is false.\nDo not use when: you only need metadata (get-note-details) or Markdown with checklist state (get-note-markdown).\nNote: password-protected notes must be unlocked in Notes.app first.\nSafety: inline images larger than APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES (default 256 KB) are replaced with '[inline image omitted: ...]' text placeholders, so the returned body is lossy whenever truncated is true. Mutations refuse attachment-bearing notes; edit those in Notes.app. Notes.app returns images inside the body as base64, so a note with a very large image can time out; the error then names the cause, and a larger timeoutSeconds gives the read more time.",
59332
+ description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the exact note id, content, contentHash revision token, parsed hashtags, nativeTags, restored links, richContentComplete/writable, and strippedImages/truncated when the body was capped. Read the warning when writable is false.\nDo not use when: you only need metadata (get-note-details) or Markdown with checklist state (get-note-markdown).\nNote: password-protected notes must be unlocked in Notes.app first.\nSafety: inline images larger than APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES (default 256 KB) are replaced with '[inline image omitted: ...]' text placeholders, so the returned body is lossy whenever truncated is true. For an attachment-bearing note, update-note refuses a full-body replacement; append-to-note with scopeText appends natively at the end and leaves attachments in place; add-attachment adds a file; move-note and delete-note work as usual. Make any other edit in Notes.app. Notes.app returns images inside the body as base64, so a note with a very large image can time out; the error then names the cause, and a larger timeoutSeconds gives the read more time.",
59333
59333
  inputSchema: {
59334
59334
  id: looseNoteId(external_exports.string()).optional().describe(`Note ID (preferred - more reliable than title): ${NOTE_ID_FORMS}`),
59335
59335
  title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
@@ -19,7 +19,7 @@ shared, body, id, name, plaintext
19
19
 
20
20
  ## Pinned notes (#28)
21
21
 
22
- **Status: not feasible via AppleScript; readable via the NoteStore database.**
22
+ **Status: not feasible via AppleScript; read from the NoteStore database and set through a Shortcut.**
23
23
  The Notes UI lets you pin a note to the top of a folder, but the `note` class
24
24
  has no `pinned` property. Asking for it raises error `-1700`:
25
25
 
@@ -33,7 +33,7 @@ end tell
33
33
  There is no alternative property, element, or command (`pin`, `pinned`,
34
34
  `favorite`, …) in the dictionary. Pinned state lives only in Notes' private
35
35
  Core Data store (`NoteStore.sqlite`), which is not part of the scriptable
36
- surface, and there is no supported way to *set* it at all.
36
+ surface, and AppleScript offers no way to _set_ it.
37
37
 
38
38
  Reading it, however, did turn out to be worth doing. Since 2.5.0 the BETA
39
39
  `get-note-metadata` tool queries `ZISPINNED` on `ZICCLOUDSYNCINGOBJECT` in that
@@ -43,14 +43,17 @@ changes across macOS releases. It requires
43
43
  [Full Disk Access](./FULL-DISK-ACCESS.md) and is marked BETA precisely because
44
44
  the schema is version-dependent.
45
45
 
46
- **Conclusion:** pin state is **readable** (BETA, from the NoteStore database,
47
- Full Disk Access required) but **not settable** — and setting will not be added
48
- while Notes lacks a scriptable property. If a future macOS exposes one, revisit
49
- by re-running the probe above.
46
+ **Conclusion:** pin state is **not scriptable** through AppleScript in either
47
+ direction. It is **readable** (BETA, from the NoteStore database, Full Disk
48
+ Access required) and, since the native background operations were added,
49
+ **settable** through the packaged Background Operations Shortcut by
50
+ `set-note-pinned`, which checks the expected current state and the note
51
+ revision first. If a future macOS exposes a scriptable property, revisit by
52
+ re-running the probe above.
50
53
 
51
54
  ## Note-to-note links (#30)
52
55
 
53
- **Status: link *relationships* are not exposed; a shareable deep link is.**
56
+ **Status: link _relationships_ are not exposed to AppleScript; the server reads them from the database and inserts links by other routes.**
54
57
  Apple Notes lets you insert a link from one note to another in the UI, but
55
58
  AppleScript exposes no property or element for that relationship:
56
59
 
@@ -58,7 +61,7 @@ AppleScript exposes no property or element for that relationship:
58
61
  (undefined). There is no element that enumerates outgoing/incoming links.
59
62
  - Nothing in the dictionary inserts a link into a note's body.
60
63
 
61
- A shareable deep link to a note *is* available, and has been since 2.6.0:
64
+ A shareable deep link to a note _is_ available, and has been since 2.6.0:
62
65
  `get-note-link` returns a `notes://showNote?identifier=<uuid>` URL that opens
63
66
  the note in Notes.app on macOS and iOS.
64
67
 
@@ -84,31 +87,39 @@ It **is** wrapped, as `show-note`, `show-folder`, `show-account`, and
84
87
  something useful on a machine with an active desktop session; to read a note's
85
88
  content, use `get-note-content` / `get-note-markdown` instead.
86
89
 
87
- **Conclusion:** link relationships between notes cannot be read, so a "list
88
- links in this note" feature is not possible, and links cannot be inserted into a
89
- body. To hand a note to a person or another app, use `get-note-link`; to address
90
- a note in a follow-up tool call, use the `id` returned by every read tool.
90
+ **Conclusion:** AppleScript can neither read link relationships between notes
91
+ nor insert a link into a body. The server does both by other routes:
92
+ `list-note-links` reads a note's or folder's links from the NoteStore database
93
+ (Full Disk Access required), `insert-link` appends a web, mail or Notes link,
94
+ and `insert-note-link` appends another note's real deep link through the
95
+ Background Operations Shortcut. To hand a note to a person or another app, use
96
+ `get-note-link`; to address a note in a follow-up tool call, use the `id`
97
+ returned by every read tool.
91
98
 
92
99
  ## Tags / hashtags (#29)
93
100
 
94
- **Status: parsed from the body, not first-class.** Apple Notes "tags" are inline
95
- `#hashtag` tokens you type into a note's text. They are **not** a scriptable
96
- property — the `note` class exposes no `tags` element, and the tag relationship
97
- lives only in Notes' private Core Data store. So the only way to surface a
98
- note's tags via AppleScript is to read them back out of the body text.
99
-
100
- This server does that: `get-note-content` parses the body and returns the tags
101
- as `hashtags` in its `structuredContent` (see `src/utils/hashtags.ts`). The
102
- rules match Notes' own behaviour — a token is `#` followed by letters/digits/
103
- underscores containing **at least one letter**, so `#123` is not a tag; tokens
104
- are de-duplicated case-insensitively.
101
+ **Status: not scriptable; native tags go through Shortcuts.** A native Apple
102
+ Notes tag is an inline tag attachment backed by an `ICHashtag` object in Notes'
103
+ private Core Data store. It is **not** a scriptable property — the `note` class
104
+ exposes no `tags` element — and `#word` text written through AppleScript stays
105
+ plain text: Notes does not convert it into a native tag.
106
+
107
+ This server therefore separates two things. `get-note-content` parses textual
108
+ `#hashtag` tokens out of the body and returns them as `hashtags` (see
109
+ `src/utils/hashtags.ts`); the rules match Notes' own — `#` followed by
110
+ letters/digits/underscores containing **at least one letter**, so `#123` is not
111
+ a tag; tokens are de-duplicated case-insensitively. Actual native tags are read
112
+ from the database (`nativeTags`, `list-native-tags`, Full Disk Access) and
113
+ written with `add-native-tags` / `remove-native-tags` / `replace-native-tag`,
114
+ which drive the Notes Shortcuts action. A textual `hashtags` entry is not proof
115
+ that Notes registered a native tag.
105
116
 
106
117
  Two related caveats:
107
118
 
108
119
  - The `tags` parameter on `create-note` is an application-level pass-through. It
109
120
  is stored on the returned object but Notes does **not** persist it, and it does
110
- **not** create real `#hashtags` in the body. To make a real tag, put `#tag`
111
- in the note content.
121
+ **not** create tags. Typing `#tag` into the content does not either; use
122
+ `add-native-tags` on the created note.
112
123
  - **Smart folders are not scriptable.** Notes' tag-driven Smart Folders cannot be
113
124
  created, read, or enumerated via AppleScript; there is no `smart folder` class
114
125
  in the dictionary. Only regular folders are scriptable.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.9.20",
4
- "description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and other AI assistants",
3
+ "version": "2.9.22",
4
+ "description": "MCP server for Apple Notes - read, search, create, edit, organize, and export notes on macOS via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
7
7
  "types": "build/index.d.ts",
@@ -25,7 +25,16 @@
25
25
  "applescript",
26
26
  "macos",
27
27
  "notes",
28
- "model-context-protocol"
28
+ "model-context-protocol",
29
+ "mcp-server",
30
+ "apple",
31
+ "notes-app",
32
+ "claude-code",
33
+ "claude-desktop",
34
+ "shortcuts",
35
+ "markdown",
36
+ "note-taking",
37
+ "icloud"
29
38
  ],
30
39
  "author": "Rob Sweet <rob@superiortech.io>",
31
40
  "license": "MIT",