mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-26 01:12:21 +00:00
fix(mcp): preserve empty LazyMCP scope
This commit is contained in:
parent
dfa0c1a392
commit
6593b0788e
3 changed files with 103 additions and 2 deletions
|
|
@ -68,7 +68,7 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
def _encode_lazymcp_tool_server_map_value(
|
||||
mcp_servers: Optional[List[str]], toolset_id: Optional[str]
|
||||
) -> str:
|
||||
payload = {"mcp_servers": mcp_servers or [], "toolset_id": toolset_id}
|
||||
payload = {"mcp_servers": mcp_servers, "toolset_id": toolset_id}
|
||||
return f"{LITELLM_PROXY_LAZYMCP_TOOL_SERVER_MAP_PREFIX}{json.dumps(payload, sort_keys=True)}"
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -88,6 +88,8 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
if not isinstance(decoded, dict):
|
||||
return {"mcp_servers": [], "toolset_id": None}
|
||||
mcp_servers = decoded.get("mcp_servers")
|
||||
if mcp_servers is None and "mcp_servers" in decoded:
|
||||
return decoded
|
||||
if not isinstance(mcp_servers, list):
|
||||
decoded["mcp_servers"] = []
|
||||
return decoded
|
||||
|
|
@ -886,7 +888,7 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
set_auth_context,
|
||||
)
|
||||
|
||||
lazy_mcp_servers = lazymcp_scope.get("mcp_servers") or None
|
||||
lazy_mcp_servers = lazymcp_scope.get("mcp_servers")
|
||||
if not isinstance(lazy_mcp_servers, list):
|
||||
lazy_mcp_servers = None
|
||||
lazy_toolset_id = lazymcp_scope.get("toolset_id")
|
||||
|
|
|
|||
|
|
@ -378,6 +378,60 @@ async def test_lazymcp_call_uses_unavailable_server_error_when_server_missing():
|
|||
assert payload["error"] == "MCP server is not available for this request."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lazymcp_call_empty_scope_cannot_execute_allowed_server():
|
||||
try:
|
||||
from mcp.types import Tool as MCPTool
|
||||
|
||||
from litellm.proxy._experimental.mcp_server.server import (
|
||||
_lazymcp_call,
|
||||
set_auth_context,
|
||||
)
|
||||
except ImportError:
|
||||
pytest.skip("MCP server not available")
|
||||
|
||||
allowed_server = MCPServer(
|
||||
server_id="allowed-server",
|
||||
name="github",
|
||||
alias="github",
|
||||
server_name="github",
|
||||
transport=MCPTransport.http,
|
||||
)
|
||||
visible_tool = MCPTool(
|
||||
name="github-search",
|
||||
description="Search GitHub",
|
||||
inputSchema={"type": "object"},
|
||||
)
|
||||
|
||||
set_auth_context(UserAPIKeyAuth(api_key="sk-test", user_id="user"), mcp_servers=[])
|
||||
with (
|
||||
patch(
|
||||
"litellm.proxy._experimental.mcp_server.server.global_mcp_server_manager.get_allowed_mcp_servers",
|
||||
AsyncMock(return_value=["allowed-server"]),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy._experimental.mcp_server.server.global_mcp_server_manager.get_mcp_server_by_id",
|
||||
MagicMock(return_value=allowed_server),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy._experimental.mcp_server.server.global_mcp_server_manager._get_tools_from_server",
|
||||
AsyncMock(return_value=[visible_tool]),
|
||||
) as tools_mock,
|
||||
patch(
|
||||
"litellm.proxy._experimental.mcp_server.server.call_mcp_tool",
|
||||
AsyncMock(),
|
||||
) as call_mock,
|
||||
):
|
||||
result = await _lazymcp_call(
|
||||
{"server": "github", "tool": "github-search", "arguments": {}}
|
||||
)
|
||||
|
||||
payload = json.loads(result.content[0].text)
|
||||
assert payload["error"] == "MCP server is not available for this request."
|
||||
tools_mock.assert_not_awaited()
|
||||
call_mock.assert_not_awaited()
|
||||
|
||||
|
||||
def test_lazymcp_cache_get_set_and_invalidate_paths(monkeypatch):
|
||||
try:
|
||||
from litellm.proxy._experimental.mcp_server import server as mcp_server_module
|
||||
|
|
|
|||
|
|
@ -254,6 +254,9 @@ def test_decode_lazymcp_tool_server_map_value_handles_invalid_payloads():
|
|||
assert LiteLLM_Proxy_MCP_Handler._decode_lazymcp_tool_server_map_value(
|
||||
'lazymcp:{"mcp_servers":"github"}'
|
||||
) == {"mcp_servers": []}
|
||||
assert LiteLLM_Proxy_MCP_Handler._decode_lazymcp_tool_server_map_value(
|
||||
LiteLLM_Proxy_MCP_Handler._encode_lazymcp_tool_server_map_value(None, "toolset")
|
||||
) == {"mcp_servers": None, "toolset_id": "toolset"}
|
||||
|
||||
|
||||
def test_should_use_litellm_mcp_gateway_matches_proxy_urls():
|
||||
|
|
@ -573,6 +576,48 @@ async def test_execute_tool_calls_passes_lazymcp_route_scope(monkeypatch):
|
|||
assert captured["mcp_servers"] == ["github"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_tool_calls_preserves_empty_lazymcp_scope(monkeypatch):
|
||||
proxy_module = types.SimpleNamespace(proxy_logging_obj=object())
|
||||
monkeypatch.setitem(sys.modules, "litellm.proxy.proxy_server", proxy_module)
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_set_auth_context(**kwargs):
|
||||
captured.update(kwargs)
|
||||
|
||||
async def fake_lazymcp_tool_call(_name, _arguments):
|
||||
return _DummyMCPResult()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy._experimental.mcp_server.server.set_auth_context",
|
||||
fake_set_auth_context,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy._experimental.mcp_server.server.lazymcp_tool_call",
|
||||
fake_lazymcp_tool_call,
|
||||
)
|
||||
tool_server_map_value = (
|
||||
LiteLLM_Proxy_MCP_Handler._encode_lazymcp_tool_server_map_value([], None)
|
||||
)
|
||||
|
||||
await LiteLLM_Proxy_MCP_Handler._execute_tool_calls(
|
||||
tool_server_map={"mcp_call": tool_server_map_value},
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call-lazy-empty-scope",
|
||||
"function": {
|
||||
"name": "mcp_call",
|
||||
"arguments": '{"server":"github","tool":"search","arguments":{}}',
|
||||
},
|
||||
}
|
||||
],
|
||||
user_api_key_auth=None,
|
||||
)
|
||||
|
||||
assert captured["mcp_servers"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_tool_calls_passes_lazymcp_client_ip_and_scoped_permissions(
|
||||
monkeypatch,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue