# 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.
[](https://www.npmjs.com/package/apple-numbers-mcp)
[](https://www.npmjs.com/package/apple-numbers-mcp)
[](https://www.npmjs.com/package/apple-numbers-mcp)
[](https://github.com/sweetrb/apple-numbers-mcp/actions/workflows/ci.yml)
[](https://scorecard.dev/viewer/?uri=github.com/sweetrb/apple-numbers-mcp)
[](https://www.apple.com/macos/)
[](https://opensource.org/licenses/MIT)
[](https://modelcontextprotocol.io)
## 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.