apple-notes-mcp 2.8.41 → 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.
- package/README.md +3 -3
- package/build/index.js +157 -23
- 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
|
|
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
|
|
|
@@ -1602,7 +1602,7 @@ Saves a note attachment to disk.
|
|
|
1602
1602
|
|-----------|------|----------|-------------|
|
|
1603
1603
|
| `noteId` | string | Yes | CoreData note ID (from `search-notes`/`list-notes`) |
|
|
1604
1604
|
| `attachmentId` | string | Yes | Attachment ID (from `list-attachments`) |
|
|
1605
|
-
| `savePath` | string | Yes | Absolute destination file path. Must be under your home directory, a temp directory, or `/Volumes` |
|
|
1605
|
+
| `savePath` | string | Yes | Absolute destination file path. Must be under your home directory, a temp directory, or `/Volumes`, and never inside the Notes data folder (`~/Library/Group Containers/group.com.apple.notes`), however it is reached (symlink or different letter case) |
|
|
1606
1606
|
|
|
1607
1607
|
**Returns:** Confirmation with the saved path, name, and content type (also in `structuredContent`).
|
|
1608
1608
|
|
package/build/index.js
CHANGED
|
@@ -40961,6 +40961,9 @@ function allowedSaveRoots() {
|
|
|
40961
40961
|
"/private/tmp"
|
|
40962
40962
|
];
|
|
40963
40963
|
}
|
|
40964
|
+
function deniedSaveRoots() {
|
|
40965
|
+
return [join6(homedir6(), "Library/Group Containers/group.com.apple.notes")];
|
|
40966
|
+
}
|
|
40964
40967
|
function canonicalize(path9) {
|
|
40965
40968
|
return realpathSync.native(path9);
|
|
40966
40969
|
}
|
|
@@ -41002,11 +41005,11 @@ function deepestExistingAncestor(abs) {
|
|
|
41002
41005
|
current = parent;
|
|
41003
41006
|
}
|
|
41004
41007
|
}
|
|
41005
|
-
function ensureParentDir(abs, roots = allowedSaveRoots()) {
|
|
41006
|
-
assertSafeSavePath(abs, roots);
|
|
41008
|
+
function ensureParentDir(abs, roots = allowedSaveRoots(), denied = deniedSaveRoots()) {
|
|
41009
|
+
assertSafeSavePath(abs, roots, denied);
|
|
41007
41010
|
mkdirSync(dirname(abs), { recursive: true });
|
|
41008
41011
|
}
|
|
41009
|
-
function assertSafeSavePath(p, roots = allowedSaveRoots()) {
|
|
41012
|
+
function assertSafeSavePath(p, roots = allowedSaveRoots(), denied = deniedSaveRoots()) {
|
|
41010
41013
|
if (!p || !p.trim()) throw new Error("A destination path is required.");
|
|
41011
41014
|
if (!isAbsolute(p)) throw new Error(`Destination path must be absolute: "${p}"`);
|
|
41012
41015
|
const abs = resolve(p);
|
|
@@ -41034,6 +41037,12 @@ function assertSafeSavePath(p, roots = allowedSaveRoots()) {
|
|
|
41034
41037
|
`Refusing to write outside allowed locations (home, temp, /Volumes): "${abs}" resolves to "${canonicalDest}" through a symbolic link.`
|
|
41035
41038
|
);
|
|
41036
41039
|
}
|
|
41040
|
+
const deniedForms = [
|
|
41041
|
+
.../* @__PURE__ */ new Set([...denied.map((d) => resolve(d)), ...canonicalRoots(denied)])
|
|
41042
|
+
].map((d) => d.toLowerCase());
|
|
41043
|
+
if ([abs, canonicalDest].some((form) => isWithinRoots(form.toLowerCase(), deniedForms))) {
|
|
41044
|
+
throw new Error(`Refusing to write inside the Notes library container: "${abs}"`);
|
|
41045
|
+
}
|
|
41037
41046
|
return abs;
|
|
41038
41047
|
}
|
|
41039
41048
|
var DEFAULT_MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
@@ -42364,6 +42373,16 @@ var AppleNotesManager = class {
|
|
|
42364
42373
|
}
|
|
42365
42374
|
return this.defaultAccountName || void 0;
|
|
42366
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
|
+
}
|
|
42367
42386
|
/**
|
|
42368
42387
|
* Checks if a note is password-protected by its ID.
|
|
42369
42388
|
*
|
|
@@ -45672,6 +45691,13 @@ function describeSearchScope(searchContent, resultCount) {
|
|
|
45672
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.";
|
|
45673
45692
|
}
|
|
45674
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
|
+
|
|
45675
45701
|
// src/utils/noteQuery.ts
|
|
45676
45702
|
var QUERY_LIMITS = {
|
|
45677
45703
|
/** Maximum number of tokens (terms, operators, parentheses). */
|
|
@@ -46118,11 +46144,6 @@ function positiveTextTerms(node, negated = false) {
|
|
|
46118
46144
|
}
|
|
46119
46145
|
|
|
46120
46146
|
// src/utils/noteQueryStore.ts
|
|
46121
|
-
import { execFileSync as execFileSync13 } from "child_process";
|
|
46122
|
-
import * as fs8 from "fs";
|
|
46123
|
-
import * as os8 from "os";
|
|
46124
|
-
import * as path8 from "path";
|
|
46125
|
-
import { gunzipSync as gunzipSync7 } from "zlib";
|
|
46126
46147
|
var NOTES_DB_PATH9 = path8.join(
|
|
46127
46148
|
os8.homedir(),
|
|
46128
46149
|
"Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
|
|
@@ -46318,7 +46339,13 @@ function buildSnippet(text2, terms) {
|
|
|
46318
46339
|
}
|
|
46319
46340
|
function queryNotes(expression, options = {}) {
|
|
46320
46341
|
const ast = parseNoteQuery(expression);
|
|
46321
|
-
|
|
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;
|
|
46322
46349
|
const scanLimit = Math.min(options.scanLimit ?? QUERY_SCAN.DEFAULT, QUERY_SCAN.MAX);
|
|
46323
46350
|
const includeDeleted = options.includeDeleted ?? false;
|
|
46324
46351
|
const withBodies = needsContent(ast);
|
|
@@ -46455,6 +46482,66 @@ function queryNotes(expression, options = {}) {
|
|
|
46455
46482
|
};
|
|
46456
46483
|
}
|
|
46457
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
|
+
|
|
46458
46545
|
// src/tools/doctor.ts
|
|
46459
46546
|
import { spawnSync } from "child_process";
|
|
46460
46547
|
|
|
@@ -50972,10 +51059,12 @@ registerTool(
|
|
|
50972
51059
|
registerTool(
|
|
50973
51060
|
"search-notes",
|
|
50974
51061
|
{
|
|
50975
|
-
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)
|
|
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.",
|
|
50976
51063
|
inputSchema: {
|
|
50977
51064
|
query: external_exports.string().min(1, "Search query is required").max(MAX.QUERY),
|
|
50978
|
-
searchContent: external_exports.boolean().optional().describe(
|
|
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
|
+
),
|
|
50979
51068
|
account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account to search in"),
|
|
50980
51069
|
folder: external_exports.string().max(MAX.FOLDER).optional().describe("Limit search to a specific folder"),
|
|
50981
51070
|
modifiedSince: external_exports.string().max(64).optional().describe(
|
|
@@ -50987,21 +51076,59 @@ registerTool(
|
|
|
50987
51076
|
},
|
|
50988
51077
|
outputSchema: {
|
|
50989
51078
|
notes: external_exports.array(external_exports.object({}).passthrough()).optional(),
|
|
50990
|
-
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()
|
|
50991
51082
|
}
|
|
50992
51083
|
},
|
|
50993
51084
|
withErrorHandling(({ query: query2, searchContent = false, account, folder, modifiedSince, limit }) => {
|
|
50994
51085
|
const effectiveLimit = resolveSearchLimit(limit);
|
|
50995
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
|
+
};
|
|
50996
51123
|
const {
|
|
50997
51124
|
result: notes,
|
|
50998
51125
|
syncBefore,
|
|
50999
51126
|
syncInterference
|
|
51000
|
-
} = withSyncAwarenessSync(
|
|
51001
|
-
|
|
51002
|
-
|
|
51003
|
-
|
|
51004
|
-
const
|
|
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);
|
|
51005
51132
|
const folderInfo = folder ? ` in folder "${folder}"` : "";
|
|
51006
51133
|
const dateInfo = modifiedSince ? ` modified since ${modifiedSince}` : "";
|
|
51007
51134
|
const { info: limitInfo, truncationNote } = describeSearchLimit(
|
|
@@ -51022,8 +51149,8 @@ ${syncWarnings.join(" ")}` : "";
|
|
|
51022
51149
|
if (notes.length === 0) {
|
|
51023
51150
|
const scopeHint = describeSearchScope(searchContent, notes.length);
|
|
51024
51151
|
return successResponse(
|
|
51025
|
-
`No notes found matching "${query2}" in ${searchType}${folderInfo}${dateInfo}${scopeHint}${syncNote}`,
|
|
51026
|
-
{ notes: [], count: 0 }
|
|
51152
|
+
`No notes found matching "${query2}" in ${searchType}${folderInfo}${dateInfo}${scopeHint}${scanNote}${syncNote}`,
|
|
51153
|
+
{ notes: [], count: 0, ...sourceFields, ...scanFields }
|
|
51027
51154
|
);
|
|
51028
51155
|
}
|
|
51029
51156
|
const noteList = notes.map((n) => {
|
|
@@ -51037,8 +51164,13 @@ ${syncWarnings.join(" ")}` : "";
|
|
|
51037
51164
|
}).join("\n");
|
|
51038
51165
|
return successResponse(
|
|
51039
51166
|
`Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):
|
|
51040
|
-
${noteList}${truncationNote}${syncNote}`,
|
|
51041
|
-
{
|
|
51167
|
+
${noteList}${truncationNote}${scanNote}${syncNote}`,
|
|
51168
|
+
{
|
|
51169
|
+
notes: withStableIdentifiers(notes, "ICNote"),
|
|
51170
|
+
count: notes.length,
|
|
51171
|
+
...sourceFields,
|
|
51172
|
+
...scanFields
|
|
51173
|
+
}
|
|
51042
51174
|
);
|
|
51043
51175
|
}, "Error searching notes")
|
|
51044
51176
|
);
|
|
@@ -52764,13 +52896,15 @@ registerTool(
|
|
|
52764
52896
|
registerTool(
|
|
52765
52897
|
"save-attachment",
|
|
52766
52898
|
{
|
|
52767
|
-
description: "Use when: writing one note attachment to a file on disk.\nReturns: the saved path.\nDo not use when: you want the bytes in-memory as base64 (fetch-attachment).\nSafety: writes a file; savePath must be absolute and under the home directory, a temp dir, or /Volumes. Get the ids from list-attachments first.",
|
|
52899
|
+
description: "Use when: writing one note attachment to a file on disk.\nReturns: the saved path.\nDo not use when: you want the bytes in-memory as base64 (fetch-attachment).\nSafety: writes a file; savePath must be absolute and under the home directory, a temp dir, or /Volumes, and not inside the Notes data folder. Get the ids from list-attachments first.",
|
|
52768
52900
|
inputSchema: {
|
|
52769
52901
|
noteId: looseNoteId(external_exports.string().min(1, "noteId is required")).describe(
|
|
52770
52902
|
`Note id (from search/list): ${NOTE_ID_FORMS}`
|
|
52771
52903
|
),
|
|
52772
52904
|
attachmentId: external_exports.string().min(1, "attachmentId is required").max(MAX.ATTACHMENT_ID).describe("Attachment id (from list-attachments)"),
|
|
52773
|
-
savePath: external_exports.string().min(1, "savePath is required").max(MAX.SAVE_PATH).describe(
|
|
52905
|
+
savePath: external_exports.string().min(1, "savePath is required").max(MAX.SAVE_PATH).describe(
|
|
52906
|
+
"Absolute destination file path (must be under home, temp, or /Volumes; never inside the Notes data folder)"
|
|
52907
|
+
)
|
|
52774
52908
|
},
|
|
52775
52909
|
outputSchema: {
|
|
52776
52910
|
savedPath: external_exports.string().optional(),
|
package/package.json
CHANGED