一篇让金仓数据库"开口说话"的实战文。100 行 Python + Anthropic 官方 mcp SDK,做出一个 6 工具 / 3 道安全闸的 MCP Server,让任何 AI 客户端(Claude Desktop / Claude Code / Cursor)能用中文问金仓------不仅能查数据,还能让 AI 看执行计划、自动给出索引 DDL 建议。代码、协议往返、攻击拦截、自动调优全过程实测,环境 KingbaseES V009R001C010。
一、为什么"AI + 数据库"是国产数据库的下一个护城河
国产数据库的 SQL 兼容性、TPC-C 跑分已经卷得差不多了,下一站的差距其实在生态------尤其是"AI 原生访问数据库"的能力。
2025 年 Anthropic 推出的 MCP(Model Context Protocol) 把这件事的解法定了型:它是一个开放协议,让 AI 应用与外部工具之间用标准格式通信。你把数据库能力包装成一个 MCP Server,任何 MCP 兼容的客户端接上来(Claude Desktop、Cline、Cursor、Claude Code......),就能让大模型自己决定"该查哪张表、写什么 SQL",然后用大白话把结果讲出来。
金仓是 PG 内核 ------这意味着我们不需要等官方出 MCP Server,用 psycopg2 + 官方 mcp SDK 就能自己手撸一个,100 行核心代码就能跑通。
本文的差异化
中文圈已有的金仓 MCP 实践(如 CSDN 上的若干文章)大多停在"4 工具 + 简单 SELECT"的水平。本文在此基础上多走两步:
- 新增
explain_plan和suggest_index两个差异化工具 ------让 AI 不只查数据,还能看执行计划、自动给出索引 DDL 建议。这是把金仓 KWR/KDDM 性能诊断思路搬到 AI 工具链里的关键一步。 - 接 Claude Code(不是 Claude Desktop)------Claude Code 是程序员的日常 IDE,AI 工具直接长在工位上,DBA 写代码时就能用。
- 配套的 100 万行真实订单表------不是 10 行玩具数据,所有数字实测。
技术栈:
| 组件 | 版本 | 角色 |
|---|---|---|
| KingbaseES | V009R001C010 | PG 内核国产数据库 |
| psycopg2-binary | 2.9.12 | PG 协议驱动 |
| mcp 官方 Python SDK | 1.27.2 | FastMCP Server |
| Python | 3.14 | 运行时 |
| Claude Code | 任意版本 | MCP 客户端 |
二、架构:100 行代码的"翻译官"
整条链路只有三个角色:
Python
[AI 客户端] [MCP Server] [KingbaseES]
Claude Code stdio kingbase_mcp.py SQL ai_ro 只读账号
Claude Desktop ←───── 6 个 tool ←──── t_order 100 万行
Cursor 3 道安全闸
MCP Server 向 AI 暴露 6 个工具(Tool),大模型像调用函数一样调用它们:
| 工具 | 作用 | 设计意图 |
|---|---|---|
| db_info | 数据库身份(版本/兼容模式/当前用户) | 验明后端真身 |
| list_tables | 列业务表 + 估算行数 | 让 AI"看清战场"再动手 |
| describe_table | 表结构(字段名/类型)+ 3 行样例 | 让 AI 写 SQL 时不会瞎拼字段名 |
| run_query | 执行只读 SELECT(最多 50 行) | 核心查数据能力 |
| explain_plan | EXPLAIN ANALYZE 任意 SELECT | 差异化:让 AI 看到执行计划 |
| suggest_index | 基于 EXPLAIN 自动给索引 DDL | 差异化核心:AI 自动调优 |
用 Anthropic 官方 mcp SDK 的 FastMCP,一个装饰器就把普通 Python 函数变成 MCP 工具:
python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("kingbase")
@mcp.tool()
def run_query(sql: str) -> str:
"""执行一条只读 SELECT 查询,返回最多 50 行结果。"""
... # 三道安全闸 + psycopg2 执行
if name == "main":
mcp.run() # stdio 传输
关键认知 :工具函数的 docstring 不是注释,是给大模型看的说明书------Agent 靠它判断什么时候该调哪个工具。这是写 MCP Server 和写普通后端接口最不一样的地方:你的注释第一次有了"读者是 AI"。
三、三道安全闸:把 AI 关进笼子里
Demo 谁都能跑通,能不能上生产,全看安全 。一个能连生产库、还听大模型指挥的服务,如果只做到"能查",那不是功能,是事故。本文给 Server 设计了 三道互相独立、任意一道都能单独兜底 的闸门。
闸 1:数据库账号本身只读
MCP Server 连库用的 ai_ro 账号只被 GRANT SELECT 。建账号的脚本(step4_mcp/setup_ai_ro.py):
sql
CREATE USER ai_ro WITH PASSWORD 'Kingbase@2026';
GRANT USAGE ON SCHEMA public TO ai_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ai_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO ai_ro; -- 未来新建的表也自动授权
实测验证(脚本里直接拿 ai_ro 连库执行 DELETE):
python
→ 试图 DELETE FROM t_order(应被权限层挡掉):
✓ 闸 1 生效:对表 t_order 权限不够
→ 试图 DROP TABLE(应被权限层挡掉):
✓ 闸 1 生效:必须是表 t_order 的属主
这是最硬的一道------就算前面两道全被绕过,数据库自己也不会让 AI 写入。
闸 2:会话级只读 + 超时
代码里每次开连接都强制:
python
c.set_session(readonly=True) # 会话级只读
options=f"-c statement_timeout=5000" # 5 秒超时,防 AI 写出慢查询拖垮库
闸 3:应用层 SQL 白名单
python
run_query / explain_plan / suggest_index 都共用一道正则白名单:
_WRITE_KEYWORDS = re.compile(
r"\b(insert|update|delete|drop|truncate|alter|create|grant|revoke|"
r"merge|vacuum|reindex|call|do|copy)\b", re.IGNORECASE)
_MULTI_STMT = re.compile(r";\s*\S") # 分号后还有内容 → 多语句注入
def _gate_sql(sql):
s = sql.strip().rstrip(";").strip()
if _MULTI_STMT.search(s):
return False, "已拒绝:只允许单条语句"
head = s.split(None, 1)[0].upper()
if head not in ("SELECT", "WITH"):
return False, f"已拒绝:只允许 SELECT/WITH(实际开头: {head})"
if _WRITE_KEYWORDS.search(s):
return False, "已拒绝:检测到写操作关键字"
return True, "OK"
安全设计哲学 :三道闸冗余但不多余。应用层白名单可能被更刁钻的 SQL 绕过,但会话只读会挡;会话只读万一失效,账号权限还在。安全不赌"我的正则天衣无缝",安全赌"攻破一层还有下一层"。
四、实战 1:协议握手 + 6 工具跑通
光说不算,我用官方 mcp SDK 写了个真正的 MCP 协议客户端 去连它(step4_mcp/test_client.py),走完整的 initialize → tools/list → tools/call 握手:
python
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
params = StdioServerParameters(command="python", args=[SERVER_PATH])
async with stdio_client(params) as (r, w):
async with ClientSession(r, w) as session:
init = await session.initialize()
print(init.protocolVersion, init.serverInfo.name) # 2025-11-25 kingbase
tools = await session.list_tools() # 拉到 6 个 tool
await session.call_tool("db_info", {}) # 真实调用
握手 + 工具发现
python
✓ 协议版本: 2025-11-25
✓ Server 名: kingbase
✓ Server 版本: 1.27.2
db_info 返回数据库的身份信息:版本、兼容模式、当前连接账号、连接数。
list_tables 列出当前库下所有用户表 + 每张表的估算行数。
describe_table 返回某张表的结构(字段名/类型/可空/默认值)+ 3 行样例数据。
run_query 执行一条只读 SELECT/WITH 查询,返回最多 50 行结果 + 总匹配行数。
explain_plan 对一条 SELECT 跑 EXPLAIN ANALYZE,返回执行计划原文。
suggest_index 基于 EXPLAIN ANALYZE 自动给出索引建议。

工具调用:db_info
python
{
"version": "KingbaseES V009R001C010",
"database_mode": "mysql",
"current_user": "ai_ro",
"database": "test",
"active_connections": 12,
"readonly_account": "ai_ro"
}
后端确实是金仓 V009R001C010,连接身份是只读账号 ai_ro------验明正身。
工具调用:list_tables + describe_table
python
{
"tables": [
{"schema": "public", "table_name": "t_order", "approx_rows": 1000000},
{"schema": "public", "table_name": "t_department","approx_rows": 3},
{"schema": "public", "table_name": "t_employee", "approx_rows": 4}
]
}
describe_table('t_order') 返回 8 个字段的完整定义 + 3 行样例------大模型靠这个才能写出字段名拼对的 SQL 。

五、实战 2:5 种越权攻击全部拦截
我把 5 种最经典的攻击手法扔给 run_query,全军覆没:
| 攻击手法 | 拦截结果 | 拦截闸 |
|---|---|---|
| DELETE FROM t_order WHERE id = 1 | 已拒绝:只允许 SELECT/WITH(开头: DELETE) | 闸 3 |
| DROP TABLE t_order | 已拒绝:只允许 SELECT/WITH(开头: DROP) | 闸 3 |
| SELECT 1; DROP TABLE t_order(多语句注入) | 已拒绝:只允许单条语句 | 闸 3 |
| UpDaTe t_order SET amount=0 WHERE id=1(大小写绕过) | 已拒绝:开头 UPDATE | 闸 3 |
| SELECT (DELETE FROM t_order RETURNING 1)(子查询藏写) | 已拒绝:检测到写操作关键字 | 闸 3 |
JSON
{"rejected": "已拒绝:只允许 SELECT/WITH 查询(实际开头: DELETE)",
"sql": "DELETE FROM t_order WHERE id = 1"}
{"rejected": "已拒绝:只允许单条语句(检测到分号后还有内容)",
"sql": "SELECT 1; DROP TABLE t_order"}
{"rejected": "已拒绝:检测到写操作关键字(insert/update/delete/...)",
"sql": "SELECT (DELETE FROM t_order RETURNING 1)"}

注意 :闸 3 已经全部挡住,但就算某个被精心构造的 SQL 绕过了正则,闸 2(会话只读)会兜底;就算会话只读也失效,**闸 1(账号级只读)**是金仓权限层的硬拦截,永远生效。
六、实战 3:差异化核心 ------ AI 自动给索引建议
这是本文最重要的差异化。前 4 个工具(list/describe/run_query/db_info)解决"AI 能查数据",但真正能帮到 DBA 的是让 AI 自己看懂执行计划、给出优化建议。
6.1 explain_plan:返回执行计划原文
让 AI 调用 explain_plan,传入一条 SELECT,就能拿到 EXPLAIN ANALYZE 的完整文本:
python
@mcp.tool()
def explain_plan(sql: str) -> str:
"""对一条 SELECT 跑 EXPLAIN ANALYZE,返回执行计划原文。"""
ok, reason = _gate_sql(sql) # 闸 3
if not ok:
return json.dumps({"rejected": reason})
with _conn() as c:
with c.cursor() as cur:
cur.execute("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) " + sql)
plan = "\n".join(r[0] for r in cur.fetchall())
return json.dumps({"sql": sql, "plan": plan}, ensure_ascii=False, indent=2)
对一条已经加过索引的 SQL(同库已有业务索引):
sql
SELECT * FROM t_order
WHERE status = 5 AND create_time BETWEEN '2025-06-01' AND '2025-09-30'
ORDER BY amount DESC LIMIT 50
返回:
python
Limit (cost=15273.72..15273.85 ...) (actual time=39.973..39.980 ...)
-> Sort (cost=15273.72..15370.08 ...) (actual time=39.971..39.975 ...)
Sort Key: amount DESC NULLS LAST
-> Bitmap Heap Scan on t_order (...rows=37351...)
-> Bitmap Index Scan on idx_order_status_time_amt
6.2 suggest_index:自动 DDL 推荐
更进一步的工具------读 EXPLAIN,自己给出索引建议:
Python
@mcp.tool()
def suggest_index(sql: str) -> str:
"""基于 EXPLAIN ANALYZE 自动给出索引建议。"""
... # 内部调 _analyze_plan
诊断逻辑(启发式规则):
- Seq Scan + Rows Removed by Filter > 1000 → 强烈建议加索引
- 提取 WHERE 的等值/范围条件列
- 复合索引列顺序:等值列在前 → 范围列在中 → ORDER BY 列在后
场景 A:已有索引的 SQL
Python
{
"needs_index": false,
"diagnosis": ["已使用索引扫描,无需新增索引"],
"suggested_ddl": []
}
AI 自己判断出"不需要加索引"------这种克制比无脑推荐强太多了。
场景 B:新场景,没建过索引的 SQL
这是更典型的实战场景。给 AI 一条新查询:
python
SELECT * FROM t_order
WHERE user_id = 12345 AND city = '北京'
执行计划(先扫 100 万行 + 扔掉 33 万行 + 1390 ms):
sql
Gather (cost=1000.00..19305.20 ...) (actual time=8.787..1389.518 ...)
Workers Planned: 2 Workers Launched: 2
-> Parallel Seq Scan on t_order (...rows=1 loops=3)
Filter: ((user_id = 12345) AND ((city)::text = '北京'))
Rows Removed by Filter: 333332
Execution Time: 1389.956 ms
AI 调用 suggest_index,自动返回:
JSON
{
"needs_index": true,
"target_table": "t_order",
"filter_columns": ["user_id", "city"],
"sort_column": null,
"diagnosis": [
"Seq Scan 扔掉了 333332 行(>1000),强烈建议加索引",
"建议复合索引列顺序:['user_id', 'city']"
],
"suggested_ddl": [
"CREATE INDEX idx_t_order_user_id_city ON t_order(user_id, city);"
]
}

AI 不仅识别出"该加索引",还自动生成了正确的复合索引 DDL ------列顺序遵循"等值在前、范围在后"的最优原则。DBA 拿到这个 DDL 直接 CREATE INDEX 就完事了。
以前 DBA 看完 EXPLAIN 才能写出来的索引 DDL,现在 AI 在工具调用层就帮你生成好了。
七、接 Claude Code:3 行配置上工位
跑通协议只是验证可用。要把它真实插到工位上,最便捷的方式是接 Claude Code------程序员写代码时就在用的 IDE。
7.1 安装依赖 + 启动账号
python
pip install "mcp[cli]" psycopg2-binary
python step4_mcp/setup_ai_ro.py # 创建 ai_ro 只读账号(一次性)
7.2 注册 MCP Server 到 Claude Code
在任意目录执行(一次性,永久生效):
python
claude mcp add kingbase --env KES_USER=ai_ro --env KES_PASS=Kingbase@2026 -- python F:/------文章撰写------/金仓/kes-python-demo/step4_mcp/kingbase_mcp.py

或者用项目级配置(.mcp.json):
JSON
{
"mcpServers": {
"kingbase": {
"command": "python",
"args": ["F:/------文章撰写------/金仓/kes-python-demo/step4_mcp/kingbase_mcp.py"],
"env": {
"KES_HOST": "localhost",
"KES_PORT": "54321",
"KES_DB": "test",
"KES_USER": "ai_ro",
"KES_PASS": "Kingbase@2026"
}
}
}
}
7.3 重启 Claude Code,开始问数
之后对着 Claude Code 直接用中文问:
python
用户:连上的金仓数据库里有哪些业务表?t_order表里有多少行订单?
AI :连上的金仓数据库(public schema)里共有 4 张业务表:
┌──────────────┬─────────────┐
│ 表名 │ 估算行数 │
├──────────────┼─────────────┤
│ t_order │ 约 100 万 │
├──────────────┼─────────────┤
│ t_department │ ---(未统计) │
├──────────────┼─────────────┤
│ t_employee │ ---(未统计) │
├──────────────┼─────────────┤
│ t_null_test │ ---(未统计) │
└──────────────┴─────────────┘
t_order 实际订单数:1,000,000 行(精确 COUNT,与估算一致)。
/////////////////////////////////////////////////////////////////////////////
用户:哪个城市的订单金额总和最高?前三名分别是多少?
AI :订单金额总和最高的城市:深圳(约 10.02 亿元)。
金额前三名:
┌──────┬──────┬──────────────────┬─────────┐
│ 排名 │ 城市 │ 订单总金额 │ 订单数 │
├──────┼──────┼──────────────────┼─────────┤
│ 1 │ 深圳 │ 1,001,825,669.18 │ 200,122 │
├──────┼──────┼──────────────────┼─────────┤
│ 2 │ 上海 │ 1,000,376,058.49 │ 199,823 │
├──────┼──────┼──────────────────┼─────────┤
│ 3 │ 北京 │ 999,781,765.16 │ 199,968 │
└──────┴──────┴──────────────────┴─────────┘
补充:共 5 个城市(另有杭州、广州),总金额彼此咬得很紧,都在 10 亿元上下,差距仅约千分之三。


这就是 MCP 的价值:Server 写一次,所有 AI 客户端通用。把数据库的"嘴"装上,谁来都能问。
八、可回溯:AI 触达数据库的每次调用都留痕
生产环境还有个绕不开的问题:出了事,能不能查清是谁、什么时候、让 AI 对数据库做了什么。我给每个工具加了一行审计日志:
python
logging.basicConfig(filename="mcp_calls.log", ...)
log = logging.getLogger("kes_mcp")
@mcp.tool()
def run_query(sql: str) -> str:
log.info("run_query sql=%s", sql)
...
跑完上面的测试后,mcp_calls.log 完整记录了每一次调用------三条正常业务查询,以及五条被拒的攻击尝试:
sql
2026-08-08 ... | INFO | run_query sql=SELECT status, count(*) ...
2026-08-08 ... | WARNING | REJECTED sql=DELETE FROM t_order ...
2026-08-08 ... | WARNING | REJECTED sql=DROP TABLE t_order ...
2026-08-08 ... | WARNING | REJECTED sql=SELECT 1; DROP TABLE t_order
2026-08-08 ... | INFO | explain_plan sql=SELECT * FROM t_order ...
2026-08-08 ... | INFO | suggest_index sql=SELECT * FROM t_order WHERE user_id ...

被拦截的攻击也照样留痕------这正是安全审计最想要的:不仅记成功,更要记下"有人试图越权"。这只是 40 行雏形,生产上可以接统一日志平台、加调用方身份、做异常告警,但**"每一次 AI 触达数据库都可回溯"**这个原则,从第一版就得立住。
九、心得 + 国产化意义
AI + 国产数据库的"国产化"重点不在协议,在权限层
MCP 是开放协议,谁都能用------金仓、达梦、OceanBase、PG 都一样接。真正决定能不能上生产的,是国产数据库自己的权限粒度和审计能力 。本文三道闸里最硬的就是金仓的 GRANT SELECT------这是金仓作为国产数据库在权限层就顶住了 DELETE/DROP 的能力证明。AI 时代的数据库选型,"能不能精细授权 + 能不能完整审计"比"支持几种 SQL 方言"重要得多。
AI 工具不要只做"翻译官",要做"顾问"
中文圈已有的 MCP 实践大多停在"自然语言翻译成 SQL 查数据"------这只是把 AI 当翻译官用。本文加的 explain_plan 和 suggest_index 把 AI 升级成"DBA 顾问"------AI 不仅查数据,还看执行计划、给优化建议 。这才是 AI Agent 真正能帮 DBA 减负的方向。下一站可以继续扩展:compare_plans(前后执行计划对比)、profile_workload(拉 KWR 报告分析 TOP SQL)......MCP 的工具粒度让这件事可以无限叠加。
写 MCP Server 时注释第一次有了"读者是 AI"
工具函数的 docstring 不是给程序员看的,是给大模型看的说明书------大模型靠它判断什么时候该调哪个工具。这意味着:
- 描述要写"动词 + 名词 + 限制条件",比如"执行一条只读 SELECT,最多返回 50 行"------大模型看到"只读"就知道不能用来改数据。
- 参数说明要具体,比如"不要带末尾分号"------大模型才会主动去掉分号。
- 错误返回也是上下文,比如
{"rejected": "...原因..."}------大模型看到拒绝原因会自己改写 SQL 重试,或者主动告知用户。
这是 AI 时代编程范式的一次小但根本的转变。写给 AI 看的代码,要像写给一个聪明但不了解你系统的同事看一样。
附:完整代码结构
Python
kes-python-demo/
└── step4_mcp/
├── setup_ai_ro.py # 创建 ai_ro 只读账号(闸 1)
├── kingbase_mcp.py # MCP Server(6 工具 + 3 道安全闸)
├── test_client.py # 协议握手 + 6 工具 + 5 攻击测试
├── demo_suggest.py # suggest_index 自动索引推荐 demo
└── mcp_calls.log # 审计日志
让 AI 能查金仓只是起点,让 AI 安全可控地 查金仓、给出 DBA 级的优化建议,才是 MCP Server 真正的工程价值。