MCP(Model Context Protocol)入门与实践:让 AI Agent 跨进程调用工具

MCP(Model Context Protocol)入门与实践:让 AI Agent 跨进程调用工具

一、问题的起点:Tool 的复用困境

在构建 AI Agent 时,我们经常需要给大模型绑定各种工具(Tool)。比如一个查询用户信息的工具:

arduino 复制代码
const queryUserTool = {
  name: 'query_user',
  description: '查询用户信息',
  // ...
}

但这样写存在两个明显的问题:

  1. 只能在这个项目中使用,不好复用------工具代码和 Agent 代码耦合在一起,换个项目就得重写。
  2. 语言绑定问题------如果工具是用 Node.js 写的,那 Java、Python、Rust 的项目怎么用?

核心诉求就是:让 Tool 独立于 LLM,实现本地/远程、跨进程、跨语言的调用。


二、MCP 协议是什么

MCP(Model Context Protocol) 是一种标准化的协议,用于规范 LLM 与 Tool、Resource 之间的通信,核心目标是 解耦 LLM 和 Tool

通信方式

MCP 支持两种通信模式:

模式 传输方式 适用场景
stdio 标准输入输出流 本地跨进程调用
HTTP 远程 HTTP 通信 远程跨进程调用

本质上,MCP 就是让 Agent 能够跨进程调用工具------不管是本地进程还是远程进程,通过 MCP 协议就能搞定。

用一句话概括:MCP 是给 Model 扩展 Context(上下文),让它能做的更多 (Tool)、知道的更多(Resource)的协议。

与普通 API 调用的区别

MCP 和 fetch 调用接口不一样------它不是去拿接口数据,而是要扩展 Context(工具能力 + 资源知识),让 LLM 在推理时拥有更丰富的上下文。


三、MCP 的核心概念

3.1 Tool(工具)

本质就是工具,和普通的 Function Calling / Tool Use 没有本质差别,区别在于它是跨进程提供的。Agent(MCP Client / Host)通过协议去发现和调用远程进程中的工具------就像"抛饵"出去,让别的进程来执行。

3.2 Resource(资源)

Resource 是 MCP 另一大亮点。它允许 MCP Server 提供静态资源(文档、指南等),这些资源可以作为 System Prompt 的一部分注入到 Context 中。

看一个实际的 Resource 注册例子(来自 my-mcp-server.mjs):

php 复制代码
server.registerResource(
  '使用指南',
  'docs://guide',   // URI 格式的访问路径
  {
    description: '使用指南',
    mimeType: 'text/plain'
  },
  async () => {
    return {
      contents: [{
        uri: 'docs://guide',
        mimeType: 'text/plain',
        text: `
          MCP Server 使用指南
          功能:提供用户查询等工具。
          使用:在 Cursor 等 MCP Client 中通过自然语言对话,Cursor 会自动调用相应工具。
        `
      }]
    }
  }
)

在 Client 端读取 Resource 的方式:

ini 复制代码
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;
  }
}

// 将 resource 内容作为 SystemMessage 注入
const messages = [
  new SystemMessage(resourceContent),  // 资源内容成为上下文的一部分
  new HumanMessage(query),
];

Resource vs RAG:Resource 是 RAG 之外另一种丰富上下文的手段。相比 RAG 需要先检索,Resource 更适合提供固定的、结构化的参考文档。当然,受限于上下文窗口大小,不是所有文档都适合直接塞进去。


四、实战:搭建一个完整的 MCP 调用链路

下面通过实际代码展示一个完整的 MCP 工作流程。架构如下:

arduino 复制代码
┌──────────────┐    stdio     ┌─────────────────┐
│  MCP Client  │ ◄──────────► │  MCP Server     │
│  (LangChain) │  跨进程通信   │  (Node.js 进程)  │
│              │              │                 │
│  - 获取工具   │              │  - query_user   │
│  - 获取资源   │              │  - docs://guide │
│  - Agent 循环 │              │                 │
└──────────────┘              └─────────────────┘

4.1 MCP Server 端

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: '祖豪', email: 'zh@qq.com', role: 'admin' },
    '002': { id: '002', name: '光光', email: 'gg@qq.com', role: 'user' },
    '003': { id: '003', name: '小红', email: 'xh@qq.com', role: 'user' },
  }
}

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

// 注册工具
server.registerTool('query_user', {
  description: '查询数据库中的用户信息',
  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} 不存在` }]
    }
  }
  return {
    content: [{
      type: 'text',
      text: `用户 ${user.id} 的信息是:姓名:${user.name}, 邮箱:${user.email}, 角色:${user.role}`
    }]
  }
});

// 注册资源
server.registerResource('使用指南', 'docs://guide', { /* ... */ }, async () => { /* ... */ });

// 通过 stdio 启动通信
const transport = new StdioServerTransport();
await server.connect(transport);

关键点:

  • McpServer 创建服务实例
  • 通过 registerTool 注册工具,使用 Zod 定义参数 Schema
  • 通过 registerResource 注册静态资源
  • 使用 StdioServerTransport 建立 stdio 通信通道

4.2 MCP Client 端

javascript 复制代码
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
import { ChatOpenAI } from '@langchain/openai';

const model = new ChatOpenAI({
  modelName: 'deepseek-v4-flash',
  // ...配置
});

// 配置 MCP Client,可以同时连接多个 MCP Server
const mcpClient = new MultiServerMCPClient({
  mcpServers: {
    'my-mcp-server': {
      command: 'node',                          // 启动命令
      args: ['src/my-mcp-server.mjs'],          // 脚本路径
      cwd: '/path/to/mcp-demo'                  // 工作目录
    }
  }
});

// 获取工具和资源
const tools = await mcpClient.getTools();
// 获取资源并拼成上下文字符串
const res = await mcpClient.listResources();
// ...拼接 resourceContent ...

// 绑定工具到模型
const modelWithTools = model.bindTools(tools);

关键点:

  • MultiServerMCPClient 可以同时配置多个 MCP Server
  • 每个 Server 配置指定启动命令、参数和工作目录
  • Client 内部通过 child_process 启动子进程,通过 stdio 通信

4.3 Agent 工具调用循环

ini 复制代码
async function runAgentWithTools(query, maxIterations = 30) {
  const messages = [
    new SystemMessage(resourceContent),   // 资源内容作为系统提示
    new HumanMessage(query),
  ];

  for (let i = 0; i < maxIterations; i++) {
    const response = await modelWithTools.invoke(messages);
    messages.push(response);

    // 没有工具调用 → 直接返回最终回复
    if (!response.tool_calls || response.tool_calls.length === 0) {
      return response.content;
    }

    // 执行每个工具调用
    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.push(new ToolMessage({
          content: toolResult,
          tool_call_id: toolCall.id,   // ⚠️ 必须带上 tool_call_id
        }));
      }
    }
  }
  return messages[messages.length - 1].content;
}

这个循环有几个值得注意的细节:

  1. SystemMessage 注入 Resource:把 MCP Server 提供的文档资源作为系统提示,让模型"知道得更多"
  2. 最大迭代次数:设置 30 轮上限防止无限循环
  3. tool_call_id 必须回传 :ToolMessage 必须带上对应的 tool_call_id,这是模型关联工具调用和结果的唯一标识
  4. tools.find() 匹配find 方法找到第一个匹配项就停止,适合工具名唯一的场景

4.4 资源清理

csharp 复制代码
// 关闭所有 MCP 子进程与通信通道,释放进程资源
await mcpClient.close();

这一步很重要! close() 会:

  • 关闭与 MCP Server 的 stdio 连接
  • 终止 child_process 启动的子进程
  • 避免脚本挂起不退出

五、关键技术细节

5.1 跨进程通信(IPC)的本质

arduino 复制代码
主进程 (Agent/LangChain)
    │
    │  child_process.spawn('node', ['my-mcp-server.mjs'])
    │
    ├── stdin  ──►  MCP Server 子进程
    │                 (接收工具调用请求)
    │
    ◄── stdout ──  MCP Server 子进程
                      (返回工具执行结果)
  • 父子进程模型 :Client 通过 child_process 启动 Server 作为子进程
  • stdio 通信:请求通过 stdin 发送,响应通过 stdout 返回
  • 多语言支持:任何语言的进程只要实现了 MCP 协议的 stdio 通信,就能被调用

5.2 JavaScript 的异步特性

整个通信链路中,JavaScript 的单线程异步无阻塞模型保证了:

  • 主线程不会被子进程通信阻塞
  • 多个 MCP Server 可以并发通信
  • Agent 循环中的每轮推理和工具调用都是异步的

5.3 Object.entries 的妙用

在处理 MCP 返回的资源列表时,Object.entries() 非常实用:

javascript 复制代码
// MCP 返回的是按 Server 分组的资源对象
const res = {
  'my-mcp-server': [
    { uri: 'docs://guide', name: '使用指南' },
    { uri: 'docs://api', name: 'API文档' },
  ]
};

// Object.entries 拆解为 [key, value] 遍历
for (const [serverName, resources] of Object.entries(res)) {
  for (const resource of resources) {
    // 逐个读取资源内容
  }
}

六、总结

维度 传统 Tool MCP Tool
复用性 绑定在项目中 独立进程,任意项目可用
跨语言 仅限同语言 通过 stdio/http,任意语言
通信方式 同进程函数调用 跨进程(本地 stdio / 远程 HTTP)
扩展性 手动添加 配置式添加 MCP Server
上下文丰富 仅 Tool Tool + Resource + Prompt

MCP 协议的核心价值在于:

  1. 解耦:Tool 和 LLM 彻底分离,各自独立开发、部署、迭代
  2. 跨进程:不管是本地子进程还是远程服务,统一通过 MCP 协议通信
  3. 跨语言:Node.js 写的工具可以被 Python Agent 调用,反之亦然
  4. 标准化:统一的 Tool 定义、Resource 提供、通信方式,生态可共享

MCP 让 AI Agent 从一个"孤岛"变成了可以接入各种能力(Tool)和知识(Resource)的"开放平台"。这就是它被称为 AI 的 USB-C 接口的原因。

相关推荐
CTA终结者1 小时前
近期AI量化工具推荐,围绕最难推进的环节选择
人工智能·python
无糖可可果1 小时前
远程 MCP 实战:让 AI 成为「千里眼」和「魔术手」
人工智能
wuhanzhanhui1 小时前
从芯片到精密制造:2026武汉电子元器件、材料及生产设备展会见证微观世界的跃迁
大数据·人工智能
冬奇Lab1 小时前
每日一个开源项目(第156篇):OpenDataLoader PDF - 基准测试第一名的开源 PDF 解析器,专为 RAG 设计
人工智能·开源·资讯
冬奇Lab1 小时前
MCP 系列(03):生态导航——官方 Server 清单与社区精选
人工智能·资讯·mcp
百胜软件@百胜软件1 小时前
百胜软件推出RT业务:零售技术服务全链路飞轮如何助力品牌商“降本+增收”?
大数据·人工智能·零售
AGI_Eval1 小时前
姚顺雨的“半年考”:Hy3用21B激活参数,交出了一份怎样的答卷?
人工智能
武子康1 小时前
OpenAI Workspace Agents 进入信用计费:Coding Agent 从额度体验走向成本治理
人工智能·openai·claude
周末程序猿1 小时前
图解 120 个大语言模型(LLM)核心概念(31-60)
人工智能