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 的核心处理机制,主要由两部分构成:
functionMap:方法注册表HandleRequest():统一分发入口
客户端请求进入系统后的基本路径如下:
text
客户端发送 JSON
↓
transport_->Read() / ReadAsync()
↓
json::parse()
↓
HandleRequest()
↓
根据 method 查找 functionMap
↓
调用对应 XxxCmd()
↓
返回 json response
↓
transport_->Write()
这条链路说明,HandleRequest() 是服务端请求处理的中心入口。
1. functionMap 的作用
在构造阶段,Server 会注册一组 method 与对应处理函数的映射,例如:
initializepingresources/listresources/readtools/listtools/callprompts/listnotifications/...
其本质是:
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-else 或 switch 逻辑,使 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. 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
transport 是 Server 与外部世界之间的抽象通信层。
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 与线程生命周期
从设计上看,这是一套将传输层、协议处理层与业务扩展层分离的服务端骨架实现。