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 +4 -1
- package/build/index.js +68 -10
- 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
|
{
|
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.
|
|
56014
|
-
if (!body)
|
|
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.
|
|
56528
|
+
const { body: rawContent2, error: readError2 } = notesManager.readNoteBodyById(id2);
|
|
56471
56529
|
if (!rawContent2) {
|
|
56472
|
-
return errorResponse(
|
|
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.
|
|
56570
|
+
const { body: rawContent, error: readError } = notesManager.readNoteBodyById(note.id);
|
|
56513
56571
|
if (!rawContent) {
|
|
56514
|
-
return errorResponse(
|
|
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