feat: Pass through and expose additional parameters in ClientSessionGroup.call_tool and .connect_to_server (#1576)
This commit is contained in:
parent
9724ad1ce6
commit
a357380cfa
2 changed files with 101 additions and 11 deletions
|
|
@ -11,20 +11,23 @@ hook.
|
||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any, TypeAlias
|
from typing import Any, TypeAlias, overload
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self, deprecated
|
||||||
|
|
||||||
import mcp
|
import mcp
|
||||||
from mcp import types
|
from mcp import types
|
||||||
|
from mcp.client.session import ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
|
||||||
from mcp.client.sse import sse_client
|
from mcp.client.sse import sse_client
|
||||||
from mcp.client.stdio import StdioServerParameters
|
from mcp.client.stdio import StdioServerParameters
|
||||||
from mcp.client.streamable_http import streamablehttp_client
|
from mcp.client.streamable_http import streamablehttp_client
|
||||||
from mcp.shared.exceptions import McpError
|
from mcp.shared.exceptions import McpError
|
||||||
|
from mcp.shared.session import ProgressFnT
|
||||||
|
|
||||||
|
|
||||||
class SseServerParameters(BaseModel):
|
class SseServerParameters(BaseModel):
|
||||||
|
|
@ -65,6 +68,21 @@ class StreamableHttpParameters(BaseModel):
|
||||||
ServerParameters: TypeAlias = StdioServerParameters | SseServerParameters | StreamableHttpParameters
|
ServerParameters: TypeAlias = StdioServerParameters | SseServerParameters | StreamableHttpParameters
|
||||||
|
|
||||||
|
|
||||||
|
# Use dataclass instead of pydantic BaseModel
|
||||||
|
# because pydantic BaseModel cannot handle Protocol fields.
|
||||||
|
@dataclass
|
||||||
|
class ClientSessionParameters:
|
||||||
|
"""Parameters for establishing a client session to an MCP server."""
|
||||||
|
|
||||||
|
read_timeout_seconds: timedelta | None = None
|
||||||
|
sampling_callback: SamplingFnT | None = None
|
||||||
|
elicitation_callback: ElicitationFnT | None = None
|
||||||
|
list_roots_callback: ListRootsFnT | None = None
|
||||||
|
logging_callback: LoggingFnT | None = None
|
||||||
|
message_handler: MessageHandlerFnT | None = None
|
||||||
|
client_info: types.Implementation | None = None
|
||||||
|
|
||||||
|
|
||||||
class ClientSessionGroup:
|
class ClientSessionGroup:
|
||||||
"""Client for managing connections to multiple MCP servers.
|
"""Client for managing connections to multiple MCP servers.
|
||||||
|
|
||||||
|
|
@ -172,11 +190,49 @@ class ClientSessionGroup:
|
||||||
"""Returns the tools as a dictionary of names to tools."""
|
"""Returns the tools as a dictionary of names to tools."""
|
||||||
return self._tools
|
return self._tools
|
||||||
|
|
||||||
async def call_tool(self, name: str, args: dict[str, Any]) -> types.CallToolResult:
|
@overload
|
||||||
|
async def call_tool(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
arguments: dict[str, Any],
|
||||||
|
read_timeout_seconds: timedelta | None = None,
|
||||||
|
progress_callback: ProgressFnT | None = None,
|
||||||
|
*,
|
||||||
|
meta: dict[str, Any] | None = None,
|
||||||
|
) -> types.CallToolResult: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
@deprecated("The 'args' parameter is deprecated. Use 'arguments' instead.")
|
||||||
|
async def call_tool(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
args: dict[str, Any],
|
||||||
|
read_timeout_seconds: timedelta | None = None,
|
||||||
|
progress_callback: ProgressFnT | None = None,
|
||||||
|
meta: dict[str, Any] | None = None,
|
||||||
|
) -> types.CallToolResult: ...
|
||||||
|
|
||||||
|
async def call_tool(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
arguments: dict[str, Any] | None = None,
|
||||||
|
read_timeout_seconds: timedelta | None = None,
|
||||||
|
progress_callback: ProgressFnT | None = None,
|
||||||
|
*,
|
||||||
|
meta: dict[str, Any] | None = None,
|
||||||
|
args: dict[str, Any] | None = None,
|
||||||
|
) -> types.CallToolResult:
|
||||||
"""Executes a tool given its name and arguments."""
|
"""Executes a tool given its name and arguments."""
|
||||||
session = self._tool_to_session[name]
|
session = self._tool_to_session[name]
|
||||||
session_tool_name = self.tools[name].name
|
session_tool_name = self.tools[name].name
|
||||||
return await session.call_tool(session_tool_name, args)
|
return await session.call_tool(
|
||||||
|
session_tool_name,
|
||||||
|
arguments if args is None else args,
|
||||||
|
read_timeout_seconds=read_timeout_seconds,
|
||||||
|
progress_callback=progress_callback,
|
||||||
|
meta=meta,
|
||||||
|
)
|
||||||
|
|
||||||
async def disconnect_from_server(self, session: mcp.ClientSession) -> None:
|
async def disconnect_from_server(self, session: mcp.ClientSession) -> None:
|
||||||
"""Disconnects from a single MCP server."""
|
"""Disconnects from a single MCP server."""
|
||||||
|
|
@ -225,13 +281,16 @@ class ClientSessionGroup:
|
||||||
async def connect_to_server(
|
async def connect_to_server(
|
||||||
self,
|
self,
|
||||||
server_params: ServerParameters,
|
server_params: ServerParameters,
|
||||||
|
session_params: ClientSessionParameters | None = None,
|
||||||
) -> mcp.ClientSession:
|
) -> mcp.ClientSession:
|
||||||
"""Connects to a single MCP server."""
|
"""Connects to a single MCP server."""
|
||||||
server_info, session = await self._establish_session(server_params)
|
server_info, session = await self._establish_session(server_params, session_params or ClientSessionParameters())
|
||||||
return await self.connect_with_session(server_info, session)
|
return await self.connect_with_session(server_info, session)
|
||||||
|
|
||||||
async def _establish_session(
|
async def _establish_session(
|
||||||
self, server_params: ServerParameters
|
self,
|
||||||
|
server_params: ServerParameters,
|
||||||
|
session_params: ClientSessionParameters,
|
||||||
) -> tuple[types.Implementation, mcp.ClientSession]:
|
) -> tuple[types.Implementation, mcp.ClientSession]:
|
||||||
"""Establish a client session to an MCP server."""
|
"""Establish a client session to an MCP server."""
|
||||||
|
|
||||||
|
|
@ -259,7 +318,20 @@ class ClientSessionGroup:
|
||||||
)
|
)
|
||||||
read, write, _ = await session_stack.enter_async_context(client)
|
read, write, _ = await session_stack.enter_async_context(client)
|
||||||
|
|
||||||
session = await session_stack.enter_async_context(mcp.ClientSession(read, write))
|
session = await session_stack.enter_async_context(
|
||||||
|
mcp.ClientSession(
|
||||||
|
read,
|
||||||
|
write,
|
||||||
|
read_timeout_seconds=session_params.read_timeout_seconds,
|
||||||
|
sampling_callback=session_params.sampling_callback,
|
||||||
|
elicitation_callback=session_params.elicitation_callback,
|
||||||
|
list_roots_callback=session_params.list_roots_callback,
|
||||||
|
logging_callback=session_params.logging_callback,
|
||||||
|
message_handler=session_params.message_handler,
|
||||||
|
client_info=session_params.client_info,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
result = await session.initialize()
|
result = await session.initialize()
|
||||||
|
|
||||||
# Session successfully initialized.
|
# Session successfully initialized.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,12 @@ import pytest
|
||||||
|
|
||||||
import mcp
|
import mcp
|
||||||
from mcp import types
|
from mcp import types
|
||||||
from mcp.client.session_group import ClientSessionGroup, SseServerParameters, StreamableHttpParameters
|
from mcp.client.session_group import (
|
||||||
|
ClientSessionGroup,
|
||||||
|
ClientSessionParameters,
|
||||||
|
SseServerParameters,
|
||||||
|
StreamableHttpParameters,
|
||||||
|
)
|
||||||
from mcp.client.stdio import StdioServerParameters
|
from mcp.client.stdio import StdioServerParameters
|
||||||
from mcp.shared.exceptions import McpError
|
from mcp.shared.exceptions import McpError
|
||||||
|
|
||||||
|
|
@ -62,7 +67,7 @@ class TestClientSessionGroup:
|
||||||
# --- Test Execution ---
|
# --- Test Execution ---
|
||||||
result = await mcp_session_group.call_tool(
|
result = await mcp_session_group.call_tool(
|
||||||
name="server1-my_tool",
|
name="server1-my_tool",
|
||||||
args={
|
arguments={
|
||||||
"name": "value1",
|
"name": "value1",
|
||||||
"args": {},
|
"args": {},
|
||||||
},
|
},
|
||||||
|
|
@ -73,6 +78,9 @@ class TestClientSessionGroup:
|
||||||
mock_session.call_tool.assert_called_once_with(
|
mock_session.call_tool.assert_called_once_with(
|
||||||
"my_tool",
|
"my_tool",
|
||||||
{"name": "value1", "args": {}},
|
{"name": "value1", "args": {}},
|
||||||
|
read_timeout_seconds=None,
|
||||||
|
progress_callback=None,
|
||||||
|
meta=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def test_connect_to_server(self, mock_exit_stack: contextlib.AsyncExitStack):
|
async def test_connect_to_server(self, mock_exit_stack: contextlib.AsyncExitStack):
|
||||||
|
|
@ -329,7 +337,7 @@ class TestClientSessionGroup:
|
||||||
(
|
(
|
||||||
returned_server_info,
|
returned_server_info,
|
||||||
returned_session,
|
returned_session,
|
||||||
) = await group._establish_session(server_params_instance)
|
) = await group._establish_session(server_params_instance, ClientSessionParameters())
|
||||||
|
|
||||||
# --- Assertions ---
|
# --- Assertions ---
|
||||||
# 1. Assert the correct specific client function was called
|
# 1. Assert the correct specific client function was called
|
||||||
|
|
@ -357,7 +365,17 @@ class TestClientSessionGroup:
|
||||||
mock_client_cm_instance.__aenter__.assert_awaited_once()
|
mock_client_cm_instance.__aenter__.assert_awaited_once()
|
||||||
|
|
||||||
# 2. Assert ClientSession was called correctly
|
# 2. Assert ClientSession was called correctly
|
||||||
mock_ClientSession_class.assert_called_once_with(mock_read_stream, mock_write_stream)
|
mock_ClientSession_class.assert_called_once_with(
|
||||||
|
mock_read_stream,
|
||||||
|
mock_write_stream,
|
||||||
|
read_timeout_seconds=None,
|
||||||
|
sampling_callback=None,
|
||||||
|
elicitation_callback=None,
|
||||||
|
list_roots_callback=None,
|
||||||
|
logging_callback=None,
|
||||||
|
message_handler=None,
|
||||||
|
client_info=None,
|
||||||
|
)
|
||||||
mock_raw_session_cm.__aenter__.assert_awaited_once()
|
mock_raw_session_cm.__aenter__.assert_awaited_once()
|
||||||
mock_entered_session.initialize.assert_awaited_once()
|
mock_entered_session.initialize.assert_awaited_once()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue