Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/_experimental/mcp_server/faults/render_oauth.py: 16%

50 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1"""Render upstream OAuth/DCR faults onto the wire. The only place that chooses statuses and bodies 

2for these faults, so every consumer emits the same contract: RFC 6749 §5.2-shaped JSON with the §5.1 

3no-store headers on token endpoints, HTTPException details on registration. Status, code, and prose 

4all derive from the fault tag; exhaustive matches keep a new fault arm from shipping unrendered. 

5""" 

6 

7from __future__ import annotations 

8 

9from typing import Final 

10 

11from fastapi.responses import JSONResponse 

12from typing_extensions import assert_never 

13 

14from litellm.proxy._experimental.mcp_server.faults.types import CallerRejected, UpstreamOAuthFault 

15from litellm.proxy._experimental.mcp_server.oauth_utils import TOKEN_NO_CACHE_HEADERS 

16 

17 

18def _gateway_rejected_description(code: str) -> str: 

19 if code == "invalid_target": 

20 return ( 

21 "the upstream authorization server rejected the request (invalid_target); it did not " 

22 "accept this server's RFC 8707 resource indicator. Set upstream_resource on the MCP " 

23 "server to the resource identifier the authorization server expects, or unset it if " 

24 "that authorization server does not support resource indicators" 

25 ) 

26 return ( 

27 f"the upstream authorization server rejected the gateway's configured client credentials " 

28 f"({code}); verify the MCP server's client_id and client_secret" 

29 ) 

30 

31 

32def _upstream_reported_status_and_description(code: str) -> tuple[int, str]: 

33 if code == "temporarily_unavailable": 

34 return 503, "the upstream authorization server is temporarily unavailable; retry shortly" 

35 return 502, "the upstream authorization server reported an internal error" 

36 

37 

38def _registration_refused_description(status_code: int) -> str: 

39 return ( 

40 f"the upstream authorization server refused dynamic client registration (HTTP {status_code}). " 

41 "This provider may require a pre-registered OAuth client. Configure client_id and, if required " 

42 "by the provider, client_secret for this MCP server to skip dynamic registration" 

43 ) 

44 

45 

46def _render_caller_rejected(fault: CallerRejected) -> JSONResponse: 

47 content: Final = { 

48 "error": fault.code, 

49 **({"error_description": fault.description} if fault.description else {}), 

50 **({"error_uri": fault.error_uri} if fault.error_uri else {}), 

51 } 

52 status_code: Final = 401 if fault.code == "invalid_client" else 400 

53 return JSONResponse(status_code=status_code, content=content, headers=TOKEN_NO_CACHE_HEADERS) 

54 

55 

56def render_token_fault(fault: UpstreamOAuthFault) -> JSONResponse: 

57 """RFC 6749 §5.2 response for a token-endpoint fault. Caller-actionable rejections relay the 

58 upstream's code on the status that code implies (401 for invalid_client per §5.2, else 400); 

59 gateway-side faults are 502 ``server_error`` with gateway-authored prose so a caller is never 

60 blamed for, or shown the internals of, a failure only the operator can fix.""" 

61 match fault.tag: 

62 case "caller_rejected": 

63 return _render_caller_rejected(fault) 

64 case "gateway_rejected": 

65 return JSONResponse( 

66 status_code=502, 

67 content={ 

68 "error": "server_error", 

69 "error_description": _gateway_rejected_description(fault.code), 

70 }, 

71 headers=TOKEN_NO_CACHE_HEADERS, 

72 ) 

73 case "upstream_reported_fault": 

74 status_code, description = _upstream_reported_status_and_description(fault.code) 

75 return JSONResponse( 

76 status_code=status_code, 

77 content={"error": fault.code, "error_description": description}, 

78 headers=TOKEN_NO_CACHE_HEADERS, 

79 ) 

80 case "upstream_registration_refused": 

81 return _render_caller_rejected( 

82 CallerRejected( 

83 code="unauthorized_client", 

84 description=_registration_refused_description(fault.status_code), 

85 ) 

86 ) 

87 case "upstream_protocol_fault": 

88 return JSONResponse( 

89 status_code=502, 

90 content={"error": "server_error", "error_description": fault.note}, 

91 headers=TOKEN_NO_CACHE_HEADERS, 

92 ) 

93 case _: 

94 assert_never(fault.tag) 

95 

96 

97def dcr_fault_detail(fault: UpstreamOAuthFault) -> tuple[int, str]: 

98 """Status and detail string for a registration fault, raised as HTTPException by the caller. 

99 RFC 7591 §3.2.2 defines registration errors as 400, so a contract-conformant rejection is 400 

100 regardless of the upstream status; a bare 401/403 is a registration refusal rendered as 403.""" 

101 match fault.tag: 

102 case "caller_rejected": 

103 detail: Final = f"{fault.code}: {fault.description}" if fault.description else fault.code 

104 return 400, detail 

105 case "gateway_rejected": 

106 return 502, _gateway_rejected_description(fault.code) 

107 case "upstream_reported_fault": 

108 return _upstream_reported_status_and_description(fault.code) 

109 case "upstream_registration_refused": 

110 return 403, _registration_refused_description(fault.status_code) 

111 case "upstream_protocol_fault": 

112 return 502, fault.note 

113 case _: 

114 assert_never(fault.tag)