From 4dce83796dc9fa5aa0263c8edd6bbf3d3fd2de1b Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:04:48 +0000 Subject: [PATCH] Omit an unset experimental capability from the initialize result `create_initialization_options()` turned a missing experimental map into `{}`, so a server with nothing configured sent `"experimental": {}` on `initialize` while its `server/discover` result left the field out. Pass the argument through unchanged so both results omit it. An experimental map the caller passes explicitly, including an empty one, is still sent as given. Fixes #3254 --- src/mcp/server/lowlevel/server.py | 2 +- tests/client/test_client.py | 1 - tests/interaction/lowlevel/test_initialize.py | 3 +- tests/server/lowlevel/test_server_discover.py | 32 ++++++++++++++++--- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 8a886dcc24..c276bc4c36 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -545,7 +545,7 @@ def create_initialization_options( description=self.description, capabilities=self.get_capabilities( notification_options or NotificationOptions(), - experimental_capabilities or {}, + experimental_capabilities, extensions if extensions is not None else self.extensions, ), instructions=self.instructions, diff --git a/tests/client/test_client.py b/tests/client/test_client.py index d7278e3a81..ca5c8aec2f 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -114,7 +114,6 @@ async def test_client_is_initialized(app: MCPServer): async with Client(app, mode="legacy") as client: assert client.server_capabilities == snapshot( ServerCapabilities( - experimental={}, prompts=PromptsCapability(list_changed=False), resources=ResourcesCapability(subscribe=False, list_changed=False), tools=ToolsCapability(list_changed=False), diff --git a/tests/interaction/lowlevel/test_initialize.py b/tests/interaction/lowlevel/test_initialize.py index f3fd9589a6..1ebc9e2681 100644 --- a/tests/interaction/lowlevel/test_initialize.py +++ b/tests/interaction/lowlevel/test_initialize.py @@ -142,7 +142,6 @@ async def completion(ctx: ServerRequestContext, params: types.CompleteRequestPar assert capabilities == snapshot( ServerCapabilities( - experimental={}, logging=LoggingCapability(), prompts=PromptsCapability(list_changed=False), resources=ResourcesCapability(subscribe=True, list_changed=False), @@ -158,7 +157,7 @@ async def test_initialize_minimal_server_advertises_no_capabilities(connect: Con async with connect(Server("bare")) as client: capabilities = client.server_capabilities - assert capabilities == snapshot(ServerCapabilities(experimental={})) + assert capabilities == snapshot(ServerCapabilities()) @requirement("lifecycle:initialize:client-info") diff --git a/tests/server/lowlevel/test_server_discover.py b/tests/server/lowlevel/test_server_discover.py index 23a29327ee..98fffabde0 100644 --- a/tests/server/lowlevel/test_server_discover.py +++ b/tests/server/lowlevel/test_server_discover.py @@ -2,10 +2,12 @@ These call the registered handler via the public `Server.get_request_handler` accessor without spinning up a `ServerRunner` or any transport, so they verify -the handler's contract in isolation from the dispatch pipeline. The exception -is the server-identity pair: the serverInfo `_meta` stamp is applied by the -runner (spec 2026-07-28, #3002), not the handler, so those two drive one -request through `serve_one` to observe it. +the handler's contract in isolation from the dispatch pipeline. The exceptions +are the server-identity pair and the handshake comparison. The serverInfo +`_meta` stamp is applied by the runner (spec 2026-07-28, #3002), not the +handler, so that pair drives one request through `serve_one` to observe it. +The handshake comparison needs a real `initialize` result to set beside the +`server/discover` one, so it connects a `Client` in each mode. """ from collections.abc import Mapping @@ -15,8 +17,9 @@ import anyio import mcp_types as types import pytest -from mcp_types.version import MODERN_PROTOCOL_VERSIONS +from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS, MODERN_PROTOCOL_VERSIONS +from mcp import Client from mcp.server import NotificationOptions, Server, ServerRequestContext from mcp.server.connection import Connection from mcp.server.runner import serve_one @@ -271,3 +274,22 @@ async def list_tools( opted_in = server.get_capabilities(NotificationOptions(tools_changed=True)) assert opted_in.tools is not None and opted_in.tools.list_changed is True + + +@pytest.mark.anyio +async def test_unconfigured_experimental_is_omitted_from_both_initialize_and_discover() -> None: + """SDK-defined: a server with no experimental capabilities configured leaves + `experimental` out of the `initialize` result as well as the `server/discover` + result, so a client reads the same thing whichever way it connects.""" + server = Server("bare") + + async with Client(server, mode="legacy") as legacy: + assert legacy.protocol_version in HANDSHAKE_PROTOCOL_VERSIONS + initialize_capabilities = legacy.server_capabilities + + async with Client(server, mode="auto") as modern: + assert modern.protocol_version in MODERN_PROTOCOL_VERSIONS + discover_capabilities = modern.server_capabilities + + assert initialize_capabilities.experimental is None + assert discover_capabilities.experimental is None