apple-notes-mcp 2.8.10 → 2.8.11

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.
Files changed (3) hide show
  1. package/README.md +34 -3
  2. package/build/index.js +227 -80
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -815,11 +815,41 @@ after the note's actual container folder ID matches the destination folder ID.
815
815
 
816
816
  #### `export-notes-json`
817
817
 
818
- Exports all notes as a JSON structure.
818
+ Exports notes as JSON — metadata, HTML content, and plaintext, grouped by account and folder — one page at a time. A whole library rarely fits in one MCP message (note bodies embed images as base64), so each call returns a page and says where the next one starts.
819
819
 
820
- **Parameters:** None
820
+ | Parameter | Type | Required | Description |
821
+ |-----------|------|----------|-------------|
822
+ | `offset` | number | No | 0-based position to start from, counting notes in account → folder → note order (default `0`). Pass the previous page's `page.nextOffset` |
823
+ | `limit` | number | No | Maximum notes in this page (default `50`, max `500`). A page holds fewer when it reaches the response size limit |
824
+ | `modifiedSince` | string | No | ISO 8601 date string; export only notes modified on or after this date (e.g., `"2025-01-01"`). Keep the same value while paging |
825
+
826
+ **Example - First page:**
827
+ ```json
828
+ {}
829
+ ```
830
+
831
+ **Example - A later page of an incremental backup:**
832
+ ```json
833
+ {
834
+ "offset": 50,
835
+ "modifiedSince": "2025-06-01"
836
+ }
837
+ ```
838
+
839
+ **Returns:** `exportDate`, `version`, `accounts` (every account and folder, holding the notes that fall in this page), `summary` (`totalNotes` in this page, `totalFolders`, `totalAccounts`), and `page`:
840
+
841
+ | Field | Description |
842
+ |-------|-------------|
843
+ | `offset` / `limit` | The window that was applied |
844
+ | `totalAvailable` | Notes in the library, after `modifiedSince` |
845
+ | `returned` | Notes in this page |
846
+ | `nextOffset` | Where the next page starts; absent on the last page |
847
+ | `hasMore` | `true` until the last page — call again with `offset` set to `nextOffset` |
848
+ | `stoppedAtSizeLimit` | `true` when the page closed early to stay under the response size limit |
849
+
850
+ **Size limit:** each response stays under `APPLE_NOTES_MCP_EXPORT_MAX_BYTES` (default 8 MB), below the 10 MB per-message limit of MCP SDK stdio clients, which drop the connection on anything larger without passing on any error text. A note too large to fit on its own is still returned: its oversized inline images are replaced with placeholders (`strippedImages`), or failing that its HTML body, and if necessary its plaintext, is left empty with `contentOmitted: true`. Read such a note with `get-note-content`, and its files with `list-attachments` / `save-attachment`. Lower `limit` if your MCP client caps tool output below that size.
821
851
 
822
- **Returns:** Complete JSON export with all accounts, folders, and notes including metadata.
852
+ **Paging:** positions are worked out on every call, so notes created, deleted, or moved between calls can shift a page. Page through promptly, or use `modifiedSince` for incremental backups.
823
853
 
824
854
  ---
825
855
 
@@ -1189,6 +1219,7 @@ All configuration is optional — the server works out of the box. Override beha
1189
1219
  | `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. |
1190
1220
  | `APPLE_NOTES_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-notes-mcp/config.json` | Path to the JSON config file (see below). |
1191
1221
  | `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. |
1222
+ | `APPLE_NOTES_MCP_EXPORT_MAX_BYTES` | `8388608` (8 MB) | Largest response `export-notes-json` sends; a page closes early to stay under it. The default sits below the 10 MB per-message limit of MCP SDK stdio clients, which drop the connection on anything larger. Raise it only if your MCP client accepts bigger messages. |
1192
1223
  | `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. |
1193
1224
  | `APPLE_NOTES_MCP_RETRY_DELAY_MS` | `1000` (1 s) | Base delay before the first retry; subsequent retries back off exponentially (1s, 2s, 4s, ...). |
1194
1225
  | `DEBUG` / `VERBOSE` | unset | Set either to enable verbose diagnostic logging to stderr. |
package/build/index.js CHANGED
@@ -39484,6 +39484,46 @@ function getChecklistItems(noteId3) {
39484
39484
  return { items };
39485
39485
  }
39486
39486
 
39487
+ // src/utils/inlineImages.ts
39488
+ var DEFAULT_MAX_INLINE_IMAGE_BYTES = 256 * 1024;
39489
+ function maxInlineImageBytes(env = process.env) {
39490
+ const raw = env.APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES;
39491
+ if (raw !== void 0) {
39492
+ const n = Number(raw);
39493
+ if (Number.isFinite(n) && n > 0) return n;
39494
+ }
39495
+ return DEFAULT_MAX_INLINE_IMAGE_BYTES;
39496
+ }
39497
+ function formatBytes(bytes) {
39498
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
39499
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
39500
+ return `${bytes} B`;
39501
+ }
39502
+ var INLINE_IMG_RE = /<img\b[^>]*\bsrc\s*=\s*(["'])data:([^;'"]+);base64,([^"']*)\1[^>]*\/?>/gi;
39503
+ function stripLargeInlineImages(html, maxBytes = maxInlineImageBytes()) {
39504
+ let strippedCount = 0;
39505
+ let strippedBytes = 0;
39506
+ const result = html.replace(INLINE_IMG_RE, (tag, _quote, mediaType, b64) => {
39507
+ if (b64.length <= maxBytes) return tag;
39508
+ const decodedBytes = Math.floor(b64.length * 3 / 4);
39509
+ strippedCount += 1;
39510
+ strippedBytes += decodedBytes;
39511
+ return `<div>[inline image omitted: ${mediaType}, ~${formatBytes(
39512
+ decodedBytes
39513
+ )}; use list-attachments and save-attachment or fetch-attachment to export it]</div>`;
39514
+ });
39515
+ return { html: result, strippedCount, strippedBytes };
39516
+ }
39517
+ function strippedImagesWarning(stripped) {
39518
+ if (stripped.strippedCount === 0) return null;
39519
+ const plural = stripped.strippedCount === 1 ? "image" : "images";
39520
+ return `
39521
+
39522
+ \u26A0\uFE0F ${stripped.strippedCount} inline ${plural} (~${formatBytes(
39523
+ stripped.strippedBytes
39524
+ )} 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.`;
39525
+ }
39526
+
39487
39527
  // src/utils/noteRichText.ts
39488
39528
  import { execFileSync as execFileSync3 } from "node:child_process";
39489
39529
  import { createHash } from "node:crypto";
@@ -39934,6 +39974,35 @@ var FIELD_SEP = "";
39934
39974
  var RECORD_SEP = "";
39935
39975
  var AS_FIELD_SEP = "(ASCII character 31)";
39936
39976
  var AS_RECORD_SEP = "(ASCII character 30)";
39977
+ var AS_FIELD_SEP_LOCAL = "(character id 31)";
39978
+ var AS_RECORD_SEP_LOCAL = "(character id 30)";
39979
+ var DEFAULT_EXPORT_PAGE_SIZE = 50;
39980
+ var DEFAULT_EXPORT_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
39981
+ function exportMaxResponseBytes(env = process.env) {
39982
+ const raw = env.APPLE_NOTES_MCP_EXPORT_MAX_BYTES;
39983
+ if (raw !== void 0) {
39984
+ const n = Number(raw);
39985
+ if (Number.isFinite(n) && n > 0) return Math.floor(n);
39986
+ }
39987
+ return DEFAULT_EXPORT_MAX_RESPONSE_BYTES;
39988
+ }
39989
+ function estimateExportNoteBytes(note) {
39990
+ return Buffer.byteLength(JSON.stringify(note)) + Buffer.byteLength(JSON.stringify(JSON.stringify(note, null, 2))) + 256;
39991
+ }
39992
+ function fitExportNoteToBudget(note, maxBytes) {
39993
+ const stripped = stripLargeInlineImages(note.content);
39994
+ if (stripped.strippedCount > 0) {
39995
+ const withPlaceholders = {
39996
+ ...note,
39997
+ content: stripped.html,
39998
+ strippedImages: stripped.strippedCount
39999
+ };
40000
+ if (estimateExportNoteBytes(withPlaceholders) <= maxBytes) return withPlaceholders;
40001
+ }
40002
+ const withoutHtml = { ...note, content: "", contentOmitted: true };
40003
+ if (estimateExportNoteBytes(withoutHtml) <= maxBytes) return withoutHtml;
40004
+ return { ...withoutHtml, plaintext: "" };
40005
+ }
39937
40006
  function executeMutationAppleScript(script) {
39938
40007
  return executeAppleScript(script, { maxRetries: 1 });
39939
40008
  }
@@ -40729,8 +40798,13 @@ var AppleNotesManager = class {
40729
40798
  set resultList to {}
40730
40799
  if fetchCount > 0 then
40731
40800
  try
40732
- set noteNames to name of ${slicedSource}
40733
- set noteIds to id of ${slicedSource}
40801
+ if fetchCount is totalCount then
40802
+ set noteNames to name of ${fullSource}
40803
+ set noteIds to id of ${fullSource}
40804
+ else
40805
+ set noteNames to name of ${slicedSource}
40806
+ set noteIds to id of ${slicedSource}
40807
+ end if
40734
40808
  on error errMsg number errNum
40735
40809
  if errNum is -1719 or errNum is -1728 then
40736
40810
  error "${BULK_LIST_MUTATION_ERROR}"
@@ -40740,11 +40814,11 @@ var AppleNotesManager = class {
40740
40814
  end try
40741
40815
  ${countGuard("noteIds")}
40742
40816
  repeat with i from 1 to count of noteNames
40743
- set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP} & (item i of noteIds)
40817
+ set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP_LOCAL} & (item i of noteIds)
40744
40818
  end repeat
40745
40819
  end if
40746
- set AppleScript's text item delimiters to ${AS_RECORD_SEP}
40747
- return (totalCount as text) & ${AS_RECORD_SEP} & (resultList as text)
40820
+ set AppleScript's text item delimiters to ${AS_RECORD_SEP_LOCAL}
40821
+ return (totalCount as text) & ${AS_RECORD_SEP_LOCAL} & (resultList as text)
40748
40822
  `;
40749
40823
  }
40750
40824
  const dateFetch = dateSetup ? `set noteDates to modification date of ${fullSource}
@@ -40761,9 +40835,9 @@ var AppleNotesManager = class {
40761
40835
  ${dateFetch}${countGuard("noteIds")}
40762
40836
  ${dateCountGuard}set resultList to {}
40763
40837
  repeat with i from 1 to count of noteNames
40764
- ${dateGuardOpen}set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP} & (item i of noteIds)${dateGuardClose}
40838
+ ${dateGuardOpen}set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP_LOCAL} & (item i of noteIds)${dateGuardClose}
40765
40839
  end repeat
40766
- set AppleScript's text item delimiters to ${AS_RECORD_SEP}
40840
+ set AppleScript's text item delimiters to ${AS_RECORD_SEP_LOCAL}
40767
40841
  return resultList as text
40768
40842
  `;
40769
40843
  }
@@ -40877,18 +40951,32 @@ var AppleNotesManager = class {
40877
40951
  const sharedNotes = [];
40878
40952
  const accounts = this.listAccounts();
40879
40953
  for (const account of accounts) {
40954
+ const countGuard = (listVar) => `if (count of ${listVar}) is not (count of noteShared) then error "${BULK_LIST_MUTATION_ERROR}"`;
40880
40955
  const script = buildAccountScopedScript(
40881
40956
  { account: account.name },
40882
40957
  `
40958
+ set noteShared to shared of notes
40883
40959
  set resultList to {}
40884
- repeat with n in notes
40885
- if shared of n is true then
40886
- set cd to creation date of n
40887
- set md to modification date of n
40888
- set end of resultList to (name of n) & ${AS_FIELD_SEP} & (id of n) & ${AS_FIELD_SEP} & ${asDatePartsExpr("cd")} & ${AS_FIELD_SEP} & ${asDatePartsExpr("md")} & ${AS_FIELD_SEP} & (shared of n as text) & ${AS_FIELD_SEP} & (password protected of n as text)
40889
- end if
40890
- end repeat
40891
- set AppleScript's text item delimiters to ${AS_RECORD_SEP}
40960
+ if noteShared contains true then
40961
+ set noteNames to name of notes
40962
+ set noteIds to id of notes
40963
+ set noteCreated to creation date of notes
40964
+ set noteModified to modification date of notes
40965
+ set noteLocked to password protected of notes
40966
+ ${countGuard("noteIds")}
40967
+ ${countGuard("noteNames")}
40968
+ ${countGuard("noteCreated")}
40969
+ ${countGuard("noteModified")}
40970
+ ${countGuard("noteLocked")}
40971
+ repeat with i from 1 to count of noteShared
40972
+ if (item i of noteShared) is true then
40973
+ set cd to item i of noteCreated
40974
+ set md to item i of noteModified
40975
+ set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP_LOCAL} & (item i of noteIds) & ${AS_FIELD_SEP_LOCAL} & ${asDatePartsExpr("cd")} & ${AS_FIELD_SEP_LOCAL} & ${asDatePartsExpr("md")} & ${AS_FIELD_SEP_LOCAL} & "true" & ${AS_FIELD_SEP_LOCAL} & ((item i of noteLocked) as text)
40976
+ end if
40977
+ end repeat
40978
+ end if
40979
+ set AppleScript's text item delimiters to ${AS_RECORD_SEP_LOCAL}
40892
40980
  return resultList as text
40893
40981
  `
40894
40982
  );
@@ -42117,31 +42205,57 @@ var AppleNotesManager = class {
42117
42205
  return text.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#92;/g, "\\").replace(/&amp;/g, "&").replace(/\n{3,}/g, "\n\n").trim();
42118
42206
  }
42119
42207
  /**
42120
- * Exports all notes as a JSON structure for backup/migration.
42208
+ * Exports notes as a JSON structure for backup/migration, one page at a time.
42121
42209
  *
42122
42210
  * Exports complete note data including:
42123
42211
  * - Metadata (id, title, dates, flags)
42124
42212
  * - Content (HTML and plaintext)
42125
42213
  * - Organization (folder, account)
42126
42214
  *
42215
+ * Notes are numbered 0..N-1 in account, folder, note order, after the optional
42216
+ * `modifiedSince` filter. A call returns the window starting at `offset`, at
42217
+ * most `limit` notes (DEFAULT_EXPORT_PAGE_SIZE by default), and closes the page
42218
+ * early once the next note would push the estimated tool response past
42219
+ * `maxResponseBytes`. Every account and folder is listed on every page so the
42220
+ * structure is stable; `page.nextOffset` says where to continue. Only the
42221
+ * page's notes have their metadata and bodies read.
42222
+ *
42223
+ * Why pages (#162): the whole library does not fit in one MCP message. A
42224
+ * 359-note library holding inline images serialized to a 95 MB response, and
42225
+ * the MCP SDK stdio reader closes the connection on any message over 10 MB, so
42226
+ * the caller saw a bare "Connection closed" with no error text.
42227
+ *
42228
+ * A note that alone exceeds the budget is degraded rather than dropped:
42229
+ * oversized inline images become placeholders (`strippedImages`), or failing
42230
+ * that its HTML, and if necessary its plaintext, is omitted (`contentOmitted`).
42231
+ *
42127
42232
  * Note: Password-protected notes are included with metadata only (no content).
42128
42233
  *
42129
- * @returns JSON-serializable export object
42234
+ * @param options - offset, limit, modifiedSince, and maxResponseBytes
42235
+ * @returns JSON-serializable export object for the requested page
42130
42236
  *
42131
42237
  * @example
42132
42238
  * ```typescript
42133
- * const snapshot = manager.exportNotesAsJson();
42134
- * fs.writeFileSync('notes-backup.json', JSON.stringify(snapshot, null, 2));
42239
+ * let offset: number | undefined = 0;
42240
+ * while (offset !== undefined) {
42241
+ * const snapshot = manager.exportNotesAsJson({ offset });
42242
+ * fs.appendFileSync("notes-backup.jsonl", JSON.stringify(snapshot) + "\n");
42243
+ * offset = snapshot.page.nextOffset;
42244
+ * }
42135
42245
  * ```
42136
42246
  */
42137
- exportNotesAsJson() {
42247
+ exportNotesAsJson(options = {}) {
42248
+ const offset = Math.max(0, Math.floor(options.offset ?? 0));
42249
+ const limit = Math.max(1, Math.floor(options.limit ?? DEFAULT_EXPORT_PAGE_SIZE));
42250
+ const maxResponseBytes = options.maxResponseBytes ?? exportMaxResponseBytes();
42251
+ const exportDate = (/* @__PURE__ */ new Date()).toISOString();
42138
42252
  const accounts = this.listAccounts();
42139
- const exportData = {
42140
- exportDate: (/* @__PURE__ */ new Date()).toISOString(),
42141
- version: "1.0",
42142
- accounts: [],
42143
- summary: { totalNotes: 0, totalFolders: 0, totalAccounts: accounts.length }
42144
- };
42253
+ const exportedAccounts = [];
42254
+ const summary = { totalNotes: 0, totalFolders: 0, totalAccounts: accounts.length };
42255
+ let position = 0;
42256
+ let usedBytes = 0;
42257
+ let nextOffset;
42258
+ let stoppedAtSizeLimit = false;
42145
42259
  for (const account of accounts) {
42146
42260
  const folders = this.listFolders(account.name);
42147
42261
  const accountData = {
@@ -42153,8 +42267,14 @@ var AppleNotesManager = class {
42153
42267
  name: folder.name,
42154
42268
  notes: []
42155
42269
  };
42156
- const noteRefs = this.listNoteRefs(account.name, folder.name);
42270
+ const noteRefs = this.listNoteRefs(account.name, folder.name, options.modifiedSince);
42157
42271
  for (const ref of noteRefs) {
42272
+ const index = position++;
42273
+ if (index < offset || nextOffset !== void 0) continue;
42274
+ if (summary.totalNotes >= limit) {
42275
+ nextOffset = index;
42276
+ continue;
42277
+ }
42158
42278
  const note = this.getNoteById(ref.id);
42159
42279
  if (!note) continue;
42160
42280
  note.account = account.name;
@@ -42162,15 +42282,41 @@ var AppleNotesManager = class {
42162
42282
  if (!note.passwordProtected) {
42163
42283
  content = this.getNoteContentById(ref.id);
42164
42284
  }
42165
- folderData.notes.push(this.exportNote(note, content));
42166
- exportData.summary.totalNotes++;
42285
+ let exported = this.exportNote(note, content);
42286
+ let bytes = estimateExportNoteBytes(exported);
42287
+ if (usedBytes + bytes > maxResponseBytes) {
42288
+ if (summary.totalNotes > 0) {
42289
+ nextOffset = index;
42290
+ stoppedAtSizeLimit = true;
42291
+ continue;
42292
+ }
42293
+ exported = fitExportNoteToBudget(exported, maxResponseBytes);
42294
+ bytes = estimateExportNoteBytes(exported);
42295
+ }
42296
+ folderData.notes.push(exported);
42297
+ usedBytes += bytes;
42298
+ summary.totalNotes++;
42167
42299
  }
42168
42300
  accountData.folders.push(folderData);
42169
- exportData.summary.totalFolders++;
42301
+ summary.totalFolders++;
42170
42302
  }
42171
- exportData.accounts.push(accountData);
42303
+ exportedAccounts.push(accountData);
42172
42304
  }
42173
- return exportData;
42305
+ return {
42306
+ exportDate,
42307
+ version: "1.0",
42308
+ accounts: exportedAccounts,
42309
+ summary,
42310
+ page: {
42311
+ offset,
42312
+ limit,
42313
+ totalAvailable: position,
42314
+ returned: summary.totalNotes,
42315
+ ...nextOffset !== void 0 ? { nextOffset } : {},
42316
+ hasMore: nextOffset !== void 0,
42317
+ stoppedAtSizeLimit
42318
+ }
42319
+ };
42174
42320
  }
42175
42321
  // ===========================================================================
42176
42322
  // Markdown Conversion
@@ -42521,46 +42667,6 @@ function parseHashtags(body) {
42521
42667
  return result;
42522
42668
  }
42523
42669
 
42524
- // src/utils/inlineImages.ts
42525
- var DEFAULT_MAX_INLINE_IMAGE_BYTES = 256 * 1024;
42526
- function maxInlineImageBytes(env = process.env) {
42527
- const raw = env.APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES;
42528
- if (raw !== void 0) {
42529
- const n = Number(raw);
42530
- if (Number.isFinite(n) && n > 0) return n;
42531
- }
42532
- return DEFAULT_MAX_INLINE_IMAGE_BYTES;
42533
- }
42534
- function formatBytes(bytes) {
42535
- if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
42536
- if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
42537
- return `${bytes} B`;
42538
- }
42539
- var INLINE_IMG_RE = /<img\b[^>]*\bsrc\s*=\s*(["'])data:([^;'"]+);base64,([^"']*)\1[^>]*\/?>/gi;
42540
- function stripLargeInlineImages(html, maxBytes = maxInlineImageBytes()) {
42541
- let strippedCount = 0;
42542
- let strippedBytes = 0;
42543
- const result = html.replace(INLINE_IMG_RE, (tag, _quote, mediaType, b64) => {
42544
- if (b64.length <= maxBytes) return tag;
42545
- const decodedBytes = Math.floor(b64.length * 3 / 4);
42546
- strippedCount += 1;
42547
- strippedBytes += decodedBytes;
42548
- return `<div>[inline image omitted: ${mediaType}, ~${formatBytes(
42549
- decodedBytes
42550
- )}; use list-attachments and save-attachment or fetch-attachment to export it]</div>`;
42551
- });
42552
- return { html: result, strippedCount, strippedBytes };
42553
- }
42554
- function strippedImagesWarning(stripped) {
42555
- if (stripped.strippedCount === 0) return null;
42556
- const plural = stripped.strippedCount === 1 ? "image" : "images";
42557
- return `
42558
-
42559
- \u26A0\uFE0F ${stripped.strippedCount} inline ${plural} (~${formatBytes(
42560
- stripped.strippedBytes
42561
- )} 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.`;
42562
- }
42563
-
42564
42670
  // src/utils/updateResponseTitle.ts
42565
42671
  var BLOCK_END_RE = /<\/(?:div|h[1-6]|p|li)>/gi;
42566
42672
  var BREAK_RE = /<br\s*\/?\s*>/gi;
@@ -45744,26 +45850,60 @@ registerTool(
45744
45850
  });
45745
45851
  }, "Error showing attachment")
45746
45852
  );
45853
+ var EXPORT_RESPONSE_OVERHEAD_BYTES = 64 * 1024;
45854
+ var formatMegabytes = (bytes) => `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
45747
45855
  registerTool(
45748
45856
  "export-notes-json",
45749
45857
  {
45750
- description: "Use when: exporting the entire notes library as structured JSON for backup or bulk processing.\nReturns: a summary plus the full JSON of all notes, folders, and accounts.\nDo not use when: you need a single note (get-note-content) \u2014 this reads everything and can be large.\nRead-only.",
45751
- inputSchema: {},
45858
+ description: "Use when: exporting notes as structured JSON for backup, migration, or bulk processing.\nReturns: one page of notes (default 50) with metadata, HTML content, and plaintext, grouped by account and folder, plus page info; while page.hasMore is true, call again with offset set to page.nextOffset.\nDo not use when: you need one note (get-note-content) or only titles and ids (list-notes).\nNote: a page stops early to stay under the response size limit (APPLE_NOTES_MCP_EXPORT_MAX_BYTES, default 8 MB), and a note too large on its own comes back with strippedImages or contentOmitted set. Read-only.",
45859
+ inputSchema: {
45860
+ offset: external_exports.number().int().min(0).optional().describe(
45861
+ "0-based position to start from, counting notes in account, folder, note order (default 0). Pass the previous page's page.nextOffset."
45862
+ ),
45863
+ limit: external_exports.number().int().positive().max(500).optional().describe(
45864
+ `Maximum notes in this page (default ${DEFAULT_EXPORT_PAGE_SIZE}). A page holds fewer when it reaches the response size limit; lower it if your client caps tool output.`
45865
+ ),
45866
+ modifiedSince: external_exports.string().max(64).optional().describe(
45867
+ "ISO 8601 date string; export only notes modified on or after this date (e.g., '2025-01-01'). Keep the same value while paging."
45868
+ )
45869
+ },
45752
45870
  outputSchema: {
45753
45871
  exportDate: external_exports.string().optional(),
45754
45872
  version: external_exports.string().optional(),
45755
45873
  accounts: external_exports.array(external_exports.object({}).passthrough()).optional(),
45756
- summary: external_exports.object({}).passthrough().optional()
45874
+ summary: external_exports.object({}).passthrough().optional(),
45875
+ page: external_exports.object({}).passthrough().optional()
45757
45876
  }
45758
45877
  },
45759
- withErrorHandling(() => {
45760
- const exportData = notesManager.exportNotesAsJson();
45761
- const { summary } = exportData;
45762
- return {
45878
+ withErrorHandling(({ offset, limit, modifiedSince }) => {
45879
+ const maxResponseBytes = exportMaxResponseBytes();
45880
+ const exportData = notesManager.exportNotesAsJson({
45881
+ offset,
45882
+ limit,
45883
+ modifiedSince,
45884
+ maxResponseBytes: Math.max(1, maxResponseBytes - EXPORT_RESPONSE_OVERHEAD_BYTES)
45885
+ });
45886
+ const { summary, page } = exportData;
45887
+ const since = modifiedSince ? ` modified since ${modifiedSince}` : "";
45888
+ const lines = [
45889
+ `Exported ${summary.totalNotes} of ${page.totalAvailable} notes${since} (offset ${page.offset}, limit ${page.limit}) from ${summary.totalFolders} folders across ${summary.totalAccounts} account(s).`
45890
+ ];
45891
+ if (page.hasMore) {
45892
+ lines.push(
45893
+ `More notes remain: call export-notes-json again with offset ${page.nextOffset}${modifiedSince ? " and the same modifiedSince" : ""}.` + (page.stoppedAtSizeLimit ? ` This page stopped early to stay under the ${formatMegabytes(maxResponseBytes)} response limit.` : "")
45894
+ );
45895
+ }
45896
+ const degraded = exportData.accounts.flatMap((a) => a.folders.flatMap((f) => f.notes)).filter((n) => n.strippedImages || n.contentOmitted);
45897
+ if (degraded.length > 0) {
45898
+ lines.push(
45899
+ `Too large to return whole, so oversized inline images were replaced (strippedImages) or the body was left out (contentOmitted): ${degraded.map((n) => n.id).join(", ")}. Read these with get-note-content, and their files with list-attachments and save-attachment.`
45900
+ );
45901
+ }
45902
+ const response = {
45763
45903
  content: [
45764
45904
  {
45765
45905
  type: "text",
45766
- text: `Exported ${summary.totalNotes} notes from ${summary.totalFolders} folders across ${summary.totalAccounts} account(s).
45906
+ text: `${lines.join("\n")}
45767
45907
 
45768
45908
  Full JSON export:`
45769
45909
  },
@@ -45774,6 +45914,13 @@ Full JSON export:`
45774
45914
  ],
45775
45915
  structuredContent: { ...exportData }
45776
45916
  };
45917
+ const responseBytes = Buffer.byteLength(JSON.stringify(response));
45918
+ if (responseBytes > maxResponseBytes) {
45919
+ return errorResponse(
45920
+ `Error exporting notes: this page is ${formatMegabytes(responseBytes)}, over the ${formatMegabytes(maxResponseBytes)} response limit, so it was not sent. Call export-notes-json again with offset ${page.offset} and a smaller limit (for example ${Math.max(1, Math.floor(page.returned / 2))}), or raise APPLE_NOTES_MCP_EXPORT_MAX_BYTES if your MCP client accepts larger messages.`
45921
+ );
45922
+ }
45923
+ return response;
45777
45924
  }, "Error exporting notes")
45778
45925
  );
45779
45926
  registerTool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.8.10",
3
+ "version": "2.8.11",
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",