MCP项目笔记十(客户端 MCPClient)

C++ 实现MCP 客户端

Model Context Protocol(MCP) 是 Anthropic 推出的开放协议,旨在让 AI 模型能够标准化地调用外部工具、读取资源、执行 Prompt。本文结合 C++ 实现代码,讲解 MCP 客户端的架构设计与核心机制。


一、什么是 MCP?

MCP 是一套 AI 与外部工具之间的通信协议。它定义了三种核心能力:

  • Tools(工具):AI 可以调用的函数,例如"查询天气"、"执行代码"
  • Prompts(提示模板):预定义的提示词模板,支持参数化
  • Resources(资源):AI 可以读取的外部数据,例如文件、数据库条目

这套协议使用 JSON-RPC 2.0 作为消息格式,保证了跨语言、跨平台的互操作性。


二、整体架构

这份代码包含三个核心类,分工明确:

复制代码
MCPServiceIntegrator          ← 顶层集成器,统一对外暴露
    └── MCPClient             ← 传输层,负责与 MCP Server 通信
    └── MCPToolManager        ← 工具管理层,缓存工具列表、处理调用

传输层:两种通信模式

MCPClient 支持两种传输方式,由 MCPTransportType 枚举区分:

模式 适用场景 实现方式
STDIO 本地进程 fork() + pipe() + execv()
SSE 远程 HTTP 服务 libcurl + Server-Sent Events

三、STDIO 模式:如何与本地进程通信

核心流程

复制代码
父进程(MCPClient)          子进程(MCP Server)
       │                           │
       │──── stdin_pipe ──────────>│  写入 JSON-RPC 请求
       │                           │
       │<─── stdout_pipe ──────────│  读取 JSON-RPC 响应/通知

启动子进程(startMCPServer()

cpp 复制代码
// 1. 创建两对管道
pipe(stdin_pipe_fd);    // 父写 → 子读
pipe(stdout_pipe_fd);   // 子写 → 父读

// 2. fork 出子进程
server_pid_ = fork();

// 3. 子进程内:重定向 stdin/stdout 到管道
dup2(stdin_pipe_fd[0], STDIN_FILENO);
dup2(stdout_pipe_fd[1], STDOUT_FILENO);
execv(server_path_.c_str(), argv.data());

// 4. 父进程内:保存可用端,设置非阻塞读
stdin_pipe_ = stdin_pipe_fd[1];   // 可写端
stdout_pipe_ = stdout_pipe_fd[0]; // 可读端
fcntl(stdout_pipe_, F_SETFL, O_NONBLOCK);

通过 dup2() 将标准输入输出"嫁接"到管道,子进程完全感知不到自己在被远程控制------这正是 STDIO 传输的精妙之处。

后台读取线程(processNotificationsStdio()

连接建立后,主线程启动一个后台线程持续从 stdout_pipe_ 读取数据:

cpp 复制代码
notification_thread_ = std::thread([this]() {
    processNotificationsStdio();
});

读到完整的 \n 分隔行后,解析 JSON-RPC,判断是响应 还是通知,分别处理:

  • 响应 (有 id 字段)→ 推入 response_queue_,唤醒等待的调用方
  • 通知method == "notifications/message")→ 调用用户注册的回调

四、SSE 模式:如何与远程服务通信

SSE(Server-Sent Events)是一种基于 HTTP 的单向推送协议,非常适合服务端主动推送事件流。

整体设计

复制代码
MCPClient
  ├── HTTP POST ────────────────→  /message  (发送 JSON-RPC 请求)
  └── SSE 长连接 ←────────────────  /sse      (接收响应和通知)

请求与响应走两条独立通道:

  1. 通过 sendRequestSSE() 发一个普通 POST 请求
  2. 后台 SSE 监听线程(processNotificationsSSE())持续接收服务端推送的事件流
  3. sseWriteCallback() 解析 data: 行中的 JSON,分发到响应队列或通知回调

CURL 回调机制

cpp 复制代码
// CURL 每收到一段数据就调用这个静态函数
size_t MCPClient::sseWriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata) {
    MCPClient* client = static_cast<MCPClient*>(userdata);
    // 追加到缓冲区,寻找 "\n\n" 作为事件分隔符
    // 解析 "data: {...}" 行,提取 JSON 数据
    // 判断是响应还是通知,分别处理
    return size * nmemb;
}

五、请求-响应同步机制

无论哪种传输方式,请求和响应之间的同步都依赖同一套生产者-消费者模型:

cpp 复制代码
// 生产者(后台读取线程)
{
    std::lock_guard<std::mutex> lock(queue_mutex_);
    response_queue_.push(response);
    queue_cv_.notify_one();  // 唤醒等待方
}

// 消费者(调用方线程)
std::unique_lock<std::mutex> lock(queue_mutex_);
queue_cv_.wait_for(lock, std::chrono::seconds(30), [this] {
    return !response_queue_.empty();
});
MCPResponse response = response_queue_.front();
response_queue_.pop();

这是经典的 条件变量 + 互斥锁 模式,保证线程安全的同时,避免了忙等(busy-waiting)带来的 CPU 浪费。


六、JSON-RPC 消息构造

所有请求统一通过 buildJSONRPCRequest() 序列化:

json 复制代码
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "id": "call_tool_1712345678",
  "params": {
    "name": "search",
    "arguments": { "query": "MCP protocol" }
  }
}

响应通过 parseJSONRPCResponse() 反序列化,区分成功结果(result 字段)与错误(error.message 字段)。


std::atomic<bool> 的使用

connected_running_ 均使用原子类型,避免多线程读写时的数据竞争:

cpp 复制代码
std::atomic<bool> connected_{false};
std::atomic<bool> running_{false};

七、工具调用的完整链路

以调用一个工具为例,梳理完整的数据流:

复制代码
用户代码
  │
  ├─→ MCPToolManager::executeTool("search", "{\"query\":\"MCP\"}")
  │         │
  │         ├─→ MCPClient::callTool()
  │         │         │
  │         │         ├─→ 构造 MCPRequest {method="tools/call", ...}
  │         │         ├─→ sendRequest() → sendRequestStdio() → write(stdin_pipe_)
  │         │         │
  │         │         └─→ receiveResponse()  ← 阻塞等待(最多 30s)
  │         │                   ↑
  │         │      [后台线程] processNotificationsStdio()
  │         │                   │
  │         │              read(stdout_pipe_) → parseJSONRPCResponse()
  │         │                   │
  │         │              response_queue_.push() + queue_cv_.notify_one()
  │         │
  │         └─→ 返回 MCPResponse {result: "搜索结果..."}
  │
  └─→ 使用工具执行结果

八、总结

这份 C++ MCP 客户端实现涵盖了以下核心工程要点:

  • 进程间通信fork + pipe + dup2 + execv 构成 STDIO 模式的基础
  • HTTP 长连接:libcurl + SSE 事件流实现远程传输
  • 线程同步:条件变量 + 互斥锁保证响应的安全传递
  • 协议设计:JSON-RPC 2.0 统一请求/响应/通知的消息格式
  • 接口抽象IMCPClient 纯虚接口使传输层可替换、可测试

MCP 协议的魅力在于它把 AI 能力的扩展标准化了------无论是本地脚本还是远程服务,都能通过同一套接口被 AI 模型调用。随着 MCP 生态的成熟,这类客户端实现将成为 AI 应用开发的基础设施之一。

相关推荐
哥布林学者11 小时前
深度学习进阶(三十一)FlashAttention:IO 感知的精确注意力
机器学习·ai
岳小哥AI20 小时前
AI大模型"幻觉"从何而来?解密GPT-4、DeepSeek一本正经胡说八道的真相
ai·ai基础
ServBay1 天前
打通 AI 编程本地运维边界,利用 MCP 协议简化环境与服务管理
后端·ai编程·mcp
JaguarJack1 天前
Openai Codex 重大更新 已支持接入任意开源大模型
ai·openai·codex
clint4562 天前
C++进阶(1)——前景提要
c++
夜悊2 天前
C++代码示例:进制数简单生成工具
c++
郝学胜_神的一滴2 天前
CMake 021: IF 条件判据详诠
c++·cmake
Artech2 天前
[MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF
ai·c#·agent·agent skills·maf
岳小哥AI2 天前
读懂计算机视觉CV、语言感知(ASR/TTS)、多模态,就能理解AI是如何“看到”与“听到”世界的
ai·ai基础
_wyt0012 天前
洛谷 B3930 [GESP202312 五级] 烹饪问题 题解
c++·gesp