Files
modelcontextprotocol--pytho…/tests/server/test_session.py
2025-11-11 14:09:32 +01:00

348 lines
12 KiB
Python

from typing import Any
import anyio
import pytest
import mcp.types as types
from mcp.client.session import ClientSession
from mcp.server import Server
from mcp.server.lowlevel import NotificationOptions
from mcp.server.models import InitializationOptions
from mcp.server.session import ServerSession
from mcp.shared.message import SessionMessage
from mcp.shared.session import RequestResponder
from mcp.types import (
ClientNotification,
Completion,
CompletionArgument,
CompletionContext,
CompletionsCapability,
InitializedNotification,
Prompt,
PromptReference,
PromptsCapability,
Resource,
ResourcesCapability,
ResourceTemplateReference,
ServerCapabilities,
)
@pytest.mark.anyio
async def test_server_session_initialize():
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](1)
# Create a message handler to catch exceptions
async def message_handler( # pragma: no cover
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
) -> None:
if isinstance(message, Exception):
raise message
received_initialized = False
async def run_server():
nonlocal received_initialized
async with ServerSession(
client_to_server_receive,
server_to_client_send,
InitializationOptions(
server_name="mcp",
server_version="0.1.0",
capabilities=ServerCapabilities(),
),
) as server_session:
async for message in server_session.incoming_messages: # pragma: no branch
if isinstance(message, Exception): # pragma: no cover
raise message
if isinstance(message, ClientNotification) and isinstance(
message.root, InitializedNotification
): # pragma: no branch
received_initialized = True
return
try:
async with (
ClientSession(
server_to_client_receive,
client_to_server_send,
message_handler=message_handler,
) as client_session,
anyio.create_task_group() as tg,
):
tg.start_soon(run_server)
await client_session.initialize()
except anyio.ClosedResourceError: # pragma: no cover
pass
assert received_initialized
@pytest.mark.anyio
async def test_server_capabilities():
server = Server("test")
notification_options = NotificationOptions()
experimental_capabilities: dict[str, Any] = {}
# Initially no capabilities
caps = server.get_capabilities(notification_options, experimental_capabilities)
assert caps.prompts is None
assert caps.resources is None
assert caps.completions is None
# Add a prompts handler
@server.list_prompts()
async def list_prompts() -> list[Prompt]: # pragma: no cover
return []
caps = server.get_capabilities(notification_options, experimental_capabilities)
assert caps.prompts == PromptsCapability(listChanged=False)
assert caps.resources is None
assert caps.completions is None
# Add a resources handler
@server.list_resources()
async def list_resources() -> list[Resource]: # pragma: no cover
return []
caps = server.get_capabilities(notification_options, experimental_capabilities)
assert caps.prompts == PromptsCapability(listChanged=False)
assert caps.resources == ResourcesCapability(subscribe=False, listChanged=False)
assert caps.completions is None
# Add a complete handler
@server.completion()
async def complete( # pragma: no cover
ref: PromptReference | ResourceTemplateReference,
argument: CompletionArgument,
context: CompletionContext | None,
) -> Completion | None:
return Completion(
values=["completion1", "completion2"],
)
caps = server.get_capabilities(notification_options, experimental_capabilities)
assert caps.prompts == PromptsCapability(listChanged=False)
assert caps.resources == ResourcesCapability(subscribe=False, listChanged=False)
assert caps.completions == CompletionsCapability()
@pytest.mark.anyio
async def test_server_session_initialize_with_older_protocol_version():
"""Test that server accepts and responds with older protocol (2024-11-05)."""
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)
received_initialized = False
received_protocol_version = None
async def run_server():
nonlocal received_initialized
async with ServerSession(
client_to_server_receive,
server_to_client_send,
InitializationOptions(
server_name="mcp",
server_version="0.1.0",
capabilities=ServerCapabilities(),
),
) as server_session:
async for message in server_session.incoming_messages: # pragma: no branch
if isinstance(message, Exception): # pragma: no cover
raise message
if isinstance(message, types.ClientNotification) and isinstance(
message.root, InitializedNotification
): # pragma: no branch
received_initialized = True
return
async def mock_client():
nonlocal received_protocol_version
# Send initialization request with older protocol version (2024-11-05)
await client_to_server_send.send(
SessionMessage(
types.JSONRPCMessage(
types.JSONRPCRequest(
jsonrpc="2.0",
id=1,
method="initialize",
params=types.InitializeRequestParams(
protocolVersion="2024-11-05",
capabilities=types.ClientCapabilities(),
clientInfo=types.Implementation(name="test-client", version="1.0.0"),
).model_dump(by_alias=True, mode="json", exclude_none=True),
)
)
)
)
# Wait for the initialize response
init_response_message = await server_to_client_receive.receive()
assert isinstance(init_response_message.message.root, types.JSONRPCResponse)
result_data = init_response_message.message.root.result
init_result = types.InitializeResult.model_validate(result_data)
# Check that the server responded with the requested protocol version
received_protocol_version = init_result.protocolVersion
assert received_protocol_version == "2024-11-05"
# Send initialized notification
await client_to_server_send.send(
SessionMessage(
types.JSONRPCMessage(
types.JSONRPCNotification(
jsonrpc="2.0",
method="notifications/initialized",
)
)
)
)
async with (
client_to_server_send,
client_to_server_receive,
server_to_client_send,
server_to_client_receive,
anyio.create_task_group() as tg,
):
tg.start_soon(run_server)
tg.start_soon(mock_client)
assert received_initialized
assert received_protocol_version == "2024-11-05"
@pytest.mark.anyio
async def test_ping_request_before_initialization():
"""Test that ping requests are allowed before initialization is complete."""
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)
ping_response_received = False
ping_response_id = None
async def run_server():
async with ServerSession(
client_to_server_receive,
server_to_client_send,
InitializationOptions(
server_name="mcp",
server_version="0.1.0",
capabilities=ServerCapabilities(),
),
) as server_session:
async for message in server_session.incoming_messages: # pragma: no branch
if isinstance(message, Exception): # pragma: no cover
raise message
# We should receive a ping request before initialization
if isinstance(message, RequestResponder) and isinstance(
message.request.root, types.PingRequest
): # pragma: no branch
# Respond to the ping
with message:
await message.respond(types.ServerResult(types.EmptyResult()))
return
async def mock_client():
nonlocal ping_response_received, ping_response_id
# Send ping request before any initialization
await client_to_server_send.send(
SessionMessage(
types.JSONRPCMessage(
types.JSONRPCRequest(
jsonrpc="2.0",
id=42,
method="ping",
)
)
)
)
# Wait for the ping response
ping_response_message = await server_to_client_receive.receive()
assert isinstance(ping_response_message.message.root, types.JSONRPCResponse)
ping_response_received = True
ping_response_id = ping_response_message.message.root.id
async with (
client_to_server_send,
client_to_server_receive,
server_to_client_send,
server_to_client_receive,
anyio.create_task_group() as tg,
):
tg.start_soon(run_server)
tg.start_soon(mock_client)
assert ping_response_received
assert ping_response_id == 42
@pytest.mark.anyio
async def test_other_requests_blocked_before_initialization():
"""Test that non-ping requests are still blocked before initialization."""
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)
error_response_received = False
error_code = None
async def run_server():
async with ServerSession(
client_to_server_receive,
server_to_client_send,
InitializationOptions(
server_name="mcp",
server_version="0.1.0",
capabilities=ServerCapabilities(),
),
):
# Server should handle the request and send an error response
# No need to process incoming_messages since the error is handled automatically
await anyio.sleep(0.1) # Give time for the request to be processed
async def mock_client():
nonlocal error_response_received, error_code
# Try to send a non-ping request before initialization
await client_to_server_send.send(
SessionMessage(
types.JSONRPCMessage(
types.JSONRPCRequest(
jsonrpc="2.0",
id=1,
method="prompts/list",
)
)
)
)
# Wait for the error response
error_message = await server_to_client_receive.receive()
if isinstance(error_message.message.root, types.JSONRPCError): # pragma: no branch
error_response_received = True
error_code = error_message.message.root.error.code
async with (
client_to_server_send,
client_to_server_receive,
server_to_client_send,
server_to_client_receive,
anyio.create_task_group() as tg,
):
tg.start_soon(run_server)
tg.start_soon(mock_client)
assert error_response_received
assert error_code == types.INVALID_PARAMS