mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
[MCP] Bug fix - adding SSE MCP tools - fix connection test when adding MCPs (#14048)
* fix: _execute_with_mcp_client * add warning for SSE MCP * docs - MCP videos / guides
This commit is contained in:
parent
37a599932d
commit
f3338bef9f
3 changed files with 112 additions and 41 deletions
|
|
@ -40,7 +40,28 @@ LiteLLM supports the following MCP transports:
|
|||
style={{width: '80%', display: 'block', margin: '0'}}
|
||||
/>
|
||||
|
||||
### Adding a stdio MCP Server
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
### Add HTTP MCP Server
|
||||
|
||||
This video walks through adding and using an HTTP MCP server on LiteLLM UI and using it in Cursor IDE.
|
||||
|
||||
<iframe width="840" height="500" src="https://www.loom.com/embed/e2aebce78e8d46beafeb4bacdde31f14" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
### Add SSE MCP Server
|
||||
|
||||
This video walks through adding and using an SSE MCP server on LiteLLM UI and using it in Cursor IDE.
|
||||
|
||||
<iframe width="840" height="500" src="https://www.loom.com/embed/07e04e27f5e74475b9cf8ef8247d2c3e" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
### 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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,23 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
const [tools, setTools] = useState<any[]>([])
|
||||
const [transportType, setTransportType] = useState<string>("sse")
|
||||
const [searchValue, setSearchValue] = useState<string>("")
|
||||
const [urlWarning, setUrlWarning] = useState<string>("")
|
||||
|
||||
// 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<string, any>) => {
|
||||
setIsLoading(true)
|
||||
|
|
@ -110,6 +127,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
form.resetFields()
|
||||
setCostConfig({})
|
||||
setTools([])
|
||||
setUrlWarning("")
|
||||
setModalVisible(false)
|
||||
onCreateSuccess(response)
|
||||
}
|
||||
|
|
@ -125,6 +143,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
form.resetFields()
|
||||
setCostConfig({})
|
||||
setTools([])
|
||||
setUrlWarning("")
|
||||
setModalVisible(false)
|
||||
}
|
||||
|
||||
|
|
@ -133,8 +152,14 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
// 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<CreateMCPServerProps> = ({
|
|||
{ validator: (_, value) => validateMCPServerUrl(value) },
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="https://your-mcp-server.com"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<TextInput
|
||||
placeholder="https://your-mcp-server.com"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
onChange={(e) => checkUrlFormat(e.target.value, transportType)}
|
||||
/>
|
||||
{urlWarning && (
|
||||
<div className="mt-1 text-red-500 text-sm font-medium">
|
||||
{urlWarning}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue