apple-notes-mcp 2.9.34 → 2.10.0
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 -4
- package/build/index.js +47 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -521,7 +521,7 @@ can match titles and bodies together.
|
|
|
521
521
|
|-----------|------|----------|-------------|
|
|
522
522
|
| `query` | string | Yes | Query expression (syntax below), at most 2000 characters |
|
|
523
523
|
| `limit` | number | No | Maximum notes to return. Defaults to 50, maximum 500. The response reports the total match count. |
|
|
524
|
-
| `scanLimit` | number | No | How many of the most recently modified notes to examine. Defaults to 500, maximum
|
|
524
|
+
| `scanLimit` | number | No | How many of the most recently modified notes to examine. Defaults to 500, maximum 10000. The response says when older notes were left unscanned. A large scan decodes more bodies, so it takes longer. |
|
|
525
525
|
| `includeDeleted` | boolean | No | Also scan notes in Recently Deleted, notes pending deletion, and folderless notes. Defaults to `false`. |
|
|
526
526
|
| `includeWordCount` | boolean | No | Add `wordCount` to each returned note, the same count `words:` filters on (`null` when locked or unreadable). Free when the query already reads bodies; a metadata-only query (for example `pinned`) reads just the returned notes' bodies in one extra read-only query. Defaults to `false`. |
|
|
527
527
|
|
|
@@ -534,9 +534,9 @@ can match titles and bodies together.
|
|
|
534
534
|
| `folder:Work`, `folder:"Work/Clients"` | The note's own folder, by name or full path, case-insensitive (notes in subfolders are not included); a literal `/` in a name can be written `\/` as in `list-folders` |
|
|
535
535
|
| `account:iCloud` | Account name, case-insensitive |
|
|
536
536
|
| `tag:finance` | Native Notes tag (with or without `#`); textual hashtags are ordinary words |
|
|
537
|
-
| `has:link`, `has:attachment`, `has:checklist`, `has:drawing`, `has:image`, `has:video`, `has:audio`, `has:pdf`, `has:table`, `has:scan`, `has:tag` | The note body contains that kind of object |
|
|
537
|
+
| `has:link`, `has:attachment`, `has:checklist`, `has:drawing`, `has:image`, `has:video`, `has:audio`, `has:pdf`, `has:table`, `has:scan`, `has:url`, `has:map`, `has:tag` | The note body contains that kind of object. `has:url` is a link preview card (it also counts as `has:link`, as inline links do); `has:map` is a map attachment |
|
|
538
538
|
| `checklist:open`, `checklist:done` | At least one unchecked item; or items present and all checked |
|
|
539
|
-
| `pinned`, `locked`, `shared` (or `is:pinned` …) | Note flags; `shared` includes notes in a shared folder |
|
|
539
|
+
| `pinned`, `locked`, `shared`, `quicknote` (or `is:pinned` …) | Note flags; `shared` includes notes in a shared folder, and `quicknote` is a note created as a Quick Note (the same flag `list-special-notes kind=quick-notes` reads) |
|
|
540
540
|
| `words:>250` | Word count, with `=`, `>`, `>=`, `<`, `<=` |
|
|
541
541
|
| `created:>=2026-07-01`, `modified:<2026-09-01` | Dates as `YYYY-MM-DD` in local time, with the same operators; `=` means that whole day |
|
|
542
542
|
| `a b`, `a AND b`, `a OR b`, `NOT a`, `-a`, `( … )` | AND is implicit and binds tighter than OR |
|
|
@@ -2097,7 +2097,10 @@ On macOS 27, Notes' AppleScript does not list PDF attachments, so it cannot see
|
|
|
2097
2097
|
a PDF this tool just added. When that happens and the server has Full Disk
|
|
2098
2098
|
Access, it verifies through the read-only NoteStore database instead: success
|
|
2099
2099
|
requires exactly one new attachment row on the note whose media file matches
|
|
2100
|
-
the source bytes,
|
|
2100
|
+
the source bytes, whose type fits the file's extension, and, when the decoded
|
|
2101
|
+
note body lists attachments, that the body references. The check repeats for
|
|
2102
|
+
about two seconds while Notes' database catches up, and `contentHash` is read
|
|
2103
|
+
after it finishes. The result carries `verifiedBy: "database"`. Without Full
|
|
2101
2104
|
Disk Access a PDF attach reports "insertion outcome uncertain"; read the note
|
|
2102
2105
|
before retrying, because the attachment was probably created.
|
|
2103
2106
|
|
package/build/index.js
CHANGED
|
@@ -47162,9 +47162,11 @@ var FACETS = [
|
|
|
47162
47162
|
"pdf",
|
|
47163
47163
|
"table",
|
|
47164
47164
|
"scan",
|
|
47165
|
+
"url",
|
|
47166
|
+
"map",
|
|
47165
47167
|
"tag"
|
|
47166
47168
|
];
|
|
47167
|
-
var FLAGS = ["pinned", "locked", "shared"];
|
|
47169
|
+
var FLAGS = ["pinned", "locked", "shared", "quicknote"];
|
|
47168
47170
|
var NoteQueryError = class extends Error {
|
|
47169
47171
|
constructor(message, position) {
|
|
47170
47172
|
super(position === void 0 ? message : `${message} (at position ${position + 1})`);
|
|
@@ -47649,7 +47651,7 @@ var NOTES_DB_PATH10 = path8.join(
|
|
|
47649
47651
|
os8.homedir(),
|
|
47650
47652
|
"Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
|
|
47651
47653
|
);
|
|
47652
|
-
var QUERY_SCAN = { DEFAULT: 500, MAX:
|
|
47654
|
+
var QUERY_SCAN = { DEFAULT: 500, MAX: 1e4 };
|
|
47653
47655
|
var QUERY_RESULTS = { DEFAULT: 50, MAX: 500 };
|
|
47654
47656
|
var CORE_DATA_EPOCH_MS2 = Date.UTC(2001, 0, 1);
|
|
47655
47657
|
var SNIPPET_BEFORE = 60;
|
|
@@ -47670,7 +47672,8 @@ function facetsForAttachmentType(uti) {
|
|
|
47670
47672
|
if (type.startsWith("com.apple.notes.inlinetextattachment.")) return [];
|
|
47671
47673
|
if (type === "com.apple.notes.table") return ["table"];
|
|
47672
47674
|
const facets = ["attachment"];
|
|
47673
|
-
if (type === "public.url") facets.push("link");
|
|
47675
|
+
if (type === "public.url") facets.push("link", "url");
|
|
47676
|
+
else if (type === "com.apple.mapkit.map") facets.push("map");
|
|
47674
47677
|
else if (type === "com.apple.paper.doc.scan" || type === "com.apple.notes.gallery")
|
|
47675
47678
|
facets.push("scan");
|
|
47676
47679
|
else if (type === "com.adobe.pdf" || type === "com.apple.paper.doc.pdf") facets.push("pdf");
|
|
@@ -47771,7 +47774,7 @@ function buildScanSql(available, options) {
|
|
|
47771
47774
|
const folderRow = `SELECT json_object('k', 'folder', 'pk', f.Z_PK, 'name', ${col2(available, "f", "ZTITLE2")}, 'parent', ${col2(available, "f", "ZPARENT")}, 'type', ${col2(available, "f", "ZFOLDERTYPE")}, 'owner', ${col2(available, "f", "ZOWNER")}, 'shared', ${notNull("f", "ZSERVERSHAREDATA")}) FROM ZICCLOUDSYNCINGOBJECT f WHERE f.Z_ENT = ${entity2("ICFolder")};`;
|
|
47772
47775
|
const accountRow = `SELECT json_object('k', 'account', 'pk', a.Z_PK, 'name', ${col2(available, "a", "ZNAME")}) FROM ZICCLOUDSYNCINGOBJECT a WHERE a.Z_ENT = ${entity2("ICAccount")};`;
|
|
47773
47776
|
const dataExpr = options.withBodies ? "(SELECT hex(d.ZDATA) FROM ZICNOTEDATA d WHERE d.ZNOTE = n.Z_PK ORDER BY d.Z_PK DESC LIMIT 1)" : "NULL";
|
|
47774
|
-
const noteRow = `SELECT json_object('k', 'note', 'pk', n.Z_PK, 'title', n.ZTITLE1, 'folder', n.ZFOLDER, 'created', ${createdExpr}, 'modified', n.ZMODIFICATIONDATE1, 'pinned', ${bool("ZISPINNED")}, 'locked', ${bool("ZISPASSWORDPROTECTED")}, 'shared', ${notNull("n", "ZSERVERSHAREDATA")}, 'snippet', ${col2(available, "n", "ZSNIPPET")}, 'data', ${dataExpr}, 'tags', ${tagsExpr}) FROM ZICCLOUDSYNCINGOBJECT n WHERE ${whereSql} ORDER BY n.ZMODIFICATIONDATE1 DESC, n.Z_PK DESC LIMIT ${scan};`;
|
|
47777
|
+
const noteRow = `SELECT json_object('k', 'note', 'pk', n.Z_PK, 'title', n.ZTITLE1, 'folder', n.ZFOLDER, 'created', ${createdExpr}, 'modified', n.ZMODIFICATIONDATE1, 'pinned', ${bool("ZISPINNED")}, 'locked', ${bool("ZISPASSWORDPROTECTED")}, 'shared', ${notNull("n", "ZSERVERSHAREDATA")}, 'quicknote', ${bool("ZISSYSTEMPAPER")}, 'snippet', ${col2(available, "n", "ZSNIPPET")}, 'data', ${dataExpr}, 'tags', ${tagsExpr}) FROM ZICCLOUDSYNCINGOBJECT n WHERE ${whereSql} ORDER BY n.ZMODIFICATIONDATE1 DESC, n.Z_PK DESC LIMIT ${scan};`;
|
|
47775
47778
|
return [
|
|
47776
47779
|
"BEGIN;",
|
|
47777
47780
|
"SELECT json_object('k', 'meta', 'uuid', (SELECT Z_UUID FROM Z_METADATA LIMIT 1));",
|
|
@@ -47972,6 +47975,7 @@ function runNoteQuery(ast, options = {}) {
|
|
|
47972
47975
|
pinned: Boolean(row.pinned),
|
|
47973
47976
|
locked,
|
|
47974
47977
|
shared: Boolean(row.shared) || Boolean(folder?.shared),
|
|
47978
|
+
quicknote: Boolean(row.quicknote),
|
|
47975
47979
|
created: coreDataMs(row.created),
|
|
47976
47980
|
modified: coreDataMs(row.modified),
|
|
47977
47981
|
content: () => {
|
|
@@ -48038,6 +48042,7 @@ function runNoteQuery(ast, options = {}) {
|
|
|
48038
48042
|
}
|
|
48039
48043
|
|
|
48040
48044
|
// src/utils/searchContentDb.ts
|
|
48045
|
+
var SEARCH_CONTENT_SCAN_LIMIT = 5e3;
|
|
48041
48046
|
function buildSearchContentQuery(options) {
|
|
48042
48047
|
const children = [{ type: "text", field: "any", value: options.query }];
|
|
48043
48048
|
const folder = options.folder?.trim();
|
|
@@ -48062,7 +48067,7 @@ function buildSearchContentQuery(options) {
|
|
|
48062
48067
|
function searchContentViaDatabase(options) {
|
|
48063
48068
|
const result = runNoteQuery(buildSearchContentQuery(options), {
|
|
48064
48069
|
limit: options.limit,
|
|
48065
|
-
scanLimit:
|
|
48070
|
+
scanLimit: SEARCH_CONTENT_SCAN_LIMIT,
|
|
48066
48071
|
includeWordCount: options.includeWordCount,
|
|
48067
48072
|
dbPath: options.dbPath
|
|
48068
48073
|
});
|
|
@@ -54118,18 +54123,34 @@ function storedAttachmentIds(manager, id2) {
|
|
|
54118
54123
|
return null;
|
|
54119
54124
|
}
|
|
54120
54125
|
}
|
|
54121
|
-
|
|
54126
|
+
var KIND_BY_EXTENSION = {
|
|
54127
|
+
".pdf": "pdf",
|
|
54128
|
+
".png": "image",
|
|
54129
|
+
".jpg": "image",
|
|
54130
|
+
".jpeg": "image",
|
|
54131
|
+
".heic": "image",
|
|
54132
|
+
".tif": "image",
|
|
54133
|
+
".tiff": "image",
|
|
54134
|
+
".gif": "image"
|
|
54135
|
+
};
|
|
54136
|
+
function referencedByBody(assets, row) {
|
|
54137
|
+
return assets.orderSource === "body" ? row.bodyIndex !== null : void 0;
|
|
54138
|
+
}
|
|
54139
|
+
function storedInsertion(manager, id2, before, bytes, name, returnedId) {
|
|
54122
54140
|
let added = [];
|
|
54141
|
+
let assets;
|
|
54123
54142
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
54124
54143
|
if (attempt > 0) pause(250);
|
|
54125
54144
|
try {
|
|
54126
|
-
|
|
54145
|
+
assets = manager.getAttachmentAssetsById(id2);
|
|
54146
|
+
added = assets.attachments.filter(
|
|
54127
54147
|
(item) => item.parentIdentifier === null && !before.has(item.identifier.toLowerCase())
|
|
54128
54148
|
);
|
|
54129
54149
|
} catch {
|
|
54130
54150
|
added = [];
|
|
54131
54151
|
}
|
|
54132
|
-
if (added.length === 1 && added[0].assetPaths.length > 0)
|
|
54152
|
+
if (added.length === 1 && added[0].assetPaths.length > 0 && referencedByBody(assets, added[0]) !== false)
|
|
54153
|
+
break;
|
|
54133
54154
|
}
|
|
54134
54155
|
if (added.length === 0) return null;
|
|
54135
54156
|
if (added.length > 1) throw new Error(UNCERTAIN);
|
|
@@ -54137,6 +54158,15 @@ function storedInsertion(manager, id2, before, bytes, returnedId) {
|
|
|
54137
54158
|
const attachmentId = attachmentCoreDataId(id2, row.pk);
|
|
54138
54159
|
if (returnedId && /\/ICAttachment\/p\d+$/.test(returnedId) && returnedId !== attachmentId)
|
|
54139
54160
|
throw new Error(UNCERTAIN);
|
|
54161
|
+
const expectedKind = KIND_BY_EXTENSION[extname7(name).toLowerCase()];
|
|
54162
|
+
if (expectedKind && row.kind !== expectedKind)
|
|
54163
|
+
throw new Error(
|
|
54164
|
+
`Notes' database shows new attachment ${attachmentId} on this note, but its type (${row.uti ?? "unknown"}) does not fit ${name}; read the exact note and do not attach the file again`
|
|
54165
|
+
);
|
|
54166
|
+
if (referencedByBody(assets, row) === false)
|
|
54167
|
+
throw new Error(
|
|
54168
|
+
`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`
|
|
54169
|
+
);
|
|
54140
54170
|
const expected = sha256(bytes);
|
|
54141
54171
|
const matches = row.assetPaths.some((path10) => fileMatches(path10, bytes.length, expected));
|
|
54142
54172
|
if (!matches)
|
|
@@ -54188,7 +54218,7 @@ function attachFile(manager, args, checked, progress = { insertionStarted: false
|
|
|
54188
54218
|
if (!saved.success || !fileMatches(saved.savedPath ?? verifyPath, bytes.length, sha256(bytes)))
|
|
54189
54219
|
throw new Error("Attachment bytes were not verified; read the exact note before retrying");
|
|
54190
54220
|
} else {
|
|
54191
|
-
const stored = inserted.length === 0 && beforeStored ? storedInsertion(manager, id2, beforeStored, bytes, returnedId) : null;
|
|
54221
|
+
const stored = inserted.length === 0 && beforeStored ? storedInsertion(manager, id2, beforeStored, bytes, name, returnedId) : null;
|
|
54192
54222
|
if (!stored)
|
|
54193
54223
|
throw new Error(
|
|
54194
54224
|
inserted.length === 0 && !beforeStored ? `${UNCERTAIN}. Notes' AppleScript does not list some attachments (PDFs on macOS 27); grant Full Disk Access so the server can verify through the Notes database` : UNCERTAIN
|
|
@@ -54198,11 +54228,17 @@ function attachFile(manager, args, checked, progress = { insertionStarted: false
|
|
|
54198
54228
|
verifiedBy = "database";
|
|
54199
54229
|
}
|
|
54200
54230
|
const nameVerified = reportedName === name;
|
|
54231
|
+
let contentHash = after.hash;
|
|
54232
|
+
if (verifiedBy === "database") {
|
|
54233
|
+
const final = readSnapshot(manager, id2);
|
|
54234
|
+
assertExistingContentPreserved(before, final);
|
|
54235
|
+
contentHash = final.hash;
|
|
54236
|
+
}
|
|
54201
54237
|
return {
|
|
54202
54238
|
ok: true,
|
|
54203
54239
|
id: id2,
|
|
54204
54240
|
attachmentId,
|
|
54205
|
-
contentHash
|
|
54241
|
+
contentHash,
|
|
54206
54242
|
bytes: bytes.length,
|
|
54207
54243
|
name: reportedName,
|
|
54208
54244
|
...verifiedBy === "database" ? { verifiedBy } : {},
|
|
@@ -59922,7 +59958,7 @@ ${noteList}${truncationNote}${scanNote}${wordCountNote}${syncNote}`,
|
|
|
59922
59958
|
registerTool(
|
|
59923
59959
|
"query-notes",
|
|
59924
59960
|
{
|
|
59925
|
-
description: 'Use when: finding notes with a boolean expression over text and metadata \u2014 e.g. `folder:Work has:checklist -checklist:done`, `(title:invoice OR tag:finance) modified:>=2026-07-01`, `pinned words:>250`. Reads the Notes database directly, so it is fast and can match title OR body in one call.\nSyntax: bare words and "quoted phrases" match title or body (case-insensitive substring); fields title:, body:, text:, folder:, account:, tag: (values may be quoted, e.g. folder:"Work Projects"); facets has:link|attachment|checklist|drawing|image|video|audio|pdf|table|scan|tag; checklist:open|done; flags pinned, locked, shared (or is:pinned); words:>250 and created:/modified: with =, >, >=, <, <= and YYYY-MM-DD local dates. AND is implicit; OR, NOT, leading -, and parentheses are supported; operators are case-insensitive and a quoted "and" searches the literal word.\nReturns: matching notes (most recently modified first) with id, title, folder, account, modified date, snippet, and matchedIn (where the positive text terms occur: title, body, or both; absent when the body is unreadable or the query has no text term), plus scan/match counts; includeWordCount adds wordCount. Ids work with get-note-content and every other id-based tool.\nDo not use when: Full Disk Access is unavailable (use search-notes). Scans the most recent scanLimit notes (default 500); raise it for older notes.\nSafety: read-only; never writes the database. Excludes Recently Deleted and folderless notes unless includeDeleted is true. Locked notes match on title and metadata only; body predicates (including negated ones such as -body:x) never match them.',
|
|
59961
|
+
description: 'Use when: finding notes with a boolean expression over text and metadata \u2014 e.g. `folder:Work has:checklist -checklist:done`, `(title:invoice OR tag:finance) modified:>=2026-07-01`, `pinned words:>250`. Reads the Notes database directly, so it is fast and can match title OR body in one call.\nSyntax: bare words and "quoted phrases" match title or body (case-insensitive substring); fields title:, body:, text:, folder:, account:, tag: (values may be quoted, e.g. folder:"Work Projects"); facets has:link|attachment|checklist|drawing|image|video|audio|pdf|table|scan|url|map|tag (has:url is a link preview card, has:map a map); checklist:open|done; flags pinned, locked, shared, quicknote (or is:pinned); quote a flag word such as "quicknote" to search it as text; words:>250 and created:/modified: with =, >, >=, <, <= and YYYY-MM-DD local dates. AND is implicit; OR, NOT, leading -, and parentheses are supported; operators are case-insensitive and a quoted "and" searches the literal word.\nReturns: matching notes (most recently modified first) with id, title, folder, account, modified date, snippet, and matchedIn (where the positive text terms occur: title, body, or both; absent when the body is unreadable or the query has no text term), plus scan/match counts; includeWordCount adds wordCount. Ids work with get-note-content and every other id-based tool.\nDo not use when: Full Disk Access is unavailable (use search-notes). Scans the most recent scanLimit notes (default 500); raise it for older notes.\nSafety: read-only; never writes the database. Excludes Recently Deleted and folderless notes unless includeDeleted is true. Locked notes match on title and metadata only; body predicates (including negated ones such as -body:x) never match them.',
|
|
59926
59962
|
inputSchema: {
|
|
59927
59963
|
query: external_exports.string().min(1, "A query expression is required").max(MAX.QUERY).describe(
|
|
59928
59964
|
'Boolean query expression, e.g. `folder:"Work Projects" has:checklist -checklist:done`'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-notes-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
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",
|