Files
openclaw-notes/plugin-tools.md
T
clz 3c0cd86352 docs: add MCP, plugin/LSP tool, and RAG notes
- notes/mcp-overview.md: MCP 协议概览,跟 tool use 的分层差异
- notes/mcp-client-internals.md: MCP client 内部实现(发现/连接/catalog)
- notes/plugin-tools.md: plugin tool 与 native/MCP 的对比
- notes/lsp-tools.md: LSP 协议科普 + OpenClaw LSP tool 包装
- notes/rag-overview.md: RAG 与向量化(向量只用来检索,prompt 用原文)
- notes/blog/mcp-101.md: 面向开发者的 MCP 科普,LSP 类比引入
- README 索引更新
2026-06-16 00:59:32 +08:00

10 KiB
Raw Blame History

Plugin Tool

基于 OpenClaw 2026.6.2 版本源码。

本文讲 OpenClaw 的 plugin tool:跟 native tool、MCP tool 同列, 但归插件作者拥有、用 TypeScript 写、跑在 OpenClaw 同进程里的那一类工具。

定位

OpenClaw agent 看见的工具一共有三种来源(详见 mcp-overview.md 的 "MCP 与 tool use 的区别"):

拥有者 在哪儿 进程 协议
native tool OpenClaw core src/agents/tools/* 同进程
plugin tool 插件作者 extensions/<name>/(可第三方) 同进程 OpenClaw Plugin SDK
MCP tool 第三方/用户 任何 MCP server 子进程/远端 MCP JSON-RPC

plugin tool 占的格子是:用 TypeScript 写、走 OpenClaw 内部 SDK 接口、 不需要跑子进程。介于 "core 写死的 native" 和 "完全解耦的 MCP" 之间, 专门用来做"深度集成 OpenClaw 内部能力,但又不属于 core" 的工具。

怎么写一个

入口 src/plugin-sdk/tool-plugin.tsdefineToolPlugin。最小例子 (extensions/llm-task/index.ts):

import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
import { Type } from "typebox";

export default defineToolPlugin({
  id: "llm-task",
  name: "LLM Task",
  description: "Generic JSON-only LLM tool for structured tasks.",
  configSchema: Type.Object({
    defaultProvider: Type.Optional(Type.String()),
    defaultModel:    Type.Optional(Type.String()),
    // ...
  }),
  tools: (tool) => [
    tool({
      name: "llm-task",
      description: "...",
      parameters: Type.Object({                       // TypeBox schema
        prompt: Type.String(),
        input:  Type.Optional(Type.Unknown()),
        // ...
      }),
      execute: async (params, config, context) => {   // 直接执行
        // 用 context.api 调用 host 服务(LLM 推理、存储等)
        // 返回 text 或 JSON
      },
      // 或者:
      // factory: ({ api, config, toolContext }) => createMyTool(api),
    }),
  ],
});

两种声明方式(tool-plugin.ts ToolPluginToolDefinition 的两个分支):

  • execute:声明完就是个完整的 tool,框架直接调
  • factory:返回一个或多个 AnyAgentTool,适合需要根据 config / capability 动态构造工具的场景(llm-task 用的就是 factory)

defineToolPlugin 内部把每个 tool 注册到 plugin entry,在 plugin activation 时跑 register(api),把 tool 加进 agent 可用 tool 池 (tool-plugin.ts:187-200)。

Manifest 让发现"不跑代码"

每个 plugin 同时有一份 openclaw.plugin.json (extensions/llm-task/openclaw.plugin.json):

{
  "id": "llm-task",
  "activation": { "onStartup": true },
  "configSchema": { "..." },
  "contracts": {
    "tools": ["llm-task"]
  },
  "toolMetadata": {
    "llm-task": { "optional": true }
  }
}

contracts.tools 把 plugin 提供的工具名列出来。这样 OpenClaw 在 还没加载 plugin 代码时就知道"装了这个 plugin 会多哪些工具"—— openclaw plugins list、setup、文档生成都不用 import 真实运行时。

这条规则在 src/plugins/CLAUDE.md 里写明:

Preserve manifest-first behavior: discovery, config validation, and setup should work from metadata before plugin runtime executes.

defineToolPlugin 输出的对象上挂了一个 [toolPluginMetadataSymbol], 包含同样的 metadata,这样 manifest 落地后可以做 contract 校验, 保证 manifest 跟代码不漂移(tool-plugin.ts:109-116)。

Plugin SDK 边界

来自根 AGENTS.md 的硬规则:

插件 prod 代码:no core src/**, src/plugin-sdk-internal/**, other plugin src/**, or relative outside package.

插件只能通过 openclaw/plugin-sdk/* 跟 core 交互。defineToolPluginregister(api) 拿到的 OpenClawPluginApi 就是 plugin 看得见的所有 host 能力(LLM 推理、存储、其他 plugin 句柄等),core 内部的具体实现 对 plugin 透明。

这条边界让 plugin tool 既能用上 host 内部能力(不像 MCP server 那样 完全隔离),又不会被 core 实现变更直接打死(只要 SDK 不破坏)。

仓库里有哪些 plugin tool

grep "contracts.*tools" extensions/*/openclaw.plugin.json 找到的:

插件 提供的能力
browser 浏览器自动化
canvas 画图
diffs 大文件 diff
llm-task "调一次 LLM 跑结构化任务"
memory-core 知识/记忆存取
memory-lancedb 向量记忆
memory-wiki wiki 风格记忆
tavily Web 搜索
firecrawl Web 抓取
feishu 飞书集成
qqbot QQ 集成
google-meet Google Meet 集成
file-transfer 文件传输
codex-supervisor Codex 任务编排
lobster (内部任务)

extensions/大多数其实不是 tool plugin,而是另外两类——见下节。

它在更大的 plugin 生态里的位置

extensions/ 里有三种主要 plugin,用不同的 define*Plugin 入口:

入口 用途 例子
defineToolPlugin 给 agent 注册工具 llm-task, tavily, browser
defineChannelPluginEntry 接入消息渠道 telegram, discord, imessage
defineSingleProviderPluginEntry 接入 LLM provider anthropic, openai, google
defineSetupPluginEntry 安装/onboarding 流程 各 plugin 的 setup 部分

不同入口在 SDK 内部都收敛到 definePluginEntry(src/plugin-sdk/plugin-entry.ts) 拿到统一的 plugin lifecycle(activation、register、config)。

channel / provider plugin 不直接给 agent 注册工具——它们暴露的是 消息渠道或模型推理能力。tool plugin 是这个生态里专门给 agent 加 function 的那一类

(理论上 channel plugin / provider plugin 也可以通过 register(api) 里 顺手注册 tool,但很少这么干;一旦这么做就既是 channel/provider plugin 又有 tool plugin 的属性。)

它怎么进到模型的 tool 列表

跟 MCP tool 走同一条路径(见 mcp-client-internals.md 的 "Attempt 引爆点"):

plugin 启动时(activation.onStartup 或按需)
  → tools 列表里每个 tool({...}) 被注册成 AnyAgentTool
  → 加进 agent 的可用 tool 池(在 host 进程内存里)

每次 attempt 开始
  → tools = [...native, ...plugin, ...bundleMcp, ...bundleLsp]
  → 走 provider adapter
  → 发给 LLM

模型看见的就是一个名字 llm-task 或者 tavily_search,没有任何 "这是 plugin"的标识。execute 时框架找到对应 tool,调它的 execute 函数,这一切都在 OpenClaw 同一个 Node 进程里,跟 MCP 走 stdio 子进程或 HTTP 完全不同。

跟 MCP tool 的关键对比

维度 plugin tool MCP tool
语言 TypeScript(锁死 Node) 任意(JSON-RPC)
进程 同进程 子进程 / 远端
启动开销 几乎 0(import 即可) spawn + 握手 + listTools
故障隔离 一个 plugin 崩了可能影响整个进程 子进程崩了不影响别人,有失败退避
访问 host 服务 通过 OpenClawPluginApi 完全隔离,什么都拿不到
跨应用复用 OpenClaw 专属 任何 MCP host 都能用
分发 npm package(@openclaw/plugin-*) 任何二进制 / 服务
适合做什么 深度集成 OpenClaw 内部能力的工具 通用、跨应用、可独立运行的工具

例子:llm-task 必须是 plugin tool,因为它要复用 OpenClaw 的 provider/auth/catalog 体系跑一次内嵌 LLM 推理; @modelcontextprotocol/server-filesystem 适合做 MCP server,因为它 跟 OpenClaw 没耦合,Claude Code 也能用。

在"两根轴"里的位置

回顾 mcp-overview.md 的所有权 × 时机两根轴, plugin tool 占了"用户/第三方拥有 + 编译时确定"那一格:

                  开发者拥有          用户/第三方拥有
              ┌──────────────────┬──────────────────┐
   编译时确定 │  native tool      │  plugin tool ★    │
              ├──────────────────┼──────────────────┤
   运行时发现 │  (动态 native)    │  MCP tool        │
              └──────────────────┴──────────────────┘

这个反例的价值在于:它证明了 "动态/静态" 跟 "开发者/用户" 是 两个独立的轴——不能简单说 "用户拥有的就一定是运行时发现"。 plugin tool 是用户拥有的(插件作者写、用户装),但因为还是 npm package 同语言同进程,完全可以编译时确定。

相关