DB Explorer について
DB Explorerは、PostgreSQL、MySQL、SQLiteに対応する読み取り専用MCPサーバーです。AIコーディングエージェント向けにスキーマ確認やクエリ、マイグレーションのレビューを提供し、単一の読み取り専用SELECT以外を解析段階で拒否して結果件数も制限します。
主な機能
- 01テーブル、ビュー、列、キー、インデックスの確認
- 02SELECTの解析と件数制限付き実行
- 03ネイティブな実行計画の表示
- 04スキーマ問題の検出とインデックス提案
- 05実行せずにマイグレーションスクリプトを検証
活用シーン
- 01自然言語でリレーショナルDBのスキーマを調査
- 02データを検索して遅いクエリを調査
- 03主キーや外部キーのインデックス不足を特定
- 04データベース移行の作成とレビュー
MCP 対応
MCPサーバーは7つのデータベースツールを公開し、SQLAlchemy経由でPostgreSQL、MySQL、SQLiteに接続します。ローカルstdioと、トークン認証付きのリモートHTTPに対応し、データベースへのアクセス前にクエリを安全性検査します。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
7掲載時のツール一覧 · 1 週間前に更新
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
| limit | integer | いいえ | 1 ページあたりのテーブル数。1~1000。 |
| detail | boolean | いいえ | 列数ではなく、各テーブルの列、キー、インデックスを一覧表示します。複数のテーブルに関する質問では、テーブルごとに table_name を呼び出すのではなく、こちらを使用してください。ページが大きくなるため、大規模なスキーマは name_pattern で絞り込んでください。 |
| offset | integer | いいえ | スキップするテーブル数。前のページの next_offset を渡します。 |
| table_name | any | いいえ | テーブルまたはビューの正確な名前。列、キー、インデックスを返し、テーブルの場合は行数も返します。リスト用パラメーターは無視されます。 |
| name_pattern | any | いいえ | 大文字と小文字を区別せず、名前でテーブルを絞り込みます。通常のテキストは部分文字列に一致します('order')。*、?、[] は glob パターンとして機能します('order_*')。 |
| include_sample_data | boolean | いいえ | table_name と併用すると、サンプル行も 3 件返します。 |
詳細
DB Explorer MCP
PostgreSQL、MySQL、SQLite 用の読み取り専用 Model Context Protocol(MCP)サーバーです。AI コーディングアシスタントが、データベースに書き込みを一切行わずに、リレーショナルデータベースを調査、クエリ、監査できます。
MCP クライアントをデータベースに接続し、自然な言葉で質問してください。クライアントの LLM が SQL を作成し、このサーバーが解析して、単一の読み取り専用 SELECT 以外は拒否したうえで、行数の上限を設けて実行し、構造化された結果を返します。スキーマの調査、実行計画、インデックスの推奨、マイグレーションのレビューも利用できます。
MCP client LLM -> FastMCP tools -> safety layer -> SQLAlchemy -> database
(writes SQL) (7 tools) (rejects writes) (any dialect)
このサーバー自体は LLM API を呼び出さないため、設定する API キーは不要です。推論は接続先のクライアントで行われます。SQLAlchemy を通じて SQLite、PostgreSQL、MySQL に対応しています。
テストおよび評価済みです。 すべてのプッシュとプルリクエストで336件のテストを実行し、さらに CI で実際の PostgreSQL と MySQL に対するテストを26件実行します(テスト)。エージェント評価では、Claude はこれらのツールを通じて現実的なデータベースタスクを14件中14件完了し、ツール呼び出しの失敗はなく、タスクあたりの平均呼び出し回数は2回、独立した評価者による平均スコアは5点満点中4.6点でした(エージェント評価)。
このサーバーの利点
アシスタントにデータベースの認証情報をそのまま渡すと、混乱したりプロンプトインジェクションを受けたりした場合に、テーブルを削除されるおそれがあります。読み取り専用レプリカを渡すと、スキーマ情報や実行計画の分析ができなくなります。このサーバーはその中間を実現します。完全な調査機能とクエリ機能を提供しつつ、モデルに適切な振る舞いを求めるのではなく、パーサーレベルで変更を構造的に不可能にします。
アーキテクチャ
┌──────────────────────────────────────────────────────────────┐
│ MCP client (Claude Code / Claude Desktop / Inspector) │
│ owns the LLM: reads schema, authors SQL, interprets results │
└───────────────────────────┬──────────────────────────────────┘
│ MCP · stdio (local)
│ · streamable HTTP + bearer token (remote)
┌───────────────────────────▼──────────────────────────────────┐
│ server.py — FastMCP instance + one engine per token group │
│ │
│ explore_schema execute_query explain_query │
│ validate_schema suggest_index migration_context │
│ validate_migration │
└──────┬───────────────────────┬───────────────────┬───────────┘
│ │ │
│ read path │ metadata path │ review path
│ │ │
┌──────▼────────────┐ ┌───────▼─────────┐ ┌──────▼──────────┐
│ safety.py │ │ inspector.py │ │ migration.py │
│ ── trust boundary │ │ schema_health.py│ │ parses up/down, │
│ sqlparse AST │ │ index_suggest.py│ │ never executes │
│ SELECT-only │ │ explain.py │ │ │
│ 1 stmt · no cmnts │ │ │ │ │
│ denylist · LIMIT │ │ │ │ │
└──────┬────────────┘ └───────┬─────────┘ └─────────────────┘
│ │
└───────────┬───────────┘
│ SQLAlchemy Core
┌──────────────────▼───────────────────────────────────────────┐
│ Target database · PostgreSQL / MySQL / SQLite │
└──────────────────────────────────────────────────────────────┘
safety.py に渡される SQL はクライアントのモデルが作成するため、信頼できない入力です。そのため、パターンマッチングではなく解析を行い、拒否されたクエリがデータベースに到達することはありません。
| モード | トランスポート | 認証 |
|---|---|---|
| ローカル | stdio | 不要。クライアントがサーバーを起動します |
| リモート | ストリーミング可能な HTTP | ベアラートークン、または Prefect Horizon のプラットフォームエッジで OAuth |
どちらのモードでも同じツールコードが動作し、変更するのは MCP_TRANSPORT だけです。
クイックスタート
Python 3.11 以降と uv が必要です。
PyPI から
リポジトリをクローンする必要はありません。uvx がパッケージをダウンロードし、専用の環境で実行します。
claude mcp add db-explorer --env DATABASE_URL="postgresql+psycopg2://user:pass@localhost:5432/example" -- uvx db-explorer-mcp-server
まず同梱のデモ用データベースで試す場合は、--env を省略してください。他のクライアントの設定では、"command": "uvx" と "args": ["db-explorer-mcp-server"] を使用します。
ソースから
git clone https://github.com/AYUSH-148/DB-Explorer-MCP
cd DB-Explorer-MCP
uv sync
uv run python seed_demo_db.py # creates sample.db, a small shop with deliberate schema flaws
uv run pytest # 336 tests on SQLite; 26 more with TEST_*_URL set
Windows で uv が PATH にない場合は、先頭に py -m を付けてください(py -m uv sync)。
サーバーを自分で起動する必要はありません。MCP クライアントが stdio 経由で起動するため、クライアントを接続するに進んでください。手動で実行する場合、uv run server.py は何も表示せず、標準入力でクライアントを待ち受けます。Ctrl+C で停止できます。
デフォルトのデータベースは sample.db です。以下のクライアント設定、または .env.example をコピーして作成した .env ファイルで DATABASE_URL を指定すると、独自のデータベースに接続できます。
postgresql+psycopg2://user:password@localhost:5432/example
mysql+pymysql://user:password@localhost:3306/example
sqlite:///C:/data/example.db
パスワード内の特殊文字はパーセントエンコードしてください(@ → %40、# → %23、/ → %2F)。
クライアントを接続する
Claude Code — ローカル
claude mcp add db-explorer --env DATABASE_URL="postgresql+psycopg2://user:pass@localhost:5432/example" -- uv --directory "C:/path/to/DB-Explorer-MCP" run server.py
セッションで /mcp を実行し、7つのツールが表示されることを確認してください。すべてのプロジェクトで使用できるようにするには、-s user を追加します。
Claude Code — リモート
claude mcp add --transport http db-explorer https://your-deployment.fastmcp.app/mcp
/mcp → 認証を実行して OAuth フローを開始します。トークンはキャッシュされ、自動的に更新されます。
ベアラートークンを使用して自分でホストするサーバーでは、ヘッダーとして送信してください。環境変数から読み込むことで、チームで共有する .mcp.json に秘密情報を含めずに済みます。
{
"mcpServers": {
"db-explorer": {
"type": "http",
"url": "https://dbx.example.com/mcp",
"headers": { "Authorization": "Bearer ${DBX_TOKEN}" }
}
}
}
Claude Desktop
ローカル接続の場合は、claude_desktop_config.json に次のように記述します。
{
"mcpServers": {
"db-explorer": {
"command": "uv",
"args": ["--directory", "C:/path/to/DB-Explorer-MCP", "run", "server.py"],
"env": { "DATABASE_URL": "postgresql+psycopg2://user:pass@localhost:5432/example" }
}
}
}
カスタムコネクターを使わずにリモートデプロイへ接続するには、stdio 経由でプロキシします。
{
"mcpServers": {
"db-explorer": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://your-deployment.fastmcp.app/mcp"]
}
}
}
VS Code
.vscode/mcp.json はリポジトリにチェックインされており、Windows の py ランチャーを通じて stdio でサーバーを起動します。macOS または Linux では、command を uv に、args を ["run", "server.py"] に変更してください。
MCP Inspector
npx @modelcontextprotocol/inspector
Streamable HTTP トランスポートと /mcp URL を使うか、uv run server.py を使った stdio で接続してください。Inspector ではツールの生の応答と、言い換えられていないエラーが表示されるため、サーバー側の問題かクライアント側の問題かをすばやく判断できます。
Python
import asyncio
from fastmcp import Client
async def main():
async with Client("https://your-deployment.fastmcp.app/mcp", auth="oauth") as client:
print([tool.name for tool in await client.list_tools()])
print(await client.call_tool("explore_schema", {}))
asyncio.run(main())
試してみる
Quickstart のデモ用 sample.db に接続すると、次のようなプロンプトをそのまま使えます。
- 「どのテーブルがありますか?また、主キーがないテーブルはどれですか?」
- 「
ordersから合計額が最も高い 5 行を表示してください。」 - 「このクエリが遅いのはなぜですか?
SELECT * FROM orders WHERE created_at >= '2026-01-01'」 - 「このデータベースでインデックスがない外部キーはどれですか?
CREATE INDEX文を提示してください。」 - 「
ordersにshipped_at列を追加するマイグレーションを作成し、up スクリプトと down スクリプトを検証してください。」
ガードレールの動作を確認するには、DELETE FROM users の実行を依頼してください。呼び出しは Unsafe query blocked: Only SELECT queries are allowed. Got: DELETE というエラーで失敗し、データベースは変更されません。
エージェントスキル
skill/db-explorer/SKILL.md は、モデルがこれらのツールをうまく使う方法を教える任意のエージェントスキルです。クエリの前にスキーマを一度読み取ること、返された行からではなく SQL で合計を計算すること、結果が truncated の場合はその旨を伝えること、エラーのヒントに従って再試行を避けることを教えます。このスキルはモデルをガイドするだけであり、サーバーには引き続き接続する必要があります。
Claude Code では、skill/db-explorer フォルダーをすべてのプロジェクトで使う場合は ~/.claude/skills/db-explorer/ に、1 つのプロジェクト内で使う場合はそのプロジェクトの .claude/skills/db-explorer/ にコピーしてください。
ツール
| ツール | 引数 | 戻り値 |
|---|---|---|
explore_schema | table_name?, include_sample_data=false, name_pattern?, detail=false, limit=200, offset=0 | SQL の作成に使用する dialect と、テーブルおよびビューの一覧(それぞれに kind と列数を含む)、または 1 つのリレーションの kind、列、PK、FK、インデックス、行数(テーブルのみ)、最大 3 件のサンプル行を返します。1 つのリレーション、または detail=true のページには、データベースに保存されているテーブルと列の comment も含まれます |
execute_query | sql, row_limit=100(最大 1000) | 検証済みの SELECT 1 件に対する columns、rows、count、truncated、有効な row_limit を返します。結果に同じ列名が繰り返し含まれる場合は、duplicate_columns と note も返します(重複する列名には id:1 のような、有効な SQL ではないラベルが付きます)。また、サイズ上限を超えないように行数を削減した場合は note を返します |
explain_query | sql | ネイティブの実行計画と、解決された dialect |
validate_schema | table_name?, name_pattern?, limit=200, offset=0 | severity、code、message、suggestion を含むスキーマの問題を返します。table_name を指定しない場合は、explore_schema と同様にページ分割されたテーブルのページを 1 件返します(ビューは除外) |
suggest_index | query? xor table_name? | 理由を含む CREATE INDEX の推奨 |
migration_context | name_pattern?, limit=200, offset=0 | クライアント側でマイグレーションを作成するための、列、キー、インデックスのページ 1 件(行数は含まない)と、ページ分割された explore_schema と同様の dialect |
validate_migration | up_sql, down_sql | スクリプトごとの解析済みステートメントの種類を返します。SELECT、コメント、対応していない括弧は拒否されます。構文チェックではなく、実行されることはありません |
validate_schema は 4 つのコードを報告します。missing_primary_key、unindexed_foreign_key、wide_table(50 列以上)、no_indexes です。外部キーの列から始まるインデックス、主キー、または一意制約がある場合、その外部キーにはインデックスがあると判定されます。部分インデックス、GIN/GiST/BRIN インデックス、FULLTEXT/SPATIAL インデックスは対象外です。
explore_schema は、デフォルトでは低コストで利用できます。引数を指定しない場合、テーブル名と列数を返します。行数が返されるのは table_name を指定した場合のみです(テーブルのみ。上限は 100,000 で、上限に達した場合は row_count_capped: true が付きます)。大規模なスキーマを絞り込むには name_pattern を使用します(order は名前にその文字列を含むものに一致し、order_* はグロブとして扱われます)。limit/offset でページ分割し、detail=true を指定すると、1 回の呼び出しでページ全体の列、キー、インデックスを取得できます。ビューとマテリアライズドビューは、それぞれに kind を付けてテーブルとともに一覧表示されます。
安全モデル
すべての execute_query、explain_query、suggest_index 呼び出しは、データベースにアクセスする前にsafety.pyを通ります。次の条件をすべて満たさないクエリは拒否されます。
- 単一のステートメント。
SELECT 1; DROP TABLE users→Exactly one SQL statement is required SELECTのみ。 文字列の先頭ではなく、解析されたステートメントの種類に基づいて判定します →Only SELECT queries are allowed. Got: DELETE- SQL コメントなし。 文字列リテラルや引用符で囲まれた名前の外にある
--、/*、*/は拒否され、コメントを紛れ込ませる経路を塞ぎます。#はコメントの開始文字となる MySQL と MariaDB でのみ拒否されます。 - トークン列内にブロック対象のキーワードがないこと。
ALTER、CREATE、DELETE、DROP、EXEC、EXECUTE、GRANT、INSERT、INTO、REVOKE、TRUNCATE、UPDATEが対象です。単語全体に一致するため、SELECT grant_date FROM permissionsは許可されます。4つのキーワードは特に重要です。INSERT、UPDATE、DELETEは、ステートメントの種類がSELECTのままの PostgreSQL のデータ変更 CTE に隠せるほか、SELECT * INTO archive FROM usersはテーブルを作成します。残りのキーワードはSELECT型のステートメントには現れませんが、sqlparseの型判定に不具合が生じた場合に備えた第2の防御線として残しています。 - ロック句なし。
FOR UPDATE、FOR NO KEY UPDATE、FOR SHARE、FOR KEY SHARE、および MySQL のLOCK IN SHARE MODEは拒否されます。ロック読み取りはほかの書き込みをブロックするためです。キーワード単体ではなく句として判定するため、shareという名前の列は引き続き使用できます。
許可されたクエリはすべて SELECT * FROM (<your query>) AS limited_query LIMIT <row_limit + 1> でラップされるため、上限のないスキャンでクライアントのコンテキストが埋め尽くされることはありません。クエリ内の LIMIT は内側の結果を絞り込みますが、row_limit(上限は1000)によって返される結果も制限されます。追加の確認用の行が返された場合、その行は破棄され、結果に "truncated": true が付与されるため、上限で切り詰められた結果を完全なものと誤認することはありません。
キーワードのブロックリストでは、構文上は正しくても有害なクエリを検出できないため、検証は3つある防御層の第1段階にすぎません。
-
ステートメントのタイムアウト。
SELECT pg_sleep(600)は上記のすべてのチェックを通過するため、構文とは別に実行時間を制限します。PostgreSQL ではstatement_timeout(トランザクションごとに設定されるため、PgBouncer、Supabase、Neon のプーラー経由でも機能します)、MySQL ではmax_execution_time、SQLite ではプログレスハンドラーによる期限を使用します。QUERY_TIMEOUT_SECONDSで設定し、db.pyで適用されます。 -
読み取り専用トランザクション。 読み取りは、PostgreSQL では
BEGIN READ ONLY、MySQL ではSTART TRANSACTION READ ONLY、SQLite ではPRAGMA query_onlyを通じて実行されます。書き込みそのものをデータベースが拒否するため、これはブロックリストでは実現できない保証です。 -
権限。 最外層の境界として機能します。.env.example](.env.example)を参照してください。キーワードチェックでは確実に捕捉できない
pg_read_file()のようなサーバー側のファイル読み取りを阻止するのは、SELECTのみを許可されたユーザーです。読み取り可能な対象を制限する場合も、権限を使います。トークンは、そのグループのデータベースユーザーが
SELECTできるものを読み取れるため、各グループに専用のユーザーを割り当ててください(名前付きトークンとグループを参照)。認証情報や個人を特定できる情報(PII)は、サーバー側のテーブルまたは列のブロックリストではなく、権限付与で隠してください。パーサーはSELECT *、SELECT u FROM users u、row_to_json(u)の背後にある列を把握できませんが、データベースはこれらすべてに対する権限付与を強制します。-- PostgreSQL: whole tables, minus one GRANT SELECT ON ALL TABLES IN SCHEMA public TO explorer; REVOKE SELECT ON users FROM explorer; -- then only the safe columns of it GRANT SELECT (id, name, created_at) ON users TO explorer;MySQL でも同じ列リストを使用します。
GRANT SELECT (id, name) ON app.users TO 'explorer'@'%';。権限を取り消された列に触れるクエリはsql_error(permission denied)で失敗します。
すべてのツール呼び出しは、stderr の db_explorer.audit ロガーに1行を出力します。内容は、ツール、終了状態(ok またはエラーコード)、呼び出し元(HTTP 経由の場合は user=<token name> group=<group>)、および引数(最大2000文字)です。引数はそのまま記録されるため、ログもデータと同じように機密性の高いものとして扱ってください。
エラー
ツールのエラーは、次に何をすべきかが示されて初めてモデルにとって有用です。そのため、すべての失敗には、単なるメッセージではなく、コードと修正のヒントが含まれます。
[table_not_found] Table not found: usrs. Did you mean: users.
Hint: Call explore_schema() to list the tables in this database.
[sql_error] The database rejected the query: no such column: totl.
Hint: Call explore_schema(table_name=...) to confirm the table and column names before retrying.
[query_timeout] The query exceeded the statement timeout of 15s: interrupted.
Hint: Add a WHERE clause, aggregate instead of scanning, or query a smaller table.
| コード | 原因 |
|---|---|
table_not_found | テーブルが見つかりません。データベースに存在する、最も近い名前が示されます |
sql_error | データベースがクエリを拒否しました。列が存在しない、型が一致しない、構文が正しくないなどの原因が考えられます |
query_timeout | クエリの実行時間が QUERY_TIMEOUT_SECONDS を超えたため、キャンセルされました |
database_unavailable | サーバーがデータベースに接続できないか、クエリの実行中に接続が切れました。詳細はサーバーログにのみ記録されます |
unsafe_query | safety.py によってブロックされました。ヒントには、該当した具体的なルールが示されます |
query_too_complex | SQL が 50,000 文字を超えているか、パーサーが処理するには長すぎるか、ネストが深すぎます(例:値が約 3,000 個ある IN (...)) |
too_many_concurrent_calls | 呼び出し元のトークンですでに MAX_CONCURRENT_CALLS_PER_CLIENT 件の呼び出しが実行中です |
unknown_group | 呼び出し元のトークンで指定されたグループには、サーバーのデータベースログインがありません |
invalid_argument | row_limit が 1 未満など、引数が範囲外です。または、ツールの入力スキーマに適合しない引数(必須の引数がない、型が異なる、名前が不明など)です |
missing_argument / conflicting_arguments | suggest_index には query または table_name のどちらか一方だけが必要です |
comments_not_allowed / unparsable_sql / select_in_migration | validate_migration がスクリプトを拒否しました |
server_busy | DB_POOL_TIMEOUT_SECONDS の間、すべてのデータベース接続が使用中でした。クエリは実行されていません。変更せずに再試行してください |
internal_error | サーバーのバグです。トレースバックはサーバーログに記録され、呼び出し元にはコードのみが表示されます |
設定
| 変数 | デフォルト | 備考 |
|---|---|---|
DATABASE_URL | config.py と同じディレクトリにある sample.db(読み取り専用、stdio のみ) | MCP_TRANSPORT が stdio 以外の場合は必須です。指定されていないと、起動時に明示的に失敗します |
MCP_TRANSPORT | stdio | stdio、streamable-http、または sse |
MCP_HOST | 127.0.0.1 | HTTP トランスポートのみ |
MCP_PORT | 8000 | HTTP トランスポートのみ |
QUERY_TIMEOUT_SECONDS | 15 | 個々のステートメントの上限時間であり、呼び出し全体の制限時間でもあります。時間を使い切ると、それ以降のステートメントは開始されません。正の整数を指定してください |
DB_POOL_SIZE | 5 | データベースへの接続を維持する数 |
DB_MAX_OVERFLOW | 10 | 負荷時に追加で開く接続数。返却時に閉じられます |
DB_POOL_TIMEOUT_SECONDS | 5 | 空き接続を待つ時間。この時間を超えると server_busy で失敗します |
MAX_CONCURRENT_CALLS_PER_CLIENT | 5 | 1 つのトークンで同時に実行できるツール呼び出し数(サーバープロセスごと)。stdio には上限がありません |
MCP_AUTH_TOKEN | — | 共通トークンを 1 つ指定します。HTTP 経由でサービスを提供する場合は、MCP_AUTH_TOKENS とこの変数のどちらかが必須です。server.py:mcp を自ら読み込むホストでも同様です。32 文字以上である必要があります |
MCP_AUTH_TOKENS | — | 名前付きトークンをカンマ区切りで指定します。形式は name:token または name:group:token です。MCP_AUTH_TOKEN の代わりに使用し、併用しないでください。名前付きトークンとグループを参照してください |
DATABASE_URL_<GROUP> | — | トークングループのデータベース URL です。例:DATABASE_URL_ANALYST。MCP_AUTH_TOKENS で指定したすべてのグループに対して必須です |
MCP_ALLOW_UNAUTHENTICATED | false | 信頼できるネットワーク、または独自の認証を前段に配置したホスト(Horizon)向けに、トークン要件を明示的に無効化します |
一度に接続を保持できる呼び出し数は、最大 DB_POOL_SIZE + DB_MAX_OVERFLOW(デフォルトでは 15 件)です。この数を超える呼び出しは DB_POOL_TIMEOUT_SECONDS の間待機し、その後 server_busy で失敗します。すべてのサーバーインスタンスの合計が、データベースの max_connections を超えないようにプールを設定してください。
SQLite のフォールバックはローカル開発専用です。config.py と同じディレクトリにある sample.db を読み取り専用で開きます。起動時に .env が読み込まれます(最初に config.py と同じディレクトリにあるファイル、次に作業ディレクトリにあるファイル)。実際の環境変数は、どちらの値よりも優先されます。
HTTP 経由で提供する
$env:MCP_TRANSPORT = "streamable-http"
$env:MCP_HOST = "0.0.0.0"
$env:MCP_PORT = "8000"
$env:DATABASE_URL = "postgresql+psycopg2://user:password@host:5432/example"
$env:MCP_AUTH_TOKEN = python -c "import secrets; print(secrets.token_urlsafe(32))"
uv run server.py
macOS または Linux でも同様です。
export MCP_TRANSPORT=streamable-http MCP_HOST=0.0.0.0 MCP_PORT=8000
export DATABASE_URL="postgresql+psycopg2://user:password@host:5432/example"
export MCP_AUTH_TOKEN="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
uv run server.py
HTTP エンドポイントは、設定されたデータベースの SELECT をポートに到達できるすべてのユーザーに公開するため、MCP_AUTH_TOKEN または MCP_AUTH_TOKENS が設定されていない場合、サーバーは保護されない状態で起動せず、起動に失敗します。クライアントは Authorization: Bearer <token> としてトークンを送信し、auth.py で定時間比較されます。本当に信頼できるネットワーク上で上書きする場合は MCP_ALLOW_UNAUTHENTICATED=true を設定してください。その場合、サーバーは起動のたびに標準エラー出力に警告を表示します。
server.py:mcp を直接読み込むホスト(Horizon、fastmcp run)は、トークンまたは MCP_ALLOW_UNAUTHENTICATED=true が設定されていない限り、すべての HTTP リクエストに 401 を返します。
ループバックにバインドした場合、外部の Host ヘッダーと Origin ヘッダーは拒否されるため、DNS リバインディングによってウェブページがローカルサーバーにアクセスすることはできません。トランスポートはステートレスなので、ロードバランサーの背後にあるどのレプリカでも任意のリクエストに応答できます。GET /health はロードバランサーのプローブ用に、トークンなしで {"status": "ok"} を返します。データベースには一切アクセスしません。
名前付きトークンとグループ
各ユーザーまたはサービスに個別のトークンを割り当て、必要に応じて、読み取り可能なデータをデータベースユーザーが決定するグループを指定します。
MCP_AUTH_TOKENS=alice:analyst:<token>,carol:analyst:<token>,fran:finance:<token>,ci:<token>
DATABASE_URL=postgresql+psycopg2://dbx_readonly:...@db/app # tokens with no group (ci)
DATABASE_URL_ANALYST=postgresql+psycopg2://dbx_analyst:...@db/app
DATABASE_URL_FINANCE=postgresql+psycopg2://dbx_finance:...@db/app
各エントリは name:token または name:group:token です。トークン名は監査ログの各行に記録されます。グループによって、呼び出しの実行に使われるデータベースのログインが決まり、その呼び出し元が読み取れるデータは、データベース独自の権限設定(および PostgreSQL の行レベルセキュリティ)によって決まります。MCP_AUTH_TOKEN のみを設定した場合は、グループなしの default という名前のトークンとして扱われます。
不正な形式のエントリ、重複する名前またはトークン、32 文字未満のトークン、対応する DATABASE_URL_<GROUP> がないグループ、あるいは MCP_AUTH_TOKEN と MCP_AUTH_TOKENS の両方が設定されている場合、サーバーは起動を拒否します。トークンは定時間比較され、起動時に読み込まれるため、変更後は再起動してください。各グループには個別の接続プールがあります。
PostgreSQL では、explore_schema はグループが SELECT できないテーブルも一覧表示します(カタログは全員が読み取れます)が、その行は保護されたままです。DEPLOYMENT.md には、両データベース用の GRANT 文、Horizon での OAuth、HTTPS を使った VM のセットアップ手順が記載されています。
ホスト型 Supabase に関する注意: 直接接続(db.<ref>.supabase.co)は IPv6 専用のため、IPv4 のみを使用するコンテナからは接続に失敗し、psycopg2.OperationalError の内容が空のように見えます。ダッシュボードの Connect パネルにあるプーラーホストを使用してください。また、ユーザー名が postgres.<project-ref> に変わる点に注意してください。
テスト
uv run pytest
安全性レイヤー、シリアライズ、読み取り専用の適用とタイムアウト、HTTP 認証とトークングループ、すべてのツール、エラー報告を対象とした 336 件のテストがあります。各テストでは一時的な SQLite データベースを使用するため、テストスイートの実行に認証情報や稼働中のサーバーは必要ありません。
tests/test_live_databases.py では、SQLite では検証できない読み取り専用トランザクション、ステートメントのタイムアウト、NUMERIC/TIMESTAMP 値、カタログのリフレクションを、実際の PostgreSQL と MySQL に対してテストします。使い捨てのデータベースを指定するには TEST_POSTGRES_URL や TEST_MYSQL_URL を設定してください(dbx_live_* テーブルを作成して削除します)。CI では両方を実行します。
エージェントの評価
テストでは各ツールを検証し、評価ではモデルがツールを適切に使うかどうかを検証します。evals/run.py は、evals/scenarios.py にある各タスクを、ヘッドレスモード(claude -p)の Claude Code を通じて実行します。このサーバーのみをツールソースとして、シード済みの evals/eval.db を対象にします。各実行では、ツールの選択、呼び出し予算、データベースから読み取った正解に基づいて採点し、2 回目の claude -p 呼び出しで実行全体を 1~5 点で評価します。
uv run python evals/seed_eval_db.py # build evals/eval.db (the runner does this if it is missing)
uv run python evals/run.py # every scenario, on Sonnet
uv run python evals/run.py --only top_customers --model opus
PATH 上に claude CLI があり、ログイン済みである必要があります。実行はそのログインの使用量上限に算入され、API キーは必要ありません。実行ごとに結果が異なるため、評価は CI の対象外です。レポートは evals/results/ に書き込まれます。
最新の結果
エージェントおよび判定役として Claude Sonnet 5.5 を使用、2026-10-14。すべてのツール呼び出しとその結果を含む完全なレポート:evals/reports/2026-10-14-sonnet.json。
14/14 合格 · ツールエラー 0 件 · 繰り返し呼び出し 0 件 · ツール呼び出し合計 28 回 · 判定役の平均スコア 4.64 / 5
| タスク | エージェントが選択したツール | 呼び出し回数 / 予算 | 判定役 |
|---|---|---|---|
| テーブルとビューを一覧表示する | explore_schema | 1/3 | 5/5 |
| ステータスごとの注文数 | explore_schema、execute_query | 2/4 | 5/5 |
| 配達済みの購入額が多い上位 3 人の顧客 | explore_schema、execute_query | 3/5 | 4/5 |
| 最も売上の多いカテゴリ | explore_schema、execute_query | 2/5 | 5/5 |
| 平均評価が最も高いカテゴリ | explore_schema、execute_query | 2/5 | 5/5 |
| 一度も注文していないユーザー | explore_schema、execute_query | 2/4 | 5/5 |
| 顧客名を含む直近 5 件の注文 | explore_schema、execute_query | 2/4 | 5/5 |
「ユーザーテーブル」の行数(実際の名前:users) | explore_schema、execute_query | 2/4 | 5/5 |
| 日付で絞り込んだクエリが遅い理由 | explore_schema、explain_query | 2/4 | 5/5 |
| 構造上の問題がないかスキーマを監査する | validate_schema、explore_schema | 2/4 | 4/5 |
reviews のどの外部キーにインデックスが必要か | suggest_index、explore_schema | 2/3 | 4/5 |
phone 列を追加するマイグレーション | migration_context、validate_migration | 2/4 | 5/5 |
| 「キャンセルされた注文をすべて削除する」(読み取り専用のため拒否する必要あり) | explore_schema、execute_query | 2/5 | 4/5 |
| pro プランのユーザー | explore_schema、execute_query | 2/3 | 4/5 |
正解が 1 つの値に定まる回答はすべて、判定だけでなくデータベースと照合しました。スコア 4 は、必要以上に 1 回多く呼び出した場合、または質問に対する回答以外の説明を加えた場合です。
既知の制限:explore_schema は UNIQUE 制約や CHECK 制約をまだ報告しないため、スキーマ監査で、実際には存在する制約がないとモデルが誤って主張することがあります。
プロジェクトの構成
server.py FastMCP instance, engine, and the 7 tool definitions
safety.py query validation and row-limited execution
db.py engine construction, statement timeouts, read-only transactions
serialization.py driver values to JSON-safe primitives
auth.py bearer-token verification for HTTP transports
inspector.py schema reflection (columns, PK, FKs, indexes, samples)
explain.py dialect-aware EXPLAIN
index_suggest.py index recommendations from plans or FK metadata
schema_health.py objective schema issue reporting
indexes.py index coverage shared by schema_health and index_suggest
migration.py migration context and non-executing script validation
errors.py coded errors with hints, and driver-error classification
config.py environment configuration with fail-fast checks
seed_demo_db.py builds the demo sample.db used in Quickstart
skill/ optional agent skill for clients that load SKILL.md files
tests/ pytest suite over temporary SQLite databases
evals/ agent evals: a Claude model works scripted tasks through the tools
設計上の注意点と制限
- クエリモードの
suggest_indexは、インデックスが一般に不足しているかどうかではなく、フルテーブルスキャンを検出します。 どの列にインデックスを付けるかはクエリによって異なるため、CREATE INDEXを記述するのではなく、スキャンを示します。小さなテーブルでのスキャンは想定内であり、問題ではありません。 - 調査対象はデフォルトスキーマのみです:PostgreSQL では
search_pathの先頭のスキーマ(通常はpublic)、MySQL ではDATABASE_URLで指定されたデータベースです。SQL でスキーマを指定すれば、execute_queryは引き続きbilling.invoicesを読み取ります。PostgreSQL で別のスキーマを調査するには、そのスキーマを先頭に設定します:ALTER ROLE explorer SET search_path = billing, public;。 - 一部の読み取り専用クエリ形式は拒否されます。
sqlparseは、かっこで囲まれた(SELECT 1)、VALUES (1)、TABLE usersを解析できないため、通常のSELECTを記述するよう案内して拒否します。 - SQL は 50,000 文字までに制限されています。また、
sqlparseのトークン数またはネストの上限を超えると、呼び出しはquery_too_complexで失敗します。 - 行数上限はコンテキストを保護するものであり、処理コストを抑えるものではありません。 負荷の高い集計処理も全件実行されます。これには
QUERY_TIMEOUT_SECONDSが上限を設定します。 - 長いテキストや大きな結果は切り詰められます。 2000 文字を超えるテキスト値は、全長を示したうえで切り詰められて返されます。クエリ結果は 500,000 バイトで停止し、
truncatedが設定されます。スキーマページも同じ位置で停止し、next_offsetから続きます。 - MariaDB はテストされていません。 MySQL と同じコードパスを使用しますが、行数上限を適用するサブクエリ内の
ORDER BYが無視される場合があります。その場合は、順序付けしたクエリに独自のLIMITを追加してください。 - 256 バイト以下のバイナリ値は 16 進数で返されます。それより大きい値はサイズのみが報告されます。
- 幅の広い
NUMERIC値は文字列として返されるため、丸められずに桁が正確に保たれます。
ライセンス
MIT。詳細は LICENSE をご覧ください。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。