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

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, the decorator appears at lines 15‑16:

@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 (lines 55‑65). This wrapper checks the ALLOW_MODIFY_OPERATIONS environment variable before delegating to mcp.tool():

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

  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 (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).
  • Structured logging – All mutations log intent via logger.info() before executing API calls, creating audit trails.
  • Result validationhandle_operation_result standardizes error messages and detects partial failures in bulk operations.

Practical Code Examples

Querying Instances (Read‑Only)


# 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)


# 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)

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 Representative read‑only tool implementing query logic and pagination
src/mcp_openstack_ops/tools/set_instance.py Representative mutating tool with target discovery and bulk handling
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 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 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →