Expose the middleware chain on MCPServer and stop sending unrequested change notifications (#3201)
This commit is contained in:
parent
528e366558
commit
c9c431b71a
1 changed files with 234 additions and 28 deletions
|
|
@ -8,6 +8,7 @@ behaviour under test. Driver tests (`serve_connection`, `serve_one`,
|
|||
"""
|
||||
|
||||
import contextvars
|
||||
import logging
|
||||
from collections.abc import AsyncIterator, Mapping
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass, field, replace
|
||||
|
|
@ -39,6 +40,8 @@ from mcp_types import (
|
|||
InitializeRequestParams,
|
||||
JSONRPCRequest,
|
||||
ListToolsResult,
|
||||
LoggingMessageNotification,
|
||||
LoggingMessageNotificationParams,
|
||||
NotificationParams,
|
||||
PaginatedRequestParams,
|
||||
ProgressNotificationParams,
|
||||
|
|
@ -1783,17 +1786,23 @@ async def test_dual_era_loop_a_notifications_meta_never_opens_the_log_gate(serve
|
|||
@pytest.mark.anyio
|
||||
async def test_dual_era_loop_modern_server_notifications_ride_the_pipe(server: SrvT):
|
||||
"""A modern handler's standalone notification reaches the client over the
|
||||
duplex stream - the notify-only outbound forwards it."""
|
||||
duplex stream - the notify-only outbound forwards it. Change notifications
|
||||
are the exception: at this era they reach clients only via
|
||||
`subscriptions/listen` streams, so a bare one is dropped rather than sent
|
||||
unrequested."""
|
||||
|
||||
async def emit(ctx: Ctx, params: RequestParams | None) -> dict[str, Any]:
|
||||
await ctx.session.send_tool_list_changed()
|
||||
await ctx.session.send_tool_list_changed() # dropped: an unrequested change notification
|
||||
await ctx.session.send_notification(
|
||||
LoggingMessageNotification(params=LoggingMessageNotificationParams(level="info", data="hi"))
|
||||
)
|
||||
return {}
|
||||
|
||||
server.add_request_handler("x/emit", RequestParams, emit)
|
||||
async with dual_era_client(server) as (client, recorder):
|
||||
await client.send_raw_request("x/emit", _modern_params())
|
||||
await recorder.notified.wait()
|
||||
assert recorder.notifications[0][0] == "notifications/tools/list_changed"
|
||||
assert [method for method, _ in recorder.notifications] == ["notifications/message"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
|
@ -1992,12 +2001,35 @@ def test_no_server_requests_dispatch_context_passes_an_already_denying_transport
|
|||
async def test_notify_only_outbound_forwards_notifications_and_refuses_requests():
|
||||
inner = _RecordingInnerDctx()
|
||||
outbound = NotifyOnlyOutbound(inner)
|
||||
await outbound.notify("notifications/tools/list_changed", None)
|
||||
assert inner.notifies == ["notifications/tools/list_changed"]
|
||||
await outbound.notify("notifications/message", None)
|
||||
assert inner.notifies == ["notifications/message"]
|
||||
with pytest.raises(NoBackChannelError):
|
||||
await outbound.send_raw_request("ping", None)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"method",
|
||||
[
|
||||
"notifications/tools/list_changed",
|
||||
"notifications/prompts/list_changed",
|
||||
"notifications/resources/list_changed",
|
||||
"notifications/resources/updated",
|
||||
],
|
||||
)
|
||||
async def test_notify_only_outbound_drops_change_notifications(method: str, caplog: pytest.LogCaptureFixture):
|
||||
"""Spec: the server never sends a notification type a subscription did not
|
||||
request. At the modern era change notifications reach a client only through
|
||||
a `subscriptions/listen` stream, so a bare copy on the shared channel would
|
||||
be an unrequested notification - the standalone channel drops it."""
|
||||
inner = _RecordingInnerDctx()
|
||||
outbound = NotifyOnlyOutbound(inner)
|
||||
with caplog.at_level(logging.DEBUG, logger="mcp.server.connection"):
|
||||
await outbound.notify(method, None)
|
||||
assert inner.notifies == []
|
||||
assert f"dropped {method}: delivered via subscriptions/listen at this era" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dual_era_loop_carries_the_sender_context_through_the_replay():
|
||||
"""A context-aware read stream's per-message sender context still reaches
|
||||
|
|
|
|||
Loading…
Reference in a new issue