Add pyright strict mode on the whole project (#1254)
This commit is contained in:
parent
ef4e167c1a
commit
c7671e470c
6 changed files with 652 additions and 674 deletions
|
|
@ -4,13 +4,8 @@ from typing_extensions import AsyncGenerator
|
|||
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.server import Server
|
||||
from mcp.shared.memory import (
|
||||
create_connected_server_and_client_session,
|
||||
)
|
||||
from mcp.types import (
|
||||
EmptyResult,
|
||||
Resource,
|
||||
)
|
||||
from mcp.shared.memory import create_connected_server_and_client_session
|
||||
from mcp.types import EmptyResult, Resource
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
|
|
@ -11,11 +11,7 @@ from mcp.server.models import InitializationOptions
|
|||
from mcp.server.session import ServerSession
|
||||
from mcp.shared.context import RequestContext
|
||||
from mcp.shared.progress import progress
|
||||
from mcp.shared.session import (
|
||||
BaseSession,
|
||||
RequestResponder,
|
||||
SessionMessage,
|
||||
)
|
||||
from mcp.shared.session import BaseSession, RequestResponder, SessionMessage
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
@ -47,8 +43,8 @@ async def test_bidirectional_progress_notifications():
|
|||
raise e
|
||||
|
||||
# Track progress updates
|
||||
server_progress_updates = []
|
||||
client_progress_updates = []
|
||||
server_progress_updates: list[dict[str, Any]] = []
|
||||
client_progress_updates: list[dict[str, Any]] = []
|
||||
|
||||
# Progress tokens
|
||||
server_progress_token = "server_token_123"
|
||||
|
|
@ -87,7 +83,7 @@ async def test_bidirectional_progress_notifications():
|
|||
|
||||
# Register tool handler
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(name: str, arguments: dict | None) -> list:
|
||||
async def handle_call_tool(name: str, arguments: dict[str, Any] | None) -> list[types.TextContent]:
|
||||
# Make sure we received a progress token
|
||||
if name == "test_tool":
|
||||
if arguments and "_meta" in arguments:
|
||||
|
|
@ -124,7 +120,7 @@ async def test_bidirectional_progress_notifications():
|
|||
else:
|
||||
raise ValueError("Progress token not sent.")
|
||||
|
||||
return ["Tool executed successfully"]
|
||||
return [types.TextContent(type="text", text="Tool executed successfully")]
|
||||
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
|
|
@ -217,7 +213,7 @@ async def test_progress_context_manager():
|
|||
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](5)
|
||||
|
||||
# Track progress updates
|
||||
server_progress_updates = []
|
||||
server_progress_updates: list[dict[str, Any]] = []
|
||||
|
||||
server = Server(name="ProgressContextTestServer")
|
||||
|
||||
|
|
@ -230,12 +226,7 @@ async def test_progress_context_manager():
|
|||
message: str | None,
|
||||
):
|
||||
server_progress_updates.append(
|
||||
{
|
||||
"token": progress_token,
|
||||
"progress": progress,
|
||||
"total": total,
|
||||
"message": message,
|
||||
}
|
||||
{"token": progress_token, "progress": progress, "total": total, "message": message}
|
||||
)
|
||||
|
||||
# Run server session to receive progress updates
|
||||
|
|
@ -288,13 +279,7 @@ async def test_progress_context_manager():
|
|||
)
|
||||
|
||||
# cast for type checker
|
||||
typed_context = cast(
|
||||
RequestContext[
|
||||
BaseSession[Any, Any, Any, Any, Any],
|
||||
Any,
|
||||
],
|
||||
request_context,
|
||||
)
|
||||
typed_context = cast(RequestContext[BaseSession[Any, Any, Any, Any, Any], Any], request_context)
|
||||
|
||||
# Utilize progress context manager
|
||||
with progress(typed_context, total=100) as p:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
|
|
@ -7,16 +8,14 @@ import mcp.types as types
|
|||
from mcp.client.session import ClientSession
|
||||
from mcp.server.lowlevel.server import Server
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.memory import (
|
||||
create_client_server_memory_streams,
|
||||
create_connected_server_and_client_session,
|
||||
)
|
||||
from mcp.shared.memory import create_client_server_memory_streams, create_connected_server_and_client_session
|
||||
from mcp.types import (
|
||||
CancelledNotification,
|
||||
CancelledNotificationParams,
|
||||
ClientNotification,
|
||||
ClientRequest,
|
||||
EmptyResult,
|
||||
TextContent,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -61,7 +60,7 @@ async def test_request_cancellation():
|
|||
|
||||
# Register the tool handler
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(name: str, arguments: dict | None) -> list:
|
||||
async def handle_call_tool(name: str, arguments: dict[str, Any] | None) -> list[TextContent]:
|
||||
nonlocal request_id, ev_tool_called
|
||||
if name == "slow_tool":
|
||||
request_id = server.request_context.request_id
|
||||
|
|
@ -83,7 +82,7 @@ async def test_request_cancellation():
|
|||
|
||||
return server
|
||||
|
||||
async def make_request(client_session):
|
||||
async def make_request(client_session: ClientSession):
|
||||
nonlocal ev_cancelled
|
||||
try:
|
||||
await client_session.send_request(
|
||||
|
|
@ -134,14 +133,11 @@ async def test_connection_closed():
|
|||
ev_closed = anyio.Event()
|
||||
ev_response = anyio.Event()
|
||||
|
||||
async with create_client_server_memory_streams() as (
|
||||
client_streams,
|
||||
server_streams,
|
||||
):
|
||||
async with create_client_server_memory_streams() as (client_streams, server_streams):
|
||||
client_read, client_write = client_streams
|
||||
server_read, server_write = server_streams
|
||||
|
||||
async def make_request(client_session):
|
||||
async def make_request(client_session: ClientSession):
|
||||
"""Send a request in a separate task"""
|
||||
nonlocal ev_response
|
||||
try:
|
||||
|
|
@ -165,10 +161,7 @@ async def test_connection_closed():
|
|||
|
||||
async with (
|
||||
anyio.create_task_group() as tg,
|
||||
ClientSession(
|
||||
read_stream=client_read,
|
||||
write_stream=client_write,
|
||||
) as client_session,
|
||||
ClientSession(read_stream=client_read, write_stream=client_write) as client_session,
|
||||
):
|
||||
tg.start_soon(make_request, client_session)
|
||||
tg.start_soon(mock_server)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import multiprocessing
|
|||
import socket
|
||||
import time
|
||||
from collections.abc import AsyncGenerator, Generator
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
import httpx
|
||||
|
|
@ -74,7 +75,7 @@ class ServerTest(Server):
|
|||
]
|
||||
|
||||
@self.call_tool()
|
||||
async def handle_call_tool(name: str, args: dict) -> list[TextContent]:
|
||||
async def handle_call_tool(name: str, args: dict[str, Any]) -> list[TextContent]:
|
||||
return [TextContent(type="text", text=f"Called {name}")]
|
||||
|
||||
|
||||
|
|
@ -147,7 +148,7 @@ def server(server_port: int) -> Generator[None, None, None]:
|
|||
|
||||
|
||||
@pytest.fixture()
|
||||
async def http_client(server, server_url) -> AsyncGenerator[httpx.AsyncClient, None]:
|
||||
async def http_client(server: None, server_url: str) -> AsyncGenerator[httpx.AsyncClient, None]:
|
||||
"""Create test client"""
|
||||
async with httpx.AsyncClient(base_url=server_url) as client:
|
||||
yield client
|
||||
|
|
@ -194,7 +195,7 @@ async def test_sse_client_basic_connection(server: None, server_url: str) -> Non
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
async def initialized_sse_client_session(server, server_url: str) -> AsyncGenerator[ClientSession, None]:
|
||||
async def initialized_sse_client_session(server: None, server_url: str) -> AsyncGenerator[ClientSession, None]:
|
||||
async with sse_client(server_url + "/sse", sse_read_timeout=0.5) as streams:
|
||||
async with ClientSession(*streams) as session:
|
||||
await session.initialize()
|
||||
|
|
@ -305,7 +306,7 @@ class RequestContextServer(Server[object, Request]):
|
|||
super().__init__("request_context_server")
|
||||
|
||||
@self.call_tool()
|
||||
async def handle_call_tool(name: str, args: dict) -> list[TextContent]:
|
||||
async def handle_call_tool(name: str, args: dict[str, Any]) -> list[TextContent]:
|
||||
headers_info = {}
|
||||
context = self.request_context
|
||||
if context.request:
|
||||
|
|
@ -435,7 +436,7 @@ async def test_request_context_propagation(context_server: None, server_url: str
|
|||
@pytest.mark.anyio
|
||||
async def test_request_context_isolation(context_server: None, server_url: str) -> None:
|
||||
"""Test that request contexts are isolated between different SSE clients."""
|
||||
contexts = []
|
||||
contexts: list[dict[str, Any]] = []
|
||||
|
||||
# Create multiple clients with different headers
|
||||
for i in range(3):
|
||||
|
|
@ -501,7 +502,7 @@ def test_sse_message_id_coercion():
|
|||
)
|
||||
def test_sse_server_transport_endpoint_validation(endpoint: str, expected_result: str | type[Exception]):
|
||||
"""Test that SseServerTransport properly validates and normalizes endpoints."""
|
||||
if isinstance(expected_result, type) and issubclass(expected_result, Exception):
|
||||
if isinstance(expected_result, type):
|
||||
# Test invalid endpoints that should raise an exception
|
||||
with pytest.raises(expected_result, match="is not a relative path.*expecting a relative path"):
|
||||
SseServerTransport(endpoint)
|
||||
|
|
|
|||
|
|
@ -40,16 +40,9 @@ from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|||
from mcp.server.transport_security import TransportSecuritySettings
|
||||
from mcp.shared.context import RequestContext
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.shared.message import (
|
||||
ClientMessageMetadata,
|
||||
)
|
||||
from mcp.shared.message import ClientMessageMetadata
|
||||
from mcp.shared.session import RequestResponder
|
||||
from mcp.types import (
|
||||
InitializeResult,
|
||||
TextContent,
|
||||
TextResourceContents,
|
||||
Tool,
|
||||
)
|
||||
from mcp.types import InitializeResult, TextContent, TextResourceContents, Tool
|
||||
|
||||
# Test constants
|
||||
SERVER_NAME = "test_streamable_http_server"
|
||||
|
|
@ -173,7 +166,7 @@ class ServerTest(Server):
|
|||
]
|
||||
|
||||
@self.call_tool()
|
||||
async def handle_call_tool(name: str, args: dict) -> list[TextContent]:
|
||||
async def handle_call_tool(name: str, args: dict[str, Any]) -> list[TextContent]:
|
||||
ctx = self.request_context
|
||||
|
||||
# When the tool is called, send a notification to test GET stream
|
||||
|
|
@ -261,7 +254,7 @@ class ServerTest(Server):
|
|||
return [TextContent(type="text", text=f"Called {name}")]
|
||||
|
||||
|
||||
def create_app(is_json_response_enabled=False, event_store: EventStore | None = None) -> Starlette:
|
||||
def create_app(is_json_response_enabled: bool = False, event_store: EventStore | None = None) -> Starlette:
|
||||
"""Create a Starlette application for testing using the session manager.
|
||||
|
||||
Args:
|
||||
|
|
@ -294,7 +287,7 @@ def create_app(is_json_response_enabled=False, event_store: EventStore | None =
|
|||
return app
|
||||
|
||||
|
||||
def run_server(port: int, is_json_response_enabled=False, event_store: EventStore | None = None) -> None:
|
||||
def run_server(port: int, is_json_response_enabled: bool = False, event_store: EventStore | None = None) -> None:
|
||||
"""Run the test server.
|
||||
|
||||
Args:
|
||||
|
|
@ -462,7 +455,7 @@ def json_server_url(json_server_port: int) -> str:
|
|||
|
||||
|
||||
# Basic request validation tests
|
||||
def test_accept_header_validation(basic_server, basic_server_url):
|
||||
def test_accept_header_validation(basic_server: None, basic_server_url: str):
|
||||
"""Test that Accept header is properly validated."""
|
||||
# Test without Accept header
|
||||
response = requests.post(
|
||||
|
|
@ -474,7 +467,7 @@ def test_accept_header_validation(basic_server, basic_server_url):
|
|||
assert "Not Acceptable" in response.text
|
||||
|
||||
|
||||
def test_content_type_validation(basic_server, basic_server_url):
|
||||
def test_content_type_validation(basic_server: None, basic_server_url: str):
|
||||
"""Test that Content-Type header is properly validated."""
|
||||
# Test with incorrect Content-Type
|
||||
response = requests.post(
|
||||
|
|
@ -490,7 +483,7 @@ def test_content_type_validation(basic_server, basic_server_url):
|
|||
assert "Invalid Content-Type" in response.text
|
||||
|
||||
|
||||
def test_json_validation(basic_server, basic_server_url):
|
||||
def test_json_validation(basic_server: None, basic_server_url: str):
|
||||
"""Test that JSON content is properly validated."""
|
||||
# Test with invalid JSON
|
||||
response = requests.post(
|
||||
|
|
@ -505,7 +498,7 @@ def test_json_validation(basic_server, basic_server_url):
|
|||
assert "Parse error" in response.text
|
||||
|
||||
|
||||
def test_json_parsing(basic_server, basic_server_url):
|
||||
def test_json_parsing(basic_server: None, basic_server_url: str):
|
||||
"""Test that JSON content is properly parse."""
|
||||
# Test with valid JSON but invalid JSON-RPC
|
||||
response = requests.post(
|
||||
|
|
@ -520,7 +513,7 @@ def test_json_parsing(basic_server, basic_server_url):
|
|||
assert "Validation error" in response.text
|
||||
|
||||
|
||||
def test_method_not_allowed(basic_server, basic_server_url):
|
||||
def test_method_not_allowed(basic_server: None, basic_server_url: str):
|
||||
"""Test that unsupported HTTP methods are rejected."""
|
||||
# Test with unsupported method (PUT)
|
||||
response = requests.put(
|
||||
|
|
@ -535,7 +528,7 @@ def test_method_not_allowed(basic_server, basic_server_url):
|
|||
assert "Method Not Allowed" in response.text
|
||||
|
||||
|
||||
def test_session_validation(basic_server, basic_server_url):
|
||||
def test_session_validation(basic_server: None, basic_server_url: str):
|
||||
"""Test session ID validation."""
|
||||
# session_id not used directly in this test
|
||||
|
||||
|
|
@ -610,7 +603,7 @@ def test_streamable_http_transport_init_validation():
|
|||
StreamableHTTPServerTransport(mcp_session_id="test\n")
|
||||
|
||||
|
||||
def test_session_termination(basic_server, basic_server_url):
|
||||
def test_session_termination(basic_server: None, basic_server_url: str):
|
||||
"""Test session termination via DELETE and subsequent request handling."""
|
||||
response = requests.post(
|
||||
f"{basic_server_url}/mcp",
|
||||
|
|
@ -650,7 +643,7 @@ def test_session_termination(basic_server, basic_server_url):
|
|||
assert "Session has been terminated" in response.text
|
||||
|
||||
|
||||
def test_response(basic_server, basic_server_url):
|
||||
def test_response(basic_server: None, basic_server_url: str):
|
||||
"""Test response handling for a valid request."""
|
||||
mcp_url = f"{basic_server_url}/mcp"
|
||||
response = requests.post(
|
||||
|
|
@ -685,7 +678,7 @@ def test_response(basic_server, basic_server_url):
|
|||
assert tools_response.headers.get("Content-Type") == "text/event-stream"
|
||||
|
||||
|
||||
def test_json_response(json_response_server, json_server_url):
|
||||
def test_json_response(json_response_server: None, json_server_url: str):
|
||||
"""Test response handling when is_json_response_enabled is True."""
|
||||
mcp_url = f"{json_server_url}/mcp"
|
||||
response = requests.post(
|
||||
|
|
@ -700,7 +693,7 @@ def test_json_response(json_response_server, json_server_url):
|
|||
assert response.headers.get("Content-Type") == "application/json"
|
||||
|
||||
|
||||
def test_get_sse_stream(basic_server, basic_server_url):
|
||||
def test_get_sse_stream(basic_server: None, basic_server_url: str):
|
||||
"""Test establishing an SSE stream via GET request."""
|
||||
# First, we need to initialize a session
|
||||
mcp_url = f"{basic_server_url}/mcp"
|
||||
|
|
@ -760,7 +753,7 @@ def test_get_sse_stream(basic_server, basic_server_url):
|
|||
assert second_get.status_code == 409
|
||||
|
||||
|
||||
def test_get_validation(basic_server, basic_server_url):
|
||||
def test_get_validation(basic_server: None, basic_server_url: str):
|
||||
"""Test validation for GET requests."""
|
||||
# First, we need to initialize a session
|
||||
mcp_url = f"{basic_server_url}/mcp"
|
||||
|
|
@ -815,14 +808,14 @@ def test_get_validation(basic_server, basic_server_url):
|
|||
|
||||
# Client-specific fixtures
|
||||
@pytest.fixture
|
||||
async def http_client(basic_server, basic_server_url):
|
||||
async def http_client(basic_server: None, basic_server_url: str):
|
||||
"""Create test client matching the SSE test pattern."""
|
||||
async with httpx.AsyncClient(base_url=basic_server_url) as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def initialized_client_session(basic_server, basic_server_url):
|
||||
async def initialized_client_session(basic_server: None, basic_server_url: str):
|
||||
"""Create initialized StreamableHTTP client session."""
|
||||
async with streamablehttp_client(f"{basic_server_url}/mcp") as (
|
||||
read_stream,
|
||||
|
|
@ -838,7 +831,7 @@ async def initialized_client_session(basic_server, basic_server_url):
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_streamablehttp_client_basic_connection(basic_server, basic_server_url):
|
||||
async def test_streamablehttp_client_basic_connection(basic_server: None, basic_server_url: str):
|
||||
"""Test basic client connection with initialization."""
|
||||
async with streamablehttp_client(f"{basic_server_url}/mcp") as (
|
||||
read_stream,
|
||||
|
|
@ -856,16 +849,17 @@ async def test_streamablehttp_client_basic_connection(basic_server, basic_server
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_streamablehttp_client_resource_read(initialized_client_session):
|
||||
async def test_streamablehttp_client_resource_read(initialized_client_session: ClientSession):
|
||||
"""Test client resource read functionality."""
|
||||
response = await initialized_client_session.read_resource(uri=AnyUrl("foobar://test-resource"))
|
||||
assert len(response.contents) == 1
|
||||
assert response.contents[0].uri == AnyUrl("foobar://test-resource")
|
||||
assert isinstance(response.contents[0], TextResourceContents)
|
||||
assert response.contents[0].text == "Read test-resource"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_streamablehttp_client_tool_invocation(initialized_client_session):
|
||||
async def test_streamablehttp_client_tool_invocation(initialized_client_session: ClientSession):
|
||||
"""Test client tool invocation."""
|
||||
# First list tools
|
||||
tools = await initialized_client_session.list_tools()
|
||||
|
|
@ -880,7 +874,7 @@ async def test_streamablehttp_client_tool_invocation(initialized_client_session)
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_streamablehttp_client_error_handling(initialized_client_session):
|
||||
async def test_streamablehttp_client_error_handling(initialized_client_session: ClientSession):
|
||||
"""Test error handling in client."""
|
||||
with pytest.raises(McpError) as exc_info:
|
||||
await initialized_client_session.read_resource(uri=AnyUrl("unknown://test-error"))
|
||||
|
|
@ -889,7 +883,7 @@ async def test_streamablehttp_client_error_handling(initialized_client_session):
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_streamablehttp_client_session_persistence(basic_server, basic_server_url):
|
||||
async def test_streamablehttp_client_session_persistence(basic_server: None, basic_server_url: str):
|
||||
"""Test that session ID persists across requests."""
|
||||
async with streamablehttp_client(f"{basic_server_url}/mcp") as (
|
||||
read_stream,
|
||||
|
|
@ -917,7 +911,7 @@ async def test_streamablehttp_client_session_persistence(basic_server, basic_ser
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_streamablehttp_client_json_response(json_response_server, json_server_url):
|
||||
async def test_streamablehttp_client_json_response(json_response_server: None, json_server_url: str):
|
||||
"""Test client with JSON response mode."""
|
||||
async with streamablehttp_client(f"{json_server_url}/mcp") as (
|
||||
read_stream,
|
||||
|
|
@ -945,12 +939,12 @@ async def test_streamablehttp_client_json_response(json_response_server, json_se
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_streamablehttp_client_get_stream(basic_server, basic_server_url):
|
||||
async def test_streamablehttp_client_get_stream(basic_server: None, basic_server_url: str):
|
||||
"""Test GET stream functionality for server-initiated messages."""
|
||||
import mcp.types as types
|
||||
from mcp.shared.session import RequestResponder
|
||||
|
||||
notifications_received = []
|
||||
notifications_received: list[types.ServerNotification] = []
|
||||
|
||||
# Define message handler to capture notifications
|
||||
async def message_handler(
|
||||
|
|
@ -986,7 +980,7 @@ async def test_streamablehttp_client_get_stream(basic_server, basic_server_url):
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_streamablehttp_client_session_termination(basic_server, basic_server_url):
|
||||
async def test_streamablehttp_client_session_termination(basic_server: None, basic_server_url: str):
|
||||
"""Test client session termination functionality."""
|
||||
|
||||
captured_session_id = None
|
||||
|
|
@ -1008,7 +1002,7 @@ async def test_streamablehttp_client_session_termination(basic_server, basic_ser
|
|||
tools = await session.list_tools()
|
||||
assert len(tools.tools) == 6
|
||||
|
||||
headers = {}
|
||||
headers: dict[str, str] = {}
|
||||
if captured_session_id:
|
||||
headers[MCP_SESSION_ID_HEADER] = captured_session_id
|
||||
|
||||
|
|
@ -1027,7 +1021,9 @@ async def test_streamablehttp_client_session_termination(basic_server, basic_ser
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_streamablehttp_client_session_termination_204(basic_server, basic_server_url, monkeypatch):
|
||||
async def test_streamablehttp_client_session_termination_204(
|
||||
basic_server: None, basic_server_url: str, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
"""Test client session termination functionality with a 204 response.
|
||||
|
||||
This test patches the httpx client to return a 204 response for DELETEs.
|
||||
|
|
@ -1037,7 +1033,7 @@ async def test_streamablehttp_client_session_termination_204(basic_server, basic
|
|||
original_delete = httpx.AsyncClient.delete
|
||||
|
||||
# Mock the client's delete method to return a 204
|
||||
async def mock_delete(self, *args, **kwargs):
|
||||
async def mock_delete(self: httpx.AsyncClient, *args: Any, **kwargs: Any) -> httpx.Response:
|
||||
# Call the original method to get the real response
|
||||
response = await original_delete(self, *args, **kwargs)
|
||||
|
||||
|
|
@ -1072,7 +1068,7 @@ async def test_streamablehttp_client_session_termination_204(basic_server, basic
|
|||
tools = await session.list_tools()
|
||||
assert len(tools.tools) == 6
|
||||
|
||||
headers = {}
|
||||
headers: dict[str, str] = {}
|
||||
if captured_session_id:
|
||||
headers[MCP_SESSION_ID_HEADER] = captured_session_id
|
||||
|
||||
|
|
@ -1091,14 +1087,14 @@ async def test_streamablehttp_client_session_termination_204(basic_server, basic
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_streamablehttp_client_resumption(event_server):
|
||||
async def test_streamablehttp_client_resumption(event_server: tuple[SimpleEventStore, str]):
|
||||
"""Test client session resumption using sync primitives for reliable coordination."""
|
||||
_, server_url = event_server
|
||||
|
||||
# Variables to track the state
|
||||
captured_session_id = None
|
||||
captured_resumption_token = None
|
||||
captured_notifications = []
|
||||
captured_notifications: list[types.ServerNotification] = []
|
||||
captured_protocol_version = None
|
||||
first_notification_received = False
|
||||
|
||||
|
|
@ -1170,7 +1166,7 @@ async def test_streamablehttp_client_resumption(event_server):
|
|||
captured_notifications = []
|
||||
|
||||
# Now resume the session with the same mcp-session-id and protocol version
|
||||
headers = {}
|
||||
headers: dict[str, Any] = {}
|
||||
if captured_session_id:
|
||||
headers[MCP_SESSION_ID_HEADER] = captured_session_id
|
||||
if captured_protocol_version:
|
||||
|
|
@ -1211,11 +1207,12 @@ async def test_streamablehttp_client_resumption(event_server):
|
|||
# We should have received the remaining notifications
|
||||
assert len(captured_notifications) == 1
|
||||
|
||||
assert isinstance(captured_notifications[0].root, types.LoggingMessageNotification)
|
||||
assert captured_notifications[0].root.params.data == "Second notification after lock"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_streamablehttp_server_sampling(basic_server, basic_server_url):
|
||||
async def test_streamablehttp_server_sampling(basic_server: None, basic_server_url: str):
|
||||
"""Test server-initiated sampling request through streamable HTTP transport."""
|
||||
# Variable to track if sampling callback was invoked
|
||||
sampling_callback_invoked = False
|
||||
|
|
@ -1298,7 +1295,7 @@ class ContextAwareServerTest(Server):
|
|||
]
|
||||
|
||||
@self.call_tool()
|
||||
async def handle_call_tool(name: str, args: dict) -> list[TextContent]:
|
||||
async def handle_call_tool(name: str, args: dict[str, Any]) -> list[TextContent]:
|
||||
ctx = self.request_context
|
||||
|
||||
if name == "echo_headers":
|
||||
|
|
@ -1306,16 +1303,11 @@ class ContextAwareServerTest(Server):
|
|||
headers_info = {}
|
||||
if ctx.request and isinstance(ctx.request, Request):
|
||||
headers_info = dict(ctx.request.headers)
|
||||
return [
|
||||
TextContent(
|
||||
type="text",
|
||||
text=json.dumps(headers_info),
|
||||
)
|
||||
]
|
||||
return [TextContent(type="text", text=json.dumps(headers_info))]
|
||||
|
||||
elif name == "echo_context":
|
||||
# Return full context information
|
||||
context_data = {
|
||||
context_data: dict[str, Any] = {
|
||||
"request_id": args.get("request_id"),
|
||||
"headers": {},
|
||||
"method": None,
|
||||
|
|
@ -1430,7 +1422,7 @@ async def test_streamablehttp_request_context_propagation(context_aware_server:
|
|||
@pytest.mark.anyio
|
||||
async def test_streamablehttp_request_context_isolation(context_aware_server: None, basic_server_url: str) -> None:
|
||||
"""Test that request contexts are isolated between StreamableHTTP clients."""
|
||||
contexts = []
|
||||
contexts: list[dict[str, Any]] = []
|
||||
|
||||
# Create multiple clients with different headers
|
||||
for i in range(3):
|
||||
|
|
@ -1462,7 +1454,7 @@ async def test_streamablehttp_request_context_isolation(context_aware_server: No
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_client_includes_protocol_version_header_after_init(context_aware_server, basic_server_url):
|
||||
async def test_client_includes_protocol_version_header_after_init(context_aware_server: None, basic_server_url: str):
|
||||
"""Test that client includes mcp-protocol-version header after initialization."""
|
||||
async with streamablehttp_client(f"{basic_server_url}/mcp") as (
|
||||
read_stream,
|
||||
|
|
@ -1486,7 +1478,7 @@ async def test_client_includes_protocol_version_header_after_init(context_aware_
|
|||
assert headers_data[MCP_PROTOCOL_VERSION_HEADER] == negotiated_version
|
||||
|
||||
|
||||
def test_server_validates_protocol_version_header(basic_server, basic_server_url):
|
||||
def test_server_validates_protocol_version_header(basic_server: None, basic_server_url: str):
|
||||
"""Test that server returns 400 Bad Request version if header unsupported or invalid."""
|
||||
# First initialize a session to get a valid session ID
|
||||
init_response = requests.post(
|
||||
|
|
@ -1544,7 +1536,7 @@ def test_server_validates_protocol_version_header(basic_server, basic_server_url
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_server_backwards_compatibility_no_protocol_version(basic_server, basic_server_url):
|
||||
def test_server_backwards_compatibility_no_protocol_version(basic_server: None, basic_server_url: str):
|
||||
"""Test server accepts requests without protocol version header."""
|
||||
# First initialize a session to get a valid session ID
|
||||
init_response = requests.post(
|
||||
|
|
@ -1574,7 +1566,7 @@ def test_server_backwards_compatibility_no_protocol_version(basic_server, basic_
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_client_crash_handled(basic_server, basic_server_url):
|
||||
async def test_client_crash_handled(basic_server: None, basic_server_url: str):
|
||||
"""Test that cases where the client crashes are handled gracefully."""
|
||||
|
||||
# Simulate bad client that crashes after init
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import multiprocessing
|
|||
import socket
|
||||
import time
|
||||
from collections.abc import AsyncGenerator, Generator
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
|
|
@ -9,6 +10,7 @@ import uvicorn
|
|||
from pydantic import AnyUrl
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import WebSocketRoute
|
||||
from starlette.websockets import WebSocket
|
||||
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.client.websocket import websocket_client
|
||||
|
|
@ -67,7 +69,7 @@ class ServerTest(Server):
|
|||
]
|
||||
|
||||
@self.call_tool()
|
||||
async def handle_call_tool(name: str, args: dict) -> list[TextContent]:
|
||||
async def handle_call_tool(name: str, args: dict[str, Any]) -> list[TextContent]:
|
||||
return [TextContent(type="text", text=f"Called {name}")]
|
||||
|
||||
|
||||
|
|
@ -76,7 +78,7 @@ def make_server_app() -> Starlette:
|
|||
"""Create test Starlette app with WebSocket transport"""
|
||||
server = ServerTest()
|
||||
|
||||
async def handle_ws(websocket):
|
||||
async def handle_ws(websocket: WebSocket):
|
||||
async with websocket_server(websocket.scope, websocket.receive, websocket.send) as streams:
|
||||
await server.run(streams[0], streams[1], server.create_initialization_options())
|
||||
|
||||
|
|
@ -133,7 +135,7 @@ def server(server_port: int) -> Generator[None, None, None]:
|
|||
|
||||
|
||||
@pytest.fixture()
|
||||
async def initialized_ws_client_session(server, server_url: str) -> AsyncGenerator[ClientSession, None]:
|
||||
async def initialized_ws_client_session(server: None, server_url: str) -> AsyncGenerator[ClientSession, None]:
|
||||
"""Create and initialize a WebSocket client session"""
|
||||
async with websocket_client(server_url + "/ws") as streams:
|
||||
async with ClientSession(*streams) as session:
|
||||
|
|
|
|||
Loading…
Reference in a new issue