Skip to content

Commit 0ac1c6e

Browse files
author
Jianke LIN
committed
fix(client): respect negotiated capabilities in ClientSessionGroup
1 parent e942d00 commit 0ac1c6e

2 files changed

Lines changed: 75 additions & 25 deletions

File tree

src/mcp/client/session_group.py

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ async def _establish_session(
371371
async def _aggregate_components(self, server_info: types.Implementation, session: mcp.ClientSession) -> None:
372372
"""Aggregates prompts, resources, and tools from a given session."""
373373

374+
capabilities = session.initialize_result.capabilities if session.initialize_result else None
375+
374376
# Create a reverse index so we can find all prompts, resources, and
375377
# tools belonging to this session. Used for removing components from
376378
# the session group via self.disconnect_from_server.
@@ -384,35 +386,38 @@ async def _aggregate_components(self, server_info: types.Implementation, session
384386
tool_to_session_temp: dict[str, mcp.ClientSession] = {}
385387

386388
# Query the server for its prompts and aggregate to list.
387-
try:
388-
prompts = (await session.list_prompts()).prompts
389-
for prompt in prompts:
390-
name = self._component_name(prompt.name, server_info)
391-
prompts_temp[name] = prompt
392-
component_names.prompts.add(name)
393-
except MCPError as err: # pragma: no cover
394-
logging.warning(f"Could not fetch prompts: {err}")
389+
if capabilities is None or capabilities.prompts is not None:
390+
try:
391+
prompts = (await session.list_prompts()).prompts
392+
for prompt in prompts:
393+
name = self._component_name(prompt.name, server_info)
394+
prompts_temp[name] = prompt
395+
component_names.prompts.add(name)
396+
except MCPError as err: # pragma: no cover
397+
logging.warning(f"Could not fetch prompts: {err}")
395398

396399
# Query the server for its resources and aggregate to list.
397-
try:
398-
resources = (await session.list_resources()).resources
399-
for resource in resources:
400-
name = self._component_name(resource.name, server_info)
401-
resources_temp[name] = resource
402-
component_names.resources.add(name)
403-
except MCPError as err: # pragma: no cover
404-
logging.warning(f"Could not fetch resources: {err}")
400+
if capabilities is None or capabilities.resources is not None:
401+
try:
402+
resources = (await session.list_resources()).resources
403+
for resource in resources:
404+
name = self._component_name(resource.name, server_info)
405+
resources_temp[name] = resource
406+
component_names.resources.add(name)
407+
except MCPError as err: # pragma: no cover
408+
logging.warning(f"Could not fetch resources: {err}")
405409

406410
# Query the server for its tools and aggregate to list.
407-
try:
408-
tools = (await session.list_tools()).tools
409-
for tool in tools:
410-
name = self._component_name(tool.name, server_info)
411-
tools_temp[name] = tool
412-
tool_to_session_temp[name] = session
413-
component_names.tools.add(name)
414-
except MCPError as err: # pragma: no cover
415-
logging.warning(f"Could not fetch tools: {err}")
411+
if capabilities is None or capabilities.tools is not None:
412+
try:
413+
tools = (await session.list_tools()).tools
414+
for tool in tools:
415+
name = self._component_name(tool.name, server_info)
416+
tools_temp[name] = tool
417+
tool_to_session_temp[name] = session
418+
component_names.tools.add(name)
419+
except MCPError as err: # pragma: no cover
420+
logging.warning(f"Could not fetch tools: {err}")
416421

417422
# Clean up exit stack for session if we couldn't retrieve anything
418423
# from the server.

tests/client/test_session_group.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import contextlib
2+
import logging
23
from unittest import mock
34

45
import httpx
@@ -142,6 +143,50 @@ async def test_client_session_group_connect_to_server(mock_exit_stack: contextli
142143
mock_session.list_prompts.assert_awaited_once()
143144

144145

146+
@pytest.mark.anyio
147+
async def test_client_session_group_connect_with_session_respects_negotiated_capabilities(
148+
caplog: pytest.LogCaptureFixture,
149+
):
150+
from mcp import Client
151+
from mcp.server import Server, ServerRequestContext
152+
153+
async def handle_list_tools(
154+
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
155+
) -> types.ListToolsResult:
156+
return types.ListToolsResult(
157+
tools=[
158+
types.Tool(
159+
name="ping",
160+
description="Ping",
161+
input_schema={"type": "object", "properties": {}},
162+
)
163+
]
164+
)
165+
166+
async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
167+
return types.CallToolResult(content=[types.TextContent(type="text", text="pong")])
168+
169+
server = Server(
170+
"tools-only-server",
171+
on_list_tools=handle_list_tools,
172+
on_call_tool=handle_call_tool,
173+
)
174+
175+
group = ClientSessionGroup()
176+
177+
with caplog.at_level(logging.WARNING):
178+
async with Client(server) as client:
179+
assert client.initialize_result.capabilities.prompts is None
180+
assert client.initialize_result.capabilities.resources is None
181+
182+
client.session.list_prompts = mock.AsyncMock(side_effect=AssertionError("list_prompts() was called"))
183+
client.session.list_resources = mock.AsyncMock(side_effect=AssertionError("list_resources() was called"))
184+
185+
await group.connect_with_session(client.initialize_result.server_info, client.session)
186+
187+
assert not caplog.records
188+
189+
145190
@pytest.mark.anyio
146191
async def test_client_session_group_connect_to_server_with_name_hook(mock_exit_stack: contextlib.AsyncExitStack):
147192
"""Test connecting with a component name hook."""

0 commit comments

Comments
 (0)