# How the ALLOW_MODIFY_OPERATIONS Flag Controls Resource Modification Requests in the MCP OpenStack Server

> Discover how the ALLOW_MODIFY_OPERATIONS flag in MCP OpenStack Ops controls resource modification requests. Learn to prevent accidental changes by understanding this crucial safety switch.

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

---

**The `ALLOW_MODIFY_OPERATIONS` environment variable acts as a runtime safety switch that prevents accidental creation, update, or deletion of OpenStack resources by conditionally unregistering modify tools from the MCP framework when set to `false` (default).**

In the `call518/mcp-openstack-ops` repository, this flag provides a critical guardrail for production environments. When disabled, write-capable tools like `set_instance` or `set_network` are never exposed to the MCP client, ensuring that read-only operations remain available while destructive actions are physically blocked. The implementation spans three coordinated mechanisms in the core server bootstrap file.

## Core Protection Mechanisms

The safety system relies on three distinct components working in concert within [`src/mcp_openstack_ops/mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/mcp_main.py):

### `_is_modify_operation_allowed()` Boolean Helper

This simple utility reads the environment variable and returns `True` only when the value is `"true"` (case-insensitive). The implementation parses `os.environ.get("ALLOW_MODIFY_OPERATIONS", "false")`, converts it to lowercase, and compares against `"true"` (lines 220-226). All other values—including the default `"false"`—result in a `False` return, triggering the protective behavior.

### `conditional_tool` Decorator

Every write tool (e.g., `set_instance`, `set_network`, `set_image`) uses this decorator to determine MCP registration eligibility. If `_is_modify_operation_allowed()` returns `True`, the decorator wraps the function with `mcp.tool()`, making it a live callable. Otherwise, it returns the original function **without** registration, effectively rendering the tool invisible to the MCP client (lines 55-63). This import-time decision happens during `register_all_tools()` in [`src/mcp_openstack_ops/tools/__init__.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/tools/__init__.py).

### `_check_modify_operation_permission()` Runtime Guard

As a secondary defense, each protected tool invokes this function before executing OpenStack API calls. When the flag is disabled, it returns a multi-line markdown error block explaining that modify operations are blocked and instructing the user to set `ALLOW_MODIFY_OPERATIONS=true` in their `.env` file (lines 227-253). This prevents direct Python imports from bypassing the decorator-based protection.

## How the Protection Flow Works

1. **Server startup** imports all tools via `register_all_tools()`, triggering the `@conditional_tool` decorator on every modify-capable function.

2. **Registration decision**: If `ALLOW_MODIFY_OPERATIONS` is not explicitly `"true"`, the decorator returns the raw function without `mcp.tool()` wrapping, excluding it from the MCP tool registry.

3. **Client invocation**: Chat commands targeting unregistered tools fail immediately because the MCP framework has no knowledge of the function's existence.

4. **Direct import fallback**: If a developer imports and calls a protected function directly (e.g., `from mcp_openstack_ops.tools.set_instance import set_instance`), the internal call to `_check_modify_operation_permission()` returns a permission error before any OpenStack connection occurs.

Thus, **read-only tools** (e.g., `get_instance_details`, `get_quota`) remain universally available, while **modify tools** (prefixed with `set_…`) become active only after explicit opt-in.

## Configuration Examples

### Disabling Modify Operations (Default)

When the environment variable is unset or explicitly false, the server operates in read-only mode:

```bash

# .env or shell export

export ALLOW_MODIFY_OPERATIONS=false

```

Attempting to invoke a protected tool returns the guard message:

```python
from mcp_openstack_ops.tools.set_instance import set_instance

result = await set_instance(instance_names="demo-vm", action="start")
print(result)

```

**Output**:

```

❌ **MODIFY OPERATION BLOCKED**

This operation can modify or delete OpenStack resources and has been disabled for safety.

To enable modify operations, set the following in your .env file:

```

ALLOW_MODIFY_OPERATIONS=true

```

```

### Enabling Modify Operations

Setting the flag to `"true"` registers all tools and permits OpenStack mutations:

```bash
export ALLOW_MODIFY_OPERATIONS=true

```

Now the same Python call executes against the OpenStack API and returns a JSON success payload processed by `handle_operation_result()`.

## Implementing Protection in New Tools

When adding a new resource-modifying tool, apply the safety pattern used in [`src/mcp_openstack_ops/tools/set_instance.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/tools/set_instance.py):

```python

# src/mcp_openstack_ops/tools/set_custom_resource.py

from ..mcp_main import conditional_tool, _check_modify_operation_permission

@conditional_tool
async def set_custom_resource(name: str, action: str) -> str:
    # Runtime guard for direct imports

    permission_msg = _check_modify_operation_permission()
    if permission_msg:
        return permission_msg

    # Proceed with actual OpenStack API call

    return "✅ custom resource modified"

```

If `ALLOW_MODIFY_OPERATIONS` is disabled, the `@conditional_tool` decorator prevents MCP registration, and `_check_modify_operation_permission()` ensures any direct invocation returns the standard permission error.

## Summary

- The `ALLOW_MODIFY_OPERATIONS` environment variable defaults to `"false"`, enforcing read-only mode unless explicitly enabled.
- The `conditional_tool` decorator in [`mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/mcp_main.py) (lines 55-63) controls tool registration at import time, physically excluding modify tools from the MCP framework when the flag is disabled.
- `_check_modify_operation_permission()` (lines 227-253) provides runtime protection against direct Python imports, returning a formatted error message instead of executing OpenStack calls.
- Tools prefixed with `set_` (such as `set_instance`) are protected, while `get_` tools remain always available.

## Frequently Asked Questions

### How do I check if modify operations are enabled programmatically?

Import the boolean helper from the main module and inspect its return value:

```python
from mcp_openstack_ops.mcp_main import _is_modify_operation_allowed

if _is_modify_operation_allowed():
    print("Modify operations enabled")

```

This function performs a case-insensitive check against the environment variable, returning `True` only for the exact string `"true"`.

### Why can I still import the function when ALLOW_MODIFY_OPERATIONS is false?

The `conditional_tool` decorator only controls **MCP framework registration**, not Python module imports. The function object remains importable, but the `_check_modify_operation_permission()` guard inside the function body returns an error message before any OpenStack API call executes. This dual-layer protection prevents accidental usage while maintaining code availability for testing.

### Does this flag affect read-only operations like listing instances?

No. Tools such as `get_instance_details`, `get_quota`, and other read-only functions do not use the `@conditional_tool` decorator. They are always registered with the MCP framework regardless of the `ALLOW_MODIFY_OPERATIONS` value, ensuring continuous observability even in heavily restricted environments.

### What happens if ALLOW_MODIFY_OPERATIONS is set to "TRUE" or "True"?

The `_is_modify_operation_allowed()` function converts the environment variable to lowercase before comparison, so `"TRUE"`, `"True"`, and `"true"` are all valid values that enable modify operations. Any other value—including unset variables, `"yes"`, or `"1"`—results in the default protective behavior.