apple-notes-mcp 2.9.8 → 2.9.10

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 +12 -1
  2. package/build/index.js +147 -26
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -261,7 +261,7 @@ Creates a new note in Apple Notes.
261
261
  | `account` | string | No | Account name (defaults to Notes.app's default account; matched exactly or by a *unique* prefix — an ambiguous prefix is refused). Must be an account Notes.app already has configured — see [`list-accounts`](#list-accounts) |
262
262
  | `format` | string | No | Content format: `"plaintext"` (default), `"html"`, or `"markdown"`. In all formats, the title is automatically prepended as the note's title line. In plaintext mode, newlines become `<br>`, tabs become `<br>`, and backslashes are preserved as HTML entities. `"markdown"` produces real Title/Heading/Subheading styles through a Shortcut; see [Markdown notes](#markdown-notes) |
263
263
  | `markdownRoute` | string | No | With `format: "markdown"` only: `"shortcut"` (default) or `"html"`. See [Markdown through HTML](#markdown-through-html) |
264
- | `timeoutSeconds` | number | No | Whole seconds, 1–120, for each Notes.app automation step this call runs; overrides `APPLE_NOTES_MCP_TIMEOUT_MS` for this call only. A timed-out write is uncertain, not failed: read the note by id before any retry. Also accepted by `update-note`, `append-to-note`, `delete-note` and `move-note` |
264
+ | `timeoutSeconds` | number | No | Whole seconds, 1–120, for each Notes.app automation step this call runs; overrides `APPLE_NOTES_MCP_TIMEOUT_MS` for this call only. A timed-out write is uncertain, not failed: read the note by id before any retry. Also accepted by `get-note-content`, `update-note`, `append-to-note`, `delete-note` and `move-note` |
265
265
 
266
266
  **Example (tagged with inline hashtags):**
267
267
  ```json
@@ -531,9 +531,12 @@ Retrieves the full content of a specific note.
531
531
  | `id` | string | No | Note ID (preferred - more reliable than title) |
532
532
  | `title` | string | No | Note title (use `id` instead when available) |
533
533
  | `account` | string | No | Account containing the note (defaults to Notes.app's default account; exact or unique-prefix match, ignored if `id` is provided) |
534
+ | `timeoutSeconds` | number | No | Whole seconds, 1–120, for the body read; overrides `APPLE_NOTES_MCP_TIMEOUT_MS` for this call only |
534
535
 
535
536
  **Note:** Either `id` or `title` must be provided. Using `id` is recommended as it's unique and avoids issues with duplicate titles.
536
537
 
538
+ **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.
539
+
537
540
  **Example - Using ID (recommended):**
538
541
  ```json
539
542
  {
@@ -1873,6 +1876,14 @@ fetched attachment bytes with the source before reporting success.
1873
1876
  that exact name. A mismatch is a warning (`filenameWarning`), not a failure,
1874
1877
  because the attachment and its bytes are already verified.
1875
1878
 
1879
+ On macOS 27, Notes' AppleScript does not list PDF attachments, so it cannot see
1880
+ a PDF this tool just added. When that happens and the server has Full Disk
1881
+ Access, it verifies through the read-only NoteStore database instead: success
1882
+ requires exactly one new attachment row on the note whose media file matches
1883
+ the source bytes, and the result carries `verifiedBy: "database"`. Without Full
1884
+ Disk Access a PDF attach reports "insertion outcome uncertain"; read the note
1885
+ before retrying, because the attachment was probably created.
1886
+
1876
1887
  ---
1877
1888
 
1878
1889
  #### `create-note-with-attachment`
package/build/index.js CHANGED
@@ -42825,15 +42825,27 @@ var AppleNotesManager = class {
42825
42825
  * @returns HTML content of the note, or empty string if not found
42826
42826
  */
42827
42827
  getNoteContentById(id2) {
42828
+ return this.readNoteBodyById(id2).body;
42829
+ }
42830
+ /**
42831
+ * Like {@link getNoteContentById}, but keeps the automation error so a
42832
+ * caller can explain a failed read (for example a timeout on a note whose
42833
+ * body carries a very large inline image, #237) instead of reporting a bare
42834
+ * failure.
42835
+ *
42836
+ * @param id - CoreData URL identifier for the note
42837
+ * @returns The HTML body, or an empty body plus the error that stopped the read
42838
+ */
42839
+ readNoteBodyById(id2) {
42828
42840
  const safeId = sanitizeId(id2);
42829
42841
  const getCommand = `get body of note id "${safeId}"`;
42830
42842
  const script = buildAppLevelScript(getCommand);
42831
42843
  const result = executeAppleScript(script);
42832
42844
  if (!result.success) {
42833
42845
  console.error(`Failed to get content of note with ID "${id2}":`, result.error);
42834
- return "";
42846
+ return { body: "", error: result.error };
42835
42847
  }
42836
- return result.output;
42848
+ return { body: result.output };
42837
42849
  }
42838
42850
  /**
42839
42851
  * Retrieves the plain-text content of a note by its exact title.
@@ -49593,6 +49605,37 @@ function readNoteStructure(id2, { dbPath: dbPath2 = NOTES_DB_PATH7, includeText
49593
49605
  };
49594
49606
  }
49595
49607
 
49608
+ // src/utils/bodyReadFailure.ts
49609
+ var LARGE_ATTACHMENT_BYTES = 5 * 1024 * 1024;
49610
+ function classifyBodyReadError(error2) {
49611
+ if (!error2) return "other";
49612
+ if (/ENOBUFS|maxBuffer/i.test(error2)) return "buffer";
49613
+ if (/timed out|timeout|-1712/i.test(error2)) return "timeout";
49614
+ return "other";
49615
+ }
49616
+ function formatBytes2(bytes) {
49617
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
49618
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
49619
+ return `${bytes} bytes`;
49620
+ }
49621
+ function largeAttachments(attachments, threshold = LARGE_ATTACHMENT_BYTES) {
49622
+ const flat = attachments.flatMap((a) => [a, ...a.children ?? []]);
49623
+ return flat.filter((a) => typeof a.fileSize === "number" && a.fileSize >= threshold).map((a) => ({ name: a.filename || a.title || "attachment", bytes: a.fileSize })).sort((a, b) => b.bytes - a.bytes);
49624
+ }
49625
+ function describeBodyReadFailure(title, error2, attachments) {
49626
+ const base = `Failed to read content of note "${title}"${error2 ? `: ${error2}` : ""}`;
49627
+ const kind = classifyBodyReadError(error2);
49628
+ if (kind === "other") return base;
49629
+ const large = attachments ? largeAttachments(attachments) : [];
49630
+ const cause = large.length > 0 ? `The note holds ${large.length === 1 ? "a large attachment" : "large attachments"} (${large.map((a) => `${a.name}, ${formatBytes2(a.bytes)}`).join(
49631
+ "; "
49632
+ )}). 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.";
49633
+ const remedy = kind === "buffer" ? "Raise APPLE_NOTES_MCP_MAX_BUFFER (bytes) and retry, or remove the attachment in Notes.app." : "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.";
49634
+ return `${base}
49635
+
49636
+ ${cause} ${remedy}`;
49637
+ }
49638
+
49596
49639
  // src/utils/noteLinkInventory.ts
49597
49640
  import { dirname as dirname5 } from "node:path";
49598
49641
  var BODY_BATCH = 100;
@@ -51463,7 +51506,7 @@ function registerDirectOperations(server2, manager) {
51463
51506
  };
51464
51507
  tool(
51465
51508
  "add-attachment",
51466
- "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: 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.",
51509
+ "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: 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.",
51467
51510
  { id: noteId, expectedContentHash: revision, ...attachmentInput },
51468
51511
  (args) => attachFile(manager, args)
51469
51512
  );
@@ -51532,6 +51575,56 @@ function attachmentName(path10, filename) {
51532
51575
  );
51533
51576
  return filename;
51534
51577
  }
51578
+ var UNCERTAIN = "Attachment insertion outcome uncertain; read the exact note before retrying";
51579
+ var pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
51580
+ var sha256 = (data) => createHash2("sha256").update(data).digest("hex");
51581
+ function storedAttachmentIds(manager, id2) {
51582
+ try {
51583
+ return new Set(
51584
+ manager.getAttachmentAssetsById(id2).attachments.filter((item) => item.parentIdentifier === null).map((item) => item.identifier.toLowerCase())
51585
+ );
51586
+ } catch {
51587
+ return null;
51588
+ }
51589
+ }
51590
+ function storedInsertion(manager, id2, before, bytes, returnedId) {
51591
+ let added = [];
51592
+ for (let attempt = 0; attempt < 8; attempt++) {
51593
+ if (attempt > 0) pause(250);
51594
+ try {
51595
+ added = manager.getAttachmentAssetsById(id2).attachments.filter(
51596
+ (item) => item.parentIdentifier === null && !before.has(item.identifier.toLowerCase())
51597
+ );
51598
+ } catch {
51599
+ added = [];
51600
+ }
51601
+ if (added.length === 1 && added[0].assetPaths.length > 0) break;
51602
+ }
51603
+ if (added.length === 0) return null;
51604
+ if (added.length > 1) throw new Error(UNCERTAIN);
51605
+ const row = added[0];
51606
+ const attachmentId = attachmentCoreDataId(id2, row.pk);
51607
+ if (returnedId && /\/ICAttachment\/p\d+$/.test(returnedId) && returnedId !== attachmentId)
51608
+ throw new Error(UNCERTAIN);
51609
+ const expected = sha256(bytes);
51610
+ const matches = row.assetPaths.some((path10) => {
51611
+ let descriptor;
51612
+ try {
51613
+ descriptor = openSync5(path10, constants5.O_RDONLY | constants5.O_NOFOLLOW);
51614
+ const stat = fstatSync5(descriptor);
51615
+ return stat.isFile() && stat.size === bytes.length && sha256(readFileSync3(descriptor)) === expected;
51616
+ } catch {
51617
+ return false;
51618
+ } finally {
51619
+ if (descriptor !== void 0) closeSync5(descriptor);
51620
+ }
51621
+ });
51622
+ if (!matches)
51623
+ throw new Error(
51624
+ `Notes' database shows new attachment ${attachmentId} on this note, but its file bytes could not be verified; read the exact note and do not attach the file again`
51625
+ );
51626
+ return { attachmentId, name: row.filename };
51627
+ }
51535
51628
  function attachFile(manager, args) {
51536
51629
  const { id: id2, expectedContentHash, path: path10 } = args;
51537
51630
  const name = attachmentName(path10, args.filename);
@@ -51539,6 +51632,7 @@ function attachFile(manager, args) {
51539
51632
  if (before.hash !== expectedContentHash) throw new Error("Note revision changed");
51540
51633
  const bytes = localAttachment(path10);
51541
51634
  const beforeAttachments = manager.listAttachmentsById(id2);
51635
+ const beforeStored = storedAttachmentIds(manager, id2);
51542
51636
  const directory = mkdtempSync4(join22(tmpdir4(), "notes-attachment-add-"));
51543
51637
  const temporaryFile = join22(directory, name);
51544
51638
  try {
@@ -51558,27 +51652,39 @@ function attachFile(manager, args) {
51558
51652
  );
51559
51653
  let inserted = readInserted();
51560
51654
  for (let attempt = 0; attempt < 4 && (inserted.length !== 1 || returnedId && returnedId !== inserted[0].id); attempt++) {
51561
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
51655
+ pause(250);
51562
51656
  inserted = readInserted();
51563
51657
  }
51564
51658
  const persistentReturnedId = returnedId && /\/ICAttachment\/p\d+$/.test(returnedId);
51565
- if (inserted.length !== 1 || persistentReturnedId && returnedId !== inserted[0].id)
51566
- throw new Error(
51567
- "Attachment insertion outcome uncertain; read the exact note before retrying"
51568
- );
51569
- const attachmentId = inserted[0].id;
51570
- const fetched = manager.getAttachmentBase64ById(id2, attachmentId);
51571
- const actual = typeof fetched.base64 === "string" ? Buffer.from(fetched.base64, "base64") : null;
51572
- if (!actual || createHash2("sha256").update(actual).digest("hex") !== createHash2("sha256").update(bytes).digest("hex"))
51573
- throw new Error("Attachment bytes were not verified; read the exact note before retrying");
51574
- const nameVerified = inserted[0].name === name;
51659
+ let attachmentId;
51660
+ let reportedName;
51661
+ let verifiedBy = "applescript";
51662
+ if (inserted.length === 1 && !(persistentReturnedId && returnedId !== inserted[0].id)) {
51663
+ attachmentId = inserted[0].id;
51664
+ reportedName = inserted[0].name;
51665
+ const fetched = manager.getAttachmentBase64ById(id2, attachmentId);
51666
+ const actual = typeof fetched.base64 === "string" ? Buffer.from(fetched.base64, "base64") : null;
51667
+ if (!actual || sha256(actual) !== sha256(bytes))
51668
+ throw new Error("Attachment bytes were not verified; read the exact note before retrying");
51669
+ } else {
51670
+ const stored = inserted.length === 0 && beforeStored ? storedInsertion(manager, id2, beforeStored, bytes, returnedId) : null;
51671
+ if (!stored)
51672
+ throw new Error(
51673
+ inserted.length === 0 && !beforeStored ? `${UNCERTAIN}. Notes' AppleScript does not list some attachments (PDFs on macOS 27); grant Full Disk Access so the server can verify through the Notes database` : UNCERTAIN
51674
+ );
51675
+ attachmentId = stored.attachmentId;
51676
+ reportedName = stored.name;
51677
+ verifiedBy = "database";
51678
+ }
51679
+ const nameVerified = reportedName === name;
51575
51680
  return {
51576
51681
  ok: true,
51577
51682
  id: id2,
51578
51683
  attachmentId,
51579
51684
  contentHash: after.hash,
51580
51685
  bytes: bytes.length,
51581
- name: inserted[0].name,
51686
+ name: reportedName,
51687
+ ...verifiedBy === "database" ? { verifiedBy } : {},
51582
51688
  ...args.filename === void 0 ? {} : {
51583
51689
  filenameVerified: nameVerified,
51584
51690
  ...nameVerified ? {} : {
@@ -53000,7 +53106,7 @@ function canonicalJson(value) {
53000
53106
  return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(",")}}`;
53001
53107
  return JSON.stringify(value);
53002
53108
  }
53003
- function sha256(text2) {
53109
+ function sha2562(text2) {
53004
53110
  return createHash4("sha256").update(text2).digest("hex");
53005
53111
  }
53006
53112
  var round = (v, digits) => {
@@ -53840,7 +53946,7 @@ function analysisFor(source, analyzer, viewport) {
53840
53946
  const classification = !importable || requiredLosses.includes("drop-content") ? "unsupported" : requiredLosses.length ? "lossy" : "safe";
53841
53947
  const analysis = {
53842
53948
  analyzer: SVG_ANALYZER_VERSION,
53843
- source: { sha256: sha256(source), bytes: source.length },
53949
+ source: { sha256: sha2562(source), bytes: source.length },
53844
53950
  classification,
53845
53951
  importable,
53846
53952
  defaultWriteAllowed: importable && classification === "safe",
@@ -53861,7 +53967,7 @@ function analysisFor(source, analyzer, viewport) {
53861
53967
  issues: analyzer.issues,
53862
53968
  issuesTruncated: analyzer.issuesTruncated
53863
53969
  };
53864
- const analysisDigest = "sha256:" + sha256(canonicalJson({ analysis, drawing }));
53970
+ const analysisDigest = "sha256:" + sha2562(canonicalJson({ analysis, drawing }));
53865
53971
  return { analysis: { ...analysis, analysisDigest }, drawing };
53866
53972
  }
53867
53973
  function analyzeSvgBuffer(source) {
@@ -55947,11 +56053,25 @@ function readExactNoteSnapshot(id2) {
55947
56053
  error: `Note "${note.title}" is password-protected and cannot be changed. Unlock it in Notes.app first.`
55948
56054
  };
55949
56055
  }
55950
- const body = notesManager.getNoteContentById(id2);
55951
- if (!body) return { error: `Failed to read content of note "${note.title}"` };
56056
+ const { body, error: error2 } = notesManager.readNoteBodyById(id2);
56057
+ if (!body) {
56058
+ const reason = bodyReadFailureMessage(id2, note.title, error2);
56059
+ return { error: `${reason}${error2 ? "\n\nNothing was changed." : ""}` };
56060
+ }
55952
56061
  const rich = enrichNoteRead(id2, body);
55953
56062
  return { note, body, rich, contentHash: richContentHash(body, rich) };
55954
56063
  }
56064
+ function bodyReadFailureMessage(id2, title, error2) {
56065
+ let attachments;
56066
+ if (classifyBodyReadError(error2) !== "other") {
56067
+ try {
56068
+ attachments = readNoteStructure(id2, { includeText: false }).attachments;
56069
+ } catch {
56070
+ attachments = void 0;
56071
+ }
56072
+ }
56073
+ return describeBodyReadFailure(title, error2, attachments);
56074
+ }
55955
56075
  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.";
55956
56076
  function containerUnknownMessage(title) {
55957
56077
  return `Could not read which folder note "${title}" is in, so it may already be in Recently Deleted, where deleting it would remove it permanently. Nothing was deleted. Retry in a moment; if it keeps failing, quit and reopen Notes.app, then retry.`;
@@ -56365,13 +56485,14 @@ ${lines}${footer}`,
56365
56485
  registerTool(
56366
56486
  "get-note-content",
56367
56487
  {
56368
- description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the exact note id, content, contentHash revision token, parsed hashtags, nativeTags, restored links, richContentComplete/writable, and strippedImages/truncated when the body was capped. Read the warning when writable is false.\nDo not use when: you only need metadata (get-note-details) or Markdown with checklist state (get-note-markdown).\nNote: password-protected notes must be unlocked in Notes.app first.\nSafety: inline images larger than APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES (default 256 KB) are replaced with '[inline image omitted: ...]' text placeholders, so the returned body is lossy whenever truncated is true. Mutations refuse attachment-bearing notes; edit those in Notes.app.",
56488
+ description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the exact note id, content, contentHash revision token, parsed hashtags, nativeTags, restored links, richContentComplete/writable, and strippedImages/truncated when the body was capped. Read the warning when writable is false.\nDo not use when: you only need metadata (get-note-details) or Markdown with checklist state (get-note-markdown).\nNote: password-protected notes must be unlocked in Notes.app first.\nSafety: inline images larger than APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES (default 256 KB) are replaced with '[inline image omitted: ...]' text placeholders, so the returned body is lossy whenever truncated is true. Mutations refuse attachment-bearing notes; edit those in Notes.app. Notes.app returns images inside the body as base64, so a note with a very large image can time out; the error then names the cause, and a larger timeoutSeconds gives the read more time.",
56369
56489
  inputSchema: {
56370
56490
  id: looseNoteId(external_exports.string()).optional().describe(`Note ID (preferred - more reliable than title): ${NOTE_ID_FORMS}`),
56371
56491
  title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
56372
56492
  account: external_exports.string().max(MAX.ACCOUNT).optional().describe(
56373
56493
  "Account name (defaults to Notes.app's default account; exact or unique-prefix match, ignored if id is provided)"
56374
- )
56494
+ ),
56495
+ timeoutSeconds: timeoutSecondsInput
56375
56496
  },
56376
56497
  outputSchema: {
56377
56498
  id: external_exports.string().optional(),
@@ -56404,9 +56525,9 @@ registerTool(
56404
56525
  `Note "${note2.title}" is password-protected and cannot be read. Unlock it in Notes.app first.`
56405
56526
  );
56406
56527
  }
56407
- const rawContent2 = notesManager.getNoteContentById(id2);
56528
+ const { body: rawContent2, error: readError2 } = notesManager.readNoteBodyById(id2);
56408
56529
  if (!rawContent2) {
56409
- return errorResponse(`Failed to read content of note "${note2.title}"`);
56530
+ return errorResponse(bodyReadFailureMessage(id2, note2.title, readError2));
56410
56531
  }
56411
56532
  const rich2 = enrichNoteRead(id2, rawContent2);
56412
56533
  const stripped2 = stripLargeInlineImages(rich2.content);
@@ -56446,9 +56567,9 @@ registerTool(
56446
56567
  `Note "${title}" is password-protected and cannot be read. Unlock it in Notes.app first.`
56447
56568
  );
56448
56569
  }
56449
- const rawContent = notesManager.getNoteContent(title, account);
56570
+ const { body: rawContent, error: readError } = notesManager.readNoteBodyById(note.id);
56450
56571
  if (!rawContent) {
56451
- return errorResponse(`Failed to read content of note "${title}"`);
56572
+ return errorResponse(bodyReadFailureMessage(note.id, title, readError));
56452
56573
  }
56453
56574
  const rich = enrichNoteRead(note.id, rawContent);
56454
56575
  const stripped = stripLargeInlineImages(rich.content);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.9.8",
3
+ "version": "2.9.10",
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",