[FastMCP设计、原理与应用-17]从服务器向客户端的反向通知

从通信或者消息交换模式来看,前面涉及的都是从客户端发送请求到服务器并得到对应的响应,这是典型的从客户端到服务器的请求/响应模式,接下来我们介绍两种从服务器向客户端的反向通信模式:

  • 通知:服务端发送单向通知给客户端,客户端不需要回复。比较典型就是进度报告和日志回传,这也是本篇文章着重介绍的内容;
  • 请求:服务端发送请求到客户端,并得到对方的响应。比较典型的就是信息征询(Elicitation)和客户端采样(Client Sampling);

1. 进度报告

如果客户端调用的工具涉及以长耗时的操作,服务端可用通过向客户端实时报告工作进度的方式来提高用户体验。进度报告通过Context如下这个report_progress方法来完成,参数progresstotal通过数值的形式指定完成工作量和总体工作量。

python 复制代码
@dataclass
class Context:
    async def report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None

我们可以通过创建Client对象或者利用它调用工具的时候指定一个ProgressHandler来接收进度通知。ProgressHandler这个可执行对象签名如下:

python 复制代码
ProgressHandler: TypeAlias = ProgressFnT
class ProgressFnT(Protocol):
    async def __call__(
        self, progress: float, total: float | None, message: str | None
    ) -> None: ... 

在如下这个演示程序中,客户端调用工具long_running_task模拟一个长耗时操作,我们将整个工作划分为5个步骤,每个步骤完成后调用Contextreport_progress报告一次进度。

python 复制代码
from fastmcp import FastMCP
from fastmcp.server import Context
from fastmcp.client import Client
import asyncio

server = FastMCP("Server")

@server.tool()
async def long_running_task(
    context: Context, 
) -> int:
    for i in range(1, 6):
        await context.report_progress(progress=i, total=5, message=f"Step {i} completed")
        await asyncio.sleep(1)
    return 5

async def handle_progress(
    progress: float,
    total: float|None,    
    message: str|None
) -> None:
    percentage = (progress / (total or 100)) * 100
    print(f"Progress: {percentage:.1f}% - {message or ''}")

async def main():
    async with Client(server) as client:
        result = await client.call_tool(
            name="long_running_task",
            progress_handler= handle_progress
        )
        assert result.content[0].text == "5" # type: ignore        

asyncio.run(main())
    

我们定义了handle_progress函数作为接收进度通知。在利用Client调用工具long_running_task时,我们将这个函数作为progress_handler参数。程序运行后,客户端端接收到的进度会实时打印出来:

复制代码
Progress: 20.0% - Step 1 completed
Progress: 40.0% - Step 2 completed
Progress: 60.0% - Step 3 completed
Progress: 80.0% - Step 4 completed
Progress: 100.0% - Step 5 completed

2. 日志回传

如果工具执行过程中利用Context如下这几个方法记录不同等级的日志,这些日志会自动发送给客户端。

python 复制代码
@dataclass
class Context:
    async def debug(
        self,
        message: str,
        logger_name: str | None = None,
        extra: Mapping[str, Any] | None = None,
    ) -> None

    async def info(
        self,
        message: str,
        logger_name: str | None = None,
        extra: Mapping[str, Any] | None = None,
    ) -> None

    async def warning(
        self,
        message: str,
        logger_name: str | None = None,
        extra: Mapping[str, Any] | None = None,
    ) -> None

    async def error(
        self,
        message: str,
        logger_name: str | None = None,
        extra: Mapping[str, Any] | None = None,
    ) -> None

客户端可以在创建Client对象时为其指定一个LogHandler来处理接收的日志,LogHandler对应可执行对象签名和作为参数的LogMessage类型定义如下。

python 复制代码
LogMessage: TypeAlias = LoggingMessageNotificationParams
LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
class LoggingMessageNotificationParams(NotificationParams):
    level: LoggingLevel
    logger: str | None = None
    data: Any
    model_config = ConfigDict(extra="allow")
class NotificationParams(BaseModel):
    class Meta(BaseModel):
        model_config = ConfigDict(extra="allow")
    meta: Meta | None = Field(alias="_meta", default=None)

值得一提的,LogHandler能够得到某条日志取决于Client通过set_logging_level方法设置的最低日志等级,它只能看到等级不低于这个设定等级的日志。

python 复制代码
class Client:
    async def set_logging_level(self, level: mcp.types.LoggingLevel) -> None
LoggingLevel = Literal["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"]

我们按照如下的方式将上面演示实例通过进度报告的通知形式替换成了日志形式。

python 复制代码
from fastmcp import FastMCP
from fastmcp.server import Context
from fastmcp.client import Client
from fastmcp.client.logging import LogMessage
import asyncio

server = FastMCP("Server")

@server.tool()
async def long_running_task(
    context: Context, 
) -> int:
    for i in range(1, 6):
        await context.info (message=f"Step {i} completed")
        await asyncio.sleep(1)
    return 5

log = []
async def handle_log(
    log_message: LogMessage
) -> None:
    log.append(log_message)

async def main():
    async with Client(server, log_handler=handle_log) as client:
        await client.set_logging_level("emergency")
        result = await client.call_tool(
            name="long_running_task",
        )
        assert result.content[0].text == "5" # type: ignore
        assert len(log) == 5   
        assert all(log_message.level == "info" for log_message in log) 

asyncio.run(main())    

3. 发送通知

包括上面介绍的进度和日志,以及其他相关的通知可以直接通过调用Contextsend_notification方法来完成。参数notification的类型ServerNotificationType是对多个通知类型的联合,它们都是Notification的子类。众多通知类型由mcp库提供(模块路径为mcp.types),是对MCP规范的实现。由于MCP采用JSON-RPC协议,所以Notification分别利用其methodparams字段表示JSON-RPC的方法和参数,后者的基类为NotificationParams,具有一个表示元数据的meta字段。

python 复制代码
@dataclass
class Context:
    async def send_notification(
        self, notification: mcp.types.ServerNotificationType
    ) -> None
ServerNotificationType: TypeAlias = (
    CancelledNotification
    | ProgressNotification
    | LoggingMessageNotification
    | ResourceUpdatedNotification
    | ResourceListChangedNotification
    | ToolListChangedNotification
    | PromptListChangedNotification
    | ElicitCompleteNotification
    | TaskStatusNotification
)

class Notification(BaseModel, Generic[NotificationParamsT, MethodT]):
    method: MethodT
    params: NotificationParamsT
    model_config = ConfigDict(extra="allow")

class NotificationParams(BaseModel):
    class Meta(BaseModel):
        model_config = ConfigDict(extra="allow")
    meta: Meta | None = Field(alias="_meta", default=None)

RequestParamsT = TypeVar("RequestParamsT", bound=RequestParams | dict[str, Any] | None)
NotificationParamsT = TypeVar("NotificationParamsT", bound=NotificationParams | dict[str, Any] | None)
MethodT = TypeVar("MethodT", bound=str)   

接下来我们对各种通知类型和对应的JSON-RPC方法进行概括性介绍:

  • CancelledNotification(notifications/cancelled):客户端和服务器向对方发送的取消之前请求的通知;
  • ProgressNotification(notifications/progress):客户端和服务器向对方发送的进度报告;
  • LoggingMessageNotification(notifications/message):服务端向客户端发送的日志;
  • ResourceUpdatedNotification(notifications/resources/updated):服务端向客户端发送的关于资源被更新的通知;
  • ResourceListChangedNotification(notifications/resources/list_changed):服务端向客户端发送的关于资源列表发生改变的通知;
  • ToolListChangedNotification(notifications/tools/list_changed):服务端向客户端发送的关于工具列表发生改变的通知;
  • PromptListChangedNotification(notifications/prompts/list_changed):服务端向客户端发送的关于提示词列表发生改变的通知;
  • ElicitCompleteNotification(notifications/elicitation/complete):服务器发给客户端的异步"通关信号",告知之前因权限或配置受限的URL访问已处理完成;
  • TaskStatusNotification(notifications/tasks/status):客户端和服务器向对方发送的关于任务状态改变的通知;

客户端可以在创建Client的时候创建一个MessageHandlerT对象作为其message_handler参数来处理接收到的通知。MessageHandlerT这个可执行对象的签名定义如下,它的参数通常为一个mcp.types.ServerNotification对象,可以通过后者的root字典得到上述这些个通知对象。

python 复制代码
MessageHandlerT: TypeAlias = MessageHandlerFnT
class MessageHandlerFnT(Protocol):
    async def __call__(
        self,
        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
    ) -> None: ...  
class ServerNotification(RootModel[ServerNotificationType]):
    pass
class RootModel(BaseModel, Generic[RootModelRootType], metaclass=_RootModelMetaclass):
    root: RootModelRootType

下面的程序演示了在一个工具函数中利用注入的Context向客户端发送四种类型的通知,以及客户端利用注册的MessageHandler来处理这些通知。

python 复制代码
from fastmcp import FastMCP
from fastmcp.server import Context
from fastmcp.client import Client
from pydantic import AnyUrl
from mcp.types import (
    ServerNotification,
    ServerRequest,
    ClientResult,
    ResourceUpdatedNotification,
    ResourceListChangedNotification,
    ToolListChangedNotification,
    PromptListChangedNotification,
    ResourceUpdatedNotificationParams)
from mcp.shared.session import RequestResponder
import asyncio

server = FastMCP("Server")
@server.tool()
async def fire_notifications(
    context: Context, 
) -> None:
    params = ResourceUpdatedNotificationParams(uri=AnyUrl("file:///path/to/resource"))
    await context.send_notification(ResourceUpdatedNotification(params=params))
    await context.send_notification(ResourceListChangedNotification())
    await context.send_notification(ToolListChangedNotification())
    await context.send_notification(PromptListChangedNotification())

async def handle_notifications(
    message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception,
) -> None:
    match message.root: # type: ignore
        case ResourceUpdatedNotification():
            print("Received resource updated notification for URI:", message.root.params.uri) # type: ignore
        case ResourceListChangedNotification():
            print("Received resource list changed notification")
        case ToolListChangedNotification():
            print("Received tool list changed notification")
        case PromptListChangedNotification():
            print("Received prompt list changed notification")

async def main():
    async with Client(server, message_handler=handle_notifications) as client:
        await client.call_tool(
            name="fire_notifications",
        )
        await asyncio.sleep(5)  # Wait for notifications to be processed

asyncio.run(main())

输出:

复制代码
Received resource updated notification for URI: file:///path/to/resource
Received resource list changed notification
Received tool list changed notification
Received prompt list changed notification
相关推荐
阿正的梦工坊4 小时前
深入理解 PyTorch 中的 unsqueeze 操作
人工智能·pytorch·python
FreakStudio5 小时前
硬件版【Cursor】?aily blockly IDE尝鲜封神,实战硬伤尽显
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
测试员周周7 小时前
【Appium 系列】第06节-页面对象实现 — LoginPage 实战
开发语言·前端·人工智能·python·功能测试·appium·测试用例
2301_783848657 小时前
优化文本分类中堆叠模型的网格搜索性能:避免训练卡顿的实战指南
jvm·数据库·python
CLX05058 小时前
如何安装Oracle 12c Cloud Control_OMS服务端组件与Agent部署
jvm·数据库·python
老纪9 小时前
SQL中如何查找特定的空值行:WHERE IS NULL深度解析
jvm·数据库·python
噜噜噜阿鲁~9 小时前
python学习笔记 | 10.0、面向对象编程
笔记·python·学习
weixin199701080169 小时前
[特殊字符] RESTful API 接口规范详解:构建高效、可扩展的 Web 服务(附 Python 源码)
前端·python·restful
2301_781571429 小时前
mysql数据库响应缓慢如何排查_使用EXPLAIN分析执行计划
jvm·数据库·python
彳亍1019 小时前
实现倒计时数字在到达1后自动隐藏(2为最后可见数字),同时继续运行至-1再终止
jvm·数据库·python