apple-notes-mcp 2.15.17 → 2.15.18

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
@@ -2444,6 +2444,13 @@ after it finishes. The result carries `verifiedBy: "database"`. Without Full
2444
2444
  Disk Access a PDF attach reports "insertion outcome uncertain"; read the note
2445
2445
  before retrying, because the attachment was probably created.
2446
2446
 
2447
+ Notes.app's AppleScript insertion places every new attachment in the note body
2448
+ twice (#321): one attachment, shown two times. The tool removes the extra copy
2449
+ in the same AppleScript call and checks that the note lists the attachment once.
2450
+ A PDF on macOS 27 cannot be listed, so its extra copy cannot be removed; when the
2451
+ database shows the PDF twice, the tool returns an error naming the attachment.
2452
+ Delete the extra copy in Notes.app and do not attach the file again.
2453
+
2447
2454
  ---
2448
2455
 
2449
2456
  #### `create-note-with-attachment`
package/build/index.js CHANGED
@@ -57554,25 +57554,35 @@ function parseAttachmentRows(json3) {
57554
57554
  }
57555
57555
  return rows.sort((a, b) => a.pk - b.pk);
57556
57556
  }
57557
- function attachmentOrderFromNoteData(hex5) {
57557
+ function attachmentRunsFromNoteData(hex5) {
57558
57558
  if (!hex5 || !/^[0-9a-f]+$/i.test(hex5)) return null;
57559
57559
  try {
57560
57560
  const data = gunzipSync5(Buffer.from(hex5, "hex"), { maxOutputLength: MAX_BODY_BYTES });
57561
57561
  const wrapper = embeddedMessage(getField(decodeMessage(data), 2));
57562
57562
  const body = wrapper && embeddedMessage(getField(wrapper, 3));
57563
57563
  if (!body) return null;
57564
- const ids = [];
57564
+ const refs = [];
57565
57565
  for (const run of getFields(body, 5)) {
57566
57566
  const fields = embeddedMessage(run);
57567
57567
  const info = fields && embeddedMessage(getField(fields, 12));
57568
57568
  const id2 = info && stringValue(getField(info, 1));
57569
- if (id2 && !ids.some((seen) => seen.toLowerCase() === id2.toLowerCase())) ids.push(id2);
57569
+ if (!id2) continue;
57570
+ const length = varintValue(getField(fields, 1)) ?? 1;
57571
+ for (let i = 0; i < Math.min(Math.max(length, 1), 64); i++) refs.push(id2);
57570
57572
  }
57571
- return ids;
57573
+ return refs;
57572
57574
  } catch {
57573
57575
  return null;
57574
57576
  }
57575
57577
  }
57578
+ function attachmentOrderFromNoteData(hex5) {
57579
+ const refs = attachmentRunsFromNoteData(hex5);
57580
+ if (!refs) return null;
57581
+ const ids = [];
57582
+ for (const id2 of refs)
57583
+ if (!ids.some((seen) => seen.toLowerCase() === id2.toLowerCase())) ids.push(id2);
57584
+ return ids;
57585
+ }
57576
57586
  function readNoteAttachmentRows(noteId3, dbPath2 = join9(NOTES_CONTAINER_DIR, "NoteStore.sqlite")) {
57577
57587
  const { pk } = parseNoteId2(noteId3);
57578
57588
  const columnList = runSqlite3(
@@ -57596,7 +57606,8 @@ function readNoteAttachmentRows(noteId3, dbPath2 = join9(NOTES_CONTAINER_DIR, "N
57596
57606
  } catch {
57597
57607
  throw new AttachmentStoreError("Attachment rows could not be parsed", "query_error");
57598
57608
  }
57599
- return { rows, bodyOrder: attachmentOrderFromNoteData(lines[2]?.trim()) };
57609
+ const bodyRefs = attachmentRunsFromNoteData(lines[2]?.trim());
57610
+ return { rows, bodyOrder: attachmentOrderFromNoteData(lines[2]?.trim()), bodyRefs };
57600
57611
  }
57601
57612
  function safeComponent(value) {
57602
57613
  if (typeof value !== "string" || !value || value.length > 255) return null;
@@ -57759,13 +57770,16 @@ function assetPathsFor(accountDir, row, onStale) {
57759
57770
  add(file2);
57760
57771
  return found;
57761
57772
  }
57762
- function assembleAttachmentAssets(allRows, bodyOrder, containerDir = NOTES_CONTAINER_DIR) {
57773
+ function assembleAttachmentAssets(allRows, bodyOrder, containerDir = NOTES_CONTAINER_DIR, bodyRefs = null) {
57763
57774
  const rows = allRows.filter((r) => !r.parentDeleted);
57764
57775
  const byPk = new Map(rows.map((r) => [r.pk, r]));
57765
57776
  const roots = rows.filter((r) => r.parentPk === null || !byPk.has(r.parentPk));
57766
57777
  const bodyIndex = /* @__PURE__ */ new Map();
57767
57778
  (bodyOrder ?? []).forEach((id2, index) => bodyIndex.set(id2.toLowerCase(), index));
57768
57779
  const indexOf = (r) => bodyIndex.get(r.identifier.toLowerCase()) ?? null;
57780
+ const refCount = /* @__PURE__ */ new Map();
57781
+ for (const id2 of bodyRefs ?? [])
57782
+ refCount.set(id2.toLowerCase(), (refCount.get(id2.toLowerCase()) ?? 0) + 1);
57769
57783
  const inBody = roots.filter((r) => indexOf(r) !== null);
57770
57784
  const orderSource = inBody.length > 0 ? "body" : "creation";
57771
57785
  const orderedRoots = [
@@ -57799,6 +57813,7 @@ function assembleAttachmentAssets(allRows, bodyOrder, containerDir = NOTES_CONTA
57799
57813
  parentIdentifier: parent?.identifier ?? null,
57800
57814
  filename: row.filename ?? row.mediaFilename,
57801
57815
  bodyIndex: parent ? null : indexOf(row),
57816
+ ...!parent && (refCount.get(row.identifier.toLowerCase()) ?? 0) > 1 ? { bodyReferences: refCount.get(row.identifier.toLowerCase()) } : {},
57802
57817
  assetPaths,
57803
57818
  ...fallbackStale ? { fallbackStale: true } : {},
57804
57819
  previewPath,
@@ -60091,7 +60106,17 @@ var AppleNotesManager = class {
60091
60106
  if (!result.success) throw new Error(result.error || "Could not check folder existence");
60092
60107
  return result.output.trim() === "true";
60093
60108
  }
60094
- /** Insert one file into an unchanged exact note and return Notes' attachment ID. */
60109
+ /**
60110
+ * Insert one file into an unchanged exact note and return Notes' attachment ID.
60111
+ *
60112
+ * Notes.app's `make new attachment` writes the new attachment into the note
60113
+ * body twice (#321): one attachment object, two body references, so the file
60114
+ * shows twice. Every form of the command does it (`at n`, `at end of
60115
+ * attachments of n`, `tell n to make`, an alias), and `every attachment of n`
60116
+ * then lists the same id twice. Deleting the extra entries by index removes
60117
+ * only their body references; the attachment object and its file stay. The
60118
+ * cleanup runs in the same AppleScript so no other edit can interleave.
60119
+ */
60095
60120
  addAttachmentById(id2, expectedBody, filePath) {
60096
60121
  if (!/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i.test(id2))
60097
60122
  throw new Error("Exact note ID required");
@@ -60103,7 +60128,16 @@ var AppleNotesManager = class {
60103
60128
  if body of n is not ${quote(expectedBody)} and body of n is not (${quote(expectedBody)} & linefeed) then error "Note changed before attachment insertion"
60104
60129
  end considering
60105
60130
  set a to make new attachment at n with data (POSIX file ${quote(filePath)})
60106
- return id of a
60131
+ set newId to id of a
60132
+ set hits to {}
60133
+ set attachmentIds to id of every attachment of n
60134
+ repeat with i from 1 to count of attachmentIds
60135
+ if item i of attachmentIds is newId then set end of hits to i
60136
+ end repeat
60137
+ repeat with j from (count of hits) to 2 by -1
60138
+ delete attachment (item j of hits) of n
60139
+ end repeat
60140
+ return newId
60107
60141
  end tell`);
60108
60142
  if (!result.success)
60109
60143
  throw new Error(
@@ -60111,6 +60145,27 @@ var AppleNotesManager = class {
60111
60145
  );
60112
60146
  return result.output.trim();
60113
60147
  }
60148
+ /**
60149
+ * How many entries Notes' `every attachment` holds for one attachment id
60150
+ * (#321). Read-only. Notes on macOS 27 does not list PDF attachments, so a PDF
60151
+ * counts 0 here; the NoteStore database is the check for those.
60152
+ */
60153
+ countAttachmentEntriesById(noteId3, attachmentId) {
60154
+ if (!/^x-coredata:\/\/[0-9a-f-]+\/ICAttachment\/p\d+$/i.test(attachmentId))
60155
+ throw new Error("Exact attachment ID required");
60156
+ const result = executeAppleScript(`tell application "Notes"
60157
+ set attachmentIds to id of every attachment of note id "${sanitizeId(noteId3)}"
60158
+ set hits to 0
60159
+ repeat with i from 1 to count of attachmentIds
60160
+ if item i of attachmentIds is "${attachmentId}" then set hits to hits + 1
60161
+ end repeat
60162
+ return hits as text
60163
+ end tell`);
60164
+ if (!result.success) throw new Error(result.error || "Could not count attachment entries");
60165
+ const hits = Number(result.output.trim());
60166
+ if (!Number.isInteger(hits) || hits < 0) throw new Error("Unexpected attachment count");
60167
+ return hits;
60168
+ }
60114
60169
  /**
60115
60170
  * Lists all folders in an account with full hierarchical paths.
60116
60171
  *
@@ -61124,8 +61179,8 @@ var AppleNotesManager = class {
61124
61179
  * @throws AttachmentStoreError (`no_fda`, `invalid_id`, `not_found`, `query_error`)
61125
61180
  */
61126
61181
  getAttachmentAssetsById(noteId3) {
61127
- const { rows, bodyOrder } = readNoteAttachmentRows(noteId3);
61128
- return assembleAttachmentAssets(rows, bodyOrder);
61182
+ const { rows, bodyOrder, bodyRefs } = readNoteAttachmentRows(noteId3);
61183
+ return assembleAttachmentAssets(rows, bodyOrder, void 0, bodyRefs);
61129
61184
  }
61130
61185
  /**
61131
61186
  * The note's lead visual: the first image in body order (even when its asset
@@ -71262,7 +71317,7 @@ function registerDirectOperations(server2, manager) {
71262
71317
  };
71263
71318
  tool(
71264
71319
  "add-attachment",
71265
- "Use when: adding one local file to an exact note without replacing its body.\nReturns: the new attachment id, byte count, attachment name, and post-write content hash after exact byte verification.\nDo not use when: reading or exporting an existing attachment.\nSafety: reads only a regular file in home, temp or /Volumes, refusing hidden paths (~/.ssh, .env) and ~/Library outside iCloud Drive and CloudStorage unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1; requires a fresh rich revision, copies at most 64 MiB through a private temporary file, never retries insertion, and verifies existing content plus fetched bytes. On macOS 27 Notes' AppleScript does not list PDF attachments; with Full Disk Access the new attachment is verified through the read-only NoteStore database instead (verifiedBy: database), otherwise the outcome is reported as uncertain.",
71320
+ "Use when: adding one local file to an exact note without replacing its body.\nReturns: the new attachment id, byte count, attachment name, and post-write content hash after exact byte verification.\nDo not use when: reading or exporting an existing attachment.\nSafety: reads only a regular file in home, temp or /Volumes, refusing hidden paths (~/.ssh, .env) and ~/Library outside iCloud Drive and CloudStorage unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1; requires a fresh rich revision, copies at most 64 MiB through a private temporary file, never retries insertion, removes the duplicate copy Notes.app's AppleScript inserts (#321), and verifies existing content plus fetched bytes. On macOS 27 Notes' AppleScript does not list PDF attachments; with Full Disk Access the new attachment is verified through the read-only NoteStore database instead (verifiedBy: database), otherwise the outcome is reported as uncertain. A PDF that Notes shows twice cannot be cleaned up and is reported as an error: delete the extra copy in Notes.app.",
71266
71321
  { id: noteId, expectedContentHash: revision, ...attachmentInput },
71267
71322
  (args) => attachFile(manager, args)
71268
71323
  );
@@ -71401,6 +71456,7 @@ function attachmentName(path10, filename) {
71401
71456
  return filename;
71402
71457
  }
71403
71458
  var UNCERTAIN = "Attachment insertion outcome uncertain; read the exact note before retrying";
71459
+ var duplicated = (attachmentId, copies) => `Notes.app put new attachment ${attachmentId} into this note ${copies} times and the extra ${copies === 2 ? "copy" : "copies"} could not be removed automatically; ask the user to delete the extra ${copies === 2 ? "copy" : "copies"} in Notes.app, and do not attach the file again`;
71404
71460
  var pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
71405
71461
  var sha256 = (data) => createHash4("sha256").update(data).digest("hex");
71406
71462
  function storedAttachmentIds(manager, id2) {
@@ -71456,6 +71512,7 @@ function storedInsertion(manager, id2, before, bytes, name, returnedId) {
71456
71512
  throw new Error(
71457
71513
  `Notes' database shows new attachment ${attachmentId} on this note, but the note body does not reference it; read the exact note and do not attach the file again`
71458
71514
  );
71515
+ if ((row.bodyReferences ?? 1) > 1) throw new Error(duplicated(attachmentId, row.bodyReferences));
71459
71516
  const expected = sha256(bytes);
71460
71517
  const matches = row.assetPaths.some((path10) => fileMatches(path10, bytes.length, expected));
71461
71518
  if (!matches)
@@ -71506,6 +71563,8 @@ function attachFile(manager, args, checked, progress = { insertionStarted: false
71506
71563
  const saved = manager.saveAttachmentById(id2, attachmentId, verifyPath);
71507
71564
  if (!saved.success || !fileMatches(saved.savedPath ?? verifyPath, bytes.length, sha256(bytes)))
71508
71565
  throw new Error("Attachment bytes were not verified; read the exact note before retrying");
71566
+ const copies = manager.countAttachmentEntriesById(id2, attachmentId);
71567
+ if (copies > 1) throw new Error(duplicated(attachmentId, copies));
71509
71568
  } else {
71510
71569
  const stored = inserted.length === 0 && beforeStored ? storedInsertion(manager, id2, beforeStored, bytes, name, returnedId) : null;
71511
71570
  if (!stored)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.15.17",
3
+ "version": "2.15.18",
4
4
  "description": "MCP server for Apple Notes - read, search, create, edit, organize, and export notes on macOS via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",