我花了3天让AI Agent学会查数据库,才发现MCP这玩意是真的香
如果你的 Agent 连数据库都查不了,那不叫智能体,叫高级对话框。
一、我遇到的一个真实问题
上个月接手了一个内部工具项目,需求听起来很简单:让 AI 助手能回答"上周有多少新用户""哪个功能使用率最高"这类业务问题。
我第一反应是:这还不简单?写个 SQL 工具函数让 Agent 调用不就行了。
于是我吭哧吭哧写了代码——在 Agent 系统里注册了一个 query_database 函数,接受 SQL 字符串,返回 JSON 结果。理论上 Agent 拿到用户问题→转成 SQL→调用工具→返回结果,完美闭环。
但实际上呢?Agent 碰到稍微复杂一点的查询就翻车:
- 它不知道表结构,会瞎编列名
- 它不确定字段类型,写了一堆
WHERE status = '1'这种逻辑 - 切换不同的数据库(我们有 MySQL 和 PostgreSQL 两套),它完全蒙圈了
- 更头疼的是,每次加一个工具我都得改代码、重新打包、重新部署
折腾了三天,代码写了一堆,测试用例跑得稀碎。我开始怀疑人生:难道就没有一个标准化的方式让 AI 接入外部工具吗?
二、我的解决方案
就在这时我注意到了 Anthropic 推出的 MCP(Model Context Protocol,模型上下文协议)。
一句话解释:MCP 就是 AI 应用的"USB-C 接口"。以前每个工具都得专门对接(就像以前每个设备都得用不同的充电线),有了 MCP,只要实现了这个协议,任何 AI 应用都能即插即用。
核心思路
MCP 的设计分两个角色:
- MCP Server:负责暴露工具 / 资源的一方(比如你的数据库、文件系统、API)
- MCP Client:负责消费这些工具的一方(比如 Claude Desktop、WorkBuddy 这类 AI 应用)
关键优势:Server 的开发者只需要按 MCP 规范暴露工具,所有支持 MCP 的 Client 都能直接调用,不用重复对接。
动手实现:一个数据库查询 MCP Server
下面是我实际写的一个精简版 MySQL MCP Server,用 TypeScript 实现:
// db-mcp-server/src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import mysql from "mysql2/promise";
const pool = mysql.createPool({
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "3306"),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
const server = new Server(
{
name: "mysql-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// 核心:列出所有可用工具
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "query_database",
description:
"执行只读 SQL 查询。请先使用 list_tables 和 describe_table 了解数据库结构。",
inputSchema: {
type: "object",
properties: {
sql: {
type: "string",
description: "只读 SQL 语句",
},
},
required: ["sql"],
},
},
{
name: "list_tables",
description: "列出数据库中所有表名称",
inputSchema: { type: "object", properties: {} },
},
{
name: "describe_table",
description: "获取指定表的结构信息",
inputSchema: {
type: "object",
properties: {
table_name: {
type: "string",
description: "表名",
},
},
required: ["table_name"],
},
},
],
}));
// 核心:处理工具调用
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "list_tables": {
const [rows] = await pool.query("SHOW TABLES");
return {
content: [{ type: "text", text: JSON.stringify(rows, null, 2) }],
};
}
case "describe_table": {
const [rows] = await pool.query(
`DESCRIBE ${mysql.escapeId(args?.table_name)}`
);
return {
content: [{ type: "text", text: JSON.stringify(rows, null, 2) }],
};
}
case "query_database": {
// 安全第一:只允许只读操作
if (!/^\s*(SELECT|SHOW|DESCRIBE|EXPLAIN)\b/i.test(args?.sql)) {
throw new Error("仅允许只读查询");
}
// 防止一次查太多数据导致 OOM
let sql = args.sql.trimEnd().replace(/;+$/, "");
if (!/LIMIT\s+\d+/i.test(sql)) {
sql += " LIMIT 100";
}
const [rows] = await pool.query(sql);
return {
content: [{ type: "text", text: JSON.stringify(rows, null, 2) }],
};
}
default:
throw new Error(`未知工具: ${name}`);
}
});
// 启动服务
const transport = new StdioServerTransport();
await server.connect(transport);
这段代码的精妙之处
注意我暴露了三个工具而不是一个。这是踩坑学来的教训:
① list_tables + describe_table 这俩是给 Agent "导航"用的
Agent 拿到用户问题后,第一步不是写 SQL,而是先"看看有哪些表、表结构长什么样"。这极大地减少了瞎编列名的问题。这就像你去一家新餐厅,先看菜单再点菜,而不是蒙着眼睛瞎点。
② query_database 的 description 里明确写了使用指引
MCP 的 tool description 就是 Agent 的"用户手册"。写得越细,Agent 用得越对。这里有句关键提示:"请先使用 list_tables 和 describe_table 了解数据库结构"——这句简单的描述,能让 Agent 的查询准确率从 50% 飙升到 90% 以上。
③ 用 StdioServerTransport 而不是 HTTP
MCP 支持两种传输方式:stdio(标准输入输出)和 Streamable HTTP。对于本地工具,stdio 更简单——不用开端口、不用担心防火墙、不用写配置文件。Agent 应用直接用子进程启动你的 Server,通过 stdin/stdout 通信,干净利落。
踩过的坑和意外收获
坑 1:Agent 会写 DELETE 语句
有一次我忘了在 query_database 里限制只读,Agent 居然执行了 DELETE FROM users WHERE id = 1(因为用户说"帮我清理掉这条测试数据")。幸好当时是开发环境。
解法就是代码里那段正则:/^\s*(SELECT|SHOW|DESCRIBE|EXPLAIN)\b/i,白名单之外一律拒绝。生产环境的 MCP Server,读写分离是第一铁律。
坑 2:Agent 一次查太多数据
它写了 SELECT * FROM orders,我们的 orders 表有 200 万行……Server 直接 OOM。
解法:自动给没有 LIMIT 的查询加上 LIMIT 100,并在 tool description 里说清楚有上限。如果你需要查全量数据,应该走专门的数据导出工具,而不是让 Agent 一把梭。
坑 3:JSON 序列化 Date 对象
mysql2 返回的日期是 JavaScript Date 对象,JSON.stringify 后会变成 "2026-06-05T08:23:16.000Z",Agent 阅读起来很不友好。
解法:在 pool 配置里设置 dateStrings: true,让 mysql2 返回人类可读的日期字符串。
意外收获:生态比你想象的丰富
我以为所有 MCP Server 都得自己写,结果发现社区已经有大量现成的了:
- 文件系统:
@modelcontextprotocol/server-filesystem - PostgreSQL:
@modelcontextprotocol/server-postgres - GitHub API:
@modelcontextprotocol/server-github - Puppeteer(浏览器自动化):
@modelcontextprotocol/server-puppeteer - Slack、Google Maps、Brave Search 等等……
这就意味着,你不需要从零开始造轮子。大部分常见的外部服务,都有人帮你写好了 MCP Server。你只需要 npx 一行命令就能跑起来,然后把配置扔进你用的 AI 应用里。
三、效果对比
| 维度 | 传统工具函数方式 | MCP 方式 |
|---|---|---|
| 接入新工具 | 改代码 → 打包 → 部署(10-30 分钟) | 配置 JSON → 重启(1 分钟) |
| Agent 对工具的理解 | 全靠 prompt 描述,随缘 | Schema + Description 双重结构化描述 |
| 跨应用复用 | 每个 Agent 系统单独对接 | 写一个 Server,到处用 |
| 生态丰富度 | 全靠自己写 | 社区维护了大量现成 Server |
| 安全性 | 靠自觉 | 在 Server 层统一做权限控制 |
| 调试难度 | print 大法 | MCP Inspector 可视化调试 |
切换到 MCP 后,我们团队的新工具接入时间从"半天起步"变成了"10 分钟配好"。这感觉就像从拨号上网切换到宽带——你回不去了。
四、总结
核心收获:MCP 解决的不是"怎么让 AI 更聪明",而是"怎么让 AI 更方便地接入真实世界"。它把 AI 的外部能力从"手工对接"变成了"标准化接口",就像 HTTP 之于 Web 一样重要。
给你的实用建议:
- 不要重复造轮子。先去 GitHub 搜
mcp server [你的工具名],大概率已经有人实现了 - Tool description 是你的护城河。写得越具体,Agent 调得越精准。别写"查询数据库",写"执行只读 SQL 查询,需先通过 list_tables 和 describe_table 了解表结构"
- 安全第一。生产环境的 MCP Server 一定要做权限控制——读写分离、行数限制、超时限制,一个都不能少
- 从小处开始。先找一个重复性最高的日常任务(比如查日志、查数据),写个 MCP Server 试试。感受到效率提升后你会停不下来的
AI Agent 的时代,MCP 就是你的"万能工具箱"。早点上车,早点爽。