apple-notes-mcp 2.6.11 → 2.6.13
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 +43 -31
- package/build/index.js +72 -34
- package/docs/APPLESCRIPT-LIMITATIONS.md +50 -28
- package/docs/FULL-DISK-ACCESS.md +26 -9
- package/docs/STABILITY-PERF-AUDIT-2026-06-19.md +25 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Install as a Claude Code plugin for automatic configuration and enhanced AI beha
|
|
|
56
56
|
|
|
57
57
|
This method also installs a **skill** that teaches Claude when and how to use Apple Notes effectively.
|
|
58
58
|
|
|
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** to the app that launches the server to enable the checklist-state
|
|
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** to the app that launches the server to enable the database-backed tools (`get-checklist-state`, `get-note-metadata`, `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.
|
|
60
60
|
|
|
61
61
|
### Using the Codex Marketplace
|
|
62
62
|
|
|
@@ -153,14 +153,15 @@ Resources expose read-only context the client can attach without a tool call:
|
|
|
153
153
|
`notes://note/{id}` template (returns the note as Markdown). Prompts package
|
|
154
154
|
common workflows: `find-note`, `weekly-review`, `new-meeting-note`.
|
|
155
155
|
|
|
156
|
-
###
|
|
156
|
+
### AppleScript limitations
|
|
157
157
|
|
|
158
|
-
A few Notes UI features are not exposed to AppleScript
|
|
159
|
-
supported. See
|
|
158
|
+
A few Notes UI features are not exposed to AppleScript. Some are recovered by
|
|
159
|
+
reading Notes' own database instead; the rest genuinely cannot be supported. See
|
|
160
|
+
**[docs/APPLESCRIPT-LIMITATIONS.md](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/APPLESCRIPT-LIMITATIONS.md)**
|
|
160
161
|
for the investigation and verification behind each:
|
|
161
162
|
|
|
162
163
|
- **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.
|
|
163
|
-
- **Note-to-note links** —
|
|
164
|
+
- **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).
|
|
164
165
|
|
|
165
166
|
---
|
|
166
167
|
|
|
@@ -179,8 +180,8 @@ Creates a new note in Apple Notes.
|
|
|
179
180
|
| `title` | string | Yes | The title of the note. Automatically prepended as `<h1>` — do NOT include the title in `content` |
|
|
180
181
|
| `content` | string | Yes | The body content of the note (do not repeat the title here) |
|
|
181
182
|
| `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) |
|
|
182
|
-
| `folder` | string | No | Folder to create the note in. Supports nested paths like `"Work/Clients"`. Defaults to account root |
|
|
183
|
-
| `account` | string | No | Account name (defaults to iCloud) |
|
|
183
|
+
| `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). Defaults to account root |
|
|
184
|
+
| `account` | string | No | Account name (defaults to iCloud). Must be an account Notes.app already has configured — see [`list-accounts`](#list-accounts) |
|
|
184
185
|
| `format` | string | No | Content format: `"plaintext"` (default) or `"html"`. In both formats, the title is automatically prepended as `<h1>`. In plaintext mode, newlines become `<br>`, tabs become `<br>`, and backslashes are preserved as HTML entities |
|
|
185
186
|
|
|
186
187
|
**Example (tagged with inline hashtags):**
|
|
@@ -288,6 +289,16 @@ Retrieves the full content of a specific note.
|
|
|
288
289
|
from the body. Apple Notes tags are inline hashtags, not a scriptable property;
|
|
289
290
|
see [docs/APPLESCRIPT-LIMITATIONS.md](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/APPLESCRIPT-LIMITATIONS.md#tags--hashtags-29). Smart Folders are not scriptable.
|
|
290
291
|
|
|
292
|
+
**⚠️ The returned body can be lossy — do not write it back verbatim.** Inline
|
|
293
|
+
base64 images larger than `APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES` (default
|
|
294
|
+
256 KB) are replaced with `[inline image omitted: …]` text placeholders so an
|
|
295
|
+
image-heavy note cannot blow the MCP message limit. `structuredContent` reports
|
|
296
|
+
this as `strippedImages` (count) and `truncated` (boolean). When either is set,
|
|
297
|
+
passing this body to [`update-note`](#update-note) would replace the real images
|
|
298
|
+
with the placeholder text — use [`append-to-note`](#append-to-note) for
|
|
299
|
+
additions, or export the images with `save-attachment` / `fetch-attachment`
|
|
300
|
+
first.
|
|
301
|
+
|
|
291
302
|
---
|
|
292
303
|
|
|
293
304
|
#### `get-note-plaintext`
|
|
@@ -415,7 +426,7 @@ Updates an existing note's content and/or title.
|
|
|
415
426
|
|
|
416
427
|
**Returns:** Confirmation message, or error if note not found.
|
|
417
428
|
|
|
418
|
-
**Note:** `newContent` **replaces the entire note body** — it is not appended. To
|
|
429
|
+
**Note:** `newContent` **replaces the entire note body** — it is not appended. To add to a note, prefer [`append-to-note`](#append-to-note), which does the read-and-concatenate for you and always round-trips the body as HTML. If you do read-modify-write by hand, note that `get-note-content` replaces oversized inline images with text placeholders (see [`get-note-content`](#get-note-content)) — writing that body back bakes the placeholders in.
|
|
419
430
|
|
|
420
431
|
**Attachments:** A full-body replace can drop embedded files, images, scans, PDFs, or audio. When a note may hold attachments, run [`list-attachments`](#list-attachments) first, and either save them with `save-attachment` or build a new note rather than overwriting. See the skill's [Attachment-Safe Updates](https://github.com/sweetrb/apple-notes-mcp/blob/main/skills/apple-notes/SKILL.md#attachment-safe-updates) guidance.
|
|
421
432
|
|
|
@@ -617,11 +628,11 @@ Lists all folders in an account with full hierarchical paths.
|
|
|
617
628
|
|
|
618
629
|
#### `create-folder`
|
|
619
630
|
|
|
620
|
-
Creates a new folder.
|
|
631
|
+
Creates a new folder, including a whole nested hierarchy in one call.
|
|
621
632
|
|
|
622
633
|
| Parameter | Type | Required | Description |
|
|
623
634
|
|-----------|------|----------|-------------|
|
|
624
|
-
| `name` | string | Yes |
|
|
635
|
+
| `name` | string | Yes | Folder name, or a nested path separated by `/` (e.g. `"Retro Tech/PC/CPUs"`). Every intermediate folder is created; segments that already exist are skipped |
|
|
625
636
|
| `account` | string | No | Account to create folder in (defaults to iCloud) |
|
|
626
637
|
|
|
627
638
|
**Example:**
|
|
@@ -631,7 +642,14 @@ Creates a new folder.
|
|
|
631
642
|
}
|
|
632
643
|
```
|
|
633
644
|
|
|
634
|
-
**
|
|
645
|
+
**Example - Create a nested hierarchy:**
|
|
646
|
+
```json
|
|
647
|
+
{
|
|
648
|
+
"name": "Work/Clients/Omnia"
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
**Returns:** Confirmation message. The call is **idempotent** — an already-existing folder (or path segment) is skipped rather than treated as an error, so it is safe to call before every `create-note` that targets a folder.
|
|
635
653
|
|
|
636
654
|
---
|
|
637
655
|
|
|
@@ -718,7 +736,7 @@ Deletes multiple notes at once by ID.
|
|
|
718
736
|
|
|
719
737
|
| Parameter | Type | Required | Description |
|
|
720
738
|
|-----------|------|----------|-------------|
|
|
721
|
-
| `ids` | string[] | Yes | Array of note IDs to delete |
|
|
739
|
+
| `ids` | string[] | Yes | Array of note IDs to delete (max 500 per request) |
|
|
722
740
|
|
|
723
741
|
**Returns:** Summary of successes and failures.
|
|
724
742
|
|
|
@@ -732,8 +750,8 @@ Moves multiple notes to a folder.
|
|
|
732
750
|
|
|
733
751
|
| Parameter | Type | Required | Description |
|
|
734
752
|
|-----------|------|----------|-------------|
|
|
735
|
-
| `ids` | string[] | Yes | Array of note IDs to move |
|
|
736
|
-
| `folder` | string | Yes | Destination folder name or nested path (e.g., `"Work/Clients"`) |
|
|
753
|
+
| `ids` | string[] | Yes | Array of note IDs to move (max 500 per request) |
|
|
754
|
+
| `folder` | string | Yes | Destination folder name or nested path (e.g., `"Work/Clients"`). Must already exist — create it with [`create-folder`](#create-folder) |
|
|
737
755
|
| `account` | string | No | Account containing the folder |
|
|
738
756
|
|
|
739
757
|
**Returns:** Summary of successes and failures.
|
|
@@ -770,7 +788,7 @@ Gets a note's content as Markdown instead of HTML. If the note contains checklis
|
|
|
770
788
|
|
|
771
789
|
Reads checklist done/undone state for a note. This bypasses the AppleScript limitation where `body of note` strips checklist state, by reading directly from the NoteStore SQLite database.
|
|
772
790
|
|
|
773
|
-
**Requires:** Full Disk Access for the MCP host process (see [Full Disk Access Setup](#full-disk-access
|
|
791
|
+
**Requires:** Full Disk Access for the MCP host process (see [Full Disk Access Setup](#full-disk-access)).
|
|
774
792
|
|
|
775
793
|
| Parameter | Type | Required | Description |
|
|
776
794
|
|-----------|------|----------|-------------|
|
|
@@ -798,7 +816,7 @@ Checklist for "Shopping List" (2/4 done):
|
|
|
798
816
|
|
|
799
817
|
Reads note metadata that AppleScript cannot expose, by querying the NoteStore SQLite database directly: pinned state, checklist flags, trash/recovery state, the preview snippet, and the password hint. The available fields vary by macOS version.
|
|
800
818
|
|
|
801
|
-
**Requires:** Full Disk Access for the MCP host process (see [Full Disk Access Setup](#full-disk-access
|
|
819
|
+
**Requires:** Full Disk Access for the MCP host process (see [Full Disk Access Setup](#full-disk-access)).
|
|
802
820
|
|
|
803
821
|
**BETA:** the NoteStore schema changes between macOS releases, so some fields can be absent on older or newer systems. The database is only ever read, never written.
|
|
804
822
|
|
|
@@ -1063,9 +1081,9 @@ MCP stores no secrets, but as a general rule keep only non-secret config here.
|
|
|
1063
1081
|
|
|
1064
1082
|
---
|
|
1065
1083
|
|
|
1066
|
-
## Full Disk Access
|
|
1084
|
+
## Full Disk Access
|
|
1067
1085
|
|
|
1068
|
-
|
|
1086
|
+
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-link`, the checklist annotations in `get-note-markdown`, and the database half of `get-sync-status`.
|
|
1069
1087
|
|
|
1070
1088
|
> 📘 **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.
|
|
1071
1089
|
|
|
@@ -1083,9 +1101,12 @@ The `get-checklist-state` tool and checklist annotations in `get-note-markdown`
|
|
|
1083
1101
|
|
|
1084
1102
|
### Without Full Disk Access
|
|
1085
1103
|
|
|
1086
|
-
|
|
1087
|
-
- `get-checklist-state`
|
|
1088
|
-
- `get-note-
|
|
1104
|
+
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:
|
|
1105
|
+
- `get-checklist-state` returns an error explaining that database access is needed
|
|
1106
|
+
- `get-note-metadata` returns the same kind of error — it has no non-database path
|
|
1107
|
+
- `get-note-link` returns an error on macOS 26+; on macOS 12–15 it still works via the AppleScript `note link` fallback
|
|
1108
|
+
- `get-note-markdown` returns plain list items without `[x]`/`[ ]` annotations (graceful fallback)
|
|
1109
|
+
- `get-sync-status` still answers, but reports no pending uploads and no active sync — treat that as "unknown", not "idle"
|
|
1089
1110
|
|
|
1090
1111
|
---
|
|
1091
1112
|
|
|
@@ -1104,21 +1125,12 @@ All other tools work normally without Full Disk Access. Only checklist state fea
|
|
|
1104
1125
|
|------------|--------|
|
|
1105
1126
|
| macOS only | Apple Notes and AppleScript are macOS-specific |
|
|
1106
1127
|
| 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)) |
|
|
1107
|
-
|
|
|
1128
|
+
| 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)) |
|
|
1108
1129
|
| Limited rich formatting | Use `format: "html"` on create/update for headings, lists, bold, code blocks; some complex formatting may not render |
|
|
1109
1130
|
| Title matching | Most operations require exact title matches |
|
|
1110
1131
|
| 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 |
|
|
1111
1132
|
| 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. |
|
|
1112
1133
|
|
|
1113
|
-
### Roadmap
|
|
1114
|
-
|
|
1115
|
-
A few capabilities are deliberately deferred to a future release, tracked as open issues:
|
|
1116
|
-
|
|
1117
|
-
- **Pinned-note support** ([#28](https://github.com/sweetrb/apple-notes-mcp/issues/28)) — Apple doesn't expose pin status via AppleScript.
|
|
1118
|
-
- **Tags / hashtags** ([#29](https://github.com/sweetrb/apple-notes-mcp/issues/29)).
|
|
1119
|
-
- **Note links** ([#30](https://github.com/sweetrb/apple-notes-mcp/issues/30)).
|
|
1120
|
-
- **Local integration-test suite** ([#31](https://github.com/sweetrb/apple-notes-mcp/issues/31)).
|
|
1121
|
-
|
|
1122
1134
|
### Creating Checklists
|
|
1123
1135
|
|
|
1124
1136
|
**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.
|
package/build/index.js
CHANGED
|
@@ -38669,6 +38669,14 @@ var StdioServerTransport = class {
|
|
|
38669
38669
|
|
|
38670
38670
|
// src/utils/applescript.ts
|
|
38671
38671
|
import { execFileSync } from "child_process";
|
|
38672
|
+
|
|
38673
|
+
// src/utils/docsUrls.ts
|
|
38674
|
+
var FULL_DISK_ACCESS_GUIDE_URL = "https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md";
|
|
38675
|
+
var NODE_RUNTIME_TCC_GUIDE_URL = "https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md";
|
|
38676
|
+
var AUTOMATION_PERMISSION_GUIDE_URL = "https://github.com/sweetrb/apple-notes-mcp#permission-denied";
|
|
38677
|
+
var AUTOMATION_REMEDIATION = `Grant automation access in System Settings > Privacy & Security > Automation to the app that launches this server (Claude Desktop / Terminal / iTerm2), then fully quit and relaunch it \u2014 run the doctor tool to verify. See: ${AUTOMATION_PERMISSION_GUIDE_URL}`;
|
|
38678
|
+
|
|
38679
|
+
// src/utils/applescript.ts
|
|
38672
38680
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
38673
38681
|
var DEFAULT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
38674
38682
|
function envPositiveNumber(name) {
|
|
@@ -38732,7 +38740,7 @@ var ERROR_MAPPINGS = [
|
|
|
38732
38740
|
// Permission errors
|
|
38733
38741
|
{
|
|
38734
38742
|
pattern: /not authorized|not permitted|access.*denied/i,
|
|
38735
|
-
message:
|
|
38743
|
+
message: `Permission denied. ${AUTOMATION_REMEDIATION}`
|
|
38736
38744
|
},
|
|
38737
38745
|
// Application not running
|
|
38738
38746
|
{
|
|
@@ -39000,10 +39008,6 @@ function embeddedMessage(field) {
|
|
|
39000
39008
|
return decodeMessage(bytes);
|
|
39001
39009
|
}
|
|
39002
39010
|
|
|
39003
|
-
// src/utils/docsUrls.ts
|
|
39004
|
-
var FULL_DISK_ACCESS_GUIDE_URL = "https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md";
|
|
39005
|
-
var NODE_RUNTIME_TCC_GUIDE_URL = "https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md";
|
|
39006
|
-
|
|
39007
39011
|
// src/utils/checklistParser.ts
|
|
39008
39012
|
var CHECKLIST_STYLE_TYPE = 103;
|
|
39009
39013
|
var NOTES_DB_PATH = path.join(
|
|
@@ -40033,7 +40037,7 @@ var AppleNotesManager = class {
|
|
|
40033
40037
|
`;
|
|
40034
40038
|
}
|
|
40035
40039
|
/**
|
|
40036
|
-
* Parses bulk listing output into deduplicated
|
|
40040
|
+
* Parses bulk listing output into deduplicated (title, id) pairs.
|
|
40037
40041
|
*
|
|
40038
40042
|
* Duplicate CoreData references are deduped by id; the limit is applied
|
|
40039
40043
|
* after dedup so duplicates never count against it.
|
|
@@ -40041,17 +40045,31 @@ var AppleNotesManager = class {
|
|
|
40041
40045
|
parseBulkListOutput(output, safeLimit) {
|
|
40042
40046
|
if (!output.trim()) return [];
|
|
40043
40047
|
const seenIds = /* @__PURE__ */ new Set();
|
|
40044
|
-
const
|
|
40048
|
+
const refs = [];
|
|
40045
40049
|
for (const item of output.split(RECORD_SEP)) {
|
|
40046
40050
|
const [title, id] = item.split(FIELD_SEP);
|
|
40047
40051
|
if (!title?.trim()) continue;
|
|
40048
40052
|
const noteId = id?.trim() || generateFallbackId();
|
|
40049
40053
|
if (seenIds.has(noteId)) continue;
|
|
40050
40054
|
seenIds.add(noteId);
|
|
40051
|
-
|
|
40052
|
-
if (safeLimit !== void 0 &&
|
|
40055
|
+
refs.push({ title: title.trim(), id: noteId });
|
|
40056
|
+
if (safeLimit !== void 0 && refs.length >= safeLimit) break;
|
|
40053
40057
|
}
|
|
40054
|
-
return
|
|
40058
|
+
return refs;
|
|
40059
|
+
}
|
|
40060
|
+
/**
|
|
40061
|
+
* Lists all notes in an account/folder as (title, id) pairs, unfiltered
|
|
40062
|
+
* and unlimited. Used internally where the id is needed to avoid
|
|
40063
|
+
* re-resolving identity by (possibly duplicated) title — see exportNotesAsJson.
|
|
40064
|
+
*/
|
|
40065
|
+
listNoteRefs(account, folder) {
|
|
40066
|
+
const folderRef = folder ? buildFolderReference(folder) : void 0;
|
|
40067
|
+
const script = buildAccountScopedScript({ account }, this.buildBulkListCommand({ folderRef }));
|
|
40068
|
+
const result = executeAppleScript(script);
|
|
40069
|
+
if (!result.success) {
|
|
40070
|
+
throw new Error(`Failed to list notes: ${result.error ?? "unknown error"}`);
|
|
40071
|
+
}
|
|
40072
|
+
return this.parseBulkListOutput(result.output);
|
|
40055
40073
|
}
|
|
40056
40074
|
/**
|
|
40057
40075
|
* Lists all notes in an account, optionally filtered by folder, date, and limit.
|
|
@@ -40086,9 +40104,9 @@ var AppleNotesManager = class {
|
|
|
40086
40104
|
const header = sepIdx === -1 ? result2.output : result2.output.slice(0, sepIdx);
|
|
40087
40105
|
const totalCount = Number.parseInt(header.trim(), 10);
|
|
40088
40106
|
const records = sepIdx === -1 ? "" : result2.output.slice(sepIdx + 1);
|
|
40089
|
-
const
|
|
40090
|
-
if (!Number.isNaN(totalCount) && (
|
|
40091
|
-
return
|
|
40107
|
+
const refs = this.parseBulkListOutput(records, safeLimit);
|
|
40108
|
+
if (!Number.isNaN(totalCount) && (refs.length >= safeLimit || totalCount <= safeLimit)) {
|
|
40109
|
+
return refs.map((ref) => ref.title);
|
|
40092
40110
|
}
|
|
40093
40111
|
}
|
|
40094
40112
|
const script = buildAccountScopedScript(
|
|
@@ -40099,7 +40117,7 @@ var AppleNotesManager = class {
|
|
|
40099
40117
|
if (!result.success) {
|
|
40100
40118
|
throw new Error(`Failed to list notes: ${result.error ?? "unknown error"}`);
|
|
40101
40119
|
}
|
|
40102
|
-
return this.parseBulkListOutput(result.output, safeLimit);
|
|
40120
|
+
return this.parseBulkListOutput(result.output, safeLimit).map((ref) => ref.title);
|
|
40103
40121
|
}
|
|
40104
40122
|
/**
|
|
40105
40123
|
* Lists all shared (collaborative) notes across all accounts.
|
|
@@ -40708,7 +40726,7 @@ var AppleNotesManager = class {
|
|
|
40708
40726
|
checks.push({
|
|
40709
40727
|
name: "permissions",
|
|
40710
40728
|
passed: !isPermError,
|
|
40711
|
-
message: isPermError ?
|
|
40729
|
+
message: isPermError ? `AppleScript permissions denied. ${AUTOMATION_REMEDIATION}` : `Permission check returned: ${permCheck.error}`
|
|
40712
40730
|
});
|
|
40713
40731
|
if (isPermError) {
|
|
40714
40732
|
return { healthy: false, checks };
|
|
@@ -41416,13 +41434,14 @@ var AppleNotesManager = class {
|
|
|
41416
41434
|
name: folder.name,
|
|
41417
41435
|
notes: []
|
|
41418
41436
|
};
|
|
41419
|
-
const
|
|
41420
|
-
for (const
|
|
41421
|
-
const note = this.
|
|
41437
|
+
const noteRefs = this.listNoteRefs(account.name, folder.name);
|
|
41438
|
+
for (const ref of noteRefs) {
|
|
41439
|
+
const note = this.getNoteById(ref.id);
|
|
41422
41440
|
if (!note) continue;
|
|
41441
|
+
note.account = account.name;
|
|
41423
41442
|
let content = "";
|
|
41424
41443
|
if (!note.passwordProtected) {
|
|
41425
|
-
content = this.
|
|
41444
|
+
content = this.getNoteContentById(ref.id);
|
|
41426
41445
|
}
|
|
41427
41446
|
folderData.notes.push(this.exportNote(note, content));
|
|
41428
41447
|
exportData.summary.totalNotes++;
|
|
@@ -41818,7 +41837,7 @@ function strippedImagesWarning(stripped) {
|
|
|
41818
41837
|
|
|
41819
41838
|
\u26A0\uFE0F ${stripped.strippedCount} inline ${plural} (~${formatBytes(
|
|
41820
41839
|
stripped.strippedBytes
|
|
41821
|
-
)} decoded) exceeded the per-image inline cap and ${stripped.strippedCount === 1 ? "was" : "were"} replaced with placeholders so the response stays within MCP message limits. The images are still in the note: use list-attachments with save-attachment or fetch-attachment to export them, or raise APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES.`;
|
|
41840
|
+
)} decoded) exceeded the per-image inline cap and ${stripped.strippedCount === 1 ? "was" : "were"} replaced with placeholders so the response stays within MCP message limits. This body is therefore lossy \u2014 do NOT write it back with update-note, or the real images are replaced by the placeholder text; use append-to-note to add content. The images are still in the note: use list-attachments with save-attachment or fetch-attachment to export them, or raise APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES.`;
|
|
41822
41841
|
}
|
|
41823
41842
|
|
|
41824
41843
|
// src/utils/updateResponseTitle.ts
|
|
@@ -41909,7 +41928,7 @@ function runDoctor(manager) {
|
|
|
41909
41928
|
checks.push({
|
|
41910
41929
|
name: "Full Disk Access",
|
|
41911
41930
|
status: fda ? "ok" : "warn",
|
|
41912
|
-
detail: fda ? "granted \u2014 checklist
|
|
41931
|
+
detail: fda ? "granted \u2014 the Notes database is readable (checklist state, note metadata, note links, sync detail)" : `not granted \u2014 get-checklist-state, get-note-metadata, and the checklist annotations in get-note-markdown won't work; get-note-link fails on macOS 26+ (macOS 12-15 falls back to AppleScript); get-sync-status still answers but cannot see pending uploads. Everything else is pure AppleScript and is unaffected. In System Settings > Privacy & Security > Full Disk Access, grant access to the app that launches this server (Claude Desktop / Terminal / iTerm2), then fully quit and relaunch it and re-run doctor. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL}`
|
|
41913
41932
|
});
|
|
41914
41933
|
checks.push(checkNodeRuntimeSignature());
|
|
41915
41934
|
const healthy = !checks.some((c) => c.status === "fail");
|
|
@@ -42110,7 +42129,7 @@ var folderNameSchema = {
|
|
|
42110
42129
|
server.registerTool(
|
|
42111
42130
|
"create-note",
|
|
42112
42131
|
{
|
|
42113
|
-
description: "Use when: the user wants to create a brand-new Apple Note.\nReturns: the new note's title and id \u2014 reuse the id for follow-up reads/edits.\nDo not use when: editing an existing note (use update-note).\nNote: the title is prepended as an <h1>; true Apple Notes checklists cannot be created via AppleScript (see the content field).",
|
|
42132
|
+
description: "Use when: the user wants to create a brand-new Apple Note.\nReturns: the new note's title and id \u2014 reuse the id for follow-up reads/edits.\nDo not use when: editing an existing note (use update-note).\nNote: the title is prepended as an <h1>; true Apple Notes checklists cannot be created via AppleScript (see the content field). A 'folder' must already exist \u2014 create-folder first (it is idempotent), since this tool does not create it.",
|
|
42114
42133
|
inputSchema: {
|
|
42115
42134
|
title: external_exports.string().min(1, "Title is required").max(MAX.TITLE),
|
|
42116
42135
|
content: external_exports.string().min(1, "Content is required").max(MAX.CONTENT).describe(
|
|
@@ -42120,8 +42139,12 @@ server.registerTool(
|
|
|
42120
42139
|
tags: external_exports.array(external_exports.string().max(MAX.TAG)).max(MAX.TAGS).optional().describe(
|
|
42121
42140
|
"Returned-only metadata \u2014 NOT written to Notes.app. Apple Notes tags can't be set via AppleScript, so any values passed here are echoed back in the response but do not appear on the created note. Use #hashtags inside the content body instead (Notes.app turns those into real tags)."
|
|
42122
42141
|
),
|
|
42123
|
-
folder: external_exports.string().max(MAX.FOLDER).optional().describe(
|
|
42124
|
-
|
|
42142
|
+
folder: external_exports.string().max(MAX.FOLDER).optional().describe(
|
|
42143
|
+
"Folder to create the note in (supports nested paths like 'Work/Clients'). The folder must already exist \u2014 this tool does not create it; call create-folder first, which is idempotent and creates intermediate segments."
|
|
42144
|
+
),
|
|
42145
|
+
account: external_exports.string().max(MAX.ACCOUNT).optional().describe(
|
|
42146
|
+
"Account name (defaults to iCloud). Must be an account Notes.app already has configured \u2014 see list-accounts."
|
|
42147
|
+
)
|
|
42125
42148
|
},
|
|
42126
42149
|
outputSchema: {
|
|
42127
42150
|
ok: external_exports.boolean().optional(),
|
|
@@ -42134,8 +42157,9 @@ server.registerTool(
|
|
|
42134
42157
|
withErrorHandling(({ title, content, format = "plaintext", tags = [], folder, account }) => {
|
|
42135
42158
|
const note = notesManager.createNote(title, content, tags, folder, account, format);
|
|
42136
42159
|
if (!note) {
|
|
42160
|
+
const target = folder ? ` Most often the folder "${folder}" does not exist: run list-folders to check, then create-folder to create it (it is idempotent and creates intermediate segments).` : account ? ` Most often the account "${account}" is not configured in Notes.app: run list-accounts to check the exact name.` : "";
|
|
42137
42161
|
return errorResponse(
|
|
42138
|
-
`Failed to create note "${title}"
|
|
42162
|
+
`Failed to create note "${title}".${target} Otherwise check that Notes.app is running and this server has Automation access (run the doctor tool).`
|
|
42139
42163
|
);
|
|
42140
42164
|
}
|
|
42141
42165
|
const checklistWarning = detectChecklistAttempt(content) ?? "";
|
|
@@ -42224,7 +42248,7 @@ ${noteList}${truncationNote}${syncNote}`,
|
|
|
42224
42248
|
server.registerTool(
|
|
42225
42249
|
"get-note-content",
|
|
42226
42250
|
{
|
|
42227
|
-
description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the note's content plus parsed hashtags.\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.",
|
|
42251
|
+
description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the note's content plus parsed hashtags, and strippedImages/truncated when the body was capped.\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 \u2014 do NOT write it back with update-note or the real images are replaced by that text. Use append-to-note to add content, or export the images with save-attachment / fetch-attachment first.",
|
|
42228
42252
|
inputSchema: {
|
|
42229
42253
|
id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
|
|
42230
42254
|
title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
|
|
@@ -42233,7 +42257,11 @@ server.registerTool(
|
|
|
42233
42257
|
outputSchema: {
|
|
42234
42258
|
title: external_exports.string().optional(),
|
|
42235
42259
|
content: external_exports.string().optional(),
|
|
42236
|
-
hashtags: external_exports.array(external_exports.string()).optional()
|
|
42260
|
+
hashtags: external_exports.array(external_exports.string()).optional(),
|
|
42261
|
+
/** Number of oversized inline images replaced with text placeholders. */
|
|
42262
|
+
strippedImages: external_exports.number().optional(),
|
|
42263
|
+
/** True when content is lossy — see strippedImages. Never write a truncated body back. */
|
|
42264
|
+
truncated: external_exports.boolean().optional()
|
|
42237
42265
|
}
|
|
42238
42266
|
},
|
|
42239
42267
|
withErrorHandling(({ id, title, account }) => {
|
|
@@ -42258,7 +42286,9 @@ server.registerTool(
|
|
|
42258
42286
|
return successResponse(warning2 ? content2 + warning2 : content2, {
|
|
42259
42287
|
title: note2.title,
|
|
42260
42288
|
content: content2,
|
|
42261
|
-
hashtags: hashtags2
|
|
42289
|
+
hashtags: hashtags2,
|
|
42290
|
+
strippedImages: stripped2.strippedCount,
|
|
42291
|
+
truncated: stripped2.strippedCount > 0
|
|
42262
42292
|
});
|
|
42263
42293
|
}
|
|
42264
42294
|
if (!title) {
|
|
@@ -42281,7 +42311,13 @@ server.registerTool(
|
|
|
42281
42311
|
const content = stripped.html;
|
|
42282
42312
|
const hashtags = parseHashtags(content);
|
|
42283
42313
|
const warning = strippedImagesWarning(stripped);
|
|
42284
|
-
return successResponse(warning ? content + warning : content, {
|
|
42314
|
+
return successResponse(warning ? content + warning : content, {
|
|
42315
|
+
title,
|
|
42316
|
+
content,
|
|
42317
|
+
hashtags,
|
|
42318
|
+
strippedImages: stripped.strippedCount,
|
|
42319
|
+
truncated: stripped.strippedCount > 0
|
|
42320
|
+
});
|
|
42285
42321
|
}, "Error retrieving note content")
|
|
42286
42322
|
);
|
|
42287
42323
|
server.registerTool(
|
|
@@ -42422,7 +42458,7 @@ server.registerTool(
|
|
|
42422
42458
|
server.registerTool(
|
|
42423
42459
|
"get-note-link",
|
|
42424
42460
|
{
|
|
42425
|
-
description: "Use when: you need the notes:// deep-link URL for a note so it can be stored in a Reminders task, shared, or opened directly.\nReturns: a notes://showNote?identifier=<uuid> URL that opens the note in Notes.app on iOS and macOS.\nDo not use when: you only need the note's CoreData id (get-note-by-id) or want to reveal the note on screen (show-note).\nNote:
|
|
42461
|
+
description: "Use when: you need the notes:// deep-link URL for a note so it can be stored in a Reminders task, shared, or opened directly.\nReturns: a notes://showNote?identifier=<uuid> URL that opens the note in Notes.app on iOS and macOS.\nDo not use when: you only need the note's CoreData id (get-note-by-id) or want to reveal the note on screen (show-note).\nNote: the primary path reads the note's identifier from the Notes database, so it needs Full Disk Access for the app that launches this server; macOS 12-15 can fall back to the AppleScript 'note link' property, which macOS 26+ no longer exposes. Password-protected notes cannot be linked.",
|
|
42426
42462
|
inputSchema: {
|
|
42427
42463
|
id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
|
|
42428
42464
|
title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
|
|
@@ -43117,7 +43153,7 @@ server.registerTool(
|
|
|
43117
43153
|
return ` ${icon} ${c.name}: ${c.message}`;
|
|
43118
43154
|
}).join("\n");
|
|
43119
43155
|
const fdaAvailable = hasFullDiskAccess();
|
|
43120
|
-
const fdaLine = fdaAvailable ? " \u2713 full_disk_access: Granted (checklist
|
|
43156
|
+
const fdaLine = fdaAvailable ? " \u2713 full_disk_access: Granted (Notes database readable \u2014 checklist state, note metadata, note links, sync detail)" : ` \u24D8 full_disk_access: Not granted \u2014 get-checklist-state, get-note-metadata, and the checklist annotations in get-note-markdown won't work; get-note-link fails on macOS 26+ (macOS 12-15 falls back to AppleScript); get-sync-status cannot see pending uploads. The rest of the server is pure AppleScript and is unaffected. In System Settings > Privacy & Security > Full Disk Access, grant access to the app that launches this server (Claude Desktop / Terminal / iTerm2), then fully quit and relaunch it. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL} \u2014 run the doctor tool to verify.`;
|
|
43121
43157
|
return successResponse(`${statusIcon} ${statusText}
|
|
43122
43158
|
|
|
43123
43159
|
${checkLines}
|
|
@@ -43248,7 +43284,7 @@ server.registerTool(
|
|
|
43248
43284
|
{
|
|
43249
43285
|
description: "Use when: permanently deleting multiple notes by id in one call.\nReturns: per-id success/failure counts.\nDo not use when: deleting a single note (delete-note).\nSafety: requires explicit user confirmation; this is destructive and not undoable. Prefer search-notes/list-notes first to confirm the exact ids being deleted.",
|
|
43250
43286
|
inputSchema: {
|
|
43251
|
-
ids: external_exports.array(external_exports.string().max(MAX.ID)).max(MAX.BATCH_IDS).describe(
|
|
43287
|
+
ids: external_exports.array(external_exports.string().max(MAX.ID)).max(MAX.BATCH_IDS).describe(`Array of note IDs to delete (max ${MAX.BATCH_IDS} per request)`)
|
|
43252
43288
|
},
|
|
43253
43289
|
outputSchema: {
|
|
43254
43290
|
ok: external_exports.boolean().optional(),
|
|
@@ -43284,8 +43320,10 @@ server.registerTool(
|
|
|
43284
43320
|
{
|
|
43285
43321
|
description: "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts.\nDo not use when: moving a single note (move-note).\nNote: the destination folder must already exist (create-folder).",
|
|
43286
43322
|
inputSchema: {
|
|
43287
|
-
ids: external_exports.array(external_exports.string().max(MAX.ID)).max(MAX.BATCH_IDS).describe(
|
|
43288
|
-
folder: external_exports.string().max(MAX.FOLDER).describe(
|
|
43323
|
+
ids: external_exports.array(external_exports.string().max(MAX.ID)).max(MAX.BATCH_IDS).describe(`Array of note IDs to move (max ${MAX.BATCH_IDS} per request)`),
|
|
43324
|
+
folder: external_exports.string().max(MAX.FOLDER).describe(
|
|
43325
|
+
'Destination folder name or nested path (e.g. "Work/Clients"). Must already exist \u2014 create-folder first.'
|
|
43326
|
+
),
|
|
43289
43327
|
account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the destination folder (defaults to iCloud)")
|
|
43290
43328
|
},
|
|
43291
43329
|
outputSchema: {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# AppleScript Limitations
|
|
2
2
|
|
|
3
3
|
Apple Notes is automated through its AppleScript dictionary. A few features that
|
|
4
|
-
exist in the Notes UI are simply **not exposed to AppleScript**, so
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
exist in the Notes UI are simply **not exposed to AppleScript**, so no script can
|
|
5
|
+
read or write them. Where this server recovers one of them anyway, it does so by
|
|
6
|
+
reading Notes' private `NoteStore.sqlite` store **read-only** — which needs
|
|
7
|
+
[Full Disk Access](./FULL-DISK-ACCESS.md). Each section below says which case it
|
|
8
|
+
is. This page documents what was investigated, how it was verified, and the
|
|
9
|
+
conclusion, so the limitation isn't re-investigated every release.
|
|
8
10
|
|
|
9
11
|
The full set of properties Notes exposes on a `note` is:
|
|
10
12
|
|
|
@@ -17,9 +19,9 @@ shared, body, id, name, plaintext
|
|
|
17
19
|
|
|
18
20
|
## Pinned notes (#28)
|
|
19
21
|
|
|
20
|
-
**Status: not feasible via AppleScript
|
|
21
|
-
the top of a folder, but the `note` class
|
|
22
|
-
it raises error `-1700`:
|
|
22
|
+
**Status: not feasible via AppleScript; readable via the NoteStore database.**
|
|
23
|
+
The Notes UI lets you pin a note to the top of a folder, but the `note` class
|
|
24
|
+
has no `pinned` property. Asking for it raises error `-1700`:
|
|
23
25
|
|
|
24
26
|
```applescript
|
|
25
27
|
tell application "Notes"
|
|
@@ -31,41 +33,61 @@ end tell
|
|
|
31
33
|
There is no alternative property, element, or command (`pin`, `pinned`,
|
|
32
34
|
`favorite`, …) in the dictionary. Pinned state lives only in Notes' private
|
|
33
35
|
Core Data store (`NoteStore.sqlite`), which is not part of the scriptable
|
|
34
|
-
surface
|
|
35
|
-
across macOS releases and outside what an AppleScript-based server should do —
|
|
36
|
-
and there is no supported way to *set* it at all.
|
|
36
|
+
surface, and there is no supported way to *set* it at all.
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
Reading it, however, did turn out to be worth doing. Since 2.5.0 the BETA
|
|
39
|
+
`get-note-metadata` tool queries `ZISPINNED` on `ZICCLOUDSYNCINGOBJECT` in that
|
|
40
|
+
store, opened **read-only**, feature-detecting each column with
|
|
41
|
+
`PRAGMA table_info` so it degrades instead of breaking when the private schema
|
|
42
|
+
changes across macOS releases. It requires
|
|
43
|
+
[Full Disk Access](./FULL-DISK-ACCESS.md) and is marked BETA precisely because
|
|
44
|
+
the schema is version-dependent.
|
|
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.
|
|
41
50
|
|
|
42
51
|
## Note-to-note links (#30)
|
|
43
52
|
|
|
44
|
-
**Status:
|
|
45
|
-
a link from one note to another in the UI, but
|
|
46
|
-
or element for
|
|
53
|
+
**Status: link *relationships* are not exposed; a shareable deep link is.**
|
|
54
|
+
Apple Notes lets you insert a link from one note to another in the UI, but
|
|
55
|
+
AppleScript exposes no property or element for that relationship:
|
|
47
56
|
|
|
48
57
|
- A `note` has no `URL`, `url`, or `link` property — each raises error `-2753`
|
|
49
58
|
(undefined). There is no element that enumerates outgoing/incoming links.
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
- Nothing in the dictionary inserts a link into a note's body.
|
|
60
|
+
|
|
61
|
+
A shareable deep link to a note *is* available, and has been since 2.6.0:
|
|
62
|
+
`get-note-link` returns a `notes://showNote?identifier=<uuid>` URL that opens
|
|
63
|
+
the note in Notes.app on macOS and iOS.
|
|
64
|
+
|
|
65
|
+
- **Primary path — the NoteStore database.** The UUID in that URL is
|
|
66
|
+
`ZIDENTIFIER` on `ZICCLOUDSYNCINGOBJECT`, read **read-only** from
|
|
67
|
+
`NoteStore.sqlite`. This works on every macOS version but needs
|
|
68
|
+
[Full Disk Access](./FULL-DISK-ACCESS.md).
|
|
69
|
+
- **Fallback — AppleScript.** On macOS 12–15 the Notes dictionary does expose a
|
|
70
|
+
two-word `note link` property, used when the database read fails. It is absent
|
|
71
|
+
from the Notes SDEF on macOS 26+, which is why the database is the primary
|
|
72
|
+
path. (`note link` is a different term from the `URL` / `url` / `link` names
|
|
73
|
+
probed above, which genuinely do not exist.)
|
|
74
|
+
- Password-protected notes return no link.
|
|
53
75
|
|
|
54
|
-
The
|
|
55
|
-
a note in the Notes UI by id:
|
|
76
|
+
The `show` command reveals an object in the Notes UI by id:
|
|
56
77
|
|
|
57
78
|
```applescript
|
|
58
79
|
tell application "Notes" to show note id "x-coredata://…/ICNote/p123"
|
|
59
80
|
```
|
|
60
81
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
It **is** wrapped, as `show-note`, `show-folder`, `show-account`, and
|
|
83
|
+
`show-attachment`. Those tools activate the Notes.app GUI, so they only do
|
|
84
|
+
something useful on a machine with an active desktop session; to read a note's
|
|
85
|
+
content, use `get-note-content` / `get-note-markdown` instead.
|
|
65
86
|
|
|
66
|
-
**Conclusion:**
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
69
91
|
|
|
70
92
|
## Tags / hashtags (#29)
|
|
71
93
|
|
package/docs/FULL-DISK-ACCESS.md
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
# Full Disk Access
|
|
1
|
+
# Full Disk Access
|
|
2
2
|
|
|
3
|
-
Apple Notes MCP works almost entirely without any special disk permission.
|
|
4
|
-
|
|
5
|
-
server:
|
|
3
|
+
Apple Notes MCP works almost entirely without any special disk permission. The
|
|
4
|
+
tools that need **Full Disk Access (FDA)** for the process that runs the MCP
|
|
5
|
+
server are the ones that read Notes' own SQLite store:
|
|
6
6
|
|
|
7
7
|
- **`get-checklist-state`** — reads a note's checklist done/undone state.
|
|
8
8
|
- **Checklist annotations in `get-note-markdown`** — the `[x]` / `[ ]` prefixes on
|
|
9
9
|
checklist items.
|
|
10
|
+
- **`get-note-metadata` (BETA)** — pinned state, checklist flags, trash/recovery
|
|
11
|
+
state, snippets, password hint. These columns exist nowhere else, so this tool
|
|
12
|
+
needs FDA unconditionally.
|
|
13
|
+
- **`get-note-link`** — its primary path reads the note's `ZIDENTIFIER` from the
|
|
14
|
+
database. On macOS 12–15 it can fall back to the AppleScript `note link`
|
|
15
|
+
property; that property is absent from the Notes SDEF on macOS 26+, so there
|
|
16
|
+
FDA is the only route.
|
|
17
|
+
- **`get-sync-status`** — degrades rather than fails: without database access it
|
|
18
|
+
cannot see pending uploads or recent write activity.
|
|
10
19
|
|
|
11
20
|
Everything else (creating, reading, searching, updating, moving, deleting notes;
|
|
12
21
|
folders, accounts, attachments, stats, export, etc.) works **without** Full Disk
|
|
@@ -17,7 +26,9 @@ Access.
|
|
|
17
26
|
Apple Notes stores checklist items as a paragraph style inside a gzipped protobuf
|
|
18
27
|
blob in its SQLite store, `NoteStore.sqlite`. AppleScript's `body of note`
|
|
19
28
|
interface strips that state — it can't tell you whether a checklist item is
|
|
20
|
-
checked.
|
|
29
|
+
checked. The same store also holds the pinned/trash/snippet columns behind
|
|
30
|
+
`get-note-metadata` and the `ZIDENTIFIER` value behind `get-note-link`. To
|
|
31
|
+
recover any of them, the MCP reads the SQLite store directly.
|
|
21
32
|
|
|
22
33
|
That database lives in a macOS-protected directory:
|
|
23
34
|
|
|
@@ -26,9 +37,8 @@ That database lives in a macOS-protected directory:
|
|
|
26
37
|
```
|
|
27
38
|
|
|
28
39
|
Reading anything under `~/Library/Group Containers/` requires **Full Disk
|
|
29
|
-
Access** for the host process — without it, macOS denies the read
|
|
30
|
-
|
|
31
|
-
writes to it.)
|
|
40
|
+
Access** for the host process — without it, macOS denies the read. (The MCP only
|
|
41
|
+
ever **reads** this database; it never writes to it.)
|
|
32
42
|
|
|
33
43
|
## How to grant Full Disk Access
|
|
34
44
|
|
|
@@ -64,9 +74,16 @@ The server degrades gracefully — nothing crashes:
|
|
|
64
74
|
|
|
65
75
|
- `get-checklist-state` returns a clear error explaining that database access is
|
|
66
76
|
needed (and points here).
|
|
77
|
+
- `get-note-metadata` returns the same kind of error — it has no non-database
|
|
78
|
+
path, so it cannot answer at all without FDA.
|
|
79
|
+
- `get-note-link` returns an error on macOS 26+. On macOS 12–15 it still works,
|
|
80
|
+
via the AppleScript `note link` fallback.
|
|
67
81
|
- `get-note-markdown` still returns the note as Markdown, but checklist items
|
|
68
82
|
appear as plain list items without the `[x]`/`[ ]` annotations.
|
|
69
|
-
-
|
|
83
|
+
- `get-sync-status` still answers, but with no database visibility it reports no
|
|
84
|
+
pending uploads and no active sync — treat that as "unknown", not "idle".
|
|
85
|
+
- **Every other tool works normally**, since the rest of the server is pure
|
|
86
|
+
AppleScript.
|
|
70
87
|
|
|
71
88
|
See also: [Known Limitations](../README.md#known-limitations) and
|
|
72
89
|
[Creating Checklists](../README.md#creating-checklists) in the README.
|
|
@@ -11,29 +11,33 @@ of code already hardened in apple-mail. Line numbers are against `main` @ `bb677
|
|
|
11
11
|
|
|
12
12
|
## Resolution status
|
|
13
13
|
|
|
14
|
+
All seventeen findings are closed; issues #16–#32 are all closed on GitHub. The
|
|
15
|
+
body below is kept as the dated 1.4.4 snapshot that motivated the 2.0 line — it
|
|
16
|
+
describes the code as it was on 2026-06-19, **not** as it is today.
|
|
17
|
+
|
|
14
18
|
| # | Finding | Tier | Issue | Status |
|
|
15
19
|
|---|---------|------|-------|--------|
|
|
16
|
-
| H1 | `execSync` has no `maxBuffer` cap | High | [#16](https://github.com/sweetrb/apple-notes-mcp/issues/16) |
|
|
17
|
-
| H2 | No `with timeout` + SIGTERM wedges Notes.app | High | [#17](https://github.com/sweetrb/apple-notes-mcp/issues/17) |
|
|
18
|
-
| H3 | Printable `\|\|\|` / comma delimiters collide with user content | High | [#18](https://github.com/sweetrb/apple-notes-mcp/issues/18) |
|
|
19
|
-
| H4 | Swallowed failures return `[]`/`null`/`0` | High | [#19](https://github.com/sweetrb/apple-notes-mcp/issues/19) |
|
|
20
|
-
| H5 | Unbounded full-library scans, no partial-result signal | High | [#20](https://github.com/sweetrb/apple-notes-mcp/issues/20) |
|
|
21
|
-
| M1 | No `structuredContent` on any tool | Medium | [#21](https://github.com/sweetrb/apple-notes-mcp/issues/21) |
|
|
22
|
-
| M2 | No `doctor` tool (incl. Full Disk Access check) | Medium | [#22](https://github.com/sweetrb/apple-notes-mcp/issues/22) |
|
|
23
|
-
| M3 | No MCP resources or prompts | Medium | [#23](https://github.com/sweetrb/apple-notes-mcp/issues/23) |
|
|
24
|
-
| M4 | No file-based config loader | Medium | [#24](https://github.com/sweetrb/apple-notes-mcp/issues/24) |
|
|
25
|
-
| M5 | Locale-fragile date parsing | Medium | [#25](https://github.com/sweetrb/apple-notes-mcp/issues/25) |
|
|
26
|
-
| M6 | Batch ops are N+1 osascript fan-out | Medium | [#26](https://github.com/sweetrb/apple-notes-mcp/issues/26) |
|
|
27
|
-
| M7 | No `save-attachment` / `fetch-attachment` | Medium | [#27](https://github.com/sweetrb/apple-notes-mcp/issues/27) |
|
|
28
|
-
| L1 | Pinned notes not exposed | Low | [#28](https://github.com/sweetrb/apple-notes-mcp/issues/28) |
|
|
29
|
-
| L2 | Tags/hashtags not surfaced | Low | [#29](https://github.com/sweetrb/apple-notes-mcp/issues/29) |
|
|
30
|
-
| L3 | Note-to-note links not supported | Low | [#30](https://github.com/sweetrb/apple-notes-mcp/issues/30) |
|
|
31
|
-
| L5 | No integration test suite | Low | [#31](https://github.com/sweetrb/apple-notes-mcp/issues/31) |
|
|
32
|
-
| L6 | Full Disk Access guide + commit this audit | Low | [#32](https://github.com/sweetrb/apple-notes-mcp/issues/32) |
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
| H1 | `execSync` has no `maxBuffer` cap | High | [#16](https://github.com/sweetrb/apple-notes-mcp/issues/16) | Shipped 2.0.0 (64 MB default, `APPLE_NOTES_MCP_MAX_BUFFER`) |
|
|
21
|
+
| H2 | No `with timeout` + SIGTERM wedges Notes.app | High | [#17](https://github.com/sweetrb/apple-notes-mcp/issues/17) | Shipped 2.0.0 (`with timeout` wrap + `killSignal: SIGKILL`) |
|
|
22
|
+
| H3 | Printable `\|\|\|` / comma delimiters collide with user content | High | [#18](https://github.com/sweetrb/apple-notes-mcp/issues/18) | Shipped 2.0.0 (ASCII `\x1f` / `\x1e` delimiters) |
|
|
23
|
+
| H4 | Swallowed failures return `[]`/`null`/`0` | High | [#19](https://github.com/sweetrb/apple-notes-mcp/issues/19) | Shipped 2.0.0 (failures surface as MCP errors) + 2.1.0 (`get-notes-stats` partial-coverage reporting) |
|
|
24
|
+
| H5 | Unbounded full-library scans, no partial-result signal | High | [#20](https://github.com/sweetrb/apple-notes-mcp/issues/20) | Shipped 2.0.0 (server-side counting) |
|
|
25
|
+
| M1 | No `structuredContent` on any tool | Medium | [#21](https://github.com/sweetrb/apple-notes-mcp/issues/21) | Shipped 2.0.0, completed 2.2.0 (all tools) and 2.3.0 (`outputSchema` on all tools) |
|
|
26
|
+
| M2 | No `doctor` tool (incl. Full Disk Access check) | Medium | [#22](https://github.com/sweetrb/apple-notes-mcp/issues/22) | Shipped 2.0.0 |
|
|
27
|
+
| M3 | No MCP resources or prompts | Medium | [#23](https://github.com/sweetrb/apple-notes-mcp/issues/23) | Shipped 2.0.0 |
|
|
28
|
+
| M4 | No file-based config loader | Medium | [#24](https://github.com/sweetrb/apple-notes-mcp/issues/24) | Shipped 2.0.0 |
|
|
29
|
+
| M5 | Locale-fragile date parsing | Medium | [#25](https://github.com/sweetrb/apple-notes-mcp/issues/25) | Shipped 2.0.0 |
|
|
30
|
+
| M6 | Batch ops are N+1 osascript fan-out | Medium | [#26](https://github.com/sweetrb/apple-notes-mcp/issues/26) | Shipped 2.1.0 (one osascript spawn per batch; explicitly deferred in 2.0.0) |
|
|
31
|
+
| M7 | No `save-attachment` / `fetch-attachment` | Medium | [#27](https://github.com/sweetrb/apple-notes-mcp/issues/27) | Shipped 2.0.0 |
|
|
32
|
+
| L1 | Pinned notes not exposed | Low | [#28](https://github.com/sweetrb/apple-notes-mcp/issues/28) | Partial — AppleScript infeasibility documented 2.1.0 ([APPLESCRIPT-LIMITATIONS](./APPLESCRIPT-LIMITATIONS.md)); pin state made **readable** in 2.5.0 via the BETA `get-note-metadata` NoteStore path. Setting is still unsupported |
|
|
33
|
+
| L2 | Tags/hashtags not surfaced | Low | [#29](https://github.com/sweetrb/apple-notes-mcp/issues/29) | Shipped 2.1.0, read-only — `get-note-content` parses inline `#hashtags`; `create-note`'s `tags` param stays a cosmetic pass-through |
|
|
34
|
+
| L3 | Note-to-note links not supported | Low | [#30](https://github.com/sweetrb/apple-notes-mcp/issues/30) | Partial — closed 2.1.0 as documented-infeasible, then superseded in 2.6.0 by `get-note-link` (`notes://showNote?identifier=<uuid>`). Reading link *relationships*, and inserting a link, remain infeasible |
|
|
35
|
+
| L5 | No integration test suite | Low | [#31](https://github.com/sweetrb/apple-notes-mcp/issues/31) | Shipped 2.1.0 (`test/integration.test.ts` + the required `integration` CI job) |
|
|
36
|
+
| L6 | Full Disk Access guide + commit this audit | Low | [#32](https://github.com/sweetrb/apple-notes-mcp/issues/32) | Shipped 2.0.0 ([FULL-DISK-ACCESS.md](./FULL-DISK-ACCESS.md)) |
|
|
37
|
+
|
|
38
|
+
The fixes landed across the 2.0.0–2.6.x line rather than in a single release;
|
|
39
|
+
per-item detail is in [CHANGELOG.md](../CHANGELOG.md). This document is retained
|
|
40
|
+
as a historical record and is not a live backlog.
|
|
37
41
|
|
|
38
42
|
---
|
|
39
43
|
|
package/package.json
CHANGED