Skip to content

MCP 外挂扩展

面向 已打包安装版(exe / dmg) 与开发环境:由插件开发者打好 自带依赖的 Python 插件包,普通用户安装到约定目录后即可扩展 MCP 工具,无需 pip install,也无需改主工程源码。

内置工具开发请见 MCP 开发指南。外挂与内置 双轨并存

一句话

开发者构建插件包(plugin.py + manifest.json + lib/)→ 用户拷贝到 mcp_plugins → 重启应用加载。

与内置工具的区别

内置外挂
位置src/mcp/tools/<name>/{用户数据}/mcp_plugins/<id>/
入口register.pyregister_*_toolsplugin.pyregister(host)
依赖主程序依赖插件目录 lib/ 自带(vendored)
发版跟主程序插件独立版本
用户操作升级应用安装/删除插件目录

安装目录

平台默认路径
macOS~/Library/Application Support/py-xiaozhi/mcp_plugins/
Windows%LOCALAPPDATA%\py-xiaozhi\mcp_plugins\
Linux~/.local/share/py-xiaozhi/mcp_plugins/

可用配置覆盖:

json
"MCP_PLUGINS": {
  "ENABLED": true,
  "DIR": null,
  "ENABLED_IDS": [],
  "DISABLED_IDS": [],
  "ALLOW_HOST_GET": ["config_readonly", "logger"]
}
  • DIRnull 时使用上表默认路径。
  • DISABLED_IDS 含插件 id启动时不加载
  • ALLOW_HOST_GET 控制 host.get 白名单;music_player 需显式加入。

插件包结构

text
com.example.hello/
  manifest.json     # 身份与入口
  plugin.py         # def register(host)
  lib/              # 可选:pip install --target lib/ 打进去的依赖
  native/           # 可选:按平台分子目录的二进制扩展
  README.txt        # 可选

manifest.json 示例

json
{
  "id": "com.example.hello",
  "name": "Hello Demo",
  "version": "1.0.0",
  "api_version": 1,
  "entry": "plugin:register",
  "runtime": "python-inprocess",
  "tool_name_prefix": "example.",
  "enabled_by_default": true
}
字段说明
id全局唯一,用于启用/禁用
api_version不得高于宿主支持的插件 API(当前为 1)
min_host_version可选;宿主版本过低则跳过
platforms可选;声明后与当前 OS/ARCH 不符则跳过
python_abi可选;如 cp310,与宿主不一致则跳过
entry模块:属性,默认 plugin:register
runtimepython-inprocess(默认,同进程)python-subprocess(独立子进程)
call_timeout可选;仅 python-subprocess,单次工具调用超时秒数(默认 60)
tool_name_prefix建议填写;工具名宜带此前缀

可选严格配置(默认关):ENFORCE_PREFIXREQUIRE_PYTHON_ABIREQUIRE_PLATFORMS

plugin.py 最小示例

python
def register(host):
    @host.tool(
        name="example.hello",
        description="打招呼。参数 name 可选。",
        props=[{"name": "name", "type": "string", "default": "世界"}],
    )
    async def hello(args):
        name = (args or {}).get("name") or "世界"
        return f"你好, {name}!"

也可用 host.add_tool(McpTool(...))(与内置相同的 src.mcp.tooling 类型)。

宿主 API(host)

方法说明
host.add_tool(tool)注册 McpTool
host.tool(name, description, props=None)装饰器糖,不写全局 registry
host.get(name)白名单能力;未授权返回 None

默认 get 白名单:config_readonlylogger
music_player 等需在配置 ALLOW_HOST_GET 中显式允许。

禁止依赖已删除的全局 @mcp_toolget_instance 单例。

自带依赖与分平台发布

原则

依赖类型能否一份包三端通用做法
纯 Python(如 markdown通常可以一个 lib/ + 一个 zip 即可
带原生扩展(.so / .pyd / .dylib按系统 + 架构分别构建、分别发布
系统/机器人环境 SDK(ROS2、rclpy、厂商 SDK)一般不能塞进 lib/见下文「环境依赖」

推荐发布命名(有原生库时最合适)

text
com.example.foo-1.0.0-macos-arm64.zip
com.example.foo-1.0.0-windows-amd64.zip
com.example.foo-1.0.0-linux-x86_64.zip

每个 zip 内为完整插件目录;manifest.platforms 建议只写当前平台,python_abi 与构建用的宿主 Python 一致(如 cp310)。

用户只下载自己系统那一份,解压到 mcp_plugins/

构建命令(开发者)

目标平台、与宿主相同的 Python 小版本上:

bash
# 在插件目录内
uv pip install -r requirements.txt --target lib/ --python /path/to/host/python
# 或: python -m pip install -r requirements.txt -t lib/
  • --target lib/ / -t lib/不要 pip install 进主程序环境。
  • 含二进制的包必须在对应 OS/ARCH 的机器或 CI 上执行上述命令。
  • 打 zip 时带上整个目录(含 lib/)。

环境依赖(ROS2 / 宇树 Python SDK 等)

这类依赖通常安装在机器人系统环境里(如 source /opt/ros/humble/setup.bash、厂商提供的 site-packages),不是普通 PyPI 小 wheel,也很难完整 vendored 进 lib/ 后在任意 PC 上跑通。

推荐约定:

做法说明
插件声明环境要求README / 发布页写清:需已安装 ROS2 发行版、宇树 SDK、Python 版本、架构(如 linux-aarch64
lib/ 只打可搬运的 PyPI 依赖HTTP、工具库等可 --target lib/
import 环境包时写清失败信息import rclpy 失败 → 返回「请先 source ROS 并安装 xxx」
分平台发布机器人插件多为 linux-aarch64 / linux-x86_64 专用包,不要发「三端通用」误导用户
不要假设桌面 exe/dmg 用户能跑 ROS 插件桌面安装版与工控机/宇树镜像是不同运行环境

示例(插件内):

python
def register(host):
    try:
        import rclpy  # 来自系统 ROS,而非 lib/
    except ImportError as e:
        raise RuntimeError(
            "需要 ROS2 Python 环境(rclpy)。"
            "请在已 source ROS 的环境中运行宿主,或安装对应发行版。"
        ) from e
    # 可搬运依赖仍从 lib/ 提供
    ...

同进程加载下,lib/ 只解决「免用户 pip」,不是进程级隔离。

运行时:python-inprocesspython-subprocess

默认不是子进程。 未写或写 python-inprocess 时,插件与主程序共用同一 Python 进程。

python-inprocess(默认)python-subprocess
进程与宿主同进程独立 worker 进程(stdin/stdout JSON 行协议)
崩溃/堵死隔离差(可拖垮宿主)较好(可超时结束子进程)
host.get("logger")宿主 logger子进程本地 logger
host.get("config_readonly")配置对象/只读视图可 JSON 序列化的配置快照
host.get("music_player") 等活对象白名单允许时可用不可用(无法跨进程传对象)
适用轻量、可信、需宿主能力重型 native、依赖冲突、希望与 UI/音频隔离

manifest.json 中显式开启子进程:

json
{
  "id": "com.example.hello_sub",
  "entry": "plugin:register",
  "runtime": "python-subprocess",
  "tool_name_prefix": "example.",
  "call_timeout": 30
}

仓库示例:

text
examples/mcp_plugins/com.example.hello/          # inprocess
examples/mcp_plugins/com.example.hello_sub/    # subprocess

说明:子进程隔离不是 OS 级沙箱,仍是本机同用户权限;只隔离解释器与内存空间。

用户安装步骤

  1. 取得插件包(目录或 zip)。
  2. 解压/复制到上文 mcp_plugins 目录(保证存在 manifest.json 与入口文件)。
  3. 完全退出并重启 应用。
  4. 日志中应出现:[MCP插件] 已加载 <id> (N 工具)
  5. 通过对话或 MCP tools/list 确认工具名。

仓库示例(无第三方依赖,可直接拷贝):

text
examples/mcp_plugins/com.example.hello/        # 同进程
examples/mcp_plugins/com.example.hello_sub/  # 子进程 runtime

设置页:按工具启停(MCP_TOOLS)

应用内 参数设置 → MCP 工具:按包名/插件分组列出工具,可单开或整组开关。

  • 配置键:MCP_TOOLS.DISABLED(工具全名字符串数组,黑名单;默认 [] 全开)。
  • 内置工具目录来自扫描 src/mcp/tools/*/register.py(分组 = 目录名,如 music / app)。
  • 外挂来自 mcp_plugins 的 manifest / 源码启发式扫描。
  • 关闭后:不出现在 MCP tools/listtools/call 也会拒绝。
  • 保存后:若当前已连接协议,会断开并重连以便服务端重新拉工具列表;重连后保持空闲,不会自动进入「聆听中」。
  • 未连接时仅写配置,下次连接生效。

MCP_PLUGINS.DISABLED_IDS 的区别:后者是整包不加载;前者是工具已注册前提下的暴露面裁剪

开发者检查

bash
python scripts/check_mcp_plugin.py /path/to/com.example.hello
python scripts/check_mcp_plugin.py /path/to/plugin --strict

--strict 会要求 api_versionpython_abiplatformstool_name_prefix

命名与冲突

  • 工具名建议使用 manifest.tool_name_prefix 前缀(如 example.)。
  • 已有工具同名时:宿主 拒绝重复注册(不覆盖内置)。
  • 关闭插件:MCP_PLUGINS.DISABLED_IDS 加入 id 后重启;或删除目录。

运行时 API(程序侧)

API说明
启动加载McpServer.add_common_tools 末尾调用 load_mcp_plugins_from_config
server.unload_plugin(plugin_id)移除该插件已注册工具;若为 subprocess 会结束子进程
server.reload_external_plugins(...)卸掉外挂后按配置重新扫描

签名校验、插件商店等为后续能力。

安全提示

  • python-inprocess:与主程序同进程,权限等同本机任意代码。
  • python-subprocess:工具逻辑在子进程,崩溃隔离更好,但仍是本机同用户权限,不是安全沙箱。
  • 请只安装可信来源插件;可用 MCP_PLUGINS.ENABLED=false 一键关闭全部外挂。

相关文档

  • MCP 内置工具开发指南
  • 示例目录:examples/mcp_plugins/
  • 实现:src/mcp/plugins/host.pyloader.pyregistry.pysubprocess_runtime.pysubprocess_worker.py