Metadata-Version: 2.4
Name: mysql-mcp-plus
Version: 0.1.1
Summary: 轻量级、STDIO-only、支持多个 MySQL 数据源的 MCP 服务
Keywords: mysql,mcp,model-context-protocol,multi-datasource,database
Author: 若清风
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Dist: mcp>=1.26.0,<2
Requires-Dist: pymysql>=1.1.2,<2
Requires-Dist: sqlparse>=0.5.3,<1
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/mjwyr/mysql_mcp_plus
Project-URL: Repository, https://github.com/mjwyr/mysql_mcp_plus
Project-URL: Issues, https://github.com/mjwyr/mysql_mcp_plus/issues
Description-Content-Type: text/markdown

# mysql-mcp-plus

`mysql-mcp-plus` 是一个本地运行、仅使用 STDIO 传输的 MySQL MCP 服务。单个 MCP
进程可以声明多个数据源，每次工具调用都必须显式指定 `datasource`，适合把开发、测试、
生产只读库等连接放在同一个 MCP 客户端配置中。

当前版本为 `0.1.1`，正式支持 MySQL 5.7 和 8.x。MariaDB、Percona 仅保证基础连接与 SQL
尽力兼容。

项目只提供通用的数据源发现、连通性检查和 SQL 执行能力，不提供 HTTP/SSE 服务、OAuth、
SSH 隧道、连接池、自动重试、完整 mysql CLI，也不提供专用的表结构、索引或健康检查工具。

## 运行要求

- Python 3.11 或更高版本
- [uv](https://docs.astral.sh/uv/)
- MCP 客户端能够启动本地 STDIO 服务
- 至少一个可访问的 MySQL 账号和数据库

Windows 可通过 WinGet 安装 uv：

```powershell
winget install --id astral-sh.uv --exact
uv --version
```

macOS/Linux 可使用 uv 官方安装脚本：

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
uv --version
```

安装后如果当前终端还找不到 `uv`，请按安装程序提示把 uv 目录加入 `PATH`，或重新打开终端。

从源码安装依赖：

```console
git clone <仓库地址>
cd mysql_mcp_plus
uv sync --all-groups
```

验证入口命令可用：

```console
uv run mysql-mcp-plus
uv run mysql-mcp-multi
uv run python -m mysql_mcp_multi
```

`mysql-mcp-plus` 是主命令；`mysql-mcp-multi` 是兼容别名。这三个入口都先验证环境配置，
再启动 STDIO MCP。配置错误写入 stderr 并以退出码 `2`
结束；启动阶段不会连接数据库。

## 配置

服务只读取进程环境变量，不会自动加载 `.env`。`MYSQL_SOURCES` 声明数据源名称，每个名称
再映射到一组 `MYSQL_<数据源大写>_*` 变量。

### 全局变量

| 环境变量 | 必填 | 默认值 | 规则 |
|---|---:|---:|---|
| `MYSQL_SOURCES` | 是 | 无 | 逗号分隔且至少一个；名称匹配 `^[a-z][a-z0-9_]*$`，不可重复或留空 |
| `MYSQL_MAX_ROWS_PER_RESULT` | 否 | `1000` | 正整数；单个结果集最多返回的行数 |
| `MYSQL_MAX_TOTAL_ROWS` | 否 | `5000` | 正整数，且不得小于单结果集上限；单次调用所有结果集的返回总量 |
| `MYSQL_MAX_SQL_BYTES` | 否 | `5242880` | 正整数；按 UTF-8 字节数限制完整 SQL 脚本 |
| `MYSQL_MAX_STATEMENTS` | 否 | `5000` | 正整数；单次脚本中的语句数上限 |
| `MYSQL_MCP_LOG_LEVEL` | 否 | `INFO` | `DEBUG`、`INFO`、`WARNING`、`ERROR` 或 `CRITICAL`，不区分大小写 |

### 每个数据源的变量

以下表格以名为 `dev` 的数据源为例，实际前缀为 `MYSQL_DEV_`。

| 后缀 | 完整示例 | 必填 | 默认值与规则 |
|---|---|---:|---|
| `HOST` | `MYSQL_DEV_HOST` | 否 | `localhost`；空值也回退到默认值 |
| `PORT` | `MYSQL_DEV_PORT` | 否 | `3306`；整数，范围 `1..65535` |
| `USER` | `MYSQL_DEV_USER` | 是 | 去除首尾空白后不可为空 |
| `PASSWORD` | `MYSQL_DEV_PASSWORD` | 否 | 空字符串；保留原始值，不做 `.env` 解析 |
| `DATABASE` | `MYSQL_DEV_DATABASE` | 是 | 去除首尾空白后不可为空，作为连接默认库 |
| `ROLE` | `MYSQL_DEV_ROLE` | 否 | `readonly`；可选 `readonly`、`writer`、`admin` |
| `CHARSET` | `MYSQL_DEV_CHARSET` | 否 | `utf8mb4`；不可为空 |
| `CONNECT_TIMEOUT` | `MYSQL_DEV_CONNECT_TIMEOUT` | 否 | `10` 秒；正整数 |
| `READ_TIMEOUT` | `MYSQL_DEV_READ_TIMEOUT` | 否 | `300` 秒；正整数 |
| `WRITE_TIMEOUT` | `MYSQL_DEV_WRITE_TIMEOUT` | 否 | `300` 秒；正整数 |
| `SSL_CA` | `MYSQL_DEV_SSL_CA` | 否 | CA 证书文件路径 |
| `SSL_CERT` | `MYSQL_DEV_SSL_CERT` | 否 | 客户端证书路径；必须与 `SSL_KEY` 同时配置 |
| `SSL_KEY` | `MYSQL_DEV_SSL_KEY` | 否 | 客户端私钥路径；必须与 `SSL_CERT` 同时配置 |
| `SSL_VERIFY_CERT` | `MYSQL_DEV_SSL_VERIFY_CERT` | 否 | `false`；接受 `true/false`、`1/0`、`yes/no`、`on/off` |

配置任一 TLS 变量都会启用该数据源的 TLS 参数。启用证书校验时应提供可信的
`SSL_CA`；双向 TLS 需要同时提供 `SSL_CERT` 和 `SSL_KEY`。文件是否存在以及服务端是否接受
证书由连接时的 PyMySQL/MySQL 校验，静态配置阶段只检查证书与私钥必须成对出现。

### 多数据源示例

PowerShell：

```powershell
$env:MYSQL_SOURCES = "dev,staging,prod"
$env:MYSQL_DEV_HOST = "127.0.0.1"
$env:MYSQL_DEV_USER = "app_writer"
$env:MYSQL_DEV_PASSWORD = "replace-me"
$env:MYSQL_DEV_DATABASE = "app_dev"
$env:MYSQL_DEV_ROLE = "writer"

$env:MYSQL_STAGING_HOST = "staging-db.example.com"
$env:MYSQL_STAGING_USER = "app_admin"
$env:MYSQL_STAGING_PASSWORD = "replace-me"
$env:MYSQL_STAGING_DATABASE = "app_staging"
$env:MYSQL_STAGING_ROLE = "admin"

$env:MYSQL_PROD_HOST = "prod-db.example.com"
$env:MYSQL_PROD_USER = "app_readonly"
$env:MYSQL_PROD_PASSWORD = "replace-me"
$env:MYSQL_PROD_DATABASE = "app_prod"
$env:MYSQL_PROD_ROLE = "readonly"
$env:MYSQL_PROD_SSL_CA = "C:\certs\company-ca.pem"
$env:MYSQL_PROD_SSL_VERIFY_CERT = "true"

uv run mysql-mcp-plus
```

macOS/Linux shell：

```bash
export MYSQL_SOURCES='dev,prod'
export MYSQL_DEV_HOST='127.0.0.1'
export MYSQL_DEV_USER='app_writer'
export MYSQL_DEV_PASSWORD='replace-me'
export MYSQL_DEV_DATABASE='app_dev'
export MYSQL_DEV_ROLE='writer'

export MYSQL_PROD_HOST='prod-db.example.com'
export MYSQL_PROD_USER='app_readonly'
export MYSQL_PROD_PASSWORD='replace-me'
export MYSQL_PROD_DATABASE='app_prod'
export MYSQL_PROD_ROLE='readonly'
export MYSQL_PROD_SSL_CA='/etc/company/mysql-ca.pem'
export MYSQL_PROD_SSL_VERIFY_CERT='true'

uv run mysql-mcp-plus
```

`DATABASE` 只是连接默认库，不是 MCP 级访问边界。`admin` 可以执行 `USE` 或使用跨库限定名；
最终能访问哪些对象始终由 MySQL 账号权限决定。

## MCP 客户端配置

推荐通过 `uvx` 启动 PyPI 发行包，无需克隆仓库或配置本地源码路径。下面使用常见的
`mcpServers` JSON 形状；客户端字段名若不同，只需映射相同的 `command`、`args` 和 `env`。
所有凭据均为占位值，需要替换。

```json
{
  "mcpServers": {
    "mysql-plus": {
      "command": "uvx",
      "args": ["mysql-mcp-plus"],
      "env": {
        "MYSQL_SOURCES": "dev",
        "MYSQL_DEV_USER": "app_readonly",
        "MYSQL_DEV_PASSWORD": "replace-me",
        "MYSQL_DEV_DATABASE": "app_dev"
      }
    }
  }
}
```

发布维护步骤和凭据安全要求见 [`docs/releasing.md`](docs/releasing.md)。

## MCP 工具

服务只暴露三个工具，均返回结构化对象；没有默认数据源或全局“当前数据源”。

### `list_datasources() -> dict[str, Any]`

列出静态配置，不建立数据库连接。返回名称、主机、端口、默认库、用户、角色和是否配置
TLS，不返回密码、连接串、证书路径或私钥内容。

```json
{
  "success": true,
  "datasources": [
    {
      "name": "prod",
      "host": "prod-db.example.com",
      "port": 3306,
      "database": "app_prod",
      "user": "app_readonly",
      "role": "readonly",
      "ssl": true
    }
  ]
}
```

### `test_connection(datasource: str) -> dict[str, Any]`

为指定数据源建立一次独立连接，执行探测 SQL，读取版本、默认库、实际 MySQL 用户和 TLS
cipher 状态，然后关闭连接。不会自动重试。

```json
{
  "success": true,
  "datasource": "prod",
  "latency_ms": 18,
  "server_version": "8.0.43",
  "default_database": "app_prod",
  "current_user": "app_readonly@%",
  "ssl": true
}
```

### `execute_sql(datasource: str, sql: str, max_rows: int | None = None)`

执行完整 SQL 脚本。脚本可包含多条语句；`max_rows` 只能调低全局单结果集返回上限，布尔值、
`0` 和负数无效。执行前会依次完成 UTF-8 字节限制、SQL 拆分、语句数限制、客户端命令检查、
分类和整批权限校验，任何一条不合法都不会建立连接或执行前序语句。

```json
{
  "datasource": "dev",
  "sql": "SELECT id, name FROM users ORDER BY id LIMIT 10",
  "max_rows": 10
}
```

成功响应按语句返回结果。`CALL` 等产生的多个结果集全部放在 `result_sets`；达到返回行数
上限后仍会消费服务器上的剩余行和后续结果集。

```json
{
  "success": true,
  "datasource": "dev",
  "transaction": false,
  "committed": false,
  "results": [
    {
      "index": 1,
      "statement_type": "SELECT",
      "success": true,
      "result_sets": [
        {
          "columns": ["id", "name"],
          "rows": [[1, "Alice"]],
          "row_count": 1,
          "truncated": false
        }
      ],
      "affected_rows": 0,
      "last_insert_id": null,
      "warnings": 0
    }
  ],
  "elapsed_ms": 4
}
```

常用 SQL 可直接通过 `execute_sql` 完成，无需专用工具：

```sql
SHOW TABLES;
DESCRIBE users;
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com';
SELECT * FROM users ORDER BY id DESC LIMIT 20;
```

## 角色权限

MCP 角色是执行前的语句类别保护层，不替代 MySQL `GRANT` 权限。

| 角色 | 允许的语句 |
|---|---|
| `readonly` | 安全的 SELECT/只读 CTE、SHOW、DESC/DESCRIBE、EXPLAIN |
| `writer` | `readonly` 的能力，加 INSERT、UPDATE、DELETE、REPLACE 和写入 CTE |
| `admin` | 除显式事务控制与不支持的客户端命令外，不限制服务端 SQL 类型；允许 `USE` 和跨库限定名 |

`readonly` 明确拒绝 `SELECT ... FOR UPDATE`、`LOCK IN SHARE MODE`、`INTO OUTFILE` 和
`INTO DUMPFILE`。`readonly`/`writer` 遇到无法保守分类的语句会拒绝。所有角色都拒绝脚本中的
`BEGIN`、`START TRANSACTION`、`COMMIT`、`ROLLBACK`、`SAVEPOINT`、
`RELEASE SAVEPOINT` 和 `SET AUTOCOMMIT`，也不支持 `DELIMITER`、`SOURCE`、`\.` 等
mysql 客户端命令。

最小权限仍应在 MySQL 层实现。例如给 `readonly` 数据源配置仅有 SELECT 权限的 MySQL
账号，给 `writer` 账号只授予目标库所需 DML 权限，不要因为 MCP 角色存在而复用 root 账号。

## 事务、限制与序列化

- 纯只读批次使用 autocommit 连接，不显式 `BEGIN`，响应为 `transaction: false`、
  `committed: false`。
- 包含写操作、CALL、DDL 或不确定 admin 语句的批次由 MCP 开启事务；全部成功后统一提交。
- 第一条执行错误会停止后续语句并尽可能回滚；不会自动重试。
- MySQL 的 CREATE、ALTER、DROP、TRUNCATE、RENAME、GRANT、REVOKE、LOCK、UNLOCK 等
  可能隐式提交。成功响应包含 `implicit_commit_warning: true`；失败响应包含
  `partial_commit_possible: true`，因此这类批次无法保证完全原子。
- 连接中断时不重试写入；若无法确认提交状态，失败响应包含 `commit_state: "unknown"`。
- 单结果集和单次调用总返回行数分别受全局限制控制。`truncated: true` 只表示响应省略了行，
  不表示服务器结果集未消费。

MySQL 值按以下规则转换为 JSON 安全值：

| MySQL/Python 值 | JSON 表示 |
|---|---|
| NULL | `null` |
| DECIMAL | 保留精度的字符串 |
| DATE、DATETIME、TIME | ISO 格式字符串 |
| `timedelta` | MySQL TIME 风格字符串 |
| bytes/BLOB | Base64 字符串 |
| MySQL JSON | 对象或数组；解析失败时保留原字符串 |

## 错误响应

预期的配置、参数、SQL、权限和 MySQL 错误返回结构化信息；未预料的程序缺陷才由 MCP
报告 Tool Error。

| `error_type` | 含义 | SQL 是否可能已执行 |
|---|---|---:|
| `unknown_datasource` | 请求的数据源不存在 | 否 |
| `invalid_argument` | `max_rows` 等参数无效 | 否 |
| `invalid_sql` | SQL 为空或无法解析 | 否 |
| `sql_limit_exceeded` | SQL 字节数或语句数超限 | 否 |
| `unsupported_client_command` | 使用了 `DELIMITER`、`SOURCE`、`\.` 等客户端命令 | 否 |
| `permission_denied` | 角色不允许某条语句或脚本包含事务控制 | 否 |
| `authentication_failed` | MySQL 1045，账号认证失败 | 否 |
| `unknown_database` | MySQL 1049，默认库不存在 | 否 |
| `connection_failed` | 无法建立连接 | 否 |
| `connection_lost` | 执行期间连接中断 | 可能，提交状态可能未知 |
| `lock_wait_timeout` | MySQL 1205 | 可能，服务会尝试回滚 |
| `deadlock` | MySQL 1213 | 可能，服务会尝试回滚 |
| `sql_execution_failed` | 其他 MySQL 执行错误 | 可能，服务会尝试回滚 |

预执行失败示例：

```json
{
  "success": false,
  "datasource": "prod",
  "error_type": "permission_denied",
  "message": "readonly 数据源不允许执行 UPDATE 语句",
  "failed_index": 2,
  "executed": false
}
```

执行期失败还会包含 `transaction`、`committed`、`rolled_back`、
`partial_commit_possible`、`failed_index`、`results`、MySQL `error.code`/`error.message` 和
`retryable`。`retryable` 只描述错误类别，不代表服务会自动重试。

## 日志与安全边界

- stdout 专用于 MCP STDIO 协议；普通日志和配置错误只写 stderr。
- 默认 INFO 只记录工具名、数据源、语句数、事务状态、耗时和结果状态。
- DEBUG 只记录去除注释、替换字符串/数字字面量后最多 200 字符的 SQL 摘要。
- 不记录密码、私钥、完整连接串、完整 SQL、查询结果、业务数据或证书内容。
- `list_datasources` 会显示主机、库名和账号名；如果这些元数据也敏感，应限制 MCP 客户端
  配置与进程日志的读取权限。
- MCP role 不是数据库沙箱。应使用独立 MySQL 账号、最小对象权限、网络访问控制和 TLS。
- 不要把生产凭据写入仓库、README 示例或可被其他用户读取的客户端配置。

## 测试与构建

默认测试不访问网络或 MySQL：

```console
uv sync --all-groups
uv run ruff check .
uv run ruff format --check .
uv run pytest
uv build
```

### 可选 MySQL 5.7/8.0 集成测试

`compose.test.yaml` 只提供测试基础设施，不是 MCP 的运行依赖。它使用公开的非生产测试凭据、
独立数据库和本机端口：MySQL 5.7 为 `3357`，MySQL 8.0 为 `3380`。

集成测试会在 `MYSQL_TEST_DATABASE` 中创建、删除临时表并执行 DML。只能把这些变量指向隔离、
可丢弃的测试库，不要指向生产库或包含需保留数据的数据库。

启动并等待目标服务在 `docker compose ps` 中显示 healthy：

```console
docker compose -f compose.test.yaml up -d mysql57
docker compose -f compose.test.yaml ps
```

PowerShell 运行 MySQL 5.7：

```powershell
$env:MYSQL_TEST_HOST = "127.0.0.1"
$env:MYSQL_TEST_PORT = "3357"
$env:MYSQL_TEST_USER = "mcp_test"
$env:MYSQL_TEST_PASSWORD = "mcp_test_password"
$env:MYSQL_TEST_DATABASE = "mysql_mcp_test"
$env:MYSQL_TEST_EXPECT_VERSION = "5.7"
uv run pytest -m mysql
```

PowerShell 运行 MySQL 8.0：

```powershell
docker compose -f compose.test.yaml up -d mysql80
$env:MYSQL_TEST_PORT = "3380"
$env:MYSQL_TEST_EXPECT_VERSION = "8.0"
uv run pytest -m mysql
```

macOS/Linux 运行 MySQL 5.7：

```bash
MYSQL_TEST_HOST=127.0.0.1 \
MYSQL_TEST_PORT=3357 \
MYSQL_TEST_USER=mcp_test \
MYSQL_TEST_PASSWORD=mcp_test_password \
MYSQL_TEST_DATABASE=mysql_mcp_test \
MYSQL_TEST_EXPECT_VERSION=5.7 \
uv run pytest -m mysql
```

macOS/Linux 运行 MySQL 8.0：

```bash
docker compose -f compose.test.yaml up -d mysql80
MYSQL_TEST_HOST=127.0.0.1 \
MYSQL_TEST_PORT=3380 \
MYSQL_TEST_USER=mcp_test \
MYSQL_TEST_PASSWORD=mcp_test_password \
MYSQL_TEST_DATABASE=mysql_mcp_test \
MYSQL_TEST_EXPECT_VERSION=8.0 \
uv run pytest -m mysql
```

集成测试只有同时满足以下条件才会连接数据库：命令显式包含 `-m mysql`，并且五个必需的
`MYSQL_TEST_HOST`、`PORT`、`USER`、`PASSWORD`、`DATABASE` 均已配置。缺失时会清晰跳过。

可选测试变量：

| 环境变量 | 用途 |
|---|---|
| `MYSQL_TEST_EXPECT_VERSION` | 断言服务端版本字符串以指定前缀开头 |
| `MYSQL_TEST_EXPECT_TLS` | `true/false`；断言探测到的实际 TLS 状态 |
| `MYSQL_TEST_SSL_CA` | 测试连接的 CA 路径 |
| `MYSQL_TEST_SSL_CERT` | 测试连接的客户端证书路径，必须与 KEY 成对 |
| `MYSQL_TEST_SSL_KEY` | 测试连接的客户端私钥路径，必须与 CERT 成对 |
| `MYSQL_TEST_SSL_VERIFY_CERT` | 传给数据源配置的证书校验开关 |

测试结束后删除容器和测试数据卷：

```console
docker compose -f compose.test.yaml down -v
```

## 故障排查

### 启动立即退出，退出码为 2

查看 stderr 中指出的具体环境变量。常见原因是缺少 `MYSQL_SOURCES`、某数据源没有 `USER`
或 `DATABASE`、名称包含大写/连字符、整数超出范围、角色无效，或 TLS 证书与私钥没有成对
配置。服务不会读取当前目录中的 `.env`。

### 数据库连接失败

先调用 `list_datasources` 核对脱敏后的主机、端口、默认库和用户，再调用
`test_connection`。检查 DNS/防火墙、端口、MySQL 监听地址、账号来源主机、密码、默认库和
TLS CA。启动成功只代表静态配置有效，不代表数据库可达。

### SQL 被拒绝

查看 `error_type`、`failed_index` 和 `statement_type`。`permission_denied` 表示 MCP role
不允许整批中的某条语句，整批尚未执行；MySQL 1044/1142 等则表示数据库账号对象权限不足。
需要扩大能力时，同时审查 MCP role 与 MySQL `GRANT`，优先保持最小权限。

### 执行中连接中断

服务不会自动重试。若响应包含 `commit_state: "unknown"`，不要直接重放写入脚本；先通过
业务唯一键、审计记录或只读查询确认数据库中的实际状态。

### DDL 批次出现风险提示

这是 MySQL 隐式提交语义，不是可忽略的普通 warning。把 DDL 与 DML 分开执行，避免假设
DDL 失败后前序修改一定能回滚，并在变更前准备数据库级回滚方案。

### Ruff 报告 `E902 stream did not contain valid UTF-8`

部分公司加密目录会让 Ruff 无法直接读取本来合法的 UTF-8 Python 文件。不要因此批量改编码
或重写源码。把仓库或只读校验副本放到未加密目录后运行 Ruff，并分别执行 Python 编译、
pytest 和构建来验证代码。本项目当前的正常验证工作区位于未加密的 `C:` 盘。

## License

[MIT](LICENSE)
