|
| 1 | +"""Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/2001. |
| 2 | +
|
| 3 | +Progress notifications not delivered via SSE in stateless HTTP mode. |
| 4 | +
|
| 5 | +Root cause: Context.report_progress() was calling send_progress_notification() |
| 6 | +without passing related_request_id. The SSE / streamable-HTTP transport uses |
| 7 | +that field to route server-initiated messages back to the correct client stream; |
| 8 | +without it notifications are silently dropped. |
| 9 | +
|
| 10 | +Fix: pass related_request_id=self.request_id in send_progress_notification(), |
| 11 | +consistent with how send_log_message() already works. |
| 12 | +""" |
| 13 | + |
| 14 | +from unittest.mock import AsyncMock, MagicMock |
| 15 | + |
| 16 | +import pytest |
| 17 | + |
| 18 | +from mcp.server.context import ServerRequestContext |
| 19 | +from mcp.server.experimental.request_context import Experimental |
| 20 | +from mcp.server.mcpserver import Context |
| 21 | + |
| 22 | +pytestmark = pytest.mark.anyio |
| 23 | + |
| 24 | + |
| 25 | +async def test_report_progress_passes_related_request_id() -> None: |
| 26 | + """report_progress must forward request_id as related_request_id. |
| 27 | +
|
| 28 | + Without related_request_id the streamable-HTTP transport cannot route |
| 29 | + progress notifications to the correct SSE stream; they are silently |
| 30 | + dropped. Regression test for issue #2001. |
| 31 | + """ |
| 32 | + mock_session = AsyncMock() |
| 33 | + mock_session.send_progress_notification = AsyncMock() |
| 34 | + |
| 35 | + request_context = ServerRequestContext( |
| 36 | + request_id="req-2001", |
| 37 | + session=mock_session, |
| 38 | + meta={"progress_token": "tok-progress"}, |
| 39 | + lifespan_context=None, |
| 40 | + experimental=Experimental(), |
| 41 | + ) |
| 42 | + |
| 43 | + ctx = Context(request_context=request_context, mcp_server=MagicMock()) |
| 44 | + |
| 45 | + await ctx.report_progress(25, 100, message="quarter done") |
| 46 | + await ctx.report_progress(50, 100) |
| 47 | + await ctx.report_progress(100, 100, message="complete") |
| 48 | + |
| 49 | + assert mock_session.send_progress_notification.call_count == 3 |
| 50 | + |
| 51 | + mock_session.send_progress_notification.assert_any_call( |
| 52 | + progress_token="tok-progress", |
| 53 | + progress=25.0, |
| 54 | + total=100.0, |
| 55 | + message="quarter done", |
| 56 | + related_request_id="req-2001", |
| 57 | + ) |
| 58 | + mock_session.send_progress_notification.assert_any_call( |
| 59 | + progress_token="tok-progress", |
| 60 | + progress=50.0, |
| 61 | + total=100.0, |
| 62 | + message=None, |
| 63 | + related_request_id="req-2001", |
| 64 | + ) |
| 65 | + mock_session.send_progress_notification.assert_any_call( |
| 66 | + progress_token="tok-progress", |
| 67 | + progress=100.0, |
| 68 | + total=100.0, |
| 69 | + message="complete", |
| 70 | + related_request_id="req-2001", |
| 71 | + ) |
| 72 | + |
| 73 | + |
| 74 | +async def test_report_progress_no_token_skips_notification() -> None: |
| 75 | + """report_progress is a no-op when no progress_token is present.""" |
| 76 | + mock_session = AsyncMock() |
| 77 | + mock_session.send_progress_notification = AsyncMock() |
| 78 | + |
| 79 | + request_context = ServerRequestContext( |
| 80 | + request_id="req-no-token", |
| 81 | + session=mock_session, |
| 82 | + meta={}, |
| 83 | + lifespan_context=None, |
| 84 | + experimental=Experimental(), |
| 85 | + ) |
| 86 | + |
| 87 | + ctx = Context(request_context=request_context, mcp_server=MagicMock()) |
| 88 | + |
| 89 | + await ctx.report_progress(50, 100) |
| 90 | + |
| 91 | + mock_session.send_progress_notification.assert_not_called() |
| 92 | + |
| 93 | + |
| 94 | +async def test_report_progress_integer_token() -> None: |
| 95 | + """report_progress works when progress_token is an integer (e.g. 0).""" |
| 96 | + mock_session = AsyncMock() |
| 97 | + mock_session.send_progress_notification = AsyncMock() |
| 98 | + |
| 99 | + request_context = ServerRequestContext( |
| 100 | + request_id="req-int-token", |
| 101 | + session=mock_session, |
| 102 | + meta={"progress_token": 0}, |
| 103 | + lifespan_context=None, |
| 104 | + experimental=Experimental(), |
| 105 | + ) |
| 106 | + |
| 107 | + ctx = Context(request_context=request_context, mcp_server=MagicMock()) |
| 108 | + |
| 109 | + await ctx.report_progress(1, 10) |
| 110 | + |
| 111 | + mock_session.send_progress_notification.assert_awaited_once_with( |
| 112 | + progress_token=0, |
| 113 | + progress=1.0, |
| 114 | + total=10.0, |
| 115 | + message=None, |
| 116 | + related_request_id="req-int-token", |
| 117 | + ) |
0 commit comments