Dynamic Tool Loading System Architecture in mcp-airflow-api

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, 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.


# 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 or 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
  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.


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


# 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, 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.


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


# Target Airflow 2.x (v1 API)

export AIRFLOW_API_VERSION=v1
python -m mcp_airflow_api

# 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 reads AIRFLOW_API_VERSION to determine the target API (v1 or v2).
  • Pluggable registration: v1_tools.py and 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 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, 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 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 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.

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 →