11 KiB
title, task, lineage_type, upstream_source, upstream_sha, imported_at, prompt_class, upstream_changes, author, validated
| title | task | lineage_type | upstream_source | upstream_sha | imported_at | prompt_class | upstream_changes | author | validated |
|---|---|---|---|---|---|---|---|---|---|
| JSON Tool Config Reference | import | https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-custom-tool/references/json-tool.md | e2520a96 | 2026-06-26 | prompt | accepted | upstream | false |
JSON Tool Config Reference
Minimal example
[
{
"name": "MyAPI_search",
"description": "...",
"type": "BaseRESTTool",
"fields": { "endpoint": "https://api.example.com/search" },
"parameter": {
"type": "object",
"properties": {
"q": { "type": "string", "description": "Search query" }
},
"required": ["q"]
}
}
]
All fields
| Field | Required | Description |
|---|---|---|
name |
Yes | Unique tool identifier. Convention: ProviderName_action (e.g., MyAPI_search) |
description |
Yes | What the tool does and returns. Be specific — the AI uses this to decide when to call the tool. |
type |
Yes | "BaseRESTTool" for workspace REST tools; or the class name string passed to @register_tool("ClassName") for plugin package tools |
fields.endpoint |
Yes (BaseRESTTool only) | The API endpoint URL. Only required when "type" is "BaseRESTTool". |
parameter |
Yes | JSON Schema object for the tool's input parameters |
fields.method |
No | HTTP method: "GET" (default) or "POST" |
fields.headers |
No | Static headers as key-value pairs |
fields |
No (plugin) | Any key-value pairs passed to __init__ as tool_config["fields"]. Commonly "operation" is used to route multiple tools from one class. |
test_examples |
No | List of example argument dicts — used automatically by tu test <tool_name> |
return_schema |
No | JSON Schema for the response data — validated automatically by tu test |
tags |
No | List of category tags (e.g., ["genomics", "search"]) |
Parameter types
"properties": {
"required_string": { "type": "string", "description": "..." },
"optional_int": { "type": ["integer", "null"], "description": "..." },
"optional_string": { "type": ["string", "null"], "description": "..." },
"optional_boolean": { "type": ["boolean", "null"], "description": "..." }
}
Mark optional params with ["type", "null"] — omit them from "required".
If a tool has no required parameters, use "required": [] (not omitting the key):
"parameter": {
"type": "object",
"properties": {
"query": { "type": ["string", "null"], "description": "Optional search term" },
"limit": { "type": ["integer", "null"], "description": "Max results" }
},
"required": []
}
POST example
{
"name": "MyAPI_submit",
"description": "Submit a job to the processing queue. Returns job_id.",
"type": "BaseRESTTool",
"fields": {
"endpoint": "https://api.example.com/jobs",
"method": "POST",
"headers": { "Content-Type": "application/json" }
},
"parameter": {
"type": "object",
"properties": {
"input": { "type": "string", "description": "Input data to process" },
"priority": { "type": ["integer", "null"], "description": "Job priority 1-10" }
},
"required": ["input"]
},
"test_examples": [
{ "input": "sample data", "priority": 5 }
]
}
Authentication
For APIs requiring API keys, reference the env var in a header. Store the key
value in .tooluniverse/.env (or ~/.tooluniverse/.env), never in the JSON:
"fields": {
"endpoint": "https://api.example.com/search",
"headers": { "Authorization": "Bearer ${MY_API_KEY}" }
}
Then declare the key so ToolUniverse knows the tool needs it, and
describe it with an api_key_info block so it appears in the setup UI
(/tooluniverse:setup-keys) and the generated .env.template:
{
"name": "MyAPI_search",
"type": "MyAPITool",
"required_api_keys": ["MY_API_KEY"],
"api_key_info": {
"MY_API_KEY": {
"domain": "Drugs & Chemistry",
"type": "secret",
"register_url": "https://example.com/get-a-key",
"purpose": "What this key unlocks (one sentence).",
"without": "What happens without it: blocked / demo mode / lower limits."
}
}
}
- Use
required_api_keysif the tool cannot work without the key, oroptional_api_keysif it only raises rate limits / unlocks extras. api_key_infolives in the same config — define it once per key even if several tools share it.typeissecret(an API key) orendpoint(a self-hosted server URL);domaingroups the key in the setup UI.- After editing, run
python scripts/gen_api_key_catalog.pyto refreshsrc/tooluniverse/data/api_keys_catalog.jsonand.env.template. CI also auto-syncs on push and fails PRs whose catalog is out of date.
.tooluniverse/.env:
MY_API_KEY=your-actual-key-here
return_schema
Describes the structure of result["data"]. tu test validates every result against this schema
automatically — no extra config needed. Use JSON Schema format.
Critical: The schema must match the type of what your run() puts under the "data" key —
not the full response dict. Most search tools return a list, so the top-level type is "array":
{
"name": "MyAPI_search",
...
"test_examples": [{"q": "test"}],
"return_schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"score": { "type": "number" }
},
"required": ["id", "title"]
}
}
}
If data is a single object (e.g. get / lookup operations):
"return_schema": {
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" }
},
"required": ["id"]
}
If the API can return multiple shapes (success vs error), use oneOf:
"return_schema": {
"oneOf": [
{
"type": "array",
"items": { "type": "object" }
},
{
"type": "object",
"properties": { "error": { "type": "string" } },
"required": ["error"]
}
]
}
Note: return_schema validation only runs when result["status"] == "success", so
error responses (which don't have a "data" key) are skipped automatically.
Gotcha: An empty array [] satisfies "type": "array" and passes schema validation. Make
sure test_examples use arguments that actually return non-empty results, otherwise a broken
tool will pass all tests silently.
Gotcha (tools with multiple output shapes): If your tool returns different fields depending
on the inputs (e.g., filter_type: RC returns cutoff_frequency_Hz but filter_type: LC
returns resonant_frequency_Hz), only require fields that are present in ALL execution paths.
Adding a field to required that only appears in some paths will cause schema validation to
fail for the other paths. Use a loose schema (no required list, or a minimal one) for
dispatch-style tools.
Verify test_examples with Python before writing them. Use urllib rather than curl — it
matches what the tool will actually do and handles edge cases like redirects more visibly:
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))
Some search APIs use intitle-style matching where all words must appear literally in a title
or name field — overly specific queries like "I2C pull-up resistor value" can return 0 results
even when the tool is working. Use 2-4 key words that reliably appear in real content
(e.g., "pull-up resistor" instead).
Verify the URL is a real JSON endpoint. Some URLs that look like REST APIs
(e.g., https://certification.example.org/api/projects) may redirect to a static HTML page.
A urllib fetch will show you the Content-Type and body immediately, before you write any code.
Multiple tools in one file
[
{ "name": "MyAPI_search", ... },
{ "name": "MyAPI_get_record", ... },
{ "name": "MyAPI_list_collections", ... }
]
Offline tools with an operation parameter
For pure-computation tools that handle multiple operations in a single Python class, you have two design choices:
Choice A — Single tool, user passes operation as a parameter:
The JSON config exposes operation as a parameter property. One tool, one JSON entry,
user chooses the mode at call time.
{
"name": "Circuit_wire_gauge",
"type": "WireGaugeTool",
"parameter": {
"type": "object",
"properties": {
"operation": {
"type": ["string", "null"],
"description": "Operation: 'from_current' (default) or 'from_awg'."
},
"current_A": { "type": ["number", "null"], "description": "..." },
"awg": { "type": ["number", "null"], "description": "..." }
},
"required": []
}
}
Use this when the operations share most parameters and the distinction is a simple mode switch.
Choice B — Multiple tools, each backed by the same class via fields.operation:
Each JSON entry is a separate tool with its own name, description, and parameters.
The Python class reads self.operation = tool_config["fields"]["operation"] in __init__.
[
{ "name": "Circuit_wire_from_current", "type": "WireGaugeTool",
"fields": {"operation": "from_current"}, "parameter": { ... only current_A, ambient_C ... } },
{ "name": "Circuit_wire_from_awg", "type": "WireGaugeTool",
"fields": {"operation": "from_awg"}, "parameter": { ... only awg, temp_C ... } }
]
Use this when the operations have very different parameters or descriptions — it gives the AI cleaner, more targeted tool choices.
Array-of-arrays parameters
When a tool accepts a list of structured items (e.g., RC network segments, waypoints,
coefficient lists), use "type": ["array", "null"] with an "items" schema:
"segments": {
"type": ["array", "null"],
"description": "List of [R_i, C_i] pairs from driver to load. Each R_i in ohms, C_i in farads. Example: [[100, 50e-15], [200, 50e-15], [0, 100e-15]].",
"items": {
"type": "array",
"items": { "type": "number" },
"minItems": 2,
"maxItems": 2
}
}
Always include a concrete example in the description (e.g., [[100, 50e-15], ...]) — the AI
needs to see the expected format. Use test_examples that exercise a non-trivial list:
"test_examples": [
{"mode": "chain", "segments": [[100, 50e-15], [200, 50e-15], [0, 100e-15]]}
]
The final segment often has R=0 (pure load capacitance). In the Python class, validate
with r < 0 (reject negative R) rather than r <= 0 to allow load-only segments.