fix(mcp): search explicit exception links before __context__ when finding the upstream response

This commit is contained in:
Tin Chi Lo 2026-07-13 23:21:29 -07:00
parent eefd5e31e5
commit 424443c11f
2 changed files with 34 additions and 5 deletions

View file

@ -63,7 +63,10 @@ class AggregateToolListing(NamedTuple):
def _find_upstream_response(exc: BaseException) -> httpx.Response | None:
"""Walk the exception tree (``__cause__``/``__context__``/ExceptionGroup members) for an
``httpx.Response``, mirroring how upstream failures surface through the MCP SDK's task groups."""
``httpx.Response``, mirroring how upstream failures surface through the MCP SDK's task groups.
Explicit links are searched first: each node's ``raise ... from`` cause, then group members in
raise order, then the incidental ``__context__`` chain, so a response raised while handling the
real failure can never shadow the response on the explicit causal chain."""
seen: set[int] = set()
stack = [exc]
while stack:
@ -74,12 +77,13 @@ def _find_upstream_response(exc: BaseException) -> httpx.Response | None:
response = getattr(current, "response", None)
if isinstance(response, httpx.Response):
return response
if current.__context__ is not None:
stack.append(current.__context__)
exceptions = getattr(current, "exceptions", None)
if isinstance(exceptions, tuple):
stack.extend(exceptions)
for link in (current.__cause__, current.__context__):
if link is not None:
stack.append(link)
stack.extend(reversed(exceptions))
if current.__cause__ is not None:
stack.append(current.__cause__)
return None

View file

@ -49,6 +49,31 @@ def test_embedded_401_classifies_auth_required():
assert classify_list_exception(exc).tag == "auth_required"
def test_context_response_does_not_shadow_the_causal_chain_response():
real_response = httpx.Response(401, request=httpx.Request("POST", "https://mcp.example.com/mcp"))
real = httpx.HTTPStatusError("upstream rejected", request=real_response.request, response=real_response)
incidental_response = httpx.Response(500, request=httpx.Request("POST", "https://hooks.example.com/log"))
incidental = httpx.HTTPStatusError(
"logging hook failed", request=incidental_response.request, response=incidental_response
)
wrapper = RuntimeError("wrapper")
wrapper.__cause__ = real
wrapper.__context__ = incidental
fault = classify_list_exception(wrapper)
assert fault.tag == "auth_required"
assert fault.status_code == 401
def test_exception_group_members_are_searched_in_raise_order():
first_response = httpx.Response(502, request=httpx.Request("POST", "https://mcp.example.com/mcp"))
first = httpx.HTTPStatusError("first", request=first_response.request, response=first_response)
second_response = httpx.Response(503, request=httpx.Request("POST", "https://mcp.example.com/mcp"))
second = httpx.HTTPStatusError("second", request=second_response.request, response=second_response)
fault = classify_list_exception(BaseExceptionGroup("task group", [first, second]))
assert fault.tag == "upstream_error"
assert fault.status_code == 502
def test_unknown_exception_is_internal():
assert classify_list_exception(ValueError("who knows")).tag == "internal"