AI与MCP协议入门
文章目录
-
- AI/LLM基础概念
- MCP协议概述(由Anthropic制定)
- JSON-RPC 2.0详解
- MCP协议核心方法
-
- initialize —— 能力协商握手
- tools/list —— 列出所有可用工具
- tools/call —— 调用指定工具
- prompts/list —— 列出所有可用提示模板
- prompts/get —— 获取指定提示模板
- resources/list —— 列出所有可读资源
- resources/read —— 读取指定URI资源
- resources/subscribe 和 resources/unsubscribe
- ping —— 心跳检测
- logging/setLevel —— 设置日志级别
- completion/complete —— 自动补全
- roots/list —— 根URI列表
- MCP通知机制
-
- notifications/initialized
- notifications/progress —— 带进度令牌的进度通知
- notifications/message —— 日志消息推送
- notifications/tools/list_changed —— 工具列表变更
- notifications/prompts/list_changed —— 提示列表变更
- notifications/resources/list_changed —— 资源列表变更
- notifications/cancelled
- notifications/roots/list_changed
- notifications/resources/updated
- 通知发送机制分析
- MCP能力协商
- 提示工程基础
- 资源URI方案
AI/LLM基础概念
什么是大语言模型(LLM)
当前主流的LLM产品大语言模型(Large Language Model,简称LLM)是一种基于深度学习的AI模型,核心能力是预测下一个词。给定一段文本,它能以极高的准确率继续生成合理的后续内容。通俗来说,可以把LLM想象成一个"超级完形填空机器":给它一个开头,它会用海量训练数据中学到的语言模式填充接下来最可能出现的词,逐个词地生成完整回复。
- Claude — 由Anthropic公司开发,注重安全性和有益性,也是MCP协议的制定者
- GPT — 由OpenAI公司开发(如GPT-4、GPT-4o),业界知名度最高
- Gemini — 由Google公司开发,前身为Bard
- Llama — 由Meta公司开源的LLM系列
- DeepSeek — 由中国深度求索公司开发,开源且性能强大
- 通义千问(Qwen) — 由阿里云开发
LLM如何工作
Token(词元)
LLM不直接处理"文字",而是处理Token(词元)。Token是文本的最小处理单元:
- 一个英文单词通常等于1-2个token(如 “hello” = 1 token, “unbelievable” = 3 tokens)
- 一个中文字通常等于1-2个token(如 “人工智能” = 4 tokens)
- Token数量决定了LLM"看到"了多少信息,也直接决定了API调用成本
输入文本: "Hello, how are you?"
↓ Token化
Token序列: [15496, 11, 1268, 527, 499, 30]
上下文窗口(Context Window)
上下文窗口是LLM在一次对话中能够"记住"的最大token数量。它就像一个滑动窗口——当对话超过窗口容量,最早的内容会被"遗忘"。
- Claude 3 系列:200K tokens(约15万英文单词或一本中等厚度的书)
- GPT-4 Turbo:128K tokens
- Gemini 1.5 Pro:100万tokens(业界最大)
上下文窗口包含:
- System Prompt(系统提示)—— 设定AI的行为规则
- 历史对话记录
- 当前用户输入
- 工具调用结果(Function Calling返回的数据)
- MCP资源内容
推理(Inference)
推理是指LLM根据输入的token序列,逐token生成输出文本的过程。每次生成一个token时,模型会根据已生成的内容计算下一个最可能出现的token。这个过程持续进行,直到模型生成一个特殊的"停止token"或达到最大长度限制。
输入: "法国的首都是"
推理过程:
第1步: 计算下一个token概率 → "巴" (概率 0.42)
第2步: 计算下一个token概率 → "黎" (概率 0.38)
第3步: 计算下一个token概率 → <停止> (概率 0.95)
输出: "巴黎"
关键点:LLM的每次推理是无状态的——它不会"记住"上一次推理的结果。你看到的"对话记忆"效果,其实是通过把历史对话重新打包进上下文窗口实现的。每一次发消息给LLM,都需要把所有历史记录+system prompt+新消息一起发送。
Function Calling / Tool Use 是什么
Function Calling(在Anthropic生态中称为Tool Use)是让LLM能够调用外部函数/API的机制。没有这个能力,LLM只能根据训练数据生成文本;有了这个能力,LLM可以:
- 查询实时天气(训练数据截止于某个时间点,不知道今天的天气)
- 执行数学计算(LLM本身不擅长精确计算)
- 读取文件内容
- 调用数据库
- 发送邮件
运作流程:
1. 用户: "北京今天天气怎么样?"
2. LLM: 我需要调用天气函数,但我不知道参数 → 返回function_call
→ { "name": "get_weather", "arguments": { "city": "北京" } }
3. 客户端: 实际执行 get_weather("北京") → 返回 {"temperature": 25, "condition": "晴"}
4. 客户端把结果传回LLM,LLM基于结果生成最终回复
5. LLM: "北京今天天气晴朗,气温25°C。"
在Claude的API中,Tool Use定义包含:
name:工具名称description:工具描述,帮助LLM理解何时使用input_schema:JSON Schema格式的输入参数定义
本项目中的体现:PluginAPI.h 第46-50行的 PluginTool 结构体正是对Tool Use概念的实现:
// PluginAPI.h 第46-50行
typedef struct {
const char* name; // 工具名称,如 "get_weather"
const char* description; // 工具描述
const char* inputSchema; // JSON Schema格式的参数定义
} PluginTool;
以Weather插件(plugins/weather/Weather.cpp 第31-47行)为例,它定义了一个 get_weather 工具,参数的JSON Schema描述了需要 city、latitude、longitude 三个字段。
RAG(检索增强生成)概念
RAG(Retrieval-Augmented Generation,检索增强生成)是解决LLM"知识截止日期"和"幻觉"问题的关键技术。
工作流程:
1. 用户提问
2. 系统从外部知识库检索相关文档(向量搜索)
3. 将检索到的文档片段拼接到Prompt中
4. LLM基于检索的内容 + 自身知识生成回答
5. 返回带着引用的回答
为什么需要RAG:
- LLM的训练数据有截止日期(如Claude的训练数据可能截止于2024年初)
- LLM会产生"幻觉"(自信地编造不存在的事实)
- 企业内部知识、私有文档不在LLM训练数据中
- 需要可溯源的引用
MCP与RAG的关系:MCP的Resources机制本质上就是一种RAG实现方式——MCP Server通过 resources/list 和 resources/read 让LLM能够检索外部数据源,实现检索增强。
为什么LLM需要外部工具和资源
LLM是"缸中之脑"——只能在训练数据的范围内回答问题。外部工具和资源赋予LLM与真实世界交互的能力:
| 能力缺口 | 外部工具解决方案 | 本项目示例 |
|---|---|---|
| 实时数据 | 调用API获取 | Weather插件调用Open-Meteo API |
| 精确计算 | 调用计算器/代码执行 | 工具类型插件 |
| 文件操作 | 资源读写 | BacioQuote资源URI |
| 外部知识 | RAG检索 | Resources/read机制 |
| 持续通信 | 通知/推送 | Notification插件 |
| 专业领域 | 提示模板/Prompt | CodeReview提示模板 |
System Prompt vs User Prompt
在LLM交互中,消息有不同的角色(Role):
- System Prompt(系统提示):设定AI的基础行为规则、人设、知识范围、输出格式等。这是最先发送的,权重最高。
- User Prompt(用户提示):用户的实际问题或指令。
- Assistant(助手回复):LLM之前生成的回复,在对话历史中回传。
// 一条典型的LLM API请求
{
"model": "claude-sonnet-4-20250514",
"system": "你是一个专业的C++代码审查助手。回答要简洁、技术精确。",
"messages": [
{"role": "user", "content": "这段代码有内存泄漏吗?"},
{"role": "assistant", "content": "让我分析一下..."},
{"role": "user", "content": "好的,请继续"}
]
}
System Prompt的变化:
- 实际API中,system参数是单独的顶层字段(Anthropic API)
- 在对话消息格式中,system也作为
role: "system"出现(OpenAI API) - MCP的Prompt机制返回的消息数组中可以使用
role: "system"或role: "user"
本项目的体现:CodeReview插件生成的消息使用 role: "user",将提示作为用户消息嵌入对话中(plugins/code-review/CodeReview.cpp 第58-63行)。
MCP协议概述(由Anthropic制定)
MCP是什么:Model Context Protocol
MCP(Model Context Protocol,模型上下文协议) 是由Anthropic公司于2024年11月发布并开源的一套开放协议标准。它定义了LLM应用与外部工具、数据源之间的标准化通信方式。
核心思想可以用"AI世界的USB-C接口"来类比:
没有MCP之前: 有了MCP之后:
LLM → 专用接口1 → 数据库A LLM → MCP Client
LLM → 专用接口2 → 文件系统B ↕ MCP协议(标准化JSON-RPC通信)
LLM → 专用接口3 → API服务C MCP Server A → 数据库A
LLM → 专用接口4 → 搜索引擎D MCP Server B → 文件系统B
MCP Server C → API服务C
每个集成都需要单独开发 MCP Server D → 搜索引擎D
一次对接,所有Server通用
本项目的MCP Server(版本0.8.0,见 build/version.h 第4行)正是MCP协议的C++实现,它提供了插件化的架构,允许通过动态库(.dll/.so)扩展功能。
MCP解决了什么问题(LLM孤岛问题)
:::color4
MCP解决的核心问题是LLM孤岛——每个LLM产品各自定义自己的工具/数据接入方式,导致:
- 重复开发:为Claude开发一个天气查询插件后,想给GPT用就得重写
- 厂商锁定:一旦在某个生态投入大量插件开发,迁移成本极高
- 碎片化:没有统一标准,社区力量分散
- 安全隐患:每个LLM产品都有自己的权限模型,安全审计困难
:::
:::color1
MCP通过标准化解决了这些问题:
- 统一接口:所有LLM Client使用相同的JSON-RPC 2.0协议与MCP Server通信
- 一次开发,多处运行:一个MCP Server可以被Claude Desktop、Zed、Sourcegraph Cody等所有MCP兼容客户端使用
- 能力抽象:通过Tools/Prompts/Resources三层抽象覆盖所有LLM需要的外部能力
- 传输无关:同一套协议可以通过stdio(标准输入输出)、SSE(Server-Sent Events)、HTTP Stream等多种传输方式工作
:::
MCP的三层抽象:Tools / Prompts / Resources
MCP协议将LLM需要的外部能力抽象为三个核心概念:
┌──────────────────────────────┐
│ MCP Server │
│ │
┌─────────────┐ │ ┌─────────┐ ┌───────────┐ │
│ Tools │─┼─>│ 执行操作 │ │ 天气查询 │ │
│ 工具 │ │ └─────────┘ │ 发送邮件 │ │
└─────────────┘ │ │ 文件操作 │ │
│ └───────────┘ │
┌─────────────┐ │ ┌─────────┐ ┌───────────┐ │
│ Prompts │─┼─>│ 提供模板 │ │ 代码审查 │ │
│ 提示模板 │ │ └─────────┘ │ 文档生成 │ │
└─────────────┘ │ └───────────┘ │
│ │
┌─────────────┐ │ ┌─────────┐ ┌───────────┐ │
│ Resources │─┼─>│ 提供数据 │ │ 名言引用 │ │
│ 资源 │ │ └─────────┘ │ 文件内容 │ │
└─────────────┘ │ └───────────┘ │
└──────────────────────────────┘
Tools(工具)—— 让LLM执行操作
工具是面向LLM的可执行函数。LLM可以"决定"调用某个工具,服务器执行后返回结果。
特征:
- 有副作用(可以改变外部状态,如发送邮件)
- 需要参数定义(JSON Schema)
- 调用结果返回给LLM继续处理
本项目示例:PluginAPI.h 第40行定义 PLUGIN_TYPE_TOOLS = 0。Weather插件(plugins/weather/Weather.cpp 第31-47行)定义了名为 get_weather 的工具。
Prompts(提示模板)—— 提供预定义的对话模板
提示模板是预定义的、参数化的对话模板。用户可以从中选择,填入参数后获得结构化的对话。
特征:
- 无副作用(只返回消息文本)
- 包含参数定义(可选的占位符)
- 返回messages数组,包含role(system/user/assistant)和content
本项目示例:PluginAPI.h 第41行定义 PLUGIN_TYPE_PROMPTS = 1。CodeReview插件(plugins/code-review/CodeReview.cpp 第32-42行)定义了名为 code-review 的提示模板,接受 language 参数。
Resources(资源)—— 提供只读数据
资源是服务器向LLM暴露的只读数据。通过URI标识,支持文本和二进制格式。
特征:
- 只读(不能修改)
- 通过URI访问(如
bacio:///quote) - 支持多种MIME类型(text/plain, image/png, audio/wav等)
- 支持订阅/更新通知
本项目示例:PluginAPI.h 第42行定义 PLUGIN_TYPE_RESOURCES = 2。BacioQuote插件(plugins/bacio-quote/BacioQuote.cpp 第183-190行)定义了一个 bacio:///quote 资源。
MCP的价值:标准化LLM与外部世界的交互
| 价值维度 | 说明 |
|---|---|
| 开放标准 | 任何人可以实现MCP Server或Client,无需授权费 |
| 生态效应 | 社区可以共享插件,类似VSCode扩展市场 |
| 传输灵活性 | 支持stdio、SSE、HTTP Stream,适应不同部署场景 |
| 安全可控 | 权限由客户端管理,服务器不直接暴露给LLM |
| 动态发现 | LLM可通过tools/list等接口自动发现可用能力 |
| 热插拔 | 协议支持运行时添加/移除工具和资源 |
MCP生态:Server端(本项目)/ Client端(Claude Desktop等)
┌─────────────────────────────────────────────────────────┐
│ MCP 生态 │
│ │
│ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ MCP Client 端 │ JSON-RPC │ MCP Server 端 │ │
│ │ │<────────>│ │ │
│ │ Claude Desktop │ 2.0 │ 本项目 (C++ Server) │ │
│ │ Zed Editor │ │ 社区 Python Server │ │
│ │ Sourcegraph │ │ 社区 Node.js Server │ │
│ │ Continue.dev │ │ ... │ │
│ └─────────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Client端职责:
- 发现并连接MCP Server
- 发起
initialize握手,协商协议版本和能力 - 将Server暴露的Tools注册给LLM
- 代理LLM的Tool调用请求到Server
- 管理权限和安全性
Server端职责(本项目):
- 实现JSON-RPC 2.0消息处理(
src/server/Server.cpp) - 通过插件系统暴露Tools/Prompts/Resources(
src/loader/PluginsLoader.cpp) - 支持多种传输方式:stdio、SSE、HTTP Stream(
src/transport/) - 管理插件热加载和生命周期(
src/loader/PluginsLoader.h)
本项目的传输方式通过命令行参数切换(src/main.cpp 第120-126行):
- 默认使用 stdio(标准输入输出),适用于本地进程通信
-s参数启用 SSE Server,适用于Web/远程通信-t参数启用 HTTP Stream Server
JSON-RPC 2.0详解
MCP协议的通信层基于JSON-RPC 2.0规范。这是一种轻量级的远程过程调用协议,使用JSON格式编码消息。
JSON-RPC 2.0 四种消息类型
JSON-RPC 2.0定义了四种标准消息类型。所有消息都是JSON对象,必须包含 "jsonrpc": "2.0" 字段。
Request(请求)
Request是客户端向服务器发起的调用请求。必须包含唯一的 id 字段用于匹配响应。
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"city": "北京",
"latitude": "39.9042",
"longitude": "116.4074"
}
}
}
字段说明:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
| jsonrpc | string | 是 | 固定为 “2.0” |
| id | number/string | 是 | 请求标识符,用于匹配Response |
| method | string | 是 | 调用的方法名(如 “tools/call”) |
| params | object/array | 否 | 方法参数 |
Response(成功响应)
当请求处理成功时,服务器返回Response。id 必须与对应Request的 id 匹配。
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{"type": "text", "text": "北京今天天气晴朗,气温25°C。"}
],
"isError": false
}
}
本项目的实现:src/utils/MCPBuilder.h 第45-51行
// MCPBuilder.h 第45-51行
static json Response(json request) {
json response;
response["jsonrpc"] = "2.0";
response["id"] = request["id"]; // 回传请求id
response["result"] = json::object(); // 初始化为空对象
return response;
}
Error(错误响应)
当请求处理失败时,服务器返回Error。error对象必须包含 code 和 message。
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32601,
"message": "Method not found"
}
}
JSON-RPC 2.0 标准错误码:
| 错误码 | 常量名 | 含义 |
|---|---|---|
| -32700 | ParseError | JSON解析失败(格式错误) |
| -32600 | InvalidRequest | 请求对象无效(如缺少method字段) |
| -32601 | MethodNotFound | 请求的方法不存在 |
| -32602 | InvalidParams | 方法参数无效 |
| -32603 | InternalError | 服务器内部错误 |
| -32000 ~ -32099 | (保留) | 服务器自定义错误 |
本项目的错误码定义:src/utils/MCPBuilder.h 第37-43行
// MCPBuilder.h 第37-43行
enum ErrorCode {
ParseError = -32700, // JSON解析错误
InvalidRequest = -32600, // 请求无效
MethodNotFound = -32601, // 方法未找到
InvalidParams = -32602, // 参数无效
InternalError = -32603 // 内部错误
};
项目的Error构建方法:src/utils/MCPBuilder.h 第53-59行
// MCPBuilder.h 第53-59行
static json Error(ErrorCode code, const std::string& id, const std::string &message) {
return {
{"jsonrpc", "2.0"},
{"error", {{"code", code}, {"message", message}}},
{"id", id}
};
}
项目中的实际错误使用:src/server/Server.cpp
- 第273行:缺少method字段时返回
InvalidRequest错误
if (!request.contains("method")) {
return MCPBuilder::Error(MCPBuilder::InvalidRequest, request["id"], "Missing method");
}
- 第293行:方法未注册时返回
MethodNotFound错误
return MCPBuilder::Error(MCPBuilder::MethodNotFound, std::to_string(id), "Method not found");
- 第434行:
resources/subscribe未实现时返回MethodNotFound - 第439行:
resources/unsubscribe未实现时返回MethodNotFound - 第451-463行:
prompts/get、logging/setLevel、completion/complete、roots/list默认返回MethodNotFound
Notification(通知)
Notification是没有 id 字段的Request。服务器收到后不需要响应。用于单向事件推送(进度、日志、状态变更)。
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progressToken": "abc123",
"progress": 50,
"total": 100,
"message": "Processing..."
}
}
关键特征:没有 id 字段 —— 这是区分Notification和Request的唯一方式。
本项目中的Notification处理:src/server/Server.cpp 第466-500行。所有通知处理方法都返回 nullptr(即不发送响应)。
项目中的JSON-RPC实际使用
HandleRequest —— 请求分发中枢
src/server/Server.cpp 第263-294行的 HandleRequest 方法是整个服务器的请求入口:
// Server.cpp 第263-294行
json Server::HandleRequest(const json &request) {
// 1. 校验:必须有method字段
if (!request.contains("method")) {
return MCPBuilder::Error(MCPBuilder::InvalidRequest,
request["id"], "Missing method");
}
// 2. 通过functionMap查找并执行处理器
std::string methodName = request["method"];
auto it = functionMap.find(methodName);
if (it != functionMap.end()) {
json response = it->second(request);
// 如果响应为nullptr(通知),不发送
if (response != nullptr) { /* 发送响应 */ }
return response;
}
// 3. 方法未找到
return MCPBuilder::Error(MCPBuilder::MethodNotFound,
std::to_string(id), "Method not found");
}
functionMap —— 方法注册表
src/server/Server.cpp 第34-58行的 functionMap 是核心方法路由表,将JSON-RPC method字符串映射到实际的处理函数:
// Server.cpp 第35-57行
functionMap = {
{"initialize", → InitializeCmd},
{"ping", → PingCmd},
{"tools/list", → ToolsListCmd},
{"tools/call", → ToolsCallCmd},
{"resources/list", → ResourcesListCmd},
{"resources/read", → ResourcesReadCmd},
{"resources/subscribe", → ResourcesSubscribeCmd},
{"resources/unsubscribe", → ResourcesUnsubscribeCmd},
{"prompts/list", → PromptsListCmd},
{"prompts/get", → PromptsGetCmd},
{"logging/setLevel", → LoggingSetLevelCmd},
{"completion/complete", → CompletionCompleteCmd},
{"roots/list", → RootsListCmd},
// ... 通知类型
{"notifications/initialized", → NotificationInitializedCmd},
{"notifications/cancelled", → NotificationCancelledCmd},
{"notifications/progress", → NotificationProgressCmd},
// ...
};
MCPBuilder —— 完整的消息构建工具集
src/utils/MCPBuilder.h 提供了所有MCP协议消息类型的构建方法:
| 方法 | 行号 | 用途 |
|---|---|---|
Response() |
45-51 | 构建成功响应 |
Error() |
53-59 | 构建错误响应 |
TextContent() |
61-66 | 构建文本内容块 |
ImageContent() |
68-75 | 构建图片内容块(Base64编码) |
AudioContent() |
77-84 | 构建音频内容块(Base64编码) |
ResourceText() |
86-92 | 构建资源文本内容 |
NotificationLog() |
94-100 | 构建日志通知 |
NotificationProgress() |
102-108 | 构建进度通知 |
NotificationToolsListChanged() |
111-116 | 构建工具列表变更通知 |
NotificationPromptsListChanged() |
118-123 | 构建提示列表变更通知 |
NotificationResourcesListChanged() |
125-130 | 构建资源列表变更通知 |
MCP协议核心方法
MCP协议定义了一系列标准方法(method),分为请求(Request)和通知(Notification)两类。以下分析所有核心方法和本项目中的实现。
initialize —— 能力协商握手
用途:客户端连接后必须首先调用的方法,用于协商协议版本和能力集。
请求参数:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": { /* 客户端能力 */ },
"clientInfo": {
"name": "Claude Desktop",
"version": "1.0.0"
}
}
}
服务器响应(包含服务器的能力声明):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {"listChanged": true},
"prompts": {"listChanged": true},
"resources": {"subscribe": true, "listChanged": true},
"logging": {}
},
"serverInfo": {
"name": "mcp-server",
"version": "0.8.0"
}
}
}
项目实现:src/server/Server.cpp 第304-385行的 InitializeCmd 方法。这个方法是最复杂的处理器之一,完整解析客户端发送的参数(rootUri、rootPath、initializationOptions、capabilities、trace、workspaceFolders),并在第374-384行构建响应声明服务器的能力集。
// Server.cpp 第374-384行 —— 响应构建核心代码
response["result"]["protocolVersion"] = request["params"]["protocolVersion"];
response["result"]["capabilities"]["tools"] = json::object({{"listChanged", true}});
response["result"]["capabilities"]["prompts"] = json::object({{"listChanged", true}});
response["result"]["capabilities"]["resources"]["subscribe"] = true;
response["result"]["capabilities"]["resources"]["listChanged"] = true;
response["result"]["capabilities"]["logging"] = json::object();
response["result"]["serverInfo"]["name"] = name_;
response["result"]["serverInfo"]["version"] = PROJECT_VERSION;
tools/list —— 列出所有可用工具
用途:让客户端(LLM)发现服务器提供的所有工具及其参数定义。
请求:
{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
响应示例:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "get_weather",
"description": "获取指定城市的天气预报",
"inputSchema": {
"type": "object",
"properties": {
"city": {"type": "string"},
"latitude": {"type": "string"},
"longitude": {"type": "string"}
},
"required": ["city", "latitude", "longitude"]
}
}
]
}
}
项目实现:Server.cpp第407-413行的默认实现返回空数组。在 main.cpp 第194-214行通过 OverrideCallback 被替换为实际从插件中收集所有 PLUGIN_TYPE_TOOLS 类型的工具:
// main.cpp 第194-214行
server->OverrideCallback("tools/list", [](const json& request) {
nlohmann::ordered_json response = MCPBuilder::Response(request);
response["result"]["tools"] = json::array();
auto plugins = loader->GetPluginsSnapshot(); // 获取插件快照
for (const auto& plugin : plugins) {
if (plugin->instance->GetType() == PLUGIN_TYPE_TOOLS) {
for (int i = 0; i < plugin->instance->GetToolCount(); i++) {
nlohmann::ordered_json tool;
auto pluginTool = plugin->instance->GetTool(i);
tool["name"] = pluginTool->name;
tool["description"] = pluginTool->description;
tool["inputSchema"] = json::parse(pluginTool->inputSchema);
response["result"]["tools"].push_back(tool);
}
}
}
return response;
});
tools/call —— 调用指定工具
用途:让客户端触发具体的工具执行。
请求:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {"city": "北京", "latitude": "39.9042", "longitude": "116.4074"}
}
}
响应(MCP规范定义的内容格式):
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{"type": "text", "text": "北京今天天气晴朗,25°C。"}
],
"isError": false
}
}
项目实现:src/server/Server.cpp 第415-430行默认实现返回占位提示。在 main.cpp 第215-245行通过 OverrideCallback 替换为实际执行逻辑:
// main.cpp 第215-245行 —— 核心逻辑
server->OverrideCallback("tools/call", [](const json& request) {
// ...
for (const auto& plugin : plugins) {
if (plugin->instance->GetType() == PLUGIN_TYPE_TOOLS) {
for (int i = 0; i < plugin->instance->GetToolCount(); i++) {
auto pluginTool = plugin->instance->GetTool(i);
if (pluginTool->name == request["params"]["name"]) {
// 找到匹配的工具,调用插件的HandleRequest
char* res_ptr = plugin->instance->HandleRequest(request.dump().c_str());
// ...
response["result"] = json::parse(res_ptr);
response["result"]["isError"] = false;
delete[] res_ptr;
return response;
}
}
}
}
});
内容类型:MCP支持三种内容块的构建(MCPBuilder.h):
TextContent()(第61-66行)—— 纯文本ImageContent()(第68-75行)—— 图片(Base64编码)AudioContent()(第77-84行)—— 音频(Base64编码)
prompts/list —— 列出所有可用提示模板
用途:列出服务器提供的所有预定义提示模板及其参数。
响应:
{
"result": {
"prompts": [
{
"name": "code-review",
"description": "让LLM分析代码质量并提出改进建议",
"arguments": [
{
"name": "language",
"description": "代码的编程语言",
"required": true
}
]
}
]
}
}
项目实现:main.cpp 第246-265行,遍历所有 PLUGIN_TYPE_PROMPTS 类型的插件收集提示模板。
prompts/get —— 获取指定提示模板
用途:获取特定提示模板的完整消息内容(含参数填充)。
请求:
{
"method": "prompts/get",
"params": {
"name": "code-review",
"arguments": {"language": "C++"}
}
}
响应返回messages数组,包含role和content。
项目实现:main.cpp 第266-291行,匹配提示名称后调用插件的 HandleRequest。
resources/list —— 列出所有可读资源
用途:列出服务器提供的所有只读数据资源(URI列表)。
响应:
{
"result": {
"resources": [
{
"name": "bacio-quote",
"description": "A list of the famous italian bacio perugina quotes",
"uri": "bacio:///quote",
"mimeType": "text/plain"
}
]
}
}
项目实现:main.cpp 第292-312行,收集所有 PLUGIN_TYPE_RESOURCES 类型插件的资源。
resources/read —— 读取指定URI资源
用途:读取指定URI的资源内容(文本或二进制)。
请求:
{
"method": "resources/read",
"params": {"uri": "bacio:///quote"}
}
响应返回contents数组,每个元素包含uri、mimeType和实际内容(text字段用于文本,blob字段用于二进制)。
项目实现:main.cpp 第313-337行,匹配URI后调用插件的 HandleRequest。
resources/subscribe 和 resources/unsubscribe
用途:客户端订阅或取消订阅特定URI的资源变更通知。
项目实现:src/server/Server.cpp 第432-440行 —— 这两个方法目前标记为"NOT YET IMPLEMENTED",返回MethodNotFound错误。
ping —— 心跳检测
用途:用于检测连接是否存活。
项目实现:src/server/Server.cpp 第387-393行
json Server::PingCmd(const json &request) {
nlohmann::ordered_json response = {};
response["jsonrpc"] = "2.0";
response["id"] = request["id"];
response["result"] = json::object(); // 空对象表示成功
return response;
}
logging/setLevel —— 设置日志级别
用途:客户端请求服务器调整日志详细程度。
项目实现:src/server/Server.cpp 第454-456行 —— 当前返回MethodNotFound(未实现)。
completion/complete —— 自动补全
用途:为提示参数和资源URI模板提供自动补全候选项。
项目实现:src/server/Server.cpp 第458-460行 —— 当前返回MethodNotFound(未实现)。
roots/list —— 根URI列表
用途:提示服务器哪些根URI是客户端当前关心的。
项目实现:src/server/Server.cpp 第462-464行 —— 当前返回MethodNotFound(未实现)。
MCP通知机制
MCP通知(Notification)是没有 id 字段的JSON-RPC消息,服务器主动推送,客户端不回复。本项目中共有9种通知类型。
notifications/initialized
用途:客户端发送给服务器,告知初始化完成,可以开始正常通信。这是initialize握手的最后一步。
项目实现:src/server/Server.cpp 第466-468行 —— 收到后返回 nullptr(无响应)。
json Server::NotificationInitializedCmd(const json &request) {
return nullptr; // 通知不需要响应
}
notifications/progress —— 带进度令牌的进度通知
用途:在执行长时间任务时,服务器向客户端报告进度。
有效载荷:
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progressToken": "abc123",
"progress": 50,
"total": 100,
"message": "正在处理第5/10项..."
}
}
Builder构建方法:src/utils/MCPBuilder.h 第102-108行
static json NotificationProgress(const std::string& message,
const std::string& progressToken, const int progress, const int total) {
return json::object({
{"jsonrpc", "2.0"},
{"method","notifications/progress"},
{"params", {
{"progressToken", progressToken},
{"progress", progress},
{"total", total},
{"message", message}
}}
});
}
实际使用:Notification插件(plugins/notification/Notification.cpp 第78-115行)。在 progress_test 工具中,它模拟一个10秒的长任务,每秒发送一次进度通知:
// Notification.cpp 第101-113行
for (int i = 1; i <= totalDuration; i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
int progressPercent = (i * 100) / totalDuration;
std::string progressMessage = "Progress: " + std::to_string(progressPercent) + "%";
std::string message = MCPBuilder::NotificationProgress(
progressMessage, progressToken, progressPercent, 100).dump();
g_plugin->notifications->SendToClient(GetNameImpl(), message.c_str());
}
notifications/message —— 日志消息推送
用途:服务器向客户端发送日志消息,支持多个级别(debug、info、notice、warning、error等)。
Builder构建方法:src/utils/MCPBuilder.h 第94-100行
static json NotificationLog(const std::string& level, const std::string& data) {
return json::object({
{"jsonrpc", "2.0"},
{"method","notifications/message"},
{"params", { {"level",level}, {"data", data} }}
});
}
实际使用:Notification插件第74-76行。在 logging_test 工具中发送测试日志。
notifications/tools/list_changed —— 工具列表变更
用途:当服务器的工具列表发生变化时(如插件热加载),通知客户端重新调用 tools/list。
Builder构建方法:src/utils/MCPBuilder.h 第111-116行
static json NotificationToolsListChanged() {
return json::object({
{"jsonrpc", "2.0"},
{"method", "notifications/tools/list_changed"}
});
}
触发时机:main.cpp 第165-179行,插件热加载后自动发送:
loader->SetOnPluginsChanged([](bool toolsChanged, bool promptsChanged, bool resourcesChanged) {
if (server && server->IsValid()) {
if (toolsChanged) {
server->SendNotification("mcp-server",
MCPBuilder::NotificationToolsListChanged().dump().c_str());
}
// ... promptsChanged, resourcesChanged 同理
}
});
notifications/prompts/list_changed —— 提示列表变更
用途:提示模板列表变化通知。构建方法:MCPBuilder.h 第118-123行。
notifications/resources/list_changed —— 资源列表变更
用途:资源列表变化通知。构建方法:MCPBuilder.h 第125-130行。
notifications/cancelled
用途:客户端通知服务器取消正在进行的操作。
项目实现:Server.cpp 第470-472行 —— 返回nullptr(无响应)。
notifications/roots/list_changed
用途:客户端通知服务器根URI列表发生了变化。
项目实现:Server.cpp 第478-480行 —— 返回nullptr。
notifications/resources/updated
用途:通知客户端特定URI的资源内容已更新。
项目实现:Server.cpp 第482-484行 —— 返回nullptr。
通知发送机制分析
项目的通知发送通过以下组件协作完成:
- 插件通知网关:插件通过
NotificationSystem.SendToClient回调发送(PluginAPI.h第66-67行) - Bridge回调:
main.cpp第61-66行的ClientNotificationCallbackImpl桥接 - 通知队列:
Server.h第114行的notification_queue_—— 线程安全的生产者-消费者队列 - Writer线程:
Server.cpp第65-105行的WriterLoop—— 从队列取出通知并写入transport
插件 → SendToClient() → ClientNotificationCallbackImpl() → Server::SendNotification()
↓
notification_queue_
↓
WriterLoop线程
↓
Transport::Write()
MCP能力协商
服务器capabilities结构详解
能力协商(Capability Negotiation)是MCP协议中的关键机制。服务器在 initialize 响应中声明自己支持哪些功能,客户端据此决定后续可以调用哪些方法。
本项目的capabilities声明:Server.cpp 第377-381行
// 服务器声明五种能力:
response["result"]["capabilities"]["tools"] = { {"listChanged", true} };
response["result"]["capabilities"]["prompts"] = { {"listChanged", true} };
response["result"]["capabilities"]["resources"]["subscribe"] = true;
response["result"]["capabilities"]["resources"]["listChanged"] = true;
response["result"]["capabilities"]["logging"] = json::object();
每项能力的具体含义:
| 能力声明 | 含义 |
|---|---|
tools: {listChanged: true} |
服务器支持tools/list_changed通知,工具列表可能动态变化 |
prompts: {listChanged: true} |
服务器支持prompts/list_changed通知,提示列表可能动态变化 |
resources: {subscribe: true} |
服务器支持资源订阅功能 |
resources: {listChanged: true} |
服务器支持resources/list_changed通知 |
logging: {} |
服务器支持日志功能(logging/setLevel方法) |
Server.h中的Capabilities枚举:src/server/Server.h 第41-45行
enum Capabilities {
RESOURCES = 0 << 1, // 位0:资源能力
TOOLS = 0 << 2, // 位1:工具能力
PROMPTS = 0 << 3, // 位2:提示能力
};
协议版本协商
initialize 请求和响应中都有 protocolVersion 字段。当前MCP协议版本为 "2024-11-05"(以发布日期命名)。
服务器在响应中回传客户端请求的协议版本号(Server.cpp 第376行):
response["result"]["protocolVersion"] = request["params"]["protocolVersion"];
如果服务器不支持客户端请求的协议版本,应返回错误。
ServerInfo元数据
服务器在 initialize 响应中提供自身元数据(Server.cpp 第382-383行):
response["result"]["serverInfo"]["name"] = name_; // 从命令行 -n 参数获取,默认 "mcp-server"
response["result"]["serverInfo"]["version"] = PROJECT_VERSION; // 来自 build/version.h,当前为 "0.8.0"
完整握手流程分析
一个完整的MCP连接生命周期如下:
MCP Client MCP Server
(Claude Desktop) (本项目)
│ │
┌───────── 连接建立 ──────────────────────────────────────┐
│ │ │
│ stdio/SSE连接 │ ───── TCP/进程通信 ──────→ │ 启动 Server
│ │ │
├───────── 能力协商 ──────────────────────────────────────┤
│ │ │
│ ① initialize │ ─── Request(id=1) ──────→ │ Server.cpp:304
│ │ │ 解析客户端能力
│ │ │ 声明服务器能力
│ │ ←── Response(id=1) ─────── │ 返回 capabilities
│ │ │
│ ② initialized │ ─── Notification ───────→ │ Server.cpp:466
│ │ │ 返回 nullptr
│ │ │
├───────── 发现阶段 ──────────────────────────────────────┤
│ │ │
│ ③ tools/list │ ─── Request(id=2) ──────→ │ main.cpp:194
│ │ │ 遍历工具插件
│ │ ←── Response(id=2) ─────── │ 返回工具列表
│ │ │
│ ④ prompts/list │ ─── Request(id=3) ──────→ │ main.cpp:246
│ │ │ 遍历提示插件
│ │ ←── Response(id=3) ─────── │ 返回提示列表
│ │ │
│ ⑤ resources/list │ ─── Request(id=4) ──────→ │ main.cpp:292
│ │ │ 遍历资源插件
│ │ ←── Response(id=4) ─────── │ 返回资源列表
│ │ │
├───────── 工作阶段 ──────────────────────────────────────┤
│ │ │
│ ⑥ tools/call │ ─── Request(id=5) ──────→ │ main.cpp:215
│ (LLM决定调用工具) │ │ 插件HandleRequest
│ │ ←── Response(id=5) ─────── │ 返回执行结果
│ │ │
│ ⑦ (可选) 通知 │ ←── Notification ───────── │ 进度/日志/变更
│ │ │
│ ⑧ ping │ ─── Request(id=N) ──────→ │ Server.cpp:387
│ (心跳保活) │ ←── Response(id=N) ─────── │ 返回空result
│ │ │
├───────── 终止 ──────────────────────────────────────────┤
│ │ │
│ 连接断开/Ctrl+C │ │ Stop/StopAsync
│ │ │ 清理插件和线程
└───────────────────────────────────────────────────────┘
关键代码路径:
- 连接建立:
main.cpp第339行server->Connect(transport) - 连接循环:
Server.cpp第127-154行while (!isStopping_)循环 - 通知处理:
Server.cpp第65-105行WriterLoop - 服务器停止:
Server.cpp第220-243行Stop()
提示工程基础
提示模板的JSON结构
MCP的Prompt(提示模板)是一个预定义的、参数化的对话模板。其核心结构定义在 PluginAPI.h 第52-56行:
typedef struct {
const char* name; // 提示模板名称
const char* description; // 模板描述
const char* arguments; // JSON格式的参数定义(JSON数组字符串)
} PluginPrompt;
当客户端调用 prompts/get 获取完整提示内容时,服务器返回:
{
"description": "模板的描述文本",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "实际的消息内容,参数已被替换"
}
}
]
}
messages数组格式(role: system/user/assistant)
Messages数组是提示模板的核心内容。每个消息对象包含两个字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| role | string | 消息角色:"system"、"user" 或 "assistant" |
| content | object | 消息内容,包含 type 和实际内容(text/image/audio) |
三种角色:
- system:设定AI的行为规则、专业领域、输出格式。权重最高。
- user:用户的实际输入或问题。
- assistant:之前AI的回复(用于多轮对话上下文或few-shot示例)。
// 一个包含三种角色的提示模板
{
"messages": [
{
"role": "system",
"content": {
"type": "text",
"text": "你是一个专业的C++代码审查助手。回答要简洁、技术精确。"
}
},
{
"role": "user",
"content": {
"type": "text",
"text": "请审查以下C++代码的质量:\n[代码内容]"
}
},
{
"role": "assistant",
"content": {
"type": "text",
"text": "以下是代码审查结果:\n1. 内存管理..."
}
}
]
}
内容类型:Message content支持三种类型(对应MCPBuilder.h的构建方法):
"type": "text"—— 纯文本内容(MCPBuilder::TextContent())"type": "image"—— 图片内容,data字段为Base64编码(MCPBuilder::ImageContent())"type": "audio"—— 音频内容,data字段为Base64编码(MCPBuilder::AudioContent())
参数化提示
提示模板支持参数替换。在模板定义中声明参数,运行时用户填入具体值。
参数定义格式(JSON数组):
[
{
"name": "language",
"description": "代码的编程语言",
"required": true
}
]
CodeReview插件的参数使用(plugins/code-review/CodeReview.cpp 第36-41行):
{
"code-review",
"Asks the LLM to analyze code quality and suggest improvements",
R"([{
"name" : "language",
"description" : "The programming language of the code",
"required": true
}])"
}
在 HandleRequestImpl 中获取参数值(第53-54行):
auto request = json::parse(req);
auto language = request["params"]["arguments"]["language"].get<std::string>();
然后拼接到提示文本中(第62行):
{"text", "Please analyze code quality and suggest improvements of this code written in " + language}
CodeReview插件详解
plugins/code-review/CodeReview.cpp 是一个完整的提示模板类型插件示例。
插件结构分析(按代码行号):
- 模板定义(第32-42行):
static PluginPrompt prompts[] = {
{
"code-review", // name
"Asks the LLM to analyze code quality...", // description
R"([{"name":"language","description":"...","required":true}])" // arguments (JSON数组)
}
};
- 插件元数据(第44-46行):
const char* GetNameImpl() { return "code-review"; }
const char* GetVersionImpl() { return "1.0.0"; }
PluginType GetTypeImpl() { return PLUGIN_TYPE_PROMPTS; } // 声明为提示类型
- 请求处理(第52-76行)—— 核心逻辑:
char* HandleRequestImpl(const char* req) {
auto request = json::parse(req);
auto language = request["params"]["arguments"]["language"].get<std::string>();
// 构建响应
nlohmann::json response = json::object();
nlohmann::json messages = json::array();
messages.push_back(json::object({
{"role", "user"}, // 使用user角色
{"content", json::object({
{"type", "text"}, // 文本类型内容
{"text", "Please analyze code quality and suggest improvements
of this code written in " + language}
})}
}));
response["description"] = "this is the code review prompt";
response["messages"] = messages;
// 转为C字符串返回 (MCP插件接口约定)
std::string result = response.dump();
char* buffer = new char[result.length() + 1];
strcpy_s(buffer, result.length() + 1, result.c_str());
return buffer;
}
返回的消息将被客户端合并到LLM的对话上下文中。LLM会收到一条 role: "user" 的消息:“Please analyze code quality and suggest improvements of this code written in C++”,然后基于此进行分析。
设计要点:
- 提示模板是"无副作用"的——只返回消息文本,不修改服务器状态
- 参数支持类型化定义(required、description等元数据)
- 消息内容支持多模态(text/image/audio)
- 返回的消息role可以是system或user(本项目中使用user)
资源URI方案
URI格式:协议://主机/路径
URI(Uniform Resource Identifier,统一资源标识符)是标识资源的字符串。MCP协议中使用URI来标识和访问服务器提供的资源。
标准URI格式:
scheme://authority/path?query#fragment
示例:
https://example.com/path/to/file (HTTP URI)
file:///home/user/document.txt (文件URI)
bacio:///quote (MCP自定义URI)
MCP中的资源URI:bacio:///quote
MCP资源URI使用自定义scheme,格式为 scheme:///path。注意三个斜杠——这是"无authority"URI的标准写法。
BacioQuote插件的资源定义(plugins/bacio-quote/BacioQuote.cpp 第183-190行):
static PluginResource resources[] = {
{
"bacio-quote", // name:资源名称
"A list of the famous italian bacio perugina quotes", // description
"bacio:///quote", // uri:资源URI
"text/plain", // mime:MIME类型
}
};
这里定义了一个URI为 bacio:///quote 的资源:
- scheme:
bacio - authority: (空,使用三个斜杠表示)
- path:
quote
资源内容类型(MIME Type)
资源可以是文本或二进制,通过MIME类型声明:
| MIME Type | 说明 | 内容载体 |
|---|---|---|
text/plain |
纯文本 | text 字段 |
text/html |
HTML文本 | text 字段 |
text/markdown |
Markdown文本 | text 字段 |
image/png |
PNG图片 | blob 字段(Base64) |
image/jpeg |
JPEG图片 | blob 字段(Base64) |
audio/wav |
WAV音频 | blob 字段(Base64) |
application/json |
JSON数据 | text 字段 |
资源内容结构(由 MCPBuilder::ResourceText 构建):
{
"contents": [
{
"uri": "bacio:///quote",
"mimeType": "text/plain",
"text": "Amore e tosse non si possono nascondere. (Ovidio)"
}
]
}
BacioQuote插件的资源读取(第200-223行):
char* HandleRequestImpl(const char* req) {
auto request = json::parse(req);
nlohmann::json response = json::object();
// 随机选择一条名言
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> distr(0, messages.size() - 1);
// 使用 MCPBuilder::ResourceText 构建资源内容
nlohmann::json contents = json::array();
contents.push_back(
MCPBuilder::ResourceText(
resources[0].uri, // "bacio:///quote"
resources[0].mime, // "text/plain"
messages[distr(gen)] // 随机名言
)
);
response["contents"] = contents;
// ... 返回结果
}
每次调用 resources/read 读取 bacio:///quote 时,服务器都会从178条意大利名言中随机选取一条返回(通过 std::mt19937 随机数生成器)。
资源的订阅与更新通知
MCP协议支持资源订阅机制,允许客户端订阅特定URI的变更通知。
资源结构定义(PluginAPI.h 第58-63行):
typedef struct {
const char* name; // 资源名称
const char* description; // 资源描述
const char* uri; // 资源URI
const char* mime; // MIME类型
} PluginResource;
订阅相关方法:
resources/subscribe(Server.cpp第432-434行):客户端订阅资源变更resources/unsubscribe(第437-439行):取消订阅notifications/resources/updated(第482-484行):资源内容变更后通知订阅者notifications/resources/list_changed(第479-481行):资源列表变化通知
在本项目当前版本中,subscribe/unsubscribe标记为"未实现",但框架已预留接口。热加载机制已完整实现资源列表变更通知 —— 当 ScanForChanges 检测到新资源插件加载或旧资源插件卸载时,通过 SetOnPluginsChanged 回调(main.cpp 第165-179行)自动发送 notifications/resources/list_changed 通知。
插件的资源类型注册
插件加载器通过 PluginAPI.GetType() 判断插件类型。PluginType 枚举(PluginAPI.h 第40-44行):
typedef enum {
PLUGIN_TYPE_TOOLS = 0, // 工具类插件
PLUGIN_TYPE_PROMPTS = 1, // 提示模板类插件
PLUGIN_TYPE_RESOURCES = 2 // 资源类插件
} PluginType;
在 main.cpp 中,不同的 overrideCallback 各自只处理对应类型的插件:
tools/list(第194行):只处理PLUGIN_TYPE_TOOLSprompts/list(第246行):只处理PLUGIN_TYPE_PROMPTSresources/list(第292行):只处理PLUGIN_TYPE_RESOURCES
这种按类型隔离的设计确保了每个插件只贡献其声明的能力类型,不会跨类型混淆。
资源URI方案扩展建议
虽然本项目当前只有 bacio:///quote 一个资源URI示例,但MCP协议的URI方案可以根据需求扩展:
bacio:///quote → 文本资源(名言引用)
weather:///forecast/beijing → 天气数据资源
file:///home/user/doc.txt → 本地文件资源
db:///query/users → 数据库查询资源
api:///github/issues → 外部API资源
每种URI scheme应该:
- 在
PluginResource.uri中正确定义 - 在
PluginResource.mime中声明正确的MIME类型 - 在
HandleRequestImpl中根据URI前缀分发处理逻辑
更多推荐



所有评论(0)