@@ -814,6 +814,7 @@ def __init__(
814814 description : str | None = None ,
815815 connection_timeout : int = 30 ,
816816 invocation_timeout : int = 30 ,
817+ eager_connect : bool = False ,
817818 ):
818819 """
819820 Initialize the MCP tool.
@@ -823,6 +824,9 @@ def __init__(
823824 :param description: Custom description (if None, server description will be used)
824825 :param connection_timeout: Timeout in seconds for server connection
825826 :param invocation_timeout: Default timeout in seconds for tool invocations
827+ :param eager_connect: If True, connect to server during initialization.
828+ If False (default), defer connection until warm_up or first tool use,
829+ whichever comes first.
826830 :raises MCPConnectionError: If connection to the server fails
827831 :raises MCPToolNotFoundError: If no tools are available or the requested tool is not found
828832 :raises TimeoutError: If connection times out
@@ -832,39 +836,27 @@ def __init__(
832836 self ._server_info = server_info
833837 self ._connection_timeout = connection_timeout
834838 self ._invocation_timeout = invocation_timeout
839+ self ._eager_connect = eager_connect
840+ self ._client : MCPClient | None = None
841+ self ._worker : _MCPClientSessionManager | None = None
842+ self ._lock = threading .RLock ()
843+
844+ # don't connect now; initialize permissively
845+ if not eager_connect :
846+ # Permissive placeholder JSON Schema so the Tool is valid
847+ # without discovering the remote schema during validation.
848+ # Tool parameters/schema will be replaced with the correct schema (from the MCP server) on first use.
849+ params = {"type" : "object" , "properties" : {}, "additionalProperties" : True }
850+ super ().__init__ (name = name , description = description or "" , parameters = params , function = self ._invoke_tool )
851+ return
835852
836853 logger .debug (f"TOOL: Initializing MCPTool '{ name } '" )
837854
838855 try :
839- # Create client and spin up a long-lived worker that keeps the
840- # connect/close lifecycle inside one coroutine.
841- self ._client = server_info .create_client ()
842- logger .debug (f"TOOL: Created client for MCPTool '{ name } '" )
843-
844- # The worker starts immediately and blocks here until the connection
845- # is established (or fails), returning the tool list.
846- self ._worker = _MCPClientSessionManager (self ._client , timeout = connection_timeout )
847-
848- tools = self ._worker .tools ()
849- # Handle no tools case
850- if not tools :
851- logger .debug (f"TOOL: No tools found for '{ name } '" )
852- message = "No tools available on server"
853- raise MCPToolNotFoundError (message , tool_name = name )
854-
855- # Find the specified tool
856- tool_dict = {t .name : t for t in tools }
857- logger .debug (f"TOOL: Available tools: { list (tool_dict .keys ())} " )
858-
859- tool_info : types .Tool | None = tool_dict .get (name )
860-
861- if not tool_info :
862- available = list (tool_dict .keys ())
863- logger .debug (f"TOOL: Tool '{ name } ' not found in available tools" )
864- message = f"Tool '{ name } ' not found on server. Available tools: { ', ' .join (available )} "
865- raise MCPToolNotFoundError (message , tool_name = name , available_tools = available )
866-
856+ logger .debug (f"TOOL: Connecting to MCP server for '{ name } '" )
857+ tool_info = self ._connect_and_initialize (name )
867858 logger .debug (f"TOOL: Found tool '{ name } ', initializing Tool parent class" )
859+
868860 # Initialize the parent class
869861 super ().__init__ (
870862 name = name ,
@@ -897,6 +889,36 @@ def __init__(
897889 message = f"Failed to initialize MCPTool '{ name } ': { error_message } "
898890 raise MCPConnectionError (message = message , server_info = server_info , operation = "initialize" ) from e
899891
892+ def _connect_and_initialize (self , tool_name : str ) -> types .Tool :
893+ """
894+ Connect to the MCP server and retrieve the tool schema.
895+
896+ :param tool_name: Name of the tool to look for
897+ :returns: The tool schema for this tool
898+ :raises MCPToolNotFoundError: If the tool is not found on the server
899+ """
900+ client = self ._server_info .create_client ()
901+ worker = _MCPClientSessionManager (client , timeout = self ._connection_timeout )
902+ tools = worker .tools ()
903+
904+ # Handle no tools case
905+ if not tools :
906+ message = "No tools available on server"
907+ raise MCPToolNotFoundError (message , tool_name = tool_name )
908+
909+ # Find the specified tool
910+ tool = next ((t for t in tools if t .name == tool_name ), None )
911+ if tool is None :
912+ available = [t .name for t in tools ]
913+ msg = f"Tool '{ tool_name } ' not found on server. Available tools: { ', ' .join (available )} "
914+ raise MCPToolNotFoundError (msg , tool_name = tool_name , available_tools = available )
915+
916+ # Publish connection
917+ self ._client = client
918+ self ._worker = worker
919+
920+ return tool
921+
900922 def _invoke_tool (self , ** kwargs : Any ) -> str :
901923 """
902924 Synchronous tool invocation.
@@ -906,12 +928,13 @@ def _invoke_tool(self, **kwargs: Any) -> str:
906928 """
907929 logger .debug (f"TOOL: Invoking tool '{ self .name } ' with args: { kwargs } " )
908930 try :
931+ # Connect on first use if eager_connect is turned off
932+ self .warm_up ()
909933
910934 async def invoke ():
911935 logger .debug (f"TOOL: Inside invoke coroutine for '{ self .name } '" )
912- result = await asyncio .wait_for (
913- self ._client .call_tool (self .name , kwargs ), timeout = self ._invocation_timeout
914- )
936+ client = cast (MCPClient , self ._client )
937+ result = await asyncio .wait_for (client .call_tool (self .name , kwargs ), timeout = self ._invocation_timeout )
915938 logger .debug (f"TOOL: Invoke successful for '{ self .name } '" )
916939 return result
917940
@@ -939,7 +962,9 @@ async def ainvoke(self, **kwargs: Any) -> str:
939962 :raises TimeoutError: If the operation times out
940963 """
941964 try :
942- return await asyncio .wait_for (self ._client .call_tool (self .name , kwargs ), timeout = self ._invocation_timeout )
965+ self .warm_up ()
966+ client = cast (MCPClient , self ._client )
967+ return await asyncio .wait_for (client .call_tool (self .name , kwargs ), timeout = self ._invocation_timeout )
943968 except asyncio .TimeoutError as e :
944969 message = f"Tool invocation timed out after { self ._invocation_timeout } seconds"
945970 raise TimeoutError (message ) from e
@@ -949,6 +974,14 @@ async def ainvoke(self, **kwargs: Any) -> str:
949974 message = f"Failed to invoke tool '{ self .name } ' with args: { kwargs } , got error: { e !s} "
950975 raise MCPInvocationError (message , self .name , kwargs ) from e
951976
977+ def warm_up (self ) -> None :
978+ """Connect and fetch the tool schema if eager_connect is turned off."""
979+ with self ._lock :
980+ if self ._client is not None :
981+ return
982+ tool = self ._connect_and_initialize (self .name )
983+ self .parameters = tool .inputSchema
984+
952985 def to_dict (self ) -> dict [str , Any ]:
953986 """
954987 Serializes the MCPTool to a dictionary.
@@ -966,6 +999,7 @@ def to_dict(self) -> dict[str, Any]:
966999 "server_info" : self ._server_info .to_dict (),
9671000 "connection_timeout" : self ._connection_timeout ,
9681001 "invocation_timeout" : self ._invocation_timeout ,
1002+ "eager_connect" : self ._eager_connect ,
9691003 }
9701004 return {
9711005 "type" : generate_qualified_class_name (type (self )),
@@ -998,6 +1032,7 @@ def from_dict(cls, data: dict[str, Any]) -> "Tool":
9981032 # Handle backward compatibility for timeout parameters
9991033 connection_timeout = inner_data .get ("connection_timeout" , 30 )
10001034 invocation_timeout = inner_data .get ("invocation_timeout" , 30 )
1035+ eager_connect = inner_data .get ("eager_connect" , False ) # because False is the default
10011036
10021037 # Create a new MCPTool instance with the deserialized parameters
10031038 # This will establish a new connection to the MCP server
@@ -1007,6 +1042,7 @@ def from_dict(cls, data: dict[str, Any]) -> "Tool":
10071042 server_info = server_info ,
10081043 connection_timeout = connection_timeout ,
10091044 invocation_timeout = invocation_timeout ,
1045+ eager_connect = eager_connect ,
10101046 )
10111047
10121048 def close (self ):
0 commit comments