Skip to content

Commit 198bf01

Browse files
g97iulio1609Copilot
andcommitted
feat(client): handle list_changed notifications via callbacks
Add support for ToolListChangedNotification, PromptListChangedNotification, and ResourceListChangedNotification in ClientSession._received_notification(). Previously these notifications were silently dropped, making it impossible for clients to react when a server's tool, prompt, or resource lists changed dynamically. Changes: - Add ToolListChangedFnT, PromptListChangedFnT, ResourceListChangedFnT callback Protocol types in session.py - Accept optional callbacks in ClientSession.__init__() (keyword-only) - Dispatch to callbacks in _received_notification() with try-except safety - Expose callbacks in Client dataclass and ClientSessionParameters - Pass callbacks through to ClientSession in Client.__aenter__() and ClientSessionGroup._establish_session() Fixes #2107 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 62575ed commit 198bf01

5 files changed

Lines changed: 370 additions & 2 deletions

File tree

src/mcp/client/client.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,17 @@
88

99
from mcp.client._memory import InMemoryTransport
1010
from mcp.client._transport import Transport
11-
from mcp.client.session import ClientSession, ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
11+
from mcp.client.session import (
12+
ClientSession,
13+
ElicitationFnT,
14+
ListRootsFnT,
15+
LoggingFnT,
16+
MessageHandlerFnT,
17+
PromptListChangedFnT,
18+
ResourceListChangedFnT,
19+
SamplingFnT,
20+
ToolListChangedFnT,
21+
)
1222
from mcp.client.streamable_http import streamable_http_client
1323
from mcp.server import Server
1424
from mcp.server.mcpserver import MCPServer
@@ -95,6 +105,15 @@ async def main():
95105
elicitation_callback: ElicitationFnT | None = None
96106
"""Callback for handling elicitation requests."""
97107

108+
tool_list_changed_callback: ToolListChangedFnT | None = None
109+
"""Callback invoked when the server signals its tool list has changed."""
110+
111+
prompt_list_changed_callback: PromptListChangedFnT | None = None
112+
"""Callback invoked when the server signals its prompt list has changed."""
113+
114+
resource_list_changed_callback: ResourceListChangedFnT | None = None
115+
"""Callback invoked when the server signals its resource list has changed."""
116+
98117
_session: ClientSession | None = field(init=False, default=None)
99118
_exit_stack: AsyncExitStack | None = field(init=False, default=None)
100119
_transport: Transport = field(init=False)
@@ -126,6 +145,9 @@ async def __aenter__(self) -> Client:
126145
message_handler=self.message_handler,
127146
client_info=self.client_info,
128147
elicitation_callback=self.elicitation_callback,
148+
tool_list_changed_callback=self.tool_list_changed_callback,
149+
prompt_list_changed_callback=self.prompt_list_changed_callback,
150+
resource_list_changed_callback=self.resource_list_changed_callback,
129151
)
130152
)
131153

src/mcp/client/session.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ class LoggingFnT(Protocol):
4747
async def __call__(self, params: types.LoggingMessageNotificationParams) -> None: ... # pragma: no branch
4848

4949

50+
class ResourceListChangedFnT(Protocol):
51+
async def __call__(self) -> None: ... # pragma: no branch
52+
53+
54+
class ToolListChangedFnT(Protocol):
55+
async def __call__(self) -> None: ... # pragma: no branch
56+
57+
58+
class PromptListChangedFnT(Protocol):
59+
async def __call__(self) -> None: ... # pragma: no branch
60+
61+
5062
class MessageHandlerFnT(Protocol):
5163
async def __call__(
5264
self,
@@ -95,6 +107,10 @@ async def _default_logging_callback(
95107
pass
96108

97109

110+
async def _default_list_changed_callback() -> None:
111+
pass
112+
113+
98114
ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(types.ClientResult | types.ErrorData)
99115

100116

@@ -121,6 +137,9 @@ def __init__(
121137
*,
122138
sampling_capabilities: types.SamplingCapability | None = None,
123139
experimental_task_handlers: ExperimentalTaskHandlers | None = None,
140+
tool_list_changed_callback: ToolListChangedFnT | None = None,
141+
prompt_list_changed_callback: PromptListChangedFnT | None = None,
142+
resource_list_changed_callback: ResourceListChangedFnT | None = None,
124143
) -> None:
125144
super().__init__(read_stream, write_stream, read_timeout_seconds=read_timeout_seconds)
126145
self._client_info = client_info or DEFAULT_CLIENT_INFO
@@ -130,6 +149,9 @@ def __init__(
130149
self._list_roots_callback = list_roots_callback or _default_list_roots_callback
131150
self._logging_callback = logging_callback or _default_logging_callback
132151
self._message_handler = message_handler or _default_message_handler
152+
self._tool_list_changed_callback = tool_list_changed_callback or _default_list_changed_callback
153+
self._prompt_list_changed_callback = prompt_list_changed_callback or _default_list_changed_callback
154+
self._resource_list_changed_callback = resource_list_changed_callback or _default_list_changed_callback
133155
self._tool_output_schemas: dict[str, dict[str, Any] | None] = {}
134156
self._server_capabilities: types.ServerCapabilities | None = None
135157
self._experimental_features: ExperimentalClientFeatures | None = None
@@ -470,6 +492,21 @@ async def _received_notification(self, notification: types.ServerNotification) -
470492
match notification:
471493
case types.LoggingMessageNotification(params=params):
472494
await self._logging_callback(params)
495+
case types.ToolListChangedNotification():
496+
try:
497+
await self._tool_list_changed_callback()
498+
except Exception:
499+
logger.exception("Tool list changed callback raised an exception")
500+
case types.PromptListChangedNotification():
501+
try:
502+
await self._prompt_list_changed_callback()
503+
except Exception:
504+
logger.exception("Prompt list changed callback raised an exception")
505+
case types.ResourceListChangedNotification():
506+
try:
507+
await self._resource_list_changed_callback()
508+
except Exception:
509+
logger.exception("Resource list changed callback raised an exception")
473510
case types.ElicitCompleteNotification(params=params):
474511
# Handle elicitation completion notification
475512
# Clients MAY use this to retry requests or update UI

src/mcp/client/session_group.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,16 @@
2020

2121
import mcp
2222
from mcp import types
23-
from mcp.client.session import ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
23+
from mcp.client.session import (
24+
ElicitationFnT,
25+
ListRootsFnT,
26+
LoggingFnT,
27+
MessageHandlerFnT,
28+
PromptListChangedFnT,
29+
ResourceListChangedFnT,
30+
SamplingFnT,
31+
ToolListChangedFnT,
32+
)
2433
from mcp.client.sse import sse_client
2534
from mcp.client.stdio import StdioServerParameters
2635
from mcp.client.streamable_http import streamable_http_client
@@ -80,6 +89,9 @@ class ClientSessionParameters:
8089
logging_callback: LoggingFnT | None = None
8190
message_handler: MessageHandlerFnT | None = None
8291
client_info: types.Implementation | None = None
92+
tool_list_changed_callback: ToolListChangedFnT | None = None
93+
prompt_list_changed_callback: PromptListChangedFnT | None = None
94+
resource_list_changed_callback: ResourceListChangedFnT | None = None
8395

8496

8597
class ClientSessionGroup:
@@ -310,6 +322,9 @@ async def _establish_session(
310322
logging_callback=session_params.logging_callback,
311323
message_handler=session_params.message_handler,
312324
client_info=session_params.client_info,
325+
tool_list_changed_callback=session_params.tool_list_changed_callback,
326+
prompt_list_changed_callback=session_params.prompt_list_changed_callback,
327+
resource_list_changed_callback=session_params.resource_list_changed_callback,
313328
)
314329
)
315330

0 commit comments

Comments
 (0)