1212import asyncio
1313import json
1414import logging
15- import subprocess
16- import weakref
1715from dataclasses import dataclass , field
1816from enum import Enum
1917from typing import Any , Callable , Dict , List , Optional , Protocol
2018
19+ try :
20+ from mcp .client .session import ClientSession
21+ from mcp .client .stdio import stdio_client , StdioServerParameters
22+ from mcp .client .sse import sse_client
23+ from mcp .types import Tool as MCPTool
24+ HAS_MCP = True
25+ except ImportError :
26+ # Graceful fallback when mcp is not installed
27+ HAS_MCP = False
28+ ClientSession = None
29+ StdioServerParameters = None
30+ stdio_client = None
31+ sse_client = None
32+ MCPTool = None
33+
2134logger = logging .getLogger (__name__ )
2235
2336
@@ -80,108 +93,192 @@ async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
8093
8194
8295class StdioMCPClient :
83- """MCP client using stdio transport."""
96+ """MCP client using stdio transport with proper MCP SDK ."""
8497
8598 def __init__ (self , command : str , args : List [str ]):
99+ if not HAS_MCP :
100+ raise ImportError ("MCP library not available. Install with: pip install mcp" )
101+
86102 self .command = command
87103 self .args = args
88- self .process : Optional [subprocess .Popen ] = None
104+ self .session : Optional [ClientSession ] = None
105+ self ._connected = False
89106
90107 async def connect (self ) -> bool :
91- """Connect via stdio subprocess."""
108+ """Connect via stdio subprocess using official MCP SDK ."""
92109 try :
93- self .process = subprocess .Popen (
94- [self .command ] + self .args ,
95- stdin = subprocess .PIPE ,
96- stdout = subprocess .PIPE ,
97- stderr = subprocess .PIPE ,
98- text = True
110+ # Use official MCP stdio_client instead of raw subprocess
111+ server_params = StdioServerParameters (
112+ command = self .command ,
113+ args = self .args
99114 )
100- # TODO: Implement actual MCP protocol handshake
101- # For now, assume success if process starts
102- return self .process .poll () is None
115+
116+ # Create a new session using stdio_client
117+ self .session = await stdio_client (server_params )
118+
119+ # Initialize the MCP session
120+ await self .session .initialize ()
121+
122+ self ._connected = True
123+ logger .info (f"Connected to MCP stdio server: { self .command } " )
124+ return True
125+
103126 except Exception as e :
104- logger .error (f"Failed to start MCP stdio process: { e } " )
127+ logger .error (f"Failed to connect to MCP stdio server: { e } " )
128+ self ._connected = False
105129 return False
106130
107131 async def disconnect (self ) -> None :
108- """Terminate the subprocess."""
109- if self .process :
110- self .process .terminate ()
132+ """Properly disconnect the MCP session."""
133+ if self .session and self ._connected :
111134 try :
112- self .process .wait (timeout = 5 )
113- except subprocess .TimeoutExpired :
114- self .process .kill ()
115- self .process .wait ()
116- self .process = None
135+ await self .session .close ()
136+ except Exception as e :
137+ logger .warning (f"Error during MCP session close: { e } " )
138+ finally :
139+ self .session = None
140+ self ._connected = False
117141
118142 async def list_tools (self ) -> List [ToolInfo ]:
119- """List tools via MCP protocol."""
120- # TODO: Implement actual MCP tools/list request
121- # For now, return mock data
122- return [
123- ToolInfo (
124- name = "filesystem_read" ,
125- description = "Read file contents" ,
126- input_schema = {"type" : "object" , "properties" : {"path" : {"type" : "string" }}}
127- )
128- ]
143+ """List tools via proper MCP protocol."""
144+ if not self .session or not self ._connected :
145+ return []
146+
147+ try :
148+ result = await self .session .list_tools ()
149+ tools = []
150+
151+ for tool in result .tools :
152+ tools .append (ToolInfo (
153+ name = tool .name ,
154+ description = tool .description or "" ,
155+ input_schema = tool .inputSchema or {}
156+ ))
157+
158+ return tools
159+
160+ except Exception as e :
161+ logger .error (f"Failed to list MCP tools: { e } " )
162+ return []
129163
130164 async def call_tool (self , name : str , arguments : Dict [str , Any ]) -> Any :
131- """Call tool via MCP protocol."""
132- # TODO: Implement actual MCP tools/call request
133- return {"result" : f"Tool { name } called with { arguments } " }
165+ """Call tool via proper MCP protocol."""
166+ if not self .session or not self ._connected :
167+ raise RuntimeError ("MCP session not connected" )
168+
169+ try :
170+ result = await self .session .call_tool (name , arguments )
171+ return result .content
172+
173+ except Exception as e :
174+ logger .error (f"Failed to call MCP tool { name } : { e } " )
175+ raise
134176
135177
136178class SSEMCPClient :
137179 """MCP client using Server-Sent Events transport."""
138180
139181 def __init__ (self , url : str , headers : Optional [Dict [str , str ]] = None ):
182+ if not HAS_MCP :
183+ raise ImportError ("MCP library not available. Install with: pip install mcp" )
184+
140185 self .url = url
141186 self .headers = headers or {}
187+ self .session : Optional [ClientSession ] = None
188+ self ._connected = False
142189
143190 async def connect (self ) -> bool :
144- """Connect via SSE."""
145- # TODO: Implement SSE connection
146- logger .info (f"Connecting to MCP SSE server at { self .url } " )
147- return False # Not implemented yet
191+ """Connect via SSE using official MCP SDK."""
192+ try :
193+ # Use official MCP sse_client
194+ self .session = await sse_client (self .url , headers = self .headers )
195+
196+ # Initialize the MCP session
197+ await self .session .initialize ()
198+
199+ self ._connected = True
200+ logger .info (f"Connected to MCP SSE server at { self .url } " )
201+ return True
202+
203+ except Exception as e :
204+ logger .error (f"Failed to connect to MCP SSE server: { e } " )
205+ self ._connected = False
206+ return False
148207
149208 async def disconnect (self ) -> None :
150- """Disconnect SSE."""
151- pass
209+ """Properly disconnect SSE."""
210+ if self .session and self ._connected :
211+ try :
212+ await self .session .close ()
213+ except Exception as e :
214+ logger .warning (f"Error during MCP SSE session close: { e } " )
215+ finally :
216+ self .session = None
217+ self ._connected = False
152218
153219 async def list_tools (self ) -> List [ToolInfo ]:
154- """List tools via SSE."""
155- return []
220+ """List tools via proper MCP SSE protocol."""
221+ if not self .session or not self ._connected :
222+ return []
223+
224+ try :
225+ result = await self .session .list_tools ()
226+ tools = []
227+
228+ for tool in result .tools :
229+ tools .append (ToolInfo (
230+ name = tool .name ,
231+ description = tool .description or "" ,
232+ input_schema = tool .inputSchema or {}
233+ ))
234+
235+ return tools
236+
237+ except Exception as e :
238+ logger .error (f"Failed to list MCP SSE tools: { e } " )
239+ return []
156240
157241 async def call_tool (self , name : str , arguments : Dict [str , Any ]) -> Any :
158- """Call tool via SSE."""
159- return {}
242+ """Call tool via proper MCP SSE protocol."""
243+ if not self .session or not self ._connected :
244+ raise RuntimeError ("MCP SSE session not connected" )
245+
246+ try :
247+ result = await self .session .call_tool (name , arguments )
248+ return result .content
249+
250+ except Exception as e :
251+ logger .error (f"Failed to call MCP SSE tool { name } : { e } " )
252+ raise
160253
161254
162255class HTTPMCPClient :
163- """MCP client using HTTP transport."""
256+ """MCP client using HTTP transport (not yet supported by MCP SDK) ."""
164257
165258 def __init__ (self , url : str , headers : Optional [Dict [str , str ]] = None ):
259+ if not HAS_MCP :
260+ raise ImportError ("MCP library not available. Install with: pip install mcp" )
261+
166262 self .url = url
167263 self .headers = headers or {}
264+ self .session : Optional [ClientSession ] = None
265+ self ._connected = False
168266
169267 async def connect (self ) -> bool :
170- """Connect via HTTP."""
171- # TODO: Implement HTTP connection
172- logger .info (f"Connecting to MCP HTTP server at { self .url } " )
173- return False # Not implemented yet
268+ """Connect via HTTP (not yet implemented in MCP SDK)."""
269+ logger .warning ("HTTP transport not yet supported by MCP SDK" )
270+ return False
174271
175272 async def disconnect (self ) -> None :
176273 """Disconnect HTTP."""
177274 pass
178275
179276 async def list_tools (self ) -> List [ToolInfo ]:
180- """List tools via HTTP."""
277+ """List tools via HTTP (not yet implemented) ."""
181278 return []
182279
183280 async def call_tool (self , name : str , arguments : Dict [str , Any ]) -> Any :
184- """Call tool via HTTP."""
281+ """Call tool via HTTP (not yet implemented) ."""
185282 return {}
186283
187284
@@ -249,7 +346,7 @@ async def connect_server(self, server_config: Dict[str, Any]) -> MCPServer:
249346 server ._client = client
250347
251348 # Fire connect hooks
252- await self ._fire_connect_hooks (server )
349+ await self ._fire_connect_hooks (server , None ) # TODO: pass actual session context
253350 else :
254351 server .status = MCPStatus .ERROR
255352 server .last_error = "Connection failed"
@@ -276,7 +373,7 @@ async def disconnect_server(self, name: str) -> bool:
276373 await client .disconnect ()
277374
278375 # Fire disconnect hooks
279- await self ._fire_disconnect_hooks (server )
376+ await self ._fire_disconnect_hooks (server , None ) # TODO: pass actual session context
280377
281378 except Exception as e :
282379 logger .exception (f"Error during MCP server { name } disconnect" )
@@ -303,25 +400,19 @@ async def get_server(self, name: str) -> Optional[MCPServer]:
303400 """Get a server by name."""
304401 return _mcp_servers .get (name )
305402
306- async def _fire_connect_hooks (self , server : MCPServer ) -> None :
403+ async def _fire_connect_hooks (self , server : MCPServer , session_context = None ) -> None :
307404 """Fire all registered connect hooks."""
308- # TODO: Get current session context
309- session = None # Placeholder for actual session
310-
311405 for hook in _connect_hooks :
312406 try :
313- await hook (server , session )
407+ await hook (server , session_context )
314408 except Exception as e :
315409 logger .exception (f"Error in MCP connect hook: { e } " )
316410
317- async def _fire_disconnect_hooks (self , server : MCPServer ) -> None :
411+ async def _fire_disconnect_hooks (self , server : MCPServer , session_context = None ) -> None :
318412 """Fire all registered disconnect hooks."""
319- # TODO: Get current session context
320- session = None # Placeholder for actual session
321-
322413 for hook in _disconnect_hooks :
323414 try :
324- await hook (server , session )
415+ await hook (server , session_context )
325416 except Exception as e :
326417 logger .exception (f"Error in MCP disconnect hook: { e } " )
327418
0 commit comments