apple-notes-mcp 2.15.17 → 2.15.18
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 +7 -0
- package/build/index.js +70 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2444,6 +2444,13 @@ after it finishes. The result carries `verifiedBy: "database"`. Without Full
|
|
|
2444
2444
|
Disk Access a PDF attach reports "insertion outcome uncertain"; read the note
|
|
2445
2445
|
before retrying, because the attachment was probably created.
|
|
2446
2446
|
|
|
2447
|
+
Notes.app's AppleScript insertion places every new attachment in the note body
|
|
2448
|
+
twice (#321): one attachment, shown two times. The tool removes the extra copy
|
|
2449
|
+
in the same AppleScript call and checks that the note lists the attachment once.
|
|
2450
|
+
A PDF on macOS 27 cannot be listed, so its extra copy cannot be removed; when the
|
|
2451
|
+
database shows the PDF twice, the tool returns an error naming the attachment.
|
|
2452
|
+
Delete the extra copy in Notes.app and do not attach the file again.
|
|
2453
|
+
|
|
2447
2454
|
---
|
|
2448
2455
|
|
|
2449
2456
|
#### `create-note-with-attachment`
|
package/build/index.js
CHANGED
|
@@ -57554,25 +57554,35 @@ function parseAttachmentRows(json3) {
|
|
|
57554
57554
|
}
|
|
57555
57555
|
return rows.sort((a, b) => a.pk - b.pk);
|
|
57556
57556
|
}
|
|
57557
|
-
function
|
|
57557
|
+
function attachmentRunsFromNoteData(hex5) {
|
|
57558
57558
|
if (!hex5 || !/^[0-9a-f]+$/i.test(hex5)) return null;
|
|
57559
57559
|
try {
|
|
57560
57560
|
const data = gunzipSync5(Buffer.from(hex5, "hex"), { maxOutputLength: MAX_BODY_BYTES });
|
|
57561
57561
|
const wrapper = embeddedMessage(getField(decodeMessage(data), 2));
|
|
57562
57562
|
const body = wrapper && embeddedMessage(getField(wrapper, 3));
|
|
57563
57563
|
if (!body) return null;
|
|
57564
|
-
const
|
|
57564
|
+
const refs = [];
|
|
57565
57565
|
for (const run of getFields(body, 5)) {
|
|
57566
57566
|
const fields = embeddedMessage(run);
|
|
57567
57567
|
const info = fields && embeddedMessage(getField(fields, 12));
|
|
57568
57568
|
const id2 = info && stringValue(getField(info, 1));
|
|
57569
|
-
if (
|
|
57569
|
+
if (!id2) continue;
|
|
57570
|
+
const length = varintValue(getField(fields, 1)) ?? 1;
|
|
57571
|
+
for (let i = 0; i < Math.min(Math.max(length, 1), 64); i++) refs.push(id2);
|
|
57570
57572
|
}
|
|
57571
|
-
return
|
|
57573
|
+
return refs;
|
|
57572
57574
|
} catch {
|
|
57573
57575
|
return null;
|
|
57574
57576
|
}
|
|
57575
57577
|
}
|
|
57578
|
+
function attachmentOrderFromNoteData(hex5) {
|
|
57579
|
+
const refs = attachmentRunsFromNoteData(hex5);
|
|
57580
|
+
if (!refs) return null;
|
|
57581
|
+
const ids = [];
|
|
57582
|
+
for (const id2 of refs)
|
|
57583
|
+
if (!ids.some((seen) => seen.toLowerCase() === id2.toLowerCase())) ids.push(id2);
|
|
57584
|
+
return ids;
|
|
57585
|
+
}
|
|
57576
57586
|
function readNoteAttachmentRows(noteId3, dbPath2 = join9(NOTES_CONTAINER_DIR, "NoteStore.sqlite")) {
|
|
57577
57587
|
const { pk } = parseNoteId2(noteId3);
|
|
57578
57588
|
const columnList = runSqlite3(
|
|
@@ -57596,7 +57606,8 @@ function readNoteAttachmentRows(noteId3, dbPath2 = join9(NOTES_CONTAINER_DIR, "N
|
|
|
57596
57606
|
} catch {
|
|
57597
57607
|
throw new AttachmentStoreError("Attachment rows could not be parsed", "query_error");
|
|
57598
57608
|
}
|
|
57599
|
-
|
|
57609
|
+
const bodyRefs = attachmentRunsFromNoteData(lines[2]?.trim());
|
|
57610
|
+
return { rows, bodyOrder: attachmentOrderFromNoteData(lines[2]?.trim()), bodyRefs };
|
|
57600
57611
|
}
|
|
57601
57612
|
function safeComponent(value) {
|
|
57602
57613
|
if (typeof value !== "string" || !value || value.length > 255) return null;
|
|
@@ -57759,13 +57770,16 @@ function assetPathsFor(accountDir, row, onStale) {
|
|
|
57759
57770
|
add(file2);
|
|
57760
57771
|
return found;
|
|
57761
57772
|
}
|
|
57762
|
-
function assembleAttachmentAssets(allRows, bodyOrder, containerDir = NOTES_CONTAINER_DIR) {
|
|
57773
|
+
function assembleAttachmentAssets(allRows, bodyOrder, containerDir = NOTES_CONTAINER_DIR, bodyRefs = null) {
|
|
57763
57774
|
const rows = allRows.filter((r) => !r.parentDeleted);
|
|
57764
57775
|
const byPk = new Map(rows.map((r) => [r.pk, r]));
|
|
57765
57776
|
const roots = rows.filter((r) => r.parentPk === null || !byPk.has(r.parentPk));
|
|
57766
57777
|
const bodyIndex = /* @__PURE__ */ new Map();
|
|
57767
57778
|
(bodyOrder ?? []).forEach((id2, index) => bodyIndex.set(id2.toLowerCase(), index));
|
|
57768
57779
|
const indexOf = (r) => bodyIndex.get(r.identifier.toLowerCase()) ?? null;
|
|
57780
|
+
const refCount = /* @__PURE__ */ new Map();
|
|
57781
|
+
for (const id2 of bodyRefs ?? [])
|
|
57782
|
+
refCount.set(id2.toLowerCase(), (refCount.get(id2.toLowerCase()) ?? 0) + 1);
|
|
57769
57783
|
const inBody = roots.filter((r) => indexOf(r) !== null);
|
|
57770
57784
|
const orderSource = inBody.length > 0 ? "body" : "creation";
|
|
57771
57785
|
const orderedRoots = [
|
|
@@ -57799,6 +57813,7 @@ function assembleAttachmentAssets(allRows, bodyOrder, containerDir = NOTES_CONTA
|
|
|
57799
57813
|
parentIdentifier: parent?.identifier ?? null,
|
|
57800
57814
|
filename: row.filename ?? row.mediaFilename,
|
|
57801
57815
|
bodyIndex: parent ? null : indexOf(row),
|
|
57816
|
+
...!parent && (refCount.get(row.identifier.toLowerCase()) ?? 0) > 1 ? { bodyReferences: refCount.get(row.identifier.toLowerCase()) } : {},
|
|
57802
57817
|
assetPaths,
|
|
57803
57818
|
...fallbackStale ? { fallbackStale: true } : {},
|
|
57804
57819
|
previewPath,
|
|
@@ -60091,7 +60106,17 @@ var AppleNotesManager = class {
|
|
|
60091
60106
|
if (!result.success) throw new Error(result.error || "Could not check folder existence");
|
|
60092
60107
|
return result.output.trim() === "true";
|
|
60093
60108
|
}
|
|
60094
|
-
/**
|
|
60109
|
+
/**
|
|
60110
|
+
* Insert one file into an unchanged exact note and return Notes' attachment ID.
|
|
60111
|
+
*
|
|
60112
|
+
* Notes.app's `make new attachment` writes the new attachment into the note
|
|
60113
|
+
* body twice (#321): one attachment object, two body references, so the file
|
|
60114
|
+
* shows twice. Every form of the command does it (`at n`, `at end of
|
|
60115
|
+
* attachments of n`, `tell n to make`, an alias), and `every attachment of n`
|
|
60116
|
+
* then lists the same id twice. Deleting the extra entries by index removes
|
|
60117
|
+
* only their body references; the attachment object and its file stay. The
|
|
60118
|
+
* cleanup runs in the same AppleScript so no other edit can interleave.
|
|
60119
|
+
*/
|
|
60095
60120
|
addAttachmentById(id2, expectedBody, filePath) {
|
|
60096
60121
|
if (!/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i.test(id2))
|
|
60097
60122
|
throw new Error("Exact note ID required");
|
|
@@ -60103,7 +60128,16 @@ var AppleNotesManager = class {
|
|
|
60103
60128
|
if body of n is not ${quote(expectedBody)} and body of n is not (${quote(expectedBody)} & linefeed) then error "Note changed before attachment insertion"
|
|
60104
60129
|
end considering
|
|
60105
60130
|
set a to make new attachment at n with data (POSIX file ${quote(filePath)})
|
|
60106
|
-
|
|
60131
|
+
set newId to id of a
|
|
60132
|
+
set hits to {}
|
|
60133
|
+
set attachmentIds to id of every attachment of n
|
|
60134
|
+
repeat with i from 1 to count of attachmentIds
|
|
60135
|
+
if item i of attachmentIds is newId then set end of hits to i
|
|
60136
|
+
end repeat
|
|
60137
|
+
repeat with j from (count of hits) to 2 by -1
|
|
60138
|
+
delete attachment (item j of hits) of n
|
|
60139
|
+
end repeat
|
|
60140
|
+
return newId
|
|
60107
60141
|
end tell`);
|
|
60108
60142
|
if (!result.success)
|
|
60109
60143
|
throw new Error(
|
|
@@ -60111,6 +60145,27 @@ var AppleNotesManager = class {
|
|
|
60111
60145
|
);
|
|
60112
60146
|
return result.output.trim();
|
|
60113
60147
|
}
|
|
60148
|
+
/**
|
|
60149
|
+
* How many entries Notes' `every attachment` holds for one attachment id
|
|
60150
|
+
* (#321). Read-only. Notes on macOS 27 does not list PDF attachments, so a PDF
|
|
60151
|
+
* counts 0 here; the NoteStore database is the check for those.
|
|
60152
|
+
*/
|
|
60153
|
+
countAttachmentEntriesById(noteId3, attachmentId) {
|
|
60154
|
+
if (!/^x-coredata:\/\/[0-9a-f-]+\/ICAttachment\/p\d+$/i.test(attachmentId))
|
|
60155
|
+
throw new Error("Exact attachment ID required");
|
|
60156
|
+
const result = executeAppleScript(`tell application "Notes"
|
|
60157
|
+
set attachmentIds to id of every attachment of note id "${sanitizeId(noteId3)}"
|
|
60158
|
+
set hits to 0
|
|
60159
|
+
repeat with i from 1 to count of attachmentIds
|
|
60160
|
+
if item i of attachmentIds is "${attachmentId}" then set hits to hits + 1
|
|
60161
|
+
end repeat
|
|
60162
|
+
return hits as text
|
|
60163
|
+
end tell`);
|
|
60164
|
+
if (!result.success) throw new Error(result.error || "Could not count attachment entries");
|
|
60165
|
+
const hits = Number(result.output.trim());
|
|
60166
|
+
if (!Number.isInteger(hits) || hits < 0) throw new Error("Unexpected attachment count");
|
|
60167
|
+
return hits;
|
|
60168
|
+
}
|
|
60114
60169
|
/**
|
|
60115
60170
|
* Lists all folders in an account with full hierarchical paths.
|
|
60116
60171
|
*
|
|
@@ -61124,8 +61179,8 @@ var AppleNotesManager = class {
|
|
|
61124
61179
|
* @throws AttachmentStoreError (`no_fda`, `invalid_id`, `not_found`, `query_error`)
|
|
61125
61180
|
*/
|
|
61126
61181
|
getAttachmentAssetsById(noteId3) {
|
|
61127
|
-
const { rows, bodyOrder } = readNoteAttachmentRows(noteId3);
|
|
61128
|
-
return assembleAttachmentAssets(rows, bodyOrder);
|
|
61182
|
+
const { rows, bodyOrder, bodyRefs } = readNoteAttachmentRows(noteId3);
|
|
61183
|
+
return assembleAttachmentAssets(rows, bodyOrder, void 0, bodyRefs);
|
|
61129
61184
|
}
|
|
61130
61185
|
/**
|
|
61131
61186
|
* The note's lead visual: the first image in body order (even when its asset
|
|
@@ -71262,7 +71317,7 @@ function registerDirectOperations(server2, manager) {
|
|
|
71262
71317
|
};
|
|
71263
71318
|
tool(
|
|
71264
71319
|
"add-attachment",
|
|
71265
|
-
"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: reads only a regular file in home, temp or /Volumes, refusing hidden paths (~/.ssh, .env) and ~/Library outside iCloud Drive and CloudStorage unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1; 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.",
|
|
71320
|
+
"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: reads only a regular file in home, temp or /Volumes, refusing hidden paths (~/.ssh, .env) and ~/Library outside iCloud Drive and CloudStorage unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1; requires a fresh rich revision, copies at most 64 MiB through a private temporary file, never retries insertion, removes the duplicate copy Notes.app's AppleScript inserts (#321), 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. A PDF that Notes shows twice cannot be cleaned up and is reported as an error: delete the extra copy in Notes.app.",
|
|
71266
71321
|
{ id: noteId, expectedContentHash: revision, ...attachmentInput },
|
|
71267
71322
|
(args) => attachFile(manager, args)
|
|
71268
71323
|
);
|
|
@@ -71401,6 +71456,7 @@ function attachmentName(path10, filename) {
|
|
|
71401
71456
|
return filename;
|
|
71402
71457
|
}
|
|
71403
71458
|
var UNCERTAIN = "Attachment insertion outcome uncertain; read the exact note before retrying";
|
|
71459
|
+
var duplicated = (attachmentId, copies) => `Notes.app put new attachment ${attachmentId} into this note ${copies} times and the extra ${copies === 2 ? "copy" : "copies"} could not be removed automatically; ask the user to delete the extra ${copies === 2 ? "copy" : "copies"} in Notes.app, and do not attach the file again`;
|
|
71404
71460
|
var pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
71405
71461
|
var sha256 = (data) => createHash4("sha256").update(data).digest("hex");
|
|
71406
71462
|
function storedAttachmentIds(manager, id2) {
|
|
@@ -71456,6 +71512,7 @@ function storedInsertion(manager, id2, before, bytes, name, returnedId) {
|
|
|
71456
71512
|
throw new Error(
|
|
71457
71513
|
`Notes' database shows new attachment ${attachmentId} on this note, but the note body does not reference it; read the exact note and do not attach the file again`
|
|
71458
71514
|
);
|
|
71515
|
+
if ((row.bodyReferences ?? 1) > 1) throw new Error(duplicated(attachmentId, row.bodyReferences));
|
|
71459
71516
|
const expected = sha256(bytes);
|
|
71460
71517
|
const matches = row.assetPaths.some((path10) => fileMatches(path10, bytes.length, expected));
|
|
71461
71518
|
if (!matches)
|
|
@@ -71506,6 +71563,8 @@ function attachFile(manager, args, checked, progress = { insertionStarted: false
|
|
|
71506
71563
|
const saved = manager.saveAttachmentById(id2, attachmentId, verifyPath);
|
|
71507
71564
|
if (!saved.success || !fileMatches(saved.savedPath ?? verifyPath, bytes.length, sha256(bytes)))
|
|
71508
71565
|
throw new Error("Attachment bytes were not verified; read the exact note before retrying");
|
|
71566
|
+
const copies = manager.countAttachmentEntriesById(id2, attachmentId);
|
|
71567
|
+
if (copies > 1) throw new Error(duplicated(attachmentId, copies));
|
|
71509
71568
|
} else {
|
|
71510
71569
|
const stored = inserted.length === 0 && beforeStored ? storedInsertion(manager, id2, beforeStored, bytes, name, returnedId) : null;
|
|
71511
71570
|
if (!stored)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-notes-mcp",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.18",
|
|
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",
|