Skip to content

Adding a New Tool

Step-by-Step

1. Choose a Group

Decide which group your tool belongs to. Check existing groups in tools/:

tools/
├── io/              # File I/O
├── measurements/    # Distances, angles, contacts
├── representations/ # Visual representations
├── color/           # Coloring schemes
├── analysis/        # Structural analysis
├── interactions/    # Interaction detection
├── visibility/      # Show/hide
├── selection/       # Selection management
├── labels/          # Labels
├── camera/          # Camera controls
├── ...

To create a new group, just create a new directory under tools/ with an __init__.py.

2. Create the Tool File

Create tools/<group>/<name>.py:

"""Short description of what the tool does.

Usage: /<group> <name> <arg1> [arg2]
Example: /<group> <name> protein
"""


def run(cmd, selection: str, cutoff: str = "3.5") -> str:
    """Execute the tool."""
    # Dry-run support (for testing without the rendering engine)
    if cmd is None:
        return f"[dry-run] {selection} cutoff={cutoff}"

    # Convert string args to proper types
    cutoff_val = float(cutoff)

    # Call rendering API
    result = cmd.some_function(selection, cutoff=cutoff_val)

    return f"Result: {result}"

All arguments are strings

The parser passes all arguments as strings. Convert to float, int, etc. inside run().

3. Test with Dry-Run

Run the test suite — your tool is automatically included:

pytest tests/ -v

The conftest.py fixture discovers all tools and runs dry-run tests (with cmd=None) automatically via parametrized tests.

4. Add a Shortcut (Optional)

If your tool deserves a top-level shortcut, add it to expand_shortcut() in codemol/app/command_dispatcher.py:

def expand_shortcut(text: str) -> str:
    # ... existing shortcuts ...
    if text.startswith("/myshortcut"):
        return text.replace("/myshortcut", "/<group> <tool>", 1)
    return text

5. Add Autocomplete Entry (Optional)

Add your command to build_command_list() in the same file:

def build_command_list() -> list[tuple[str, str]]:
    return [
        # ... existing entries ...
        ("/<group> <name>", "Short description"),
    ]

Conventions

Naming

  • File name = tool name (e.g., distance.py/measurements distance)
  • Use lowercase, single words when possible
  • For multi-word tools, use underscores in filenames (e.g., salt_bridges.py)

Docstrings

Include usage and example in the module docstring — this is used by the AI agent to understand tool capabilities:

"""Measure the distance between two atoms.

Usage: /measurements distance <atom1> <atom2>
Example: /measurements distance A/45/CA B/120/CA
"""

Return Values

  • Return a human-readable string summarizing what happened
  • Include units where applicable (e.g., "Distance: 3.45 Å")
  • Return error messages as strings (don't raise exceptions for user errors)

Dry-Run Pattern

Always support cmd is None for testing:

def run(cmd, selection: str) -> str:
    if cmd is None:
        return f"[dry-run] would process {selection}"

    # Real implementation...

Example: Complete Tool

Here's a complete tool that calculates solvent-accessible surface area:

"""Calculate solvent-accessible surface area for a selection.

Usage: /surface area [selection]
Example: /surface area protein
"""


def run(cmd, selection: str = "all") -> str:
    if cmd is None:
        return f"[dry-run] SASA for {selection}"

    area = cmd.get_area(selection)
    count = cmd.count_atoms(selection)

    return f"SASA: {area:.1f} Ų ({count} atoms)"

That's it — save as tools/surface/area.py and it's immediately available as /surface area.