secure-mcp-database-crud
README.md
# Secure MCP Database CRUD
[English](README.en.md) | 日本語
SQLiteまたはMySQLへ接続し、安全なタスクCRUDだけを公開するMCPサーバーです。
任意SQLを公開せず、入力検証、パラメータ化クエリ、最小権限、監査イベント、
トランザクション、署名付き削除確認を一つの実行可能なサンプルにまとめています。
このリポジトリは記事
[「データベースに接続するMCPサーバー:SQLiteとMySQLで安全なCRUD Toolを作る」](https://ynaito.dev/ja/writing/secure-mcp-database-crud/)
の完成形コードです。
## 5分で試す(SQLite)
前提は [uv](https://docs.astral.sh/uv/) のみです。uvがPython 3.12も用意します。
```bash
git clone https://github.com/yunosuke-github/secure-mcp-database-crud.git
cd secure-mcp-database-crud
cp .env.example .env
uv sync
uv run mcp-db-init
uv run mcp-database-crud
```
最後のコマンドはstdioで待機します。これは正常です。MCPクライアントには次のように登録します。
`/absolute/path/...` は実際の絶対パスへ置き換えてください。
```json
{
"mcpServers": {
"secure-task-database": {
"command": "uv",
"args": [
"--directory",
"/absolute/path/secure-mcp-database-crud",
"run",
"mcp-database-crud"
]
}
}
}
```
動作をすぐ確認する場合はテストを実行します。
```bash
uv run pytest
```
## 公開するTool
| Tool | 種別 | 役割 |
| --- | --- | --- |
| `list_tasks` | 読み取り | ステータス、件数、オフセット、許可済みソートで一覧取得 |
| `get_task` | 読み取り | IDで1件取得。不存在も正常な結果として返す |
| `create_task` | 更新 | タスクと`created`監査イベントを同時に作成 |
| `update_task` | 更新 | 許可済み項目だけを更新し、必要なら楽観ロックを適用 |
| `prepare_delete_task` | 読み取り | 対象と短時間有効な署名済みトークンを返す |
| `confirm_delete_task` | 破壊的操作 | 対象・版・期限・nonceを検証して削除 |
`execute_sql`のようなToolはありません。モデルが選べるのは用途が限定された操作だけです。
## MySQLで動かす
Docker ComposeはMySQL 8.4、スキーマ、読み取り専用ユーザー、更新専用ユーザーを用意します。
```bash
cp .env.example .env
# .env の DATABASE_BACKEND を mysql へ変更
docker compose up -d --wait mysql
uv run mcp-database-crud
```
ローカル用パスワードは公開されたデモ値です。本番では必ず変更してください。アプリケーションの
ユーザーにはDDL権限がないため、MySQLのスキーマ作成は起動時処理から分離しています。
MySQL統合テストはサービス起動後に明示的に実行します。
```bash
RUN_MYSQL_TESTS=1 uv run pytest -m mysql
docker compose down
```
## 安全性の境界
```mermaid
flowchart LR
U[ユーザー] --> C[AIクライアント]
C -->|Tool名と構造化引数| M[MCPサーバー]
M --> V[Pydantic検証]
V --> R[TaskRepository]
R -->|固定SQLとバインド値| D[(SQLite / MySQL)]
M -.->|SQL・認証情報は返さない| C
```
- 値はすべてプレースホルダーへバインドします。
- ソート列と方向はEnumから固定SQL断片へ変換します。
- SQLiteの読み取りは`mode=ro`と`query_only`を併用します。
- MySQLはreader/writerのユーザーと接続プールを分けます。
- タスク更新と`task_events`追加は同じトランザクションです。
- 更新時は取得した`updated_at`をWHERE条件へ含めます。
- 削除トークンはHMAC署名され、操作、ID、版、有効期限、nonceへ結び付きます。
- 成功済みnonceのハッシュを削除トランザクション内で一意保存し、再利用を拒否します。
- ToolエラーにはSQL、ファイルパス、ホスト名、認証情報を含めません。
## ディレクトリ構成
```text
src/mcp_database_crud/
├── config.py # 環境変数とSecretStr設定
├── confirmation.py # HMAC確認トークン
├── models.py # Toolの入力・出力Schema
├── server.py # FastMCPの組み立てとstdio起動
├── database/
│ ├── protocol.py # SQLite/MySQL共通契約
│ ├── sqlite_repository.py # 呼び出し単位の接続とトランザクション
│ ├── mysql_repository.py # reader/writer接続プール
│ └── sqlite_schema.sql
└── tools/
├── handlers.py # 安全な結果とエラー変換
├── read_tools.py # 読み取りTool登録
└── write_tools.py # 更新・削除Tool登録
docker/mysql/init/001_schema.sql # MySQLスキーマと最小権限GRANT
tests/ # セキュリティ特性とRepository契約
```
## 設定
`.env.example`を`.env`へコピーして使います。`.env`はGit管理対象外です。
| 変数 | 用途 | 既定値 |
| --- | --- | --- |
| `DATABASE_BACKEND` | `sqlite`または`mysql` | `sqlite` |
| `SQLITE_PATH` | SQLiteファイル | `data/tasks.db` |
| `CONFIRMATION_SIGNING_KEY` | 削除トークンのHMAC鍵(32文字以上) | 必須 |
| `CONFIRMATION_TTL_SECONDS` | 削除確認の有効秒数 | `120` |
| `MYSQL_*` | ホスト、DB、reader/writer認証情報 | ローカルCompose用 |
| `MYSQL_POOL_SIZE` | reader/writer各プールの接続数 | `5` |
| `DB_TIMEOUT_SECONDS` | DB接続・SQLiteロック待機秒数 | `5` |
安全な署名鍵は、例えば次のように生成できます。
```bash
python -c 'import secrets; print(secrets.token_urlsafe(48))'
```
## 開発
```bash
uv run ruff format --check .
uv run ruff check .
uv run mypy
uv run pytest --cov=mcp_database_crud --cov-report=term-missing
```
テストでは、SQLインジェクション風文字列がデータとして保存されること、監査INSERT失敗時に
更新が戻ること、読み取り専用SQLiteが書き込みを拒否すること、改変・期限切れ・別タスク用・
古い版・再利用トークンが拒否されることを確認します。
## 本番導入前の注意
このサンプルはローカルstdioサーバーを対象としています。リモート公開する場合は、Tool入力の
`user_id`を信用せず、認証コンテキストから主体を取得して各タスクへの認可を追加してください。
既存の業務APIが認可、通知、イベント発行を担っているなら、DBへ直接接続せずAPI経由を優先します。
レート制限、監視、バックアップ、マイグレーション、シークレット管理も環境に合わせて追加してください。
## トラブルシューティング
- `unable to open database file`: `uv run mcp-db-init`を先に実行してください。
- `confirmation_signing_key`の検証エラー: `.env`を作り、32文字以上の鍵を設定してください。
- MySQLへ接続できない: `docker compose ps`で`mysql`が`healthy`か確認してください。
- MySQLスキーマを作り直したい: このComposeはtmpfsです。`docker compose down`後に再起動します。
TDQS
A3.9/5.0
Scored across 6 tools
Disambiguation5/5
Each tool targets a distinct operation: create, get, list, update, and a two-step delete with prepare and confirm. There is no functional overlap.
Naming Consistency5/5
All tool names follow a consistent verb_noun pattern (e.g., create_task, update_task, prepare_delete_task). The naming is clear and predictable.
Tool Count5/5
With 6 tools, the server is well-scoped for task CRUD operations. The count is neither excessive nor insufficient for the domain.
Completeness5/5
The tool set covers full CRUD functionality plus a secure two-step delete. No obvious missing operations for the stated purpose of secure task management.
Maintenance
ActivityStale
ResponsivenessNo issues