[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
相关推荐
m0_747854522 小时前
php怎么使用PHP PM热重启_php如何零停机更新生产环境代码
jvm·数据库·python
四维迁跃2 小时前
如何提升SQL数据更新的安全性_使用行级锁与悲观锁机制
jvm·数据库·python
老刘说AI2 小时前
Text2SQL到数据智能
人工智能·python·低代码·语言模型·langchain
knight_9___2 小时前
RAG面试篇10
人工智能·python·机器学习·agent·rag
格林威2 小时前
面阵相机 vs 线阵相机:堡盟与海康相机选型差异全解析+python实战演示
开发语言·人工智能·python·数码相机·计算机视觉·视觉检测·工业相机
2301_817672262 小时前
CSS如何控制placeholder文字的颜色_使用--placeholder伪元素.txt
jvm·数据库·python
TechWayfarer2 小时前
App还是Web?IP段归属查询的工具适配与实战指南
python·tcp/ip·网络安全
B站_计算机毕业设计之家2 小时前
计算机毕业设计:Python股票投资辅助决策系统 django框架 request爬虫 协同过滤算法 数据分析 可视化 大数据 大模型(建议收藏)✅
爬虫·python·深度学习·算法·django·flask·课程设计
m0_684501982 小时前
Go语言怎么操作Word文档_Go语言Word文档生成教程【精通】
jvm·数据库·python