1+ from typing import Optional
2+ from mcp import ClientSession
3+ from mcp .client .sse import sse_client
4+ from roll .utils .logging import get_logger
5+
6+ logger = get_logger ()
7+
8+ class MCPClient :
9+ def __init__ (self , server_url : str ):
10+ """
11+ Initialize MCPClient with the server URL.
12+ Args:
13+ server_url (str): The URL of the MCP SSE server to connect to.
14+ """
15+ self .server_url = server_url
16+ self ._streams_context = None
17+ self ._session_context = None
18+ self .session : Optional [ClientSession ] = None
19+
20+ async def __aenter__ (self ):
21+ """
22+ Enter the async context manager: connect to the MCP SSE server,
23+ initialize the session and prepare for tool calls.
24+
25+ Returns:
26+ MCPClient: The connected client instance itself.
27+ """
28+ self ._streams_context = sse_client (url = self .server_url )
29+ self ._streams = await self ._streams_context .__aenter__ ()
30+ self ._session_context = ClientSession (* self ._streams )
31+ self .session = await self ._session_context .__aenter__ ()
32+
33+ initialize = await self .session .initialize ()
34+ logger .debug (f"Session initialize: { initialize } " )
35+
36+ response = await self .session .list_tools ()
37+ tools = getattr (response , "tools" , [])
38+ logger .debug (f"Connected to server with tools: { [tool .name for tool in tools ]} " )
39+
40+ return self
41+
42+ async def __aexit__ (self , exc_type , exc_val , exc_tb ):
43+ """
44+ Exit the async context: cleanly close session and streams,
45+ ensuring resources are properly released.
46+ Args:
47+ exc_type, exc_val, exc_tb: Exception information if exiting because of an exception.
48+ """
49+ if self ._session_context :
50+ await self ._session_context .__aexit__ (exc_type , exc_val , exc_tb )
51+ self ._session_context = None
52+ if self ._streams_context :
53+ await self ._streams_context .__aexit__ (exc_type , exc_val , exc_tb )
54+ self ._streams_context = None
55+
56+ async def tools (self ):
57+ """
58+ List available tools on the MCP server.
59+ Returns:
60+ List of tools info retrieved from the server.
61+ """
62+ try :
63+ # Call the server to get tools list
64+ response = await self .session .list_tools ()
65+ if not hasattr (response , 'tools' ):
66+ logger .error (f"Invalid tools response: 'tools' attribute not found in { response } " )
67+ return []
68+ tools_list = response .tools
69+ logger .debug (f"Retrieved { len (tools_list )} tools" )
70+
71+ tool_names = [tool .name for tool in tools_list ]
72+ logger .debug (f"Tools available: { tool_names } " )
73+
74+ return tools_list
75+
76+ except Exception as e :
77+ logger .exception (f"Error retrieving tools: { e } " )
78+ return []
79+
80+ async def call_tool (self , tool_name : str , tool_params : Optional [dict ] = None ):
81+ """
82+ Call a specific tool on the MCP server with optional parameters.
83+ Args:
84+ tool_name (str): The name of the tool to call.
85+ tool_params (Optional[dict]): Parameters to pass to the tool call.
86+ Returns:
87+ The result object returned from the MCP server's tool call.
88+ """
89+ if tool_params is None :
90+ tool_params = {}
91+ result = await self .session .call_tool (tool_name , tool_params )
92+ logger .debug (f"Call tool '{ tool_name } ' with params { tool_params } received: { result } " )
93+
94+ return result
0 commit comments