fix(mcp): address bugbot user_fields + BYOK conflict and header dedup

- Reject combining is_byok=True with non-empty user_fields at server
  create/update time: both share the (user_id, server_id) credential row
  and the store paths refuse cross-type overwrites, so the combination
  trapped users in an unresolvable 401 loop.
- Make the managed MCP dispatch path apply user-field headers with the
  same case-insensitive precedence as the OpenAPI/local path, so a
  static_header named 'Authorization' is no longer left alongside a
  user-field header named 'authorization' on the outbound request.
This commit is contained in:
mateo-berri 2026-05-19 09:20:57 +00:00 • committed by Claude
parent bd9a6e4872
commit a6ae106cf1
No known key found for this signature in database
2 changed files with 37 additions and 1 deletions

View file

@ -2827,7 +2827,16 @@ class MCPServerManager:
if user_field_headers:
if extra_headers is None:
extra_headers = {}
extra_headers.update(user_field_headers)
# Match the OpenAPI/local path's case-insensitive precedence: a
# user-field header replaces any existing header (e.g. from
# static_headers) whose name matches case-insensitively, instead
# of leaving two differently-cased duplicates on the wire.
existing_lower_names = {k.lower(): k for k in extra_headers}
for header_name, value in user_field_headers.items():
collision = existing_lower_names.get(header_name.lower())
if collision is not None and collision != header_name:
del extra_headers[collision]
extra_headers[header_name] = value
if hook_extra_headers:
if extra_headers is None:

View file

@ -216,6 +216,33 @@ if MCP_AVAILABLE:
def validate_and_normalize_mcp_server_payload(payload: Any) -> None:
_base_validate_and_normalize_mcp_server_payload(payload)
_validate_mcp_server_name_fields(payload)
_validate_mcp_user_fields_byok_exclusive(payload)
def _validate_mcp_user_fields_byok_exclusive(payload: Any) -> None:
"""Reject server configurations that combine is_byok with user_fields.
Both credential types share the same (user_id, server_id) row in
LiteLLM_MCPUserCredentials, and the store paths refuse to overwrite
the other type — so a user can save BYOK or user-fields for a given
server, never both. Allowing this combination at admin time would
trap end-users in an unresolvable state: every tool call would 401
on whichever check the user has not (and cannot) satisfy.
"""
if not getattr(payload, "is_byok", False):
return
user_fields = getattr(payload, "user_fields", None) or []
if user_fields:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"error": (
"MCP servers cannot enable both is_byok and user_fields. "
"Use user_fields if the server needs multiple per-user "
"values; use is_byok only for a single legacy BYOK "
"credential."
)
},
)
_VALID_MCP_REQUIRED_FIELDS: frozenset = frozenset(NewMCPServerRequest.model_fields)