1. 为什么要把 MySQL 数据校验塞进 MCP Server
数据校验这件事,做过数据质量巡检的朋友都懂:写 SQL 查空值、查重复、查外键孤儿、查金额负数,一套下来几十条语句,跑完还得手动整理成报告。更麻烦的是,每次业务方问「昨天订单表有没有异常」,你都得重新翻 SQL 文件、改日期、贴结果。我试过把这套流程直接交给 AI 客户端,让它自己生成校验 SQL、执行、汇总,效率提升非常明显——前提是,你得先有一个安全的 MySQL MCP Server 把校验能力暴露出去。
MCP(Model Context Protocol)说白了就是 AI 世界的 USB-C 接口。你按协议写一次工具,Claude Desktop、Cursor、Cline 这些支持 MCP 的客户端都能直接调用。把 MySQL 数据校验封装成 MCP Server,本质上是给 AI 装了一个「只读数据质检员」:它能看到表结构、能跑 SELECT 校验语句、能把结果整理成人话,但碰不了 DROP、UPDATE、DELETE。
这篇文章面向三类人:一是做数据质量巡检的数据工程师,想把重复的校验 SQL 沉淀成 AI 可调用的工具;二是做入库前校验的后端开发,希望在数据写库前让 AI 先跑一遍规则;三是刚接触 MCP、想找一个真实可跟做案例的开发者。全文会给到可复制的 MCP Server 配置片段、MySQL 连接参数、校验 SQL 模板,以及在 AI 客户端里触发校验、核对返回结果的完整动作。目标只有一个:让你一次跑通从配置到校验的闭环。
需要先明确一个边界:MCP Server 不是让 AI 直连生产库乱来,而是通过一层受控的服务端,把「能做什么」限定在只读校验范围内。这个思路和 TaoToken 在模型接入层做的事类似——把复杂的对接收敛成标准入口,调用方只管用,安全边界由中间层守住。
2. TaoToken 前置准备:模型入口与 Key 获取
在配置 MySQL MCP Server 之前,得先解决 AI 客户端背后的模型调用问题。MCP 负责把数据库能力暴露给 AI,但 AI 本身要能跑起来,需要一个稳定的模型入口。TaoToken 在这里扮演的是模型接入层的角色:它提供统一的 API 入口,兼容主流模型调用格式,你拿到 Key 之后,Claude Code、Cline、Cursor 这类客户端都能接。
先注册并登录控制台,地址是 https://taotoken.net/console 。进去之后左侧菜单找「API Keys」,新建一个 Key。建议按用途命名,比如mysql-mcp-dev,方便后面区分。Key 生成后只显示一次,复制到安全的地方,别直接写进会提交到 Git 的配置文件。
拿到 Key 之后,你需要确认两件事:Base URL 和 Model ID。Base URL 用 https://taotoken.net/api ,这是不带 UTM 的纯 API 地址,配置到客户端时用这个。Model ID 根据你用的模型填,比如claude-sonnet-4-5或gpt-4o这类,具体以控制台「模型对话」页面列出的为准。你可以先在 https://taotoken.net/models 里发一条测试消息,确认 Key 和模型都通,再去配 MCP。
这里有个容易踩的坑:很多人把 MCP Server 的配置和模型 API 的配置混在一起。它们是两层东西——MCP Server 负责「AI 能调用哪些工具」,模型 API 负责「AI 用哪个大脑思考」。两层都要配,但配置文件位置不同。模型 API 配在客户端的模型设置里,MCP Server 配在客户端的 MCP 配置里。
如果你打算长期跑数据校验这类编码/Agent 任务,可以看下 Coding Plan,地址是 https://taotoken.net/coding-plan ,适合高频调用场景。只是偶尔验证一下模型通不通,用「模型对话」页面就够了。
另外,Claude Code 用户如果要用 Anthropic 格式接入,可以参考 https://taotoken.net/claude-code-anthropic 这个页面,里面有 Base URL、Key、Model ID 三件套的填法。接入文档在 https://taotoken.net/doc ,遇到 401 或模型找不到的报错,先翻文档里的排障章节。
3. 可复制的 MySQL MCP Server 配置片段
这一节是全文的核心操作部分。我会给出一份完整的 MCP Server 配置,包含 MySQL 连接参数、校验工具定义、以及客户端侧的 JSON 配置。你可以直接复制改路径和账号密码。
先看服务端。用 Python 写一个最小可用的 MySQL 校验 MCP Server,依赖mcp和mysql-connector-python。安装命令:
python -m venv mcp-mysql-env source mcp-mysql-env/bin/activate pip install mcp mysql-connector-python python-dotenv服务端核心逻辑分三块:连接管理、SQL 安全校验、工具注册。连接参数从环境变量读,避免硬编码。下面这份mysql_check_server.py可以直接用:
#!/usr/bin/env python3 import asyncio import json import os import logging from typing import Any import mysql.connector from dotenv import load_dotenv from mcp.server import Server, NotificationOptions from mcp.server.models import InitializationOptions import mcp.server.stdio import mcp.types as types load_dotenv() logging.basicConfig(level=logging.INFO) logger = logging.getLogger("mysql-check-mcp") DB_CONFIG = { "host": os.getenv("MYSQL_HOST", "127.0.0.1"), "port": int(os.getenv("MYSQL_PORT", "3306")), "user": os.getenv("MYSQL_USER", "readonly_user"), "password": os.getenv("MYSQL_PASSWORD", ""), "database": os.getenv("MYSQL_DATABASE", ""), "use_pure": True, "connection_timeout": 10, } FORBIDDEN = ["DROP", "TRUNCATE", "ALTER", "CREATE", "INSERT", "UPDATE", "DELETE", "GRANT", "REVOKE"] def is_safe_select(sql: str) -> bool: s = sql.strip().upper() if not s.startswith("SELECT"): return False for kw in FORBIDDEN: if kw in s: return False if ";" in sql.rstrip(";"): return False return True def get_conn(): return mysql.connector.connect(**DB_CONFIG) server = Server("mysql-check-mcp") @server.list_tools() async def list_tools() -> list[types.Tool]: return [ types.Tool( name="run_check_sql", description="执行只读校验 SQL(仅 SELECT),返回结果集。用于数据质量巡检。", inputSchema={ "type": "object", "properties": { "sql": {"type": "string", "description": "只读 SELECT 校验语句"}, "limit": {"type": "integer", "default": 200} }, "required": ["sql"] } ), types.Tool( name="list_tables", description="列出当前库所有表名", inputSchema={"type": "object", "properties": {}} ), types.Tool( name="describe_table", description="获取指定表的列定义、类型、是否可空", inputSchema={ "type": "object", "properties": {"table": {"type": "string"}}, "required": ["table"] } ), types.Tool( name="check_null_ratio", description="统计指定表指定列的空值率,用于空值巡检", inputSchema={ "type": "object", "properties": { "table": {"type": "string"}, "column": {"type": "string"} }, "required": ["table", "column"] } ), ] @server.call_tool() async def call_tool(name: str, arguments: dict) -> list[types.TextContent]: try: if name == "run_check_sql": sql = arguments.get("sql", "") limit = arguments.get("limit", 200) if not is_safe_select(sql): return [types.TextContent(type="text", text=json.dumps( {"ok": False, "error": "仅允许单条只读 SELECT 语句"}, ensure_ascii=False))] if "LIMIT" not in sql.upper(): sql = f"{sql.rstrip(';')} LIMIT {limit}" conn = get_conn() cur = conn.cursor(dictionary=True) cur.execute(sql) rows = cur.fetchall() cur.close() conn.close() return [types.TextContent(type="text", text=json.dumps( {"ok": True, "row_count": len(rows), "rows": rows}, ensure_ascii=False, default=str))] if name == "list_tables": conn = get_conn() cur = conn.cursor() cur.execute("SHOW TABLES") tables = [r[0] for r in cur.fetchall()] cur.close() conn.close() return [types.TextContent(type="text", text=json.dumps( {"ok": True, "tables": tables}, ensure_ascii=False))] if name == "describe_table": table = arguments["table"] conn = get_conn() cur = conn.cursor(dictionary=True) cur.execute(f"DESCRIBE `{table}`") cols = cur.fetchall() cur.close() conn.close() return [types.TextContent(type="text", text=json.dumps( {"ok": True, "columns": cols}, ensure_ascii=False, default=str))] if name == "check_null_ratio": table = arguments["table"] column = arguments["column"] sql = (f"SELECT COUNT(*) AS total, " f"SUM(CASE WHEN `{column}` IS NULL THEN 1 ELSE 0 END) AS null_cnt " f"FROM `{table}`") conn = get_conn() cur = conn.cursor(dictionary=True) cur.execute(sql) row = cur.fetchone() cur.close() conn.close() total = row["total"] or 1 ratio = round((row["null_cnt"] or 0) / total, 4) return [types.TextContent(type="text", text=json.dumps( {"ok": True, "table": table, "column": column, "total": row["total"], "null_count": row["null_cnt"], "null_ratio": ratio}, ensure_ascii=False))] return [types.TextContent(type="text", text=json.dumps( {"ok": False, "error": f"未知工具 {name}"}, ensure_ascii=False))] except Exception as e: logger.error("tool call failed: %s", e) return [types.TextContent(type="text", text=json.dumps( {"ok": False, "error": str(e)}, ensure_ascii=False))] async def main(): async with mcp.server.stdio.stdio_server() as (read, write): await server.run( read, write, InitializationOptions( server_name="mysql-check-mcp", server_version="1.0.0", capabilities=server.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={} ) ) ) if __name__ == "__main__": asyncio.run(main())环境变量文件.env放在同目录:
MYSQL_HOST=127.0.0.1 MYSQL_PORT=3306 MYSQL_USER=readonly_user MYSQL_PASSWORD=your_secure_password MYSQL_DATABASE=your_db客户端侧,以 Cline 或 Claude Desktop 为例,MCP 配置 JSON 如下。注意command指向虚拟环境里的 python,args指向脚本绝对路径:
{ "mcpServers": { "mysql-check": { "command": "/path/to/mcp-mysql-env/bin/python", "args": ["/path/to/mysql_check_server.py"], "env": { "MYSQL_HOST": "127.0.0.1", "MYSQL_PORT": "3306", "MYSQL_USER": "readonly_user", "MYSQL_PASSWORD": "your_secure_password", "MYSQL_DATABASE": "your_db" } } } }如果你用 Codex 系客户端,认证信息写在auth.json里,MCP 配置单独一份,两者不要混。Base URL 填 https://taotoken.net/api ,Key 填控制台生成的,Model ID 按实际模型填。三件套齐了,客户端才能既连上模型、又连上 MCP Server。
MySQL 侧务必建一个只读账号,别用 root:
CREATE USER 'readonly_user'@'%' IDENTIFIED BY 'your_secure_password'; GRANT SELECT ON your_db.* TO 'readonly_user'@'%'; FLUSH PRIVILEGES;这一步是安全底线。MCP Server 里的 SQL 黑名单只是第二道防线,数据库权限才是第一道。
4. 验证请求:在 AI 客户端触发校验并核对结果
配置写完之后,重启客户端,让它重新加载 MCP Server。以 Cline 为例,打开 MCP 面板,应该能看到mysql-check这个 server,状态是 connected,展开后列出四个工具:run_check_sql、list_tables、describe_table、check_null_ratio。如果状态是 failed,先看客户端日志里的 stderr,通常是路径写错或依赖没装。
验证分三步走。第一步,让 AI 列出表:
帮我列出当前数据库里所有的表。
AI 会调用list_tables,返回一个表名数组。这一步验证的是 MCP 通信链路通不通。如果这里就报错,后面不用继续。
第二步,让 AI 查表结构:
看一下 orders 表的列定义。
AI 调用describe_table,返回列名、类型、是否可空。这一步验证的是工具参数传递是否正确。
第三步,跑一条真实校验。假设 orders 表有order_date、amount、status三列,你想查昨日金额为负的异常订单:
帮我检查 orders 表里昨天创建的订单,有没有金额小于 0 的异常记录,把订单号和金额列出来。
AI 会生成类似这样的 SQL 并调用run_check_sql:
SELECT order_id, amount, status FROM orders WHERE order_date = CURDATE() - INTERVAL 1 DAY AND amount < 0返回结果是一个 JSON,包含ok、row_count、rows。如果row_count为 0,说明没有异常;如果大于 0,AI 会把 rows 里的订单号念给你听。你可以拿这个结果和手动跑 SQL 的结果对一下,确认一致。
再验证一个空值巡检场景:
统计 customers 表里 email 列的空值率。
AI 调用check_null_ratio,返回total、null_count、null_ratio。这个工具的好处是把「统计空值」这种高频校验固化成了参数化调用,AI 不用每次现写 SQL,减少出错。
实测下来,从配置到跑通第一条校验,顺利的话十分钟内能完成。卡点通常在两处:一是 Python 路径没指向虚拟环境,导致mcp模块找不到;二是 MySQL 账号权限没给够,SHOW TABLES能过但SELECT被拒。这两处排查方法在下一节展开。
5. 本篇常见报错排查
这一节按真实报错来。你在配 MySQL MCP Server 时,大概率会遇到下面几类问题。
报错一:401 Unauthorized 或 invalid api key
这个报错来自模型 API 层,不是 MCP 层。说明客户端里的 Key 填错了,或者 Base URL 写成了带 UTM 的地址。检查两点:Base URL 必须是 https://taotoken.net/api ,不要带任何查询参数;Key 从控制台重新复制一次,注意前后不要有空格。如果用的是 Claude Code 的 Anthropic 格式,确认ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY都指向 TaoToken 的地址和 Key,Model ID 填控制台列出的名称。改完重启客户端。
报错二:local proxy failed 或 connection refused
这个通常出现在 MCP Server 启动阶段。客户端尝试用 stdio 拉起 Python 进程,但进程没起来。原因可能是command路径写错,比如写成了系统 python 而不是虚拟环境里的 python。用绝对路径,先手动在终端跑一遍:
/path/to/mcp-mysql-env/bin/python /path/to/mysql_check_server.py如果手动跑能启动并停在等待输入的状态,说明脚本没问题,是客户端配置路径的问题。如果手动跑就报ModuleNotFoundError: No module named 'mcp',说明依赖装到了别的环境,重新在虚拟环境里pip install mcp mysql-connector-python。
报错三:reading choices 或 model not found
这个报错说明模型名填错了。客户端把 Model ID 原样传给 API,如果这个 ID 不在可用列表里,就返回找不到。去 https://taotoken.net/models 页面确认当前可用的模型名,复制准确的 ID 填进去。注意大小写和连字符,claude-sonnet-4-5和claude-sonnet-4.5是两回事。
报错四:OAuth 相关报错或 unauthorized_client
如果你用的是需要 OAuth 的客户端,而 MCP Server 这边没配认证,会出现这类报错。MCP 协议本身支持 OAuth,但本文的 MySQL 校验 Server 走的是本地 stdio,不涉及 OAuth。出现这个报错通常是客户端把 MCP 认证和模型认证搞混了。检查客户端的 MCP 配置里有没有多余的 auth 字段,删掉;模型认证单独在模型设置里配。
报错五:MySQL 连接超时或 access denied
Access denied for user 'readonly_user'@'localhost'说明账号密码不对,或者该账号没有从当前主机连接的权限。检查CREATE USER时的 host 部分,本地连接用'readonly_user'@'localhost',远程用'%'。Connection timed out说明 host 或 port 不通,先用mysql -h 127.0.0.1 -P 3306 -u readonly_user -p手动连一次,确认网络和端口。
报错六:工具调用返回「仅允许单条只读 SELECT 语句」
这是 Server 里的安全校验拦下来的。检查你的 SQL 是不是以 SELECT 开头,有没有分号拼接多条语句,有没有包含 UPDATE、DELETE 这类关键词。注意子查询里如果出现这些词也会被拦,比如SELECT * FROM (DELETE ...)这种非法写法。正常校验 SQL 不会触发。
排查顺序建议:先确认模型 API 通(模型对话页面发消息),再确认 MCP Server 能手动启动,最后确认客户端配置路径正确。三层分开排查,比一上来就改配置高效。
6. 把校验能力沉淀成可复用工具链
跑通单条校验之后,真正有价值的是把校验规则沉淀下来。你可以把常用的校验 SQL 做成模板,让 AI 按模板调用。比如订单金额非负校验、用户邮箱空值率、外键孤儿检查,各写一条参数化 SQL,存成一个checks.yaml:
checks: - name: negative_order_amount sql: "SELECT order_id, amount FROM orders WHERE order_date = CURDATE() - INTERVAL 1 DAY AND amount < 0" - name: orphan_orders sql: "SELECT o.order_id FROM orders o LEFT JOIN customers c ON o.customer_id = c.id WHERE c.id IS NULL" - name: null_email_ratio sql: "SELECT COUNT(*) AS total, SUM(CASE WHEN email IS NULL THEN 1 ELSE 0 END) AS null_cnt FROM customers"然后在 MCP Server 里加一个run_named_check工具,读 yaml 按名字执行。这样 AI 只需要说「跑一下 negative_order_amount 这个检查」,不用每次现写 SQL。规则集中管理,改一处全生效。
再进一步,可以把校验结果写回一张data_quality_log表,记录检查名、执行时间、异常行数。MCP Server 加一个log_check_result工具,但注意这个工具需要写权限,得单独用一个有 INSERT 权限的账号,和只读账号分开。这是权限最小化原则的体现:读校验用只读账号,写日志用受限写账号,两个账号不混用。
如果你团队用 Cline 或 CC Switch 管理多个 MCP Server,可以把 MySQL 校验 Server 和文件系统 Server、Git Server 并列配置,让 AI 在一次对话里既能查库、又能读代码、又能提交报告。CC Switch 的配置里,每个 server 独立一段,Base URL、Key、Model ID 三件套在模型层配一次即可。
长期跑数据质量巡检的话,建议把 MCP Server 用 systemd 或 supervisor 托管,避免客户端重启后进程丢失。日志输出到文件,方便回溯每次校验的执行记录。校验频率高的场景,可以在 Server 里加连接池,减少频繁建连的开销。
最后提醒一句:MCP Server 暴露的是能力,不是数据本身。AI 看到的是工具返回的 JSON,不是整张表。这个边界设计好了,数据安全就有保障。校验 SQL 尽量带 WHERE 条件和 LIMIT,避免全表扫描把大结果集塞给模型。工具描述里写清楚用途和限制,AI 调用时会更准确。
整套流程跑下来,你会发现数据校验从「人写 SQL 人看结果」变成了「人说需求 AI 跑校验」,重复劳动大幅减少。而安全边界由只读账号、SQL 黑名单、LIMIT 保护三层守住,比让 AI 直连数据库靠谱得多。