49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
91 lines
4.1 KiB
Python
91 lines
4.1 KiB
Python
"""Behaviour specific to the legacy HTTP+SSE transport, exercised entirely in process.
|
|
|
|
Transport-agnostic behaviour is covered by the `connect`-fixture matrix, which runs the rest of
|
|
the suite over this transport as well; this file pins only what is observable on the SSE wiring
|
|
itself: the GET-then-POST connection lifecycle, the endpoint event, and how the message endpoint
|
|
rejects requests it cannot route to a session. Every test drives the server's real Starlette app
|
|
through the suite's streaming ASGI bridge.
|
|
"""
|
|
|
|
from uuid import UUID, uuid4
|
|
|
|
import anyio
|
|
import httpx
|
|
import pytest
|
|
from inline_snapshot import snapshot
|
|
from mcp_types import EmptyResult
|
|
|
|
from mcp.client.client import Client
|
|
from mcp.client.sse import sse_client
|
|
from mcp.server import Server
|
|
from tests.interaction._connect import BASE_URL, build_sse_app
|
|
from tests.interaction._requirements import requirement
|
|
from tests.interaction.transports._bridge import StreamingASGITransport
|
|
|
|
pytestmark = pytest.mark.anyio
|
|
|
|
|
|
@requirement("transport:sse")
|
|
@requirement("transport:sse:endpoint-event")
|
|
async def test_endpoint_event_names_the_message_endpoint_with_a_fresh_session_id() -> None:
|
|
"""Connecting opens a GET stream whose first event names the POST endpoint and a fresh
|
|
session id; messages POSTed there are answered on that stream, and disconnecting releases the
|
|
server's session entry."""
|
|
app, sse = build_sse_app(Server("legacy"))
|
|
captured_session_id: list[str] = []
|
|
|
|
def httpx_client_factory(
|
|
headers: dict[str, str] | None = None,
|
|
timeout: httpx.Timeout | None = None,
|
|
auth: httpx.Auth | None = None,
|
|
) -> httpx.AsyncClient:
|
|
return httpx.AsyncClient(
|
|
transport=StreamingASGITransport(app, cancel_on_close=False),
|
|
base_url=BASE_URL,
|
|
headers=headers,
|
|
timeout=timeout,
|
|
auth=auth,
|
|
)
|
|
|
|
transport = sse_client(
|
|
f"{BASE_URL}/sse", httpx_client_factory=httpx_client_factory, on_session_created=captured_session_id.append
|
|
)
|
|
with anyio.fail_after(5):
|
|
async with Client(transport, mode="legacy") as client:
|
|
assert len(captured_session_id) == 1
|
|
assert UUID(hex=captured_session_id[0]) in sse._read_stream_writers
|
|
assert await client.send_ping() == snapshot(EmptyResult()) # pyright: ignore[reportDeprecated]
|
|
|
|
assert sse._read_stream_writers == {}
|
|
|
|
|
|
@requirement("transport:sse:post:session-routing")
|
|
async def test_post_without_a_session_id_is_rejected() -> None:
|
|
"""A POST to the message endpoint with no session_id query parameter is answered 400."""
|
|
app, _ = build_sse_app(Server("legacy"))
|
|
async with httpx.AsyncClient(transport=StreamingASGITransport(app), base_url=BASE_URL) as http:
|
|
response = await http.post("/messages/", json={"jsonrpc": "2.0", "method": "ping", "id": 1})
|
|
assert (response.status_code, response.text) == snapshot((400, "session_id is required"))
|
|
|
|
|
|
@requirement("transport:sse:post:session-routing")
|
|
async def test_post_with_a_malformed_session_id_is_rejected() -> None:
|
|
"""A POST whose session_id query parameter is not a UUID is answered 400."""
|
|
app, _ = build_sse_app(Server("legacy"))
|
|
async with httpx.AsyncClient(transport=StreamingASGITransport(app), base_url=BASE_URL) as http:
|
|
response = await http.post(
|
|
"/messages/", params={"session_id": "not-a-uuid"}, json={"jsonrpc": "2.0", "method": "ping", "id": 1}
|
|
)
|
|
assert (response.status_code, response.text) == snapshot((400, "Invalid session ID"))
|
|
|
|
|
|
@requirement("transport:sse:post:session-routing")
|
|
async def test_post_for_an_unknown_session_is_rejected() -> None:
|
|
"""A POST naming a well-formed session_id that no SSE stream owns is answered 404."""
|
|
app, _ = build_sse_app(Server("legacy"))
|
|
async with httpx.AsyncClient(transport=StreamingASGITransport(app), base_url=BASE_URL) as http:
|
|
response = await http.post(
|
|
"/messages/", params={"session_id": uuid4().hex}, json={"jsonrpc": "2.0", "method": "ping", "id": 1}
|
|
)
|
|
assert (response.status_code, response.text) == snapshot((404, "Could not find session"))
|