apple-notes-mcp 2.6.3 → 2.6.5
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 -2
- package/build/index.js +65 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -362,6 +362,8 @@ Updates an existing note's content and/or title.
|
|
|
362
362
|
|
|
363
363
|
**Note:** Either `id` or `title` must be provided. Using `id` is recommended.
|
|
364
364
|
|
|
365
|
+
**Returns:** Confirmation with the note's visible title and, for ID-based updates, its ID. For HTML updates, the title comes from the first rendered line of `newContent`, matching Notes.app. The response also warns if the note is shared.
|
|
366
|
+
|
|
365
367
|
**Example - Using ID (recommended):**
|
|
366
368
|
```json
|
|
367
369
|
{
|
|
@@ -1017,8 +1019,8 @@ All configuration is optional — the server works out of the box. Override beha
|
|
|
1017
1019
|
| `APPLE_NOTES_MCP_MAX_ATTACHMENT_BYTES` | `26214400` (25 MB) | Max size of an attachment that [`fetch-attachment`](#fetch-attachment) will base64-encode inline. Larger attachments are rejected with an error pointing at [`save-attachment`](#save-attachment) (which streams to disk and has no such limit). Raise it to fetch bigger attachments inline; lower it to cap memory. |
|
|
1018
1020
|
| `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. |
|
|
1019
1021
|
| `APPLE_NOTES_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-notes-mcp/config.json` | Path to the JSON config file (see below). |
|
|
1020
|
-
| `APPLE_NOTES_MCP_TIMEOUT_MS` | `30000` (30 s) |
|
|
1021
|
-
| `APPLE_NOTES_MCP_MAX_RETRIES` | `2` |
|
|
1022
|
+
| `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. |
|
|
1023
|
+
| `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. |
|
|
1022
1024
|
| `APPLE_NOTES_MCP_RETRY_DELAY_MS` | `1000` (1 s) | Base delay before the first retry; subsequent retries back off exponentially (1s, 2s, 4s, ...). |
|
|
1023
1025
|
| `DEBUG` / `VERBOSE` | unset | Set either to enable verbose diagnostic logging to stderr. |
|
|
1024
1026
|
|
package/build/index.js
CHANGED
|
@@ -38677,6 +38677,7 @@ function getMaxBuffer() {
|
|
|
38677
38677
|
return envPositiveNumber("APPLE_NOTES_MCP_MAX_BUFFER") ?? DEFAULT_MAX_BUFFER_BYTES;
|
|
38678
38678
|
}
|
|
38679
38679
|
var SCRIPT_TIMEOUT_HEADROOM_MS = 5e3;
|
|
38680
|
+
var MIN_ATTEMPT_BUDGET_MS = 1e3;
|
|
38680
38681
|
function wrapWithTimeout(script, processTimeoutMs) {
|
|
38681
38682
|
const seconds = Math.max(1, Math.ceil((processTimeoutMs - SCRIPT_TIMEOUT_HEADROOM_MS) / 1e3));
|
|
38682
38683
|
return `with timeout of ${seconds} seconds
|
|
@@ -38818,7 +38819,6 @@ function executeAppleScript(script, options = {}) {
|
|
|
38818
38819
|
error: "Cannot execute empty AppleScript"
|
|
38819
38820
|
};
|
|
38820
38821
|
}
|
|
38821
|
-
const preparedScript = wrapWithTimeout(script.trim(), timeoutMs);
|
|
38822
38822
|
debugLog("Executing AppleScript", {
|
|
38823
38823
|
scriptPreview: script.trim().substring(0, 200) + (script.length > 200 ? "..." : ""),
|
|
38824
38824
|
timeout: timeoutMs,
|
|
@@ -38826,13 +38826,16 @@ function executeAppleScript(script, options = {}) {
|
|
|
38826
38826
|
});
|
|
38827
38827
|
let lastError = null;
|
|
38828
38828
|
const startTime = Date.now();
|
|
38829
|
+
const deadline = startTime + timeoutMs;
|
|
38829
38830
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
38831
|
+
const attemptTimeoutMs = Math.max(1, deadline - Date.now());
|
|
38832
|
+
const preparedScript = wrapWithTimeout(script.trim(), attemptTimeoutMs);
|
|
38830
38833
|
const attemptStart = Date.now();
|
|
38831
38834
|
try {
|
|
38832
38835
|
const output = execFileSync("osascript", ["-"], {
|
|
38833
38836
|
input: preparedScript,
|
|
38834
38837
|
encoding: "utf8",
|
|
38835
|
-
timeout:
|
|
38838
|
+
timeout: attemptTimeoutMs,
|
|
38836
38839
|
// SIGKILL (not the default SIGTERM): a wedged osascript blocked on an
|
|
38837
38840
|
// unresponsive Notes.app can ignore SIGTERM and leak, piling up and
|
|
38838
38841
|
// worsening contention. SIGKILL guarantees reaping on timeout. (#17)
|
|
@@ -38885,8 +38888,9 @@ function executeAppleScript(script, options = {}) {
|
|
|
38885
38888
|
};
|
|
38886
38889
|
const canRetry = isTimeout || isRetryableError(errorMessage);
|
|
38887
38890
|
const hasAttemptsLeft = attempt < maxRetries;
|
|
38888
|
-
|
|
38889
|
-
|
|
38891
|
+
const delayMs = retryDelayMs * Math.pow(2, attempt - 1);
|
|
38892
|
+
const hasTimeForRetry = Date.now() + delayMs + MIN_ATTEMPT_BUDGET_MS < deadline;
|
|
38893
|
+
if (canRetry && hasAttemptsLeft && hasTimeForRetry) {
|
|
38890
38894
|
console.error(
|
|
38891
38895
|
`AppleScript retry: Attempt ${attempt}/${maxRetries} failed with "${errorMessage}". Retrying in ${delayMs}ms...`
|
|
38892
38896
|
);
|
|
@@ -39221,6 +39225,9 @@ var FIELD_SEP = "";
|
|
|
39221
39225
|
var RECORD_SEP = "";
|
|
39222
39226
|
var AS_FIELD_SEP = "(ASCII character 31)";
|
|
39223
39227
|
var AS_RECORD_SEP = "(ASCII character 30)";
|
|
39228
|
+
function executeMutationAppleScript(script) {
|
|
39229
|
+
return executeAppleScript(script, { maxRetries: 1 });
|
|
39230
|
+
}
|
|
39224
39231
|
function escapeForAppleScript(text) {
|
|
39225
39232
|
if (!text) {
|
|
39226
39233
|
return "";
|
|
@@ -39481,7 +39488,7 @@ var AppleNotesManager = class {
|
|
|
39481
39488
|
`;
|
|
39482
39489
|
}
|
|
39483
39490
|
const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
|
|
39484
|
-
const result =
|
|
39491
|
+
const result = executeMutationAppleScript(script);
|
|
39485
39492
|
if (!result.success) {
|
|
39486
39493
|
console.error(`Failed to create note "${title}":`, result.error);
|
|
39487
39494
|
return null;
|
|
@@ -39824,7 +39831,7 @@ var AppleNotesManager = class {
|
|
|
39824
39831
|
const safeTitle = escapePlainStringForAppleScript(title);
|
|
39825
39832
|
const deleteCommand = `delete note "${safeTitle}"`;
|
|
39826
39833
|
const script = buildAccountScopedScript({ account: targetAccount }, deleteCommand);
|
|
39827
|
-
const result =
|
|
39834
|
+
const result = executeMutationAppleScript(script);
|
|
39828
39835
|
if (!result.success) {
|
|
39829
39836
|
console.error(`Failed to delete note "${title}":`, result.error);
|
|
39830
39837
|
return false;
|
|
@@ -39844,7 +39851,7 @@ var AppleNotesManager = class {
|
|
|
39844
39851
|
const safeId = sanitizeId(id);
|
|
39845
39852
|
const deleteCommand = `delete note id "${safeId}"`;
|
|
39846
39853
|
const script = buildAppLevelScript(deleteCommand);
|
|
39847
|
-
const result =
|
|
39854
|
+
const result = executeMutationAppleScript(script);
|
|
39848
39855
|
if (!result.success) {
|
|
39849
39856
|
console.error(`Failed to delete note with ID "${id}":`, result.error);
|
|
39850
39857
|
return false;
|
|
@@ -39888,7 +39895,7 @@ var AppleNotesManager = class {
|
|
|
39888
39895
|
}
|
|
39889
39896
|
const updateCommand = `set body of note "${safeCurrentTitle}" to "${fullBody}"`;
|
|
39890
39897
|
const script = buildAccountScopedScript({ account: targetAccount }, updateCommand);
|
|
39891
|
-
const result =
|
|
39898
|
+
const result = executeMutationAppleScript(script);
|
|
39892
39899
|
if (!result.success) {
|
|
39893
39900
|
console.error(`Failed to update note "${title}":`, result.error);
|
|
39894
39901
|
return false;
|
|
@@ -39937,7 +39944,7 @@ var AppleNotesManager = class {
|
|
|
39937
39944
|
const safeId = sanitizeId(id);
|
|
39938
39945
|
const updateCommand = `set body of note id "${safeId}" to "${fullBody}"`;
|
|
39939
39946
|
const script = buildAppLevelScript(updateCommand);
|
|
39940
|
-
const result =
|
|
39947
|
+
const result = executeMutationAppleScript(script);
|
|
39941
39948
|
if (!result.success) {
|
|
39942
39949
|
console.error(`Failed to update note with ID "${id}":`, result.error);
|
|
39943
39950
|
return false;
|
|
@@ -40256,7 +40263,7 @@ var AppleNotesManager = class {
|
|
|
40256
40263
|
createCommand = `make new folder at ${parentRef} with properties {name:"${segmentName}"}`;
|
|
40257
40264
|
}
|
|
40258
40265
|
const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
|
|
40259
|
-
const result =
|
|
40266
|
+
const result = executeMutationAppleScript(script);
|
|
40260
40267
|
if (!result.success) {
|
|
40261
40268
|
console.error(`Failed to create folder "${name}":`, result.error);
|
|
40262
40269
|
return null;
|
|
@@ -40288,7 +40295,7 @@ var AppleNotesManager = class {
|
|
|
40288
40295
|
const targetAccount = this.resolveAccount(account);
|
|
40289
40296
|
const deleteCommand = `delete ${buildFolderReference(name)}`;
|
|
40290
40297
|
const script = buildAccountScopedScript({ account: targetAccount }, deleteCommand);
|
|
40291
|
-
const result =
|
|
40298
|
+
const result = executeMutationAppleScript(script);
|
|
40292
40299
|
if (!result.success) {
|
|
40293
40300
|
console.error(`Failed to delete folder "${name}":`, result.error);
|
|
40294
40301
|
return false;
|
|
@@ -40345,7 +40352,7 @@ var AppleNotesManager = class {
|
|
|
40345
40352
|
move noteRef to destFolder
|
|
40346
40353
|
`;
|
|
40347
40354
|
const script = buildAppLevelScript(moveCommand);
|
|
40348
|
-
const result =
|
|
40355
|
+
const result = executeMutationAppleScript(script);
|
|
40349
40356
|
if (!result.success) {
|
|
40350
40357
|
console.error(
|
|
40351
40358
|
`Cannot move note to "${destinationFolder}" (folder may not exist):`,
|
|
@@ -40500,7 +40507,7 @@ var AppleNotesManager = class {
|
|
|
40500
40507
|
showNoteById(id, separately = false) {
|
|
40501
40508
|
const safeId = sanitizeId(id);
|
|
40502
40509
|
const separatelyClause = separately ? " separately true" : "";
|
|
40503
|
-
const result =
|
|
40510
|
+
const result = executeMutationAppleScript(
|
|
40504
40511
|
buildAppLevelScript(`show note id "${safeId}"${separatelyClause}`)
|
|
40505
40512
|
);
|
|
40506
40513
|
if (!result.success) {
|
|
@@ -40563,7 +40570,7 @@ var AppleNotesManager = class {
|
|
|
40563
40570
|
showFolderById(id, separately = false) {
|
|
40564
40571
|
const safeId = sanitizeId(id);
|
|
40565
40572
|
const separatelyClause = separately ? " separately true" : "";
|
|
40566
|
-
const result =
|
|
40573
|
+
const result = executeMutationAppleScript(
|
|
40567
40574
|
buildAppLevelScript(`show folder id "${safeId}"${separatelyClause}`)
|
|
40568
40575
|
);
|
|
40569
40576
|
if (!result.success) {
|
|
@@ -40585,7 +40592,7 @@ var AppleNotesManager = class {
|
|
|
40585
40592
|
showAccountById(id, separately = false) {
|
|
40586
40593
|
const safeId = sanitizeId(id);
|
|
40587
40594
|
const separatelyClause = separately ? " separately true" : "";
|
|
40588
|
-
const result =
|
|
40595
|
+
const result = executeMutationAppleScript(
|
|
40589
40596
|
buildAppLevelScript(`show account id "${safeId}"${separatelyClause}`)
|
|
40590
40597
|
);
|
|
40591
40598
|
if (!result.success) {
|
|
@@ -40628,7 +40635,7 @@ var AppleNotesManager = class {
|
|
|
40628
40635
|
return "OK"
|
|
40629
40636
|
end tell
|
|
40630
40637
|
`;
|
|
40631
|
-
const result =
|
|
40638
|
+
const result = executeMutationAppleScript(script);
|
|
40632
40639
|
if (!result.success) {
|
|
40633
40640
|
console.error(
|
|
40634
40641
|
`Failed to show attachment "${attachmentId}" on note "${noteId}":`,
|
|
@@ -41030,7 +41037,7 @@ var AppleNotesManager = class {
|
|
|
41030
41037
|
return "OK" & ${AS_FIELD_SEP} & (name of theAttachment) & ${AS_FIELD_SEP} & (content identifier of theAttachment)
|
|
41031
41038
|
end tell
|
|
41032
41039
|
`;
|
|
41033
|
-
const result =
|
|
41040
|
+
const result = executeMutationAppleScript(script);
|
|
41034
41041
|
if (!result.success) {
|
|
41035
41042
|
return { success: false, error: result.error ?? "unknown error" };
|
|
41036
41043
|
}
|
|
@@ -41164,7 +41171,7 @@ var AppleNotesManager = class {
|
|
|
41164
41171
|
end repeat
|
|
41165
41172
|
return out
|
|
41166
41173
|
`);
|
|
41167
|
-
const res =
|
|
41174
|
+
const res = executeMutationAppleScript(script);
|
|
41168
41175
|
if (!res.success) {
|
|
41169
41176
|
for (const r of runnable) {
|
|
41170
41177
|
results[r.index] = this.createBatchResult(
|
|
@@ -41274,7 +41281,7 @@ var AppleNotesManager = class {
|
|
|
41274
41281
|
end repeat
|
|
41275
41282
|
return out
|
|
41276
41283
|
`);
|
|
41277
|
-
const res =
|
|
41284
|
+
const res = executeMutationAppleScript(script);
|
|
41278
41285
|
if (!res.success) {
|
|
41279
41286
|
for (const r of runnable) {
|
|
41280
41287
|
results[r.index] = this.createBatchResult(
|
|
@@ -41766,6 +41773,40 @@ function strippedImagesWarning(stripped) {
|
|
|
41766
41773
|
)} decoded) exceeded the per-image inline cap and ${stripped.strippedCount === 1 ? "was" : "were"} replaced with placeholders so the response stays within MCP message limits. 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.`;
|
|
41767
41774
|
}
|
|
41768
41775
|
|
|
41776
|
+
// src/utils/updateResponseTitle.ts
|
|
41777
|
+
var BLOCK_END_RE = /<\/(?:div|h[1-6]|p|li)>/gi;
|
|
41778
|
+
var BREAK_RE = /<br\s*\/?\s*>/gi;
|
|
41779
|
+
var TAG_RE = /<[^>]*>/g;
|
|
41780
|
+
var NON_RENDERED_BLOCK_RE = /<(script|style)\b[^>]*>[\s\S]*?(?:<\/\1>|$)/gi;
|
|
41781
|
+
function decodeHtmlEntities(text) {
|
|
41782
|
+
const decodeCodePoint = (match, value, radix) => {
|
|
41783
|
+
const codePoint = Number.parseInt(value, radix);
|
|
41784
|
+
if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343) {
|
|
41785
|
+
return match;
|
|
41786
|
+
}
|
|
41787
|
+
return String.fromCodePoint(codePoint);
|
|
41788
|
+
};
|
|
41789
|
+
return text.replace(/&#x([0-9a-f]+);?/gi, (match, hex) => decodeCodePoint(match, hex, 16)).replace(/&#([0-9]+);?/g, (match, decimal) => decodeCodePoint(match, decimal, 10)).replace(/ (?:;|(?![0-9a-z]))/gi, " ").replace(/"(?:;|(?![0-9a-z]))/gi, '"').replace(/&apos(?:;|(?![0-9a-z]))/gi, "'").replace(/<(?:;|(?![0-9a-z]))/gi, "<").replace(/>(?:;|(?![0-9a-z]))/gi, ">").replace(/&(?:;|(?![0-9a-z]))/gi, "&");
|
|
41790
|
+
}
|
|
41791
|
+
function firstVisibleHtmlLine(html) {
|
|
41792
|
+
let text = html;
|
|
41793
|
+
let previous;
|
|
41794
|
+
do {
|
|
41795
|
+
previous = text;
|
|
41796
|
+
text = text.replace(NON_RENDERED_BLOCK_RE, "");
|
|
41797
|
+
} while (text !== previous);
|
|
41798
|
+
text = text.replace(BREAK_RE, "\n").replace(BLOCK_END_RE, "\n");
|
|
41799
|
+
do {
|
|
41800
|
+
previous = text;
|
|
41801
|
+
text = text.replace(TAG_RE, "");
|
|
41802
|
+
} while (text !== previous);
|
|
41803
|
+
return decodeHtmlEntities(text).split(/[\r\n\u2028\u2029]+/).map((line) => line.replace(/\s+/g, " ").trim()).find(Boolean);
|
|
41804
|
+
}
|
|
41805
|
+
function resolveUpdateResponseTitle(currentTitle, newTitle, format, newContent) {
|
|
41806
|
+
if (format === "html") return firstVisibleHtmlLine(newContent) ?? currentTitle;
|
|
41807
|
+
return newTitle || currentTitle;
|
|
41808
|
+
}
|
|
41809
|
+
|
|
41769
41810
|
// src/tools/doctor.ts
|
|
41770
41811
|
import { spawnSync } from "child_process";
|
|
41771
41812
|
function runDoctor(manager) {
|
|
@@ -42401,7 +42442,9 @@ server.registerTool(
|
|
|
42401
42442
|
inputSchema: {
|
|
42402
42443
|
id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
|
|
42403
42444
|
title: external_exports.string().max(MAX.TITLE).optional().describe("Current note title (use id instead when available)"),
|
|
42404
|
-
newTitle: external_exports.string().max(MAX.TITLE).optional().describe(
|
|
42445
|
+
newTitle: external_exports.string().max(MAX.TITLE).optional().describe(
|
|
42446
|
+
"New title for plaintext updates. Ignored when format is 'html'; include the visible title as the first line of newContent instead."
|
|
42447
|
+
),
|
|
42405
42448
|
newContent: external_exports.string().min(1, "New content is required").max(MAX.CONTENT).describe(
|
|
42406
42449
|
"New note body. AppleScript cannot produce true Apple Notes checklists; checkbox inputs and `- [ ]` markdown do not render as checkable items. Use a plain list and convert in Notes.app with \u21E7\u2318L."
|
|
42407
42450
|
),
|
|
@@ -42430,7 +42473,7 @@ server.registerTool(
|
|
|
42430
42473
|
if (!success2) {
|
|
42431
42474
|
return errorResponse(`Failed to update note "${note2.title}"`);
|
|
42432
42475
|
}
|
|
42433
|
-
const displayTitle = newTitle
|
|
42476
|
+
const displayTitle = resolveUpdateResponseTitle(note2.title, newTitle, format, newContent);
|
|
42434
42477
|
const sharedWarning2 = note2.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes will be visible to them." : "";
|
|
42435
42478
|
const checklistWarning2 = detectChecklistAttempt(newContent) ?? "";
|
|
42436
42479
|
return successResponse(`Note updated: "${displayTitle}"${sharedWarning2}${checklistWarning2}`, {
|
|
@@ -42458,7 +42501,7 @@ server.registerTool(
|
|
|
42458
42501
|
if (!success) {
|
|
42459
42502
|
return errorResponse(`Failed to update note "${title}"`);
|
|
42460
42503
|
}
|
|
42461
|
-
const finalTitle = newTitle
|
|
42504
|
+
const finalTitle = resolveUpdateResponseTitle(note.title, newTitle, format, newContent);
|
|
42462
42505
|
const sharedWarning = note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes will be visible to them." : "";
|
|
42463
42506
|
const checklistWarning = detectChecklistAttempt(newContent) ?? "";
|
|
42464
42507
|
return successResponse(`Note updated: "${finalTitle}"${sharedWarning}${checklistWarning}`, {
|
package/package.json
CHANGED