apple-notes-mcp 2.15.6 → 2.15.7
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 +41 -8
- package/build/index.js +80 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1376,21 +1376,28 @@ actual destination folder ID matches the requested folder.
|
|
|
1376
1376
|
|
|
1377
1377
|
#### Folder scope guards
|
|
1378
1378
|
|
|
1379
|
-
`update-note`, `append-to-note`, `delete-note`,
|
|
1380
|
-
optional folder preconditions. Use exact
|
|
1379
|
+
`update-note`, `append-to-note`, `delete-note`, `move-note`, and
|
|
1380
|
+
`batch-move-notes` accept three optional folder preconditions. Use exact
|
|
1381
|
+
folder ids from `list-folders`.
|
|
1381
1382
|
|
|
1382
1383
|
- `ifFolderId`: the note must currently be in exactly this folder.
|
|
1383
1384
|
- `ifAncestorFolderId`: the note must be inside this folder or any of its
|
|
1384
1385
|
subfolders.
|
|
1385
1386
|
- `forbiddenAncestorFolderIds` (up to 50): the note must not be inside any of
|
|
1386
|
-
these folders or their subfolders. For `move-note
|
|
1387
|
-
be either.
|
|
1387
|
+
these folders or their subfolders. For `move-note` and `batch-move-notes`,
|
|
1388
|
+
the destination must not be either.
|
|
1388
1389
|
|
|
1389
1390
|
The checks read Notes.app's live folders inside the same AppleScript as the
|
|
1390
|
-
write, immediately before it
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1391
|
+
write, immediately before it. If the folder observed by the check fails a
|
|
1392
|
+
precondition, the write is refused with `Scope guard failed: …`. The check and
|
|
1393
|
+
write are separate Notes operations; another actor can move the note between
|
|
1394
|
+
them. A native append to a protected note runs through Shortcuts, so its
|
|
1395
|
+
precheck runs in a separate AppleScript before the append.
|
|
1396
|
+
|
|
1397
|
+
For `batch-move-notes`, the same preconditions apply to every supplied note ID.
|
|
1398
|
+
Each note is checked independently immediately before its move. A scope mismatch
|
|
1399
|
+
leaves that note unchanged and appears in its failure result; the remaining
|
|
1400
|
+
items continue. A batch can therefore move some notes and refuse others.
|
|
1394
1401
|
|
|
1395
1402
|
**Example - retire a note only while it is still in the inbox:**
|
|
1396
1403
|
```json
|
|
@@ -1915,9 +1922,35 @@ Moves multiple notes to a folder.
|
|
|
1915
1922
|
| `ids` | string[] | Yes | Array of note IDs to move (max 500 per request) |
|
|
1916
1923
|
| `folder` | string | Yes | Destination folder name or nested path (e.g., `"Work/Clients"`). Must already exist — create it with [`create-folder`](#create-folder). A smart folder is refused for the whole call before any note moves |
|
|
1917
1924
|
| `account` | string | No | Account containing the folder |
|
|
1925
|
+
| `ifFolderId`, `ifAncestorFolderId`, `forbiddenAncestorFolderIds` | string, string, string[] | No | Shared folder preconditions applied independently to each note; see [Folder scope guards](#folder-scope-guards). Forbidden folders also apply to the destination |
|
|
1918
1926
|
|
|
1919
1927
|
**Returns:** Summary of successes and failures. Each success is reported only
|
|
1920
1928
|
after the note's actual container folder ID matches the destination folder ID.
|
|
1929
|
+
If every note fails, the call returns its existing error response; the exact
|
|
1930
|
+
note IDs and reasons remain in the failure text.
|
|
1931
|
+
|
|
1932
|
+
Failure rows carry `committed: false, indeterminate: false` only when that
|
|
1933
|
+
item was refused before its move. Move/readback failures and an unknown
|
|
1934
|
+
whole-script outcome carry `indeterminate: true`, with `committed` omitted.
|
|
1935
|
+
If every item fails, the batch reports `committed: false` only when every row
|
|
1936
|
+
is a proven pre-move refusal. Otherwise its error result is indeterminate;
|
|
1937
|
+
read each exact note ID before retrying, even if another row has a scope refusal.
|
|
1938
|
+
|
|
1939
|
+
**Example - move only notes that are still in the reviewed inbox:**
|
|
1940
|
+
```json
|
|
1941
|
+
{
|
|
1942
|
+
"ids": ["x-coredata://ABC123/ICNote/p456", "x-coredata://ABC123/ICNote/p457"],
|
|
1943
|
+
"folder": "Archive",
|
|
1944
|
+
"ifFolderId": "x-coredata://ABC123/ICFolder/p12"
|
|
1945
|
+
}
|
|
1946
|
+
```
|
|
1947
|
+
|
|
1948
|
+
The source-folder check runs immediately before each move in the same
|
|
1949
|
+
AppleScript. If the check finds that a note is no longer in that inbox, its
|
|
1950
|
+
result reports `Scope guard failed: …` while a note still there can move.
|
|
1951
|
+
The check does not lock the note against another actor moving it before the
|
|
1952
|
+
move. The batch is not a transaction, and successful moves are not rolled
|
|
1953
|
+
back after a refusal.
|
|
1921
1954
|
|
|
1922
1955
|
---
|
|
1923
1956
|
|
package/build/index.js
CHANGED
|
@@ -43149,8 +43149,9 @@ function parseScopeFailure(output) {
|
|
|
43149
43149
|
const trimmed = output.trim();
|
|
43150
43150
|
return trimmed.startsWith(`${SCOPE_MARKER}:`) ? trimmed.slice(SCOPE_MARKER.length + 1) : null;
|
|
43151
43151
|
}
|
|
43152
|
-
function scopeConflictMessage(reason) {
|
|
43153
|
-
|
|
43152
|
+
function scopeConflictMessage(reason, operation) {
|
|
43153
|
+
const refusal = operation === "batch-move" ? "This note's move was refused" : "Nothing was changed";
|
|
43154
|
+
return `Scope guard failed: ${reason}. ${refusal}; read the note's current folder and review before retrying.`;
|
|
43154
43155
|
}
|
|
43155
43156
|
|
|
43156
43157
|
// src/services/appleNotesManager.ts
|
|
@@ -43499,8 +43500,9 @@ function buildAccountScopedScript(scope2, command2) {
|
|
|
43499
43500
|
end tell
|
|
43500
43501
|
`;
|
|
43501
43502
|
}
|
|
43502
|
-
function buildAppLevelScript(command2) {
|
|
43503
|
+
function buildAppLevelScript(command2, handlers = "") {
|
|
43503
43504
|
return `
|
|
43505
|
+
${handlers}
|
|
43504
43506
|
tell application "Notes"
|
|
43505
43507
|
${command2}
|
|
43506
43508
|
end tell
|
|
@@ -45881,8 +45883,8 @@ var AppleNotesManager = class {
|
|
|
45881
45883
|
/**
|
|
45882
45884
|
* Result of a batch operation on a single item.
|
|
45883
45885
|
*/
|
|
45884
|
-
createBatchResult(id2, success, error2) {
|
|
45885
|
-
return error2 ? { id: id2, success, error: error2 } : { id: id2, success };
|
|
45886
|
+
createBatchResult(id2, success, error2, certainty) {
|
|
45887
|
+
return { ...error2 ? { id: id2, success, error: error2 } : { id: id2, success }, ...certainty };
|
|
45886
45888
|
}
|
|
45887
45889
|
/**
|
|
45888
45890
|
* Maps a per-item status token emitted by a batch AppleScript loop to a
|
|
@@ -45890,23 +45892,39 @@ var AppleNotesManager = class {
|
|
|
45890
45892
|
* per-note implementation. See {@link batchMoveNotes}.
|
|
45891
45893
|
*/
|
|
45892
45894
|
mapBatchStatus(id2, status, op) {
|
|
45895
|
+
const beforeMove = op === "move" ? { committed: false, indeterminate: false } : void 0;
|
|
45896
|
+
const uncertainMove = op === "move" ? { indeterminate: true } : void 0;
|
|
45897
|
+
const scopeFailure = parseScopeFailure(status ?? "");
|
|
45898
|
+
if (scopeFailure !== null)
|
|
45899
|
+
return this.createBatchResult(
|
|
45900
|
+
id2,
|
|
45901
|
+
false,
|
|
45902
|
+
scopeConflictMessage(scopeFailure, op === "move" ? "batch-move" : void 0),
|
|
45903
|
+
beforeMove
|
|
45904
|
+
);
|
|
45893
45905
|
switch (status) {
|
|
45894
45906
|
case "ok":
|
|
45895
45907
|
return this.createBatchResult(id2, true);
|
|
45896
45908
|
case "pw":
|
|
45897
|
-
return this.createBatchResult(id2, false, "Note is password-protected");
|
|
45909
|
+
return this.createBatchResult(id2, false, "Note is password-protected", beforeMove);
|
|
45898
45910
|
case "missing":
|
|
45899
|
-
return this.createBatchResult(id2, false, "Note not found");
|
|
45911
|
+
return this.createBatchResult(id2, false, "Note not found", beforeMove);
|
|
45900
45912
|
case "fail":
|
|
45901
45913
|
return this.createBatchResult(
|
|
45902
45914
|
id2,
|
|
45903
45915
|
false,
|
|
45904
|
-
op === "delete" ? "Deletion failed" : "Move failed"
|
|
45916
|
+
op === "delete" ? "Deletion failed" : "Move failed",
|
|
45917
|
+
uncertainMove
|
|
45905
45918
|
);
|
|
45906
45919
|
case "wrongfolder":
|
|
45907
|
-
return this.createBatchResult(
|
|
45920
|
+
return this.createBatchResult(
|
|
45921
|
+
id2,
|
|
45922
|
+
false,
|
|
45923
|
+
"Destination folder verification failed",
|
|
45924
|
+
uncertainMove
|
|
45925
|
+
);
|
|
45908
45926
|
default:
|
|
45909
|
-
return this.createBatchResult(id2, false, "Unknown error");
|
|
45927
|
+
return this.createBatchResult(id2, false, "Unknown error", uncertainMove);
|
|
45910
45928
|
}
|
|
45911
45929
|
}
|
|
45912
45930
|
/**
|
|
@@ -45918,6 +45936,7 @@ var AppleNotesManager = class {
|
|
|
45918
45936
|
* @param ids - Array of CoreData URL identifiers for notes to move
|
|
45919
45937
|
* @param folder - Destination folder name
|
|
45920
45938
|
* @param account - Account containing the folder (defaults to Notes.app's default account)
|
|
45939
|
+
* @param scope - Shared folder preconditions checked independently before each move
|
|
45921
45940
|
* @returns Array of results with id, success status, and optional error message
|
|
45922
45941
|
*
|
|
45923
45942
|
* @example
|
|
@@ -45928,8 +45947,26 @@ var AppleNotesManager = class {
|
|
|
45928
45947
|
* );
|
|
45929
45948
|
* ```
|
|
45930
45949
|
*/
|
|
45931
|
-
batchMoveNotes(ids, folder, account) {
|
|
45950
|
+
batchMoveNotes(ids, folder, account, scope2) {
|
|
45932
45951
|
if (ids.length === 0) return [];
|
|
45952
|
+
const scopeChecks = buildScopeGuardScript("noteRef", scope2, "destFolder");
|
|
45953
|
+
const moveHandler = `
|
|
45954
|
+
script batchMoveWorker
|
|
45955
|
+
on moveBatchNote(noteRef, destFolder, theId)
|
|
45956
|
+
tell application "Notes"
|
|
45957
|
+
${scopeChecks}
|
|
45958
|
+
move noteRef to destFolder
|
|
45959
|
+
set movedNoteRef to note id theId
|
|
45960
|
+
set actualFolder to container of movedNoteRef
|
|
45961
|
+
if (id of actualFolder) is (id of destFolder) then
|
|
45962
|
+
return "ok"
|
|
45963
|
+
else
|
|
45964
|
+
return "wrongfolder"
|
|
45965
|
+
end if
|
|
45966
|
+
end tell
|
|
45967
|
+
end moveBatchNote
|
|
45968
|
+
end script
|
|
45969
|
+
`;
|
|
45933
45970
|
const targetAccount = this.resolveAccount(account);
|
|
45934
45971
|
const destFolderSetup = buildLiveFolderResolution(folder, "destFolder", {
|
|
45935
45972
|
excludeFolderIds: this.smartFolderIds()
|
|
@@ -45943,13 +45980,15 @@ var AppleNotesManager = class {
|
|
|
45943
45980
|
results[i] = this.createBatchResult(
|
|
45944
45981
|
id2,
|
|
45945
45982
|
false,
|
|
45946
|
-
e instanceof Error ? e.message : "Invalid note ID"
|
|
45983
|
+
e instanceof Error ? e.message : "Invalid note ID",
|
|
45984
|
+
{ committed: false, indeterminate: false }
|
|
45947
45985
|
);
|
|
45948
45986
|
}
|
|
45949
45987
|
});
|
|
45950
45988
|
if (runnable.length > 0) {
|
|
45951
45989
|
const idList2 = runnable.map((r) => `"${r.safe}"`).join(", ");
|
|
45952
|
-
const script = buildAppLevelScript(
|
|
45990
|
+
const script = buildAppLevelScript(
|
|
45991
|
+
`
|
|
45953
45992
|
${buildAccountResolution(targetAccount)}
|
|
45954
45993
|
${destFolderSetup}
|
|
45955
45994
|
set out to ""
|
|
@@ -45970,14 +46009,8 @@ var AppleNotesManager = class {
|
|
|
45970
46009
|
set out to out & "pw" & ${AS_RECORD_SEP}
|
|
45971
46010
|
else
|
|
45972
46011
|
try
|
|
45973
|
-
|
|
45974
|
-
set
|
|
45975
|
-
set actualFolder to container of movedNoteRef
|
|
45976
|
-
if (id of actualFolder) is (id of destFolder) then
|
|
45977
|
-
set out to out & "ok" & ${AS_RECORD_SEP}
|
|
45978
|
-
else
|
|
45979
|
-
set out to out & "wrongfolder" & ${AS_RECORD_SEP}
|
|
45980
|
-
end if
|
|
46012
|
+
set itemStatus to batchMoveWorker's moveBatchNote(noteRef, destFolder, theId)
|
|
46013
|
+
set out to out & itemStatus & ${AS_RECORD_SEP}
|
|
45981
46014
|
on error
|
|
45982
46015
|
set out to out & "fail" & ${AS_RECORD_SEP}
|
|
45983
46016
|
end try
|
|
@@ -45985,7 +46018,9 @@ var AppleNotesManager = class {
|
|
|
45985
46018
|
end if
|
|
45986
46019
|
end repeat
|
|
45987
46020
|
return out
|
|
45988
|
-
|
|
46021
|
+
`,
|
|
46022
|
+
moveHandler
|
|
46023
|
+
);
|
|
45989
46024
|
const res = executeMutationAppleScript(script);
|
|
45990
46025
|
if (!res.success) {
|
|
45991
46026
|
throwIfAccountResolutionFailed(res.error);
|
|
@@ -45994,11 +46029,12 @@ var AppleNotesManager = class {
|
|
|
45994
46029
|
results[r.index] = this.createBatchResult(
|
|
45995
46030
|
ids[r.index],
|
|
45996
46031
|
false,
|
|
45997
|
-
res.error ?? "Batch move failed"
|
|
46032
|
+
res.error ?? "Batch move failed",
|
|
46033
|
+
{ indeterminate: true }
|
|
45998
46034
|
);
|
|
45999
46035
|
}
|
|
46000
46036
|
} else {
|
|
46001
|
-
const statuses = res.output.split(RECORD_SEP).map((s) => s.trim())
|
|
46037
|
+
const statuses = res.output.split(RECORD_SEP).map((s) => s.trim());
|
|
46002
46038
|
runnable.forEach((r, k) => {
|
|
46003
46039
|
results[r.index] = this.mapBatchStatus(ids[r.index], statuses[k], "move");
|
|
46004
46040
|
});
|
|
@@ -64232,7 +64268,7 @@ var scopeGuardInputs = {
|
|
|
64232
64268
|
maxItems: MAX_FORBIDDEN_FOLDERS
|
|
64233
64269
|
}
|
|
64234
64270
|
).optional().describe(
|
|
64235
|
-
"Precondition: the note must not be inside any of these folders or their subfolders (for move-note, neither may the destination). Re-checked immediately before the write."
|
|
64271
|
+
"Precondition: the note must not be inside any of these folders or their subfolders (for move-note and batch-move-notes, neither may the destination). Re-checked immediately before the write."
|
|
64236
64272
|
)
|
|
64237
64273
|
};
|
|
64238
64274
|
function scopeFrom(args) {
|
|
@@ -66791,8 +66827,9 @@ registerTool(
|
|
|
66791
66827
|
registerTool(
|
|
66792
66828
|
"batch-move-notes",
|
|
66793
66829
|
{
|
|
66794
|
-
description: "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts after destination-folder verification.\nDo not use when: moving a single note (move-note).\nSafety:
|
|
66830
|
+
description: "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts after destination-folder verification.\nDo not use when: moving a single note (move-note).\nSafety: optional folder preconditions apply independently to every note, checked live immediately before its move in the same AppleScript; a scope mismatch leaves that note unchanged and other items continue. Each moved note's actual container ID is compared with the destination folder ID before success is reported. The destination folder must already exist (create-folder); a smart folder is refused for the whole call (code unsupported) before any note moves.",
|
|
66795
66831
|
inputSchema: {
|
|
66832
|
+
...scopeGuardInputs,
|
|
66796
66833
|
ids: noteIdArrayInput.describe(
|
|
66797
66834
|
`Array of note IDs to move (max ${MAX.BATCH_IDS} per request)`
|
|
66798
66835
|
),
|
|
@@ -66811,11 +66848,12 @@ registerTool(
|
|
|
66811
66848
|
results: external_exports.array(external_exports.object({}).passthrough()).optional()
|
|
66812
66849
|
}
|
|
66813
66850
|
},
|
|
66814
|
-
withErrorHandling((
|
|
66851
|
+
withErrorHandling((args) => {
|
|
66852
|
+
const { ids, folder, account } = args;
|
|
66815
66853
|
if (ids.length === 0) {
|
|
66816
66854
|
return errorResponse("No note IDs provided");
|
|
66817
66855
|
}
|
|
66818
|
-
const results = notesManager.batchMoveNotes(ids, folder, account);
|
|
66856
|
+
const results = notesManager.batchMoveNotes(ids, folder, account, scopeFrom(args));
|
|
66819
66857
|
const succeeded = results.filter((r) => r.success).length;
|
|
66820
66858
|
const failed = results.filter((r) => !r.success).length;
|
|
66821
66859
|
const lines = [`Batch move to "${folder}": ${succeeded} succeeded, ${failed} failed`];
|
|
@@ -66825,13 +66863,20 @@ registerTool(
|
|
|
66825
66863
|
lines.push(` - ${result.id}: ${result.error}`);
|
|
66826
66864
|
}
|
|
66827
66865
|
}
|
|
66828
|
-
|
|
66829
|
-
|
|
66830
|
-
|
|
66831
|
-
|
|
66832
|
-
|
|
66833
|
-
|
|
66834
|
-
|
|
66866
|
+
if (succeeded > 0)
|
|
66867
|
+
return successResponse(lines.join("\n"), {
|
|
66868
|
+
ok: failed === 0,
|
|
66869
|
+
folder,
|
|
66870
|
+
succeeded,
|
|
66871
|
+
failed,
|
|
66872
|
+
results
|
|
66873
|
+
});
|
|
66874
|
+
const allRefusedBeforeMove = results.length === ids.length && results.every((result) => result.committed === false && result.indeterminate === false);
|
|
66875
|
+
if (!allRefusedBeforeMove)
|
|
66876
|
+
lines.push("\nBatch move outcome is uncertain; read each exact note ID before retrying.");
|
|
66877
|
+
const message = lines.join("\n");
|
|
66878
|
+
const envelope = allRefusedBeforeMove ? { ...classifyError(message), committed: false, indeterminate: false } : { code: "verification_failed", indeterminate: true };
|
|
66879
|
+
return errorResponse(message, new CodedError(message, envelope));
|
|
66835
66880
|
}, "Error performing batch move")
|
|
66836
66881
|
);
|
|
66837
66882
|
registerTool(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-notes-mcp",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.7",
|
|
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",
|