Skip to content

Commit 449070c

Browse files
authored
Omit an unset experimental capability from the initialize result (#3614)
1 parent 86ec15f commit 449070c

4 files changed

Lines changed: 29 additions & 9 deletions

File tree

‎src/mcp/server/lowlevel/server.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,7 @@ def create_initialization_options(
545545
description=self.description,
546546
capabilities=self.get_capabilities(
547547
notification_options or NotificationOptions(),
548-
experimental_capabilities or {},
548+
experimental_capabilities,
549549
extensions if extensions is not None else self.extensions,
550550
),
551551
instructions=self.instructions,

‎tests/client/test_client.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ async def test_client_is_initialized(app: MCPServer):
114114
async with Client(app, mode="legacy") as client:
115115
assert client.server_capabilities == snapshot(
116116
ServerCapabilities(
117-
experimental={},
118117
prompts=PromptsCapability(list_changed=False),
119118
resources=ResourcesCapability(subscribe=False, list_changed=False),
120119
tools=ToolsCapability(list_changed=False),

‎tests/interaction/lowlevel/test_initialize.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ async def completion(ctx: ServerRequestContext, params: types.CompleteRequestPar
142142

143143
assert capabilities == snapshot(
144144
ServerCapabilities(
145-
experimental={},
146145
logging=LoggingCapability(),
147146
prompts=PromptsCapability(list_changed=False),
148147
resources=ResourcesCapability(subscribe=True, list_changed=False),
@@ -158,7 +157,7 @@ async def test_initialize_minimal_server_advertises_no_capabilities(connect: Con
158157
async with connect(Server("bare")) as client:
159158
capabilities = client.server_capabilities
160159

161-
assert capabilities == snapshot(ServerCapabilities(experimental={}))
160+
assert capabilities == snapshot(ServerCapabilities())
162161

163162

164163
@requirement("lifecycle:initialize:client-info")

‎tests/server/lowlevel/test_server_discover.py‎

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
33
These call the registered handler via the public `Server.get_request_handler`
44
accessor without spinning up a `ServerRunner` or any transport, so they verify
5-
the handler's contract in isolation from the dispatch pipeline. The exception
6-
is the server-identity pair: the serverInfo `_meta` stamp is applied by the
7-
runner (spec 2026-07-28, #3002), not the handler, so those two drive one
8-
request through `serve_one` to observe it.
5+
the handler's contract in isolation from the dispatch pipeline. The exceptions
6+
are the server-identity pair and the handshake comparison. The serverInfo
7+
`_meta` stamp is applied by the runner (spec 2026-07-28, #3002), not the
8+
handler, so that pair drives one request through `serve_one` to observe it.
9+
The handshake comparison needs a real `initialize` result to set beside the
10+
`server/discover` one, so it connects a `Client` in each mode.
911
"""
1012

1113
from collections.abc import Mapping
@@ -15,8 +17,9 @@
1517
import anyio
1618
import mcp_types as types
1719
import pytest
18-
from mcp_types.version import MODERN_PROTOCOL_VERSIONS
20+
from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS, MODERN_PROTOCOL_VERSIONS
1921

22+
from mcp import Client
2023
from mcp.server import NotificationOptions, Server, ServerRequestContext
2124
from mcp.server.connection import Connection
2225
from mcp.server.runner import serve_one
@@ -271,3 +274,22 @@ async def list_tools(
271274

272275
opted_in = server.get_capabilities(NotificationOptions(tools_changed=True))
273276
assert opted_in.tools is not None and opted_in.tools.list_changed is True
277+
278+
279+
@pytest.mark.anyio
280+
async def test_unconfigured_experimental_is_omitted_from_both_initialize_and_discover() -> None:
281+
"""SDK-defined: a server with no experimental capabilities configured leaves
282+
`experimental` out of the `initialize` result as well as the `server/discover`
283+
result, so a client reads the same thing whichever way it connects."""
284+
server = Server("bare")
285+
286+
async with Client(server, mode="legacy") as legacy:
287+
assert legacy.protocol_version in HANDSHAKE_PROTOCOL_VERSIONS
288+
initialize_capabilities = legacy.server_capabilities
289+
290+
async with Client(server, mode="auto") as modern:
291+
assert modern.protocol_version in MODERN_PROTOCOL_VERSIONS
292+
discover_capabilities = modern.server_capabilities
293+
294+
assert initialize_capabilities.experimental is None
295+
assert discover_capabilities.experimental is None

0 commit comments

Comments
 (0)