# How the conditional_tool Decorator Enables Dynamic Tool Registration in the MCP Framework

> Discover how the conditional_tool decorator dynamically registers tools in the MCP framework. Learn to control mutating operations via environment variables for a flexible codebase. Optimize your OpenStack ops.

- Repository: [JungJungIn/mcp-openstack-ops](https://github.com/call518/mcp-openstack-ops)
- Tags: internals
- Published: 2026-02-26

---

**TLDR:** The `conditional_tool` decorator gates tool registration behind the `ALLOW_MODIFY_OPERATIONS` environment variable, conditionally applying `@mcp.tool()` to functions only when runtime configuration permits, enabling a single codebase to dynamically expose or suppress mutating operations.

The MCP framework (via FastMCP) registers Python functions as callable tools using the `@mcp.tool()` decorator. In the `call518/mcp-openstack-ops` repository, the `conditional_tool` decorator pattern extends this mechanism to support **dynamic tool registration**, allowing operators to toggle entire categories of tools—specifically mutating OpenStack operations—through environment configuration rather than code changes.

## How the conditional_tool Pattern Works

The pattern consists of three coordinated components: a runtime environment check, a conditional wrapper, and an eager import system that triggers registration at startup.

### Environment-Driven Registration Gate

At the core of the pattern is the `_is_modify_operation_allowed()` helper function, which evaluates the `ALLOW_MODIFY_OPERATIONS` environment variable. The `conditional_tool` decorator in [`src/mcp_openstack_ops/mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/mcp_main.py) uses this check to determine whether to register the wrapped function with the FastMCP instance `mcp = FastMCP("mcp-openstack-ops")`.

### Selective Tool Registration Logic

When Python imports a module containing a function decorated with `@conditional_tool`, the decorator executes immediately. If the environment flag is true, it returns `mcp.tool()(func)`, which registers the function as an MCP tool. If false, it returns the original function unchanged, effectively hiding the tool from the FastMCP registry.

```python

# src/mcp_openstack_ops/mcp_main.py

def conditional_tool(func):
    """
    Decorator that conditionally registers tools based on ALLOW_MODIFY_OPERATIONS setting.
    Modify operations are only registered when explicitly enabled.
    """
    if _is_modify_operation_allowed():
        return mcp.tool()(func)          # ← real registration

    else:
        return func                       # ← function stays unregistered

```

### Eager Import Trigger Mechanism

The `register_all_tools()` function in [`src/mcp_openstack_ops/tools/__init__.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/tools/__init__.py) ensures all tool modules are imported at application startup. This eager import pattern causes the `conditional_tool` decorator to execute for every defined tool, dynamically populating the MCP registry based solely on the current environment configuration.

```python

# src/mcp_openstack_ops/tools/__init__.py

def register_all_tools() -> None:
    """Import every tool module so decorators register with FastMCP."""
    for module_name in sorted(_iter_tool_modules()):
        importlib.import_module(f"{__name__}.{module_name}")

```

## Implementation in the OpenStack Ops Codebase

### The Decorator Definition

Located in [`src/mcp_openstack_ops/mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/mcp_main.py), the `conditional_tool` decorator implements the runtime gating logic that separates read-only from mutating operations.

### Module Discovery and Registration

The `register_all_tools()` function walks the `tools` package directory, importing each module by name. This import triggers the execution of all module-level decorators, including `@conditional_tool`, ensuring the FastMCP registry reflects the current environment state without explicit registration calls in each tool file.

### Dynamic Tool Set Behavior

When the application starts, `register_all_tools()` runs immediately after the FastMCP instance creation. Each tool is either added to the registry or silently ignored based on the environment flag. This makes the command set **dynamic**: a user can flip a single environment variable to expose or hide all "modify" operations (create, delete, update) while read-only tools remain always available.

## Practical Usage Examples

### Defining a Conditional Mutating Tool

Tools that modify OpenStack resources use `@conditional_tool` instead of `@mcp.tool()`. The following example from [`src/mcp_openstack_ops/tools/set_server_volume.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/tools/set_server_volume.py) demonstrates volume attachment operations:

```python

# src/mcp_openstack_ops/tools/set_server_volume.py

from ..functions import set_server_volume as _set_server_volume
from ..mcp_main import conditional_tool, handle_operation_result, logger

@conditional_tool                # ← registration is conditional

async def set_server_volume(
    instance_name: str,
    action: str,
    volume_id: Optional[str] = None,
    volume_name: Optional[str] = None,
    device: Optional[str] = None,
    attachment_id: Optional[str] = None,
) -> str:
    """Attach, detach or list volumes on a server."""
    # implementation omitted for brevity

    ...

```

### Configuring Runtime Behavior

Operators control tool availability through environment variables before launching the application.

**Disable mutating operations (read-only mode):**

```bash
export ALLOW_MODIFY_OPERATIONS=false
python -m mcp_openstack_ops

```

**Enable mutating operations:**

```bash
export ALLOW_MODIFY_OPERATIONS=true
python -m mcp_openstack_ops

```

## Benefits of Dynamic Tool Registration

- **Safety**: Provides a single, auditable kill switch for all mutating operations in production environments where accidental modifications could impact critical OpenStack infrastructure.
- **Zero-code toggling**: Toggles between read-only and read-write modes without commenting out imports, editing tool files, or redeploying code.
- **Automatic extensibility**: New mutating tools automatically inherit the registration logic simply by using the `@conditional_tool` decorator, requiring no changes to the registration infrastructure in [`mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/mcp_main.py) or [`tools/__init__.py`](https://github.com/call518/mcp-openstack-ops/blob/main/tools/__init__.py).

## Summary

- The `conditional_tool` decorator wraps FastMCP's native `@mcp.tool()` registration behind an environment variable check in [`src/mcp_openstack_ops/mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/mcp_main.py).
- It evaluates `ALLOW_MODIFY_OPERATIONS` via `_is_modify_operation_allowed()` during module import to determine registration eligibility.
- The `register_all_tools()` function in [`src/mcp_openstack_ops/tools/__init__.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/tools/__init__.py) triggers eager module imports, causing immediate decorator evaluation at startup.
- When enabled, tools register normally with the FastMCP instance; when disabled, functions remain available in Python but invisible to the MCP framework.
- This pattern enables secure, configuration-driven deployments where the same codebase serves both read-only monitoring and full administrative operations.

## Frequently Asked Questions

### What happens to a function decorated with conditional_tool when ALLOW_MODIFY_OPERATIONS is false?

The function remains defined in the Python module and callable as a regular function, but it is never registered with the FastMCP instance. The decorator returns the original function object unchanged, bypassing the `mcp.tool()` registration call entirely, so the MCP framework remains unaware of the tool's existence.

### Why use eager imports instead of explicit registration calls?

The eager import pattern in `register_all_tools()` ensures that all `@conditional_tool` decorators execute automatically during application startup. This eliminates the need to manually maintain a registration list or import statements in [`mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/mcp_main.py), reducing boilerplate and preventing registration omissions when adding new tool modules to the `src/mcp_openstack_ops/tools/` directory.

### Can I use conditional_tool for non-mutating operations?

Yes, though the pattern is designed specifically for mutating operations in this repository. Any tool function can use `@conditional_tool`, but the environment variable name `ALLOW_MODIFY_OPERATIONS` implies a semantic contract. For other conditional logic, you would need to modify the `_is_modify_operation_allowed()` helper or create additional decorator variants in [`mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/mcp_main.py).

### How does this pattern affect FastMCP's tool discovery?

FastMCP discovers tools exclusively through its internal registry populated by `@mcp.tool()` calls. Since `conditional_tool` acts as a gatekeeper that conditionally invokes the real registration, FastMCP only sees tools where the condition evaluated to true. This creates a dynamic tool surface that changes based on the runtime environment without requiring conditional logic within the FastMCP framework itself.