Tool Loading Architecture
January 6, 2026 · View on GitHub
VoiceMode discovers all available tools from the filesystem, applies include/exclude filters based on configuration, then dynamically loads the filtered list at startup.
Overview
VoiceMode's tool loading system provides automatic discovery and dynamic import of tools from the filesystem. This architecture enables:
- Zero-configuration tool registration
- Service-specific tool organization
- Selective loading for token optimization
- Seamless MCP protocol integration
Directory Structure
voice_mode/tools/
├── __init__.py # Discovery and loading logic
├── {tool_name}.py # Regular tools (e.g. converse.py, devices.py)
├── services/ # Service-specific tools
│ ├── {service}/ # Service directory (e.g. whisper/, kokoro/)
│ │ ├── {tool}.py # Service tool modules (e.g. install.py, uninstall.py)
│ │ └── helpers.py # Shared utilities (excluded)
│ └── ...
├── sound_fonts/ # Feature-specific subdirectories
└── transcription/
Naming Conventions
- Regular tools:
{tool_name}.py→ loaded as{tool_name}(e.g.converse.py→converse) - Service tools:
services/{service}/{tool}.py→ loaded as{service}_{tool}(e.g.services/whisper/install.py→whisper_install) - Excluded patterns:
__init__.py,_*.py,*_helpers.py,types.py
Discovery Mechanism
File System Scanning
The get_all_available_tools() function in voice_mode/tools/__init__.py discovers tools by:
- Scanning the tools directory for Python files
- Recursively scanning the services subdirectory
- Applying exclusion patterns
- Flattening the namespace for MCP exposure
def get_all_available_tools() -> list[str]:
"""Discover all available tools from the filesystem."""
tools = []
# Find regular tools (*.py in tools/)
for file in tools_dir.glob("*.py"):
if should_include_tool(file):
tools.append(file.stem)
# Find service tools (services/*/*.py)
services_dir = tools_dir / "services"
for service_dir in services_dir.iterdir():
if service_dir.is_dir():
for file in service_dir.glob("*.py"):
if should_include_tool(file):
tools.append(f"{service_dir.name}_{file.stem}")
return sorted(tools)
Tool Filtering
Tools are excluded if they match:
__init__.py- Package initialization files_*.py- Private modules (underscore prefix)*_helpers.py- Utility modulestypes.py- Type definition modules
Loading Process
Environment Variable Processing
Three environment variables control tool loading:
-
VOICEMODE_TOOLS_ENABLED(whitelist mode)- Comma-separated list of tools to load
- Only listed tools are loaded
- Highest priority
-
VOICEMODE_TOOLS_DISABLED(blacklist mode)- Comma-separated list of tools to exclude
- All tools except listed are loaded
- Medium priority
-
VOICEMODE_TOOLS(legacy, deprecated)- Backwards compatibility
- Will be removed in v5.0
Dynamic Import Mechanism
The load_tool() function handles the actual import:
def load_tool(tool_name: str) -> bool:
"""Load a single tool by name."""
try:
# First, try as a regular tool (even with underscores)
tool_file = tools_dir / f"{tool_name}.py"
if tool_file.exists():
importlib.import_module(f".{tool_name}", package=__name__)
return True
# If not found and contains underscore, try service pattern
if "_" in tool_name:
parts = tool_name.split("_", 1)
if len(parts) == 2:
service_name, tool_file = parts
module_path = f".services.{service_name}.{tool_file}"
importlib.import_module(module_path, package=__name__)
return True
return False
except ImportError as e:
logger.error(f"Failed to import tool {tool_name}: {e}")
return False
Loading Order
- Check for regular tool file first
- If not found and name contains underscore, try service pattern
- This prevents misinterpretation of tools like
configuration_management
Integration with FastMCP
Server Initialization
In voice_mode/server.py:
# Tools are auto-imported from the tools directory
import voice_mode.tools
# FastMCP server automatically discovers decorated tools
mcp = fastmcp.FastMCP(
name="voicemode",
version=__version__
)
Tool Registration
Tools use FastMCP decorators for automatic registration:
@mcp.tool
async def converse(message: str, wait_for_response: bool = True):
"""Voice conversation tool."""
...
No explicit registration needed - tools are discovered at import time.
Service Tools Pattern
Structure
Service tools are organized by service:
services/
├── whisper/
│ ├── install.py # whisper_install
│ ├── uninstall.py # whisper_uninstall
│ ├── model_active.py # whisper_model_active
│ └── helpers.py # Shared utilities (not loaded)
└── kokoro/
├── install.py # kokoro_install
└── uninstall.py # kokoro_uninstall
Naming Convention
Service tools follow the pattern {service}_{action}:
whisper_install- Install Whisper servicekokoro_status- Check Kokoro service status
Performance Considerations
Token Usage
- Full tool loading: ~25,000 tokens in Claude Code context
- Selective loading (converse only): ~5,000 tokens
- 20,000 token savings with selective loading
Memory Footprint
- Tools are imported on startup
- Lazy loading not currently implemented
- Import-time side effects should be avoided
Error Handling
Missing Tools
When a tool cannot be loaded:
- Warning logged to stderr
- Tool excluded from MCP exposure
- Server continues with available tools
Import Failures
try:
importlib.import_module(module_path, package=__name__)
except ImportError as e:
logger.error(f"Failed to import tool {tool_name}: {e}")
# Tool is skipped, not fatal
Extension Guidelines
Adding New Regular Tools
- Create
voice_mode/tools/{tool_name}.py - Implement tool function with FastMCP decorator
- Tool automatically discovered on next server start
Creating Service Tools
- Create directory:
voice_mode/tools/services/{service}/ - Add tool modules:
install.py,uninstall.py, etc. - Tools exposed as
{service}_{tool} - Place shared code in
helpers.py(excluded from loading)
Best Practices
-
Tool Organization
- Group related tools in service directories
- Use clear, descriptive names
- Keep tools focused on single responsibilities
-
Dependencies
- Avoid heavy imports at module level
- Use lazy imports where possible
- Handle missing dependencies gracefully
-
Documentation
- Include docstrings for MCP description
- Document parameters clearly
- Provide usage examples
Implementation Details
Key Files
voice_mode/tools/__init__.py- Discovery and loading logicvoice_mode/server.py- MCP server initialization- Individual tool modules - Tool implementations
Configuration Flow
- Server startup
- Environment variables checked
- Tool list determined
- Tools dynamically imported
- FastMCP registers decorated functions
- Server exposes tools via MCP protocol
Debugging
Verification
Check loaded tools:
# List all available tools
voicemode list-tools
# Check specific tool loading
VOICEMODE_DEBUG=1 voicemode
Logging
Enable debug logging to see tool loading details:
export VOICEMODE_DEBUG=1
Log output shows:
- Tools discovered
- Tools being loaded
- Import failures
- Final loaded tool list
Common Issues
Tool Not Loading
- Check file name matches expected pattern
- Verify no syntax errors in tool module
- Check tool not in exclusion patterns
- Review debug logs for import errors
Service Tool Conflicts
If a regular tool has an underscore in its name:
- It's checked as a regular tool first
- Only falls back to service pattern if not found
- This prevents misinterpretation
Migration from VOICEMODE_TOOLS
The legacy VOICEMODE_TOOLS variable is deprecated:
- Still functional for backwards compatibility
- Will be removed in v5.0
- Migrate to
VOICEMODE_TOOLS_ENABLEDorVOICEMODE_TOOLS_DISABLED
Migration example:
# Old (deprecated)
export VOICEMODE_TOOLS=converse,statistics
# New (preferred)
export VOICEMODE_TOOLS_ENABLED=converse,statistics