Skip to content

Commit 9d121cf

Browse files
committed
fix: ServerRunner passes None to notification handlers when params absent
Matches Server._handle_notification: when the wire omits params, the handler receives None, not an empty model. _make_context now accepts typed_params=None.
1 parent 87e0dbc commit 9d121cf

2 files changed

Lines changed: 12 additions & 4 deletions

File tree

src/mcp/server/runner.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,13 +227,17 @@ async def _on_notify(
227227
if entry is None:
228228
logger.debug("no handler for notification %s", method)
229229
return
230-
typed_params = entry.params_type.model_validate(params or {})
230+
# Absent wire params reach the handler as `None`, not an empty model
231+
# (matches the existing `Server._handle_notification`).
232+
typed_params = entry.params_type.model_validate(params) if params is not None else None
231233
ctx = self._make_context(dctx, typed_params)
232234
# TODO: cast goes away when `ServerRequestContext = Context` lands.
233235
await cast(Any, entry.handler)(ctx, typed_params)
234236

235-
def _make_context(self, dctx: DispatchContext[TransportContext], typed_params: BaseModel) -> Context[LifespanT]:
236-
meta = getattr(typed_params, "meta", None)
237+
def _make_context(
238+
self, dctx: DispatchContext[TransportContext], typed_params: BaseModel | None
239+
) -> Context[LifespanT]:
240+
meta = getattr(typed_params, "meta", None) if typed_params is not None else None
237241
return Context(dctx, lifespan=self.lifespan_state, connection=self.connection, meta=meta)
238242

239243
def _handle_initialize(self, params: Mapping[str, Any] | None) -> dict[str, Any]:

tests/server/test_runner.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,14 @@ async def on_roots_changed(ctx: Any, params: Any) -> None:
209209
server.add_notification_handler("notifications/roots/list_changed", NotificationParams, on_roots_changed)
210210
async with connected_runner(server) as (client, _):
211211
await client.notify("notifications/roots/list_changed", None)
212+
await client.notify("notifications/roots/list_changed", {})
212213
# DirectDispatcher delivers synchronously; one yield is enough.
213214
await anyio.lowlevel.checkpoint()
214-
assert len(seen) == 1
215+
assert len(seen) == 2
215216
assert isinstance(seen[0][0], Context)
217+
# Absent wire params reach the handler as None; present-but-empty validates.
218+
assert seen[0][1] is None
219+
assert isinstance(seen[1][1], NotificationParams)
216220

217221

218222
@pytest.mark.anyio

0 commit comments

Comments
 (0)