mcp-data-analyst
Enables read-only analysis of SQLite database files, exposing each table as a queryable dataset for SQL queries, profiling, and charting.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-data-analystquery sales.csv for total revenue by month and make a bar chart"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-data-analyst
A Model Context Protocol server that gives any MCP client (Claude Desktop, Claude Code, Cursor or your own
agent) safe, read-only data-analysis tools over local CSV, Excel, Parquet and SQLite files. Point it at a folder
and the model can list the tables, inspect schemas, run DuckDB SQL, profile data quality, draw charts and export
results. It cannot modify your data, read files outside that folder, or run arbitrary statements: SQL is checked
with sqlglot before it runs, and the DuckDB connection is opened read-only with file-system access turned off,
so each layer blocks writes even if the other one is bypassed.

Features
Six tools:
list_datasets,describe_dataset,query,profile,create_chart,export_result.Four file formats: CSV/TSV, Parquet, Excel (
.xlsx, one table per sheet), SQLite (one table per table). Every file in the data directory (recursively) becomes a SQL table you can join with the others.DuckDB SQL with joins, CTEs, window functions and aggregates. Results come back as typed, structured JSON (
structuredContentplus a matchingoutputSchema;create_chartreturns text and image content instead).Profiling: summary statistics, histograms, top values, Pearson correlations, and data-quality checks for duplicate rows, missing values, IQR outliers, constant columns and columns that look like identifiers.
Charts: bar, line, scatter and histogram charts drawn with matplotlib. Each chart is returned as MCP image content, so the model can see it, and is also saved as a PNG.
Resources:
dataset://catalogand thedataset://{name}/schematemplate.Prompts:
exploratory_analysisanddata_quality_report, which turn a dataset's schema into a step-by-step plan that uses the tools above.Two transports: stdio (for desktop clients) and Streamable HTTP (default port 8106).
Security: a sandboxed data directory, a SQL allow-list, a read-only engine, row/time/size/memory limits, and errors returned to the client as normal tool errors. See Security model.
Demo client:
examples/groq_agent.pyis a small MCP client agent built on Groq tool calling. It shows the client side of the protocol.
Related MCP server: text2sql-agent
Architecture
flowchart LR
subgraph Clients
CD[Claude Desktop / Code]
CU[Cursor]
AG[examples/groq_agent.py<br/>Groq tool calling]
end
CD & CU & AG -- "MCP: stdio or Streamable HTTP" --> S
subgraph S[mcp-data-analyst]
direction TB
SRV["server.py<br/>MCPServer tools / resources / prompts"]
AN["analyst.py<br/>tool logic"]
G["sql_guard.py<br/>sqlglot allow-list"]
E["engine.py<br/>read-only DuckDB<br/>timeout + row limits"]
P["profiling.py / charts.py"]
SB["sandbox.py<br/>path + size checks"]
SRV --> AN
AN --> G --> E
AN --> P
AN --> SB
end
DATA[("data dir<br/>csv / xlsx / parquet / sqlite")] -- "catalog.py: load once at startup" --> CAT[("private catalog.duckdb<br/>(temp, opened read-only)")]
E --> CAT
AN -- "PNG / CSV / XLSX" --> OUT[("output dir")]At startup, catalog.py finds the supported files in the data directory and loads each one into a private,
temporary DuckDB catalog file. Excel and SQLite files are read with pandas, and SQLite files are opened in mode=ro.
The engine then reopens that catalog read-only, with enable_external_access=false and lock_configuration=true.
Every query goes through sql_guard.py first.
Reference
Tools
Tool | Arguments | Returns |
| none | Every dataset: name, source file, format, sheet/table, row count, columns and types |
|
| Per-column type, null count and %, distinct count, plus sample rows |
|
|
|
|
| Numeric stats and histograms, top values, date ranges, top correlations, |
|
| Text summary and a PNG image content block; the file is saved to the output dir |
|
| Path, rows written, |
list_datasets, describe_dataset, query and profile are annotated readOnlyHint: true. create_chart and
export_result are marked non-destructive: they only create files in the output directory.
Resources
URI | Content |
| JSON list of all datasets and their schemas |
| JSON schema of one dataset (columns, types, row count, source) |
Prompts
Prompt | Arguments | Purpose |
|
| EDA plan: describe, profile, 3-5 targeted queries, charts, then a report backed by the numbers |
|
| Audit: profile issues, example rows for each issue, referential-integrity checks, a table of fixes |
Quickstart
Requires Python 3.12+.
git clone https://github.com/MuhammadAhmadRajpoot/mcp-data-analyst.git
cd mcp-data-analyst
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev,examples]"
# stdio (what desktop clients launch)
mcp-data-analyst --data-dir data/demo --output-dir output
# Streamable HTTP on http://127.0.0.1:8106/mcp
mcp-data-analyst --transport streamable-http --port 8106 --data-dir data/demo --output-dir outputDemo dataset
data/demo holds a small, fully synthetic e-commerce dataset (about 250 KB). It is generated from a fixed seed
by scripts/generate_demo_data.py and uses every supported format:
Table | File | Rows |
|
| 400 |
|
| 2,500 |
|
| 6,279 |
|
| 48 |
|
| 600 |
The generator adds some data-quality problems on purpose so that profile has something real to find:
14 missing emails, 9 duplicated order lines, 6 extreme quantities and 75 missing satisfaction scores.
Configuration
Each setting can be passed as a CLI flag or an environment variable. A flag overrides the variable.
Flag | Env var | Default |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
MCP_DA_MAX_PROFILE_ROWS (default 200,000) sets how many rows profile reads. Larger tables are
reservoir-sampled with a fixed seed.
Client configuration
Use absolute paths. Desktop clients do not start the server in the repository folder. The examples below
assume the repo is at /path/to/mcp-data-analyst. On Windows the executable is
C:\path\to\mcp-data-analyst\.venv\Scripts\mcp-data-analyst.exe (in JSON, escape each backslash as \\).
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"data-analyst": {
"command": "/path/to/mcp-data-analyst/.venv/bin/mcp-data-analyst",
"args": ["--data-dir", "/path/to/your/data", "--output-dir", "/path/to/mcp-data-analyst/output"]
}
}
}Claude Code:
# stdio
claude mcp add data-analyst -- /path/to/mcp-data-analyst/.venv/bin/mcp-data-analyst \
--data-dir /path/to/your/data --output-dir /path/to/mcp-data-analyst/output
# or connect to a running Streamable HTTP server
claude mcp add --transport http data-analyst http://127.0.0.1:8106/mcpCursor (~/.cursor/mcp.json or .cursor/mcp.json in a project):
{
"mcpServers": {
"data-analyst": {
"command": "/path/to/mcp-data-analyst/.venv/bin/mcp-data-analyst",
"args": ["--data-dir", "/path/to/your/data", "--output-dir", "/path/to/mcp-data-analyst/output"]
}
}
}To use a running HTTP server instead, replace command/args with "url": "http://127.0.0.1:8106/mcp".
Docker (Streamable HTTP; mount your own data read-only):
docker build -t mcp-data-analyst .
docker run --rm -p 8106:8106 -v /path/to/your/data:/data:ro mcp-data-analystDemo: a Groq agent as the MCP client
examples/groq_agent.py starts the server as a subprocess over stdio and lists its
tools. It converts each MCP tool's JSON Schema into an OpenAI-style function definition, then runs a
tool-calling loop against Groq's OpenAI-compatible API. The model is configurable with --model or GROQ_MODEL.
cp .env.example .env # add your GROQ_API_KEY
python examples/groq_agent.py "Show monthly revenue from delivered orders as a line chart saved as monthly_revenue.png, and tell me which month was highest."Transcript of a real run with openai/gpt-oss-120b. It has been re-wrapped to fit the page, the long tool payloads
are cut off with ..., and the answer's markdown is removed. The full output (with only the local path prefix removed) is in
docs/transcript.txt:
-> list_datasets({})
<- {"datasets": [{"name": "customers", "source": "customers.csv", "format": "csv", ...
-> query({"sql": "SELECT DATE_TRUNC('month', o.order_date) AS month,
SUM(oi.quantity * oi.unit_price * (1 - oi.discount)) AS revenue
FROM orders o JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.status = 'delivered' GROUP BY month ORDER BY month"})
<- {"columns": ["month", "revenue"], "column_types": ["TIMESTAMP", "DOUBLE"], "rows": [["2024-03-01T00:00:00", 53730.07...
-> create_chart({"chart_type": "line", "filename": "monthly_revenue.png", "sql": "SELECT DATE_TRUNC(...", "x": "month", "y": "revenue",
"title": "Monthly Revenue from Delivered Orders"})
<- {"saved_to": "output\\monthly_revenue.png", "rows_plotted": 21, "truncated": false}
[image content omitted]
A: - The line chart has been saved as monthly_revenue.png.
- The highest revenue month is November 2025, with a revenue of $120,155.06.Open-weight models sometimes produce a call to a tool that does not exist. Groq rejects these with
tool_use_failed, and the agent simply asks the model again (up to 3 attempts).
Security model
The server assumes the model, and anyone who can influence its prompts, may be hostile.
Threat | Mitigation |
Writing or deleting data | The SQL guard allows only a single |
Stacked statements ( | sqlglot parses the whole input and anything other than exactly one statement is rejected. |
Reading files outside the sandbox | Only registered dataset names (and CTE names) are valid table references. Quoted paths such as |
Re-enabling features at runtime |
|
Path traversal and symlinks | When the catalog is built, a data file is only registered if its resolved path is still inside the data dir, so symlinks pointing outside are skipped. Output file names must be bare names ( |
Resource exhaustion | Row caps for |
Leaking internals | Expected failures are |
Both layers are tested on their own: tests/test_security.py sends DDL, COPY, ATTACH, SET and read_csv
straight to the engine without the guard and checks that DuckDB refuses each one.
Not in scope: authentication for the HTTP transport. It binds to 127.0.0.1 by default, and the SDK's
DNS-rebinding protection is active for localhost. If you expose it more widely, put it behind an authenticating
proxy.
Running tests
python -m pytest # offline suite (integration test deselected by default)
python -m pytest -m integration # real Groq + stdio round trip, needs GROQ_API_KEY
ruff check . && ruff format --check .The offline suite calls every tool, resource and prompt through the SDK's in-memory client. It also covers the legacy initialize-handshake transport and runs the real console entry point as a stdio subprocess. The security tests cover the SQL guard (allowed and blocked queries), engine-level read-only enforcement, query timeouts, file size limits, path traversal and symlinks. The two symlink tests are skipped on Windows machines that do not allow creating symlinks.
Current result on Windows 11 / Python 3.13: 101 passed, 2 skipped offline, and 1 passed for the integration test.
Task commands
A Makefile wraps the common commands. Without make, run the command shown in the Makefile.
| Does |
|
|
| ruff check + format |
| offline suite / Groq integration test |
| regenerate |
| run over stdio / Streamable HTTP on port 8106 |
| run the Groq demo agent |
| build and run the container |
Screenshot
docs/screenshot.png was made from the real agent run shown above. scripts/render_screenshot.py combines
docs/transcript.txt, the chart the agent created and a live profile(order_items) call into an HTML page.
That page was then captured with npx -y playwright screenshot --channel msedge --viewport-size=1440,690 ....
Project structure
mcp-data-analyst/
├── src/mcp_data_analyst/
│ ├── cli.py # argparse + env -> Settings, transport selection
│ ├── config.py # pydantic-settings (MCP_DA_*)
│ ├── server.py # MCPServer: tools, resources, prompts (thin async wrappers)
│ ├── analyst.py # tool implementations, transport-agnostic
│ ├── catalog.py # file discovery -> private DuckDB catalog
│ ├── engine.py # read-only DuckDB, timeouts, row limits, JSON conversion
│ ├── sql_guard.py # sqlglot validation
│ ├── sandbox.py # data-dir discovery, safe output paths
│ ├── profiling.py # stats, histograms, correlations, quality issues
│ ├── charts.py # matplotlib (object API, headless)
│ ├── prompts.py # prompt templates
│ ├── models.py # pydantic result models (tool output schemas)
│ ├── errors.py # ToolError subclasses
│ └── logging_config.py # JSON logs to stderr
├── examples/groq_agent.py # MCP client + Groq tool calling
├── scripts/ # demo data generator, screenshot renderer
├── data/demo/ # synthetic dataset (csv, parquet, xlsx, sqlite)
├── tests/ # pytest suite (in-memory MCP client, security, stdio)
├── docs/ # screenshot + real transcript
├── Dockerfile, Makefile, pyproject.toml, .github/workflows/ci.ymlRoadmap
Pick up new or changed files without a restart (a
refreshtool or a file watcher withresources/list_changed).Optional lazy views instead of loading every file at startup, for large Parquet folders.
More chart types (grouped/stacked bars, box plots) and a series/colour column.
Authentication options for the HTTP transport (the SDK's OAuth support).
Row-level filters or column masking for sensitive columns.
License
MIT © 2026 Muhammad Ahmad
Author
Muhammad Ahmad, AI/ML Engineer
Portfolio: https://muhammad-ahmad-rajpoot.vercel.app/
LinkedIn: https://www.linkedin.com/in/muhammad-ahmad-a31b04322/
GitHub: @MuhammadAhmadRajpoot
This server cannot be deployed
Maintenance
Related MCP Connectors
Query, join, profile, clean and convert CSV/JSON/Parquet with server-side DuckDB over MCP.
Query your warehouse or a CSV with Claude/ChatGPT over MCP, governed by table-level ACL + audit.
Query your org's data in natural language — read-only MCP access to SQL, NoSQL, files & warehouses.
Query 40 databases from Claude, ChatGPT, or Cursor — on any device. Read-only, encrypted, audited.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceA secure, read-only MCP server that empowers Claude Desktop and AI agents to safely query and inspect local SQLite databases.-
- FlicenseNot gradedqualityBmaintenanceEnables AI assistants and MCP clients to ask natural-language questions about DuckDB or CSV data and receive safe, read-only SQL-generated tabular insights with automatic schema discovery and multi-table joins.1-
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to run SQL analytics directly on CSV files in place via MCP, avoiding imports and token-heavy data pasting. It provides read-only querying, schema inspection, and file listing while keeping data local.30MIT
- AlicenseAqualityCmaintenanceEnables read-only analytical access to local CSV, Parquet, JSON files or a DuckDB database through MCP, with table discovery, column profiling, guarded SQL querying, and explain plans for safe use with Claude Desktop and Claude Code.81MIT