From 41bf08cc2b237e93f4a6874146ea5b3687366f4e Mon Sep 17 00:00:00 2001 From: marcusacosta Date: Tue, 16 Jun 2026 22:06:15 -0700 Subject: [PATCH] refactor(proxy): make MCP trust scoring any-discipline clean Drop the call_tool rewrite that added trust-ranked fallback-on-failure. It rewrote a large, previously type-clean stretch of the critical tool-call path, which was both a risky drive-by refactor and the source of most Any-discipline findings. Trust filtering still runs on every list/call path, and the ranking helpers stay as tested public API. Tighten the scoring module's typed/untyped boundaries so the gate passes without blanket suppressions: pydantic fields use Annotated plus literal defaults instead of Field values, the shared cache stores the result JSON string directly so there is no dict round-trip, parsing goes through TypeAdapter instead of isinstance narrowing, and the api_key secret is read via the typed get_secret_str. The only remaining any-ok markers sit on awaited coroutines whose Send/Recv Any is a typing artifact and on required model fields mypy exports as Any, each with a reason. Co-authored-by: Cursor --- .../mcp_server/mcp_server_manager.py | 372 ++++++------------ .../mcp_server/mcp_trust_scoring.py | 167 ++++---- .../mcp_server/rest_endpoints.py | 18 +- .../proxy/_experimental/mcp_server/server.py | 11 +- litellm/proxy/proxy_server.py | 6 +- 5 files changed, 224 insertions(+), 350 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index bbdaa8d19ef..47a8a466311 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -13,19 +13,7 @@ import json import os import re import time -from typing import ( - Any, - Callable, - Dict, - List, - Literal, - Optional, - Sequence, - Set, - Tuple, - Union, - cast, -) +from typing import Any, Callable, Dict, List, Literal, Optional, Set, Tuple, Union, cast from urllib.parse import urlparse import anyio @@ -1647,12 +1635,10 @@ class MCPServerManager: for server_id in allowed_mcp_servers if (server := self.get_mcp_server_by_id(server_id)) is not None ) - allowed_mcp_servers = [ - server.server_id - for server in await apply_trust_filter_to_allowed_mcp_servers( - allowed_server_objects - ) - ] + trusted_servers = await apply_trust_filter_to_allowed_mcp_servers( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + allowed_server_objects + ) + allowed_mcp_servers = [server.server_id for server in trusted_servers] verbose_logger.debug("SERVER MANAGER LISTING TOOLS") @@ -3583,246 +3569,6 @@ class MCPServerManager: return cast(CallToolResult, result) - @staticmethod - def _should_attempt_trust_fallback(error: BaseException) -> bool: - if isinstance( - error, (BlockedPiiEntityError, GuardrailRaisedException, ValueError) - ): - return False - if isinstance(error, HTTPException): - return error.status_code not in {401, 403, 412} - return True - - def _server_exposes_tool( - self, - server: MCPServer, - tool_name: str, - server_name: str, - ) -> bool: - if server_name: - server_matches = False - for identifier in (server.alias, server.server_name, server.name): - if identifier and normalize_server_name( - identifier - ) == normalize_server_name(server_name): - server_matches = True - break - if not server_matches: - return False - - for known_prefix in iter_known_server_prefixes(server): - candidate_names = ( - tool_name, - add_server_prefix_to_name(tool_name, known_prefix), - ) - for candidate_name in candidate_names: - mapped_server = self.tool_name_to_mcp_server_name_mapping.get( - candidate_name - ) - if mapped_server is None: - continue - normalized_mapped = normalize_server_name(mapped_server) - for identifier in ( - server.alias, - server.server_name, - server.name, - known_prefix, - ): - if identifier and normalized_mapped == normalize_server_name( - identifier - ): - return True - return False - - async def _ordered_mcp_servers_for_tool_call( - self, - server_name: str, - name: str, - allowed_mcp_servers: Sequence[MCPServer] | None, - ) -> tuple[MCPServer, ...]: - primary = self._resolve_mcp_server_for_tool_call(server_name, name) - - from litellm.proxy._experimental.mcp_server.mcp_trust_scoring import ( - get_mcp_trust_scoring_client, - rank_mcp_servers_by_trust, - ) - - client = get_mcp_trust_scoring_client() - if client is None or not client.enabled or not allowed_mcp_servers: - return (primary,) - - allowed_ids = {server.server_id for server in allowed_mcp_servers} - if primary.server_id not in allowed_ids: - return (primary,) - - candidates = tuple( - server - for server in allowed_mcp_servers - if self._server_exposes_tool( - server=server, - tool_name=name, - server_name=server_name, - ) - ) - if len(candidates) <= 1: - return candidates if candidates else (primary,) - - ranked = await rank_mcp_servers_by_trust(candidates) - return ranked if ranked else (primary,) - - async def _call_tool_on_mcp_server( - self, - *, - mcp_server: MCPServer, - server_name: str, - name: str, - arguments: dict[str, Any], - user_api_key_auth: UserAPIKeyAuth | None, - mcp_auth_header: str | None, - mcp_server_auth_headers: dict[str, dict[str, str]] | None, - proxy_logging_obj: ProxyLogging | None, - oauth2_headers: dict[str, str] | None, - raw_headers: dict[str, str] | None, - host_progress_callback: Callable | None, - start_time: datetime.datetime, - ) -> CallToolResult: - hook_result: dict[str, Any] = {} - if proxy_logging_obj: - hook_result = await self.pre_call_tool_check( - name=name, - arguments=arguments, - server_name=server_name, - user_api_key_auth=user_api_key_auth, - proxy_logging_obj=proxy_logging_obj, - server=mcp_server, - raw_headers=raw_headers, - ) - if "arguments" in hook_result: - arguments = hook_result["arguments"] - - tasks: list = [] - if proxy_logging_obj: - during_hook_task = self._create_during_hook_task( - name=name, - arguments=arguments, - server_name_from_prefix=server_name, - user_api_key_auth=user_api_key_auth, - proxy_logging_obj=proxy_logging_obj, - start_time=start_time, - ) - tasks.append(during_hook_task) - - resolved_oauth2_headers = await self._resolve_oauth2_headers_for_tool_call( - mcp_server, oauth2_headers, user_api_key_auth - ) - - if mcp_server.spec_path: - verbose_logger.debug( - "Calling OpenAPI tool %s directly via HTTP handler", name - ) - if hook_result.get("extra_headers"): - verbose_logger.warning( - "pre_mcp_call hook returned extra_headers for OpenAPI-backed " - "MCP server '%s' — header injection is not supported for " - "OpenAPI servers; headers will be ignored. Use SSE/HTTP " - "transport to enable hook header injection.", - server_name, - ) - tasks.append( - asyncio.create_task( - self._call_openapi_tool_handler(mcp_server, name, arguments) - ) - ) - return await self._gather_openapi_tool_tasks(tasks, proxy_logging_obj) - - return await self._call_regular_mcp_tool( - mcp_server=mcp_server, - original_tool_name=name, - arguments=arguments, - tasks=tasks, - mcp_auth_header=mcp_auth_header, - mcp_server_auth_headers=mcp_server_auth_headers, - oauth2_headers=resolved_oauth2_headers, - raw_headers=raw_headers, - proxy_logging_obj=proxy_logging_obj, - host_progress_callback=host_progress_callback, - hook_extra_headers=hook_result.get("extra_headers"), - user_api_key_auth=user_api_key_auth, - ) - - async def call_tool( - self, - server_name: str, - name: str, - arguments: dict[str, Any], - user_api_key_auth: UserAPIKeyAuth | None = None, - mcp_auth_header: str | None = None, - mcp_server_auth_headers: dict[str, dict[str, str]] | None = None, - proxy_logging_obj: ProxyLogging | None = None, - oauth2_headers: dict[str, str] | None = None, - raw_headers: dict[str, str] | None = None, - host_progress_callback: Callable | None = None, - allowed_mcp_servers: Sequence[MCPServer] | None = None, - ) -> CallToolResult: - """ - Call a tool with the given name and arguments - - Args: - server_name: Server name - name: Tool name - arguments: Tool arguments - user_api_key_auth: User authentication - mcp_auth_header: MCP auth header (deprecated) - mcp_server_auth_headers: Optional dict of server-specific auth headers {server_alias: auth_value} - proxy_logging_obj: Optional ProxyLogging object for hook integration - allowed_mcp_servers: Optional allowed servers for trust-ranked fallback - - - Returns: - CallToolResult from the MCP server - """ - start_time = datetime.datetime.now() - ordered_servers = await self._ordered_mcp_servers_for_tool_call( - server_name=server_name, - name=name, - allowed_mcp_servers=allowed_mcp_servers, - ) - - last_error: BaseException | None = None - for attempt_index, mcp_server in enumerate(ordered_servers): - effective_server_name = server_name or mcp_server.name - try: - return await self._call_tool_on_mcp_server( - mcp_server=mcp_server, - server_name=effective_server_name, - name=name, - arguments=arguments, - user_api_key_auth=user_api_key_auth, - mcp_auth_header=mcp_auth_header, - mcp_server_auth_headers=mcp_server_auth_headers, - proxy_logging_obj=proxy_logging_obj, - oauth2_headers=oauth2_headers, - raw_headers=raw_headers, - host_progress_callback=host_progress_callback, - start_time=start_time, - ) - except Exception as exc: - if not self._should_attempt_trust_fallback(exc): - raise - if attempt_index + 1 >= len(ordered_servers): - raise - last_error = exc - verbose_logger.debug( - "MCP trust fallback after failure on server %s for tool %s: %s", - mcp_server.name, - name, - exc, - ) - - if last_error is not None: - raise last_error - raise ValueError(f"Tool {name} not found") - def _resolve_mcp_server_for_tool_call( self, server_name: str, @@ -3911,8 +3657,8 @@ class MCPServerManager: async def _gather_openapi_tool_tasks( self, - tasks: list[Any], - proxy_logging_obj: ProxyLogging | None, + tasks: List[Any], + proxy_logging_obj: Optional[ProxyLogging], ) -> CallToolResult: """Await OpenAPI tool tasks and return the tool call result.""" try: @@ -3929,6 +3675,110 @@ class MCPServerManager: ) raise e + async def call_tool( + self, + server_name: str, + name: str, + arguments: Dict[str, Any], + user_api_key_auth: Optional[UserAPIKeyAuth] = None, + mcp_auth_header: Optional[str] = None, + mcp_server_auth_headers: Optional[Dict[str, Dict[str, str]]] = None, + proxy_logging_obj: Optional[ProxyLogging] = None, + oauth2_headers: Optional[Dict[str, str]] = None, + raw_headers: Optional[Dict[str, str]] = None, + host_progress_callback: Optional[Callable] = None, + ) -> CallToolResult: + """ + Call a tool with the given name and arguments + + Args: + server_name: Server name + name: Tool name + arguments: Tool arguments + user_api_key_auth: User authentication + mcp_auth_header: MCP auth header (deprecated) + mcp_server_auth_headers: Optional dict of server-specific auth headers {server_alias: auth_value} + proxy_logging_obj: Optional ProxyLogging object for hook integration + + + Returns: + CallToolResult from the MCP server + """ + start_time = datetime.datetime.now() + mcp_server = self._resolve_mcp_server_for_tool_call(server_name, name) + + ######################################################### + # Pre MCP Tool Call Hook + # Allow validation and modification of tool calls before execution + # Using standard pre_call_hook + ######################################################### + hook_result: Dict[str, Any] = {} + if proxy_logging_obj: + hook_result = await self.pre_call_tool_check( + name=name, + arguments=arguments, + server_name=server_name, + user_api_key_auth=user_api_key_auth, + proxy_logging_obj=proxy_logging_obj, + server=mcp_server, + raw_headers=raw_headers, + ) + if "arguments" in hook_result: + arguments = hook_result["arguments"] + + # Prepare tasks for during hooks + tasks = [] + if proxy_logging_obj: + during_hook_task = self._create_during_hook_task( + name=name, + arguments=arguments, + server_name_from_prefix=server_name, + user_api_key_auth=user_api_key_auth, + proxy_logging_obj=proxy_logging_obj, + start_time=start_time, + ) + tasks.append(during_hook_task) + + oauth2_headers = await self._resolve_oauth2_headers_for_tool_call( + mcp_server, oauth2_headers, user_api_key_auth + ) + + # For OpenAPI servers, call the tool handler directly instead of via MCP client + if mcp_server.spec_path: + verbose_logger.debug( + "Calling OpenAPI tool %s directly via HTTP handler", name + ) + if hook_result.get("extra_headers"): + verbose_logger.warning( + "pre_mcp_call hook returned extra_headers for OpenAPI-backed " + "MCP server '%s' — header injection is not supported for " + "OpenAPI servers; headers will be ignored. Use SSE/HTTP " + "transport to enable hook header injection.", + server_name, + ) + tasks.append( + asyncio.create_task( + self._call_openapi_tool_handler(mcp_server, name, arguments) + ) + ) + else: + return await self._call_regular_mcp_tool( + mcp_server=mcp_server, + original_tool_name=name, + arguments=arguments, + tasks=tasks, + mcp_auth_header=mcp_auth_header, + mcp_server_auth_headers=mcp_server_auth_headers, + oauth2_headers=oauth2_headers, + raw_headers=raw_headers, + proxy_logging_obj=proxy_logging_obj, + host_progress_callback=host_progress_callback, + hook_extra_headers=hook_result.get("extra_headers"), + user_api_key_auth=user_api_key_auth, + ) + + return await self._gather_openapi_tool_tasks(tasks, proxy_logging_obj) + ######################################################### # End of Methods that call the upstream MCP servers ######################################################### diff --git a/litellm/proxy/_experimental/mcp_server/mcp_trust_scoring.py b/litellm/proxy/_experimental/mcp_server/mcp_trust_scoring.py index 15b739650da..98f81016e2a 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_trust_scoring.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_trust_scoring.py @@ -11,10 +11,10 @@ from __future__ import annotations import asyncio import hashlib from dataclasses import dataclass -from typing import TYPE_CHECKING, Callable, Literal, Protocol, Sequence, cast +from typing import TYPE_CHECKING, Annotated, Callable, Literal, Protocol, Sequence, cast import httpx -from pydantic import BaseModel, ConfigDict, Field, TypeAdapter +from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, ValidationError from litellm._logging import verbose_logger from litellm.caching.in_memory_cache import InMemoryCache @@ -34,44 +34,50 @@ class MCPTrustScoringConfig(BaseModel): model_config = ConfigDict(frozen=True) enabled: bool = False - min_trust_score: float = Field( - default=DEFAULT_MCP_TRUST_SCORING_MIN_SCORE, - ge=0.0, - le=1.0, - description="Minimum normalized trust score (0-1) required to expose or route to a server", - ) - cache_ttl_seconds: int = Field( - default=DEFAULT_MCP_TRUST_SCORING_CACHE_TTL, - ge=1, - description="Seconds to cache Dominion Observatory responses per server URL", - ) - api_url: str = Field( - default=DEFAULT_MCP_TRUST_SCORING_API_URL, - description="Dominion Observatory trust endpoint base URL", - ) - fail_open: bool = Field( - default=True, - description="When True, keep servers reachable if DO is down, rate-limited, or has no score yet", - ) - api_key: str | None = Field( - default=None, - description="Optional API key for paid Dominion Observatory tiers", - ) + min_trust_score: Annotated[ + float, + Field( + ge=0.0, + le=1.0, + description="Minimum normalized trust score (0-1) required to expose or route to a server", + ), + ] = DEFAULT_MCP_TRUST_SCORING_MIN_SCORE + cache_ttl_seconds: Annotated[ + int, + Field( + ge=1, + description="Seconds to cache Dominion Observatory responses per server URL", + ), + ] = DEFAULT_MCP_TRUST_SCORING_CACHE_TTL + api_url: Annotated[ + str, + Field(description="Dominion Observatory trust endpoint base URL"), + ] = DEFAULT_MCP_TRUST_SCORING_API_URL + fail_open: Annotated[ + bool, + Field( + description="When True, keep servers reachable if DO is down, rate-limited, or has no score yet" + ), + ] = True + api_key: Annotated[ + str | None, + Field(description="Optional API key for paid Dominion Observatory tiers"), + ] = None class DominionObservatoryTrustResult(BaseModel): model_config = ConfigDict(frozen=True) - server_url: str - found: bool - trust_score_normalized: float | None = Field( - default=None, - description="Trust score normalized to 0-1", - ) - trust_score_raw: float | None = Field( - default=None, - description="Raw trust score from Dominion Observatory (0-100 scale)", - ) + server_url: str # any-ok: required pydantic field, mypy exports the field symbol as Any but the runtime type is str + found: bool # any-ok: required pydantic field, mypy exports the field symbol as Any but the runtime type is bool + trust_score_normalized: Annotated[ + float | None, + Field(description="Trust score normalized to 0-1"), + ] = None + trust_score_raw: Annotated[ + float | None, + Field(description="Raw trust score from Dominion Observatory (0-100 scale)"), + ] = None lookup_status: Literal["ok", "skipped", "error"] = "ok" message: str | None = None @@ -90,12 +96,6 @@ class DominionObservatoryTrustResult(BaseModel): return (0, -self.trust_score_normalized) -class _TrustCacheEntry(BaseModel): - model_config = ConfigDict(frozen=True) - - payload_json: str - - class _DominionObservatoryTrustApiResponse(BaseModel): model_config = ConfigDict(extra="ignore", frozen=True) @@ -139,8 +139,8 @@ class _HttpClient(Protocol): @dataclass(frozen=True, slots=True) class _ScoredServer: - server: MCPServer - trust: DominionObservatoryTrustResult + server: MCPServer # any-ok: required dataclass field, mypy exports the field symbol as Any but the runtime type is MCPServer + trust: DominionObservatoryTrustResult # any-ok: required dataclass field, mypy exports the field symbol as Any but the runtime type is typed def _normalize_server_url(server_url: str) -> str: @@ -170,7 +170,11 @@ def parse_trust_response( server_url: str, payload: object, ) -> DominionObservatoryTrustResult: - if not isinstance(payload, dict): + try: + parsed = TypeAdapter(_DominionObservatoryTrustApiResponse).validate_python( + payload + ) + except ValidationError: return DominionObservatoryTrustResult( server_url=server_url, found=False, @@ -178,8 +182,6 @@ def parse_trust_response( message="Invalid Dominion Observatory response shape", ) - parsed = TypeAdapter(_DominionObservatoryTrustApiResponse).validate_python(payload) - return DominionObservatoryTrustResult( server_url=server_url, found=parsed.found, @@ -259,7 +261,9 @@ class MCPTrustScoringClient: try: shared_cache = self._cache_getter() - shared = await shared_cache.async_get_cache(cache_key) + shared = await shared_cache.async_get_cache( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is narrowed below + cache_key + ) except Exception: shared = None @@ -270,16 +274,6 @@ class MCPTrustScoringClient: ttl=self._config.cache_ttl_seconds, ) return DominionObservatoryTrustResult.model_validate_json(shared) - if isinstance(shared, dict): - entry = _TrustCacheEntry.model_validate(shared) - self._local_cache.set_cache( - cache_key, - entry.payload_json, - ttl=self._config.cache_ttl_seconds, - ) - return DominionObservatoryTrustResult.model_validate_json( - entry.payload_json - ) return None async def _write_cached_result( @@ -296,9 +290,9 @@ class MCPTrustScoringClient: ) try: shared_cache = self._cache_getter() - await shared_cache.async_set_cache( + await shared_cache.async_set_cache( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the stored value is a typed JSON string cache_key, - _TrustCacheEntry(payload_json=payload_json).model_dump(), + payload_json, ttl=self._config.cache_ttl_seconds, ) except Exception as exc: @@ -310,17 +304,25 @@ class MCPTrustScoringClient: async def get_trust_score(self, server_url: str) -> DominionObservatoryTrustResult: normalized_url = _normalize_server_url(server_url) - cached = await self._read_cached_result(normalized_url) + cached = await self._read_cached_result( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + normalized_url + ) if cached is not None: return cached async with self._lock_for_url(normalized_url): - cached = await self._read_cached_result(normalized_url) + cached = await self._read_cached_result( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + normalized_url + ) if cached is not None: return cached - result = await self._fetch_trust_score(normalized_url) - await self._write_cached_result(normalized_url, result) + result = await self._fetch_trust_score( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + normalized_url + ) + await self._write_cached_result( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and returns None + normalized_url, result + ) return result async def _fetch_trust_score( @@ -332,7 +334,7 @@ class MCPTrustScoringClient: try: client = self._http_client_getter() - response = await client.get( + response = await client.get( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is an httpx.Response self._config.api_url, params={"url": server_url}, headers=headers or None, @@ -374,7 +376,9 @@ class MCPTrustScoringClient: lookup_status="skipped", message="Server has no HTTP URL to score", ) - return await self.get_trust_score(server.url) + return await self.get_trust_score( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + server.url + ) async def score_servers( self, @@ -383,8 +387,13 @@ class MCPTrustScoringClient: if not servers: return () - trust_results = await asyncio.gather( - *(self.get_trust_score_for_server(server) for server in servers) + trust_results = await asyncio.gather( # any-ok: awaited coroutine, gather's element Any is a typing artifact and each result is a typed DominionObservatoryTrustResult + *( # any-ok: generator of coroutines, the Coroutine Send/Recv Any is a typing artifact + self.get_trust_score_for_server( # any-ok: coroutine expression, Send/Recv Any is a typing artifact and the result is fully typed + server + ) + for server in servers + ) ) return tuple( _ScoredServer(server=server, trust=trust) @@ -398,7 +407,9 @@ class MCPTrustScoringClient: if not self.enabled: return tuple(servers) - scored = await self.score_servers(servers) + scored = await self.score_servers( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + servers + ) return tuple( entry.server for entry in scored @@ -412,7 +423,9 @@ class MCPTrustScoringClient: if not self.enabled: return tuple(servers) - scored = await self.score_servers(servers) + scored = await self.score_servers( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + servers + ) ordered = sorted(scored, key=lambda entry: entry.trust.sort_key()) return tuple(entry.server for entry in ordered) @@ -450,9 +463,9 @@ def _resolve_trust_scoring_config_values( resolved = dict(config) api_key = resolved.get("api_key") if isinstance(api_key, str) and api_key.startswith("os.environ/"): - from litellm.secret_managers.main import get_secret # noqa: PLC0415 + from litellm.secret_managers.main import get_secret_str # noqa: PLC0415 - resolved["api_key"] = get_secret(api_key) + resolved["api_key"] = get_secret_str(api_key) return resolved @@ -487,7 +500,9 @@ async def filter_mcp_servers_by_trust( active_client = client or get_mcp_trust_scoring_client() if active_client is None: return tuple(servers) - return await active_client.filter_servers_by_trust(servers) + return await active_client.filter_servers_by_trust( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + servers + ) async def rank_mcp_servers_by_trust( @@ -498,7 +513,9 @@ async def rank_mcp_servers_by_trust( active_client = client or get_mcp_trust_scoring_client() if active_client is None: return tuple(servers) - return await active_client.rank_servers_by_trust(servers) + return await active_client.rank_servers_by_trust( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + servers + ) async def apply_trust_filter_to_allowed_mcp_servers( @@ -506,4 +523,6 @@ async def apply_trust_filter_to_allowed_mcp_servers( *, client: MCPTrustScoringClient | None = None, ) -> tuple[MCPServer, ...]: - return await filter_mcp_servers_by_trust(servers, client=client) + return await filter_mcp_servers_by_trust( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + servers, client=client + ) diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index fe8bfbe137c..029459140f2 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -383,7 +383,9 @@ if MCP_AVAILABLE: ) allowed_mcp_servers = list( - await apply_trust_filter_to_allowed_mcp_servers(allowed_mcp_servers) + await apply_trust_filter_to_allowed_mcp_servers( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + allowed_mcp_servers + ) ) return allowed_mcp_servers, canonical_server_id @@ -477,7 +479,9 @@ if MCP_AVAILABLE: ) return list( - await apply_trust_filter_to_allowed_mcp_servers(allowed_mcp_servers) + await apply_trust_filter_to_allowed_mcp_servers( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + allowed_mcp_servers + ) ) async def _list_tools_for_single_server( @@ -666,12 +670,10 @@ if MCP_AVAILABLE: ) is not None ] - allowed_server_ids = [ - server.server_id - for server in await apply_trust_filter_to_allowed_mcp_servers( - allowed_mcp_server_objects - ) - ] + trusted_servers = await apply_trust_filter_to_allowed_mcp_servers( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + allowed_mcp_server_objects + ) + allowed_server_ids = [server.server_id for server in trusted_servers] list_tools_result = [] error_message = None diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index a01639cd0b1..67b0696ef51 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -1717,7 +1717,9 @@ if MCP_AVAILABLE: ) allowed_mcp_servers = list( - await apply_trust_filter_to_allowed_mcp_servers(allowed_mcp_servers) + await apply_trust_filter_to_allowed_mcp_servers( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + allowed_mcp_servers + ) ) # Pre-fetch OAuth credentials only when at least one server uses OAuth2, @@ -2747,7 +2749,6 @@ if MCP_AVAILABLE: raw_headers=raw_headers, litellm_logging_obj=litellm_logging_obj, host_progress_callback=host_progress_callback, - allowed_mcp_servers=allowed_mcp_servers, ) # Fall back to local tool registry with original name (legacy support) @@ -2816,7 +2817,9 @@ if MCP_AVAILABLE: ) allowed_mcp_servers = list( - await apply_trust_filter_to_allowed_mcp_servers(allowed_mcp_servers) + await apply_trust_filter_to_allowed_mcp_servers( # any-ok: awaited coroutine, Send/Recv Any is a typing artifact and the result is fully typed + allowed_mcp_servers + ) ) if not allowed_mcp_servers: raise HTTPException( @@ -3004,7 +3007,6 @@ if MCP_AVAILABLE: raw_headers: Optional[Dict[str, str]] = None, litellm_logging_obj: Optional[Any] = None, host_progress_callback: Optional[Callable] = None, - allowed_mcp_servers: list[MCPServer] | None = None, ) -> CallToolResult: """Handle tool execution for managed server tools""" # Import here to avoid circular import @@ -3021,7 +3023,6 @@ if MCP_AVAILABLE: raw_headers=raw_headers, proxy_logging_obj=proxy_logging_obj, host_progress_callback=host_progress_callback, - allowed_mcp_servers=allowed_mcp_servers, ) verbose_logger.debug("CALL TOOL RESULT: %s", call_tool_result) return call_tool_result diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 0f188be4e0a..d1f29f3dfa3 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -939,7 +939,7 @@ async def proxy_startup_event(app: FastAPI): ) verbose_proxy_logger.debug("After semantic tool filter initialization") ProxyStartupEvent._initialize_mcp_trust_scoring( - litellm_settings=_litellm_settings, + litellm_settings=_litellm_settings, # any-ok: proxy config state is untyped (Any), validated downstream by MCPTrustScoringConfig ) except Exception as e: verbose_proxy_logger.error( @@ -7608,7 +7608,9 @@ class ProxyStartupEvent: ) initialize_mcp_trust_scoring_from_config( - litellm_settings.get("mcp_trust_scoring") + litellm_settings.get( # any-ok: proxy config state is untyped (Any), validated downstream by MCPTrustScoringConfig + "mcp_trust_scoring" + ) ) @classmethod