diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 4bedb4bde76..f0e6753b062 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -24,6 +24,7 @@ from litellm.types.llms.anthropic import ( AnthropicComputerTool, AnthropicHostedTools, AnthropicInputSchema, + AnthropicMcpServerTool, AnthropicMessagesTool, AnthropicMessagesToolChoice, AnthropicSystemMessageContent, @@ -41,6 +42,7 @@ from litellm.types.llms.openai import ( ChatCompletionToolCallChunk, ChatCompletionToolCallFunctionChunk, ChatCompletionToolParam, + OpenAIMcpServerTool, OpenAIWebSearchOptions, ) from litellm.types.utils import CompletionTokensDetailsWrapper @@ -175,8 +177,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): def _map_tool_helper( self, tool: ChatCompletionToolParam - ) -> AllAnthropicToolsValues: + ) -> Tuple[Optional[AllAnthropicToolsValues], Optional[AnthropicMcpServerTool]]: returned_tool: Optional[AllAnthropicToolsValues] = None + mcp_server: Optional[AnthropicMcpServerTool] = None if tool["type"] == "function" or tool["type"] == "custom": _input_schema: dict = tool["function"].get( @@ -239,33 +242,77 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): returned_tool = AnthropicHostedTools( type=tool["type"], name=function_name, **additional_tool_params # type: ignore ) - if returned_tool is None: + elif tool["type"] == "url": # mcp server tool + mcp_server = AnthropicMcpServerTool(**tool) # type: ignore + elif tool["type"] == "mcp": + mcp_server = self._map_openai_mcp_server_tool( + cast(OpenAIMcpServerTool, tool) + ) + if returned_tool is None and mcp_server is None: raise ValueError(f"Unsupported tool type: {tool['type']}") ## check if cache_control is set in the tool _cache_control = tool.get("cache_control", None) _cache_control_function = tool.get("function", {}).get("cache_control", None) - if _cache_control is not None: - returned_tool["cache_control"] = _cache_control - elif _cache_control_function is not None and isinstance( - _cache_control_function, dict - ): - returned_tool["cache_control"] = ChatCompletionCachedContent( - **_cache_control_function # type: ignore + if returned_tool is not None: + if _cache_control is not None: + returned_tool["cache_control"] = _cache_control + elif _cache_control_function is not None and isinstance( + _cache_control_function, dict + ): + returned_tool["cache_control"] = ChatCompletionCachedContent( + **_cache_control_function # type: ignore + ) + + return returned_tool, mcp_server + + def _map_openai_mcp_server_tool( + self, tool: OpenAIMcpServerTool + ) -> AnthropicMcpServerTool: + from litellm.types.llms.anthropic import AnthropicMcpServerToolConfiguration + + allowed_tools = tool.get("allowed_tools", None) + tool_configuration: Optional[AnthropicMcpServerToolConfiguration] = None + if allowed_tools is not None: + tool_configuration = AnthropicMcpServerToolConfiguration( + allowed_tools=tool.get("allowed_tools", None), ) - return returned_tool + headers = tool.get("headers", {}) + authorization_token: Optional[str] = None + if headers is not None: + bearer_token = headers.get("Authorization", None) + if bearer_token is not None: + authorization_token = bearer_token.replace("Bearer ", "") - def _map_tools(self, tools: List) -> List[AllAnthropicToolsValues]: + initial_tool = AnthropicMcpServerTool( + type="url", + url=tool["server_url"], + name=tool["server_label"], + ) + + if tool_configuration is not None: + initial_tool["tool_configuration"] = tool_configuration + if authorization_token is not None: + initial_tool["authorization_token"] = authorization_token + return initial_tool + + def _map_tools( + self, tools: List + ) -> Tuple[List[AllAnthropicToolsValues], List[AnthropicMcpServerTool]]: anthropic_tools = [] + mcp_servers = [] for tool in tools: if "input_schema" in tool: # assume in anthropic format anthropic_tools.append(tool) else: # assume openai tool call - new_tool = self._map_tool_helper(tool) + new_tool, mcp_server_tool = self._map_tool_helper(tool) - anthropic_tools.append(new_tool) - return anthropic_tools + if new_tool is not None: + anthropic_tools.append(new_tool) + if mcp_server_tool is not None: + mcp_servers.append(mcp_server_tool) + return anthropic_tools, mcp_servers def _map_stop_sequences( self, stop: Optional[Union[str, List[str]]] @@ -389,10 +436,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): optional_params["max_tokens"] = value if param == "tools": # check if optional params already has tools - tool_value = self._map_tools(value) + anthropic_tools, mcp_servers = self._map_tools(value) optional_params = self._add_tools_to_optional_params( - optional_params=optional_params, tools=tool_value + optional_params=optional_params, tools=anthropic_tools ) + if mcp_servers: + optional_params["mcp_servers"] = mcp_servers if param == "tool_choice" or param == "parallel_tool_calls": _tool_choice: Optional[ AnthropicMessagesToolChoice @@ -591,7 +640,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): and has_tool_call_blocks(messages) ): if litellm.modify_params: - optional_params["tools"] = self._map_tools( + optional_params["tools"], _ = self._map_tools( add_dummy_tool(custom_llm_provider="anthropic") ) else: diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index cab01cd0d0a..c263d903188 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -12,7 +12,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import ( ) from litellm.llms.base_llm.base_utils import BaseLLMModelInfo from litellm.llms.base_llm.chat.transformation import BaseLLMException -from litellm.types.llms.anthropic import AllAnthropicToolsValues +from litellm.types.llms.anthropic import AllAnthropicToolsValues, AnthropicMcpServerTool from litellm.types.llms.openai import AllMessageValues @@ -51,6 +51,15 @@ class AnthropicModelInfo(BaseLLMModelInfo): file_ids = get_file_ids_from_messages(messages) return len(file_ids) > 0 + def is_mcp_server_used( + self, mcp_servers: Optional[List[AnthropicMcpServerTool]] + ) -> bool: + if mcp_servers is None: + return False + if mcp_servers: + return True + return False + def is_computer_tool_used( self, tools: Optional[List[AllAnthropicToolsValues]] ) -> bool: @@ -92,6 +101,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): prompt_caching_set: bool = False, pdf_used: bool = False, file_id_used: bool = False, + mcp_server_used: bool = False, is_vertex_request: bool = False, user_anthropic_beta_headers: Optional[List[str]] = None, ) -> dict: @@ -105,6 +115,9 @@ class AnthropicModelInfo(BaseLLMModelInfo): if file_id_used: betas.add("files-api-2025-04-14") betas.add("code-execution-2025-05-22") + if mcp_server_used: + betas.add("mcp-client-2025-04-04") + headers = { "anthropic-version": anthropic_version or "2023-06-01", "x-api-key": api_key, @@ -143,6 +156,9 @@ class AnthropicModelInfo(BaseLLMModelInfo): tools = optional_params.get("tools") prompt_caching_set = self.is_cache_control_set(messages=messages) computer_tool_used = self.is_computer_tool_used(tools=tools) + mcp_server_used = self.is_mcp_server_used( + mcp_servers=optional_params.get("mcp_servers") + ) pdf_used = self.is_pdf_used(messages=messages) file_id_used = self.is_file_id_used(messages=messages) user_anthropic_beta_headers = self._get_user_anthropic_beta_headers( @@ -156,6 +172,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): file_id_used=file_id_used, is_vertex_request=optional_params.get("is_vertex_request", False), user_anthropic_beta_headers=user_anthropic_beta_headers, + mcp_server_used=mcp_server_used, ) headers = {**headers, **anthropic_headers} diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index ba22f7ac443..e7d7920769f 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -184,7 +184,9 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): return tools # if claude, convert to anthropic tool and then to databricks tool - anthropic_tools = self._map_tools(tools=tools) + anthropic_tools, _ = self._map_tools( + tools=tools + ) # unclear how mcp tool calling on databricks works databricks_tools = [ cast(DatabricksTool, self.convert_anthropic_tool_to_databricks_tool(tool)) for tool in anthropic_tools diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index 5cd67ff3043..c503240928f 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -72,6 +72,18 @@ AllAnthropicToolsValues = Union[ ] +class AnthropicMcpServerToolConfiguration(TypedDict, total=False): + allowed_tools: Optional[List[str]] + + +class AnthropicMcpServerTool(TypedDict, total=False): + type: Required[Literal["url"]] + url: Required[str] + name: Required[str] + tool_configuration: AnthropicMcpServerToolConfiguration + authorization_token: str + + class AnthropicMessagesTextParam(TypedDict, total=False): type: Required[Literal["text"]] text: Required[str] @@ -216,6 +228,7 @@ class AnthropicMessagesRequestOptionalParams(TypedDict, total=False): tools: Optional[List[Union[AllAnthropicToolsValues, Dict]]] top_k: Optional[int] top_p: Optional[float] + mcp_servers: Optional[List[AnthropicMcpServerTool]] class AnthropicMessagesRequest(AnthropicMessagesRequestOptionalParams, total=False): diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 367f86fd3f4..7cf0474b77e 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -1641,3 +1641,12 @@ class OpenAIRealtimeTurnDetection(TypedDict, total=False): silence_duration_ms: int threshold: int type: str + + +class OpenAIMcpServerTool(TypedDict, total=False): + type: Required[Literal["mcp"]] + server_label: Required[str] + server_url: Required[str] + require_approval: str + allowed_tools: Optional[List[str]] + headers: Optional[Dict[str, str]] diff --git a/tests/llm_translation/test_anthropic_completion.py b/tests/llm_translation/test_anthropic_completion.py index 4385073def9..74677cee26c 100644 --- a/tests/llm_translation/test_anthropic_completion.py +++ b/tests/llm_translation/test_anthropic_completion.py @@ -422,7 +422,7 @@ def test_anthropic_tool_helper(cache_control_location): else: tool["cache_control"] = {"type": "ephemeral"} - tool = AnthropicConfig()._map_tool_helper(tool=tool) + tool, _ = AnthropicConfig()._map_tool_helper(tool=tool) assert tool["cache_control"] == {"type": "ephemeral"} @@ -1273,3 +1273,37 @@ def test_anthropic_text_editor(): assert response is not None +@pytest.mark.parametrize("spec", ["anthropic", "openai"]) +def test_anthropic_mcp_server_tool_use(spec: str): + litellm._turn_on_debug() + + if spec == "anthropic": + tools = [ + { + "type": "url", + "url": "https://mcp.deepwiki.com/mcp", + "name": "deepwiki-mcp", + } + ] + elif spec == "openai": + tools=[ + { + "type": "mcp", + "server_label": "deepwiki", + "server_url": "https://mcp.deepwiki.com/mcp", + "require_approval": "never", + }, + ] + + params = { + "model": "anthropic/claude-sonnet-4-20250514", + "messages": [{"role": "user", "content": "Who won the World Cup in 2022?"}], + "tools": tools + } + + try: + response = litellm.completion(**params) + except litellm.InternalServerError as e: + print(e) + + assert response is not None diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index e86430cdaeb..2a676cf4c8c 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -118,7 +118,7 @@ def test_map_tool_helper(): tool = {"type": "web_search_20250305", "name": "web_search", "max_uses": 5} - result = config._map_tool_helper(tool) + result, _ = config._map_tool_helper(tool) assert result is not None assert result["name"] == "web_search" assert result["max_uses"] == 5