Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/mcp/client/sse.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ async def sse_reader(
task_status.started(endpoint_url)

case "message":
# Skip empty data (keep-alive pings)
if not sse.data:
continue
try:
message = types.JSONRPCMessage.model_validate_json( # noqa: E501
sse.data
Expand Down
18 changes: 18 additions & 0 deletions tests/shared/test_sse.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,3 +532,21 @@ def test_sse_server_transport_endpoint_validation(endpoint: str, expected_result
sse = SseServerTransport(endpoint)
assert sse._endpoint == expected_result
assert sse._endpoint.startswith("/")


@pytest.mark.anyio
async def test_sse_client_handles_empty_keepalive_pings(server: None, server_url: str) -> None:
"""Test that SSE client properly handles empty data lines (keep-alive pings)."""
async with sse_client(server_url + "/sse") as streams:
async with ClientSession(*streams) as session:
# Initialize the session
result = await session.initialize()
assert isinstance(result, InitializeResult)
assert result.serverInfo.name == SERVER_NAME

# Test that we can still make requests after receiving keep-alive pings
# The server may send empty data lines between actual messages
response = await session.read_resource(uri=AnyUrl("foobar://test"))
assert len(response.contents) == 1
assert isinstance(response.contents[0], TextResourceContents)
assert response.contents[0].text == "Read test"
Loading