Tool Component
@Tool is the most core component type in the MaiBot plugin system. It allows plugins to expose callable tool functions to the LLM, enabling the LLM to proactively call external capabilities during the reasoning process—such as searching knowledge bases, querying databases, calling external APIs, etc.
Tool vs Action
@Action is a legacy decorator that the SDK automatically converts into an @Tool declaration. New plugins should use @Tool directly and avoid @Action. See Action Component (Legacy) for details.
Decorator Signature
from maibot_sdk import Tool
from maibot_sdk.types import ToolParameterInfo, ToolParamType
@Tool(
name: str, # 工具名称(必填)
description: str = "", # 工具描述,作为备选描述字段
brief_description: str = "", # 简要描述,优先级高于 description
detailed_description: str = "", # 详细描述,可包含参数说明等
parameters: list[ToolParameterInfo] | dict | None = None, # 参数定义
**metadata, # 额外元数据
)Argument Descriptions
namestr— Tool name, must be unique within the plugin. The LLM calls the tool via this name.descriptionstr— Alternative tool description. Used whenbrief_descriptionis empty.brief_descriptionstr— Primary tool description (preferred). A summary of the tool description sent to the LLM to help it decide whether it needs to call it.detailed_descriptionstr— Detailed description, which can include parameter usage instructions, notes, etc. The SDK automatically merges the parameter Schema to generate a complete description.parameterslist | dict | None— Tool parameter definitions, supporting two formats (see below).core_toolbool— Optional, passed through**metadata, defaults toFalse. WithTruethe tool enters the core tool list and is directly visible to the LLM on every turn (equivalent tovisibility="visible"whenvisibilityis not set explicitly); with the defaultFalseit goes into the deferred pool and only appears in later turns after the model discovers it through tool search. More core tools mean a higher model selection cost, so reserveTruefor high-frequency, low-risk tools.
Description field conventions:
description: Description of the tool, including usage methods, scenarios, and notes. Whenbrief_descriptionis empty,descriptionserves as the fallback description.brief_description: A brief description used by the main program or small models to quickly determine "what this tool does".detailed_description: A detailed description of parameters, required items, optional items, and invocation constraints.
Parameter Definition
Method 1: Structured Parameters (Recommended)
Use an ToolParameterInfo list to declare parameters; the SDK automatically generates a JSON Schema:
from maibot_sdk import Tool, MaiBotPlugin
from maibot_sdk.types import ToolParameterInfo, ToolParamType
class MyPlugin(MaiBotPlugin):
@Tool(
"search",
brief_description="搜索互联网获取信息",
detailed_description="使用搜索引擎查找相关信息。参数说明:\n- query:string,必填。搜索关键词。\n- limit:integer,可选。返回结果数量上限。",
parameters=[
ToolParameterInfo(
name="query",
param_type=ToolParamType.STRING,
description="搜索关键词",
required=True,
),
ToolParameterInfo(
name="limit",
param_type=ToolParamType.INTEGER,
description="返回结果数量上限",
required=False,
default=5,
),
],
)
async def handle_search(self, query: str, limit: int = 5, **kwargs):
results = await self._do_search(query, limit)
return {"results": results}Method 2: dict parameters (Compatible with legacy declarations)
Pass a dictionary in JSON Schema style directly:
class MyPlugin(MaiBotPlugin):
@Tool(
"search",
brief_description="搜索互联网获取信息",
parameters={
"query": {"type": "string", "description": "搜索关键词"},
"limit": {"type": "integer", "description": "返回结果数量上限", "default": 5},
},
)
async def handle_search(self, query: str, limit: int = 5, **kwargs):
results = await self._do_search(query, limit)
return {"results": results}ToolParameterInfo Fields
namestr— Parameter nameparam_typeToolParamType— Parameter type enumdescriptionstr— Parameter descriptionrequiredbool· DefaultTrue— Whether requiredenum_valueslist | None— List of optional enum valuesdefaultAny— Default valueitems_schemadict | None— Array element Schema (used whenparam_type=ARRAYis set)propertiesdict | None— Object property definitions (used whenparam_type=OBJECTis set)required_propertieslist[str]— Required fields within the objectadditional_propertiesbool | dict | None— Whether extra fields are allowed
ToolParamType Enum
STRING→ JSON Schemastring— StringINTEGER→ JSON Schemainteger— IntegerNUMBER→ JSON Schemanumber— Number (integer or float)FLOAT→ JSON Schemanumber— Float (equivalent to NUMBER)BOOLEAN→ JSON Schemaboolean— BooleanARRAY→ JSON Schemaarray— ArrayOBJECT→ JSON Schemaobject— Object
Handler Functions
Tool handlers are asynchronous methods on the plugin class that receive keyword arguments corresponding to parameter names and **kwargs:
@Tool("greet", description="向用户打招呼",
parameters=[
ToolParameterInfo(name="stream_id", param_type=ToolParamType.STRING,
description="当前聊天流 ID", required=True),
])
async def handle_greet(self, stream_id: str, **kwargs):
await self.ctx.send.text("你好!", stream_id)
return {"success": True, "message": "已回复"}Return Value
The return value of a Tool handler is returned to the LLM as the tool execution result. The return value can be:
dict: Recommended, as the LLM can understand structured datastr: Simple text result- Other serializable values
The LLM decides the next step based on the return value (e.g., replying to the user, calling other tools, etc.).
When returning a dict, you may also include the boolean field stop_after_execution — set it to true to request ending the current Planner run after the whole tool batch finishes, and wait for new messages before continuing:
- Takes effect only when the tool succeeds; if any successful result in the same batch carries
true, it applies - The field must be a boolean; any other type makes this tool call be treated as a failure
- When omitted, it defaults to
falseand behavior is unchanged
async def handle_shutdown(self, stream_id: str, **kwargs):
await self.ctx.send.text("本轮操作已完成。", stream_id)
return {"success": True, "stop_after_execution": True}Returning Images and Other Media
If a Tool needs to pass an image to Maisaka for further observation or reasoning, do not embed base64 images directly into content. It is recommended to return dict, placing the text for the LLM to read in content and the image itself in content_items:
from base64 import b64encode
async def handle_draw(self, prompt: str, **kwargs):
image_bytes = await self._draw_image(prompt)
return {
"success": True,
"content": "图片已生成,请查看索引对应的图片内容。",
"content_items": [
{
"type": "image",
"data": b64encode(image_bytes).decode("ascii"),
"mime_type": "image/png",
"name": "result.png",
"description": "根据提示词生成的图片",
}
],
}You can also use data URLs:
return {
"success": True,
"content": "图片已生成。",
"content_items": [
{
"type": "image",
"uri": f"data:image/png;base64,{b64encode(image_bytes).decode('ascii')}",
"mime_type": "image/png",
"name": "result.png",
}
],
}Common fields in content_items are as follows:
type/content_typestr— Content type. Images useimage;audio,resource_link,resource, andbinaryare also supporteddata/base64str— Base64 string of the media binary; for images, prefer this fielduristr— Media URI. Images may usedata:image/...;base64,...mime_typestr— MIME type, e.g.image/png,image/jpeg,image/webpnamestr— File name or display namedescriptionstr— Short description of the media contentmetadatadict— Extra metadata
Maisaka splits this kind of return into two context messages: the first is still a plain-text Tool Result containing a media index such as tool_result:<tool_call_id>:1; a normal user message is then appended, carrying the same index and the real image component. This keeps compatibility with model APIs that do not support returning images directly inside a tool result, while letting models with vision input observe the image as an ordinary image message.
View logic
In the LLM input and Prompt preview, the extracted image follows the normal ImageComponent display logic and looks basically the same as a real received image message. The difference is that its source is marked as tool_result_media, and its message ID is the tool media index, so it is not treated as a platform message actually sent by the user.
Common Extra Arguments in kwargs
stream_idstr— Current chat stream ID, usable for sending messages withctx.send.text()and similarmessagedict— The original message that triggered this tool call
stream_id
stream_id is one of the most important arguments of a Tool component — it identifies the current conversation stream. Use ctx.send.text("message", stream_id) to send a message into the matching chat stream.
Description Generation Rules
The SDK automatically generates the complete description for a tool, with the following priority:
brief_description: used first (if provided)description: fallback (used whenbrief_descriptionis empty)detailed_description: if provided, the SDK merges it with the parameter Schema to generate the complete description- Auto-generated: if none of the fields above are provided, the SDK uses
"工具 {name}"as the description
The auto-generated parameter description has this format:
参数说明:
- query:string,必填。搜索关键词
- limit:integer,可选。返回结果数量上限。默认值:5Complete Example
from typing import Any
from maibot_sdk import MaiBotPlugin, Tool
from maibot_sdk.types import ToolParameterInfo, ToolParamType
class SearchPlugin(MaiBotPlugin):
async def on_load(self) -> None:
self.ctx.logger.info("搜索插件已加载")
async def on_unload(self) -> None:
pass
async def on_config_update(self, scope: str, config_data: dict, version: str) -> None:
pass
@Tool(
"search_web",
description="搜索互联网获取信息",
parameters=[
ToolParameterInfo(
name="query",
param_type=ToolParamType.STRING,
description="搜索关键词",
required=True,
),
ToolParameterInfo(
name="limit",
param_type=ToolParamType.INTEGER,
description="返回结果数量上限",
required=False,
default=5,
),
],
)
async def search(self, query: str, limit: int = 5, **kwargs):
"""搜索互联网"""
results = await self._do_search(query, limit)
return {"results": results, "count": len(results)}
@Tool(
"get_weather",
description="获取指定城市的天气信息",
parameters=[
ToolParameterInfo(
name="city",
param_type=ToolParamType.STRING,
description="城市名称",
required=True,
),
],
)
async def get_weather(self, city: str, **kwargs):
"""查询天气"""
weather = await self._fetch_weather(city)
return {"city": city, "weather": weather}
async def _do_search(self, query: str, limit: int) -> list:
# 实际搜索逻辑
return []
async def _fetch_weather(self, city: str) -> dict:
# 实际天气查询逻辑
return {}
def create_plugin():
return SearchPlugin()Relationship with Legacy Action
The @Action decorator is deprecated in SDK 2.0 and is internally converted into an @Tool declaration:
action_parameters→ converted into the ToolparametersSchema (all parameter types are normalized tostring)activation_type/activation_keywords→ kept as Toolmetadata- Using
@Actionraises aDeprecationWarning
New plugins should use @Tool directly to benefit from richer parameter type support and more standard Schema generation.
Verify and Troubleshoot
Verification — have the LLM call the tool once in chat (a tool in the deferred pool must be found with tool_search first): the readable text you prepared appears in the Tool Result, arguments arrive at the handler as declared, and the return value is parsed — the whole Tool chain works.
- Arguments never reach the handler, or invocation raises
TypeError— eachToolParameterInfo.namemust match a named parameter of the handler (declarelimitif you list it, or keep**kwargsas a fallback), and types must line up withToolParamType:ARRAYneedsitems_schema,OBJECTusesproperties/required_properties. Otherwise the generated JSON Schema and the function signature disagree. - The tool is registered but the model never sees it — with no
visibilityset,core_tooldefaults toFalse, so the tool goes into the deferred pool and the model must find it withtool_searchbefore it appears in later turns; setcore_tool=True(equivalent tovisibility="visible"whenvisibilityis not set) only for high-frequency, low-risk tools, since more core tools make model selection more expensive. - The model gets no readable result — for a
dictreturn, the Host takescontent(falling back tomessage) as the text for the LLM; with only custom fields such as{"results": [...]},contentis empty, the Tool Result carries no readable conclusion, and the model easily misjudges it. Put the conclusion text incontentand keep structured data in your own fields. - The tool is treated as failed —
stop_after_executionmust be a boolean; returning"true"or1makes that invocation count as a failure, and onlytrueon a successful result has any effect, since the field is ignored whensuccessisFalse. - Images never reach the model's context — do not stuff base64 into
content; return media throughcontent_items(type: "image"withdataoruri, plus a correctmime_type). Otherwise the model sees only a lump of plain text, or the item fails to parse because a field is invalid.