让 AI Agent 查询数据库,不该只靠一条万能 SQL:OctoQuery 的 MCP 与 Schema Skill 完全指南
把数据库接给 Claude Code、IDE 助手或其他 AI Agent,最常见的做法是给它一个“执行 SQL”的工具。这能很快跑通,却很容易在第二步失控:Agent 不知道软删除记录该不该排除、不知道金额以分还是元保存,也不知道订单、退款和客户表之间该怎样连接。它会不断试探 schema,得到看似合理却不可靠的答案。
OctoQuery 提供了一个更明确的分层:每个已配置数据库对应一个 MCP 工具,而数据库的表结构、关联关系和业务约定写入一份 Agent Skill。前者解决“可以调用什么”,后者解决“应当怎样理解数据”。项目是 MIT 许可的 TypeScript 服务,目前明确支持 PostgreSQL 与 MySQL;MCP 端点使用 Streamable HTTP,默认路径为 /mcp。
这不是把自然语言直接变成生产 SQL 的自动驾驶方案。它更适合只读分析、支持工单排查、运营数据问答,或让编程 Agent 在受控范围内理解开发数据库。下面从工作模型、最小可复现实验和生产边界三个层面拆开看。
为什么“一条 SQL 工具”还不够
假设有人问:“最近 30 天消费最高的客户是谁?”SQL 语法本身并不难,但答案取决于许多不在 schema 名称里的规则:
orders中的取消订单是否计入?amount是分、元还是含税金额?- 是否要扣除退款?
- 同一个人是否会有多个账户记录?
- 查询量很大时,返回多少行才不会淹没 Agent 上下文?
OctoQuery 的思路不是增加一组按表拆分的 MCP 函数,而是让每个数据库暴露为单个、命名明确的 SQL 工具,例如 sql_orders_prod 或 sql_analytics_dev。调用参数是 SQL 查询,返回 JSON 行数据。真正的业务解释被放到与数据库配对的 Markdown Skill 中:表、主外键、常见 join、状态字段、软删除规则和单位约定都可以写进去。
这种边界有两个好处。第一,工具定义不会随表数量膨胀;第二,业务知识可以像代码一样放进仓库评审和更新。对 Agent 来说,先读 Skill、再构造 SQL,往往比反复查询系统表再猜字段含义更稳定。
先跑官方双数据库演示
仓库自带两个已填充的演示库:一个电商 PostgreSQL(用户、商品、订单与订单项),以及一个博客 MySQL(作者、文章与评论)。这很适合在不接触真实数据前观察完整链路。
git clone https://github.com/benedya/octoquery.git cd octoquery npm install docker compose -f demo/docker-compose.yml up -d cp .env.example .env cp mcp-sql-tools.example.json mcp-sql-tools.json npm run start:dev
官方示例将 PostgreSQL 映射到 127.0.0.1:45432,MySQL 映射到 127.0.0.1:43306。服务启动后,本机 MCP 地址是 http://localhost:3000/mcp,示例工具名为 sql_ecommerce_demo 与 sql_blog_demo。因此,先用隔离的 Docker 演示库校验工具发现、结果格式和权限策略,再连测试环境,会比让 Agent 一开始就触碰共享数据库稳妥得多。
客户端配置并不绑定某一个 Agent;只要客户端理解标准 MCP 的 Streamable HTTP 传输即可。配置形状如下:
{
"mcpServers": {
"octoquery": {
"type": "http",
"url": "http://localhost:3000/mcp"
}
}
}
接下来不要急着让 Agent 自己探索所有表。仓库中的 .agents/skills/ 提供了电商库和博客库的示例 Skill;实际项目可以新增 .agents/skills/,并在 AGENTS.md 中登记。Skill 应优先记录那些无法从 DDL 推出的事实,例如“金额以分存储”“deleted_at 非空表示逻辑删除”“报表必须排除某些订单状态”。
把连接配置和业务说明分开
OctoQuery 从 mcp-sql-tools.json 读取数据库清单。该文件应当被忽略提交,因为其中可能有密码;仓库提交的是 mcp-sql-tools.example.json 模板。一个 PostgreSQL 连接的核心字段可以写成:
[
{
"name": "sql_orders_prod",
"label": "prod orders",
"host": "prod-db.example.com",
"port": 5432,
"database": "orders_service",
"user": "orders_reader",
"password": "replace-with-a-secret",
"enableTLS": true,
"maxRows": 100
}
]
其中 name、host、database、user 与 password 是必填项;type 可选择 postgres(默认)或 mysql。maxRows 可覆盖全局的 MCP_MAX_ROWS,默认值是 100。把“订单生产库”和“分析开发库”设置成不同工具名,比把环境参数交给自然语言提示更可审计:Agent 调用的对象、授予的凭据与返回上限都在配置里可见。
与它相配的 Skill 不必很长,但应足够具体。例如可以先规定订单金额单位、有效状态、推荐 join 路径,以及绝不读取的敏感列。这样做并不会替代数据库权限;它是在权限允许的范围内,减少 Agent 得到错误业务结论的概率。
只读不是一句提示词,而是多层限制
真正值得关注的是默认安全模型。项目文档说明,MCP_READ_ONLY=true 默认开启:每个查询作为单条语句放入 READ ONLY 事务,由数据库拒绝写操作与 DDL。对 MySQL,由于 DDL 可能借由隐式提交逃离只读事务,服务还额外限制为 SELECT、WITH、SHOW、DESCRIBE、EXPLAIN 这类只读语句。
这比在 prompt 里写“请不要修改数据”更可靠,但仍不能等同于完整的数据治理。至少还要补上三层:
- 数据库账户最小权限:给 Agent 使用专门的只读账号,限制 schema、表和视图;不要把管理员凭据放进配置文件。
- 结果最小化:为工具设置合理的
maxRows,并在 Skill 中约定先聚合、后按需下钻,避免把整张含敏感字段的表送进模型上下文。 - 环境隔离:开发、预发和生产库使用不同工具名与不同身份。即使需要写入,也应把
MCP_READ_ONLY=false视为一次显式的权限升级,而不是常规开关。
文档还提醒:关闭只读模式后,SQL 工具会执行任意 SQL,调用方必须被完全信任;访问控制由身份提供方一侧承担。因此“服务有 OAuth”并不意味着“数据库已经安全”,令牌授权本质上仍是一次数据库访问授权。
OAuth、代理和多副本部署的三个容易遗漏点
OctoQuery 可作为符合 MCP 授权规范的 OAuth 2.0 resource server,接入 OIDC 提供方时通过 AUTH_ISSUER 与 AUTH_AUDIENCE 配置。未认证请求会收到 401 和指向受保护资源元数据的 WWW-Authenticate 响应;服务会按 OIDC 的 JWKS 校验 JWT 签名、发行者、过期时间,以及可选的 audience。开发机本地验证可关闭认证,但这不是对外暴露服务的部署建议。
生产反向代理场景还有两项工程细节:
BASE_URL要写客户端真正访问到的公网地址;它会参与资源元数据和认证 challenge,不能简单沿用容器里的 localhost 地址。- MCP 会话存储在内存中。若运行多个副本,入口层需要 sticky session,否则同一会话可能被路由到不持有该会话的实例。
这类问题通常不会出现在“能连上 MCP”的第一分钟,却会在 OAuth 回调、负载均衡或扩容后暴露。把它们写入部署检查表,比在故障发生后追查偶发 401 更省时间。
一个可操作的上线顺序
如果目标是让内部 Agent 查询真实业务数据,可以按下面的顺序推进:
- 先运行官方 Docker demo,并用 MCP Inspector 检查工具是否发现、查询是否返回 JSON;Inspector 使用 Streamable HTTP 连接到
/mcp。 - 为测试库创建最小权限的只读数据库账号,配置一个独立工具名和低
maxRows。 - 为该库写一份可评审的 Skill,把单位、状态、关联和敏感字段规则写清楚;让业务同事确认它,而不是只让开发者确认 SQL 能运行。
- 用一组固定问题验收:聚合、跨表 join、空结果、超行数结果,以及尝试写入或多语句时是否被拒绝。
- 最后再启用 OIDC、设置正确
BASE_URL,并在多副本入口上验证会话粘滞。
OctoQuery 最有价值的地方,不是承诺 AI 永远写对 SQL,而是将“连接能力、schema 知识和执行权限”拆成可检查的层。MCP 工具负责窄而明确的执行面,Skill 负责语义面,数据库账号与 OAuth 负责授权面。三层都收紧后,Agent 才有机会成为可用的数据协作者,而不是拿着高权限连接字符串的即兴查询器。
相关链接