Skip to content

Commit 6e03f80

Browse files
mr-brobotclaude
andcommitted
fix(clientsessiongroup): only query negotiated capabilities
ClientSessionGroup._aggregate_components queried prompts, resources, and tools unconditionally on every connect, ignoring the ServerCapabilities returned by initialize(). A server that advertised only some of these (e.g. tools) returned JSON-RPC "Method not found" for the rest, which was swallowed into spurious WARNING logs. The MCP lifecycle spec requires clients to only use capabilities that were successfully negotiated. Gate each list_* call on the matching capability from session.initialize_result.capabilities, falling back to the prior unconditional behavior when initialize_result is absent so the existing MCPError handler still covers servers that advertise a capability but fail the method. Fixes #2689 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 616476f commit 6e03f80

2 files changed

Lines changed: 77 additions & 25 deletions

File tree

src/mcp/client/session_group.py

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -344,36 +344,45 @@ async def _aggregate_components(self, server_info: types.Implementation, session
344344
tools_temp: dict[str, types.Tool] = {}
345345
tool_to_session_temp: dict[str, mcp.ClientSession] = {}
346346

347+
# Per the lifecycle spec, only invoke methods for capabilities the
348+
# server advertised during initialization. If the initialize result
349+
# is missing, fall back to the prior unconditional behavior so the
350+
# existing MCPError handler can still cope with servers that misbehave.
351+
capabilities = session.initialize_result.capabilities if session.initialize_result is not None else None
352+
347353
# Query the server for its prompts and aggregate to list.
348-
try:
349-
prompts = (await session.list_prompts()).prompts
350-
for prompt in prompts:
351-
name = self._component_name(prompt.name, server_info)
352-
prompts_temp[name] = prompt
353-
component_names.prompts.add(name)
354-
except MCPError as err: # pragma: no cover
355-
logging.warning(f"Could not fetch prompts: {err}")
354+
if capabilities is None or capabilities.prompts is not None:
355+
try:
356+
prompts = (await session.list_prompts()).prompts
357+
for prompt in prompts:
358+
name = self._component_name(prompt.name, server_info)
359+
prompts_temp[name] = prompt
360+
component_names.prompts.add(name)
361+
except MCPError as err: # pragma: no cover
362+
logging.warning(f"Could not fetch prompts: {err}")
356363

357364
# Query the server for its resources and aggregate to list.
358-
try:
359-
resources = (await session.list_resources()).resources
360-
for resource in resources:
361-
name = self._component_name(resource.name, server_info)
362-
resources_temp[name] = resource
363-
component_names.resources.add(name)
364-
except MCPError as err: # pragma: no cover
365-
logging.warning(f"Could not fetch resources: {err}")
365+
if capabilities is None or capabilities.resources is not None:
366+
try:
367+
resources = (await session.list_resources()).resources
368+
for resource in resources:
369+
name = self._component_name(resource.name, server_info)
370+
resources_temp[name] = resource
371+
component_names.resources.add(name)
372+
except MCPError as err: # pragma: no cover
373+
logging.warning(f"Could not fetch resources: {err}")
366374

367375
# Query the server for its tools and aggregate to list.
368-
try:
369-
tools = (await session.list_tools()).tools
370-
for tool in tools:
371-
name = self._component_name(tool.name, server_info)
372-
tools_temp[name] = tool
373-
tool_to_session_temp[name] = session
374-
component_names.tools.add(name)
375-
except MCPError as err: # pragma: no cover
376-
logging.warning(f"Could not fetch tools: {err}")
376+
if capabilities is None or capabilities.tools is not None:
377+
try:
378+
tools = (await session.list_tools()).tools
379+
for tool in tools:
380+
name = self._component_name(tool.name, server_info)
381+
tools_temp[name] = tool
382+
tool_to_session_temp[name] = session
383+
component_names.tools.add(name)
384+
except MCPError as err: # pragma: no cover
385+
logging.warning(f"Could not fetch tools: {err}")
377386

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

tests/client/test_session_group.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,3 +385,46 @@ async def test_client_session_group_establish_session_parameterized(
385385
# 3. Assert returned values
386386
assert returned_server_info is mock_initialize_result.server_info
387387
assert returned_session is mock_entered_session
388+
389+
390+
@pytest.mark.anyio
391+
@pytest.mark.parametrize("advertised", ["tools", "prompts", "resources"])
392+
async def test_client_session_group_skips_unsupported_capabilities(advertised: str):
393+
"""Only the capability the server advertised is queried during aggregation."""
394+
mock_session = mock.AsyncMock(spec=mcp.ClientSession)
395+
mock_session.initialize_result = types.InitializeResult(
396+
protocol_version=types.LATEST_PROTOCOL_VERSION,
397+
capabilities=types.ServerCapabilities(
398+
tools=types.ToolsCapability() if advertised == "tools" else None,
399+
prompts=types.PromptsCapability() if advertised == "prompts" else None,
400+
resources=types.ResourcesCapability() if advertised == "resources" else None,
401+
),
402+
server_info=types.Implementation(name="srv", version="1"),
403+
)
404+
mock_tool = mock.Mock(spec=types.Tool)
405+
mock_tool.name = "tool_a"
406+
mock_resource = mock.Mock(spec=types.Resource)
407+
mock_resource.name = "resource_b"
408+
mock_prompt = mock.Mock(spec=types.Prompt)
409+
mock_prompt.name = "prompt_c"
410+
mock_session.list_tools.return_value = mock.AsyncMock(tools=[mock_tool])
411+
mock_session.list_resources.return_value = mock.AsyncMock(resources=[mock_resource])
412+
mock_session.list_prompts.return_value = mock.AsyncMock(prompts=[mock_prompt])
413+
414+
group = ClientSessionGroup()
415+
await group.connect_with_session(types.Implementation(name="srv", version="1"), mock_session)
416+
417+
list_methods = {
418+
"tools": mock_session.list_tools,
419+
"prompts": mock_session.list_prompts,
420+
"resources": mock_session.list_resources,
421+
}
422+
for capability, list_method in list_methods.items():
423+
if capability == advertised:
424+
list_method.assert_awaited_once()
425+
else:
426+
list_method.assert_not_awaited()
427+
428+
assert group.tools == ({"tool_a": mock_tool} if advertised == "tools" else {})
429+
assert group.prompts == ({"prompt_c": mock_prompt} if advertised == "prompts" else {})
430+
assert group.resources == ({"resource_b": mock_resource} if advertised == "resources" else {})

0 commit comments

Comments
 (0)