Skip to content

Commit a13aa47

Browse files
authored
[Fixes] Bug fixes to using LiteLLM MCP Gateway (#14392)
* fix: use _get_mcp_servers_in_path * fix checks for using litellm_proxy as MCP tool provider * fix: fix mcp_tools_with_litellm_proxy * fix: fix aresponses_api_with_mcp * aresponses_api_with_mcp * test_mcp_allowed_tools_filtering * fix: _filter_mcp_tools_by_allowed_tools * fix: _filter_mcp_tools_by_allowed_tools * test_streaming_responses_api_with_mcp_tools * fixes: test tools transfrom MCP->OpenaI spec * test_streaming_responses_api_with_mcp_tools * fix: chat ui allow multi select with allowed tools * fix: use correct MCP events with litellm proxy response API * fix get_event_model_class * fix litellm proxy MCP handler * fix MCPEnhancedStreamingIterator * chat ui show list tools result * UI: show MCP events * fix stream iterator * fixes: litellm proxy mcp handler * test responses + mcp * fix: update responses api with mcp handling * ruff check fix * central: _process_mcp_tools_to_openai_format * fix: refactor code * test_mcp_allowed_tools_filtering * test mcp with litellm proxy * fix mcp call * demo: video using MCP ui * fixes for using stream iterator * test_no_duplicate_mcp_tools_in_streaming_e2e * docs fix * fix code snippet
1 parent c09d9e5 commit a13aa47

File tree

20 files changed

+2814
-237
lines changed

20 files changed

+2814
-237
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
Use LiteLLM Proxy MCP Gateway to call MCP tools.
3+
4+
When using LiteLLM Proxy, you can use the same MCP tools across all your LLM providers.
5+
"""
6+
import openai
7+
8+
client = openai.OpenAI(
9+
api_key="sk-1234", # paste your litellm proxy api key here
10+
base_url="http://localhost:4000" # paste your litellm proxy base url here
11+
)
12+
print("Making API request to Responses API with MCP tools")
13+
14+
response = client.responses.create(
15+
model="gpt-5",
16+
input=[
17+
{
18+
"role": "user",
19+
"content": "give me TLDR of what BerriAI/litellm repo is about",
20+
"type": "message"
21+
}
22+
],
23+
tools=[
24+
{
25+
"type": "mcp",
26+
"server_label": "litellm",
27+
"server_url": "litellm_proxy",
28+
"require_approval": "never"
29+
}
30+
],
31+
stream=True,
32+
tool_choice="required"
33+
)
34+
35+
for chunk in response:
36+
print("response chunk: ", chunk)

docs/my-website/docs/mcp.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,17 @@ litellm_settings:
197197

198198
### Use on LiteLLM UI
199199

200+
Follow this walkthrough to use your MCP on LiteLLM UI
201+
202+
<iframe width="840" height="500" src="https://www.loom.com/embed/57e0763267254bc79dbe6658d0b8758c" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
203+
200204
### Use with Responses API
201205

202206
Replace `http://localhost:4000` with your LiteLLM Proxy base URL.
203207

208+
Demo Video Using Responses API with LiteLLM Proxy: [Demo video here](https://www.loom.com/share/34587e618c5c47c0b0d67b4e4d02718f?sid=2caf3d45-ead4-4490-bcc1-8d6dd6041c02)
209+
210+
204211
<Tabs>
205212
<TabItem value="curl" label="cURL">
206213

@@ -234,12 +241,18 @@ curl --location 'http://localhost:4000/v1/responses' \
234241
<TabItem value="python" label="Python SDK">
235242

236243
```python title="Python SDK Example" showLineNumbers
244+
"""
245+
Use LiteLLM Proxy MCP Gateway to call MCP tools.
246+
247+
When using LiteLLM Proxy, you can use the same MCP tools across all your LLM providers.
248+
"""
237249
import openai
238250
239251
client = openai.OpenAI(
240-
api_key="sk-1234",
241-
base_url="http://localhost:4000"
252+
api_key="sk-1234", # paste your litellm proxy api key here
253+
base_url="http://localhost:4000" # paste your litellm proxy base url here
242254
)
255+
print("Making API request to Responses API with MCP tools")
243256
244257
response = client.responses.create(
245258
model="gpt-5",
@@ -262,7 +275,8 @@ response = client.responses.create(
262275
tool_choice="required"
263276
)
264277
265-
print(response)
278+
for chunk in response:
279+
print("response chunk: ", chunk)
266280
```
267281

268282
</TabItem>

litellm/experimental_mcp_client/tools.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,60 @@
1717
########################################################
1818
def transform_mcp_tool_to_openai_tool(mcp_tool: MCPTool) -> ChatCompletionToolParam:
1919
"""Convert an MCP tool to an OpenAI tool."""
20+
normalized_parameters = _normalize_mcp_input_schema(mcp_tool.inputSchema)
21+
2022
return ChatCompletionToolParam(
2123
type="function",
2224
function=FunctionDefinition(
2325
name=mcp_tool.name,
2426
description=mcp_tool.description or "",
25-
parameters=mcp_tool.inputSchema,
27+
parameters=normalized_parameters,
2628
strict=False,
2729
),
2830
)
2931

3032

33+
def _normalize_mcp_input_schema(input_schema: dict) -> dict:
34+
"""
35+
Normalize MCP input schema to ensure it's valid for OpenAI function calling.
36+
37+
OpenAI requires that function parameters have:
38+
- type: 'object'
39+
- properties: dict (can be empty)
40+
- additionalProperties: false (recommended)
41+
"""
42+
if not input_schema:
43+
return {
44+
"type": "object",
45+
"properties": {},
46+
"additionalProperties": False
47+
}
48+
49+
# Make a copy to avoid modifying the original
50+
normalized_schema = dict(input_schema)
51+
52+
# Ensure type is 'object'
53+
if "type" not in normalized_schema:
54+
normalized_schema["type"] = "object"
55+
56+
# Ensure properties exists (can be empty)
57+
if "properties" not in normalized_schema:
58+
normalized_schema["properties"] = {}
59+
60+
# Add additionalProperties if not present (recommended by OpenAI)
61+
if "additionalProperties" not in normalized_schema:
62+
normalized_schema["additionalProperties"] = False
63+
64+
return normalized_schema
65+
66+
3167
def transform_mcp_tool_to_openai_responses_api_tool(mcp_tool: MCPTool) -> FunctionToolParam:
3268
"""Convert an MCP tool to an OpenAI Responses API tool."""
69+
normalized_parameters = _normalize_mcp_input_schema(mcp_tool.inputSchema)
70+
3371
return FunctionToolParam(
3472
name=mcp_tool.name,
35-
parameters=mcp_tool.inputSchema,
73+
parameters=normalized_parameters,
3674
strict=False,
3775
type="function",
3876
description=mcp_tool.description or "",

litellm/llms/openai/responses/transformation.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,14 @@ def get_event_model_class(event_type: str) -> Any:
272272
ResponsesAPIStreamEvents.WEB_SEARCH_CALL_IN_PROGRESS: WebSearchCallInProgressEvent,
273273
ResponsesAPIStreamEvents.WEB_SEARCH_CALL_SEARCHING: WebSearchCallSearchingEvent,
274274
ResponsesAPIStreamEvents.WEB_SEARCH_CALL_COMPLETED: WebSearchCallCompletedEvent,
275+
ResponsesAPIStreamEvents.MCP_LIST_TOOLS_IN_PROGRESS: MCPListToolsInProgressEvent,
276+
ResponsesAPIStreamEvents.MCP_LIST_TOOLS_COMPLETED: MCPListToolsCompletedEvent,
277+
ResponsesAPIStreamEvents.MCP_LIST_TOOLS_FAILED: MCPListToolsFailedEvent,
278+
ResponsesAPIStreamEvents.MCP_CALL_IN_PROGRESS: MCPCallInProgressEvent,
279+
ResponsesAPIStreamEvents.MCP_CALL_ARGUMENTS_DELTA: MCPCallArgumentsDeltaEvent,
280+
ResponsesAPIStreamEvents.MCP_CALL_ARGUMENTS_DONE: MCPCallArgumentsDoneEvent,
281+
ResponsesAPIStreamEvents.MCP_CALL_COMPLETED: MCPCallCompletedEvent,
282+
ResponsesAPIStreamEvents.MCP_CALL_FAILED: MCPCallFailedEvent,
275283
ResponsesAPIStreamEvents.ERROR: ErrorEvent,
276284
}
277285

litellm/proxy/_experimental/mcp_server/server.py

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,9 @@ async def mcp_server_tool_call(
215215
"""
216216
from fastapi import Request
217217

218+
from litellm.exceptions import BlockedPiiEntityError, GuardrailRaisedException
218219
from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request
219220
from litellm.proxy.proxy_server import proxy_config
220-
from litellm.exceptions import BlockedPiiEntityError, GuardrailRaisedException
221221

222222
# Validate arguments
223223
user_api_key_auth, mcp_auth_header, _, mcp_server_auth_headers, mcp_protocol_version = get_auth_context()
@@ -279,33 +279,15 @@ async def mcp_server_tool_call(
279279
############ Helper Functions ##########################
280280
########################################################
281281

282-
async def _get_tools_from_mcp_servers(
283-
user_api_key_auth: Optional[UserAPIKeyAuth],
284-
mcp_auth_header: Optional[str],
282+
async def _get_allowed_mcp_servers_from_mcp_server_names(
285283
mcp_servers: Optional[List[str]],
286-
mcp_server_auth_headers: Optional[Dict[str, str]] = None,
287-
mcp_protocol_version: Optional[str] = None,
288-
) -> List[MCPTool]:
284+
allowed_mcp_servers: List[str],
285+
) -> List[str]:
289286
"""
290-
Helper method to fetch tools from MCP servers based on server filtering criteria.
291-
292-
Args:
293-
user_api_key_auth: User authentication info for access control
294-
mcp_auth_header: Optional auth header for MCP server (deprecated)
295-
mcp_servers: Optional list of server names/aliases to filter by
296-
mcp_server_auth_headers: Optional dict of server-specific auth headers {server_alias: auth_value}
297-
298-
Returns:
299-
List[MCPTool]: Combined list of tools from filtered servers
287+
Get the filtered MCP servers from the MCP server names
300288
"""
301-
if not MCP_AVAILABLE:
302-
return []
303-
304-
# Get allowed MCP servers based on user permissions
305-
allowed_mcp_servers = await global_mcp_server_manager.get_allowed_mcp_servers(user_api_key_auth)
306-
307-
filtered_server_ids = set()
308-
289+
from typing import Set
290+
filtered_server_ids: Set[str] = set()
309291
# Filter servers based on mcp_servers parameter if provided
310292
if mcp_servers is not None:
311293
for server_or_group in mcp_servers:
@@ -336,6 +318,40 @@ async def _get_tools_from_mcp_servers(
336318

337319
if filtered_server_ids:
338320
allowed_mcp_servers = list(filtered_server_ids)
321+
322+
return allowed_mcp_servers
323+
324+
async def _get_tools_from_mcp_servers(
325+
user_api_key_auth: Optional[UserAPIKeyAuth],
326+
mcp_auth_header: Optional[str],
327+
mcp_servers: Optional[List[str]],
328+
mcp_server_auth_headers: Optional[Dict[str, str]] = None,
329+
mcp_protocol_version: Optional[str] = None,
330+
) -> List[MCPTool]:
331+
"""
332+
Helper method to fetch tools from MCP servers based on server filtering criteria.
333+
334+
Args:
335+
user_api_key_auth: User authentication info for access control
336+
mcp_auth_header: Optional auth header for MCP server (deprecated)
337+
mcp_servers: Optional list of server names/aliases to filter by
338+
mcp_server_auth_headers: Optional dict of server-specific auth headers {server_alias: auth_value}
339+
340+
Returns:
341+
List[MCPTool]: Combined list of tools from filtered servers
342+
"""
343+
if not MCP_AVAILABLE:
344+
return []
345+
346+
# Get allowed MCP servers based on user permissions
347+
allowed_mcp_servers = await global_mcp_server_manager.get_allowed_mcp_servers(user_api_key_auth)
348+
349+
if mcp_servers is not None:
350+
allowed_mcp_servers = await _get_allowed_mcp_servers_from_mcp_server_names(
351+
mcp_servers=mcp_servers,
352+
allowed_mcp_servers=allowed_mcp_servers,
353+
)
354+
339355

340356
# Get tools from each allowed server
341357
all_tools = []
@@ -556,20 +572,25 @@ async def _handle_local_mcp_tool(
556572
except Exception as e:
557573
return [TextContent(text=f"Error: {str(e)}", type="text")]
558574

559-
async def extract_mcp_auth_context(scope, path):
575+
def _get_mcp_servers_in_path(path: str) -> Optional[List[str]]:
560576
"""
561-
Extracts mcp_servers from the path and processes the MCP request for auth context.
562-
Returns: (user_api_key_auth, mcp_auth_header, mcp_servers, mcp_server_auth_headers)
577+
Get the MCP servers from the path
563578
"""
564579
import re
565-
566-
mcp_servers_from_path = None
580+
mcp_servers_from_path: Optional[List[str]] = None
567581
mcp_path_match = re.match(r"^/mcp/([^/]+)(/.*)?$", path)
568582
if mcp_path_match:
569583
mcp_servers_str = mcp_path_match.group(1)
570584
if mcp_servers_str:
571585
mcp_servers_from_path = [s.strip() for s in mcp_servers_str.split(",") if s.strip()]
586+
return mcp_servers_from_path
572587

588+
async def extract_mcp_auth_context(scope, path):
589+
"""
590+
Extracts mcp_servers from the path and processes the MCP request for auth context.
591+
Returns: (user_api_key_auth, mcp_auth_header, mcp_servers, mcp_server_auth_headers)
592+
"""
593+
mcp_servers_from_path = _get_mcp_servers_in_path(path)
573594
if mcp_servers_from_path is not None:
574595
(
575596
user_api_key_auth,

litellm/proxy/management_endpoints/mcp_management_endpoints.py

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,18 @@
1717
"""
1818

1919
import importlib
20-
from typing import Iterable, List, Optional
2120
from datetime import datetime
21+
from typing import Iterable, List, Optional
2222

2323
from fastapi import APIRouter, Depends, Header, HTTPException, Response, status
2424
from fastapi.responses import JSONResponse
2525

2626
import litellm
2727
from litellm._logging import verbose_logger, verbose_proxy_logger
2828
from litellm.constants import LITELLM_PROXY_ADMIN_NAME
29-
from litellm.proxy._experimental.mcp_server.utils import validate_and_normalize_mcp_server_payload
29+
from litellm.proxy._experimental.mcp_server.utils import (
30+
validate_and_normalize_mcp_server_payload,
31+
)
3032

3133
router = APIRouter(prefix="/v1/mcp", tags=["mcp"])
3234
MCP_AVAILABLE: bool = True
@@ -94,34 +96,17 @@ async def get_mcp_tools(
9496
"""
9597
Get all MCP tools available for the current key, including those from access groups
9698
"""
97-
from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import (
98-
MCPRequestHandler,
99-
)
100-
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
101-
global_mcp_server_manager,
99+
from litellm.proxy._experimental.mcp_server.server import _list_mcp_tools
100+
tools = await _list_mcp_tools(
101+
user_api_key_auth=user_api_key_dict,
102+
mcp_auth_header=None,
103+
mcp_servers=None,
104+
mcp_server_auth_headers=None,
105+
mcp_protocol_version=None,
102106
)
107+
dumped_tools = [dict(tool) for tool in tools]
103108

104-
# This now includes both direct and access group servers
105-
server_ids = await MCPRequestHandler._get_allowed_mcp_servers_for_key(user_api_key_dict)
106-
107-
tools = []
108-
errors = []
109-
for server_id in server_ids:
110-
try:
111-
server_tools = await global_mcp_server_manager.get_tools_for_server(server_id)
112-
tools.extend(server_tools)
113-
verbose_proxy_logger.debug(f"Successfully fetched {len(server_tools)} tools from server {server_id}")
114-
except Exception as e:
115-
error_msg = f"Failed to get tools from server {server_id}: {str(e)}"
116-
verbose_proxy_logger.warning(error_msg)
117-
errors.append(error_msg)
118-
# Continue with other servers instead of failing completely
119-
120-
verbose_proxy_logger.debug(f"Available tools: {tools}")
121-
if errors:
122-
verbose_proxy_logger.warning(f"Some servers failed to respond: {errors}")
123-
124-
return {"tools": tools}
109+
return {"tools": dumped_tools}
125110

126111
@router.get(
127112
"/access_groups",
@@ -134,8 +119,10 @@ async def get_mcp_access_groups(
134119
"""
135120
Get all available MCP access groups from the database AND config
136121
"""
122+
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
123+
global_mcp_server_manager,
124+
)
137125
from litellm.proxy.proxy_server import prisma_client
138-
from litellm.proxy._experimental.mcp_server.mcp_server_manager import global_mcp_server_manager
139126

140127
access_groups = set()
141128

litellm/proxy/proxy_config.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,16 @@ model_list:
33
litellm_params:
44
model: openai/*
55
api_base: https://exampleopenaiendpoint-production-0ee2.up.railway.app/
6+
- model_name: bedrock/*
7+
litellm_params:
8+
model: bedrock/*
9+
- model_name: openai/*
10+
litellm_params:
11+
model: openai/*
12+
- model_name: gemini/*
13+
litellm_params:
14+
model: gemini/*
15+
16+
617
litellm_settings:
718
callbacks: ["cloudzero"]

0 commit comments

Comments
 (0)