apple-notes-mcp 2.9.2 → 2.9.3

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 +46 -2
  2. package/build/index.js +363 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -634,6 +634,50 @@ stable code in brackets, such as `[encrypted]` or `[no-full-disk-access]`.
634
634
 
635
635
  ---
636
636
 
637
+ #### `get-note-structure`
638
+
639
+ Returns a read-only overview of one note from the NoteStore database, in one
640
+ call:
641
+
642
+ - `text` (decoded body), `textLength`, `wordCount`, `charCount` (Unicode
643
+ characters, attachment placeholders excluded), and `blockSummary` (the
644
+ `get-note-blocks` summary counts).
645
+ - `links`, each with a `kind`: `inline` (a hyperlink on text), `card` (a rich
646
+ link preview attachment, with `attachmentId` and `previewPath`), `note` (a
647
+ native link chip to another note), or `section` (a native link chip to a
648
+ heading or paragraph, with `section`). Notes deep links also carry
649
+ `targetNote` and `paragraphId`. `linkCounts` totals them.
650
+ - `tags` (native tags in body order) and `attachments`, listed the way
651
+ [`list-attachments`](#list-attachments) with `includePaths` lists them: the
652
+ same `kind` (`image`, `scan`, `drawing`, `pdf`, `audio`, `video`, `url`,
653
+ `table`, `other`), the same body order, and the same `previewPath`, plus the
654
+ card `title`/`url`, `fileSize`, and body position. Gallery items and
655
+ recording parts are nested under `children`; `attachmentCount` counts
656
+ top-level attachments only.
657
+ - `deepLink`, `isShared` (the note or any enclosing folder is shared),
658
+ `isLocked`, `isPinned`, `inRecentlyDeleted`, `lastViewed`,
659
+ `checklistTotal`, `checklistDone`, `hasDrawing` (classic sketches and Paper
660
+ drawings), and `firstImage` (the same lead visual `list-attachments`
661
+ returns with `firstImage`, plus its attachment `id`).
662
+
663
+ `lastViewed` is an ISO date, or null with `lastViewedStatus` set to
664
+ `never-viewed`, `not-recorded`, `malformed`, or `unsupported` (the column
665
+ does not exist on this macOS version). A password-protected note returns its
666
+ metadata and attachment rows with `bodyDecoded: false` and the body-derived
667
+ fields null.
668
+
669
+ **Requires:** Full Disk Access.
670
+
671
+ | Parameter | Type | Required | Description |
672
+ |-----------|------|----------|-------------|
673
+ | `id` | string | Yes | Exact note ID: the `x-coredata://` id, or the note's Notes UUID or numeric key (see [Identifier forms](#identifier-forms)) |
674
+ | `includeText` | boolean | No | Include the decoded text (default true). Text larger than `APPLE_NOTES_MCP_BLOCKS_MAX_BYTES` is omitted with `textOmitted: true` |
675
+
676
+ Link URLs are returned as stored; check `linkSafe` before emitting one into
677
+ HTML.
678
+
679
+ ---
680
+
637
681
  #### `list-native-tags`
638
682
 
639
683
  Lists actual native Notes tags. This differs from textual hashtag search. It is
@@ -2214,7 +2258,7 @@ All configuration is optional — the server works out of the box. Override beha
2214
2258
  | `APPLE_NOTES_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-notes-mcp/config.json` | Path to the JSON config file (see below). |
2215
2259
  | `APPLE_NOTES_MCP_TIMEOUT_MS` | `30000` (30 s) | Total AppleScript operation timeout, including retry attempts and delays. Raise it if full-library operations (large searches, exports) time out on a big Notes library. Per-call `timeoutMs` options still win, and a write tool's `timeoutSeconds` argument overrides it for that call. |
2216
2260
  | `APPLE_NOTES_MCP_EXPORT_MAX_BYTES` | `8388608` (8 MB) | Largest response `export-notes-json` sends; a page closes early to stay under it. `export-notes-markdown` returns inline Markdown up to half of it. The default sits below the 10 MB per-message limit of MCP SDK stdio clients, which drop the connection on anything larger. Raise it only if your MCP client accepts bigger messages. |
2217
- | `APPLE_NOTES_MCP_BLOCKS_MAX_BYTES` | `4194304` (4 MB) | Largest block payload one [`get-note-blocks`](#get-note-blocks) page returns; the page closes early to stay under it, and a single oversized paragraph comes back with `textOmitted: true`. |
2261
+ | `APPLE_NOTES_MCP_BLOCKS_MAX_BYTES` | `4194304` (4 MB) | Largest block payload one [`get-note-blocks`](#get-note-blocks) page returns; the page closes early to stay under it, and a single oversized paragraph comes back with `textOmitted: true`. [`get-note-structure`](#get-note-structure) also omits note text larger than this. |
2218
2262
  | `APPLE_NOTES_MCP_MAX_RETRIES` | `2` | Maximum attempts for a read-only AppleScript call that fails with a **transient** error (Notes.app busy / not responding / lost connection). `2` means one retry; set `1` to fail fast with no retries. Retries share the single `APPLE_NOTES_MCP_TIMEOUT_MS` budget rather than each getting a fresh one, and a retry is skipped when under a second of that budget remains — so this is a ceiling, not a guarantee. In particular a call that exhausts the budget with a **timeout** has no time left to retry by construction. Mutating operations run once because a timeout can occur after Notes.app applied the change. Non-transient errors (e.g. "note not found") never retry. |
2219
2263
  | `APPLE_NOTES_MCP_RETRY_DELAY_MS` | `1000` (1 s) | Base delay before the first retry; subsequent retries back off exponentially (1s, 2s, 4s, ...). |
2220
2264
  | `APPLE_NOTES_MCP_ENABLE_PRIVATE` | unset | Set to `1` to allow the opt-in [private helper](#private-helper-opt-in-unsupported-apple-api). Any other value keeps it off. |
@@ -2247,7 +2291,7 @@ MCP stores no secrets, but as a general rule keep only non-secret config here.
2247
2291
 
2248
2292
  ## Full Disk Access
2249
2293
 
2250
- Several tools read directly from the Apple Notes SQLite database, which lives in a macOS-protected directory. Those tools require **Full Disk Access** for the process running the MCP server: `get-checklist-state`, `get-note-metadata`, `get-note-blocks`, `export-notes-markdown`, `export-notes-html`, `get-audio-transcripts`, `list-special-notes`, `list-native-tags`, `list-recent-notes`, `list-folder-tree`, `get-note-link`, the checklist annotations in `get-note-markdown`, `list-attachments` with `includePaths` or `firstImage`, `export-attachments`, `list-paper-attachments`, `export-paper-image`, and the database half of `get-sync-status`.
2294
+ Several tools read directly from the Apple Notes SQLite database, which lives in a macOS-protected directory. Those tools require **Full Disk Access** for the process running the MCP server: `get-checklist-state`, `get-note-metadata`, `get-note-blocks`, `get-note-structure`, `export-notes-markdown`, `export-notes-html`, `get-audio-transcripts`, `list-special-notes`, `list-native-tags`, `list-recent-notes`, `list-folder-tree`, `get-note-link`, the checklist annotations in `get-note-markdown`, `list-attachments` with `includePaths` or `firstImage`, `export-attachments`, `list-paper-attachments`, `export-paper-image`, and the database half of `get-sync-status`.
2251
2295
 
2252
2296
  > 📘 **For the full why-and-how walkthrough (which app to grant, verifying with `doctor`, graceful degradation), see the [Full Disk Access Setup Guide](https://github.com/sweetrb/apple-notes-mcp/blob/main/docs/FULL-DISK-ACCESS.md).** The summary below is the quick version.
2253
2297
 
package/build/index.js CHANGED
@@ -46173,10 +46173,10 @@ function parseSince(input) {
46173
46173
  return { modified: (ms - CORE_DATA_EPOCH_MS) / 1e3 };
46174
46174
  }
46175
46175
  function textStats(text2) {
46176
- const visible = text2.replace(OBJECT_REPLACEMENT, "");
46177
- let wordCount = 0;
46178
- for (const token of visible.split(/\s+/u)) if (/[\p{L}\p{N}]/u.test(token)) wordCount++;
46179
- return { wordCount, charCount: [...visible].length };
46176
+ const visible2 = text2.replace(OBJECT_REPLACEMENT, "");
46177
+ let wordCount2 = 0;
46178
+ for (const token of visible2.split(/\s+/u)) if (/[\p{L}\p{N}]/u.test(token)) wordCount2++;
46179
+ return { wordCount: wordCount2, charCount: [...visible2].length };
46180
46180
  }
46181
46181
  function previewText(text2) {
46182
46182
  const flat = text2.replace(OBJECT_REPLACEMENT, " ").replace(/\s+/gu, " ").trim();
@@ -46329,12 +46329,12 @@ function folderTree(options = {}) {
46329
46329
  }
46330
46330
  function assembleFolderTree(context, counts, includeDeleted, scope2) {
46331
46331
  const paths = folderPaths(context.folders);
46332
- const visible = context.folders.filter(
46332
+ const visible2 = context.folders.filter(
46333
46333
  (folder) => (includeDeleted || !folder.tombstoned) && (!scope2 || folder.account === scope2.pk)
46334
46334
  );
46335
- const byPk = new Map(visible.map((folder) => [folder.pk, folder]));
46335
+ const byPk = new Map(visible2.map((folder) => [folder.pk, folder]));
46336
46336
  const nodes = /* @__PURE__ */ new Map();
46337
- for (const folder of visible) {
46337
+ for (const folder of visible2) {
46338
46338
  const node = {
46339
46339
  id: `x-coredata://${context.uuid}/ICFolder/p${folder.pk}`,
46340
46340
  identifier: folder.identifier,
@@ -46358,7 +46358,7 @@ function assembleFolderTree(context, counts, includeDeleted, scope2) {
46358
46358
  return folder.parent;
46359
46359
  };
46360
46360
  const roots = /* @__PURE__ */ new Map();
46361
- for (const folder of visible) {
46361
+ for (const folder of visible2) {
46362
46362
  const node = nodes.get(folder.pk);
46363
46363
  const parent = parentOf(folder);
46364
46364
  if (parent !== void 0) nodes.get(parent).children.push(node);
@@ -47790,7 +47790,7 @@ function renderMarkdown(markdown, options = {}) {
47790
47790
  }
47791
47791
  return text2;
47792
47792
  };
47793
- const visible = (html2) => withoutTags(html2).replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
47793
+ const visible2 = (html2) => withoutTags(html2).replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
47794
47794
  let html = "", list, quote = false, code, previousBlank = true;
47795
47795
  const close = () => {
47796
47796
  if (list) {
@@ -47837,7 +47837,7 @@ function renderMarkdown(markdown, options = {}) {
47837
47837
  }
47838
47838
  const rendered = inline(text2);
47839
47839
  html += `<div>${rendered}</div>`;
47840
- expect.quotes[expect.quotes.length - 1] += ` ${visible(rendered)}`;
47840
+ expect.quotes[expect.quotes.length - 1] += ` ${visible2(rendered)}`;
47841
47841
  previousBlank = false;
47842
47842
  continue;
47843
47843
  }
@@ -47860,7 +47860,7 @@ function renderMarkdown(markdown, options = {}) {
47860
47860
  }
47861
47861
  const rendered = inline(task[2]);
47862
47862
  html += `<li>${rendered}</li>`;
47863
- expect.checklist.push({ text: visible(rendered).trim(), done: task[1] === "x" });
47863
+ expect.checklist.push({ text: visible2(rendered).trim(), done: task[1] === "x" });
47864
47864
  previousBlank = false;
47865
47865
  continue;
47866
47866
  }
@@ -49052,6 +49052,293 @@ function createShutdown(stream, exit, timeoutMs = SHUTDOWN_DRAIN_TIMEOUT_MS) {
49052
49052
  };
49053
49053
  }
49054
49054
 
49055
+ // src/utils/noteStructure.ts
49056
+ import { dirname as dirname4 } from "node:path";
49057
+
49058
+ // src/utils/noteLinks.ts
49059
+ var NOTE_LINK_UTI = "com.apple.notes.inlinetextattachment.link";
49060
+ var HASHTAG_UTI2 = "com.apple.notes.inlinetextattachment.hashtag";
49061
+ var UUID = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
49062
+ function parseNotesShowUrl(url) {
49063
+ const match = /^(?:apple)?notes:\/\/showNote\?(.*)$/i.exec(url);
49064
+ if (!match) return void 0;
49065
+ const query2 = new URLSearchParams(match[1]);
49066
+ const note = query2.get("identifier") ?? "";
49067
+ const paragraph = query2.get("paragraphID") ?? "";
49068
+ const result = {};
49069
+ if (UUID.test(note)) result.targetNote = note.toUpperCase();
49070
+ if (UUID.test(paragraph)) result.paragraphId = paragraph.toUpperCase();
49071
+ return result;
49072
+ }
49073
+ function inlineLinks(doc) {
49074
+ const links = [];
49075
+ let last;
49076
+ for (const block of doc.blocks)
49077
+ for (const run of block.runs) {
49078
+ if (!run.link) {
49079
+ last = void 0;
49080
+ continue;
49081
+ }
49082
+ if (last && last.url === run.link && last.start + last.length === run.start) {
49083
+ last.length += run.length;
49084
+ last.text += run.text;
49085
+ continue;
49086
+ }
49087
+ last = {
49088
+ kind: "inline",
49089
+ url: run.link,
49090
+ linkSafe: run.linkSafe === true,
49091
+ text: run.text,
49092
+ start: run.start,
49093
+ length: run.length,
49094
+ blockIndex: block.index,
49095
+ ...parseNotesShowUrl(run.link)
49096
+ };
49097
+ links.push(last);
49098
+ }
49099
+ return links;
49100
+ }
49101
+ function markerPosition(doc, identifier) {
49102
+ if (!doc) return {};
49103
+ const wanted = identifier.toUpperCase();
49104
+ const marker = doc.attachments.find((item) => item.id.toUpperCase() === wanted);
49105
+ return marker ? { start: marker.start, blockIndex: marker.blockIndex, inBody: true } : { inBody: false };
49106
+ }
49107
+ function nativeLink(row, doc) {
49108
+ if (!row.token) return void 0;
49109
+ const target = parseNotesShowUrl(row.token) ?? {};
49110
+ const kind = target.paragraphId ? "section" : "note";
49111
+ return {
49112
+ kind,
49113
+ url: row.token,
49114
+ linkSafe: isSafeLink(row.token),
49115
+ ...row.alt ? { text: row.alt } : {},
49116
+ ...markerPosition(doc, row.identifier),
49117
+ ...target,
49118
+ ...kind === "section" && row.alt ? { section: row.alt } : {},
49119
+ attachmentIdentifier: row.identifier
49120
+ };
49121
+ }
49122
+ function cardLink(row, {
49123
+ doc,
49124
+ noteId: noteId3,
49125
+ previewPath
49126
+ } = {}) {
49127
+ if (!row.url) return void 0;
49128
+ return {
49129
+ kind: "card",
49130
+ url: row.url,
49131
+ linkSafe: isSafeLink(row.url),
49132
+ ...row.title ? { text: row.title } : {},
49133
+ ...markerPosition(doc, row.identifier),
49134
+ ...parseNotesShowUrl(row.url),
49135
+ attachmentIdentifier: row.identifier,
49136
+ ...noteId3 ? { attachmentId: attachmentCoreDataId(noteId3, row.pk) } : {},
49137
+ ...previewPath !== void 0 ? { previewPath } : {}
49138
+ };
49139
+ }
49140
+
49141
+ // src/utils/noteStructure.ts
49142
+ var APPLE_EPOCH_OFFSET = 978307200;
49143
+ var LAST_VIEWED_NEVER = -541228980;
49144
+ function lastViewedOf(raw, columnPresent = true, now = Date.now()) {
49145
+ if (!columnPresent) return { lastViewed: null, lastViewedStatus: "unsupported" };
49146
+ if (raw === null || raw === void 0)
49147
+ return { lastViewed: null, lastViewedStatus: "not-recorded" };
49148
+ const seconds = typeof raw === "number" ? raw : Number.NaN;
49149
+ if (seconds === LAST_VIEWED_NEVER) return { lastViewed: null, lastViewedStatus: "never-viewed" };
49150
+ const ms = (seconds + APPLE_EPOCH_OFFSET) * 1e3;
49151
+ if (!Number.isFinite(ms) || ms < Date.UTC(2007, 0, 1) || ms > now + 864e5)
49152
+ return { lastViewed: null, lastViewedStatus: "malformed" };
49153
+ return { lastViewed: new Date(ms).toISOString(), lastViewedStatus: "viewed" };
49154
+ }
49155
+ var visible = (text2) => text2.replace(/\ufffc/g, "");
49156
+ var wordCount = (text2) => visible(text2).split(/\s+/u).filter((word) => word !== "").length;
49157
+ var charCount = (text2) => Array.from(visible(text2)).length;
49158
+ function noteStructureSql(columns) {
49159
+ const c = (alias, name) => col(columns, alias, name);
49160
+ const sharedExpr = columns.has("ZSERVERSHAREDATA") ? `(n.ZSERVERSHAREDATA IS NOT NULL OR EXISTS (
49161
+ WITH RECURSIVE up(pk, depth) AS (
49162
+ SELECT n.ZFOLDER, 0 UNION ALL
49163
+ SELECT ${c("p", "ZPARENT")}, depth + 1 FROM up JOIN ZICCLOUDSYNCINGOBJECT p ON p.Z_PK = up.pk
49164
+ WHERE depth < 64)
49165
+ SELECT 1 FROM up JOIN ZICCLOUDSYNCINGOBJECT s ON s.Z_PK = up.pk WHERE s.ZSERVERSHAREDATA IS NOT NULL))` : "NULL";
49166
+ const children = columns.has("ZPARENTATTACHMENT") ? ` OR att.ZPARENTATTACHMENT IN (SELECT p.Z_PK FROM ZICCLOUDSYNCINGOBJECT p WHERE ${c("p", "ZNOTE")} = @pk)` : "";
49167
+ return [
49168
+ "BEGIN;",
49169
+ `SELECT json_object('k', 'note',
49170
+ 'isNote', n.Z_ENT = ${entity("ICNote")},
49171
+ 'identifier', ${c("n", "ZIDENTIFIER")},
49172
+ 'title', ${c("n", "ZTITLE1")},
49173
+ 'folder', ${c("f", "ZTITLE2")},
49174
+ 'inTrash', CASE WHEN f.Z_PK IS NULL THEN 0 ELSE ${trashFolderSql(columns, "f")} END,
49175
+ 'account', ${c("a", "ZNAME")},
49176
+ 'locked', ${c("n", "ZISPASSWORDPROTECTED")},
49177
+ 'pinned', ${c("n", "ZISPINNED")},
49178
+ 'shared', ${sharedExpr},
49179
+ 'lastViewed', ${c("n", "ZLASTVIEWEDMODIFICATIONDATE")},
49180
+ 'data', (SELECT hex(d.ZDATA) FROM ZICNOTEDATA d WHERE d.ZNOTE = n.Z_PK),
49181
+ 'encrypted', (SELECT d.ZCRYPTOINITIALIZATIONVECTOR IS NOT NULL FROM ZICNOTEDATA d WHERE d.ZNOTE = n.Z_PK))
49182
+ FROM ZICCLOUDSYNCINGOBJECT n
49183
+ LEFT JOIN ZICCLOUDSYNCINGOBJECT f ON f.Z_PK = ${c("n", "ZFOLDER")}
49184
+ LEFT JOIN ZICCLOUDSYNCINGOBJECT a ON a.Z_PK = ${accountRef(columns, "n")} AND a.Z_ENT = ${entity("ICAccount")}
49185
+ WHERE n.Z_PK = @pk;`,
49186
+ `SELECT json_object('k', 'attachment',
49187
+ 'pk', att.Z_PK,
49188
+ 'title', ${c("att", "ZTITLE")},
49189
+ 'url', ${c("att", "ZURLSTRING")},
49190
+ 'fileSize', ${c("att", "ZFILESIZE")})
49191
+ FROM ZICCLOUDSYNCINGOBJECT att
49192
+ WHERE att.Z_ENT = ${entity("ICAttachment")} AND ${notTombstonedSql(columns, "att")}
49193
+ AND (${c("att", "ZNOTE")} = @pk${children});`,
49194
+ `SELECT json_object('k', 'inline',
49195
+ 'identifier', ${c("i", "ZIDENTIFIER")},
49196
+ 'uti', ${c("i", "ZTYPEUTI1")},
49197
+ 'alt', ${c("i", "ZALTTEXT")},
49198
+ 'token', ${c("i", "ZTOKENCONTENTIDENTIFIER")})
49199
+ FROM ZICCLOUDSYNCINGOBJECT i
49200
+ WHERE i.Z_ENT = ${entity("ICInlineAttachment")} AND ${c("i", "ZNOTE1")} = @pk
49201
+ AND ${notTombstonedSql(columns, "i")} AND ${c("i", "ZTYPEUTI1")} IN ('${HASHTAG_UTI2}', '${NOTE_LINK_UTI}');`,
49202
+ "COMMIT;"
49203
+ ].join("\n");
49204
+ }
49205
+ function describeNoteStructure(s) {
49206
+ const parts = [
49207
+ s.bodyDecoded ? `${s.blockSummary.blocks} blocks, ${s.wordCount} words` : `body not decoded (${s.bodyError})`,
49208
+ `${s.links.length} links (inline ${s.linkCounts.inline}, card ${s.linkCounts.card}, note ${s.linkCounts.note}, section ${s.linkCounts.section})`,
49209
+ `${s.attachmentCount} attachments`,
49210
+ `${s.tags.length} tags`
49211
+ ];
49212
+ if (s.checklistTotal) parts.push(`checklist ${s.checklistDone}/${s.checklistTotal} done`);
49213
+ if (s.hasDrawing) parts.push("has a drawing");
49214
+ if (s.isLocked) parts.push("locked");
49215
+ if (s.isShared) parts.push("shared");
49216
+ return `Note structure: ${parts.join("; ")}.`;
49217
+ }
49218
+ function readNoteStructure(id2, { dbPath: dbPath2 = NOTES_DB_PATH7, includeText = true, maxTextBytes = 4 * 1024 * 1024 } = {}) {
49219
+ const { pk } = parseNoteId2(id2);
49220
+ const columns = readColumns(dbPath2);
49221
+ const rows = parseJsonLines(
49222
+ runReadOnlySql(dbPath2, noteStructureSql(columns), { pk: { int: pk } })
49223
+ );
49224
+ const note = rows.find((row) => row.k === "note");
49225
+ if (!note?.isNote) throw new NoteStoreError(`No note found for ID "${id2}".`, "invalid_input");
49226
+ const details = new Map(
49227
+ rows.filter((row) => row.k === "attachment").map((row) => [row.pk, row])
49228
+ );
49229
+ const inlineRows = rows.filter((row) => row.k === "inline");
49230
+ let doc;
49231
+ let bodyError;
49232
+ if (note.encrypted || note.locked) bodyError = "encrypted";
49233
+ else if (!note.data || !/^[0-9a-f]+$/i.test(note.data)) bodyError = "no-body";
49234
+ else
49235
+ try {
49236
+ doc = decodeCompressedNoteBlocks(Buffer.from(note.data, "hex"));
49237
+ } catch (error2) {
49238
+ if (!(error2 instanceof NoteBlocksError)) throw error2;
49239
+ bodyError = error2.code;
49240
+ }
49241
+ const { rows: attachmentRows, bodyOrder } = readNoteAttachmentRows(id2, dbPath2);
49242
+ const assets = assembleAttachmentAssets(attachmentRows, bodyOrder, dirname4(dbPath2));
49243
+ const roots = /* @__PURE__ */ new Map();
49244
+ const attachments = [];
49245
+ const pks = /* @__PURE__ */ new Map();
49246
+ for (const record2 of assets.attachments) {
49247
+ const detail = details.get(record2.pk);
49248
+ const parent = record2.parentIdentifier !== null ? roots.get(record2.parentIdentifier) : void 0;
49249
+ const item = {
49250
+ id: attachmentCoreDataId(id2, record2.pk),
49251
+ identifier: record2.identifier,
49252
+ uti: record2.uti,
49253
+ kind: record2.kind,
49254
+ ...detail?.title ? { title: detail.title } : {},
49255
+ ...record2.filename ? { filename: record2.filename } : {},
49256
+ ...detail?.url ? { url: detail.url } : {},
49257
+ ...typeof detail?.fileSize === "number" ? { fileSize: detail.fileSize } : {},
49258
+ ...markerPosition(doc, record2.identifier),
49259
+ previewPath: record2.previewPath,
49260
+ ...parent ? { parentId: parent.id } : {}
49261
+ };
49262
+ pks.set(item, record2.pk);
49263
+ if (parent) (parent.children ||= []).push(item);
49264
+ else {
49265
+ roots.set(record2.identifier, item);
49266
+ attachments.push(item);
49267
+ }
49268
+ }
49269
+ const flat = attachments.flatMap((item) => [item, ...item.children ?? []]);
49270
+ const links = doc ? inlineLinks(doc) : [];
49271
+ for (const item of flat) {
49272
+ if (item.kind !== "url") continue;
49273
+ const link = cardLink(
49274
+ {
49275
+ pk: pks.get(item),
49276
+ identifier: item.identifier,
49277
+ url: item.url ?? null,
49278
+ title: item.title ?? null
49279
+ },
49280
+ { doc, noteId: id2, previewPath: item.previewPath }
49281
+ );
49282
+ if (link) links.push(link);
49283
+ }
49284
+ const tagOrder = [];
49285
+ for (const row of inlineRows) {
49286
+ if (!row.identifier) continue;
49287
+ if (row.uti === NOTE_LINK_UTI) {
49288
+ const link = nativeLink({ identifier: row.identifier, token: row.token, alt: row.alt }, doc);
49289
+ if (link) links.push(link);
49290
+ } else if (row.alt) {
49291
+ const position = markerPosition(doc, row.identifier);
49292
+ if (position.inBody !== false)
49293
+ tagOrder.push({ tag: row.alt.replace(/^#/, ""), start: position.start ?? 0 });
49294
+ }
49295
+ }
49296
+ links.sort((a, b) => (a.start ?? Number.MAX_SAFE_INTEGER) - (b.start ?? Number.MAX_SAFE_INTEGER));
49297
+ const tags = [...new Set(tagOrder.sort((a, b) => a.start - b.start).map((entry) => entry.tag))];
49298
+ let text2;
49299
+ let textOmitted;
49300
+ if (doc && includeText) {
49301
+ if (Buffer.byteLength(doc.text) <= maxTextBytes) text2 = doc.text;
49302
+ else textOmitted = true;
49303
+ }
49304
+ const linkCounts = { inline: 0, card: 0, note: 0, section: 0 };
49305
+ for (const link of links) linkCounts[link.kind]++;
49306
+ const lastViewed = lastViewedOf(note.lastViewed, columns.has("ZLASTVIEWEDMODIFICATIONDATE"));
49307
+ const first2 = selectFirstImage(assets);
49308
+ return {
49309
+ id: id2,
49310
+ identifier: note.identifier,
49311
+ deepLink: note.identifier ? `notes://showNote?identifier=${note.identifier}` : null,
49312
+ title: note.title,
49313
+ folder: note.folder,
49314
+ account: note.account,
49315
+ inRecentlyDeleted: note.inTrash === 1,
49316
+ isShared: note.shared === null ? null : note.shared === 1,
49317
+ isLocked: note.locked === 1 || note.encrypted === 1,
49318
+ isPinned: note.pinned === null ? null : note.pinned === 1,
49319
+ ...lastViewed,
49320
+ bodyDecoded: doc !== void 0,
49321
+ ...bodyError ? { bodyError } : {},
49322
+ ...text2 !== void 0 ? { text: text2 } : {},
49323
+ ...textOmitted ? { textOmitted } : {},
49324
+ textLength: doc ? doc.textLength : null,
49325
+ wordCount: doc ? wordCount(doc.text) : null,
49326
+ charCount: doc ? charCount(doc.text) : null,
49327
+ blockSummary: doc ? doc.summary : null,
49328
+ links,
49329
+ linkCounts,
49330
+ linksComplete: doc !== void 0,
49331
+ tags,
49332
+ attachments,
49333
+ attachmentCount: attachments.length,
49334
+ checklistTotal: doc ? doc.summary.checklist.total : null,
49335
+ checklistDone: doc ? doc.summary.checklist.done : null,
49336
+ hasDrawing: flat.some((item) => item.kind === "drawing"),
49337
+ firstImage: first2 ? { ...first2, id: attachmentCoreDataId(id2, first2.pk) } : null,
49338
+ ...doc ? { undecodedFields: doc.undecodedFields } : {}
49339
+ };
49340
+ }
49341
+
49055
49342
  // src/utils/linkInsert.ts
49056
49343
  var MAX_LINK_URL_LENGTH = 4096;
49057
49344
  var MAX_LINK_LABEL_LENGTH = 2e3;
@@ -49169,7 +49456,7 @@ function insertLink(request, deps) {
49169
49456
 
49170
49457
  // src/services/notesExport.ts
49171
49458
  import { mkdirSync as mkdirSync5 } from "node:fs";
49172
- import { basename as basename4, dirname as dirname4, extname as extname4, join as join21 } from "node:path";
49459
+ import { basename as basename4, dirname as dirname5, extname as extname4, join as join21 } from "node:path";
49173
49460
 
49174
49461
  // src/utils/exportAssets.ts
49175
49462
  import {
@@ -50492,7 +50779,7 @@ function loadNotes(ids, single, read) {
50492
50779
  return { notes, skipped };
50493
50780
  }
50494
50781
  function openOutput(output) {
50495
- mkdirSync5(dirname4(output), { recursive: true });
50782
+ mkdirSync5(dirname5(output), { recursive: true });
50496
50783
  try {
50497
50784
  return openCreateOnly(output);
50498
50785
  } catch (error2) {
@@ -50517,7 +50804,7 @@ function exportNotesMarkdown(request, deps) {
50517
50804
  const ids = selectNotes(request, deps);
50518
50805
  const { notes, skipped } = loadNotes(ids, !!request.id, deps.readNote);
50519
50806
  const fd = output ? openOutput(output) : void 0;
50520
- const writer = assetsDir ? new SidecarWriter(assetsDir, output ? dirname4(output) : void 0) : void 0;
50807
+ const writer = assetsDir ? new SidecarWriter(assetsDir, output ? dirname5(output) : void 0) : void 0;
50521
50808
  const ctx = {
50522
50809
  stats: emptyStats(),
50523
50810
  ...writer ? { writer, locator: deps.locator ?? new AssetLocator() } : {}
@@ -50547,7 +50834,7 @@ function exportNotesMarkdown(request, deps) {
50547
50834
  return { ...receipt, markdown };
50548
50835
  }
50549
50836
  function defaultSidecarDir(output) {
50550
- return join21(dirname4(output), `${basename4(output, extname4(output))}.assets`);
50837
+ return join21(dirname5(output), `${basename4(output, extname4(output))}.assets`);
50551
50838
  }
50552
50839
  function exportNotesHtml(request, deps) {
50553
50840
  if (!request.outputPath)
@@ -50568,7 +50855,7 @@ function exportNotesHtml(request, deps) {
50568
50855
  const ids = selectNotes(request, deps);
50569
50856
  const { notes, skipped } = loadNotes(ids, !!request.id, deps.readNote);
50570
50857
  const fd = openOutput(output);
50571
- const writer = assetsDir ? new SidecarWriter(assetsDir, dirname4(output)) : new DataUrlWriter();
50858
+ const writer = assetsDir ? new SidecarWriter(assetsDir, dirname5(output)) : new DataUrlWriter();
50572
50859
  const ctx = {
50573
50860
  stats: emptyStats(),
50574
50861
  writer,
@@ -51674,7 +51961,7 @@ function registerNativeOperations(server2, manager) {
51674
51961
  import { spawnSync as spawnSync2 } from "node:child_process";
51675
51962
  import { existsSync as existsSync13 } from "node:fs";
51676
51963
  import { release as release2 } from "node:os";
51677
- import { dirname as dirname5, resolve as resolve4 } from "node:path";
51964
+ import { dirname as dirname6, resolve as resolve4 } from "node:path";
51678
51965
  import { fileURLToPath } from "node:url";
51679
51966
  var OPTIONAL_BRIDGE_NOTE = "(optional \u2014 needed only for create-note format: markdown, macOS 26+)";
51680
51967
  var MARKDOWN_MIN_DARWIN_MAJOR = 25;
@@ -51697,7 +51984,7 @@ function setupShortcuts(checkOnly, dependencies = {}) {
51697
51984
  const result = spawnSync2("/usr/bin/open", [path10], { encoding: "utf8" });
51698
51985
  return result.status === 0 ? { ok: true } : { ok: false, error: result.stderr || result.error?.message || "open failed" };
51699
51986
  });
51700
- const baseDirectory = dependencies.baseDirectory || resolve4(dirname5(fileURLToPath(import.meta.url)), "../shortcuts");
51987
+ const baseDirectory = dependencies.baseDirectory || resolve4(dirname6(fileURLToPath(import.meta.url)), "../shortcuts");
51701
51988
  const osRelease = (dependencies.osRelease || release2)();
51702
51989
  const darwinMajor = Number.parseInt(osRelease.split(".")[0], 10);
51703
51990
  const items = shortcutFiles.map(({ name, file, optional: optional2 }) => {
@@ -51778,7 +52065,7 @@ import { spawnSync as spawnSync3 } from "node:child_process";
51778
52065
  import { createHash as createHash4 } from "node:crypto";
51779
52066
  import { existsSync as existsSync14, readFileSync as readFileSync4 } from "node:fs";
51780
52067
  import { homedir as homedir19 } from "node:os";
51781
- import { dirname as dirname6, join as join24, resolve as resolve5 } from "node:path";
52068
+ import { dirname as dirname7, join as join24, resolve as resolve5 } from "node:path";
51782
52069
  import { fileURLToPath as fileURLToPath2 } from "node:url";
51783
52070
  var PRIVATE_HELPER_PROTOCOL = 1;
51784
52071
  var ENABLE_ENV = "APPLE_NOTES_MCP_ENABLE_PRIVATE";
@@ -51803,7 +52090,7 @@ var manifestSchema = external_exports.object({
51803
52090
  osVersion: external_exports.string(),
51804
52091
  compiler: external_exports.string()
51805
52092
  });
51806
- function packageRoot(fromDir = dirname6(fileURLToPath2(import.meta.url))) {
52093
+ function packageRoot(fromDir = dirname7(fileURLToPath2(import.meta.url))) {
51807
52094
  let dir = fromDir;
51808
52095
  for (; ; ) {
51809
52096
  const candidate = join24(dir, "package.json");
@@ -51814,7 +52101,7 @@ function packageRoot(fromDir = dirname6(fileURLToPath2(import.meta.url))) {
51814
52101
  } catch {
51815
52102
  }
51816
52103
  }
51817
- const parent = dirname6(dir);
52104
+ const parent = dirname7(dir);
51818
52105
  if (parent === dir) return resolve5(fromDir, "..");
51819
52106
  dir = parent;
51820
52107
  }
@@ -52023,9 +52310,9 @@ function parseOrThrow(schema, value) {
52023
52310
  );
52024
52311
  return parsed.data;
52025
52312
  }
52026
- var UUID = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
52313
+ var UUID2 = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
52027
52314
  function assertNoteIdentifier(identifier) {
52028
- if (!UUID.test(identifier))
52315
+ if (!UUID2.test(identifier))
52029
52316
  throw new PrivateHelperError("invalid_request", "identifier must be a Notes UUID");
52030
52317
  }
52031
52318
  function probePrivateHelper(deps = defaultDeps2()) {
@@ -53435,6 +53722,59 @@ registerTool(
53435
53722
  );
53436
53723
  }, "Error reading note blocks")
53437
53724
  );
53725
+ registerTool(
53726
+ "get-note-structure",
53727
+ {
53728
+ description: "Use when: you want one read-only overview of a note by exact id: decoded text, a block summary, every link with its kind (inline hyperlink, rich link card, native note link, native section link, with target note and paragraph UUIDs when the URL carries them), native tags, attachments with the same kind, body order and preview list-attachments reports (gallery and recording children nested), and metadata: deepLink, isShared, isLocked, isPinned, inRecentlyDeleted, lastViewed, wordCount, charCount, attachmentCount, checklistTotal/checklistDone, hasDrawing, firstImage.\nReturns: the structure object. For a password-protected note, metadata and attachment rows only (bodyDecoded false, body-derived fields null). lastViewed is null with lastViewedStatus never-viewed, not-recorded, malformed or unsupported when Notes holds no real view date.\nDo not use when: you need per-paragraph formatting (get-note-blocks) or the editable HTML body (get-note-content).\nSafety: read-only; reads the NoteStore database and the Notes data folder directly and requires Full Disk Access. Link URLs are returned as stored; check linkSafe before emitting them into HTML.",
53729
+ inputSchema: {
53730
+ id: noteIdInput,
53731
+ includeText: external_exports.boolean().optional().describe(
53732
+ "Include the decoded note text (default true). Text over APPLE_NOTES_MCP_BLOCKS_MAX_BYTES is omitted with textOmitted: true"
53733
+ )
53734
+ },
53735
+ outputSchema: {
53736
+ id: external_exports.string().optional(),
53737
+ identifier: external_exports.string().nullable().optional(),
53738
+ deepLink: external_exports.string().nullable().optional(),
53739
+ title: external_exports.string().nullable().optional(),
53740
+ folder: external_exports.string().nullable().optional(),
53741
+ account: external_exports.string().nullable().optional(),
53742
+ inRecentlyDeleted: external_exports.boolean().optional(),
53743
+ isShared: external_exports.boolean().nullable().optional(),
53744
+ isLocked: external_exports.boolean().optional(),
53745
+ isPinned: external_exports.boolean().nullable().optional(),
53746
+ lastViewed: external_exports.string().nullable().optional(),
53747
+ lastViewedStatus: external_exports.string().optional(),
53748
+ bodyDecoded: external_exports.boolean().optional(),
53749
+ bodyError: external_exports.string().optional(),
53750
+ text: external_exports.string().optional(),
53751
+ textOmitted: external_exports.boolean().optional(),
53752
+ textLength: external_exports.number().nullable().optional(),
53753
+ wordCount: external_exports.number().nullable().optional(),
53754
+ charCount: external_exports.number().nullable().optional(),
53755
+ blockSummary: external_exports.record(external_exports.unknown()).nullable().optional(),
53756
+ links: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
53757
+ linkCounts: external_exports.record(external_exports.unknown()).optional(),
53758
+ linksComplete: external_exports.boolean().optional(),
53759
+ tags: external_exports.array(external_exports.string()).optional(),
53760
+ attachments: external_exports.array(external_exports.record(external_exports.unknown())).optional(),
53761
+ attachmentCount: external_exports.number().optional(),
53762
+ checklistTotal: external_exports.number().nullable().optional(),
53763
+ checklistDone: external_exports.number().nullable().optional(),
53764
+ hasDrawing: external_exports.boolean().optional(),
53765
+ firstImage: external_exports.record(external_exports.unknown()).nullable().optional(),
53766
+ undecodedFields: external_exports.record(external_exports.unknown()).optional()
53767
+ },
53768
+ annotations: { readOnlyHint: true }
53769
+ },
53770
+ withErrorHandling(({ id: id2, includeText }) => {
53771
+ const structure = readNoteStructure(id2, {
53772
+ includeText: includeText ?? true,
53773
+ maxTextBytes: blocksMaxResponseBytes()
53774
+ });
53775
+ return successResponse(describeNoteStructure(structure), { ...structure });
53776
+ }, "Error reading note structure")
53777
+ );
53438
53778
  registerTool(
53439
53779
  "list-native-tags",
53440
53780
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.9.2",
3
+ "version": "2.9.3",
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",