MCP项目笔记九(插件 bacio-quote)

从零理解 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 插件跳过了 methoduri 的校验,更严谨的实现应该是:

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
相关推荐
Chester_19995 小时前
CSP202203C.计算资源调度器
开发语言·数据结构·c++·蓝桥杯
知产xiao_xin6 小时前
知识产权入门 —— 相关权(邻接权)
经验分享·笔记·知识产权
剑客的茶馆6 小时前
开发者,运维怎样转行FDE?
运维·ai·开发·fde
自小吃多7 小时前
DXF文件导入与PCB板框定义笔记
笔记·嵌入式硬件
会周易的程序员7 小时前
给 PLC 写一个字节码虚拟机:STVM 虚拟机架构设计
开发语言·c++·虚拟机·软plc·iec61131·stvm
小灰灰搞电子7 小时前
完全驾驭 Qt 与数据库:C++ ORM 框架 QxOrm 原理与实践指南
数据库·c++·qt
QT界面美化性能优化7 小时前
QT+AI:使用AI技术为QT应用程序赋能
c++·人工智能·qt·opencv·qt教程·qt6.3
俊哥V8 小时前
每日 AI 研究简报 · 2026-08-29
人工智能·ai
jike_20268 小时前
iPhone采访录音同时拍现场照片的APP:图片音频同步记录实测
笔记·智能手机·音视频·语音识别
Orange_sparkle8 小时前
从 Docker 到 Doris:建立后端基础设施全景图
运维·docker·ai·容器·claude code