mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
refactor(mcp/v2): float misconfigured M2M up as a typed error instead of deferring to v1
Make ClientCredentialsConfig's client_id/client_secret/token_url optional (matching the other OAuth configs), and move the completeness check out of to_server_spec's m2m guard into the _client_credentials fetcher: an incomplete M2M server now builds a spec and resolve() raises CredError.misconfigured at the arm, rather than to_server_spec returning None and silently deferring the misconfig to v1. The fetcher guard also narrows the now-optional fields (no new type errors). Optional makes the fields discovery/DCR-ready (token_url via RFC 8414, client_id/secret via DCR) later. Validated: 32 bridge tests, including a new test that an incomplete client_credentials server resolves to misconfigured.
This commit is contained in:
parent
d291593f1f
commit
74e516e2b9
4 changed files with 50 additions and 10 deletions
|
|
@ -113,6 +113,16 @@ class HttpxClientCredentialsFetcher:
|
|||
async def fetch(
|
||||
self, config: ClientCredentialsConfig
|
||||
) -> Result[StoredToken, CredError]:
|
||||
if (
|
||||
config.client_id is None
|
||||
or config.client_secret is None
|
||||
or config.token_url is None
|
||||
):
|
||||
return Error(
|
||||
CredError.of_misconfigured(
|
||||
"client_credentials requires client_id, client_secret, and token_url"
|
||||
)
|
||||
)
|
||||
data = {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": config.client_id,
|
||||
|
|
|
|||
|
|
@ -200,18 +200,18 @@ def to_server_spec(server: MCPServer) -> Optional[ServerSpec]:
|
|||
scopes=tuple(server.scopes or ()),
|
||||
),
|
||||
)
|
||||
if (
|
||||
server.has_client_credentials
|
||||
and server.client_id
|
||||
and server.client_secret
|
||||
and server.token_url
|
||||
):
|
||||
if server.has_client_credentials:
|
||||
# Mode selector only (oauth2_flow == client_credentials); completeness is no longer a guard.
|
||||
# An incomplete M2M config still builds, and the _client_credentials arm raises misconfigured
|
||||
# at resolve time instead of returning None and deferring to v1.
|
||||
return ServerSpec(
|
||||
server_id=server.server_id,
|
||||
resource=resource,
|
||||
config=ClientCredentialsConfig(
|
||||
client_id=server.client_id,
|
||||
client_secret=SecretStr(server.client_secret),
|
||||
client_secret=(
|
||||
SecretStr(server.client_secret) if server.client_secret else None
|
||||
),
|
||||
token_url=server.token_url,
|
||||
scopes=tuple(server.scopes or ()),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -158,9 +158,12 @@ class ClientCredentialsConfig(BaseModel):
|
|||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
kind: Literal[AuthSpecKind.client_credentials] = AuthSpecKind.client_credentials
|
||||
client_id: str
|
||||
client_secret: SecretStr
|
||||
token_url: str
|
||||
# Optional so the config can be built incomplete: the values may be supplied at runtime (token_url
|
||||
# via RFC 8414 discovery, client_id/secret via DCR) and the _client_credentials arm raises
|
||||
# CredError.misconfigured when a needed field is still absent at resolve time.
|
||||
client_id: str | None = None
|
||||
client_secret: SecretStr | None = None
|
||||
token_url: str | None = None
|
||||
scopes: tuple[str, ...] = ()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -150,6 +150,33 @@ async def test_client_credentials_maps_to_config():
|
|||
assert spec.config.scopes == ("a", "b")
|
||||
|
||||
|
||||
async def test_m2m_incomplete_creds_resolves_misconfigured():
|
||||
# A client_credentials server missing client_id/secret now BUILDS a spec (the completeness
|
||||
# pre-check was dropped) and resolve() raises misconfigured at the arm, instead of to_server_spec
|
||||
# returning None and deferring to v1.
|
||||
from litellm.proxy._experimental.mcp_server.v2_resolver_bridge import (
|
||||
provider,
|
||||
to_server_spec,
|
||||
to_subject,
|
||||
)
|
||||
from litellm.proxy.gateway.mcp.result import Error
|
||||
|
||||
server = MCPServer(
|
||||
server_id="m2m-incomplete",
|
||||
name="m2m-incomplete",
|
||||
transport=MCPTransport.http,
|
||||
url="https://up.example/mcp",
|
||||
auth_type=MCPAuth.oauth2,
|
||||
oauth2_flow="client_credentials",
|
||||
token_url="https://idp/token",
|
||||
) # has_client_credentials, but no client_id / client_secret
|
||||
spec = to_server_spec(server)
|
||||
assert spec is not None # builds incomplete now (was None -> defer to v1 before)
|
||||
result = await provider().resolve(to_subject(None, None), spec)
|
||||
assert isinstance(result, Error)
|
||||
assert result.error.tag == "misconfigured"
|
||||
|
||||
|
||||
async def test_client_credentials_graft_end_to_end(v2_on, monkeypatch):
|
||||
# M2M flows through the real fetcher; mock the IdP token endpoint and assert the Bearer.
|
||||
from litellm.proxy._experimental.mcp_server import v2_port_bodies
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue