apple-notes-mcp 2.8.42 → 2.8.43

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/build/index.js +141 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -420,7 +420,7 @@ Searches for notes by title or content.
420
420
  | Parameter | Type | Required | Description |
421
421
  |-----------|------|----------|-------------|
422
422
  | `query` | string | Yes | Text to search for |
423
- | `searchContent` | boolean | No | If `true`, searches note body; if `false` (default), searches titles only |
423
+ | `searchContent` | boolean | No | If `true`, searches note content (title line included); if `false` (default), searches titles only. With Full Disk Access, content search reads the Notes database (well under a second, the 5000 most recently modified notes, Recently Deleted excluded); without it, it falls back to AppleScript, which scans every body and can time out on a broad term |
424
424
  | `account` | string | No | Account to search in (defaults to Notes.app's default account; exact or unique-prefix match) |
425
425
  | `folder` | string | No | Limit search to a specific folder (supports nested paths like `"Work/Clients"`) |
426
426
  | `modifiedSince` | string | No | ISO 8601 date string to filter notes modified on or after this date (e.g., `"2025-01-01"`) |
@@ -451,7 +451,7 @@ Searches for notes by title or content.
451
451
  }
452
452
  ```
453
453
 
454
- **Returns:** List of matching notes with titles, folder names, and IDs. Use the returned ID for subsequent operations like `get-note-content`, `update-note`, etc.
454
+ **Returns:** List of matching notes with titles, folder names, and IDs. Use the returned ID for subsequent operations like `get-note-content`, `update-note`, etc. A content search also returns `source` (`"database"` or `"applescript"`), and `scanTruncated` when the database path left older notes unsearched.
455
455
 
456
456
  ---
457
457
 
package/build/index.js CHANGED
@@ -42373,6 +42373,16 @@ var AppleNotesManager = class {
42373
42373
  }
42374
42374
  return this.defaultAccountName || void 0;
42375
42375
  }
42376
+ /**
42377
+ * The account a search with this `account` argument is scoped to: the
42378
+ * caller's account when named, else Notes.app's default account (cached).
42379
+ * Returns `undefined` when no account was named and the default cannot be
42380
+ * determined. Used by search-notes' database path so it searches the same
42381
+ * account the AppleScript path would.
42382
+ */
42383
+ searchAccountScope(account) {
42384
+ return this.reportedAccount(this.resolveAccount(account));
42385
+ }
42376
42386
  /**
42377
42387
  * Checks if a note is password-protected by its ID.
42378
42388
  *
@@ -45681,6 +45691,13 @@ function describeSearchScope(searchContent, resultCount) {
45681
45691
  return "\n\n\u2139\uFE0F Only note titles were searched, so a term that appears in note bodies would not match. Retry with `searchContent: true` to search bodies instead.";
45682
45692
  }
45683
45693
 
45694
+ // src/utils/noteQueryStore.ts
45695
+ import { execFileSync as execFileSync13 } from "child_process";
45696
+ import * as fs8 from "fs";
45697
+ import * as os8 from "os";
45698
+ import * as path8 from "path";
45699
+ import { gunzipSync as gunzipSync7 } from "zlib";
45700
+
45684
45701
  // src/utils/noteQuery.ts
45685
45702
  var QUERY_LIMITS = {
45686
45703
  /** Maximum number of tokens (terms, operators, parentheses). */
@@ -46127,11 +46144,6 @@ function positiveTextTerms(node, negated = false) {
46127
46144
  }
46128
46145
 
46129
46146
  // src/utils/noteQueryStore.ts
46130
- import { execFileSync as execFileSync13 } from "child_process";
46131
- import * as fs8 from "fs";
46132
- import * as os8 from "os";
46133
- import * as path8 from "path";
46134
- import { gunzipSync as gunzipSync7 } from "zlib";
46135
46147
  var NOTES_DB_PATH9 = path8.join(
46136
46148
  os8.homedir(),
46137
46149
  "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
@@ -46327,7 +46339,13 @@ function buildSnippet(text2, terms) {
46327
46339
  }
46328
46340
  function queryNotes(expression, options = {}) {
46329
46341
  const ast = parseNoteQuery(expression);
46330
- const limit = Math.min(options.limit ?? QUERY_RESULTS.DEFAULT, QUERY_RESULTS.MAX);
46342
+ return runNoteQuery(ast, {
46343
+ ...options,
46344
+ limit: Math.min(options.limit ?? QUERY_RESULTS.DEFAULT, QUERY_RESULTS.MAX)
46345
+ });
46346
+ }
46347
+ function runNoteQuery(ast, options = {}) {
46348
+ const limit = options.limit ?? QUERY_RESULTS.DEFAULT;
46331
46349
  const scanLimit = Math.min(options.scanLimit ?? QUERY_SCAN.DEFAULT, QUERY_SCAN.MAX);
46332
46350
  const includeDeleted = options.includeDeleted ?? false;
46333
46351
  const withBodies = needsContent(ast);
@@ -46464,6 +46482,66 @@ function queryNotes(expression, options = {}) {
46464
46482
  };
46465
46483
  }
46466
46484
 
46485
+ // src/utils/searchContentDb.ts
46486
+ function buildSearchContentQuery(options) {
46487
+ const children = [{ type: "text", field: "any", value: options.query }];
46488
+ const folder = options.folder?.trim();
46489
+ if (folder) children.push({ type: "folder", value: folder });
46490
+ const account = options.account?.trim();
46491
+ if (account) children.push({ type: "account", value: account });
46492
+ if (options.modifiedSince) {
46493
+ const start = new Date(options.modifiedSince).getTime();
46494
+ if (Number.isFinite(start)) {
46495
+ children.push({
46496
+ type: "date",
46497
+ field: "modified",
46498
+ op: ">=",
46499
+ date: options.modifiedSince,
46500
+ start,
46501
+ end: start + 24 * 60 * 60 * 1e3
46502
+ });
46503
+ }
46504
+ }
46505
+ return children.length === 1 ? children[0] : { type: "and", children };
46506
+ }
46507
+ function searchContentViaDatabase(options) {
46508
+ const result = runNoteQuery(buildSearchContentQuery(options), {
46509
+ limit: options.limit,
46510
+ scanLimit: QUERY_SCAN.MAX,
46511
+ dbPath: options.dbPath
46512
+ });
46513
+ const notes = result.notes.map((hit) => ({
46514
+ id: hit.id,
46515
+ title: hit.title,
46516
+ content: "",
46517
+ tags: [],
46518
+ created: hit.created ? new Date(hit.created) : /* @__PURE__ */ new Date(0),
46519
+ modified: hit.modified ? new Date(hit.modified) : /* @__PURE__ */ new Date(0),
46520
+ ...hit.folder !== void 0 ? { folder: hit.folder } : {},
46521
+ ...hit.account !== void 0 ? { account: hit.account } : {}
46522
+ }));
46523
+ return {
46524
+ notes,
46525
+ scan: {
46526
+ scanned: result.scanned,
46527
+ eligible: result.eligible,
46528
+ scanTruncated: result.scanTruncated,
46529
+ matched: result.matched
46530
+ }
46531
+ };
46532
+ }
46533
+ function describeContentScan(scan) {
46534
+ if (!scan?.scanTruncated) return "";
46535
+ return `
46536
+
46537
+ \u2139\uFE0F Searched the ${scan.scanned} most recently modified of ${scan.eligible} notes; older notes were not searched. A title search (\`searchContent: false\`) covers every note.`;
46538
+ }
46539
+ function contentSearchFailureHint(message, dbUnavailable) {
46540
+ if (!/timed out/i.test(message)) return message;
46541
+ const remedy = dbUnavailable === "no_fda" ? " Grant Full Disk Access to the app that launches this server (run the doctor tool) so search-notes can search note bodies through the Notes database instead, which takes well under a second." : "";
46542
+ 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.`;
46543
+ }
46544
+
46467
46545
  // src/tools/doctor.ts
46468
46546
  import { spawnSync } from "child_process";
46469
46547
 
@@ -50981,10 +51059,12 @@ registerTool(
50981
51059
  registerTool(
50982
51060
  "search-notes",
50983
51061
  {
50984
- description: "Use when: finding notes by a keyword in the title (or body with searchContent=true) and you need their ids.\nReturns: matching notes with title, folder, and id.\nDo not use when: you already have a note id (use get-note-content) or want every note (use list-notes).\nPrefer this first to obtain ids for subsequent read/update/delete/move calls.",
51062
+ description: "Use when: finding notes by a keyword in the title (or body with searchContent=true) and you need their ids.\nReturns: matching notes with title, folder, and id; `source` says whether a body search read the Notes database or fell back to AppleScript.\nDo not use when: you already have a note id (use get-note-content), want every note (use list-notes), or need boolean or metadata filters (use query-notes).\nPrefer this first to obtain ids for subsequent read/update/delete/move calls.\nNote: with Full Disk Access, body search reads the Notes database (fast; the most recent 5000 notes, Recently Deleted excluded); without it, it falls back to AppleScript, which scans every body and can time out on broad terms.",
50985
51063
  inputSchema: {
50986
51064
  query: external_exports.string().min(1, "Search query is required").max(MAX.QUERY),
50987
- searchContent: external_exports.boolean().optional().describe("Search note content instead of titles"),
51065
+ searchContent: external_exports.boolean().optional().describe(
51066
+ "Search note content (title line included) instead of titles. Uses the Notes database when Full Disk Access is available, else AppleScript"
51067
+ ),
50988
51068
  account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account to search in"),
50989
51069
  folder: external_exports.string().max(MAX.FOLDER).optional().describe("Limit search to a specific folder"),
50990
51070
  modifiedSince: external_exports.string().max(64).optional().describe(
@@ -50996,21 +51076,59 @@ registerTool(
50996
51076
  },
50997
51077
  outputSchema: {
50998
51078
  notes: external_exports.array(external_exports.object({}).passthrough()).optional(),
50999
- count: external_exports.number().optional()
51079
+ count: external_exports.number().optional(),
51080
+ source: external_exports.enum(["database", "applescript"]).optional(),
51081
+ scanTruncated: external_exports.boolean().optional()
51000
51082
  }
51001
51083
  },
51002
51084
  withErrorHandling(({ query: query2, searchContent = false, account, folder, modifiedSince, limit }) => {
51003
51085
  const effectiveLimit = resolveSearchLimit(limit);
51004
51086
  const limitWasDefault = limit === void 0;
51087
+ let source;
51088
+ let dbScan;
51089
+ let dbUnavailable;
51090
+ const runSearch = () => {
51091
+ if (searchContent) {
51092
+ try {
51093
+ const db = searchContentViaDatabase({
51094
+ query: query2,
51095
+ account: notesManager.searchAccountScope(account),
51096
+ folder,
51097
+ modifiedSince,
51098
+ limit: effectiveLimit
51099
+ });
51100
+ source = "database";
51101
+ dbScan = db.scan;
51102
+ return db.notes;
51103
+ } catch (error2) {
51104
+ if (!(error2 instanceof NoteQueryStoreError)) throw error2;
51105
+ dbUnavailable = error2.kind;
51106
+ }
51107
+ source = "applescript";
51108
+ }
51109
+ try {
51110
+ return notesManager.searchNotes(
51111
+ query2,
51112
+ searchContent,
51113
+ account,
51114
+ folder,
51115
+ modifiedSince,
51116
+ effectiveLimit
51117
+ );
51118
+ } catch (error2) {
51119
+ if (!searchContent || !(error2 instanceof Error)) throw error2;
51120
+ throw new Error(contentSearchFailureHint(error2.message, dbUnavailable));
51121
+ }
51122
+ };
51005
51123
  const {
51006
51124
  result: notes,
51007
51125
  syncBefore,
51008
51126
  syncInterference
51009
- } = withSyncAwarenessSync(
51010
- "search-notes",
51011
- () => notesManager.searchNotes(query2, searchContent, account, folder, modifiedSince, effectiveLimit)
51012
- );
51013
- const searchType = searchContent ? "content" : "titles";
51127
+ } = withSyncAwarenessSync("search-notes", runSearch);
51128
+ const searchType = searchContent ? source === "database" ? "content via the Notes database" : "content" : "titles";
51129
+ const sourceFields = source ? { source } : {};
51130
+ const scanFields = dbScan ? { scanTruncated: dbScan.scanTruncated } : {};
51131
+ const scanNote = describeContentScan(dbScan);
51014
51132
  const folderInfo = folder ? ` in folder "${folder}"` : "";
51015
51133
  const dateInfo = modifiedSince ? ` modified since ${modifiedSince}` : "";
51016
51134
  const { info: limitInfo, truncationNote } = describeSearchLimit(
@@ -51031,8 +51149,8 @@ ${syncWarnings.join(" ")}` : "";
51031
51149
  if (notes.length === 0) {
51032
51150
  const scopeHint = describeSearchScope(searchContent, notes.length);
51033
51151
  return successResponse(
51034
- `No notes found matching "${query2}" in ${searchType}${folderInfo}${dateInfo}${scopeHint}${syncNote}`,
51035
- { notes: [], count: 0 }
51152
+ `No notes found matching "${query2}" in ${searchType}${folderInfo}${dateInfo}${scopeHint}${scanNote}${syncNote}`,
51153
+ { notes: [], count: 0, ...sourceFields, ...scanFields }
51036
51154
  );
51037
51155
  }
51038
51156
  const noteList = notes.map((n) => {
@@ -51046,8 +51164,13 @@ ${syncWarnings.join(" ")}` : "";
51046
51164
  }).join("\n");
51047
51165
  return successResponse(
51048
51166
  `Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):
51049
- ${noteList}${truncationNote}${syncNote}`,
51050
- { notes: withStableIdentifiers(notes, "ICNote"), count: notes.length }
51167
+ ${noteList}${truncationNote}${scanNote}${syncNote}`,
51168
+ {
51169
+ notes: withStableIdentifiers(notes, "ICNote"),
51170
+ count: notes.length,
51171
+ ...sourceFields,
51172
+ ...scanFields
51173
+ }
51051
51174
  );
51052
51175
  }, "Error searching notes")
51053
51176
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.8.42",
3
+ "version": "2.8.43",
4
4
  "description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",