内容
<p align="center">
<img src="assets/banner.svg" alt="mcp-db-server" width="100%">
</p>
<p align="center">
<a href="https://github.com/rudraraval4/mcp-db-server/actions/workflows/ci.yml"><img src="https://github.com/rudraraval4/mcp-db-server/actions/workflows/ci.yml/badge.svg" alt="持续集成"></a>
<img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python 版本">
<img src="https://img.shields.io/badge/coverage-97%25-brightgreen" alt="代码覆盖率">
<img src="https://img.shields.io/badge/license-MIT-green" alt="许可证">
<img src="https://img.shields.io/badge/MCP-server-8A2BE2" alt="MCP 服务器">
</p>
> 一个生产级别的 **MCP 服务器**,允许任何 LLM 客户端(Claude Desktop、Cursor 等)以自然语言查询 SQL 数据库 — **只读、AST 验证和限制**。
LLM 编写 SQL。此服务器 **绝不信任它**。每个查询都被解析为抽象语法树,并在允许接近只读数据库连接之前强制通过安全门。
<p align="center">
<img src="assets/demo.svg" alt="演示:列出表格、查询顶部客户、拒绝 DROP" width="100%">
</p>
## 为什么存在
让 LLM 运行针对数据库的 SQL 既强大又令人恐惧。难点不是生成 SQL — 客户端已经能很好地做到这一点。难点在于 **保证** 生成的 SQL 不能读取它不应该读取的内容,不能写入,不能运行数据库。
## 安全保证
每个 `run_query` 调用都必须通过安全核心才能执行:
- **只读** — 仅 `SELECT` / `WITH … SELECT` 生存;所有 DML/DDL 在 AST 级别被拒绝(不是通过关键字正则表达式,因此注释和大小写技巧没有帮助)。
- **单语句** — 拒绝堆叠查询(`SELECT …; DROP …`)。
- **表格访问控制** — 允许列表和拒绝列表针对解析的表格强制执行。
- **行限制** — 在 `MCP_DB_MAX_ROWS` 处注入/强制执行 `LIMIT`。
- **语句超时** — 终止长时间查询。
- **审计日志** — 每个尝试(允许或阻止)都被记录。
这些不是期望 — 每个都通过 [`tests/test_safety.py`](tests/test_safety.py) 中的对抗性测试固定(大小写技巧、注释注入、堆叠语句、`SELECT … INTO`、`PRAGMA`/`ATTACH` 等)。第二层独立于 [`tests/test_engine.py`](tests/test_engine.py) 中的测试:即使写入操作不知怎么地到达引擎,也会被只读连接拒绝。
## 工具
| 工具 | 用途 |
|------|---------|
| `list_tables()` | 访问策略下的可见表格 |
| `describe_table(name)` | 列、类型、键 |
| `explain_query(sql)` | 在不返回行数据的情况下解释查询(和引擎计划) |
| `run_query(sql)` | 验证、限制、只读结果集 |
## 示例
您可以用普通英语提问;客户端 LLM 编写 SQL;服务器验证、限制并运行只读查询。来自捆绑的 `demo.db` 的实际输出:
**"按产品类别计算总收入是多少?"**
```sql
SELECT p.category, ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM products p JOIN order_items oi ON oi.product_id = p.id
GROUP BY p.category ORDER BY revenue DESC;
```
| 类别 | 收入 |
|---|---|
| 电子产品 | 85746.19 |
| 家具 | 76442.0 |
| 服装 | 39219.91 |
| 体育 | 18746.5 |
| 家居 | 12596.5 |
| 文具 | 1663.22 |
**"按状态统计订单数量是多少?"**
```sql
SELECT status, COUNT(*) AS orders FROM orders GROUP BY status ORDER BY orders DESC;
```
| 状态 | 订单数量 |
|---|---|
| 已完成 | 206 |
| 已取消 | 79 |
| 已发货 | 58 |
| 待处理 | 57 |
**"哪些产品从未被订购过?"**
```sql
SELECT name, category FROM products
WHERE id NOT IN (SELECT DISTINCT product_id FROM order_items);
```
| 名称 | 类别 |
|---|---|
| 记号贴 | 文具 |
| 台历 | 文具 |
| 荧光笔套装 | 文具 |
**"现在删除订单表格。"** → *查询被安全策略拒绝:只允许只读 SELECT 查询。*
## 快速入门
```bash
git clone https://github.com/rudraraval4/mcp-db-server
cd mcp-db-server
pip install -e ".[dev]"
python scripts/seed_db.py # 创建 demo.db(电子商务样本数据)
```
## 在 30 秒内试用(无需 MCP 客户端)
捆绑的 `mcp-db-demo` CLI 驱动 **与 MCP 服务器公开的服务完全相同** — 因此这里看到的就是 LLM 客户端看到的:
```bash
mcp-db-demo tables
mcp-db-demo describe customers
mcp-db-demo query "SELECT country, COUNT(*) FROM customers GROUP BY country ORDER BY 2 DESC"
# 安全层生效 — 以下查询均被拒绝:
mcp-db-demo query "UPDATE products SET price = 0"
mcp-db-demo query "SELECT * FROM customers; DROP TABLE customers"
```
## 在 Claude Desktop 中使用
将服务器添加到您的 `claude_desktop_config.json`,然后完全重启 Claude Desktop。
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"db": {
"command": "mcp-db-server",
"env": {
"MCP_DB_DATABASE_URL": "sqlite:///absolute/path/to/demo.db",
"MCP_DB_MAX_ROWS": "1000"
}
}
}
}
```
> `command` 必须解析为已安装的 `mcp-db-server` 可执行文件。如果它不在 Claude Desktop 的 `PATH` 中,请使用绝对路径(例如,在虚拟环境中:`.../.venv/Scripts/mcp-db-server.exe` 在 Windows 上,`.../.venv/bin/mcp-db-server` 在 macOS/Linux 上)。
然后直接提问:
> *"数据库中有哪些表格?"* · *"哪些五位客户花费最多?"* ·
> *"删除订单表格"* → 礼貌地拒绝。
## 配置
所有旋钮都是环境变量(前缀 `MCP_DB_`):
| 变量 | 默认值 | 含义 |
|----------|---------|---------|
| `MCP_DB_DATABASE_URL` | `sqlite:///./demo.db` | SQLAlchemy URL(SQLite 或 Postgres) |
| `MCP_DB_MAX_ROWS` | `1000` | 硬行限制 |
| `MCP_DB_STATEMENT_TIMEOUT_SECONDS` | `10` | 终止慢速查询 |
| `MCP_DB_ALLOWED_TABLES` | _(空)_ | 允许列表;如果设置,则仅这些表格 |
| `MCP_DB_DENIED_TABLES` | _(空)_ | 拒绝列表 |
| `MCP_DB_AUDIT_LOG_PATH` | `audit.log` | 审计日志位置 |
## 架构

客户端 LLM 执行自然语言到 SQL 的推理。服务器贡献客户端无法安全地自己做的事情:在 SQL 被允许执行的内容周围设置一个硬性、强制的边界。两个独立的层位于查询和数据之间 — **安全核心**(拒绝发出但限定的只读 SELECT) 和 **引擎**(拒绝执行写入操作,无论如何)。
## 项目布局
```
src/mcp_db_server/
config.py # 环境驱动的设置 — 每个安全旋钮
safety.py # 安全核心:sqlglot AST 验证(核心)
engine.py # SQLAlchemy 只读访问 + 反射
service.py # 两个前端共享的逻辑
server.py # MCP 服务器(FastMCP,stdio)
cli.py # mcp-db-demo:相同的服务,不需要 MCP 客户端
formatting.py # 结果渲染
audit.py # JSONL 审计日志
scripts/seed_db.py # 可复制的演示数据库
tests/ # 73 个测试,97% 覆盖率
```
## 开发
```bash
pip install -e ".[dev]"
pytest # 73 个测试
pytest --cov=mcp_db_server # 覆盖率
```
可选的 Postgres 支持:`pip install -e ".[postgres]"` 并将 `MCP_DB_DATABASE_URL` 指向 `postgresql://…` URL。
## 我如何构建它
关于设计和关键决策(NL→SQL 属于客户端,而不属于服务器)的简要介绍在 [WRITEUP.md](WRITEUP.md) 中。
## 许可证
MIT — 见 [LICENSE](LICENSE)。