apple-notes-mcp 2.5.11 → 2.6.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.
Files changed (3) hide show
  1. package/README.md +65 -0
  2. package/build/index.js +254 -10
  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);
@@ -39200,6 +39206,8 @@ function cleanupTempDir(dir) {
39200
39206
  // src/services/appleNotesManager.ts
39201
39207
  var import_turndown = __toESM(require_turndown_cjs(), 1);
39202
39208
  import { existsSync as existsSync3 } from "fs";
39209
+ import { homedir as homedir3 } from "os";
39210
+ import { join as join2 } from "path";
39203
39211
  var FIELD_SEP = "";
39204
39212
  var RECORD_SEP = "";
39205
39213
  var AS_FIELD_SEP = "(ASCII character 31)";
@@ -39342,6 +39350,27 @@ function buildAppLevelScript(command) {
39342
39350
  end tell
39343
39351
  `;
39344
39352
  }
39353
+ function getNoteLinkFromDB(coreDataId) {
39354
+ const match = coreDataId.match(/\/p(\d+)$/);
39355
+ if (!match) return null;
39356
+ const pk = parseInt(match[1], 10);
39357
+ const dbPath = join2(homedir3(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
39358
+ if (!existsSync3(dbPath)) return null;
39359
+ try {
39360
+ const { DatabaseSync } = __require("node:sqlite");
39361
+ const db = new DatabaseSync(dbPath, { readOnly: true });
39362
+ try {
39363
+ const row = db.prepare("SELECT ZIDENTIFIER FROM ZICCLOUDSYNCINGOBJECT WHERE Z_PK = ?").get(pk);
39364
+ const identifier = row?.ZIDENTIFIER;
39365
+ return identifier ? `notes://showNote?identifier=${identifier}` : null;
39366
+ } finally {
39367
+ db.close();
39368
+ }
39369
+ } catch (err) {
39370
+ console.error("getNoteLinkFromDB: failed to query Notes database:", err);
39371
+ return null;
39372
+ }
39373
+ }
39345
39374
  function extractCoreDataId(output, prefix) {
39346
39375
  const pattern = new RegExp(`${prefix} id ([^\\s]+)`);
39347
39376
  const match = output.match(pattern);
@@ -40406,6 +40435,47 @@ var AppleNotesManager = class {
40406
40435
  }
40407
40436
  return true;
40408
40437
  }
40438
+ /**
40439
+ * Returns the notes:// deep-link URL for a note by its CoreData ID.
40440
+ *
40441
+ * Primary path: queries the Notes SQLite database for ZIDENTIFIER, which
40442
+ * is the UUID used in the notes://showNote?identifier= URL scheme. This
40443
+ * is more reliable than the AppleScript `note link` property, which is
40444
+ * absent from the Notes SDEF on macOS 26+.
40445
+ *
40446
+ * Fallback: AppleScript `note link` property (macOS 12–15).
40447
+ *
40448
+ * @param id - CoreData URL identifier for the note
40449
+ * @returns notes://showNote?identifier=<uuid> string, or null on failure
40450
+ */
40451
+ getNoteLinkById(id) {
40452
+ const note = this.getNoteById(id);
40453
+ if (!note) return null;
40454
+ if (note.passwordProtected) return null;
40455
+ const sqliteLink = getNoteLinkFromDB(id);
40456
+ if (sqliteLink) return sqliteLink;
40457
+ const safeId = sanitizeId(id);
40458
+ const result = executeAppleScript(
40459
+ buildAppLevelScript(`return note link of (note id "${safeId}")`)
40460
+ );
40461
+ if (result.success && result.output.trim()) {
40462
+ return result.output.trim();
40463
+ }
40464
+ console.error(`Failed to get note link for ID "${id}":`, result.error);
40465
+ return null;
40466
+ }
40467
+ /**
40468
+ * Returns the notes:// deep-link URL for a note by title.
40469
+ *
40470
+ * @param title - Exact note title
40471
+ * @param account - Account to search in (defaults to iCloud)
40472
+ * @returns notes://showNote?identifier=<uuid> string, or null on failure
40473
+ */
40474
+ getNoteLink(title, account) {
40475
+ const note = this.getNoteDetails(title, account);
40476
+ if (!note) return null;
40477
+ return this.getNoteLinkById(note.id);
40478
+ }
40409
40479
  /**
40410
40480
  * Reveals a folder in the Notes.app UI by its id.
40411
40481
  *
@@ -41396,9 +41466,13 @@ function getSyncStatus(useCache = true) {
41396
41466
  status.recentActivity = secondsAgo < RECENT_ACTIVITY_THRESHOLD_SECONDS;
41397
41467
  }
41398
41468
  const query = `
41399
- SELECT COUNT(*) FROM ZICCLOUDSTATE
41400
- WHERE ZCURRENTLOCALVERSION > ZLATESTVERSIONSYNCEDTOCLOUD
41401
- AND ZLATESTVERSIONSYNCEDTOCLOUD IS NOT NULL;
41469
+ SELECT COUNT(*) FROM ZICCLOUDSTATE state
41470
+ WHERE state.ZCURRENTLOCALVERSION > state.ZLATESTVERSIONSYNCEDTOCLOUD
41471
+ AND state.ZLATESTVERSIONSYNCEDTOCLOUD IS NOT NULL
41472
+ AND EXISTS (
41473
+ SELECT 1 FROM ZICCLOUDSYNCINGOBJECT object
41474
+ WHERE object.ZCLOUDSTATE = state.Z_PK
41475
+ );
41402
41476
  `;
41403
41477
  const result = execFileSync3(
41404
41478
  "sqlite3",
@@ -41693,12 +41767,12 @@ function formatDoctorReport(r) {
41693
41767
 
41694
41768
  // src/services/fileConfig.ts
41695
41769
  import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
41696
- import { join as join4 } from "path";
41697
- import { homedir as homedir5 } from "os";
41770
+ import { join as join5 } from "path";
41771
+ import { homedir as homedir6 } from "os";
41698
41772
  function fileConfigPath(env = process.env) {
41699
41773
  const override = env.APPLE_NOTES_MCP_CONFIG_FILE;
41700
41774
  if (override && override.trim()) return override.trim();
41701
- return join4(homedir5(), "Library", "Application Support", "apple-notes-mcp", "config.json");
41775
+ return join5(homedir6(), "Library", "Application Support", "apple-notes-mcp", "config.json");
41702
41776
  }
41703
41777
  function loadFileConfig(env = process.env, path4 = fileConfigPath(env)) {
41704
41778
  const applied = [];
@@ -42149,6 +42223,61 @@ server.registerTool(
42149
42223
  return successResponse(`Shown note with ID "${id}" in Notes.app`, { id, separately });
42150
42224
  }, "Error showing note")
42151
42225
  );
42226
+ server.registerTool(
42227
+ "get-note-link",
42228
+ {
42229
+ 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.",
42230
+ inputSchema: {
42231
+ id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
42232
+ title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
42233
+ account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)")
42234
+ },
42235
+ outputSchema: {
42236
+ id: external_exports.string().optional(),
42237
+ title: external_exports.string().optional(),
42238
+ url: external_exports.string().optional()
42239
+ }
42240
+ },
42241
+ withErrorHandling(({ id, title, account }) => {
42242
+ if (id) {
42243
+ const note2 = notesManager.getNoteById(id);
42244
+ if (!note2) {
42245
+ return errorResponse(`Note with ID "${id}" not found`);
42246
+ }
42247
+ if (note2.passwordProtected) {
42248
+ return errorResponse(
42249
+ `Note "${note2.title}" is password-protected. Unlock it in Notes.app first.`
42250
+ );
42251
+ }
42252
+ const url2 = notesManager.getNoteLinkById(id);
42253
+ if (!url2) {
42254
+ return errorResponse(
42255
+ `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.)`
42256
+ );
42257
+ }
42258
+ return successResponse(`Note link: ${url2}`, { id, title: note2.title, url: url2 });
42259
+ }
42260
+ if (!title) {
42261
+ return errorResponse("Either 'id' or 'title' is required");
42262
+ }
42263
+ const note = notesManager.getNoteDetails(title, account);
42264
+ if (!note) {
42265
+ return errorResponse(
42266
+ `Note "${title}" not found. Use search-notes to find notes, then use the note's ID for reliable operations.`
42267
+ );
42268
+ }
42269
+ if (note.passwordProtected) {
42270
+ return errorResponse(`Note "${title}" is password-protected. Unlock it in Notes.app first.`);
42271
+ }
42272
+ const url = notesManager.getNoteLink(title, account);
42273
+ if (!url) {
42274
+ return errorResponse(
42275
+ `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.)`
42276
+ );
42277
+ }
42278
+ return successResponse(`Note link: ${url}`, { title, url });
42279
+ }, "Error getting note link")
42280
+ );
42152
42281
  server.registerTool(
42153
42282
  "show-folder",
42154
42283
  {
@@ -42265,6 +42394,121 @@ server.registerTool(
42265
42394
  });
42266
42395
  }, "Error updating note")
42267
42396
  );
42397
+ server.registerTool(
42398
+ "append-to-note",
42399
+ {
42400
+ 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.",
42401
+ inputSchema: {
42402
+ id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
42403
+ title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
42404
+ content: external_exports.string().min(1, "Content to append is required").max(MAX.CONTENT).describe("Text to append to the note body"),
42405
+ position: external_exports.enum(["after", "before"]).optional().default("after").describe(
42406
+ "Where to insert: 'after' appends to the end (default), 'before' prepends to the start"
42407
+ ),
42408
+ separator: external_exports.string().max(20).optional().default("\n\n").describe("String placed between existing content and new content (default: two newlines)"),
42409
+ format: external_exports.enum(["plaintext", "html"]).optional().default("plaintext").describe("Format of the content being appended: 'plaintext' (default) or 'html'"),
42410
+ account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)")
42411
+ },
42412
+ outputSchema: {
42413
+ ok: external_exports.boolean().optional(),
42414
+ id: external_exports.string().optional(),
42415
+ title: external_exports.string().optional(),
42416
+ shared: external_exports.boolean().optional()
42417
+ }
42418
+ },
42419
+ withErrorHandling(
42420
+ ({
42421
+ id,
42422
+ title,
42423
+ content,
42424
+ position = "after",
42425
+ separator = "\n\n",
42426
+ format = "plaintext",
42427
+ account
42428
+ }) => {
42429
+ const contentToHtml = (text) => {
42430
+ if (format === "html") return text;
42431
+ return text.split("\n").map((line) => {
42432
+ const escaped = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
42433
+ return `<div>${escaped || "<br>"}</div>`;
42434
+ }).join("");
42435
+ };
42436
+ const separatorToHtml = (sep2) => {
42437
+ if (format === "html") return sep2;
42438
+ if (sep2 === "\n\n") return "<div><br></div>";
42439
+ const escaped = sep2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
42440
+ return `<div>${escaped}</div>`;
42441
+ };
42442
+ if (id) {
42443
+ const note2 = notesManager.getNoteById(id);
42444
+ if (!note2) {
42445
+ return errorResponse(`Note with ID "${id}" not found`);
42446
+ }
42447
+ if (note2.passwordProtected) {
42448
+ return errorResponse(
42449
+ `Note "${note2.title}" is password-protected and cannot be updated. Unlock it in Notes.app first.`
42450
+ );
42451
+ }
42452
+ const existingHtml2 = notesManager.getNoteContentById(id);
42453
+ if (existingHtml2 === null || existingHtml2 === void 0) {
42454
+ return errorResponse(`Failed to read content of note "${note2.title}"`);
42455
+ }
42456
+ const firstDivEnd2 = existingHtml2.indexOf("</div>");
42457
+ const titleDiv2 = firstDivEnd2 !== -1 ? existingHtml2.slice(0, firstDivEnd2 + 6) : "";
42458
+ const bodyHtml2 = firstDivEnd2 !== -1 ? existingHtml2.slice(firstDivEnd2 + 6) : existingHtml2;
42459
+ const newBlock2 = contentToHtml(content);
42460
+ const sepHtml2 = separatorToHtml(separator);
42461
+ const combinedBody2 = position === "before" ? titleDiv2 + newBlock2 + sepHtml2 + bodyHtml2 : titleDiv2 + bodyHtml2 + sepHtml2 + newBlock2;
42462
+ const success2 = notesManager.updateNoteById(id, void 0, combinedBody2, "html");
42463
+ if (!success2) {
42464
+ return errorResponse(`Failed to append to note "${note2.title}"`);
42465
+ }
42466
+ const sharedWarning2 = note2.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes will be visible to them." : "";
42467
+ return successResponse(`Note appended: "${note2.title}"${sharedWarning2}`, {
42468
+ ok: true,
42469
+ id,
42470
+ title: note2.title,
42471
+ shared: note2.shared ?? false
42472
+ });
42473
+ }
42474
+ if (!title) {
42475
+ return errorResponse("Either 'id' or 'title' is required");
42476
+ }
42477
+ const note = notesManager.getNoteDetails(title, account);
42478
+ if (!note) {
42479
+ return errorResponse(
42480
+ `Note "${title}" not found. Use search-notes to find notes, then use the note's ID for reliable operations.`
42481
+ );
42482
+ }
42483
+ if (note.passwordProtected) {
42484
+ return errorResponse(
42485
+ `Note "${title}" is password-protected and cannot be updated. Unlock it in Notes.app first.`
42486
+ );
42487
+ }
42488
+ const existingHtml = notesManager.getNoteContent(title, account);
42489
+ if (existingHtml === null || existingHtml === void 0) {
42490
+ return errorResponse(`Failed to read content of note "${title}"`);
42491
+ }
42492
+ const firstDivEnd = existingHtml.indexOf("</div>");
42493
+ const titleDiv = firstDivEnd !== -1 ? existingHtml.slice(0, firstDivEnd + 6) : "";
42494
+ const bodyHtml = firstDivEnd !== -1 ? existingHtml.slice(firstDivEnd + 6) : existingHtml;
42495
+ const newBlock = contentToHtml(content);
42496
+ const sepHtml = separatorToHtml(separator);
42497
+ const combinedBody = position === "before" ? titleDiv + newBlock + sepHtml + bodyHtml : titleDiv + bodyHtml + sepHtml + newBlock;
42498
+ const success = notesManager.updateNote(title, void 0, combinedBody, account, "html");
42499
+ if (!success) {
42500
+ return errorResponse(`Failed to append to note "${title}"`);
42501
+ }
42502
+ const sharedWarning = note.shared ? "\n\n\u26A0\uFE0F This note is shared with collaborators. Your changes will be visible to them." : "";
42503
+ return successResponse(`Note appended: "${title}"${sharedWarning}`, {
42504
+ ok: true,
42505
+ title,
42506
+ shared: note.shared ?? false
42507
+ });
42508
+ },
42509
+ "Error appending to note"
42510
+ )
42511
+ );
42268
42512
  server.registerTool(
42269
42513
  "delete-note",
42270
42514
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.5.11",
3
+ "version": "2.6.0",
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",