apple-numbers-mcp 1.2.11 → 1.3.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
@@ -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
@@ -37252,7 +37252,7 @@ function unmergeCells(filePath, sheet, table, startRow, startCol, endRow, endCol
37252
37252
  }
37253
37253
 
37254
37254
  // src/services/numbersManager.ts
37255
- import { existsSync as existsSync2 } from "node:fs";
37255
+ import { existsSync as existsSync2, lstatSync as lstatSync2 } from "node:fs";
37256
37256
  import { extname } from "node:path";
37257
37257
 
37258
37258
  // src/utils/exportPath.ts
@@ -37421,6 +37421,29 @@ var NumbersManager = class {
37421
37421
  resolveReadPath(filePath) {
37422
37422
  return resolveWithinAllowedRoots(filePath, "Input path");
37423
37423
  }
37424
+ /**
37425
+ * Refuse to replace an existing file unless the caller opted in.
37426
+ *
37427
+ * `export-table`, `create-spreadsheet` and `import-csv` used to overwrite
37428
+ * whatever was at the destination, so a reused or mistyped path silently
37429
+ * destroyed a real file — for the last two, a real spreadsheet. The roots
37430
+ * boundary limits WHERE they write; this limits WHAT they replace. Checked on
37431
+ * the resolved path with `lstat`, so a dangling symlink also counts as taken.
37432
+ */
37433
+ assertWritable(resolved, overwrite) {
37434
+ if (overwrite) return;
37435
+ let taken = false;
37436
+ try {
37437
+ lstatSync2(resolved);
37438
+ taken = true;
37439
+ } catch {
37440
+ }
37441
+ if (taken) {
37442
+ throw new Error(
37443
+ `Output file already exists: ${resolved}. Pass overwrite: true to replace it, or choose a new path.`
37444
+ );
37445
+ }
37446
+ }
37424
37447
  /**
37425
37448
  * Get file structure: sheets, tables, dimensions, headers.
37426
37449
  */
@@ -37459,9 +37482,10 @@ var NumbersManager = class {
37459
37482
  /**
37460
37483
  * Export a table to CSV, TSV, or JSON file.
37461
37484
  */
37462
- exportTable(filePath, format, outputPath, sheet, table) {
37485
+ exportTable(filePath, format, outputPath, sheet, table, overwrite) {
37463
37486
  const resolved = this.validatePath(filePath);
37464
37487
  const outputResolved = this.resolveWritePath(outputPath);
37488
+ this.assertWritable(outputResolved, overwrite);
37465
37489
  const args = [resolved, format, outputResolved];
37466
37490
  if (sheet) args.push("--sheet", sheet);
37467
37491
  if (table) args.push("--table", table);
@@ -37485,6 +37509,7 @@ var NumbersManager = class {
37485
37509
  */
37486
37510
  createSpreadsheet(filePath, headers, options) {
37487
37511
  const resolved = this.validateOutputPath(filePath);
37512
+ this.assertWritable(resolved, options?.overwrite);
37488
37513
  const args = [resolved, JSON.stringify(headers)];
37489
37514
  if (options?.sheetName) args.push("--sheet-name", options.sheetName);
37490
37515
  if (options?.tableName) args.push("--table-name", options.tableName);
@@ -37580,6 +37605,7 @@ var NumbersManager = class {
37580
37605
  throw new Error(`Input file not found: ${inputResolved}`);
37581
37606
  }
37582
37607
  const outputResolved = this.validateOutputPath(outputPath);
37608
+ this.assertWritable(outputResolved, options?.overwrite);
37583
37609
  const args = [inputResolved, outputResolved];
37584
37610
  if (options?.format && options.format !== "auto") args.push("--format", options.format);
37585
37611
  if (options?.sheetName) args.push("--sheet-name", options.sheetName);
@@ -38273,13 +38299,16 @@ ${lines.join("\n")}`, {
38273
38299
  registerTool(
38274
38300
  "export-table",
38275
38301
  {
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.",
38302
+ 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
38303
  inputSchema: {
38278
38304
  path: external_exports.string().describe("Path to the .numbers file"),
38279
38305
  format: external_exports.enum(["csv", "tsv", "json"]).describe("Output format"),
38280
38306
  outputPath: external_exports.string().describe("Path for the output file"),
38281
38307
  sheet: external_exports.string().optional().describe("Sheet name (default: first sheet)"),
38282
- table: external_exports.string().optional().describe("Table name (default: first table)")
38308
+ table: external_exports.string().optional().describe("Table name (default: first table)"),
38309
+ overwrite: external_exports.boolean().optional().describe(
38310
+ "Replace an existing file at the destination (default: false \u2014 an existing file is refused)"
38311
+ )
38283
38312
  },
38284
38313
  outputSchema: {
38285
38314
  outputPath: external_exports.string().optional(),
@@ -38289,8 +38318,8 @@ registerTool(
38289
38318
  tableName: external_exports.string().optional()
38290
38319
  }
38291
38320
  },
38292
- withErrorHandling(({ path, format, outputPath, sheet, table }) => {
38293
- const result = manager.exportTable(path, format, outputPath, sheet, table);
38321
+ withErrorHandling(({ path, format, outputPath, sheet, table, overwrite }) => {
38322
+ const result = manager.exportTable(path, format, outputPath, sheet, table, overwrite);
38294
38323
  return successResponse(
38295
38324
  `Exported ${result.rowCount} rows from "${result.sheetName}/${result.tableName}" to ${result.format.toUpperCase()}: ${result.outputPath}`,
38296
38325
  { ...result }
@@ -38337,13 +38366,16 @@ Merged: yes`;
38337
38366
  registerTool(
38338
38367
  "create-spreadsheet",
38339
38368
  {
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.",
38369
+ 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
38370
  inputSchema: {
38342
38371
  path: external_exports.string().describe("Path for the new .numbers file"),
38343
38372
  headers: external_exports.array(headerCellSchema).min(1).max(MAX_BATCH).describe("Column header names"),
38344
38373
  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
38374
  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')")
38375
+ tableName: external_exports.string().max(MAX_NAME_LEN).optional().describe("Table name (default: 'Table 1')"),
38376
+ overwrite: external_exports.boolean().optional().describe(
38377
+ "Replace an existing file at the destination (default: false \u2014 an existing file is refused)"
38378
+ )
38347
38379
  },
38348
38380
  outputSchema: {
38349
38381
  path: external_exports.string().optional(),
@@ -38353,8 +38385,13 @@ registerTool(
38353
38385
  numRows: external_exports.number().optional()
38354
38386
  }
38355
38387
  },
38356
- withErrorHandling(({ path, headers, rows, sheetName, tableName }) => {
38357
- const result = manager.createSpreadsheet(path, headers, { sheetName, tableName, rows });
38388
+ withErrorHandling(({ path, headers, rows, sheetName, tableName, overwrite }) => {
38389
+ const result = manager.createSpreadsheet(path, headers, {
38390
+ sheetName,
38391
+ tableName,
38392
+ rows,
38393
+ overwrite
38394
+ });
38358
38395
  return successResponse(
38359
38396
  `Created ${result.path}
38360
38397
  Sheet: "${result.sheetName}" | Table: "${result.tableName}"
@@ -38543,13 +38580,16 @@ registerTool(
38543
38580
  registerTool(
38544
38581
  "import-csv",
38545
38582
  {
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.',
38583
+ 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
38584
  inputSchema: {
38548
38585
  inputPath: external_exports.string().describe("Path to the CSV/TSV/JSON input file"),
38549
38586
  outputPath: external_exports.string().describe("Path for the output .numbers file"),
38550
38587
  format: external_exports.enum(["auto", "csv", "tsv", "json"]).optional().describe("Input format (default: auto-detect from extension)"),
38551
38588
  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')")
38589
+ tableName: external_exports.string().max(MAX_NAME_LEN).optional().describe("Table name (default: 'Table 1')"),
38590
+ overwrite: external_exports.boolean().optional().describe(
38591
+ "Replace an existing file at the destination (default: false \u2014 an existing file is refused)"
38592
+ )
38553
38593
  },
38554
38594
  outputSchema: {
38555
38595
  path: external_exports.string().optional(),
@@ -38561,8 +38601,13 @@ registerTool(
38561
38601
  numRows: external_exports.number().optional()
38562
38602
  }
38563
38603
  },
38564
- withErrorHandling(({ inputPath, outputPath, format, sheetName, tableName }) => {
38565
- const result = manager.importFile(inputPath, outputPath, { format, sheetName, tableName });
38604
+ withErrorHandling(({ inputPath, outputPath, format, sheetName, tableName, overwrite }) => {
38605
+ const result = manager.importFile(inputPath, outputPath, {
38606
+ format,
38607
+ sheetName,
38608
+ tableName,
38609
+ overwrite
38610
+ });
38566
38611
  return successResponse(
38567
38612
  `Imported ${result.numRows} rows (${result.numHeaders} columns) from ${result.format.toUpperCase()}
38568
38613
  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.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",