- 人工智能
- MCP 服务
- MCP Clients
【免费下载链接】python-sdk
The official Python SDK for Model Context Protocol servers and clients
导读
在 MCP(Model Context Protocol)Python SDK 中,处理器(handler)是一个工具(tool)、资源(resource)或提示(prompt)背后的普通 Python 函数,其参数由客户端(模型)传入。而除此之外的一切——请求本身、服务器共享状态、向客户端反馈的通道——则集中在这份《Inside your handler》指南所覆盖的能力中:处理器能读取什么(Context、依赖、生命周期状态),运行中能做什么(elicitation 交互、采样与 roots、进度汇报、日志、订阅通知)。读完本文,你将掌握在@mcp.tool()、@mcp.resource()与@mcp.prompt()处理器中正确使用这八类能力的完整路线图,并理解 2026-07-28 协议时代下多轮往返(multi-round-trip)请求对它们的影响。
处理器能力的全景地图
本指南(docs/handlers/index.md)把处理器的能力划分为两大阵营:
能读取什么(来自服务器框架本身,而非模型):
| 能力 | 一句话说明 | 详细文档 |
|---|---|---|
| Context | 任意处理器唯一可要求注入的附加参数,携带当前请求、请求头、会话,以及进度与变更通知的操作 | docs/handlers/context.md |
| 依赖(Dependencies) | 模型永远看不到的参数,由你自己的函数通过Resolve填充 | docs/handlers/dependencies.md |
| 生命周期(Lifespan) | 服务器启动时只构建一次的状态,处理器通过Context访问 | docs/handlers/lifespan.md |
运行中能做什么(向客户端方向发出的动作):
| 能力 | 一句话说明 | 详细文档 |
|---|---|---|
| Elicitation / 多轮往返 | 中途向用户索要额外输入(表单或跳转 URL) | docs/handlers/elicitation.md、docs/handlers/multi-round-trip.md |
| Sampling 与 Roots | 请求客户端执行 LLM 补全、获取其工作区目录(已弃用但仍提供服务) | docs/handlers/sampling-and-roots.md |
| 进度(Progress) | 为耗时操作上报进度通知 | docs/handlers/progress.md |
| 日志(Logging) | 面向服务器运维者输出日志(写到标准错误) | docs/handlers/logging.md |
| 订阅(Subscriptions) | 告知已订阅的客户端列表发生了变更 | docs/handlers/subscriptions.md |
如果你还没有注册过任何处理器,请先阅读 docs/servers/tools.md;上述每篇页面都假设你已经有了一个可运行的处理器。
处理器能读取什么:三项只进不出的注入
Context:唯一一个任意处理器都能要求的附加参数
工具的普通参数来自模型,而其余一切(正在服务的请求、所在的服务器、与客户端对话的通道)都来自同一个对象:Context。你不需要构造它、也不需要配置它——只需要要求它:在任意工具函数上添加一个标注为Context的参数即可(示例见 docs_src/context/tutorial001.py)。
SDK 为每次请求构建全新的Context并注入;参数名无关紧要(ctx、context、c都可以,SDK 按类型标注识别);资源与提示处理器可以用同样方式声明;ctx.request_id即当前所服务请求的 id。在源码层面,Context定义于 src/mcp/server/mcpserver/context.py,其核心能力request_id、report_progress、read_resource、elicit、elicit_url均在此实现。
对模型不可见是必须内化的关键点。tools/list为search_books上报的输入 schema 只有一个属性query:
{ "type": "object", "properties": { "query": {"title": "Query", "type": "string"} }, "required": ["query"], "title": "search_booksArguments" }ctx不是参数:它永远不会出现在 schema 中,模型永远不会被告知它的存在,客户端也无法填充它。这是你与 SDK 之间的契约,在线上对协议不可见。
Context提供的主要能力还包括:
await ctx.read_resource(uri):从工具内部读取服务器自己的资源,与resources/read走同一注册表,返回ReadResourceContents的可迭代对象;await ctx.report_progress(progress, total, message):长调用期间向调用方流式上报进度;await ctx.elicit(message, schema)/ctx.elicit_url(...):暂停工具并向用户提问;ctx.session:与当前客户端对话的服务端一侧,发往客户端的通知都在这里;ctx.headers:传输层携带的请求头(stdio 下为None),可用(ctx.headers or {}).get("x-...")读取自定义头——注意头部属于客户端提供的输入,适合做语言区域或特性开关,永远不能当作身份凭证;ctx.request_context:原始请求记录,最常用字段是lifespan_context(即生命周期启动代码 yield 出的对象)。
日志刻意不在这个清单里:服务器应当像任何普通 Python 程序一样使用标准库logging。另需注意:注入只发生在你注册的那个函数上,工具内部调用的辅助函数不会获得自己的Context,需要把ctx当作普通参数向下传递——不存在可以从别处取用的"隐式当前上下文"。
依赖(Dependencies):模型不能编造的参数
有些值永远不该由模型来填:从你自己的记录里查出的价格、只有真人能给出的确认。依赖正是由你自己的函数填充的参数——你标注参数、指明函数,SDK 会在工具运行前调用它:
def check_stock(title: str) -> Stock: ... # 参数声明处 stock: Annotated[Stock, Resolve(check_stock)]check_stock是一个resolver(解析器):SDK 在reserve_book之前运行它的普通函数,返回值成为stock参数。resolver 可以声明自己的依赖(Resolve套Resolve),SDK 按依赖图顺序执行,且每次调用每个 resolver 最多运行一次——多个消费者共享一次库存查询。依赖图在注册时(而非调用时)被分析:无法归类的参数、resolver 之间的循环依赖都会在启动时抛出InvalidSignature,服务器在客户端连接之前就失败并指明违规参数。实现位于 src/mcp/server/mcpserver/resolve.py(Resolve类)与 src/mcp/server/mcpserver/exceptions.py(InvalidSignature)。
与Context一样,resolver 填充的参数对模型不可见:reserve_book的输入 schema 只有title一个属性。模型无法供应的参数,就是模型无法搞错的参数。resolver 还可以返回Elicit(message, Model)在必要时向用户提问(见后文),或返回Sample(...)、ListRoots()转而向客户端请求(见采样与 roots 一节)。完整的依赖机制(含一次调用去重、依赖图的InvalidSignature校验、resolver 中Context的使用)见 docs/handlers/dependencies.md。
生命周期(Lifespan):一次构建、全程共享的状态
真实服务器大多持有贯穿整个生命周期的对象:数据库连接池、HTTP 客户端、加载好的模型。你不想每次调用都重建它,也希望它能被干净地关闭——这就是 lifespan 的用途。
生命周期是一个@asynccontextmanager,接收服务器并yield一个对象(示例见 docs_src/lifespan/tutorial001.py)。yield之前的代码是启动(startup),finally中的代码是关闭(shutdown);整个接线只有一行:MCPServer("Bookshop", lifespan=app_lifespan)。处理器通过ctx.request_context.lifespan_context取用 yield 出的对象。
三个要点:
- 它真的只运行一次:服务器启动时进入(第一个请求之前),停止时退出,期间所有请求共享同一个状态对象。
- 类型化访问:在工具中写
ctx: Context[AppContext],类型检查器就能确认ctx.request_context.lifespan_context是AppContext,字段自动补全、拼写错误提前报错。但这个写法仅限工具:放在@mcp.resource()或@mcp.prompt()上会导致每次调用失败(报错Context is not available outside of a request)——资源与提示处理器请写裸的ctx: Context。 - 永远存在一个 lifespan:不传时 SDK 默认 yield 一个空
dict,所以lifespan_context是{}而非None。
验证生命周期时序的方式(见 docs_src/lifespan/tutorial002.py):给Database一个connected标志,服务器启动前为False,运行中调用工具返回"connected",停止后finally执行又回到False——工作确实发生在yield周围,而不是导入时或每次请求时。详见 docs/handlers/lifespan.md。
处理器运行中能做什么:六类向客户端反馈的动作
提问用户:Elicitation 与承载它的多轮往返
一个进行到一半的工具缺一个答案,不必失败。Elicitation(诱导)让它在工具调用中途向用户发问,答案回到同一个函数调用里。有两种模式:
- 表单模式(form mode):需要某个值(确认、日期、数量),你描述字段,客户端渲染表单;
- URL 模式(url mode):需要用户去别处(OAuth 授权页、支付页),用户在那边做的事不经过协议。
提问有两种方式。首选是resolver:把问题挂在参数上,SDK 在任何连接、任何协议时代下替你发问。直接方式await ctx.elicit(...)是服务器到客户端的请求,该通道只对 legacy 连接(2025-11-25 及更早)的客户端存在。在 2026-07-28 连接上,服务器返回问题而非推送问题,客户端下一次尝试携带答案——这正是 docs/handlers/multi-round-trip.md 讲述的机制(详见下文)。resolver 形式两种时代都可用,你的 resolver 代码无需区分。
表单模式中,ctx.elicit(message, schema=Model)接受一条消息和一个 Pydantic 模型(示例见 docs_src/elicitation/tutorial001.py)。客户端收到消息和由模型生成的 JSON Schema(字段Field(description=...)即表单标签,default预填输入并使字段可选)。答案有且仅有三种:"accept"(提交了表单,result.data是已验证的模型实例)、"decline"(用户拒绝)、"cancel"(用户未选择就关闭)。拒绝不是错误——由工具决定拒绝意味着什么,并正常答复模型。
一个重要的约束:elicitation schema 不如工具的输入 schema 表达力强——只支持扁平的原始字段(str、int、float、bool或字符串Literal),模型里嵌模型会在发送前直接抛错(TypeError: Elicitation schema field ... is not a valid PrimitiveSchemaDefinition)。因为你是在打断一个正在做任务的人:答案如果需要嵌套,它本应是工具的参数。
URL 模式用ctx.elicit_url(message, url, elicitation_id)(示例见 docs_src/elicitation/tutorial002.py):凭证、卡号、OAuth 同意等必须经过模型或客户端的事情,交给用户在浏览器侧带外完成;"accept"只表示用户同意打开 URL,不代表另一端流程已完成。当服务器从 webhook 或轮询得知带外流程结束时,调用ctx.session.send_elicit_complete(elicitation_id)发送notifications/elicitation/complete,客户端才能停止显示"等待支付…"。
客户端一侧通过给Client(...)传入一个elicitation_callback来应答(示例见 docs_src/elicitation/tutorial003.py):一个回调同时处理两种模式,params是ElicitRequestFormParams与ElicitRequestURLParams的联合类型,用isinstance分支。传入回调同时也是能力声明——服务器据此知道这个客户端可以被提问。注意 legacy 连接才需要这个 server→client 通道,所以示例客户端传入mode="legacy"。若客户端没有注册回调就调用需要提问的工具,整个调用会以协议错误失败:Elicitation not supported。
多轮往返(Multi-round-trip):2026-07-28 时代的"返回而非回调"
有时工具一个来回完不成:它需要只有用户有的东西。2026-07-28 之前的做法是服务器回拨——在处理原请求中途向客户端开一个新请求(elicitation、sampling)。2026-07-28 规范废除了这条反向通道,改为服务器返回:
服务器用InputRequiredResult应答tools/call而非CallToolResult,两个字段起作用:
input_requests:服务器还缺什么,一个按服务器自选键名组织的 dict,每个值是一个ElicitRequest、CreateMessageRequest或ListRootsRequest;request_state:不透明令牌,客户端在重试时原样回显,只有你的服务器能读它。
客户端满足每个请求后,再次调用同一个工具,在input_responses中携带答案、在request_state中携带令牌;服务器拿到缺的东西,返回正常的CallToolResult。协议的每一段都是客户端到服务器的普通请求,没有任何反向流量。
在@mcp.tool()上你很少手工构建它:声明一个会提问的依赖(Elicit)、采样客户端 LLM(Sample)或列出其 roots(ListRoots),SDK 就替你返回InputRequiredResult。两种形式不能混用:一个调用只有一条input_responses/request_state通道,所以使用Resolve(...)参数的工具体内不能再返回InputRequiredResult(注册时报InvalidSignature)。手工形式是底层Server:其on_call_tool处理器返回类型为CallToolResult | InputRequiredResult,返回后者就是完整的服务端 API(示例见 docs_src/mrtr/tutorial001.py)。
tools/call并不特殊:2026-07-28 下服务器同样可以用InputRequiredResult应答prompts/get和resources/read。@mcp.prompt()函数或@mcp.resource()模板函数返回InputRequiredResult本身,并在重试时从ctx.input_responses读取答案。静态@mcp.resource()函数不参与——它们不接收Context,无法读取重试。在 2026-07-28 连接上,URL 模式的 elicitation 正是乘坐这个机制:input_requests中的条目是携带ElicitRequestURLParams的ElicitRequest,用户完成带外流程后客户端重试即可。
客户端侧由Client替你运行循环:注册服务器可能用到的回调(elicitation_callback、sampling_callback、list_roots_callback)然后调用工具即可。收到InputRequiredResult时,Client把input_requests的每条分发给匹配回调,用答案和回显的request_state重试,直到拿到CallToolResult;call_tool最终返回普通的CallToolResult,中间轮次对调用方不可见。get_prompt与read_resource驱动同一个循环。循环有界:Client(..., input_required_max_rounds=10)是默认上限;如果某一轮只携带request_state而无input_requests(服务器在说"还没好"),Client会短暂休眠(50ms 起、翻倍至 250ms 封顶)再重试,避免忙轮询。
分布式客户端可自行驱动循环:当渲染问题的进程不是调用call_tool的进程时,使用底层会话client.session.call_tool(..., allow_input_required=True)拿到联合返回类型,自行while isinstance(result, InputRequiredResult)循环——request_state是你可以跨进程持久化的令牌,input_responses是对端随它送回的东西。每条input_requests都要在input_responses的同一个键下放一条InputResponse;工具名与arguments每一轮都相同,重试是原调用的再次执行,而非新方法。
保护requestState是部署前必须知道的最后一件事。客户端在轮次之间持有它(跨进程写下正是上一节所提倡的),所以回传的它是客户端提供的输入:可能被篡改、过期,或从别的调用中取出。MCPServer默认保护它:每个服务器在进程启动时生成密钥,对发出的requestState加密密封,并验证每一次回显——resolver 状态与手工构建状态一视同仁。你配置什么都不需要,写明文、读明文,线上只携带不透明的加密令牌。默认密钥随进程生灭,因此多进程部署必须显式配置:
from mcp.server.mcpserver import MCPServer, RequestStateSecurity # 多实例或重启后仍需验证:一个或多个共享密钥(每个 >= 32 字节)。 mcp = MCPServer("fleet", request_state_security=RequestStateSecurity(keys=[key]))密封不仅保证完整性,每个令牌还绑定到:时间窗口(RequestStateSecurity(ttl=...)默认 600 秒,限制的是单轮思考时间而非整个流程)、已认证主体(请求携带经 SDK 验证的 OAuth 访问令牌时绑定其 client、issuer、subject)、发起请求(方法、工具/提示名或资源 URI 及参数摘要)、被问的确切问题(每个 resolver 答案都钉在客户端所看到的问题渲染上)。验证失败的统一答复是冻结的错误:
{"code": -32602, "message": "Invalid or expired requestState"}无论篡改、过期还是密钥不符都是这一个答复,线上不暴露任何校验细节。密钥轮换分三阶段:[OLD, NEW](所有人学会验证 NEW)→[NEW, OLD](NEW 铸造、在途 OLD 仍可验证)→[NEW](一个 ttl 后淘汰 OLD),切不可先提升铸造者。也可以自带加密:RequestStateSecurity(codec=...)接受任何实现seal(bytes) -> str与unseal(str) -> bytes且对未铸造令牌抛InvalidRequestState的对象——典型形态是 KMS 信封加密(示例见 docs_src/mrtr/tutorial005.py)。TTL、主体绑定、请求绑定都不是 codec 的职责:SDK 在seal之前把它们盖进载荷、在unseal之后重新验证。
InputRequiredResult只存在于协议版本2026-07-28。Client默认mode="auto"在任何连接上自动发现;连接后client.protocol_version告诉你得到了什么。在 legacy 会话上返回它,客户端只会得到-32603"Handler returned an invalid result"——同时服务两个时代的服务器必须先检查ctx.protocol_version。完整机制见 docs/handlers/multi-round-trip.md,它取代了服务器发起的采样等推送式反向通道,弃用清单见 docs/deprecated.md。
采样与 Roots:借用客户端的模型与目录(已弃用)
处理器还可以向已连接的客户端索要两样东西:客户端自己模型的补全(sampling)与客户端的工作区目录(roots)。两者在所有协议版本上仍可工作,但已被 2026-07-28 规范弃用(SEP-2577):至少十二个月内保持完整功能,但新实现不应在此基础上构建。建议的迁移方向:直接集成你的 LLM 提供商 API 取代 sampling;用工具参数、资源 URI 或服务器配置传递目录取代 roots。
- 采样:resolver 返回
Sample(messages, max_tokens=...)(镜像sampling/createMessage参数,示例见 docs_src/sampling_and_roots/tutorial001.py),工具通过依赖机制收到客户端的CreateMessageResult(传入tools或tool_choice时为CreateMessageResultWithTools)。客户端必须声明过sampling能力(传tools/tool_choice时需要sampling.tools),否则调用以-32021协议错误失败。include_context除"none"之外的值本身也已被弃用(SEP-2596),不要动它。 - Roots:resolver 返回
ListRoots()(示例见 docs_src/sampling_and_roots/tutorial002.py),注入的ListRootsResult携带Root列表(file://URI 与可选显示名)。roots 是信息性指引,不是访问控制机制。未声明roots能力同样以-32021失败。
两条路径的传输时代选择与 elicitation 一致:2026-07-28 下在多轮往返流程内送达,2025-11-25 下是独立的服务器→客户端请求。注意多轮往返的规则:请求必须跨重试轮次渲染一致,所以只能用工具参数和其他稳定数据构建。客户端用已有的sampling_callback与list_roots_callback应答(见 docs/client/callbacks.md)。ctx.session.create_message(...)与ctx.session.list_roots()仍然存在以驱动旧代码,但只在存在反向通道的 2025 时代连接上工作,且调用会触发弃用警告。详见 docs/handlers/sampling-and-roots.md。
进度(Progress):让三十秒的工具看起来还活着
一个要跑三十秒却三十秒一言不发的工具看起来像坏了。进度通知解决这个问题:工具上报进度到哪了,客户端决定画成进度条、旋转指示器还是日志行。
服务端只需接收一个Context参数并调用report_progress(示例见 docs_src/progress/tutorial001.py):
await ctx.report_progress(progress, total=None, message=None)三个参数的含义由你决定:progress表示进行到哪(规范要求每次上报递增,不要重复或回退)、total表示总量(可选,只有你知道时才给)、message是关于当前这一步的一行人类可读文本(可选)。字节、行数、页数——选用户能识别的单位,并且只承诺你能兑现的total。
客户端每次调用通过progress_callback=选择加入(call_tool的参数,不是Client构造参数——不同调用想要不同的回调,一个驱动下载条,下一个驱动日志行)。回调是async (progress, total, message) -> None,在工具仍在运行时逐个触发(每次通知独立送达,与响应并列;因此慢回调可能在call_tool返回后仍在运行,只有进程内测试连接是内联执行的):
async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") result = await client.call_tool( "import_catalog", {"urls": [...]}, progress_callback=show, )关键的宽容设计:没有回调时report_progress是 no-op——不报错、不警告、结果相同。所以你在服务端无条件上报,永远不必担心有没有人在听。当不知道总量时(正在排空一个 feed、游标扫描、无长度头的下载),省略total,回调收到None,客户端仍可显示"已导入 3 个…"之类的活动信息,但无法显示百分比——不要为了好看的进度条编造一个 total。完整细节见 docs/handlers/progress.md。
日志(Logging):用标准库,写给运维者
从工具里记录日志,和从任何其他 Python 函数里记录一样:用标准库。MCP 协议曾有一个协议级 logging 能力——服务器通过Context上的方法把日志消息作为通知推给客户端——但 2026-07-28 修订版弃用了该能力且未提供替代品,因此本指南不教它(完整弃用清单见 docs/deprecated.md)。替代方案就是你在每个 Python 程序里做的:标准库。
import logging logger = logging.getLogger(__name__) @mcp.tool() async def search_books(query: str, ctx: Context) -> ...: logger.info("Searching for %r", query) ...logging.getLogger(__name__)给你一个以模块命名的 logger,在顶部创建一次;工具内调用logger.info(...)与任何函数无异——无需注入、无需await、没有任何 MCP 特有的东西。日志行永远不出现在工具结果里(result.content与result.structured_content都没有它)——日志是写给你(服务器运维者)的,模型永远看不到;如果模型应该读到什么,请return它。
去向问题对stdio服务器尤其重要:宿主编译你的服务器为子进程,从stdout读取 MCP 消息——标准错误是你的地盘。标准库默认就把日志输出到sys.stderr,协议流保持干净。不要在 stdio 服务器里用print():print写 stdout,而 stdout 属于协议。虽然服务期间 SDK 会把真正 flush 的游离 stdout 转向 stderr 以免污染线路,但块缓冲进程中的print()通常滞留在sys.stdout缓冲区直到解释器退出时才排空,直落协议流;即使被转向,那行文本也是无级别、无 logger 名、无法过滤的裸文本。logger.debug("got here")同样一行工作量,却去了正确的地方。
你也不必自己调用logging.basicConfig():构造MCPServer时已经调用过,带一个指向标准错误的 handler,级别就是你传入的log_level=。因此MCPServer("Bookshop", log_level="DEBUG")一条就够了;默认是"INFO"。logging.basicConfig()从不替换已存在的 handler——如果你在创建服务器之前自己配置了日志,你的配置胜出。处理器抛异常时 SDK 也会替你记录(见 docs/servers/handling-errors.md)。如果你的真正需求是追踪(每个请求、耗时、是否失败),你要的不是日志行而是 span:SDK 已经用 OpenTelemetry 内置追踪每条消息(见 docs/run/opentelemetry.md)。详见 docs/handlers/logging.md。
订阅(Subscriptions):告诉已订阅客户端"有变化"
服务器的目录不是固定的:工具在运行时出现,资源 URI 背后的内容会变。订阅是客户端获知这些变化的方式:客户端发送一个subscriptions/listen请求,而该请求的响应本身就是流——它保持打开,承载客户端要求的变更通知。
服务端一侧只是一行:发布变更(示例见 docs_src/subscriptions/tutorial001.py)。
await ctx.notify_resource_updated("board://sprint") # 只到达订阅该 URI 的流 await ctx.notify_tools_changed() # 到达所有请求了工具列表变更的流兄弟方法是notify_prompts_changed()与notify_resources_changed()。没有订阅者就没有工作:向空闲服务器发布是 no-op,所以你从不需要检查有没有人在听——你只陈述发生了什么。MCPServer替你服务subscriptions/listen:线上的确认首帧、按流过滤、每帧上的订阅 id 都是 SDK 的职责。
线上形态:每个帧都携带 listen 请求的 JSON-RPC id 于_meta之下,那个 id 就是订阅 id——由客户端铸造(PythonClient用"listen-1"这样的字符串,其他客户端可能用整数)。变更通知不携带内容(更新的不是看板本身,只是"看板变了"),因此订阅是线索(cue)而非载荷,两端都要重新拉取。
过滤是契约:只请求了工具列表变更和一个资源 URI 的流,只收到这两种通知,其余保持沉默。MCPServer将资源 URI 按精确字符串匹配,订阅board://sprint的流听不到board://sprint/tasks/1的变化。还要明确流不是什么:它不是重放日志(断开的流就没了,无人连接时发布的事件不会排队,客户端重新 listen 并重新拉取);它也不是 2025 的路径(调用过resources/subscribe的客户端由ctx.session.send_resource_updated(uri)服务,notify_*方法只到达subscriptions/listen流)。
谁可以看默认是开放的——任何调用者可以监视你发布的任何 URI,不会咨询你的读取处理器。默认宽但窄:看客永远学不到内容,也无法探测存在性(未知 URI 也被接受,只是永不触发)。但从多租户服务器发布按用户区分的 URI 前,请务必加一道中间件闸门(示例见 docs_src/subscriptions/tutorial006.py):中间件在 SDK 确认subscriptions/listen之前看到请求,ctx.params是原始请求,校验为SubscriptionsListenRequestParams后读取客户端要求的过滤条件;拒绝就是在call_next(ctx)之前抛MCPError,客户端收到错误且没有流。一个can_access(user, uri)同时回答两个问题:资源处理器在resources/read上问它,中间件在subscriptions/listen上问它。决策在流的整个生命周期内有效,没有逐事件复查——如果调用者的访问权限可能中途失效(如令牌过期),到期时就结束该调用者的连接。中间件完整契约见 docs/advanced/middleware.md。
客户端一侧:进入client.listen(...)就发送请求并等待确认,因此块开始时流已经活跃,每个类型化事件都是重新拉取的提示(完整客户端故事见 docs/client/subscriptions.md)。
跨进程扩展:发布经由一个SubscriptionBus从处理器到达打开的流。默认是内存实现——单进程内所有流。负载均衡后的多副本部署中,客户端的流被钉在某一个副本上,另一个副本上的发布必须能到达它。这个接缝由你实现:两个方法(publish与subscribe)对接你的 pub/sub 后端:
from mcp.server.mcpserver import MCPServer from mcp.server.subscriptions import ServerEvent # SubscriptionBus 是 Protocol,无基类 class RedisSubscriptionBus: def __init__(self, redis): ... # 持有 Redis 客户端与本地 listener 注册表 async def publish(self, event: ServerEvent) -> None: ... # 发布到每个副本 def subscribe(self, listener) -> Callable[[], None]: ... # 返回注销函数 mcp = MCPServer("Sprint Board", subscriptions=RedisSubscriptionBus(redis))总线携带类型化的ServerEvent值(四个小型 dataclass),从不携带 JSON-RPC:盖章、过滤、流生命周期都留在 SDK 内,所以总线实现不可能破坏协议,只能把事件在进程间搬运。要在请求之外发布(lifespan 任务、webhook),自行构造总线并持有引用即可;MCPServer在你不传时内部构建一个且不暴露它。底层Server上无预接线,同样部件三行组装:自持总线直接await bus.publish(ResourceUpdated(uri=...))、ListenHandler(bus)是MCPServer注册的同一个处理器、on_subscriptions_listen=是普通处理器槽位;ListenHandler.close()优雅结束每条打开的流(每流收到 listen 请求的结果作为最后一帧)。详见 docs/handlers/subscriptions.md。
源码层面的能力印证
上述能力在仓库源码中均有对应实现,可作为深入阅读的入口:
Context注入机制:定义于 src/mcp/server/mcpserver/context.py,包含report_progress(第 113 行)、read_resource(第 151 行)、elicit/elicit_url(第 216/222 行)、request_id(第 292 行)等核心方法;request_context属性按需从ServerRequestContext解析,未注入时给出明确报错。- 依赖解析器:
Resolve、Elicit、Sample、ListRoots标记类定义于 src/mcp/server/mcpserver/resolve.py;依赖图在注册时分析,无法分类的参数或循环依赖抛InvalidSignature(同一模块第 235、301 行)。 - 订阅总线:
SubscriptionBus协议与InMemorySubscriptionBus位于 src/mcp/server/subscriptions.py,ListenHandler亦在其附近。 - 教学示例源码:本文引用的全部可运行示例位于 docs_src/ 下(如 docs_src/context/tutorial001.py、docs_src/elicitation/tutorial001.py、docs_src/mrtr/tutorial001.py、docs_src/subscriptions/tutorial001.py),对应测试见 tests/docs_src/(
test_context.py、test_elicitation.py、test_mrtr.py、test_subscriptions.py等)。
从哪开始
处理器能力的两大阵营有一条清晰的规则:来自模型之外的输入走注入(Context、依赖、lifespan),去往客户端的动作走Context提供的通道(进度、elicitation、订阅);而在 2026-07-28 协议时代,向用户/客户端要东西的统一载体是返回InputRequiredResult的多轮往返机制,resolver 则是它在高层 API 上的声明式形态。
如果你还没有注册过任何处理器,请先以 docs/servers/tools.md 为起点注册第一个工具;本系列每篇页面都假设你已经有一个可运行的处理器。之后按需深入:docs/handlers/context.md(注入基础)→ docs/handlers/dependencies.md(模型不可见的参数)→ docs/handlers/lifespan.md(共享状态)→ 再按业务需要选择 docs/handlers/elicitation.md、docs/handlers/multi-round-trip.md、docs/handlers/progress.md、docs/handlers/logging.md、docs/handlers/subscriptions.md 中的对应篇章。
- 人工智能
- MCP 服务
- MCP Clients
【免费下载链接】python-sdk
The official Python SDK for Model Context Protocol servers and clients
相关推荐
深入 MCP Python SDK 的多轮往返请求:InputRequiredResult 与 requestState 安全机制
深入 MCP Python SDK 的多轮往返请求:InputRequiredResult 与 requestState 安全机制 多轮往返(multi rou
人工智能MCP 服务MCP ClientsMCP Python SDK 多轮往返请求(Multi-round-trip)实战:从 `InputRequiredResult` 到 `requestState` 安全机制
MCP Python SDK 多轮往返请求(Multi round trip)实战:从 InputRequiredResult 到 requestState 安
人工智能MCP 服务MCP ClientsPython SDK 多轮往返(Multi-Round-Trip)请求实战:从 InputRequiredResult 到 requestState 保护
Python SDK 多轮往返(Multi Round Trip)请求实战:从 InputRequiredResult 到 requestState 保护 导读
人工智能MCP 服务MCP Clients
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考