Add pyright strict mode on the whole project (#1254)

This commit is contained in:
Marcelo Trylesinski 2025-08-11 19:56:37 +02:00 • committed by GitHub
commit c7671e470c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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