Contributing Local Tools to ToolUniverse#
This guide covers how to contribute local Python tools to the ToolUniverse repository. Local tools run within the ToolUniverse process and are available to all users.
Note
Key Difference: Contributing to the repository requires additional steps compared to using tools locally. The most critical step is modifying __init__.py in 4 specific locations.
Note
For Local Development Only: If you just want to use a tool locally without contributing to the repository, see the single-file example in examples/my_new_tool/single_file_example.py. This approach doesn’t require modifying any core ToolUniverse files (__init__.py, default_config.py, or data/ directory). The complete working examples are available in examples/my_new_tool/ directory with a README explaining both approaches.
Quick Overview#
10 Steps to Contribute a Local Tool:
Environment Setup - Fork, clone, install dependencies
Create Tool File - Python class in
src/tooluniverse/Register Tool - Use
@register_tool('Type')decoratorCreate Config - JSON file in
data/xxx_tools.json(Optional) Add unit tests
Done! Tool is auto-discovered.
Code Quality - Pre-commit hooks (automatic)
Documentation - Docstrings and examples
Create Examples - Working examples in
examples/Submit PR - Follow contribution guidelines
Step-by-Step Guide#
Step 1: Environment Setup#
# Fork the repository on GitHub first
git clone https://github.com/yourusername/ToolUniverse.git
cd ToolUniverse
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install development dependencies
pip install -e ".[dev]"
# Install pre-commit hooks
./setup_precommit.sh
Step 2: Create Tool File#
Create your tool file in src/tooluniverse/xxx_tool.py:
from tooluniverse.tool_registry import register_tool
from tooluniverse.base_tool import BaseTool
from typing import Dict, Any
@register_tool('MyNewTool') # Note: No config here for contributions
class MyNewTool(BaseTool):
"""My new tool for ToolUniverse."""
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""Execute the tool."""
# Your tool logic here
input_value = arguments.get('input', '')
return {
"result": input_value.upper(),
"success": True
}
def validate_input(self, **kwargs) -> None:
"""Validate input parameters."""
input_val = kwargs.get('input')
if not input_val:
raise ValueError("Input is required")
Step 3: Register Tool#
The @register_tool('MyNewTool') decorator registers your tool class. Note that for contributions, we don’t include the config in the decorator - that goes in a separate JSON file.
Important
There are two different registration paths:
Contributing to ToolUniverse: put the class under
src/tooluniverse/and put the JSON spec undersrc/tooluniverse/data/. The package discovery system can find the decorated class from the ToolUniverse package tree.Local experiment outside the package tree: either import the Python file before calling
load_tools(), or include the full config in@register_tool(..., config={...})as shown in Local Tools Tutorial.
Passing only tool_config_files loads the JSON specification; it does not
import an arbitrary Python file from your current directory. If your class is
outside src/tooluniverse/, import that module first so the decorator runs.
Step 4: Create Configuration File#
Create or edit src/tooluniverse/data/xxx_tools.json:
[
{
"name": "my_new_tool",
"type": "MyNewTool",
"description": "Convert text to uppercase",
"parameter": {
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "Text to convert to uppercase"
}
},
"required": ["input"]
},
"examples": [
{
"description": "Convert text to uppercase",
"arguments": {"input": "hello world"}
}
],
"tags": ["text", "utility"],
"author": "Your Name <your.email@example.com>",
"version": "1.0.0"
}
]
Step 5: No Modifications Needed in __init__.py!#
With the new automated discovery system, you do NOT need to modify `src/tooluniverse/__init__.py`.
The system will automatically find your tool class if it is decorated with @register_tool and located inside the src/tooluniverse package tree.
Verification:
# Test that your tool can be imported immediately
from tooluniverse import MyNewTool
print(MyNewTool) # Should show the class or lazy proxy
Step 6: Write Tests#
Create tests in tests/unit/test_my_new_tool.py:
import pytest
from tooluniverse.my_new_tool import MyNewTool
class TestMyNewTool:
def setup_method(self):
self.tool = MyNewTool()
def test_success(self):
"""Test successful execution."""
result = self.tool.run({"input": "hello"})
assert result["success"] is True
assert result["result"] == "HELLO"
def test_validation(self):
"""Test input validation."""
with pytest.raises(ValueError):
self.tool.validate_input(input="")
def test_empty_input(self):
"""Test empty input handling."""
result = self.tool.run({"input": ""})
assert result["success"] is True
assert result["result"] == ""
Run tests with coverage:
pytest tests/unit/test_my_new_tool.py --cov=tooluniverse --cov-report=html
Step 7: Code Quality Check (Automatic)#
Pre-commit hooks will automatically run when you commit:
git add .
git commit -m "feat: add MyNewTool"
# Pre-commit will run: Black, Flake8, Ruff, etc.
# If checks fail, fix the issues and commit again
Step 8: Documentation#
Add comprehensive docstrings to your tool class:
class MyNewTool(BaseTool):
"""
Convert text to uppercase.
This tool takes a string input and returns it converted to uppercase.
Useful for text processing workflows.
Args:
input (str): The text to convert to uppercase
Returns:
dict: Result dictionary with 'result' and 'success' keys
Example:
>>> tool = MyNewTool()
>>> result = tool.run({"input": "hello"})
>>> print(result["result"])
HELLO
"""
Step 9: Create Examples#
Create examples/my_new_tool/my_new_tool_example.py:
"""Example usage of MyNewTool.
This example follows the documentation pattern for contributing tools to the
repository. It demonstrates the multi-file structure:
- my_new_tool.py: Tool class definition
- my_new_tool_tools.json: Tool configuration
- my_new_tool_example.py: Example usage
Note: In a real contribution, these files would be placed in:
- src/tooluniverse/my_new_tool.py
- src/tooluniverse/data/my_new_tool_tools.json
- examples/my_new_tool_example.py
And you would need to modify __init__.py in 4 locations.
"""
import os
import sys
# Import the tool class to register it
from my_new_tool import MyNewTool # noqa: E402, F401
from tooluniverse import ToolUniverse # noqa: E402
def main():
# Initialize ToolUniverse
tu = ToolUniverse()
# Load tools with the config file
# In a real contribution, this would be in default_tool_files
current_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(current_dir, 'my_new_tool_tools.json')
tu.load_tools(tool_config_files={"my_new_tool": config_path})
# Use the tool
result = tu.run({
"name": "my_new_tool",
"arguments": {"input": "hello world"}
})
print(f"Result: {result}")
# Test with different inputs
test_inputs = ["hello", "world", "python"]
for text in test_inputs:
result = tu.run({
"name": "my_new_tool",
"arguments": {"input": text}
})
print(f"'{text}' -> '{result.get('result', 'ERROR')}'")
if __name__ == "__main__":
main()
Note: A complete working example can be found in the
examples/my_new_tool/ directory. That example imports the tool module before
loading its JSON file because the example tool lives outside the installed
tooluniverse package tree. It also includes a single-file local-development
example. See examples/my_new_tool/README.md for details.
Step 10: Submit Pull Request#
# Create feature branch
git checkout -b feature/add-my-new-tool
# Add all files
git add src/tooluniverse/my_new_tool.py
git add src/tooluniverse/data/xxx_tools.json
git add src/tooluniverse/__init__.py
git add tests/unit/test_my_new_tool.py
git add examples/my_new_tool_example.py
# Commit with descriptive message
git commit -m "feat: add MyNewTool for text processing
- Implement MyNewTool class with uppercase conversion
- Add comprehensive unit tests with >95% coverage
- Include usage examples and documentation
- Support input validation and error handling
Closes #[issue-number]"
# Push and create PR
git push origin feature/add-my-new-tool
PR Template:
## Description
This PR adds MyNewTool, a new local tool for text processing.
## Changes Made
- ✅ **Tool Implementation**: Complete MyNewTool class
- ✅ **Testing**: Unit tests with >95% coverage
- ✅ **Documentation**: Comprehensive docstrings and examples
- ✅ **Configuration**: JSON config in data/xxx_tools.json
- ✅ **Integration**: Modified __init__.py in 4 locations
## Testing
```bash
pytest tests/unit/test_my_new_tool.py --cov=tooluniverse
python examples/my_new_tool/my_new_tool_example.py
```
## Checklist
- [x] Tests pass locally
- [x] Code follows project style guidelines
- [x] Documentation is complete
- [x] __init__.py modified in all 4 locations
- [x] Examples work as expected
Common Mistakes#
❌ Most Common: Missing @register_tool decorator
- Tool won’t be discovered if not decorated
- Solution: Add @register_tool("MyTool")
❌ Config in wrong place
- Don’t put config in @register_tool() decorator (for contributions)
- Put it in data/my_new_tool_tools.json instead
- Note: For local development only, you CAN put config in the decorator (see examples/my_new_tool/single_file_example.py)
❌ Wrong file location
- Tool file must be in src/tooluniverse/
- Not in your project directory
❌ Missing tests - Coverage must be >90% - Test both success and error cases
❌ Import errors - Check module name matches file name - Check class name matches exactly (case-sensitive)
Troubleshooting#
ImportError: cannot import name ‘MyNewTool’ .. code-block:: python
# Check if tool is in __all__ list from tooluniverse import __all__ print(“MyNewTool” in __all__) # Should be True
# Check if import statement exists # Look for: from .my_new_tool import MyNewTool
AttributeError: module ‘tooluniverse’ has no attribute ‘MyNewTool’
- Verify the tool name is in __all__ list
- Check that the tool name matches the class name exactly
Tool not found when using ToolUniverse .. code-block:: python
# Verify tool loads correctly from tooluniverse import ToolUniverse tu = ToolUniverse() tu.load_tools()
# Check if tool is in the loaded tools print(“my_new_tool” in tu.all_tool_dict) # Should be True
Next Steps#
After successfully contributing your local tool:
🚀 Remote Tools: Contributing Remote Tools to ToolUniverse - Learn about contributing remote tools
🔍 Architecture: ToolUniverse Architecture - Understand ToolUniverse internals
📊 Comparison: Review the tool type comparison table in Contributing to ToolUniverse
Tip
Success Tips: Start with simple tools, test thoroughly, and ask for help in GitHub discussions if you get stuck!