# Typical Workflow for Developing and Integrating a New Octavia Load Balancer Management Tool

> Learn the typical workflow for integrating a new Octavia load balancer management tool into MCP-OpenStack-Ops. Discover how to implement core functions and decorators for seamless integration.

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

---

**Adding a new Octavia load balancer management tool to MCP-OpenStack-Ops requires implementing a synchronous core function in the service layer, exporting it via the module initializer, and wrapping it with an asynchronous MCP tool decorator that automatically registers at server startup.**

The **MCP-OpenStack-Ops** repository provides a modular framework for managing OpenStack resources through the Model Context Protocol (MCP). When extending this server to support new Octavia load balancer operations—such as managing L7 rules, listeners, or pools—developers follow a consistent two-layer architecture that separates OpenStack SDK interactions from MCP protocol exposure. This workflow ensures that new capabilities remain project-isolated, safely gated, and automatically discoverable by MCP clients.

## Understanding the Two-Layer Architecture

The codebase is structured around a **service layer** and a **tool-wrapper layer**. The service layer contains synchronous functions that interact directly with the OpenStack SDK, while the tool layer provides thin asynchronous wrappers that expose these functions to the MCP protocol.

**Service Layer** (`src/mcp_openstack_ops/services/load_balancer/`): Houses pure OpenStack-SDK interactions. Functions like `set_load_balancer_l7_rule` in [`services/load_balancer/l7_policies.py`](https://github.com/call518/mcp-openstack-ops/blob/main/services/load_balancer/l7_policies.py) accept an `action` string and `**kwargs`, perform the SDK call, and return plain Python dictionaries. This design makes the core logic reusable by CLI tools, monitoring scripts, or other automation outside the MCP context.

**Tool Layer** (`src/mcp_openstack_ops/tools/`): Contains asynchronous coroutines decorated with `@mcp.tool()`. Each wrapper calls the corresponding service function, adds a timestamp, logs the operation, and returns a JSON string. The file [`src/mcp_openstack_ops/mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/mcp_main.py) automatically imports all modules under `tools/` at startup, eliminating manual registration boilerplate.

## Step-by-Step Implementation Workflow

Follow this sequence to add a new Octavia management capability, using the implementation of L7 rule management as a reference pattern.

### Step 1: Implement Core Logic in the Service Module

Create or extend the appropriate service module in `src/mcp_openstack_ops/services/load_balancer/`. The function must use `get_openstack_connection()` to ensure project isolation and accept an `action` parameter to distinguish between create, update, and delete operations. This follows the pattern established by existing functions like `set_load_balancer_l7_policy`.

```python

# src/mcp_openstack_ops/services/load_balancer/l7_policies.py

def set_load_balancer_l7_rule(action: str, **kwargs):
    """Create, update, or delete an Octavia L7 rule."""
    conn = get_openstack_connection()
    if action == "create":
        rule = conn.load_balancer.create_l7_rule(
            listener_id=kwargs["listener_id"],
            l7policy_id=kwargs["policy_id"],
            type=kwargs["type"],
            compare_type=kwargs["compare_type"],
            value=kwargs["value"],
        )
        return {"success": True, "rule_id": rule.id, "message": "L7 rule created"}
    # Handle update and delete similarly...

```

Core functions like `get_load_balancer_list` in [`services/load_balancer/core.py`](https://github.com/call518/mcp-openstack-ops/blob/main/services/load_balancer/core.py) (lines 34-38) demonstrate the standard logging pattern using `logger.info` for entry/exit tracking, which aids troubleshooting in large OpenStack deployments.

### Step 2: Export the Service Function

Make the new function available to the tool layer by adding it to the export list in [`src/mcp_openstack_ops/services/load_balancer/__init__.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/services/load_balancer/__init__.py) (lines 37-44).

```python

# src/mcp_openstack_ops/services/load_balancer/__init__.py

from .l7_policies import (
    get_load_balancer_l7_policies,
    set_load_balancer_l7_policy,
    set_load_balancer_l7_rule,  # New export

)

```

This re-export pattern allows tool wrappers to import from the package root while maintaining clean separation of concerns.

### Step 3: Create the MCP Tool Wrapper

Implement a thin asynchronous wrapper in `src/mcp_openstack_ops/tools/`. The wrapper must be decorated with `@mcp.tool()` to enable automatic MCP registration, handle exceptions, and return a timestamped JSON payload. This mirrors the pattern found in [`tools/set_load_balancer_listener.py`](https://github.com/call518/mcp-openstack-ops/blob/main/tools/set_load_balancer_listener.py).

```python

# src/mcp_openstack_ops/tools/set_load_balancer_l7_rule.py

import json
from datetime import datetime
from ..functions import set_load_balancer_l7_rule as _set_load_balancer_l7_rule
from ..mcp_main import logger, mcp

@mcp.tool()
async def set_load_balancer_l7_rule(action: str, **kwargs) -> str:
    """MCP-exposed async wrapper for L7 rule management."""
    try:
        logger.info(f"Octavia L7 rule – action={action} kwargs={kwargs}")
        result = _set_load_balancer_l7_rule(action, **kwargs)
        return json.dumps(
            {
                "timestamp": datetime.now().isoformat(),
                "result": result,
                "success": True
            },
            indent=2,
            ensure_ascii=False,
        )
    except Exception as e:
        err = f"Error: Failed to manage L7 rule – {e}"
        logger.error(err)
        return json.dumps(
            {"timestamp": datetime.now().isoformat(), "error": err, "success": False},
            indent=2,
        )

```

### Step 4: Verify Safety Gates and Environment Controls

The server startup logic 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 45-55) validates the `ALLOW_MODIFY_OPERATIONS` environment variable. Unless this flag is set to `true`, the server runs in read-only mode and blocks mutating SDK calls. Ensure your new tool follows this safety model by relying on the core service functions, which respect these server-wide safety controls.

### Step 5: Test the New Tool via MCP Client

Once the server is running (`uv run python -m mcp_openstack_ops`), invoke the new tool through an MCP client. Listing operations work immediately, while mutating operations require `ALLOW_MODIFY_OPERATIONS=true`.

```bash

# List existing L7 rules for a listener

mcp-client get_load_balancer_l7_rules --lb-name-or-id my-lb --listener-name-or-id listener-1

# Create a new rule (requires ALLOW_MODIFY_OPERATIONS=true)

mcp-client set_load_balancer_l7_rule \
    --action create \
    --listener-id listener-1 \
    --policy-id policy-42 \
    --type PATH \
    --compare_type STARTS_WITH \
    --value /api/v2/

```

The client receives a JSON payload containing the operation result and an ISO timestamp.

### Step 6: Update Documentation

Add the new tool to the load balancer reference table in [`README.md`](https://github.com/call518/mcp-openstack-ops/blob/main/README.md) (section 7. Load Balancer (Octavia)) to maintain the discoverability standard shown in the existing load balancer documentation. Include natural-language usage examples in [`src/mcp_openstack_ops/prompt_template.md`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/prompt_template.md) to help AI assistants generate correct command syntax.

## Summary

- **Implement core logic** in `src/mcp_openstack_ops/services/load_balancer/<module>.py` using synchronous OpenStack SDK calls that return plain dictionaries and respect project isolation via `get_openstack_connection()`.
- **Export the function** in [`src/mcp_openstack_ops/services/load_balancer/__init__.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/services/load_balancer/__init__.py) (lines 37-44) to make it available to the tool layer.
- **Create an async wrapper** in `src/mcp_openstack_ops/tools/` decorated with `@mcp.tool()` that returns timestamped JSON and handles logging, following the pattern in existing tool files.
- **Respect safety gates** by ensuring mutating operations honor the `ALLOW_MODIFY_OPERATIONS` environment variable enforced by [`mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/mcp_main.py) (lines 45-55).
- **Document** the new capability in [`README.md`](https://github.com/call518/mcp-openstack-ops/blob/main/README.md) and [`prompt_template.md`](https://github.com/call518/mcp-openstack-ops/blob/main/prompt_template.md) for discoverability, and add unit tests that mock the OpenStack SDK to validate return dictionary shapes.

## Frequently Asked Questions

### How does the MCP server automatically register new tools?

The [`src/mcp_openstack_ops/mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/src/mcp_openstack_ops/mcp_main.py) file scans and imports all modules under the `tools/` package at startup. Any coroutine decorated with `@mcp.tool()` is automatically registered without requiring manual entry in a registry file or configuration list.

### What safety mechanisms prevent accidental modifications to load balancers?

The server startup logic in [`mcp_main.py`](https://github.com/call518/mcp-openstack-ops/blob/main/mcp_main.py) (lines 45-55) checks for `ALLOW_MODIFY_OPERATIONS=true` in the environment variables. Unless this flag is set, the server runs in read-only mode and blocks mutating SDK calls. Additionally, `get_openstack_connection()` enforces project isolation by binding to a specific `OS_PROJECT_NAME`, preventing cross-tenant operations.

### Why separate the service layer from the tool layer?

This separation allows the synchronous OpenStack SDK logic to remain reusable by non-MCP components such as CLI scripts or monitoring automation, while the thin async wrappers handle MCP-specific concerns like JSON serialization, timestamping, and protocol-compliant error handling. It also keeps the codebase modular and easier to unit test.

### Where should I add tests for a new Octavia management tool?

Create unit tests in the `tests/` directory (if present) or alongside existing test files, mocking the OpenStack SDK to validate the shape of returned dictionaries. Test both the core service function (verifying SDK call parameters) and the tool wrapper (verifying JSON output structure and error handling).