MCP项目笔记三(server)

MCP Server服务端设计:请求分发、回调注册、通知发送与 transport 连接

Server 实现了一个典型 MCP 服务端骨架的核心结构。它并不直接承载具体业务,而是提供了一组通用能力,包括:

  • 请求接收与 JSON 解析
  • 方法分发
  • 回调覆盖
  • 异步通知发送
  • transport 抽象与连接管理
  • 同步 / 异步运行模式

从职责划分上看,这个 Server 更接近一个通用框架:通信层由 transport 负责,协议处理与分发由 Server 负责,具体业务逻辑则通过默认命令函数或回调覆盖注入。


一、Server 的整体角色

Server 的主要职责可以概括为以下几部分:

  • 持有并管理 transport_
  • 通过 functionMap 建立 method 与 handler 的映射关系
  • 通过 HandleRequest() 统一处理客户端请求
  • 通过 OverrideCallback() 提供运行时覆盖默认行为的能力
  • 通过 notification_queue_WriterLoop() 实现异步通知发送
  • 通过同步或异步连接函数驱动整个生命周期

从结构上看,可以将它抽象为:

text 复制代码
Server = 协议层 + 分发层 + 生命周期管理
transport = 底层通信通道
callback / plugin = 业务逻辑实现

在这种设计中,Server 不直接依赖底层通信细节,也不将业务逻辑硬编码在主流程中,而是将协议处理、传输层和业务扩展点区分开来。


二、请求分发

请求分发是整个 Server 的核心处理机制,主要由两部分构成:

  1. functionMap:方法注册表
  2. HandleRequest():统一分发入口

客户端请求进入系统后的基本路径如下:

text 复制代码
客户端发送 JSON
    ↓
transport_->Read() / ReadAsync()
    ↓
json::parse()
    ↓
HandleRequest()
    ↓
根据 method 查找 functionMap
    ↓
调用对应 XxxCmd()
    ↓
返回 json response
    ↓
transport_->Write()

这条链路说明,HandleRequest() 是服务端请求处理的中心入口。

1. functionMap 的作用

在构造阶段,Server 会注册一组 method 与对应处理函数的映射,例如:

  • initialize
  • ping
  • resources/list
  • resources/read
  • tools/list
  • tools/call
  • prompts/list
  • notifications/...

其本质是:

text 复制代码
method 字符串 → std::function<json(const json&)>

即通过 method 字符串定位具体 handler。

例如:

  • "initialize" 对应 InitializeCmd
  • "ping" 对应 PingCmd
  • "tools/call" 对应 ToolsCallCmd

这里使用 lambda 的原因在于:成员函数需要绑定 this,而 functionMap 需要保存统一签名的可调用对象,因此 lambda 充当了包装层。

2. HandleRequest 的处理过程

HandleRequest() 的职责包括:

  • 可选地输出请求日志
  • 校验请求中是否包含 method
  • functionMap 中查找对应 handler
  • 调用 handler 并返回其结果
  • 在找不到 method 时构造标准错误响应

其核心流程可表示为:

text 复制代码
JSON Request
    ↓
HandleRequest()
    ↓
是否包含 method?
   ↓       					↓
  否         				是
  ↓          				↓
InvalidRequest   functionMap.find(method)
                    ↓
            是否找到 handler?
               ↓        ↓
              否        是
              ↓         ↓
     MethodNotFound   handler(request)
                          ↓
                     返回 response

这种分发方式避免了大量 if-elseswitch 逻辑,使 method 扩展与默认处理的管理集中在统一路由表中。

3. request 与 notification 的区别

在读循环中,处理结果通常会经过如下判断:

cpp 复制代码
if (response != nullptr) {
    transport_->Write(response.dump());
}

这意味着:

  • handler 返回非空 JSON:表示该消息是 request,需要返回 response
  • handler 返回 nullptr:表示该消息是 notification,不返回任何响应

因此,请求与通知的区分,并不依赖额外的独立流程,而是依赖 handler 的返回值语义。


三、回调注册

回调注册机制用于在运行时覆盖默认 method 的处理逻辑。它的核心接口是:

cpp 复制代码
bool Server::OverrideCallback(
    const std::string &method,
    std::function<json(const json&)> function
)

其本质是修改 functionMap 中指定 method 的 handler。

1. OverrideCallback 的工作方式

其核心行为可以概括为:

cpp 复制代码
functionMap[method] = std::move(function);

但现有实现只允许覆盖已存在的 method,即先检查该 method 是否已经注册。

因此它的语义不是"动态添加任意新方法",而是"替换框架已有方法的默认实现"。

2. 在整体流程中的位置

覆盖完成后,请求链路变为:

text 复制代码
客户端请求
    ↓
HandleRequest()
    ↓
查 functionMap
    ↓
调用 handler

其中 handler 既可能是构造函数中预注册的默认命令函数,也可能是通过 OverrideCallback() 替换后的函数。

也就是说,回调注册改变的是"查表后的落点"。

3. 框架与业务的分工

这种设计体现了清晰的分工:

框架负责

  • transport 生命周期管理
  • JSON 解析
  • 请求分发
  • 线程管理
  • 默认协议方法实现

业务层负责

  • 具体的工具调用逻辑
  • 资源读取逻辑
  • prompt 获取逻辑
  • 对默认方法进行覆盖

例如,tools/call 默认实现只是一个占位逻辑,提示需要在插件中重写,这表明该方法本身就是为业务层覆盖预留的。

4. 设计边界

该实现中的回调注册有几个明确边界:

  • 默认仅允许覆盖已有 method
  • functionMap 的修改未显式加锁,运行期动态覆盖不具备线程安全保障
  • 通知类 method 若被覆盖,仍应遵循 notification 语义,返回 nullptr

因此,该机制更适合在服务启动前完成注册,而不是在高并发运行过程中频繁调整。


四、通知发送

通知发送采用的是异步生产者-消费者模型,而不是调用方直接执行 transport_->Write()

其基本链路如下:

text 复制代码
业务代码 / 插件
    ↓
SendNotification(pluginName, notification)
    ↓
notification_queue_
    ↓
queue_cv_.notify_one()
    ↓
WriterLoop()
    ↓
transport_->Write(notification)

这意味着,通知发送被拆分为两个阶段:

  1. 业务线程提交通知
  2. 写线程统一串行发送通知

1. SendNotification 的职责

SendNotification() 的职责主要包括:

  • 检查服务是否处于停止状态
  • 将通知写入 notification_queue_
  • 通过条件变量唤醒写线程

它本身并不直接执行底层写操作,而只是充当通知生产者。

2. WriterLoop 的职责

WriterLoop() 是发送通知的消费者线程。其典型逻辑为:

  • 等待队列中出现待发送通知
  • 从队列中取出一条通知
  • 在适当的锁保护下调用 transport_->Write()

当前实现中,WriterLoop 只消费通知队列,因此它在语义上是"通知专用写线程"。

3. 为什么通知采用队列而不是直接写

采用通知队列有三个直接原因:

第一,避免并发写 transport

多个线程若同时调用 transport_->Write(),容易导致输出交叉或线程安全问题。

第二,保证通知顺序

队列天然提供 FIFO 顺序,能够保证通知按入队顺序发送。

第三,避免业务线程阻塞

如果底层 transport 写操作较慢,直接写会拖慢调用方线程;改为入队后,由写线程统一消费,可以将发送阻塞与业务线程解耦。

4. response 与 notification 的写路径差异

该实现中存在两类输出路径:

请求响应

由读循环在处理 request 后直接写回:

cpp 复制代码
transport_->Write(response.dump());

服务端主动通知

SendNotification() 提交到队列,再由 WriterLoop() 发送:

cpp 复制代码
transport_->Write(notification_to_send);

因此,当前实现并没有将所有输出统一到单写线程,而是将 notification 单独放入异步写路径。

5. output_mutex_ 的作用

由于 response 与 notification 可能来自不同线程,底层写操作可能并发发生,因此 output_mutex_ 同时保护:

  • 队列访问
  • transport 写操作

这样可以避免响应输出与通知输出在底层通道中发生交叉。

虽然这种设计的锁粒度较大,但实现相对简单。


五、连接 transport

transportServer 与外部世界之间的抽象通信层。
Server 并不直接依赖具体通信方式,而是通过 ITransport 接口与底层 IO 解耦。

1. transport 的抽象角色

在类中,transport_ 的类型为:

cpp 复制代码
std::shared_ptr<ITransport> transport_;

这意味着,Server 只依赖如下抽象能力:

  • Start()
  • Read() / ReadAsync()
  • Write()
  • Stop()

至于底层是标准输入输出、SSE 还是其他传输方式,都由具体 ITransport 实现决定。

这体现的是协议层与传输层分离的设计。

2. Connect:同步连接模式

同步模式入口为:

cpp 复制代码
bool Connect(const std::shared_ptr<ITransport>& transport);

其主要流程为:

  • 校验 transport 非空
  • 保存到 transport_
  • 重置停止状态
  • 启动 writer_thread_
  • 调用 transport_->Start()
  • 进入循环调用 transport->Read()
  • 对读到的 JSON 执行解析、分发与响应写回
  • 在结束时执行停止流程

在该模式下,Connect() 调用后当前线程会被阻塞在读循环中。

3. ConnectAsync:异步连接模式

异步模式入口为:

cpp 复制代码
bool ConnectAsync(const std::shared_ptr<ITransport>& transport);

它与同步模式的区别在于:

  • 同样保存 transport、初始化状态并启动写线程
  • 额外启动 reader_thread_
  • 在读线程中调用 transport_->ReadAsync()
  • ConnectAsync() 自身会快速返回

因此异步模式中:

  • 主线程负责启动
  • 读线程负责接收请求
  • 写线程负责发送通知

4. Stop:transport 生命周期结束点

Stop() 的典型逻辑包括:

  • 设置 isStopping_ = true
  • 调用 transport_->Stop()
  • 释放 transport_
  • 停止写线程并唤醒条件变量
  • join 写线程
  • 停止读线程并 join 读线程

因此,transport 生命周期是由 Server 统一驱动的:

  • 连接时注入并启动
  • 停止时关闭并释放

从这个角度看,Connect() / ConnectAsync() 的含义更接近"将当前 Server 绑定到一个通信通道并启动运行",而不是传统意义上的主动建立网络连接。


六、完整调用链

将上述四部分合并后,可以得到整个 Server 从启动到处理消息的完整调用链。

1. 客户端 request → 服务端 response

text 复制代码
Server::Connect / ConnectAsync
    ↓
绑定 transport_,初始化状态
    ↓
启动 WriterLoop
    ↓
同步模式:调用 Read()
异步模式:读线程中调用 ReadAsync()
    ↓
得到 json_string
    ↓
json::parse()
    ↓
HandleRequest()
    ↓
根据 method 查找 functionMap
    ↓
调用对应 XxxCmd() 或覆盖后的 callback
    ↓
返回 response
    ↓
if (response != nullptr)
    ↓
transport_->Write(response.dump())

这条链路对应标准 request-response 处理过程。

2. 客户端 notification → 服务端处理但不回包

text 复制代码
客户端发送 notification
    ↓
Read / ReadAsync
    ↓
json::parse()
    ↓
HandleRequest()
    ↓
调用 NotificationXXXCmd()
    ↓
返回 nullptr
    ↓
不执行 transport_->Write()

在这条链路中,notification 与 request 的处理入口相同,但由于返回值为空,因此不会产生回包。

3. 服务端主动 notification → 客户端接收通知

text 复制代码
业务代码 / 插件
    ↓
SendNotification()
    ↓
notification_queue_
    ↓
queue_cv_.notify_one()
    ↓
WriterLoop()
    ↓
transport_->Write(notification)

这条链路独立于请求分发流程,属于服务端主动推送机制。


七、总结

这份 Server 实现的整体结构可以概括为:

  • 通过 Connect() / ConnectAsync() 绑定并启动 transport
  • 通过 Read() / ReadAsync() 接收客户端消息
  • 通过 json::parse()HandleRequest() 完成请求分发
  • 通过 functionMap 建立 method 到 handler 的映射
  • 通过 OverrideCallback() 提供运行时覆盖默认逻辑的能力
  • 通过 SendNotification()、通知队列和 WriterLoop() 实现异步主动通知
  • 通过 Stop() 统一收束 transport 与线程生命周期

从设计上看,这是一套将传输层、协议处理层与业务扩展层分离的服务端骨架实现。

相关推荐
似水明俊德2 小时前
06-C#
开发语言·c++·算法·c#
ysa0510302 小时前
模拟【打牌游戏】
数据结构·c++·笔记·算法
勤劳的执着的运维农民工2 小时前
使用ubnt protect chime门铃有感
运维·笔记
ht巷子2 小时前
boost.asio网络学习:Http Server
网络·c++·http
weixin_649555672 小时前
C语言程序设计第四版(何钦铭、颜晖)第八章指针之循环后移
c语言·c++·算法
福楠2 小时前
C++ | 哈希的应用
开发语言·c++·哈希算法
_饭团2 小时前
C语言数组全解析:从入门到精通
c语言·开发语言·数据结构·经验分享·笔记·学习·算法
乾元2 小时前
安全官(CISO)的困惑:AI 投入产出比(ROI)的衡量
网络·人工智能·安全·网络安全·chatgpt·架构·安全架构
快乐柠檬不快乐2 小时前
C++中的代理模式实现
开发语言·c++·算法