tooluniverse-custom-tool
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAdding Custom Tools to ToolUniverse
向ToolUniverse添加自定义工具
Three ways to add tools — pick the one that fits your needs:
| Approach | When to use |
|---|---|
| JSON config | REST API with standard request/response — no coding needed |
| Python class (workspace) | Custom logic for local/private use only |
| Plugin package | Reusable tools you want to share or install via pip |
添加工具的三种方式——选择最适合你的一种:
| 实现方式 | 适用场景 |
|---|---|
| JSON配置 | 适用于标准请求/响应的REST API —— 无需编写代码 |
| Python类(工作区) | 仅用于本地/私有的自定义逻辑 |
| 插件包 | 可复用、可分享、可通过pip安装的工具集 |
Option A — Workspace tools (local use)
选项A —— 工作区工具(本地使用)
Tools in are auto-discovered at startup. No installation needed.
.tooluniverse/tools/bash
mkdir -p .tooluniverse/tools.tooluniverse/tools/bash
mkdir -p .tooluniverse/toolsJSON config
JSON配置
Create :
.tooluniverse/tools/my_tools.jsonjson
[
{
"name": "MyAPI_search",
"description": "Search my internal database. Returns matching records with id, title, and score.",
"type": "BaseRESTTool",
"fields": {
"endpoint": "https://my-api.example.com/search"
},
"parameter": {
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "Search query"
},
"limit": {
"type": ["integer", "null"],
"description": "Max results to return (default 10)"
}
},
"required": ["q"]
}
}
]One JSON file can define multiple tools — just add more objects to the array.
For the full JSON field reference, see references/json-tool.md.
创建:
.tooluniverse/tools/my_tools.jsonjson
[
{
"name": "MyAPI_search",
"description": "Search my internal database. Returns matching records with id, title, and score.",
"type": "BaseRESTTool",
"fields": {
"endpoint": "https://my-api.example.com/search"
},
"parameter": {
"type": "object",
"properties": {
"q": {
"type": "string",
"description": "Search query"
},
"limit": {
"type": ["integer", "null"],
"description": "Max results to return (default 10)"
}
},
"required": ["q"]
}
}
]单个JSON文件可定义多个工具——只需向数组中添加更多对象即可。
完整的JSON字段参考,请查看references/json-tool.md。
Python class
Python类
Create :
.tooluniverse/tools/my_tool.pypython
from tooluniverse.tool_registry import register_tool
@register_tool
class MyAPI_search:
name = "MyAPI_search"
description = "Search my internal database. Returns matching records with id, title, and score."
input_schema = {
"type": "object",
"properties": {
"q": {"type": "string", "description": "Search query"},
"limit": {"type": "integer", "description": "Max results (default 10)"}
},
"required": ["q"]
}
def run(self, q: str, limit: int = 10) -> dict:
import requests
resp = requests.get(
"https://my-api.example.com/search",
params={"q": q, "limit": limit},
timeout=30,
)
resp.raise_for_status()
return {"status": "success", "data": resp.json()}Note: workspace Python tools use — arguments are unpacked as keyword
arguments matching the properties.
run(self, **named_params)input_schemaFor the full Python class reference, see references/python-tool.md.
创建:
.tooluniverse/tools/my_tool.pypython
from tooluniverse.tool_registry import register_tool
@register_tool
class MyAPI_search:
name = "MyAPI_search"
description = "Search my internal database. Returns matching records with id, title, and score."
input_schema = {
"type": "object",
"properties": {
"q": {"type": "string", "description": "Search query"},
"limit": {"type": "integer", "description": "Max results (default 10)"}
},
"required": ["q"]
}
def run(self, q: str, limit: int = 10) -> dict:
import requests
resp = requests.get(
"https://my-api.example.com/search",
params={"q": q, "limit": limit},
timeout=30,
)
resp.raise_for_status()
return {"status": "success", "data": resp.json()}注意:工作区Python工具使用——参数会被解包为与属性匹配的关键字参数。
run(self, **named_params)input_schema完整的Python类参考,请查看references/python-tool.md。
Test workspace tools
测试工作区工具
bash
undefinedbash
undefinedUses test_examples from the tool's JSON config — zero config needed
使用工具JSON配置中的test_examples —— 无需额外配置
tu test MyAPI_search
tu test MyAPI_search
Single ad-hoc call
单次临时调用
tu test MyAPI_search '{"q": "test"}'
tu test MyAPI_search '{"q": "test"}'
Full config with assertions
带断言的完整配置测试
tu test --config my_tool_tests.json
`tu test` automatically runs these checks on every call:
- Result is not None or empty
- `return_schema` validation — validates `result["data"]` against the JSON Schema defined in `return_schema` (if present)
- `expect_status` and `expect_keys` — only if set in the config file
**Gotcha:** `tu test` does NOT verify that results are non-empty. An empty array `[]` satisfies
`"type": "array"` and passes all checks. Make sure your `test_examples` use args that actually
return results — otherwise a completely broken tool can pass all tests silently.
**Verify test_examples manually before finalizing.** Run a quick Python snippet against
the real API with your chosen args BEFORE writing them into `test_examples`. Some APIs require
all query words to appear literally in a title field (`intitle`-style); overly specific queries
like "I2C pull-up resistor value" will return 0 results even though the tool works. Use 2-4 key
words that are reliably present in real content.
Use `urllib` rather than `curl` for API verification — `curl` requires shell quoting tricks and
may not follow redirects correctly, while `urllib` matches what the tool will actually do:
```python
import urllib.request, json
with urllib.request.urlopen("https://api.example.com/search?q=test") as r:
print(json.dumps(json.loads(r.read()), indent=2))Also check that the URL is still a real JSON API before writing any tool code. Some
candidate URLs (e.g. ) may redirect to a GitHub Pages
static site that returns HTML, not JSON. A quick urllib fetch will reveal this immediately.
certification.oshwa.org/api/projectsmy_tool_tests.jsonjson
{
"tool_name": "MyAPI_search",
"tests": [
{
"name": "basic search",
"args": {"q": "climate change"},
"expect_status": "success",
"expect_keys": ["data"]
}
]
}Add and to your JSON config for best coverage:
test_examplesreturn_schemajson
{
"name": "MyAPI_search",
...
"test_examples": [
{"q": "climate change"},
{"q": "CRISPR", "limit": 3}
],
"return_schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"score": { "type": "number" }
}
}
}
}tu testresult["data"]return_schemarun()"data"- If is a list:
data"type": "array" - If is a dict:
data"type": "object"
tu test --config my_tool_tests.json
`tu test`会自动对每次调用执行以下检查:
- 结果不为None或空值
- `return_schema`验证——验证`result["data"]`是否符合`return_schema`中定义的JSON Schema(如果存在)
- `expect_status`和`expect_keys`——仅在配置文件中设置时生效
**注意事项**:`tu test`不会验证结果是否非空。空数组`[]`满足`"type": "array"`并通过所有检查。确保你的`test_examples`使用能实际返回结果的参数——否则完全失效的工具也能静默通过所有测试。
**在最终确定前手动验证test_examples**。在将选定的参数写入`test_examples`之前,先运行一段简单的Python代码调用真实API进行验证。有些API要求所有查询词必须字面出现在标题字段中(类似`intitle`风格);过于具体的查询如"I2C pull-up resistor value"会返回0条结果,即使工具本身是正常工作的。使用2-4个在真实内容中可靠存在的关键词。
使用`urllib`而非`curl`进行API验证——`curl`需要shell转义技巧,且可能无法正确跟随重定向,而`urllib`与工具实际执行的操作一致:
```python
import urllib.request, json
with urllib.request.urlopen("https://api.example.com/search?q=test") as r:
print(json.dumps(json.loads(r.read()), indent=2))在编写任何工具代码前,还要检查URL是否为有效的JSON API。一些候选URL(如)可能会重定向到GitHub Pages静态站点,返回HTML而非JSON。快速使用urllib请求就能立即发现这个问题。
certification.oshwa.org/api/projectsmy_tool_tests.jsonjson
{
"tool_name": "MyAPI_search",
"tests": [
{
"name": "basic search",
"args": {"q": "climate change"},
"expect_status": "success",
"expect_keys": ["data"]
}
]
}为了获得最佳测试覆盖率,向JSON配置中添加和:
test_examplesreturn_schemajson
{
"name": "MyAPI_search",
...
"test_examples": [
{"q": "climate change"},
{"q": "CRISPR", "limit": 3}
],
"return_schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"score": { "type": "number" }
}
}
}
}tu testreturn_schemaresult["data"]run()"data"- 如果是列表:
data"type": "array" - 如果是字典:
data"type": "object"
Use with MCP server
与MCP服务器配合使用
Tools in are automatically available when you run:
.tooluniverse/tools/bash
tu serve # MCP stdio server (Claude Desktop, etc.)
tooluniverse # sameThe workspace directory is auto-detected in this priority order:
flag → env var → (current dir) → (global)
--workspaceTOOLUNIVERSE_HOME./.tooluniverse/~/.tooluniverse/当你运行以下命令时,目录下的工具会自动可用:
.tooluniverse/tools/bash
tu serve # MCP标准输入输出服务器(适用于Claude Desktop等)
tooluniverse # 同上工作区目录的自动检测优先级为:参数 → 环境变量 → 当前目录下的 → 全局目录
--workspaceTOOLUNIVERSE_HOME./.tooluniverse/~/.tooluniverse/Point to a different tools directory
指定其他工具目录
Add a entry in :
sources.tooluniverse/profile.yamlyaml
name: my-profile
sources:
- ./my-custom-tools/ # relative to profile.yaml location
- /absolute/path/tools/Then start with:
bash
tu serve --load .tooluniverse/profile.yaml在中添加条目:
.tooluniverse/profile.yamlsourcesyaml
name: my-profile
sources:
- ./my-custom-tools/ # 相对于profile.yaml的路径
- /absolute/path/tools/然后启动:
bash
tu serve --load .tooluniverse/profile.yamlOption B — Plugin package (shareable, pip-installable)
选项B —— 插件包(可分享、可通过pip安装)
Use this when you want to distribute tools as a reusable Python package that other users can
install with . The plugin package has the same directory layout as a workspace, plus a
that declares the entry point.
pip installpyproject.toml当你希望将工具作为可复用的Python包分发,让其他用户可以通过安装时,可使用此方式。插件包的目录结构与工作区相同,额外增加一个文件用于声明入口点。
pip installpyproject.tomlPackage layout
包目录结构
my_project_root/ # directory containing pyproject.toml
pyproject.toml
my_tools_package/ # importable Python package (matches entry-point value)
__init__.py # minimal — one-line docstring, no registration code
my_api_tool.py # tool class(es) with @register_tool
data/
my_api_tools.json # JSON tool configs (type must match registered class name)
profile.yaml # optional: name, description, required_envJSON config files are discovered from both and the package root directory. The convention is .
data/data/my_project_root/ # 包含pyproject.toml的目录
pyproject.toml
my_tools_package/ # 可导入的Python包(需与入口点值匹配)
__init__.py # 保持简洁——仅一行文档字符串,无需注册代码
my_api_tool.py # 带有@register_tool装饰器的工具类
data/
my_api_tools.json # JSON工具配置(type必须与注册的类名匹配)
profile.yaml # 可选:名称、描述、所需环境变量JSON配置文件会在目录和包根目录中被自动识别,通常建议放在目录下。
data/data/pyproject.toml
entry point
pyproject.tomlpyproject.toml
入口点
pyproject.tomltoml
[project.entry-points."tooluniverse.plugins"]
my-tools = "my_tools_package"The value () must be the importable Python package name.
my_tools_packagetoml
[project.entry-points."tooluniverse.plugins"]
my-tools = "my_tools_package"值()必须是可导入的Python包名称。
my_tools_packagePython class in a plugin package
插件包中的Python类
Plugin package tools use and receive all arguments as a single :
BaseToolDictpython
import requests
from typing import Dict, Any
from tooluniverse.base_tool import BaseTool
from tooluniverse.tool_registry import register_tool
@register_tool("MyAPITool")
class MyAPITool(BaseTool):
"""Tool description here."""
def __init__(self, tool_config: Dict[str, Any]):
super().__init__(tool_config)
self.timeout = tool_config.get("timeout", 30)
fields = tool_config.get("fields", {})
self.operation = fields.get("operation", "search")
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
query = arguments.get("query", "")
if not query:
return {"error": "query parameter is required"}
try:
resp = requests.get(
"https://my-api.example.com/search",
params={"q": query},
timeout=self.timeout,
)
resp.raise_for_status()
return {"status": "success", "data": resp.json()}
except requests.exceptions.RequestException as e:
return {"error": str(e)}Key differences from the workspace pattern:
- Inherit from (from
BaseTool)tooluniverse.base_tool - takes the class name as a string argument
@register_tool("ClassName") - receives all arguments in a single dict — extract them with
run(self, arguments: Dict).get() - receives
__init__dict; calltool_configfirstsuper().__init__(tool_config)
插件包工具使用,并接收所有参数作为单个:
BaseToolDictpython
import requests
from typing import Dict, Any
from tooluniverse.base_tool import BaseTool
from tooluniverse.tool_registry import register_tool
@register_tool("MyAPITool")
class MyAPITool(BaseTool):
"""Tool description here."""
def __init__(self, tool_config: Dict[str, Any]):
super().__init__(tool_config)
self.timeout = tool_config.get("timeout", 30)
fields = tool_config.get("fields", {})
self.operation = fields.get("operation", "search")
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
query = arguments.get("query", "")
if not query:
return {"error": "query parameter is required"}
try:
resp = requests.get(
"https://my-api.example.com/search",
params={"q": query},
timeout=self.timeout,
)
resp.raise_for_status()
return {"status": "success", "data": resp.json()}
except requests.exceptions.RequestException as e:
return {"error": str(e)}与工作区模式的主要区别:
- 继承自(来自
BaseTool)tooluniverse.base_tool - 接收类名字符串作为参数
@register_tool("ClassName") - 接收所有参数作为单个字典——使用
run(self, arguments: Dict)提取参数.get() - 接收
__init__字典;需先调用tool_configsuper().__init__(tool_config)
JSON config in a plugin package
插件包中的JSON配置
Place configs in . The field must match the string passed to
:
data/my_api_tools.json"type"@register_tool(...)json
[
{
"name": "MyAPI_search",
"description": "Search my API. Returns matching records.",
"type": "MyAPITool",
"fields": { "operation": "search" },
"parameter": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query" },
"limit": { "type": ["integer", "null"], "description": "Max results" }
},
"required": ["query"]
}
}
]将配置文件放在中。字段必须与中传入的字符串匹配:
data/my_api_tools.json"type"@register_tool(...)json
[
{
"name": "MyAPI_search",
"description": "Search my API. Returns matching records.",
"type": "MyAPITool",
"fields": { "operation": "search" },
"parameter": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query" },
"limit": { "type": ["integer", "null"], "description": "Max results" }
},
"required": ["query"]
}
}
]__init__.py
__init__.py__init__.py
__init__.pyKeep it minimal — no registration code needed. The plugin system imports every file in the
package directory automatically (via ), so
decorators fire on their own:
.py_discover_entry_point_plugins()@register_toolpython
"""My tools plugin for ToolUniverse."""If you want IDE autocompletion or to make it easy to import specific classes directly, you can
add explicit imports — they are harmless because is idempotent (registering
the same class twice is a no-op):
@register_toolpython
"""My tools plugin for ToolUniverse."""
from . import my_api_tool # optional — for IDE support
from . import my_other_tool # optionalDo not add registration logic, JSON loading, or calls here.
Those run automatically at plugin discovery time.
register_tool_configs()保持简洁——无需注册代码。插件系统会自动导入包目录下的所有文件(通过),因此装饰器会自动生效:
.py_discover_entry_point_plugins()@register_toolpython
"""My tools plugin for ToolUniverse."""如果你需要IDE自动补全或方便直接导入特定类,可以添加显式导入——这不会有问题,因为是幂等的(重复注册同一个类不会产生影响):
@register_toolpython
"""My tools plugin for ToolUniverse."""
from . import my_api_tool # 可选——用于IDE支持
from . import my_other_tool # 可选请勿在此处添加注册逻辑、JSON加载或调用。这些操作会在插件发现时自动执行。
register_tool_configs()Install and verify
安装与验证
bash
undefinedbash
undefinedInstall in editable mode — path must point to the directory containing pyproject.toml
以可编辑模式安装——路径必须指向包含pyproject.toml的目录
pip install -e /path/to/my_project_root
pip install -e /path/to/my_project_root
Verify the entry point is registered
验证入口点是否已注册
python -c "
from importlib.metadata import entry_points
eps = entry_points(group='tooluniverse.plugins')
print([ep.name for ep in eps])
"
python -c "
from importlib.metadata import entry_points
eps = entry_points(group='tooluniverse.plugins')
print([ep.name for ep in eps])
"
Test the tool — MUST run from the plugin repo directory
测试工具——必须从插件仓库目录运行
cd /path/to/my_project_root
tu test MyAPI_search '{"query": "test"}'
`tu test` finds plugin tools via the installed entry point — the package must be
`pip install -e`'d first. Always run `tu test` from the plugin repo directory (not
from an arbitrary location): ToolUniverse's workspace auto-detection looks for
`.tooluniverse/` in the current directory, which is where the plugin's `profile.yaml`
and any workspace-level config lives.
Add `test_examples` to your JSON config for zero-config testing:
```json
{ "name": "MyAPI_search", ..., "test_examples": [{"query": "test"}] }Then:
tu test MyAPI_searchNote: shows tool counts grouped by category, not individual tool names. To confirm
your specific tool loaded, use or run directly.
If "Tool not found", see the gotcha above about the lazy registry refresh.
tu listtu info MyAPI_searchtu test MyAPI_searchcd /path/to/my_project_root
tu test MyAPI_search '{"query": "test"}'
`tu test`通过已安装的入口点查找插件工具——包必须先通过`pip install -e`安装。请始终从插件仓库目录运行`tu test`(而非任意目录):ToolUniverse的工作区自动检测会在当前目录下查找`.tooluniverse/`,这是插件的`profile.yaml`和任何工作区级配置所在的位置。
向JSON配置中添加`test_examples`以实现零配置测试:
```json
{ "name": "MyAPI_search", ..., "test_examples": [{"query": "test"}] }然后运行:
tu test MyAPI_search注意:按类别显示工具数量,而非单个工具名称。要确认特定工具是否已加载,请使用或直接运行。如果提示“Tool not found”,请参考上述关于延迟注册表刷新的注意事项。
tu listtu info MyAPI_searchtu test MyAPI_searchOffline / pure-computation tools
离线/纯计算工具
Calculator tools that perform local math (no HTTP) follow the plugin-package pattern
but skip the HTTP layer entirely. Common designs:
执行本地数学运算(无需HTTP请求)的计算器工具遵循插件包模式,但跳过HTTP层。常见设计如下:
Preset lookup tables
预设查找表
Define named presets at module level as a , then resolve the
parameter with a priority chain: explicit user value → preset name → default.
Always include the preset table in so callers can discover valid names
without reading source code:
Dict[str, float]metadatapython
_PACKAGE_THETA_JA = {"sot-23": 200.0, "to-220": 50.0, "bga-256": 20.0}
def run(self, arguments):
theta = arguments.get("theta_ja")
if theta is None and arguments.get("package"):
key = arguments["package"].lower()
if key not in _PACKAGE_THETA_JA:
return {"status": "error",
"message": f"Unknown package. Known: {list(_PACKAGE_THETA_JA)}"}
theta = _PACKAGE_THETA_JA[key]
return {
"status": "success",
"data": {"theta_ja": theta, ...},
"metadata": {"package_presets": _PACKAGE_THETA_JA},
}在模块级别将预设值定义为,然后按优先级解析参数:显式用户值 → 预设名称 → 默认值。请务必将预设表包含在中,以便调用者无需阅读源代码即可发现有效的预设名称:
Dict[str, float]metadatapython
_PACKAGE_THETA_JA = {"sot-23": 200.0, "to-220": 50.0, "bga-256": 20.0}
def run(self, arguments):
theta = arguments.get("theta_ja")
if theta is None and arguments.get("package"):
key = arguments["package"].lower()
if key not in _PACKAGE_THETA_JA:
return {"status": "error",
"message": f"Unknown package. Known: {list(_PACKAGE_THETA_JA)}"}
theta = _PACKAGE_THETA_JA[key]
return {
"status": "success",
"data": {"theta_ja": theta, ...},
"metadata": {"package_presets": _PACKAGE_THETA_JA},
}Solving the same equation in both directions
双向求解同一公式
When the same formula can be rearranged to solve for different unknowns, expose them
as separate values with a single runtime-dispatch tool:
operationpython
undefined当同一个公式可被重排以求解不同未知数时,可将其作为单独的值暴露出来,使用单个运行时分发工具:
operationpython
undefinedC_min = I × Δt / ΔV → also: ΔV = I × Δt / C
C_min = I × Δt / ΔV → 也可变形为: ΔV = I × Δt / C
op = arguments.get("operation") or self.operation
if op == "solve_capacitance":
dV = _req_float(arguments, "voltage_droop_V")
C_min = I * dt / dV
...
elif op == "solve_droop":
C = _req_float(arguments, "capacitance_F")
dV = I * dt / C
...
The two directions share a single JSON config entry. Use `"fields": {"operation": "default_op"}`
in the JSON to set the default, and document both modes clearly in the description.op = arguments.get("operation") or self.operation
if op == "solve_capacitance":
dV = _req_float(arguments, "voltage_droop_V")
C_min = I * dt / dV
...
elif op == "solve_droop":
C = _req_float(arguments, "capacitance_F")
dV = I * dt / C
...
两种求解方式共享单个JSON配置条目。在JSON中使用`"fields": {"operation": "default_op"}`设置默认操作,并在描述中清晰说明两种模式。Physical constants at module level
模块级物理常量
Define fundamental constants once, near the top of the file, so they appear in code
review and are easy to update:
python
import math
_MU0 = 4.0 * math.pi * 1e-7 # H/m — permeability of free space
_KB_EV = 8.617333e-5 # eV/K — Boltzmann constant在文件顶部定义基础常量一次,以便于代码审查和更新:
python
import math
_MU0 = 4.0 * math.pi * 1e-7 # H/m — 真空磁导率
_KB_EV = 8.617333e-5 # eV/K — 玻尔兹曼常数Material-specific values as a named dict
材料特定值定义为命名字典
_MATERIAL_EA = {"al": 0.7, "cu": 0.9, "w": 1.0} # activation energy in eV
undefined_MATERIAL_EA = {"al": 0.7, "cu": 0.9, "w": 1.0} # 激活能,单位eV
undefinedMulti-output operations
多输出操作
When a single computation naturally yields multiple related results (e.g., Tj AND
headroom AND pass/fail), return them all in rather than forcing a second call:
datapython
data = {
"junction_temp_C": tj,
"headroom_C": tj_max - tj,
"passes_thermal": (tj_max - tj) >= 0,
...
}For the complete patterns (significant-figure rounding, helper, preset
resolution), see references/python-tool.md.
_req_float当单次计算自然产生多个相关结果时(如Tj、裕量和通过/失败状态),请将所有结果放在中返回,而非强制用户进行第二次调用:
datapython
data = {
"junction_temp_C": tj,
"headroom_C": tj_max - tj,
"passes_thermal": (tj_max - tj) >= 0,
...
}完整的实现模式(有效数字舍入、辅助函数、预设解析),请查看references/python-tool.md。
_req_float