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把三类接口全部放在同一个结构体里。对于不适用的接口,惯例是返回0或nullptr。
五、完整调用链追踪
以一个"计算器工具插件"为例,追踪从加载到返回 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)
主程序遍历工具,拿到 PluginTool:name="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(反向推送通道)