Menu

AI

How to Build an MCP Server for Excel Files (.xlsx) in Python

Build a Python MCP server that lists workbooks and reads Excel sheets with openpyxl, handles formulas and cached values, and connects to Claude or Cursor.

By: Irina ShvayaNovember 2, 2026

How to Build an MCP Server for Excel Files (.xlsx) in Python

This tutorial is part of Build an MCP Server, within The MCP Guide. Want it built for you? See MCP development.

An MCP server for Excel files lets an AI assistant open the workbooks on your computer and read them sheet by sheet. You build it in Python with the official MCP SDK and the openpyxl library. It needs two tools to be useful: one that lists workbooks and their sheets, and one that reads a sheet.

The tricky part is not MCP. It is Excel formulas. openpyxl reads files, but it does not run Excel, so formula results depend on what Excel saved last. This guide shows that problem in a real test and how to handle it.

Checked against: the Python SDK docs and the openpyxl docs on 2 November 2026, with mcp 2.3.0 (the latest PyPI release) and openpyxl 3.1.5. The code ran locally on 2 November 2026 on Python 3.12.

Key Takeaways

  • Use openpyxl.load_workbook(..., read_only=True) for large files, and always call close() afterwards, as the openpyxl docs require.
  • data_only=True returns the values Excel stored when the file was last saved. A workbook never opened in Excel can return None for formula cells.
  • Offer a "formulas" switch so the model can see the formula text when values are missing.
  • Lock the server to one folder and to .xlsx and .xlsm files.
  • Keep the first version read-only. Writing to a workbook someone has open in Excel causes conflicts.

What this server is for

Many small businesses run on spreadsheets: budgets, price lists, schedules, inventory. A local Excel server lets you ask, "What is the Q2 total for ads in budget.xlsx?" and get an answer from the actual file.

It reads local .xlsx files. If your workbooks live in Google Sheets, see building an MCP server for Google Sheets. For plain exports, a CSV server is simpler.

Prerequisites

  • Python 3.11 or newer.
  • A folder of .xlsx files. The openpyxl docs describe the library as reading and writing "Excel 2010 xlsx/xlsm files," so convert older .xls files first.
  • uv or pip, and Node.js 22.19+ for the Inspector CLI.

New to MCP? Lesson 14 of our course walks through your first Python tool.

Install

uv init excel-mcp --python 3.12
cd excel-mcp
uv add "mcp[cli]" openpyxl
mkdir files

Minimal working server

Create server.py:

import os
from pathlib import Path

from openpyxl import load_workbook

from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import ToolAnnotations

FILES_DIR = Path(os.environ.get("EXCEL_DIR", "files")).resolve()
READ_ONLY = ToolAnnotations(read_only_hint=True, open_world_hint=False)

mcp = MCPServer("excel-files")


def workbook_path(name: str) -> Path:
    path = (FILES_DIR / name).resolve()
    if not path.is_relative_to(FILES_DIR) or path.suffix.lower() not in {".xlsx", ".xlsm"}:
        raise ToolError(f"{name!r} is not an .xlsx or .xlsm file in the files folder.")
    if not path.exists():
        raise ToolError(f"No workbook named {name!r}. Call list_workbooks first.")
    return path


@mcp.tool(annotations=READ_ONLY)
def list_workbooks() -> dict[str, list[str]]:
    """List each workbook and its sheet names."""
    result = {}
    for path in sorted(FILES_DIR.glob("*.xls[xm]")):
        wb = load_workbook(path, read_only=True)
        result[path.name] = wb.sheetnames
        wb.close()
    return result


if __name__ == "__main__":
    mcp.run()

What it does:

  • workbook_path keeps every request inside FILES_DIR and limits it to Excel files. The model picks the file name, so never trust it blindly.
  • read_only=True. The openpyxl optimized modes page says read-only mode uses "lazy loading" and is meant for very large files. It also says "The workbook must be explicitly closed with the close() method."
  • list_workbooks gives the model a map of what exists, so it does not guess sheet names.

Add a tool that reads a sheet

Add this below list_workbooks:

from typing import Annotated, Any

from pydantic import Field


@mcp.tool(annotations=READ_ONLY)
def read_sheet(
    workbook: str,
    sheet: str,
    max_rows: Annotated[int, Field(ge=1, le=1000)] = 100,
    formulas: Annotated[bool, Field(description="True returns formula text instead of cached values.")] = False,
) -> list[list[Any]]:
    """Read rows from one sheet. Values are what Excel last saved, not recalculated."""
    wb = load_workbook(workbook_path(workbook), read_only=True, data_only=not formulas)
    try:
        if sheet not in wb.sheetnames:
            raise ToolError(f"No sheet {sheet!r}. Sheets: {', '.join(wb.sheetnames)}")
        rows = []
        for row in wb[sheet].iter_rows(values_only=True):
            rows.append([str(v) if v is not None and not isinstance(v, (int, float, str, bool)) else v for v in row])
            if len(rows) >= max_rows:
                break
        return rows
    finally:
        wb.close()

Key details:

  • data_only. The openpyxl tutorial says this flag controls whether formula cells return the formula or "the value stored the last time Excel read the sheet."
  • values_only=True returns plain cell values instead of cell objects.
  • The str(v) conversion turns dates and times into text, so the result is always valid JSON.
  • The finally block closes the workbook even when the sheet name is wrong.
  • The sheet error lists real sheet names, so the model can correct itself.

Test it in memory

To test, create a workbook with a formula. This script builds one with openpyxl, which is exactly the case that catches people out:

from openpyxl import Workbook

wb = Workbook()
ws = wb.active
ws.title = "Budget"
ws.append(["Item", "Q1", "Q2", "Total"])
ws.append(["Ads", 1000, 1200, "=B2+C2"])
ws.append(["Hosting", 300, 300, "=B3+C3"])
wb.create_sheet("Notes").append(["Owner", "Dana"])
wb.save("files/budget.xlsx")

Then create check.py:

import anyio
from mcp import Client
from server import mcp


async def main() -> None:
    async with Client(mcp) as c:
        print((await c.call_tool("list_workbooks", {})).structured_content)
        print((await c.call_tool("read_sheet", {"workbook": "budget.xlsx", "sheet": "Budget"})).structured_content)
        print((await c.call_tool("read_sheet", {
            "workbook": "budget.xlsx", "sheet": "Budget", "formulas": True})).structured_content)
        r = await c.call_tool("read_sheet", {"workbook": "budget.xlsx", "sheet": "Sheet1"})
        print(r.is_error, r.content[0].text)

anyio.run(main)

The output on 2 November 2026:

{'budget.xlsx': ['Budget', 'Notes']}
{'result': [['Item', 'Q1', 'Q2', 'Total'], ['Ads', 1000, 1200, None], ['Hosting', 300, 300, None]]}
{'result': [['Item', 'Q1', 'Q2', 'Total'], ['Ads', 1000, 1200, '=B2+C2'], ['Hosting', 300, 300, '=B3+C3']]}
True Error executing tool read_sheet: No sheet 'Sheet1'. Sheets: Budget, Notes

Look at the second line. The Total column is None. The file was written by openpyxl and never opened in Excel, so no calculated value was stored. With formulas: True, the model sees =B2+C2 and can do the math itself or tell you the file needs recalculating.

Files saved by Excel normally store calculated values, so data_only returns numbers. Still, write your tool description so the model knows that values are "what Excel last saved."

Test with the MCP Inspector

npx @modelcontextprotocol/inspector --cli \
  -e EXCEL_DIR=/ABSOLUTE/PATH/excel-mcp/files \
  uv --directory /ABSOLUTE/PATH/excel-mcp run server.py \
  --method tools/call --tool-name list_workbooks --format json

The Inspector docs explain the web UI and CLI. Our Inspector lesson shows how to script checks like this one.

Connect it to a host

Register it in Claude Code, with options before the server name:

claude mcp add -e EXCEL_DIR=/ABSOLUTE/PATH/excel-mcp/files --transport stdio excel-files \
  -- uv run --directory /ABSOLUTE/PATH/excel-mcp server.py

For Claude Desktop, Cursor or VS Code, follow the SDK's real-host guide, or build the JSON with our MCP config generator. Use absolute paths everywhere.

Deploy notes

Keep an Excel server local. The workbooks stay on your machine and nothing listens on a network port. If several people need the same workbook, a shared spreadsheet service or a database is a better home than a file on one laptop.

If you later add writes, use a separate tool that writes to a new file, such as budget-ai-edits.xlsx. The openpyxl docs say read-only worksheets cannot be edited, so you would load the workbook in normal mode for writes. Saving with openpyxl also drops cached formula values until Excel recalculates, as the test above shows.

Pitfalls

  1. Forgetting close(). Read-only workbooks hold the file open until closed, which can lock it on Windows.
  2. Trusting data_only blindly. Cells with formulas can return None or stale values. Offer formula text as a fallback.
  3. Huge sheets. Cap rows. A 50,000-row sheet will overflow the model's context. Add a range or filter tool instead.
  4. v1 code. from mcp.server.fastmcp import FastMCP does not exist in SDK v2. Use from mcp.server import MCPServer.
  5. Hidden sheets and personal data. The server reads every sheet, including hidden ones. Remove sheets the AI should not see.
  6. Macros. .xlsm files can contain macros. This server reads cell values only and never runs them.

Next step

Learn the rest of the MCP building blocks in the free Learn MCP course. If your team runs on spreadsheets and wants a safe, scoped server for them, eSEOspace MCP development can help design it. Before connecting any server, review its permissions with our tool permission planner.

More in the MCP Guide

Free tools: server.json Validator · MCP Error Code Lookup

Latest research: State of MCP: October 2026

Frequently asked questions

Can openpyxl recalculate formulas?

No calculation step appears in its docs. With data_only=True you get the value Excel stored at last save. If you need fresh results, open and save the file in Excel, or compute the totals in your own Python code.

Does this work with .xls or .csv files?

Not as written. openpyxl targets .xlsx and .xlsm. Convert .xls files in Excel first, and use a CSV server for .csv exports.

Can the model edit cells?

You can add a write tool, but start read-only. If you add one, write to a copy and keep the original untouched until a person reviews it.

What about Excel files in OneDrive or SharePoint?

This server reads files on disk. A synced OneDrive folder works if the files are downloaded locally. For cloud-only workbooks you need an API-based server instead.

Will dates come back correctly?

openpyxl returns Python date and time objects for date cells. The server converts them to text, so the model sees a readable date string.

You Might Also like to Read

Get a FREE Website Audit

Enter your website and get a 20–30 page SEO, GEO, AEO & CRO audit within 24 hours. Completely free.

Don't have a site yet? Get in touch →

Ready to speak with a marketing expert?
Give us a call! (916) 866-7893

Websites Launched
1407
Websites Launched
Marketing Experts
29
Marketing Experts
Hours of Dedicated Work
204533
Hours of Dedicated Work
Irina S.Benjamin G.Jeanette M.David G.