[v2] Dispatcher/ServerRunner receive-path swap — replaces BaseSession (#2710)
This commit is contained in:
parent
b478bff56d
commit
5d826490b6
1 changed files with 6594 additions and 1701 deletions
|
|
@ -7,45 +7,35 @@ 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
|
||||
from unittest.mock import Mock
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
|
||||
from mcp import types
|
||||
from mcp.server.models import InitializationOptions
|
||||
from mcp.server.connection import Connection
|
||||
from mcp.server.context import ServerRequestContext
|
||||
from mcp.server.lowlevel.server import Server
|
||||
from mcp.server.session import ServerSession
|
||||
from mcp.shared.exceptions import StatelessModeNotSupported
|
||||
from mcp.shared.exceptions import NoBackChannelError, StatelessModeNotSupported
|
||||
from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher
|
||||
from mcp.shared.message import SessionMessage
|
||||
from mcp.types import ServerCapabilities
|
||||
from mcp.types import JSONRPCRequest, JSONRPCResponse, ListToolsResult, PaginatedRequestParams
|
||||
|
||||
|
||||
def _make_session(*, stateless: bool) -> ServerSession:
|
||||
"""A `ServerSession` with a mock dispatcher; the stateless guard fires before any send."""
|
||||
return ServerSession(
|
||||
Mock(spec=JSONRPCDispatcher),
|
||||
Connection(Mock(), has_standalone_channel=False),
|
||||
stateless=stateless,
|
||||
)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
init_options = InitializationOptions(
|
||||
server_name="test",
|
||||
server_version="0.1.0",
|
||||
capabilities=ServerCapabilities(),
|
||||
)
|
||||
|
||||
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,
|
||||
init_options,
|
||||
stateless=True,
|
||||
) as session:
|
||||
yield session
|
||||
def stateless_session() -> ServerSession:
|
||||
return _make_session(stateless=True)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
@ -126,30 +116,8 @@ async def test_exception_has_method_attribute(stateless_session: ServerSession):
|
|||
|
||||
|
||||
@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,
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
server_to_client_receive,
|
||||
):
|
||||
async with ServerSession(
|
||||
client_to_server_receive,
|
||||
server_to_client_send,
|
||||
init_options,
|
||||
stateless=False,
|
||||
) as session:
|
||||
yield session
|
||||
def stateful_session() -> ServerSession:
|
||||
return _make_session(stateless=False)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
@ -175,3 +143,45 @@ async def test_stateful_mode_does_not_raise_stateless_error(
|
|||
|
||||
assert send_request_called
|
||||
assert isinstance(result, types.ListRootsResult)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_server_run_stateless_wires_no_standalone_channel():
|
||||
"""`Server.run(stateless=True)` must wire `Connection.has_standalone_channel=False`.
|
||||
|
||||
Stateless HTTP has no standalone GET stream, so server-initiated requests on
|
||||
the connection must fail fast with `NoBackChannelError` rather than write to
|
||||
a channel that will never deliver a response. The `ServerSession` typed
|
||||
helpers carry their own stateless guard (tested above); this pins the
|
||||
`Connection` wiring that `Server.run` produces.
|
||||
"""
|
||||
captured: list[Connection] = []
|
||||
|
||||
async def list_tools(ctx: ServerRequestContext[Any], params: PaginatedRequestParams | None) -> ListToolsResult:
|
||||
# `ServerRequestContext` doesn't expose `connection` directly yet (it
|
||||
# will after the Context rework); reach it via the session for now.
|
||||
captured.append(ctx.session._connection) # pyright: ignore[reportPrivateUsage]
|
||||
return ListToolsResult(tools=[])
|
||||
|
||||
server: Server[Any] = Server("test", on_list_tools=list_tools)
|
||||
|
||||
to_server, server_read = anyio.create_memory_object_stream[SessionMessage | Exception](10)
|
||||
server_write, from_server = anyio.create_memory_object_stream[SessionMessage](10)
|
||||
|
||||
async def run_server() -> None:
|
||||
await server.run(server_read, server_write, server.create_initialization_options(), stateless=True)
|
||||
|
||||
async with anyio.create_task_group() as tg, to_server, server_read, server_write, from_server:
|
||||
tg.start_soon(run_server)
|
||||
# stateless=True skips the init gate, so tools/list routes immediately.
|
||||
await to_server.send(SessionMessage(JSONRPCRequest(jsonrpc="2.0", id=1, method="tools/list")))
|
||||
with anyio.fail_after(5):
|
||||
response = (await from_server.receive()).message
|
||||
assert isinstance(response, JSONRPCResponse)
|
||||
tg.cancel_scope.cancel()
|
||||
|
||||
assert len(captured) == 1
|
||||
conn = captured[0]
|
||||
assert conn.has_standalone_channel is False
|
||||
with pytest.raises(NoBackChannelError):
|
||||
await conn.ping()
|
||||
|
|
|
|||
Loading…
Reference in a new issue