Skip to main content
Glama
davidmrguo

tabulite-mcp

by davidmrguo

Tabulite MCP

スプレッドシートには大きすぎ、チャットに貼り付けるにも大きすぎるCSVファイルを分析するには、データそのものではなく、ローカルのSQLiteランタイムをAIアシスタントに渡します。

Tabulite MCP(略称Tabulite)は、ローカルMCPサーバーです。CSVファイルのフォルダを指定すると、デスクトップのAIクライアントがそれらをSQLiteにインポートし、カラムに実際に何が入っているかを調べ、SQLを書いて質問に答えることができます — データの1行たりともマシンの外に出たり、会話に入り込んだりすることはありません。

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

サーバーの中にLLMはありません。考えるのはAIクライアントです。Tabuliteは、考えるためのメタデータ、探索のための読み取り専用SQLインターフェース、そして答えが文章ではなくデータセットである場合のディスクへの直接パスを提供します。


なぜ

500 MBのCSVについてAIに尋ねるとき、選択肢はどれも悪いものばかりです:サンプルを貼り付ければ答えを失い、全体をアップロードすればコンテキストウィンドウを消費し(そしてデータをどこかに送ることになり)、自分でスクリプトを書くことになります。

1台のマシンと1つのSQLiteファイルがあれば、このサイズでも余裕で処理できます。Tabuliteはそのランタイムをデータの隣に配置し、MCP経由で公開します。アシスタントは数百トークンのカラムプロファイルを読み、SQLを書き、集計結果を受け取ります。行データはディスクに残ったままです。

向いている用途: 自分のラップトップでのCSVエクスポート、ログダンプ、抽出データの単発分析 — Excelでは扱いきれなくなったが、1台のマシンに収まるべきファイル。 向いていない用途: 本番パイプライン、定期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 にあります。

2つの小さなサンプルCSV(source/sales.csv、source/customers.csv)がリポジトリに同梱されているので、すぐに試せます。AIクライアントを接続し(下記)、次のように尋ねてみてください:

「sales.csvを分析して。どのチャネルが最も収益を生み出した?」

アシスタントは list_sources()、import_source("sales.csv")、profile_table("sales") を呼び出し、次のようなSQLを書きます:

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が使えるツール

Tool

機能

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)

1つのカラムの詳細(例付き)

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"]。

推論は控えめです — 非NULL値の99%以上がその型としてパースできる場合にのみ型が割り当てられます。プロファイルはAIへの証拠であり、ストレージ層への指示ではありません。インポート済みデータが推測に合わせて書き換えられることは決してありません。

CAST の代わりに TRY_* 関数

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であり、別途の読み取りではなくインポート処理の最中に計算されます。1バイト変えると、独自のテーブルを持つ新しいソースになります。

インポートされたものはすべて1つのデータベース(workspace/databases/main.sqlite)に格納されるため、アシスタントは通常のSQLでファイルをまたいでJOINできます。2つのファイルが同じテーブル名を名乗る場合 — たとえば異なる2つのフォルダに sales.csv がある場合 — 2つ目は自身のコンテンツハッシュから接尾辞を取得します(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が生成したすべてのクエリは読み取り専用であり、4つの層で強制されます:

  1. 接続は file:…?mode=ro として開かれるため、OSがファイルを読み取り専用で保持します;

  2. PRAGMA query_only=ON により、SQLite自体がそのハンドルへの書き込みを拒否します;

  3. 拡張機能の読み込みが明示的に無効化されます;

  4. set_authorizer() コールバックは SQLITE_SELECT、SQLITE_READ、SQLITE_FUNCTION(ファイルシステムに到達する組み込み関数を除く)、SQLITE_RECURSIVE のみを許可し、それ以外のすべて — 書き込み、スキーマ変更、ATTACH/DETACH、すべての PRAGMA、トランザクション制御、メンテナンス — を拒否します。

第4層が実際のメカニズムです。これはステートメントの準備中にSQLite内部で実行されるため、クエリのテキストがどう綴られているかではなく、クエリが何を行うかを判断します。その前にSQLスクラバーが多層防御として配置され、モデルに素の not authorized ではなく読みやすいエラー(only read-only statements are allowed; found 'DROP')を返します。

この区別は両方向に働き、テストスイートがそれを固定しています: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

オリジン許可リスト(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==2.1.1 に固定された公式MCP Python SDK(v2 API:MCPServer、ホスト/ポートは run() で指定)。pandasもNumPyもORMもなし — 中核は一目でわかる普通のPythonです:sqlite3.connect()、conn.executemany()、conn.create_function()、cursor.fetchmany()。

スケール: 133 MB / 2,000,000行のCSVは、コンテナメモリが約100 MBでほぼ一定のまま、約2分でインポートおよびプロファイリングが完了します。集計には数秒かかります。インポートはRAMではなくディスクに依存します。


トラブルシューティング

ポート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
    A
    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