apple-numbers-mcp 1.2.11 → 1.3.1

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 +6 -3
  2. package/build/index.js +139 -38
  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. 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.
306
+ **⚠️ Safety:** Writes a file to disk at `outputPath`. An existing file there is **refused** unless you pass `overwrite: true` — confirm with the user before passing it. 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
  |-----------|------|----------|-------------|
@@ -312,6 +312,7 @@ Export a table to CSV, TSV, or JSON.
312
312
  | `outputPath` | string | Yes | Path for the output file |
313
313
  | `sheet` | string | No | Sheet name (default: first sheet) |
314
314
  | `table` | string | No | Table name (default: first table) |
315
+ | `overwrite` | boolean | No | Replace an existing file at the destination (default: `false` — refused) |
315
316
 
316
317
  ---
317
318
 
@@ -321,7 +322,7 @@ Export a table to CSV, TSV, or JSON.
321
322
 
322
323
  Create a new `.numbers` file with one sheet and table.
323
324
 
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
+ **⚠️ Safety:** An existing file at `path` is **refused** unless you pass `overwrite: true` — confirm with the user before passing it. 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
326
 
326
327
  | Parameter | Type | Required | Description |
327
328
  |-----------|------|----------|-------------|
@@ -330,6 +331,7 @@ Create a new `.numbers` file with one sheet and table.
330
331
  | `rows` | (string \| number \| boolean \| null)[][] | No | Initial data rows |
331
332
  | `sheetName` | string | No | Default: `"Sheet 1"` |
332
333
  | `tableName` | string | No | Default: `"Table 1"` |
334
+ | `overwrite` | boolean | No | Replace an existing file at the destination (default: `false` — refused) |
333
335
 
334
336
  ---
335
337
 
@@ -573,7 +575,7 @@ Merge a rectangular range of cells, or undo a merge.
573
575
 
574
576
  Import a CSV, TSV, or JSON file into a new `.numbers` spreadsheet. Auto-detects format from extension, or pass `format` explicitly.
575
577
 
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.
578
+ **⚠️ Safety:** An existing file at `outputPath` is **refused** unless you pass `overwrite: true` — confirm with the user before passing it. 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
579
 
578
580
  | Parameter | Type | Required | Description |
579
581
  |-----------|------|----------|-------------|
@@ -582,6 +584,7 @@ Import a CSV, TSV, or JSON file into a new `.numbers` spreadsheet. Auto-detects
582
584
  | `format` | `"auto" \| "csv" \| "tsv" \| "json"` | No | Default: auto-detect from extension |
583
585
  | `sheetName` | string | No | Default: `"Sheet 1"` |
584
586
  | `tableName` | string | No | Default: `"Table 1"` |
587
+ | `overwrite` | boolean | No | Replace an existing file at the destination (default: `false` — refused) |
585
588
 
586
589
  **CSV/TSV fields are auto-typed.** Every field from a `csv`/`tsv` source is converted before it is written: `""` → empty cell, `true`/`false` → boolean, and anything Python's `int()`/`float()` parses → number. So a zip code `01234` becomes the number `1234`, `007` becomes `7`, and `1e5` becomes the number `100000`. This is deliberately less conservative than `set-cell`/`add-rows`, which keep leading-zero strings intact, and there is no per-column type control on import. For zero-padded identifiers, import from JSON instead (JSON values are passed through untouched) or repair the column afterwards with `set-cells-batch` using `type: "string"`.
587
590
 
package/build/index.js CHANGED
@@ -3104,9 +3104,9 @@ var require_data = __commonJS({
3104
3104
  }
3105
3105
  });
3106
3106
 
3107
- // node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/lib/utils.js
3107
+ // node_modules/.pnpm/fast-uri@3.1.9/node_modules/fast-uri/lib/utils.js
3108
3108
  var require_utils = __commonJS({
3109
- "node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/lib/utils.js"(exports, module) {
3109
+ "node_modules/.pnpm/fast-uri@3.1.9/node_modules/fast-uri/lib/utils.js"(exports, module) {
3110
3110
  "use strict";
3111
3111
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
3112
3112
  var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
@@ -3338,6 +3338,37 @@ var require_utils = __commonJS({
3338
3338
  }
3339
3339
  return output2.join("");
3340
3340
  }
3341
+ function removeDotSegmentsRelative(path) {
3342
+ if (path === "" || path[0] === "/") {
3343
+ return removeDotSegments(path);
3344
+ }
3345
+ const segments = path.split("/");
3346
+ const last = segments.length - 1;
3347
+ const output2 = [];
3348
+ let trailingDirectory = false;
3349
+ for (let i = 0; i <= last; i++) {
3350
+ const segment = segments[i];
3351
+ if (segment === "." || segment === "..") {
3352
+ if (segment === "..") {
3353
+ if (output2.length !== 0 && output2[output2.length - 1] !== "..") {
3354
+ output2.pop();
3355
+ } else {
3356
+ output2.push("..");
3357
+ }
3358
+ }
3359
+ trailingDirectory = i === last;
3360
+ } else {
3361
+ output2.push(segment);
3362
+ }
3363
+ }
3364
+ if (trailingDirectory && output2[output2.length - 1] !== "..") {
3365
+ output2.push("");
3366
+ }
3367
+ if (output2.length === 0 || output2[0] === "" || output2[0].includes(":")) {
3368
+ output2.unshift(".");
3369
+ }
3370
+ return output2.join("/");
3371
+ }
3341
3372
  var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
3342
3373
  var HOST_DELIM_RE = /[@/?#:]/g;
3343
3374
  var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
@@ -3598,6 +3629,7 @@ var require_utils = __commonJS({
3598
3629
  encodeFragment,
3599
3630
  escapePreservingEscapes,
3600
3631
  removeDotSegments,
3632
+ removeDotSegmentsRelative,
3601
3633
  isIPv4,
3602
3634
  isUUID,
3603
3635
  normalizeIPv6,
@@ -3606,9 +3638,9 @@ var require_utils = __commonJS({
3606
3638
  }
3607
3639
  });
3608
3640
 
3609
- // node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/lib/schemes.js
3641
+ // node_modules/.pnpm/fast-uri@3.1.9/node_modules/fast-uri/lib/schemes.js
3610
3642
  var require_schemes = __commonJS({
3611
- "node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/lib/schemes.js"(exports, module) {
3643
+ "node_modules/.pnpm/fast-uri@3.1.9/node_modules/fast-uri/lib/schemes.js"(exports, module) {
3612
3644
  "use strict";
3613
3645
  var { isUUID } = require_utils();
3614
3646
  var URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu;
@@ -3817,20 +3849,22 @@ var require_schemes = __commonJS({
3817
3849
  }
3818
3850
  });
3819
3851
 
3820
- // node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/index.js
3852
+ // node_modules/.pnpm/fast-uri@3.1.9/node_modules/fast-uri/index.js
3821
3853
  var require_fast_uri = __commonJS({
3822
- "node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/index.js"(exports, module) {
3854
+ "node_modules/.pnpm/fast-uri@3.1.9/node_modules/fast-uri/index.js"(exports, module) {
3823
3855
  "use strict";
3824
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3856
+ var { normalizeIPv6, removeDotSegments, removeDotSegmentsRelative, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3825
3857
  var { SCHEMES, getSchemeHandler } = require_schemes();
3826
3858
  var VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
3827
3859
  var MALFORMED_SCHEME_ERROR = "URI scheme is malformed.";
3828
- function decodeValidScheme(scheme) {
3829
- const decodedScheme = unescape(String(scheme));
3830
- if (!VALID_SCHEME.test(decodedScheme)) {
3860
+ var MALFORMED_HOST_ERROR = "URI host is malformed.";
3861
+ var FORBIDDEN_DOMAIN_HOST_CHARACTER = /[\0-\x20"#/:<>?@[\\\]^`{|}\x7F]/u;
3862
+ function validateScheme(scheme) {
3863
+ const stringScheme = String(scheme);
3864
+ if (!VALID_SCHEME.test(stringScheme)) {
3831
3865
  throw new TypeError(MALFORMED_SCHEME_ERROR);
3832
3866
  }
3833
- return decodedScheme;
3867
+ return stringScheme;
3834
3868
  }
3835
3869
  function normalize(uri, options) {
3836
3870
  if (typeof uri === "string") {
@@ -3867,6 +3901,9 @@ var require_fast_uri = __commonJS({
3867
3901
  const resolvedSchemeHandler = getSchemeHandler(options && options.scheme || resolved.scheme);
3868
3902
  const resolvedHost = resolved.host;
3869
3903
  const resolvedHostIsIP = resolvedHost !== void 0 && resolvedHost !== "" && (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6);
3904
+ if (hasForbiddenDomainHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)) {
3905
+ throw new Error(MALFORMED_HOST_ERROR);
3906
+ }
3870
3907
  canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP);
3871
3908
  const encodedASCIIHost = resolvedHost && resolvedHost.indexOf("%") !== -1 && !new RegExp("\\P{ASCII}", "u").test(resolvedHost);
3872
3909
  if (resolved.error && !encodedASCIIHost) {
@@ -3908,14 +3945,15 @@ var require_fast_uri = __commonJS({
3908
3945
  if (relative.path[0] === "/") {
3909
3946
  target.path = removeDotSegments(relative.path);
3910
3947
  } else {
3911
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
3948
+ const baseHasAuthority = base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0;
3949
+ if (baseHasAuthority && !base.path) {
3912
3950
  target.path = "/" + relative.path;
3913
3951
  } else if (!base.path) {
3914
3952
  target.path = relative.path;
3915
3953
  } else {
3916
3954
  target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
3917
3955
  }
3918
- target.path = removeDotSegments(target.path);
3956
+ target.path = base.scheme === void 0 && !baseHasAuthority && target.path[0] !== "/" ? removeDotSegmentsRelative(target.path) : removeDotSegments(target.path);
3919
3957
  }
3920
3958
  target.query = relative.query;
3921
3959
  }
@@ -3953,12 +3991,13 @@ var require_fast_uri = __commonJS({
3953
3991
  const options = Object.assign({}, opts);
3954
3992
  const uriTokens = [];
3955
3993
  if (component.scheme) {
3956
- component.scheme = decodeValidScheme(component.scheme);
3994
+ component.scheme = validateScheme(component.scheme);
3957
3995
  }
3958
3996
  const schemeHandler = getSchemeHandler(options.scheme || component.scheme);
3959
3997
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
3960
3998
  const hasAuthority = component.userinfo !== void 0 || component.host !== void 0 || component.port !== void 0;
3961
- const pathNoScheme = !options.skipEscape && component.scheme === void 0 && !hasAuthority;
3999
+ const schemelessReference = (component.scheme === void 0 || options.reference === "suffix") && !hasAuthority;
4000
+ const pathNoScheme = !options.skipEscape && schemelessReference;
3962
4001
  if (component.path !== void 0) {
3963
4002
  if (!options.skipEscape) {
3964
4003
  component.path = serializePathEncoding(component.path, pathNoScheme);
@@ -3967,7 +4006,7 @@ var require_fast_uri = __commonJS({
3967
4006
  }
3968
4007
  }
3969
4008
  if (options.reference !== "suffix" && component.scheme) {
3970
- component.scheme = decodeValidScheme(component.scheme);
4009
+ component.scheme = validateScheme(component.scheme);
3971
4010
  uriTokens.push(component.scheme, ":");
3972
4011
  }
3973
4012
  const authority = recomposeAuthority(component);
@@ -3983,9 +4022,9 @@ var require_fast_uri = __commonJS({
3983
4022
  if (component.path !== void 0) {
3984
4023
  let s = component.path;
3985
4024
  if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) {
3986
- s = removeDotSegments(s);
4025
+ s = schemelessReference && s[0] !== "/" ? removeDotSegmentsRelative(s) : removeDotSegments(s);
3987
4026
  }
3988
- if (pathNoScheme) {
4027
+ if (schemelessReference) {
3989
4028
  s = serializePathEncoding(s, true);
3990
4029
  }
3991
4030
  if (authority === void 0 && s[0] === "/" && s[1] === "/") {
@@ -4032,16 +4071,34 @@ var require_fast_uri = __commonJS({
4032
4071
  return hasMalformedPercentEncoding(matches[3]) || host !== void 0 && !isIPLiteral(host) && hasMalformedPercentEncoding(host) || hasMalformedPercentEncoding(matches[6]) || hasMalformedPercentEncoding(matches[7]) || hasMalformedPercentEncoding(matches[8]);
4033
4072
  }
4034
4073
  function canonicalizeHost(parsed, options, schemeHandler, isIP) {
4074
+ if (hasForbiddenDomainHost(parsed, options, schemeHandler, isIP)) {
4075
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
4076
+ return true;
4077
+ }
4035
4078
  if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
4079
+ let url2;
4036
4080
  try {
4037
- parsed.host = new URL("http://" + parsed.host).hostname;
4081
+ url2 = new URL("http://" + parsed.host);
4038
4082
  } catch (e) {
4039
4083
  parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
4040
4084
  return true;
4041
4085
  }
4086
+ if (!isHostOnlyURL(url2)) {
4087
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
4088
+ return true;
4089
+ }
4090
+ parsed.host = url2.hostname;
4042
4091
  }
4043
4092
  return false;
4044
4093
  }
4094
+ function hasForbiddenDomainHost(parsed, options, schemeHandler, isIP) {
4095
+ return Boolean(
4096
+ parsed.host && isIP === false && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && FORBIDDEN_DOMAIN_HOST_CHARACTER.test(parsed.host)
4097
+ );
4098
+ }
4099
+ function isHostOnlyURL(url2) {
4100
+ return url2.username === "" && url2.password === "" && url2.port === "" && url2.pathname === "/" && url2.search === "" && url2.hash === "" && url2.hostname !== "";
4101
+ }
4045
4102
  function parseWithStatus(uri, opts) {
4046
4103
  const options = Object.assign({}, opts);
4047
4104
  const parsed = {
@@ -4096,9 +4153,8 @@ var require_fast_uri = __commonJS({
4096
4153
  parsed.query = matches[7];
4097
4154
  parsed.fragment = matches[8];
4098
4155
  if (parsed.scheme !== void 0) {
4099
- const decodedScheme = unescape(parsed.scheme);
4100
- if (VALID_SCHEME.test(decodedScheme)) {
4101
- parsed.scheme = decodedScheme.toLowerCase();
4156
+ if (VALID_SCHEME.test(parsed.scheme)) {
4157
+ parsed.scheme = parsed.scheme.toLowerCase();
4102
4158
  } else {
4103
4159
  parsed.error = parsed.error || MALFORMED_SCHEME_ERROR;
4104
4160
  malformedScheme = true;
@@ -4126,7 +4182,7 @@ var require_fast_uri = __commonJS({
4126
4182
  malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
4127
4183
  parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
4128
4184
  if (malformedIPLiteral) {
4129
- parsed.error = parsed.error || "URI host is malformed.";
4185
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
4130
4186
  malformedAuthorityOrPort = true;
4131
4187
  }
4132
4188
  } else {
@@ -4154,7 +4210,7 @@ var require_fast_uri = __commonJS({
4154
4210
  if (!isIP) {
4155
4211
  host = normalizePercentEncoding(host.toLowerCase());
4156
4212
  }
4157
- parsed.host = reescapeHostDelimiters(host, isIP);
4213
+ parsed.host = isIP || options.scheme !== "null" ? reescapeHostDelimiters(host, isIP) : host;
4158
4214
  }
4159
4215
  if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4160
4216
  if (parsed.path) {
@@ -37252,7 +37308,7 @@ function unmergeCells(filePath, sheet, table, startRow, startCol, endRow, endCol
37252
37308
  }
37253
37309
 
37254
37310
  // src/services/numbersManager.ts
37255
- import { existsSync as existsSync2 } from "node:fs";
37311
+ import { existsSync as existsSync2, lstatSync as lstatSync2 } from "node:fs";
37256
37312
  import { extname } from "node:path";
37257
37313
 
37258
37314
  // src/utils/exportPath.ts
@@ -37421,6 +37477,29 @@ var NumbersManager = class {
37421
37477
  resolveReadPath(filePath) {
37422
37478
  return resolveWithinAllowedRoots(filePath, "Input path");
37423
37479
  }
37480
+ /**
37481
+ * Refuse to replace an existing file unless the caller opted in.
37482
+ *
37483
+ * `export-table`, `create-spreadsheet` and `import-csv` used to overwrite
37484
+ * whatever was at the destination, so a reused or mistyped path silently
37485
+ * destroyed a real file — for the last two, a real spreadsheet. The roots
37486
+ * boundary limits WHERE they write; this limits WHAT they replace. Checked on
37487
+ * the resolved path with `lstat`, so a dangling symlink also counts as taken.
37488
+ */
37489
+ assertWritable(resolved, overwrite) {
37490
+ if (overwrite) return;
37491
+ let taken = false;
37492
+ try {
37493
+ lstatSync2(resolved);
37494
+ taken = true;
37495
+ } catch {
37496
+ }
37497
+ if (taken) {
37498
+ throw new Error(
37499
+ `Output file already exists: ${resolved}. Pass overwrite: true to replace it, or choose a new path.`
37500
+ );
37501
+ }
37502
+ }
37424
37503
  /**
37425
37504
  * Get file structure: sheets, tables, dimensions, headers.
37426
37505
  */
@@ -37459,9 +37538,10 @@ var NumbersManager = class {
37459
37538
  /**
37460
37539
  * Export a table to CSV, TSV, or JSON file.
37461
37540
  */
37462
- exportTable(filePath, format, outputPath, sheet, table) {
37541
+ exportTable(filePath, format, outputPath, sheet, table, overwrite) {
37463
37542
  const resolved = this.validatePath(filePath);
37464
37543
  const outputResolved = this.resolveWritePath(outputPath);
37544
+ this.assertWritable(outputResolved, overwrite);
37465
37545
  const args = [resolved, format, outputResolved];
37466
37546
  if (sheet) args.push("--sheet", sheet);
37467
37547
  if (table) args.push("--table", table);
@@ -37485,6 +37565,7 @@ var NumbersManager = class {
37485
37565
  */
37486
37566
  createSpreadsheet(filePath, headers, options) {
37487
37567
  const resolved = this.validateOutputPath(filePath);
37568
+ this.assertWritable(resolved, options?.overwrite);
37488
37569
  const args = [resolved, JSON.stringify(headers)];
37489
37570
  if (options?.sheetName) args.push("--sheet-name", options.sheetName);
37490
37571
  if (options?.tableName) args.push("--table-name", options.tableName);
@@ -37580,6 +37661,7 @@ var NumbersManager = class {
37580
37661
  throw new Error(`Input file not found: ${inputResolved}`);
37581
37662
  }
37582
37663
  const outputResolved = this.validateOutputPath(outputPath);
37664
+ this.assertWritable(outputResolved, options?.overwrite);
37583
37665
  const args = [inputResolved, outputResolved];
37584
37666
  if (options?.format && options.format !== "auto") args.push("--format", options.format);
37585
37667
  if (options?.sheetName) args.push("--sheet-name", options.sheetName);
@@ -38273,13 +38355,16 @@ ${lines.join("\n")}`, {
38273
38355
  registerTool(
38274
38356
  "export-table",
38275
38357
  {
38276
- 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.",
38358
+ 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. An existing file at outputPath is refused unless overwrite is true; confirm with the user before passing it. 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.",
38277
38359
  inputSchema: {
38278
38360
  path: external_exports.string().describe("Path to the .numbers file"),
38279
38361
  format: external_exports.enum(["csv", "tsv", "json"]).describe("Output format"),
38280
38362
  outputPath: external_exports.string().describe("Path for the output file"),
38281
38363
  sheet: external_exports.string().optional().describe("Sheet name (default: first sheet)"),
38282
- table: external_exports.string().optional().describe("Table name (default: first table)")
38364
+ table: external_exports.string().optional().describe("Table name (default: first table)"),
38365
+ overwrite: external_exports.boolean().optional().describe(
38366
+ "Replace an existing file at the destination (default: false \u2014 an existing file is refused)"
38367
+ )
38283
38368
  },
38284
38369
  outputSchema: {
38285
38370
  outputPath: external_exports.string().optional(),
@@ -38289,8 +38374,8 @@ registerTool(
38289
38374
  tableName: external_exports.string().optional()
38290
38375
  }
38291
38376
  },
38292
- withErrorHandling(({ path, format, outputPath, sheet, table }) => {
38293
- const result = manager.exportTable(path, format, outputPath, sheet, table);
38377
+ withErrorHandling(({ path, format, outputPath, sheet, table, overwrite }) => {
38378
+ const result = manager.exportTable(path, format, outputPath, sheet, table, overwrite);
38294
38379
  return successResponse(
38295
38380
  `Exported ${result.rowCount} rows from "${result.sheetName}/${result.tableName}" to ${result.format.toUpperCase()}: ${result.outputPath}`,
38296
38381
  { ...result }
@@ -38337,13 +38422,16 @@ Merged: yes`;
38337
38422
  registerTool(
38338
38423
  "create-spreadsheet",
38339
38424
  {
38340
- 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.",
38425
+ 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). An existing file at path is refused unless overwrite is true; confirm with the user before passing it. 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.",
38341
38426
  inputSchema: {
38342
38427
  path: external_exports.string().describe("Path for the new .numbers file"),
38343
38428
  headers: external_exports.array(headerCellSchema).min(1).max(MAX_BATCH).describe("Column header names"),
38344
38429
  rows: external_exports.array(external_exports.array(external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()]))).max(MAX_BATCH).optional().describe("Optional data rows (array of arrays)"),
38345
38430
  sheetName: external_exports.string().max(MAX_NAME_LEN).optional().describe("Sheet name (default: 'Sheet 1')"),
38346
- tableName: external_exports.string().max(MAX_NAME_LEN).optional().describe("Table name (default: 'Table 1')")
38431
+ tableName: external_exports.string().max(MAX_NAME_LEN).optional().describe("Table name (default: 'Table 1')"),
38432
+ overwrite: external_exports.boolean().optional().describe(
38433
+ "Replace an existing file at the destination (default: false \u2014 an existing file is refused)"
38434
+ )
38347
38435
  },
38348
38436
  outputSchema: {
38349
38437
  path: external_exports.string().optional(),
@@ -38353,8 +38441,13 @@ registerTool(
38353
38441
  numRows: external_exports.number().optional()
38354
38442
  }
38355
38443
  },
38356
- withErrorHandling(({ path, headers, rows, sheetName, tableName }) => {
38357
- const result = manager.createSpreadsheet(path, headers, { sheetName, tableName, rows });
38444
+ withErrorHandling(({ path, headers, rows, sheetName, tableName, overwrite }) => {
38445
+ const result = manager.createSpreadsheet(path, headers, {
38446
+ sheetName,
38447
+ tableName,
38448
+ rows,
38449
+ overwrite
38450
+ });
38358
38451
  return successResponse(
38359
38452
  `Created ${result.path}
38360
38453
  Sheet: "${result.sheetName}" | Table: "${result.tableName}"
@@ -38543,13 +38636,16 @@ registerTool(
38543
38636
  registerTool(
38544
38637
  "import-csv",
38545
38638
  {
38546
- 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.',
38639
+ 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). An existing file at outputPath is refused unless overwrite is true; confirm with the user before passing it. 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.',
38547
38640
  inputSchema: {
38548
38641
  inputPath: external_exports.string().describe("Path to the CSV/TSV/JSON input file"),
38549
38642
  outputPath: external_exports.string().describe("Path for the output .numbers file"),
38550
38643
  format: external_exports.enum(["auto", "csv", "tsv", "json"]).optional().describe("Input format (default: auto-detect from extension)"),
38551
38644
  sheetName: external_exports.string().max(MAX_NAME_LEN).optional().describe("Sheet name (default: 'Sheet 1')"),
38552
- tableName: external_exports.string().max(MAX_NAME_LEN).optional().describe("Table name (default: 'Table 1')")
38645
+ tableName: external_exports.string().max(MAX_NAME_LEN).optional().describe("Table name (default: 'Table 1')"),
38646
+ overwrite: external_exports.boolean().optional().describe(
38647
+ "Replace an existing file at the destination (default: false \u2014 an existing file is refused)"
38648
+ )
38553
38649
  },
38554
38650
  outputSchema: {
38555
38651
  path: external_exports.string().optional(),
@@ -38561,8 +38657,13 @@ registerTool(
38561
38657
  numRows: external_exports.number().optional()
38562
38658
  }
38563
38659
  },
38564
- withErrorHandling(({ inputPath, outputPath, format, sheetName, tableName }) => {
38565
- const result = manager.importFile(inputPath, outputPath, { format, sheetName, tableName });
38660
+ withErrorHandling(({ inputPath, outputPath, format, sheetName, tableName, overwrite }) => {
38661
+ const result = manager.importFile(inputPath, outputPath, {
38662
+ format,
38663
+ sheetName,
38664
+ tableName,
38665
+ overwrite
38666
+ });
38566
38667
  return successResponse(
38567
38668
  `Imported ${result.numRows} rows (${result.numHeaders} columns) from ${result.format.toUpperCase()}
38568
38669
  Input: ${result.inputPath}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-numbers-mcp",
3
- "version": "1.2.11",
3
+ "version": "1.3.1",
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",