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 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 and note-metadata features; see the [Full Disk Access Setup Guide](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md). Everything else works without it.
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
- ### Known limitations
156
+ ### AppleScript limitations
157
157
 
158
- A few Notes UI features are not exposed to AppleScript and therefore cannot be
159
- supported. See **[docs/APPLESCRIPT-LIMITATIONS.md](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/APPLESCRIPT-LIMITATIONS.md)**
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** — there is no `applenotes://` deep link or link property; the only stable handle is the `x-coredata://` note id.
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 preserve existing content, read it first (e.g. with `get-note-content`) and include it in `newContent`.
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 | Name for the new folder |
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
- **Returns:** Confirmation message, or error if folder already exists.
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-for-checklist-features)).
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-for-checklist-features)).
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 for Checklist Features
1084
+ ## Full Disk Access
1067
1085
 
1068
- The `get-checklist-state` tool and checklist annotations in `get-note-markdown` read directly from the Apple Notes SQLite database. This requires **Full Disk Access** for the process running the MCP server.
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
- All other tools work normally without Full Disk Access. Only checklist state features are affected:
1087
- - `get-checklist-state` will return an error explaining that database access is needed
1088
- - `get-note-markdown` will return plain list items without `[x]`/`[ ]` annotations (graceful fallback)
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
- | No pinned notes | Pin status is not exposed via AppleScript ([#28](https://github.com/sweetrb/apple-notes-mcp/issues/28)) |
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: "Permission denied. Grant automation access in System Settings > Privacy & Security > Automation."
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 note titles.
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 titles = [];
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
- titles.push(title.trim());
40052
- if (safeLimit !== void 0 && titles.length >= safeLimit) break;
40055
+ refs.push({ title: title.trim(), id: noteId });
40056
+ if (safeLimit !== void 0 && refs.length >= safeLimit) break;
40053
40057
  }
40054
- return titles;
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 titles = this.parseBulkListOutput(records, safeLimit);
40090
- if (!Number.isNaN(totalCount) && (titles.length >= safeLimit || totalCount <= safeLimit)) {
40091
- return titles;
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 ? "AppleScript permissions denied. Grant access in System Settings > Privacy & Security > Automation" : `Permission check returned: ${permCheck.error}`
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 noteTitles = this.listNotes(account.name, folder.name);
41420
- for (const title of noteTitles) {
41421
- const note = this.getNoteDetails(title, account.name);
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.getNoteContent(title, account.name);
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 features available" : `not granted \u2014 get-checklist-state and checklist annotations in get-note-markdown won't work. 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}`
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("Folder to create the note in (supports nested paths like 'Work/Clients')"),
42124
- account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account name (defaults to iCloud)")
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}". Check that Notes.app is configured and accessible.`
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, { title, content, hashtags });
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: requires macOS 12+; returns an error on older systems.",
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 features available)" : ` \u24D8 full_disk_access: Not granted (optional \u2014 needed for get-checklist-state and checklist annotations in get-note-markdown). 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.`;
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("Array of note IDs to delete")
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("Array of note IDs to move"),
43288
- folder: external_exports.string().max(MAX.FOLDER).describe("Destination folder name"),
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 this MCP
5
- server cannot read or write them no matter how the script is written. This page
6
- documents what was investigated, how it was verified, and the conclusion, so the
7
- limitation isn't re-investigated every release.
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.** The Notes UI lets you pin a note to
21
- the top of a folder, but the `note` class has no `pinned` property. Asking for
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. Reading it would require parsing the SQLite store directly brittle
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
- **Conclusion:** pinned read/write is not supported and will not be added while
39
- Notes lacks a scriptable property. If a future macOS exposes one, revisit by
40
- re-running the probe above.
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: not supported as data; navigation-only.** Apple Notes lets you insert
45
- a link from one note to another in the UI, but AppleScript exposes no property
46
- or element for it:
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
- - There is no readable or constructable `applenotes://` / `notes://` deep link.
51
- The note's `id` (`x-coredata://…/ICNote/p123`) is the only stable handle, and
52
- it is a Core Data URI, not a shareable or clickable link.
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 one related capability that *does* work is the `show` command, which reveals
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
- This is deliberately **not** wrapped as a tool: it pops the GUI (unhelpful for a
62
- headless server), and an agent already has the note's content via
63
- `get-note-content` / `get-note-markdown`. Link relationships between notes
64
- cannot be read at all, so a "list links in this note" feature is not possible.
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:** note-to-note link data is not exposed and cannot be surfaced.
67
- The `id` field already returned by every read tool is the canonical reference;
68
- use that to address a specific note.
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
 
@@ -1,12 +1,21 @@
1
- # Full Disk Access for Checklist Features
1
+ # Full Disk Access
2
2
 
3
- Apple Notes MCP works almost entirely without any special disk permission. **One
4
- feature area** needs **Full Disk Access (FDA)** for the process that runs the MCP
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. To recover it, the MCP reads the SQLite store directly.
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 and the MCP
30
- cannot parse checklist state. (The MCP only ever **reads** this database; it never
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
- - **All other tools work normally.**
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) | Open |
17
- | H2 | No `with timeout` + SIGTERM wedges Notes.app | High | [#17](https://github.com/sweetrb/apple-notes-mcp/issues/17) | Open |
18
- | H3 | Printable `\|\|\|` / comma delimiters collide with user content | High | [#18](https://github.com/sweetrb/apple-notes-mcp/issues/18) | Open |
19
- | H4 | Swallowed failures return `[]`/`null`/`0` | High | [#19](https://github.com/sweetrb/apple-notes-mcp/issues/19) | Open |
20
- | H5 | Unbounded full-library scans, no partial-result signal | High | [#20](https://github.com/sweetrb/apple-notes-mcp/issues/20) | Open |
21
- | M1 | No `structuredContent` on any tool | Medium | [#21](https://github.com/sweetrb/apple-notes-mcp/issues/21) | Open |
22
- | M2 | No `doctor` tool (incl. Full Disk Access check) | Medium | [#22](https://github.com/sweetrb/apple-notes-mcp/issues/22) | Open |
23
- | M3 | No MCP resources or prompts | Medium | [#23](https://github.com/sweetrb/apple-notes-mcp/issues/23) | Open |
24
- | M4 | No file-based config loader | Medium | [#24](https://github.com/sweetrb/apple-notes-mcp/issues/24) | Open |
25
- | M5 | Locale-fragile date parsing | Medium | [#25](https://github.com/sweetrb/apple-notes-mcp/issues/25) | Open |
26
- | M6 | Batch ops are N+1 osascript fan-out | Medium | [#26](https://github.com/sweetrb/apple-notes-mcp/issues/26) | Open |
27
- | M7 | No `save-attachment` / `fetch-attachment` | Medium | [#27](https://github.com/sweetrb/apple-notes-mcp/issues/27) | Open |
28
- | L1 | Pinned notes not exposed | Low | [#28](https://github.com/sweetrb/apple-notes-mcp/issues/28) | Open |
29
- | L2 | Tags/hashtags not surfaced | Low | [#29](https://github.com/sweetrb/apple-notes-mcp/issues/29) | Open |
30
- | L3 | Note-to-note links not supported | Low | [#30](https://github.com/sweetrb/apple-notes-mcp/issues/30) | Open |
31
- | L5 | No integration test suite | Low | [#31](https://github.com/sweetrb/apple-notes-mcp/issues/31) | Open |
32
- | L6 | Full Disk Access guide + commit this audit | Low | [#32](https://github.com/sweetrb/apple-notes-mcp/issues/32) | Open |
33
-
34
- Target release for the fixes: **2.0.0** (full parity with apple-mail), built on a
35
- long-lived `v2` branch, one item at a time with tests, then full regression +
36
- docs + merge.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.6.11",
3
+ "version": "2.6.13",
4
4
  "description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",