fix(mcp): preserve empty LazyMCP scope

This commit is contained in:
jibanez-staticduo 2026-05-19 09:29:10 +02:00
parent dfa0c1a392
commit 6593b0788e
No known key found for this signature in database
3 changed files with 103 additions and 2 deletions

View file

@ -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")

View file

@ -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

View file

@ -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,