MCP项目笔记五(PluginAPI)

MCP 插件接口设计:PluginAPI 从协议到 JSON 的完整解析

深入剖析 PluginAPI 头文件的每一行设计决策------为什么要用函数指针表?为什么需要 extern "C"?从加载插件到返回 JSON 结果,走完一次完整的工具调用流程。


一、接口文件整体结构

PluginAPI.h 是整个 MCP 插件系统的"协议契约"。它规定了主程序与插件之间通信的一切规则:插件要暴露哪些函数、数据结构长什么样、如何创建和销毁插件对象。

核心定位 :这是一份 C 风格插件 SDK 接口定义 。主程序通过动态库导出的 CreatePlugin() 拿到 PluginAPI,再通过其中的函数指针完成:获取信息、初始化/关闭、处理请求、枚举能力、发送通知。

三个关键机制

① 导出宏 PLUGIN_API --- 跨平台符号导出

cpp 复制代码
#ifdef _WIN32
#define PLUGIN_API __declspec(dllexport)             // Windows:从 DLL 导出符号
#else
#define PLUGIN_API __attribute__((visibility("default")))  // POSIX:默认可见性
#endif

extern "C" --- 禁止 C++ 名字修饰(Name Mangling)

cpp 复制代码
#ifdef __cplusplus
extern "C" {
#endif
// ... 所有类型和函数声明 ...
#ifdef __cplusplus
}
#endif

为什么需要 extern "C" :C++ 编译器会对函数名做修饰,例如 CreatePlugin 编译后可能变成 _Z12CreatePluginv。而主程序用 dlsym(handle, "CreatePlugin") 查找符号时,必须精确匹配原始名称。extern "C" 告诉编译器:这些函数按 C 方式导出,不要改名。

③ 两个固定入口函数 --- 插件的唯一暴露点

cpp 复制代码
PLUGIN_API PluginAPI* CreatePlugin();        // 工厂函数:创建插件对象
PLUGIN_API void DestroyPlugin(PluginAPI*);   // 析构函数:销毁插件对象

二、三种插件类型(PluginType)

PluginType 枚举将插件按"能力类别"分类,让主程序能在枚举工具/资源前,先判断当前插件属于哪种类型,从而只调用相关接口。

c 复制代码
typedef enum {
    PLUGIN_TYPE_TOOLS     = 0,   // 工具型:提供可执行功能
    PLUGIN_TYPE_PROMPTS   = 1,   // Prompt 型:提供模板/提示词
    PLUGIN_TYPE_RESOURCES = 2    // 资源型:提供可读取内容
} PluginType;
类型 本质 核心接口 典型请求 method
TOOLS 函数/动作 GetToolCount / GetTool tools/call
PROMPTS 模板 GetPromptCount / GetPrompt prompts/get
RESOURCES 可读内容 GetResourceCount / GetResource resources/read

三种类型的区别:

  • TOOLS:强调"执行能力"。如计算器、天气查询、文件转换、代码执行
  • PROMPTS:强调"模板提供"。如写邮件模板、总结模板、格式化输出
  • RESOURCES:强调"内容提供"。如文档库、图片资源、静态配置文件

三、核心数据结构详解

PluginTool --- 工具的说明书

PluginTool 不是工具逻辑本身,而是工具的元数据描述。主程序靠它注册工具、展示给 AI、并在调用时校验参数。

c 复制代码
typedef struct {
    const char* name;          // 工具名(唯一标识,tools/call 时按此匹配)
    const char* description;   // 工具描述(AI 靠它决定是否选用此工具)
    const char* inputSchema;   // 参数定义,JSON Schema 字符串
} PluginTool;

inputSchema 的典型内容:

json 复制代码
{
  "type": "object",
  "properties": {
    "expression": {
      "type": "string",
      "description": "数学表达式,如 2+3*4"
    }
  },
  "required": ["expression"]
}

PluginResource --- 资源的目录项

PluginResource 类比成"文件系统目录项"就很好理解:

c 复制代码
typedef struct {
    const char* name;          // 资源名(展示用)
    const char* description;   // 资源说明
    const char* uri;           // 资源定位符(resources/read 时按此查找)
    const char* mime;          // MIME 类型,如 text/plain、image/png
} PluginResource;
字段 文件系统类比 说明
uri 文件路径 资源的唯一定位符,如 my-plugin:///data
name 文件名 人类可读的资源名称
description 文件注释 说明该资源的内容或用途
mime 文件扩展名语义 指导接收方如何解析资源内容

NotificationSystem --- 插件→主程序的反向通道

c 复制代码
typedef void (*ClientNotificationCallback)
    (const char* pluginName, const char* notification);

typedef struct {
    ClientNotificationCallback SendToClient;  // 由主程序注入,插件只调用
} NotificationSystem;

设计意图 :这是一个反向调用通道 。主程序把回调函数地址注入给插件,插件执行过程中可随时调用 notifications->SendToClient() 向客户端推送消息。注释 "you should not touch this" 明确表示该指针由宿主管理,插件不应覆写。


四、PluginAPI 接口全览

PluginAPI 是整个头文件最核心的结构体。它本质上是一张 C 风格的虚函数表(vtable) :里面全是函数指针,主程序通过 CreatePlugin() 拿到这张表后,就能用统一的方式调用任何插件。

基础信息接口

函数签名 说明
GetName() → char* 返回插件名称,用于日志标识、注册表、通知来源等
GetVersion() → char* 返回版本号,用于兼容性检查、排错、部署管理
GetType() → PluginType 返回插件类别,决定主程序应优先调用哪类枚举接口

生命周期接口

函数签名 说明
Initialize() → int 初始化:加载配置、分配资源、注册工具列表。返回 0 表示成功
Shutdown() → void 关闭:释放资源、断开连接、清空状态。与 Initialize() 对应

请求处理接口

函数签名 说明
HandleRequest(req) → char* 核心入口:接收 JSON 字符串请求,执行业务逻辑,返回 JSON 字符串结果

HandleRequest() 是插件的"总路由",一个规范的实现内部通常形如:

cpp 复制代码
// 解析 JSON
// 取出 method
if (method == "tools/call") { ... }
else if (method == "resources/read") { ... }
// 组装响应 JSON 并返回

能力枚举接口

c 复制代码
int            (*GetToolCount)();
const PluginTool*     (*GetTool)(int index);

int            (*GetPromptCount)();
const PluginPrompt*   (*GetPrompt)(int index);

int            (*GetResourceCount)();
const PluginResource* (*GetResource)(int index);

注意 :虽然插件被分为三种类型,但 PluginAPI 把三类接口全部放在同一个结构体里。对于不适用的接口,惯例是返回 0nullptr


五、完整调用链追踪

以一个"计算器工具插件"为例,追踪从加载到返回 JSON 结果的完整流程。

复制代码
加载动态库 → CreatePlugin() → Initialize() → GetTool()
→ HandleRequest() → 解析 JSON → 执行逻辑 → 返回 JSON

阶段 A:加载与注册

Step 1 --- dlopen("calculator.so")

将动态库映射入进程,取得句柄。此时只是"代码进来了",没有插件对象。

Step 2 --- CreatePlugin() → PluginAPI*

调用工厂函数,插件返回填满函数指针的接口表。此后主程序通过这张表操作一切。

Step 3 --- GetName() / GetVersion() / GetType()

读取插件元数据,完成"登记":确认这是工具型插件,记录名字和版本。

Step 4 --- Initialize()

插件将工具 calculator 注册进内部列表(如 g_tools),准备好可被枚举的元数据。

Step 5 --- GetToolCount() + GetTool(0)

主程序遍历工具,拿到 PluginToolname="calculator"inputSchema 要求一个 expression 参数。将工具注册入系统,暴露给 AI。


阶段 B:一次工具调用

Step 6 --- 用户输入:帮我算 2+3*4

模型根据工具描述,选中 calculator 工具,构造参数 {"expression": "2+3*4"}

Step 7 --- 主程序构造 JSON-RPC 请求

json 复制代码
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "calculator",
    "arguments": { "expression": "2+3*4" }
  },
  "id": "1"
}

Step 8 --- HandleRequest() 解析 → 路由 → 执行

插件解析 JSON,识别 method="tools/call",取出 tool_name="calculator",读出 expression,计算得 14

Step 9 --- 插件返回 JSON 结果

json 复制代码
{
  "content": [
    {
      "type": "text",
      "text": "2+3*4 = 14"
    }
  ]
}

Step 10 --- 主程序解析结果,返回给用户

提取 content 字段,将文本结果呈现给用户或传回 AI 进行下一步处理。


阶段 C:卸载

cpp 复制代码
api->Shutdown();        // 插件清理内部状态(g_tools.clear() 等)
DestroyPlugin(api);     // 销毁 PluginAPI 对象
dlclose(handle);        // 卸载动态库

总结

PluginAPI.h 的本质是:基于函数指针表的插件协议 + JSON 字符串通信

主程序先通过枚举接口"发现能力",再通过 HandleRequest() "触发执行",二者之间的一切信息交换都以 JSON 字符串为载体,实现了彻底的接口解耦。

用一张图理解四个核心类型的关系:

复制代码
PluginAPI
 ├── GetType()             → PluginType(插件是哪类)
 ├── GetToolCount/Tool     → PluginTool(工具的说明书)
 ├── GetResourceCount/Resource → PluginResource(资源的目录项)
 ├── HandleRequest()       → JSON 字符串(实际执行入口)
 └── notifications         → NotificationSystem(反向推送通道)

相关推荐
BullSmall2 小时前
接口测试系列-JSON 结构注入测试系统(全解 + 实战案例)
json·安全性测试
C_Si沉思2 小时前
C++与硬件交互编程
开发语言·c++·算法
tankeven2 小时前
HJ148 迷宫寻路
c++·算法
今儿敲了吗2 小时前
Linux学习笔记第二章——虚拟机基础操作
linux·笔记·学习
老虎06272 小时前
LeetCode热题100 刷题笔记(第一天)哈希 「两数之和」
笔记·算法·leetcode
xiaoye-duck2 小时前
《算法题讲解指南:优选算法-链表》--53.重排链表,54.合并 K 个升序链表,55.K个一组翻转链表
c++·算法·链表
CODE_RabbitV2 小时前
【保姆级实操版 - STM32 系列笔记】STM32F103标准库开发:Keil5新建工程完整教程
笔记·stm32·嵌入式硬件
j_xxx404_3 小时前
蓝桥杯基础--前缀和
数据结构·c++·算法·蓝桥杯·排序算法
chh5633 小时前
从零开始学习C++ -- 基础知识
开发语言·c++·windows·学习·算法