apple-notes-mcp 2.9.25 → 2.9.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.
package/README.md CHANGED
@@ -2062,7 +2062,7 @@ success, with no read cap below the 64 MiB write limit.
2062
2062
  |-----------|------|----------|-------------|
2063
2063
  | `id` | string | Yes | Exact CoreData note ID |
2064
2064
  | `expectedContentHash` | string | Yes | `contentHash` from the note version being extended |
2065
- | `path` | string | Yes | Absolute path of the local file; symbolic links are refused |
2065
+ | `path` | string | Yes | Absolute path of the local file. Read under the same rules as `create-note`'s [`contentPath`](#create-note): home, temp or `/Volumes` only; hidden paths (any component starting with `.`, such as `~/.ssh` or a project `.env`) and `~/Library` other than iCloud Drive (`~/Library/Mobile Documents`) and `~/Library/CloudStorage` are refused, checked again after resolving symbolic links and letter case, unless the server sets `APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1`. Symbolic links and anything but a regular file (a FIFO, a device) are refused |
2066
2066
  | `filename` | string | No | Name the attachment gets in Notes instead of the source file's name. One path component that keeps the source file's extension, with no slash, colon, backslash, control character, leading dot or surrounding spaces. Notes names a file attachment after the file it receives, so the server gives its private temporary copy this name |
2067
2067
 
2068
2068
  **Returns:** `attachmentId`, `bytes`, the `name` Notes reports, and the new
@@ -2093,7 +2093,7 @@ same verification as [`add-attachment`](#add-attachment).
2093
2093
  | `content` | string | No | Plain-text body placed above the attachment |
2094
2094
  | `folder` | string | No | Existing folder or nested path; create it first with [`create-folder`](#create-folder) |
2095
2095
  | `account` | string | No | Account name; defaults to Notes.app's default account |
2096
- | `path` | string | Yes | Absolute path of the local file (at most 64 MiB) |
2096
+ | `path` | string | Yes | Absolute path of the local file (at most 64 MiB), under the same read rules as `add-attachment`; a refused path creates no note |
2097
2097
  | `filename` | string | No | Attachment name override, as in `add-attachment` |
2098
2098
 
2099
2099
  ```json
@@ -2775,7 +2775,7 @@ All configuration is optional — the server works out of the box. Override beha
2775
2775
  | `APPLE_NOTES_MCP_BACKGROUND_SHORTCUT` | `Apple Notes MCP - Background Operations v5` | Name or UUID of the installed Background Operations bridge to run. `setup` still installs and checks the default name. |
2776
2776
  | `APPLE_NOTES_MCP_MARKDOWN_SHORTCUT` | `Apple Notes MCP - Create Markdown Note` | Name or UUID of the installed Create Markdown Note bridge to run. `setup` still installs and checks the default name. |
2777
2777
  | `APPLE_NOTES_MCP_PASTEBOARD_NAME` | unset | Testing only: makes [`add-attachment-from-pasteboard`](#add-attachment-from-pasteboard) read a private named pasteboard instead of the general clipboard. |
2778
- | `APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS` | unset | Set to `1` to let `create-note` read a [`contentPath`](#create-note) inside a hidden directory or file (such as `~/.config`) or `~/Library` outside iCloud Drive and `~/Library/CloudStorage`. Off by default because those places hold keys, tokens and app data. |
2778
+ | `APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS` | unset | Set to `1` to let `create-note` read a [`contentPath`](#create-note), and [`add-attachment`](#add-attachment) and [`create-note-with-attachment`](#create-note-with-attachment) attach a `path`, inside a hidden directory or file (such as `~/.config`) or `~/Library` outside iCloud Drive and `~/Library/CloudStorage`. Off by default because those places hold keys, tokens and app data. |
2779
2779
  | `DEBUG` / `VERBOSE` | unset | Set either to enable verbose diagnostic logging to stderr. |
2780
2780
 
2781
2781
  ### Configuration file (when the host strips `env`)
package/build/index.js CHANGED
@@ -41474,8 +41474,11 @@ function privateContentReason(p, roots) {
41474
41474
  return isWithinRoots(p, cloudDocuments) ? null : "~/Library";
41475
41475
  }
41476
41476
  var CLOUD_DOCUMENT_DIRS = ["Mobile Documents", "CloudStorage"];
41477
- function readAllowedTextFile(p, maxBytes, roots = allowedSaveRoots(), allowPrivate = process.env[ALLOW_PRIVATE_CONTENT_ENV] === "1") {
41478
- const abs = assertReadableInRoots(p, roots);
41477
+ function readAllowedFile(p, maxBytes, options = {}) {
41478
+ const roots = options.roots ?? allowedSaveRoots();
41479
+ const allowPrivate = options.allowPrivate ?? process.env[ALLOW_PRIVATE_CONTENT_ENV] === "1";
41480
+ const label = options.label ?? "Content file";
41481
+ const abs = assertReadableInRoots(p, roots, label);
41479
41482
  const assertNotPrivate = (candidate, candidateRoots) => {
41480
41483
  if (allowPrivate) return;
41481
41484
  const reason = privateContentReason(candidate, candidateRoots);
@@ -41496,12 +41499,12 @@ function readAllowedTextFile(p, maxBytes, roots = allowedSaveRoots(), allowPriva
41496
41499
  }
41497
41500
  try {
41498
41501
  const stat = fstatSync(descriptor);
41499
- if (!stat.isFile()) throw new Error(`Content file is not a regular file: "${abs}"`);
41502
+ if (!stat.isFile()) throw new Error(`${label} is not a regular file: "${abs}"`);
41500
41503
  let after;
41501
41504
  try {
41502
41505
  after = canonicalize(abs);
41503
41506
  } catch {
41504
- throw new Error(`Content file changed while it was being opened; try again: "${abs}"`);
41507
+ throw new Error(`${label} changed while it was being opened; try again: "${abs}"`);
41505
41508
  }
41506
41509
  if (!isWithinRoots(after, canonicalRoots(roots)))
41507
41510
  throw new Error(
@@ -41510,22 +41513,26 @@ function readAllowedTextFile(p, maxBytes, roots = allowedSaveRoots(), allowPriva
41510
41513
  assertNotPrivate(after, canonicalRoots(roots));
41511
41514
  const named = statSync(after);
41512
41515
  if (named.dev !== stat.dev || named.ino !== stat.ino)
41513
- throw new Error(`Content file changed while it was being opened; try again: "${abs}"`);
41514
- if (stat.size === 0) throw new Error(`Content file is empty: "${abs}"`);
41516
+ throw new Error(`${label} changed while it was being opened; try again: "${abs}"`);
41517
+ if (stat.size === 0) throw new Error(`${label} is empty: "${abs}"`);
41515
41518
  if (stat.size > maxBytes)
41516
- throw new Error(`Content file is ${stat.size} bytes, over the ${maxBytes}-byte limit.`);
41519
+ throw new Error(`${label} is ${stat.size} bytes, over the ${maxBytes}-byte limit.`);
41517
41520
  const bytes = readFileSync(descriptor);
41518
41521
  if (bytes.length !== stat.size)
41519
- throw new Error("Content file changed while it was being read; try again");
41520
- try {
41521
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes).replace(/^\uFEFF/, "");
41522
- } catch {
41523
- throw new Error(`Content file is not valid UTF-8 text: "${abs}"`);
41524
- }
41522
+ throw new Error(`${label} changed while it was being read; try again`);
41523
+ return bytes;
41525
41524
  } finally {
41526
41525
  closeSync(descriptor);
41527
41526
  }
41528
41527
  }
41528
+ function readAllowedTextFile(p, maxBytes, roots = allowedSaveRoots(), allowPrivate = process.env[ALLOW_PRIVATE_CONTENT_ENV] === "1") {
41529
+ const bytes = readAllowedFile(p, maxBytes, { roots, allowPrivate });
41530
+ try {
41531
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes).replace(/^\uFEFF/, "");
41532
+ } catch {
41533
+ throw new Error(`Content file is not valid UTF-8 text: "${resolve(p)}"`);
41534
+ }
41535
+ }
41529
41536
  function cleanupTempDir(dir) {
41530
41537
  try {
41531
41538
  if (existsSync5(dir)) rmSync(dir, { recursive: true, force: true });
@@ -53258,7 +53265,6 @@ import {
53258
53265
  fstatSync as fstatSync7,
53259
53266
  mkdtempSync as mkdtempSync6,
53260
53267
  openSync as openSync7,
53261
- readFileSync as readFileSync4,
53262
53268
  readSync as readSync5,
53263
53269
  rmSync as rmSync6,
53264
53270
  writeFileSync as writeFileSync5
@@ -53538,17 +53544,14 @@ function assertExistingContentPreserved(before, after) {
53538
53544
  }
53539
53545
  function localAttachment(path10) {
53540
53546
  if (!isAbsolute4(path10)) throw new Error("An absolute local file path is required");
53541
- const descriptor = openSync7(path10, constants7.O_RDONLY | constants7.O_NOFOLLOW);
53542
53547
  try {
53543
- const stat = fstatSync7(descriptor);
53544
- if (!stat.isFile() || stat.size === 0 || stat.size > MAX_ADD_ATTACHMENT_BYTES)
53545
- throw new Error("Attachment must be a nonempty regular file of at most 64 MiB");
53546
- const bytes = readFileSync4(descriptor);
53547
- if (bytes.length !== stat.size)
53548
- throw new Error("Attachment changed while it was being read; try again");
53549
- return bytes;
53550
- } finally {
53551
- closeSync7(descriptor);
53548
+ return readAllowedFile(path10, MAX_ADD_ATTACHMENT_BYTES, { label: "Attachment" });
53549
+ } catch (error2) {
53550
+ throw new CodedError(error2 instanceof Error ? error2.message : String(error2), {
53551
+ code: "validation_error",
53552
+ committed: false,
53553
+ indeterminate: false
53554
+ });
53552
53555
  }
53553
53556
  }
53554
53557
  var MAX_ADD_ATTACHMENT_BYTES = 64 * 1024 * 1024;
@@ -53623,20 +53626,22 @@ function registerDirectOperations(server2, manager) {
53623
53626
  })
53624
53627
  );
53625
53628
  const attachmentInput = {
53626
- path: external_exports.string().min(1).max(4096).describe("Absolute path of the local file to attach"),
53629
+ path: external_exports.string().min(1).max(4096).describe(
53630
+ "Absolute path of the local regular file to attach, in home, temp or /Volumes; hidden paths and ~/Library (except iCloud Drive and CloudStorage) are refused"
53631
+ ),
53627
53632
  filename: external_exports.string().min(1).max(255).optional().describe(
53628
53633
  "Name the attachment gets in Notes instead of the source file's name. One path component with the source file's extension; no slash, colon, or control characters, and no leading dot."
53629
53634
  )
53630
53635
  };
53631
53636
  tool(
53632
53637
  "add-attachment",
53633
- "Use when: adding one local file to an exact note without replacing its body.\nReturns: the new attachment id, byte count, attachment name, 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. On macOS 27 Notes' AppleScript does not list PDF attachments; with Full Disk Access the new attachment is verified through the read-only NoteStore database instead (verifiedBy: database), otherwise the outcome is reported as uncertain.",
53638
+ "Use when: adding one local file to an exact note without replacing its body.\nReturns: the new attachment id, byte count, attachment name, and post-write content hash after exact byte verification.\nDo not use when: reading or exporting an existing attachment.\nSafety: reads only a regular file in home, temp or /Volumes, refusing hidden paths (~/.ssh, .env) and ~/Library outside iCloud Drive and CloudStorage unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1; 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. On macOS 27 Notes' AppleScript does not list PDF attachments; with Full Disk Access the new attachment is verified through the read-only NoteStore database instead (verifiedBy: database), otherwise the outcome is reported as uncertain.",
53634
53639
  { id: noteId, expectedContentHash: revision, ...attachmentInput },
53635
53640
  (args) => attachFile(manager, args)
53636
53641
  );
53637
53642
  tool(
53638
53643
  "create-note-with-attachment",
53639
- "Use when: creating a new note that holds one local file, in one call.\nReturns: the new note id plus the add-attachment result (attachment id, bytes, name, content hash).\nDo not use when: the note already exists (add-attachment).\nSafety: checks the file and filename before creating anything, creates the note through Notes.app like create-note, then attaches with the same byte verification as add-attachment. If the attachment step fails before the file is inserted, the error names the new note's id; attach to it with add-attachment instead of creating another note. If insertion started but could not be verified, the outcome is uncertain: read the note with list-attachments before attaching again.",
53644
+ "Use when: creating a new note that holds one local file, in one call.\nReturns: the new note id plus the add-attachment result (attachment id, bytes, name, content hash).\nDo not use when: the note already exists (add-attachment).\nSafety: checks the file and filename before creating anything, with the same read scope as add-attachment (home, temp or /Volumes; no hidden paths or ~/Library outside iCloud Drive and CloudStorage), creates the note through Notes.app like create-note, then attaches with the same byte verification as add-attachment. If the attachment step fails before the file is inserted, the error names the new note's id; attach to it with add-attachment instead of creating another note. If insertion started but could not be verified, the outcome is uncertain: read the note with list-attachments before attaching again.",
53640
53645
  {
53641
53646
  title: external_exports.string().min(1).max(2e3).refine((s) => !/[\r\n\0]/u.test(s), "One-line title"),
53642
53647
  content: external_exports.string().min(1).max(1024 * 1024).optional().describe("Optional plain-text body placed above the attachment"),
@@ -57408,7 +57413,7 @@ import {
57408
57413
  existsSync as existsSync14,
57409
57414
  mkdirSync as mkdirSync8,
57410
57415
  mkdtempSync as mkdtempSync7,
57411
- readFileSync as readFileSync5,
57416
+ readFileSync as readFileSync4,
57412
57417
  renameSync as renameSync2,
57413
57418
  rmSync as rmSync7,
57414
57419
  writeFileSync as writeFileSync6
@@ -57445,7 +57450,7 @@ function packageRoot(fromDir = dirname8(fileURLToPath2(import.meta.url))) {
57445
57450
  const candidate = join27(dir, "package.json");
57446
57451
  if (existsSync14(candidate)) {
57447
57452
  try {
57448
- if (JSON.parse(readFileSync5(candidate, "utf8")).name === "apple-notes-mcp")
57453
+ if (JSON.parse(readFileSync4(candidate, "utf8")).name === "apple-notes-mcp")
57449
57454
  return dir;
57450
57455
  } catch {
57451
57456
  }
@@ -57461,7 +57466,7 @@ function defaultPublicHelperDeps(overrides = {}) {
57461
57466
  platform: process.platform,
57462
57467
  sourcePath: join27(packageRoot(), PUBLIC_HELPER_SOURCE),
57463
57468
  exists: existsSync14,
57464
- readFile: (path10) => readFileSync5(path10),
57469
+ readFile: (path10) => readFileSync4(path10),
57465
57470
  spawn: spawnSync3,
57466
57471
  spawnAsync: spawn,
57467
57472
  ...overrides
@@ -58401,7 +58406,7 @@ import { join as join30 } from "node:path";
58401
58406
  // src/services/privateHelper.ts
58402
58407
  import { spawnSync as spawnSync4 } from "node:child_process";
58403
58408
  import { createHash as createHash7 } from "node:crypto";
58404
- import { existsSync as existsSync15, readFileSync as readFileSync6 } from "node:fs";
58409
+ import { existsSync as existsSync15, readFileSync as readFileSync5 } from "node:fs";
58405
58410
  import { homedir as homedir21 } from "node:os";
58406
58411
  import { dirname as dirname9, join as join29, resolve as resolve8 } from "node:path";
58407
58412
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -58434,7 +58439,7 @@ function packageRoot2(fromDir = dirname9(fileURLToPath3(import.meta.url))) {
58434
58439
  const candidate = join29(dir, "package.json");
58435
58440
  if (existsSync15(candidate)) {
58436
58441
  try {
58437
- const pkg = JSON.parse(readFileSync6(candidate, "utf8"));
58442
+ const pkg = JSON.parse(readFileSync5(candidate, "utf8"));
58438
58443
  if (pkg.name === "apple-notes-mcp") return dir;
58439
58444
  } catch {
58440
58445
  }
@@ -58450,7 +58455,7 @@ function defaultDeps2(overrides = {}) {
58450
58455
  platform: process.platform,
58451
58456
  sourcePath: join29(packageRoot2(), HELPER_SOURCE_RELATIVE),
58452
58457
  exists: existsSync15,
58453
- readFile: (path10) => readFileSync6(path10),
58458
+ readFile: (path10) => readFileSync5(path10),
58454
58459
  spawn: spawnSync4,
58455
58460
  ...overrides
58456
58461
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.9.25",
3
+ "version": "2.9.26",
4
4
  "description": "MCP server for Apple Notes - read, search, create, edit, organize, and export notes on macOS via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",