一、为什么现在需要 MCP
AI Agent 落地时,最常见的需求不是“再换一个模型”,而是让模型安全、稳定地访问外部能力。查询数据库、读取文件、调用业务 API、执行计算、搜索知识库,这些操作都需要一个统一的工具边界。
如果没有协议约定,系统很容易演变成下面的局面:
- 每个模型框架定义一套工具格式。
- 每个业务系统写一套适配代码。
- 工具参数、错误、权限、日志和传输方式各不相同。
- Agent 从一个平台迁移到另一个平台时,大量集成代码需要重写。
Model Context Protocol,简称 MCP,目的就是把这部分集成标准化。它把能力提供方抽象为 Server,把能力使用方抽象为 Client,再用 JSON-RPC 和明确的初始化流程统一工具、资源、提示词、生命周期和传输。
从工程角度看,MCP 的价值不是“让模型更聪明”,而是让工具调用从一堆私有约定,变成可发现、可复用、可测试的协议能力。
这篇文章不会停留在概念介绍,而是以一个可运行的 Python 示例集为主线,完整跑通过:
- Server 与 Client 如何握手。
- 如何定义和调用工具。
- 如何返回结构化数据。
- 如何暴露资源和提示词。
- 如何管理 Server 生命周期。
- 如何接入 LangChain 和 LangGraph。
- 如何聚合多个 MCP Server。
- 如何在 stdio、SSE 和 Streamable HTTP 之间选择。
- 工程化落地时需要关注什么。
配套代码固定使用:
- Python 3.11 至 3.13
mcp==2.2.0- LangChain 1.x
- LangGraph 1.x
- Pydantic 2.x
需要特别说明的是,MCP Python SDK 2.x 已经将旧版 FastMCP 改名为 MCPServer:
from mcp.server.mcpserver import MCPServer
如果项目仍在 SDK 1.x,常见导入是:
from mcp.server.fastmcp import FastMCP
两者不能混用。本文的代码和测试都以 MCP 2.2.0 为准。
完整示例代码:python-mcp
二、MCP 中的三个角色
MCP 的整体架构设计清晰易懂,只要吃透三个核心角色的定位,就能基本理解整套协议的设计思路与运行逻辑:
Host
Host 是用户真正使用的应用,例如 IDE、聊天客户端、桌面 Agent 或企业工作台。Host 负责模型、会话、用户权限和交互体验。
Client
Client 由 Host 创建,负责连接一个 MCP Server。它管理协议握手、请求发送、响应解析、能力协商和连接关闭。
在 Python 中,核心对象通常是 ClientSession:
from mcp import ClientSession
Server
Server 暴露具体能力。MCP 中最常见的能力有三类:
- Tools:模型可以调用的函数。
- Resources:可以读取的上下文数据。
- Prompts:可复用的提示词模板。
除了这三类能力,Server 还参与协议初始化、能力声明、生命周期管理和传输协商。
如果把 Agent 比作应用层,那么 MCP 解决的是应用层和工具层之间的标准接口问题。
三、最小 Server 与 Client 调用
先看最小 Server:
from mcp.server.mcpserver import MCPServer
server = MCPServer(
name="server-client-demo",
title="MCP Server 与 Client 调用",
description="演示初始化握手、能力协商和安全关闭。",
instructions="演示初始化握手、能力协商和 ping",
version="1.0.0",
)
if __name__ == "__main__":
server.run(transport="stdio")
这里还没有注册工具,但它已经具备一个完整 MCP Server 的基础元数据:
-
name:协议层面的服务唯一标识,用于内部匹配
-
title:面向开发者展示的服务名称,便于直观识别
-
description:简单阐述服务核心能力与用途
-
instructions:面向大模型与客户端的功能说明,辅助能力调用
-
version:服务版本标识,便于后续迭代兼容、版本管控
客户端通过 stdio 启动 Server,然后初始化会话:
async with stdio_client(server_params) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
initialized = await session.initialize()
print(initialized.server_info.name)
print(initialized.protocol_version)
print(session.instructions)
await session.send_ping()
初始化阶段主要完成三件事:
- Client 声明自身协议版本和能力。
- Server 返回自身信息、协议版本和能力。
- 双方在共同支持的范围内建立会话。
这一步看起来简单,但它决定了后续能不能调用工具、读取资源以及使用某些扩展能力。
一个常见错误是把调试日志直接打印到 stdout。对于 stdio 传输,stdout 是 JSON-RPC 数据通道,任何额外输出都可能破坏协议流。调试信息应该写 stderr,这也是示例项目统一遵守的约束。
四、Tools:让模型调用外部能力
Tools 是 MCP 体系中使用频率最高的核心能力,主要用于承载各类可变的业务操作。SDK 提供了简洁的装饰器注册方式,能够自动完成参数校验、工具描述生成。
@server.tool()
def add(a: int, b: int) -> int:
"""返回两个整数之和。"""
return a + b
函数名、类型标注和 docstring 会被 SDK 转换成工具名称、输入 Schema 和描述。客户端可以先获取工具列表:
result = await session.list_tools()
for tool in result.tools:
print(tool.name, tool.description)
再调用工具:
result = await session.call_tool("add", {"a": 20, "b": 22})
返回结果中的 content 是协议内容块。对于普通文本结果,可以这样读取:
text = "".join(
item.text
for item in result.content
if getattr(item, "type", None) == "text"
)
异步工具与进度
工具可以是同步函数,也可以是异步函数:
@server.tool()
async def slow_echo(
text: str,
ctx: Context,
delay: float = 0.05,
) -> str:
await ctx.report_progress(1, 2, "开始")
await asyncio.sleep(delay)
await ctx.report_progress(2, 2, "完成")
return text.upper()
客户端可以传入进度回调:
async def on_progress(
progress: float,
total: float | None,
message: str | None,
) -> None:
print(progress, total, message)
await session.call_tool(
"slow_echo",
{"text": "hello mcp"},
progress_callback=on_progress,
)
进度通知很适合耗时任务。但要注意:进度更新不是结果本身。真正的业务结果仍然应通过工具返回值表达,进度只用于观测。
预期错误与未预期异常
工具调用过程中的报错大致可分为两类,合理区分、针对性处理,能够提升模型的自主纠错能力,也更便于线上问题快速排查:
第一类是预期业务错误,例如除零、记录不存在、状态不允许:
from mcp.server.mcpserver.exceptions import ToolError
@server.tool()
def divide(a: float, b: float) -> float:
if b == 0:
raise ToolError("division by zero")
return a / b
客户端会收到 is_error=True 的结果,模型可以读取错误内容并决定下一步动作。
第二类是非预期异常,例如数据库断开、代码缺陷或依赖故障。这类异常不应该伪装成业务结果。服务端应记录堆栈,客户端只获得受控的错误信息。
日常开发可以参考这套稳健思路:参数定义清晰规范、返回结构尽量稳定、报错信息可读可追溯。
五、结构化输出:从文本转向可验证对象
传统工具调用经常返回一段 JSON 字符串。模型和程序都能“看到”结果,但调用方还需要自己解析和校验。
MCP 支持结构化输出。服务端可以直接声明 Pydantic 返回模型:
class ReportDetails(BaseModel):
owner: str
reviewed: bool
notes: list[str] = Field(default_factory=list)
class Report(BaseModel):
title: str
score: float
tags: list[str]
details: ReportDetails
@server.tool(structured_output=True)
def build_report(topic: str, score: float) -> Report:
return Report(
title=f"{topic} Report",
score=score,
tags=["python", "mcp"],
details=ReportDetails(
owner="Ada",
reviewed=True,
notes=["schema validated", "nested object"],
),
)
客户端读取:
result = await session.call_tool(
"build_report",
{"topic": "Quarterly", "score": 97.5},
)
data = result.structured_content
这里有两个名字需要区分:
小细节:协议原生字段为 structuredContent,Python SDK 2.2.0 已自动适配为蛇形命名 structured_content,无需开发者手动兼容转换。
整体优势十分实用:数据结构可自动校验、嵌套结构完美适配、数据类型规范统一,不用过度依赖模型的文本格式自律,工具调用的稳定性会明显提升。
六、Resources:把上下文作为可寻址资源
Tools 强调“执行动作”,Resources 强调“读取内容”。
静态资源可以绑定固定 URI:
@server.resource(
"demo://config",
name="config",
mime_type="application/json",
)
def config_resource() -> dict[str, object]:
return {
"server": "examples",
"resource_type": "static",
"features": ["tools", "resources", "prompts"],
}
动态资源使用 URI 模板:
@server.resource(
"demo://users/{user_id}",
name="user_profile",
mime_type="application/json",
)
def user_profile(user_id: str) -> UserProfile:
return UserProfile(
user_id=user_id,
name="Ada Lovelace",
role="admin",
)
客户端可以发现并读取:
resources = await session.list_resources()
templates = await session.list_resource_templates()
config = await session.read_resource("demo://config")
profile = await session.read_resource("demo://users/1")
Resources 适合以下场景:
- 项目文档和配置。
- 数据源 Schema。
- 用户资料。
- 可版本化的知识片段。
- 需要被多个工具或提示词复用的上下文。
Resources 和 Tools 不应该互相替代。读取一段配置通常更适合 Resource;执行数据库修改通常更适合 Tool。
七、Prompts:把提示词作为可复用能力
Prompt 不是模型调用的替代品,而是 Server 提供的提示词模板。它可以帮助 Host 统一提示结构、参数和消息序列。
同步提示词:
@server.prompt(
name="explain_code",
description="根据编程语言和源码生成代码解释任务。",
)
def explain_code(language: str, code: str) -> list[UserMessage]:
return [
UserMessage(
content=(
f"请解释下面这段 {language} 代码。\n"
f"```{language.lower()}\n{code}\n```"
)
)
]
异步提示词可以访问数据库、文件或其他异步依赖:
@server.prompt()
async def review_code(
language: str,
code: str,
focus: str = "正确性、可读性和边界条件",
) -> list[UserMessage | AssistantMessage]:
return [
UserMessage(content=f"请审查 {language} 代码,重点关注 {focus}。"),
AssistantMessage(content="## 问题清单\n"),
]
客户端先获取提示词,再按参数渲染:
prompts = await session.list_prompts()
result = await session.get_prompt(
"explain_code",
arguments={
"language": "Python",
"code": "def divide(a, b):\n return a / b",
},
)
需要注意,Prompt 提供的是消息模板,不是自动执行流程。是否使用、如何组合、是否发送给模型,仍由 Host 决定。
八、Lifespan:管理 Server 生命周期
真实 Server 往往需要初始化连接池、加载模型、读取配置或创建缓存。这些工作不应该放在每次请求里。
MCP Server 支持 lifespan:
@dataclass
class AppState:
values: list[str] = field(default_factory=list)
started: bool = False
closed: bool = False
@asynccontextmanager
async def lifespan(server: MCPServer[AppState]) -> AsyncIterator[AppState]:
state = AppState(started=True)
try:
yield state
finally:
state.closed = True
注册到 Server:
server = MCPServer(
name="lifespan-demo",
version="1.0.0",
lifespan=lifespan,
)
工具中通过 Context 访问:
@server.tool()
async def remember(
value: str,
ctx: Context[AppState, Any],
) -> dict[str, object]:
state = ctx.request_context.lifespan_context
state.values.append(value)
return {
"count": len(state.values),
"values": list(state.values),
}
同一条连接内,两次调用会得到递增状态。连接关闭后,lifespan 的 finally 负责清理。
比较适合放入生命周期管理: 数据库连接池、HTTP 客户端、全局缓存、模型句柄、服务启动校验、关闭日志记录 不太适合: 单次请求临时变量、用户独立会话、不适合全局共享的可变数据
九、接入 LangChain 与 LangGraph
想要将 MCP 工具接入主流 Agent 框架,需要做一层简易适配转换。这里分享一个实际落地的兼容问题:目前主流的 langchain-mcp-adapters 0.3.1 适配库仅支持 MCP 1.x 版本,暂时无法兼容 2.2.0 新版接口。
因此我们可以手动实现一层轻量桥接工具 MCPToolkit,适配新版 MCP 与 Lang 系列框架的接入逻辑。
class MCPToolkit:
async def get_tools(self) -> list[StructuredTool]:
tools: list[StructuredTool] = []
for server_name, connection in self.connections.items():
async with self._open(connection) as session:
result = await session.list_tools()
tools.extend(
self._convert(server_name, connection, mcp_tool)
for mcp_tool in result.tools
)
return tools
每个 MCP 工具被转换为 LangChain StructuredTool:
async def call_tool(**arguments: object) -> str:
async with self._open(connection) as session:
result = await session.call_tool(mcp_tool.name, arguments)
if result.is_error:
raise ToolException(self._result_text(result))
return self._result_text(result)
return StructuredTool.from_function(
coroutine=call_tool,
name=name,
description=mcp_tool.description or "",
args_schema=mcp_tool.input_schema,
)
Agent 使用 LangChain 当前推荐入口:
from langchain.agents import create_agent
tools = await MCPToolkit(connections).get_tools()
agent = create_agent(model=model, tools=tools)
result = await agent.ainvoke(
{"messages": [HumanMessage(content="Use multiply to calculate 6 * 7.")]}
)
开发参考:LangGraph 1.x 已逐步废弃 create_react_agent 接口,建议优先使用全新的 create_agent,可以有效规避版本兼容问题。 示例支持双模式调试:离线 Fake 模型可用于单元测试与 CI 自动化校验,配置 OpenAI 密钥后可切换真实大模型,适配不同开发、测试场景。
默认示例使用 Fake Model,因此离线就能验证工具绑定、工具调用和图执行。设置 OPENAI_API_KEY 后,可以切换到真实 ChatOpenAI:
$env:OPENAI_API_KEY="..."
uv run python examples/07_langchain_tools/client.py --real
这种“双模式”设计很适合工程团队:
- 离线模式用于单元测试和 CI。
- 真实模型模式用于端到端验收。
- 协议连接、工具转换和 Agent 执行可以分层定位问题。
十、多 Server 聚合
在复杂度较高的企业级 Agent 项目中,一般不会采用单一 MCP Server 承载所有能力。我们可以按照业务维度拆分独立服务,比如数据库服务、文件处理服务、业务 API 服务、搜索服务等,各司其职、解耦协作。
这种服务拆分模式具备诸多优势:权限边界更清晰、各服务可独立部署扩容、通用能力可复用、单服务故障影响范围可控,整体架构更稳健。
多 Server 示例同时配置两个 stdio Server:
connections = {
"math": StdioConnection(
command=sys.executable,
args=[str(ROOT / "math_server.py")],
cwd=str(ROOT),
),
"text": StdioConnection(
command=sys.executable,
args=[str(ROOT / "text_server.py")],
cwd=str(ROOT),
),
}
tools = await MCPToolkit(connections).get_tools()
agent = create_agent(model=model, tools=tools)
当多个 Server 暴露同名工具时,需要制定命名策略,例如:
math_add
text_add
多服务开发注意点
多服务聚合场景下,比较容易出现工具重名冲突的问题。建议提前制定统一的命名规范,例如 math_add、text_add,也可以通过命名空间、工具白名单做简单管控,规避冲突问题。
同时还需要关注各类工程细节:服务启动超时处理、单服务故障降级、权限隔离、链路追踪、并发限流、多服务结果聚合等,建议搭配简易路由治理层使用,保障整体服务稳定性。
十一、传输模式:stdio、SSE 与 Streamable HTTP
MCP 支持多种传输方式。选择传输模式时,不能只看“能不能连上”,还要看部署边界、生命周期、认证和可观测性。
stdio
Server 由 Client 作为子进程启动,通过标准输入和标准输出通信:
server.run(transport="stdio")
适合:
- 本地 CLI。
- IDE 插件。
- 桌面 Agent。
- 单用户工具进程。
优势是部署简单、进程边界明确。限制是难以跨网络复用,服务和客户端生命周期绑定。
SSE
SSE 使用长连接接收服务端事件,再通过 HTTP 发送消息:
server.run(
transport="sse",
host="127.0.0.1",
port=8000,
)
客户端:
async with sse_client("http://127.0.0.1:8000/sse") as streams:
async with ClientSession(*streams) as session:
await session.initialize()
适用场景: 老旧项目兼容、需要服务端主动推送消息的业务场景
选型建议: 新项目如需使用 HTTP 类传输,可优先考虑更现代化、适配性更强的 Streamable HTTP
Streamable HTTP
Streamable HTTP 是更现代的 HTTP 传输方式:
server.run(
transport="streamable-http",
host="127.0.0.1",
port=8000,
)
客户端:
async with streamable_http_client("http://127.0.0.1:8000/mcp") as streams:
async with ClientSession(*streams) as session:
await session.initialize()
适合:
- 远程 Server。
- 多客户端复用。
- 与现有 HTTP 基础设施集成。
- 需要网关、负载均衡和统一鉴权的场景。
无论使用哪种传输方式,都建议统一处理:
- 连接超时。
- 重试和退避。
- 会话关闭。
- 空闲连接回收。
- 认证和授权。
- 请求日志和链路追踪。
十二、工程落地参考清单:从 Demo 到生产可用
跑通示例代码仅为入门基础,想要适配生产环境、实现稳定落地,可以参考以下优化方向,逐步完善工程能力:
1. 合理划分能力边界
尽量保持职责清晰、各司其职:Tool 负责执行业务操作、产生数据变更;Resource 负责承载只读上下文、可缓存数据;Prompt 负责通用模板复用,避免各类能力混用混乱。
2. 规范输入输出设计
参数命名语义清晰,用枚举约束固定状态值,复杂业务场景优先使用结构化输出,尽量保证返回值版本兼容,报错信息兼顾可读性、可追溯性与安全性。
3. 分层处理异常机制
可预判的业务异常通过 ToolError 标准化返回,方便模型自主调整调用逻辑;系统级异常完整记录日志用于内部排查,对外统一脱敏展示,规避信息泄露风险。
4. 精细化管理生命周期
服务启动时校验外部依赖可用性,复用连接池等重型资源,服务关闭时主动释放各类句柄,尽量避免在单次请求中重复初始化资源,减少性能损耗。
5. 保障传输与进程安全
stdio 模式杜绝污染通信数据流,HTTP 服务配置合理的鉴权与限流规则,设置合规的资源配额,异常进程留存完整日志,方便线上问题复盘排查。
6. 搭建多层测试体系
可参考四层测试思路稳步落地:单元测试校验参数与数据结构、集成测试验证协议连通性、Agent 测试校验工具调度逻辑、端到端测试覆盖真实业务场景。
7. 完善可观测能力
完善调用日志、耗时统计、异常分层、链路追踪等能力,能够快速定位问题根源,区分故障出在传输层、协议层、服务层还是模型层。。
十三、建议的学习路径
不建议新手直接尝试搭建复杂的企业级 Agent 平台,循序渐进、由浅入深的学习方式,更容易吃透核心知识点:
-
跑通最简 Server + Client 案例,理解基础握手与通信逻辑
-
熟悉协议协商、心跳连通的底层基础机制
-
掌握同步/异步工具、进度上报、上下文调用的使用方式
-
尝试用结构化输出替代传统易出错的 JSON 字符串拼接
-
理清 Resources、Prompts 两类能力的适用边界与最佳实践
-
掌握 Lifespan 机制,实现全局资源的合理管理
-
对接 LangChain/LangGraph 框架,实现基础 Agent 调度能力
-
实践多 Server 聚合部署,掌握工具冲突的治理方案
-
对比三种传输方式的优劣,形成适配业务的选型思路
-
最后逐步完善权限管控、限流容错、部署运维等工程能力
十四、结语:聊聊 MCP 的工程价值
MCP 并非颠覆行业的全新技术,更多是为 AI Agent 工程化落地提供一套标准化、可复用的基础设施。
从代码层面来看,MCP 的使用方式十分简洁,仅需少量装饰器与启动代码即可快速搭建服务;但从工程落地层面来看,它统一了协议交互、工具定义、异常规范、生命周期管控、传输策略等零散的开发环节。
它在很大程度上优化了行业通用的集成痛点,弱化了工具与各类 AI 框架的强绑定关系,减少了业务私有接口的重复适配成本。通过标准化的服务拆分与协议调用,能够小幅提升 Agent 项目的复用性、可测试性、可迭代性与可维护性。
简单来说:MCP 让“模型调用工具”这件事,从零散的手动代码适配,变成了一套相对稳定、可落地的标准化工程方案。