MCP 协议实战:给你的 AI 装上「工具手」(含自建 Server 完整代码)
MCP 协议实战:给你的 AI 装上「工具手」(含自建 Server 完整代码)
大模型本身只会说话。它不知道你数据库里有什么、不能读你的私有文档、也发不出一封邮件。要让它真正干活,就得给它接工具。
过去每家 AI 产品都有自己的一套「插件 / Function Calling」格式,接一个工具要为每个平台重写一遍。MCP(Model Context Protocol)就是来终结这件事的——它定义了一套统一的协议,工具写一次,任何支持 MCP 的客户端都能用。
到 2026 年,Claude Desktop、Claude Code、Cursor、Codex 以及一批国产工具都已支持 MCP。这篇讲清楚它是什么、怎么接现成的、以及怎么自己写一个。
一、MCP 到底是什么
一句话:MCP 之于 AI 工具,就像 USB 之于外设。
它规定了三方之间怎么通信:
┌─────────────┐ MCP 协议 ┌──────────────┐
│ MCP Client │ ◄──────────► │ MCP Server │
│ (Claude/ │ │ (你写的工具) │
│ Cursor...) │ │ │
└─────────────┘ └──────┬───────┘
│
┌──────▼───────┐
│ 数据库/API/ │
│ 文件系统... │
└──────────────┘
Server 向 Client 暴露三类能力:
| 类型 | 作用 | 例子 |
|---|---|---|
| Tools | AI 可以调用的函数 | 查数据库、发邮件、创建工单 |
| Resources | AI 可以读取的数据 | 文件内容、API 返回、配置 |
| Prompts | 预置的提示词模板 | 「代码审查」「周报生成」模板 |
日常用得最多的是 Tools,这篇重点讲它。
二、先用起来:接一个现成的 Server
社区已经有大量现成的 MCP Server。最快的体验方式是接一个文件系统 Server。
以 Claude Desktop 为例,编辑配置文件:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/you/Documents"
]
}
}
}
重启客户端,你就能对它说:
读一下 Documents 里的 notes.md,总结成三条要点
它会通过 MCP 调用文件系统工具去读。
几个常用的官方 Server:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxx" }
},
"sqlite": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sqlite", "--db", "./app.db"]
}
}
}
配置改完必须重启客户端才生效,这是新手最常卡住的地方。
三、自己写一个:给博客加个「文章查询」工具
现成的不够用,就自己写。下面用 Node.js 写一个能查询 SQLite 文章库的 MCP Server,完整可跑。
1. 初始化
mkdir blog-mcp && cd blog-mcp
npm init -y
npm install @modelcontextprotocol/sdk
在 package.json 里加上:
{
"type": "module"
}
2. 写 Server
新建 index.js:
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { DatabaseSync } from 'node:sqlite';
const DB_PATH = process.argv[2] || './data/site.db';
const db = new DatabaseSync(DB_PATH);
const server = new Server(
{ name: 'blog-mcp', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
// ---- 1. 声明有哪些工具 ----
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'search_posts',
description: '按关键词搜索博客文章,返回标题、日期、摘要',
inputSchema: {
type: 'object',
properties: {
keyword: { type: 'string', description: '搜索关键词' },
limit: { type: 'number', description: '返回条数,默认 5' },
},
required: ['keyword'],
},
},
{
name: 'get_post',
description: '按 slug 获取一篇文章的完整内容',
inputSchema: {
type: 'object',
properties: {
slug: { type: 'string', description: '文章 slug' },
},
required: ['slug'],
},
},
],
}));
// ---- 2. 实现工具逻辑 ----
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args } = req.params;
if (name === 'search_posts') {
const kw = `%${args.keyword}%`;
const limit = args.limit ?? 5;
const rows = db
.prepare(
`SELECT slug, title, date, excerpt FROM posts
WHERE title LIKE ? OR content LIKE ?
ORDER BY date DESC LIMIT ?`
)
.all(kw, kw, limit);
return {
content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }],
};
}
if (name === 'get_post') {
const row = db
.prepare('SELECT * FROM posts WHERE slug = ?')
.get(args.slug);
if (!row) {
return {
content: [{ type: 'text', text: `未找到 slug=${args.slug} 的文章` }],
isError: true,
};
}
return {
content: [{ type: 'text', text: row.content }],
};
}
throw new Error(`未知工具: ${name}`);
});
// ---- 3. 启动 ----
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('blog-mcp server started'); // 注意:日志必须走 stderr
3. 接进客户端
{
"mcpServers": {
"blog": {
"command": "node",
"args": [
"/absolute/path/to/blog-mcp/index.js",
"/absolute/path/to/data/site.db"
]
}
}
}
重启客户端,然后就可以:
搜一下我博客里关于 SQLite 的文章,把最相关的那篇总结成三段
AI 会自动调 search_posts,拿到结果后再调 get_post,最后给你总结。整个过程你不用写任何胶水代码。
四、写 Server 的几条关键规则
规则 1:日志必须走 stderr
stdio 传输模式下,stdout 是协议通道。你 console.log 一句话,整个连接就崩了。调试信息一律用 console.error。
规则 2:description 是给 AI 看的,要写清楚
工具的 description 直接决定 AI 会不会正确调用它。
差的写法:
description: '查询文章'
好的写法:
description: '按关键词搜索博客文章。在标题和正文中做模糊匹配,返回 slug、标题、日期、摘要。适合用户想找某个主题的文章时使用。不返回全文,需要全文请再调 get_post。'
把「什么时候用」「返回什么」「有什么限制」都写进去。这是提升调用准确率最有效的一招。
规则 3:错误要返回,不要抛
工具执行失败时,返回带 isError: true 的结果,让 AI 知道发生了什么、可以怎么补救。直接抛异常会让整个调用链断掉。
规则 4:控制返回体积
返回 5000 行数据 = 烧掉一大把 token。加上 limit 参数,默认值给小一点,让 AI 需要更多时自己加。
规则 5:危险操作要有护栏
写操作(删除、修改、发送)尽量做成需要二次确认,或者在 Server 层面加白名单。别让 AI 一句话就能把生产库删了。
五、调试:MCP Inspector
官方提供了调试工具,不用反复重启客户端:
npx @modelcontextprotocol/inspector node index.js ./data/site.db
它会起一个网页界面,你可以:
- 看到 Server 暴露了哪些工具
- 手动填参数调用,看返回结果
- 看完整的协议往返消息
开发流程建议:先用 Inspector 把工具调通,再接进真实客户端。能省掉大量「改一行重启一次」的时间。
六、常见问题排查
Server 没出现在客户端里
- 配置文件 JSON 语法有没有错(少个逗号就全废)
- 路径必须是绝对路径
- 客户端重启了吗(真重启,不是关窗口)
- 手动跑一下
node index.js,看有没有直接报错
工具存在但 AI 不调用
description 写得太模糊。改清楚,或者在对话里直接点名:「用 search_posts 工具查一下」。
调用了但返回乱码 / 报错
检查是不是有 console.log 污染了 stdout。
性能很慢
每次调用都新建数据库连接会很慢。连接对象放在模块顶层复用(像上面代码那样)。
七、MCP 值得投入吗
值得的场景:
- 你有私有数据要给 AI 用(内部文档、业务数据库、私有 API)
- 你希望一次开发多处复用(同一个工具,Claude、Cursor、Codex 都能用)
- 你要做团队级的 AI 工具(统一分发一套 MCP Server 给全组)
不值得的场景:
- 一次性的小需求 —— 直接让 AI 写个脚本跑一下更快
- 已经有成熟 SaaS 集成的 —— 别重复造轮子
- 纯读公开信息 —— 联网搜索就够了
小结
MCP 的意义在于把「AI 能做什么」这个问题,从模型厂商手里交回到了你手里。
模型能力是给定的,但它能接触到什么数据、能调用什么系统,取决于你写了什么 Server。在企业场景里,后者往往比前者更决定成败——一个接上了内部 CRM 和工单系统的中等模型,比一个什么都碰不到的顶级模型有用得多。
写一个 MCP Server 的成本,大概就是上面那一百行代码。这个投入产出比,值得试。
MCP 规范与 SDK 仍在演进,具体 API 请以 modelcontextprotocol.io 官方文档为准。本文基于 2026 年 8 月的版本整理。