How to Add a Custom Tool to OpenMontage Using Inheritance and Auto-Discovery

OpenMontage automatically discovers every concrete tool class that inherits from BaseTool and resides within the tools package, requiring no explicit registration code.

The OpenMontage framework provides a pluggable architecture that allows developers to extend functionality without modifying core source files. By leveraging Python inheritance and the built-in auto-discovery mechanism, you can add custom capabilities that agents and CLI commands consume immediately. This guide walks through the exact implementation details based on the current source code in calesthio/OpenMontage.

Understanding the Tool Registry Architecture

The discovery engine lives in tools/tool_registry.py and implements a singleton pattern that scans the entire tools package at runtime.

The Discovery Mechanism

According to the source code, the ToolRegistry.discover() method (lines 18-33) uses pkgutil.walk_packages to iterate through every module under the tools directory. When it encounters a class that inherits from BaseTool, it instantiates the class and adds it to the internal self._tools mapping. The singleton registry instance (defined at lines 90-92) ensures this discovery happens once per application lifecycle, though you can trigger it manually via registry.ensure_discovered().

The Base Class Contract

All tools must inherit from BaseTool, defined in tools/base_tool.py (lines 27-45). This abstract base class enforces a strict contract:

  • Metadata attributes: name, version, capability, tier, stability, runtime
  • Schema definitions: input_schema and output_schema for validation
  • Dependency checking: dependencies list and check_dependencies() method (lines 5-12)
  • Execution logic: The abstract execute(self, inputs: dict) -> ToolResult method

Creating a Custom Tool Class

To add a custom tool, create a new Python file anywhere under the tools/ directory and subclass BaseTool.

Required Implementation Steps

  1. Import the base class and supporting types:
from tools.base_tool import BaseTool, ToolResult, ToolTier, ToolStability, ToolRuntime
  1. Override metadata attributes to describe your tool's identity and requirements:
class MyAwesomeTool(BaseTool):
    name = "my_awesome_tool"
    version = "0.1.0"
    tier = ToolTier.CORE
    stability = ToolStability.EXPERIMENTAL
    runtime = ToolRuntime.LOCAL
    capability = "text_generation"
    provider = "my_company"
  1. Define input and output schemas using JSON Schema format:
    input_schema = {
        "type": "object",
        "properties": {"message": {"type": "string"}},
        "required": ["message"],
    }
    output_schema = {
        "type": "object",
        "properties": {"reply": {"type": "string"}},
    }
  1. Implement the execute method containing your business logic:
    def execute(self, inputs: dict) -> ToolResult:
        msg = inputs.get("message", "")
        reply = f"Echo: {msg}"
        return ToolResult(success=True, data={"reply": reply})

Complete Working Example

Place this file at tools/custom/my_awesome_tool.py:

from tools.base_tool import BaseTool, ToolResult, ToolTier, ToolStability, ToolRuntime

class MyAwesomeTool(BaseTool):
    """A simple example that echoes a string back to the user."""
    
    # ---- Identity ---------------------------------------------------------

    name = "my_awesome_tool"
    version = "0.1.0"
    tier = ToolTier.CORE
    stability = ToolStability.EXPERIMENTAL
    runtime = ToolRuntime.LOCAL
    
    # ---- Capabilities ------------------------------------------------------

    capability = "text_generation"
    provider = "my_company"
    capabilities = ["echo"]
    best_for = ["quick demos"]
    
    # ---- Dependencies -------------------------------------------------------

    dependencies = []                     # No external deps

    install_instructions = "Just import – nothing to install."
    
    # ---- Schemas ------------------------------------------------------------

    input_schema = {
        "type": "object",
        "properties": {"message": {"type": "string"}},
        "required": ["message"],
    }
    output_schema = {
        "type": "object",
        "properties": {"reply": {"type": "string"}},
    }
    
    # ---- Execution ----------------------------------------------------------

    def execute(self, inputs: dict) -> ToolResult:
        msg = inputs.get("message", "")
        reply = f"Echo: {msg}"
        return ToolResult(success=True, data={"reply": reply})

Registering Your Tool via Auto-Discovery

No explicit registration is required. The auto-discovery system finds your tool automatically when you place the subclass in a module under the tools package.

File Placement Requirements

  • Location: Any subdirectory of tools/ (e.g., tools/custom/, tools/contrib/)
  • Module visibility: The file must be importable as part of the tools package
  • Class visibility: The class must be defined at module level (not nested inside functions)

The registry uses pkgutil.walk_packages (as implemented in tool_registry.py lines 27-33) to locate modules, then inspects each for BaseTool subclasses using Python's __subclasses__() mechanism.

Handling Dependencies

If your tool requires external binaries, environment variables, or Python packages, populate the dependencies list and check_dependencies() will validate availability at runtime (see tools/base_tool.py lines 4-9). This allows agents to skip unavailable tools gracefully.

Verifying and Testing Your Tool

After adding your file, verify registration through the singleton registry instance.

Check Registry Discovery

from tools.tool_registry import registry

# Trigger discovery if not already done

registry.ensure_discovered()

# List all available tools

print(registry.list_all())

# Output: ['my_awesome_tool', ...other tools...]

# Retrieve specific tool instance

tool = registry.get("my_awesome_tool")
print(tool)

# Output: <MyAwesomeTool ...>

Execute Direct Testing

Test the tool logic directly without the agent framework:

tool = registry.get("my_awesome_tool")
result = tool.execute({"message": "Hello"})

print(result.success)  # True

print(result.data)     # {'reply': 'Echo: Hello'}

The tool immediately appears in the auto-generated provider menu used by agents and becomes available to CLI commands throughout OpenMontage.

Summary

  • Inherit from BaseTool: Define your tool class in tools/base_tool.py and override required metadata and the execute() method.
  • Place in tools package: Save your module anywhere under the tools/ directory; subdirectories are automatically scanned.
  • Auto-discovery activates: The ToolRegistry in tools/tool_registry.py uses pkgutil.walk_packages to find and instantiate your class without explicit registration.
  • Dependencies are optional: Use the dependencies list to enable check_dependencies() validation for external requirements.
  • Immediate availability: Once discovered, tools appear in registry.list_all(), registry.get(name), and agent provider menus.

Frequently Asked Questions

Where exactly should I save my custom tool file?

Save your Python file in any subdirectory of the tools/ folder, such as tools/custom/my_tool.py. The registry recursively scans all subdirectories using pkgutil.walk_packages, so organizational subfolders are supported natively. Ensure the directory contains an __init__.py file to make it a proper Python package.

Do I need to import my tool anywhere or edit the registry?

No. The auto-discovery mechanism in tools/tool_registry.py automatically imports every module in the tools package and registers any concrete subclass of BaseTool it finds. You do not need to modify tool_registry.py or any initialization files.

What methods and attributes are mandatory when subclassing BaseTool?

You must override the execute(self, inputs: dict) -> ToolResult method with your implementation logic. Required attributes include name, version, capability, tier, stability, runtime, input_schema, and output_schema. These are defined in tools/base_tool.py and enforced by the discovery system.

How do I handle external dependencies like API keys or binaries?

Populate the dependencies class attribute with a list of requirement strings (e.g., ["ffmpeg", "OPENAI_API_KEY"]). The BaseTool.check_dependencies() method (lines 5-12 in base_tool.py) validates these at runtime, allowing agents to filter out unavailable tools. Include installation instructions in the install_instructions attribute for user guidance.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →