动手写一个 MCP Server:数据库查询 Tool 的完整实现笔记
上个月接了个需求:让团队用自然语言查开发数据库。一开始想的自然是写个 API 查完返回 JSON,但很快发现问题——数据拿到了还得人工粘贴给 AI 分析,流程断的。正好在研究 MCP,索性直接写了个 Server,让 LLM 自己调。
两个星期踩了不少坑,记录下从零到一的完整过程。
最小的 MCP Server
MCP 的 Server 端说白了就是一个 JSON-RPC 端点,stdio 或 HTTP(S) 都行。用 TypeScript + @modelcontextprotocol/sdk 起步:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new Server({
name: 'db-query-server',
version: '0.1.0'
}, {
capabilities: { tools: {} }
});
const transport = new StdioServerTransport();
await server.connect(transport);
这套模版是我所有 MCP Server 的起点。注意 capabilities 里要声明 tools,不然 Client 不会发 ListTools 请求。
Tool 定义:带 JSON Schema 的才是好 Tool
关键来了——Tool 的入参用 JSON Schema 描述,LLM 才能正确生成参数。我一开始图省事,参数全用 string,结果 Claude 每次传参都瞎编:
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: 'query_database',
description: '对 SQLite 数据库执行 SELECT 查询,返回行数组',
inputSchema: {
type: 'object',
properties: {
sql: {
type: 'string',
description: '完整的 SELECT SQL,必须用 ? 占位符传参'
},
params: {
type: 'array',
items: { type: ['string', 'number', 'null'] },
description: '与 ? 一一对应的参数值'
},
limit: {
type: 'number',
description: '最大返回行数,默认 50',
default: 50
}
},
required: ['sql']
}
}]
}))
schema 写得越细,LLM 参数命中率越高。尤其 description 字段——别写 “SQL 语句”,要写 “完整的 SELECT SQL,必须用 ? 占位符传参”,模型才会乖乖用预处理语句。
执行查询:这里踩了个坑
查询实现看起来简单,但第一次写的时候我忘了关闭数据库连接:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { sql, params = [], limit = 50 } = request.params.arguments;
const db = new Database('dev.db');
// ⚠️ 第一版没有 try/finally,连接泄漏!
try {
const rows = db.prepare(sql).all(...params).slice(0, limit);
return {
content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }]
};
} finally {
db.close();
}
});
连接泄漏只是第一个坑。第二个坑是 LLM 生成 SQL 时可能传 delete 或 drop——必须做安全校验:
const dangerous = /\b(drop|delete|truncate|alter|create|insert|update|replace)\b/i;
if (dangerous.test(sql)) {
return {
content: [{ type: 'text', text: '危险操作已拦截:只允许 SELECT 查询' }],
isError: true
};
}
MCP Inspector 调试
写完之后怎么测?官方的 MCP Inspector 比 Postman 好用十倍:
npx @modelcontextprotocol/inspector node dist/server.js
打开 http://localhost:5173,能看到自动展示所有注册的 Tool、填参数调用的界面、以及完整的 JSON-RPC 报文。当初调 JSON Schema 的时候,就是靠 Inspector 发现某个 parameter 的 type 写成了 stirng,模型传参一直 400。
接入 Claude
Claude Desktop 的 mcpServers 配置:
{
"mcpServers": {
"db-query": {
"command": "node",
"args": ["D:/projects/db-query-server/dist/server.js"]
}
}
}
配置完重启 Claude,对话框里会出现一个锤子图标,点开就能看到 query_database。这时候你可以直接说 “查一下上个月注册的用户数”,Claude 会自动调你的 Server。(第一次成功的时候还挺有成就感的。)
查了下已经公开的 MCP Server 仓库,很多只暴露了工具定义就发布了,连参数描述都是空的。模型拿这种工具就像人用一台没有标签的机器——全靠猜。写 schema 的时候把自己当成用户和模型之间的翻译,描述写到位了,效果天差地别。另外安全拦截无论如何都不能省——LLM 有时候真的会生成 drop table,别问我怎么知道的。
更多推荐
所有评论(0)