Skip to content

Commit 889a863

Browse files
authored
Contain a raising progress callback on the in-process dispatcher (#3623)
1 parent 0809579 commit 889a863

3 files changed

Lines changed: 26 additions & 20 deletions

File tree

‎src/mcp/shared/direct_dispatcher.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ async def send_raw_request(
9090

9191
async def progress(self, progress: float, total: float | None = None, message: str | None = None) -> None:
9292
if self._on_progress is not None:
93-
await self._on_progress(progress, total, message)
93+
try:
94+
await self._on_progress(progress, total, message)
95+
except Exception:
96+
logger.exception("progress callback raised")
9497

9598

9699
class DirectDispatcher:

‎tests/shared/test_dispatcher.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,28 @@ async def server_on_request(
230230
assert result == {"ok": True}
231231

232232

233+
@pytest.mark.anyio
234+
async def test_raising_on_progress_callback_is_logged_and_request_still_succeeds(
235+
pair_factory: PairFactory, caplog: pytest.LogCaptureFixture
236+
):
237+
"""A caller's `on_progress` callback that raises is logged and does not fail the request (SDK-defined)."""
238+
239+
async def server_on_request(
240+
ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None
241+
) -> dict[str, Any]:
242+
await ctx.progress(0.5)
243+
return {"ok": True}
244+
245+
async def on_progress(progress: float, total: float | None, message: str | None) -> None:
246+
raise RuntimeError("progress callback boom")
247+
248+
async with running_pair(pair_factory, server_on_request=server_on_request) as (client, *_):
249+
with anyio.fail_after(5):
250+
result = await client.send_raw_request("tools/call", None, {"on_progress": on_progress})
251+
assert result == {"ok": True}
252+
assert "progress callback raised" in caplog.text
253+
254+
233255
@pytest.mark.anyio
234256
async def test_ctx_message_metadata_is_none_when_transport_attaches_nothing(pair_factory: PairFactory):
235257
"""Plain requests carry no transport metadata, so handlers see `None`."""

‎tests/shared/test_jsonrpc_dispatcher.py‎

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,25 +1449,6 @@ async def on_progress(progress: float, total: float | None, message: str | None)
14491449
assert crec.notifications == []
14501450

14511451

1452-
@pytest.mark.anyio
1453-
async def test_progress_callback_exception_is_swallowed_and_logged(caplog: pytest.LogCaptureFixture):
1454-
"""A user progress callback raising must not crash the dispatcher."""
1455-
1456-
async def boom(progress: float, total: float | None, message: str | None) -> None:
1457-
raise RuntimeError("progress callback boom")
1458-
1459-
async def server_on_request(ctx: DCtx, method: str, params: Mapping[str, Any] | None) -> dict[str, Any]:
1460-
await ctx.progress(0.5)
1461-
return {"ok": True}
1462-
1463-
opts: CallOptions = {"on_progress": boom}
1464-
async with running_pair(jsonrpc_pair, server_on_request=server_on_request) as (client, *_):
1465-
with anyio.fail_after(5):
1466-
result = await client.send_raw_request("t", None, opts)
1467-
assert result == {"ok": True}
1468-
assert "progress callback raised" in caplog.text
1469-
1470-
14711452
@pytest.mark.anyio
14721453
async def test_inline_methods_are_handled_before_next_message_is_dequeued():
14731454
"""An `inline_methods` method runs to completion before the next message is dispatched."""

0 commit comments

Comments
 (0)