Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/mcp/server/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,29 @@ async def _received_request(self, responder: RequestResponder[types.ClientReques
pass
case _:
if self._initialization_state != InitializationState.Initialized:
raise RuntimeError("Received request before initialization was complete")
# Answer the request directly with a self-describing error
# instead of raising. A bare exception here would propagate
# to BaseSession._receive_loop's blanket except-Exception
# handler, which discards the exception's message entirely
# and always responds with the generic, misleading
# ErrorData(code=INVALID_PARAMS, message="Invalid request
# parameters") -- indistinguishable from an actually
# malformed request. INVALID_REQUEST (not INVALID_PARAMS)
# is used because the request's parameters aren't the
# problem; the session's state is.
with responder:
await responder.respond(
types.ErrorData(
code=types.INVALID_REQUEST,
message=(
"MCP session not initialized: this session_id's "
"stream was reconnected or lost without a fresh "
"'initialize' handshake. Reconnect and initialize "
"a new session."
),
data="session_not_initialized",
)
)

async def _received_notification(self, notification: types.ClientNotification) -> None:
# Need this to avoid ASYNC910
Expand Down
13 changes: 11 additions & 2 deletions tests/server/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ async def test_other_requests_blocked_before_initialization():

error_response_received = False
error_code = None
error_message_text = None

async def run_server():
async with ServerSession(
Expand All @@ -488,7 +489,7 @@ async def run_server():
await anyio.sleep(0.1) # Give time for the request to be processed

async def mock_client():
nonlocal error_response_received, error_code
nonlocal error_response_received, error_code, error_message_text

# Try to send a non-ping request before initialization
await client_to_server_send.send(
Expand All @@ -508,6 +509,7 @@ async def mock_client():
if isinstance(error_message.message.root, types.JSONRPCError): # pragma: no branch
error_response_received = True
error_code = error_message.message.root.error.code
error_message_text = error_message.message.root.error.message

async with (
client_to_server_send,
Expand All @@ -520,4 +522,11 @@ async def mock_client():
tg.start_soon(mock_client)

assert error_response_received
assert error_code == types.INVALID_PARAMS
# INVALID_REQUEST (not INVALID_PARAMS): the request's parameters aren't
# the problem, the session's initialization state is. Previously this
# fell through to BaseSession._receive_loop's generic exception handler,
# which always reported INVALID_PARAMS with a generic "Invalid request
# parameters" message regardless of the actual cause -- indistinguishable
# from a genuinely malformed request.
assert error_code == types.INVALID_REQUEST
assert error_message_text is not None and "session not initialized" in error_message_text.lower()
Loading