Skip to main content
Glama
NibelungT

db-mcp-server

by NibelungT
README.md
# db-mcp-server

给 AI 用的多数据源 MySQL 查询服务,带一个网页配置中心。

解决的问题很具体:**同一个服务在不同环境(开发 / 测试 / 预发 / 生产)连着不同的库**。AI 只有一张 `execute_sql(sql)` 的手牌时,很容易查了测试库却让用户以为在看生产数据。这个项目让 AI 说"查订单服务的测试库",而不是让它猜一个连接串。

```
用户:查一下订单服务的测试库有多少张表
  ↓
AI 调用 query(sql="...", project="订单服务", env="test")
  ↓
服务端解析 → order-test · 测试 · myapp_order
  ↓
返回结果,并附一行来源标注
```

## 特性

| | |
|---|---|
| **按项目路由** | 项目名或别名(`订单服务` / `order` / `订单`)都能命中,一个项目在每个环境绑一个数据源 |
| **显式指定必须命中** | 说了 `env=prod` 但没绑生产库,直接报错并列出已绑定的环境,**不会降级**到别的库返回数据 |
| **只读保护** | 标记为只读的数据源(通常是生产),`execute` 在服务端被拒绝,不依赖 AI 自觉 |
| **配置热加载** | 配置中心改完数据源,MCP 下次调用就生效,不用重启客户端 |
| **执行记录** | 每次查询落 `query_log.jsonl`,界面可分页查看:时间、来源、SQL、耗时、成功/失败、失败原因 |
| **无库模式** | `database` 可以留空,用 `库名.表名` 跨库查询 |
| **单文件前端** | 零构建、零框架,改样式只需编辑 `webui/index.html` |

## 界面

启动后打开 `http://127.0.0.1:8765`:

- 上半部分:**数据源**(按环境分 tab)与**项目映射**左右并列,各有常驻的新增入口
- 下半部分:**执行记录**,左侧查询流水,右侧成功/失败/平均耗时统计

只有 `127.0.0.1` 监听,没有登录鉴权,靠回环地址隔离。

## 快速开始

### 1. 安装依赖

```bash
python -m pip install -r requirements.txt
```

需要 Python 3.10+(用到了 `str | None` 类型标注)。依赖:`mcp`、`mysql-connector-python`、`python-dotenv`、`fastapi`、`uvicorn`。

### 2. 启动配置中心

```bash
python webui_server.py
```

或双击 `start_webui.bat`(Windows)。端口从 8765 起,被占用就自动顺延,启动时会打印实际地址。

首次运行若 `config.json` 不存在,会从 `.env` 尝试迁移一份;没有 `.env` 就用默认配置(dev / test / staging / prod 四个环境)。

### 3. 配置数据源和项目

**新增数据源** —— 填主机、端口、账号。库名那栏点「加载库列表」可以真实连库列出所有可访问的库,不用手打。**库名留空表示不限定库**。

**新增项目** —— 项目名 + 别名,然后给每个环境挑一个数据源。绑定规则:

- 只绑 1 个环境 → 这个环境自动成为默认,不需要配 `defaultEnv`
- 绑多个环境 → 必须指定默认环境,下拉只列出已绑定的环境
- 一个都不绑 → 保存被拦截

**设默认数据源** —— 数据源行首的星标。AI 完全不指定目标时用它。

### 4. 接入 MCP 客户端

```json
{
  "mcpServers": {
    "db-mcp": {
      "command": "/absolute/path/to/python",
      "args": ["/absolute/path/to/db_mcp_server.py"],
      "env": { "PYTHONIOENCODING": "utf-8" }
    }
  }
}
```

三个必须注意的点:

1. **`command` 写绝对路径**。如果 `python` 是另一个未装依赖的解释器(比如被其他工具托管的版本),MCP 进程会启动失败,客户端通常只显示"未连接",不告诉你原因。
2. **`args` 也写绝对路径**。客户端拉起子进程时的 cwd 不一定是项目目录。
3. **`PYTHONIOENCODING=utf-8`**。Windows 下 Python 的 stdout 默认 GBK,而 MCP 走 UTF-8;错误信息里有中文时会出现乱码或 JSON-RPC 解析失败。

不同客户端放配置的位置:

| 客户端 | 位置 |
|---|---|
| Qoder | `~/.qoder/settings.json` → `mcpServers`,或项目根 `.mcp.json` |
| Claude Desktop | `%APPDATA%\Claude\claude_desktop_config.json` |
| WorkBuddy | `~/.workbuddy/mcp.json` |

新增或删除工具后**必须重启客户端**(工具列表会被缓存);只改数据源不用。

## 工具清单

| 工具 | 用途 | 关键参数 |
|---|---|---|
| `query` | 执行 SELECT,返回 JSON 结果集 | `sql`、`params`、`project` / `env` / `datasource` |
| `execute` | 执行 INSERT / UPDATE / DELETE | 同上,只读数据源上会被拒绝 |
| `list_tables` | 列出目标库的表名 | `project` / `env` / `datasource` |
| `describe_table` | 查看表结构(列名、类型、主键) | 另加 `table` |
| `get_schema` | 完整结构(所有表及其字段) | 表多时结果很大,优先用 `describe_table` |
| `search_data` | 在所有文本字段中模糊搜索 | 另加 `table`、`keyword`、`limit` |
| `list_projects` | 列出项目、别名、各环境绑定 | — |
| `list_datasources` | 列出数据源,不含密码 | — |

`query` / `execute` 的返回里会附一行来源标注:

```
[
  { "db": "myapp_order", "host": "db-test.internal" }
]

-- 来源:order-test · 测试 · myapp_order
```

这行不是装饰。没有它,AI 查了哪个库只能靠猜。

## 目标解析优先级

```
1. datasource 显式指定            → 直接取该数据源
2. project + env                  → project.bindings[env]
   ├─ env 不在环境列表里           → 报错,列出可用 key
   └─ 项目未绑定该环境             → 报错,列出已绑定的环境
3. project 无 env                 → 绑 1 个就用它 / 绑多个用 defaultEnv
4. 都没传                         → 默认数据源 → 默认项目
5. 仍解析不出                     → 报错,列出可用项目与数据源
```

第 2 条是刻意设计成"报错"的。原方案里写的是"项目没绑该环境就降级到默认环境",改成报错的原因:

```
用户:查一下订单服务的生产数据
AI:query(project="订单服务", env="prod")
降级:订单服务没绑 prod → 用测试库 → 返回测试数据
用户看着屏幕上的数字,以为那是生产环境的
```

静默给错东西比直接报错危险得多。

## CLI

同一个脚本带子命令就是命令行工具,适合调试:

```bash
python db_mcp_server.py list-projects
python db_mcp_server.py list-datasources
python db_mcp_server.py query --project 订单服务 --env test --sql "SELECT 1"
python db_mcp_server.py query --sql "SHOW DATABASES"          # 走默认数据源
python db_mcp_server.py describe-table --datasource order-test --table db.tbl
```

结果 JSON 走 stdout,`-- 来源:xxx` 走 stderr,方便脚本管道处理。CLI 调用同样会写执行记录。

## 配置

`config.json`(不提交,见 `.gitignore`):

```json
{
  "version": 7,
  "environments": [
    { "key": "test", "label": "测试", "order": 2, "readonlyHint": false },
    { "key": "prod", "label": "生产", "order": 4, "readonlyHint": true }
  ],
  "global": {
    "defaultDatasource": "ds_order_test",
    "defaultProject": null,
    "defaultEnv": null
  },
  "datasources": [
    {
      "id": "ds_order_test",
      "alias": "order-test",
      "env": "test",
      "driver": "mysql",
      "host": "10.0.0.1",
      "port": 3306,
      "user": "readonly_user",
      "password": "${ORDER_TEST_PASSWORD}",
      "database": "",
      "readonly": false
    }
  ],
  "projects": [
    {
      "id": "p_order",
      "name": "订单服务",
      "aliases": ["order", "订单"],
      "defaultEnv": "test",
      "bindings": { "test": "ds_order_test" }
    }
  ]
}
```

从零开始可以照抄 `config.example.json`。

**密码两种写法**:

- 明文 `"password": "secret"` —— 简单,适合内网自用
- 变量引用 `"password": "${ORDER_TEST_PASSWORD}"` —— 从环境变量取,配置文件可以安全备份和分享

界面上密码始终显示为 `******`,编辑时留空即保持原值。

**环境变量**:

| 变量 | 作用 |
|---|---|
| `DB_MCP_CONFIG` | 指向另一个配置文件,测试时用,避免污染真实配置 |
| `DB_MCP_LOG` | 指向另一个执行记录文件 |

## 目录结构

```
config_store.py     配置层:原子写、mtime 热加载、密码解析、解析器、乐观锁
db_mcp_server.py    MCP 服务(stdio)+ CLI,8 个工具
webui_server.py     FastAPI 后端,17 个路由
query_log.py        执行记录:JSONL 追加、5MB 轮转、分页、统计
migrate_env.py      .env → config.json,幂等
webui/index.html    配置界面,单文件零构建
```

**一条硬约束**:`config_store.py` 的 `resolve()` 是 MCP 服务和 Web 界面共用的唯一解析入口。两边各写一套的话,会出现"界面显示默认是测试库、AI 实际查了生产库"这种无法排查的问题。

## 安全

- **只监听 `127.0.0.1`**,没有登录鉴权。不要改成 `0.0.0.0`——界面上存着数据库凭据
- **`config.json`、`.env`、`query_log.jsonl` 已加入 `.gitignore`**。执行记录里有完整 SQL,泄露的业务信息比密码更多
- **只读拦截在服务端**。AI 看不到密码,也绕不过 `readonly` 检查
- 生产库建议:`readonly: true` + 只读账号 + 密码用变量引用,三重

## 已知限制

| 限制 | 说明 |
|---|---|
| 只支持 MySQL | `driver` 字段已预留,接 PostgreSQL 需要给 `list_tables` / `describe_table` 按方言分支 |
| 无登录鉴权 | 靠回环地址隔离 |
| `list_tables` / `get_schema` 在无库数据源上不可用 | `SHOW TABLES` 必须有当前库,会给出提示改用 `库名.表名` |
| `get_schema` 不缓存 | 表多的库较慢,优先用 `describe_table` |
| 一个项目每环境只能绑一个数据源 | 需要"一个环境查两个库"时用无库数据源 + 全限定名 |

## License

MIT