从零理解 MCP 插件开发:Resource、Tool 与资源读取协议详解
本文以 C++ 插件BacioQuote,系统讲解 MCP 插件的核心概念、资源读取协议以及 Tool 与 Resource 的本质区别。
一、从一个"随机名言插件"说起
想象这样一个简单需求:写一个插件,每次被调用时随机返回一句意大利 Bacio Perugina 风格的名言。
这个需求对应的 C++ 插件,结构如下:
BacioQuote.cpp
├── 名言数据(std::vector<std::string>)
├── 资源声明(PluginResource)
├── 插件元信息(GetName / GetVersion / GetType)
├── 初始化与销毁(Initialize / Shutdown)
└── 核心请求处理(HandleRequestImpl)
整体思路清晰:插件向外暴露一个叫 bacio:///quote 的资源,每次收到请求时从预设名言里随机抽一条,按协议格式包装后返回。
二、代码结构逐层解析
2.1 头文件依赖
cpp
#include <vector>
#include <string>
#include <random>
#include "PluginAPI.h"
#include "json.hpp"
#include "../../src/utils/MCPBuilder.h"
各模块职责:
vector/string:存储和操作名言文本random:提供现代化随机数生成能力PluginAPI.h:定义插件接口规范json.hpp(nlohmann::json):处理 JSON 序列化与反序列化MCPBuilder.h:辅助构建符合 MCP 协议的资源响应
2.2 资源声明
cpp
static PluginResource resources[] = {
{
"bacio-quote",
"A list of the famous italian bacio perugina quotes",
"bacio:///quote",
"text/plain",
}
};
这四个字段分别代表:资源名称、描述、URI 和 MIME 类型。外界只需要知道 bacio:///quote 这个地址,就能读取这个资源。
2.3 插件元信息
cpp
const char* GetNameImpl() { return "bacio-quote"; }
const char* GetVersionImpl() { return "1.0.0"; }
PluginType GetTypeImpl() { return PLUGIN_TYPE_RESOURCES; }
PLUGIN_TYPE_RESOURCES 表明这是一个资源型插件,而非工具型插件。这个区别在第四节会详细展开。
2.4 初始化函数
cpp
int InitializeImpl() {
return 1;
}
返回 1 表示初始化成功。
三、核心函数:HandleRequestImpl 详解
这是整个插件最关键的部分,负责接收请求并生成响应。
cpp
char* HandleRequestImpl(const char* req) {
// 解析传入的 JSON 字符串,至少验证其合法性
auto request = json::parse(req);
// 初始化空响应对象
nlohmann::json response = json::object();
// ── 随机数生成 ──────────────────────────────
std::random_device rd; // 硬件随机种子
std::mt19937 gen(rd()); // Mersenne Twister 引擎
std::uniform_int_distribution<> distr(0, messages.size() - 1); // 均匀分布
// ── 构建响应内容 ─────────────────────────────
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;
// ── 序列化并返回 ─────────────────────────────
std::string result = response.dump();
char* buffer = new char[result.length() + 1];
#ifdef _WIN32
strcpy_s(buffer, result.length() + 1, result.c_str());
#else
strcpy(buffer, result.c_str());
#endif
return buffer;
}
设计细节:
- 随机数引擎使用
std::mt19937,比老式的rand()质量更高 - 返回的是堆上分配的
char*,调用方需要负责释放
四、resources/read:资源读取协议详解
4.1 它是什么
resources/read 是 MCP 协议中"读取资源"的标准方法。可以理解为:
"请把这个 URI 对应的资源内容读给我。"
4.2 请求与响应格式
请求:
json
{
"jsonrpc": "2.0",
"method": "resources/read",
"params": {
"uri": "bacio:///quote"
},
"id": "request-id"
}
响应:
json
{
"contents": [
{
"uri": "bacio:///quote",
"mimeType": "text/plain",
"text": "某一句名言"
}
]
}
关键参数只有一个:uri。这也是 Resource 和 Tool 最直观的区别之一------Resource 的输入非常简洁。
4.3 完整执行链
插件声明资源(GetResourceCountImpl / GetResourceImpl)
↓
宿主发起 resources/read 请求,携带目标 URI
↓
HandleRequestImpl 解析请求,匹配 URI
↓
生成内容,封装进 contents 数组
↓
返回 JSON 响应给宿主
4.4 更规范的实现方式
当前的 BacioQuote 插件跳过了 method 和 uri 的校验,更严谨的实现应该是:
cpp
char* HandleRequestImpl(const char* req) {
auto request = json::parse(req);
std::string method = request["method"];
if (method == "resources/read") {
std::string uri = request["params"]["uri"];
if (uri == "bacio:///quote") {
// 构建并返回响应
}
return buildError("Resource not found");
}
return buildError("Unknown method");
}
先校验方法名,再校验 URI,最后才生成内容------这才是生产级别的实现思路。
五、Tool vs Resource:本质区别
Tool = 帮你做事(执行动作)
Resource = 给你内容(读取数据)
5.1 协议层对比
| 维度 | Tool | Resource |
|---|---|---|
| 请求方法 | tools/call |
resources/read |
| 核心参数 | name + arguments |
uri |
| 响应字段 | content |
contents |
| 语义 | 执行 | 读取 |
| 副作用 | 可能有 | 通常没有 |
5.2 数据流对比
Tool 的调用流:
json
// 请求
{
"method": "tools/call",
"params": {
"name": "calculator",
"arguments": { "expression": "2+3*4" }
}
}
// 响应
{
"content": [{ "type": "text", "text": "14" }]
}
Resource 的调用流:
json
// 请求
{
"method": "resources/read",
"params": { "uri": "bacio:///quote" }
}
// 响应
{
"contents": [{ "uri": "bacio:///quote", "mimeType": "text/plain", "text": "名言" }]
}
5.3 一个常见的误区
很多人认为"返回数据 = Resource",但这并不准确。
判断依据应该是:数据是被动提供的,还是通过动态逻辑生成的?
- 随机名言(简单取值)→ Resource
- 根据条件筛选的名言 → Tool
- 固定配置 → Resource
- 需要计算或查询的结果 → Tool
六、整体流程回顾
宿主程序加载插件
↓
调用 CreatePlugin() 获取接口表
↓
调用 InitializeImpl() 完成初始化
↓
通过 GetResourceCountImpl / GetResourceImpl 发现资源
↓
发送 resources/read 请求
↓
HandleRequestImpl 随机选取名言,封装 JSON 响应
↓
宿主收到 contents,完成资源读取
↓
程序结束时调用 ShutdownImpl / DestroyPlugin