MCP Python SDK Extension Method Collisions: Fail Before the Server Starts
MCP Python SDK Extension Method Collisions: Fail Before the Server Starts
MCP Python SDK 扩展方法冲突:在服务器启动前拦截失败
MCP Python SDK extension method collisions are configuration defects, not runtime edge cases. If two extensions claim the same method—or one claims a core MCP method—the server should reject that setup before it accepts a request. I prefer making method ownership an executable contract so registration order can never decide which handler wins. MCP Python SDK 的扩展方法冲突属于配置缺陷,而非运行时边缘情况。如果两个扩展声明了同一个方法,或者某个扩展声明了核心 MCP 方法,服务器应当在接受请求之前拒绝该配置。我倾向于将方法所有权设定为一种可执行的契约,从而确保注册顺序永远不会决定哪个处理程序生效。
The official MCP Python SDK extension documentation defines three useful safeguards: core methods cannot be registered as extension methods, duplicate extension methods are rejected during registration, and every binding must declare at least one supported protocol version. 官方的 MCP Python SDK 扩展文档定义了三个有用的保障措施:核心方法不能被注册为扩展方法;重复的扩展方法在注册期间会被拒绝;每个绑定必须至少声明一个支持的协议版本。
Why collisions should fail during startup
为什么冲突应该在启动阶段失败
An extension adds vendor-specific behavior to the same dispatch table used by the rest of the server. That makes method names part of the server’s public contract. Consider two independently configured extensions that both expose: com.example/catalog.search. A last-write-wins registry would make the active handler depend on extension order. Reordering configuration could silently change request behavior without changing the client call. The safer contract is one owner per method name.
扩展程序会将供应商特定的行为添加到服务器其余部分所使用的同一个分发表中。这使得方法名称成为了服务器公共契约的一部分。考虑两个独立配置的扩展,它们都暴露了 com.example/catalog.search。如果采用“最后写入者胜出”的注册机制,活跃的处理程序将取决于扩展的加载顺序。重新排列配置可能会在不更改客户端调用的情况下悄悄改变请求行为。更安全的契约是每个方法名称只能有一个所有者。
In the sample, a valid extension starts normally, while a second extension claiming the same method causes MCPServer construction to raise ValueError. Core protocol methods have an even stronger boundary. A vendor extension must not replace methods such as tools/list. The SDK’s MCP extension core method guard rejects that binding when MethodBinding is constructed.
在示例中,有效的扩展可以正常启动,而第二个声明相同方法的扩展会导致 MCPServer 在构建时抛出 ValueError。核心协议方法具有更强的边界。供应商扩展不得替换诸如 tools/list 之类的方法。当构建 MethodBinding 时,SDK 的 MCP 扩展核心方法防护机制会拒绝此类绑定。
The sample targets protocol version 2026-07-28, announced in the project’s final MCP release post, and pins the stable mcp==2.1.1 package so the checks are reproducible.
该示例针对的是项目最终 MCP 发布公告中宣布的 2026-07-28 协议版本,并锁定了稳定的 mcp==2.1.1 包,以确保检查的可复现性。
Build a version-pinned MethodBinding
构建版本锁定的 MethodBinding
I start with a namespaced method and an explicit protocol-version set: 我从一个命名空间方法和一个显式的协议版本集合开始:
PROTOCOL_VERSION = "2026-07-28"
EXTENSION_ID = "com.example/catalog"
METHOD = "com.example/catalog.search"
def search_binding(method: str = METHOD) -> MethodBinding:
return MethodBinding(
method,
SearchParams,
search,
protocol_versions=frozenset({PROTOCOL_VERSION}),
)
The reverse-domain prefix keeps the vendor method separate from core MCP names. More importantly, protocol_versions states exactly where the binding is reachable. That protocol version validation prevents a subtle configuration mistake. An empty set describes a method that cannot be used under any protocol version, so the SDK rejects it immediately:
反向域名后缀将供应商方法与核心 MCP 名称分离开来。更重要的是,protocol_versions 明确指出了绑定在何处可达。这种协议版本验证防止了一个微妙的配置错误。空集合描述了一个在任何协议版本下都无法使用的方法,因此 SDK 会立即拒绝它:
def build_unreachable_binding() -> MethodBinding:
return MethodBinding(
METHOD,
SearchParams,
search,
protocol_versions=frozenset(),
)
The valid extension returns one binding: 有效的扩展返回一个绑定:
class CatalogSearch(Extension):
identifier = EXTENSION_ID
def methods(self) -> Sequence[MethodBinding]:
return [search_binding()]
A second extension deliberately returns the same method name: 第二个扩展故意返回相同的方法名称:
class ShadowSearch(Extension):
identifier = "com.example/catalog-shadow"
def methods(self) -> Sequence[MethodBinding]:
return [search_binding()]
Neither class is inherently invalid in isolation. The collision appears when both are registered with one server: 这两个类在孤立情况下本身都没有问题。当两者同时注册到同一个服务器时,冲突就会出现:
MCPServer(
"extension-contract",
extensions=[CatalogSearch(), ShadowSearch()],
)
This is the MethodBinding duplicate-method boundary I want to test: the server registry sees two owners and refuses to start. Checking this boundary during construction keeps the failure close to the configuration that caused it. A deployment never reaches the point where the first unlucky request discovers an ambiguous handler. It also makes the regression test independent of extension ordering: swapping the two classes cannot turn the failure into success. In a larger server, I would keep these ownership tests beside the composition root where optional packages are assembled.
这就是我想要测试的 MethodBinding 重复方法边界:服务器注册表发现了两个所有者并拒绝启动。在构建期间检查此边界可以将失败限制在导致问题的配置附近。部署永远不会到达第一个倒霉的请求发现歧义处理程序的阶段。这也使得回归测试独立于扩展顺序:交换这两个类无法将失败转变为成功。在大型服务器中,我会将这些所有权测试放在组合根目录(composition root)旁边,即组装可选包的地方。
Verify MCP Python SDK extension method collisions offline
离线验证 MCP Python SDK 扩展方法冲突
The runnable sample checks one valid path and three invalid configurations. Its valid case uses the SDK’s in-memory client, so it does not open a port or require an external MCP host: 可运行的示例检查了一个有效路径和三个无效配置。其有效案例使用了 SDK 的内存客户端,因此它不需要打开端口或依赖外部 MCP 主机:
server = build_valid_server()
async with Client(
server,
extensions=[advertise(EXTENSION_ID)],
) as client:
request = SearchRequest(params=SearchParams(query="mcp"))
result = await client.session.send_request(
request,
SearchResult,
)
The handler returns deterministic values: ["mcp-0", "mcp-1"]. The positive request is as important as the rejection cases. It proves the namespaced method remains callable when it has one owner, the client advertises the extension identifier, and the typed result survives the same registry being guarded. Without that control case, a test could pass simply because every extension path was broken.
处理程序返回确定性的值:["mcp-0", "mcp-1"]。这个正面请求案例与拒绝案例同样重要。它证明了当命名空间方法只有一个所有者时,它是可调用的;客户端能够通告扩展标识符;并且类型化的结果在受到保护的注册表中依然有效。如果没有这个对照案例,测试可能会因为所有扩展路径都已损坏而“通过”。
The remaining checks construct a duplicate server, attempt to bind tools/list, and create a binding with an empty version set. The verifier catches ValueError only to assert the boundary; production startup should let those errors stop the process.
其余的检查包括构建一个重复的服务器、尝试绑定 tools/list,以及创建一个带有空版本集合的绑定。验证器捕获 ValueError 只是为了断言边界;在生产环境启动时,应该让这些错误直接终止进程。
Run the full validation with: 使用以下命令运行完整验证:
uv sync --all-groups
uv lock --check
uv run ruff format --check .
uv run ruff check .
uv run mypy extension_contract.py verify.py test_extension_contract.py
uv run python -m compileall -q extension_contract.py verify.py test_extension_contract.py
uv run python -m unittest -v
uv run python verify.py
uv run pip-audit
The deterministic verifier reports: 确定性验证器报告:
[PASS] unique vendor method starts normally
[PASS] typed request keeps the vendor method
[PASS] duplicate method fails during server construction
[PASS] core MCP method cannot be claimed
[PASS] empty protocol version set is rejected
5/5 checks passed
The merged changes and validation record are also available in the sample pull request. 合并后的更改和验证记录也可在示例 Pull Request 中查看。
Limitations and when not to use this pattern
局限性及何时不应使用此模式
This sample verifies construction and in-memory request dispatch. It does not test stdio, Streamable HTTP, authentication, extension result claims, or notification bindings. It also pins exact exception-message fragments for SDK 2.1.1. If I were supporting several SDK releases, I would make the exception type and offending method the durable assertions, then keep message checks narrow enough to… 此示例验证了构建和内存请求分发。它没有测试 stdio、流式 HTTP、身份验证、扩展结果声明或通知绑定。它还针对 SDK 2.1.1 锁定了精确的异常消息片段。如果我要支持多个 SDK 版本,我会将异常类型和违规方法作为持久断言,然后将消息检查保持在足够窄的范围内以……