{"slug": "mcp-python-sdk-extension-method-collisions-fail-before-the-server-starts", "title": "MCP Python SDK Extension Method Collisions: Fail Before the Server Starts", "summary": "A developer demonstrated that the MCP Python SDK rejects extension method collisions at server construction time rather than at request time, using the stable mcp==2.1.1 package and protocol version 2026-07-28. The sample shows that registering two extensions claiming the same method name causes MCPServer construction to raise ValueError, while a vendor extension attempting to bind a core method such as tools/list is rejected when MethodBinding is constructed. The approach makes method ownership an executable contract so registration order cannot silently determine which handler wins.", "body_md": "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.\n\nThe official [MCP Python SDK extension documentation](https://py.sdk.modelcontextprotocol.io/advanced/extensions/) 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.\n\nAn 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.\n\nConsider two independently configured extensions that both expose:\n\n```\ncom.example/catalog.search\n```\n\nA 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.\n\nThe safer contract is one owner per method name. In the sample, a valid extension starts normally, while a second extension claiming the same method causes `MCPServer` construction to raise `ValueError`.\n\nCore 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.\n\nThe sample targets protocol version `2026-07-28`, announced in the project's [final MCP release post](https://blog.modelcontextprotocol.io/posts/2026-07-28/), and pins the stable [`mcp==2.1.1` package](https://pypi.org/project/mcp/2.1.1/) so the checks are reproducible.\n\n`MethodBinding`\nI start with a namespaced method and an explicit protocol-version set:\n\n```\nPROTOCOL_VERSION = \"2026-07-28\"\nEXTENSION_ID = \"com.example/catalog\"\nMETHOD = \"com.example/catalog.search\"\n\ndef search_binding(method: str = METHOD) -> MethodBinding:\n    return MethodBinding(\n        method,\n        SearchParams,\n        search,\n        protocol_versions=frozenset({PROTOCOL_VERSION}),\n    )\n```\n\nThe reverse-domain prefix keeps the vendor method separate from core MCP names. More importantly, `protocol_versions` states exactly where the binding is reachable.\n\nThat 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:\n\n``` php\ndef build_unreachable_binding() -> MethodBinding:\n    return MethodBinding(\n        METHOD,\n        SearchParams,\n        search,\n        protocol_versions=frozenset(),\n    )\n```\n\nThe valid extension returns one binding:\n\n``` python\nclass CatalogSearch(Extension):\n    identifier = EXTENSION_ID\n\n    def methods(self) -> Sequence[MethodBinding]:\n        return [search_binding()]\n```\n\nA second extension deliberately returns the same method name:\n\n```\nclass ShadowSearch(Extension):\n    identifier = \"com.example/catalog-shadow\"\n\n    def methods(self) -> Sequence[MethodBinding]:\n        return [search_binding()]\n```\n\nNeither class is inherently invalid in isolation. The collision appears when both are registered with one server:\n\n```\nMCPServer(\n    \"extension-contract\",\n    extensions=[CatalogSearch(), ShadowSearch()],\n)\n```\n\nThis is the `MethodBinding` duplicate-method boundary I want to test: the server registry sees two owners and refuses to start.\n\nChecking 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.\n\nThe runnable [sample](https://github.com/ssukhpinder/dev-to-code-samples/tree/main/107-mcp-extension-collisions) 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:\n\n```\nserver = build_valid_server()\n\nasync with Client(\n    server,\n    extensions=[advertise(EXTENSION_ID)],\n) as client:\n    request = SearchRequest(params=SearchParams(query=\"mcp\"))\n    result = await client.session.send_request(\n        request,\n        SearchResult,\n    )\n```\n\nThe handler returns deterministic values:\n\n```\n[\"mcp-0\", \"mcp-1\"]\n```\n\nThe 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.\n\nThe 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.\n\nRun the full validation with:\n\n```\nuv sync --all-groups\nuv lock --check\nuv run ruff format --check .\nuv run ruff check .\nuv run mypy extension_contract.py verify.py test_extension_contract.py\nuv run python -m compileall -q extension_contract.py verify.py test_extension_contract.py\nuv run python -m unittest -v\nuv run python verify.py\nuv run pip-audit\n```\n\nThe deterministic verifier reports:\n\n```\n[PASS] unique vendor method starts normally\n[PASS] typed request keeps the vendor method\n[PASS] duplicate method fails during server construction\n[PASS] core MCP method cannot be claimed\n[PASS] empty protocol version set is rejected\n5/5 checks passed\n```\n\nThe merged changes and validation record are also available in the [sample pull request](https://github.com/ssukhpinder/dev-to-code-samples/pull/97).\n\nThis sample verifies construction and in-memory request dispatch. It does not test stdio, Streamable HTTP, authentication, extension result claims, or notification bindings.\n\nIt 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 tolerate wording changes.\n\nThese checks are most useful when a server composes extensions from multiple packages or configuration sources. For a small server with no extensions, the extra contract tests may add little value. They also complement integration tests rather than replacing transport and authorization coverage.\n\nHow are you testing extension method ownership before your MCP server accepts traffic?\n\nHappy building!", "url": "https://wpnews.pro/news/mcp-python-sdk-extension-method-collisions-fail-before-the-server-starts", "canonical_source": "https://dev.to/ssukhpinder/mcp-python-sdk-extension-method-collisions-fail-before-the-server-starts-329l", "published_at": "2026-09-10 23:09:06+00:00", "updated_at": "2026-09-10 23:17:26.007449+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-infrastructure"], "entities": ["MCP Python SDK", "Model Context Protocol", "MCPServer", "MethodBinding", "mcp==2.1.1"], "alternates": {"html": "https://wpnews.pro/news/mcp-python-sdk-extension-method-collisions-fail-before-the-server-starts", "markdown": "https://wpnews.pro/news/mcp-python-sdk-extension-method-collisions-fail-before-the-server-starts.md", "text": "https://wpnews.pro/news/mcp-python-sdk-extension-method-collisions-fail-before-the-server-starts.txt", "jsonld": "https://wpnews.pro/news/mcp-python-sdk-extension-method-collisions-fail-before-the-server-starts.jsonld"}}