feat(client): store InitializeResult as initialize_result (#2300)
This commit is contained in:
parent
67201a9bbd
commit
20dd94632e
7 changed files with 64 additions and 37 deletions
|
|
@ -169,6 +169,30 @@ result = await session.list_resources(params=PaginatedRequestParams(cursor="next
|
||||||
result = await session.list_tools(params=PaginatedRequestParams(cursor="next_page_token"))
|
result = await session.list_tools(params=PaginatedRequestParams(cursor="next_page_token"))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `ClientSession.get_server_capabilities()` replaced by `initialize_result` property
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
**Before (v1):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
capabilities = session.get_server_capabilities()
|
||||||
|
# server_info, instructions, protocol_version were not stored — had to capture initialize() return value
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (v2):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = session.initialize_result
|
||||||
|
if result is not None:
|
||||||
|
capabilities = result.capabilities
|
||||||
|
server_info = result.server_info
|
||||||
|
instructions = result.instructions
|
||||||
|
version = result.protocol_version
|
||||||
|
```
|
||||||
|
|
||||||
|
The high-level `Client.initialize_result` returns the same `InitializeResult` but is non-nullable — initialization is guaranteed inside the context manager, so no `None` check is needed. This replaces v1's `Client.server_capabilities`; use `client.initialize_result.capabilities` instead.
|
||||||
|
|
||||||
### `McpError` renamed to `MCPError`
|
### `McpError` renamed to `MCPError`
|
||||||
|
|
||||||
The `McpError` exception class has been renamed to `MCPError` for consistent naming with the MCP acronym style used throughout the SDK.
|
The `McpError` exception class has been renamed to `MCPError` for consistent naming with the MCP acronym style used throughout the SDK.
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from mcp.types import (
|
||||||
EmptyResult,
|
EmptyResult,
|
||||||
GetPromptResult,
|
GetPromptResult,
|
||||||
Implementation,
|
Implementation,
|
||||||
|
InitializeResult,
|
||||||
ListPromptsResult,
|
ListPromptsResult,
|
||||||
ListResourcesResult,
|
ListResourcesResult,
|
||||||
ListResourceTemplatesResult,
|
ListResourceTemplatesResult,
|
||||||
|
|
@ -29,7 +30,6 @@ from mcp.types import (
|
||||||
ReadResourceResult,
|
ReadResourceResult,
|
||||||
RequestParamsMeta,
|
RequestParamsMeta,
|
||||||
ResourceTemplateReference,
|
ResourceTemplateReference,
|
||||||
ServerCapabilities,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -155,9 +155,16 @@ class Client:
|
||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def server_capabilities(self) -> ServerCapabilities | None:
|
def initialize_result(self) -> InitializeResult:
|
||||||
"""The server capabilities received during initialization, or None if not yet initialized."""
|
"""The server's InitializeResult.
|
||||||
return self.session.get_server_capabilities()
|
|
||||||
|
Contains server_info, capabilities, instructions, and the negotiated protocol_version.
|
||||||
|
Raises RuntimeError if accessed outside the context manager.
|
||||||
|
"""
|
||||||
|
result = self.session.initialize_result
|
||||||
|
if result is None: # pragma: no cover
|
||||||
|
raise RuntimeError("Client must be used within an async context manager")
|
||||||
|
return result
|
||||||
|
|
||||||
async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
|
async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> EmptyResult:
|
||||||
"""Send a ping request to the server."""
|
"""Send a ping request to the server."""
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ class ClientSession(
|
||||||
self._logging_callback = logging_callback or _default_logging_callback
|
self._logging_callback = logging_callback or _default_logging_callback
|
||||||
self._message_handler = message_handler or _default_message_handler
|
self._message_handler = message_handler or _default_message_handler
|
||||||
self._tool_output_schemas: dict[str, dict[str, Any] | None] = {}
|
self._tool_output_schemas: dict[str, dict[str, Any] | None] = {}
|
||||||
self._server_capabilities: types.ServerCapabilities | None = None
|
self._initialize_result: types.InitializeResult | None = None
|
||||||
self._experimental_features: ExperimentalClientFeatures | None = None
|
self._experimental_features: ExperimentalClientFeatures | None = None
|
||||||
|
|
||||||
# Experimental: Task handlers (use defaults if not provided)
|
# Experimental: Task handlers (use defaults if not provided)
|
||||||
|
|
@ -185,18 +185,19 @@ class ClientSession(
|
||||||
if result.protocol_version not in SUPPORTED_PROTOCOL_VERSIONS:
|
if result.protocol_version not in SUPPORTED_PROTOCOL_VERSIONS:
|
||||||
raise RuntimeError(f"Unsupported protocol version from the server: {result.protocol_version}")
|
raise RuntimeError(f"Unsupported protocol version from the server: {result.protocol_version}")
|
||||||
|
|
||||||
self._server_capabilities = result.capabilities
|
self._initialize_result = result
|
||||||
|
|
||||||
await self.send_notification(types.InitializedNotification())
|
await self.send_notification(types.InitializedNotification())
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_server_capabilities(self) -> types.ServerCapabilities | None:
|
@property
|
||||||
"""Return the server capabilities received during initialization.
|
def initialize_result(self) -> types.InitializeResult | None:
|
||||||
|
"""The server's InitializeResult. None until initialize() has been called.
|
||||||
|
|
||||||
Returns None if the session has not been initialized yet.
|
Contains server_info, capabilities, instructions, and the negotiated protocol_version.
|
||||||
"""
|
"""
|
||||||
return self._server_capabilities
|
return self._initialize_result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def experimental(self) -> ExperimentalClientFeatures:
|
def experimental(self) -> ExperimentalClientFeatures:
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ def app() -> MCPServer:
|
||||||
async def test_client_is_initialized(app: MCPServer):
|
async def test_client_is_initialized(app: MCPServer):
|
||||||
"""Test that the client is initialized after entering context."""
|
"""Test that the client is initialized after entering context."""
|
||||||
async with Client(app) as client:
|
async with Client(app) as client:
|
||||||
assert client.server_capabilities == snapshot(
|
assert client.initialize_result.capabilities == snapshot(
|
||||||
ServerCapabilities(
|
ServerCapabilities(
|
||||||
experimental={},
|
experimental={},
|
||||||
prompts=PromptsCapability(list_changed=False),
|
prompts=PromptsCapability(list_changed=False),
|
||||||
|
|
@ -107,6 +107,7 @@ async def test_client_is_initialized(app: MCPServer):
|
||||||
tools=ToolsCapability(list_changed=False),
|
tools=ToolsCapability(list_changed=False),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
assert client.initialize_result.server_info.name == "test"
|
||||||
|
|
||||||
|
|
||||||
async def test_client_with_simple_server(simple_server: Server):
|
async def test_client_with_simple_server(simple_server: Server):
|
||||||
|
|
|
||||||
|
|
@ -540,8 +540,8 @@ async def test_client_capabilities_with_sampling_tools():
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_get_server_capabilities():
|
async def test_initialize_result():
|
||||||
"""Test that get_server_capabilities returns None before init and capabilities after"""
|
"""Test that initialize_result is None before init and contains the full result after."""
|
||||||
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)
|
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)
|
||||||
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)
|
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)
|
||||||
|
|
||||||
|
|
@ -551,6 +551,8 @@ async def test_get_server_capabilities():
|
||||||
resources=types.ResourcesCapability(subscribe=True, list_changed=True),
|
resources=types.ResourcesCapability(subscribe=True, list_changed=True),
|
||||||
tools=types.ToolsCapability(list_changed=False),
|
tools=types.ToolsCapability(list_changed=False),
|
||||||
)
|
)
|
||||||
|
expected_server_info = Implementation(name="mock-server", version="0.1.0")
|
||||||
|
expected_instructions = "Use the tools wisely."
|
||||||
|
|
||||||
async def mock_server():
|
async def mock_server():
|
||||||
session_message = await client_to_server_receive.receive()
|
session_message = await client_to_server_receive.receive()
|
||||||
|
|
@ -564,7 +566,8 @@ async def test_get_server_capabilities():
|
||||||
result = InitializeResult(
|
result = InitializeResult(
|
||||||
protocol_version=LATEST_PROTOCOL_VERSION,
|
protocol_version=LATEST_PROTOCOL_VERSION,
|
||||||
capabilities=expected_capabilities,
|
capabilities=expected_capabilities,
|
||||||
server_info=Implementation(name="mock-server", version="0.1.0"),
|
server_info=expected_server_info,
|
||||||
|
instructions=expected_instructions,
|
||||||
)
|
)
|
||||||
|
|
||||||
async with server_to_client_send:
|
async with server_to_client_send:
|
||||||
|
|
@ -590,21 +593,17 @@ async def test_get_server_capabilities():
|
||||||
server_to_client_send,
|
server_to_client_send,
|
||||||
server_to_client_receive,
|
server_to_client_receive,
|
||||||
):
|
):
|
||||||
assert session.get_server_capabilities() is None
|
assert session.initialize_result is None
|
||||||
|
|
||||||
tg.start_soon(mock_server)
|
tg.start_soon(mock_server)
|
||||||
await session.initialize()
|
await session.initialize()
|
||||||
|
|
||||||
capabilities = session.get_server_capabilities()
|
result = session.initialize_result
|
||||||
assert capabilities is not None
|
assert result is not None
|
||||||
assert capabilities == expected_capabilities
|
assert result.server_info == expected_server_info
|
||||||
assert capabilities.logging is not None
|
assert result.capabilities == expected_capabilities
|
||||||
assert capabilities.prompts is not None
|
assert result.instructions == expected_instructions
|
||||||
assert capabilities.prompts.list_changed is True
|
assert result.protocol_version == LATEST_PROTOCOL_VERSION
|
||||||
assert capabilities.resources is not None
|
|
||||||
assert capabilities.resources.subscribe is True
|
|
||||||
assert capabilities.tools is not None
|
|
||||||
assert capabilities.tools.list_changed is False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ async def test_with_mcpserver(mcpserver_server: MCPServer):
|
||||||
async def test_server_is_running(mcpserver_server: MCPServer):
|
async def test_server_is_running(mcpserver_server: MCPServer):
|
||||||
"""Test that the server is running and responding to requests."""
|
"""Test that the server is running and responding to requests."""
|
||||||
async with Client(mcpserver_server) as client:
|
async with Client(mcpserver_server) as client:
|
||||||
assert client.server_capabilities is not None
|
assert client.initialize_result.capabilities.tools is not None
|
||||||
|
|
||||||
|
|
||||||
async def test_list_tools(mcpserver_server: MCPServer):
|
async def test_list_tools(mcpserver_server: MCPServer):
|
||||||
|
|
|
||||||
|
|
@ -109,8 +109,7 @@ async def elicitation_callback(context: RequestContext[ClientSession], params: E
|
||||||
async def test_basic_tools() -> None:
|
async def test_basic_tools() -> None:
|
||||||
"""Test basic tool functionality."""
|
"""Test basic tool functionality."""
|
||||||
async with Client(basic_tool.mcp) as client:
|
async with Client(basic_tool.mcp) as client:
|
||||||
assert client.server_capabilities is not None
|
assert client.initialize_result.capabilities.tools is not None
|
||||||
assert client.server_capabilities.tools is not None
|
|
||||||
|
|
||||||
# Test sum tool
|
# Test sum tool
|
||||||
tool_result = await client.call_tool("sum", {"a": 5, "b": 3})
|
tool_result = await client.call_tool("sum", {"a": 5, "b": 3})
|
||||||
|
|
@ -128,8 +127,7 @@ async def test_basic_tools() -> None:
|
||||||
async def test_basic_resources() -> None:
|
async def test_basic_resources() -> None:
|
||||||
"""Test basic resource functionality."""
|
"""Test basic resource functionality."""
|
||||||
async with Client(basic_resource.mcp) as client:
|
async with Client(basic_resource.mcp) as client:
|
||||||
assert client.server_capabilities is not None
|
assert client.initialize_result.capabilities.resources is not None
|
||||||
assert client.server_capabilities.resources is not None
|
|
||||||
|
|
||||||
# Test document resource
|
# Test document resource
|
||||||
doc_content = await client.read_resource("file://documents/readme")
|
doc_content = await client.read_resource("file://documents/readme")
|
||||||
|
|
@ -151,8 +149,7 @@ async def test_basic_resources() -> None:
|
||||||
async def test_basic_prompts() -> None:
|
async def test_basic_prompts() -> None:
|
||||||
"""Test basic prompt functionality."""
|
"""Test basic prompt functionality."""
|
||||||
async with Client(basic_prompt.mcp) as client:
|
async with Client(basic_prompt.mcp) as client:
|
||||||
assert client.server_capabilities is not None
|
assert client.initialize_result.capabilities.prompts is not None
|
||||||
assert client.server_capabilities.prompts is not None
|
|
||||||
|
|
||||||
# Test review_code prompt
|
# Test review_code prompt
|
||||||
prompts = await client.list_prompts()
|
prompts = await client.list_prompts()
|
||||||
|
|
@ -223,8 +220,7 @@ async def test_tool_progress() -> None:
|
||||||
async def test_sampling() -> None:
|
async def test_sampling() -> None:
|
||||||
"""Test sampling (LLM interaction) functionality."""
|
"""Test sampling (LLM interaction) functionality."""
|
||||||
async with Client(sampling.mcp, sampling_callback=sampling_callback) as client:
|
async with Client(sampling.mcp, sampling_callback=sampling_callback) as client:
|
||||||
assert client.server_capabilities is not None
|
assert client.initialize_result.capabilities.tools is not None
|
||||||
assert client.server_capabilities.tools is not None
|
|
||||||
|
|
||||||
# Test sampling tool
|
# Test sampling tool
|
||||||
sampling_result = await client.call_tool("generate_poem", {"topic": "nature"})
|
sampling_result = await client.call_tool("generate_poem", {"topic": "nature"})
|
||||||
|
|
@ -294,9 +290,8 @@ async def test_notifications() -> None:
|
||||||
async def test_completion() -> None:
|
async def test_completion() -> None:
|
||||||
"""Test completion (autocomplete) functionality."""
|
"""Test completion (autocomplete) functionality."""
|
||||||
async with Client(completion.mcp) as client:
|
async with Client(completion.mcp) as client:
|
||||||
assert client.server_capabilities is not None
|
assert client.initialize_result.capabilities.resources is not None
|
||||||
assert client.server_capabilities.resources is not None
|
assert client.initialize_result.capabilities.prompts is not None
|
||||||
assert client.server_capabilities.prompts is not None
|
|
||||||
|
|
||||||
# Test resource completion
|
# Test resource completion
|
||||||
completion_result = await client.complete(
|
completion_result = await client.complete(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue