mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-08 08:08:37 +00:00
* refactor(mcp): drop the experimental label from the MCP server and client paths Move litellm/proxy/_experimental/mcp_server to litellm/proxy/mcp_server and litellm/experimental_mcp_client to litellm/mcp_client, with the mirrored tests moved under tests/unit/proxy/mcp_server and tests/unit/mcp_client. Both old packages stay as shims that warn with DeprecationWarning, re-export the new package's public names and alias every submodule in sys.modules so deep imports keep resolving to the same objects. The OpenAPI docs gate now puts the merge base's enterprise package on PYTHONPATH so its base counts do not mix the checked-out enterprise code with the base litellm tree Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ci(mcp): keep tests/unit/proxy/_experimental as a sharded root for the shim tests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(mcp): alias namespace subpackages and bind attributes in the deprecated MCP shims pkgutil.walk_packages skipped mcp_server/auth because it has no __init__.py, so old imports of litellm.proxy._experimental.mcp_server.auth.* failed, and sys.modules entries alone left litellm.experimental_mcp_client.tools unreachable as an attribute. Walk the package on disk and setattr each alias on its old parent. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
73 lines
3.2 KiB
Python
73 lines
3.2 KiB
Python
from dataclasses import FrozenInstanceError
|
|
from typing import Final
|
|
|
|
import pytest
|
|
from pydantic import SecretStr
|
|
|
|
from litellm.proxy._types import UserAPIKeyAuth
|
|
from litellm.proxy.mcp_server.operations import prepare_context
|
|
|
|
|
|
def test_guardrail_bearer_stays_out_of_egress_and_context_representations() -> None:
|
|
token: Final = SecretStr("caller.jwt.signature")
|
|
context: Final = prepare_context(incoming_bearer_token=token)
|
|
assert context.incoming_bearer_token is token
|
|
assert token.get_secret_value() not in repr(context)
|
|
assert token not in context.legacy_auth()
|
|
assert context.raw_headers is None
|
|
assert context.oauth2_headers is None
|
|
assert context.mcp_server_auth_headers is None
|
|
|
|
|
|
def test_operation_context_isolates_nested_headers_and_caller_permissions():
|
|
caller = UserAPIKeyAuth(user_id="alpha", models=["allowed"])
|
|
caller.mcp_admitted_user_subject = True
|
|
caller.mcp_session_resource_server_ids = ("alpha-server",)
|
|
caller.mcp_toolset_id = "toolset-alpha"
|
|
caller.mcp_source_team_rpm_limits = {"team": {"alpha-server": 2}}
|
|
headers = {"x-caller": "alpha"}
|
|
server_headers = {"alpha-server": {"authorization": "alpha-token"}}
|
|
context = prepare_context(caller, raw_headers=headers, mcp_server_auth_headers=server_headers)
|
|
|
|
caller.models.append("forbidden")
|
|
caller.mcp_source_team_rpm_limits["team"]["alpha-server"] = 999
|
|
headers["x-caller"] = "bravo"
|
|
server_headers["alpha-server"]["authorization"] = "bravo-token"
|
|
captured = context.user_api_key_auth
|
|
assert captured is not None
|
|
assert captured.models == ["allowed"]
|
|
assert captured.mcp_admitted_user_subject is True
|
|
assert captured.mcp_session_resource_server_ids == ("alpha-server",)
|
|
assert captured.mcp_toolset_id == "toolset-alpha"
|
|
assert captured.mcp_source_team_rpm_limits == {"team": {"alpha-server": 2}}
|
|
captured.models.append("also-forbidden")
|
|
assert context.user_api_key_auth.models == ["allowed"]
|
|
assert context.raw_headers == {"x-caller": "alpha"}
|
|
assert context.mcp_server_auth_headers == {"alpha-server": {"authorization": "alpha-token"}}
|
|
with pytest.raises(TypeError):
|
|
context.raw_headers["x-caller"] = "changed"
|
|
with pytest.raises(TypeError):
|
|
context.mcp_server_auth_headers["alpha-server"]["authorization"] = "changed"
|
|
with pytest.raises(FrozenInstanceError):
|
|
context.client_ip = "untrusted"
|
|
|
|
|
|
def test_operation_context_preserves_missing_and_empty_inputs():
|
|
missing = prepare_context()
|
|
empty = prepare_context(mcp_servers=[], raw_headers={}, oauth2_headers={}, mcp_server_auth_headers={})
|
|
assert missing.user_api_key_auth is None
|
|
assert missing.mcp_servers is None
|
|
assert missing.raw_headers is None
|
|
assert missing.oauth2_headers is None
|
|
assert missing.mcp_server_auth_headers is None
|
|
assert empty.mcp_servers == ()
|
|
assert empty.raw_headers == {}
|
|
assert empty.oauth2_headers == {}
|
|
assert empty.mcp_server_auth_headers == {}
|
|
|
|
|
|
def test_toolset_request_marker_cannot_be_supplied_by_caller_or_serialized():
|
|
auth = UserAPIKeyAuth.model_validate({"user_id": "alpha", "mcp_toolset_id": "forged"})
|
|
assert auth.mcp_toolset_id is None
|
|
auth.mcp_toolset_id = "server-resolved"
|
|
assert "mcp_toolset_id" not in auth.model_dump()
|