【AI开发】MCP 介绍与实践—— C++实现简易mcp服务
一、前言
这份教程的目标有两个:
- 讲清楚 MCP 是什么、解决什么问题、消息是怎么流动的。
- 用 C++ 实现一个最小可用的 MCP 服务,并说明如何接到 Codex CLI。
一、MCP 的概念
MCP,全称 Model Context Protocol,可以理解成:让大模型和外部工具、外部数据源用统一方式说话的协议。
如果没有 MCP,大模型要调用一个工具,通常会遇到这些问题:
- 每个工具的调用方式都不一样
- 有的走命令行,有的走 HTTP,有的走本地脚本
- 工具返回格式不统一
- 客户端很难自动发现“这个服务到底提供了哪些能力”
MCP 想解决的就是这些问题,它给出了一套统一规则:
- 客户端怎么发现服务提供了哪些工具
- 客户端怎么调用工具
- 服务端怎么返回结果
- 双方怎么在建立连接时确认自己支持哪些能力
官方文档把 MCP 说明为一个由多层组成的协议,至少包括:
- 基础消息协议
- 生命周期管理
- 服务端能力,例如 tools、resources、prompts

参考:
- Overview: https://modelcontextprotocol.io/specification/2025-11-25/basic
- Architecture overview: https://modelcontextprotocol.io/docs/learn/architecture
二、MCP 里的角色
MCP里面主要可以抽象出3个角色:客户端、服务端、工具,下面详细介绍
2.1 客户端
客户端是“使用 MCP 服务的一方”,比如:
- Codex CLI
- Claude Code
- Trae IDE
客户端负责:
- 启动本地 MCP 服务,或者连接远程 MCP 服务
- 读取服务能力
- 在合适的时候调用工具
2.2 服务端
服务端是“提供能力的一方”,它负责:
- 告诉客户端自己有哪些工具
- 接收工具调用请求
- 执行业务逻辑
- 返回结构化结果
2.3 工具
工具就是服务端暴露出来的可调用动作,例如:
add_numberquery_weathersearch_infomation
工具可以理解成“远程函数”或者“协议化函数入口”。
三、MCP 最常见的三类能力
MCP 里最常见的是这三类:
toolsresourcesprompts
3.1 tools
工具,重点是“执行动作”,例如:
- 转码视频
- 查天气
- 调数据库查询
- 创建文件
3.2 resources
资源,重点是“读取内容”,例如:
- 读取某个文件
- 读取一段数据库记录
- 提供某个 URI 对应的数据
3.3 prompts
提示模板,重点是“给客户端一套可复用提示结构”
四、 MCP 底层通讯格式
MCP 的消息格式基于 JSON-RPC 2.0,也就是说,双方传的每条消息本质上都是 JSON 对象,常见字段有:
jsonrpcidmethodparamsresulterror
官方说明:
- 所有消息都必须符合 JSON-RPC 2.0
- JSON-RPC 消息必须用 UTF-8 编码
参考:
- Overview: https://modelcontextprotocol.io/specification/2025-11-25/basic
- Schema reference: https://modelcontextprotocol.io/specification/2025-11-25/schema
五、stdio 是什么
本地 MCP 最常见的传输方式是 stdio,意思很简单:
- 客户端启动一个子进程
- 服务端从
stdin读请求 - 服务端往
stdout写响应
官方 draft 传输说明里,stdio 的关键点是:
- 服务端从
stdin读 JSON-RPC 消息 - 服务端往
stdout写 JSON-RPC 消息 - 一条消息一行
stdout不能输出任何非协议内容- 日志应该写到
stderr
参考:
- Transports: https://modelcontextprotocol.io/specification/draft/basic/transports
因此,如果你的服务端往 stdout 打了调试日志,就可能把协议打坏。
六、MCP 调用流程
先看最基础、最常见、最容易理解的流程。
6.1 初始化
客户端先发:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "example-client",
"version": "1.0.0"
}
}
}
字段解析:
- “jsonrpc”: “2.0”:固定不变,表示这是 JSON-RPC 2.0 格式,MCP 就是基于这个协议的。
- “id”: 1,客户端给这次请求的编号,你回复时必须把 id 原样返回,不然客户端不认。
- “method”: "initialize"握手,客户端说:我要连接你了,我们先对一下版本和信息。
- “params”: { … } 客户端告诉你 3 件事:
- “protocolVersion”: “2024-11-05”,我用的 MCP 版本是 2024-11-05,你必须回复同样的版本,不然连不上。
- “capabilities”: {},我支持的功能是空的(代表只支持基础功能)
- “clientInfo”,客户端自我介绍:我是谁(name)我的版本(version)比如:Claude、Cursor、自研客户端 都会发这个
服务端回:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {
"listChanged": false
}
},
"serverInfo": {
"name": "cpp-demo",
"version": "0.1.0"
}
}
}
字段解析:
- “jsonrpc”: “2.0”:固定不变,表示这是 JSON‑RPC 2.0 格式,MCP 就是基于这个协议的。
- “id”: 1,客户端给这次请求的编号,你回复时必须把 id 原样返回,不然客户端不认。
- “result”: {…},服务端初始化成功返回的数据体,握手成功必须返回 result,失败返回 error。
- “protocolVersion”: “2024‑11‑05”,我支持的 MCP 版本是 2024‑11‑05,必须和客户端版本一致,不然连不上。
- “capabilities”: {…},服务端告诉客户端自身具备的能力:
- “tools”: {…},声明本 MCP 服务提供工具调用能力。
- “listChanged”: false,代表工具列表静态不变,不会运行时动态增删工具。
- “serverInfo”: {…},服务端自我介绍:我是谁(name)、我的版本(version),用于客户端识别服务。
然后客户端再发一个通知:
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
字段解析
- “jsonrpc”: “2.0”:固定不变,表示这是 JSON‑RPC 2.0 格式,MCP 就是基于这个协议的。
- “method”: “notifications/initialized”:通知类型消息,客户端告知服务端初始化握手已完成,连接正式就绪
上述过程是一个典型的MCP初始化的握手过程
参考:
- Lifecycle: https://modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle
6.2 可用工具查询
客户端发:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}
字段解析:
- “jsonrpc”: “2.0”:固定不变,表示这是 JSON-RPC 2.0 格式,MCP 就是基于这个协议的。
- “id”: 2,客户端给这次请求的编号,你回复时必须把 id 原样返回,不然客户端不认。
- “method”: “tools/list”:获取工具列表,客户端说:请把你提供的所有工具告诉我,我要让 AI 使用。
服务端回:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "echo_upper",
"description": "把输入文本转成大写",
"inputSchema": {
"type": "object",
"properties": {
"text": {
"type": "string"
}
},
"required": ["text"]
}
}
]
}
}
字段解析:
- “jsonrpc”: “2.0”:固定不变,表示这是 JSON-RPC 2.0 格式,MCP 就是基于这个协议的。
- “id”: 2,客户端给这次请求的编号,你回复时必须把 id 原样返回,不然客户端不认。
- “result”: {…}:服务端成功返回工具列表数据,请求正常处理。
- “tools”:
[...]:服务端把自己支持的所有工具列表返回给客户端。- “name”: “echo_upper”:工具的唯一名称,客户端通过这个名字调用工具。
- “description”: “把输入文本转成大写”:工具的功能描述,给大模型理解用途。
- “inputSchema”:工具的入参格式定义,告诉客户端和大模型需要传什么参数。
- “type”: “object”:参数是一个对象格式。
- “properties”: {“text”: { “type”: “string”} }:定义参数 text 是字符串类型。
- “required”:
["text"]:声明 text 是必传参数,不传会报错。
6.3 调用工具
客户端发:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "echo_upper",
"arguments": {
"text": "hello mcp"
}
}
}
字段解析:
- “jsonrpc”: “2.0”:固定不变,表示这是 JSON-RPC 2.0 格式,MCP 就是基于这个协议的。
- “id”: 3,客户端给这次请求的编号,你回复时必须把 id 原样返回,不然客户端不认。
- “method”: “tools/call”:执行工具调用,客户端说:我现在要调用你提供的工具。
- “params”: {…}:客户端告诉你本次调用的详细信息:
- “name”: “echo_upper”:要调用的工具名称,服务端根据名字反射执行对应函数。
- “arguments”: {“text”: “hello mcp”}:传给工具的实际参数,包含必填字段 text。
服务端回:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "HELLO MCP"
}
],
"isError": false
}
}
这就是一个最小工具服务的核心闭环。
七、实现一个简易MCP服务
7.1 实现目标
这里简单使用C++实现一个MCP服务,只实现tool,而 resources 和 prompts 暂不涉及,因为对初学者来说,tools 最直接。
tools 的思维模型很简单:
- 定义一个动作
- 列出来
- 按名字调用
- 返回结果
简易的MCP服务只需要支持以下功能即可:
- 支持
stdio - 支持
initialize - 支持
notifications/initialized - 支持
tools/list - 支持
tools/call - 只暴露 1 个简单工具
实现后需要确认:
- 进程能启动
- 客户端能发现工具
- 客户端能调用工具
这一步通了,基本就可以让agent接入我们的mcp服务了
7.2 代码实现
这里我们实现一个最小服务,名字叫 cpp-demo,它只提供 1 个工具:echo_upper,这个工具做的事很简单:输入一段字符串,返回它的大写版本
为了把例子写清楚,我们用一个非常常见的 JSON 库:nlohmann/json
项目地址:
- https://github.com/nlohmann/json
完整 C++ 示例代码如下:
#include <algorithm>
#include <cctype>
#include <iostream>
#include <string>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
static void log_error(const std::string& message) {
std::cerr << "[mcp-cpp-demo] " << message << std::endl;
}
static std::string to_upper_copy(std::string s) {
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char ch) {
return static_cast<char>(std::toupper(ch));
});
return s;
}
static json make_error_response(const json& id, int code, const std::string& message) {
return {
{"jsonrpc", "2.0"},
{"id", id},
{"error", {
{"code", code},
{"message", message}
}}
};
}
static json make_initialize_response(const json& id) {
return {
{"jsonrpc", "2.0"},
{"id", id},
{"result", {
{"protocolVersion", "2024-11-05"},
{"capabilities", {
{"tools", {
{"listChanged", false}
}}
}},
{"serverInfo", {
{"name", "cpp-demo"},
{"version", "0.1.0"}
}}
}}
};
}
static json make_tools_list_response(const json& id) {
return {
{"jsonrpc", "2.0"},
{"id", id},
{"result", {
{"tools", json::array({
{
{"name", "echo_upper"},
{"description", "把输入文本转成大写"},
{"inputSchema", {
{"type", "object"},
{"properties", {
{"text", {{"type", "string"}}}
}},
{"required", json::array({"text"})}
}}
}
})}
}}
};
}
static json make_tool_call_response(const json& id, const std::string& text) {
return {
{"jsonrpc", "2.0"},
{"id", id},
{"result", {
{"content", json::array({
{{"type", "text"}, {"text", text}}
})},
{"isError", false}
}}
};
}
static void write_message(const json& message) {
std::cout << message.dump() << '\n';
std::cout.flush();
}
static void handle_message(const json& message) {
if (!message.contains("jsonrpc") || message["jsonrpc"] != "2.0") {
if (message.contains("id"))
write_message(make_error_response(message["id"], -32600, "Invalid Request"));
return;
}
const bool has_id = message.contains("id");
const json id = has_id ? message["id"] : json(nullptr);
const std::string method = message.value("method", "");
if (method == "initialize") {
if (!has_id) return;
write_message(make_initialize_response(id));
return;
}
if (method == "notifications/initialized")
return;
if (method == "tools/list") {
if (!has_id) return;
write_message(make_tools_list_response(id));
return;
}
if (method == "tools/call") {
if (!has_id) return;
if (!message.contains("params") || !message["params"].is_object()) {
write_message(make_error_response(id, -32602, "Invalid params"));
return;
}
const auto& params = message["params"];
const std::string& tool_name = params.value("name", "");
if (tool_name != "echo_upper") {
write_message(make_error_response(id, -32601, "Unknown tool"));
return;
}
if (!params.contains("arguments") || !params["arguments"].is_object()) {
write_message(make_error_response(id, -32602, "Missing arguments"));
return;
}
const auto& arguments = params["arguments"];
if (!arguments.contains("text") || !arguments["text"].is_string()) {
write_message(make_error_response(id, -32602, "Argument 'text' must be a string"));
return;
}
const std::string& input = arguments["text"].get<std::string>();
const std::string output = to_upper_copy(input);
write_message(make_tool_call_response(id, output));
return;
}
if (has_id)
write_message(make_error_response(id, -32601, "Method not found"));
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::string line;
while (std::getline(std::cin, line)) {
if (line.empty()) continue;
try {
json message = json::parse(line);
handle_message(message);
} catch (const std::exception& ex) {
log_error(std::string("JSON parse or handling error: ") + ex.what());
}
}
return 0;
}
这个基于C++的MCP服务实现了:
- 从标准输入按行读取 JSON-RPC
- 处理初始化
- 列出工具
- 调用
echo_upper - 把日志写到标准错误
7.2.1 常见问题
1. 为什么用一行一条消息
因为 stdio 传输的 draft 说明里写得很清楚:
- 一条消息一行
- 不要在消息里塞多行
所以输出时用了:
std::cout << message.dump() << '\n';
2. 为什么日志写 stderr
因为 stdout 是协议通道,如果你这样写:
std::cout << "starting server..." << std::endl;
客户端看到的就不是合法 JSON-RPC 了,协议会坏掉,所以日志必须写到:
std::cerr
3. 为什么 notifications/initialized 不回消息
因为这是通知,不是请求,通知没有 id,不期待响应。
4. 为什么 tools/list 要带 inputSchema
因为客户端和大模型需要知道:
- 这个工具叫什么
- 它做什么
- 它要什么参数
inputSchema就是参数说明书。
八、 本地测试
8.1 手动测试
1. initialize
可以先不用接 Codex CLI,直接手工喂消息,把程序跑起来后,输入一行:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual-test","version":"0.1.0"}}}
可以看到一行响应 JSON:
2. notifications/initialized
{"jsonrpc":"2.0","method":"notifications/initialized"}
这里不应该有响应:
3. tools/list
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
可以看到一行响应 JSON:
4. tools/call
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo_upper","arguments":{"text":"hello mcp"}}}
可以看到一行响应 JSON:
8.2 接入Codex CLI 测试
我编译生成的.exe文件在这个目录:
D:\mcp_example\build\Release\mcp_cpp_demo.exe
那么在 C:\Users\Lenovo\.codex\config.toml 里加:
[mcp_servers.cpp_demo]
type = "stdio"
command = "D:\\mcp_example\\mcp_cpp_demo\\x64\\Debug\\mcp_cpp_demo.exe"
cwd = "D:\\mcp_example"
startup_timeout_sec = 120

字段解析:
[mcp_servers.cpp_demo]:定义一个 MCP 服务,名称为cpp_demo,用于标识当前服务配置。- type:
stdio,固定类型,表示服务通过标准输入输出进行通信,是 MCP 最常用的协议模式。 - command:
D:\\mcp_example\\build\\Release\\mcp_cpp_demo.exe,指定 MCP 服务端可执行文件的绝对路径,客户端会启动该程序。 - cwd:
D:\\mcp_example,设置程序运行的工作目录,所有相对路径都以此目录为基准。 - startup_timeout_sec:
120,客户端启动服务的超时时间(秒),120 秒内服务未正常响应则判定启动失败。
重启 Codex CLI 后,它就会把这个服务当作 MCP server 加载:
让codex调用这个mcp:
九、参考资料
- MCP Overview: https://modelcontextprotocol.io/specification/2025-11-25/basic
- MCP Lifecycle 2024-11-05: https://modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle
- MCP Transports draft: https://modelcontextprotocol.io/specification/draft/basic/transports
- MCP Architecture overview: https://modelcontextprotocol.io/docs/learn/architecture
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)