mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
Merge pull request #47804 from BerriAI/litellm_backport_47677_stable_1_105_x
fix(mcp): backport per-user auth MCP tool listing to stable/1.105.x
This commit is contained in:
commit
653f67eafc
3 changed files with 72 additions and 3 deletions
|
|
@ -4225,6 +4225,7 @@ class MCPServerManager:
|
|||
raw_headers: Mapping[str, str] | None = None,
|
||||
client_ip: str | None = None,
|
||||
protocol_version_override: MCPUpstreamProtocol | None = None,
|
||||
for_tool_listing: bool = False,
|
||||
) -> MCPClient:
|
||||
"""
|
||||
Create an MCPClient instance for the given server.
|
||||
|
|
@ -4377,6 +4378,7 @@ class MCPServerManager:
|
|||
sampling_callback=sampling_cb,
|
||||
elicitation_callback=elicitation_cb,
|
||||
),
|
||||
for_tool_listing=for_tool_listing,
|
||||
)
|
||||
|
||||
# Create SigV4 auth if configured
|
||||
|
|
@ -4421,6 +4423,7 @@ class MCPServerManager:
|
|||
sampling_callback=sampling_cb,
|
||||
elicitation_callback=elicitation_cb,
|
||||
),
|
||||
for_tool_listing=for_tool_listing,
|
||||
)
|
||||
|
||||
async def _get_tools_from_server(
|
||||
|
|
@ -4529,6 +4532,7 @@ class MCPServerManager:
|
|||
user_api_key_auth=user_api_key_auth,
|
||||
raw_headers=raw_headers,
|
||||
client_ip=client_ip,
|
||||
for_tool_listing=True,
|
||||
)
|
||||
|
||||
## HANDLE OPENAPI TOOLS
|
||||
|
|
|
|||
|
|
@ -427,6 +427,8 @@ def validate_static_credential(
|
|||
headers: Mapping[str, str],
|
||||
upstream_token_header: str | None = None,
|
||||
static_header_names: Iterable[str] = (),
|
||||
*,
|
||||
allow_absent: bool = False,
|
||||
) -> Result[None, CredError]:
|
||||
if auth_type not in _STATIC_MODES:
|
||||
return Ok(None)
|
||||
|
|
@ -442,17 +444,23 @@ def validate_static_credential(
|
|||
)
|
||||
)
|
||||
values: Final = tuple((name.lower(), value.strip()) for name, value in headers.items() if name.lower() in slots)
|
||||
if allow_absent and not values:
|
||||
return Ok(None)
|
||||
if any(_usable_credential_value(auth_type, name, value) for name, value in values):
|
||||
return Ok(None)
|
||||
return Error(CredError.of_misconfigured(f"{auth_type} requires a usable upstream credential"))
|
||||
|
||||
|
||||
async def prepare_mcp_client(server: MCPServer, client: MCPClient) -> MCPClient:
|
||||
async def prepare_mcp_client(server: MCPServer, client: MCPClient, *, for_tool_listing: bool = False) -> MCPClient:
|
||||
if server.auth_type not in _STATIC_MODES or client.transport_type == MCPTransport.stdio:
|
||||
return client
|
||||
request: Final = await client.prepare_request_auth()
|
||||
match validate_static_credential(
|
||||
server.auth_type, request.headers, server.upstream_token_header, server.static_headers or ()
|
||||
server.auth_type,
|
||||
request.headers,
|
||||
server.upstream_token_header,
|
||||
server.static_headers or (),
|
||||
allow_absent=for_tool_listing and server.auth_type == MCPAuth.authorization,
|
||||
):
|
||||
case Error(error):
|
||||
raise_public(error)
|
||||
|
|
|
|||
|
|
@ -14047,6 +14047,7 @@ def _mcp_upstream(respond):
|
|||
class _DiscoveryUpstream:
|
||||
def __init__(self) -> None:
|
||||
self.requests: tuple[tuple[str, str], ...] = ()
|
||||
self.tools: tuple[dict[str, object], ...] = ()
|
||||
self.outcome = "supported"
|
||||
self.entered = asyncio.Event()
|
||||
self.release = asyncio.Event()
|
||||
|
|
@ -14092,7 +14093,7 @@ class _DiscoveryUpstream:
|
|||
"resources/templates/list": {
|
||||
"resourceTemplates": [{"name": "example", "uriTemplate": "test://{name}", "description": "original"}]
|
||||
},
|
||||
"tools/list": {"tools": []},
|
||||
"tools/list": {"tools": list(self.tools)},
|
||||
}[payload.method]
|
||||
continuation: Final = (
|
||||
{"nextCursor": "last-page"}
|
||||
|
|
@ -14110,6 +14111,62 @@ def _discovery_server() -> MCPServer:
|
|||
return MCPServer(server_id="discovery", name="discovery", url="https://discovery.example/mcp", transport=MCPTransport.http)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_per_user_authorization_server_lists_without_static_credential() -> None:
|
||||
upstream: Final = _DiscoveryUpstream()
|
||||
upstream.tools = ({"name": "search", "inputSchema": {"type": "object"}},)
|
||||
server: Final = MCPServer(
|
||||
server_id="per-user-auth",
|
||||
name="per-user-auth",
|
||||
url="https://discovery.example/mcp",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.authorization,
|
||||
)
|
||||
manager: Final = MCPServerManager()
|
||||
manager.registry[server.server_id] = server
|
||||
|
||||
with _mcp_upstream(upstream.respond):
|
||||
tools: Final = await manager._get_tools_from_server(server, add_prefix=False)
|
||||
|
||||
assert [tool.name for tool in tools] == ["search"]
|
||||
assert ("tools/list", "") in upstream.requests
|
||||
with pytest.raises(HTTPException, match="requires a usable upstream credential"):
|
||||
await manager._create_mcp_client(server)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("auth_type", "credential", "upstream_token_header"),
|
||||
(
|
||||
(MCPAuth.authorization, "Bearer", None),
|
||||
(MCPAuth.authorization, "Bearer", "X-Token"),
|
||||
(MCPAuth.authorization, " ", None),
|
||||
(MCPAuth.authorization, " ", "X-Token"),
|
||||
(MCPAuth.bearer_token, None, None),
|
||||
),
|
||||
)
|
||||
async def test_tool_listing_rejects_malformed_or_other_missing_static_credentials(
|
||||
auth_type: MCPAuth, credential: str | None, upstream_token_header: str | None
|
||||
) -> None:
|
||||
upstream: Final = _DiscoveryUpstream()
|
||||
server: Final = MCPServer(
|
||||
server_id="invalid-static-auth",
|
||||
name="invalid-static-auth",
|
||||
url="https://discovery.example/mcp",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=auth_type,
|
||||
authentication_token=credential,
|
||||
upstream_token_header=upstream_token_header,
|
||||
)
|
||||
manager: Final = MCPServerManager()
|
||||
manager.registry[server.server_id] = server
|
||||
|
||||
with _mcp_upstream(upstream.respond), pytest.raises(MCPServerListError):
|
||||
await manager._get_tools_from_server(server)
|
||||
|
||||
assert upstream.requests == ()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("kind", ("prompts", "resources", "templates"))
|
||||
async def test_discovery_cache_reuses_raw_results_and_expires(kind: str) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue