apple-notes-mcp 2.8.31 → 2.8.32

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 +47 -1
  2. package/build/index.js +141 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1349,6 +1349,50 @@ latest `expectedContentHash`, and an absolute `path`. The server never retries
1349
1349
  the insertion. It verifies that existing rich content survived and compares the
1350
1350
  fetched attachment bytes with the source before reporting success.
1351
1351
 
1352
+ | Parameter | Type | Required | Description |
1353
+ |-----------|------|----------|-------------|
1354
+ | `id` | string | Yes | Exact CoreData note ID |
1355
+ | `expectedContentHash` | string | Yes | `contentHash` from the note version being extended |
1356
+ | `path` | string | Yes | Absolute path of the local file; symbolic links are refused |
1357
+ | `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 |
1358
+
1359
+ **Returns:** `attachmentId`, `bytes`, the `name` Notes reports, and the new
1360
+ `contentHash`. With `filename`, `filenameVerified` says whether Notes reports
1361
+ that exact name. A mismatch is a warning (`filenameWarning`), not a failure,
1362
+ because the attachment and its bytes are already verified.
1363
+
1364
+ ---
1365
+
1366
+ #### `create-note-with-attachment`
1367
+
1368
+ Creates a note and attaches one local file to it in a single call. It checks the
1369
+ file and `filename` before creating anything, creates the note through Notes.app
1370
+ the way [`create-note`](#create-note) does in plaintext, then attaches with the
1371
+ same verification as [`add-attachment`](#add-attachment).
1372
+
1373
+ | Parameter | Type | Required | Description |
1374
+ |-----------|------|----------|-------------|
1375
+ | `title` | string | Yes | One-line note title |
1376
+ | `content` | string | No | Plain-text body placed above the attachment |
1377
+ | `folder` | string | No | Existing folder or nested path; create it first with [`create-folder`](#create-folder) |
1378
+ | `account` | string | No | Account name; defaults to Notes.app's default account |
1379
+ | `path` | string | Yes | Absolute path of the local file (at most 64 MiB) |
1380
+ | `filename` | string | No | Attachment name override, as in `add-attachment` |
1381
+
1382
+ ```json
1383
+ {
1384
+ "title": "Signed lease",
1385
+ "folder": "Home",
1386
+ "path": "/Users/me/Downloads/scan-0042.pdf",
1387
+ "filename": "Lease 2026.pdf"
1388
+ }
1389
+ ```
1390
+
1391
+ **Returns:** the new note's `id` with `noteCreated: true` and the
1392
+ `add-attachment` result. If the attachment step fails after the note exists,
1393
+ the error names the new note's id: attach to it with `add-attachment` instead of
1394
+ calling this tool again, which would create a second note.
1395
+
1352
1396
  ---
1353
1397
 
1354
1398
  #### `list-attachments`
@@ -1570,7 +1614,9 @@ the note before retrying, and retry only the items that are not present.
1570
1614
  #### `create-table`
1571
1615
 
1572
1616
  Appends a native table from rectangular string rows and verifies every decoded
1573
- cell. It never substitutes a text table.
1617
+ cell. It never substitutes a text table. Omit `rows` for an empty 2 × 2 table,
1618
+ the size Notes itself inserts from Format > Table; its four empty cells are
1619
+ verified the same way.
1574
1620
 
1575
1621
  #### `set-note-pinned`
1576
1622
 
package/build/index.js CHANGED
@@ -46748,7 +46748,7 @@ import {
46748
46748
  writeFileSync as writeFileSync3
46749
46749
  } from "node:fs";
46750
46750
  import { tmpdir as tmpdir4 } from "node:os";
46751
- import { basename, isAbsolute as isAbsolute2, join as join15 } from "node:path";
46751
+ import { basename, extname, isAbsolute as isAbsolute2, join as join15 } from "node:path";
46752
46752
  var noteId = exactIdInput(
46753
46753
  "ICNote",
46754
46754
  /^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i,
@@ -46764,7 +46764,7 @@ function readSnapshot(manager, id2) {
46764
46764
  if (!body) throw new Error("Note content is unavailable");
46765
46765
  const enriched = enrichNoteRead(id2, body);
46766
46766
  const rich = readRichNote(id2);
46767
- if (!enriched.complete || enriched.revision !== rich.revision)
46767
+ if (enriched.revision !== rich.revision)
46768
46768
  throw new Error("Native metadata changed during read; read the note again");
46769
46769
  return { id: id2, title: note.title, body, rich, hash: richContentHash(body, enriched) };
46770
46770
  }
@@ -46853,66 +46853,144 @@ function registerDirectOperations(server2, manager) {
46853
46853
  ...manager.renameFolderById(args.id, args.expectedName, args.expectedParentId, args.newName)
46854
46854
  })
46855
46855
  );
46856
+ const attachmentInput = {
46857
+ path: external_exports.string().min(1).max(4096).describe("Absolute path of the local file to attach"),
46858
+ filename: external_exports.string().min(1).max(255).optional().describe(
46859
+ "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."
46860
+ )
46861
+ };
46856
46862
  tool(
46857
46863
  "add-attachment",
46858
- "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.",
46859
- { id: noteId, expectedContentHash: revision, path: external_exports.string().min(1).max(4096) },
46860
- ({ id: id2, expectedContentHash, path: path7 }) => {
46861
- const before = readSnapshot(manager, id2);
46862
- if (before.hash !== expectedContentHash) throw new Error("Note revision changed");
46863
- const bytes = localAttachment(path7);
46864
- const beforeAttachments = manager.listAttachmentsById(id2);
46865
- const directory = mkdtempSync4(join15(tmpdir4(), "notes-attachment-add-"));
46866
- const temporaryFile = join15(directory, basename(path7));
46864
+ "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.",
46865
+ { id: noteId, expectedContentHash: revision, ...attachmentInput },
46866
+ (args) => attachFile(manager, args)
46867
+ );
46868
+ tool(
46869
+ "create-note-with-attachment",
46870
+ "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 after the note exists, the error names the new note's id; attach to it with add-attachment instead of creating another note.",
46871
+ {
46872
+ title: external_exports.string().min(1).max(2e3).refine((s) => !/[\r\n\0]/u.test(s), "One-line title"),
46873
+ content: external_exports.string().min(1).max(1024 * 1024).optional().describe("Optional plain-text body placed above the attachment"),
46874
+ folder: external_exports.string().max(1e3).optional().describe("Existing folder or nested path; create it first with create-folder"),
46875
+ account: external_exports.string().max(200).optional().describe("Account name; defaults to Notes' default"),
46876
+ ...attachmentInput
46877
+ },
46878
+ (args) => {
46879
+ const name = attachmentName(args.path, args.filename);
46880
+ localAttachment(args.path);
46881
+ const note = manager.createNote(
46882
+ args.title,
46883
+ args.content ?? "",
46884
+ [],
46885
+ args.folder,
46886
+ args.account,
46887
+ "plaintext"
46888
+ );
46889
+ if (!note)
46890
+ throw new Error(
46891
+ `Failed to create note "${args.title}". Check that the folder and account exist (list-folders, list-accounts); nothing was attached`
46892
+ );
46893
+ const handOff = `Note ${note.id} was created; attach to it with add-attachment instead of creating another note`;
46894
+ let snapshot;
46895
+ try {
46896
+ snapshot = readSnapshot(manager, note.id);
46897
+ } catch (error2) {
46898
+ throw new Error(`${handOff}. The new note could not be read back: ${String(error2)}`);
46899
+ }
46867
46900
  try {
46868
- writeFileSync3(temporaryFile, bytes, { mode: 384 });
46869
- if (readSnapshot(manager, id2).hash !== before.hash)
46870
- throw new Error("Note revision changed");
46871
- let returnedId;
46872
- let transportUncertain = false;
46873
- try {
46874
- returnedId = manager.addAttachmentById(id2, before.body, temporaryFile);
46875
- } catch {
46876
- transportUncertain = true;
46877
- }
46878
- const after = readSnapshot(manager, id2);
46879
- assertExistingContentPreserved(before, after);
46880
- const readInserted = () => [
46881
- ...new Map(manager.listAttachmentsById(id2).map((item) => [item.id, item])).values()
46882
- ].filter((item) => !beforeAttachments.some((existing) => existing.id === item.id));
46883
- let inserted = readInserted();
46884
- for (let attempt = 0; attempt < 4 && (inserted.length !== 1 || returnedId && returnedId !== inserted[0].id); attempt++) {
46885
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
46886
- inserted = readInserted();
46887
- }
46888
- const persistentReturnedId = returnedId && /\/ICAttachment\/p\d+$/.test(returnedId);
46889
- if (inserted.length !== 1 || persistentReturnedId && returnedId !== inserted[0].id)
46890
- throw new Error(
46891
- "Attachment insertion outcome uncertain; read the exact note before retrying"
46892
- );
46893
- const attachmentId = inserted[0].id;
46894
- const fetched = manager.getAttachmentBase64ById(id2, attachmentId);
46895
- const actual = typeof fetched.base64 === "string" ? Buffer.from(fetched.base64, "base64") : null;
46896
- if (!actual || createHash2("sha256").update(actual).digest("hex") !== createHash2("sha256").update(bytes).digest("hex"))
46897
- throw new Error(
46898
- "Attachment bytes were not verified; read the exact note before retrying"
46899
- );
46900
46901
  return {
46901
- ok: true,
46902
- id: id2,
46903
- attachmentId,
46904
- contentHash: after.hash,
46905
- bytes: bytes.length,
46906
- ...transportUncertain ? {
46907
- transportWarning: "Transport was uncertain; exact bytes and prior content were verified"
46908
- } : {}
46902
+ ...attachFile(manager, {
46903
+ id: note.id,
46904
+ expectedContentHash: snapshot.hash,
46905
+ path: args.path,
46906
+ filename: name
46907
+ }),
46908
+ title: args.title,
46909
+ folder: args.folder,
46910
+ noteCreated: true
46909
46911
  };
46910
- } finally {
46911
- rmSync4(directory, { recursive: true, force: true });
46912
+ } catch (error2) {
46913
+ throw new Error(`${handOff}. ${error2 instanceof Error ? error2.message : String(error2)}`);
46912
46914
  }
46913
46915
  }
46914
46916
  );
46915
46917
  }
46918
+ function attachmentName(path7, filename) {
46919
+ const source = basename(path7);
46920
+ if (filename === void 0) return source;
46921
+ if (Buffer.byteLength(filename, "utf8") > 255)
46922
+ throw new Error("filename must be at most 255 bytes");
46923
+ if (filename !== filename.trim() || filename === "" || filename.startsWith(".") || /[/:\\\p{Cc}]/u.test(filename))
46924
+ throw new Error(
46925
+ "filename must be one path component with no slash, colon, backslash, control character, leading dot, or surrounding spaces"
46926
+ );
46927
+ if (extname(filename).toLowerCase() !== extname(source).toLowerCase())
46928
+ throw new Error(
46929
+ `filename must keep the source file's extension (${extname(source) || "none"})`
46930
+ );
46931
+ return filename;
46932
+ }
46933
+ function attachFile(manager, args) {
46934
+ const { id: id2, expectedContentHash, path: path7 } = args;
46935
+ const name = attachmentName(path7, args.filename);
46936
+ const before = readSnapshot(manager, id2);
46937
+ if (before.hash !== expectedContentHash) throw new Error("Note revision changed");
46938
+ const bytes = localAttachment(path7);
46939
+ const beforeAttachments = manager.listAttachmentsById(id2);
46940
+ const directory = mkdtempSync4(join15(tmpdir4(), "notes-attachment-add-"));
46941
+ const temporaryFile = join15(directory, name);
46942
+ try {
46943
+ writeFileSync3(temporaryFile, bytes, { mode: 384 });
46944
+ if (readSnapshot(manager, id2).hash !== before.hash) throw new Error("Note revision changed");
46945
+ let returnedId;
46946
+ let transportUncertain = false;
46947
+ try {
46948
+ returnedId = manager.addAttachmentById(id2, before.body, temporaryFile);
46949
+ } catch {
46950
+ transportUncertain = true;
46951
+ }
46952
+ const after = readSnapshot(manager, id2);
46953
+ assertExistingContentPreserved(before, after);
46954
+ const readInserted = () => [...new Map(manager.listAttachmentsById(id2).map((item) => [item.id, item])).values()].filter(
46955
+ (item) => !beforeAttachments.some((existing) => existing.id === item.id)
46956
+ );
46957
+ let inserted = readInserted();
46958
+ for (let attempt = 0; attempt < 4 && (inserted.length !== 1 || returnedId && returnedId !== inserted[0].id); attempt++) {
46959
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
46960
+ inserted = readInserted();
46961
+ }
46962
+ const persistentReturnedId = returnedId && /\/ICAttachment\/p\d+$/.test(returnedId);
46963
+ if (inserted.length !== 1 || persistentReturnedId && returnedId !== inserted[0].id)
46964
+ throw new Error(
46965
+ "Attachment insertion outcome uncertain; read the exact note before retrying"
46966
+ );
46967
+ const attachmentId = inserted[0].id;
46968
+ const fetched = manager.getAttachmentBase64ById(id2, attachmentId);
46969
+ const actual = typeof fetched.base64 === "string" ? Buffer.from(fetched.base64, "base64") : null;
46970
+ if (!actual || createHash2("sha256").update(actual).digest("hex") !== createHash2("sha256").update(bytes).digest("hex"))
46971
+ throw new Error("Attachment bytes were not verified; read the exact note before retrying");
46972
+ const nameVerified = inserted[0].name === name;
46973
+ return {
46974
+ ok: true,
46975
+ id: id2,
46976
+ attachmentId,
46977
+ contentHash: after.hash,
46978
+ bytes: bytes.length,
46979
+ name: inserted[0].name,
46980
+ ...args.filename === void 0 ? {} : {
46981
+ filenameVerified: nameVerified,
46982
+ ...nameVerified ? {} : {
46983
+ filenameWarning: "The attachment and its bytes were verified, but Notes reports a different name"
46984
+ }
46985
+ },
46986
+ ...transportUncertain ? {
46987
+ transportWarning: "Transport was uncertain; exact bytes and prior content were verified"
46988
+ } : {}
46989
+ };
46990
+ } finally {
46991
+ rmSync4(directory, { recursive: true, force: true });
46992
+ }
46993
+ }
46916
46994
 
46917
46995
  // src/tools/nativeTagsBridge.ts
46918
46996
  var noteId2 = exactIdInput(
@@ -47040,6 +47118,10 @@ var common = {
47040
47118
  "Distinctive existing phrase used by Notes search. Prefer plain words without punctuation, hashtags, or paths."
47041
47119
  )
47042
47120
  };
47121
+ var EMPTY_TABLE_ROWS = [
47122
+ ["", ""],
47123
+ ["", ""]
47124
+ ];
47043
47125
  var htmlEscape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
47044
47126
  var MAX_CHECKLIST_BATCH = 20;
47045
47127
  var byPosition = (items = []) => [...items].sort((a, b) => a.start - b.start);
@@ -47241,13 +47323,16 @@ function registerNativeOperations(server2, manager) {
47241
47323
  );
47242
47324
  tool(
47243
47325
  "create-table",
47244
- "Use when: appending one native Notes table from a rectangular array of strings.\nReturns: the native table identity and decoded cells after readback.\nDo not use when: a text table is acceptable or the rows are not rectangular.\nSafety: never substitutes text; preserves existing rich content and reports failure unless the native table is verified.",
47326
+ "Use when: appending one native Notes table from a rectangular array of strings, or an empty table when rows is omitted.\nReturns: the native table identity and decoded cells after readback.\nDo not use when: a text table is acceptable or the rows are not rectangular.\nSafety: never substitutes text; preserves existing rich content and reports failure unless the native table is verified.",
47245
47327
  {
47246
47328
  ...common,
47247
- rows: external_exports.array(external_exports.array(external_exports.string().max(1e4)).min(1).max(100)).min(1).max(1e3)
47329
+ rows: external_exports.array(external_exports.array(external_exports.string().max(1e4)).min(1).max(100)).min(1).max(1e3).optional().describe(
47330
+ "Cell text, row by row. Omit for an empty 2 x 2 table, the size Notes itself inserts."
47331
+ )
47248
47332
  },
47249
- (args) => {
47333
+ (input) => {
47250
47334
  requireValidated("create-table");
47335
+ const args = { ...input, rows: input.rows ?? EMPTY_TABLE_ROWS };
47251
47336
  if (args.rows.some((row) => row.length !== args.rows[0].length))
47252
47337
  throw new Error("Table rows must have equal cell counts");
47253
47338
  const before = readRichNote(args.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.8.31",
3
+ "version": "2.8.32",
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",