如果你的 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
  • SlackGoogle MapsBrave 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 一样重要。

给你的实用建议

  1. 不要重复造轮子。先去 GitHub 搜 mcp server [你的工具名],大概率已经有人实现了
  2. Tool description 是你的护城河。写得越具体,Agent 调得越精准。别写"查询数据库",写"执行只读 SQL 查询,需先通过 list_tables 和 describe_table 了解表结构"
  3. 安全第一。生产环境的 MCP Server 一定要做权限控制——读写分离、行数限制、超时限制,一个都不能少
  4. 从小处开始。先找一个重复性最高的日常任务(比如查日志、查数据),写个 MCP Server 试试。感受到效率提升后你会停不下来的

AI Agent 的时代,MCP 就是你的"万能工具箱"。早点上车,早点爽。