tabulite-mcp
Click on "Install 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., "@tabulite-mcpAnalyze sales.csv. Which channel generated the most revenue?"
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.
Tabulite MCP
Analyze CSV files that are too big for a spreadsheet — and too big to paste into a chat — by giving your AI assistant a local SQLite runtime instead of the data.
Tabulite MCP — Tabulite for short — is a local MCP server. Point it at a folder of CSV files, and your desktop AI client can import them into SQLite, inspect what the columns actually contain, and answer questions by writing SQL — without a single row of your data leaving your machine or entering the conversation.
Desktop AI client → MCP → Tabulite → sqlite3 → your CSV files
(the reasoning) (safe, deterministic tools)There is no LLM inside the server. Your AI client does the thinking; Tabulite gives it metadata to think about, a read-only SQL interface to explore with, and a direct path to disk when the answer is a dataset rather than a sentence.
Why
Ask an AI about a 500 MB CSV and you have bad options: paste a sample and lose the answer, upload the whole thing and burn your context window (and send your data somewhere), or go write a script yourself.
A single machine and a single SQLite file handle this size without breaking a sweat. Tabulite puts that runtime next to the data and exposes it over MCP. Your assistant reads a few hundred tokens of column profiles, writes the SQL, and gets back aggregates. The rows stay on disk.
Good fit: one-off analysis of CSV exports, log dumps and extracts on your own laptop — files that outgrew Excel but still belong on one machine. Not a fit: production pipelines, scheduled ETL, multi-user access, or anything that belongs in a real data warehouse.
Related MCP server: csv-mcp-server
Quickstart
Requirements: Docker Desktop (or Docker Engine + Compose). Nothing else — no Python setup needed.
git clone https://github.com/davidmrguo/tabulite-mcp.git
cd tabulite-mcp
docker compose up --buildThe server is now on http://localhost:8000/mcp, with a health check at
http://localhost:8000/health.
Two small sample CSVs (source/sales.csv, source/customers.csv) ship with the
repo so you can try it immediately. Connect your AI client (below), then ask:
"Analyze sales.csv. Which channel generated the most revenue?"
Your assistant will call list_sources(), import_source("sales.csv"),
profile_table("sales"), and then write something like:
SELECT channel,
SUM(TRY_REAL(revenue)) AS revenue,
COUNT(TRY_REAL(revenue)) AS valid_rows,
COUNT(*) AS total_rows
FROM sales
GROUP BY channel
ORDER BY revenue DESC;Connect your AI client
Claude Code
claude mcp add --transport http tabulite http://localhost:8000/mcpAny client with a JSON config (Claude Desktop, Cursor, and similar):
{
"mcpServers": {
"tabulite": {
"type": "http",
"url": "http://localhost:8000/mcp"
}
}
}Clients that only speak stdio: put a bridge such as
mcp-remote in front of the URL.
Use your own data
Drop CSV files into source/ — that's it, no restart needed:
cp ~/Downloads/huge_export.csv source/Your files are mounted read-only and are gitignored, so they never get
committed and the server can never modify them. Everything Tabulite creates
(databases, exports) lands in workspace/.
The tools your AI gets
Tool | What it does |
| CSV files under |
| columns, delimiter and a few sample rows — without importing |
| stream a CSV into SQLite and profile it |
| imported tables with row counts and where they came from |
| compact profile of every column |
| full detail for one column, with examples |
| a few rows, to see what the data looks like |
| read-only analytical SQL (capped at 1,000 rows) |
| complete result streamed to a file |
| permanently remove an imported table — two-step, see below |
Notably absent: anything domain-specific. There is no top_products() or
calculate_revenue(). Your assistant writes the SQL, which is the whole point —
it can answer questions nobody anticipated.
How it works
CSV fields are stored as TEXT, on purpose
Every imported column is TEXT:
CREATE TABLE sales (
transaction_id TEXT,
transaction_date TEXT,
revenue TEXT,
quantity TEXT
);Guessing types at import time destroys data before anyone has looked at it:
"1,234" becomes 1, a leading-zero product code becomes an integer,
"2025-13-40" silently becomes NULL. So storage keeps what the file said, and
interpretation happens later, where it is visible and reversible.
Profiles tell the AI what the columns mean
After import, every column is profiled and the result is stored in
workspace/catalog.sqlite. Here is the real output for the bundled sample:
column logical_type confidence nulls invalid recommended_cast
transaction_id TEXT 1.000 0 0 none
transaction_date DATE 1.000 0 0 TRY_DATE
customer TEXT 1.000 0 0 none
product TEXT 1.000 0 0 none
channel TEXT 1.000 9 0 none
quantity INTEGER 0.996 0 2 TRY_INTEGER
revenue REAL 0.996 36 2 TRY_REALprofile_column("sales", "revenue") goes further and shows the actual offenders:
invalid_examples: ["pending", "unknown"].
Inference is conservative — a type is assigned only when ≥99% of non-null values parse as it. Profiles are evidence for the AI, never an instruction to the storage layer: your imported data is never rewritten to match a guess.
TRY_* functions instead of CAST
SQLite's CAST is dangerously permissive:
CAST('unknown' AS REAL) -- 0.0 ← quietly wrong
CAST('12 apples' AS REAL) -- 12.0 ← quietly wrongAn AVG() over a column with a few thousand 'unknown' values silently
averages in zeros. So Tabulite registers strict conversions on every connection:
TRY_REAL('125.5') -- 125.5
TRY_REAL('') -- NULL
TRY_REAL('unknown') -- NULLAlso available: TRY_INTEGER, TRY_DATE, TRY_DATETIME, TRY_BOOLEAN.
Because SQLite's aggregates skip NULL, bad values are excluded rather than
counted as zero — and your assistant can check the denominator:
SELECT AVG(TRY_REAL(revenue)) AS average_revenue,
COUNT(TRY_REAL(revenue)) AS valid_rows, -- 462
COUNT(*) AS total_rows -- 500
FROM sales;Missing and invalid data stay distinguishable
Only configured missing-value markers become SQL NULL. Values that merely fail
to parse are kept exactly as written:
CSV value | Stored as |
|
|
(empty) |
|
|
|
|
|
|
|
Default markers: empty string, NULL, null, N/A, NA. "This field was
blank" and "this field contained garbage" are different findings, and collapsing
them at import time would hide a data-quality problem worth seeing.
Files are identified by content, not by name
Rename sales.csv to sales_FINAL_v2.csv, import it again, and Tabulite
recognizes the content and reuses the existing table instead of duplicating it.
Identity is the SHA-256 of the file, computed during the import pass rather
than in a separate read. Change one byte and it becomes a new source with its
own table.
Everything imported lives in one database (workspace/databases/main.sqlite) so
your assistant can join across files with ordinary SQL. When two files would
claim the same table name — say a sales.csv in two different folders — the
second gets a suffix from its own content hash (sales and sales_4b11d3),
which means a given file always lands on the same table name regardless of
import order.
Big results go to disk, not into the chat
query_sql() returns at most 1,000 rows and always says so
("truncated": true), which is a nudge to aggregate in SQL rather than paginate
a large result into the conversation. Pathological queries — an accidental
Cartesian product, an unbounded recursive CTE — are canceled after a timeout.
When the user actually wants the rows, export_query() runs the same read-only
SQL with no row cap and streams the cursor straight into a file under
workspace/exports/:
"Give me all email transactions from 2025 over $1,000 and export them."
Your assistant builds the query, calls export_query(), and hands back the
path — this is the real result on the bundled sample data:
{"file_name": "email_2025_high_value.csv",
"relative_path": "exports/email_2025_high_value.csv",
"row_count": 58, "file_size_bytes": 3084}Neither the server nor the conversation ever holds the whole result, so this works the same way at 58 rows or 5 million.
Deleting a table
Imports are cheap to redo but expensive to lose, so delete_table() is
deliberately two-step. Ask your assistant to delete a table and the first call
deletes nothing — it returns a warning saying exactly what would go (row count,
columns, profiles) and, importantly, whether the original CSV is still in
source/ to re-import from:
This will permanently delete the table 'sales' (500 rows, 7 columns) along with its column profiles and its entry in the catalog. The source file sales.csv is still in source/, so the table could be rebuilt with import_source() afterwards.
You then have to type DELETE in capitals. Nothing else counts — not
"yes", not "go ahead", not lowercase "delete". Your assistant passes that word
back along with a single-use token from the warning, and only then is the table
dropped, its profiles and catalog entry removed, and the database compacted so
the disk space actually comes back.
The two-step design is enforced by the server, not by the model's good
manners: no single call can delete anything, because the token only exists
once a warning has been issued. What a server cannot verify is that a human
typed the word rather than the model — so treat the warning in your chat as
the real checkpoint. Your CSV in source/ and anything already written to
workspace/exports/ are never touched.
Safety
Your CSVs are never modified. source/ is mounted read-only at the Docker
level. Everything written goes to workspace/.
Every AI-generated query is read-only, enforced in four layers:
the connection is opened
file:…?mode=ro, so the OS holds the file read-only;PRAGMA query_only=ONmakes SQLite itself refuse writes on that handle;extension loading is disabled explicitly;
a
set_authorizer()callback allows onlySQLITE_SELECT,SQLITE_READ,SQLITE_FUNCTION(minus filesystem-reaching builtins) andSQLITE_RECURSIVE, denying everything else — writes, schema changes,ATTACH/DETACH, everyPRAGMA, transaction control, maintenance.
Layer 4 is the real mechanism: it runs inside SQLite during statement
preparation, so it judges what a query does, not how its text is spelled. A
SQL scrubber sits in front of it as defense in depth and to give the model a
readable error (only read-only statements are allowed; found 'DROP') instead
of a bare not authorized.
This distinction cuts both ways, and the test suite pins it: CASE … END and the
replace() scalar function are ordinary analytical SQL and keep working, while
REPLACE INTO, PRAGMA writable_schema = ON and load_extension() are rejected.
Paths are contained. The server only reads inside source/ and only writes
inside workspace/exports/. Traversal (../), absolute paths and symlinks
pointing outside the project are rejected; export filenames are sanitized and an
existing export is never overwritten.
No authentication — by design. The container publishes to 127.0.0.1 only
and is meant for a client on the same machine. Don't expose it to a network.
Configuration
All optional; set them in compose.yaml.
Variable | Default | What it controls |
|
| read-only source directory |
|
| writable workspace |
|
| values imported as SQL NULL |
|
| interactive row cap |
|
| seconds before a query is canceled |
|
| seconds before an export is canceled |
|
| rows per |
|
| bind address inside the container |
| localhost origins | Origin allow-list (DNS-rebinding protection) |
Project layout
tabulite-mcp/
├── source/ # your CSV files (read-only mount, gitignored)
├── workspace/ # everything generated (gitignored)
│ ├── catalog.sqlite # source, import, profile and export metadata
│ ├── databases/main.sqlite# the imported analytical tables
│ └── exports/ # query results written to disk
├── src/tabulite_mcp/
│ ├── server.py # the MCP tools
│ ├── config.py # paths and limits
│ ├── confirm.py # two-step confirmation for destructive tools
│ ├── security.py # path containment + read-only enforcement
│ ├── database.py # connections, row caps, cancellation
│ ├── importer.py # streaming CSV → SQLite
│ ├── profiler.py # logical type inference
│ ├── casting.py # TRY_* functions
│ ├── catalog.py # catalog.sqlite
│ └── exporter.py # streaming results to files
├── tests/
├── Dockerfile
└── compose.yamlDevelopment
Run it without Docker:
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
TABULITE_SOURCE_DIR=./source TABULITE_WORKSPACE_DIR=./workspace tabulite-mcpRun the tests:
pytest232 tests cover source discovery and traversal rejection, streamed import, NULL
vs invalid handling, SHA-256 identity (including renamed and modified files),
deterministic table naming, profiling and type inference, the TRY_* functions,
AVG ignoring invalid values, SELECT/GROUP BY/CTE/join/window queries, result
limits, query cancellation, read-only enforcement at both the scrubber and
authorizer layers, CSV and JSON export, export streaming, filename sanitization,
the two-step delete confirmation, and tool invocation over a real in-process
MCP session.
Stack: Python 3.11+, the standard library's sqlite3, and the official MCP
Python SDK pinned at mcp==2.1.1 (v2 API: MCPServer, host/port on run()).
No pandas, no NumPy, no ORM — the core is recognizably ordinary Python:
sqlite3.connect(), conn.executemany(), conn.create_function(),
cursor.fetchmany().
Scale: a 133 MB / 2,000,000-row CSV imports and profiles in about two minutes with container memory flat around 100 MB; aggregating over it takes a couple of seconds. Import is bounded by disk, not RAM.
Troubleshooting
Port 8000 already in use — change the host side of the mapping in
compose.yaml ("127.0.0.1:8001:8000") and point your client at the new port.
Client can't connect — check the server is up with
curl http://localhost:8000/health, then docker compose logs -f.
Permission errors writing to workspace/ (Linux) — uncomment the user:
line in compose.yaml so files are created as you rather than as the container
user.
A file in source/ isn't listed — only .csv and .tsv are discovered, and
dotfiles are skipped.
"unknown table" after editing a CSV — changing a file changes its hash, so
re-run import_source(); the new content gets its own table.
Not in scope
No embedded LLM, no natural-language-to-SQL in the server, no arbitrary Python execution, no pandas/NumPy/matplotlib, no Excel, DuckDB, Polars or Parquet, no embeddings or vector search, no cloud deployment, authentication, multi-user support or background jobs. Your AI client is already the interface and the reasoning layer.
Contributing
Bug reports, questions and pull requests are all welcome.
Something broken? Open an issue.
A question, or an idea you want to talk through? Discussions is the place for anything open-ended.
Want to send code? Read CONTRIBUTING.md first — especially the scope section, which will tell you quickly whether an idea fits before you write it.
Found a security problem? Please don't open a public issue. SECURITY.md explains how to report it privately.
Everyone taking part is expected to follow the Code of Conduct.
License
MIT — do what you like with it, keep the notice.
Contributions are accepted under the same license (no CLA, no copyright assignment). Copyright stays with the people who wrote the code, which is deliberate: this is meant to stay an open source project rather than become someone's product.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceAI-first CSV analysis tool that enables AI agents to analyze, query, and audit large CSV files directly within conversations, turning raw data into actionable insights.2
- FlicenseBqualityDmaintenanceEnables Claude to directly access, query, and analyze local CSV files using natural language, keeping data private and local.41
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to interact with local CSV and Parquet data files through natural language queries, facilitating tasks like summarizing datasets or retrieving specific information.5
- AlicenseNot gradedqualityDmaintenanceEnables querying Excel and CSV files using SQL via natural language, allowing AI assistants to analyze data without manual SQL writing.1MIT
Related MCP Connectors
Explore, query, and inspect SQLite databases with ease. List tables, preview results, and view det…
Connect AI assistants to your GitHub-hosted Obsidian vault to seamlessly access, search, and analy…
Query PostgreSQL databases in plain English — LLM-generated, safety-validated SQL.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/davidmrguo/tabulite-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server