apple-notes-mcp 2.6.4 → 2.6.6

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
@@ -807,6 +807,8 @@ Lists attachments in a note.
807
807
 
808
808
  **Returns:** List of attachments with IDs, names, content identifiers, URLs when available, created/modified dates, and shared state.
809
809
 
810
+ **⚠️ Safety:** A lookup failure is reported as an error, never as an empty list — so an empty result reliably means the note has no attachments and is safe to replace wholesale. Treat an error as "unknown", not "none".
811
+
810
812
  ---
811
813
 
812
814
  #### `save-attachment`
@@ -1019,8 +1021,8 @@ All configuration is optional — the server works out of the box. Override beha
1019
1021
  | `APPLE_NOTES_MCP_MAX_ATTACHMENT_BYTES` | `26214400` (25 MB) | Max size of an attachment that [`fetch-attachment`](#fetch-attachment) will base64-encode inline. Larger attachments are rejected with an error pointing at [`save-attachment`](#save-attachment) (which streams to disk and has no such limit). Raise it to fetch bigger attachments inline; lower it to cap memory. |
1020
1022
  | `APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES` | `262144` (256 KB) | Per-image cap on the base64 payload kept inline in a [`get-note-content`](#get-note-content) response. Inline images over the cap are replaced with placeholders (with a warning appended) so an image-heavy note cannot exceed the MCP client's message limit and drop the connection; export the real files with [`save-attachment`](#save-attachment) or [`fetch-attachment`](#fetch-attachment). Raise it to keep bigger images inline. |
1021
1023
  | `APPLE_NOTES_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-notes-mcp/config.json` | Path to the JSON config file (see below). |
1022
- | `APPLE_NOTES_MCP_TIMEOUT_MS` | `30000` (30 s) | Per-call AppleScript timeout. Raise it if full-library operations (large searches, exports) time out on a big Notes library. Per-call `timeoutMs` options still win. |
1023
- | `APPLE_NOTES_MCP_MAX_RETRIES` | `2` | Total attempts for an AppleScript call that fails with a **transient** error (Notes.app busy / not responding / lost connection / timeout). `2` means one retry; set `1` to fail fast with no retries. Non-transient errors (e.g. "note not found") never retry. |
1024
+ | `APPLE_NOTES_MCP_TIMEOUT_MS` | `30000` (30 s) | Total AppleScript operation timeout, including retry attempts and delays. Raise it if full-library operations (large searches, exports) time out on a big Notes library. Per-call `timeoutMs` options still win. |
1025
+ | `APPLE_NOTES_MCP_MAX_RETRIES` | `2` | Maximum attempts for a read-only AppleScript call that fails with a **transient** error (Notes.app busy / not responding / lost connection). `2` means one retry; set `1` to fail fast with no retries. Retries share the single `APPLE_NOTES_MCP_TIMEOUT_MS` budget rather than each getting a fresh one, and a retry is skipped when under a second of that budget remains — so this is a ceiling, not a guarantee. In particular a call that exhausts the budget with a **timeout** has no time left to retry by construction. Mutating operations run once because a timeout can occur after Notes.app applied the change. Non-transient errors (e.g. "note not found") never retry. |
1024
1026
  | `APPLE_NOTES_MCP_RETRY_DELAY_MS` | `1000` (1 s) | Base delay before the first retry; subsequent retries back off exponentially (1s, 2s, 4s, ...). |
1025
1027
  | `DEBUG` / `VERBOSE` | unset | Set either to enable verbose diagnostic logging to stderr. |
1026
1028
 
package/build/index.js CHANGED
@@ -38677,6 +38677,7 @@ function getMaxBuffer() {
38677
38677
  return envPositiveNumber("APPLE_NOTES_MCP_MAX_BUFFER") ?? DEFAULT_MAX_BUFFER_BYTES;
38678
38678
  }
38679
38679
  var SCRIPT_TIMEOUT_HEADROOM_MS = 5e3;
38680
+ var MIN_ATTEMPT_BUDGET_MS = 1e3;
38680
38681
  function wrapWithTimeout(script, processTimeoutMs) {
38681
38682
  const seconds = Math.max(1, Math.ceil((processTimeoutMs - SCRIPT_TIMEOUT_HEADROOM_MS) / 1e3));
38682
38683
  return `with timeout of ${seconds} seconds
@@ -38818,7 +38819,6 @@ function executeAppleScript(script, options = {}) {
38818
38819
  error: "Cannot execute empty AppleScript"
38819
38820
  };
38820
38821
  }
38821
- const preparedScript = wrapWithTimeout(script.trim(), timeoutMs);
38822
38822
  debugLog("Executing AppleScript", {
38823
38823
  scriptPreview: script.trim().substring(0, 200) + (script.length > 200 ? "..." : ""),
38824
38824
  timeout: timeoutMs,
@@ -38826,13 +38826,16 @@ function executeAppleScript(script, options = {}) {
38826
38826
  });
38827
38827
  let lastError = null;
38828
38828
  const startTime = Date.now();
38829
+ const deadline = startTime + timeoutMs;
38829
38830
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
38831
+ const attemptTimeoutMs = Math.max(1, deadline - Date.now());
38832
+ const preparedScript = wrapWithTimeout(script.trim(), attemptTimeoutMs);
38830
38833
  const attemptStart = Date.now();
38831
38834
  try {
38832
38835
  const output = execFileSync("osascript", ["-"], {
38833
38836
  input: preparedScript,
38834
38837
  encoding: "utf8",
38835
- timeout: timeoutMs,
38838
+ timeout: attemptTimeoutMs,
38836
38839
  // SIGKILL (not the default SIGTERM): a wedged osascript blocked on an
38837
38840
  // unresponsive Notes.app can ignore SIGTERM and leak, piling up and
38838
38841
  // worsening contention. SIGKILL guarantees reaping on timeout. (#17)
@@ -38885,8 +38888,9 @@ function executeAppleScript(script, options = {}) {
38885
38888
  };
38886
38889
  const canRetry = isTimeout || isRetryableError(errorMessage);
38887
38890
  const hasAttemptsLeft = attempt < maxRetries;
38888
- if (canRetry && hasAttemptsLeft) {
38889
- const delayMs = retryDelayMs * Math.pow(2, attempt - 1);
38891
+ const delayMs = retryDelayMs * Math.pow(2, attempt - 1);
38892
+ const hasTimeForRetry = Date.now() + delayMs + MIN_ATTEMPT_BUDGET_MS < deadline;
38893
+ if (canRetry && hasAttemptsLeft && hasTimeForRetry) {
38890
38894
  console.error(
38891
38895
  `AppleScript retry: Attempt ${attempt}/${maxRetries} failed with "${errorMessage}". Retrying in ${delayMs}ms...`
38892
38896
  );
@@ -39221,6 +39225,9 @@ var FIELD_SEP = "";
39221
39225
  var RECORD_SEP = "";
39222
39226
  var AS_FIELD_SEP = "(ASCII character 31)";
39223
39227
  var AS_RECORD_SEP = "(ASCII character 30)";
39228
+ function executeMutationAppleScript(script) {
39229
+ return executeAppleScript(script, { maxRetries: 1 });
39230
+ }
39224
39231
  function escapeForAppleScript(text) {
39225
39232
  if (!text) {
39226
39233
  return "";
@@ -39481,7 +39488,7 @@ var AppleNotesManager = class {
39481
39488
  `;
39482
39489
  }
39483
39490
  const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
39484
- const result = executeAppleScript(script);
39491
+ const result = executeMutationAppleScript(script);
39485
39492
  if (!result.success) {
39486
39493
  console.error(`Failed to create note "${title}":`, result.error);
39487
39494
  return null;
@@ -39824,7 +39831,7 @@ var AppleNotesManager = class {
39824
39831
  const safeTitle = escapePlainStringForAppleScript(title);
39825
39832
  const deleteCommand = `delete note "${safeTitle}"`;
39826
39833
  const script = buildAccountScopedScript({ account: targetAccount }, deleteCommand);
39827
- const result = executeAppleScript(script);
39834
+ const result = executeMutationAppleScript(script);
39828
39835
  if (!result.success) {
39829
39836
  console.error(`Failed to delete note "${title}":`, result.error);
39830
39837
  return false;
@@ -39844,7 +39851,7 @@ var AppleNotesManager = class {
39844
39851
  const safeId = sanitizeId(id);
39845
39852
  const deleteCommand = `delete note id "${safeId}"`;
39846
39853
  const script = buildAppLevelScript(deleteCommand);
39847
- const result = executeAppleScript(script);
39854
+ const result = executeMutationAppleScript(script);
39848
39855
  if (!result.success) {
39849
39856
  console.error(`Failed to delete note with ID "${id}":`, result.error);
39850
39857
  return false;
@@ -39888,7 +39895,7 @@ var AppleNotesManager = class {
39888
39895
  }
39889
39896
  const updateCommand = `set body of note "${safeCurrentTitle}" to "${fullBody}"`;
39890
39897
  const script = buildAccountScopedScript({ account: targetAccount }, updateCommand);
39891
- const result = executeAppleScript(script);
39898
+ const result = executeMutationAppleScript(script);
39892
39899
  if (!result.success) {
39893
39900
  console.error(`Failed to update note "${title}":`, result.error);
39894
39901
  return false;
@@ -39937,7 +39944,7 @@ var AppleNotesManager = class {
39937
39944
  const safeId = sanitizeId(id);
39938
39945
  const updateCommand = `set body of note id "${safeId}" to "${fullBody}"`;
39939
39946
  const script = buildAppLevelScript(updateCommand);
39940
- const result = executeAppleScript(script);
39947
+ const result = executeMutationAppleScript(script);
39941
39948
  if (!result.success) {
39942
39949
  console.error(`Failed to update note with ID "${id}":`, result.error);
39943
39950
  return false;
@@ -40256,7 +40263,7 @@ var AppleNotesManager = class {
40256
40263
  createCommand = `make new folder at ${parentRef} with properties {name:"${segmentName}"}`;
40257
40264
  }
40258
40265
  const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
40259
- const result = executeAppleScript(script);
40266
+ const result = executeMutationAppleScript(script);
40260
40267
  if (!result.success) {
40261
40268
  console.error(`Failed to create folder "${name}":`, result.error);
40262
40269
  return null;
@@ -40288,7 +40295,7 @@ var AppleNotesManager = class {
40288
40295
  const targetAccount = this.resolveAccount(account);
40289
40296
  const deleteCommand = `delete ${buildFolderReference(name)}`;
40290
40297
  const script = buildAccountScopedScript({ account: targetAccount }, deleteCommand);
40291
- const result = executeAppleScript(script);
40298
+ const result = executeMutationAppleScript(script);
40292
40299
  if (!result.success) {
40293
40300
  console.error(`Failed to delete folder "${name}":`, result.error);
40294
40301
  return false;
@@ -40345,7 +40352,7 @@ var AppleNotesManager = class {
40345
40352
  move noteRef to destFolder
40346
40353
  `;
40347
40354
  const script = buildAppLevelScript(moveCommand);
40348
- const result = executeAppleScript(script);
40355
+ const result = executeMutationAppleScript(script);
40349
40356
  if (!result.success) {
40350
40357
  console.error(
40351
40358
  `Cannot move note to "${destinationFolder}" (folder may not exist):`,
@@ -40500,7 +40507,7 @@ var AppleNotesManager = class {
40500
40507
  showNoteById(id, separately = false) {
40501
40508
  const safeId = sanitizeId(id);
40502
40509
  const separatelyClause = separately ? " separately true" : "";
40503
- const result = executeAppleScript(
40510
+ const result = executeMutationAppleScript(
40504
40511
  buildAppLevelScript(`show note id "${safeId}"${separatelyClause}`)
40505
40512
  );
40506
40513
  if (!result.success) {
@@ -40563,7 +40570,7 @@ var AppleNotesManager = class {
40563
40570
  showFolderById(id, separately = false) {
40564
40571
  const safeId = sanitizeId(id);
40565
40572
  const separatelyClause = separately ? " separately true" : "";
40566
- const result = executeAppleScript(
40573
+ const result = executeMutationAppleScript(
40567
40574
  buildAppLevelScript(`show folder id "${safeId}"${separatelyClause}`)
40568
40575
  );
40569
40576
  if (!result.success) {
@@ -40585,7 +40592,7 @@ var AppleNotesManager = class {
40585
40592
  showAccountById(id, separately = false) {
40586
40593
  const safeId = sanitizeId(id);
40587
40594
  const separatelyClause = separately ? " separately true" : "";
40588
- const result = executeAppleScript(
40595
+ const result = executeMutationAppleScript(
40589
40596
  buildAppLevelScript(`show account id "${safeId}"${separatelyClause}`)
40590
40597
  );
40591
40598
  if (!result.success) {
@@ -40628,7 +40635,7 @@ var AppleNotesManager = class {
40628
40635
  return "OK"
40629
40636
  end tell
40630
40637
  `;
40631
- const result = executeAppleScript(script);
40638
+ const result = executeMutationAppleScript(script);
40632
40639
  if (!result.success) {
40633
40640
  console.error(
40634
40641
  `Failed to show attachment "${attachmentId}" on note "${noteId}":`,
@@ -40858,7 +40865,9 @@ var AppleNotesManager = class {
40858
40865
  * Note: The position within the note cannot be determined via AppleScript.
40859
40866
  *
40860
40867
  * @param id - CoreData URL identifier for the note
40861
- * @returns Array of Attachment objects, or empty array if none found
40868
+ * @returns Array of Attachment objects, or empty array if the note genuinely has none
40869
+ * @throws If the AppleScript call fails, so a lookup failure is never mistaken for
40870
+ * an attachment-free note (callers gate destructive full-body updates on this)
40862
40871
  *
40863
40872
  * @example
40864
40873
  * ```typescript
@@ -40872,19 +40881,34 @@ var AppleNotesManager = class {
40872
40881
  tell application "Notes"
40873
40882
  set theNote to note id "${safeId}"
40874
40883
  set attachmentList to {}
40875
- repeat with a in attachments of theNote
40876
- set attachId to id of a
40877
- set attachName to name of a
40878
- set attachContentId to content identifier of a
40879
- set attachUrl to ""
40880
- try
40881
- set attachUrl to URL of a as text
40882
- end try
40883
- set createdDate to creation date of a
40884
- set modifiedDate to modification date of a
40884
+ set attachmentIds to id of every attachment of theNote
40885
+ set attachmentNames to name of every attachment of theNote
40886
+ set attachmentContentIds to content identifier of every attachment of theNote
40887
+ set attachmentUrls to URL of every attachment of theNote
40888
+ set attachmentCreatedDates to creation date of every attachment of theNote
40889
+ set attachmentModifiedDates to modification date of every attachment of theNote
40890
+ set attachmentSharedFlags to shared of every attachment of theNote
40891
+ set attachmentIdsAfter to id of every attachment of theNote
40892
+ if (count of attachmentNames) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40893
+ if (count of attachmentContentIds) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40894
+ if (count of attachmentUrls) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40895
+ if (count of attachmentCreatedDates) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40896
+ if (count of attachmentModifiedDates) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40897
+ if (count of attachmentSharedFlags) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40898
+ if (count of attachmentIdsAfter) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40899
+ repeat with i from 1 to count of attachmentIds
40900
+ if (item i of attachmentIdsAfter) is not (item i of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40901
+ end repeat
40902
+ repeat with i from 1 to count of attachmentIds
40903
+ set attachId to item i of attachmentIds
40904
+ set attachName to item i of attachmentNames
40905
+ set attachContentId to item i of attachmentContentIds
40906
+ set attachUrl to item i of attachmentUrls as text
40907
+ set createdDate to item i of attachmentCreatedDates
40908
+ set modifiedDate to item i of attachmentModifiedDates
40885
40909
  set createdParts to ${asDatePartsExpr("createdDate")}
40886
40910
  set modifiedParts to ${asDatePartsExpr("modifiedDate")}
40887
- set sharedFlag to shared of a as text
40911
+ set sharedFlag to item i of attachmentSharedFlags as text
40888
40912
  set end of attachmentList to attachId & ${AS_FIELD_SEP} & attachName & ${AS_FIELD_SEP} & attachContentId & ${AS_FIELD_SEP} & attachUrl & ${AS_FIELD_SEP} & createdParts & ${AS_FIELD_SEP} & modifiedParts & ${AS_FIELD_SEP} & sharedFlag
40889
40913
  end repeat
40890
40914
  set output to ""
@@ -40895,10 +40919,12 @@ var AppleNotesManager = class {
40895
40919
  end tell
40896
40920
  `;
40897
40921
  const result = executeAppleScript(script);
40898
- if (!result.success || !result.output) {
40899
- if (result.error) {
40900
- console.error(`Failed to list attachments for note ID "${id}":`, result.error);
40901
- }
40922
+ if (!result.success) {
40923
+ throw new Error(
40924
+ `Failed to list attachments for note ID "${id}": ${result.error ?? "unknown AppleScript error"}`
40925
+ );
40926
+ }
40927
+ if (!result.output) {
40902
40928
  return [];
40903
40929
  }
40904
40930
  const attachments = [];
@@ -40925,7 +40951,9 @@ var AppleNotesManager = class {
40925
40951
  *
40926
40952
  * @param title - Title of the note
40927
40953
  * @param account - Account containing the note (defaults to iCloud)
40928
- * @returns Array of Attachment objects, or empty array if none found
40954
+ * @returns Array of Attachment objects, or empty array if the note genuinely has none
40955
+ * @throws If the AppleScript call fails, so a lookup failure is never mistaken for
40956
+ * an attachment-free note (callers gate destructive full-body updates on this)
40929
40957
  */
40930
40958
  listAttachments(title, account) {
40931
40959
  const targetAccount = this.resolveAccount(account);
@@ -40936,19 +40964,34 @@ var AppleNotesManager = class {
40936
40964
  tell account "${safeAccount}"
40937
40965
  set theNote to note "${safeTitle}"
40938
40966
  set attachmentList to {}
40939
- repeat with a in attachments of theNote
40940
- set attachId to id of a
40941
- set attachName to name of a
40942
- set attachContentId to content identifier of a
40943
- set attachUrl to ""
40944
- try
40945
- set attachUrl to URL of a as text
40946
- end try
40947
- set createdDate to creation date of a
40948
- set modifiedDate to modification date of a
40967
+ set attachmentIds to id of every attachment of theNote
40968
+ set attachmentNames to name of every attachment of theNote
40969
+ set attachmentContentIds to content identifier of every attachment of theNote
40970
+ set attachmentUrls to URL of every attachment of theNote
40971
+ set attachmentCreatedDates to creation date of every attachment of theNote
40972
+ set attachmentModifiedDates to modification date of every attachment of theNote
40973
+ set attachmentSharedFlags to shared of every attachment of theNote
40974
+ set attachmentIdsAfter to id of every attachment of theNote
40975
+ if (count of attachmentNames) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40976
+ if (count of attachmentContentIds) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40977
+ if (count of attachmentUrls) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40978
+ if (count of attachmentCreatedDates) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40979
+ if (count of attachmentModifiedDates) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40980
+ if (count of attachmentSharedFlags) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40981
+ if (count of attachmentIdsAfter) is not (count of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40982
+ repeat with i from 1 to count of attachmentIds
40983
+ if (item i of attachmentIdsAfter) is not (item i of attachmentIds) then error "${BULK_LIST_MUTATION_ERROR}"
40984
+ end repeat
40985
+ repeat with i from 1 to count of attachmentIds
40986
+ set attachId to item i of attachmentIds
40987
+ set attachName to item i of attachmentNames
40988
+ set attachContentId to item i of attachmentContentIds
40989
+ set attachUrl to item i of attachmentUrls as text
40990
+ set createdDate to item i of attachmentCreatedDates
40991
+ set modifiedDate to item i of attachmentModifiedDates
40949
40992
  set createdParts to ${asDatePartsExpr("createdDate")}
40950
40993
  set modifiedParts to ${asDatePartsExpr("modifiedDate")}
40951
- set sharedFlag to shared of a as text
40994
+ set sharedFlag to item i of attachmentSharedFlags as text
40952
40995
  set end of attachmentList to attachId & ${AS_FIELD_SEP} & attachName & ${AS_FIELD_SEP} & attachContentId & ${AS_FIELD_SEP} & attachUrl & ${AS_FIELD_SEP} & createdParts & ${AS_FIELD_SEP} & modifiedParts & ${AS_FIELD_SEP} & sharedFlag
40953
40996
  end repeat
40954
40997
  set output to ""
@@ -40960,10 +41003,12 @@ var AppleNotesManager = class {
40960
41003
  end tell
40961
41004
  `;
40962
41005
  const result = executeAppleScript(script);
40963
- if (!result.success || !result.output) {
40964
- if (result.error) {
40965
- console.error(`Failed to list attachments for note "${title}":`, result.error);
40966
- }
41006
+ if (!result.success) {
41007
+ throw new Error(
41008
+ `Failed to list attachments for note "${title}": ${result.error ?? "unknown AppleScript error"}`
41009
+ );
41010
+ }
41011
+ if (!result.output) {
40967
41012
  return [];
40968
41013
  }
40969
41014
  const attachments = [];
@@ -41030,7 +41075,7 @@ var AppleNotesManager = class {
41030
41075
  return "OK" & ${AS_FIELD_SEP} & (name of theAttachment) & ${AS_FIELD_SEP} & (content identifier of theAttachment)
41031
41076
  end tell
41032
41077
  `;
41033
- const result = executeAppleScript(script);
41078
+ const result = executeMutationAppleScript(script);
41034
41079
  if (!result.success) {
41035
41080
  return { success: false, error: result.error ?? "unknown error" };
41036
41081
  }
@@ -41164,7 +41209,7 @@ var AppleNotesManager = class {
41164
41209
  end repeat
41165
41210
  return out
41166
41211
  `);
41167
- const res = executeAppleScript(script);
41212
+ const res = executeMutationAppleScript(script);
41168
41213
  if (!res.success) {
41169
41214
  for (const r of runnable) {
41170
41215
  results[r.index] = this.createBatchResult(
@@ -41274,7 +41319,7 @@ var AppleNotesManager = class {
41274
41319
  end repeat
41275
41320
  return out
41276
41321
  `);
41277
- const res = executeAppleScript(script);
41322
+ const res = executeMutationAppleScript(script);
41278
41323
  if (!res.success) {
41279
41324
  for (const r of runnable) {
41280
41325
  results[r.index] = this.createBatchResult(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.6.4",
3
+ "version": "2.6.6",
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",