apple-notes-mcp 2.8.37 → 2.8.38

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 +45 -1
  2. package/build/index.js +455 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1360,6 +1360,50 @@ deleted if a later step fails.
1360
1360
 
1361
1361
  ---
1362
1362
 
1363
+ #### `export-notes-html`
1364
+
1365
+ Exports one note, or the notes of one folder, as one standalone HTML file
1366
+ rendered from the decoded note body. It uses the same block model and
1367
+ attachment handling as [`export-notes-markdown`](#export-notes-markdown).
1368
+ Headings become `h1`-`h3`, lists nest by indent as `ul`/`ol`, checklists show
1369
+ disabled checkboxes, block quotes and monospaced paragraphs become
1370
+ `blockquote` and `pre`, and inline runs keep bold, italic, underline,
1371
+ strikethrough, highlight, superscript, subscript, text color and safe links.
1372
+ Tables are semantic `<table>` elements with a header row. Images, drawings
1373
+ (Notes' fallback image, or its preview), scans (PDF with preview), audio,
1374
+ video, files and link cards (title, domain and preview thumbnail) appear in
1375
+ body order. Attachments with no body marker are appended in creation order.
1376
+ An attachment with no usable source renders a visible
1377
+ `[Image unavailable: name]` marker. The document contains no script, no
1378
+ `file:` URL and no Notes library path.
1379
+
1380
+ A folder export is one presentation document with notes separated by
1381
+ `<hr class="note-separator">`. It is not a backup or restore format.
1382
+
1383
+ **Requires:** Full Disk Access. Password-protected notes are skipped in a
1384
+ folder export and refused for a single note.
1385
+
1386
+ | Parameter | Type | Required | Description |
1387
+ |-----------|------|----------|-------------|
1388
+ | `id` | string | One of `id`/`folder` | Exact note ID |
1389
+ | `folder` | string | One of `id`/`folder` | Folder path, as for `list-notes` |
1390
+ | `account` | string | No | Account holding `folder` |
1391
+ | `limit` | number | No | Maximum notes read from the folder (default 100, max 1000) |
1392
+ | `outputPath` | string | Yes | Absolute HTML file to create. Create-only: an existing file is refused with `[output_exists]` |
1393
+ | `embedAssets` | boolean | No | Embed assets as data URLs (default `true`). Each asset is capped at 10 MiB and a document at 256 MiB of embedded assets; a larger one renders as unavailable with a hint to use `embedAssets: false` |
1394
+ | `assetsDir` | string | No | With `embedAssets: false`, the sidecar directory (default `<output stem>.assets` beside the file). Existing files are never replaced; a taken name gets `-2`, `-3`, ... |
1395
+
1396
+ The HTML is always written to a file, because an embedded document is too
1397
+ large for an MCP message. Paths follow the `save-attachment` rules and may not
1398
+ point inside the Notes library container. Sidecar URLs are relative to the
1399
+ HTML file, so the file and its `.assets` directory can be moved together.
1400
+
1401
+ **Returns:** `format`, `count`, `bytes`, `output`, either `embedded` (assets
1402
+ embedded) or `assets` (`dir`, `files`), `stats`, and `skipped`. Nothing
1403
+ already written is deleted if a later step fails.
1404
+
1405
+ ---
1406
+
1363
1407
  #### `get-checklist-state`
1364
1408
 
1365
1409
  Reads checklist done/undone state for a note. This bypasses the AppleScript limitation where `body of note` strips checklist state, by reading directly from the NoteStore SQLite database.
@@ -1978,7 +2022,7 @@ MCP stores no secrets, but as a general rule keep only non-secret config here.
1978
2022
 
1979
2023
  ## Full Disk Access
1980
2024
 
1981
- 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`, `get-audio-transcripts`, `list-special-notes`, `list-native-tags`, `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`.
2025
+ 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`, `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`.
1982
2026
 
1983
2027
  > 📘 **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.
1984
2028
 
package/build/index.js CHANGED
@@ -24465,14 +24465,14 @@ var require_turndown_cjs = __commonJS({
24465
24465
  } else if (node.nodeType === 1) {
24466
24466
  replacement = replacementForNode.call(self, node);
24467
24467
  }
24468
- return join21(output, replacement);
24468
+ return join22(output, replacement);
24469
24469
  }, "");
24470
24470
  }
24471
24471
  function postProcess(output) {
24472
24472
  var self = this;
24473
24473
  this.rules.forEach(function(rule) {
24474
24474
  if (typeof rule.append === "function") {
24475
- output = join21(output, rule.append(self.options));
24475
+ output = join22(output, rule.append(self.options));
24476
24476
  }
24477
24477
  });
24478
24478
  return output.replace(/^[\t\r\n]+/, "").replace(/[\t\r\n\s]+$/, "");
@@ -24484,7 +24484,7 @@ var require_turndown_cjs = __commonJS({
24484
24484
  if (whitespace.leading || whitespace.trailing) content = content.trim();
24485
24485
  return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
24486
24486
  }
24487
- function join21(output, replacement) {
24487
+ function join22(output, replacement) {
24488
24488
  var s1 = trimTrailingNewlines(output);
24489
24489
  var s2 = trimLeadingNewlines(replacement);
24490
24490
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
@@ -48274,7 +48274,7 @@ function insertLink(request, deps) {
48274
48274
 
48275
48275
  // src/services/notesExport.ts
48276
48276
  import { mkdirSync as mkdirSync5 } from "node:fs";
48277
- import { dirname as dirname4 } from "node:path";
48277
+ import { basename as basename4, dirname as dirname4, extname as extname4, join as join20 } from "node:path";
48278
48278
 
48279
48279
  // src/utils/exportAssets.ts
48280
48280
  import {
@@ -48293,6 +48293,7 @@ import { homedir as homedir15 } from "node:os";
48293
48293
  import { basename as basename3, extname as extname3, join as join18, relative as relative3, resolve as resolve3, sep as sep3 } from "node:path";
48294
48294
  var NOTES_CONTAINER = join18(homedir15(), "Library/Group Containers/group.com.apple.notes");
48295
48295
  var MAX_EMBED_BYTES = 10 * 1024 * 1024;
48296
+ var MAX_EMBED_TOTAL_BYTES = 256 * 1024 * 1024;
48296
48297
  var MAX_DIRECTORY_ENTRIES = 2e4;
48297
48298
  var MAX_BUNDLE_ENTRIES2 = 64;
48298
48299
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".heic", ".gif", ".tiff", ".webp"]);
@@ -48652,6 +48653,49 @@ var SidecarWriter = class {
48652
48653
  }
48653
48654
  }
48654
48655
  };
48656
+ var DataUrlWriter = class {
48657
+ constructor(maxBytes = MAX_EMBED_BYTES, totalBytes = MAX_EMBED_TOTAL_BYTES) {
48658
+ this.maxBytes = maxBytes;
48659
+ this.totalBytes = totalBytes;
48660
+ }
48661
+ maxBytes;
48662
+ totalBytes;
48663
+ placed = /* @__PURE__ */ new Map();
48664
+ embeddedBytes = 0;
48665
+ count = 0;
48666
+ place(asset) {
48667
+ const done = this.placed.get(asset.path);
48668
+ if (done) return done;
48669
+ let source;
48670
+ try {
48671
+ source = openSource(asset.path);
48672
+ } catch {
48673
+ return { error: "unreadable" };
48674
+ }
48675
+ try {
48676
+ if (source.size > this.maxBytes || this.embeddedBytes + source.size > this.totalBytes)
48677
+ return { error: "too-large" };
48678
+ const data = Buffer.alloc(source.size);
48679
+ let read = 0;
48680
+ while (read < source.size) {
48681
+ const n = readSync3(source.fd, data, read, source.size - read, read);
48682
+ if (n <= 0) break;
48683
+ read += n;
48684
+ }
48685
+ const mime = sniffMime(data.subarray(0, 16), asset.name);
48686
+ const result = {
48687
+ url: `data:${mime};base64,${data.subarray(0, read).toString("base64")}`,
48688
+ mime
48689
+ };
48690
+ this.count++;
48691
+ this.embeddedBytes += read;
48692
+ this.placed.set(asset.path, result);
48693
+ return result;
48694
+ } finally {
48695
+ closeSync4(source.fd);
48696
+ }
48697
+ }
48698
+ };
48655
48699
 
48656
48700
  // src/utils/exportRender.ts
48657
48701
  var emptyStats = () => ({
@@ -48818,6 +48862,289 @@ function unreferencedAttachments(note) {
48818
48862
  );
48819
48863
  }
48820
48864
 
48865
+ // src/utils/htmlExport.ts
48866
+ function escapeHtml(text2) {
48867
+ return text2.replace(
48868
+ /[&<>"']/g,
48869
+ (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]
48870
+ );
48871
+ }
48872
+ var HIGHLIGHT_CLASSES = /* @__PURE__ */ new Set(["purple", "pink", "orange", "mint", "blue"]);
48873
+ var ALIGN = /* @__PURE__ */ new Set(["center", "right", "justify"]);
48874
+ var REASONS = {
48875
+ "too-large": " (too large to embed; export with embedAssets false)",
48876
+ unreadable: " (unreadable)",
48877
+ undecodable: " (could not be decoded)"
48878
+ };
48879
+ function wrapText(text2, fmt) {
48880
+ let out = escapeHtml(text2).replace(/\n/g, "<br>");
48881
+ if (fmt.subscript) out = `<sub>${out}</sub>`;
48882
+ if (fmt.superscript) out = `<sup>${out}</sup>`;
48883
+ if (fmt.highlight)
48884
+ out = `<mark class="hl-${HIGHLIGHT_CLASSES.has(fmt.highlight) ? fmt.highlight : "other"}">${out}</mark>`;
48885
+ if (fmt.strikethrough) out = `<s>${out}</s>`;
48886
+ if (fmt.underline) out = `<u>${out}</u>`;
48887
+ if (fmt.italic) out = `<em>${out}</em>`;
48888
+ if (fmt.bold) out = `<strong>${out}</strong>`;
48889
+ if (fmt.color && /^#[0-9A-F]{6}(?:[0-9A-F]{2})?$/i.test(fmt.color))
48890
+ out = `<span style="color:${fmt.color}">${out}</span>`;
48891
+ return out;
48892
+ }
48893
+ var bracket = (label, name) => escapeHtml(`[${name ? `${label}: ${name}` : label}]`);
48894
+ function domainOf(url) {
48895
+ try {
48896
+ return new URL(url).hostname;
48897
+ } catch {
48898
+ return url;
48899
+ }
48900
+ }
48901
+ function isHtmlBlockPlan(plan) {
48902
+ switch (plan.type) {
48903
+ case "table":
48904
+ case "divider":
48905
+ case "gallery":
48906
+ case "card":
48907
+ return true;
48908
+ case "asset":
48909
+ return plan.display !== "link" || !!plan.previewUrl;
48910
+ default:
48911
+ return false;
48912
+ }
48913
+ }
48914
+ function inlinePlanHtml(plan) {
48915
+ switch (plan.type) {
48916
+ case "inline":
48917
+ return plan.link ? `<a href="${escapeHtml(plan.link)}">${escapeHtml(plan.text)}</a>` : escapeHtml(plan.text);
48918
+ case "placeholder":
48919
+ return `<span class="attachment-placeholder">${bracket(plan.label, plan.name)}</span>`;
48920
+ case "unavailable":
48921
+ return `<span class="attachment-unavailable" role="note">${bracket(
48922
+ `${plan.label} unavailable${REASONS[plan.reason] ?? ""}`,
48923
+ plan.name
48924
+ )}</span>`;
48925
+ case "asset":
48926
+ return `<a class="attachment attachment-file" href="${escapeHtml(plan.url)}">${escapeHtml(
48927
+ plan.name ?? plan.label
48928
+ )}</a>`;
48929
+ default:
48930
+ return blockPlanHtml(plan);
48931
+ }
48932
+ }
48933
+ function blockPlanHtml(plan) {
48934
+ switch (plan.type) {
48935
+ case "table":
48936
+ return tableHtml(plan.rows);
48937
+ case "divider":
48938
+ return "<hr>";
48939
+ case "gallery":
48940
+ return `<div class="attachment-gallery">${plan.items.map((item) => isHtmlBlockPlan(item) ? blockPlanHtml(item) : inlinePlanHtml(item)).join("")}</div>`;
48941
+ case "card": {
48942
+ const image = plan.previewUrl ? `<img src="${escapeHtml(plan.previewUrl)}" alt="" loading="lazy">` : "";
48943
+ const text2 = `<span class="link-card-text"><span class="link-card-title">${escapeHtml(plan.title)}</span><span class="link-card-domain">${escapeHtml(domainOf(plan.displayUrl))}</span></span>`;
48944
+ return plan.url ? `<a class="link-card" href="${escapeHtml(plan.url)}">${image}${text2}</a>` : `<div class="link-card">${image}${text2}</div>`;
48945
+ }
48946
+ case "asset": {
48947
+ const name = escapeHtml(plan.name ?? plan.label);
48948
+ const src = escapeHtml(plan.url);
48949
+ const caption = plan.name ? `<figcaption>${name}</figcaption>` : "";
48950
+ if (plan.display === "image")
48951
+ return `<figure class="attachment attachment-image"><img src="${src}" alt="${name}" loading="lazy"></figure>`;
48952
+ if (plan.display === "audio" || plan.display === "video")
48953
+ return `<figure class="attachment attachment-${plan.display}"><${plan.display} controls preload="metadata" src="${src}"></${plan.display}>${caption}</figure>`;
48954
+ return `<figure class="attachment attachment-document"><a href="${src}"><img src="${escapeHtml(plan.previewUrl)}" alt="${name}" loading="lazy"></a><figcaption><a href="${src}">${name}</a></figcaption></figure>`;
48955
+ }
48956
+ default:
48957
+ return `<p>${inlinePlanHtml(plan)}</p>`;
48958
+ }
48959
+ }
48960
+ function tableHtml(rows) {
48961
+ const width = Math.max(1, ...rows.map((row) => row.length));
48962
+ const cells = (row, tag) => Array.from(
48963
+ { length: width },
48964
+ (_, i) => `<${tag}>${escapeHtml(row[i] ?? "").replace(/\r?\n/g, "<br>")}</${tag}>`
48965
+ ).join("");
48966
+ const [head = [], ...body] = rows;
48967
+ return `<table><thead><tr>${cells(head, "th")}</tr></thead><tbody>${body.map((row) => `<tr>${cells(row, "td")}</tr>`).join("")}</tbody></table>`;
48968
+ }
48969
+ function inlineHtml(pieces) {
48970
+ let out = "";
48971
+ for (let i = 0; i < pieces.length; ) {
48972
+ const piece = pieces[i];
48973
+ if (piece.type === "attachment") {
48974
+ out += inlinePlanHtml(piece.plan);
48975
+ i++;
48976
+ continue;
48977
+ }
48978
+ const link = piece.fmt.link;
48979
+ let inner = "";
48980
+ let j = i;
48981
+ for (; j < pieces.length; j++) {
48982
+ const next = pieces[j];
48983
+ if (next.type !== "text" || next.fmt.link !== link) break;
48984
+ inner += wrapText(next.text, next.fmt);
48985
+ }
48986
+ out += link ? `<a href="${escapeHtml(link)}">${inner}</a>` : inner;
48987
+ i = j;
48988
+ }
48989
+ return out;
48990
+ }
48991
+ function blockParts(pieces) {
48992
+ const parts = [];
48993
+ let segment = [];
48994
+ const flush = () => {
48995
+ const html = inlineHtml(segment).trim();
48996
+ if (html) parts.push({ inline: html });
48997
+ segment = [];
48998
+ };
48999
+ for (const piece of pieces) {
49000
+ if (piece.type === "attachment" && isHtmlBlockPlan(piece.plan)) {
49001
+ flush();
49002
+ parts.push({ block: blockPlanHtml(piece.plan) });
49003
+ } else segment.push(piece);
49004
+ }
49005
+ flush();
49006
+ return parts;
49007
+ }
49008
+ var LIST_TAGS = {
49009
+ bulleted: "ul",
49010
+ dashed: "ul",
49011
+ checklist: "ul",
49012
+ numbered: "ol"
49013
+ };
49014
+ var alignAttr = (block) => ALIGN.has(block.alignment) ? ` style="text-align:${block.alignment}"` : "";
49015
+ function renderNoteHtml(note, ctx) {
49016
+ const plan = (id2) => planAttachment(note.attachments.get(id2), ctx);
49017
+ const titleIndex = titleBlockIndex(note);
49018
+ const out = [];
49019
+ const lists = [];
49020
+ let quote = false;
49021
+ let code;
49022
+ const closeLists = (depth = 0) => {
49023
+ while (lists.length > depth) {
49024
+ const list = lists.pop();
49025
+ out.push(`${list.open ? "</li>" : ""}</${list.tag}>`);
49026
+ }
49027
+ };
49028
+ const flushCode = () => {
49029
+ if (code) out.push(`<pre><code>${escapeHtml(code.join("\n"))}</code></pre>`);
49030
+ code = void 0;
49031
+ };
49032
+ const setQuote = (on) => {
49033
+ if (on === quote) return;
49034
+ flushCode();
49035
+ closeLists();
49036
+ out.push(on ? "<blockquote>" : "</blockquote>");
49037
+ quote = on;
49038
+ };
49039
+ if (titleIndex === -1 && note.title.trim()) out.push(`<h1>${escapeHtml(note.title.trim())}</h1>`);
49040
+ for (const block of note.doc.blocks) {
49041
+ setQuote(block.blockQuote);
49042
+ if (block.style === "monospaced") {
49043
+ closeLists();
49044
+ (code ??= []).push(block.text.replace(//g, ""));
49045
+ continue;
49046
+ }
49047
+ flushCode();
49048
+ const listTag = LIST_TAGS[block.style];
49049
+ if (!listTag) closeLists();
49050
+ if (!block.text.trim()) continue;
49051
+ const parts = blockParts(blockPieces(block, plan));
49052
+ if (!parts.length) continue;
49053
+ if (listTag) {
49054
+ const level = Math.min(block.indent, 20);
49055
+ const cls = block.style === "checklist" || block.style === "dashed" ? block.style : "";
49056
+ closeLists(level + 1);
49057
+ const top = lists[level];
49058
+ if (top && (top.tag !== listTag || top.cls !== cls)) closeLists(level);
49059
+ if (lists[level]?.open) out.push("</li>");
49060
+ while (lists.length < level) {
49061
+ out.push(`<${listTag}><li class="nest">`);
49062
+ lists.push({ tag: listTag, cls: "", open: true });
49063
+ }
49064
+ if (lists.length === level) {
49065
+ out.push(`<${listTag}${cls ? ` class="${cls}"` : ""}>`);
49066
+ lists.push({ tag: listTag, cls, open: false });
49067
+ }
49068
+ lists[level].open = true;
49069
+ const box = block.style === "checklist" ? `<input type="checkbox" disabled${block.checklist?.done ? " checked" : ""}> ` : "";
49070
+ const done = block.style === "checklist" && block.checklist?.done ? ` class="done"` : "";
49071
+ const body = parts.map((part) => "inline" in part ? part.inline : part.block).join("");
49072
+ out.push(`<li${done}${alignAttr(block)}>${box}${body}`);
49073
+ continue;
49074
+ }
49075
+ const tag = block.index === titleIndex || block.style === "title" ? "h1" : block.style === "heading" ? "h2" : block.style === "subheading" ? "h3" : "p";
49076
+ for (const part of parts)
49077
+ out.push(
49078
+ "inline" in part ? `<${tag}${alignAttr(block)}>${part.inline}</${tag}>` : part.block
49079
+ );
49080
+ }
49081
+ flushCode();
49082
+ closeLists();
49083
+ setQuote(false);
49084
+ for (const attachment of unreferencedAttachments(note)) {
49085
+ ctx.stats.unreferenced++;
49086
+ out.push(blockPlanHtml(planAttachment(attachment, ctx)));
49087
+ }
49088
+ return `<article class="note">
49089
+ ${out.filter(Boolean).join("\n")}
49090
+ </article>`;
49091
+ }
49092
+ var CSS = `
49093
+ :root { color-scheme: light dark; --fg: #1d1d1f; --bg: #fff; --muted: #6e6e73; --line: #d2d2d7; --card: #f5f5f7; }
49094
+ @media (prefers-color-scheme: dark) { :root { --fg: #f5f5f7; --bg: #1d1d1f; --muted: #a1a1a6; --line: #424245; --card: #2c2c2e; } }
49095
+ body { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; }
49096
+ main { max-width: 46rem; margin: 0 auto; padding: 2rem 1rem; }
49097
+ h1, h2, h3 { line-height: 1.25; }
49098
+ blockquote { margin: 1rem 0; padding-left: 1rem; border-left: 3px solid var(--line); color: var(--muted); }
49099
+ pre { background: var(--card); padding: .75rem 1rem; overflow-x: auto; border-radius: 6px; }
49100
+ code { font: 14px/1.4 ui-monospace, Menlo, monospace; }
49101
+ ul.dashed { list-style-type: "\u2013 "; }
49102
+ ul.checklist { list-style: none; padding-left: 1.25rem; }
49103
+ ul.checklist li.done { color: var(--muted); text-decoration: line-through; }
49104
+ li.nest { list-style: none; }
49105
+ table { border-collapse: collapse; margin: 1rem 0; }
49106
+ th, td { border: 1px solid var(--line); padding: .35rem .6rem; text-align: left; vertical-align: top; }
49107
+ th { background: var(--card); }
49108
+ figure { margin: 1rem 0; }
49109
+ figure img, figure video { max-width: 100%; height: auto; border-radius: 6px; }
49110
+ figcaption { color: var(--muted); font-size: .875rem; }
49111
+ .attachment-gallery { display: flex; flex-wrap: wrap; gap: .5rem; }
49112
+ .attachment-gallery figure { margin: 0; flex: 1 1 12rem; }
49113
+ .link-card { display: flex; gap: .75rem; align-items: center; margin: 1rem 0; padding: .5rem; border: 1px solid var(--line); border-radius: 8px; background: var(--card); color: inherit; text-decoration: none; }
49114
+ .link-card img { width: 4rem; height: 4rem; object-fit: cover; border-radius: 4px; }
49115
+ .link-card-text { display: flex; flex-direction: column; min-width: 0; }
49116
+ .link-card-title { font-weight: 600; }
49117
+ .link-card-domain { color: var(--muted); font-size: .875rem; }
49118
+ .attachment-unavailable, .attachment-placeholder { display: inline-block; padding: 0 .35rem; border: 1px dashed var(--muted); border-radius: 4px; color: var(--muted); font-size: .875rem; }
49119
+ mark { border-radius: 2px; padding: 0 .1em; }
49120
+ mark.hl-purple { background: #e5d4ff; } mark.hl-pink { background: #ffd1e3; } mark.hl-orange { background: #ffe0b8; }
49121
+ mark.hl-mint { background: #c8f2de; } mark.hl-blue { background: #cfe3ff; } mark.hl-other { background: #fff3a8; }
49122
+ hr.note-separator { margin: 3rem 0; border: 0; border-top: 2px solid var(--line); }
49123
+ `.trim();
49124
+ function renderNotesHtml(notes, ctx, { title }) {
49125
+ const body = notes.map((note) => renderNoteHtml(note, ctx)).join('\n<hr class="note-separator">\n');
49126
+ return [
49127
+ "<!DOCTYPE html>",
49128
+ "<html>",
49129
+ "<head>",
49130
+ '<meta charset="utf-8">',
49131
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
49132
+ '<meta name="generator" content="apple-notes-mcp">',
49133
+ `<title>${escapeHtml(title)}</title>`,
49134
+ `<style>
49135
+ ${CSS}
49136
+ </style>`,
49137
+ "</head>",
49138
+ "<body>",
49139
+ "<main>",
49140
+ body,
49141
+ "</main>",
49142
+ "</body>",
49143
+ "</html>",
49144
+ ""
49145
+ ].join("\n");
49146
+ }
49147
+
48821
49148
  // src/utils/markdownExport.ts
48822
49149
  var NOTE_SEPARATOR = "\n\n---\n\n";
48823
49150
  function escapeMarkdown(text2) {
@@ -48866,7 +49193,7 @@ function layered(pieces, depth = 0) {
48866
49193
  }
48867
49194
  return out;
48868
49195
  }
48869
- var bracket = (label, name) => `\\[${escapeMarkdown(name ? `${label}: ${name}` : label)}\\]`;
49196
+ var bracket2 = (label, name) => `\\[${escapeMarkdown(name ? `${label}: ${name}` : label)}\\]`;
48870
49197
  function planMarkdown(plan) {
48871
49198
  switch (plan.type) {
48872
49199
  case "inline":
@@ -48876,9 +49203,9 @@ function planMarkdown(plan) {
48876
49203
  case "table":
48877
49204
  return tableMarkdown(plan.rows);
48878
49205
  case "placeholder":
48879
- return bracket(plan.label, plan.name);
49206
+ return bracket2(plan.label, plan.name);
48880
49207
  case "unavailable":
48881
- return bracket(`${plan.label} unavailable`, plan.name);
49208
+ return bracket2(`${plan.label} unavailable`, plan.name);
48882
49209
  case "asset": {
48883
49210
  const text2 = escapeMarkdown(plan.name ?? plan.label);
48884
49211
  if (plan.display === "image") return `![${text2}](${linkDestination(plan.url)})`;
@@ -49269,6 +49596,24 @@ function loadNotes(ids, single, read) {
49269
49596
  }
49270
49597
  return { notes, skipped };
49271
49598
  }
49599
+ function openOutput(output) {
49600
+ mkdirSync5(dirname4(output), { recursive: true });
49601
+ try {
49602
+ return openCreateOnly(output);
49603
+ } catch (error2) {
49604
+ if (error2 instanceof OutputExistsError)
49605
+ throw new NotesExportError("output_exists", error2.message);
49606
+ throw error2;
49607
+ }
49608
+ }
49609
+ function renderInto(fd, render) {
49610
+ try {
49611
+ return render();
49612
+ } catch (error2) {
49613
+ if (fd !== void 0) writeAllAndClose(fd, "");
49614
+ throw error2;
49615
+ }
49616
+ }
49272
49617
  function exportNotesMarkdown(request, deps) {
49273
49618
  const output = request.outputPath ? validPath(request.outputPath, "outputPath") : void 0;
49274
49619
  const assetsDir = request.assetsDir ? validPath(request.assetsDir, "assetsDir") : void 0;
@@ -49276,29 +49621,16 @@ function exportNotesMarkdown(request, deps) {
49276
49621
  throw new NotesExportError("invalid-path", "outputPath and assetsDir must differ.");
49277
49622
  const ids = selectNotes(request, deps);
49278
49623
  const { notes, skipped } = loadNotes(ids, !!request.id, deps.readNote);
49279
- let fd;
49280
- if (output) {
49281
- mkdirSync5(dirname4(output), { recursive: true });
49282
- try {
49283
- fd = openCreateOnly(output);
49284
- } catch (error2) {
49285
- if (error2 instanceof OutputExistsError)
49286
- throw new NotesExportError("output_exists", error2.message);
49287
- throw error2;
49288
- }
49289
- }
49624
+ const fd = output ? openOutput(output) : void 0;
49290
49625
  const writer = assetsDir ? new SidecarWriter(assetsDir, output ? dirname4(output) : void 0) : void 0;
49291
49626
  const ctx = {
49292
49627
  stats: emptyStats(),
49293
49628
  ...writer ? { writer, locator: deps.locator ?? new AssetLocator() } : {}
49294
49629
  };
49295
- let markdown;
49296
- try {
49297
- markdown = renderNotesMarkdown(notes, ctx, { wrap: request.wrap ?? 0 });
49298
- } catch (error2) {
49299
- if (fd !== void 0) writeAllAndClose(fd, "");
49300
- throw error2;
49301
- }
49630
+ const markdown = renderInto(
49631
+ fd,
49632
+ () => renderNotesMarkdown(notes, ctx, { wrap: request.wrap ?? 0 })
49633
+ );
49302
49634
  const bytes = Buffer.byteLength(markdown);
49303
49635
  const receipt = {
49304
49636
  format: "markdown",
@@ -49319,6 +49651,47 @@ function exportNotesMarkdown(request, deps) {
49319
49651
  );
49320
49652
  return { ...receipt, markdown };
49321
49653
  }
49654
+ function defaultSidecarDir(output) {
49655
+ return join20(dirname4(output), `${basename4(output, extname4(output))}.assets`);
49656
+ }
49657
+ function exportNotesHtml(request, deps) {
49658
+ if (!request.outputPath)
49659
+ throw new NotesExportError(
49660
+ "invalid-request",
49661
+ "outputPath is required for HTML export; the document is written to a file."
49662
+ );
49663
+ const embed = request.embedAssets ?? !request.assetsDir;
49664
+ if (embed && request.assetsDir)
49665
+ throw new NotesExportError(
49666
+ "invalid-request",
49667
+ "assetsDir applies only with embedAssets false (sidecar assets)."
49668
+ );
49669
+ const output = validPath(request.outputPath, "outputPath");
49670
+ const assetsDir = embed ? void 0 : validPath(request.assetsDir ?? defaultSidecarDir(output), "assetsDir");
49671
+ if (assetsDir === output)
49672
+ throw new NotesExportError("invalid-path", "outputPath and assetsDir must differ.");
49673
+ const ids = selectNotes(request, deps);
49674
+ const { notes, skipped } = loadNotes(ids, !!request.id, deps.readNote);
49675
+ const fd = openOutput(output);
49676
+ const writer = assetsDir ? new SidecarWriter(assetsDir, dirname4(output)) : new DataUrlWriter();
49677
+ const ctx = {
49678
+ stats: emptyStats(),
49679
+ writer,
49680
+ locator: deps.locator ?? new AssetLocator()
49681
+ };
49682
+ const title = request.id ? notes[0]?.title || "Note" : request.folder;
49683
+ const html = renderInto(fd, () => renderNotesHtml(notes, ctx, { title }));
49684
+ const bytes = writeAllAndClose(fd, html);
49685
+ return {
49686
+ format: "html",
49687
+ count: notes.length,
49688
+ bytes,
49689
+ output,
49690
+ stats: ctx.stats,
49691
+ skipped,
49692
+ ...assetsDir ? { assets: { dir: assetsDir, files: writer.count } } : { embedded: writer.count }
49693
+ };
49694
+ }
49322
49695
 
49323
49696
  // src/tools/directOperations.ts
49324
49697
  import { createHash as createHash2 } from "node:crypto";
@@ -49333,7 +49706,7 @@ import {
49333
49706
  writeFileSync as writeFileSync3
49334
49707
  } from "node:fs";
49335
49708
  import { tmpdir as tmpdir4 } from "node:os";
49336
- import { basename as basename4, extname as extname4, isAbsolute as isAbsolute3, join as join20 } from "node:path";
49709
+ import { basename as basename5, extname as extname5, isAbsolute as isAbsolute3, join as join21 } from "node:path";
49337
49710
  var noteId = exactIdInput(
49338
49711
  "ICNote",
49339
49712
  /^x-coredata:\/\/[0-9a-f-]+\/ICNote\/p\d+$/i,
@@ -49501,7 +49874,7 @@ function registerDirectOperations(server2, manager) {
49501
49874
  );
49502
49875
  }
49503
49876
  function attachmentName(path8, filename) {
49504
- const source = basename4(path8);
49877
+ const source = basename5(path8);
49505
49878
  if (filename === void 0) return source;
49506
49879
  if (Buffer.byteLength(filename, "utf8") > 255)
49507
49880
  throw new Error("filename must be at most 255 bytes");
@@ -49509,9 +49882,9 @@ function attachmentName(path8, filename) {
49509
49882
  throw new Error(
49510
49883
  "filename must be one path component with no slash, colon, backslash, control character, leading dot, or surrounding spaces"
49511
49884
  );
49512
- if (extname4(filename).toLowerCase() !== extname4(source).toLowerCase())
49885
+ if (extname5(filename).toLowerCase() !== extname5(source).toLowerCase())
49513
49886
  throw new Error(
49514
- `filename must keep the source file's extension (${extname4(source) || "none"})`
49887
+ `filename must keep the source file's extension (${extname5(source) || "none"})`
49515
49888
  );
49516
49889
  return filename;
49517
49890
  }
@@ -49522,8 +49895,8 @@ function attachFile(manager, args) {
49522
49895
  if (before.hash !== expectedContentHash) throw new Error("Note revision changed");
49523
49896
  const bytes = localAttachment(path8);
49524
49897
  const beforeAttachments = manager.listAttachmentsById(id2);
49525
- const directory = mkdtempSync4(join20(tmpdir4(), "notes-attachment-add-"));
49526
- const temporaryFile = join20(directory, name);
49898
+ const directory = mkdtempSync4(join21(tmpdir4(), "notes-attachment-add-"));
49899
+ const temporaryFile = join21(directory, name);
49527
49900
  try {
49528
49901
  writeFileSync3(temporaryFile, bytes, { mode: 384 });
49529
49902
  if (readSnapshot(manager, id2).hash !== before.hash) throw new Error("Note revision changed");
@@ -52584,6 +52957,57 @@ registerTool(
52584
52957
  return successResponse(receipt.markdown ?? "", { ...receipt });
52585
52958
  }, "Error exporting Markdown")
52586
52959
  );
52960
+ registerTool(
52961
+ "export-notes-html",
52962
+ {
52963
+ description: "Use when: exporting one note (by exact id) or a folder's notes as one standalone HTML file rendered from the decoded note body, with semantic tables and images, drawings, scans, audio, files and link cards in body order.\nReturns: a receipt {format, count, bytes, output} plus embedded or sidecar asset counts, attachment counts and skipped notes. The HTML itself is never returned inline.\nDo not use when: you want Markdown (export-notes-markdown) or a restorable backup (export-notes-json). A folder document is a presentation format, not something to import back.\nSafety: read-only against Notes; requires Full Disk Access. outputPath is required and create-only ([output_exists] if it exists). Assets are embedded as data URLs (each up to 10 MiB) unless embedAssets is false, which copies them to a sidecar directory (assetsDir, default <output stem>.assets) with relative URLs and never replaces existing files. No file: URLs or Notes library paths are written; missing assets show a visible unavailable marker.",
52964
+ inputSchema: {
52965
+ id: noteIdInput.optional(),
52966
+ folder: external_exports.string().min(1).max(MAX.FOLDER).optional().describe("Folder path to export instead of one note (nested paths use '/')"),
52967
+ account: external_exports.string().max(MAX.ACCOUNT).optional().describe("Account holding the folder (defaults to Notes.app's default account)"),
52968
+ limit: external_exports.number().int().min(1).max(MAX_FOLDER_EXPORT_LIMIT).optional().describe(
52969
+ `Maximum notes read from the folder (default 100, max ${MAX_FOLDER_EXPORT_LIMIT})`
52970
+ ),
52971
+ outputPath: external_exports.string().min(1).max(MAX.SAVE_PATH).describe(
52972
+ "HTML file to create (absolute; under home, a temp dir, or /Volumes; never inside the Notes library)"
52973
+ ),
52974
+ embedAssets: external_exports.boolean().optional().describe("Embed assets as data URLs (default true). False writes a sidecar directory"),
52975
+ assetsDir: exportPathInput(
52976
+ "Sidecar directory when embedAssets is false (default <stem>.assets)"
52977
+ )
52978
+ },
52979
+ outputSchema: {
52980
+ format: external_exports.string().optional(),
52981
+ count: external_exports.number().optional(),
52982
+ bytes: external_exports.number().optional(),
52983
+ output: external_exports.string().optional(),
52984
+ assets: external_exports.object({ dir: external_exports.string(), files: external_exports.number() }).optional(),
52985
+ embedded: external_exports.number().optional(),
52986
+ stats: exportStatsSchema.optional(),
52987
+ skipped: external_exports.array(external_exports.object({ id: external_exports.string(), code: external_exports.string() })).optional()
52988
+ },
52989
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
52990
+ },
52991
+ withErrorHandling((request) => {
52992
+ let receipt;
52993
+ try {
52994
+ receipt = exportNotesHtml(request, {
52995
+ listNoteRefs: (account, folder, since, limit) => notesManager.listNoteRefs(account, folder, since, limit),
52996
+ maxInlineBytes: 0
52997
+ });
52998
+ } catch (error2) {
52999
+ if (!(error2 instanceof NotesExportError || error2 instanceof NoteBlocksError)) throw error2;
53000
+ const hint = error2.code === "no-full-disk-access" ? ` Grant Full Disk Access to the app that launches this server: ${FULL_DISK_ACCESS_GUIDE_URL}` : "";
53001
+ return errorResponse(`Error exporting HTML [${error2.code}]: ${error2.message}${hint}`);
53002
+ }
53003
+ const assets = receipt.assets ? `; copied ${receipt.assets.files} asset file(s) to ${receipt.assets.dir}` : `; embedded ${receipt.embedded ?? 0} asset(s)`;
53004
+ const skipped = receipt.skipped.length ? `; skipped ${receipt.skipped.length}` : "";
53005
+ return successResponse(
53006
+ `Wrote ${receipt.count} note(s) as HTML (${receipt.bytes} bytes) to ${receipt.output}${assets}${skipped}.`,
53007
+ { ...receipt }
53008
+ );
53009
+ }, "Error exporting HTML")
53010
+ );
52587
53011
  registerTool(
52588
53012
  "get-checklist-state",
52589
53013
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.8.37",
3
+ "version": "2.8.38",
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",