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
相关推荐
沉鱼.442 小时前
第十三届题目
c语言·c++·算法
Xudde.2 小时前
班级作业笔记报告0x10
笔记·学习·安全·web安全·php
liulilittle2 小时前
C++ 无锁编程:单停多发送场景高性能方案
服务器·开发语言·c++·高性能·无锁·原子
无限进步_2 小时前
【C++】巧用静态变量与构造函数:一种非常规的求和实现
开发语言·c++·git·算法·leetcode·github·visual studio
程序员鱼皮2 小时前
AI 时代,满分的程序员简历是怎么样的?附简历模板
ai·程序员·编程·求职·简历
降临-max3 小时前
Git 协同开发与冲突解决
笔记·git
小超超爱学习99373 小时前
大数乘法,超级简单模板
开发语言·c++·算法
刘佬GEO3 小时前
【无标题】
网络·人工智能·搜索引擎·ai·语言模型
熊猫钓鱼>_>4 小时前
AI驱动的Web应用智能化:WebMCP、WebSkills与WebAgent的融合实践
前端·人工智能·ai·skill·webagent·webmcp·webskills