MCP 入门实战:Tool 和 LLM 解耦?跨进程跨语言调用工具原来是这么回事

你有没有遇到过这样的窘境:用 LangChain 写了一堆 Tool,结果发现只能在当前项目里用?换个项目就得重新拷一遍?更糟糕的是,团队里有人用 Java、有人用 Python,大家的 Tool 完全没法共享?

这篇笔记,我们就从"传统 Tool 的痛点"出发,一步步搞懂 MCP 协议是如何解决这些问题的,并附上真实可跑的代码示例。


一、先看看我们现在写的 Tool 有什么问题?

在接触 MCP 之前,我们写 Tool 的方式大概是这样的:直接在项目里用 @langchain/core/toolstool() 函数注册工具,然后和模型 bindTools 绑定完事。

javascript 复制代码
// all-tools.mjs 片段
import { tool } from '@langchain/core/tools';
import fs from 'node:fs/promises';

const readFileTool = tool(
    async({ filePath }) => {
        const content = await fs.readFile(filePath, 'utf-8');
        return content;
    },
    {
        name: 'read_file',
        description: '用此工具来读取文件内容...',
        schema: z.object({
            filePath: z.string().describe('要读取的文件路径')
        })
    }
)
// ... 还有 writeFileTool, listDirectoryTool, executeCommandTool
export { readFileTool, writeFileTool, ... };

这样写能用,但有两个致命问题

问题 1:只能在当前项目用,不能复用

这些 Tool 跟 LangChain 的 tool() 强绑定,直接写在项目里。换一个项目(比如不用 LangChain、或者用 Python 的 LangChain),这些 Tool 就没法直接用了------你得拷代码、改依赖、改语法......

问题 2:只能用 Node.js 写,跨语言?想都别想

上面的 Tool 是 JavaScript 写的。那如果我有一个用 Rust 写的高性能日志分析工具、或者一个用 Python 写的机器学习推理脚本,怎么让 LLM 调用?传统做法是:你得在 Node 里用 child_process 去 spawn 子进程,自己处理输入输出、自己封装协议。各种边界情况全靠自己踩坑。

结论:Tool 应该独立于 LLM

理想状态下,Tool 应该是一个独立的服务------不管 LLM 用什么框架(LangChain/ LlamaIndex/ 自己写的 Agent)、不管用什么语言(JS/Java/Python/Rust)、不管在本地还是远程,都能统一调用。

这,就是 MCP 协议要做的事。


二、MCP 协议到底是什么?

MCP = Model Context Protocol,模型上下文协议。一句话总结:

MCP 是一个标准化 LLM 与 Tool / Resource 之间通信 的协议,目的是把 LLM 和 Tool 彻底解耦

MCP 的核心思想:通信方式只有两种

MCP 不搞复杂的定制协议,它就用两种最通用的方式来通信:

通信方式 适用场景 说明
stdio(标准输入输出流) 本地跨进程调用 就是键盘输入、控制台输出那套东西。Agent(父进程)启动一个子进程(比如一个 node 脚本 / python 脚本),双方通过 stdin / stdout 传递 JSON 消息。
HTTP 远程跨进程调用 工具部署在远端服务器,Agent 通过 HTTP 请求调用。MCP 掌管统一的接口格式。

所以不管是:

  • 本机一个 Node 写的 Tool
  • 本机一个 Python 写的 Tool
  • 局域网另一台机器上用 Rust 写的 Tool
  • 云上部署的 Java Tool Service

只要它们都遵守 MCP 协议,Agent 就能统一调用。

MCP 在"拓展什么"?

MCP 的全称里有个关键词------Context(上下文)。它不是在给 LLM 加 API 接口,而是在拓展 LLM 的"上下文能力":

  • Tool(工具) :让 LLM 能做的更多(比如查数据库、读文件、调用外部系统)
  • Resource(资源) :让 LLM 知道的更多(比如把一份文档、一份配置直接注入上下文)

三、MCP 的最大特点:跨进程调用工具

这是 MCP 最核心的价值。我们用一张图来理解:

scss 复制代码
┌──────────────────────────────────────────────────┐
│                  AI Agent (Host)                 │
│   (LangChain / Claude Code / Trae / Cursor 等)  │
│                                                  │
│   MultiServerMCPClient(管理多个 MCP Server)     │
└──────────┬───────────────────┬───────────────────┘
           │ stdio             │ HTTP
           ▼                   ▼
┌──────────────────┐  ┌──────────────────────────┐
│  MCP Server #1   │  │     MCP Server #2        │
│  (Node 子进程)    │  │  (远程 HTTP 服务)        │
│  - query_user    │  │  - 高复杂度计算工具       │
│  - 使用指南资源   │  │  - 企业内部系统对接      │
└──────────────────┘  └──────────────────────────┘
           │ stdio
           ▼
┌──────────────────────────┐
│     MCP Server #3        │
│   (Python / Rust 子进程)  │
│  - 机器学习推理 / 高性能  │
└──────────────────────────┘

AI Agent 作为 MCP Host(客户端),通过 clients 配置可以同时连接多个 MCP Server。每个 Server 可以是:

  • 本地子进程:通过 stdio 通信(Node / Python / Rust 啥都行)
  • 远程服务:通过 HTTP 通信

而这一切,对 LLM 来说完全透明------它只需要知道"我有这些 Tool 可以用",至于 Tool 在哪个进程、用什么语言写的、在本地还是远程,它根本不关心。

和普通的 API 调用(fetch)有什么区别?

fetch 是"拿接口数据",返回的是原始 JSON,LLM 不会主动去调用 fetch,你得在 prompt 里或者代码里明确告诉它什么时候调。

而 MCP 的 Tool:

  1. 自动注册到 LLM 的 tools 列表里
  2. LLM 自主决策什么时候调用、调哪个、传什么参数
  3. 返回结果自动注入下一轮对话上下文

MCP 不是一个简单的接口封装,它是直接给 Model 拓展 Context 能力的协议层


四、MCP Tool 实战:写一个用户查询服务

光说不练假把式,我们直接看项目里的真实代码 my-mcp-server.mjs

4.1 写 MCP Server(工具提供方)

javascript 复制代码
// 1. 引入 MCP SDK(注意 SDK 1.30.0 的坑,要用子路径导入)
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

// 2. 假数据(未来可以换成真实数据库查询)
const database = {
    users: {
        '001': { id: '001', name: '小a', email: 'a@qq.com', role: 'admin' },
        '002': { id: '002', name: '小b', email: 'b@qq.com', role: 'user' },
        '003': { id: '003', name: '小c', email: 'c@qq.com', role: 'user' },
    }
}

// 3. 实例化 MCP Server
const server = new McpServer({
    name: 'my-mcp-server',
    version: '1.0.0',
});

// 4. 注册 Tool:query_user
server.registerTool('query_user', {
    description: '查询数据库中的用户信息。输入用户ID,返回该用户的详细信息',
    inputSchema: {
        userId: z.string().describe('用户ID,例如:001,002,003')
    }
}, async (args) => {
    const userId = typeof args === 'string' ? args : args.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}`
        }]
    }
})

// 5. 用 stdio 方式启动(跨进程通信的核心!)
const transport = new StdioServerTransport();
await server.connect(transport);

就这么简单!注意几个关键点:

  • registerTool:注册工具,参数依次是工具名、描述、zod 参数 Schema、处理函数
  • 返回格式固定 :必须是 { content: [{ type: 'text', text: '...' }] },这是 MCP 协议规定的
  • StdioServerTransport :启动后,这个脚本就变成了一个"从 stdin 读请求、往 stdout 写响应"的服务。它自己不监听端口------是 Host(Agent)启动它当子进程,通过标准流通信。

4.2 写 MCP Host(工具调用方,即 Agent)

然后看 Agent 端怎么用

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

// 1. 初始化模型
const model = new ChatOpenAI({
    modelName: 'deepseek-v4-flash',
    apiKey: process.env.DEEPSEEK_API_KEY,
    baseURL: 'https://api.deepseek.com/v1',
});

// 2. 配置 MCP Client------关键!告诉 Agent 有哪些 Server 可以用
const mcpClient = new MultiServerMCPClient({
    mcpServers: {
        'my-mcp-server': {
            command: 'node',
            args: ['C:/Users/xhj/Desktop/db_ai/xhj_ai/ai/agent_in_action/mcp-demo/src/my-mcp-server.mjs']
        }
        // ★ 这里可以继续加!
        // 'python-mcp-server': { command: 'python', args: ['./my_tool.py'] },
        // 'rust-mcp-server': { command: 'cargo', args: ['run', '-p', 'my-mcp'] },
        // 'remote-mcp-server': { url: 'https://mcp.example.com/sse' },
    }
});

// 3. 从所有 Server 获取 Tools
const tools = await mcpClient.getTools();
// 4. 把 Tools 绑定到模型上
const modelWithTools = model.bindTools(tools);

// 5. Agent 推理循环
async function runAgentWithTools(query, maxIterations = 30) {
    const messages = [new HumanMessage(query)];
    
    for (let i = 0; i < maxIterations; i++) {
        const response = await modelWithTools.invoke(messages);
        messages.push(response);

        // 模型判断:不需要调工具 → 直接出最终答案
        if (!response.tool_calls?.length) {
            console.log(`最终回复:${response.content}`);
            return;
        }

        // 模型判断:要调工具 → 自动找到对应 Tool 执行,结果回灌上下文
        for (const toolCall of response.tool_calls) {
            const foundTool = tools.find(t => t.name === toolCall.name);
            const toolResult = await foundTool.invoke(toolCall.args);
            messages.push(new ToolMessage({
                content: toolResult,
                tool_call_id: toolCall.id  // ★ 必须带,不然模型不知道对应哪个调用
            }));
        }
    }
}

// 使用!
await runAgentWithTools('查询用户002的信息');
// 用完要关,不然进程挂着
await mcpClient.close();

运行流程拆解:

  1. MultiServerMCPClient 根据配置,自动启动子进程 node my-mcp-server.mjs(底层就是 Node 的 child_process.spawn
  2. 父子进程通过 stdio 建立 MCP 协议通信
  3. Agent 通过 getTools() 拿到所有注册的工具(内部通过 MCP 协议发 tools/list 请求)
  4. 模型推理时自动决定是否调用工具,调用结果通过 ToolMessage 回灌
  5. 最终模型综合工具返回结果,生成回答

4.3 另一个 MCP Server 示例:读文件工具

项目里还有一个写得非常详细的版本 server.js,用的是新版 server.tool() 语法(和 registerTool 功能等价,更简洁):

javascript 复制代码
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from 'fs/promises';

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

// 新版语法:server.tool(name, desc, schema, handler)
// 一行顶旧版 60+ 行代码
server.tool(
    "read_file",
    "读取指定路径的本地文件内容",
    {
        path: z.string().describe("文件的绝对或相对路径")
    },
    async ({ path }) => {
        try {
            const content = await fs.readFile(path, 'utf-8');
            return { content: [{ type: "text", text: content }] };
        } catch (err) {
            return {
                isError: true,
                content: [{ type: "text", text: `读取文件失败:${err.message}` }]
            };
        }
    }
);

// ★ 调试日志必须打 stderr!stdout 是 MCP 协议通道
async function main() {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error("MCP read_file 服务已启动(stdio模式)");
}
main().catch(console.error);

⚠️ 踩坑提醒 :调试日志用 console.error(输出到 stderr),千万别用 console.log!因为 stdout 被 MCP 协议占了,你打非协议内容上去,Client 解析 JSON-RPC 会直接炸掉。


五、MCP Resource:除了 Tool,还能给 LLM "喂知识"

MCP 不止 Tool,还有一个重要概念:Resource(资源) 。Resource 的作用是------把静态内容作为 SystemMessage 的一部分,直接注入 LLM 的上下文

5.1 怎么注册 Resource?

javascript 复制代码
// Server 端注册 Resource
server.registerResource(
    '使用指南',               // 名称
    'docs://guide',           // URI(自定义协议头,比如 docs://)
    {
        description: 'MCP Server 使用指南',
        mimeType: 'text/plain'
    },
    async () => {
        return {
            contents: [{
                uri: 'docs://guide',
                mimeType: 'text/plain',
                text: `
                    MCP Server 使用指南
                    功能:提供用户查询等工具。
                    使用:在 Cursor 等 MCP Client 中通过自然语言对话即可。
                `
            }]
        }
    }
)

5.2 Host 端怎么用 Resource?

在 Agent 启动时,把所有 Resource 的内容读出来,拼成 SystemMessage:

javascript 复制代码
// langchain-mcp-test.mjs 片段
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;
    }
}
console.log(resourceContent, '---------------');

// ★ 关键:作为 SystemMessage 注入上下文!
const messages = [
    new SystemMessage(resourceContent),
    new HumanMessage(query)
];

5.3 Resource 和 RAG 有什么区别?

这是个非常好的问题。它们都是"给 LLM 加知识",但定位不同:

维度 MCP Resource RAG
数据规模 小而精的文档(比如使用指南、配置说明、API 文档) 大规模知识库(百万级文档)
触发时机 Agent 启动时全部加载进 SystemMessage 对话时根据用户问题实时检索,只取相关片段
上下文窗口 内容不能太长,否则撑爆 Token 限制 不受知识库大小限制,每次只塞入最相关的几条
使用场景 工具的使用说明、系统的基础配置、固定的业务规则等 长尾问答、企业知识库搜索等

一句话 :Resource 是"我一启动就该知道的常识",RAG 是"用户问了我才去查的大百科"。两者完全可以搭配使用


六、踩过的坑 & 注意事项

6.1 MCP SDK 1.30.0 的入口 Bug

项目依赖的 @modelcontextprotocol/sdk@1.30.0 有个发布缺陷:主入口文件 ./dist/esm/index.js 缺失。所以不能用默认导入:

javascript 复制代码
// ❌ 会报 ERR_MODULE_NOT_FOUND
import { McpServer } from '@modelcontextprotocol/sdk';

// ✅ 必须用子路径导入
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';

6.2 pnpm 装不上依赖?换 npm

项目用 pnpm 时即使显示 Already up to datenode_modules 也可能没生成(linker/cache 的坑)。直接换 npm install 就完事。

6.3 stdio 模式下的进程管理

mcpClient.close() 千万不能忘!因为每个 MCP Server 都是一个独立的子进程,不手动 close 会导致脚本一直挂着不退出。


七、总结:MCP 的价值是什么?

回顾一下我们开头提出的两个问题:

痛点 MCP 的解法
Tool 只能在当前项目用,不能复用 Tool 封装成独立的 MCP Server,任何支持 MCP 协议的 Agent 都能直接用
只能用 Node 写 Tool,跨语言难 通过 stdio 子进程通信,Python / Rust / Java / Go 写的程序都能包装成 MCP Server

MCP 的本质是标准化:它没有发明什么新技术(stdio 和 HTTP 都是几十年的老东西了),但它定义了一套 LLM 和工具之间的"通用语言"。

有了 MCP 之后:

  • 前端用 Node 写了一个文件操作工具 → 全团队的 LLM Agent 都能用
  • 算法组用 Python 写了一个推理脚本 → 包个 MCP Server 即可接入
  • 运维组用 Rust 写了个日志分析工具 → 同样只要实现 MCP 协议

Tool 不再是某个项目的私有财产,而是整个团队可共享的能力资产。


下一步行动

  1. 试着把项目里 all-tools.mjsread_file / write_file 工具改写成独立的 MCP Server
  2. 试试用 Python 写一个 MCP Server(官方 SDK 支持 Python),用同一个 Agent 去调用它
  3. 试试在 Trae / Cursor 这类原生支持 MCP 的 IDE 里配置你的 MCP Server,直接和 AI 对话调用工具

欢迎在评论区分享你的 MCP 实践经验和踩过的坑

相关推荐
星火102424 分钟前
【从 0 到 1 动手造 Agent】02、确定性铁笼 LangGraph
人工智能·后端·agent
桃西西呀30 分钟前
模型都能自己写代码了,你的 Agent 为什么还接不进一个日历?——一篇讲透 MCP 这个 AI 世界「USB-C」
人工智能·ai编程·mcp
星火102430 分钟前
【从 0 到 1 动手造 Agent】03、给 Agent 装上操作系统——MemGPT/Letta 内存分层与自我演化
人工智能·后端·agent
Csvn33 分钟前
第 7 章 MCP 标准化工具接入
人工智能·aigc·agent
武子康35 分钟前
拆开 Pi Monorepo:改模型、循环、产品和 UI 时,代码应该放在哪一层
人工智能·llm·agent
武子康39 分钟前
一次 Agent 失败后,到底该改模型、Prompt 还是 Router?
人工智能·llm·agent
深念Y1 小时前
# CC-Switch + Claude/Codex 折腾教训记录
运维·服务器·网络·ai·agent·web·ccsiwtch
淇奥71 小时前
Learn-CC 学习笔记2
agent
AI创飞人类1 小时前
企业 AI Agent 如何从 Demo 走向生产?知识库、权限、工具调用与版本治理
agent·智能体