Skip to main content
Glama
tabulite-mcp

tabulite-mcp

by davidmrguo

Tabulite MCP

分析那些大得无法用电子表格打开、也大得无法粘贴到聊天窗口中的 CSV 文件——方法是给你的 AI 助手一个本地 SQLite 运行时,而不是数据本身。

Tabulite MCP(简称 Tabulite)是一个本地 MCP 服务器。把它指向一个 CSV 文件夹,你的桌面 AI 客户端就可以将文件导入 SQLite,检查各列实际包含的内容,并通过编写 SQL 来回答问题——你的数据不会有一行离开你的机器,或进入对话。

Desktop AI client  →  MCP  →  Tabulite  →  sqlite3  →  your CSV files
   (the reasoning)                (safe, deterministic tools)

服务器内部没有 LLM。思考由你的 AI 客户端完成;Tabulite 为它提供可供思考的元数据、一个可探索的只读 SQL 接口,以及当答案是数据集而非一句话时直通磁盘的路径。


为什么

向 AI 询问一个 500 MB 的 CSV,你面临的选择都不怎么样:粘贴样本会丢失答案,上传整个文件会烧掉你的上下文窗口(还会把数据发到某个地方),或者自己动手写脚本。

单台机器加单个 SQLite 文件应付这个体量绰绰有余。Tabulite 把这个运行时放在数据旁边,并通过 MCP 暴露出来。你的助手读取几百个 token 的列概要,编写 SQL,然后拿回聚合结果。数据行始终留在磁盘上。

适合的场景: 在自己的笔记本上对 CSV 导出、日志转储和提取数据进行一次性分析——这些文件已经超出了 Excel 的承受范围,但仍应放在一台机器上。 不适合的场景: 生产管道、定时 ETL、多用户访问,或任何属于真正数据仓库的东西。


Related MCP server: mix_server

快速开始

系统要求: Docker Desktop(或 Docker Engine + Compose)。仅此而已——无需安装 Python。

git clone https://github.com/davidmrguo/tabulite-mcp.git
cd tabulite-mcp
docker compose up --build

服务器现在运行在 http://localhost:8000/mcp,健康检查地址为 http://localhost:8000/health。

仓库自带两个小样本 CSV(source/sales.csv、source/customers.csv),你可以立即试用。先连接你的 AI 客户端(见下文),然后提问:

“分析 sales.csv。哪个渠道带来的收入最高?”

你的助手会调用 list_sources()、import_source("sales.csv")、profile_table("sales"),然后写出类似这样的内容:

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;

连接你的 AI 客户端

Claude Code

claude mcp add --transport http tabulite http://localhost:8000/mcp

任何使用 JSON 配置的客户端(Claude Desktop、Cursor 等):

{
  "mcpServers": {
    "tabulite": {
      "type": "http",
      "url": "http://localhost:8000/mcp"
    }
  }
}

只支持 stdio 的客户端: 在 URL 前面加一个桥接器,例如 mcp-remote。

使用你自己的数据

把 CSV 文件放进 source/ 即可——就是这样,无需重启:

cp ~/Downloads/huge_export.csv source/

你的文件以 只读 方式挂载,并被 gitignore 忽略,因此它们永远不会被提交,服务器也永远无法修改它们。Tabulite 创建的一切(数据库、导出)都会放在 workspace/ 中。


你的 AI 可用的工具

工具

作用

list_sources()

source/ 下的 CSV 文件,含大小和导入状态

inspect_source(path)

列、分隔符和几行示例数据——无需导入

import_source(path, table_name?, delimiter?, force?)

将 CSV 流式导入 SQLite 并对其做概要分析

list_tables()

已导入的表,含行数和来源

profile_table(table_name, refresh?)

每列的简明概要

profile_column(table_name, column_name)

单列的完整详情,含示例

sample_table(table_name, limit=20)

几行数据,用于查看数据长什么样

query_sql(sql)

只读分析型 SQL(上限 1,000 行)

export_query(sql, file_name?, format="csv")

将完整结果流式写入文件

值得注意的是,工具列表里没有任何领域特定功能。没有 top_products(),也没有 calculate_revenue()。SQL 由你的助手编写,这正是重点所在——它可以回答任何人事先没有预料到的问题。


工作原理

CSV 字段有意以 TEXT 存储

每个导入的列都是 TEXT:

CREATE TABLE sales (
    transaction_id   TEXT,
    transaction_date TEXT,
    revenue          TEXT,
    quantity         TEXT
);

在导入时猜测类型,会在任何人查看数据之前就破坏数据:"1,234" 变成 1,带前导零的产品编码变成整数,"2025-13-40" 在无声无息中变成 NULL。因此,存储层保留文件原本的内容,解释发生在之后——在那个阶段,它可见且可逆。

概要告诉 AI 各列的含义

导入后,每一列都会被分析,结果存储在 workspace/catalog.sqlite 中。以下是内置样本的真实输出:

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_REAL

profile_column("sales", "revenue") 更进一步,展示真正的异常值:invalid_examples: ["pending", "unknown"]。

推断是保守的——只有当 ≥99% 的非 NULL 值都能按某种类型解析时,才会指派该类型。概要只是给 AI 的证据,绝不是对存储层的指示:你导入的数据绝不会为了迎合某个猜测而被重写。

使用 TRY_* 函数而非 CAST

SQLite 的 CAST 宽松得危险:

CAST('unknown' AS REAL)    -- 0.0   ← quietly wrong
CAST('12 apples' AS REAL)  -- 12.0  ← quietly wrong

对包含几千个 'unknown' 值的列执行 AVG(),会在无声无息中把零值纳入平均。因此,Tabulite 会在每个连接上注册严格转换函数:

TRY_REAL('125.5')    -- 125.5
TRY_REAL('')         -- NULL
TRY_REAL('unknown')  -- NULL

此外还提供:TRY_INTEGER、TRY_DATE、TRY_DATETIME、TRY_BOOLEAN。由于 SQLite 的聚合函数会跳过 NULL,坏值会被排除,而不是按零计算——你的助手还可以检查分母:

SELECT AVG(TRY_REAL(revenue)) AS average_revenue,
       COUNT(TRY_REAL(revenue)) AS valid_rows,   -- 462
       COUNT(*)                 AS total_rows    -- 500
FROM sales;

缺失数据与无效数据始终可区分

只有配置过的缺失值标记才会变成 SQL NULL。仅仅无法解析的值会原样保留:

CSV 值

存储为

125.40

"125.40"

(空)

NULL

N/A

NULL

unknown

"unknown"

-

"-"

默认标记:空字符串、NULL、null、N/A、NA。“这个字段是空白的”和“这个字段里是垃圾数据”是两种不同的发现;如果在导入时把它们混为一谈,就会掩盖一个值得关注的数据质量问题。

文件按内容而非名称识别

把 sales.csv 改名为 sales_FINAL_v2.csv 再导入一次,Tabulite 会识别出相同内容,并复用已有表,而不是再复制一份。文件身份由 SHA-256 决定,该哈希在导入过程中计算,而不是单独读一遍。只要改动一个字节,它就会成为一个带有新表的新数据源。

所有导入的内容都存放在同一个数据库(workspace/databases/main.sqlite)中,因此你的助手可以用普通 SQL 跨文件联表。当两个文件要使用同一个表名时——例如两个不同文件夹里各有一个 sales.csv——第二个文件会获得源自自身内容哈希的后缀(sales 和 sales_4b11d3)。这意味着,无论导入顺序如何,同一个文件总会落到同一个表名上。

大结果写入磁盘,而非进入聊天

query_sql() 最多返回 1,000 行,并且总会说明这一点("truncated": true),这是在提示你:应在 SQL 中做聚合,而不是把大结果分页搬进对话。病态查询——意外的笛卡尔积、无界递归 CTE——会在超时后被取消。

当用户真的需要这些行时,export_query() 会执行同样的只读 SQL,没有行数上限,并将游标直接流式写入 workspace/exports/ 下的文件:

“给我 2025 年所有超过 $1,000 的邮件交易,并导出它们。”

你的助手构造查询、调用 export_query(),然后把路径交回来——这是内置样本数据上的真实结果:

{"file_name": "email_2025_high_value.csv",
 "relative_path": "exports/email_2025_high_value.csv",
 "row_count": 58, "file_size_bytes": 3084}

无论是服务器还是对话,都从未持有完整结果,因此无论 58 行还是 500 万行,它的工作方式都一样。


安全性

你的 CSV 永远不会被修改。 source/ 在 Docker 层面以只读方式挂载。所有写入都进入 workspace/。

每一条 AI 生成的查询都是只读的,通过四层机制强制保证:

  1. 连接以 file:…?mode=ro 方式打开,因此操作系统会以只读方式持有该文件;

  2. PRAGMA query_only=ON 让 SQLite 本身拒绝在该句柄上的写入;

  3. 显式禁用扩展加载;

  4. set_authorizer() 回调只允许 SQLITE_SELECT、SQLITE_READ、SQLITE_FUNCTION(将可触及文件系统的内置函数排除在外)和 SQLITE_RECURSIVE,拒绝其他一切操作——写入、schema 变更、ATTACH/DETACH、所有 PRAGMA、事务控制、维护操作。

第 4 层才是真正的机制:它在语句准备期间运行于 SQLite 内部,因此它判断的是查询实际做什么,而不是其文本如何拼写。一个 SQL 清洗器位于它前面,既作为纵深防御,也为了让模型看到可读的错误信息(only read-only statements are allowed; found 'DROP'),而不是光秃秃的 not authorized。

这种区分是双向的,测试套件也对此做了固定:CASE … END 和 replace() 标量函数是普通的分析型 SQL,照常工作;而 REPLACE INTO、PRAGMA writable_schema = ON 和 load_extension() 则会被拒绝。

路径受到严格限制。 服务器只读取 source/ 内部,只写入 workspace/exports/ 内部。目录穿越(../)、绝对路径以及指向项目外部的符号链接都会被拒绝;导出文件名会被清理,已有的导出文件绝不会被覆盖。

无身份验证——这是有意为之。容器只发布到 127.0.0.1,面向同一台机器上的客户端。不要将其暴露到网络上。


配置

全部可选;在 compose.yaml 中设置。

变量

默认值

作用

TABULITE_SOURCE_DIR

/project/source

只读源目录

TABULITE_WORKSPACE_DIR

/project/workspace

可写工作区

TABULITE_NULL_MARKERS

,NULL,null,N/A,NA

作为 SQL NULL 导入的值

TABULITE_MAX_QUERY_ROWS

1000

交互式行数上限

TABULITE_QUERY_TIMEOUT

30

查询被取消前的秒数

TABULITE_EXPORT_TIMEOUT

600

导出被取消前的秒数

TABULITE_BATCH_SIZE

5000

导入时每次 executemany() 处理的行数

TABULITE_HOST / TABULITE_PORT

0.0.0.0 / 8000

容器内的绑定地址

TABULITE_ALLOWED_ORIGINS

localhost origins

Origin 白名单(DNS 重绑定防护)


项目结构

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
│   ├── 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.yaml

开发

无需 Docker 运行:

python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
TABULITE_SOURCE_DIR=./source TABULITE_WORKSPACE_DIR=./workspace tabulite-mcp

运行测试:

pytest

211 个测试覆盖了源发现与目录穿越拒绝、流式导入、NULL 与无效数据区分、SHA-256 身份识别(包括重命名和修改过的文件)、确定性表命名、概要分析与类型推断、TRY_* 函数、AVG 忽略无效值、SELECT/GROUP BY/CTE/join/window 查询、结果行数限制、查询取消、清洗器和授权器两层上的只读强制、CSV 与 JSON 导出、导出流式化、文件名清理,以及通过真实进程内 MCP 会话调用工具。

技术栈: Python 3.11+、标准库的 sqlite3,以及官方 MCP Python SDK,版本锁定为 mcp==2.1.1(v2 API:MCPServer,run() 上指定 host/port)。不使用 pandas、NumPy 或 ORM——核心是大家一看就熟的普通 Python:sqlite3.connect()、conn.executemany()、conn.create_function()、cursor.fetchmany()。

规模: 一个 133 MB / 2,000,000 行的 CSV 导入和剖析大约需要两分钟,容器内存稳定在 100 MB 左右;对其聚合操作只需几秒。导入受磁盘限制,而非内存。


故障排除

端口 8000 已被占用 — 修改 compose.yaml 中映射的主机侧("127.0.0.1:8001:8000"),并将客户端指向新端口。

客户端无法连接 — 先用 curl http://localhost:8000/health 检查服务是否已启动,然后运行 docker compose logs -f。

向 workspace/ 写入时出现权限错误(Linux) — 取消注释 compose.yaml 中的 user: 行,以便文件以你的身份而不是容器用户身份创建。

source/ 中的某个文件未列出 — 只会发现 .csv 和 .tsv 文件,并跳过以点开头的文件。

编辑 CSV 后出现“未知表” — 修改文件会改变其哈希值,因此请重新运行 import_source();新内容会获得自己的表。


不在范围内

没有嵌入式 LLM,没有服务器端的自然语言转 SQL,没有任意 Python 代码执行,没有 pandas/NumPy/matplotlib,没有 Excel、DuckDB、Polars 或 Parquet,没有嵌入或向量搜索,没有云部署、身份验证、多用户支持或后台任务。你的 AI 客户端已经是界面和推理层。

许可证

MIT — 你可以随意使用,只需保留版权声明。

欢迎贡献,贡献内容将在同一许可证下被接受(无需 CLA,无需版权转让)。版权归代码作者所有,这是有意为之:该项目旨在保持为开源项目,而非成为某个人的产品。

Related MCP Connectors

Related MCP Servers

  • F
    license
    B
    quality
    D
    maintenance
    Enables Claude to directly access, query, and analyze local CSV files using natural language, keeping data private and local.
    4
    1
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants to interact with local CSV and Parquet data files through natural language queries, facilitating tasks like summarizing datasets or retrieving specific information.
    4
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables 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.
    30
    MIT