329 lines
11 KiB
Markdown
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.
|