Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2153,6 +2153,56 @@ if __name__ == "__main__":
_Full example: [examples/snippets/clients/streamable_basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/streamable_basic.py)_
<!-- /snippet-source -->

### Handling Server Notifications

Servers may send notifications, which derive from the `ServerNotification` class. To handle these, follow the following steps:

1. For each notification type you want to support, write a callback function that follows implements the matching protocol, such as `ToolListChangedFnT` for the tool list changed notification.
2. Pass that function to the appropriate parameter when instantiating your client, e.g. `tool_list_changed_callback` for the tool list changed notification. This will be called every time your client receives the matching notification.

<!-- snippet-source examples/snippets/clients/server_notification_client.py -->
```python
# Snippets demonstrating handling known and custom server notifications

import asyncio

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

# Create dummy server parameters for stdio connection
server_params = StdioServerParameters(
command="uv",
args=["run"],
env={},
)


# Create a custom handler for the resource list changed notification
async def custom_resource_list_changed_handler() -> None:
"""Custom handler for resource list changed notifications."""
print("RESOURCE LIST CHANGED")


async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(
read,
write,
resource_list_changed_callback=custom_resource_list_changed_handler,
) as session:
# Initialize the connection
await session.initialize()

# Do client stuff here


if __name__ == "__main__":
asyncio.run(run())
```

_Full example: [examples/snippets/clients/server_notification_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/server_notification_client.py)_
<!-- /snippet-source -->

### Client Display Utilities

When building MCP clients, the SDK provides utilities to help display human-readable names for tools, resources, and prompts:
Expand Down
36 changes: 36 additions & 0 deletions examples/snippets/clients/server_notification_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Snippets demonstrating handling known and custom server notifications

import asyncio

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

# Create dummy server parameters for stdio connection
server_params = StdioServerParameters(
command="uv",
args=["run"],
env={},
)


# Create a custom handler for the resource list changed notification
async def custom_resource_list_changed_handler() -> None:
"""Custom handler for resource list changed notifications."""
print("RESOURCE LIST CHANGED")


async def run():
async with stdio_client(server_params) as (read, write):
async with ClientSession(
read,
write,
resource_list_changed_callback=custom_resource_list_changed_handler,
) as session:
# Initialize the connection
await session.initialize()

# Do client stuff here


if __name__ == "__main__":
asyncio.run(run())
81 changes: 80 additions & 1 deletion src/mcp/client/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,38 @@ async def __call__(
) -> None: ... # pragma: no branch


class ProgressNotificationFnT(Protocol):
async def __call__(
self,
params: types.ProgressNotificationParams,
) -> None: ...


class ResourceUpdatedFnT(Protocol):
async def __call__(
self,
params: types.ResourceUpdatedNotificationParams,
) -> None: ...


class ResourceListChangedFnT(Protocol):
async def __call__(
self,
) -> None: ...


class ToolListChangedFnT(Protocol):
async def __call__(
self,
) -> None: ...


class PromptListChangedFnT(Protocol):
async def __call__(
self,
) -> None: ...


class MessageHandlerFnT(Protocol):
async def __call__(
self,
Expand Down Expand Up @@ -96,6 +128,32 @@ async def _default_logging_callback(
pass


async def _default_progress_callback(
params: types.ProgressNotificationParams,
) -> None:
"""Note: Default progress handling happens in the BaseSession class. This callback will only be called after the
default progress handling has completed."""
pass


async def _default_resource_updated_callback(
params: types.ResourceUpdatedNotificationParams,
) -> None:
pass


async def _default_resource_list_changed_callback() -> None:
pass


async def _default_tool_list_changed_callback() -> None:
pass


async def _default_prompt_list_changed_callback() -> None:
pass


ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(types.ClientResult | types.ErrorData)


Expand All @@ -117,6 +175,11 @@ def __init__(
elicitation_callback: ElicitationFnT | None = None,
list_roots_callback: ListRootsFnT | None = None,
logging_callback: LoggingFnT | None = None,
progress_notification_callback: ProgressNotificationFnT | None = None,
resource_updated_callback: ResourceUpdatedFnT | None = None,
resource_list_changed_callback: ResourceListChangedFnT | None = None,
tool_list_changed_callback: ToolListChangedFnT | None = None,
prompt_list_changed_callback: PromptListChangedFnT | None = None,
message_handler: MessageHandlerFnT | None = None,
client_info: types.Implementation | None = None,
) -> None:
Expand All @@ -132,6 +195,11 @@ def __init__(
self._elicitation_callback = elicitation_callback or _default_elicitation_callback
self._list_roots_callback = list_roots_callback or _default_list_roots_callback
self._logging_callback = logging_callback or _default_logging_callback
self._progress_notification_callback = progress_notification_callback or _default_progress_callback
self._resource_updated_callback = resource_updated_callback or _default_resource_updated_callback
self._resource_list_changed_callback = resource_list_changed_callback or _default_resource_list_changed_callback
self._tool_list_changed_callback = tool_list_changed_callback or _default_tool_list_changed_callback
self._prompt_list_changed_callback = prompt_list_changed_callback or _default_prompt_list_changed_callback
self._message_handler = message_handler or _default_message_handler
self._tool_output_schemas: dict[str, dict[str, Any] | None] = {}
self._server_capabilities: types.ServerCapabilities | None = None
Expand Down Expand Up @@ -547,9 +615,20 @@ async def _handle_incoming(

async def _received_notification(self, notification: types.ServerNotification) -> None:
"""Handle notifications from the server."""
# Process specific notification types
match notification.root:
case types.LoggingMessageNotification(params=params):
await self._logging_callback(params)
case types.ProgressNotification(params=params):
await self._progress_notification_callback(params)
case types.ResourceUpdatedNotification(params=params):
await self._resource_updated_callback(params)
case types.ResourceListChangedNotification():
await self._resource_list_changed_callback()
case types.ToolListChangedNotification():
await self._tool_list_changed_callback()
case types.PromptListChangedNotification():
await self._prompt_list_changed_callback()
case _:
# CancelledNotification is handled separately in shared/session.py
# and should never reach this point. This case is defensive.
pass
24 changes: 23 additions & 1 deletion src/mcp/shared/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,19 @@
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream

import mcp.types as types
from mcp.client.session import ClientSession, ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
from mcp.client.session import (
ClientSession,
ElicitationFnT,
ListRootsFnT,
LoggingFnT,
MessageHandlerFnT,
ProgressNotificationFnT,
PromptListChangedFnT,
ResourceListChangedFnT,
ResourceUpdatedFnT,
SamplingFnT,
ToolListChangedFnT,
)
from mcp.server import Server
from mcp.server.fastmcp import FastMCP
from mcp.shared.message import SessionMessage
Expand Down Expand Up @@ -53,6 +65,11 @@ async def create_connected_server_and_client_session(
sampling_callback: SamplingFnT | None = None,
list_roots_callback: ListRootsFnT | None = None,
logging_callback: LoggingFnT | None = None,
progress_notification_callback: ProgressNotificationFnT | None = None,
resource_updated_callback: ResourceUpdatedFnT | None = None,
resource_list_changed_callback: ResourceListChangedFnT | None = None,
tool_list_changed_callback: ToolListChangedFnT | None = None,
prompt_list_changed_callback: PromptListChangedFnT | None = None,
message_handler: MessageHandlerFnT | None = None,
client_info: types.Implementation | None = None,
raise_exceptions: bool = False,
Expand Down Expand Up @@ -88,6 +105,11 @@ async def create_connected_server_and_client_session(
sampling_callback=sampling_callback,
list_roots_callback=list_roots_callback,
logging_callback=logging_callback,
progress_notification_callback=progress_notification_callback,
resource_updated_callback=resource_updated_callback,
resource_list_changed_callback=resource_list_changed_callback,
tool_list_changed_callback=tool_list_changed_callback,
prompt_list_changed_callback=prompt_list_changed_callback,
message_handler=message_handler,
client_info=client_info,
elicitation_callback=elicitation_callback,
Expand Down
Loading
Loading