Claude
Skills
Sign in
Back

tooluniverse-custom-tool

Included with Lifetime
$97 forever

Add custom local tools to ToolUniverse alongside the 1000+ built-in tools. Covers JSON-config tools (simplest, no code), Python class tools (REST/SOAP/GraphQL APIs, computational logic), and best-practices for return schemas. Use for wrapping new APIs, adding domain-specific computations, or contributing tools to the registry.

Backend & APIs

What this skill does


# Adding Custom Tools to ToolUniverse

**When to create a custom tool:** Create one if you need to access an API that ToolUniverse
doesn't cover, or if you need a specialized data transformation that no existing tool provides.
Start with the JSON config approach (simplest — no Python needed); escalate to a Python class
only if you need custom response parsing or stateful logic.

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 |

---

## Option A — Workspace tools (local use)

Tools in `.tooluniverse/tools/` are auto-discovered at startup. No installation needed.

```bash
mkdir -p .tooluniverse/tools
```

### JSON config

Create `.tooluniverse/tools/my_tools.json`:

```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"]
    }
  }
]
```

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](references/json-tool.md).

### Python class

Create `.tooluniverse/tools/my_tool.py`:

```python
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 `run(self, **named_params)` — arguments are unpacked as keyword
arguments matching the `input_schema` properties.

For the full Python class reference, see [references/python-tool.md](references/python-tool.md).

### Test workspace tools

```bash
# Uses test_examples from the tool's JSON config — zero config needed
tu test MyAPI_search

# Single ad-hoc call
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

**Gotchas:** (1) `tu test` does NOT verify non-empty results — `[]` passes schema validation. Use test_examples args that return real data. (2) Verify test_examples manually first with urllib (not curl) to confirm the API returns JSON, not HTML. Use 2-4 broad keywords.

Add `test_examples` and `return_schema` to JSON config for best coverage. `tu test` validates `result["data"]` against `return_schema` (match `"type": "array"` or `"type": "object"` to your data shape).

Optional `my_tool_tests.json` for extra assertions (`expect_status`, `expect_keys`).

### Use with MCP server

Tools in `.tooluniverse/tools/` are auto-available via `tu serve`. Workspace priority: `--workspace` flag → `TOOLUNIVERSE_HOME` env → `./.tooluniverse/` → `~/.tooluniverse/`.

To use a different tools directory, add `sources: [./my-custom-tools/]` in `.tooluniverse/profile.yaml` and start with `tooluniverse --load .tooluniverse/profile.yaml`.

---

## Option B — Plugin package (shareable, pip-installable)

Use this when you want to distribute tools as a reusable Python package that other users can
install with `pip install`. The plugin package has the same directory layout as a workspace, plus a
`pyproject.toml` that declares the entry point.

### Package 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_env
```

JSON config files are discovered from both `data/` and the package root directory. The convention is `data/`.

### `pyproject.toml` entry point

```toml
[project.entry-points."tooluniverse.plugins"]
my-tools = "my_tools_package"
```

The value (`my_tools_package`) must be the importable Python package name.

### Python class in a plugin package

Plugin package tools use `BaseTool` and receive all arguments as a single `Dict`:

```python
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 `BaseTool` (from `tooluniverse.base_tool`)
- `@register_tool("ClassName")` takes the class name as a string argument
- `run(self, arguments: Dict)` receives all arguments in a single dict — extract them with `.get()`
- `__init__` receives `tool_config` dict; call `super().__init__(tool_config)` first

### JSON config in a plugin package

Place configs in `data/my_api_tools.json`. The `"type"` field must match the string passed to
`@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`

Keep minimal — just a docstring. The plugin system auto-imports all `.py` files via `_discover_entry_point_plugins()`, so `@register_tool` decorators fire automatically. Optional: add `from . import my_api_tool` for IDE support (idempotent). Do NOT add registration logic or JSON loading here.

### Install and verify

```bash
pip install -e /path/to/my_project_root
cd /path/to/my_project_ro

Related in Backend & APIs