Repository navigation
[v2] MCPServer reports empty experimental capabilities as {} via initialize but None via server/discover #3254
Description
Activity
- addedv2Affects the v2 line (2.x on main)Affects the v2 line (2.x on main)P1Significant bug affecting many users, highly requested featureSignificant bug affecting many users, highly requested featureP2Moderate issues affecting some users, edge cases, potentially valuable featureModerate issues affecting some users, edge cases, potentially valuable featurespec-2026-07-28Concerns the SDK's implementation of the 2026-07-28 MCP spec revisionConcerns the SDK's implementation of the 2026-07-28 MCP spec revisionand removedP1Significant bug affecting many users, highly requested featureSignificant bug affecting many users, highly requested feature
on Aug 11, 2026 I reproduced this on current
main(57394b0) through the actual default discover handler and runner. Omittedexperimentaland"experimental": {}both validate for the 2025-11-25 and 2026-07-28 schemas and contain zero named experimental capabilities, but the spec does not require one canonical JSON or SDK representation; parsing preservesNoneversus{}.There are two compatibility boundaries: normalizing
get_capabilities()globally to{}(the approach in #3255) also changes the observable default of that public method, while passing{}only from_handle_discoveraligns the two default wire discovery paths without changing direct callers. The narrower handler-only change appears safer under the v2 compatibility contract.A regression test should exercise the runner's full discover result, mark this as SDK-defined rather than a spec MUST, and stay scoped to the unconfigured case. Non-empty experimental maps passed to initialization are not stored on
Server, so making those automatically appear in discover would be a broader API design. Would maintainers prefer the handler-only boundary, or the broader global normalization already in #3255?AI disclosure: I used Codex to inspect the current SDK and specification sources and to run the local reproduction and schema checks.
Fixed in #3614, thanks for the detailed write-up.
A server with no experimental capabilities configured now leaves
experimentalout of itsinitializeresult too, so both paths give youNone.I went with omitting it rather than sending
{}everywhere because that's whatserver/discoverand the other SDKs I checked already do. Socaps.experimental or {}is the safe way to read it on either path.If that doesn't work for your migration gate, please open a new issue.
Description
With
mcp==2.0.0, the same unconfigured server exposes empty experimental capabilities differently through its two public discovery paths:capabilities.experimental == {}and the field is present on the wireserver/discover:capabilities.experimental is None; the field is omitted on the wire, while the parsed SDK model materializesNoneIn a sanitized capture this is visible at both
$.handshake.capabilities.experimentaland$.handshake.result.capabilities.experimentalas{}tonull. Thenullis a diagnostic model dump, not a literal modern wire value.This distinction is client-visible. Code using
.get(...)on the legacy value works but raises on the modern value, while checks such asis not Nonealso change meaning.Minimal reproduction
Observed with Python 3.14.3,
mcp==2.0.0,mcp-types==2.0.0, and Pydantic 2.13.4:Expected behavior
The two supported discovery paths should expose consistent public SDK semantics for an unconfigured experimental capability map, or the intentional difference should be documented with migration guidance.
Source diagnosis
The tagged v2.0.0 source appears to explain the mismatch:
{}:python-sdk/src/mcp/server/lowlevel/server.py
Lines 527 to 548 in 6f69a37
get_capabilitiespreservesNone:python-sdk/src/mcp/server/lowlevel/server.py
Lines 555 to 625 in 6f69a37
python-sdk/src/mcp/server/lowlevel/server.py
Lines 660 to 675 in 6f69a37
experimentaltoNone:python-sdk/src/mcp-types/mcp_types/_types.py
Lines 485 to 489 in 6f69a37
Nonefrom the modern wire response:python-sdk/src/mcp/server/runner.py
Lines 110 to 123 in 6f69a37
python-sdk/src/mcp/client/session.py
Lines 719 to 755 in 6f69a37
python-sdk/src/mcp/client/session.py
Lines 791 to 797 in 6f69a37
python-sdk/schema/2026-07-28.json
Lines 3117 to 3177 in 6f69a37
Downstream impact and revisit condition
A migration gate currently needs a provisional expected delta for this client-visible transition. We will retest the first 2.x release that fixes or documents this behavior and remove or revise that delta when the two representations converge or the intended contract is clarified.
Version