Skip to content

Conversation

@habema
Copy link
Contributor

@habema habema commented Jan 7, 2026

Fixes an issue where MCP server connection failures (e.g., 401 Unauthorized) surfaced as confusing BaseExceptionGroup exceptions instead of clear errors.

What changed:

  • Added _unwrap_exception_group() to extract meaningful exceptions from anyio task group exception groups
  • Updated connect() to unwrap exceptions and re-raise the underlying error (e.g., HTTPStatusError for 401s)
  • Updated cleanup() to handle exception groups during cleanup and surface meaningful errors

Result:

Cryptic BaseExceptionGroup with multiple nested exceptions now become clear httpx.HTTPStatusError: Client error '401 Unauthorized' messages


Logs before:

an error occurred during closing of asynchronous generator <async_generator object streamablehttp_client at 0x1116ac490>
asyncgen: <async_generator object streamablehttp_client at 0x1116ac490>
  + Exception Group Traceback (most recent call last):
  |   File "./openai-agents-python/.venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 772, in __aexit__
  |     raise BaseExceptionGroup(
  |         "unhandled errors in a TaskGroup", self._exceptions
  |     ) from None
  | BaseExceptionGroup: unhandled errors in a TaskGroup (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "./openai-agents-python/.venv/lib/python3.13/site-packages/mcp/client/streamable_http.py", line 405, in handle_request_async
    |     await self._handle_post_request(ctx)
    |   File "./openai-agents-python/.venv/lib/python3.13/site-packages/mcp/client/streamable_http.py", line 277, in _handle_post_request
    |     response.raise_for_status()
    |     ~~~~~~~~~~~~~~~~~~~~~~~~~^^
    |   File "./openai-agents-python/.venv/lib/python3.13/site-packages/httpx/_models.py", line 829, in raise_for_status
    |     raise HTTPStatusError(message, request=request, response=self)
    | httpx.HTTPStatusError: Client error '401 Unauthorized' for url 'http://localhost:8000/mcp'
    | For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "./openai-agents-python/.venv/lib/python3.13/site-packages/mcp/client/streamable_http.py", line 498, in streamablehttp_client
    |     yield (
    |     ...<3 lines>...
    |     )
    | GeneratorExit
    +------------------------------------

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/mcp/client/streamable_http.py", line 474, in streamablehttp_client
    async with anyio.create_task_group() as tg:
               ~~~~~~~~~~~~~~~~~~~~~~~^^
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 778, in __aexit__
    if self.cancel_scope.__exit__(type(exc), exc, exc.__traceback__):
       ~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 457, in __exit__
    raise RuntimeError(
    ...<2 lines>...
    )
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
Traceback (most recent call last):
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/anyio/streams/memory.py", line 111, in receive
    return self.receive_nowait()
           ~~~~~~~~~~~~~~~~~~~^^
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/anyio/streams/memory.py", line 106, in receive_nowait
    raise WouldBlock
anyio.WouldBlock

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "./openai-agents-python/mcp_test/main.py", line 34, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "/Users/user/.local/share/uv/python/cpython-3.13.9-macos-aarch64-none/lib/python3.13/asyncio/runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "/Users/user/.local/share/uv/python/cpython-3.13.9-macos-aarch64-none/lib/python3.13/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/Users/user/.local/share/uv/python/cpython-3.13.9-macos-aarch64-none/lib/python3.13/asyncio/base_events.py", line 725, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "./openai-agents-python/mcp_test/main.py", line 23, in main
    async with MCPServerStreamableHttp(
               ~~~~~~~~~~~~~~~~~~~~~~~^
        name="Streamable HTTP Python Server",
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<3 lines>...
        },
        ^^
    ) as server:
    ^
  File "./openai-agents-python/src/agents/mcp/server.py", line 244, in __aenter__
    await self.connect()
  File "./openai-agents-python/src/agents/mcp/server.py", line 285, in connect
    server_result = await session.initialize()
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/mcp/client/session.py", line 151, in initialize
    result = await self.send_request(
             ^^^^^^^^^^^^^^^^^^^^^^^^
    ...<16 lines>...
    )
    ^
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/mcp/shared/session.py", line 272, in send_request
    response_or_error = await response_stream_reader.receive()
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/anyio/streams/memory.py", line 119, in receive
    await receive_event.wait()
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 1774, in wait
    await self._event.wait()
  File "/Users/user/.local/share/uv/python/cpython-3.13.9-macos-aarch64-none/lib/python3.13/asyncio/locks.py", line 213, in wait
    await fut
asyncio.exceptions.CancelledError: Cancelled by cancel scope 111777230

Logs After:

Error cleaning up MCP server Streamable HTTP Python Server: Client error '401 Unauthorized' for url 'http://localhost:8000/mcp'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
Error initializing MCP server Streamable HTTP Python Server: Client error '401 Unauthorized' for url 'http://localhost:8000/mcp'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
Traceback (most recent call last):
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/anyio/streams/memory.py", line 111, in receive
    return self.receive_nowait()
           ~~~~~~~~~~~~~~~~~~~^^
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/anyio/streams/memory.py", line 106, in receive_nowait
    raise WouldBlock
anyio.WouldBlock

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "./openai-agents-python/src/agents/mcp/server.py", line 319, in connect
    server_result = await session.initialize()
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/mcp/client/session.py", line 151, in initialize
    result = await self.send_request(
             ^^^^^^^^^^^^^^^^^^^^^^^^
    ...<16 lines>...
    )
    ^
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/mcp/shared/session.py", line 272, in send_request
    response_or_error = await response_stream_reader.receive()
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/anyio/streams/memory.py", line 119, in receive
    await receive_event.wait()
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 1774, in wait
    await self._event.wait()
  File "/Users/user/.local/share/uv/python/cpython-3.13.9-macos-aarch64-none/lib/python3.13/asyncio/locks.py", line 213, in wait
    await fut
asyncio.exceptions.CancelledError: Cancelled by cancel scope 10fd93230

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "./openai-agents-python/mcp_test/main.py", line 34, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "/Users/user/.local/share/uv/python/cpython-3.13.9-macos-aarch64-none/lib/python3.13/asyncio/runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "/Users/user/.local/share/uv/python/cpython-3.13.9-macos-aarch64-none/lib/python3.13/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/Users/user/.local/share/uv/python/cpython-3.13.9-macos-aarch64-none/lib/python3.13/asyncio/base_events.py", line 725, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "./openai-agents-python/mcp_test/main.py", line 23, in main
    async with MCPServerStreamableHttp(
               ~~~~~~~~~~~~~~~~~~~~~~~^
        name="Streamable HTTP Python Server",
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<3 lines>...
        },
        ^^
    ) as server:
    ^
  File "./openai-agents-python/src/agents/mcp/server.py", line 278, in __aenter__
    await self.connect()
  File "./openai-agents-python/src/agents/mcp/server.py", line 336, in connect
    raise error_to_raise from e
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/mcp/client/streamable_http.py", line 405, in handle_request_async
    await self._handle_post_request(ctx)
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/mcp/client/streamable_http.py", line 277, in _handle_post_request
    response.raise_for_status()
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "./openai-agents-python/.venv/lib/python3.13/site-packages/httpx/_models.py", line 829, in raise_for_status
    raise HTTPStatusError(message, request=request, response=self)
httpx.HTTPStatusError: Client error '401 Unauthorized' for url 'http://localhost:8000/mcp'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401

Repro Scripts:

  • main.py
    # main.py
    import asyncio
    
    from agents import Agent, Runner
    from agents.mcp import MCPServer, MCPServerStreamableHttp
    from agents.model_settings import ModelSettings
    
    
    async def run(mcp_server: MCPServer):
        agent = Agent(
            name="Assistant",
            instructions="Use the tools to answer the questions.",
            mcp_servers=[mcp_server],
            model_settings=ModelSettings(tool_choice="required"),
        )
    
        message = "Add these numbers: 7 and 22."
        print(f"Running: {message}")
        result = await Runner.run(starting_agent=agent, input=message)
        print(result.final_output)
    
    
    async def main():
        async with MCPServerStreamableHttp(
            name="Streamable HTTP Python Server",
            params={
                "url": "http://localhost:8000/mcp",
                # "headers": {"Authorization": "Bearer my-secret-token"},
            },
        ) as server:
            await run(server)
    
    
    if __name__ == "__main__":
        asyncio.run(main())
  • server.py
    # server.py
    from mcp.server.auth.provider import AccessToken, TokenVerifier
    from mcp.server.auth.settings import AuthSettings
    from mcp.server.fastmcp import FastMCP
    
    SECRET_TOKEN = "my-secret-token"
    
    
    class SimpleTokenVerifier(TokenVerifier):
        async def verify_token(self, token: str) -> AccessToken | None:
            """
            Verifies the incoming authorization token.
            """
            if token == SECRET_TOKEN:
                print("Token verified successfully.")
                return AccessToken(
                    token=token,
                    client_id="static-client-id",
                    scopes=["read", "write"],
                    expires_at=None,
                    resource="http://localhost:8000/mcp/tools",
                )
            print("Token not verified.")
            return None
    
    
    mcp = FastMCP(
        "Echo Server",
        auth=AuthSettings(
            issuer_url="http://localhost:8000/mcp/auth",
            resource_server_url="http://localhost:8000/mcp",
        ),
        token_verifier=SimpleTokenVerifier(),
    )
    
    
    @mcp.tool()
    def add(a: int, b: int) -> int:
        """Add two numbers"""
        print(f"[debug-server] add({a}, {b})")
        return a + b
    
    
    if __name__ == "__main__":
        mcp.run(transport="streamable-http")

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 65f330839d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +403 to +406
except BaseException as e:
# During cleanup, exception groups can occur from anyio task group cancellation.
# Unwrap to get the meaningful error for logging and potential re-raising.
unwrapped = _unwrap_exception_group(e)

Choose a reason for hiding this comment

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

P2 Badge Re-raise interrupts during cleanup

The new except BaseException in cleanup() suppresses KeyboardInterrupt/SystemExit (and any other BaseException) instead of letting them abort the program. If a user hits Ctrl+C while cleanup is running, the interrupt will be logged and swallowed, so shutdown can hang or continue unexpectedly. Before this change these exceptions would propagate. Consider catching BaseExceptionGroup separately and re-raising KeyboardInterrupt/SystemExit (or re-raising any non-group BaseException) to preserve expected termination behavior.

Useful? React with 👍 / 👎.

@habema habema marked this pull request as draft January 7, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants