apple-numbers-mcp 1.1.18 → 1.1.19

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 +8 -3
  2. package/build/index.js +150 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -303,7 +303,7 @@ Case-insensitive partial match across every cell in a `.numbers` file.
303
303
 
304
304
  Export a table to CSV, TSV, or JSON.
305
305
 
306
- **⚠️ Safety:** Writes a file to disk at `outputPath`. The path is written unconditionally — any existing file there is **overwritten** — so confirm the destination before calling.
306
+ **⚠️ Safety:** Writes a file to disk at `outputPath`. The path is written unconditionally — any existing file there is **overwritten** — so confirm the destination before calling. Paths are bounded: `outputPath` must resolve — after `~` expansion and symlink resolution — to a location under your **home directory**, `/tmp`, `/private/tmp`, or `/Volumes`; anything else is rejected with an error naming those roots.
307
307
 
308
308
  | Parameter | Type | Required | Description |
309
309
  |-----------|------|----------|-------------|
@@ -321,7 +321,7 @@ Export a table to CSV, TSV, or JSON.
321
321
 
322
322
  Create a new `.numbers` file with one sheet and table.
323
323
 
324
- **⚠️ Safety:** Overwrites the file at `path` if it already exists — confirm the destination first.
324
+ **⚠️ Safety:** Overwrites the file at `path` if it already exists — confirm the destination first. Paths are bounded: `path` must resolve — after `~` expansion and symlink resolution — to a location under your **home directory**, `/tmp`, `/private/tmp`, or `/Volumes`; anything else is rejected with an error naming those roots.
325
325
 
326
326
  | Parameter | Type | Required | Description |
327
327
  |-----------|------|----------|-------------|
@@ -573,7 +573,7 @@ Merge a rectangular range of cells, or undo a merge.
573
573
 
574
574
  Import a CSV, TSV, or JSON file into a new `.numbers` spreadsheet. Auto-detects format from extension, or pass `format` explicitly.
575
575
 
576
- **⚠️ Safety:** Overwrites the file at `outputPath` if it already exists — confirm the destination first.
576
+ **⚠️ Safety:** Overwrites the file at `outputPath` if it already exists — confirm the destination first. Paths are bounded: both `inputPath` and `outputPath` must resolve — after `~` expansion and symlink resolution — to locations under your **home directory**, `/tmp`, `/private/tmp`, or `/Volumes`; anything else is rejected with an error naming those roots.
577
577
 
578
578
  | Parameter | Type | Required | Description |
579
579
  |-----------|------|----------|-------------|
@@ -774,6 +774,11 @@ These are tracked for future releases. The underlying `numbers-parser` library h
774
774
  - Check the path; expand `~` if your shell isn't doing it.
775
775
  - Ensure the file extension is `.numbers`.
776
776
 
777
+ ### "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
+ - 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.
781
+
777
782
  ### Formatting / formula tools fail with "Numbers.app not running" or "Not authorized to send Apple events to Numbers"
778
783
  - Open Numbers.app at least once. macOS will prompt for automation permission — accept it.
779
784
  - Verify in **System Settings → Privacy & Security → Automation**, or reset with `tccutil reset AppleEvents`.
package/build/index.js CHANGED
@@ -2985,7 +2985,7 @@ var require_compile = __commonJS({
2985
2985
  const schOrFunc = root.refs[ref];
2986
2986
  if (schOrFunc)
2987
2987
  return schOrFunc;
2988
- let _sch = resolve2.call(this, root, ref);
2988
+ let _sch = resolve3.call(this, root, ref);
2989
2989
  if (_sch === void 0) {
2990
2990
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
2991
2991
  const { schemaId } = this.opts;
@@ -3012,7 +3012,7 @@ var require_compile = __commonJS({
3012
3012
  function sameSchemaEnv(s1, s2) {
3013
3013
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3014
3014
  }
3015
- function resolve2(root, ref) {
3015
+ function resolve3(root, ref) {
3016
3016
  let sch;
3017
3017
  while (typeof (sch = this.refs[ref]) == "string")
3018
3018
  ref = sch;
@@ -3643,7 +3643,7 @@ var require_fast_uri = __commonJS({
3643
3643
  }
3644
3644
  return uri;
3645
3645
  }
3646
- function resolve2(baseURI, relativeURI, options) {
3646
+ function resolve3(baseURI, relativeURI, options) {
3647
3647
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3648
3648
  const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3649
3649
  const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
@@ -3927,7 +3927,7 @@ var require_fast_uri = __commonJS({
3927
3927
  var fastUri = {
3928
3928
  SCHEMES,
3929
3929
  normalize,
3930
- resolve: resolve2,
3930
+ resolve: resolve3,
3931
3931
  resolveComponent,
3932
3932
  equal,
3933
3933
  serialize,
@@ -19023,7 +19023,7 @@ var Protocol = class {
19023
19023
  return;
19024
19024
  }
19025
19025
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
19026
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
19026
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
19027
19027
  options?.signal?.throwIfAborted();
19028
19028
  }
19029
19029
  } catch (error2) {
@@ -19040,7 +19040,7 @@ var Protocol = class {
19040
19040
  */
19041
19041
  request(request, resultSchema, options) {
19042
19042
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
19043
- return new Promise((resolve2, reject) => {
19043
+ return new Promise((resolve3, reject) => {
19044
19044
  const earlyReject = (error2) => {
19045
19045
  reject(error2);
19046
19046
  };
@@ -19118,7 +19118,7 @@ var Protocol = class {
19118
19118
  if (!parseResult.success) {
19119
19119
  reject(parseResult.error);
19120
19120
  } else {
19121
- resolve2(parseResult.data);
19121
+ resolve3(parseResult.data);
19122
19122
  }
19123
19123
  } catch (error2) {
19124
19124
  reject(error2);
@@ -19379,12 +19379,12 @@ var Protocol = class {
19379
19379
  }
19380
19380
  } catch {
19381
19381
  }
19382
- return new Promise((resolve2, reject) => {
19382
+ return new Promise((resolve3, reject) => {
19383
19383
  if (signal.aborted) {
19384
19384
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
19385
19385
  return;
19386
19386
  }
19387
- const timeoutId = setTimeout(resolve2, interval);
19387
+ const timeoutId = setTimeout(resolve3, interval);
19388
19388
  signal.addEventListener("abort", () => {
19389
19389
  clearTimeout(timeoutId);
19390
19390
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -20697,7 +20697,7 @@ var McpServer = class {
20697
20697
  let task = createTaskResult.task;
20698
20698
  const pollInterval = task.pollInterval ?? 5e3;
20699
20699
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
20700
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
20700
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
20701
20701
  const updatedTask = await extra.taskStore.getTask(taskId);
20702
20702
  if (!updatedTask) {
20703
20703
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -21385,12 +21385,12 @@ var StdioServerTransport = class {
21385
21385
  this.onclose?.();
21386
21386
  }
21387
21387
  send(message) {
21388
- return new Promise((resolve2) => {
21388
+ return new Promise((resolve3) => {
21389
21389
  const json2 = serializeMessage(message);
21390
21390
  if (this._stdout.write(json2)) {
21391
- resolve2();
21391
+ resolve3();
21392
21392
  } else {
21393
- this._stdout.once("drain", resolve2);
21393
+ this._stdout.once("drain", resolve3);
21394
21394
  }
21395
21395
  });
21396
21396
  }
@@ -21399,7 +21399,7 @@ var StdioServerTransport = class {
21399
21399
  // src/index.ts
21400
21400
  import { readFileSync as readFileSync3 } from "node:fs";
21401
21401
  import { fileURLToPath as fileURLToPath2 } from "node:url";
21402
- import { dirname as dirname2, join as join3 } from "node:path";
21402
+ import { dirname as dirname3, join as join4 } from "node:path";
21403
21403
 
21404
21404
  // src/utils/python.ts
21405
21405
  import { execSync, execFileSync } from "node:child_process";
@@ -21812,14 +21812,110 @@ function unmergeCells(filePath, sheet, table, startRow, startCol, endRow, endCol
21812
21812
  }
21813
21813
 
21814
21814
  // src/services/numbersManager.ts
21815
- import { existsSync as existsSync2 } from "node:fs";
21816
- import { resolve, extname } from "node:path";
21817
- import { homedir } from "node:os";
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";
21818
+
21819
+ // src/utils/exportPath.ts
21820
+ import { lstatSync, readlinkSync, realpathSync } from "node:fs";
21821
+ import { homedir, tmpdir } from "node:os";
21822
+ import { basename, dirname as dirname2, isAbsolute, join as join2, resolve, sep } from "node:path";
21823
+ var ALLOWED_EXPORT_ROOTS = [
21824
+ homedir(),
21825
+ // os.tmpdir() is the canonical per-user temp dir on macOS (/var/folders/<hash>/T,
21826
+ // real path /private/var/folders/...). It is what Node's os.tmpdir(), Python's
21827
+ // tempfile and $TMPDIR all return — and what this repo's own fixtures use — so
21828
+ // omitting it refuses the most ordinary scratch destination there is.
21829
+ tmpdir(),
21830
+ "/private/var/folders",
21831
+ "/tmp",
21832
+ "/private/tmp",
21833
+ "/Volumes"
21834
+ ];
21835
+ var ALLOWED_EXPORT_ROOTS_TEXT = "your home directory, /tmp, /private/tmp, or /Volumes";
21836
+ function canonicalize(path) {
21837
+ return realpathSync.native(path);
21838
+ }
21839
+ function allowedRoots() {
21840
+ const roots = [];
21841
+ for (const root of ALLOWED_EXPORT_ROOTS) {
21842
+ if (!roots.includes(root)) roots.push(root);
21843
+ try {
21844
+ const canonical = canonicalize(root);
21845
+ if (!roots.includes(canonical)) roots.push(canonical);
21846
+ } catch {
21847
+ }
21848
+ }
21849
+ return roots;
21850
+ }
21851
+ function isPathWithinAllowedRoots(resolvedPath) {
21852
+ return allowedRoots().some((root) => {
21853
+ const base = root.endsWith(sep) ? root.slice(0, -1) : root;
21854
+ return resolvedPath === base || resolvedPath.startsWith(base + sep);
21855
+ });
21856
+ }
21857
+ function expandTilde(p) {
21858
+ if (p === "~") return homedir();
21859
+ if (p.startsWith(`~${sep}`) || p.startsWith("~/")) return join2(homedir(), p.slice(2));
21860
+ return p;
21861
+ }
21862
+ function entryPresent(p) {
21863
+ try {
21864
+ lstatSync(p);
21865
+ return true;
21866
+ } catch (e) {
21867
+ const code = e.code;
21868
+ return !(code === "ENOENT" || code === "ENOTDIR");
21869
+ }
21870
+ }
21871
+ function canonicalizeCandidate(p, depth = 0) {
21872
+ if (depth > 32) throw new Error(`Too many symbolic links resolving "${p}"`);
21873
+ let existing = p;
21874
+ const tail = [];
21875
+ while (!entryPresent(existing)) {
21876
+ const parent = dirname2(existing);
21877
+ if (parent === existing) break;
21878
+ tail.unshift(basename(existing));
21879
+ existing = parent;
21880
+ }
21881
+ let real;
21882
+ try {
21883
+ real = canonicalize(existing);
21884
+ } catch {
21885
+ const viaLink = resolveDanglingLink(existing, depth);
21886
+ if (viaLink !== null) {
21887
+ return canonicalizeCandidate(tail.length ? join2(viaLink, ...tail) : viaLink, depth + 1);
21888
+ }
21889
+ real = existing;
21890
+ }
21891
+ return tail.length ? join2(real, ...tail) : real;
21892
+ }
21893
+ function resolveDanglingLink(p, depth) {
21894
+ try {
21895
+ if (!lstatSync(p).isSymbolicLink()) return null;
21896
+ const target = readlinkSync(p);
21897
+ if (isAbsolute(target)) return target;
21898
+ return resolve(canonicalizeCandidate(dirname2(p), depth + 1), target);
21899
+ } catch {
21900
+ return null;
21901
+ }
21902
+ }
21903
+ function resolveWithinAllowedRoots(path, label) {
21904
+ const resolved = canonicalizeCandidate(resolve(expandTilde(path)));
21905
+ if (!isPathWithinAllowedRoots(resolved)) {
21906
+ throw new Error(
21907
+ `${label} "${path}" resolves to "${resolved}", which is outside the allowed roots (${ALLOWED_EXPORT_ROOTS_TEXT}). Choose a path under one of those roots.`
21908
+ );
21909
+ }
21910
+ return resolved;
21911
+ }
21912
+
21913
+ // src/services/numbersManager.ts
21818
21914
  var NumbersManager = class {
21819
21915
  validatePath(filePath) {
21820
- const expanded = filePath.startsWith("~") ? filePath.replace(/^~/, homedir()) : filePath;
21821
- const resolved = resolve(expanded);
21822
- if (!existsSync2(resolved)) {
21916
+ const expanded = filePath.startsWith("~") ? filePath.replace(/^~/, homedir2()) : filePath;
21917
+ const resolved = resolve2(expanded);
21918
+ if (!existsSync3(resolved)) {
21823
21919
  throw new Error(`File not found: ${resolved}`);
21824
21920
  }
21825
21921
  if (extname(resolved).toLowerCase() !== ".numbers") {
@@ -21829,22 +21925,33 @@ var NumbersManager = class {
21829
21925
  }
21830
21926
  /**
21831
21927
  * Resolve and validate an output path for new files.
21832
- * Expands ~ and checks .numbers extension, but does NOT require the file to exist.
21928
+ * Expands ~, enforces the allowed-roots boundary, and checks the .numbers
21929
+ * extension, but does NOT require the file to exist. The extension is checked
21930
+ * on the CANONICAL path so a symlink can't present a .numbers name for a
21931
+ * target that is something else entirely.
21833
21932
  */
21834
21933
  validateOutputPath(filePath) {
21835
- const expanded = filePath.startsWith("~") ? filePath.replace(/^~/, homedir()) : filePath;
21836
- const resolved = resolve(expanded);
21934
+ const resolved = resolveWithinAllowedRoots(filePath, "Output path");
21837
21935
  if (extname(resolved).toLowerCase() !== ".numbers") {
21838
21936
  throw new Error(`Not a Numbers file path: ${resolved}. Expected .numbers extension.`);
21839
21937
  }
21840
21938
  return resolved;
21841
21939
  }
21842
21940
  /**
21843
- * Resolve a generic path (expand ~ and resolve).
21941
+ * Resolve a path that will be WRITTEN (export destinations), enforcing the
21942
+ * allowed-roots boundary. Symlinks are resolved before the check, so a link
21943
+ * inside an allowed directory cannot escape to a system location.
21844
21944
  */
21845
- resolvePath(filePath) {
21846
- const expanded = filePath.startsWith("~") ? filePath.replace(/^~/, homedir()) : filePath;
21847
- return resolve(expanded);
21945
+ resolveWritePath(filePath) {
21946
+ return resolveWithinAllowedRoots(filePath, "Output path");
21947
+ }
21948
+ /**
21949
+ * Resolve a path that will be READ (import sources), enforcing the same
21950
+ * allowed-roots boundary so this server can't be turned into an arbitrary
21951
+ * local-file reader.
21952
+ */
21953
+ resolveReadPath(filePath) {
21954
+ return resolveWithinAllowedRoots(filePath, "Input path");
21848
21955
  }
21849
21956
  /**
21850
21957
  * Get file structure: sheets, tables, dimensions, headers.
@@ -21886,7 +21993,7 @@ var NumbersManager = class {
21886
21993
  */
21887
21994
  exportTable(filePath, format, outputPath, sheet, table) {
21888
21995
  const resolved = this.validatePath(filePath);
21889
- const outputResolved = this.resolvePath(outputPath);
21996
+ const outputResolved = this.resolveWritePath(outputPath);
21890
21997
  const args = [resolved, format, outputResolved];
21891
21998
  if (sheet) args.push("--sheet", sheet);
21892
21999
  if (table) args.push("--table", table);
@@ -22000,8 +22107,8 @@ var NumbersManager = class {
22000
22107
  * Import a CSV/TSV/JSON file into a new .numbers file.
22001
22108
  */
22002
22109
  importFile(inputPath, outputPath, options) {
22003
- const inputResolved = this.resolvePath(inputPath);
22004
- if (!existsSync2(inputResolved)) {
22110
+ const inputResolved = this.resolveReadPath(inputPath);
22111
+ if (!existsSync3(inputResolved)) {
22005
22112
  throw new Error(`Input file not found: ${inputResolved}`);
22006
22113
  }
22007
22114
  const outputResolved = this.validateOutputPath(outputPath);
@@ -22139,7 +22246,7 @@ function withErrorHandling(handler, prefix) {
22139
22246
 
22140
22247
  // src/tools/doctor.ts
22141
22248
  import { execFileSync as execFileSync3 } from "node:child_process";
22142
- import { existsSync as existsSync3 } from "node:fs";
22249
+ import { existsSync as existsSync4 } from "node:fs";
22143
22250
  var NUMBERS_APP_PATHS = [
22144
22251
  "/Applications/Numbers.app",
22145
22252
  "/System/Applications/Numbers.app",
@@ -22148,7 +22255,7 @@ var NUMBERS_APP_PATHS = [
22148
22255
  ];
22149
22256
  var NUMBERS_BUNDLE_IDS = ["com.apple.Numbers", "com.apple.iWork.Numbers"];
22150
22257
  function findNumbersApp() {
22151
- const byPath = NUMBERS_APP_PATHS.find((p) => existsSync3(p));
22258
+ const byPath = NUMBERS_APP_PATHS.find((p) => existsSync4(p));
22152
22259
  if (byPath) return byPath;
22153
22260
  for (const id of NUMBERS_BUNDLE_IDS) {
22154
22261
  try {
@@ -22158,7 +22265,7 @@ function findNumbersApp() {
22158
22265
  { encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
22159
22266
  ).trim();
22160
22267
  const path = out.endsWith("/") ? out.slice(0, -1) : out;
22161
- if (path && existsSync3(path)) return path;
22268
+ if (path && existsSync4(path)) return path;
22162
22269
  } catch {
22163
22270
  }
22164
22271
  }
@@ -22335,18 +22442,18 @@ First use get-file-info and read-table to read the relevant table(s) so you know
22335
22442
  }
22336
22443
 
22337
22444
  // src/services/fileConfig.ts
22338
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
22339
- import { join as join2 } from "node:path";
22340
- import { homedir as homedir2 } from "node:os";
22445
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
22446
+ import { join as join3 } from "node:path";
22447
+ import { homedir as homedir3 } from "node:os";
22341
22448
  function fileConfigPath(env = process.env) {
22342
22449
  const override = env.APPLE_NUMBERS_MCP_CONFIG_FILE;
22343
22450
  if (override && override.trim()) return override.trim();
22344
- return join2(homedir2(), "Library", "Application Support", "apple-numbers-mcp", "config.json");
22451
+ return join3(homedir3(), "Library", "Application Support", "apple-numbers-mcp", "config.json");
22345
22452
  }
22346
22453
  function loadFileConfig(env = process.env, path = fileConfigPath(env)) {
22347
22454
  const applied = [];
22348
22455
  try {
22349
- if (!existsSync4(path)) return applied;
22456
+ if (!existsSync5(path)) return applied;
22350
22457
  const parsed = JSON.parse(readFileSync2(path, "utf8"));
22351
22458
  if (!parsed || typeof parsed !== "object") return applied;
22352
22459
  for (const [k, v] of Object.entries(parsed)) {
@@ -22476,8 +22583,8 @@ function withJsonSchema2020_12(transport) {
22476
22583
  // src/index.ts
22477
22584
  loadFileConfig();
22478
22585
  var __filename2 = fileURLToPath2(import.meta.url);
22479
- var __dirname2 = dirname2(__filename2);
22480
- var pkg = JSON.parse(readFileSync3(join3(__dirname2, "..", "package.json"), "utf-8"));
22586
+ var __dirname2 = dirname3(__filename2);
22587
+ var pkg = JSON.parse(readFileSync3(join4(__dirname2, "..", "package.json"), "utf-8"));
22481
22588
  var version2 = pkg.version;
22482
22589
  var manager = new NumbersManager();
22483
22590
  var MAX_INDEX = 1e6;
@@ -22661,7 +22768,7 @@ ${lines.join("\n")}`, {
22661
22768
  registerTool(
22662
22769
  "export-table",
22663
22770
  {
22664
- description: "Use when: you want to write a table's data out to a CSV, TSV, or JSON file on disk. Defaults to the first sheet and first table if sheet/table are omitted.\nReturns: the exported row count, format, and output path.\nDo not use when: you want the data inline in the response rather than a file \u2014 use read-table; or you want to build a new .numbers file from a CSV/TSV/JSON source \u2014 use import-csv.\nSafety: writes a file to disk at outputPath. The path is written unconditionally \u2014 any existing file there is OVERWRITTEN \u2014 so confirm the destination before calling.",
22771
+ description: "Use when: you want to write a table's data out to a CSV, TSV, or JSON file on disk. Defaults to the first sheet and first table if sheet/table are omitted.\nReturns: the exported row count, format, and output path.\nDo not use when: you want the data inline in the response rather than a file \u2014 use read-table; or you want to build a new .numbers file from a CSV/TSV/JSON source \u2014 use import-csv.\nSafety: writes a file to disk at outputPath. The path is written unconditionally \u2014 any existing file there is OVERWRITTEN \u2014 so confirm the destination before calling. outputPath must resolve \u2014 after ~ expansion and symlink resolution \u2014 to a path under your home directory, /tmp, /private/tmp, or /Volumes; anything else is rejected with an error naming those roots.",
22665
22772
  inputSchema: {
22666
22773
  path: external_exports.string().describe("Path to the .numbers file"),
22667
22774
  format: external_exports.enum(["csv", "tsv", "json"]).describe("Output format"),
@@ -22725,7 +22832,7 @@ Merged: yes`;
22725
22832
  registerTool(
22726
22833
  "create-spreadsheet",
22727
22834
  {
22728
- description: "Use when: you want to create a brand-new .numbers file with a single sheet and table from a list of headers, optionally with initial data rows.\nReturns: the file path, sheet name, table name, header count, and data-row count.\nDo not use when: you want to build the file from an existing CSV/TSV/JSON source \u2014 use import-csv; or you want to add to a file that already exists \u2014 use add-sheet / add-table / add-rows.\nSafety: writes a .numbers file via the numbers-parser sidecar (does not require Numbers.app). The target path is written unconditionally \u2014 if a file already exists at that path it is OVERWRITTEN in place; choose a fresh path or confirm overwrite first.",
22835
+ description: "Use when: you want to create a brand-new .numbers file with a single sheet and table from a list of headers, optionally with initial data rows.\nReturns: the file path, sheet name, table name, header count, and data-row count.\nDo not use when: you want to build the file from an existing CSV/TSV/JSON source \u2014 use import-csv; or you want to add to a file that already exists \u2014 use add-sheet / add-table / add-rows.\nSafety: writes a .numbers file via the numbers-parser sidecar (does not require Numbers.app). The target path is written unconditionally \u2014 if a file already exists at that path it is OVERWRITTEN in place; choose a fresh path or confirm overwrite first. path must resolve \u2014 after ~ expansion and symlink resolution \u2014 to a location under your home directory, /tmp, /private/tmp, or /Volumes; anything else is rejected with an error naming those roots.",
22729
22836
  inputSchema: {
22730
22837
  path: external_exports.string().describe("Path for the new .numbers file"),
22731
22838
  headers: external_exports.array(headerCellSchema).min(1).max(MAX_BATCH).describe("Column header names"),
@@ -22931,7 +23038,7 @@ registerTool(
22931
23038
  registerTool(
22932
23039
  "import-csv",
22933
23040
  {
22934
- description: 'Use when: you want to convert an existing CSV, TSV, or JSON file into a new .numbers spreadsheet. Format is auto-detected from the input file extension unless you specify it explicitly; any unrecognized extension falls back to CSV. CSV/TSV fields are auto-typed (numbers, booleans, empty cells), so zero-padded identifiers LOSE their leading zeros \u2014 "01234" becomes 1234. JSON values are passed through untouched, but an array of objects takes its column set from the FIRST object only, silently dropping keys that appear later.\nReturns: the imported row and column counts, detected format, and the input/output paths plus sheet/table names.\nDo not use when: building a file from headers/rows you already have in hand \u2014 use create-spreadsheet; or exporting a .numbers table out to CSV/TSV/JSON \u2014 use export-table.\nSafety: writes the output .numbers file via the numbers-parser sidecar (does not require Numbers.app). The output path is written unconditionally \u2014 if a file already exists there it is OVERWRITTEN in place; choose a fresh output path or confirm overwrite first.',
23041
+ description: 'Use when: you want to convert an existing CSV, TSV, or JSON file into a new .numbers spreadsheet. Format is auto-detected from the input file extension unless you specify it explicitly; any unrecognized extension falls back to CSV. CSV/TSV fields are auto-typed (numbers, booleans, empty cells), so zero-padded identifiers LOSE their leading zeros \u2014 "01234" becomes 1234. JSON values are passed through untouched, but an array of objects takes its column set from the FIRST object only, silently dropping keys that appear later.\nReturns: the imported row and column counts, detected format, and the input/output paths plus sheet/table names.\nDo not use when: building a file from headers/rows you already have in hand \u2014 use create-spreadsheet; or exporting a .numbers table out to CSV/TSV/JSON \u2014 use export-table.\nSafety: writes the output .numbers file via the numbers-parser sidecar (does not require Numbers.app). The output path is written unconditionally \u2014 if a file already exists there it is OVERWRITTEN in place; choose a fresh output path or confirm overwrite first. Both inputPath and outputPath must resolve \u2014 after ~ expansion and symlink resolution \u2014 to locations under your home directory, /tmp, /private/tmp, or /Volumes; anything else is rejected with an error naming those roots.',
22935
23042
  inputSchema: {
22936
23043
  inputPath: external_exports.string().describe("Path to the CSV/TSV/JSON input file"),
22937
23044
  outputPath: external_exports.string().describe("Path for the output .numbers file"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-numbers-mcp",
3
- "version": "1.1.18",
3
+ "version": "1.1.19",
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",