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
6 changed files with 652 additions and 674 deletions

View file

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

View file

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

View file

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

View file

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

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

View file

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