apple-notes-mcp 2.9.17 → 2.9.20

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 +14 -2
  2. package/build/index.js +90 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1355,7 +1355,7 @@ Lists all folders in an account with full hierarchical paths.
1355
1355
  {}
1356
1356
  ```
1357
1357
 
1358
- **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`).
1358
+ **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`). Smart folders are listed too, because Notes' AppleScript lists them like ordinary folders; with Full Disk Access they carry `smartFolder: true` (and `(smart folder)` in the text list). A smart folder cannot hold notes or folders.
1359
1359
 
1360
1360
  ---
1361
1361
 
@@ -2016,7 +2016,8 @@ Each take gets a deadline of 1.5 times its length plus a minute (at most 30 minu
2016
2016
  Adds one nonempty local file of at most 64 MiB to an exact note using `id`, the
2017
2017
  latest `expectedContentHash`, and an absolute `path`. The server never retries
2018
2018
  the insertion. It verifies that existing rich content survived and compares the
2019
- fetched attachment bytes with the source before reporting success.
2019
+ size and streamed SHA-256 of Notes' saved copy with the source before reporting
2020
+ success, with no read cap below the 64 MiB write limit.
2020
2021
 
2021
2022
  | Parameter | Type | Required | Description |
2022
2023
  |-----------|------|----------|-------------|
@@ -2353,6 +2354,17 @@ distinctive existing `scopeText` so the Shortcut and server can independently
2353
2354
  resolve the same note. Creating a note from Markdown is
2354
2355
  [`create-note`](#create-note) with `format: "markdown"`.
2355
2356
 
2357
+ When the Shortcut refuses a request (its Find Notes step did not return
2358
+ exactly one note with that exact title and `scopeText`) or stops with its own
2359
+ action error, and the note reads back unchanged, the tool reports the
2360
+ Shortcut's reason with `code: "operation_failed"`, `committed: false` and
2361
+ `indeterminate: false`: nothing was written, so a retry is safe once the cause
2362
+ is fixed. Notes search can lag behind a note created or edited moments ago, so
2363
+ a refusal on a brand-new note may clear after a minute. A timeout, or a failed
2364
+ readback on a note that did change, is still reported as indeterminate.
2365
+ If the Shortcut is not installed, the error is `code: "shortcut_not_installed"`
2366
+ with `committed: false`.
2367
+
2356
2368
  #### `get-capabilities`
2357
2369
 
2358
2370
  Reports which background operations are implemented, live-verified, installed,
package/build/index.js CHANGED
@@ -42435,7 +42435,7 @@ function readSmartFolderIds(read = readSmartFolders) {
42435
42435
  }
42436
42436
  function smartFolderDestinationError(folderPath) {
42437
42437
  return new CodedError(
42438
- `Refused: "${folderPath}" is a smart folder. Smart folders only gather notes by their rules and cannot hold notes or folders; choose an ordinary folder (list-folders). Nothing was changed`,
42438
+ `Refused: "${folderPath}" is a smart folder. Smart folders only gather notes by their rules and cannot hold notes or folders; choose an ordinary folder (list-folders marks smart folders smartFolder: true). Nothing was changed`,
42439
42439
  {
42440
42440
  code: "unsupported",
42441
42441
  committed: false,
@@ -43881,6 +43881,9 @@ var AppleNotesManager = class {
43881
43881
  /**
43882
43882
  * Lists all folders in an account with full hierarchical paths.
43883
43883
  *
43884
+ * Smart folders are included (AppleScript lists them like ordinary folders)
43885
+ * and carry `smartFolder: true` when the NoteStore database is readable.
43886
+ *
43884
43887
  * Each folder's `name` field contains the full path (e.g., "Work/Clients/Omnia")
43885
43888
  * so that duplicate folder names (e.g., multiple "Archive" folders) are
43886
43889
  * distinguishable and can be used directly in other operations.
@@ -43939,11 +43942,13 @@ var AppleNotesManager = class {
43939
43942
  }
43940
43943
  return safeName;
43941
43944
  };
43945
+ const smart = new Set(entries2.some((entry) => entry.id) ? this.smartFolderIds() : []);
43942
43946
  return entries2.map((entry) => ({
43943
43947
  id: entry.id,
43944
43948
  name: buildPath(entry),
43945
43949
  account: entry.account || targetAccount || "",
43946
- shared: entry.shared
43950
+ shared: entry.shared,
43951
+ ...smart.has(entry.id) ? { smartFolder: true } : {}
43947
43952
  }));
43948
43953
  }
43949
43954
  /**
@@ -48265,6 +48270,7 @@ function comparableVisibleText(html) {
48265
48270
  }
48266
48271
 
48267
48272
  // src/services/backgroundNotes.ts
48273
+ var capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
48268
48274
  var BACKGROUND_SHORTCUT = "Apple Notes MCP - Background Operations v5";
48269
48275
  var backgroundShortcutName = () => process.env.APPLE_NOTES_MCP_BACKGROUND_SHORTCUT || BACKGROUND_SHORTCUT;
48270
48276
  var backgroundStatus = () => nativeTagsStatus(backgroundShortcutName());
@@ -48375,10 +48381,14 @@ function validateAppendContent(content, format, options = {}) {
48375
48381
  );
48376
48382
  }
48377
48383
  }
48384
+ var BRIDGE_REFUSED = "refused the request without running it: its Find Notes step did not find exactly one note with this exact title whose body contains the scope text. Notes search can lag behind a note created or edited moments ago, so wait a minute and retry, or pass a longer, more distinctive scopeText";
48378
48385
  function runBackgroundShortcut(input, status = backgroundStatus()) {
48379
48386
  if (!status.installed)
48380
- throw new Error(
48381
- `Install the supplied "${status.shortcut}" Shortcut once; Shortcuts must list it exactly once`
48387
+ throw Object.assign(
48388
+ new Error(
48389
+ `Install the supplied "${status.shortcut}" Shortcut once; Shortcuts must list it exactly once`
48390
+ ),
48391
+ { shortcut: status.shortcut, notInstalled: true }
48382
48392
  );
48383
48393
  const directory = mkdtempSync4(join17(tmpdir4(), "apple-notes-background-"));
48384
48394
  try {
@@ -48401,16 +48411,31 @@ function runBackgroundShortcut(input, status = backgroundStatus()) {
48401
48411
  }),
48402
48412
  { mode: 384 }
48403
48413
  );
48414
+ let output;
48404
48415
  try {
48405
- execFileSync16("/usr/bin/shortcuts", ["run", status.identifier, "--input-path", file], {
48406
- encoding: "utf8",
48407
- timeout: callTimeoutMs() ?? 6e4,
48408
- maxBuffer: 1024 * 1024,
48409
- stdio: ["ignore", "pipe", "pipe"]
48410
- });
48416
+ output = execFileSync16(
48417
+ "/usr/bin/shortcuts",
48418
+ ["run", status.identifier, "--input-path", file],
48419
+ {
48420
+ encoding: "utf8",
48421
+ timeout: callTimeoutMs() ?? 6e4,
48422
+ maxBuffer: 1024 * 1024,
48423
+ stdio: ["ignore", "pipe", "pipe"]
48424
+ }
48425
+ );
48411
48426
  } catch (error2) {
48412
48427
  throw Object.assign(error2, { shortcut: status.shortcut });
48413
48428
  }
48429
+ if (/_REFUSED\b/.test(String(output ?? "")))
48430
+ throw Object.assign(
48431
+ new Error(
48432
+ status.shortcut === markdownShortcutName() ? "refused the request without running it" : BRIDGE_REFUSED
48433
+ ),
48434
+ {
48435
+ shortcut: status.shortcut,
48436
+ refused: true
48437
+ }
48438
+ );
48414
48439
  } finally {
48415
48440
  rmSync4(directory, { recursive: true, force: true });
48416
48441
  }
@@ -48480,6 +48505,7 @@ function assertPreserved(before, after, options = {}) {
48480
48505
  function describeTransportFailure(error2) {
48481
48506
  const detail = error2;
48482
48507
  const named = detail?.shortcut ? `the "${detail.shortcut}" Shortcut` : "the background Shortcut";
48508
+ if (detail?.refused) return `${named} ${detail.message}`;
48483
48509
  return detail?.code === "ETIMEDOUT" ? `Shortcuts timed out waiting for ${named}. ${shortcutConsentHint(detail.shortcut)}` : `${named} failed: ${String(detail?.stderr || detail?.message || "no output").trim().slice(0, 400)}`;
48484
48510
  }
48485
48511
  function mutateBackground(request, operation, data, verify, deps) {
@@ -48497,16 +48523,30 @@ function mutateBackground(request, operation, data, verify, deps) {
48497
48523
  throw new Error("Note revision changed during preflight");
48498
48524
  let transportUncertain = false;
48499
48525
  let transportMessage = "";
48526
+ let transportError;
48500
48527
  try {
48501
48528
  deps.run({ ...data, operation, title: before.title, scopeText: request.scopeText });
48502
48529
  } catch (error2) {
48503
48530
  transportUncertain = true;
48531
+ transportError = error2 ?? {};
48504
48532
  transportMessage = describeTransportFailure(error2);
48505
48533
  }
48506
48534
  const after = deps.read(request.id);
48507
48535
  try {
48508
48536
  verify(before, after);
48509
48537
  } catch (error2) {
48538
+ const unchanged = after.hash === before.hash && after.pinned === before.pinned;
48539
+ if (transportError?.notInstalled && unchanged)
48540
+ throw new CodedError(
48541
+ `${transportError.message}. The Shortcut never ran, so nothing was written; run apple-notes-mcp setup, then retry.`,
48542
+ { code: "shortcut_not_installed", committed: false, indeterminate: false }
48543
+ );
48544
+ const exitedWithError = typeof transportError?.status === "number" && transportError.status !== 0 && !transportError.signal && transportError.code !== "ETIMEDOUT";
48545
+ if (transportError && (transportError.refused || exitedWithError) && unchanged)
48546
+ throw new CodedError(
48547
+ `${capitalize(transportMessage)}. The note is unchanged, so nothing was written; it is safe to retry once the cause is fixed.`,
48548
+ { code: "operation_failed", committed: false, indeterminate: false }
48549
+ );
48510
48550
  throw new Error(
48511
48551
  `Operation outcome uncertain; read exact note before any retry: ${error2 instanceof Error ? error2.message : "readback failed"}${transportMessage ? "; " + transportMessage : ""}`
48512
48552
  );
@@ -53063,6 +53103,7 @@ import {
53063
53103
  mkdtempSync as mkdtempSync6,
53064
53104
  openSync as openSync7,
53065
53105
  readFileSync as readFileSync4,
53106
+ readSync as readSync5,
53066
53107
  rmSync as rmSync6,
53067
53108
  writeFileSync as writeFileSync5
53068
53109
  } from "node:fs";
@@ -53344,7 +53385,7 @@ function localAttachment(path10) {
53344
53385
  const descriptor = openSync7(path10, constants7.O_RDONLY | constants7.O_NOFOLLOW);
53345
53386
  try {
53346
53387
  const stat = fstatSync7(descriptor);
53347
- if (!stat.isFile() || stat.size === 0 || stat.size > 64 * 1024 * 1024)
53388
+ if (!stat.isFile() || stat.size === 0 || stat.size > MAX_ADD_ATTACHMENT_BYTES)
53348
53389
  throw new Error("Attachment must be a nonempty regular file of at most 64 MiB");
53349
53390
  const bytes = readFileSync4(descriptor);
53350
53391
  if (bytes.length !== stat.size)
@@ -53354,6 +53395,30 @@ function localAttachment(path10) {
53354
53395
  closeSync7(descriptor);
53355
53396
  }
53356
53397
  }
53398
+ var MAX_ADD_ATTACHMENT_BYTES = 64 * 1024 * 1024;
53399
+ function fileMatches(path10, size, expected) {
53400
+ let descriptor;
53401
+ try {
53402
+ descriptor = openSync7(path10, constants7.O_RDONLY | constants7.O_NOFOLLOW);
53403
+ const stat = fstatSync7(descriptor);
53404
+ if (!stat.isFile() || stat.size !== size) return false;
53405
+ const hash = createHash3("sha256");
53406
+ const chunk = Buffer.allocUnsafe(1024 * 1024);
53407
+ let total = 0;
53408
+ for (; ; ) {
53409
+ const read = readSync5(descriptor, chunk, 0, chunk.length, null);
53410
+ if (read === 0) break;
53411
+ total += read;
53412
+ if (total > size) return false;
53413
+ hash.update(chunk.subarray(0, read));
53414
+ }
53415
+ return total === size && hash.digest("hex") === expected;
53416
+ } catch {
53417
+ return false;
53418
+ } finally {
53419
+ if (descriptor !== void 0) closeSync7(descriptor);
53420
+ }
53421
+ }
53357
53422
  function registerDirectOperations(server2, manager) {
53358
53423
  function tool(name, description, inputSchema, handler, readOnly = false) {
53359
53424
  server2.registerTool(
@@ -53560,18 +53625,7 @@ function storedInsertion(manager, id2, before, bytes, returnedId) {
53560
53625
  if (returnedId && /\/ICAttachment\/p\d+$/.test(returnedId) && returnedId !== attachmentId)
53561
53626
  throw new Error(UNCERTAIN);
53562
53627
  const expected = sha256(bytes);
53563
- const matches = row.assetPaths.some((path10) => {
53564
- let descriptor;
53565
- try {
53566
- descriptor = openSync7(path10, constants7.O_RDONLY | constants7.O_NOFOLLOW);
53567
- const stat = fstatSync7(descriptor);
53568
- return stat.isFile() && stat.size === bytes.length && sha256(readFileSync4(descriptor)) === expected;
53569
- } catch {
53570
- return false;
53571
- } finally {
53572
- if (descriptor !== void 0) closeSync7(descriptor);
53573
- }
53574
- });
53628
+ const matches = row.assetPaths.some((path10) => fileMatches(path10, bytes.length, expected));
53575
53629
  if (!matches)
53576
53630
  throw new Error(
53577
53631
  `Notes' database shows new attachment ${attachmentId} on this note, but its file bytes could not be verified; read the exact note and do not attach the file again`
@@ -53615,9 +53669,9 @@ function attachFile(manager, args, checked) {
53615
53669
  if (inserted.length === 1 && !(persistentReturnedId && returnedId !== inserted[0].id)) {
53616
53670
  attachmentId = inserted[0].id;
53617
53671
  reportedName = inserted[0].name;
53618
- const fetched = manager.getAttachmentBase64ById(id2, attachmentId);
53619
- const actual = typeof fetched.base64 === "string" ? Buffer.from(fetched.base64, "base64") : null;
53620
- if (!actual || sha256(actual) !== sha256(bytes))
53672
+ const verifyPath = join24(directory, "verify", "attachment.bin");
53673
+ const saved = manager.saveAttachmentById(id2, attachmentId, verifyPath);
53674
+ if (!saved.success || !fileMatches(saved.savedPath ?? verifyPath, bytes.length, sha256(bytes)))
53621
53675
  throw new Error("Attachment bytes were not verified; read the exact note before retrying");
53622
53676
  } else {
53623
53677
  const stored = inserted.length === 0 && beforeStored ? storedInsertion(manager, id2, beforeStored, bytes, returnedId) : null;
@@ -53987,7 +54041,7 @@ import {
53987
54041
  mkdirSync as mkdirSync7,
53988
54042
  openSync as openSync8,
53989
54043
  readdirSync as readdirSync4,
53990
- readSync as readSync5,
54044
+ readSync as readSync6,
53991
54045
  renameSync,
53992
54046
  unlinkSync as unlinkSync3,
53993
54047
  writeSync as writeSync5
@@ -54078,7 +54132,7 @@ var TemplateStore = class {
54078
54132
  const data = Buffer.alloc(stat.size);
54079
54133
  let read = 0;
54080
54134
  while (read < stat.size) {
54081
- const n = readSync5(fd, data, read, stat.size - read, read);
54135
+ const n = readSync6(fd, data, read, stat.size - read, read);
54082
54136
  if (n <= 0) break;
54083
54137
  read += n;
54084
54138
  }
@@ -54395,7 +54449,7 @@ function registerMarkdownTemplates(server2, store = () => new TemplateStore()) {
54395
54449
 
54396
54450
  // src/utils/svgAnalyzer.ts
54397
54451
  import { createHash as createHash5 } from "node:crypto";
54398
- import { closeSync as closeSync9, constants as constants9, fstatSync as fstatSync9, openSync as openSync9, readSync as readSync6 } from "node:fs";
54452
+ import { closeSync as closeSync9, constants as constants9, fstatSync as fstatSync9, openSync as openSync9, readSync as readSync7 } from "node:fs";
54399
54453
 
54400
54454
  // src/utils/svgColor.ts
54401
54455
  var NAMED = {
@@ -56382,7 +56436,7 @@ function readSvgSource(path10) {
56382
56436
  const buffer = Buffer.alloc(SVG_LIMITS.maxSourceBytes + 1);
56383
56437
  let total = 0;
56384
56438
  for (; ; ) {
56385
- const n = readSync6(fd, buffer, total, buffer.length - total, null);
56439
+ const n = readSync7(fd, buffer, total, buffer.length - total, null);
56386
56440
  if (n === 0) break;
56387
56441
  total += n;
56388
56442
  if (total > SVG_LIMITS.maxSourceBytes)
@@ -56633,7 +56687,9 @@ function appendChecklistItems(args, deps, readRich) {
56633
56687
  stoppedAt: {
56634
56688
  index,
56635
56689
  text: text2,
56636
- outcome: wrote ? "uncertain" : "not-written",
56690
+ // A bridge refusal or failure with an unchanged readback is
56691
+ // reported as not written even though verification ran (#248).
56692
+ outcome: wrote && !(error2 instanceof CodedError && error2.envelope.committed === false) ? "uncertain" : "not-written",
56637
56693
  error: error2 instanceof Error ? error2.message : String(error2)
56638
56694
  },
56639
56695
  notAttempted: args.items.slice(index + 1),
@@ -60581,7 +60637,7 @@ ${noteList}`, {
60581
60637
  registerTool(
60582
60638
  "list-folders",
60583
60639
  {
60584
- description: "Use when: listing all folders, with full nested paths, for an account.\nReturns: folder names/paths.\nDo not use when: listing notes (list-notes).\nNote: warns if iCloud sync is active.",
60640
+ description: "Use when: listing all folders, with full nested paths, for an account.\nReturns: folder names/paths.\nDo not use when: listing notes (list-notes).\nNote: smart folders are listed too (AppleScript lists them like ordinary folders) and are marked smartFolder: true when Full Disk Access lets the server read the Notes database; they cannot hold notes. Warns if iCloud sync is active.",
60585
60641
  inputSchema: {
60586
60642
  account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account to list folders from")
60587
60643
  },
@@ -60611,7 +60667,7 @@ ${syncWarnings.join(" ")}` : "";
60611
60667
  return successResponse(`No folders found${acct}${syncNote}`, { folders: [], count: 0 });
60612
60668
  }
60613
60669
  const resolvedAcct = folders[0]?.account ? ` (${folders[0].account})` : acct;
60614
- const folderList = folders.map((f) => ` - ${f.name}`).join("\n");
60670
+ const folderList = folders.map((f) => ` - ${f.name}${f.smartFolder ? " (smart folder)" : ""}`).join("\n");
60615
60671
  return successResponse(
60616
60672
  `Found ${folders.length} folders${resolvedAcct}:
60617
60673
  ${folderList}${syncNote}`,
@@ -60627,7 +60683,7 @@ registerTool(
60627
60683
  {
60628
60684
  description: `Use when: listing Smart Folders and the rules that define them.
60629
60685
  Returns: each smart folder's name, ids, account, and parent, its rules decoded as match ("all"/"any"/"none") plus filters (each with a readable description), the stored query with the outer deleted wrapper removed, and the raw stored query JSON. With includeMatchingNotes, also the notes Notes.app currently shows in each folder.
60630
- Do not use when: listing ordinary folders (list-folders) or searching notes (search-notes).
60686
+ Do not use when: listing all folders with paths (list-folders, which lists smart folders alongside ordinary ones) or searching notes (search-notes).
60631
60687
  Safety: read-only; reads the NoteStore database and requires Full Disk Access. includeMatchingNotes asks Notes.app (Automation permission) for each folder's current contents rather than re-evaluating the rules. Unrecognized rules are kept as "unknown" filters and set fullyDecoded false.`,
60632
60688
  inputSchema: {
60633
60689
  includeMatchingNotes: external_exports.boolean().optional().describe(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.9.17",
3
+ "version": "2.9.20",
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",