Files

329 lines
11 KiB
Markdown

---
title: "JSON Tool Config Reference"
task: ""
lineage_type: import
upstream_source: https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-custom-tool/references/json-tool.md
upstream_sha: e2520a96
imported_at: 2026-06-26
prompt_class: prompt
upstream_changes: accepted
author: upstream
validated: false
---
# JSON Tool Config Reference
## Minimal example
```json
[
{
"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
```json
"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):
```json
"parameter": {
"type": "object",
"properties": {
"query": { "type": ["string", "null"], "description": "Optional search term" },
"limit": { "type": ["integer", "null"], "description": "Max results" }
},
"required": []
}
```
## POST example
```json
{
"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:
```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`:
```json
{
"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_keys` if the tool cannot work without the key, or
`optional_api_keys` if it only raises rate limits / unlocks extras.
- `api_key_info` lives in the same config — define it **once per key** even if
several tools share it. `type` is `secret` (an API key) or `endpoint` (a
self-hosted server URL); `domain` groups the key in the setup UI.
- After editing, run `python scripts/gen_api_key_catalog.py` to refresh
`src/tooluniverse/data/api_keys_catalog.json` and `.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"`:
```json
{
"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):
```json
"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`:
```json
"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:
```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))
```
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
```json
[
{ "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.
```json
{
"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__`.
```json
[
{ "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:
```json
"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:
```json
"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.