apple-notes-mcp 2.9.27 → 2.9.28

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.
package/README.md CHANGED
@@ -1752,11 +1752,12 @@ single note.
1752
1752
  | `assetsDir` | string | No | Absolute directory for attachment copies. Existing files are never replaced; a taken name gets `-2`, `-3`, ... |
1753
1753
  | `wrap` | number | No | Hard-wrap prose at this many columns. Code, tables and headings are never wrapped |
1754
1754
  | `template` | string | No | Render through a template: `standard-markdown`, `obsidian`, or a saved template's name. Exclusive with `templateFile` |
1755
- | `templateFile` | string | No | Render through the JSON template in this `.json` file (same path rules; at most 256 KiB) |
1755
+ | `templateFile` | string | No | Render through the JSON template in this `.json` file (at most 256 KiB), read under the same rules as `create-note`'s [`contentPath`](#create-note): hidden paths (such as `~/.docker` or a project `.env`) and `~/Library` other than iCloud Drive and `~/Library/CloudStorage` are refused, checked again after resolving symbolic links and letter case, unless the server sets `APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1`. Errors name the file, never its contents |
1756
1756
 
1757
1757
  Both paths follow the `save-attachment` rules (absolute, under the home
1758
1758
  directory, a temp directory, or `/Volumes`, no symlink escapes) and may not
1759
- point inside the Notes library container. With `outputPath`, asset links are
1759
+ point inside the Notes library container. `templateFile` is a read, so it
1760
+ also follows `contentPath`'s private-location rule. With `outputPath`, asset links are
1760
1761
  relative to the document's directory; without it they are absolute paths.
1761
1762
 
1762
1763
  **Returns:** without `outputPath`, the Markdown itself (refused with
@@ -1863,7 +1864,7 @@ or `templateFile`.
1863
1864
  |-----------|------|----------|-------------|
1864
1865
  | `name` | string | One of three | A saved or built-in template |
1865
1866
  | `template` | object or string | One of three | The template as a JSON object or JSON text |
1866
- | `templateFile` | string | One of three | Absolute path of a JSON template file (home, a temp directory, or `/Volumes`; at most 256 KiB) |
1867
+ | `templateFile` | string | One of three | Absolute path of a JSON template file (home, a temp directory, or `/Volumes`; at most 256 KiB), read under the same rules as `create-note`'s [`contentPath`](#create-note): hidden paths (such as `~/.docker` or a project `.env`) and `~/Library` other than iCloud Drive and `~/Library/CloudStorage` are refused, checked again after resolving symbolic links and letter case, unless the server sets `APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1` |
1867
1868
 
1868
1869
  **Returns:** `valid`, and `errors` as `{path, message}` pairs such as
1869
1870
  `$.rules["inline.bold"].after: is required when mode is "wrap"`. An invalid
@@ -1880,7 +1881,7 @@ can use it by `template` name.
1880
1881
  |-----------|------|----------|-------------|
1881
1882
  | `name` | string | Yes | Lowercase `a-z`, `0-9`, `-` and `_`, 1-64 characters, starting with a letter or digit. Built-in names are reserved |
1882
1883
  | `template` | object or string | One of `template`/`templateFile` | The template |
1883
- | `templateFile` | string | One of `template`/`templateFile` | A JSON template file |
1884
+ | `templateFile` | string | One of `template`/`templateFile` | A JSON template file, under the same rules as `validate-markdown-template`'s |
1884
1885
  | `force` | boolean | No | Replace an existing template of this name (default `false`) |
1885
1886
 
1886
1887
  Saving is create-only: an existing name is refused with `[template-exists]`
@@ -2264,7 +2265,7 @@ Analyzes one local SVG file and reports whether it can be represented as editabl
2264
2265
 
2265
2266
  | Parameter | Type | Required | Description |
2266
2267
  |-----------|------|----------|-------------|
2267
- | `path` | string | Yes | Absolute path of one `.svg` file: a regular file, not a symbolic link, at most 1 MiB of UTF-8, in home, temp, or `/Volumes` |
2268
+ | `path` | string | Yes | Absolute path of one `.svg` file: a regular file, not a symbolic link, at most 1 MiB of UTF-8, in home, temp, or `/Volumes`, read under the same rules as `create-note`'s [`contentPath`](#create-note): hidden paths (such as `~/.docker` or a project `.env`) and `~/Library` other than iCloud Drive and `~/Library/CloudStorage` are refused, checked again after resolving symbolic links and letter case, unless the server sets `APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1`. A file whose first element is not `<svg>` is refused without quoting any of it |
2268
2269
  | `includeDrawing` | boolean | No | Also return the normalized drawing. Off by default because it can be large |
2269
2270
 
2270
2271
  **Returns:** `classification`, `importable`, `defaultWriteAllowed`, `requiredLosses`, `issues` (each with a `code`, the `loss` it causes or `null`, an element `location` such as `svg/g[2]/path[1]#id`, and a message), `viewport`, `counts` (elements, references, path segments, output strokes and points, and the `geometryWork`, `dashWork`, and `scanWork` budgets as `{used, max}`), `source` (`sha256`, `bytes`), `drawingBytes`, and `analysisDigest`.
@@ -2780,7 +2781,7 @@ All configuration is optional — the server works out of the box. Override beha
2780
2781
  | `APPLE_NOTES_MCP_BACKGROUND_SHORTCUT` | `Apple Notes MCP - Background Operations v5` | Name or UUID of the installed Background Operations bridge to run. `setup` still installs and checks the default name. |
2781
2782
  | `APPLE_NOTES_MCP_MARKDOWN_SHORTCUT` | `Apple Notes MCP - Create Markdown Note` | Name or UUID of the installed Create Markdown Note bridge to run. `setup` still installs and checks the default name. |
2782
2783
  | `APPLE_NOTES_MCP_PASTEBOARD_NAME` | unset | Testing only: makes [`add-attachment-from-pasteboard`](#add-attachment-from-pasteboard) read a private named pasteboard instead of the general clipboard. |
2783
- | `APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS` | unset | Set to `1` to let `create-note` read a [`contentPath`](#create-note), and [`add-attachment`](#add-attachment) and [`create-note-with-attachment`](#create-note-with-attachment) attach a `path`, inside a hidden directory or file (such as `~/.config`) or `~/Library` outside iCloud Drive and `~/Library/CloudStorage`. Off by default because those places hold keys, tokens and app data. |
2784
+ | `APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS` | unset | Set to `1` to let `create-note` read a [`contentPath`](#create-note), [`add-attachment`](#add-attachment) and [`create-note-with-attachment`](#create-note-with-attachment) attach a `path`, [`analyze-svg`](#analyze-svg) read a `path`, and the Markdown template tools read a `templateFile`, inside a hidden directory or file (such as `~/.config`) or `~/Library` outside iCloud Drive and `~/Library/CloudStorage`. Off by default because those places hold keys, tokens and app data. |
2784
2785
  | `DEBUG` / `VERBOSE` | unset | Set either to enable verbose diagnostic logging to stderr. |
2785
2786
 
2786
2787
  ### Configuration file (when the host strips `env`)
package/build/index.js CHANGED
@@ -417,11 +417,11 @@ var require_codegen = __commonJS({
417
417
  const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
418
418
  return `${varKind} ${this.name}${rhs};` + _n;
419
419
  }
420
- optimizeNames(names, constants10) {
420
+ optimizeNames(names, constants9) {
421
421
  if (!names[this.name.str])
422
422
  return;
423
423
  if (this.rhs)
424
- this.rhs = optimizeExpr(this.rhs, names, constants10);
424
+ this.rhs = optimizeExpr(this.rhs, names, constants9);
425
425
  return this;
426
426
  }
427
427
  get names() {
@@ -438,10 +438,10 @@ var require_codegen = __commonJS({
438
438
  render({ _n }) {
439
439
  return `${this.lhs} = ${this.rhs};` + _n;
440
440
  }
441
- optimizeNames(names, constants10) {
441
+ optimizeNames(names, constants9) {
442
442
  if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
443
443
  return;
444
- this.rhs = optimizeExpr(this.rhs, names, constants10);
444
+ this.rhs = optimizeExpr(this.rhs, names, constants9);
445
445
  return this;
446
446
  }
447
447
  get names() {
@@ -502,8 +502,8 @@ var require_codegen = __commonJS({
502
502
  optimizeNodes() {
503
503
  return `${this.code}` ? this : void 0;
504
504
  }
505
- optimizeNames(names, constants10) {
506
- this.code = optimizeExpr(this.code, names, constants10);
505
+ optimizeNames(names, constants9) {
506
+ this.code = optimizeExpr(this.code, names, constants9);
507
507
  return this;
508
508
  }
509
509
  get names() {
@@ -532,12 +532,12 @@ var require_codegen = __commonJS({
532
532
  }
533
533
  return nodes.length > 0 ? this : void 0;
534
534
  }
535
- optimizeNames(names, constants10) {
535
+ optimizeNames(names, constants9) {
536
536
  const { nodes } = this;
537
537
  let i = nodes.length;
538
538
  while (i--) {
539
539
  const n = nodes[i];
540
- if (n.optimizeNames(names, constants10))
540
+ if (n.optimizeNames(names, constants9))
541
541
  continue;
542
542
  subtractNames(names, n.names);
543
543
  nodes.splice(i, 1);
@@ -590,12 +590,12 @@ var require_codegen = __commonJS({
590
590
  return void 0;
591
591
  return this;
592
592
  }
593
- optimizeNames(names, constants10) {
593
+ optimizeNames(names, constants9) {
594
594
  var _a;
595
- this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants10);
596
- if (!(super.optimizeNames(names, constants10) || this.else))
595
+ this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants9);
596
+ if (!(super.optimizeNames(names, constants9) || this.else))
597
597
  return;
598
- this.condition = optimizeExpr(this.condition, names, constants10);
598
+ this.condition = optimizeExpr(this.condition, names, constants9);
599
599
  return this;
600
600
  }
601
601
  get names() {
@@ -618,10 +618,10 @@ var require_codegen = __commonJS({
618
618
  render(opts) {
619
619
  return `for(${this.iteration})` + super.render(opts);
620
620
  }
621
- optimizeNames(names, constants10) {
622
- if (!super.optimizeNames(names, constants10))
621
+ optimizeNames(names, constants9) {
622
+ if (!super.optimizeNames(names, constants9))
623
623
  return;
624
- this.iteration = optimizeExpr(this.iteration, names, constants10);
624
+ this.iteration = optimizeExpr(this.iteration, names, constants9);
625
625
  return this;
626
626
  }
627
627
  get names() {
@@ -657,10 +657,10 @@ var require_codegen = __commonJS({
657
657
  render(opts) {
658
658
  return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
659
659
  }
660
- optimizeNames(names, constants10) {
661
- if (!super.optimizeNames(names, constants10))
660
+ optimizeNames(names, constants9) {
661
+ if (!super.optimizeNames(names, constants9))
662
662
  return;
663
- this.iterable = optimizeExpr(this.iterable, names, constants10);
663
+ this.iterable = optimizeExpr(this.iterable, names, constants9);
664
664
  return this;
665
665
  }
666
666
  get names() {
@@ -702,11 +702,11 @@ var require_codegen = __commonJS({
702
702
  (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
703
703
  return this;
704
704
  }
705
- optimizeNames(names, constants10) {
705
+ optimizeNames(names, constants9) {
706
706
  var _a, _b;
707
- super.optimizeNames(names, constants10);
708
- (_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants10);
709
- (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants10);
707
+ super.optimizeNames(names, constants9);
708
+ (_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants9);
709
+ (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants9);
710
710
  return this;
711
711
  }
712
712
  get names() {
@@ -1007,7 +1007,7 @@ var require_codegen = __commonJS({
1007
1007
  function addExprNames(names, from) {
1008
1008
  return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
1009
1009
  }
1010
- function optimizeExpr(expr, names, constants10) {
1010
+ function optimizeExpr(expr, names, constants9) {
1011
1011
  if (expr instanceof code_1.Name)
1012
1012
  return replaceName(expr);
1013
1013
  if (!canOptimize(expr))
@@ -1022,14 +1022,14 @@ var require_codegen = __commonJS({
1022
1022
  return items;
1023
1023
  }, []));
1024
1024
  function replaceName(n) {
1025
- const c = constants10[n.str];
1025
+ const c = constants9[n.str];
1026
1026
  if (c === void 0 || names[n.str] !== 1)
1027
1027
  return n;
1028
1028
  delete names[n.str];
1029
1029
  return c;
1030
1030
  }
1031
1031
  function canOptimize(e) {
1032
- return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants10[c.str] !== void 0);
1032
+ return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants9[c.str] !== void 0);
1033
1033
  }
1034
1034
  }
1035
1035
  function subtractNames(names, from) {
@@ -7422,7 +7422,7 @@ var require_DOMException = __commonJS({
7422
7422
  "INVALID_NODE_TYPE_ERR (24): the supplied node is invalid or has an invalid ancestor for this operation",
7423
7423
  "DATA_CLONE_ERR (25): the object can not be cloned."
7424
7424
  ];
7425
- var constants10 = {
7425
+ var constants9 = {
7426
7426
  INDEX_SIZE_ERR,
7427
7427
  DOMSTRING_SIZE_ERR: 2,
7428
7428
  // historical
@@ -7461,8 +7461,8 @@ var require_DOMException = __commonJS({
7461
7461
  this.name = names[code];
7462
7462
  }
7463
7463
  DOMException.prototype.__proto__ = Error.prototype;
7464
- for (c in constants10) {
7465
- v = { value: constants10[c] };
7464
+ for (c in constants9) {
7465
+ v = { value: constants9[c] };
7466
7466
  Object.defineProperty(DOMException, c, v);
7467
7467
  Object.defineProperty(DOMException.prototype, c, v);
7468
7468
  }
@@ -39857,6 +39857,10 @@ var CodedError = class extends Error {
39857
39857
  };
39858
39858
  var RULES = [
39859
39859
  { code: "timeout_indeterminate", pattern: /timed out|\bETIMEDOUT\b/i },
39860
+ // A scope guard is checked before the write runs, so nothing was written:
39861
+ // the note is not where it was reviewed. Its advice to read the note before
39862
+ // retrying must not make it look like an unverified write.
39863
+ { code: "revision_conflict", pattern: /\bScope guard failed:/ },
39860
39864
  {
39861
39865
  code: "verification_failed",
39862
39866
  pattern: /\buncertain\b|may have (?:been|succeeded)|\baccepted (?:the|an) \w+, but|\bnot verified\b|Do not retry automatically|before any retry|read (?:the )?(?:exact )?(?:note|ID)\b[^.]*before retr/i
@@ -39885,7 +39889,7 @@ var RULES = [
39885
39889
  },
39886
39890
  {
39887
39891
  code: "validation_error",
39888
- pattern: /\bis required\b|required\b|\bInvalid\b|\bmust\b|\bProvide\b|No (?:note IDs|reviewed notes) provided|response limit|Refusing to write|cannot be resolved|at most \d|between \d+ and \d+|supports the (?:end|default)|Use a distinctive|Use create-table|too large|equal cell counts|Folder path is empty/i
39892
+ pattern: /\bis required\b|required\b|\bInvalid\b|\bmust\b|\bProvide\b|No (?:note IDs|reviewed notes) provided|response limit|Refusing to (?:read|write)|cannot be resolved|at most \d|between \d+ and \d+|supports the (?:end|default)|Use a distinctive|Use create-table|too large|equal cell counts|Folder path is empty/i
39889
39893
  }
39890
39894
  ];
39891
39895
  var NOTHING_WRITTEN = /nothing (?:was )?(?:changed|created|written)|no replacement started|No content was (?:replaced|appended)|Nothing was created/i;
@@ -53141,33 +53145,9 @@ function templateAssetsDir(directory, outputDir, values) {
53141
53145
  return dir;
53142
53146
  }
53143
53147
  function readTemplateFile(path10) {
53144
- const abs = assertExportPath(path10);
53145
- if (extname4(abs).toLowerCase() !== ".json")
53146
- throw new Error(`Template file must have a .json extension: ${abs}`);
53147
- let fd;
53148
- try {
53149
- fd = openRegular(abs);
53150
- } catch (error2) {
53151
- const code = error2.code;
53152
- throw new Error(
53153
- code === "ENOENT" ? `Template file not found: ${abs}` : code === "ELOOP" ? `Refusing to read the symbolic link ${abs}` : `Template file is not a readable regular file: ${abs}`
53154
- );
53155
- }
53156
- try {
53157
- const size = fstatSync5(fd).size;
53158
- if (size > MAX_TEMPLATE_BYTES)
53159
- throw new Error(`Template file is ${size} bytes; the limit is ${MAX_TEMPLATE_BYTES}`);
53160
- const data = Buffer.alloc(size);
53161
- let read = 0;
53162
- while (read < size) {
53163
- const n = readSync4(fd, data, read, size - read, read);
53164
- if (n <= 0) break;
53165
- read += n;
53166
- }
53167
- return data.subarray(0, read).toString("utf8");
53168
- } finally {
53169
- closeSync5(fd);
53170
- }
53148
+ if (extname4(path10).toLowerCase() !== ".json")
53149
+ throw new Error(`Template file must have a .json extension: ${path10}`);
53150
+ return readAllowedFile(path10, MAX_TEMPLATE_BYTES, { label: "Template file" }).toString("utf8");
53171
53151
  }
53172
53152
 
53173
53153
  // src/services/notesExport.ts
@@ -54759,7 +54739,7 @@ function failure(action, error2) {
54759
54739
  var nameInput = external_exports.string().min(1).max(64).describe("Template name: a built-in (standard-markdown, obsidian) or a saved template's name");
54760
54740
  var templateInput = external_exports.union([external_exports.record(external_exports.unknown()), external_exports.string().max(MAX_TEMPLATE_BYTES)]).optional().describe("The template itself, as a JSON object or JSON text (schemaVersion 1)");
54761
54741
  var templateFileInput = external_exports.string().min(1).max(4096).optional().describe(
54762
- "Absolute path of a JSON template file ending in .json (home, a temp dir, or /Volumes; symlinks refused; at most 256 KiB)"
54742
+ "Absolute path of a JSON template file ending in .json (home, a temp dir, or /Volumes; hidden paths and ~/Library outside iCloud Drive and CloudStorage refused unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1; symlinks refused; at most 256 KiB)"
54763
54743
  );
54764
54744
  function sourceText(args) {
54765
54745
  if (args.template === void 0 === (args.templateFile === void 0))
@@ -54829,7 +54809,7 @@ function registerMarkdownTemplates(server2, store = () => new TemplateStore()) {
54829
54809
  server2.registerTool(
54830
54810
  "validate-markdown-template",
54831
54811
  {
54832
- description: 'Use when: checking a template before saving it or exporting with it.\nReturns: valid true, or valid false with every problem as {path, message}, where path is a JSON path such as $.rules["inline.bold"].after.\nDo not use when: you want to store it (save-markdown-template validates too).\nSafety: read-only. Pass exactly one of name (a saved or built-in template), template (JSON object or text) or templateFile.',
54812
+ description: 'Use when: checking a template before saving it or exporting with it.\nReturns: valid true, or valid false with every problem as {path, message}, where path is a JSON path such as $.rules["inline.bold"].after.\nDo not use when: you want to store it (save-markdown-template validates too).\nSafety: read-only. Pass exactly one of name (a saved or built-in template), template (JSON object or text) or templateFile. templateFile reads only a regular .json file in home, temp or /Volumes, refusing hidden paths (~/.docker, .env) and ~/Library outside iCloud Drive and CloudStorage unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1; errors never quote the file.',
54833
54813
  inputSchema: validateInput,
54834
54814
  outputSchema: loose,
54835
54815
  annotations: { readOnlyHint: true }
@@ -54870,7 +54850,7 @@ function registerMarkdownTemplates(server2, store = () => new TemplateStore()) {
54870
54850
  server2.registerTool(
54871
54851
  "save-markdown-template",
54872
54852
  {
54873
- description: "Use when: storing a template so export-notes-markdown can use it by name.\nReturns: name, path, bytes and whether an existing template was replaced.\nDo not use when: exporting once (pass templateFile to export-notes-markdown instead).\nSafety: writes one file in the template library only (APPLE_NOTES_MCP_TEMPLATE_DIR, default ~/Library/Application Support/apple-notes-mcp/templates, mode 0600). Validates first; an invalid template is refused with every JSON path. Create-only: an existing name is refused with [template-exists] unless force is true. Built-in names are reserved.",
54853
+ description: "Use when: storing a template so export-notes-markdown can use it by name.\nReturns: name, path, bytes and whether an existing template was replaced.\nDo not use when: exporting once (pass templateFile to export-notes-markdown instead).\nSafety: writes one file in the template library only (APPLE_NOTES_MCP_TEMPLATE_DIR, default ~/Library/Application Support/apple-notes-mcp/templates, mode 0600). Validates first; an invalid template is refused with every JSON path. Create-only: an existing name is refused with [template-exists] unless force is true. Built-in names are reserved. templateFile follows the same read scope as validate-markdown-template.",
54874
54854
  inputSchema: saveInput,
54875
54855
  outputSchema: loose,
54876
54856
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
@@ -54913,7 +54893,6 @@ function registerMarkdownTemplates(server2, store = () => new TemplateStore()) {
54913
54893
 
54914
54894
  // src/utils/svgAnalyzer.ts
54915
54895
  import { createHash as createHash5 } from "node:crypto";
54916
- import { closeSync as closeSync9, constants as constants9, fstatSync as fstatSync9, openSync as openSync9, readSync as readSync7 } from "node:fs";
54917
54896
 
54918
54897
  // src/utils/svgColor.ts
54919
54898
  var NAMED = {
@@ -55756,7 +55735,7 @@ function decodeEntities(raw, line) {
55756
55735
  if (body in PREDEFINED) return PREDEFINED[body];
55757
55736
  throw new SvgError(
55758
55737
  "svg_unsafe",
55759
- `Named entity "&${body.slice(0, 40)};" on line ${line}: only the five XML entities are allowed`
55738
+ `A named entity on line ${line} is not allowed: only the five XML entities are`
55760
55739
  );
55761
55740
  });
55762
55741
  }
@@ -55827,6 +55806,8 @@ function parseXml(source, limits) {
55827
55806
  if (!NAME_START.test(source[i] ?? "")) fail("Invalid tag");
55828
55807
  while (i < source.length && NAME_CHAR.test(source[i])) i++;
55829
55808
  const name = source.slice(pos + 1, i);
55809
+ if (!root && !stack.length && name.slice(name.indexOf(":") + 1) !== "svg")
55810
+ fail("The root element is not <svg>");
55830
55811
  const rawAttributes = [];
55831
55812
  const attributeNames = /* @__PURE__ */ new Set();
55832
55813
  let selfClosing = false;
@@ -56944,50 +56925,12 @@ function analyzeSvgBuffer(source) {
56944
56925
  const viewport = analyzer.run();
56945
56926
  return analysisFor(source, analyzer, viewport);
56946
56927
  }
56947
- function readSvgSource(path10) {
56948
- let fd;
56949
- try {
56950
- fd = openSync9(path10, constants9.O_RDONLY | constants9.O_NOFOLLOW | constants9.O_NONBLOCK);
56951
- } catch (error2) {
56952
- if (error2.code === "ELOOP")
56953
- throw new SvgError("svg_file_invalid", "The SVG path is a symbolic link");
56954
- throw new SvgError("svg_file_invalid", "The SVG file does not exist or cannot be read");
56955
- }
56956
- try {
56957
- const info = fstatSync9(fd);
56958
- if (!info.isFile())
56959
- throw new SvgError("svg_file_invalid", "The SVG path is not a regular file");
56960
- if (info.size > SVG_LIMITS.maxSourceBytes)
56961
- throw new SvgError(
56962
- "svg_file_invalid",
56963
- `The SVG is larger than ${SVG_LIMITS.maxSourceBytes} bytes`
56964
- );
56965
- const buffer = Buffer.alloc(SVG_LIMITS.maxSourceBytes + 1);
56966
- let total = 0;
56967
- for (; ; ) {
56968
- const n = readSync7(fd, buffer, total, buffer.length - total, null);
56969
- if (n === 0) break;
56970
- total += n;
56971
- if (total > SVG_LIMITS.maxSourceBytes)
56972
- throw new SvgError(
56973
- "svg_file_invalid",
56974
- `The SVG is larger than ${SVG_LIMITS.maxSourceBytes} bytes`
56975
- );
56976
- }
56977
- return buffer.subarray(0, total);
56978
- } finally {
56979
- closeSync9(fd);
56980
- }
56981
- }
56982
- function analyzeSvgFile(path10) {
56983
- return analyzeSvgBuffer(readSvgSource(path10));
56984
- }
56985
56928
 
56986
56929
  // src/tools/svgAnalysis.ts
56987
56930
  function runSvgAnalysis(args, roots = allowedSaveRoots()) {
56988
- let abs;
56931
+ let source;
56989
56932
  try {
56990
- abs = assertReadableInRoots(args.path, roots, "SVG file");
56933
+ source = readAllowedFile(args.path, SVG_LIMITS.maxSourceBytes, { roots, label: "SVG file" });
56991
56934
  } catch (error2) {
56992
56935
  throw new CodedError(error2.message, {
56993
56936
  code: "validation_error",
@@ -56995,7 +56938,7 @@ function runSvgAnalysis(args, roots = allowedSaveRoots()) {
56995
56938
  });
56996
56939
  }
56997
56940
  try {
56998
- const result = analyzeSvgFile(abs);
56941
+ const result = analyzeSvgBuffer(source);
56999
56942
  return {
57000
56943
  ...result.analysis,
57001
56944
  ...args.includeDrawing ? { drawing: result.drawing } : {}
@@ -57013,7 +56956,7 @@ function runSvgAnalysis(args, roots = allowedSaveRoots()) {
57013
56956
  function registerSvgAnalysis(server2) {
57014
56957
  const inputSchema = {
57015
56958
  path: external_exports.string().min(1).max(4096).describe(
57016
- `Absolute path of one .svg file (home, temp, or /Volumes; not a symbolic link; at most ${SVG_LIMITS.maxSourceBytes} bytes of UTF-8)`
56959
+ `Absolute path of one .svg file (home, temp, or /Volumes; not a hidden path or ~/Library outside iCloud Drive and CloudStorage; not a symbolic link; at most ${SVG_LIMITS.maxSourceBytes} bytes of UTF-8)`
57017
56960
  ),
57018
56961
  includeDrawing: external_exports.boolean().optional().describe(
57019
56962
  "Also return the normalized drawing (strokes with sRGB color, width and points). Off by default: it can be large."
@@ -57022,7 +56965,7 @@ function registerSvgAnalysis(server2) {
57022
56965
  server2.registerTool(
57023
56966
  "analyze-svg",
57024
56967
  {
57025
- description: "Use when: checking whether an SVG file can be represented as editable monoline strokes, and what that conversion would approximate or drop.\nReturns: classification (safe, lossy, unsupported), importable, defaultWriteAllowed, requiredLosses (geometry-approximation, paint-approximation, drop-content), issues with element locations, counts and work budgets, the source SHA-256, and analysisDigest, a SHA-256 over the canonical analysis and normalized drawing. includeDrawing adds the drawing.\nDo not use when: you want to attach the SVG file itself to a note (add-attachment).\nSafety: read-only and local. Opens no Notes data and makes no network request. Scripts, event handlers, style elements, animation, DOCTYPE/entities and external resources are refused with svgCode svg_unsafe; malformed files with svg_invalid; oversized work with svg_complexity_limit.",
56968
+ description: "Use when: checking whether an SVG file can be represented as editable monoline strokes, and what that conversion would approximate or drop.\nReturns: classification (safe, lossy, unsupported), importable, defaultWriteAllowed, requiredLosses (geometry-approximation, paint-approximation, drop-content), issues with element locations, counts and work budgets, the source SHA-256, and analysisDigest, a SHA-256 over the canonical analysis and normalized drawing. includeDrawing adds the drawing.\nDo not use when: you want to attach the SVG file itself to a note (add-attachment).\nSafety: read-only and local. Opens no Notes data and makes no network request. Reads only a regular file in home, temp or /Volumes, refusing hidden paths (~/.ssh, .env) and ~/Library outside iCloud Drive and CloudStorage unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1; a file whose root element is not <svg> is refused without quoting it. Scripts, event handlers, style elements, animation, DOCTYPE/entities and external resources are refused with svgCode svg_unsafe; malformed files with svg_invalid; oversized work with svg_complexity_limit.",
57026
56969
  inputSchema,
57027
56970
  outputSchema: external_exports.object({ classification: external_exports.string().optional() }).passthrough(),
57028
56971
  annotations: { readOnlyHint: true, openWorldHint: false }
@@ -62209,7 +62152,7 @@ var exportStatsSchema = external_exports.object({
62209
62152
  registerTool(
62210
62153
  "export-notes-markdown",
62211
62154
  {
62212
- description: "Use when: exporting one note (by exact id) or a folder's notes as one Markdown document rendered from the decoded note body: headings, bulleted/dashed/numbered lists with indent, checklists with state, block quotes, monospaced blocks, bold/italic/strikethrough/underline/highlight, links, tables, and attachments in body order.\nReturns: the Markdown inline (capped by APPLE_NOTES_MCP_EXPORT_MAX_BYTES), or with outputPath a receipt {format, count, bytes, output}; plus attachment counts and skipped notes (for example password-protected ones).\nDo not use when: you need the legacy HTML-converted Markdown of one note (get-note-markdown) or a restorable backup (export-notes-json). A folder document separates notes with '---' and is a presentation format, not something to import back.\nSafety: read-only against Notes; requires Full Disk Access. outputPath is create-only (an existing file is refused with [output_exists]); assetsDir copies attachment files without replacing existing ones (collisions get -2, -3 suffixes). Without assetsDir, attachments render as labeled placeholders.\nTemplates: pass template ('standard-markdown' reproduces the default output; 'obsidian' adds YAML front matter and copies attachments beside outputPath) or templateFile (a JSON template) to control how every block, inline style, attachment, per-note header/footer and separator renders. Templated receipts add template, warnings (e.g. missing_asset) and assetFiles; an invalid template is refused with [invalid-template] and every problem's JSON path.",
62155
+ description: "Use when: exporting one note (by exact id) or a folder's notes as one Markdown document rendered from the decoded note body: headings, bulleted/dashed/numbered lists with indent, checklists with state, block quotes, monospaced blocks, bold/italic/strikethrough/underline/highlight, links, tables, and attachments in body order.\nReturns: the Markdown inline (capped by APPLE_NOTES_MCP_EXPORT_MAX_BYTES), or with outputPath a receipt {format, count, bytes, output}; plus attachment counts and skipped notes (for example password-protected ones).\nDo not use when: you need the legacy HTML-converted Markdown of one note (get-note-markdown) or a restorable backup (export-notes-json). A folder document separates notes with '---' and is a presentation format, not something to import back.\nSafety: read-only against Notes; requires Full Disk Access. outputPath is create-only (an existing file is refused with [output_exists]); assetsDir copies attachment files without replacing existing ones (collisions get -2, -3 suffixes). Without assetsDir, attachments render as labeled placeholders.\nTemplates: pass template ('standard-markdown' reproduces the default output; 'obsidian' adds YAML front matter and copies attachments beside outputPath) or templateFile (a JSON template; hidden paths and ~/Library outside iCloud Drive and CloudStorage are refused unless the server sets APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1) to control how every block, inline style, attachment, per-note header/footer and separator renders. Templated receipts add template, warnings (e.g. missing_asset) and assetFiles; an invalid template is refused with [invalid-template] and every problem's JSON path.",
62213
62156
  inputSchema: {
62214
62157
  id: noteIdInput.optional(),
62215
62158
  folder: external_exports.string().min(1).max(MAX.FOLDER).optional().describe("Folder path to export instead of one note (nested paths use '/')"),
@@ -62224,7 +62167,7 @@ registerTool(
62224
62167
  `Render through this template: built-in ${BUILTIN_TEMPLATE_NAMES.map((n) => `'${n}'`).join(" or ")}, or a saved template's name (list-markdown-templates). Exclusive with templateFile`
62225
62168
  ),
62226
62169
  templateFile: exportPathInput(
62227
- "JSON template file (.json) to render through (exclusive with template; at most 256 KiB)"
62170
+ "JSON template file (.json) to render through (exclusive with template; at most 256 KiB; hidden paths and ~/Library outside iCloud Drive and CloudStorage refused unless APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1)"
62228
62171
  )
62229
62172
  },
62230
62173
  outputSchema: {
@@ -25,7 +25,7 @@ Pass exactly one of these to `export-notes-markdown`:
25
25
  | Parameter | Meaning |
26
26
  | --- | --- |
27
27
  | `template` | A built-in name (`standard-markdown` or `obsidian`) or a saved template's name |
28
- | `templateFile` | An absolute path to a JSON template file ending in `.json` (home, a temp directory, or `/Volumes`; at most 256 KiB; symlinks refused) |
28
+ | `templateFile` | An absolute path to a JSON template file ending in `.json` (home, a temp directory, or `/Volumes`; at most 256 KiB; symlinks refused). Hidden paths and `~/Library` other than iCloud Drive and `~/Library/CloudStorage` are refused unless the server sets `APPLE_NOTES_MCP_ALLOW_PRIVATE_CONTENT_PATHS=1`. Errors name the file, never its contents |
29
29
 
30
30
  The template is read and validated before any note is opened. An invalid
31
31
  template is refused with `[invalid-template]` and one line per problem, each
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-notes-mcp",
3
- "version": "2.9.27",
3
+ "version": "2.9.28",
4
4
  "description": "MCP server for Apple Notes - read, search, create, edit, organize, and export notes on macOS via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",