Skip to content

feat(session): propagate callback exceptions to the awaiter#2674

Open
Ar-maan05 wants to merge 12 commits into
modelcontextprotocol:mainfrom
Ar-maan05:feat/propagate-callback-exceptions
Open

feat(session): propagate callback exceptions to the awaiter#2674
Ar-maan05 wants to merge 12 commits into
modelcontextprotocol:mainfrom
Ar-maan05:feat/propagate-callback-exceptions

Conversation

@Ar-maan05
Copy link
Copy Markdown

@Ar-maan05 Ar-maan05 commented May 24, 2026

Allow exceptions raised inside user-defined client callbacks (elicitation, sampling, list roots) to propagate back to the awaiter of the outgoing request (e.g., session.call_tool), instead of being silently swallowed by the receive loop and converted into a generic INVALID_PARAMS JSON-RPC error.

Usage Example

Users opt-in to propagation by adding a __mcp_propagate__ marker attribute to their custom exceptions:

class MyControlFlowException(Exception):
__mcp_propagate__ = True

async def on_elicit(ctx, params):
# This exception will bubble up to the caller of send_request / call_tool
raise MyControlFlowException("abort")

Solution

• Added self._propagate_errors: dict[RequestId, BaseException] = {} to BaseSession to stash exceptions marked with mcp_propagate = True .
• In _receive_loop , if a callback exception has mcp_propagate = True :
• Send an INTERNAL_ERROR response back to the server so it doesn't hang.
• Populate _propagate_errors for active request IDs.
• Close active outgoing request streams and exit the receive loop.
• In send_request , catch anyio.EndOfStream and raise the stashed exception on the caller's task.
• Added test_callback_exception_propagation in tests/shared/test_session.py to prevent regressions.

Closes #2673

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

Labels

None yet

Projects

None yet

1 participant