diff --git a/docs/my-website/docs/mcp.md b/docs/my-website/docs/mcp.md index 684c2e6ca74..7eaccf3180f 100644 --- a/docs/my-website/docs/mcp.md +++ b/docs/my-website/docs/mcp.md @@ -40,7 +40,28 @@ LiteLLM supports the following MCP transports: style={{width: '80%', display: 'block', margin: '0'}} /> -### Adding a stdio MCP Server +
+
+ +### Add HTTP MCP Server + +This video walks through adding and using an HTTP MCP server on LiteLLM UI and using it in Cursor IDE. + + + +
+
+ +### Add SSE MCP Server + +This video walks through adding and using an SSE MCP server on LiteLLM UI and using it in Cursor IDE. + + + +
+
+ +### Add STDIO MCP Server For stdio MCP servers, select "Standard Input/Output (stdio)" as the transport type and provide the stdio configuration in JSON format: diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index 6624eb7e64e..048b25fa35a 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -1,5 +1,5 @@ import importlib -from typing import Optional, Dict +from typing import Dict, List, Optional from fastapi import APIRouter, Depends, Query, Request @@ -21,9 +21,10 @@ router = APIRouter( ) if MCP_AVAILABLE: + from litellm.experimental_mcp_client.client import MCPTool from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( - global_mcp_server_manager, _convert_protocol_version_to_enum, + global_mcp_server_manager, ) from litellm.proxy._experimental.mcp_server.server import ( ListMCPToolsRestAPIResponseObject, @@ -100,7 +101,9 @@ if MCP_AVAILABLE: "message": "Successfully retrieved tools" } """ - from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import MCPRequestHandler + from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import ( + MCPRequestHandler, + ) try: # Extract auth headers from request @@ -172,10 +175,11 @@ if MCP_AVAILABLE: """ REST API to call a specific MCP tool with the provided arguments """ - from litellm.proxy.proxy_server import add_litellm_data_to_request, proxy_config - from litellm.exceptions import BlockedPiiEntityError, GuardrailRaisedException from fastapi import HTTPException + from litellm.exceptions import BlockedPiiEntityError, GuardrailRaisedException + from litellm.proxy.proxy_server import add_litellm_data_to_request, proxy_config + try: data = await request.json() data = await add_litellm_data_to_request( @@ -230,13 +234,19 @@ if MCP_AVAILABLE: from litellm.proxy.management_endpoints.mcp_management_endpoints import ( NewMCPServerRequest, ) - @router.post("/test/connection") - async def test_connection( - request: NewMCPServerRequest, - ): + + async def _execute_with_mcp_client(request: NewMCPServerRequest, operation): """ - Test if we can connect to the provided MCP server before adding it + Common helper to create MCP client, execute operation, and ensure proper cleanup. + + Args: + request: MCP server configuration + operation: Async function that takes a client and returns the operation result + + Returns: + Operation result or error response """ + client = None try: client = global_mcp_server_manager._create_mcp_client( server=MCPServer( @@ -250,12 +260,31 @@ if MCP_AVAILABLE: ), mcp_auth_header=None, ) - - await client.connect() + + return await operation(client) + except Exception as e: - verbose_logger.error(f"Error in test_connection: {e}", exc_info=True) + verbose_logger.error(f"Error in MCP operation: {e}", exc_info=True) return {"status": "error", "message": "An internal error has occurred."} - return {"status": "ok"} + finally: + # Ensure client is properly disconnected before response is sent + if client is not None: + try: + await client.disconnect() + except Exception as e: + verbose_logger.warning(f"Error disconnecting MCP client: {e}") + @router.post("/test/connection") + async def test_connection( + request: NewMCPServerRequest, + ): + """ + Test if we can connect to the provided MCP server before adding it + """ + async def _test_connection_operation(client): + await client.connect() + return {"status": "ok"} + + return await _execute_with_mcp_client(request, _test_connection_operation) @router.post("/test/tools/list") @@ -266,25 +295,13 @@ if MCP_AVAILABLE: """ Preview tools available from MCP server before adding it """ - try: - client = global_mcp_server_manager._create_mcp_client( - server=MCPServer( - server_id=request.server_id or "", - name=request.alias or request.server_name or "", - url=request.url, - transport=request.transport, - spec_version=_convert_protocol_version_to_enum(request.spec_version), - auth_type=request.auth_type, - mcp_info=request.mcp_info, - ), - mcp_auth_header=None, - ) - list_tools_result = await client.list_tools() - except Exception as e: - verbose_logger.error(f"Error in test_tools_list: {e}", exc_info=True) - return {"status": "error", "message": "An internal error has occurred."} - return { - "tools": list_tools_result, - "error": None, - "message": "Successfully retrieved tools" - } + async def _list_tools_operation(client): + list_tools_result: List[MCPTool] = await client.list_tools() + model_dumped_tools: List[dict] = [tool.model_dump() for tool in list_tools_result] + return { + "tools": model_dumped_tools, + "error": None, + "message": "Successfully retrieved tools" + } + + return await _execute_with_mcp_client(request, _list_tools_operation) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx index 528da6856e1..1087977b4c5 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx @@ -39,6 +39,23 @@ const CreateMCPServer: React.FC = ({ const [tools, setTools] = useState([]) const [transportType, setTransportType] = useState("sse") const [searchValue, setSearchValue] = useState("") + const [urlWarning, setUrlWarning] = useState("") + + // Function to check URL format based on transport type + const checkUrlFormat = (url: string, transport: string) => { + if (!url) { + setUrlWarning("") + return + } + + if (transport === "sse" && !url.endsWith("/sse")) { + setUrlWarning("Typically MCP SSE URLs end with /sse. You can add this url but this is a warning.") + } else if (transport === "http" && !url.endsWith("/mcp")) { + setUrlWarning("Typically MCP HTTP URLs end with /mcp. You can add this url but this is a warning.") + } else { + setUrlWarning("") + } + } const handleCreate = async (formValues: Record) => { setIsLoading(true) @@ -110,6 +127,7 @@ const CreateMCPServer: React.FC = ({ form.resetFields() setCostConfig({}) setTools([]) + setUrlWarning("") setModalVisible(false) onCreateSuccess(response) } @@ -125,6 +143,7 @@ const CreateMCPServer: React.FC = ({ form.resetFields() setCostConfig({}) setTools([]) + setUrlWarning("") setModalVisible(false) } @@ -133,8 +152,14 @@ const CreateMCPServer: React.FC = ({ // Clear fields that are not relevant for the selected transport if (value === "stdio") { form.setFieldsValue({ url: undefined, auth_type: undefined }) + setUrlWarning("") } else { form.setFieldsValue({ command: undefined, args: undefined, env: undefined }) + // Check URL format for the new transport type + const currentUrl = form.getFieldValue("url") + if (currentUrl) { + checkUrlFormat(currentUrl, value) + } } } @@ -310,10 +335,18 @@ const CreateMCPServer: React.FC = ({ { validator: (_, value) => validateMCPServerUrl(value) }, ]} > - +
+ checkUrlFormat(e.target.value, transportType)} + /> + {urlWarning && ( +
+ {urlWarning} +
+ )} +
)}