11"""
2- Module for integrating Model Context Protocol (MCP) servers with Marvin.
3-
4- **Status:** Experimental
2+ Experimental module for integrating Model Context Protocol (MCP) servers with Marvin.
53"""
64
75import asyncio
6+ import os
87import uuid
8+ from contextlib import AsyncExitStack , asynccontextmanager
99from functools import partial
10- from typing import TYPE_CHECKING , Any , Coroutine
10+ from typing import TYPE_CHECKING , Any , AsyncIterator , Coroutine
1111
1212from mcp .types import CallToolResult
13- from pydantic_ai .mcp import MCPServer
13+ from pydantic_ai .mcp import MCPServer , MCPServerStdio
1414from pydantic_ai .messages import ToolCallPart , ToolReturnPart
1515from pydantic_ai .tools import Tool , ToolDefinition
1616
2020from marvin .utilities .logging import get_logger
2121
2222if TYPE_CHECKING :
23+ import marvin .agents .agent
2324 import marvin .engine .orchestrator
2425
2526logger = get_logger (__name__ )
@@ -169,10 +170,9 @@ async def discover_mcp_tools(
169170 # Use a default argument to capture the current value of wrapped_func
170171 # This prevents the closure from capturing the loop variable by reference.
171172 async def async_wrapped_func_fixed (
172- _bound_partial = wrapped_func , # No type hint here for Pydantic inspection
173+ _bound_partial = wrapped_func , # TODO: investigate type hinting here # pyright: ignore
173174 ** kwargs : Any ,
174175 ) -> Any :
175- # Call the captured partial, not the loop variable
176176 return await _bound_partial (** kwargs )
177177
178178 mcp_tools .append (
@@ -185,3 +185,77 @@ async def async_wrapped_func_fixed(
185185 )
186186
187187 return mcp_tools
188+
189+
190+ @asynccontextmanager
191+ async def manage_mcp_servers (
192+ actor : "marvin.agents.agent.Agent | Actor" ,
193+ ) -> AsyncIterator [list ["MCPServer" ]]:
194+ """Context manager to start and stop MCP servers for a given actor."""
195+ from marvin .agents .agent import Agent
196+
197+ logger .debug (f"[manage_mcp_servers] Preparing MCP servers for { actor .name } ..." )
198+ mcp_exit_stack = AsyncExitStack ()
199+ active_servers : list ["MCPServer" ] = []
200+ servers_started = False
201+
202+ if not isinstance (actor , Agent ) or not hasattr (actor , "get_mcp_servers" ):
203+ logger .debug (
204+ f"[manage_mcp_servers] Actor { actor .name } is not an Agent or does not have get_mcp_servers method."
205+ )
206+ yield active_servers
207+ return
208+
209+ servers_to_manage = actor .get_mcp_servers ()
210+
211+ if not servers_to_manage :
212+ logger .debug (
213+ f"[manage_mcp_servers] Actor { actor .name } has no configured MCP servers."
214+ )
215+ yield active_servers
216+ return
217+
218+ logger .debug (
219+ f"[manage_mcp_servers] Found { len (servers_to_manage )} server configurations."
220+ )
221+ for i , server in enumerate (servers_to_manage ):
222+ logger .debug (f"[manage_mcp_servers] Processing server #{ i + 1 } : { server !r} " )
223+ try :
224+ # Set environment variables for stdio servers if not already set
225+ if isinstance (server , MCPServerStdio ) and server .env is None :
226+ logger .debug (
227+ f"[manage_mcp_servers] Server #{ i + 1 } is MCPServerStdio with no env set. Setting env=dict(os.environ)."
228+ )
229+ server .env = dict (os .environ )
230+
231+ logger .debug (
232+ f"[manage_mcp_servers] Entering context for server #{ i + 1 } ..."
233+ )
234+ await mcp_exit_stack .enter_async_context (server )
235+ active_servers .append (server )
236+ logger .debug (
237+ f"[manage_mcp_servers] Context entered successfully for server #{ i + 1 } ."
238+ )
239+ servers_started = True
240+ except Exception as e :
241+ logger .error (
242+ f"[manage_mcp_servers] Failed to start MCP server #{ i + 1 } ({ server !r} ): { e } " ,
243+ exc_info = True ,
244+ )
245+ # Optionally re-raise or handle specific errors? For now, just log.
246+
247+ if servers_started :
248+ logger .debug (
249+ f"[manage_mcp_servers] Yielding control with { len (active_servers )} active servers."
250+ )
251+ else :
252+ logger .debug (
253+ "[manage_mcp_servers] No servers were successfully started, yielding control."
254+ )
255+
256+ try :
257+ yield active_servers
258+ finally :
259+ logger .debug ("[manage_mcp_servers] Cleaning up MCP servers..." )
260+ await mcp_exit_stack .aclose ()
261+ logger .debug ("[manage_mcp_servers] MCP server cleanup complete." )
0 commit comments