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 (接收响应和通知)
请求与响应走两条独立通道:
- 通过
sendRequestSSE()发一个普通 POST 请求 - 后台 SSE 监听线程(
processNotificationsSSE())持续接收服务端推送的事件流 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 应用开发的基础设施之一。