Skip to content

Python: fix(mcp): name the real error when a cancel scope masks MCP init failures - #7704

Open
Yufeng He (he-yufeng) wants to merge 3 commits into
microsoft:mainfrom
he-yufeng:fix/mcp-cancel-scope-error-masking
Open

Python: fix(mcp): name the real error when a cancel scope masks MCP init failures#7704
Yufeng He (he-yufeng) wants to merge 3 commits into
microsoft:mainfrom
he-yufeng:fix/mcp-cancel-scope-error-masking

Conversation

@he-yufeng

@he-yufengYufeng He (he-yufeng) commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

An MCP server can reject initialization with HTTP 401 while the client surfaces only Cancelled via cancel scope. That hides the useful authentication error and sends users toward cancellation debugging.

Description & Review Guide

_describe_error() follows a masked exception chain and unwraps a single member exception group, while preserving a bare cancellation when no better error exists. The connect path also remembers a failure raised during exit stack cleanup, so that error wins when initialization itself reports only a bare cancellation.

Please focus on the chain termination rules and the cleanup failure precedence.

Related Issue

Fixes#7699

Testing

The complete MCP test module passes locally with 289 tests and 2 environment skips. Ruff and all five core test type checkers pass on the current rebased branch, including Python 3.10 static analysis of the Python 3.11 exception group cases.

Checklist

  • This is a bug fix
  • Tests added or updated and passing
  • Code follows the project coding standards

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an internal _describe_error helper to improve MCP connection/session error messages by unmasking “cancel scope” cancellations and single-leaf ExceptionGroups so the real underlying failure is surfaced.

Changes:

  • Introduced _describe_error helper in agent_framework/_mcp.py and used it in multiple MCP connection/session error messages.
  • Added unit tests covering plain exceptions, cancel-scope unmasking via __context__, and single-leaf ExceptionGroup unwrapping.
  • Added an integration-style test ensuring ToolException messages prefer the inner auth failure over the cancel-scope message.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

FileDescription
python/packages/core/agent_framework/_mcp.pyAdds _describe_error and applies it to MCP connection/session initialization error messages.
python/packages/core/tests/core/test_mcp.pyAdds tests validating _describe_error behavior and improved connect() error reporting.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +405 to +407
inner = getattr(current, "exceptions", None) # ExceptionGroup leaf
if inner is not None and len(inner) == 1:
current = inner[0]
bare cancellation keeps its own message.
"""
current = ex
for _ in range(10): # pathological chains only; normally 0-2 hops
def _describe_error(ex: BaseException) -> str:
"""Return the most specific message in *ex*'s chain, unmasking cancel scopes.

anyio cancel scopes and task groups surface internal failures as a bare
@agent-framework-automationagent-framework-automationBot added the python Usage: [Issues, PRs], Target: Python label Aug 17, 2026
error_msg = f"MCP server '{full_command}' failed to initialize: {_describe_error(ex)}"
else:
error_msg = f"MCP server failed to initialize: {ex}"
error_msg = f"MCP server failed to initialize: {_describe_error(ex)}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we carry the exception raised by _safe_close_exit_stack() into this message instead of describing only ex? In the reported 401 path, session.initialize() raises a bare CancelledError; the sibling task's HTTPStatusError appears later in the ExceptionGroup from aclose(), which _safe_close_exit_stack() logs and discards before this line runs. With a real 401 endpoint this still produces MCP server failed to initialize: WouldBlock(), so capturing the cleanup group and selecting its leaf is needed for the linked issue.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, that path still reported the cancellation. In 2a3f77a_safe_close_exit_stack now returns the failure it swallowed during aclose(), and the three cancel-catch sites that build a ToolException describe that close-time error when ex itself is a bare CancelledError. Your 401 path now reads MCP server failed to initialize: 401 Client Error: Unauthorized instead of WouldBlock(). Regression test added with the close raising a single-leaf ExceptionGroup (3.11+ gated, matching the neighboring tests). Also folded in the Copilot notes above: the unwrap loop now gates group membership on the type name rather than the attribute, and the hop bound is a named constant.

@github-actions

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
_mcp.py141911691%254, 260, 369, 388, 422, 655, 734–735, 849, 874, 925, 1040, 1043, 1053, 1057, 1104–1105, 1110, 1117–1118, 1125, 1130–1131, 1138–1139, 1143, 1148–1149, 1158, 1165–1166, 1184, 1197, 1221–1222, 1241–1244, 1246–1247, 1251, 1283, 1323–1325, 1327, 1382–1384, 1446–1447, 1730, 1771–1772, 1785, 1788, 1797–1798, 1803–1804, 1810, 1864–1865, 1885–1886, 1895–1896, 1901–1902, 1908, 2001, 2004, 2031, 2054–2058, 2081–2083, 2088, 2092–2093, 2195, 2202, 2204, 2275, 2290–2291, 2298–2299, 2304–2305, 2310, 2314, 2329, 2391, 2574, 2576, 2598, 2600–2603, 2616–2617, 2661, 2723, 3162–3163, 3208, 3428–3429, 3447
TOTAL47120437490%

Python Unit Test Overview

TestsSkippedFailuresErrorsTime
954736 💤0 ❌0 🔥2m 37s ⏱️

…nit failures
When the MCP client stack cancels (e.g. an HTTP 401 from the server during session creation), anyio surfaces a bare CancelledError and the wrap sites reported 'Cancelled via cancel scope ...' as the failure. Unwrap single-member exception groups and __cause__/__context__ chains when composing the ToolException message so the real error is named. Genuine caller-driven cancellations still propagate unchanged and bare cancellations keep their own message.
Fixesmicrosoft#7699
@he-yufeng

Copy link
Copy Markdown
ContributorAuthor

Rebased onto current main and fixed the Python 3.10 typing failure by resolving ExceptionGroup dynamically inside tests that already run only on Python 3.11 and newer. The full MCP test module passes with 289 tests and 2 environment skips. Ruff and all five core test type checkers pass as well.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pythonUsage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: MCP HTTP auth failures (401) surface as "Cancelled via cancel scope ..." instead of the actual error

3 participants

@he-yufeng@moonbox3