MCP 协议:打破工具围墙,让 AI Agent 跨语言跨进程调用任意工具

摘要:手写内联工具的两个致命缺陷------项目锁定和语言锁定,MCP 协议如何通过 stdio/HTTP 跨进程通信彻底解决,包含 MCP Server 注册工具与资源、MCP Client 获取与调用的完整实战。


在上一篇文章中,我们用 200 多行代码手写了一个 mini-Cursor Agent。它通过四个内联工具(read_filewrite_filelist_directoryexecute_command)实现了自动创建项目、写入代码、安装依赖和启动服务的能力。但很快你会发现这套方案有两个致命问题:

  1. 项目锁定 :这四个工具定义在 all-tools.mjs 中,和 Agent 代码紧耦合。换个项目想复用这些工具?只能复制粘贴。
  2. 语言锁定:Node.js 写的工具,Java 或 Python 项目怎么用?Rust 写的 CLI 工具能直接整合进 Agent 吗?

这些问题的本质是:工具和 LLM 之间缺乏一个标准化的通信协议。而 MCP(Model Context Protocol)正是为了解决这个问题而生的------它让工具独立于 LLM,支持本地/远程、跨语言调用,通过标准化协议实现 LLM 和工具的解耦。


为什么需要 MCP:从内联工具到跨进程调用

回顾此前 mini-Cursor 的工具定义方式:

javascript 复制代码
const tools = [
    readFileTool,      // 定义在 all-tools.mjs
    writeFileTool,     // 定义在 all-tools.mjs
    listDirectoryTool, // 定义在 all-tools.mjs
    executeCommandTool, // 定义在 all-tools.mjs
];
const modelWithTools = model.bindTools(tools);

这段代码存在几个结构性问题。工具和 Agent 在同一个进程里,工具代码和 Agent 代码在同一个仓库里,而且工具只能用 Node.js 写------因为 bindTools 接收的是 JavaScript 函数对象。如果一个团队用 Python 写了一个强大的数据分析工具,想给 Node.js 的 Agent 用,只能用 Python 重写一遍,或者在 Node.js 里通过 child_process 手动调用 Python 脚本,再手动解析输出。

MCP 的解决思路是:把工具从 Agent 进程里"拿出去",放到一个独立的 MCP Server 进程中,Agent 通过标准化的协议与它通信。不管是本地工具还是远程工具,Agent 想跨进程调用某个工具,通过 MCP 协议就行。

两种传输模式:本地用 stdio,远程用 HTTP

MCP 支持两种传输方式,覆盖了本地和远程两种场景:

传输方式 适用场景 底层机制
stdio(标准输入输出) 本地跨进程调用 键盘输入、控制台输出------当一个进程(Agent)启动一个子进程(MCP Server)时,通过 stdio 实现进程间通信
HTTP 远程跨网络调用 标准的 HTTP 请求/响应,MCP 掌管远程通信

stdio 模式的核心是 IPC(进程间通信) 。Agent 进程通过 child_process 启动一个 MCP Server 子进程,两者通过标准输入输出流交换 JSON 数据。这和 fetch 调用 API 接口有本质区别:MCP 不是去拿接口数据,而是去扩展 Context------让 LLM 能做的事情更多(Tool),知道的东西更多(Resource)。


MCP Server:注册工具和资源

MCP Server 是一个独立的进程,通过 @modelcontextprotocol/sdk 创建。Server 可以注册两种内容:Tool (工具)和 Resource(资源)。

注册 Tool:让 Agent 能查询用户信息

以下是一个完整的 MCP Server 实现,它注册了一个 query_user 工具和一个使用指南资源:

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: 'zs@qq.com', role: 'admin' },
        '002': { id: '002', name: '李四', email: 'ls@qq.com', role: 'user' },
        '003': { id: '003', name: '王五', email: 'ww@qq.com', role: 'user' },
    },
};

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

// 注册 Tool
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: `用户 ${user.id} 的信息是:姓名:${user.name},邮箱:${user.email},角色:${user.role}`,
                },
            ],
        };
    }
);

registerTool 的三个参数分别对应了工具的三个核心要素:名称query_user)、描述和 Schema (告诉 LLM 这个工具的参数和用途)、执行函数 (实际干活的逻辑)。这和 LangChain 的 tool() 函数在结构上完全一致------只是 MCP 的工具定义是跨进程的,而 LangChain 的工具定义是进程内的。

注册 Resource:给 LLM 补充背景知识

除了 Tool,MCP Server 还可以注册 Resource。Resource 是一种静态内容,可以作为 SystemMessage 的一部分注入到 LLM 的上下文中,让 LLM 在对话前就"知道"一些背景信息:

javascript 复制代码
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 会自动调用相应工具。
                    `,
                },
            ],
        };
    }
);

Resource 的 URI 是一个逻辑标识符(如 docs://guide),不指向真实的文件路径。LLM 通过这个 URI 来"读取"资源内容。Resource 和 RAG 是两种不同的上下文补充策略:RAG 是先检索再注入,适合海量文档的按需查询;Resource 是直接注入,适合篇幅不大、需要 LLM 在对话开始前就了解的基础信息。

启动 Server:通过 stdio 暴露服务

javascript 复制代码
const transport = new StdioServerTransport();
await server.connect(transport);

StdioServerTransport 把 MCP Server 挂载到标准输入输出流上。当 Agent 进程通过 child_process 启动这个 Server 文件时,两个进程之间的 stdio 通道就建立起来了------Agent 通过 stdin 发送请求,Server 通过 stdout 返回结果。


MCP Client:Agent 如何接入 MCP

有了 MCP Server,Agent 端需要一个 MCP Client 来发现和调用这些工具。LangChain 的 @langchain/mcp-adapters 提供了 MultiServerMCPClient,可以同时连接多个 MCP Server:

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

const model = new ChatOpenAI({
    modelName: 'deepseek-v4-flash',
    apiKey: process.env.DEEPSEEK_API_KEY,
    configuration: {
        baseURL: process.env.DEEPSEEK_BASE_URL,
    },
});

// 配置 MCP Server ------ 可以配置多个
const mcpClient = new MultiServerMCPClient({
    mcpServers: {
        'my-mcp-server': {
            command: 'node',
            args: [
                'F:/workspace/zzd_ai/ai/agent_in_action/mcp-demo/src/my-mcp-server.mjs',
            ],
        },
    },
});

MultiServerMCPClient 的配置中,每个 Server 用 command + args 指定启动方式。底层原理是 child_process.spawn------Agent 进程启动一个 Node 子进程来运行 MCP Server,两个进程通过 stdio 通信。主进程通过 stdio 和他们"通话",close() 则把这个链接和子进程一起关掉。

获取 Tool 和 Resource

javascript 复制代码
// 获取所有 MCP Server 注册的工具
const tools = await mcpClient.getTools();

// 获取所有 MCP Server 注册的资源
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;
    }
}

getTools() 返回的工具列表可以直接传给 model.bindTools(tools),和之前内联工具的用法完全一致。区别在于:这些工具现在运行在另一个进程 中,甚至可以用另一种语言编写。

listResources() 返回的是一个嵌套结构:外层是 Server 名称到资源列表的映射,内层是每个资源的 URI 和描述。上面的代码用 Object.entries() 遍历这个嵌套对象,把每个资源的内容拼成一个字符串,作为 SystemMessage 的一部分注入到 LLM 的上下文中。Object.entries() 把对象转换为 [key, value] 二维数组,是处理这种嵌套结构的标准方式:

javascript 复制代码
const obj = {
    'bytedance': ['AI全栈开发', 'Agent 工程师'],
    'tencent': ['后端开发', 'Agent 工程师'],
    '163': ['前端开发'],
};
for (let [key, value] of Object.entries(obj)) {
    console.log(key, value);
}

ReAct 循环 + MCP 工具

工具和资源获取完毕后,Agent 的 ReAct 循环和之前完全一致,只是 SystemMessage 中多了一段资源内容:

javascript 复制代码
const modelWithTools = model.bindTools(tools);

async function runAgentWithTools(query, maxIterations = 30) {
    const messages = [
        new SystemMessage(resourceContent),  // 资源作为 SystemMessage 的一部分
        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,
                    })
                );
            }
        }
    }
    return messages[messages.length - 1].content;
}

await runAgentWithTools('MCP Server 的使用指南是什么?');
// 或者
await runAgentWithTools('查询用户ID为002的信息');

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

mcpClient.close() 是一个容易被忽略但至关重要的步骤。Agent 进程启动 MCP Server 子进程后,子进程会一直运行。如果不调用 close(),Node 脚本不会退出,会一直挂着占用系统资源。close() 关闭和 MCP Server 的通信通道,同时终止子进程。


MCP 与 Tool Use 的本质区别

回顾整个 MCP 的实现,可以总结出 MCP 和传统 Tool Use 之间的一个关键对比:

维度 内联 Tool Use MCP
工具位置 Agent 进程内 独立进程(本地或远程)
语言限制 必须和 Agent 同语言 任意语言,通过 stdio/HTTP 通信
复用性 项目锁定,复制粘贴 一个 Server 可被多个 Agent 复用
生命周期 随 Agent 启动/销毁 独立生命周期,Agent 通过 close() 控制
传输方式 内存中的函数调用 stdio(本地 IPC)或 HTTP(远程)

MCP 不是取代 Tool Use,而是把 Tool Use 的能力从"进程内"扩展到了"跨进程"。Tool 本身的定义方式(名称 + Schema + 执行函数)没有变化,变化的是调用路径:从内存中的函数调用变成了跨进程的 stdio/HTTP 通信。


总结

MCP(Model Context Protocol)用一句话概括就是:给 Model 扩展 Context,让它能做的更多(Tool),知道的更多(Resource),通过标准化的 Protocol 实现。核心机制可以拆为三个层次:

  1. 跨进程通信 :Agent 通过 child_process 启动 MCP Server 子进程,两者通过 stdio 交换 JSON 数据。主进程负责发起请求,子进程负责执行工具并返回结果。
  2. 工具与资源分离:Tool 是"做事"的能力(查询用户、执行命令),Resource 是"知道"的信息(使用指南、文档说明),两者都作为 Context 注入 LLM 的推理过程。
  3. 生命周期管理MultiServerMCPClient 负责启动子进程、获取工具和资源、执行调用,close() 负责释放子进程和通信通道。

回到文章开头的两个问题------项目锁定和语言锁定------MCP 给出了一个清晰的答案:把工具从 Agent 进程中分离出来,用 stdio 或 HTTP 作为通信桥梁,让任何语言编写的工具都能被任何 Agent 调用。 在这个架构下,一个 Python 数据科学家写的数据分析工具,可以被一个 Node.js 的 Agent 通过 MCP 直接调用,无需任何代码重写。

MCP 正在成为 AI Agent 生态的基础设施。Cursor、Trae 等 IDE 的 MCP 支持,本质上就是内置了一个 MCP Client,让用户可以通过配置 mcpServers 来扩展 IDE 的能力。理解了 MCP,你就理解了 AI Agent 从"单机工具"走向"分布式能力网络"的关键一步。

相关推荐
凛风前行2 小时前
多智能体架构的 5 种经典模式
架构·agent
leeyi2 小时前
调试 Agent 的 3 个姿势:日志、断点、可视化(第49篇-E35)
aigc·agent·devops
ch8562 小时前
智能体13-RAG
agent
小林ixn2 小时前
远程 MCP 实战:让 AI 无缝调用地图、浏览器与文件系统
人工智能·agent·mcp
chanalbert2 小时前
Agent 工程化指南(一):概念与认知架构 — 智能体到底是什么、怎么思考
llm·agent
小林ixn2 小时前
AI Agent的“万能工具箱”:MCP协议实战,让工具调用不再受语言和进程束缚
人工智能·agent·mcp
冬奇Lab3 小时前
MCP 系列(03):生态导航——官方 Server 清单与社区精选
人工智能·资讯·mcp
Esaka_Forever4 小时前
MCP(Model‑Context‑Protocol)整体架构
mcp
一个处女座的程序猿4 小时前
Agent之Memory:claude-mem的简介、安装和使用方法、案例应用之详细攻略
agent·memory·claude-mem