From d4c364e7d00fcd718374a8590e5f5e1eb6cb581c Mon Sep 17 00:00:00 2001 From: Kaushik Iyer <54633514+kaushik701@users.noreply.github.com> Date: Sun, 31 May 2026 18:02:27 -0700 Subject: [PATCH 1/4] test: add regression test for issue #2001 (progress related_request_id) Adds a dedicated regression test file for issue #2001. Context.report_progress() was silently dropping progress notifications in stateless HTTP / SSE transports because send_progress_notification() was called without related_request_id. The fix (already in main via mcpserver/context.py) passes related_request_id=self.request_id, consistent with send_log_message(). This test file covers: - The primary regression: related_request_id is forwarded on every call - Edge case: no progress_token -> notification is skipped (no-op) - Edge case: integer progress_token (e.g. 0) works correctly Closes #2001 --- .../test_2001_progress_related_request_id.py | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/issues/test_2001_progress_related_request_id.py diff --git a/tests/issues/test_2001_progress_related_request_id.py b/tests/issues/test_2001_progress_related_request_id.py new file mode 100644 index 0000000000..cd3e10ba28 --- /dev/null +++ b/tests/issues/test_2001_progress_related_request_id.py @@ -0,0 +1,117 @@ +"""Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/2001. + +Progress notifications not delivered via SSE in stateless HTTP mode. + +Root cause: Context.report_progress() was calling send_progress_notification() +without passing related_request_id. The SSE / streamable-HTTP transport uses +that field to route server-initiated messages back to the correct client stream; +without it notifications are silently dropped. + +Fix: pass related_request_id=self.request_id in send_progress_notification(), +consistent with how send_log_message() already works. +""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from mcp.server.context import ServerRequestContext +from mcp.server.experimental.request_context import Experimental +from mcp.server.mcpserver import Context + +pytestmark = pytest.mark.anyio + + +async def test_report_progress_passes_related_request_id() -> None: + """report_progress must forward request_id as related_request_id. + + Without related_request_id the streamable-HTTP transport cannot route + progress notifications to the correct SSE stream; they are silently + dropped. Regression test for issue #2001. + """ + mock_session = AsyncMock() + mock_session.send_progress_notification = AsyncMock() + + request_context = ServerRequestContext( + request_id="req-2001", + session=mock_session, + meta={"progress_token": "tok-progress"}, + lifespan_context=None, + experimental=Experimental(), + ) + + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + + await ctx.report_progress(25, 100, message="quarter done") + await ctx.report_progress(50, 100) + await ctx.report_progress(100, 100, message="complete") + + assert mock_session.send_progress_notification.call_count == 3 + + mock_session.send_progress_notification.assert_any_call( + progress_token="tok-progress", + progress=25.0, + total=100.0, + message="quarter done", + related_request_id="req-2001", + ) + mock_session.send_progress_notification.assert_any_call( + progress_token="tok-progress", + progress=50.0, + total=100.0, + message=None, + related_request_id="req-2001", + ) + mock_session.send_progress_notification.assert_any_call( + progress_token="tok-progress", + progress=100.0, + total=100.0, + message="complete", + related_request_id="req-2001", + ) + + +async def test_report_progress_no_token_skips_notification() -> None: + """report_progress is a no-op when no progress_token is present.""" + mock_session = AsyncMock() + mock_session.send_progress_notification = AsyncMock() + + request_context = ServerRequestContext( + request_id="req-no-token", + session=mock_session, + meta={}, + lifespan_context=None, + experimental=Experimental(), + ) + + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + + await ctx.report_progress(50, 100) + + mock_session.send_progress_notification.assert_not_called() + + +async def test_report_progress_integer_token() -> None: + """report_progress works when progress_token is an integer (e.g. 0).""" + mock_session = AsyncMock() + mock_session.send_progress_notification = AsyncMock() + + request_context = ServerRequestContext( + request_id="req-int-token", + session=mock_session, + meta={"progress_token": 0}, + lifespan_context=None, + experimental=Experimental(), + ) + + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + + await ctx.report_progress(1, 10) + + mock_session.send_progress_notification.assert_awaited_once_with( + progress_token=0, + progress=1.0, + total=10.0, + message=None, + related_request_id="req-int-token", + ) From 0b1d01a0d920ffe3ff53c4620a00f966763593c0 Mon Sep 17 00:00:00 2001 From: Kaushik Iyer <54633514+kaushik701@users.noreply.github.com> Date: Sun, 31 May 2026 18:24:35 -0700 Subject: [PATCH 2/4] fix: correct indentation and simplify docstrings in regression test --- .../test_2001_progress_related_request_id.py | 156 +++++++----------- 1 file changed, 63 insertions(+), 93 deletions(-) diff --git a/tests/issues/test_2001_progress_related_request_id.py b/tests/issues/test_2001_progress_related_request_id.py index cd3e10ba28..918b5714be 100644 --- a/tests/issues/test_2001_progress_related_request_id.py +++ b/tests/issues/test_2001_progress_related_request_id.py @@ -1,15 +1,6 @@ -"""Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/2001. - -Progress notifications not delivered via SSE in stateless HTTP mode. - -Root cause: Context.report_progress() was calling send_progress_notification() -without passing related_request_id. The SSE / streamable-HTTP transport uses -that field to route server-initiated messages back to the correct client stream; -without it notifications are silently dropped. - -Fix: pass related_request_id=self.request_id in send_progress_notification(), -consistent with how send_log_message() already works. -""" +# Regression test for issue #2001 - progress notifications via SSE in stateless HTTP +# Root cause: send_progress_notification() called without related_request_id. +# Fix: pass related_request_id=self.request_id - see mcpserver/context.py from unittest.mock import AsyncMock, MagicMock @@ -21,97 +12,76 @@ pytestmark = pytest.mark.anyio - async def test_report_progress_passes_related_request_id() -> None: - """report_progress must forward request_id as related_request_id. - - Without related_request_id the streamable-HTTP transport cannot route - progress notifications to the correct SSE stream; they are silently - dropped. Regression test for issue #2001. - """ + """report_progress must forward request_id as related_request_id.""" mock_session = AsyncMock() mock_session.send_progress_notification = AsyncMock() - - request_context = ServerRequestContext( - request_id="req-2001", - session=mock_session, - meta={"progress_token": "tok-progress"}, - lifespan_context=None, - experimental=Experimental(), - ) - - ctx = Context(request_context=request_context, mcp_server=MagicMock()) - - await ctx.report_progress(25, 100, message="quarter done") - await ctx.report_progress(50, 100) - await ctx.report_progress(100, 100, message="complete") - - assert mock_session.send_progress_notification.call_count == 3 - - mock_session.send_progress_notification.assert_any_call( - progress_token="tok-progress", - progress=25.0, - total=100.0, - message="quarter done", - related_request_id="req-2001", - ) - mock_session.send_progress_notification.assert_any_call( - progress_token="tok-progress", - progress=50.0, - total=100.0, - message=None, - related_request_id="req-2001", - ) - mock_session.send_progress_notification.assert_any_call( - progress_token="tok-progress", - progress=100.0, - total=100.0, - message="complete", - related_request_id="req-2001", - ) - + request_context = ServerRequestContext( + request_id="req-2001", + session=mock_session, + meta={"progress_token": "tok-progress"}, + lifespan_context=None, + experimental=Experimental(), + ) + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + await ctx.report_progress(25, 100, message="quarter done") + await ctx.report_progress(50, 100) + await ctx.report_progress(100, 100, message="complete") + assert mock_session.send_progress_notification.call_count == 3 + mock_session.send_progress_notification.assert_any_call( + progress_token="tok-progress", + progress=25.0, + total=100.0, + message="quarter done", + related_request_id="req-2001", + ) + mock_session.send_progress_notification.assert_any_call( + progress_token="tok-progress", + progress=50.0, + total=100.0, + message=None, + related_request_id="req-2001", + ) + mock_session.send_progress_notification.assert_any_call( + progress_token="tok-progress", + progress=100.0, + total=100.0, + message="complete", + related_request_id="req-2001", + ) async def test_report_progress_no_token_skips_notification() -> None: """report_progress is a no-op when no progress_token is present.""" mock_session = AsyncMock() mock_session.send_progress_notification = AsyncMock() - - request_context = ServerRequestContext( - request_id="req-no-token", - session=mock_session, - meta={}, - lifespan_context=None, - experimental=Experimental(), - ) - - ctx = Context(request_context=request_context, mcp_server=MagicMock()) - - await ctx.report_progress(50, 100) - - mock_session.send_progress_notification.assert_not_called() - + request_context = ServerRequestContext( + request_id="req-no-token", + session=mock_session, + meta={}, + lifespan_context=None, + experimental=Experimental(), + ) + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + await ctx.report_progress(50, 100) +mock_session.send_progress_notification.assert_not_called() async def test_report_progress_integer_token() -> None: """report_progress works when progress_token is an integer (e.g. 0).""" mock_session = AsyncMock() mock_session.send_progress_notification = AsyncMock() - - request_context = ServerRequestContext( - request_id="req-int-token", - session=mock_session, - meta={"progress_token": 0}, - lifespan_context=None, - experimental=Experimental(), - ) - - ctx = Context(request_context=request_context, mcp_server=MagicMock()) - - await ctx.report_progress(1, 10) - - mock_session.send_progress_notification.assert_awaited_once_with( - progress_token=0, - progress=1.0, - total=10.0, - message=None, - related_request_id="req-int-token", - ) + request_context = ServerRequestContext( + request_id="req-int-token", + session=mock_session, + meta={"progress_token": 0}, + lifespan_context=None, + experimental=Experimental(), + ) + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + await ctx.report_progress(1, 10) + mock_session.send_progress_notification.assert_awaited_once_with( + progress_token=0, + progress=1.0, + total=10.0, + message=None, + related_request_id="req-int-token", + ) From 490786581fa5961387f95f4c396e953a7c50fe24 Mon Sep 17 00:00:00 2001 From: Kaushik Iyer <54633514+kaushik701@users.noreply.github.com> Date: Sun, 31 May 2026 18:47:45 -0700 Subject: [PATCH 3/4] style: normalize indentation to 4 spaces for ruff-format compliance --- .../test_2001_progress_related_request_id.py | 134 +++++++++--------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/tests/issues/test_2001_progress_related_request_id.py b/tests/issues/test_2001_progress_related_request_id.py index 918b5714be..7279722905 100644 --- a/tests/issues/test_2001_progress_related_request_id.py +++ b/tests/issues/test_2001_progress_related_request_id.py @@ -13,75 +13,75 @@ pytestmark = pytest.mark.anyio async def test_report_progress_passes_related_request_id() -> None: - """report_progress must forward request_id as related_request_id.""" - mock_session = AsyncMock() - mock_session.send_progress_notification = AsyncMock() - request_context = ServerRequestContext( - request_id="req-2001", - session=mock_session, - meta={"progress_token": "tok-progress"}, - lifespan_context=None, - experimental=Experimental(), - ) - ctx = Context(request_context=request_context, mcp_server=MagicMock()) - await ctx.report_progress(25, 100, message="quarter done") - await ctx.report_progress(50, 100) - await ctx.report_progress(100, 100, message="complete") - assert mock_session.send_progress_notification.call_count == 3 - mock_session.send_progress_notification.assert_any_call( - progress_token="tok-progress", - progress=25.0, - total=100.0, - message="quarter done", - related_request_id="req-2001", - ) - mock_session.send_progress_notification.assert_any_call( - progress_token="tok-progress", - progress=50.0, - total=100.0, - message=None, - related_request_id="req-2001", - ) - mock_session.send_progress_notification.assert_any_call( - progress_token="tok-progress", - progress=100.0, - total=100.0, - message="complete", - related_request_id="req-2001", - ) + """report_progress must forward request_id as related_request_id.""" + mock_session = AsyncMock() + mock_session.send_progress_notification = AsyncMock() + request_context = ServerRequestContext( + request_id="req-2001", + session=mock_session, + meta={"progress_token": "tok-progress"}, + lifespan_context=None, + experimental=Experimental(), + ) + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + await ctx.report_progress(25, 100, message="quarter done") + await ctx.report_progress(50, 100) + await ctx.report_progress(100, 100, message="complete") + assert mock_session.send_progress_notification.call_count == 3 + mock_session.send_progress_notification.assert_any_call( + progress_token="tok-progress", + progress=25.0, + total=100.0, + message="quarter done", + related_request_id="req-2001", + ) + mock_session.send_progress_notification.assert_any_call( + progress_token="tok-progress", + progress=50.0, + total=100.0, + message=None, + related_request_id="req-2001", + ) + mock_session.send_progress_notification.assert_any_call( + progress_token="tok-progress", + progress=100.0, + total=100.0, + message="complete", + related_request_id="req-2001", + ) async def test_report_progress_no_token_skips_notification() -> None: - """report_progress is a no-op when no progress_token is present.""" - mock_session = AsyncMock() - mock_session.send_progress_notification = AsyncMock() - request_context = ServerRequestContext( - request_id="req-no-token", - session=mock_session, - meta={}, - lifespan_context=None, - experimental=Experimental(), - ) - ctx = Context(request_context=request_context, mcp_server=MagicMock()) - await ctx.report_progress(50, 100) + """report_progress is a no-op when no progress_token is present.""" + mock_session = AsyncMock() + mock_session.send_progress_notification = AsyncMock() + request_context = ServerRequestContext( + request_id="req-no-token", + session=mock_session, + meta={}, + lifespan_context=None, + experimental=Experimental(), + ) + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + await ctx.report_progress(50, 100) mock_session.send_progress_notification.assert_not_called() async def test_report_progress_integer_token() -> None: - """report_progress works when progress_token is an integer (e.g. 0).""" - mock_session = AsyncMock() - mock_session.send_progress_notification = AsyncMock() - request_context = ServerRequestContext( - request_id="req-int-token", - session=mock_session, - meta={"progress_token": 0}, - lifespan_context=None, - experimental=Experimental(), - ) - ctx = Context(request_context=request_context, mcp_server=MagicMock()) - await ctx.report_progress(1, 10) - mock_session.send_progress_notification.assert_awaited_once_with( - progress_token=0, - progress=1.0, - total=10.0, - message=None, - related_request_id="req-int-token", - ) + """report_progress works when progress_token is an integer (e.g. 0).""" + mock_session = AsyncMock() + mock_session.send_progress_notification = AsyncMock() + request_context = ServerRequestContext( + request_id="req-int-token", + session=mock_session, + meta={"progress_token": 0}, + lifespan_context=None, + experimental=Experimental(), + ) + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + await ctx.report_progress(1, 10) + mock_session.send_progress_notification.assert_awaited_once_with( + progress_token=0, + progress=1.0, + total=10.0, + message=None, + related_request_id="req-int-token", + ) From 0d3e9ef64acd5785644da0ce01e0c0a968edde78 Mon Sep 17 00:00:00 2001 From: Kaushik Iyer <54633514+kaushik701@users.noreply.github.com> Date: Sun, 31 May 2026 18:53:49 -0700 Subject: [PATCH 4/4] Update test_2001_progress_related_request_id.py --- .../test_2001_progress_related_request_id.py | 88 +++++++++++-------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/tests/issues/test_2001_progress_related_request_id.py b/tests/issues/test_2001_progress_related_request_id.py index 7279722905..b12269c836 100644 --- a/tests/issues/test_2001_progress_related_request_id.py +++ b/tests/issues/test_2001_progress_related_request_id.py @@ -12,76 +12,92 @@ pytestmark = pytest.mark.anyio + async def test_report_progress_passes_related_request_id() -> None: """report_progress must forward request_id as related_request_id.""" mock_session = AsyncMock() mock_session.send_progress_notification = AsyncMock() + request_context = ServerRequestContext( - request_id="req-2001", - session=mock_session, - meta={"progress_token": "tok-progress"}, - lifespan_context=None, - experimental=Experimental(), + request_id="req-2001", + session=mock_session, + meta={"progress_token": "tok-progress"}, + lifespan_context=None, + experimental=Experimental(), ) + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + await ctx.report_progress(25, 100, message="quarter done") await ctx.report_progress(50, 100) await ctx.report_progress(100, 100, message="complete") + assert mock_session.send_progress_notification.call_count == 3 + mock_session.send_progress_notification.assert_any_call( - progress_token="tok-progress", - progress=25.0, - total=100.0, - message="quarter done", - related_request_id="req-2001", + progress_token="tok-progress", + progress=25.0, + total=100.0, + message="quarter done", + related_request_id="req-2001", ) mock_session.send_progress_notification.assert_any_call( - progress_token="tok-progress", - progress=50.0, - total=100.0, - message=None, - related_request_id="req-2001", + progress_token="tok-progress", + progress=50.0, + total=100.0, + message=None, + related_request_id="req-2001", ) mock_session.send_progress_notification.assert_any_call( - progress_token="tok-progress", - progress=100.0, - total=100.0, - message="complete", - related_request_id="req-2001", + progress_token="tok-progress", + progress=100.0, + total=100.0, + message="complete", + related_request_id="req-2001", ) + async def test_report_progress_no_token_skips_notification() -> None: """report_progress is a no-op when no progress_token is present.""" mock_session = AsyncMock() mock_session.send_progress_notification = AsyncMock() + request_context = ServerRequestContext( - request_id="req-no-token", - session=mock_session, - meta={}, - lifespan_context=None, - experimental=Experimental(), + request_id="req-no-token", + session=mock_session, + meta={}, + lifespan_context=None, + experimental=Experimental(), ) + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + await ctx.report_progress(50, 100) -mock_session.send_progress_notification.assert_not_called() + + mock_session.send_progress_notification.assert_not_called() + async def test_report_progress_integer_token() -> None: """report_progress works when progress_token is an integer (e.g. 0).""" mock_session = AsyncMock() mock_session.send_progress_notification = AsyncMock() + request_context = ServerRequestContext( - request_id="req-int-token", - session=mock_session, - meta={"progress_token": 0}, - lifespan_context=None, - experimental=Experimental(), + request_id="req-int-token", + session=mock_session, + meta={"progress_token": 0}, + lifespan_context=None, + experimental=Experimental(), ) + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + await ctx.report_progress(1, 10) + mock_session.send_progress_notification.assert_awaited_once_with( - progress_token=0, - progress=1.0, - total=10.0, - message=None, - related_request_id="req-int-token", + progress_token=0, + progress=1.0, + total=10.0, + message=None, + related_request_id="req-int-token", )