apple-notes-mcp 2.8.25 → 2.8.26

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 +25 -8
  2. package/build/index.js +438 -135
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -201,6 +201,20 @@ for the investigation and verification behind each:
201
201
 
202
202
  This section documents all available tools. AI agents should use these tool names and parameters exactly as specified.
203
203
 
204
+ ### Identifier forms
205
+
206
+ Every tool that takes a note id (`id`, `noteId`, `ids`, `linkedNoteId`, or the `id` inside a batch entry) accepts any of three forms of the same note:
207
+
208
+ | Form | Example shape | Needs Full Disk Access |
209
+ |------|---------------|------------------------|
210
+ | AppleScript id (canonical) | `x-coredata://<store-uuid>/ICNote/p123` | No |
211
+ | Notes UUID (`identifier`) | `8-4-4-4-12` hex digits, as in `notes://showNote?identifier=` links | Yes |
212
+ | Numeric Core Data key | `123` (the digits after `p`) | Yes |
213
+
214
+ The server turns a UUID or numeric key into the canonical id before the tool runs, reading the Notes database read-only. A numeric key or UUID resolves only to a note, never to a folder or attachment. Without Full Disk Access those two forms fail with an error that says so, while `x-coredata` ids keep working as before. Folder-id inputs (`show-folder`, `get-folder-by-id`, `rename-folder`) accept a folder's UUID or numeric key the same way, resolving only to folders.
215
+
216
+ When Full Disk Access is granted, list and read tools also return stable identifiers next to each `id`: notes carry `identifier`, `folderIdentifier`, and `accountIdentifier`; folders carry `identifier`, `parentIdentifier` (nested folders only), and `accountIdentifier`; accounts carry `identifier`. These fields come from one batched read-only query per call and are omitted when the database cannot be read. Tools that return them: `search-notes`, `list-notes`, `get-selected-notes`, `list-shared-notes`, `get-note-content`, `get-note-by-id`, `get-note-details`, `list-folders`, `get-folder-by-id`, `list-accounts`, and `get-default-location`.
217
+
204
218
  ### Error results
205
219
 
206
220
  A failed call returns `isError: true` with the same human-readable text as
@@ -583,9 +597,9 @@ Retrieves a note using its unique CoreData identifier.
583
597
 
584
598
  | Parameter | Type | Required | Description |
585
599
  |-----------|------|----------|-------------|
586
- | `id` | string | Yes | The CoreData URL identifier (e.g., `x-coredata://...`) |
600
+ | `id` | string | Yes | The CoreData URL identifier (e.g., `x-coredata://...`), or the note's Notes UUID or numeric key (see [Identifier forms](#identifier-forms)) |
587
601
 
588
- **Returns:** JSON with note metadata, or error if not found.
602
+ **Returns:** JSON with note metadata, plus `identifier`, `folderIdentifier`, and `accountIdentifier` when Full Disk Access is granted, or error if not found.
589
603
 
590
604
  ---
591
605
 
@@ -595,7 +609,7 @@ Reveals a note in Notes.app using its unique CoreData identifier.
595
609
 
596
610
  | Parameter | Type | Required | Description |
597
611
  |-----------|------|----------|-------------|
598
- | `id` | string | Yes | The CoreData URL identifier (e.g., `x-coredata://...`) |
612
+ | `id` | string | Yes | The CoreData URL identifier (e.g., `x-coredata://...`), or the note's Notes UUID or numeric key |
599
613
  | `separately` | boolean | No | Open in a separate note window when supported by Notes.app |
600
614
 
601
615
  **Returns:** Confirmation that Notes.app accepted the show command.
@@ -879,7 +893,7 @@ Lists all notes, optionally filtered by folder, date, and limit.
879
893
  }
880
894
  ```
881
895
 
882
- **Returns:** List of notes as `{title, id}` pairs — `notes: Array<{title, id}>`, plus `count`. The human-readable line is ` - <title> [id: <id>]`.
896
+ **Returns:** List of notes as `{title, id}` pairs — `notes: Array<{title, id}>`, plus `count`. The human-readable line is ` - <title> [id: <id>]`. With Full Disk Access each entry also carries `identifier`, `folderIdentifier`, and `accountIdentifier` (see [Identifier forms](#identifier-forms)).
883
897
 
884
898
  Use the returned `id` for any follow-up read/update/move/delete rather than re-resolving the title: titles are not unique, and a by-title lookup resolves a duplicated title to the same one note every time, silently skipping the others.
885
899
 
@@ -912,7 +926,7 @@ Lists all folders in an account with full hierarchical paths.
912
926
  {}
913
927
  ```
914
928
 
915
- **Returns:** List of folders with IDs, paths, account names, and shared state. Nested folders are shown as full paths (e.g., `Work/Clients/Omnia`). Duplicate folder names are disambiguated by their full path. Literal slashes in folder names are escaped as `\/` (e.g., `Spain\/Portugal 2023`).
929
+ **Returns:** List of folders with IDs, paths, account names, and shared state, plus `identifier`, `parentIdentifier`, and `accountIdentifier` when Full Disk Access is granted. Nested folders are shown as full paths (e.g., `Work/Clients/Omnia`). Duplicate folder names are disambiguated by their full path. Literal slashes in folder names are escaped as `\/` (e.g., `Spain\/Portugal 2023`).
916
930
 
917
931
  ---
918
932
 
@@ -946,7 +960,10 @@ Creates a new folder, including a whole nested hierarchy in one call.
946
960
  #### `get-folder-by-id`
947
961
 
948
962
  Reads one exact folder's current name and parent ID. Use these values with
949
- `rename-folder`; this avoids relying on ambiguous folder names or paths.
963
+ `rename-folder`; this avoids relying on ambiguous folder names or paths. The
964
+ `id` may also be the folder's Notes UUID or numeric key, and the result adds
965
+ `identifier`, `parentIdentifier`, and `accountIdentifier` when Full Disk Access
966
+ is granted.
950
967
 
951
968
  ---
952
969
 
@@ -986,7 +1003,7 @@ Reveals a folder in Notes.app using its unique CoreData identifier.
986
1003
 
987
1004
  | Parameter | Type | Required | Description |
988
1005
  |-----------|------|----------|-------------|
989
- | `id` | string | Yes | The folder's CoreData identifier (from `list-folders`) |
1006
+ | `id` | string | Yes | The folder's CoreData identifier (from `list-folders`), or its Notes UUID or numeric key |
990
1007
  | `separately` | boolean | No | Open in a separate window when supported by Notes.app |
991
1008
 
992
1009
  **Returns:** Confirmation that Notes.app accepted the show command.
@@ -1006,7 +1023,7 @@ Lists all configured Notes accounts.
1006
1023
  {}
1007
1024
  ```
1008
1025
 
1009
- **Returns:** List of accounts with names, IDs, upgraded state, and default folder metadata.
1026
+ **Returns:** List of accounts with names, IDs, upgraded state, and default folder metadata, plus each account's `identifier` (Notes UUID) when Full Disk Access is granted.
1010
1027
 
1011
1028
  ---
1012
1029
 
package/build/index.js CHANGED
@@ -3268,8 +3268,8 @@ var require_utils = __commonJS({
3268
3268
  }
3269
3269
  return ind;
3270
3270
  }
3271
- function removeDotSegments(path5) {
3272
- let input = path5;
3271
+ function removeDotSegments(path6) {
3272
+ let input = path6;
3273
3273
  const output = [];
3274
3274
  let nextSlash = -1;
3275
3275
  let len = 0;
@@ -3674,8 +3674,8 @@ var require_schemes = __commonJS({
3674
3674
  }
3675
3675
  if (wsComponent.resourceName) {
3676
3676
  const queryIndex = wsComponent.resourceName.indexOf("?");
3677
- const path5 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3678
- wsComponent.path = path5 && path5 !== "/" ? path5 : void 0;
3677
+ const path6 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3678
+ wsComponent.path = path6 && path6 !== "/" ? path6 : void 0;
3679
3679
  wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
3680
3680
  wsComponent.resourceName = void 0;
3681
3681
  }
@@ -7181,12 +7181,12 @@ var require_dist = __commonJS({
7181
7181
  throw new Error(`Unknown format "${name}"`);
7182
7182
  return f;
7183
7183
  };
7184
- function addFormats(ajv, list, fs5, exportName) {
7184
+ function addFormats(ajv, list, fs6, exportName) {
7185
7185
  var _a;
7186
7186
  var _b;
7187
7187
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
7188
7188
  for (const f of list)
7189
- ajv.addFormat(f, fs5[f]);
7189
+ ajv.addFormat(f, fs6[f]);
7190
7190
  }
7191
7191
  module.exports = exports = formatsPlugin;
7192
7192
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -12238,32 +12238,32 @@ var require_URL = __commonJS({
12238
12238
  else
12239
12239
  return basepath.substring(0, lastslash + 1) + refpath;
12240
12240
  }
12241
- function remove_dot_segments(path5) {
12242
- if (!path5) return path5;
12241
+ function remove_dot_segments(path6) {
12242
+ if (!path6) return path6;
12243
12243
  var output = "";
12244
- while (path5.length > 0) {
12245
- if (path5 === "." || path5 === "..") {
12246
- path5 = "";
12244
+ while (path6.length > 0) {
12245
+ if (path6 === "." || path6 === "..") {
12246
+ path6 = "";
12247
12247
  break;
12248
12248
  }
12249
- var twochars = path5.substring(0, 2);
12250
- var threechars = path5.substring(0, 3);
12251
- var fourchars = path5.substring(0, 4);
12249
+ var twochars = path6.substring(0, 2);
12250
+ var threechars = path6.substring(0, 3);
12251
+ var fourchars = path6.substring(0, 4);
12252
12252
  if (threechars === "../") {
12253
- path5 = path5.substring(3);
12253
+ path6 = path6.substring(3);
12254
12254
  } else if (twochars === "./") {
12255
- path5 = path5.substring(2);
12255
+ path6 = path6.substring(2);
12256
12256
  } else if (threechars === "/./") {
12257
- path5 = "/" + path5.substring(3);
12258
- } else if (twochars === "/." && path5.length === 2) {
12259
- path5 = "/";
12260
- } else if (fourchars === "/../" || threechars === "/.." && path5.length === 3) {
12261
- path5 = "/" + path5.substring(4);
12257
+ path6 = "/" + path6.substring(3);
12258
+ } else if (twochars === "/." && path6.length === 2) {
12259
+ path6 = "/";
12260
+ } else if (fourchars === "/../" || threechars === "/.." && path6.length === 3) {
12261
+ path6 = "/" + path6.substring(4);
12262
12262
  output = output.replace(/\/?[^\/]*$/, "");
12263
12263
  } else {
12264
- var segment = path5.match(/(\/?([^\/]*))/)[0];
12264
+ var segment = path6.match(/(\/?([^\/]*))/)[0];
12265
12265
  output += segment;
12266
- path5 = path5.substring(segment.length);
12266
+ path6 = path6.substring(segment.length);
12267
12267
  }
12268
12268
  }
12269
12269
  return output;
@@ -24465,14 +24465,14 @@ var require_turndown_cjs = __commonJS({
24465
24465
  } else if (node.nodeType === 1) {
24466
24466
  replacement = replacementForNode.call(self, node);
24467
24467
  }
24468
- return join12(output, replacement);
24468
+ return join13(output, replacement);
24469
24469
  }, "");
24470
24470
  }
24471
24471
  function postProcess(output) {
24472
24472
  var self = this;
24473
24473
  this.rules.forEach(function(rule) {
24474
24474
  if (typeof rule.append === "function") {
24475
- output = join12(output, rule.append(self.options));
24475
+ output = join13(output, rule.append(self.options));
24476
24476
  }
24477
24477
  });
24478
24478
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -24484,7 +24484,7 @@ var require_turndown_cjs = __commonJS({
24484
24484
  if (whitespace.leading || whitespace.trailing) content = content.trim();
24485
24485
  return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
24486
24486
  }
24487
- function join12(output, replacement) {
24487
+ function join13(output, replacement) {
24488
24488
  var s1 = trimTrailingNewlines(output);
24489
24489
  var s2 = trimLeadingNewlines(replacement);
24490
24490
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -24979,8 +24979,8 @@ function getErrorMap() {
24979
24979
 
24980
24980
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
24981
24981
  var makeIssue = (params) => {
24982
- const { data, path: path5, errorMaps, issueData } = params;
24983
- const fullPath = [...path5, ...issueData.path || []];
24982
+ const { data, path: path6, errorMaps, issueData } = params;
24983
+ const fullPath = [...path6, ...issueData.path || []];
24984
24984
  const fullIssue = {
24985
24985
  ...issueData,
24986
24986
  path: fullPath
@@ -25096,11 +25096,11 @@ var errorUtil;
25096
25096
 
25097
25097
  // node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
25098
25098
  var ParseInputLazyPath = class {
25099
- constructor(parent, value, path5, key) {
25099
+ constructor(parent, value, path6, key) {
25100
25100
  this._cachedPath = [];
25101
25101
  this.parent = parent;
25102
25102
  this.data = value;
25103
- this._path = path5;
25103
+ this._path = path6;
25104
25104
  this._key = key;
25105
25105
  }
25106
25106
  get path() {
@@ -28737,10 +28737,10 @@ function assignProp(target, prop, value) {
28737
28737
  configurable: true
28738
28738
  });
28739
28739
  }
28740
- function getElementAtPath(obj, path5) {
28741
- if (!path5)
28740
+ function getElementAtPath(obj, path6) {
28741
+ if (!path6)
28742
28742
  return obj;
28743
- return path5.reduce((acc, key) => acc?.[key], obj);
28743
+ return path6.reduce((acc, key) => acc?.[key], obj);
28744
28744
  }
28745
28745
  function promiseAllObject(promisesObj) {
28746
28746
  const keys = Object.keys(promisesObj);
@@ -29060,11 +29060,11 @@ function aborted(x, startIndex = 0) {
29060
29060
  }
29061
29061
  return false;
29062
29062
  }
29063
- function prefixIssues(path5, issues) {
29063
+ function prefixIssues(path6, issues) {
29064
29064
  return issues.map((iss) => {
29065
29065
  var _a;
29066
29066
  (_a = iss).path ?? (_a.path = []);
29067
- iss.path.unshift(path5);
29067
+ iss.path.unshift(path6);
29068
29068
  return iss;
29069
29069
  });
29070
29070
  }
@@ -32475,11 +32475,11 @@ function normalizeObjectSchema(schema) {
32475
32475
  }
32476
32476
  return void 0;
32477
32477
  }
32478
- function getDotPath(path5) {
32479
- if (path5.length === 0) {
32478
+ function getDotPath(path6) {
32479
+ if (path6.length === 0) {
32480
32480
  return "object root";
32481
32481
  }
32482
- return path5.reduce((acc, seg, index) => {
32482
+ return path6.reduce((acc, seg, index) => {
32483
32483
  if (index === 0) {
32484
32484
  return String(seg);
32485
32485
  }
@@ -39862,8 +39862,8 @@ function allowedSaveRoots() {
39862
39862
  "/private/tmp"
39863
39863
  ];
39864
39864
  }
39865
- function canonicalize(path5) {
39866
- return realpathSync.native(path5);
39865
+ function canonicalize(path6) {
39866
+ return realpathSync.native(path6);
39867
39867
  }
39868
39868
  function isWithinRoots(candidate, roots) {
39869
39869
  return roots.some((root) => {
@@ -39885,9 +39885,9 @@ function canonicalRoots(roots) {
39885
39885
  }
39886
39886
  return canonical;
39887
39887
  }
39888
- function entryExists(path5) {
39888
+ function entryExists(path6) {
39889
39889
  try {
39890
- lstatSync(path5);
39890
+ lstatSync(path6);
39891
39891
  return true;
39892
39892
  } catch (e) {
39893
39893
  const code = e.code;
@@ -40203,8 +40203,8 @@ function buildAppLevelScript(command) {
40203
40203
  end tell
40204
40204
  `;
40205
40205
  }
40206
- function getNoteLinkFromDB(coreDataId) {
40207
- const match = coreDataId.match(/\/p(\d+)$/);
40206
+ function getNoteLinkFromDB(coreDataId2) {
40207
+ const match = coreDataId2.match(/\/p(\d+)$/);
40208
40208
  if (!match) return null;
40209
40209
  const pk = parseInt(match[1], 10);
40210
40210
  const dbPath2 = join4(homedir4(), "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite");
@@ -41206,9 +41206,9 @@ var AppleNotesManager = class {
41206
41206
  );
41207
41207
  const idResult = executeAppleScript(idScript);
41208
41208
  const [rawId = "", rawAccount = ""] = idResult.success ? idResult.output.split(FIELD_SEP) : [];
41209
- const folderId = idResult.success ? extractCoreDataId(rawId, "folder") : "";
41209
+ const folderId2 = idResult.success ? extractCoreDataId(rawId, "folder") : "";
41210
41210
  return {
41211
- id: folderId,
41211
+ id: folderId2,
41212
41212
  name,
41213
41213
  account: rawAccount.trim() || targetAccount || ""
41214
41214
  };
@@ -42644,6 +42644,244 @@ function getNoteMetadata(noteId3) {
42644
42644
  }
42645
42645
  }
42646
42646
 
42647
+ // src/utils/noteIdentifiers.ts
42648
+ import { execFileSync as execFileSync6 } from "child_process";
42649
+ import * as fs4 from "fs";
42650
+ import * as os4 from "os";
42651
+ import * as path4 from "path";
42652
+ var NOTES_DB_PATH4 = path4.join(
42653
+ os4.homedir(),
42654
+ "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
42655
+ );
42656
+ var UUID_PATTERN = /^[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$/;
42657
+ var NUMERIC_KEY_PATTERN = /^\d{1,18}$/;
42658
+ var NOTE_ID_MESSAGE = "A canonical Apple Note ID is required (x-coredata://.../ICNote/p...), or the note's Notes UUID or numeric key";
42659
+ function identifierForm(value) {
42660
+ if (UUID_PATTERN.test(value)) return "uuid";
42661
+ if (NUMERIC_KEY_PATTERN.test(value)) return "key";
42662
+ return "other";
42663
+ }
42664
+ function isAlternateIdentifier(value) {
42665
+ return identifierForm(value) !== "other";
42666
+ }
42667
+ var IdentifierResolutionError = class extends Error {
42668
+ constructor(code, message) {
42669
+ super(message);
42670
+ this.code = code;
42671
+ this.name = "IdentifierResolutionError";
42672
+ }
42673
+ code;
42674
+ };
42675
+ var ENTITY_LABEL = {
42676
+ ICNote: "note",
42677
+ ICFolder: "folder",
42678
+ ICAccount: "account"
42679
+ };
42680
+ var NO_FDA_MESSAGE = `Resolving a Notes UUID or numeric key reads the Notes database, which needs Full Disk Access for the app that launches this server (System Settings > Privacy & Security > Full Disk Access, then fully quit and relaunch it). x-coredata ids from search-notes, list-notes, or list-folders work without it. Setup guide: ${FULL_DISK_ACCESS_GUIDE_URL}`;
42681
+ function canonicalKey(value) {
42682
+ return BigInt(value).toString();
42683
+ }
42684
+ function assertAll(values, pattern, label) {
42685
+ for (const value of values) {
42686
+ if (!pattern.test(value)) throw new Error(`Refusing to query with an invalid ${label}`);
42687
+ }
42688
+ }
42689
+ function entityClause(entity2) {
42690
+ return `(SELECT Z_ENT FROM Z_PRIMARYKEY WHERE Z_NAME = '${entity2}')`;
42691
+ }
42692
+ function buildResolveSql(entity2, keys, uuids) {
42693
+ assertAll(keys, NUMERIC_KEY_PATTERN, "numeric key");
42694
+ assertAll(uuids, UUID_PATTERN, "UUID");
42695
+ const matches = [];
42696
+ if (keys.length > 0) {
42697
+ matches.push(`o.Z_PK IN (${[...new Set(keys.map(canonicalKey))].join(", ")})`);
42698
+ }
42699
+ if (uuids.length > 0) {
42700
+ const variants = /* @__PURE__ */ new Set();
42701
+ for (const uuid2 of uuids) {
42702
+ variants.add(uuid2);
42703
+ variants.add(uuid2.toUpperCase());
42704
+ variants.add(uuid2.toLowerCase());
42705
+ }
42706
+ matches.push(`o.ZIDENTIFIER IN (${[...variants].map((v) => `'${v}'`).join(", ")})`);
42707
+ }
42708
+ const where = matches.length > 0 ? matches.join(" OR ") : "0";
42709
+ return `SELECT json_object('store', (SELECT Z_UUID FROM Z_METADATA LIMIT 1), 'rows', (SELECT json_group_array(json_object('pk', o.Z_PK, 'identifier', o.ZIDENTIFIER)) FROM ZICCLOUDSYNCINGOBJECT o WHERE o.Z_ENT = ${entityClause(entity2)} AND (${where})));`;
42710
+ }
42711
+ function buildLookupSql(entity2, keys) {
42712
+ assertAll(keys, NUMERIC_KEY_PATTERN, "numeric key");
42713
+ const pks = [...new Set(keys.map(canonicalKey))];
42714
+ const inList = pks.length > 0 ? pks.join(", ") : "NULL";
42715
+ let fields = "'pk', o.Z_PK, 'identifier', o.ZIDENTIFIER";
42716
+ let joins = "";
42717
+ if (entity2 === "ICNote") {
42718
+ fields += ", 'folderIdentifier', f.ZIDENTIFIER, 'accountIdentifier', a.ZIDENTIFIER";
42719
+ joins = "LEFT JOIN ZICCLOUDSYNCINGOBJECT f ON f.Z_PK = o.ZFOLDER LEFT JOIN ZICCLOUDSYNCINGOBJECT a ON a.Z_PK = f.ZOWNER ";
42720
+ } else if (entity2 === "ICFolder") {
42721
+ fields += ", 'parentIdentifier', p.ZIDENTIFIER, 'accountIdentifier', a.ZIDENTIFIER";
42722
+ joins = "LEFT JOIN ZICCLOUDSYNCINGOBJECT p ON p.Z_PK = o.ZPARENT LEFT JOIN ZICCLOUDSYNCINGOBJECT a ON a.Z_PK = o.ZOWNER ";
42723
+ }
42724
+ return `SELECT json_object('store', (SELECT Z_UUID FROM Z_METADATA LIMIT 1), 'rows', (SELECT json_group_array(json_object(${fields})) FROM ZICCLOUDSYNCINGOBJECT o ${joins}WHERE o.Z_ENT = ${entityClause(entity2)} AND o.Z_PK IN (${inList})));`;
42725
+ }
42726
+ function runJsonQuery(sql, dbPath2) {
42727
+ if (!fs4.existsSync(dbPath2)) throw new IdentifierResolutionError("no_fda", NO_FDA_MESSAGE);
42728
+ let out;
42729
+ try {
42730
+ out = execFileSync6("sqlite3", ["-readonly", dbPath2, sql], {
42731
+ encoding: "utf8",
42732
+ timeout: 5e3,
42733
+ stdio: ["pipe", "pipe", "pipe"]
42734
+ }).trim();
42735
+ } catch (error2) {
42736
+ const detail = error2 instanceof Error ? `${error2.message} ${String(error2.stderr ?? "")}` : String(error2);
42737
+ if (detail.includes("authorization denied") || detail.includes("unable to open database")) {
42738
+ throw new IdentifierResolutionError("no_fda", NO_FDA_MESSAGE);
42739
+ }
42740
+ console.error(`Identifier query failed: ${detail}`);
42741
+ throw new IdentifierResolutionError("query_error", "Failed to read the Notes database.");
42742
+ }
42743
+ const parsed = JSON.parse(out || "{}");
42744
+ const rows = typeof parsed.rows === "string" ? JSON.parse(parsed.rows) : parsed.rows;
42745
+ return { store: parsed.store ?? null, rows: rows ?? [] };
42746
+ }
42747
+ function coreDataId(store, entity2, pk) {
42748
+ return `x-coredata://${store}/${entity2}/p${pk}`;
42749
+ }
42750
+ function resolveIdentifiers(values, entity2, dbPath2 = NOTES_DB_PATH4) {
42751
+ const resolved = /* @__PURE__ */ new Map();
42752
+ const keys = [];
42753
+ const uuids = [];
42754
+ for (const value of values) {
42755
+ const form = identifierForm(value);
42756
+ if (form === "key") keys.push(value);
42757
+ else if (form === "uuid") uuids.push(value);
42758
+ else resolved.set(value, value);
42759
+ }
42760
+ if (keys.length === 0 && uuids.length === 0) return resolved;
42761
+ const { store, rows } = runJsonQuery(
42762
+ buildResolveSql(entity2, keys, uuids),
42763
+ dbPath2
42764
+ );
42765
+ if (!store) {
42766
+ throw new IdentifierResolutionError(
42767
+ "query_error",
42768
+ "The Notes database has no store UUID, so an x-coredata id cannot be built."
42769
+ );
42770
+ }
42771
+ const byKey = new Map(rows.map((row) => [String(row.pk), row]));
42772
+ const byUuid = new Map(
42773
+ rows.filter((row) => row.identifier).map((row) => [String(row.identifier).toUpperCase(), row])
42774
+ );
42775
+ const label = ENTITY_LABEL[entity2];
42776
+ const missing = [];
42777
+ for (const key of keys) {
42778
+ const row = byKey.get(canonicalKey(key));
42779
+ if (row) resolved.set(key, coreDataId(store, entity2, row.pk));
42780
+ else missing.push(`numeric key ${key}`);
42781
+ }
42782
+ for (const uuid2 of uuids) {
42783
+ const row = byUuid.get(uuid2.toUpperCase());
42784
+ if (row) resolved.set(uuid2, coreDataId(store, entity2, row.pk));
42785
+ else missing.push(`identifier ${uuid2}`);
42786
+ }
42787
+ if (missing.length > 0) {
42788
+ throw new IdentifierResolutionError(
42789
+ "not_found",
42790
+ `No ${label} found for ${missing.join(", ")}. A numeric key must belong to a ${label}, not another object type.`
42791
+ );
42792
+ }
42793
+ return resolved;
42794
+ }
42795
+ var COREDATA_PARTS = /^x-coredata:\/\/([0-9A-Fa-f-]+)\/(ICNote|ICFolder|ICAccount)\/p(\d{1,18})$/;
42796
+ function lookupStableIdentifiers(ids, entity2, dbPath2 = NOTES_DB_PATH4) {
42797
+ const result = /* @__PURE__ */ new Map();
42798
+ const wanted = /* @__PURE__ */ new Map();
42799
+ for (const id2 of ids) {
42800
+ const match = COREDATA_PARTS.exec(id2);
42801
+ if (!match || match[2] !== entity2) continue;
42802
+ const pk = canonicalKey(match[3]);
42803
+ const list = wanted.get(pk) ?? [];
42804
+ list.push({ id: id2, store: match[1] });
42805
+ wanted.set(pk, list);
42806
+ }
42807
+ if (wanted.size === 0) return result;
42808
+ let query;
42809
+ try {
42810
+ query = runJsonQuery(buildLookupSql(entity2, [...wanted.keys()]), dbPath2);
42811
+ } catch {
42812
+ return result;
42813
+ }
42814
+ const store = query.store?.toUpperCase();
42815
+ for (const row of query.rows) {
42816
+ const fields = {};
42817
+ for (const key of [
42818
+ "identifier",
42819
+ "folderIdentifier",
42820
+ "parentIdentifier",
42821
+ "accountIdentifier"
42822
+ ]) {
42823
+ const value = row[key];
42824
+ if (typeof value === "string" && value) fields[key] = value;
42825
+ }
42826
+ for (const target of wanted.get(String(row.pk)) ?? []) {
42827
+ if (target.store.toUpperCase() === store) result.set(target.id, fields);
42828
+ }
42829
+ }
42830
+ return result;
42831
+ }
42832
+ function withStableIdentifiers(items, entity2, dbPath2 = NOTES_DB_PATH4) {
42833
+ const ids = items.map((item) => item.id).filter((id2) => typeof id2 === "string");
42834
+ if (ids.length === 0) return items;
42835
+ const found = lookupStableIdentifiers(ids, entity2, dbPath2);
42836
+ if (found.size === 0) return items;
42837
+ return items.map(
42838
+ (item) => item.id && found.has(item.id) ? { ...item, ...found.get(item.id) } : item
42839
+ );
42840
+ }
42841
+ var defaultResolver = (values, entity2) => resolveIdentifiers(values, entity2);
42842
+ var DEFAULT_MAX_ID_LENGTH = 2e3;
42843
+ function acceptAlternateForms(pattern) {
42844
+ return new RegExp(
42845
+ `${pattern.source}|${UUID_PATTERN.source}|${NUMERIC_KEY_PATTERN.source}`,
42846
+ pattern.flags
42847
+ );
42848
+ }
42849
+ function resolveInSchema(values, entity2, ctx, resolver) {
42850
+ if (!values.some(isAlternateIdentifier)) return new Map(values.map((v) => [v, v]));
42851
+ try {
42852
+ return resolver(values, entity2);
42853
+ } catch (error2) {
42854
+ ctx.addIssue({
42855
+ code: external_exports.ZodIssueCode.custom,
42856
+ message: error2 instanceof Error ? error2.message : String(error2)
42857
+ });
42858
+ return null;
42859
+ }
42860
+ }
42861
+ function exactIdInput(entity2, pattern, message, options = {}) {
42862
+ const resolver = options.resolver ?? defaultResolver;
42863
+ return external_exports.string().max(options.maxLength ?? DEFAULT_MAX_ID_LENGTH).regex(acceptAlternateForms(pattern), message).transform((value, ctx) => {
42864
+ const map = resolveInSchema([value], entity2, ctx, resolver);
42865
+ return map ? map.get(value) ?? value : external_exports.NEVER;
42866
+ });
42867
+ }
42868
+ function exactIdArrayInput(entity2, pattern, message, options = {}) {
42869
+ const resolver = options.resolver ?? defaultResolver;
42870
+ const item = external_exports.string().max(options.maxLength ?? DEFAULT_MAX_ID_LENGTH).regex(acceptAlternateForms(pattern), message);
42871
+ const array2 = options.maxItems === void 0 ? external_exports.array(item) : external_exports.array(item).max(options.maxItems);
42872
+ return array2.transform((values, ctx) => {
42873
+ const map = resolveInSchema(values, entity2, ctx, resolver);
42874
+ return map ? values.map((value) => map.get(value) ?? value) : external_exports.NEVER;
42875
+ });
42876
+ }
42877
+ function looseIdTransform(entity2, resolver = defaultResolver) {
42878
+ return (value, ctx) => {
42879
+ if (!isAlternateIdentifier(value)) return value;
42880
+ const map = resolveInSchema([value], entity2, ctx, resolver);
42881
+ return map ? map.get(value) ?? value : external_exports.NEVER;
42882
+ };
42883
+ }
42884
+
42647
42885
  // src/utils/contentWarnings.ts
42648
42886
  function detectChecklistAttempt(content) {
42649
42887
  if (!content) return null;
@@ -43179,13 +43417,13 @@ function positiveTextTerms(node, negated = false) {
43179
43417
  }
43180
43418
 
43181
43419
  // src/utils/noteQueryStore.ts
43182
- import { execFileSync as execFileSync6 } from "child_process";
43183
- import * as fs4 from "fs";
43184
- import * as os4 from "os";
43185
- import * as path4 from "path";
43420
+ import { execFileSync as execFileSync7 } from "child_process";
43421
+ import * as fs5 from "fs";
43422
+ import * as os5 from "os";
43423
+ import * as path5 from "path";
43186
43424
  import { gunzipSync as gunzipSync3 } from "zlib";
43187
- var NOTES_DB_PATH4 = path4.join(
43188
- os4.homedir(),
43425
+ var NOTES_DB_PATH5 = path5.join(
43426
+ os5.homedir(),
43189
43427
  "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
43190
43428
  );
43191
43429
  var QUERY_SCAN = { DEFAULT: 500, MAX: 5e3 };
@@ -43320,7 +43558,7 @@ function buildScanSql(available, options) {
43320
43558
  ].join(" ");
43321
43559
  }
43322
43560
  function runSqlite2(dbPath2, query) {
43323
- return execFileSync6("sqlite3", ["-readonly", dbPath2, query], {
43561
+ return execFileSync7("sqlite3", ["-readonly", dbPath2, query], {
43324
43562
  encoding: "utf8",
43325
43563
  timeout: 3e4,
43326
43564
  maxBuffer: 512 * 1024 * 1024,
@@ -43347,12 +43585,12 @@ function resolveFolders(rows) {
43347
43585
  seen.add(pk);
43348
43586
  const parent = row.parent !== null ? resolve3(row.parent, seen) : void 0;
43349
43587
  const name = row.name ?? "";
43350
- const path5 = parent ? `${parent.path}/${escapeSegment(name)}` : escapeSegment(name);
43588
+ const path6 = parent ? `${parent.path}/${escapeSegment(name)}` : escapeSegment(name);
43351
43589
  const plainPath = parent ? `${parent.plainPath}/${name}` : name;
43352
43590
  const info = {
43353
- path: path5,
43591
+ path: path6,
43354
43592
  plainPath,
43355
- keys: [...new Set([name, plainPath, path5].map(normalizeForMatch))],
43593
+ keys: [...new Set([name, plainPath, path6].map(normalizeForMatch))],
43356
43594
  accountPk: row.owner ?? parent?.accountPk ?? null,
43357
43595
  shared: Boolean(row.shared) || Boolean(parent?.shared)
43358
43596
  };
@@ -43384,8 +43622,8 @@ function queryNotes(expression, options = {}) {
43384
43622
  const includeDeleted = options.includeDeleted ?? false;
43385
43623
  const withBodies = needsContent(ast);
43386
43624
  const withTags = needsTags(ast);
43387
- const dbPath2 = options.dbPath ?? NOTES_DB_PATH4;
43388
- if (!fs4.existsSync(dbPath2)) throw new NoteQueryStoreError(QUERY_FDA_MESSAGE, "no_fda");
43625
+ const dbPath2 = options.dbPath ?? NOTES_DB_PATH5;
43626
+ if (!fs5.existsSync(dbPath2)) throw new NoteQueryStoreError(QUERY_FDA_MESSAGE, "no_fda");
43389
43627
  let output;
43390
43628
  try {
43391
43629
  const available = presentColumns2(dbPath2);
@@ -43520,10 +43758,10 @@ function queryNotes(expression, options = {}) {
43520
43758
  import { spawnSync } from "child_process";
43521
43759
 
43522
43760
  // src/services/nativeTags.ts
43523
- import { execFileSync as execFileSync7 } from "node:child_process";
43761
+ import { execFileSync as execFileSync8 } from "node:child_process";
43524
43762
  import { mkdtempSync as mkdtempSync2, writeFileSync, rmSync as rmSync2 } from "node:fs";
43525
43763
  import { tmpdir as tmpdir2 } from "node:os";
43526
- import { join as join8 } from "node:path";
43764
+ import { join as join9 } from "node:path";
43527
43765
 
43528
43766
  // src/services/shortcutConsent.ts
43529
43767
  function shortcutConsentHint(shortcut) {
@@ -43599,7 +43837,7 @@ function addNativeTags(request, deps) {
43599
43837
  };
43600
43838
  }
43601
43839
  function listInstalledShortcuts() {
43602
- return execFileSync7("/usr/bin/shortcuts", ["list", "--show-identifiers"], {
43840
+ return execFileSync8("/usr/bin/shortcuts", ["list", "--show-identifiers"], {
43603
43841
  encoding: "utf8",
43604
43842
  timeout: 15e3,
43605
43843
  maxBuffer: 1024 * 1024,
@@ -43625,11 +43863,11 @@ function runNativeTagsShortcut(input) {
43625
43863
  const status = nativeTagsStatus();
43626
43864
  if (!status.installed)
43627
43865
  throw new Error(`Import the supplied ${status.shortcut}.shortcut in Shortcuts first`);
43628
- const directory = mkdtempSync2(join8(tmpdir2(), "apple-notes-native-tags-"));
43866
+ const directory = mkdtempSync2(join9(tmpdir2(), "apple-notes-native-tags-"));
43629
43867
  try {
43630
- const path5 = join8(directory, "request.json");
43631
- writeFileSync(path5, JSON.stringify(input), { mode: 384 });
43632
- execFileSync7("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", path5], {
43868
+ const path6 = join9(directory, "request.json");
43869
+ writeFileSync(path6, JSON.stringify(input), { mode: 384 });
43870
+ execFileSync8("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", path6], {
43633
43871
  encoding: "utf8",
43634
43872
  timeout: 6e4,
43635
43873
  maxBuffer: 1024 * 1024,
@@ -43648,10 +43886,10 @@ function runNativeTagsShortcut(input) {
43648
43886
  }
43649
43887
 
43650
43888
  // src/services/backgroundNotes.ts
43651
- import { execFileSync as execFileSync8 } from "node:child_process";
43889
+ import { execFileSync as execFileSync9 } from "node:child_process";
43652
43890
  import { mkdtempSync as mkdtempSync3, writeFileSync as writeFileSync2, rmSync as rmSync3 } from "node:fs";
43653
43891
  import { tmpdir as tmpdir3 } from "node:os";
43654
- import { join as join9 } from "node:path";
43892
+ import { join as join10 } from "node:path";
43655
43893
 
43656
43894
  // src/utils/appendMarkdown.ts
43657
43895
  var escape2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -43840,9 +44078,9 @@ function runBackgroundShortcut(input, status = backgroundStatus()) {
43840
44078
  throw new Error(
43841
44079
  `Install the supplied "${status.shortcut}" Shortcut once; Shortcuts must list it exactly once`
43842
44080
  );
43843
- const directory = mkdtempSync3(join9(tmpdir3(), "apple-notes-background-"));
44081
+ const directory = mkdtempSync3(join10(tmpdir3(), "apple-notes-background-"));
43844
44082
  try {
43845
- const file = join9(directory, "request.json");
44083
+ const file = join10(directory, "request.json");
43846
44084
  writeFileSync2(
43847
44085
  file,
43848
44086
  JSON.stringify({
@@ -43862,7 +44100,7 @@ function runBackgroundShortcut(input, status = backgroundStatus()) {
43862
44100
  { mode: 384 }
43863
44101
  );
43864
44102
  try {
43865
- execFileSync8("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", file], {
44103
+ execFileSync9("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", file], {
43866
44104
  encoding: "utf8",
43867
44105
  timeout: 6e4,
43868
44106
  maxBuffer: 1024 * 1024,
@@ -44065,7 +44303,7 @@ function createMarkdownNote(manager, request, run = runBackgroundShortcut) {
44065
44303
  throw new Error(
44066
44304
  `Install the supplied "${status.shortcut}" Shortcut once; Shortcuts must list it exactly once`
44067
44305
  );
44068
- const segments = (path5) => JSON.stringify(splitFolderPath(path5).map((part) => part.toLocaleLowerCase()));
44306
+ const segments = (path6) => JSON.stringify(splitFolderPath(path6).map((part) => part.toLocaleLowerCase()));
44069
44307
  if (request.folder) {
44070
44308
  const wanted = segments(request.folder);
44071
44309
  if (!manager.listAccounts().some(
@@ -44154,7 +44392,7 @@ ${request.content}`
44154
44392
  }
44155
44393
 
44156
44394
  // src/services/capabilityMatrix.ts
44157
- import { execFileSync as execFileSync9 } from "node:child_process";
44395
+ import { execFileSync as execFileSync10 } from "node:child_process";
44158
44396
  import { release } from "node:os";
44159
44397
  var MACOS_SHORTCUTS_CLI = "12.0";
44160
44398
  var MACOS_MARKDOWN_IMPORT = "26.0";
@@ -44361,7 +44599,7 @@ function evaluateFeatures(env, features = FEATURES) {
44361
44599
  function readMacOSVersion() {
44362
44600
  if (process.platform !== "darwin") return null;
44363
44601
  try {
44364
- const out = execFileSync9("/usr/bin/sw_vers", ["-productVersion"], {
44602
+ const out = execFileSync10("/usr/bin/sw_vers", ["-productVersion"], {
44365
44603
  encoding: "utf8",
44366
44604
  timeout: 3e3,
44367
44605
  stdio: ["ignore", "pipe", "ignore"]
@@ -44394,9 +44632,9 @@ function getCapabilityMatrix(probe = probeCapabilityEnvironment) {
44394
44632
  return evaluateFeatures(probe());
44395
44633
  }
44396
44634
  function formatCapabilityMatrix(matrix) {
44397
- const os5 = matrix.runtimeOS;
44635
+ const os6 = matrix.runtimeOS;
44398
44636
  const lines = [
44399
- `Feature matrix (macOS ${os5.macOSVersion ?? "unknown"}, Darwin ${os5.darwinRelease}):`
44637
+ `Feature matrix (macOS ${os6.macOSVersion ?? "unknown"}, Darwin ${os6.darwinRelease}):`
44400
44638
  ];
44401
44639
  for (const [name, status] of Object.entries(matrix.features)) {
44402
44640
  lines.push(
@@ -44514,19 +44752,19 @@ function formatDoctorReport(r) {
44514
44752
  }
44515
44753
 
44516
44754
  // src/services/fileConfig.ts
44517
- import { existsSync as existsSync7, readFileSync as readFileSync2 } from "fs";
44518
- import { join as join10 } from "path";
44519
- import { homedir as homedir8 } from "os";
44755
+ import { existsSync as existsSync8, readFileSync as readFileSync2 } from "fs";
44756
+ import { join as join11 } from "path";
44757
+ import { homedir as homedir9 } from "os";
44520
44758
  function fileConfigPath(env = process.env) {
44521
44759
  const override = env.APPLE_NOTES_MCP_CONFIG_FILE;
44522
44760
  if (override && override.trim()) return override.trim();
44523
- return join10(homedir8(), "Library", "Application Support", "apple-notes-mcp", "config.json");
44761
+ return join11(homedir9(), "Library", "Application Support", "apple-notes-mcp", "config.json");
44524
44762
  }
44525
- function loadFileConfig(env = process.env, path5 = fileConfigPath(env)) {
44763
+ function loadFileConfig(env = process.env, path6 = fileConfigPath(env)) {
44526
44764
  const applied = [];
44527
44765
  try {
44528
- if (!existsSync7(path5)) return applied;
44529
- const parsed = JSON.parse(readFileSync2(path5, "utf8"));
44766
+ if (!existsSync8(path6)) return applied;
44767
+ const parsed = JSON.parse(readFileSync2(path6, "utf8"));
44530
44768
  if (!parsed || typeof parsed !== "object") return applied;
44531
44769
  for (const [k, v] of Object.entries(parsed)) {
44532
44770
  if (typeof v !== "string") continue;
@@ -44536,7 +44774,7 @@ function loadFileConfig(env = process.env, path5 = fileConfigPath(env)) {
44536
44774
  }
44537
44775
  }
44538
44776
  } catch (e) {
44539
- console.error(`Failed to load apple-notes-mcp config file ${path5}: ${String(e)}`);
44777
+ console.error(`Failed to load apple-notes-mcp config file ${path6}: ${String(e)}`);
44540
44778
  }
44541
44779
  return applied;
44542
44780
  }
@@ -45047,9 +45285,14 @@ import {
45047
45285
  writeFileSync as writeFileSync3
45048
45286
  } from "node:fs";
45049
45287
  import { tmpdir as tmpdir4 } from "node:os";
45050
- import { basename, isAbsolute as isAbsolute2, join as join11 } from "node:path";
45051
- var noteId = external_exports.string().regex(/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i);
45288
+ import { basename, isAbsolute as isAbsolute2, join as join12 } from "node:path";
45289
+ var noteId = exactIdInput(
45290
+ "ICNote",
45291
+ /^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i,
45292
+ NOTE_ID_MESSAGE
45293
+ );
45052
45294
  var revision = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/);
45295
+ var folderId = external_exports.string().max(2e3).transform(looseIdTransform("ICFolder"));
45053
45296
  function readSnapshot(manager, id2) {
45054
45297
  const note = manager.getNoteById(id2);
45055
45298
  if (!note) throw new Error("Note not found");
@@ -45085,9 +45328,9 @@ function assertExistingContentPreserved(before, after) {
45085
45328
  throw new Error("Existing native object content or presentation changed");
45086
45329
  }
45087
45330
  }
45088
- function localAttachment(path5) {
45089
- if (!isAbsolute2(path5)) throw new Error("An absolute local file path is required");
45090
- const descriptor = openSync(path5, constants.O_RDONLY | constants.O_NOFOLLOW);
45331
+ function localAttachment(path6) {
45332
+ if (!isAbsolute2(path6)) throw new Error("An absolute local file path is required");
45333
+ const descriptor = openSync(path6, constants.O_RDONLY | constants.O_NOFOLLOW);
45091
45334
  try {
45092
45335
  const stat = fstatSync(descriptor);
45093
45336
  if (!stat.isFile() || stat.size === 0 || stat.size > 64 * 1024 * 1024)
@@ -45126,15 +45369,18 @@ function registerDirectOperations(server2, manager) {
45126
45369
  tool(
45127
45370
  "get-folder-by-id",
45128
45371
  "Use when: reading the exact folder name and parent before a guarded rename.\nReturns: folder id, current name, and parent id.\nDo not use when: listing folders by path (list-folders).\nSafety: read-only.",
45129
- { id: external_exports.string().max(2e3) },
45130
- ({ id: id2 }) => manager.getFolderById(id2),
45372
+ { id: folderId },
45373
+ ({ id: id2 }) => {
45374
+ const folder = manager.getFolderById(id2);
45375
+ return { ...folder, ...lookupStableIdentifiers([folder.id], "ICFolder").get(folder.id) };
45376
+ },
45131
45377
  true
45132
45378
  );
45133
45379
  tool(
45134
45380
  "rename-folder",
45135
45381
  "Use when: renaming one previously read folder in place.\nReturns: the unchanged folder id, new name, and parent id after readback.\nDo not use when: creating, moving, or deleting a folder.\nSafety: requires the expected current name and parent; refuses stale metadata and sibling conflicts.",
45136
45382
  {
45137
- id: external_exports.string().max(2e3),
45383
+ id: folderId,
45138
45384
  expectedName: external_exports.string().max(1e3),
45139
45385
  expectedParentId: external_exports.string().max(2e3),
45140
45386
  newName: external_exports.string().min(1).max(1e3)
@@ -45148,13 +45394,13 @@ function registerDirectOperations(server2, manager) {
45148
45394
  "add-attachment",
45149
45395
  "Use when: adding one local file to an exact note without replacing its body.\nReturns: the new attachment id, byte count, and post-write content hash after exact byte verification.\nDo not use when: reading or exporting an existing attachment.\nSafety: requires a fresh rich revision, copies at most 64 MiB through a private temporary file, never retries insertion, and verifies existing content plus fetched bytes.",
45150
45396
  { id: noteId, expectedContentHash: revision, path: external_exports.string().min(1).max(4096) },
45151
- ({ id: id2, expectedContentHash, path: path5 }) => {
45397
+ ({ id: id2, expectedContentHash, path: path6 }) => {
45152
45398
  const before = readSnapshot(manager, id2);
45153
45399
  if (before.hash !== expectedContentHash) throw new Error("Note revision changed");
45154
- const bytes = localAttachment(path5);
45400
+ const bytes = localAttachment(path6);
45155
45401
  const beforeAttachments = manager.listAttachmentsById(id2);
45156
- const directory = mkdtempSync4(join11(tmpdir4(), "notes-attachment-add-"));
45157
- const temporaryFile = join11(directory, basename(path5));
45402
+ const directory = mkdtempSync4(join12(tmpdir4(), "notes-attachment-add-"));
45403
+ const temporaryFile = join12(directory, basename(path6));
45158
45404
  try {
45159
45405
  writeFileSync3(temporaryFile, bytes, { mode: 384 });
45160
45406
  if (readSnapshot(manager, id2).hash !== before.hash)
@@ -45206,7 +45452,11 @@ function registerDirectOperations(server2, manager) {
45206
45452
  }
45207
45453
 
45208
45454
  // src/tools/nativeTagsBridge.ts
45209
- var noteId2 = external_exports.string().regex(/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i);
45455
+ var noteId2 = exactIdInput(
45456
+ "ICNote",
45457
+ /^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i,
45458
+ NOTE_ID_MESSAGE
45459
+ );
45210
45460
  var revision2 = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/);
45211
45461
  function registerNativeTagsBridge(server2, manager) {
45212
45462
  function tool(name, description, inputSchema, handler) {
@@ -45318,7 +45568,7 @@ function requireValidated(name) {
45318
45568
  UNAVAILABLE[name] || LIVE_VALIDATION_BLOCKERS[name] || `${name} has not passed live background validation in this build; see get-capabilities`
45319
45569
  );
45320
45570
  }
45321
- var id = external_exports.string().regex(/^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i);
45571
+ var id = exactIdInput("ICNote", /^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i, NOTE_ID_MESSAGE);
45322
45572
  var revision3 = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/);
45323
45573
  var common = {
45324
45574
  id,
@@ -45696,7 +45946,7 @@ function registerNativeOperations(server2, manager) {
45696
45946
 
45697
45947
  // src/setupShortcuts.ts
45698
45948
  import { spawnSync as spawnSync2 } from "node:child_process";
45699
- import { existsSync as existsSync8 } from "node:fs";
45949
+ import { existsSync as existsSync9 } from "node:fs";
45700
45950
  import { release as release2 } from "node:os";
45701
45951
  import { dirname as dirname2, resolve as resolve2 } from "node:path";
45702
45952
  import { fileURLToPath } from "node:url";
@@ -45716,16 +45966,16 @@ var shortcutFiles = [
45716
45966
  ];
45717
45967
  function setupShortcuts(checkOnly, dependencies = {}) {
45718
45968
  const status = dependencies.status || nativeTagsStatus;
45719
- const exists = dependencies.exists || existsSync8;
45720
- const open = dependencies.open || ((path5) => {
45721
- const result = spawnSync2("/usr/bin/open", [path5], { encoding: "utf8" });
45969
+ const exists = dependencies.exists || existsSync9;
45970
+ const open = dependencies.open || ((path6) => {
45971
+ const result = spawnSync2("/usr/bin/open", [path6], { encoding: "utf8" });
45722
45972
  return result.status === 0 ? { ok: true } : { ok: false, error: result.stderr || result.error?.message || "open failed" };
45723
45973
  });
45724
45974
  const baseDirectory = dependencies.baseDirectory || resolve2(dirname2(fileURLToPath(import.meta.url)), "../shortcuts");
45725
45975
  const osRelease = (dependencies.osRelease || release2)();
45726
45976
  const darwinMajor = Number.parseInt(osRelease.split(".")[0], 10);
45727
45977
  const items = shortcutFiles.map(({ name, file, optional: optional2 }) => {
45728
- const path5 = resolve2(baseDirectory, file);
45978
+ const path6 = resolve2(baseDirectory, file);
45729
45979
  let installed = false;
45730
45980
  let identifier;
45731
45981
  let error2;
@@ -45741,9 +45991,9 @@ function setupShortcuts(checkOnly, dependencies = {}) {
45741
45991
  if (!installed && !checkOnly) {
45742
45992
  if (optional2 && !(darwinMajor >= MARKDOWN_MIN_DARWIN_MAJOR))
45743
45993
  skipped = `requires macOS 26 or later (this Mac reports Darwin ${osRelease})`;
45744
- else if (!exists(path5)) error2 = `Packaged Shortcut is missing: ${path5}`;
45994
+ else if (!exists(path6)) error2 = `Packaged Shortcut is missing: ${path6}`;
45745
45995
  else {
45746
- const result = open(path5);
45996
+ const result = open(path6);
45747
45997
  opened = result.ok;
45748
45998
  if (!result.ok) error2 = result.error || `Could not open ${file}`;
45749
45999
  }
@@ -45752,7 +46002,7 @@ function setupShortcuts(checkOnly, dependencies = {}) {
45752
46002
  name,
45753
46003
  installed,
45754
46004
  identifier,
45755
- file: path5,
46005
+ file: path6,
45756
46006
  opened,
45757
46007
  ...optional2 ? { optional: optional2 } : {},
45758
46008
  ...skipped ? { skipped } : {},
@@ -45838,10 +46088,25 @@ var noteTitleSchema = {
45838
46088
  "Account name (defaults to Notes.app's default account; exact or unique-prefix match)"
45839
46089
  )
45840
46090
  };
45841
- var noteIdInput = external_exports.string().min(1, "Note ID is required").max(MAX.ID).regex(
45842
- /^x-coredata:\/\/[0-9A-Fa-f-]+\/ICNote\/p\d+$/,
45843
- "A canonical Apple Note ID is required (x-coredata://.../ICNote/p...)"
45844
- ).describe("Exact CoreData note ID returned by search-notes, list-notes, or create-note");
46091
+ var NOTE_COREDATA_ID = /^x-coredata:\/\/[0-9A-Fa-f-]+\/ICNote\/p\d+$/;
46092
+ var NOTE_ID_FORMS = "x-coredata id, Notes UUID, or numeric key";
46093
+ var noteIdInput = exactIdInput("ICNote", NOTE_COREDATA_ID, NOTE_ID_MESSAGE, {
46094
+ maxLength: MAX.ID
46095
+ }).describe(`Exact note ID returned by search-notes, list-notes, or create-note: ${NOTE_ID_FORMS}`);
46096
+ var noteIdArrayInput = exactIdArrayInput("ICNote", NOTE_COREDATA_ID, NOTE_ID_MESSAGE, {
46097
+ maxLength: MAX.ID,
46098
+ maxItems: MAX.BATCH_IDS
46099
+ });
46100
+ var looseNoteId = (base) => base.max(MAX.ID).transform(looseIdTransform("ICNote"));
46101
+ var looseFolderId = (base) => base.max(MAX.ID).transform(looseIdTransform("ICFolder"));
46102
+ var noteIdentifierOutput = {
46103
+ identifier: external_exports.string().optional(),
46104
+ folderIdentifier: external_exports.string().optional(),
46105
+ accountIdentifier: external_exports.string().optional()
46106
+ };
46107
+ function noteIdentifiers(id2) {
46108
+ return lookupStableIdentifiers([id2], "ICNote").get(id2) ?? {};
46109
+ }
45845
46110
  var expectedContentHashInput = external_exports.string().regex(/^sha256:[a-f0-9]{64}$/, "expectedContentHash must come from get-note-content").describe(
45846
46111
  "Revision token returned by get-note-content for this exact ID. The mutation stops if the note changed since that read."
45847
46112
  );
@@ -46017,7 +46282,7 @@ ${syncWarnings.join(" ")}` : "";
46017
46282
  return successResponse(
46018
46283
  `Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):
46019
46284
  ${noteList}${truncationNote}${syncNote}`,
46020
- { notes, count: notes.length }
46285
+ { notes: withStableIdentifiers(notes, "ICNote"), count: notes.length }
46021
46286
  );
46022
46287
  }, "Error searching notes")
46023
46288
  );
@@ -46101,7 +46366,7 @@ registerTool(
46101
46366
  {
46102
46367
  description: "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the exact note id, content, contentHash revision token, parsed hashtags, nativeTags, restored links, richContentComplete/writable, and strippedImages/truncated when the body was capped. Read the warning when writable is false.\nDo not use when: you only need metadata (get-note-details) or Markdown with checklist state (get-note-markdown).\nNote: password-protected notes must be unlocked in Notes.app first.\nSafety: inline images larger than APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES (default 256 KB) are replaced with '[inline image omitted: ...]' text placeholders, so the returned body is lossy whenever truncated is true. Mutations refuse attachment-bearing notes; edit those in Notes.app.",
46103
46368
  inputSchema: {
46104
- id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
46369
+ id: looseNoteId(external_exports.string()).optional().describe(`Note ID (preferred - more reliable than title): ${NOTE_ID_FORMS}`),
46105
46370
  title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
46106
46371
  account: external_exports.string().max(MAX.ACCOUNT).optional().describe(
46107
46372
  "Account name (defaults to Notes.app's default account; exact or unique-prefix match, ignored if id is provided)"
@@ -46109,6 +46374,7 @@ registerTool(
46109
46374
  },
46110
46375
  outputSchema: {
46111
46376
  id: external_exports.string().optional(),
46377
+ ...noteIdentifierOutput,
46112
46378
  title: external_exports.string().optional(),
46113
46379
  content: external_exports.string().optional(),
46114
46380
  contentHash: external_exports.string().optional(),
@@ -46148,6 +46414,7 @@ registerTool(
46148
46414
  const warning2 = [strippedImagesWarning(stripped2), rich2.warning].filter(Boolean).join("\n\n");
46149
46415
  return successResponse(warning2 ? content2 + warning2 : content2, {
46150
46416
  id: id2,
46417
+ ...noteIdentifiers(id2),
46151
46418
  title: note2.title,
46152
46419
  content: content2,
46153
46420
  contentHash: richContentHash(rawContent2, rich2),
@@ -46189,6 +46456,7 @@ registerTool(
46189
46456
  const warning = [strippedImagesWarning(stripped), rich.warning].filter(Boolean).join("\n\n");
46190
46457
  return successResponse(warning ? content + warning : content, {
46191
46458
  id: note.id,
46459
+ ...noteIdentifiers(note.id),
46192
46460
  title,
46193
46461
  content,
46194
46462
  contentHash: richContentHash(rawContent, rich),
@@ -46213,7 +46481,7 @@ registerTool(
46213
46481
  {
46214
46482
  description: "Use when: reading one note's body as plain text with no HTML, by id (preferred) or title.\nReturns: the note's plaintext exactly as Notes exposes it.\nDo not use when: you need the HTML body (get-note-content) or Markdown with checklist state (get-note-markdown).\nNote: this reads the note's native plaintext property, so it skips the HTML-to-text conversion; password-protected notes must be unlocked in Notes.app first.",
46215
46483
  inputSchema: {
46216
- id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
46484
+ id: looseNoteId(external_exports.string()).optional().describe(`Note ID (preferred - more reliable than title): ${NOTE_ID_FORMS}`),
46217
46485
  title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
46218
46486
  account: external_exports.string().max(MAX.ACCOUNT).optional().describe(
46219
46487
  "Account name (defaults to Notes.app's default account; exact or unique-prefix match, ignored if id is provided)"
@@ -46265,7 +46533,9 @@ registerTool(
46265
46533
  {
46266
46534
  description: "Use when: you have a note id and need its metadata only.\nReturns: id, title, created, modified, shared, passwordProtected.\nDo not use when: you need the body text (get-note-content) or only have a title (get-note-details).",
46267
46535
  inputSchema: {
46268
- id: external_exports.string().min(1, "Note ID is required").max(MAX.ID)
46536
+ id: looseNoteId(external_exports.string().min(1, "Note ID is required")).describe(
46537
+ `Note ID: ${NOTE_ID_FORMS}`
46538
+ )
46269
46539
  },
46270
46540
  outputSchema: {
46271
46541
  id: external_exports.string().optional(),
@@ -46273,7 +46543,8 @@ registerTool(
46273
46543
  created: external_exports.string().optional(),
46274
46544
  modified: external_exports.string().optional(),
46275
46545
  shared: external_exports.boolean().optional(),
46276
- passwordProtected: external_exports.boolean().optional()
46546
+ passwordProtected: external_exports.boolean().optional(),
46547
+ ...noteIdentifierOutput
46277
46548
  }
46278
46549
  },
46279
46550
  withErrorHandling(({ id: id2 }) => {
@@ -46287,7 +46558,8 @@ registerTool(
46287
46558
  created: note.created.toISOString(),
46288
46559
  modified: note.modified.toISOString(),
46289
46560
  shared: note.shared,
46290
- passwordProtected: note.passwordProtected
46561
+ passwordProtected: note.passwordProtected,
46562
+ ...noteIdentifiers(note.id)
46291
46563
  };
46292
46564
  return successResponse(JSON.stringify(metadata, null, 2), metadata);
46293
46565
  }, "Error retrieving note")
@@ -46304,7 +46576,8 @@ registerTool(
46304
46576
  modified: external_exports.string().optional(),
46305
46577
  shared: external_exports.boolean().optional(),
46306
46578
  passwordProtected: external_exports.boolean().optional(),
46307
- account: external_exports.string().optional()
46579
+ account: external_exports.string().optional(),
46580
+ ...noteIdentifierOutput
46308
46581
  }
46309
46582
  },
46310
46583
  withErrorHandling(({ title, account }) => {
@@ -46319,7 +46592,8 @@ registerTool(
46319
46592
  modified: note.modified.toISOString(),
46320
46593
  shared: note.shared,
46321
46594
  passwordProtected: note.passwordProtected,
46322
- account: note.account
46595
+ account: note.account,
46596
+ ...noteIdentifiers(note.id)
46323
46597
  };
46324
46598
  return successResponse(JSON.stringify(metadata, null, 2), metadata);
46325
46599
  }, "Error retrieving note details")
@@ -46329,7 +46603,9 @@ registerTool(
46329
46603
  {
46330
46604
  description: "Use when: the user wants to reveal a known note in Notes.app by id.\nReturns: confirmation that Notes.app accepted the show command.\nDo not use when: you only need note content (get-note-content) or metadata (get-note-by-id).\nNote: this opens or focuses the Notes UI.",
46331
46605
  inputSchema: {
46332
- id: external_exports.string().min(1, "Note ID is required").max(MAX.ID),
46606
+ id: looseNoteId(external_exports.string().min(1, "Note ID is required")).describe(
46607
+ `Note ID: ${NOTE_ID_FORMS}`
46608
+ ),
46333
46609
  separately: external_exports.boolean().optional().describe("Open in a separate note window when supported by Notes.app")
46334
46610
  },
46335
46611
  outputSchema: {
@@ -46350,7 +46626,7 @@ registerTool(
46350
46626
  {
46351
46627
  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: the primary path reads the note's identifier from the Notes database, so it needs Full Disk Access for the app that launches this server; macOS 12-15 can fall back to the AppleScript 'note link' property, which macOS 26+ no longer exposes. Password-protected notes cannot be linked.",
46352
46628
  inputSchema: {
46353
- id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
46629
+ id: looseNoteId(external_exports.string()).optional().describe(`Note ID (preferred - more reliable than title): ${NOTE_ID_FORMS}`),
46354
46630
  title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
46355
46631
  account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)")
46356
46632
  },
@@ -46405,7 +46681,9 @@ registerTool(
46405
46681
  {
46406
46682
  description: "Use when: the user wants to reveal a known folder in Notes.app by id.\nReturns: confirmation that Notes.app accepted the show command.\nDo not use when: you only need the folder list (list-folders).\nNote: this opens or focuses the Notes UI. Get the id from list-folders.",
46407
46683
  inputSchema: {
46408
- id: external_exports.string().min(1, "Folder ID is required").max(MAX.ID),
46684
+ id: looseFolderId(external_exports.string().min(1, "Folder ID is required")).describe(
46685
+ "Folder ID from list-folders: x-coredata id, Notes UUID, or numeric key"
46686
+ ),
46409
46687
  separately: external_exports.boolean().optional().describe("Open in a separate window when supported by Notes.app")
46410
46688
  },
46411
46689
  outputSchema: {
@@ -46949,7 +47227,15 @@ registerTool(
46949
47227
  limit: external_exports.number().int().positive().optional().describe("Maximum number of notes to return")
46950
47228
  },
46951
47229
  outputSchema: {
46952
- notes: external_exports.array(external_exports.object({ title: external_exports.string(), id: external_exports.string() })).optional(),
47230
+ notes: external_exports.array(
47231
+ external_exports.object({
47232
+ title: external_exports.string(),
47233
+ id: external_exports.string(),
47234
+ identifier: external_exports.string().optional(),
47235
+ folderIdentifier: external_exports.string().optional(),
47236
+ accountIdentifier: external_exports.string().optional()
47237
+ })
47238
+ ).optional(),
46953
47239
  count: external_exports.number().optional()
46954
47240
  }
46955
47241
  },
@@ -46986,7 +47272,7 @@ ${syncWarnings.join(" ")}` : "";
46986
47272
  return successResponse(
46987
47273
  `Found ${notes.length} notes${location}${acct}${dateInfo}${limitInfo}:
46988
47274
  ${noteList}${syncNote}`,
46989
- { notes, count: notes.length }
47275
+ { notes: withStableIdentifiers(notes, "ICNote"), count: notes.length }
46990
47276
  );
46991
47277
  }, "Error listing notes")
46992
47278
  );
@@ -47010,7 +47296,10 @@ registerTool(
47010
47296
  }
47011
47297
  const noteList = notes.map((n) => ` - ${n.title} [id: ${n.id}]`).join("\n");
47012
47298
  return successResponse(`Selected note(s):
47013
- ${noteList}`, { notes, count: notes.length });
47299
+ ${noteList}`, {
47300
+ notes: withStableIdentifiers(notes, "ICNote"),
47301
+ count: notes.length
47302
+ });
47014
47303
  }, "Error getting selected notes")
47015
47304
  );
47016
47305
  registerTool(
@@ -47051,7 +47340,7 @@ ${syncWarnings.join(" ")}` : "";
47051
47340
  `Found ${folders.length} folders${resolvedAcct}:
47052
47341
  ${folderList}${syncNote}`,
47053
47342
  {
47054
- folders,
47343
+ folders: withStableIdentifiers(folders, "ICFolder"),
47055
47344
  count: folders.length
47056
47345
  }
47057
47346
  );
@@ -47127,7 +47416,7 @@ registerTool(
47127
47416
  }).join("\n");
47128
47417
  return successResponse(`Found ${accounts.length} accounts:
47129
47418
  ${accountList}`, {
47130
- accounts,
47419
+ accounts: withStableIdentifiers(accounts, "ICAccount"),
47131
47420
  count: accounts.length
47132
47421
  });
47133
47422
  }, "Error listing accounts")
@@ -47146,7 +47435,9 @@ registerTool(
47146
47435
  const location = notesManager.getDefaultLocation();
47147
47436
  const message = `Default account: ${location.account.name} [id: ${location.account.id}]
47148
47437
  Default folder: ${location.folder.name} [id: ${location.folder.id}]`;
47149
- return successResponse(message, { ...location });
47438
+ const [account] = withStableIdentifiers([location.account], "ICAccount");
47439
+ const [folder] = withStableIdentifiers([location.folder], "ICFolder");
47440
+ return successResponse(message, { account, folder });
47150
47441
  }, "Error getting default Notes location")
47151
47442
  );
47152
47443
  registerTool(
@@ -47176,7 +47467,7 @@ registerTool(
47176
47467
  ${noteList}
47177
47468
 
47178
47469
  \u26A0\uFE0F Changes to shared notes are visible to all collaborators.`,
47179
- { notes: sharedNotes, count: sharedNotes.length }
47470
+ { notes: withStableIdentifiers(sharedNotes, "ICNote"), count: sharedNotes.length }
47180
47471
  );
47181
47472
  }, "Error listing shared notes")
47182
47473
  );
@@ -47317,7 +47608,7 @@ registerTool(
47317
47608
  {
47318
47609
  description: "Use when: listing the attachments of one note, by id (preferred) or title.\nReturns: each attachment's name, content type, and id (use with save-attachment/fetch-attachment).\nDo not use when: you want the attachment bytes (fetch-attachment) or a file on disk (save-attachment).",
47319
47610
  inputSchema: {
47320
- id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
47611
+ id: looseNoteId(external_exports.string()).optional().describe(`Note ID (preferred - more reliable than title): ${NOTE_ID_FORMS}`),
47321
47612
  title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
47322
47613
  account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)")
47323
47614
  },
@@ -47425,7 +47716,9 @@ registerTool(
47425
47716
  {
47426
47717
  description: "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts after destination-folder verification.\nDo not use when: moving a single note (move-note).\nSafety: each moved note's actual container ID is compared with the destination folder ID before success is reported. The destination folder must already exist (create-folder).",
47427
47718
  inputSchema: {
47428
- ids: external_exports.array(noteIdInput).max(MAX.BATCH_IDS).describe(`Array of note IDs to move (max ${MAX.BATCH_IDS} per request)`),
47719
+ ids: noteIdArrayInput.describe(
47720
+ `Array of note IDs to move (max ${MAX.BATCH_IDS} per request)`
47721
+ ),
47429
47722
  folder: external_exports.string().max(MAX.FOLDER).describe(
47430
47723
  'Destination folder name or nested path (e.g. "Work/Clients"). Must already exist \u2014 create-folder first.'
47431
47724
  ),
@@ -47469,7 +47762,9 @@ registerTool(
47469
47762
  {
47470
47763
  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.",
47471
47764
  inputSchema: {
47472
- noteId: external_exports.string().min(1, "noteId is required").max(MAX.ID).describe("CoreData note id (from search/list)"),
47765
+ noteId: looseNoteId(external_exports.string().min(1, "noteId is required")).describe(
47766
+ `Note id (from search/list): ${NOTE_ID_FORMS}`
47767
+ ),
47473
47768
  attachmentId: external_exports.string().min(1, "attachmentId is required").max(MAX.ATTACHMENT_ID).describe("Attachment id (from list-attachments)"),
47474
47769
  savePath: external_exports.string().min(1, "savePath is required").max(MAX.SAVE_PATH).describe("Absolute destination file path (must be under home, temp, or /Volumes)")
47475
47770
  },
@@ -47496,7 +47791,9 @@ registerTool(
47496
47791
  {
47497
47792
  description: "Use when: retrieving one note attachment's bytes inline as base64 (no file written).\nReturns: name, content type, byte count, and base64 data.\nDo not use when: you want it saved to disk (save-attachment).\nNote: get the ids from list-attachments first.",
47498
47793
  inputSchema: {
47499
- noteId: external_exports.string().min(1, "noteId is required").max(MAX.ID).describe("CoreData note id (from search/list)"),
47794
+ noteId: looseNoteId(external_exports.string().min(1, "noteId is required")).describe(
47795
+ `Note id (from search/list): ${NOTE_ID_FORMS}`
47796
+ ),
47500
47797
  attachmentId: external_exports.string().min(1, "attachmentId is required").max(MAX.ATTACHMENT_ID).describe("Attachment id (from list-attachments)")
47501
47798
  },
47502
47799
  outputSchema: {
@@ -47522,7 +47819,9 @@ registerTool(
47522
47819
  {
47523
47820
  description: "Use when: the user wants to reveal one note attachment in Notes.app.\nReturns: confirmation that Notes.app revealed the attachment.\nDo not use when: you want the bytes (fetch-attachment) or a file on disk (save-attachment).\nNote: this opens or focuses the Notes UI. Get the ids from list-attachments first.",
47524
47821
  inputSchema: {
47525
- noteId: external_exports.string().min(1, "noteId is required").max(MAX.ID).describe("CoreData note id (from search/list)"),
47822
+ noteId: looseNoteId(external_exports.string().min(1, "noteId is required")).describe(
47823
+ `Note id (from search/list): ${NOTE_ID_FORMS}`
47824
+ ),
47526
47825
  attachmentId: external_exports.string().min(1, "attachmentId is required").max(MAX.ATTACHMENT_ID).describe("Attachment id (from list-attachments)"),
47527
47826
  separately: external_exports.boolean().optional().describe("Open in a separate window when supported by Notes.app")
47528
47827
  },
@@ -47622,7 +47921,7 @@ registerTool(
47622
47921
  {
47623
47922
  description: "Use when: reading a note as Markdown, with checklist items annotated [x]/[ ] when Full Disk Access is granted.\nReturns: the note's Markdown.\nDo not use when: you need the raw HTML/plaintext body (get-note-content) or only metadata (get-note-details).\nNote: falls back to plain lists (no checkmarks) without Full Disk Access.",
47624
47923
  inputSchema: {
47625
- id: external_exports.string().max(MAX.ID).optional().describe("Note ID (preferred - more reliable than title)"),
47924
+ id: looseNoteId(external_exports.string()).optional().describe(`Note ID (preferred - more reliable than title): ${NOTE_ID_FORMS}`),
47626
47925
  title: external_exports.string().max(MAX.TITLE).optional().describe("Note title (use id instead when available)"),
47627
47926
  account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account containing the note (ignored if id is provided)")
47628
47927
  },
@@ -47655,7 +47954,9 @@ registerTool(
47655
47954
  {
47656
47955
  description: "Use when: reading the checked/unchecked state of a note's checklist items, by id.\nReturns: each item's text and done state plus checked/total counts.\nDo not use when: you only have a title (get the id via search-notes first) or want the full body text (get-note-content).\nNote: requires Full Disk Access; reads the NoteStore database directly.",
47657
47956
  inputSchema: {
47658
- id: external_exports.string().min(1, "Note ID is required. Use search-notes to find the note ID first.").max(MAX.ID)
47957
+ id: looseNoteId(
47958
+ external_exports.string().min(1, "Note ID is required. Use search-notes to find the note ID first.")
47959
+ ).describe(`Note ID: ${NOTE_ID_FORMS}`)
47659
47960
  },
47660
47961
  outputSchema: {
47661
47962
  items: external_exports.array(external_exports.object({}).passthrough()).optional(),
@@ -47691,7 +47992,9 @@ registerTool(
47691
47992
  {
47692
47993
  description: "[BETA] Use when: reading note metadata AppleScript cannot expose \u2014 pinned state, checklist flags, trash/recovery state, preview snippet, password hint \u2014 by id.\nReturns: a metadata object; fields vary by macOS version and are omitted when unavailable.\nDo not use when: you need the body (get-note-content) or per-item checklist state (get-checklist-state).\nNote: reads the NoteStore SQLite database read-only and requires Full Disk Access. BETA \u2014 the database schema changes between macOS releases, so some fields may be absent. Works on trashed notes that AppleScript can no longer resolve.",
47693
47994
  inputSchema: {
47694
- id: external_exports.string().min(1, "Note ID is required. Use search-notes to find the note ID first.").max(MAX.ID)
47995
+ id: looseNoteId(
47996
+ external_exports.string().min(1, "Note ID is required. Use search-notes to find the note ID first.")
47997
+ ).describe(`Note ID: ${NOTE_ID_FORMS}`)
47695
47998
  },
47696
47999
  outputSchema: {
47697
48000
  pinned: external_exports.boolean().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.8.25",
3
+ "version": "2.8.26",
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",