# Apple Numbers MCP Server A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that enables AI assistants like Claude to read, write, search, and modify Apple Numbers (`.numbers`) spreadsheet files. Backed by the [numbers-parser](https://pypi.org/project/numbers-parser/) Python library. [![npm version](https://img.shields.io/npm/v/apple-numbers-mcp)](https://www.npmjs.com/package/apple-numbers-mcp) [![npm downloads](https://img.shields.io/npm/dm/apple-numbers-mcp)](https://www.npmjs.com/package/apple-numbers-mcp) [![node](https://img.shields.io/node/v/apple-numbers-mcp)](https://www.npmjs.com/package/apple-numbers-mcp) [![CI](https://github.com/sweetrb/apple-numbers-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/sweetrb/apple-numbers-mcp/actions/workflows/ci.yml) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/sweetrb/apple-numbers-mcp/badge)](https://scorecard.dev/viewer/?uri=github.com/sweetrb/apple-numbers-mcp) [![platform: macOS](https://img.shields.io/badge/platform-macOS-111?logo=apple&logoColor=white)](https://www.apple.com/macos/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![MCP](https://img.shields.io/badge/MCP-server-blue)](https://modelcontextprotocol.io)

Apple Numbers MCP — read, write, search, and format spreadsheets from Codex, Claude, and other AI assistants

## What is This? This server acts as a bridge between AI assistants and Apple Numbers spreadsheets. Once configured, you can ask Claude (or any MCP-compatible AI) to: - "What's in this spreadsheet?" — inspect sheets, tables, dimensions - "Find every row where the customer is Acme Corp" - "Export the Q3 Results table to CSV" - "Set cell B5 to =SUM(B2:B4)" - "Append these three rows to the Inventory table" - "Bold and center the header row" - "Import this CSV into a new spreadsheet" The AI assistant communicates with this server, which uses [numbers-parser](https://pypi.org/project/numbers-parser/) to read and write `.numbers` files directly (no Numbers.app required for most operations). All data stays local on your machine. ## Quick Start ### Using Claude Code (Easiest) If you're using [Claude Code](https://claude.com/product/claude-code) (in Terminal or VS Code), just ask Claude to install it: ``` Install the sweetrb/apple-numbers-mcp MCP server so you can help me work with my Numbers spreadsheets ``` Claude will handle the installation and configuration automatically. After install, you'll need to install `numbers-parser` (Python) — see [Requirements](#requirements) below. Or register it yourself in one deterministic line (no clone needed): ```bash claude mcp add apple-numbers -s user -- npx -y apple-numbers-mcp ``` ### Using the Plugin Marketplace Install as a Claude Code plugin for automatic configuration and enhanced AI behavior: ```bash /plugin marketplace add sweetrb/apple-numbers-mcp /plugin install apple-numbers ``` This method also installs a **skill** that teaches Claude when and how to use Apple Numbers effectively. The plugin runs straight from its marketplace clone under `~/.claude/plugins/` (the clone lives at `~/.claude/plugins/marketplaces/apple-numbers-mcp/`). On the **first tool call** the server auto-bootstraps a project-local Python venv inside that directory with `numbers-parser` — a one-time setup that takes about a minute and **requires Python >= 3.11 on PATH** (stock macOS ships 3.9 — `brew install python@3.12` first). To pre-warm it instead of waiting on the first call, run `scripts/setup.sh` inside that install directory. ### Using the Codex Marketplace Install the same public marketplace in Codex: ```bash codex plugin marketplace add sweetrb/apple-numbers-mcp codex plugin add apple-numbers@apple-numbers-mcp ``` The Codex package registers the `apple-numbers` MCP server through `npx -y apple-numbers-mcp` — the same published-package invocation documented for Claude Desktop below — and bundles the Apple Numbers skill guidance. As with every install path, the `numbers-parser` Python sidecar must be available (`pip3 install numbers-parser`); see [Requirements](#requirements). ### Other Hosts (Hermes, Antigravity) Two more hosts can run the same `apple-numbers` MCP server (`npx -y apple-numbers-mcp`). All paths still need the `numbers-parser` Python sidecar available; see [Requirements](#requirements). - **[Hermes Agent](https://hermes-agent.nousresearch.com/)** (NousResearch) — Hermes has no plugin/marketplace drop-in, so there is nothing in this repo to install from. Register the server with the CLI: ```bash hermes mcp add apple-numbers --command npx --args -y apple-numbers-mcp ``` Or add it to `~/.hermes/config.yaml` by hand: ```yaml mcp_servers: apple-numbers: command: npx args: ["-y", "apple-numbers-mcp"] ``` Restart your Hermes session afterward so the tools load. - **[Antigravity](https://antigravity.google/)** (Google) — add the server entry from [`.antigravity-plugin/mcp_config.json`](https://github.com/sweetrb/apple-numbers-mcp/blob/main/.antigravity-plugin/mcp_config.json) to `~/.gemini/config/mcp_config.json` (or via Antigravity's MCP settings). ### Manual Installation **1. Install the server** (from the npm registry): ```bash npm install -g apple-numbers-mcp ``` **2. Install numbers-parser** (the Python library this server depends on): ```bash pip3 install numbers-parser ``` Or, if you cloned the repo, run `pnpm run setup` to create a project-local Python venv with `numbers-parser` pre-installed. **3. Add to Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`): ```json { "mcpServers": { "apple-numbers": { "command": "npx", "args": ["apple-numbers-mcp"] } } } ``` **4. Restart Claude Desktop** and start using natural language: ``` "What sheets are in ~/Documents/budget.numbers?" ``` #### Running from a clone in Claude Code (project-scope `.mcp.json`) This repo ships a `.mcp.json` at its root so that, when you run `claude` from inside a clone, the server is registered automatically as a **project-scope** server — no manual config needed. Before launching, run **both**: ```bash pnpm run build # compile TypeScript to build/ pnpm run setup # create the ./venv the Python sidecar needs ``` `pnpm run setup` is required because this is a Python-sidecar server: it shells out to `./venv/bin/python3` running `numbers-parser`. Without the venv, the server starts but every tool call fails. Then launch Claude Code from the repo directory and approve the server when prompted. The entrypoint is written as: ```text "args": ["${CLAUDE_PROJECT_DIR:-.}/build/index.js"] ``` `CLAUDE_PROJECT_DIR` is the variable Claude Code injects into a project/user-scoped server's environment, and it resolves to the repo root. **You must launch `claude` from inside the repo** for this to work — the bare `.` fallback is only a last resort and is *not* reliable, because it resolves against the launching process's working directory, not the repo. > **Why not `${CLAUDE_PLUGIN_ROOT}`?** `CLAUDE_PLUGIN_ROOT` is set **only** for marketplace plugin installs, never for a project-scope clone, so it can't drive the clone workflow. Conversely, a plugin install can't use `CLAUDE_PROJECT_DIR` (in a plugin, that points at the *user's* project, not the plugin's own directory). Claude Code does **not** support nested defaults like `${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PROJECT_DIR:-.}}`, so a single entrypoint string cannot serve both contexts. The two distribution paths are therefore decoupled: the **plugin** carries its own MCP config in `.claude-plugin/plugin.json` (using `${CLAUDE_PLUGIN_ROOT}`), while the root `.mcp.json` is dedicated to the **clone** workflow (using `${CLAUDE_PROJECT_DIR:-.}`). Because `plugin.json` declares its own `mcpServers`, the plugin does not also auto-load the root `.mcp.json`, so there is no double-registration. > **Heads-up on scope precedence:** project-scope (`.mcp.json`) outranks user-scope. If you *also* have an `apple-numbers` entry registered at user scope (e.g. an absolute path in `~/.claude.json`), the project-scope entry wins and the user-scope one is ignored entirely. Pick one — for local development on this repo, the project-scope `.mcp.json` is the intended source. To pin a specific local build instead, register it at **local** scope (`claude mcp add apple-numbers -s local -- node /abs/path/build/index.js`), which outranks project scope. ## Requirements - **macOS** — the npm package is macOS-only (it declares `os: ["darwin"]`). Reads **and value/structure writes** go through `numbers-parser` directly (no Numbers.app needed); **formatting and formula tools additionally require Numbers.app.** - **Node.js 20+** — Required for the MCP server - **Python 3.11+** — the `numbers-parser` library installs automatically into a project-local venv on first use (or pre-warm with `pnpm run setup`). numbers-parser requires Python ≥ 3.10; macOS ships 3.9, so install a newer Python first (e.g. `brew install python@3.12`). - **Automation permission (formulas & formatting only)** — Reads, exports and ordinary value/structure writes (`set-cell(s)`, `add`/`update`/`delete-rows`, `add-sheet`/`add-table`, `rename-*`, `create-spreadsheet`, `import-csv`) need no special permission and no Numbers.app. Only the eight formula/format tools — `set-formula(s)`, `set-cell(s)-style`, `set-column-width`/`set-row-height`, `merge-cells`/`unmerge-cells` — drive Numbers.app via AppleScript and require the host app to have Automation permission for Numbers, granted on first use. See the [Automation Permission guide](https://github.com/sweetrb/apple-numbers-mcp/blob/main/docs/AUTOMATION-PERMISSION.md). Run the **`doctor`** tool to verify your setup. ## Features ### Read | Feature | Description | |---------|-------------| | **File Inspection** | List sheets, tables, dimensions, and header rows | | **Table Read** | Read data with optional row range and column filtering | | **Cell Read** | Single cells by 0-based index, with optional formula/format/merge metadata | | **Search** | Case-insensitive text search across every cell, optionally scoped to one sheet | | **Export** | Export a table to CSV, TSV, or JSON | ### Write | Feature | Description | |---------|-------------| | **Create Spreadsheet** | New `.numbers` file with headers and optional initial rows | | **Set Cell** | Write a value to a single cell, with optional type coercion | | **Set Cells Batch** | Write many cells in one operation (more efficient than multiple set-cell calls) | | **Add / Update / Delete Rows** | Append, replace, or remove rows by index | | **Sheets and Tables** | Add new sheets or tables to an existing file; rename either | ### Formulas (requires Numbers.app) | Feature | Description | |---------|-------------| | **Set Formula** | Write a formula like `=SUM(B2:B10)` to a cell | | **Set Formulas Batch** | Write many formulas at once | ### Formatting (requires Numbers.app) | Feature | Description | |---------|-------------| | **Cell Styles** | Font, size, colors, number format, alignment | | **Cell Styles Batch** | Style many cells at once | | **Column Width / Row Height** | Set dimensions in pixels | | **Merge / Unmerge** | Merge a range of cells, or undo a merge | ### Import / Diagnostics | Feature | Description | |---------|-------------| | **Import CSV/TSV/JSON** | Convert a tabular file into a new `.numbers` spreadsheet | | **Health Check** | Verify Python 3 and numbers-parser are installed | | **Doctor** | Richer setup diagnostic — Python interpreter (path + version), read sidecar, Numbers.app, and Automation permission, each reported ok / warn / fail with actionable advice | All tools also return **structured JSON** (`structuredContent`) alongside the human-readable text, so agents can consume results without parsing prose. ### MCP resources & prompts Resources expose read-only context the client can attach without a tool call: the `numbers://file/{path}` template (file structure — sheets, tables, dimensions, headers) and the `numbers://table/{path}` template (the default table's data). Both are templated by a URL-encoded `.numbers` file path. Prompts package common workflows: `analyze-spreadsheet`, `bulk-edit`, `import-csv-guide`. --- ## Tool Reference This section documents all available tools. AI agents should use these tool names and parameters exactly as specified. ### Read #### `health-check` Verify Python 3 and `numbers-parser` are installed and reachable from the server. **Parameters:** None **Returns:** `numbers-parser` version, or an error explaining how to install. --- #### `doctor` Run a full setup diagnostic with four separate checks: `python_interpreter` (the resolved Python's path and version — warns when it's older than 3.11, the most common setup failure since stock macOS ships 3.9), `numbers_parser` (the read sidecar — required for all reads/exports), `numbers_app` (Numbers.app present — required for the formula/format tools only), and `automation_permission` (an informational reminder that those tools need Automation permission for Numbers.app). Each is reported as ok / warn / fail with actionable advice. This is the richer counterpart to `health-check`; reach for it first when a tool returns a permission or setup error. **Parameters:** None **Returns:** A per-check report. The `structuredContent` carries the raw `{ healthy, checks[] }`, where each check has `name`, `status` (`ok`/`warn`/`fail`), and `detail`. Only the formula/format tools need Automation permission — see [docs/AUTOMATION-PERMISSION.md](https://github.com/sweetrb/apple-numbers-mcp/blob/main/docs/AUTOMATION-PERMISSION.md). --- #### `get-file-info` Get the structure of a `.numbers` file: sheets, tables, dimensions, header rows. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Absolute or `~`-relative path to the `.numbers` file | **Returns:** Default sheet name, plus each sheet with its tables (name, dimensions, header row). --- #### `read-table` Read data from a table. Returns headers and rows. Defaults to the first sheet and first table if not specified. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `sheet` | string | No | Sheet name (default: first sheet) | | `table` | string | No | Table name (default: first table) | | `startRow` | number | No | 0-based start row, inclusive (default: 1, after header) | | `endRow` | number | No | 0-based end row, inclusive (default: last row) | | `columns` | (string \| number)[] | No | Column filter: header names or 0-based indices | **Returns:** Sheet name, table name, the headers and rows selected, and `numRows` / `numCols` describing **the selection** — not the table. When you pass `startRow`/`endRow`/`columns`, those counts describe what came back; use [`get-file-info`](#get-file-info) for the table's real dimensions. --- #### `get-cell` Read a single cell value by row and column index (0-based). | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `sheet` | string | Yes | Sheet name | | `table` | string | Yes | Table name | | `row` | number | Yes | Row index (0-based) | | `col` | number | Yes | Column index (0-based) | | `verbose` | boolean | No | Include formula, formatted value, and merge info | --- #### `search` Case-insensitive partial match across every cell in a `.numbers` file. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `query` | string | Yes | Text to search for | | `sheet` | string | No | Limit search to one sheet | **Returns:** Each match with sheet, table, row, column header, and value. --- #### `export-table` Export a table to CSV, TSV, or JSON. **⚠️ 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. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `format` | `"csv" \| "tsv" \| "json"` | Yes | Output format | | `outputPath` | string | Yes | Path for the output file | | `sheet` | string | No | Sheet name (default: first sheet) | | `table` | string | No | Table name (default: first table) | | `overwrite` | boolean | No | Replace an existing file at the destination (default: `false` — refused) | --- ### Write #### `create-spreadsheet` Create a new `.numbers` file with one sheet and table. **⚠️ 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. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path for the new file | | `headers` | string[] | Yes | Column header names | | `rows` | (string \| number \| boolean \| null)[][] | No | Initial data rows | | `sheetName` | string | No | Default: `"Sheet 1"` | | `tableName` | string | No | Default: `"Table 1"` | | `overwrite` | boolean | No | Replace an existing file at the destination (default: `false` — refused) | --- #### `set-cell` Write a value to a single cell. **⚠️ Safety:** Overwrites the existing cell value in place in the `.numbers` file. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `row` | number | Yes | Row index (0-based) | | `col` | number | Yes | Column index (0-based) | | `value` | string \| number \| boolean \| null | Yes | Value to write | | `sheet` | string | No | Sheet name (default: first sheet) | | `table` | string | No | Table name (default: first table) | | `type` | `"string" \| "number" \| "boolean" \| "date"` | No | Force value type (default: auto-detect) | --- #### `set-cells-batch` Write multiple cells in a single operation. Much more efficient than multiple `set-cell` calls. **⚠️ Safety:** Overwrites the existing cell values in place in the `.numbers` file. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `updates` | array | Yes | Array of `{row, col, value, type?}` objects | | `sheet` | string | No | Sheet name | | `table` | string | No | Table name | --- #### `add-rows` Append rows of data after the last existing row. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `rows` | array[] | Yes | Rows to append (one array per row) | | `sheet` | string | No | Sheet name | | `table` | string | No | Table name | --- #### `update-rows` Write full rows by index. Each update is a complete row replacement. **⚠️ Safety:** Overwrites the existing row data in place in the `.numbers` file. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `updates` | array | Yes | Array of `{row, values}` objects | | `sheet` | string | No | Sheet name | | `table` | string | No | Table name | --- #### `delete-rows` Delete a range of rows by 0-based inclusive indices. **⚠️ Safety:** Destructive and not undoable — requires explicit user confirmation. Modifies the `.numbers` file in place; verify the row range first. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `startRow` | number | Yes | First row to delete (0-based, inclusive) | | `endRow` | number | Yes | Last row to delete (0-based, inclusive) | | `sheet` | string | No | Sheet name | | `table` | string | No | Table name | --- ### Sheets and Tables #### `add-sheet` Add a new sheet, optionally with headers for the default table. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `sheetName` | string | Yes | Name for the new sheet | | `tableName` | string | No | Name for the default table | | `headers` | string[] | No | Headers for the default table | **Table geometry:** pass `headers` and the new table is created **1 row × `headers.length` columns**; omit them and you get a **12 × 8** grid of empty cells. There is no way to override the size through MCP — delete the extra rows with `delete-rows` if you don't want them. The response reports the dimensions that were created. --- #### `add-table` Add a new table to an existing sheet. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `sheet` | string | No | Sheet name (default: first sheet) | | `tableName` | string | No | Name for the new table | | `headers` | string[] | No | Column headers | **Table geometry:** same rule as [`add-sheet`](#add-sheet) — with `headers` the table is **1 × `headers.length`**, without them **12 × 8**. --- #### `rename-sheet` / `rename-table` Rename a sheet or table. You identify the target by its **current** name via the optional `sheet` / `table` parameters (omit them to target the first sheet / first table); there is no `oldName` input — the old name is returned in the result, not passed in. **`rename-sheet`** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `newName` | string | Yes | New name for the sheet | | `sheet` | string | No | Current sheet name (default: first sheet) | **`rename-table`** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `newName` | string | Yes | New name for the table | | `sheet` | string | No | Sheet containing the table (default: first sheet) | | `table` | string | No | Current table name (default: first table) | --- ### Formulas (requires Numbers.app) #### `set-formula` Write a formula to a cell. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `sheet` | string | Yes | Sheet name | | `table` | string | Yes | Table name | | `row` | number | Yes | 0-based row | | `col` | number | Yes | 0-based column | | `formula` | string | Yes | Formula text including the leading `=` | --- #### `set-formulas-batch` Write multiple formulas in a single operation. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `sheet` | string | Yes | Sheet name | | `table` | string | Yes | Table name | | `formulas` | array | Yes | Array of `{row, col, formula}` objects | --- ### Formatting (requires Numbers.app) #### `set-cell-style` Apply font, color, number format, and alignment to a single cell. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `sheet` | string | Yes | Sheet name | | `table` | string | Yes | Table name | | `row` | number | Yes | 0-based row | | `col` | number | Yes | 0-based column | | `style` | object | Yes | Style properties to set (all optional) — see the table below | **`style` object fields** (all optional; omit a field to leave it unchanged): | Field | Type | Description | |-------|------|-------------| | `fontName` | string | Font name, e.g. `"Helvetica-Bold"`, `"HelveticaNeue"`. There is no separate `bold`/`italic` flag — pick a font face that already encodes the weight/style. | | `fontSize` | number | Font size in points | | `textColor` | `{red, green, blue}` | Text color, RGB with each channel **0–65535** | | `backgroundColor` | `{red, green, blue}` | Cell fill color, RGB with each channel **0–65535** | | `format` | enum | Cell number format: `"automatic"`, `"number"`, `"currency"`, `"date and time"`, `"duration"`, `"fraction"`, `"scientific"`, `"numeral system"`, `"checkbox"`, `"star rating"`, `"text"` | | `alignment` | enum | Horizontal alignment: `"auto align"`, `"left"`, `"center"`, `"right"`, `"justify"` | | `verticalAlignment` | enum | Vertical alignment: `"top"`, `"center"`, `"bottom"` | | `textWrap` | boolean | Enable text wrapping | Example: `{ "fontName": "Helvetica-Bold", "fontSize": 14, "textColor": { "red": 65535, "green": 0, "blue": 0 }, "alignment": "center" }` --- #### `set-cells-style-batch` Apply styles to multiple cells in a single operation. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `sheet` | string | Yes | Sheet name | | `table` | string | Yes | Table name | | `entries` | array | Yes | Array of `{row, col, style}` objects — each `style` uses the same fields documented under [`set-cell-style`](#set-cell-style) | --- #### `set-column-width` / `set-row-height` Set the width of a column or height of a row in pixels. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `sheet` | string | Yes | Sheet name | | `table` | string | Yes | Table name | | `col` (column) / `row` (row) | number | Yes | 0-based index | | `width` (column) / `height` (row) | number | Yes | Size in pixels | --- #### `merge-cells` / `unmerge-cells` Merge a rectangular range of cells, or undo a merge. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `path` | string | Yes | Path to the `.numbers` file | | `sheet` | string | Yes | Sheet name | | `table` | string | Yes | Table name | | `startRow`, `startCol`, `endRow`, `endCol` | number | Yes | 0-based corners (inclusive) | --- ### Import #### `import-csv` Import a CSV, TSV, or JSON file into a new `.numbers` spreadsheet. Auto-detects format from extension, or pass `format` explicitly. **⚠️ 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. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `inputPath` | string | Yes | Path to the CSV/TSV/JSON input | | `outputPath` | string | Yes | Path for the output `.numbers` file | | `format` | `"auto" \| "csv" \| "tsv" \| "json"` | No | Default: auto-detect from extension | | `sheetName` | string | No | Default: `"Sheet 1"` | | `tableName` | string | No | Default: `"Table 1"` | | `overwrite` | boolean | No | Replace an existing file at the destination (default: `false` — refused) | **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"`. **JSON column sets come from the first object.** For an array of objects, the columns are the keys of `data[0]`; a key that appears only in a later object is dropped and objects missing an early key get blank cells — normalize the keys first. For an array of arrays, columns are named `Column_0`, `Column_1`, … and the first row is kept as data. **`format: "auto"` falls back to CSV.** Any extension that isn't `.csv`, `.tsv` or `.json` is parsed as comma-separated rather than erroring, so pass `format` explicitly for unusual extensions. The format actually used is echoed back in the result. --- ## Usage Patterns ### Basic Workflow ``` User: "What's in ~/Documents/budget.numbers?" AI: [calls get-file-info] "It has 3 sheets: Q1, Q2, Q3. Q1 contains a 'Budget' table with 12 rows × 5 columns..." User: "Show me Q3" AI: [calls read-table with sheet='Q3'] "Q3 / Budget (12 rows × 5 cols): ..." User: "Find every line about Acme Corp" AI: [calls search with query='Acme Corp'] "Found 4 matches in Q1/Budget and Q3/Vendors..." ``` ### Iterative Edits ``` User: "Set B5 to =SUM(B2:B4) and bold the header row" AI: [calls set-formula] [calls set-cells-style-batch with style {fontName: "Helvetica-Bold"} on row 0] "Done — B5 is =SUM(B2:B4) and the header row is bold." ``` ### Importing CSV ``` User: "Import ~/Downloads/customers.csv into a new spreadsheet" AI: [calls import-csv with inputPath, outputPath] "Imported 240 rows (6 columns) into ~/Documents/customers.numbers" ``` --- ## Installation Options ### npm (Recommended) ```bash npm install -g apple-numbers-mcp pip3 install numbers-parser ``` ### From Source (with Project-Local venv) ```bash git clone https://github.com/sweetrb/apple-numbers-mcp.git cd apple-numbers-mcp pnpm install pnpm run setup # creates ./venv and installs numbers-parser pnpm run build ``` Installing the repo directly from GitHub (`npm install -g github:sweetrb/apple-numbers-mcp`) also counts as a from-source install: it runs the repo's build on your machine and **requires pnpm**. Prefer the published npm package above unless you specifically want unreleased code. If installed from source, use this configuration: ```json { "mcpServers": { "apple-numbers": { "command": "node", "args": ["/absolute/path/to/apple-numbers-mcp/build/index.js"] } } } ``` The server prefers a project-local venv at `./venv/bin/python3` if present, and falls back to system `python3`. Global npm install works fine as long as `numbers-parser` is on the system Python. --- ## Configuration All configuration is optional — the server works out of the box. ### Environment variables | Variable | Default | Description | |----------|---------|-------------| | `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. | | `APPLE_NUMBERS_MCP_CONFIG_FILE` | `~/Library/Application Support/apple-numbers-mcp/config.json` | Path to the JSON config file (see below). | | `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`). | | `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.** | | `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. | > **Per-call timeouts are not configurable.** Every `numbers-parser` sidecar call > runs under a fixed **30-second** timeout and every AppleScript call under a fixed > **60-second** one; there is no environment variable for either. > `APPLE_NUMBERS_MCP_SETUP_TIMEOUT` above governs only the one-time venv bootstrap, > and `APPLE_NUMBERS_MCP_MAX_BUFFER` caps stdout size, not wall-clock time. If you > hit `Operation timed out after 30000ms`, narrow the request — use `read-table`'s > `startRow`/`endRow` and `columns`, split large batches — rather than looking for > a knob. ### Configuration file (when the host strips `env`) Some host apps (e.g. Claude Desktop) launch the MCP server with a scrubbed environment and ignore the `env` block in their server config, so there's no way to pass `APPLE_NUMBERS_MCP_*` settings through it. In that case, put them in a JSON file the host doesn't manage — `APPLE_NUMBERS_MCP_CONFIG_FILE`, or by default `~/Library/Application Support/apple-numbers-mcp/config.json`: ```json { "APPLE_NUMBERS_MCP_MAX_BUFFER": "104857600" } ``` The server reads it at startup and merges string values into the environment **without overriding** anything already set there (so an explicit `env` still wins). Keep only non-secret config here. --- ## Architecture This package is a **TypeScript MCP server with a Python sidecar**: - The MCP server (Node) speaks the Model Context Protocol over stdio. - A bundled Python script (`src/utils/numbers_reader.py`) uses `numbers-parser` to read and write `.numbers` files and returns JSON. - For formatting, formulas, and cell-dimension changes, an AppleScript layer (`src/utils/applescript.ts`) drives Numbers.app directly — `numbers-parser` doesn't write styles. - TypeScript spawns Python via `child_process.execFileSync`. This is the same Python-sidecar pattern used by [apple-photos-mcp](https://github.com/sweetrb/apple-photos-mcp) for the `osxphotos` library. --- ## Security and Privacy - **Local only** — All operations happen on the local machine. No data is sent to external servers. - **No credential storage** — The server doesn't store any passwords or authentication tokens. - **File-system access** — Tools take explicit paths; the server only reads or writes files you name. - **Numbers.app automation** — Formatting and formula tools drive Numbers.app via AppleScript. macOS will prompt for automation permission on first use. These tools **open the file in Numbers.app** (launching it if it isn't running), **save the whole document** — including any unsaved edits you have open in it — and **leave it open** afterwards. Everything else, including all value and structure writes, runs entirely in the Python sidecar and never touches the app. --- ## Known Limitations For the full rundown — the values-vs-formatting backend split, AppleScript-only formulas/styles, `import-csv` auto-typing, fixed per-call timeouts, indexing, dates, format lag, and concurrent edits — see **[docs/LIMITATIONS.md](https://github.com/sweetrb/apple-numbers-mcp/blob/main/docs/LIMITATIONS.md)**. The summary below is the quick version. | Limitation | Reason | |------------|--------| | Formatting / formulas / dimensions need Numbers.app | `numbers-parser` doesn't write styles or formulas; the AppleScript layer fills that gap (macOS only). Value and structure writes do **not** need it | | `import-csv` auto-types CSV/TSV fields | Leading zeros are lost (`01234` → `1234`); JSON input is passed through untouched | | Per-call timeouts are fixed | 30 s for sidecar calls, 60 s for AppleScript; neither is configurable — narrow the request instead | | No conditional formatting | Not exposed by `numbers-parser` | | No charts or images | Not exposed by `numbers-parser` | | Sheet deletion not supported | Not exposed by `numbers-parser` | | Computed-value writes only | `set-cell` / `add-rows` etc. write computed values; use `set-formula` to write formulas | | Concurrent writes are last-writer-wins | Saves are atomic (no torn files), but overlapping writes to the same file can lose updates — serialize writes per file | | Date filter format | ISO 8601 (`YYYY-MM-DD` or full ISO datetime) | These are tracked for future releases. The underlying `numbers-parser` library has partial support for styles via its `Style` API, which provides a path forward for some of these. --- ## Troubleshooting ### "numbers-parser not installed" - **Most common cause: your `python3` is older than 3.11** — stock macOS ships Python 3.9, which `numbers-parser` can't run on. Install a newer Python (`brew install python@3.12`), then simply retry the tool call: the venv rebuilds automatically. - Otherwise, install the library directly: `pip3 install numbers-parser` (global Python), or run `scripts/setup.sh` from a repo checkout (`pnpm run setup`) to build the project-local venv. - Run the **`doctor`** tool — it reports the resolved Python interpreter path and version, so a too-old stock Python is visible at a glance. - If you used a virtualenv, make sure it's the one at `./venv/` in the project directory. ### `apple-numbers` server fails to connect when run from a clone - Launch `claude` from inside the repo directory so `CLAUDE_PROJECT_DIR` resolves to the repo root (the bare `.` fallback is unreliable). - Run **both** `pnpm run build` and `pnpm run setup` first — `build` compiles the entrypoint, `setup` creates the `./venv` the Python sidecar needs. - Run `claude mcp list` to check for conflicting scopes; project-scope `.mcp.json` outranks a user-scope `apple-numbers` entry, and a local-scope entry outranks both. - If the server is listed as pending, approve the project-scope server when Claude Code prompts. ### Every tool is rejected with "invalid outputSchema … unsupported dialect" - Full symptom: `Tool '' has an invalid outputSchema: JSON Schema declares an unsupported dialect ("$schema": "http://json-schema.org/draft-07/schema#"). The default validator supports JSON Schema 2020-12 only.` The host refuses **every** tool, so the server looks completely dead even though it started fine. - **Upgrade to 1.1.17 or newer.** MCP standardized on JSON Schema 2020-12, and hosts now reject any other dialect; releases before 1.1.17 advertised draft-07 because the MCP SDK emits it. 1.1.17 advertises `https://json-schema.org/draft/2020-12/schema` on every tool's `inputSchema` and `outputSchema`. - If you installed via `npx -y apple-numbers-mcp`, restart the host to pick up the new version; if you run from a clone, `git pull && pnpm install && pnpm run build`. ### "File not found" - Check the path; expand `~` if your shell isn't doing it. - Ensure the file extension is `.numbers`. ### "Output path … is outside the allowed roots" / "Input path … is outside the allowed roots" - 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. - **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. - 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. - 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). ### Formatting / formula tools fail with "Numbers.app not running" or "Not authorized to send Apple events to Numbers" - Open Numbers.app at least once. macOS will prompt for automation permission — accept it. - Verify in **System Settings → Privacy & Security → Automation**, or reset with `tccutil reset AppleEvents`. - Run the **`doctor`** tool to confirm your setup — it reports the Numbers.app and Automation-permission checks separately. See the [Automation Permission guide](https://github.com/sweetrb/apple-numbers-mcp/blob/main/docs/AUTOMATION-PERMISSION.md). Reads don't need this permission; only writes do. ### Output looks wrong (dates as ISO, floats with decimals) - The server normalizes dates to ISO 8601 and rounds floats that are within `1e-9` of an integer. If you want raw values, file an issue. --- ## Development ```bash pnpm install # Install dependencies pnpm run setup # Create ./venv with numbers-parser pnpm run build # Compile TypeScript pnpm test # Unit tests (mocked) pnpm run test:integration # Integration tests against real .numbers fixtures pnpm run test:all # Both pnpm run test:coverage # Unit tests with coverage report pnpm run typecheck # Type-check without emitting pnpm run lint # Check code style pnpm run format # Format code ``` ### Integration tests Integration tests exercise the full pipeline against real `.numbers` fixture files. Generate the fixtures first, then run: ```bash pnpm run setup ./venv/bin/python3 test/fixtures/generate-fixtures.py pnpm run test:all ``` Unit tests run everywhere; integration tests auto-skip when fixtures or `numbers-parser` are not available. --- ## Author **Rob Sweet** - President, [Superior Technologies Research](https://www.superiortech.io) A software consulting, contracting, and development company. - Email: rob@superiortech.io - GitHub: [@sweetrb](https://github.com/sweetrb) ## License MIT License - see [LICENSE](https://github.com/sweetrb/apple-numbers-mcp/blob/main/LICENSE) for details. This project is not affiliated with Apple Inc. or the [numbers-parser](https://pypi.org/project/numbers-parser/) project. ## Contributing Contributions are welcome! Please open an issue or PR at [github.com/sweetrb/apple-numbers-mcp](https://github.com/sweetrb/apple-numbers-mcp). ## Related Projects Part of a family of macOS MCP servers: - [apple-mail-mcp](https://github.com/sweetrb/apple-mail-mcp) — MCP server for Apple Mail (read, search, send, and organize email) - [apple-notes-mcp](https://github.com/sweetrb/apple-notes-mcp) — MCP server for Apple Notes (create, search, update, and export notes) - [apple-photos-mcp](https://github.com/sweetrb/apple-photos-mcp) — MCP server for Apple Photos (query metadata and export originals) - [numbers-parser](https://pypi.org/project/numbers-parser/) — The Python library that powers this server ## Recurring macOS permission prompts If macOS keeps re-prompting for Full Disk Access or Automation for `node` (often after a `brew upgrade`), see [docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md](https://github.com/sweetrb/apple-numbers-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md) — the fix is to run this server under the official, Developer-ID-signed Node so the grant survives Node updates.