Skip to content

MCP:可跨进程调用的 Tool

我们已经写了一些 Tool 了:读写文件和目录、执行命令。

只要声明 Tool 的名字、描述、参数格式,模型会在发现需要用到 Tool 的时候自动解析出参数传进来调用,然后把执行结果封装成 ToolMessage 传入 chat。

比如上节我们实现了简易的 Cursor:声明了读写文件和目录、执行命令的 Tool,这样你让大模型创建 react + vite 项目,它就会自动判断什么时候调用哪个 Tool,自动实现目录、文件的创建,以及 pnpm installpnpm run dev 的执行。

我们只是告诉它要创建的项目,然后安装依赖跑起来。这些 Tool 怎么调用、参数是什么,都是大模型自己决定的。

Tool 给大模型扩展了做事情的能力——本来它只能思考,不能做事情,现在可以自己调用 Tool 来帮你做事情了。

但你会发现 Tool 有个问题:Node 写的 AI Agent 代码,你的 Tool 也得是 Node 写。如果你之前有一些工具是 Java、Python、Rust 写的呢?你想封装成 Tool 怎么办?

从跨语言到统一协议

有同学说:现在不是可以执行命令么,通过单独进程把这些其他语言写的代码跑一下就行了啊。

确实,也就是这样:当进程跑一个子进程,就可以用 stdio(标准输入输出流,也就是键盘输入、控制台输出)这种方式通信。

还有的同学说:简单,用 HTTP 啊!本地跑个服务就好了。

也就是这样:通过 HTTP 连接远程服务进程。

现在解决了跨语言调用工具的问题。但新的问题来了:如果每个人都这样搞,它们提供的服务都不一样,我想接入别的 Tool,是不是要了解每个服务都是怎么定义的呢?

能不能定义一个统一的通信协议,我们都按照这个格式来沟通? 这样所有的跨进程工具调用就都可以接入了。

想跨进程调用某个工具,通过这个协议通信就行:本地工具直接跑那个进程,然后 stdio 通信;远程工具通过 HTTP 连接远程服务进程。

这个协议叫什么?它是给 Model 扩展 Context 上下文、让它能做的更多、知道的更多的 Protocol 协议——就叫 MCP 吧。

恭喜你,你发明了 MCP!

什么是 MCP

MCP 最大的特点就是可以跨进程调用工具

  • 跨本地的进程调用,用 stdio
  • 跨远程的进程调用,用 HTTP

提到 MCP 都会提到这张经典的架构图:

MCP 架构图

你的 AI Agent 就是 MCP 客户端,可以通过 MCP 协议调用各种 MCP Server,实现跨进程的工具调用。

当然,在 LangChain 里,它也是 Tool——只不过是 Tool 的一种而已:你在 Tool 的函数里,调用下 MCP Client,访问下远程 MCP Server。它本质上还是 Tool,但是集成了 MCP 工具。

MCP 由 AI 巨头 Anthropic 公司发起并开发,2025 年 12 月交给了 Linux 基金会维护。也就是说,它现在是完全中立、不绑定任何一家模型的行业通用协议。

有一个很形象的类比:USB-C 是接口标准,不同设备可以通过 USB-C 接入电脑;MCP 是 AI 工具接口标准,不同工具服务可以通过 MCP 接入 AI 应用。MCP 让 AI 应用可以通过标准协议,调用运行在另一个进程里的工具。

大概知道 MCP 是啥就行,我们自己写个 MCP 服务就明白了。继续在 tool-test 这个项目里写:

bash
pnpm install @modelcontextprotocol/sdk

从包名就可以看出来,它是中立、不属于任何一家公司的。

写一个 MCP Server

创建 src/my-mcp-server.mjs

js
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: 'zhangsan@example.com', role: 'admin' },
    '002': { id: '002', name: '李四', email: 'lisi@example.com', role: 'user' },
    '003': { id: '003', name: '王五', email: 'wangwu@example.com', role: 'user' },
  }
};

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

// 注册工具:查询用户信息
server.registerTool('query_user', {
  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: `用户信息:\n- ID: ${user.id}\n- 姓名: ${user.name}\n- 邮箱: ${user.email}\n- 角色: ${user.role}`,
      },
    ],
  };
});

// 注册资源:静态文档
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 中通过自然语言对话,Cursor 会自动调用相应工具。`,
      },
    ],
  };
});

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

代码很容易看懂:

  • new McpServer 创建了 MCP Server 实例
  • server.registerTool 注册了一个工具,声明 namedescriptionschema
  • server.registerResource 注册了一个资源,就是静态数据

和我们写 Tool 的时候差不多,只不过这里分了 resourcetool:resource 一般返回静态数据(用来查询信息,read),tool 来做一些事情(执行功能,call)。

最后,可以提供 stdio 的本地进程调用方式,也可以提供 HTTP 的远程调用方式。这里用的是 stdio 的传输方式(Transport)。

这样我们的 MCP 服务就创建好了——是不是很简单?其实就是 Tool,加上了协议而已。

在 Cursor 里配置测试

我们在 Cursor 里配置下这个 MCP Server:

json
{
  "mcpServers": {
    "my-mcp-server": {
      "command": "node",
      "args": ["/path/to/tool-test/src/my-mcp-server.mjs"]
    }
  }
}

配置好之后测试下,让 Cursor 查询一个用户的信息,确实能检测到这个 MCP 然后调用。

Cursor 有个坑注意下:点一下 Tool 是禁用,再点一下是启用。但 Cursor 这个状态颜色区分不明显——如果你没有调用 MCP 工具,可能是不小心关掉了。

这就是 MCP 的好处:写好之后可以插拔到任何地方当 Tool 用。因为有了 MCP,除了 Cursor,别的软件同样可以调用这个服务。

在 LangChain 里调用 MCP Server

用这个包:

bash
pnpm install @langchain/mcp-adapters

创建 src/langchain-mcp-test.mjs

js
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';

const model = new ChatOpenAI({
  modelName: "qwen-plus",
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

const mcpClient = new MultiServerMCPClient({
  mcpServers: {
    'my-mcp-server': {
      command: "node",
      args: [
        "/Users/guang/code/tool-test/src/my-mcp-server.mjs"
      ]
    }
  }
});

const tools = await mcpClient.getTools();
const modelWithTools = model.bindTools(tools);

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

  for (let i = 0; i < maxIterations; i++) {
    console.log(chalk.bgGreen(`⏳ 正在等待 AI 思考...`));
    const response = await modelWithTools.invoke(messages);
    messages.push(response);

    // 检查是否有工具调用
    if (!response.tool_calls || response.tool_calls.length === 0) {
      console.log(`\n✨ AI 最终回复:\n${response.content}\n`);
      return response.content;
    }

    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.push(new ToolMessage({
          content: toolResult,
          tool_call_id: toolCall.id,
        }));
      }
    }
  }

  return messages[messages.length - 1].content;
}

await runAgentWithTools("查一下用户 002 的信息");

await mcpClient.close();

我们用 @langchain/mcp-adapters 创建了 MCP Client,写法跟在 Cursor 里配置一样:用命令行启动这个进程,之后用 stdio 的方式做通信。

拿到 tools 之后绑定到模型。模型调用返回 tool_calls 消息,需要自己调用 Tool,调用完通过 ToolMessage 封装返回的消息,继续调用——这个循环我们写过很多次了。

调用试试:你让大模型查询用户,它识别到了工具调用,然后调用了 MCP 的工具。

这里进程没退出,因为你跑了一个子进程作为 MCP Server,需要把它关掉:

js
await mcpClient.close();

Resource 怎么用

那种静态信息可以放到 SystemMessage 里。先查一下 resource:

js
const res = await mcpClient.listResources();
console.log(res);

遍历依次读取 uri 内容:

js
const res = await mcpClient.listResources();
for (const [serverName, resources] of Object.entries(res)) {
  for (const resource of resources) {
    const content = await mcpClient.readResource(serverName, resource.uri);
    console.log(content);
  }
}

然后把它放到 SystemMessage 里作为上下文就好了:

js
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 messages = [
  new SystemMessage(resourceContent),
  new HumanMessage(query)
];

调用下试试(await runAgentWithTools("MCP Server 的使用指南是什么")),现在大模型就知道这个 resource 的信息,可以用来回答问题了。

Resource 可以用在 SystemMessage 里,也可以用在 HumanMessage 里,总之是作为信息引用的。我们主要还是用 MCP 的 tools。

这样,我们就写了一个 MCP Server,并分别在 Cursor、LangChain 里用上了它。

常见问题

大模型怎么知道要调用这个 tool? 启动 MCP 连接后,会自动读取 MCP Server 下每个 tool 的信息(description 和 schema),绑定到模型,和之前写 tool 然后 model.bindTools() 一样。

MCP Server 文件里没 export 任何东西,为什么能拿到工具? 因为 MCP 是通过进程通信(stdio)来交互的:你的代码启动了一个子进程运行 MCP Server,双方按协议在标准输入输出上通信,不需要模块导入。

MCP Server 只能用 Node 写吗? 任何语言都可以,MCP 是跨语言协议,Python、Java、Rust 都有对应的 SDK,这也是它最大的价值。

接入现成 MCP Server 有哪几种方式? 三种:本地调用(command 写开发语言如 node/python,args 填绝对路径)、安装使用(command 写 npx/uvx,args 填包名)、远程调用(直接配 url 连接远程服务)。

CallToolResultContentSchema 相关错误? 一般是 @langchain/mcp-adapters 版本和 Node 版本不匹配,降级到 0.4.0 或者升级 Node 版本即可。

总结

这节我们学了 MCP,它是可跨进程调用的 Tool

  • 可以是本地进程,用 stdio 进程通信
  • 可以是远程进程,用 HTTP 通信
  • 在 LangChain 里用 @langchain/mcp-adapters 封装成 tools 来用,其实和其他 tool 没区别
  • 跨进程就意味着不限语言,开发好之后可以被任意 MCP Client 调用,比如 Cursor、LangChain 等

当然,MCP 本质上还是 Tool,和之前的 Tool 的区别只不过是可以跨进程调用:当你不需要跨进程用的时候,还是之前那样写更好,还少了进程通信的成本。

除了自己写 MCP Server,现在也有很多现成的 MCP Server 可以直接用,下一节我们来用一下。

基于 VitePress 构建 · 专注前端与 AI 实战