1010from typing import Dict , Any , List , Optional , Union
1111from subprocess import SubprocessError
1212
13- from plexir .core .config_manager import config_manager , ProviderConfig
13+ from plexir .core .config_manager import config_manager , MCPServerConfig
1414from plexir .tools .base import Tool , ToolRegistry
1515from pydantic import BaseModel , Field , ValidationError
1616
@@ -28,7 +28,8 @@ class MCPClient:
2828 A robust JSON-RPC 2.0 client for the Model Context Protocol.
2929 Supports stdio transport, lifecycle management, tools, and resources.
3030 """
31- def __init__ (self , config : ProviderConfig , tool_registry : ToolRegistry ):
31+ def __init__ (self , name : str , config : MCPServerConfig , tool_registry : ToolRegistry ):
32+ self .name = name
3233 self .config = config
3334 self .tool_registry = tool_registry
3435 self .process = None
@@ -40,31 +41,32 @@ def __init__(self, config: ProviderConfig, tool_registry: ToolRegistry):
4041 self .resources : List [Dict [str , Any ]] = []
4142 self .resource_templates : List [Dict [str , Any ]] = []
4243 self .prompts : List [Dict [str , Any ]] = []
43- logger .info (f"MCP Client initialized for { config . name } ." )
44+ logger .info (f"MCP Client initialized for { name } ." )
4445
4546 async def connect (self ):
4647 """Establishes connection and performs MCP handshake."""
47- if not self .config .base_url or not self .config .base_url .startswith ("stdio://" ):
48- logger .warning (f"MCP Client { self .config .name } : Invalid/Missing stdio URL." )
49- return
48+ cmd = self .config .command
49+ args = self .config .args
50+ env_vars = os .environ .copy ()
51+ env_vars .update (self .config .env )
5052
51- command_str = self .config .base_url [len ("stdio://" ):]
52- logger .info (f"Starting MCP server: { command_str } " )
53+ logger .info (f"Starting MCP server '{ self .name } ': { cmd } { args } " )
5354
5455 try :
5556 self .process = await asyncio .create_subprocess_exec (
56- * command_str . split (),
57+ cmd , * args ,
5758 stdin = asyncio .subprocess .PIPE ,
5859 stdout = asyncio .subprocess .PIPE ,
59- stderr = asyncio .subprocess .PIPE
60+ stderr = asyncio .subprocess .PIPE ,
61+ env = env_vars
6062 )
6163 self .running = True
6264 self ._read_task = asyncio .create_task (self ._read_loop ())
6365 self ._stderr_task = asyncio .create_task (self ._read_stderr ())
6466
6567 # --- MCP Handshake ---
6668 # 1. Initialize
67- logger .info (f"MCP { self .config . name } : Sending initialize..." )
69+ logger .info (f"MCP { self .name } : Sending initialize..." )
6870 init_result = await self .send_request ("initialize" , {
6971 "protocolVersion" : "2024-11-05" ,
7072 "capabilities" : {
@@ -75,7 +77,7 @@ async def connect(self):
7577 },
7678 "clientInfo" : {"name" : "Plexir" , "version" : "1.5.0" }
7779 })
78- logger .info (f"MCP { self .config . name } Initialized. Server: { init_result .get ('serverInfo' , 'Unknown' )} " )
80+ logger .info (f"MCP { self .name } Initialized. Server: { init_result .get ('serverInfo' , 'Unknown' )} " )
7981
8082 # 2. Initialized Notification
8183 await self .send_notification ("notifications/initialized" )
@@ -88,10 +90,10 @@ async def connect(self):
8890 )
8991
9092 except FileNotFoundError :
91- logger .error (f"MCP Client { self .config . name } failed: Command not found." )
93+ logger .error (f"MCP Client { self .name } failed: Command ' { cmd } ' not found." )
9294 await self .disconnect ()
9395 except Exception as e :
94- logger .error (f"MCP Connection failed: { e } " )
96+ logger .error (f"MCP Connection for { self . name } failed: { e } " )
9597 await self .disconnect ()
9698
9799 async def refresh_tools (self ):
@@ -100,9 +102,9 @@ async def refresh_tools(self):
100102 result = await self .send_request ("tools/list" )
101103 tools = result .get ("tools" , [])
102104 self ._register_mcp_tools (tools )
103- logger .info (f"MCP { self .config . name } : Registered { len (tools )} tools." )
105+ logger .info (f"MCP { self .name } : Registered { len (tools )} tools." )
104106 except Exception as e :
105- logger .error (f"Failed to list tools for { self .config . name } : { e } " )
107+ logger .error (f"Failed to list tools for { self .name } : { e } " )
106108
107109 async def refresh_resources (self ):
108110 """Fetches resources and templates from the server."""
@@ -115,9 +117,9 @@ async def refresh_resources(self):
115117
116118 if self .resources or self .resource_templates :
117119 self ._register_resource_tool ()
118- logger .info (f"MCP { self .config . name } : Found { len (self .resources )} resources and { len (self .resource_templates )} templates." )
120+ logger .info (f"MCP { self .name } : Found { len (self .resources )} resources and { len (self .resource_templates )} templates." )
119121 except Exception as e :
120- logger .debug (f"MCP { self .config . name } resources/list failed: { e } " )
122+ logger .debug (f"MCP { self .name } resources/list failed: { e } " )
121123
122124 async def refresh_prompts (self ):
123125 """Fetches prompts from the server."""
@@ -126,14 +128,14 @@ async def refresh_prompts(self):
126128 self .prompts = result .get ("prompts" , [])
127129 if self .prompts :
128130 self ._register_prompt_tool ()
129- logger .info (f"MCP { self .config . name } : Found { len (self .prompts )} prompts." )
131+ logger .info (f"MCP { self .name } : Found { len (self .prompts )} prompts." )
130132 except Exception as e :
131- logger .debug (f"MCP { self .config . name } prompts/list failed: { e } " )
133+ logger .debug (f"MCP { self .name } prompts/list failed: { e } " )
132134
133135 def _register_resource_tool (self ):
134136 """Registers a tool that allows the agent to list and read MCP resources."""
135137 client = self
136- server_name = self .config . name
138+ server_name = self .name
137139
138140 class MCPResourceSchema (BaseModel ):
139141 action : str = Field (..., description = "Action: 'list' or 'read'" )
@@ -177,7 +179,7 @@ async def run(self, action: str, uri: Optional[str] = None) -> str:
177179 def _register_prompt_tool (self ):
178180 """Registers a tool that allows the agent to list and use MCP prompts."""
179181 client = self
180- server_name = self .config . name
182+ server_name = self .name
181183
182184 class MCPPromptSchema (BaseModel ):
183185 action : str = Field (..., description = "Action: 'list' or 'get'" )
@@ -261,7 +263,7 @@ async def _write_json(self, data: Dict):
261263 self .process .stdin .write (json_str .encode ())
262264 await self .process .stdin .drain ()
263265 except Exception as e :
264- logger .error (f"Write error { self .config . name } : { e } " )
266+ logger .error (f"Write error { self .name } : { e } " )
265267 raise
266268
267269 async def _read_loop (self ):
@@ -279,7 +281,7 @@ async def _read_loop(self):
279281 try :
280282 message = json .loads (line_str )
281283 except json .JSONDecodeError :
282- logger .warning (f"MCP { self .config . name } Non-JSON output: { line_str } " )
284+ logger .warning (f"MCP { self .name } Non-JSON output: { line_str } " )
283285 continue
284286
285287 if "id" in message :
@@ -303,18 +305,18 @@ async def _read_loop(self):
303305 # logger.debug(f"MCP Notification: {method}")
304306
305307 except Exception as e :
306- logger .error (f"Read loop error { self .config . name } : { e } " )
308+ logger .error (f"Read loop error { self .name } : { e } " )
307309 break
308310
309- logger .info (f"MCP { self .config . name } read loop ended." )
311+ logger .info (f"MCP { self .name } read loop ended." )
310312 await self .disconnect ()
311313
312314 async def _read_stderr (self ):
313315 """Logs stderr from the server process."""
314316 while self .running and self .process .stderr :
315317 line = await self .process .stderr .readline ()
316318 if not line : break
317- logger .warning (f"MCP STDERR [{ self .config . name } ]: { line .decode ().strip ()} " )
319+ logger .warning (f"MCP STDERR [{ self .name } ]: { line .decode ().strip ()} " )
318320
319321 def _register_mcp_tools (self , tool_definitions : List [Dict [str , Any ]]):
320322 """Dynamically creates Tool classes for MCP tools."""
@@ -393,4 +395,4 @@ async def disconnect(self):
393395 await self .process .wait ()
394396 except Exception :
395397 pass
396- logger .info (f"MCP Client { self .config . name } disconnected." )
398+ logger .info (f"MCP Client { self .name } disconnected." )
0 commit comments