mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
fix(mcp): apply key/team-attached guardrails on MCP tool calls
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
cd63c7e5a7
commit
feaab767d1
2 changed files with 65 additions and 0 deletions
|
|
@ -5082,6 +5082,18 @@ class MCPServerManager:
|
|||
synthetic_llm_data: Final = proxy_logging_obj._convert_mcp_to_llm_format(mcp_request_obj, pre_hook_kwargs)
|
||||
synthetic_llm_data["litellm_logging_obj"] = litellm_logging_obj
|
||||
|
||||
# The synthetic request never went through ``add_litellm_data_to_request``, so without this
|
||||
# a guardrail attached to the key/team/project (rather than ``default_on``) is invisible to
|
||||
# ``CustomGuardrail.should_run_guardrail`` and silently skipped on the MCP path.
|
||||
if user_api_key_auth is not None:
|
||||
from litellm.proxy.litellm_pre_call_utils import move_guardrails_to_metadata
|
||||
|
||||
await move_guardrails_to_metadata(
|
||||
data=synthetic_llm_data,
|
||||
_metadata_variable_name="metadata",
|
||||
user_api_key_dict=user_api_key_auth,
|
||||
)
|
||||
|
||||
try:
|
||||
# Use standard pre_call_hook
|
||||
modified_data: Final = await proxy_logging_obj.pre_call_hook(
|
||||
|
|
|
|||
|
|
@ -10882,3 +10882,56 @@ class TestOpenApiHandlerRelaysUpstreamAuth:
|
|||
|
||||
assert result.isError is True
|
||||
assert "upstream returned HTTP 503" in result.content[0].text
|
||||
|
||||
|
||||
class TestKeyAttachedGuardrailsReachTheMCPPreCallHook:
|
||||
"""MCP tool calls build their own synthetic request instead of going through
|
||||
``add_litellm_data_to_request``, so guardrails attached to the key/team/project
|
||||
have to be moved into that request's metadata here. Without it,
|
||||
``CustomGuardrail.should_run_guardrail`` sees no requested guardrails and skips
|
||||
everything that is not ``default_on``, i.e. the guardrail a key was explicitly
|
||||
granted never runs on the MCP path.
|
||||
"""
|
||||
|
||||
def _server(self) -> MCPServer:
|
||||
return MCPServer(
|
||||
server_id="guardrail-metadata-server",
|
||||
name="guardrail_metadata_server",
|
||||
server_name="guardrail_metadata_server",
|
||||
url="https://example.com/mcp",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.none,
|
||||
)
|
||||
|
||||
async def _synthetic_request_metadata(self, key_metadata: dict[str, Any]) -> dict[str, Any]:
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.utils import ProxyLogging
|
||||
|
||||
proxy_logging_obj: Final = ProxyLogging(user_api_key_cache=MagicMock())
|
||||
seen: Final[dict[str, Any]] = {}
|
||||
|
||||
async def capture_pre_call_hook(*, user_api_key_dict, data, call_type):
|
||||
seen.update(data)
|
||||
return None
|
||||
|
||||
with patch.object(proxy_logging_obj, "pre_call_hook", side_effect=capture_pre_call_hook):
|
||||
await MCPServerManager().pre_call_tool_check(
|
||||
name="echo",
|
||||
arguments={"text": "hi"},
|
||||
server_name="guardrail_metadata_server",
|
||||
user_api_key_auth=UserAPIKeyAuth(api_key="sk-test", metadata=key_metadata),
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
server=self._server(),
|
||||
)
|
||||
|
||||
return seen["metadata"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_key_guardrails_are_moved_into_the_synthetic_request_metadata(self):
|
||||
metadata = await self._synthetic_request_metadata({"guardrails": ["model-armor-guard"]})
|
||||
assert metadata["guardrails"] == ["model-armor-guard"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_key_without_guardrails_stays_untouched(self):
|
||||
metadata = await self._synthetic_request_metadata({})
|
||||
assert "guardrails" not in metadata
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue