From b95c8a4d95b7e8e821cf948fe0c02b1bb200bd7a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 17 May 2026 09:27:34 +0100 Subject: [PATCH] docs: add tool plugin authoring guide --- docs/cli/plugins.md | 3 +- docs/docs.json | 1 + docs/plugins/building-plugins.md | 13 +- docs/plugins/sdk-entrypoints.md | 5 +- docs/plugins/tool-plugins.md | 369 +++++++++++++++++++++++++++++++ 5 files changed, 384 insertions(+), 7 deletions(-) create mode 100644 docs/plugins/tool-plugins.md diff --git a/docs/cli/plugins.md b/docs/cli/plugins.md index c6d500516a8..9317e7eec14 100644 --- a/docs/cli/plugins.md +++ b/docs/cli/plugins.md @@ -90,7 +90,8 @@ npm run plugin:validate `defineToolPlugin`. `plugins build` imports that entry, reads its static tool metadata, writes `openclaw.plugin.json`, and keeps `package.json` `openclaw.extensions` aligned. `plugins validate` checks that the generated -manifest, package metadata, and current entry export still agree. +manifest, package metadata, and current entry export still agree. See +[Tool Plugins](/plugins/tool-plugins) for the full authoring workflow. The scaffold writes TypeScript source but generates metadata from the built `./dist/index.js` entry so the workflow also works with the published CLI. Use diff --git a/docs/docs.json b/docs/docs.json index 9cdc9c70086..da143712089 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1220,6 +1220,7 @@ "group": "Building plugins", "pages": [ "plugins/building-plugins", + "plugins/tool-plugins", "plugins/sdk-channel-plugins", "plugins/sdk-provider-plugins", "plugins/cli-backend-plugins", diff --git a/docs/plugins/building-plugins.md b/docs/plugins/building-plugins.md index e63bffd9898..1af5dd114f1 100644 --- a/docs/plugins/building-plugins.md +++ b/docs/plugins/building-plugins.md @@ -38,8 +38,11 @@ install from npm during the launch cutover. Map a local AI CLI into OpenClaw's text fallback runner - - Register agent tools, event hooks, or services - continue below + + Add simple typed agent tools with generated manifest metadata + + + Register event hooks, services, or advanced runtime integrations @@ -53,6 +56,7 @@ until the plugin is installed. This walkthrough creates a minimal plugin that registers an agent tool. Channel and provider plugins have dedicated guides linked above. +For the detailed tool-only workflow, see [Tool Plugins](/plugins/tool-plugins). @@ -133,8 +137,9 @@ and provider plugins have dedicated guides linked above. `defineToolPlugin` is for simple agent-tool plugins. For providers, hooks, services, and other advanced non-channel plugins, use `definePluginEntry`. For channels, use `defineChannelPluginEntry` - see - [Channel Plugins](/plugins/sdk-channel-plugins). For full entry point - options, see [Entry Points](/plugins/sdk-entrypoints). + [Channel Plugins](/plugins/sdk-channel-plugins). For the full + `defineToolPlugin` workflow, see [Tool Plugins](/plugins/tool-plugins). For + full entry point options, see [Entry Points](/plugins/sdk-entrypoints). diff --git a/docs/plugins/sdk-entrypoints.md b/docs/plugins/sdk-entrypoints.md index 5379901b661..947f267fb76 100644 --- a/docs/plugins/sdk-entrypoints.md +++ b/docs/plugins/sdk-entrypoints.md @@ -40,8 +40,9 @@ and inferred built JavaScript peers do not make an escaping `extensions` or `setupEntry` source path valid. - **Looking for a walkthrough?** See [Channel Plugins](/plugins/sdk-channel-plugins) - or [Provider Plugins](/plugins/sdk-provider-plugins) for step-by-step guides. + **Looking for a walkthrough?** See [Tool Plugins](/plugins/tool-plugins), + [Channel Plugins](/plugins/sdk-channel-plugins), or + [Provider Plugins](/plugins/sdk-provider-plugins) for step-by-step guides. ## `defineToolPlugin` diff --git a/docs/plugins/tool-plugins.md b/docs/plugins/tool-plugins.md new file mode 100644 index 00000000000..af6acb38cbc --- /dev/null +++ b/docs/plugins/tool-plugins.md @@ -0,0 +1,369 @@ +--- +summary: "Build simple typed agent tools with defineToolPlugin and openclaw plugins init/build/validate" +title: "Tool plugins" +sidebarTitle: "Tool Plugins" +read_when: + - You want to build a simple OpenClaw plugin that only adds agent tools + - You want to use defineToolPlugin instead of hand-writing plugin manifest metadata + - You need to scaffold, generate, validate, test, or publish a tool-only plugin +--- + +Tool plugins add agent-callable tools to OpenClaw without adding a channel, +model provider, hook, service, or setup backend. Use `defineToolPlugin` when the +plugin owns a fixed list of tools and you want OpenClaw to generate the manifest +metadata that keeps those tools discoverable without loading runtime code. + +The recommended flow is: + +1. Scaffold a package with `openclaw plugins init`. +2. Write tools with `defineToolPlugin`. +3. Build JavaScript. +4. Generate `openclaw.plugin.json` and `package.json` metadata with + `openclaw plugins build`. +5. Validate the generated metadata before publishing or installing. + +For provider, channel, hook, service, or mixed-capability plugins, start with +[Building plugins](/plugins/building-plugins), [Channel Plugins](/plugins/sdk-channel-plugins), +or [Provider Plugins](/plugins/sdk-provider-plugins) instead. + +## Requirements + +- Node >= 22. +- TypeScript ESM package output. +- `typebox` for config and tool parameter schemas. +- `openclaw >=2026.5.17`, the first OpenClaw version that exports + `openclaw/plugin-sdk/tool-plugin`. +- A package root that can ship `dist/`, `openclaw.plugin.json`, and + `package.json`. + +The generated plugin imports `typebox` at runtime, so keep `typebox` in +`dependencies`, not only `devDependencies`. + +## Quickstart + +Create a new plugin package: + +```bash +openclaw plugins init stock-quotes --name "Stock Quotes" +cd stock-quotes +npm install +npm run plugin:build +npm run plugin:validate +npm test +``` + +The scaffold creates: + +- `src/index.ts`: a `defineToolPlugin` entry with an `echo` tool. +- `src/index.test.ts`: a small metadata test. +- `tsconfig.json`: NodeNext TypeScript output to `dist/`. +- `package.json`: scripts, runtime dependencies, and + `openclaw.extensions: ["./dist/index.js"]`. +- `openclaw.plugin.json`: generated manifest metadata for the initial tool. + +Expected validation output: + +```text +Plugin stock-quotes is valid. +``` + +## Write a tool + +`defineToolPlugin` takes plugin identity, an optional config schema, and a +static list of tools. Parameter and config types are inferred from TypeBox +schemas. + +```typescript +import { Type } from "typebox"; +import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin"; + +export default defineToolPlugin({ + id: "stock-quotes", + name: "Stock Quotes", + description: "Fetch stock quote snapshots.", + configSchema: Type.Object({ + apiKey: Type.Optional(Type.String({ description: "Quote API key." })), + baseUrl: Type.Optional(Type.String({ description: "Quote API base URL." })), + }), + tools: (tool) => [ + tool({ + name: "stock_quote", + label: "Stock Quote", + description: "Fetch a stock quote snapshot.", + parameters: Type.Object({ + symbol: Type.String({ description: "Ticker symbol, for example OPEN." }), + }), + async execute({ symbol }, config, context) { + context.signal?.throwIfAborted(); + return { + symbol: symbol.toUpperCase(), + configured: Boolean(config.apiKey), + baseUrl: config.baseUrl ?? "https://api.example.com", + }; + }, + }), + ], +}); +``` + +Tool names are the stable API. Pick names that are unique, lowercase, and +specific enough to avoid collisions with core tools or other plugins. + +## Return values + +`defineToolPlugin` wraps plain return values into the OpenClaw tool-result +format: + +- Return a string when the model should see that exact text. +- Return a JSON-compatible value when you want the model to see formatted JSON + and OpenClaw to keep the original value in `details`. + +```typescript +tool({ + name: "echo_text", + description: "Echo input text.", + parameters: Type.Object({ + input: Type.String(), + }), + execute: ({ input }) => input, +}); +``` + +```typescript +tool({ + name: "echo_json", + description: "Echo input as structured JSON.", + parameters: Type.Object({ + input: Type.String(), + }), + execute: ({ input }) => ({ input, length: input.length }), +}); +``` + +Use `definePluginEntry` instead of `defineToolPlugin` when you need to return a +custom `AgentToolResult`, stream custom progress semantics, register dynamic +tools, or combine tools with hooks, services, providers, commands, or other +advanced surfaces. + +## Configuration + +`configSchema` is optional. If you omit it, OpenClaw uses a strict empty object +schema and the generated manifest still includes `configSchema`. + +```typescript +export default defineToolPlugin({ + id: "no-config-tools", + name: "No Config Tools", + description: "Adds tools that do not need configuration.", + tools: () => [], +}); +``` + +When you include `configSchema`, the second `execute` argument is typed from the +schema: + +```typescript +const configSchema = Type.Object({ + apiKey: Type.String(), +}); + +export default defineToolPlugin({ + id: "configured-tools", + name: "Configured Tools", + description: "Adds configured tools.", + configSchema, + tools: (tool) => [ + tool({ + name: "configured_ping", + description: "Check whether configuration is available.", + parameters: Type.Object({}), + execute: (_params, config) => ({ hasKey: config.apiKey.length > 0 }), + }), + ], +}); +``` + +OpenClaw reads plugin config from the plugin entry in the Gateway config. Do not +hard-code secrets in source or in docs examples. Use config, environment +variables, or SecretRefs according to the plugin's security model. + +## Generated metadata + +OpenClaw discovers installed plugins from cold metadata. It must be able to read +the plugin manifest before importing plugin runtime code. `defineToolPlugin` +therefore exposes static metadata, and `openclaw plugins build` writes that +metadata into the package. + +Run the generator after changing plugin id, name, description, config schema, +activation, or tool names: + +```bash +npm run build +openclaw plugins build --entry ./dist/index.js +``` + +For a one-tool plugin, the generated manifest looks like this: + +```json +{ + "id": "stock-quotes", + "name": "Stock Quotes", + "description": "Fetch stock quote snapshots.", + "version": "0.1.0", + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "activation": { + "onStartup": true + }, + "contracts": { + "tools": ["stock_quote"] + } +} +``` + +`contracts.tools` is the important discovery contract. It tells OpenClaw which +plugin owns each tool without loading every installed plugin runtime. If the +manifest is stale, the tool may be missing from discovery or the wrong plugin +may be blamed for a registration error. + +## Package metadata + +For the simple tool-plugin workflow, `openclaw plugins build` aligns +`package.json` to the selected single runtime entry: + +```json +{ + "type": "module", + "files": ["dist", "openclaw.plugin.json", "README.md"], + "dependencies": { + "typebox": "^1.1.38" + }, + "peerDependencies": { + "openclaw": ">=2026.5.17" + }, + "openclaw": { + "extensions": ["./dist/index.js"] + } +} +``` + +Use built JavaScript such as `./dist/index.js` for installed packages. Source +entries are useful in workspace development, but published packages should not +depend on TypeScript runtime loading. + +## Validate in CI + +Use `plugins build --check` to fail CI when generated metadata is stale without +rewriting files: + +```bash +npm run build +openclaw plugins build --entry ./dist/index.js --check +openclaw plugins validate --entry ./dist/index.js +npm test +``` + +`plugins validate` checks that: + +- `openclaw.plugin.json` exists and passes the normal manifest loader. +- The current entry exports `defineToolPlugin` metadata. +- Generated manifest fields match the entry metadata. +- `contracts.tools` matches the declared tool names. +- `package.json` points `openclaw.extensions` at the selected runtime entry. + +## Install and inspect locally + +From a separate OpenClaw checkout or installed CLI, install the package path: + +```bash +openclaw plugins install ./stock-quotes +openclaw plugins inspect stock-quotes --runtime +``` + +For a packaged smoke, pack first and install the tarball: + +```bash +npm pack +openclaw plugins install npm-pack:./openclaw-plugin-stock-quotes-0.1.0.tgz +openclaw plugins inspect stock-quotes --runtime --json +``` + +After installation, start or restart the Gateway and ask the agent to use the +tool. If you are debugging tool visibility, inspect the plugin runtime and the +effective tool catalog before changing the code. + +## Publish + +Publish through ClawHub when the package is ready: + +```bash +clawhub package publish your-org/stock-quotes --dry-run +clawhub package publish your-org/stock-quotes +``` + +Install with an explicit ClawHub locator: + +```bash +openclaw plugins install clawhub:your-org/stock-quotes +``` + +Bare npm package specs remain supported during the launch cutover, but ClawHub +is the preferred discovery and distribution surface for OpenClaw plugins. + +## Troubleshooting + +### `plugin entry not found: ./dist/index.js` + +The selected entry file does not exist. Run `npm run build`, then rerun +`openclaw plugins build --entry ./dist/index.js` or +`openclaw plugins validate --entry ./dist/index.js`. + +### `plugin entry does not expose defineToolPlugin metadata` + +The entry did not export a value created by `defineToolPlugin`. Check that the +module default export is the `defineToolPlugin(...)` result, or pass the correct +entry with `--entry`. + +### `openclaw.plugin.json generated metadata is stale` + +The manifest no longer matches the entry metadata. Run: + +```bash +npm run build +openclaw plugins build --entry ./dist/index.js +``` + +Commit both `openclaw.plugin.json` and `package.json` changes. + +### `package.json openclaw.extensions must include ./dist/index.js` + +The package metadata points at a different runtime entry. Run +`openclaw plugins build --entry ./dist/index.js` so the generator aligns the +package metadata with the entry you intend to ship. + +### `Cannot find package 'typebox'` + +The built plugin imports `typebox` at runtime. Keep `typebox` in +`dependencies`, reinstall package dependencies, rebuild, and rerun validation. + +### Tool does not appear after install + +Check these in order: + +1. `openclaw plugins inspect --runtime` +2. `openclaw plugins validate --root --entry ./dist/index.js` +3. `openclaw.plugin.json` has `contracts.tools` with the expected tool names. +4. `package.json` has `openclaw.extensions: ["./dist/index.js"]`. +5. The Gateway was restarted or reloaded after installing the plugin. + +## See also + +- [Building plugins](/plugins/building-plugins) +- [Plugin entry points](/plugins/sdk-entrypoints) +- [Plugin SDK subpaths](/plugins/sdk-subpaths) +- [Plugin manifest](/plugins/manifest) +- [Plugins CLI](/cli/plugins) +- [ClawHub publishing](/clawhub/publishing)