Loading...
Loading...
Compare original and translation side by side
| 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安装的工具集 |
.tooluniverse/tools/mkdir -p .tooluniverse/tools.tooluniverse/tools/mkdir -p .tooluniverse/tools.tooluniverse/tools/my_tools.json[
{
"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"]
}
}
].tooluniverse/tools/my_tools.json[
{
"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"]
}
}
].tooluniverse/tools/my_tool.pyfrom 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()}run(self, **named_params)input_schema.tooluniverse/tools/my_tool.pyfrom 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()}run(self, **named_params)input_schemaundefinedundefined
`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))certification.oshwa.org/api/projectsmy_tool_tests.json{
"tool_name": "MyAPI_search",
"tests": [
{
"name": "basic search",
"args": {"q": "climate change"},
"expect_status": "success",
"expect_keys": ["data"]
}
]
}test_examplesreturn_schema{
"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"data"type": "array"data"type": "object"
`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))certification.oshwa.org/api/projectsmy_tool_tests.json{
"tool_name": "MyAPI_search",
"tests": [
{
"name": "basic search",
"args": {"q": "climate change"},
"expect_status": "success",
"expect_keys": ["data"]
}
]
}test_examplesreturn_schema{
"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".tooluniverse/tools/tu serve # MCP stdio server (Claude Desktop, etc.)
tooluniverse # same--workspaceTOOLUNIVERSE_HOME./.tooluniverse/~/.tooluniverse/.tooluniverse/tools/tu serve # MCP标准输入输出服务器(适用于Claude Desktop等)
tooluniverse # 同上--workspaceTOOLUNIVERSE_HOME./.tooluniverse/~/.tooluniverse/sources.tooluniverse/profile.yamlname: my-profile
sources:
- ./my-custom-tools/ # relative to profile.yaml location
- /absolute/path/tools/tu serve --load .tooluniverse/profile.yaml.tooluniverse/profile.yamlsourcesname: my-profile
sources:
- ./my-custom-tools/ # 相对于profile.yaml的路径
- /absolute/path/tools/tu serve --load .tooluniverse/profile.yamlpip installpyproject.tomlpip installpyproject.tomlmy_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_envdata/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 # 可选:名称、描述、所需环境变量data/data/pyproject.tomlpyproject.toml[project.entry-points."tooluniverse.plugins"]
my-tools = "my_tools_package"my_tools_package[project.entry-points."tooluniverse.plugins"]
my-tools = "my_tools_package"my_tools_packageBaseToolDictimport 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)}BaseTooltooluniverse.base_tool@register_tool("ClassName")run(self, arguments: Dict).get()__init__tool_configsuper().__init__(tool_config)BaseToolDictimport 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)}BaseTooltooluniverse.base_tool@register_tool("ClassName")run(self, arguments: Dict).get()__init__tool_configsuper().__init__(tool_config)data/my_api_tools.json"type"@register_tool(...)[
{
"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(...)[
{
"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.py_discover_entry_point_plugins()@register_tool"""My tools plugin for ToolUniverse."""@register_tool"""My tools plugin for ToolUniverse."""
from . import my_api_tool # optional — for IDE support
from . import my_other_tool # optionalregister_tool_configs().py_discover_entry_point_plugins()@register_tool"""My tools plugin for ToolUniverse."""@register_tool"""My tools plugin for ToolUniverse."""
from . import my_api_tool # 可选——用于IDE支持
from . import my_other_tool # 可选register_tool_configs()undefinedundefined
`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"}] }tu test MyAPI_searchtu listtu info MyAPI_searchtu test MyAPI_search
`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_searchtu listtu info MyAPI_searchtu test MyAPI_searchDict[str, float]metadata_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]metadata_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},
}operationundefinedoperationundefined
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.
两种求解方式共享单个JSON配置条目。在JSON中使用`"fields": {"operation": "default_op"}`设置默认操作,并在描述中清晰说明两种模式。import math
_MU0 = 4.0 * math.pi * 1e-7 # H/m — permeability of free space
_KB_EV = 8.617333e-5 # eV/K — Boltzmann constantimport math
_MU0 = 4.0 * math.pi * 1e-7 # H/m — 真空磁导率
_KB_EV = 8.617333e-5 # eV/K — 玻尔兹曼常数undefinedundefineddatadata = {
"junction_temp_C": tj,
"headroom_C": tj_max - tj,
"passes_thermal": (tj_max - tj) >= 0,
...
}_req_floatdatadata = {
"junction_temp_C": tj,
"headroom_C": tj_max - tj,
"passes_thermal": (tj_max - tj) >= 0,
...
}_req_float