apple-notes-mcp 2.15.15 → 2.15.16
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 +2 -0
- package/build/index.js +80 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -728,6 +728,8 @@ Retrieves the full content of a specific note.
|
|
|
728
728
|
|
|
729
729
|
**Note:** Either `id` or `title` must be provided. Using `id` is recommended as it's unique and avoids issues with duplicate titles.
|
|
730
730
|
|
|
731
|
+
**Busy Notes.app:** with an `id` and Full Disk Access, the note's existence and password flag are read from the NoteStore database (as `get-note-structure` does), so only the body read goes through Notes.app. A timeout is always reported as `timeout_indeterminate`, never as `not_found`, even when an automatic retry then fails differently; and when Notes.app cannot resolve an id the database still holds, the error is `notes_unavailable`. Retry in a moment, or use `get-note-structure` for a read that does not need Notes.app.
|
|
732
|
+
|
|
731
733
|
**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.
|
|
732
734
|
|
|
733
735
|
**Example - Using ID (recommended):**
|
package/build/index.js
CHANGED
|
@@ -54466,6 +54466,7 @@ var RETRYABLE_ERROR_PATTERNS = [
|
|
|
54466
54466
|
/busy/i,
|
|
54467
54467
|
/changed during listing/i
|
|
54468
54468
|
];
|
|
54469
|
+
var TIMED_OUT_PATTERN = /timed? out|\(-1712\)/i;
|
|
54469
54470
|
function isRetryableError(errorMessage) {
|
|
54470
54471
|
return RETRYABLE_ERROR_PATTERNS.some((pattern2) => pattern2.test(errorMessage));
|
|
54471
54472
|
}
|
|
@@ -54594,6 +54595,7 @@ function executeAppleScript(script, options = {}) {
|
|
|
54594
54595
|
maxRetries
|
|
54595
54596
|
});
|
|
54596
54597
|
let lastError = null;
|
|
54598
|
+
let timeoutDiagnostic;
|
|
54597
54599
|
const startTime = Date.now();
|
|
54598
54600
|
const deadline = startTime + timeoutMs;
|
|
54599
54601
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
@@ -54652,10 +54654,12 @@ function executeAppleScript(script, options = {}) {
|
|
|
54652
54654
|
errorMessage,
|
|
54653
54655
|
rawError: rawError?.substring(0, 500)
|
|
54654
54656
|
});
|
|
54657
|
+
const attemptTimedOut = isTimeout || TIMED_OUT_PATTERN.test(stripQuoted(errorMessage));
|
|
54658
|
+
if (attemptTimedOut) timeoutDiagnostic ??= errorMessage;
|
|
54655
54659
|
lastError = {
|
|
54656
54660
|
success: false,
|
|
54657
54661
|
output: "",
|
|
54658
|
-
error: errorMessage
|
|
54662
|
+
error: timeoutDiagnostic && !attemptTimedOut ? `${timeoutDiagnostic} A retry then failed: ${errorMessage}` : errorMessage
|
|
54659
54663
|
};
|
|
54660
54664
|
const canRetry = isTimeout || isRetryableError(errorMessage);
|
|
54661
54665
|
const hasAttemptsLeft = attempt < maxRetries;
|
|
@@ -67196,6 +67200,42 @@ function readNoteStructure(id2, { dbPath: dbPath2 = NOTES_DB_PATH8, includeText
|
|
|
67196
67200
|
};
|
|
67197
67201
|
}
|
|
67198
67202
|
|
|
67203
|
+
// src/utils/notePresence.ts
|
|
67204
|
+
var NOTE_ID3 = /^x-coredata:\/\/([0-9A-Fa-f-]+)\/ICNote\/p(\d+)$/;
|
|
67205
|
+
function readNotePresence(id2, dbPath2 = NOTES_DB_PATH8) {
|
|
67206
|
+
const match = NOTE_ID3.exec(id2);
|
|
67207
|
+
if (!match) return null;
|
|
67208
|
+
const [, store, pk] = match;
|
|
67209
|
+
try {
|
|
67210
|
+
const columns = readColumns(dbPath2);
|
|
67211
|
+
const c = (alias, name) => col(columns, alias, name);
|
|
67212
|
+
const sql = `SELECT json_object(
|
|
67213
|
+
'store', (SELECT Z_UUID FROM Z_METADATA LIMIT 1),
|
|
67214
|
+
'isNote', n.Z_ENT = ${entity("ICNote")},
|
|
67215
|
+
'live', ${notTombstonedSql(columns, "n")},
|
|
67216
|
+
'title', ${c("n", "ZTITLE1")},
|
|
67217
|
+
'locked', ${c("n", "ZISPASSWORDPROTECTED")},
|
|
67218
|
+
'encrypted', (SELECT d.ZCRYPTOINITIALIZATIONVECTOR IS NOT NULL FROM ZICNOTEDATA d WHERE d.ZNOTE = n.Z_PK),
|
|
67219
|
+
'inTrash', CASE WHEN f.Z_PK IS NULL THEN 0 ELSE ${trashFolderSql(columns, "f")} END)
|
|
67220
|
+
FROM ZICCLOUDSYNCINGOBJECT n
|
|
67221
|
+
LEFT JOIN ZICCLOUDSYNCINGOBJECT f ON f.Z_PK = ${c("n", "ZFOLDER")}
|
|
67222
|
+
WHERE n.Z_PK = @pk;`;
|
|
67223
|
+
const [row] = parseJsonLines(
|
|
67224
|
+
runReadOnlySql(dbPath2, sql, { pk: { int: Number(pk) } })
|
|
67225
|
+
);
|
|
67226
|
+
if (!row || typeof row.store !== "string" || row.store.toUpperCase() !== store.toUpperCase())
|
|
67227
|
+
return null;
|
|
67228
|
+
if (row.isNote !== 1 || row.live !== 1) return null;
|
|
67229
|
+
return {
|
|
67230
|
+
title: typeof row.title === "string" ? row.title : null,
|
|
67231
|
+
locked: row.locked === 1 || row.encrypted === 1,
|
|
67232
|
+
inRecentlyDeleted: row.inTrash === 1
|
|
67233
|
+
};
|
|
67234
|
+
} catch {
|
|
67235
|
+
return null;
|
|
67236
|
+
}
|
|
67237
|
+
}
|
|
67238
|
+
|
|
67199
67239
|
// src/utils/bodyReadFailure.ts
|
|
67200
67240
|
var LARGE_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
67201
67241
|
var OUTPUT_OVERFLOW = /Notes\.app returned more than [\d.]+ (?:MB|bytes) of output/;
|
|
@@ -67229,6 +67269,11 @@ function describeBodyReadFailure(title, error62, attachments) {
|
|
|
67229
67269
|
return `${base}
|
|
67230
67270
|
|
|
67231
67271
|
${cause2} ${remedy2}`;
|
|
67272
|
+
}
|
|
67273
|
+
if (attachments && attachments.length === 0) {
|
|
67274
|
+
return `${base}
|
|
67275
|
+
|
|
67276
|
+
The note has no attachments, so its size is not the cause: Notes.app was most likely busy (for example, syncing iCloud) and did not answer in time. Retry in a moment, or with a longer timeoutSeconds (up to 120). get-note-structure reads the note from the NoteStore database without Notes.app.`;
|
|
67232
67277
|
}
|
|
67233
67278
|
const cause = large.length > 0 ? `${holds}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.";
|
|
67234
67279
|
const remedy = "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.";
|
|
@@ -79607,6 +79652,12 @@ function bodyReadFailureMessage(id2, title, error62) {
|
|
|
79607
79652
|
}
|
|
79608
79653
|
return describeBodyReadFailure(title, error62, attachments);
|
|
79609
79654
|
}
|
|
79655
|
+
function isExactIdNotFound(error62) {
|
|
79656
|
+
return error62 === NOTE_ID_NOT_FOUND_MESSAGE || error62 !== void 0 && /^Can[’']t get note id\b/i.test(error62);
|
|
79657
|
+
}
|
|
79658
|
+
function storedNoteUnresolvedMessage(id2, title, error62) {
|
|
79659
|
+
return `Failed to read content of note "${title}" (ID "${id2}"): Notes.app did not resolve the ID (${error62}), but the NoteStore database holds this note, so it is not reported as not found. Notes.app may be busy, for example syncing iCloud. Retry in a moment; get-note-structure reads the note from the database without Notes.app.`;
|
|
79660
|
+
}
|
|
79610
79661
|
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.";
|
|
79611
79662
|
function deleteUnverifiedMessage(title, id2) {
|
|
79612
79663
|
return `Notes.app accepted the delete for note "${title}", but its original folder could not be re-read to confirm the note left it. The outcome is uncertain; read exact ID ${id2} before retrying.`;
|
|
@@ -80098,19 +80149,36 @@ registerTool(
|
|
|
80098
80149
|
},
|
|
80099
80150
|
withErrorHandling(({ id: id2, title, account }) => {
|
|
80100
80151
|
if (id2) {
|
|
80101
|
-
const
|
|
80102
|
-
|
|
80103
|
-
|
|
80104
|
-
|
|
80105
|
-
|
|
80106
|
-
|
|
80107
|
-
|
|
80108
|
-
|
|
80109
|
-
|
|
80152
|
+
const stored = readNotePresence(id2);
|
|
80153
|
+
let noteTitle;
|
|
80154
|
+
if (stored && stored.title !== null) {
|
|
80155
|
+
if (stored.locked) {
|
|
80156
|
+
return errorResponse(
|
|
80157
|
+
`Note "${stored.title}" is password-protected and cannot be read. Unlock it in Notes.app first.`
|
|
80158
|
+
);
|
|
80159
|
+
}
|
|
80160
|
+
noteTitle = stored.title;
|
|
80161
|
+
} else {
|
|
80162
|
+
const { note: note2, error: metadataError } = notesManager.readNoteById(id2);
|
|
80163
|
+
if (!note2) {
|
|
80164
|
+
const message = `Failed to read metadata of note with ID "${id2}": ${metadataError}`;
|
|
80165
|
+
return metadataError === NOTE_ID_NOT_FOUND_MESSAGE ? notFoundResponse(message) : errorResponse(message);
|
|
80166
|
+
}
|
|
80167
|
+
if (note2.passwordProtected) {
|
|
80168
|
+
return errorResponse(
|
|
80169
|
+
`Note "${note2.title}" is password-protected and cannot be read. Unlock it in Notes.app first.`
|
|
80170
|
+
);
|
|
80171
|
+
}
|
|
80172
|
+
noteTitle = note2.title;
|
|
80110
80173
|
}
|
|
80111
80174
|
const { body: rawContent2, error: readError2 } = notesManager.readNoteBodyById(id2);
|
|
80112
80175
|
if (!rawContent2) {
|
|
80113
|
-
|
|
80176
|
+
if (isExactIdNotFound(readError2)) {
|
|
80177
|
+
if (!stored) return notFoundResponse(bodyReadFailureMessage(id2, noteTitle, readError2));
|
|
80178
|
+
const message = storedNoteUnresolvedMessage(id2, noteTitle, readError2);
|
|
80179
|
+
return errorResponse(message, new CodedError(message, { code: "notes_unavailable" }));
|
|
80180
|
+
}
|
|
80181
|
+
return errorResponse(bodyReadFailureMessage(id2, noteTitle, readError2));
|
|
80114
80182
|
}
|
|
80115
80183
|
const rich2 = enrichNoteRead(id2, rawContent2);
|
|
80116
80184
|
const stripped2 = stripLargeInlineImages(rich2.content);
|
|
@@ -80120,7 +80188,7 @@ registerTool(
|
|
|
80120
80188
|
return successResponse(warning2 ? content2 + warning2 : content2, {
|
|
80121
80189
|
id: id2,
|
|
80122
80190
|
...noteIdentifiers(id2),
|
|
80123
|
-
title:
|
|
80191
|
+
title: noteTitle,
|
|
80124
80192
|
content: content2,
|
|
80125
80193
|
contentHash: richContentHash(rawContent2, rich2),
|
|
80126
80194
|
links: rich2.links,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-notes-mcp",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.16",
|
|
4
4
|
"description": "MCP server for Apple Notes - read, search, create, edit, organize, and export notes on macOS via Claude and other AI assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|