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

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 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.


# 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 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.


# 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, 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 demonstrates volume attachment operations:


# 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):

export ALLOW_MODIFY_OPERATIONS=false
python -m mcp_openstack_ops

Enable mutating operations:

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 or 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.
  • 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 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, 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.

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.

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 →