1. 从自然语言到 SQL:数据库 MCP Server 到底解决什么问题
数据库 MCP Server 是一个把「自然语言请求」翻译成「可执行 SQL」并安全落库的中间层服务。它对外暴露 MCP 协议接口,对内封装数据库连接、SQL 生成、权限校验和执行审计。适合谁?适合手里有 MySQL/PostgreSQL、想让 AI Agent 直接查数、又不想把数据库账号密码硬塞进 Agent 代码里的开发者。
传统做法是:Agent 里写死数据库连接串,模型生成 SQL 后直接执行。问题有三个。第一,模型生成的 SQL 不可控,一句DROP TABLE就可能把生产库干废。第二,每个 Agent 都要重复实现数据库连接、重试、结果序列化。第三,模型调用要走大模型 API,Key 散落在各个项目里,换一次 Key 要改一堆地方。
MCP Server 的思路是把这些脏活收拢到一层服务里。Agent 只负责「理解意图 + 决定调哪个工具」,MCP Server 负责「生成 SQL + 校验 + 执行 + 返回结构化结果」。大模型的调用统一走一个 API 通道,Key 只配一次。
我试过把这条链路拆成三段来理解:模型层(负责把中文转成 SQL 草稿)、工具层(负责校验和执行)、协议层(负责让 Agent 按标准格式调用)。三段之间用统一的 Base URL 和 Key 串起来,这样换模型、换数据库都只动一个配置点。
这里的关键角色是 TaoToken。它提供统一的 API 通道,把大模型调用收敛成一个 OpenAI 兼容的接口。你不需要在 MCP Server 里分别对接各家模型的 SDK,只要把 Base URL 指向https://taotoken.net/api,用同一个 Key 就能调不同模型。对数据库 MCP Server 来说,这意味着「SQL 生成」这一步可以随时换模型做 A/B 测试,而不用改工具层代码。
具体能做什么?举几个真实场景。运营同学问「上个月华东区退款率最高的三个品类是什么」,Agent 通过 MCP 调用db_query工具,模型生成 SQL,Server 执行后返回表格。开发同学问「orders 表里 status 字段有哪些取值」,同样一条链路。区别只在于模型生成的 SQL 复杂度不同,Server 的校验规则可以按操作类型分级。
适合谁上手?如果你已经会用 Python 写 Flask/FastAPI,懂基本的 SQL,并且手上有一个能连的数据库,那这篇的路径可以直接跟做。如果你只是想验证模型能不能生成正确 SQL,可以先跳到第 4 节用一条查询跑通端到端,再回头补配置。
需要提前说明的是:MCP Server 不是让模型直接连生产库。正确的姿势是给 MCP Server 配一个只读账号,写操作走单独的审批通道。这一点在第 5 节的排错里会展开,因为很多 401 和权限报错都源于账号配置不对。
2. 前置准备:用 TaoToken 统一 Key 打通模型调用通道
在写 MCP Server 代码之前,先把模型调用通道配好。这一步的核心是拿到一个能用的 API Key,并确认 Base URL 指向正确。TaoToken 的 API 地址是https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为 OpenAI 兼容的 base_url 使用。
先注册并创建 Key。打开https://taotoken.net/api-keys,登录后在控制台创建新的 API Key。创建时建议按用途命名,比如mcp-db-server-dev,这样后面排查哪个 Key 被滥用时能快速定位。Key 只在创建时显示一次,复制后存到环境变量里,不要写进代码。
拿到 Key 后,先做一次最小验证,确认通道是通的。用 curl 发一个 chat completions 请求:
export TAOTOKEN_API_KEY="sk-你的Key" curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "只回复两个字:通了"}] }'如果返回的 JSON 里choices[0].message.content是「通了」,说明 Key 和通道都没问题。如果返回 401,先检查 Key 有没有复制完整、有没有多余空格。如果返回model not found,说明模型 ID 写错了,去https://taotoken.net/models看当前可用的模型列表。
接下来配置 MCP Server 项目。我习惯用 Python 虚拟环境隔离依赖:
python -m venv venv source venv/bin/activate pip install fastapi uvicorn openai sqlalchemy pymysql python-dotenv这里openai包用来调 TaoToken 的兼容接口,sqlalchemy管数据库连接,pymysql是 MySQL 驱动。如果你用 PostgreSQL,把pymysql换成psycopg2-binary。
创建.env文件,把敏感配置集中管理:
TAOTOKEN_API_KEY=sk-你的Key TAOTOKEN_BASE_URL=https://taotoken.net/api DB_URL=mysql+pymysql://readonly_user:password@127.0.0.1:3306/biz_db DEFAULT_MODEL=gpt-4o-mini注意DB_URL里的账号建议用只读账号。MCP Server 的查询工具默认只执行 SELECT,写操作单独开工具并加审批。这样即使模型生成了危险 SQL,第一道闸门就拦住了。
关于模型选择,SQL 生成对模型的指令遵循能力要求较高。实测下来,gpt-4o-mini在简单查询上够用,复杂多表 JOIN 建议换更强的模型。TaoToken 的好处是换模型只改DEFAULT_MODEL一个值,不用动代码。你可以先用小模型跑通链路,再按需升级。
如果你打算长期跑 Agent 任务,可以了解下 Coding Plan,它适合需要持续调用模型做代码生成和任务编排的场景。入口在https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite。不过对于本篇的数据库 MCP Server,按量调用就够,先把链路跑通更重要。
配置完成后,写一个config.py把环境变量读进来:
import os from dotenv import load_dotenv load_dotenv() TAOTOKEN_API_KEY = os.getenv("TAOTOKEN_API_KEY") TAOTOKEN_BASE_URL = os.getenv("TAOTOKEN_BASE_URL", "https://taotoken.net/api") DB_URL = os.getenv("DB_URL") DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "gpt-4o-mini")到这里前置准备就完成了。下一步是把模型调用和数据库工具接起来,写成可复制的配置片段。
3. 可复制配置:MCP Server 工具注册与模型接入片段
这一节给出可以直接复制运行的配置。先写模型客户端,再写数据库工具,最后写 MCP 协议入口。三部分拼起来就是一个最小可用的 MCP Server。
先看模型客户端。用 OpenAI SDK 指向 TaoToken 的 Base URL:
from openai import OpenAI from config import TAOTOKEN_API_KEY, TAOTOKEN_BASE_URL, DEFAULT_MODEL client = OpenAI( api_key=TAOTOKEN_API_KEY, base_url=TAOTOKEN_BASE_URL, ) def generate_sql(user_query: str, schema_hint: str) -> str: system_prompt = f"""你是一个 SQL 生成器。根据用户问题生成一条 SELECT 语句。 数据库表结构如下: {schema_hint} 要求: 1. 只输出 SQL,不要解释。 2. 只允许 SELECT,禁止 INSERT/UPDATE/DELETE/DROP。 3. 表名和字段名必须来自上面的结构。 """ resp = client.chat.completions.create( model=DEFAULT_MODEL, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_query}, ], temperature=0, ) return resp.choices[0].message.content.strip()这段代码的关键点是base_url指向 TaoToken,api_key用统一 Key。换模型只改DEFAULT_MODEL。temperature=0是为了让 SQL 生成稳定,避免同一个问题每次生成不同写法。
接下来是数据库工具。用 SQLAlchemy 管理连接,加一层 SQL 白名单校验:
import re from sqlalchemy import create_engine, text from config import DB_URL engine = create_engine(DB_URL, pool_pre_ping=True) FORBIDDEN = re.compile( r"\b(insert|update|delete|drop|alter|truncate|grant|create)\b", re.IGNORECASE, ) def safe_query(sql: str, limit: int = 200): if FORBIDDEN.search(sql): raise ValueError("检测到非查询语句,已拦截") if not sql.strip().lower().startswith("select"): raise ValueError("只允许 SELECT 语句") if " limit " not in sql.lower(): sql = sql.rstrip(";") + f" LIMIT {limit}" with engine.connect() as conn: result = conn.execute(text(sql)) return [dict(row._mapping) for row in result]FORBIDDEN正则做第一层拦截,startswith("select")做第二层。自动补LIMIT是防止模型生成全表扫描把内存打爆。这两层加起来,基本能挡住大部分误操作。
然后是 MCP 协议入口。MCP 的工具注册本质是暴露一个 JSON Schema 描述,让 Agent 知道有哪些工具、参数是什么。用 FastAPI 写一个最小实现:
from fastapi import FastAPI, HTTPException from pydantic import BaseModel from tools import generate_sql, safe_query app = FastAPI() class QueryRequest(BaseModel): query: str SCHEMA_HINT = """ 表 orders:id, user_id, amount, status, created_at, region 表 users:id, name, email, created_at """ @app.post("/mcp/tools/db_query") def db_query(req: QueryRequest): try: sql = generate_sql(req.query, SCHEMA_HINT) rows = safe_query(sql) return {"sql": sql, "rows": rows, "count": len(rows)} except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=f"执行失败: {e}")这个接口接收自然语言,返回生成的 SQL 和查询结果。Agent 侧只需要按 MCP 的工具调用格式发 POST 请求即可。SCHEMA_HINT是给模型的表结构提示,实际项目里可以从information_schema动态读取,避免手写维护。
如果你用 Claude Code 或 Cline 这类支持 MCP 的客户端,配置片段长这样:
{ "mcpServers": { "db-server": { "url": "http://127.0.0.1:8000/mcp", "env": { "TAOTOKEN_API_KEY": "sk-你的Key", "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "DEFAULT_MODEL": "gpt-4o-mini" } } } }注意这里三件套齐全:Base URL、Key、Model ID。缺任何一个都会在调用时报错。如果你用的是 Codex 的auth.json体系,把 Key 填到对应字段,Base URL 指向 TaoToken 的 API 地址即可。
启动服务:
uvicorn main:app --host 127.0.0.1 --port 8000到这里配置片段就齐了。下一节用一条真实查询验证整条链路。
4. 端到端验证:一条自然语言查询跑通 Agent 到 SQL
验证的目标是:发一句中文,MCP Server 返回正确的 SQL 和查询结果。先准备一张测试表,插几条数据:
CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT, amount DECIMAL(10,2), status VARCHAR(20), region VARCHAR(20), created_at DATETIME ); INSERT INTO orders (user_id, amount, status, region, created_at) VALUES (1, 12000.00, 'paid', 'east', '2024-01-15 10:00:00'), (2, 8000.00, 'paid', 'east', '2024-01-20 11:00:00'), (3, 15000.00, 'refunded', 'north', '2024-02-01 09:30:00'), (4, 3000.00, 'paid', 'south', '2024-02-10 14:00:00');然后发请求:
curl -X POST http://127.0.0.1:8000/mcp/tools/db_query \ -H "Content-Type: application/json" \ -d '{"query": "查询金额大于10000的订单,按金额降序"}'预期返回类似:
{ "sql": "SELECT * FROM orders WHERE amount > 10000 ORDER BY amount DESC LIMIT 200", "rows": [ {"id": 3, "user_id": 3, "amount": "15000.00", "status": "refunded", "region": "north", "created_at": "2024-02-01T09:30:00"}, {"id": 1, "user_id": 1, "amount": "12000.00", "status": "paid", "region": "east", "created_at": "2024-01-15T10:00:00"} ], "count": 2 }看到count: 2且 SQL 里带了LIMIT 200,说明链路通了。模型正确理解了「金额大于10000」和「降序」,Server 也自动补了限制。
再测一个多表场景。假设要查「每个用户的订单总金额」:
curl -X POST http://127.0.0.1:8000/mcp/tools/db_query \ -H "Content-Type: application/json" \ -d '{"query": "统计每个用户的订单总金额,按总金额降序"}'模型应该生成类似SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id ORDER BY total DESC的 SQL。如果生成的 SQL 里表名或字段名不对,说明SCHEMA_HINT没写清楚,回去补上字段说明。
验证安全拦截。故意发一个危险请求:
curl -X POST http://127.0.0.1:8000/mcp/tools/db_query \ -H "Content-Type: application/json" \ -d '{"query": "删除所有订单"}'预期返回 400,detail 是「检测到非查询语句,已拦截」。如果模型生成了DELETE FROM orders,FORBIDDEN正则会在执行前拦住。这一步很重要,说明安全闸门生效了。
验证模型切换。把.env里的DEFAULT_MODEL改成另一个模型,重启服务,再发同样的查询。对比两次生成的 SQL 是否一致。实测下来,简单查询不同模型差异不大,复杂 JOIN 会有明显区别。这也是用 TaoToken 统一通道的价值:换模型只改一个环境变量,不用改代码。
如果你想在图形界面里直接和模型对话验证 SQL 生成效果,可以用模型对话入口https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite,把表结构和问题贴进去,看模型生成的 SQL 是否符合预期,再决定要不要接进 MCP Server。
端到端跑通后,你会得到一条完整的链路:中文问题 → 模型生成 SQL → 安全校验 → 数据库执行 → 结构化返回。这条链路可以复用到任何查询场景。
5. 常见报错排查:401、local proxy failed 与 SQL 生成异常
这一节按真实报错来排查。第一个高频错误是 401 Unauthorized。表现是调用模型接口时返回:
{"error": {"message": "Invalid API key", "type": "invalid_request_error"}}原因通常是三个:Key 没配、Key 复制不完整、Key 前后有空格。排查步骤:先确认.env里TAOTOKEN_API_KEY的值以sk-开头且没有换行;再确认代码里读的是这个变量而不是硬编码的旧 Key;最后用第 2 节的 curl 命令单独测一次,排除是 MCP Server 代码问题还是 Key 本身问题。如果 curl 也 401,去https://taotoken.net/api-keys重新生成一个 Key。
第二个错误是local proxy failed或连接超时。表现是请求发不出去,报Connection refused或timeout。先检查TAOTOKEN_BASE_URL是不是写成了https://taotoken.net/api/(末尾多了斜杠),有些 SDK 拼接路径时会出问题。再检查本机网络能不能访问外网,用curl -I https://taotoken.net/api看返回码。如果返回 200 或 401,说明网络通,问题在 Key;如果直接超时,检查本机 DNS 和防火墙设置。
第三个错误是reading choices相关报错,比如KeyError: 'choices'或list index out of range。这通常说明返回的 JSON 结构不是预期的 chat completions 格式。原因可能是模型 ID 写错了,服务端返回了错误信息而不是正常响应。排查方法:在generate_sql里把原始响应打出来:
resp = client.chat.completions.create(...) print(resp.model_dump())看返回里有没有choices字段。如果没有,看error字段的内容。常见的是model not found,去https://taotoken.net/models核对模型 ID 拼写。
第四个错误是 SQL 生成异常,比如模型返回了带 markdown 代码块的 SQL:
```sql SELECT * FROM orders WHERE amount > 10000这种直接丢给数据库会报语法错误。解决办法是在 `generate_sql` 里加清洗: ```python import re def clean_sql(raw: str) -> str: raw = re.sub(r"```sql|```", "", raw, flags=re.IGNORECASE) return raw.strip()第五个错误是数据库连接失败,报Access denied或Unknown database。检查DB_URL里的用户名、密码、库名、端口。如果用的是只读账号,确认它至少有目标表的 SELECT 权限。可以用mysql -u readonly_user -p -h 127.0.0.1 biz_db手动登录测试。
第六个错误是 OAuth 相关报错。如果你用 Claude Code 接入,可能会遇到OAuth token expired。这类问题通常出在客户端侧的认证配置,跟 MCP Server 本身无关。检查客户端的 MCP 配置里 Base URL 和 Key 是否填对,三件套缺一不可。如果用的是 Codex 的auth.json,确认 Key 字段名和官方要求一致。
第七个错误是查询结果为空但 SQL 看起来正确。先手动在数据库里执行生成的 SQL,确认数据本身存在。如果手动执行有结果但 MCP 返回空,检查safe_query里的LIMIT拼接有没有把 SQL 改坏。比如原 SQL 末尾有分号,拼接后变成SELECT ...; LIMIT 200,这是语法错误。代码里已经用rstrip(";")处理了,但如果你改过代码,注意保留这一步。
排错的核心思路是分层定位:先确认模型通道通不通(curl 测),再确认数据库连不连得上(手动登录测),最后确认 MCP Server 的拼接逻辑对不对(打印中间变量)。三层分开测,比一次性猜问题快得多。
6. 把链路固定下来:从验证到日常使用的配置建议
链路跑通后,下一步是让它稳定可用。几个实操建议。
第一,把SCHEMA_HINT改成动态读取。手写表结构容易漏字段,用一条查询从information_schema拉:
def load_schema(): sql = """ SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_schema = 'biz_db' ORDER BY table_name, ordinal_position """ rows = safe_query(sql) lines = [] current = None for r in rows: if r["table_name"] != current: current = r["table_name"] lines.append(f"\n表 {current}:") lines.append(f" {r['column_name']} {r['data_type']}") return "\n".join(lines)这样加表加字段不用改代码,模型每次拿到的都是最新结构。
第二,给safe_query加超时。数据库慢查询会拖垮整个服务:
with engine.connect() as conn: conn.execute(text("SET SESSION MAX_EXECUTION_TIME=5000")) result = conn.execute(text(sql))MySQL 用MAX_EXECUTION_TIME,PostgreSQL 用statement_timeout。5 秒够大部分查询用,超时直接中断。
第三,把每次调用的 SQL 和耗时记下来。不用上重型日志系统,写个本地文件就够:
import time, json, datetime def log_call(query, sql, elapsed, count): with open("mcp_calls.log", "a") as f: f.write(json.dumps({ "ts": datetime.datetime.now().isoformat(), "query": query, "sql": sql, "elapsed_ms": round(elapsed * 1000), "rows": count, }, ensure_ascii=False) + "\n")跑一段时间后翻日志,能看出哪些问题模型经常生成错误 SQL,针对性优化SCHEMA_HINT或换模型。
第四,写操作单独开工具并加确认。查询工具保持只读,db_insert、db_update这类工具在 MCP 配置里标记为需要人工确认。Agent 调用时先返回待执行 SQL,人工确认后再执行。这样既保留了自动化能力,又不会让模型直接改数据。
第五,Key 轮换。TaoToken 的 Key 支持在控制台管理,建议按环境分 Key:开发一个、生产一个。生产 Key 只配在服务器环境变量里,不进代码仓库。轮换时在控制台新建 Key,更新环境变量,重启服务,旧 Key 再删除。
如果你要把这套 MCP Server 接到长期运行的 Agent 任务里,比如每天定时跑数据巡检,可以看下 Coding Plan 的额度方案,入口在https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite。按量调用适合验证期,固定额度适合稳定跑任务。
最后一步是把 MCP Server 注册到你的 Agent 客户端。以 Claude Code 为例,在 MCP 配置里加上第 3 节给的 JSON 片段,重启客户端,用/mcp命令确认db-server已连接。然后直接在对话里问「orders 表里有多少条记录」,Agent 会自动调用db_query工具,返回结果。到这一步,从自然语言到 SQL 的整条链路就固定下来了。
后续要扩展的话,方向有三个:加更多数据库类型(PostgreSQL、SQLite)、加结果导出工具(CSV/Excel)、加多轮上下文(记住上一句查询的表)。每加一个工具,都在 MCP 配置里注册一次,模型会自动学会调用。核心的模型通道和安全校验不用动,这就是把 Key 统一到 TaoToken 之后的好处:扩展只发生在工具层,模型层保持稳定。