Remove the unsupported WebSocket transport (#2785)

This commit is contained in:
Max 2026-06-08 12:05:27 +01:00 • committed by GitHub
commit b478bff56d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 9 additions and 331 deletions

View file

@ -21,7 +21,6 @@ import pytest
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamable_http_client
from mcp.client.websocket import websocket_client
@contextmanager
@ -104,16 +103,3 @@ async def test_streamable_http_client_closes_all_streams_on_exit() -> None:
with _assert_no_memory_stream_leak():
async with streamable_http_client("http://127.0.0.1:1/mcp"):
pass
@pytest.mark.anyio
async def test_websocket_client_closes_all_streams_on_connection_error(free_tcp_port: int) -> None:
"""websocket_client must close all 4 stream ends when ws_connect fails.
Before the fix, there was no try/finally at all — if ws_connect raised,
all 4 streams were leaked.
"""
with _assert_no_memory_stream_leak():
with pytest.raises(OSError):
async with websocket_client(f"ws://127.0.0.1:{free_tcp_port}/ws"):
pytest.fail("should not reach here") # pragma: no cover

View file

@ -1,51 +0,0 @@
"""Smoke test for the WebSocket transport.
Runs the full WS stack end-to-end over a real TCP connection, covering both
``src/mcp/client/websocket.py`` and ``src/mcp/server/websocket.py``. MCP
semantics (error propagation, timeouts, etc.) are transport-agnostic and are
covered in ``tests/client/test_client.py`` and ``tests/issues/test_88_random_error.py``.
"""
from collections.abc import Generator
import pytest
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
from mcp.server import Server
from mcp.server.websocket import websocket_server
from mcp.types import EmptyResult, InitializeResult
from tests.test_helpers import run_uvicorn_in_thread
SERVER_NAME = "test_server_for_WS"
def make_server_app() -> Starlette:
srv = Server(SERVER_NAME)
async def handle_ws(websocket: WebSocket) -> None:
async with websocket_server(websocket.scope, websocket.receive, websocket.send) as streams:
await srv.run(streams[0], streams[1], srv.create_initialization_options())
return Starlette(routes=[WebSocketRoute("/ws", endpoint=handle_ws)])
@pytest.fixture
def ws_server_url() -> Generator[str, None, None]:
with run_uvicorn_in_thread(make_server_app()) as base_url:
yield base_url.replace("http://", "ws://") + "/ws"
@pytest.mark.anyio
async def test_ws_client_basic_connection(ws_server_url: str) -> None:
async with websocket_client(ws_server_url) as streams:
async with ClientSession(*streams) as session:
result = await session.initialize()
assert isinstance(result, InitializeResult)
assert result.server_info.name == SERVER_NAME
ping_result = await session.send_ping()
assert isinstance(ping_result, EmptyResult)

View file

@ -1,57 +0,0 @@
"""Common test utilities for MCP server tests."""
import socket
import threading
from collections.abc import Generator
from contextlib import contextmanager
from typing import Any
import uvicorn
_SERVER_SHUTDOWN_TIMEOUT_S = 5.0
@contextmanager
def run_uvicorn_in_thread(app: Any, **config_kwargs: Any) -> Generator[str, None, None]:
"""Run a uvicorn server in a background thread on an ephemeral port.
The socket is bound and put into listening state *before* the thread
starts, so the port is known immediately with no wait. The kernel's
listen queue buffers any connections that arrive before uvicorn's event
loop reaches ``accept()``, so callers can connect as soon as this
function yields — no polling, no sleeps, no startup race.
This also avoids the TOCTOU race of the old pick-a-port-then-rebind
pattern: the socket passed here is the one uvicorn serves on, with no
gap where another pytest-xdist worker could claim it.
Args:
app: ASGI application to serve.
**config_kwargs: Additional keyword arguments for :class:`uvicorn.Config`
(e.g. ``log_level``). ``host``/``port`` are ignored since the
socket is pre-bound.
Yields:
The base URL of the running server, e.g. ``http://127.0.0.1:54321``.
"""
host = "127.0.0.1"
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((host, 0))
sock.listen()
port = sock.getsockname()[1]
config_kwargs.setdefault("log_level", "error")
# Uvicorn's interface autodetection calls asyncio.iscoroutinefunction,
# which Python 3.14 deprecates. Under filterwarnings=error this crashes
# the server thread silently. Starlette is asgi3; skip the autodetect.
config_kwargs.setdefault("interface", "asgi3")
server = uvicorn.Server(config=uvicorn.Config(app=app, **config_kwargs))
thread = threading.Thread(target=server.run, kwargs={"sockets": [sock]}, daemon=True)
thread.start()
try:
yield f"http://{host}:{port}"
finally:
server.should_exit = True
thread.join(timeout=_SERVER_SHUTDOWN_TIMEOUT_S)