zhin-tool-service
Zhin Tool Service Guide
Use this skill to create, register, and manage tools in Zhin plugins. The Tool Service provides a unified abstraction that bridges AI agent tool-calling and traditional message commands.
Core Concepts
A Tool in Zhin can serve three purposes simultaneously:
- AI Agent tool — callable by LLM agents via function calling
- Message Command — executable by users as a chat command
- MCP tool — exposable via the Model Context Protocol
ZhinTool Class (Chain API)
ZhinTool provides a builder-style API similar to MessageCommand:
import { usePlugin, ZhinTool } from 'zhin.js'
const plugin = usePlugin()
const weatherTool = new ZhinTool('weather')
.desc('Query weather information')
.param('city', { type: 'string', description: 'City name' }, true)
.param('days', { type: 'number', description: 'Forecast days' })
.platform('qq', 'telegram')
.scope('group', 'private')
.permission('user')
.usage('Query the weather for a city')
.examples('weather Beijing', 'weather Tokyo 3')
.alias('天气')
.execute(async (args, ctx) => {
return `Weather in ${args.city}: Sunny`
})
.action(async (message, result) => {
return `Weather: ${result.params.city}`
})
plugin.addTool(weatherTool)
ZhinTool Methods
| Method | Description |
|---|---|
.desc(text) |
Set tool description (for AI and help) |
.param(name, schema, required?) |
Add a parameter (ordered) |
.platform(...names) |
Restrict to platforms (e.g. 'qq', 'telegram') |
.scope(...types) |
Restrict to scene types ('private', 'group', 'channel') |
.permission(level) |
Set minimum permission level |
.permit(...perms) |
Add legacy permission strings (compatible with MessageCommand) |
.tag(...tags) |
Add tags for filtering |
.hidden(value?) |
Hide from help listings |
.usage(...texts) |
Add usage descriptions |
.examples(...texts) |
Add usage examples |
.alias(...names) |
Add command aliases |
.pattern(pat) |
Override the auto-generated command pattern |
.execute(fn) |
Required. Set the AI tool execution function (args, ctx) => result |
.action(fn) |
Set the command callback (message, matchResult) => result |
execute vs action
execute(required) — Called when the tool is invoked by an AI agent. Receives(args, context?)whereargsare parsed parameters andcontextis aToolContext.action(optional) — Called when the tool is invoked as a chat command. Receives(message, matchResult)just likeMessageCommand.action. If omitted,executeis used as a fallback to auto-generate the command handler.
defineTool Helper
Use defineTool for a type-safe object-style definition:
import { usePlugin, defineTool } from 'zhin.js'
const plugin = usePlugin()
const calcTool = defineTool<{ expression: string }>({
name: 'calculator',
description: 'Calculate a math expression',
parameters: {
type: 'object',
properties: {
expression: {
type: 'string',
description: 'Math expression',
},
},
required: ['expression'],
},
command: {
pattern: 'calc <expression:rest>',
alias: ['calculate'],
usage: ['Calculate math expressions'],
},
execute: async (args) => {
// args.expression has full type inference
const math = await import('mathjs')
return `Result: ${math.evaluate(args.expression)}`
},
})
plugin.addTool(calcTool)
The generic parameter <{ expression: string }> gives you type checking on args inside execute.
Registering Tools
addTool — register with command generation
plugin.addTool(tool)
Registers the tool and auto-generates a corresponding MessageCommand (unless command: false).
addToolOnly — register without command
plugin.addToolOnly(tool)
Registers the tool for AI agent use only, without generating a command.
Both methods return a dispose function:
const dispose = plugin.addTool(weatherTool)
// Later: dispose() removes the tool and its command
Tools are automatically cleaned up when the plugin is disposed.
Tool ↔ Command Conversion
The ToolService can convert in both directions:
Tool → Command (automatic)
When you call plugin.addTool(tool), a MessageCommand is auto-generated from the tool's parameters:
// Tool with parameters: { city: string (required), days: number (optional) }
// Generates command pattern: "weather <city:text> [days:number]"
Parameter ordering: required parameters come first, then optional ones.
Command → Tool (via ToolService)
Existing commands can be wrapped as tools for AI agent use:
const toolService = plugin.inject('tool')
const tool = toolService.commandToTool(existingCommand, 'my-plugin')
collectAll — gather all available tools
const toolService = plugin.inject('tool')
const allTools = toolService.collectAll(plugin)
This collects tools from three sources:
- ToolService tools — tools registered via
addTool/addToolOnly - Commands — existing
MessageCommandinstances converted to tools - Adapter tools — platform-specific tools from adapters
Permission Levels
Tools support a hierarchical permission system (from lowest to highest):
| Level | Priority | Description |
|---|---|---|
user |
0 | Regular user (default) |
group_admin |
1 | Group administrator |
group_owner |
2 | Group owner |
bot_admin |
3 | Bot administrator |
owner |
4 | Zhin instance owner |
new ZhinTool('admin-action')
.desc('Admin-only operation')
.permission('bot_admin')
.execute(async (args) => { /* ... */ })
The ToolService automatically checks permission levels when filtering tools for a given context.
Platform and Scope Filtering
Restrict where a tool can be used:
new ZhinTool('group-only')
.platform('qq', 'discord') // Only on QQ and Discord
.scope('group') // Only in group chats
.execute(async (args) => { /* ... */ })
The filterByContext method checks all restrictions:
const toolService = plugin.inject('tool')
const context = {
platform: 'qq',
scope: 'group',
senderPermissionLevel: 'user',
}
const available = toolService.filterByContext(allTools, context)
ToolContext
When a tool is executed, it receives a ToolContext with runtime information:
interface ToolContext {
platform?: string // Source platform (e.g. 'qq')
botId?: string // Bot identifier
sceneId?: string // Scene ID (group/channel/user)
senderId?: string // Sender's user ID
scope?: ToolScope // 'private' | 'group' | 'channel'
message?: Message // Original message object
senderPermissionLevel?: ToolPermissionLevel
isGroupAdmin?: boolean
isGroupOwner?: boolean
isBotAdmin?: boolean
isOwner?: boolean
extra?: Record<string, any>
}
ToolService API
Access the ToolService via context injection:
const toolService = plugin.inject('tool')
| Method | Description |
|---|---|
add(tool, pluginName, generateCommand?) |
Register a tool |
remove(name) |
Remove a tool by name |
get(name) |
Get a tool by name |
getAll() |
Get all registered tools |
getByTags(tags) |
Filter tools by tags |
execute(name, args, context?) |
Execute a tool by name |
commandToTool(command, pluginName) |
Convert a Command to a Tool |
collectAll(plugin) |
Collect all tools from all sources |
filterByContext(tools, context) |
Filter tools by platform/scope/permission |
Parameter Schema
Tool parameters use JSON Schema format with Zhin extensions:
{
type: 'object',
properties: {
city: {
type: 'string',
description: 'City name',
paramType: 'text', // Zhin extension: command param type
},
count: {
type: 'number',
description: 'Number of results',
default: 5,
},
format: {
type: 'string',
enum: ['json', 'text'],
description: 'Output format',
},
},
required: ['city'],
}
The paramType extension controls how the parameter is parsed in command mode:
'text'(default for strings)'number'(default for numbers)'boolean''rest'(captures all remaining text)
Serialization
toJSON — for AI function calling
const tool = new ZhinTool('weather').desc('...').param(...)
const schema = tool.toJSON()
// Returns: { name, description, parameters, platforms?, scopes?, permissionLevel?, tags? }
// Compatible with OpenAI Function Calling format
help — human-readable help text
console.log(tool.help)
// weather <city:text> [days:number]
// Query weather information
// Parameters:
// city: string (required) City name
// days: number (optional) Forecast days
Disabling Command Generation
Set command: false to prevent auto-generating a command:
const tool = defineTool({
name: 'internal_lookup',
description: 'Internal data lookup',
parameters: { type: 'object', properties: {} },
command: false,
execute: async () => { /* AI-only tool */ },
})
Checklist
- Use
ZhinTool(chain API) ordefineTool(object API) to create tools. - Always provide
execute()— it is required for AI agent invocation. - Use
action()on ZhinTool for custom command behavior; omit it to auto-generate fromexecute. - Register with
plugin.addTool()(tool + command) orplugin.addToolOnly()(tool only). - Set
permission(),platform(), andscope()to control access. - Use
collectAll()andfilterByContext()when building AI agent tool lists. - Tools are auto-disposed when their parent plugin is disposed.