Skip to content

Commit 3455447

Browse files
committed
refactor(client): rename to initialize_result, decompose Client properties
- ClientSession.server_params -> initialize_result (avoids collision with StdioServerParameters idiom; matches Go SDK and FastMCP) - Client: replace server_params proxy with non-nullable server_capabilities, server_info, server_instructions (init is guaranteed inside the context manager, so | None was unreachable) - Client.server_capabilities is preserved from v1 with a better type Github-Issue: #1018
1 parent 4504d0c commit 3455447

File tree

6 files changed

+61
-34
lines changed

6 files changed

+61
-34
lines changed

docs/migration.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,9 @@ result = await session.list_resources(params=PaginatedRequestParams(cursor="next
169169
result = await session.list_tools(params=PaginatedRequestParams(cursor="next_page_token"))
170170
```
171171

172-
### `ClientSession.get_server_capabilities()` replaced by `server_params` property
172+
### `ClientSession.get_server_capabilities()` replaced by `initialize_result` property
173173

174-
`ClientSession` now stores the full `InitializeResult` via a `server_params` property, mirroring `ServerSession.client_params`. This is the new way to access server metadata after initialization — `server_info`, `capabilities`, `instructions`, and the negotiated `protocol_version` are all available through this single property. The `get_server_capabilities()` method has been removed.
174+
`ClientSession` now stores the full `InitializeResult` via an `initialize_result` property. This provides access to `server_info`, `capabilities`, `instructions`, and the negotiated `protocol_version` through a single property. The `get_server_capabilities()` method has been removed.
175175

176176
**Before (v1):**
177177

@@ -183,15 +183,15 @@ capabilities = session.get_server_capabilities()
183183
**After (v2):**
184184

185185
```python
186-
params = session.server_params
187-
if params is not None:
188-
capabilities = params.capabilities
189-
server_info = params.server_info
190-
instructions = params.instructions
191-
version = params.protocol_version
186+
result = session.initialize_result
187+
if result is not None:
188+
capabilities = result.capabilities
189+
server_info = result.server_info
190+
instructions = result.instructions
191+
version = result.protocol_version
192192
```
193193

194-
The high-level `Client.server_capabilities` property has similarly been replaced by `Client.server_params`.
194+
The high-level `Client` exposes these directly as non-nullable properties (initialization is guaranteed inside the context manager): `client.server_capabilities`, `client.server_info`, and `client.server_instructions`.
195195

196196
### `McpError` renamed to `MCPError`
197197

src/mcp/client/client.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
ReadResourceResult,
3131
RequestParamsMeta,
3232
ResourceTemplateReference,
33+
ServerCapabilities,
3334
)
3435

3536

@@ -96,6 +97,7 @@ async def main():
9697
"""Callback for handling elicitation requests."""
9798

9899
_session: ClientSession | None = field(init=False, default=None)
100+
_initialize_result: InitializeResult | None = field(init=False, default=None)
99101
_exit_stack: AsyncExitStack | None = field(init=False, default=None)
100102
_transport: Transport = field(init=False)
101103

@@ -129,7 +131,7 @@ async def __aenter__(self) -> Client:
129131
)
130132
)
131133

132-
await self._session.initialize()
134+
self._initialize_result = await self._session.initialize()
133135

134136
# Transfer ownership to self for __aexit__ to handle
135137
self._exit_stack = exit_stack.pop_all()
@@ -140,6 +142,7 @@ async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseExc
140142
if self._exit_stack: # pragma: no branch
141143
await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)
142144
self._session = None
145+
self._initialize_result = None
143146

144147
@property
145148
def session(self) -> ClientSession:
@@ -155,12 +158,25 @@ def session(self) -> ClientSession:
155158
return self._session
156159

157160
@property
158-
def server_params(self) -> InitializeResult | None:
159-
"""The server's initialization response. None if not yet initialized.
161+
def server_capabilities(self) -> ServerCapabilities:
162+
"""Capabilities the server advertised during initialization."""
163+
if self._initialize_result is None:
164+
raise RuntimeError("Client must be used within an async context manager")
165+
return self._initialize_result.capabilities
160166

161-
Contains server_info, capabilities, instructions, and the negotiated protocol_version.
162-
"""
163-
return self.session.server_params
167+
@property
168+
def server_info(self) -> Implementation:
169+
"""The server's name, version, and other implementation details."""
170+
if self._initialize_result is None:
171+
raise RuntimeError("Client must be used within an async context manager")
172+
return self._initialize_result.server_info
173+
174+
@property
175+
def server_instructions(self) -> str | None:
176+
"""Instructions describing how to use the server and its features, if provided."""
177+
if self._initialize_result is None:
178+
raise RuntimeError("Client must be used within an async context manager")
179+
return self._initialize_result.instructions
164180

165181
async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
166182
"""Send a ping request to the server."""

src/mcp/client/session.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def __init__(
131131
self._logging_callback = logging_callback or _default_logging_callback
132132
self._message_handler = message_handler or _default_message_handler
133133
self._tool_output_schemas: dict[str, dict[str, Any] | None] = {}
134-
self._server_params: types.InitializeResult | None = None
134+
self._initialize_result: types.InitializeResult | None = None
135135
self._experimental_features: ExperimentalClientFeatures | None = None
136136

137137
# Experimental: Task handlers (use defaults if not provided)
@@ -185,20 +185,19 @@ async def initialize(self) -> types.InitializeResult:
185185
if result.protocol_version not in SUPPORTED_PROTOCOL_VERSIONS:
186186
raise RuntimeError(f"Unsupported protocol version from the server: {result.protocol_version}")
187187

188-
self._server_params = result
188+
self._initialize_result = result
189189

190190
await self.send_notification(types.InitializedNotification())
191191

192192
return result
193193

194194
@property
195-
def server_params(self) -> types.InitializeResult | None:
196-
"""The server's initialization response. None if not yet initialized.
195+
def initialize_result(self) -> types.InitializeResult | None:
196+
"""The server's InitializeResult. None until initialize() has been called.
197197
198-
Mirrors ServerSession.client_params. Contains server_info, capabilities,
199-
instructions, and the negotiated protocol_version.
198+
Contains server_info, capabilities, instructions, and the negotiated protocol_version.
200199
"""
201-
return self._server_params
200+
return self._initialize_result
202201

203202
@property
204203
def experimental(self) -> ExperimentalClientFeatures:

tests/client/test_client.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,16 @@ def greeting_prompt(name: str) -> str:
9999
async def test_client_is_initialized(app: MCPServer):
100100
"""Test that the client is initialized after entering context."""
101101
async with Client(app) as client:
102-
assert client.server_params is not None
103-
assert client.server_params.capabilities == snapshot(
102+
assert client.server_capabilities == snapshot(
104103
ServerCapabilities(
105104
experimental={},
106105
prompts=PromptsCapability(list_changed=False),
107106
resources=ResourcesCapability(subscribe=False, list_changed=False),
108107
tools=ToolsCapability(list_changed=False),
109108
)
110109
)
110+
assert client.server_info.name == "test"
111+
assert client.server_instructions is None
111112

112113

113114
async def test_client_with_simple_server(simple_server: Server):
@@ -195,6 +196,17 @@ def test_client_session_property_before_enter(app: MCPServer):
195196
client.session
196197

197198

199+
def test_client_server_properties_before_enter(app: MCPServer):
200+
"""Test that server_* properties raise RuntimeError outside the context manager."""
201+
client = Client(app)
202+
with pytest.raises(RuntimeError, match="Client must be used within an async context manager"):
203+
client.server_capabilities
204+
with pytest.raises(RuntimeError, match="Client must be used within an async context manager"):
205+
client.server_info
206+
with pytest.raises(RuntimeError, match="Client must be used within an async context manager"):
207+
client.server_instructions
208+
209+
198210
async def test_client_reentry_raises_runtime_error(app: MCPServer):
199211
"""Test that reentering a client raises RuntimeError."""
200212
async with Client(app) as client:

tests/client/test_session.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -540,8 +540,8 @@ async def mock_server():
540540

541541

542542
@pytest.mark.anyio
543-
async def test_server_params():
544-
"""Test that server_params is None before init and contains the full result after."""
543+
async def test_initialize_result():
544+
"""Test that initialize_result is None before init and contains the full result after."""
545545
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)
546546
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)
547547

@@ -593,17 +593,17 @@ async def mock_server():
593593
server_to_client_send,
594594
server_to_client_receive,
595595
):
596-
assert session.server_params is None
596+
assert session.initialize_result is None
597597

598598
tg.start_soon(mock_server)
599599
await session.initialize()
600600

601-
params = session.server_params
602-
assert params is not None
603-
assert params.server_info == expected_server_info
604-
assert params.capabilities == expected_capabilities
605-
assert params.instructions == expected_instructions
606-
assert params.protocol_version == LATEST_PROTOCOL_VERSION
601+
result = session.initialize_result
602+
assert result is not None
603+
assert result.server_info == expected_server_info
604+
assert result.capabilities == expected_capabilities
605+
assert result.instructions == expected_instructions
606+
assert result.protocol_version == LATEST_PROTOCOL_VERSION
607607

608608

609609
@pytest.mark.anyio

tests/client/transports/test_memory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ async def test_with_mcpserver(mcpserver_server: MCPServer):
6969
async def test_server_is_running(mcpserver_server: MCPServer):
7070
"""Test that the server is running and responding to requests."""
7171
async with Client(mcpserver_server) as client:
72-
assert client.server_params is not None
72+
assert client.server_capabilities.tools is not None
7373

7474

7575
async def test_list_tools(mcpserver_server: MCPServer):

0 commit comments

Comments
 (0)