# Dynamic Tool Loading System Architecture in mcp-airflow-api

> Explore the dynamic tool loading system architecture in mcp-airflow-api. Learn how it supports Airflow 2x and 3x with version-specific handlers from a single codebase.

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

---

**The `mcp-airflow-api` repository implements a runtime dynamic tool loading system that detects the target Airflow API version from environment variables and injects version-specific request handlers into a shared core of 43 tools, enabling a single codebase to serve both Airflow 2.x and 3.x deployments.**

The `call518/mcp-airflow-api` project provides a Model Context Protocol (MCP) server for Apache Airflow, designed to operate across major Airflow versions without maintaining separate codebases. At the heart of this flexibility lies a **dynamic tool loading system** that adapts the server's toolset at startup based on the detected Airflow API version, swapping in the correct request handling logic while reusing a common implementation layer.

## Three-Layer Architecture Overview

The dynamic tool loading system consists of three coordinated layers that execute sequentially during server initialization. This architecture separates version detection from tool registration, allowing the system to remain extensible for future Airflow API versions.

### Version Detection Layer

The loading process begins in [`src/mcp_airflow_api/functions.py`](https://github.com/call518/mcp-airflow-api/blob/main/src/mcp_airflow_api/functions.py), where the `get_api_version()` function reads the **`AIRFLOW_API_VERSION`** environment variable (defaulting to `v1`) and normalizes it to lowercase. This simple string value—either `v1` for Airflow 2.x compatibility or `v2` for Airflow 3.x—drives the entire downstream registration process.

```python

# From functions.py

def get_api_version() -> str:
    return os.getenv("AIRFLOW_API_VERSION", "v1").lower()

```

### Version-Specific Registration Modules

Based on the detected version, the system loads one of two registration modules located in [`src/mcp_airflow_api/tools/v1_tools.py`](https://github.com/call518/mcp-airflow-api/blob/main/src/mcp_airflow_api/tools/v1_tools.py) or [`src/mcp_airflow_api/tools/v2_tools.py`](https://github.com/call518/mcp-airflow-api/blob/main/src/mcp_airflow_api/tools/v2_tools.py). Each module exports a `register_tools(mcp)` function that performs three critical operations:

1. **Import shared implementations** from [`common_tools.py`](https://github.com/call518/mcp-airflow-api/blob/main/common_tools.py)
2. **Inject the version-specific request helper** by assigning either `airflow_request_v1` or `airflow_request_v2` to `common_tools.airflow_request`
3. **Register the core toolset** by calling `common_tools.register_common_tools(mcp)`, which adds 43 standard tools to the MCP instance

The v2 module performs an additional step: it registers two extra asset-management tools that exist only in the Airflow 3.x API surface.

```python

# Conceptual flow inside v2_tools.py

import common_tools
from functions import airflow_request_v2

def register_tools(mcp):
    common_tools.airflow_request = airflow_request_v2  # Injection

    common_tools.register_common_tools(mcp)             # 43 core tools

    
    # Add v2-specific tools

    @mcp.tool()
    async def list_assets(...): ...

```

### Bootstrap Orchestration

The entry point in [`src/mcp_airflow_api/mcp_main.py`](https://github.com/call518/mcp-airflow-api/blob/main/src/mcp_airflow_api/mcp_main.py) contains the `create_mcp_server()` function, which orchestrates the dynamic loading. This function instantiates a `FastMCP` object, queries `get_api_version()`, imports the appropriate registration module, and executes its `register_tools` method. The returned server instance contains a toolset that exactly matches the target Airflow API version.

```python

# From mcp_main.py

def create_mcp_server() -> FastMCP:
    api_version = get_api_version()
    mcp_instance = FastMCP("mcp-airflow-api")
    
    if api_version == "v1":
        from mcp_airflow_api.tools import v1_tools
        v1_tools.register_tools(mcp_instance)
    elif api_version == "v2":
        from mcp_airflow_api.tools import v2_tools
        v2_tools.register_tools(mcp_instance)
    else:
        raise ValueError("Unsupported API version")
    
    return mcp_instance

```

## The Injection Mechanism

The architecture uses a **dependency injection pattern** to avoid code duplication across API versions. Instead of passing request helpers as arguments to every tool function, the system employs module-level attribute assignment.

In [`src/mcp_airflow_api/tools/common_tools.py`](https://github.com/call518/mcp-airflow-api/blob/main/src/mcp_airflow_api/tools/common_tools.py), a placeholder `airflow_request` variable is defined at module scope. During registration, the version-specific module overwrites this variable with the appropriate implementation. When the 43 core tools execute, they reference `common_tools.airflow_request`, which now points to the correct versioned function.

```python

# Inside common_tools.py

airflow_request = None  # Injected at registration time

@mcp.tool()
async def list_dags(...):
    # Uses the version-specific function that was injected

    resp = await airflow_request("GET", "/dags")
    return resp

```

This approach allows [`common_tools.py`](https://github.com/call518/mcp-airflow-api/blob/main/common_tools.py) to remain completely agnostic of HTTP implementation details, while the version-specific modules handle the peculiarities of Airflow 2.x versus 3.x REST endpoints.

## Configuration and Deployment

To activate the dynamic tool loading system for a specific environment, set the environment variable before invoking the server:

```bash

# Target Airflow 2.x (v1 API)

export AIRFLOW_API_VERSION=v1
python -m mcp_airflow_api

```

```bash

# Target Airflow 3.x (v2 API)

export AIRFLOW_API_VERSION=v2
python -m mcp_airflow_api

```

The system validates the version string during bootstrap and raises a `ValueError` for unsupported values, preventing silent misconfiguration.

## Summary

- **Environment-driven detection**: The `get_api_version()` function in [`functions.py`](https://github.com/call518/mcp-airflow-api/blob/main/functions.py) reads `AIRFLOW_API_VERSION` to determine the target API (v1 or v2).
- **Pluggable registration**: [`v1_tools.py`](https://github.com/call518/mcp-airflow-api/blob/main/v1_tools.py) and [`v2_tools.py`](https://github.com/call518/mcp-airflow-api/blob/main/v2_tools.py) each provide a `register_tools()` function that injects the correct request handler and registers version-appropriate tools.
- **Shared core**: [`common_tools.py`](https://github.com/call518/mcp-airflow-api/blob/main/common_tools.py) contains 43 tool implementations that rely on the injected `airflow_request` variable, eliminating code duplication.
- **Startup binding**: Tool selection happens once during `create_mcp_server()` execution in [`mcp_main.py`](https://github.com/call518/mcp-airflow-api/blob/main/mcp_main.py), resulting in a static but version-correct toolset for the server's lifetime.

## Frequently Asked Questions

### How does the dynamic tool loading system determine which Airflow version to target?

The system checks the **`AIRFLOW_API_VERSION`** environment variable via `functions.get_api_version()`. Valid values are `v1` (default) for Airflow 2.x compatibility and `v2` for Airflow 3.x. The value is normalized to lowercase and used to select the appropriate registration module during server initialization.

### What happens to the core tools when the API version changes?

The 43 core tools defined in [`common_tools.py`](https://github.com/call518/mcp-airflow-api/blob/main/common_tools.py) remain unchanged. The **dependency injection** mechanism swaps only the `airflow_request` function reference. When `v1_tools.register_tools()` runs, it assigns the v1 request helper; when `v2_tools.register_tools()` runs, it assigns the v2 helper. The tools themselves call the injected function without knowing which version is active.

### Can I add tools that exist only in one Airflow version?

Yes. Place version-specific tool definitions in the respective registration module. For Airflow 3.x exclusives, add the tool decorator and function to [`src/mcp_airflow_api/tools/v2_tools.py`](https://github.com/call518/mcp-airflow-api/blob/main/src/mcp_airflow_api/tools/v2_tools.py) after the call to `register_common_tools()`. The v1 module will not load these tools, keeping the v1 toolset clean and preventing errors against Airflow 2.x endpoints.

### Is the tool selection hot-swappable without restarting the server?

No. The dynamic tool loading system executes **once at startup** inside `create_mcp_server()`. After the `FastMCP` instance is configured and returned, the toolset is fixed for that process lifetime. To change API versions, you must restart the server with a different `AIRFLOW_API_VERSION` value.