apple-notes-mcp 2.9.9 → 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.
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
  {
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;
@@ -56010,11 +56053,25 @@ function readExactNoteSnapshot(id2) {
56010
56053
  error: `Note "${note.title}" is password-protected and cannot be changed. Unlock it in Notes.app first.`
56011
56054
  };
56012
56055
  }
56013
- const body = notesManager.getNoteContentById(id2);
56014
- 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
+ }
56015
56061
  const rich = enrichNoteRead(id2, body);
56016
56062
  return { note, body, rich, contentHash: richContentHash(body, rich) };
56017
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
+ }
56018
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.";
56019
56076
  function containerUnknownMessage(title) {
56020
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.`;
@@ -56428,13 +56485,14 @@ ${lines}${footer}`,
56428
56485
  registerTool(
56429
56486
  "get-note-content",
56430
56487
  {
56431
- 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.",
56432
56489
  inputSchema: {
56433
56490
  id: looseNoteId(external_exports.string()).optional().describe(`Note ID (preferred - more reliable than title): ${NOTE_ID_FORMS}`),
56434
56491
  title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
56435
56492
  account: external_exports.string().max(MAX.ACCOUNT).optional().describe(
56436
56493
  "Account name (defaults to Notes.app's default account; exact or unique-prefix match, ignored if id is provided)"
56437
- )
56494
+ ),
56495
+ timeoutSeconds: timeoutSecondsInput
56438
56496
  },
56439
56497
  outputSchema: {
56440
56498
  id: external_exports.string().optional(),
@@ -56467,9 +56525,9 @@ registerTool(
56467
56525
  `Note "${note2.title}" is password-protected and cannot be read. Unlock it in Notes.app first.`
56468
56526
  );
56469
56527
  }
56470
- const rawContent2 = notesManager.getNoteContentById(id2);
56528
+ const { body: rawContent2, error: readError2 } = notesManager.readNoteBodyById(id2);
56471
56529
  if (!rawContent2) {
56472
- return errorResponse(`Failed to read content of note "${note2.title}"`);
56530
+ return errorResponse(bodyReadFailureMessage(id2, note2.title, readError2));
56473
56531
  }
56474
56532
  const rich2 = enrichNoteRead(id2, rawContent2);
56475
56533
  const stripped2 = stripLargeInlineImages(rich2.content);
@@ -56509,9 +56567,9 @@ registerTool(
56509
56567
  `Note "${title}" is password-protected and cannot be read. Unlock it in Notes.app first.`
56510
56568
  );
56511
56569
  }
56512
- const rawContent = notesManager.getNoteContent(title, account);
56570
+ const { body: rawContent, error: readError } = notesManager.readNoteBodyById(note.id);
56513
56571
  if (!rawContent) {
56514
- return errorResponse(`Failed to read content of note "${title}"`);
56572
+ return errorResponse(bodyReadFailureMessage(note.id, title, readError));
56515
56573
  }
56516
56574
  const rich = enrichNoteRead(note.id, rawContent);
56517
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.9",
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",