Contributing to ToolUniverse¶
We welcome contributions to ToolUniverse! This guide covers both general contributions and specialized tool contributions.
Quick Start¶
Fork and clone the repository
Set up development environment:
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -e ".[dev]"
./setup_precommit.sh
Make your changes and test:
pytest
black src/tooluniverse/
flake8 src/tooluniverse/
Submit a pull request
Development Standards¶
Code Style¶
Use Black for formatting:
black src/tooluniverse/
Follow PEP 8 guidelines
Include type hints for all new code
Write comprehensive docstrings
Testing¶
Write tests for all new functionality
Aim for >90% test coverage
Run tests with:
pytest --cov=tooluniverse
Documentation¶
Document all public APIs
Include usage examples
Update guides for new features
Use Google-style docstrings
Tool Contributions¶
Adding New Tools¶
ToolUniverse supports both local and remote tools. Here’s how to add a new tool:
Important: When adding a new tool, you must modify src/tooluniverse/__init__.py
in four specific locations to ensure the tool is properly exposed and importable. This step is critical and often overlooked by contributors.
Required __init__.py Modifications¶
For every new tool class you add, you must update src/tooluniverse/__init__.py
in these four locations:
Add tool class declarations (around lines 105-165): Add type annotations for your tool class
Add import statements (around lines 173-258): Import your tool class in the non-lazy loading section
Add lazy import proxies (around lines 260-360): Create lazy import proxy for your tool class
Add tool names to __all__ list (around lines 362-449): Add your tool class name to the
__all__
list
Example: Adding a new tool called MyNewTool
# 1. Add class declaration (around line 165)
MyNewTool: Any
# 2. Add import statement (around line 258, in the non-lazy loading section)
from .my_new_tool import MyNewTool
# 3. Add lazy import proxy (around line 360, in the else block)
MyNewTool = _LazyImportProxy("my_new_tool", "MyNewTool")
# 4. Add to __all__ list (around line 449)
__all__ = [
# ... existing tools ...
"MyNewTool",
]
Validation Steps: After making these changes, verify your tool is properly exposed by testing:
from tooluniverse import MyNewTool # Should work without errors
print(MyNewTool) # Should show the class or lazy proxy
Local Tool Example¶
from tooluniverse.tool_registry import register_tool
from typing import Dict, Any
@register_tool('MyTool', config={
"name": "my_tool",
"type": "MyTool",
"description": "Tool description",
"parameter": {
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "Input parameter"
}
},
"required": ["input"]
},
"examples": [
{
"description": "Example usage",
"arguments": {"input": "example_value"}
}
],
"tags": ["category", "subcategory"],
"author": "Your Name <your.email@example.com>",
"version": "1.0.0",
"license": "MIT"
})
class MyTool:
"""Tool description."""
def __init__(self, tool_config=None):
self.tool_config = tool_config or {}
def run(self, arguments):
"""Execute tool."""
try:
input_value = arguments["input"]
# Your tool logic here
result = self._process(input_value)
return {"result": result, "success": True}
except Exception as e:
return {"error": str(e), "success": False}
def _process(self, input_value):
"""Process input."""
return {"processed": input_value}
Remote Tool Example¶
For remote tools, create an MCP server:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Dict, Any
app = FastAPI(title="My Tool MCP Server")
class ToolRequest(BaseModel):
input: str
options: Dict[str, Any] = {}
class ToolResponse(BaseModel):
success: bool
result: Dict[str, Any]
error: str = None
@app.post("/process", response_model=ToolResponse)
async def process_request(request: ToolRequest):
"""Process tool request."""
try:
# Your tool logic here
result = {"processed": request.input}
return ToolResponse(success=True, result=result)
except Exception as e:
return ToolResponse(success=False, result={}, error=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Then create a client configuration file:
{
"tools": [
{
"name": "my_remote_tool",
"type": "MyRemoteTool",
"description": "Remote tool description",
"parameter": {
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "Input parameter"
}
},
"required": ["input"]
},
"settings": {
"server_url": "http://localhost:8000",
"timeout": 30,
"retries": 3
},
"tags": ["category", "remote"],
"author": "Your Name <your.email@example.com>",
"version": "1.0.0",
"license": "MIT"
}
]
}
Complete Tool Development Workflow¶
When adding a new tool, follow this complete workflow to ensure proper integration:
Create your tool file in
src/tooluniverse/tools/
(e.g.,my_new_tool.py
)Implement your tool class following the BaseTool interface
Modify __init__.py (critical step - see Required __init__.py Modifications above)
Create comprehensive tests in
tests/unit/
ortests/integration/
Add documentation in
docs/tools/
Update tool registry if using custom registration
Test the complete integration to ensure your tool is properly exposed
Step-by-step __init__.py modification checklist:
# 1. Add class declaration (find the section around line 105-165)
# Look for: "MonarchTool: Any" and add your tool after similar entries
# 2. Add import statement (find the non-lazy loading section around line 173-258)
# Look for: "from .restful_tool import MonarchTool" and add your import
# 3. Add lazy import proxy (find the else block around line 260-360)
# Look for: "MonarchTool = _LazyImportProxy("restful_tool", "MonarchTool")" and add yours
# 4. Add to __all__ list (find the __all__ list around line 362-449)
# Add your tool name as a string in the list
Common mistakes to avoid: - Forgetting to add the tool to all four locations - Adding the import in the wrong section (lazy vs non-lazy) - Incorrect module name in the lazy import proxy - Missing quotes around the tool name in the __all__ list - Not testing the import after making changes
Testing Your Tool¶
Create tests for your tool:
import pytest
from tooluniverse.my_tool import MyTool
class TestMyTool:
def setup_method(self):
self.tool = MyTool()
def test_success(self):
"""Test successful execution."""
result = self.tool.run({"input": "test_value"})
assert result["success"] is True
assert "processed" in result["result"]
def test_error(self):
"""Test error handling."""
result = self.tool.run({"input": ""})
assert result["success"] is False
assert "error" in result
Documentation¶
Create documentation for your tool:
My Tool
=======
Tool description and features.
Usage
-----
.. code-block:: python
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
result = tu.run_one_function({
"name": "my_tool",
"arguments": {"input": "example_value"}
})
Parameters
----------
- **input** (string, required): Input parameter description
Contributing Workflow¶
Create a feature branch:
git checkout -b feature/my-feature
Make your changes and commit:
git add .
git commit -m "feat: add my new tool
- Implement MyTool class
- Add comprehensive tests
- Update documentation"
Push and create a pull request:
git push origin feature/my-feature
Commit Types¶
feat
: New featuresfix
: Bug fixesdocs
: Documentation updatestest
: Test additions or modificationsrefactor
: Code refactoringstyle
: Code style changeschore
: Build/maintenance tasks
Review Process¶
All contributions go through:
Automated Checks: CI runs tests, linting, and type checking
Manual Review: Maintainers review code quality and design
Documentation Review: Ensure docs are clear and complete
Testing: Verify functionality works as expected
Troubleshooting Tool Integration¶
If your tool is not being imported correctly, check these common issues:
ImportError: cannot import name ‘MyTool’
- Verify you added the tool to all four locations in __init__.py
- Check that the module name in the import statement matches your file name
- Ensure the class name matches exactly (case-sensitive)
AttributeError: module ‘tooluniverse’ has no attribute ‘MyTool’
- Verify the tool name is added to the __all__
list
- Check that the tool name in __all__
matches the class name exactly
- Ensure you’re importing from the correct module
LazyImportProxy issues
- Verify the module name in _LazyImportProxy("module_name", "ClassName")
matches your file name
- Check that the class name in the proxy matches your actual class name
- Ensure the module is in the correct location (src/tooluniverse/tools/
)
Testing your integration:
# Test 1: Direct import
from tooluniverse import MyTool
print(f"Tool class: {MyTool}")
# Test 2: Check if it's in __all__
from tooluniverse import __all__
print(f"MyTool in __all__: {'MyTool' in __all__}")
# Test 3: Instantiate the tool
tool = MyTool()
print(f"Tool instance: {tool}")
Getting Help¶
GitHub Issues: Bug reports and feature requests
GitHub Discussions: General questions and ideas
Email: shanghuagao@gmail.com
Types of Contributions¶
Bug Reports¶
Include: - Python version and OS - ToolUniverse version - Minimal code to reproduce - Full error traceback - Expected vs actual behavior
Feature Requests¶
Provide: - Clear use case description - Proposed API design - Implementation suggestions - Impact on existing code
Documentation Improvements¶
Help by: - Fixing typos and grammar - Adding missing examples - Clarifying confusing sections - Translating to other languages
Thank you for contributing to ToolUniverse! 🧬🔬