diff --git a/litellm/experimental_mcp_client/client.py b/litellm/experimental_mcp_client/client.py index 6ab8033bf9..a87762ca50 100644 --- a/litellm/experimental_mcp_client/client.py +++ b/litellm/experimental_mcp_client/client.py @@ -183,9 +183,18 @@ class MCPClient: elif self.auth_type == MCPAuth.api_key: headers["X-API-Key"] = self._mcp_auth_value - headers["MCP-Protocol-Version"] = self.protocol_version.value + # Handle protocol version - it might be a string or enum + if hasattr(self.protocol_version, 'value'): + # It's an enum + protocol_version_str = self.protocol_version.value + else: + # It's a string + protocol_version_str = str(self.protocol_version) + + headers["MCP-Protocol-Version"] = protocol_version_str return headers + async def list_tools(self) -> List[MCPTool]: """List available tools from the server.""" if not self._session: diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 7589321973..b36c41f38c 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -67,6 +67,34 @@ def _deserialize_env_dict(env_data: Any) -> Optional[Dict[str, str]]: return env_data +def _convert_protocol_version_to_enum(protocol_version: Optional[str | MCPSpecVersionType]) -> MCPSpecVersionType: + """ + Convert string protocol version to MCPSpecVersion enum. + + Args: + protocol_version: String protocol version, enum, or None + + Returns: + MCPSpecVersionType: The enum value + """ + if not protocol_version: + return MCPSpecVersion.jun_2025 # type: ignore + + # If it's already an MCPSpecVersion enum, return it + if isinstance(protocol_version, MCPSpecVersion): + return protocol_version # type: ignore + + # If it's a string, try to match it to enum values + if isinstance(protocol_version, str): + for version in MCPSpecVersion: + if version.value == protocol_version: + return version # type: ignore + + # If no match found, return default + verbose_logger.warning(f"Unknown protocol version '{protocol_version}', using default") + return MCPSpecVersion.jun_2025 # type: ignore + + class MCPServerManager: def __init__(self): self.registry: Dict[str, MCPServer] = {} @@ -231,7 +259,7 @@ class MCPServerManager: server_name=getattr(mcp_server, 'server_name', None), url=mcp_server.url, transport=cast(MCPTransportType, mcp_server.transport), - spec_version=cast(MCPSpecVersionType, mcp_server.spec_version), + spec_version=_convert_protocol_version_to_enum(mcp_server.spec_version), auth_type=cast(MCPAuthType, mcp_server.auth_type), mcp_info=MCPInfo( server_name=mcp_server.server_name or mcp_server.server_id, @@ -354,6 +382,9 @@ class MCPServerManager: """ transport = server.transport or MCPTransport.sse + # Convert protocol version string to enum + protocol_version_enum = _convert_protocol_version_to_enum(protocol_version or server.spec_version) + # Handle stdio transport if transport == MCPTransport.stdio: # For stdio, we need to get the stdio config from the server @@ -372,7 +403,7 @@ class MCPServerManager: auth_value=mcp_auth_header or server.authentication_token, timeout=60.0, stdio_config=stdio_config, - protocol_version=cast(MCPSpecVersionType, protocol_version or server.spec_version), + protocol_version=protocol_version_enum, ) else: # For HTTP/SSE transports @@ -383,7 +414,7 @@ class MCPServerManager: auth_type=server.auth_type, auth_value=mcp_auth_header or server.authentication_token, timeout=60.0, - protocol_version=cast(MCPSpecVersionType, protocol_version or server.spec_version), + protocol_version=protocol_version_enum, ) async def _get_tools_from_server(self, server: MCPServer, mcp_auth_header: Optional[str] = None, mcp_protocol_version: Optional[str] = None) -> List[MCPTool]: @@ -400,6 +431,9 @@ class MCPServerManager: verbose_logger.debug(f"Connecting to url: {server.url}") verbose_logger.info(f"_get_tools_from_server for {server.name}...") + # Use protocol version from request if provided, otherwise use server's default + protocol_version = mcp_protocol_version if mcp_protocol_version else server.spec_version + client = None try: # Use protocol version from request if provided, otherwise use server's default @@ -562,39 +596,32 @@ class MCPServerManager: protocol_version=mcp_protocol_version, ) - ######################################################### - # During MCP Tool Call Hook - # Allow concurrent monitoring and validation during execution - ######################################################### - if litellm_logging_obj: - during_hook_kwargs = { - "name": name, - "arguments": arguments, - "server_name": server_name_from_prefix, - } - - # Start the during hook in a separate task for concurrent execution - during_hook_task = asyncio.create_task( - litellm_logging_obj.async_during_mcp_tool_call_hook( - kwargs=during_hook_kwargs, - request_obj=None, # Will be created in the hook - start_time=start_time, - end_time=start_time, - ) - ) - async with client: # Use the original tool name (without prefix) for the actual call call_tool_params = MCPCallToolRequestParams( name=original_tool_name, arguments=arguments, ) + + # Initialize during_hook_task as None + during_hook_task = None + + # Start during hook if litellm_logging_obj is available + if litellm_logging_obj: + try: + during_hook_task = litellm_logging_obj.async_during_mcp_tool_call_hook( + kwargs=litellm_logging_obj.model_call_details, + start_time=start_time, + ) + except Exception as e: + verbose_logger.warning(f"During hook error (non-blocking): {str(e)}") + result = await client.call_tool(call_tool_params) ######################################################### # Check during hook result if it completed ######################################################### - if litellm_logging_obj and 'during_hook_task' in locals(): + if litellm_logging_obj and during_hook_task is not None: try: during_hook_result = await during_hook_task if during_hook_result and not during_hook_result.get("should_continue", True): diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index 86cb13746a..c4783c6df0 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -23,6 +23,7 @@ router = APIRouter( if MCP_AVAILABLE: from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( global_mcp_server_manager, + _convert_protocol_version_to_enum, ) from litellm.proxy._experimental.mcp_server.server import ( ListMCPToolsRestAPIResponseObject, @@ -174,7 +175,7 @@ if MCP_AVAILABLE: name=request.alias or request.server_name or "", url=request.url, transport=request.transport, - spec_version=request.spec_version, + spec_version=_convert_protocol_version_to_enum(request.spec_version), auth_type=request.auth_type, mcp_info=request.mcp_info, ), @@ -203,7 +204,7 @@ if MCP_AVAILABLE: name=request.alias or request.server_name or "", url=request.url, transport=request.transport, - spec_version=request.spec_version, + spec_version=_convert_protocol_version_to_enum(request.spec_version), auth_type=request.auth_type, mcp_info=request.mcp_info, ), diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 05ff137f55..1be44179b2 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -168,25 +168,30 @@ if MCP_AVAILABLE: """ List all available tools """ - # Get user authentication from context variable - user_api_key_auth, mcp_auth_header, mcp_servers, mcp_server_auth_headers, mcp_protocol_version = get_auth_context() - verbose_logger.debug( - f"MCP list_tools - User API Key Auth from context: {user_api_key_auth}" - ) - verbose_logger.debug( - f"MCP list_tools - MCP servers from context: {mcp_servers}" - ) - verbose_logger.debug( - f"MCP list_tools - MCP server auth headers: {list(mcp_server_auth_headers.keys()) if mcp_server_auth_headers else None}" - ) - # Get mcp_servers from context variable - return await _list_mcp_tools( - user_api_key_auth=user_api_key_auth, - mcp_auth_header=mcp_auth_header, - mcp_servers=mcp_servers, - mcp_server_auth_headers=mcp_server_auth_headers, - mcp_protocol_version=mcp_protocol_version, - ) + try: + # Get user authentication from context variable + user_api_key_auth, mcp_auth_header, mcp_servers, mcp_server_auth_headers, mcp_protocol_version = get_auth_context() + verbose_logger.debug( + f"MCP list_tools - User API Key Auth from context: {user_api_key_auth}" + ) + verbose_logger.debug( + f"MCP list_tools - MCP servers from context: {mcp_servers}" + ) + verbose_logger.debug( + f"MCP list_tools - MCP server auth headers: {list(mcp_server_auth_headers.keys()) if mcp_server_auth_headers else None}" + ) + # Get mcp_servers from context variable + return await _list_mcp_tools( + user_api_key_auth=user_api_key_auth, + mcp_auth_header=mcp_auth_header, + mcp_servers=mcp_servers, + mcp_server_auth_headers=mcp_server_auth_headers, + mcp_protocol_version=mcp_protocol_version, + ) + except Exception as e: + verbose_logger.exception(f"Error in list_tools endpoint: {str(e)}") + # Return empty list instead of failing completely + return [] @server.call_tool() async def mcp_server_tool_call( @@ -362,29 +367,37 @@ if MCP_AVAILABLE: """ if not MCP_AVAILABLE: return [] - - # Get tools from managed MCP servers - managed_tools = await _get_tools_from_mcp_servers( - user_api_key_auth=user_api_key_auth, - mcp_auth_header=mcp_auth_header, - mcp_servers=mcp_servers, - mcp_server_auth_headers=mcp_server_auth_headers, - mcp_protocol_version=mcp_protocol_version, - ) + # Get tools from managed MCP servers with error handling + managed_tools = [] + try: + managed_tools = await _get_tools_from_mcp_servers( + user_api_key_auth=user_api_key_auth, + mcp_auth_header=mcp_auth_header, + mcp_servers=mcp_servers, + mcp_server_auth_headers=mcp_server_auth_headers, + mcp_protocol_version=mcp_protocol_version, + ) + except Exception as e: + verbose_logger.exception(f"Error getting tools from managed MCP servers: {str(e)}") + # Continue with empty managed tools list instead of failing completely # Get tools from local registry - local_tools_raw = global_mcp_tool_registry.list_tools() - - # Convert local tools to MCPTool format local_tools = [] - for tool in local_tools_raw: - # Convert from litellm.types.mcp_server.tool_registry.MCPTool to mcp.types.Tool - mcp_tool = MCPTool( - name=tool.name, - description=tool.description, - inputSchema=tool.input_schema - ) - local_tools.append(mcp_tool) + try: + local_tools_raw = global_mcp_tool_registry.list_tools() + + # Convert local tools to MCPTool format + for tool in local_tools_raw: + # Convert from litellm.types.mcp_server.tool_registry.MCPTool to mcp.types.Tool + mcp_tool = MCPTool( + name=tool.name, + description=tool.description, + inputSchema=tool.input_schema + ) + local_tools.append(mcp_tool) + except Exception as e: + verbose_logger.exception(f"Error getting tools from local registry: {str(e)}") + # Continue with empty local tools list instead of failing completely # Combine all tools all_tools = managed_tools + local_tools