modelcontextprotocol / modelcontextprotocol/python-sdk

[v2] MCPServer reports empty experimental capabilities as {} via initialize but None via server/discover

Open
#3,254 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

P2 spec-2026-07-28 v2
Dominant language
Python
Stars
24.3k
Forks
4k
Avg merge
1d 1h
Merged PRs (30d)
31

Description

Description

With mcp==2.0.0, the same unconfigured server exposes empty experimental capabilities differently through its two public discovery paths:

  • initialize: capabilities.experimental == {} and the field is present on the wire
  • server/discover: capabilities.experimental is None; the field is omitted on the wire, while the parsed SDK model materializes None

In a sanitized capture this is visible at both $.handshake.capabilities.experimental and $.handshake.result.capabilities.experimental as {} to null. The null is 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 as is not None also change meaning.

Minimal reproduction
from mcp.server.lowlevel import Server

server = Server("repro", version="0.0.0")
legacy = server.create_initialization_options().capabilities
modern = server.get_capabilities(protocol_version="2026-07-28")

for name, capabilities in (("legacy", legacy), ("modern", modern)):
    wire = capabilities.model_dump(by_alias=True, mode="json", exclude_none=True)
    print(name, capabilities.experimental, "experimental" in wire)

Observed with Python 3.14.3, mcp==2.0.0, mcp-types==2.0.0, and Pydantic 2.13.4:

legacy {} True
modern None False
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:

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
  • Python: 3.14.3
  • MCP Python SDK: 2.0.0
  • mcp-types: 2.0.0
  • Pydantic: 2.13.4
  • OS: Windows

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Compare the initialize and get_capabilities paths in src/mcp/server/lowlevel/server.py, then trace the type in src/mcp-types/mcp_types/_types.py and response handling in src/mcp/server/runner.py. Review the client exposure in src/mcp/client/session.py and the optional field in schema/2026-07-28.json. Done means the two discovery representations converge or the intentional difference is documented with migration guidance.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.