MCP Server 接入现有数据库:从只读查询到安全写入的完整路径

📅 2026/10/25 ✍️ 小文 📖 约 1 分钟

把公司数据库接给 AI 用,最怕的不是它查得慢,而是它改错了数据。本文给出分层授权、SQL 白名单、影子行校验与审计日志的工程做法,含可复用的实现要点。

MCP 让 AI 访问数据库变得很简单:写一个 server,暴露几个工具,模型就能自己查数了。于是很多团队第一周上线了只读查询,第二周就觉得”为什么不干脆让它也能改数据呢”。

这一步跨错,代价可能是删库、改错订单、泄露他人数据。而且这类事故的特点是不报错——AI 会自信地告诉你”已更新 3 条记录”,实际上更新错了行。

这篇文章讲怎么把这件事做对:从只读开始,一步步放开到安全写入。

第一层:只读查询的正确做法

看起来最简单的部分,也有几个坑。

坑一:把整库暴露成一个 run_sql 工具。 这是最常见的错误。模型可以查任何表,包括用户表、密钥表、日志表。正确做法是按业务域拆成明确的工具:

query_sales_orders(start_date, end_date, status, limit)
query_product_inventory(sku, warehouse)
query_customer_summary(customer_id)   # 只返回聚合指标,不返回 PII

工具名和参数本身就是给模型的约束。模型不需要知道表结构,也不会误查不该查的表。

坑二:没有行数上限。 模型很容易写出一个返回十万行的查询,然后把结果塞进上下文。每个工具都必须强制 LIMIT,并且超限时返回”结果过多,请缩小范围”而不是截断——截断会让模型以为拿到了全量,得出错误结论。

坑三:只读账号权限过大。 即使只读,也不该能查所有表。给 MCP server 配一个独立数据库账号,只授予必要表的 SELECT 权限。这是最后一道防线。

第二层:参数化查询,别信模型的拼接

如果工具内部是 f"SELECT * FROM orders WHERE status = '{status}'",那么模型只要生成一个带引号的参数就能注入。虽然模型通常没有恶意,但它会幻觉出不存在的值——比如把 status 填成 'paid' OR 1=1,因为它看过太多这样的字符串。

所有查询必须参数化。参数值必须经过枚举校验:

ALLOWED_STATUS = {"pending", "paid", "shipped", "refunded"}

@tool
def query_sales_orders(start_date: str, end_date: str, status: str, limit: int = 50):
    if status not in ALLOWED_STATUS:
        return {"error": f"status 必须是 {sorted(ALLOWED_STATUS)} 之一"}
    if limit > 200:
        limit = 200
    # 参数化执行
    ...

把校验写在工具服务端,不要指望提示词里的一句”请使用合法的状态值”。

第三层:写入前必须有的四道闸

放开写权限时,按下面四道闸设计。

闸一:写操作单独成工具,语义明确。 不要提供通用的 update_record(table, id, fields),而要有 update_order_shipping_address(order_id, new_address) 这类业务动作级工具。区别是前者模型可以改任意字段,后者它只能做一件事。

闸二:先读后写的两阶段模式。 写工具的内部实现必须是”先查出目标记录并返回摘要,等确认后再执行”。在实践中更靠谱的做法是拆成两个工具:

preview_order_change(order_id, new_address)  → 返回当前值、新值、影响范围
apply_order_change(change_token)             → 凭 token 执行

change_token 由 preview 生成,带有效期(比如 5 分钟)和内容哈希。模型必须先 preview 再 apply,人在中间可以看到差异。这一步把”AI 直接改”变成”AI 提议、人确认”,同时保留了自动化能力(低风险变更可以配置为自动确认)。

闸三:影响行数的硬性断言。 每个写操作执行前计算预期影响行数,执行后比对。如果 preview 说 1 行、实际影响 3 行,必须回滚并报错。同时限制单次操作最多影响 N 行(比如 5 行),超过就拒绝,要求人工操作。

闸四:影子行校验。 改动完成后,重新读一遍该记录,把关键字段与预期值比对,确认写入真的生效且没有连带修改其他字段。很多 ORM 的”更新”会意外覆盖没有指定的字段,这一步能抓住这类问题。

审计日志不是可选项

每一次工具调用都要记录:

  • 调用时间、会话 ID、发起用户
  • 工具名与完整参数
  • 返回摘要(行数、状态)
  • 如果是写操作:preview 内容、确认人、执行结果、回滚情况

这份日志有两个用途:出问题时追责与恢复,以及定期复盘模型的调用模式——有没有反复查询某个表(说明工具设计不合理)、有没有频繁预览不执行(说明工具语义不清、模型没把握)。

日志要写到模型无法修改的地方。MCP server 的日志如果也暴露成工具给模型看,就失去了审计意义。

一个渐进放开的路线图

不要一次做到位,按阶段推进,每个阶段稳定两周再放开下一层:

  1. 只读、明确工具:按业务域拆分,全部限行数、限字段
  2. 只读 + 聚合:加上统计类工具,注意聚合可能反向泄露(比如求最小值的字段可能暴露个体)
  3. 写入 + 人工确认:preview/apply 两阶段,全部写操作需人确认
  4. 写入 + 条件自动确认:对低风险、小影响面的操作放开自动执行
  5. 批量操作:仅限人工通过专用界面发起,不通过对话触发

大部分团队做到第 3 步就足够了,收益/风险比最好。

别忘了一件事

数据库接入 AI 之后,数据模型本身就是安全边界。如果某些字段是敏感的(薪资、身份证、健康信息),最好的做法不是在工具层过滤,而是根本不让这些字段进入这个数据库的可访问视图——用视图或专门的只读副本,把敏感字段彻底隔离出去。

工具层的过滤是防误操作的,不是防数据泄露的。后者要靠架构。

📤 分享到