From 93836fa84afcf7ccf953bad15251aa63de052a3f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 15:37:24 -0700 Subject: [PATCH 01/40] sample mcp server --- .../proxy/_experimental/mcp_server/main.py | 96 +++++++++++ tests/pass_through_tests/test_mcp_client.py | 160 ++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 litellm/proxy/_experimental/mcp_server/main.py create mode 100644 tests/pass_through_tests/test_mcp_client.py diff --git a/litellm/proxy/_experimental/mcp_server/main.py b/litellm/proxy/_experimental/mcp_server/main.py new file mode 100644 index 00000000000..9ce45cfa5c7 --- /dev/null +++ b/litellm/proxy/_experimental/mcp_server/main.py @@ -0,0 +1,96 @@ +# from typing import Any +# import httpx +# from mcp.server.fastmcp import FastMCP + +# # Initialize FastMCP server +# mcp = FastMCP("weather") + +# # Constants +# NWS_API_BASE = "https://api.weather.gov" +# USER_AGENT = "weather-app/1.0" + +# async def make_nws_request(url: str) -> dict[str, Any] | None: +# """Make a request to the NWS API with proper error handling.""" +# headers = { +# "User-Agent": USER_AGENT, +# "Accept": "application/geo+json" +# } +# async with httpx.AsyncClient() as client: +# try: +# response = await client.get(url, headers=headers, timeout=30.0) +# response.raise_for_status() +# return response.json() +# except Exception: +# return None + +# def format_alert(feature: dict) -> str: +# """Format an alert feature into a readable string.""" +# props = feature["properties"] +# return f""" +# Event: {props.get('event', 'Unknown')} +# Area: {props.get('areaDesc', 'Unknown')} +# Severity: {props.get('severity', 'Unknown')} +# Description: {props.get('description', 'No description available')} +# Instructions: {props.get('instruction', 'No specific instructions provided')} +# """ + +# @mcp.tool() +# async def get_alerts(state: str) -> str: +# """Get weather alerts for a US state. + +# Args: +# state: Two-letter US state code (e.g. CA, NY) +# """ +# url = f"{NWS_API_BASE}/alerts/active/area/{state}" +# data = await make_nws_request(url) + +# if not data or "features" not in data: +# return "Unable to fetch alerts or no alerts found." + +# if not data["features"]: +# return "No active alerts for this state." + +# alerts = [format_alert(feature) for feature in data["features"]] +# return "\n---\n".join(alerts) + +# @mcp.tool() +# async def get_forecast(latitude: float, longitude: float) -> str: +# """Get weather forecast for a location. + +# Args: +# latitude: Latitude of the location +# longitude: Longitude of the location +# """ +# # First get the forecast grid endpoint +# points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}" +# points_data = await make_nws_request(points_url) + +# if not points_data: +# return "Unable to fetch forecast data for this location." + +# # Get the forecast URL from the points response +# forecast_url = points_data["properties"]["forecast"] +# forecast_data = await make_nws_request(forecast_url) + +# if not forecast_data: +# return "Unable to fetch detailed forecast." + +# # Format the periods into a readable forecast +# periods = forecast_data["properties"]["periods"] +# forecasts = [] +# for period in periods[:5]: # Only show next 5 periods +# forecast = f""" +# {period['name']}: +# Temperature: {period['temperature']}°{period['temperatureUnit']} +# Wind: {period['windSpeed']} {period['windDirection']} +# Forecast: {period['detailedForecast']} +# """ +# forecasts.append(forecast) + +# return "\n---\n".join(forecasts) + + +# if __name__ == "__main__": +# # Initialize and run the server +# print("Starting server...") +# mcp.run(transport='stdio') diff --git a/tests/pass_through_tests/test_mcp_client.py b/tests/pass_through_tests/test_mcp_client.py new file mode 100644 index 00000000000..326b5471ad2 --- /dev/null +++ b/tests/pass_through_tests/test_mcp_client.py @@ -0,0 +1,160 @@ +# import asyncio +# from typing import Optional +# from contextlib import AsyncExitStack + +# from mcp import ClientSession, StdioServerParameters +# from mcp.client.stdio import stdio_client +# from mcp.client.websocket import websocket_client + +# from anthropic import Anthropic +# from dotenv import load_dotenv + +# load_dotenv() # load environment variables from .env + +# class MCPClient: +# def __init__(self): +# # Initialize session and client objects +# self.session: Optional[ClientSession] = None +# self.exit_stack = AsyncExitStack() +# self.anthropic = Anthropic() +# # methods will go here + +# async def connect_to_server(self, server_path: str): +# """Connect to an MCP server + +# Args: +# server_path: Either a path to a server script (.py or .js) or a websocket endpoint URL +# """ +# # Check if the server_path is a URL (endpoint) or a file path +# if server_path.startswith(('http://', 'https://', 'ws://', 'wss://')): +# # Connect to endpoint +# websocket = await self.exit_stack.enter_async_context(websocket_client(server_path)) +# self.session = await self.exit_stack.enter_async_context(ClientSession(websocket.receive, websocket.send)) +# else: +# # Connect to local script (existing functionality) +# is_python = server_path.endswith('.py') +# is_js = server_path.endswith('.js') +# if not (is_python or is_js): +# raise ValueError("Server script must be a .py or .js file") + +# command = "python" if is_python else "node" +# server_params = StdioServerParameters( +# command=command, +# args=[server_path], +# env=None +# ) + +# stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) +# self.stdio, self.write = stdio_transport +# self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) + +# await self.session.initialize() + +# # List available tools +# response = await self.session.list_tools() +# tools = response.tools +# print("\nConnected to server with tools:", [tool.name for tool in tools]) + +# async def process_query(self, query: str) -> str: +# """Process a query using Claude and available tools""" +# messages = [ +# { +# "role": "user", +# "content": query +# } +# ] + +# response = await self.session.list_tools() +# available_tools = [{ +# "name": tool.name, +# "description": tool.description, +# "input_schema": tool.inputSchema +# } for tool in response.tools] + +# # Initial Claude API call +# response = self.anthropic.messages.create( +# model="claude-3-5-sonnet-20241022", +# max_tokens=1000, +# messages=messages, +# tools=available_tools +# ) + +# # Process response and handle tool calls +# final_text = [] + +# assistant_message_content = [] +# for content in response.content: +# if content.type == 'text': +# final_text.append(content.text) +# assistant_message_content.append(content) +# elif content.type == 'tool_use': +# tool_name = content.name +# tool_args = content.input + +# # Execute tool call +# result = await self.session.call_tool(tool_name, tool_args) +# final_text.append(f"[Calling tool {tool_name} with args {tool_args}]") + +# assistant_message_content.append(content) +# messages.append({ +# "role": "assistant", +# "content": assistant_message_content +# }) +# messages.append({ +# "role": "user", +# "content": [ +# { +# "type": "tool_result", +# "tool_use_id": content.id, +# "content": result.content +# } +# ] +# }) + +# # Get next response from Claude +# response = self.anthropic.messages.create( +# model="claude-3-5-sonnet-20241022", +# max_tokens=1000, +# messages=messages, +# tools=available_tools +# ) + +# final_text.append(response.content[0].text) + +# return "\n".join(final_text) +# async def chat_loop(self): +# """Run an interactive chat loop""" +# print("\nMCP Client Started!") +# print("Type your queries or 'quit' to exit.") + +# while True: +# try: +# query = input("\nQuery: ").strip() + +# if query.lower() == 'quit': +# break + +# response = await self.process_query(query) +# print("\n" + response) + +# except Exception as e: +# print(f"\nError: {str(e)}") + +# async def cleanup(self): +# """Clean up resources""" +# await self.exit_stack.aclose() +# async def main(): +# if len(sys.argv) < 2: +# print("Usage: python client.py ") +# sys.exit(1) + +# client = MCPClient() +# try: +# await client.connect_to_server(sys.argv[1]) +# await client.chat_loop() +# finally: +# await client.cleanup() + +# if __name__ == "__main__": +# import sys +# asyncio.run(main()) From 9f22cf729a8423b2470b4ba19980349c40e1bee8 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 17:16:55 -0700 Subject: [PATCH 02/40] mock config for MCP tools --- litellm/proxy/proxy_config.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index 0877a02a74d..ee969180c6b 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -9,3 +9,29 @@ model_list: litellm_settings: callbacks: ["custom_prompt_management.x42_prompt_management"] + +mcp_tools: + - name: "get_current_time" + description: "Get the current time" + input_schema: { + type: object + properties: + format: + type: string + description: "The format of the time to return" + enum: + - "short" + } + handler: "get_current_time" + - name: "get_current_date" + description: "Get the current date" + input_schema: { + type: object + properties: + format: + type: string + description: "The format of the date to return" + enum: + - "short" + } + handler: "get_current_date" From 3fccf5fbef784181c3f9af041fc5441e772a678c Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 17:18:28 -0700 Subject: [PATCH 03/40] mock langchain MCP interface --- tests/pass_through_tests/test_mcp_routes.py | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/pass_through_tests/test_mcp_routes.py diff --git a/tests/pass_through_tests/test_mcp_routes.py b/tests/pass_through_tests/test_mcp_routes.py new file mode 100644 index 00000000000..69ae1b85227 --- /dev/null +++ b/tests/pass_through_tests/test_mcp_routes.py @@ -0,0 +1,31 @@ +# Create server parameters for stdio connection +import asyncio +import os + +from langchain_mcp_adapters.tools import load_mcp_tools +from langchain_openai import ChatOpenAI +from langgraph.prebuilt import create_react_agent +from mcp import ClientSession +from mcp.client.sse import sse_client + + +async def main(): + model = ChatOpenAI(model="gpt-4o", api_key="sk-12") + + async with sse_client(url="http://localhost:8000/mcp/") as (read, write): + async with ClientSession(read, write) as session: + # Initialize the connection + await session.initialize() + + # Get tools + tools = await load_mcp_tools(session) + print(tools) + + # # Create and run the agent + # agent = create_react_agent(model, tools) + # agent_response = await agent.ainvoke({"messages": "what's (3 + 5) x 12?"}) + + +# Run the async function +if __name__ == "__main__": + asyncio.run(main()) From 5377daeede1e630c5da4c6d1b90e701606929142 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 17:22:12 -0700 Subject: [PATCH 04/40] add MCPToolRegistry --- .../_experimental/mcp_server/tool_registry.py | 104 ++++++++++++++++++ litellm/types/mcp_server/tool_registry.py | 10 ++ 2 files changed, 114 insertions(+) create mode 100644 litellm/proxy/_experimental/mcp_server/tool_registry.py create mode 100644 litellm/types/mcp_server/tool_registry.py diff --git a/litellm/proxy/_experimental/mcp_server/tool_registry.py b/litellm/proxy/_experimental/mcp_server/tool_registry.py new file mode 100644 index 00000000000..b99e37cee84 --- /dev/null +++ b/litellm/proxy/_experimental/mcp_server/tool_registry.py @@ -0,0 +1,104 @@ +from typing import Any, Callable, Dict, List, Optional + +from pydantic import BaseModel + +from litellm._logging import verbose_logger +from litellm.types.mcp_server.tool_registry import MCPTool + + +class MCPToolRegistry: + """ + A registry for managing MCP tools + """ + + def __init__(self): + # Registry to store all registered tools + self.tools: Dict[str, MCPTool] = {} + + def register_tool( + self, + name: str, + description: str, + input_schema: Dict[str, Any], + handler: Callable, + ) -> None: + """ + Register a new tool in the registry + """ + self.tools[name] = MCPTool( + name=name, + description=description, + input_schema=input_schema, + handler=handler, + ) + + def get_tool(self, name: str) -> Optional[MCPTool]: + """ + Get a tool from the registry by name + """ + return self.tools.get(name) + + def list_tools(self) -> List[MCPTool]: + """ + List all registered tools + """ + return list(self.tools.values()) + + def load_tools_from_config(self, config: Dict[str, Any]) -> None: + """ + Load and register tools from the proxy config + + Args: + config: The loaded proxy configuration + """ + if "mcp_tools" not in config: + return + + # Import handlers dynamically + import importlib + import sys + + for tool_config in config["mcp_tools"]: + name = tool_config.get("name") + description = tool_config.get("description") + input_schema = tool_config.get("input_schema", {}) + handler_name = tool_config.get("handler") + + if not all([name, description, handler_name]): + continue + + # Try to resolve the handler + # First check if it's a module path (e.g., "module.submodule.function") + if "." in handler_name: + module_path, func_name = handler_name.rsplit(".", 1) + try: + module = importlib.import_module(module_path) + handler = getattr(module, func_name) + except (ImportError, AttributeError): + verbose_logger.warning( + f"Warning: Could not load handler {handler_name} for tool {name}" + ) + continue + else: + # Check if it's in the global namespace + handler = globals().get(handler_name) + if handler is None: + # Check if it's in sys.modules + for module_name, module in sys.modules.items(): + if hasattr(module, handler_name): + handler = getattr(module, handler_name) + break + + if handler is None: + verbose_logger.warning( + f"Warning: Could not find handler {handler_name} for tool {name}" + ) + continue + + # Register the tool + self.register_tool( + name=name, + description=description, + input_schema=input_schema, + handler=handler, + ) diff --git a/litellm/types/mcp_server/tool_registry.py b/litellm/types/mcp_server/tool_registry.py new file mode 100644 index 00000000000..7b306ea8be7 --- /dev/null +++ b/litellm/types/mcp_server/tool_registry.py @@ -0,0 +1,10 @@ +from typing import Any, Callable, Dict + +from pydantic import BaseModel + + +class MCPTool(BaseModel): + name: str + description: str + input_schema: Dict[str, Any] + handler: Callable From 3dcba108ca019bbaa71ad43e577eaf222a2fa604 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 17:36:17 -0700 Subject: [PATCH 05/40] load load_tools_from_config --- litellm/types/mcp_server/tool_registry.py | 30 ++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/litellm/types/mcp_server/tool_registry.py b/litellm/types/mcp_server/tool_registry.py index 7b306ea8be7..266159343df 100644 --- a/litellm/types/mcp_server/tool_registry.py +++ b/litellm/types/mcp_server/tool_registry.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List, Optional from pydantic import BaseModel @@ -8,3 +8,31 @@ class MCPTool(BaseModel): description: str input_schema: Dict[str, Any] handler: Callable + + +class ToolSchema(BaseModel): + name: str + description: str + inputSchema: Dict[str, Any] + + +class ListToolsResponse(BaseModel): + tools: List[ToolSchema] + nextCursor: Optional[str] = None + _meta: Optional[Dict[str, Any]] = None + + +class CallToolRequest(BaseModel): + method: str = "tools/call" + params: Dict[str, Any] + + +class ContentItem(BaseModel): + type: str + text: Optional[str] = None + + +class CallToolResponse(BaseModel): + content: List[ContentItem] + isError: Optional[bool] = None + _meta: Optional[Dict[str, Any]] = None From 89cc404a575b5f6fbf70297f9b3b59ebc08fe6b6 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 17:51:03 -0700 Subject: [PATCH 06/40] add mcp tool registry --- .../_experimental/mcp_server/tool_registry.py | 67 +++++++++---------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/tool_registry.py b/litellm/proxy/_experimental/mcp_server/tool_registry.py index b99e37cee84..c08b7979683 100644 --- a/litellm/proxy/_experimental/mcp_server/tool_registry.py +++ b/litellm/proxy/_experimental/mcp_server/tool_registry.py @@ -1,8 +1,8 @@ +import json from typing import Any, Callable, Dict, List, Optional -from pydantic import BaseModel - from litellm._logging import verbose_logger +from litellm.proxy.types_utils.utils import get_instance_fn from litellm.types.mcp_server.tool_registry import MCPTool @@ -31,6 +31,7 @@ class MCPToolRegistry: input_schema=input_schema, handler=handler, ) + verbose_logger.debug(f"Registered tool: {name}") def get_tool(self, name: str) -> Optional[MCPTool]: """ @@ -44,21 +45,24 @@ class MCPToolRegistry: """ return list(self.tools.values()) - def load_tools_from_config(self, config: Dict[str, Any]) -> None: + def load_tools_from_config( + self, mcp_tools_config: Optional[Dict[str, Any]] = None + ) -> None: """ Load and register tools from the proxy config Args: - config: The loaded proxy configuration + mcp_tools_config: The mcp_tools config from the proxy config """ - if "mcp_tools" not in config: - return + if mcp_tools_config is None: + raise ValueError( + "mcp_tools_config is required, please set `mcp_tools` in your proxy config" + ) - # Import handlers dynamically - import importlib - import sys + for tool_config in mcp_tools_config: + if not isinstance(tool_config, dict): + raise ValueError("mcp_tools_config must be a list of dictionaries") - for tool_config in config["mcp_tools"]: name = tool_config.get("name") description = tool_config.get("description") input_schema = tool_config.get("input_schema", {}) @@ -69,36 +73,31 @@ class MCPToolRegistry: # Try to resolve the handler # First check if it's a module path (e.g., "module.submodule.function") - if "." in handler_name: - module_path, func_name = handler_name.rsplit(".", 1) - try: - module = importlib.import_module(module_path) - handler = getattr(module, func_name) - except (ImportError, AttributeError): - verbose_logger.warning( - f"Warning: Could not load handler {handler_name} for tool {name}" - ) - continue - else: - # Check if it's in the global namespace - handler = globals().get(handler_name) - if handler is None: - # Check if it's in sys.modules - for module_name, module in sys.modules.items(): - if hasattr(module, handler_name): - handler = getattr(module, handler_name) - break + if handler_name is None: + raise ValueError(f"handler is required for tool {name}") + handler = get_instance_fn(handler_name) - if handler is None: - verbose_logger.warning( - f"Warning: Could not find handler {handler_name} for tool {name}" - ) - continue + if handler is None: + verbose_logger.warning( + f"Warning: Could not find handler {handler_name} for tool {name}" + ) + continue # Register the tool + if name is None: + raise ValueError(f"name is required for tool {name}") + if description is None: + raise ValueError(f"description is required for tool {name}") + self.register_tool( name=name, description=description, input_schema=input_schema, handler=handler, ) + verbose_logger.debug( + "all registered tools: %s", json.dumps(self.tools, indent=4, default=str) + ) + + +global_mcp_tool_registry = MCPToolRegistry() From 45ee1182aead920be965d0221681b7fb29316d62 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 17:53:20 -0700 Subject: [PATCH 07/40] example mcp tools --- litellm/proxy/mcp_tools.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 litellm/proxy/mcp_tools.py diff --git a/litellm/proxy/mcp_tools.py b/litellm/proxy/mcp_tools.py new file mode 100644 index 00000000000..eb63e6b43bc --- /dev/null +++ b/litellm/proxy/mcp_tools.py @@ -0,0 +1,35 @@ +from typing import Any, Dict, Optional + + +def get_current_time(params: Optional[Dict[str, Any]] = None) -> str: + """ + Get the current time (hardcoded sample implementation) + + Args: + params: Optional dictionary with parameters + - format: The format of the time to return (e.g., "short") + + Returns: + A string representing the current time + """ + # Hardcoded time value for sample implementation + if params and params.get("format") == "short": + return "10:30 AM" + return "10:30:45 AM" + + +def get_current_date(params: Optional[Dict[str, Any]] = None) -> str: + """ + Get the current date (hardcoded sample implementation) + + Args: + params: Optional dictionary with parameters + - format: The format of the date to return (e.g., "short") + + Returns: + A string representing the current date + """ + # Hardcoded date value for sample implementation + if params and params.get("format") == "short": + return "Oct 15" + return "October 15, 2023" From dc33cfa0740a2e84858ad5702ca1b262fb8690ea Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 17:53:37 -0700 Subject: [PATCH 08/40] init global_mcp_tool_registry --- litellm/proxy/proxy_server.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index ae1c8d18af5..390c215ce65 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -126,6 +126,9 @@ from litellm.litellm_core_utils.core_helpers import ( from litellm.litellm_core_utils.credential_accessor import CredentialAccessor from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler +from litellm.proxy._experimental.mcp_server.tool_registry import ( + global_mcp_tool_registry, +) from litellm.proxy._types import * from litellm.proxy.analytics_endpoints.analytics_endpoints import ( router as analytics_router, @@ -2153,6 +2156,11 @@ class ProxyConfig: all_guardrails=guardrails_v2, config_file_path=config_file_path ) + ## MCP TOOLS + mcp_tools_config = config.get("mcp_tools", None) + if mcp_tools_config: + global_mcp_tool_registry.load_tools_from_config(mcp_tools_config) + ## CREDENTIALS credential_list_dict = self.load_credential_list(config=config) litellm.credential_list = credential_list_dict From f44a5a86dba022bbf96ec4aa03da7b39750a3a0d Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 17:55:43 -0700 Subject: [PATCH 09/40] litellm mcp routes --- .../_experimental/mcp_server/mcp_routes.py | 74 +++++++++++++++++++ litellm/proxy/proxy_config.yaml | 34 +++++---- 2 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 litellm/proxy/_experimental/mcp_server/mcp_routes.py diff --git a/litellm/proxy/_experimental/mcp_server/mcp_routes.py b/litellm/proxy/_experimental/mcp_server/mcp_routes.py new file mode 100644 index 00000000000..1885146488e --- /dev/null +++ b/litellm/proxy/_experimental/mcp_server/mcp_routes.py @@ -0,0 +1,74 @@ +""" +LiteLLM MCP Server Routes +""" + +from typing import Optional + +from fastapi import APIRouter, HTTPException + +router = APIRouter( + prefix="/mcp", + tags=["mcp"], +) + + +from litellm.proxy._experimental.mcp_server.tool_registry import ( + global_mcp_tool_registry, +) +from litellm.types.mcp_server.tool_registry import * + +router = APIRouter() + + +@router.get("/tools/list", response_model=ListToolsResponse) +async def list_tools(cursor: Optional[str] = None): + """ + List all available tools + """ + tools = [] + for tool in global_mcp_tool_registry.list_tools(): + tools.append( + ToolSchema( + name=tool.name, + description=tool.description, + inputSchema=tool.input_schema, + ) + ) + + return ListToolsResponse(tools=tools) + + +@router.post("/tools/call", response_model=CallToolResponse) +async def call_tool(request: CallToolRequest): + """ + Call a specific tool with the provided arguments + """ + if request.method != "tools/call": + raise HTTPException(status_code=400, detail="Invalid method") + + if "name" not in request.params: + raise HTTPException(status_code=400, detail="Tool name is required") + + tool_name = request.params["name"] + arguments = request.params.get("arguments", {}) + + tool = global_mcp_tool_registry.get_tool(tool_name) + if not tool: + raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found") + + try: + result = tool.handler(**arguments) + + # Convert result to text content + if isinstance(result, str): + content = [ContentItem(type="text", text=result)] + elif isinstance(result, dict): + content = [ContentItem(type="text", text=str(result))] + else: + content = [ContentItem(type="text", text=str(result))] + + return CallToolResponse(content=content) + except Exception as e: + return CallToolResponse( + content=[ContentItem(type="text", text=f"Error: {str(e)}")], isError=True + ) diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index ee969180c6b..bca2b96cb49 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -14,24 +14,26 @@ mcp_tools: - name: "get_current_time" description: "Get the current time" input_schema: { - type: object - properties: - format: - type: string - description: "The format of the time to return" - enum: - - "short" + "type": "object", + "properties": { + "format": { + "type": "string", + "description": "The format of the time to return", + "enum": ["short"] + } + } } - handler: "get_current_time" + handler: "mcp_tools.get_current_time" - name: "get_current_date" description: "Get the current date" input_schema: { - type: object - properties: - format: - type: string - description: "The format of the date to return" - enum: - - "short" + "type": "object", + "properties": { + "format": { + "type": "string", + "description": "The format of the date to return", + "enum": ["short"] + } + } } - handler: "get_current_date" + handler: "mcp_tools.get_current_date" From 8909e24efccb6f12c051d0ca77a8d5aec48a3241 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 17:58:33 -0700 Subject: [PATCH 10/40] add mcp routes --- litellm/proxy/_experimental/mcp_server/mcp_routes.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_routes.py b/litellm/proxy/_experimental/mcp_server/mcp_routes.py index 1885146488e..7df0dee435c 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_routes.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_routes.py @@ -17,7 +17,15 @@ from litellm.proxy._experimental.mcp_server.tool_registry import ( ) from litellm.types.mcp_server.tool_registry import * -router = APIRouter() +router = APIRouter( + prefix="/mcp", + tags=["MCP"], +) + + +@router.get("/") +async def root(): + return {"message": "MCP Server is running"} @router.get("/tools/list", response_model=ListToolsResponse) From 08d6ea37ef557fce29ab61bfbc179b172abc81ca Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 17:59:19 -0700 Subject: [PATCH 11/40] remove stale file --- .../proxy/_experimental/mcp_server/main.py | 96 ------------------- 1 file changed, 96 deletions(-) delete mode 100644 litellm/proxy/_experimental/mcp_server/main.py diff --git a/litellm/proxy/_experimental/mcp_server/main.py b/litellm/proxy/_experimental/mcp_server/main.py deleted file mode 100644 index 9ce45cfa5c7..00000000000 --- a/litellm/proxy/_experimental/mcp_server/main.py +++ /dev/null @@ -1,96 +0,0 @@ -# from typing import Any -# import httpx -# from mcp.server.fastmcp import FastMCP - -# # Initialize FastMCP server -# mcp = FastMCP("weather") - -# # Constants -# NWS_API_BASE = "https://api.weather.gov" -# USER_AGENT = "weather-app/1.0" - -# async def make_nws_request(url: str) -> dict[str, Any] | None: -# """Make a request to the NWS API with proper error handling.""" -# headers = { -# "User-Agent": USER_AGENT, -# "Accept": "application/geo+json" -# } -# async with httpx.AsyncClient() as client: -# try: -# response = await client.get(url, headers=headers, timeout=30.0) -# response.raise_for_status() -# return response.json() -# except Exception: -# return None - -# def format_alert(feature: dict) -> str: -# """Format an alert feature into a readable string.""" -# props = feature["properties"] -# return f""" -# Event: {props.get('event', 'Unknown')} -# Area: {props.get('areaDesc', 'Unknown')} -# Severity: {props.get('severity', 'Unknown')} -# Description: {props.get('description', 'No description available')} -# Instructions: {props.get('instruction', 'No specific instructions provided')} -# """ - -# @mcp.tool() -# async def get_alerts(state: str) -> str: -# """Get weather alerts for a US state. - -# Args: -# state: Two-letter US state code (e.g. CA, NY) -# """ -# url = f"{NWS_API_BASE}/alerts/active/area/{state}" -# data = await make_nws_request(url) - -# if not data or "features" not in data: -# return "Unable to fetch alerts or no alerts found." - -# if not data["features"]: -# return "No active alerts for this state." - -# alerts = [format_alert(feature) for feature in data["features"]] -# return "\n---\n".join(alerts) - -# @mcp.tool() -# async def get_forecast(latitude: float, longitude: float) -> str: -# """Get weather forecast for a location. - -# Args: -# latitude: Latitude of the location -# longitude: Longitude of the location -# """ -# # First get the forecast grid endpoint -# points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}" -# points_data = await make_nws_request(points_url) - -# if not points_data: -# return "Unable to fetch forecast data for this location." - -# # Get the forecast URL from the points response -# forecast_url = points_data["properties"]["forecast"] -# forecast_data = await make_nws_request(forecast_url) - -# if not forecast_data: -# return "Unable to fetch detailed forecast." - -# # Format the periods into a readable forecast -# periods = forecast_data["properties"]["periods"] -# forecasts = [] -# for period in periods[:5]: # Only show next 5 periods -# forecast = f""" -# {period['name']}: -# Temperature: {period['temperature']}°{period['temperatureUnit']} -# Wind: {period['windSpeed']} {period['windDirection']} -# Forecast: {period['detailedForecast']} -# """ -# forecasts.append(forecast) - -# return "\n---\n".join(forecasts) - - -# if __name__ == "__main__": -# # Initialize and run the server -# print("Starting server...") -# mcp.run(transport='stdio') From 0e2838ab4f3aedd31fbeb80f6af0d700318c2561 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 18:00:23 -0700 Subject: [PATCH 12/40] remove stale file --- tests/pass_through_tests/test_mcp_client.py | 160 -------------------- 1 file changed, 160 deletions(-) delete mode 100644 tests/pass_through_tests/test_mcp_client.py diff --git a/tests/pass_through_tests/test_mcp_client.py b/tests/pass_through_tests/test_mcp_client.py deleted file mode 100644 index 326b5471ad2..00000000000 --- a/tests/pass_through_tests/test_mcp_client.py +++ /dev/null @@ -1,160 +0,0 @@ -# import asyncio -# from typing import Optional -# from contextlib import AsyncExitStack - -# from mcp import ClientSession, StdioServerParameters -# from mcp.client.stdio import stdio_client -# from mcp.client.websocket import websocket_client - -# from anthropic import Anthropic -# from dotenv import load_dotenv - -# load_dotenv() # load environment variables from .env - -# class MCPClient: -# def __init__(self): -# # Initialize session and client objects -# self.session: Optional[ClientSession] = None -# self.exit_stack = AsyncExitStack() -# self.anthropic = Anthropic() -# # methods will go here - -# async def connect_to_server(self, server_path: str): -# """Connect to an MCP server - -# Args: -# server_path: Either a path to a server script (.py or .js) or a websocket endpoint URL -# """ -# # Check if the server_path is a URL (endpoint) or a file path -# if server_path.startswith(('http://', 'https://', 'ws://', 'wss://')): -# # Connect to endpoint -# websocket = await self.exit_stack.enter_async_context(websocket_client(server_path)) -# self.session = await self.exit_stack.enter_async_context(ClientSession(websocket.receive, websocket.send)) -# else: -# # Connect to local script (existing functionality) -# is_python = server_path.endswith('.py') -# is_js = server_path.endswith('.js') -# if not (is_python or is_js): -# raise ValueError("Server script must be a .py or .js file") - -# command = "python" if is_python else "node" -# server_params = StdioServerParameters( -# command=command, -# args=[server_path], -# env=None -# ) - -# stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) -# self.stdio, self.write = stdio_transport -# self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) - -# await self.session.initialize() - -# # List available tools -# response = await self.session.list_tools() -# tools = response.tools -# print("\nConnected to server with tools:", [tool.name for tool in tools]) - -# async def process_query(self, query: str) -> str: -# """Process a query using Claude and available tools""" -# messages = [ -# { -# "role": "user", -# "content": query -# } -# ] - -# response = await self.session.list_tools() -# available_tools = [{ -# "name": tool.name, -# "description": tool.description, -# "input_schema": tool.inputSchema -# } for tool in response.tools] - -# # Initial Claude API call -# response = self.anthropic.messages.create( -# model="claude-3-5-sonnet-20241022", -# max_tokens=1000, -# messages=messages, -# tools=available_tools -# ) - -# # Process response and handle tool calls -# final_text = [] - -# assistant_message_content = [] -# for content in response.content: -# if content.type == 'text': -# final_text.append(content.text) -# assistant_message_content.append(content) -# elif content.type == 'tool_use': -# tool_name = content.name -# tool_args = content.input - -# # Execute tool call -# result = await self.session.call_tool(tool_name, tool_args) -# final_text.append(f"[Calling tool {tool_name} with args {tool_args}]") - -# assistant_message_content.append(content) -# messages.append({ -# "role": "assistant", -# "content": assistant_message_content -# }) -# messages.append({ -# "role": "user", -# "content": [ -# { -# "type": "tool_result", -# "tool_use_id": content.id, -# "content": result.content -# } -# ] -# }) - -# # Get next response from Claude -# response = self.anthropic.messages.create( -# model="claude-3-5-sonnet-20241022", -# max_tokens=1000, -# messages=messages, -# tools=available_tools -# ) - -# final_text.append(response.content[0].text) - -# return "\n".join(final_text) -# async def chat_loop(self): -# """Run an interactive chat loop""" -# print("\nMCP Client Started!") -# print("Type your queries or 'quit' to exit.") - -# while True: -# try: -# query = input("\nQuery: ").strip() - -# if query.lower() == 'quit': -# break - -# response = await self.process_query(query) -# print("\n" + response) - -# except Exception as e: -# print(f"\nError: {str(e)}") - -# async def cleanup(self): -# """Clean up resources""" -# await self.exit_stack.aclose() -# async def main(): -# if len(sys.argv) < 2: -# print("Usage: python client.py ") -# sys.exit(1) - -# client = MCPClient() -# try: -# await client.connect_to_server(sys.argv[1]) -# await client.chat_loop() -# finally: -# await client.cleanup() - -# if __name__ == "__main__": -# import sys -# asyncio.run(main()) From 4dec84f883c89c1b32088a6b62a416b633435264 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 18:02:02 -0700 Subject: [PATCH 13/40] cleanup --- litellm/proxy/_experimental/mcp_server/mcp_routes.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_routes.py b/litellm/proxy/_experimental/mcp_server/mcp_routes.py index 7df0dee435c..4b6c1581151 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_routes.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_routes.py @@ -6,12 +6,6 @@ from typing import Optional from fastapi import APIRouter, HTTPException -router = APIRouter( - prefix="/mcp", - tags=["mcp"], -) - - from litellm.proxy._experimental.mcp_server.tool_registry import ( global_mcp_tool_registry, ) @@ -19,7 +13,7 @@ from litellm.types.mcp_server.tool_registry import * router = APIRouter( prefix="/mcp", - tags=["MCP"], + tags=["mcp"], ) From 2afa73d5840ab7bbe0e32db669b1df30e42f20ca Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 18:05:14 -0700 Subject: [PATCH 14/40] fix mcp router --- litellm/proxy/proxy_server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 390c215ce65..e836119ea5b 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -126,6 +126,7 @@ from litellm.litellm_core_utils.core_helpers import ( from litellm.litellm_core_utils.credential_accessor import CredentialAccessor from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler +from litellm.proxy._experimental.mcp_server.mcp_routes import router as mcp_router from litellm.proxy._experimental.mcp_server.tool_registry import ( global_mcp_tool_registry, ) @@ -8171,6 +8172,7 @@ app.include_router(fine_tuning_router) app.include_router(credential_router) app.include_router(vertex_router) app.include_router(llm_passthrough_router) +app.include_router(mcp_router) app.include_router(anthropic_router) app.include_router(langfuse_router) app.include_router(pass_through_router) From 7826c9bd218f505d445f2c1d70f1cf2bf1f484a8 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 21:12:56 -0700 Subject: [PATCH 15/40] add litellm mcp endpoints --- .../_experimental/mcp_server/mcp_routes.py | 105 ++++++++----- .../_experimental/mcp_server/sse_transport.py | 148 ++++++++++++++++++ litellm/proxy/proxy_config.yaml | 2 - litellm/types/mcp_server/tool_registry.py | 6 - tests/pass_through_tests/test_mcp_routes.py | 6 +- 5 files changed, 221 insertions(+), 46 deletions(-) create mode 100644 litellm/proxy/_experimental/mcp_server/sse_transport.py diff --git a/litellm/proxy/_experimental/mcp_server/mcp_routes.py b/litellm/proxy/_experimental/mcp_server/mcp_routes.py index 4b6c1581151..29041eab97c 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_routes.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_routes.py @@ -2,75 +2,106 @@ LiteLLM MCP Server Routes """ -from typing import Optional +import asyncio -from fastapi import APIRouter, HTTPException +import mcp.types as types +from anyio import BrokenResourceError +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import StreamingResponse +from mcp.server import NotificationOptions, Server +from mcp.server.models import InitializationOptions +from pydantic import ValidationError +from litellm._logging import verbose_logger from litellm.proxy._experimental.mcp_server.tool_registry import ( global_mcp_tool_registry, ) from litellm.types.mcp_server.tool_registry import * +from .sse_transport import SseServerTransport + +######################################################## +############ Initialize the MCP Server ################# +######################################################## router = APIRouter( prefix="/mcp", tags=["mcp"], ) +server = Server("litellm-mcp-server") +sse = SseServerTransport("/mcp/sse/messages") + +######################################################## +############### MCP Server Routes ####################### +######################################################## -@router.get("/") -async def root(): - return {"message": "MCP Server is running"} - - -@router.get("/tools/list", response_model=ListToolsResponse) -async def list_tools(cursor: Optional[str] = None): +@server.list_tools() +async def list_tools() -> list[types.Tool]: """ List all available tools """ tools = [] for tool in global_mcp_tool_registry.list_tools(): tools.append( - ToolSchema( + types.Tool( name=tool.name, description=tool.description, inputSchema=tool.input_schema, ) ) - return ListToolsResponse(tools=tools) + return tools -@router.post("/tools/call", response_model=CallToolResponse) -async def call_tool(request: CallToolRequest): +@server.call_tool() +async def handle_call_tool( + name: str, arguments: dict | None +) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: """ Call a specific tool with the provided arguments """ - if request.method != "tools/call": - raise HTTPException(status_code=400, detail="Invalid method") - - if "name" not in request.params: - raise HTTPException(status_code=400, detail="Tool name is required") - - tool_name = request.params["name"] - arguments = request.params.get("arguments", {}) - - tool = global_mcp_tool_registry.get_tool(tool_name) + tool = global_mcp_tool_registry.get_tool(name) if not tool: - raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found") + raise HTTPException(status_code=404, detail=f"Tool '{name}' not found") + if arguments is None: + raise HTTPException(status_code=400, detail="Request arguments are required") try: result = tool.handler(**arguments) - - # Convert result to text content - if isinstance(result, str): - content = [ContentItem(type="text", text=result)] - elif isinstance(result, dict): - content = [ContentItem(type="text", text=str(result))] - else: - content = [ContentItem(type="text", text=str(result))] - - return CallToolResponse(content=content) + return [types.TextContent(text=str(result), type="text")] except Exception as e: - return CallToolResponse( - content=[ContentItem(type="text", text=f"Error: {str(e)}")], isError=True - ) + return [types.TextContent(text=f"Error: {str(e)}", type="text")] + + +@router.get("/", response_class=StreamingResponse) +async def handle_sse(request: Request): + verbose_logger.info("new incoming SSE connection established") + async with sse.connect_sse(request) as streams: + try: + await server.run(streams[0], streams[1], options) + except BrokenResourceError: + pass + except asyncio.CancelledError: + pass + except ValidationError: + pass + except Exception: + raise + await request.close() + + +@router.post("/sse/messages") +async def handle_messages(request: Request): + verbose_logger.info("incoming SSE message received") + await sse.handle_post_message(request.scope, request.receive, request._send) + await request.close() + + +options = InitializationOptions( + server_name="litellm-mcp-server", + server_version="0.1.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), +) diff --git a/litellm/proxy/_experimental/mcp_server/sse_transport.py b/litellm/proxy/_experimental/mcp_server/sse_transport.py new file mode 100644 index 00000000000..05a75b3b8a9 --- /dev/null +++ b/litellm/proxy/_experimental/mcp_server/sse_transport.py @@ -0,0 +1,148 @@ +""" +This is a modification of code from: https://github.com/SecretiveShell/MCP-Bridge/blob/master/mcp_bridge/mcp_server/sse_transport.py + +Credit to the maintainers of SecretiveShell for their SSE Transport implementation + +""" + +from contextlib import asynccontextmanager +from typing import Any +from urllib.parse import quote +from uuid import UUID, uuid4 + +import anyio +import mcp.types as types +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream +from fastapi.requests import Request +from fastapi.responses import Response +from pydantic import ValidationError +from sse_starlette import EventSourceResponse +from starlette.types import Receive, Scope, Send + +from litellm._logging import verbose_logger + + +class SseServerTransport: + """ + SSE server transport for MCP. This class provides _two_ ASGI applications, + suitable to be used with a framework like Starlette and a server like Hypercorn: + + 1. connect_sse() is an ASGI application which receives incoming GET requests, + and sets up a new SSE stream to send server messages to the client. + 2. handle_post_message() is an ASGI application which receives incoming POST + requests, which should contain client messages that link to a + previously-established SSE session. + """ + + _endpoint: str + _read_stream_writers: dict[ + UUID, MemoryObjectSendStream[types.JSONRPCMessage | Exception] + ] + + def __init__(self, endpoint: str) -> None: + """ + Creates a new SSE server transport, which will direct the client to POST + messages to the relative or absolute URL given. + """ + + super().__init__() + self._endpoint = endpoint + self._read_stream_writers = {} + verbose_logger.debug( + f"SseServerTransport initialized with endpoint: {endpoint}" + ) + + @asynccontextmanager + async def connect_sse(self, request: Request): + if request.scope["type"] != "http": + verbose_logger.error("connect_sse received non-HTTP request") + raise ValueError("connect_sse can only handle HTTP requests") + + verbose_logger.debug("Setting up SSE connection") + read_stream: MemoryObjectReceiveStream[types.JSONRPCMessage | Exception] + read_stream_writer: MemoryObjectSendStream[types.JSONRPCMessage | Exception] + + write_stream: MemoryObjectSendStream[types.JSONRPCMessage] + write_stream_reader: MemoryObjectReceiveStream[types.JSONRPCMessage] + + read_stream_writer, read_stream = anyio.create_memory_object_stream(0) + write_stream, write_stream_reader = anyio.create_memory_object_stream(0) + + session_id = uuid4() + session_uri = f"{quote(self._endpoint)}?session_id={session_id.hex}" + self._read_stream_writers[session_id] = read_stream_writer + verbose_logger.debug(f"Created new session with ID: {session_id}") + + sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream( + 0, dict[str, Any] + ) + + async def sse_writer(): + verbose_logger.debug("Starting SSE writer") + async with sse_stream_writer, write_stream_reader: + await sse_stream_writer.send({"event": "endpoint", "data": session_uri}) + verbose_logger.debug(f"Sent endpoint event: {session_uri}") + + async for message in write_stream_reader: + verbose_logger.debug(f"Sending message via SSE: {message}") + await sse_stream_writer.send( + { + "event": "message", + "data": message.model_dump_json( + by_alias=True, exclude_none=True + ), + } + ) + + async with anyio.create_task_group() as tg: + response = EventSourceResponse( + content=sse_stream_reader, data_sender_callable=sse_writer + ) + verbose_logger.debug("Starting SSE response task") + tg.start_soon(response, request.scope, request.receive, request._send) + + verbose_logger.debug("Yielding read and write streams") + yield (read_stream, write_stream) + + async def handle_post_message( + self, scope: Scope, receive: Receive, send: Send + ) -> Response: + verbose_logger.debug("Handling POST message") + request = Request(scope, receive) + + session_id_param = request.query_params.get("session_id") + if session_id_param is None: + verbose_logger.warning("Received request without session_id") + response = Response("session_id is required", status_code=400) + return response + + try: + session_id = UUID(hex=session_id_param) + verbose_logger.debug(f"Parsed session ID: {session_id}") + except ValueError: + verbose_logger.warning(f"Received invalid session ID: {session_id_param}") + response = Response("Invalid session ID", status_code=400) + return response + + writer = self._read_stream_writers.get(session_id) + if not writer: + verbose_logger.warning(f"Could not find session for ID: {session_id}") + response = Response("Could not find session", status_code=404) + return response + + json = await request.json() + verbose_logger.debug(f"Received JSON: {json}") + + try: + message = types.JSONRPCMessage.model_validate(json) + verbose_logger.debug(f"Validated client message: {message}") + except ValidationError as err: + verbose_logger.error(f"Failed to parse message: {err}") + response = Response("Could not parse message", status_code=400) + await writer.send(err) + return response + + verbose_logger.debug(f"Sending message to writer: {message}") + response = Response("Accepted", status_code=202) + await writer.send(message) + return response diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index bca2b96cb49..26ce6cb8f84 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -7,8 +7,6 @@ model_list: api_key: os.environ/AZURE_API_KEY -litellm_settings: - callbacks: ["custom_prompt_management.x42_prompt_management"] mcp_tools: - name: "get_current_time" diff --git a/litellm/types/mcp_server/tool_registry.py b/litellm/types/mcp_server/tool_registry.py index 266159343df..9ff06ec3514 100644 --- a/litellm/types/mcp_server/tool_registry.py +++ b/litellm/types/mcp_server/tool_registry.py @@ -30,9 +30,3 @@ class CallToolRequest(BaseModel): class ContentItem(BaseModel): type: str text: Optional[str] = None - - -class CallToolResponse(BaseModel): - content: List[ContentItem] - isError: Optional[bool] = None - _meta: Optional[Dict[str, Any]] = None diff --git a/tests/pass_through_tests/test_mcp_routes.py b/tests/pass_through_tests/test_mcp_routes.py index 69ae1b85227..687efe6195d 100644 --- a/tests/pass_through_tests/test_mcp_routes.py +++ b/tests/pass_through_tests/test_mcp_routes.py @@ -12,13 +12,17 @@ from mcp.client.sse import sse_client async def main(): model = ChatOpenAI(model="gpt-4o", api_key="sk-12") - async with sse_client(url="http://localhost:8000/mcp/") as (read, write): + async with sse_client(url="http://localhost:4000/mcp/") as (read, write): async with ClientSession(read, write) as session: # Initialize the connection + print("Initializing session") await session.initialize() + print("Session initialized") # Get tools + print("Loading tools") tools = await load_mcp_tools(session) + print("Tools loaded") print(tools) # # Create and run the agent From 8089c5e3c0c72a1879c46c705410645fd0a47ead Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 21:35:06 -0700 Subject: [PATCH 16/40] litellm MCP --- docs/my-website/docs/mcp.md | 104 ++++++++++++++++++++++++++++++++++++ docs/my-website/sidebars.js | 1 + 2 files changed, 105 insertions(+) create mode 100644 docs/my-website/docs/mcp.md diff --git a/docs/my-website/docs/mcp.md b/docs/my-website/docs/mcp.md new file mode 100644 index 00000000000..3458686c9e1 --- /dev/null +++ b/docs/my-website/docs/mcp.md @@ -0,0 +1,104 @@ +# /mcp Model Context Protocol [BETA] + +## Overview + +LiteLLM's MCP implementation allows you to define tools that can be called by any MCP compatible client. Define your `mcp_tools` with LiteLLM and all your clients can `list` and `call` available tools. + +## How it works + +LiteLLM exposes the following MCP endpoints: + +- `/mcp/list_tools` - List all available tools +- `/mcp/call_tool` - Call a specific tool with the provided arguments + +When MCP clients connect to LiteLLM they can follow this workflow: + +1. Connect to the LiteLLM MCP server +2. List all available tools on LiteLLM +3. Client makes LLM API request with tool call(s) +4. LLM API returns which tools to call and with what arguments +5. MCP client makes tool calls to LiteLLM +6. LiteLLM makes the tool calls to the appropriate handlers +7. LiteLLM returns the tool call results to the MCP client + +## Quick Start + +### 1. Define your tools on mcp_tools + +```yaml +model_list: + - model_name: gpt-3.5-turbo-end-user-test + litellm_params: + model: azure/chatgpt-v-2 + api_base: https://openai-gpt-4-test-v-1.openai.azure.com/ + api_version: "2023-05-15" + api_key: os.environ/AZURE_API_KEY + + + +mcp_tools: + - name: "get_current_time" + description: "Get the current time" + input_schema: { + "type": "object", + "properties": { + "format": { + "type": "string", + "description": "The format of the time to return", + "enum": ["short"] + } + } + } + handler: "mcp_tools.get_current_time" +``` + +### 2. Start LiteLLM Proxy Server + +### 3. Make an LLM API request + + + +```python +import asyncio +from langchain_mcp_adapters.tools import load_mcp_tools +from langchain_openai import ChatOpenAI +from langgraph.prebuilt import create_react_agent +from mcp import ClientSession +from mcp.client.sse import sse_client + + +async def main(): + # Initialize the model with your API key + model = ChatOpenAI(model="gpt-4o") + + # Connect to the MCP server + async with sse_client(url="http://localhost:4000/mcp/") as (read, write): + async with ClientSession(read, write) as session: + # Initialize the session + print("Initializing session...") + await session.initialize() + print("Session initialized") + + # Load available tools from MCP + print("Loading tools...") + tools = await load_mcp_tools(session) + print(f"Loaded {len(tools)} tools") + + # Create a ReAct agent with the model and tools + agent = create_react_agent(model, tools) + + # Run the agent with a user query + user_query = "What's the weather in Tokyo?" + print(f"Asking: {user_query}") + agent_response = await agent.ainvoke({"messages": user_query}) + print("Agent response:") + print(agent_response) + + +if __name__ == "__main__": + asyncio.run(main()) + +``` + + + diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 1df988f372d..baae2dfe6dc 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -301,6 +301,7 @@ const sidebars = { "image_variations", ] }, + "mcp", { type: "category", label: "/audio", From 7a07f2e305e6decdb2117aa581e9c6d85c37a207 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 21:36:28 -0700 Subject: [PATCH 17/40] litellm MCP example --- docs/my-website/docs/mcp.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/my-website/docs/mcp.md b/docs/my-website/docs/mcp.md index 3458686c9e1..176ccb504a6 100644 --- a/docs/my-website/docs/mcp.md +++ b/docs/my-website/docs/mcp.md @@ -27,12 +27,10 @@ When MCP clients connect to LiteLLM they can follow this workflow: ```yaml model_list: - - model_name: gpt-3.5-turbo-end-user-test + - model_name: gpt-4o litellm_params: - model: azure/chatgpt-v-2 - api_base: https://openai-gpt-4-test-v-1.openai.azure.com/ - api_version: "2023-05-15" - api_key: os.environ/AZURE_API_KEY + model: openai/gpt-4o + api_key: sk-xxxxxxx From 9173685ec2b99bd6bd3f32cdb2c79ddca740ac4c Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 21:40:47 -0700 Subject: [PATCH 18/40] docs mcp tool spec --- docs/my-website/docs/mcp.md | 81 ++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/docs/my-website/docs/mcp.md b/docs/my-website/docs/mcp.md index 176ccb504a6..4bdca6cf471 100644 --- a/docs/my-website/docs/mcp.md +++ b/docs/my-website/docs/mcp.md @@ -1,3 +1,7 @@ +import Image from '@theme/IdealImage'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + # /mcp Model Context Protocol [BETA] ## Overview @@ -50,7 +54,38 @@ mcp_tools: handler: "mcp_tools.get_current_time" ``` -### 2. Start LiteLLM Proxy Server + +### 3. Start LiteLLM Gateway + + + + +Mount your `custom_logger.py` on the LiteLLM Docker container. + +```shell +docker run -d \ + -p 4000:4000 \ + -e OPENAI_API_KEY=$OPENAI_API_KEY \ + --name my-app \ + -v $(pwd)/my_config.yaml:/app/config.yaml \ + -v $(pwd)/custom_logger.py:/app/custom_logger.py \ + my-app:latest \ + --config /app/config.yaml \ + --port 4000 \ + --detailed_debug \ +``` + + + + + +```shell +litellm --config config.yaml --detailed_debug +``` + + + + ### 3. Make an LLM API request @@ -99,4 +134,48 @@ if __name__ == "__main__": ``` +## Specification for `mcp_tools` +The `mcp_tools` section in your LiteLLM config defines tools that can be called by MCP-compatible clients. + +### Tool Definition Format + +```yaml +mcp_tools: + - name: string # Required: Name of the tool + description: string # Required: Description of what the tool does + input_schema: object # Required: JSON Schema defining the tool's input parameters + handler: string # Required: Path to the function that implements the tool +``` + +### Field Details + +- `name`: A unique identifier for the tool +- `description`: A clear description of what the tool does, used by LLMs to determine when to call it +- `input_schema`: JSON Schema object defining the expected input parameters +- `handler`: String path to the Python function that implements the tool (e.g., "module.submodule.function_name") + +### Example Tool Definition + +```yaml +mcp_tools: + - name: "get_current_time" + description: "Get the current time in a specified format" + input_schema: { + "type": "object", + "properties": { + "format": { + "type": "string", + "description": "The format of the time to return", + "enum": ["short", "long", "iso"] + }, + "timezone": { + "type": "string", + "description": "The timezone to use (e.g., 'UTC', 'America/New_York')", + "default": "UTC" + } + }, + "required": ["format"] + } + handler: "mcp_tools.get_current_time" +``` From 435fba929ec1c11f0d4ffc5982f8a8d5fca23a44 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 21:48:55 -0700 Subject: [PATCH 19/40] litellm mcp support --- docs/my-website/docs/mcp.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/my-website/docs/mcp.md b/docs/my-website/docs/mcp.md index 4bdca6cf471..42489477cfb 100644 --- a/docs/my-website/docs/mcp.md +++ b/docs/my-website/docs/mcp.md @@ -29,6 +29,8 @@ When MCP clients connect to LiteLLM they can follow this workflow: ### 1. Define your tools on mcp_tools +LiteLLM allows you to define your tools on the `mcp_tools` section in your config.yaml file. All tools listed here will be available to MCP clients (when they connect to LiteLLM and call `list_tools`). + ```yaml model_list: - model_name: gpt-4o @@ -54,13 +56,38 @@ mcp_tools: handler: "mcp_tools.get_current_time" ``` +### 2. Define a handler for your tool + +Create a new file called `mcp_tools.py` and add this code. The key method here is `get_current_time` which gets executed when the `get_current_time` tool is called. + +```python +# mcp_tools.py + +from datetime import datetime + +def get_current_time(format: str = "short"): + """ + Simple handler for the 'get_current_time' tool. + + Args: + format (str): The format of the time to return ('short'). + + Returns: + str: The current time formatted as 'HH:MM'. + """ + # Get the current time + current_time = datetime.now() + + # Format the time as 'HH:MM' + return current_time.strftime('%H:%M') +``` ### 3. Start LiteLLM Gateway -Mount your `custom_logger.py` on the LiteLLM Docker container. +Mount your `mcp_tools.py` on the LiteLLM Docker container. ```shell docker run -d \ @@ -68,7 +95,7 @@ docker run -d \ -e OPENAI_API_KEY=$OPENAI_API_KEY \ --name my-app \ -v $(pwd)/my_config.yaml:/app/config.yaml \ - -v $(pwd)/custom_logger.py:/app/custom_logger.py \ + -v $(pwd)/mcp_tools.py:/app/mcp_tools.py \ my-app:latest \ --config /app/config.yaml \ --port 4000 \ From 2cd170daeb58fe25536d479044f4d0f9428f80c5 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 21:50:11 -0700 Subject: [PATCH 20/40] add mcp==1.4.1 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 0e90c69b73d..5ac92f3e141 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ numpy==2.1.1 # semantic caching prisma==0.11.0 # for db mangum==0.17.0 # for aws lambda functions pynacl==1.5.0 # for encrypting keys +mcp==1.4.1 # for MCP server google-cloud-aiplatform==1.47.0 # for vertex ai calls anthropic[vertex]==0.21.3 google-generativeai==0.5.0 # for vertex ai calls From 90877d2c1cb64033e37cf90a1f9a14c53148af6d Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 21:56:10 -0700 Subject: [PATCH 21/40] bump requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5ac92f3e141..abe021c709a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # LITELLM PROXY DEPENDENCIES # -anyio==4.4.0 # openai + http req. +anyio==4.5.0 # openai + http req. httpx==0.27.0 # Pin Httpx dependency openai==1.66.1 # openai req. fastapi==0.115.5 # server dep From fd95499e157c58a4c6a06a7c31f144b69592006d Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 21:57:28 -0700 Subject: [PATCH 22/40] fix naming --- .../proxy/_experimental/mcp_server/{mcp_routes.py => server.py} | 0 litellm/proxy/proxy_server.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename litellm/proxy/_experimental/mcp_server/{mcp_routes.py => server.py} (100%) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_routes.py b/litellm/proxy/_experimental/mcp_server/server.py similarity index 100% rename from litellm/proxy/_experimental/mcp_server/mcp_routes.py rename to litellm/proxy/_experimental/mcp_server/server.py diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index e836119ea5b..9185b2d22a2 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -126,7 +126,7 @@ from litellm.litellm_core_utils.core_helpers import ( from litellm.litellm_core_utils.credential_accessor import CredentialAccessor from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler -from litellm.proxy._experimental.mcp_server.mcp_routes import router as mcp_router +from litellm.proxy._experimental.mcp_server.server import router as mcp_router from litellm.proxy._experimental.mcp_server.tool_registry import ( global_mcp_tool_registry, ) From 5bc07b0c5dbd315a31b5701a629cd6301120d330 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Thu, 20 Mar 2025 22:03:56 -0700 Subject: [PATCH 23/40] test tool registry --- .../mcp_server/test_tool_registry.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tests/litellm/proxy/experimental/mcp_server/test_tool_registry.py diff --git a/tests/litellm/proxy/experimental/mcp_server/test_tool_registry.py b/tests/litellm/proxy/experimental/mcp_server/test_tool_registry.py new file mode 100644 index 00000000000..d5ba9744c7d --- /dev/null +++ b/tests/litellm/proxy/experimental/mcp_server/test_tool_registry.py @@ -0,0 +1,95 @@ +import json +import os +import sys + +import pytest + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system path + +from litellm.proxy._experimental.mcp_server.tool_registry import MCPToolRegistry + + +# Test handler function +def example_handler(input_data): + return {"result": input_data} + + +def test_register_and_get_tool(): + registry = MCPToolRegistry() + + # Test registering a tool + registry.register_tool( + name="test_tool", + description="A test tool", + input_schema={"type": "object", "properties": {"test": {"type": "string"}}}, + handler=example_handler, + ) + + # Test getting the registered tool + tool = registry.get_tool("test_tool") + assert tool is not None + assert tool.name == "test_tool" + assert tool.description == "A test tool" + assert callable(tool.handler) + + # Test getting non-existent tool + assert registry.get_tool("non_existent") is None + + +def test_list_tools(): + registry = MCPToolRegistry() + + # Register multiple tools + registry.register_tool( + name="tool1", description="Tool 1", input_schema={}, handler=example_handler + ) + registry.register_tool( + name="tool2", description="Tool 2", input_schema={}, handler=example_handler + ) + + # Test listing tools + tools = registry.list_tools() + assert len(tools) == 2 + assert {tool.name for tool in tools} == {"tool1", "tool2"} + + +def test_load_tools_from_config(): + registry = MCPToolRegistry() + + # Test valid config + valid_config = [ + { + "name": "config_tool", + "description": "A tool from config", + "input_schema": {"type": "object"}, + "handler": "test_tool_registry.example_handler", + } + ] + + registry.load_tools_from_config(valid_config) + assert "config_tool" in registry.tools + assert registry.tools["config_tool"].name == "config_tool" + assert registry.tools["config_tool"].description == "A tool from config" + assert callable(registry.tools["config_tool"].handler) + + +def test_tool_execution(): + registry = MCPToolRegistry() + + # Register a tool + registry.register_tool( + name="echo", + description="Echo the input", + input_schema={"type": "object"}, + handler=example_handler, + ) + + # Get and execute the tool + tool = registry.get_tool("echo") + assert tool is not None + + test_input = {"message": "hello"} + result = tool.handler(test_input) + assert result == {"result": test_input} From 3395004f0a7bb5299374949e7fbc1b2a037336c7 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Sat, 22 Mar 2025 19:00:25 -0700 Subject: [PATCH 24/40] bump pydantic --- .circleci/config.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 886e121f35d..fd87c3b025c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,7 +66,7 @@ jobs: pip install python-multipart pip install google-cloud-aiplatform pip install prometheus-client==0.20.0 - pip install "pydantic==2.7.1" + pip install "pydantic==2.10.0" pip install "diskcache==5.6.1" pip install "Pillow==10.3.0" pip install "jsonschema==4.22.0" @@ -185,7 +185,7 @@ jobs: pip install python-multipart pip install google-cloud-aiplatform pip install prometheus-client==0.20.0 - pip install "pydantic==2.7.1" + pip install "pydantic==2.10.0" pip install "diskcache==5.6.1" pip install "Pillow==10.3.0" pip install "jsonschema==4.22.0" @@ -285,7 +285,7 @@ jobs: pip install python-multipart pip install google-cloud-aiplatform pip install prometheus-client==0.20.0 - pip install "pydantic==2.7.1" + pip install "pydantic==2.10.0" pip install "diskcache==5.6.1" pip install "Pillow==10.3.0" pip install "jsonschema==4.22.0" @@ -530,7 +530,7 @@ jobs: pip install python-multipart pip install google-cloud-aiplatform pip install prometheus-client==0.20.0 - pip install "pydantic==2.7.1" + pip install "pydantic==2.10.0" pip install "diskcache==5.6.1" pip install "Pillow==10.3.0" pip install "jsonschema==4.22.0" @@ -2014,7 +2014,7 @@ jobs: pip install "openai==1.68.2" pip install "assemblyai==0.37.0" python -m pip install --upgrade pip - pip install "pydantic==2.7.1" + pip install "pydantic==2.10.0" pip install "pytest==7.3.1" pip install "pytest-mock==3.12.0" pip install "pytest-asyncio==0.21.1" @@ -2289,7 +2289,7 @@ jobs: pip install aiohttp pip install "openai==1.68.2" python -m pip install --upgrade pip - pip install "pydantic==2.7.1" + pip install "pydantic==2.10.0" pip install "pytest==7.3.1" pip install "pytest-mock==3.12.0" pip install "pytest-asyncio==0.21.1" From 5017332690d36299122f8c5bc1640c03738aad0c Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 07:11:48 -0700 Subject: [PATCH 25/40] fix pydantic import error --- litellm/proxy/_experimental/mcp_server/server.py | 4 ++-- litellm/types/mcp_server/tool_registry.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 29041eab97c..b9820fb41e3 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -3,6 +3,7 @@ LiteLLM MCP Server Routes """ import asyncio +from typing import Any, Dict import mcp.types as types from anyio import BrokenResourceError @@ -16,7 +17,6 @@ from litellm._logging import verbose_logger from litellm.proxy._experimental.mcp_server.tool_registry import ( global_mcp_tool_registry, ) -from litellm.types.mcp_server.tool_registry import * from .sse_transport import SseServerTransport @@ -55,7 +55,7 @@ async def list_tools() -> list[types.Tool]: @server.call_tool() async def handle_call_tool( - name: str, arguments: dict | None + name: str, arguments: Dict[str, Any] | None ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: """ Call a specific tool with the provided arguments diff --git a/litellm/types/mcp_server/tool_registry.py b/litellm/types/mcp_server/tool_registry.py index 9ff06ec3514..f2c1cf1a30b 100644 --- a/litellm/types/mcp_server/tool_registry.py +++ b/litellm/types/mcp_server/tool_registry.py @@ -9,6 +9,9 @@ class MCPTool(BaseModel): input_schema: Dict[str, Any] handler: Callable + class Config: + arbitrary_types_allowed = True + class ToolSchema(BaseModel): name: str From 8b8fca1e206e72e37be3a21b1f3b099c7384a702 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 11:01:29 -0700 Subject: [PATCH 26/40] fix mcp type imports --- .../proxy/_experimental/mcp_server/server.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index b9820fb41e3..2dcfe60a064 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -3,14 +3,17 @@ LiteLLM MCP Server Routes """ import asyncio -from typing import Any, Dict +from typing import Any, Dict, List, Union -import mcp.types as types from anyio import BrokenResourceError from fastapi import APIRouter, HTTPException, Request from fastapi.responses import StreamingResponse from mcp.server import NotificationOptions, Server from mcp.server.models import InitializationOptions +from mcp.types import EmbeddedResource as MCPEmbeddedResource +from mcp.types import ImageContent as MCPImageContent +from mcp.types import TextContent as MCPTextContent +from mcp.types import Tool as MCPTool from pydantic import ValidationError from litellm._logging import verbose_logger @@ -36,14 +39,14 @@ sse = SseServerTransport("/mcp/sse/messages") @server.list_tools() -async def list_tools() -> list[types.Tool]: +async def list_tools() -> list[MCPTool]: """ List all available tools """ tools = [] for tool in global_mcp_tool_registry.list_tools(): tools.append( - types.Tool( + MCPTool( name=tool.name, description=tool.description, inputSchema=tool.input_schema, @@ -56,7 +59,7 @@ async def list_tools() -> list[types.Tool]: @server.call_tool() async def handle_call_tool( name: str, arguments: Dict[str, Any] | None -) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: +) -> List[Union[MCPTextContent, MCPImageContent, MCPEmbeddedResource]]: """ Call a specific tool with the provided arguments """ @@ -68,9 +71,9 @@ async def handle_call_tool( try: result = tool.handler(**arguments) - return [types.TextContent(text=str(result), type="text")] + return [MCPTextContent(text=str(result), type="text")] except Exception as e: - return [types.TextContent(text=f"Error: {str(e)}", type="text")] + return [MCPTextContent(text=f"Error: {str(e)}", type="text")] @router.get("/", response_class=StreamingResponse) From ade2ea8f203e37d584c2975ab4b3f26a70826b20 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 15:55:51 -0700 Subject: [PATCH 27/40] fix order of load tests on config.yml --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fd87c3b025c..02b4de99c80 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -622,10 +622,10 @@ jobs: name: Install Dependencies command: | python -m pip install --upgrade pip - python -m pip install -r requirements.txt pip install "pytest==7.3.1" pip install "pytest-retry==1.6.3" pip install "pytest-asyncio==0.21.1" + python -m pip install -r requirements.txt # Run pytest and generate JUnit XML report - run: name: Run tests From 0faf23c75119795da1507ac6af0ab446a9d9fb39 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 16:01:32 -0700 Subject: [PATCH 28/40] update to cimg/python:3.13.1 on ci/cd --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 02b4de99c80..e131b70b6ef 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ orbs: jobs: local_testing: docker: - - image: cimg/python:3.11 + - image: cimg/python:3.13.1 auth: username: ${DOCKERHUB_USERNAME} password: ${DOCKERHUB_PASSWORD} @@ -126,7 +126,7 @@ jobs: - local_testing_coverage langfuse_logging_unit_tests: docker: - - image: cimg/python:3.11 + - image: cimg/python:3.13.1 auth: username: ${DOCKERHUB_USERNAME} password: ${DOCKERHUB_PASSWORD} @@ -610,7 +610,7 @@ jobs: - litellm_assistants_api_coverage load_testing: docker: - - image: cimg/python:3.11 + - image: cimg/python:3.13.1 auth: username: ${DOCKERHUB_USERNAME} password: ${DOCKERHUB_PASSWORD} From ff8a6d7cea9fbe647e7d4bea4411bf27259cd94b Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 16:41:50 -0700 Subject: [PATCH 29/40] litellm_utils_testing --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e131b70b6ef..d1632ae5842 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -870,6 +870,7 @@ jobs: name: Install Dependencies command: | python -m pip install --upgrade pip + pip install numpydoc python -m pip install -r requirements.txt pip install "respx==0.21.1" pip install "pytest==7.3.1" @@ -878,7 +879,6 @@ jobs: pip install "pytest-cov==5.0.0" pip install "google-generativeai==0.3.2" pip install "google-cloud-aiplatform==1.43.0" - pip install numpydoc # Run pytest and generate JUnit XML report - run: name: Run tests From eef8043a86a63dbff814e31dd80934d3ea728233 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 16:43:24 -0700 Subject: [PATCH 30/40] update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e3715e55e7..37e9d6441e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,6 @@ numpy==2.1.1 # semantic caching prisma==0.11.0 # for db mangum==0.17.0 # for aws lambda functions pynacl==1.5.0 # for encrypting keys -mcp==1.4.1 # for MCP server google-cloud-aiplatform==1.47.0 # for vertex ai calls anthropic[vertex]==0.21.3 google-generativeai==0.5.0 # for vertex ai calls @@ -52,4 +51,5 @@ tenacity==8.2.3 # for retrying requests, when litellm.num_retries set pydantic==2.10.0 # proxy + openai req. jsonschema==4.22.0 # validating json schema websockets==13.1.0 # for realtime API +mcp==1.4.1 # for MCP server #### \ No newline at end of file From 60242f9e484be85c6f70e6ea3a11b8d529c0f8d1 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 16:45:43 -0700 Subject: [PATCH 31/40] fix py version --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d1632ae5842..113f2db2e6d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ orbs: jobs: local_testing: docker: - - image: cimg/python:3.13.1 + - image: cimg/python:3.11 auth: username: ${DOCKERHUB_USERNAME} password: ${DOCKERHUB_PASSWORD} @@ -126,7 +126,7 @@ jobs: - local_testing_coverage langfuse_logging_unit_tests: docker: - - image: cimg/python:3.13.1 + - image: cimg/python:3.11 auth: username: ${DOCKERHUB_USERNAME} password: ${DOCKERHUB_PASSWORD} @@ -610,7 +610,7 @@ jobs: - litellm_assistants_api_coverage load_testing: docker: - - image: cimg/python:3.13.1 + - image: cimg/python:3.11 auth: username: ${DOCKERHUB_USERNAME} password: ${DOCKERHUB_PASSWORD} From fe2e2cb357b612784e03d635fc5cad5f3536e015 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 16:46:53 -0700 Subject: [PATCH 32/40] Show current pydantic version --- .circleci/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 113f2db2e6d..6174f9ec737 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -626,6 +626,10 @@ jobs: pip install "pytest-retry==1.6.3" pip install "pytest-asyncio==0.21.1" python -m pip install -r requirements.txt + - run: + name: Show current pydantic version + command: | + python -m pip show pydantic # Run pytest and generate JUnit XML report - run: name: Run tests From aaa567737b74cf01cab7d124010782dde05cd63b Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 16:53:01 -0700 Subject: [PATCH 33/40] bump MCP --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 37e9d6441e0..f84c8df154b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -51,5 +51,5 @@ tenacity==8.2.3 # for retrying requests, when litellm.num_retries set pydantic==2.10.0 # proxy + openai req. jsonschema==4.22.0 # validating json schema websockets==13.1.0 # for realtime API -mcp==1.4.1 # for MCP server +mcp==1.5.0 # for MCP server #### \ No newline at end of file From 5411aed2a894b8c98d60b8d6be9269ba98c3d139 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 16:57:47 -0700 Subject: [PATCH 34/40] fix mcp version --- .circleci/config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6174f9ec737..fd78ff64cd0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -622,10 +622,12 @@ jobs: name: Install Dependencies command: | python -m pip install --upgrade pip + python -m pip install -r requirements.txt pip install "pytest==7.3.1" pip install "pytest-retry==1.6.3" + pip install "pytest-cov==5.0.0" pip install "pytest-asyncio==0.21.1" - python -m pip install -r requirements.txt + pip install "respx==0.21.1" - run: name: Show current pydantic version command: | From 6e3f86f12fb2364255dd5ac7fd6e81ca9a743330 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 18:36:42 -0700 Subject: [PATCH 35/40] fix using mcp router --- .circleci/config.yml | 20 ++++++++++---------- requirements.txt | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fd78ff64cd0..c48d9e5fd07 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,7 +66,7 @@ jobs: pip install python-multipart pip install google-cloud-aiplatform pip install prometheus-client==0.20.0 - pip install "pydantic==2.10.0" + pip install "pydantic==2.10.2" pip install "diskcache==5.6.1" pip install "Pillow==10.3.0" pip install "jsonschema==4.22.0" @@ -185,7 +185,7 @@ jobs: pip install python-multipart pip install google-cloud-aiplatform pip install prometheus-client==0.20.0 - pip install "pydantic==2.10.0" + pip install "pydantic==2.10.2" pip install "diskcache==5.6.1" pip install "Pillow==10.3.0" pip install "jsonschema==4.22.0" @@ -285,7 +285,7 @@ jobs: pip install python-multipart pip install google-cloud-aiplatform pip install prometheus-client==0.20.0 - pip install "pydantic==2.10.0" + pip install "pydantic==2.10.2" pip install "diskcache==5.6.1" pip install "Pillow==10.3.0" pip install "jsonschema==4.22.0" @@ -530,7 +530,7 @@ jobs: pip install python-multipart pip install google-cloud-aiplatform pip install prometheus-client==0.20.0 - pip install "pydantic==2.10.0" + pip install "pydantic==2.10.2" pip install "diskcache==5.6.1" pip install "Pillow==10.3.0" pip install "jsonschema==4.22.0" @@ -706,8 +706,8 @@ jobs: pip install "pytest-cov==5.0.0" pip install "pytest-asyncio==0.21.1" pip install "respx==0.21.1" - pip install "pydantic==2.7.2" - pip install "mcp==1.4.1" + pip install "pydantic==2.10.2" + pip install "mcp==1.5.0" # Run pytest and generate JUnit XML report - run: name: Run tests @@ -794,8 +794,8 @@ jobs: pip install "pytest-asyncio==0.21.1" pip install "respx==0.21.1" pip install "hypercorn==0.17.3" - pip install "pydantic==2.7.2" - pip install "mcp==1.4.1" + pip install "pydantic==2.10.2" + pip install "mcp==1.5.0" # Run pytest and generate JUnit XML report - run: name: Run tests @@ -2020,7 +2020,7 @@ jobs: pip install "openai==1.68.2" pip install "assemblyai==0.37.0" python -m pip install --upgrade pip - pip install "pydantic==2.10.0" + pip install "pydantic==2.10.2" pip install "pytest==7.3.1" pip install "pytest-mock==3.12.0" pip install "pytest-asyncio==0.21.1" @@ -2295,7 +2295,7 @@ jobs: pip install aiohttp pip install "openai==1.68.2" python -m pip install --upgrade pip - pip install "pydantic==2.10.0" + pip install "pydantic==2.10.2" pip install "pytest==7.3.1" pip install "pytest-mock==3.12.0" pip install "pytest-asyncio==0.21.1" diff --git a/requirements.txt b/requirements.txt index f84c8df154b..ced1bdd959a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,7 @@ mangum==0.17.0 # for aws lambda functions pynacl==1.5.0 # for encrypting keys google-cloud-aiplatform==1.47.0 # for vertex ai calls anthropic[vertex]==0.21.3 +mcp==1.5.0 # for MCP server google-generativeai==0.5.0 # for vertex ai calls async_generator==1.10.0 # for async ollama calls langfuse==2.45.0 # for langfuse self-hosted logging @@ -48,8 +49,7 @@ jinja2==3.1.6 # for prompt templates aiohttp==3.10.2 # for network calls aioboto3==12.3.0 # for async sagemaker calls tenacity==8.2.3 # for retrying requests, when litellm.num_retries set -pydantic==2.10.0 # proxy + openai req. +pydantic==2.10.2 # proxy + openai req. jsonschema==4.22.0 # validating json schema websockets==13.1.0 # for realtime API -mcp==1.5.0 # for MCP server #### \ No newline at end of file From ffce25cd94f58ccd2648d40aca457a58ca8a5437 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 18:44:24 -0700 Subject: [PATCH 36/40] bump deps --- .circleci/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/requirements.txt b/.circleci/requirements.txt index 356a9840f5b..5f09fa7f1fd 100644 --- a/.circleci/requirements.txt +++ b/.circleci/requirements.txt @@ -7,7 +7,8 @@ cohere redis anthropic orjson==3.9.15 -pydantic==2.7.1 +pydantic==2.10.2 google-cloud-aiplatform==1.43.0 fastapi-sso==0.10.0 uvloop==0.21.0 +mcp==1.5.0 # for MCP server From 002837f962dcdfafd085968be46d2b9f497dce2a Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 18:48:43 -0700 Subject: [PATCH 37/40] fix linting errors --- litellm/proxy/_experimental/mcp_server/server.py | 4 ++-- litellm/proxy/_experimental/mcp_server/sse_transport.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 2dcfe60a064..02a9303be5d 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -30,8 +30,8 @@ router = APIRouter( prefix="/mcp", tags=["mcp"], ) -server = Server("litellm-mcp-server") -sse = SseServerTransport("/mcp/sse/messages") +server: Server = Server("litellm-mcp-server") +sse: SseServerTransport = SseServerTransport("/mcp/sse/messages") ######################################################## ############### MCP Server Routes ####################### diff --git a/litellm/proxy/_experimental/mcp_server/sse_transport.py b/litellm/proxy/_experimental/mcp_server/sse_transport.py index 05a75b3b8a9..63ffd403c66 100644 --- a/litellm/proxy/_experimental/mcp_server/sse_transport.py +++ b/litellm/proxy/_experimental/mcp_server/sse_transport.py @@ -73,6 +73,8 @@ class SseServerTransport: self._read_stream_writers[session_id] = read_stream_writer verbose_logger.debug(f"Created new session with ID: {session_id}") + sse_stream_writer: MemoryObjectSendStream[dict[str, Any]] + sse_stream_reader: MemoryObjectReceiveStream[dict[str, Any]] sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream( 0, dict[str, Any] ) From ab1f87bb59fce0f23484b54d415d8fcfc6ffabab Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 18:56:32 -0700 Subject: [PATCH 38/40] add "mcp==1.5.0" to optional pyproject for litellm proxy --- .circleci/config.yml | 1 + poetry.lock | 89 ++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 4 +- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c48d9e5fd07..e0e4e56978c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1062,6 +1062,7 @@ jobs: pip install jinja2 pip install tokenizers=="0.20.0" pip install uvloop==0.21.0 + pip install "mcp==1.5.0" pip install jsonschema - run: name: Run tests diff --git a/poetry.lock b/poetry.lock index 5834dd2e704..8b793c43361 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1297,6 +1297,19 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "httpx-sse" +version = "0.4.0" +description = "Consume Server-Sent Event (SSE) messages with HTTPX." +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"proxy\"" +files = [ + {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, + {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, +] + [[package]] name = "huggingface-hub" version = "0.29.3" @@ -1659,6 +1672,34 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "mcp" +version = "1.5.0" +description = "Model Context Protocol SDK" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"proxy\"" +files = [ + {file = "mcp-1.5.0-py3-none-any.whl", hash = "sha256:51c3f35ce93cb702f7513c12406bbea9665ef75a08db909200b07da9db641527"}, + {file = "mcp-1.5.0.tar.gz", hash = "sha256:5b2766c05e68e01a2034875e250139839498c61792163a7b221fc170c12f5aa9"}, +] + +[package.dependencies] +anyio = ">=4.5" +httpx = ">=0.27" +httpx-sse = ">=0.4" +pydantic = ">=2.7.2,<3.0.0" +pydantic-settings = ">=2.5.2" +sse-starlette = ">=1.6.1" +starlette = ">=0.27" +uvicorn = ">=0.23.1" + +[package.extras] +cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"] +rich = ["rich (>=13.9.4)"] +ws = ["websockets (>=15.0.1)"] + [[package]] name = "msal" version = "1.32.0" @@ -2458,6 +2499,28 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydantic-settings" +version = "2.8.1" +description = "Settings management using Pydantic" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"proxy\"" +files = [ + {file = "pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c"}, + {file = "pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" + +[package.extras] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + [[package]] name = "pyflakes" version = "3.1.0" @@ -3036,6 +3099,27 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sse-starlette" +version = "2.1.3" +description = "SSE plugin for Starlette" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.10\" and extra == \"proxy\"" +files = [ + {file = "sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772"}, + {file = "sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169"}, +] + +[package.dependencies] +anyio = "*" +starlette = "*" +uvicorn = "*" + +[package.extras] +examples = ["fastapi"] + [[package]] name = "starlette" version = "0.44.0" @@ -3051,7 +3135,6 @@ files = [ [package.dependencies] anyio = ">=3.4.0,<5" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] @@ -3608,9 +3691,9 @@ type = ["pytest-mypy"] [extras] extra-proxy = ["azure-identity", "azure-keyvault-secrets", "google-cloud-kms", "prisma", "resend"] -proxy = ["PyJWT", "apscheduler", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "gunicorn", "orjson", "pynacl", "python-multipart", "pyyaml", "rq", "uvicorn", "uvloop", "websockets"] +proxy = ["PyJWT", "apscheduler", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "gunicorn", "mcp", "orjson", "pynacl", "python-multipart", "pyyaml", "rq", "uvicorn", "uvloop", "websockets"] [metadata] lock-version = "2.1" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "55078af47c1af79bd3ebadacb7ba92844d550a577bb0c49f5096693701ea4322" +content-hash = "4b3fbfd1b8e62e0a23f793fca0d2e3000fdeb18db3e4bf5aaee3448e9526f5de" diff --git a/pyproject.toml b/pyproject.toml index 208804c5629..9b0f4b941de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ resend = {version = "^0.8.0", optional = true} pynacl = {version = "^1.5.0", optional = true} websockets = {version = "^13.1.0", optional = true} boto3 = {version = "1.34.34", optional = true} +mcp = {version = "1.5.0", optional = true, python = ">=3.10"} [tool.poetry.extras] proxy = [ @@ -71,7 +72,8 @@ proxy = [ "cryptography", "pynacl", "websockets", - "boto3" + "boto3", + "mcp" ] extra_proxy = [ From 395e4e73d0e4cb5e5963626b2153ba3e74a6fef3 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 18:59:32 -0700 Subject: [PATCH 39/40] langchain_mcp_adapters --- .circleci/config.yml | 1 + poetry.lock | 303 ++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 + 3 files changed, 299 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e0e4e56978c..d0346d5aa13 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1455,6 +1455,7 @@ jobs: pip install "boto3==1.34.34" pip install "aioboto3==12.3.0" pip install langchain + pip install "langchain_mcp_adapters==0.0.5" pip install "langfuse>=2.0.0" pip install "logfire==0.29.0" pip install numpydoc diff --git a/poetry.lock b/poetry.lock index 8b793c43361..b01fdc74804 100644 --- a/poetry.lock +++ b/poetry.lock @@ -215,7 +215,7 @@ description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_full_version < \"3.11.3\" and extra == \"proxy\" or python_version < \"3.11\"" +markers = "python_full_version < \"3.11.3\" and (extra == \"extra-proxy\" or extra == \"proxy\" or python_version < \"3.11\")" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -428,8 +428,8 @@ files = [ jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" urllib3 = [ - {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""}, + {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, ] [package.extras] @@ -671,6 +671,25 @@ files = [ ] markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} +[[package]] +name = "coloredlogs" +version = "15.0.1" +description = "Colored terminal output for Python's logging module" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" +files = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] + +[package.dependencies] +humanfriendly = ">=9.1" + +[package.extras] +cron = ["capturer (>=2.4)"] + [[package]] name = "cryptography" version = "43.0.3" @@ -1024,12 +1043,12 @@ files = [ google-auth = ">=2.14.1,<3.0.0" googleapis-common-protos = ">=1.56.2,<2.0.0" grpcio = [ - {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, + {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, ] grpcio-status = [ - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, + {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, ] proto-plus = [ {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, @@ -1345,6 +1364,22 @@ testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gr torch = ["safetensors[torch]", "torch"] typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] +[[package]] +name = "humanfriendly" +version = "10.0" +description = "Human friendly output for text interfaces using Python" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" +files = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] + +[package.dependencies] +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} + [[package]] name = "idna" version = "3.10" @@ -1700,6 +1735,45 @@ cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"] rich = ["rich (>=13.9.4)"] ws = ["websockets (>=15.0.1)"] +[[package]] +name = "ml-dtypes" +version = "0.4.1" +description = "" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" +files = [ + {file = "ml_dtypes-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1fe8b5b5e70cd67211db94b05cfd58dace592f24489b038dc6f9fe347d2e07d5"}, + {file = "ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c09a6d11d8475c2a9fd2bc0695628aec105f97cab3b3a3fb7c9660348ff7d24"}, + {file = "ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5e8f75fa371020dd30f9196e7d73babae2abd51cf59bdd56cb4f8de7e13354"}, + {file = "ml_dtypes-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:15fdd922fea57e493844e5abb930b9c0bd0af217d9edd3724479fc3d7ce70e3f"}, + {file = "ml_dtypes-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2d55b588116a7085d6e074cf0cdb1d6fa3875c059dddc4d2c94a4cc81c23e975"}, + {file = "ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e138a9b7a48079c900ea969341a5754019a1ad17ae27ee330f7ebf43f23877f9"}, + {file = "ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c6cfb5cf78535b103fde9ea3ded8e9f16f75bc07789054edc7776abfb3d752"}, + {file = "ml_dtypes-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:274cc7193dd73b35fb26bef6c5d40ae3eb258359ee71cd82f6e96a8c948bdaa6"}, + {file = "ml_dtypes-0.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:827d3ca2097085cf0355f8fdf092b888890bb1b1455f52801a2d7756f056f54b"}, + {file = "ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772426b08a6172a891274d581ce58ea2789cc8abc1c002a27223f314aaf894e7"}, + {file = "ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126e7d679b8676d1a958f2651949fbfa182832c3cd08020d8facd94e4114f3e9"}, + {file = "ml_dtypes-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0fb650d5c582a9e72bb5bd96cfebb2cdb889d89daff621c8fbc60295eba66c"}, + {file = "ml_dtypes-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e35e486e97aee577d0890bc3bd9e9f9eece50c08c163304008587ec8cfe7575b"}, + {file = "ml_dtypes-0.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:560be16dc1e3bdf7c087eb727e2cf9c0e6a3d87e9f415079d2491cc419b3ebf5"}, + {file = "ml_dtypes-0.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad0b757d445a20df39035c4cdeed457ec8b60d236020d2560dbc25887533cf50"}, + {file = "ml_dtypes-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:ef0d7e3fece227b49b544fa69e50e607ac20948f0043e9f76b44f35f229ea450"}, + {file = "ml_dtypes-0.4.1.tar.gz", hash = "sha256:fad5f2de464fd09127e49b7fd1252b9006fb43d2edc1ff112d390c324af5ca7a"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.23.3", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=1.21.2", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">1.20", markers = "python_version < \"3.10\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] + +[package.extras] +dev = ["absl-py", "pyink", "pylint (>=2.6.0)", "pytest", "pytest-xdist"] + [[package]] name = "msal" version = "1.32.0" @@ -1930,6 +2004,119 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.12\"" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "numpy" +version = "2.2.4" +description = "Fundamental package for array computing in Python" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version >= \"3.12\" and extra == \"extra-proxy\" and python_version < \"3.14\"" +files = [ + {file = "numpy-2.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8146f3550d627252269ac42ae660281d673eb6f8b32f113538e0cc2a9aed42b9"}, + {file = "numpy-2.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e642d86b8f956098b564a45e6f6ce68a22c2c97a04f5acd3f221f57b8cb850ae"}, + {file = "numpy-2.2.4-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:a84eda42bd12edc36eb5b53bbcc9b406820d3353f1994b6cfe453a33ff101775"}, + {file = "numpy-2.2.4-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:4ba5054787e89c59c593a4169830ab362ac2bee8a969249dc56e5d7d20ff8df9"}, + {file = "numpy-2.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7716e4a9b7af82c06a2543c53ca476fa0b57e4d760481273e09da04b74ee6ee2"}, + {file = "numpy-2.2.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf8c1d66f432ce577d0197dceaac2ac00c0759f573f28516246351c58a85020"}, + {file = "numpy-2.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:218f061d2faa73621fa23d6359442b0fc658d5b9a70801373625d958259eaca3"}, + {file = "numpy-2.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:df2f57871a96bbc1b69733cd4c51dc33bea66146b8c63cacbfed73eec0883017"}, + {file = "numpy-2.2.4-cp310-cp310-win32.whl", hash = "sha256:a0258ad1f44f138b791327961caedffbf9612bfa504ab9597157806faa95194a"}, + {file = "numpy-2.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:0d54974f9cf14acf49c60f0f7f4084b6579d24d439453d5fc5805d46a165b542"}, + {file = "numpy-2.2.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e9e0a277bb2eb5d8a7407e14688b85fd8ad628ee4e0c7930415687b6564207a4"}, + {file = "numpy-2.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eeea959168ea555e556b8188da5fa7831e21d91ce031e95ce23747b7609f8a4"}, + {file = "numpy-2.2.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bd3ad3b0a40e713fc68f99ecfd07124195333f1e689387c180813f0e94309d6f"}, + {file = "numpy-2.2.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cf28633d64294969c019c6df4ff37f5698e8326db68cc2b66576a51fad634880"}, + {file = "numpy-2.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fa8fa7697ad1646b5c93de1719965844e004fcad23c91228aca1cf0800044a1"}, + {file = "numpy-2.2.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4162988a360a29af158aeb4a2f4f09ffed6a969c9776f8f3bdee9b06a8ab7e5"}, + {file = "numpy-2.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:892c10d6a73e0f14935c31229e03325a7b3093fafd6ce0af704be7f894d95687"}, + {file = "numpy-2.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db1f1c22173ac1c58db249ae48aa7ead29f534b9a948bc56828337aa84a32ed6"}, + {file = "numpy-2.2.4-cp311-cp311-win32.whl", hash = "sha256:ea2bb7e2ae9e37d96835b3576a4fa4b3a97592fbea8ef7c3587078b0068b8f09"}, + {file = "numpy-2.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:f7de08cbe5551911886d1ab60de58448c6df0f67d9feb7d1fb21e9875ef95e91"}, + {file = "numpy-2.2.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a7b9084668aa0f64e64bd00d27ba5146ef1c3a8835f3bd912e7a9e01326804c4"}, + {file = "numpy-2.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dbe512c511956b893d2dacd007d955a3f03d555ae05cfa3ff1c1ff6df8851854"}, + {file = "numpy-2.2.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bb649f8b207ab07caebba230d851b579a3c8711a851d29efe15008e31bb4de24"}, + {file = "numpy-2.2.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:f34dc300df798742b3d06515aa2a0aee20941c13579d7a2f2e10af01ae4901ee"}, + {file = "numpy-2.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3f7ac96b16955634e223b579a3e5798df59007ca43e8d451a0e6a50f6bfdfba"}, + {file = "numpy-2.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f92084defa704deadd4e0a5ab1dc52d8ac9e8a8ef617f3fbb853e79b0ea3592"}, + {file = "numpy-2.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4e84a6283b36632e2a5b56e121961f6542ab886bc9e12f8f9818b3c266bfbb"}, + {file = "numpy-2.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11c43995255eb4127115956495f43e9343736edb7fcdb0d973defd9de14cd84f"}, + {file = "numpy-2.2.4-cp312-cp312-win32.whl", hash = "sha256:65ef3468b53269eb5fdb3a5c09508c032b793da03251d5f8722b1194f1790c00"}, + {file = "numpy-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:2aad3c17ed2ff455b8eaafe06bcdae0062a1db77cb99f4b9cbb5f4ecb13c5146"}, + {file = "numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7"}, + {file = "numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0"}, + {file = "numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392"}, + {file = "numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc"}, + {file = "numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298"}, + {file = "numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7"}, + {file = "numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6"}, + {file = "numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd"}, + {file = "numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c"}, + {file = "numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3"}, + {file = "numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8"}, + {file = "numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39"}, + {file = "numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd"}, + {file = "numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0"}, + {file = "numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960"}, + {file = "numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8"}, + {file = "numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc"}, + {file = "numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff"}, + {file = "numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286"}, + {file = "numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d"}, + {file = "numpy-2.2.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7051ee569db5fbac144335e0f3b9c2337e0c8d5c9fee015f259a5bd70772b7e8"}, + {file = "numpy-2.2.4-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ab2939cd5bec30a7430cbdb2287b63151b77cf9624de0532d629c9a1c59b1d5c"}, + {file = "numpy-2.2.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f35b19894a9e08639fd60a1ec1978cb7f5f7f1eace62f38dd36be8aecdef4d"}, + {file = "numpy-2.2.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b4adfbbc64014976d2f91084915ca4e626fbf2057fb81af209c1a6d776d23e3d"}, + {file = "numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f"}, +] + [[package]] name = "oauthlib" version = "3.2.2" @@ -2583,6 +2770,22 @@ cffi = ">=1.4.1" docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] +[[package]] +name = "pyreadline3" +version = "3.5.4" +description = "A python implementation of GNU readline." +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and sys_platform == \"win32\" and python_version < \"3.14\"" +files = [ + {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, + {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, +] + +[package.extras] +dev = ["build", "flake8", "mypy", "pytest", "twine"] + [[package]] name = "pytest" version = "7.4.4" @@ -2668,6 +2871,22 @@ files = [ {file = "python_multipart-0.0.18.tar.gz", hash = "sha256:7a68db60c8bfb82e460637fa4750727b45af1d5e2ed215593f917f64694d34fe"}, ] +[[package]] +name = "python-ulid" +version = "3.0.0" +description = "Universally unique lexicographically sortable identifier" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" +files = [ + {file = "python_ulid-3.0.0-py3-none-any.whl", hash = "sha256:e4c4942ff50dbd79167ad01ac725ec58f924b4018025ce22c858bfcff99a5e31"}, + {file = "python_ulid-3.0.0.tar.gz", hash = "sha256:e50296a47dc8209d28629a22fc81ca26c00982c78934bd7766377ba37ea49a9f"}, +] + +[package.extras] +pydantic = ["pydantic (>=2.0)"] + [[package]] name = "pyyaml" version = "6.0.2" @@ -2738,7 +2957,7 @@ description = "Python client for Redis database and key-value store" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"proxy\"" +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\" or extra == \"proxy\"" files = [ {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, @@ -2751,6 +2970,42 @@ async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\ hiredis = ["hiredis (>=3.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] +[[package]] +name = "redisvl" +version = "0.4.1" +description = "Python client library and CLI for using Redis as a vector database" +optional = true +python-versions = "<3.14,>=3.9" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" +files = [ + {file = "redisvl-0.4.1-py3-none-any.whl", hash = "sha256:6db5d5bc95b1fe8032a1cdae74ce1c65bc7fe9054e5429b5d34d5a91d28bae5f"}, + {file = "redisvl-0.4.1.tar.gz", hash = "sha256:fd6a36426ba94792c0efca20915c31232d4ee3cc58eb23794a62c142696401e6"}, +] + +[package.dependencies] +coloredlogs = ">=15.0,<16.0" +ml-dtypes = ">=0.4.0,<0.5.0" +numpy = [ + {version = ">=1,<2", markers = "python_version < \"3.12\""}, + {version = ">=1.26.0,<3", markers = "python_version >= \"3.12\""}, +] +pydantic = ">=2,<3" +python-ulid = ">=3.0.0,<4.0.0" +pyyaml = ">=5.4,<7.0" +redis = ">=5.0,<6.0" +tabulate = ">=0.9.0,<0.10.0" +tenacity = ">=8.2.2" + +[package.extras] +bedrock = ["boto3[bedrock] (>=1.36.0,<2.0.0)"] +cohere = ["cohere (>=4.44)"] +mistralai = ["mistralai (>=1.0.0)"] +openai = ["openai (>=1.13.0,<2.0.0)"] +sentence-transformers = ["scipy (<1.15)", "scipy (>=1.15,<2.0)", "sentence-transformers (>=3.4.0,<4.0.0)"] +vertexai = ["google-cloud-aiplatform (>=1.26,<2.0)", "protobuf (>=5.29.1,<6.0.0)"] +voyageai = ["voyageai (>=0.2.2)"] + [[package]] name = "referencing" version = "0.35.1" @@ -3135,10 +3390,44 @@ files = [ [package.dependencies] anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +optional = true +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "tenacity" +version = "9.0.0" +description = "Retry code until it succeeds" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.9\" and extra == \"extra-proxy\" and python_version < \"3.14\"" +files = [ + {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, + {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, +] + +[package.extras] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] + [[package]] name = "tiktoken" version = "0.7.0" @@ -3690,10 +3979,10 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", type = ["pytest-mypy"] [extras] -extra-proxy = ["azure-identity", "azure-keyvault-secrets", "google-cloud-kms", "prisma", "resend"] +extra-proxy = ["azure-identity", "azure-keyvault-secrets", "google-cloud-kms", "prisma", "redisvl", "resend"] proxy = ["PyJWT", "apscheduler", "backoff", "boto3", "cryptography", "fastapi", "fastapi-sso", "gunicorn", "mcp", "orjson", "pynacl", "python-multipart", "pyyaml", "rq", "uvicorn", "uvloop", "websockets"] [metadata] lock-version = "2.1" python-versions = ">=3.8.1,<4.0, !=3.9.7" -content-hash = "4b3fbfd1b8e62e0a23f793fca0d2e3000fdeb18db3e4bf5aaee3448e9526f5de" +content-hash = "f7c21b3d659e4a15cd46bb42fb905ad039028f4f6b82507fd1278ac05c412569" diff --git a/pyproject.toml b/pyproject.toml index 9b0f4b941de..47590d69b8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ resend = {version = "^0.8.0", optional = true} pynacl = {version = "^1.5.0", optional = true} websockets = {version = "^13.1.0", optional = true} boto3 = {version = "1.34.34", optional = true} +redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"} mcp = {version = "1.5.0", optional = true, python = ">=3.10"} [tool.poetry.extras] @@ -82,6 +83,7 @@ extra_proxy = [ "azure-keyvault-secrets", "google-cloud-kms", "resend", + "redisvl" ] [tool.isort] From 4a675da0f9e3702430a3983ce2396c54f986dc5a Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 24 Mar 2025 19:10:59 -0700 Subject: [PATCH 40/40] fix setup --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ee6e85616ee..c8d9419c93c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1060,8 +1060,8 @@ jobs: pip install click pip install "boto3==1.34.34" pip install jinja2 - pip install tokenizers=="0.20.0" - pip install uvloop==0.21.0 + pip install "tokenizers==0.20.0" + pip install "uvloop==0.21.0" pip install "mcp==1.5.0" pip install jsonschema - run: