apple-numbers-mcp 1.1.19 → 1.2.0

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
@@ -675,6 +675,7 @@ All configuration is optional — the server works out of the box.
675
675
  | `APPLE_NUMBERS_MCP_MAX_BUFFER` | 50 MB (Python reader) / 64 MB (AppleScript) | Max bytes captured from a subprocess's stdout, applied to both the Python reader and the AppleScript layer. Raise it if a very large spreadsheet is truncated; lower it to cap memory. |
676
676
  | `APPLE_NUMBERS_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-numbers-mcp/config.json` | Path to the JSON config file (see below). |
677
677
  | `APPLE_NUMBERS_MCP_NO_AUTO_SETUP` | unset (auto-setup on) | Set to a truthy value (`1`, `true`) to disable the one-time automatic creation of the Python venv. When set, the server will not run `setup.sh` on its own — you must provide `numbers-parser` yourself (`pip3 install numbers-parser` or `pnpm run setup`). |
678
+ | `APPLE_NUMBERS_MCP_EXTRA_ROOTS` | unset | Colon-separated **absolute** directories to add to the allowed-path roots, e.g. `/Data/Finance:/srv/shared`. The built-in roots already cover your home directory, the temp dirs and `/Volumes` (where external and network mounts appear), so this is for genuinely unusual layouts rather than routine configuration. Entries are canonicalized like the built-ins, so a symlinked entry cannot smuggle in a wider parent; relative and empty entries are ignored. **Widening a security boundary — set it deliberately.** |
678
679
  | `APPLE_NUMBERS_MCP_SETUP_TIMEOUT` | `300000` (5 minutes) | Timeout in milliseconds for the automatic venv bootstrap (`setup.sh`). Raise it on a slow network where the `numbers-parser` pip install would otherwise time out. |
679
680
 
680
681
  > **Per-call timeouts are not configurable.** Every `numbers-parser` sidecar call
@@ -775,9 +776,10 @@ These are tracked for future releases. The underlying `numbers-parser` library h
775
776
  - Ensure the file extension is `.numbers`.
776
777
 
777
778
  ### "Output path … is outside the allowed roots" / "Input path … is outside the allowed roots"
778
- - `export-table`, `create-spreadsheet` and `import-csv` read and write ordinary files, so their paths are restricted to your **home directory**, `/tmp`, `/private/tmp`, and `/Volumes` — the server will not write into `/etc`, `/Library`, an app bundle, or any other system location, and will not read a source file from one.
779
+ - Every tool that touches a file on disk is bounded to your **home directory**, `/tmp`, `/private/tmp`, and `/Volumes` — the server will not write into `/etc`, `/Library`, an app bundle, or any other system location, and will not read from one.
780
+ - **As of 1.2.0 this covers the `.numbers` file itself**, not just export/import destinations. `get-file-info`, `read-table`, `search` and the in-place writes (`set-cell`, `add-rows`, `set-formula`, …) previously accepted a path anywhere on disk.
779
781
  - Symlinks are resolved **before** the check, so a link inside an allowed directory that points outside it is rejected too; the error reports the resolved path, which is usually the quickest way to see where a path really went.
780
- - Fix: choose a path under one of those roots (`~/Documents/report.csv`, `/tmp/report.csv`). There is no override setting.
782
+ - Fix: choose a path under one of those roots (`~/Documents/report.numbers`, `/tmp/report.csv`). If your spreadsheets genuinely live somewhere else, add that directory to `APPLE_NUMBERS_MCP_EXTRA_ROOTS` — see [Environment variables](#environment-variables).
781
783
 
782
784
  ### Formatting / formula tools fail with "Numbers.app not running" or "Not authorized to send Apple events to Numbers"
783
785
  - Open Numbers.app at least once. macOS will prompt for automation permission — accept it.
package/build/index.js CHANGED
@@ -21813,8 +21813,7 @@ function unmergeCells(filePath, sheet, table, startRow, startCol, endRow, endCol
21813
21813
 
21814
21814
  // src/services/numbersManager.ts
21815
21815
  import { existsSync as existsSync3 } from "node:fs";
21816
- import { resolve as resolve2, extname } from "node:path";
21817
- import { homedir as homedir2 } from "node:os";
21816
+ import { extname } from "node:path";
21818
21817
 
21819
21818
  // src/utils/exportPath.ts
21820
21819
  import { lstatSync, readlinkSync, realpathSync } from "node:fs";
@@ -21833,12 +21832,18 @@ var ALLOWED_EXPORT_ROOTS = [
21833
21832
  "/Volumes"
21834
21833
  ];
21835
21834
  var ALLOWED_EXPORT_ROOTS_TEXT = "your home directory, /tmp, /private/tmp, or /Volumes";
21835
+ var EXTRA_ROOTS_ENV = "APPLE_NUMBERS_MCP_EXTRA_ROOTS";
21836
+ function extraRoots() {
21837
+ const raw = process.env[EXTRA_ROOTS_ENV];
21838
+ if (!raw) return [];
21839
+ return raw.split(":").map((s) => s.trim()).filter((s) => s.length > 0 && isAbsolute(s));
21840
+ }
21836
21841
  function canonicalize(path) {
21837
21842
  return realpathSync.native(path);
21838
21843
  }
21839
21844
  function allowedRoots() {
21840
21845
  const roots = [];
21841
- for (const root of ALLOWED_EXPORT_ROOTS) {
21846
+ for (const root of [...ALLOWED_EXPORT_ROOTS, ...extraRoots()]) {
21842
21847
  if (!roots.includes(root)) roots.push(root);
21843
21848
  try {
21844
21849
  const canonical = canonicalize(root);
@@ -21912,9 +21917,32 @@ function resolveWithinAllowedRoots(path, label) {
21912
21917
 
21913
21918
  // src/services/numbersManager.ts
21914
21919
  var NumbersManager = class {
21920
+ /**
21921
+ * Resolve and validate the path of an EXISTING `.numbers` file.
21922
+ *
21923
+ * This is the resolver behind `get-file-info`, `read-table`, `search` and
21924
+ * every in-place write (`set-cell`, `add-rows`, `set-formula`, …). It used to
21925
+ * expand `~`, `resolve()` and check the extension — and nothing else — so
21926
+ * those tools could read and modify a `.numbers` file **anywhere on disk**,
21927
+ * while its sibling `validateOutputPath` had enforced the allowed-roots
21928
+ * boundary since 1.1.19. That asymmetry was the largest remaining gap here.
21929
+ *
21930
+ * It now goes through the same `resolveWithinAllowedRoots` as the write side,
21931
+ * which also brings the hardening that came with it: canonicalization via
21932
+ * `realpathSync.native` (so a case-respelled segment cannot defeat the check
21933
+ * on case-insensitive APFS) and symlink resolution BEFORE the comparison (so
21934
+ * a link inside an allowed directory cannot point out of it).
21935
+ *
21936
+ * The extension is checked on the CANONICAL path for the same reason it is on
21937
+ * the output side: a symlink must not be able to present a `.numbers` name
21938
+ * for a target that is something else entirely.
21939
+ *
21940
+ * The built-in roots cover home, the temp dirs and `/Volumes`, so a file the
21941
+ * user could plausibly want to open is almost always already inside them.
21942
+ * `APPLE_NUMBERS_MCP_EXTRA_ROOTS` exists for the layouts they are not.
21943
+ */
21915
21944
  validatePath(filePath) {
21916
- const expanded = filePath.startsWith("~") ? filePath.replace(/^~/, homedir2()) : filePath;
21917
- const resolved = resolve2(expanded);
21945
+ const resolved = resolveWithinAllowedRoots(filePath, "Input path");
21918
21946
  if (!existsSync3(resolved)) {
21919
21947
  throw new Error(`File not found: ${resolved}`);
21920
21948
  }
@@ -22444,11 +22472,11 @@ First use get-file-info and read-table to read the relevant table(s) so you know
22444
22472
  // src/services/fileConfig.ts
22445
22473
  import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
22446
22474
  import { join as join3 } from "node:path";
22447
- import { homedir as homedir3 } from "node:os";
22475
+ import { homedir as homedir2 } from "node:os";
22448
22476
  function fileConfigPath(env = process.env) {
22449
22477
  const override = env.APPLE_NUMBERS_MCP_CONFIG_FILE;
22450
22478
  if (override && override.trim()) return override.trim();
22451
- return join3(homedir3(), "Library", "Application Support", "apple-numbers-mcp", "config.json");
22479
+ return join3(homedir2(), "Library", "Application Support", "apple-numbers-mcp", "config.json");
22452
22480
  }
22453
22481
  function loadFileConfig(env = process.env, path = fileConfigPath(env)) {
22454
22482
  const applied = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-numbers-mcp",
3
- "version": "1.1.19",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for Apple Numbers - read, write, search, and format .numbers spreadsheets via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",