New to Claude Skills? Learn how to install them →

Cmims-harvard on GitHub

Custom Tool Integration

Free

Easily add local tools to ToolUniverse.

Get this skill

Free · Opens the source repo

What Custom Tool Integration does

Custom Tool Integration allows developers to enhance their ToolUniverse experience by adding local tools tailored to their specific needs. With over 1000 built-in tools available, this skill empowers users to create custom solutions when existing tools fall short. Whether you need to access a unique API or perform specialized data transformations, this skill provides the flexibility to extend ToolUniverse functionality.

The skill supports three primary methods for adding tools: JSON configuration, Python classes, and plugin packages. The JSON config method is the simplest, requiring no coding, making it ideal for standard REST API integrations. For more complex requirements, such as custom logic or stateful operations, users can implement Python class tools. Finally, plugin packages facilitate the sharing of tools as reusable Python packages, allowing for easy distribution and installation via pip.

Developers can quickly test their tools using the built-in tu test command, which verifies the functionality and correctness of the tool against predefined schemas. This ensures that the tools not only work as expected but also adhere to the specified input and output formats. The skill is particularly beneficial for teams looking to streamline their workflows by integrating domain-specific tools directly into their development environment.

When to use it

Use this skill when you need to integrate APIs or create specialized tools that are not available in ToolUniverse.

When not to use it

This skill may not be suitable for users who only require basic functionality provided by existing built-in tools without any customization.

What you can build with it

Integrating a New API

You need to access a third-party API not covered by ToolUniverse. Create a custom tool using JSON configuration.

Implementing Custom Logic

Your project requires specific data transformations that existing tools can't provide. Use a Python class to implement the necessary logic.

Distributing Tools Across Teams

You developed several useful tools for your team. Package them as a plugin for easy installation and sharing.

How to install Custom Tool Integration

View source

1. Install with the skills CLI

npx skills add mims-harvard/tooluniverse/tooluniverse-custom-tool --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by mims-harvard

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:

ApproachWhen to use
JSON configREST API with standard request/response — no coding needed
Python class (workspace)Custom logic for local/private use only
Plugin packageReusable 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.

mkdir -p .tooluniverse/tools

JSON config

Create .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"]
    }
  }
]

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.

Python class

Create .tooluniverse/tools/my_tool.py:

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.

Test workspace tools

# 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

[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:

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(...):

[
  {
    "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

pip install -e /path/to/my_project_root
cd /path/to/my_project_root   # MUST run from plugin repo directory
tu test MyAPI_search '{"query": "test"}'

Must pip install -e first. Run tu test from plugin repo dir (workspace auto-detection needs .tooluniverse/). Add test_examples to JSON config for zero-config testing. Use tu info MyAPI_search to confirm the tool loaded.


Offline / pure-computation tools

Calculator tools (no HTTP) follow the plugin-package pattern but skip the HTTP layer. Key design patterns:

  • Preset lookup tables: Define Dict[str, float] at module level. Resolution priority: explicit value → preset name → default. Include presets in metadata for discoverability.
  • Bidirectional equations: Expose as separate operation values in a single tool. Use "fields": {"operation": "default_op"} in JSON config.
  • Physical constants: Define at module level (_MU0 = 4*pi*1e-7, etc.). Material-specific values as named dicts.
  • Multi-output: Return all related results in data (e.g., temperature + headroom + pass/fail) rather than forcing multiple calls.

For complete patterns, see references/python-tool.md.

Frequently asked questions about Custom Tool Integration

Similar skills