Skip to content

Commit 0b2de2a

Browse files
committed
[minimcp] Add Responder for server-to-client notifications
- Implement Responder class for sending notifications from handlers to clients - Add report_progress() method for progress updates during long-running operations - Add send_notification() method for general server notifications - Support automatic idle timeout reset when sending notifications - Add comprehensive unit test suite
1 parent 025d25b commit 0b2de2a

2 files changed

Lines changed: 755 additions & 0 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import json
2+
import logging
3+
from operator import attrgetter
4+
5+
import mcp.types as types
6+
from mcp.server.minimcp.limiter import TimeLimiter
7+
from mcp.server.minimcp.types import Message, Send
8+
from mcp.server.minimcp.utils import json_rpc
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class Responder:
14+
"""
15+
Responder enables message handlers to send notifications back to the client.
16+
17+
The Responder is available in the handler context via mcp.context.get_responder()
18+
and provides methods for sending server-to-client notifications, including
19+
progress updates during long-running operations.
20+
21+
The responder automatically resets the idle timeout when sending notifications,
22+
ensuring the handler doesn't time out while actively communicating with the client.
23+
"""
24+
25+
_request: Message
26+
_progress_token: types.ProgressToken | None
27+
_time_limiter: TimeLimiter
28+
29+
_send: Send
30+
31+
def __init__(self, request: Message, send: Send, time_limiter: TimeLimiter):
32+
"""
33+
Args:
34+
request: The incoming message that triggered the handler.
35+
send: The send function for transmitting messages to the client.
36+
time_limiter: The TimeLimiter for managing handler idle timeout.
37+
"""
38+
self._request = request
39+
self._progress_token = self._get_progress_token(request)
40+
41+
self._send = send
42+
self._time_limiter = time_limiter
43+
44+
def _get_progress_token(self, request: Message) -> types.ProgressToken | None:
45+
"""Extract the progress token from the request metadata, if present."""
46+
try:
47+
client_request = types.ClientRequest.model_validate(json.loads(request))
48+
return attrgetter("params.meta.progressToken")(client_request.root)
49+
except Exception:
50+
return None
51+
52+
async def report_progress(
53+
self, progress: float, total: float | None = None, message: str | None = None
54+
) -> types.ProgressToken | None:
55+
"""Report progress for the current operation to the client.
56+
57+
This method sends a progress notification to the client, useful for long-running
58+
operations where you want to keep the client informed. The notification is only
59+
sent if the client provided a progress token in the request metadata.
60+
61+
The idle timeout is automatically reset when sending the progress notification.
62+
63+
Args:
64+
progress: Current progress value (e.g., 24 for 24 items processed).
65+
total: Optional total value (e.g., 100 for total items). Useful for
66+
calculating completion percentage.
67+
message: Optional human-readable message to display to the user
68+
(e.g., "Processing file 24 of 100").
69+
70+
Returns:
71+
The progress token if the notification was sent successfully, None otherwise.
72+
Returns None if no progress token was provided in the request.
73+
"""
74+
75+
if self._progress_token is None:
76+
logger.warning("report_progress failed: Progress token is not available.")
77+
return None
78+
79+
notification = types.ServerNotification(
80+
types.ProgressNotification(
81+
method="notifications/progress", # TODO: Remove once python-sdk/pull/1292 is merged.
82+
params=types.ProgressNotificationParams(
83+
progressToken=self._progress_token,
84+
progress=progress,
85+
total=total,
86+
message=message,
87+
),
88+
)
89+
)
90+
91+
await self.send_notification(notification)
92+
93+
return self._progress_token
94+
95+
async def send_notification(self, notification: types.ServerNotification) -> None:
96+
"""Send a notification to the client.
97+
98+
Notifications are one-way messages from the server to the client that do not
99+
expect a response. This enables bidirectional communication in MiniMCP servers,
100+
allowing handlers to proactively push updates to the client.
101+
102+
The idle timeout is automatically reset when sending notifications, ensuring
103+
the handler remains active while communicating with the client.
104+
105+
Args:
106+
notification: The server notification to send. Common notification types
107+
include progress notifications, log messages, and resource updates etc.
108+
"""
109+
logger.debug("Sending notification: %s", notification)
110+
message = json_rpc.build_notification_message(notification)
111+
112+
# Reset time limiter
113+
self._time_limiter.reset()
114+
115+
# Just call the sender with the message and let transport layer handle the rest.
116+
await self._send(message)

0 commit comments

Comments
 (0)