db-query-mcp
by asd369932
README.md
# db-query-mcp
让 AI 助手安全查询 SQLite 数据库的 MCP Server —— 只读强制、SQL 校验、结果封顶。
## 为什么做这个
给 AI 接数据库,最大的风险不是"查不到",而是三件事:
1. **误写** —— 模型把 `SELECT` 写成 `UPDATE`,或者 `DROP TABLE` 出现在"顺手清理"里
2. **灌爆上下文** —— 一句 `SELECT * FROM orders` 拉回 200 万行,对话直接崩
3. **读到不该读的** —— 路径没限制,`db_path: ../../etc/passwd` 之类的穿越
这个服务把这三条做成设计约束,而不是"提示词里提醒一句"。
## 三条安全边界
| 边界 | 实现 | 绕过尝试的结果 |
|------|------|----------------|
| 只读 | ① SQL 白名单前缀(SELECT/WITH/PRAGMA/EXPLAIN)② 写操作关键字黑名单(引号感知剥注释,防 `DEL/**/ETE` 混淆,且不会误伤字符串)③ PRAGMA 白名单 + 拒绝赋值/括号传参 ④ 路径 URI 转义 + `mode=ro` ⑤ `PRAGMA query_only=ON` | 拒绝并返回原因,不执行 |
| 结果封顶 | 硬上限 500 行 + 单元格超 2000 字符截断 | 返回 `truncated: true` |
| 路径限制 | 库文件必须位于 `DB_QUERY_MCP_ROOT`(默认当前目录)内 | 拒绝并列出允许目录 |
只读为什么需要这么多层 —— 对抗性测试与逐层隔离实验的真实教训:
- **URI 注入**:不转义直接拼 `f"file:{path}?mode=ro"` 时,文件名里的 `#`
会被解析成 fragment,把 `?mode=ro` 整段丢弃 → 连接偷偷变成读写模式。
必须 `quote()` 转义路径。
- **两层各能拦什么(逐层隔离实测,每用例全新库)**:
| 连接配置 | journal_mode=WAL | user_version 写 | INSERT |
|----------|------------------|-----------------|--------|
| 仅 `mode=ro` | 拦 | 拦 | 拦 |
| 仅 `query_only` | **拦不住**(文件真变 WAL) | 拦 | 拦 |
| 两层叠加(本实现) | 拦 | 拦 | 拦 |
`query_only` 单独拦不住 `journal_mode=WAL` 是 SQLite 的 pragma 特性,
这正是两层都留的原因;行为由 `test_each_layer_isolation` 钉住。
- **括号传参绕过**:`PRAGMA user_version(123)` 是 SQLite 的合法语法,
但等号检查拦不住它。只有"名字参数"类 pragma(table_info 等)才允许
带括号。
- **注释混淆与字符串误伤**:`DEL/**/ETE` 在 SQLite 里等于 `DELETE`,
校验前必须剥注释;但剥注释必须引号感知 —— 字符串里的 `'--x--'`
不是注释,正则直接剥会破坏合法查询。实现用双视图扫描:
执行用 cleaned(注释删、字符串留),校验用 code_only(字符串遮蔽,
数据里的 `DELETE`/`;` 不会误判)。
这些点都是先被实际绕过、修复后才写进这段文档的(见 git 历史里的
`fix(security)` 提交和 `TestSecurityRegressions` 回归测试)。
## 安装
```bash
pip install -e .
```
## 配置(以 Claude Desktop 为例)
```json
{
"mcpServers": {
"db-query": {
"command": "db-query-mcp",
"env": {
"DB_QUERY_MCP_ROOT": "/path/to/your/databases"
}
}
}
}
```
`DB_QUERY_MCP_ROOT` 支持多个目录,用 `:` 分隔(Linux/macOS)或 `;`(Windows)。
## 工具
### `query`
```json
{
"db_path": "data/app.db",
"sql": "SELECT id, email FROM users WHERE created_at > '2026-01-01' LIMIT 50",
"max_rows": 50
}
```
返回:
```json
{
"columns": ["id", "email"],
"rows": [[1, "a@example.com"]],
"row_count": 1,
"truncated": false,
"max_rows": 50
}
```
### `schema`
不传 `table`:列出全部表和视图。
传 `table`:返回列定义(类型/非空/默认值/主键)和建表 SQL。
推荐工作流:先 `schema` 看结构,再写 `query` —— 这也是给模型的 instructions 里写明的。
## 会拒绝什么
```
[拒绝] 只允许 SELECT / WITH / PRAGMA / EXPLAIN 开头的只读查询 # UPDATE ...
[拒绝] 检测到写操作关键字: DROP(本服务只读) # SELECT 1; DROP TABLE x
[拒绝] 只允许单条语句,检测到多个分号
[拒绝] 路径不在允许的根目录内。允许: /home/me/data
```
## 测试
```bash
pip install -e ".[dev]"
pytest tests/ -v
```
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues