游乐游手机版
首页/AI教程/文章详情

从零理解MCP:手写本地服务器接入LangChain Agent的完整教程

时间:2026-07-12 14:59
MCP通过stdio管道将工具从智能体解耦为独立进程,实现跨项目复用与跨语言调用。手写MCP服务器实例,注册工具和资源后启动。智能体端通过多服务器MCP客户端连接子进程,获取工具与资源内容,作为系统消息注入大语言模型,完成ReAct循环调用。

MCP 本地调用(stdio)完全解析

这篇文章会一步步拆解 MCP 的本地调用模式——也就是通过 stdio 管道通信的方式。我们会从实际痛点出发,写一个完整的 MCP Server,再让 Agent 端连上来用。全程有代码、有流程图、有对比,适合想彻底搞懂 MCP 工作原理的开发者。

一、为什么需要 MCP

之前 Tool 的问题

tool.mjsmini-cursor.mjs 里,工具是直接写在 Agent 代码里的:

从零理解 MCP:手写一个本地 MCP Server 并接入 LangChain Agent

// 工具定义和 Agent 在同一文件
const readFileTool = tool(async ({ filePath }) => {
const content = await fs.readFile(filePath, 'utf-8');
return content;
}, { name: 'read_file', ... });
const tools = [readFileTool];
const modelWithTools = model.bindTools(tools);

这样写看着挺直接,但一细想,问题就出来了:

  1. 不能跨项目复用: 换个项目,工具代码得拷过去重写
  2. 不能跨语言: 用 Python 写的工具,Node 项目调不了
  3. 耦合太紧: 工具开发者必须懂 Agent 代码,Agent 开发者必须了解工具细节

MCP 解决了什么

MCP(Model Context Protocol)把 Tool 从 Agent 里拆出来,变成独立进程,通过标准协议通信:

之前:                     之后:
┌──────────────┐          ┌──────────┐ ┌──────────────┐
│      Agent   │          │   Agent  │ │  MCP Server  │
│    + LLM     │          │  + LLM   │←───→│  + Tools      │
│    + Tools   │          └──────────┘ └──────────────┘
└──────────────┘                解耦!可复用!可跨语言!

二、两个文件,两种角色

mcp-demo/src/ 下有两个文件,一个提供服务,一个消费服务:

langchain-mcp-test.mjs(父进程 / Agent / MCP Client)
│
│ child_process.spawn('node', ['my-mcp-server.mjs'])
│ 通过 stdin/stdout 管道通信
│
▼
my-mcp-server.mjs(子进程 / MCP Server)
│
│ 提供:query_user 工具 + docs://guide 资源

和 API Server 与 API Client 的关系一模一样——一个是后端服务,一个是前端调用。

三、my-mcp-server.mjs — 写一个 MCP Server

3.1 创建 Server 实例

const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0'
});

这是固定步骤。 McpServer 是 SDK 提供的类,new 一个实例出来,后面才能在上面注册工具和资源。

  • name:自己取名,标识这个 Server,Agent 端配置时会用到
  • version:版本号,随意

3.2 注册工具

server.registerTool('query_user',   // ① 工具名(LLM 看到的名字)
{
  // ② 描述对象
  description: '查询数据库中的用户信息。输入用户ID,返回该用户的详细信息(姓名、邮箱、角色)',
  inputSchema: z.object({
    userId: z.string().describe('用户ID, 例如:001, 002, 003')
  })
},
async ({ userId }) => {  // ③ 处理函数(LLM 提取的参数传进来)
  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.role}`}]};
});

三个参数拆解:

参数是什么谁用
'query_user'工具名LLM 看到这个名字,决定是否调用
{ description, inputSchema }描述 + 参数约束LLM 根据 description 判断用途,根据 schema 填参数
async ({ userId }) => {...}真正干活的函数代码执行,userId是 LLM 自动从用户话里提取的

返回格式固定:

return {
  content: [{ type: 'text', text: '返回给 LLM 的文字内容' }]
};

content 是数组,通常只有一项 [0]。协议设计成数组是为了支持多段内容(比如同时返回文字 + 图片),但绝大多数情况一项就够了。

3.3 注册资源

server.registerResource(
  '使用指南',          // ① 给人看的名称
  'docs://guide',      // ② URI,Agent 通过这个地址读取
  {                     // ③ 元信息
    description: 'MCP Server 使用指南',
    mimeType: 'text/plain'  // 告诉 Agent 这是纯文本
  },
  async () => {         // ④ 返回资源内容的函数
    return {
      contents: [{
        uri: 'docs://guide',
        mimeType: 'text/plain',
        text: `MCP Server 使用指南
功能:提供用户查询等工具。
使用:在 Cursor 等 MCP Client 中通过自然语言对话,Cursor 会自动调用相应工具。`
      }]
    };
  }
);

四个参数:

参数含义
'使用指南'显示名
'docs://guide'URI,自己取名。docs://只是命名习惯,不是真实网址。叫app://help也行
{ description, mimeType }描述 + 格式说明
async () => {...}返回资源内容,和工具返回格式一样

资源的作用: 给 LLM 当"说明书"——告诉 LLM 这个 Server 有什么能力、该怎么用。在 Agent 端读取后作为 SystemMessage 注入,LLM 就能看到。

3.4 启动 Server

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

固定两步: 创建传输通道 → 绑定并启动。

StdioServerTransport 是 stdio 模式的传输方式——Agent 通过 stdin/stdout 管道和它通信。如果是远程 MCP,这里换成 StreamableHTTPServerTransport 即可,Server 逻辑不用改。

四、langchain-mcp-test.mjs — Agent 端调用

4.1 连接 MCP Server

const mcpClient = new MultiServerMCPClient({
  mcpServers: {
    'my-mcp-server': {
      command: 'node',
      args: ['C:UsersrogDesktopwork spacexll_aiaiagent_in_actionmcp-demosrcmy-mcp-server.mjs']
    }
  }
});

固定格式:

{
  'Server 名称': {
    command: '用什么启动',  // node / python / npx
    args: ['路径或参数']       // 传给 command 的参数
  }
}

本质就是 child_process.spawn(command, args) 。LangChain 底层帮你 spawn 子进程、连 stdio。不同语言都一样:

{ command: 'node', args: ['./server.mjs'] }   // Node
{ command: 'python', args: ['./server.py'] } // Python
{ command: 'npx', args: ['-y', '@amap/mcp-server'] } // npx,自动下载 npm 包

npx -y 包名:自动下载 + 运行,不用先 npm install-y 跳过确认。

4.2 获取工具和资源

const tools = await mcpClient.getTools();   // 从所有 MCP Server 拿工具列表
const res = await mcpClient.listResources(); // 从所有 MCP Server 拿资源列表

getTools()listResources()MultiServerMCPClient 的固定方法,不能改名。

对比 Tool 阶段:

// 之前:手动定义,手动塞数组
const readFileTool = tool(...)
const tools = [readFileTool]

// 现在:一行自动拿回所有工具
const tools = await mcpClient.getTools()

不管连了几个 MCP Server,getTools() 把所有工具合并到一个数组里给你。

4.3 读取资源内容

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;
  }
}

逐层拆解:

res 的结构是一个对象:

res = {
  'my-mcp-server': [{ uri: 'docs://guide', name: '使用指南', mimeType: 'text/plain' }]
  // 如果有多个 Server,还会有更多 key
}

第一层循环: Object.entries(res) 把对象拆成 [server名, 资源数组]

// 第 1 圈:serverName = 'my-mcp-server', resources = [{ uri: 'docs://guide', name: '使用指南', ... }]

第二层循环: 遍历当前 Server 下的每个资源

// 第 1 圈:resource = { uri: 'docs://guide', name: '使用指南', mimeType: 'text/plain' }

读取资源:

const content = await mcpClient.readResource(serverName, resource.uri);
//                 ↑↑                ↑↑
// 从哪个 Server         读哪个资源

两个参数:"去谁家、拿哪个东西"。

取第一段文本:

resourceContent += content[0].text;
//                 ↑   ↑   ↑
//                追加拼接第一项文本内容

readResource 返回的是数组(和工具返回格式一样),资源通常只有一项,取 [0].text

全部遍历完的结果:

resourceContent = "MCP Server 使用指南n功能:提供用户查询等工具。n使用:在 Cursor 等..."

所有资源的文字内容拼在一起了。

4.4 作为 SystemMessage 注入 LLM

const messages = [
  new SystemMessage(resourceContent),  // MCP 资源内容 → 角色知识
  new HumanMessage(query)              // 用户任务
];

资源内容变成了 SystemMessage。 LLM 看到它就知道:"哦,我有一个叫 query_user 的工具,用来查用户信息,参数是 userId"。

4.5 绑定工具 + ReAct 循环

const modelWithTools = model.bindTools(tools);

async function runAgentWithTools(query, maxIterations = 30) {
  const messages = [
    new SystemMessage(resourceContent),
    new HumanMessage(query)
  ];

  for (let i = 0; i < maxIterations; i++) {
    console.log(chalk.bgGreen(`正在等待AI思考, 第${i}轮....`));

    const response = await modelWithTools.invoke(messages);
    messages.push(response);

    // 没有 tool_calls → LLM 直接答了 → 结束
    if (!response.tool_calls || response.tool_calls.length === 0) {
      console.log(`\n AI 最终回复:\n ${response.content}`);
      return response.content;
    }

    // 有 tool_calls → LLM 要调工具
    console.log(chalk.bgBlue(`检测到 ${response.tool_calls.length} 个工具调用`));
    console.log(chalk.bgBlue(`工具调用: ${response.tool_calls.map(t => t.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);
        // 结果塞回 messages,必须带 tool_call_id
        messages.push(new ToolMessage({
          content: toolResult,
          tool_call_id: toolCall.id
        }));
      }
    }
  }

  // 兜底:循环到上限还没结束,返回最后一条消息内容
  return messages[messages.length - 1].content;
}

完整执行流程:

第 1 轮 invoke:
  messages = [SystemMessage(资源内容), HumanMessage("查用户002")]
  LLM 看到资源 → 知道有 query_user 工具
  LLM 判断要查用户 → 返回 tool_calls: [{ name: 'query_user', args: { userId: '002' }, id: 'call_001' }]
  ↓
  遍历 tool_calls → find("query_user") → 找到 → invoke({ userId: '002' })
  ↓
  push ToolMessage({ content: "用户 002:光光...", tool_call_id: 'call_001' })
  ↓
第 2 轮 invoke:
  messages = [SystemMessage, HumanMessage, AIMessage(tool_calls), ToolMessage(用户信息)]
  LLM 看到工具结果 → 没有更多要调了 → 返回 content: "用户 002 是光光..."
  ↓
  检测到没有 tool_calls → return "用户 002 是光光..."

4.6 关闭连接

await mcpClient.close();

关闭所有 MCP 连接、杀掉子进程、释放资源。必须调用,否则脚本跑完子进程还挂着。

五、核心概念串讲

5.1 变量名 vs 方法名

const mcpClient = new MultiServerMCPClient({...})  // mcpClient 自己取的
await mcpClient.getTools()        // getTools 框架定的
await mcpClient.listResources()   // listResources 框架定的
await mcpClient.readResource(...) // readResource 框架定的
await mcpClient.close()           // close 框架定的

规律:点号前面自己取名,点号后面框架写死。

5.2 工具调用决策全流程

用户说"查询用户002"
→ invoke(messages)
→ LLM 云端决策:需要调 query_user,userId = "002"
→ 返回 { tool_calls: [{ name: 'query_user', args: { userId: '002' }, id: 'call_001' }] }
→ find("query_user") 在 tools 里找匹配
→ tool.invoke({ userId: '002' }) 执行
→ ToolMessage({ content: 结果, tool_call_id: 'call_001' })
→ 再 invoke,LLM 看到结果,回复用户

决策是 LLM 做的,代码只负责执行。 invoke 那一行,LLM 在云端完成推理。

5.3 tool_call_id 为什么重要

LLM 可能一次调多个工具:

tool_calls: [
  { name: 'read_file', args: {...}, id: 'call_001' },
  { name: 'write_file', args: {...}, id: 'call_002' },
]

每个 ToolMessage 必须带对应的 id,LLM 才知道"这个结果是对应 call_001 还是 call_002"。就像同时给三个人发微信,每条回复必须标记是对哪条的。

5.4 资源内容读取链路

Server: registerResource('使用指南', 'docs://guide', ...)
→ Agent: listResources() 拿到 [{ uri: 'docs://guide', name: '使用指南' }]
→ Agent: readResource('my-mcp-server', 'docs://guide')
→ Server: 返回 { contents: [{ text: 'MCP Server 使用指南...' }] }
→ Agent: content[0].text 取出文字 → 拼进 resourceContent
→ new SystemMessage(resourceContent) → LLM 看到

5.5 ReAct 循环的两层判断

// 外层 for 循环:控制最大轮数
for (let i = 0; i < maxIterations; i++) {
  // 内层判断:这轮要不要调工具
  if (!response.tool_calls || response.tool_calls.length === 0) {
    return response.content;  // 不用 → 结束
  }
  // 需要 → 逐一执行
  for (const toolCall of response.tool_calls) { ... }
}
// 到了 maxIterations → 兜底返回
return messages[messages.length - 1].content;

六、MCP 的核心价值

Tool 阶段MCP Server 阶段
工具放哪Agent 代码里独立进程,独立文件
怎么调函数调用MCP 标准协议(stdio)
换项目能用吗不能,得重写能,一行command+args配置
跨语言不能Ja va/Python/Rust 都能写 MCP Server
谁维护Agent 开发者工具开发者独立维护
能不能多个手动拼数组MultiServerMCPClient自动合并

一句话:让工具脱离 Agent 独立存在,任何 Agent 想用就连上来,不管语言、不管在哪。

七、复习检查清单

  • 能写出创建 MCP Server 的 4 个固定步骤
  • 能写出 Agent 端连接 MCP Server 的固定格式(command + args
  • 理解getTools()/listResources()/readResource()的作用和返回值结构
  • 能解释资源读取的两层循环在做什么
  • 理解tool_call_id为什么必须带
  • 能画出 ReAct 循环的完整流程图
  • 能说出 MCP 相比原始 Tool 的 4 个优势
来源:https://juejin.cn/post/7660820926615388202
上一篇GPT-Live全双工语音Agent深度拆解:连续交互与后台委托的三层架构 下一篇零基础搭建远程MCP:串联3个Server实现AI自动化工作流
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
天学会AI应用开发上下文与RAG阶段性总结
AI教程 · 2026-07-12

天学会AI应用开发上下文与RAG阶段性总结

系统梳理了AI应用开发中的上下文与RAG核心概念。搭建Python3 10开发环境后,通过提示词工程管理历史对话,利用截断、摘要提取及离线大模型压缩上下文。RAG采用加载文档、分块、向量化、存入FAISS或Chroma库、检索五个步骤,中文场景推荐BGE-small嵌入模型,实战证明离线检索有效。

Yank Note系列14:如何与AI协作写文章
AI教程 · 2026-07-12

Yank Note系列14:如何与AI协作写文章

YankNote3 92 1发布,新增“审阅批注”和“编辑装订线”扩展,聚焦AI写作的人机协作循环。审阅批注将反馈绑定到选中文本,编辑装订线高亮显示修改行,帮助人看清AI改动。同时升级Markdown-it至14 3 0,优化中日韩文本渲染。

一文读懂MCP:让AI大模型万能插拔协议
AI教程 · 2026-07-12

一文读懂MCP:让AI大模型万能插拔协议

MCP(模型上下文协议)由Anthropic提出,为AI大模型提供通用接口标准,解决工具调用碎片化问题。采用Host Client Server架构,支持动态发现工具,通过stdio或HTTP通信,实现工具即插即用、跨语言调用和统一对接规范。

BeeWeave开源:为AI Agent打造越用越懂你的知识创作台
AI教程 · 2026-07-12

BeeWeave开源:为AI Agent打造越用越懂你的知识创作台

BeeWeave是一个Agent原生的知识创作台,通过workbench和vault双层结构实现素材获取、内容创作、知识沉淀与上下文复用的持续闭环。它支持多Agent共用同一套知识库,内置41个Skills,采用MIT开源协议,旨在解决跨会话知识丢失问题。

AI闭环工程师自动推进工程任务
AI教程 · 2026-07-12

AI闭环工程师自动推进工程任务

LoopEngineer是一种将AI嵌入可执行、可验证、可持续迭代的工作循环中的工程协作方式。通过触发器、上下文、工具和验证机制,AI在循环中自动分析、执行、验证并调整,直至任务完成或需人工介入,实现复杂任务的可观察、可审计、可批准的分段自动化。