apple-notes-mcp 2.5.12 → 2.6.1

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 +65 -0
  2. package/build/index.js +383 -81
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -469,6 +469,71 @@ Moves a note to a different folder. The note is relocated in place via Notes.app
469
469
 
470
470
  ---
471
471
 
472
+ #### `append-to-note`
473
+
474
+ Appends or prepends content to an existing note without replacing it. Always reads and writes as HTML, preserving all existing rich formatting.
475
+
476
+ | Parameter | Type | Required | Description |
477
+ |-----------|------|----------|-------------|
478
+ | `id` | string | No | Note ID (preferred - more reliable than title) |
479
+ | `title` | string | No | Note title (use `id` instead when available) |
480
+ | `content` | string | Yes | Text to append to the note body |
481
+ | `position` | string | No | `"after"` (default) appends to the end; `"before"` prepends to the start |
482
+ | `separator` | string | No | String placed between existing content and new content (default: two newlines → `<div><br></div>` in HTML) |
483
+ | `format` | string | No | Format of the content being appended: `"plaintext"` (default) or `"html"` |
484
+ | `account` | string | No | Account containing the note (defaults to iCloud, ignored if `id` is provided) |
485
+
486
+ **Note:** Either `id` or `title` must be provided. Using `id` is recommended.
487
+
488
+ **Example - Append plaintext:**
489
+ ```json
490
+ {
491
+ "id": "x-coredata://ABC123/ICNote/p456",
492
+ "content": "New item added today"
493
+ }
494
+ ```
495
+
496
+ **Example - Prepend HTML:**
497
+ ```json
498
+ {
499
+ "id": "x-coredata://ABC123/ICNote/p456",
500
+ "content": "<div><b>Status:</b> done</div>",
501
+ "format": "html",
502
+ "position": "before"
503
+ }
504
+ ```
505
+
506
+ **Returns:** Confirmation with note id and title. Warns when the note is shared with collaborators.
507
+
508
+ **⚠️ Safety:** Reads the existing body first, concatenates, then writes back. Run `list-attachments` first if the note may hold embedded files — a full-body rewrite can drop attachments.
509
+
510
+ ---
511
+
512
+ #### `get-note-link`
513
+
514
+ Returns the `notes://showNote?identifier=<uuid>` deep-link URL for a note. The URL opens the note in Notes.app on iOS and macOS and can be stored in Reminders tasks or shared links.
515
+
516
+ | Parameter | Type | Required | Description |
517
+ |-----------|------|----------|-------------|
518
+ | `id` | string | No | Note ID (preferred - more reliable than title) |
519
+ | `title` | string | No | Note title (use `id` instead when available) |
520
+ | `account` | string | No | Account containing the note (defaults to iCloud, ignored if `id` is provided) |
521
+
522
+ **Note:** Either `id` or `title` must be provided. Using `id` is recommended. Password-protected notes cannot be linked.
523
+
524
+ **Example:**
525
+ ```json
526
+ {
527
+ "id": "x-coredata://ABC123/ICNote/p456"
528
+ }
529
+ ```
530
+
531
+ **Returns:** `notes://showNote?identifier=<uuid>` URL string, plus the note id and title.
532
+
533
+ **Note:** Requires Full Disk Access for the app that launches the server so the Notes SQLite database is readable. On macOS 12–15 the tool also falls back to the AppleScript `note link` property. Run the `doctor` tool to verify access.
534
+
535
+ ---
536
+
472
537
  #### `list-notes`
473
538
 
474
539
  Lists all notes, optionally filtered by folder, date, and limit.
package/build/index.js CHANGED
@@ -6,7 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __commonJS = (cb, mod) => function __require() {
9
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
10
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
11
+ }) : x)(function(x) {
12
+ if (typeof require !== "undefined") return require.apply(this, arguments);
13
+ throw Error('Dynamic require of "' + x + '" is not supported');
14
+ });
15
+ var __commonJS = (cb, mod) => function __require2() {
10
16
  try {
11
17
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
18
  } catch (e) {
@@ -24161,14 +24167,14 @@ var require_turndown_cjs = __commonJS({
24161
24167
  } else if (node.nodeType === 1) {
24162
24168
  replacement = replacementForNode.call(self, node);
24163
24169
  }
24164
- return join5(output, replacement);
24170
+ return join6(output, replacement);
24165
24171
  }, "");
24166
24172
  }
24167
24173
  function postProcess(output) {
24168
24174
  var self = this;
24169
24175
  this.rules.forEach(function(rule) {
24170
24176
  if (typeof rule.append === "function") {
24171
- output = join5(output, rule.append(self.options));
24177
+ output = join6(output, rule.append(self.options));
24172
24178
  }
24173
24179
  });
24174
24180
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -24180,7 +24186,7 @@ var require_turndown_cjs = __commonJS({
24180
24186
  if (whitespace.leading || whitespace.trailing) content = content.trim();
24181
24187
  return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
24182
24188
  }
24183
- function join5(output, replacement) {
24189
+ function join6(output, replacement) {
24184
24190
  var s1 = trimTrailingNewlines(output);
24185
24191
  var s2 = trimLeadingNewlines(replacement);
24186
24192
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -38700,12 +38706,14 @@ function isTimeoutError(error2) {
38700
38706
  }
38701
38707
  return false;
38702
38708
  }
38709
+ var BULK_LIST_MUTATION_ERROR = "Notes changed during listing";
38703
38710
  var RETRYABLE_ERROR_PATTERNS = [
38704
38711
  /timed? out/i,
38705
38712
  /not responding/i,
38706
38713
  /connection.*invalid/i,
38707
38714
  /lost connection/i,
38708
- /busy/i
38715
+ /busy/i,
38716
+ /changed during listing/i
38709
38717
  ];
38710
38718
  function isRetryableError(errorMessage) {
38711
38719
  return RETRYABLE_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage));
@@ -38764,6 +38772,13 @@ var ERROR_MAPPINGS = [
38764
38772
  pattern: /password protected|locked note/i,
38765
38773
  message: "Note is password-protected. Unlock it in Notes.app first."
38766
38774
  },
38775
+ // Mid-listing library mutation detected by a bulk-list count guard (#86).
38776
+ // The message must keep the phrase "changed during listing" so
38777
+ // RETRYABLE_ERROR_PATTERNS still matches it after this mapping.
38778
+ {
38779
+ pattern: /changed during listing/i,
38780
+ message: "Notes changed during listing (an iCloud sync may have landed mid-read). The operation is retried automatically; run it again if this persists."
38781
+ },
38767
38782
  // Syntax/script errors (usually programming bugs)
38768
38783
  {
38769
38784
  pattern: /syntax error|expected/i,
@@ -39200,6 +39215,8 @@ function cleanupTempDir(dir) {
39200
39215
  // src/services/appleNotesManager.ts
39201
39216
  var import_turndown = __toESM(require_turndown_cjs(), 1);
39202
39217
  import { existsSync as existsSync3 } from "fs";
39218
+ import { homedir as homedir3 } from "os";
39219
+ import { join as join2 } from "path";
39203
39220
  var FIELD_SEP = "";
39204
39221
  var RECORD_SEP = "";
39205
39222
  var AS_FIELD_SEP = "(ASCII character 31)";
@@ -39276,6 +39293,12 @@ function buildAppleScriptDateVar(date3, varName = "thresholdDate") {
39276
39293
  const timeInSeconds = date3.getHours() * 3600 + date3.getMinutes() * 60 + date3.getSeconds();
39277
39294
  return [
39278
39295
  `set ${varName} to current date`,
39296
+ // Reset day to 1 BEFORE assigning month: AppleScript date components roll
39297
+ // over, so setting month to (say) June while the variable still holds the
39298
+ // 31st inherited from `current date` produces July 1 (June 31 doesn't
39299
+ // exist), and the day assignment below then lands in the wrong month.
39300
+ // Day 1 exists in every month, so year/month can never roll over. (#86)
39301
+ `set day of ${varName} to 1`,
39279
39302
  `set year of ${varName} to ${year}`,
39280
39303
  `set month of ${varName} to ${month}`,
39281
39304
  `set day of ${varName} to ${day}`,
@@ -39342,6 +39365,27 @@ function buildAppLevelScript(command) {
39342
39365
  end tell
39343
39366
  `;
39344
39367
  }
39368
+ function getNoteLinkFromDB(coreDataId) {
39369
+ const match = coreDataId.match(/\/p(\d+)$/);
39370
+ if (!match) return null;
39371
+ const pk = parseInt(match[1], 10);
39372
+ const dbPath = join2(homedir3(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
39373
+ if (!existsSync3(dbPath)) return null;
39374
+ try {
39375
+ const { DatabaseSync } = __require("node:sqlite");
39376
+ const db = new DatabaseSync(dbPath, { readOnly: true });
39377
+ try {
39378
+ const row = db.prepare("SELECT ZIDENTIFIER FROM ZICCLOUDSYNCINGOBJECT WHERE Z_PK = ?").get(pk);
39379
+ const identifier = row?.ZIDENTIFIER;
39380
+ return identifier ? `notes://showNote?identifier=${identifier}` : null;
39381
+ } finally {
39382
+ db.close();
39383
+ }
39384
+ } catch (err) {
39385
+ console.error("getNoteLinkFromDB: failed to query Notes database:", err);
39386
+ return null;
39387
+ }
39388
+ }
39345
39389
  function extractCoreDataId(output, prefix) {
39346
39390
  const pattern = new RegExp(`${prefix} id ([^\\s]+)`);
39347
39391
  const match = output.match(pattern);
@@ -39888,6 +39932,101 @@ var AppleNotesManager = class {
39888
39932
  }
39889
39933
  return true;
39890
39934
  }
39935
+ /**
39936
+ * Builds the AppleScript body for a bulk note listing.
39937
+ *
39938
+ * Names, ids, and (when date-filtering) modification dates are fetched as
39939
+ * whole-list Apple Events instead of two events per note; per-note round
39940
+ * trips scale linearly and push large libraries past client tool timeouts
39941
+ * (#86). The lists are separate snapshots of a live, syncing collection, so
39942
+ * every script guards that they are the same length before zipping them by
39943
+ * index — a mid-listing mutation would otherwise silently mispair names and
39944
+ * ids (grow) or read past the end of a list (shrink). On mismatch the
39945
+ * script raises BULK_LIST_MUTATION_ERROR, which executeAppleScript treats
39946
+ * as retryable, re-running the whole script on a fresh snapshot. A length
39947
+ * check cannot see an exactly-offsetting delete+create landing in the
39948
+ * milliseconds between two fetches; that residual window is accepted —
39949
+ * closing it would cost an extra whole-list fetch per listing.
39950
+ *
39951
+ * @param folderRef - Optional AppleScript folder reference to scope to
39952
+ * @param dateSetup - AppleScript defining thresholdDate; enables date filtering
39953
+ * @param sliceLimit - Fetch only the first N notes (mutually exclusive with
39954
+ * dateSetup: a date filter must scan every note's date). The script then
39955
+ * returns the total note count as a leading record so the caller can
39956
+ * detect a dedup shortfall and fall back to a full fetch.
39957
+ */
39958
+ buildBulkListCommand(opts) {
39959
+ const { folderRef, dateSetup, sliceLimit } = opts;
39960
+ const fullSource = folderRef ? `notes of ${folderRef}` : "notes";
39961
+ const countGuard = (listVar) => `if (count of ${listVar}) is not (count of noteNames) then error "${BULK_LIST_MUTATION_ERROR}"`;
39962
+ if (sliceLimit !== void 0) {
39963
+ const slicedSource = folderRef ? `(notes 1 thru fetchCount of ${folderRef})` : `(notes 1 thru fetchCount)`;
39964
+ return `
39965
+ set totalCount to count of ${fullSource}
39966
+ set fetchCount to ${sliceLimit}
39967
+ if fetchCount > totalCount then set fetchCount to totalCount
39968
+ set resultList to {}
39969
+ if fetchCount > 0 then
39970
+ try
39971
+ set noteNames to name of ${slicedSource}
39972
+ set noteIds to id of ${slicedSource}
39973
+ on error errMsg number errNum
39974
+ if errNum is -1719 or errNum is -1728 then
39975
+ error "${BULK_LIST_MUTATION_ERROR}"
39976
+ else
39977
+ error errMsg number errNum
39978
+ end if
39979
+ end try
39980
+ ${countGuard("noteIds")}
39981
+ repeat with i from 1 to count of noteNames
39982
+ set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP} & (item i of noteIds)
39983
+ end repeat
39984
+ end if
39985
+ set AppleScript's text item delimiters to ${AS_RECORD_SEP}
39986
+ return (totalCount as text) & ${AS_RECORD_SEP} & (resultList as text)
39987
+ `;
39988
+ }
39989
+ const dateFetch = dateSetup ? `set noteDates to modification date of ${fullSource}
39990
+ ` : "";
39991
+ const dateCountGuard = dateSetup ? `${countGuard("noteDates")}
39992
+ ` : "";
39993
+ const dateGuardOpen = dateSetup ? `if (item i of noteDates) >= thresholdDate then
39994
+ ` : "";
39995
+ const dateGuardClose = dateSetup ? `
39996
+ end if` : "";
39997
+ return `
39998
+ ${dateSetup ?? ""}set noteNames to name of ${fullSource}
39999
+ set noteIds to id of ${fullSource}
40000
+ ${dateFetch}${countGuard("noteIds")}
40001
+ ${dateCountGuard}set resultList to {}
40002
+ repeat with i from 1 to count of noteNames
40003
+ ${dateGuardOpen}set end of resultList to (item i of noteNames) & ${AS_FIELD_SEP} & (item i of noteIds)${dateGuardClose}
40004
+ end repeat
40005
+ set AppleScript's text item delimiters to ${AS_RECORD_SEP}
40006
+ return resultList as text
40007
+ `;
40008
+ }
40009
+ /**
40010
+ * Parses bulk listing output into deduplicated note titles.
40011
+ *
40012
+ * Duplicate CoreData references are deduped by id; the limit is applied
40013
+ * after dedup so duplicates never count against it.
40014
+ */
40015
+ parseBulkListOutput(output, safeLimit) {
40016
+ if (!output.trim()) return [];
40017
+ const seenIds = /* @__PURE__ */ new Set();
40018
+ const titles = [];
40019
+ for (const item of output.split(RECORD_SEP)) {
40020
+ const [title, id] = item.split(FIELD_SEP);
40021
+ if (!title?.trim()) continue;
40022
+ const noteId = id?.trim() || generateFallbackId();
40023
+ if (seenIds.has(noteId)) continue;
40024
+ seenIds.add(noteId);
40025
+ titles.push(title.trim());
40026
+ if (safeLimit !== void 0 && titles.length >= safeLimit) break;
40027
+ }
40028
+ return titles;
40029
+ }
39891
40030
  /**
39892
40031
  * Lists all notes in an account, optionally filtered by folder, date, and limit.
39893
40032
  *
@@ -39900,89 +40039,41 @@ var AppleNotesManager = class {
39900
40039
  listNotes(account, folder, modifiedSince, limit) {
39901
40040
  const targetAccount = this.resolveAccount(account);
39902
40041
  const safeLimit = limit !== void 0 && limit > 0 ? Math.floor(limit) : void 0;
39903
- if (modifiedSince || safeLimit !== void 0) {
39904
- const baseNotesSource = folder ? `notes of ${buildFolderReference(folder)}` : "notes";
39905
- let dateSetup = "";
39906
- let notesSource = baseNotesSource;
39907
- if (modifiedSince) {
39908
- const date3 = new Date(modifiedSince);
39909
- if (!isNaN(date3.getTime())) {
39910
- dateSetup = buildAppleScriptDateVar(date3) + "\n";
39911
- notesSource = `(${baseNotesSource} whose modification date >= thresholdDate)`;
39912
- }
39913
- }
39914
- const limitCheck = safeLimit !== void 0 ? `
39915
- if (count of resultList) >= ${safeLimit} then exit repeat` : "";
39916
- const listCommand2 = `
39917
- ${dateSetup}set resultList to {}
39918
- set seenIds to {}
39919
- repeat with n in ${notesSource}
39920
- try
39921
- set noteName to name of n
39922
- set noteId to id of n
39923
- if seenIds does not contain noteId then
39924
- set end of seenIds to noteId
39925
- set end of resultList to noteName & ${AS_FIELD_SEP} & noteId${limitCheck}
39926
- end if
39927
- end try
39928
- end repeat
39929
- set AppleScript's text item delimiters to ${AS_RECORD_SEP}
39930
- return resultList as text
39931
- `;
39932
- const script2 = buildAccountScopedScript({ account: targetAccount }, listCommand2);
40042
+ const folderRef = folder ? buildFolderReference(folder) : void 0;
40043
+ let dateSetup;
40044
+ if (modifiedSince) {
40045
+ const date3 = new Date(modifiedSince);
40046
+ if (!isNaN(date3.getTime())) {
40047
+ dateSetup = buildAppleScriptDateVar(date3) + "\n";
40048
+ }
40049
+ }
40050
+ if (safeLimit !== void 0 && !dateSetup) {
40051
+ const script2 = buildAccountScopedScript(
40052
+ { account: targetAccount },
40053
+ this.buildBulkListCommand({ folderRef, sliceLimit: safeLimit })
40054
+ );
39933
40055
  const result2 = executeAppleScript(script2);
39934
40056
  if (!result2.success) {
39935
40057
  throw new Error(`Failed to list notes: ${result2.error ?? "unknown error"}`);
39936
40058
  }
39937
- if (!result2.output.trim()) {
39938
- return [];
40059
+ const sepIdx = result2.output.indexOf(RECORD_SEP);
40060
+ const header = sepIdx === -1 ? result2.output : result2.output.slice(0, sepIdx);
40061
+ const totalCount = Number.parseInt(header.trim(), 10);
40062
+ const records = sepIdx === -1 ? "" : result2.output.slice(sepIdx + 1);
40063
+ const titles = this.parseBulkListOutput(records, safeLimit);
40064
+ if (!Number.isNaN(totalCount) && (titles.length >= safeLimit || totalCount <= safeLimit)) {
40065
+ return titles;
39939
40066
  }
39940
- const seenIds2 = /* @__PURE__ */ new Set();
39941
- const titles2 = [];
39942
- for (const item of result2.output.split(RECORD_SEP)) {
39943
- const [title, id] = item.split(FIELD_SEP);
39944
- if (!title?.trim()) continue;
39945
- const noteId = id?.trim() || generateFallbackId();
39946
- if (seenIds2.has(noteId)) continue;
39947
- seenIds2.add(noteId);
39948
- titles2.push(title.trim());
39949
- }
39950
- return titles2;
39951
40067
  }
39952
- const notesRef = folder ? `notes of ${buildFolderReference(folder)}` : `notes`;
39953
- const listCommand = `
39954
- set resultList to {}
39955
- set seenIds to {}
39956
- repeat with n in ${notesRef}
39957
- try
39958
- set noteName to name of n
39959
- set noteId to id of n
39960
- if seenIds does not contain noteId then
39961
- set end of seenIds to noteId
39962
- set end of resultList to noteName & ${AS_FIELD_SEP} & noteId
39963
- end if
39964
- end try
39965
- end repeat
39966
- set AppleScript's text item delimiters to ${AS_RECORD_SEP}
39967
- return resultList as text
39968
- `;
39969
- const script = buildAccountScopedScript({ account: targetAccount }, listCommand);
40068
+ const script = buildAccountScopedScript(
40069
+ { account: targetAccount },
40070
+ this.buildBulkListCommand({ folderRef, dateSetup })
40071
+ );
39970
40072
  const result = executeAppleScript(script);
39971
40073
  if (!result.success) {
39972
40074
  throw new Error(`Failed to list notes: ${result.error ?? "unknown error"}`);
39973
40075
  }
39974
- if (!result.output.trim()) return [];
39975
- const seenIds = /* @__PURE__ */ new Set();
39976
- const titles = [];
39977
- for (const item of result.output.split(RECORD_SEP)) {
39978
- const [title, id] = item.split(FIELD_SEP);
39979
- if (!title?.trim()) continue;
39980
- const noteId = id?.trim() || generateFallbackId();
39981
- if (seenIds.has(noteId)) continue;
39982
- seenIds.add(noteId);
39983
- titles.push(title.trim());
39984
- }
39985
- return titles;
40076
+ return this.parseBulkListOutput(result.output, safeLimit);
39986
40077
  }
39987
40078
  /**
39988
40079
  * Lists all shared (collaborative) notes across all accounts.
@@ -40406,6 +40497,47 @@ var AppleNotesManager = class {
40406
40497
  }
40407
40498
  return true;
40408
40499
  }
40500
+ /**
40501
+ * Returns the notes:// deep-link URL for a note by its CoreData ID.
40502
+ *
40503
+ * Primary path: queries the Notes SQLite database for ZIDENTIFIER, which
40504
+ * is the UUID used in the notes://showNote?identifier= URL scheme. This
40505
+ * is more reliable than the AppleScript `note link` property, which is
40506
+ * absent from the Notes SDEF on macOS 26+.
40507
+ *
40508
+ * Fallback: AppleScript `note link` property (macOS 12–15).
40509
+ *
40510
+ * @param id - CoreData URL identifier for the note
40511
+ * @returns notes://showNote?identifier=<uuid> string, or null on failure
40512
+ */
40513
+ getNoteLinkById(id) {
40514
+ const note = this.getNoteById(id);
40515
+ if (!note) return null;
40516
+ if (note.passwordProtected) return null;
40517
+ const sqliteLink = getNoteLinkFromDB(id);
40518
+ if (sqliteLink) return sqliteLink;
40519
+ const safeId = sanitizeId(id);
40520
+ const result = executeAppleScript(
40521
+ buildAppLevelScript(`return note link of (note id "${safeId}")`)
40522
+ );
40523
+ if (result.success && result.output.trim()) {
40524
+ return result.output.trim();
40525
+ }
40526
+ console.error(`Failed to get note link for ID "${id}":`, result.error);
40527
+ return null;
40528
+ }
40529
+ /**
40530
+ * Returns the notes:// deep-link URL for a note by title.
40531
+ *
40532
+ * @param title - Exact note title
40533
+ * @param account - Account to search in (defaults to iCloud)
40534
+ * @returns notes://showNote?identifier=<uuid> string, or null on failure
40535
+ */
40536
+ getNoteLink(title, account) {
40537
+ const note = this.getNoteDetails(title, account);
40538
+ if (!note) return null;
40539
+ return this.getNoteLinkById(note.id);
40540
+ }
40409
40541
  /**
40410
40542
  * Reveals a folder in the Notes.app UI by its id.
40411
40543
  *
@@ -41697,12 +41829,12 @@ function formatDoctorReport(r) {
41697
41829
 
41698
41830
  // src/services/fileConfig.ts
41699
41831
  import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
41700
- import { join as join4 } from "path";
41701
- import { homedir as homedir5 } from "os";
41832
+ import { join as join5 } from "path";
41833
+ import { homedir as homedir6 } from "os";
41702
41834
  function fileConfigPath(env = process.env) {
41703
41835
  const override = env.APPLE_NOTES_MCP_CONFIG_FILE;
41704
41836
  if (override && override.trim()) return override.trim();
41705
- return join4(homedir5(), "Library", "Application Support", "apple-notes-mcp", "config.json");
41837
+ return join5(homedir6(), "Library", "Application Support", "apple-notes-mcp", "config.json");
41706
41838
  }
41707
41839
  function loadFileConfig(env = process.env, path4 = fileConfigPath(env)) {
41708
41840
  const applied = [];
@@ -42153,6 +42285,61 @@ server.registerTool(
42153
42285
  return successResponse(`Shown note with ID "${id}" in Notes.app`, { id, separately });
42154
42286
  }, "Error showing note")
42155
42287
  );
42288
+ server.registerTool(
42289
+ "get-note-link",
42290
+ {
42291
+ description: "Use when: you need the notes:// deep-link URL for a note so it can be stored in a Reminders task, shared, or opened directly.\nReturns: a notes://showNote?identifier=<uuid> URL that opens the note in Notes.app on iOS and macOS.\nDo not use when: you only need the note's CoreData id (get-note-by-id) or want to reveal the note on screen (show-note).\nNote: requires macOS 12+; returns an error on older systems.",
42292
+ inputSchema: {
42293
+ id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
42294
+ title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
42295
+ account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)")
42296
+ },
42297
+ outputSchema: {
42298
+ id: external_exports.string().optional(),
42299
+ title: external_exports.string().optional(),
42300
+ url: external_exports.string().optional()
42301
+ }
42302
+ },
42303
+ withErrorHandling(({ id, title, account }) => {
42304
+ if (id) {
42305
+ const note2 = notesManager.getNoteById(id);
42306
+ if (!note2) {
42307
+ return errorResponse(`Note with ID "${id}" not found`);
42308
+ }
42309
+ if (note2.passwordProtected) {
42310
+ return errorResponse(
42311
+ `Note "${note2.title}" is password-protected. Unlock it in Notes.app first.`
42312
+ );
42313
+ }
42314
+ const url2 = notesManager.getNoteLinkById(id);
42315
+ if (!url2) {
42316
+ return errorResponse(
42317
+ `Failed to get note link for "${note2.title}". The Notes database may not be accessible \u2014 grant Full Disk Access to the app that launches the server, fully quit and relaunch, then run the doctor tool. See: ${FULL_DISK_ACCESS_GUIDE_URL}. (On macOS 12\u201315 this also falls back to the AppleScript note link property.)`
42318
+ );
42319
+ }
42320
+ return successResponse(`Note link: ${url2}`, { id, title: note2.title, url: url2 });
42321
+ }
42322
+ if (!title) {
42323
+ return errorResponse("Either 'id' or 'title' is required");
42324
+ }
42325
+ const note = notesManager.getNoteDetails(title, account);
42326
+ if (!note) {
42327
+ return errorResponse(
42328
+ `Note "${title}" not found. Use search-notes to find notes, then use the note's ID for reliable operations.`
42329
+ );
42330
+ }
42331
+ if (note.passwordProtected) {
42332
+ return errorResponse(`Note "${title}" is password-protected. Unlock it in Notes.app first.`);
42333
+ }
42334
+ const url = notesManager.getNoteLink(title, account);
42335
+ if (!url) {
42336
+ return errorResponse(
42337
+ `Failed to get note link for "${title}". The Notes database may not be accessible \u2014 grant Full Disk Access to the app that launches the server, fully quit and relaunch, then run the doctor tool. See: ${FULL_DISK_ACCESS_GUIDE_URL}. (On macOS 12\u201315 this also falls back to the AppleScript note link property.)`
42338
+ );
42339
+ }
42340
+ return successResponse(`Note link: ${url}`, { title, url });
42341
+ }, "Error getting note link")
42342
+ );
42156
42343
  server.registerTool(
42157
42344
  "show-folder",
42158
42345
  {
@@ -42269,6 +42456,121 @@ server.registerTool(
42269
42456
  });
42270
42457
  }, "Error updating note")
42271
42458
  );
42459
+ server.registerTool(
42460
+ "append-to-note",
42461
+ {
42462
+ description: "Use when: adding content to an existing note without replacing it, by id (preferred) or title.\nReturns: confirmation with the note id and title.\nDo not use when: creating a new note (create-note) or replacing the entire body (update-note).\nSafety: reads the existing body first, concatenates, then writes back. Run list-attachments first if the note may hold embedded files \u2014 a full-body rewrite can drop attachments.",
42463
+ inputSchema: {
42464
+ id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
42465
+ title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
42466
+ content: external_exports.string().min(1, "Content to append is required").max(MAX.CONTENT).describe("Text to append to the note body"),
42467
+ position: external_exports.enum(["after", "before"]).optional().default("after").describe(
42468
+ "Where to insert: 'after' appends to the end (default), 'before' prepends to the start"
42469
+ ),
42470
+ separator: external_exports.string().max(20).optional().default("\n\n").describe("String placed between existing content and new content (default: two newlines)"),
42471
+ format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Format of the content being appended: 'plaintext' (default) or 'html'"),
42472
+ account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)")
42473
+ },
42474
+ outputSchema: {
42475
+ ok: external_exports.boolean().optional(),
42476
+ id: external_exports.string().optional(),
42477
+ title: external_exports.string().optional(),
42478
+ shared: external_exports.boolean().optional()
42479
+ }
42480
+ },
42481
+ withErrorHandling(
42482
+ ({
42483
+ id,
42484
+ title,
42485
+ content,
42486
+ position = "after",
42487
+ separator = "\n\n",
42488
+ format = "plaintext",
42489
+ account
42490
+ }) => {
42491
+ const contentToHtml = (text) => {
42492
+ if (format === "html") return text;
42493
+ return text.split("\n").map((line) => {
42494
+ const escaped = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
42495
+ return `<div>${escaped || "<br>"}</div>`;
42496
+ }).join("");
42497
+ };
42498
+ const separatorToHtml = (sep2) => {
42499
+ if (format === "html") return sep2;
42500
+ if (sep2 === "\n\n") return "<div><br></div>";
42501
+ const escaped = sep2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
42502
+ return `<div>${escaped}</div>`;
42503
+ };
42504
+ if (id) {
42505
+ const note2 = notesManager.getNoteById(id);
42506
+ if (!note2) {
42507
+ return errorResponse(`Note with ID "${id}" not found`);
42508
+ }
42509
+ if (note2.passwordProtected) {
42510
+ return errorResponse(
42511
+ `Note "${note2.title}" is password-protected and cannot be updated. Unlock it in Notes.app first.`
42512
+ );
42513
+ }
42514
+ const existingHtml2 = notesManager.getNoteContentById(id);
42515
+ if (existingHtml2 === null || existingHtml2 === void 0) {
42516
+ return errorResponse(`Failed to read content of note "${note2.title}"`);
42517
+ }
42518
+ const firstDivEnd2 = existingHtml2.indexOf("</div>");
42519
+ const titleDiv2 = firstDivEnd2 !== -1 ? existingHtml2.slice(0, firstDivEnd2 + 6) : "";
42520
+ const bodyHtml2 = firstDivEnd2 !== -1 ? existingHtml2.slice(firstDivEnd2 + 6) : existingHtml2;
42521
+ const newBlock2 = contentToHtml(content);
42522
+ const sepHtml2 = separatorToHtml(separator);
42523
+ const combinedBody2 = position === "before" ? titleDiv2 + newBlock2 + sepHtml2 + bodyHtml2 : titleDiv2 + bodyHtml2 + sepHtml2 + newBlock2;
42524
+ const success2 = notesManager.updateNoteById(id, void 0, combinedBody2, "html");
42525
+ if (!success2) {
42526
+ return errorResponse(`Failed to append to note "${note2.title}"`);
42527
+ }
42528
+ const sharedWarning2 = note2.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes will be visible to them." : "";
42529
+ return successResponse(`Note appended: "${note2.title}"${sharedWarning2}`, {
42530
+ ok: true,
42531
+ id,
42532
+ title: note2.title,
42533
+ shared: note2.shared ?? false
42534
+ });
42535
+ }
42536
+ if (!title) {
42537
+ return errorResponse("Either 'id' or 'title' is required");
42538
+ }
42539
+ const note = notesManager.getNoteDetails(title, account);
42540
+ if (!note) {
42541
+ return errorResponse(
42542
+ `Note "${title}" not found. Use search-notes to find notes, then use the note's ID for reliable operations.`
42543
+ );
42544
+ }
42545
+ if (note.passwordProtected) {
42546
+ return errorResponse(
42547
+ `Note "${title}" is password-protected and cannot be updated. Unlock it in Notes.app first.`
42548
+ );
42549
+ }
42550
+ const existingHtml = notesManager.getNoteContent(title, account);
42551
+ if (existingHtml === null || existingHtml === void 0) {
42552
+ return errorResponse(`Failed to read content of note "${title}"`);
42553
+ }
42554
+ const firstDivEnd = existingHtml.indexOf("</div>");
42555
+ const titleDiv = firstDivEnd !== -1 ? existingHtml.slice(0, firstDivEnd + 6) : "";
42556
+ const bodyHtml = firstDivEnd !== -1 ? existingHtml.slice(firstDivEnd + 6) : existingHtml;
42557
+ const newBlock = contentToHtml(content);
42558
+ const sepHtml = separatorToHtml(separator);
42559
+ const combinedBody = position === "before" ? titleDiv + newBlock + sepHtml + bodyHtml : titleDiv + bodyHtml + sepHtml + newBlock;
42560
+ const success = notesManager.updateNote(title, void 0, combinedBody, account, "html");
42561
+ if (!success) {
42562
+ return errorResponse(`Failed to append to note "${title}"`);
42563
+ }
42564
+ const sharedWarning = note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes will be visible to them." : "";
42565
+ return successResponse(`Note appended: "${title}"${sharedWarning}`, {
42566
+ ok: true,
42567
+ title,
42568
+ shared: note.shared ?? false
42569
+ });
42570
+ },
42571
+ "Error appending to note"
42572
+ )
42573
+ );
42272
42574
  server.registerTool(
42273
42575
  "delete-note",
42274
42576
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.5.12",
3
+ "version": "2.6.1",
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",