深入理解 MCP 协议:让 AI Agent 工具调用突破边界
引言
在构建 AI Agent 应用时,Tool(工具)是一个核心概念。Tool 让 LLM 拥有了操作外部世界的能力------查询数据库、调用 API、操作文件系统。但传统的 Tool 实现方式存在一些根本性的局限。本文将从一个实际项目(mcp-demo)的思考出发,带你深入理解 MCP(Model Context Protocol)协议是什么、解决了什么问题、以及它是如何工作的。
传统 Tool 的痛点
回顾传统 Agent 开发中 Tool 的实现方式,会发现两个明显的问题:
1. Tool 与项目强耦合
在大多数框架中,Tool 以函数或类的形式直接写在 Agent 项目的代码里。这意味着:
- 你在项目 A 中写的 Tool,无法直接在项目 B 中使用
- 每次开始一个新项目,你都需要重新编写或手动迁移 Tool 代码
- Tool 的复用几乎为零,团队之间无法共享工具能力
2. 语言绑定的限制
假设你用 Node.js 写好了一套完美的 Tool 集合,但你的同事或另一个团队使用 Python / Java / Rust 开发 Agent------这些 Tool 就无法直接复用。每种编程语言生态都有各自的优势库和工具,语言壁垒让跨团队协作变得困难重重。
核心矛盾:Tool 应当独立于 LLM,实现本地/远程、跨进程、跨语言的调用。
MCP 协议是什么
MCP 全称 Model Context Protocol(模型上下文协议),由 Anthropic 提出并开源。它的核心定位是:
标准化 LLM 与 Tool、Resource 之间的通信协议,实现 LLM 与 Tool 的解耦。
通俗地说,MCP 是一种"通用语言"。它让 AI Agent(充当 MCP 客户端 / Host)可以通过统一的方式,调用各种不同语言、不同位置、不同实现方式的 MCP 服务器所提供的工具和资源。
MCP 的通信方式
MCP 设计了两种通信方式,分别覆盖本地和远程两种场景:
stdio(标准输入输出流)------ 本地跨进程通信
这是 MCP 最轻量的通信方式。当一个 Agent 进程需要调用另一个子进程中的工具时,它通过 标准输入输出流(stdin/stdout) 来收发 JSON-RPC 格式的消息。
scss
┌─────────────┐ stdin (JSON-RPC) ┌──────────────┐
│ AI Agent │ ◄──────────────────────► │ MCP Server │
│ (Host) │ stdout (JSON-RPC) │ (子进程) │
└─────────────┘ └──────────────┘
- Agent 通过
child_process等方式启动子进程 - 通信就像键盘输入和控制台输出一样简单直接
- 子进程可以是任何语言实现------Node.js、Python、Go、Rust 等
- 无需网络端口,无需额外配置,天然安全
HTTP ------ 远程跨进程通信
对于远程工具调用场景,MCP 支持通过 HTTP 协议进行通信。这让你可以:
- 部署远程 MCP Server,供多个 Agent 实例共享
- 调用部署在云端或内网其他机器上的工具
- 利用 HTTP 生态的成熟基础设施(负载均衡、鉴权、监控等)
scss
┌─────────────┐ HTTP (SSE/JSON-RPC) ┌──────────────┐
│ AI Agent │ ◄─────────────────────────────► │ MCP Server │
│ (Host) │ │ (远程服务) │
└─────────────┘ └──────────────┘
实战:手写一个 MCP Server
理论讲完了,我们来看一个完整的 MCP Server 实例。以下代码来自 src/my-mcp-server.mjs:
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: 'nike', email: 'nike@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: `查询数据库中的用户信息。输入用户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}`
}
]
};
});
// 跨进程通信方式 stdio
const transport = new StdioServerTransport();
await server.connect(transport);
下面我们逐段解析这段代码,看看一个 MCP Server 到底由哪些部分组成。
第一步:引入依赖
javascript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
这里引入了三个核心模块:
| 模块 | 作用 |
|---|---|
McpServer |
MCP 服务器的核心类,负责创建 Server 实例、注册 Tool 和 Resource,是整座房子的"骨架" |
StdioServerTransport |
stdio 传输层实现,负责通过标准输入输出流与 Agent Host 通信,是数据进出 MCP Server 的"管道" |
z(Zod) |
一个 TypeScript 优先的 schema 声明与验证库,用来定义 Tool 的输入参数格式。Agent 在调用 Tool 前会先拿到这个 schema,从而知道该传什么参数、参数是什么类型 |
💡 为什么需要 Zod? MCP 协议本身并不规定你必须用 Zod。但 SDK 选择了 Zod 作为 inputSchema 的描述方式------它既是运行时的参数校验,又是 LLM 的参数理解依据。Agent 拿到你的 Tool 时,会读到
inputSchema中的z.string().describe(...),从而推理出"调用这个工具需要传一个字符串类型的 userId"。
第二步:准备数据------Tool 独立于 Agent 项目
javascript
const database = {
users: {
'001': { id: '001', name: '张三', email: 'zs@qq.com', role: 'admin' },
'002': { id: '002', name: 'nike', email: 'nike@qq.com', role: 'user' },
'003': { id: '003', name: '小红', email: 'xh@qq.com', role: 'user' },
}
};
这里用一个内存对象模拟了数据库。注意一个关键点:这段数据存在于 MCP Server 进程内部,而不是 Agent 项目里。
这就是 MCP 的"解耦"思想在代码层面的体现:
- Agent(Host)不需要知道数据怎么存的、存了什么
- MCP Server 独立持有数据、独立运行、独立部署
- 未来把这个内存对象替换成真实的 MySQL / PostgreSQL / Redis,Agent 端一行代码都不需要改
第三步:创建 MCP Server 实例
javascript
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0'
});
两行代码,创建了一个 MCP Server。name 和 version 是必须提供的元信息------当 Agent(Host)连接到这个 Server 时,首先拿到的就是这些基本信息。
类比理解:这就像是启动了一个"微服务",只不过它不是监听某个端口,而是等待父进程通过 stdio 发来的 JSON-RPC 消息。
第四步:注册 Tool------核心步骤
javascript
server.registerTool('query_user', {
description: `查询数据库中的用户信息。输入用户ID,
返回该用户的详细信息(姓名、邮箱、角色)`,
inputSchema: {
userId: z.string().describe('用户ID, 例如:001, 002, 003')
}
}, async ({ userId }) => {
// ... 处理逻辑
});
registerTool 是 MCP Server 最重要的 API。它接收三个核心参数:
4.1 Tool 名称 ------ 'query_user'
这是 Tool 的唯一标识符。Agent 在调用时会说"我要调用 query_user 工具"。命名应该简洁、动词开头、语义清晰。
4.2 Tool 元数据 ------ description + inputSchema
这是 MCP 协议的精髓所在------Tool 会"自我介绍":
-
description:用自然语言告诉 LLM 这个工具是干什么的。LLM 会根据这段描述,自主判断"当前用户的问题是否需要调用这个工具"。描述越清晰精准,LLM 调用工具的时机就越准确。 -
inputSchema:定义这个工具需要什么参数。这里用了 Zod:javascript{ userId: z.string().describe('用户ID, 例如:001, 002, 003') }它告诉 LLM:"你调用我时需要传一个
userId参数,它是字符串类型,取值类似001、002、003"。LLM 看到这段 schema 后,就能从用户的话里提取出 userId,正确地构造调用参数。
这就是 MCP 的"扩展 Context":Tool 把自己的能力描述(description)和调用规范(inputSchema)注入到 LLM 的上下文中。不是 LLM 主动去查,而是 Tool 主动报到。
4.3 Tool 执行逻辑 ------ Handler 函数
javascript
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}` }
]
};
}
Handler 函数的执行流程非常清晰:
- 接收参数 ------
{ userId }由 MCP 框架自动从 Agent 的调用请求中解析并注入,类型已在 inputSchema 中校验 - 执行业务逻辑 ------ 查数据库(这里是内存对象),找到对应 user
- 返回标准化结果 ------ 返回一个
content数组,每个元素包含type和text
返回结构的标准化同样重要:
javascript
{ content: [{ type: 'text', text: '...' }] }
无论成功还是失败,MCP Server 都返回同样结构的结果。Agent 拿到后,会把它注入到 LLM 上下文中------LLM 看到这段文本,就能把查询结果用自然语言转述给用户。
- 找到用户时:返回详细信息,LLM 可以向用户直接播报
- 未找到用户时:返回友好提示 + 可用 ID 列表,LLM 会告诉用户"没有这个用户哦,试试 001、002、003 吧"
这就是 Tool 的"闭环":描述(告诉 LLM 怎么用) → 参数 Schema(约束调用格式) → Handler 执行业务 → 结构化返回(注入 LLM 上下文) → LLM 用自然语言回答用户。
第五步:连接传输层------启动服务
javascript
const transport = new StdioServerTransport();
await server.connect(transport);
这是 MCP Server 的"最后一公里"------把 Server 实例挂载到一个传输层上,让它真正运转起来。
StdioServerTransport创建了一个 stdio 传输层实例。它做的事情很简单:监听 stdin,把收到的 JSON-RPC 消息转给 server 处理,然后把 server 的返回结果写到 stdout。server.connect(transport)是一个异步操作,把 server 和传输管道连接起来。连接成功后,server 就处于"就绪"状态------等待 Agent Host 发来的指令。
整个数据流可以这样理解:
c
Agent (Host) MCP Server (这个文件)
│ │
│ ① 启动子进程: node my-mcp-server.mjs │
│───────────────────────────────────────►│
│ │
│ ② 通过 stdin 发送 JSON-RPC: │
│ {"method":"tools/call", │
│ "params":{"name":"query_user", │
│ "arguments":{"userId":"001"}}} │
│───────────────────────────────────────►│ ③ transport 接收 → server 处理
│ │ → handler 查数据库
│ ④ 通过 stdout 返回 JSON-RPC: │
│ {"content":[{"type":"text", │
│ "text":"用户 001 的信息是:..."}]} │
│◄───────────────────────────────────────│
│ │
│ ⑤ LLM 将结果转述给用户 │
这段代码揭示的 MCP 设计哲学
从这 45 行代码中,我们可以抽象出 MCP Server 的几个核心设计原则:
-
声明式注册 :Tool 通过
registerTool声明式注册,包含名称、描述、参数 schema、处理逻辑。Agent 端不需要任何代码层面的"引入"或"import"------它在连接时自动发现所有 Tool。 -
标准化通信:无论是 Tool 注册的元数据(description + inputSchema)还是 Tool 调用的结果(content\[\]),都有固定的结构。这就是 MCP 作为"协议"的价值------双方都按约定好的格式说话,无需猜测。
-
传输层可替换 :这段代码用的是
StdioServerTransport。如果未来要改成 HTTP 模式,只需要把最后两行换成new StreamableHTTPServerTransport()并启动 HTTP 服务------Server 本身(注册的 Tool、Handler 逻辑)完全不需要改动。 -
进程即边界:整个 MCP Server 是一个独立的 Node.js 进程。它和 Agent 之间唯一的耦合就是 stdio(或 HTTP)。这意味着你可以用 Python 写另一个 MCP Server,Agent 同时连接 Node.js Server 和 Python Server------对 Agent 来说,它们都是 MCP 协议下的平等工具提供者。
MCP 的核心特点
跨进程调用工具
这是 MCP 最大的特点。无论是本地进程还是远程服务,Agent 都可以通过统一的 MCP 协议来发现和调用工具。你只需要在 Client 端配置好 MCP Server 的连接信息,工具就会自动出现在 Agent 的可用工具列表中。
不是 Fetch,而是扩展 Context
这里有一个关键认知:MCP 不是简单的接口调用。
普通的 HTTP 调用(fetch)是你向某个 API 发起请求并获取数据,这是"拿数据"的逻辑。而 MCP 的目标更高一层------它是扩展 LLM 的 Context(上下文)。
MCP 为 LLM 提供两类核心扩展:
| 扩展类型 | 说明 | 举例 |
|---|---|---|
| Tool | 让模型能"做更多" | 查数据库、发邮件、操作文件 |
| Resource | 让模型能"知道更多" | 读取文档、获取实时数据、访问知识库 |
真实的生态价值
基于 MCP,社区已经构建了丰富的工具生态。一个用 Python 写的 MCP Server(比如文件系统操作工具),可以被任何支持 MCP 的 Agent(无论是 Claude Desktop、Cursor、还是你自己写的 Agent)直接使用------无需重写、无需适配、即插即用。
总结
MCP 协议解决了一个根本性的工程问题:如何让 AI Agent 的工具能力可复用、可共享、跨语言、跨平台。 它通过标准化的通信层(stdio + HTTP),让 Tool 从"项目代码的一部分"变成"独立的、可被任意 Agent 调用的服务"。
其设计哲学可以概括为:
- 解耦:LLM 和 Tool 独立演进,互不依赖
- 标准化:统一的协议,消除碎片化
- 开放:任何语言、任何平台都可以实现 MCP Server
- 扩展性:本地 stdio 和远程 HTTP 两种模式覆盖所有场景
MCP 正在成为 AI Agent 时代的"通用接口标准"------就如同 USB 之于外设、HTTP 之于 Web 服务。理解并掌握 MCP,是每一个 AI Agent 开发者的必修课。