Internal Mechanism for Registering Tools from the tools/ Directory in MCP OpenStack Ops
The MCP server automatically discovers and registers tools by scanning the tools/ package with pkgutil.iter_modules, importing each module to trigger the @conditional_tool decorator, which conditionally binds functions to the FastMCP registry based on the ALLOW_MODIFY_OPERATIONS environment variable.
The call518/mcp-openstack-ops repository implements a dynamic tool registration system that eliminates manual wiring. When the server initializes, it scans the tools/ directory and automatically exposes Python functions as MCP tools through a coordinated import mechanism and conditional decorator pattern.
How the Registration System Works
The Dynamic Import Engine
At the heart of the internal mechanism for registering tools from the tools/ directory lies the register_all_tools() function in src/mcp_openstack_ops/tools/__init__.py. This function uses Python's pkgutil module to perform dynamic module discovery:
import pkgutil
import importlib
def register_all_tools():
"""Dynamically import all modules in the tools package."""
for importer, modname, ispkg in pkgutil.iter_modules(__path__):
if not modname.startswith('_'):
importlib.import_module(f'{__name__}.{modname}')
When register_all_tools() executes, it iterates over every non-private module in the tools/ package. Each import operation triggers the top-level code execution within the module, which includes applying the @conditional_tool decorator to function definitions.
The Conditional Binding Decorator
The actual binding to the MCP registry occurs in src/mcp_openstack_ops/mcp_main.py through the conditional_tool decorator. This decorator implements environment-based access control:
import os
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("mcp-openstack-ops")
def conditional_tool(func):
"""
Decorator that conditionally registers a function as an MCP tool
based on ALLOW_MODIFY_OPERATIONS environment variable.
"""
if os.getenv("ALLOW_MODIFY_OPERATIONS", "false").lower() == "true":
return mcp.tool()(func)
return func
When a module from the tools/ directory is imported, functions decorated with @conditional_tool are evaluated immediately. If the ALLOW_MODIFY_OPERATIONS environment variable is set to "true", the decorator wraps the function with mcp.tool(), registering it with the FastMCP instance. Otherwise, the function remains unregistered, effectively gating write operations while allowing read-only tools to remain available.
Step-by-Step Tool Discovery Process
The internal mechanism for registering tools from the tools/ directory follows a precise initialization sequence:
-
FastMCP Instantiation: The server creates a
FastMCPinstance named"mcp-openstack-ops"inmcp_main.py. -
Registration Trigger: Immediately after instantiation, the code calls
register_all_tools()to initiate the discovery process. -
Package Scanning: The
pkgutil.iter_modules(__path__)call enumerates all modules insrc/mcp_openstack_ops/tools/, such asset_instance.pyandset_network.py. -
Module Import: Each discovered module is imported via
importlib.import_module(), executing the module-level code. -
Decorator Evaluation: During import, the
@conditional_tooldecorator processes each function definition, checking environment variables and conditionally invokingmcp.tool()to register the function.
This design ensures that adding a new tool requires only creating a Python file in the tools/ directory with the appropriate decorator—no manual registry updates or configuration files are necessary.
Environment-Based Access Control
The registration system implements a security boundary through the ALLOW_MODIFY_OPERATIONS environment variable. This mechanism distinguishes between read-only operations and destructive OpenStack operations:
-
When
ALLOW_MODIFY_OPERATIONS=true: All tools, including those that modify OpenStack resources (create, update, delete operations), are registered with the MCP server. -
When
ALLOW_MODIFY_OPERATIONS=false(default): Only read-only tools are exposed. Functions decorated with@conditional_toolthat represent write operations return unwrapped, remaining unavailable to LLM clients.
This conditional registration occurs at import time, meaning the security posture is determined when the server starts and remains static during runtime.
Summary
- The internal mechanism for registering tools from the tools/ directory relies on dynamic module discovery using
pkgutil.iter_modulesinsrc/mcp_openstack_ops/tools/__init__.py. - The
register_all_tools()function imports all modules in thetools/package, triggering decorator evaluation at import time. - The
@conditional_tooldecorator insrc/mcp_openstack_ops/mcp_main.pyconditionally binds functions to theFastMCPregistry based on theALLOW_MODIFY_OPERATIONSenvironment variable. - This architecture enables zero-configuration tool addition—simply placing a new Python file in
tools/with the@conditional_tooldecorator automatically exposes it to the MCP server.
Frequently Asked Questions
How does the server know which files to import from the tools directory?
The server uses Python's pkgutil.iter_modules() function in src/mcp_openstack_ops/tools/__init__.py to programmatically list all modules within the tools/ package. It filters out private modules (those starting with underscore) and imports each remaining module dynamically using importlib.import_module(). This approach requires no hardcoded file lists or manual registry maintenance.
What happens if ALLOW_MODIFY_OPERATIONS is set to false?
When the ALLOW_MODIFY_OPERATIONS environment variable is missing or set to "false", the @conditional_tool decorator in src/mcp_openstack_ops/mcp_main.py returns the original function without wrapping it in mcp.tool(). Consequently, write operations (such as creating or deleting OpenStack resources) remain unregistered and invisible to MCP clients, while read-only operations are still available.
Can I add new tools without restarting the server?
No, the current implementation requires a server restart to register new tools. The register_all_tools() function executes only once during server initialization in src/mcp_openstack_ops/mcp_main.py, and the @conditional_tool decorator evaluates at module import time. To expose a new tool, you must place the Python file in the tools/ directory and restart the MCP server to trigger the re-import process.
Where is the FastMCP instance defined?
The FastMCP instance is defined in src/mcp_openstack_ops/mcp_main.py at the module level. The code creates a single instance named mcp = FastMCP("mcp-openstack-ops"), which serves as the central registry for all tools. The @conditional_tool decorator references this global mcp object to conditionally invoke mcp.tool() when registering functions.
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 →