refactor(mcp): keep tools failure handling in place and drop catalog fault log

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
joshua 2026-09-19 06:22:52 +00:00
parent 2c9c042cf0
commit 6030fcc01d
2 changed files with 45 additions and 21 deletions

View file

@ -1415,11 +1415,12 @@ def _upstream_failure_suffix(exc: BaseException) -> str:
return f"\n upstream exchange: {detail}" if detail else ""
def _raise_single_server_list_failure(error: Exception, server: MCPServer, catalog: str) -> NoReturn:
"""Relay a failed single-server catalog fetch: auth challenges (upstream, or a v2 resolver's
HTTPException 401/403 raised at client-build time) become ``MCPUpstreamAuthError`` with the
``WWW-Authenticate`` kept (dropped for dcr_bridge servers, whose upstream challenge points at the
wrong metadata); anything else becomes a classified ``MCPServerListError``."""
def _raise_single_server_list_failure(error: Exception, server: MCPServer) -> NoReturn:
"""Relay a failed single-server catalog fetch the way ``_get_tools_from_server`` does: auth
challenges (upstream, or a v2 resolver's HTTPException 401/403 raised at client-build time) become
``MCPUpstreamAuthError`` with the ``WWW-Authenticate`` kept (dropped for dcr_bridge servers, whose
upstream challenge points at the wrong metadata); anything else becomes a classified
``MCPServerListError``. Callers log before delegating here."""
match error:
case MCPUpstreamAuthError() if server.is_dcr_bridge and error.www_authenticate is not None:
raise MCPUpstreamAuthError(
@ -1438,18 +1439,10 @@ def _raise_single_server_list_failure(error: Exception, server: MCPServer, catal
server_name=server.name,
) from error
case HTTPException():
verbose_logger.warning("Failed to get %s from server %s: %s", catalog, server.name, error)
raise MCPServerListError(
ServerListFault(tag="internal", status_code=error.status_code), server.name
) from error
case _:
verbose_logger.warning(
"Failed to get %s from server %s: %s%s",
catalog,
server.name,
type(error).__name__,
_upstream_failure_suffix(error),
)
raise_classified_list_failure(error, server.name, suppress_challenge=server.is_dcr_bridge)
@ -4446,8 +4439,40 @@ class MCPServerManager:
return prefixed_or_original_tools
except MCPUpstreamAuthError as upstream_auth_error:
# Pass-through 401 must surface to single-server routes so the
# client triggers the upstream OAuth flow. The multi-server
# aggregator catches this explicitly to keep absorbing.
if server.is_dcr_bridge and upstream_auth_error.www_authenticate is not None:
raise MCPUpstreamAuthError(
status_code=upstream_auth_error.status_code,
www_authenticate=None,
server_name=upstream_auth_error.server_name,
) from upstream_auth_error
raise
except HTTPException as e:
# A v2 resolver auth challenge (token_exchange's RFC 9728 401, authorization_code's
# browser-OAuth 401, or a 403) is raised at client-build time, inside this try. Route it
# through the same MCPUpstreamAuthError channel as pass-through so single-server routes
# surface the challenge (the client re-authenticates) while the aggregator keeps absorbing.
# Non-auth HTTP errors stay absorbed so one misconfigured server can't blank the listing.
if e.status_code in (401, 403):
headers: Final = e.headers or {}
challenge_header: Final = headers.get("WWW-Authenticate") or headers.get("www-authenticate")
raise MCPUpstreamAuthError(
status_code=e.status_code,
www_authenticate=None if server.is_dcr_bridge else challenge_header,
server_name=server.name,
) from e
verbose_logger.warning("Failed to get tools from server %s: %s", server.name, e)
raise MCPServerListError(ServerListFault(tag="internal", status_code=e.status_code), server.name) from e
except MCPServerListError:
raise
except Exception as e:
_raise_single_server_list_failure(e, server, "tools")
verbose_logger.warning(
"Failed to get tools from server %s: %s%s", server.name, type(e).__name__, _upstream_failure_suffix(e)
)
raise_classified_list_failure(e, server.name, suppress_challenge=server.is_dcr_bridge)
async def _resolve_list_headers(
self,
@ -4569,9 +4594,9 @@ class MCPServerManager:
items: Final = await self._prompt_discovery_cache.get(key, fetch)
return self._create_prefixed_prompts(items, server, add_prefix=add_prefix)
except Exception as error:
if raise_on_error:
_raise_single_server_list_failure(error, server, "prompts")
verbose_logger.warning("Failed to get prompts from server %s: %s", server.name, error)
if raise_on_error:
_raise_single_server_list_failure(error, server)
return []
async def get_resources_from_server(
@ -4613,9 +4638,9 @@ class MCPServerManager:
items: Final = await self._resource_discovery_cache.get(key, fetch)
return self._create_prefixed_resources(items, server, add_prefix=add_prefix)
except Exception as error:
if raise_on_error:
_raise_single_server_list_failure(error, server, "resources")
verbose_logger.warning("Failed to get resources from server %s: %s", server.name, error)
if raise_on_error:
_raise_single_server_list_failure(error, server)
return []
async def get_resource_templates_from_server(
@ -4657,9 +4682,9 @@ class MCPServerManager:
items: Final = await self._template_discovery_cache.get(key, fetch)
return self._create_prefixed_resource_templates(items, server, add_prefix=add_prefix)
except Exception as error:
if raise_on_error:
_raise_single_server_list_failure(error, server, "resource templates")
verbose_logger.warning("Failed to get resource_templates from server %s: %s", server.name, error)
if raise_on_error:
_raise_single_server_list_failure(error, server)
return []
async def read_resource_from_server(

View file

@ -1121,7 +1121,6 @@ if MCP_AVAILABLE:
error: MCPServerListError, server: MCPServer, catalog: Literal["prompts", "resources"]
) -> HTTPException:
fault: Final = classify_list_exception(error)
verbose_logger.info("Listing %s from %s failed with a %s fault", catalog, server.name, fault.tag)
return HTTPException(
status_code=list_fault_http_status(fault),
detail={