前阵子写 Agent 工具调用的时候,我一直觉得别扭。写个查询用户的小工具,只能绑在当前 Node 项目里用,换个项目就得拷过去改改;想接个 Python 写的数据分析工具,还得额外包一层 HTTP 接口,处理参数、鉴权、错误返回,麻烦得要死。
那时候我就琢磨,有没有什么标准,能把工具和 Agent 彻底拆开?不管工具用啥语言写、跑在哪,Agent 都能直接调用。直到我照着文档搭完第一个 MCP Server,盯着控制台那两行绿底的日志发呆的时候,才反应过来:哦,原来这玩意儿就是干这个的。
先看跑通的效果:两行日志背后的逻辑
这是我第一次跑通测试时截的图,现在看很简单,但当时看到「正在等待第 2 轮 AI 思考」跳出来的时候,确实有点小激动。

我就输入了一句「查一下用户 002 的信息」,剩下的事全是自动完成的:AI 自己判断要调用查询用户的工具,自动把参数传过去,拿到返回结果之后,还自己整理成了表格再回复给我。
整个过程我没写任何手动调用工具的判断逻辑,也没处理参数解析,全是 MCP 协议和 SDK 在背后干活。
MCP 到底是个啥?我用人话捋一遍
MCP 全称是 Model Context Protocol,直译过来叫模型上下文协议。听着挺高大上,其实说白了就是一套统一的通信规矩。
以前我们给大模型加工具,都是和业务代码写在一起的,工具怎么定义、参数怎么传、结果怎么返,全是自己定的。换个项目、换门语言,就得重写一遍,相当于每个电器都自己做个专属插头,互相之间完全不通用。
MCP 就相当于大模型世界的通用插座标准。它规定了客户端(也就是我们写的 Agent)和服务端(提供工具、资源的一方)之间,该怎么打招呼、怎么传参数、怎么返回结果、怎么报错。只要两边都遵守这个标准,不管你服务端是用 Node、Java 还是 Python 写的,不管跑在本地还是远程服务器,客户端都能直接用。
它和普通的 HTTP 接口调用不是一回事。接口调用是我们写代码去拿数据,而 MCP 是给大模型扩展上下文用的 ------ 要么给它增加能调用的工具(让它能做更多事),要么给它提供额外的资料(让它知道更多事)。本质上都是在给大模型的上下文添东西,这也是「Context Protocol」名字的由来。

MCP 最大的特点就是天生支持跨进程调用。本地跑就用 stdio 标准输入输出流,父子进程之间传数据;远程部署就换成 HTTP 协议,走网络通信。两种方式底层协议是一致的,只是传输通道不一样。
动手写第一个 MCP Server
说再多不如写两行代码。我写了个最小可运行的 demo,就做两件事:提供一个查询用户的工具,再提供一份使用指南文档。整个 demo 分成 Server 端和 Client 端两部分。
先搭 Server 端
Server 端就是提供工具和资源的一方,用官方的 SDK 写起来很简单。
javascript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
// 整个假数据库,省得连库麻烦,实际用可以换成真实数据库
const database = {
users: {
'001': {id: '001', name: 'Alice', email: '111@qq.com', age: 30},
'002': {id: '002', name: 'Bob', email: '222@qq.com', age: 25},
'003': {id: '003', name: 'Charlie', email: '333@qq.com', age: 28},
}
}
// 初始化服务,起个名字,标个版本
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
// 注册查询用户的工具
server.registerTool('query_user',
{
// 描述一定要写清楚,不然AI真的会瞎调用
description: '查询数据库中的用户信息,输入用户 ID,返回该用户的详细信息 (姓名、邮箱、年龄)',
inputSchema: {
userId: z.string().describe('用户 ID,例如 001、002、003 等'),
}
},
async ({ userId }) => {
const user = database.users[userId];
if(!user) {
return {
content: [
{ type: 'text', text: `用户 ID ${userId} 不存在,可用的ID: 001、002、003`}
]
}
}
return {
content: [
{ type: 'text', text: `用户 ${user.id} 的姓名是 ${user.name},邮箱是 ${user.email},年龄是 ${user.age}`}
]
}
});
// 注册一份使用指南资源
server.registerResource(
'使用指南',
'docs://guide',
{
description: 'MCP Server 使用指南',
mimeType: 'text/plain'
},
async () => {
return {
contents: [
{
uri: 'docs://guide',
mimeType: 'text/plain',
text: `
MCP Server 使用指南
功能:提供用户查询等工具。
使用:在 Cursor 等 MCP Client 中通过自然语言对话,会自动调用相应工具。
`
}
]
}
}
);
// 用 stdio 方式启动服务,也就是通过标准输入输出和客户端通信
const transport = new StdioServerTransport();
await server.connect(transport);
这里我一开始没搞懂 registerResource 是干嘛的,以为和工具差不多。后来跑通了才明白:
- Tool 是给 AI 「用」的,是能力,AI 可以调用它来做事、拿数据
- Resource 是给 AI 「读」的,是资料,客户端可以提前把资源内容读出来,当成系统提示词喂给 AI
就像这个使用指南,客户端启动的时候先读一遍,然后塞给 AI 当背景知识,AI 就知道这个 Server 有啥功能、该怎么用了。
Resource 是一种很轻量的上下文补充方式,适合放使用说明、字段定义、固定规则这类内容。和 RAG 不一样,它不需要检索,是 Server 自带的固定内容,直接读取就行。
再写 Client 端
客户端就是我们的 Agent 程序,负责连接 MCP Server,拿到工具和资源,然后交给大模型去调度。我这里用了 LangChain 的适配包,用起来更顺手。
javascript
import 'dotenv/config';
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
import { ChatOpenAI } from '@langchain/openai';
import chalk from 'chalk';
import {
HumanMessage,
SystemMessage,
ToolMessage
} from '@langchain/core/messages';
// 初始化大模型,这里用的DeepSeek,换成别的也一样
const model = new ChatOpenAI({
modelName: process.env.DEEPSEEK_API_MODEL,
apiKey: process.env.DEEPSEEK_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.DEEPSEEK_API_BASE_URL,
},
})
// 初始化 MCP 客户端,配置要连接的 Server
// 可以同时配置好多个,统一管理
const mcpClient = new MultiServerMCPClient({
mcpServers: {
'my-mcp-server': {
command: 'node',
args: ['你的server文件路径/my-mcp-server.mjs'],
}
}
});
// 获取所有工具,还有所有资源内容
const tools = await mcpClient.getTools();
const res = await mcpClient.listResources();
let resourceContent = '';
for(const [serverName, resources] of Object.entries(res)){
for(const resource of resources){
const content = await mcpClient.readResource(serverName, resource.uri);
resourceContent += content[0].text;
}
}
// 把工具绑定给模型
const modelWithTools = model.bindTools(tools);
// 核心的 Agent 多轮调用逻辑
async function runAgentWithTools(query, maxIterations = 30) {
// 把资源内容当成系统消息,先给AI看一遍说明书
const messages = [
new SystemMessage(resourceContent),
new HumanMessage(query)
];
for(let i = 0; i < maxIterations; i++){
console.log(chalk.bgGreen(`正在等待第${i+1}轮 AI 思考...`));
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 没有工具调用了,说明AI思考完了,直接返回结果
if(!response.tool_calls || response.tool_calls.length === 0){
console.log(`\n AI 最终回复: \n ${response.content}`);
return response.content;
}
console.log(chalk.bgBlue(`检测到 ${response.tool_calls.length} 个工具调用`));
console.log(chalk.bgBlue(`工具调用: ${response.tool_calls.map(tc => tc.name).join(', ')}`));
// 逐个执行工具调用,把结果塞回消息列表
for(const toolcall of response.tool_calls){
const foundTool = tools.find(t => t.name === toolcall.name);
if(foundTool){
const toolResult = await foundTool.invoke(toolcall.args);
// 注意这里必须传 tool_call_id,不然AI对应不上结果
// 别问我为什么知道,漏了之后卡了半小时
messages.push(new ToolMessage({
tool_call_id: toolcall.id,
content: toolResult
}))
}
}
}
return messages[messages.length - 1].content;
}
// 测试一下
await runAgentWithTools('查一下用户 002 的信息');
// 这行一定要加!不然脚本跑完不会退出
// 别问我怎么知道的,对着卡着的终端发呆了二十分钟
await mcpClient.close();
运行之后,就能看到最开始截图里的效果了。
我踩过的几个坑,个个都卡半小时
写这个 demo 看着简单,其实踩了好几个坑,都是不看文档想当然踩的。说出来给大家避避坑。
第一个坑就是上面说的,忘了调用 close ()。MCP Server 是作为子进程被客户端启动的,你不主动关闭,它就会一直挂在后台等着接收请求,主进程自然就不会退出。一开始我以为是异步没处理好,翻了半天 Promise 写法,最后才发现是没关子进程。
使用完 MCP Client 后,务必调用 close () 方法释放资源,终止子进程,否则脚本会一直挂起无法正常退出。
第二个坑,ToolMessage 漏传 tool_call_id。这个坑更隐蔽。我最开始写 ToolMessage 的时候,只传了 content 内容,没带 tool_call_id。结果就是 AI 永远收不到工具的返回结果,控制台一直在刷「第 N 轮 AI 思考」,无限循环调用工具,就是不输出最终答案。
我当时还吐槽模型笨,调用完工具都不会看结果。后来翻了半天才发现,大模型是靠 tool_call_id 来匹配「哪次调用对应哪次结果」的,你不传 id,它根本不知道这个结果是给谁的,自然就一直重复调用了。
第三个坑,工具描述写得太敷衍。最开始 query_user 的描述我就写了「查询用户信息」六个字,结果 AI 经常传错参数,一会儿传姓名,一会儿传邮箱,死活不知道传 userId。后来我把描述补得巨详细,连参数示例都加上了,调用准确率一下就到了 100%。
给 AI 写描述真的比写产品需求文档还要严谨,半点歧义都不能有。
说点实在的:这玩意儿什么时候该用?
聊到这肯定有人会说,这不就是套了层协议的工具调用吗,我直接写接口不行?
确实,简单场景下完全没必要用 MCP。如果你的项目就是个单语言应用,工具也没几个,直接用框架原生的工具调用就行,上 MCP 反而平白增加复杂度。
但如果碰到下面这几种情况,MCP 就能体现出价值了:
- 工具需要跨语言开发。比如部分算法工具用 Python 写,业务工具用 Java 写,不用再各自包接口,统一走 MCP 标准就行。
- 工具需要给多个项目复用。比如公司内部有很多 Agent 项目,都要用到用户查询、权限校验这类通用工具,做成 MCP Server 一次开发,所有项目都能接。
- 需要远程调用工具能力。把工具部署在服务器上,本地或者其他端的 Agent 通过 HTTP 方式远程调用,不用把代码拷来拷去。
- 想统一管理工具和文档资源。所有能力都按 MCP 标准封装,有统一的描述、参数规范,维护起来也方便。
最后随便聊两句
说实话,最开始听到 MCP 这个新名词的时候,我是有点抵触的,觉得又是 AI 圈在炒概念。真的沉下心写了个 demo,踩了几个坑,才明白它解决的是实实在在的痛点 ------ 把大模型的能力扩展,从单进程、单语言的限制里解放出来了。
技术这东西就是这样,听一百遍概念讲解,不如自己动手跑一遍。当你亲手把 Server 启起来,看着 AI 自动调用工具、整理结果返回的那一刻,很多原理性的东西自然就通了。
要是你也跑了 MCP 的 demo,或者有不一样的理解,欢迎在评论区聊聊,我也想看看大家都用它做了啥有意思的东西。