Skip to content

Commit e0a2f22

Browse files
committed
fix: contain DirectDispatcher callback failures
Signed-off-by: 1fanwang <1fannnw@gmail.com>
1 parent d060b36 commit e0a2f22

2 files changed

Lines changed: 45 additions & 2 deletions

File tree

src/mcp/shared/direct_dispatcher.py

Lines changed: 8 additions & 2 deletions
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:
@@ -301,7 +304,10 @@ async def _dispatch_notify(self, method: str, params: Mapping[str, Any] | None)
301304
return
302305
assert self._on_notify is not None
303306
dctx = self._make_context()
304-
await self._on_notify(dctx, method, params)
307+
try:
308+
await self._on_notify(dctx, method, params)
309+
except Exception:
310+
logger.exception("notification handler for %r raised", method)
305311

306312

307313
def create_direct_dispatcher_pair(

tests/shared/test_dispatcher.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,43 @@ async def on_progress(progress: float, total: float | None, message: str | None)
216216
assert received == [(0.5, 1.0, "halfway")]
217217

218218

219+
@pytest.mark.anyio
220+
async def test_progress_callback_exception_does_not_fail_request(
221+
pair_factory: PairFactory, caplog: pytest.LogCaptureFixture
222+
) -> None:
223+
async def on_progress(progress: float, total: float | None, message: str | None) -> None:
224+
raise RuntimeError("progress callback failed")
225+
226+
async def server_on_request(
227+
ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None
228+
) -> dict[str, Any]:
229+
await ctx.progress(0.5)
230+
return {"ok": True}
231+
232+
async with running_pair(pair_factory, server_on_request=server_on_request) as (client, *_):
233+
with anyio.fail_after(5):
234+
result = await client.send_raw_request("tools/call", None, {"on_progress": on_progress})
235+
assert result == {"ok": True}
236+
assert "progress callback raised" in caplog.text
237+
238+
239+
@pytest.mark.anyio
240+
async def test_notification_handler_exception_does_not_reach_sender(
241+
pair_factory: PairFactory, caplog: pytest.LogCaptureFixture
242+
) -> None:
243+
called = anyio.Event()
244+
245+
async def on_notify(ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None) -> None:
246+
called.set()
247+
raise RuntimeError("notification handler failed")
248+
249+
async with running_pair(pair_factory, server_on_notify=on_notify) as (client, *_):
250+
with anyio.fail_after(5):
251+
await client.notify("notifications/message", None)
252+
await called.wait()
253+
assert "notification handler for 'notifications/message' raised" in caplog.text
254+
255+
219256
@pytest.mark.anyio
220257
async def test_ctx_progress_is_noop_when_caller_supplied_no_callback(pair_factory: PairFactory):
221258
async def server_on_request(

0 commit comments

Comments
 (0)