# Architectural Differences Between get_* and set_* Tool Categories in MCP OpenStack Ops

> Explore the architectural differences between get_* and set_* tool categories in MCP OpenStack Ops. Learn about their distinct registration and post-action handling mechanisms for efficient operations.

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

---

**The `get_*` and `set_*` tool categories differ primarily in registration mechanism—read‑only tools use `@mcp.tool()` directly while mutating tools use `@conditional_tool` gated by `ALLOW_MODIFY_OPERATIONS`—and in post‑action handling, where set tools perform verification and enrichment via `handle_operation_result`.**

The **mcp‑openstack‑ops** repository organizes OpenStack operations into two distinct families that determine how LLM agents interact with cloud infrastructure. Understanding the architectural separation between these tool categories is essential for securing production deployments and optimizing agent behavior. This analysis examines the registration patterns, safety controls, and implementation details that distinguish read‑only queries from state‑changing operations.

## Tool Registration Architecture

### Unconditional Registration for get_* Tools

Every file under `src/mcp_openstack_ops/tools` that begins with `get_` registers its async function using the standard `@mcp.tool()` decorator. In [`src/mcp_openstack_ops/tools/get_instance.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/tools/get_instance.py), the decorator appears at lines 15‑16:

```python
@mcp.tool()
async def get_instance(...):
    # implementation

```

This unconditional registration ensures that read‑only operations—such as listing instances, volumes, or networks—are always available to the LLM regardless of environment configuration.

### Conditional Registration for set_* Tools

Mutating tools employ a custom `@conditional_tool` decorator 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) (lines 55‑65). This wrapper checks the `ALLOW_MODIFY_OPERATIONS` environment variable before delegating to `mcp.tool()`:

```python
def conditional_tool(func):
    if os.environ.get("ALLOW_MODIFY_OPERATIONS", "false").lower() == "true":
        return mcp.tool()(func)
    return func  # Returns unregistered function when disabled

```

When modification operations are disabled, `conditional_tool` returns the original function unchanged, effectively hiding the tool from the MCP runtime and preventing accidental state mutations.

## Implementation Patterns and Control Flow

### Get Tool Pattern: Direct Query with JSON Response

Read‑only tools follow a streamlined execution path:

1. Log the incoming request via the shared `logger`
2. Call low‑level wrappers (e.g., `_get_instance_details`, `_get_volume_list`) from [`src/mcp_openstack_ops/functions.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/functions.py)
3. Assemble a JSON payload containing timestamps, counts, and pagination metadata
4. Return formatted JSON via `json.dumps(payload, indent=2)`

The [`get_instance.py`](https://github.com/call518/mcp-openstack-ops/blob/main/get_instance.py) implementation demonstrates this pattern through its decision tree (lines 62‑84), which handles query modes like `"all"`, `"names"`, `"ids"`, `"status"`, and `"search"`. Since these calls never change state, they require no post‑action verification or result enrichment.

### Set Tool Pattern: Validation, Discovery, and Verification

Mutating tools implement a more elaborate workflow orchestrated through [`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):

1. **Permission validation** – The `@conditional_tool` decorator ensures registration only when explicitly permitted
2. **Input validation** – Verify required arguments (e.g., `action` cannot be empty)
3. **Target identification** – Support both direct naming (`instance_names`) and filter‑based discovery using `name_contains`, `status`, or other criteria
4. **Core operation** – Invoke the corresponding `_set_*` wrapper (e.g., `_set_instance`)
5. **Result normalization** – Pass raw results to `handle_operation_result` for consistent formatting
6. **Post‑action verification** – Retrieve current status for each processed resource and embed emoji indicators (🟢 ACTIVE, 🔴 SHUTOFF)
7. **Bulk summary assembly** – Generate human‑readable output listing successes, failures, and final statuses

The `handle_operation_result` function in [`mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/mcp_main.py) (lines 12‑48) centralizes success detection and adds asynchronous operation notes when actions like `"start"` or `"create"` are detected, tagging results with `"operation_type": "asynchronous"`.

## Safety Controls and Permission Mechanisms

The repository implements multiple layers of protection for mutating operations:

- **`ALLOW_MODIFY_OPERATIONS` environment variable** – Controls tool registration at startup. When set to `"false"`, set tools exist as Python functions but remain invisible to the LLM.
- **`_check_modify_operation_permission`** – Generates user‑facing warnings describing the block and listing available read‑only alternatives (lines 27‑52 in [`mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/mcp_main.py)).
- **Structured logging** – All mutations log intent via `logger.info()` before executing API calls, creating audit trails.
- **Result validation** – `handle_operation_result` standardizes error messages and detects partial failures in bulk operations.

## Practical Code Examples

### Querying Instances (Read‑Only)

```python

# Retrieve summarized details for specific VMs

response = await get_instance(
    names="web01,db01",
    detailed=False,
    limit=2
)
print(response)

```

This returns a JSON payload with `"mode": "summary"` and a brief instance list. The tool is always available because it belongs to the `get_*` family.

### Stopping Instances (Mutating)

```python

# Stop all instances with names containing "dev"

response = await set_instance(
    name_contains="dev",
    action="stop"
)
print(response)

```

This produces a human‑readable summary showing each processed instance with ✅/❌ indicators and post‑action status emojis. The tool only executes when `ALLOW_MODIFY_OPERATIONS=true`.

### Creating a New Server (Mutating)

```python
response = await set_instance(
    instance_names="new-app-01",
    action="create",
    flavor="m1.small",
    image="ubuntu-22.04",
    networks="private-net",
    security_groups="default"
)
print(response)

```

The `handle_operation_result` enriches the output with async guidance (e.g., "expected completion: 30‑60 seconds") because `"create"` triggers asynchronous provisioning.

## Key Implementation Files

| File | Role |
|------|------|
| [`src/mcp_openstack_ops/tools/get_instance.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/tools/get_instance.py) | Representative read‑only tool implementing query logic and pagination |
| [`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) | Representative mutating tool with target discovery and bulk handling |
| [`src/mcp_openstack_ops/mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/mcp_main.py) | Contains `conditional_tool` decorator (lines 55‑65) and `handle_operation_result` helper (lines 12‑48) |
| [`src/mcp_openstack_ops/functions.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/functions.py) | Low‑level OpenStack SDK wrappers (`_get_*` and `_set_*`) used by both families |

## Summary

- **get_* tools** use `@mcp.tool()` for unconditional registration, perform read‑only queries, and return straightforward JSON payloads without post‑action verification.
- **set_* tools** use `@conditional_tool` gated by `ALLOW_MODIFY_OPERATIONS`, enforce validation and target discovery, and utilize `handle_operation_result` for enriched, user‑friendly output including async operation notes and status emojis.
- **Safety architecture** prevents accidental mutations through environment‑driven registration control, structured logging, and permission checking.

## Frequently Asked Questions

### Why do set_* tools require ALLOW_MODIFY_OPERATIONS while get_* tools do not?

The `get_*` tools perform read‑only operations that cannot alter cloud infrastructure state, making them safe for unrestricted LLM access. The `set_*` tools can create, modify, or delete resources, so the `ALLOW_MODIFY_OPERATIONS` flag provides a circuit breaker to prevent accidental mutations in production environments where agents should only observe rather than modify infrastructure.

### How does the conditional_tool decorator prevent set_* tools from appearing in the tool list?

When `ALLOW_MODIFY_OPERATIONS` is not set to `"true"`, the `conditional_tool` decorator returns the original function object without applying `@mcp.tool()`. Since MCP only exposes functions decorated with `@mcp.tool()` to the LLM runtime, the undecorated functions remain accessible as Python code but invisible to the agent, effectively removing them from the available tool catalog.

### What happens when a set_* tool targets multiple resources?

The [`set_instance.py`](https://github.com/call518/mcp-openstack-ops/blob/main/set_instance.py) implementation (lines 150‑188) handles bulk operations by iterating through target instances, executing the action for each, and collecting individual results. It then assembles a comprehensive summary showing per‑instance success/failure status, post‑operation state with emoji indicators (🟢 ACTIVE, 🔴 SHUTOFF), and any error messages, providing complete visibility into partial failures across the resource set.

### Why do set_* tools use handle_operation_result instead of returning raw API responses?

The `handle_operation_result` function standardizes output format across all mutating operations, adds contextual notes for asynchronous actions (identifying operations that require polling), and ensures consistent error handling. This abstraction prevents each tool from implementing redundant formatting logic and guarantees that LLM agents receive predictable, human‑readable summaries rather than raw OpenStack SDK data structures.