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_tooldecorator, requiring no changes to the registration infrastructure inmcp_main.pyortools/__init__.py.
Summary
- The
conditional_tooldecorator wraps FastMCP's native@mcp.tool()registration behind an environment variable check insrc/mcp_openstack_ops/mcp_main.py. - It evaluates
ALLOW_MODIFY_OPERATIONSvia_is_modify_operation_allowed()during module import to determine registration eligibility. - The
register_all_tools()function insrc/mcp_openstack_ops/tools/__init__.pytriggers 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →