Description
What happened?
When an MCP server rejects a request with HTTP 401, MCPStreamableHTTPTool fails with a misleading ToolException: MCP server failed to initialize: Cancelled via cancel scope ... instead of reporting the real 401. The underlying httpx.HTTPStatusError(401) is swallowed, making auth failures very hard to diagnose.
What did you expect to happen?
The 401 should surface as a proper error (e.g. a ToolException naming the HTTP status / auth failure) so a client can react to it.
Steps to reproduce
- Run a protected MCP server (
streamable-http) whose JWT verifier rejects the provided token (e.g. wrong audience/scope -> 401).
- Connect via
MCPStreamableHTTPTool with a header_provider sending a Bearer token (see code sample).
- Observe the
ToolException with the cancellation message instead of a 401.
Root cause
In the mcp SDK's streamable HTTP transport, response.raise_for_status() (streamable_http.py) raises httpx.HTTPStatusError(401) inside a task spawned via tg.start_soon(...). The exception is never routed to the request's response stream, so the task group cancels all siblings - including session.initialize() - which then raises CancelledError. In agent-framework's _connect_on_owner (_mcp.py), _should_propagate_cancelled_error returns False (the cancellation comes from the group's cancel scope, not a caller-driven task.cancel()), so the CancelledError is wrapped into the ToolException above. The real 401 lives only in the cleanup ExceptionGroup, which agent-framework catches in _safe_close_exit_stack and only logs as a warning.
Suggested fix
Related issues
#5667 (fixed the raw CancelledError leak via #5687, but the underlying HTTP error is still lost), #4759 (same symptom via AKS Ingress), #1564 (same "root cause hidden" principle for stdio).
Code Sample
async with MCPStreamableHTTPTool(
name="auth-mcp",
url="http://127.0.0.1:8000/mcp",
header_provider=lambda _kwargs: {"Authorization": f"Bearer {token}"},
) as mcp_tool:
...
Client-side workaround currently in use:
except ToolException as exc:
if "Cancelled via cancel scope" in str(exc):
probe = await client.post(MCP_SERVER_URL, headers={"Authorization": f"Bearer {token}"})
if probe.status_code == 401:
raise PermissionError("MCP auth failed: HTTP 401") from exc
raise
Error Messages / Stack Traces
ToolException: MCP server failed to initialize: Cancelled via cancel scope ...
preceded by the warning:
Could not cleanly close MCP exit stack due to cleanup error group. Error: unhandled errors in a TaskGroup (1 sub-exception)
Environment: agent-framework 1.14.0 (also reproduced on earlier versions), mcp 1.29.0 (pinned constraint in agent-framework is >=1.27.2), Python 3.14, Windows.
Package Versions
agent-framework: 1.14.0 (also reproduced on earlier versions), mcp: 1.29.0
Python Version
Python 3.14
Additional Context
No response
Description
What happened?
When an MCP server rejects a request with HTTP 401,
MCPStreamableHTTPToolfails with a misleadingToolException: MCP server failed to initialize: Cancelled via cancel scope ...instead of reporting the real 401. The underlyinghttpx.HTTPStatusError(401)is swallowed, making auth failures very hard to diagnose.What did you expect to happen?
The 401 should surface as a proper error (e.g. a
ToolExceptionnaming the HTTP status / auth failure) so a client can react to it.Steps to reproduce
streamable-http) whose JWT verifier rejects the provided token (e.g. wrong audience/scope -> 401).MCPStreamableHTTPToolwith aheader_providersending aBearertoken (see code sample).ToolExceptionwith the cancellation message instead of a 401.Root cause
In the
mcpSDK's streamable HTTP transport,response.raise_for_status()(streamable_http.py) raiseshttpx.HTTPStatusError(401)inside a task spawned viatg.start_soon(...). The exception is never routed to the request's response stream, so the task group cancels all siblings - includingsession.initialize()- which then raisesCancelledError. In agent-framework's_connect_on_owner(_mcp.py),_should_propagate_cancelled_errorreturnsFalse(the cancellation comes from the group's cancel scope, not a caller-driventask.cancel()), so theCancelledErroris wrapped into theToolExceptionabove. The real 401 lives only in the cleanupExceptionGroup, which agent-framework catches in_safe_close_exit_stackand only logs as a warning.Suggested fix
ExceptionGroup/ innerhttpx.HTTPStatusErrorand re-raise aToolExceptionthat includes the HTTP status.mcp(401 in Streamable HTTP should be handled gracefully modelcontextprotocol/python-sdk#1295) to route HTTP errors to the request's response stream, and surface those.Related issues
#5667 (fixed the raw
CancelledErrorleak via #5687, but the underlying HTTP error is still lost), #4759 (same symptom via AKS Ingress), #1564 (same "root cause hidden" principle for stdio).Code Sample
async with MCPStreamableHTTPTool( name="auth-mcp", url="http://127.0.0.1:8000/mcp", header_provider=lambda _kwargs: {"Authorization": f"Bearer {token}"}, ) as mcp_tool: ... Client-side workaround currently in use: except ToolException as exc: if "Cancelled via cancel scope" in str(exc): probe = await client.post(MCP_SERVER_URL, headers={"Authorization": f"Bearer {token}"}) if probe.status_code == 401: raise PermissionError("MCP auth failed: HTTP 401") from exc raiseError Messages / Stack Traces
Package Versions
agent-framework: 1.14.0 (also reproduced on earlier versions), mcp: 1.29.0
Python Version
Python 3.14
Additional Context
No response