apple-notes-mcp 2.8.28 → 2.8.29

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 +22 -0
  2. package/build/index.js +232 -99
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -513,6 +513,28 @@ Disk Access.
513
513
 
514
514
  ---
515
515
 
516
+ #### `get-note-tables`
517
+
518
+ Reads every native table in one exact note, in body order, from the NoteStore
519
+ database. Each table is returned as GitHub-flavored Markdown and as JSON `rows`
520
+ with stable `rowIds` and `columnIds`. Notes tables have no header row, so the
521
+ Markdown uses the first row as the header. Pipes are escaped as `\|`,
522
+ backslashes are doubled, and line breaks inside a cell become `<br>`.
523
+
524
+ | Parameter | Type | Required | Description |
525
+ |-----------|------|----------|-------------|
526
+ | `id` | string | Yes | Exact note ID (`x-coredata://…/ICNote/pNNN`) |
527
+
528
+ `tableCellsComplete` is false when any table or cell could not be decoded. A
529
+ cell holding an embedded object is `null` in `rows`, listed in
530
+ `incompleteCells`, and shown as `[undecoded cell]` in Markdown. A table whose
531
+ data cannot be decoded at all has `complete: false`, a `reason`, and no rows.
532
+ Cell text is never guessed. Links and styling inside cells are not rendered.
533
+ A note without tables returns an empty `tables` list. This tool is read-only,
534
+ requires Full Disk Access, and refuses password-protected notes.
535
+
536
+ ---
537
+
516
538
  #### `get-note-blocks`
517
539
 
518
540
  Decodes one note's body into typed blocks, read-only, from the NoteStore
package/build/index.js CHANGED
@@ -39901,6 +39901,185 @@ function assertLinkedWrite(rich, content, format, allowLinkChanges = false) {
39901
39901
  }
39902
39902
  }
39903
39903
 
39904
+ // src/utils/noteTables.ts
39905
+ import { gunzipSync as gunzipSync3 } from "node:zlib";
39906
+ var sub = (f, n) => {
39907
+ const value = embeddedMessage(getField(f, n));
39908
+ if (!value) throw new Error(`Missing table field ${n}`);
39909
+ return value;
39910
+ };
39911
+ var num = (f, n) => {
39912
+ const v = varintValue(getField(f, n));
39913
+ if (v === void 0) throw new Error(`Missing table index ${n}`);
39914
+ return v;
39915
+ };
39916
+ var many = (f, n) => getFields(f, n).map((v) => {
39917
+ const m = embeddedMessage(v);
39918
+ if (!m) throw new Error("Invalid table entry");
39919
+ return m;
39920
+ });
39921
+ var hex = (f) => {
39922
+ if (!(f?.value instanceof Uint8Array)) throw new Error("Missing table UUID");
39923
+ return Buffer.from(f.value).toString("hex");
39924
+ };
39925
+ function parseNoteTable(compressed) {
39926
+ const table = decodeTable(compressed, true);
39927
+ return { rows: table.rows, rowIds: table.rowIds, columnIds: table.columnIds };
39928
+ }
39929
+ function parseNoteTableCells(compressed) {
39930
+ return decodeTable(compressed, false);
39931
+ }
39932
+ function decodeTable(compressed, strict) {
39933
+ const root = decodeMessage(gunzipSync3(compressed, { maxOutputLength: 16 * 1024 * 1024 }));
39934
+ const data = sub(sub(root, 2), 3), entries = many(data, 3);
39935
+ if (entries.length > 1e5) throw new Error("Table too large");
39936
+ const keys = getFields(data, 4).map(stringValue), types = getFields(data, 5).map(stringValue), uuids = getFields(data, 6).map(hex);
39937
+ const entry = (index) => {
39938
+ if (!entries[index]) throw new Error("Invalid table reference");
39939
+ return entries[index];
39940
+ };
39941
+ const uuidIndex = (index) => num(sub(many(sub(entry(index), 13), 3)[0], 2), 2);
39942
+ const roots = entries.filter((e) => {
39943
+ const map = embeddedMessage(getField(e, 13));
39944
+ return map && types[num(map, 1)] === "com.apple.notes.ICTable";
39945
+ });
39946
+ if (roots.length !== 1) throw new Error("Ambiguous native table root");
39947
+ const refs = new Map(
39948
+ many(sub(roots[0], 13), 3).filter((m) => ["crRows", "crColumns", "cellColumns"].includes(keys[num(m, 1)] || "")).map((m) => [keys[num(m, 1)], num(sub(m, 2), 6)])
39949
+ );
39950
+ const ordered = (key) => {
39951
+ const ref = refs.get(key);
39952
+ if (ref === void 0) throw new Error("Missing table dimension");
39953
+ const ordering = sub(sub(entry(ref), 16), 1), array2 = sub(ordering, 1);
39954
+ const ids = many(array2, 2).map((a) => hex(getField(a, 2)));
39955
+ const map = /* @__PURE__ */ new Map();
39956
+ ids.forEach((id2, i) => {
39957
+ const index = uuids.indexOf(id2);
39958
+ if (index < 0) throw new Error("Missing dimension UUID");
39959
+ map.set(index, i);
39960
+ });
39961
+ const aliases = many(sub(ordering, 2), 1).map((pair) => [
39962
+ uuidIndex(num(sub(pair, 1), 6)),
39963
+ uuidIndex(num(sub(pair, 2), 6))
39964
+ ]);
39965
+ for (let pass = 0; pass < aliases.length + 1; pass++) {
39966
+ let changed = false;
39967
+ for (const [key2, value] of aliases)
39968
+ if (map.has(key2) && !map.has(value)) {
39969
+ map.set(value, map.get(key2));
39970
+ changed = true;
39971
+ }
39972
+ if (!changed) break;
39973
+ }
39974
+ return { ids, map };
39975
+ };
39976
+ const rows = ordered("crRows"), columns = ordered("crColumns");
39977
+ if (!rows.ids.length || !columns.ids.length || rows.ids.length * columns.ids.length > 1e5)
39978
+ throw new Error("Unsupported table size");
39979
+ const values = rows.ids.map(() => columns.ids.map(() => ""));
39980
+ const incomplete = [];
39981
+ const cellRef = refs.get("cellColumns");
39982
+ if (cellRef === void 0) throw new Error("Missing table cells");
39983
+ for (const column of many(sub(entry(cellRef), 6), 1)) {
39984
+ const ci = columns.map.get(uuidIndex(num(sub(column, 1), 6)));
39985
+ const cells = entry(num(sub(column, 2), 6));
39986
+ for (const row of many(sub(cells, 6), 1)) {
39987
+ const ri = rows.map.get(uuidIndex(num(sub(row, 1), 6)));
39988
+ if (ri === void 0 || ci === void 0) continue;
39989
+ const note = sub(entry(num(sub(row, 2), 6)), 10);
39990
+ const text = stringValue(getField(note, 2));
39991
+ if (text === void 0 || text.includes("\uFFFC")) {
39992
+ if (strict) throw new Error("Embedded or unsupported table cell");
39993
+ values[ri][ci] = null;
39994
+ incomplete.push({
39995
+ ri,
39996
+ ci,
39997
+ reason: text === void 0 ? "Cell text is missing" : "Cell contains an embedded object"
39998
+ });
39999
+ continue;
40000
+ }
40001
+ values[ri][ci] = text.replace(/\n$/u, "");
40002
+ }
40003
+ }
40004
+ const rtl = entries.some((e) => {
40005
+ const map = embeddedMessage(getField(e, 13));
40006
+ return map && many(map, 3).some(
40007
+ (m) => stringValue(getField(sub(m, 2), 4)) === "CRTableColumnDirectionRightToLeft"
40008
+ );
40009
+ });
40010
+ const width = columns.ids.length;
40011
+ if (rtl) {
40012
+ for (const row of values) row.reverse();
40013
+ columns.ids.reverse();
40014
+ }
40015
+ const incompleteCells = incomplete.map(({ ri, ci, reason }) => ({ row: ri, column: rtl ? width - 1 - ci : ci, reason })).sort((a, b) => a.row - b.row || a.column - b.column);
40016
+ return { rows: values, rowIds: rows.ids, columnIds: columns.ids, incompleteCells };
40017
+ }
40018
+
40019
+ // src/utils/tableMarkdown.ts
40020
+ var UNDECODED_CELL_MARKER = "[undecoded cell]";
40021
+ function escapeMarkdownTableCell(text) {
40022
+ return text.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r\n|[\r\n\u2028\u2029]/g, "<br>");
40023
+ }
40024
+ function renderMarkdownTable(rows) {
40025
+ const width = rows.reduce((max, row) => Math.max(max, row.length), 0);
40026
+ if (rows.length === 0 || width === 0) return "";
40027
+ const line = (row) => "| " + Array.from({ length: width }, (_, i) => {
40028
+ const cell = row[i];
40029
+ return cell === null ? UNDECODED_CELL_MARKER : escapeMarkdownTableCell(cell ?? "");
40030
+ }).join(" | ") + " |";
40031
+ const [header, ...body] = rows;
40032
+ return [line(header), "| " + Array(width).fill("---").join(" | ") + " |", ...body.map(line)].join(
40033
+ "\n"
40034
+ );
40035
+ }
40036
+ function collectNoteTables(rich, noteId3) {
40037
+ const seen = /* @__PURE__ */ new Set();
40038
+ const ordered = (rich.objects || []).filter((object3) => object3.type.includes("table")).sort((a, b) => a.start - b.start).filter((object3) => {
40039
+ if (seen.has(object3.id)) return false;
40040
+ seen.add(object3.id);
40041
+ return true;
40042
+ });
40043
+ const tables = ordered.map((object3, i) => {
40044
+ const index = i + 1;
40045
+ const data = (rich.objectData || []).find((row) => row.id === object3.id);
40046
+ if (!data || !data.mergeable) {
40047
+ return { index, id: object3.id, complete: false, reason: "Native table data is unavailable" };
40048
+ }
40049
+ const attachmentId = noteId3.replace(/ICNote\/p\d+$/, `ICAttachment/p${data.pk}`);
40050
+ try {
40051
+ const table = parseNoteTableCells(Buffer.from(data.mergeable, "hex"));
40052
+ const complete = table.incompleteCells.length === 0;
40053
+ return {
40054
+ index,
40055
+ id: object3.id,
40056
+ attachmentId,
40057
+ complete,
40058
+ ...complete ? {} : { reason: `${table.incompleteCells.length} cell(s) could not be decoded` },
40059
+ rows: table.rows,
40060
+ rowIds: table.rowIds,
40061
+ columnIds: table.columnIds,
40062
+ rowCount: table.rowIds.length,
40063
+ columnCount: table.columnIds.length,
40064
+ incompleteCells: table.incompleteCells,
40065
+ markdown: renderMarkdownTable(table.rows)
40066
+ };
40067
+ } catch (error2) {
40068
+ return {
40069
+ index,
40070
+ id: object3.id,
40071
+ attachmentId,
40072
+ complete: false,
40073
+ reason: error2 instanceof Error ? error2.message : String(error2)
40074
+ };
40075
+ }
40076
+ });
40077
+ const markdown = tables.map(
40078
+ (table) => table.markdown !== void 0 ? table.markdown : `[table ${table.index} could not be decoded: ${table.reason}]`
40079
+ ).join("\n\n");
40080
+ return { tables, tableCellsComplete: tables.every((table) => table.complete), markdown };
40081
+ }
40082
+
39904
40083
  // src/utils/smartFolders.ts
39905
40084
  import { execFileSync as execFileSync4 } from "child_process";
39906
40085
  import * as fs2 from "fs";
@@ -41094,6 +41273,21 @@ var AppleNotesManager = class {
41094
41273
  passwordProtected: parsed.passwordProtected
41095
41274
  };
41096
41275
  }
41276
+ /**
41277
+ * Reads every native table in one note, in body order, as JSON rows and
41278
+ * GitHub-flavored Markdown.
41279
+ *
41280
+ * Reads the NoteStore database read-only (Full Disk Access required); no
41281
+ * AppleScript is involved. Cells that cannot be decoded are returned as null
41282
+ * and flagged, never guessed.
41283
+ *
41284
+ * @param id - CoreData URL identifier for the note
41285
+ * @returns Tables in body order with a combined Markdown rendering
41286
+ * @throws Error when the note's rich data cannot be read (e.g. no Full Disk Access)
41287
+ */
41288
+ getNoteTablesById(id2) {
41289
+ return collectNoteTables(readRichNote(id2), id2);
41290
+ }
41097
41291
  /**
41098
41292
  * Retrieves detailed metadata for a note by title.
41099
41293
  *
@@ -43918,7 +44112,7 @@ import { execFileSync as execFileSync8 } from "child_process";
43918
44112
  import * as fs6 from "fs";
43919
44113
  import * as os6 from "os";
43920
44114
  import * as path6 from "path";
43921
- import { gunzipSync as gunzipSync3 } from "zlib";
44115
+ import { gunzipSync as gunzipSync4 } from "zlib";
43922
44116
  var NOTES_DB_PATH6 = path6.join(
43923
44117
  os6.homedir(),
43924
44118
  "Library/Group Containers/group.com.apple.notes/NoteStore.sqlite"
@@ -44180,7 +44374,7 @@ function queryNotes(expression, options = {}) {
44180
44374
  try {
44181
44375
  decoded = decodeNoteBody(
44182
44376
  new Uint8Array(
44183
- gunzipSync3(Buffer.from(row.data, "hex"), { maxOutputLength: 32 * 1024 * 1024 })
44377
+ gunzipSync4(Buffer.from(row.data, "hex"), { maxOutputLength: 32 * 1024 * 1024 })
44184
44378
  )
44185
44379
  );
44186
44380
  } catch {
@@ -45557,103 +45751,6 @@ function createShutdown(stream, exit, timeoutMs = SHUTDOWN_DRAIN_TIMEOUT_MS) {
45557
45751
  };
45558
45752
  }
45559
45753
 
45560
- // src/utils/noteTables.ts
45561
- import { gunzipSync as gunzipSync4 } from "node:zlib";
45562
- var sub = (f, n) => {
45563
- const value = embeddedMessage(getField(f, n));
45564
- if (!value) throw new Error(`Missing table field ${n}`);
45565
- return value;
45566
- };
45567
- var num = (f, n) => {
45568
- const v = varintValue(getField(f, n));
45569
- if (v === void 0) throw new Error(`Missing table index ${n}`);
45570
- return v;
45571
- };
45572
- var many = (f, n) => getFields(f, n).map((v) => {
45573
- const m = embeddedMessage(v);
45574
- if (!m) throw new Error("Invalid table entry");
45575
- return m;
45576
- });
45577
- var hex = (f) => {
45578
- if (!(f?.value instanceof Uint8Array)) throw new Error("Missing table UUID");
45579
- return Buffer.from(f.value).toString("hex");
45580
- };
45581
- function parseNoteTable(compressed) {
45582
- const root = decodeMessage(gunzipSync4(compressed, { maxOutputLength: 16 * 1024 * 1024 }));
45583
- const data = sub(sub(root, 2), 3), entries = many(data, 3);
45584
- if (entries.length > 1e5) throw new Error("Table too large");
45585
- const keys = getFields(data, 4).map(stringValue), types = getFields(data, 5).map(stringValue), uuids = getFields(data, 6).map(hex);
45586
- const entry = (index) => {
45587
- if (!entries[index]) throw new Error("Invalid table reference");
45588
- return entries[index];
45589
- };
45590
- const uuidIndex = (index) => num(sub(many(sub(entry(index), 13), 3)[0], 2), 2);
45591
- const roots = entries.filter((e) => {
45592
- const map = embeddedMessage(getField(e, 13));
45593
- return map && types[num(map, 1)] === "com.apple.notes.ICTable";
45594
- });
45595
- if (roots.length !== 1) throw new Error("Ambiguous native table root");
45596
- const refs = new Map(
45597
- many(sub(roots[0], 13), 3).filter((m) => ["crRows", "crColumns", "cellColumns"].includes(keys[num(m, 1)] || "")).map((m) => [keys[num(m, 1)], num(sub(m, 2), 6)])
45598
- );
45599
- const ordered = (key) => {
45600
- const ref = refs.get(key);
45601
- if (ref === void 0) throw new Error("Missing table dimension");
45602
- const ordering = sub(sub(entry(ref), 16), 1), array2 = sub(ordering, 1);
45603
- const ids = many(array2, 2).map((a) => hex(getField(a, 2)));
45604
- const map = /* @__PURE__ */ new Map();
45605
- ids.forEach((id2, i) => {
45606
- const index = uuids.indexOf(id2);
45607
- if (index < 0) throw new Error("Missing dimension UUID");
45608
- map.set(index, i);
45609
- });
45610
- const aliases = many(sub(ordering, 2), 1).map((pair) => [
45611
- uuidIndex(num(sub(pair, 1), 6)),
45612
- uuidIndex(num(sub(pair, 2), 6))
45613
- ]);
45614
- for (let pass = 0; pass < aliases.length + 1; pass++) {
45615
- let changed = false;
45616
- for (const [key2, value] of aliases)
45617
- if (map.has(key2) && !map.has(value)) {
45618
- map.set(value, map.get(key2));
45619
- changed = true;
45620
- }
45621
- if (!changed) break;
45622
- }
45623
- return { ids, map };
45624
- };
45625
- const rows = ordered("crRows"), columns = ordered("crColumns");
45626
- if (!rows.ids.length || !columns.ids.length || rows.ids.length * columns.ids.length > 1e5)
45627
- throw new Error("Unsupported table size");
45628
- const values = rows.ids.map(() => columns.ids.map(() => ""));
45629
- const cellRef = refs.get("cellColumns");
45630
- if (cellRef === void 0) throw new Error("Missing table cells");
45631
- for (const column of many(sub(entry(cellRef), 6), 1)) {
45632
- const ci = columns.map.get(uuidIndex(num(sub(column, 1), 6)));
45633
- const cells = entry(num(sub(column, 2), 6));
45634
- for (const row of many(sub(cells, 6), 1)) {
45635
- const ri = rows.map.get(uuidIndex(num(sub(row, 1), 6)));
45636
- if (ri === void 0 || ci === void 0) continue;
45637
- const note = sub(entry(num(sub(row, 2), 6)), 10);
45638
- const text = stringValue(getField(note, 2));
45639
- if (text === void 0 || text.includes("\uFFFC"))
45640
- throw new Error("Embedded or unsupported table cell");
45641
- values[ri][ci] = text.replace(/\n$/u, "");
45642
- }
45643
- }
45644
- const rtl = entries.some((e) => {
45645
- const map = embeddedMessage(getField(e, 13));
45646
- return map && many(map, 3).some(
45647
- (m) => stringValue(getField(sub(m, 2), 4)) === "CRTableColumnDirectionRightToLeft"
45648
- );
45649
- });
45650
- if (rtl) {
45651
- for (const row of values) row.reverse();
45652
- columns.ids.reverse();
45653
- }
45654
- return { rows: values, rowIds: rows.ids, columnIds: columns.ids };
45655
- }
45656
-
45657
45754
  // src/utils/noteBlocks.ts
45658
45755
  import { execFileSync as execFileSync12 } from "node:child_process";
45659
45756
  import { existsSync as existsSync10 } from "node:fs";
@@ -47675,6 +47772,42 @@ registerTool(
47675
47772
  });
47676
47773
  }, "Error reading native objects")
47677
47774
  );
47775
+ registerTool(
47776
+ "get-note-tables",
47777
+ {
47778
+ description: "Use when: reading the native tables in one exact note as data or Markdown.\nReturns: every table in body order as GitHub-flavored Markdown (first row as header) plus JSON rows with stable row/column ids, and tableCellsComplete.\nDo not use when: you need the whole note (get-note-markdown / get-note-content) or native object ranges and checklist ids (get-native-objects).\nSafety: read-only; reads the NoteStore database and requires Full Disk Access. A cell that cannot be decoded is null in rows, listed in incompleteCells, and marked [undecoded cell] in Markdown; it is never guessed. Cell text only: links and styling inside cells are not rendered.",
47779
+ inputSchema: { id: noteIdInput },
47780
+ outputSchema: {
47781
+ id: external_exports.string().optional(),
47782
+ tables: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
47783
+ tableCount: external_exports.number().optional(),
47784
+ tableCellsComplete: external_exports.boolean().optional(),
47785
+ markdown: external_exports.string().optional()
47786
+ },
47787
+ annotations: { readOnlyHint: true }
47788
+ },
47789
+ withErrorHandling(({ id: id2 }) => {
47790
+ const { metadata, message } = getNoteMetadata(id2);
47791
+ if (!metadata) return errorResponse(message || `Failed to read note "${id2}"`);
47792
+ if (metadata.passwordProtected) {
47793
+ return errorResponse(
47794
+ `Note "${id2}" is password-protected; its tables are encrypted. Unlock it in Notes.app first.`
47795
+ );
47796
+ }
47797
+ const result = notesManager.getNoteTablesById(id2);
47798
+ const count = result.tables.length;
47799
+ const summary = count === 0 ? "This note has no native tables." : `${count} table(s)${result.tableCellsComplete ? "" : " (some content could not be decoded; see tables[].reason)"}:
47800
+
47801
+ ${result.markdown}`;
47802
+ return successResponse(summary, {
47803
+ id: id2,
47804
+ tables: result.tables,
47805
+ tableCount: count,
47806
+ tableCellsComplete: result.tableCellsComplete,
47807
+ markdown: result.markdown
47808
+ });
47809
+ }, "Error reading note tables")
47810
+ );
47678
47811
  registerTool(
47679
47812
  "get-note-blocks",
47680
47813
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.8.28",
3
+ "version": "2.8.29",
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",