Merge pull request #37384 from daniel-meismer-zocdoc/litellm_mcp_dcr_bridge_complete_challenges

fix(mcp): complete DCR bridge OAuth challenges
This commit is contained in:
Mateo Wang 2026-08-26 12:07:54 -07:00 committed by GitHub
commit ace28fd97a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 346 additions and 50 deletions

View file

@ -1,6 +1,8 @@
import re
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import datetime, timezone
from types import MappingProxyType
from typing import TYPE_CHECKING, Final, cast
from fastapi import HTTPException
@ -13,6 +15,7 @@ import litellm
from litellm._logging import verbose_logger
from litellm.proxy._experimental.mcp_server.oauth_utils import (
get_passthrough_resource_metadata_url,
get_passthrough_www_authenticate,
get_request_base_url,
well_known_root_suffix,
)
@ -298,6 +301,16 @@ def _admission_failure_fallback(
raise exc
@dataclass(frozen=True, slots=True)
class DcrBridgeTarget:
"""The single DCR-bridge server a request targets, paired with the exact name the caller
used to reach it (alias or server_name, whichever they typed), which is the spelling an
``invalid_token`` challenge must echo back."""
requested_name: str
server: MCPServer
class MCPRequestHandler:
"""
Class to handle MCP request processing, including:
@ -437,27 +450,33 @@ class MCPRequestHandler:
path=request_route,
mcp_servers=mcp_servers,
client_ip=IPAddressUtils.get_mcp_client_ip(request),
) or (
MCPRequestHandler._single_dcr_bridge_delegate_target(
path=request_route,
mcp_servers=mcp_servers,
client_ip=IPAddressUtils.get_mcp_client_ip(request),
)
is not None
and not oauth2_headers
and not mcp_server_auth_headers
and not mcp_auth_header
):
validated_user_api_key_auth = UserAPIKeyAuth()
elif (
(
bridge_delegate_target := MCPRequestHandler._single_dcr_bridge_delegate_target(
path=request_route,
mcp_servers=mcp_servers,
client_ip=IPAddressUtils.get_mcp_client_ip(request),
)
bridge_delegate_target := MCPRequestHandler._single_dcr_bridge_delegate_target(
path=request_route,
mcp_servers=mcp_servers,
client_ip=IPAddressUtils.get_mcp_client_ip(request),
)
is not None
and oauth2_headers
and is_bridge_envelope_shaped(oauth2_headers["Authorization"])
):
# A single DCR-bridge oauth_delegate target carrying an envelope-shaped
# Authorization: open the envelope, admit under its recovered identity, and
# inject the inner upstream token for egress. A non-envelope bearer on the same
# server is NOT admitted here — it falls through to the oauth2 arm, which 401s.
validated_user_api_key_auth, mcp_server_auth_headers = await MCPRequestHandler._admit_dcr_bridge_delegate(
server=bridge_delegate_target,
) is not None and oauth2_headers:
(
validated_user_api_key_auth,
mcp_server_auth_headers,
) = await MCPRequestHandler._admit_dcr_bridge_authorization(
server=bridge_delegate_target.server,
requested_name=bridge_delegate_target.requested_name,
authorization_value=oauth2_headers["Authorization"],
litellm_api_key=litellm_api_key,
mcp_server_auth_headers=mcp_server_auth_headers,
request=request,
route=request_route,
@ -723,10 +742,10 @@ class MCPRequestHandler:
@staticmethod
def _single_dcr_bridge_delegate_target(
path: str, mcp_servers: list[str] | None, client_ip: str | None
) -> MCPServer | None:
) -> DcrBridgeTarget | None:
"""The one DCR-bridge ``oauth_delegate`` server this request targets, or ``None``.
Returns the server only when EXACTLY ONE target resolves and it is both
Returns the target only when EXACTLY ONE name resolves and its server is both
``is_oauth_delegate`` and ``is_dcr_bridge``. Fails closed (``None``) on a
multi-target request, an unresolved target, or a non-matching server, so the
envelope admission arm never fires for an aggregate scope or a server that did not
@ -740,17 +759,21 @@ class MCPRequestHandler:
if len(target_names) != 1:
return None
server: Final = global_mcp_server_manager.get_mcp_server_by_name(target_names[0], client_ip=client_ip)
if server is None or not server.is_oauth_delegate or not server.is_dcr_bridge:
# Both flags are security-sensitive opt-ins. Require literal booleans so
# partially populated objects and truthy proxy values cannot enable bridge
# admission accidentally.
if server is None or server.is_oauth_delegate is not True or server.is_dcr_bridge is not True:
return None
# Egress resolves the injected per-server token only by alias / server_name; a server with
# neither cannot receive the forwarded token, so fail closed rather than admit-and-drop.
if not (server.server_name or server.alias):
return None
return server
return DcrBridgeTarget(requested_name=target_names[0], server=server)
@staticmethod
async def _admit_dcr_bridge_delegate(
server: MCPServer,
requested_name: str,
authorization_value: str,
mcp_server_auth_headers: dict[str, dict[str, str]] | None,
request: Request,
@ -798,10 +821,62 @@ class MCPRequestHandler:
new_headers: Final = {**(mcp_server_auth_headers or {}), **injected}
return admitted, new_headers
case BridgeEnvelopeInvalid() | NotBridgeEnvelope():
raise HTTPException(status_code=401, detail="Invalid or expired credential")
raise MCPRequestHandler._dcr_bridge_invalid_token_challenge(
requested_name=requested_name, request=request
)
case _:
assert_never(result)
@staticmethod
async def _admit_dcr_bridge_authorization(
server: MCPServer,
requested_name: str,
authorization_value: str,
litellm_api_key: str,
mcp_server_auth_headers: dict[str, dict[str, str]] | None, # mutable-ok: existing MCP sink shape
request: Request,
route: str,
) -> tuple[UserAPIKeyAuth, dict[str, dict[str, str]] | None]: # mutable-ok: existing MCP sink shape
if is_bridge_envelope_shaped(authorization_value):
return await MCPRequestHandler._admit_dcr_bridge_delegate(
server=server,
requested_name=requested_name,
authorization_value=authorization_value,
mcp_server_auth_headers=mcp_server_auth_headers,
request=request,
route=route,
)
try:
admitted: Final = await user_api_key_auth(api_key=litellm_api_key, request=request)
except (HTTPException, ProxyException) as exc:
if not _is_litellm_auth_admission_error(exc):
raise
raise MCPRequestHandler._dcr_bridge_invalid_token_challenge(
requested_name=requested_name, request=request
) from exc
return admitted, mcp_server_auth_headers
@staticmethod
def _dcr_bridge_invalid_token_challenge(requested_name: str, request: Request) -> HTTPException:
"""The RFC 6750 ``invalid_token`` challenge for a failed bridge admission.
Named by the exact spelling the caller requested, matching the per-server well-known
document and the other challenge emitters, so ``resource_metadata`` always points at the
resource the client actually asked for even when alias and server_name differ."""
return HTTPException(
status_code=401,
detail="Invalid or expired credential",
headers=MappingProxyType(
{
"www-authenticate": get_passthrough_www_authenticate(
scope=request.scope,
server_name=requested_name,
invalid_token=True,
)
}
),
)
@staticmethod
async def _admit_gateway_session(
authorization_value: str,

View file

@ -7,8 +7,6 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from starlette.datastructures import Headers
from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import (
@ -5097,13 +5095,10 @@ class TestMCPDcrBridgeDelegateAdmission:
"""Admission-side arm for a DCR-bridge ``oauth_delegate`` client that authenticates with
a single envelope bearer (LIT-4338).
The arm fires only for a single ``is_dcr_bridge`` ``is_oauth_delegate`` target carrying an
envelope-shaped Authorization. It opens the litellm-signed envelope, reloads the live key
record the sealed ``key_hash`` references so the caller is admitted under the key's current
authorization context (team/org/object-permission) and revocation state, and injects the inner
upstream token under the server's per-server auth-header key so egress forwards it. A key that
is missing, blocked, or expired fails closed with a 401. Everything else must stay on its
existing admission path.
A credential-free request reaches the named MCP handler so it can issue the initial OAuth
challenge. Every bearer on that same route enters envelope resolution. A valid envelope opens
under its live authorization context, while invalid envelopes and non-envelope bearers receive
a named ``invalid_token`` challenge. Everything else stays on its existing admission path.
"""
_MASTER_KEY = "sk-bridge-master-key-for-envelope-derivation"
@ -5138,6 +5133,8 @@ class TestMCPDcrBridgeDelegateAdmission:
minted_at=None,
master_key=None,
):
from pydantic import SecretStr
from litellm.proxy._experimental.mcp_server.outbound_credentials.bridge_credentials import (
envelope_keys_from_master_key,
)
@ -5148,7 +5145,6 @@ class TestMCPDcrBridgeDelegateAdmission:
mint_envelope,
user_identity,
)
from pydantic import SecretStr
identity = (
user_identity(server_id=server_id, user_id=user_id)
@ -5272,6 +5268,92 @@ class TestMCPDcrBridgeDelegateAdmission:
request.body = mock_body
return request
async def test_bridge_target_requires_literal_boolean_opt_ins(self):
"""Truthy proxy values must not opt an unresolved server into bridge admission."""
for delegate_value, bridge_value in ((MagicMock(), True), (True, MagicMock())):
server = MagicMock()
server.is_oauth_delegate = delegate_value
server.is_dcr_bridge = bridge_value
server.server_name = "bridge_delegate_server"
server.alias = None
with patch( # test-quality-ok: isolate the MCP registry when testing target selection
"litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager"
) as mock_mgr:
mock_mgr.get_mcp_server_by_name.return_value = server
assert (
MCPRequestHandler._single_dcr_bridge_delegate_target(
path="/mcp/bridge_delegate_server",
mcp_servers=None,
client_ip=None,
)
is None
)
async def test_credential_free_named_bridge_request_reaches_mcp_handler(self):
scope = {
"type": "http",
"method": "POST",
"path": "/mcp/bridge_delegate_server",
"headers": [],
}
with (
patch( # test-quality-ok: observe the auth boundary while testing admission orchestration
"litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth",
new_callable=AsyncMock,
) as mock_auth,
patch( # test-quality-ok: isolate the MCP registry used by request admission
"litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager"
) as mock_mgr,
):
mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server()
(
auth_result,
_mcp_auth_header,
_mcp_servers,
mcp_server_auth_headers,
_oauth2_headers,
_raw_headers,
) = await MCPRequestHandler.process_mcp_request(scope)
mock_auth.assert_not_called()
assert auth_result == UserAPIKeyAuth()
assert mcp_server_auth_headers == {}
@pytest.mark.parametrize(
"headers",
(
[(b"x-mcp-auth", b"Bearer upstream-token")],
[(b"x-mcp-bridge_delegate_server-authorization", b"Bearer upstream-token")],
),
ids=("deprecated-mcp-auth", "per-server-auth"),
)
async def test_client_mcp_credentials_do_not_receive_keyless_bridge_admission(self, headers):
scope = {
"type": "http",
"method": "POST",
"path": "/mcp/bridge_delegate_server",
"headers": headers,
}
with (
patch( # test-quality-ok: force credential rejection through request admission
"litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth",
new_callable=AsyncMock,
side_effect=HTTPException(status_code=401, detail="Invalid key"),
) as mock_auth,
patch( # test-quality-ok: isolate the MCP registry used by request admission
"litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager"
) as mock_mgr,
):
mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server()
with pytest.raises(HTTPException) as exc_info:
await MCPRequestHandler.process_mcp_request(scope)
assert exc_info.value.status_code == 401
mock_auth.assert_awaited_once()
async def test_valid_envelope_reloads_live_key_and_admits_its_authorization_context(self):
"""A valid envelope admits under the LIVE key record the sealed key_hash references, not a
blank identity: the reload is keyed by that exact hash, and the admitted auth carries the
@ -5809,6 +5891,7 @@ class TestMCPDcrBridgeDelegateAdmission:
):
_auth, new_headers = await MCPRequestHandler._admit_dcr_bridge_delegate(
server=self._bridge_delegate_server(server_name="bridge_name", alias="bridge_alias"),
requested_name="bridge_name",
authorization_value=f"Bearer {envelope}",
mcp_server_auth_headers=attacker_forwarded,
request=self._mcp_request(),
@ -5845,6 +5928,7 @@ class TestMCPDcrBridgeDelegateAdmission:
):
_auth, new_headers = await MCPRequestHandler._admit_dcr_bridge_delegate(
server=server,
requested_name="bridge_delegate_server",
authorization_value=f"Bearer {envelope}",
mcp_server_auth_headers=None,
request=self._mcp_request(),
@ -5887,8 +5971,7 @@ class TestMCPDcrBridgeDelegateAdmission:
mock_auth.assert_called_once()
async def test_expired_envelope_fails_closed_401(self):
"""An envelope whose exp is in the past must fail closed with a 401, never fall through to
anonymous admission."""
"""An expired envelope fails closed and tells the client where to reauthorize."""
expired = self._mint_bridge_envelope(
expires_in=60,
minted_at=datetime.now(timezone.utc) - timedelta(hours=2),
@ -5897,7 +5980,10 @@ class TestMCPDcrBridgeDelegateAdmission:
"type": "http",
"method": "POST",
"path": "/mcp/bridge_delegate_server",
"headers": [(b"authorization", f"Bearer {expired}".encode("latin-1"))],
"headers": [
(b"host", b"testserver"),
(b"authorization", f"Bearer {expired}".encode("latin-1")),
],
}
with (
@ -5914,6 +6000,12 @@ class TestMCPDcrBridgeDelegateAdmission:
assert exc_info.value.status_code == 401
mock_auth.assert_not_called()
assert exc_info.value.headers == {
"www-authenticate": (
'Bearer error="invalid_token", '
'resource_metadata="http://testserver/.well-known/oauth-protected-resource/mcp/bridge_delegate_server"'
)
}
async def test_envelope_minted_for_a_different_server_fails_closed_401(self):
"""An envelope sealed for another server_id must be rejected when presented to this server,
@ -5924,7 +6016,10 @@ class TestMCPDcrBridgeDelegateAdmission:
"type": "http",
"method": "POST",
"path": "/mcp/bridge_delegate_server",
"headers": [(b"authorization", f"Bearer {wrong_server}".encode("latin-1"))],
"headers": [
(b"host", b"testserver"),
(b"authorization", f"Bearer {wrong_server}".encode("latin-1")),
],
}
with (
@ -5941,6 +6036,12 @@ class TestMCPDcrBridgeDelegateAdmission:
assert exc_info.value.status_code == 401
mock_auth.assert_not_called()
assert exc_info.value.headers == {
"www-authenticate": (
'Bearer error="invalid_token", '
'resource_metadata="http://testserver/.well-known/oauth-protected-resource/mcp/bridge_delegate_server"'
)
}
async def test_envelope_under_wrong_master_key_fails_closed_401(self):
"""An envelope-shaped bearer whose signature does not verify under the proxy's derived keys
@ -5950,7 +6051,10 @@ class TestMCPDcrBridgeDelegateAdmission:
"type": "http",
"method": "POST",
"path": "/mcp/bridge_delegate_server",
"headers": [(b"authorization", f"Bearer {foreign}".encode("latin-1"))],
"headers": [
(b"host", b"testserver"),
(b"authorization", f"Bearer {foreign}".encode("latin-1")),
],
}
with (
@ -5967,26 +6071,71 @@ class TestMCPDcrBridgeDelegateAdmission:
assert exc_info.value.status_code == 401
mock_auth.assert_not_called()
assert exc_info.value.headers == {
"www-authenticate": (
'Bearer error="invalid_token", '
'resource_metadata="http://testserver/.well-known/oauth-protected-resource/mcp/bridge_delegate_server"'
)
}
async def test_non_envelope_bearer_on_bridge_server_falls_through_to_oauth2_arm(self):
"""A plain (non-envelope) bearer on the same bridge server must NOT be admitted by the
envelope arm: it falls through to the oauth2 arm, which validates it as a LiteLLM key and
401s here. Proves the arm is gated on envelope shape, not merely on the target being a
bridge server."""
@pytest.mark.parametrize("requested_name", ["bridge_name", "bridge_alias"])
async def test_invalid_envelope_challenge_names_the_requested_spelling(self, requested_name):
"""A server reachable under both its server_name and a distinct alias must challenge with
metadata for the exact spelling the caller used, matching the per-server well-known
document, so the client rediscovers against the resource it actually asked for."""
foreign = self._mint_bridge_envelope(master_key="a-different-master-key-entirely")
scope = {
"type": "http",
"method": "POST",
"path": f"/mcp/{requested_name}",
"headers": [
(b"host", b"testserver"),
(b"authorization", f"Bearer {foreign}".encode("latin-1")),
],
}
with (
patch( # test-quality-ok: prove standard admission is never consulted for an envelope bearer
"litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth",
new_callable=AsyncMock,
) as mock_auth,
patch( # test-quality-ok: isolate the MCP registry, same seam as the sibling challenge tests
"litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager"
) as mock_mgr,
patch("litellm.proxy.proxy_server.master_key", self._MASTER_KEY), # test-quality-ok: envelope keys derive from the proxy master_key module global
):
mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server(
server_name="bridge_name", alias="bridge_alias"
)
with pytest.raises(HTTPException) as exc_info:
await MCPRequestHandler.process_mcp_request(scope)
assert exc_info.value.status_code == 401
mock_auth.assert_not_called()
assert exc_info.value.headers == {
"www-authenticate": (
'Bearer error="invalid_token", '
f'resource_metadata="http://testserver/.well-known/oauth-protected-resource/mcp/{requested_name}"'
)
}
async def test_non_envelope_bearer_on_bridge_server_returns_named_challenge(self):
"""A raw provider bearer cannot authorize a bridge route and triggers reauthorization."""
scope = {
"type": "http",
"method": "POST",
"path": "/mcp/bridge_delegate_server",
"headers": [(b"authorization", b"Bearer plain-upstream-bearer-not-an-envelope")],
"headers": [
(b"host", b"testserver"),
(b"authorization", b"Bearer plain-upstream-bearer-not-an-envelope"),
],
}
async def mock_user_api_key_auth_fails(api_key, request):
raise HTTPException(status_code=401, detail="Invalid API key")
with (
patch(
"litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth",
side_effect=mock_user_api_key_auth_fails,
new_callable=AsyncMock,
side_effect=HTTPException(status_code=401, detail="Invalid key"),
) as mock_auth,
patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager") as mock_mgr,
patch("litellm.proxy.proxy_server.master_key", self._MASTER_KEY),
@ -5996,8 +6145,77 @@ class TestMCPDcrBridgeDelegateAdmission:
await MCPRequestHandler.process_mcp_request(scope)
assert exc_info.value.status_code == 401
# The envelope arm was skipped, so the oauth2 arm ran and validated the bearer.
mock_auth.assert_called_once()
mock_auth.assert_awaited_once()
assert exc_info.value.headers == {
"www-authenticate": (
'Bearer error="invalid_token", '
'resource_metadata="http://testserver/.well-known/oauth-protected-resource/mcp/bridge_delegate_server"'
)
}
async def test_valid_litellm_authorization_key_uses_standard_admission(self):
scope = {
"type": "http",
"method": "POST",
"path": "/mcp/bridge_delegate_server",
"headers": [(b"authorization", b"Bearer sk-valid-litellm-key")],
}
admitted = UserAPIKeyAuth(api_key="hashed-key", user_id="litellm-key-user")
with (
patch( # test-quality-ok: supply standard key admission through the auth boundary
"litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth",
new_callable=AsyncMock,
return_value=admitted,
) as mock_auth,
patch( # test-quality-ok: isolate the MCP registry used by request admission
"litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager"
) as mock_mgr,
patch( # test-quality-ok: configure key classification for the orchestration test
"litellm.proxy.proxy_server.master_key", self._MASTER_KEY
),
):
mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server()
(
auth_result,
_mcp_auth,
_servers,
mcp_server_auth_headers,
_oauth,
_raw,
) = await MCPRequestHandler.process_mcp_request(scope)
assert auth_result is admitted
assert mcp_server_auth_headers == {}
assert mock_auth.await_args.kwargs["api_key"] == "Bearer sk-valid-litellm-key"
async def test_non_401_litellm_key_failure_is_not_converted_to_oauth_challenge(self):
scope = {
"type": "http",
"method": "POST",
"path": "/mcp/bridge_delegate_server",
"headers": [(b"authorization", b"Bearer sk-blocked-litellm-key")],
}
with (
patch( # test-quality-ok: force a non-401 auth result through request admission
"litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth",
new_callable=AsyncMock,
side_effect=HTTPException(status_code=403, detail="Key blocked"),
),
patch( # test-quality-ok: isolate the MCP registry used by request admission
"litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager"
) as mock_mgr,
patch( # test-quality-ok: configure key classification for the orchestration test
"litellm.proxy.proxy_server.master_key", self._MASTER_KEY
),
):
mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server()
with pytest.raises(HTTPException) as exc_info:
await MCPRequestHandler.process_mcp_request(scope)
assert exc_info.value.status_code == 403
assert not exc_info.value.headers
async def test_explicit_litellm_key_wins_over_envelope_arm(self):
"""An explicit x-litellm-api-key is always a LiteLLM credential and its arm precedes the
@ -6126,6 +6344,7 @@ class TestMCPDcrBridgeDelegateAdmission:
):
auth_result, new_headers = await MCPRequestHandler._admit_dcr_bridge_delegate(
server=self._bridge_delegate_server(),
requested_name="bridge_delegate_server",
authorization_value=f"Bearer {envelope}",
mcp_server_auth_headers=existing,
request=self._mcp_request(),
@ -6150,6 +6369,7 @@ class TestMCPDcrBridgeDelegateAdmission:
with pytest.raises(HTTPException) as exc_info:
await MCPRequestHandler._admit_dcr_bridge_delegate(
server=self._bridge_delegate_server(),
requested_name="bridge_delegate_server",
authorization_value=f"Bearer {envelope}",
mcp_server_auth_headers=None,
request=self._mcp_request(),
@ -6168,6 +6388,7 @@ class TestMCPDcrBridgeDelegateAdmission:
with pytest.raises(HTTPException) as exc_info:
await MCPRequestHandler._admit_dcr_bridge_delegate(
server=self._bridge_delegate_server(),
requested_name="bridge_delegate_server",
authorization_value=f"Bearer {envelope}",
mcp_server_auth_headers=None,
request=self._mcp_request(),
@ -6480,8 +6701,8 @@ class TestGatewaySessionAdmission:
)
from litellm.proxy._experimental.mcp_server.outbound_credentials.session_token import (
SessionPrincipal,
mint_session_token,
mint_session_refresh_token,
mint_session_token,
)
keys = session_keys_from_master_key(self._MASTER_KEY)
@ -6947,8 +7168,8 @@ class TestUserSubjectTeamUnion:
def _manager_with(self, server_ids, allow_all=()):
from litellm.proxy._experimental.mcp_server.mcp_server_manager import MCPServerManager
from litellm.types.mcp_server.mcp_server_manager import MCPServer
from litellm.types.mcp import MCPTransport
from litellm.types.mcp_server.mcp_server_manager import MCPServer
manager = MCPServerManager()
for sid in server_ids: