excel-mcp
# excel-mcp
An MCP (Model Context Protocol) server for reading Excel workbooks (`.xlsx`). Built with [FastMCP](https://github.com/jlowin/fastmcp) and [openpyxl](https://openpyxl.readthedocs.io/).
## Features
- List all sheets in a workbook
- List all named tables across sheets
- List all pivot tables with source range info
- Read raw cell data from any sheet or optional cell range
- Read structured data (headers + rows) from named Excel tables
- Export any sheet to CSV, plain-text (delimited), or Markdown — save to file or return inline
## Usage with Claude Desktop
No installation required. Add the following to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"excel-mcp": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/urjeetpatel/excel_mcp_server",
"excel-mcp"
]
}
}
}
```
`uvx` will pull the package directly from GitHub and run it in an isolated environment — no `pip install` or virtual environment setup needed.
## Running manually
```bash
uvx --from git+https://github.com/urjeetpatel/excel_mcp_server excel-mcp
```
## Tools
All tools accept a `file_path` parameter — the full path to the `.xlsx` file.
| Tool | Description |
|------|-------------|
| `list_sheets` | Returns the names of all sheets in the workbook |
| `list_tables` | Returns all named tables with name, sheet, and cell range |
| `list_pivot_tables` | Returns all pivot tables with location and source range info |
| `get_sheet_data` | Returns cell data from a sheet; optionally scoped to a range (e.g. `A1:D10`) |
| `get_table_data` | Returns headers and row data from a named Excel table |
| `export_sheet_to_csv` | Exports a sheet to a CSV file with configurable delimiter; supports inline return |
| `export_sheet_to_text` | Exports a sheet to a plain-text delimited file; supports inline return |
| `export_sheet_to_markdown` | Exports a sheet to a padded Markdown table; supports inline return |
| `create_blank_file` | Creates a new blank Excel file at the given path |
| `add_sheet` | Adds a new sheet to an existing Excel file |
| `add_data_to_sheet` | Adds a 2D array of data to a sheet, starting at a specified cell |
| `add_table_to_sheet` | Adds a table to a sheet over a given cell range |
| `set_cell_value` | Sets the value of a single cell in a sheet |
| `set_cell_formula` | Sets a formula in a single cell in a sheet |
### Write tool details
#### `create_blank_file(file_path)`
Creates a new blank Excel file at the specified path.
#### `add_sheet(file_path, sheet_name)`
Adds a new sheet to the Excel file. Fails if the sheet already exists.
#### `add_data_to_sheet(file_path, sheet_name, data, start_cell="A1")`
Adds a 2D array of data to the specified sheet, starting at the given cell (default A1).
#### `add_table_to_sheet(file_path, sheet_name, table_name, ref)`
Adds a table to the specified sheet, covering the given cell range (e.g. "A1:D10").
#### `set_cell_value(file_path, sheet_name, cell, value)`
Sets the value of a single cell (e.g. C5) in the specified sheet.
#### `set_cell_formula(file_path, sheet_name, cell, formula)`
Sets a formula (e.g. "=SUM(A1:A10)") in a single cell in the specified sheet.
### Tool details
#### `list_sheets(file_path)`
```json
["Sheet1", "Sheet2"]
```
#### `list_tables(file_path)`
```json
[{ "name": "SalesTable", "sheet": "Sheet1", "ref": "A1:D20" }]
```
#### `list_pivot_tables(file_path)`
```json
[{
"name": "PivotTable1",
"sheet": "Summary",
"ref": "A1:C10",
"source_sheet": "RawData",
"source_ref": "A1:F500"
}]
```
#### `get_sheet_data(file_path, sheet_name, cell_range?)`
```json
{
"sheet": "Sheet1",
"range": "A1:D10",
"rows": [["Name", "Age", "City"], ["Alice", 30, "New York"]]
}
```
- `cell_range` is optional. When omitted, the full used range is returned.
- Range strings are case-insensitive (`a1:d10` == `A1:D10`).
#### `get_table_data(file_path, table_name)`
```json
{
"table": "PeopleTable",
"sheet": "Sheet1",
"ref": "A1:C4",
"headers": ["Name", "Age", "City"],
"rows": [["Alice", 30, "New York"], ["Bob", 25, "Chicago"]]
}
```
---
### Export tools
All three export tools share a common pattern:
- `output_path` — path to write the output file, **or** `"return inline"` to skip writing and return the content directly in the response.
- `cell_range` — optional Excel range string (e.g. `A1:D10`). When omitted, the full used range is exported.
When saving to a file the response contains `output_path`. When returning inline, `output_path` is replaced by `content`.
#### `export_sheet_to_csv(file_path, sheet_name, output_path, delimiter?, cell_range?)`
Exports a sheet using Python's `csv` module (values containing the delimiter or newlines are properly quoted).
`delimiter` options: `"comma"` (default), `"pipe"`, `"tab"`.
```json
{ "output_path": "/tmp/data.csv", "sheet": "Sheet1", "range": "A1:C4", "rows_written": 4 }
```
Inline variant (`output_path = "return inline"`):
```json
{ "content": "Name,Age,City\r\nAlice,30,New York\r\n...", "sheet": "Sheet1", "range": "A1:C4", "rows_written": 4 }
```
#### `export_sheet_to_text(file_path, sheet_name, output_path, delimiter?, cell_range?)`
Exports a sheet as a plain delimited text file — values are joined with the delimiter with no CSV quoting, making output easy to read or pipe into other tools.
`delimiter` options: `"pipe"` (default), `"comma"`, `"tab"`.
```json
{ "output_path": "/tmp/data.txt", "sheet": "Sheet1", "range": "A1:C4", "rows_written": 4 }
```
Inline variant:
```json
{ "content": "Name|Age|City\nAlice|30|New York\n...", "sheet": "Sheet1", "range": "A1:C4", "rows_written": 4 }
```
#### `export_sheet_to_markdown(file_path, sheet_name, output_path, cell_range?)`
Exports a sheet as a padded Markdown table. The first row is used as the header; a separator line is inserted beneath it. All columns are padded to align.
```json
{ "output_path": "/tmp/data.md", "sheet": "Sheet1", "range": "A1:C4", "rows_written": 4 }
```
Inline variant:
```json
{ "content": "| Name | Age | City |\n| ----- | --- | -------- |\n| Alice | 30 | New York |\n...", "sheet": "Sheet1", "range": "A1:C4", "rows_written": 4 }
```
## Local development
Requires Python >= 3.14 and [uv](https://docs.astral.sh/uv/).
```bash
git clone https://github.com/urjeetpatel/excel_mcp_server
cd excel_mcp_server
uv sync --group dev
# Run tests
uv run pytest
# Type check
uv run mypy src
```
## Project Structure
```
src/excel_mcp/
├── __init__.py # Exposes main() entry point
├── server.py # FastMCP server and tool registration
├── workbook.py # Workbook loading helpers
└── tools/
├── __init__.py
├── read.py # Read tool implementations
└── export.py # Export tool implementations (CSV, text, Markdown)
tests/
├── conftest.py # pytest fixtures (builds workbooks programmatically)
├── test_read.py # Tests for read tools
├── test_export.py # Tests for export tools
└── test_workbook.py # Tests for workbook helpers
```
## Notes
- Workbooks are opened in `data_only=True` mode — formula results are read, not formula strings.
- Export tools support `"return inline"` as `output_path` to return content directly to the agent without touching the filesystem.
## License
GPL-3.0-or-later
TDQS
Scored across 14 tools
Each tool targets a distinct resource or action: listing vs reading vs exporting vs writing are clearly separated, and the three export tools differ by output format. Even the similar get_sheet_data and get_table_data are unambiguous because one returns raw sheet cells and the other returns a named table with headers.
Tool names follow a consistent verb_noun pattern: list_*, get_*, export_sheet_to_*, add_*, and set_cell_*. The naming style is uniform and predictable across the entire set.
14 tools is well within the ideal range for an Excel-focused MCP server. Each tool covers a distinct operation and the set feels appropriately scoped without unnecessary duplication.
The server covers the core Excel lifecycle: discovering structure, reading data, exporting, creating files, adding sheets/data/tables, and setting cell values or formulas. Minor gaps exist—such as no delete/rename/clear operations or updating existing tables—but agents can accomplish most common workflows.