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 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 5000. The response says when older notes were left unscanned. |
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, and the result carries `verifiedBy: "database"`. Without Full
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: 5e3 };
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: QUERY_SCAN.MAX,
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
- function storedInsertion(manager, id2, before, bytes, returnedId) {
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
- added = manager.getAttachmentAssetsById(id2).attachments.filter(
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) break;
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: after.hash,
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.9.34",
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",