fix: add StatelessModeNotSupported exception and improve tests (#1828)
This commit is contained in:
parent
bb6cb029f0
commit
37de50144f
1 changed files with 132 additions and 215 deletions
|
|
@ -7,47 +7,32 @@ that appropriate errors are raised when attempting to use unsupported features.
|
|||
See: https://github.com/modelcontextprotocol/python-sdk/issues/1097
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
|
||||
import mcp.types as types
|
||||
from mcp.server.models import InitializationOptions
|
||||
from mcp.server.session import ServerSession
|
||||
from mcp.shared.exceptions import StatelessModeNotSupported
|
||||
from mcp.shared.message import SessionMessage
|
||||
from mcp.types import ServerCapabilities
|
||||
|
||||
|
||||
def create_test_streams():
|
||||
"""Create memory streams for testing."""
|
||||
@pytest.fixture
|
||||
async def stateless_session() -> AsyncGenerator[ServerSession, None]:
|
||||
"""Create a stateless ServerSession for testing."""
|
||||
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)
|
||||
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)
|
||||
return (
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
)
|
||||
|
||||
|
||||
def create_init_options():
|
||||
"""Create default initialization options for testing."""
|
||||
return InitializationOptions(
|
||||
init_options = InitializationOptions(
|
||||
server_name="test",
|
||||
server_version="0.1.0",
|
||||
capabilities=ServerCapabilities(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_list_roots_fails_in_stateless_mode():
|
||||
"""Test that list_roots raises RuntimeError in stateless mode."""
|
||||
(
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
) = create_test_streams()
|
||||
|
||||
async with (
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
|
|
@ -57,187 +42,100 @@ async def test_list_roots_fails_in_stateless_mode():
|
|||
async with ServerSession(
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
create_init_options(),
|
||||
init_options,
|
||||
stateless=True,
|
||||
) as session:
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await session.list_roots()
|
||||
|
||||
assert "stateless HTTP mode" in str(exc_info.value)
|
||||
assert "list_roots" in str(exc_info.value)
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_create_message_fails_in_stateless_mode():
|
||||
"""Test that create_message raises RuntimeError in stateless mode."""
|
||||
(
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
) = create_test_streams()
|
||||
async def test_list_roots_fails_in_stateless_mode(stateless_session: ServerSession):
|
||||
"""Test that list_roots raises StatelessModeNotSupported in stateless mode."""
|
||||
with pytest.raises(StatelessModeNotSupported, match="list_roots"):
|
||||
await stateless_session.list_roots()
|
||||
|
||||
async with (
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
):
|
||||
async with ServerSession(
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
create_init_options(),
|
||||
stateless=True,
|
||||
) as session:
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await session.create_message(
|
||||
messages=[
|
||||
types.SamplingMessage(
|
||||
role="user",
|
||||
content=types.TextContent(type="text", text="hello"),
|
||||
)
|
||||
],
|
||||
max_tokens=100,
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_create_message_fails_in_stateless_mode(stateless_session: ServerSession):
|
||||
"""Test that create_message raises StatelessModeNotSupported in stateless mode."""
|
||||
with pytest.raises(StatelessModeNotSupported, match="sampling"):
|
||||
await stateless_session.create_message(
|
||||
messages=[
|
||||
types.SamplingMessage(
|
||||
role="user",
|
||||
content=types.TextContent(type="text", text="hello"),
|
||||
)
|
||||
|
||||
assert "stateless HTTP mode" in str(exc_info.value)
|
||||
assert "sampling" in str(exc_info.value)
|
||||
],
|
||||
max_tokens=100,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_elicit_form_fails_in_stateless_mode():
|
||||
"""Test that elicit_form raises RuntimeError in stateless mode."""
|
||||
(
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
) = create_test_streams()
|
||||
|
||||
async with (
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
):
|
||||
async with ServerSession(
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
create_init_options(),
|
||||
stateless=True,
|
||||
) as session:
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await session.elicit_form(
|
||||
message="Please provide input",
|
||||
requestedSchema={"type": "object", "properties": {}},
|
||||
)
|
||||
|
||||
assert "stateless HTTP mode" in str(exc_info.value)
|
||||
assert "elicitation" in str(exc_info.value)
|
||||
async def test_elicit_form_fails_in_stateless_mode(stateless_session: ServerSession):
|
||||
"""Test that elicit_form raises StatelessModeNotSupported in stateless mode."""
|
||||
with pytest.raises(StatelessModeNotSupported, match="elicitation"):
|
||||
await stateless_session.elicit_form(
|
||||
message="Please provide input",
|
||||
requestedSchema={"type": "object", "properties": {}},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_elicit_url_fails_in_stateless_mode():
|
||||
"""Test that elicit_url raises RuntimeError in stateless mode."""
|
||||
(
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
) = create_test_streams()
|
||||
|
||||
async with (
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
):
|
||||
async with ServerSession(
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
create_init_options(),
|
||||
stateless=True,
|
||||
) as session:
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await session.elicit_url(
|
||||
message="Please authenticate",
|
||||
url="https://example.com/auth",
|
||||
elicitation_id="test-123",
|
||||
)
|
||||
|
||||
assert "stateless HTTP mode" in str(exc_info.value)
|
||||
assert "elicitation" in str(exc_info.value)
|
||||
async def test_elicit_url_fails_in_stateless_mode(stateless_session: ServerSession):
|
||||
"""Test that elicit_url raises StatelessModeNotSupported in stateless mode."""
|
||||
with pytest.raises(StatelessModeNotSupported, match="elicitation"):
|
||||
await stateless_session.elicit_url(
|
||||
message="Please authenticate",
|
||||
url="https://example.com/auth",
|
||||
elicitation_id="test-123",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_elicit_deprecated_fails_in_stateless_mode():
|
||||
async def test_elicit_deprecated_fails_in_stateless_mode(stateless_session: ServerSession):
|
||||
"""Test that the deprecated elicit method also fails in stateless mode."""
|
||||
(
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
) = create_test_streams()
|
||||
|
||||
async with (
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
):
|
||||
async with ServerSession(
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
create_init_options(),
|
||||
stateless=True,
|
||||
) as session:
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await session.elicit(
|
||||
message="Please provide input",
|
||||
requestedSchema={"type": "object", "properties": {}},
|
||||
)
|
||||
|
||||
assert "stateless HTTP mode" in str(exc_info.value)
|
||||
assert "elicitation" in str(exc_info.value)
|
||||
with pytest.raises(StatelessModeNotSupported, match="elicitation"):
|
||||
await stateless_session.elicit(
|
||||
message="Please provide input",
|
||||
requestedSchema={"type": "object", "properties": {}},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_require_stateful_mode_does_not_raise_in_stateful_mode():
|
||||
"""Test that _require_stateful_mode does not raise in stateful mode."""
|
||||
(
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
) = create_test_streams()
|
||||
|
||||
async with (
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
):
|
||||
async with ServerSession(
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
create_init_options(),
|
||||
stateless=False, # Stateful mode
|
||||
) as session:
|
||||
# These should not raise - the check passes in stateful mode
|
||||
session._require_stateful_mode("list_roots")
|
||||
session._require_stateful_mode("sampling")
|
||||
session._require_stateful_mode("elicitation")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_stateless_error_message_is_actionable():
|
||||
async def test_stateless_error_message_is_actionable(stateless_session: ServerSession):
|
||||
"""Test that the error message provides actionable guidance."""
|
||||
(
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
client_to_server_send,
|
||||
client_to_server_receive,
|
||||
) = create_test_streams()
|
||||
with pytest.raises(StatelessModeNotSupported) as exc_info:
|
||||
await stateless_session.list_roots()
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
# Should mention it's stateless mode
|
||||
assert "stateless HTTP mode" in error_message
|
||||
# Should explain why it doesn't work
|
||||
assert "server-to-client requests" in error_message
|
||||
# Should tell user how to fix it
|
||||
assert "stateless_http=False" in error_message
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_exception_has_method_attribute(stateless_session: ServerSession):
|
||||
"""Test that the exception has a method attribute for programmatic access."""
|
||||
with pytest.raises(StatelessModeNotSupported) as exc_info:
|
||||
await stateless_session.list_roots()
|
||||
|
||||
assert exc_info.value.method == "list_roots"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def stateful_session() -> AsyncGenerator[ServerSession, None]:
|
||||
"""Create a stateful ServerSession for testing."""
|
||||
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)
|
||||
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)
|
||||
|
||||
init_options = InitializationOptions(
|
||||
server_name="test",
|
||||
server_version="0.1.0",
|
||||
capabilities=ServerCapabilities(),
|
||||
)
|
||||
|
||||
async with (
|
||||
client_to_server_send,
|
||||
|
|
@ -248,16 +146,32 @@ async def test_stateless_error_message_is_actionable():
|
|||
async with ServerSession(
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
create_init_options(),
|
||||
stateless=True,
|
||||
init_options,
|
||||
stateless=False,
|
||||
) as session:
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await session.list_roots()
|
||||
yield session
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
# Should mention it's stateless mode
|
||||
assert "stateless HTTP mode" in error_message
|
||||
# Should explain why it doesn't work
|
||||
assert "server-to-client requests" in error_message
|
||||
# Should tell user how to fix it
|
||||
assert "stateless_http=False" in error_message
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_stateful_mode_does_not_raise_stateless_error(
|
||||
stateful_session: ServerSession, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
"""Test that StatelessModeNotSupported is not raised in stateful mode.
|
||||
|
||||
We mock send_request to avoid blocking on I/O while still verifying
|
||||
that the stateless check passes.
|
||||
"""
|
||||
send_request_called = False
|
||||
|
||||
async def mock_send_request(*_: Any, **__: Any) -> types.ListRootsResult:
|
||||
nonlocal send_request_called
|
||||
send_request_called = True
|
||||
return types.ListRootsResult(roots=[])
|
||||
|
||||
monkeypatch.setattr(stateful_session, "send_request", mock_send_request)
|
||||
|
||||
# This should NOT raise StatelessModeNotSupported
|
||||
result = await stateful_session.list_roots()
|
||||
|
||||
assert send_request_called
|
||||
assert isinstance(result, types.ListRootsResult)
|
||||
|
|
|
|||
Loading…
Reference in a new issue