# Internal Mechanism for Registering Tools from the tools/ Directory in MCP OpenStack Ops

> Discover how MCP OpenStack Ops automatically registers tools from the tools directory using pkgutil and conditional decorators. Learn about the internal mechanism for tool discovery and registration.

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

---

**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`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/tools/__init__.py). This function uses Python's `pkgutil` module to perform dynamic module discovery:

```python
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`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/mcp_main.py) through the `conditional_tool` decorator. This decorator implements environment-based access control:

```python
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:

1. **FastMCP Instantiation**: The server creates a `FastMCP` instance named `"mcp-openstack-ops"` in [`mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/mcp_main.py).

2. **Registration Trigger**: Immediately after instantiation, the code calls `register_all_tools()` to initiate the discovery process.

3. **Package Scanning**: The `pkgutil.iter_modules(__path__)` call enumerates all modules in `src/mcp_openstack_ops/tools/`, such as [`set_instance.py`](https://github.com/call518/mcp-openstack-ops/blob/main/set_instance.py) and [`set_network.py`](https://github.com/call518/mcp-openstack-ops/blob/main/set_network.py).

4. **Module Import**: Each discovered module is imported via `importlib.import_module()`, executing the module-level code.

5. **Decorator Evaluation**: During import, the `@conditional_tool` decorator processes each function definition, checking environment variables and conditionally invoking `mcp.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_tool` that 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_modules` in [`src/mcp_openstack_ops/tools/__init__.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/tools/__init__.py).
- The `register_all_tools()` function imports all modules in the `tools/` package, triggering decorator evaluation at import time.
- 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) conditionally binds functions to the `FastMCP` registry based on the `ALLOW_MODIFY_OPERATIONS` environment variable.
- This architecture enables zero-configuration tool addition—simply placing a new Python file in `tools/` with the `@conditional_tool` decorator 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`](https://github.com/call518/mcp-openstack-ops/blob/main/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`](https://github.com/call518/mcp-openstack-ops/blob/main/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`](https://github.com/call518/mcp-openstack-ops/blob/main/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`](https://github.com/call518/mcp-openstack-ops/blob/main/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.