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.
- package/README.md +34 -3
- package/build/index.js +227 -80
- 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
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
40733
|
-
|
|
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) & ${
|
|
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 ${
|
|
40747
|
-
return (totalCount 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) & ${
|
|
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 ${
|
|
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
|
-
|
|
40885
|
-
|
|
40886
|
-
|
|
40887
|
-
|
|
40888
|
-
|
|
40889
|
-
|
|
40890
|
-
|
|
40891
|
-
|
|
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(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/\/g, "\\").replace(/&/g, "&").replace(/\n{3,}/g, "\n\n").trim();
|
|
42118
42206
|
}
|
|
42119
42207
|
/**
|
|
42120
|
-
* Exports
|
|
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
|
-
* @
|
|
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
|
-
*
|
|
42134
|
-
*
|
|
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
|
|
42140
|
-
|
|
42141
|
-
|
|
42142
|
-
|
|
42143
|
-
|
|
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
|
-
|
|
42166
|
-
|
|
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
|
-
|
|
42301
|
+
summary.totalFolders++;
|
|
42170
42302
|
}
|
|
42171
|
-
|
|
42303
|
+
exportedAccounts.push(accountData);
|
|
42172
42304
|
}
|
|
42173
|
-
return
|
|
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
|
|
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
|
|
45761
|
-
const
|
|
45762
|
-
|
|
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:
|
|
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