从零搭建一个 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() 四个参数:
- tool 名称
- 描述(给 AI 看的)
- 参数 schema(zod 格式)
- 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 用这个命令启动你的 serverenv:注入的环境变量,你的代码通过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。