从0开始搭建mcp

从零搭建一个 MCP Server:让 AI 直接查你的数据库

前言

用 Claude Code 写代码时,经常需要查数据库验证想法------表结构对不对、数据存不存在、某个字段的实际值是什么。

每次手动切到终端敲 SQL 再把结果贴回来,太蠢了。

MCP(Model Context Protocol)能解决这个问题:你写一个本地服务,把"查数据库"这个能力暴露给 AI,之后 AI 需要数据库信息时自己去查,不用你当中间人。

这篇文章带你从零写一个 db-readonly-mcp------一个只读 MySQL 查询的 MCP server。跟着做完,你的 Claude Code 就能直接执行 SELECT 查询了。

MCP 是什么

一句话:MCP 是 AI 调用本地工具的标准协议。

你写一个程序,声明"我提供一个叫 query 的工具,接收 sql 参数,返回查询结果"。Claude Code 启动这个程序,需要时通过标准协议调用它。

技术上,MCP 就是三个东西的组合:

1. JSON-RPC 2.0 ------ 消息格式

类似 HTTP 规定了请求/响应的格式,JSON-RPC 规定了一个更简单的格式:

json 复制代码
// 请求
{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "query", "arguments": {"sql": "SELECT 1"}}}

// 响应
{"jsonrpc": "2.0", "id": 1, "result": {"content": [{"type": "text", "text": "[{\"1\":1}]"}]}}

没有 header,没有状态码,没有 URL。一个 JSON 进去,一个 JSON 出来。

2. stdio ------ 传输通道

stdio(standard input/output)是操作系统的概念。每个进程都有 stdin(输入流)和 stdout(输出流)。

Claude Code 启动你的 MCP server 进程后,通过 stdin 发请求,从 stdout 读响应:

c 复制代码
Claude Code 进程                    MCP Server 进程
     |                                    |
     |--- JSON-RPC 请求 ---> stdin ------>|
     |                                    | (执行逻辑)
     |<--- stdout <--- JSON-RPC 响应 -----|

不走网络,不需要端口,两个进程通过管道直连。

3. Tool ------ 你暴露的能力

你声明一个 tool,包含:

  • name:工具名
  • description:AI 根据这段话决定什么时候调用
  • inputSchema:参数结构(JSON Schema 格式)
  • handler:实际执行函数

Claude 看到 tool 列表后,会在合适的时机自动调用。

开始搭建

第一步:初始化项目

bash 复制代码
mkdir db-readonly-mcp && cd db-readonly-mcp
npm init -y

编辑 package.json

json 复制代码
{
  "name": "db-readonly-mcp",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.1",
    "mysql2": "^3.19.1"
  }
}

安装依赖:

bash 复制代码
npm install

两个依赖:

  • @modelcontextprotocol/sdk:Anthropic 官方 SDK,处理协议细节
  • mysql2:MySQL 驱动

第二步:写 MCP Server

创建 index.js,完整代码如下。我分块讲解。

引入依赖:

js 复制代码
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import mysql from "mysql2/promise";
import { z } from "zod";

zod 是一个运行时类型校验库,MCP SDK 用它来定义 tool 的参数结构。SDK 会自动把 zod schema 转成 JSON Schema 发给 Claude。不需要单独安装,SDK 自带。

数据库配置:

js 复制代码
const dbConfig = {
  host: process.env.DB_HOST || "localhost",
  port: parseInt(process.env.DB_PORT, 10) || 3306,
  user: process.env.DB_USER || "root",
  password: process.env.DB_PASSWORD || "",
  database: process.env.DB_NAME || "",
};

从环境变量读取,后面注册到 Claude Code 时通过配置注入。

只读校验:

js 复制代码
const READONLY_PREFIXES = ["SELECT", "SHOW", "DESCRIBE", "EXPLAIN", "WITH"];

function isReadonly(sql) {
  const upper = sql.trim().toUpperCase();
  return READONLY_PREFIXES.some((p) => upper.startsWith(p));
}

安全第一。只允许读操作,拒绝一切写入。

创建 Server 并注册 Tool:

js 复制代码
const server = new McpServer({
  name: "db-readonly",
  version: "1.0.0",
});

server.tool(
  "query",
  "Execute a read-only SQL query against MySQL. Supports SELECT, SHOW, DESCRIBE, EXPLAIN, WITH.",
  {
    sql: z.string().describe("The read-only SQL statement to execute"),
    database: z.string().optional().describe("Override the default database"),
  },
  async ({ sql, database }) => {
    if (!isReadonly(sql)) {
      return {
        isError: true,
        content: [{ type: "text", text: "Rejected: only read-only queries allowed." }],
      };
    }

    let connection;
    try {
      const config = { ...dbConfig };
      if (database) config.database = database;

      connection = await mysql.createConnection(config);
      const [rows] = await connection.query(sql);

      return {
        content: [{ type: "text", text: JSON.stringify(rows, null, 2) }],
      };
    } catch (err) {
      return {
        isError: true,
        content: [{ type: "text", text: `Query failed: ${err.message}` }],
      };
    } finally {
      if (connection) await connection.end();
    }
  }
);

server.tool() 四个参数:

  1. tool 名称
  2. 描述(给 AI 看的)
  3. 参数 schema(zod 格式)
  4. handler 函数

handler 返回值固定结构:{ content: [{ type: "text", text: "..." }] }。出错时加 isError: true

启动传输层:

js 复制代码
const transport = new StdioServerTransport();
await server.connect(transport);

两行代码,server 就跑起来了。它会阻塞在这里,等待 stdin 的请求。

第三步:注册到 Claude Code

编辑 ~/.claude/settings.json,加入 mcpServers 字段:

json 复制代码
{
  "mcpServers": {
    "db-readonly": {
      "command": "node",
      "args": ["/你的路径/db-readonly-mcp/index.js"],
      "env": {
        "DB_HOST": "localhost",
        "DB_PORT": "3306",
        "DB_USER": "root",
        "DB_PASSWORD": "your_password",
        "DB_NAME": "your_default_database"
      }
    }
  }
}

配置含义:

  • command + args:Claude Code 用这个命令启动你的 server
  • env:注入的环境变量,你的代码通过 process.env 读取

保存后重启 Claude Code session,它会自动启动你的 MCP server。

第四步:验证

方法一:用 MCP Inspector

SDK 提供了一个调试工具:

bash 复制代码
npx @modelcontextprotocol/inspector node index.js

会启动一个 Web UI,你可以手动发 tools/list 看 tool 是否注册成功,发 tools/call 测试查询。

方法二:直接在 Claude Code 里用

重启 session 后,让 Claude 查数据库:

shell 复制代码
> 帮我看一下 users 表有多少条数据

Claude 会自动调用 mcp__db-readonly__query tool,执行 SELECT COUNT(*) FROM users,把结果告诉你。

运行时序

理解了代码,再看一遍从 Claude Code 启动到拿到查询结果的完整流程,包括实际传输的 JSON:

阶段一:启动与握手

bash 复制代码
1. Claude Code 读 settings.json,发现 mcpServers.db-readonly
2. spawn("node", ["/path/to/index.js"]),注入 env 环境变量
json 复制代码
// 3. Claude Code 通过 stdin 发送 initialize 请求
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"clientInfo":{"name":"claude-code"}}}

// 4. server 通过 stdout 回复,声明自己支持 tools
{"jsonrpc":"2.0","id":0,"result":{"serverInfo":{"name":"db-readonly"},"capabilities":{"tools":{}}}}

// 5. Claude Code 发送 tools/list,问"你有哪些工具"
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}

// 6. server 回复完整的 tool 列表(name、description、inputSchema)
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"query","description":"Execute a read-only SQL query against MySQL.","inputSchema":{"type":"object","properties":{"sql":{"type":"string"},"database":{"type":"string"}},"required":["sql"]}}]}}

握手完成。Claude 现在知道有个 query tool 可用,接收 sql 字符串参数。

阶段二:用户触发调用

markdown 复制代码
7. 用户问:"帮我查一下 users 表有多少行"
8. Claude 判断需要查数据库,决定调用 query tool
json 复制代码
// 9. Claude Code 发送 tools/call
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query","arguments":{"sql":"SELECT COUNT(*) FROM users"}}}
markdown 复制代码
10. 你的 handler 执行:连接 MySQL → 执行 SQL → 拿到结果
json 复制代码
// 11. server 通过 stdout 返回查询结果
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"[{\"COUNT(*)\":42}]"}]}}
arduino 复制代码
12. Claude 拿到结果,组织语言回复用户:"users 表目前有 42 条数据。"

步骤 3-6 是 SDK 自动处理的,你不需要写任何代码。你写的代码只负责步骤 10------接收 sql 参数,连数据库,跑查询,返回结果。

完整代码

index.js 全文,可直接复制:

js 复制代码
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import mysql from "mysql2/promise";
import { z } from "zod";

const dbConfig = {
  host: process.env.DB_HOST || "localhost",
  port: parseInt(process.env.DB_PORT, 10) || 3306,
  user: process.env.DB_USER || "root",
  password: process.env.DB_PASSWORD || "",
  database: process.env.DB_NAME || "",
};

const READONLY_PREFIXES = ["SELECT", "SHOW", "DESCRIBE", "EXPLAIN", "WITH"];

function isReadonly(sql) {
  const upper = sql.trim().toUpperCase();
  return READONLY_PREFIXES.some((p) => upper.startsWith(p));
}

const server = new McpServer({ name: "db-readonly", version: "1.0.0" });

server.tool(
  "query",
  "Execute a read-only SQL query against MySQL. Supports SELECT, SHOW, DESCRIBE, EXPLAIN, WITH.",
  {
    sql: z.string().describe("The read-only SQL statement to execute"),
    database: z.string().optional().describe("Override the default database"),
  },
  async ({ sql, database }) => {
    if (!isReadonly(sql)) {
      return {
        isError: true,
        content: [{ type: "text", text: "Rejected: only read-only queries allowed." }],
      };
    }

    let connection;
    try {
      const config = { ...dbConfig };
      if (database) config.database = database;

      connection = await mysql.createConnection(config);
      const [rows] = await connection.query(sql);

      return {
        content: [{ type: "text", text: JSON.stringify(rows, null, 2) }],
      };
    } catch (err) {
      return {
        isError: true,
        content: [{ type: "text", text: `Query failed: ${err.message}` }],
      };
    } finally {
      if (connection) await connection.end();
    }
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

扩展思路

这个 server 只有一个 tool。你可以继续加:

js 复制代码
server.tool("list-tables", "List all tables in a database", { ... }, handler);
server.tool("describe-table", "Show table schema", { ... }, handler);

也可以加 Resource,把数据库 schema 作为上下文自动注入给 AI,不需要每次都 DESCRIBE。

MCP 能做的不止数据库。任何本地能力都可以包装成 tool:文件搜索、Git 操作、HTTP 请求、Redis 查询、日志检索。核心模式不变------声明 tool,实现 handler,通过 stdio 通信。

总结

MCP server 本质就是一个本地进程,通过 stdin/stdout 跟 Claude Code 交换 JSON 消息。你用 SDK 注册 tool,实现 handler,剩下的协议细节 SDK 全包了。

整个 db-readonly-mcp 不到 50 行有效代码,但它让 AI 获得了直接查询数据库的能力。这就是 MCP 的价值------用最小的代码量,把本地能力桥接给 AI。

相关推荐
Duang3 小时前
我把 Claude、Codex、Copilot、Gemini 拼成了一个工作流,接力写代码
人工智能·程序员·架构
SimonKing4 小时前
别再死磕 Elasticsearch 了,这个轻量级搜索引擎更香
java·后端·程序员
AI绘画哇哒哒20 小时前
Agent三种思考模式深度解析:CoT/ReAct/Plan-and-Execute,小白程序员必看,助你轻松掌握大模型精髓(收藏版)
人工智能·学习·ai·程序员·大模型·产品经理·转行
SimonKing1 天前
从惊艳到踩坑:AI结对编程的真实复盘
java·后端·程序员
程序员cxuan2 天前
微信读书官方发了 skills,把我给秀麻了。
人工智能·后端·程序员
浪里行舟3 天前
你的品牌正在被AI“遗忘”?用BuildSOM找回搜索的下一个风口
人工智能·python·程序员
程序员cxuan3 天前
当 00 后开始用 token 给学校送礼
人工智能·后端·程序员
诸神缄默不语3 天前
营销体系4M模型:MVP(最小可行性产品)、PMF(产品市场匹配)、GTM(市场进入)和MTU(市场转化)
程序员
Hilaku3 天前
从搜索排名到 AI 回答? 先聊一聊 AI 可见度工具 BuildSOM !
前端·javascript·程序员