docs/v2/clients/messages.mdx
import { VersionBadge } from "/snippets/version-badge.mdx";
<VersionBadge version="2.9.1" />MCP clients can receive various types of messages from servers, including requests that need responses and notifications that don't. The message handler provides a unified way to process all these messages.
The simplest way to handle messages is with a function that receives all messages:
from fastmcp import Client
async def message_handler(message):
"""Handle all MCP messages from the server."""
if hasattr(message, 'root'):
method = message.root.method
print(f"Received: {method}")
# Handle specific notifications
if method == "notifications/tools/list_changed":
print("Tools have changed - might want to refresh tool cache")
elif method == "notifications/resources/list_changed":
print("Resources have changed")
client = Client(
"my_mcp_server.py",
message_handler=message_handler,
)
For fine-grained targeting, FastMCP provides a MessageHandler class you can subclass to take advantage of specific hooks:
from fastmcp import Client
from fastmcp.client.messages import MessageHandler
import mcp.types
class MyMessageHandler(MessageHandler):
async def on_tool_list_changed(
self, notification: mcp.types.ToolListChangedNotification
) -> None:
"""Handle tool list changes specifically."""
print("Tool list changed - refreshing available tools")
client = Client(
"my_mcp_server.py",
message_handler=MyMessageHandler(),
)
All handler methods receive a single argument - the specific message type:
<Card icon="code" title="Message Handler Methods"> <ResponseField name="on_message(message)" type="Any MCP message"> Called for ALL messages (requests and notifications) </ResponseField> <ResponseField name="on_request(request)" type="mcp.types.ClientRequest"> Called for requests that expect responses </ResponseField> <ResponseField name="on_notification(notification)" type="mcp.types.ServerNotification"> Called for notifications (fire-and-forget) </ResponseField> <ResponseField name="on_tool_list_changed(notification)" type="mcp.types.ToolListChangedNotification"> Called when the server's tool list changes </ResponseField> <ResponseField name="on_resource_list_changed(notification)" type="mcp.types.ResourceListChangedNotification"> Called when the server's resource list changes </ResponseField> <ResponseField name="on_prompt_list_changed(notification)" type="mcp.types.PromptListChangedNotification"> Called when the server's prompt list changes </ResponseField> <ResponseField name="on_progress(notification)" type="mcp.types.ProgressNotification"> Called for progress updates during long-running operations </ResponseField> <ResponseField name="on_logging_message(notification)" type="mcp.types.LoggingMessageNotification"> Called for log messages from the server </ResponseField> </Card>Here's a practical example of handling tool list changes:
from fastmcp.client.messages import MessageHandler
import mcp.types
class ToolCacheHandler(MessageHandler):
def __init__(self):
self.cached_tools = []
async def on_tool_list_changed(
self, notification: mcp.types.ToolListChangedNotification
) -> None:
"""Clear tool cache when tools change."""
print("Tools changed - clearing cache")
self.cached_tools = [] # Force refresh on next access
client = Client("server.py", message_handler=ToolCacheHandler())
While the message handler receives server-initiated requests, for most use cases you should use the dedicated callback parameters instead:
sampling_handlerprogress_handlerlog_handlerThe message handler is primarily for monitoring and handling notifications rather than responding to requests.