apple-notes-mcp 2.9.27 → 2.9.30

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`.
@@ -2408,7 +2409,7 @@ listed under the tool itself:
2408
2409
  |-----------|------|----------|-------------|
2409
2410
  | `id` | string | Yes | Exact note ID: x-coredata id, Notes UUID, or numeric key (see [Identifier forms](#identifier-forms)) |
2410
2411
  | `expectedContentHash` | string | Yes | `contentHash` (`sha256:…`) from a fresh read of the note |
2411
- | `scopeText` | string | Yes | Distinctive phrase already in the note, 12–500 characters, that Notes search can find. Prefer plain words without punctuation, hashtags, or paths |
2412
+ | `scopeText` | string | Yes | Distinctive phrase already in the note below its title line, 12–500 characters, that Notes search can find. Prefer plain words without punctuation, hashtags, or paths |
2412
2413
 
2413
2414
  When the Shortcut refuses a request (its Find Notes step did not return
2414
2415
  exactly one note with that exact title and `scopeText`) or stops with its own
@@ -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;
@@ -48719,6 +48723,12 @@ function describeTransportFailure(error2) {
48719
48723
  if (detail?.refused) return `${named} ${detail.message}`;
48720
48724
  return detail?.code === "ETIMEDOUT" ? `Shortcuts timed out waiting for ${named}. ${shortcutConsentHint(detail.shortcut)}` : `${named} failed: ${String(detail?.stderr || detail?.message || "no output").trim().slice(0, 400)}`;
48721
48725
  }
48726
+ function textBelowTitle(text2, title) {
48727
+ const lineEnd = text2.indexOf("\n");
48728
+ const firstLine = lineEnd < 0 ? text2 : text2.slice(0, lineEnd);
48729
+ if (firstLine.trim() !== title.trim()) return text2;
48730
+ return lineEnd < 0 ? "" : text2.slice(lineEnd + 1);
48731
+ }
48722
48732
  function mutateBackground(request, operation, data, verify, deps) {
48723
48733
  if (request.scopeText.length < 12 || request.scopeText.length > 500 || /[\r\n\0]/u.test(request.scopeText))
48724
48734
  throw new Error("Use a distinctive existing single-line scope of 12\u2013500 characters");
@@ -48727,6 +48737,11 @@ function mutateBackground(request, operation, data, verify, deps) {
48727
48737
  throw new Error("Note revision changed; read it again");
48728
48738
  if (!before.rich.text.includes(request.scopeText))
48729
48739
  throw new Error("Scope is absent from exact note");
48740
+ if (!textBelowTitle(before.rich.text, before.title).includes(request.scopeText))
48741
+ throw new CodedError(
48742
+ "scopeText appears only in the note's title line. The bridge's Find Notes step matches it against the body below the title, so it would refuse; pass a phrase from below the title. Nothing changed",
48743
+ { code: "validation_error", committed: false, indeterminate: false }
48744
+ );
48730
48745
  const candidates = deps.candidates(before.title, request.scopeText);
48731
48746
  if (candidates.length !== 1 || candidates[0] !== request.id)
48732
48747
  throw new Error("Ambiguous note selection; nothing changed");
@@ -53141,33 +53156,9 @@ function templateAssetsDir(directory, outputDir, values) {
53141
53156
  return dir;
53142
53157
  }
53143
53158
  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
- }
53159
+ if (extname4(path10).toLowerCase() !== ".json")
53160
+ throw new Error(`Template file must have a .json extension: ${path10}`);
53161
+ return readAllowedFile(path10, MAX_TEMPLATE_BYTES, { label: "Template file" }).toString("utf8");
53171
53162
  }
53172
53163
 
53173
53164
  // src/services/notesExport.ts
@@ -54759,7 +54750,7 @@ function failure(action, error2) {
54759
54750
  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
54751
  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
54752
  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)"
54753
+ "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
54754
  );
54764
54755
  function sourceText(args) {
54765
54756
  if (args.template === void 0 === (args.templateFile === void 0))
@@ -54829,7 +54820,7 @@ function registerMarkdownTemplates(server2, store = () => new TemplateStore()) {
54829
54820
  server2.registerTool(
54830
54821
  "validate-markdown-template",
54831
54822
  {
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.',
54823
+ 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
54824
  inputSchema: validateInput,
54834
54825
  outputSchema: loose,
54835
54826
  annotations: { readOnlyHint: true }
@@ -54870,7 +54861,7 @@ function registerMarkdownTemplates(server2, store = () => new TemplateStore()) {
54870
54861
  server2.registerTool(
54871
54862
  "save-markdown-template",
54872
54863
  {
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.",
54864
+ 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
54865
  inputSchema: saveInput,
54875
54866
  outputSchema: loose,
54876
54867
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
@@ -54913,7 +54904,6 @@ function registerMarkdownTemplates(server2, store = () => new TemplateStore()) {
54913
54904
 
54914
54905
  // src/utils/svgAnalyzer.ts
54915
54906
  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
54907
 
54918
54908
  // src/utils/svgColor.ts
54919
54909
  var NAMED = {
@@ -55756,7 +55746,7 @@ function decodeEntities(raw, line) {
55756
55746
  if (body in PREDEFINED) return PREDEFINED[body];
55757
55747
  throw new SvgError(
55758
55748
  "svg_unsafe",
55759
- `Named entity "&${body.slice(0, 40)};" on line ${line}: only the five XML entities are allowed`
55749
+ `A named entity on line ${line} is not allowed: only the five XML entities are`
55760
55750
  );
55761
55751
  });
55762
55752
  }
@@ -55827,6 +55817,8 @@ function parseXml(source, limits) {
55827
55817
  if (!NAME_START.test(source[i] ?? "")) fail("Invalid tag");
55828
55818
  while (i < source.length && NAME_CHAR.test(source[i])) i++;
55829
55819
  const name = source.slice(pos + 1, i);
55820
+ if (!root && !stack.length && name.slice(name.indexOf(":") + 1) !== "svg")
55821
+ fail("The root element is not <svg>");
55830
55822
  const rawAttributes = [];
55831
55823
  const attributeNames = /* @__PURE__ */ new Set();
55832
55824
  let selfClosing = false;
@@ -56944,50 +56936,12 @@ function analyzeSvgBuffer(source) {
56944
56936
  const viewport = analyzer.run();
56945
56937
  return analysisFor(source, analyzer, viewport);
56946
56938
  }
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
56939
 
56986
56940
  // src/tools/svgAnalysis.ts
56987
56941
  function runSvgAnalysis(args, roots = allowedSaveRoots()) {
56988
- let abs;
56942
+ let source;
56989
56943
  try {
56990
- abs = assertReadableInRoots(args.path, roots, "SVG file");
56944
+ source = readAllowedFile(args.path, SVG_LIMITS.maxSourceBytes, { roots, label: "SVG file" });
56991
56945
  } catch (error2) {
56992
56946
  throw new CodedError(error2.message, {
56993
56947
  code: "validation_error",
@@ -56995,7 +56949,7 @@ function runSvgAnalysis(args, roots = allowedSaveRoots()) {
56995
56949
  });
56996
56950
  }
56997
56951
  try {
56998
- const result = analyzeSvgFile(abs);
56952
+ const result = analyzeSvgBuffer(source);
56999
56953
  return {
57000
56954
  ...result.analysis,
57001
56955
  ...args.includeDrawing ? { drawing: result.drawing } : {}
@@ -57013,7 +56967,7 @@ function runSvgAnalysis(args, roots = allowedSaveRoots()) {
57013
56967
  function registerSvgAnalysis(server2) {
57014
56968
  const inputSchema = {
57015
56969
  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)`
56970
+ `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
56971
  ),
57018
56972
  includeDrawing: external_exports.boolean().optional().describe(
57019
56973
  "Also return the normalized drawing (strokes with sRGB color, width and points). Off by default: it can be large."
@@ -57022,7 +56976,7 @@ function registerSvgAnalysis(server2) {
57022
56976
  server2.registerTool(
57023
56977
  "analyze-svg",
57024
56978
  {
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.",
56979
+ 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
56980
  inputSchema,
57027
56981
  outputSchema: external_exports.object({ classification: external_exports.string().optional() }).passthrough(),
57028
56982
  annotations: { readOnlyHint: true, openWorldHint: false }
@@ -57165,7 +57119,7 @@ var common = {
57165
57119
  id,
57166
57120
  expectedContentHash: revision3,
57167
57121
  scopeText: external_exports.string().min(12).max(500).describe(
57168
- "Distinctive existing phrase used by Notes search. Prefer plain words without punctuation, hashtags, or paths."
57122
+ "Distinctive existing phrase from below the title line, used by Notes search. Prefer plain words without punctuation, hashtags, or paths."
57169
57123
  )
57170
57124
  };
57171
57125
  var EMPTY_TABLE_ROWS = [
@@ -62209,7 +62163,7 @@ var exportStatsSchema = external_exports.object({
62209
62163
  registerTool(
62210
62164
  "export-notes-markdown",
62211
62165
  {
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.",
62166
+ 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
62167
  inputSchema: {
62214
62168
  id: noteIdInput.optional(),
62215
62169
  folder: external_exports.string().min(1).max(MAX.FOLDER).optional().describe("Folder path to export instead of one note (nested paths use '/')"),
@@ -62224,7 +62178,7 @@ registerTool(
62224
62178
  `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
62179
  ),
62226
62180
  templateFile: exportPathInput(
62227
- "JSON template file (.json) to render through (exclusive with template; at most 256 KiB)"
62181
+ "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
62182
  )
62229
62183
  },
62230
62184
  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.30",
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",