MCP:让 Agent 的工具生态不再各自为战
从工具集成的 N×M 问题出发,系统解析 MCP 的架构设计、核心原语、传输机制与安全模型,说明它为什么可能成为 Agent 工具层的基础协议。
Hex4C59 16 分钟阅读 更新于 2026-10-17
AI 与 Agent·原创·概念与设计
在 Tool Use 那篇里,我在最后提到过 MCP:当 Tool Use 要走向规模化复用时,工具生态的标准化就成了绕不开的问题。在工具接口设计那篇里,我们讨论了面向 Agent 的工具应该怎么设计——粒度、返回值、错误处理、状态管理。
但那两篇都有一个隐含假设:工具是你自己写的,集成逻辑也是你自己控的。
现实往往不是这样。你可能想让 Agent 同时用上 GitHub、Slack、数据库、本地文件系统、Jira、Notion——这些工具来自不同的提供方,每个都有自己的 API 风格、认证方式和数据格式。如果每个 Agent 框架都要自己写一遍这些适配层,整个生态就会陷入重复造轮子的困局。
MCP(Model Context Protocol,模型上下文协议)约定应用与工具服务怎样交换请求和结果。本文面向会写函数和简单客户端、还没有接触过 MCP 的读者,按发现工具、提出请求、执行并回传的顺序说明角色分工。下文请求中的协议版本是 2025-03-26;代码和产品配置用于解释结构,不能据此判断其他版本的兼容性。
先给结论 #
- MCP 用共同的通信协议减少重复适配。 N×M 与 N+M 是集成关系的简化模型:应用实现客户端,工具方实现服务端,便可复用协议层。实际接入仍需配置版本、权限、认证和工具参数。
- MCP 是一个开放协议,不是一个框架。 它定义的是 Host、Client、Server 三层之间的通信规范,不绑定任何特定模型或 Agent 框架。
- MCP 提供三种原语:Tools(工具)、Resources(资源)和 Prompts(提示模板)。 其中 Tools 是最核心的——模型可以主动调用;Resources 是被动读取的数据源;Prompts 是可复用的交互模板。
- MCP 的角色分工帮助划分检查责任。 Host 决定授权,Client 负责通信,Server 执行并校验请求。仅按角色分层,不会自动阻止越权或数据泄露。
- MCP 已经在主流 Agent 产品中落地。 Cursor、Claude Desktop、Windsurf、Claude Code 都已原生支持 MCP,生态在快速增长。
工具集成的 N×M 问题 #
在没有标准协议之前,每个 Agent 应用要接入一个外部工具,都需要自己写一套集成逻辑:理解工具的 API、处理认证、解析返回格式、包装成模型能理解的工具描述。
假设有 N 个 Agent 应用(Cursor、Claude Desktop、自研 Agent、其他 IDE 插件……),M 个工具提供方(GitHub、Slack、PostgreSQL、文件系统、搜索引擎……),那么需要的集成工作量是 N×M。
这不仅浪费开发资源,还导致了一个更严重的问题:工具的质量取决于谁来写适配层。 同一个 GitHub API,不同 Agent 框架包装出来的工具描述、错误处理、返回格式可能完全不同。在工具接口设计里讨论过的那些设计原则——粒度、返回值去噪、结构化错误——每个集成方都需要独立实现一遍,质量参差不齐。
MCP 的解法是在中间插入一层标准协议:
工具提供方实现 MCP Server 后,兼容的客户端可以复用发现与调用方式。所谓从 N×M 降到 N+M,指不再为每一对应用与工具单独设计通信协议,并不表示所有认证、部署和兼容性工作都消失了。
这和 USB 解决外设连接问题的逻辑是一样的。在 USB 之前,每种外设都需要专用接口;有了 USB,设备只要遵守同一个协议标准,就能即插即用。MCP 想成为 AI Agent 工具层的"USB"。
MCP 的三层架构 #
MCP 把整个系统分成三个角色:Host、Client 和 Server。
Host #
Host 是面向用户的应用程序,比如 Cursor、Claude Desktop、你自己写的 Agent 应用。它负责:
- 创建和管理 Client 实例
- 控制用户交互和授权策略(比如"这个工具调用需要用户确认")
- 把 Client 发现的工具注册到模型的上下文里
- 执行安全策略,决定哪些工具可以自动执行、哪些需要人工批准
Host 是信任链的最上层。用户信任 Host,Host 信任(或不信任)各个 Server。
Client #
Client(客户端)是 Host 内负责通信的组件,向 Server(服务端)发送请求并接收结果。本文的连接结构中,每个 Client 实例对应一个 Server;Host 可以管理多个这样的实例。
Client 的职责是纯粹的协议通信:
- 发送
initialize请求,完成能力协商 - 调用 Server 暴露的 Tools / Resources / Prompts
- 处理 Server 端的通知和状态更新
- 维护连接的生命周期
Client 不直接面向用户,也不直接和模型交互。它是 Host 和 Server 之间的桥梁。
Server #
Server 是工具或数据的实际提供方。每个 Server 封装一组特定的能力:
- 一个 GitHub MCP Server 可能暴露
create_issue、list_pull_requests、get_file_contents等工具 - 一个 PostgreSQL MCP Server 可能暴露
query、list_tables、describe_table等工具 - 一个文件系统 MCP Server 可能暴露
read_file、write_file、list_directory等工具
Server 不必根据上游模型分别实现接口,但仍需检查调用者是否有权执行请求。它提供能力说明、校验参数并执行操作;Host 决定是否把这些能力开放给模型。
这种分层的好处是职责清晰:Server 只管"我能做什么",Client 只管"怎么通信",Host 只管"要不要执行、怎么展示给用户"。 三层各自独立演进,互不依赖。
三种核心原语 #
这里的“原语”(Primitive)指协议提供的基本交互类别:Tools 用来请求操作,Resources 用来读取资料,Prompts 用来获取可复用的消息模板。
Tools:模型主动调用的能力 #
Tools 是 MCP 里最核心的原语。它和我们在 Tool Use 里讨论的概念完全一致:模型在推理过程中判断需要调用外部能力,生成一个结构化的调用请求,系统执行后把结果回传给模型。
MCP 规范中,一个 Tool 的描述长这样:
{
"name": "create_issue",
"description": "Create a new issue in a GitHub repository. Use this when you need to report a bug, request a feature, or track a task.",
"inputSchema": {
"type": "object",
"properties": {
"repo": {
"type": "string",
"description": "Repository in owner/repo format, e.g. 'anthropics/mcp'"
},
"title": {
"type": "string",
"description": "Issue title, should be concise and descriptive"
},
"body": {
"type": "string",
"description": "Issue body in Markdown format"
},
"labels": {
"type": "array",
"items": {"type": "string"},
"description": "Labels to apply, e.g. ['bug', 'priority:high']"
}
},
"required": ["repo", "title"]
}
}这个结构和 OpenAI 的 function calling 定义很像,但 MCP 把它标准化了——无论是哪个 Server 提供的工具,描述格式都是一样的。Host 拿到描述后,再按所用模型 API 的要求转换工具定义;协议统一了服务端的描述方式,但不保证任意模型接口都能原样接收。
常见用法是由模型提出工具请求。 模型选择工具和参数,Host 再按权限与批准策略决定是否执行。提出请求与取得执行权限是两件事,协议也不要求所有应用都只能由模型发起调用。
Resources:被动读取的数据源 #
Resources 代表 Server 可以提供的数据——文件内容、数据库 schema、API 文档、配置信息等。和 Tools 不同,Resources 通常不是模型主动调用的,而是由 Host 或用户主动选择后注入到模型的上下文里。
{
"uri": "file:///workspace/src/main.py",
"name": "main.py",
"description": "Application entry point",
"mimeType": "text/x-python"
}Resources 的 URI 可以是文件路径、数据库标识、API 端点等。Host 通过 resources/list 获取可用资源列表,通过 resources/read 读取具体内容。
本文按常见控制方式区分两者: 模型提出工具请求,应用或用户选择加载资料。Resources 仍需由应用管理何时读取、读取多少;内容一旦放入模型输入,同样占用上下文,不能因为叫“资源”就忽略预算。
一个典型场景:用户在 Cursor 里用 @ 符号引用了一个数据库的表结构,Cursor 通过 MCP 的 Resources 接口读取这个结构,注入到模型的上下文里。模型不需要主动调用任何工具就能看到这些信息。
Prompts:可复用的交互模板 #
Prompts 是预定义的提示模板,由 Server 提供,用户可以选择使用。
{
"name": "code_review",
"description": "Review code for bugs, security issues, and best practices",
"arguments": [
{
"name": "code",
"description": "The code to review",
"required": true
},
{
"name": "focus",
"description": "Specific area to focus on: security, performance, readability",
"required": false
}
]
}用户选择 Prompt 模板并提供参数后,Server 返回模板生成的消息,Host 再决定怎样把它们加入对话。模板由服务端提供,不应仅因它被称作 Prompt,就自动取得应用系统指令的权限。
Prompts 的使用频率不如 Tools 和 Resources,但它在某些场景下很有价值:比如一个安全审计 MCP Server 可以提供标准化的审计流程模板,确保每次审计都遵循相同的检查清单。
三种原语的对比 #
| Tools | Resources | Prompts | |
|---|---|---|---|
| 控制方 | 模型(LLM 决定是否调用) | 应用/用户(主动选择加载) | 用户(主动选择使用) |
| 典型用途 | 执行操作、查询数据 | 提供上下文信息 | 标准化交互流程 |
| 对上下文的影响 | 每次调用都产生结果消息 | 注入一次,持续可见 | 生成完整消息序列 |
| 是否有副作用 | 可能有(写数据、发消息) | 通常没有(只读) | 没有(只生成消息) |
传输机制 #
文中请求使用 JSON-RPC 2.0:消息带有方法名、参数和请求标识,接收方用对应标识回传结果。下面区分本地子进程通信与远程 HTTP 通信。
stdio(标准输入输出) #
Client 以子进程方式启动 Server,通过 stdin/stdout 通信。
stdio 是标准输入输出:Client 启动本地 Server 子进程,通过输入输出流交换协议消息,不必额外开放网络端口。子进程仍可能继承启动者的文件与网络权限;进程分开不等于已经建立沙箱。
配置通常长这样:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "ghp_xxx"
}
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
}
}
}Streamable HTTP #
远程场景下使用 HTTP 传输。Client 通过 HTTP POST 发送 JSON-RPC 请求,Server 可以返回普通 JSON 响应,也可以升级为 SSE(Server-Sent Events)流式推送进度。
Streamable HTTP 适合 Server 运行在远程服务器上的场景——比如企业内部的数据库 MCP Server 部署在内网,多个开发者的 Agent 客户端远程接入。
两种方式可以承载相同的协议请求,但连接配置不同:本地方式要管理子进程,远程方式要管理地址、网络和认证。Client 仍需选择受支持的传输,不能仅更换名称就假定实现兼容。
连接生命周期 #
一次 MCP 连接从建立到关闭,经历三个阶段。
1. 初始化:能力协商 #
Client 发送 initialize 请求,告诉 Server 自己支持哪些协议版本和能力:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {
"roots": { "listChanged": true }
},
"clientInfo": {
"name": "Cursor",
"version": "1.0.0"
}
}
}Server 返回自己的能力声明:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true },
"prompts": { "listChanged": true }
},
"serverInfo": {
"name": "github-mcp-server",
"version": "0.5.0"
}
}
}能力协商的作用是双方对齐预期。如果 Server 声明了 tools 能力,Client 就知道可以调用 tools/list 和 tools/call。如果 Server 没有声明 resources,Client 就不会尝试读取资源。
2. 正常操作 #
初始化完成后,双方进入正常通信阶段。典型的调用流程:
这个阶段 Server 也可以主动发送通知,比如工具列表发生了变化(notifications/tools/list_changed),Client 收到后重新拉取工具列表。
3. 关闭 #
结束时,Client 按所用传输关闭连接或终止本地子进程,Server 清理资源。这里用 close 表示应用中的关闭动作,不把它当作所有 MCP 实现通用的通知方法名。
初始化、正常请求和关闭是阅读实现时需要区分的阶段。异常断线后怎样恢复、是否保留会话,仍需按所用传输和实现处理。
动手写一个 MCP Server #
下面的 Python 示例用 MCP SDK 展示笔记管理:添加笔记、按名读取、列出笔记,以及把索引作为资源提供。它把数据放在进程内字典里,进程退出后不会保留笔记;依赖版本和启动配置还需在实际环境中确认。
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("notes")
notes: dict[str, str] = {}
@mcp.tool()
def add_note(name: str, content: str) -> str:
"""
Add a new note or update an existing one.
Use this when the user wants to save information for later reference.
The name should be descriptive, e.g. 'meeting-notes-2026-03-23'.
Args:
name: Unique identifier for the note (lowercase, hyphens allowed)
content: The note content in plain text or Markdown
"""
notes[name] = content
return f"Note '{name}' saved ({len(content)} characters)"
@mcp.tool()
def get_note(name: str) -> str:
"""
Retrieve a note by name.
Use this when you need to recall previously saved information.
If the note doesn't exist, returns a list of available notes.
Args:
name: The name of the note to retrieve
"""
if name in notes:
return notes[name]
available = list(notes.keys()) if notes else []
return (
f"Note '{name}' not found. "
f"Available notes: {available if available else '(none)'}"
)
@mcp.tool()
def list_notes() -> str:
"""
List all saved notes with their sizes.
Use this to check what notes are available before trying to retrieve one.
"""
if not notes:
return "No notes saved yet."
lines = [f"- {name} ({len(content)} chars)" for name, content in notes.items()]
return f"Saved notes ({len(notes)} total):\n" + "\n".join(lines)
@mcp.resource("notes://list")
def notes_index() -> str:
"""All notes as a browsable index"""
if not notes:
return "No notes yet."
return "\n".join(f"- {name}: {content[:80]}..." for name, content in notes.items())
if __name__ == "__main__":
mcp.run()这段代码做了几件事:
- 用
@mcp.tool()装饰器注册了三个工具。 函数的 docstring 会自动变成工具描述,类型标注会变成参数 schema。这和在 Tool Use 里讨论的工具描述设计完全对应——描述里说明了什么时候该用、什么时候不该用、参数格式是什么。 get_note的错误处理遵循了工具接口设计里的原则。 没有抛出一个空洞的 “not found” 错误,而是返回了可用笔记的列表,给模型一个明确的恢复路径。- 用
@mcp.resource()暴露了一个 Resource。 Host 或用户可以主动读取这个资源,把笔记索引注入到上下文里。
要让 Cursor 或 Claude Desktop 接入这个 Server,只需要在配置里加一行:
{
"mcpServers": {
"notes": {
"command": "python",
"args": ["notes_server.py"]
}
}
}这就是 MCP 降低工具接入成本的直观体现:Server 开发者不需要知道上游是 Cursor 还是 Claude Desktop,不需要适配不同的 API 格式,只要遵守 MCP 协议就行。
MCP 和直接 Function Calling 的区别 #
有人可能会问:我直接在 Agent 代码里定义 tools 传给模型 API 不就行了,为什么要绕一圈走 MCP?
对于简单场景,确实不需要 MCP。如果你的 Agent 只用到两三个自定义工具,直接写 function calling 是最简单的。MCP 解决的是另一类问题:
工具复用 #
你用 Python 写了一个很好用的 GitHub 工具集,但同事的 Agent 用的是 TypeScript。没有 MCP 的话,他要么自己重写一遍,要么想办法在 TypeScript 里调你的 Python 代码。有了 MCP,你只需要把它包装成 MCP Server,他的 TypeScript Agent 通过 MCP Client 就能直接用。
工具发现 #
Agent 在运行时不一定知道有哪些工具可用。MCP 的 tools/list 接口让 Agent 可以动态发现可用工具,而不是在编译时硬编码。Server 甚至可以在运行时增减工具,然后通过 notifications/tools/list_changed 通知 Client 更新。
关注点分离 #
直接写 function calling 时,工具的实现逻辑和 Agent 的调度逻辑混在一起。当工具数量增多、需要独立部署和维护时,这种耦合会变成负担。MCP 强制把工具实现(Server)和工具使用(Client/Host)分开,让两边可以独立迭代。
生态效应 #
这是最大的区别。当越来越多的工具提供方实现 MCP Server,越来越多的 Agent 框架支持 MCP Client,整个生态的网络效应就形成了。你今天为 Cursor 开发的 MCP Server,明天 Claude Desktop 的用户也能直接用。
当然,MCP 也引入了额外的复杂度:协议层的 overhead、Server 进程管理、连接状态维护。如果你的 Agent 是一个封闭的单体系统,不需要工具复用,直接 function calling 仍然是更简单的选择。
真实产品中的 MCP 落地 #
Cursor #
Cursor 是最早原生支持 MCP 的 IDE 级 Agent 之一。用户在项目的 .cursor/mcp.json 或全局配置中声明 MCP Server,Cursor 启动后自动连接,把 Server 暴露的 Tools 注册到 Agent 的工具列表里。
Cursor 的 MCP 集成有几个值得注意的设计:
- 自动发现:Agent 在推理时可以看到所有已连接 MCP Server 的工具,像使用内置工具一样自然地调用
- 权限控制:有副作用的工具调用(写文件、执行命令等)会弹窗请求用户确认
- 子 Agent 继承:通过 Task 工具启动的子 Agent 可以继承父 Agent 的 MCP 工具集
Claude Desktop #
Claude Desktop 通过 claude_desktop_config.json 管理 MCP Server。配置方式和 Cursor 类似,支持 stdio 和 Streamable HTTP 两种传输。Claude Desktop 的 MCP 实现特别强调了 Resources 的使用——用户可以在对话中通过 UI 选择注入某个 Resource 的内容。
Claude Code #
Claude Code 除了支持标准的 MCP Server 配置,还利用 MCP 实现了一些更高级的功能。在多 Agent 协作里提到的 Agent Teams,每个 Teammate 都会加载项目配置的 MCP Server,这意味着所有 Agent 共享同一套外部工具集。
社区生态 #
MCP 的开源生态正在快速增长。官方维护了一批参考实现(GitHub、Slack、Google Drive、PostgreSQL、Puppeteer 等),社区也贡献了大量第三方 Server(Notion、Linear、Jira、各种数据库、云服务 API 等)。
这种增长是协议标准化带来的正反馈:可用的 Server 越多,Agent 客户端越有动力支持 MCP;支持 MCP 的 Agent 越多,工具提供方越有动力实现 MCP Server。
安全模型 #
工具能力越强,安全问题越重要。MCP 的安全设计基于几个层次。
信任边界 #
MCP 的三层架构天然定义了两道信任边界:
- 用户信任 Host:用户选择使用 Cursor 或 Claude Desktop,就意味着信任这个应用
- Host 有限信任 Server:Host 知道 Server 能做什么(通过能力协商),但不假设 Server 的行为一定安全
- Server 不信任 Client 发来的内容:Server 要做参数校验,不能把 Client 传来的内容直接当作可信输入执行
用户确认 #
对于有副作用的操作(写文件、发消息、执行命令),Host 应该在执行前请求用户确认。这是在 Tool Use 里讨论过的"操作分级"原则在协议层的体现。
MCP 规范建议工具在描述中通过 annotations 标注自己的行为特征:
{
"name": "delete_file",
"description": "Permanently delete a file from the filesystem",
"inputSchema": { ... },
"annotations": {
"title": "Delete File",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": false
}
}Host 可以把这些标注作为授权参考,例如区分 readOnlyHint: true 与 destructiveHint: true。但标注由 Server 声明,不能单凭“只读”提示就认定安全或自动批准;Host 还需结合工具来源、参数和实际授予的权限判断。
最小权限 #
每个 MCP Server 应该只暴露完成其职责所需的最小能力集。一个只做代码搜索的 Server 不应该暴露写文件的工具。这和在 Tool Use 里讨论的最小权限原则完全一致。
数据隔离 #
Host 应检查资料会被发送给谁。例如,读到一个 Server 的内部文档后,若要把内容作为另一个 Server 的工具参数,需确认任务授权允许这次传递。重点是控制数据流向;同处一个对话,不代表所有服务都可读取全部内容。
MCP 的局限与挑战 #
MCP 不是银弹。在实际使用中,它还面临一些真实的问题。
工具描述质量仍然参差不齐 #
MCP 标准化了工具描述的格式,但没有标准化描述的质量。一个 Server 开发者写的 description 是"Get data"还是精心设计的使用指南,完全取决于开发者自身的意识。而我们在工具接口设计里讨论过,描述质量会直接影响模型的调用决策。
协议能保证格式统一,但保证不了语义质量。这一层仍然需要社区约定和最佳实践来推动。
Server 质量和安全审计 #
任何人都可以发布一个 MCP Server,但用户很难判断一个第三方 Server 是否安全可靠。Server 可能存在安全漏洞、可能在工具调用时做了不该做的事、可能返回不准确的信息。
目前还没有一个成熟的 MCP Server 审计和信任评级机制。用户在使用第三方 Server 时需要自行评估风险。
状态管理 #
MCP 的连接模型是相对无状态的,但某些工具场景天然需要状态——比如数据库事务、多步骤文件编辑、长时间运行的任务。当前 MCP 的协议层对这些场景的支持还不够优雅,通常需要 Server 开发者自己在应用层实现状态管理。
性能开销 #
每次工具调用都要经过 JSON-RPC 序列化、进程间通信(stdio)或网络通信(HTTP)。对于高频工具调用场景(比如一个 Agent 在一轮推理中调用几十次文件系统操作),这些开销会累积起来。在延迟敏感的场景下,直接 function calling 仍然有优势。
总结 #
回到开头的问题:为什么 Agent 的工具生态需要 MCP?
因为工具集成的 N×M 问题,在 Agent 应用爆发式增长的背景下,已经成为整个生态的效率瓶颈。每个 Agent 框架各自实现一遍工具适配层,不仅浪费资源,还导致工具质量参差不齐、无法复用。
MCP 通过定义一套标准协议,把这个问题的复杂度从 N×M 降到 N+M:
- 三层架构(Host-Client-Server)让职责清晰分离,各层独立演进
- 三种原语(Tools-Resources-Prompts)覆盖了 Agent 与外部世界交互的核心需求
- 协议层的标准化让工具写一次、到处可用,形成生态网络效应
但 MCP 也不是所有场景的最优解。对于简单、封闭的 Agent 系统,直接 function calling 仍然更轻量。MCP 的价值在规模化场景下才真正显现——当你需要接入多个第三方工具、当你希望工具能被不同 Agent 复用、当你想让工具生态像 npm 包一样即装即用时,MCP 提供的标准化价值是最大的。
从更长远的视角看,MCP 解决的不是某个单点能力问题,而是 Agent 工具层能不能形成真正的生态。协议有了,生态在增长,但工具描述质量、安全审计、性能优化这些工程问题,仍然需要社区持续推进。
一起讨论
选中正文中的文字,可以针对这段内容发起讨论。
正在读取讨论…
在 GitHub 查看与管理讨论 ↗