apple-notes-mcp 2.5.8 → 2.5.10

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 +1 -0
  2. package/build/index.js +118 -46
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -943,6 +943,7 @@ All configuration is optional — the server works out of the box. Override beha
943
943
  |----------|---------|-------------|
944
944
  | `APPLE_NOTES_MCP_MAX_BUFFER` | `67108864` (64 MB) | Max bytes captured from a single AppleScript invocation. Raise it if a very large export/list is truncated; lower it to cap memory. |
945
945
  | `APPLE_NOTES_MCP_MAX_ATTACHMENT_BYTES` | `26214400` (25 MB) | Max size of an attachment that [`fetch-attachment`](#fetch-attachment) will base64-encode inline. Larger attachments are rejected with an error pointing at [`save-attachment`](#save-attachment) (which streams to disk and has no such limit). Raise it to fetch bigger attachments inline; lower it to cap memory. |
946
+ | `APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES` | `262144` (256 KB) | Per-image cap on the base64 payload kept inline in a [`get-note-content`](#get-note-content) response. Inline images over the cap are replaced with placeholders (with a warning appended) so an image-heavy note cannot exceed the MCP client's message limit and drop the connection; export the real files with [`save-attachment`](#save-attachment) or [`fetch-attachment`](#fetch-attachment). Raise it to keep bigger images inline. |
946
947
  | `APPLE_NOTES_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-notes-mcp/config.json` | Path to the JSON config file (see below). |
947
948
  | `APPLE_NOTES_MCP_TIMEOUT_MS` | `30000` (30 s) | Per-call AppleScript timeout. Raise it if full-library operations (large searches, exports) time out on a big Notes library. Per-call `timeoutMs` options still win. |
948
949
  | `APPLE_NOTES_MCP_MAX_RETRIES` | `2` | Total attempts for an AppleScript call that fails with a **transient** error (Notes.app busy / not responding / lost connection / timeout). `2` means one retry; set `1` to fail fast with no retries. Non-transient errors (e.g. "note not found") never retry. |
package/build/index.js CHANGED
@@ -38656,7 +38656,7 @@ var StdioServerTransport = class {
38656
38656
  };
38657
38657
 
38658
38658
  // src/utils/applescript.ts
38659
- import { execSync, spawnSync } from "child_process";
38659
+ import { execFileSync } from "child_process";
38660
38660
  var DEFAULT_TIMEOUT_MS = 3e4;
38661
38661
  var DEFAULT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
38662
38662
  function envPositiveNumber(name) {
@@ -38693,13 +38693,10 @@ function debugLog(message, data) {
38693
38693
  console.error(`[DEBUG ${timestamp}] ${message}`);
38694
38694
  }
38695
38695
  }
38696
- function escapeForShell(script) {
38697
- return script.replace(/'/g, "'\\''");
38698
- }
38699
38696
  function isTimeoutError(error2) {
38700
38697
  if (error2 instanceof Error) {
38701
38698
  const execError = error2;
38702
- return execError.killed === true || execError.signal === "SIGTERM";
38699
+ return execError.code === "ETIMEDOUT" || execError.killed === true || execError.signal === "SIGKILL" || execError.signal === "SIGTERM";
38703
38700
  }
38704
38701
  return false;
38705
38702
  }
@@ -38714,13 +38711,7 @@ function isRetryableError(errorMessage) {
38714
38711
  return RETRYABLE_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage));
38715
38712
  }
38716
38713
  function sleep(ms) {
38717
- const seconds = ms / 1e3;
38718
- const result = spawnSync("sleep", [seconds.toString()], { stdio: "ignore" });
38719
- if (result.error) {
38720
- const end = Date.now() + ms;
38721
- while (Date.now() < end) {
38722
- }
38723
- }
38714
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
38724
38715
  }
38725
38716
  var ERROR_MAPPINGS = [
38726
38717
  // Permission errors
@@ -38812,8 +38803,7 @@ function executeAppleScript(script, options = {}) {
38812
38803
  error: "Cannot execute empty AppleScript"
38813
38804
  };
38814
38805
  }
38815
- const preparedScript = escapeForShell(wrapWithTimeout(script.trim(), timeoutMs));
38816
- const command = `osascript -e '${preparedScript}'`;
38806
+ const preparedScript = wrapWithTimeout(script.trim(), timeoutMs);
38817
38807
  debugLog("Executing AppleScript", {
38818
38808
  scriptPreview: script.trim().substring(0, 200) + (script.length > 200 ? "..." : ""),
38819
38809
  timeout: timeoutMs,
@@ -38824,7 +38814,8 @@ function executeAppleScript(script, options = {}) {
38824
38814
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
38825
38815
  const attemptStart = Date.now();
38826
38816
  try {
38827
- const output = execSync(command, {
38817
+ const output = execFileSync("osascript", ["-"], {
38818
+ input: preparedScript,
38828
38819
  encoding: "utf8",
38829
38820
  timeout: timeoutMs,
38830
38821
  // SIGKILL (not the default SIGTERM): a wedged osascript blocked on an
@@ -38833,9 +38824,7 @@ function executeAppleScript(script, options = {}) {
38833
38824
  killSignal: "SIGKILL",
38834
38825
  // Raise the output cap above Node's 1 MB default so large exports /
38835
38826
  // long notes aren't truncated into an ENOBUFS failure. (#16)
38836
- maxBuffer: getMaxBuffer(),
38837
- // Capture stderr separately to get error details
38838
- stdio: ["pipe", "pipe", "pipe"]
38827
+ maxBuffer: getMaxBuffer()
38839
38828
  });
38840
38829
  const duration3 = Date.now() - attemptStart;
38841
38830
  debugLog("AppleScript succeeded", {
@@ -38901,7 +38890,7 @@ function executeAppleScript(script, options = {}) {
38901
38890
  }
38902
38891
 
38903
38892
  // src/utils/checklistParser.ts
38904
- import { execFileSync } from "child_process";
38893
+ import { execFileSync as execFileSync2 } from "child_process";
38905
38894
  import * as zlib from "zlib";
38906
38895
  import * as fs from "fs";
38907
38896
  import * as path from "path";
@@ -38995,7 +38984,7 @@ var NOTES_DB_PATH = path.join(
38995
38984
  function hasFullDiskAccess() {
38996
38985
  try {
38997
38986
  if (!fs.existsSync(NOTES_DB_PATH)) return false;
38998
- execFileSync("sqlite3", ["-readonly", NOTES_DB_PATH, "SELECT 1;"], {
38987
+ execFileSync2("sqlite3", ["-readonly", NOTES_DB_PATH, "SELECT 1;"], {
38999
38988
  encoding: "utf8",
39000
38989
  timeout: 3e3,
39001
38990
  stdio: ["pipe", "pipe", "pipe"]
@@ -39014,7 +39003,7 @@ function queryNoteData(noteId) {
39014
39003
  const pk = pkMatch[1];
39015
39004
  const query = `SELECT hex(nd.ZDATA) FROM ZICNOTEDATA nd JOIN ZICCLOUDSYNCINGOBJECT n ON nd.ZNOTE = n.Z_PK WHERE n.Z_PK = ${pk};`;
39016
39005
  try {
39017
- const result = execFileSync("sqlite3", ["-readonly", NOTES_DB_PATH, query], {
39006
+ const result = execFileSync2("sqlite3", ["-readonly", NOTES_DB_PATH, query], {
39018
39007
  encoding: "utf8",
39019
39008
  timeout: 5e3,
39020
39009
  stdio: ["pipe", "pipe", "pipe"]
@@ -39140,11 +39129,21 @@ function getChecklistItems(noteId) {
39140
39129
  }
39141
39130
 
39142
39131
  // src/utils/attachmentFs.ts
39143
- import { existsSync as existsSync2, mkdtempSync, readFileSync, rmSync, statSync } from "fs";
39144
- import { isAbsolute, resolve, sep } from "path";
39132
+ import { existsSync as existsSync2, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync } from "fs";
39133
+ import { dirname, isAbsolute, resolve, sep } from "path";
39145
39134
  import { homedir as homedir2, tmpdir } from "os";
39146
39135
  function allowedSaveRoots() {
39147
- return [resolve(homedir2()), resolve(tmpdir()), "/Volumes", "/private/var/folders", "/tmp"];
39136
+ return [
39137
+ resolve(homedir2()),
39138
+ resolve(tmpdir()),
39139
+ "/Volumes",
39140
+ "/private/var/folders",
39141
+ "/tmp",
39142
+ "/private/tmp"
39143
+ ];
39144
+ }
39145
+ function ensureParentDir(abs) {
39146
+ mkdirSync(dirname(abs), { recursive: true });
39148
39147
  }
39149
39148
  function assertSafeSavePath(p, roots = allowedSaveRoots()) {
39150
39149
  if (!p || !p.trim()) throw new Error("A destination path is required.");
@@ -39282,6 +39281,10 @@ function buildAppleScriptDateVar(date3, varName = "thresholdDate") {
39282
39281
  function asDatePartsExpr(v) {
39283
39282
  return `((year of ${v}) as text) & "-" & ((month of ${v}) as integer as text) & "-" & ((day of ${v}) as text) & "-" & ((hours of ${v}) as text) & "-" & ((minutes of ${v}) as text) & "-" & ((seconds of ${v}) as text)`;
39284
39283
  }
39284
+ function normalizeAppleScriptText(field) {
39285
+ const trimmed = field?.trim();
39286
+ return trimmed && trimmed !== "missing value" ? trimmed : void 0;
39287
+ }
39285
39288
  function parseNotePropertiesOutput(output) {
39286
39289
  const parts = output.split(FIELD_SEP);
39287
39290
  if (parts.length < 6) {
@@ -40471,7 +40474,7 @@ var AppleNotesManager = class {
40471
40474
  end if
40472
40475
  end repeat
40473
40476
  if theAttachment is missing value then
40474
- return "ERR${AS_FIELD_SEP}attachment not found"
40477
+ return "ERR" & ${AS_FIELD_SEP} & "attachment not found"
40475
40478
  end if
40476
40479
  show theAttachment${separatelyClause}
40477
40480
  return "OK"
@@ -40737,8 +40740,8 @@ var AppleNotesManager = class {
40737
40740
  set end of attachmentList to attachId & ${AS_FIELD_SEP} & attachName & ${AS_FIELD_SEP} & attachContentId & ${AS_FIELD_SEP} & attachUrl & ${AS_FIELD_SEP} & createdParts & ${AS_FIELD_SEP} & modifiedParts & ${AS_FIELD_SEP} & sharedFlag
40738
40741
  end repeat
40739
40742
  set output to ""
40740
- repeat with item in attachmentList
40741
- set output to output & item & ${AS_RECORD_SEP}
40743
+ repeat with recordItem in attachmentList
40744
+ set output to output & recordItem & ${AS_RECORD_SEP}
40742
40745
  end repeat
40743
40746
  return output
40744
40747
  end tell
@@ -40760,7 +40763,7 @@ var AppleNotesManager = class {
40760
40763
  name: parts[1].trim(),
40761
40764
  contentType: parts[2].trim(),
40762
40765
  contentId: parts[2].trim() || void 0,
40763
- url: parts[3]?.trim() || void 0,
40766
+ url: normalizeAppleScriptText(parts[3]),
40764
40767
  created: parts[4] ? parseAppleScriptDate(parts[4].trim()) : void 0,
40765
40768
  modified: parts[5] ? parseAppleScriptDate(parts[5].trim()) : void 0,
40766
40769
  shared: parts[6] ? parts[6].trim().toLowerCase() === "true" : void 0
@@ -40801,8 +40804,8 @@ var AppleNotesManager = class {
40801
40804
  set end of attachmentList to attachId & ${AS_FIELD_SEP} & attachName & ${AS_FIELD_SEP} & attachContentId & ${AS_FIELD_SEP} & attachUrl & ${AS_FIELD_SEP} & createdParts & ${AS_FIELD_SEP} & modifiedParts & ${AS_FIELD_SEP} & sharedFlag
40802
40805
  end repeat
40803
40806
  set output to ""
40804
- repeat with item in attachmentList
40805
- set output to output & item & ${AS_RECORD_SEP}
40807
+ repeat with recordItem in attachmentList
40808
+ set output to output & recordItem & ${AS_RECORD_SEP}
40806
40809
  end repeat
40807
40810
  return output
40808
40811
  end tell
@@ -40825,7 +40828,7 @@ var AppleNotesManager = class {
40825
40828
  name: parts[1].trim(),
40826
40829
  contentType: parts[2].trim(),
40827
40830
  contentId: parts[2].trim() || void 0,
40828
- url: parts[3]?.trim() || void 0,
40831
+ url: normalizeAppleScriptText(parts[3]),
40829
40832
  created: parts[4] ? parseAppleScriptDate(parts[4].trim()) : void 0,
40830
40833
  modified: parts[5] ? parseAppleScriptDate(parts[5].trim()) : void 0,
40831
40834
  shared: parts[6] ? parts[6].trim().toLowerCase() === "true" : void 0
@@ -40847,6 +40850,7 @@ var AppleNotesManager = class {
40847
40850
  let abs;
40848
40851
  try {
40849
40852
  abs = assertSafeSavePath(savePath);
40853
+ ensureParentDir(abs);
40850
40854
  } catch (e) {
40851
40855
  return { success: false, error: e instanceof Error ? e.message : String(e) };
40852
40856
  }
@@ -40864,10 +40868,18 @@ var AppleNotesManager = class {
40864
40868
  end if
40865
40869
  end repeat
40866
40870
  if theAttachment is missing value then
40867
- return "ERR${AS_FIELD_SEP}attachment not found"
40871
+ return "ERR" & ${AS_FIELD_SEP} & "attachment not found"
40868
40872
  end if
40869
- save theAttachment in (POSIX file "${safePath}")
40870
- return "OK${AS_FIELD_SEP}" & (name of theAttachment) & "${AS_FIELD_SEP}" & (content identifier of theAttachment)
40873
+ set attachUrl to ""
40874
+ try
40875
+ set attachUrl to URL of theAttachment as text
40876
+ end try
40877
+ try
40878
+ save theAttachment in (POSIX file "${safePath}")
40879
+ on error errMsg
40880
+ return "ERRSAVE" & ${AS_FIELD_SEP} & errMsg & ${AS_FIELD_SEP} & attachUrl
40881
+ end try
40882
+ return "OK" & ${AS_FIELD_SEP} & (name of theAttachment) & ${AS_FIELD_SEP} & (content identifier of theAttachment)
40871
40883
  end tell
40872
40884
  `;
40873
40885
  const result = executeAppleScript(script);
@@ -40875,6 +40887,16 @@ var AppleNotesManager = class {
40875
40887
  return { success: false, error: result.error ?? "unknown error" };
40876
40888
  }
40877
40889
  const parts = (result.output ?? "").trim().split(FIELD_SEP);
40890
+ if (parts[0] === "ERRSAVE") {
40891
+ const saveErr = (parts[1]?.trim() || "unknown error").replace(/\.$/, "");
40892
+ const rawUrl = parts[2]?.trim();
40893
+ const attachUrl = rawUrl && rawUrl !== "missing value" ? rawUrl : void 0;
40894
+ const linkHint = attachUrl ? ` This attachment appears to be a link preview (URL: ${attachUrl}) rather than a file, and link previews have no file payload to save.` : "";
40895
+ return {
40896
+ success: false,
40897
+ error: `Notes could not save this attachment: ${saveErr}.${linkHint}`
40898
+ };
40899
+ }
40878
40900
  if (parts[0] !== "OK") {
40879
40901
  return { success: false, error: parts[1]?.trim() || "attachment not found" };
40880
40902
  }
@@ -41333,7 +41355,7 @@ var AppleNotesManager = class {
41333
41355
  };
41334
41356
 
41335
41357
  // src/utils/syncDetection.ts
41336
- import { execFileSync as execFileSync2 } from "child_process";
41358
+ import { execFileSync as execFileSync3 } from "child_process";
41337
41359
  import * as fs2 from "fs";
41338
41360
  import * as path2 from "path";
41339
41361
  import * as os2 from "os";
@@ -41374,7 +41396,7 @@ function getSyncStatus(useCache = true) {
41374
41396
  WHERE ZCURRENTLOCALVERSION > ZLATESTVERSIONSYNCEDTOCLOUD
41375
41397
  AND ZLATESTVERSIONSYNCEDTOCLOUD IS NOT NULL;
41376
41398
  `;
41377
- const result = execFileSync2(
41399
+ const result = execFileSync3(
41378
41400
  "sqlite3",
41379
41401
  ["-readonly", NOTES_DB_PATH2, query.replace(/\n/g, " ")],
41380
41402
  {
@@ -41433,7 +41455,7 @@ function withSyncAwarenessSync(operation, fn) {
41433
41455
  }
41434
41456
 
41435
41457
  // src/utils/noteMetadata.ts
41436
- import { execFileSync as execFileSync3 } from "child_process";
41458
+ import { execFileSync as execFileSync4 } from "child_process";
41437
41459
  import * as fs3 from "fs";
41438
41460
  import * as path3 from "path";
41439
41461
  import * as os3 from "os";
@@ -41454,7 +41476,7 @@ var COLUMN_MAP = [
41454
41476
  { key: "smartFolderQuery", column: "ZSMARTFOLDERQUERYJSON", type: "text" }
41455
41477
  ];
41456
41478
  function runSqlite(query) {
41457
- return execFileSync3("sqlite3", ["-readonly", NOTES_DB_PATH3, query], {
41479
+ return execFileSync4("sqlite3", ["-readonly", NOTES_DB_PATH3, query], {
41458
41480
  encoding: "utf8",
41459
41481
  timeout: 5e3,
41460
41482
  stdio: ["pipe", "pipe", "pipe"]
@@ -41552,8 +41574,48 @@ function parseHashtags(body) {
41552
41574
  return result;
41553
41575
  }
41554
41576
 
41577
+ // src/utils/inlineImages.ts
41578
+ var DEFAULT_MAX_INLINE_IMAGE_BYTES = 256 * 1024;
41579
+ function maxInlineImageBytes(env = process.env) {
41580
+ const raw = env.APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES;
41581
+ if (raw !== void 0) {
41582
+ const n = Number(raw);
41583
+ if (Number.isFinite(n) && n > 0) return n;
41584
+ }
41585
+ return DEFAULT_MAX_INLINE_IMAGE_BYTES;
41586
+ }
41587
+ function formatBytes(bytes) {
41588
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
41589
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
41590
+ return `${bytes} B`;
41591
+ }
41592
+ var INLINE_IMG_RE = /<img\b[^>]*\bsrc\s*=\s*(["'])data:([^;'"]+);base64,([^"']*)\1[^>]*\/?>/gi;
41593
+ function stripLargeInlineImages(html, maxBytes = maxInlineImageBytes()) {
41594
+ let strippedCount = 0;
41595
+ let strippedBytes = 0;
41596
+ const result = html.replace(INLINE_IMG_RE, (tag, _quote, mediaType, b64) => {
41597
+ if (b64.length <= maxBytes) return tag;
41598
+ const decodedBytes = Math.floor(b64.length * 3 / 4);
41599
+ strippedCount += 1;
41600
+ strippedBytes += decodedBytes;
41601
+ return `<div>[inline image omitted: ${mediaType}, ~${formatBytes(
41602
+ decodedBytes
41603
+ )}; use list-attachments and save-attachment or fetch-attachment to export it]</div>`;
41604
+ });
41605
+ return { html: result, strippedCount, strippedBytes };
41606
+ }
41607
+ function strippedImagesWarning(stripped) {
41608
+ if (stripped.strippedCount === 0) return null;
41609
+ const plural = stripped.strippedCount === 1 ? "image" : "images";
41610
+ return `
41611
+
41612
+ \u26A0\uFE0F ${stripped.strippedCount} inline ${plural} (~${formatBytes(
41613
+ stripped.strippedBytes
41614
+ )} decoded) exceeded the per-image inline cap and ${stripped.strippedCount === 1 ? "was" : "were"} replaced with placeholders so the response stays within MCP message limits. The images are still in the note: use list-attachments with save-attachment or fetch-attachment to export them, or raise APPLE_NOTES_MCP_MAX_INLINE_IMAGE_BYTES.`;
41615
+ }
41616
+
41555
41617
  // src/tools/doctor.ts
41556
- import { spawnSync as spawnSync2 } from "child_process";
41618
+ import { spawnSync } from "child_process";
41557
41619
  function runDoctor(manager) {
41558
41620
  const checks = [];
41559
41621
  const hc = manager.healthCheck();
@@ -41591,7 +41653,7 @@ function runDoctor(manager) {
41591
41653
  function checkNodeRuntimeSignature() {
41592
41654
  const name = "Node runtime signature";
41593
41655
  try {
41594
- const r = spawnSync2("codesign", ["-dvvv", process.execPath], { encoding: "utf8" });
41656
+ const r = spawnSync("codesign", ["-dvvv", process.execPath], { encoding: "utf8" });
41595
41657
  const out = `${r.stdout ?? ""}${r.stderr ?? ""}`;
41596
41658
  if (r.error || !out.trim()) {
41597
41659
  return {
@@ -41911,12 +41973,19 @@ server.registerTool(
41911
41973
  `Note "${note2.title}" is password-protected and cannot be read. Unlock it in Notes.app first.`
41912
41974
  );
41913
41975
  }
41914
- const content2 = notesManager.getNoteContentById(id);
41915
- if (!content2) {
41976
+ const rawContent2 = notesManager.getNoteContentById(id);
41977
+ if (!rawContent2) {
41916
41978
  return errorResponse(`Failed to read content of note "${note2.title}"`);
41917
41979
  }
41980
+ const stripped2 = stripLargeInlineImages(rawContent2);
41981
+ const content2 = stripped2.html;
41918
41982
  const hashtags2 = parseHashtags(content2);
41919
- return successResponse(content2, { title: note2.title, content: content2, hashtags: hashtags2 });
41983
+ const warning2 = strippedImagesWarning(stripped2);
41984
+ return successResponse(warning2 ? content2 + warning2 : content2, {
41985
+ title: note2.title,
41986
+ content: content2,
41987
+ hashtags: hashtags2
41988
+ });
41920
41989
  }
41921
41990
  if (!title) {
41922
41991
  return errorResponse("Either 'id' or 'title' is required");
@@ -41930,12 +41999,15 @@ server.registerTool(
41930
41999
  `Note "${title}" is password-protected and cannot be read. Unlock it in Notes.app first.`
41931
42000
  );
41932
42001
  }
41933
- const content = notesManager.getNoteContent(title, account);
41934
- if (!content) {
42002
+ const rawContent = notesManager.getNoteContent(title, account);
42003
+ if (!rawContent) {
41935
42004
  return errorResponse(`Failed to read content of note "${title}"`);
41936
42005
  }
42006
+ const stripped = stripLargeInlineImages(rawContent);
42007
+ const content = stripped.html;
41937
42008
  const hashtags = parseHashtags(content);
41938
- return successResponse(content, { title, content, hashtags });
42009
+ const warning = strippedImagesWarning(stripped);
42010
+ return successResponse(warning ? content + warning : content, { title, content, hashtags });
41939
42011
  }, "Error retrieving note content")
41940
42012
  );
41941
42013
  server.registerTool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.5.8",
3
+ "version": "2.5.10",
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",