Menu

AI

How to Build an MCP Server for CSV Files (Python, No Database Needed)

Build a Python MCP server that lists, previews and summarizes CSV files from one folder, with path checks, typed inputs, Inspector tests and host setup.

By: Irina ShvayaNovember 2, 2026

How to Build an MCP Server for CSV Files (Python, No Database Needed)

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 CSV data lets an AI assistant read the exports you already have: sales reports, ad spend, CRM lists. You point the server at one folder. It gives the model three tools: list the files, preview one, and total a column by group. No database is needed.

This guide builds that server with the official Python SDK, version 2, and only the standard library csv module. It locks the server to one folder, returns clear errors the model can fix, and keeps large files from flooding the model's context.

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

Key Takeaways

  • Keep the server inside one data folder and reject any path that resolves outside it.
  • Give the model a preview tool that returns column names and a few rows, so it knows what it is working with.
  • Do the math in your code, not in the model. A sum_by tool returns exact totals.
  • Open CSV files with newline="" as the Python docs require, and utf-8-sig to strip the byte-order mark Excel adds.
  • For big files or complex questions, switch to SQLite or a database rather than loading everything into the model.

Why a CSV server instead of pasting the file?

You can paste a small CSV into a chat. That stops working once the file has thousands of rows, or when you want the same answer every week. Language models are also unreliable at adding up long columns by reading them.

A server fixes both. The model asks for what it needs, and your Python code does the counting. The answer is exact, and the model only sees the summary.

Prerequisites

  • Python 3.11 or newer.
  • A folder of CSV files with a header row. This guide uses data/sales.csv with columns date, region, product, units, revenue.
  • uv or pip, plus Node.js 22.19+ for the Inspector CLI.

If you have not built an MCP tool before, lesson 14 of the Learn MCP course is a good first step.

Install

uv init csv-mcp --python 3.12
cd csv-mcp
uv add "mcp[cli]"
mkdir data

Copy your CSV exports into data/.

Minimal working server

Create server.py:

import csv
import os
from pathlib import Path
from typing import Annotated, Any

from pydantic import Field

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

DATA_DIR = Path(os.environ.get("CSV_DIR", "data")).resolve()
READ_ONLY = ToolAnnotations(read_only_hint=True, open_world_hint=False)

mcp = MCPServer("csv-data")


def safe_path(name: str) -> Path:
    """Resolve a file name inside DATA_DIR and refuse anything outside it."""
    path = (DATA_DIR / name).resolve()
    if not path.is_relative_to(DATA_DIR) or path.suffix.lower() != ".csv":
        raise ToolError(f"{name!r} is not a CSV file in the data folder.")
    if not path.exists():
        raise ToolError(f"No file named {name!r}. Call list_csv_files first.")
    return path


def read_rows(name: str) -> list[dict[str, str]]:
    with open(safe_path(name), newline="", encoding="utf-8-sig") as f:
        return list(csv.DictReader(f))


@mcp.tool(annotations=READ_ONLY)
def list_csv_files() -> list[str]:
    """List the CSV files you can read."""
    return sorted(p.name for p in DATA_DIR.glob("*.csv"))


@mcp.tool(annotations=READ_ONLY)
def preview_csv(
    name: str,
    rows: Annotated[int, Field(ge=1, le=50)] = 5,
) -> dict[str, Any]:
    """Show the column names, row count and the first few rows of a CSV file."""
    data = read_rows(name)
    return {"columns": list(data[0].keys()) if data else [], "row_count": len(data), "rows": data[:rows]}


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

Line by line:

  • safe_path resolves the requested name and checks it is still inside DATA_DIR. A request for ../server.py or /etc/passwd resolves elsewhere and is refused. This matters because the model chooses the file name, and text inside your data could try to steer it.
  • newline="". The csv docs say a file object "should be opened with newline=''," because otherwise newlines inside quoted fields are not read correctly.
  • csv.DictReader uses the first row as field names when you do not pass any, per the same docs. Each row becomes a dict keyed by header.
  • utf-8-sig removes the invisible byte-order mark that Excel often writes at the start of CSV exports. Without it, your first column name can come back as date.
  • READ_ONLY annotations tell hosts these tools change nothing and work on a closed set of files. The SDK's Tools guide describes open_world_hint=False as working on a closed set of things, not the open web.

Add a tool that does the math

Add a grouping tool below preview_csv:

from collections import defaultdict


@mcp.tool(annotations=READ_ONLY)
def sum_by(
    name: str,
    group_column: Annotated[str, Field(description="Column to group by, e.g. region.")],
    value_column: Annotated[str, Field(description="Numeric column to add up, e.g. revenue.")],
) -> dict[str, float]:
    """Add up a numeric column for each value of a grouping column."""
    data = read_rows(name)
    if data and (group_column not in data[0] or value_column not in data[0]):
        raise ToolError(f"Unknown column. Available: {', '.join(data[0])}")
    totals: dict[str, float] = defaultdict(float)
    for row in data:
        try:
            totals[row[group_column]] += float(row[value_column] or 0)
        except ValueError:
            raise ToolError(f"{value_column} has a non-numeric value: {row[value_column]!r}")
    return dict(totals)

The error for an unknown column lists the real column names. That is deliberate. The model reads the message, picks a valid column and tries again without asking you. Our error handling lesson covers this pattern in depth.

You can add more tools the same way: filter_rows with a column and value, top_n by a numeric column, or date_range for time series. Keep each one narrow and typed.

Test it in memory

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_csv_files", {})).structured_content)
        print((await c.call_tool("sum_by", {
            "name": "sales.csv", "group_column": "region", "value_column": "revenue"})).structured_content)
        r = await c.call_tool("preview_csv", {"name": "../server.py"})
        print(r.is_error, r.content[0].text)
        r = await c.call_tool("sum_by", {
            "name": "sales.csv", "group_column": "country", "value_column": "revenue"})
        print(r.is_error, r.content[0].text)

anyio.run(main)

With a four-row sample file, the output on 2 November 2026 was:

{'result': ['sales.csv']}
{'West': 330.0, 'East': 340.0}
True Error executing tool preview_csv: '../server.py' is not a CSV file in the data folder.
True Error executing tool sum_by: Unknown column. Available: date, region, product, units, revenue

The path escape was refused, and the bad column came back with a list of valid ones.

Test it with the MCP Inspector

npx @modelcontextprotocol/inspector --cli \
  -e CSV_DIR=/ABSOLUTE/PATH/csv-mcp/data \
  uv --directory /ABSOLUTE/PATH/csv-mcp run server.py \
  --method tools/list --format json

Check that each tool's description and input schema read clearly. The model sees only that text when deciding which tool to call. The MCP Inspector docs and our Inspector lesson explain the other methods and flags.

Connect it to a host

Use absolute paths for both the project and CSV_DIR. In Claude Desktop's claude_desktop_config.json:

{
  "mcpServers": {
    "csv-data": {
      "command": "/ABSOLUTE/PATH/TO/uv",
      "args": ["run", "--directory", "/ABSOLUTE/PATH/csv-mcp", "server.py"],
      "env": { "CSV_DIR": "/ABSOLUTE/PATH/csv-mcp/data" }
    }
  }
}

The SDK's real-host guide lists the config file location for macOS and Windows, and the equivalent setup for Claude Code, Cursor and VS Code. You can also generate the JSON with our MCP config generator.

Then ask: "Which region had the most revenue in sales.csv?" The model should call sum_by and quote exact totals.

Deploy notes

A CSV server is usually local. The files stay on your computer, and the server runs only while the host is open. That is the simplest and safest setup.

If a team needs shared access, put the files in one controlled location and run the server remotely with sign-in. At that point, loading the CSVs into SQLite or PostgreSQL often pays off. See building an MCP server for SQLite.

Pitfalls

  1. Loading huge files every call. This server reads the whole file each time. That is fine for files of a few megabytes. For larger ones, cache parsed rows in the lifespan, or move the data into SQLite.
  2. Numbers stored as text. CSV has no types. "1,200.00" with a thousands separator fails float(). Clean the export or strip separators in code.
  3. Different delimiters. European exports often use semicolons. The csv module's Sniffer can guess the format, but the docs call its header check "a rough heuristic." Prefer a known format.
  4. Old tutorials. Code that imports mcp.server.fastmcp is written for SDK v1 and fails on v2. Use from mcp.server import MCPServer.
  5. Sensitive columns. If a file holds emails or phone numbers, the model will see them. Export only the columns the job needs.

Next step

The free Learn MCP course takes this server further with resources, prompts and remote deployment. If you want reporting tools built on your real exports, eSEOspace's MCP development service can scope them with you. Working with workbooks instead? See building an MCP server for Excel files.

More in the MCP Guide

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

Latest research: State of MCP: October 2026

Frequently asked questions

Why not let the model read the raw file as a resource?

You can expose a file as an MCP resource, and for small files that works. Tools are better for analysis, because your code does the math and returns only the answer. Our page on MCP resources explains when resources fit.

Can I use pandas instead of the csv module?

Yes. pandas is handy for grouping and dates. It adds a large dependency, so the standard library is a good start for simple totals.

How do I stop the model reading files outside the folder?

Resolve every requested path and check it is still inside the data folder, as safe_path does. Never join a model-supplied name to a path without that check.

Can the server write CSV files?

It can, but start read-only. If you add a write tool, make it create new files with a fixed name pattern rather than overwrite existing exports.

What file size is too big?

There is no fixed rule. If calls feel slow or results get cut off, move the data into a database and query it there.

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.