apple-notes-mcp 2.15.15 → 2.15.16

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
@@ -728,6 +728,8 @@ Retrieves the full content of a specific note.
728
728
 
729
729
  **Note:** Either `id` or `title` must be provided. Using `id` is recommended as it's unique and avoids issues with duplicate titles.
730
730
 
731
+ **Busy Notes.app:** with an `id` and Full Disk Access, the note's existence and password flag are read from the NoteStore database (as `get-note-structure` does), so only the body read goes through Notes.app. A timeout is always reported as `timeout_indeterminate`, never as `not_found`, even when an automatic retry then fails differently; and when Notes.app cannot resolve an id the database still holds, the error is `notes_unavailable`. Retry in a moment, or use `get-note-structure` for a read that does not need Notes.app.
732
+
731
733
  **Large images:** Notes.app returns images inside the body as base64, so a note holding a very large image (tens of MB) can take longer to read than the timeout allows. When the read times out or overflows the output buffer, the error says so and, with Full Disk Access, names the attachments of 5 MB or more. Retry with a larger `timeoutSeconds`; `delete-note` accepts the same argument, and it needs a successful read to verify the note before deleting it.
732
734
 
733
735
  **Example - Using ID (recommended):**
package/build/index.js CHANGED
@@ -54466,6 +54466,7 @@ var RETRYABLE_ERROR_PATTERNS = [
54466
54466
  /busy/i,
54467
54467
  /changed during listing/i
54468
54468
  ];
54469
+ var TIMED_OUT_PATTERN = /timed? out|\(-1712\)/i;
54469
54470
  function isRetryableError(errorMessage) {
54470
54471
  return RETRYABLE_ERROR_PATTERNS.some((pattern2) => pattern2.test(errorMessage));
54471
54472
  }
@@ -54594,6 +54595,7 @@ function executeAppleScript(script, options = {}) {
54594
54595
  maxRetries
54595
54596
  });
54596
54597
  let lastError = null;
54598
+ let timeoutDiagnostic;
54597
54599
  const startTime = Date.now();
54598
54600
  const deadline = startTime + timeoutMs;
54599
54601
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -54652,10 +54654,12 @@ function executeAppleScript(script, options = {}) {
54652
54654
  errorMessage,
54653
54655
  rawError: rawError?.substring(0, 500)
54654
54656
  });
54657
+ const attemptTimedOut = isTimeout || TIMED_OUT_PATTERN.test(stripQuoted(errorMessage));
54658
+ if (attemptTimedOut) timeoutDiagnostic ??= errorMessage;
54655
54659
  lastError = {
54656
54660
  success: false,
54657
54661
  output: "",
54658
- error: errorMessage
54662
+ error: timeoutDiagnostic && !attemptTimedOut ? `${timeoutDiagnostic} A retry then failed: ${errorMessage}` : errorMessage
54659
54663
  };
54660
54664
  const canRetry = isTimeout || isRetryableError(errorMessage);
54661
54665
  const hasAttemptsLeft = attempt < maxRetries;
@@ -67196,6 +67200,42 @@ function readNoteStructure(id2, { dbPath: dbPath2 = NOTES_DB_PATH8, includeText
67196
67200
  };
67197
67201
  }
67198
67202
 
67203
+ // src/utils/notePresence.ts
67204
+ var NOTE_ID3 = /^x-coredata:\/\/([0-9A-Fa-f-]+)\/ICNote\/p(\d+)$/;
67205
+ function readNotePresence(id2, dbPath2 = NOTES_DB_PATH8) {
67206
+ const match = NOTE_ID3.exec(id2);
67207
+ if (!match) return null;
67208
+ const [, store, pk] = match;
67209
+ try {
67210
+ const columns = readColumns(dbPath2);
67211
+ const c = (alias, name) => col(columns, alias, name);
67212
+ const sql = `SELECT json_object(
67213
+ 'store', (SELECT Z_UUID FROM Z_METADATA LIMIT 1),
67214
+ 'isNote', n.Z_ENT = ${entity("ICNote")},
67215
+ 'live', ${notTombstonedSql(columns, "n")},
67216
+ 'title', ${c("n", "ZTITLE1")},
67217
+ 'locked', ${c("n", "ZISPASSWORDPROTECTED")},
67218
+ 'encrypted', (SELECT d.ZCRYPTOINITIALIZATIONVECTOR IS NOT NULL FROM ZICNOTEDATA d WHERE d.ZNOTE = n.Z_PK),
67219
+ 'inTrash', CASE WHEN f.Z_PK IS NULL THEN 0 ELSE ${trashFolderSql(columns, "f")} END)
67220
+ FROM ZICCLOUDSYNCINGOBJECT n
67221
+ LEFT JOIN ZICCLOUDSYNCINGOBJECT f ON f.Z_PK = ${c("n", "ZFOLDER")}
67222
+ WHERE n.Z_PK = @pk;`;
67223
+ const [row] = parseJsonLines(
67224
+ runReadOnlySql(dbPath2, sql, { pk: { int: Number(pk) } })
67225
+ );
67226
+ if (!row || typeof row.store !== "string" || row.store.toUpperCase() !== store.toUpperCase())
67227
+ return null;
67228
+ if (row.isNote !== 1 || row.live !== 1) return null;
67229
+ return {
67230
+ title: typeof row.title === "string" ? row.title : null,
67231
+ locked: row.locked === 1 || row.encrypted === 1,
67232
+ inRecentlyDeleted: row.inTrash === 1
67233
+ };
67234
+ } catch {
67235
+ return null;
67236
+ }
67237
+ }
67238
+
67199
67239
  // src/utils/bodyReadFailure.ts
67200
67240
  var LARGE_ATTACHMENT_BYTES = 5 * 1024 * 1024;
67201
67241
  var OUTPUT_OVERFLOW = /Notes\.app returned more than [\d.]+ (?:MB|bytes) of output/;
@@ -67229,6 +67269,11 @@ function describeBodyReadFailure(title, error62, attachments) {
67229
67269
  return `${base}
67230
67270
 
67231
67271
  ${cause2} ${remedy2}`;
67272
+ }
67273
+ if (attachments && attachments.length === 0) {
67274
+ return `${base}
67275
+
67276
+ The note has no attachments, so its size is not the cause: Notes.app was most likely busy (for example, syncing iCloud) and did not answer in time. Retry in a moment, or with a longer timeoutSeconds (up to 120). get-note-structure reads the note from the NoteStore database without Notes.app.`;
67232
67277
  }
67233
67278
  const cause = large.length > 0 ? `${holds}Notes.app returns images inside the note body as base64, so a large image makes the body too big to read in time.` : "Notes.app returns images inside the note body as base64, so a note with a very large image can take longer to read than the timeout allows.";
67234
67279
  const remedy = "Retry with a longer timeoutSeconds (up to 120) or raise APPLE_NOTES_MCP_TIMEOUT_MS. If it still fails, delete or shrink the attachment in Notes.app; delete-note needs a successful read to verify the note first.";
@@ -79607,6 +79652,12 @@ function bodyReadFailureMessage(id2, title, error62) {
79607
79652
  }
79608
79653
  return describeBodyReadFailure(title, error62, attachments);
79609
79654
  }
79655
+ function isExactIdNotFound(error62) {
79656
+ return error62 === NOTE_ID_NOT_FOUND_MESSAGE || error62 !== void 0 && /^Can[’']t get note id\b/i.test(error62);
79657
+ }
79658
+ function storedNoteUnresolvedMessage(id2, title, error62) {
79659
+ return `Failed to read content of note "${title}" (ID "${id2}"): Notes.app did not resolve the ID (${error62}), but the NoteStore database holds this note, so it is not reported as not found. Notes.app may be busy, for example syncing iCloud. Retry in a moment; get-note-structure reads the note from the database without Notes.app.`;
79660
+ }
79610
79661
  var NOT_DELETED_MESSAGE = "Notes.app accepted the delete, but the note is still in its original folder, so it was not moved to Recently Deleted. Nothing was deleted; read the note again before retrying.";
79611
79662
  function deleteUnverifiedMessage(title, id2) {
79612
79663
  return `Notes.app accepted the delete for note "${title}", but its original folder could not be re-read to confirm the note left it. The outcome is uncertain; read exact ID ${id2} before retrying.`;
@@ -80098,19 +80149,36 @@ registerTool(
80098
80149
  },
80099
80150
  withErrorHandling(({ id: id2, title, account }) => {
80100
80151
  if (id2) {
80101
- const { note: note2, error: metadataError } = notesManager.readNoteById(id2);
80102
- if (!note2) {
80103
- const message = `Failed to read metadata of note with ID "${id2}": ${metadataError}`;
80104
- return metadataError === NOTE_ID_NOT_FOUND_MESSAGE ? notFoundResponse(message) : errorResponse(message);
80105
- }
80106
- if (note2.passwordProtected) {
80107
- return errorResponse(
80108
- `Note "${note2.title}" is password-protected and cannot be read. Unlock it in Notes.app first.`
80109
- );
80152
+ const stored = readNotePresence(id2);
80153
+ let noteTitle;
80154
+ if (stored && stored.title !== null) {
80155
+ if (stored.locked) {
80156
+ return errorResponse(
80157
+ `Note "${stored.title}" is password-protected and cannot be read. Unlock it in Notes.app first.`
80158
+ );
80159
+ }
80160
+ noteTitle = stored.title;
80161
+ } else {
80162
+ const { note: note2, error: metadataError } = notesManager.readNoteById(id2);
80163
+ if (!note2) {
80164
+ const message = `Failed to read metadata of note with ID "${id2}": ${metadataError}`;
80165
+ return metadataError === NOTE_ID_NOT_FOUND_MESSAGE ? notFoundResponse(message) : errorResponse(message);
80166
+ }
80167
+ if (note2.passwordProtected) {
80168
+ return errorResponse(
80169
+ `Note "${note2.title}" is password-protected and cannot be read. Unlock it in Notes.app first.`
80170
+ );
80171
+ }
80172
+ noteTitle = note2.title;
80110
80173
  }
80111
80174
  const { body: rawContent2, error: readError2 } = notesManager.readNoteBodyById(id2);
80112
80175
  if (!rawContent2) {
80113
- return errorResponse(bodyReadFailureMessage(id2, note2.title, readError2));
80176
+ if (isExactIdNotFound(readError2)) {
80177
+ if (!stored) return notFoundResponse(bodyReadFailureMessage(id2, noteTitle, readError2));
80178
+ const message = storedNoteUnresolvedMessage(id2, noteTitle, readError2);
80179
+ return errorResponse(message, new CodedError(message, { code: "notes_unavailable" }));
80180
+ }
80181
+ return errorResponse(bodyReadFailureMessage(id2, noteTitle, readError2));
80114
80182
  }
80115
80183
  const rich2 = enrichNoteRead(id2, rawContent2);
80116
80184
  const stripped2 = stripLargeInlineImages(rich2.content);
@@ -80120,7 +80188,7 @@ registerTool(
80120
80188
  return successResponse(warning2 ? content2 + warning2 : content2, {
80121
80189
  id: id2,
80122
80190
  ...noteIdentifiers(id2),
80123
- title: note2.title,
80191
+ title: noteTitle,
80124
80192
  content: content2,
80125
80193
  contentHash: richContentHash(rawContent2, rich2),
80126
80194
  links: rich2.links,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.15.15",
3
+ "version": "2.15.16",
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",