apple-notes-mcp 2.8.45 → 2.8.47
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 -5
- package/build/index.js +99 -33
- package/docs/FULL-DISK-ACCESS.md +40 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Install as a Claude Code plugin for automatic configuration and enhanced AI beha
|
|
|
56
56
|
|
|
57
57
|
This method also installs a **skill** that teaches Claude when and how to use Apple Notes effectively.
|
|
58
58
|
|
|
59
|
-
On the first tool call, macOS shows an Automation permission prompt ("Claude" wants access to control "Notes") — click **OK**. Optionally, grant **Full Disk Access** to the
|
|
59
|
+
On the first tool call, macOS shows an Automation permission prompt ("Claude" wants access to control "Notes") — click **OK**. Optionally, grant **Full Disk Access** (under Claude Desktop, to the Node binary that runs the server; from a terminal, to the terminal app) to enable the database-backed tools (`get-checklist-state`, `get-note-metadata`, `get-audio-transcripts`, `list-special-notes`, `list-native-tags`, `get-note-link`, checklist annotations in `get-note-markdown`, and full `get-sync-status` detail); see the [Full Disk Access Setup Guide](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md). The rest of the server is pure AppleScript and works without it.
|
|
60
60
|
|
|
61
61
|
Native tag, checklist, table, pin, and rich append operations use two packaged
|
|
62
62
|
Apple Shortcuts. A third, `Apple Notes MCP - Create Markdown Note`, is optional:
|
|
@@ -1035,7 +1035,7 @@ Returns the `notes://showNote?identifier=<uuid>` deep-link URL for a note. The U
|
|
|
1035
1035
|
|
|
1036
1036
|
**Returns:** `notes://showNote?identifier=<uuid>` URL string, plus the note id and title.
|
|
1037
1037
|
|
|
1038
|
-
**Note:** Requires Full Disk Access for the
|
|
1038
|
+
**Note:** Requires Full Disk Access for the process that runs the server so the Notes SQLite database is readable. On macOS 12–15 the tool also falls back to the AppleScript `note link` property. Run the `doctor` tool to verify access.
|
|
1039
1039
|
|
|
1040
1040
|
---
|
|
1041
1041
|
|
|
@@ -1174,6 +1174,8 @@ Creates a new folder, including a whole nested hierarchy in one call.
|
|
|
1174
1174
|
|
|
1175
1175
|
**Returns:** Confirmation message. The call is **idempotent** — an already-existing folder (or path segment) is skipped rather than treated as an error, so it is safe to call before every `create-note` that targets a folder.
|
|
1176
1176
|
|
|
1177
|
+
Existence is decided by folder id, not by name: a folder deleted earlier in the same Notes session no longer counts as existing, and the call succeeds only once the created folder is confirmed by its id. Otherwise it reports failure.
|
|
1178
|
+
|
|
1177
1179
|
---
|
|
1178
1180
|
|
|
1179
1181
|
#### `get-folder-by-id`
|
|
@@ -2125,12 +2127,12 @@ Several tools read directly from the Apple Notes SQLite database, which lives in
|
|
|
2125
2127
|
1. Open **System Settings** (or System Preferences on older macOS)
|
|
2126
2128
|
2. Go to **Privacy & Security > Full Disk Access**
|
|
2127
2129
|
3. Click the **+** button
|
|
2128
|
-
4. Add the
|
|
2129
|
-
- **Claude Desktop**:
|
|
2130
|
+
4. Add the entry that matches how the server runs:
|
|
2131
|
+
- **Claude Desktop**: add the **Node binary** that runs the server (the `doctor` tool prints its path, e.g. `~/.nvm/versions/node/v24.11.1/bin/node`; press ⌘⇧G in the file picker to paste it). Claude Desktop launches MCP servers as their own responsible process, so a grant on `/Applications/Claude.app` alone does not reach them ([#220](https://github.com/sweetrb/apple-notes-mcp/issues/220)).
|
|
2130
2132
|
- **Terminal**: Add `/Applications/Utilities/Terminal.app`
|
|
2131
2133
|
- **VS Code**: Add `/Applications/Visual Studio Code.app`
|
|
2132
2134
|
- **iTerm**: Add `/Applications/iTerm.app`
|
|
2133
|
-
5.
|
|
2135
|
+
5. Fully quit (⌘Q) and relaunch the host app after granting access; if `doctor` still reports it missing, restart the Mac
|
|
2134
2136
|
|
|
2135
2137
|
### Without Full Disk Access
|
|
2136
2138
|
|
package/build/index.js
CHANGED
|
@@ -39536,7 +39536,7 @@ function getChecklistItems(noteId3) {
|
|
|
39536
39536
|
return {
|
|
39537
39537
|
items: null,
|
|
39538
39538
|
error: "no_fda",
|
|
39539
|
-
message: `Full Disk Access is required to read checklist state. In System Settings > Privacy & Security > Full Disk Access, grant access to the
|
|
39539
|
+
message: `Full Disk Access is required to read checklist state. In System Settings > Privacy & Security > Full Disk Access, grant access to the Node binary running this server (required under Claude Desktop) or the terminal that launches it, then fully quit and relaunch it. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL} \u2014 run the doctor tool to verify.`
|
|
39540
39540
|
};
|
|
39541
39541
|
}
|
|
39542
39542
|
if (!hexData) {
|
|
@@ -40199,7 +40199,7 @@ function readAudioTranscripts(noteId3, options = {}) {
|
|
|
40199
40199
|
if (/authorization denied|unable to open database/i.test(message))
|
|
40200
40200
|
throw new AudioTranscriptError(
|
|
40201
40201
|
"no_fda",
|
|
40202
|
-
`Full Disk Access is required to read stored transcripts. Grant it to the
|
|
40202
|
+
`Full Disk Access is required to read stored transcripts. Grant it to the Node binary running this server (or the terminal that launches it), then fully quit and relaunch it. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL}`
|
|
40203
40203
|
);
|
|
40204
40204
|
throw new AudioTranscriptError("query_error", "Failed to read the Notes database.");
|
|
40205
40205
|
}
|
|
@@ -40516,7 +40516,7 @@ var NOTES_DB_PATH3 = path2.join(
|
|
|
40516
40516
|
os2.homedir(),
|
|
40517
40517
|
"Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
|
|
40518
40518
|
);
|
|
40519
|
-
var FDA_MESSAGE = `Full Disk Access is required to read smart folders. In System Settings > Privacy & Security > Full Disk Access, grant access to the
|
|
40519
|
+
var FDA_MESSAGE = `Full Disk Access is required to read smart folders. In System Settings > Privacy & Security > Full Disk Access, grant access to the Node binary running this server (required under Claude Desktop) or the terminal that launches it, then fully quit and relaunch it. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL} \u2014 run the doctor tool to verify.`;
|
|
40520
40520
|
var COCOA_EPOCH_OFFSET = 978307200;
|
|
40521
40521
|
var MAX_DEPTH = 32;
|
|
40522
40522
|
var RELATIVE_RANGES = {
|
|
@@ -41275,7 +41275,7 @@ function runSqlite3(dbPath2, sql) {
|
|
|
41275
41275
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
41276
41276
|
if (/authorization denied|unable to open database/i.test(message)) {
|
|
41277
41277
|
throw new AttachmentStoreError(
|
|
41278
|
-
"Full Disk Access is required to read attachment paths. Grant it to the
|
|
41278
|
+
"Full Disk Access is required to read attachment paths. Grant it to the Node binary running this server (or the terminal that launches it), then relaunch it (run the doctor tool to verify).",
|
|
41279
41279
|
"no_fda"
|
|
41280
41280
|
);
|
|
41281
41281
|
}
|
|
@@ -41809,7 +41809,7 @@ function runSqlite4(dbPath2, sql) {
|
|
|
41809
41809
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
41810
41810
|
if (/authorization denied|unable to open database/i.test(message)) {
|
|
41811
41811
|
throw new AttachmentStoreError(
|
|
41812
|
-
"Full Disk Access is required to read drawing attachments. Grant it to the
|
|
41812
|
+
"Full Disk Access is required to read drawing attachments. Grant it to the Node binary running this server (or the terminal that launches it), then relaunch it (run the doctor tool to verify).",
|
|
41813
41813
|
"no_fda"
|
|
41814
41814
|
);
|
|
41815
41815
|
}
|
|
@@ -42321,6 +42321,58 @@ function buildFolderReference(folderPath) {
|
|
|
42321
42321
|
return parts.reverse().map((part) => `folder "${escapePlainStringForAppleScript(part)}"`).join(" of ");
|
|
42322
42322
|
}
|
|
42323
42323
|
var AS_ACCOUNT_REF = "__acctRef";
|
|
42324
|
+
var LIVE_FOLDER_NOT_FOUND = "Folder not found";
|
|
42325
|
+
function buildLiveFolderResolution(folderPath, varName, opts = {}) {
|
|
42326
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) throw new Error("Invalid AppleScript variable");
|
|
42327
|
+
buildFolderReference(folderPath);
|
|
42328
|
+
const parts = splitFolderPath(folderPath);
|
|
42329
|
+
const c = `${varName}_c`;
|
|
42330
|
+
const cid = `${varName}_cid`;
|
|
42331
|
+
const any = `${varName}_any`;
|
|
42332
|
+
const parent = `${varName}_p`;
|
|
42333
|
+
const notFound = `error "${LIVE_FOLDER_NOT_FOUND}: ${escapePlainStringForAppleScript(folderPath)}" number -1728`;
|
|
42334
|
+
const lines = [];
|
|
42335
|
+
parts.forEach((part, i) => {
|
|
42336
|
+
const name = escapePlainStringForAppleScript(part);
|
|
42337
|
+
if (i === 0) {
|
|
42338
|
+
lines.push(
|
|
42339
|
+
`set ${varName} to missing value`,
|
|
42340
|
+
`set ${any} to missing value`,
|
|
42341
|
+
`repeat with ${c} in (folders of ${AS_ACCOUNT_REF} whose name is "${name}")`,
|
|
42342
|
+
` try`,
|
|
42343
|
+
` set ${cid} to id of ${c}`,
|
|
42344
|
+
` if exists folder id ${cid} then`,
|
|
42345
|
+
` if class of (container of ${c}) is not folder then`,
|
|
42346
|
+
` set ${varName} to folder id ${cid}`,
|
|
42347
|
+
` exit repeat`,
|
|
42348
|
+
` else if ${any} is missing value then`,
|
|
42349
|
+
` set ${any} to folder id ${cid}`,
|
|
42350
|
+
` end if`,
|
|
42351
|
+
` end if`,
|
|
42352
|
+
` end try`,
|
|
42353
|
+
`end repeat`
|
|
42354
|
+
);
|
|
42355
|
+
if (!opts.rootOnly)
|
|
42356
|
+
lines.push(`if ${varName} is missing value then set ${varName} to ${any}`);
|
|
42357
|
+
} else {
|
|
42358
|
+
lines.push(
|
|
42359
|
+
`set ${parent} to ${varName}`,
|
|
42360
|
+
`set ${varName} to missing value`,
|
|
42361
|
+
`repeat with ${c} in (folders of ${parent} whose name is "${name}")`,
|
|
42362
|
+
` try`,
|
|
42363
|
+
` set ${cid} to id of ${c}`,
|
|
42364
|
+
` if exists folder id ${cid} then`,
|
|
42365
|
+
` set ${varName} to folder id ${cid}`,
|
|
42366
|
+
` exit repeat`,
|
|
42367
|
+
` end if`,
|
|
42368
|
+
` end try`,
|
|
42369
|
+
`end repeat`
|
|
42370
|
+
);
|
|
42371
|
+
}
|
|
42372
|
+
lines.push(`if ${varName} is missing value then ${notFound}`);
|
|
42373
|
+
});
|
|
42374
|
+
return lines.join("\n");
|
|
42375
|
+
}
|
|
42324
42376
|
var ACCOUNT_RESOLUTION_ERROR = "AccountResolutionError:";
|
|
42325
42377
|
function throwIfAccountResolutionFailed(error2) {
|
|
42326
42378
|
if (!error2) return;
|
|
@@ -42550,8 +42602,8 @@ var AppleNotesManager = class {
|
|
|
42550
42602
|
const safeBody = escapeHtmlForAppleScript(`<h1>${htmlTitle}</h1>${bodyContent}`);
|
|
42551
42603
|
let createCommand;
|
|
42552
42604
|
if (folder) {
|
|
42553
|
-
|
|
42554
|
-
|
|
42605
|
+
createCommand = `${buildLiveFolderResolution(folder, "__folder")}
|
|
42606
|
+
make new note at __folder with properties {body:"${safeBody}"}`;
|
|
42555
42607
|
} else {
|
|
42556
42608
|
createCommand = `
|
|
42557
42609
|
set newNote to make new note with properties {body:"${safeBody}"}
|
|
@@ -43663,10 +43715,10 @@ var AppleNotesManager = class {
|
|
|
43663
43715
|
}
|
|
43664
43716
|
for (let i = 0; i < parts.length; i++) {
|
|
43665
43717
|
const currentPath = parts.slice(0, i + 1).map((p) => escapeFolderName(p)).join("/");
|
|
43666
|
-
const currentRef = buildFolderReference(currentPath);
|
|
43667
43718
|
const checkScript = buildAccountScopedScript(
|
|
43668
43719
|
{ account: targetAccount },
|
|
43669
|
-
|
|
43720
|
+
`${buildLiveFolderResolution(currentPath, "__folder", { rootOnly: true })}
|
|
43721
|
+
return id of __folder`
|
|
43670
43722
|
);
|
|
43671
43723
|
const checkResult = executeAppleScript(checkScript);
|
|
43672
43724
|
if (checkResult.success) {
|
|
@@ -43678,8 +43730,8 @@ var AppleNotesManager = class {
|
|
|
43678
43730
|
createCommand = `make new folder with properties {name:"${segmentName}"}`;
|
|
43679
43731
|
} else {
|
|
43680
43732
|
const parentPath = parts.slice(0, i).map((p) => escapeFolderName(p)).join("/");
|
|
43681
|
-
|
|
43682
|
-
|
|
43733
|
+
createCommand = `${buildLiveFolderResolution(parentPath, "__parent", { rootOnly: true })}
|
|
43734
|
+
make new folder at __parent with properties {name:"${segmentName}"}`;
|
|
43683
43735
|
}
|
|
43684
43736
|
const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
|
|
43685
43737
|
const result = executeMutationAppleScript(script);
|
|
@@ -43689,14 +43741,23 @@ var AppleNotesManager = class {
|
|
|
43689
43741
|
return null;
|
|
43690
43742
|
}
|
|
43691
43743
|
}
|
|
43692
|
-
const fullRef = buildFolderReference(name);
|
|
43693
43744
|
const idScript = buildAccountScopedScript(
|
|
43694
43745
|
{ account: targetAccount },
|
|
43695
|
-
|
|
43746
|
+
`${buildLiveFolderResolution(name, "__folder", { rootOnly: true })}
|
|
43747
|
+
return (id of __folder) & ${AS_FIELD_SEP} & (name of it)`
|
|
43696
43748
|
);
|
|
43697
43749
|
const idResult = executeAppleScript(idScript);
|
|
43698
43750
|
const [rawId = "", rawAccount = ""] = idResult.success ? idResult.output.split(FIELD_SEP) : [];
|
|
43699
|
-
const
|
|
43751
|
+
const bareId = rawId.trim();
|
|
43752
|
+
const folderId2 = !idResult.success ? "" : FOLDER_ID_PATTERN.test(bareId) ? bareId : extractCoreDataId(rawId, "folder");
|
|
43753
|
+
if (!folderId2) {
|
|
43754
|
+
throwIfAccountResolutionFailed(idResult.error);
|
|
43755
|
+
console.error(
|
|
43756
|
+
`Folder "${name}" could not be confirmed after creation:`,
|
|
43757
|
+
idResult.error ?? "no folder id returned"
|
|
43758
|
+
);
|
|
43759
|
+
return null;
|
|
43760
|
+
}
|
|
43700
43761
|
return {
|
|
43701
43762
|
id: folderId2,
|
|
43702
43763
|
name,
|
|
@@ -43714,7 +43775,8 @@ var AppleNotesManager = class {
|
|
|
43714
43775
|
*/
|
|
43715
43776
|
deleteFolder(name, account) {
|
|
43716
43777
|
const targetAccount = this.resolveAccount(account);
|
|
43717
|
-
const deleteCommand =
|
|
43778
|
+
const deleteCommand = `${buildLiveFolderResolution(name, "__folder")}
|
|
43779
|
+
delete __folder`;
|
|
43718
43780
|
const script = buildAccountScopedScript({ account: targetAccount }, deleteCommand);
|
|
43719
43781
|
const result = executeMutationAppleScript(script);
|
|
43720
43782
|
if (!result.success) {
|
|
@@ -43740,10 +43802,10 @@ var AppleNotesManager = class {
|
|
|
43740
43802
|
moveNoteById(id2, destinationFolder, account, scope2) {
|
|
43741
43803
|
const targetAccount = this.resolveAccount(account);
|
|
43742
43804
|
const safeId = sanitizeNoteId(id2);
|
|
43743
|
-
const
|
|
43805
|
+
const destFolderSetup = buildLiveFolderResolution(destinationFolder, "destFolder");
|
|
43744
43806
|
const moveCommand = `
|
|
43745
43807
|
${buildAccountResolution(targetAccount)}
|
|
43746
|
-
|
|
43808
|
+
${destFolderSetup}
|
|
43747
43809
|
set noteRef to note id "${safeId}"${buildScopeGuardScript("noteRef", scope2, "destFolder")}
|
|
43748
43810
|
move noteRef to destFolder
|
|
43749
43811
|
set movedNoteRef to note id "${safeId}"
|
|
@@ -44649,7 +44711,7 @@ var AppleNotesManager = class {
|
|
|
44649
44711
|
batchMoveNotes(ids, folder, account) {
|
|
44650
44712
|
if (ids.length === 0) return [];
|
|
44651
44713
|
const targetAccount = this.resolveAccount(account);
|
|
44652
|
-
const
|
|
44714
|
+
const destFolderSetup = buildLiveFolderResolution(folder, "destFolder");
|
|
44653
44715
|
const results = new Array(ids.length);
|
|
44654
44716
|
const runnable = [];
|
|
44655
44717
|
ids.forEach((id2, i) => {
|
|
@@ -44667,7 +44729,7 @@ var AppleNotesManager = class {
|
|
|
44667
44729
|
const idList2 = runnable.map((r) => `"${r.safe}"`).join(", ");
|
|
44668
44730
|
const script = buildAppLevelScript(`
|
|
44669
44731
|
${buildAccountResolution(targetAccount)}
|
|
44670
|
-
|
|
44732
|
+
${destFolderSetup}
|
|
44671
44733
|
set out to ""
|
|
44672
44734
|
repeat with rawId in {${idList2}}
|
|
44673
44735
|
set theId to (rawId as text)
|
|
@@ -45118,7 +45180,7 @@ var NOTES_DB_PATH6 = path5.join(
|
|
|
45118
45180
|
os5.homedir(),
|
|
45119
45181
|
"Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
|
|
45120
45182
|
);
|
|
45121
|
-
var FDA_MESSAGE2 = `Full Disk Access is required to read note metadata. In System Settings > Privacy & Security > Full Disk Access, grant access to the
|
|
45183
|
+
var FDA_MESSAGE2 = `Full Disk Access is required to read note metadata. In System Settings > Privacy & Security > Full Disk Access, grant access to the Node binary running this server (required under Claude Desktop) or the terminal that launches it, then fully quit and relaunch it. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL} \u2014 run the doctor tool to verify.`;
|
|
45122
45184
|
var COLUMN_MAP = [
|
|
45123
45185
|
{ key: "pinned", column: "ZISPINNED", type: "bool" },
|
|
45124
45186
|
{ key: "hasChecklist", column: "ZHASCHECKLIST", type: "bool" },
|
|
@@ -45211,7 +45273,7 @@ var NOTES_DB_PATH7 = path6.join(
|
|
|
45211
45273
|
"Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
|
|
45212
45274
|
);
|
|
45213
45275
|
var CORE_DATA_EPOCH_MS = Date.UTC(2001, 0, 1);
|
|
45214
|
-
var STORE_FDA_MESSAGE = `Full Disk Access is required to read the Notes database. In System Settings > Privacy & Security > Full Disk Access, grant access to the
|
|
45276
|
+
var STORE_FDA_MESSAGE = `Full Disk Access is required to read the Notes database. In System Settings > Privacy & Security > Full Disk Access, grant access to the Node binary running this server (required under Claude Desktop) or the terminal that launches it, then fully quit and relaunch it. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL} \u2014 run the doctor tool to verify.`;
|
|
45215
45277
|
var NoteStoreError = class extends Error {
|
|
45216
45278
|
constructor(message, kind) {
|
|
45217
45279
|
super(message);
|
|
@@ -45635,7 +45697,7 @@ var ENTITY_LABEL = {
|
|
|
45635
45697
|
ICFolder: "folder",
|
|
45636
45698
|
ICAccount: "account"
|
|
45637
45699
|
};
|
|
45638
|
-
var NO_FDA_MESSAGE = `Resolving a Notes UUID or numeric key reads the Notes database, which needs Full Disk Access for the
|
|
45700
|
+
var NO_FDA_MESSAGE = `Resolving a Notes UUID or numeric key reads the Notes database, which needs Full Disk Access for the Node binary running this server, or the terminal that launches it (System Settings > Privacy & Security > Full Disk Access, then fully quit and relaunch it). x-coredata ids from search-notes, list-notes, or list-folders work without it. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL}`;
|
|
45639
45701
|
function canonicalKey(value) {
|
|
45640
45702
|
return BigInt(value).toString();
|
|
45641
45703
|
}
|
|
@@ -46391,7 +46453,7 @@ var QUERY_RESULTS = { DEFAULT: 50, MAX: 500 };
|
|
|
46391
46453
|
var CORE_DATA_EPOCH_MS2 = Date.UTC(2001, 0, 1);
|
|
46392
46454
|
var SNIPPET_BEFORE = 60;
|
|
46393
46455
|
var SNIPPET_LENGTH = 180;
|
|
46394
|
-
var QUERY_FDA_MESSAGE = `Full Disk Access is required to query notes. In System Settings > Privacy & Security > Full Disk Access, grant access to the
|
|
46456
|
+
var QUERY_FDA_MESSAGE = `Full Disk Access is required to query notes. In System Settings > Privacy & Security > Full Disk Access, grant access to the Node binary running this server (required under Claude Desktop) or the terminal that launches it, then fully quit and relaunch it. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL} \u2014 run the doctor tool to verify.`;
|
|
46395
46457
|
var NoteQueryStoreError = class extends Error {
|
|
46396
46458
|
constructor(message, kind) {
|
|
46397
46459
|
super(message);
|
|
@@ -46776,7 +46838,7 @@ function describeContentScan(scan) {
|
|
|
46776
46838
|
}
|
|
46777
46839
|
function contentSearchFailureHint(message, dbUnavailable) {
|
|
46778
46840
|
if (!/timed out/i.test(message)) return message;
|
|
46779
|
-
const remedy = dbUnavailable === "no_fda" ? " Grant Full Disk Access to the
|
|
46841
|
+
const remedy = dbUnavailable === "no_fda" ? " Grant Full Disk Access to the Node binary running this server (run the doctor tool for its path) so search-notes can search note bodies through the Notes database instead, which takes well under a second." : "";
|
|
46780
46842
|
return `${message} Body search through AppleScript scans every note body before the result limit applies, so a broad term can exceed the time budget on a large library.${remedy} Otherwise narrow the search with \`folder\` or \`modifiedSince\`, or use a more specific term.`;
|
|
46781
46843
|
}
|
|
46782
46844
|
|
|
@@ -47868,7 +47930,7 @@ function runDoctor(manager, capabilityMatrix = getCapabilityMatrix) {
|
|
|
47868
47930
|
checks.push({
|
|
47869
47931
|
name: "Full Disk Access",
|
|
47870
47932
|
status: fda ? "ok" : "warn",
|
|
47871
|
-
detail: fda ? "granted \u2014 the Notes database is readable (checklist state, note metadata, note links, sync detail)" :
|
|
47933
|
+
detail: fda ? "granted \u2014 the Notes database is readable (checklist state, note metadata, note links, sync detail)" : "not granted \u2014 get-checklist-state, get-note-metadata, and the checklist annotations in get-note-markdown won't work; get-note-link fails on macOS 26+ (macOS 12-15 falls back to AppleScript); get-sync-status still answers but cannot see pending uploads. Everything else is pure AppleScript and is unaffected. " + fdaRemediation()
|
|
47872
47934
|
});
|
|
47873
47935
|
const consentReminder = "After install or upgrade, run each bridge Shortcut once in the foreground in Shortcuts.app and choose Always Allow: a background run cannot display a first-run consent prompt, so an unanswered one stalls that bridge's native writes until they time out while this check stays ok";
|
|
47874
47936
|
try {
|
|
@@ -47899,6 +47961,10 @@ function runDoctor(manager, capabilityMatrix = getCapabilityMatrix) {
|
|
|
47899
47961
|
}
|
|
47900
47962
|
return matrix ? { healthy, checks, runtimeOS: matrix.runtimeOS, features: matrix.features } : { healthy, checks };
|
|
47901
47963
|
}
|
|
47964
|
+
function fdaRemediation(execPath = process.execPath) {
|
|
47965
|
+
const versioned = /\/(\.nvm|\.fnm|\.volta|\.asdf|\.local\/share\/mise|\.nodenv|n\/versions)\//.test(execPath);
|
|
47966
|
+
return `In System Settings > Privacy & Security > Full Disk Access, click + and add the Node binary running this server: ${execPath} (press Cmd+Shift+G in the file picker to paste the path). Under Claude Desktop that entry is required: Claude Desktop launches servers as their own responsible process, so a grant on Claude.app does not reach them. When the server runs from a terminal (Terminal, iTerm2) or an editor, granting that app is enough. Then fully quit (Cmd+Q) and relaunch the host app and re-run doctor; if it still reports not granted, restart the Mac. ` + (versioned ? "This Node lives under a version manager, so the path changes with each Node version and the grant has to be added again after switching; pointing the MCP config at one fixed Node path avoids that. " : "") + `Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL}`;
|
|
47967
|
+
}
|
|
47902
47968
|
function markdownBridgeDetail() {
|
|
47903
47969
|
const purpose = 'needed only for create-note format: "markdown" on macOS 26+';
|
|
47904
47970
|
try {
|
|
@@ -50522,7 +50588,7 @@ function classify(error2) {
|
|
|
50522
50588
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
50523
50589
|
if (/authorization denied|unable to open database|not authorized/i.test(message)) {
|
|
50524
50590
|
return new FolderStoreError(
|
|
50525
|
-
"Full Disk Access is required to verify folder type and contents before deleting. Grant it to the
|
|
50591
|
+
"Full Disk Access is required to verify folder type and contents before deleting. Grant it to the Node binary running this server (or the terminal that launches it), then relaunch it.",
|
|
50526
50592
|
"no_fda"
|
|
50527
50593
|
);
|
|
50528
50594
|
}
|
|
@@ -52104,7 +52170,7 @@ registerTool(
|
|
|
52104
52170
|
registerTool(
|
|
52105
52171
|
"get-note-link",
|
|
52106
52172
|
{
|
|
52107
|
-
description: "Use when: you need the notes:// deep-link URL for a note so it can be stored in a Reminders task, shared, or opened directly.\nReturns: a notes://showNote?identifier=<uuid> URL that opens the note in Notes.app on iOS and macOS.\nDo not use when: you only need the note's CoreData id (get-note-by-id) or want to reveal the note on screen (show-note).\nNote: the primary path reads the note's identifier from the Notes database, so it needs Full Disk Access for the
|
|
52173
|
+
description: "Use when: you need the notes:// deep-link URL for a note so it can be stored in a Reminders task, shared, or opened directly.\nReturns: a notes://showNote?identifier=<uuid> URL that opens the note in Notes.app on iOS and macOS.\nDo not use when: you only need the note's CoreData id (get-note-by-id) or want to reveal the note on screen (show-note).\nNote: the primary path reads the note's identifier from the Notes database, so it needs Full Disk Access for the Node binary running this server; macOS 12-15 can fall back to the AppleScript 'note link' property, which macOS 26+ no longer exposes. Password-protected notes cannot be linked.",
|
|
52108
52174
|
inputSchema: {
|
|
52109
52175
|
id: looseNoteId(external_exports.string()).optional().describe(`Note ID (preferred - more reliable than title): ${NOTE_ID_FORMS}`),
|
|
52110
52176
|
title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
|
|
@@ -52130,7 +52196,7 @@ registerTool(
|
|
|
52130
52196
|
const url2 = notesManager.getNoteLinkById(id2);
|
|
52131
52197
|
if (!url2) {
|
|
52132
52198
|
return errorResponse(
|
|
52133
|
-
`Failed to get note link for "${note2.title}". The Notes database may not be accessible \u2014 grant Full Disk Access to the
|
|
52199
|
+
`Failed to get note link for "${note2.title}". The Notes database may not be accessible \u2014 grant Full Disk Access to the Node binary running the server (or the terminal that launches it), fully quit and relaunch, then run the doctor tool. See: ${FULL_DISK_ACCESS_GUIDE_URL}. (On macOS 12\u201315 this also falls back to the AppleScript note link property.)`
|
|
52134
52200
|
);
|
|
52135
52201
|
}
|
|
52136
52202
|
return successResponse(`Note link: ${url2}`, { id: id2, title: note2.title, url: url2 });
|
|
@@ -52150,7 +52216,7 @@ registerTool(
|
|
|
52150
52216
|
const url = notesManager.getNoteLink(title, account);
|
|
52151
52217
|
if (!url) {
|
|
52152
52218
|
return errorResponse(
|
|
52153
|
-
`Failed to get note link for "${title}". The Notes database may not be accessible \u2014 grant Full Disk Access to the
|
|
52219
|
+
`Failed to get note link for "${title}". The Notes database may not be accessible \u2014 grant Full Disk Access to the Node binary running the server (or the terminal that launches it), fully quit and relaunch, then run the doctor tool. See: ${FULL_DISK_ACCESS_GUIDE_URL}. (On macOS 12\u201315 this also falls back to the AppleScript note link property.)`
|
|
52154
52220
|
);
|
|
52155
52221
|
}
|
|
52156
52222
|
return successResponse(`Note link: ${url}`, { title, url });
|
|
@@ -52323,7 +52389,7 @@ registerTool(
|
|
|
52323
52389
|
page = pageNoteBlocks(readNoteBlocks(id2), { offset, limit });
|
|
52324
52390
|
} catch (error2) {
|
|
52325
52391
|
if (!(error2 instanceof NoteBlocksError)) throw error2;
|
|
52326
|
-
const hint = error2.code === "no-full-disk-access" ? ` Grant Full Disk Access to the
|
|
52392
|
+
const hint = error2.code === "no-full-disk-access" ? ` Grant Full Disk Access to the Node binary running this server (run the doctor tool for its path): ${FULL_DISK_ACCESS_GUIDE_URL}` : "";
|
|
52327
52393
|
return errorResponse(`Error reading note blocks [${error2.code}]: ${error2.message}${hint}`);
|
|
52328
52394
|
}
|
|
52329
52395
|
const { summary } = page;
|
|
@@ -53207,7 +53273,7 @@ registerTool(
|
|
|
53207
53273
|
return ` ${icon} ${c.name}: ${c.message}`;
|
|
53208
53274
|
}).join("\n");
|
|
53209
53275
|
const fdaAvailable = hasFullDiskAccess();
|
|
53210
|
-
const fdaLine = fdaAvailable ? " \u2713 full_disk_access: Granted (Notes database readable \u2014 checklist state, note metadata, note links, sync detail)" :
|
|
53276
|
+
const fdaLine = fdaAvailable ? " \u2713 full_disk_access: Granted (Notes database readable \u2014 checklist state, note metadata, note links, sync detail)" : " \u24D8 full_disk_access: Not granted \u2014 get-checklist-state, get-note-metadata, and the checklist annotations in get-note-markdown won't work; get-note-link fails on macOS 26+ (macOS 12-15 falls back to AppleScript); get-sync-status cannot see pending uploads. The rest of the server is pure AppleScript and is unaffected. " + fdaRemediation();
|
|
53211
53277
|
return successResponse(`${statusIcon} ${statusText}
|
|
53212
53278
|
|
|
53213
53279
|
${checkLines}
|
|
@@ -53887,7 +53953,7 @@ registerTool(
|
|
|
53887
53953
|
});
|
|
53888
53954
|
} catch (error2) {
|
|
53889
53955
|
if (!(error2 instanceof NotesExportError || error2 instanceof NoteBlocksError)) throw error2;
|
|
53890
|
-
const hint = error2.code === "no-full-disk-access" ? ` Grant Full Disk Access to the
|
|
53956
|
+
const hint = error2.code === "no-full-disk-access" ? ` Grant Full Disk Access to the Node binary running this server (run the doctor tool for its path): ${FULL_DISK_ACCESS_GUIDE_URL}` : "";
|
|
53891
53957
|
return errorResponse(`Error exporting Markdown [${error2.code}]: ${error2.message}${hint}`);
|
|
53892
53958
|
}
|
|
53893
53959
|
const skipped = receipt.skipped.length ? `; skipped ${receipt.skipped.length}` : "";
|
|
@@ -53939,7 +54005,7 @@ registerTool(
|
|
|
53939
54005
|
});
|
|
53940
54006
|
} catch (error2) {
|
|
53941
54007
|
if (!(error2 instanceof NotesExportError || error2 instanceof NoteBlocksError)) throw error2;
|
|
53942
|
-
const hint = error2.code === "no-full-disk-access" ? ` Grant Full Disk Access to the
|
|
54008
|
+
const hint = error2.code === "no-full-disk-access" ? ` Grant Full Disk Access to the Node binary running this server (run the doctor tool for its path): ${FULL_DISK_ACCESS_GUIDE_URL}` : "";
|
|
53943
54009
|
return errorResponse(`Error exporting HTML [${error2.code}]: ${error2.message}${hint}`);
|
|
53944
54010
|
}
|
|
53945
54011
|
const assets = receipt.assets ? `; copied ${receipt.assets.files} asset file(s) to ${receipt.assets.dir}` : `; embedded ${receipt.embedded ?? 0} asset(s)`;
|
package/docs/FULL-DISK-ACCESS.md
CHANGED
|
@@ -37,7 +37,7 @@ That database lives in a macOS-protected directory:
|
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Reading anything under `~/Library/Group Containers/` requires **Full Disk
|
|
40
|
-
Access** for the
|
|
40
|
+
Access** for the process that runs the server — without it, macOS denies the read. (The MCP only
|
|
41
41
|
ever **reads** this database; it never writes to it.)
|
|
42
42
|
|
|
43
43
|
## How to grant Full Disk Access
|
|
@@ -45,21 +45,47 @@ ever **reads** this database; it never writes to it.)
|
|
|
45
45
|
1. Open **System Settings** (or **System Preferences** on older macOS).
|
|
46
46
|
2. Go to **Privacy & Security → Full Disk Access**.
|
|
47
47
|
3. Click the **+** button (you may need to unlock with Touch ID / your password
|
|
48
|
-
first)
|
|
49
|
-
that
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
first) and add the right entry for how you run the server:
|
|
49
|
+
- **Claude Desktop** → the **Node binary** that runs the server, e.g.
|
|
50
|
+
`/usr/local/bin/node` or `~/.nvm/versions/node/v24.11.1/bin/node`. The
|
|
51
|
+
`doctor` tool prints the exact path. In the file picker, press **⌘⇧G** and
|
|
52
|
+
paste it (use **⌘⇧.** to show hidden folders such as `~/.nvm`). Adding
|
|
53
|
+
`/Applications/Claude.app` as well does no harm, but on its own it is not
|
|
54
|
+
enough (see below).
|
|
55
|
+
- **Claude Code or another client in a terminal** → the terminal app:
|
|
56
|
+
`/Applications/Utilities/Terminal.app` or `/Applications/iTerm.app`
|
|
53
57
|
- **VS Code** → `/Applications/Visual Studio Code.app`
|
|
54
|
-
4. Make sure the toggle next to the
|
|
58
|
+
4. Make sure the toggle next to the entry is **on**.
|
|
55
59
|
5. **Fully quit and reopen the host app.** macOS only applies the new permission
|
|
56
|
-
to processes started *after* the change
|
|
57
|
-
enough; the host application itself
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
>
|
|
61
|
-
>
|
|
62
|
-
>
|
|
60
|
+
to processes started *after* the change. A reload or restart-server is not
|
|
61
|
+
enough; quit the host application itself (⌘Q) and relaunch it. If `doctor`
|
|
62
|
+
still reports Full Disk Access as not granted after that, restart the Mac.
|
|
63
|
+
|
|
64
|
+
> **Why Claude Desktop needs the Node binary itself.** macOS checks Full Disk
|
|
65
|
+
> Access against the *responsible process*, the app it holds accountable for a
|
|
66
|
+
> request. A terminal passes that role on to everything it launches, so a grant
|
|
67
|
+
> on Terminal or iTerm covers the server. Claude Desktop does not: it starts each
|
|
68
|
+
> MCP server through a helper that *disclaims* responsibility, which makes the
|
|
69
|
+
> `node` process its own responsible process. macOS then looks for a grant on the
|
|
70
|
+
> Node binary's path and ignores the one on Claude.app
|
|
71
|
+
> ([#220](https://github.com/sweetrb/apple-notes-mcp/issues/220)).
|
|
72
|
+
>
|
|
73
|
+
> The grant is tied to that exact path. With a version manager (nvm, fnm, Volta,
|
|
74
|
+
> asdf, mise) the path changes with every Node version, so switching versions
|
|
75
|
+
> drops the grant, and `npx` runs whichever `node` is first on the `PATH` Claude
|
|
76
|
+
> Desktop sees. Pointing the config at one fixed Node binary keeps the grant
|
|
77
|
+
> stable:
|
|
78
|
+
>
|
|
79
|
+
> ```json
|
|
80
|
+
> "apple-notes": {
|
|
81
|
+
> "command": "/Users/you/.nvm/versions/node/v24.11.1/bin/node",
|
|
82
|
+
> "args": ["/path/to/global/node_modules/apple-notes-mcp/build/index.js"]
|
|
83
|
+
> }
|
|
84
|
+
> ```
|
|
85
|
+
>
|
|
86
|
+
> A Developer-ID-signed Node keeps its grant across updates at the same path;
|
|
87
|
+
> an ad-hoc-signed one (typically Homebrew's) does not. See
|
|
88
|
+
> [Node runtime and TCC permissions](NODE-RUNTIME-AND-TCC-PERMISSIONS.md).
|
|
63
89
|
|
|
64
90
|
## Verifying it worked
|
|
65
91
|
|
package/package.json
CHANGED